@snowcone-app/ui 0.1.42 → 0.2.0
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/CHANGELOG.md +33 -0
- package/README.md +18 -4
- package/package.json +9 -5
- package/src/components/CanvasIsolationBoundary.tsx +202 -0
- package/src/components/LoadingOverlayPrism.tsx +251 -0
- package/src/composed/AddToCart.tsx +229 -0
- package/src/composed/ArtAlignment.tsx +703 -0
- package/src/composed/ArtSelector.tsx +290 -0
- package/src/composed/ArtworkCustomizer.tsx +212 -0
- package/src/composed/CanvasEditor.tsx +79 -0
- package/src/composed/ColorPicker.tsx +111 -0
- package/src/composed/CurrentSelectionDisplay.tsx +86 -0
- package/src/composed/HeroProductImage.tsx +1071 -0
- package/src/composed/Lightbox.index.ts +2 -0
- package/src/composed/Lightbox.tsx +230 -0
- package/src/composed/PlacementClipShapeSelector.tsx +88 -0
- package/src/composed/PlacementTabs.tsx +179 -0
- package/src/composed/ProductCard.tsx +298 -0
- package/src/composed/ProductGallery.tsx +54 -0
- package/src/composed/ProductImage.tsx +129 -0
- package/src/composed/ProductList.tsx +147 -0
- package/src/composed/ProductOptions.tsx +305 -0
- package/src/composed/RealtimeMockup.tsx +121 -0
- package/src/composed/TileCount.tsx +348 -0
- package/src/composed/carousels/HeroCarousel.tsx +240 -0
- package/src/composed/carousels/MobileProductCarousel.tsx +1002 -0
- package/src/composed/carousels/index.ts +11 -0
- package/src/composed/carousels/types.ts +58 -0
- package/src/composed/grids/MasonryGrid.tsx +238 -0
- package/src/composed/grids/index.ts +9 -0
- package/src/composed/search/CurrentRefinements.tsx +80 -0
- package/src/composed/search/Filters.tsx +49 -0
- package/src/composed/search/FiltersButton.tsx +57 -0
- package/src/composed/search/FiltersDrawer.tsx +375 -0
- package/src/composed/search/ProductGrid.tsx +118 -0
- package/src/composed/search/ProductHit.tsx +56 -0
- package/src/composed/search/SearchBox.tsx +109 -0
- package/src/composed/search/SearchProvider.tsx +136 -0
- package/src/composed/search/facetConfig.ts +16 -0
- package/src/composed/search/index.ts +22 -0
- package/src/composed/search/meilisearchAdapter.ts +20 -0
- package/src/composed/search/types.ts +22 -0
- package/src/composed/zoom/EnhancedImageViewer.tsx +505 -0
- package/src/composed/zoom/ResponsiveZoom.tsx +134 -0
- package/src/composed/zoom/ZoomOverlay.tsx +194 -0
- package/src/composed/zoom/index.ts +12 -0
- package/src/composed/zoom/types.ts +12 -0
- package/src/design-system/ColorPalette.tsx +126 -0
- package/src/design-system/ColorSwatch.tsx +49 -0
- package/src/design-system/DesignSystemPage.tsx +130 -0
- package/src/design-system/ThemeSwitcher.tsx +181 -0
- package/src/design-system/TypographyScale.tsx +106 -0
- package/src/design-system/index.ts +5 -0
- package/src/ecommerce/stories/HeroProductImage.stories.tsx +66 -0
- package/src/ecommerce/stories/PDPHeroGallery.stories.tsx +105 -0
- package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +472 -0
- package/src/ecommerce/stories/PDPLayout.stories.tsx +365 -0
- package/src/hooks/useBrand.ts +41 -0
- package/src/hooks/useCanvasContext.ts +127 -0
- package/src/hooks/useDeviceDetection.ts +64 -0
- package/src/hooks/useFocusTrap.ts +70 -0
- package/src/hooks/useImagePreloader.ts +268 -0
- package/src/hooks/useImageTransition.ts +608 -0
- package/src/hooks/usePlacementsProcessor.ts +74 -0
- package/src/hooks/useProductGallery.ts +193 -0
- package/src/hooks/useProductPage.ts +467 -0
- package/src/hooks/useRenderGuard.ts +96 -0
- package/src/hooks/useScrollDirection.ts +196 -0
- package/src/hooks/viewport/index.ts +25 -0
- package/src/hooks/viewport/useContainerWidth.ts +59 -0
- package/src/hooks/viewport/useMediaQuery.ts +52 -0
- package/src/hooks/viewport/useResponsiveImageCap.ts +149 -0
- package/src/hooks/viewport/useViewportDimensions.ts +135 -0
- package/src/hooks/viewport/useWideMonitorMode.ts +150 -0
- package/src/hooks/visibility/index.ts +15 -0
- package/src/hooks/visibility/observerPool.ts +150 -0
- package/src/index.ts +240 -0
- package/src/layouts/hero-zoom/HeroShrinkLayout.tsx +209 -0
- package/src/layouts/hero-zoom/HeroZoomLayout.tsx +351 -0
- package/src/layouts/hero-zoom/index.ts +30 -0
- package/src/layouts/hero-zoom/stories/HeroZoomLayout.stories.tsx +350 -0
- package/src/layouts/hero-zoom/types.ts +113 -0
- package/src/layouts/hero-zoom/useHeroZoomScales.ts +156 -0
- package/src/layouts/index.ts +9 -0
- package/src/layouts/pdp/EdgeBlurBox.tsx +210 -0
- package/src/layouts/pdp/ImageBlurExtension.tsx +215 -0
- package/src/layouts/pdp/ImageEdgeBlur.tsx +215 -0
- package/src/layouts/pdp/PDPLayout.tsx +246 -0
- package/src/layouts/pdp/SimpleImageBlur.tsx +140 -0
- package/src/layouts/pdp/index.ts +40 -0
- package/src/lib/env.ts +15 -0
- package/src/lib/locale.ts +167 -0
- package/src/lib/router.tsx +46 -0
- package/src/lib/utils.ts +6 -0
- package/src/lightbox/README.md +77 -0
- package/src/next/index.tsx +26 -0
- package/src/patterns/MockupPriorityProvider.tsx +1014 -0
- package/src/patterns/Product.tsx +850 -0
- package/src/patterns/ProductPageProvider.tsx +224 -0
- package/src/patterns/RealtimeProvider.tsx +1162 -0
- package/src/patterns/ShopProvider.tsx +603 -0
- package/src/personalization/PersonalizationBridge.tsx +235 -0
- package/src/personalization/PersonalizationContext.ts +29 -0
- package/src/personalization/PersonalizationInputs.tsx +110 -0
- package/src/personalization/PersonalizationProvider.tsx +407 -0
- package/src/personalization/canvas-stub.d.ts +22 -0
- package/src/personalization/index.ts +43 -0
- package/src/personalization/types.ts +48 -0
- package/src/personalization/usePersonalization.ts +32 -0
- package/src/personalization/usePersonalizationShimmer.ts +159 -0
- package/src/personalization/utils.ts +59 -0
- package/src/primitives/BrandLogo.tsx +65 -0
- package/src/primitives/BrandName.tsx +51 -0
- package/src/primitives/Button.tsx +123 -0
- package/src/primitives/ColorSwatch.tsx +221 -0
- package/src/primitives/DragHintAnimation.tsx +190 -0
- package/src/primitives/EdgeSwipeGuards.tsx +60 -0
- package/src/primitives/FloatingActionGroup.tsx +176 -0
- package/src/primitives/ProductPrice.tsx +171 -0
- package/src/primitives/ProgressiveBlur.tsx +295 -0
- package/src/primitives/ThemeToggle.tsx +125 -0
- package/src/primitives/__tests__/story-coverage.test.ts +98 -0
- package/src/primitives/accordion.tsx +280 -0
- package/src/primitives/badge.tsx +137 -0
- package/src/primitives/card.tsx +61 -0
- package/src/primitives/checkbox.tsx +56 -0
- package/src/primitives/collapsible.tsx +51 -0
- package/src/primitives/drawer.tsx +828 -0
- package/src/primitives/dropdown-menu.tsx +197 -0
- package/src/primitives/fieldset.tsx +73 -0
- package/src/primitives/index.ts +138 -0
- package/src/primitives/input.tsx +91 -0
- package/src/primitives/kbd.tsx +130 -0
- package/src/primitives/label.tsx +20 -0
- package/src/primitives/link.tsx +182 -0
- package/src/primitives/popover.tsx +80 -0
- package/src/primitives/radio-group.tsx +79 -0
- package/src/primitives/scroll-fade.tsx +159 -0
- package/src/primitives/select.tsx +170 -0
- package/src/primitives/separator.tsx +25 -0
- package/src/primitives/slider.tsx +221 -0
- package/src/primitives/spinner.tsx +72 -0
- package/src/primitives/stories/Accordion.stories.tsx +121 -0
- package/src/primitives/stories/Badge.stories.tsx +221 -0
- package/src/primitives/stories/Button.stories.tsx +185 -0
- package/src/primitives/stories/Card.stories.tsx +171 -0
- package/src/primitives/stories/Checkbox.stories.tsx +214 -0
- package/src/primitives/stories/Collapsible.stories.tsx +230 -0
- package/src/primitives/stories/Drawer.stories.tsx +378 -0
- package/src/primitives/stories/DropdownMenu.stories.tsx +182 -0
- package/src/primitives/stories/Fieldset.stories.tsx +212 -0
- package/src/primitives/stories/Input.stories.tsx +172 -0
- package/src/primitives/stories/Kbd.stories.tsx +183 -0
- package/src/primitives/stories/Label.stories.tsx +98 -0
- package/src/primitives/stories/Link.stories.tsx +260 -0
- package/src/primitives/stories/Popover.stories.tsx +178 -0
- package/src/primitives/stories/RadioGroup.stories.tsx +205 -0
- package/src/primitives/stories/Select.stories.tsx +222 -0
- package/src/primitives/stories/Separator.stories.tsx +134 -0
- package/src/primitives/stories/Slider.stories.tsx +203 -0
- package/src/primitives/stories/Spinner.stories.tsx +142 -0
- package/src/primitives/stories/Surface.stories.tsx +257 -0
- package/src/primitives/stories/Switch.stories.tsx +131 -0
- package/src/primitives/stories/Tabs.stories.tsx +275 -0
- package/src/primitives/stories/TextField.stories.tsx +139 -0
- package/src/primitives/stories/Textarea.stories.tsx +148 -0
- package/src/primitives/stories/Tooltip.stories.tsx +119 -0
- package/src/primitives/surface.tsx +86 -0
- package/src/primitives/switch.tsx +35 -0
- package/src/primitives/tabs.tsx +206 -0
- package/src/primitives/text-field.tsx +84 -0
- package/src/primitives/textarea.tsx +50 -0
- package/src/primitives/tooltip.tsx +58 -0
- package/src/services/CanvasExportService.ts +518 -0
- package/src/styles/base.css +380 -0
- package/src/styles/defaults.css +280 -0
- package/src/styles/globals.css +1242 -0
- package/src/styles/index.css +17 -0
- package/src/styles/ne-themes.css +4740 -0
- package/src/styles/tailwind.css +11 -0
- package/src/styles/tokens.css +117 -0
- package/src/styles/utilities.css +188 -0
- package/src/themes/apply-theme.ts +449 -0
- package/src/themes/getThemeStyles.ts +454 -0
- package/src/themes/index.ts +48 -0
- package/src/themes/oklch-theme.ts +283 -0
- package/src/themes/presets.ts +989 -0
- package/src/themes/types.ts +386 -0
- package/src/themes/useTheme.tsx +450 -0
- package/src/utils/dev-warnings.ts +161 -0
- package/src/utils/devWarnings.ts +153 -0
- package/dist/styles.css +0 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ThemeSwitcher - Dropdown to switch between preset themes
|
|
7
|
+
*
|
|
8
|
+
* Works standalone without any context - applies themes directly to the DOM.
|
|
9
|
+
* Also watches for data-theme attribute changes in case theme is applied after mount.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useState, useEffect } from 'react';
|
|
13
|
+
import { baseThemes, findTheme, findThemeById, getThemeVariant, applyTheme, getBaseThemeName } from '../themes';
|
|
14
|
+
import type { ThemeConfig } from '../themes';
|
|
15
|
+
|
|
16
|
+
interface ThemeSwitcherProps {
|
|
17
|
+
className?: string;
|
|
18
|
+
/** Show only base themes (not dark variants separately) */
|
|
19
|
+
showBaseThemesOnly?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read current theme from DOM
|
|
24
|
+
*/
|
|
25
|
+
function readThemeFromDOM(): { themeName: string; isDark: boolean } {
|
|
26
|
+
if (typeof document === 'undefined') {
|
|
27
|
+
return { themeName: 'Linear', isDark: false };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const root = document.documentElement;
|
|
31
|
+
const themeId = root.getAttribute('data-theme');
|
|
32
|
+
const dark = root.classList.contains('dark');
|
|
33
|
+
|
|
34
|
+
if (themeId) {
|
|
35
|
+
const theme = findThemeById(themeId);
|
|
36
|
+
if (theme) {
|
|
37
|
+
const baseName = getBaseThemeName(theme);
|
|
38
|
+
return { themeName: baseName, isDark: theme.isDark || dark };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { themeName: 'Linear', isDark: dark };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function ThemeSwitcher({ className = '', showBaseThemesOnly = true }: ThemeSwitcherProps) {
|
|
46
|
+
const [currentTheme, setCurrentTheme] = useState<string>('Linear');
|
|
47
|
+
const [isDark, setIsDark] = useState(false);
|
|
48
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
49
|
+
|
|
50
|
+
// Initialize and watch for data-theme attribute changes
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
// Read initial theme
|
|
53
|
+
const { themeName, isDark: dark } = readThemeFromDOM();
|
|
54
|
+
setCurrentTheme(themeName);
|
|
55
|
+
setIsDark(dark);
|
|
56
|
+
|
|
57
|
+
// Watch for attribute changes (theme may be applied after mount by ThemeProvider)
|
|
58
|
+
const observer = new MutationObserver((mutations) => {
|
|
59
|
+
for (const mutation of mutations) {
|
|
60
|
+
if (mutation.type === 'attributes' &&
|
|
61
|
+
(mutation.attributeName === 'data-theme' || mutation.attributeName === 'class')) {
|
|
62
|
+
const { themeName: newName, isDark: newDark } = readThemeFromDOM();
|
|
63
|
+
setCurrentTheme(newName);
|
|
64
|
+
setIsDark(newDark);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
observer.observe(document.documentElement, {
|
|
71
|
+
attributes: true,
|
|
72
|
+
attributeFilter: ['data-theme', 'class'],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return () => observer.disconnect();
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const handleThemeSelect = (themeName: string) => {
|
|
79
|
+
// Find the base theme
|
|
80
|
+
const theme = findTheme(themeName);
|
|
81
|
+
if (!theme) return;
|
|
82
|
+
|
|
83
|
+
// Get the appropriate variant based on current dark mode
|
|
84
|
+
const targetTheme = getThemeVariant(theme, isDark);
|
|
85
|
+
|
|
86
|
+
const config: ThemeConfig = { name: targetTheme.name };
|
|
87
|
+
applyTheme(config);
|
|
88
|
+
setCurrentTheme(themeName);
|
|
89
|
+
setIsOpen(false);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handleDarkModeToggle = () => {
|
|
93
|
+
const newIsDark = !isDark;
|
|
94
|
+
setIsDark(newIsDark);
|
|
95
|
+
|
|
96
|
+
// Find the current base theme
|
|
97
|
+
const theme = findTheme(currentTheme);
|
|
98
|
+
if (!theme) return;
|
|
99
|
+
|
|
100
|
+
// Get the target variant (light or dark)
|
|
101
|
+
const targetTheme = getThemeVariant(theme, newIsDark);
|
|
102
|
+
|
|
103
|
+
const config: ThemeConfig = { name: targetTheme.name };
|
|
104
|
+
applyTheme(config);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const themes = showBaseThemesOnly ? baseThemes : baseThemes;
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className={`flex items-center gap-3 ${className}`}>
|
|
111
|
+
{/* Theme Dropdown */}
|
|
112
|
+
<div className="relative">
|
|
113
|
+
<button
|
|
114
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
115
|
+
className="flex items-center gap-2 px-4 py-2 bg-card border border-border rounded-lg hover:bg-muted transition-colors text-foreground"
|
|
116
|
+
>
|
|
117
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
118
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
|
|
119
|
+
</svg>
|
|
120
|
+
<span className="font-medium">{currentTheme}</span>
|
|
121
|
+
<svg className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
122
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
|
123
|
+
</svg>
|
|
124
|
+
</button>
|
|
125
|
+
|
|
126
|
+
{isOpen && (
|
|
127
|
+
<>
|
|
128
|
+
{/* Backdrop */}
|
|
129
|
+
<div
|
|
130
|
+
className="fixed inset-0 z-40"
|
|
131
|
+
onClick={() => setIsOpen(false)}
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
{/* Dropdown */}
|
|
135
|
+
<div className="absolute top-full left-0 mt-2 w-56 max-h-80 overflow-auto bg-card border border-border rounded-lg shadow-lg z-50">
|
|
136
|
+
{themes.map((theme) => (
|
|
137
|
+
<button
|
|
138
|
+
key={theme.name}
|
|
139
|
+
onClick={() => handleThemeSelect(theme.name)}
|
|
140
|
+
className={`w-full px-4 py-2 text-left hover:bg-muted transition-colors flex items-center gap-3 ${
|
|
141
|
+
currentTheme === theme.name ? 'bg-muted' : ''
|
|
142
|
+
}`}
|
|
143
|
+
>
|
|
144
|
+
<div
|
|
145
|
+
className="w-4 h-4 rounded-full border border-border"
|
|
146
|
+
style={{ backgroundColor: theme.primaryColor || '#888' }}
|
|
147
|
+
/>
|
|
148
|
+
<span className="text-foreground">{theme.name}</span>
|
|
149
|
+
{currentTheme === theme.name && (
|
|
150
|
+
<svg className="w-4 h-4 ml-auto text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
151
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
152
|
+
</svg>
|
|
153
|
+
)}
|
|
154
|
+
</button>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
</>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* Dark Mode Toggle */}
|
|
162
|
+
<button
|
|
163
|
+
onClick={handleDarkModeToggle}
|
|
164
|
+
className="flex items-center gap-2 px-4 py-2 bg-card border border-border rounded-lg hover:bg-muted transition-colors text-foreground"
|
|
165
|
+
title={`Switch to ${isDark ? 'light' : 'dark'} mode`}
|
|
166
|
+
>
|
|
167
|
+
{isDark ? (
|
|
168
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
169
|
+
<circle cx="12" cy="12" r="5" />
|
|
170
|
+
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
|
|
171
|
+
</svg>
|
|
172
|
+
) : (
|
|
173
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
174
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
175
|
+
</svg>
|
|
176
|
+
)}
|
|
177
|
+
<span className="text-sm">{isDark ? 'Light' : 'Dark'}</span>
|
|
178
|
+
</button>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypographyScale - Displays font families and type scale
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const FONT_SAMPLES = [
|
|
6
|
+
{
|
|
7
|
+
name: 'Heading',
|
|
8
|
+
cssVar: '--font-heading',
|
|
9
|
+
sample: 'The quick brown fox jumps over the lazy dog',
|
|
10
|
+
sizes: ['text-4xl', 'text-3xl', 'text-2xl', 'text-xl'],
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'Body',
|
|
14
|
+
cssVar: '--font-body',
|
|
15
|
+
sample: 'The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs.',
|
|
16
|
+
sizes: ['text-lg', 'text-base', 'text-sm'],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'Label',
|
|
20
|
+
cssVar: '--font-label',
|
|
21
|
+
sample: 'CATEGORY · PRODUCT NAME · $99.00',
|
|
22
|
+
sizes: ['text-sm', 'text-xs'],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'Button',
|
|
26
|
+
cssVar: '--font-button',
|
|
27
|
+
sample: 'Add to Cart · Buy Now · Learn More',
|
|
28
|
+
sizes: ['text-base', 'text-sm'],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'Caption',
|
|
32
|
+
cssVar: '--font-caption',
|
|
33
|
+
sample: 'Image caption · Timestamp · Fine print',
|
|
34
|
+
sizes: ['text-sm', 'text-xs'],
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
interface TypographyScaleProps {
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function TypographyScale({ className = '' }: TypographyScaleProps) {
|
|
43
|
+
return (
|
|
44
|
+
<div className={`space-y-8 ${className}`}>
|
|
45
|
+
{FONT_SAMPLES.map((font) => (
|
|
46
|
+
<section key={font.name} className="bg-card rounded-lg border border-border p-6">
|
|
47
|
+
<div className="flex items-center justify-between mb-4">
|
|
48
|
+
<h3 className="text-lg font-semibold text-foreground">{font.name}</h3>
|
|
49
|
+
<code className="text-xs text-muted-foreground font-mono bg-muted px-2 py-1 rounded">
|
|
50
|
+
{font.cssVar}
|
|
51
|
+
</code>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="space-y-4">
|
|
55
|
+
{font.sizes.map((size) => (
|
|
56
|
+
<div key={size} className="flex items-baseline gap-4">
|
|
57
|
+
<span className="text-xs text-muted-foreground w-20 flex-shrink-0">
|
|
58
|
+
{size}
|
|
59
|
+
</span>
|
|
60
|
+
<p
|
|
61
|
+
className={`${size} text-foreground`}
|
|
62
|
+
style={{ fontFamily: `var(${font.cssVar})` }}
|
|
63
|
+
>
|
|
64
|
+
{font.sample}
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
</section>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const RADIUS_SAMPLES = [
|
|
76
|
+
{ name: 'None', value: '0px' },
|
|
77
|
+
{ name: 'Small', cssVar: '--radius-small' },
|
|
78
|
+
{ name: 'Medium', cssVar: '--radius-medium' },
|
|
79
|
+
{ name: 'Large', cssVar: '--radius-large' },
|
|
80
|
+
{ name: 'Full', value: '9999px' },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
export function RadiusScale({ className = '' }: { className?: string }) {
|
|
84
|
+
return (
|
|
85
|
+
<div className={`space-y-4 ${className}`}>
|
|
86
|
+
<div className="grid grid-cols-5 gap-4">
|
|
87
|
+
{RADIUS_SAMPLES.map((radius) => (
|
|
88
|
+
<div key={radius.name} className="flex flex-col items-center gap-2">
|
|
89
|
+
<div
|
|
90
|
+
className="h-16 w-16 bg-primary"
|
|
91
|
+
style={{
|
|
92
|
+
borderRadius: radius.cssVar
|
|
93
|
+
? `var(${radius.cssVar})`
|
|
94
|
+
: radius.value,
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
<span className="text-sm font-medium text-foreground">{radius.name}</span>
|
|
98
|
+
<code className="text-xs text-muted-foreground font-mono">
|
|
99
|
+
{radius.cssVar || radius.value}
|
|
100
|
+
</code>
|
|
101
|
+
</div>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { DesignSystemPage } from './DesignSystemPage';
|
|
2
|
+
export { ColorPalette } from './ColorPalette';
|
|
3
|
+
export { ColorTokenSwatch, ColorRow } from './ColorSwatch';
|
|
4
|
+
export { TypographyScale, RadiusScale } from './TypographyScale';
|
|
5
|
+
export { ThemeSwitcher } from './ThemeSwitcher';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { HeroProductImage } from '../../composed/HeroProductImage';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof HeroProductImage> = {
|
|
6
|
+
title: 'Layouts/Hero Product Image',
|
|
7
|
+
component: HeroProductImage,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'centered',
|
|
10
|
+
},
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
type Story = StoryObj<typeof HeroProductImage>;
|
|
16
|
+
|
|
17
|
+
// Mock product data
|
|
18
|
+
const mockProduct = {
|
|
19
|
+
id: "test-product",
|
|
20
|
+
placements: [
|
|
21
|
+
{ label: "Front", type: "image", width: 1000, height: 1000 }
|
|
22
|
+
]
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const Default: Story = {
|
|
26
|
+
args: {
|
|
27
|
+
width: 600,
|
|
28
|
+
className: "w-[600px] h-[600px] bg-gray-50",
|
|
29
|
+
artwork: {
|
|
30
|
+
type: "regular",
|
|
31
|
+
src: "https://placehold.co/600x600/png?text=Artwork"
|
|
32
|
+
},
|
|
33
|
+
product: mockProduct,
|
|
34
|
+
placement: "Front",
|
|
35
|
+
mockupId: "mockup-1",
|
|
36
|
+
variantId: "default",
|
|
37
|
+
},
|
|
38
|
+
render: (args) => (
|
|
39
|
+
<div className="w-[600px] h-[600px] border border-gray-200 rounded-lg overflow-hidden">
|
|
40
|
+
<HeroProductImage {...args} />
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const Loading: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
width: 600,
|
|
48
|
+
className: "w-[600px] h-[600px] bg-gray-50",
|
|
49
|
+
artwork: {
|
|
50
|
+
type: "regular",
|
|
51
|
+
src: "https://placehold.co/600x600/png?text=Artwork"
|
|
52
|
+
},
|
|
53
|
+
product: mockProduct,
|
|
54
|
+
placement: "Front",
|
|
55
|
+
mockupId: "mockup-1",
|
|
56
|
+
variantId: "default",
|
|
57
|
+
},
|
|
58
|
+
render: (args) => (
|
|
59
|
+
<div className="w-[600px] h-[600px] border border-gray-200 rounded-lg overflow-hidden">
|
|
60
|
+
{/* Simulate initial loading state by not providing enough info or forcing load state if possible */}
|
|
61
|
+
{/* Actually, the component handles loading shimmer when artwork is present but image not ready */}
|
|
62
|
+
<HeroProductImage {...args} />
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
};
|
|
66
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Icon } from '@iconify/react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* PDPHeroGallery - Product Detail Page Hero Image Gallery
|
|
7
|
+
*
|
|
8
|
+
* The main product image area showing large mockups that stack vertically.
|
|
9
|
+
* On desktop, the info panel overlaps these images on the right side.
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Full-width hero images that stack vertically
|
|
13
|
+
* - Each image takes ~70vh height
|
|
14
|
+
* - Placement label in bottom left
|
|
15
|
+
* - Click to trigger zoom/lightbox
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Demo mockup images
|
|
19
|
+
const demoMockups = [
|
|
20
|
+
{ id: 'front', label: 'Front' },
|
|
21
|
+
{ id: 'back', label: 'Back' },
|
|
22
|
+
{ id: 'detail', label: 'Detail' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
interface PDPHeroGalleryProps {
|
|
26
|
+
mockups?: { id: string; label: string }[];
|
|
27
|
+
onImageClick?: (mockupId: string) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const PDPHeroGallery = ({
|
|
31
|
+
mockups = demoMockups,
|
|
32
|
+
onImageClick,
|
|
33
|
+
}: PDPHeroGalleryProps) => {
|
|
34
|
+
return (
|
|
35
|
+
<div className="w-full">
|
|
36
|
+
{mockups.map((mockup) => (
|
|
37
|
+
<div
|
|
38
|
+
key={mockup.id}
|
|
39
|
+
className="relative w-full cursor-pointer"
|
|
40
|
+
style={{ height: '70vh' }}
|
|
41
|
+
onClick={() => onImageClick?.(mockup.id)}
|
|
42
|
+
>
|
|
43
|
+
{/* Placeholder gradient - in real usage this is HeroProductImage */}
|
|
44
|
+
<div className="w-full h-full bg-gradient-to-br from-muted to-muted/50 flex items-center justify-center">
|
|
45
|
+
<div className="text-center text-muted-foreground">
|
|
46
|
+
<Icon
|
|
47
|
+
icon="gravity-ui:t-shirt"
|
|
48
|
+
className="w-24 h-24 mx-auto mb-4 opacity-20"
|
|
49
|
+
/>
|
|
50
|
+
<p className="text-lg font-medium opacity-60">{mockup.label} View</p>
|
|
51
|
+
<p className="text-sm opacity-40">Product mockup</p>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
</div>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const meta: Meta<typeof PDPHeroGallery> = {
|
|
62
|
+
title: 'Layouts/PDP Hero Gallery',
|
|
63
|
+
component: PDPHeroGallery,
|
|
64
|
+
parameters: {
|
|
65
|
+
layout: 'fullscreen',
|
|
66
|
+
docs: {
|
|
67
|
+
description: {
|
|
68
|
+
component: `
|
|
69
|
+
The PDP Hero Gallery displays large product mockup images in a vertical stack.
|
|
70
|
+
|
|
71
|
+
## Layout
|
|
72
|
+
|
|
73
|
+
- Images are full-width and stack vertically
|
|
74
|
+
- Each image is approximately 70vh tall
|
|
75
|
+
- The info panel (not shown here) overlaps on the right side on desktop
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
In the actual PDP, this is used with \`HeroProductImage\` components:
|
|
80
|
+
|
|
81
|
+
\`\`\`tsx
|
|
82
|
+
{heroImages.map((image) => (
|
|
83
|
+
<HeroProductImage
|
|
84
|
+
key={image.mockupId}
|
|
85
|
+
mockupId={image.mockupId}
|
|
86
|
+
placement={image.placement}
|
|
87
|
+
onClick={() => openLightbox(image)}
|
|
88
|
+
/>
|
|
89
|
+
))}
|
|
90
|
+
\`\`\`
|
|
91
|
+
`,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
tags: ['autodocs'],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export default meta;
|
|
99
|
+
type Story = StoryObj<typeof meta>;
|
|
100
|
+
|
|
101
|
+
export const Default: Story = {
|
|
102
|
+
args: {
|
|
103
|
+
onImageClick: (id) => console.log(`Clicked: ${id}`),
|
|
104
|
+
},
|
|
105
|
+
};
|