@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.
- package/LICENSE +21 -0
- package/NOTICE +52 -0
- package/README.md +143 -0
- package/dist/editor.css +2 -0
- package/package.json +94 -0
- package/src/components/DeckSlideRender.tsx +21 -0
- package/src/components/SaveNote.tsx +21 -0
- package/src/components/auth/AuthProvider.tsx +44 -0
- package/src/components/ui/accordion.tsx +57 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +76 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +11 -0
- package/src/components/ui/context-menu.tsx +200 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/scrubbable-label.tsx +95 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/components/viewer/useDeck.ts +58 -0
- package/src/editor/EditorContext.tsx +632 -0
- package/src/editor/EditorToolbar.tsx +446 -0
- package/src/editor/FrontendVisualEditor.tsx +2595 -0
- package/src/editor/autosave.ts +34 -0
- package/src/editor/components/AiPromptPopover.tsx +378 -0
- package/src/editor/components/BreakpointGuides.tsx +323 -0
- package/src/editor/components/BreakpointSelector.tsx +180 -0
- package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
- package/src/editor/components/CssEditorDialog.tsx +168 -0
- package/src/editor/components/EditorCanvas.tsx +987 -0
- package/src/editor/components/EditorContextMenu.tsx +515 -0
- package/src/editor/components/EditorFooter.tsx +81 -0
- package/src/editor/components/EditorHeader.tsx +524 -0
- package/src/editor/components/EditorLayerPanel.tsx +1197 -0
- package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
- package/src/editor/components/EditorSidebar.tsx +226 -0
- package/src/editor/components/FigmaColorPicker.tsx +1577 -0
- package/src/editor/components/HtmlEditorDialog.tsx +71 -0
- package/src/editor/components/HtmlImportDialog.tsx +346 -0
- package/src/editor/components/JsEditorDialog.tsx +175 -0
- package/src/editor/components/LayerTreeItem.tsx +494 -0
- package/src/editor/components/MasterComponentEditor.tsx +846 -0
- package/src/editor/components/MediaLibraryDialog.tsx +447 -0
- package/src/editor/components/PageSettingsDialog.tsx +776 -0
- package/src/editor/components/VariablePicker.tsx +355 -0
- package/src/editor/components/VariablesDialog.tsx +564 -0
- package/src/editor/components/VariantEditor.tsx +687 -0
- package/src/editor/components/VariantMatrix.tsx +507 -0
- package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
- package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
- package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
- package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
- package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
- package/src/editor/components/component-panel/VariantList.tsx +260 -0
- package/src/editor/components/component-panel/index.ts +12 -0
- package/src/editor/components/index.ts +48 -0
- package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
- package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
- package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
- package/src/editor/components/multi-page/PageLabel.tsx +34 -0
- package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
- package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
- package/src/editor/components/multi-page/index.ts +7 -0
- package/src/editor/components/ppt/PptChrome.tsx +2508 -0
- package/src/editor/components/ppt/PptComments.tsx +589 -0
- package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
- package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
- package/src/editor/components/ppt/PptNotes.tsx +246 -0
- package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
- package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
- package/src/editor/components/property-panel/BorderSection.tsx +104 -0
- package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
- package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
- package/src/editor/components/property-panel/EffectSection.tsx +157 -0
- package/src/editor/components/property-panel/FillSection.tsx +69 -0
- package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
- package/src/editor/components/property-panel/ImageSection.tsx +206 -0
- package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
- package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
- package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
- package/src/editor/components/property-panel/LinkSection.tsx +103 -0
- package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
- package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
- package/src/editor/components/property-panel/TypographySection.tsx +230 -0
- package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
- package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
- package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
- package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
- package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
- package/src/editor/components/property-panel/index.ts +24 -0
- package/src/editor/components/property-panel/unit-utils.ts +261 -0
- package/src/editor/components/shell/EditorTopBar.tsx +392 -0
- package/src/editor/components/shell/LeftPanel.tsx +47 -0
- package/src/editor/components/shell/PagesPanel.tsx +118 -0
- package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
- package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
- package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
- package/src/editor/components/variables-panel/index.ts +7 -0
- package/src/editor/constants.ts +468 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
- package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
- package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
- package/src/editor/contexts/EditorRefsContext.tsx +204 -0
- package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
- package/src/editor/contexts/EditorToolContext.tsx +58 -0
- package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
- package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
- package/src/editor/contexts/EditorViewContext.tsx +101 -0
- package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
- package/src/editor/contexts/index.ts +17 -0
- package/src/editor/editor-skin.css +122 -0
- package/src/editor/hooks/index.ts +38 -0
- package/src/editor/hooks/useAiReplace.ts +397 -0
- package/src/editor/hooks/useAltMeasure.ts +67 -0
- package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
- package/src/editor/hooks/useCanvasControls.ts +1222 -0
- package/src/editor/hooks/useComponentEditMode.ts +658 -0
- package/src/editor/hooks/useComponentInstances.ts +515 -0
- package/src/editor/hooks/useContextMenuHandler.ts +157 -0
- package/src/editor/hooks/useCoordinateTransform.ts +207 -0
- package/src/editor/hooks/useDragResize.ts +1317 -0
- package/src/editor/hooks/useDrawingMode.ts +472 -0
- package/src/editor/hooks/useEditorColors.ts +144 -0
- package/src/editor/hooks/useEditorMessages.ts +108 -0
- package/src/editor/hooks/useElementActions.ts +1754 -0
- package/src/editor/hooks/useElementSelection.ts +1573 -0
- package/src/editor/hooks/useFocusManagement.ts +249 -0
- package/src/editor/hooks/useGoogleFonts.ts +305 -0
- package/src/editor/hooks/useIframeInitializer.ts +584 -0
- package/src/editor/hooks/useIframeSetup.ts +360 -0
- package/src/editor/hooks/useImageUpload.ts +270 -0
- package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
- package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
- package/src/editor/hooks/useMarqueeSelection.ts +388 -0
- package/src/editor/hooks/useMediaLibrary.ts +129 -0
- package/src/editor/hooks/usePageSettingsManager.ts +739 -0
- package/src/editor/hooks/useResizablePanel.ts +163 -0
- package/src/editor/hooks/useRichPaste.ts +255 -0
- package/src/editor/hooks/useTouchGestures.ts +197 -0
- package/src/editor/index.ts +27 -0
- package/src/editor/types/page-settings.ts +513 -0
- package/src/editor/types.ts +382 -0
- package/src/editor/utils/align-elements.ts +169 -0
- package/src/editor/utils/api-json.ts +21 -0
- package/src/editor/utils/aspect-lock.ts +50 -0
- package/src/editor/utils/component-renderer.ts +702 -0
- package/src/editor/utils/component-sync.ts +845 -0
- package/src/editor/utils/crop-mode.ts +265 -0
- package/src/editor/utils/dom-utils.ts +2238 -0
- package/src/editor/utils/element-effects.ts +218 -0
- package/src/editor/utils/eyedropper.ts +35 -0
- package/src/editor/utils/figma-export.ts +560 -0
- package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
- package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
- package/src/editor/utils/figma-paste.ts +2262 -0
- package/src/editor/utils/flex-reorder.ts +231 -0
- package/src/editor/utils/flex-utils.ts +1230 -0
- package/src/editor/utils/geometry.ts +232 -0
- package/src/editor/utils/grid-layout.ts +210 -0
- package/src/editor/utils/html-utils.ts +470 -0
- package/src/editor/utils/index.ts +11 -0
- package/src/editor/utils/ink-style.ts +57 -0
- package/src/editor/utils/inline-format.ts +236 -0
- package/src/editor/utils/measure-distance.ts +120 -0
- package/src/editor/utils/override-apply.ts +526 -0
- package/src/editor/utils/override-detection.ts +613 -0
- package/src/editor/utils/paste-processors.ts +609 -0
- package/src/editor/utils/paste-sanitizer.ts +244 -0
- package/src/editor/utils/restore-flow.ts +188 -0
- package/src/editor/utils/shape-library.ts +171 -0
- package/src/editor/utils/slide-root.ts +29 -0
- package/src/editor/utils/smart-guides.ts +218 -0
- package/src/editor/utils/style-utils.ts +685 -0
- package/src/editor/utils/table-edit.ts +151 -0
- package/src/editor/utils/tailwind-mappings.ts +778 -0
- package/src/editor/utils/tailwind-utils.ts +804 -0
- package/src/editor/utils/text-highlight.ts +216 -0
- package/src/editor/utils/theme-to-variables.ts +341 -0
- package/src/editor/utils/variant-resolver.ts +456 -0
- package/src/editor/utils/viewport-utils.ts +295 -0
- package/src/hooks/useEditorHistory.ts +195 -0
- package/src/hooks/useEditorShortcuts.ts +260 -0
- package/src/index.ts +40 -0
- package/src/io.ts +134 -0
- package/src/lib/agent/slide-agent/types.ts +547 -0
- package/src/lib/agent/website-agent/types.ts +805 -0
- package/src/lib/api/auth-fetch.ts +58 -0
- package/src/lib/deck.ts +69 -0
- package/src/lib/export.ts +47 -0
- package/src/lib/firebase/config.ts +29 -0
- package/src/lib/firebase/css-variables.ts +378 -0
- package/src/lib/firebase/editor-components.ts +677 -0
- package/src/lib/firebase/storage.ts +469 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/editor.css +52 -0
- package/src/styles/skin.css +122 -0
- package/src/types/css-variables.ts +645 -0
- package/src/types/editor-components.ts +944 -0
- package/src/types/editor.ts +420 -0
- package/src/types/page-master.ts +197 -0
- package/src/types/page.ts +419 -0
- package/src/types/slide.ts +402 -0
- package/src/types/website-theme.ts +788 -0
- package/src/types/website.ts +374 -0
- package/src/vendor/firebase-firestore.ts +13 -0
- 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
|
+
}
|