@justin_evo/evo-ui 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +70 -70
  3. package/dist/declarations.d.ts +6 -6
  4. package/package.json +52 -52
  5. package/src/Alert/Alert.tsx +49 -49
  6. package/src/AutoComplete/AutoComplete.tsx +810 -810
  7. package/src/Badge/Badge.tsx +53 -53
  8. package/src/Breadcrumb/Breadcrumb.tsx +53 -53
  9. package/src/Button/Button.tsx +125 -125
  10. package/src/Card/Card.tsx +257 -257
  11. package/src/Checkbox/Checkbox.tsx +59 -59
  12. package/src/CommandPalette/CommandPalette.tsx +185 -185
  13. package/src/Container/Container.tsx +31 -31
  14. package/src/Divider/Divider.tsx +31 -31
  15. package/src/Form/Form.tsx +185 -185
  16. package/src/Grid/Grid.tsx +66 -66
  17. package/src/ImageCropper/ImageCropper.tsx +911 -911
  18. package/src/Input/Input.tsx +74 -74
  19. package/src/Modal/Modal.tsx +77 -77
  20. package/src/Nav/Nav.tsx +708 -708
  21. package/src/Notification/Notification.tsx +1503 -1503
  22. package/src/Pagination/Pagination.tsx +76 -76
  23. package/src/Radio/Radio.tsx +69 -69
  24. package/src/RichTextArea/RichTextArea.tsx +886 -886
  25. package/src/Select/Select.tsx +515 -515
  26. package/src/Skeleton/Skeleton.tsx +70 -70
  27. package/src/Stack/Stack.tsx +52 -52
  28. package/src/Table/Table.tsx +335 -335
  29. package/src/Tabs/Tabs.tsx +90 -90
  30. package/src/Theme/ThemeProvider.tsx +253 -253
  31. package/src/Theme/ThemeToggle.tsx +79 -79
  32. package/src/Toggle/Toggle.tsx +48 -48
  33. package/src/Tooltip/Tooltip.tsx +38 -38
  34. package/src/TopNav/TopNav.tsx +1163 -1163
  35. package/src/TreeSelect/TreeSelect.tsx +825 -825
  36. package/src/css/alert.module.scss +93 -93
  37. package/src/css/autocomplete.module.scss +416 -416
  38. package/src/css/badge.module.scss +82 -82
  39. package/src/css/base/_color.scss +159 -159
  40. package/src/css/base/_theme.scss +237 -237
  41. package/src/css/base/_variables.scss +161 -161
  42. package/src/css/breadcrumb.module.scss +50 -50
  43. package/src/css/button.module.scss +385 -385
  44. package/src/css/card.module.scss +217 -217
  45. package/src/css/checkbox.module.scss +123 -123
  46. package/src/css/commandpalette.module.scss +211 -211
  47. package/src/css/container.module.scss +18 -18
  48. package/src/css/divider.module.scss +41 -41
  49. package/src/css/form.module.scss +245 -245
  50. package/src/css/imagecropper.module.scss +397 -397
  51. package/src/css/input.module.scss +89 -89
  52. package/src/css/modal.module.scss +105 -105
  53. package/src/css/nav.module.scss +494 -494
  54. package/src/css/notification.module.scss +691 -691
  55. package/src/css/pagination.module.scss +63 -63
  56. package/src/css/radio.module.scss +89 -89
  57. package/src/css/richtextarea.module.scss +307 -307
  58. package/src/css/select.module.scss +525 -525
  59. package/src/css/skeleton.module.scss +30 -30
  60. package/src/css/table.module.scss +386 -386
  61. package/src/css/tabs.module.scss +63 -63
  62. package/src/css/theme-toggle.module.scss +83 -83
  63. package/src/css/toggle.module.scss +54 -54
  64. package/src/css/tooltip.module.scss +97 -97
  65. package/src/css/topnav.module.scss +568 -568
  66. package/src/css/treeselect.module.scss +558 -558
  67. package/src/css/utilities/_borders.scss +111 -111
  68. package/src/css/utilities/_colors.scss +66 -66
  69. package/src/css/utilities/_effects.scss +216 -216
  70. package/src/css/utilities/_layout.scss +181 -181
  71. package/src/css/utilities/_position.scss +75 -75
  72. package/src/css/utilities/_sizing.scss +138 -138
  73. package/src/css/utilities/_spacing.scss +99 -99
  74. package/src/css/utilities/_typography.scss +121 -121
  75. package/src/css/utilities/index.scss +24 -24
  76. package/src/declarations.d.ts +6 -6
  77. package/src/index.ts +60 -60
