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