@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,450 @@
1
+ 'use client';;
2
+ import React, { useState, useEffect } from 'react';
3
+ import { useMotionPreference } from '../../../hooks/useMotionPreference';
4
+ import { NavLink } from '../../navigation/NavLink';
5
+ import { Menu, X, ChevronDown } from 'lucide-react';
6
+
7
+ export interface HeaderNavLink {
8
+ label: string;
9
+ href?: string;
10
+ /**
11
+ * Whether this link represents the current/active page
12
+ * @default false
13
+ */
14
+ active?: boolean;
15
+ /**
16
+ * Nested links for dropdown menus
17
+ */
18
+ children?: Array<{
19
+ label: string;
20
+ href: string;
21
+ active?: boolean;
22
+ }>;
23
+ }
24
+
25
+ export interface HeaderProps {
26
+ /**
27
+ * Brand/logo element or text
28
+ */
29
+ logo?: React.ReactNode;
30
+ /**
31
+ * Array of navigation links
32
+ */
33
+ navLinks?: HeaderNavLink[];
34
+ /**
35
+ * Content for the right side (e.g., Sign In, CTA buttons)
36
+ */
37
+ actions?: React.ReactNode;
38
+ /**
39
+ * Whether to apply glass morphism effect on scroll
40
+ * @default true
41
+ */
42
+ glassOnScroll?: boolean;
43
+ /**
44
+ * Scroll threshold in pixels before applying glass effect
45
+ * @default 10
46
+ */
47
+ scrollThreshold?: number;
48
+ /**
49
+ * Whether the header is sticky (fixed position)
50
+ * @default true
51
+ */
52
+ sticky?: boolean;
53
+ /**
54
+ * Font size for desktop navigation links
55
+ * @default 'text-sm' (14px)
56
+ */
57
+ navLinkSize?: 'text-xs' | 'text-sm' | 'text-base' | 'text-lg';
58
+ /**
59
+ * Font family for navigation links
60
+ * Uses CSS variable --font-header-nav by default
61
+ * Logo font is controlled by the logo ReactNode itself or --font-header-logo
62
+ * @default 'var(--font-header-nav)'
63
+ */
64
+ fontFamily?: string;
65
+ /**
66
+ * Maximum width for header content
67
+ * @default 'max-w-7xl' (1280px)
68
+ */
69
+ maxWidth?: 'max-w-7xl' | 'max-w-[1440px]' | 'max-w-4xl';
70
+ /**
71
+ * Alignment of the navigation links
72
+ * @default 'center'
73
+ */
74
+ navAlignment?: 'center' | 'left' | 'right';
75
+ /**
76
+ * Additional className for customization
77
+ */
78
+ className?: string;
79
+ }
80
+
81
+ export const Header = (
82
+ {
83
+ ref,
84
+ logo,
85
+ navLinks = [],
86
+ actions,
87
+ glassOnScroll = true,
88
+ scrollThreshold = 10,
89
+ sticky = true,
90
+ navLinkSize = 'text-sm',
91
+ navAlignment = 'center',
92
+ fontFamily = 'var(--font-header-nav)',
93
+ maxWidth = 'max-w-7xl',
94
+ className = ''
95
+ }: HeaderProps & {
96
+ ref?: React.Ref<HTMLElement>;
97
+ }
98
+ ) => {
99
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
100
+ const [hasScrolled, setHasScrolled] = useState(false);
101
+ const [openDropdown, setOpenDropdown] = useState<string | null>(null);
102
+ const [expandedMobileSection, setExpandedMobileSection] = useState<string | null>(null);
103
+ const { shouldAnimate, scale } = useMotionPreference();
104
+
105
+ // Calculate motion factors
106
+ const motionFactor = shouldAnimate && scale > 0 ? (5 / scale) : 0;
107
+ const transitionDuration = `${300 * motionFactor}ms`;
108
+
109
+ // Handle scroll detection
110
+ useEffect(() => {
111
+ if (!glassOnScroll) return;
112
+
113
+ const handleScroll = () => {
114
+ setHasScrolled(window.scrollY > scrollThreshold);
115
+ };
116
+
117
+ window.addEventListener('scroll', handleScroll, { passive: true });
118
+ return () => window.removeEventListener('scroll', handleScroll);
119
+ }, [glassOnScroll, scrollThreshold]);
120
+
121
+ // Lock body scroll when mobile menu is open
122
+ useEffect(() => {
123
+ if (isMenuOpen) {
124
+ document.body.style.overflow = 'hidden';
125
+ } else {
126
+ document.body.style.overflow = '';
127
+ }
128
+ return () => {
129
+ document.body.style.overflow = '';
130
+ };
131
+ }, [isMenuOpen]);
132
+
133
+ const baseStyles = 'top-0 left-0 right-0 z-50';
134
+ const positionStyles = sticky ? 'fixed' : 'relative';
135
+ const transitionStyles = shouldAnimate ? 'transition-all' : '';
136
+
137
+ // Liquid Glass Effect
138
+ // Unscrolled: Transparent & Borderless (looks printed on background), but with blur for "liquid" feel over Orb
139
+ // Scrolled: Wetter glass, more opaque, shadow for depth, no harsh borders
140
+ const backgroundStyles = hasScrolled && glassOnScroll
141
+ ? 'backdrop-blur-3xl bg-[var(--color-surface)]/60 border-b border-transparent shadow-xs supports-[backdrop-filter]:bg-[var(--color-surface)]/50'
142
+ : 'bg-transparent border-b border-transparent backdrop-blur-xl';
143
+
144
+ // Nav Alignment Classes
145
+ const getNavClasses = () => {
146
+ switch (navAlignment) {
147
+ case 'left':
148
+ return 'ml-8 mr-auto';
149
+ case 'right':
150
+ return 'ml-auto mr-8';
151
+ case 'center':
152
+ default:
153
+ return 'absolute left-1/2 -translate-x-1/2';
154
+ }
155
+ };
156
+
157
+ return (
158
+ <>
159
+ <header
160
+ ref={ref}
161
+ className={`${baseStyles} ${positionStyles} ${transitionStyles} ${backgroundStyles} ${className}`}
162
+ style={{ transitionDuration }}
163
+ >
164
+ <div className={`${maxWidth} mx-auto px-4 sm:px-6 lg:px-8`}>
165
+ <div className="flex items-center justify-between h-16 lg:h-20 relative">
166
+ {/* Logo */}
167
+ {logo && (
168
+ <div className="flex-shrink-0 z-10">
169
+ {logo}
170
+ </div>
171
+ )}
172
+
173
+ {/* Desktop Navigation */}
174
+ {navLinks.length > 0 && (
175
+ <nav
176
+ className={`hidden lg:flex items-center gap-8 ${getNavClasses()}`}
177
+ aria-label="Main navigation"
178
+ >
179
+ {navLinks.map((link) => {
180
+ const hasDropdown = link.children && link.children.length > 0;
181
+ const isOpen = openDropdown === link.label;
182
+
183
+ if (hasDropdown) {
184
+ return (
185
+ <div
186
+ key={link.label}
187
+ className="relative group"
188
+ onMouseEnter={() => setOpenDropdown(link.label)}
189
+ onMouseLeave={() => setOpenDropdown(null)}
190
+ >
191
+ <button
192
+ className={`
193
+ ${navLinkSize}
194
+ relative
195
+ pb-1
196
+ flex items-center gap-1
197
+ focus-visible:outline
198
+ focus-visible:outline-2
199
+ focus-visible:outline-offset-4
200
+ focus-visible:outline-[var(--color-focus)]
201
+ rounded-xs
202
+ ${shouldAnimate ? 'transition-colors' : ''}
203
+ ${link.active
204
+ ? 'text-[var(--color-text-primary)] font-medium after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-[var(--color-primary)] after:rounded-full'
205
+ : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'
206
+ }
207
+ `}
208
+ style={{ fontFamily, transitionDuration }}
209
+ aria-expanded={isOpen}
210
+ aria-haspopup="true"
211
+ >
212
+ {link.label}
213
+ <ChevronDown className={`w-3 h-3 ${shouldAnimate ? 'transition-transform' : ''} ${isOpen ? 'rotate-180' : ''}`} style={{ transitionDuration }} />
214
+ </button>
215
+ {/* Invisible bridge to prevent dropdown from closing */}
216
+ {isOpen && <div className="absolute top-full left-1/2 -translate-x-1/2 w-[200px] h-2" />}
217
+ {isOpen && (
218
+ <div className={`
219
+ absolute top-full left-1/2 -translate-x-1/2 mt-2 min-w-[200px] z-50
220
+ bg-[var(--color-surface)] border border-[var(--color-border)]
221
+ rounded-lg shadow-xl py-1 p-1
222
+ backdrop-blur-3xl bg-[var(--color-surface)]/95
223
+ ${shouldAnimate ? 'animate-fade-in' : ''}
224
+ `} style={{ animationDuration: `${0.2 * motionFactor}s` }}>
225
+ {link.children?.map((child) => (
226
+ <NavLink
227
+ key={child.label}
228
+ href={child.href}
229
+ active={child.active}
230
+ variant="pill"
231
+ className="w-full"
232
+ >
233
+ {child.label}
234
+ </NavLink>
235
+ ))}
236
+ </div>
237
+ )}
238
+ </div>
239
+ );
240
+ }
241
+
242
+ return (
243
+ <NavLink
244
+ key={link.label}
245
+ href={link.href}
246
+ active={link.active}
247
+ variant="minimal"
248
+ className={navLinkSize}
249
+ style={{ fontFamily }}
250
+ >
251
+ {link.label}
252
+ </NavLink>
253
+ );
254
+ })}
255
+ </nav>
256
+ )}
257
+
258
+ {/* Desktop Actions */}
259
+ {actions && (
260
+ <div className="hidden lg:flex items-center gap-4 z-10">
261
+ {actions}
262
+ </div>
263
+ )}
264
+
265
+ {/* Mobile Menu Button */}
266
+ <button
267
+ onClick={() => setIsMenuOpen(!isMenuOpen)}
268
+ className={`
269
+ lg:hidden
270
+ p-2
271
+ text-[var(--color-text-primary)]
272
+ hover:bg-[var(--color-surface)]
273
+ rounded-lg
274
+ focus-visible:outline
275
+ focus-visible:outline-2
276
+ focus-visible:outline-offset-2
277
+ focus-visible:outline-[var(--color-focus)]
278
+ ${shouldAnimate ? 'transition-colors' : ''}
279
+ `}
280
+ style={{ transitionDuration }}
281
+ aria-label={isMenuOpen ? 'Close menu' : 'Open menu'}
282
+ aria-expanded={isMenuOpen}
283
+ >
284
+ {isMenuOpen ? (
285
+ <X className="w-6 h-6" />
286
+ ) : (
287
+ <Menu className="w-6 h-6" />
288
+ )}
289
+ </button>
290
+ </div>
291
+ </div>
292
+ </header>
293
+
294
+ {/* Mobile Full-Screen Menu */}
295
+ <div
296
+ className={`
297
+ fixed inset-0 z-[100] lg:hidden
298
+ ${shouldAnimate ? 'transition-all' : ''}
299
+ ${isMenuOpen
300
+ ? 'opacity-100 pointer-events-auto'
301
+ : 'opacity-0 pointer-events-none'
302
+ }
303
+ `}
304
+ style={{ transitionDuration }}
305
+ aria-hidden={!isMenuOpen}
306
+ >
307
+ <div className="absolute inset-0 bg-[var(--color-background)]">
308
+ <div className="flex flex-col items-center justify-center h-full gap-8 px-4">
309
+ {/* Mobile Navigation Links */}
310
+ {navLinks.map((link, index) => {
311
+ const hasDropdown = link.children && link.children.length > 0;
312
+ const isExpanded = expandedMobileSection === link.label;
313
+
314
+ if (hasDropdown) {
315
+ return (
316
+ <div key={link.label} className="w-full max-w-xs">
317
+ <button
318
+ onClick={() => setExpandedMobileSection(isExpanded ? null : link.label)}
319
+ className={`
320
+ text-3xl w-full text-center
321
+ focus-visible:outline
322
+ focus-visible:outline-2
323
+ focus-visible:outline-offset-4
324
+ focus-visible:outline-[var(--color-focus)]
325
+ rounded-xs
326
+ ${shouldAnimate ? 'transition-all' : ''}
327
+ ${link.active
328
+ ? 'text-[var(--color-primary)] font-semibold'
329
+ : 'text-[var(--color-text-primary)] hover:text-[var(--color-text-secondary)]'
330
+ }
331
+ `}
332
+ style={
333
+ shouldAnimate && isMenuOpen
334
+ ? {
335
+ animation: `fadeInUp ${0.5 * motionFactor}s ease-out ${index * 0.1 * motionFactor}s forwards`,
336
+ opacity: 0,
337
+ fontFamily,
338
+ transitionDuration
339
+ }
340
+ : { opacity: 1, fontFamily }
341
+ }
342
+ aria-expanded={isExpanded}
343
+ >
344
+ {link.label}
345
+ </button>
346
+ {isExpanded && (
347
+ <div className="flex flex-col gap-3 mt-4">
348
+ {link.children?.map((child) => (
349
+ <a
350
+ key={child.label}
351
+ href={child.href}
352
+ onClick={() => setIsMenuOpen(false)}
353
+ className={`
354
+ text-xl text-center block
355
+ focus-visible:outline
356
+ focus-visible:outline-2
357
+ focus-visible:outline-offset-4
358
+ focus-visible:outline-[var(--color-focus)]
359
+ rounded-xs
360
+ ${shouldAnimate ? 'transition-colors' : ''}
361
+ ${child.active
362
+ ? 'text-[var(--color-primary)] font-medium'
363
+ : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'
364
+ }
365
+ `}
366
+ style={{ transitionDuration }}
367
+ aria-current={child.active ? 'page' : undefined}
368
+ >
369
+ {child.label}
370
+ </a>
371
+ ))}
372
+ </div>
373
+ )}
374
+ </div>
375
+ );
376
+ }
377
+
378
+ return (
379
+ <a
380
+ key={link.label}
381
+ href={link.href}
382
+ onClick={() => setIsMenuOpen(false)}
383
+ aria-current={link.active ? 'page' : undefined}
384
+ className={`
385
+ text-3xl
386
+ focus-visible:outline
387
+ focus-visible:outline-2
388
+ focus-visible:outline-offset-4
389
+ focus-visible:outline-[var(--color-focus)]
390
+ rounded-xs
391
+ ${shouldAnimate ? 'transition-all' : ''}
392
+ ${link.active
393
+ ? 'text-[var(--color-primary)] font-semibold'
394
+ : 'text-[var(--color-text-primary)] hover:text-[var(--color-text-secondary)]'
395
+ }
396
+ `}
397
+ style={
398
+ shouldAnimate && isMenuOpen
399
+ ? {
400
+ animation: `fadeInUp ${0.5 * motionFactor}s ease-out ${index * 0.1 * motionFactor}s forwards`,
401
+ opacity: 0,
402
+ fontFamily,
403
+ transitionDuration
404
+ }
405
+ : { opacity: 1, fontFamily }
406
+ }
407
+ >
408
+ {link.label}
409
+ </a>
410
+ );
411
+ })}
412
+
413
+ {/* Mobile Actions */}
414
+ {actions && (
415
+ <div
416
+ className="flex flex-col gap-4 mt-8 w-full max-w-xs"
417
+ style={
418
+ shouldAnimate && isMenuOpen
419
+ ? {
420
+ animation: `fadeInUp ${0.5 * motionFactor}s ease-out ${navLinks.length * 0.1 * motionFactor}s forwards`,
421
+ opacity: 0,
422
+ }
423
+ : { opacity: 1 }
424
+ }
425
+ >
426
+ {actions}
427
+ </div>
428
+ )}
429
+ </div>
430
+ </div>
431
+ </div>
432
+
433
+ {/* Animation keyframes - only added if motion is enabled */}
434
+ {shouldAnimate && (
435
+ <style>{`
436
+ @keyframes fadeInUp {
437
+ from {
438
+ opacity: 0;
439
+ transform: translateY(20px);
440
+ }
441
+ to {
442
+ opacity: 1;
443
+ transform: translateY(0);
444
+ }
445
+ }
446
+ `}</style>
447
+ )}
448
+ </>
449
+ );
450
+ };
@@ -0,0 +1,2 @@
1
+ export { Header } from './Header';
2
+ export type { HeaderProps, HeaderNavLink } from './Header';
@@ -0,0 +1,180 @@
1
+ import React from 'react';
2
+
3
+ export interface PageLayoutProps {
4
+ /** Optional header configuration */
5
+ header?: React.ReactNode;
6
+
7
+ /** Whether the header is sticky (adds top padding to first content element) */
8
+ stickyHeader?: boolean;
9
+
10
+ /** Optional breadcrumbs */
11
+ breadcrumbs?: React.ReactNode;
12
+
13
+ /** Breadcrumbs position: 'top' (sticky below header) or 'below-title' (static below title+subtitle) */
14
+ breadcrumbsPosition?: 'top' | 'below-title';
15
+
16
+ /** Optional page title - rendered in content-width container */
17
+ title?: React.ReactNode;
18
+
19
+ /** Optional page subtitle - rendered below title */
20
+ subtitle?: React.ReactNode;
21
+
22
+ /** Apply Swiss Grid Design spacing to title/subtitle area */
23
+ swissGridSpacing?: boolean;
24
+
25
+ /** Maximum width for title/subtitle area - should match content width for alignment */
26
+ contentMaxWidth?: 'max-w-7xl' | 'max-w-[1440px]' | 'max-w-4xl';
27
+
28
+ /** Optional secondary navigation (first stack) */
29
+ secondaryNav?: React.ReactNode;
30
+
31
+ /** Optional tertiary navigation (second stack) */
32
+ tertiaryNav?: React.ReactNode;
33
+
34
+ /** Optional footer */
35
+ footer?: React.ReactNode;
36
+
37
+ /** Main content */
38
+ children: React.ReactNode;
39
+
40
+ /** Optional className for main content */
41
+ className?: string;
42
+ }
43
+
44
+ /**
45
+ * PageLayout Component
46
+ *
47
+ * A flexible layout organism that composes Header, Breadcrumbs, SecondaryNav,
48
+ * TertiaryNav, and Footer with automatic z-index and sticky positioning management.
49
+ *
50
+ * Features:
51
+ * - Automatic z-index stacking (50 → 45 → 40 → 30)
52
+ * - Dynamic sticky positioning calculations
53
+ * - Optional title/subtitle slots with Swiss Grid spacing
54
+ * - Flexible breadcrumb positioning (sticky top or static below title)
55
+ * - Optional composition (all props optional)
56
+ * - Handles full-height layouts
57
+ * - Theme-aware styling
58
+ *
59
+ * Z-Index Stack:
60
+ * - Header: z-50, h-16 lg:h-20
61
+ * - Breadcrumbs (if position='top'): z-45, sticky below header
62
+ * - SecondaryNav: z-40, first navigation stack
63
+ * - TertiaryNav: z-30, second navigation stack
64
+ *
65
+ * Swiss Grid Design:
66
+ * - Title/subtitle area uses structured spacing (48-96px sections)
67
+ * - Typography hierarchy: text-4xl/5xl title, text-lg subtitle
68
+ * - Content-width container (max-w-7xl) for proper alignment
69
+ *
70
+ * Example:
71
+ * ```tsx
72
+ * <PageLayout
73
+ * header={<Header logo={logo} navLinks={links} />}
74
+ * title={<h1>Page Title</h1>}
75
+ * subtitle={<p>Page subtitle</p>}
76
+ * breadcrumbs={<Breadcrumbs items={breadcrumbItems} />}
77
+ * breadcrumbsPosition="below-title"
78
+ * swissGridSpacing
79
+ * secondaryNav={<SecondaryNav items={sections} />}
80
+ * >
81
+ * <article>Your content here</article>
82
+ * </PageLayout>
83
+ * ```
84
+ */
85
+ export function PageLayout({
86
+ header,
87
+ stickyHeader = false,
88
+ breadcrumbs,
89
+ breadcrumbsPosition = 'top',
90
+ title,
91
+ subtitle,
92
+ swissGridSpacing = false,
93
+ contentMaxWidth = 'max-w-7xl',
94
+ secondaryNav,
95
+ tertiaryNav,
96
+ footer,
97
+ children,
98
+ className = '',
99
+ }: PageLayoutProps) {
100
+ // Determine if breadcrumbs should be at the top (sticky) or below title (static)
101
+ const showBreadcrumbsAtTop = breadcrumbsPosition === 'top';
102
+ const showBreadcrumbsBelowTitle = breadcrumbsPosition === 'below-title';
103
+
104
+ // Sticky header spacing - add top padding to first content element
105
+ const stickyHeaderSpacing = stickyHeader ? 'pt-16 lg:pt-20' : '';
106
+
107
+ // Swiss Grid spacing classes
108
+ // When breadcrumbs are below title, reduce bottom padding on title area to avoid excessive space
109
+ const titleAreaTopSpacing = swissGridSpacing ? 'pt-12 lg:pt-16' : 'pt-8';
110
+ const titleAreaBottomSpacing = swissGridSpacing && showBreadcrumbsBelowTitle ? 'pb-3' : swissGridSpacing ? 'pb-12 lg:pb-16' : 'pb-8';
111
+ const titleBottomMargin = swissGridSpacing ? 'mb-4' : 'mb-3';
112
+ const breadcrumbsAreaSpacing = swissGridSpacing ? 'pt-4 pb-8' : 'pt-3 pb-6';
113
+
114
+ return (
115
+ <div className="min-h-screen flex flex-col w-full min-w-0">
116
+ {/* Header - z-50, h-16 lg:h-20 */}
117
+ {header}
118
+
119
+ {/* Breadcrumbs - z-45, sticky below header (only if position='top') */}
120
+ {breadcrumbs && showBreadcrumbsAtTop && (
121
+ <div
122
+ className={`
123
+ sticky bg-[var(--color-background)]/95 backdrop-blur-xs
124
+ border-b border-[var(--color-border)]
125
+ transition-all duration-300
126
+ top-16 lg:top-20
127
+ ${stickyHeaderSpacing}
128
+ `}
129
+ style={{ zIndex: 45 }}
130
+ >
131
+ <div className={`${contentMaxWidth} mx-auto px-4 sm:px-6 lg:px-8 py-3`}>
132
+ {breadcrumbs}
133
+ </div>
134
+ </div>
135
+ )}
136
+
137
+ {/* Title/Subtitle Area - Swiss Grid Design */}
138
+ {(title || subtitle) && (
139
+ <div className={`${titleAreaTopSpacing} ${titleAreaBottomSpacing} ${!showBreadcrumbsAtTop ? stickyHeaderSpacing : ''} bg-[var(--color-background)]`}>
140
+ <div className={`${contentMaxWidth} mx-auto px-4 sm:px-6 lg:px-8`}>
141
+ {/* Title */}
142
+ {title && (
143
+ <div className={titleBottomMargin}>
144
+ {title}
145
+ </div>
146
+ )}
147
+
148
+ {/* Subtitle */}
149
+ {subtitle && <div>{subtitle}</div>}
150
+ </div>
151
+ </div>
152
+ )}
153
+
154
+ {/* Breadcrumbs below title+subtitle (only if position='below-title') */}
155
+ {breadcrumbs && showBreadcrumbsBelowTitle && (
156
+ <div className={`${breadcrumbsAreaSpacing} bg-[var(--color-background)]`}>
157
+ <div className={`${contentMaxWidth} mx-auto px-4 sm:px-6 lg:px-8`}>
158
+ {breadcrumbs}
159
+ </div>
160
+ </div>
161
+ )}
162
+
163
+ {/* Secondary Nav - z-40, first navigation stack */}
164
+ {secondaryNav}
165
+
166
+ {/* Tertiary Nav - z-30, second navigation stack */}
167
+ {tertiaryNav}
168
+
169
+ {/* Main Content - flexible, fills remaining space */}
170
+ <main className={`flex-1 ${className}`}>
171
+ <div className={`${contentMaxWidth} mx-auto px-4 sm:px-6 lg:px-8 py-12`}>
172
+ {children}
173
+ </div>
174
+ </main>
175
+
176
+ {/* Footer */}
177
+ {footer}
178
+ </div>
179
+ );
180
+ }