@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#600](https://github.com/snowcone-app/snowcone-monorepo/pull/600) [`3349e6c`](https://github.com/snowcone-app/snowcone-monorepo/commit/3349e6cd6bc1cfa2e1b44bcbb5199eb2aa7dfc81) Thanks [@kevinsproles](https://github.com/kevinsproles)! - BREAKING: the zero-config prebuilt stylesheet is removed. `@snowcone-app/ui/styles.css` (compiled `dist/styles.css`) and the `.app-modern` theme-variable scope no longer exist — the package's unscoped compiled Tailwind utilities could collide with a host app's own classes, and the prebuilt sheet contradicted the primary host-compiled consumption mode.
|
|
8
|
+
|
|
9
|
+
The single supported setup is a **Tailwind v4 host** that compiles ui's styles:
|
|
10
|
+
|
|
11
|
+
```css
|
|
12
|
+
/* app/globals.css — your Tailwind v4 entry */
|
|
13
|
+
@import "@snowcone-app/ui/styles/globals.css"; /* Tailwind + theme tokens + ui @source */
|
|
14
|
+
@source '../app/**/*.{js,ts,jsx,tsx}'; /* scan your own code too */
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// next.config.ts
|
|
19
|
+
const nextConfig = { transpilePackages: ["@snowcone-app/ui"] };
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The published package now ships its `src/` (so the Tailwind `@source` scan and style imports work from npm) and exports `./styles/globals.css`, `./styles/defaults.css`, `./styles/base.css`, and `./styles/utilities.css`. Theme variables live on `:root` (dark on `.dark`) — no wrapper class. Full story: https://developers.snowcone.app/shop-setup
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [[`b04c995`](https://github.com/snowcone-app/snowcone-monorepo/commit/b04c995d0a115760341f8b51f1ecbfb01deb8d6b), [`2adb836`](https://github.com/snowcone-app/snowcone-monorepo/commit/2adb836ef477809747d64053d55a84cfa166df9a)]:
|
|
27
|
+
- @snowcone-app/sdk@0.12.0
|
|
28
|
+
|
|
29
|
+
## 0.1.43
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- Updated dependencies [[`f15c421`](https://github.com/snowcone-app/snowcone-monorepo/commit/f15c421f11a793064fc5471f3286760f393ac4d7)]:
|
|
34
|
+
- @snowcone-app/sdk@0.11.0
|
|
35
|
+
|
|
3
36
|
## 0.1.42
|
|
4
37
|
|
|
5
38
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -121,16 +121,30 @@ const pattern: SeamlessPattern = { type: 'pattern', src: 'https://…/p.jpg', ti
|
|
|
121
121
|
Components use Tailwind CSS with semantic theme tokens (`bg-background`,
|
|
122
122
|
`text-foreground`, `bg-primary`, `border-border`, …), so they inherit your theme.
|
|
123
123
|
|
|
124
|
-
**Tailwind v4**
|
|
124
|
+
The package requires a **Tailwind v4** host: your app compiles the styles, with
|
|
125
|
+
ui's source as an extra `@source`. Import ui's stylesheet (it brings in
|
|
126
|
+
Tailwind, the theme tokens, and an `@source` for the package's own components)
|
|
127
|
+
and add an `@source` for your own code:
|
|
125
128
|
|
|
126
129
|
```css
|
|
127
130
|
/* app/globals.css */
|
|
128
|
-
@import "
|
|
131
|
+
@import "@snowcone-app/ui/styles/globals.css";
|
|
129
132
|
@source "../app/**/*.{js,ts,jsx,tsx}";
|
|
130
|
-
@source "../node_modules/@snowcone-app/ui/src/**/*.{js,ts,jsx,tsx}";
|
|
131
133
|
```
|
|
132
134
|
|
|
133
|
-
|
|
135
|
+
In Next.js, also transpile the package (it ships TypeScript source for the
|
|
136
|
+
Tailwind scan):
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// next.config.ts
|
|
140
|
+
const nextConfig = {
|
|
141
|
+
transpilePackages: ["@snowcone-app/ui"],
|
|
142
|
+
};
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
For dark mode, toggle the `dark` class on an ancestor (e.g. `<html
|
|
146
|
+
class="dark">`). To customize the theme, override the CSS variables (declared
|
|
147
|
+
on `:root`) in your own stylesheet after the import.
|
|
134
148
|
|
|
135
149
|
## Development with mock data
|
|
136
150
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snowcone-app/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "React components for merchandise visualization and customization",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
"require": "./dist/themes/index.cjs",
|
|
42
42
|
"default": "./dist/themes/index.js"
|
|
43
43
|
},
|
|
44
|
+
"./styles/defaults.css": "./src/styles/defaults.css",
|
|
45
|
+
"./styles/base.css": "./src/styles/base.css",
|
|
46
|
+
"./styles/utilities.css": "./src/styles/utilities.css",
|
|
47
|
+
"./styles/globals.css": "./src/styles/globals.css",
|
|
44
48
|
"./personalization/bridge": {
|
|
45
49
|
"types": "./dist/personalization/bridge.d.ts",
|
|
46
50
|
"import": "./dist/personalization/bridge.js",
|
|
@@ -52,11 +56,11 @@
|
|
|
52
56
|
"import": "./dist/next/index.js",
|
|
53
57
|
"require": "./dist/next/index.cjs",
|
|
54
58
|
"default": "./dist/next/index.js"
|
|
55
|
-
}
|
|
56
|
-
"./styles.css": "./dist/styles.css"
|
|
59
|
+
}
|
|
57
60
|
},
|
|
58
61
|
"files": [
|
|
59
62
|
"dist",
|
|
63
|
+
"src",
|
|
60
64
|
"README.md",
|
|
61
65
|
"CHANGELOG.md"
|
|
62
66
|
],
|
|
@@ -99,7 +103,7 @@
|
|
|
99
103
|
"react-instantsearch": "^7.15.5",
|
|
100
104
|
"react-zoom-pan-pinch": "^3.6.4",
|
|
101
105
|
"tailwind-merge": "^3.0.0",
|
|
102
|
-
"@snowcone-app/sdk": "0.
|
|
106
|
+
"@snowcone-app/sdk": "0.12.0"
|
|
103
107
|
},
|
|
104
108
|
"devDependencies": {
|
|
105
109
|
"@chromatic-com/storybook": "^4.1.2",
|
|
@@ -130,7 +134,7 @@
|
|
|
130
134
|
"@snowcone-app/brand": "0.1.0"
|
|
131
135
|
},
|
|
132
136
|
"scripts": {
|
|
133
|
-
"build": "tsup
|
|
137
|
+
"build": "tsup",
|
|
134
138
|
"clean": "rimraf dist",
|
|
135
139
|
"build:sandpack": "tsup src/index.ts --format esm --minify --treeshake --splitting false --out-dir build-sandpack --clean --external react,react-dom,next/navigation",
|
|
136
140
|
"test": "vitest run --environment=jsdom",
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CanvasIsolationBoundary - Prevents React re-renders from reaching canvas
|
|
5
|
+
*
|
|
6
|
+
* This component creates an isolation boundary between React's render cycle
|
|
7
|
+
* and the SnowconeCanvas. It ensures that context updates (like mockup results
|
|
8
|
+
* arriving) don't cause the canvas to re-render and lag during drag operations.
|
|
9
|
+
*
|
|
10
|
+
* Architecture:
|
|
11
|
+
* - Uses React.memo with a custom comparison that ALWAYS returns true
|
|
12
|
+
* - Children are rendered once and never re-rendered due to parent changes
|
|
13
|
+
* - Configuration updates flow through the CanvasExportService singleton
|
|
14
|
+
* - The canvas reads current values from the service, not from props/context
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <Product productId="...">
|
|
19
|
+
* <RealtimeProvider>
|
|
20
|
+
* <CanvasIsolationBoundary>
|
|
21
|
+
* <SnowconeCanvas onExport={canvasExportService.handleExport} />
|
|
22
|
+
* </CanvasIsolationBoundary>
|
|
23
|
+
*
|
|
24
|
+
* {/* These can re-render freely without affecting canvas *\/}
|
|
25
|
+
* <HeroProductImage />
|
|
26
|
+
* </RealtimeProvider>
|
|
27
|
+
* <ProductOptions />
|
|
28
|
+
* </Product>
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* IMPORTANT: The children should use canvasExportService for callbacks,
|
|
32
|
+
* not props passed from parent components.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import React, { useEffect } from 'react';
|
|
36
|
+
import { useProductOptional } from '../patterns/Product';
|
|
37
|
+
import { useRealtimeOptional } from '../patterns/RealtimeProvider';
|
|
38
|
+
import { canvasExportService } from '../services/CanvasExportService';
|
|
39
|
+
|
|
40
|
+
interface CanvasIsolationBoundaryProps {
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
/** Optional: Override placement name (defaults to selectedPlacement from context) */
|
|
43
|
+
placementName?: string;
|
|
44
|
+
/** Optional: Callback when artwork changes (for updating parent state on exit) */
|
|
45
|
+
onArtworkChange?: (src: string) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Optional: Function to clear ImageBitmap cache (memory management for iOS Safari)
|
|
48
|
+
* Pass clearImageBitmapCache from @snowcone-app/canvas
|
|
49
|
+
*/
|
|
50
|
+
clearImageBitmapCache?: () => void;
|
|
51
|
+
/**
|
|
52
|
+
* Optional: Function to serialize canvas state for server-side rendering.
|
|
53
|
+
* Pass serializeStateForServer from @snowcone-app/canvas.
|
|
54
|
+
* When provided, enables JSON mode: canvas state is sent as JSON instead of PNG blob.
|
|
55
|
+
*/
|
|
56
|
+
serializeStateForServer?: (state: any) => any;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Inner component that subscribes to contexts and configures the service.
|
|
61
|
+
* This component WILL re-render when context changes, but its children won't
|
|
62
|
+
* because they're wrapped in the isolation boundary.
|
|
63
|
+
*
|
|
64
|
+
* Can be used standalone (without CanvasIsolationBoundary) when you need
|
|
65
|
+
* to configure the singleton service but don't need isolation (e.g., when
|
|
66
|
+
* using a memoized canvas that handles its own re-render prevention).
|
|
67
|
+
*/
|
|
68
|
+
export function CanvasIsolationConfigurator({
|
|
69
|
+
placementName: placementNameProp,
|
|
70
|
+
onArtworkChange,
|
|
71
|
+
clearImageBitmapCache,
|
|
72
|
+
serializeStateForServer,
|
|
73
|
+
}: Omit<CanvasIsolationBoundaryProps, 'children'>) {
|
|
74
|
+
const productContext = useProductOptional();
|
|
75
|
+
const realtimeContext = useRealtimeOptional();
|
|
76
|
+
|
|
77
|
+
// Configure the service whenever context values change
|
|
78
|
+
// This effect runs on re-renders but doesn't cause child re-renders
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
canvasExportService.configure({
|
|
81
|
+
sendCanvasBlob: realtimeContext?.sendCanvasBlob,
|
|
82
|
+
sendCanvasState: realtimeContext?.sendCanvasState,
|
|
83
|
+
serializeStateForServer,
|
|
84
|
+
mockupCount: productContext?.product?.mockups?.length ?? 1,
|
|
85
|
+
isRealtimeEnabled: realtimeContext?.isEnabled ?? false,
|
|
86
|
+
placementName: placementNameProp || productContext?.selectedPlacement || productContext?.product?.placements?.[0]?.label || 'Front',
|
|
87
|
+
onArtworkChange,
|
|
88
|
+
enableRealtime: realtimeContext?.enableRealtime,
|
|
89
|
+
disableRealtime: realtimeContext?.disableRealtime,
|
|
90
|
+
clearImageBitmapCache,
|
|
91
|
+
// Wire CES's "I sent nothing" signal to the realtime context's
|
|
92
|
+
// cancellation broadcaster. Roll back any optimistic
|
|
93
|
+
// notifyPendingExport that didn't end up producing a real export.
|
|
94
|
+
notifyExportSkipped: realtimeContext?.notifyPendingExportCancelled,
|
|
95
|
+
});
|
|
96
|
+
}, [
|
|
97
|
+
realtimeContext?.sendCanvasBlob,
|
|
98
|
+
realtimeContext?.sendCanvasState,
|
|
99
|
+
serializeStateForServer,
|
|
100
|
+
productContext?.product?.mockups?.length,
|
|
101
|
+
realtimeContext?.isEnabled,
|
|
102
|
+
productContext?.selectedPlacement,
|
|
103
|
+
realtimeContext?.enableRealtime,
|
|
104
|
+
realtimeContext?.disableRealtime,
|
|
105
|
+
realtimeContext?.notifyPendingExportCancelled,
|
|
106
|
+
placementNameProp,
|
|
107
|
+
onArtworkChange,
|
|
108
|
+
clearImageBitmapCache,
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
// Cleanup on unmount
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
return () => {
|
|
114
|
+
canvasExportService.reset();
|
|
115
|
+
};
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
return null; // This component doesn't render anything
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The actual isolation boundary - NEVER re-renders children based on parent context changes
|
|
123
|
+
*
|
|
124
|
+
* NOTE: This prevents children from re-rendering when parent re-renders due to context
|
|
125
|
+
* changes (like mockup results arriving). Children can still re-render if their own
|
|
126
|
+
* props/state changes directly.
|
|
127
|
+
*
|
|
128
|
+
* However, since this uses React.memo with () => true, it blocks ALL re-renders
|
|
129
|
+
* from propagating to children. For artboard switching to work, the children must
|
|
130
|
+
* use a different mechanism (like refs or service calls) rather than props.
|
|
131
|
+
*/
|
|
132
|
+
const IsolatedChildren = React.memo(
|
|
133
|
+
function IsolatedChildren({ children }: { children: React.ReactNode }) {
|
|
134
|
+
return <>{children}</>;
|
|
135
|
+
},
|
|
136
|
+
// Custom comparison: ALWAYS return true (never re-render due to parent changes)
|
|
137
|
+
// This is intentional - children should use refs/services for updates that need to bypass this
|
|
138
|
+
() => true
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Main component that combines configurator and isolation boundary
|
|
143
|
+
*/
|
|
144
|
+
export function CanvasIsolationBoundary({
|
|
145
|
+
children,
|
|
146
|
+
placementName,
|
|
147
|
+
onArtworkChange,
|
|
148
|
+
clearImageBitmapCache,
|
|
149
|
+
serializeStateForServer,
|
|
150
|
+
}: CanvasIsolationBoundaryProps) {
|
|
151
|
+
return (
|
|
152
|
+
<>
|
|
153
|
+
{/* Configurator subscribes to context and updates service */}
|
|
154
|
+
<CanvasIsolationConfigurator
|
|
155
|
+
placementName={placementName}
|
|
156
|
+
onArtworkChange={onArtworkChange}
|
|
157
|
+
clearImageBitmapCache={clearImageBitmapCache}
|
|
158
|
+
serializeStateForServer={serializeStateForServer}
|
|
159
|
+
/>
|
|
160
|
+
{/* Children are isolated from re-renders */}
|
|
161
|
+
<IsolatedChildren>{children}</IsolatedChildren>
|
|
162
|
+
</>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Hook to use inside the isolation boundary.
|
|
168
|
+
* Returns stable references that don't change during drag.
|
|
169
|
+
*
|
|
170
|
+
* Usage:
|
|
171
|
+
* ```tsx
|
|
172
|
+
* function MyCanvasComponent() {
|
|
173
|
+
* const { handleExport, onEnterEditorMode, onExitEditorMode } = useIsolatedCanvas();
|
|
174
|
+
*
|
|
175
|
+
* return (
|
|
176
|
+
* <SnowconeCanvas
|
|
177
|
+
* onExport={handleExport}
|
|
178
|
+
* // ...
|
|
179
|
+
* />
|
|
180
|
+
* );
|
|
181
|
+
* }
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
export function useIsolatedCanvas() {
|
|
185
|
+
// These are stable references from the singleton
|
|
186
|
+
return {
|
|
187
|
+
/** Stable export handler - pass to SnowconeCanvas onExport */
|
|
188
|
+
handleExport: canvasExportService.handleExport,
|
|
189
|
+
/** Stable state change handler - pass to SnowconeCanvas onChange for JSON mode */
|
|
190
|
+
handleStateChange: canvasExportService.handleStateChange,
|
|
191
|
+
/** Call when entering editor mode */
|
|
192
|
+
onEnterEditorMode: () => canvasExportService.onEnterEditorMode(),
|
|
193
|
+
/** Call when exiting editor mode */
|
|
194
|
+
onExitEditorMode: () => canvasExportService.onExitEditorMode(),
|
|
195
|
+
/** Set initial artwork when entering editor */
|
|
196
|
+
setInitialArtwork: (src: string | null) => canvasExportService.setInitialArtwork(src),
|
|
197
|
+
/** Get last exported artwork */
|
|
198
|
+
getLastExportedArtwork: () => canvasExportService.getLastExportedArtwork(),
|
|
199
|
+
/** Check if artwork changed since entering editor */
|
|
200
|
+
hasArtworkChanged: () => canvasExportService.hasArtworkChanged(),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Loading overlay — frosted glass with soft prismatic color bands
|
|
5
|
+
* and twinkling sparkle particles. No center spinner or indicator.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
9
|
+
|
|
10
|
+
type Particle = {
|
|
11
|
+
left: number;
|
|
12
|
+
top: number;
|
|
13
|
+
delay: string;
|
|
14
|
+
duration: string;
|
|
15
|
+
size: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function generateParticles(): Particle[] {
|
|
19
|
+
return Array.from({ length: 16 }, () => ({
|
|
20
|
+
left: 10 + Math.random() * 80,
|
|
21
|
+
top: 15 + Math.random() * 70,
|
|
22
|
+
delay: (Math.random() * 3.5).toFixed(1),
|
|
23
|
+
duration: (3 + Math.random() * 2).toFixed(1),
|
|
24
|
+
size: 2 + Math.random() * 2.5,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const FADE_IN_MS = 600;
|
|
29
|
+
const FADE_OUT_MS = 400;
|
|
30
|
+
|
|
31
|
+
const STYLES = `
|
|
32
|
+
.prism-loading-overlay {
|
|
33
|
+
background: rgba(255, 255, 255, 0.35);
|
|
34
|
+
opacity: 0;
|
|
35
|
+
z-index: 1;
|
|
36
|
+
pointer-events: none;
|
|
37
|
+
will-change: opacity;
|
|
38
|
+
transition: opacity ${FADE_IN_MS}ms ease-in;
|
|
39
|
+
}
|
|
40
|
+
.prism-loading-overlay.prism-fade-in {
|
|
41
|
+
opacity: 1;
|
|
42
|
+
}
|
|
43
|
+
.prism-loading-overlay.prism-fade-out {
|
|
44
|
+
opacity: 0;
|
|
45
|
+
pointer-events: none;
|
|
46
|
+
transition: opacity ${FADE_OUT_MS}ms ease-out;
|
|
47
|
+
}
|
|
48
|
+
.prism-loading-rainbow {
|
|
49
|
+
position: absolute;
|
|
50
|
+
inset: 0;
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
}
|
|
53
|
+
.prism-loading-band {
|
|
54
|
+
position: absolute;
|
|
55
|
+
width: 200%;
|
|
56
|
+
height: 45%;
|
|
57
|
+
left: -50%;
|
|
58
|
+
opacity: 0;
|
|
59
|
+
filter: blur(25px);
|
|
60
|
+
animation: prism-loading-sweep 6s ease-in-out infinite;
|
|
61
|
+
}
|
|
62
|
+
.prism-loading-band:nth-child(1) {
|
|
63
|
+
top: 5%;
|
|
64
|
+
background: linear-gradient(90deg, transparent, rgba(255, 50, 50, 0.15), transparent);
|
|
65
|
+
animation-delay: -0.6s;
|
|
66
|
+
}
|
|
67
|
+
.prism-loading-band:nth-child(2) {
|
|
68
|
+
top: 20%;
|
|
69
|
+
background: linear-gradient(90deg, transparent, rgba(255, 170, 30, 0.12), transparent);
|
|
70
|
+
animation-delay: -1.2s;
|
|
71
|
+
}
|
|
72
|
+
.prism-loading-band:nth-child(3) {
|
|
73
|
+
top: 35%;
|
|
74
|
+
background: linear-gradient(90deg, transparent, rgba(40, 200, 80, 0.12), transparent);
|
|
75
|
+
animation-delay: -2.4s;
|
|
76
|
+
}
|
|
77
|
+
.prism-loading-band:nth-child(4) {
|
|
78
|
+
top: 50%;
|
|
79
|
+
background: linear-gradient(90deg, transparent, rgba(40, 120, 255, 0.15), transparent);
|
|
80
|
+
animation-delay: -2.7s;
|
|
81
|
+
}
|
|
82
|
+
.prism-loading-band:nth-child(5) {
|
|
83
|
+
top: 65%;
|
|
84
|
+
background: linear-gradient(90deg, transparent, rgba(140, 50, 255, 0.12), transparent);
|
|
85
|
+
animation-delay: -3.0s;
|
|
86
|
+
}
|
|
87
|
+
@keyframes prism-loading-sweep {
|
|
88
|
+
0% { transform: translateX(-35%) rotate(-6deg); opacity: 0; }
|
|
89
|
+
25% { opacity: 1; }
|
|
90
|
+
75% { opacity: 1; }
|
|
91
|
+
100% { transform: translateX(35%) rotate(-6deg); opacity: 0; }
|
|
92
|
+
}
|
|
93
|
+
.prism-loading-particle {
|
|
94
|
+
position: absolute;
|
|
95
|
+
border-radius: 50%;
|
|
96
|
+
background: white;
|
|
97
|
+
opacity: 0;
|
|
98
|
+
animation: prism-loading-sparkle infinite ease-in-out;
|
|
99
|
+
}
|
|
100
|
+
@keyframes prism-loading-sparkle {
|
|
101
|
+
0% { opacity: 0; transform: scale(0); }
|
|
102
|
+
20% { opacity: 0.8; transform: scale(1); }
|
|
103
|
+
40% { opacity: 0.3; transform: scale(0.6); }
|
|
104
|
+
60% { opacity: 0.7; transform: scale(1.1); }
|
|
105
|
+
80% { opacity: 0.2; transform: scale(0.5); }
|
|
106
|
+
100% { opacity: 0; transform: scale(0); }
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
// Inject styles once
|
|
111
|
+
let stylesInjected = false;
|
|
112
|
+
function ensureStyles() {
|
|
113
|
+
if (stylesInjected || typeof document === "undefined") return;
|
|
114
|
+
const style = document.createElement("style");
|
|
115
|
+
style.textContent = STYLES;
|
|
116
|
+
document.head.appendChild(style);
|
|
117
|
+
stylesInjected = true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface LoadingOverlayPrismProps {
|
|
121
|
+
/** When false, the overlay fades out then calls onExited. Default true. */
|
|
122
|
+
visible?: boolean;
|
|
123
|
+
/** Called after the fade-out transition completes. Use this to unmount. */
|
|
124
|
+
onExited?: () => void;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function LoadingOverlayPrism({
|
|
128
|
+
visible = true,
|
|
129
|
+
onExited,
|
|
130
|
+
}: LoadingOverlayPrismProps) {
|
|
131
|
+
ensureStyles();
|
|
132
|
+
|
|
133
|
+
console.log(`[Prism] render visible=${visible}`);
|
|
134
|
+
|
|
135
|
+
// Trigger fade-in on next frame after mount so the CSS transition plays
|
|
136
|
+
const [mounted, setMounted] = useState(false);
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
console.log(`[Prism] MOUNTED — scheduling fade-in`);
|
|
139
|
+
const raf = requestAnimationFrame(() => setMounted(true));
|
|
140
|
+
return () => cancelAnimationFrame(raf);
|
|
141
|
+
}, []);
|
|
142
|
+
|
|
143
|
+
// Empty on first render so SSR matches; populate after mount so each
|
|
144
|
+
// show of the loader gets a fresh random field.
|
|
145
|
+
const [particles, setParticles] = useState<Particle[]>([]);
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
setParticles(generateParticles());
|
|
148
|
+
}, []);
|
|
149
|
+
|
|
150
|
+
// Log visibility changes
|
|
151
|
+
const prevVisibleRef = useRef(visible);
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (prevVisibleRef.current && !visible) {
|
|
154
|
+
console.log(`[Prism] visible changed true→false — FADE-OUT STARTING`);
|
|
155
|
+
} else if (!prevVisibleRef.current && visible) {
|
|
156
|
+
console.log(`[Prism] visible changed false→true — FADE-IN RESTARTING`);
|
|
157
|
+
}
|
|
158
|
+
prevVisibleRef.current = visible;
|
|
159
|
+
}, [visible]);
|
|
160
|
+
|
|
161
|
+
const handleTransitionEnd = useCallback(() => {
|
|
162
|
+
if (!visible) {
|
|
163
|
+
console.log(`[Prism] FADE-OUT COMPLETE — calling onExited`);
|
|
164
|
+
onExited?.();
|
|
165
|
+
} else {
|
|
166
|
+
console.log(`[Prism] FADE-IN COMPLETE`);
|
|
167
|
+
}
|
|
168
|
+
}, [visible, onExited]);
|
|
169
|
+
|
|
170
|
+
let fadeClass = "";
|
|
171
|
+
if (!visible) {
|
|
172
|
+
fadeClass = " prism-fade-out";
|
|
173
|
+
} else if (mounted) {
|
|
174
|
+
fadeClass = " prism-fade-in";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
className={`prism-loading-overlay absolute inset-0${fadeClass}`}
|
|
180
|
+
onTransitionEnd={handleTransitionEnd}
|
|
181
|
+
>
|
|
182
|
+
<div className="prism-loading-rainbow">
|
|
183
|
+
<div className="prism-loading-band" />
|
|
184
|
+
<div className="prism-loading-band" />
|
|
185
|
+
<div className="prism-loading-band" />
|
|
186
|
+
<div className="prism-loading-band" />
|
|
187
|
+
<div className="prism-loading-band" />
|
|
188
|
+
</div>
|
|
189
|
+
{particles.map((p, i) => (
|
|
190
|
+
<div
|
|
191
|
+
key={i}
|
|
192
|
+
className="prism-loading-particle"
|
|
193
|
+
style={{
|
|
194
|
+
left: `${p.left}%`,
|
|
195
|
+
top: `${p.top}%`,
|
|
196
|
+
animationDelay: `${p.delay}s`,
|
|
197
|
+
animationDuration: `${p.duration}s`,
|
|
198
|
+
width: `${p.size}px`,
|
|
199
|
+
height: `${p.size}px`,
|
|
200
|
+
boxShadow: `0 0 6px rgba(255, 255, 255, 0.5)`,
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** How long to wait before showing the overlay (ms). If loading finishes
|
|
209
|
+
* within this window (e.g. cached image), the overlay never appears. */
|
|
210
|
+
const SHOW_DELAY_MS = 200;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Hook that manages the mount/fade-out lifecycle with a grace period.
|
|
214
|
+
*
|
|
215
|
+
* - Waits `SHOW_DELAY_MS` before mounting the overlay so fast loads skip it entirely.
|
|
216
|
+
* - When loading ends, keeps overlay mounted for a smooth fade-out, then unmounts.
|
|
217
|
+
*/
|
|
218
|
+
export function useLoadingOverlay(isLoading: boolean) {
|
|
219
|
+
const [showOverlay, setShowOverlay] = useState(false);
|
|
220
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
221
|
+
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (isLoading) {
|
|
224
|
+
timerRef.current = setTimeout(() => {
|
|
225
|
+
setShowOverlay(true);
|
|
226
|
+
}, SHOW_DELAY_MS);
|
|
227
|
+
} else {
|
|
228
|
+
if (timerRef.current) {
|
|
229
|
+
clearTimeout(timerRef.current);
|
|
230
|
+
timerRef.current = null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return () => {
|
|
235
|
+
if (timerRef.current) {
|
|
236
|
+
clearTimeout(timerRef.current);
|
|
237
|
+
timerRef.current = null;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}, [isLoading]);
|
|
241
|
+
|
|
242
|
+
const handleExited = useCallback(() => {
|
|
243
|
+
setShowOverlay(false);
|
|
244
|
+
}, []);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
showOverlay,
|
|
248
|
+
overlayVisible: isLoading,
|
|
249
|
+
handleExited,
|
|
250
|
+
};
|
|
251
|
+
}
|