@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,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
|
+
<head>タグ内に追加するHTMLを記述できます。
|
|
472
|
+
Analytics、Webフォント、その他のメタタグなどに使用してください。
|
|
473
|
+
</div>
|
|
474
|
+
|
|
475
|
+
<div className="space-y-2">
|
|
476
|
+
<Label className="text-gray-300">カスタム <head> 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">プロジェクト共通 <head>(継承時に適用)</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"><body> 開始直後に挿入</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"></body> 直前に挿入</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;
|