@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,468 @@
1
+ /**
2
+ * スライドエディタの定数定義
3
+ */
4
+
5
+ // スライドの基準サイズ(16:9)
6
+ export const SLIDE_WIDTH = 1920;
7
+ export const SLIDE_HEIGHT = 1080;
8
+
9
+ // Webページの基準サイズ(フルハイト対応)
10
+ export const WEBPAGE_WIDTH = 1440;
11
+ export const WEBPAGE_MIN_HEIGHT = 900; // 最小高さ
12
+
13
+ // レスポンシブブレイクポイントプリセット(webpageモード用)
14
+ export const BREAKPOINT_PRESETS = [
15
+ { id: 'default', name: 'デフォルト', width: 1400, icon: 'monitor', minRange: 1190, maxRange: 1920 },
16
+ { id: 'small', name: 'スモール', width: 1140, icon: 'laptop', minRange: 990, maxRange: 1189 },
17
+ { id: 'tablet', name: 'タブレット', width: 840, icon: 'tablet', minRange: 690, maxRange: 989 },
18
+ { id: 'mobile', name: 'モバイル', width: 540, icon: 'mobile', minRange: 400, maxRange: 689 },
19
+ { id: 'mini', name: 'ミニ', width: 320, icon: 'mobile-small', minRange: 0, maxRange: 399 },
20
+ ] as const;
21
+
22
+ export type BreakpointPreset = typeof BREAKPOINT_PRESETS[number];
23
+ export type BreakpointId = BreakpointPreset['id'];
24
+ export type BreakpointIcon = BreakpointPreset['icon'];
25
+
26
+ // フォントファミリーリスト
27
+ export const FONT_FAMILIES = [
28
+ { value: 'Noto Sans JP', label: 'Noto Sans JP' },
29
+ { value: 'Arial', label: 'Arial' },
30
+ { value: 'Helvetica', label: 'Helvetica' },
31
+ { value: 'Georgia', label: 'Georgia' },
32
+ { value: 'Times New Roman', label: 'Times New Roman' },
33
+ { value: 'Courier New', label: 'Courier New' },
34
+ { value: 'Verdana', label: 'Verdana' },
35
+ { value: 'system-ui', label: 'System UI' },
36
+ ] as const;
37
+
38
+ // フォントウェイトリスト
39
+ export const FONT_WEIGHTS = [
40
+ { value: '100', label: 'Thin (100)' },
41
+ { value: '200', label: 'Extra Light (200)' },
42
+ { value: '300', label: 'Light (300)' },
43
+ { value: '400', label: 'Regular (400)' },
44
+ { value: '500', label: 'Medium (500)' },
45
+ { value: '600', label: 'Semi Bold (600)' },
46
+ { value: '700', label: 'Bold (700)' },
47
+ { value: '800', label: 'Extra Bold (800)' },
48
+ { value: '900', label: 'Black (900)' },
49
+ ] as const;
50
+
51
+ // ボーダースタイルリスト
52
+ export const BORDER_STYLES = [
53
+ { value: 'none', label: 'なし' },
54
+ { value: 'solid', label: '実線' },
55
+ { value: 'dashed', label: '破線' },
56
+ { value: 'dotted', label: '点線' },
57
+ ] as const;
58
+
59
+ // 画像フィルモードリスト
60
+ export const IMAGE_FILL_MODES = [
61
+ { value: 'fill', label: '塗り(アスペクト比無視)' },
62
+ { value: 'fit', label: '自動(収める)' },
63
+ { value: 'crop', label: 'トリミング(埋める)' },
64
+ { value: 'tile', label: 'タイル(繰り返し)' },
65
+ ] as const;
66
+
67
+ // デフォルトのシャドウ設定
68
+ export const DEFAULT_SHADOW = {
69
+ color: 'rgba(0, 0, 0, 0.25)',
70
+ x: 4,
71
+ y: 4,
72
+ blur: 10,
73
+ spread: 0,
74
+ } as const;
75
+
76
+ // マーキー選択のドラッグ閾値(クリックとドラッグを区別)
77
+ export const MARQUEE_DRAG_THRESHOLD = 5;
78
+
79
+ // プロパティパネルのデフォルト開閉状態
80
+ export const DEFAULT_PANEL_SECTIONS = {
81
+ layout: true,
82
+ position: true,
83
+ appearance: true,
84
+ image: true,
85
+ typography: true,
86
+ link: true,
87
+ fill: true,
88
+ stroke: false,
89
+ effects: false,
90
+ } as const;
91
+
92
+ // エディタ内のiframeに注入するスタイル
93
+ export const EDITOR_IFRAME_STYLES = `
94
+ html, body {
95
+ touch-action: none !important;
96
+ overscroll-behavior: none !important;
97
+ -webkit-text-size-adjust: none !important;
98
+ }
99
+ body {
100
+ background-color: transparent !important;
101
+ overflow: hidden !important; /* スクロールバー非表示 */
102
+ }
103
+ [data-editable="true"] {
104
+ outline: 2px solid transparent;
105
+ outline-offset: 0px;
106
+ transition: outline-color 0.15s ease;
107
+ cursor: pointer;
108
+ /* [移植時の修正] min-height:1em は罫線(h-px)や細い装飾要素を
109
+ 16px相当に膨らませて表示が崩れるため撤去。代わりに空要素だけ
110
+ 選択しやすい最小高さを与える(:empty かつ高さ指定なしのもの) */
111
+ }
112
+ [data-editable="true"]:empty:not([style*="height"]):not([class*="h-"]) {
113
+ min-height: 1em;
114
+ }
115
+ /*
116
+ * [選択セマンティクスの一本化] ホバー輪郭は CSS の :hover をやめる。
117
+ * :hover はカーソル下の「祖先チェーン全体」に当たるため、緑カードの上では
118
+ * 実測5本の輪郭が同時に出ていた。クリックで選ばれるのは最外の1つだけなので、
119
+ * 輪郭がクリック結果の予告になっていなかった。
120
+ * どの要素に付けるかは JS 側(useIframeSetup の hover mousemove)が
121
+ * クリックとまったく同じ resolver で決め、.hover-preview を1要素だけに付け替える。
122
+ * [暫定] 見た目の最終定義は item4 が上書きする前提の値。
123
+ */
124
+ /*
125
+ * [ホバーと選択の描き分け]
126
+ * ホバー = 「いまクリックしたら選ばれる要素」の予告なので、細く・淡く。
127
+ * 選択 = 2px の実線 + ハンドル(.selection-outline / .resize-handle)。
128
+ * 以前は両方とも 2px #0d99ff で、選択中かホバー中かが判別できなかった。
129
+ * 太さだけだと既定ズーム(51%)では 1px と 2px の差が画面上 0.5px しかないため、
130
+ * 色も変えて区別する。太さは --overlay-scale でズーム非依存にしている。
131
+ */
132
+ /*
133
+ * !important を付けるのは、スライド側が Tailwind の outline 系ユーティリティを
134
+ * 持っている場合に「同じ詳細度で後から読み込まれた側」に負けて、
135
+ * ホバー予告が黒い太線になってしまうのを防ぐため。
136
+ * 直後の .selected も !important なので、選択中の要素にカーソルが乗ったときは
137
+ * (後勝ちで)選択枠だけが出る。
138
+ */
139
+ [data-editable="true"].hover-preview {
140
+ outline: calc(1px * var(--overlay-scale, 1)) solid #66c0ff !important;
141
+ outline-offset: 0px !important;
142
+ }
143
+ [data-editable="true"].selected {
144
+ outline: none !important;
145
+ cursor: move;
146
+ }
147
+ /* 編集中の目印に背景色を敷いてはいけない。要素自身の塗り(ラベルの緑など)を
148
+ 上書きしてしまい、編集を始めた瞬間に色が消える。目印は下の .editing の
149
+ outline が担う */
150
+ [data-editable="true"].editing {
151
+ cursor: text !important;
152
+ }
153
+ [data-editable="true"].dragging {
154
+ opacity: 0.7;
155
+ cursor: grabbing !important;
156
+ }
157
+ ::selection {
158
+ background-color: rgba(13, 153, 255, 0.3);
159
+ }
160
+ body.move-mode {
161
+ cursor: grab !important;
162
+ }
163
+ body.move-mode:active {
164
+ cursor: grabbing !important;
165
+ }
166
+ body.move-mode [data-editable="true"] {
167
+ pointer-events: none;
168
+ }
169
+ /* トリミング中: エディタの選択枠を隠す(操作はcrop-modeが持つ) */
170
+ body.gg-cropping .selection-box {
171
+ display: none !important;
172
+ }
173
+
174
+ body.draw-mode {
175
+ cursor: crosshair !important;
176
+ }
177
+ body.draw-mode [data-editable="true"] {
178
+ pointer-events: none;
179
+ }
180
+ body.text-mode {
181
+ cursor: text !important;
182
+ }
183
+ body.text-mode [data-editable="true"] {
184
+ pointer-events: none;
185
+ }
186
+ body:not(.draw-mode):not(.text-mode):not(.move-mode) [data-editable="true"] {
187
+ pointer-events: auto !important;
188
+ cursor: pointer;
189
+ }
190
+ .drawing-preview {
191
+ position: absolute;
192
+ pointer-events: none;
193
+ opacity: 0.7;
194
+ border: 2px dashed #0d99ff;
195
+ background-color: rgba(13, 153, 255, 0.1);
196
+ }
197
+ .drawing-preview.ellipse {
198
+ border-radius: 50%;
199
+ }
200
+ .drawing-preview.frame {
201
+ border: 2px dashed #9CA3AF;
202
+ background-color: transparent;
203
+ }
204
+ .drawing-preview svg {
205
+ overflow: visible;
206
+ }
207
+ /* ==========================================================
208
+ * 選択オーバーレイ(枠 / ハンドル / 当たり判定)
209
+ * ==========================================================
210
+ * [なぜ CSS変数でズームを打ち消すか]
211
+ * オーバーレイは #artboard の中に置かれるので #artboard-wrapper の
212
+ * transform: scale() を要素と一緒に受ける。8px 固定にすると既定ズーム(51%)では
213
+ * 画面上 4.1px しか無く、掴み代として成立しない(実測: NEハンドル 4.08px)。
214
+ * dom-utils の applyOverlayScale() が <html> に
215
+ * --overlay-scale = 1 / zoom
216
+ * を書き込み、すべての寸法にこれを掛けることで「画面上の実寸」を一定にする。
217
+ * 変数が未設定でも壊れないよう既定値 1 をフォールバックに持たせる。
218
+ *
219
+ * [なぜ見た目と当たり判定を分けるか]
220
+ * 見た目 8px のままでは掴みにくく、逆にハンドルを大きく描くと紙面が見えない。
221
+ * そこで .resize-handle 自体を「透明な12px の当たり判定」にし、
222
+ * 見える四角は ::after で描く。
223
+ * ハンドルを子要素にせず疑似要素にしているのは、mousedown 側
224
+ * (useElementSelection / useIframeInitializer / MultiPageCanvasView) が
225
+ * e.target の data-handle を「直接」読む実装で、子要素だと属性が取れず
226
+ * 無反応になるため。
227
+ *
228
+ * [z-index について]
229
+ * .selection-box は position:absolute + z-index を持つので stacking context に
230
+ * なる。中の 10001 は「箱の中の順序」でしかなく、以前は追加順で
231
+ * 角丸ハンドルが NE を完全に覆っていた(実測: NE中心の elementFromPoint が
232
+ * border-radius-handle を返す)。順序を明示的な値で固定する:
233
+ * 角/中点ハンドル 30 > 辺の帯 20 > 角丸 10 > 回転 5
234
+ */
235
+ .selection-box {
236
+ position: absolute;
237
+ pointer-events: none;
238
+ z-index: 10000;
239
+ /* 画面上の実寸(px) → CSS px 換算値 */
240
+ --ov: var(--overlay-scale, 1);
241
+ --handle-visual: calc(8px * var(--ov)); /* 見える四角 8px */
242
+ --handle-hit: calc(12px * var(--ov)); /* 掴み代 12px */
243
+ --handle-hit-half: calc(6px * var(--ov));
244
+ --edge-hit: calc(8px * var(--ov)); /* 辺の帯 8px */
245
+ --edge-hit-half: calc(4px * var(--ov));
246
+ --outline-w: calc(2px * var(--ov));
247
+ --rot-gap: calc(10px * var(--ov)); /* 角ハンドル(±6px)の外側に4pxの隙間 */
248
+ --rot-size: calc(22px * var(--ov));
249
+ }
250
+ .resize-handle {
251
+ position: absolute;
252
+ width: var(--handle-hit);
253
+ height: var(--handle-hit);
254
+ background: transparent;
255
+ border: none;
256
+ border-radius: 0;
257
+ box-sizing: border-box;
258
+ pointer-events: auto;
259
+ z-index: 30;
260
+ }
261
+ .resize-handle::after {
262
+ content: '';
263
+ position: absolute;
264
+ top: 50%;
265
+ left: 50%;
266
+ width: var(--handle-visual);
267
+ height: var(--handle-visual);
268
+ margin: calc(var(--handle-visual) / -2) 0 0 calc(var(--handle-visual) / -2);
269
+ background: #ffffff;
270
+ /* border-width は 1px 未満に下がらない(Chrome が切り上げる)ので、
271
+ 400%ズームでは 8px の四角が枠線で埋まって真っ青になる。
272
+ spread が小数のまま効く box-shadow で縁取りする。 */
273
+ box-shadow: 0 0 0 calc(1px * var(--ov)) #0d99ff;
274
+ border: none;
275
+ border-radius: calc(1px * var(--ov));
276
+ box-sizing: border-box;
277
+ }
278
+ /* 角・辺中点: 当たり判定の中心を枠の角/中点に合わせる */
279
+ .resize-handle.nw { top: calc(var(--handle-hit-half) * -1); left: calc(var(--handle-hit-half) * -1); cursor: nwse-resize; }
280
+ .resize-handle.ne { top: calc(var(--handle-hit-half) * -1); right: calc(var(--handle-hit-half) * -1); cursor: nesw-resize; }
281
+ .resize-handle.sw { bottom: calc(var(--handle-hit-half) * -1); left: calc(var(--handle-hit-half) * -1); cursor: nesw-resize; }
282
+ .resize-handle.se { bottom: calc(var(--handle-hit-half) * -1); right: calc(var(--handle-hit-half) * -1); cursor: nwse-resize; }
283
+ .resize-handle.n { top: calc(var(--handle-hit-half) * -1); left: 50%; margin-left: calc(var(--handle-hit-half) * -1); cursor: ns-resize; }
284
+ .resize-handle.s { bottom: calc(var(--handle-hit-half) * -1); left: 50%; margin-left: calc(var(--handle-hit-half) * -1); cursor: ns-resize; }
285
+ .resize-handle.w { left: calc(var(--handle-hit-half) * -1); top: 50%; margin-top: calc(var(--handle-hit-half) * -1); cursor: ew-resize; }
286
+ .resize-handle.e { right: calc(var(--handle-hit-half) * -1); top: 50%; margin-top: calc(var(--handle-hit-half) * -1); cursor: ew-resize; }
287
+ /*
288
+ * 辺(エッジ)の当たり判定。Figma と同じく「辺を掴んで1軸リサイズ」できるようにする。
289
+ * 見た目は持たない(枠線は .selection-outline が描く)。
290
+ * data-handle は n/e/s/w のまま。useDragResize が name.includes('e') 等で軸を
291
+ * 判定しているため、独自の値(edge-n 等)にすると軸判定が壊れる。
292
+ * 位置指定用のクラス名だけ edge-top/right/bottom/left にして、
293
+ * 上の .resize-handle.n などの指定と衝突しないようにしている。
294
+ */
295
+ .resize-handle.edge {
296
+ z-index: 20;
297
+ border-radius: 0;
298
+ }
299
+ .resize-handle.edge::after { content: none; }
300
+ .resize-handle.edge.edge-top { top: calc(var(--edge-hit-half) * -1); left: 0; width: 100%; height: var(--edge-hit); margin: 0; cursor: ns-resize; }
301
+ .resize-handle.edge.edge-bottom { bottom: calc(var(--edge-hit-half) * -1); left: 0; width: 100%; height: var(--edge-hit); margin: 0; cursor: ns-resize; }
302
+ .resize-handle.edge.edge-left { left: calc(var(--edge-hit-half) * -1); top: 0; width: var(--edge-hit); height: 100%; margin: 0; cursor: ew-resize; }
303
+ .resize-handle.edge.edge-right { right: calc(var(--edge-hit-half) * -1); top: 0; width: var(--edge-hit); height: 100%; margin: 0; cursor: ew-resize; }
304
+ /*
305
+ * 角丸ハンドル。
306
+ * 以前は NE ハンドルと同じ z-index で NE の後に append され、
307
+ * border-radius:0 の要素では top/right:0px となって NE を完全に覆っていた。
308
+ * 位置(内側オフセットの下限)は dom-utils 側で角ハンドルと重ならない値に
309
+ * クランプし、ここでは重なった場合でも必ず負けるよう z-index を下げる。
310
+ */
311
+ .border-radius-handle {
312
+ position: absolute;
313
+ width: var(--handle-hit);
314
+ height: var(--handle-hit);
315
+ background: transparent;
316
+ border: none;
317
+ box-sizing: border-box;
318
+ pointer-events: auto;
319
+ z-index: 10;
320
+ cursor: pointer;
321
+ }
322
+ .border-radius-handle::after {
323
+ content: '';
324
+ position: absolute;
325
+ top: 50%;
326
+ left: 50%;
327
+ width: var(--handle-visual);
328
+ height: var(--handle-visual);
329
+ margin: calc(var(--handle-visual) / -2) 0 0 calc(var(--handle-visual) / -2);
330
+ background: #ffffff;
331
+ box-shadow: 0 0 0 calc(1px * var(--ov)) #f97316;
332
+ border: none;
333
+ border-radius: 50%;
334
+ box-sizing: border-box;
335
+ }
336
+ /* transform:scale は当たり判定の位置をずらすので、色だけ変える */
337
+ .border-radius-handle:hover::after {
338
+ background: #f97316;
339
+ }
340
+ /*
341
+ * 複数選択時の「群バウンディングボックス」(Figma相当)
342
+ * 選択中の全要素の矩形の和集合を1枚の枠として描く。
343
+ * .selection-box を兼ねているので、保存時のサニタイズ処理に自動的に拾われる。
344
+ * メンバーの枠(10000)より前面に置き、群のハンドルが必ず掴めるようにする。
345
+ */
346
+ .selection-box.selection-bounds {
347
+ z-index: 10002;
348
+ background: transparent;
349
+ }
350
+ /* 群の枠は少し外側に出して、メンバーの枠と重ならないようにする */
351
+ .selection-box.selection-bounds > .selection-outline {
352
+ inset: calc(-2px * var(--ov));
353
+ }
354
+ /*
355
+ * [なぜ outline ではなく box-shadow か]
356
+ * outline-width の使用値は整数CSSpxに切り捨てられる(実測: 2px×1.96 = 3.92px が
357
+ * "3px" になり、ズーム51%では画面上1.5pxまで痩せる)。
358
+ * box-shadow の spread は小数のまま描かれるので、
359
+ * 「画面上ちょうど2px」を保てる。この要素はオーバーレイ専用の div なので、
360
+ * スライド側の box-shadow を壊す心配もない。
361
+ */
362
+ .selection-box .selection-outline {
363
+ position: absolute;
364
+ inset: 0;
365
+ border: none;
366
+ outline: none;
367
+ box-shadow: 0 0 0 var(--outline-w) #0d99ff;
368
+ pointer-events: none;
369
+ }
370
+ /*
371
+ * 複数選択時のメンバーは「細い輪郭のみ」。
372
+ * ハンドルとサイズラベルは群バウンディングボックス1枚に集約する
373
+ * (40件マーキーすると 40個のサイズラベルが重なって紙面が読めなくなるため)。
374
+ */
375
+ .selection-box.selection-member .selection-outline {
376
+ box-shadow: 0 0 0 calc(1px * var(--ov)) rgba(13, 153, 255, 0.75);
377
+ }
378
+ .size-label {
379
+ position: absolute;
380
+ bottom: calc(-22px * var(--ov));
381
+ left: 50%;
382
+ transform: translateX(-50%);
383
+ background: #0d99ff;
384
+ color: white;
385
+ font-size: calc(10px * var(--ov));
386
+ line-height: 1.4;
387
+ padding: calc(2px * var(--ov)) calc(6px * var(--ov));
388
+ border-radius: calc(2px * var(--ov));
389
+ white-space: nowrap;
390
+ pointer-events: none;
391
+ z-index: 40;
392
+ }
393
+ /*
394
+ * 回転ハンドル。角ハンドルの当たり判定(±6px)の外側に 4px の隙間を空けて置く。
395
+ * 以前は角から 6px 外に出た時点で回転域に入り、掴んだつもりが無言で
396
+ * transform:rotate() を書き込んでいた。
397
+ */
398
+ .rotation-handle {
399
+ position: absolute;
400
+ width: var(--rot-size);
401
+ height: var(--rot-size);
402
+ background: transparent;
403
+ border: none;
404
+ pointer-events: auto;
405
+ z-index: 5;
406
+ cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%230d99ff' stroke-width='2'%3E%3Cpath d='M21 12a9 9 0 1 1-9-9'/%3E%3Cpath d='M21 3v9h-9'/%3E%3C/svg%3E") 12 12, pointer;
407
+ }
408
+ .rotation-handle.nw { top: calc((var(--rot-gap) + var(--rot-size)) * -1); left: calc((var(--rot-gap) + var(--rot-size)) * -1); }
409
+ .rotation-handle.ne { top: calc((var(--rot-gap) + var(--rot-size)) * -1); right: calc((var(--rot-gap) + var(--rot-size)) * -1); }
410
+ .rotation-handle.sw { bottom: calc((var(--rot-gap) + var(--rot-size)) * -1); left: calc((var(--rot-gap) + var(--rot-size)) * -1); }
411
+ .rotation-handle.se { bottom: calc((var(--rot-gap) + var(--rot-size)) * -1); right: calc((var(--rot-gap) + var(--rot-size)) * -1); }
412
+ .rotation-handle:hover {
413
+ background: rgba(13, 153, 255, 0.12);
414
+ border-radius: 50%;
415
+ }
416
+ /*
417
+ * 右上だけ ⟳ を出して回転域の在り処を示す(PowerPointの回転ハンドル相当)。
418
+ * 印は表示専用。当たり判定は親の .rotation-handle が持ったままにする
419
+ * (mousedown は e.target の data-handle だけを見るため)。
420
+ */
421
+ /* 印は掴み代(22px)より小さく描く。当たり判定は広く、見た目は邪魔しない */
422
+ .rotation-handle-glyph {
423
+ position: absolute;
424
+ top: 50%;
425
+ left: 50%;
426
+ transform: translate(-50%, -50%);
427
+ width: calc(14px * var(--ov));
428
+ height: calc(14px * var(--ov));
429
+ display: flex;
430
+ align-items: center;
431
+ justify-content: center;
432
+ background: #ffffff;
433
+ border-radius: 50%;
434
+ box-shadow: 0 0 0 calc(1px * var(--ov)) #0d99ff;
435
+ color: #0d99ff;
436
+ font-size: calc(9px * var(--ov));
437
+ line-height: 1;
438
+ pointer-events: none;
439
+ user-select: none;
440
+ }
441
+ body.rotating {
442
+ cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%230d99ff' stroke-width='2'%3E%3Cpath d='M21 12a9 9 0 1 1-9-9'/%3E%3Cpath d='M21 3v9h-9'/%3E%3C/svg%3E") 12 12, pointer !important;
443
+ }
444
+ .marquee-selection-box {
445
+ position: absolute;
446
+ border: 1px dashed #0d99ff;
447
+ background-color: rgba(13, 153, 255, 0.1);
448
+ pointer-events: none;
449
+ z-index: 10000;
450
+ }
451
+ [data-editable="true"].marquee-hover {
452
+ outline: calc(2px * var(--overlay-scale, 1)) solid rgba(13, 153, 255, 0.6) !important;
453
+ outline-offset: calc(1px * var(--overlay-scale, 1));
454
+ }
455
+ /* テキスト要素ホバー時の下線(Figmaスタイル) */
456
+ [data-editable="true"].text-editable-hover {
457
+ text-decoration: underline !important;
458
+ text-decoration-color: #0d99ff !important;
459
+ text-decoration-thickness: 2px !important;
460
+ text-underline-offset: 2px !important;
461
+ }
462
+ /* 編集中のテキスト要素 */
463
+ [data-editable="true"].editing {
464
+ outline: calc(2px * var(--overlay-scale, 1)) solid #0d99ff !important;
465
+ outline-offset: calc(2px * var(--overlay-scale, 1));
466
+ cursor: text !important;
467
+ }
468
+ `;
@@ -0,0 +1,229 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * EditorArtboardContext
5
+ *
6
+ * エディタのアートボード管理を行うContext
7
+ * 変更頻度: 低(アートボード切り替え時)
8
+ *
9
+ * 含まれる状態:
10
+ * - artboardStates: アートボードごとの状態Map
11
+ * - activeArtboardId: アクティブなアートボードID
12
+ * - setActiveArtboard: アートボード切り替え(状態保存・復元付き)
13
+ * - getArtboardState/updateArtboardState: 状態取得・更新
14
+ * - contentList: コンテンツリスト(スライド、ページ等)
15
+ */
16
+
17
+ import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
18
+ import type { DOMTreeNode } from '../types';
19
+
20
+ // コンテンツリスト用の軽量型(スライド、ページ、コンポーネント等で共用)
21
+ export interface ContentListItem {
22
+ id: string;
23
+ title: string;
24
+ /** 表示順序(スライド番号、ページ番号等) */
25
+ order: number;
26
+ thumbnailHtml?: string;
27
+ }
28
+
29
+ // アートボード(スライド/ページ)ごとの状態
30
+ export interface ArtboardState {
31
+ id: string;
32
+ zoom: number;
33
+ scrollPosition: { x: number; y: number };
34
+ selectedElementIds: string[];
35
+ domTree: DOMTreeNode[];
36
+ expandedNodes: Set<string>;
37
+ html: string;
38
+ }
39
+
40
+ export interface EditorArtboardContextValue {
41
+ // アートボード状態管理
42
+ artboardStates: Map<string, ArtboardState>;
43
+ activeArtboardId: string | null;
44
+ setActiveArtboard: (id: string) => void;
45
+ getArtboardState: (id: string) => ArtboardState | undefined;
46
+ updateArtboardState: (id: string, updates: Partial<ArtboardState>) => void;
47
+
48
+ // コンテンツリスト
49
+ contentList: ContentListItem[];
50
+ currentContentId: string | null;
51
+ onContentChange: ((contentId: string) => void) | null;
52
+
53
+ // Deprecated aliases
54
+ /** @deprecated Use contentList instead */
55
+ slides: ContentListItem[];
56
+ /** @deprecated Use currentContentId instead */
57
+ currentSlideId: string | null;
58
+ /** @deprecated Use onContentChange instead */
59
+ onSlideChange: ((slideId: string) => void) | null;
60
+ }
61
+
62
+ const EditorArtboardContext = createContext<EditorArtboardContextValue | null>(null);
63
+
64
+ export function useEditorArtboard(): EditorArtboardContextValue {
65
+ const context = useContext(EditorArtboardContext);
66
+ if (!context) {
67
+ throw new Error('useEditorArtboard must be used within EditorArtboardProvider');
68
+ }
69
+ return context;
70
+ }
71
+
72
+ interface EditorArtboardProviderProps {
73
+ contentList?: ContentListItem[];
74
+ currentContentId?: string;
75
+ onContentChange?: (contentId: string) => void;
76
+ // 状態同期用のコールバック(ファサードで設定)
77
+ onArtboardSwitch?: (
78
+ newId: string,
79
+ currentState: {
80
+ zoom: number;
81
+ selectedElementIds: string[];
82
+ domTree: DOMTreeNode[];
83
+ expandedNodes: Set<string>;
84
+ html: string;
85
+ }
86
+ ) => {
87
+ zoom: number;
88
+ selectedElementIds: string[];
89
+ domTree: DOMTreeNode[];
90
+ expandedNodes: Set<string>;
91
+ html: string;
92
+ } | null;
93
+ // Deprecated props
94
+ /** @deprecated Use contentList instead */
95
+ slides?: ContentListItem[];
96
+ /** @deprecated Use currentContentId instead */
97
+ currentSlideId?: string;
98
+ /** @deprecated Use onContentChange instead */
99
+ onSlideChange?: (slideId: string) => void;
100
+ children: React.ReactNode;
101
+ }
102
+
103
+ export function EditorArtboardProvider({
104
+ contentList: contentListProp,
105
+ currentContentId: currentContentIdProp,
106
+ onContentChange: onContentChangeProp,
107
+ slides: slidesProp,
108
+ currentSlideId: currentSlideIdProp,
109
+ onSlideChange: onSlideChangeProp,
110
+ children,
111
+ }: EditorArtboardProviderProps) {
112
+ // Support deprecated props
113
+ const contentList = contentListProp ?? slidesProp ?? [];
114
+ const currentContentId = currentContentIdProp ?? currentSlideIdProp ?? null;
115
+ const onContentChange = onContentChangeProp ?? onSlideChangeProp ?? null;
116
+
117
+ // Deprecated aliases
118
+ const slides = contentList;
119
+ const currentSlideId = currentContentId;
120
+ const onSlideChange = onContentChange;
121
+
122
+ // アートボード状態管理(contentListから初期状態を同期的に生成)
123
+ const [artboardStates, setArtboardStates] = useState<Map<string, ArtboardState>>(() => {
124
+ const initialStates = new Map<string, ArtboardState>();
125
+ contentList.forEach(content => {
126
+ initialStates.set(content.id, {
127
+ id: content.id,
128
+ zoom: 100,
129
+ scrollPosition: { x: 0, y: 0 },
130
+ selectedElementIds: [],
131
+ domTree: [],
132
+ expandedNodes: new Set(),
133
+ html: content.thumbnailHtml || '',
134
+ });
135
+ });
136
+ return initialStates;
137
+ });
138
+ const [activeArtboardId, setActiveArtboardId] = useState<string | null>(currentContentId);
139
+
140
+ // アートボード状態取得
141
+ const getArtboardState = useCallback((id: string): ArtboardState | undefined => {
142
+ return artboardStates.get(id);
143
+ }, [artboardStates]);
144
+
145
+ // アートボード状態更新
146
+ const updateArtboardState = useCallback((id: string, updates: Partial<ArtboardState>) => {
147
+ setArtboardStates(prev => {
148
+ const newMap = new Map(prev);
149
+ const existing = newMap.get(id);
150
+ if (existing) {
151
+ newMap.set(id, { ...existing, ...updates });
152
+ } else {
153
+ newMap.set(id, {
154
+ id,
155
+ zoom: 100,
156
+ scrollPosition: { x: 0, y: 0 },
157
+ selectedElementIds: [],
158
+ domTree: [],
159
+ expandedNodes: new Set(),
160
+ html: '',
161
+ ...updates,
162
+ });
163
+ }
164
+ return newMap;
165
+ });
166
+ }, []);
167
+
168
+ // アクティブアートボード切り替え
169
+ // 注: 状態保存・復元はファサード(EditorProvider)で行う
170
+ const setActiveArtboard = useCallback((id: string) => {
171
+ setActiveArtboardId(id);
172
+
173
+ // コンテンツ変更コールバックを呼び出し
174
+ if (onContentChange && id !== currentContentId) {
175
+ onContentChange(id);
176
+ }
177
+ }, [onContentChange, currentContentId]);
178
+
179
+ // コンテンツリストが変わったときにアートボード状態を初期化
180
+ useEffect(() => {
181
+ const newStates = new Map<string, ArtboardState>();
182
+ contentList.forEach(content => {
183
+ const existing = artboardStates.get(content.id);
184
+ if (existing) {
185
+ newStates.set(content.id, existing);
186
+ } else {
187
+ newStates.set(content.id, {
188
+ id: content.id,
189
+ zoom: 100,
190
+ scrollPosition: { x: 0, y: 0 },
191
+ selectedElementIds: [],
192
+ domTree: [],
193
+ expandedNodes: new Set(),
194
+ html: content.thumbnailHtml || '',
195
+ });
196
+ }
197
+ });
198
+ setArtboardStates(newStates);
199
+ // eslint-disable-next-line react-hooks/exhaustive-deps
200
+ }, [contentList]); // artboardStatesは意図的に依存配列から除外(無限ループ防止)
201
+
202
+ // currentContentIdが変わったときにactiveArtboardIdを同期
203
+ useEffect(() => {
204
+ if (currentContentId && currentContentId !== activeArtboardId) {
205
+ setActiveArtboardId(currentContentId);
206
+ }
207
+ }, [currentContentId, activeArtboardId]);
208
+
209
+ const value: EditorArtboardContextValue = {
210
+ artboardStates,
211
+ activeArtboardId,
212
+ setActiveArtboard,
213
+ getArtboardState,
214
+ updateArtboardState,
215
+ contentList,
216
+ currentContentId,
217
+ onContentChange,
218
+ // Deprecated
219
+ slides,
220
+ currentSlideId,
221
+ onSlideChange,
222
+ };
223
+
224
+ return (
225
+ <EditorArtboardContext.Provider value={value}>
226
+ {children}
227
+ </EditorArtboardContext.Provider>
228
+ );
229
+ }