@opencosmos/ui 1.3.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.
- package/.claude/CLAUDE.md +239 -0
- package/README.md +161 -0
- package/dist/cli.mjs +151 -0
- package/dist/dates.d.mts +20 -0
- package/dist/dates.d.ts +20 -0
- package/dist/dates.js +240 -0
- package/dist/dates.js.map +1 -0
- package/dist/dates.mjs +203 -0
- package/dist/dates.mjs.map +1 -0
- package/dist/dnd.d.mts +126 -0
- package/dist/dnd.d.ts +126 -0
- package/dist/dnd.js +274 -0
- package/dist/dnd.js.map +1 -0
- package/dist/dnd.mjs +250 -0
- package/dist/dnd.mjs.map +1 -0
- package/dist/fontThemes-Dh8mtXES.d.mts +868 -0
- package/dist/fontThemes-Dh8mtXES.d.ts +868 -0
- package/dist/forms.d.mts +38 -0
- package/dist/forms.d.ts +38 -0
- package/dist/forms.js +198 -0
- package/dist/forms.js.map +1 -0
- package/dist/forms.mjs +159 -0
- package/dist/forms.mjs.map +1 -0
- package/dist/hooks-1b8WaQf1.d.mts +225 -0
- package/dist/hooks-CKW8vE9H.d.ts +225 -0
- package/dist/hooks.d.mts +3 -0
- package/dist/hooks.d.ts +3 -0
- package/dist/hooks.js +971 -0
- package/dist/hooks.js.map +1 -0
- package/dist/hooks.mjs +943 -0
- package/dist/hooks.mjs.map +1 -0
- package/dist/index-DscTIrZ2.d.mts +29 -0
- package/dist/index-DscTIrZ2.d.ts +29 -0
- package/dist/index.d.mts +3382 -0
- package/dist/index.d.ts +3382 -0
- package/dist/index.js +15146 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +14802 -0
- package/dist/index.mjs.map +1 -0
- package/dist/providers-CXPDMsl7.d.mts +30 -0
- package/dist/providers-Dn_Msjvz.d.ts +30 -0
- package/dist/providers.d.mts +3 -0
- package/dist/providers.d.ts +3 -0
- package/dist/providers.js +1885 -0
- package/dist/providers.js.map +1 -0
- package/dist/providers.mjs +1859 -0
- package/dist/providers.mjs.map +1 -0
- package/dist/tables.d.mts +10 -0
- package/dist/tables.d.ts +10 -0
- package/dist/tables.js +248 -0
- package/dist/tables.js.map +1 -0
- package/dist/tables.mjs +218 -0
- package/dist/tables.mjs.map +1 -0
- package/dist/tokens.d.mts +1065 -0
- package/dist/tokens.d.ts +1065 -0
- package/dist/tokens.js +2637 -0
- package/dist/tokens.js.map +1 -0
- package/dist/tokens.mjs +2555 -0
- package/dist/tokens.mjs.map +1 -0
- package/dist/utils-CIIM7dAC.d.ts +986 -0
- package/dist/utils-Cs04sxth.d.mts +986 -0
- package/dist/utils.d.mts +4 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +874 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.mjs +806 -0
- package/dist/utils.mjs.map +1 -0
- package/dist/validation-Bj1ye-v_.d.mts +114 -0
- package/dist/validation-Bj1ye-v_.d.ts +114 -0
- package/dist/webgl.d.mts +104 -0
- package/dist/webgl.d.ts +104 -0
- package/dist/webgl.js +226 -0
- package/dist/webgl.js.map +1 -0
- package/dist/webgl.mjs +195 -0
- package/dist/webgl.mjs.map +1 -0
- package/package.json +267 -0
- package/src/cli.ts +206 -0
- package/src/component-registry.ts +183 -0
- package/src/components/actions/Button.test.tsx +61 -0
- package/src/components/actions/Button.tsx +70 -0
- package/src/components/actions/Link.tsx +78 -0
- package/src/components/actions/Magnetic.tsx +68 -0
- package/src/components/actions/Toggle.test.tsx +40 -0
- package/src/components/actions/Toggle.tsx +47 -0
- package/src/components/actions/ToggleGroup.tsx +70 -0
- package/src/components/actions/index.ts +5 -0
- package/src/components/backgrounds/FaultyTerminal.tsx +426 -0
- package/src/components/backgrounds/OrbBackground.tsx +424 -0
- package/src/components/backgrounds/WarpBackground.tsx +358 -0
- package/src/components/backgrounds/index.ts +3 -0
- package/src/components/blocks/Hero.tsx +142 -0
- package/src/components/blocks/social/OpenGraphCard.tsx +243 -0
- package/src/components/cursor/SplashCursor.tsx +1315 -0
- package/src/components/cursor/TargetCursor.tsx +187 -0
- package/src/components/cursor/index.ts +2 -0
- package/src/components/data-display/AspectImage.tsx +73 -0
- package/src/components/data-display/Avatar.test.tsx +35 -0
- package/src/components/data-display/Avatar.tsx +55 -0
- package/src/components/data-display/Badge.test.tsx +43 -0
- package/src/components/data-display/Badge.tsx +84 -0
- package/src/components/data-display/Brand.tsx +123 -0
- package/src/components/data-display/Calendar.tsx +70 -0
- package/src/components/data-display/Card.test.tsx +92 -0
- package/src/components/data-display/Card.tsx +115 -0
- package/src/components/data-display/Code.tsx +210 -0
- package/src/components/data-display/CollapsibleCodeBlock.tsx +238 -0
- package/src/components/data-display/DataTable.tsx +119 -0
- package/src/components/data-display/DescriptionList.tsx +41 -0
- package/src/components/data-display/GitHubIcon.tsx +44 -0
- package/src/components/data-display/Heading.test.tsx +36 -0
- package/src/components/data-display/Heading.tsx +83 -0
- package/src/components/data-display/StatCard.tsx +195 -0
- package/src/components/data-display/Table.tsx +133 -0
- package/src/components/data-display/Text.test.tsx +48 -0
- package/src/components/data-display/Text.tsx +144 -0
- package/src/components/data-display/Timeline.tsx +194 -0
- package/src/components/data-display/TreeView.tsx +226 -0
- package/src/components/data-display/Typewriter.tsx +119 -0
- package/src/components/data-display/VariableWeightText.tsx +130 -0
- package/src/components/data-display/index.ts +19 -0
- package/src/components/feedback/Alert.test.tsx +44 -0
- package/src/components/feedback/Alert.tsx +65 -0
- package/src/components/feedback/EmptyState.tsx +113 -0
- package/src/components/feedback/Progress.test.tsx +60 -0
- package/src/components/feedback/Progress.tsx +30 -0
- package/src/components/feedback/ProgressBar.tsx +158 -0
- package/src/components/feedback/Skeleton.test.tsx +39 -0
- package/src/components/feedback/Skeleton.tsx +45 -0
- package/src/components/feedback/Sonner.tsx +28 -0
- package/src/components/feedback/Spinner.test.tsx +33 -0
- package/src/components/feedback/Spinner.tsx +99 -0
- package/src/components/feedback/Stepper.tsx +307 -0
- package/src/components/feedback/Toast/Toast.tsx +243 -0
- package/src/components/feedback/Toast/index.ts +2 -0
- package/src/components/feedback/index.ts +9 -0
- package/src/components/forms/Checkbox.test.tsx +40 -0
- package/src/components/forms/Checkbox.tsx +31 -0
- package/src/components/forms/ColorPicker.tsx +118 -0
- package/src/components/forms/Combobox.tsx +96 -0
- package/src/components/forms/DragDrop.tsx +440 -0
- package/src/components/forms/FileUpload.tsx +252 -0
- package/src/components/forms/FilterButton.tsx +65 -0
- package/src/components/forms/Form.tsx +197 -0
- package/src/components/forms/Input.test.tsx +46 -0
- package/src/components/forms/Input.tsx +43 -0
- package/src/components/forms/InputOTP.tsx +81 -0
- package/src/components/forms/Label.test.tsx +20 -0
- package/src/components/forms/Label.tsx +25 -0
- package/src/components/forms/RadioGroup.tsx +51 -0
- package/src/components/forms/SearchBar.tsx +215 -0
- package/src/components/forms/Select.test.tsx +118 -0
- package/src/components/forms/Select.tsx +274 -0
- package/src/components/forms/Slider.tsx +29 -0
- package/src/components/forms/Switch.test.tsx +76 -0
- package/src/components/forms/Switch.tsx +30 -0
- package/src/components/forms/TextField.tsx +152 -0
- package/src/components/forms/Textarea.test.tsx +41 -0
- package/src/components/forms/Textarea.tsx +29 -0
- package/src/components/forms/ThemeSwitcher.tsx +290 -0
- package/src/components/forms/ThemeToggle.tsx +151 -0
- package/src/components/forms/index.ts +19 -0
- package/src/components/layout/Accordion.test.tsx +66 -0
- package/src/components/layout/Accordion.tsx +64 -0
- package/src/components/layout/AspectRatio.tsx +7 -0
- package/src/components/layout/Carousel.tsx +277 -0
- package/src/components/layout/Collapsible.test.tsx +40 -0
- package/src/components/layout/Collapsible.tsx +31 -0
- package/src/components/layout/Container.test.tsx +45 -0
- package/src/components/layout/Container.tsx +99 -0
- package/src/components/layout/CustomizerPanel.tsx +400 -0
- package/src/components/layout/DatePicker.tsx +57 -0
- package/src/components/layout/Footer/Footer.tsx +175 -0
- package/src/components/layout/Footer/index.ts +2 -0
- package/src/components/layout/GlassSurface.tsx +82 -0
- package/src/components/layout/Grid.test.tsx +31 -0
- package/src/components/layout/Grid.tsx +130 -0
- package/src/components/layout/Header/Header.tsx +450 -0
- package/src/components/layout/Header/index.ts +2 -0
- package/src/components/layout/PageLayout.tsx +180 -0
- package/src/components/layout/PageTemplate.tsx +158 -0
- package/src/components/layout/Resizable.tsx +48 -0
- package/src/components/layout/ScrollArea.tsx +53 -0
- package/src/components/layout/Separator.test.tsx +28 -0
- package/src/components/layout/Separator.tsx +29 -0
- package/src/components/layout/Sidebar.tsx +171 -0
- package/src/components/layout/Stack.test.tsx +41 -0
- package/src/components/layout/Stack.tsx +89 -0
- package/src/components/layout/glass-surface.css +60 -0
- package/src/components/layout/index.ts +18 -0
- package/src/components/motion/AnimatedBeam.tsx +159 -0
- package/src/components/navigation/Breadcrumb.test.tsx +57 -0
- package/src/components/navigation/Breadcrumb.tsx +119 -0
- package/src/components/navigation/Breadcrumbs.tsx +221 -0
- package/src/components/navigation/Command.tsx +159 -0
- package/src/components/navigation/Menubar.tsx +115 -0
- package/src/components/navigation/NavLink.tsx +55 -0
- package/src/components/navigation/NavigationMenu.tsx +125 -0
- package/src/components/navigation/Pagination.tsx +121 -0
- package/src/components/navigation/SecondaryNav.tsx +100 -0
- package/src/components/navigation/Tabs.test.tsx +47 -0
- package/src/components/navigation/Tabs.tsx +60 -0
- package/src/components/navigation/TertiaryNav.tsx +90 -0
- package/src/components/navigation/index.ts +10 -0
- package/src/components/overlays/AlertDialog.test.tsx +69 -0
- package/src/components/overlays/AlertDialog.tsx +166 -0
- package/src/components/overlays/ContextMenu.tsx +243 -0
- package/src/components/overlays/Dialog.test.tsx +79 -0
- package/src/components/overlays/Dialog.tsx +158 -0
- package/src/components/overlays/Drawer.tsx +128 -0
- package/src/components/overlays/Dropdown.tsx +253 -0
- package/src/components/overlays/DropdownMenu.tsx +242 -0
- package/src/components/overlays/HoverCard.tsx +32 -0
- package/src/components/overlays/Modal.tsx +250 -0
- package/src/components/overlays/NotificationCenter.tsx +364 -0
- package/src/components/overlays/Popover.test.tsx +40 -0
- package/src/components/overlays/Popover.tsx +46 -0
- package/src/components/overlays/Sheet.tsx +163 -0
- package/src/components/overlays/Tooltip.test.tsx +33 -0
- package/src/components/overlays/Tooltip.tsx +32 -0
- package/src/components/overlays/index.ts +12 -0
- package/src/dates.ts +2 -0
- package/src/dnd.ts +1 -0
- package/src/forms.ts +1 -0
- package/src/globals.css +187 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useForm.ts +247 -0
- package/src/hooks/useMotionPreference.test.ts +102 -0
- package/src/hooks/useMotionPreference.ts +78 -0
- package/src/hooks/useTheme.ts +58 -0
- package/src/hooks.ts +9 -0
- package/src/index.ts +168 -0
- package/src/lib/animations.ts +356 -0
- package/src/lib/breadcrumbs.ts +94 -0
- package/src/lib/colors.ts +493 -0
- package/src/lib/store/customizer.ts +482 -0
- package/src/lib/store/index.ts +3 -0
- package/src/lib/store/theme.ts +55 -0
- package/src/lib/syntax-parser/index.ts +50 -0
- package/src/lib/syntax-parser/patterns.ts +64 -0
- package/src/lib/syntax-parser/tokenizer.ts +117 -0
- package/src/lib/syntax-parser/types.ts +27 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validation.ts +204 -0
- package/src/lib/webgl/Color.ts +11 -0
- package/src/lib/webgl/Mesh.ts +41 -0
- package/src/lib/webgl/Program.ts +118 -0
- package/src/lib/webgl/Renderer.ts +51 -0
- package/src/lib/webgl/Triangle.ts +27 -0
- package/src/lib/webgl/Vec3.ts +18 -0
- package/src/lib/webgl/index.ts +13 -0
- package/src/nativewind-env.d.ts +1 -0
- package/src/providers/ThemeProvider.tsx +461 -0
- package/src/providers/index.ts +1 -0
- package/src/providers.ts +7 -0
- package/src/tables.ts +1 -0
- package/src/test/setup.ts +39 -0
- package/src/theme.css +158 -0
- package/src/tokens.ts +7 -0
- package/src/utils.ts +12 -0
- package/src/webgl.ts +1 -0
package/src/dates.ts
ADDED
package/src/dnd.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './components/forms/DragDrop';
|
package/src/forms.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './components/forms/Form';
|
package/src/globals.css
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default CSS Variables — Studio Theme (Light/Dark)
|
|
3
|
+
*
|
|
4
|
+
* These values serve as fallback defaults and are overridden at runtime by ThemeProvider
|
|
5
|
+
* when a different theme is selected. ThemeProvider injects CSS variables dynamically
|
|
6
|
+
* based on the active theme (Studio, Terra, Volt, Speedboat) and color mode.
|
|
7
|
+
*
|
|
8
|
+
* See: packages/ui/src/providers/ThemeProvider.tsx
|
|
9
|
+
* See: packages/tokens/src/studio.ts, terra.ts, volt.ts, speedboat.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
@layer base {
|
|
13
|
+
:root {
|
|
14
|
+
/* ── Color Palette (Studio Light) ────────────────────────────── */
|
|
15
|
+
--color-background: #ffffff;
|
|
16
|
+
--color-background-secondary: #fafafa;
|
|
17
|
+
--color-background-tertiary: #f5f5f5;
|
|
18
|
+
--color-foreground: #0a0a0a;
|
|
19
|
+
--color-text-primary: var(--color-foreground);
|
|
20
|
+
--color-text-secondary: #525252;
|
|
21
|
+
--color-text-muted: #a3a3a3;
|
|
22
|
+
|
|
23
|
+
--color-primary: #0a0a0a;
|
|
24
|
+
--color-primary-foreground: #ffffff;
|
|
25
|
+
|
|
26
|
+
--color-secondary: #f5f5f5;
|
|
27
|
+
--color-secondary-foreground: #0a0a0a;
|
|
28
|
+
|
|
29
|
+
--color-accent: #0070f3;
|
|
30
|
+
--color-accent-foreground: #ffffff;
|
|
31
|
+
|
|
32
|
+
--color-destructive: #ef4444;
|
|
33
|
+
--color-destructive-foreground: #ffffff;
|
|
34
|
+
|
|
35
|
+
--color-muted: #f5f5f5;
|
|
36
|
+
--color-muted-foreground: #737373;
|
|
37
|
+
|
|
38
|
+
--color-popover: #ffffff;
|
|
39
|
+
--color-popover-foreground: #0a0a0a;
|
|
40
|
+
|
|
41
|
+
--color-card: #ffffff;
|
|
42
|
+
--color-card-foreground: #0a0a0a;
|
|
43
|
+
|
|
44
|
+
--color-success: #00a86b;
|
|
45
|
+
--color-success-foreground: #ffffff;
|
|
46
|
+
|
|
47
|
+
--color-warning: #f59e0b;
|
|
48
|
+
--color-warning-foreground: #ffffff;
|
|
49
|
+
|
|
50
|
+
--color-error: #ef4444;
|
|
51
|
+
--color-error-foreground: #ffffff;
|
|
52
|
+
|
|
53
|
+
--color-info: #0070f3;
|
|
54
|
+
--color-info-foreground: #ffffff;
|
|
55
|
+
|
|
56
|
+
--color-surface: #fafafa;
|
|
57
|
+
--color-border: #d4d4d4;
|
|
58
|
+
--color-input: #d4d4d4;
|
|
59
|
+
--color-ring: #0a0a0a;
|
|
60
|
+
|
|
61
|
+
--color-glass: rgba(255, 255, 255, 0.7);
|
|
62
|
+
--color-glass-border: rgba(0, 0, 0, 0.1);
|
|
63
|
+
|
|
64
|
+
--radius: 0.5rem;
|
|
65
|
+
|
|
66
|
+
/* ── Interaction Tokens ──────────────────────────────────────── */
|
|
67
|
+
--color-interaction-overlay: #000000;
|
|
68
|
+
--opacity-interaction-hover: 0.06;
|
|
69
|
+
--scale-interaction-active: 0.98;
|
|
70
|
+
--color-interaction-focus-ring: var(--color-ring);
|
|
71
|
+
--width-interaction-focus-ring: 2px;
|
|
72
|
+
--width-interaction-focus-offset: 2px;
|
|
73
|
+
--opacity-interaction-disabled: 0.5;
|
|
74
|
+
|
|
75
|
+
/* ── Effects — Blur (raw values for Tailwind utilities) ─────── */
|
|
76
|
+
--blur-sm: 4px;
|
|
77
|
+
--blur-md: 8px;
|
|
78
|
+
--blur-lg: 16px;
|
|
79
|
+
--blur-xl: 24px;
|
|
80
|
+
|
|
81
|
+
/* ── Effects — Shadow ─────────────────────────────────────────── */
|
|
82
|
+
--effect-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
83
|
+
--effect-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
84
|
+
--effect-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
85
|
+
--effect-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
|
86
|
+
--effect-shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
87
|
+
|
|
88
|
+
/* ── Motion — Easing ──────────────────────────────────────────── */
|
|
89
|
+
--ease-default: cubic-bezier(0.4, 0, 0.2, 1);
|
|
90
|
+
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
|
91
|
+
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
|
92
|
+
--ease-spring: cubic-bezier(0.16, 1, 0.3, 1);
|
|
93
|
+
|
|
94
|
+
/* ── Motion — Duration (overridden by ThemeProvider) ──────────── */
|
|
95
|
+
--duration-default: 200ms;
|
|
96
|
+
--duration-fast: 100ms;
|
|
97
|
+
--duration-slow: 300ms;
|
|
98
|
+
|
|
99
|
+
/* ── Primary Color Scale (populated by Customizer) ────────────── */
|
|
100
|
+
--color-primary-50: initial;
|
|
101
|
+
--color-primary-100: initial;
|
|
102
|
+
--color-primary-200: initial;
|
|
103
|
+
--color-primary-300: initial;
|
|
104
|
+
--color-primary-400: initial;
|
|
105
|
+
--color-primary-500: initial;
|
|
106
|
+
--color-primary-600: initial;
|
|
107
|
+
--color-primary-700: initial;
|
|
108
|
+
--color-primary-800: initial;
|
|
109
|
+
--color-primary-900: initial;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.dark {
|
|
113
|
+
/* ── Color Palette (Studio Dark) ─────────────────────────────── */
|
|
114
|
+
--color-background: #000000;
|
|
115
|
+
--color-background-secondary: #171717;
|
|
116
|
+
--color-background-tertiary: #262626;
|
|
117
|
+
--color-foreground: #fafafa;
|
|
118
|
+
--color-text-primary: var(--color-foreground);
|
|
119
|
+
--color-text-secondary: #a3a3a3;
|
|
120
|
+
--color-text-muted: #858585;
|
|
121
|
+
|
|
122
|
+
--color-primary: #ffffff;
|
|
123
|
+
--color-primary-foreground: #0a0a0a;
|
|
124
|
+
|
|
125
|
+
--color-secondary: #262626;
|
|
126
|
+
--color-secondary-foreground: #fafafa;
|
|
127
|
+
|
|
128
|
+
--color-accent: #0090ff;
|
|
129
|
+
--color-accent-foreground: #ffffff;
|
|
130
|
+
|
|
131
|
+
--color-destructive: #ef4444;
|
|
132
|
+
--color-destructive-foreground: #ffffff;
|
|
133
|
+
|
|
134
|
+
--color-muted: #262626;
|
|
135
|
+
--color-muted-foreground: #a3a3a3;
|
|
136
|
+
|
|
137
|
+
--color-popover: #0a0a0a;
|
|
138
|
+
--color-popover-foreground: #fafafa;
|
|
139
|
+
|
|
140
|
+
--color-card: #0a0a0a;
|
|
141
|
+
--color-card-foreground: #fafafa;
|
|
142
|
+
|
|
143
|
+
--color-success: #10b981;
|
|
144
|
+
--color-success-foreground: #ffffff;
|
|
145
|
+
|
|
146
|
+
--color-warning: #f59e0b;
|
|
147
|
+
--color-warning-foreground: #ffffff;
|
|
148
|
+
|
|
149
|
+
--color-error: #ef4444;
|
|
150
|
+
--color-error-foreground: #ffffff;
|
|
151
|
+
|
|
152
|
+
--color-info: #0090ff;
|
|
153
|
+
--color-info-foreground: #ffffff;
|
|
154
|
+
|
|
155
|
+
--color-surface: #171717;
|
|
156
|
+
--color-border: #404040;
|
|
157
|
+
--color-input: #404040;
|
|
158
|
+
--color-ring: #d4d4d4;
|
|
159
|
+
|
|
160
|
+
--color-glass: rgba(0, 0, 0, 0.7);
|
|
161
|
+
--color-glass-border: rgba(255, 255, 255, 0.1);
|
|
162
|
+
|
|
163
|
+
/* ── Interaction Tokens — Dark Mode ──────────────────────────── */
|
|
164
|
+
--color-interaction-overlay: #ffffff;
|
|
165
|
+
--color-interaction-focus-ring: var(--color-ring);
|
|
166
|
+
|
|
167
|
+
/* ── Effects — Shadow (dark mode, heavier) ───────────────────── */
|
|
168
|
+
--effect-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
|
|
169
|
+
--effect-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.5);
|
|
170
|
+
--effect-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.6);
|
|
171
|
+
--effect-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.7);
|
|
172
|
+
--effect-shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.8);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@layer base {
|
|
177
|
+
body {
|
|
178
|
+
background-color: var(--color-background);
|
|
179
|
+
color: var(--color-foreground);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
* {
|
|
183
|
+
border-color: var(--color-border);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@import './components/layout/glass-surface.css';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { useTheme } from './useTheme';
|
|
2
|
+
export { useMotionPreference } from './useMotionPreference';
|
|
3
|
+
export { useForm } from './useForm';
|
|
4
|
+
export type { ThemeHook } from './useTheme';
|
|
5
|
+
export type { MotionPreference } from './useMotionPreference';
|
|
6
|
+
export type { UseFormOptions, UseFormReturn } from './useForm';
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
import type { FieldValidation, FormErrors } from '../lib/validation';
|
|
5
|
+
import { validateField, validateForm } from '../lib/validation';
|
|
6
|
+
|
|
7
|
+
export interface UseFormOptions<T> {
|
|
8
|
+
/**
|
|
9
|
+
* Initial form values
|
|
10
|
+
*/
|
|
11
|
+
initialValues: T;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validation rules for each field
|
|
15
|
+
*/
|
|
16
|
+
validations?: Partial<Record<keyof T, FieldValidation>>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Callback fired when form is submitted and valid
|
|
20
|
+
*/
|
|
21
|
+
onSubmit?: (values: T) => void | Promise<void>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* When to validate fields
|
|
25
|
+
* @default 'onBlur'
|
|
26
|
+
*/
|
|
27
|
+
validateOn?: 'onChange' | 'onBlur' | 'onSubmit';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UseFormReturn<T> {
|
|
31
|
+
/**
|
|
32
|
+
* Current form values
|
|
33
|
+
*/
|
|
34
|
+
values: T;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Current form errors
|
|
38
|
+
*/
|
|
39
|
+
errors: FormErrors;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether the form is currently submitting
|
|
43
|
+
*/
|
|
44
|
+
isSubmitting: boolean;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether the form has been touched/modified
|
|
48
|
+
*/
|
|
49
|
+
isDirty: boolean;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Set value for a specific field
|
|
53
|
+
*/
|
|
54
|
+
setValue: (name: keyof T, value: any) => void;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Set error for a specific field
|
|
58
|
+
*/
|
|
59
|
+
setError: (name: keyof T, error: string | undefined) => void;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Handle input change event
|
|
63
|
+
*/
|
|
64
|
+
handleChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Handle input blur event
|
|
68
|
+
*/
|
|
69
|
+
handleBlur: (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Handle form submit
|
|
73
|
+
*/
|
|
74
|
+
handleSubmit: (e?: React.FormEvent<HTMLFormElement>) => Promise<void>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Reset form to initial values
|
|
78
|
+
*/
|
|
79
|
+
reset: () => void;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Manually validate all fields
|
|
83
|
+
*/
|
|
84
|
+
validate: () => boolean;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get props for a field (value, onChange, onBlur, error)
|
|
88
|
+
*/
|
|
89
|
+
getFieldProps: (name: keyof T) => {
|
|
90
|
+
name: string;
|
|
91
|
+
value: any;
|
|
92
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
|
|
93
|
+
onBlur: (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
|
|
94
|
+
error: boolean;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* useForm Hook
|
|
100
|
+
*
|
|
101
|
+
* A lightweight form state management hook with built-in validation.
|
|
102
|
+
*
|
|
103
|
+
* Features:
|
|
104
|
+
* - Field-level validation
|
|
105
|
+
* - Configurable validation timing (onChange, onBlur, onSubmit)
|
|
106
|
+
* - Dirty state tracking
|
|
107
|
+
* - Submit handling with loading state
|
|
108
|
+
* - Helper functions for common patterns
|
|
109
|
+
*
|
|
110
|
+
* Example:
|
|
111
|
+
* ```tsx
|
|
112
|
+
* const form = useForm({
|
|
113
|
+
* initialValues: { email: '', password: '' },
|
|
114
|
+
* validations: {
|
|
115
|
+
* email: { required: true, pattern: patterns.email },
|
|
116
|
+
* password: { required: true, minLength: { value: 8, message: 'Min 8 chars' } }
|
|
117
|
+
* },
|
|
118
|
+
* onSubmit: async (values) => {
|
|
119
|
+
* await login(values);
|
|
120
|
+
* }
|
|
121
|
+
* });
|
|
122
|
+
*
|
|
123
|
+
* return (
|
|
124
|
+
* <form onSubmit={form.handleSubmit}>
|
|
125
|
+
* <FormField label="Email" error={form.errors.email}>
|
|
126
|
+
* <TextField {...form.getFieldProps('email')} />
|
|
127
|
+
* </FormField>
|
|
128
|
+
* <Button type="submit" loading={form.isSubmitting}>Submit</Button>
|
|
129
|
+
* </form>
|
|
130
|
+
* );
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export function useForm<T extends Record<string, any>>({
|
|
134
|
+
initialValues,
|
|
135
|
+
validations = {},
|
|
136
|
+
onSubmit,
|
|
137
|
+
validateOn = 'onBlur',
|
|
138
|
+
}: UseFormOptions<T>): UseFormReturn<T> {
|
|
139
|
+
const [values, setValues] = useState<T>(initialValues);
|
|
140
|
+
const [errors, setErrors] = useState<FormErrors>({});
|
|
141
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
142
|
+
const [isDirty, setIsDirty] = useState(false);
|
|
143
|
+
|
|
144
|
+
const setValue = useCallback((name: keyof T, value: any) => {
|
|
145
|
+
setValues((prev) => ({ ...prev, [name]: value }));
|
|
146
|
+
setIsDirty(true);
|
|
147
|
+
}, []);
|
|
148
|
+
|
|
149
|
+
const setError = useCallback((name: keyof T, error: string | undefined) => {
|
|
150
|
+
setErrors((prev) => ({ ...prev, [name as string]: error }));
|
|
151
|
+
}, []);
|
|
152
|
+
|
|
153
|
+
const validateFieldByName = useCallback(
|
|
154
|
+
(name: keyof T) => {
|
|
155
|
+
const fieldRules = validations[name];
|
|
156
|
+
if (!fieldRules) return;
|
|
157
|
+
|
|
158
|
+
const error = validateField(values[name], fieldRules);
|
|
159
|
+
setError(name, error);
|
|
160
|
+
return !error;
|
|
161
|
+
},
|
|
162
|
+
[values, validations, setError]
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const handleChange = useCallback(
|
|
166
|
+
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
|
167
|
+
const { name, value, type } = e.target;
|
|
168
|
+
const fieldValue = type === 'checkbox' ? (e.target as HTMLInputElement).checked : value;
|
|
169
|
+
|
|
170
|
+
setValue(name as keyof T, fieldValue);
|
|
171
|
+
|
|
172
|
+
if (validateOn === 'onChange') {
|
|
173
|
+
validateFieldByName(name as keyof T);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
[setValue, validateOn, validateFieldByName]
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const handleBlur = useCallback(
|
|
180
|
+
(e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
|
181
|
+
const { name } = e.target;
|
|
182
|
+
|
|
183
|
+
if (validateOn === 'onBlur') {
|
|
184
|
+
validateFieldByName(name as keyof T);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
[validateOn, validateFieldByName]
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const validate = useCallback(() => {
|
|
191
|
+
const formErrors = validateForm(values, validations as any);
|
|
192
|
+
setErrors(formErrors);
|
|
193
|
+
return Object.keys(formErrors).length === 0;
|
|
194
|
+
}, [values, validations]);
|
|
195
|
+
|
|
196
|
+
const handleSubmit = useCallback(
|
|
197
|
+
async (e?: React.FormEvent<HTMLFormElement>) => {
|
|
198
|
+
e?.preventDefault();
|
|
199
|
+
|
|
200
|
+
const isValid = validate();
|
|
201
|
+
if (!isValid) return;
|
|
202
|
+
|
|
203
|
+
if (onSubmit) {
|
|
204
|
+
setIsSubmitting(true);
|
|
205
|
+
try {
|
|
206
|
+
await onSubmit(values);
|
|
207
|
+
} finally {
|
|
208
|
+
setIsSubmitting(false);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
[validate, onSubmit, values]
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const reset = useCallback(() => {
|
|
216
|
+
setValues(initialValues);
|
|
217
|
+
setErrors({});
|
|
218
|
+
setIsDirty(false);
|
|
219
|
+
setIsSubmitting(false);
|
|
220
|
+
}, [initialValues]);
|
|
221
|
+
|
|
222
|
+
const getFieldProps = useCallback(
|
|
223
|
+
(name: keyof T) => ({
|
|
224
|
+
name: name as string,
|
|
225
|
+
value: values[name] ?? '',
|
|
226
|
+
onChange: handleChange,
|
|
227
|
+
onBlur: handleBlur,
|
|
228
|
+
error: !!errors[name as string],
|
|
229
|
+
}),
|
|
230
|
+
[values, errors, handleChange, handleBlur]
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
values,
|
|
235
|
+
errors,
|
|
236
|
+
isSubmitting,
|
|
237
|
+
isDirty,
|
|
238
|
+
setValue,
|
|
239
|
+
setError,
|
|
240
|
+
handleChange,
|
|
241
|
+
handleBlur,
|
|
242
|
+
handleSubmit,
|
|
243
|
+
reset,
|
|
244
|
+
validate,
|
|
245
|
+
getFieldProps,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react'
|
|
2
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
3
|
+
import { useMotionPreference } from './useMotionPreference'
|
|
4
|
+
import { useCustomizer } from '../lib/store/customizer'
|
|
5
|
+
|
|
6
|
+
// Helper to mock matchMedia with a specific matches value
|
|
7
|
+
function mockMatchMedia(matches: boolean) {
|
|
8
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
9
|
+
writable: true,
|
|
10
|
+
value: vi.fn().mockImplementation((query: string) => ({
|
|
11
|
+
matches,
|
|
12
|
+
media: query,
|
|
13
|
+
onchange: null,
|
|
14
|
+
addListener: vi.fn(),
|
|
15
|
+
removeListener: vi.fn(),
|
|
16
|
+
addEventListener: vi.fn(),
|
|
17
|
+
removeEventListener: vi.fn(),
|
|
18
|
+
dispatchEvent: vi.fn(),
|
|
19
|
+
})),
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('useMotionPreference', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
// Reset the Zustand store to defaults before each test
|
|
26
|
+
useCustomizer.setState({
|
|
27
|
+
motion: 5,
|
|
28
|
+
prefersReducedMotion: false,
|
|
29
|
+
})
|
|
30
|
+
// Default: system does not prefer reduced motion
|
|
31
|
+
mockMatchMedia(false)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('returns scale, shouldAnimate, and prefersReducedMotion', () => {
|
|
35
|
+
const { result } = renderHook(() => useMotionPreference())
|
|
36
|
+
|
|
37
|
+
expect(result.current).toHaveProperty('scale')
|
|
38
|
+
expect(result.current).toHaveProperty('shouldAnimate')
|
|
39
|
+
expect(result.current).toHaveProperty('prefersReducedMotion')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('returns default values from the store', () => {
|
|
43
|
+
const { result } = renderHook(() => useMotionPreference())
|
|
44
|
+
|
|
45
|
+
expect(result.current.scale).toBe(5)
|
|
46
|
+
expect(result.current.shouldAnimate).toBe(true)
|
|
47
|
+
expect(result.current.prefersReducedMotion).toBe(false)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('shouldAnimate is false when scale is 0', () => {
|
|
51
|
+
useCustomizer.setState({ motion: 0 })
|
|
52
|
+
|
|
53
|
+
const { result } = renderHook(() => useMotionPreference())
|
|
54
|
+
|
|
55
|
+
expect(result.current.scale).toBe(0)
|
|
56
|
+
expect(result.current.shouldAnimate).toBe(false)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('shouldAnimate is false when system prefers reduced motion', () => {
|
|
60
|
+
// Mock the system preference to prefer reduced motion
|
|
61
|
+
mockMatchMedia(true)
|
|
62
|
+
|
|
63
|
+
const { result } = renderHook(() => useMotionPreference())
|
|
64
|
+
|
|
65
|
+
expect(result.current.prefersReducedMotion).toBe(true)
|
|
66
|
+
expect(result.current.shouldAnimate).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('shouldAnimate is false when both scale is 0 and system prefers reduced motion', () => {
|
|
70
|
+
useCustomizer.setState({ motion: 0 })
|
|
71
|
+
mockMatchMedia(true)
|
|
72
|
+
|
|
73
|
+
const { result } = renderHook(() => useMotionPreference())
|
|
74
|
+
|
|
75
|
+
expect(result.current.scale).toBe(0)
|
|
76
|
+
expect(result.current.prefersReducedMotion).toBe(true)
|
|
77
|
+
expect(result.current.shouldAnimate).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('shouldAnimate is true when scale is above 0 and system does not prefer reduced motion', () => {
|
|
81
|
+
useCustomizer.setState({ motion: 10 })
|
|
82
|
+
|
|
83
|
+
const { result } = renderHook(() => useMotionPreference())
|
|
84
|
+
|
|
85
|
+
expect(result.current.scale).toBe(10)
|
|
86
|
+
expect(result.current.shouldAnimate).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('reflects changes when motion is updated via the store', () => {
|
|
90
|
+
const { result } = renderHook(() => useMotionPreference())
|
|
91
|
+
|
|
92
|
+
expect(result.current.scale).toBe(5)
|
|
93
|
+
expect(result.current.shouldAnimate).toBe(true)
|
|
94
|
+
|
|
95
|
+
act(() => {
|
|
96
|
+
useCustomizer.setState({ motion: 0 })
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
expect(result.current.scale).toBe(0)
|
|
100
|
+
expect(result.current.shouldAnimate).toBe(false)
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { useCustomizer } from '../lib/store/customizer';
|
|
5
|
+
|
|
6
|
+
export interface MotionPreference {
|
|
7
|
+
/**
|
|
8
|
+
* Motion intensity level (0-10)
|
|
9
|
+
* 0 = no motion, 10 = full motion
|
|
10
|
+
*/
|
|
11
|
+
scale: number;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether animations should be displayed
|
|
15
|
+
* False when scale is 0 or prefersReducedMotion is true
|
|
16
|
+
*/
|
|
17
|
+
shouldAnimate: boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* System preference for reduced motion
|
|
21
|
+
*/
|
|
22
|
+
prefersReducedMotion: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Hook to access motion preferences
|
|
27
|
+
*
|
|
28
|
+
* Automatically syncs with system `prefers-reduced-motion` media query
|
|
29
|
+
* and respects user's manual motion intensity setting.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* function AnimatedComponent() {
|
|
34
|
+
* const { scale, shouldAnimate } = useMotionPreference();
|
|
35
|
+
*
|
|
36
|
+
* if (!shouldAnimate) {
|
|
37
|
+
* return <div>Content without animation</div>;
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* return (
|
|
41
|
+
* <motion.div
|
|
42
|
+
* animate={{ opacity: 1 }}
|
|
43
|
+
* transition={{ duration: 0.3 * (scale / 10) }}
|
|
44
|
+
* >
|
|
45
|
+
* Content with scaled animation
|
|
46
|
+
* </motion.div>
|
|
47
|
+
* );
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function useMotionPreference(): MotionPreference {
|
|
52
|
+
const { motion, prefersReducedMotion, setPrefersReducedMotion } = useCustomizer();
|
|
53
|
+
|
|
54
|
+
// Listen for system prefers-reduced-motion changes
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
// Only run in browser environment
|
|
57
|
+
if (typeof window === 'undefined') return;
|
|
58
|
+
|
|
59
|
+
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
60
|
+
|
|
61
|
+
// Set initial value
|
|
62
|
+
setPrefersReducedMotion(mediaQuery.matches);
|
|
63
|
+
|
|
64
|
+
// Listen for changes
|
|
65
|
+
const handleChange = (e: MediaQueryListEvent) => {
|
|
66
|
+
setPrefersReducedMotion(e.matches);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
70
|
+
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
71
|
+
}, [setPrefersReducedMotion]);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
scale: motion,
|
|
75
|
+
shouldAnimate: motion > 0 && !prefersReducedMotion,
|
|
76
|
+
prefersReducedMotion,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useThemeStore } from '../lib/store/theme';
|
|
4
|
+
import type { ThemeName, ColorMode } from '@thesage/tokens';
|
|
5
|
+
|
|
6
|
+
export interface ThemeHook {
|
|
7
|
+
/**
|
|
8
|
+
* Current theme name
|
|
9
|
+
*/
|
|
10
|
+
theme: ThemeName;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Current color mode (light/dark)
|
|
14
|
+
*/
|
|
15
|
+
mode: ColorMode;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Set the theme
|
|
19
|
+
*/
|
|
20
|
+
setTheme: (theme: ThemeName) => void;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Set the color mode
|
|
24
|
+
*/
|
|
25
|
+
setMode: (mode: ColorMode) => void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Toggle between light and dark mode
|
|
29
|
+
*/
|
|
30
|
+
toggleMode: () => void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Combined theme configuration
|
|
34
|
+
*/
|
|
35
|
+
themeConfig: { name: ThemeName; mode: ColorMode };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Hook to access and control theme settings
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* function ThemeSelector() {
|
|
44
|
+
* const { theme, mode, setTheme, toggleMode } = useTheme();
|
|
45
|
+
*
|
|
46
|
+
* return (
|
|
47
|
+
* <div>
|
|
48
|
+
* <p>Current: {theme} - {mode}</p>
|
|
49
|
+
* <button onClick={() => setTheme('sage')}>Sage Theme</button>
|
|
50
|
+
* <button onClick={toggleMode}>Toggle Mode</button>
|
|
51
|
+
* </div>
|
|
52
|
+
* );
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function useTheme(): ThemeHook {
|
|
57
|
+
return useThemeStore();
|
|
58
|
+
}
|