@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.
Files changed (192) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +18 -4
  3. package/package.json +9 -5
  4. package/src/components/CanvasIsolationBoundary.tsx +202 -0
  5. package/src/components/LoadingOverlayPrism.tsx +251 -0
  6. package/src/composed/AddToCart.tsx +229 -0
  7. package/src/composed/ArtAlignment.tsx +703 -0
  8. package/src/composed/ArtSelector.tsx +290 -0
  9. package/src/composed/ArtworkCustomizer.tsx +212 -0
  10. package/src/composed/CanvasEditor.tsx +79 -0
  11. package/src/composed/ColorPicker.tsx +111 -0
  12. package/src/composed/CurrentSelectionDisplay.tsx +86 -0
  13. package/src/composed/HeroProductImage.tsx +1071 -0
  14. package/src/composed/Lightbox.index.ts +2 -0
  15. package/src/composed/Lightbox.tsx +230 -0
  16. package/src/composed/PlacementClipShapeSelector.tsx +88 -0
  17. package/src/composed/PlacementTabs.tsx +179 -0
  18. package/src/composed/ProductCard.tsx +298 -0
  19. package/src/composed/ProductGallery.tsx +54 -0
  20. package/src/composed/ProductImage.tsx +129 -0
  21. package/src/composed/ProductList.tsx +147 -0
  22. package/src/composed/ProductOptions.tsx +305 -0
  23. package/src/composed/RealtimeMockup.tsx +121 -0
  24. package/src/composed/TileCount.tsx +348 -0
  25. package/src/composed/carousels/HeroCarousel.tsx +240 -0
  26. package/src/composed/carousels/MobileProductCarousel.tsx +1002 -0
  27. package/src/composed/carousels/index.ts +11 -0
  28. package/src/composed/carousels/types.ts +58 -0
  29. package/src/composed/grids/MasonryGrid.tsx +238 -0
  30. package/src/composed/grids/index.ts +9 -0
  31. package/src/composed/search/CurrentRefinements.tsx +80 -0
  32. package/src/composed/search/Filters.tsx +49 -0
  33. package/src/composed/search/FiltersButton.tsx +57 -0
  34. package/src/composed/search/FiltersDrawer.tsx +375 -0
  35. package/src/composed/search/ProductGrid.tsx +118 -0
  36. package/src/composed/search/ProductHit.tsx +56 -0
  37. package/src/composed/search/SearchBox.tsx +109 -0
  38. package/src/composed/search/SearchProvider.tsx +136 -0
  39. package/src/composed/search/facetConfig.ts +16 -0
  40. package/src/composed/search/index.ts +22 -0
  41. package/src/composed/search/meilisearchAdapter.ts +20 -0
  42. package/src/composed/search/types.ts +22 -0
  43. package/src/composed/zoom/EnhancedImageViewer.tsx +505 -0
  44. package/src/composed/zoom/ResponsiveZoom.tsx +134 -0
  45. package/src/composed/zoom/ZoomOverlay.tsx +194 -0
  46. package/src/composed/zoom/index.ts +12 -0
  47. package/src/composed/zoom/types.ts +12 -0
  48. package/src/design-system/ColorPalette.tsx +126 -0
  49. package/src/design-system/ColorSwatch.tsx +49 -0
  50. package/src/design-system/DesignSystemPage.tsx +130 -0
  51. package/src/design-system/ThemeSwitcher.tsx +181 -0
  52. package/src/design-system/TypographyScale.tsx +106 -0
  53. package/src/design-system/index.ts +5 -0
  54. package/src/ecommerce/stories/HeroProductImage.stories.tsx +66 -0
  55. package/src/ecommerce/stories/PDPHeroGallery.stories.tsx +105 -0
  56. package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +472 -0
  57. package/src/ecommerce/stories/PDPLayout.stories.tsx +365 -0
  58. package/src/hooks/useBrand.ts +41 -0
  59. package/src/hooks/useCanvasContext.ts +127 -0
  60. package/src/hooks/useDeviceDetection.ts +64 -0
  61. package/src/hooks/useFocusTrap.ts +70 -0
  62. package/src/hooks/useImagePreloader.ts +268 -0
  63. package/src/hooks/useImageTransition.ts +608 -0
  64. package/src/hooks/usePlacementsProcessor.ts +74 -0
  65. package/src/hooks/useProductGallery.ts +193 -0
  66. package/src/hooks/useProductPage.ts +467 -0
  67. package/src/hooks/useRenderGuard.ts +96 -0
  68. package/src/hooks/useScrollDirection.ts +196 -0
  69. package/src/hooks/viewport/index.ts +25 -0
  70. package/src/hooks/viewport/useContainerWidth.ts +59 -0
  71. package/src/hooks/viewport/useMediaQuery.ts +52 -0
  72. package/src/hooks/viewport/useResponsiveImageCap.ts +149 -0
  73. package/src/hooks/viewport/useViewportDimensions.ts +135 -0
  74. package/src/hooks/viewport/useWideMonitorMode.ts +150 -0
  75. package/src/hooks/visibility/index.ts +15 -0
  76. package/src/hooks/visibility/observerPool.ts +150 -0
  77. package/src/index.ts +240 -0
  78. package/src/layouts/hero-zoom/HeroShrinkLayout.tsx +209 -0
  79. package/src/layouts/hero-zoom/HeroZoomLayout.tsx +351 -0
  80. package/src/layouts/hero-zoom/index.ts +30 -0
  81. package/src/layouts/hero-zoom/stories/HeroZoomLayout.stories.tsx +350 -0
  82. package/src/layouts/hero-zoom/types.ts +113 -0
  83. package/src/layouts/hero-zoom/useHeroZoomScales.ts +156 -0
  84. package/src/layouts/index.ts +9 -0
  85. package/src/layouts/pdp/EdgeBlurBox.tsx +210 -0
  86. package/src/layouts/pdp/ImageBlurExtension.tsx +215 -0
  87. package/src/layouts/pdp/ImageEdgeBlur.tsx +215 -0
  88. package/src/layouts/pdp/PDPLayout.tsx +246 -0
  89. package/src/layouts/pdp/SimpleImageBlur.tsx +140 -0
  90. package/src/layouts/pdp/index.ts +40 -0
  91. package/src/lib/env.ts +15 -0
  92. package/src/lib/locale.ts +167 -0
  93. package/src/lib/router.tsx +46 -0
  94. package/src/lib/utils.ts +6 -0
  95. package/src/lightbox/README.md +77 -0
  96. package/src/next/index.tsx +26 -0
  97. package/src/patterns/MockupPriorityProvider.tsx +1014 -0
  98. package/src/patterns/Product.tsx +850 -0
  99. package/src/patterns/ProductPageProvider.tsx +224 -0
  100. package/src/patterns/RealtimeProvider.tsx +1162 -0
  101. package/src/patterns/ShopProvider.tsx +603 -0
  102. package/src/personalization/PersonalizationBridge.tsx +235 -0
  103. package/src/personalization/PersonalizationContext.ts +29 -0
  104. package/src/personalization/PersonalizationInputs.tsx +110 -0
  105. package/src/personalization/PersonalizationProvider.tsx +407 -0
  106. package/src/personalization/canvas-stub.d.ts +22 -0
  107. package/src/personalization/index.ts +43 -0
  108. package/src/personalization/types.ts +48 -0
  109. package/src/personalization/usePersonalization.ts +32 -0
  110. package/src/personalization/usePersonalizationShimmer.ts +159 -0
  111. package/src/personalization/utils.ts +59 -0
  112. package/src/primitives/BrandLogo.tsx +65 -0
  113. package/src/primitives/BrandName.tsx +51 -0
  114. package/src/primitives/Button.tsx +123 -0
  115. package/src/primitives/ColorSwatch.tsx +221 -0
  116. package/src/primitives/DragHintAnimation.tsx +190 -0
  117. package/src/primitives/EdgeSwipeGuards.tsx +60 -0
  118. package/src/primitives/FloatingActionGroup.tsx +176 -0
  119. package/src/primitives/ProductPrice.tsx +171 -0
  120. package/src/primitives/ProgressiveBlur.tsx +295 -0
  121. package/src/primitives/ThemeToggle.tsx +125 -0
  122. package/src/primitives/__tests__/story-coverage.test.ts +98 -0
  123. package/src/primitives/accordion.tsx +280 -0
  124. package/src/primitives/badge.tsx +137 -0
  125. package/src/primitives/card.tsx +61 -0
  126. package/src/primitives/checkbox.tsx +56 -0
  127. package/src/primitives/collapsible.tsx +51 -0
  128. package/src/primitives/drawer.tsx +828 -0
  129. package/src/primitives/dropdown-menu.tsx +197 -0
  130. package/src/primitives/fieldset.tsx +73 -0
  131. package/src/primitives/index.ts +138 -0
  132. package/src/primitives/input.tsx +91 -0
  133. package/src/primitives/kbd.tsx +130 -0
  134. package/src/primitives/label.tsx +20 -0
  135. package/src/primitives/link.tsx +182 -0
  136. package/src/primitives/popover.tsx +80 -0
  137. package/src/primitives/radio-group.tsx +79 -0
  138. package/src/primitives/scroll-fade.tsx +159 -0
  139. package/src/primitives/select.tsx +170 -0
  140. package/src/primitives/separator.tsx +25 -0
  141. package/src/primitives/slider.tsx +221 -0
  142. package/src/primitives/spinner.tsx +72 -0
  143. package/src/primitives/stories/Accordion.stories.tsx +121 -0
  144. package/src/primitives/stories/Badge.stories.tsx +221 -0
  145. package/src/primitives/stories/Button.stories.tsx +185 -0
  146. package/src/primitives/stories/Card.stories.tsx +171 -0
  147. package/src/primitives/stories/Checkbox.stories.tsx +214 -0
  148. package/src/primitives/stories/Collapsible.stories.tsx +230 -0
  149. package/src/primitives/stories/Drawer.stories.tsx +378 -0
  150. package/src/primitives/stories/DropdownMenu.stories.tsx +182 -0
  151. package/src/primitives/stories/Fieldset.stories.tsx +212 -0
  152. package/src/primitives/stories/Input.stories.tsx +172 -0
  153. package/src/primitives/stories/Kbd.stories.tsx +183 -0
  154. package/src/primitives/stories/Label.stories.tsx +98 -0
  155. package/src/primitives/stories/Link.stories.tsx +260 -0
  156. package/src/primitives/stories/Popover.stories.tsx +178 -0
  157. package/src/primitives/stories/RadioGroup.stories.tsx +205 -0
  158. package/src/primitives/stories/Select.stories.tsx +222 -0
  159. package/src/primitives/stories/Separator.stories.tsx +134 -0
  160. package/src/primitives/stories/Slider.stories.tsx +203 -0
  161. package/src/primitives/stories/Spinner.stories.tsx +142 -0
  162. package/src/primitives/stories/Surface.stories.tsx +257 -0
  163. package/src/primitives/stories/Switch.stories.tsx +131 -0
  164. package/src/primitives/stories/Tabs.stories.tsx +275 -0
  165. package/src/primitives/stories/TextField.stories.tsx +139 -0
  166. package/src/primitives/stories/Textarea.stories.tsx +148 -0
  167. package/src/primitives/stories/Tooltip.stories.tsx +119 -0
  168. package/src/primitives/surface.tsx +86 -0
  169. package/src/primitives/switch.tsx +35 -0
  170. package/src/primitives/tabs.tsx +206 -0
  171. package/src/primitives/text-field.tsx +84 -0
  172. package/src/primitives/textarea.tsx +50 -0
  173. package/src/primitives/tooltip.tsx +58 -0
  174. package/src/services/CanvasExportService.ts +518 -0
  175. package/src/styles/base.css +380 -0
  176. package/src/styles/defaults.css +280 -0
  177. package/src/styles/globals.css +1242 -0
  178. package/src/styles/index.css +17 -0
  179. package/src/styles/ne-themes.css +4740 -0
  180. package/src/styles/tailwind.css +11 -0
  181. package/src/styles/tokens.css +117 -0
  182. package/src/styles/utilities.css +188 -0
  183. package/src/themes/apply-theme.ts +449 -0
  184. package/src/themes/getThemeStyles.ts +454 -0
  185. package/src/themes/index.ts +48 -0
  186. package/src/themes/oklch-theme.ts +283 -0
  187. package/src/themes/presets.ts +989 -0
  188. package/src/themes/types.ts +386 -0
  189. package/src/themes/useTheme.tsx +450 -0
  190. package/src/utils/dev-warnings.ts +161 -0
  191. package/src/utils/devWarnings.ts +153 -0
  192. package/dist/styles.css +0 -1
