@snowcone-app/ui 0.1.43 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +18 -4
  3. package/dist/index.cjs +5 -2
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.js +5 -2
  6. package/dist/index.js.map +1 -1
  7. package/package.json +9 -5
  8. package/src/components/CanvasIsolationBoundary.tsx +202 -0
  9. package/src/components/LoadingOverlayPrism.tsx +251 -0
  10. package/src/composed/AddToCart.tsx +229 -0
  11. package/src/composed/ArtAlignment.tsx +703 -0
  12. package/src/composed/ArtSelector.tsx +290 -0
  13. package/src/composed/ArtworkCustomizer.tsx +212 -0
  14. package/src/composed/CanvasEditor.tsx +79 -0
  15. package/src/composed/ColorPicker.tsx +111 -0
  16. package/src/composed/CurrentSelectionDisplay.tsx +86 -0
  17. package/src/composed/HeroProductImage.tsx +1079 -0
  18. package/src/composed/Lightbox.index.ts +2 -0
  19. package/src/composed/Lightbox.tsx +230 -0
  20. package/src/composed/PlacementClipShapeSelector.tsx +88 -0
  21. package/src/composed/PlacementTabs.tsx +179 -0
  22. package/src/composed/ProductCard.tsx +298 -0
  23. package/src/composed/ProductGallery.tsx +54 -0
  24. package/src/composed/ProductImage.tsx +129 -0
  25. package/src/composed/ProductList.tsx +147 -0
  26. package/src/composed/ProductOptions.tsx +305 -0
  27. package/src/composed/RealtimeMockup.tsx +121 -0
  28. package/src/composed/TileCount.tsx +348 -0
  29. package/src/composed/carousels/HeroCarousel.tsx +240 -0
  30. package/src/composed/carousels/MobileProductCarousel.tsx +1002 -0
  31. package/src/composed/carousels/index.ts +11 -0
  32. package/src/composed/carousels/types.ts +58 -0
  33. package/src/composed/grids/MasonryGrid.tsx +238 -0
  34. package/src/composed/grids/index.ts +9 -0
  35. package/src/composed/search/CurrentRefinements.tsx +80 -0
  36. package/src/composed/search/Filters.tsx +49 -0
  37. package/src/composed/search/FiltersButton.tsx +57 -0
  38. package/src/composed/search/FiltersDrawer.tsx +375 -0
  39. package/src/composed/search/ProductGrid.tsx +118 -0
  40. package/src/composed/search/ProductHit.tsx +56 -0
  41. package/src/composed/search/SearchBox.tsx +109 -0
  42. package/src/composed/search/SearchProvider.tsx +136 -0
  43. package/src/composed/search/facetConfig.ts +16 -0
  44. package/src/composed/search/index.ts +22 -0
  45. package/src/composed/search/meilisearchAdapter.ts +20 -0
  46. package/src/composed/search/types.ts +22 -0
  47. package/src/composed/zoom/EnhancedImageViewer.tsx +505 -0
  48. package/src/composed/zoom/ResponsiveZoom.tsx +134 -0
  49. package/src/composed/zoom/ZoomOverlay.tsx +194 -0
  50. package/src/composed/zoom/index.ts +12 -0
  51. package/src/composed/zoom/types.ts +12 -0
  52. package/src/design-system/ColorPalette.tsx +126 -0
  53. package/src/design-system/ColorSwatch.tsx +49 -0
  54. package/src/design-system/DesignSystemPage.tsx +130 -0
  55. package/src/design-system/ThemeSwitcher.tsx +181 -0
  56. package/src/design-system/TypographyScale.tsx +106 -0
  57. package/src/design-system/index.ts +5 -0
  58. package/src/ecommerce/stories/HeroProductImage.stories.tsx +66 -0
  59. package/src/ecommerce/stories/PDPHeroGallery.stories.tsx +105 -0
  60. package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +472 -0
  61. package/src/ecommerce/stories/PDPLayout.stories.tsx +365 -0
  62. package/src/hooks/useBrand.ts +41 -0
  63. package/src/hooks/useCanvasContext.ts +127 -0
  64. package/src/hooks/useDeviceDetection.ts +64 -0
  65. package/src/hooks/useFocusTrap.ts +70 -0
  66. package/src/hooks/useImagePreloader.ts +268 -0
  67. package/src/hooks/useImageTransition.ts +608 -0
  68. package/src/hooks/usePlacementsProcessor.ts +74 -0
  69. package/src/hooks/useProductGallery.ts +193 -0
  70. package/src/hooks/useProductPage.ts +467 -0
  71. package/src/hooks/useRenderGuard.ts +96 -0
  72. package/src/hooks/useScrollDirection.ts +196 -0
  73. package/src/hooks/viewport/index.ts +25 -0
  74. package/src/hooks/viewport/useContainerWidth.ts +59 -0
  75. package/src/hooks/viewport/useMediaQuery.ts +52 -0
  76. package/src/hooks/viewport/useResponsiveImageCap.ts +149 -0
  77. package/src/hooks/viewport/useViewportDimensions.ts +135 -0
  78. package/src/hooks/viewport/useWideMonitorMode.ts +150 -0
  79. package/src/hooks/visibility/index.ts +15 -0
  80. package/src/hooks/visibility/observerPool.ts +150 -0
  81. package/src/index.ts +240 -0
  82. package/src/layouts/hero-zoom/HeroShrinkLayout.tsx +209 -0
  83. package/src/layouts/hero-zoom/HeroZoomLayout.tsx +351 -0
  84. package/src/layouts/hero-zoom/index.ts +30 -0
  85. package/src/layouts/hero-zoom/stories/HeroZoomLayout.stories.tsx +350 -0
  86. package/src/layouts/hero-zoom/types.ts +113 -0
  87. package/src/layouts/hero-zoom/useHeroZoomScales.ts +156 -0
  88. package/src/layouts/index.ts +9 -0
  89. package/src/layouts/pdp/EdgeBlurBox.tsx +210 -0
  90. package/src/layouts/pdp/ImageBlurExtension.tsx +215 -0
  91. package/src/layouts/pdp/ImageEdgeBlur.tsx +215 -0
  92. package/src/layouts/pdp/PDPLayout.tsx +246 -0
  93. package/src/layouts/pdp/SimpleImageBlur.tsx +140 -0
  94. package/src/layouts/pdp/index.ts +40 -0
  95. package/src/lib/env.ts +15 -0
  96. package/src/lib/locale.ts +167 -0
  97. package/src/lib/router.tsx +46 -0
  98. package/src/lib/utils.ts +6 -0
  99. package/src/lightbox/README.md +77 -0
  100. package/src/next/index.tsx +26 -0
  101. package/src/patterns/MockupPriorityProvider.tsx +1014 -0
  102. package/src/patterns/Product.tsx +850 -0
  103. package/src/patterns/ProductPageProvider.tsx +224 -0
  104. package/src/patterns/RealtimeProvider.tsx +1162 -0
  105. package/src/patterns/ShopProvider.tsx +603 -0
  106. package/src/personalization/PersonalizationBridge.tsx +235 -0
  107. package/src/personalization/PersonalizationContext.ts +29 -0
  108. package/src/personalization/PersonalizationInputs.tsx +110 -0
  109. package/src/personalization/PersonalizationProvider.tsx +407 -0
  110. package/src/personalization/canvas-stub.d.ts +22 -0
  111. package/src/personalization/index.ts +43 -0
  112. package/src/personalization/types.ts +48 -0
  113. package/src/personalization/usePersonalization.ts +32 -0
  114. package/src/personalization/usePersonalizationShimmer.ts +159 -0
  115. package/src/personalization/utils.ts +59 -0
  116. package/src/primitives/BrandLogo.tsx +65 -0
  117. package/src/primitives/BrandName.tsx +51 -0
  118. package/src/primitives/Button.tsx +123 -0
  119. package/src/primitives/ColorSwatch.tsx +221 -0
  120. package/src/primitives/DragHintAnimation.tsx +190 -0
  121. package/src/primitives/EdgeSwipeGuards.tsx +60 -0
  122. package/src/primitives/FloatingActionGroup.tsx +176 -0
  123. package/src/primitives/ProductPrice.tsx +171 -0
  124. package/src/primitives/ProgressiveBlur.tsx +295 -0
  125. package/src/primitives/ThemeToggle.tsx +125 -0
  126. package/src/primitives/__tests__/story-coverage.test.ts +98 -0
  127. package/src/primitives/accordion.tsx +280 -0
  128. package/src/primitives/badge.tsx +137 -0
  129. package/src/primitives/card.tsx +61 -0
  130. package/src/primitives/checkbox.tsx +56 -0
  131. package/src/primitives/collapsible.tsx +51 -0
  132. package/src/primitives/drawer.tsx +828 -0
  133. package/src/primitives/dropdown-menu.tsx +197 -0
  134. package/src/primitives/fieldset.tsx +73 -0
  135. package/src/primitives/index.ts +138 -0
  136. package/src/primitives/input.tsx +91 -0
  137. package/src/primitives/kbd.tsx +130 -0
  138. package/src/primitives/label.tsx +20 -0
  139. package/src/primitives/link.tsx +182 -0
  140. package/src/primitives/popover.tsx +80 -0
  141. package/src/primitives/radio-group.tsx +79 -0
  142. package/src/primitives/scroll-fade.tsx +159 -0
  143. package/src/primitives/select.tsx +170 -0
  144. package/src/primitives/separator.tsx +25 -0
  145. package/src/primitives/slider.tsx +221 -0
  146. package/src/primitives/spinner.tsx +72 -0
  147. package/src/primitives/stories/Accordion.stories.tsx +121 -0
  148. package/src/primitives/stories/Badge.stories.tsx +221 -0
  149. package/src/primitives/stories/Button.stories.tsx +185 -0
  150. package/src/primitives/stories/Card.stories.tsx +171 -0
  151. package/src/primitives/stories/Checkbox.stories.tsx +214 -0
  152. package/src/primitives/stories/Collapsible.stories.tsx +230 -0
  153. package/src/primitives/stories/Drawer.stories.tsx +378 -0
  154. package/src/primitives/stories/DropdownMenu.stories.tsx +182 -0
  155. package/src/primitives/stories/Fieldset.stories.tsx +212 -0
  156. package/src/primitives/stories/Input.stories.tsx +172 -0
  157. package/src/primitives/stories/Kbd.stories.tsx +183 -0
  158. package/src/primitives/stories/Label.stories.tsx +98 -0
  159. package/src/primitives/stories/Link.stories.tsx +260 -0
  160. package/src/primitives/stories/Popover.stories.tsx +178 -0
  161. package/src/primitives/stories/RadioGroup.stories.tsx +205 -0
  162. package/src/primitives/stories/Select.stories.tsx +222 -0
  163. package/src/primitives/stories/Separator.stories.tsx +134 -0
  164. package/src/primitives/stories/Slider.stories.tsx +203 -0
  165. package/src/primitives/stories/Spinner.stories.tsx +142 -0
  166. package/src/primitives/stories/Surface.stories.tsx +257 -0
  167. package/src/primitives/stories/Switch.stories.tsx +131 -0
  168. package/src/primitives/stories/Tabs.stories.tsx +275 -0
  169. package/src/primitives/stories/TextField.stories.tsx +139 -0
  170. package/src/primitives/stories/Textarea.stories.tsx +148 -0
  171. package/src/primitives/stories/Tooltip.stories.tsx +119 -0
  172. package/src/primitives/surface.tsx +86 -0
  173. package/src/primitives/switch.tsx +35 -0
  174. package/src/primitives/tabs.tsx +206 -0
  175. package/src/primitives/text-field.tsx +84 -0
  176. package/src/primitives/textarea.tsx +50 -0
  177. package/src/primitives/tooltip.tsx +58 -0
  178. package/src/services/CanvasExportService.ts +518 -0
  179. package/src/styles/base.css +380 -0
  180. package/src/styles/defaults.css +280 -0
  181. package/src/styles/globals.css +1242 -0
  182. package/src/styles/index.css +17 -0
  183. package/src/styles/ne-themes.css +4740 -0
  184. package/src/styles/tailwind.css +11 -0
  185. package/src/styles/tokens.css +117 -0
  186. package/src/styles/utilities.css +188 -0
  187. package/src/themes/apply-theme.ts +449 -0
  188. package/src/themes/getThemeStyles.ts +454 -0
  189. package/src/themes/index.ts +48 -0
  190. package/src/themes/oklch-theme.ts +283 -0
  191. package/src/themes/presets.ts +989 -0
  192. package/src/themes/types.ts +386 -0
  193. package/src/themes/useTheme.tsx +450 -0
  194. package/src/utils/dev-warnings.ts +161 -0
  195. package/src/utils/devWarnings.ts +153 -0
  196. package/dist/styles.css +0 -1
