A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
1import React from "react"; 2import type { ColorSchemePreference } from "../hooks/useColorScheme"; 3 4/** 5 * Props for the `ColorSchemeToggle` segmented control. 6 */ 7export interface ColorSchemeToggleProps { 8 /** 9 * Current color scheme preference selection. 10 */ 11 value: ColorSchemePreference; 12 /** 13 * Change handler invoked when the user selects a new scheme. 14 */ 15 onChange: (value: ColorSchemePreference) => void; 16 /** 17 * Theme used to style the control itself; defaults to `'light'`. 18 */ 19 scheme?: "light" | "dark"; 20} 21 22const options: Array<{ 23 label: string; 24 value: ColorSchemePreference; 25 description: string; 26}> = [ 27 { label: "System", value: "system", description: "Follow OS preference" }, 28 { label: "Light", value: "light", description: "Always light mode" }, 29 { label: "Dark", value: "dark", description: "Always dark mode" }, 30]; 31 32/** 33 * A button group that lets users choose between light, dark, or system color modes. 34 * 35 * @param value - Current scheme selection displayed as active. 36 * @param onChange - Callback fired when a new option is selected. 37 * @param scheme - Theme used to style the control itself. Defaults to `'light'`. 38 * @returns A fully keyboard-accessible toggle rendered as a radio group. 39 */ 40export const ColorSchemeToggle: React.FC<ColorSchemeToggleProps> = ({ 41 value, 42 onChange, 43 scheme = "light", 44}) => { 45 const palette = scheme === "dark" ? darkTheme : lightTheme; 46 47 return ( 48 <div 49 aria-label="Color scheme" 50 role="radiogroup" 51 style={{ ...containerStyle, ...palette.container }} 52 > 53 {options.map((option) => { 54 const isActive = option.value === value; 55 const activeStyles = isActive ? palette.active : undefined; 56 return ( 57 <button 58 key={option.value} 59 role="radio" 60 aria-checked={isActive} 61 type="button" 62 onClick={() => onChange(option.value)} 63 style={{ 64 ...buttonStyle, 65 ...palette.button, 66 ...(activeStyles ?? {}), 67 }} 68 title={option.description} 69 > 70 {option.label} 71 </button> 72 ); 73 })} 74 </div> 75 ); 76}; 77 78const containerStyle: React.CSSProperties = { 79 display: "inline-flex", 80 borderRadius: 999, 81 padding: 4, 82 gap: 4, 83 border: "1px solid transparent", 84 background: "#f8fafc", 85}; 86 87const buttonStyle: React.CSSProperties = { 88 border: "1px solid transparent", 89 borderRadius: 999, 90 padding: "4px 12px", 91 fontSize: 12, 92 fontWeight: 500, 93 cursor: "pointer", 94 background: "transparent", 95 transition: 96 "background-color 160ms ease, border-color 160ms ease, color 160ms ease", 97}; 98 99const lightTheme = { 100 container: { 101 borderColor: "#e2e8f0", 102 background: "rgba(241, 245, 249, 0.8)", 103 }, 104 button: { 105 color: "#334155", 106 }, 107 active: { 108 background: "#2563eb", 109 borderColor: "#2563eb", 110 color: "#f8fafc", 111 }, 112} satisfies Record<string, React.CSSProperties>; 113 114const darkTheme = { 115 container: { 116 borderColor: "#2e3540ff", 117 background: "rgba(30, 38, 49, 0.6)", 118 }, 119 button: { 120 color: "#e2e8f0", 121 }, 122 active: { 123 background: "#38bdf8", 124 borderColor: "#38bdf8", 125 color: "#020617", 126 }, 127} satisfies Record<string, React.CSSProperties>; 128 129export default ColorSchemeToggle;