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;