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