@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,235 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PersonalizationBridge — rendered inside SnowconeCanvas overlay (EditorProvider context).
|
|
5
|
+
* Syncs external personalization values into canvas elements.
|
|
6
|
+
*
|
|
7
|
+
* INTERNAL: Not exported from the package. Used by PersonalizationProvider only.
|
|
8
|
+
*
|
|
9
|
+
* Text fields: finds elements by name, calls setText().
|
|
10
|
+
* Color fields: finds all elements matching the original color, updates them.
|
|
11
|
+
* Image fields: uses useImageBinding to update image by name.
|
|
12
|
+
*
|
|
13
|
+
* Key design: effects depend ONLY on the input value (text string / hex color).
|
|
14
|
+
* Callbacks and element arrays are accessed via refs to avoid effect churn from
|
|
15
|
+
* the canvas re-rendering after each update.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useRef, useEffect } from "react";
|
|
19
|
+
import { useEditor, useCommands, useImageBinding } from "@snowcone-app/canvas";
|
|
20
|
+
import type { ImageFitMode } from "@snowcone-app/canvas";
|
|
21
|
+
import type {
|
|
22
|
+
PersonalizationField,
|
|
23
|
+
PersonalizationValues,
|
|
24
|
+
} from "./types";
|
|
25
|
+
import { normalizeHex, isValidImageUrl, useDebouncedValue } from "./utils";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// TextBinder — syncs a single text personalization value into canvas
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function TextBinder({ name, value }: { name: string; value: string }) {
|
|
32
|
+
const { elements } = useEditor();
|
|
33
|
+
const { executeElementUpdate } = useCommands();
|
|
34
|
+
|
|
35
|
+
const elementsRef = useRef(elements);
|
|
36
|
+
elementsRef.current = elements;
|
|
37
|
+
const executeRef = useRef(executeElementUpdate);
|
|
38
|
+
executeRef.current = executeElementUpdate;
|
|
39
|
+
|
|
40
|
+
const hasUserInput = useRef(false);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (value == null) return;
|
|
44
|
+
// Skip initial empty value to preserve the designer's original text
|
|
45
|
+
if (value === "" && !hasUserInput.current) return;
|
|
46
|
+
hasUserInput.current = true;
|
|
47
|
+
|
|
48
|
+
const currentElements = elementsRef.current;
|
|
49
|
+
const exec = executeRef.current;
|
|
50
|
+
|
|
51
|
+
for (const el of currentElements) {
|
|
52
|
+
if (el.name !== name) continue;
|
|
53
|
+
if (!("setText" in el) || typeof (el as any).setText !== "function")
|
|
54
|
+
continue;
|
|
55
|
+
const cloned = el.clone();
|
|
56
|
+
(cloned as any).setText(value);
|
|
57
|
+
exec(el, cloned);
|
|
58
|
+
}
|
|
59
|
+
}, [value, name, elements.length]); // Re-run when elements become available
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// ColorBinder — updates all elements matching originalColor to newColor
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
function ColorBinder({
|
|
69
|
+
originalColor,
|
|
70
|
+
newColor,
|
|
71
|
+
}: {
|
|
72
|
+
originalColor: string;
|
|
73
|
+
newColor: string;
|
|
74
|
+
}) {
|
|
75
|
+
const { elements } = useEditor();
|
|
76
|
+
const { executeElementUpdate } = useCommands();
|
|
77
|
+
|
|
78
|
+
const elementsRef = useRef(elements);
|
|
79
|
+
elementsRef.current = elements;
|
|
80
|
+
const executeRef = useRef(executeElementUpdate);
|
|
81
|
+
executeRef.current = executeElementUpdate;
|
|
82
|
+
|
|
83
|
+
const appliedColorRef = useRef(normalizeHex(originalColor));
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const normalizedNew = normalizeHex(newColor);
|
|
87
|
+
if (normalizedNew === appliedColorRef.current) return;
|
|
88
|
+
|
|
89
|
+
const matchColor = appliedColorRef.current;
|
|
90
|
+
const currentElements = elementsRef.current;
|
|
91
|
+
const exec = executeRef.current;
|
|
92
|
+
|
|
93
|
+
for (const el of currentElements) {
|
|
94
|
+
let needsUpdate = false;
|
|
95
|
+
const cloned = el.clone();
|
|
96
|
+
|
|
97
|
+
// Text elements: check .color
|
|
98
|
+
if ("color" in el && typeof (el as any).color === "string") {
|
|
99
|
+
if (normalizeHex((el as any).color) === matchColor) {
|
|
100
|
+
if (typeof (cloned as any).setColor === "function") {
|
|
101
|
+
(cloned as any).setColor(newColor);
|
|
102
|
+
} else {
|
|
103
|
+
(cloned as any).color = newColor;
|
|
104
|
+
}
|
|
105
|
+
needsUpdate = true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Shape/Path elements: check transformData.fillColor
|
|
110
|
+
if (
|
|
111
|
+
normalizeHex((el as any).transformData?.fillColor ?? "") === matchColor
|
|
112
|
+
) {
|
|
113
|
+
(cloned as any).transformData = {
|
|
114
|
+
...(cloned as any).transformData,
|
|
115
|
+
fillColor: newColor,
|
|
116
|
+
};
|
|
117
|
+
needsUpdate = true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Path elements: check transformData.strokeColor
|
|
121
|
+
if (
|
|
122
|
+
normalizeHex((el as any).transformData?.strokeColor ?? "") ===
|
|
123
|
+
matchColor
|
|
124
|
+
) {
|
|
125
|
+
(cloned as any).transformData = {
|
|
126
|
+
...(cloned as any).transformData,
|
|
127
|
+
strokeColor: newColor,
|
|
128
|
+
};
|
|
129
|
+
needsUpdate = true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Stroke color
|
|
133
|
+
if (
|
|
134
|
+
(el as any).stroke?.enabled &&
|
|
135
|
+
normalizeHex((el as any).stroke?.color ?? "") === matchColor
|
|
136
|
+
) {
|
|
137
|
+
(cloned as any).stroke = {
|
|
138
|
+
...(cloned as any).stroke,
|
|
139
|
+
color: newColor,
|
|
140
|
+
};
|
|
141
|
+
needsUpdate = true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (needsUpdate) {
|
|
145
|
+
exec(el, cloned);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
appliedColorRef.current = normalizedNew;
|
|
150
|
+
}, [newColor, elements.length]); // Re-run when elements become available
|
|
151
|
+
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// ImageBinder — uses useImageBinding hook from @snowcone-app/canvas
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
function ImageBinder({
|
|
160
|
+
name,
|
|
161
|
+
value,
|
|
162
|
+
fit = "cover",
|
|
163
|
+
}: {
|
|
164
|
+
name: string;
|
|
165
|
+
value: string;
|
|
166
|
+
fit?: "cover" | "contain";
|
|
167
|
+
}) {
|
|
168
|
+
const { setImageUrl } = useImageBinding(name, {
|
|
169
|
+
fit: fit as ImageFitMode,
|
|
170
|
+
});
|
|
171
|
+
const hasUserInput = useRef(false);
|
|
172
|
+
const debouncedValue = useDebouncedValue(value, 500);
|
|
173
|
+
|
|
174
|
+
const setImageUrlRef = useRef(setImageUrl);
|
|
175
|
+
setImageUrlRef.current = setImageUrl;
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (debouncedValue == null) return;
|
|
179
|
+
if (debouncedValue === "" && !hasUserInput.current) return;
|
|
180
|
+
hasUserInput.current = true;
|
|
181
|
+
if (debouncedValue !== "" && !isValidImageUrl(debouncedValue)) return;
|
|
182
|
+
|
|
183
|
+
setImageUrlRef.current(debouncedValue);
|
|
184
|
+
}, [debouncedValue]);
|
|
185
|
+
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// PersonalizationBridge — maps personalization fields to binder components
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
export function PersonalizationBridge({
|
|
194
|
+
fields,
|
|
195
|
+
values,
|
|
196
|
+
}: {
|
|
197
|
+
fields: PersonalizationField[];
|
|
198
|
+
values: PersonalizationValues;
|
|
199
|
+
}) {
|
|
200
|
+
return (
|
|
201
|
+
<>
|
|
202
|
+
{fields.map((field) => {
|
|
203
|
+
if (field.type === "text") {
|
|
204
|
+
return (
|
|
205
|
+
<TextBinder
|
|
206
|
+
key={`text-${field.name}`}
|
|
207
|
+
name={field.name}
|
|
208
|
+
value={values[field.name] ?? ""}
|
|
209
|
+
/>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (field.type === "color") {
|
|
213
|
+
return (
|
|
214
|
+
<ColorBinder
|
|
215
|
+
key={`color-${field.color}`}
|
|
216
|
+
originalColor={field.color}
|
|
217
|
+
newColor={values[field.color] ?? field.color}
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (field.type === "image") {
|
|
222
|
+
return (
|
|
223
|
+
<ImageBinder
|
|
224
|
+
key={`image-${field.name}`}
|
|
225
|
+
name={field.name}
|
|
226
|
+
value={values[field.name] ?? ""}
|
|
227
|
+
fit={field.fit ?? "cover"}
|
|
228
|
+
/>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
})}
|
|
233
|
+
</>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
import type { PersonalizationField, PersonalizationValues } from "./types";
|
|
5
|
+
|
|
6
|
+
export interface PersonalizationContextValue {
|
|
7
|
+
/** Personalization field definitions */
|
|
8
|
+
fields: PersonalizationField[];
|
|
9
|
+
/** Current buyer input values */
|
|
10
|
+
personValues: PersonalizationValues;
|
|
11
|
+
/** Update a field value (text by name, color by original hex) */
|
|
12
|
+
updateField: (key: string, value: string) => void;
|
|
13
|
+
/** Whether the buyer has interacted with any field */
|
|
14
|
+
isActive: boolean;
|
|
15
|
+
/** Whether a canvas export is in progress */
|
|
16
|
+
isExporting: boolean;
|
|
17
|
+
/** Latest exported blob URLs keyed by artboard/placement name */
|
|
18
|
+
exportedBlobUrls: Record<string, string>;
|
|
19
|
+
/** Increments each time a canvas export completes (blob ready to send) */
|
|
20
|
+
exportCount: number;
|
|
21
|
+
/** Reset all values to initial state */
|
|
22
|
+
reset: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const PersonalizationContext = createContext<PersonalizationContextValue | null>(null);
|
|
26
|
+
|
|
27
|
+
export function usePersonalizationContext(): PersonalizationContextValue | null {
|
|
28
|
+
return useContext(PersonalizationContext);
|
|
29
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PersonalizationInputs — auto-renders input controls from personalization field config.
|
|
5
|
+
*
|
|
6
|
+
* Consumes usePersonalization() internally — no prop drilling needed.
|
|
7
|
+
* Renders text inputs, color pickers, and image URL inputs based on field type.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <PersonalizationProvider fields={fields} canvasElements={elements}>
|
|
12
|
+
* <PersonalizationInputs className="space-y-3" />
|
|
13
|
+
* </PersonalizationProvider>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React from "react";
|
|
18
|
+
import { usePersonalization } from "./usePersonalization";
|
|
19
|
+
import { scrollInputAboveKeyboard } from "./utils";
|
|
20
|
+
import type { PersonalizationField } from "./types";
|
|
21
|
+
|
|
22
|
+
interface PersonalizationInputsProps {
|
|
23
|
+
/** CSS class for the container */
|
|
24
|
+
className?: string;
|
|
25
|
+
/** CSS class for each input */
|
|
26
|
+
inputClassName?: string;
|
|
27
|
+
/** Called when any input receives focus (for keyboard handling) */
|
|
28
|
+
onInputFocus?: () => void;
|
|
29
|
+
/** Called when any input loses focus */
|
|
30
|
+
onInputBlur?: () => void;
|
|
31
|
+
/** Enable iOS keyboard scroll behavior (default: true) */
|
|
32
|
+
autoScrollOnFocus?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function PersonalizationInputs({
|
|
36
|
+
className,
|
|
37
|
+
inputClassName = "w-full px-4 py-3 rounded-lg border text-base text-foreground placeholder:text-muted-foreground bg-white/50 outline-none focus:ring-2 focus:ring-foreground/20",
|
|
38
|
+
onInputFocus,
|
|
39
|
+
onInputBlur,
|
|
40
|
+
autoScrollOnFocus = true,
|
|
41
|
+
}: PersonalizationInputsProps) {
|
|
42
|
+
const { fields, personValues, updateField } = usePersonalization();
|
|
43
|
+
|
|
44
|
+
const textFields = fields.filter(
|
|
45
|
+
(f): f is Extract<PersonalizationField, { type: "text" }> =>
|
|
46
|
+
f.type === "text"
|
|
47
|
+
);
|
|
48
|
+
const colorFields = fields.filter(
|
|
49
|
+
(f): f is Extract<PersonalizationField, { type: "color" }> =>
|
|
50
|
+
f.type === "color"
|
|
51
|
+
);
|
|
52
|
+
const imageFields = fields.filter(
|
|
53
|
+
(f): f is Extract<PersonalizationField, { type: "image" }> =>
|
|
54
|
+
f.type === "image"
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
58
|
+
onInputFocus?.();
|
|
59
|
+
if (autoScrollOnFocus) {
|
|
60
|
+
scrollInputAboveKeyboard(e.target as HTMLElement);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className={className || "flex flex-col gap-3"}>
|
|
66
|
+
{/* Text fields */}
|
|
67
|
+
{textFields.map((field) => (
|
|
68
|
+
<input
|
|
69
|
+
key={`text-${field.name}`}
|
|
70
|
+
type="text"
|
|
71
|
+
placeholder={field.placeholder || field.label}
|
|
72
|
+
value={personValues[field.name] ?? ""}
|
|
73
|
+
onChange={(e) => updateField(field.name, e.target.value)}
|
|
74
|
+
onFocus={handleFocus}
|
|
75
|
+
onBlur={onInputBlur}
|
|
76
|
+
className={inputClassName}
|
|
77
|
+
style={{ borderColor: "rgba(0,0,0,0.15)" }}
|
|
78
|
+
/>
|
|
79
|
+
))}
|
|
80
|
+
|
|
81
|
+
{/* Color fields */}
|
|
82
|
+
{colorFields.map((field) => (
|
|
83
|
+
<div key={`color-${field.color}`} className="flex items-center gap-3">
|
|
84
|
+
<input
|
|
85
|
+
type="color"
|
|
86
|
+
defaultValue={field.color}
|
|
87
|
+
className="w-10 h-10 rounded-lg border-0 cursor-pointer"
|
|
88
|
+
onChange={(e) => updateField(field.color, e.target.value)}
|
|
89
|
+
/>
|
|
90
|
+
<span className="text-base text-muted-foreground">{field.label}</span>
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
|
|
94
|
+
{/* Image fields */}
|
|
95
|
+
{imageFields.map((field) => (
|
|
96
|
+
<input
|
|
97
|
+
key={`image-${field.name}`}
|
|
98
|
+
type="text"
|
|
99
|
+
placeholder={field.placeholder || `${field.label} URL`}
|
|
100
|
+
value={personValues[field.name] ?? ""}
|
|
101
|
+
onChange={(e) => updateField(field.name, e.target.value)}
|
|
102
|
+
onFocus={handleFocus}
|
|
103
|
+
onBlur={onInputBlur}
|
|
104
|
+
className={inputClassName}
|
|
105
|
+
style={{ borderColor: "rgba(0,0,0,0.15)" }}
|
|
106
|
+
/>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|