@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,524 @@
1
+ "use client";
2
+
3
+ import { useRef, useState } from "react";
4
+ import { Button } from "../../components/ui/button";
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuSeparator,
10
+ DropdownMenuTrigger,
11
+ } from "../../components/ui/dropdown-menu";
12
+ import {
13
+ ZoomIn,
14
+ ZoomOut,
15
+ Maximize,
16
+ RotateCcw,
17
+ Import,
18
+ Code,
19
+ Braces,
20
+ Settings,
21
+ FileText,
22
+ FolderArchive,
23
+ ChevronDown,
24
+ MoreHorizontal,
25
+ MessageSquare,
26
+ } from "lucide-react";
27
+ import { useEditorContext } from "../EditorContext";
28
+ import { EditorTopBar } from "./shell/EditorTopBar";
29
+ import { MIN_ZOOM, MAX_ZOOM, editorZoomApiRef } from "../hooks/useCanvasControls";
30
+ import { generateEditableHtml } from "../utils/html-utils";
31
+ import { useDeck } from "../../components/viewer/useDeck";
32
+ import { unresolvedCount } from "./ppt/PptComments";
33
+ import {
34
+ convertToAbsolutePositioning,
35
+ buildDomTree,
36
+ getArtboardContent,
37
+ } from "../utils/dom-utils";
38
+ import { can } from "../../io";
39
+
40
+ interface EditorHeaderProps {
41
+ /** コメントパネルの開閉(Figma風にもPowerPoint同等のコメントを出す) */
42
+ comments?: { open: boolean; toggle: () => void; page: number };
43
+ /** PowerPoint風UIへの切り替え(任意) */
44
+ onSwitchUi?: () => void;
45
+ onSave: (html: string) => Promise<void>;
46
+ onSaveSettings?: () => Promise<void>;
47
+ onClose: () => void;
48
+ /**
49
+ * 自動保存の状態。エディタ本体(FrontendVisualEditor)が算出したものを表示するだけ。
50
+ * dirty=このあと自動保存される / saving=保存中 / saved=保存済み / error=自動保存に失敗
51
+ */
52
+ saveStatus?: 'saved' | 'dirty' | 'saving' | 'error';
53
+ /** キャンバス編集モード: trueの場合、閉じるボタンを「キャンバスに戻る」に変更 */
54
+ isCanvasEditing?: boolean;
55
+ onImport?: () => void;
56
+ onCssEdit?: () => void;
57
+ hasCss?: boolean;
58
+ onJsEdit?: () => void;
59
+ hasJs?: boolean;
60
+ onPageSettings?: () => void;
61
+ hasPageSettings?: boolean;
62
+ onExport?: (format: 'html' | 'zip') => void;
63
+ /** [移植時の追加] 編集中スライドの番号(1始まり)。ビューアと同じ文脈表示に使う */
64
+ contextNumber?: number;
65
+ /** [移植時の追加] 編集中スライドのタイトル */
66
+ contextTitle?: string;
67
+ }
68
+
69
+ export function EditorHeader({
70
+ onSwitchUi,
71
+ onSave,
72
+ onSaveSettings,
73
+ onClose,
74
+ saveStatus = 'saved',
75
+ isCanvasEditing,
76
+ onImport,
77
+ onCssEdit,
78
+ hasCss,
79
+ onJsEdit,
80
+ hasJs,
81
+ onPageSettings,
82
+ hasPageSettings,
83
+ onExport,
84
+ contextNumber,
85
+ comments,
86
+ contextTitle,
87
+ }: EditorHeaderProps) {
88
+ const {
89
+ iframeRef,
90
+ originalHtml,
91
+ zoom,
92
+ setZoom,
93
+ fitZoom,
94
+ setSelectedElement,
95
+ setSelectedElementIds,
96
+ setHtml,
97
+ getIframeDoc,
98
+ layoutMode,
99
+ setLayoutMode,
100
+ notifyIframeChange,
101
+ setDomTree,
102
+ setExpandedNodes,
103
+ autoLayoutHtml,
104
+ setAutoLayoutHtml,
105
+ editorMode,
106
+ } = useEditorContext();
107
+
108
+ // 保存・共有・プレビュー・閉じるは共通トップバー(EditorTopBar)が持つ。
109
+ // ここに実装を残すと、PowerPoint風UIと2本の保存経路ができてしまう
110
+
111
+ // リセット処理
112
+ const handleReset = () => {
113
+ const iframe = iframeRef.current;
114
+ if (iframe) {
115
+ const blob = new Blob([generateEditableHtml(originalHtml, editorMode)], {
116
+ type: "text/html",
117
+ });
118
+ iframe.src = URL.createObjectURL(blob);
119
+ }
120
+ setHtml(originalHtml);
121
+ setSelectedElement(null);
122
+ setSelectedElementIds([]);
123
+ };
124
+
125
+ // レイアウトモード切替
126
+ const handleLayoutModeChange = (mode: "absolute" | "auto") => {
127
+ // Webページを絶対配置へ倒すと流し込みレイアウトが固定ピクセルに固まるため受け付けない
128
+ if (editorMode === "webpage" && mode === "absolute") return;
129
+ // キャンバスモードでは状態が実際の表示と不整合の場合があるため、
130
+ // 同じモードでもクリック時に強制変換する(isCanvasEditing時)
131
+ if (mode === layoutMode && !isCanvasEditing) return;
132
+
133
+ const iframeDoc = getIframeDoc();
134
+ if (!iframeDoc) return;
135
+
136
+ if (mode === "absolute") {
137
+ // オートレイアウト → 絶対配置: 変換前のHTMLを保存(リセット用)
138
+ setAutoLayoutHtml(getArtboardContent(iframeDoc));
139
+
140
+ // 全要素を絶対配置に変換
141
+ const count = convertToAbsolutePositioning(iframeDoc);
142
+ console.log(
143
+ "[Header] Converted",
144
+ count,
145
+ "elements to absolute positioning",
146
+ );
147
+
148
+ // DOMツリーを再構築
149
+ const tree = buildDomTree(iframeDoc);
150
+ setDomTree(tree);
151
+ setExpandedNodes(new Set(tree.map((n) => n.id)));
152
+
153
+ // 変更を通知
154
+ notifyIframeChange();
155
+ } else if (mode === "auto") {
156
+ // [移植時の修正] 絶対配置 → オートレイアウト
157
+ // 元実装はインラインstyleを削るだけで、classベースのレイアウト
158
+ // (Tailwind等)が絶対配置変換で崩れたケースを復元できなかった。
159
+ // 変換前に保存した autoLayoutHtml があればそれを復元する。
160
+ if (autoLayoutHtml) {
161
+ const ok = window.confirm(
162
+ "オートレイアウトに戻します。\n絶対配置モードで加えた変更は破棄され、切り替え前の状態に復元されます。よろしいですか?",
163
+ );
164
+ if (!ok) return;
165
+ const artboard = iframeDoc.getElementById("artboard");
166
+ if (artboard) {
167
+ artboard.innerHTML = autoLayoutHtml;
168
+ setAutoLayoutHtml(null);
169
+ const restoredTree = buildDomTree(iframeDoc);
170
+ setDomTree(restoredTree);
171
+ setExpandedNodes(new Set(restoredTree.map((n) => n.id)));
172
+ setSelectedElement(null);
173
+ setSelectedElementIds([]);
174
+ notifyIframeChange();
175
+ setLayoutMode(mode);
176
+ return;
177
+ }
178
+ }
179
+
180
+ // フォールバック: 変換前HTMLが無い場合は従来どおりインラインstyleを削除
181
+ let editableElements = iframeDoc.querySelectorAll(
182
+ '[data-editable="true"]',
183
+ );
184
+ // フォールバック: data-editableが無い場合は#artboard直下のdata-element-id要素
185
+ if (editableElements.length === 0) {
186
+ editableElements = iframeDoc.querySelectorAll(
187
+ '#artboard > [data-element-id]',
188
+ );
189
+ }
190
+ editableElements.forEach((el) => {
191
+ const element = el as HTMLElement;
192
+ // 絶対配置関連のスタイルを削除
193
+ element.style.removeProperty("position");
194
+ element.style.removeProperty("left");
195
+ element.style.removeProperty("top");
196
+ // width/heightは削除(autoに戻す)
197
+ element.style.removeProperty("width");
198
+ element.style.removeProperty("height");
199
+ element.style.removeProperty("margin");
200
+ element.style.removeProperty("flex");
201
+ element.style.removeProperty("flex-grow");
202
+ element.style.removeProperty("flex-shrink");
203
+ });
204
+ console.log(
205
+ "[Header] Removed absolute positioning from",
206
+ editableElements.length,
207
+ "elements",
208
+ );
209
+
210
+ // 保存したHTMLをクリア
211
+ setAutoLayoutHtml(null);
212
+
213
+ // DOMツリーを再構築
214
+ const tree = buildDomTree(iframeDoc);
215
+ setDomTree(tree);
216
+ setExpandedNodes(new Set(tree.map((n) => n.id)));
217
+
218
+ // 選択をクリア
219
+ setSelectedElement(null);
220
+ setSelectedElementIds([]);
221
+
222
+ // 変更を通知
223
+ notifyIframeChange();
224
+ }
225
+
226
+ setLayoutMode(mode);
227
+ };
228
+
229
+ // ズーム操作
230
+ //
231
+ // [移植時の修正] 刻みは従来どおり 10% のままにして、頭打ちの値だけ直す。
232
+ // 以前は上限200%/下限25%で止めていたが、キャンバス側(useCanvasControls)の
233
+ // 実際の可動域は 10〜400%。ボタンだけ手前で止まるため、ホイールで250%まで
234
+ // 拡大したあとに「+を押しても何も起きない」状態が生まれていた。
235
+ const clampZoom = (v: number) => Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, v));
236
+ const canZoomIn = zoom < MAX_ZOOM - 0.01;
237
+ const canZoomOut = zoom > MIN_ZOOM + 0.01;
238
+
239
+ const handleZoomIn = () => setZoom(clampZoom(Math.floor(zoom / 10) * 10 + 10));
240
+ const handleZoomOut = () => setZoom(clampZoom(Math.ceil(zoom / 10) * 10 - 10));
241
+ // 全体表示/100% はキャンバス側(useCanvasControls)の実装に委ねる。
242
+ // Cmd+0 / Cmd+1 と同じ関数を呼ぶことで、「ヘッダーから押したときだけ
243
+ // 固定点の扱いが違う」というズレを作らない。未登録時は従来どおり値を入れる。
244
+ const handleFitZoom = () => {
245
+ const api = editorZoomApiRef.current;
246
+ if (api) api.fit();
247
+ else setZoom(fitZoom);
248
+ };
249
+ const handleActualZoom = () => {
250
+ const api = editorZoomApiRef.current;
251
+ if (api) api.actual();
252
+ else setZoom(100);
253
+ };
254
+
255
+ // 倍率表示のメニュー
256
+ //
257
+ // [移植時の修正] ここは以前ただの <span> で、ダブルクリックしたときだけ
258
+ // 100% に戻るという隠し機能があるだけだった。倍率は「見るもの」ではなく
259
+ // 「決めるもの」なので、押せることが分かるボタンにして
260
+ // 全体表示 / 100% / 50% / 200% と直接入力をまとめて置く。
261
+ // 検証・自動操作から掴みやすいよう data-zoom-* を目印に付けている。
262
+ const [zoomMenuOpen, setZoomMenuOpen] = useState(false);
263
+ const [zoomInput, setZoomInput] = useState("");
264
+ const zoomInputRef = useRef<HTMLInputElement>(null);
265
+
266
+ const applyZoomInput = () => {
267
+ const parsed = parseFloat(zoomInput);
268
+ if (!Number.isFinite(parsed)) return;
269
+ setZoom(clampZoom(parsed));
270
+ setZoomMenuOpen(false);
271
+ };
272
+
273
+ const openZoomMenu = (open: boolean) => {
274
+ setZoomMenuOpen(open);
275
+ if (!open) return;
276
+ // 開いたときは現在値を初期値にする(そのまま Enter しても倍率が変わらない)
277
+ setZoomInput(String(Math.round(zoom)));
278
+ // Radix のメニューは開くと先頭項目にフォーカスを移すので、
279
+ // その後(2フレーム後)に数値入力へ移し替える。開いてすぐ数字を打てる状態にする。
280
+ requestAnimationFrame(() => {
281
+ requestAnimationFrame(() => {
282
+ zoomInputRef.current?.focus();
283
+ zoomInputRef.current?.select();
284
+ });
285
+ });
286
+ };
287
+
288
+ return (
289
+ <EditorTopBar
290
+ variant="figma"
291
+ /* 一覧・PPT風UIはスライドのデッキが前提。Webページでは出さない */
292
+ showBackToList={editorMode !== "webpage"}
293
+ title={contextTitle}
294
+ pageNumber={contextNumber}
295
+ saveStatus={saveStatus}
296
+ onSave={onSave}
297
+ onSaveSettings={onSaveSettings}
298
+ onClose={onClose}
299
+ isCanvasEditing={isCanvasEditing}
300
+ onSwitchUi={editorMode === "webpage" ? undefined : onSwitchUi}
301
+ /* Figma風の固有機能: ズームコントロール */
302
+ rightExtra={
303
+ <div className="flex items-center gap-1">
304
+ {comments && can("commentAction") && (
305
+ <CommentToggleButton open={comments.open} page={comments.page} onToggle={comments.toggle} />
306
+ )}
307
+ {/* [移植時の修正] 押しても何も起きないボタンを作らないため、
308
+ 可動域の端では disabled にして「これ以上は無い」ことを見た目で示す。
309
+ title にショートカットを併記して、キー操作の存在も分かるようにした。 */}
310
+ <Button
311
+ variant="ghost"
312
+ size="icon"
313
+ data-zoom-out
314
+ onClick={handleZoomOut}
315
+ disabled={!canZoomOut}
316
+ title={canZoomOut ? "縮小" : `これ以上縮小できません(下限 ${MIN_ZOOM}%)`}
317
+ className="h-7 w-7 text-gray-400 hover:text-white hover:bg-[#444444] disabled:opacity-30"
318
+ >
319
+ <ZoomOut className="h-4 w-4" />
320
+ </Button>
321
+ {/* 倍率表示(押せる) */}
322
+ <DropdownMenu open={zoomMenuOpen} onOpenChange={openZoomMenu}>
323
+ <DropdownMenuTrigger asChild>
324
+ <button
325
+ data-zoom-menu
326
+ title="表示倍率(クリックでメニュー・⌘0 全体表示 / ⌘1 100% / ⌘2 選択範囲)"
327
+ className="flex h-7 min-w-[3.5rem] items-center justify-center gap-0.5 rounded-md text-xs tabular-nums text-gray-300 transition-colors hover:bg-[#444444] hover:text-white"
328
+ >
329
+ {Math.round(zoom)}%
330
+ <ChevronDown className="h-3 w-3 opacity-60" />
331
+ </button>
332
+ </DropdownMenuTrigger>
333
+ <DropdownMenuContent align="center" className="w-48 bg-[#2c2c2c] border-[#444444]">
334
+ {/* 数値入力。メニューのキー操作(先頭文字ジャンプ等)に数字を食われないよう
335
+ この階層で keydown を止める */}
336
+ <div
337
+ className="flex items-center gap-1 px-2 py-1.5"
338
+ onKeyDown={(e) => e.stopPropagation()}
339
+ >
340
+ <input
341
+ ref={zoomInputRef}
342
+ data-zoom-input
343
+ type="number"
344
+ min={MIN_ZOOM}
345
+ max={MAX_ZOOM}
346
+ value={zoomInput}
347
+ onChange={(e) => setZoomInput(e.target.value)}
348
+ onKeyDown={(e) => {
349
+ if (e.key === "Enter") {
350
+ e.preventDefault();
351
+ applyZoomInput();
352
+ }
353
+ if (e.key === "Escape") setZoomMenuOpen(false);
354
+ }}
355
+ className="h-7 w-full rounded border border-[#4a4a4a] bg-[#1e1e1e] px-2 text-xs tabular-nums text-white outline-none focus:border-[#0d99ff]"
356
+ aria-label={`表示倍率(%) ${MIN_ZOOM}〜${MAX_ZOOM}`}
357
+ />
358
+ <span className="text-xs text-gray-500">%</span>
359
+ <Button
360
+ variant="ghost"
361
+ size="sm"
362
+ data-zoom-apply
363
+ onClick={applyZoomInput}
364
+ className="h-7 px-2 text-xs text-gray-300 hover:bg-[#444444] hover:text-white"
365
+ >
366
+ 適用
367
+ </Button>
368
+ </div>
369
+ <DropdownMenuSeparator className="bg-[#444444]" />
370
+ <DropdownMenuItem
371
+ data-zoom-preset="fit"
372
+ onClick={handleFitZoom}
373
+ className="cursor-pointer justify-between text-gray-300 hover:bg-[#444444] hover:text-white"
374
+ >
375
+ 全体表示
376
+ <span className="text-[10px] text-gray-500">⌘0</span>
377
+ </DropdownMenuItem>
378
+ <DropdownMenuItem
379
+ data-zoom-preset="100"
380
+ onClick={handleActualZoom}
381
+ className="cursor-pointer justify-between text-gray-300 hover:bg-[#444444] hover:text-white"
382
+ >
383
+ 100%
384
+ <span className="text-[10px] text-gray-500">⌘1</span>
385
+ </DropdownMenuItem>
386
+ <DropdownMenuItem
387
+ data-zoom-preset="50"
388
+ onClick={() => setZoom(50)}
389
+ className="cursor-pointer text-gray-300 hover:bg-[#444444] hover:text-white"
390
+ >
391
+ 50%
392
+ </DropdownMenuItem>
393
+ <DropdownMenuItem
394
+ data-zoom-preset="200"
395
+ onClick={() => setZoom(200)}
396
+ className="cursor-pointer text-gray-300 hover:bg-[#444444] hover:text-white"
397
+ >
398
+ 200%
399
+ </DropdownMenuItem>
400
+ </DropdownMenuContent>
401
+ </DropdownMenu>
402
+ <Button
403
+ variant="ghost"
404
+ size="icon"
405
+ data-zoom-in
406
+ onClick={handleZoomIn}
407
+ disabled={!canZoomIn}
408
+ title={canZoomIn ? "拡大" : `これ以上拡大できません(上限 ${MAX_ZOOM}%)`}
409
+ className="h-7 w-7 text-gray-400 hover:text-white hover:bg-[#444444] disabled:opacity-30"
410
+ >
411
+ <ZoomIn className="h-4 w-4" />
412
+ </Button>
413
+ <Button
414
+ variant="ghost"
415
+ size="icon"
416
+ data-zoom-fit
417
+ onClick={handleFitZoom}
418
+ className="h-7 w-7 text-gray-400 hover:text-white hover:bg-[#444444]"
419
+ title="全体を表示(⌘0)"
420
+ >
421
+ <Maximize className="h-4 w-4" />
422
+ </Button>
423
+ <div className="mx-1 h-4 w-px bg-[#444444]" />
424
+ </div>
425
+ }
426
+ /*
427
+ [移植時の修正] リセット/インポート/CSS/JS/設定/エクスポートが常時並んでいて
428
+ ヘッダーが横に伸び、編集中スライド名の居場所が無くなっていた。
429
+ 主要導線(保存・共有・閉じる)は共通トップバーが持ち、残りは「…」メニューへ畳む。
430
+ */
431
+ menuExtra={
432
+ <DropdownMenu>
433
+ <DropdownMenuTrigger asChild>
434
+ <Button
435
+ variant="ghost"
436
+ size="icon"
437
+ data-header-more
438
+ className="h-7 w-7 text-gray-400 hover:bg-[#444444] hover:text-white"
439
+ title="その他の操作"
440
+ >
441
+ <MoreHorizontal className="h-4 w-4" />
442
+ </Button>
443
+ </DropdownMenuTrigger>
444
+ <DropdownMenuContent align="end" className="w-52 bg-[#2c2c2c] border-[#444444]">
445
+ <DropdownMenuItem onClick={handleReset} className="cursor-pointer gap-2 text-gray-300 hover:bg-[#444444] hover:text-white">
446
+ <RotateCcw className="h-4 w-4" />
447
+ 変更をリセット
448
+ </DropdownMenuItem>
449
+ {onImport && (
450
+ <DropdownMenuItem onClick={onImport} className="cursor-pointer gap-2 text-gray-300 hover:bg-[#444444] hover:text-white">
451
+ <Import className="h-4 w-4" />
452
+ インポート
453
+ </DropdownMenuItem>
454
+ )}
455
+ {(onCssEdit || onJsEdit || onPageSettings) && <DropdownMenuSeparator className="bg-[#444444]" />}
456
+ {onCssEdit && (
457
+ <DropdownMenuItem onClick={onCssEdit} className={`cursor-pointer gap-2 hover:bg-[#444444] hover:text-white ${hasCss ? 'text-[#4fb8ff]' : 'text-gray-300'}`}>
458
+ <Code className="h-4 w-4" />
459
+ {hasCss ? 'CSSを編集' : 'CSSを追加'}
460
+ </DropdownMenuItem>
461
+ )}
462
+ {onJsEdit && (
463
+ <DropdownMenuItem onClick={onJsEdit} className={`cursor-pointer gap-2 hover:bg-[#444444] hover:text-white ${hasJs ? 'text-yellow-400' : 'text-gray-300'}`}>
464
+ <Braces className="h-4 w-4" />
465
+ {hasJs ? 'JSを編集' : 'JSを追加'}
466
+ </DropdownMenuItem>
467
+ )}
468
+ {onPageSettings && (
469
+ <DropdownMenuItem onClick={onPageSettings} className={`cursor-pointer gap-2 hover:bg-[#444444] hover:text-white ${hasPageSettings ? 'text-green-400' : 'text-gray-300'}`}>
470
+ <Settings className="h-4 w-4" />
471
+ ページ設定
472
+ </DropdownMenuItem>
473
+ )}
474
+ {onExport && (
475
+ <>
476
+ <DropdownMenuSeparator className="bg-[#444444]" />
477
+ <DropdownMenuItem onClick={() => onExport('html')} className="cursor-pointer gap-2 text-gray-300 hover:bg-[#444444] hover:text-white">
478
+ <FileText className="h-4 w-4" />
479
+ HTMLで書き出し
480
+ </DropdownMenuItem>
481
+ <DropdownMenuItem onClick={() => onExport('zip')} className="cursor-pointer gap-2 text-gray-300 hover:bg-[#444444] hover:text-white">
482
+ <FolderArchive className="h-4 w-4" />
483
+ ZIPで書き出し(画像含む)
484
+ </DropdownMenuItem>
485
+ </>
486
+ )}
487
+ </DropdownMenuContent>
488
+ </DropdownMenu>
489
+ }
490
+ />
491
+ );
492
+ }
493
+
494
+
495
+ /**
496
+ * Figma風トップバーのコメントトグル。実機Figmaの吹き出しアイコンに相当し、
497
+ * 未解決コメント数をバッジで示す(PowerPoint風のコメントピルと同じ情報)。
498
+ */
499
+ function CommentToggleButton({
500
+ open, page, onToggle,
501
+ }: {
502
+ open: boolean;
503
+ page: number;
504
+ onToggle: () => void;
505
+ }) {
506
+ const deck = useDeck();
507
+ const count = unresolvedCount(deck.slides[page - 1]?.comments);
508
+ return (
509
+ <Button
510
+ variant="ghost"
511
+ size="icon"
512
+ onClick={onToggle}
513
+ title="コメントパネルの表示/非表示"
514
+ className={`relative h-8 w-8 hover:bg-[#444444] ${open ? "text-[#4fb8ff]" : "text-gray-400 hover:text-white"}`}
515
+ >
516
+ <MessageSquare className="h-4 w-4" />
517
+ {count > 0 && (
518
+ <span className="absolute -right-0.5 -top-0.5 rounded-full bg-[#0d99ff] px-1 text-[9px] font-bold leading-[14px] text-white">
519
+ {count}
520
+ </span>
521
+ )}
522
+ </Button>
523
+ );
524
+ }