@growgroup/visual-editor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +52 -0
  3. package/README.md +143 -0
  4. package/dist/editor.css +2 -0
  5. package/package.json +94 -0
  6. package/src/components/DeckSlideRender.tsx +21 -0
  7. package/src/components/SaveNote.tsx +21 -0
  8. package/src/components/auth/AuthProvider.tsx +44 -0
  9. package/src/components/ui/accordion.tsx +57 -0
  10. package/src/components/ui/alert-dialog.tsx +141 -0
  11. package/src/components/ui/badge.tsx +36 -0
  12. package/src/components/ui/button.tsx +56 -0
  13. package/src/components/ui/card.tsx +76 -0
  14. package/src/components/ui/checkbox.tsx +30 -0
  15. package/src/components/ui/collapsible.tsx +11 -0
  16. package/src/components/ui/context-menu.tsx +200 -0
  17. package/src/components/ui/dialog.tsx +122 -0
  18. package/src/components/ui/dropdown-menu.tsx +201 -0
  19. package/src/components/ui/input.tsx +22 -0
  20. package/src/components/ui/label.tsx +26 -0
  21. package/src/components/ui/popover.tsx +33 -0
  22. package/src/components/ui/scroll-area.tsx +48 -0
  23. package/src/components/ui/scrubbable-label.tsx +95 -0
  24. package/src/components/ui/select.tsx +159 -0
  25. package/src/components/ui/separator.tsx +31 -0
  26. package/src/components/ui/slider.tsx +28 -0
  27. package/src/components/ui/switch.tsx +29 -0
  28. package/src/components/ui/tabs.tsx +55 -0
  29. package/src/components/ui/textarea.tsx +22 -0
  30. package/src/components/ui/tooltip.tsx +32 -0
  31. package/src/components/viewer/useDeck.ts +58 -0
  32. package/src/editor/EditorContext.tsx +632 -0
  33. package/src/editor/EditorToolbar.tsx +446 -0
  34. package/src/editor/FrontendVisualEditor.tsx +2595 -0
  35. package/src/editor/autosave.ts +34 -0
  36. package/src/editor/components/AiPromptPopover.tsx +378 -0
  37. package/src/editor/components/BreakpointGuides.tsx +323 -0
  38. package/src/editor/components/BreakpointSelector.tsx +180 -0
  39. package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
  40. package/src/editor/components/CssEditorDialog.tsx +168 -0
  41. package/src/editor/components/EditorCanvas.tsx +987 -0
  42. package/src/editor/components/EditorContextMenu.tsx +515 -0
  43. package/src/editor/components/EditorFooter.tsx +81 -0
  44. package/src/editor/components/EditorHeader.tsx +524 -0
  45. package/src/editor/components/EditorLayerPanel.tsx +1197 -0
  46. package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
  47. package/src/editor/components/EditorSidebar.tsx +226 -0
  48. package/src/editor/components/FigmaColorPicker.tsx +1577 -0
  49. package/src/editor/components/HtmlEditorDialog.tsx +71 -0
  50. package/src/editor/components/HtmlImportDialog.tsx +346 -0
  51. package/src/editor/components/JsEditorDialog.tsx +175 -0
  52. package/src/editor/components/LayerTreeItem.tsx +494 -0
  53. package/src/editor/components/MasterComponentEditor.tsx +846 -0
  54. package/src/editor/components/MediaLibraryDialog.tsx +447 -0
  55. package/src/editor/components/PageSettingsDialog.tsx +776 -0
  56. package/src/editor/components/VariablePicker.tsx +355 -0
  57. package/src/editor/components/VariablesDialog.tsx +564 -0
  58. package/src/editor/components/VariantEditor.tsx +687 -0
  59. package/src/editor/components/VariantMatrix.tsx +507 -0
  60. package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
  61. package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
  62. package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
  63. package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
  64. package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
  65. package/src/editor/components/component-panel/VariantList.tsx +260 -0
  66. package/src/editor/components/component-panel/index.ts +12 -0
  67. package/src/editor/components/index.ts +48 -0
  68. package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
  69. package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
  70. package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
  71. package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
  72. package/src/editor/components/multi-page/PageLabel.tsx +34 -0
  73. package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
  74. package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
  75. package/src/editor/components/multi-page/index.ts +7 -0
  76. package/src/editor/components/ppt/PptChrome.tsx +2508 -0
  77. package/src/editor/components/ppt/PptComments.tsx +589 -0
  78. package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
  79. package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
  80. package/src/editor/components/ppt/PptNotes.tsx +246 -0
  81. package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
  82. package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
  83. package/src/editor/components/property-panel/BorderSection.tsx +104 -0
  84. package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
  85. package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
  86. package/src/editor/components/property-panel/EffectSection.tsx +157 -0
  87. package/src/editor/components/property-panel/FillSection.tsx +69 -0
  88. package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
  89. package/src/editor/components/property-panel/ImageSection.tsx +206 -0
  90. package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
  91. package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
  92. package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
  93. package/src/editor/components/property-panel/LinkSection.tsx +103 -0
  94. package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
  95. package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
  96. package/src/editor/components/property-panel/TypographySection.tsx +230 -0
  97. package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
  98. package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
  99. package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
  100. package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
  101. package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
  102. package/src/editor/components/property-panel/index.ts +24 -0
  103. package/src/editor/components/property-panel/unit-utils.ts +261 -0
  104. package/src/editor/components/shell/EditorTopBar.tsx +392 -0
  105. package/src/editor/components/shell/LeftPanel.tsx +47 -0
  106. package/src/editor/components/shell/PagesPanel.tsx +118 -0
  107. package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
  108. package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
  109. package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
  110. package/src/editor/components/variables-panel/index.ts +7 -0
  111. package/src/editor/constants.ts +468 -0
  112. package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
  113. package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
  114. package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
  115. package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
  116. package/src/editor/contexts/EditorRefsContext.tsx +204 -0
  117. package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
  118. package/src/editor/contexts/EditorToolContext.tsx +58 -0
  119. package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
  120. package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
  121. package/src/editor/contexts/EditorViewContext.tsx +101 -0
  122. package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
  123. package/src/editor/contexts/index.ts +17 -0
  124. package/src/editor/editor-skin.css +122 -0
  125. package/src/editor/hooks/index.ts +38 -0
  126. package/src/editor/hooks/useAiReplace.ts +397 -0
  127. package/src/editor/hooks/useAltMeasure.ts +67 -0
  128. package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
  129. package/src/editor/hooks/useCanvasControls.ts +1222 -0
  130. package/src/editor/hooks/useComponentEditMode.ts +658 -0
  131. package/src/editor/hooks/useComponentInstances.ts +515 -0
  132. package/src/editor/hooks/useContextMenuHandler.ts +157 -0
  133. package/src/editor/hooks/useCoordinateTransform.ts +207 -0
  134. package/src/editor/hooks/useDragResize.ts +1317 -0
  135. package/src/editor/hooks/useDrawingMode.ts +472 -0
  136. package/src/editor/hooks/useEditorColors.ts +144 -0
  137. package/src/editor/hooks/useEditorMessages.ts +108 -0
  138. package/src/editor/hooks/useElementActions.ts +1754 -0
  139. package/src/editor/hooks/useElementSelection.ts +1573 -0
  140. package/src/editor/hooks/useFocusManagement.ts +249 -0
  141. package/src/editor/hooks/useGoogleFonts.ts +305 -0
  142. package/src/editor/hooks/useIframeInitializer.ts +584 -0
  143. package/src/editor/hooks/useIframeSetup.ts +360 -0
  144. package/src/editor/hooks/useImageUpload.ts +270 -0
  145. package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
  146. package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
  147. package/src/editor/hooks/useMarqueeSelection.ts +388 -0
  148. package/src/editor/hooks/useMediaLibrary.ts +129 -0
  149. package/src/editor/hooks/usePageSettingsManager.ts +739 -0
  150. package/src/editor/hooks/useResizablePanel.ts +163 -0
  151. package/src/editor/hooks/useRichPaste.ts +255 -0
  152. package/src/editor/hooks/useTouchGestures.ts +197 -0
  153. package/src/editor/index.ts +27 -0
  154. package/src/editor/types/page-settings.ts +513 -0
  155. package/src/editor/types.ts +382 -0
  156. package/src/editor/utils/align-elements.ts +169 -0
  157. package/src/editor/utils/api-json.ts +21 -0
  158. package/src/editor/utils/aspect-lock.ts +50 -0
  159. package/src/editor/utils/component-renderer.ts +702 -0
  160. package/src/editor/utils/component-sync.ts +845 -0
  161. package/src/editor/utils/crop-mode.ts +265 -0
  162. package/src/editor/utils/dom-utils.ts +2238 -0
  163. package/src/editor/utils/element-effects.ts +218 -0
  164. package/src/editor/utils/eyedropper.ts +35 -0
  165. package/src/editor/utils/figma-export.ts +560 -0
  166. package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
  167. package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
  168. package/src/editor/utils/figma-paste.ts +2262 -0
  169. package/src/editor/utils/flex-reorder.ts +231 -0
  170. package/src/editor/utils/flex-utils.ts +1230 -0
  171. package/src/editor/utils/geometry.ts +232 -0
  172. package/src/editor/utils/grid-layout.ts +210 -0
  173. package/src/editor/utils/html-utils.ts +470 -0
  174. package/src/editor/utils/index.ts +11 -0
  175. package/src/editor/utils/ink-style.ts +57 -0
  176. package/src/editor/utils/inline-format.ts +236 -0
  177. package/src/editor/utils/measure-distance.ts +120 -0
  178. package/src/editor/utils/override-apply.ts +526 -0
  179. package/src/editor/utils/override-detection.ts +613 -0
  180. package/src/editor/utils/paste-processors.ts +609 -0
  181. package/src/editor/utils/paste-sanitizer.ts +244 -0
  182. package/src/editor/utils/restore-flow.ts +188 -0
  183. package/src/editor/utils/shape-library.ts +171 -0
  184. package/src/editor/utils/slide-root.ts +29 -0
  185. package/src/editor/utils/smart-guides.ts +218 -0
  186. package/src/editor/utils/style-utils.ts +685 -0
  187. package/src/editor/utils/table-edit.ts +151 -0
  188. package/src/editor/utils/tailwind-mappings.ts +778 -0
  189. package/src/editor/utils/tailwind-utils.ts +804 -0
  190. package/src/editor/utils/text-highlight.ts +216 -0
  191. package/src/editor/utils/theme-to-variables.ts +341 -0
  192. package/src/editor/utils/variant-resolver.ts +456 -0
  193. package/src/editor/utils/viewport-utils.ts +295 -0
  194. package/src/hooks/useEditorHistory.ts +195 -0
  195. package/src/hooks/useEditorShortcuts.ts +260 -0
  196. package/src/index.ts +40 -0
  197. package/src/io.ts +134 -0
  198. package/src/lib/agent/slide-agent/types.ts +547 -0
  199. package/src/lib/agent/website-agent/types.ts +805 -0
  200. package/src/lib/api/auth-fetch.ts +58 -0
  201. package/src/lib/deck.ts +69 -0
  202. package/src/lib/export.ts +47 -0
  203. package/src/lib/firebase/config.ts +29 -0
  204. package/src/lib/firebase/css-variables.ts +378 -0
  205. package/src/lib/firebase/editor-components.ts +677 -0
  206. package/src/lib/firebase/storage.ts +469 -0
  207. package/src/lib/utils.ts +6 -0
  208. package/src/styles/editor.css +52 -0
  209. package/src/styles/skin.css +122 -0
  210. package/src/types/css-variables.ts +645 -0
  211. package/src/types/editor-components.ts +944 -0
  212. package/src/types/editor.ts +420 -0
  213. package/src/types/page-master.ts +197 -0
  214. package/src/types/page.ts +419 -0
  215. package/src/types/slide.ts +402 -0
  216. package/src/types/website-theme.ts +788 -0
  217. package/src/types/website.ts +374 -0
  218. package/src/vendor/firebase-firestore.ts +13 -0
  219. package/src/vendor/firebase-functions.ts +17 -0
@@ -0,0 +1,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;