@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,34 @@
1
+ /**
2
+ * 自動保存のフラッシュ窓口。
3
+ *
4
+ * ページ切替(サムネイルのクリック・新しいスライドの挿入)は殻(PptChrome)側で起きるが、
5
+ * 保存の実体はエディタ本体(FrontendVisualEditor)にしかない。
6
+ * 殻へ props を通すと編集中の他ファイルとぶつかるため、
7
+ * navigation.ts の registerNavCommit と同じくモジュールに窓口を1つ置いて繋ぐ。
8
+ */
9
+
10
+ /** 未保存の変更を保存しきる関数。保存できた(または保存するものが無い)なら true */
11
+ type Flush = () => Promise<boolean>
12
+
13
+ let flush: Flush | null = null
14
+
15
+ /** エディタ本体が自身の保存関数を登録する。戻り値は解除関数 */
16
+ export function registerAutoSaveFlush(fn: Flush): () => void {
17
+ flush = fn
18
+ return () => {
19
+ if (flush === fn) flush = null
20
+ }
21
+ }
22
+
23
+ /**
24
+ * 未保存の変更を保存しきってから次の操作へ進むために呼ぶ。
25
+ * エディタが開いていなければ何もせず true(遷移を止める理由が無い)。
26
+ */
27
+ export async function flushAutoSave(): Promise<boolean> {
28
+ if (!flush) return true
29
+ try {
30
+ return await flush()
31
+ } catch {
32
+ return false
33
+ }
34
+ }
@@ -0,0 +1,378 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * AI プロンプト入力ポップオーバー
5
+ * Figmaライクなミニマムなフローティング入力UI
6
+ * 画像やPDFファイルの添付に対応
7
+ */
8
+
9
+ import { useState, useRef, useEffect, useCallback } from 'react';
10
+ import { Sparkles, X, Loader2, Send, Paperclip, FileImage, FileText, Trash2 } from 'lucide-react';
11
+ import { Button } from '../../components/ui/button';
12
+ import { Textarea } from '../../components/ui/textarea';
13
+ import type { AttachedFile } from '../../lib/agent/slide-agent/types';
14
+
15
+ // サポートするファイルタイプ
16
+ const SUPPORTED_FILE_TYPES = {
17
+ 'image/png': true,
18
+ 'image/jpeg': true,
19
+ 'image/gif': true,
20
+ 'image/webp': true,
21
+ 'application/pdf': true,
22
+ };
23
+
24
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
25
+ const MAX_FILES = 5;
26
+
27
+ export interface AiPromptPopoverProps {
28
+ isOpen: boolean;
29
+ position: { x: number; y: number };
30
+ onClose: () => void;
31
+ onGenerate: (prompt: string, attachedFiles?: AttachedFile[], engine?: 'codex' | 'claude') => Promise<void>;
32
+ isGenerating: boolean;
33
+ selectedElementInfo?: string; // 選択要素の簡易説明
34
+ }
35
+
36
+ export function AiPromptPopover({
37
+ isOpen,
38
+ position,
39
+ onClose,
40
+ onGenerate,
41
+ isGenerating,
42
+ selectedElementInfo,
43
+ }: AiPromptPopoverProps) {
44
+ const [prompt, setPrompt] = useState('');
45
+ // [移植時の追加] 生成に使うAIエンジン(ローカルCLI)を選択する
46
+ const [engine, setEngine] = useState<'codex' | 'claude'>(() => {
47
+ const saved = typeof window !== 'undefined' ? localStorage.getItem('gg-ai-engine') : null;
48
+ return saved === 'claude' ? 'claude' : 'codex';
49
+ });
50
+ const [attachedFiles, setAttachedFiles] = useState<AttachedFile[]>([]);
51
+ const [fileError, setFileError] = useState<string | null>(null);
52
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
53
+ const containerRef = useRef<HTMLDivElement>(null);
54
+ const fileInputRef = useRef<HTMLInputElement>(null);
55
+
56
+ // ファイル添付ハンドラー
57
+ const handleFileSelect = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
58
+ const files = e.target.files;
59
+ if (!files || files.length === 0) return;
60
+
61
+ setFileError(null);
62
+
63
+ // ファイル数チェック
64
+ if (attachedFiles.length + files.length > MAX_FILES) {
65
+ setFileError(`ファイルは最大${MAX_FILES}個まで添付できます`);
66
+ return;
67
+ }
68
+
69
+ const newFiles: AttachedFile[] = [];
70
+
71
+ for (let i = 0; i < files.length; i++) {
72
+ const file = files[i];
73
+
74
+ // ファイルタイプチェック
75
+ if (!SUPPORTED_FILE_TYPES[file.type as keyof typeof SUPPORTED_FILE_TYPES]) {
76
+ setFileError(`${file.name}: サポートされていないファイル形式です(画像またはPDFのみ)`);
77
+ continue;
78
+ }
79
+
80
+ // ファイルサイズチェック
81
+ if (file.size > MAX_FILE_SIZE) {
82
+ setFileError(`${file.name}: ファイルサイズが大きすぎます(最大10MB)`);
83
+ continue;
84
+ }
85
+
86
+ // Base64に変換
87
+ try {
88
+ const data = await fileToBase64(file);
89
+ newFiles.push({
90
+ name: file.name,
91
+ type: file.type,
92
+ size: file.size,
93
+ data,
94
+ });
95
+ } catch (err) {
96
+ console.error('ファイル読み込みエラー:', err);
97
+ setFileError(`${file.name}: ファイルの読み込みに失敗しました`);
98
+ }
99
+ }
100
+
101
+ if (newFiles.length > 0) {
102
+ setAttachedFiles(prev => [...prev, ...newFiles]);
103
+ }
104
+
105
+ // input をリセット
106
+ if (fileInputRef.current) {
107
+ fileInputRef.current.value = '';
108
+ }
109
+ }, [attachedFiles.length]);
110
+
111
+ // ファイル削除ハンドラー
112
+ const handleRemoveFile = useCallback((index: number) => {
113
+ setAttachedFiles(prev => prev.filter((_, i) => i !== index));
114
+ setFileError(null);
115
+ }, []);
116
+
117
+ // ファイルをBase64に変換
118
+ const fileToBase64 = (file: File): Promise<string> => {
119
+ return new Promise((resolve, reject) => {
120
+ const reader = new FileReader();
121
+ reader.onload = () => resolve(reader.result as string);
122
+ reader.onerror = reject;
123
+ reader.readAsDataURL(file);
124
+ });
125
+ };
126
+
127
+ // ファイル選択ダイアログを開く
128
+ const openFileDialog = useCallback(() => {
129
+ fileInputRef.current?.click();
130
+ }, []);
131
+
132
+ // ポップオーバーが開いたらテキストエリアにフォーカス
133
+ useEffect(() => {
134
+ if (isOpen && textareaRef.current) {
135
+ setTimeout(() => {
136
+ textareaRef.current?.focus();
137
+ }, 100);
138
+ }
139
+ }, [isOpen]);
140
+
141
+ // Escキーで閉じる
142
+ useEffect(() => {
143
+ if (!isOpen) return;
144
+
145
+ const handleKeyDown = (e: KeyboardEvent) => {
146
+ if (e.key === 'Escape') {
147
+ onClose();
148
+ }
149
+ // Cmd/Ctrl + Enter で送信
150
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter' && prompt.trim() && !isGenerating) {
151
+ e.preventDefault();
152
+ handleGenerate();
153
+ }
154
+ };
155
+
156
+ document.addEventListener('keydown', handleKeyDown);
157
+ return () => document.removeEventListener('keydown', handleKeyDown);
158
+ }, [isOpen, onClose, prompt, isGenerating]);
159
+
160
+ // 外側クリックで閉じる
161
+ useEffect(() => {
162
+ if (!isOpen) return;
163
+
164
+ const handleClickOutside = (e: MouseEvent) => {
165
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
166
+ onClose();
167
+ }
168
+ };
169
+
170
+ // 次のフレームでリスナーを追加(トリガーイベントの完了を待つ)
171
+ requestAnimationFrame(() => {
172
+ document.addEventListener('mousedown', handleClickOutside);
173
+ });
174
+
175
+ return () => {
176
+ document.removeEventListener('mousedown', handleClickOutside);
177
+ };
178
+ }, [isOpen, onClose]);
179
+
180
+ // 生成実行
181
+ const handleGenerate = useCallback(async () => {
182
+ if (!prompt.trim() || isGenerating) return;
183
+
184
+ try {
185
+ await onGenerate(prompt, attachedFiles.length > 0 ? attachedFiles : undefined, engine);
186
+ setPrompt('');
187
+ setAttachedFiles([]);
188
+ setFileError(null);
189
+ onClose();
190
+ } catch (error) {
191
+ console.error('AI generation failed:', error);
192
+ }
193
+ }, [prompt, isGenerating, onGenerate, onClose, attachedFiles, engine]);
194
+
195
+ // 位置調整(画面外にはみ出さないように)
196
+ const getPopoverStyle = useCallback(() => {
197
+ const width = 320;
198
+ const height = 200;
199
+ const padding = 16;
200
+
201
+ let x = position.x;
202
+ let y = position.y;
203
+
204
+ // 右端チェック
205
+ if (x + width > window.innerWidth - padding) {
206
+ x = window.innerWidth - width - padding;
207
+ }
208
+
209
+ // 左端チェック
210
+ if (x < padding) {
211
+ x = padding;
212
+ }
213
+
214
+ // 下端チェック
215
+ if (y + height > window.innerHeight - padding) {
216
+ y = position.y - height - 10; // 上に表示
217
+ }
218
+
219
+ // 上端チェック
220
+ if (y < padding) {
221
+ y = padding;
222
+ }
223
+
224
+ return {
225
+ left: `${x}px`,
226
+ top: `${y}px`,
227
+ };
228
+ }, [position]);
229
+
230
+ if (!isOpen) return null;
231
+
232
+ return (
233
+ <div
234
+ ref={containerRef}
235
+ className="fixed z-[70] bg-[#1e1e1e] border border-[#444444] rounded-xl shadow-2xl overflow-hidden"
236
+ style={{
237
+ ...getPopoverStyle(),
238
+ width: '320px',
239
+ }}
240
+ >
241
+ {/* ヘッダー */}
242
+ <div className="flex items-center justify-between px-3 py-2 border-b border-[#444444] bg-[#2c2c2c]">
243
+ <div className="flex items-center gap-2">
244
+ <Sparkles className="w-4 h-4 text-purple-400" />
245
+ <span className="text-xs font-medium text-gray-200">AIで生成</span>
246
+ </div>
247
+ <button
248
+ onClick={onClose}
249
+ className="p-1 rounded hover:bg-[#444444] text-gray-400 hover:text-gray-200 transition-colors"
250
+ >
251
+ <X className="w-3.5 h-3.5" />
252
+ </button>
253
+ </div>
254
+
255
+ {/* 選択要素の情報 */}
256
+ {selectedElementInfo && (
257
+ <div className="px-3 py-2 bg-[#2c2c2c] border-b border-[#444444]">
258
+ <p className="text-[10px] text-gray-500 truncate">
259
+ 選択中: {selectedElementInfo}
260
+ </p>
261
+ </div>
262
+ )}
263
+
264
+ {/* プロンプト入力 */}
265
+ <div className="p-3">
266
+ <Textarea
267
+ ref={textareaRef}
268
+ value={prompt}
269
+ onChange={(e) => setPrompt(e.target.value)}
270
+ placeholder="どのように変更しますか?&#10;例: 青いグラデーション背景のカード、モダンなデザインに..."
271
+ className="min-h-[80px] text-xs bg-[#2c2c2c] border-[#444444] text-white placeholder:text-gray-500 resize-none focus:ring-purple-500 focus:border-purple-500"
272
+ disabled={isGenerating}
273
+ />
274
+
275
+ {/* 添付ファイル一覧 */}
276
+ {attachedFiles.length > 0 && (
277
+ <div className="mt-2 space-y-1">
278
+ {attachedFiles.map((file, index) => (
279
+ <div
280
+ key={`${file.name}-${index}`}
281
+ className="flex items-center gap-2 px-2 py-1.5 bg-[#2c2c2c] border border-[#444444] rounded text-[10px] text-gray-300"
282
+ >
283
+ {file.type.startsWith('image/') ? (
284
+ <FileImage className="w-3 h-3 text-[#4fb8ff] shrink-0" />
285
+ ) : (
286
+ <FileText className="w-3 h-3 text-red-400 shrink-0" />
287
+ )}
288
+ <span className="truncate flex-1">{file.name}</span>
289
+ <span className="text-gray-500 shrink-0">
290
+ {(file.size / 1024).toFixed(0)}KB
291
+ </span>
292
+ <button
293
+ onClick={() => handleRemoveFile(index)}
294
+ className="p-0.5 rounded hover:bg-[#444444] text-gray-400 hover:text-red-400 shrink-0"
295
+ disabled={isGenerating}
296
+ >
297
+ <Trash2 className="w-3 h-3" />
298
+ </button>
299
+ </div>
300
+ ))}
301
+ </div>
302
+ )}
303
+
304
+ {/* ファイルエラー */}
305
+ {fileError && (
306
+ <p className="mt-2 text-[10px] text-red-400">{fileError}</p>
307
+ )}
308
+
309
+ {/* フッター */}
310
+ <div className="flex items-center justify-between mt-3">
311
+ <div className="flex items-center gap-2">
312
+ {/* ファイル添付ボタン */}
313
+ <input
314
+ ref={fileInputRef}
315
+ type="file"
316
+ accept="image/*,application/pdf"
317
+ multiple
318
+ onChange={handleFileSelect}
319
+ className="hidden"
320
+ />
321
+ <button
322
+ onClick={openFileDialog}
323
+ disabled={isGenerating || attachedFiles.length >= MAX_FILES}
324
+ className="p-1.5 rounded hover:bg-[#444444] text-gray-400 hover:text-gray-200 transition-colors disabled:opacity-50 disabled:hover:bg-transparent"
325
+ title="ファイルを添付(画像、PDF)"
326
+ >
327
+ <Paperclip className="w-3.5 h-3.5" />
328
+ </button>
329
+ <div className="flex items-center gap-0.5 rounded border border-[#444444] bg-[#2c2c2c] p-0.5">
330
+ {(['codex', 'claude'] as const).map((e) => (
331
+ <button
332
+ key={e}
333
+ type="button"
334
+ onClick={() => {
335
+ setEngine(e);
336
+ localStorage.setItem('gg-ai-engine', e);
337
+ }}
338
+ className={`rounded px-1.5 py-0.5 text-[10px] transition-colors ${
339
+ engine === e
340
+ ? 'bg-purple-600 text-white'
341
+ : 'text-gray-400 hover:text-gray-200'
342
+ }`}
343
+ title={`${e === 'codex' ? 'Codex' : 'Claude'} で生成`}
344
+ >
345
+ {e === 'codex' ? 'Codex' : 'Claude'}
346
+ </button>
347
+ ))}
348
+ </div>
349
+ <span className="text-[10px] text-gray-500">
350
+ ⌘+Enter
351
+ </span>
352
+ </div>
353
+ <Button
354
+ size="sm"
355
+ onClick={handleGenerate}
356
+ disabled={!prompt.trim() || isGenerating}
357
+ className="h-7 px-3 text-xs bg-purple-600 hover:bg-purple-700 text-white disabled:opacity-50"
358
+ >
359
+ {isGenerating ? (
360
+ <>
361
+ {/* SVGアニメーションはdivラッパーで適用(ハードウェアアクセラレーション対応) */}
362
+ <div className="animate-spin mr-1.5">
363
+ <Loader2 className="w-3 h-3" />
364
+ </div>
365
+ 生成中...
366
+ </>
367
+ ) : (
368
+ <>
369
+ <Send className="w-3 h-3 mr-1.5" />
370
+ 生成
371
+ </>
372
+ )}
373
+ </Button>
374
+ </div>
375
+ </div>
376
+ </div>
377
+ );
378
+ }