@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.
Files changed (260) hide show
  1. package/.claude/CLAUDE.md +239 -0
  2. package/README.md +161 -0
  3. package/dist/cli.mjs +151 -0
  4. package/dist/dates.d.mts +20 -0
  5. package/dist/dates.d.ts +20 -0
  6. package/dist/dates.js +240 -0
  7. package/dist/dates.js.map +1 -0
  8. package/dist/dates.mjs +203 -0
  9. package/dist/dates.mjs.map +1 -0
  10. package/dist/dnd.d.mts +126 -0
  11. package/dist/dnd.d.ts +126 -0
  12. package/dist/dnd.js +274 -0
  13. package/dist/dnd.js.map +1 -0
  14. package/dist/dnd.mjs +250 -0
  15. package/dist/dnd.mjs.map +1 -0
  16. package/dist/fontThemes-Dh8mtXES.d.mts +868 -0
  17. package/dist/fontThemes-Dh8mtXES.d.ts +868 -0
  18. package/dist/forms.d.mts +38 -0
  19. package/dist/forms.d.ts +38 -0
  20. package/dist/forms.js +198 -0
  21. package/dist/forms.js.map +1 -0
  22. package/dist/forms.mjs +159 -0
  23. package/dist/forms.mjs.map +1 -0
  24. package/dist/hooks-1b8WaQf1.d.mts +225 -0
  25. package/dist/hooks-CKW8vE9H.d.ts +225 -0
  26. package/dist/hooks.d.mts +3 -0
  27. package/dist/hooks.d.ts +3 -0
  28. package/dist/hooks.js +971 -0
  29. package/dist/hooks.js.map +1 -0
  30. package/dist/hooks.mjs +943 -0
  31. package/dist/hooks.mjs.map +1 -0
  32. package/dist/index-DscTIrZ2.d.mts +29 -0
  33. package/dist/index-DscTIrZ2.d.ts +29 -0
  34. package/dist/index.d.mts +3382 -0
  35. package/dist/index.d.ts +3382 -0
  36. package/dist/index.js +15146 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/index.mjs +14802 -0
  39. package/dist/index.mjs.map +1 -0
  40. package/dist/providers-CXPDMsl7.d.mts +30 -0
  41. package/dist/providers-Dn_Msjvz.d.ts +30 -0
  42. package/dist/providers.d.mts +3 -0
  43. package/dist/providers.d.ts +3 -0
  44. package/dist/providers.js +1885 -0
  45. package/dist/providers.js.map +1 -0
  46. package/dist/providers.mjs +1859 -0
  47. package/dist/providers.mjs.map +1 -0
  48. package/dist/tables.d.mts +10 -0
  49. package/dist/tables.d.ts +10 -0
  50. package/dist/tables.js +248 -0
  51. package/dist/tables.js.map +1 -0
  52. package/dist/tables.mjs +218 -0
  53. package/dist/tables.mjs.map +1 -0
  54. package/dist/tokens.d.mts +1065 -0
  55. package/dist/tokens.d.ts +1065 -0
  56. package/dist/tokens.js +2637 -0
  57. package/dist/tokens.js.map +1 -0
  58. package/dist/tokens.mjs +2555 -0
  59. package/dist/tokens.mjs.map +1 -0
  60. package/dist/utils-CIIM7dAC.d.ts +986 -0
  61. package/dist/utils-Cs04sxth.d.mts +986 -0
  62. package/dist/utils.d.mts +4 -0
  63. package/dist/utils.d.ts +4 -0
  64. package/dist/utils.js +874 -0
  65. package/dist/utils.js.map +1 -0
  66. package/dist/utils.mjs +806 -0
  67. package/dist/utils.mjs.map +1 -0
  68. package/dist/validation-Bj1ye-v_.d.mts +114 -0
  69. package/dist/validation-Bj1ye-v_.d.ts +114 -0
  70. package/dist/webgl.d.mts +104 -0
  71. package/dist/webgl.d.ts +104 -0
  72. package/dist/webgl.js +226 -0
  73. package/dist/webgl.js.map +1 -0
  74. package/dist/webgl.mjs +195 -0
  75. package/dist/webgl.mjs.map +1 -0
  76. package/package.json +267 -0
  77. package/src/cli.ts +206 -0
  78. package/src/component-registry.ts +183 -0
  79. package/src/components/actions/Button.test.tsx +61 -0
  80. package/src/components/actions/Button.tsx +70 -0
  81. package/src/components/actions/Link.tsx +78 -0
  82. package/src/components/actions/Magnetic.tsx +68 -0
  83. package/src/components/actions/Toggle.test.tsx +40 -0
  84. package/src/components/actions/Toggle.tsx +47 -0
  85. package/src/components/actions/ToggleGroup.tsx +70 -0
  86. package/src/components/actions/index.ts +5 -0
  87. package/src/components/backgrounds/FaultyTerminal.tsx +426 -0
  88. package/src/components/backgrounds/OrbBackground.tsx +424 -0
  89. package/src/components/backgrounds/WarpBackground.tsx +358 -0
  90. package/src/components/backgrounds/index.ts +3 -0
  91. package/src/components/blocks/Hero.tsx +142 -0
  92. package/src/components/blocks/social/OpenGraphCard.tsx +243 -0
  93. package/src/components/cursor/SplashCursor.tsx +1315 -0
  94. package/src/components/cursor/TargetCursor.tsx +187 -0
  95. package/src/components/cursor/index.ts +2 -0
  96. package/src/components/data-display/AspectImage.tsx +73 -0
  97. package/src/components/data-display/Avatar.test.tsx +35 -0
  98. package/src/components/data-display/Avatar.tsx +55 -0
  99. package/src/components/data-display/Badge.test.tsx +43 -0
  100. package/src/components/data-display/Badge.tsx +84 -0
  101. package/src/components/data-display/Brand.tsx +123 -0
  102. package/src/components/data-display/Calendar.tsx +70 -0
  103. package/src/components/data-display/Card.test.tsx +92 -0
  104. package/src/components/data-display/Card.tsx +115 -0
  105. package/src/components/data-display/Code.tsx +210 -0
  106. package/src/components/data-display/CollapsibleCodeBlock.tsx +238 -0
  107. package/src/components/data-display/DataTable.tsx +119 -0
  108. package/src/components/data-display/DescriptionList.tsx +41 -0
  109. package/src/components/data-display/GitHubIcon.tsx +44 -0
  110. package/src/components/data-display/Heading.test.tsx +36 -0
  111. package/src/components/data-display/Heading.tsx +83 -0
  112. package/src/components/data-display/StatCard.tsx +195 -0
  113. package/src/components/data-display/Table.tsx +133 -0
  114. package/src/components/data-display/Text.test.tsx +48 -0
  115. package/src/components/data-display/Text.tsx +144 -0
  116. package/src/components/data-display/Timeline.tsx +194 -0
  117. package/src/components/data-display/TreeView.tsx +226 -0
  118. package/src/components/data-display/Typewriter.tsx +119 -0
  119. package/src/components/data-display/VariableWeightText.tsx +130 -0
  120. package/src/components/data-display/index.ts +19 -0
  121. package/src/components/feedback/Alert.test.tsx +44 -0
  122. package/src/components/feedback/Alert.tsx +65 -0
  123. package/src/components/feedback/EmptyState.tsx +113 -0
  124. package/src/components/feedback/Progress.test.tsx +60 -0
  125. package/src/components/feedback/Progress.tsx +30 -0
  126. package/src/components/feedback/ProgressBar.tsx +158 -0
  127. package/src/components/feedback/Skeleton.test.tsx +39 -0
  128. package/src/components/feedback/Skeleton.tsx +45 -0
  129. package/src/components/feedback/Sonner.tsx +28 -0
  130. package/src/components/feedback/Spinner.test.tsx +33 -0
  131. package/src/components/feedback/Spinner.tsx +99 -0
  132. package/src/components/feedback/Stepper.tsx +307 -0
  133. package/src/components/feedback/Toast/Toast.tsx +243 -0
  134. package/src/components/feedback/Toast/index.ts +2 -0
  135. package/src/components/feedback/index.ts +9 -0
  136. package/src/components/forms/Checkbox.test.tsx +40 -0
  137. package/src/components/forms/Checkbox.tsx +31 -0
  138. package/src/components/forms/ColorPicker.tsx +118 -0
  139. package/src/components/forms/Combobox.tsx +96 -0
  140. package/src/components/forms/DragDrop.tsx +440 -0
  141. package/src/components/forms/FileUpload.tsx +252 -0
  142. package/src/components/forms/FilterButton.tsx +65 -0
  143. package/src/components/forms/Form.tsx +197 -0
  144. package/src/components/forms/Input.test.tsx +46 -0
  145. package/src/components/forms/Input.tsx +43 -0
  146. package/src/components/forms/InputOTP.tsx +81 -0
  147. package/src/components/forms/Label.test.tsx +20 -0
  148. package/src/components/forms/Label.tsx +25 -0
  149. package/src/components/forms/RadioGroup.tsx +51 -0
  150. package/src/components/forms/SearchBar.tsx +215 -0
  151. package/src/components/forms/Select.test.tsx +118 -0
  152. package/src/components/forms/Select.tsx +274 -0
  153. package/src/components/forms/Slider.tsx +29 -0
  154. package/src/components/forms/Switch.test.tsx +76 -0
  155. package/src/components/forms/Switch.tsx +30 -0
  156. package/src/components/forms/TextField.tsx +152 -0
  157. package/src/components/forms/Textarea.test.tsx +41 -0
  158. package/src/components/forms/Textarea.tsx +29 -0
  159. package/src/components/forms/ThemeSwitcher.tsx +290 -0
  160. package/src/components/forms/ThemeToggle.tsx +151 -0
  161. package/src/components/forms/index.ts +19 -0
  162. package/src/components/layout/Accordion.test.tsx +66 -0
  163. package/src/components/layout/Accordion.tsx +64 -0
  164. package/src/components/layout/AspectRatio.tsx +7 -0
  165. package/src/components/layout/Carousel.tsx +277 -0
  166. package/src/components/layout/Collapsible.test.tsx +40 -0
  167. package/src/components/layout/Collapsible.tsx +31 -0
  168. package/src/components/layout/Container.test.tsx +45 -0
  169. package/src/components/layout/Container.tsx +99 -0
  170. package/src/components/layout/CustomizerPanel.tsx +400 -0
  171. package/src/components/layout/DatePicker.tsx +57 -0
  172. package/src/components/layout/Footer/Footer.tsx +175 -0
  173. package/src/components/layout/Footer/index.ts +2 -0
  174. package/src/components/layout/GlassSurface.tsx +82 -0
  175. package/src/components/layout/Grid.test.tsx +31 -0
  176. package/src/components/layout/Grid.tsx +130 -0
  177. package/src/components/layout/Header/Header.tsx +450 -0
  178. package/src/components/layout/Header/index.ts +2 -0
  179. package/src/components/layout/PageLayout.tsx +180 -0
  180. package/src/components/layout/PageTemplate.tsx +158 -0
  181. package/src/components/layout/Resizable.tsx +48 -0
  182. package/src/components/layout/ScrollArea.tsx +53 -0
  183. package/src/components/layout/Separator.test.tsx +28 -0
  184. package/src/components/layout/Separator.tsx +29 -0
  185. package/src/components/layout/Sidebar.tsx +171 -0
  186. package/src/components/layout/Stack.test.tsx +41 -0
  187. package/src/components/layout/Stack.tsx +89 -0
  188. package/src/components/layout/glass-surface.css +60 -0
  189. package/src/components/layout/index.ts +18 -0
  190. package/src/components/motion/AnimatedBeam.tsx +159 -0
  191. package/src/components/navigation/Breadcrumb.test.tsx +57 -0
  192. package/src/components/navigation/Breadcrumb.tsx +119 -0
  193. package/src/components/navigation/Breadcrumbs.tsx +221 -0
  194. package/src/components/navigation/Command.tsx +159 -0
  195. package/src/components/navigation/Menubar.tsx +115 -0
  196. package/src/components/navigation/NavLink.tsx +55 -0
  197. package/src/components/navigation/NavigationMenu.tsx +125 -0
  198. package/src/components/navigation/Pagination.tsx +121 -0
  199. package/src/components/navigation/SecondaryNav.tsx +100 -0
  200. package/src/components/navigation/Tabs.test.tsx +47 -0
  201. package/src/components/navigation/Tabs.tsx +60 -0
  202. package/src/components/navigation/TertiaryNav.tsx +90 -0
  203. package/src/components/navigation/index.ts +10 -0
  204. package/src/components/overlays/AlertDialog.test.tsx +69 -0
  205. package/src/components/overlays/AlertDialog.tsx +166 -0
  206. package/src/components/overlays/ContextMenu.tsx +243 -0
  207. package/src/components/overlays/Dialog.test.tsx +79 -0
  208. package/src/components/overlays/Dialog.tsx +158 -0
  209. package/src/components/overlays/Drawer.tsx +128 -0
  210. package/src/components/overlays/Dropdown.tsx +253 -0
  211. package/src/components/overlays/DropdownMenu.tsx +242 -0
  212. package/src/components/overlays/HoverCard.tsx +32 -0
  213. package/src/components/overlays/Modal.tsx +250 -0
  214. package/src/components/overlays/NotificationCenter.tsx +364 -0
  215. package/src/components/overlays/Popover.test.tsx +40 -0
  216. package/src/components/overlays/Popover.tsx +46 -0
  217. package/src/components/overlays/Sheet.tsx +163 -0
  218. package/src/components/overlays/Tooltip.test.tsx +33 -0
  219. package/src/components/overlays/Tooltip.tsx +32 -0
  220. package/src/components/overlays/index.ts +12 -0
  221. package/src/dates.ts +2 -0
  222. package/src/dnd.ts +1 -0
  223. package/src/forms.ts +1 -0
  224. package/src/globals.css +187 -0
  225. package/src/hooks/index.ts +6 -0
  226. package/src/hooks/useForm.ts +247 -0
  227. package/src/hooks/useMotionPreference.test.ts +102 -0
  228. package/src/hooks/useMotionPreference.ts +78 -0
  229. package/src/hooks/useTheme.ts +58 -0
  230. package/src/hooks.ts +9 -0
  231. package/src/index.ts +168 -0
  232. package/src/lib/animations.ts +356 -0
  233. package/src/lib/breadcrumbs.ts +94 -0
  234. package/src/lib/colors.ts +493 -0
  235. package/src/lib/store/customizer.ts +482 -0
  236. package/src/lib/store/index.ts +3 -0
  237. package/src/lib/store/theme.ts +55 -0
  238. package/src/lib/syntax-parser/index.ts +50 -0
  239. package/src/lib/syntax-parser/patterns.ts +64 -0
  240. package/src/lib/syntax-parser/tokenizer.ts +117 -0
  241. package/src/lib/syntax-parser/types.ts +27 -0
  242. package/src/lib/utils.ts +6 -0
  243. package/src/lib/validation.ts +204 -0
  244. package/src/lib/webgl/Color.ts +11 -0
  245. package/src/lib/webgl/Mesh.ts +41 -0
  246. package/src/lib/webgl/Program.ts +118 -0
  247. package/src/lib/webgl/Renderer.ts +51 -0
  248. package/src/lib/webgl/Triangle.ts +27 -0
  249. package/src/lib/webgl/Vec3.ts +18 -0
  250. package/src/lib/webgl/index.ts +13 -0
  251. package/src/nativewind-env.d.ts +1 -0
  252. package/src/providers/ThemeProvider.tsx +461 -0
  253. package/src/providers/index.ts +1 -0
  254. package/src/providers.ts +7 -0
  255. package/src/tables.ts +1 -0
  256. package/src/test/setup.ts +39 -0
  257. package/src/theme.css +158 -0
  258. package/src/tokens.ts +7 -0
  259. package/src/utils.ts +12 -0
  260. package/src/webgl.ts +1 -0
package/src/dates.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './components/data-display/Calendar';
2
+ export * from './components/layout/DatePicker';
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';
@@ -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
+ }
package/src/hooks.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Hooks subpath export
3
+ * Allows: import { ... } from '@thesage/ui/hooks'
4
+ */
5
+
6
+ // Re-export all hooks
7
+ export * from './hooks/useTheme';
8
+ export * from './hooks/useMotionPreference';
9
+ export * from './hooks/useForm';