@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.
Files changed (192) hide show
  1. package/CHANGELOG.md +33 -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,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";