@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,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
|
+
}
|