@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,305 @@
1
+ "use client";
2
+
3
+ import React, { useCallback } from "react";
4
+ import { useProduct } from "../patterns/Product";
5
+ import {
6
+ prepareOptionRenderData,
7
+ handleOptionChange,
8
+ resolveBestCombination,
9
+ } from "@snowcone-app/sdk";
10
+ import { Button } from "../primitives/Button";
11
+
12
+ /**
13
+ * ProductOptions - Product variant selector with theme-aware styling
14
+ *
15
+ * ⚠️ **IMPORTANT: Must be used within a `<Product>` context provider!**
16
+ *
17
+ * A composed component that renders interactive UI for selecting product variants
18
+ * (size, color, material, etc.). Uses inline implementations with custom theme-aware
19
+ * styling for maximum control and visual quality.
20
+ *
21
+ * **Context Requirements:**
22
+ * - Reads product options, combinations, and current selection from Product context
23
+ * - Automatically updates context when user selects options
24
+ * - Returns `null` if product has no options
25
+ *
26
+ * Features:
27
+ * - Theme-aware borders using CSS variables
28
+ * - Custom color swatches with subtle borders (ring-border/30)
29
+ * - Smooth hover transitions and opacity states
30
+ * - Size buttons with proper spacing and rounded corners
31
+ * - Theme-controlled internal spacing via CSS variables
32
+ * - Inline implementations for full style control (no primitives)
33
+ *
34
+ * Spacing:
35
+ * - Internal spacing controlled by CSS variables:
36
+ * - `--spacing-option-groups` (default: 1rem) - gap between option groups
37
+ * - `--spacing-option-items` (default: 0.5rem) - gap between label and choices
38
+ * - External spacing should be controlled by parent container
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * // ✅ CORRECT - Basic usage within Product context
43
+ * <Product productId="shirt-123">
44
+ * <ProductOptions />
45
+ * </Product>
46
+ * ```
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * // ✅ CORRECT - Recommended layout with proper spacing
51
+ * <Product productId="shirt-123">
52
+ * <div className="flex flex-col gap-6">
53
+ * <ProductImage />
54
+ * <ProductOptions />
55
+ * <ProductPrice />
56
+ * <AddToCart />
57
+ * </div>
58
+ * </Product>
59
+ * ```
60
+ *
61
+ * @example
62
+ * ```tsx
63
+ * // ❌ WRONG - Missing Product context (will throw error!)
64
+ * <ProductOptions /> // ❌ useProduct() will throw!
65
+ * ```
66
+ *
67
+ * @example
68
+ * ```css
69
+ * // Customize internal spacing via CSS
70
+ * :root {
71
+ * --spacing-option-groups: 1.5rem; // Increase spacing between Size and Color
72
+ * --spacing-option-items: 0.75rem; // Increase spacing between label and buttons
73
+ * }
74
+ * ```
75
+ */
76
+ export function ProductOptions({ className }: { className?: string }) {
77
+ const context = useProduct();
78
+ const { optionAttributes, combinations, selection, updateSelection } =
79
+ context;
80
+
81
+ if (!optionAttributes || Object.keys(optionAttributes).length === 0) {
82
+ return null;
83
+ }
84
+
85
+ const optionRenderData = prepareOptionRenderData(
86
+ optionAttributes,
87
+ selection || {},
88
+ combinations || []
89
+ );
90
+
91
+ // Memoized handler to prevent unnecessary re-renders of child components
92
+ const handleChange = useCallback(
93
+ (attributeName: string, value: string) => {
94
+ const nextSelection = handleOptionChange(
95
+ attributeName,
96
+ value,
97
+ selection || {},
98
+ optionAttributes
99
+ );
100
+ const best = resolveBestCombination(
101
+ nextSelection,
102
+ optionAttributes,
103
+ combinations || []
104
+ );
105
+
106
+ if (updateSelection) {
107
+ updateSelection(nextSelection);
108
+ }
109
+ },
110
+ [selection, optionAttributes, combinations, updateSelection]
111
+ );
112
+
113
+ return (
114
+ <div
115
+ className={`flex flex-col ${className || ""}`}
116
+ style={{ gap: 'var(--spacing-option-groups, 1rem)' }}
117
+ >
118
+ {optionRenderData.map((optionData) => (
119
+ <div
120
+ key={optionData.key}
121
+ className="flex flex-col"
122
+ style={{ gap: 'var(--spacing-option-items, 0.5rem)' }}
123
+ >
124
+ <label className="flex items-center gap-3 text-base font-label">
125
+ {optionData.label}
126
+ {optionData.value && optionData.type === "swatch" && (
127
+ <span className="text-base font-caption ml-1 text-primary">
128
+ {optionData.value}
129
+ </span>
130
+ )}
131
+ </label>
132
+ <OptionGroup
133
+ optionData={optionData}
134
+ onChange={(value) => handleChange(optionData.key, value)}
135
+ />
136
+ </div>
137
+ ))}
138
+ </div>
139
+ );
140
+ }
141
+
142
+ // Internal component that renders different UI types based on option data
143
+ function OptionGroup({
144
+ optionData,
145
+ onChange,
146
+ }: {
147
+ optionData: any;
148
+ onChange: (value: string) => void;
149
+ }) {
150
+ switch (optionData.type) {
151
+ case "color-picker":
152
+ return <ColorPicker optionData={optionData} onChange={onChange} />;
153
+
154
+ case "swatch":
155
+ return <ColorSwatches optionData={optionData} onChange={onChange} />;
156
+
157
+ case "select":
158
+ default:
159
+ // Always use buttons, never dropdowns
160
+ return <OptionButtons optionData={optionData} onChange={onChange} />;
161
+ }
162
+ }
163
+
164
+ // Color picker with rainbow gradient border
165
+ function ColorPicker({
166
+ optionData,
167
+ onChange,
168
+ }: {
169
+ optionData: any;
170
+ onChange: (value: string) => void;
171
+ }) {
172
+ const inputId = `color-picker-${optionData.key}`;
173
+
174
+ return (
175
+ <div className="flex items-center gap-3">
176
+ <label
177
+ htmlFor={inputId}
178
+ aria-label={`Select color for ${optionData.label}`}
179
+ className="relative h-12 w-12 cursor-pointer rounded-full"
180
+ style={{
181
+ background: `conic-gradient(from 0deg,
182
+ hsl(0, 100%, 50%),
183
+ hsl(30, 100%, 50%),
184
+ hsl(60, 100%, 50%),
185
+ hsl(90, 100%, 50%),
186
+ hsl(120, 100%, 50%),
187
+ hsl(150, 100%, 50%),
188
+ hsl(180, 100%, 50%),
189
+ hsl(210, 100%, 50%),
190
+ hsl(240, 100%, 50%),
191
+ hsl(270, 100%, 50%),
192
+ hsl(300, 100%, 50%),
193
+ hsl(330, 100%, 50%),
194
+ hsl(360, 100%, 50%))`,
195
+ padding: "3px",
196
+ }}
197
+ >
198
+ <input
199
+ id={inputId}
200
+ type="color"
201
+ value={optionData.value || "#000000"}
202
+ onChange={(e) => onChange(e.target.value)}
203
+ className="sr-only"
204
+ />
205
+ <div
206
+ className="w-full h-full rounded-full border-2 border-background"
207
+ style={{ backgroundColor: optionData.value || "#000000" }}
208
+ />
209
+ </label>
210
+ <div className="text-base font-caption text-muted-foreground ml-1">
211
+ {optionData.label}
212
+ </div>
213
+ </div>
214
+ );
215
+ }
216
+
217
+ // Color swatches using Button primitive with option-swatch variant
218
+ function ColorSwatches({
219
+ optionData,
220
+ onChange,
221
+ }: {
222
+ optionData: any;
223
+ onChange: (value: string) => void;
224
+ }) {
225
+ return (
226
+ <div className="flex flex-wrap gap-2">
227
+ {optionData.choices.map((choice: any) => (
228
+ <Button
229
+ key={choice.value}
230
+ variant="option-swatch"
231
+ size="none"
232
+ selected={choice.selected}
233
+ disabled={choice.disabled}
234
+ onClick={() => onChange(choice.value)}
235
+ aria-label={choice.label}
236
+ aria-pressed={choice.selected}
237
+ role="radio"
238
+ aria-checked={choice.selected}
239
+ title={choice.label}
240
+ >
241
+ <span
242
+ className={`absolute ${
243
+ choice.selected
244
+ ? "inset-[3px]"
245
+ : "inset-0"
246
+ } rounded-full block bg-cover bg-center`}
247
+ style={{
248
+ backgroundColor: choice.hex || "var(--color-border, #ccc)",
249
+ backgroundImage: choice.imageUrl
250
+ ? `url(${choice.imageUrl})`
251
+ : undefined,
252
+ }}
253
+ />
254
+ {choice.disabled && (
255
+ <>
256
+ {/* Diagonal X lines using foreground color */}
257
+ <span
258
+ className="absolute inset-0 rounded-full pointer-events-none opacity-80"
259
+ style={{
260
+ background: `linear-gradient(45deg, transparent calc(50% - 1.5px), var(--color-foreground) calc(50% - 1.5px), var(--color-foreground) calc(50% + 1.5px), transparent calc(50% + 1.5px))`,
261
+ }}
262
+ />
263
+ <span
264
+ className="absolute inset-0 rounded-full pointer-events-none opacity-80"
265
+ style={{
266
+ background: `linear-gradient(-45deg, transparent calc(50% - 1.5px), var(--color-foreground) calc(50% - 1.5px), var(--color-foreground) calc(50% + 1.5px), transparent calc(50% + 1.5px))`,
267
+ }}
268
+ />
269
+ {/* Semi-transparent overlay to dim the color - uses background for contrast */}
270
+ <span className="absolute inset-0 rounded-full pointer-events-none bg-background opacity-40" />
271
+ </>
272
+ )}
273
+ </Button>
274
+ ))}
275
+ </div>
276
+ );
277
+ }
278
+
279
+ // Option buttons using Button primitive with option-text variant
280
+ function OptionButtons({
281
+ optionData,
282
+ onChange,
283
+ }: {
284
+ optionData: any;
285
+ onChange: (value: string) => void;
286
+ }) {
287
+ return (
288
+ <div className="flex flex-wrap gap-2">
289
+ {optionData.choices.map((choice: any) => (
290
+ <Button
291
+ key={choice.value}
292
+ variant="option-text"
293
+ selected={choice.selected}
294
+ disabled={choice.disabled}
295
+ onClick={() => onChange(choice.value)}
296
+ aria-pressed={choice.selected}
297
+ role="radio"
298
+ aria-checked={choice.selected}
299
+ >
300
+ {choice.label}
301
+ </Button>
302
+ ))}
303
+ </div>
304
+ );
305
+ }
@@ -0,0 +1,121 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useRef, useState } from 'react';
4
+ import { useRealtimeMockup } from '@snowcone-app/sdk/react';
5
+ import type { WebSocketConfig } from '@snowcone-app/sdk';
6
+
7
+ export interface RealtimeMockupProps {
8
+ config: WebSocketConfig;
9
+ onMockupRendered?: (mockupUrl: string) => void;
10
+ className?: string;
11
+ showStatus?: boolean;
12
+ showLogs?: boolean;
13
+ autoConnect?: boolean;
14
+ }
15
+
16
+ export const RealtimeMockup: React.FC<RealtimeMockupProps> = ({
17
+ config,
18
+ onMockupRendered,
19
+ className = '',
20
+ showStatus = false,
21
+ showLogs = false,
22
+ autoConnect = true
23
+ }) => {
24
+ const [currentMockupUrl, setCurrentMockupUrl] = useState<string>('');
25
+ const configSentRef = useRef(false);
26
+
27
+ const {
28
+ isConnected,
29
+ sessionId,
30
+ isConfigured,
31
+ mockupResults,
32
+ status,
33
+ logs,
34
+ connect,
35
+ disconnect,
36
+ sendConfig,
37
+ clearLogs,
38
+ clearMockups
39
+ } = useRealtimeMockup({
40
+ onMockupRendered: (result) => {
41
+ setCurrentMockupUrl(result.imageUrl);
42
+ onMockupRendered?.(result.imageUrl);
43
+ }
44
+ });
45
+
46
+ useEffect(() => {
47
+ if (autoConnect) {
48
+ connect();
49
+ }
50
+ return () => disconnect();
51
+ }, [autoConnect]);
52
+
53
+ useEffect(() => {
54
+ if (isConnected && sessionId && !configSentRef.current) {
55
+ sendConfig(config);
56
+ configSentRef.current = true;
57
+ }
58
+ }, [isConnected, sessionId, config]);
59
+
60
+ useEffect(() => {
61
+ configSentRef.current = false;
62
+ }, [config.variantId]);
63
+
64
+ return (
65
+ <div className={`realtime-mockup ${className || ""}`}>
66
+ {currentMockupUrl && (
67
+ <div className="mockup-display">
68
+ <img
69
+ src={currentMockupUrl}
70
+ alt="Product mockup"
71
+ crossOrigin="anonymous"
72
+ style={{ maxWidth: '100%', height: 'auto' }}
73
+ />
74
+ </div>
75
+ )}
76
+
77
+ {showStatus && (
78
+ <div className="status-display">
79
+ <p>Status: {status}</p>
80
+ {sessionId && <p>Session: {sessionId}</p>}
81
+ <p>Connected: {isConnected ? 'Yes' : 'No'}</p>
82
+ <p>Configured: {isConfigured ? 'Yes' : 'No'}</p>
83
+ <p>Mockups rendered: {mockupResults.length}</p>
84
+ </div>
85
+ )}
86
+
87
+ {showLogs && (
88
+ <div className="logs-display">
89
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
90
+ <h4>Logs</h4>
91
+ <button onClick={clearLogs}>Clear</button>
92
+ </div>
93
+ <div style={{
94
+ maxHeight: '200px',
95
+ overflowY: 'auto',
96
+ fontSize: '12px',
97
+ fontFamily: 'monospace',
98
+ backgroundColor: 'var(--color-muted, #f5f5f5)',
99
+ padding: '8px',
100
+ borderRadius: '4px'
101
+ }}>
102
+ {logs.map((log, index) => (
103
+ <div key={index}>{log}</div>
104
+ ))}
105
+ </div>
106
+ </div>
107
+ )}
108
+
109
+ {!autoConnect && (
110
+ <div className="controls">
111
+ {!isConnected ? (
112
+ <button onClick={connect}>Connect</button>
113
+ ) : (
114
+ <button onClick={disconnect}>Disconnect</button>
115
+ )}
116
+ <button onClick={clearMockups}>Clear Mockups</button>
117
+ </div>
118
+ )}
119
+ </div>
120
+ );
121
+ };