@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.
- package/LICENSE +21 -0
- package/NOTICE +52 -0
- package/README.md +143 -0
- package/dist/editor.css +2 -0
- package/package.json +94 -0
- package/src/components/DeckSlideRender.tsx +21 -0
- package/src/components/SaveNote.tsx +21 -0
- package/src/components/auth/AuthProvider.tsx +44 -0
- package/src/components/ui/accordion.tsx +57 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +76 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +11 -0
- package/src/components/ui/context-menu.tsx +200 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/scrubbable-label.tsx +95 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/components/viewer/useDeck.ts +58 -0
- package/src/editor/EditorContext.tsx +632 -0
- package/src/editor/EditorToolbar.tsx +446 -0
- package/src/editor/FrontendVisualEditor.tsx +2595 -0
- package/src/editor/autosave.ts +34 -0
- package/src/editor/components/AiPromptPopover.tsx +378 -0
- package/src/editor/components/BreakpointGuides.tsx +323 -0
- package/src/editor/components/BreakpointSelector.tsx +180 -0
- package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
- package/src/editor/components/CssEditorDialog.tsx +168 -0
- package/src/editor/components/EditorCanvas.tsx +987 -0
- package/src/editor/components/EditorContextMenu.tsx +515 -0
- package/src/editor/components/EditorFooter.tsx +81 -0
- package/src/editor/components/EditorHeader.tsx +524 -0
- package/src/editor/components/EditorLayerPanel.tsx +1197 -0
- package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
- package/src/editor/components/EditorSidebar.tsx +226 -0
- package/src/editor/components/FigmaColorPicker.tsx +1577 -0
- package/src/editor/components/HtmlEditorDialog.tsx +71 -0
- package/src/editor/components/HtmlImportDialog.tsx +346 -0
- package/src/editor/components/JsEditorDialog.tsx +175 -0
- package/src/editor/components/LayerTreeItem.tsx +494 -0
- package/src/editor/components/MasterComponentEditor.tsx +846 -0
- package/src/editor/components/MediaLibraryDialog.tsx +447 -0
- package/src/editor/components/PageSettingsDialog.tsx +776 -0
- package/src/editor/components/VariablePicker.tsx +355 -0
- package/src/editor/components/VariablesDialog.tsx +564 -0
- package/src/editor/components/VariantEditor.tsx +687 -0
- package/src/editor/components/VariantMatrix.tsx +507 -0
- package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
- package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
- package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
- package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
- package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
- package/src/editor/components/component-panel/VariantList.tsx +260 -0
- package/src/editor/components/component-panel/index.ts +12 -0
- package/src/editor/components/index.ts +48 -0
- package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
- package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
- package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
- package/src/editor/components/multi-page/PageLabel.tsx +34 -0
- package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
- package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
- package/src/editor/components/multi-page/index.ts +7 -0
- package/src/editor/components/ppt/PptChrome.tsx +2508 -0
- package/src/editor/components/ppt/PptComments.tsx +589 -0
- package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
- package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
- package/src/editor/components/ppt/PptNotes.tsx +246 -0
- package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
- package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
- package/src/editor/components/property-panel/BorderSection.tsx +104 -0
- package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
- package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
- package/src/editor/components/property-panel/EffectSection.tsx +157 -0
- package/src/editor/components/property-panel/FillSection.tsx +69 -0
- package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
- package/src/editor/components/property-panel/ImageSection.tsx +206 -0
- package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
- package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
- package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
- package/src/editor/components/property-panel/LinkSection.tsx +103 -0
- package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
- package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
- package/src/editor/components/property-panel/TypographySection.tsx +230 -0
- package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
- package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
- package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
- package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
- package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
- package/src/editor/components/property-panel/index.ts +24 -0
- package/src/editor/components/property-panel/unit-utils.ts +261 -0
- package/src/editor/components/shell/EditorTopBar.tsx +392 -0
- package/src/editor/components/shell/LeftPanel.tsx +47 -0
- package/src/editor/components/shell/PagesPanel.tsx +118 -0
- package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
- package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
- package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
- package/src/editor/components/variables-panel/index.ts +7 -0
- package/src/editor/constants.ts +468 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
- package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
- package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
- package/src/editor/contexts/EditorRefsContext.tsx +204 -0
- package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
- package/src/editor/contexts/EditorToolContext.tsx +58 -0
- package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
- package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
- package/src/editor/contexts/EditorViewContext.tsx +101 -0
- package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
- package/src/editor/contexts/index.ts +17 -0
- package/src/editor/editor-skin.css +122 -0
- package/src/editor/hooks/index.ts +38 -0
- package/src/editor/hooks/useAiReplace.ts +397 -0
- package/src/editor/hooks/useAltMeasure.ts +67 -0
- package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
- package/src/editor/hooks/useCanvasControls.ts +1222 -0
- package/src/editor/hooks/useComponentEditMode.ts +658 -0
- package/src/editor/hooks/useComponentInstances.ts +515 -0
- package/src/editor/hooks/useContextMenuHandler.ts +157 -0
- package/src/editor/hooks/useCoordinateTransform.ts +207 -0
- package/src/editor/hooks/useDragResize.ts +1317 -0
- package/src/editor/hooks/useDrawingMode.ts +472 -0
- package/src/editor/hooks/useEditorColors.ts +144 -0
- package/src/editor/hooks/useEditorMessages.ts +108 -0
- package/src/editor/hooks/useElementActions.ts +1754 -0
- package/src/editor/hooks/useElementSelection.ts +1573 -0
- package/src/editor/hooks/useFocusManagement.ts +249 -0
- package/src/editor/hooks/useGoogleFonts.ts +305 -0
- package/src/editor/hooks/useIframeInitializer.ts +584 -0
- package/src/editor/hooks/useIframeSetup.ts +360 -0
- package/src/editor/hooks/useImageUpload.ts +270 -0
- package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
- package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
- package/src/editor/hooks/useMarqueeSelection.ts +388 -0
- package/src/editor/hooks/useMediaLibrary.ts +129 -0
- package/src/editor/hooks/usePageSettingsManager.ts +739 -0
- package/src/editor/hooks/useResizablePanel.ts +163 -0
- package/src/editor/hooks/useRichPaste.ts +255 -0
- package/src/editor/hooks/useTouchGestures.ts +197 -0
- package/src/editor/index.ts +27 -0
- package/src/editor/types/page-settings.ts +513 -0
- package/src/editor/types.ts +382 -0
- package/src/editor/utils/align-elements.ts +169 -0
- package/src/editor/utils/api-json.ts +21 -0
- package/src/editor/utils/aspect-lock.ts +50 -0
- package/src/editor/utils/component-renderer.ts +702 -0
- package/src/editor/utils/component-sync.ts +845 -0
- package/src/editor/utils/crop-mode.ts +265 -0
- package/src/editor/utils/dom-utils.ts +2238 -0
- package/src/editor/utils/element-effects.ts +218 -0
- package/src/editor/utils/eyedropper.ts +35 -0
- package/src/editor/utils/figma-export.ts +560 -0
- package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
- package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
- package/src/editor/utils/figma-paste.ts +2262 -0
- package/src/editor/utils/flex-reorder.ts +231 -0
- package/src/editor/utils/flex-utils.ts +1230 -0
- package/src/editor/utils/geometry.ts +232 -0
- package/src/editor/utils/grid-layout.ts +210 -0
- package/src/editor/utils/html-utils.ts +470 -0
- package/src/editor/utils/index.ts +11 -0
- package/src/editor/utils/ink-style.ts +57 -0
- package/src/editor/utils/inline-format.ts +236 -0
- package/src/editor/utils/measure-distance.ts +120 -0
- package/src/editor/utils/override-apply.ts +526 -0
- package/src/editor/utils/override-detection.ts +613 -0
- package/src/editor/utils/paste-processors.ts +609 -0
- package/src/editor/utils/paste-sanitizer.ts +244 -0
- package/src/editor/utils/restore-flow.ts +188 -0
- package/src/editor/utils/shape-library.ts +171 -0
- package/src/editor/utils/slide-root.ts +29 -0
- package/src/editor/utils/smart-guides.ts +218 -0
- package/src/editor/utils/style-utils.ts +685 -0
- package/src/editor/utils/table-edit.ts +151 -0
- package/src/editor/utils/tailwind-mappings.ts +778 -0
- package/src/editor/utils/tailwind-utils.ts +804 -0
- package/src/editor/utils/text-highlight.ts +216 -0
- package/src/editor/utils/theme-to-variables.ts +341 -0
- package/src/editor/utils/variant-resolver.ts +456 -0
- package/src/editor/utils/viewport-utils.ts +295 -0
- package/src/hooks/useEditorHistory.ts +195 -0
- package/src/hooks/useEditorShortcuts.ts +260 -0
- package/src/index.ts +40 -0
- package/src/io.ts +134 -0
- package/src/lib/agent/slide-agent/types.ts +547 -0
- package/src/lib/agent/website-agent/types.ts +805 -0
- package/src/lib/api/auth-fetch.ts +58 -0
- package/src/lib/deck.ts +69 -0
- package/src/lib/export.ts +47 -0
- package/src/lib/firebase/config.ts +29 -0
- package/src/lib/firebase/css-variables.ts +378 -0
- package/src/lib/firebase/editor-components.ts +677 -0
- package/src/lib/firebase/storage.ts +469 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/editor.css +52 -0
- package/src/styles/skin.css +122 -0
- package/src/types/css-variables.ts +645 -0
- package/src/types/editor-components.ts +944 -0
- package/src/types/editor.ts +420 -0
- package/src/types/page-master.ts +197 -0
- package/src/types/page.ts +419 -0
- package/src/types/slide.ts +402 -0
- package/src/types/website-theme.ts +788 -0
- package/src/types/website.ts +374 -0
- package/src/vendor/firebase-firestore.ts +13 -0
- 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 "{master?.name}"? 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;
|