@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,589 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PowerPoint風UIのコメント機能(実機の「モダンコメント」に寄せたフル実装)。
|
|
5
|
+
*
|
|
6
|
+
* - スレッド形式(返信・解決/再開・削除)。deck.json のエントリに永続化
|
|
7
|
+
* - 要素アンカー: 追加時に要素を選択していれば、その要素の刻印(data-gg-src)に紐づき、
|
|
8
|
+
* キャンバス上にコメントバブル(マーカー)が出る。クリックでパネルの該当スレッドへ
|
|
9
|
+
* - マーカーは iframe 内のオーバーレイ層(.gg-comment-layer)に描く。
|
|
10
|
+
* 保存時に丸ごと剥がされるため、上書きHTML・原本TSXには一切混入しない
|
|
11
|
+
* - 名前は localStorage に記憶(ローカルツールなのでアカウントは要求しない)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
15
|
+
import { Check, CornerUpLeft, Loader2, MapPin, RotateCcw, Send, Sparkles, Trash2, X } from 'lucide-react';
|
|
16
|
+
import { readApiJson } from '../../utils/api-json';
|
|
17
|
+
import { useEditorContext } from '../../EditorContext';
|
|
18
|
+
import { useDeck, refreshDeck } from '../../../components/viewer/useDeck';
|
|
19
|
+
import { commentAction, type SlideComment } from '../../../lib/deck';
|
|
20
|
+
import { can } from '../../../io';
|
|
21
|
+
import { PPT_PALETTES, type PptTheme } from './PptChrome';
|
|
22
|
+
|
|
23
|
+
const AUTHOR_KEY = 'gg-editor:comment-author';
|
|
24
|
+
|
|
25
|
+
export function loadAuthor(): string {
|
|
26
|
+
try {
|
|
27
|
+
return localStorage.getItem(AUTHOR_KEY) || '';
|
|
28
|
+
} catch {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 未解決コメント数(サムネイルのバッジ用) */
|
|
34
|
+
export function unresolvedCount(comments?: SlideComment[]): number {
|
|
35
|
+
return (comments ?? []).filter((c) => !c.resolved).length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const fmtTime = (iso: string) => {
|
|
39
|
+
const d = new Date(iso);
|
|
40
|
+
if (Number.isNaN(d.getTime())) return '';
|
|
41
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
42
|
+
return `${d.getMonth() + 1}/${d.getDate()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** 著者アイコン(頭文字の丸)。同じ名前は同じ色になる */
|
|
46
|
+
function Avatar({ name }: { name: string }) {
|
|
47
|
+
const colors = ['#0F6CBD', '#7A7574', '#038387', '#C239B3', '#CA5010', '#498205'];
|
|
48
|
+
let h = 0;
|
|
49
|
+
for (const ch of name) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
|
|
50
|
+
return (
|
|
51
|
+
<span
|
|
52
|
+
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[11px] font-bold text-white"
|
|
53
|
+
style={{ backgroundColor: colors[h % colors.length] }}
|
|
54
|
+
>
|
|
55
|
+
{(name || '?').slice(0, 1)}
|
|
56
|
+
</span>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ============================ キャンバス上のマーカー ============================ */
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* アンカー付きコメントのバブルを iframe 内へ描く。
|
|
64
|
+
* 座標はアートボード座標(要素の offset 系)なので、ズーム・パンに自動追従する。
|
|
65
|
+
*/
|
|
66
|
+
export function useCommentMarkers({
|
|
67
|
+
page,
|
|
68
|
+
active,
|
|
69
|
+
onOpenThread,
|
|
70
|
+
}: {
|
|
71
|
+
page: number;
|
|
72
|
+
active: boolean;
|
|
73
|
+
onOpenThread: (commentId: string) => void;
|
|
74
|
+
}) {
|
|
75
|
+
const { getIframeDoc } = useEditorContext();
|
|
76
|
+
const deck = useDeck();
|
|
77
|
+
const comments = deck.slides[page - 1]?.comments ?? [];
|
|
78
|
+
const onOpenRef = useRef(onOpenThread);
|
|
79
|
+
onOpenRef.current = onOpenThread;
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!active) return;
|
|
83
|
+
let alive = true;
|
|
84
|
+
|
|
85
|
+
const render = () => {
|
|
86
|
+
if (!alive) return;
|
|
87
|
+
const doc = getIframeDoc();
|
|
88
|
+
const artboard = doc?.getElementById('artboard');
|
|
89
|
+
if (!doc || !artboard) return;
|
|
90
|
+
let layer = artboard.querySelector<HTMLElement>(':scope > .gg-comment-layer');
|
|
91
|
+
if (!layer) {
|
|
92
|
+
layer = doc.createElement('div');
|
|
93
|
+
layer.className = 'gg-comment-layer';
|
|
94
|
+
// 保存時に丸ごと剥がされる前提の表示専用レイヤー
|
|
95
|
+
layer.style.cssText = 'position:absolute;inset:0;pointer-events:none;z-index:9000;';
|
|
96
|
+
artboard.appendChild(layer);
|
|
97
|
+
}
|
|
98
|
+
layer.innerHTML = '';
|
|
99
|
+
const anchored = comments.filter((c) => c.anchorSrc && !c.resolved);
|
|
100
|
+
for (const c of anchored) {
|
|
101
|
+
const target = artboard.querySelector<HTMLElement>(
|
|
102
|
+
`[data-gg-src="${CSS.escape(c.anchorSrc!)}"]`,
|
|
103
|
+
);
|
|
104
|
+
const bubble = doc.createElement('button');
|
|
105
|
+
bubble.setAttribute('data-comment-id', c.id);
|
|
106
|
+
bubble.title = `${c.author}: ${c.text.slice(0, 60)}`;
|
|
107
|
+
bubble.textContent = (c.author || '?').slice(0, 1);
|
|
108
|
+
const x = target ? target.offsetLeft + target.offsetWidth - 6 : 1860;
|
|
109
|
+
const y = target ? Math.max(0, target.offsetTop - 10) : 20;
|
|
110
|
+
bubble.style.cssText =
|
|
111
|
+
`position:absolute;left:${x}px;top:${y}px;width:34px;height:34px;` +
|
|
112
|
+
'border-radius:50% 50% 50% 4px;background:#0F6CBD;color:#fff;font-size:15px;' +
|
|
113
|
+
'font-weight:700;border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,.35);' +
|
|
114
|
+
'cursor:pointer;pointer-events:auto;display:flex;align-items:center;justify-content:center;';
|
|
115
|
+
bubble.addEventListener('mousedown', (e) => {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
e.stopPropagation();
|
|
118
|
+
});
|
|
119
|
+
bubble.addEventListener('click', (e) => {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
e.stopPropagation();
|
|
122
|
+
onOpenRef.current(c.id);
|
|
123
|
+
});
|
|
124
|
+
layer.appendChild(bubble);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
render();
|
|
129
|
+
// 要素の移動・追加編集にゆるく追従する(コメント数は少ないので軽い)
|
|
130
|
+
const timer = setInterval(render, 900);
|
|
131
|
+
return () => {
|
|
132
|
+
alive = false;
|
|
133
|
+
clearInterval(timer);
|
|
134
|
+
const layer = getIframeDoc()?.querySelector('.gg-comment-layer');
|
|
135
|
+
layer?.remove();
|
|
136
|
+
};
|
|
137
|
+
}, [active, page, getIframeDoc, JSON.stringify(comments.map((c) => [c.id, c.anchorSrc, c.resolved, c.author]))]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ============================ 右パネル ============================ */
|
|
141
|
+
|
|
142
|
+
export function PptCommentsPanel({
|
|
143
|
+
page,
|
|
144
|
+
theme,
|
|
145
|
+
onClose,
|
|
146
|
+
focusSignal,
|
|
147
|
+
activeThreadId,
|
|
148
|
+
onActiveThread,
|
|
149
|
+
}: {
|
|
150
|
+
page: number;
|
|
151
|
+
theme: PptTheme;
|
|
152
|
+
onClose: () => void;
|
|
153
|
+
/** 値が変わったら新規コメント入力へフォーカス */
|
|
154
|
+
focusSignal: number;
|
|
155
|
+
activeThreadId: string | null;
|
|
156
|
+
onActiveThread: (id: string | null) => void;
|
|
157
|
+
}) {
|
|
158
|
+
const pal = PPT_PALETTES[theme];
|
|
159
|
+
const deck = useDeck();
|
|
160
|
+
const { getIframeDoc, selectedElement } = useEditorContext();
|
|
161
|
+
const comments = deck.slides[page - 1]?.comments ?? [];
|
|
162
|
+
|
|
163
|
+
const [author, setAuthor] = useState(loadAuthor);
|
|
164
|
+
const [draft, setDraft] = useState('');
|
|
165
|
+
const [replyFor, setReplyFor] = useState<string | null>(null);
|
|
166
|
+
const [replyDraft, setReplyDraft] = useState('');
|
|
167
|
+
const [showResolved, setShowResolved] = useState(false);
|
|
168
|
+
const [busy, setBusy] = useState(false);
|
|
169
|
+
const composerRef = useRef<HTMLTextAreaElement>(null);
|
|
170
|
+
const threadRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
|
171
|
+
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (focusSignal > 0) composerRef.current?.focus();
|
|
174
|
+
}, [focusSignal]);
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (activeThreadId) {
|
|
178
|
+
threadRefs.current[activeThreadId]?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
179
|
+
}
|
|
180
|
+
}, [activeThreadId]);
|
|
181
|
+
|
|
182
|
+
const saveAuthor = (v: string) => {
|
|
183
|
+
setAuthor(v);
|
|
184
|
+
try {
|
|
185
|
+
localStorage.setItem(AUTHOR_KEY, v);
|
|
186
|
+
} catch { /* 記憶できなくても続行 */ }
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 「AIで修正」の進行状況(スレッドIDごと)。
|
|
191
|
+
*
|
|
192
|
+
* 共通の run()(busyで全ボタンを止め、失敗をalertで出す)には載せない。
|
|
193
|
+
* このジョブは1〜3分かかるため、走らせている間ほかのコメント操作まで
|
|
194
|
+
* 止まってしまうし、結果はスレッドの中に出したいため。
|
|
195
|
+
*/
|
|
196
|
+
const [fixJobs, setFixJobs] = useState<Record<string, { message: string; error?: string }>>({});
|
|
197
|
+
|
|
198
|
+
const startFix = useCallback(
|
|
199
|
+
async (commentId: string) => {
|
|
200
|
+
setFixJobs((prev) => ({ ...prev, [commentId]: { message: '開始中…' } }));
|
|
201
|
+
try {
|
|
202
|
+
const res = await fetch('/__comment-fix', {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: { 'content-type': 'application/json' },
|
|
205
|
+
body: JSON.stringify({ page, commentId }),
|
|
206
|
+
});
|
|
207
|
+
const { jobId, error } = await readApiJson<{ jobId?: string; error?: string }>(res);
|
|
208
|
+
if (!jobId) throw new Error(error ?? '開始できませんでした');
|
|
209
|
+
|
|
210
|
+
const startedAt = Date.now();
|
|
211
|
+
while (Date.now() - startedAt < 8 * 60 * 1000) {
|
|
212
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
213
|
+
const st = await readApiJson<{ state: string; message?: string; error?: string }>(
|
|
214
|
+
await fetch(`/__comment-fix/status/${jobId}`),
|
|
215
|
+
);
|
|
216
|
+
if (st.state === 'running') {
|
|
217
|
+
setFixJobs((prev) => ({ ...prev, [commentId]: { message: st.message || '実行中…' } }));
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (st.state === 'done') {
|
|
221
|
+
// 原本TSXが変わったので、画面ごと読み直す。
|
|
222
|
+
// (このページのモジュールは差分更新されないため、部分再描画では新しい姿にならない)
|
|
223
|
+
setFixJobs((prev) => ({ ...prev, [commentId]: { message: '反映しました。読み込み中…' } }));
|
|
224
|
+
window.location.reload();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
throw new Error(st.error || '修正に失敗しました');
|
|
228
|
+
}
|
|
229
|
+
throw new Error('時間内に終わりませんでした');
|
|
230
|
+
} catch (e) {
|
|
231
|
+
setFixJobs((prev) => ({
|
|
232
|
+
...prev,
|
|
233
|
+
[commentId]: { message: '', error: String(e instanceof Error ? e.message : e).slice(0, 200) },
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
[page],
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
/** 全スライドの未解決コメントを一括修正。進捗はヘッダー下に出す */
|
|
241
|
+
const [fixAll, setFixAll] = useState<{ running: boolean; message: string; error?: string } | null>(null);
|
|
242
|
+
const deckAll = useDeck();
|
|
243
|
+
const unresolvedTotal = deckAll.slides.reduce((n, sl) => n + unresolvedCount(sl.comments), 0);
|
|
244
|
+
|
|
245
|
+
const startFixAll = useCallback(async () => {
|
|
246
|
+
if (fixAll?.running) return;
|
|
247
|
+
if (!window.confirm(
|
|
248
|
+
`未解決のコメント${unresolvedTotal}件を、スライド順にAIが修正します。\n` +
|
|
249
|
+
'1件あたり1〜3分かかります。実行しますか?\n' +
|
|
250
|
+
'(各スレッドには対応内容が返信として残ります)',
|
|
251
|
+
)) return;
|
|
252
|
+
setFixAll({ running: true, message: '開始中…' });
|
|
253
|
+
try {
|
|
254
|
+
const res = await fetch('/__comment-fix', {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: { 'content-type': 'application/json' },
|
|
257
|
+
body: JSON.stringify({ all: true }),
|
|
258
|
+
});
|
|
259
|
+
const { jobId, error } = await readApiJson<{ jobId?: string; error?: string }>(res);
|
|
260
|
+
if (!jobId) throw new Error(error ?? '開始できませんでした');
|
|
261
|
+
for (;;) {
|
|
262
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
263
|
+
const st = await readApiJson<{ state: string; message?: string; error?: string }>(
|
|
264
|
+
await fetch(`/__comment-fix/status/${jobId}`),
|
|
265
|
+
);
|
|
266
|
+
if (st.state === 'running') {
|
|
267
|
+
setFixAll({ running: true, message: st.message || '実行中…' });
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (st.state === 'done') {
|
|
271
|
+
setFixAll({ running: false, message: st.message || '完了しました。読み込み中…' });
|
|
272
|
+
window.location.reload();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
throw new Error(st.error || '一括修正に失敗しました');
|
|
276
|
+
}
|
|
277
|
+
} catch (e) {
|
|
278
|
+
setFixAll({ running: false, message: '', error: String(e instanceof Error ? e.message : e).slice(0, 200) });
|
|
279
|
+
}
|
|
280
|
+
}, [fixAll?.running, unresolvedTotal]);
|
|
281
|
+
|
|
282
|
+
const run = useCallback(async (fn: () => Promise<unknown>) => {
|
|
283
|
+
if (busy) return;
|
|
284
|
+
setBusy(true);
|
|
285
|
+
try {
|
|
286
|
+
await fn();
|
|
287
|
+
await refreshDeck();
|
|
288
|
+
} catch (e) {
|
|
289
|
+
window.alert(`コメント操作に失敗しました: ${String(e).slice(0, 120)}`);
|
|
290
|
+
} finally {
|
|
291
|
+
setBusy(false);
|
|
292
|
+
}
|
|
293
|
+
}, [busy]);
|
|
294
|
+
|
|
295
|
+
/** 選択中の要素からアンカーを拾う(選択なしならスライド全体) */
|
|
296
|
+
const currentAnchor = useMemo(() => {
|
|
297
|
+
const doc = getIframeDoc();
|
|
298
|
+
if (!doc || !selectedElement) return null;
|
|
299
|
+
const el = doc.querySelector<HTMLElement>(`[data-element-id="${selectedElement.id}"]`);
|
|
300
|
+
const src = el?.getAttribute('data-gg-src');
|
|
301
|
+
if (!el || !src) return null;
|
|
302
|
+
const label = (el.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 24);
|
|
303
|
+
return { src, label };
|
|
304
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
305
|
+
}, [selectedElement, getIframeDoc]);
|
|
306
|
+
|
|
307
|
+
const post = () => {
|
|
308
|
+
const text = draft.trim();
|
|
309
|
+
if (!text) return;
|
|
310
|
+
void run(() =>
|
|
311
|
+
commentAction(page, {
|
|
312
|
+
action: 'add',
|
|
313
|
+
author: author.trim() || 'ゲスト',
|
|
314
|
+
text,
|
|
315
|
+
anchorSrc: currentAnchor?.src,
|
|
316
|
+
anchorLabel: currentAnchor?.label,
|
|
317
|
+
}),
|
|
318
|
+
).then(() => setDraft(''));
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const open = comments.filter((c) => !c.resolved);
|
|
322
|
+
const resolved = comments.filter((c) => c.resolved);
|
|
323
|
+
|
|
324
|
+
const Thread = ({ c }: { c: SlideComment }) => (
|
|
325
|
+
<div
|
|
326
|
+
ref={(el) => { threadRefs.current[c.id] = el; }}
|
|
327
|
+
className="rounded-lg border p-2.5"
|
|
328
|
+
style={{
|
|
329
|
+
borderColor: activeThreadId === c.id ? '#0F6CBD' : pal.border,
|
|
330
|
+
backgroundColor: pal.control,
|
|
331
|
+
opacity: c.resolved ? 0.72 : 1,
|
|
332
|
+
}}
|
|
333
|
+
onClick={() => onActiveThread(c.id)}
|
|
334
|
+
>
|
|
335
|
+
<div className="flex items-center gap-2">
|
|
336
|
+
<Avatar name={c.author} />
|
|
337
|
+
<div className="min-w-0 flex-1">
|
|
338
|
+
<div className="truncate text-[12px] font-medium" style={{ color: pal.text }}>{c.author}</div>
|
|
339
|
+
<div className="text-[10px]" style={{ color: pal.sub }}>{fmtTime(c.createdAt)}</div>
|
|
340
|
+
</div>
|
|
341
|
+
{c.resolved ? (
|
|
342
|
+
<button
|
|
343
|
+
title="スレッドを再開"
|
|
344
|
+
onClick={(e) => { e.stopPropagation(); void run(() => commentAction(page, { action: 'resolve', commentId: c.id, resolved: false })); }}
|
|
345
|
+
className="rounded p-1" style={{ color: pal.sub }}
|
|
346
|
+
>
|
|
347
|
+
<RotateCcw className="h-3.5 w-3.5" />
|
|
348
|
+
</button>
|
|
349
|
+
) : (
|
|
350
|
+
<button
|
|
351
|
+
title="解決済みにする"
|
|
352
|
+
onClick={(e) => { e.stopPropagation(); void run(() => commentAction(page, { action: 'resolve', commentId: c.id, resolved: true })); }}
|
|
353
|
+
className="rounded p-1" style={{ color: pal.sub }}
|
|
354
|
+
>
|
|
355
|
+
<Check className="h-3.5 w-3.5" />
|
|
356
|
+
</button>
|
|
357
|
+
)}
|
|
358
|
+
<button
|
|
359
|
+
title="スレッドを削除"
|
|
360
|
+
onClick={(e) => {
|
|
361
|
+
e.stopPropagation();
|
|
362
|
+
if (window.confirm('このコメントスレッドを削除しますか?')) {
|
|
363
|
+
void run(() => commentAction(page, { action: 'delete', commentId: c.id }));
|
|
364
|
+
}
|
|
365
|
+
}}
|
|
366
|
+
className="rounded p-1" style={{ color: pal.sub }}
|
|
367
|
+
>
|
|
368
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
369
|
+
</button>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
{c.anchorSrc && (
|
|
373
|
+
<div className="mt-1.5 flex items-center gap-1 text-[10px]" style={{ color: '#0F6CBD' }}>
|
|
374
|
+
<MapPin className="h-3 w-3" />
|
|
375
|
+
{c.anchorLabel ? `「${c.anchorLabel}」` : '要素に添付'}
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
|
|
379
|
+
<p className="mt-1.5 whitespace-pre-wrap text-[12.5px] leading-relaxed" style={{ color: pal.text }}>
|
|
380
|
+
{c.text}
|
|
381
|
+
</p>
|
|
382
|
+
|
|
383
|
+
{(c.replies ?? []).map((r) => (
|
|
384
|
+
<div key={r.id} className="mt-2 border-l-2 pl-2.5" style={{ borderColor: pal.border }}>
|
|
385
|
+
<div className="flex items-center gap-1.5">
|
|
386
|
+
<Avatar name={r.author} />
|
|
387
|
+
<span className="text-[11px] font-medium" style={{ color: pal.text }}>{r.author}</span>
|
|
388
|
+
<span className="text-[10px]" style={{ color: pal.sub }}>{fmtTime(r.createdAt)}</span>
|
|
389
|
+
</div>
|
|
390
|
+
<p className="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed" style={{ color: pal.text }}>{r.text}</p>
|
|
391
|
+
</div>
|
|
392
|
+
))}
|
|
393
|
+
|
|
394
|
+
{/* AIで修正: 指摘の内容どおりに原本TSXを直す。完了後はリロードして新しい姿を出す */}
|
|
395
|
+
{!c.resolved && (
|
|
396
|
+
<div className="mt-2">
|
|
397
|
+
{fixJobs[c.id] && !fixJobs[c.id].error ? (
|
|
398
|
+
<div className="flex items-center gap-1.5 text-[11px]" style={{ color: pal.sub }}>
|
|
399
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
400
|
+
{fixJobs[c.id].message}
|
|
401
|
+
</div>
|
|
402
|
+
) : (
|
|
403
|
+
can('apiFetch') && (
|
|
404
|
+
<button
|
|
405
|
+
onClick={(e) => {
|
|
406
|
+
e.stopPropagation();
|
|
407
|
+
void startFix(c.id);
|
|
408
|
+
}}
|
|
409
|
+
title="この指摘のとおりにAIが原本(TSX)を修正します"
|
|
410
|
+
className="flex items-center gap-1 rounded border px-1.5 py-1 text-[11px] transition-colors"
|
|
411
|
+
style={{ borderColor: pal.border, color: '#8B5CF6' }}
|
|
412
|
+
>
|
|
413
|
+
<Sparkles className="h-3 w-3" />
|
|
414
|
+
AIで修正
|
|
415
|
+
</button>
|
|
416
|
+
)
|
|
417
|
+
)}
|
|
418
|
+
{fixJobs[c.id]?.error && (
|
|
419
|
+
<p className="mt-1 text-[11px] leading-snug" style={{ color: '#d13438' }}>
|
|
420
|
+
{fixJobs[c.id].error}
|
|
421
|
+
</p>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
425
|
+
|
|
426
|
+
{replyFor === c.id ? (
|
|
427
|
+
<div className="mt-2 flex items-end gap-1.5">
|
|
428
|
+
<textarea
|
|
429
|
+
autoFocus
|
|
430
|
+
value={replyDraft}
|
|
431
|
+
onChange={(e) => setReplyDraft(e.target.value)}
|
|
432
|
+
onKeyDown={(e) => {
|
|
433
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
434
|
+
e.preventDefault();
|
|
435
|
+
const t = replyDraft.trim();
|
|
436
|
+
if (t) {
|
|
437
|
+
void run(() => commentAction(page, { action: 'reply', commentId: c.id, author: author.trim() || 'ゲスト', text: t }))
|
|
438
|
+
.then(() => { setReplyDraft(''); setReplyFor(null); });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}}
|
|
442
|
+
rows={2}
|
|
443
|
+
placeholder="返信… (⌘+Enterで送信)"
|
|
444
|
+
className="min-h-[40px] flex-1 resize-none rounded border p-1.5 text-[12px] outline-none"
|
|
445
|
+
style={{ backgroundColor: pal.chrome, borderColor: pal.border, color: pal.text }}
|
|
446
|
+
/>
|
|
447
|
+
<button
|
|
448
|
+
title="返信を送信"
|
|
449
|
+
onClick={() => {
|
|
450
|
+
const t = replyDraft.trim();
|
|
451
|
+
if (t) {
|
|
452
|
+
void run(() => commentAction(page, { action: 'reply', commentId: c.id, author: author.trim() || 'ゲスト', text: t }))
|
|
453
|
+
.then(() => { setReplyDraft(''); setReplyFor(null); });
|
|
454
|
+
}
|
|
455
|
+
}}
|
|
456
|
+
className="rounded p-1.5" style={{ color: '#0F6CBD' }}
|
|
457
|
+
>
|
|
458
|
+
<Send className="h-4 w-4" />
|
|
459
|
+
</button>
|
|
460
|
+
</div>
|
|
461
|
+
) : (
|
|
462
|
+
!c.resolved && (
|
|
463
|
+
<button
|
|
464
|
+
onClick={(e) => { e.stopPropagation(); setReplyFor(c.id); setReplyDraft(''); }}
|
|
465
|
+
className="mt-1.5 flex items-center gap-1 text-[11px]"
|
|
466
|
+
style={{ color: '#0F6CBD' }}
|
|
467
|
+
>
|
|
468
|
+
<CornerUpLeft className="h-3 w-3" />
|
|
469
|
+
返信
|
|
470
|
+
</button>
|
|
471
|
+
)
|
|
472
|
+
)}
|
|
473
|
+
</div>
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
return (
|
|
477
|
+
<div
|
|
478
|
+
className="flex w-[292px] shrink-0 flex-col border-l"
|
|
479
|
+
style={{ backgroundColor: PPT_PALETTES[theme].rail, borderColor: pal.border }}
|
|
480
|
+
>
|
|
481
|
+
<div className="flex items-center gap-2 border-b px-3 py-2" style={{ borderColor: pal.border }}>
|
|
482
|
+
<span className="text-[13px] font-medium" style={{ color: pal.text }}>コメント</span>
|
|
483
|
+
<span className="truncate text-[11px]" style={{ color: pal.sub }}>
|
|
484
|
+
{deckAll.slides[page - 1]?.title ?? `ページ ${page}`}
|
|
485
|
+
</span>
|
|
486
|
+
{unresolvedTotal > 0 && !fixAll?.running && can('apiFetch') && (
|
|
487
|
+
<button
|
|
488
|
+
onClick={() => void startFixAll()}
|
|
489
|
+
title="全スライドの未解決コメントをAIが順に修正し、各スレッドへ対応報告を返信します"
|
|
490
|
+
className="ml-2 flex items-center gap-1 rounded border px-1.5 py-0.5 text-[11px]"
|
|
491
|
+
style={{ borderColor: pal.border, color: pal.text }}
|
|
492
|
+
>
|
|
493
|
+
<Sparkles className="h-3 w-3" />
|
|
494
|
+
すべてAIで修正({unresolvedTotal})
|
|
495
|
+
</button>
|
|
496
|
+
)}
|
|
497
|
+
<button onClick={onClose} className="ml-auto rounded p-1" style={{ color: pal.sub }} title="閉じる">
|
|
498
|
+
<X className="h-4 w-4" />
|
|
499
|
+
</button>
|
|
500
|
+
</div>
|
|
501
|
+
{(fixAll?.running || fixAll?.error) && (
|
|
502
|
+
<div className="border-b px-3 py-1.5 text-[11px]" style={{ borderColor: pal.border, color: fixAll.error ? '#f66' : pal.sub }}>
|
|
503
|
+
{fixAll.error ? `一括修正に失敗: ${fixAll.error}` : `⏳ ${fixAll.message}`}
|
|
504
|
+
</div>
|
|
505
|
+
)}
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
{/* 新規コメント */}
|
|
509
|
+
<div className="border-b p-2.5" style={{ borderColor: pal.border }}>
|
|
510
|
+
<div className="flex items-center gap-1.5">
|
|
511
|
+
<Avatar name={author || 'ゲ'} />
|
|
512
|
+
<input
|
|
513
|
+
value={author}
|
|
514
|
+
onChange={(e) => saveAuthor(e.target.value)}
|
|
515
|
+
placeholder="名前(記憶されます)"
|
|
516
|
+
className="h-6 flex-1 rounded border px-1.5 text-[11px] outline-none"
|
|
517
|
+
style={{ backgroundColor: pal.control, borderColor: pal.border, color: pal.text }}
|
|
518
|
+
/>
|
|
519
|
+
</div>
|
|
520
|
+
{currentAnchor && (
|
|
521
|
+
<div className="mt-1.5 flex items-center gap-1 text-[10px]" style={{ color: '#0F6CBD' }}>
|
|
522
|
+
<MapPin className="h-3 w-3" />
|
|
523
|
+
選択中の「{currentAnchor.label || '要素'}」に添付されます
|
|
524
|
+
</div>
|
|
525
|
+
)}
|
|
526
|
+
<div className="mt-1.5 flex items-end gap-1.5">
|
|
527
|
+
<textarea
|
|
528
|
+
ref={composerRef}
|
|
529
|
+
value={draft}
|
|
530
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
531
|
+
onKeyDown={(e) => {
|
|
532
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
533
|
+
e.preventDefault();
|
|
534
|
+
post();
|
|
535
|
+
}
|
|
536
|
+
}}
|
|
537
|
+
rows={2}
|
|
538
|
+
placeholder={currentAnchor ? 'コメント… (⌘+Enter)' : 'この内容へのコメント… (⌘+Enter)'}
|
|
539
|
+
className="min-h-[44px] flex-1 resize-none rounded border p-1.5 text-[12px] outline-none"
|
|
540
|
+
style={{ backgroundColor: pal.control, borderColor: pal.border, color: pal.text }}
|
|
541
|
+
/>
|
|
542
|
+
<button
|
|
543
|
+
onClick={post}
|
|
544
|
+
disabled={busy || !draft.trim()}
|
|
545
|
+
title="コメントを投稿"
|
|
546
|
+
className="rounded p-1.5 disabled:opacity-40"
|
|
547
|
+
style={{ color: '#0F6CBD' }}
|
|
548
|
+
>
|
|
549
|
+
<Send className="h-4 w-4" />
|
|
550
|
+
</button>
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
|
|
554
|
+
{/* スレッド一覧 */}
|
|
555
|
+
<div className="flex-1 space-y-2 overflow-y-auto p-2.5">
|
|
556
|
+
{open.length === 0 && resolved.length === 0 && (
|
|
557
|
+
<p className="px-1 pt-2 text-[12px]" style={{ color: pal.sub }}>
|
|
558
|
+
まだコメントはありません。要素を選択して投稿すると、その要素に吹き出しが付きます。
|
|
559
|
+
</p>
|
|
560
|
+
)}
|
|
561
|
+
{open.map((c) => <Thread key={c.id} c={c} />)}
|
|
562
|
+
|
|
563
|
+
{resolved.length > 0 && (
|
|
564
|
+
<button
|
|
565
|
+
onClick={() => setShowResolved((v) => !v)}
|
|
566
|
+
className="w-full pt-1 text-left text-[11px]"
|
|
567
|
+
style={{ color: pal.sub }}
|
|
568
|
+
>
|
|
569
|
+
{showResolved ? '▾' : '▸'} 解決済み ({resolved.length})
|
|
570
|
+
</button>
|
|
571
|
+
)}
|
|
572
|
+
{showResolved && resolved.map((c) => <Thread key={c.id} c={c} />)}
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
/** マーカーだけを常駐させる薄いホスト(パネルが閉じていても吹き出しは見える) */
|
|
580
|
+
export function PptCommentMarkers({
|
|
581
|
+
page,
|
|
582
|
+
onOpenThread,
|
|
583
|
+
}: {
|
|
584
|
+
page: number;
|
|
585
|
+
onOpenThread: (commentId: string) => void;
|
|
586
|
+
}) {
|
|
587
|
+
useCommentMarkers({ page, active: true, onOpenThread });
|
|
588
|
+
return null;
|
|
589
|
+
}
|