@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,368 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ComponentItem - Single component thumbnail item
|
|
5
|
+
*
|
|
6
|
+
* Displays a component with:
|
|
7
|
+
* - Thumbnail preview (actual component rendering)
|
|
8
|
+
* - Component name
|
|
9
|
+
* - Drag start handler for drag-and-drop
|
|
10
|
+
*
|
|
11
|
+
* Supports keyboard navigation and accessibility.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, { useCallback, useState, useRef, memo, useEffect } from 'react';
|
|
15
|
+
import { GripVertical, Component as ComponentIcon, ChevronDown, ChevronRight } from 'lucide-react';
|
|
16
|
+
import type { MasterComponent, ComponentElement } from '../../../types/editor-components';
|
|
17
|
+
import { createDOMElement } from '../../utils/component-renderer';
|
|
18
|
+
import { VariantList } from './VariantList';
|
|
19
|
+
|
|
20
|
+
interface ComponentItemProps {
|
|
21
|
+
/** The component to display */
|
|
22
|
+
component: MasterComponent;
|
|
23
|
+
/** Callback when the component is clicked */
|
|
24
|
+
onClick?: (component: MasterComponent) => void;
|
|
25
|
+
/** Callback when the component is double-clicked (to edit) */
|
|
26
|
+
onDoubleClick?: (component: MasterComponent) => void;
|
|
27
|
+
/** Whether this component is selected (highlighted) */
|
|
28
|
+
isSelected?: boolean;
|
|
29
|
+
/** Whether variants are expanded (controlled) */
|
|
30
|
+
variantsExpanded?: boolean;
|
|
31
|
+
/** Callback when variants expansion is toggled */
|
|
32
|
+
onToggleVariants?: (componentId: string) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Calculate the scale factor to fit the element within the container
|
|
37
|
+
*/
|
|
38
|
+
function calculateScale(
|
|
39
|
+
elementWidth: number,
|
|
40
|
+
elementHeight: number,
|
|
41
|
+
containerWidth: number,
|
|
42
|
+
containerHeight: number,
|
|
43
|
+
padding: number = 8
|
|
44
|
+
): number {
|
|
45
|
+
const availableWidth = containerWidth - padding * 2;
|
|
46
|
+
const availableHeight = containerHeight - padding * 2;
|
|
47
|
+
|
|
48
|
+
const scaleX = availableWidth / elementWidth;
|
|
49
|
+
const scaleY = availableHeight / elementHeight;
|
|
50
|
+
|
|
51
|
+
// Use the smaller scale to ensure the element fits
|
|
52
|
+
return Math.min(scaleX, scaleY, 1); // Cap at 1 to not enlarge small elements
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Render actual component preview using createDOMElement
|
|
57
|
+
*/
|
|
58
|
+
function ComponentThumbnail({
|
|
59
|
+
component,
|
|
60
|
+
}: {
|
|
61
|
+
component: MasterComponent;
|
|
62
|
+
}) {
|
|
63
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
64
|
+
const [hasError, setHasError] = useState(false);
|
|
65
|
+
const [isReady, setIsReady] = useState(false);
|
|
66
|
+
|
|
67
|
+
// Get the default variant's root element
|
|
68
|
+
const defaultVariant = component.variants.find(
|
|
69
|
+
(v) => v.id === component.defaultVariantId
|
|
70
|
+
);
|
|
71
|
+
const rootElement = defaultVariant?.rootElement;
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!containerRef.current || !rootElement) {
|
|
75
|
+
setHasError(true);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setHasError(false);
|
|
80
|
+
setIsReady(false);
|
|
81
|
+
|
|
82
|
+
const container = containerRef.current;
|
|
83
|
+
|
|
84
|
+
// Clear previous content
|
|
85
|
+
container.innerHTML = '';
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// Create the DOM element from the component structure
|
|
89
|
+
const domElement = createDOMElement(rootElement);
|
|
90
|
+
|
|
91
|
+
// Create a wrapper for scaling
|
|
92
|
+
const wrapper = document.createElement('div');
|
|
93
|
+
wrapper.style.cssText = `
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
justify-content: center;
|
|
97
|
+
width: 100%;
|
|
98
|
+
height: 100%;
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
pointer-events: none;
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
// Create a scaling container
|
|
104
|
+
const scaleContainer = document.createElement('div');
|
|
105
|
+
scaleContainer.style.cssText = `
|
|
106
|
+
transform-origin: center center;
|
|
107
|
+
transition: transform 0.15s ease;
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
// Reset some styles that might interfere with preview
|
|
111
|
+
domElement.style.position = 'relative';
|
|
112
|
+
domElement.style.left = 'auto';
|
|
113
|
+
domElement.style.top = 'auto';
|
|
114
|
+
domElement.style.right = 'auto';
|
|
115
|
+
domElement.style.bottom = 'auto';
|
|
116
|
+
domElement.style.transform = 'none';
|
|
117
|
+
domElement.style.margin = '0';
|
|
118
|
+
|
|
119
|
+
scaleContainer.appendChild(domElement);
|
|
120
|
+
wrapper.appendChild(scaleContainer);
|
|
121
|
+
container.appendChild(wrapper);
|
|
122
|
+
|
|
123
|
+
// Wait for styles to apply, then calculate scale
|
|
124
|
+
requestAnimationFrame(() => {
|
|
125
|
+
const containerRect = container.getBoundingClientRect();
|
|
126
|
+
|
|
127
|
+
// [移植時の修正] スライド用の部品は実幅が1000px超のものがあり、
|
|
128
|
+
// そのまま縮小するとサムネイルが線一本になって中身が判別できない。
|
|
129
|
+
// プレビューのときだけ幅を詰めて、内容が見える縮尺に収める。
|
|
130
|
+
if (domElement.getBoundingClientRect().width > 360) {
|
|
131
|
+
domElement.style.width = '360px';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const elementRect = domElement.getBoundingClientRect();
|
|
135
|
+
|
|
136
|
+
// Only scale if the element has dimensions
|
|
137
|
+
if (elementRect.width > 0 && elementRect.height > 0) {
|
|
138
|
+
const scale = calculateScale(
|
|
139
|
+
elementRect.width,
|
|
140
|
+
elementRect.height,
|
|
141
|
+
containerRect.width,
|
|
142
|
+
containerRect.height,
|
|
143
|
+
4 // padding
|
|
144
|
+
);
|
|
145
|
+
scaleContainer.style.transform = `scale(${scale})`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setIsReady(true);
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('[ComponentThumbnail] Error creating preview:', error);
|
|
152
|
+
setHasError(true);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Cleanup
|
|
156
|
+
return () => {
|
|
157
|
+
if (container) {
|
|
158
|
+
container.innerHTML = '';
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}, [rootElement, component.id]);
|
|
162
|
+
|
|
163
|
+
// Fallback for components without valid rootElement
|
|
164
|
+
if (!rootElement || hasError) {
|
|
165
|
+
return (
|
|
166
|
+
<div className="w-full h-full flex items-center justify-center bg-[#1e1e1e]">
|
|
167
|
+
<ComponentIcon className="w-6 h-6 text-gray-600" />
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div
|
|
174
|
+
ref={containerRef}
|
|
175
|
+
className={`w-full h-full bg-white transition-opacity duration-200 ${
|
|
176
|
+
isReady ? 'opacity-100' : 'opacity-0'
|
|
177
|
+
}`}
|
|
178
|
+
style={{
|
|
179
|
+
// Provide a neutral background for previews
|
|
180
|
+
background: 'linear-gradient(45deg, #f0f0f0 25%, transparent 25%), linear-gradient(-45deg, #f0f0f0 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #f0f0f0 75%), linear-gradient(-45deg, transparent 75%, #f0f0f0 75%)',
|
|
181
|
+
backgroundSize: '8px 8px',
|
|
182
|
+
backgroundPosition: '0 0, 0 4px, 4px -4px, -4px 0px',
|
|
183
|
+
}}
|
|
184
|
+
/>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* ComponentItem displays a single draggable component.
|
|
190
|
+
* Supports drag-and-drop to add components to the canvas.
|
|
191
|
+
*/
|
|
192
|
+
export const ComponentItem = memo(function ComponentItem({
|
|
193
|
+
component,
|
|
194
|
+
onClick,
|
|
195
|
+
onDoubleClick,
|
|
196
|
+
isSelected = false,
|
|
197
|
+
variantsExpanded = false,
|
|
198
|
+
onToggleVariants,
|
|
199
|
+
}: ComponentItemProps) {
|
|
200
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
201
|
+
const [localExpanded, setLocalExpanded] = useState(false);
|
|
202
|
+
const itemRef = useRef<HTMLDivElement>(null);
|
|
203
|
+
|
|
204
|
+
// Use controlled or local expansion state
|
|
205
|
+
const isVariantsExpanded = onToggleVariants !== undefined ? variantsExpanded : localExpanded;
|
|
206
|
+
const hasMultipleVariants = component.variants.length > 1;
|
|
207
|
+
|
|
208
|
+
// Toggle variants expansion
|
|
209
|
+
const handleToggleVariants = useCallback((e: React.MouseEvent) => {
|
|
210
|
+
e.stopPropagation();
|
|
211
|
+
if (onToggleVariants) {
|
|
212
|
+
onToggleVariants(component.id);
|
|
213
|
+
} else {
|
|
214
|
+
setLocalExpanded(prev => !prev);
|
|
215
|
+
}
|
|
216
|
+
}, [component.id, onToggleVariants]);
|
|
217
|
+
|
|
218
|
+
// Auto-scroll to selected component
|
|
219
|
+
React.useEffect(() => {
|
|
220
|
+
if (isSelected && itemRef.current) {
|
|
221
|
+
itemRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
222
|
+
}
|
|
223
|
+
}, [isSelected]);
|
|
224
|
+
|
|
225
|
+
// Handle double click to edit
|
|
226
|
+
const handleDoubleClick = useCallback(() => {
|
|
227
|
+
onDoubleClick?.(component);
|
|
228
|
+
}, [onDoubleClick, component]);
|
|
229
|
+
|
|
230
|
+
// Handle drag start
|
|
231
|
+
const handleDragStart = useCallback(
|
|
232
|
+
(e: React.DragEvent<HTMLDivElement>) => {
|
|
233
|
+
setIsDragging(true);
|
|
234
|
+
|
|
235
|
+
// Set drag data with component info
|
|
236
|
+
e.dataTransfer.setData(
|
|
237
|
+
'application/x-editor-component',
|
|
238
|
+
JSON.stringify({
|
|
239
|
+
type: 'component',
|
|
240
|
+
componentId: component.id,
|
|
241
|
+
componentName: component.name,
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
e.dataTransfer.effectAllowed = 'copy';
|
|
245
|
+
|
|
246
|
+
// Create a custom drag image
|
|
247
|
+
const dragImage = document.createElement('div');
|
|
248
|
+
dragImage.className =
|
|
249
|
+
'bg-[#2c2c2c] border border-[#0d99ff] rounded px-2 py-1 text-xs text-white shadow-lg';
|
|
250
|
+
dragImage.textContent = component.name;
|
|
251
|
+
dragImage.style.position = 'absolute';
|
|
252
|
+
dragImage.style.top = '-1000px';
|
|
253
|
+
document.body.appendChild(dragImage);
|
|
254
|
+
e.dataTransfer.setDragImage(dragImage, 0, 0);
|
|
255
|
+
|
|
256
|
+
// Clean up drag image after a short delay
|
|
257
|
+
setTimeout(() => {
|
|
258
|
+
document.body.removeChild(dragImage);
|
|
259
|
+
}, 0);
|
|
260
|
+
},
|
|
261
|
+
[component.id, component.name]
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Handle drag end
|
|
265
|
+
const handleDragEnd = useCallback(() => {
|
|
266
|
+
setIsDragging(false);
|
|
267
|
+
}, []);
|
|
268
|
+
|
|
269
|
+
// Handle click
|
|
270
|
+
const handleClick = useCallback(() => {
|
|
271
|
+
onClick?.(component);
|
|
272
|
+
}, [onClick, component]);
|
|
273
|
+
|
|
274
|
+
// Handle keyboard interaction
|
|
275
|
+
const handleKeyDown = useCallback(
|
|
276
|
+
(e: React.KeyboardEvent) => {
|
|
277
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
onClick?.(component);
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
[onClick, component]
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<div
|
|
287
|
+
ref={itemRef}
|
|
288
|
+
className={`
|
|
289
|
+
group relative flex flex-col rounded border cursor-grab
|
|
290
|
+
transition-all duration-150
|
|
291
|
+
${
|
|
292
|
+
isDragging
|
|
293
|
+
? 'opacity-50 border-[#0d99ff] bg-[#0d99ff]/10'
|
|
294
|
+
: isSelected
|
|
295
|
+
? 'border-[#0d99ff] bg-[#0d99ff]/20 ring-1 ring-[#0d99ff]'
|
|
296
|
+
: 'border-[#444444] bg-[#2c2c2c] hover:border-gray-500 hover:bg-[#353535]'
|
|
297
|
+
}
|
|
298
|
+
focus:outline-none focus:ring-1 focus:ring-[#0d99ff]
|
|
299
|
+
`}
|
|
300
|
+
draggable
|
|
301
|
+
onDragStart={handleDragStart}
|
|
302
|
+
onDragEnd={handleDragEnd}
|
|
303
|
+
onClick={handleClick}
|
|
304
|
+
onDoubleClick={handleDoubleClick}
|
|
305
|
+
onKeyDown={handleKeyDown}
|
|
306
|
+
tabIndex={0}
|
|
307
|
+
role="listitem"
|
|
308
|
+
aria-label={`${component.name}コンポーネント。ドラッグしてキャンバスに追加。ダブルクリックで編集。`}
|
|
309
|
+
>
|
|
310
|
+
{/* Drag handle indicator */}
|
|
311
|
+
<div className="absolute top-1 left-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
312
|
+
<GripVertical className="w-3 h-3 text-gray-500" />
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
{/* Thumbnail */}
|
|
316
|
+
<div className="h-16 bg-[#1e1e1e] rounded-t overflow-hidden">
|
|
317
|
+
<ComponentThumbnail component={component} />
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
{/* Component name and info */}
|
|
321
|
+
<div className="px-2 py-1.5 border-t border-[#444444]">
|
|
322
|
+
<div className="flex items-center gap-1">
|
|
323
|
+
<p className="text-[10px] text-gray-300 truncate font-medium flex-1">
|
|
324
|
+
{component.name}
|
|
325
|
+
</p>
|
|
326
|
+
{hasMultipleVariants && (
|
|
327
|
+
<button
|
|
328
|
+
onClick={handleToggleVariants}
|
|
329
|
+
className="p-0.5 hover:bg-[#444444] rounded transition-colors flex-shrink-0"
|
|
330
|
+
aria-label={isVariantsExpanded ? 'バリアントを閉じる' : 'バリアントを開く'}
|
|
331
|
+
aria-expanded={isVariantsExpanded}
|
|
332
|
+
>
|
|
333
|
+
{isVariantsExpanded ? (
|
|
334
|
+
<ChevronDown className="w-3 h-3 text-purple-400" />
|
|
335
|
+
) : (
|
|
336
|
+
<ChevronRight className="w-3 h-3 text-purple-400" />
|
|
337
|
+
)}
|
|
338
|
+
</button>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
<div className="flex items-center gap-1 mt-0.5">
|
|
342
|
+
{hasMultipleVariants && (
|
|
343
|
+
<button
|
|
344
|
+
onClick={handleToggleVariants}
|
|
345
|
+
className="text-[8px] text-purple-400 bg-purple-500/20 px-1 rounded hover:bg-purple-500/30 transition-colors"
|
|
346
|
+
>
|
|
347
|
+
{component.variants.length}バリアント
|
|
348
|
+
</button>
|
|
349
|
+
)}
|
|
350
|
+
{component.tags && component.tags.length > 0 && (
|
|
351
|
+
<p className="text-[8px] text-gray-500 truncate flex-1">
|
|
352
|
+
{component.tags.slice(0, 2).join(', ')}
|
|
353
|
+
</p>
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
{/* Variant List */}
|
|
359
|
+
{hasMultipleVariants && isVariantsExpanded && (
|
|
360
|
+
<div className="px-2 pb-2 border-t border-[#444444]">
|
|
361
|
+
<VariantList component={component} isExpanded={true} />
|
|
362
|
+
</div>
|
|
363
|
+
)}
|
|
364
|
+
</div>
|
|
365
|
+
);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
export default ComponentItem;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ComponentPanel - Main panel container for the component system
|
|
5
|
+
*
|
|
6
|
+
* Displays a searchable, categorized list of reusable components.
|
|
7
|
+
* Components can be dragged onto the canvas to create instances.
|
|
8
|
+
*
|
|
9
|
+
* Layout:
|
|
10
|
+
* ┌───────────────────────────────────┐
|
|
11
|
+
* │ Components [x] │
|
|
12
|
+
* ├───────────────────────────────────┤
|
|
13
|
+
* │ Search components... │
|
|
14
|
+
* ├───────────────────────────────────┤
|
|
15
|
+
* │ Categories with components... │
|
|
16
|
+
* └───────────────────────────────────┘
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
|
20
|
+
import { X, Component } from 'lucide-react';
|
|
21
|
+
import { Button } from '../../../components/ui/button';
|
|
22
|
+
import { ScrollArea } from '../../../components/ui/scroll-area';
|
|
23
|
+
import { useEditorComponents } from '../../contexts/EditorComponentsContext';
|
|
24
|
+
import { useResizablePanel } from '../../hooks/useResizablePanel';
|
|
25
|
+
import { ComponentSearch } from './ComponentSearch';
|
|
26
|
+
import { CategoryAccordion } from './CategoryAccordion';
|
|
27
|
+
|
|
28
|
+
interface ComponentPanelProps {
|
|
29
|
+
/** Callback when the panel close button is clicked */
|
|
30
|
+
onClose?: () => void;
|
|
31
|
+
/** Whether to show the close button */
|
|
32
|
+
showCloseButton?: boolean;
|
|
33
|
+
/** Initial width of the panel */
|
|
34
|
+
initialWidth?: number;
|
|
35
|
+
/** Website ID for loading components */
|
|
36
|
+
websiteId?: string;
|
|
37
|
+
/** ID of the component to highlight (for "Go to Main Component" feature) */
|
|
38
|
+
selectedComponentId?: string | null;
|
|
39
|
+
/** Callback when selection is cleared */
|
|
40
|
+
onClearSelection?: () => void;
|
|
41
|
+
/** Callback when a component is double-clicked (to edit) */
|
|
42
|
+
onEditComponent?: (componentId: string) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* ComponentPanel displays a searchable list of reusable components
|
|
47
|
+
* organized by category. Components can be dragged onto the canvas
|
|
48
|
+
* to create new instances.
|
|
49
|
+
*/
|
|
50
|
+
export function ComponentPanel({
|
|
51
|
+
onClose,
|
|
52
|
+
showCloseButton = true,
|
|
53
|
+
initialWidth = 280,
|
|
54
|
+
websiteId,
|
|
55
|
+
selectedComponentId,
|
|
56
|
+
onClearSelection,
|
|
57
|
+
onEditComponent,
|
|
58
|
+
}: ComponentPanelProps) {
|
|
59
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
60
|
+
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
|
61
|
+
new Set()
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const {
|
|
65
|
+
masterComponents,
|
|
66
|
+
componentLibrary,
|
|
67
|
+
isLoadingComponents,
|
|
68
|
+
loadComponents,
|
|
69
|
+
} = useEditorComponents();
|
|
70
|
+
|
|
71
|
+
// Resizable panel
|
|
72
|
+
const { width, isDragging, resizeHandleProps } = useResizablePanel({
|
|
73
|
+
initialWidth,
|
|
74
|
+
minWidth: 200,
|
|
75
|
+
maxWidth: 400,
|
|
76
|
+
direction: 'right',
|
|
77
|
+
storageKey: 'editor-component-panel-width',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Load components when websiteId changes
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (websiteId) {
|
|
83
|
+
loadComponents(websiteId);
|
|
84
|
+
}
|
|
85
|
+
}, [websiteId, loadComponents]);
|
|
86
|
+
|
|
87
|
+
// Initialize expanded categories
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (componentLibrary.length > 0 && expandedCategories.size === 0) {
|
|
90
|
+
// Expand the first category by default
|
|
91
|
+
setExpandedCategories(new Set([componentLibrary[0].id]));
|
|
92
|
+
}
|
|
93
|
+
}, [componentLibrary, expandedCategories.size]);
|
|
94
|
+
|
|
95
|
+
// Expand category containing the selected component
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (selectedComponentId && componentLibrary.length > 0) {
|
|
98
|
+
const categoryWithComponent = componentLibrary.find(cat =>
|
|
99
|
+
cat.componentIds.includes(selectedComponentId)
|
|
100
|
+
);
|
|
101
|
+
if (categoryWithComponent) {
|
|
102
|
+
setExpandedCategories(prev => {
|
|
103
|
+
const next = new Set(prev);
|
|
104
|
+
next.add(categoryWithComponent.id);
|
|
105
|
+
return next;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}, [selectedComponentId, componentLibrary]);
|
|
110
|
+
|
|
111
|
+
// Filter components based on search query
|
|
112
|
+
const filteredComponents = useMemo(() => {
|
|
113
|
+
if (!searchQuery.trim()) {
|
|
114
|
+
return masterComponents;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const query = searchQuery.toLowerCase();
|
|
118
|
+
const filtered = new Map(
|
|
119
|
+
Array.from(masterComponents).filter(([_, component]) => {
|
|
120
|
+
return (
|
|
121
|
+
component.name.toLowerCase().includes(query) ||
|
|
122
|
+
(component.tags?.some((tag) => tag.toLowerCase().includes(query)) ?? false)
|
|
123
|
+
);
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return filtered;
|
|
128
|
+
}, [masterComponents, searchQuery]);
|
|
129
|
+
|
|
130
|
+
// Get components for a specific category
|
|
131
|
+
const getComponentsForCategory = useCallback(
|
|
132
|
+
(categoryId: string) => {
|
|
133
|
+
const category = componentLibrary.find((cat) => cat.id === categoryId);
|
|
134
|
+
if (!category) return [];
|
|
135
|
+
|
|
136
|
+
return category.componentIds
|
|
137
|
+
.map((id) => filteredComponents.get(id))
|
|
138
|
+
.filter(Boolean);
|
|
139
|
+
},
|
|
140
|
+
[componentLibrary, filteredComponents]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Toggle category expansion
|
|
144
|
+
const toggleCategory = useCallback((categoryId: string) => {
|
|
145
|
+
setExpandedCategories((prev) => {
|
|
146
|
+
const next = new Set(prev);
|
|
147
|
+
if (next.has(categoryId)) {
|
|
148
|
+
next.delete(categoryId);
|
|
149
|
+
} else {
|
|
150
|
+
next.add(categoryId);
|
|
151
|
+
}
|
|
152
|
+
return next;
|
|
153
|
+
});
|
|
154
|
+
}, []);
|
|
155
|
+
|
|
156
|
+
// Handle keyboard navigation
|
|
157
|
+
const handleKeyDown = useCallback(
|
|
158
|
+
(e: React.KeyboardEvent) => {
|
|
159
|
+
if (e.key === 'Escape' && onClose) {
|
|
160
|
+
onClose();
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
[onClose]
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Handle component edit (double-click)
|
|
167
|
+
const handleEditComponent = useCallback(
|
|
168
|
+
(component: { id: string }) => {
|
|
169
|
+
onEditComponent?.(component.id);
|
|
170
|
+
},
|
|
171
|
+
[onEditComponent]
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Filter categories that have components matching the search
|
|
175
|
+
const visibleCategories = useMemo(() => {
|
|
176
|
+
if (!searchQuery.trim()) {
|
|
177
|
+
return componentLibrary;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return componentLibrary.filter((category) => {
|
|
181
|
+
const components = getComponentsForCategory(category.id);
|
|
182
|
+
return components.length > 0;
|
|
183
|
+
});
|
|
184
|
+
}, [componentLibrary, searchQuery, getComponentsForCategory]);
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div
|
|
188
|
+
className="bg-[#2c2c2c] border-r border-[#444444] flex flex-col relative flex-shrink-0"
|
|
189
|
+
style={{ width: `${width}px` }}
|
|
190
|
+
onKeyDown={handleKeyDown}
|
|
191
|
+
role="region"
|
|
192
|
+
aria-label="コンポーネントパネル"
|
|
193
|
+
>
|
|
194
|
+
{/* Resize handle */}
|
|
195
|
+
<div {...resizeHandleProps} />
|
|
196
|
+
|
|
197
|
+
{/* Drag overlay for smooth resizing */}
|
|
198
|
+
{isDragging && <div className="fixed inset-0 z-50 cursor-col-resize" />}
|
|
199
|
+
|
|
200
|
+
{/* Header */}
|
|
201
|
+
<div className="flex items-center justify-between p-3 border-b border-[#444444]">
|
|
202
|
+
<div className="flex items-center gap-2">
|
|
203
|
+
<Component className="w-4 h-4 text-gray-400" />
|
|
204
|
+
<span className="text-sm font-medium text-white">コンポーネント</span>
|
|
205
|
+
</div>
|
|
206
|
+
{showCloseButton && onClose && (
|
|
207
|
+
<Button
|
|
208
|
+
variant="ghost"
|
|
209
|
+
size="icon"
|
|
210
|
+
className="h-6 w-6 text-gray-400 hover:text-white hover:bg-[#444444]"
|
|
211
|
+
onClick={onClose}
|
|
212
|
+
aria-label="コンポーネントパネルを閉じる"
|
|
213
|
+
>
|
|
214
|
+
<X className="w-4 h-4" />
|
|
215
|
+
</Button>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{/* Search */}
|
|
220
|
+
<div className="p-2 border-b border-[#444444]">
|
|
221
|
+
<ComponentSearch
|
|
222
|
+
value={searchQuery}
|
|
223
|
+
onChange={setSearchQuery}
|
|
224
|
+
placeholder="コンポーネントを検索..."
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
{/* Component list */}
|
|
229
|
+
<ScrollArea className="flex-1">
|
|
230
|
+
<div className="p-2">
|
|
231
|
+
{isLoadingComponents ? (
|
|
232
|
+
<div className="flex items-center justify-center py-8">
|
|
233
|
+
<div className="animate-spin rounded-full h-6 w-6 border-2 border-gray-500 border-t-blue-500" />
|
|
234
|
+
</div>
|
|
235
|
+
) : visibleCategories.length === 0 ? (
|
|
236
|
+
<div className="text-center py-8">
|
|
237
|
+
<p className="text-xs text-gray-500">
|
|
238
|
+
{searchQuery
|
|
239
|
+
? '検索に一致するコンポーネントがありません'
|
|
240
|
+
: 'コンポーネントがありません'}
|
|
241
|
+
</p>
|
|
242
|
+
</div>
|
|
243
|
+
) : (
|
|
244
|
+
visibleCategories.map((category) => (
|
|
245
|
+
<CategoryAccordion
|
|
246
|
+
key={category.id}
|
|
247
|
+
category={category}
|
|
248
|
+
components={getComponentsForCategory(category.id)}
|
|
249
|
+
isExpanded={
|
|
250
|
+
expandedCategories.has(category.id) ||
|
|
251
|
+
(searchQuery.trim() !== '' &&
|
|
252
|
+
getComponentsForCategory(category.id).length > 0)
|
|
253
|
+
}
|
|
254
|
+
onToggle={() => toggleCategory(category.id)}
|
|
255
|
+
selectedComponentId={selectedComponentId}
|
|
256
|
+
onClearSelection={onClearSelection}
|
|
257
|
+
onEditComponent={handleEditComponent}
|
|
258
|
+
/>
|
|
259
|
+
))
|
|
260
|
+
)}
|
|
261
|
+
</div>
|
|
262
|
+
</ScrollArea>
|
|
263
|
+
|
|
264
|
+
{/* Footer with component count */}
|
|
265
|
+
<div className="px-3 py-2 border-t border-[#444444]">
|
|
266
|
+
<p className="text-[10px] text-gray-500">
|
|
267
|
+
{filteredComponents.size}個のコンポーネント
|
|
268
|
+
{searchQuery && ` (「${searchQuery}」で検索中)`}
|
|
269
|
+
</p>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export default ComponentPanel;
|