@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,494 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * LayerTreeItem - レイヤーパネルのツリーアイテム
5
+ *
6
+ * Phase 2: パフォーマンス最適化
7
+ * - React.memoでラップして不要な再レンダリングを防止
8
+ * - カスタム比較関数で必要な変更のみ検知
9
+ */
10
+
11
+ import React, { memo } from 'react';
12
+ import {
13
+ ContextMenu,
14
+ ContextMenuContent,
15
+ ContextMenuItem,
16
+ ContextMenuSeparator,
17
+ ContextMenuTrigger,
18
+ } from '../../components/ui/context-menu';
19
+ import {
20
+ ChevronRight,
21
+ ChevronDown,
22
+ Trash2,
23
+ Copy,
24
+ ArrowUp,
25
+ ArrowDown,
26
+ ChevronsUp,
27
+ ChevronsDown,
28
+ Eye,
29
+ EyeOff,
30
+ GripVertical,
31
+ Paintbrush,
32
+ ClipboardPaste,
33
+ Component,
34
+ Type,
35
+ Heading,
36
+ Image as ImageIcon,
37
+ Star,
38
+ MousePointerClick,
39
+ Link as LinkIcon,
40
+ TextCursorInput,
41
+ List as ListIcon,
42
+ Table as TableIcon,
43
+ Square,
44
+ Minus,
45
+ Frame,
46
+ } from 'lucide-react';
47
+ import type { DOMTreeNode } from '../types';
48
+
49
+ /**
50
+ * レイヤーの種別。
51
+ * 「div .absolute」のような機械的な表示をやめ、Figma のように
52
+ * アイコン+人が読める名前で中身が分かるようにするための分類。
53
+ */
54
+ export type LayerKind =
55
+ | 'heading'
56
+ | 'text'
57
+ | 'image'
58
+ | 'icon'
59
+ | 'button'
60
+ | 'link'
61
+ | 'input'
62
+ | 'list'
63
+ | 'table'
64
+ | 'line'
65
+ | 'shape'
66
+ | 'group';
67
+
68
+ /** 種別 → アイコン。名前だけだと同名が並ぶので、形でも見分けられるようにする */
69
+ const KIND_ICONS: Record<LayerKind, React.ComponentType<{ className?: string }>> = {
70
+ heading: Heading,
71
+ text: Type,
72
+ image: ImageIcon,
73
+ icon: Star,
74
+ button: MousePointerClick,
75
+ link: LinkIcon,
76
+ input: TextCursorInput,
77
+ list: ListIcon,
78
+ table: TableIcon,
79
+ line: Minus,
80
+ shape: Square,
81
+ group: Frame,
82
+ };
83
+
84
+ export interface LayerTreeItemProps {
85
+ node: DOMTreeNode;
86
+ depth: number;
87
+ isExpanded: boolean;
88
+ isSelected: boolean;
89
+ isDragging: boolean;
90
+ isDragOver: boolean;
91
+ dragOverPosition: 'before' | 'after' | 'inside' | null;
92
+ isHidden: boolean;
93
+ hasStyleInClipboard: boolean;
94
+ /** 行に出す人が読める名前(実 DOM から生成) */
95
+ label: string;
96
+ /** レイヤー種別(アイコン用) */
97
+ kind: LayerKind;
98
+ /** キャンバス or レイヤー行のホバー対象がこの行か */
99
+ isHovered: boolean;
100
+ /** 選択状態が変わったことを検知するためのキー(selectedElementIds.join(',')など) */
101
+ selectionKey: string;
102
+ /** 展開状態が変わったことを検知するためのキー */
103
+ expandedKey: string;
104
+ /**
105
+ * ホバー対象が変わったことを検知するためのキー。
106
+ * この行自身の isHovered が変わらなくても、深い階層にいる別の行の
107
+ * isHovered を更新するには「親も再レンダリングされる」必要があるため、
108
+ * selectionKey と同じ理由で全行に配る。
109
+ */
110
+ hoverKey: string;
111
+ /** ラベルが変わったことを検知するためのキー(テキスト編集で名前が変わる) */
112
+ labelKey: string;
113
+ // Event handlers
114
+ onToggleExpand: (nodeId: string) => void;
115
+ onToggleVisibility: (nodeId: string) => void;
116
+ onNodeClick: (e: React.MouseEvent, nodeId: string) => void;
117
+ /** 行ホバー開始(キャンバス側をハイライトする) */
118
+ onRowMouseEnter: (nodeId: string) => void;
119
+ /** 行ホバー終了 */
120
+ onRowMouseLeave: () => void;
121
+ onDragStart: (e: React.DragEvent, nodeId: string) => void;
122
+ onDragOver: (e: React.DragEvent, nodeId: string, rect: DOMRect) => void;
123
+ onDragLeave: () => void;
124
+ onDrop: (e: React.DragEvent, nodeId: string) => void;
125
+ onDragEnd: () => void;
126
+ // Context menu actions
127
+ onSelectForContextMenu: (nodeId: string) => void;
128
+ onDuplicate: () => void;
129
+ onCopyStyle: () => void;
130
+ onPasteStyle: () => void;
131
+ onBringToFront: () => void;
132
+ onBringForward: () => void;
133
+ onSendBackward: () => void;
134
+ onSendToBack: () => void;
135
+ onDelete: () => void;
136
+ // Ref for scrolling
137
+ nodeRef: (ref: HTMLDivElement | null) => void;
138
+ // Children renderer (for recursive rendering)
139
+ renderChildren: () => React.ReactNode;
140
+ }
141
+
142
+ /**
143
+ * メモ化されたレイヤーツリーアイテム
144
+ *
145
+ * 以下の条件でのみ再レンダリング:
146
+ * - node.id が変更
147
+ * - isExpanded, isSelected, isDragging, isDragOver, isHidden が変更
148
+ * - dragOverPosition が変更
149
+ */
150
+ export const LayerTreeItem = memo(function LayerTreeItem({
151
+ node,
152
+ depth,
153
+ isExpanded,
154
+ isSelected,
155
+ isDragging,
156
+ isDragOver,
157
+ dragOverPosition,
158
+ isHidden,
159
+ hasStyleInClipboard,
160
+ label,
161
+ kind,
162
+ isHovered,
163
+ selectionKey: _selectionKey, // 比較用のみ、レンダリングには使用しない
164
+ expandedKey: _expandedKey, // 比較用のみ、レンダリングには使用しない
165
+ hoverKey: _hoverKey, // 比較用のみ、レンダリングには使用しない
166
+ labelKey: _labelKey, // 比較用のみ、レンダリングには使用しない
167
+ onToggleExpand,
168
+ onToggleVisibility,
169
+ onNodeClick,
170
+ onRowMouseEnter,
171
+ onRowMouseLeave,
172
+ onDragStart,
173
+ onDragOver,
174
+ onDragLeave,
175
+ onDrop,
176
+ onDragEnd,
177
+ onSelectForContextMenu,
178
+ onDuplicate,
179
+ onCopyStyle,
180
+ onPasteStyle,
181
+ onBringToFront,
182
+ onBringForward,
183
+ onSendBackward,
184
+ onSendToBack,
185
+ onDelete,
186
+ nodeRef,
187
+ renderChildren,
188
+ }: LayerTreeItemProps) {
189
+ const hasChildren = node.children.length > 0;
190
+ const KindIcon = KIND_ICONS[kind] ?? Frame;
191
+
192
+ return (
193
+ <div className={isDragging ? 'opacity-50' : ''}>
194
+ <ContextMenu>
195
+ <ContextMenuTrigger asChild>
196
+ <div
197
+ ref={nodeRef}
198
+ className={`flex items-center gap-1 py-0.5 px-1 rounded cursor-pointer text-xs group transition-colors ${
199
+ isSelected
200
+ ? 'bg-[#0d99ff] text-white'
201
+ : isHovered
202
+ ? // キャンバス側のホバーと連動した薄いハイライト
203
+ 'text-gray-100 bg-[#37373d] ring-1 ring-inset ring-[#0d99ff]/50'
204
+ : 'text-gray-300 hover:bg-[#444444]'
205
+ } ${
206
+ isDragOver && dragOverPosition === 'before'
207
+ ? 'border-t-2 border-[#0d99ff]'
208
+ : ''
209
+ } ${
210
+ isDragOver && dragOverPosition === 'after'
211
+ ? 'border-b-2 border-[#0d99ff]'
212
+ : ''
213
+ } ${
214
+ isDragOver && dragOverPosition === 'inside'
215
+ ? 'bg-[#0d99ff]/20'
216
+ : ''
217
+ }`}
218
+ style={{ paddingLeft: `${depth * 12 + 4}px` }}
219
+ draggable
220
+ onDragStart={(e) => onDragStart(e, node.id)}
221
+ onDragOver={(e) => {
222
+ const rect = e.currentTarget.getBoundingClientRect();
223
+ onDragOver(e, node.id, rect);
224
+ }}
225
+ onDragLeave={onDragLeave}
226
+ onDrop={(e) => onDrop(e, node.id)}
227
+ onDragEnd={onDragEnd}
228
+ onClick={(e) => onNodeClick(e, node.id)}
229
+ // ハンドラは親で useCallback 済みの安定参照なので、
230
+ // memo で再レンダリングされない行でも古いクロージャにならない
231
+ onMouseEnter={() => onRowMouseEnter(node.id)}
232
+ onMouseLeave={onRowMouseLeave}
233
+ >
234
+ {/* ドラッグハンドル */}
235
+ <GripVertical
236
+ className={`w-3 h-3 opacity-0 group-hover:opacity-100 cursor-grab ${
237
+ isSelected ? 'text-white' : 'text-gray-500'
238
+ }`}
239
+ />
240
+
241
+ {/* 展開/折りたたみ */}
242
+ {hasChildren ? (
243
+ <button
244
+ onClick={(e) => {
245
+ e.stopPropagation();
246
+ onToggleExpand(node.id);
247
+ }}
248
+ className={`p-0.5 rounded ${
249
+ isSelected ? 'hover:bg-[#0d99ff]' : 'hover:bg-gray-600'
250
+ }`}
251
+ >
252
+ {isExpanded ? (
253
+ <ChevronDown className="w-3 h-3" />
254
+ ) : (
255
+ <ChevronRight className="w-3 h-3" />
256
+ )}
257
+ </button>
258
+ ) : (
259
+ <span className="w-4" />
260
+ )}
261
+
262
+ {/* 表示/非表示アイコン */}
263
+ <button
264
+ onClick={(e) => {
265
+ e.stopPropagation();
266
+ onToggleVisibility(node.id);
267
+ }}
268
+ className={`p-0.5 rounded ${
269
+ isHidden
270
+ ? 'text-gray-500'
271
+ : isSelected
272
+ ? 'text-white'
273
+ : 'text-gray-400'
274
+ } opacity-0 group-hover:opacity-100 ${
275
+ isSelected ? 'hover:bg-[#0d99ff]' : 'hover:bg-gray-600'
276
+ }`}
277
+ >
278
+ {isHidden ? (
279
+ <EyeOff className="w-3 h-3" />
280
+ ) : (
281
+ <Eye className="w-3 h-3" />
282
+ )}
283
+ </button>
284
+
285
+ {/* コンポーネントインスタンスアイコン */}
286
+ {node.componentInstanceId && (
287
+ <Component
288
+ className={`w-3 h-3 flex-shrink-0 ${
289
+ isSelected ? 'text-purple-200' : 'text-purple-400'
290
+ }`}
291
+ // React 18 の @types/react では SVGAttributes に title が無く lucide のアイコンに直接渡せないため、
292
+ // DOM にはそのまま出力される title 属性を spread 経由で渡す(描画結果は変わらない)
293
+ {...({ title: 'コンポーネントインスタンス' } as { title?: string })}
294
+ />
295
+ )}
296
+
297
+ {/*
298
+ [レイヤー名] 「<div .absolute>」のような機械的な表示をやめ、
299
+ 実 DOM から作った人が読める名前(見出し文言・alt・種別名)を主役にする。
300
+ タグ名は識別のために右端へ薄く逃がす。
301
+ */}
302
+ <KindIcon
303
+ className={`w-3 h-3 flex-shrink-0 ${
304
+ isHidden
305
+ ? 'text-gray-600'
306
+ : isSelected
307
+ ? 'text-blue-100'
308
+ : 'text-gray-500'
309
+ }`}
310
+ />
311
+ <span
312
+ className={`truncate flex-1 min-w-0 ${
313
+ isHidden
314
+ ? 'text-gray-500'
315
+ : isSelected
316
+ ? 'text-white'
317
+ : 'text-gray-200'
318
+ }`}
319
+ title={`${label} (${node.tagName})`}
320
+ >
321
+ {label}
322
+ </span>
323
+ <span
324
+ className={`text-[10px] flex-shrink-0 tabular-nums ${
325
+ isSelected ? 'text-blue-200' : 'text-gray-600'
326
+ }`}
327
+ >
328
+ {node.tagName}
329
+ </span>
330
+ </div>
331
+ </ContextMenuTrigger>
332
+
333
+ <ContextMenuContent className="w-48 bg-[#2c2c2c] border-[#444444]">
334
+ <ContextMenuItem
335
+ onClick={() => {
336
+ onSelectForContextMenu(node.id);
337
+ setTimeout(() => onDuplicate(), 50);
338
+ }}
339
+ className="text-xs text-gray-300 hover:bg-gray-700 focus:bg-gray-700"
340
+ >
341
+ <Copy className="w-3.5 h-3.5 mr-2" />
342
+ 複製
343
+ <span className="ml-auto text-gray-500">⌘D</span>
344
+ </ContextMenuItem>
345
+
346
+ <ContextMenuSeparator className="bg-[#444444]" />
347
+
348
+ <ContextMenuItem
349
+ onClick={() => {
350
+ onSelectForContextMenu(node.id);
351
+ setTimeout(() => onCopyStyle(), 50);
352
+ }}
353
+ className="text-xs text-gray-300 hover:bg-gray-700 focus:bg-gray-700"
354
+ >
355
+ <Paintbrush className="w-3.5 h-3.5 mr-2" />
356
+ スタイルをコピー
357
+ <span className="ml-auto text-gray-500">⌘⌥C</span>
358
+ </ContextMenuItem>
359
+
360
+ <ContextMenuItem
361
+ onClick={() => {
362
+ onSelectForContextMenu(node.id);
363
+ setTimeout(() => onPasteStyle(), 50);
364
+ }}
365
+ disabled={!hasStyleInClipboard}
366
+ className="text-xs text-gray-300 hover:bg-gray-700 focus:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
367
+ >
368
+ <ClipboardPaste className="w-3.5 h-3.5 mr-2" />
369
+ スタイルをペースト
370
+ <span className="ml-auto text-gray-500">⌘⌥V</span>
371
+ </ContextMenuItem>
372
+
373
+ <ContextMenuSeparator className="bg-[#444444]" />
374
+
375
+ <ContextMenuItem
376
+ onClick={() => {
377
+ onSelectForContextMenu(node.id);
378
+ setTimeout(() => onBringToFront(), 50);
379
+ }}
380
+ className="text-xs text-gray-300 hover:bg-gray-700 focus:bg-gray-700"
381
+ >
382
+ <ChevronsUp className="w-3.5 h-3.5 mr-2" />
383
+ 最前面へ
384
+ <span className="ml-auto text-gray-500">⌘⇧]</span>
385
+ </ContextMenuItem>
386
+
387
+ <ContextMenuItem
388
+ onClick={() => {
389
+ onSelectForContextMenu(node.id);
390
+ setTimeout(() => onBringForward(), 50);
391
+ }}
392
+ className="text-xs text-gray-300 hover:bg-gray-700 focus:bg-gray-700"
393
+ >
394
+ <ArrowUp className="w-3.5 h-3.5 mr-2" />
395
+ 前面へ
396
+ <span className="ml-auto text-gray-500">⌘]</span>
397
+ </ContextMenuItem>
398
+
399
+ <ContextMenuItem
400
+ onClick={() => {
401
+ onSelectForContextMenu(node.id);
402
+ setTimeout(() => onSendBackward(), 50);
403
+ }}
404
+ className="text-xs text-gray-300 hover:bg-gray-700 focus:bg-gray-700"
405
+ >
406
+ <ArrowDown className="w-3.5 h-3.5 mr-2" />
407
+ 背面へ
408
+ <span className="ml-auto text-gray-500">⌘[</span>
409
+ </ContextMenuItem>
410
+
411
+ <ContextMenuItem
412
+ onClick={() => {
413
+ onSelectForContextMenu(node.id);
414
+ setTimeout(() => onSendToBack(), 50);
415
+ }}
416
+ className="text-xs text-gray-300 hover:bg-gray-700 focus:bg-gray-700"
417
+ >
418
+ <ChevronsDown className="w-3.5 h-3.5 mr-2" />
419
+ 最背面へ
420
+ <span className="ml-auto text-gray-500">⌘⇧[</span>
421
+ </ContextMenuItem>
422
+
423
+ <ContextMenuSeparator className="bg-[#444444]" />
424
+
425
+ <ContextMenuItem
426
+ onClick={() => onToggleVisibility(node.id)}
427
+ className="text-xs text-gray-300 hover:bg-gray-700 focus:bg-gray-700"
428
+ >
429
+ {isHidden ? (
430
+ <>
431
+ <Eye className="w-3.5 h-3.5 mr-2" />
432
+ 表示
433
+ </>
434
+ ) : (
435
+ <>
436
+ <EyeOff className="w-3.5 h-3.5 mr-2" />
437
+ 非表示
438
+ </>
439
+ )}
440
+ </ContextMenuItem>
441
+
442
+ <ContextMenuSeparator className="bg-[#444444]" />
443
+
444
+ <ContextMenuItem
445
+ onClick={() => {
446
+ onSelectForContextMenu(node.id);
447
+ setTimeout(() => onDelete(), 50);
448
+ }}
449
+ className="text-xs text-red-400 hover:bg-gray-700 focus:bg-gray-700"
450
+ >
451
+ <Trash2 className="w-3.5 h-3.5 mr-2" />
452
+ 削除
453
+ <span className="ml-auto text-gray-500">⌫</span>
454
+ </ContextMenuItem>
455
+ </ContextMenuContent>
456
+ </ContextMenu>
457
+
458
+ {/* 子要素のレンダリング */}
459
+ {hasChildren && isExpanded && <div>{renderChildren()}</div>}
460
+ </div>
461
+ );
462
+ }, (prevProps, nextProps) => {
463
+ // カスタム比較関数: 以下の条件が全て同じなら再レンダリングしない
464
+ //
465
+ // [重要] この比較関数はプロパティを列挙している。ここに書き忘れたプロパティは
466
+ // 「変わっていない」と判定されて画面に反映されない。ホバー連動を足すときに
467
+ // hoverKey / isHovered を入れ忘れると、深い階層の行が永遠に光らなくなる。
468
+ return (
469
+ prevProps.node.id === nextProps.node.id &&
470
+ prevProps.node.tagName === nextProps.node.tagName &&
471
+ prevProps.node.children.length === nextProps.node.children.length &&
472
+ // className/text は表示しなくなったので、代わりに表示名そのものを比較する
473
+ prevProps.label === nextProps.label &&
474
+ prevProps.kind === nextProps.kind &&
475
+ prevProps.depth === nextProps.depth &&
476
+ prevProps.isExpanded === nextProps.isExpanded &&
477
+ prevProps.isSelected === nextProps.isSelected &&
478
+ prevProps.isHovered === nextProps.isHovered &&
479
+ prevProps.isDragging === nextProps.isDragging &&
480
+ prevProps.isDragOver === nextProps.isDragOver &&
481
+ prevProps.dragOverPosition === nextProps.dragOverPosition &&
482
+ prevProps.isHidden === nextProps.isHidden &&
483
+ prevProps.hasStyleInClipboard === nextProps.hasStyleInClipboard &&
484
+ // selectionKeyが変わると子要素も再レンダリングが必要
485
+ prevProps.selectionKey === nextProps.selectionKey &&
486
+ // expandedKeyが変わると子要素も再レンダリングが必要
487
+ prevProps.expandedKey === nextProps.expandedKey &&
488
+ // hoverKey / labelKey も同じ理由(子孫の isHovered / label を届けるため)
489
+ prevProps.hoverKey === nextProps.hoverKey &&
490
+ prevProps.labelKey === nextProps.labelKey
491
+ );
492
+ });
493
+
494
+ export default LayerTreeItem;