@snowcone-app/ui 0.1.42 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +18 -4
- package/package.json +9 -5
- package/src/components/CanvasIsolationBoundary.tsx +202 -0
- package/src/components/LoadingOverlayPrism.tsx +251 -0
- package/src/composed/AddToCart.tsx +229 -0
- package/src/composed/ArtAlignment.tsx +703 -0
- package/src/composed/ArtSelector.tsx +290 -0
- package/src/composed/ArtworkCustomizer.tsx +212 -0
- package/src/composed/CanvasEditor.tsx +79 -0
- package/src/composed/ColorPicker.tsx +111 -0
- package/src/composed/CurrentSelectionDisplay.tsx +86 -0
- package/src/composed/HeroProductImage.tsx +1071 -0
- package/src/composed/Lightbox.index.ts +2 -0
- package/src/composed/Lightbox.tsx +230 -0
- package/src/composed/PlacementClipShapeSelector.tsx +88 -0
- package/src/composed/PlacementTabs.tsx +179 -0
- package/src/composed/ProductCard.tsx +298 -0
- package/src/composed/ProductGallery.tsx +54 -0
- package/src/composed/ProductImage.tsx +129 -0
- package/src/composed/ProductList.tsx +147 -0
- package/src/composed/ProductOptions.tsx +305 -0
- package/src/composed/RealtimeMockup.tsx +121 -0
- package/src/composed/TileCount.tsx +348 -0
- package/src/composed/carousels/HeroCarousel.tsx +240 -0
- package/src/composed/carousels/MobileProductCarousel.tsx +1002 -0
- package/src/composed/carousels/index.ts +11 -0
- package/src/composed/carousels/types.ts +58 -0
- package/src/composed/grids/MasonryGrid.tsx +238 -0
- package/src/composed/grids/index.ts +9 -0
- package/src/composed/search/CurrentRefinements.tsx +80 -0
- package/src/composed/search/Filters.tsx +49 -0
- package/src/composed/search/FiltersButton.tsx +57 -0
- package/src/composed/search/FiltersDrawer.tsx +375 -0
- package/src/composed/search/ProductGrid.tsx +118 -0
- package/src/composed/search/ProductHit.tsx +56 -0
- package/src/composed/search/SearchBox.tsx +109 -0
- package/src/composed/search/SearchProvider.tsx +136 -0
- package/src/composed/search/facetConfig.ts +16 -0
- package/src/composed/search/index.ts +22 -0
- package/src/composed/search/meilisearchAdapter.ts +20 -0
- package/src/composed/search/types.ts +22 -0
- package/src/composed/zoom/EnhancedImageViewer.tsx +505 -0
- package/src/composed/zoom/ResponsiveZoom.tsx +134 -0
- package/src/composed/zoom/ZoomOverlay.tsx +194 -0
- package/src/composed/zoom/index.ts +12 -0
- package/src/composed/zoom/types.ts +12 -0
- package/src/design-system/ColorPalette.tsx +126 -0
- package/src/design-system/ColorSwatch.tsx +49 -0
- package/src/design-system/DesignSystemPage.tsx +130 -0
- package/src/design-system/ThemeSwitcher.tsx +181 -0
- package/src/design-system/TypographyScale.tsx +106 -0
- package/src/design-system/index.ts +5 -0
- package/src/ecommerce/stories/HeroProductImage.stories.tsx +66 -0
- package/src/ecommerce/stories/PDPHeroGallery.stories.tsx +105 -0
- package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +472 -0
- package/src/ecommerce/stories/PDPLayout.stories.tsx +365 -0
- package/src/hooks/useBrand.ts +41 -0
- package/src/hooks/useCanvasContext.ts +127 -0
- package/src/hooks/useDeviceDetection.ts +64 -0
- package/src/hooks/useFocusTrap.ts +70 -0
- package/src/hooks/useImagePreloader.ts +268 -0
- package/src/hooks/useImageTransition.ts +608 -0
- package/src/hooks/usePlacementsProcessor.ts +74 -0
- package/src/hooks/useProductGallery.ts +193 -0
- package/src/hooks/useProductPage.ts +467 -0
- package/src/hooks/useRenderGuard.ts +96 -0
- package/src/hooks/useScrollDirection.ts +196 -0
- package/src/hooks/viewport/index.ts +25 -0
- package/src/hooks/viewport/useContainerWidth.ts +59 -0
- package/src/hooks/viewport/useMediaQuery.ts +52 -0
- package/src/hooks/viewport/useResponsiveImageCap.ts +149 -0
- package/src/hooks/viewport/useViewportDimensions.ts +135 -0
- package/src/hooks/viewport/useWideMonitorMode.ts +150 -0
- package/src/hooks/visibility/index.ts +15 -0
- package/src/hooks/visibility/observerPool.ts +150 -0
- package/src/index.ts +240 -0
- package/src/layouts/hero-zoom/HeroShrinkLayout.tsx +209 -0
- package/src/layouts/hero-zoom/HeroZoomLayout.tsx +351 -0
- package/src/layouts/hero-zoom/index.ts +30 -0
- package/src/layouts/hero-zoom/stories/HeroZoomLayout.stories.tsx +350 -0
- package/src/layouts/hero-zoom/types.ts +113 -0
- package/src/layouts/hero-zoom/useHeroZoomScales.ts +156 -0
- package/src/layouts/index.ts +9 -0
- package/src/layouts/pdp/EdgeBlurBox.tsx +210 -0
- package/src/layouts/pdp/ImageBlurExtension.tsx +215 -0
- package/src/layouts/pdp/ImageEdgeBlur.tsx +215 -0
- package/src/layouts/pdp/PDPLayout.tsx +246 -0
- package/src/layouts/pdp/SimpleImageBlur.tsx +140 -0
- package/src/layouts/pdp/index.ts +40 -0
- package/src/lib/env.ts +15 -0
- package/src/lib/locale.ts +167 -0
- package/src/lib/router.tsx +46 -0
- package/src/lib/utils.ts +6 -0
- package/src/lightbox/README.md +77 -0
- package/src/next/index.tsx +26 -0
- package/src/patterns/MockupPriorityProvider.tsx +1014 -0
- package/src/patterns/Product.tsx +850 -0
- package/src/patterns/ProductPageProvider.tsx +224 -0
- package/src/patterns/RealtimeProvider.tsx +1162 -0
- package/src/patterns/ShopProvider.tsx +603 -0
- package/src/personalization/PersonalizationBridge.tsx +235 -0
- package/src/personalization/PersonalizationContext.ts +29 -0
- package/src/personalization/PersonalizationInputs.tsx +110 -0
- package/src/personalization/PersonalizationProvider.tsx +407 -0
- package/src/personalization/canvas-stub.d.ts +22 -0
- package/src/personalization/index.ts +43 -0
- package/src/personalization/types.ts +48 -0
- package/src/personalization/usePersonalization.ts +32 -0
- package/src/personalization/usePersonalizationShimmer.ts +159 -0
- package/src/personalization/utils.ts +59 -0
- package/src/primitives/BrandLogo.tsx +65 -0
- package/src/primitives/BrandName.tsx +51 -0
- package/src/primitives/Button.tsx +123 -0
- package/src/primitives/ColorSwatch.tsx +221 -0
- package/src/primitives/DragHintAnimation.tsx +190 -0
- package/src/primitives/EdgeSwipeGuards.tsx +60 -0
- package/src/primitives/FloatingActionGroup.tsx +176 -0
- package/src/primitives/ProductPrice.tsx +171 -0
- package/src/primitives/ProgressiveBlur.tsx +295 -0
- package/src/primitives/ThemeToggle.tsx +125 -0
- package/src/primitives/__tests__/story-coverage.test.ts +98 -0
- package/src/primitives/accordion.tsx +280 -0
- package/src/primitives/badge.tsx +137 -0
- package/src/primitives/card.tsx +61 -0
- package/src/primitives/checkbox.tsx +56 -0
- package/src/primitives/collapsible.tsx +51 -0
- package/src/primitives/drawer.tsx +828 -0
- package/src/primitives/dropdown-menu.tsx +197 -0
- package/src/primitives/fieldset.tsx +73 -0
- package/src/primitives/index.ts +138 -0
- package/src/primitives/input.tsx +91 -0
- package/src/primitives/kbd.tsx +130 -0
- package/src/primitives/label.tsx +20 -0
- package/src/primitives/link.tsx +182 -0
- package/src/primitives/popover.tsx +80 -0
- package/src/primitives/radio-group.tsx +79 -0
- package/src/primitives/scroll-fade.tsx +159 -0
- package/src/primitives/select.tsx +170 -0
- package/src/primitives/separator.tsx +25 -0
- package/src/primitives/slider.tsx +221 -0
- package/src/primitives/spinner.tsx +72 -0
- package/src/primitives/stories/Accordion.stories.tsx +121 -0
- package/src/primitives/stories/Badge.stories.tsx +221 -0
- package/src/primitives/stories/Button.stories.tsx +185 -0
- package/src/primitives/stories/Card.stories.tsx +171 -0
- package/src/primitives/stories/Checkbox.stories.tsx +214 -0
- package/src/primitives/stories/Collapsible.stories.tsx +230 -0
- package/src/primitives/stories/Drawer.stories.tsx +378 -0
- package/src/primitives/stories/DropdownMenu.stories.tsx +182 -0
- package/src/primitives/stories/Fieldset.stories.tsx +212 -0
- package/src/primitives/stories/Input.stories.tsx +172 -0
- package/src/primitives/stories/Kbd.stories.tsx +183 -0
- package/src/primitives/stories/Label.stories.tsx +98 -0
- package/src/primitives/stories/Link.stories.tsx +260 -0
- package/src/primitives/stories/Popover.stories.tsx +178 -0
- package/src/primitives/stories/RadioGroup.stories.tsx +205 -0
- package/src/primitives/stories/Select.stories.tsx +222 -0
- package/src/primitives/stories/Separator.stories.tsx +134 -0
- package/src/primitives/stories/Slider.stories.tsx +203 -0
- package/src/primitives/stories/Spinner.stories.tsx +142 -0
- package/src/primitives/stories/Surface.stories.tsx +257 -0
- package/src/primitives/stories/Switch.stories.tsx +131 -0
- package/src/primitives/stories/Tabs.stories.tsx +275 -0
- package/src/primitives/stories/TextField.stories.tsx +139 -0
- package/src/primitives/stories/Textarea.stories.tsx +148 -0
- package/src/primitives/stories/Tooltip.stories.tsx +119 -0
- package/src/primitives/surface.tsx +86 -0
- package/src/primitives/switch.tsx +35 -0
- package/src/primitives/tabs.tsx +206 -0
- package/src/primitives/text-field.tsx +84 -0
- package/src/primitives/textarea.tsx +50 -0
- package/src/primitives/tooltip.tsx +58 -0
- package/src/services/CanvasExportService.ts +518 -0
- package/src/styles/base.css +380 -0
- package/src/styles/defaults.css +280 -0
- package/src/styles/globals.css +1242 -0
- package/src/styles/index.css +17 -0
- package/src/styles/ne-themes.css +4740 -0
- package/src/styles/tailwind.css +11 -0
- package/src/styles/tokens.css +117 -0
- package/src/styles/utilities.css +188 -0
- package/src/themes/apply-theme.ts +449 -0
- package/src/themes/getThemeStyles.ts +454 -0
- package/src/themes/index.ts +48 -0
- package/src/themes/oklch-theme.ts +283 -0
- package/src/themes/presets.ts +989 -0
- package/src/themes/types.ts +386 -0
- package/src/themes/useTheme.tsx +450 -0
- package/src/utils/dev-warnings.ts +161 -0
- package/src/utils/devWarnings.ts +153 -0
- package/dist/styles.css +0 -1
|
@@ -0,0 +1,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
|
+
};
|