@growgroup/visual-editor 0.1.6 → 0.2.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/README.md +59 -2
- package/dist/editor.css +1 -1
- package/package.json +7 -3
- package/src/components/ui/alert-dialog.tsx +18 -4
- package/src/components/ui/context-menu.tsx +18 -4
- package/src/components/ui/dialog.tsx +19 -5
- package/src/components/ui/dropdown-menu.tsx +18 -4
- package/src/components/ui/popover.tsx +10 -2
- package/src/components/ui/select.tsx +10 -2
- package/src/components/ui/tooltip.tsx +10 -2
- package/src/editor/EditorToolbar.tsx +10 -20
- package/src/editor/FrontendVisualEditor.tsx +157 -43
- package/src/editor/components/EditorCanvas.tsx +1 -1
- package/src/editor/components/EditorContextMenu.tsx +53 -71
- package/src/editor/components/EditorFooter.tsx +36 -70
- package/src/editor/components/EditorHeader.tsx +37 -20
- package/src/editor/components/EditorLayerPanel.tsx +7 -9
- package/src/editor/components/EditorPropertyPanel.tsx +84 -8
- package/src/editor/components/LayerTreeItem.tsx +28 -5
- package/src/editor/components/component-panel/ComponentPanel.tsx +4 -3
- package/src/editor/components/ppt/PptChrome.tsx +156 -344
- package/src/editor/components/ppt/PptComments.tsx +135 -87
- package/src/editor/components/ppt/PptDesignProposals.tsx +21 -26
- package/src/editor/components/ppt/PptNotes.tsx +15 -15
- package/src/editor/components/shell/CanvasAppearance.tsx +19 -0
- package/src/editor/components/shell/ConfirmDialog.tsx +15 -0
- package/src/editor/components/shell/EditorTopBar.tsx +29 -68
- package/src/editor/components/shell/LeftPanel.tsx +20 -94
- package/src/editor/components/shell/PagesPanel.tsx +4 -2
- package/src/editor/contexts/EditorAppearanceContext.tsx +44 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +278 -16
- package/src/editor/editor-skin.css +2 -122
- package/src/editor/hooks/useComponentEditMode.ts +95 -1
- package/src/editor/hooks/useIframeSetup.ts +10 -0
- package/src/editor/hooks/useMediaLibrary.ts +4 -5
- package/src/editor/hooks/useResizablePanel.ts +40 -11
- package/src/editor/parts.ts +321 -0
- package/src/editor/utils/dom-utils.ts +5 -4
- package/src/index.ts +25 -0
- package/src/io.ts +54 -0
- package/src/styles/skin.css +159 -0
- package/src/types/editor-components.ts +23 -1
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
16
|
-
import { Check, CornerUpLeft, Loader2, MapPin, RotateCcw, Send, Sparkles, Trash2, X } from 'lucide-react';
|
|
17
|
-
import { readApiJson } from '../../utils/api-json';
|
|
16
|
+
import { Check, CornerUpLeft, Loader2, MapPin, RotateCcw, Send, MessageSquare, Sparkles, Trash2, X } from 'lucide-react';
|
|
18
17
|
import { useEditorContext } from '../../EditorContext';
|
|
19
|
-
import { useDeck,
|
|
18
|
+
import { useDeck, applyDeck } from '../../../components/viewer/useDeck';
|
|
20
19
|
import { commentAction, type SlideComment } from '../../../lib/deck';
|
|
21
|
-
import { can } from '../../../io';
|
|
20
|
+
import { can, io, type EditorDeck } from '../../../io';
|
|
21
|
+
import { useResizablePanel } from '../../hooks/useResizablePanel';
|
|
22
|
+
import { ConfirmDialog } from '../shell/ConfirmDialog';
|
|
22
23
|
import { PPT_PALETTES, type PptTheme } from './PptChrome';
|
|
23
24
|
|
|
24
25
|
const AUTHOR_KEY = 'gg-editor:comment-author';
|
|
@@ -131,7 +132,7 @@ function Avatar({ name }: { name: string }) {
|
|
|
131
132
|
return (
|
|
132
133
|
<span
|
|
133
134
|
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[11px] font-bold text-white"
|
|
134
|
-
style={{ backgroundColor: colors[h % colors.length] }}
|
|
135
|
+
style={{ backgroundColor: colors[h % colors.length], color: "#ffffff" }}
|
|
135
136
|
>
|
|
136
137
|
{(name || '?').slice(0, 1)}
|
|
137
138
|
</span>
|
|
@@ -176,31 +177,57 @@ export function useCommentMarkers({
|
|
|
176
177
|
layer.style.cssText = 'position:absolute;inset:0;pointer-events:none;z-index:9000;';
|
|
177
178
|
artboard.appendChild(layer);
|
|
178
179
|
}
|
|
179
|
-
layer.innerHTML = '';
|
|
180
180
|
const anchored = comments.filter((c) => c.anchorSrc && !c.resolved);
|
|
181
|
+
const wanted = new Set(anchored.map((c) => c.id));
|
|
182
|
+
layer.querySelectorAll<HTMLButtonElement>('button').forEach((button) => {
|
|
183
|
+
if (!wanted.has(button.dataset.commentId!)) button.remove();
|
|
184
|
+
});
|
|
185
|
+
const rootRect = artboard.getBoundingClientRect();
|
|
186
|
+
const scale = rootRect.width / artboard.offsetWidth || 1;
|
|
187
|
+
const size = 32 / scale;
|
|
188
|
+
const stacks = new Map<string, number>();
|
|
181
189
|
for (const c of anchored) {
|
|
182
190
|
const target = findAnchored(artboard, c.anchorSrc!);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
bubble
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
'
|
|
193
|
-
'
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
191
|
+
// 要素が削除されたコメントは一覧から参照できる。別の場所に誤って付けない。
|
|
192
|
+
if (!target) {
|
|
193
|
+
layer.querySelector(`[data-comment-id="${CSS.escape(c.id)}"]`)?.remove();
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
let bubble = layer.querySelector<HTMLButtonElement>(`[data-comment-id="${CSS.escape(c.id)}"]`);
|
|
197
|
+
if (!bubble) {
|
|
198
|
+
bubble = doc.createElement('button');
|
|
199
|
+
bubble.type = 'button';
|
|
200
|
+
bubble.setAttribute('data-comment-id', c.id);
|
|
201
|
+
bubble.addEventListener('mousedown', (e) => { e.stopPropagation(); });
|
|
202
|
+
bubble.addEventListener('click', (e) => {
|
|
203
|
+
e.preventDefault(); e.stopPropagation(); onOpenRef.current(c.id);
|
|
204
|
+
});
|
|
205
|
+
bubble.addEventListener('focus', () => { bubble!.style.outline = '3px solid #2459c4'; });
|
|
206
|
+
bubble.addEventListener('blur', () => { bubble!.style.outline = ''; });
|
|
207
|
+
layer.appendChild(bubble);
|
|
208
|
+
}
|
|
209
|
+
const label = `${c.author}のコメント:${c.text.slice(0, 80)}`;
|
|
210
|
+
bubble.title = label;
|
|
211
|
+
bubble.setAttribute('aria-label', label);
|
|
212
|
+
const letter = (c.author || '?').slice(0, 1);
|
|
213
|
+
if (bubble.textContent !== letter) bubble.textContent = letter;
|
|
214
|
+
const rect = target.getBoundingClientRect();
|
|
215
|
+
const stack = stacks.get(c.anchorSrc!) ?? 0;
|
|
216
|
+
stacks.set(c.anchorSrc!, stack + 1);
|
|
217
|
+
const x = Math.max(0, Math.min(artboard.offsetWidth - size, (rect.right - rootRect.left) / scale - size / 2));
|
|
218
|
+
const y = Math.max(0, (rect.top - rootRect.top) / scale - size / 2) + stack * (size + 4 / scale);
|
|
219
|
+
const styles: Partial<CSSStyleDeclaration> = {
|
|
220
|
+
position: 'absolute', left: `${x}px`, top: `${y}px`, width: `${size}px`, height: `${size}px`,
|
|
221
|
+
borderRadius: '50% 50% 50% 4px', background: '#2459c4', color: '#fff', fontSize: `${13 / scale}px`,
|
|
222
|
+
fontFamily: 'system-ui, sans-serif', fontWeight: '700', border: `${2 / scale}px solid #fff`,
|
|
223
|
+
boxShadow: '0 2px 8px #0003', cursor: 'pointer', pointerEvents: 'auto', display: 'flex',
|
|
224
|
+
alignItems: 'center', justifyContent: 'center', padding: '0px',
|
|
225
|
+
};
|
|
226
|
+
// 同じ値を再代入してMutationObserverを起こさない。フォーカス中のDOMも維持する。
|
|
227
|
+
const style = bubble.style as unknown as Record<string, string>;
|
|
228
|
+
for (const [key, value] of Object.entries(styles)) {
|
|
229
|
+
if (style[key] !== value) style[key] = value as string;
|
|
230
|
+
}
|
|
204
231
|
}
|
|
205
232
|
};
|
|
206
233
|
|
|
@@ -213,7 +240,7 @@ export function useCommentMarkers({
|
|
|
213
240
|
const layer = getIframeDoc()?.querySelector('.gg-comment-layer');
|
|
214
241
|
layer?.remove();
|
|
215
242
|
};
|
|
216
|
-
}, [active, page, getIframeDoc, JSON.stringify(comments.map((c) => [c.id, c.anchorSrc, c.resolved, c.author]))]);
|
|
243
|
+
}, [active, page, getIframeDoc, JSON.stringify(comments.map((c) => [c.id, c.anchorSrc, c.resolved, c.author, c.text]))]);
|
|
217
244
|
}
|
|
218
245
|
|
|
219
246
|
/* ============================ 右パネル ============================ */
|
|
@@ -245,6 +272,10 @@ export function PptCommentsPanel({
|
|
|
245
272
|
const [replyDraft, setReplyDraft] = useState('');
|
|
246
273
|
const [showResolved, setShowResolved] = useState(false);
|
|
247
274
|
const [busy, setBusy] = useState(false);
|
|
275
|
+
const busyRef = useRef(false);
|
|
276
|
+
const [error, setError] = useState('');
|
|
277
|
+
const [deleteId, setDeleteId] = useState<string | null>(null);
|
|
278
|
+
const { width, isDragging, resizeHandleProps } = useResizablePanel({ initialWidth: 336, minWidth: 304, maxWidth: 480, direction: 'left', storageKey: 'gg-editor:comments-width' });
|
|
248
279
|
const composerRef = useRef<HTMLTextAreaElement>(null);
|
|
249
280
|
const threadRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
|
250
281
|
|
|
@@ -254,7 +285,7 @@ export function PptCommentsPanel({
|
|
|
254
285
|
|
|
255
286
|
useEffect(() => {
|
|
256
287
|
if (activeThreadId) {
|
|
257
|
-
threadRefs.current[activeThreadId]?.scrollIntoView({ block: 'nearest', behavior: '
|
|
288
|
+
threadRefs.current[activeThreadId]?.scrollIntoView({ block: 'nearest', behavior: 'auto' });
|
|
258
289
|
}
|
|
259
290
|
}, [activeThreadId]);
|
|
260
291
|
|
|
@@ -276,20 +307,18 @@ export function PptCommentsPanel({
|
|
|
276
307
|
async (commentId: string) => {
|
|
277
308
|
setFixJobs((prev) => ({ ...prev, [commentId]: { message: '開始中…' } }));
|
|
278
309
|
try {
|
|
279
|
-
const
|
|
310
|
+
const data = await io().apiFetch!('/__comment-fix', {
|
|
280
311
|
method: 'POST',
|
|
281
312
|
headers: { 'content-type': 'application/json' },
|
|
282
313
|
body: JSON.stringify({ page, commentId }),
|
|
283
314
|
});
|
|
284
|
-
const { jobId, error } =
|
|
315
|
+
const { jobId, error } = data as { jobId?: string; error?: string };
|
|
285
316
|
if (!jobId) throw new Error(error ?? '開始できませんでした');
|
|
286
317
|
|
|
287
318
|
const startedAt = Date.now();
|
|
288
319
|
while (Date.now() - startedAt < 8 * 60 * 1000) {
|
|
289
320
|
await new Promise((r) => setTimeout(r, 1500));
|
|
290
|
-
const st = await
|
|
291
|
-
await fetch(`/__comment-fix/status/${jobId}`),
|
|
292
|
-
);
|
|
321
|
+
const st = await io().apiFetch!(`/__comment-fix/status/${jobId}`) as { state: string; message?: string; error?: string };
|
|
293
322
|
if (st.state === 'running') {
|
|
294
323
|
setFixJobs((prev) => ({ ...prev, [commentId]: { message: st.message || '実行中…' } }));
|
|
295
324
|
continue;
|
|
@@ -328,18 +357,16 @@ export function PptCommentsPanel({
|
|
|
328
357
|
)) return;
|
|
329
358
|
setFixAll({ running: true, message: '開始中…' });
|
|
330
359
|
try {
|
|
331
|
-
const
|
|
360
|
+
const data = await io().apiFetch!('/__comment-fix', {
|
|
332
361
|
method: 'POST',
|
|
333
362
|
headers: { 'content-type': 'application/json' },
|
|
334
363
|
body: JSON.stringify({ all: true }),
|
|
335
364
|
});
|
|
336
|
-
const { jobId, error } =
|
|
365
|
+
const { jobId, error } = data as { jobId?: string; error?: string };
|
|
337
366
|
if (!jobId) throw new Error(error ?? '開始できませんでした');
|
|
338
367
|
for (;;) {
|
|
339
368
|
await new Promise((r) => setTimeout(r, 2000));
|
|
340
|
-
const st = await
|
|
341
|
-
await fetch(`/__comment-fix/status/${jobId}`),
|
|
342
|
-
);
|
|
369
|
+
const st = await io().apiFetch!(`/__comment-fix/status/${jobId}`) as { state: string; message?: string; error?: string };
|
|
343
370
|
if (st.state === 'running') {
|
|
344
371
|
setFixAll({ running: true, message: st.message || '実行中…' });
|
|
345
372
|
continue;
|
|
@@ -356,18 +383,21 @@ export function PptCommentsPanel({
|
|
|
356
383
|
}
|
|
357
384
|
}, [fixAll?.running, unresolvedTotal]);
|
|
358
385
|
|
|
359
|
-
const run = useCallback(async (fn: () => Promise<
|
|
360
|
-
if (
|
|
361
|
-
|
|
386
|
+
const run = useCallback(async (fn: () => Promise<EditorDeck>): Promise<boolean> => {
|
|
387
|
+
if (busyRef.current) return false;
|
|
388
|
+
busyRef.current = true;
|
|
389
|
+
setBusy(true); setError('');
|
|
362
390
|
try {
|
|
363
|
-
await fn();
|
|
364
|
-
|
|
391
|
+
applyDeck(await fn());
|
|
392
|
+
return true;
|
|
365
393
|
} catch (e) {
|
|
366
|
-
|
|
394
|
+
setError(`操作できませんでした。${e instanceof Error ? e.message : String(e)}`);
|
|
395
|
+
return false;
|
|
367
396
|
} finally {
|
|
397
|
+
busyRef.current = false;
|
|
368
398
|
setBusy(false);
|
|
369
399
|
}
|
|
370
|
-
}, [
|
|
400
|
+
}, []);
|
|
371
401
|
|
|
372
402
|
/** 選択中の要素からアンカーを拾う(選択なしならページ全体へのコメント) */
|
|
373
403
|
const currentAnchor = useMemo(() => {
|
|
@@ -393,7 +423,7 @@ export function PptCommentsPanel({
|
|
|
393
423
|
anchorSrc: currentAnchor?.src,
|
|
394
424
|
anchorLabel: currentAnchor?.label,
|
|
395
425
|
}),
|
|
396
|
-
).then(() => setDraft(''));
|
|
426
|
+
).then((ok) => { if (ok) setDraft(''); });
|
|
397
427
|
};
|
|
398
428
|
|
|
399
429
|
const open = comments.filter((c) => !c.resolved);
|
|
@@ -414,11 +444,13 @@ export function PptCommentsPanel({
|
|
|
414
444
|
<div
|
|
415
445
|
key={c.id}
|
|
416
446
|
ref={(el) => { threadRefs.current[c.id] = el; }}
|
|
417
|
-
className="rounded-lg border p-
|
|
447
|
+
className="ed-comment-thread rounded-lg border p-4"
|
|
448
|
+
data-comment-thread={c.id}
|
|
449
|
+
aria-label={`${c.author}のコメント`}
|
|
418
450
|
style={{
|
|
419
|
-
borderColor: activeThreadId === c.id ? '
|
|
451
|
+
borderColor: activeThreadId === c.id ? 'var(--ed-accent)' : pal.border,
|
|
420
452
|
backgroundColor: pal.control,
|
|
421
|
-
|
|
453
|
+
|
|
422
454
|
}}
|
|
423
455
|
onClick={() => onActiveThread(c.id)}
|
|
424
456
|
>
|
|
@@ -430,7 +462,7 @@ export function PptCommentsPanel({
|
|
|
430
462
|
</div>
|
|
431
463
|
{c.resolved ? (
|
|
432
464
|
<button
|
|
433
|
-
title="スレッドを再開"
|
|
465
|
+
disabled={busy} aria-label="スレッドを再開" title="スレッドを再開"
|
|
434
466
|
onClick={(e) => { e.stopPropagation(); void run(() => commentAction(page, { action: 'resolve', commentId: c.id, resolved: false })); }}
|
|
435
467
|
className="rounded p-1" style={{ color: pal.sub }}
|
|
436
468
|
>
|
|
@@ -438,7 +470,7 @@ export function PptCommentsPanel({
|
|
|
438
470
|
</button>
|
|
439
471
|
) : (
|
|
440
472
|
<button
|
|
441
|
-
title="解決済みにする"
|
|
473
|
+
disabled={busy} aria-label="解決済みにする" title="解決済みにする"
|
|
442
474
|
onClick={(e) => { e.stopPropagation(); void run(() => commentAction(page, { action: 'resolve', commentId: c.id, resolved: true })); }}
|
|
443
475
|
className="rounded p-1" style={{ color: pal.sub }}
|
|
444
476
|
>
|
|
@@ -446,12 +478,10 @@ export function PptCommentsPanel({
|
|
|
446
478
|
</button>
|
|
447
479
|
)}
|
|
448
480
|
<button
|
|
449
|
-
title="スレッドを削除"
|
|
481
|
+
disabled={busy} aria-label="スレッドを削除" title="スレッドを削除"
|
|
450
482
|
onClick={(e) => {
|
|
451
483
|
e.stopPropagation();
|
|
452
|
-
|
|
453
|
-
void run(() => commentAction(page, { action: 'delete', commentId: c.id }));
|
|
454
|
-
}
|
|
484
|
+
setDeleteId(c.id);
|
|
455
485
|
}}
|
|
456
486
|
className="rounded p-1" style={{ color: pal.sub }}
|
|
457
487
|
>
|
|
@@ -460,12 +490,13 @@ export function PptCommentsPanel({
|
|
|
460
490
|
</div>
|
|
461
491
|
|
|
462
492
|
{c.anchorSrc && (
|
|
463
|
-
<div className="mt-1.5 flex items-center gap-1 text-[10px]" style={{ color: '
|
|
493
|
+
<div className="mt-1.5 flex items-center gap-1 text-[10px]" style={{ color: 'var(--ed-accent)' }}>
|
|
464
494
|
<MapPin className="h-3 w-3" />
|
|
465
495
|
{c.anchorLabel ? `「${c.anchorLabel}」` : '要素に添付'}
|
|
466
496
|
</div>
|
|
467
497
|
)}
|
|
468
498
|
|
|
499
|
+
{c.resolved && <p className="mt-2 text-xs text-gray-400">解決済み</p>}
|
|
469
500
|
<p className="mt-1.5 whitespace-pre-wrap text-[12.5px] leading-relaxed" style={{ color: pal.text }}>
|
|
470
501
|
{c.text}
|
|
471
502
|
</p>
|
|
@@ -498,7 +529,7 @@ export function PptCommentsPanel({
|
|
|
498
529
|
}}
|
|
499
530
|
title="この指摘のとおりにAIが原本(TSX)を修正します"
|
|
500
531
|
className="flex items-center gap-1 rounded border px-1.5 py-1 text-[11px] transition-colors"
|
|
501
|
-
style={{ borderColor: pal.border, color: '
|
|
532
|
+
style={{ borderColor: pal.border, color: 'var(--ed-accent)' }}
|
|
502
533
|
>
|
|
503
534
|
<Sparkles className="h-3 w-3" />
|
|
504
535
|
AIで修正
|
|
@@ -517,15 +548,17 @@ export function PptCommentsPanel({
|
|
|
517
548
|
<div className="mt-2 flex items-end gap-1.5">
|
|
518
549
|
<textarea
|
|
519
550
|
autoFocus
|
|
551
|
+
aria-label="返信内容"
|
|
552
|
+
disabled={busy}
|
|
520
553
|
value={replyDraft}
|
|
521
554
|
onChange={(e) => setReplyDraft(e.target.value)}
|
|
522
555
|
onKeyDown={(e) => {
|
|
523
|
-
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
556
|
+
if (e.key === 'Enter' && !e.nativeEvent.isComposing && (e.metaKey || e.ctrlKey)) {
|
|
524
557
|
e.preventDefault();
|
|
525
558
|
const t = replyDraft.trim();
|
|
526
559
|
if (t) {
|
|
527
560
|
void run(() => commentAction(page, { action: 'reply', commentId: c.id, author: author.trim() || 'ゲスト', text: t }))
|
|
528
|
-
.then(() => { setReplyDraft(''); setReplyFor(null); });
|
|
561
|
+
.then((ok) => { if (ok) { setReplyDraft(''); setReplyFor(null); } });
|
|
529
562
|
}
|
|
530
563
|
}
|
|
531
564
|
}}
|
|
@@ -535,15 +568,15 @@ export function PptCommentsPanel({
|
|
|
535
568
|
style={{ backgroundColor: pal.chrome, borderColor: pal.border, color: pal.text }}
|
|
536
569
|
/>
|
|
537
570
|
<button
|
|
538
|
-
title="返信を送信"
|
|
571
|
+
disabled={busy || !replyDraft.trim()} aria-label="返信を送信" title="返信を送信"
|
|
539
572
|
onClick={() => {
|
|
540
573
|
const t = replyDraft.trim();
|
|
541
574
|
if (t) {
|
|
542
575
|
void run(() => commentAction(page, { action: 'reply', commentId: c.id, author: author.trim() || 'ゲスト', text: t }))
|
|
543
|
-
.then(() => { setReplyDraft(''); setReplyFor(null); });
|
|
576
|
+
.then((ok) => { if (ok) { setReplyDraft(''); setReplyFor(null); } });
|
|
544
577
|
}
|
|
545
578
|
}}
|
|
546
|
-
className="rounded p-1.5" style={{ color: '
|
|
579
|
+
className="rounded p-1.5" style={{ color: 'var(--ed-accent)' }}
|
|
547
580
|
>
|
|
548
581
|
<Send className="h-4 w-4" />
|
|
549
582
|
</button>
|
|
@@ -553,7 +586,7 @@ export function PptCommentsPanel({
|
|
|
553
586
|
<button
|
|
554
587
|
onClick={(e) => { e.stopPropagation(); setReplyFor(c.id); setReplyDraft(''); }}
|
|
555
588
|
className="mt-1.5 flex items-center gap-1 text-[11px]"
|
|
556
|
-
style={{ color: '
|
|
589
|
+
style={{ color: 'var(--ed-accent)' }}
|
|
557
590
|
>
|
|
558
591
|
<CornerUpLeft className="h-3 w-3" />
|
|
559
592
|
返信
|
|
@@ -565,28 +598,33 @@ export function PptCommentsPanel({
|
|
|
565
598
|
|
|
566
599
|
return (
|
|
567
600
|
<div
|
|
568
|
-
className="flex
|
|
569
|
-
|
|
601
|
+
className="relative flex shrink-0 flex-col border-l ed-comments-panel"
|
|
602
|
+
aria-label="コメント"
|
|
603
|
+
data-comments-panel
|
|
604
|
+
style={{ width, backgroundColor: "var(--ed-surface)", borderColor: pal.border }}
|
|
570
605
|
>
|
|
571
|
-
<div
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
</span>
|
|
606
|
+
<div {...resizeHandleProps} />
|
|
607
|
+
{isDragging && <div className="fixed inset-0 z-50 cursor-col-resize" />}
|
|
608
|
+
<div className="ed-panel-heading" style={{ borderColor: pal.border }}>
|
|
609
|
+
<MessageSquare className="h-4 w-4" /><span style={{ color: pal.text }}>コメント</span>
|
|
610
|
+
<span className="text-xs font-normal text-gray-400">未解決 {comments.filter((c) => !c.resolved).length}</span>
|
|
611
|
+
|
|
612
|
+
<button onClick={onClose} className="ed-icon-button ml-auto" style={{ color: pal.sub }} title="コメントを閉じる" aria-label="コメントを閉じる">
|
|
613
|
+
<X className="h-4 w-4" />
|
|
614
|
+
</button>
|
|
615
|
+
</div>
|
|
616
|
+
<div className="px-4">
|
|
576
617
|
{unresolvedTotal > 0 && !fixAll?.running && can('apiFetch') && (
|
|
577
618
|
<button
|
|
578
619
|
onClick={() => void startFixAll()}
|
|
579
620
|
title="全スライドの未解決コメントをAIが順に修正し、各スレッドへ対応報告を返信します"
|
|
580
|
-
className="
|
|
621
|
+
className="my-2 flex items-center gap-1 rounded border px-1.5 py-0.5 text-[11px]"
|
|
581
622
|
style={{ borderColor: pal.border, color: pal.text }}
|
|
582
623
|
>
|
|
583
624
|
<Sparkles className="h-3 w-3" />
|
|
584
625
|
すべてAIで修正({unresolvedTotal})
|
|
585
626
|
</button>
|
|
586
627
|
)}
|
|
587
|
-
<button onClick={onClose} className="ml-auto rounded p-1" style={{ color: pal.sub }} title="閉じる">
|
|
588
|
-
<X className="h-4 w-4" />
|
|
589
|
-
</button>
|
|
590
628
|
</div>
|
|
591
629
|
{(fixAll?.running || fixAll?.error) && (
|
|
592
630
|
<div className="border-b px-3 py-1.5 text-[11px]" style={{ borderColor: pal.border, color: fixAll.error ? '#f66' : pal.sub }}>
|
|
@@ -595,15 +633,18 @@ export function PptCommentsPanel({
|
|
|
595
633
|
)}
|
|
596
634
|
|
|
597
635
|
|
|
636
|
+
{error && <p role="alert" className="px-4 py-3 text-xs" style={{ color: "var(--ed-danger)" }}>{error} 入力内容は残っています。</p>}
|
|
637
|
+
<p className="border-b px-4 py-2 text-xs text-gray-400">{deck.slides[page - 1]?.title ?? `ページ ${page}`}</p>
|
|
598
638
|
{/* 新規コメント */}
|
|
599
|
-
<div className="border-b p-
|
|
639
|
+
<div className="border-b p-4" style={{ borderColor: pal.border }}>
|
|
600
640
|
<div className="flex items-center gap-1.5">
|
|
601
641
|
<Avatar name={author || 'ゲ'} />
|
|
602
642
|
<input
|
|
643
|
+
aria-label="投稿者の名前"
|
|
603
644
|
value={author}
|
|
604
645
|
onChange={(e) => saveAuthor(e.target.value)}
|
|
605
646
|
placeholder="名前(記憶されます)"
|
|
606
|
-
className="h-
|
|
647
|
+
className="h-8 flex-1 rounded border px-2 text-[11px] outline-none"
|
|
607
648
|
style={{ backgroundColor: pal.control, borderColor: pal.border, color: pal.text }}
|
|
608
649
|
/>
|
|
609
650
|
</div>
|
|
@@ -612,22 +653,21 @@ export function PptCommentsPanel({
|
|
|
612
653
|
<div
|
|
613
654
|
className="mt-1.5 flex items-start gap-1.5 rounded border px-1.5 py-1 text-[10px]"
|
|
614
655
|
style={{
|
|
615
|
-
color: '
|
|
616
|
-
borderColor: '
|
|
656
|
+
color: 'var(--ed-accent)',
|
|
657
|
+
borderColor: 'var(--ed-accent)',
|
|
617
658
|
backgroundColor: 'rgba(15,108,189,.08)',
|
|
618
659
|
}}
|
|
619
|
-
title={currentAnchor.src}
|
|
620
660
|
>
|
|
621
661
|
<MapPin className="mt-px h-3 w-3 shrink-0" />
|
|
622
662
|
<span className="min-w-0">
|
|
623
663
|
{currentAnchor.kind === 'range' ? (
|
|
624
664
|
<>
|
|
625
|
-
|
|
665
|
+
「
|
|
626
666
|
<span className="font-bold">{currentAnchor.label}</span>」を含む範囲)へのコメント
|
|
627
667
|
</>
|
|
628
668
|
) : (
|
|
629
669
|
<>
|
|
630
|
-
|
|
670
|
+
選択中の「
|
|
631
671
|
<span className="font-bold">{currentAnchor.label}</span>」へのコメント
|
|
632
672
|
</>
|
|
633
673
|
)}
|
|
@@ -647,10 +687,12 @@ export function PptCommentsPanel({
|
|
|
647
687
|
<div className="mt-1.5 flex items-end gap-1.5">
|
|
648
688
|
<textarea
|
|
649
689
|
ref={composerRef}
|
|
690
|
+
aria-label="コメント内容"
|
|
691
|
+
disabled={busy}
|
|
650
692
|
value={draft}
|
|
651
693
|
onChange={(e) => setDraft(e.target.value)}
|
|
652
694
|
onKeyDown={(e) => {
|
|
653
|
-
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
695
|
+
if (e.key === 'Enter' && !e.nativeEvent.isComposing && (e.metaKey || e.ctrlKey)) {
|
|
654
696
|
e.preventDefault();
|
|
655
697
|
post();
|
|
656
698
|
}
|
|
@@ -664,27 +706,30 @@ export function PptCommentsPanel({
|
|
|
664
706
|
onClick={post}
|
|
665
707
|
disabled={busy || !draft.trim()}
|
|
666
708
|
title="コメントを投稿"
|
|
667
|
-
className="
|
|
668
|
-
style={{ color: '#0F6CBD' }}
|
|
709
|
+
className="ed-button ed-button-primary"
|
|
669
710
|
>
|
|
670
711
|
<Send className="h-4 w-4" />
|
|
712
|
+
{busy ? "送信中" : "送信"}
|
|
671
713
|
</button>
|
|
672
714
|
</div>
|
|
715
|
+
<p className="mt-2 text-xs text-gray-400">⌘ / Ctrl+Enter で送信</p>
|
|
673
716
|
</div>
|
|
674
717
|
|
|
675
718
|
{/* スレッド一覧 */}
|
|
676
|
-
<div className="flex-1 space-y-
|
|
719
|
+
<div className="min-h-0 flex-1 space-y-3 overflow-y-auto p-4">
|
|
677
720
|
{open.length === 0 && resolved.length === 0 && (
|
|
678
721
|
<p className="px-1 pt-2 text-[12px]" style={{ color: pal.sub }}>
|
|
679
722
|
まだコメントはありません。要素を選択して投稿すると、その要素に吹き出しが付きます。
|
|
680
723
|
</p>
|
|
681
724
|
)}
|
|
725
|
+
{open.length === 0 && resolved.length > 0 && <p className="py-2 text-xs text-gray-400">未解決のコメントはありません。</p>}
|
|
682
726
|
{open.map((c) => renderThread(c))}
|
|
683
727
|
|
|
684
728
|
{resolved.length > 0 && (
|
|
685
729
|
<button
|
|
686
730
|
onClick={() => setShowResolved((v) => !v)}
|
|
687
|
-
|
|
731
|
+
aria-expanded={showResolved}
|
|
732
|
+
className="w-full rounded py-2 text-left text-xs"
|
|
688
733
|
style={{ color: pal.sub }}
|
|
689
734
|
>
|
|
690
735
|
{showResolved ? '▾' : '▸'} 解決済み ({resolved.length})
|
|
@@ -692,6 +737,9 @@ export function PptCommentsPanel({
|
|
|
692
737
|
)}
|
|
693
738
|
{showResolved && resolved.map((c) => renderThread(c))}
|
|
694
739
|
</div>
|
|
740
|
+
<ConfirmDialog open={deleteId !== null} onOpenChange={(open) => { if (!open) setDeleteId(null); }}
|
|
741
|
+
title="コメントを削除しますか?" description="このスレッドと返信を削除します。この操作は取り消せません。"
|
|
742
|
+
onConfirm={() => { if (deleteId) void run(() => commentAction(page, { action: 'delete', commentId: deleteId })); }} />
|
|
695
743
|
</div>
|
|
696
744
|
);
|
|
697
745
|
}
|
|
@@ -12,7 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
14
14
|
import { readApiJson } from '../../utils/api-json';
|
|
15
|
-
import {
|
|
15
|
+
import { Dialog, DialogContent, DialogTitle, DialogDescription } from '../../../components/ui/dialog';
|
|
16
|
+
import { io } from '../../../io';
|
|
17
|
+
|
|
18
|
+
/** 既存のジョブ応答処理へ、利用側IOのJSONを渡す。HTTPの実装は利用側だけが持つ。 */
|
|
19
|
+
async function proposalRequest(path: string, init?: RequestInit): Promise<Response> {
|
|
20
|
+
const data = await io().apiFetch!(path, init);
|
|
21
|
+
return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } });
|
|
22
|
+
}
|
|
16
23
|
import { X, Sparkles, Loader2, Check, AlertTriangle } from 'lucide-react';
|
|
17
24
|
import { useEditorContext } from '../../EditorContext';
|
|
18
25
|
|
|
@@ -61,19 +68,13 @@ export function PptDesignProposals({
|
|
|
61
68
|
return () => { aliveRef.current = false; };
|
|
62
69
|
}, []);
|
|
63
70
|
|
|
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
71
|
/** ジョブが片付くまで status を叩き続ける */
|
|
71
72
|
const poll = useCallback(
|
|
72
73
|
async <T,>(url: string, onTick: (v: T & { state: string; message?: string }) => void) => {
|
|
73
74
|
for (;;) {
|
|
74
75
|
if (!aliveRef.current) return null;
|
|
75
76
|
await new Promise((r) => setTimeout(r, 2500));
|
|
76
|
-
const res = await
|
|
77
|
+
const res = await proposalRequest(url).catch(() => null);
|
|
77
78
|
if (!res || !res.ok) continue;
|
|
78
79
|
const v = (await readApiJson<T & { state: string; message?: string; error?: string }>(res));
|
|
79
80
|
onTick(v);
|
|
@@ -88,7 +89,7 @@ export function PptDesignProposals({
|
|
|
88
89
|
setPatterns([]);
|
|
89
90
|
setGenState('running');
|
|
90
91
|
setGenMessage('ジョブを開始しています…');
|
|
91
|
-
const res = await
|
|
92
|
+
const res = await proposalRequest('/__design-gen', {
|
|
92
93
|
method: 'POST',
|
|
93
94
|
headers: { 'Content-Type': 'application/json' },
|
|
94
95
|
body: JSON.stringify({ page, instruction, count }),
|
|
@@ -119,7 +120,7 @@ export function PptDesignProposals({
|
|
|
119
120
|
setError('');
|
|
120
121
|
setApplyingUrl(url);
|
|
121
122
|
setApplyMessage('ジョブを開始しています…');
|
|
122
|
-
const res = await
|
|
123
|
+
const res = await proposalRequest('/__design-apply', {
|
|
123
124
|
method: 'POST',
|
|
124
125
|
headers: { 'Content-Type': 'application/json' },
|
|
125
126
|
body: JSON.stringify({ page, image: url, note: instruction }),
|
|
@@ -161,21 +162,16 @@ export function PptDesignProposals({
|
|
|
161
162
|
|
|
162
163
|
const busy = genState === 'running' || !!applyingUrl;
|
|
163
164
|
|
|
164
|
-
return
|
|
165
|
-
<
|
|
166
|
-
className="
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
>
|
|
165
|
+
return (
|
|
166
|
+
<Dialog open onOpenChange={(open) => { if (!open && !busy) onClose(); }}>
|
|
167
|
+
<DialogContent data-design-proposals="1" className="flex w-[min(1100px,95vw)] max-w-none flex-col p-0 [&>button:last-child]:hidden"
|
|
168
|
+
onEscapeKeyDown={(e) => { if (busy) e.preventDefault(); }}
|
|
169
|
+
onInteractOutside={(e) => { if (busy) e.preventDefault(); }}>
|
|
170
|
+
<DialogDescription className="sr-only">スライドのデザイン案を生成して選択します。</DialogDescription>
|
|
175
171
|
{/* 見出し */}
|
|
176
172
|
<div className="flex items-center gap-2 border-b px-4 py-3" style={{ borderColor: pal.border }}>
|
|
177
173
|
<Sparkles className="h-4 w-4" />
|
|
178
|
-
<
|
|
174
|
+
<DialogTitle className="text-base">デザイン提案</DialogTitle>
|
|
179
175
|
<span className="text-[11px]" style={{ color: pal.sub }}>
|
|
180
176
|
{page}枚目のスライドを、内容はそのままに別の見せ方で提案します
|
|
181
177
|
</span>
|
|
@@ -218,7 +214,7 @@ export function PptDesignProposals({
|
|
|
218
214
|
onClick={() => void generate()}
|
|
219
215
|
disabled={busy}
|
|
220
216
|
className="flex h-[30px] items-center gap-1.5 rounded px-3 text-[12px] font-medium"
|
|
221
|
-
style={{ backgroundColor: busy ? pal.disabled : '
|
|
217
|
+
style={{ backgroundColor: busy ? pal.disabled : 'var(--ed-accent-strong)', color: '#fff' }}
|
|
222
218
|
>
|
|
223
219
|
{genState === 'running' ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Sparkles className="h-3.5 w-3.5" />}
|
|
224
220
|
生成
|
|
@@ -268,8 +264,7 @@ export function PptDesignProposals({
|
|
|
268
264
|
<div className="border-t px-4 py-2 text-[11px]" style={{ borderColor: pal.border, color: pal.sub }}>
|
|
269
265
|
適用するとスライドの実装(TSX)が書き直されます。描画できない結果になった場合は自動で元に戻します。
|
|
270
266
|
</div>
|
|
271
|
-
</
|
|
272
|
-
</
|
|
273
|
-
document.body,
|
|
267
|
+
</DialogContent>
|
|
268
|
+
</Dialog>
|
|
274
269
|
);
|
|
275
270
|
}
|