@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,249 @@
1
+ /**
2
+ * フォーカス管理フック
3
+ *
4
+ * ショートカットが効かなくなる問題を解決するため、
5
+ * エディタ全体のフォーカス状態を管理する
6
+ */
7
+
8
+ import { useCallback, useRef, useEffect } from 'react';
9
+ import { useEditorContext } from '../EditorContext';
10
+
11
+ interface UseFocusManagementReturn {
12
+ /**
13
+ * iframeにフォーカスを復元する
14
+ * ダイアログを閉じた後などに呼び出す
15
+ */
16
+ restoreFocus: () => void;
17
+
18
+ /**
19
+ * フォーカスがエディタ内にあるかチェック
20
+ */
21
+ isEditorFocused: () => boolean;
22
+
23
+ /**
24
+ * フォーカス喪失時の自動復元を設定
25
+ * @returns クリーンアップ関数
26
+ */
27
+ setupFocusRecovery: () => () => void;
28
+ }
29
+
30
+ /**
31
+ * フォーカス管理フック
32
+ *
33
+ * エディタのフォーカス状態を管理し、ショートカットが
34
+ * 確実に機能するようにする
35
+ */
36
+ export function useFocusManagement(): UseFocusManagementReturn {
37
+ const { iframeRef, getIframeDoc } = useEditorContext();
38
+
39
+ // フォーカス復元のタイマー(デバウンス用)
40
+ const focusRecoveryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
41
+
42
+ // 最後にフォーカスがあった場所を記録
43
+ const lastFocusedElementRef = useRef<'iframe' | 'parent' | null>(null);
44
+
45
+ /**
46
+ * iframeにフォーカスを復元する
47
+ */
48
+ const restoreFocus = useCallback(() => {
49
+ const iframe = iframeRef.current;
50
+ const iframeDoc = getIframeDoc();
51
+ if (!iframeDoc) {
52
+ console.log('[FocusManagement] Cannot restore focus: iframe doc not available');
53
+ return;
54
+ }
55
+
56
+ // テキスト編集中の要素があればそこにフォーカス
57
+ const editingElement = iframeDoc.querySelector('[contenteditable="true"]') as HTMLElement;
58
+ if (editingElement) {
59
+ // まずiframe要素自体にフォーカス(親ドキュメントの観点から)
60
+ if (iframe) iframe.focus();
61
+ editingElement.focus();
62
+ console.log('[FocusManagement] Restored focus to editing element');
63
+ return;
64
+ }
65
+
66
+ // iframeとbodyにフォーカス
67
+ // 重要: iframe要素自体にもフォーカスを当てないとキーボードイベントが届かない
68
+ if (iframe) {
69
+ iframe.focus();
70
+ }
71
+ if (iframeDoc.body) {
72
+ iframeDoc.body.focus();
73
+ console.log('[FocusManagement] Restored focus to iframe and body');
74
+ }
75
+ }, [iframeRef, getIframeDoc]);
76
+
77
+ /**
78
+ * フォーカスがエディタ内にあるかチェック
79
+ */
80
+ const isEditorFocused = useCallback(() => {
81
+ const iframe = iframeRef.current;
82
+ const iframeDoc = getIframeDoc();
83
+
84
+ // 親ドキュメントのアクティブ要素をチェック
85
+ const activeElement = document.activeElement;
86
+
87
+ // iframeにフォーカスがあるか(iframe内の要素にフォーカスがあるとiframeがactiveElementになる)
88
+ if (activeElement === iframe) {
89
+ return true;
90
+ }
91
+
92
+ // エディタ関連の要素にフォーカスがあるか
93
+ const editorContainer = document.querySelector('[data-frontend-visual-editor="true"]');
94
+ if (editorContainer?.contains(activeElement)) {
95
+ return true;
96
+ }
97
+
98
+ // iframe内にフォーカスがあるか(bodyを含む)
99
+ // 重要: bodyにフォーカスがある場合もキーボードイベントは受け取れるのでtrueを返す
100
+ if (iframeDoc && iframeDoc.activeElement) {
101
+ return true;
102
+ }
103
+
104
+ return false;
105
+ }, [iframeRef, getIframeDoc]);
106
+
107
+ /**
108
+ * フォーカス喪失時の自動復元を設定
109
+ */
110
+ const setupFocusRecovery = useCallback(() => {
111
+ const iframe = iframeRef.current;
112
+ const iframeDoc = getIframeDoc();
113
+
114
+ /**
115
+ * フォーカスアウト時のハンドラ
116
+ * フォーカスがエディタ外に出た場合、一定時間後に復元を試みる
117
+ */
118
+ const handleFocusOut = (e: FocusEvent) => {
119
+ // 次にフォーカスが移動する先を確認
120
+ const relatedTarget = e.relatedTarget as HTMLElement | null;
121
+
122
+ // ダイアログやポップオーバー内への移動は許可
123
+ if (relatedTarget) {
124
+ const isDialogOrPopover =
125
+ relatedTarget.closest('[role="dialog"]') ||
126
+ relatedTarget.closest('[role="menu"]') ||
127
+ relatedTarget.closest('[role="listbox"]') ||
128
+ relatedTarget.closest('[role="combobox"]') ||
129
+ relatedTarget.closest('[role="slider"]') || // Radix Slider(カラーピッカーの透明度等)
130
+ relatedTarget.closest('[data-radix-slider-thumb]') || // Radix Sliderのつまみ
131
+ relatedTarget.closest('[data-radix-slider-track]') || // Radix Sliderのトラック
132
+ relatedTarget.closest('[data-radix-popper-content-wrapper]') ||
133
+ relatedTarget.closest('[data-radix-select-content]') ||
134
+ relatedTarget.closest('[data-radix-dropdown-menu-content]') ||
135
+ relatedTarget.closest('[data-radix-popover-content]') ||
136
+ relatedTarget.closest('.ai-prompt-popover') ||
137
+ relatedTarget.closest('[data-state="open"]') ||
138
+ relatedTarget.closest('[data-color-picker]') ||
139
+ relatedTarget.closest('[data-dropdown-menu]');
140
+
141
+ if (isDialogOrPopover) {
142
+ console.log('[FocusManagement] Focus moved to dialog/popover, allowing');
143
+ return;
144
+ }
145
+
146
+ // 入力フィールドへの移動は許可(ただし後でフォーカス復元のためにトラッキング)
147
+ const isInputField =
148
+ relatedTarget instanceof HTMLInputElement ||
149
+ relatedTarget instanceof HTMLTextAreaElement ||
150
+ relatedTarget instanceof HTMLSelectElement ||
151
+ relatedTarget.getAttribute('contenteditable') === 'true';
152
+
153
+ if (isInputField) {
154
+ console.log('[FocusManagement] Focus moved to input field, allowing');
155
+ return;
156
+ }
157
+
158
+ // エディタ内の他の要素への移動は許可
159
+ const editorContainer = document.querySelector('[data-frontend-visual-editor="true"]');
160
+ if (editorContainer?.contains(relatedTarget)) {
161
+ return;
162
+ }
163
+ }
164
+
165
+ // フォーカスが完全に外に出た場合、遅延して復元
166
+ if (focusRecoveryTimerRef.current) {
167
+ clearTimeout(focusRecoveryTimerRef.current);
168
+ }
169
+
170
+ focusRecoveryTimerRef.current = setTimeout(() => {
171
+ // まだフォーカスがエディタ外にあれば復元
172
+ if (!isEditorFocused()) {
173
+ console.log('[FocusManagement] Focus lost, attempting recovery');
174
+ restoreFocus();
175
+ }
176
+ }, 30); // 30msに短縮してフォーカス復元を高速化
177
+ };
178
+
179
+ /**
180
+ * クリック時のフォーカス確認
181
+ * エディタ内をクリックした場合、フォーカスを確保
182
+ */
183
+ const handleClick = (e: MouseEvent) => {
184
+ const target = e.target as HTMLElement;
185
+ const editorContainer = document.querySelector('[data-frontend-visual-editor="true"]');
186
+
187
+ // エディタ内のクリックでフォーカスが失われていれば復元
188
+ if (editorContainer?.contains(target)) {
189
+ // 入力要素でなければiframeにフォーカス
190
+ if (
191
+ !(target instanceof HTMLInputElement) &&
192
+ !(target instanceof HTMLTextAreaElement) &&
193
+ !target.isContentEditable &&
194
+ target.getAttribute('contenteditable') !== 'true'
195
+ ) {
196
+ // 少し遅延してフォーカス状態を確認
197
+ setTimeout(() => {
198
+ if (!isEditorFocused()) {
199
+ restoreFocus();
200
+ }
201
+ }, 50);
202
+ }
203
+ }
204
+ };
205
+
206
+ // iframe内のフォーカス管理
207
+ const handleIframeFocusIn = () => {
208
+ lastFocusedElementRef.current = 'iframe';
209
+ };
210
+
211
+ const handleParentFocusIn = () => {
212
+ const editorContainer = document.querySelector('[data-frontend-visual-editor="true"]');
213
+ if (editorContainer?.contains(document.activeElement)) {
214
+ lastFocusedElementRef.current = 'parent';
215
+ }
216
+ };
217
+
218
+ // イベントリスナーを登録
219
+ document.addEventListener('focusout', handleFocusOut, true);
220
+ document.addEventListener('click', handleClick, true);
221
+ document.addEventListener('focusin', handleParentFocusIn, true);
222
+
223
+ if (iframeDoc) {
224
+ iframeDoc.addEventListener('focusin', handleIframeFocusIn, true);
225
+ }
226
+
227
+ // クリーンアップ
228
+ return () => {
229
+ if (focusRecoveryTimerRef.current) {
230
+ clearTimeout(focusRecoveryTimerRef.current);
231
+ }
232
+ document.removeEventListener('focusout', handleFocusOut, true);
233
+ document.removeEventListener('click', handleClick, true);
234
+ document.removeEventListener('focusin', handleParentFocusIn, true);
235
+
236
+ if (iframeDoc) {
237
+ iframeDoc.removeEventListener('focusin', handleIframeFocusIn, true);
238
+ }
239
+ };
240
+ }, [iframeRef, getIframeDoc, isEditorFocused, restoreFocus]);
241
+
242
+ return {
243
+ restoreFocus,
244
+ isEditorFocused,
245
+ setupFocusRecovery,
246
+ };
247
+ }
248
+
249
+ export default useFocusManagement;
@@ -0,0 +1,305 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useMemo } from 'react';
4
+
5
+ /**
6
+ * Google Fontの情報
7
+ */
8
+ export interface GoogleFont {
9
+ family: string;
10
+ variants: string[];
11
+ subsets: string[];
12
+ category: 'sans-serif' | 'serif' | 'display' | 'handwriting' | 'monospace';
13
+ /**
14
+ * 適用時に書き込む実際の font-family 値(デバイスフォント用)。
15
+ * 游ゴシックは macOS と Windows で family 名が違うため、単一名で書くと
16
+ * 片方のOSで外れる。スタックごと書き込んで両対応にする。
17
+ */
18
+ stack?: string;
19
+ }
20
+
21
+ /**
22
+ * フォントカテゴリ
23
+ */
24
+ export const FONT_CATEGORIES = [
25
+ { value: 'all', label: 'すべて' },
26
+ { value: 'sans-serif', label: 'Sans Serif' },
27
+ { value: 'serif', label: 'Serif' },
28
+ { value: 'display', label: 'Display' },
29
+ { value: 'handwriting', label: '手書き' },
30
+ { value: 'monospace', label: 'Monospace' },
31
+ ] as const;
32
+
33
+ export type FontCategory = typeof FONT_CATEGORIES[number]['value'];
34
+
35
+ /**
36
+ * 人気のGoogle Fontsリスト(APIなしでも使えるフォールバック)
37
+ */
38
+ const POPULAR_GOOGLE_FONTS: GoogleFont[] = [
39
+ { family: 'Noto Sans JP', variants: ['100', '300', '400', '500', '700', '900'], subsets: ['japanese', 'latin'], category: 'sans-serif' },
40
+ { family: 'Noto Serif JP', variants: ['200', '300', '400', '500', '600', '700', '900'], subsets: ['japanese', 'latin'], category: 'serif' },
41
+ { family: 'M PLUS 1p', variants: ['100', '300', '400', '500', '700', '800', '900'], subsets: ['japanese', 'latin'], category: 'sans-serif' },
42
+ { family: 'M PLUS Rounded 1c', variants: ['100', '300', '400', '500', '700', '800', '900'], subsets: ['japanese', 'latin'], category: 'sans-serif' },
43
+ { family: 'Kosugi Maru', variants: ['400'], subsets: ['japanese', 'latin'], category: 'sans-serif' },
44
+ { family: 'Kosugi', variants: ['400'], subsets: ['japanese', 'latin'], category: 'sans-serif' },
45
+ { family: 'Sawarabi Gothic', variants: ['400'], subsets: ['japanese', 'latin'], category: 'sans-serif' },
46
+ { family: 'Sawarabi Mincho', variants: ['400'], subsets: ['japanese', 'latin'], category: 'serif' },
47
+ { family: 'Zen Kaku Gothic New', variants: ['300', '400', '500', '700', '900'], subsets: ['japanese', 'latin'], category: 'sans-serif' },
48
+ { family: 'Zen Maru Gothic', variants: ['300', '400', '500', '700', '900'], subsets: ['japanese', 'latin'], category: 'sans-serif' },
49
+ { family: 'Shippori Mincho', variants: ['400', '500', '600', '700', '800'], subsets: ['japanese', 'latin'], category: 'serif' },
50
+ { family: 'BIZ UDGothic', variants: ['400', '700'], subsets: ['japanese', 'latin'], category: 'sans-serif' },
51
+ { family: 'BIZ UDMincho', variants: ['400', '700'], subsets: ['japanese', 'latin'], category: 'serif' },
52
+ { family: 'Roboto', variants: ['100', '300', '400', '500', '700', '900'], subsets: ['latin'], category: 'sans-serif' },
53
+ { family: 'Open Sans', variants: ['300', '400', '500', '600', '700', '800'], subsets: ['latin'], category: 'sans-serif' },
54
+ { family: 'Lato', variants: ['100', '300', '400', '700', '900'], subsets: ['latin'], category: 'sans-serif' },
55
+ { family: 'Montserrat', variants: ['100', '200', '300', '400', '500', '600', '700', '800', '900'], subsets: ['latin'], category: 'sans-serif' },
56
+ { family: 'Poppins', variants: ['100', '200', '300', '400', '500', '600', '700', '800', '900'], subsets: ['latin'], category: 'sans-serif' },
57
+ { family: 'Inter', variants: ['100', '200', '300', '400', '500', '600', '700', '800', '900'], subsets: ['latin'], category: 'sans-serif' },
58
+ { family: 'Oswald', variants: ['200', '300', '400', '500', '600', '700'], subsets: ['latin'], category: 'sans-serif' },
59
+ { family: 'Raleway', variants: ['100', '200', '300', '400', '500', '600', '700', '800', '900'], subsets: ['latin'], category: 'sans-serif' },
60
+ { family: 'Playfair Display', variants: ['400', '500', '600', '700', '800', '900'], subsets: ['latin'], category: 'serif' },
61
+ { family: 'Merriweather', variants: ['300', '400', '700', '900'], subsets: ['latin'], category: 'serif' },
62
+ { family: 'Lora', variants: ['400', '500', '600', '700'], subsets: ['latin'], category: 'serif' },
63
+ { family: 'PT Serif', variants: ['400', '700'], subsets: ['latin'], category: 'serif' },
64
+ { family: 'Source Code Pro', variants: ['200', '300', '400', '500', '600', '700', '800', '900'], subsets: ['latin'], category: 'monospace' },
65
+ { family: 'Fira Code', variants: ['300', '400', '500', '600', '700'], subsets: ['latin'], category: 'monospace' },
66
+ { family: 'JetBrains Mono', variants: ['100', '200', '300', '400', '500', '600', '700', '800'], subsets: ['latin'], category: 'monospace' },
67
+ { family: 'Dancing Script', variants: ['400', '500', '600', '700'], subsets: ['latin'], category: 'handwriting' },
68
+ { family: 'Pacifico', variants: ['400'], subsets: ['latin'], category: 'handwriting' },
69
+ { family: 'Caveat', variants: ['400', '500', '600', '700'], subsets: ['latin'], category: 'handwriting' },
70
+ { family: 'Bebas Neue', variants: ['400'], subsets: ['latin'], category: 'display' },
71
+ { family: 'Anton', variants: ['400'], subsets: ['latin'], category: 'sans-serif' },
72
+ { family: 'Abril Fatface', variants: ['400'], subsets: ['latin'], category: 'display' },
73
+ ];
74
+
75
+ /**
76
+ * システムフォント
77
+ */
78
+ const SYSTEM_FONTS: GoogleFont[] = [
79
+ // 日本語のデバイスフォント。読み込み不要で即座に効き、書き出したPPTX(游ゴシック)とも揃う
80
+ {
81
+ family: '游ゴシック',
82
+ variants: ['400', '500', '700'],
83
+ subsets: ['japanese'],
84
+ category: 'sans-serif',
85
+ stack: '"Yu Gothic Medium", "游ゴシック Medium", YuGothic, "游ゴシック体", "Yu Gothic", "游ゴシック", sans-serif',
86
+ },
87
+ {
88
+ family: '游明朝',
89
+ variants: ['400', '500', '700'],
90
+ subsets: ['japanese'],
91
+ category: 'serif',
92
+ stack: '"Yu Mincho", "游明朝", YuMincho, "游明朝体", serif',
93
+ },
94
+ {
95
+ family: 'ヒラギノ角ゴシック',
96
+ variants: ['400', '600'],
97
+ subsets: ['japanese'],
98
+ category: 'sans-serif',
99
+ stack: '"Hiragino Kaku Gothic ProN", "Hiragino Sans", sans-serif',
100
+ },
101
+ {
102
+ family: 'ヒラギノ明朝',
103
+ variants: ['400', '600'],
104
+ subsets: ['japanese'],
105
+ category: 'serif',
106
+ stack: '"Hiragino Mincho ProN", serif',
107
+ },
108
+ {
109
+ family: 'メイリオ',
110
+ variants: ['400', '700'],
111
+ subsets: ['japanese'],
112
+ category: 'sans-serif',
113
+ stack: 'Meiryo, sans-serif',
114
+ },
115
+ { family: 'Arial', variants: ['400', '700'], subsets: ['latin'], category: 'sans-serif' },
116
+ { family: 'Helvetica', variants: ['400', '700'], subsets: ['latin'], category: 'sans-serif' },
117
+ { family: 'Georgia', variants: ['400', '700'], subsets: ['latin'], category: 'serif' },
118
+ { family: 'Times New Roman', variants: ['400', '700'], subsets: ['latin'], category: 'serif' },
119
+ { family: 'Verdana', variants: ['400', '700'], subsets: ['latin'], category: 'sans-serif' },
120
+ { family: 'Courier New', variants: ['400', '700'], subsets: ['latin'], category: 'monospace' },
121
+ { family: 'system-ui', variants: ['400', '700'], subsets: ['latin'], category: 'sans-serif' },
122
+ ];
123
+
124
+ // ローカルストレージのキー
125
+ const FONTS_CACHE_KEY = 'google-fonts-cache';
126
+ const FONTS_CACHE_EXPIRY_KEY = 'google-fonts-cache-expiry';
127
+ const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7日間
128
+
129
+ /**
130
+ * Google Fontsを取得・管理するhook
131
+ */
132
+ export function useGoogleFonts() {
133
+ const [fonts, setFonts] = useState<GoogleFont[]>([...SYSTEM_FONTS, ...POPULAR_GOOGLE_FONTS]);
134
+ const [isLoading, setIsLoading] = useState(false);
135
+ const [error, setError] = useState<string | null>(null);
136
+ const [loadedFonts, setLoadedFonts] = useState<Set<string>>(new Set());
137
+
138
+ // Google Fonts APIからフォントリストを取得
139
+ const fetchGoogleFonts = useCallback(async () => {
140
+ // キャッシュをチェック
141
+ try {
142
+ const cachedFonts = localStorage.getItem(FONTS_CACHE_KEY);
143
+ const cacheExpiry = localStorage.getItem(FONTS_CACHE_EXPIRY_KEY);
144
+
145
+ if (cachedFonts && cacheExpiry && Date.now() < parseInt(cacheExpiry, 10)) {
146
+ const parsed = JSON.parse(cachedFonts) as GoogleFont[];
147
+ setFonts([...SYSTEM_FONTS, ...parsed]);
148
+ return;
149
+ }
150
+ } catch {
151
+ // キャッシュ読み込みエラーは無視
152
+ }
153
+
154
+ setIsLoading(true);
155
+ setError(null);
156
+
157
+ try {
158
+ // Google Fonts APIキーを環境変数から取得(オプション)
159
+ // [移植時の修正] Vite には process.env が無い。
160
+ // 未設定なら取得をスキップする(以前は毎回 ReferenceError を投げていた)
161
+ // [パッケージ化での変更] Next/Vite どちらでも読める形にする
162
+ const apiKey =
163
+ (typeof process !== 'undefined' ? process.env?.NEXT_PUBLIC_GOOGLE_FONTS_API_KEY : undefined) ??
164
+ undefined;
165
+
166
+ // APIキーがある場合は直接Google Fonts APIを使用
167
+ // ない場合は内部APIエンドポイント経由で取得
168
+ const apiUrl = apiKey
169
+ ? `https://www.googleapis.com/webfonts/v1/webfonts?key=${apiKey}&sort=popularity`
170
+ : '/api/google-fonts';
171
+
172
+ const response = await fetch(apiUrl);
173
+
174
+ if (!response.ok) {
175
+ throw new Error('Failed to fetch Google Fonts');
176
+ }
177
+
178
+ const data = await response.json();
179
+
180
+ // APIエラーまたは空の場合はフォールバック
181
+ if (data.error || !data.items || data.items.length === 0) {
182
+ console.log('[useGoogleFonts] API returned empty, using popular fonts');
183
+ setFonts([...SYSTEM_FONTS, ...POPULAR_GOOGLE_FONTS]);
184
+ return;
185
+ }
186
+
187
+ const googleFonts: GoogleFont[] = data.items.map((item: any) => ({
188
+ family: item.family,
189
+ variants: item.variants,
190
+ subsets: item.subsets,
191
+ category: item.category,
192
+ }));
193
+
194
+ // キャッシュに保存
195
+ try {
196
+ localStorage.setItem(FONTS_CACHE_KEY, JSON.stringify(googleFonts));
197
+ localStorage.setItem(FONTS_CACHE_EXPIRY_KEY, String(Date.now() + CACHE_DURATION));
198
+ } catch {
199
+ // ストレージが一杯の場合は無視
200
+ }
201
+
202
+ setFonts([...SYSTEM_FONTS, ...googleFonts]);
203
+ } catch (err) {
204
+ console.error('[useGoogleFonts] Error fetching fonts:', err);
205
+ setError(err instanceof Error ? err.message : 'Unknown error');
206
+ // エラー時は人気フォントを使用
207
+ setFonts([...SYSTEM_FONTS, ...POPULAR_GOOGLE_FONTS]);
208
+ } finally {
209
+ setIsLoading(false);
210
+ }
211
+ }, []);
212
+
213
+ // 初回ロード
214
+ useEffect(() => {
215
+ fetchGoogleFonts();
216
+ }, [fetchGoogleFonts]);
217
+
218
+ /**
219
+ * フォントをiframe内にロードする
220
+ */
221
+ const loadFontInIframe = useCallback((
222
+ fontFamily: string,
223
+ iframeDoc: Document,
224
+ weights: string[] = ['400', '700']
225
+ ) => {
226
+ // システムフォントはロード不要
227
+ const isSystemFont = SYSTEM_FONTS.some(f => f.family === fontFamily);
228
+ if (isSystemFont) return;
229
+
230
+ // 既にロード済みの場合はスキップ
231
+ const fontKey = `${fontFamily}-${weights.join(',')}`;
232
+ if (loadedFonts.has(fontKey)) return;
233
+
234
+ // linkタグが既に存在するかチェック
235
+ const existingLink = iframeDoc.querySelector(`link[data-font-family="${fontFamily}"]`);
236
+ if (existingLink) return;
237
+
238
+ // Google Fonts URLを生成
239
+ const encodedFamily = encodeURIComponent(fontFamily);
240
+ const weightsParam = weights.join(';');
241
+ const url = `https://fonts.googleapis.com/css2?family=${encodedFamily}:wght@${weightsParam}&display=swap`;
242
+
243
+ // linkタグを作成してheadに追加
244
+ const link = iframeDoc.createElement('link');
245
+ link.rel = 'stylesheet';
246
+ link.href = url;
247
+ link.dataset.fontFamily = fontFamily;
248
+ iframeDoc.head.appendChild(link);
249
+
250
+ // ロード済みセットに追加
251
+ setLoadedFonts(prev => new Set(prev).add(fontKey));
252
+
253
+ console.log(`[useGoogleFonts] Loaded font: ${fontFamily}`);
254
+ }, [loadedFonts]);
255
+
256
+ /**
257
+ * フォントのプレビュー用CSSを取得
258
+ */
259
+ const getFontPreviewStyle = useCallback((fontFamily: string): React.CSSProperties => {
260
+ // スタック(カンマ入り)やデバイスフォントの表示名はそのまま/変換して使う。
261
+ // 二重に引用すると `""Yu Gothic", sans-serif"` のような壊れた値になる
262
+ if (fontFamily.includes(',')) return { fontFamily };
263
+ const sys = SYSTEM_FONTS.find((f) => f.family === fontFamily);
264
+ if (sys?.stack) return { fontFamily: sys.stack };
265
+ return { fontFamily: `"${fontFamily}", sans-serif` };
266
+ }, []);
267
+
268
+ return {
269
+ fonts,
270
+ isLoading,
271
+ error,
272
+ loadFontInIframe,
273
+ getFontPreviewStyle,
274
+ systemFonts: SYSTEM_FONTS,
275
+ popularFonts: POPULAR_GOOGLE_FONTS,
276
+ };
277
+ }
278
+
279
+ /**
280
+ * フォントをフィルタリングするhook
281
+ */
282
+ export function useFilteredFonts(
283
+ fonts: GoogleFont[],
284
+ searchQuery: string,
285
+ category: FontCategory
286
+ ) {
287
+ return useMemo(() => {
288
+ let filtered = fonts;
289
+
290
+ // カテゴリフィルター
291
+ if (category !== 'all') {
292
+ filtered = filtered.filter(f => f.category === category);
293
+ }
294
+
295
+ // 検索フィルター
296
+ if (searchQuery.trim()) {
297
+ const query = searchQuery.toLowerCase();
298
+ filtered = filtered.filter(f =>
299
+ f.family.toLowerCase().includes(query)
300
+ );
301
+ }
302
+
303
+ return filtered;
304
+ }, [fonts, searchQuery, category]);
305
+ }