@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,846 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * MasterComponentEditor - Dialog for editing master component definitions
5
+ *
6
+ * Features:
7
+ * - Edit name, category, description, tags
8
+ * - Preview the component (rendered from rootElement)
9
+ * - Manage variants
10
+ * - Add/remove exposed properties (ComponentProperty)
11
+ * - Save/Cancel/Delete actions with confirmation
12
+ */
13
+
14
+ import React, { useState, useCallback, useMemo, useEffect } from 'react';
15
+ import {
16
+ Dialog,
17
+ DialogContent,
18
+ DialogHeader,
19
+ DialogTitle,
20
+ DialogDescription,
21
+ DialogFooter,
22
+ } from '../../components/ui/dialog';
23
+ import {
24
+ AlertDialog,
25
+ AlertDialogAction,
26
+ AlertDialogCancel,
27
+ AlertDialogContent,
28
+ AlertDialogDescription,
29
+ AlertDialogFooter,
30
+ AlertDialogHeader,
31
+ AlertDialogTitle,
32
+ } from '../../components/ui/alert-dialog';
33
+ import { Button } from '../../components/ui/button';
34
+ import { Input } from '../../components/ui/input';
35
+ import { Label } from '../../components/ui/label';
36
+ import { Textarea } from '../../components/ui/textarea';
37
+ import {
38
+ Select,
39
+ SelectContent,
40
+ SelectItem,
41
+ SelectTrigger,
42
+ SelectValue,
43
+ } from '../../components/ui/select';
44
+ import {
45
+ Collapsible,
46
+ CollapsibleContent,
47
+ CollapsibleTrigger,
48
+ } from '../../components/ui/collapsible';
49
+ import { ScrollArea } from '../../components/ui/scroll-area';
50
+ import { Badge } from '../../components/ui/badge';
51
+ import {
52
+ Settings,
53
+ ChevronRight,
54
+ Plus,
55
+ Pencil,
56
+ Trash2,
57
+ Save,
58
+ Loader2,
59
+ Component as ComponentIcon,
60
+ Layers,
61
+ ToggleLeft,
62
+ Type,
63
+ Repeat,
64
+ X,
65
+ Eye,
66
+ } from 'lucide-react';
67
+ import { cn } from '../../lib/utils';
68
+ import type {
69
+ MasterComponent,
70
+ ComponentProperty,
71
+ ComponentVariant,
72
+ ComponentElement,
73
+ ComponentLibraryCategory,
74
+ } from '../../types/editor-components';
75
+ import { ComponentPropertyEditor } from './ComponentPropertyEditor';
76
+ import { renderComponentElement } from '../utils/component-renderer';
77
+
78
+ // ============================================
79
+ // Types
80
+ // ============================================
81
+
82
+ interface MasterComponentEditorProps {
83
+ /** Master component to edit (null for creating new) */
84
+ master: MasterComponent | null;
85
+ /** Whether the dialog is open */
86
+ open: boolean;
87
+ /** Callback when dialog open state changes */
88
+ onOpenChange: (open: boolean) => void;
89
+ /** Callback when component is saved */
90
+ onSave: (master: MasterComponent) => Promise<void>;
91
+ /** Callback when component is deleted (optional) */
92
+ onDelete?: (masterId: string) => Promise<void>;
93
+ /** Available categories for the component */
94
+ categories: ComponentLibraryCategory[];
95
+ /** Available components for instance swap properties (optional) */
96
+ availableComponents?: { id: string; name: string }[];
97
+ /** Callback to open variant editor (optional) */
98
+ onEditVariant?: (variant: ComponentVariant) => void;
99
+ }
100
+
101
+ // ============================================
102
+ // Helper Functions
103
+ // ============================================
104
+
105
+ function generateId(): string {
106
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
107
+ }
108
+
109
+ /**
110
+ * Extract available elements from a ComponentElement tree for targeting
111
+ */
112
+ function extractAvailableElements(
113
+ element: ComponentElement,
114
+ path: string = ''
115
+ ): { id: string; name: string }[] {
116
+ const results: { id: string; name: string }[] = [];
117
+
118
+ const displayName = element.displayName ||
119
+ element.textContent?.substring(0, 20) ||
120
+ `${element.tagName}#${element.id.substring(0, 8)}`;
121
+
122
+ results.push({
123
+ id: element.id,
124
+ name: displayName,
125
+ });
126
+
127
+ element.children.forEach((child, index) => {
128
+ const childPath = path ? `${path}.children.${index}` : `children.${index}`;
129
+ results.push(...extractAvailableElements(child, childPath));
130
+ });
131
+
132
+ return results;
133
+ }
134
+
135
+ /**
136
+ * Get property type icon
137
+ */
138
+ function getPropertyTypeIcon(type: string): React.ReactNode {
139
+ switch (type) {
140
+ case 'boolean':
141
+ return <ToggleLeft className="h-3.5 w-3.5" />;
142
+ case 'text':
143
+ return <Type className="h-3.5 w-3.5" />;
144
+ case 'instanceSwap':
145
+ return <Repeat className="h-3.5 w-3.5" />;
146
+ default:
147
+ return <Settings className="h-3.5 w-3.5" />;
148
+ }
149
+ }
150
+
151
+ // ============================================
152
+ // Preview Component
153
+ // ============================================
154
+
155
+ interface ComponentPreviewProps {
156
+ element: ComponentElement | undefined;
157
+ }
158
+
159
+ function ComponentPreview({ element }: ComponentPreviewProps) {
160
+ const [previewHtml, setPreviewHtml] = useState<string>('');
161
+
162
+ useEffect(() => {
163
+ if (!element) {
164
+ setPreviewHtml('');
165
+ return;
166
+ }
167
+
168
+ try {
169
+ const htmlElement = renderComponentElement(element);
170
+ setPreviewHtml(htmlElement.outerHTML);
171
+ } catch (error) {
172
+ console.error('[ComponentPreview] Failed to render:', error);
173
+ setPreviewHtml('<div class="text-red-500 text-xs">Preview failed</div>');
174
+ }
175
+ }, [element]);
176
+
177
+ if (!element) {
178
+ return (
179
+ <div className="flex items-center justify-center h-full">
180
+ <div className="text-center">
181
+ <ComponentIcon className="h-8 w-8 text-gray-600 mx-auto mb-2" />
182
+ <p className="text-xs text-gray-500">No preview available</p>
183
+ </div>
184
+ </div>
185
+ );
186
+ }
187
+
188
+ return (
189
+ <div className="h-full overflow-auto p-4 flex items-center justify-center">
190
+ <div
191
+ className="component-preview-container"
192
+ dangerouslySetInnerHTML={{ __html: previewHtml }}
193
+ />
194
+ </div>
195
+ );
196
+ }
197
+
198
+ // ============================================
199
+ // Property Item Component
200
+ // ============================================
201
+
202
+ interface PropertyItemProps {
203
+ property: ComponentProperty;
204
+ onEdit: () => void;
205
+ onDelete: () => void;
206
+ }
207
+
208
+ function PropertyItem({ property, onEdit, onDelete }: PropertyItemProps) {
209
+ return (
210
+ <div className="flex items-center gap-2 py-2 px-3 bg-[#2c2c2c] rounded-lg group">
211
+ <div className="flex items-center justify-center w-6 h-6 rounded bg-[#444444] text-gray-400">
212
+ {getPropertyTypeIcon(property.type)}
213
+ </div>
214
+ <div className="flex-1 min-w-0">
215
+ <div className="flex items-center gap-2">
216
+ <span className="text-sm text-gray-200 truncate">{property.name}</span>
217
+ <Badge variant="outline" className="text-[10px] h-4 border-[#4a4a4a] text-gray-400">
218
+ {property.type}
219
+ </Badge>
220
+ {property.required && (
221
+ <Badge variant="outline" className="text-[10px] h-4 border-red-500/50 text-red-400">
222
+ required
223
+ </Badge>
224
+ )}
225
+ </div>
226
+ {property.description && (
227
+ <p className="text-xs text-gray-500 truncate">{property.description}</p>
228
+ )}
229
+ </div>
230
+ <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
231
+ <Button
232
+ variant="ghost"
233
+ size="icon"
234
+ className="h-6 w-6 text-gray-400 hover:text-gray-200"
235
+ onClick={onEdit}
236
+ >
237
+ <Pencil className="h-3 w-3" />
238
+ </Button>
239
+ <Button
240
+ variant="ghost"
241
+ size="icon"
242
+ className="h-6 w-6 text-gray-400 hover:text-red-400"
243
+ onClick={onDelete}
244
+ >
245
+ <Trash2 className="h-3 w-3" />
246
+ </Button>
247
+ </div>
248
+ </div>
249
+ );
250
+ }
251
+
252
+ // ============================================
253
+ // Variant Item Component
254
+ // ============================================
255
+
256
+ interface VariantItemProps {
257
+ variant: ComponentVariant;
258
+ isDefault: boolean;
259
+ onEdit?: () => void;
260
+ }
261
+
262
+ function VariantItem({ variant, isDefault, onEdit }: VariantItemProps) {
263
+ return (
264
+ <div
265
+ className={cn(
266
+ 'flex items-center gap-2 py-2 px-3 rounded-lg group cursor-pointer transition-colors',
267
+ isDefault ? 'bg-[#0d99ff]/10 border border-blue-600/30' : 'bg-[#2c2c2c] hover:bg-[#353535]'
268
+ )}
269
+ onClick={onEdit}
270
+ >
271
+ <div className="flex items-center justify-center w-6 h-6 rounded bg-[#444444] text-gray-400">
272
+ <Layers className="h-3.5 w-3.5" />
273
+ </div>
274
+ <div className="flex-1 min-w-0">
275
+ <div className="flex items-center gap-2">
276
+ <span className="text-sm text-gray-200 truncate">{variant.name}</span>
277
+ {isDefault && (
278
+ <Badge className="text-[10px] h-4 bg-[#0d99ff]/20 text-[#4fb8ff] border-blue-600/30">
279
+ default
280
+ </Badge>
281
+ )}
282
+ </div>
283
+ {variant.propertyValues && Object.keys(variant.propertyValues).length > 0 && (
284
+ <p className="text-xs text-gray-500 truncate">
285
+ {Object.entries(variant.propertyValues)
286
+ .map(([key, value]) => `${key}=${value}`)
287
+ .join(', ')}
288
+ </p>
289
+ )}
290
+ </div>
291
+ {onEdit && (
292
+ <Button
293
+ variant="ghost"
294
+ size="icon"
295
+ className="h-6 w-6 text-gray-400 hover:text-gray-200 opacity-0 group-hover:opacity-100 transition-opacity"
296
+ onClick={(e) => {
297
+ e.stopPropagation();
298
+ onEdit();
299
+ }}
300
+ >
301
+ <Pencil className="h-3 w-3" />
302
+ </Button>
303
+ )}
304
+ </div>
305
+ );
306
+ }
307
+
308
+ // ============================================
309
+ // Main Component
310
+ // ============================================
311
+
312
+ export function MasterComponentEditor({
313
+ master,
314
+ open,
315
+ onOpenChange,
316
+ onSave,
317
+ onDelete,
318
+ categories,
319
+ availableComponents = [],
320
+ onEditVariant,
321
+ }: MasterComponentEditorProps) {
322
+ // Form state
323
+ const [name, setName] = useState('');
324
+ const [categoryId, setCategoryId] = useState('');
325
+ const [description, setDescription] = useState('');
326
+ const [tags, setTags] = useState<string[]>([]);
327
+ const [tagInput, setTagInput] = useState('');
328
+ const [exposedProperties, setExposedProperties] = useState<ComponentProperty[]>([]);
329
+ const [variants, setVariants] = useState<ComponentVariant[]>([]);
330
+ const [defaultVariantId, setDefaultVariantId] = useState('');
331
+
332
+ // UI state
333
+ const [isSaving, setIsSaving] = useState(false);
334
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
335
+ const [editingPropertyId, setEditingPropertyId] = useState<string | null>(null);
336
+ const [showPropertyEditor, setShowPropertyEditor] = useState(false);
337
+ const [sectionsOpen, setSectionsOpen] = useState({
338
+ variants: true,
339
+ properties: true,
340
+ });
341
+
342
+ const isNew = !master;
343
+
344
+ // Initialize form when master changes
345
+ useEffect(() => {
346
+ if (master) {
347
+ setName(master.name);
348
+ setCategoryId(master.categoryId);
349
+ setDescription(master.description || '');
350
+ setTags(master.tags || []);
351
+ setExposedProperties(master.exposedProperties || []);
352
+ setVariants(master.variants || []);
353
+ setDefaultVariantId(master.defaultVariantId);
354
+ } else {
355
+ // Reset for new component
356
+ setName('');
357
+ setCategoryId(categories[0]?.id || '');
358
+ setDescription('');
359
+ setTags([]);
360
+ setExposedProperties([]);
361
+ setVariants([]);
362
+ setDefaultVariantId('');
363
+ }
364
+ setEditingPropertyId(null);
365
+ setShowPropertyEditor(false);
366
+ setTagInput('');
367
+ }, [master, categories, open]);
368
+
369
+ // Get default variant for preview
370
+ const defaultVariant = useMemo(() => {
371
+ return variants.find((v) => v.id === defaultVariantId) || variants[0];
372
+ }, [variants, defaultVariantId]);
373
+
374
+ // Get available elements for property targeting
375
+ const availableElements = useMemo(() => {
376
+ if (!defaultVariant?.rootElement) return [];
377
+ return extractAvailableElements(defaultVariant.rootElement);
378
+ }, [defaultVariant]);
379
+
380
+ // Validation
381
+ const isValid = useMemo(() => {
382
+ if (!name.trim()) return false;
383
+ if (!categoryId) return false;
384
+ if (variants.length === 0) return false;
385
+ return true;
386
+ }, [name, categoryId, variants]);
387
+
388
+ // Toggle section
389
+ const toggleSection = useCallback((section: 'variants' | 'properties') => {
390
+ setSectionsOpen((prev) => ({
391
+ ...prev,
392
+ [section]: !prev[section],
393
+ }));
394
+ }, []);
395
+
396
+ // Handle adding a tag
397
+ const handleAddTag = useCallback(() => {
398
+ const trimmedTag = tagInput.trim();
399
+ if (trimmedTag && !tags.includes(trimmedTag)) {
400
+ setTags((prev) => [...prev, trimmedTag]);
401
+ setTagInput('');
402
+ }
403
+ }, [tagInput, tags]);
404
+
405
+ // Handle removing a tag
406
+ const handleRemoveTag = useCallback((tag: string) => {
407
+ setTags((prev) => prev.filter((t) => t !== tag));
408
+ }, []);
409
+
410
+ // Handle tag input key down
411
+ const handleTagKeyDown = useCallback(
412
+ (e: React.KeyboardEvent) => {
413
+ if (e.key === 'Enter') {
414
+ e.preventDefault();
415
+ handleAddTag();
416
+ }
417
+ },
418
+ [handleAddTag]
419
+ );
420
+
421
+ // Handle property save
422
+ const handlePropertySave = useCallback((property: ComponentProperty) => {
423
+ setExposedProperties((prev) => {
424
+ const existingIndex = prev.findIndex((p) => p.id === property.id);
425
+ if (existingIndex >= 0) {
426
+ const next = [...prev];
427
+ next[existingIndex] = property;
428
+ return next;
429
+ }
430
+ return [...prev, property];
431
+ });
432
+ setEditingPropertyId(null);
433
+ setShowPropertyEditor(false);
434
+ }, []);
435
+
436
+ // Handle property delete
437
+ const handlePropertyDelete = useCallback((propertyId: string) => {
438
+ setExposedProperties((prev) => prev.filter((p) => p.id !== propertyId));
439
+ }, []);
440
+
441
+ // Handle save
442
+ const handleSave = useCallback(async () => {
443
+ if (!isValid) return;
444
+
445
+ setIsSaving(true);
446
+ try {
447
+ const now = new Date().toISOString();
448
+ const updatedMaster: MasterComponent = {
449
+ id: master?.id || generateId(),
450
+ name: name.trim(),
451
+ categoryId,
452
+ description: description.trim() || undefined,
453
+ tags: tags.length > 0 ? tags : undefined,
454
+ variants,
455
+ defaultVariantId: defaultVariantId || variants[0]?.id || '',
456
+ exposedProperties,
457
+ createdAt: master?.createdAt || now,
458
+ updatedAt: now,
459
+ websiteId: master?.websiteId || '',
460
+ version: (master?.version || 0) + 1,
461
+ };
462
+
463
+ await onSave(updatedMaster);
464
+ onOpenChange(false);
465
+ } catch (error) {
466
+ console.error('[MasterComponentEditor] Save failed:', error);
467
+ } finally {
468
+ setIsSaving(false);
469
+ }
470
+ }, [
471
+ isValid,
472
+ master,
473
+ name,
474
+ categoryId,
475
+ description,
476
+ tags,
477
+ variants,
478
+ defaultVariantId,
479
+ exposedProperties,
480
+ onSave,
481
+ onOpenChange,
482
+ ]);
483
+
484
+ // Handle delete
485
+ const handleDelete = useCallback(async () => {
486
+ if (!master?.id || !onDelete) return;
487
+
488
+ setIsSaving(true);
489
+ try {
490
+ await onDelete(master.id);
491
+ setShowDeleteConfirm(false);
492
+ onOpenChange(false);
493
+ } catch (error) {
494
+ console.error('[MasterComponentEditor] Delete failed:', error);
495
+ } finally {
496
+ setIsSaving(false);
497
+ }
498
+ }, [master, onDelete, onOpenChange]);
499
+
500
+ // Currently editing property
501
+ const editingProperty = useMemo(() => {
502
+ if (!editingPropertyId) return null;
503
+ return exposedProperties.find((p) => p.id === editingPropertyId) || null;
504
+ }, [editingPropertyId, exposedProperties]);
505
+
506
+ return (
507
+ <>
508
+ <Dialog open={open} onOpenChange={onOpenChange}>
509
+ <DialogContent className="max-w-2xl max-h-[90vh] flex flex-col bg-[#2c2c2c] border-[#444444] text-gray-200">
510
+ <DialogHeader>
511
+ <DialogTitle className="flex items-center gap-2">
512
+ <Settings className="h-5 w-5" />
513
+ {isNew ? 'Create Component' : `Edit Component: "${master?.name}"`}
514
+ </DialogTitle>
515
+ <DialogDescription className="text-gray-400">
516
+ {isNew
517
+ ? 'Define a new reusable component for your design system.'
518
+ : 'Modify the component definition. Changes will affect all instances.'}
519
+ </DialogDescription>
520
+ </DialogHeader>
521
+
522
+ <ScrollArea className="flex-1 -mx-6 px-6">
523
+ <div className="space-y-6 py-4">
524
+ {/* Basic Info */}
525
+ <div className="grid grid-cols-2 gap-4">
526
+ {/* Name */}
527
+ <div className="grid gap-1.5">
528
+ <Label className="text-xs text-gray-400">Name</Label>
529
+ <Input
530
+ value={name}
531
+ onChange={(e) => setName(e.target.value)}
532
+ placeholder="Component name..."
533
+ className="h-9 bg-[#2c2c2c] border-[#444444] text-gray-200 placeholder:text-gray-500"
534
+ />
535
+ </div>
536
+
537
+ {/* Category */}
538
+ <div className="grid gap-1.5">
539
+ <Label className="text-xs text-gray-400">Category</Label>
540
+ <Select value={categoryId} onValueChange={setCategoryId}>
541
+ <SelectTrigger className="h-9 bg-[#2c2c2c] border-[#444444] text-gray-200">
542
+ <SelectValue placeholder="Select category..." />
543
+ </SelectTrigger>
544
+ <SelectContent className="bg-[#2c2c2c] border-[#444444]">
545
+ {categories.map((cat) => (
546
+ <SelectItem
547
+ key={cat.id}
548
+ value={cat.id}
549
+ className="text-gray-200 focus:bg-[#444444] focus:text-white"
550
+ >
551
+ {cat.name}
552
+ </SelectItem>
553
+ ))}
554
+ </SelectContent>
555
+ </Select>
556
+ </div>
557
+ </div>
558
+
559
+ {/* Description */}
560
+ <div className="grid gap-1.5">
561
+ <Label className="text-xs text-gray-400">Description</Label>
562
+ <Textarea
563
+ value={description}
564
+ onChange={(e) => setDescription(e.target.value)}
565
+ placeholder="Describe what this component does..."
566
+ className="h-20 resize-none bg-[#2c2c2c] border-[#444444] text-gray-200 placeholder:text-gray-500"
567
+ />
568
+ </div>
569
+
570
+ {/* Tags */}
571
+ <div className="grid gap-1.5">
572
+ <Label className="text-xs text-gray-400">Tags</Label>
573
+ <div className="flex flex-wrap gap-2 mb-2">
574
+ {tags.map((tag) => (
575
+ <Badge
576
+ key={tag}
577
+ variant="secondary"
578
+ className="bg-[#444444] text-gray-300 hover:bg-[#4a4a4a] cursor-pointer"
579
+ onClick={() => handleRemoveTag(tag)}
580
+ >
581
+ {tag}
582
+ <X className="h-3 w-3 ml-1" />
583
+ </Badge>
584
+ ))}
585
+ </div>
586
+ <div className="flex gap-2">
587
+ <Input
588
+ value={tagInput}
589
+ onChange={(e) => setTagInput(e.target.value)}
590
+ onKeyDown={handleTagKeyDown}
591
+ placeholder="Add tag..."
592
+ className="h-8 text-sm bg-[#2c2c2c] border-[#444444] text-gray-200 placeholder:text-gray-500"
593
+ />
594
+ <Button
595
+ size="sm"
596
+ variant="outline"
597
+ onClick={handleAddTag}
598
+ disabled={!tagInput.trim()}
599
+ className="border-[#444444] text-gray-300 hover:bg-[#444444]"
600
+ >
601
+ <Plus className="h-3.5 w-3.5" />
602
+ </Button>
603
+ </div>
604
+ </div>
605
+
606
+ {/* Preview */}
607
+ <div className="grid gap-1.5">
608
+ <Label className="text-xs text-gray-400 flex items-center gap-1">
609
+ <Eye className="h-3.5 w-3.5" />
610
+ Preview
611
+ </Label>
612
+ <div className="h-32 bg-[#1e1e1e] border border-[#444444] rounded-lg overflow-hidden">
613
+ <ComponentPreview element={defaultVariant?.rootElement} />
614
+ </div>
615
+ </div>
616
+
617
+ {/* Variants Section */}
618
+ <Collapsible
619
+ open={sectionsOpen.variants}
620
+ onOpenChange={() => toggleSection('variants')}
621
+ >
622
+ <CollapsibleTrigger className="flex items-center justify-between w-full py-2 text-sm font-medium text-gray-300 hover:text-gray-100">
623
+ <span className="flex items-center gap-2">
624
+ <Layers className="h-4 w-4 text-gray-500" />
625
+ Variants
626
+ <span className="text-xs text-gray-500 font-normal">
627
+ ({variants.length})
628
+ </span>
629
+ </span>
630
+ <ChevronRight
631
+ className={cn(
632
+ 'h-4 w-4 text-gray-500 transition-transform',
633
+ sectionsOpen.variants && 'rotate-90'
634
+ )}
635
+ />
636
+ </CollapsibleTrigger>
637
+ <CollapsibleContent className="pt-2">
638
+ <div className="space-y-2">
639
+ {variants.length === 0 ? (
640
+ <p className="text-xs text-gray-500 text-center py-4">
641
+ No variants defined
642
+ </p>
643
+ ) : (
644
+ variants.map((variant) => (
645
+ <VariantItem
646
+ key={variant.id}
647
+ variant={variant}
648
+ isDefault={variant.id === defaultVariantId}
649
+ onEdit={onEditVariant ? () => onEditVariant(variant) : undefined}
650
+ />
651
+ ))
652
+ )}
653
+ {onEditVariant && (
654
+ <Button
655
+ variant="outline"
656
+ size="sm"
657
+ className="w-full border-dashed border-[#444444] text-gray-400 hover:text-gray-200 hover:bg-[#2c2c2c]"
658
+ onClick={() => {
659
+ // Create empty variant and open editor
660
+ const newVariant: ComponentVariant = {
661
+ id: generateId(),
662
+ name: `Variant ${variants.length + 1}`,
663
+ rootElement: defaultVariant?.rootElement || {
664
+ id: generateId(),
665
+ tagName: 'div',
666
+ attributes: {},
667
+ className: '',
668
+ children: [],
669
+ overridable: {
670
+ text: true,
671
+ fill: true,
672
+ stroke: true,
673
+ visibility: true,
674
+ image: true,
675
+ },
676
+ styles: {},
677
+ },
678
+ };
679
+ setVariants((prev) => [...prev, newVariant]);
680
+ onEditVariant(newVariant);
681
+ }}
682
+ >
683
+ <Plus className="h-3.5 w-3.5 mr-1" />
684
+ Add Variant
685
+ </Button>
686
+ )}
687
+ </div>
688
+ </CollapsibleContent>
689
+ </Collapsible>
690
+
691
+ {/* Exposed Properties Section */}
692
+ <Collapsible
693
+ open={sectionsOpen.properties}
694
+ onOpenChange={() => toggleSection('properties')}
695
+ >
696
+ <CollapsibleTrigger className="flex items-center justify-between w-full py-2 text-sm font-medium text-gray-300 hover:text-gray-100">
697
+ <span className="flex items-center gap-2">
698
+ <Settings className="h-4 w-4 text-gray-500" />
699
+ Exposed Properties
700
+ <span className="text-xs text-gray-500 font-normal">
701
+ ({exposedProperties.length})
702
+ </span>
703
+ </span>
704
+ <ChevronRight
705
+ className={cn(
706
+ 'h-4 w-4 text-gray-500 transition-transform',
707
+ sectionsOpen.properties && 'rotate-90'
708
+ )}
709
+ />
710
+ </CollapsibleTrigger>
711
+ <CollapsibleContent className="pt-2">
712
+ <div className="space-y-2">
713
+ {/* Property Editor */}
714
+ {(showPropertyEditor || editingPropertyId) && (
715
+ <ComponentPropertyEditor
716
+ property={editingProperty}
717
+ availableElements={availableElements}
718
+ availableComponents={availableComponents}
719
+ onSave={handlePropertySave}
720
+ onCancel={() => {
721
+ setShowPropertyEditor(false);
722
+ setEditingPropertyId(null);
723
+ }}
724
+ />
725
+ )}
726
+
727
+ {/* Property List */}
728
+ {!showPropertyEditor && !editingPropertyId && (
729
+ <>
730
+ {exposedProperties.length === 0 ? (
731
+ <p className="text-xs text-gray-500 text-center py-4">
732
+ No exposed properties
733
+ </p>
734
+ ) : (
735
+ exposedProperties.map((property) => (
736
+ <PropertyItem
737
+ key={property.id}
738
+ property={property}
739
+ onEdit={() => setEditingPropertyId(property.id)}
740
+ onDelete={() => handlePropertyDelete(property.id)}
741
+ />
742
+ ))
743
+ )}
744
+
745
+ {/* Add Property Buttons */}
746
+ <div className="flex flex-wrap gap-2 pt-2">
747
+ <Button
748
+ variant="outline"
749
+ size="sm"
750
+ className="border-dashed border-[#444444] text-gray-400 hover:text-gray-200 hover:bg-[#2c2c2c]"
751
+ onClick={() => setShowPropertyEditor(true)}
752
+ >
753
+ <Plus className="h-3.5 w-3.5 mr-1" />
754
+ Add Property
755
+ </Button>
756
+ </div>
757
+ </>
758
+ )}
759
+ </div>
760
+ </CollapsibleContent>
761
+ </Collapsible>
762
+ </div>
763
+ </ScrollArea>
764
+
765
+ {/* Footer */}
766
+ <DialogFooter className="flex items-center justify-between pt-4 border-t border-[#444444]">
767
+ <div>
768
+ {!isNew && onDelete && (
769
+ <Button
770
+ variant="ghost"
771
+ size="sm"
772
+ className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
773
+ onClick={() => setShowDeleteConfirm(true)}
774
+ disabled={isSaving}
775
+ >
776
+ <Trash2 className="h-4 w-4 mr-1" />
777
+ Delete
778
+ </Button>
779
+ )}
780
+ </div>
781
+ <div className="flex items-center gap-2">
782
+ <Button
783
+ variant="ghost"
784
+ onClick={() => onOpenChange(false)}
785
+ disabled={isSaving}
786
+ className="text-gray-400 hover:text-gray-200"
787
+ >
788
+ Cancel
789
+ </Button>
790
+ <Button
791
+ onClick={handleSave}
792
+ disabled={!isValid || isSaving}
793
+ className="bg-[#0d99ff] hover:bg-[#0c8ce9] text-white"
794
+ >
795
+ {isSaving ? (
796
+ <>
797
+ <Loader2 className="h-4 w-4 mr-1 animate-spin" />
798
+ Saving...
799
+ </>
800
+ ) : (
801
+ <>
802
+ <Save className="h-4 w-4 mr-1" />
803
+ Save Changes
804
+ </>
805
+ )}
806
+ </Button>
807
+ </div>
808
+ </DialogFooter>
809
+ </DialogContent>
810
+ </Dialog>
811
+
812
+ {/* Delete Confirmation Dialog */}
813
+ <AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
814
+ <AlertDialogContent className="bg-[#2c2c2c] border-[#444444]">
815
+ <AlertDialogHeader>
816
+ <AlertDialogTitle className="text-gray-200">Delete Component</AlertDialogTitle>
817
+ <AlertDialogDescription className="text-gray-400">
818
+ Are you sure you want to delete &quot;{master?.name}&quot;? This action cannot be
819
+ undone. All instances of this component will be affected.
820
+ </AlertDialogDescription>
821
+ </AlertDialogHeader>
822
+ <AlertDialogFooter>
823
+ <AlertDialogCancel className="bg-transparent border-[#444444] text-gray-300 hover:bg-[#444444]">
824
+ Cancel
825
+ </AlertDialogCancel>
826
+ <AlertDialogAction
827
+ onClick={handleDelete}
828
+ className="bg-red-600 hover:bg-red-700 text-white"
829
+ >
830
+ {isSaving ? (
831
+ <>
832
+ <Loader2 className="h-4 w-4 mr-1 animate-spin" />
833
+ Deleting...
834
+ </>
835
+ ) : (
836
+ 'Delete'
837
+ )}
838
+ </AlertDialogAction>
839
+ </AlertDialogFooter>
840
+ </AlertDialogContent>
841
+ </AlertDialog>
842
+ </>
843
+ );
844
+ }
845
+
846
+ export default MasterComponentEditor;