@@ -0,0 +1,246 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import {
5
+ useMediaQuery,
6
+ useViewportDimensions,
7
+ useResponsiveImageCap,
8
+ useWideMonitorMode,
9
+ type ViewportDimensions,
10
+ type ResponsiveImageCapInfo,
11
+ } from "../../hooks/viewport";
12
+ import { ImageEdgeBlur } from "./ImageEdgeBlur";
13
+
14
+ /**
15
+ * Props passed to the hero image render function.
16
+ */
17
+ export interface HeroImageRenderProps {
18
+ /** Maximum width in pixels for the image (responsive to ultra-wide monitors) */
19
+ maxWidth: number;
20
+ /** Whether we're in wide monitor mode (images don't cover full viewport) */
21
+ isWideMonitor: boolean;
22
+ /** Full viewport dimensions */
23
+ dimensions: ViewportDimensions;
24
+ /** Detailed image cap info */
25
+ imageCapInfo: ResponsiveImageCapInfo;
26
+ }
27
+
28
+ /**
29
+ * Props passed to the mobile carousel render function.
30
+ */
31
+ export interface CarouselRenderProps {
32
+ /** Image URLs or CarouselImage objects */
33
+ images: string[];
34
+ }
35
+
36
+ /**
37
+ * Props passed to the content render function.
38
+ */
39
+ export interface ContentRenderProps {
40
+ /** Whether we're on desktop (true), mobile (false), or unknown/SSR (null) */
41
+ isDesktop: boolean | null;
42
+ /** Whether we're in wide monitor mode */
43
+ isWideMonitor: boolean;
44
+ }
45
+
46
+ export interface PDPLayoutProps {
47
+ /**
48
+ * Render function for the hero/main product image on desktop.
49
+ * Receives responsive sizing info based on viewport.
50
+ */
51
+ renderHeroImage: (props: HeroImageRenderProps) => React.ReactNode;
52
+
53
+ /**
54
+ * Render function for the main content (options, cart button, etc.).
55
+ * This renders in both mobile and desktop layouts.
56
+ */
57
+ renderContent: (props: ContentRenderProps) => React.ReactNode;
58
+
59
+ /**
60
+ * Optional render function for the mobile product carousel.
61
+ * If not provided, mobile will only show the content section.
62
+ */
63
+ renderMobileCarousel?: (props: CarouselRenderProps) => React.ReactNode;
64
+
65
+ /**
66
+ * Image URLs for mobile carousel.
67
+ * Only used if renderMobileCarousel is provided.
68
+ */
69
+ carouselImages?: string[];
70
+
71
+ /**
72
+ * Sidebar width as a percentage (default: 0.35 for 35%).
73
+ * Used for wide monitor calculations.
74
+ */
75
+ sidebarPercent?: number;
76
+
77
+ /**
78
+ * Sidebar width as CSS value for desktop layout (default: "35%").
79
+ */
80
+ sidebarWidth?: string;
81
+
82
+ /**
83
+ * Whether to show edge blur on wide monitors (default: true).
84
+ */
85
+ showEdgeBlur?: boolean;
86
+
87
+ /**
88
+ * Current image URL for the edge blur effect.
89
+ * Required if showEdgeBlur is true.
90
+ */
91
+ blurImageSrc?: string;
92
+
93
+ /**
94
+ * Height of the image container as CSS value (default: "83vw").
95
+ * Used for edge blur positioning.
96
+ */
97
+ containerHeight?: string;
98
+
99
+ /**
100
+ * Additional class names for the layout container.
101
+ */
102
+ className?: string;
103
+
104
+ /**
105
+ * Children rendered after the layout (floating elements, FABs, etc.).
106
+ */
107
+ children?: React.ReactNode;
108
+ }
109
+
110
+ /**
111
+ * PDPLayout - All-in-one Product Detail Page layout component.
112
+ *
113
+ * Handles:
114
+ * - Mobile vs desktop layout switching (SSR-safe)
115
+ * - Wide monitor detection and handling
116
+ * - Responsive image capping for ultra-wide monitors
117
+ * - Edge blur effect for image gaps
118
+ *
119
+ * Uses render props for maximum flexibility while handling the
120
+ * complex viewport-aware logic internally.
121
+ *
122
+ * @example Basic usage
123
+ * ```tsx
124
+ * <PDPLayout
125
+ * carouselImages={mockupUrls}
126
+ * renderHeroImage={({ maxWidth }) => (
127
+ * <HeroProductImage maxWidth={maxWidth} />
128
+ * )}
129
+ * renderContent={() => (
130
+ * <>
131
+ * <ProductOptions />
132
+ * <AddToCart />
133
+ * </>
134
+ * )}
135
+ * renderMobileCarousel={({ images }) => (
136
+ * <MobileProductCarousel images={images} />
137
+ * )}
138
+ * />
139
+ * ```
140
+ *
141
+ * @example With edge blur
142
+ * ```tsx
143
+ * <PDPLayout
144
+ * showEdgeBlur
145
+ * blurImageSrc={currentMockupUrl}
146
+ * renderHeroImage={...}
147
+ * renderContent={...}
148
+ * >
149
+ * <FloatingActionGroup />
150
+ * </PDPLayout>
151
+ * ```
152
+ */
153
+ export function PDPLayout({
154
+ renderHeroImage,
155
+ renderContent,
156
+ renderMobileCarousel,
157
+ carouselImages = [],
158
+ sidebarPercent = 0.35,
159
+ sidebarWidth = "35%",
160
+ showEdgeBlur = true,
161
+ blurImageSrc,
162
+ containerHeight = "83vw",
163
+ className = "",
164
+ children,
165
+ }: PDPLayoutProps) {
166
+ // SSR-safe viewport detection
167
+ const isDesktop = useMediaQuery("(min-width: 768px)");
168
+
169
+ // Consolidated viewport dimensions (single resize listener on desktop, none on mobile)
170
+ const dimensions = useViewportDimensions(isDesktop);
171
+
172
+ // Responsive image capping for ultra-wide monitors
173
+ const imageCapInfo = useResponsiveImageCap(dimensions);
174
+
175
+ // Wide monitor mode detection
176
+ const isWideMonitor = useWideMonitorMode(imageCapInfo, sidebarPercent);
177
+
178
+ // Render props data
179
+ const heroProps: HeroImageRenderProps = {
180
+ maxWidth: imageCapInfo.maxWidthPx,
181
+ isWideMonitor,
182
+ dimensions,
183
+ imageCapInfo,
184
+ };
185
+
186
+ const contentProps: ContentRenderProps = {
187
+ isDesktop,
188
+ isWideMonitor,
189
+ };
190
+
191
+ return (
192
+ <div className={`pdp-layout ${className}`}>
193
+ {/* Mobile Layout */}
194
+ {(isDesktop === false || isDesktop === null) && (
195
+ <div className="md:hidden">
196
+ {/* Mobile Carousel (if provided) */}
197
+ {renderMobileCarousel && (
198
+ <div className="mobile-carousel-container">
199
+ {renderMobileCarousel({ images: carouselImages })}
200
+ </div>
201
+ )}
202
+
203
+ {/* Mobile Content */}
204
+ <div className="mobile-content p-4">
205
+ {renderContent(contentProps)}
206
+ </div>
207
+ </div>
208
+ )}
209
+
210
+ {/* Desktop Layout */}
211
+ {(isDesktop === true || isDesktop === null) && (
212
+ <div className="hidden md:block relative">
213
+ {/* Hero Image Section */}
214
+ <div className="desktop-hero relative">
215
+ {renderHeroImage(heroProps)}
216
+
217
+ {/* Edge Blur for Wide Monitors */}
218
+ {showEdgeBlur && blurImageSrc && isWideMonitor && (
219
+ <ImageEdgeBlur
220
+ imageSrc={blurImageSrc}
221
+ imageCapInfo={imageCapInfo}
222
+ sidebarPercent={sidebarPercent}
223
+ sidebarWidth={sidebarWidth}
224
+ containerHeight={containerHeight}
225
+ />
226
+ )}
227
+ </div>
228
+
229
+ {/* Desktop Sidebar/Content */}
230
+ <div
231
+ className="desktop-sidebar"
232
+ style={{
233
+ width: sidebarWidth,
234
+ maxWidth: "24rem",
235
+ }}
236
+ >
237
+ {renderContent(contentProps)}
238
+ </div>
239
+ </div>
240
+ )}
241
+
242
+ {/* Floating Elements (FABs, etc.) */}
243
+ {children}
244
+ </div>
245
+ );
246
+ }
@@ -0,0 +1,140 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+
5
+ /**
6
+ * Blur configuration - these values produce smooth edge blur effects
7
+ */
8
+ const BLUR_CONFIG = {
9
+ blur: 24,
10
+ crossfade: 60,
11
+ } as const;
12
+
13
+ /**
14
+ * Progressive blur layers for smooth sharp-to-blurred transition.
15
+ */
16
+ const BLUR_LAYERS = [
17
+ { blur: Math.round(BLUR_CONFIG.blur * 0.2), zone: 0.3 },
18
+ { blur: Math.round(BLUR_CONFIG.blur * 0.5), zone: 0.6 },
19
+ { blur: BLUR_CONFIG.blur, zone: 1.0 },
20
+ ];
21
+
22
+ /**
23
+ * Generate smooth fade-in mask gradient from left edge
24
+ */
25
+ function getFadeInMask(crossfadeWidth: number): string {
26
+ return `linear-gradient(to right,
27
+ transparent 0px,
28
+ rgba(0,0,0,0.02) ${crossfadeWidth * 0.05}px,
29
+ rgba(0,0,0,0.08) ${crossfadeWidth * 0.15}px,
30
+ rgba(0,0,0,0.2) ${crossfadeWidth * 0.3}px,
31
+ rgba(0,0,0,0.4) ${crossfadeWidth * 0.5}px,
32
+ rgba(0,0,0,0.6) ${crossfadeWidth * 0.7}px,
33
+ rgba(0,0,0,0.8) ${crossfadeWidth * 0.85}px,
34
+ rgba(0,0,0,0.95) ${crossfadeWidth}px,
35
+ black ${crossfadeWidth + 10}px
36
+ )`;
37
+ }
38
+
39
+ export interface SimpleImageBlurProps {
40
+ /** Source URL of the image to blur */
41
+ imageSrc: string;
42
+ /** Width of the blur box in pixels */
43
+ width: number;
44
+ /** Height of the blur box (CSS value, e.g., "100%", "500px") */
45
+ height?: string;
46
+ /** Additional CSS class names */
47
+ className?: string;
48
+ /** Object position for the image (default: "center") */
49
+ objectPosition?: string;
50
+ }
51
+
52
+ /**
53
+ * SimpleImageBlur - A simple blur box component with explicit dimensions.
54
+ *
55
+ * Renders a box of specified dimensions filled with progressively blurred
56
+ * versions of the source image. The blur fades in from the left edge for
57
+ * seamless blending with adjacent sharp images.
58
+ *
59
+ * Usage: Position this component where you need blur, and specify exact dimensions.
60
+ * The caller is responsible for layout/positioning.
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * // Position as overlay on right side of container
65
+ * <div className="relative">
66
+ * <img src={heroImage} className="w-full" />
67
+ * <div className="absolute top-0 right-0 bottom-0" style={{ width: 300 }}>
68
+ * <SimpleImageBlur
69
+ * imageSrc={heroImage}
70
+ * width={300}
71
+ * height="100%"
72
+ * />
73
+ * </div>
74
+ * </div>
75
+ * ```
76
+ */
77
+ export const SimpleImageBlur = React.memo(function SimpleImageBlur({
78
+ imageSrc,
79
+ width,
80
+ height = "100%",
81
+ className = "",
82
+ objectPosition = "center",
83
+ }: SimpleImageBlurProps) {
84
+ if (!imageSrc || width <= 0) return null;
85
+
86
+ // Generate fade-in mask for smooth transition from left
87
+ const fadeInMask = getFadeInMask(BLUR_CONFIG.crossfade);
88
+
89
+ // Extend width slightly to prevent blur edge artifacts
90
+ const extendedWidth = width + BLUR_CONFIG.blur * 2;
91
+
92
+ return (
93
+ <div
94
+ className={`relative overflow-hidden ${className}`}
95
+ style={{
96
+ width,
97
+ height,
98
+ }}
99
+ >
100
+ {/* Blur layers with progressive blur amounts */}
101
+ {BLUR_LAYERS.map((layer, index) => {
102
+ // Each layer has a mask that reveals it in its zone
103
+ // Last layer has no mask (fills everything)
104
+ const layerMask =
105
+ index === BLUR_LAYERS.length - 1
106
+ ? fadeInMask
107
+ : `linear-gradient(to right,
108
+ black 0%,
109
+ black ${layer.zone * 100}%,
110
+ transparent ${layer.zone * 100 + 10}%)`;
111
+
112
+ return (
113
+ <img
114
+ key={`blur-${index}`}
115
+ src={imageSrc}
116
+ alt=""
117
+ crossOrigin="anonymous"
118
+ style={{
119
+ position: "absolute",
120
+ // Extend bounds slightly to prevent edge artifacts
121
+ top: "-5%",
122
+ left: "-5%",
123
+ bottom: "-5%",
124
+ width: `${extendedWidth * 1.1}px`,
125
+ height: "110%",
126
+ objectFit: "cover",
127
+ objectPosition,
128
+ filter: `blur(${layer.blur}px)`,
129
+ clipPath: "inset(0)",
130
+ maskImage: layerMask,
131
+ WebkitMaskImage: layerMask,
132
+ }}
133
+ />
134
+ );
135
+ })}
136
+ </div>
137
+ );
138
+ });
139
+
140
+ export default SimpleImageBlur;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * PDP (Product Detail Page) Layout Components
3
+ *
4
+ * A collection of components for building product detail pages with:
5
+ * - Mobile/desktop responsive switching
6
+ * - Ultra-wide monitor support
7
+ * - Render props for maximum flexibility
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * import { PDPLayout } from '@snowcone-app/ui';
12
+ *
13
+ * <PDPLayout
14
+ * renderHeroImage={({ maxWidth }) => <HeroProductImage maxWidth={maxWidth} />}
15
+ * renderContent={() => <ProductOptions />}
16
+ * renderMobileCarousel={({ images }) => <MobileProductCarousel images={images} />}
17
+ * carouselImages={mockupUrls}
18
+ * />
19
+ * ```
20
+ */
21
+
22
+ export { PDPLayout } from "./PDPLayout";
23
+ export type {
24
+ PDPLayoutProps,
25
+ HeroImageRenderProps,
26
+ CarouselRenderProps,
27
+ ContentRenderProps,
28
+ } from "./PDPLayout";
29
+
30
+ export { ImageEdgeBlur } from "./ImageEdgeBlur";
31
+ export type { ImageEdgeBlurProps } from "./ImageEdgeBlur";
32
+
33
+ export { SimpleImageBlur } from "./SimpleImageBlur";
34
+ export type { SimpleImageBlurProps } from "./SimpleImageBlur";
35
+
36
+ export { EdgeBlurBox } from "./EdgeBlurBox";
37
+ export type { EdgeBlurBoxProps } from "./EdgeBlurBox";
38
+
39
+ export { ImageBlurExtension } from "./ImageBlurExtension";
40
+ export type { ImageBlurExtensionProps } from "./ImageBlurExtension";
package/src/lib/env.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Bundler-agnostic env reader.
3
+ *
4
+ * `@snowcone-app/ui` ships browser code that must import cleanly under any
5
+ * bundler — Next, but also Vite/CRA/esbuild where `process` is not defined.
6
+ * A bare `process.env.X` at module top-level throws `ReferenceError: process
7
+ * is not defined` during module evaluation, which surfaces as a silent
8
+ * white-screen with zero console errors (an uncaught module-eval exception).
9
+ *
10
+ * `globalThis.process?.env?.[key]` never throws: it reads undefined instead of
11
+ * referencing a missing binding. Always use this instead of bare `process.env`.
12
+ */
13
+ export function readEnv(key: string): string | undefined {
14
+ return globalThis.process?.env?.[key];
15
+ }
@@ -0,0 +1,167 @@
1
+ import brandConfig from "@snowcone-app/brand";
2
+ import type {
3
+ SupportedLanguage,
4
+ SupportedCountry,
5
+ SupportedLocale,
6
+ BrandLocale,
7
+ BrandAssets,
8
+ } from "@snowcone-app/brand";
9
+
10
+ export type { SupportedLanguage, SupportedCountry, SupportedLocale, BrandLocale, BrandAssets };
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Brand access
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * Returns brand data for a language. Throws if the language is missing —
18
+ * brand tokens must never silently fall back. See ADR-0035.
19
+ */
20
+ export function getBrand(language: SupportedLanguage): BrandLocale {
21
+ const data = (brandConfig.locales as Record<string, BrandLocale>)[language];
22
+ if (!data) {
23
+ throw new Error(
24
+ `[brand] Missing language "${language}" in brand.json. ` +
25
+ `Available: ${Object.keys(brandConfig.locales).join(", ")}. ` +
26
+ `Add it to brand.json (requires trademark + marketing sign-off).`
27
+ );
28
+ }
29
+ return data;
30
+ }
31
+
32
+ /** Global brand assets shared across all locales. */
33
+ export const brandAssets: BrandAssets = brandConfig.global_assets;
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Currency formatting
37
+ // ---------------------------------------------------------------------------
38
+
39
+ export interface FormatCurrencyOptions {
40
+ /** Input is in cents (default: true). When false, input is whole dollars. */
41
+ inCents?: boolean;
42
+ /** ISO 4217 currency code (default: "USD"). */
43
+ currency?: string;
44
+ /** Minimum fraction digits (default: 0 — hides ".00"). */
45
+ minimumFractionDigits?: number;
46
+ /** Maximum fraction digits (default: 2). */
47
+ maximumFractionDigits?: number;
48
+ /** Fallback string when amount is null/undefined (default: "—"). */
49
+ fallback?: string;
50
+ }
51
+
52
+ /**
53
+ * Locale-aware currency formatting. Language controls number formatting
54
+ * (decimal separator, digit grouping); currency is independent.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * formatCurrency(2999) // "$29.99" (cents, en, USD)
59
+ * formatCurrency(2900) // "$29" (hides .00)
60
+ * formatCurrency(29.99, "en", { inCents: false }) // "$29.99"
61
+ * formatCurrency(2999, "es", { currency: "EUR" }) // "29,99 €"
62
+ * formatCurrency(2999, "es", { currency: "USD" }) // "29,99 US$"
63
+ * formatCurrency(2999, "zh-CN") // "US$29.99"
64
+ * formatCurrency(null) // "—"
65
+ * ```
66
+ */
67
+ export function formatCurrency(
68
+ amount: number | null | undefined,
69
+ language: SupportedLanguage | string = "en",
70
+ options: FormatCurrencyOptions = {}
71
+ ): string {
72
+ if (amount == null) return options.fallback ?? "—";
73
+
74
+ const {
75
+ inCents = true,
76
+ currency = "USD",
77
+ minimumFractionDigits = 0,
78
+ maximumFractionDigits = 2,
79
+ } = options;
80
+
81
+ const value = inCents ? amount / 100 : amount;
82
+
83
+ // Map our language keys to BCP 47 tags that Intl understands
84
+ const bcp47 = languageToIntl(language);
85
+
86
+ return new Intl.NumberFormat(bcp47, {
87
+ style: "currency",
88
+ currency,
89
+ minimumFractionDigits,
90
+ maximumFractionDigits,
91
+ }).format(value);
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Date formatting
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /**
99
+ * Locale-aware date formatting. Replaces all hardcoded `toLocaleDateString("en-US", ...)`.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * formatDate(new Date()) // "Apr 10, 2026" (en)
104
+ * formatDate("2026-04-10", "zh-CN") // "2026年4月10日"
105
+ * formatDate(new Date(), "en", { dateStyle: "full" }) // "Friday, April 10, 2026"
106
+ * formatDate(null) // "—"
107
+ * ```
108
+ */
109
+ export function formatDate(
110
+ date: Date | string | number | null | undefined,
111
+ language: SupportedLanguage | string = "en",
112
+ options?: Intl.DateTimeFormatOptions,
113
+ fallback = "—"
114
+ ): string {
115
+ if (date == null) return fallback;
116
+
117
+ const d = date instanceof Date ? date : new Date(date);
118
+ if (isNaN(d.getTime())) return fallback;
119
+
120
+ const bcp47 = languageToIntl(language);
121
+ const defaults: Intl.DateTimeFormatOptions = options ?? {
122
+ year: "numeric",
123
+ month: "short",
124
+ day: "numeric",
125
+ };
126
+
127
+ return new Intl.DateTimeFormat(bcp47, defaults).format(d);
128
+ }
129
+
130
+ /**
131
+ * Locale-aware date+time formatting.
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * formatDateTime(new Date()) // "Apr 10, 2026, 3:45 PM"
136
+ * formatDateTime(new Date(), "zh-CN") // "2026年4月10日 下午3:45"
137
+ * ```
138
+ */
139
+ export function formatDateTime(
140
+ date: Date | string | number | null | undefined,
141
+ language: SupportedLanguage | string = "en",
142
+ options?: Intl.DateTimeFormatOptions,
143
+ fallback = "—"
144
+ ): string {
145
+ const defaults: Intl.DateTimeFormatOptions = options ?? {
146
+ year: "numeric",
147
+ month: "short",
148
+ day: "numeric",
149
+ hour: "numeric",
150
+ minute: "2-digit",
151
+ };
152
+ return formatDate(date, language, defaults, fallback);
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Internal helpers
157
+ // ---------------------------------------------------------------------------
158
+
159
+ /**
160
+ * Maps our language keys (e.g. "en", "zh-CN", "es") to BCP 47 tags that Intl APIs need.
161
+ * "en" → "en-US", "es" → "es-ES", "zh-CN" stays "zh-CN".
162
+ */
163
+ function languageToIntl(language: string): string {
164
+ if (language === "en") return "en-US";
165
+ if (language === "es") return "es-ES";
166
+ return language;
167
+ }
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+
5
+ /**
6
+ * The minimal navigation surface @snowcone-app/ui needs — framework-agnostic.
7
+ * ui never imports next/navigation directly; components go through this so the
8
+ * library resolves in any framework (Vite, Remix, Astro, plain React, Next).
9
+ */
10
+ export interface UiRouter {
11
+ push(url: string): void;
12
+ replace(url: string): void;
13
+ }
14
+
15
+ // Default: drive the browser's History API. Works everywhere; does a real
16
+ // navigation (not a no-op). SSR-guarded.
17
+ const historyRouter: UiRouter = {
18
+ push: (url) => {
19
+ if (typeof window !== "undefined") window.location.assign(url);
20
+ },
21
+ replace: (url) => {
22
+ if (typeof window !== "undefined") window.location.replace(url);
23
+ },
24
+ };
25
+
26
+ const UiRouterContext = createContext<UiRouter | null>(null);
27
+
28
+ /**
29
+ * Inject your framework's client-side router so ui navigation (e.g. search
30
+ * result clicks) is SPA-smooth instead of a full-page load. Next apps can use
31
+ * `UiNextRouterProvider` from `@snowcone-app/ui/next`; other frameworks pass
32
+ * their own `{ push, replace }`.
33
+ */
34
+ export const UiRouterProvider = UiRouterContext.Provider;
35
+
36
+ /** Resolve the active router: the injected one, else the History default. */
37
+ export function useUiRouter(): UiRouter {
38
+ return useContext(UiRouterContext) ?? historyRouter;
39
+ }
40
+
41
+ /** Read the current URL query params. Framework-agnostic, SSR-safe. */
42
+ export function useUiSearchParams(): URLSearchParams {
43
+ return new URLSearchParams(
44
+ typeof window !== "undefined" ? window.location.search : "",
45
+ );
46
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }