@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,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
+ }