@@ -1,253 +1,253 @@
1
- import {
2
- createContext,
3
- useCallback,
4
- useContext,
5
- useEffect,
6
- useMemo,
7
- useRef,
8
- useState,
9
- type ReactNode,
10
- } from 'react';
11
-
12
- /**
13
- * The three theme modes EvoUI supports.
14
- * - `'light'` / `'dark'` — force the theme regardless of OS preference.
15
- * - `'system'` — follow the user's OS-level color-scheme preference.
16
- */
17
- export type EvoTheme = 'light' | 'dark' | 'system';
18
-
19
- /**
20
- * The actually-applied theme after resolving `'system'` against
21
- * `window.matchMedia('(prefers-color-scheme: dark)')`. Always either
22
- * `'light'` or `'dark'` — never `'system'`.
23
- */
24
- export type EvoResolvedTheme = 'light' | 'dark';
25
-
26
- export interface EvoThemeContextValue {
27
- /** The user-selected mode (may be `'system'`). */
28
- theme: EvoTheme;
29
- /** The mode that is actually painted right now (`'light'` or `'dark'`). */
30
- resolvedTheme: EvoResolvedTheme;
31
- /** Switch to a specific mode. */
32
- setTheme: (theme: EvoTheme) => void;
33
- /** Convenience: flip between light and dark (treats `'system'` as its resolved value). */
34
- toggleTheme: () => void;
35
- }
36
-
37
- const ThemeContext = createContext<EvoThemeContextValue | null>(null);
38
-
39
- export interface EvoThemeProviderProps {
40
- /** Subtree to provide the theme to. */
41
- children: ReactNode;
42
- /**
43
- * Initial theme used before any persisted value is read.
44
- * @default 'system'
45
- */
46
- defaultTheme?: EvoTheme;
47
- /**
48
- * localStorage key used to persist the user's choice across reloads.
49
- * Pass `null` to disable persistence entirely.
50
- * @default 'evo-ui-theme'
51
- */
52
- storageKey?: string | null;
53
- /**
54
- * HTML attribute written to the document root. Most apps want
55
- * `'data-theme'`; pass `'class'` to instead toggle `light` / `dark`
56
- * as className (useful if you're sharing tokens with Tailwind).
57
- * @default 'data-theme'
58
- */
59
- attribute?: 'data-theme' | 'class';
60
- /**
61
- * Animate color transitions when the theme flips.
62
- * Automatically disabled for users with `prefers-reduced-motion`.
63
- * @default true
64
- */
65
- enableTransitions?: boolean;
66
- /**
67
- * Element to apply the theme attribute to.
68
- * @default document.documentElement
69
- */
70
- target?: HTMLElement;
71
- }
72
-
73
- const STORAGE_KEY_DEFAULT = 'evo-ui-theme';
74
-
75
- function getSystemTheme(): EvoResolvedTheme {
76
- if (typeof window === 'undefined' || !window.matchMedia) return 'light';
77
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
78
- }
79
-
80
- function readStoredTheme(key: string | null): EvoTheme | null {
81
- if (!key || typeof window === 'undefined') return null;
82
- try {
83
- const value = window.localStorage.getItem(key);
84
- if (value === 'light' || value === 'dark' || value === 'system') return value;
85
- } catch {
86
- // localStorage can throw in private mode / sandboxed iframes.
87
- }
88
- return null;
89
- }
90
-
91
- /**
92
- * Provides EvoUI theming context to descendant components.
93
- *
94
- * @example
95
- * ```tsx
96
- * import { EvoThemeProvider } from '@justin_evo/evo-ui';
97
- *
98
- * <EvoThemeProvider defaultTheme="system">
99
- * <App />
100
- * </EvoThemeProvider>
101
- * ```
102
- */
103
- export const EvoThemeProvider = ({
104
- children,
105
- defaultTheme = 'system',
106
- storageKey = STORAGE_KEY_DEFAULT,
107
- attribute = 'data-theme',
108
- enableTransitions = true,
109
- target,
110
- }: EvoThemeProviderProps) => {
111
- const [theme, setThemeState] = useState<EvoTheme>(() => {
112
- return readStoredTheme(storageKey) ?? defaultTheme;
113
- });
114
-
115
- const [resolvedTheme, setResolvedTheme] = useState<EvoResolvedTheme>(() => {
116
- const initial = readStoredTheme(storageKey) ?? defaultTheme;
117
- return initial === 'system' ? getSystemTheme() : initial;
118
- });
119
-
120
- const isFirstApply = useRef(true);
121
-
122
- const applyToDOM = useCallback(
123
- (resolved: EvoResolvedTheme) => {
124
- if (typeof document === 'undefined') return;
125
- const el = target ?? document.documentElement;
126
-
127
- // Enable transitions only AFTER the first paint, so the page
128
- // doesn't fade in from the wrong colors on initial load.
129
- if (enableTransitions && !isFirstApply.current) {
130
- el.setAttribute('data-theme-transition', 'true');
131
- window.clearTimeout((el as any).__evoThemeTimer);
132
- (el as any).__evoThemeTimer = window.setTimeout(() => {
133
- el.removeAttribute('data-theme-transition');
134
- }, 250);
135
- }
136
-
137
- if (attribute === 'class') {
138
- el.classList.remove('light', 'dark');
139
- el.classList.add(resolved);
140
- // Always set data-theme too so our CSS variables resolve.
141
- el.setAttribute('data-theme', resolved);
142
- } else {
143
- el.setAttribute('data-theme', resolved);
144
- }
145
-
146
- isFirstApply.current = false;
147
- },
148
- [attribute, enableTransitions, target],
149
- );
150
-
151
- // Apply theme to DOM whenever resolvedTheme changes.
152
- useEffect(() => {
153
- applyToDOM(resolvedTheme);
154
- }, [resolvedTheme, applyToDOM]);
155
-
156
- // Recompute resolvedTheme when theme changes.
157
- useEffect(() => {
158
- setResolvedTheme(theme === 'system' ? getSystemTheme() : theme);
159
- }, [theme]);
160
-
161
- // When the user is on 'system', listen for OS-level changes.
162
- useEffect(() => {
163
- if (theme !== 'system' || typeof window === 'undefined' || !window.matchMedia) return;
164
- const mql = window.matchMedia('(prefers-color-scheme: dark)');
165
- const handler = (e: MediaQueryListEvent) => {
166
- setResolvedTheme(e.matches ? 'dark' : 'light');
167
- };
168
- mql.addEventListener('change', handler);
169
- return () => mql.removeEventListener('change', handler);
170
- }, [theme]);
171
-
172
- const setTheme = useCallback(
173
- (next: EvoTheme) => {
174
- setThemeState(next);
175
- if (storageKey && typeof window !== 'undefined') {
176
- try {
177
- window.localStorage.setItem(storageKey, next);
178
- } catch {
179
- // Storage might be unavailable — fail silently.
180
- }
181
- }
182
- },
183
- [storageKey],
184
- );
185
-
186
- const toggleTheme = useCallback(() => {
187
- setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
188
- }, [resolvedTheme, setTheme]);
189
-
190
- const value = useMemo<EvoThemeContextValue>(
191
- () => ({ theme, resolvedTheme, setTheme, toggleTheme }),
192
- [theme, resolvedTheme, setTheme, toggleTheme],
193
- );
194
-
195
- return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
196
- };
197
-
198
- /**
199
- * Read and update the EvoUI theme.
200
- *
201
- * Must be called from a descendant of `<EvoThemeProvider>`. If used
202
- * outside the provider, returns a no-op object with `resolvedTheme`
203
- * set to whatever is currently on `document.documentElement`.
204
- *
205
- * @example
206
- * ```tsx
207
- * const { resolvedTheme, setTheme, toggleTheme } = useEvoTheme();
208
- * <button onClick={toggleTheme}>
209
- * {resolvedTheme === 'dark' ? '☀️' : '🌙'}
210
- * </button>
211
- * ```
212
- */
213
- export const useEvoTheme = (): EvoThemeContextValue => {
214
- const ctx = useContext(ThemeContext);
215
- if (ctx) return ctx;
216
-
217
- // Graceful fallback if hook is called without a provider —
218
- // common in standalone widgets where the host app sets theme manually.
219
- const fallbackResolved: EvoResolvedTheme =
220
- typeof document !== 'undefined' &&
221
- document.documentElement.getAttribute('data-theme') === 'dark'
222
- ? 'dark'
223
- : 'light';
224
-
225
- return {
226
- theme: fallbackResolved,
227
- resolvedTheme: fallbackResolved,
228
- setTheme: () => {
229
- if (typeof document !== 'undefined') {
230
- // eslint-disable-next-line no-console
231
- console.warn(
232
- '[evo-ui] useEvoTheme called without <EvoThemeProvider>. ' +
233
- 'Wrap your app in <EvoThemeProvider> to enable setTheme().',
234
- );
235
- }
236
- },
237
- toggleTheme: () => {},
238
- };
239
- };
240
-
241
- /**
242
- * Inline script that applies the persisted theme before React hydrates,
243
- * preventing the white-flash on first paint in dark mode. Drop into your
244
- * `<head>` (or Next.js `<Script strategy="beforeInteractive">`):
245
- *
246
- * @example
247
- * ```html
248
- * <script dangerouslySetInnerHTML={{ __html: getEvoThemeScript() }} />
249
- * ```
250
- */
251
- export const getEvoThemeScript = (storageKey: string = STORAGE_KEY_DEFAULT): string => {
252
- return `(function(){try{var s=localStorage.getItem('${storageKey}');var t=s||'system';var r=t==='system'?(matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'):t;document.documentElement.setAttribute('data-theme',r);}catch(e){}})();`;
253
- };
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type ReactNode,
10
+ } from 'react';
11
+
12
+ /**
13
+ * The three theme modes EvoUI supports.
14
+ * - `'light'` / `'dark'` — force the theme regardless of OS preference.
15
+ * - `'system'` — follow the user's OS-level color-scheme preference.
16
+ */
17
+ export type EvoTheme = 'light' | 'dark' | 'system';
18
+
19
+ /**
20
+ * The actually-applied theme after resolving `'system'` against
21
+ * `window.matchMedia('(prefers-color-scheme: dark)')`. Always either
22
+ * `'light'` or `'dark'` — never `'system'`.
23
+ */
24
+ export type EvoResolvedTheme = 'light' | 'dark';
25
+
26
+ export interface EvoThemeContextValue {
27
+ /** The user-selected mode (may be `'system'`). */
28
+ theme: EvoTheme;
29
+ /** The mode that is actually painted right now (`'light'` or `'dark'`). */
30
+ resolvedTheme: EvoResolvedTheme;
31
+ /** Switch to a specific mode. */
32
+ setTheme: (theme: EvoTheme) => void;
33
+ /** Convenience: flip between light and dark (treats `'system'` as its resolved value). */
34
+ toggleTheme: () => void;
35
+ }
36
+
37
+ const ThemeContext = createContext<EvoThemeContextValue | null>(null);
38
+
39
+ export interface EvoThemeProviderProps {
40
+ /** Subtree to provide the theme to. */
41
+ children: ReactNode;
42
+ /**
43
+ * Initial theme used before any persisted value is read.
44
+ * @default 'system'
45
+ */
46
+ defaultTheme?: EvoTheme;
47
+ /**
48
+ * localStorage key used to persist the user's choice across reloads.
49
+ * Pass `null` to disable persistence entirely.
50
+ * @default 'evo-ui-theme'
51
+ */
52
+ storageKey?: string | null;
53
+ /**
54
+ * HTML attribute written to the document root. Most apps want
55
+ * `'data-theme'`; pass `'class'` to instead toggle `light` / `dark`
56
+ * as className (useful if you're sharing tokens with Tailwind).
57
+ * @default 'data-theme'
58
+ */
59
+ attribute?: 'data-theme' | 'class';
60
+ /**
61
+ * Animate color transitions when the theme flips.
62
+ * Automatically disabled for users with `prefers-reduced-motion`.
63
+ * @default true
64
+ */
65
+ enableTransitions?: boolean;
66
+ /**
67
+ * Element to apply the theme attribute to.
68
+ * @default document.documentElement
69
+ */
70
+ target?: HTMLElement;
71
+ }
72
+
73
+ const STORAGE_KEY_DEFAULT = 'evo-ui-theme';
74
+
75
+ function getSystemTheme(): EvoResolvedTheme {
76
+ if (typeof window === 'undefined' || !window.matchMedia) return 'light';
77
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
78
+ }
79
+
80
+ function readStoredTheme(key: string | null): EvoTheme | null {
81
+ if (!key || typeof window === 'undefined') return null;
82
+ try {
83
+ const value = window.localStorage.getItem(key);
84
+ if (value === 'light' || value === 'dark' || value === 'system') return value;
85
+ } catch {
86
+ // localStorage can throw in private mode / sandboxed iframes.
87
+ }
88
+ return null;
89
+ }
90
+
91
+ /**
92
+ * Provides EvoUI theming context to descendant components.
93
+ *
94
+ * @example
95
+ * ```tsx
96
+ * import { EvoThemeProvider } from '@justin_evo/evo-ui';
97
+ *
98
+ * <EvoThemeProvider defaultTheme="system">
99
+ * <App />
100
+ * </EvoThemeProvider>
101
+ * ```
102
+ */
103
+ export const EvoThemeProvider = ({
104
+ children,
105
+ defaultTheme = 'system',
106
+ storageKey = STORAGE_KEY_DEFAULT,
107
+ attribute = 'data-theme',
108
+ enableTransitions = true,
109
+ target,
110
+ }: EvoThemeProviderProps) => {
111
+ const [theme, setThemeState] = useState<EvoTheme>(() => {
112
+ return readStoredTheme(storageKey) ?? defaultTheme;
113
+ });
114
+
115
+ const [resolvedTheme, setResolvedTheme] = useState<EvoResolvedTheme>(() => {
116
+ const initial = readStoredTheme(storageKey) ?? defaultTheme;
117
+ return initial === 'system' ? getSystemTheme() : initial;
118
+ });
119
+
120
+ const isFirstApply = useRef(true);
121
+
122
+ const applyToDOM = useCallback(
123
+ (resolved: EvoResolvedTheme) => {
124
+ if (typeof document === 'undefined') return;
125
+ const el = target ?? document.documentElement;
126
+
127
+ // Enable transitions only AFTER the first paint, so the page
128
+ // doesn't fade in from the wrong colors on initial load.
129
+ if (enableTransitions && !isFirstApply.current) {
130
+ el.setAttribute('data-theme-transition', 'true');
131
+ window.clearTimeout((el as any).__evoThemeTimer);
132
+ (el as any).__evoThemeTimer = window.setTimeout(() => {
133
+ el.removeAttribute('data-theme-transition');
134
+ }, 250);
135
+ }
136
+
137
+ if (attribute === 'class') {
138
+ el.classList.remove('light', 'dark');
139
+ el.classList.add(resolved);
140
+ // Always set data-theme too so our CSS variables resolve.
141
+ el.setAttribute('data-theme', resolved);
142
+ } else {
143
+ el.setAttribute('data-theme', resolved);
144
+ }
145
+
146
+ isFirstApply.current = false;
147
+ },
148
+ [attribute, enableTransitions, target],
149
+ );
150
+
151
+ // Apply theme to DOM whenever resolvedTheme changes.
152
+ useEffect(() => {
153
+ applyToDOM(resolvedTheme);
154
+ }, [resolvedTheme, applyToDOM]);
155
+
156
+ // Recompute resolvedTheme when theme changes.
157
+ useEffect(() => {
158
+ setResolvedTheme(theme === 'system' ? getSystemTheme() : theme);
159
+ }, [theme]);
160
+
161
+ // When the user is on 'system', listen for OS-level changes.
162
+ useEffect(() => {
163
+ if (theme !== 'system' || typeof window === 'undefined' || !window.matchMedia) return;
164
+ const mql = window.matchMedia('(prefers-color-scheme: dark)');
165
+ const handler = (e: MediaQueryListEvent) => {
166
+ setResolvedTheme(e.matches ? 'dark' : 'light');
167
+ };
168
+ mql.addEventListener('change', handler);
169
+ return () => mql.removeEventListener('change', handler);
170
+ }, [theme]);
171
+
172
+ const setTheme = useCallback(
173
+ (next: EvoTheme) => {
174
+ setThemeState(next);
175
+ if (storageKey && typeof window !== 'undefined') {
176
+ try {
177
+ window.localStorage.setItem(storageKey, next);
178
+ } catch {
179
+ // Storage might be unavailable — fail silently.
180
+ }
181
+ }
182
+ },
183
+ [storageKey],
184
+ );
185
+
186
+ const toggleTheme = useCallback(() => {
187
+ setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
188
+ }, [resolvedTheme, setTheme]);
189
+
190
+ const value = useMemo<EvoThemeContextValue>(
191
+ () => ({ theme, resolvedTheme, setTheme, toggleTheme }),
192
+ [theme, resolvedTheme, setTheme, toggleTheme],
193
+ );
194
+
195
+ return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
196
+ };
197
+
198
+ /**
199
+ * Read and update the EvoUI theme.
200
+ *
201
+ * Must be called from a descendant of `<EvoThemeProvider>`. If used
202
+ * outside the provider, returns a no-op object with `resolvedTheme`
203
+ * set to whatever is currently on `document.documentElement`.
204
+ *
205
+ * @example
206
+ * ```tsx
207
+ * const { resolvedTheme, setTheme, toggleTheme } = useEvoTheme();
208
+ * <button onClick={toggleTheme}>
209
+ * {resolvedTheme === 'dark' ? '☀️' : '🌙'}
210
+ * </button>
211
+ * ```
212
+ */
213
+ export const useEvoTheme = (): EvoThemeContextValue => {
214
+ const ctx = useContext(ThemeContext);
215
+ if (ctx) return ctx;
216
+
217
+ // Graceful fallback if hook is called without a provider —
218
+ // common in standalone widgets where the host app sets theme manually.
219
+ const fallbackResolved: EvoResolvedTheme =
220
+ typeof document !== 'undefined' &&
221
+ document.documentElement.getAttribute('data-theme') === 'dark'
222
+ ? 'dark'
223
+ : 'light';
224
+
225
+ return {
226
+ theme: fallbackResolved,
227
+ resolvedTheme: fallbackResolved,
228
+ setTheme: () => {
229
+ if (typeof document !== 'undefined') {
230
+ // eslint-disable-next-line no-console
231
+ console.warn(
232
+ '[evo-ui] useEvoTheme called without <EvoThemeProvider>. ' +
233
+ 'Wrap your app in <EvoThemeProvider> to enable setTheme().',
234
+ );
235
+ }
236
+ },
237
+ toggleTheme: () => {},
238
+ };
239
+ };
240
+
241
+ /**
242
+ * Inline script that applies the persisted theme before React hydrates,
243
+ * preventing the white-flash on first paint in dark mode. Drop into your
244
+ * `<head>` (or Next.js `<Script strategy="beforeInteractive">`):
245
+ *
246
+ * @example
247
+ * ```html
248
+ * <script dangerouslySetInnerHTML={{ __html: getEvoThemeScript() }} />
249
+ * ```
250
+ */
251
+ export const getEvoThemeScript = (storageKey: string = STORAGE_KEY_DEFAULT): string => {
252
+ return `(function(){try{var s=localStorage.getItem('${storageKey}');var t=s||'system';var r=t==='system'?(matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'):t;document.documentElement.setAttribute('data-theme',r);}catch(e){}})();`;
253
+ };
@@ -1,79 +1,79 @@
1
- import { useEvoTheme } from './ThemeProvider';
2
- import styles from '../css/theme-toggle.module.scss';
3
-
4
- export interface EvoThemeToggleProps {
5
- /** Visual size of the toggle. @default 'md' */
6
- size?: 'sm' | 'md' | 'lg';
7
- /** Accessible label. @default 'Toggle color theme' */
8
- ariaLabel?: string;
9
- /** Extra className to merge with the built-in styles. */
10
- className?: string;
11
- }
12
-
13
- /**
14
- * A drop-in button that flips between light and dark mode.
15
- *
16
- * Sits inside an `<EvoThemeProvider>`. The icon and animation
17
- * automatically reflect the resolved theme.
18
- *
19
- * @example
20
- * ```tsx
21
- * <EvoThemeProvider>
22
- * <EvoThemeToggle />
23
- * </EvoThemeProvider>
24
- * ```
25
- */
26
- export const EvoThemeToggle = ({
27
- size = 'md',
28
- ariaLabel = 'Toggle color theme',
29
- className,
30
- }: EvoThemeToggleProps) => {
31
- const { resolvedTheme, toggleTheme } = useEvoTheme();
32
- const isDark = resolvedTheme === 'dark';
33
-
34
- const classes = [styles.toggle, styles[size], className].filter(Boolean).join(' ');
35
-
36
- return (
37
- <button
38
- type="button"
39
- role="switch"
40
- aria-checked={isDark}
41
- aria-label={ariaLabel}
42
- onClick={toggleTheme}
43
- className={classes}
44
- data-theme-state={resolvedTheme}
45
- >
46
- <span className={styles.track}>
47
- <span className={styles.thumb}>
48
- {/* Sun icon */}
49
- <svg
50
- className={styles.sun}
51
- viewBox="0 0 24 24"
52
- fill="none"
53
- stroke="currentColor"
54
- strokeWidth="2"
55
- strokeLinecap="round"
56
- strokeLinejoin="round"
57
- aria-hidden="true"
58
- >
59
- <circle cx="12" cy="12" r="4" />
60
- <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
61
- </svg>
62
- {/* Moon icon */}
63
- <svg
64
- className={styles.moon}
65
- viewBox="0 0 24 24"
66
- fill="none"
67
- stroke="currentColor"
68
- strokeWidth="2"
69
- strokeLinecap="round"
70
- strokeLinejoin="round"
71
- aria-hidden="true"
72
- >
73
- <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
74
- </svg>
75
- </span>
76
- </span>
77
- </button>
78
- );
79
- };
1
+ import { useEvoTheme } from './ThemeProvider';
2
+ import styles from '../css/theme-toggle.module.scss';
3
+
4
+ export interface EvoThemeToggleProps {
5
+ /** Visual size of the toggle. @default 'md' */
6
+ size?: 'sm' | 'md' | 'lg';
7
+ /** Accessible label. @default 'Toggle color theme' */
8
+ ariaLabel?: string;
9
+ /** Extra className to merge with the built-in styles. */
10
+ className?: string;
11
+ }
12
+
13
+ /**
14
+ * A drop-in button that flips between light and dark mode.
15
+ *
16
+ * Sits inside an `<EvoThemeProvider>`. The icon and animation
17
+ * automatically reflect the resolved theme.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <EvoThemeProvider>
22
+ * <EvoThemeToggle />
23
+ * </EvoThemeProvider>
24
+ * ```
25
+ */
26
+ export const EvoThemeToggle = ({
27
+ size = 'md',
28
+ ariaLabel = 'Toggle color theme',
29
+ className,
30
+ }: EvoThemeToggleProps) => {
31
+ const { resolvedTheme, toggleTheme } = useEvoTheme();
32
+ const isDark = resolvedTheme === 'dark';
33
+
34
+ const classes = [styles.toggle, styles[size], className].filter(Boolean).join(' ');
35
+
36
+ return (
37
+ <button
38
+ type="button"
39
+ role="switch"
40
+ aria-checked={isDark}
41
+ aria-label={ariaLabel}
42
+ onClick={toggleTheme}
43
+ className={classes}
44
+ data-theme-state={resolvedTheme}
45
+ >
46
+ <span className={styles.track}>
47
+ <span className={styles.thumb}>
48
+ {/* Sun icon */}
49
+ <svg
50
+ className={styles.sun}
51
+ viewBox="0 0 24 24"
52
+ fill="none"
53
+ stroke="currentColor"
54
+ strokeWidth="2"
55
+ strokeLinecap="round"
56
+ strokeLinejoin="round"
57
+ aria-hidden="true"
58
+ >
59
+ <circle cx="12" cy="12" r="4" />
60
+ <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
61
+ </svg>
62
+ {/* Moon icon */}
63
+ <svg
64
+ className={styles.moon}
65
+ viewBox="0 0 24 24"
66
+ fill="none"
67
+ stroke="currentColor"
68
+ strokeWidth="2"
69
+ strokeLinecap="round"
70
+ strokeLinejoin="round"
71
+ aria-hidden="true"
72
+ >
73
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
74
+ </svg>
75
+ </span>
76
+ </span>
77
+ </button>
78
+ );
79
+ };