@growgroup/visual-editor 0.1.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 (219) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +52 -0
  3. package/README.md +143 -0
  4. package/dist/editor.css +2 -0
  5. package/package.json +94 -0
  6. package/src/components/DeckSlideRender.tsx +21 -0
  7. package/src/components/SaveNote.tsx +21 -0
  8. package/src/components/auth/AuthProvider.tsx +44 -0
  9. package/src/components/ui/accordion.tsx +57 -0
  10. package/src/components/ui/alert-dialog.tsx +141 -0
  11. package/src/components/ui/badge.tsx +36 -0
  12. package/src/components/ui/button.tsx +56 -0
  13. package/src/components/ui/card.tsx +76 -0
  14. package/src/components/ui/checkbox.tsx +30 -0
  15. package/src/components/ui/collapsible.tsx +11 -0
  16. package/src/components/ui/context-menu.tsx +200 -0
  17. package/src/components/ui/dialog.tsx +122 -0
  18. package/src/components/ui/dropdown-menu.tsx +201 -0
  19. package/src/components/ui/input.tsx +22 -0
  20. package/src/components/ui/label.tsx +26 -0
  21. package/src/components/ui/popover.tsx +33 -0
  22. package/src/components/ui/scroll-area.tsx +48 -0
  23. package/src/components/ui/scrubbable-label.tsx +95 -0
  24. package/src/components/ui/select.tsx +159 -0
  25. package/src/components/ui/separator.tsx +31 -0
  26. package/src/components/ui/slider.tsx +28 -0
  27. package/src/components/ui/switch.tsx +29 -0
  28. package/src/components/ui/tabs.tsx +55 -0
  29. package/src/components/ui/textarea.tsx +22 -0
  30. package/src/components/ui/tooltip.tsx +32 -0
  31. package/src/components/viewer/useDeck.ts +58 -0
  32. package/src/editor/EditorContext.tsx +632 -0
  33. package/src/editor/EditorToolbar.tsx +446 -0
  34. package/src/editor/FrontendVisualEditor.tsx +2595 -0
  35. package/src/editor/autosave.ts +34 -0
  36. package/src/editor/components/AiPromptPopover.tsx +378 -0
  37. package/src/editor/components/BreakpointGuides.tsx +323 -0
  38. package/src/editor/components/BreakpointSelector.tsx +180 -0
  39. package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
  40. package/src/editor/components/CssEditorDialog.tsx +168 -0
  41. package/src/editor/components/EditorCanvas.tsx +987 -0
  42. package/src/editor/components/EditorContextMenu.tsx +515 -0
  43. package/src/editor/components/EditorFooter.tsx +81 -0
  44. package/src/editor/components/EditorHeader.tsx +524 -0
  45. package/src/editor/components/EditorLayerPanel.tsx +1197 -0
  46. package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
  47. package/src/editor/components/EditorSidebar.tsx +226 -0
  48. package/src/editor/components/FigmaColorPicker.tsx +1577 -0
  49. package/src/editor/components/HtmlEditorDialog.tsx +71 -0
  50. package/src/editor/components/HtmlImportDialog.tsx +346 -0
  51. package/src/editor/components/JsEditorDialog.tsx +175 -0
  52. package/src/editor/components/LayerTreeItem.tsx +494 -0
  53. package/src/editor/components/MasterComponentEditor.tsx +846 -0
  54. package/src/editor/components/MediaLibraryDialog.tsx +447 -0
  55. package/src/editor/components/PageSettingsDialog.tsx +776 -0
  56. package/src/editor/components/VariablePicker.tsx +355 -0
  57. package/src/editor/components/VariablesDialog.tsx +564 -0
  58. package/src/editor/components/VariantEditor.tsx +687 -0
  59. package/src/editor/components/VariantMatrix.tsx +507 -0
  60. package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
  61. package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
  62. package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
  63. package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
  64. package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
  65. package/src/editor/components/component-panel/VariantList.tsx +260 -0
  66. package/src/editor/components/component-panel/index.ts +12 -0
  67. package/src/editor/components/index.ts +48 -0
  68. package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
  69. package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
  70. package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
  71. package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
  72. package/src/editor/components/multi-page/PageLabel.tsx +34 -0
  73. package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
  74. package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
  75. package/src/editor/components/multi-page/index.ts +7 -0
  76. package/src/editor/components/ppt/PptChrome.tsx +2508 -0
  77. package/src/editor/components/ppt/PptComments.tsx +589 -0
  78. package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
  79. package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
  80. package/src/editor/components/ppt/PptNotes.tsx +246 -0
  81. package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
  82. package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
  83. package/src/editor/components/property-panel/BorderSection.tsx +104 -0
  84. package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
  85. package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
  86. package/src/editor/components/property-panel/EffectSection.tsx +157 -0
  87. package/src/editor/components/property-panel/FillSection.tsx +69 -0
  88. package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
  89. package/src/editor/components/property-panel/ImageSection.tsx +206 -0
  90. package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
  91. package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
  92. package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
  93. package/src/editor/components/property-panel/LinkSection.tsx +103 -0
  94. package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
  95. package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
  96. package/src/editor/components/property-panel/TypographySection.tsx +230 -0
  97. package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
  98. package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
  99. package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
  100. package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
  101. package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
  102. package/src/editor/components/property-panel/index.ts +24 -0
  103. package/src/editor/components/property-panel/unit-utils.ts +261 -0
  104. package/src/editor/components/shell/EditorTopBar.tsx +392 -0
  105. package/src/editor/components/shell/LeftPanel.tsx +47 -0
  106. package/src/editor/components/shell/PagesPanel.tsx +118 -0
  107. package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
  108. package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
  109. package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
  110. package/src/editor/components/variables-panel/index.ts +7 -0
  111. package/src/editor/constants.ts +468 -0
  112. package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
  113. package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
  114. package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
  115. package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
  116. package/src/editor/contexts/EditorRefsContext.tsx +204 -0
  117. package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
  118. package/src/editor/contexts/EditorToolContext.tsx +58 -0
  119. package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
  120. package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
  121. package/src/editor/contexts/EditorViewContext.tsx +101 -0
  122. package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
  123. package/src/editor/contexts/index.ts +17 -0
  124. package/src/editor/editor-skin.css +122 -0
  125. package/src/editor/hooks/index.ts +38 -0
  126. package/src/editor/hooks/useAiReplace.ts +397 -0
  127. package/src/editor/hooks/useAltMeasure.ts +67 -0
  128. package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
  129. package/src/editor/hooks/useCanvasControls.ts +1222 -0
  130. package/src/editor/hooks/useComponentEditMode.ts +658 -0
  131. package/src/editor/hooks/useComponentInstances.ts +515 -0
  132. package/src/editor/hooks/useContextMenuHandler.ts +157 -0
  133. package/src/editor/hooks/useCoordinateTransform.ts +207 -0
  134. package/src/editor/hooks/useDragResize.ts +1317 -0
  135. package/src/editor/hooks/useDrawingMode.ts +472 -0
  136. package/src/editor/hooks/useEditorColors.ts +144 -0
  137. package/src/editor/hooks/useEditorMessages.ts +108 -0
  138. package/src/editor/hooks/useElementActions.ts +1754 -0
  139. package/src/editor/hooks/useElementSelection.ts +1573 -0
  140. package/src/editor/hooks/useFocusManagement.ts +249 -0
  141. package/src/editor/hooks/useGoogleFonts.ts +305 -0
  142. package/src/editor/hooks/useIframeInitializer.ts +584 -0
  143. package/src/editor/hooks/useIframeSetup.ts +360 -0
  144. package/src/editor/hooks/useImageUpload.ts +270 -0
  145. package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
  146. package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
  147. package/src/editor/hooks/useMarqueeSelection.ts +388 -0
  148. package/src/editor/hooks/useMediaLibrary.ts +129 -0
  149. package/src/editor/hooks/usePageSettingsManager.ts +739 -0
  150. package/src/editor/hooks/useResizablePanel.ts +163 -0
  151. package/src/editor/hooks/useRichPaste.ts +255 -0
  152. package/src/editor/hooks/useTouchGestures.ts +197 -0
  153. package/src/editor/index.ts +27 -0
  154. package/src/editor/types/page-settings.ts +513 -0
  155. package/src/editor/types.ts +382 -0
  156. package/src/editor/utils/align-elements.ts +169 -0
  157. package/src/editor/utils/api-json.ts +21 -0
  158. package/src/editor/utils/aspect-lock.ts +50 -0
  159. package/src/editor/utils/component-renderer.ts +702 -0
  160. package/src/editor/utils/component-sync.ts +845 -0
  161. package/src/editor/utils/crop-mode.ts +265 -0
  162. package/src/editor/utils/dom-utils.ts +2238 -0
  163. package/src/editor/utils/element-effects.ts +218 -0
  164. package/src/editor/utils/eyedropper.ts +35 -0
  165. package/src/editor/utils/figma-export.ts +560 -0
  166. package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
  167. package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
  168. package/src/editor/utils/figma-paste.ts +2262 -0
  169. package/src/editor/utils/flex-reorder.ts +231 -0
  170. package/src/editor/utils/flex-utils.ts +1230 -0
  171. package/src/editor/utils/geometry.ts +232 -0
  172. package/src/editor/utils/grid-layout.ts +210 -0
  173. package/src/editor/utils/html-utils.ts +470 -0
  174. package/src/editor/utils/index.ts +11 -0
  175. package/src/editor/utils/ink-style.ts +57 -0
  176. package/src/editor/utils/inline-format.ts +236 -0
  177. package/src/editor/utils/measure-distance.ts +120 -0
  178. package/src/editor/utils/override-apply.ts +526 -0
  179. package/src/editor/utils/override-detection.ts +613 -0
  180. package/src/editor/utils/paste-processors.ts +609 -0
  181. package/src/editor/utils/paste-sanitizer.ts +244 -0
  182. package/src/editor/utils/restore-flow.ts +188 -0
  183. package/src/editor/utils/shape-library.ts +171 -0
  184. package/src/editor/utils/slide-root.ts +29 -0
  185. package/src/editor/utils/smart-guides.ts +218 -0
  186. package/src/editor/utils/style-utils.ts +685 -0
  187. package/src/editor/utils/table-edit.ts +151 -0
  188. package/src/editor/utils/tailwind-mappings.ts +778 -0
  189. package/src/editor/utils/tailwind-utils.ts +804 -0
  190. package/src/editor/utils/text-highlight.ts +216 -0
  191. package/src/editor/utils/theme-to-variables.ts +341 -0
  192. package/src/editor/utils/variant-resolver.ts +456 -0
  193. package/src/editor/utils/viewport-utils.ts +295 -0
  194. package/src/hooks/useEditorHistory.ts +195 -0
  195. package/src/hooks/useEditorShortcuts.ts +260 -0
  196. package/src/index.ts +40 -0
  197. package/src/io.ts +134 -0
  198. package/src/lib/agent/slide-agent/types.ts +547 -0
  199. package/src/lib/agent/website-agent/types.ts +805 -0
  200. package/src/lib/api/auth-fetch.ts +58 -0
  201. package/src/lib/deck.ts +69 -0
  202. package/src/lib/export.ts +47 -0
  203. package/src/lib/firebase/config.ts +29 -0
  204. package/src/lib/firebase/css-variables.ts +378 -0
  205. package/src/lib/firebase/editor-components.ts +677 -0
  206. package/src/lib/firebase/storage.ts +469 -0
  207. package/src/lib/utils.ts +6 -0
  208. package/src/styles/editor.css +52 -0
  209. package/src/styles/skin.css +122 -0
  210. package/src/types/css-variables.ts +645 -0
  211. package/src/types/editor-components.ts +944 -0
  212. package/src/types/editor.ts +420 -0
  213. package/src/types/page-master.ts +197 -0
  214. package/src/types/page.ts +419 -0
  215. package/src/types/slide.ts +402 -0
  216. package/src/types/website-theme.ts +788 -0
  217. package/src/types/website.ts +374 -0
  218. package/src/vendor/firebase-firestore.ts +13 -0
  219. package/src/vendor/firebase-functions.ts +17 -0
