@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,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;
|