@snowcone-app/ui 0.1.43 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +18 -4
  3. package/dist/index.cjs +5 -2
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.js +5 -2
  6. package/dist/index.js.map +1 -1
  7. package/package.json +9 -5
  8. package/src/components/CanvasIsolationBoundary.tsx +202 -0
  9. package/src/components/LoadingOverlayPrism.tsx +251 -0
  10. package/src/composed/AddToCart.tsx +229 -0
  11. package/src/composed/ArtAlignment.tsx +703 -0
  12. package/src/composed/ArtSelector.tsx +290 -0
  13. package/src/composed/ArtworkCustomizer.tsx +212 -0
  14. package/src/composed/CanvasEditor.tsx +79 -0
  15. package/src/composed/ColorPicker.tsx +111 -0
  16. package/src/composed/CurrentSelectionDisplay.tsx +86 -0
  17. package/src/composed/HeroProductImage.tsx +1079 -0
  18. package/src/composed/Lightbox.index.ts +2 -0
  19. package/src/composed/Lightbox.tsx +230 -0
  20. package/src/composed/PlacementClipShapeSelector.tsx +88 -0
  21. package/src/composed/PlacementTabs.tsx +179 -0
  22. package/src/composed/ProductCard.tsx +298 -0
  23. package/src/composed/ProductGallery.tsx +54 -0
  24. package/src/composed/ProductImage.tsx +129 -0
  25. package/src/composed/ProductList.tsx +147 -0
  26. package/src/composed/ProductOptions.tsx +305 -0
  27. package/src/composed/RealtimeMockup.tsx +121 -0
  28. package/src/composed/TileCount.tsx +348 -0
  29. package/src/composed/carousels/HeroCarousel.tsx +240 -0
  30. package/src/composed/carousels/MobileProductCarousel.tsx +1002 -0
  31. package/src/composed/carousels/index.ts +11 -0
  32. package/src/composed/carousels/types.ts +58 -0
  33. package/src/composed/grids/MasonryGrid.tsx +238 -0
  34. package/src/composed/grids/index.ts +9 -0
  35. package/src/composed/search/CurrentRefinements.tsx +80 -0
  36. package/src/composed/search/Filters.tsx +49 -0
  37. package/src/composed/search/FiltersButton.tsx +57 -0
  38. package/src/composed/search/FiltersDrawer.tsx +375 -0
  39. package/src/composed/search/ProductGrid.tsx +118 -0
  40. package/src/composed/search/ProductHit.tsx +56 -0
  41. package/src/composed/search/SearchBox.tsx +109 -0
  42. package/src/composed/search/SearchProvider.tsx +136 -0
  43. package/src/composed/search/facetConfig.ts +16 -0
  44. package/src/composed/search/index.ts +22 -0
  45. package/src/composed/search/meilisearchAdapter.ts +20 -0
  46. package/src/composed/search/types.ts +22 -0
  47. package/src/composed/zoom/EnhancedImageViewer.tsx +505 -0
  48. package/src/composed/zoom/ResponsiveZoom.tsx +134 -0
  49. package/src/composed/zoom/ZoomOverlay.tsx +194 -0
  50. package/src/composed/zoom/index.ts +12 -0
  51. package/src/composed/zoom/types.ts +12 -0
  52. package/src/design-system/ColorPalette.tsx +126 -0
  53. package/src/design-system/ColorSwatch.tsx +49 -0
  54. package/src/design-system/DesignSystemPage.tsx +130 -0
  55. package/src/design-system/ThemeSwitcher.tsx +181 -0
  56. package/src/design-system/TypographyScale.tsx +106 -0
  57. package/src/design-system/index.ts +5 -0
  58. package/src/ecommerce/stories/HeroProductImage.stories.tsx +66 -0
  59. package/src/ecommerce/stories/PDPHeroGallery.stories.tsx +105 -0
  60. package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +472 -0
  61. package/src/ecommerce/stories/PDPLayout.stories.tsx +365 -0
  62. package/src/hooks/useBrand.ts +41 -0
  63. package/src/hooks/useCanvasContext.ts +127 -0
  64. package/src/hooks/useDeviceDetection.ts +64 -0
  65. package/src/hooks/useFocusTrap.ts +70 -0
  66. package/src/hooks/useImagePreloader.ts +268 -0
  67. package/src/hooks/useImageTransition.ts +608 -0
  68. package/src/hooks/usePlacementsProcessor.ts +74 -0
  69. package/src/hooks/useProductGallery.ts +193 -0
  70. package/src/hooks/useProductPage.ts +467 -0
  71. package/src/hooks/useRenderGuard.ts +96 -0
  72. package/src/hooks/useScrollDirection.ts +196 -0
  73. package/src/hooks/viewport/index.ts +25 -0
  74. package/src/hooks/viewport/useContainerWidth.ts +59 -0
  75. package/src/hooks/viewport/useMediaQuery.ts +52 -0
  76. package/src/hooks/viewport/useResponsiveImageCap.ts +149 -0
  77. package/src/hooks/viewport/useViewportDimensions.ts +135 -0
  78. package/src/hooks/viewport/useWideMonitorMode.ts +150 -0
  79. package/src/hooks/visibility/index.ts +15 -0
  80. package/src/hooks/visibility/observerPool.ts +150 -0
  81. package/src/index.ts +240 -0
  82. package/src/layouts/hero-zoom/HeroShrinkLayout.tsx +209 -0
  83. package/src/layouts/hero-zoom/HeroZoomLayout.tsx +351 -0
  84. package/src/layouts/hero-zoom/index.ts +30 -0
  85. package/src/layouts/hero-zoom/stories/HeroZoomLayout.stories.tsx +350 -0
  86. package/src/layouts/hero-zoom/types.ts +113 -0
  87. package/src/layouts/hero-zoom/useHeroZoomScales.ts +156 -0
  88. package/src/layouts/index.ts +9 -0
  89. package/src/layouts/pdp/EdgeBlurBox.tsx +210 -0
  90. package/src/layouts/pdp/ImageBlurExtension.tsx +215 -0
  91. package/src/layouts/pdp/ImageEdgeBlur.tsx +215 -0
  92. package/src/layouts/pdp/PDPLayout.tsx +246 -0
  93. package/src/layouts/pdp/SimpleImageBlur.tsx +140 -0
  94. package/src/layouts/pdp/index.ts +40 -0
  95. package/src/lib/env.ts +15 -0
  96. package/src/lib/locale.ts +167 -0
  97. package/src/lib/router.tsx +46 -0
  98. package/src/lib/utils.ts +6 -0
  99. package/src/lightbox/README.md +77 -0
  100. package/src/next/index.tsx +26 -0
  101. package/src/patterns/MockupPriorityProvider.tsx +1014 -0
  102. package/src/patterns/Product.tsx +850 -0
  103. package/src/patterns/ProductPageProvider.tsx +224 -0
  104. package/src/patterns/RealtimeProvider.tsx +1162 -0
  105. package/src/patterns/ShopProvider.tsx +603 -0
  106. package/src/personalization/PersonalizationBridge.tsx +235 -0
  107. package/src/personalization/PersonalizationContext.ts +29 -0
  108. package/src/personalization/PersonalizationInputs.tsx +110 -0
  109. package/src/personalization/PersonalizationProvider.tsx +407 -0
  110. package/src/personalization/canvas-stub.d.ts +22 -0
  111. package/src/personalization/index.ts +43 -0
  112. package/src/personalization/types.ts +48 -0
  113. package/src/personalization/usePersonalization.ts +32 -0
  114. package/src/personalization/usePersonalizationShimmer.ts +159 -0
  115. package/src/personalization/utils.ts +59 -0
  116. package/src/primitives/BrandLogo.tsx +65 -0
  117. package/src/primitives/BrandName.tsx +51 -0
  118. package/src/primitives/Button.tsx +123 -0
  119. package/src/primitives/ColorSwatch.tsx +221 -0
  120. package/src/primitives/DragHintAnimation.tsx +190 -0
  121. package/src/primitives/EdgeSwipeGuards.tsx +60 -0
  122. package/src/primitives/FloatingActionGroup.tsx +176 -0
  123. package/src/primitives/ProductPrice.tsx +171 -0
  124. package/src/primitives/ProgressiveBlur.tsx +295 -0
  125. package/src/primitives/ThemeToggle.tsx +125 -0
  126. package/src/primitives/__tests__/story-coverage.test.ts +98 -0
  127. package/src/primitives/accordion.tsx +280 -0
  128. package/src/primitives/badge.tsx +137 -0
  129. package/src/primitives/card.tsx +61 -0
  130. package/src/primitives/checkbox.tsx +56 -0
  131. package/src/primitives/collapsible.tsx +51 -0
  132. package/src/primitives/drawer.tsx +828 -0
  133. package/src/primitives/dropdown-menu.tsx +197 -0
  134. package/src/primitives/fieldset.tsx +73 -0
  135. package/src/primitives/index.ts +138 -0
  136. package/src/primitives/input.tsx +91 -0
  137. package/src/primitives/kbd.tsx +130 -0
  138. package/src/primitives/label.tsx +20 -0
  139. package/src/primitives/link.tsx +182 -0
  140. package/src/primitives/popover.tsx +80 -0
  141. package/src/primitives/radio-group.tsx +79 -0
  142. package/src/primitives/scroll-fade.tsx +159 -0
  143. package/src/primitives/select.tsx +170 -0
  144. package/src/primitives/separator.tsx +25 -0
  145. package/src/primitives/slider.tsx +221 -0
  146. package/src/primitives/spinner.tsx +72 -0
  147. package/src/primitives/stories/Accordion.stories.tsx +121 -0
  148. package/src/primitives/stories/Badge.stories.tsx +221 -0
  149. package/src/primitives/stories/Button.stories.tsx +185 -0
  150. package/src/primitives/stories/Card.stories.tsx +171 -0
  151. package/src/primitives/stories/Checkbox.stories.tsx +214 -0
  152. package/src/primitives/stories/Collapsible.stories.tsx +230 -0
  153. package/src/primitives/stories/Drawer.stories.tsx +378 -0
  154. package/src/primitives/stories/DropdownMenu.stories.tsx +182 -0
  155. package/src/primitives/stories/Fieldset.stories.tsx +212 -0
  156. package/src/primitives/stories/Input.stories.tsx +172 -0
  157. package/src/primitives/stories/Kbd.stories.tsx +183 -0
  158. package/src/primitives/stories/Label.stories.tsx +98 -0
  159. package/src/primitives/stories/Link.stories.tsx +260 -0
  160. package/src/primitives/stories/Popover.stories.tsx +178 -0
  161. package/src/primitives/stories/RadioGroup.stories.tsx +205 -0
  162. package/src/primitives/stories/Select.stories.tsx +222 -0
  163. package/src/primitives/stories/Separator.stories.tsx +134 -0
  164. package/src/primitives/stories/Slider.stories.tsx +203 -0
  165. package/src/primitives/stories/Spinner.stories.tsx +142 -0
  166. package/src/primitives/stories/Surface.stories.tsx +257 -0
  167. package/src/primitives/stories/Switch.stories.tsx +131 -0
  168. package/src/primitives/stories/Tabs.stories.tsx +275 -0
  169. package/src/primitives/stories/TextField.stories.tsx +139 -0
  170. package/src/primitives/stories/Textarea.stories.tsx +148 -0
  171. package/src/primitives/stories/Tooltip.stories.tsx +119 -0
  172. package/src/primitives/surface.tsx +86 -0
  173. package/src/primitives/switch.tsx +35 -0
  174. package/src/primitives/tabs.tsx +206 -0
  175. package/src/primitives/text-field.tsx +84 -0
  176. package/src/primitives/textarea.tsx +50 -0
  177. package/src/primitives/tooltip.tsx +58 -0
  178. package/src/services/CanvasExportService.ts +518 -0
  179. package/src/styles/base.css +380 -0
  180. package/src/styles/defaults.css +280 -0
  181. package/src/styles/globals.css +1242 -0
  182. package/src/styles/index.css +17 -0
  183. package/src/styles/ne-themes.css +4740 -0
  184. package/src/styles/tailwind.css +11 -0
  185. package/src/styles/tokens.css +117 -0
  186. package/src/styles/utilities.css +188 -0
  187. package/src/themes/apply-theme.ts +449 -0
  188. package/src/themes/getThemeStyles.ts +454 -0
  189. package/src/themes/index.ts +48 -0
  190. package/src/themes/oklch-theme.ts +283 -0
  191. package/src/themes/presets.ts +989 -0
  192. package/src/themes/types.ts +386 -0
  193. package/src/themes/useTheme.tsx +450 -0
  194. package/src/utils/dev-warnings.ts +161 -0
  195. package/src/utils/devWarnings.ts +153 -0
  196. 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
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Layout Components
3
+ *
4
+ * Pre-built layout patterns for common UI scenarios.
5
+ * Each layout is designed to be composable and customizable.
6
+ */
7
+
8
+ export * from "./hero-zoom";
9
+ export * from "./pdp";