@@ -0,0 +1,268 @@
1
+ "use client";
2
+
3
+ /**
4
+ * useImagePreloader - Preload images before they enter the viewport
5
+ *
6
+ * Uses IntersectionObserver with a configurable rootMargin to detect when images
7
+ * are approaching the viewport and preload them. This provides consistent behavior
8
+ * across browsers (Safari's native lazy loading has a more conservative threshold
9
+ * than Chrome).
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * function ImageGallery({ images }) {
14
+ * const { registerElement, isPreloaded } = useImagePreloader({
15
+ * preloadMargin: 1000,
16
+ * });
17
+ *
18
+ * return images.map((img, index) => (
19
+ * <div
20
+ * key={index}
21
+ * ref={(el) => registerElement(`img-${index}`, el, img.src)}
22
+ * >
23
+ * <img
24
+ * src={img.src}
25
+ * loading={index <= 1 || isPreloaded(img.src) ? "eager" : "lazy"}
26
+ * />
27
+ * </div>
28
+ * ));
29
+ * }
30
+ * ```
31
+ */
32
+
33
+ import { useCallback, useEffect, useRef, useState } from "react";
34
+ import { observe } from "./visibility/observerPool";
35
+
36
+ export interface UseImagePreloaderOptions {
37
+ /** Distance in pixels before viewport to start preloading (default: 1000) */
38
+ preloadMargin?: number;
39
+ /** Maximum concurrent preloads (default: 2) */
40
+ maxConcurrent?: number;
41
+ }
42
+
43
+ export interface UseImagePreloaderReturn {
44
+ /**
45
+ * Register an element for visibility-based preloading.
46
+ * Can be used as a ref callback.
47
+ */
48
+ registerElement: (
49
+ key: string,
50
+ element: HTMLElement | null,
51
+ src: string | undefined
52
+ ) => void;
53
+ /** Set of image URLs that have been preloaded */
54
+ preloadedUrls: Set<string>;
55
+ /** Check if a specific URL has been preloaded */
56
+ isPreloaded: (src: string | undefined) => boolean;
57
+ }
58
+
59
+ interface PreloadEntry {
60
+ element: HTMLElement;
61
+ src: string;
62
+ unobserve?: () => void;
63
+ }
64
+
65
+ /**
66
+ * Hook for preloading images before they enter the viewport.
67
+ *
68
+ * Uses IntersectionObserver with a configurable margin to detect images
69
+ * approaching the viewport and trigger preloading via hidden Image objects.
70
+ */
71
+ export function useImagePreloader(
72
+ options: UseImagePreloaderOptions = {}
73
+ ): UseImagePreloaderReturn {
74
+ const { preloadMargin = 1000, maxConcurrent = 2 } = options;
75
+
76
+ // Track which URLs have been preloaded
77
+ const [preloadedUrls, setPreloadedUrls] = useState<Set<string>>(new Set());
78
+
79
+ // Track registered elements
80
+ const entriesRef = useRef<Map<string, PreloadEntry>>(new Map());
81
+
82
+ // Track currently preloading count
83
+ const preloadingCountRef = useRef(0);
84
+
85
+ // Queue for pending preloads
86
+ const preloadQueueRef = useRef<string[]>([]);
87
+
88
+ // Track preloading state (to avoid duplicate preloads)
89
+ const preloadingUrlsRef = useRef<Set<string>>(new Set());
90
+
91
+ // Process the preload queue
92
+ const processQueue = useCallback(() => {
93
+ while (
94
+ preloadingCountRef.current < maxConcurrent &&
95
+ preloadQueueRef.current.length > 0
96
+ ) {
97
+ const src = preloadQueueRef.current.shift()!;
98
+
99
+ // Skip if already preloaded or preloading
100
+ if (
101
+ preloadingUrlsRef.current.has(src) ||
102
+ preloadedUrls.has(src)
103
+ ) {
104
+ continue;
105
+ }
106
+
107
+ preloadingUrlsRef.current.add(src);
108
+ preloadingCountRef.current++;
109
+
110
+ // Preload using hidden Image object
111
+ const img = new Image();
112
+ img.crossOrigin = 'anonymous';
113
+ img.src = src;
114
+
115
+ const handleComplete = () => {
116
+ preloadingCountRef.current--;
117
+ preloadingUrlsRef.current.delete(src);
118
+
119
+ // Add to preloaded set
120
+ setPreloadedUrls((prev) => {
121
+ const next = new Set(prev);
122
+ next.add(src);
123
+ return next;
124
+ });
125
+
126
+ // Process next in queue
127
+ processQueue();
128
+ };
129
+
130
+ img.onload = handleComplete;
131
+ img.onerror = handleComplete; // Still mark as "preloaded" to avoid retries
132
+ }
133
+ }, [maxConcurrent, preloadedUrls]);
134
+
135
+ // IntersectionObserver options
136
+ const observerOptionsRef = useRef<IntersectionObserverInit>({
137
+ rootMargin: `${preloadMargin}px 0px`,
138
+ threshold: 0,
139
+ });
140
+
141
+ // Update observer options if margin changes
142
+ useEffect(() => {
143
+ observerOptionsRef.current = {
144
+ rootMargin: `${preloadMargin}px 0px`,
145
+ threshold: 0,
146
+ };
147
+ }, [preloadMargin]);
148
+
149
+ // Register element for preloading
150
+ const registerElement = useCallback(
151
+ (key: string, element: HTMLElement | null, src: string | undefined) => {
152
+ const entries = entriesRef.current;
153
+
154
+ // Get existing entry
155
+ const existingEntry = entries.get(key);
156
+
157
+ // If element is null, unregister
158
+ if (!element) {
159
+ if (existingEntry?.unobserve) {
160
+ existingEntry.unobserve();
161
+ }
162
+ entries.delete(key);
163
+ return;
164
+ }
165
+
166
+ // If no src, nothing to preload
167
+ if (!src) {
168
+ if (existingEntry?.unobserve) {
169
+ existingEntry.unobserve();
170
+ }
171
+ entries.delete(key);
172
+ return;
173
+ }
174
+
175
+ // If already preloaded, no need to observe
176
+ if (preloadedUrls.has(src)) {
177
+ if (existingEntry?.unobserve) {
178
+ existingEntry.unobserve();
179
+ }
180
+ entries.delete(key);
181
+ return;
182
+ }
183
+
184
+ // If element/src unchanged, keep existing observer
185
+ if (existingEntry?.element === element && existingEntry?.src === src) {
186
+ return;
187
+ }
188
+
189
+ // Clean up old observer if exists
190
+ if (existingEntry?.unobserve) {
191
+ existingEntry.unobserve();
192
+ }
193
+
194
+ // Create new entry
195
+ const entry: PreloadEntry = {
196
+ element,
197
+ src,
198
+ };
199
+
200
+ // SSR check - IntersectionObserver not available
201
+ if (typeof IntersectionObserver === "undefined") {
202
+ // On SSR, mark everything as preloaded to fall back to native behavior
203
+ setPreloadedUrls((prev) => {
204
+ const next = new Set(prev);
205
+ next.add(src);
206
+ return next;
207
+ });
208
+ entries.set(key, entry);
209
+ return;
210
+ }
211
+
212
+ // Set up observer
213
+ entry.unobserve = observe(
214
+ element,
215
+ (intersectionEntry) => {
216
+ if (intersectionEntry.isIntersecting) {
217
+ // Element is in preload zone - queue for preload
218
+ if (
219
+ !preloadingUrlsRef.current.has(src) &&
220
+ !preloadedUrls.has(src) &&
221
+ !preloadQueueRef.current.includes(src)
222
+ ) {
223
+ preloadQueueRef.current.push(src);
224
+ processQueue();
225
+ }
226
+
227
+ // Unobserve after triggering preload
228
+ if (entry.unobserve) {
229
+ entry.unobserve();
230
+ entry.unobserve = undefined;
231
+ }
232
+ }
233
+ },
234
+ observerOptionsRef.current
235
+ );
236
+
237
+ entries.set(key, entry);
238
+ },
239
+ [preloadedUrls, processQueue]
240
+ );
241
+
242
+ // Check if URL is preloaded
243
+ const isPreloaded = useCallback(
244
+ (src: string | undefined): boolean => {
245
+ if (!src) return false;
246
+ return preloadedUrls.has(src);
247
+ },
248
+ [preloadedUrls]
249
+ );
250
+
251
+ // Cleanup on unmount
252
+ useEffect(() => {
253
+ return () => {
254
+ entriesRef.current.forEach((entry) => {
255
+ if (entry.unobserve) {
256
+ entry.unobserve();
257
+ }
258
+ });
259
+ entriesRef.current.clear();
260
+ };
261
+ }, []);
262
+
263
+ return {
264
+ registerElement,
265
+ preloadedUrls,
266
+ isPreloaded,
267
+ };
268
+ }