@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
@@ -0,0 +1,99 @@
1
+ import React from 'react';
2
+
3
+ export interface ContainerProps {
4
+ /**
5
+ * Content to wrap
6
+ */
7
+ children: React.ReactNode;
8
+
9
+ /**
10
+ * Maximum width variant
11
+ * @default 'standard'
12
+ */
13
+ variant?: 'standard' | 'wide' | 'narrow';
14
+
15
+ /**
16
+ * Add horizontal padding
17
+ * @default true
18
+ */
19
+ padding?: boolean;
20
+
21
+ /**
22
+ * Additional className for customization
23
+ */
24
+ className?: string;
25
+
26
+ /**
27
+ * HTML element to render as
28
+ * @default 'div'
29
+ */
30
+ as?: 'div' | 'section' | 'article' | 'main' | 'aside' | 'header' | 'footer';
31
+ }
32
+
33
+ /**
34
+ * Container Component
35
+ *
36
+ * Manages consistent max-widths and horizontal padding across the design system.
37
+ * This component ensures perfect alignment between header, content, and footer
38
+ * without manually coordinating max-width classes.
39
+ *
40
+ * **Swiss Grid Integration:**
41
+ * - Uses standard max-widths that align with the 8px base unit
42
+ * - Coordinates with PageTemplate variant system
43
+ * - Provides consistent horizontal padding
44
+ *
45
+ * **Why This Component Exists:**
46
+ * Before Container, every component had hardcoded max-widths:
47
+ * - Header: max-w-[1440px]
48
+ * - SecondaryNav: max-w-7xl
49
+ * - Content: max-w-4xl
50
+ *
51
+ * This caused misalignment. Container solves this by centralizing width management.
52
+ *
53
+ * Usage:
54
+ * ```tsx
55
+ * // Standard width (1280px)
56
+ * <Container>Content</Container>
57
+ *
58
+ * // Wide width (1440px) - for dashboards, data-heavy pages
59
+ * <Container variant="wide">Dashboard</Container>
60
+ *
61
+ * // Narrow width (896px) - for reading-focused content
62
+ * <Container variant="narrow">Article</Container>
63
+ *
64
+ * // Without padding (when you need edge-to-edge content)
65
+ * <Container padding={false}>Full bleed content</Container>
66
+ *
67
+ * // As different HTML element
68
+ * <Container as="main">Main content</Container>
69
+ * ```
70
+ */
71
+ export const Container = (
72
+ {
73
+ ref,
74
+ children,
75
+ variant = 'standard',
76
+ padding = true,
77
+ className = '',
78
+ as: Component = 'div'
79
+ }: ContainerProps & {
80
+ ref?: React.Ref<HTMLElement>;
81
+ }
82
+ ) => {
83
+ const maxWidthClasses = {
84
+ standard: 'max-w-7xl', // 1280px - default for most content
85
+ wide: 'max-w-[1440px]', // 1440px - for data-heavy layouts
86
+ narrow: 'max-w-4xl', // 896px - for reading comfort
87
+ };
88
+
89
+ const paddingClasses = padding ? 'px-4 sm:px-6 lg:px-8' : '';
90
+
91
+ return React.createElement(
92
+ Component,
93
+ {
94
+ ref,
95
+ className: `${maxWidthClasses[variant]} mx-auto ${paddingClasses} ${className}`,
96
+ },
97
+ children
98
+ );
99
+ };
@@ -0,0 +1,400 @@
1
+ 'use client';
2
+ import React from 'react';
3
+ import { SlidersHorizontal, Sun, Moon, SunMoon, Building2, Leaf, Zap, Rocket, X, Palette } from 'lucide-react';
4
+ import { studioTokens, terraTokens, voltTokens, speedboatTokens, PUBLIC_THEME_NAMES } from '@thesage/tokens';
5
+ import type { ThemeName } from '@thesage/tokens';
6
+ import { useCustomizer } from '../../lib/store/customizer';
7
+ import { useThemeStore } from '../../lib/store/theme';
8
+ import { ColorPicker } from '../forms/ColorPicker';
9
+ import { Button } from '../actions/Button';
10
+
11
+ export interface CustomizerPanelProps {
12
+ /**
13
+ * Mode of the customizer:
14
+ * - "full": Shows all controls (theme, mode, motion)
15
+ * - "lightweight": Shows only light/dark mode toggle
16
+ * @default "full"
17
+ */
18
+ mode?: 'full' | 'lightweight';
19
+ /**
20
+ * Whether to show the Motion Intensity slider
21
+ * @default false
22
+ */
23
+ showMotionIntensity?: boolean;
24
+ /**
25
+ * Which themes to show in the selector.
26
+ * Defaults to public themes only (studio, terra, volt).
27
+ * Pass ['speedboat'] to lock to Speedboat, or include it
28
+ * explicitly to make it visible alongside other themes.
29
+ */
30
+ themes?: ThemeName[];
31
+ }
32
+
33
+ const allThemeOptions = [
34
+ { id: 'studio' as ThemeName, label: 'Studio', icon: <Building2 className="w-4 h-4" /> },
35
+ { id: 'terra' as ThemeName, label: 'Terra', icon: <Leaf className="w-4 h-4" /> },
36
+ { id: 'volt' as ThemeName, label: 'Volt', icon: <Zap className="w-4 h-4" /> },
37
+ { id: 'speedboat' as ThemeName, label: 'Speedboat', icon: <Rocket className="w-4 h-4" /> },
38
+ ];
39
+
40
+ export const CustomizerPanel = ({ mode = 'full', showMotionIntensity = false, themes }: CustomizerPanelProps) => {
41
+ const [mounted, setMounted] = React.useState(false);
42
+ const [isOpen, setIsOpen] = React.useState(false);
43
+ const panelRef = React.useRef<HTMLDivElement>(null);
44
+ const {
45
+ motion,
46
+ setMotion,
47
+ customizationMode,
48
+ setCustomizationMode,
49
+ applyColorPalette,
50
+ getActiveColorPalette,
51
+ resetCustomColors
52
+ } = useCustomizer();
53
+ const { theme, mode: colorMode, setTheme, setMode } = useThemeStore();
54
+
55
+ // Filter visible themes based on the themes prop
56
+ // Default: only public themes (excludes Speedboat unless explicitly included)
57
+ const visibleThemes = themes
58
+ ? allThemeOptions.filter((t) => themes.includes(t.id))
59
+ : allThemeOptions.filter((t) => (PUBLIC_THEME_NAMES as readonly string[]).includes(t.id));
60
+ const showThemeSelector = visibleThemes.length > 1;
61
+
62
+ // Helper to get default primary color for current theme/mode
63
+ const getDefaultPrimary = React.useCallback((t: string, m: string) => {
64
+ if (t === 'speedboat') return m === 'dark' ? speedboatTokens.dark.colors.primary : speedboatTokens.light.colors.primary;
65
+ if (t === 'volt') return m === 'dark' ? voltTokens.dark.colors.primary : voltTokens.light.colors.primary;
66
+ if (t === 'terra') return m === 'dark' ? terraTokens.dark.colors.primary : terraTokens.light.colors.primary;
67
+ // Studio default
68
+ return m === 'dark' ? studioTokens.dark.colors.primary : studioTokens.light.colors.primary;
69
+ }, []);
70
+
71
+ // Get current custom colors
72
+ const currentPalette = getActiveColorPalette(theme, colorMode);
73
+
74
+ // Initialize with current custom color OR default metric for the theme
75
+ const [tempPrimaryColor, setTempPrimaryColor] = React.useState(
76
+ currentPalette?.primary || getDefaultPrimary(theme, colorMode)
77
+ );
78
+ const [tempSecondaryColor, setTempSecondaryColor] = React.useState(currentPalette?.secondary || '#5a67d8');
79
+ const [tempAccentColor, setTempAccentColor] = React.useState(currentPalette?.accent || '#ff6b35');
80
+
81
+ // Update temp color when palette changes OR theme/mode changes
82
+ React.useEffect(() => {
83
+ if (currentPalette) {
84
+ setTempPrimaryColor(currentPalette.primary);
85
+ setTempSecondaryColor(currentPalette.secondary || currentPalette.primary);
86
+ setTempAccentColor(currentPalette.accent || '#ff6b35');
87
+ } else {
88
+ // Reset to default if no custom palette exists
89
+ setTempPrimaryColor(getDefaultPrimary(theme, colorMode));
90
+ }
91
+ }, [currentPalette, theme, colorMode, getDefaultPrimary]);
92
+
93
+ const handleApplyColor = () => {
94
+ // Apply all colors atomically, clearing secondary/accent in simple mode
95
+ applyColorPalette(theme, colorMode, {
96
+ primary: tempPrimaryColor,
97
+ secondary: customizationMode === 'advanced' ? tempSecondaryColor : undefined,
98
+ accent: customizationMode === 'advanced' ? tempAccentColor : undefined,
99
+ });
100
+ };
101
+
102
+ const handleResetColors = () => {
103
+ resetCustomColors(theme, colorMode);
104
+ // Will be handled by useEffect above, but for immediate feedback:
105
+ setTempPrimaryColor(getDefaultPrimary(theme, colorMode));
106
+ setTempSecondaryColor('#5a67d8');
107
+ setTempAccentColor('#ff6b35');
108
+ };
109
+
110
+ React.useEffect(() => {
111
+ setMounted(true);
112
+ }, []);
113
+
114
+ // Handle click outside to close panel
115
+ React.useEffect(() => {
116
+ if (!isOpen) return;
117
+
118
+ const handleClickOutside = (event: MouseEvent) => {
119
+ if (panelRef.current && !panelRef.current.contains(event.target as Node)) {
120
+ setIsOpen(false);
121
+ }
122
+ };
123
+
124
+ const handleKeyDown = (event: KeyboardEvent) => {
125
+ if (event.key === 'Escape') {
126
+ setIsOpen(false);
127
+ }
128
+ };
129
+
130
+ // Add small delay to prevent immediate closing when opening
131
+ const timeoutId = setTimeout(() => {
132
+ document.addEventListener('mousedown', handleClickOutside);
133
+ }, 100);
134
+ document.addEventListener('keydown', handleKeyDown);
135
+
136
+ return () => {
137
+ clearTimeout(timeoutId);
138
+ document.removeEventListener('mousedown', handleClickOutside);
139
+ document.removeEventListener('keydown', handleKeyDown);
140
+ };
141
+ }, [isOpen]);
142
+
143
+ if (!mounted) return null;
144
+
145
+ if (!isOpen) {
146
+ return (
147
+ <button
148
+ onClick={() => setIsOpen(true)}
149
+ aria-label={mode === 'lightweight' ? 'Open theme settings' : 'Open experience customizer'}
150
+ className="fixed bottom-4 right-4 bg-background text-foreground px-4 py-2 rounded-full shadow-lg border border-[var(--color-glass-border)] font-medium hover:opacity-80 transition-all z-50 flex items-center gap-2"
151
+ style={{ backdropFilter: 'var(--effect-blur-sm)' }}
152
+ >
153
+ {mode === 'lightweight' ? <SunMoon className="w-5 h-5" /> : <SlidersHorizontal className="w-5 h-5" />}
154
+ {mode === 'lightweight' ? 'Theme' : 'Customizer'}
155
+ </button>
156
+ );
157
+ }
158
+
159
+ return (
160
+ <div
161
+ ref={panelRef}
162
+ className={`
163
+ fixed bottom-4 right-4 z-50
164
+ bg-background p-6 rounded-2xl shadow-2xl border border-[var(--color-glass-border)]
165
+ text-foreground
166
+ left-4 sm:left-auto
167
+ w-auto sm:w-80
168
+ max-h-[calc(100vh-2rem)]
169
+ overflow-y-auto
170
+ `}
171
+ style={{
172
+ boxShadow: 'var(--effect-shadow-xl)',
173
+ backdropFilter: 'var(--effect-blur-md)',
174
+ backgroundColor: 'var(--color-glass)'
175
+ }}
176
+ >
177
+ <div className="flex justify-between items-center mb-6">
178
+ <h3 className="font-bold text-lg">{mode === 'lightweight' ? 'Theme Settings' : 'Experience Customizer'}</h3>
179
+ <button
180
+ onClick={() => setIsOpen(false)}
181
+ aria-label="Close customizer"
182
+ className="text-foreground opacity-60 hover:opacity-100 transition-opacity p-1"
183
+ >
184
+ <X className="w-5 h-5" aria-hidden="true" />
185
+ </button>
186
+ </div>
187
+
188
+ <div className="space-y-6">
189
+ {/* Motion Intensity Slider - Full mode only + showMotionIntensity enabled */}
190
+ {mode === 'full' && showMotionIntensity && (
191
+ <div>
192
+ <div className="flex justify-between mb-2">
193
+ <label className="text-sm font-medium opacity-80">Motion Intensity</label>
194
+ <span className="text-sm opacity-60">{motion}</span>
195
+ </div>
196
+ <input
197
+ type="range"
198
+ min="0"
199
+ max="10"
200
+ value={motion}
201
+ onChange={(e) => setMotion(Number(e.target.value))}
202
+ aria-label="Motion intensity"
203
+ aria-valuemin={0}
204
+ aria-valuemax={10}
205
+ aria-valuenow={motion}
206
+ className="w-full h-2 bg-[var(--color-surface)] rounded-lg appearance-none cursor-pointer accent-primary"
207
+ />
208
+ </div>
209
+ )}
210
+
211
+ {/* Theme Selector - Full mode only, hidden when single theme */}
212
+ {mode === 'full' && showThemeSelector && (
213
+ <div>
214
+ <label className="block text-sm font-medium opacity-80 mb-3">Theme</label>
215
+ <div className={`grid gap-2 mb-3 ${visibleThemes.length <= 3 ? 'grid-cols-3' : 'grid-cols-4'}`}>
216
+ {visibleThemes.map((t) => (
217
+ <button
218
+ key={t.id}
219
+ onClick={() => setTheme(t.id)}
220
+ aria-pressed={theme === t.id}
221
+ className={`
222
+ px-3 py-2.5 rounded-lg text-sm font-medium transition-all flex flex-col items-center gap-1 border
223
+ ${theme === t.id
224
+ ? 'shadow-md'
225
+ : 'bg-background-secondary text-foreground opacity-60 hover:opacity-100 border-[var(--color-glass-border)]'
226
+ }
227
+ `}
228
+ style={theme === t.id ? {
229
+ backgroundColor: 'var(--color-primary)',
230
+ color: 'var(--color-primary-foreground)',
231
+ borderColor: 'var(--color-primary)'
232
+ } : {}}
233
+ >
234
+ <span className="text-base">{t.icon}</span>
235
+ <span>{t.label}</span>
236
+ </button>
237
+ ))}
238
+ </div>
239
+ {/* Typography Preview */}
240
+ <div className="text-xs opacity-60 space-y-1">
241
+ <div>
242
+ <span className="font-heading">Heading:</span> {
243
+ theme === 'studio' ? 'Outfit' :
244
+ theme === 'terra' ? 'Lora' :
245
+ theme === 'speedboat' ? 'Montserrat' :
246
+ 'Space Grotesk'
247
+ }
248
+ </div>
249
+ <div>
250
+ <span className="font-body">Body:</span> {
251
+ theme === 'studio' ? 'Manrope' :
252
+ theme === 'terra' ? 'Instrument Sans' :
253
+ theme === 'speedboat' ? 'Roboto' :
254
+ 'Space Grotesk'
255
+ }
256
+ </div>
257
+ </div>
258
+ </div>
259
+ )}
260
+
261
+ {/* Mode Selector - Always visible */}
262
+ <div>
263
+ <label className="block text-sm font-medium opacity-80 mb-3">Mode</label>
264
+ <div className="grid grid-cols-2 gap-2">
265
+ {[
266
+ { id: 'light', label: 'Light', icon: <Sun className="w-4 h-4" /> },
267
+ { id: 'dark', label: 'Dark', icon: <Moon className="w-4 h-4" /> },
268
+ ].map((m) => (
269
+ <button
270
+ key={m.id}
271
+ onClick={() => setMode(m.id as any)}
272
+ aria-pressed={colorMode === m.id}
273
+ className={`
274
+ px-3 py-2.5 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2 border
275
+ ${colorMode === m.id
276
+ ? 'shadow-md'
277
+ : 'bg-background-secondary text-foreground opacity-60 hover:opacity-100 border-[var(--color-glass-border)]'
278
+ }
279
+ `}
280
+ style={colorMode === m.id ? {
281
+ backgroundColor: 'var(--color-primary)',
282
+ color: 'var(--color-primary-foreground)',
283
+ borderColor: 'var(--color-primary)'
284
+ } : {}}
285
+ >
286
+ <span>{m.icon}</span>
287
+ <span>{m.label}</span>
288
+ </button>
289
+ ))}
290
+ </div>
291
+ </div>
292
+
293
+ {/* Color Customizer - Full mode only */}
294
+ {mode === 'full' && (
295
+ <div className="pt-4 border-t border-[var(--color-border)]">
296
+ <div className="flex items-center justify-between mb-4">
297
+ <div className="flex items-center gap-2">
298
+ <Palette className="w-4 h-4 opacity-80" />
299
+ <label className="text-sm font-medium opacity-80">Color Customization</label>
300
+ </div>
301
+ {/* Mode Toggle */}
302
+ <div className="flex gap-1 bg-[var(--color-surface)] rounded-md p-0.5">
303
+ <button
304
+ onClick={() => setCustomizationMode('simple')}
305
+ aria-pressed={customizationMode === 'simple'}
306
+ className={`
307
+ px-2 py-1 text-xs rounded transition-all
308
+ ${customizationMode === 'simple'
309
+ ? 'bg-[var(--color-primary)] text-[var(--color-primary-foreground)]'
310
+ : 'opacity-60 hover:opacity-100'
311
+ }
312
+ `}
313
+ >
314
+ Simple
315
+ </button>
316
+ <button
317
+ onClick={() => setCustomizationMode('advanced')}
318
+ aria-pressed={customizationMode === 'advanced'}
319
+ className={`
320
+ px-2 py-1 text-xs rounded transition-all
321
+ ${customizationMode === 'advanced'
322
+ ? 'bg-[var(--color-primary)] text-[var(--color-primary-foreground)]'
323
+ : 'opacity-60 hover:opacity-100'
324
+ }
325
+ `}
326
+ >
327
+ Advanced
328
+ </button>
329
+ </div>
330
+ </div>
331
+
332
+ <div className="space-y-4">
333
+ {/* Primary Color */}
334
+ <div>
335
+ <label className="text-xs font-medium opacity-70 mb-2 block">Primary Color</label>
336
+ <ColorPicker
337
+ value={tempPrimaryColor}
338
+ onChange={setTempPrimaryColor}
339
+ />
340
+ </div>
341
+
342
+ {/* Secondary Color - Advanced mode only */}
343
+ {customizationMode === 'advanced' && (
344
+ <div>
345
+ <label className="text-xs font-medium opacity-70 mb-2 block">Secondary Color</label>
346
+ <ColorPicker
347
+ value={tempSecondaryColor}
348
+ onChange={setTempSecondaryColor}
349
+ />
350
+ </div>
351
+ )}
352
+
353
+ {/* Accent Color - Advanced mode only */}
354
+ {customizationMode === 'advanced' && (
355
+ <div>
356
+ <label className="text-xs font-medium opacity-70 mb-2 block">Accent Color</label>
357
+ <ColorPicker
358
+ value={tempAccentColor}
359
+ onChange={setTempAccentColor}
360
+ />
361
+ </div>
362
+ )}
363
+ </div>
364
+
365
+ {/* Action Buttons */}
366
+ <div className="flex gap-2 mt-4">
367
+ <Button
368
+ onClick={handleApplyColor}
369
+ size="sm"
370
+ className="flex-1"
371
+ disabled={currentPalette?.primary === tempPrimaryColor &&
372
+ (customizationMode === 'simple' ||
373
+ (currentPalette?.secondary === tempSecondaryColor &&
374
+ currentPalette?.accent === tempAccentColor))}
375
+ >
376
+ Apply Colors
377
+ </Button>
378
+ {currentPalette && (
379
+ <Button
380
+ onClick={handleResetColors}
381
+ variant="outline"
382
+ size="sm"
383
+ >
384
+ Reset
385
+ </Button>
386
+ )}
387
+ </div>
388
+
389
+ {/* Status Indicator */}
390
+ {currentPalette && (
391
+ <p className="text-xs opacity-60 mt-2">
392
+ Custom colors active for {theme} {colorMode} mode
393
+ </p>
394
+ )}
395
+ </div>
396
+ )}
397
+ </div>
398
+ </div>
399
+ );
400
+ };
@@ -0,0 +1,57 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { format } from "date-fns"
5
+ import { Calendar as CalendarIcon } from "lucide-react"
6
+
7
+ import { cn } from "../../lib/utils"
8
+ import { Button } from "../actions/Button"
9
+ import { Calendar } from "../data-display/Calendar"
10
+ import {
11
+ Popover,
12
+ PopoverContent,
13
+ PopoverTrigger,
14
+ } from "../overlays/Popover"
15
+
16
+ export interface DatePickerProps {
17
+ date?: Date
18
+ onDateChange?: (date: Date | undefined) => void
19
+ placeholder?: string
20
+ className?: string
21
+ disabled?: boolean
22
+ }
23
+
24
+ export function DatePicker({
25
+ date,
26
+ onDateChange,
27
+ placeholder = "Pick a date",
28
+ className,
29
+ disabled = false,
30
+ }: DatePickerProps) {
31
+ return (
32
+ <Popover>
33
+ <PopoverTrigger asChild>
34
+ <Button
35
+ variant="outline"
36
+ className={cn(
37
+ "w-[280px] justify-start text-left font-normal",
38
+ !date && "text-muted-foreground",
39
+ className
40
+ )}
41
+ disabled={disabled}
42
+ >
43
+ <CalendarIcon className="mr-2 h-4 w-4" />
44
+ {date ? format(date, "PPP") : <span>{placeholder}</span>}
45
+ </Button>
46
+ </PopoverTrigger>
47
+ <PopoverContent className="w-auto p-0">
48
+ <Calendar
49
+ mode="single"
50
+ selected={date}
51
+ onSelect={onDateChange}
52
+ initialFocus
53
+ />
54
+ </PopoverContent>
55
+ </Popover>
56
+ )
57
+ }