@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,275 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* デザイン提案(PowerPointの「デザイナー」相当)のパネル。
|
|
5
|
+
*
|
|
6
|
+
* 「いまのスライドを、別の見せ方にするとどうなるか」を画像で数案出し、
|
|
7
|
+
* 気に入った1案を選ぶとスライドの実装(TSX)がその意匠で書き直される。
|
|
8
|
+
*
|
|
9
|
+
* サーバー側は vite-plugin-design-proposals.ts。生成も適用もAI CLIを回すので
|
|
10
|
+
* 数分かかる。押しっぱなしで待たせないよう、状態と経過をここに出し続ける。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
14
|
+
import { readApiJson } from '../../utils/api-json';
|
|
15
|
+
import { createPortal } from 'react-dom';
|
|
16
|
+
import { X, Sparkles, Loader2, Check, AlertTriangle } from 'lucide-react';
|
|
17
|
+
import { useEditorContext } from '../../EditorContext';
|
|
18
|
+
|
|
19
|
+
type Pattern = { url: string; concept: string };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* リボンと同じ配色。PptChrome から値で受け取る。
|
|
23
|
+
* import で取りに行くと PptChrome ↔ このファイルが循環参照になるため。
|
|
24
|
+
*/
|
|
25
|
+
export type PptPalette = {
|
|
26
|
+
chrome: string; text: string; sub: string; border: string;
|
|
27
|
+
hover: string; activeBg: string; control: string; rail: string;
|
|
28
|
+
canvas: string; disabled: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const COUNTS = [1, 2, 3, 4];
|
|
32
|
+
|
|
33
|
+
export function PptDesignProposals({
|
|
34
|
+
page, pal, onClose,
|
|
35
|
+
}: {
|
|
36
|
+
page: number;
|
|
37
|
+
pal: PptPalette;
|
|
38
|
+
onClose: () => void;
|
|
39
|
+
}) {
|
|
40
|
+
const { hasChanges } = useEditorContext();
|
|
41
|
+
|
|
42
|
+
const [instruction, setInstruction] = useState('');
|
|
43
|
+
const [count, setCount] = useState(3);
|
|
44
|
+
/** 生成 */
|
|
45
|
+
const [genState, setGenState] = useState<'idle' | 'running' | 'done' | 'error'>('idle');
|
|
46
|
+
const [genMessage, setGenMessage] = useState('');
|
|
47
|
+
const [patterns, setPatterns] = useState<Pattern[]>([]);
|
|
48
|
+
/** 適用 */
|
|
49
|
+
const [applyingUrl, setApplyingUrl] = useState<string | null>(null);
|
|
50
|
+
const [applyMessage, setApplyMessage] = useState('');
|
|
51
|
+
const [error, setError] = useState('');
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* アンマウント後にポーリングが状態を触らないようにする。
|
|
55
|
+
* StrictModeでは mount → cleanup → mount と2度走るので、
|
|
56
|
+
* 立ち上げ直しのたびに true へ戻す(戻さないとポーリングが死んだままになる)。
|
|
57
|
+
*/
|
|
58
|
+
const aliveRef = useRef(true);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
aliveRef.current = true;
|
|
61
|
+
return () => { aliveRef.current = false; };
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
|
|
66
|
+
document.addEventListener('keydown', onKey);
|
|
67
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
68
|
+
}, [onClose]);
|
|
69
|
+
|
|
70
|
+
/** ジョブが片付くまで status を叩き続ける */
|
|
71
|
+
const poll = useCallback(
|
|
72
|
+
async <T,>(url: string, onTick: (v: T & { state: string; message?: string }) => void) => {
|
|
73
|
+
for (;;) {
|
|
74
|
+
if (!aliveRef.current) return null;
|
|
75
|
+
await new Promise((r) => setTimeout(r, 2500));
|
|
76
|
+
const res = await fetch(url).catch(() => null);
|
|
77
|
+
if (!res || !res.ok) continue;
|
|
78
|
+
const v = (await readApiJson<T & { state: string; message?: string; error?: string }>(res));
|
|
79
|
+
onTick(v);
|
|
80
|
+
if (v.state !== 'running') return v;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
[],
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const generate = useCallback(async () => {
|
|
87
|
+
setError('');
|
|
88
|
+
setPatterns([]);
|
|
89
|
+
setGenState('running');
|
|
90
|
+
setGenMessage('ジョブを開始しています…');
|
|
91
|
+
const res = await fetch('/__design-gen', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'Content-Type': 'application/json' },
|
|
94
|
+
body: JSON.stringify({ page, instruction, count }),
|
|
95
|
+
}).catch(() => null);
|
|
96
|
+
const started = res && res.ok ? await readApiJson<{ jobId: string }>(res) : null;
|
|
97
|
+
if (!started?.jobId) {
|
|
98
|
+
const msg = res ? ((await res.json().catch(() => ({}))) as { error?: string }).error : null;
|
|
99
|
+
setGenState('error');
|
|
100
|
+
setError(msg || '生成を開始できませんでした');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const final = await poll<{ patterns: Pattern[]; error?: string }>(
|
|
104
|
+
`/__design-gen/status/${started.jobId}`,
|
|
105
|
+
(v) => setGenMessage(v.message || ''),
|
|
106
|
+
);
|
|
107
|
+
if (!final || !aliveRef.current) return;
|
|
108
|
+
if (final.state === 'error') {
|
|
109
|
+
setGenState('error');
|
|
110
|
+
setError(final.error || '生成に失敗しました');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
setPatterns(final.patterns ?? []);
|
|
114
|
+
setGenState('done');
|
|
115
|
+
}, [page, instruction, count, poll]);
|
|
116
|
+
|
|
117
|
+
const apply = useCallback(
|
|
118
|
+
async (url: string) => {
|
|
119
|
+
setError('');
|
|
120
|
+
setApplyingUrl(url);
|
|
121
|
+
setApplyMessage('ジョブを開始しています…');
|
|
122
|
+
const res = await fetch('/__design-apply', {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'Content-Type': 'application/json' },
|
|
125
|
+
body: JSON.stringify({ page, image: url, note: instruction }),
|
|
126
|
+
}).catch(() => null);
|
|
127
|
+
const started = res && res.ok ? await readApiJson<{ jobId: string }>(res) : null;
|
|
128
|
+
if (!started?.jobId) {
|
|
129
|
+
const msg = res ? ((await res.json().catch(() => ({}))) as { error?: string }).error : null;
|
|
130
|
+
setApplyingUrl(null);
|
|
131
|
+
setError(msg || '適用を開始できませんでした');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const final = await poll<{ error?: string; warning?: string }>(
|
|
135
|
+
`/__design-apply/status/${started.jobId}`,
|
|
136
|
+
(v) => setApplyMessage(v.message || ''),
|
|
137
|
+
);
|
|
138
|
+
if (!final || !aliveRef.current) return;
|
|
139
|
+
if (final.state === 'error') {
|
|
140
|
+
setApplyingUrl(null);
|
|
141
|
+
setError(final.error || '適用に失敗しました');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// 文言が変わっているときは、読み込み直す前に必ず知らせる(黙って通さない)
|
|
145
|
+
if (final.warning && !window.confirm(`${final.warning}\n\nこのまま反映しますか?`)) {
|
|
146
|
+
setApplyingUrl(null);
|
|
147
|
+
setApplyMessage('適用済みです。読み込み直すと反映されます');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// 書き直したTSXを反映するには読み込み直しが要る。
|
|
151
|
+
// 未保存の編集がある場合だけ確認する(黙って捨てない)
|
|
152
|
+
if (hasChanges && !window.confirm('保存していない編集があります。破棄して新しいデザインを読み込みますか?')) {
|
|
153
|
+
setApplyingUrl(null);
|
|
154
|
+
setApplyMessage('適用済みです。読み込み直すと反映されます');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
location.reload();
|
|
158
|
+
},
|
|
159
|
+
[page, instruction, poll, hasChanges],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const busy = genState === 'running' || !!applyingUrl;
|
|
163
|
+
|
|
164
|
+
return createPortal(
|
|
165
|
+
<div
|
|
166
|
+
className="fixed inset-0 z-[10001] flex items-center justify-center p-6"
|
|
167
|
+
style={{ backgroundColor: 'rgba(0,0,0,0.45)' }}
|
|
168
|
+
onMouseDown={(e) => { if (e.target === e.currentTarget && !busy) onClose(); }}
|
|
169
|
+
>
|
|
170
|
+
<div
|
|
171
|
+
data-design-proposals="1"
|
|
172
|
+
className="flex max-h-full w-[min(1100px,95vw)] flex-col overflow-hidden rounded-lg border shadow-2xl"
|
|
173
|
+
style={{ backgroundColor: pal.chrome, borderColor: pal.border, color: pal.text }}
|
|
174
|
+
>
|
|
175
|
+
{/* 見出し */}
|
|
176
|
+
<div className="flex items-center gap-2 border-b px-4 py-3" style={{ borderColor: pal.border }}>
|
|
177
|
+
<Sparkles className="h-4 w-4" />
|
|
178
|
+
<span className="text-[13px] font-semibold">デザイン提案</span>
|
|
179
|
+
<span className="text-[11px]" style={{ color: pal.sub }}>
|
|
180
|
+
{page}枚目のスライドを、内容はそのままに別の見せ方で提案します
|
|
181
|
+
</span>
|
|
182
|
+
<button
|
|
183
|
+
onClick={onClose}
|
|
184
|
+
disabled={busy}
|
|
185
|
+
title={busy ? '処理中は閉じられません' : '閉じる'}
|
|
186
|
+
className="ml-auto flex h-6 w-6 items-center justify-center rounded"
|
|
187
|
+
style={{ color: busy ? pal.disabled : pal.text }}
|
|
188
|
+
>
|
|
189
|
+
<X className="h-4 w-4" />
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{/* 生成の指示 */}
|
|
194
|
+
<div className="flex items-end gap-2 border-b px-4 py-3" style={{ borderColor: pal.border }}>
|
|
195
|
+
<label className="flex-1 text-[11px]" style={{ color: pal.sub }}>
|
|
196
|
+
指示(任意)
|
|
197
|
+
<textarea
|
|
198
|
+
value={instruction}
|
|
199
|
+
onChange={(e) => setInstruction(e.target.value)}
|
|
200
|
+
rows={2}
|
|
201
|
+
placeholder="例: 左右2分割はやめて、図版を大きく使った構図で"
|
|
202
|
+
className="mt-1 w-full resize-none rounded border px-2 py-1.5 text-[12px] outline-none"
|
|
203
|
+
style={{ backgroundColor: pal.control, borderColor: pal.border, color: pal.text }}
|
|
204
|
+
/>
|
|
205
|
+
</label>
|
|
206
|
+
<label className="text-[11px]" style={{ color: pal.sub }}>
|
|
207
|
+
案の数
|
|
208
|
+
<select
|
|
209
|
+
value={count}
|
|
210
|
+
onChange={(e) => setCount(Number(e.target.value))}
|
|
211
|
+
className="mt-1 block h-[30px] rounded border px-2 text-[12px] outline-none"
|
|
212
|
+
style={{ backgroundColor: pal.control, borderColor: pal.border, color: pal.text }}
|
|
213
|
+
>
|
|
214
|
+
{COUNTS.map((n) => <option key={n} value={n}>{n}</option>)}
|
|
215
|
+
</select>
|
|
216
|
+
</label>
|
|
217
|
+
<button
|
|
218
|
+
onClick={() => void generate()}
|
|
219
|
+
disabled={busy}
|
|
220
|
+
className="flex h-[30px] items-center gap-1.5 rounded px-3 text-[12px] font-medium"
|
|
221
|
+
style={{ backgroundColor: busy ? pal.disabled : '#ED6C47', color: '#fff' }}
|
|
222
|
+
>
|
|
223
|
+
{genState === 'running' ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Sparkles className="h-3.5 w-3.5" />}
|
|
224
|
+
生成
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
{/* 経過・エラー */}
|
|
229
|
+
{(genState === 'running' || applyingUrl || error) && (
|
|
230
|
+
<div className="flex items-center gap-2 px-4 py-2 text-[12px]" style={{ color: error ? '#d64426' : pal.sub }}>
|
|
231
|
+
{error ? <AlertTriangle className="h-3.5 w-3.5 shrink-0" /> : <Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin" />}
|
|
232
|
+
<span className="whitespace-pre-wrap">{error || applyMessage || genMessage}</span>
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
|
|
236
|
+
{/* ギャラリー */}
|
|
237
|
+
<div className="flex-1 overflow-y-auto px-4 py-3">
|
|
238
|
+
{patterns.length === 0 && genState !== 'running' && (
|
|
239
|
+
<div className="py-10 text-center text-[12px]" style={{ color: pal.sub }}>
|
|
240
|
+
「生成」を押すと、このスライドのデザイン案を作ります(数分かかります)
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
<div className="grid grid-cols-2 gap-3">
|
|
244
|
+
{patterns.map((p) => (
|
|
245
|
+
<div key={p.url} className="rounded border" style={{ borderColor: pal.border, backgroundColor: pal.control }}>
|
|
246
|
+
<img src={p.url} alt={p.concept} className="block w-full" style={{ aspectRatio: '16 / 9', objectFit: 'contain' }} />
|
|
247
|
+
<div className="flex items-start gap-2 px-2 py-2">
|
|
248
|
+
<span className="flex-1 text-[11px] leading-snug" style={{ color: pal.sub }}>{p.concept}</span>
|
|
249
|
+
<button
|
|
250
|
+
onClick={() => void apply(p.url)}
|
|
251
|
+
disabled={busy}
|
|
252
|
+
className="flex shrink-0 items-center gap-1 rounded border px-2 py-1 text-[11px]"
|
|
253
|
+
style={{
|
|
254
|
+
borderColor: pal.border,
|
|
255
|
+
color: busy ? pal.disabled : pal.text,
|
|
256
|
+
backgroundColor: applyingUrl === p.url ? pal.activeBg : undefined,
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
{applyingUrl === p.url ? <Loader2 className="h-3 w-3 animate-spin" /> : <Check className="h-3 w-3" />}
|
|
260
|
+
この案で適用
|
|
261
|
+
</button>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
))}
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<div className="border-t px-4 py-2 text-[11px]" style={{ borderColor: pal.border, color: pal.sub }}>
|
|
269
|
+
適用するとスライドの実装(TSX)が書き直されます。描画できない結果になった場合は自動で元に戻します。
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
</div>,
|
|
273
|
+
document.body,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 右ペイン「図形/図の書式設定」。実機の書式ウィンドウと同じ構成で
|
|
5
|
+
* 影・反射・光彩・ぼかし(ソフトエッジ)をスライダーで調整する。
|
|
6
|
+
*
|
|
7
|
+
* 値は要素の data-gg-fx にJSONで持たせ、そこからCSSを組み立てて
|
|
8
|
+
* inline style に反映する(box-shadow/-webkit-box-reflect/mask-image)。
|
|
9
|
+
* CSSが正で、data-gg-fx は「スライダーの位置を思い出すためのメモ」。
|
|
10
|
+
* 保存時に data-gg-* は原本TSXへは書かれないので、TSXには効果のCSSだけが残る。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
14
|
+
import { ChevronDown, ChevronRight, X } from 'lucide-react';
|
|
15
|
+
import { useEditorContext } from '../../EditorContext';
|
|
16
|
+
import { PPT_PALETTES, type PptTheme } from './PptChrome';
|
|
17
|
+
import { applyShadowAndGlow, getFxHost, unwrapFxHostIfEmpty } from '../../utils/element-effects';
|
|
18
|
+
|
|
19
|
+
const FX_ATTR = 'data-gg-fx';
|
|
20
|
+
|
|
21
|
+
type Shadow = { on: boolean; color: string; opacity: number; size: number; blur: number; angle: number; distance: number };
|
|
22
|
+
type Reflection = { on: boolean; opacity: number; size: number; blur: number; distance: number };
|
|
23
|
+
type Glow = { on: boolean; color: string; size: number; opacity: number };
|
|
24
|
+
type SoftEdge = { on: boolean; size: number };
|
|
25
|
+
type Fx = { shadow: Shadow; reflection: Reflection; glow: Glow; softEdge: SoftEdge };
|
|
26
|
+
|
|
27
|
+
const DEFAULT_FX: Fx = {
|
|
28
|
+
shadow: { on: false, color: '#000000', opacity: 60, size: 0, blur: 12, angle: 90, distance: 8 },
|
|
29
|
+
reflection: { on: false, opacity: 50, size: 35, blur: 2, distance: 4 },
|
|
30
|
+
glow: { on: false, color: '#0d99ff', size: 12, opacity: 60 },
|
|
31
|
+
softEdge: { on: false, size: 10 },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const SHADOW_PRESETS: { label: string; value: Partial<Shadow> | null }[] = [
|
|
35
|
+
{ label: 'なし', value: null },
|
|
36
|
+
{ label: '右下(小)', value: { on: true, angle: 45, distance: 6, blur: 8, size: 0, opacity: 45 } },
|
|
37
|
+
{ label: '右下(中)', value: { on: true, angle: 45, distance: 12, blur: 18, size: 0, opacity: 50 } },
|
|
38
|
+
{ label: '真下(大)', value: { on: true, angle: 90, distance: 18, blur: 30, size: 0, opacity: 55 } },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function hexToRgba(hex: string, alphaPct: number): string {
|
|
42
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
43
|
+
const a = Math.max(0, Math.min(1, alphaPct / 100));
|
|
44
|
+
if (!m) return `rgba(0,0,0,${a})`;
|
|
45
|
+
const n = parseInt(m[1], 16);
|
|
46
|
+
return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${a})`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 効果を要素へ反映する。
|
|
51
|
+
* 影と光彩は element-effects に任せる(切り抜き図形や画像は drop-shadow になる)。
|
|
52
|
+
*/
|
|
53
|
+
function applyFx(el: HTMLElement, fx: Fx): void {
|
|
54
|
+
const rad = (fx.shadow.angle * Math.PI) / 180;
|
|
55
|
+
// 影・光彩は要素(切り抜き図形なら枠)に載る。返ってきた要素へ反射とぼかしも載せる
|
|
56
|
+
const host = applyShadowAndGlow(
|
|
57
|
+
el,
|
|
58
|
+
fx.shadow.on
|
|
59
|
+
? {
|
|
60
|
+
dx: Math.round(Math.cos(rad) * fx.shadow.distance * 10) / 10,
|
|
61
|
+
dy: Math.round(Math.sin(rad) * fx.shadow.distance * 10) / 10,
|
|
62
|
+
blur: fx.shadow.blur,
|
|
63
|
+
size: fx.shadow.size,
|
|
64
|
+
color: hexToRgba(fx.shadow.color, fx.shadow.opacity),
|
|
65
|
+
}
|
|
66
|
+
: null,
|
|
67
|
+
fx.glow.on ? { size: fx.glow.size, color: hexToRgba(fx.glow.color, fx.glow.opacity) } : null,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// 反射・ぼかしも枠に載せる(図形本体に載せると切り抜きで消えるため)
|
|
71
|
+
const target = fx.reflection.on || fx.softEdge.on ? getFxHost(el, true) : host;
|
|
72
|
+
target.style.setProperty(
|
|
73
|
+
'-webkit-box-reflect',
|
|
74
|
+
fx.reflection.on
|
|
75
|
+
? `below ${fx.reflection.distance}px linear-gradient(transparent ${100 - fx.reflection.size}%, rgba(255,255,255,${fx.reflection.opacity / 100}))`
|
|
76
|
+
: '',
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const mask = fx.softEdge.on
|
|
80
|
+
? `radial-gradient(closest-side, #000 ${Math.max(0, 100 - fx.softEdge.size * 2)}%, transparent 100%)`
|
|
81
|
+
: '';
|
|
82
|
+
target.style.setProperty('-webkit-mask-image', mask);
|
|
83
|
+
target.style.maskImage = mask;
|
|
84
|
+
unwrapFxHostIfEmpty(target);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readFx(el: HTMLElement | null): Fx {
|
|
88
|
+
if (!el) return DEFAULT_FX;
|
|
89
|
+
const raw = el.getAttribute(FX_ATTR);
|
|
90
|
+
if (!raw) {
|
|
91
|
+
// 既に box-shadow だけ付いている要素は「影あり」として拾う
|
|
92
|
+
const has =
|
|
93
|
+
(!!el.style.boxShadow && el.style.boxShadow !== 'none') ||
|
|
94
|
+
/drop-shadow\(/.test(el.style.filter || '');
|
|
95
|
+
return has ? { ...DEFAULT_FX, shadow: { ...DEFAULT_FX.shadow, on: true } } : DEFAULT_FX;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(raw) as Partial<Fx>;
|
|
99
|
+
return {
|
|
100
|
+
shadow: { ...DEFAULT_FX.shadow, ...parsed.shadow },
|
|
101
|
+
reflection: { ...DEFAULT_FX.reflection, ...parsed.reflection },
|
|
102
|
+
glow: { ...DEFAULT_FX.glow, ...parsed.glow },
|
|
103
|
+
softEdge: { ...DEFAULT_FX.softEdge, ...parsed.softEdge },
|
|
104
|
+
};
|
|
105
|
+
} catch {
|
|
106
|
+
return DEFAULT_FX;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 実機のスライダー行(ラベル / スライダー / 数値) */
|
|
111
|
+
function Row({
|
|
112
|
+
label, value, min, max, step, unit, onChange, pal, disabled,
|
|
113
|
+
}: {
|
|
114
|
+
label: string;
|
|
115
|
+
value: number;
|
|
116
|
+
min: number;
|
|
117
|
+
max: number;
|
|
118
|
+
step?: number;
|
|
119
|
+
unit?: string;
|
|
120
|
+
onChange: (v: number) => void;
|
|
121
|
+
pal: (typeof PPT_PALETTES)['dark'];
|
|
122
|
+
disabled?: boolean;
|
|
123
|
+
}) {
|
|
124
|
+
return (
|
|
125
|
+
<div className={`flex items-center gap-2 px-3 py-[3px] ${disabled ? 'opacity-40' : ''}`}>
|
|
126
|
+
<span className="w-[52px] shrink-0 text-[11px]" style={{ color: pal.sub }}>{label}</span>
|
|
127
|
+
<input
|
|
128
|
+
type="range"
|
|
129
|
+
min={min}
|
|
130
|
+
max={max}
|
|
131
|
+
step={step ?? 1}
|
|
132
|
+
value={value}
|
|
133
|
+
disabled={disabled}
|
|
134
|
+
onChange={(e) => onChange(Number(e.target.value))}
|
|
135
|
+
className="h-1 flex-1 accent-[#0F6CBD]"
|
|
136
|
+
/>
|
|
137
|
+
<input
|
|
138
|
+
type="number"
|
|
139
|
+
min={min}
|
|
140
|
+
max={max}
|
|
141
|
+
step={step ?? 1}
|
|
142
|
+
value={value}
|
|
143
|
+
disabled={disabled}
|
|
144
|
+
onChange={(e) => onChange(Number(e.target.value))}
|
|
145
|
+
className="h-6 w-[54px] rounded border px-1 text-right text-[11px] outline-none"
|
|
146
|
+
style={{ backgroundColor: pal.control, borderColor: pal.border, color: pal.text }}
|
|
147
|
+
/>
|
|
148
|
+
<span className="w-[16px] shrink-0 text-[10px]" style={{ color: pal.sub }}>{unit ?? ''}</span>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function Section({
|
|
154
|
+
title, open, onToggle, pal, children,
|
|
155
|
+
}: {
|
|
156
|
+
title: string;
|
|
157
|
+
open: boolean;
|
|
158
|
+
onToggle: () => void;
|
|
159
|
+
pal: (typeof PPT_PALETTES)['dark'];
|
|
160
|
+
children: React.ReactNode;
|
|
161
|
+
}) {
|
|
162
|
+
return (
|
|
163
|
+
<div className="border-b" style={{ borderColor: pal.border }}>
|
|
164
|
+
<button
|
|
165
|
+
onClick={onToggle}
|
|
166
|
+
className="flex w-full items-center gap-1 px-2 py-1.5 text-[12px] font-medium"
|
|
167
|
+
style={{ color: pal.text }}
|
|
168
|
+
>
|
|
169
|
+
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
|
170
|
+
{title}
|
|
171
|
+
</button>
|
|
172
|
+
{open && <div className="pb-2">{children}</div>}
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function PptFormatPane({
|
|
178
|
+
theme, onClose,
|
|
179
|
+
}: {
|
|
180
|
+
theme: PptTheme;
|
|
181
|
+
onClose: () => void;
|
|
182
|
+
}) {
|
|
183
|
+
const pal = PPT_PALETTES[theme];
|
|
184
|
+
const { getIframeDoc, selectedElement, selectedElementIds, notifyIframeChange } = useEditorContext();
|
|
185
|
+
|
|
186
|
+
const el = useMemo((): HTMLElement | null => {
|
|
187
|
+
const doc = getIframeDoc();
|
|
188
|
+
const id = selectedElementIds[0] ?? selectedElement?.id;
|
|
189
|
+
if (!doc || !id) return null;
|
|
190
|
+
const found = doc.querySelector<HTMLElement>(`[data-element-id="${id}"]`);
|
|
191
|
+
return found;
|
|
192
|
+
}, [getIframeDoc, selectedElement, selectedElementIds]);
|
|
193
|
+
|
|
194
|
+
const [fx, setFx] = useState<Fx>(DEFAULT_FX);
|
|
195
|
+
const [open, setOpen] = useState({ shadow: true, reflection: false, glow: false, softEdge: false });
|
|
196
|
+
|
|
197
|
+
// 選択が変わったら、その要素の効果を読み直す
|
|
198
|
+
useEffect(() => { setFx(readFx(el)); }, [el]);
|
|
199
|
+
|
|
200
|
+
const commit = useCallback(
|
|
201
|
+
(next: Fx) => {
|
|
202
|
+
setFx(next);
|
|
203
|
+
if (!el) return;
|
|
204
|
+
applyFx(el, next);
|
|
205
|
+
el.setAttribute(FX_ATTR, JSON.stringify(next));
|
|
206
|
+
notifyIframeChange();
|
|
207
|
+
},
|
|
208
|
+
[el, notifyIframeChange],
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const set = <K extends keyof Fx>(key: K, patch: Partial<Fx[K]>) =>
|
|
212
|
+
commit({ ...fx, [key]: { ...fx[key], ...patch } } as Fx);
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div
|
|
216
|
+
className="flex w-[280px] shrink-0 flex-col overflow-y-auto border-l"
|
|
217
|
+
style={{ backgroundColor: pal.chrome, borderColor: pal.border, color: pal.text }}
|
|
218
|
+
>
|
|
219
|
+
<div className="flex items-center justify-between border-b px-3 py-2" style={{ borderColor: pal.border }}>
|
|
220
|
+
<span className="text-[13px] font-semibold">図の書式設定</span>
|
|
221
|
+
<button onClick={onClose} title="閉じる" className="rounded p-0.5" style={{ color: pal.sub }}>
|
|
222
|
+
<X className="h-3.5 w-3.5" />
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
{!el && (
|
|
227
|
+
<div className="px-3 py-4 text-[11.5px]" style={{ color: pal.sub }}>
|
|
228
|
+
図形・画像を選択すると、影や反射などの効果を調整できます。
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{el && (
|
|
233
|
+
<>
|
|
234
|
+
<Section title="影" open={open.shadow} onToggle={() => setOpen((o) => ({ ...o, shadow: !o.shadow }))} pal={pal}>
|
|
235
|
+
<div className="flex items-center gap-1 px-3 py-1">
|
|
236
|
+
<span className="w-[52px] shrink-0 text-[11px]" style={{ color: pal.sub }}>標準スタイル</span>
|
|
237
|
+
{SHADOW_PRESETS.map((p) => (
|
|
238
|
+
<button
|
|
239
|
+
key={p.label}
|
|
240
|
+
onClick={() => commit({ ...fx, shadow: p.value ? { ...fx.shadow, ...p.value } : { ...fx.shadow, on: false } })}
|
|
241
|
+
className="rounded border px-1.5 py-0.5 text-[10.5px]"
|
|
242
|
+
style={{ borderColor: pal.border, color: pal.text }}
|
|
243
|
+
>
|
|
244
|
+
{p.label}
|
|
245
|
+
</button>
|
|
246
|
+
))}
|
|
247
|
+
</div>
|
|
248
|
+
<div className="flex items-center gap-2 px-3 py-[3px]">
|
|
249
|
+
<span className="w-[52px] shrink-0 text-[11px]" style={{ color: pal.sub }}>色</span>
|
|
250
|
+
<input type="color" value={fx.shadow.color}
|
|
251
|
+
onChange={(e) => set('shadow', { color: e.target.value, on: true })}
|
|
252
|
+
className="h-6 w-8 cursor-pointer rounded border" style={{ borderColor: pal.border }} />
|
|
253
|
+
</div>
|
|
254
|
+
<Row label="透明度" value={fx.shadow.opacity} min={0} max={100} unit="%" disabled={!fx.shadow.on}
|
|
255
|
+
onChange={(v) => set('shadow', { opacity: v })} pal={pal} />
|
|
256
|
+
<Row label="サイズ" value={fx.shadow.size} min={-20} max={40} unit="px" disabled={!fx.shadow.on}
|
|
257
|
+
onChange={(v) => set('shadow', { size: v })} pal={pal} />
|
|
258
|
+
<Row label="ぼかし" value={fx.shadow.blur} min={0} max={80} unit="px" disabled={!fx.shadow.on}
|
|
259
|
+
onChange={(v) => set('shadow', { blur: v })} pal={pal} />
|
|
260
|
+
<Row label="角度" value={fx.shadow.angle} min={0} max={360} unit="°" disabled={!fx.shadow.on}
|
|
261
|
+
onChange={(v) => set('shadow', { angle: v })} pal={pal} />
|
|
262
|
+
<Row label="距離" value={fx.shadow.distance} min={0} max={80} unit="px" disabled={!fx.shadow.on}
|
|
263
|
+
onChange={(v) => set('shadow', { distance: v, on: true })} pal={pal} />
|
|
264
|
+
</Section>
|
|
265
|
+
|
|
266
|
+
<Section title="反射" open={open.reflection} onToggle={() => setOpen((o) => ({ ...o, reflection: !o.reflection }))} pal={pal}>
|
|
267
|
+
<div className="flex items-center gap-1 px-3 py-1">
|
|
268
|
+
<span className="w-[52px] shrink-0 text-[11px]" style={{ color: pal.sub }}>標準スタイル</span>
|
|
269
|
+
<button onClick={() => set('reflection', { on: false })} className="rounded border px-1.5 py-0.5 text-[10.5px]" style={{ borderColor: pal.border, color: pal.text }}>なし</button>
|
|
270
|
+
<button onClick={() => set('reflection', { on: true, opacity: 50, size: 35, distance: 4 })} className="rounded border px-1.5 py-0.5 text-[10.5px]" style={{ borderColor: pal.border, color: pal.text }}>接触</button>
|
|
271
|
+
<button onClick={() => set('reflection', { on: true, opacity: 40, size: 50, distance: 16 })} className="rounded border px-1.5 py-0.5 text-[10.5px]" style={{ borderColor: pal.border, color: pal.text }}>離す</button>
|
|
272
|
+
</div>
|
|
273
|
+
<Row label="透明度" value={fx.reflection.opacity} min={0} max={100} unit="%" disabled={!fx.reflection.on}
|
|
274
|
+
onChange={(v) => set('reflection', { opacity: v })} pal={pal} />
|
|
275
|
+
<Row label="サイズ" value={fx.reflection.size} min={0} max={100} unit="%" disabled={!fx.reflection.on}
|
|
276
|
+
onChange={(v) => set('reflection', { size: v })} pal={pal} />
|
|
277
|
+
<Row label="距離" value={fx.reflection.distance} min={0} max={60} unit="px" disabled={!fx.reflection.on}
|
|
278
|
+
onChange={(v) => set('reflection', { distance: v })} pal={pal} />
|
|
279
|
+
</Section>
|
|
280
|
+
|
|
281
|
+
<Section title="光彩" open={open.glow} onToggle={() => setOpen((o) => ({ ...o, glow: !o.glow }))} pal={pal}>
|
|
282
|
+
<div className="flex items-center gap-2 px-3 py-[3px]">
|
|
283
|
+
<span className="w-[52px] shrink-0 text-[11px]" style={{ color: pal.sub }}>色</span>
|
|
284
|
+
<input type="color" value={fx.glow.color}
|
|
285
|
+
onChange={(e) => set('glow', { color: e.target.value, on: true })}
|
|
286
|
+
className="h-6 w-8 cursor-pointer rounded border" style={{ borderColor: pal.border }} />
|
|
287
|
+
<button onClick={() => set('glow', { on: !fx.glow.on })}
|
|
288
|
+
className="ml-auto rounded border px-1.5 py-0.5 text-[10.5px]" style={{ borderColor: pal.border, color: pal.text }}>
|
|
289
|
+
{fx.glow.on ? '光彩を消す' : '光彩を付ける'}
|
|
290
|
+
</button>
|
|
291
|
+
</div>
|
|
292
|
+
<Row label="サイズ" value={fx.glow.size} min={0} max={60} unit="px" disabled={!fx.glow.on}
|
|
293
|
+
onChange={(v) => set('glow', { size: v })} pal={pal} />
|
|
294
|
+
<Row label="透明度" value={fx.glow.opacity} min={0} max={100} unit="%" disabled={!fx.glow.on}
|
|
295
|
+
onChange={(v) => set('glow', { opacity: v })} pal={pal} />
|
|
296
|
+
</Section>
|
|
297
|
+
|
|
298
|
+
<Section title="ぼかし(ソフトエッジ)" open={open.softEdge} onToggle={() => setOpen((o) => ({ ...o, softEdge: !o.softEdge }))} pal={pal}>
|
|
299
|
+
<div className="px-3 py-1">
|
|
300
|
+
<button onClick={() => set('softEdge', { on: !fx.softEdge.on })}
|
|
301
|
+
className="rounded border px-1.5 py-0.5 text-[10.5px]" style={{ borderColor: pal.border, color: pal.text }}>
|
|
302
|
+
{fx.softEdge.on ? 'ソフトエッジを消す' : 'ソフトエッジを付ける'}
|
|
303
|
+
</button>
|
|
304
|
+
</div>
|
|
305
|
+
<Row label="サイズ" value={fx.softEdge.size} min={0} max={45} unit="%" disabled={!fx.softEdge.on}
|
|
306
|
+
onChange={(v) => set('softEdge', { size: v })} pal={pal} />
|
|
307
|
+
</Section>
|
|
308
|
+
|
|
309
|
+
<button
|
|
310
|
+
onClick={() => commit(DEFAULT_FX)}
|
|
311
|
+
className="m-3 rounded border px-2 py-1 text-[11.5px]"
|
|
312
|
+
style={{ borderColor: pal.border, color: pal.text }}
|
|
313
|
+
>
|
|
314
|
+
効果をすべて解除
|
|
315
|
+
</button>
|
|
316
|
+
</>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
}
|