@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,507 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VariantMatrix - Grid display of all variant combinations
|
|
5
|
+
*
|
|
6
|
+
* Displays a matrix where:
|
|
7
|
+
* - For 1 property: Shows a simple list of variants
|
|
8
|
+
* - For 2 properties: Shows a 2D grid with rows and columns
|
|
9
|
+
* - For 3+ properties: Shows a hierarchical grid with nested sections
|
|
10
|
+
*
|
|
11
|
+
* Cells show:
|
|
12
|
+
* - Checkmark if variant exists
|
|
13
|
+
* - Empty/dashed if variant is missing (clickable to create)
|
|
14
|
+
* - Highlighted if currently selected
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React, { useMemo, useCallback } from 'react';
|
|
18
|
+
import { Check, Plus } from 'lucide-react';
|
|
19
|
+
import { cn } from '../../lib/utils';
|
|
20
|
+
import type {
|
|
21
|
+
VariantProperty,
|
|
22
|
+
VariantDefinition,
|
|
23
|
+
} from '../../types/editor-components';
|
|
24
|
+
import {
|
|
25
|
+
createVariantKey,
|
|
26
|
+
createVariantMap,
|
|
27
|
+
generateVariantCombinations,
|
|
28
|
+
} from '../utils/variant-resolver';
|
|
29
|
+
|
|
30
|
+
interface VariantMatrixProps {
|
|
31
|
+
/** Array of variant property definitions */
|
|
32
|
+
variantProperties: VariantProperty[];
|
|
33
|
+
/** Array of existing variant definitions */
|
|
34
|
+
variants: VariantDefinition[];
|
|
35
|
+
/** Currently selected variant ID */
|
|
36
|
+
selectedVariantId: string | null;
|
|
37
|
+
/** Callback when a variant is selected */
|
|
38
|
+
onSelectVariant: (variantId: string) => void;
|
|
39
|
+
/** Optional callback to create a new variant (shows + icon for missing) */
|
|
40
|
+
onCreateVariant?: (propertyValues: Record<string, string>) => void;
|
|
41
|
+
/** Optional: compact mode for smaller displays */
|
|
42
|
+
compact?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* VariantMatrix displays all possible variant combinations in a grid format.
|
|
47
|
+
* Supports 1D (list), 2D (grid), and multi-dimensional (nested) layouts.
|
|
48
|
+
*/
|
|
49
|
+
export function VariantMatrix({
|
|
50
|
+
variantProperties,
|
|
51
|
+
variants,
|
|
52
|
+
selectedVariantId,
|
|
53
|
+
onSelectVariant,
|
|
54
|
+
onCreateVariant,
|
|
55
|
+
compact = false,
|
|
56
|
+
}: VariantMatrixProps) {
|
|
57
|
+
// Create a map for fast variant lookup by key
|
|
58
|
+
const variantMap = useMemo(() => createVariantMap(variants), [variants]);
|
|
59
|
+
|
|
60
|
+
// Get variant ID by property values
|
|
61
|
+
const getVariantId = useCallback(
|
|
62
|
+
(propertyValues: Record<string, string>): string | null => {
|
|
63
|
+
const key = createVariantKey(propertyValues);
|
|
64
|
+
return variantMap.get(key)?.id ?? null;
|
|
65
|
+
},
|
|
66
|
+
[variantMap]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Check if a variant exists for given property values
|
|
70
|
+
const hasVariant = useCallback(
|
|
71
|
+
(propertyValues: Record<string, string>): boolean => {
|
|
72
|
+
const key = createVariantKey(propertyValues);
|
|
73
|
+
return variantMap.has(key);
|
|
74
|
+
},
|
|
75
|
+
[variantMap]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Handle cell click
|
|
79
|
+
const handleCellClick = useCallback(
|
|
80
|
+
(propertyValues: Record<string, string>) => {
|
|
81
|
+
const variantId = getVariantId(propertyValues);
|
|
82
|
+
if (variantId) {
|
|
83
|
+
onSelectVariant(variantId);
|
|
84
|
+
} else if (onCreateVariant) {
|
|
85
|
+
onCreateVariant(propertyValues);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
[getVariantId, onSelectVariant, onCreateVariant]
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Render based on number of properties
|
|
92
|
+
if (variantProperties.length === 0) {
|
|
93
|
+
return (
|
|
94
|
+
<div className="text-center py-4 text-gray-500 text-sm">
|
|
95
|
+
No variant properties defined
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (variantProperties.length === 1) {
|
|
101
|
+
return (
|
|
102
|
+
<SinglePropertyList
|
|
103
|
+
property={variantProperties[0]}
|
|
104
|
+
variantMap={variantMap}
|
|
105
|
+
selectedVariantId={selectedVariantId}
|
|
106
|
+
onCellClick={handleCellClick}
|
|
107
|
+
hasVariant={hasVariant}
|
|
108
|
+
canCreate={!!onCreateVariant}
|
|
109
|
+
compact={compact}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (variantProperties.length === 2) {
|
|
115
|
+
return (
|
|
116
|
+
<TwoPropertyGrid
|
|
117
|
+
properties={variantProperties}
|
|
118
|
+
variantMap={variantMap}
|
|
119
|
+
selectedVariantId={selectedVariantId}
|
|
120
|
+
onCellClick={handleCellClick}
|
|
121
|
+
hasVariant={hasVariant}
|
|
122
|
+
canCreate={!!onCreateVariant}
|
|
123
|
+
compact={compact}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// For 3+ properties, use a multi-dimensional view
|
|
129
|
+
return (
|
|
130
|
+
<MultiPropertyGrid
|
|
131
|
+
properties={variantProperties}
|
|
132
|
+
variantMap={variantMap}
|
|
133
|
+
selectedVariantId={selectedVariantId}
|
|
134
|
+
onCellClick={handleCellClick}
|
|
135
|
+
hasVariant={hasVariant}
|
|
136
|
+
canCreate={!!onCreateVariant}
|
|
137
|
+
compact={compact}
|
|
138
|
+
/>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// =============================================================================
|
|
143
|
+
// Single Property List (1D)
|
|
144
|
+
// =============================================================================
|
|
145
|
+
|
|
146
|
+
interface SinglePropertyListProps {
|
|
147
|
+
property: VariantProperty;
|
|
148
|
+
variantMap: Map<string, VariantDefinition>;
|
|
149
|
+
selectedVariantId: string | null;
|
|
150
|
+
onCellClick: (propertyValues: Record<string, string>) => void;
|
|
151
|
+
hasVariant: (propertyValues: Record<string, string>) => boolean;
|
|
152
|
+
canCreate: boolean;
|
|
153
|
+
compact: boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function SinglePropertyList({
|
|
157
|
+
property,
|
|
158
|
+
variantMap,
|
|
159
|
+
selectedVariantId,
|
|
160
|
+
onCellClick,
|
|
161
|
+
hasVariant,
|
|
162
|
+
canCreate,
|
|
163
|
+
compact,
|
|
164
|
+
}: SinglePropertyListProps) {
|
|
165
|
+
return (
|
|
166
|
+
<div className="space-y-1">
|
|
167
|
+
<div className="text-xs font-medium text-gray-400 mb-2">
|
|
168
|
+
{property.name}
|
|
169
|
+
</div>
|
|
170
|
+
<div className={cn('grid gap-1', compact ? 'grid-cols-4' : 'grid-cols-3')}>
|
|
171
|
+
{property.values.map((value) => {
|
|
172
|
+
const propertyValues = { [property.name]: value };
|
|
173
|
+
const key = createVariantKey(propertyValues);
|
|
174
|
+
const variant = variantMap.get(key);
|
|
175
|
+
const isSelected = variant?.id === selectedVariantId;
|
|
176
|
+
const exists = hasVariant(propertyValues);
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<VariantCell
|
|
180
|
+
key={value}
|
|
181
|
+
label={value}
|
|
182
|
+
exists={exists}
|
|
183
|
+
isSelected={isSelected}
|
|
184
|
+
canCreate={canCreate}
|
|
185
|
+
onClick={() => onCellClick(propertyValues)}
|
|
186
|
+
compact={compact}
|
|
187
|
+
/>
|
|
188
|
+
);
|
|
189
|
+
})}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// =============================================================================
|
|
196
|
+
// Two Property Grid (2D)
|
|
197
|
+
// =============================================================================
|
|
198
|
+
|
|
199
|
+
interface TwoPropertyGridProps {
|
|
200
|
+
properties: VariantProperty[];
|
|
201
|
+
variantMap: Map<string, VariantDefinition>;
|
|
202
|
+
selectedVariantId: string | null;
|
|
203
|
+
onCellClick: (propertyValues: Record<string, string>) => void;
|
|
204
|
+
hasVariant: (propertyValues: Record<string, string>) => boolean;
|
|
205
|
+
canCreate: boolean;
|
|
206
|
+
compact: boolean;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function TwoPropertyGrid({
|
|
210
|
+
properties,
|
|
211
|
+
variantMap,
|
|
212
|
+
selectedVariantId,
|
|
213
|
+
onCellClick,
|
|
214
|
+
hasVariant,
|
|
215
|
+
canCreate,
|
|
216
|
+
compact,
|
|
217
|
+
}: TwoPropertyGridProps) {
|
|
218
|
+
const [rowProperty, colProperty] = properties;
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<div className="overflow-x-auto">
|
|
222
|
+
<table className="w-full border-collapse">
|
|
223
|
+
<thead>
|
|
224
|
+
<tr>
|
|
225
|
+
<th className="p-1.5 text-xs font-medium text-gray-500 text-left border-b border-[#444444]">
|
|
226
|
+
{rowProperty.name} / {colProperty.name}
|
|
227
|
+
</th>
|
|
228
|
+
{colProperty.values.map((colValue) => (
|
|
229
|
+
<th
|
|
230
|
+
key={colValue}
|
|
231
|
+
className="p-1.5 text-xs font-medium text-gray-400 text-center border-b border-[#444444]"
|
|
232
|
+
>
|
|
233
|
+
{colValue}
|
|
234
|
+
</th>
|
|
235
|
+
))}
|
|
236
|
+
</tr>
|
|
237
|
+
</thead>
|
|
238
|
+
<tbody>
|
|
239
|
+
{rowProperty.values.map((rowValue) => (
|
|
240
|
+
<tr key={rowValue}>
|
|
241
|
+
<td className="p-1.5 text-xs font-medium text-gray-400 border-b border-[#444444]">
|
|
242
|
+
{rowValue}
|
|
243
|
+
</td>
|
|
244
|
+
{colProperty.values.map((colValue) => {
|
|
245
|
+
const propertyValues = {
|
|
246
|
+
[rowProperty.name]: rowValue,
|
|
247
|
+
[colProperty.name]: colValue,
|
|
248
|
+
};
|
|
249
|
+
const key = createVariantKey(propertyValues);
|
|
250
|
+
const variant = variantMap.get(key);
|
|
251
|
+
const isSelected = variant?.id === selectedVariantId;
|
|
252
|
+
const exists = hasVariant(propertyValues);
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<td
|
|
256
|
+
key={colValue}
|
|
257
|
+
className="p-1 text-center border-b border-[#444444]"
|
|
258
|
+
>
|
|
259
|
+
<VariantCell
|
|
260
|
+
exists={exists}
|
|
261
|
+
isSelected={isSelected}
|
|
262
|
+
canCreate={canCreate}
|
|
263
|
+
onClick={() => onCellClick(propertyValues)}
|
|
264
|
+
compact={compact}
|
|
265
|
+
isGridCell
|
|
266
|
+
/>
|
|
267
|
+
</td>
|
|
268
|
+
);
|
|
269
|
+
})}
|
|
270
|
+
</tr>
|
|
271
|
+
))}
|
|
272
|
+
</tbody>
|
|
273
|
+
</table>
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// =============================================================================
|
|
279
|
+
// Multi Property Grid (3+ dimensions)
|
|
280
|
+
// =============================================================================
|
|
281
|
+
|
|
282
|
+
interface MultiPropertyGridProps {
|
|
283
|
+
properties: VariantProperty[];
|
|
284
|
+
variantMap: Map<string, VariantDefinition>;
|
|
285
|
+
selectedVariantId: string | null;
|
|
286
|
+
onCellClick: (propertyValues: Record<string, string>) => void;
|
|
287
|
+
hasVariant: (propertyValues: Record<string, string>) => boolean;
|
|
288
|
+
canCreate: boolean;
|
|
289
|
+
compact: boolean;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function MultiPropertyGrid({
|
|
293
|
+
properties,
|
|
294
|
+
variantMap,
|
|
295
|
+
selectedVariantId,
|
|
296
|
+
onCellClick,
|
|
297
|
+
hasVariant,
|
|
298
|
+
canCreate,
|
|
299
|
+
compact,
|
|
300
|
+
}: MultiPropertyGridProps) {
|
|
301
|
+
// Generate all combinations
|
|
302
|
+
const combinations = useMemo(
|
|
303
|
+
() => generateVariantCombinations(properties),
|
|
304
|
+
[properties]
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// Group by the first property for organization
|
|
308
|
+
const groupedByFirst = useMemo(() => {
|
|
309
|
+
const groups = new Map<string, Record<string, string>[]>();
|
|
310
|
+
const firstProp = properties[0];
|
|
311
|
+
|
|
312
|
+
for (const combo of combinations) {
|
|
313
|
+
const groupKey = combo[firstProp.name];
|
|
314
|
+
if (!groups.has(groupKey)) {
|
|
315
|
+
groups.set(groupKey, []);
|
|
316
|
+
}
|
|
317
|
+
groups.get(groupKey)!.push(combo);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return groups;
|
|
321
|
+
}, [combinations, properties]);
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<div className="space-y-4">
|
|
325
|
+
{Array.from(groupedByFirst.entries()).map(([groupValue, combos]) => (
|
|
326
|
+
<div key={groupValue} className="border border-[#444444] rounded-lg p-2">
|
|
327
|
+
<div className="text-xs font-medium text-gray-400 mb-2 px-1">
|
|
328
|
+
{properties[0].name}: {groupValue}
|
|
329
|
+
</div>
|
|
330
|
+
<div className={cn('grid gap-1', compact ? 'grid-cols-4' : 'grid-cols-3')}>
|
|
331
|
+
{combos.map((combo) => {
|
|
332
|
+
const key = createVariantKey(combo);
|
|
333
|
+
const variant = variantMap.get(key);
|
|
334
|
+
const isSelected = variant?.id === selectedVariantId;
|
|
335
|
+
const exists = hasVariant(combo);
|
|
336
|
+
|
|
337
|
+
// Create label from remaining properties
|
|
338
|
+
const label = properties
|
|
339
|
+
.slice(1)
|
|
340
|
+
.map((p) => combo[p.name])
|
|
341
|
+
.join(', ');
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<VariantCell
|
|
345
|
+
key={key}
|
|
346
|
+
label={label}
|
|
347
|
+
exists={exists}
|
|
348
|
+
isSelected={isSelected}
|
|
349
|
+
canCreate={canCreate}
|
|
350
|
+
onClick={() => onCellClick(combo)}
|
|
351
|
+
compact={compact}
|
|
352
|
+
/>
|
|
353
|
+
);
|
|
354
|
+
})}
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
))}
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// =============================================================================
|
|
363
|
+
// Variant Cell Component
|
|
364
|
+
// =============================================================================
|
|
365
|
+
|
|
366
|
+
interface VariantCellProps {
|
|
367
|
+
/** Optional label to display */
|
|
368
|
+
label?: string;
|
|
369
|
+
/** Whether the variant exists */
|
|
370
|
+
exists: boolean;
|
|
371
|
+
/** Whether this cell is selected */
|
|
372
|
+
isSelected: boolean;
|
|
373
|
+
/** Whether new variants can be created */
|
|
374
|
+
canCreate: boolean;
|
|
375
|
+
/** Click handler */
|
|
376
|
+
onClick: () => void;
|
|
377
|
+
/** Compact display mode */
|
|
378
|
+
compact: boolean;
|
|
379
|
+
/** Whether this is a grid cell (minimal styling) */
|
|
380
|
+
isGridCell?: boolean;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function VariantCell({
|
|
384
|
+
label,
|
|
385
|
+
exists,
|
|
386
|
+
isSelected,
|
|
387
|
+
canCreate,
|
|
388
|
+
onClick,
|
|
389
|
+
compact,
|
|
390
|
+
isGridCell = false,
|
|
391
|
+
}: VariantCellProps) {
|
|
392
|
+
const baseClasses = cn(
|
|
393
|
+
'flex items-center justify-center cursor-pointer transition-all',
|
|
394
|
+
'rounded border',
|
|
395
|
+
isGridCell
|
|
396
|
+
? 'w-8 h-8 mx-auto'
|
|
397
|
+
: compact
|
|
398
|
+
? 'h-8 px-2'
|
|
399
|
+
: 'h-10 px-3',
|
|
400
|
+
// Exists state
|
|
401
|
+
exists
|
|
402
|
+
? [
|
|
403
|
+
'bg-[#2c2c2c] border-[#4a4a4a]',
|
|
404
|
+
'hover:bg-[#444444] hover:border-[#5d5d5d]',
|
|
405
|
+
]
|
|
406
|
+
: [
|
|
407
|
+
'bg-transparent border-dashed border-[#4a4a4a]',
|
|
408
|
+
canCreate
|
|
409
|
+
? 'hover:bg-[#2c2c2c] hover:border-[#5d5d5d]'
|
|
410
|
+
: 'opacity-50 cursor-not-allowed',
|
|
411
|
+
],
|
|
412
|
+
// Selected state
|
|
413
|
+
isSelected && 'ring-2 ring-[#0d99ff] border-[#0d99ff]'
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
return (
|
|
417
|
+
<button
|
|
418
|
+
type="button"
|
|
419
|
+
className={baseClasses}
|
|
420
|
+
onClick={onClick}
|
|
421
|
+
disabled={!exists && !canCreate}
|
|
422
|
+
aria-label={
|
|
423
|
+
label
|
|
424
|
+
? `Variant: ${label}${exists ? '' : ' (missing)'}${isSelected ? ' (selected)' : ''}`
|
|
425
|
+
: exists
|
|
426
|
+
? 'Variant exists'
|
|
427
|
+
: 'Variant missing'
|
|
428
|
+
}
|
|
429
|
+
aria-pressed={isSelected}
|
|
430
|
+
>
|
|
431
|
+
{exists ? (
|
|
432
|
+
<>
|
|
433
|
+
{isGridCell ? (
|
|
434
|
+
<Check className="w-4 h-4 text-green-400" />
|
|
435
|
+
) : (
|
|
436
|
+
<div className="flex items-center gap-1.5">
|
|
437
|
+
<Check className="w-3.5 h-3.5 text-green-400 flex-shrink-0" />
|
|
438
|
+
{label && (
|
|
439
|
+
<span className="text-xs text-gray-300 truncate">{label}</span>
|
|
440
|
+
)}
|
|
441
|
+
</div>
|
|
442
|
+
)}
|
|
443
|
+
</>
|
|
444
|
+
) : canCreate ? (
|
|
445
|
+
<>
|
|
446
|
+
{isGridCell ? (
|
|
447
|
+
<Plus className="w-4 h-4 text-gray-500" />
|
|
448
|
+
) : (
|
|
449
|
+
<div className="flex items-center gap-1.5">
|
|
450
|
+
<Plus className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
|
|
451
|
+
{label && (
|
|
452
|
+
<span className="text-xs text-gray-500 truncate">{label}</span>
|
|
453
|
+
)}
|
|
454
|
+
</div>
|
|
455
|
+
)}
|
|
456
|
+
</>
|
|
457
|
+
) : (
|
|
458
|
+
<>
|
|
459
|
+
{!isGridCell && label && (
|
|
460
|
+
<span className="text-xs text-gray-600 truncate">{label}</span>
|
|
461
|
+
)}
|
|
462
|
+
</>
|
|
463
|
+
)}
|
|
464
|
+
</button>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// =============================================================================
|
|
469
|
+
// Stats Component
|
|
470
|
+
// =============================================================================
|
|
471
|
+
|
|
472
|
+
interface VariantMatrixStatsProps {
|
|
473
|
+
variantProperties: VariantProperty[];
|
|
474
|
+
variants: VariantDefinition[];
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Display statistics about the variant matrix.
|
|
479
|
+
*/
|
|
480
|
+
export function VariantMatrixStats({
|
|
481
|
+
variantProperties,
|
|
482
|
+
variants,
|
|
483
|
+
}: VariantMatrixStatsProps) {
|
|
484
|
+
const totalPossible = useMemo(() => {
|
|
485
|
+
return generateVariantCombinations(variantProperties).length;
|
|
486
|
+
}, [variantProperties]);
|
|
487
|
+
|
|
488
|
+
const defined = variants.length;
|
|
489
|
+
const missing = totalPossible - defined;
|
|
490
|
+
const percentage = totalPossible > 0 ? Math.round((defined / totalPossible) * 100) : 0;
|
|
491
|
+
|
|
492
|
+
return (
|
|
493
|
+
<div className="flex items-center gap-4 text-xs text-gray-500">
|
|
494
|
+
<span>
|
|
495
|
+
<span className="text-green-400 font-medium">{defined}</span> / {totalPossible} variants
|
|
496
|
+
</span>
|
|
497
|
+
{missing > 0 && (
|
|
498
|
+
<span className="text-amber-400">
|
|
499
|
+
{missing} missing
|
|
500
|
+
</span>
|
|
501
|
+
)}
|
|
502
|
+
<span>{percentage}% complete</span>
|
|
503
|
+
</div>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export default VariantMatrix;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CategoryAccordion - Collapsible category section for components
|
|
5
|
+
*
|
|
6
|
+
* Displays a category header that can be expanded/collapsed
|
|
7
|
+
* to show the components within that category.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useCallback } from 'react';
|
|
11
|
+
import { ChevronRight, ChevronDown } from 'lucide-react';
|
|
12
|
+
import type {
|
|
13
|
+
ComponentLibraryCategory,
|
|
14
|
+
MasterComponent,
|
|
15
|
+
} from '../../../types/editor-components';
|
|
16
|
+
import { ComponentGrid } from './ComponentGrid';
|
|
17
|
+
|
|
18
|
+
interface CategoryAccordionProps {
|
|
19
|
+
/** The category to display */
|
|
20
|
+
category: ComponentLibraryCategory;
|
|
21
|
+
/** Components in this category */
|
|
22
|
+
components: (MasterComponent | undefined)[];
|
|
23
|
+
/** Whether the category is expanded */
|
|
24
|
+
isExpanded: boolean;
|
|
25
|
+
/** Callback to toggle expansion */
|
|
26
|
+
onToggle: () => void;
|
|
27
|
+
/** ID of the selected component (for highlighting) */
|
|
28
|
+
selectedComponentId?: string | null;
|
|
29
|
+
/** Callback when selection is cleared */
|
|
30
|
+
onClearSelection?: () => void;
|
|
31
|
+
/** Callback when a component is double-clicked (to edit) */
|
|
32
|
+
onEditComponent?: (component: MasterComponent) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get icon component for category
|
|
37
|
+
*/
|
|
38
|
+
function getCategoryIcon(iconName: string): string {
|
|
39
|
+
const iconMap: Record<string, string> = {
|
|
40
|
+
layout: '#3b82f6',
|
|
41
|
+
navigation: '#10b981',
|
|
42
|
+
content: '#f59e0b',
|
|
43
|
+
'form-input': '#ef4444',
|
|
44
|
+
form: '#ef4444',
|
|
45
|
+
image: '#8b5cf6',
|
|
46
|
+
media: '#8b5cf6',
|
|
47
|
+
};
|
|
48
|
+
return iconMap[iconName] || '#6b7280';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* CategoryAccordion displays a collapsible section for a component category.
|
|
53
|
+
* Supports keyboard navigation and accessibility.
|
|
54
|
+
*/
|
|
55
|
+
export function CategoryAccordion({
|
|
56
|
+
category,
|
|
57
|
+
components,
|
|
58
|
+
isExpanded,
|
|
59
|
+
onToggle,
|
|
60
|
+
selectedComponentId,
|
|
61
|
+
onClearSelection,
|
|
62
|
+
onEditComponent,
|
|
63
|
+
}: CategoryAccordionProps) {
|
|
64
|
+
const validComponents = components.filter(
|
|
65
|
+
(c): c is MasterComponent => c !== undefined
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const handleKeyDown = useCallback(
|
|
69
|
+
(e: React.KeyboardEvent) => {
|
|
70
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
onToggle();
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
[onToggle]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const iconColor = category.color || getCategoryIcon(category.icon || 'default');
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="mb-1">
|
|
82
|
+
{/* Category Header */}
|
|
83
|
+
<button
|
|
84
|
+
className="w-full flex items-center gap-2 py-1.5 px-2 rounded text-xs text-gray-300 hover:bg-[#444444] transition-colors focus:outline-none focus:ring-1 focus:ring-[#0d99ff]"
|
|
85
|
+
onClick={onToggle}
|
|
86
|
+
onKeyDown={handleKeyDown}
|
|
87
|
+
aria-expanded={isExpanded}
|
|
88
|
+
aria-controls={`category-content-${category.id}`}
|
|
89
|
+
>
|
|
90
|
+
{/* Expand/Collapse Icon */}
|
|
91
|
+
{isExpanded ? (
|
|
92
|
+
<ChevronDown className="w-3 h-3 text-gray-500 flex-shrink-0" />
|
|
93
|
+
) : (
|
|
94
|
+
<ChevronRight className="w-3 h-3 text-gray-500 flex-shrink-0" />
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{/* Category Color Indicator */}
|
|
98
|
+
<div
|
|
99
|
+
className="w-2 h-2 rounded-sm flex-shrink-0"
|
|
100
|
+
style={{ backgroundColor: iconColor }}
|
|
101
|
+
/>
|
|
102
|
+
|
|
103
|
+
{/* Category Name */}
|
|
104
|
+
<span className="font-medium flex-1 text-left">{category.name}</span>
|
|
105
|
+
|
|
106
|
+
{/* Component Count */}
|
|
107
|
+
<span className="text-[10px] text-gray-500 tabular-nums">
|
|
108
|
+
{validComponents.length}
|
|
109
|
+
</span>
|
|
110
|
+
</button>
|
|
111
|
+
|
|
112
|
+
{/* Category Content */}
|
|
113
|
+
{isExpanded && (
|
|
114
|
+
<div
|
|
115
|
+
id={`category-content-${category.id}`}
|
|
116
|
+
className="mt-1 ml-4 overflow-hidden animate-in fade-in slide-in-from-top-1 duration-200"
|
|
117
|
+
role="region"
|
|
118
|
+
aria-label={`${category.name}のコンポーネント`}
|
|
119
|
+
>
|
|
120
|
+
{validComponents.length > 0 ? (
|
|
121
|
+
<ComponentGrid
|
|
122
|
+
components={validComponents}
|
|
123
|
+
selectedComponentId={selectedComponentId}
|
|
124
|
+
onEditComponent={onEditComponent}
|
|
125
|
+
/>
|
|
126
|
+
) : (
|
|
127
|
+
<div className="py-3 text-center">
|
|
128
|
+
<p className="text-[10px] text-gray-500">
|
|
129
|
+
このカテゴリにコンポーネントがありません
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export default CategoryAccordion;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ComponentGrid - Grid display for components in a category
|
|
5
|
+
*
|
|
6
|
+
* Displays components in a responsive grid layout.
|
|
7
|
+
* Each component is shown as a draggable item.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import type { MasterComponent } from '../../../types/editor-components';
|
|
12
|
+
import { ComponentItem } from './ComponentItem';
|
|
13
|
+
|
|
14
|
+
interface ComponentGridProps {
|
|
15
|
+
/** Components to display */
|
|
16
|
+
components: MasterComponent[];
|
|
17
|
+
/** Number of columns in the grid */
|
|
18
|
+
columns?: number;
|
|
19
|
+
/** ID of the selected component (for highlighting) */
|
|
20
|
+
selectedComponentId?: string | null;
|
|
21
|
+
/** Callback when a component is double-clicked (to edit) */
|
|
22
|
+
onEditComponent?: (component: MasterComponent) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* ComponentGrid displays components in a grid layout.
|
|
27
|
+
* Components can be dragged onto the canvas to create instances.
|
|
28
|
+
*/
|
|
29
|
+
export function ComponentGrid({ components, columns = 2, selectedComponentId, onEditComponent }: ComponentGridProps) {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
className="grid gap-2"
|
|
33
|
+
style={{
|
|
34
|
+
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
|
|
35
|
+
}}
|
|
36
|
+
role="list"
|
|
37
|
+
aria-label="コンポーネント"
|
|
38
|
+
>
|
|
39
|
+
{components.map((component) => (
|
|
40
|
+
<ComponentItem
|
|
41
|
+
key={component.id}
|
|
42
|
+
component={component}
|
|
43
|
+
isSelected={component.id === selectedComponentId}
|
|
44
|
+
onDoubleClick={onEditComponent}
|
|
45
|
+
/>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default ComponentGrid;
|