@opencosmos/ui 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.md +239 -0
- package/README.md +161 -0
- package/dist/cli.mjs +151 -0
- package/dist/dates.d.mts +20 -0
- package/dist/dates.d.ts +20 -0
- package/dist/dates.js +240 -0
- package/dist/dates.js.map +1 -0
- package/dist/dates.mjs +203 -0
- package/dist/dates.mjs.map +1 -0
- package/dist/dnd.d.mts +126 -0
- package/dist/dnd.d.ts +126 -0
- package/dist/dnd.js +274 -0
- package/dist/dnd.js.map +1 -0
- package/dist/dnd.mjs +250 -0
- package/dist/dnd.mjs.map +1 -0
- package/dist/fontThemes-Dh8mtXES.d.mts +868 -0
- package/dist/fontThemes-Dh8mtXES.d.ts +868 -0
- package/dist/forms.d.mts +38 -0
- package/dist/forms.d.ts +38 -0
- package/dist/forms.js +198 -0
- package/dist/forms.js.map +1 -0
- package/dist/forms.mjs +159 -0
- package/dist/forms.mjs.map +1 -0
- package/dist/hooks-1b8WaQf1.d.mts +225 -0
- package/dist/hooks-CKW8vE9H.d.ts +225 -0
- package/dist/hooks.d.mts +3 -0
- package/dist/hooks.d.ts +3 -0
- package/dist/hooks.js +971 -0
- package/dist/hooks.js.map +1 -0
- package/dist/hooks.mjs +943 -0
- package/dist/hooks.mjs.map +1 -0
- package/dist/index-DscTIrZ2.d.mts +29 -0
- package/dist/index-DscTIrZ2.d.ts +29 -0
- package/dist/index.d.mts +3382 -0
- package/dist/index.d.ts +3382 -0
- package/dist/index.js +15146 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +14802 -0
- package/dist/index.mjs.map +1 -0
- package/dist/providers-CXPDMsl7.d.mts +30 -0
- package/dist/providers-Dn_Msjvz.d.ts +30 -0
- package/dist/providers.d.mts +3 -0
- package/dist/providers.d.ts +3 -0
- package/dist/providers.js +1885 -0
- package/dist/providers.js.map +1 -0
- package/dist/providers.mjs +1859 -0
- package/dist/providers.mjs.map +1 -0
- package/dist/tables.d.mts +10 -0
- package/dist/tables.d.ts +10 -0
- package/dist/tables.js +248 -0
- package/dist/tables.js.map +1 -0
- package/dist/tables.mjs +218 -0
- package/dist/tables.mjs.map +1 -0
- package/dist/tokens.d.mts +1065 -0
- package/dist/tokens.d.ts +1065 -0
- package/dist/tokens.js +2637 -0
- package/dist/tokens.js.map +1 -0
- package/dist/tokens.mjs +2555 -0
- package/dist/tokens.mjs.map +1 -0
- package/dist/utils-CIIM7dAC.d.ts +986 -0
- package/dist/utils-Cs04sxth.d.mts +986 -0
- package/dist/utils.d.mts +4 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +874 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.mjs +806 -0
- package/dist/utils.mjs.map +1 -0
- package/dist/validation-Bj1ye-v_.d.mts +114 -0
- package/dist/validation-Bj1ye-v_.d.ts +114 -0
- package/dist/webgl.d.mts +104 -0
- package/dist/webgl.d.ts +104 -0
- package/dist/webgl.js +226 -0
- package/dist/webgl.js.map +1 -0
- package/dist/webgl.mjs +195 -0
- package/dist/webgl.mjs.map +1 -0
- package/package.json +267 -0
- package/src/cli.ts +206 -0
- package/src/component-registry.ts +183 -0
- package/src/components/actions/Button.test.tsx +61 -0
- package/src/components/actions/Button.tsx +70 -0
- package/src/components/actions/Link.tsx +78 -0
- package/src/components/actions/Magnetic.tsx +68 -0
- package/src/components/actions/Toggle.test.tsx +40 -0
- package/src/components/actions/Toggle.tsx +47 -0
- package/src/components/actions/ToggleGroup.tsx +70 -0
- package/src/components/actions/index.ts +5 -0
- package/src/components/backgrounds/FaultyTerminal.tsx +426 -0
- package/src/components/backgrounds/OrbBackground.tsx +424 -0
- package/src/components/backgrounds/WarpBackground.tsx +358 -0
- package/src/components/backgrounds/index.ts +3 -0
- package/src/components/blocks/Hero.tsx +142 -0
- package/src/components/blocks/social/OpenGraphCard.tsx +243 -0
- package/src/components/cursor/SplashCursor.tsx +1315 -0
- package/src/components/cursor/TargetCursor.tsx +187 -0
- package/src/components/cursor/index.ts +2 -0
- package/src/components/data-display/AspectImage.tsx +73 -0
- package/src/components/data-display/Avatar.test.tsx +35 -0
- package/src/components/data-display/Avatar.tsx +55 -0
- package/src/components/data-display/Badge.test.tsx +43 -0
- package/src/components/data-display/Badge.tsx +84 -0
- package/src/components/data-display/Brand.tsx +123 -0
- package/src/components/data-display/Calendar.tsx +70 -0
- package/src/components/data-display/Card.test.tsx +92 -0
- package/src/components/data-display/Card.tsx +115 -0
- package/src/components/data-display/Code.tsx +210 -0
- package/src/components/data-display/CollapsibleCodeBlock.tsx +238 -0
- package/src/components/data-display/DataTable.tsx +119 -0
- package/src/components/data-display/DescriptionList.tsx +41 -0
- package/src/components/data-display/GitHubIcon.tsx +44 -0
- package/src/components/data-display/Heading.test.tsx +36 -0
- package/src/components/data-display/Heading.tsx +83 -0
- package/src/components/data-display/StatCard.tsx +195 -0
- package/src/components/data-display/Table.tsx +133 -0
- package/src/components/data-display/Text.test.tsx +48 -0
- package/src/components/data-display/Text.tsx +144 -0
- package/src/components/data-display/Timeline.tsx +194 -0
- package/src/components/data-display/TreeView.tsx +226 -0
- package/src/components/data-display/Typewriter.tsx +119 -0
- package/src/components/data-display/VariableWeightText.tsx +130 -0
- package/src/components/data-display/index.ts +19 -0
- package/src/components/feedback/Alert.test.tsx +44 -0
- package/src/components/feedback/Alert.tsx +65 -0
- package/src/components/feedback/EmptyState.tsx +113 -0
- package/src/components/feedback/Progress.test.tsx +60 -0
- package/src/components/feedback/Progress.tsx +30 -0
- package/src/components/feedback/ProgressBar.tsx +158 -0
- package/src/components/feedback/Skeleton.test.tsx +39 -0
- package/src/components/feedback/Skeleton.tsx +45 -0
- package/src/components/feedback/Sonner.tsx +28 -0
- package/src/components/feedback/Spinner.test.tsx +33 -0
- package/src/components/feedback/Spinner.tsx +99 -0
- package/src/components/feedback/Stepper.tsx +307 -0
- package/src/components/feedback/Toast/Toast.tsx +243 -0
- package/src/components/feedback/Toast/index.ts +2 -0
- package/src/components/feedback/index.ts +9 -0
- package/src/components/forms/Checkbox.test.tsx +40 -0
- package/src/components/forms/Checkbox.tsx +31 -0
- package/src/components/forms/ColorPicker.tsx +118 -0
- package/src/components/forms/Combobox.tsx +96 -0
- package/src/components/forms/DragDrop.tsx +440 -0
- package/src/components/forms/FileUpload.tsx +252 -0
- package/src/components/forms/FilterButton.tsx +65 -0
- package/src/components/forms/Form.tsx +197 -0
- package/src/components/forms/Input.test.tsx +46 -0
- package/src/components/forms/Input.tsx +43 -0
- package/src/components/forms/InputOTP.tsx +81 -0
- package/src/components/forms/Label.test.tsx +20 -0
- package/src/components/forms/Label.tsx +25 -0
- package/src/components/forms/RadioGroup.tsx +51 -0
- package/src/components/forms/SearchBar.tsx +215 -0
- package/src/components/forms/Select.test.tsx +118 -0
- package/src/components/forms/Select.tsx +274 -0
- package/src/components/forms/Slider.tsx +29 -0
- package/src/components/forms/Switch.test.tsx +76 -0
- package/src/components/forms/Switch.tsx +30 -0
- package/src/components/forms/TextField.tsx +152 -0
- package/src/components/forms/Textarea.test.tsx +41 -0
- package/src/components/forms/Textarea.tsx +29 -0
- package/src/components/forms/ThemeSwitcher.tsx +290 -0
- package/src/components/forms/ThemeToggle.tsx +151 -0
- package/src/components/forms/index.ts +19 -0
- package/src/components/layout/Accordion.test.tsx +66 -0
- package/src/components/layout/Accordion.tsx +64 -0
- package/src/components/layout/AspectRatio.tsx +7 -0
- package/src/components/layout/Carousel.tsx +277 -0
- package/src/components/layout/Collapsible.test.tsx +40 -0
- package/src/components/layout/Collapsible.tsx +31 -0
- package/src/components/layout/Container.test.tsx +45 -0
- package/src/components/layout/Container.tsx +99 -0
- package/src/components/layout/CustomizerPanel.tsx +400 -0
- package/src/components/layout/DatePicker.tsx +57 -0
- package/src/components/layout/Footer/Footer.tsx +175 -0
- package/src/components/layout/Footer/index.ts +2 -0
- package/src/components/layout/GlassSurface.tsx +82 -0
- package/src/components/layout/Grid.test.tsx +31 -0
- package/src/components/layout/Grid.tsx +130 -0
- package/src/components/layout/Header/Header.tsx +450 -0
- package/src/components/layout/Header/index.ts +2 -0
- package/src/components/layout/PageLayout.tsx +180 -0
- package/src/components/layout/PageTemplate.tsx +158 -0
- package/src/components/layout/Resizable.tsx +48 -0
- package/src/components/layout/ScrollArea.tsx +53 -0
- package/src/components/layout/Separator.test.tsx +28 -0
- package/src/components/layout/Separator.tsx +29 -0
- package/src/components/layout/Sidebar.tsx +171 -0
- package/src/components/layout/Stack.test.tsx +41 -0
- package/src/components/layout/Stack.tsx +89 -0
- package/src/components/layout/glass-surface.css +60 -0
- package/src/components/layout/index.ts +18 -0
- package/src/components/motion/AnimatedBeam.tsx +159 -0
- package/src/components/navigation/Breadcrumb.test.tsx +57 -0
- package/src/components/navigation/Breadcrumb.tsx +119 -0
- package/src/components/navigation/Breadcrumbs.tsx +221 -0
- package/src/components/navigation/Command.tsx +159 -0
- package/src/components/navigation/Menubar.tsx +115 -0
- package/src/components/navigation/NavLink.tsx +55 -0
- package/src/components/navigation/NavigationMenu.tsx +125 -0
- package/src/components/navigation/Pagination.tsx +121 -0
- package/src/components/navigation/SecondaryNav.tsx +100 -0
- package/src/components/navigation/Tabs.test.tsx +47 -0
- package/src/components/navigation/Tabs.tsx +60 -0
- package/src/components/navigation/TertiaryNav.tsx +90 -0
- package/src/components/navigation/index.ts +10 -0
- package/src/components/overlays/AlertDialog.test.tsx +69 -0
- package/src/components/overlays/AlertDialog.tsx +166 -0
- package/src/components/overlays/ContextMenu.tsx +243 -0
- package/src/components/overlays/Dialog.test.tsx +79 -0
- package/src/components/overlays/Dialog.tsx +158 -0
- package/src/components/overlays/Drawer.tsx +128 -0
- package/src/components/overlays/Dropdown.tsx +253 -0
- package/src/components/overlays/DropdownMenu.tsx +242 -0
- package/src/components/overlays/HoverCard.tsx +32 -0
- package/src/components/overlays/Modal.tsx +250 -0
- package/src/components/overlays/NotificationCenter.tsx +364 -0
- package/src/components/overlays/Popover.test.tsx +40 -0
- package/src/components/overlays/Popover.tsx +46 -0
- package/src/components/overlays/Sheet.tsx +163 -0
- package/src/components/overlays/Tooltip.test.tsx +33 -0
- package/src/components/overlays/Tooltip.tsx +32 -0
- package/src/components/overlays/index.ts +12 -0
- package/src/dates.ts +2 -0
- package/src/dnd.ts +1 -0
- package/src/forms.ts +1 -0
- package/src/globals.css +187 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useForm.ts +247 -0
- package/src/hooks/useMotionPreference.test.ts +102 -0
- package/src/hooks/useMotionPreference.ts +78 -0
- package/src/hooks/useTheme.ts +58 -0
- package/src/hooks.ts +9 -0
- package/src/index.ts +168 -0
- package/src/lib/animations.ts +356 -0
- package/src/lib/breadcrumbs.ts +94 -0
- package/src/lib/colors.ts +493 -0
- package/src/lib/store/customizer.ts +482 -0
- package/src/lib/store/index.ts +3 -0
- package/src/lib/store/theme.ts +55 -0
- package/src/lib/syntax-parser/index.ts +50 -0
- package/src/lib/syntax-parser/patterns.ts +64 -0
- package/src/lib/syntax-parser/tokenizer.ts +117 -0
- package/src/lib/syntax-parser/types.ts +27 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validation.ts +204 -0
- package/src/lib/webgl/Color.ts +11 -0
- package/src/lib/webgl/Mesh.ts +41 -0
- package/src/lib/webgl/Program.ts +118 -0
- package/src/lib/webgl/Renderer.ts +51 -0
- package/src/lib/webgl/Triangle.ts +27 -0
- package/src/lib/webgl/Vec3.ts +18 -0
- package/src/lib/webgl/index.ts +13 -0
- package/src/nativewind-env.d.ts +1 -0
- package/src/providers/ThemeProvider.tsx +461 -0
- package/src/providers/index.ts +1 -0
- package/src/providers.ts +7 -0
- package/src/tables.ts +1 -0
- package/src/test/setup.ts +39 -0
- package/src/theme.css +158 -0
- package/src/tokens.ts +7 -0
- package/src/utils.ts +12 -0
- package/src/webgl.ts +1 -0
|
@@ -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,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
|
+
}
|