@@ -0,0 +1,687 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * VariantEditor - Dialog for editing variant properties of a master component
5
+ *
6
+ * Features:
7
+ * - Edit variant properties (add/remove property values like size: sm/md/lg)
8
+ * - View variant matrix showing all combinations
9
+ * - Set default variant
10
+ * - Create missing variants
11
+ * - Delete unused variants
12
+ *
13
+ * Matches dark theme styling consistent with the editor.
14
+ */
15
+
16
+ import React, { useState, useCallback, useMemo, useId } from 'react';
17
+ import {
18
+ Dialog,
19
+ DialogContent,
20
+ DialogHeader,
21
+ DialogTitle,
22
+ DialogDescription,
23
+ } from '../../components/ui/dialog';
24
+ import { Button } from '../../components/ui/button';
25
+ import { Input } from '../../components/ui/input';
26
+ import { Label } from '../../components/ui/label';
27
+ import { ScrollArea } from '../../components/ui/scroll-area';
28
+ import {
29
+ Accordion,
30
+ AccordionContent,
31
+ AccordionItem,
32
+ AccordionTrigger,
33
+ } from '../../components/ui/accordion';
34
+ import {
35
+ AlertDialog,
36
+ AlertDialogAction,
37
+ AlertDialogCancel,
38
+ AlertDialogContent,
39
+ AlertDialogDescription,
40
+ AlertDialogFooter,
41
+ AlertDialogHeader,
42
+ AlertDialogTitle,
43
+ } from '../../components/ui/alert-dialog';
44
+ import { Badge } from '../../components/ui/badge';
45
+ import {
46
+ Layers,
47
+ Plus,
48
+ Trash2,
49
+ Save,
50
+ X,
51
+ Check,
52
+ AlertTriangle,
53
+ Grid3x3,
54
+ Settings2,
55
+ Star,
56
+ } from 'lucide-react';
57
+ import { cn } from '../../lib/utils';
58
+ import type {
59
+ MasterComponent,
60
+ VariantProperty,
61
+ VariantDefinition,
62
+ } from '../../types/editor-components';
63
+ import { VariantMatrix, VariantMatrixStats } from './VariantMatrix';
64
+ import {
65
+ generateVariantCombinations,
66
+ createVariantKey,
67
+ generateVariantName,
68
+ createVariantMap,
69
+ } from '../utils/variant-resolver';
70
+
71
+ // =============================================================================
72
+ // Types
73
+ // =============================================================================
74
+
75
+ interface VariantEditorProps {
76
+ /** The master component being edited */
77
+ master: MasterComponent;
78
+ /** Whether the dialog is open */
79
+ open: boolean;
80
+ /** Callback when open state changes */
81
+ onOpenChange: (open: boolean) => void;
82
+ /** Callback when changes are saved */
83
+ onSave: (updates: {
84
+ variantProperties: VariantProperty[];
85
+ variants: VariantDefinition[];
86
+ defaultVariantId: string;
87
+ }) => void;
88
+ }
89
+
90
+ interface EditableVariantProperty extends VariantProperty {
91
+ /** Flag for newly added properties */
92
+ isNew?: boolean;
93
+ }
94
+
95
+ // =============================================================================
96
+ // Main Component
97
+ // =============================================================================
98
+
99
+ /**
100
+ * VariantEditor provides a comprehensive interface for managing
101
+ * component variants and their properties.
102
+ */
103
+ export function VariantEditor({
104
+ master,
105
+ open,
106
+ onOpenChange,
107
+ onSave,
108
+ }: VariantEditorProps) {
109
+ const dialogId = useId();
110
+
111
+ // Local state for editing
112
+ const [properties, setProperties] = useState<EditableVariantProperty[]>(() =>
113
+ (master.variantProperties ?? []).map((p) => ({ ...p }))
114
+ );
115
+ const [variants, setVariants] = useState<VariantDefinition[]>(() =>
116
+ [...master.variants]
117
+ );
118
+ const [defaultVariantId, setDefaultVariantId] = useState<string>(
119
+ master.defaultVariantId
120
+ );
121
+ const [selectedVariantId, setSelectedVariantId] = useState<string | null>(
122
+ master.defaultVariantId
123
+ );
124
+
125
+ // UI state
126
+ const [activeSection, setActiveSection] = useState<string>('properties');
127
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(null);
128
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
129
+
130
+ // Track changes
131
+ const markChanged = useCallback(() => {
132
+ setHasUnsavedChanges(true);
133
+ }, []);
134
+
135
+ // =============================================================================
136
+ // Property Management
137
+ // =============================================================================
138
+
139
+ const handleAddProperty = useCallback(() => {
140
+ const newId = `prop-${Date.now()}`;
141
+ const newProperty: EditableVariantProperty = {
142
+ id: newId,
143
+ name: '',
144
+ values: ['default'],
145
+ defaultValue: 'default',
146
+ isNew: true,
147
+ };
148
+ setProperties((prev) => [...prev, newProperty]);
149
+ markChanged();
150
+ }, [markChanged]);
151
+
152
+ const handleUpdateProperty = useCallback(
153
+ (propertyId: string, updates: Partial<VariantProperty>) => {
154
+ setProperties((prev) =>
155
+ prev.map((p) => (p.id === propertyId ? { ...p, ...updates } : p))
156
+ );
157
+ markChanged();
158
+ },
159
+ [markChanged]
160
+ );
161
+
162
+ const handleDeleteProperty = useCallback(
163
+ (propertyId: string) => {
164
+ setProperties((prev) => prev.filter((p) => p.id !== propertyId));
165
+ // Also update variants to remove this property from their values
166
+ setVariants((prev) =>
167
+ prev.map((v) => {
168
+ if (v.propertyValues) {
169
+ const prop = properties.find((p) => p.id === propertyId);
170
+ if (prop && v.propertyValues[prop.name]) {
171
+ const { [prop.name]: _, ...rest } = v.propertyValues;
172
+ return { ...v, propertyValues: rest };
173
+ }
174
+ }
175
+ return v;
176
+ })
177
+ );
178
+ markChanged();
179
+ },
180
+ [properties, markChanged]
181
+ );
182
+
183
+ const handleAddPropertyValue = useCallback(
184
+ (propertyId: string, value: string) => {
185
+ if (!value.trim()) return;
186
+ setProperties((prev) =>
187
+ prev.map((p) =>
188
+ p.id === propertyId && !p.values.includes(value.trim())
189
+ ? { ...p, values: [...p.values, value.trim()] }
190
+ : p
191
+ )
192
+ );
193
+ markChanged();
194
+ },
195
+ [markChanged]
196
+ );
197
+
198
+ const handleRemovePropertyValue = useCallback(
199
+ (propertyId: string, value: string) => {
200
+ setProperties((prev) =>
201
+ prev.map((p) => {
202
+ if (p.id !== propertyId) return p;
203
+ const newValues = p.values.filter((v) => v !== value);
204
+ // Ensure at least one value remains
205
+ if (newValues.length === 0) {
206
+ newValues.push('default');
207
+ }
208
+ // Update default if removed
209
+ const newDefault = newValues.includes(p.defaultValue)
210
+ ? p.defaultValue
211
+ : newValues[0];
212
+ return { ...p, values: newValues, defaultValue: newDefault };
213
+ })
214
+ );
215
+ markChanged();
216
+ },
217
+ [markChanged]
218
+ );
219
+
220
+ // =============================================================================
221
+ // Variant Management
222
+ // =============================================================================
223
+
224
+ const handleCreateVariant = useCallback(
225
+ (propertyValues: Record<string, string>) => {
226
+ // Clone the default variant's root element as a starting point
227
+ const defaultVariant = variants.find((v) => v.id === defaultVariantId);
228
+ const baseElement = defaultVariant?.rootElement ?? master.variants[0]?.rootElement;
229
+
230
+ if (!baseElement) {
231
+ console.error('No base element available for new variant');
232
+ return;
233
+ }
234
+
235
+ const newId = `variant-${Date.now()}`;
236
+ const newVariant: VariantDefinition = {
237
+ id: newId,
238
+ name: generateVariantName(propertyValues),
239
+ propertyValues,
240
+ rootElement: JSON.parse(JSON.stringify(baseElement)), // Deep clone
241
+ };
242
+
243
+ setVariants((prev) => [...prev, newVariant]);
244
+ setSelectedVariantId(newId);
245
+ markChanged();
246
+ },
247
+ [variants, defaultVariantId, master.variants, markChanged]
248
+ );
249
+
250
+ const handleDeleteVariant = useCallback(
251
+ (variantId: string) => {
252
+ // Prevent deleting the last variant
253
+ if (variants.length <= 1) {
254
+ return;
255
+ }
256
+
257
+ setVariants((prev) => prev.filter((v) => v.id !== variantId));
258
+
259
+ // Update default if deleted
260
+ if (defaultVariantId === variantId) {
261
+ const remaining = variants.filter((v) => v.id !== variantId);
262
+ setDefaultVariantId(remaining[0]?.id ?? '');
263
+ }
264
+
265
+ // Update selection
266
+ if (selectedVariantId === variantId) {
267
+ const remaining = variants.filter((v) => v.id !== variantId);
268
+ setSelectedVariantId(remaining[0]?.id ?? null);
269
+ }
270
+
271
+ markChanged();
272
+ setShowDeleteConfirm(null);
273
+ },
274
+ [variants, defaultVariantId, selectedVariantId, markChanged]
275
+ );
276
+
277
+ const handleSetDefaultVariant = useCallback(
278
+ (variantId: string) => {
279
+ setDefaultVariantId(variantId);
280
+ markChanged();
281
+ },
282
+ [markChanged]
283
+ );
284
+
285
+ // =============================================================================
286
+ // Save Handler
287
+ // =============================================================================
288
+
289
+ const handleSave = useCallback(() => {
290
+ // Clean up properties (remove isNew flag, filter empty names)
291
+ const cleanProperties: VariantProperty[] = properties
292
+ .filter((p) => p.name.trim())
293
+ .map(({ isNew, ...p }) => p);
294
+
295
+ onSave({
296
+ variantProperties: cleanProperties,
297
+ variants,
298
+ defaultVariantId,
299
+ });
300
+
301
+ setHasUnsavedChanges(false);
302
+ onOpenChange(false);
303
+ }, [properties, variants, defaultVariantId, onSave, onOpenChange]);
304
+
305
+ // =============================================================================
306
+ // Computed Values
307
+ // =============================================================================
308
+
309
+ const variantMap = useMemo(() => createVariantMap(variants), [variants]);
310
+
311
+ const allCombinations = useMemo(
312
+ () => generateVariantCombinations(properties),
313
+ [properties]
314
+ );
315
+
316
+ const missingVariants = useMemo(() => {
317
+ return allCombinations.filter((combo) => {
318
+ const key = createVariantKey(combo);
319
+ return !variantMap.has(key);
320
+ });
321
+ }, [allCombinations, variantMap]);
322
+
323
+ const selectedVariant = useMemo(
324
+ () => variants.find((v) => v.id === selectedVariantId),
325
+ [variants, selectedVariantId]
326
+ );
327
+
328
+ // =============================================================================
329
+ // Render
330
+ // =============================================================================
331
+
332
+ return (
333
+ <>
334
+ <Dialog open={open} onOpenChange={onOpenChange}>
335
+ <DialogContent className="max-w-3xl max-h-[85vh] flex flex-col bg-[#1e1e1e] border-[#444444] text-white">
336
+ <DialogHeader>
337
+ <DialogTitle className="flex items-center gap-2 text-white">
338
+ <Layers className="h-5 w-5 text-[#4fb8ff]" />
339
+ Variant Editor: {master.name}
340
+ </DialogTitle>
341
+ <DialogDescription className="text-gray-400">
342
+ Manage variant properties and combinations for this component.
343
+ </DialogDescription>
344
+ </DialogHeader>
345
+
346
+ <Accordion
347
+ type="single"
348
+ value={activeSection}
349
+ onValueChange={setActiveSection}
350
+ className="w-full"
351
+ >
352
+ {/* Properties Section */}
353
+ <AccordionItem value="properties" className="border-[#444444]">
354
+ <AccordionTrigger className="text-sm font-medium text-gray-300 hover:text-white hover:no-underline">
355
+ <div className="flex items-center gap-2">
356
+ <Settings2 className="h-4 w-4" />
357
+ Variant Properties
358
+ <Badge variant="secondary" className="ml-2 bg-[#444444] text-gray-300">
359
+ {properties.length}
360
+ </Badge>
361
+ </div>
362
+ </AccordionTrigger>
363
+ <AccordionContent>
364
+ <div className="space-y-4 pt-2">
365
+ {properties.map((property) => (
366
+ <PropertyEditor
367
+ key={property.id}
368
+ property={property}
369
+ onUpdate={(updates) =>
370
+ handleUpdateProperty(property.id, updates)
371
+ }
372
+ onDelete={() => handleDeleteProperty(property.id)}
373
+ onAddValue={(value) =>
374
+ handleAddPropertyValue(property.id, value)
375
+ }
376
+ onRemoveValue={(value) =>
377
+ handleRemovePropertyValue(property.id, value)
378
+ }
379
+ />
380
+ ))}
381
+
382
+ <Button
383
+ variant="outline"
384
+ size="sm"
385
+ onClick={handleAddProperty}
386
+ className="w-full border-dashed border-[#4a4a4a] text-gray-400 hover:text-white hover:bg-[#2c2c2c]"
387
+ >
388
+ <Plus className="h-4 w-4 mr-2" />
389
+ Add Property
390
+ </Button>
391
+ </div>
392
+ </AccordionContent>
393
+ </AccordionItem>
394
+
395
+ {/* Variant Matrix Section */}
396
+ <AccordionItem value="matrix" className="border-[#444444]">
397
+ <AccordionTrigger className="text-sm font-medium text-gray-300 hover:text-white hover:no-underline">
398
+ <div className="flex items-center gap-2">
399
+ <Grid3x3 className="h-4 w-4" />
400
+ Variant Matrix
401
+ {missingVariants.length > 0 && (
402
+ <Badge variant="destructive" className="ml-2">
403
+ {missingVariants.length} missing
404
+ </Badge>
405
+ )}
406
+ </div>
407
+ </AccordionTrigger>
408
+ <AccordionContent>
409
+ <div className="space-y-4 pt-2">
410
+ <VariantMatrixStats
411
+ variantProperties={properties}
412
+ variants={variants}
413
+ />
414
+
415
+ <ScrollArea className="max-h-[300px]">
416
+ <VariantMatrix
417
+ variantProperties={properties}
418
+ variants={variants}
419
+ selectedVariantId={selectedVariantId}
420
+ onSelectVariant={setSelectedVariantId}
421
+ onCreateVariant={handleCreateVariant}
422
+ />
423
+ </ScrollArea>
424
+
425
+ {/* Selected variant details */}
426
+ {selectedVariant && (
427
+ <div className="mt-4 p-3 bg-[#2c2c2c] rounded-lg border border-[#444444]">
428
+ <div className="flex items-center justify-between">
429
+ <div className="flex items-center gap-2">
430
+ <span className="text-sm font-medium text-white">
431
+ {selectedVariant.name}
432
+ </span>
433
+ {selectedVariant.id === defaultVariantId && (
434
+ <Badge className="bg-amber-600 text-white">
435
+ <Star className="h-3 w-3 mr-1" />
436
+ Default
437
+ </Badge>
438
+ )}
439
+ </div>
440
+ <div className="flex items-center gap-2">
441
+ {selectedVariant.id !== defaultVariantId && (
442
+ <Button
443
+ variant="ghost"
444
+ size="sm"
445
+ onClick={() =>
446
+ handleSetDefaultVariant(selectedVariant.id)
447
+ }
448
+ className="text-amber-400 hover:text-amber-300 hover:bg-[#444444]"
449
+ >
450
+ <Star className="h-4 w-4 mr-1" />
451
+ Set Default
452
+ </Button>
453
+ )}
454
+ {variants.length > 1 && (
455
+ <Button
456
+ variant="ghost"
457
+ size="sm"
458
+ onClick={() =>
459
+ setShowDeleteConfirm(selectedVariant.id)
460
+ }
461
+ className="text-red-400 hover:text-red-300 hover:bg-[#444444]"
462
+ >
463
+ <Trash2 className="h-4 w-4" />
464
+ </Button>
465
+ )}
466
+ </div>
467
+ </div>
468
+ {selectedVariant.propertyValues && (
469
+ <div className="mt-2 flex flex-wrap gap-1">
470
+ {Object.entries(selectedVariant.propertyValues).map(
471
+ ([key, value]) => (
472
+ <Badge
473
+ key={key}
474
+ variant="outline"
475
+ className="text-xs border-[#4a4a4a] text-gray-300"
476
+ >
477
+ {key}: {value}
478
+ </Badge>
479
+ )
480
+ )}
481
+ </div>
482
+ )}
483
+ </div>
484
+ )}
485
+ </div>
486
+ </AccordionContent>
487
+ </AccordionItem>
488
+ </Accordion>
489
+
490
+ {/* Footer */}
491
+ <div className="flex items-center justify-between pt-4 border-t border-[#444444]">
492
+ <div className="text-xs text-gray-500">
493
+ {hasUnsavedChanges && (
494
+ <span className="text-amber-400 flex items-center gap-1">
495
+ <AlertTriangle className="h-3 w-3" />
496
+ Unsaved changes
497
+ </span>
498
+ )}
499
+ </div>
500
+ <div className="flex items-center gap-2">
501
+ <Button
502
+ variant="outline"
503
+ onClick={() => onOpenChange(false)}
504
+ className="border-[#4a4a4a] text-gray-300 hover:text-white hover:bg-[#2c2c2c]"
505
+ >
506
+ Cancel
507
+ </Button>
508
+ <Button
509
+ onClick={handleSave}
510
+ className="bg-[#0d99ff] hover:bg-[#0c8ce9]"
511
+ >
512
+ <Save className="h-4 w-4 mr-2" />
513
+ Save Changes
514
+ </Button>
515
+ </div>
516
+ </div>
517
+ </DialogContent>
518
+ </Dialog>
519
+
520
+ {/* Delete Confirmation Dialog */}
521
+ <AlertDialog
522
+ open={!!showDeleteConfirm}
523
+ onOpenChange={(open) => !open && setShowDeleteConfirm(null)}
524
+ >
525
+ <AlertDialogContent className="bg-[#1e1e1e] border-[#444444]">
526
+ <AlertDialogHeader>
527
+ <AlertDialogTitle className="text-white">
528
+ Delete Variant?
529
+ </AlertDialogTitle>
530
+ <AlertDialogDescription className="text-gray-400">
531
+ This will permanently delete this variant. Any instances using this
532
+ variant will fall back to the default.
533
+ </AlertDialogDescription>
534
+ </AlertDialogHeader>
535
+ <AlertDialogFooter>
536
+ <AlertDialogCancel className="border-[#4a4a4a] text-gray-300 hover:text-white hover:bg-[#2c2c2c]">
537
+ Cancel
538
+ </AlertDialogCancel>
539
+ <AlertDialogAction
540
+ onClick={() =>
541
+ showDeleteConfirm && handleDeleteVariant(showDeleteConfirm)
542
+ }
543
+ className="bg-red-600 hover:bg-red-700"
544
+ >
545
+ Delete
546
+ </AlertDialogAction>
547
+ </AlertDialogFooter>
548
+ </AlertDialogContent>
549
+ </AlertDialog>
550
+ </>
551
+ );
552
+ }
553
+
554
+ // =============================================================================
555
+ // Property Editor Sub-Component
556
+ // =============================================================================
557
+
558
+ interface PropertyEditorProps {
559
+ property: EditableVariantProperty;
560
+ onUpdate: (updates: Partial<VariantProperty>) => void;
561
+ onDelete: () => void;
562
+ onAddValue: (value: string) => void;
563
+ onRemoveValue: (value: string) => void;
564
+ }
565
+
566
+ function PropertyEditor({
567
+ property,
568
+ onUpdate,
569
+ onDelete,
570
+ onAddValue,
571
+ onRemoveValue,
572
+ }: PropertyEditorProps) {
573
+ const [newValue, setNewValue] = useState('');
574
+
575
+ const handleAddValue = useCallback(() => {
576
+ if (newValue.trim() && !property.values.includes(newValue.trim())) {
577
+ onAddValue(newValue.trim());
578
+ setNewValue('');
579
+ }
580
+ }, [newValue, property.values, onAddValue]);
581
+
582
+ const handleKeyDown = useCallback(
583
+ (e: React.KeyboardEvent) => {
584
+ if (e.key === 'Enter') {
585
+ e.preventDefault();
586
+ handleAddValue();
587
+ }
588
+ },
589
+ [handleAddValue]
590
+ );
591
+
592
+ return (
593
+ <div className="p-3 bg-[#2c2c2c] rounded-lg border border-[#444444]">
594
+ <div className="flex items-start gap-3">
595
+ {/* Property Name */}
596
+ <div className="flex-1">
597
+ <Label className="text-xs text-gray-400 mb-1 block">
598
+ Property Name
599
+ </Label>
600
+ <Input
601
+ value={property.name}
602
+ onChange={(e) => onUpdate({ name: e.target.value })}
603
+ placeholder="e.g., size, state, variant"
604
+ className="h-8 bg-[#1e1e1e] border-[#4a4a4a] text-white placeholder:text-gray-500"
605
+ />
606
+ </div>
607
+
608
+ {/* Default Value Selector */}
609
+ <div className="w-32">
610
+ <Label className="text-xs text-gray-400 mb-1 block">Default</Label>
611
+ <select
612
+ value={property.defaultValue}
613
+ onChange={(e) => onUpdate({ defaultValue: e.target.value })}
614
+ className="h-8 w-full rounded-md bg-[#1e1e1e] border border-[#4a4a4a] text-white text-sm px-2"
615
+ >
616
+ {property.values.map((v) => (
617
+ <option key={v} value={v}>
618
+ {v}
619
+ </option>
620
+ ))}
621
+ </select>
622
+ </div>
623
+
624
+ {/* Delete Button */}
625
+ <Button
626
+ variant="ghost"
627
+ size="icon"
628
+ onClick={onDelete}
629
+ className="h-8 w-8 text-gray-500 hover:text-red-400 hover:bg-[#444444] mt-5"
630
+ >
631
+ <Trash2 className="h-4 w-4" />
632
+ </Button>
633
+ </div>
634
+
635
+ {/* Property Values */}
636
+ <div className="mt-3">
637
+ <Label className="text-xs text-gray-400 mb-2 block">Values</Label>
638
+ <div className="flex flex-wrap gap-1.5">
639
+ {property.values.map((value) => (
640
+ <Badge
641
+ key={value}
642
+ variant="secondary"
643
+ className={cn(
644
+ 'bg-[#444444] text-gray-200 hover:bg-[#4a4a4a] cursor-default',
645
+ value === property.defaultValue && 'border border-amber-500'
646
+ )}
647
+ >
648
+ {value}
649
+ {property.values.length > 1 && (
650
+ <button
651
+ type="button"
652
+ onClick={() => onRemoveValue(value)}
653
+ className="ml-1.5 hover:text-red-400 transition-colors"
654
+ aria-label={`Remove value: ${value}`}
655
+ >
656
+ <X className="h-3 w-3" />
657
+ </button>
658
+ )}
659
+ </Badge>
660
+ ))}
661
+
662
+ {/* Add Value Input */}
663
+ <div className="flex items-center gap-1">
664
+ <Input
665
+ value={newValue}
666
+ onChange={(e) => setNewValue(e.target.value)}
667
+ onKeyDown={handleKeyDown}
668
+ placeholder="Add value..."
669
+ className="h-6 w-24 bg-[#1e1e1e] border-[#4a4a4a] text-white text-xs placeholder:text-gray-500"
670
+ />
671
+ <Button
672
+ variant="ghost"
673
+ size="icon"
674
+ onClick={handleAddValue}
675
+ disabled={!newValue.trim()}
676
+ className="h-6 w-6 text-gray-400 hover:text-green-400 hover:bg-[#444444]"
677
+ >
678
+ <Plus className="h-3 w-3" />
679
+ </Button>
680
+ </div>
681
+ </div>
682
+ </div>
683
+ </div>
684
+ );
685
+ }
686
+
687
+ export default VariantEditor;