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