@justin_evo/evo-ui 1.1.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 (80) hide show
  1. package/README.md +3 -3
  2. package/dist/TopNav/TopNav.d.ts +19 -0
  3. package/dist/declarations.d.ts +6 -6
  4. package/dist/evo-ui.css +1 -1
  5. package/dist/index.cjs.js +1 -1
  6. package/dist/index.es.js +3301 -3197
  7. package/package.json +52 -52
  8. package/src/Alert/Alert.tsx +49 -49
  9. package/src/AutoComplete/AutoComplete.tsx +810 -810
  10. package/src/Badge/Badge.tsx +53 -53
  11. package/src/Breadcrumb/Breadcrumb.tsx +53 -53
  12. package/src/Button/Button.tsx +125 -125
  13. package/src/Card/Card.tsx +257 -257
  14. package/src/Checkbox/Checkbox.tsx +59 -59
  15. package/src/CommandPalette/CommandPalette.tsx +185 -185
  16. package/src/Container/Container.tsx +31 -31
  17. package/src/Divider/Divider.tsx +31 -31
  18. package/src/Form/Form.tsx +185 -185
  19. package/src/Grid/Grid.tsx +66 -66
  20. package/src/ImageCropper/ImageCropper.tsx +911 -911
  21. package/src/Input/Input.tsx +74 -74
  22. package/src/Modal/Modal.tsx +77 -77
  23. package/src/Nav/Nav.tsx +708 -708
  24. package/src/Notification/Notification.tsx +1503 -1503
  25. package/src/Pagination/Pagination.tsx +76 -76
  26. package/src/Radio/Radio.tsx +69 -69
  27. package/src/RichTextArea/RichTextArea.tsx +886 -869
  28. package/src/Select/Select.tsx +515 -515
  29. package/src/Skeleton/Skeleton.tsx +70 -70
  30. package/src/Stack/Stack.tsx +52 -52
  31. package/src/Table/Table.tsx +335 -335
  32. package/src/Tabs/Tabs.tsx +90 -90
  33. package/src/Theme/ThemeProvider.tsx +253 -253
  34. package/src/Theme/ThemeToggle.tsx +79 -79
  35. package/src/Toggle/Toggle.tsx +48 -48
  36. package/src/Tooltip/Tooltip.tsx +38 -38
  37. package/src/TopNav/TopNav.tsx +1163 -994
  38. package/src/TreeSelect/TreeSelect.tsx +825 -825
  39. package/src/css/alert.module.scss +93 -93
  40. package/src/css/autocomplete.module.scss +416 -416
  41. package/src/css/badge.module.scss +82 -82
  42. package/src/css/base/_color.scss +159 -159
  43. package/src/css/base/_theme.scss +237 -237
  44. package/src/css/base/_variables.scss +161 -161
  45. package/src/css/breadcrumb.module.scss +50 -50
  46. package/src/css/button.module.scss +385 -385
  47. package/src/css/card.module.scss +217 -217
  48. package/src/css/checkbox.module.scss +123 -120
  49. package/src/css/commandpalette.module.scss +211 -211
  50. package/src/css/container.module.scss +18 -18
  51. package/src/css/divider.module.scss +41 -41
  52. package/src/css/form.module.scss +245 -245
  53. package/src/css/imagecropper.module.scss +397 -397
  54. package/src/css/input.module.scss +89 -89
  55. package/src/css/modal.module.scss +105 -105
  56. package/src/css/nav.module.scss +494 -494
  57. package/src/css/notification.module.scss +691 -691
  58. package/src/css/pagination.module.scss +63 -63
  59. package/src/css/radio.module.scss +89 -89
  60. package/src/css/richtextarea.module.scss +307 -307
  61. package/src/css/select.module.scss +525 -525
  62. package/src/css/skeleton.module.scss +30 -30
  63. package/src/css/table.module.scss +386 -386
  64. package/src/css/tabs.module.scss +63 -63
  65. package/src/css/theme-toggle.module.scss +83 -83
  66. package/src/css/toggle.module.scss +54 -54
  67. package/src/css/tooltip.module.scss +97 -97
  68. package/src/css/topnav.module.scss +568 -396
  69. package/src/css/treeselect.module.scss +558 -558
  70. package/src/css/utilities/_borders.scss +111 -111
  71. package/src/css/utilities/_colors.scss +66 -66
  72. package/src/css/utilities/_effects.scss +216 -216
  73. package/src/css/utilities/_layout.scss +181 -181
  74. package/src/css/utilities/_position.scss +75 -75
  75. package/src/css/utilities/_sizing.scss +138 -138
  76. package/src/css/utilities/_spacing.scss +99 -99
  77. package/src/css/utilities/_typography.scss +121 -121
  78. package/src/css/utilities/index.scss +24 -24
  79. package/src/declarations.d.ts +6 -6
  80. 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
+ };