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;