@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,739 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { useEditorContext } from '../EditorContext';
3
+ import { useAuth } from '../../components/auth/AuthProvider';
4
+ import { createAuthApi } from '../../lib/api/auth-fetch';
5
+ import type { PageSettings, ProjectPageSettings } from '../types/page-settings';
6
+ import type { HtmlImportResult } from '../components';
7
+ import { buildDomTree, getArtboardContent } from '../utils/dom-utils';
8
+ import { makeChildrenEditable } from '../utils/html-utils';
9
+
10
+ interface UsePageSettingsManagerProps {
11
+ parentId?: string;
12
+ contentId?: string;
13
+ }
14
+
15
+ interface UsePageSettingsManagerReturn {
16
+ // CSS editor
17
+ isCssEditorOpen: boolean;
18
+ setIsCssEditorOpen: (open: boolean) => void;
19
+ importedCss: string;
20
+ handleOpenCssEditor: () => void;
21
+ handleSaveCss: (css: string) => void;
22
+ handleClearCss: () => void;
23
+ // JS editor
24
+ isJsEditorOpen: boolean;
25
+ setIsJsEditorOpen: (open: boolean) => void;
26
+ importedJs: string;
27
+ handleOpenJsEditor: () => void;
28
+ handleSaveJs: (js: string) => void;
29
+ handleClearJs: () => void;
30
+ // Page settings
31
+ isPageSettingsOpen: boolean;
32
+ setIsPageSettingsOpen: (open: boolean) => void;
33
+ pageSettings: PageSettings;
34
+ projectSettings: ProjectPageSettings | undefined;
35
+ handleOpenPageSettings: () => void;
36
+ handleSavePageSettings: (settings: PageSettings) => Promise<void>;
37
+ handleSaveCurrentSettings: () => Promise<void>;
38
+ // Export
39
+ handleExport: (format?: 'html' | 'zip') => Promise<void>;
40
+ // OGP
41
+ handleUploadOgpImage: (file: File) => Promise<string>;
42
+ // HTML Import
43
+ handleHtmlImport: (result: HtmlImportResult) => void;
44
+ }
45
+
46
+ /**
47
+ * ページ設定管理フック
48
+ * CSS/JS編集、ページ設定、エクスポート、OGP画像アップロード、HTMLインポートを管理
49
+ */
50
+ export function usePageSettingsManager({
51
+ parentId,
52
+ contentId,
53
+ }: UsePageSettingsManagerProps): UsePageSettingsManagerReturn {
54
+ const {
55
+ editorMode,
56
+ getIframeDoc,
57
+ notifyIframeChange,
58
+ iframeReady,
59
+ pushHistory,
60
+ setDomTree,
61
+ setExpandedNodes,
62
+ setSelectedElement,
63
+ setSelectedElementIds,
64
+ } = useEditorContext();
65
+
66
+ const { getIdToken } = useAuth();
67
+
68
+ // CSS編集ダイアログ状態
69
+ const [isCssEditorOpen, setIsCssEditorOpen] = useState(false);
70
+ const [importedCss, setImportedCss] = useState<string>('');
71
+
72
+ // JS編集ダイアログ状態
73
+ const [isJsEditorOpen, setIsJsEditorOpen] = useState(false);
74
+ const [importedJs, setImportedJs] = useState<string>('');
75
+
76
+ // ページ設定ダイアログ状態
77
+ const [isPageSettingsOpen, setIsPageSettingsOpen] = useState(false);
78
+ const [pageSettings, setPageSettings] = useState<PageSettings>({
79
+ inheritProjectSettings: true,
80
+ });
81
+ const [isPageSettingsLoaded, setIsPageSettingsLoaded] = useState(false);
82
+
83
+ // contentId変更時にページ設定をリセット(再取得を促す)
84
+ const prevContentIdRef = useRef(contentId);
85
+ useEffect(() => {
86
+ if (contentId !== prevContentIdRef.current) {
87
+ prevContentIdRef.current = contentId;
88
+ setIsPageSettingsLoaded(false);
89
+ setImportedCss('');
90
+ setImportedJs('');
91
+ setPageSettings({ inheritProjectSettings: true });
92
+ }
93
+ }, [contentId]);
94
+
95
+ // プロジェクトレベル設定(将来的にはFirestoreから取得)
96
+ const [projectSettings] = useState<ProjectPageSettings | undefined>(undefined);
97
+
98
+ // ページ設定をFirestoreから読み込み
99
+ useEffect(() => {
100
+ if (!parentId || !contentId || isPageSettingsLoaded) return;
101
+
102
+ const loadPageSettings = async () => {
103
+ try {
104
+ const api = await createAuthApi(getIdToken);
105
+ const apiPath = editorMode === 'webpage'
106
+ ? `/api/websites/${parentId}/pages/${contentId}`
107
+ : `/api/presentations/${parentId}/slides/${contentId}`;
108
+
109
+ const data = await api.get(apiPath);
110
+ const pageData = editorMode === 'webpage' ? data.page : data.slide;
111
+
112
+ if (pageData?.settings) {
113
+ // CSS/JSがStorageにある場合はフェッチ
114
+ let customCss = pageData.settings.customCss || '';
115
+ let customJs = pageData.settings.customJs || '';
116
+
117
+ // CSSがStorageにある場合
118
+ if (pageData.settings.customCssUrl) {
119
+ console.log('[loadPageSettings] Fetching CSS from Storage:', pageData.settings.customCssUrl);
120
+ try {
121
+ const response = await fetch(pageData.settings.customCssUrl);
122
+ if (response.ok) {
123
+ customCss = await response.text();
124
+ console.log(`[loadPageSettings] CSS fetched: ${customCss.length} chars`);
125
+ }
126
+ } catch (e) {
127
+ console.error('[loadPageSettings] Failed to fetch CSS from Storage:', e);
128
+ }
129
+ }
130
+
131
+ // JSがStorageにある場合
132
+ if (pageData.settings.customJsUrl) {
133
+ console.log('[loadPageSettings] Fetching JS from Storage:', pageData.settings.customJsUrl);
134
+ try {
135
+ const response = await fetch(pageData.settings.customJsUrl);
136
+ if (response.ok) {
137
+ customJs = await response.text();
138
+ console.log(`[loadPageSettings] JS fetched: ${customJs.length} chars`);
139
+ }
140
+ } catch (e) {
141
+ console.error('[loadPageSettings] Failed to fetch JS from Storage:', e);
142
+ }
143
+ }
144
+
145
+ // Firestore型をエディタ型に変換
146
+ const loadedSettings: PageSettings = {
147
+ inheritProjectSettings: pageData.settings.inheritProjectSettings ?? true,
148
+ title: pageData.settings.metaTitle,
149
+ description: pageData.settings.metaDescription,
150
+ keywords: pageData.settings.metaKeywords,
151
+ canonicalUrl: pageData.settings.canonicalUrl,
152
+ robots: pageData.settings.robots,
153
+ ogp: pageData.settings.ogp,
154
+ twitter: pageData.settings.twitter,
155
+ customHeadHtml: pageData.settings.customHeadHtml,
156
+ bodyStartHtml: pageData.settings.bodyStartHtml,
157
+ bodyEndHtml: pageData.settings.bodyEndHtml,
158
+ externalCss: pageData.settings.externalCss,
159
+ externalJs: pageData.settings.externalJs,
160
+ customCss: customCss,
161
+ customJs: customJs,
162
+ // Storage参照情報も保持
163
+ customCssUrl: pageData.settings.customCssUrl,
164
+ customCssPath: pageData.settings.customCssPath,
165
+ customCssSize: pageData.settings.customCssSize,
166
+ customJsUrl: pageData.settings.customJsUrl,
167
+ customJsPath: pageData.settings.customJsPath,
168
+ customJsSize: pageData.settings.customJsSize,
169
+ };
170
+ setPageSettings(loadedSettings);
171
+
172
+ // CSS/JSがある場合はiframeにも反映
173
+ if (customCss) {
174
+ setImportedCss(customCss);
175
+ }
176
+ if (customJs) {
177
+ setImportedJs(customJs);
178
+ }
179
+
180
+ console.log('[loadPageSettings] Loaded settings:', {
181
+ ...loadedSettings,
182
+ customCss: customCss.length + ' chars',
183
+ customJs: customJs.length + ' chars',
184
+ });
185
+ }
186
+ setIsPageSettingsLoaded(true);
187
+ } catch (error) {
188
+ console.error('[loadPageSettings] Failed to load settings:', error);
189
+ setIsPageSettingsLoaded(true);
190
+ }
191
+ };
192
+
193
+ loadPageSettings();
194
+ }, [parentId, contentId, editorMode, getIdToken, isPageSettingsLoaded]);
195
+
196
+ // ページ設定読み込み後、CSS/JSをiframeに反映
197
+ useEffect(() => {
198
+ if (!isPageSettingsLoaded) return;
199
+
200
+ let retryCount = 0;
201
+ const maxRetries = 5;
202
+ const applyStylesToIframe = () => {
203
+ const iframeDoc = getIframeDoc();
204
+ if (!iframeDoc) {
205
+ retryCount++;
206
+ if (retryCount <= maxRetries) {
207
+ setTimeout(applyStylesToIframe, 100);
208
+ }
209
+ return;
210
+ }
211
+
212
+ // CSSをiframeに反映
213
+ if (importedCss) {
214
+ let styleElement = iframeDoc.getElementById('imported-styles');
215
+ if (!styleElement) {
216
+ styleElement = iframeDoc.createElement('style');
217
+ styleElement.id = 'imported-styles';
218
+ iframeDoc.head.appendChild(styleElement);
219
+ }
220
+ if (styleElement.textContent !== importedCss) {
221
+ styleElement.textContent = importedCss;
222
+ console.log('[applyStylesToIframe] Applied CSS:', importedCss.length, 'chars');
223
+ }
224
+ }
225
+
226
+ // JSをiframeに反映
227
+ if (importedJs) {
228
+ const existingScript = iframeDoc.getElementById('imported-scripts');
229
+ // 先頭にセミコロンを追加してIIFE連結問題を防ぐ
230
+ // また、try-catchでラップしてエラーがページを壊さないようにする
231
+ const safeJs = `;try{${importedJs}}catch(e){console.error('[imported-scripts] Error:',e);}`;
232
+ // 既にスクリプトが存在し、同じ内容であればスキップ
233
+ if (existingScript?.textContent === safeJs) {
234
+ console.log('[applyStylesToIframe] JS already applied, skipping');
235
+ return;
236
+ }
237
+ // 既存のスクリプトを削除
238
+ if (existingScript) {
239
+ existingScript.remove();
240
+ }
241
+ // 新しいスクリプトを追加
242
+ const scriptElement = iframeDoc.createElement('script');
243
+ scriptElement.id = 'imported-scripts';
244
+ scriptElement.textContent = safeJs;
245
+ iframeDoc.body.appendChild(scriptElement);
246
+ console.log('[applyStylesToIframe] Applied JS:', importedJs.length, 'chars');
247
+ }
248
+ };
249
+
250
+ applyStylesToIframe();
251
+ }, [isPageSettingsLoaded, importedCss, importedJs, getIframeDoc]);
252
+
253
+ // CSS編集ダイアログを開く(現在のCSSを取得)
254
+ const handleOpenCssEditor = useCallback(() => {
255
+ const iframeDoc = getIframeDoc();
256
+ if (iframeDoc) {
257
+ const styleElement = iframeDoc.getElementById('imported-styles');
258
+ const currentCss = styleElement?.textContent || importedCss;
259
+ setImportedCss(currentCss);
260
+ }
261
+ setIsCssEditorOpen(true);
262
+ }, [getIframeDoc, importedCss]);
263
+
264
+ // CSSを保存(iframeに反映)
265
+ const handleSaveCss = useCallback((css: string) => {
266
+ const iframeDoc = getIframeDoc();
267
+ if (!iframeDoc) return;
268
+
269
+ // 既存のスタイル要素を取得または作成
270
+ let styleElement = iframeDoc.getElementById('imported-styles');
271
+ if (css.trim()) {
272
+ if (!styleElement) {
273
+ styleElement = iframeDoc.createElement('style');
274
+ styleElement.id = 'imported-styles';
275
+ iframeDoc.head.appendChild(styleElement);
276
+ }
277
+ styleElement.textContent = css;
278
+ console.log('[handleSaveCss] Updated CSS:', css.length, 'chars');
279
+ } else if (styleElement) {
280
+ // CSSが空の場合はスタイル要素を削除
281
+ styleElement.remove();
282
+ console.log('[handleSaveCss] Removed empty CSS');
283
+ }
284
+
285
+ // 状態を更新
286
+ setImportedCss(css);
287
+ // ページ設定にも保存(Firestore保存用)
288
+ setPageSettings(prev => ({ ...prev, customCss: css }));
289
+
290
+ // 変更を通知
291
+ notifyIframeChange();
292
+ }, [getIframeDoc, notifyIframeChange]);
293
+
294
+ // CSSをクリア
295
+ const handleClearCss = useCallback(() => {
296
+ const iframeDoc = getIframeDoc();
297
+ if (iframeDoc) {
298
+ const styleElement = iframeDoc.getElementById('imported-styles');
299
+ if (styleElement) {
300
+ styleElement.remove();
301
+ console.log('[handleClearCss] Cleared imported CSS');
302
+ }
303
+ }
304
+ setImportedCss('');
305
+ notifyIframeChange();
306
+ }, [getIframeDoc, notifyIframeChange]);
307
+
308
+ // JS編集ダイアログを開く(現在のJSを取得)
309
+ const handleOpenJsEditor = useCallback(() => {
310
+ const iframeDoc = getIframeDoc();
311
+ if (iframeDoc) {
312
+ const scriptElement = iframeDoc.getElementById('imported-scripts');
313
+ const currentJs = scriptElement?.textContent || importedJs;
314
+ setImportedJs(currentJs);
315
+ }
316
+ setIsJsEditorOpen(true);
317
+ }, [getIframeDoc, importedJs]);
318
+
319
+ // JSを保存(iframeに反映)
320
+ const handleSaveJs = useCallback((js: string) => {
321
+ const iframeDoc = getIframeDoc();
322
+ if (!iframeDoc) return;
323
+
324
+ // 既存のスクリプト要素を取得または作成
325
+ let scriptElement = iframeDoc.getElementById('imported-scripts');
326
+ if (js.trim()) {
327
+ if (!scriptElement) {
328
+ scriptElement = iframeDoc.createElement('script');
329
+ scriptElement.id = 'imported-scripts';
330
+ iframeDoc.body.appendChild(scriptElement);
331
+ }
332
+ // 古いスクリプトを削除して新しいものを追加(JSを再実行するため)
333
+ scriptElement.remove();
334
+ const newScriptElement = iframeDoc.createElement('script');
335
+ newScriptElement.id = 'imported-scripts';
336
+ // 先頭にセミコロンを追加してIIFE連結問題を防ぐ
337
+ // また、try-catchでラップしてエラーがページを壊さないようにする
338
+ const safeJs = `;try{${js}}catch(e){console.error('[imported-scripts] Error:',e);}`;
339
+ newScriptElement.textContent = safeJs;
340
+ iframeDoc.body.appendChild(newScriptElement);
341
+ console.log('[handleSaveJs] Updated JS:', js.length, 'chars');
342
+ } else if (scriptElement) {
343
+ // JSが空の場合はスクリプト要素を削除
344
+ scriptElement.remove();
345
+ console.log('[handleSaveJs] Removed empty JS');
346
+ }
347
+
348
+ // 状態を更新
349
+ setImportedJs(js);
350
+ // ページ設定にも保存(Firestore保存用)
351
+ setPageSettings(prev => ({ ...prev, customJs: js }));
352
+
353
+ // 変更を通知
354
+ notifyIframeChange();
355
+ }, [getIframeDoc, notifyIframeChange]);
356
+
357
+ // JSをクリア
358
+ const handleClearJs = useCallback(() => {
359
+ const iframeDoc = getIframeDoc();
360
+ if (iframeDoc) {
361
+ const scriptElement = iframeDoc.getElementById('imported-scripts');
362
+ if (scriptElement) {
363
+ scriptElement.remove();
364
+ console.log('[handleClearJs] Cleared imported JS');
365
+ }
366
+ }
367
+ setImportedJs('');
368
+ notifyIframeChange();
369
+ }, [getIframeDoc, notifyIframeChange]);
370
+
371
+ // ページ設定ダイアログを開く
372
+ const handleOpenPageSettings = useCallback(() => {
373
+ // iframeから現在のCSS/JSを取得して設定に反映
374
+ const iframeDoc = getIframeDoc();
375
+ if (iframeDoc) {
376
+ const styleElement = iframeDoc.getElementById('imported-styles');
377
+ const scriptElement = iframeDoc.getElementById('imported-scripts');
378
+ setPageSettings(prev => ({
379
+ ...prev,
380
+ customCss: styleElement?.textContent || importedCss || prev.customCss,
381
+ customJs: scriptElement?.textContent || importedJs || prev.customJs,
382
+ }));
383
+ }
384
+ setIsPageSettingsOpen(true);
385
+ }, [getIframeDoc, importedCss, importedJs]);
386
+
387
+ // ページ設定を保存
388
+ const handleSavePageSettings = useCallback(async (settings: PageSettings) => {
389
+ setPageSettings(settings);
390
+
391
+ const iframeDoc = getIframeDoc();
392
+ if (iframeDoc) {
393
+ // CSSをiframeに反映
394
+ if (settings.customCss !== undefined) {
395
+ let styleElement = iframeDoc.getElementById('imported-styles');
396
+ if (settings.customCss.trim()) {
397
+ if (!styleElement) {
398
+ styleElement = iframeDoc.createElement('style');
399
+ styleElement.id = 'imported-styles';
400
+ iframeDoc.head.appendChild(styleElement);
401
+ }
402
+ styleElement.textContent = settings.customCss;
403
+ } else if (styleElement) {
404
+ styleElement.remove();
405
+ }
406
+ setImportedCss(settings.customCss);
407
+ }
408
+
409
+ // JSをiframeに反映
410
+ if (settings.customJs !== undefined) {
411
+ let scriptElement = iframeDoc.getElementById('imported-scripts');
412
+ if (settings.customJs.trim()) {
413
+ // 古いスクリプトを削除して新しいものを追加(JSを再実行するため)
414
+ if (scriptElement) {
415
+ scriptElement.remove();
416
+ }
417
+ const newScriptElement = iframeDoc.createElement('script');
418
+ newScriptElement.id = 'imported-scripts';
419
+ // 先頭にセミコロンを追加してIIFE連結問題を防ぐ
420
+ // また、try-catchでラップしてエラーがページを壊さないようにする
421
+ const safeJs = `;try{${settings.customJs}}catch(e){console.error('[imported-scripts] Error:',e);}`;
422
+ newScriptElement.textContent = safeJs;
423
+ iframeDoc.body.appendChild(newScriptElement);
424
+ } else if (scriptElement) {
425
+ scriptElement.remove();
426
+ }
427
+ setImportedJs(settings.customJs);
428
+ }
429
+
430
+ notifyIframeChange();
431
+ }
432
+
433
+ // Firestoreに保存
434
+ if (parentId && contentId) {
435
+ try {
436
+ const api = await createAuthApi(getIdToken);
437
+ const apiPath = editorMode === 'webpage'
438
+ ? `/api/websites/${parentId}/pages/${contentId}`
439
+ : `/api/presentations/${parentId}/slides/${contentId}`;
440
+
441
+ // エディタ型をFirestore型に変換(undefinedを除外)
442
+ const firestoreSettings: Record<string, unknown> = {};
443
+
444
+ // 値がundefinedでない場合のみ設定
445
+ if (settings.inheritProjectSettings !== undefined) {
446
+ firestoreSettings.inheritProjectSettings = settings.inheritProjectSettings;
447
+ }
448
+ if (settings.title) firestoreSettings.metaTitle = settings.title;
449
+ if (settings.description) firestoreSettings.metaDescription = settings.description;
450
+ if (settings.keywords) firestoreSettings.metaKeywords = settings.keywords;
451
+ if (settings.canonicalUrl) firestoreSettings.canonicalUrl = settings.canonicalUrl;
452
+ if (settings.robots) firestoreSettings.robots = settings.robots;
453
+ if (settings.ogp) firestoreSettings.ogp = settings.ogp;
454
+ if (settings.twitter) firestoreSettings.twitter = settings.twitter;
455
+ if (settings.customHeadHtml) firestoreSettings.customHeadHtml = settings.customHeadHtml;
456
+ if (settings.bodyStartHtml) firestoreSettings.bodyStartHtml = settings.bodyStartHtml;
457
+ if (settings.bodyEndHtml) firestoreSettings.bodyEndHtml = settings.bodyEndHtml;
458
+ if (settings.externalCss?.length) firestoreSettings.externalCss = settings.externalCss;
459
+ if (settings.externalJs?.length) firestoreSettings.externalJs = settings.externalJs;
460
+ if (settings.customCss) firestoreSettings.customCss = settings.customCss;
461
+ if (settings.customJs) firestoreSettings.customJs = settings.customJs;
462
+
463
+ await api.patch(apiPath, { settings: firestoreSettings });
464
+ console.log('[handleSavePageSettings] Saved settings to Firestore:', firestoreSettings);
465
+ } catch (error) {
466
+ console.error('[handleSavePageSettings] Failed to save settings to Firestore:', error);
467
+ }
468
+ }
469
+
470
+ console.log('[handleSavePageSettings] Saved settings:', settings);
471
+ }, [getIframeDoc, notifyIframeChange, parentId, contentId, editorMode, getIdToken]);
472
+
473
+ // 現在のページ設定を保存(メイン保存ボタンから呼ばれる)
474
+ const handleSaveCurrentSettings = useCallback(async () => {
475
+ if (!parentId || !contentId) return;
476
+
477
+ // iframeから最新のCSS/JSを取得
478
+ const iframeDoc = getIframeDoc();
479
+ let currentCss = pageSettings.customCss || '';
480
+ let currentJs = pageSettings.customJs || '';
481
+
482
+ if (iframeDoc) {
483
+ const styleElement = iframeDoc.getElementById('imported-styles');
484
+ if (styleElement?.textContent) {
485
+ currentCss = styleElement.textContent;
486
+ }
487
+ const scriptElement = iframeDoc.getElementById('imported-scripts');
488
+ if (scriptElement?.textContent) {
489
+ currentJs = scriptElement.textContent;
490
+ }
491
+ }
492
+
493
+ // CSS/JSが空の場合は保存不要
494
+ if (!currentCss && !currentJs && !pageSettings.title && !pageSettings.description) {
495
+ console.log('[handleSaveCurrentSettings] No settings to save');
496
+ return;
497
+ }
498
+
499
+ try {
500
+ const api = await createAuthApi(getIdToken);
501
+ const apiPath = editorMode === 'webpage'
502
+ ? `/api/websites/${parentId}/pages/${contentId}`
503
+ : `/api/presentations/${parentId}/slides/${contentId}`;
504
+
505
+ // エディタ型をFirestore型に変換(undefinedを除外)
506
+ const firestoreSettings: Record<string, unknown> = {};
507
+
508
+ // 値がundefinedでない場合のみ設定
509
+ if (pageSettings.inheritProjectSettings !== undefined) {
510
+ firestoreSettings.inheritProjectSettings = pageSettings.inheritProjectSettings;
511
+ }
512
+ if (pageSettings.title) firestoreSettings.metaTitle = pageSettings.title;
513
+ if (pageSettings.description) firestoreSettings.metaDescription = pageSettings.description;
514
+ if (pageSettings.keywords) firestoreSettings.metaKeywords = pageSettings.keywords;
515
+ if (pageSettings.canonicalUrl) firestoreSettings.canonicalUrl = pageSettings.canonicalUrl;
516
+ if (pageSettings.robots) firestoreSettings.robots = pageSettings.robots;
517
+ if (pageSettings.ogp) firestoreSettings.ogp = pageSettings.ogp;
518
+ if (pageSettings.twitter) firestoreSettings.twitter = pageSettings.twitter;
519
+ if (pageSettings.customHeadHtml) firestoreSettings.customHeadHtml = pageSettings.customHeadHtml;
520
+ if (pageSettings.bodyStartHtml) firestoreSettings.bodyStartHtml = pageSettings.bodyStartHtml;
521
+ if (pageSettings.bodyEndHtml) firestoreSettings.bodyEndHtml = pageSettings.bodyEndHtml;
522
+ if (pageSettings.externalCss?.length) firestoreSettings.externalCss = pageSettings.externalCss;
523
+ if (pageSettings.externalJs?.length) firestoreSettings.externalJs = pageSettings.externalJs;
524
+ if (currentCss) firestoreSettings.customCss = currentCss;
525
+ if (currentJs) firestoreSettings.customJs = currentJs;
526
+
527
+ await api.patch(apiPath, { settings: firestoreSettings });
528
+ console.log('[handleSaveCurrentSettings] Saved settings to Firestore:', {
529
+ customCss: currentCss.length + ' chars',
530
+ customJs: currentJs.length + ' chars',
531
+ });
532
+ } catch (error) {
533
+ console.error('[handleSaveCurrentSettings] Failed to save settings:', error);
534
+ }
535
+ }, [parentId, contentId, editorMode, getIdToken, pageSettings, getIframeDoc]);
536
+
537
+ // HTMLエクスポート処理
538
+ const handleExport = useCallback(async (format: 'html' | 'zip' = 'html') => {
539
+ if (!parentId || !contentId) {
540
+ console.error('[handleExport] Missing parentId or contentId');
541
+ return;
542
+ }
543
+
544
+ try {
545
+ // フォーマットに応じてAPIパラメータを設定
546
+ const formatParam = format === 'zip' ? '&format=zip' : '';
547
+ const apiPath = editorMode === 'webpage'
548
+ ? `/api/websites/${parentId}/pages/${contentId}/export?download=true${formatParam}`
549
+ : `/api/presentations/${parentId}/slides/${contentId}/export?download=true${formatParam}`;
550
+
551
+ const response = await fetch(apiPath, {
552
+ method: 'GET',
553
+ headers: {
554
+ 'Authorization': `Bearer ${await getIdToken()}`,
555
+ },
556
+ });
557
+
558
+ if (!response.ok) {
559
+ throw new Error('Export failed');
560
+ }
561
+
562
+ // Content-Disposition からファイル名を取得
563
+ const contentDisposition = response.headers.get('Content-Disposition');
564
+ const defaultFilename = format === 'zip' ? 'page.zip' : 'page.html';
565
+ let filename = defaultFilename;
566
+ if (contentDisposition) {
567
+ const match = contentDisposition.match(/filename="?([^"]+)"?/);
568
+ if (match) {
569
+ filename = decodeURIComponent(match[1]);
570
+ }
571
+ }
572
+
573
+ // Blobとしてダウンロード
574
+ const blob = await response.blob();
575
+ const url = URL.createObjectURL(blob);
576
+ const a = document.createElement('a');
577
+ a.href = url;
578
+ a.download = filename;
579
+ document.body.appendChild(a);
580
+ a.click();
581
+ document.body.removeChild(a);
582
+ URL.revokeObjectURL(url);
583
+
584
+ console.log('[handleExport] Downloaded:', filename, 'format:', format);
585
+ } catch (error) {
586
+ console.error('[handleExport] Export failed:', error);
587
+ }
588
+ }, [parentId, contentId, editorMode, getIdToken]);
589
+
590
+ // OGP画像アップロード
591
+ const handleUploadOgpImage = useCallback(async (file: File): Promise<string> => {
592
+ // 既存の画像アップロード機能を利用
593
+ // Firebase Storageにアップロードして公開URLを返す
594
+ const formData = new FormData();
595
+ formData.append('file', file);
596
+ formData.append('type', 'ogp');
597
+ if (parentId) {
598
+ formData.append('parentId', parentId);
599
+ }
600
+
601
+ try {
602
+ const response = await fetch('/api/upload/image', {
603
+ method: 'POST',
604
+ body: formData,
605
+ headers: {
606
+ 'Authorization': `Bearer ${await getIdToken()}`,
607
+ },
608
+ });
609
+
610
+ if (!response.ok) {
611
+ throw new Error('Upload failed');
612
+ }
613
+
614
+ const data = await response.json();
615
+ return data.url;
616
+ } catch (error) {
617
+ console.error('[handleUploadOgpImage] Upload failed:', error);
618
+ throw error;
619
+ }
620
+ }, [parentId, getIdToken]);
621
+
622
+ // HTMLインポート処理(html + css + resources対応)
623
+ const handleHtmlImport = useCallback((result: HtmlImportResult) => {
624
+ const iframeDoc = getIframeDoc();
625
+ if (!iframeDoc) return;
626
+
627
+ // アートボードコンテンツを取得して置換
628
+ const artboard = iframeDoc.getElementById('artboard') || iframeDoc.getElementById('slide-artboard') || iframeDoc.body;
629
+ if (artboard) {
630
+ // インポートされたHTMLで内容を置換
631
+ artboard.innerHTML = result.html;
632
+
633
+ // CSSをiframeの<head>に注入
634
+ if (result.css) {
635
+ // 既存のインポート済みスタイルがあれば削除
636
+ const existingStyle = iframeDoc.getElementById('imported-styles');
637
+ if (existingStyle) {
638
+ existingStyle.remove();
639
+ }
640
+
641
+ // 新しいスタイル要素を作成して追加
642
+ const styleElement = iframeDoc.createElement('style');
643
+ styleElement.id = 'imported-styles';
644
+ styleElement.textContent = result.css;
645
+ iframeDoc.head.appendChild(styleElement);
646
+ console.log('[handleHtmlImport] Injected CSS:', result.css.length, 'chars');
647
+
648
+ // CSS状態を保存(編集用)
649
+ setImportedCss(result.css);
650
+ // ページ設定にも保存(Firestore保存用)
651
+ setPageSettings(prev => ({ ...prev, customCss: result.css }));
652
+ }
653
+
654
+ // JSをiframeの<body>末尾に注入
655
+ if (result.js) {
656
+ // 既存のインポート済みスクリプトがあれば削除
657
+ const existingScript = iframeDoc.getElementById('imported-scripts');
658
+ if (existingScript) {
659
+ existingScript.remove();
660
+ }
661
+
662
+ // 新しいスクリプト要素を作成して追加
663
+ const scriptElement = iframeDoc.createElement('script');
664
+ scriptElement.id = 'imported-scripts';
665
+ // 先頭にセミコロンを追加してIIFE連結問題を防ぐ
666
+ // また、try-catchでラップしてエラーがページを壊さないようにする
667
+ const safeJs = `;try{${result.js}}catch(e){console.error('[imported-scripts] Error:',e);}`;
668
+ scriptElement.textContent = safeJs;
669
+ iframeDoc.body.appendChild(scriptElement);
670
+ console.log('[handleHtmlImport] Injected JS:', result.js.length, 'chars');
671
+
672
+ // JS状態を保存(編集用)- 元のJSを保存
673
+ setImportedJs(result.js);
674
+ // ページ設定にも保存(Firestore保存用)
675
+ setPageSettings(prev => ({ ...prev, customJs: result.js }));
676
+ }
677
+
678
+ // インポートされた要素に data-editable と data-element-id を付与
679
+ const count = makeChildrenEditable(artboard);
680
+ console.log('[handleHtmlImport] Made', count, 'elements editable');
681
+
682
+ // リソース情報をログ
683
+ if (result.resources) {
684
+ console.log('[handleHtmlImport] Resources:', {
685
+ images: result.resources.images?.length || 0,
686
+ stylesheets: result.resources.stylesheets?.length || 0,
687
+ scripts: result.resources.scripts?.length || 0,
688
+ });
689
+ }
690
+
691
+ // DOMツリーを再構築
692
+ const tree = buildDomTree(iframeDoc);
693
+ setDomTree(tree);
694
+ setExpandedNodes(new Set(tree.map((n: { id: string }) => n.id)));
695
+
696
+ // 履歴に追加
697
+ const newHtml = getArtboardContent(iframeDoc);
698
+ pushHistory(newHtml);
699
+
700
+ // 変更通知
701
+ notifyIframeChange();
702
+
703
+ // 選択をクリア
704
+ setSelectedElement(null);
705
+ setSelectedElementIds([]);
706
+ }
707
+ }, [getIframeDoc, pushHistory, setDomTree, setExpandedNodes, notifyIframeChange, setSelectedElement, setSelectedElementIds]);
708
+
709
+ return {
710
+ // CSS editor
711
+ isCssEditorOpen,
712
+ setIsCssEditorOpen,
713
+ importedCss,
714
+ handleOpenCssEditor,
715
+ handleSaveCss,
716
+ handleClearCss,
717
+ // JS editor
718
+ isJsEditorOpen,
719
+ setIsJsEditorOpen,
720
+ importedJs,
721
+ handleOpenJsEditor,
722
+ handleSaveJs,
723
+ handleClearJs,
724
+ // Page settings
725
+ isPageSettingsOpen,
726
+ setIsPageSettingsOpen,
727
+ pageSettings,
728
+ projectSettings,
729
+ handleOpenPageSettings,
730
+ handleSavePageSettings,
731
+ handleSaveCurrentSettings,
732
+ // Export
733
+ handleExport,
734
+ // OGP
735
+ handleUploadOgpImage,
736
+ // HTML Import
737
+ handleHtmlImport,
738
+ };
739
+ }