@snowcone-app/ui 0.1.42 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +18 -4
- package/package.json +9 -5
- package/src/components/CanvasIsolationBoundary.tsx +202 -0
- package/src/components/LoadingOverlayPrism.tsx +251 -0
- package/src/composed/AddToCart.tsx +229 -0
- package/src/composed/ArtAlignment.tsx +703 -0
- package/src/composed/ArtSelector.tsx +290 -0
- package/src/composed/ArtworkCustomizer.tsx +212 -0
- package/src/composed/CanvasEditor.tsx +79 -0
- package/src/composed/ColorPicker.tsx +111 -0
- package/src/composed/CurrentSelectionDisplay.tsx +86 -0
- package/src/composed/HeroProductImage.tsx +1071 -0
- package/src/composed/Lightbox.index.ts +2 -0
- package/src/composed/Lightbox.tsx +230 -0
- package/src/composed/PlacementClipShapeSelector.tsx +88 -0
- package/src/composed/PlacementTabs.tsx +179 -0
- package/src/composed/ProductCard.tsx +298 -0
- package/src/composed/ProductGallery.tsx +54 -0
- package/src/composed/ProductImage.tsx +129 -0
- package/src/composed/ProductList.tsx +147 -0
- package/src/composed/ProductOptions.tsx +305 -0
- package/src/composed/RealtimeMockup.tsx +121 -0
- package/src/composed/TileCount.tsx +348 -0
- package/src/composed/carousels/HeroCarousel.tsx +240 -0
- package/src/composed/carousels/MobileProductCarousel.tsx +1002 -0
- package/src/composed/carousels/index.ts +11 -0
- package/src/composed/carousels/types.ts +58 -0
- package/src/composed/grids/MasonryGrid.tsx +238 -0
- package/src/composed/grids/index.ts +9 -0
- package/src/composed/search/CurrentRefinements.tsx +80 -0
- package/src/composed/search/Filters.tsx +49 -0
- package/src/composed/search/FiltersButton.tsx +57 -0
- package/src/composed/search/FiltersDrawer.tsx +375 -0
- package/src/composed/search/ProductGrid.tsx +118 -0
- package/src/composed/search/ProductHit.tsx +56 -0
- package/src/composed/search/SearchBox.tsx +109 -0
- package/src/composed/search/SearchProvider.tsx +136 -0
- package/src/composed/search/facetConfig.ts +16 -0
- package/src/composed/search/index.ts +22 -0
- package/src/composed/search/meilisearchAdapter.ts +20 -0
- package/src/composed/search/types.ts +22 -0
- package/src/composed/zoom/EnhancedImageViewer.tsx +505 -0
- package/src/composed/zoom/ResponsiveZoom.tsx +134 -0
- package/src/composed/zoom/ZoomOverlay.tsx +194 -0
- package/src/composed/zoom/index.ts +12 -0
- package/src/composed/zoom/types.ts +12 -0
- package/src/design-system/ColorPalette.tsx +126 -0
- package/src/design-system/ColorSwatch.tsx +49 -0
- package/src/design-system/DesignSystemPage.tsx +130 -0
- package/src/design-system/ThemeSwitcher.tsx +181 -0
- package/src/design-system/TypographyScale.tsx +106 -0
- package/src/design-system/index.ts +5 -0
- package/src/ecommerce/stories/HeroProductImage.stories.tsx +66 -0
- package/src/ecommerce/stories/PDPHeroGallery.stories.tsx +105 -0
- package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +472 -0
- package/src/ecommerce/stories/PDPLayout.stories.tsx +365 -0
- package/src/hooks/useBrand.ts +41 -0
- package/src/hooks/useCanvasContext.ts +127 -0
- package/src/hooks/useDeviceDetection.ts +64 -0
- package/src/hooks/useFocusTrap.ts +70 -0
- package/src/hooks/useImagePreloader.ts +268 -0
- package/src/hooks/useImageTransition.ts +608 -0
- package/src/hooks/usePlacementsProcessor.ts +74 -0
- package/src/hooks/useProductGallery.ts +193 -0
- package/src/hooks/useProductPage.ts +467 -0
- package/src/hooks/useRenderGuard.ts +96 -0
- package/src/hooks/useScrollDirection.ts +196 -0
- package/src/hooks/viewport/index.ts +25 -0
- package/src/hooks/viewport/useContainerWidth.ts +59 -0
- package/src/hooks/viewport/useMediaQuery.ts +52 -0
- package/src/hooks/viewport/useResponsiveImageCap.ts +149 -0
- package/src/hooks/viewport/useViewportDimensions.ts +135 -0
- package/src/hooks/viewport/useWideMonitorMode.ts +150 -0
- package/src/hooks/visibility/index.ts +15 -0
- package/src/hooks/visibility/observerPool.ts +150 -0
- package/src/index.ts +240 -0
- package/src/layouts/hero-zoom/HeroShrinkLayout.tsx +209 -0
- package/src/layouts/hero-zoom/HeroZoomLayout.tsx +351 -0
- package/src/layouts/hero-zoom/index.ts +30 -0
- package/src/layouts/hero-zoom/stories/HeroZoomLayout.stories.tsx +350 -0
- package/src/layouts/hero-zoom/types.ts +113 -0
- package/src/layouts/hero-zoom/useHeroZoomScales.ts +156 -0
- package/src/layouts/index.ts +9 -0
- package/src/layouts/pdp/EdgeBlurBox.tsx +210 -0
- package/src/layouts/pdp/ImageBlurExtension.tsx +215 -0
- package/src/layouts/pdp/ImageEdgeBlur.tsx +215 -0
- package/src/layouts/pdp/PDPLayout.tsx +246 -0
- package/src/layouts/pdp/SimpleImageBlur.tsx +140 -0
- package/src/layouts/pdp/index.ts +40 -0
- package/src/lib/env.ts +15 -0
- package/src/lib/locale.ts +167 -0
- package/src/lib/router.tsx +46 -0
- package/src/lib/utils.ts +6 -0
- package/src/lightbox/README.md +77 -0
- package/src/next/index.tsx +26 -0
- package/src/patterns/MockupPriorityProvider.tsx +1014 -0
- package/src/patterns/Product.tsx +850 -0
- package/src/patterns/ProductPageProvider.tsx +224 -0
- package/src/patterns/RealtimeProvider.tsx +1162 -0
- package/src/patterns/ShopProvider.tsx +603 -0
- package/src/personalization/PersonalizationBridge.tsx +235 -0
- package/src/personalization/PersonalizationContext.ts +29 -0
- package/src/personalization/PersonalizationInputs.tsx +110 -0
- package/src/personalization/PersonalizationProvider.tsx +407 -0
- package/src/personalization/canvas-stub.d.ts +22 -0
- package/src/personalization/index.ts +43 -0
- package/src/personalization/types.ts +48 -0
- package/src/personalization/usePersonalization.ts +32 -0
- package/src/personalization/usePersonalizationShimmer.ts +159 -0
- package/src/personalization/utils.ts +59 -0
- package/src/primitives/BrandLogo.tsx +65 -0
- package/src/primitives/BrandName.tsx +51 -0
- package/src/primitives/Button.tsx +123 -0
- package/src/primitives/ColorSwatch.tsx +221 -0
- package/src/primitives/DragHintAnimation.tsx +190 -0
- package/src/primitives/EdgeSwipeGuards.tsx +60 -0
- package/src/primitives/FloatingActionGroup.tsx +176 -0
- package/src/primitives/ProductPrice.tsx +171 -0
- package/src/primitives/ProgressiveBlur.tsx +295 -0
- package/src/primitives/ThemeToggle.tsx +125 -0
- package/src/primitives/__tests__/story-coverage.test.ts +98 -0
- package/src/primitives/accordion.tsx +280 -0
- package/src/primitives/badge.tsx +137 -0
- package/src/primitives/card.tsx +61 -0
- package/src/primitives/checkbox.tsx +56 -0
- package/src/primitives/collapsible.tsx +51 -0
- package/src/primitives/drawer.tsx +828 -0
- package/src/primitives/dropdown-menu.tsx +197 -0
- package/src/primitives/fieldset.tsx +73 -0
- package/src/primitives/index.ts +138 -0
- package/src/primitives/input.tsx +91 -0
- package/src/primitives/kbd.tsx +130 -0
- package/src/primitives/label.tsx +20 -0
- package/src/primitives/link.tsx +182 -0
- package/src/primitives/popover.tsx +80 -0
- package/src/primitives/radio-group.tsx +79 -0
- package/src/primitives/scroll-fade.tsx +159 -0
- package/src/primitives/select.tsx +170 -0
- package/src/primitives/separator.tsx +25 -0
- package/src/primitives/slider.tsx +221 -0
- package/src/primitives/spinner.tsx +72 -0
- package/src/primitives/stories/Accordion.stories.tsx +121 -0
- package/src/primitives/stories/Badge.stories.tsx +221 -0
- package/src/primitives/stories/Button.stories.tsx +185 -0
- package/src/primitives/stories/Card.stories.tsx +171 -0
- package/src/primitives/stories/Checkbox.stories.tsx +214 -0
- package/src/primitives/stories/Collapsible.stories.tsx +230 -0
- package/src/primitives/stories/Drawer.stories.tsx +378 -0
- package/src/primitives/stories/DropdownMenu.stories.tsx +182 -0
- package/src/primitives/stories/Fieldset.stories.tsx +212 -0
- package/src/primitives/stories/Input.stories.tsx +172 -0
- package/src/primitives/stories/Kbd.stories.tsx +183 -0
- package/src/primitives/stories/Label.stories.tsx +98 -0
- package/src/primitives/stories/Link.stories.tsx +260 -0
- package/src/primitives/stories/Popover.stories.tsx +178 -0
- package/src/primitives/stories/RadioGroup.stories.tsx +205 -0
- package/src/primitives/stories/Select.stories.tsx +222 -0
- package/src/primitives/stories/Separator.stories.tsx +134 -0
- package/src/primitives/stories/Slider.stories.tsx +203 -0
- package/src/primitives/stories/Spinner.stories.tsx +142 -0
- package/src/primitives/stories/Surface.stories.tsx +257 -0
- package/src/primitives/stories/Switch.stories.tsx +131 -0
- package/src/primitives/stories/Tabs.stories.tsx +275 -0
- package/src/primitives/stories/TextField.stories.tsx +139 -0
- package/src/primitives/stories/Textarea.stories.tsx +148 -0
- package/src/primitives/stories/Tooltip.stories.tsx +119 -0
- package/src/primitives/surface.tsx +86 -0
- package/src/primitives/switch.tsx +35 -0
- package/src/primitives/tabs.tsx +206 -0
- package/src/primitives/text-field.tsx +84 -0
- package/src/primitives/textarea.tsx +50 -0
- package/src/primitives/tooltip.tsx +58 -0
- package/src/services/CanvasExportService.ts +518 -0
- package/src/styles/base.css +380 -0
- package/src/styles/defaults.css +280 -0
- package/src/styles/globals.css +1242 -0
- package/src/styles/index.css +17 -0
- package/src/styles/ne-themes.css +4740 -0
- package/src/styles/tailwind.css +11 -0
- package/src/styles/tokens.css +117 -0
- package/src/styles/utilities.css +188 -0
- package/src/themes/apply-theme.ts +449 -0
- package/src/themes/getThemeStyles.ts +454 -0
- package/src/themes/index.ts +48 -0
- package/src/themes/oklch-theme.ts +283 -0
- package/src/themes/presets.ts +989 -0
- package/src/themes/types.ts +386 -0
- package/src/themes/useTheme.tsx +450 -0
- package/src/utils/dev-warnings.ts +161 -0
- package/src/utils/devWarnings.ts +153 -0
- package/dist/styles.css +0 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useLayoutEffect } from "react";
|
|
4
|
+
|
|
5
|
+
export interface ViewportDimensions {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
aspectRatio: number;
|
|
9
|
+
/** Increments on each resize event - use for Safari mask repaint workaround */
|
|
10
|
+
resizeVersion: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Single source of truth for viewport dimensions.
|
|
15
|
+
* Only registers resize listener on desktop to avoid mobile jank.
|
|
16
|
+
*
|
|
17
|
+
* On mobile:
|
|
18
|
+
* - Returns initial dimensions (measured once on mount)
|
|
19
|
+
* - No resize listener is registered (saves performance)
|
|
20
|
+
* - Dimensions are static since mobile layouts don't depend on resize
|
|
21
|
+
*
|
|
22
|
+
* On desktop:
|
|
23
|
+
* - Registers a single resize listener
|
|
24
|
+
* - Updates dimensions on viewport changes
|
|
25
|
+
* - Used by useWideMonitorMode, useResponsiveImageCap, etc.
|
|
26
|
+
*
|
|
27
|
+
* @param isDesktop - Pass the result of useMediaQuery("(min-width: 768px)")
|
|
28
|
+
* null = SSR/unknown, false = mobile, true = desktop
|
|
29
|
+
* @returns Viewport dimensions (or defaults if SSR)
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* const isDesktop = useMediaQuery("(min-width: 768px)");
|
|
34
|
+
* const dimensions = useViewportDimensions(isDesktop);
|
|
35
|
+
*
|
|
36
|
+
* // dimensions.width, dimensions.height, dimensions.aspectRatio
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function useViewportDimensions(
|
|
40
|
+
isDesktop: boolean | null
|
|
41
|
+
): ViewportDimensions {
|
|
42
|
+
// Always start with zeros to match server render and avoid hydration mismatch.
|
|
43
|
+
// The useEffect below will update with actual dimensions after mount.
|
|
44
|
+
const [dimensions, setDimensions] = useState<ViewportDimensions>({
|
|
45
|
+
width: 0,
|
|
46
|
+
height: 0,
|
|
47
|
+
aspectRatio: 0,
|
|
48
|
+
resizeVersion: 0,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Measure dimensions on initial mount (runs once after hydration)
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const width = window.innerWidth;
|
|
54
|
+
const height = window.innerHeight;
|
|
55
|
+
setDimensions((prev) => ({
|
|
56
|
+
width,
|
|
57
|
+
height,
|
|
58
|
+
aspectRatio: height > 0 ? width / height : 0,
|
|
59
|
+
resizeVersion: prev.resizeVersion, // Keep at 0 for initial mount
|
|
60
|
+
}));
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
// Synchronously measure dimensions when switching to desktop mode
|
|
64
|
+
// useLayoutEffect runs BEFORE paint, ensuring correct dimensions on first render
|
|
65
|
+
useLayoutEffect(() => {
|
|
66
|
+
if (isDesktop !== true) return;
|
|
67
|
+
|
|
68
|
+
// Immediately measure when switching to desktop mode
|
|
69
|
+
// This handles iPad split view changes where isDesktop changes without a resize event
|
|
70
|
+
const width = window.innerWidth;
|
|
71
|
+
const height = window.innerHeight;
|
|
72
|
+
setDimensions((prev) => ({
|
|
73
|
+
width,
|
|
74
|
+
height,
|
|
75
|
+
aspectRatio: height > 0 ? width / height : 0,
|
|
76
|
+
// Increment to force Safari mask repaint
|
|
77
|
+
resizeVersion: prev.resizeVersion + 1,
|
|
78
|
+
}));
|
|
79
|
+
}, [isDesktop]);
|
|
80
|
+
|
|
81
|
+
// Register resize listener only on desktop (can use regular useEffect)
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (isDesktop !== true) return;
|
|
84
|
+
|
|
85
|
+
let resizeTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
86
|
+
let lastWidth = window.innerWidth;
|
|
87
|
+
let lastHeight = window.innerHeight;
|
|
88
|
+
|
|
89
|
+
const handleResize = () => {
|
|
90
|
+
const width = window.innerWidth;
|
|
91
|
+
const height = window.innerHeight;
|
|
92
|
+
|
|
93
|
+
// Check if this is likely an address bar show/hide (height-only change < 150px)
|
|
94
|
+
const widthChanged = Math.abs(width - lastWidth) > 1;
|
|
95
|
+
const heightChange = Math.abs(height - lastHeight);
|
|
96
|
+
const isAddressBarChange = !widthChanged && heightChange > 0 && heightChange < 150;
|
|
97
|
+
|
|
98
|
+
if (isAddressBarChange) {
|
|
99
|
+
// Safari iPad address bar show/hide - ignore completely to prevent flash
|
|
100
|
+
// Don't update dimensions, don't schedule resizeVersion increment
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Real resize - update tracking variables
|
|
105
|
+
lastWidth = width;
|
|
106
|
+
lastHeight = height;
|
|
107
|
+
|
|
108
|
+
// Update dimensions immediately for responsive layout
|
|
109
|
+
setDimensions((prev) => ({
|
|
110
|
+
width,
|
|
111
|
+
height,
|
|
112
|
+
aspectRatio: height > 0 ? width / height : 0,
|
|
113
|
+
resizeVersion: prev.resizeVersion, // Don't increment yet
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
// Debounce resizeVersion increment for real resizes only
|
|
117
|
+
// This is for Safari mask repaint workaround
|
|
118
|
+
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
119
|
+
resizeTimeout = setTimeout(() => {
|
|
120
|
+
setDimensions((prev) => ({
|
|
121
|
+
...prev,
|
|
122
|
+
resizeVersion: prev.resizeVersion + 1,
|
|
123
|
+
}));
|
|
124
|
+
}, 300); // Wait 300ms after last resize
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
window.addEventListener("resize", handleResize);
|
|
128
|
+
return () => {
|
|
129
|
+
window.removeEventListener("resize", handleResize);
|
|
130
|
+
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
131
|
+
};
|
|
132
|
+
}, [isDesktop]);
|
|
133
|
+
|
|
134
|
+
return dimensions;
|
|
135
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import type { ResponsiveImageCapInfo } from "./useResponsiveImageCap";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Layout configuration for gap calculations.
|
|
8
|
+
* Must match DesktopHeroImages LAYOUT_CONFIG.
|
|
9
|
+
*/
|
|
10
|
+
const LAYOUT_CONFIG = {
|
|
11
|
+
SIDEBAR_MAX_PX: 384,
|
|
12
|
+
SIDEBAR_MARGIN_PX: 32,
|
|
13
|
+
IMAGE_WIDTH_VW: 140, // Image width in vw units (matches DesktopHeroImages)
|
|
14
|
+
/**
|
|
15
|
+
* Gap threshold for wide monitor mode activation.
|
|
16
|
+
*
|
|
17
|
+
* This is intentionally HIGHER than ImageEdgeBlur's 10px threshold.
|
|
18
|
+
* - ImageEdgeBlur shows blur at 10px gap (fills small gaps)
|
|
19
|
+
* - useWideMonitorMode activates at 100px gap (changes layout behavior)
|
|
20
|
+
*
|
|
21
|
+
* This separation allows blur to fill small gaps while keeping
|
|
22
|
+
* the sidebar sticky at moderate viewport sizes like 937px.
|
|
23
|
+
*/
|
|
24
|
+
WIDE_MONITOR_GAP_THRESHOLD: 100,
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Calculate the gap between the image right edge and viewport right edge.
|
|
29
|
+
* This is the core calculation used by both isWideMonitor and hasEdgeBlur.
|
|
30
|
+
*/
|
|
31
|
+
function calculateEdgeGap(
|
|
32
|
+
viewportWidth: number,
|
|
33
|
+
maxWidthPx: number,
|
|
34
|
+
sidebarPercent: number,
|
|
35
|
+
sidebarWidthPx?: number
|
|
36
|
+
): number {
|
|
37
|
+
if (viewportWidth === 0 || maxWidthPx === 0) return 0;
|
|
38
|
+
|
|
39
|
+
// Use exact pixel width if provided, otherwise calculate from percent with cap
|
|
40
|
+
const sidebarPx = sidebarWidthPx ?? Math.min(
|
|
41
|
+
viewportWidth * sidebarPercent,
|
|
42
|
+
LAYOUT_CONFIG.SIDEBAR_MAX_PX
|
|
43
|
+
);
|
|
44
|
+
const contentAreaWidth = viewportWidth - sidebarPx - LAYOUT_CONFIG.SIDEBAR_MARGIN_PX;
|
|
45
|
+
|
|
46
|
+
// Image width is min(140vw, maxWidthPx) - matches DesktopHeroImages CSS
|
|
47
|
+
const imageWidthFromVw = (viewportWidth * LAYOUT_CONFIG.IMAGE_WIDTH_VW) / 100;
|
|
48
|
+
const imageWidth = Math.min(imageWidthFromVw, maxWidthPx);
|
|
49
|
+
const leftOffset = (contentAreaWidth - imageWidth) / 2;
|
|
50
|
+
const imageRightEdge = leftOffset + imageWidth;
|
|
51
|
+
|
|
52
|
+
return viewportWidth - imageRightEdge;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if we're in "wide monitor mode" where images don't cover the full viewport.
|
|
57
|
+
* Uses a HIGHER threshold (100px) than ImageEdgeBlur (10px).
|
|
58
|
+
*
|
|
59
|
+
* This is used for header stickiness - we want the header to stay sticky
|
|
60
|
+
* at moderate viewport sizes even if there's a small blur gap.
|
|
61
|
+
*
|
|
62
|
+
* @param viewportWidth - Current viewport width in pixels
|
|
63
|
+
* @param maxWidthPx - Maximum image width from imageCapInfo
|
|
64
|
+
* @param sidebarPercent - Sidebar width as a percentage (default: 0.35 for XL)
|
|
65
|
+
* @param sidebarWidthPx - Optional: exact sidebar width in pixels (bypasses 384px cap)
|
|
66
|
+
* @returns true if in wide monitor mode (uncovered gap > 100px)
|
|
67
|
+
*/
|
|
68
|
+
export function isWideMonitor(
|
|
69
|
+
viewportWidth: number,
|
|
70
|
+
maxWidthPx: number,
|
|
71
|
+
sidebarPercent: number = 0.35,
|
|
72
|
+
sidebarWidthPx?: number
|
|
73
|
+
): boolean {
|
|
74
|
+
const gap = calculateEdgeGap(viewportWidth, maxWidthPx, sidebarPercent, sidebarWidthPx);
|
|
75
|
+
// Higher threshold to keep header sticky at moderate viewports
|
|
76
|
+
return gap > LAYOUT_CONFIG.WIDE_MONITOR_GAP_THRESHOLD;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if ImageEdgeBlur would render (gap > 10px).
|
|
81
|
+
* Uses the SAME threshold as ImageEdgeBlur component.
|
|
82
|
+
*
|
|
83
|
+
* This is used for sidebar minHeight - we want the sidebar to fill
|
|
84
|
+
* the vertical space whenever blur is visible.
|
|
85
|
+
*
|
|
86
|
+
* @param viewportWidth - Current viewport width in pixels
|
|
87
|
+
* @param maxWidthPx - Maximum image width from imageCapInfo
|
|
88
|
+
* @param sidebarPercent - Sidebar width as a percentage (default: 0.35 for XL)
|
|
89
|
+
* @param sidebarWidthPx - Optional: exact sidebar width in pixels (bypasses 384px cap)
|
|
90
|
+
* @returns true if edge blur would be visible (gap > 10px)
|
|
91
|
+
*/
|
|
92
|
+
export function hasEdgeBlur(
|
|
93
|
+
viewportWidth: number,
|
|
94
|
+
maxWidthPx: number,
|
|
95
|
+
sidebarPercent: number = 0.35,
|
|
96
|
+
sidebarWidthPx?: number
|
|
97
|
+
): boolean {
|
|
98
|
+
const gap = calculateEdgeGap(viewportWidth, maxWidthPx, sidebarPercent, sidebarWidthPx);
|
|
99
|
+
// Same 10px threshold as ImageEdgeBlur
|
|
100
|
+
return gap > 10;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Hook to detect "wide monitor mode" - when the viewport is wide enough that
|
|
105
|
+
* images are capped and don't cover the full width (ImageEdgeBlur kicks in).
|
|
106
|
+
*
|
|
107
|
+
* In this mode:
|
|
108
|
+
* - Header should NOT be sticky (users scroll to get back to header)
|
|
109
|
+
* - Sidebar should have minHeight to fill the gap
|
|
110
|
+
* - ImageEdgeBlur renders to fill the gap on the right side
|
|
111
|
+
*
|
|
112
|
+
* This hook uses the same calculation as ImageEdgeBlur to ensure they always agree.
|
|
113
|
+
*
|
|
114
|
+
* @param imageCapInfo - Image cap info from useResponsiveImageCap hook
|
|
115
|
+
* @param sidebarPercent - Sidebar width as a percentage (auto-detects based on breakpoint if not provided)
|
|
116
|
+
* @returns boolean indicating if in wide monitor mode
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```tsx
|
|
120
|
+
* const isDesktop = useMediaQuery("(min-width: 768px)");
|
|
121
|
+
* const dimensions = useViewportDimensions(isDesktop);
|
|
122
|
+
* const imageCapInfo = useResponsiveImageCap(dimensions);
|
|
123
|
+
* const isWideMonitorMode = useWideMonitorMode(imageCapInfo);
|
|
124
|
+
*
|
|
125
|
+
* // Adjust layout based on wide monitor mode
|
|
126
|
+
* <div style={isWideMonitorMode ? { minHeight: '...' } : {}}>
|
|
127
|
+
* ...
|
|
128
|
+
* </div>
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function useWideMonitorMode(
|
|
132
|
+
imageCapInfo: ResponsiveImageCapInfo,
|
|
133
|
+
sidebarPercent?: number
|
|
134
|
+
): boolean {
|
|
135
|
+
return useMemo(() => {
|
|
136
|
+
const { viewportWidth, maxWidthPx } = imageCapInfo;
|
|
137
|
+
|
|
138
|
+
// Early return for mobile/SSR (dimensions will be 0 or small)
|
|
139
|
+
// Wide monitor mode is never true on mobile devices
|
|
140
|
+
if (viewportWidth < 768) return false;
|
|
141
|
+
|
|
142
|
+
// Use provided sidebarPercent, or auto-detect based on breakpoint:
|
|
143
|
+
// - MD (768-1279px): 30% sidebar
|
|
144
|
+
// - XL (1280px+): 35% sidebar
|
|
145
|
+
// This matches the breakpoint behavior in DesktopHeroImages
|
|
146
|
+
const effectiveSidebarPercent = sidebarPercent ?? (viewportWidth >= 1280 ? 0.35 : 0.3);
|
|
147
|
+
|
|
148
|
+
return isWideMonitor(viewportWidth, maxWidthPx, effectiveSidebarPercent);
|
|
149
|
+
}, [imageCapInfo.viewportWidth, imageCapInfo.maxWidthPx, sidebarPercent]);
|
|
150
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visibility detection utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides shared IntersectionObserver infrastructure for tracking
|
|
5
|
+
* element visibility across the mockup rendering system.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
observe,
|
|
10
|
+
isObserved,
|
|
11
|
+
getObserverCount,
|
|
12
|
+
getObservedElementCount,
|
|
13
|
+
VISIBILITY_THRESHOLDS,
|
|
14
|
+
DEFAULT_VISIBILITY_OPTIONS,
|
|
15
|
+
} from './observerPool';
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared IntersectionObserver Pool
|
|
3
|
+
*
|
|
4
|
+
* Creates a pool of IntersectionObservers that can be reused across multiple elements.
|
|
5
|
+
* This is more efficient than creating one observer per element, especially for pages
|
|
6
|
+
* with many mockup images.
|
|
7
|
+
*
|
|
8
|
+
* Key features:
|
|
9
|
+
* - Observers are pooled by options (rootMargin + threshold)
|
|
10
|
+
* - WeakMap for automatic cleanup when elements are garbage collected
|
|
11
|
+
* - Supports multiple threshold arrays for granular visibility tracking
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const unobserve = observe(
|
|
16
|
+
* element,
|
|
17
|
+
* (entry) => console.log('Visibility:', entry.intersectionRatio),
|
|
18
|
+
* { threshold: [0, 0.25, 0.5, 0.75, 1.0] }
|
|
19
|
+
* );
|
|
20
|
+
*
|
|
21
|
+
* // Cleanup
|
|
22
|
+
* unobserve();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
type VisibilityCallback = (entry: IntersectionObserverEntry) => void;
|
|
27
|
+
|
|
28
|
+
// Store callbacks for each observed element
|
|
29
|
+
// Using WeakMap allows automatic cleanup when elements are garbage collected
|
|
30
|
+
const observerCallbacks = new WeakMap<Element, VisibilityCallback>();
|
|
31
|
+
|
|
32
|
+
// Pool observers by their options (rootMargin + threshold combination)
|
|
33
|
+
const observersByKey = new Map<string, IntersectionObserver>();
|
|
34
|
+
|
|
35
|
+
// Track which elements are observed by which observer (for cleanup)
|
|
36
|
+
const elementsByObserver = new Map<IntersectionObserver, Set<Element>>();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generate a unique key for observer options
|
|
40
|
+
*/
|
|
41
|
+
function getOptionsKey(options: IntersectionObserverInit): string {
|
|
42
|
+
const root = options.root ? 'custom' : 'viewport';
|
|
43
|
+
const rootMargin = options.rootMargin || '0px';
|
|
44
|
+
const threshold = Array.isArray(options.threshold)
|
|
45
|
+
? options.threshold.join(',')
|
|
46
|
+
: String(options.threshold || 0);
|
|
47
|
+
|
|
48
|
+
return `${root}|${rootMargin}|${threshold}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get or create an observer for the given options
|
|
53
|
+
*/
|
|
54
|
+
function getObserver(options: IntersectionObserverInit): IntersectionObserver {
|
|
55
|
+
const key = getOptionsKey(options);
|
|
56
|
+
|
|
57
|
+
if (!observersByKey.has(key)) {
|
|
58
|
+
const observer = new IntersectionObserver((entries) => {
|
|
59
|
+
entries.forEach((entry) => {
|
|
60
|
+
const callback = observerCallbacks.get(entry.target);
|
|
61
|
+
if (callback) {
|
|
62
|
+
callback(entry);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}, options);
|
|
66
|
+
|
|
67
|
+
observersByKey.set(key, observer);
|
|
68
|
+
elementsByObserver.set(observer, new Set());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return observersByKey.get(key)!;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Default threshold array for visibility tracking
|
|
76
|
+
* Provides callbacks at 0%, 25%, 50%, 75%, and 100% visibility
|
|
77
|
+
*/
|
|
78
|
+
export const VISIBILITY_THRESHOLDS = [0, 0.25, 0.5, 0.75, 1.0];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Default options for visibility observation
|
|
82
|
+
*/
|
|
83
|
+
export const DEFAULT_VISIBILITY_OPTIONS: IntersectionObserverInit = {
|
|
84
|
+
threshold: VISIBILITY_THRESHOLDS,
|
|
85
|
+
rootMargin: '0px',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Observe an element for visibility changes using a pooled IntersectionObserver
|
|
90
|
+
*
|
|
91
|
+
* @param element - The DOM element to observe
|
|
92
|
+
* @param callback - Called when visibility changes (receives IntersectionObserverEntry)
|
|
93
|
+
* @param options - IntersectionObserver options (optional, uses default thresholds)
|
|
94
|
+
* @returns Unobserve function - call to stop observing
|
|
95
|
+
*/
|
|
96
|
+
export function observe(
|
|
97
|
+
element: Element,
|
|
98
|
+
callback: VisibilityCallback,
|
|
99
|
+
options: IntersectionObserverInit = DEFAULT_VISIBILITY_OPTIONS
|
|
100
|
+
): () => void {
|
|
101
|
+
const observer = getObserver(options);
|
|
102
|
+
const elements = elementsByObserver.get(observer)!;
|
|
103
|
+
|
|
104
|
+
// Store the callback for this element
|
|
105
|
+
observerCallbacks.set(element, callback);
|
|
106
|
+
|
|
107
|
+
// Track and observe
|
|
108
|
+
elements.add(element);
|
|
109
|
+
observer.observe(element);
|
|
110
|
+
|
|
111
|
+
// Return unobserve function
|
|
112
|
+
return () => {
|
|
113
|
+
observerCallbacks.delete(element);
|
|
114
|
+
elements.delete(element);
|
|
115
|
+
observer.unobserve(element);
|
|
116
|
+
|
|
117
|
+
// Clean up empty observers (optional optimization)
|
|
118
|
+
// Commented out to avoid observer recreation overhead
|
|
119
|
+
// if (elements.size === 0) {
|
|
120
|
+
// observer.disconnect();
|
|
121
|
+
// observersByKey.delete(getOptionsKey(options));
|
|
122
|
+
// elementsByObserver.delete(observer);
|
|
123
|
+
// }
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if an element is currently being observed
|
|
129
|
+
*/
|
|
130
|
+
export function isObserved(element: Element): boolean {
|
|
131
|
+
return observerCallbacks.has(element);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get the number of active observers (for debugging)
|
|
136
|
+
*/
|
|
137
|
+
export function getObserverCount(): number {
|
|
138
|
+
return observersByKey.size;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get the total number of observed elements (for debugging)
|
|
143
|
+
*/
|
|
144
|
+
export function getObservedElementCount(): number {
|
|
145
|
+
let count = 0;
|
|
146
|
+
elementsByObserver.forEach((elements) => {
|
|
147
|
+
count += elements.size;
|
|
148
|
+
});
|
|
149
|
+
return count;
|
|
150
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// Brand — Protected brand identity components (ADR-0035)
|
|
2
|
+
export * from "./primitives/BrandName";
|
|
3
|
+
export * from "./primitives/BrandLogo";
|
|
4
|
+
export { useBrand } from "./hooks/useBrand";
|
|
5
|
+
export type { UseBrandResult } from "./hooks/useBrand";
|
|
6
|
+
|
|
7
|
+
// Locale — Formatting utilities (i18n foundation)
|
|
8
|
+
export {
|
|
9
|
+
formatCurrency,
|
|
10
|
+
formatDate,
|
|
11
|
+
formatDateTime,
|
|
12
|
+
getBrand,
|
|
13
|
+
brandAssets,
|
|
14
|
+
} from "./lib/locale";
|
|
15
|
+
export type {
|
|
16
|
+
SupportedLanguage,
|
|
17
|
+
SupportedCountry,
|
|
18
|
+
SupportedLocale,
|
|
19
|
+
BrandLocale,
|
|
20
|
+
BrandAssets,
|
|
21
|
+
FormatCurrencyOptions,
|
|
22
|
+
} from "./lib/locale";
|
|
23
|
+
|
|
24
|
+
// Router — framework-agnostic navigation. ui never imports next/navigation in
|
|
25
|
+
// the main entry; inject your framework's router here (Next apps: use
|
|
26
|
+
// UiNextRouterProvider from "@snowcone-app/ui/next"), else it falls back to the
|
|
27
|
+
// browser History API.
|
|
28
|
+
export { UiRouterProvider, type UiRouter } from "./lib/router";
|
|
29
|
+
|
|
30
|
+
// Primitives - Basic building blocks (all components with stories)
|
|
31
|
+
export * from "./primitives/Button";
|
|
32
|
+
export * from "./primitives/ColorSwatch";
|
|
33
|
+
export * from "./primitives/ProductPrice";
|
|
34
|
+
export * from "./primitives/FloatingActionGroup";
|
|
35
|
+
export * from "./primitives/ThemeToggle";
|
|
36
|
+
export * from "./primitives/DragHintAnimation";
|
|
37
|
+
export * from "./primitives/ProgressiveBlur";
|
|
38
|
+
// EdgeSwipeGuards - iOS Safari back/forward gesture prevention
|
|
39
|
+
export * from "./primitives/EdgeSwipeGuards";
|
|
40
|
+
|
|
41
|
+
// UI Primitives - Radix-based components
|
|
42
|
+
export * from "./primitives/accordion";
|
|
43
|
+
export * from "./primitives/badge";
|
|
44
|
+
export * from "./primitives/card";
|
|
45
|
+
export * from "./primitives/checkbox";
|
|
46
|
+
export * from "./primitives/collapsible";
|
|
47
|
+
// Drawer - Mobile-optimized drawer component (works on all platforms including iOS Safari)
|
|
48
|
+
export * from "./primitives/drawer";
|
|
49
|
+
// ScrollFade - Container with gradient fade indicator for scrollable content
|
|
50
|
+
export * from "./primitives/scroll-fade";
|
|
51
|
+
export * from "./primitives/dropdown-menu";
|
|
52
|
+
export * from "./primitives/fieldset";
|
|
53
|
+
export * from "./primitives/input";
|
|
54
|
+
export * from "./primitives/kbd";
|
|
55
|
+
export * from "./primitives/label";
|
|
56
|
+
export * from "./primitives/link";
|
|
57
|
+
export * from "./primitives/popover";
|
|
58
|
+
export * from "./primitives/radio-group";
|
|
59
|
+
export * from "./primitives/select";
|
|
60
|
+
export * from "./primitives/separator";
|
|
61
|
+
export * from "./primitives/slider";
|
|
62
|
+
export * from "./primitives/spinner";
|
|
63
|
+
export * from "./primitives/surface";
|
|
64
|
+
export * from "./primitives/switch";
|
|
65
|
+
export * from "./primitives/tabs";
|
|
66
|
+
export * from "./primitives/textarea";
|
|
67
|
+
export * from "./primitives/text-field";
|
|
68
|
+
export * from "./primitives/tooltip";
|
|
69
|
+
|
|
70
|
+
// Composed - Pre-built component combinations
|
|
71
|
+
export * from "./composed/ProductOptions";
|
|
72
|
+
export * from "./composed/ProductCard";
|
|
73
|
+
export * from "./composed/ProductList";
|
|
74
|
+
export * from "./composed/ProductGallery";
|
|
75
|
+
export * from "./composed/AddToCart";
|
|
76
|
+
export * from "./composed/TileCount";
|
|
77
|
+
export * from "./composed/CurrentSelectionDisplay";
|
|
78
|
+
export * from "./composed/ArtSelector";
|
|
79
|
+
export * from "./composed/ColorPicker";
|
|
80
|
+
export * from "./composed/RealtimeMockup";
|
|
81
|
+
export type { RealtimeMockupProps } from './composed/RealtimeMockup';
|
|
82
|
+
export { Lightbox } from './composed/Lightbox.index';
|
|
83
|
+
export type { LightboxProps } from './composed/Lightbox.index';
|
|
84
|
+
export * from "./composed/PlacementClipShapeSelector";
|
|
85
|
+
// PlacementTabs - Responsive placement selection with floating toolbar placeholder
|
|
86
|
+
export * from "./composed/PlacementTabs";
|
|
87
|
+
|
|
88
|
+
// Hooks
|
|
89
|
+
export * from "./hooks/useProductGallery";
|
|
90
|
+
export * from "./hooks/usePlacementsProcessor";
|
|
91
|
+
export * from "./hooks/useFocusTrap";
|
|
92
|
+
export * from "./hooks/useCanvasContext";
|
|
93
|
+
export * from "./hooks/useRenderGuard";
|
|
94
|
+
export * from "./hooks/useScrollDirection";
|
|
95
|
+
export * from "./hooks/useImageTransition";
|
|
96
|
+
// useImagePreloader - Preload images before they enter viewport (cross-browser consistent)
|
|
97
|
+
export * from "./hooks/useImagePreloader";
|
|
98
|
+
// useDeviceDetection - Touch device and Safari browser detection
|
|
99
|
+
export * from "./hooks/useDeviceDetection";
|
|
100
|
+
|
|
101
|
+
// useProductPage - Unified hook for building product pages (facade over all contexts)
|
|
102
|
+
export {
|
|
103
|
+
useProductPage,
|
|
104
|
+
useProductPageOptional,
|
|
105
|
+
useProviderStatus,
|
|
106
|
+
useCurrentPlacements,
|
|
107
|
+
useArtworkAlignment,
|
|
108
|
+
useSetArtworkAlignment,
|
|
109
|
+
useSelectedArtwork,
|
|
110
|
+
useProductSelection,
|
|
111
|
+
usePlacementClipShape,
|
|
112
|
+
} from "./hooks/useProductPage";
|
|
113
|
+
export type { ProductPageContext, ProviderStatus } from "./hooks/useProductPage";
|
|
114
|
+
|
|
115
|
+
// Visibility - Shared IntersectionObserver pool for efficient multi-element observation
|
|
116
|
+
export * from "./hooks/visibility";
|
|
117
|
+
|
|
118
|
+
// Viewport - Consolidated viewport handling hooks for responsive layouts
|
|
119
|
+
export * from "./hooks/viewport";
|
|
120
|
+
|
|
121
|
+
// Services - Singletons for zero-lag canvas operations
|
|
122
|
+
export { canvasExportService } from "./services/CanvasExportService";
|
|
123
|
+
export type { CanvasExportServiceConfig } from "./services/CanvasExportService";
|
|
124
|
+
|
|
125
|
+
// Components - Isolation boundaries for performance
|
|
126
|
+
export { CanvasIsolationBoundary, CanvasIsolationConfigurator, useIsolatedCanvas } from "./components/CanvasIsolationBoundary";
|
|
127
|
+
export { LoadingOverlayPrism, useLoadingOverlay } from "./components/LoadingOverlayPrism";
|
|
128
|
+
export type { LoadingOverlayPrismProps } from "./components/LoadingOverlayPrism";
|
|
129
|
+
|
|
130
|
+
// Re-export realtime mockup hook from SDK for convenience
|
|
131
|
+
export { useRealtimeMockup } from "@snowcone-app/sdk/react";
|
|
132
|
+
export type { UseRealtimeMockupOptions } from "@snowcone-app/sdk/react";
|
|
133
|
+
|
|
134
|
+
// Re-export commonly used SDK functions for convenience
|
|
135
|
+
export { getProduct, listProducts, getMockupUrl } from "@snowcone-app/sdk";
|
|
136
|
+
export type { WebSocketConfig } from "@snowcone-app/sdk";
|
|
137
|
+
|
|
138
|
+
// Patterns - Context providers and shared patterns
|
|
139
|
+
export * from "./patterns/ShopProvider";
|
|
140
|
+
export { useShopOptional } from "./patterns/ShopProvider";
|
|
141
|
+
export * from "./patterns/Product";
|
|
142
|
+
export type { PlacementDesign, Artwork } from "./patterns/Product";
|
|
143
|
+
// RealtimeProvider - separate context for realtime mockup updates (prevents re-render cascades)
|
|
144
|
+
export {
|
|
145
|
+
RealtimeProvider,
|
|
146
|
+
useRealtime,
|
|
147
|
+
useRealtimeOptional,
|
|
148
|
+
} from "./patterns/RealtimeProvider";
|
|
149
|
+
export type {
|
|
150
|
+
RealtimeContextValue,
|
|
151
|
+
RealtimeProviderProps,
|
|
152
|
+
MockupResult,
|
|
153
|
+
RealtimeState,
|
|
154
|
+
} from "./patterns/RealtimeProvider";
|
|
155
|
+
|
|
156
|
+
// MockupPriorityProvider - priority-based mockup rendering coordination
|
|
157
|
+
export {
|
|
158
|
+
MockupPriorityProvider,
|
|
159
|
+
useMockupPriority,
|
|
160
|
+
useMockupPriorityOptional,
|
|
161
|
+
} from "./patterns/MockupPriorityProvider";
|
|
162
|
+
export type {
|
|
163
|
+
MockupPriorityContextValue,
|
|
164
|
+
MockupPriorityProviderProps,
|
|
165
|
+
PriorityLevel,
|
|
166
|
+
} from "./patterns/MockupPriorityProvider";
|
|
167
|
+
|
|
168
|
+
// ProductPageProvider - Quick-start providers for product pages
|
|
169
|
+
export {
|
|
170
|
+
ProductPageProvider,
|
|
171
|
+
ProductPageProviderMinimal,
|
|
172
|
+
} from "./patterns/ProductPageProvider";
|
|
173
|
+
export type {
|
|
174
|
+
ProductPageProviderProps,
|
|
175
|
+
ProductPageProviderMinimalProps,
|
|
176
|
+
} from "./patterns/ProductPageProvider";
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
// Composed - Product-related composed components (moved from product/)
|
|
180
|
+
export * from "./composed/ProductImage";
|
|
181
|
+
export type { RegularArtwork, SeamlessPattern, Artwork as ProductImageArtwork, AspectRatio } from "./composed/ProductImage";
|
|
182
|
+
// Export ArtAlignment for advanced use cases (prefer ArtworkCustomizer for most cases)
|
|
183
|
+
export * from "./composed/ArtAlignment";
|
|
184
|
+
export * from "./composed/ArtworkCustomizer";
|
|
185
|
+
export type { ArtworkCustomizerProps } from "./composed/ArtworkCustomizer";
|
|
186
|
+
// TEMPORARY: Canvas editor mockup for testing
|
|
187
|
+
export * from "./composed/CanvasEditor";
|
|
188
|
+
export * from "./composed/HeroProductImage";
|
|
189
|
+
|
|
190
|
+
// Re-export types from SDK for convenience
|
|
191
|
+
export type {
|
|
192
|
+
ProductPlacement,
|
|
193
|
+
ProductVariant,
|
|
194
|
+
ProductMockupData,
|
|
195
|
+
ArtworkData,
|
|
196
|
+
ImageAlignment,
|
|
197
|
+
ProductData,
|
|
198
|
+
ProductArtAlignmentOptions,
|
|
199
|
+
ProductArtAlignmentContext,
|
|
200
|
+
} from "@snowcone-app/sdk";
|
|
201
|
+
|
|
202
|
+
// Re-export utility functions from SDK that are commonly used with React components
|
|
203
|
+
export {
|
|
204
|
+
describeProductArtAlignment,
|
|
205
|
+
getSnapPoints,
|
|
206
|
+
} from "@snowcone-app/sdk";
|
|
207
|
+
|
|
208
|
+
// Search - Composable search components for InstantSearch + Meilisearch
|
|
209
|
+
export * from "./composed/search";
|
|
210
|
+
|
|
211
|
+
// Zoom - Responsive image zoom components
|
|
212
|
+
export * from "./composed/zoom";
|
|
213
|
+
|
|
214
|
+
// Carousels - Production-grade image carousels for product display
|
|
215
|
+
export * from "./composed/carousels";
|
|
216
|
+
|
|
217
|
+
// Grids - Layout primitives for grid-based displays
|
|
218
|
+
export * from "./composed/grids";
|
|
219
|
+
|
|
220
|
+
// Development utilities (tree-shaken in production)
|
|
221
|
+
export {
|
|
222
|
+
runConfigChecks,
|
|
223
|
+
checkTailwindConfig,
|
|
224
|
+
checkThemeTokens,
|
|
225
|
+
checkComponentStyles,
|
|
226
|
+
} from "./utils/dev-warnings";
|
|
227
|
+
|
|
228
|
+
// Theme system — import from "@snowcone-app/ui/themes" for runtime theme switching.
|
|
229
|
+
// For CSS-only theming, use defaults.css instead. See CUSTOMIZATION.md.
|
|
230
|
+
// Types re-exported here for convenience:
|
|
231
|
+
export type { ThemeConfig, ThemePreset, FontPairing } from "./themes";
|
|
232
|
+
|
|
233
|
+
// Layouts - Pre-built layout patterns
|
|
234
|
+
export * from "./layouts";
|
|
235
|
+
|
|
236
|
+
// Design System - Visual reference for theme tokens
|
|
237
|
+
export * from "./design-system";
|
|
238
|
+
|
|
239
|
+
// Personalization - Buyer customization system for product designs
|
|
240
|
+
export * from "./personalization";
|