@snowcone-app/ui 0.1.43 → 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 +26 -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,350 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { HeroZoomLayout } from "../HeroZoomLayout";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* HeroZoomLayout - Scroll-Driven Hero Image Layout
|
|
7
|
+
*
|
|
8
|
+
* A cinematic scroll experience where a hero image starts scaled up and
|
|
9
|
+
* zooms down as the user scrolls, with an optional header that slides up.
|
|
10
|
+
*
|
|
11
|
+
* ## Features
|
|
12
|
+
*
|
|
13
|
+
* - **GPU-Accelerated**: Uses modern CSS `animation-timeline: scroll()` for 60fps performance
|
|
14
|
+
* - **Fallback Support**: JavaScript fallback for browsers without scroll-driven animations
|
|
15
|
+
* - **Retina-Ready**: Renders at device pixel ratio for crisp images
|
|
16
|
+
* - **Mobile-Safe**: Handles iOS address bar changes without jarring jumps
|
|
17
|
+
*
|
|
18
|
+
* ## Animation Phases
|
|
19
|
+
*
|
|
20
|
+
* 1. **Scale Phase**: Hero image scales down from ~2x to 1x as you scroll
|
|
21
|
+
* 2. **Slide Phase**: Header and image slide up, transitioning to content below
|
|
22
|
+
*
|
|
23
|
+
* ## Usage
|
|
24
|
+
*
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <HeroZoomLayout
|
|
27
|
+
* config={{ aspectRatio: 16/9, headerHeight: 80 }}
|
|
28
|
+
* header={<MyHeader />}
|
|
29
|
+
* hero={<img src="/hero.jpg" alt="" className="w-full h-full object-cover" />}
|
|
30
|
+
* >
|
|
31
|
+
* <PageContent />
|
|
32
|
+
* </HeroZoomLayout>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
// Demo header component
|
|
37
|
+
const DemoHeader = ({ transparent = false }: { transparent?: boolean }) => (
|
|
38
|
+
<div
|
|
39
|
+
className={`h-full flex items-center justify-center ${
|
|
40
|
+
transparent ? "bg-white/30 backdrop-blur-sm" : "bg-background"
|
|
41
|
+
}`}
|
|
42
|
+
>
|
|
43
|
+
<span className="text-lg font-semibold">Header</span>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Demo content component
|
|
48
|
+
const DemoContent = () => (
|
|
49
|
+
<div className="bg-background">
|
|
50
|
+
<div className="max-w-2xl mx-auto px-6 pb-24 space-y-8 text-lg leading-relaxed pt-12">
|
|
51
|
+
<h1 className="text-4xl font-bold mb-12">The Mountains</h1>
|
|
52
|
+
<p>
|
|
53
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod
|
|
54
|
+
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
|
|
55
|
+
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
|
|
56
|
+
commodo consequat.
|
|
57
|
+
</p>
|
|
58
|
+
<p>
|
|
59
|
+
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
|
|
60
|
+
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
|
|
61
|
+
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
|
62
|
+
</p>
|
|
63
|
+
<p>
|
|
64
|
+
Sed ut perspiciatis unde omnis iste natus error sit voluptatem
|
|
65
|
+
accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab
|
|
66
|
+
illo inventore veritatis et quasi architecto beatae vitae dicta sunt
|
|
67
|
+
explicabo.
|
|
68
|
+
</p>
|
|
69
|
+
{Array.from({ length: 10 }).map((_, i) => (
|
|
70
|
+
<p key={i}>
|
|
71
|
+
Paragraph {i + 1}: At vero eos et accusamus et iusto odio dignissimos
|
|
72
|
+
ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti
|
|
73
|
+
quos dolores et quas molestias excepturi sint occaecati cupiditate non
|
|
74
|
+
provident.
|
|
75
|
+
</p>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const meta: Meta<typeof HeroZoomLayout> = {
|
|
82
|
+
title: "Layouts/HeroZoomLayout",
|
|
83
|
+
component: HeroZoomLayout,
|
|
84
|
+
parameters: {
|
|
85
|
+
layout: "fullscreen",
|
|
86
|
+
docs: {
|
|
87
|
+
description: {
|
|
88
|
+
component: `
|
|
89
|
+
# HeroZoomLayout
|
|
90
|
+
|
|
91
|
+
A scroll-driven hero image layout that creates a cinematic zoom effect.
|
|
92
|
+
|
|
93
|
+
## How It Works
|
|
94
|
+
|
|
95
|
+
The layout uses CSS scroll-driven animations (\`animation-timeline: scroll()\`)
|
|
96
|
+
for GPU-accelerated performance. For browsers that don't support this feature,
|
|
97
|
+
it falls back to a JavaScript scroll handler using \`requestAnimationFrame\`.
|
|
98
|
+
|
|
99
|
+
## Configuration
|
|
100
|
+
|
|
101
|
+
| Option | Default | Description |
|
|
102
|
+
|--------|---------|-------------|
|
|
103
|
+
| \`aspectRatio\` | 16/9 | Hero image aspect ratio (width / height) |
|
|
104
|
+
| \`initialFillPercent\` | 85 | Initial viewport height fill percentage |
|
|
105
|
+
| \`headerHeight\` | 100 | Height of the header area in pixels |
|
|
106
|
+
| \`accountForRetina\` | true | Render at higher resolution for Retina displays |
|
|
107
|
+
| \`maxRetinaMultiplier\` | 2 | Cap the retina multiplier |
|
|
108
|
+
|
|
109
|
+
## Browser Support
|
|
110
|
+
|
|
111
|
+
- **Chrome 115+**: Native scroll-driven animations
|
|
112
|
+
- **Safari 16+**: Native scroll-driven animations
|
|
113
|
+
- **Firefox**: JavaScript fallback (smooth)
|
|
114
|
+
- **Mobile Safari**: Optimized for iOS address bar changes
|
|
115
|
+
`,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
tags: ["autodocs"],
|
|
120
|
+
argTypes: {
|
|
121
|
+
config: {
|
|
122
|
+
description: "Configuration options for the scroll animation",
|
|
123
|
+
},
|
|
124
|
+
header: {
|
|
125
|
+
description: "Optional header content",
|
|
126
|
+
},
|
|
127
|
+
hero: {
|
|
128
|
+
description: "Hero content (image, video, etc.)",
|
|
129
|
+
},
|
|
130
|
+
children: {
|
|
131
|
+
description: "Content below the hero",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export default meta;
|
|
137
|
+
type Story = StoryObj<typeof meta>;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Default configuration with a 16:9 aspect ratio hero image and header.
|
|
141
|
+
*/
|
|
142
|
+
export const Default: Story = {
|
|
143
|
+
args: {
|
|
144
|
+
config: {
|
|
145
|
+
aspectRatio: 16 / 9,
|
|
146
|
+
headerHeight: 100,
|
|
147
|
+
initialFillPercent: 85,
|
|
148
|
+
},
|
|
149
|
+
header: <DemoHeader transparent />,
|
|
150
|
+
hero: (
|
|
151
|
+
<img
|
|
152
|
+
src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=2000&q=80"
|
|
153
|
+
alt="Mountain landscape"
|
|
154
|
+
className="w-full h-full object-cover object-center"
|
|
155
|
+
/>
|
|
156
|
+
),
|
|
157
|
+
children: <DemoContent />,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Without a header - the hero image starts at the top of the viewport.
|
|
163
|
+
*/
|
|
164
|
+
export const WithoutHeader: Story = {
|
|
165
|
+
args: {
|
|
166
|
+
config: {
|
|
167
|
+
aspectRatio: 16 / 9,
|
|
168
|
+
initialFillPercent: 90,
|
|
169
|
+
},
|
|
170
|
+
hero: (
|
|
171
|
+
<img
|
|
172
|
+
src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=2000&q=80"
|
|
173
|
+
alt="Mountain peaks"
|
|
174
|
+
className="w-full h-full object-cover object-center"
|
|
175
|
+
/>
|
|
176
|
+
),
|
|
177
|
+
children: <DemoContent />,
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Square aspect ratio (1:1) - useful for product images.
|
|
183
|
+
*/
|
|
184
|
+
export const SquareAspectRatio: Story = {
|
|
185
|
+
args: {
|
|
186
|
+
config: {
|
|
187
|
+
aspectRatio: 1,
|
|
188
|
+
headerHeight: 80,
|
|
189
|
+
initialFillPercent: 80,
|
|
190
|
+
},
|
|
191
|
+
header: <DemoHeader />,
|
|
192
|
+
hero: (
|
|
193
|
+
<img
|
|
194
|
+
src="https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=1500&q=80"
|
|
195
|
+
alt="Product shoe"
|
|
196
|
+
className="w-full h-full object-cover object-center"
|
|
197
|
+
/>
|
|
198
|
+
),
|
|
199
|
+
children: <DemoContent />,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 4:3 aspect ratio - classic photo dimensions.
|
|
205
|
+
*/
|
|
206
|
+
export const FourThreeAspectRatio: Story = {
|
|
207
|
+
args: {
|
|
208
|
+
config: {
|
|
209
|
+
aspectRatio: 4 / 3,
|
|
210
|
+
headerHeight: 100,
|
|
211
|
+
initialFillPercent: 85,
|
|
212
|
+
},
|
|
213
|
+
header: <DemoHeader transparent />,
|
|
214
|
+
hero: (
|
|
215
|
+
<img
|
|
216
|
+
src="https://images.unsplash.com/photo-1519681393784-d120267933ba?w=2000&q=80"
|
|
217
|
+
alt="Night mountain"
|
|
218
|
+
className="w-full h-full object-cover object-center"
|
|
219
|
+
/>
|
|
220
|
+
),
|
|
221
|
+
children: <DemoContent />,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Ultra-wide 21:9 aspect ratio - cinematic experience.
|
|
227
|
+
*/
|
|
228
|
+
export const UltraWide: Story = {
|
|
229
|
+
args: {
|
|
230
|
+
config: {
|
|
231
|
+
aspectRatio: 21 / 9,
|
|
232
|
+
headerHeight: 60,
|
|
233
|
+
initialFillPercent: 70,
|
|
234
|
+
},
|
|
235
|
+
header: <DemoHeader transparent />,
|
|
236
|
+
hero: (
|
|
237
|
+
<img
|
|
238
|
+
src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=2000&q=80"
|
|
239
|
+
alt="Landscape"
|
|
240
|
+
className="w-full h-full object-cover object-center"
|
|
241
|
+
/>
|
|
242
|
+
),
|
|
243
|
+
children: <DemoContent />,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* With a video hero instead of an image.
|
|
249
|
+
*/
|
|
250
|
+
export const VideoHero: Story = {
|
|
251
|
+
args: {
|
|
252
|
+
config: {
|
|
253
|
+
aspectRatio: 16 / 9,
|
|
254
|
+
headerHeight: 100,
|
|
255
|
+
initialFillPercent: 85,
|
|
256
|
+
},
|
|
257
|
+
header: <DemoHeader transparent />,
|
|
258
|
+
hero: (
|
|
259
|
+
<video
|
|
260
|
+
autoPlay
|
|
261
|
+
muted
|
|
262
|
+
loop
|
|
263
|
+
playsInline
|
|
264
|
+
className="w-full h-full object-cover"
|
|
265
|
+
poster="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=2000&q=80"
|
|
266
|
+
>
|
|
267
|
+
<source
|
|
268
|
+
src="https://assets.mixkit.co/videos/preview/mixkit-white-sand-beach-and-palm-trees-1564-large.mp4"
|
|
269
|
+
type="video/mp4"
|
|
270
|
+
/>
|
|
271
|
+
</video>
|
|
272
|
+
),
|
|
273
|
+
children: <DemoContent />,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Gradient background hero - useful for abstract or branded headers.
|
|
279
|
+
*/
|
|
280
|
+
export const GradientHero: Story = {
|
|
281
|
+
args: {
|
|
282
|
+
config: {
|
|
283
|
+
aspectRatio: 16 / 9,
|
|
284
|
+
headerHeight: 100,
|
|
285
|
+
initialFillPercent: 85,
|
|
286
|
+
},
|
|
287
|
+
header: <DemoHeader transparent />,
|
|
288
|
+
hero: (
|
|
289
|
+
<div className="w-full h-full bg-gradient-to-br from-purple-600 via-pink-500 to-orange-400 flex items-center justify-center">
|
|
290
|
+
<h1 className="text-white text-6xl font-bold tracking-tight drop-shadow-lg">
|
|
291
|
+
Your Brand
|
|
292
|
+
</h1>
|
|
293
|
+
</div>
|
|
294
|
+
),
|
|
295
|
+
children: <DemoContent />,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Product mockup use case - typical e-commerce product hero.
|
|
301
|
+
*/
|
|
302
|
+
export const ProductMockup: Story = {
|
|
303
|
+
args: {
|
|
304
|
+
config: {
|
|
305
|
+
aspectRatio: 16 / 9,
|
|
306
|
+
headerHeight: 80,
|
|
307
|
+
initialFillPercent: 80,
|
|
308
|
+
},
|
|
309
|
+
header: (
|
|
310
|
+
<div className="h-full flex items-center justify-between px-6 bg-background/80 backdrop-blur-sm">
|
|
311
|
+
<span className="font-bold">Store</span>
|
|
312
|
+
<div className="flex gap-4">
|
|
313
|
+
<button className="text-sm">Shop</button>
|
|
314
|
+
<button className="text-sm">Cart (0)</button>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
),
|
|
318
|
+
hero: (
|
|
319
|
+
<img
|
|
320
|
+
src="https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=2000&q=80"
|
|
321
|
+
alt="Watch product"
|
|
322
|
+
className="w-full h-full object-cover object-center"
|
|
323
|
+
/>
|
|
324
|
+
),
|
|
325
|
+
children: (
|
|
326
|
+
<div className="bg-background">
|
|
327
|
+
<div className="max-w-2xl mx-auto px-6 py-12">
|
|
328
|
+
<span className="text-primary text-sm font-medium">New Release</span>
|
|
329
|
+
<h1 className="text-3xl font-bold mt-2">Premium Watch</h1>
|
|
330
|
+
<p className="text-2xl font-semibold mt-4">$299.00</p>
|
|
331
|
+
<p className="text-muted-foreground mt-4">
|
|
332
|
+
Crafted with precision and designed for everyday elegance. This
|
|
333
|
+
premium timepiece combines modern aesthetics with reliable
|
|
334
|
+
performance.
|
|
335
|
+
</p>
|
|
336
|
+
<button className="mt-6 px-8 py-3 bg-primary text-primary-foreground rounded-lg font-medium">
|
|
337
|
+
Add to Cart
|
|
338
|
+
</button>
|
|
339
|
+
{Array.from({ length: 8 }).map((_, i) => (
|
|
340
|
+
<p key={i} className="mt-8 text-muted-foreground">
|
|
341
|
+
Product detail paragraph {i + 1}: Lorem ipsum dolor sit amet,
|
|
342
|
+
consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut
|
|
343
|
+
labore et dolore magna aliqua.
|
|
344
|
+
</p>
|
|
345
|
+
))}
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
),
|
|
349
|
+
},
|
|
350
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { ReactNode, CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration options for the HeroZoomLayout component.
|
|
5
|
+
*
|
|
6
|
+
* These control the scroll-driven animation behavior including
|
|
7
|
+
* the hero image scaling and header slide-up effects.
|
|
8
|
+
*/
|
|
9
|
+
export interface HeroZoomConfig {
|
|
10
|
+
/**
|
|
11
|
+
* Aspect ratio of the hero image (width / height).
|
|
12
|
+
* Common values: 16/9 (1.778), 4/3 (1.333), 1 (square)
|
|
13
|
+
* @default 16/9
|
|
14
|
+
*/
|
|
15
|
+
aspectRatio?: number;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initial visual scale factor.
|
|
19
|
+
* Hero starts at initialScale× its final size, then zooms down to 1×.
|
|
20
|
+
* The final size is always 100vw width (full viewport width).
|
|
21
|
+
* @default 2.5
|
|
22
|
+
* @example initialScale: 2.0 means hero starts at 200% width and shrinks to 100%
|
|
23
|
+
*/
|
|
24
|
+
initialScale?: number;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Height of the header area in pixels.
|
|
28
|
+
* This determines the scroll range for the slide-up phase.
|
|
29
|
+
* @default 100
|
|
30
|
+
*/
|
|
31
|
+
headerHeight?: number;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether to account for Retina displays by rendering at higher resolution.
|
|
35
|
+
* When true, renders at devicePixelRatio for crisp images (uses more GPU memory).
|
|
36
|
+
* @default true
|
|
37
|
+
*/
|
|
38
|
+
accountForRetina?: boolean;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Maximum retina multiplier to cap memory usage on 3x displays.
|
|
42
|
+
* Only applies when accountForRetina is true.
|
|
43
|
+
* @default 2
|
|
44
|
+
*/
|
|
45
|
+
maxRetinaMultiplier?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Props for the HeroZoomLayout component.
|
|
50
|
+
*/
|
|
51
|
+
export interface HeroZoomLayoutProps {
|
|
52
|
+
/**
|
|
53
|
+
* Configuration for the scroll animation behavior.
|
|
54
|
+
* All options have sensible defaults.
|
|
55
|
+
*/
|
|
56
|
+
config?: HeroZoomConfig;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Optional header content that slides up as the user scrolls.
|
|
60
|
+
* Rendered in a fixed position at the top of the viewport.
|
|
61
|
+
*/
|
|
62
|
+
header?: ReactNode;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The hero content (typically an image) that scales down on scroll.
|
|
66
|
+
* This is the main visual element that animates.
|
|
67
|
+
*/
|
|
68
|
+
hero: ReactNode;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Content that appears below the hero after scrolling.
|
|
72
|
+
* This is the main page content.
|
|
73
|
+
*/
|
|
74
|
+
children: ReactNode;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Additional CSS class names for the root container.
|
|
78
|
+
*/
|
|
79
|
+
className?: string;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Additional inline styles for the root container.
|
|
83
|
+
*/
|
|
84
|
+
style?: CSSProperties;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Internal scale values computed from viewport and config.
|
|
89
|
+
* Used by the useHeroZoomScales hook.
|
|
90
|
+
*/
|
|
91
|
+
export interface HeroZoomScales {
|
|
92
|
+
/**
|
|
93
|
+
* The visual scale the user sees (e.g., 2x → 1x during animation).
|
|
94
|
+
*/
|
|
95
|
+
visual: number;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The render scale including retina multiplier for pixel density.
|
|
99
|
+
* Container is sized at this scale for crisp rendering.
|
|
100
|
+
*/
|
|
101
|
+
render: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Default configuration values for HeroZoomLayout.
|
|
106
|
+
*/
|
|
107
|
+
export const DEFAULT_HERO_ZOOM_CONFIG: Required<HeroZoomConfig> = {
|
|
108
|
+
aspectRatio: 16 / 9,
|
|
109
|
+
initialScale: 2.5,
|
|
110
|
+
headerHeight: 100,
|
|
111
|
+
accountForRetina: true,
|
|
112
|
+
maxRetinaMultiplier: 2,
|
|
113
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback } from "react";
|
|
4
|
+
import type { HeroZoomConfig, HeroZoomScales } from "./types";
|
|
5
|
+
import { DEFAULT_HERO_ZOOM_CONFIG } from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extended scales including hydration state for SSR-safe rendering.
|
|
9
|
+
*/
|
|
10
|
+
export interface HeroZoomScalesWithHydration extends HeroZoomScales {
|
|
11
|
+
/**
|
|
12
|
+
* Whether JS has hydrated and computed exact scales.
|
|
13
|
+
* When false, the layout uses SSR-safe default values.
|
|
14
|
+
* When true, the layout uses JS-computed scales (may include DPR adjustment).
|
|
15
|
+
*/
|
|
16
|
+
isHydrated: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Computes the visual and render scales for the hero zoom animation.
|
|
21
|
+
*
|
|
22
|
+
* This hook is significantly simplified from the viewport-measurement approach.
|
|
23
|
+
* The visual scale now comes directly from config (initialScale), making it:
|
|
24
|
+
* - SSR-safe: No viewport measurement needed
|
|
25
|
+
* - Predictable: Direct control over zoom level
|
|
26
|
+
* - Layout-shift-free: Same value used for SSR and hydration
|
|
27
|
+
*
|
|
28
|
+
* The only client-side computation is the device pixel ratio (DPR) for
|
|
29
|
+
* retina rendering, which only affects render quality, not visual size.
|
|
30
|
+
*
|
|
31
|
+
* @param config - Configuration options for the animation
|
|
32
|
+
* @returns The computed visual and render scales, plus hydration state
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* const scales = useHeroZoomScales({ initialScale: 2.0, aspectRatio: 16/9 });
|
|
37
|
+
* // scales.visual = 2.0 (from config, what user sees)
|
|
38
|
+
* // scales.render = 4.0 (includes retina multiplier for crisp images)
|
|
39
|
+
* // scales.isHydrated = true (after client-side DPR calculation)
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function useHeroZoomScales(
|
|
43
|
+
config: HeroZoomConfig = {}
|
|
44
|
+
): HeroZoomScalesWithHydration {
|
|
45
|
+
const { initialScale, accountForRetina, maxRetinaMultiplier } = {
|
|
46
|
+
...DEFAULT_HERO_ZOOM_CONFIG,
|
|
47
|
+
...config,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Track whether we've hydrated (client-side calculation has run)
|
|
51
|
+
const [isHydrated, setIsHydrated] = useState(false);
|
|
52
|
+
|
|
53
|
+
// SSR-safe initial state - visual comes directly from config!
|
|
54
|
+
// No viewport measurement needed.
|
|
55
|
+
// For SSR, use the same multiplier that will be used after hydration.
|
|
56
|
+
// If accountForRetina is true, use maxRetinaMultiplier (capped DPR).
|
|
57
|
+
// If accountForRetina is false, use 2 (the fallback multiplier).
|
|
58
|
+
const ssrMultiplier = accountForRetina ? maxRetinaMultiplier : 2;
|
|
59
|
+
const [scales, setScales] = useState<HeroZoomScales>(() => ({
|
|
60
|
+
visual: initialScale,
|
|
61
|
+
render: initialScale * ssrMultiplier,
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
const calculateScales = useCallback((): HeroZoomScales => {
|
|
65
|
+
// Visual scale comes directly from config - no viewport measurement!
|
|
66
|
+
// Clamp to reasonable bounds (1x to 4x)
|
|
67
|
+
const visualScale = Math.max(1, Math.min(4, initialScale));
|
|
68
|
+
|
|
69
|
+
// Render scale for retina displays
|
|
70
|
+
// This is the only client-side computation - affects image quality, not layout
|
|
71
|
+
let renderScale: number;
|
|
72
|
+
if (accountForRetina) {
|
|
73
|
+
const dpr = Math.min(
|
|
74
|
+
window.devicePixelRatio || 1,
|
|
75
|
+
maxRetinaMultiplier
|
|
76
|
+
);
|
|
77
|
+
renderScale = visualScale * dpr;
|
|
78
|
+
} else {
|
|
79
|
+
// Even without retina, we need render > visual for the scale animation.
|
|
80
|
+
// Use 2x as default multiplier.
|
|
81
|
+
renderScale = visualScale * 2;
|
|
82
|
+
}
|
|
83
|
+
renderScale = Math.max(1, Math.min(8, renderScale));
|
|
84
|
+
|
|
85
|
+
return { visual: visualScale, render: renderScale };
|
|
86
|
+
}, [initialScale, accountForRetina, maxRetinaMultiplier]);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const supportsScrollTimeline = CSS.supports(
|
|
90
|
+
"animation-timeline",
|
|
91
|
+
"scroll()"
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Track last known width to detect real resizes vs address bar changes
|
|
95
|
+
let lastWidth = window.innerWidth;
|
|
96
|
+
|
|
97
|
+
const updateScales = (force = false) => {
|
|
98
|
+
const currentWidth = window.innerWidth;
|
|
99
|
+
|
|
100
|
+
// For scroll-timeline browsers, only update on actual width changes
|
|
101
|
+
// This prevents flash when iOS address bar expands/collapses
|
|
102
|
+
if (!force && supportsScrollTimeline && currentWidth === lastWidth) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
lastWidth = currentWidth;
|
|
106
|
+
|
|
107
|
+
const newScales = calculateScales();
|
|
108
|
+
setScales(newScales);
|
|
109
|
+
|
|
110
|
+
// Update CSS custom properties for scroll-driven animations
|
|
111
|
+
document.documentElement.style.setProperty(
|
|
112
|
+
"--hero-zoom-render-scale",
|
|
113
|
+
String(newScales.render)
|
|
114
|
+
);
|
|
115
|
+
document.documentElement.style.setProperty(
|
|
116
|
+
"--hero-zoom-initial-transform",
|
|
117
|
+
String(newScales.visual / newScales.render)
|
|
118
|
+
);
|
|
119
|
+
document.documentElement.style.setProperty(
|
|
120
|
+
"--hero-zoom-final-transform",
|
|
121
|
+
String(1 / newScales.render)
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Initial update - mark as hydrated after first calculation
|
|
126
|
+
updateScales(true);
|
|
127
|
+
setIsHydrated(true);
|
|
128
|
+
|
|
129
|
+
// For scroll-timeline browsers, prefer orientationchange over resize
|
|
130
|
+
// to avoid updates during address bar changes
|
|
131
|
+
if (supportsScrollTimeline) {
|
|
132
|
+
const handleOrientationChange = () => {
|
|
133
|
+
// Small delay to let the viewport settle after orientation change
|
|
134
|
+
setTimeout(() => updateScales(true), 100);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const handleResize = () => updateScales(false);
|
|
138
|
+
|
|
139
|
+
window.addEventListener("orientationchange", handleOrientationChange);
|
|
140
|
+
window.addEventListener("resize", handleResize);
|
|
141
|
+
|
|
142
|
+
return () => {
|
|
143
|
+
window.removeEventListener("orientationchange", handleOrientationChange);
|
|
144
|
+
window.removeEventListener("resize", handleResize);
|
|
145
|
+
};
|
|
146
|
+
} else {
|
|
147
|
+
// Fallback browsers: update on all resizes
|
|
148
|
+
const handleResize = () => updateScales(true);
|
|
149
|
+
window.addEventListener("resize", handleResize);
|
|
150
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
151
|
+
}
|
|
152
|
+
}, [calculateScales]);
|
|
153
|
+
|
|
154
|
+
// Return calculated scales with hydration state
|
|
155
|
+
return { ...scales, isHydrated };
|
|
156
|
+
}
|