@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,776 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * ページ設定ダイアログ
5
+ * SEO、OGP、カスタムコード、外部リソースなどを統合管理
6
+ */
7
+
8
+ import { useState, useCallback, useEffect, useRef } from 'react';
9
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../../components/ui/dialog';
10
+ import { Button } from '../../components/ui/button';
11
+ import { Input } from '../../components/ui/input';
12
+ import { Textarea } from '../../components/ui/textarea';
13
+ import { Label } from '../../components/ui/label';
14
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../components/ui/tabs';
15
+ import { Switch } from '../../components/ui/switch';
16
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select';
17
+ import {
18
+ Settings,
19
+ Search,
20
+ Share2,
21
+ Code,
22
+ FileCode,
23
+ Link2,
24
+ Image as ImageIcon,
25
+ Plus,
26
+ Trash2,
27
+ Upload,
28
+ ExternalLink,
29
+ AlertCircle,
30
+ ChevronDown,
31
+ ChevronUp,
32
+ } from 'lucide-react';
33
+ import {
34
+ PageSettings,
35
+ ProjectPageSettings,
36
+ ExternalResource,
37
+ DEFAULT_PAGE_SETTINGS,
38
+ } from '../types/page-settings';
39
+
40
+ export interface PageSettingsDialogProps {
41
+ isOpen: boolean;
42
+ onClose: () => void;
43
+ pageSettings: PageSettings;
44
+ projectSettings?: ProjectPageSettings;
45
+ onSave: (settings: PageSettings) => void;
46
+ onUploadImage?: (file: File) => Promise<string>;
47
+ }
48
+
49
+ export function PageSettingsDialog({
50
+ isOpen,
51
+ onClose,
52
+ pageSettings,
53
+ projectSettings,
54
+ onSave,
55
+ onUploadImage,
56
+ }: PageSettingsDialogProps) {
57
+ const [settings, setSettings] = useState<PageSettings>({ ...DEFAULT_PAGE_SETTINGS, ...pageSettings });
58
+ const [activeTab, setActiveTab] = useState('seo');
59
+ const [hasChanges, setHasChanges] = useState(false);
60
+ const [uploadingImage, setUploadingImage] = useState(false);
61
+ const fileInputRef = useRef<HTMLInputElement>(null);
62
+ const [uploadTarget, setUploadTarget] = useState<'ogp' | 'twitter' | null>(null);
63
+
64
+ // ダイアログが開かれたときに設定を初期化
65
+ useEffect(() => {
66
+ if (isOpen) {
67
+ setSettings({ ...DEFAULT_PAGE_SETTINGS, ...pageSettings });
68
+ setHasChanges(false);
69
+ }
70
+ }, [isOpen, pageSettings]);
71
+
72
+ // 設定変更ハンドラ
73
+ const updateSettings = useCallback((updates: Partial<PageSettings>) => {
74
+ setSettings(prev => ({ ...prev, ...updates }));
75
+ setHasChanges(true);
76
+ }, []);
77
+
78
+ // ネストされた設定の更新
79
+ const updateNestedSettings = useCallback(<K extends keyof PageSettings>(
80
+ key: K,
81
+ updates: Partial<NonNullable<PageSettings[K]>>
82
+ ) => {
83
+ setSettings(prev => ({
84
+ ...prev,
85
+ [key]: { ...(prev[key] as object || {}), ...updates },
86
+ }));
87
+ setHasChanges(true);
88
+ }, []);
89
+
90
+ // 保存ハンドラ
91
+ const handleSave = useCallback(() => {
92
+ onSave(settings);
93
+ onClose();
94
+ }, [settings, onSave, onClose]);
95
+
96
+ // 画像アップロード
97
+ const handleImageUpload = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
98
+ const file = e.target.files?.[0];
99
+ if (!file || !onUploadImage || !uploadTarget) return;
100
+
101
+ setUploadingImage(true);
102
+ try {
103
+ const url = await onUploadImage(file);
104
+ if (uploadTarget === 'ogp') {
105
+ updateNestedSettings('ogp', { image: url });
106
+ } else if (uploadTarget === 'twitter') {
107
+ updateNestedSettings('twitter', { image: url });
108
+ }
109
+ } catch (err) {
110
+ console.error('Image upload failed:', err);
111
+ } finally {
112
+ setUploadingImage(false);
113
+ setUploadTarget(null);
114
+ if (fileInputRef.current) {
115
+ fileInputRef.current.value = '';
116
+ }
117
+ }
118
+ }, [onUploadImage, uploadTarget, updateNestedSettings]);
119
+
120
+ // 外部リソース追加
121
+ const addExternalResource = useCallback((type: 'css' | 'js') => {
122
+ const newResource: ExternalResource = {
123
+ id: `${type}-${Date.now()}`,
124
+ url: '',
125
+ type,
126
+ position: type === 'js' ? 'body-end' : undefined,
127
+ };
128
+ const key = type === 'css' ? 'externalCss' : 'externalJs';
129
+ updateSettings({
130
+ [key]: [...(settings[key] || []), newResource],
131
+ });
132
+ }, [settings, updateSettings]);
133
+
134
+ // 外部リソース更新
135
+ const updateExternalResource = useCallback((
136
+ type: 'css' | 'js',
137
+ id: string,
138
+ updates: Partial<ExternalResource>
139
+ ) => {
140
+ const key = type === 'css' ? 'externalCss' : 'externalJs';
141
+ const resources = settings[key] || [];
142
+ updateSettings({
143
+ [key]: resources.map(r => r.id === id ? { ...r, ...updates } : r),
144
+ });
145
+ }, [settings, updateSettings]);
146
+
147
+ // 外部リソース削除
148
+ const removeExternalResource = useCallback((type: 'css' | 'js', id: string) => {
149
+ const key = type === 'css' ? 'externalCss' : 'externalJs';
150
+ const resources = settings[key] || [];
151
+ updateSettings({
152
+ [key]: resources.filter(r => r.id !== id),
153
+ });
154
+ }, [settings, updateSettings]);
155
+
156
+ return (
157
+ <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
158
+ <DialogContent className="sm:max-w-[900px] w-[95vw] h-[90vh] max-h-[90vh] bg-[#2c2c2c] border-[#444444] text-white flex flex-col p-0">
159
+ <DialogHeader className="flex-shrink-0 px-6 pt-6 pb-2">
160
+ <DialogTitle className="text-white flex items-center gap-2">
161
+ <Settings className="w-5 h-5" />
162
+ ページ設定
163
+ </DialogTitle>
164
+ </DialogHeader>
165
+
166
+ <input
167
+ ref={fileInputRef}
168
+ type="file"
169
+ accept="image/*"
170
+ className="hidden"
171
+ onChange={handleImageUpload}
172
+ />
173
+
174
+ <Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col min-h-0 px-6">
175
+ <TabsList className="grid w-full grid-cols-5 bg-[#1e1e1e] flex-shrink-0">
176
+ <TabsTrigger value="seo" className="data-[state=active]:bg-[#444444] text-xs">
177
+ <Search className="w-3 h-3 mr-1" />
178
+ SEO
179
+ </TabsTrigger>
180
+ <TabsTrigger value="social" className="data-[state=active]:bg-[#444444] text-xs">
181
+ <Share2 className="w-3 h-3 mr-1" />
182
+ OGP/SNS
183
+ </TabsTrigger>
184
+ <TabsTrigger value="head" className="data-[state=active]:bg-[#444444] text-xs">
185
+ <Code className="w-3 h-3 mr-1" />
186
+ Head
187
+ </TabsTrigger>
188
+ <TabsTrigger value="body" className="data-[state=active]:bg-[#444444] text-xs">
189
+ <FileCode className="w-3 h-3 mr-1" />
190
+ Body
191
+ </TabsTrigger>
192
+ <TabsTrigger value="resources" className="data-[state=active]:bg-[#444444] text-xs">
193
+ <Link2 className="w-3 h-3 mr-1" />
194
+ リソース
195
+ </TabsTrigger>
196
+ </TabsList>
197
+
198
+ {/* SEO タブ */}
199
+ <TabsContent value="seo" className="flex-1 overflow-y-auto space-y-4 mt-4 pr-2">
200
+ <div className="flex items-center justify-between p-3 bg-[#1e1e1e] rounded-lg">
201
+ <div>
202
+ <Label className="text-gray-300">プロジェクト設定を継承</Label>
203
+ <p className="text-xs text-gray-500 mt-1">
204
+ オフにするとプロジェクトのデフォルト設定を使用しません
205
+ </p>
206
+ </div>
207
+ <Switch
208
+ checked={settings.inheritProjectSettings !== false}
209
+ onCheckedChange={(checked) => updateSettings({ inheritProjectSettings: checked })}
210
+ />
211
+ </div>
212
+
213
+ <div className="space-y-2">
214
+ <Label htmlFor="title" className="text-gray-300">ページタイトル</Label>
215
+ <Input
216
+ id="title"
217
+ value={settings.title || ''}
218
+ onChange={(e) => updateSettings({ title: e.target.value })}
219
+ placeholder={projectSettings?.titleTemplate?.replace('{page}', 'ページ名') || 'ページタイトル'}
220
+ className="bg-[#1e1e1e] border-[#444444] text-white"
221
+ />
222
+ {projectSettings?.titleTemplate && (
223
+ <p className="text-xs text-gray-500">
224
+ テンプレート: {projectSettings.titleTemplate}
225
+ </p>
226
+ )}
227
+ </div>
228
+
229
+ <div className="space-y-2">
230
+ <Label htmlFor="description" className="text-gray-300">説明文 (meta description)</Label>
231
+ <Textarea
232
+ id="description"
233
+ value={settings.description || ''}
234
+ onChange={(e) => updateSettings({ description: e.target.value })}
235
+ placeholder={projectSettings?.defaultDescription || 'ページの説明文(120〜160文字推奨)'}
236
+ className="bg-[#1e1e1e] border-[#444444] text-white resize-none h-20"
237
+ />
238
+ <p className="text-xs text-gray-500">
239
+ {(settings.description || '').length} / 160文字
240
+ </p>
241
+ </div>
242
+
243
+ <div className="space-y-2">
244
+ <Label htmlFor="keywords" className="text-gray-300">キーワード (カンマ区切り)</Label>
245
+ <Input
246
+ id="keywords"
247
+ value={settings.keywords || ''}
248
+ onChange={(e) => updateSettings({ keywords: e.target.value })}
249
+ placeholder="キーワード1, キーワード2, キーワード3"
250
+ className="bg-[#1e1e1e] border-[#444444] text-white"
251
+ />
252
+ </div>
253
+
254
+ <div className="space-y-2">
255
+ <Label htmlFor="canonical" className="text-gray-300">Canonical URL</Label>
256
+ <Input
257
+ id="canonical"
258
+ type="url"
259
+ value={settings.canonicalUrl || ''}
260
+ onChange={(e) => updateSettings({ canonicalUrl: e.target.value })}
261
+ placeholder="https://example.com/page"
262
+ className="bg-[#1e1e1e] border-[#444444] text-white"
263
+ />
264
+ </div>
265
+
266
+ <div className="space-y-2">
267
+ <Label htmlFor="robots" className="text-gray-300">Robots</Label>
268
+ <Select
269
+ value={settings.robots || 'index,follow'}
270
+ onValueChange={(value) => updateSettings({ robots: value })}
271
+ >
272
+ <SelectTrigger className="bg-[#1e1e1e] border-[#444444] text-white">
273
+ <SelectValue />
274
+ </SelectTrigger>
275
+ <SelectContent className="bg-[#2c2c2c] border-[#444444]">
276
+ <SelectItem value="index,follow">index, follow(デフォルト)</SelectItem>
277
+ <SelectItem value="noindex,follow">noindex, follow</SelectItem>
278
+ <SelectItem value="index,nofollow">index, nofollow</SelectItem>
279
+ <SelectItem value="noindex,nofollow">noindex, nofollow</SelectItem>
280
+ </SelectContent>
281
+ </Select>
282
+ </div>
283
+ </TabsContent>
284
+
285
+ {/* Social/OGP タブ */}
286
+ <TabsContent value="social" className="flex-1 overflow-y-auto space-y-6 mt-4 pr-2">
287
+ {/* OGP セクション */}
288
+ <div className="space-y-4">
289
+ <h3 className="text-sm font-medium text-gray-300 flex items-center gap-2">
290
+ <Share2 className="w-4 h-4" />
291
+ Open Graph (Facebook, LINE等)
292
+ </h3>
293
+
294
+ <div className="grid grid-cols-2 gap-4">
295
+ <div className="space-y-2">
296
+ <Label className="text-gray-300">og:title</Label>
297
+ <Input
298
+ value={settings.ogp?.title || ''}
299
+ onChange={(e) => updateNestedSettings('ogp', { title: e.target.value })}
300
+ placeholder={settings.title || 'ページタイトルを使用'}
301
+ className="bg-[#1e1e1e] border-[#444444] text-white"
302
+ />
303
+ </div>
304
+ <div className="space-y-2">
305
+ <Label className="text-gray-300">og:type</Label>
306
+ <Select
307
+ value={settings.ogp?.type || 'website'}
308
+ onValueChange={(value) => updateNestedSettings('ogp', { type: value })}
309
+ >
310
+ <SelectTrigger className="bg-[#1e1e1e] border-[#444444] text-white">
311
+ <SelectValue />
312
+ </SelectTrigger>
313
+ <SelectContent className="bg-[#2c2c2c] border-[#444444]">
314
+ <SelectItem value="website">website</SelectItem>
315
+ <SelectItem value="article">article</SelectItem>
316
+ <SelectItem value="product">product</SelectItem>
317
+ <SelectItem value="profile">profile</SelectItem>
318
+ </SelectContent>
319
+ </Select>
320
+ </div>
321
+ </div>
322
+
323
+ <div className="space-y-2">
324
+ <Label className="text-gray-300">og:description</Label>
325
+ <Textarea
326
+ value={settings.ogp?.description || ''}
327
+ onChange={(e) => updateNestedSettings('ogp', { description: e.target.value })}
328
+ placeholder={settings.description || '説明文を使用'}
329
+ className="bg-[#1e1e1e] border-[#444444] text-white resize-none h-16"
330
+ />
331
+ </div>
332
+
333
+ <div className="space-y-2">
334
+ <Label className="text-gray-300">og:image</Label>
335
+ <div className="flex gap-2">
336
+ <Input
337
+ value={settings.ogp?.image || ''}
338
+ onChange={(e) => updateNestedSettings('ogp', { image: e.target.value })}
339
+ placeholder="https://example.com/ogp-image.jpg"
340
+ className="bg-[#1e1e1e] border-[#444444] text-white flex-1"
341
+ />
342
+ {onUploadImage && (
343
+ <Button
344
+ variant="outline"
345
+ size="sm"
346
+ onClick={() => {
347
+ setUploadTarget('ogp');
348
+ fileInputRef.current?.click();
349
+ }}
350
+ disabled={uploadingImage}
351
+ className="border-[#444444] text-gray-300 hover:text-white"
352
+ >
353
+ <Upload className="w-4 h-4" />
354
+ </Button>
355
+ )}
356
+ </div>
357
+ {settings.ogp?.image && (
358
+ <div className="mt-2 relative w-full h-32 bg-[#1e1e1e] rounded overflow-hidden">
359
+ <img
360
+ src={settings.ogp.image}
361
+ alt="OGP Preview"
362
+ className="w-full h-full object-contain"
363
+ />
364
+ </div>
365
+ )}
366
+ <p className="text-xs text-gray-500">推奨サイズ: 1200 x 630 px</p>
367
+ </div>
368
+
369
+ <div className="grid grid-cols-2 gap-4">
370
+ <div className="space-y-2">
371
+ <Label className="text-gray-300">og:url</Label>
372
+ <Input
373
+ value={settings.ogp?.url || ''}
374
+ onChange={(e) => updateNestedSettings('ogp', { url: e.target.value })}
375
+ placeholder="https://example.com/page"
376
+ className="bg-[#1e1e1e] border-[#444444] text-white"
377
+ />
378
+ </div>
379
+ <div className="space-y-2">
380
+ <Label className="text-gray-300">og:site_name</Label>
381
+ <Input
382
+ value={settings.ogp?.siteName || ''}
383
+ onChange={(e) => updateNestedSettings('ogp', { siteName: e.target.value })}
384
+ placeholder={projectSettings?.ogp?.siteName || 'サイト名'}
385
+ className="bg-[#1e1e1e] border-[#444444] text-white"
386
+ />
387
+ </div>
388
+ </div>
389
+ </div>
390
+
391
+ <div className="border-t border-[#444444]" />
392
+
393
+ {/* Twitter Card セクション */}
394
+ <div className="space-y-4">
395
+ <h3 className="text-sm font-medium text-gray-300 flex items-center gap-2">
396
+ <svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
397
+ <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
398
+ </svg>
399
+ Twitter Card
400
+ </h3>
401
+
402
+ <div className="grid grid-cols-2 gap-4">
403
+ <div className="space-y-2">
404
+ <Label className="text-gray-300">twitter:card</Label>
405
+ <Select
406
+ value={settings.twitter?.card || 'summary_large_image'}
407
+ onValueChange={(value) => updateNestedSettings('twitter', { card: value as 'summary' | 'summary_large_image' })}
408
+ >
409
+ <SelectTrigger className="bg-[#1e1e1e] border-[#444444] text-white">
410
+ <SelectValue />
411
+ </SelectTrigger>
412
+ <SelectContent className="bg-[#2c2c2c] border-[#444444]">
413
+ <SelectItem value="summary">summary</SelectItem>
414
+ <SelectItem value="summary_large_image">summary_large_image</SelectItem>
415
+ </SelectContent>
416
+ </Select>
417
+ </div>
418
+ <div className="space-y-2">
419
+ <Label className="text-gray-300">twitter:site</Label>
420
+ <Input
421
+ value={settings.twitter?.site || ''}
422
+ onChange={(e) => updateNestedSettings('twitter', { site: e.target.value })}
423
+ placeholder="@username"
424
+ className="bg-[#1e1e1e] border-[#444444] text-white"
425
+ />
426
+ </div>
427
+ </div>
428
+
429
+ <div className="space-y-2">
430
+ <Label className="text-gray-300">twitter:title(空欄でog:titleを使用)</Label>
431
+ <Input
432
+ value={settings.twitter?.title || ''}
433
+ onChange={(e) => updateNestedSettings('twitter', { title: e.target.value })}
434
+ placeholder="og:titleと同じ場合は空欄"
435
+ className="bg-[#1e1e1e] border-[#444444] text-white"
436
+ />
437
+ </div>
438
+
439
+ <div className="space-y-2">
440
+ <Label className="text-gray-300">twitter:image(空欄でog:imageを使用)</Label>
441
+ <div className="flex gap-2">
442
+ <Input
443
+ value={settings.twitter?.image || ''}
444
+ onChange={(e) => updateNestedSettings('twitter', { image: e.target.value })}
445
+ placeholder="og:imageと同じ場合は空欄"
446
+ className="bg-[#1e1e1e] border-[#444444] text-white flex-1"
447
+ />
448
+ {onUploadImage && (
449
+ <Button
450
+ variant="outline"
451
+ size="sm"
452
+ onClick={() => {
453
+ setUploadTarget('twitter');
454
+ fileInputRef.current?.click();
455
+ }}
456
+ disabled={uploadingImage}
457
+ className="border-[#444444] text-gray-300 hover:text-white"
458
+ >
459
+ <Upload className="w-4 h-4" />
460
+ </Button>
461
+ )}
462
+ </div>
463
+ </div>
464
+ </div>
465
+ </TabsContent>
466
+
467
+ {/* Head タブ */}
468
+ <TabsContent value="head" className="flex-1 overflow-y-auto space-y-4 mt-4 pr-2">
469
+ <div className="p-3 bg-[#0d99ff]/10 border border-[#0d99ff]/30 rounded-md text-xs text-[#7cc4ff]">
470
+ <AlertCircle className="w-4 h-4 inline mr-2" />
471
+ &lt;head&gt;タグ内に追加するHTMLを記述できます。
472
+ Analytics、Webフォント、その他のメタタグなどに使用してください。
473
+ </div>
474
+
475
+ <div className="space-y-2">
476
+ <Label className="text-gray-300">カスタム &lt;head&gt; HTML</Label>
477
+ <Textarea
478
+ value={settings.customHeadHtml || ''}
479
+ onChange={(e) => updateSettings({ customHeadHtml: e.target.value })}
480
+ placeholder={`<!-- Google Analytics -->
481
+ <script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
482
+ <script>
483
+ window.dataLayer = window.dataLayer || [];
484
+ function gtag(){dataLayer.push(arguments);}
485
+ gtag('js', new Date());
486
+ gtag('config', 'GA_MEASUREMENT_ID');
487
+ </script>
488
+
489
+ <!-- Google Fonts -->
490
+ <link rel="preconnect" href="https://fonts.googleapis.com">
491
+ <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP&display=swap" rel="stylesheet">`}
492
+ className="bg-[#1e1e1e] border-[#444444] text-white font-mono text-sm resize-none h-64"
493
+ />
494
+ </div>
495
+
496
+ {projectSettings?.commonHeadHtml && (
497
+ <div className="space-y-2 opacity-60">
498
+ <Label className="text-gray-400">プロジェクト共通 &lt;head&gt;(継承時に適用)</Label>
499
+ <pre className="bg-[#1e1e1e] border-[#444444] p-3 rounded text-xs text-gray-400 overflow-x-auto">
500
+ {projectSettings.commonHeadHtml}
501
+ </pre>
502
+ </div>
503
+ )}
504
+ </TabsContent>
505
+
506
+ {/* Body タブ */}
507
+ <TabsContent value="body" className="flex-1 overflow-y-auto space-y-4 mt-4 pr-2">
508
+ <div className="space-y-2">
509
+ <Label className="text-gray-300">&lt;body&gt; 開始直後に挿入</Label>
510
+ <Textarea
511
+ value={settings.bodyStartHtml || ''}
512
+ onChange={(e) => updateSettings({ bodyStartHtml: e.target.value })}
513
+ placeholder={`<!-- Google Tag Manager (noscript) -->
514
+ <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXX"
515
+ height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
516
+ <!-- End Google Tag Manager (noscript) -->`}
517
+ className="bg-[#1e1e1e] border-[#444444] text-white font-mono text-sm resize-none h-32"
518
+ />
519
+ <p className="text-xs text-gray-500">
520
+ GTMのnoscriptタグ、ローディングインジケーターなどに使用
521
+ </p>
522
+ </div>
523
+
524
+ <div className="space-y-2">
525
+ <Label className="text-gray-300">&lt;/body&gt; 直前に挿入</Label>
526
+ <Textarea
527
+ value={settings.bodyEndHtml || ''}
528
+ onChange={(e) => updateSettings({ bodyEndHtml: e.target.value })}
529
+ placeholder={`<!-- Chat Widget -->
530
+ <script src="https://example.com/chat-widget.js"></script>
531
+
532
+ <!-- Custom tracking -->
533
+ <script>
534
+ // Page view tracking
535
+ console.log('Page loaded');
536
+ </script>`}
537
+ className="bg-[#1e1e1e] border-[#444444] text-white font-mono text-sm resize-none h-32"
538
+ />
539
+ <p className="text-xs text-gray-500">
540
+ チャットウィジェット、追加スクリプトなどに使用
541
+ </p>
542
+ </div>
543
+
544
+ <div className="border-t border-[#444444] pt-4" />
545
+
546
+ {/* カスタムCSS */}
547
+ <div className="space-y-2">
548
+ <div className="flex items-center justify-between">
549
+ <Label className="text-gray-300">カスタム CSS</Label>
550
+ {settings.customCss && (
551
+ <span className="text-xs text-gray-500">
552
+ {settings.customCss.length} 文字
553
+ </span>
554
+ )}
555
+ </div>
556
+ <Textarea
557
+ value={settings.customCss || ''}
558
+ onChange={(e) => updateSettings({ customCss: e.target.value })}
559
+ placeholder={`.custom-class {
560
+ color: #333;
561
+ font-size: 16px;
562
+ }
563
+
564
+ /* インポートされたCSSもここに統合されます */`}
565
+ className="bg-[#1e1e1e] border-[#444444] text-white font-mono text-sm resize-none h-40"
566
+ />
567
+ </div>
568
+
569
+ {/* カスタムJS */}
570
+ <div className="space-y-2">
571
+ <div className="flex items-center justify-between">
572
+ <Label className="text-gray-300">カスタム JavaScript</Label>
573
+ {settings.customJs && (
574
+ <span className="text-xs text-gray-500">
575
+ {settings.customJs.length} 文字
576
+ </span>
577
+ )}
578
+ </div>
579
+ <div className="text-xs text-yellow-400 flex items-center gap-2 p-2 bg-yellow-500/10 rounded border border-yellow-500/30 mb-2">
580
+ <AlertCircle className="w-4 h-4 flex-shrink-0" />
581
+ <span>
582
+ JavaScriptはプレビュー時に実行されます。信頼できるソースからのみ使用してください。
583
+ </span>
584
+ </div>
585
+ <Textarea
586
+ value={settings.customJs || ''}
587
+ onChange={(e) => updateSettings({ customJs: e.target.value })}
588
+ placeholder={`// DOMContentLoaded後に実行
589
+ document.addEventListener('DOMContentLoaded', function() {
590
+ console.log('Page ready');
591
+ });
592
+
593
+ /* インポートされたJSもここに統合されます */`}
594
+ className="bg-[#1e1e1e] border-[#444444] text-white font-mono text-sm resize-none h-40"
595
+ />
596
+ </div>
597
+ </TabsContent>
598
+
599
+ {/* Resources タブ */}
600
+ <TabsContent value="resources" className="flex-1 overflow-y-auto space-y-6 mt-4 pr-2">
601
+ {/* 外部CSS */}
602
+ <div className="space-y-3">
603
+ <div className="flex items-center justify-between">
604
+ <h3 className="text-sm font-medium text-gray-300">外部 CSS</h3>
605
+ <Button
606
+ variant="ghost"
607
+ size="sm"
608
+ onClick={() => addExternalResource('css')}
609
+ className="text-gray-400 hover:text-white"
610
+ >
611
+ <Plus className="w-4 h-4 mr-1" />
612
+ 追加
613
+ </Button>
614
+ </div>
615
+
616
+ {(settings.externalCss || []).length === 0 ? (
617
+ <p className="text-xs text-gray-500 p-3 bg-[#1e1e1e] rounded">
618
+ 外部CSSファイルはありません
619
+ </p>
620
+ ) : (
621
+ <div className="space-y-2">
622
+ {(settings.externalCss || []).map((resource) => (
623
+ <div key={resource.id} className="flex items-center gap-2 p-2 bg-[#1e1e1e] rounded">
624
+ <Link2 className="w-4 h-4 text-gray-500 flex-shrink-0" />
625
+ <Input
626
+ value={resource.url}
627
+ onChange={(e) => updateExternalResource('css', resource.id, { url: e.target.value })}
628
+ placeholder="https://cdn.example.com/style.css"
629
+ className="bg-transparent border-none text-white flex-1 h-8"
630
+ />
631
+ <Button
632
+ variant="ghost"
633
+ size="sm"
634
+ onClick={() => removeExternalResource('css', resource.id)}
635
+ className="text-red-400 hover:text-red-300 h-8 w-8 p-0"
636
+ >
637
+ <Trash2 className="w-4 h-4" />
638
+ </Button>
639
+ </div>
640
+ ))}
641
+ </div>
642
+ )}
643
+ </div>
644
+
645
+ <div className="border-t border-[#444444]" />
646
+
647
+ {/* 外部JS */}
648
+ <div className="space-y-3">
649
+ <div className="flex items-center justify-between">
650
+ <h3 className="text-sm font-medium text-gray-300">外部 JavaScript</h3>
651
+ <Button
652
+ variant="ghost"
653
+ size="sm"
654
+ onClick={() => addExternalResource('js')}
655
+ className="text-gray-400 hover:text-white"
656
+ >
657
+ <Plus className="w-4 h-4 mr-1" />
658
+ 追加
659
+ </Button>
660
+ </div>
661
+
662
+ {(settings.externalJs || []).length === 0 ? (
663
+ <p className="text-xs text-gray-500 p-3 bg-[#1e1e1e] rounded">
664
+ 外部JavaScriptファイルはありません
665
+ </p>
666
+ ) : (
667
+ <div className="space-y-2">
668
+ {(settings.externalJs || []).map((resource) => (
669
+ <div key={resource.id} className="p-3 bg-[#1e1e1e] rounded space-y-2">
670
+ <div className="flex items-center gap-2">
671
+ <FileCode className="w-4 h-4 text-gray-500 flex-shrink-0" />
672
+ <Input
673
+ value={resource.url}
674
+ onChange={(e) => updateExternalResource('js', resource.id, { url: e.target.value })}
675
+ placeholder="https://cdn.example.com/script.js"
676
+ className="bg-transparent border-none text-white flex-1 h-8"
677
+ />
678
+ <Button
679
+ variant="ghost"
680
+ size="sm"
681
+ onClick={() => removeExternalResource('js', resource.id)}
682
+ className="text-red-400 hover:text-red-300 h-8 w-8 p-0"
683
+ >
684
+ <Trash2 className="w-4 h-4" />
685
+ </Button>
686
+ </div>
687
+ <div className="flex items-center gap-4 pl-6 text-xs">
688
+ <Select
689
+ value={resource.position || 'body-end'}
690
+ onValueChange={(value) => updateExternalResource('js', resource.id, { position: value as ExternalResource['position'] })}
691
+ >
692
+ <SelectTrigger className="bg-[#2c2c2c] border-[#444444] text-white h-7 w-32">
693
+ <SelectValue />
694
+ </SelectTrigger>
695
+ <SelectContent className="bg-[#2c2c2c] border-[#444444]">
696
+ <SelectItem value="head">head内</SelectItem>
697
+ <SelectItem value="body-start">body開始直後</SelectItem>
698
+ <SelectItem value="body-end">body終了直前</SelectItem>
699
+ </SelectContent>
700
+ </Select>
701
+ <label className="flex items-center gap-1 text-gray-400">
702
+ <input
703
+ type="checkbox"
704
+ checked={resource.async || false}
705
+ onChange={(e) => updateExternalResource('js', resource.id, { async: e.target.checked })}
706
+ className="rounded bg-[#2c2c2c] border-[#444444]"
707
+ />
708
+ async
709
+ </label>
710
+ <label className="flex items-center gap-1 text-gray-400">
711
+ <input
712
+ type="checkbox"
713
+ checked={resource.defer || false}
714
+ onChange={(e) => updateExternalResource('js', resource.id, { defer: e.target.checked })}
715
+ className="rounded bg-[#2c2c2c] border-[#444444]"
716
+ />
717
+ defer
718
+ </label>
719
+ </div>
720
+ </div>
721
+ ))}
722
+ </div>
723
+ )}
724
+ </div>
725
+
726
+ {/* プロジェクト共通リソース表示 */}
727
+ {(projectSettings?.externalCss?.length || projectSettings?.externalJs?.length) && settings.inheritProjectSettings !== false && (
728
+ <>
729
+ <div className="border-t border-[#444444]" />
730
+ <div className="space-y-3 opacity-60">
731
+ <h3 className="text-sm font-medium text-gray-400">プロジェクト共通リソース(継承)</h3>
732
+ {projectSettings?.externalCss?.map((css) => (
733
+ <div key={css.id} className="flex items-center gap-2 p-2 bg-[#1e1e1e] rounded text-gray-400">
734
+ <Link2 className="w-4 h-4" />
735
+ <span className="text-xs truncate">{css.url}</span>
736
+ </div>
737
+ ))}
738
+ {projectSettings?.externalJs?.map((js) => (
739
+ <div key={js.id} className="flex items-center gap-2 p-2 bg-[#1e1e1e] rounded text-gray-400">
740
+ <FileCode className="w-4 h-4" />
741
+ <span className="text-xs truncate">{js.url}</span>
742
+ </div>
743
+ ))}
744
+ </div>
745
+ </>
746
+ )}
747
+ </TabsContent>
748
+ </Tabs>
749
+
750
+ <DialogFooter className="flex items-center justify-between sm:justify-between gap-2 flex-shrink-0 px-6 py-4 border-t border-[#444444]">
751
+ <div className="text-xs text-gray-500">
752
+ {hasChanges && '変更があります'}
753
+ </div>
754
+ <div className="flex items-center gap-2">
755
+ <Button
756
+ variant="ghost"
757
+ onClick={onClose}
758
+ className="text-gray-400 hover:text-white"
759
+ >
760
+ キャンセル
761
+ </Button>
762
+ <Button
763
+ onClick={handleSave}
764
+ disabled={!hasChanges}
765
+ className="bg-[#0d99ff] hover:bg-[#0c8ce9]"
766
+ >
767
+ 保存
768
+ </Button>
769
+ </div>
770
+ </DialogFooter>
771
+ </DialogContent>
772
+ </Dialog>
773
+ );
774
+ }
775
+
776
+ export default PageSettingsDialog;