@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,615 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { Button } from '../../../components/ui/button';
5
+ import { Label } from '../../../components/ui/label';
6
+ import { CompactNumberInput } from './CompactNumberInput';
7
+ import { VariableAwareUnitInput } from './VariableAwareUnitInput';
8
+ import { SPACING_UNITS } from './unit-utils';
9
+ import {
10
+ ArrowRight,
11
+ ArrowDown,
12
+ WrapText,
13
+ Grid3X3,
14
+ Plus,
15
+ Minus,
16
+ LayoutGrid,
17
+ Columns,
18
+ Rows,
19
+ } from 'lucide-react';
20
+ import {
21
+ Tooltip,
22
+ TooltipContent,
23
+ TooltipProvider,
24
+ TooltipTrigger,
25
+ } from '../../../components/ui/tooltip';
26
+ import { useEditorContext } from '../../EditorContext';
27
+ import {
28
+ AUTOLAYOUT_CLASS,
29
+ disableAutoLayout,
30
+ enableAutoLayout,
31
+ relaxContainerSize,
32
+ restoreChildrenToFlow,
33
+ } from '../../utils/restore-flow';
34
+ import type { SelectedElementInfo } from '../../types';
35
+
36
+ interface AutoLayoutPanelProps {
37
+ element: SelectedElementInfo;
38
+ onStyleChange: (styles: Record<string, string>) => void;
39
+ }
40
+
41
+ type LayoutMode = 'none' | 'flex' | 'grid';
42
+ type FlowDirection = 'row' | 'column' | 'wrap';
43
+ type AlignmentPosition = 'start-start' | 'start-center' | 'start-end'
44
+ | 'center-start' | 'center-center' | 'center-end'
45
+ | 'end-start' | 'end-center' | 'end-end';
46
+
47
+ // グリッドプリセット
48
+ const GRID_PRESETS = [
49
+ { name: '2列', cols: 2, rows: 1, icon: '⬛⬛' },
50
+ { name: '3列', cols: 3, rows: 1, icon: '⬛⬛⬛' },
51
+ { name: '4列', cols: 4, rows: 1, icon: '⬛⬛⬛⬛' },
52
+ { name: '2x2', cols: 2, rows: 2, icon: '⊞' },
53
+ { name: '3x3', cols: 3, rows: 3, icon: '⊞⊞' },
54
+ { name: '12列', cols: 12, rows: 1, icon: '▦' },
55
+ ];
56
+
57
+ // グリッドトラックのパース(カラム/ロー数を取得)
58
+ function parseGridTemplateCount(template: string): number {
59
+ if (!template || template === 'none') return 0;
60
+ // repeat(n, ...) 形式をパース
61
+ const repeatMatch = template.match(/repeat\((\d+)/);
62
+ if (repeatMatch) return parseInt(repeatMatch[1]);
63
+ // スペース区切りの数をカウント
64
+ return template.split(/\s+/).filter(t => t && t !== 'none').length;
65
+ }
66
+
67
+ // カラム数からgrid-template-columnsを生成
68
+ function buildGridColumns(count: number): string {
69
+ if (count <= 0) return 'none';
70
+ return `repeat(${count}, 1fr)`;
71
+ }
72
+
73
+ // ロー数からgrid-template-rowsを生成
74
+ function buildGridRows(count: number): string {
75
+ if (count <= 0) return 'auto';
76
+ if (count === 1) return 'auto';
77
+ return `repeat(${count}, 1fr)`;
78
+ }
79
+
80
+ export function AutoLayoutPanel({ element, onStyleChange }: AutoLayoutPanelProps) {
81
+ const { getIframeDoc, notifyIframeChange } = useEditorContext();
82
+
83
+ /** 選択中の要素の実体(iframe内)を引く */
84
+ const containerEl = (): HTMLElement | null =>
85
+ getIframeDoc()?.querySelector<HTMLElement>(`[data-element-id="${element.id}"]`) ?? null;
86
+
87
+ /**
88
+ * この要素は「明示的にオートレイアウトON」か。
89
+ *
90
+ * computed の display で判定すると、テンプレート由来の flex を
91
+ * 「ONになっている」と誤って表示してしまう(嘘をつく)。
92
+ * 目印クラスの有無だけを見る。
93
+ */
94
+ // SelectedElementInfo はクラスを持たないので、実体から読む。
95
+ // element.id はスタイル変更のたびに作り直されるため、変更後も再評価される
96
+ const isAutoLayoutOn = containerEl()?.classList.contains(AUTOLAYOUT_CLASS) ?? false;
97
+
98
+ /**
99
+ * 既にONの器に対して、レイアウトの微調整(gap・配置など)をする前の地ならし。
100
+ *
101
+ * ONにした時点で子はフロー化済みだが、後から足された子や別経路で
102
+ * 絶対配置が焼き込まれた子が混じると、その子だけ並びから外れる。
103
+ */
104
+ const prepareContainerForLayout = (options?: { relaxSize?: boolean }) => {
105
+ const el = containerEl();
106
+ if (!el) return;
107
+ restoreChildrenToFlow(el);
108
+ // 方向を変えるときは、焼き込まれた幅・高さも解く(縦積みで中身が溢れるため)
109
+ if (options?.relaxSize) relaxContainerSize(el);
110
+ // 位置が変わるので選択枠と履歴を追随させる
111
+ notifyIframeChange();
112
+ };
113
+
114
+ /**
115
+ * オートレイアウトをONにする(Figmaの「オートレイアウトを追加」に相当)。
116
+ *
117
+ * 目印クラスを付け、今見えている位置の順に子を並べ直してフロー化する。
118
+ * これをやらないと、一括変換で絶対配置になった子には gap も方向も効かない。
119
+ */
120
+ const turnAutoLayoutOn = (direction: 'row' | 'column') => {
121
+ const el = containerEl();
122
+ if (!el) return;
123
+ enableAutoLayout(el, direction);
124
+ relaxContainerSize(el);
125
+ notifyIframeChange();
126
+ };
127
+
128
+ /** オートレイアウトをOFFにする。印を外すだけで、子は今の見た目のまま残す */
129
+ const turnAutoLayoutOff = () => {
130
+ const el = containerEl();
131
+ if (!el) return;
132
+ disableAutoLayout(el);
133
+ notifyIframeChange();
134
+ };
135
+
136
+ const [colCount, setColCount] = useState(() =>
137
+ parseGridTemplateCount(element.gridTemplateColumns) || 2
138
+ );
139
+ const [rowCount, setRowCount] = useState(() =>
140
+ parseGridTemplateCount(element.gridTemplateRows) || 1
141
+ );
142
+
143
+ // 要素が変わったらグリッド状態を同期
144
+ useEffect(() => {
145
+ const cols = parseGridTemplateCount(element.gridTemplateColumns);
146
+ const rows = parseGridTemplateCount(element.gridTemplateRows);
147
+ if (cols > 0) setColCount(cols);
148
+ if (rows > 0) setRowCount(rows);
149
+ }, [element.gridTemplateColumns, element.gridTemplateRows]);
150
+
151
+ /**
152
+ * 現在のレイアウトモード。
153
+ *
154
+ * [重要] computed の display だけでは判定しない。テンプレート由来の flex は
155
+ * スライド中に山ほどあり、それを「レイアウトON」と表示すると
156
+ * 「ONに見えるのに触っても何も起きない」という嘘になる。
157
+ * 明示的にONにした器(gg-autolayout)だけを ON として扱う。
158
+ */
159
+ const getLayoutMode = (): LayoutMode => {
160
+ if (!isAutoLayoutOn) return 'none';
161
+ if (element.display === 'grid') return 'grid';
162
+ return 'flex';
163
+ };
164
+
165
+ const layoutMode = getLayoutMode();
166
+ const isFlex = layoutMode === 'flex';
167
+ const isGrid = layoutMode === 'grid';
168
+
169
+ // 現在のフロー方向を判定(Flexbox用)
170
+ const getFlowDirection = (): FlowDirection => {
171
+ if (element.flexWrap === 'wrap') return 'wrap';
172
+ if (element.flexDirection === 'column') return 'column';
173
+ return 'row';
174
+ };
175
+
176
+ const flowDirection = getFlowDirection();
177
+
178
+ // フロー方向を変更(Flexbox用)
179
+ const handleFlowChange = (direction: FlowDirection) => {
180
+ // wrap は行方向の折り返しなので、並びの基準は row と同じ
181
+ prepareContainerForLayout({ relaxSize: true });
182
+ if (direction === 'wrap') {
183
+ onStyleChange({
184
+ display: 'flex',
185
+ flexDirection: 'row',
186
+ flexWrap: 'wrap',
187
+ });
188
+ } else {
189
+ onStyleChange({
190
+ display: 'flex',
191
+ flexDirection: direction,
192
+ flexWrap: 'nowrap',
193
+ });
194
+ }
195
+ };
196
+
197
+ // レイアウトモードを変更
198
+ const handleLayoutModeChange = (mode: LayoutMode) => {
199
+ // ONにする瞬間だけ、子を「今見えている順」に並べ直してフロー化する。
200
+ // これが Figma の「オートレイアウトを追加」と同じ中身
201
+ if (mode === 'flex') turnAutoLayoutOn('row');
202
+ else if (mode === 'grid') turnAutoLayoutOn('row');
203
+ else turnAutoLayoutOff();
204
+
205
+ if (mode === 'none') {
206
+ onStyleChange({ display: 'block' });
207
+ } else if (mode === 'flex') {
208
+ onStyleChange({
209
+ display: 'flex',
210
+ flexDirection: 'row',
211
+ flexWrap: 'nowrap',
212
+ alignItems: 'flex-start',
213
+ justifyContent: 'flex-start',
214
+ gap: '8px',
215
+ });
216
+ } else if (mode === 'grid') {
217
+ onStyleChange({
218
+ display: 'grid',
219
+ gridTemplateColumns: buildGridColumns(colCount),
220
+ gridTemplateRows: buildGridRows(rowCount),
221
+ gap: '8px',
222
+ alignItems: 'stretch',
223
+ justifyItems: 'stretch',
224
+ });
225
+ }
226
+ };
227
+
228
+ // 配置位置を判定
229
+ const getAlignmentPosition = (): AlignmentPosition => {
230
+ const justify = isGrid ? element.justifyItems : element.justifyContent;
231
+ const align = element.alignItems;
232
+
233
+ let row: 'start' | 'center' | 'end' = 'start';
234
+ let col: 'start' | 'center' | 'end' = 'start';
235
+
236
+ if (justify === 'center') col = 'center';
237
+ else if (justify === 'flex-end' || justify === 'end') col = 'end';
238
+ else if (justify === 'space-between' || justify === 'space-around') col = 'center';
239
+
240
+ if (align === 'center') row = 'center';
241
+ else if (align === 'flex-end' || align === 'end') row = 'end';
242
+
243
+ return `${row}-${col}` as AlignmentPosition;
244
+ };
245
+
246
+ // 配置を変更
247
+ const handleAlignmentChange = (position: AlignmentPosition) => {
248
+ prepareContainerForLayout();
249
+ const [rowPos, colPos] = position.split('-') as ['start' | 'center' | 'end', 'start' | 'center' | 'end'];
250
+
251
+ if (isFlex) {
252
+ let justifyContent = 'flex-start';
253
+ if (colPos === 'center') justifyContent = 'center';
254
+ else if (colPos === 'end') justifyContent = 'flex-end';
255
+
256
+ let alignItems = 'flex-start';
257
+ if (rowPos === 'center') alignItems = 'center';
258
+ else if (rowPos === 'end') alignItems = 'flex-end';
259
+
260
+ onStyleChange({ justifyContent, alignItems });
261
+ } else if (isGrid) {
262
+ let justifyItems = 'start';
263
+ if (colPos === 'center') justifyItems = 'center';
264
+ else if (colPos === 'end') justifyItems = 'end';
265
+
266
+ let alignItems = 'start';
267
+ if (rowPos === 'center') alignItems = 'center';
268
+ else if (rowPos === 'end') alignItems = 'end';
269
+
270
+ onStyleChange({ justifyItems, alignItems });
271
+ }
272
+ };
273
+
274
+ const alignmentPosition = getAlignmentPosition();
275
+
276
+ // カラム数を変更
277
+ const updateColumnCount = (newCount: number) => {
278
+ const count = Math.max(1, Math.min(12, newCount));
279
+ setColCount(count);
280
+ onStyleChange({ gridTemplateColumns: buildGridColumns(count) });
281
+ };
282
+
283
+ // ロー数を変更
284
+ const updateRowCount = (newCount: number) => {
285
+ const count = Math.max(1, Math.min(12, newCount));
286
+ setRowCount(count);
287
+ onStyleChange({ gridTemplateRows: buildGridRows(count) });
288
+ };
289
+
290
+ // プリセットを適用
291
+ const applyPreset = (cols: number, rows: number) => {
292
+ setColCount(cols);
293
+ setRowCount(rows);
294
+ onStyleChange({
295
+ display: 'grid',
296
+ gridTemplateColumns: buildGridColumns(cols),
297
+ gridTemplateRows: buildGridRows(rows),
298
+ gap: '8px',
299
+ });
300
+ };
301
+
302
+ return (
303
+ <div className="space-y-2">
304
+ {/* レイアウトモード選択 */}
305
+ <div className="flex items-center justify-between">
306
+ <Label className="text-[10px] text-gray-500">レイアウト</Label>
307
+ <div className="flex gap-0.5">
308
+ <TooltipButton
309
+ icon={<span className="text-[9px] font-medium">OFF</span>}
310
+ label="レイアウトなし"
311
+ testId="mode-none"
312
+ onClick={() => handleLayoutModeChange('none')}
313
+ active={layoutMode === 'none'}
314
+ />
315
+ <TooltipButton
316
+ icon={<ArrowRight className="h-3 w-3" />}
317
+ label="Flexbox"
318
+ testId="mode-flex"
319
+ onClick={() => handleLayoutModeChange('flex')}
320
+ active={layoutMode === 'flex'}
321
+ />
322
+ <TooltipButton
323
+ icon={<Grid3X3 className="h-3 w-3" />}
324
+ label="Grid"
325
+ testId="mode-grid"
326
+ onClick={() => handleLayoutModeChange('grid')}
327
+ active={layoutMode === 'grid'}
328
+ />
329
+ </div>
330
+ </div>
331
+
332
+ {/* Flexbox設定 */}
333
+ {isFlex && (
334
+ <div className="flex items-start gap-3">
335
+ {/* フロー方向 */}
336
+ <div>
337
+ <Label className="text-[9px] text-gray-600 mb-0.5 block">フロー</Label>
338
+ <div className="flex gap-0.5">
339
+ <TooltipButton
340
+ icon={<ArrowRight className="h-3 w-3" />}
341
+ label="横並び"
342
+ testId="flow-row"
343
+ onClick={() => handleFlowChange('row')}
344
+ active={flowDirection === 'row'}
345
+ />
346
+ <TooltipButton
347
+ icon={<ArrowDown className="h-3 w-3" />}
348
+ label="縦並び"
349
+ testId="flow-column"
350
+ onClick={() => handleFlowChange('column')}
351
+ active={flowDirection === 'column'}
352
+ />
353
+ <TooltipButton
354
+ icon={<WrapText className="h-3 w-3" />}
355
+ label="折り返し"
356
+ testId="flow-wrap"
357
+ onClick={() => handleFlowChange('wrap')}
358
+ active={flowDirection === 'wrap'}
359
+ />
360
+ </div>
361
+ </div>
362
+
363
+ {/* 配置(9グリッド) */}
364
+ <div>
365
+ <Label className="text-[9px] text-gray-600 mb-0.5 block">配置</Label>
366
+ <AlignmentGrid
367
+ position={alignmentPosition}
368
+ onChange={handleAlignmentChange}
369
+ />
370
+ </div>
371
+
372
+ {/* 間隔 (Gap) */}
373
+ <div>
374
+ <Label className="text-[9px] text-gray-600 mb-0.5 block">Gap</Label>
375
+ <VariableAwareUnitInput
376
+ value={element.rawGap || `${Math.round(element.gap)}px`}
377
+ onChange={(val) => {
378
+ prepareContainerForLayout();
379
+ onStyleChange({ gap: val });
380
+ }}
381
+ units={SPACING_UNITS}
382
+ defaultUnit="px"
383
+ category="spacing"
384
+ min={0}
385
+ compact
386
+ />
387
+ </div>
388
+ </div>
389
+ )}
390
+
391
+ {/* Grid設定 */}
392
+ {isGrid && (
393
+ <div className="space-y-2">
394
+ {/* ビジュアルグリッドプレビュー */}
395
+ <div className="bg-[#2a2a2a] rounded p-2">
396
+ <div
397
+ className="grid gap-0.5 mx-auto"
398
+ style={{
399
+ gridTemplateColumns: `repeat(${Math.min(colCount, 6)}, 1fr)`,
400
+ gridTemplateRows: `repeat(${Math.min(rowCount, 4)}, 1fr)`,
401
+ width: 'fit-content',
402
+ maxWidth: '100%',
403
+ }}
404
+ >
405
+ {Array.from({ length: Math.min(colCount * rowCount, 24) }).map((_, i) => (
406
+ <div
407
+ key={i}
408
+ className="w-4 h-3 bg-[#0d99ff]/40 rounded-sm border border-[#0d99ff]/60"
409
+ />
410
+ ))}
411
+ </div>
412
+ {(colCount > 6 || rowCount > 4) && (
413
+ <div className="text-[9px] text-gray-500 text-center mt-1">
414
+ {colCount} × {rowCount}
415
+ </div>
416
+ )}
417
+ </div>
418
+
419
+ {/* プリセットボタン */}
420
+ <div>
421
+ <Label className="text-[9px] text-gray-600 mb-1 block">プリセット</Label>
422
+ <div className="flex flex-wrap gap-1">
423
+ {GRID_PRESETS.map((preset) => (
424
+ <button
425
+ key={preset.name}
426
+ onClick={() => applyPreset(preset.cols, preset.rows)}
427
+ className={`px-2 py-0.5 text-[9px] rounded border transition-colors ${
428
+ colCount === preset.cols && rowCount === preset.rows
429
+ ? 'bg-[#0d99ff] border-[#0d99ff] text-white'
430
+ : 'bg-[#383838] border-[#4a4a4a] text-gray-400 hover:bg-[#4a4a4a] hover:text-white'
431
+ }`}
432
+ >
433
+ {preset.name}
434
+ </button>
435
+ ))}
436
+ </div>
437
+ </div>
438
+
439
+ {/* カラム・ロー数コントロール */}
440
+ <div className="flex items-start gap-4">
441
+ {/* カラム数 */}
442
+ <div>
443
+ <Label className="text-[9px] text-gray-600 mb-0.5 flex items-center gap-1">
444
+ <Columns className="h-3 w-3" />
445
+
446
+ </Label>
447
+ <div className="flex items-center gap-1">
448
+ <Button
449
+ variant="ghost"
450
+ size="icon"
451
+ className="h-5 w-5 text-gray-400 hover:text-white hover:bg-[#4a4a4a]"
452
+ onClick={() => updateColumnCount(colCount - 1)}
453
+ disabled={colCount <= 1}
454
+ >
455
+ <Minus className="h-3 w-3" />
456
+ </Button>
457
+ <span className="w-6 text-center text-xs font-medium">{colCount}</span>
458
+ <Button
459
+ variant="ghost"
460
+ size="icon"
461
+ className="h-5 w-5 text-gray-400 hover:text-white hover:bg-[#4a4a4a]"
462
+ onClick={() => updateColumnCount(colCount + 1)}
463
+ disabled={colCount >= 12}
464
+ >
465
+ <Plus className="h-3 w-3" />
466
+ </Button>
467
+ </div>
468
+ </div>
469
+
470
+ {/* ロー数 */}
471
+ <div>
472
+ <Label className="text-[9px] text-gray-600 mb-0.5 flex items-center gap-1">
473
+ <Rows className="h-3 w-3" />
474
+
475
+ </Label>
476
+ <div className="flex items-center gap-1">
477
+ <Button
478
+ variant="ghost"
479
+ size="icon"
480
+ className="h-5 w-5 text-gray-400 hover:text-white hover:bg-[#4a4a4a]"
481
+ onClick={() => updateRowCount(rowCount - 1)}
482
+ disabled={rowCount <= 1}
483
+ >
484
+ <Minus className="h-3 w-3" />
485
+ </Button>
486
+ <span className="w-6 text-center text-xs font-medium">{rowCount}</span>
487
+ <Button
488
+ variant="ghost"
489
+ size="icon"
490
+ className="h-5 w-5 text-gray-400 hover:text-white hover:bg-[#4a4a4a]"
491
+ onClick={() => updateRowCount(rowCount + 1)}
492
+ disabled={rowCount >= 12}
493
+ >
494
+ <Plus className="h-3 w-3" />
495
+ </Button>
496
+ </div>
497
+ </div>
498
+
499
+ {/* Gap */}
500
+ <div>
501
+ <Label className="text-[9px] text-gray-600 mb-0.5 block">Gap</Label>
502
+ <VariableAwareUnitInput
503
+ value={element.rawGap || `${Math.round(element.gridGap || element.gap)}px`}
504
+ onChange={(val) => {
505
+ prepareContainerForLayout();
506
+ onStyleChange({ gap: val });
507
+ }}
508
+ units={SPACING_UNITS}
509
+ defaultUnit="px"
510
+ category="spacing"
511
+ min={0}
512
+ compact
513
+ />
514
+ </div>
515
+ </div>
516
+
517
+ {/* 配置 */}
518
+ <div className="flex items-start gap-3">
519
+ <div>
520
+ <Label className="text-[9px] text-gray-600 mb-0.5 block">配置</Label>
521
+ <AlignmentGrid
522
+ position={alignmentPosition}
523
+ onChange={handleAlignmentChange}
524
+ />
525
+ </div>
526
+ </div>
527
+ </div>
528
+ )}
529
+ </div>
530
+ );
531
+ }
532
+
533
+ // 配置グリッドコンポーネント
534
+ function AlignmentGrid({
535
+ position,
536
+ onChange
537
+ }: {
538
+ position: AlignmentPosition;
539
+ onChange: (position: AlignmentPosition) => void;
540
+ }) {
541
+ return (
542
+ <div className="grid grid-cols-3 gap-0.5 bg-[#383838] p-0.5 rounded">
543
+ {(['start', 'center', 'end'] as const).map(row => (
544
+ (['start', 'center', 'end'] as const).map(col => {
545
+ const pos = `${row}-${col}` as AlignmentPosition;
546
+ const isActive = position === pos;
547
+ return (
548
+ <button
549
+ key={pos}
550
+ data-al={`align-${pos}`}
551
+ data-active={isActive ? 'true' : 'false'}
552
+ aria-pressed={isActive}
553
+ title={`配置: ${pos}`}
554
+ onClick={() => onChange(pos)}
555
+ className={`w-3 h-3 rounded-sm transition-colors ${
556
+ isActive
557
+ ? 'bg-[#0d99ff]'
558
+ : 'bg-[#5a5a5a] hover:bg-[#6a6a6a]'
559
+ }`}
560
+ />
561
+ );
562
+ })
563
+ ))}
564
+ </div>
565
+ );
566
+ }
567
+
568
+ function TooltipButton({
569
+ icon,
570
+ label,
571
+ onClick,
572
+ active,
573
+ testId,
574
+ }: {
575
+ icon: React.ReactNode;
576
+ label: string;
577
+ onClick: () => void;
578
+ active?: boolean;
579
+ /** 自動検証・目視の両方から掴めるようにする目印(見た目には出ない) */
580
+ testId?: string;
581
+ }) {
582
+ const handleClick = (e: React.MouseEvent) => {
583
+ e.preventDefault();
584
+ e.stopPropagation();
585
+ onClick();
586
+ };
587
+
588
+ return (
589
+ <TooltipProvider delayDuration={300}>
590
+ <Tooltip>
591
+ <TooltipTrigger asChild>
592
+ <button
593
+ type="button"
594
+ data-al={testId}
595
+ data-active={active ? 'true' : 'false'}
596
+ aria-pressed={active ? true : false}
597
+ title={label}
598
+ className={`h-6 w-6 rounded-sm inline-flex items-center justify-center transition-colors ${
599
+ active
600
+ ? 'bg-[#0d99ff] text-white'
601
+ : 'text-gray-400 hover:text-white hover:bg-[#4a4a4a]'
602
+ }`}
603
+ onClick={handleClick}
604
+ onPointerDown={(e) => e.stopPropagation()}
605
+ >
606
+ {icon}
607
+ </button>
608
+ </TooltipTrigger>
609
+ <TooltipContent>
610
+ <p className="text-xs">{label}</p>
611
+ </TooltipContent>
612
+ </Tooltip>
613
+ </TooltipProvider>
614
+ );
615
+ }