@open-press/cli 0.7.0 → 0.8.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 +6 -1
- package/package.json +1 -1
- package/template/core/AGENTS.md +126 -0
- package/template/core/CHANGELOG.md +65 -0
- package/template/core/engine/commands/dev.mjs +2 -2
- package/template/core/engine/commands/upgrade.mjs +47 -5
- package/template/core/engine/output/chrome-pdf.mjs +18 -3
- package/template/core/engine/output/static-server.mjs +39 -0
- package/template/core/engine/react/comment-endpoint.mjs +13 -39
- package/template/core/engine/react/comment-marker.mjs +30 -6
- package/template/core/engine/react/document-entry.mjs +11 -0
- package/template/core/engine/react/document-export.mjs +45 -5
- package/template/core/engine/react/http-json.mjs +24 -0
- package/template/core/engine/react/mdx-compile.mjs +187 -3
- package/template/core/engine/react/measurement-css.mjs +93 -1
- package/template/core/engine/react/object-entities.mjs +119 -0
- package/template/core/engine/react/pipeline/allocate.mjs +10 -7
- package/template/core/engine/react/pipeline/frame-measurement.mjs +40 -9
- package/template/core/engine/react/project-asset-endpoint.mjs +6 -24
- package/template/core/engine/react/source-edit-endpoint.d.mts +10 -0
- package/template/core/engine/react/source-edit-endpoint.mjs +75 -0
- package/template/core/engine/react/sources/mdx-resolver.mjs +12 -14
- package/template/core/engine/react/style-discovery.mjs +1 -4
- package/template/core/engine/runtime/file-walk.mjs +22 -0
- package/template/core/engine/runtime/inspection.mjs +1 -20
- package/template/core/engine/runtime/path-utils.mjs +20 -0
- package/template/core/engine/runtime/source-text-tools.d.mts +102 -0
- package/template/core/engine/runtime/source-text-tools.mjs +551 -16
- package/template/core/engine/runtime/source-workspace.mjs +4 -31
- package/template/core/package.json +1 -1
- package/template/core/src/main.tsx +2 -2
- package/template/core/src/openpress/{App.tsx → app/OpenPressApp.tsx} +25 -12
- package/template/core/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +10 -7
- package/template/core/src/openpress/app/index.ts +2 -0
- package/template/core/src/openpress/core/Frame.tsx +9 -11
- package/template/core/src/openpress/core/FrameContext.tsx +8 -3
- package/template/core/src/openpress/core/MdxArea.tsx +11 -12
- package/template/core/src/openpress/core/cn.ts +4 -0
- package/template/core/src/openpress/core/index.tsx +2 -1
- package/template/core/src/openpress/core/primitives.tsx +29 -8
- package/template/core/src/openpress/core/types.ts +8 -0
- package/template/core/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
- package/template/core/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
- package/template/core/src/openpress/{types.ts → document-model/documentTypes.ts} +42 -0
- package/template/core/src/openpress/document-model/index.ts +6 -0
- package/template/core/src/openpress/document-model/objectEntityModel.ts +51 -0
- package/template/core/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
- package/template/core/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
- package/template/core/src/openpress/manuscript/index.tsx +49 -7
- package/template/core/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
- package/template/core/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
- package/template/core/src/openpress/reader/index.ts +10 -0
- package/template/core/src/openpress/reader/pageViewportScaleModel.ts +73 -0
- package/template/core/src/openpress/reader/readerTypes.ts +4 -0
- package/template/core/src/openpress/reader/usePageViewportScale.ts +119 -0
- package/template/core/src/openpress/reader/usePanelState.ts +56 -0
- package/template/core/src/openpress/reader/useReaderHashSync.ts +61 -0
- package/template/core/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
- package/template/core/src/openpress/reader/useReaderRuntime.ts +146 -0
- package/template/core/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
- package/template/core/src/openpress/shared/Panel.tsx +77 -0
- package/template/core/src/openpress/shared/index.ts +4 -0
- package/template/core/src/openpress/shared/numberUtils.ts +3 -0
- package/template/core/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
- package/template/core/src/openpress/workbench/Workbench.tsx +407 -0
- package/template/core/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
- package/template/core/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
- package/template/core/src/openpress/workbench/actions/SearchControl.tsx +345 -0
- package/template/core/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
- package/template/core/src/openpress/workbench/actions/index.ts +5 -0
- package/template/core/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
- package/template/core/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
- package/template/core/src/openpress/workbench/dialog/index.ts +1 -0
- package/template/core/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
- package/template/core/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
- package/template/core/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
- package/template/core/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
- package/template/core/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
- package/template/core/src/openpress/workbench/document/index.ts +10 -0
- package/template/core/src/openpress/workbench/index.ts +2 -0
- package/template/core/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
- package/template/core/src/openpress/workbench/inspector/index.ts +5 -0
- package/template/core/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
- package/template/core/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
- package/template/core/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
- package/template/core/src/openpress/workbench/inspector/useInspectorComments.ts +248 -0
- package/template/core/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
- package/template/core/src/openpress/workbench/mentions/index.ts +2 -0
- package/template/core/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
- package/template/core/src/openpress/workbench/panels/Panel.tsx +1 -0
- package/template/core/src/openpress/workbench/panels/PendingCommentsPanel.tsx +76 -0
- package/template/core/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
- package/template/core/src/openpress/workbench/panels/index.ts +3 -0
- package/template/core/src/openpress/workbench/project/ProjectEntryPanel.tsx +523 -0
- package/template/core/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
- package/template/core/src/openpress/workbench/project/index.ts +2 -0
- package/template/core/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
- package/template/core/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
- package/template/core/src/openpress/workbench/shell/index.ts +1 -0
- package/template/core/src/openpress/workbench/workbenchFormatters.ts +120 -0
- package/template/core/src/openpress/workbench/workbenchTypes.ts +35 -0
- package/template/core/src/styles/openpress/print-route.css +0 -2
- package/template/core/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
- package/template/core/src/styles/openpress/public-viewer.css +25 -320
- package/template/core/src/styles/openpress/reader-runtime.css +243 -55
- package/template/core/src/styles/openpress/responsive.css +145 -270
- package/template/core/src/styles/openpress/workbench-panels.css +214 -178
- package/template/core/src/styles/openpress/workbench.css +986 -451
- package/template/core/src/styles/openpress.css +1 -1
- package/template/core/vite.config.ts +50 -0
- package/template/packs/academic-paper/document/chapters/01-introduction/content/01-introduction.mdx +26 -12
- package/template/packs/academic-paper/document/chapters/02-methods/content/01-methods.mdx +37 -17
- package/template/packs/academic-paper/document/chapters/03-results-and-discussion/content/01-results.mdx +34 -16
- package/template/packs/academic-paper/document/chapters/04-acknowledgment/content/01-acknowledgment.mdx +22 -8
- package/template/packs/academic-paper/document/chapters/05-references/content/01-references.mdx +20 -15
- package/template/packs/academic-paper/document/components/Page.tsx +26 -3
- package/template/packs/academic-paper/document/index.tsx +51 -59
- package/template/packs/academic-paper/document/media/figure-placeholder.svg +9 -0
- package/template/packs/academic-paper/document/theme/base/page-contract.css +30 -13
- package/template/packs/academic-paper/document/theme/base/typography.css +30 -33
- package/template/packs/academic-paper/document/theme/page-surfaces/cover.css +74 -47
- package/template/core/src/openpress/inspector.ts +0 -282
- package/template/core/src/openpress/projectWorkspace.tsx +0 -919
- package/template/core/src/openpress/readerRuntime.ts +0 -230
- package/template/core/src/openpress/workbench.tsx +0 -1265
- package/template/core/src/openpress/workbenchTypes.ts +0 -4
- /package/template/core/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
- /package/template/core/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
- /package/template/core/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
- /package/template/core/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
- /package/template/core/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
- /package/template/core/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import {
|
|
2
|
+
memo,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type FormEvent,
|
|
10
|
+
type RefObject,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { ArrowUp, Pencil, Plus, Trash2 } from "lucide-react";
|
|
13
|
+
import { MentionSuggestionList, useComposerMentions } from "../mentions";
|
|
14
|
+
import type {
|
|
15
|
+
InspectorState,
|
|
16
|
+
ObjectSelection,
|
|
17
|
+
} from "./inspectorModel";
|
|
18
|
+
import type { ProjectMentionItem } from "../project";
|
|
19
|
+
import {
|
|
20
|
+
collectInspectorBlockElements,
|
|
21
|
+
createInspectorComposerStyle,
|
|
22
|
+
createInspectorInsertTargets,
|
|
23
|
+
createInspectorMarkerStyle,
|
|
24
|
+
rectToFixedStyle,
|
|
25
|
+
resolveInspectorSelectionRect,
|
|
26
|
+
syncInspectorSelectedBlock,
|
|
27
|
+
} from "./inspectorGeometryModel";
|
|
28
|
+
import {
|
|
29
|
+
getInlineSavedCommentForTarget,
|
|
30
|
+
getInlineSavedCommentMarkers,
|
|
31
|
+
} from "./inlineCommentModel";
|
|
32
|
+
import type {
|
|
33
|
+
InlineSavedComment,
|
|
34
|
+
InlineSavedCommentMarkerEntry,
|
|
35
|
+
InspectorCommentStatus,
|
|
36
|
+
InspectorInsertTargetView,
|
|
37
|
+
InspectorLayerRect,
|
|
38
|
+
} from "../workbenchTypes";
|
|
39
|
+
|
|
40
|
+
type ComposerAction = "add" | "edit" | "delete";
|
|
41
|
+
|
|
42
|
+
const COMPOSER_ACTIONS: Array<{ action: ComposerAction; label: string; icon: typeof Plus; prefix: string }> = [
|
|
43
|
+
{ action: "add", label: "Add", icon: Plus, prefix: "請新增:" },
|
|
44
|
+
{ action: "edit", label: "Edit", icon: Pencil, prefix: "請修改:" },
|
|
45
|
+
{ action: "delete", label: "Remove", icon: Trash2, prefix: "請刪除這個物件。" },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export interface InlineCommentController {
|
|
49
|
+
saved: InlineSavedComment[];
|
|
50
|
+
active: InlineSavedComment | null;
|
|
51
|
+
status: InspectorCommentStatus;
|
|
52
|
+
statusMessage: string;
|
|
53
|
+
totalCount?: number;
|
|
54
|
+
onOpenSaved: (comment: InlineSavedComment) => void;
|
|
55
|
+
onRemoveSaved: (comment: InlineSavedComment) => Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface InlineComposerController {
|
|
59
|
+
text: string;
|
|
60
|
+
submitDisabled: boolean;
|
|
61
|
+
mentionItems: ProjectMentionItem[];
|
|
62
|
+
onTextChange: (value: string) => void;
|
|
63
|
+
onSubmit: (event?: FormEvent<HTMLFormElement>) => Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface InlineInspectorLayerProps {
|
|
67
|
+
sourceContainerRef: RefObject<HTMLDivElement | null>;
|
|
68
|
+
inspector: InspectorState;
|
|
69
|
+
comments: InlineCommentController;
|
|
70
|
+
composer: InlineComposerController;
|
|
71
|
+
geometryVersion?: unknown;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function InlineInspectorLayerImpl({
|
|
75
|
+
sourceContainerRef,
|
|
76
|
+
inspector,
|
|
77
|
+
comments,
|
|
78
|
+
composer,
|
|
79
|
+
geometryVersion,
|
|
80
|
+
}: InlineInspectorLayerProps) {
|
|
81
|
+
const savedComments = comments.saved;
|
|
82
|
+
const savedCommentTotalCount = comments.totalCount ?? savedComments.length;
|
|
83
|
+
const activeSavedComment = comments.active;
|
|
84
|
+
const commentText = composer.text;
|
|
85
|
+
const commentStatus = comments.status;
|
|
86
|
+
const commentStatusMessage = comments.statusMessage;
|
|
87
|
+
const submitDisabled = composer.submitDisabled;
|
|
88
|
+
const mentionItems = composer.mentionItems;
|
|
89
|
+
const onOpenSavedComment = comments.onOpenSaved;
|
|
90
|
+
const onRemoveSavedComment = comments.onRemoveSaved;
|
|
91
|
+
const onCommentTextChange = composer.onTextChange;
|
|
92
|
+
const onSubmitComment = composer.onSubmit;
|
|
93
|
+
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
94
|
+
const composerRef = useRef<HTMLFormElement | null>(null);
|
|
95
|
+
const rafRef = useRef<number | null>(null);
|
|
96
|
+
const active = inspector.enabled && inspector.inspectorMode;
|
|
97
|
+
const selectedTarget = inspector.selectedTarget;
|
|
98
|
+
const selectedTargetKey = objectSelectionKey(selectedTarget);
|
|
99
|
+
const activeSavedCommentForTarget = selectedTarget && activeSavedComment
|
|
100
|
+
&& inlineCommentTargetKey(activeSavedComment) === selectedTargetKey
|
|
101
|
+
&& activeSavedComment.placement === selectedTarget.placement
|
|
102
|
+
? activeSavedComment
|
|
103
|
+
: null;
|
|
104
|
+
const savedCommentForTarget = activeSavedCommentForTarget
|
|
105
|
+
?? getInlineSavedCommentForTarget(savedComments, selectedTarget);
|
|
106
|
+
const markerEntries = useMemo<InlineSavedCommentMarkerEntry[]>(
|
|
107
|
+
() => getInlineSavedCommentMarkers(savedComments),
|
|
108
|
+
[savedComments],
|
|
109
|
+
);
|
|
110
|
+
const savedCommentLabels = useMemo(() => {
|
|
111
|
+
const labels = new Map<string, string>();
|
|
112
|
+
savedComments.forEach((comment, index) => labels.set(comment.id, String(index + 1)));
|
|
113
|
+
return labels;
|
|
114
|
+
}, [savedComments]);
|
|
115
|
+
const markerEntriesByTarget = useMemo(
|
|
116
|
+
() => new Set(markerEntries.map(({ target }) => objectSelectionKey(target))),
|
|
117
|
+
[markerEntries],
|
|
118
|
+
);
|
|
119
|
+
const markerDisplayEntries = useMemo(
|
|
120
|
+
() => [
|
|
121
|
+
...markerEntries,
|
|
122
|
+
...(selectedTarget && !markerEntriesByTarget.has(selectedTargetKey ?? "")
|
|
123
|
+
? [{ target: selectedTarget, comments: [] }]
|
|
124
|
+
: []),
|
|
125
|
+
],
|
|
126
|
+
[markerEntries, markerEntriesByTarget, selectedTarget, selectedTargetKey],
|
|
127
|
+
);
|
|
128
|
+
const [insertTargets, setInsertTargets] = useState<InspectorInsertTargetView[]>([]);
|
|
129
|
+
const [selectionRect, setSelectionRect] = useState<InspectorLayerRect | null>(null);
|
|
130
|
+
const [composerTargetKey, setComposerTargetKey] = useState<string | null>(null);
|
|
131
|
+
const composerOpen = Boolean(selectedTargetKey && composerTargetKey === selectedTargetKey);
|
|
132
|
+
const markerOnly = Boolean(savedCommentForTarget && !composerOpen && !activeSavedCommentForTarget);
|
|
133
|
+
const {
|
|
134
|
+
activeMention,
|
|
135
|
+
handleMentionKeyDown,
|
|
136
|
+
highlightedMentionIndex,
|
|
137
|
+
mentionSuggestions,
|
|
138
|
+
setHighlightedMentionIndex,
|
|
139
|
+
setComposerCursor,
|
|
140
|
+
syncCursor,
|
|
141
|
+
insertMention,
|
|
142
|
+
} = useComposerMentions({
|
|
143
|
+
text: commentText,
|
|
144
|
+
items: mentionItems,
|
|
145
|
+
textareaRef,
|
|
146
|
+
onTextChange: onCommentTextChange,
|
|
147
|
+
enabled: composerOpen,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const updateLayer = useCallback(() => {
|
|
151
|
+
const root = sourceContainerRef.current;
|
|
152
|
+
if (!active || !root) {
|
|
153
|
+
setInsertTargets([]);
|
|
154
|
+
setSelectionRect(null);
|
|
155
|
+
if (root) syncInspectorSelectedBlock(root, null);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const blockElements = collectInspectorBlockElements(root);
|
|
160
|
+
const nextInsertTargets = createInspectorInsertTargets(blockElements);
|
|
161
|
+
setInsertTargets(nextInsertTargets);
|
|
162
|
+
setSelectionRect(resolveInspectorSelectionRect(root, selectedTarget, nextInsertTargets));
|
|
163
|
+
syncInspectorSelectedBlock(root, markerOnly ? null : selectedTarget);
|
|
164
|
+
}, [active, geometryVersion, markerOnly, selectedTarget, sourceContainerRef]);
|
|
165
|
+
|
|
166
|
+
const scheduleLayerUpdate = useCallback(() => {
|
|
167
|
+
if (rafRef.current !== null) window.cancelAnimationFrame(rafRef.current);
|
|
168
|
+
rafRef.current = window.requestAnimationFrame(() => {
|
|
169
|
+
rafRef.current = null;
|
|
170
|
+
updateLayer();
|
|
171
|
+
});
|
|
172
|
+
}, [updateLayer]);
|
|
173
|
+
|
|
174
|
+
useLayoutEffect(() => {
|
|
175
|
+
updateLayer();
|
|
176
|
+
}, [updateLayer]);
|
|
177
|
+
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (!active) return undefined;
|
|
180
|
+
const root = sourceContainerRef.current;
|
|
181
|
+
const resizeObserver = typeof ResizeObserver === "undefined" || !root
|
|
182
|
+
? null
|
|
183
|
+
: new ResizeObserver(scheduleLayerUpdate);
|
|
184
|
+
if (root && resizeObserver) resizeObserver.observe(root);
|
|
185
|
+
window.addEventListener("resize", scheduleLayerUpdate);
|
|
186
|
+
window.addEventListener("scroll", scheduleLayerUpdate, true);
|
|
187
|
+
|
|
188
|
+
return () => {
|
|
189
|
+
resizeObserver?.disconnect();
|
|
190
|
+
window.removeEventListener("resize", scheduleLayerUpdate);
|
|
191
|
+
window.removeEventListener("scroll", scheduleLayerUpdate, true);
|
|
192
|
+
if (rafRef.current !== null) window.cancelAnimationFrame(rafRef.current);
|
|
193
|
+
rafRef.current = null;
|
|
194
|
+
};
|
|
195
|
+
}, [active, scheduleLayerUpdate, sourceContainerRef]);
|
|
196
|
+
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (!selectedTarget || composerTargetKey !== selectedTargetKey) return undefined;
|
|
199
|
+
let innerFrame: number | null = null;
|
|
200
|
+
const outerFrame = window.requestAnimationFrame(() => {
|
|
201
|
+
innerFrame = window.requestAnimationFrame(() => textareaRef.current?.focus({ preventScroll: true }));
|
|
202
|
+
});
|
|
203
|
+
return () => {
|
|
204
|
+
window.cancelAnimationFrame(outerFrame);
|
|
205
|
+
if (innerFrame !== null) window.cancelAnimationFrame(innerFrame);
|
|
206
|
+
};
|
|
207
|
+
}, [composerTargetKey, selectedTarget, selectedTargetKey]);
|
|
208
|
+
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
setComposerTargetKey(null);
|
|
211
|
+
}, [selectedTargetKey]);
|
|
212
|
+
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
if (commentStatus === "saved") setComposerTargetKey(null);
|
|
215
|
+
}, [commentStatus]);
|
|
216
|
+
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (!composerOpen) return undefined;
|
|
219
|
+
|
|
220
|
+
const isInsideComposer = (target: EventTarget | null) => {
|
|
221
|
+
const composerElement = composerRef.current;
|
|
222
|
+
return Boolean(composerElement && target instanceof Node && composerElement.contains(target));
|
|
223
|
+
};
|
|
224
|
+
const blockOutsideComposer = (event: Event) => {
|
|
225
|
+
if (isInsideComposer(event.target)) return;
|
|
226
|
+
event.preventDefault();
|
|
227
|
+
event.stopPropagation();
|
|
228
|
+
};
|
|
229
|
+
const blockScrollKeyOutsideComposer = (event: KeyboardEvent) => {
|
|
230
|
+
if (!isScrollKey(event) || isInsideComposer(event.target)) return;
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
event.stopPropagation();
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
window.addEventListener("wheel", blockOutsideComposer, { capture: true, passive: false });
|
|
236
|
+
window.addEventListener("touchmove", blockOutsideComposer, { capture: true, passive: false });
|
|
237
|
+
window.addEventListener("keydown", blockScrollKeyOutsideComposer, { capture: true });
|
|
238
|
+
return () => {
|
|
239
|
+
window.removeEventListener("wheel", blockOutsideComposer, true);
|
|
240
|
+
window.removeEventListener("touchmove", blockOutsideComposer, true);
|
|
241
|
+
window.removeEventListener("keydown", blockScrollKeyOutsideComposer, true);
|
|
242
|
+
};
|
|
243
|
+
}, [composerOpen]);
|
|
244
|
+
|
|
245
|
+
if (!active) return null;
|
|
246
|
+
|
|
247
|
+
const composerStyle = selectionRect ? createInspectorComposerStyle(selectionRect, composerOpen) : undefined;
|
|
248
|
+
const visibleActionItems = savedCommentForTarget
|
|
249
|
+
? COMPOSER_ACTIONS.filter((item) => item.action !== "add")
|
|
250
|
+
: COMPOSER_ACTIONS;
|
|
251
|
+
const applyComposerAction = (action: ComposerAction) => {
|
|
252
|
+
if (!selectedTargetKey) return;
|
|
253
|
+
const item = COMPOSER_ACTIONS.find((entry) => entry.action === action);
|
|
254
|
+
if (!item) return;
|
|
255
|
+
setComposerTargetKey(selectedTargetKey);
|
|
256
|
+
if (!commentText.trim()) onCommentTextChange(item.prefix);
|
|
257
|
+
};
|
|
258
|
+
const handleMarkerClick = (target: ObjectSelection, comments: InlineSavedComment[]) => {
|
|
259
|
+
if (!target) return;
|
|
260
|
+
inspector.selectSelection(target);
|
|
261
|
+
setComposerTargetKey(null);
|
|
262
|
+
if (comments.length === 0) {
|
|
263
|
+
onCommentTextChange("");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
onOpenSavedComment(comments[0]!);
|
|
267
|
+
};
|
|
268
|
+
const getMarkerRect = (target: ObjectSelection) => {
|
|
269
|
+
const root = sourceContainerRef.current;
|
|
270
|
+
if (!root) return null;
|
|
271
|
+
return resolveInspectorSelectionRect(root, target, insertTargets);
|
|
272
|
+
};
|
|
273
|
+
const markerViews = markerDisplayEntries.flatMap((markerEntry) => {
|
|
274
|
+
const markerRect = getMarkerRect(markerEntry.target);
|
|
275
|
+
if (!markerRect) return [];
|
|
276
|
+
if (!isMarkerRectNearViewport(markerRect)) return [];
|
|
277
|
+
return [{
|
|
278
|
+
markerEntry,
|
|
279
|
+
markerRect,
|
|
280
|
+
markerLabel: markerLabelForEntry(markerEntry, savedCommentLabels, savedCommentTotalCount),
|
|
281
|
+
markerStyle: createInspectorMarkerStyle(markerRect),
|
|
282
|
+
}];
|
|
283
|
+
}).sort((left, right) => compareMarkerRects(left.markerRect, right.markerRect));
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<div
|
|
287
|
+
className="openpress-inline-inspector-layer"
|
|
288
|
+
data-openpress-inline-inspector-layer
|
|
289
|
+
data-openpress-composer-lock-events={composerOpen ? "true" : "false"}
|
|
290
|
+
>
|
|
291
|
+
{insertTargets.map((target) => {
|
|
292
|
+
const isSelected = selectedTarget?.blockId === target.blockId && selectedTarget.placement === "before";
|
|
293
|
+
return (
|
|
294
|
+
<button
|
|
295
|
+
type="button"
|
|
296
|
+
className={`openpress-inline-insert-target${isSelected ? " is-selected" : ""}`}
|
|
297
|
+
data-openpress-insert-before-block-id={target.blockId}
|
|
298
|
+
style={rectToFixedStyle(target.rect)}
|
|
299
|
+
aria-label="在此新增註解"
|
|
300
|
+
key={target.blockId}
|
|
301
|
+
onClick={() => inspector.selectSelection({ blockId: target.blockId, placement: "before" })}
|
|
302
|
+
/>
|
|
303
|
+
);
|
|
304
|
+
})}
|
|
305
|
+
|
|
306
|
+
{markerViews.map(({ markerEntry, markerLabel, markerStyle }) => {
|
|
307
|
+
const markerCount = markerEntry.comments.length;
|
|
308
|
+
const hasSavedComment = markerEntry.comments.length > 0;
|
|
309
|
+
return (
|
|
310
|
+
<button
|
|
311
|
+
type="button"
|
|
312
|
+
className="openpress-inline-comment-marker"
|
|
313
|
+
data-openpress-inline-comment-marker
|
|
314
|
+
data-openpress-inline-comment-marker-object-id={markerEntry.target.objectId}
|
|
315
|
+
data-openpress-inline-comment-marker-block-id={markerEntry.target.blockId}
|
|
316
|
+
data-openpress-inline-comment-marker-placement={markerEntry.target.placement}
|
|
317
|
+
data-openpress-marker-label={markerLabel}
|
|
318
|
+
data-openpress-marker-state={hasSavedComment ? "saved" : "draft"}
|
|
319
|
+
style={markerStyle}
|
|
320
|
+
aria-label={hasSavedComment ? `編輯註解 ${markerLabel},${markerCount} 則` : `目前選取區塊 ${markerLabel}`}
|
|
321
|
+
key={objectSelectionKey(markerEntry.target) ?? markerEntry.target.placement}
|
|
322
|
+
onClick={() => handleMarkerClick(markerEntry.target, markerEntry.comments)}
|
|
323
|
+
>
|
|
324
|
+
<span className="openpress-inline-comment-marker__index">{markerLabel}</span>
|
|
325
|
+
</button>
|
|
326
|
+
);
|
|
327
|
+
})}
|
|
328
|
+
|
|
329
|
+
{selectionRect && selectedTarget && !markerOnly ? (
|
|
330
|
+
<form
|
|
331
|
+
ref={composerRef}
|
|
332
|
+
className="openpress-inline-comment-composer"
|
|
333
|
+
data-openpress-inline-comment-composer
|
|
334
|
+
data-openpress-comment-placement={selectedTarget.placement}
|
|
335
|
+
data-openpress-composer-open={composerOpen ? "true" : "false"}
|
|
336
|
+
data-openpress-composer-saved={savedCommentForTarget ? "true" : "false"}
|
|
337
|
+
style={composerStyle}
|
|
338
|
+
onSubmit={(event) => void onSubmitComment(event)}
|
|
339
|
+
>
|
|
340
|
+
{!composerOpen ? (
|
|
341
|
+
<div className="openpress-inline-comment-composer__intents" aria-label="註解意圖">
|
|
342
|
+
{visibleActionItems.map((item) => {
|
|
343
|
+
const Icon = item.icon;
|
|
344
|
+
return (
|
|
345
|
+
<button
|
|
346
|
+
type="button"
|
|
347
|
+
aria-label={item.label}
|
|
348
|
+
title={item.label}
|
|
349
|
+
key={item.action}
|
|
350
|
+
onClick={() => {
|
|
351
|
+
if (savedCommentForTarget && item.action === "delete") {
|
|
352
|
+
void onRemoveSavedComment(savedCommentForTarget);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
applyComposerAction(item.action);
|
|
356
|
+
}}
|
|
357
|
+
>
|
|
358
|
+
<Icon aria-hidden="true" />
|
|
359
|
+
</button>
|
|
360
|
+
);
|
|
361
|
+
})}
|
|
362
|
+
</div>
|
|
363
|
+
) : null}
|
|
364
|
+
{composerOpen ? (
|
|
365
|
+
<div className="openpress-inline-comment-composer__body">
|
|
366
|
+
<textarea
|
|
367
|
+
ref={textareaRef}
|
|
368
|
+
value={commentText}
|
|
369
|
+
disabled={commentStatus === "submitting"}
|
|
370
|
+
onChange={(event) => {
|
|
371
|
+
onCommentTextChange(event.target.value);
|
|
372
|
+
setComposerCursor(event.target.selectionStart ?? event.target.value.length);
|
|
373
|
+
}}
|
|
374
|
+
onClick={syncCursor}
|
|
375
|
+
onKeyUp={syncCursor}
|
|
376
|
+
onKeyDown={(event) => {
|
|
377
|
+
if (handleMentionKeyDown(event)) return;
|
|
378
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
379
|
+
event.preventDefault();
|
|
380
|
+
void onSubmitComment();
|
|
381
|
+
}
|
|
382
|
+
}}
|
|
383
|
+
aria-label={savedCommentForTarget ? "編輯註解" : "新增註解"}
|
|
384
|
+
placeholder="新增註解..."
|
|
385
|
+
rows={3}
|
|
386
|
+
/>
|
|
387
|
+
<button type="submit" disabled={submitDisabled} aria-label="送出註解">
|
|
388
|
+
<ArrowUp aria-hidden="true" />
|
|
389
|
+
</button>
|
|
390
|
+
</div>
|
|
391
|
+
) : null}
|
|
392
|
+
{composerOpen ? (
|
|
393
|
+
<MentionSuggestionList
|
|
394
|
+
className="openpress-inline-comment-composer__suggestions"
|
|
395
|
+
suggestions={mentionSuggestions}
|
|
396
|
+
highlightedIndex={highlightedMentionIndex}
|
|
397
|
+
ariaLabel={activeMention?.trigger === "/" ? "Skill suggestions" : "Mention suggestions"}
|
|
398
|
+
onHighlight={setHighlightedMentionIndex}
|
|
399
|
+
onSelect={insertMention}
|
|
400
|
+
/>
|
|
401
|
+
) : null}
|
|
402
|
+
{composerOpen && commentStatusMessage ? (
|
|
403
|
+
<p role="status" aria-live="polite" data-openpress-inspector-comment-status={commentStatus}>
|
|
404
|
+
{commentStatusMessage}
|
|
405
|
+
</p>
|
|
406
|
+
) : null}
|
|
407
|
+
</form>
|
|
408
|
+
) : null}
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function objectSelectionKey(target: ObjectSelection | null) {
|
|
414
|
+
if (!target) return null;
|
|
415
|
+
return `${target.objectId ?? target.blockId ?? "unknown"}:${target.placement}`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function inlineCommentTargetKey(comment: InlineSavedComment) {
|
|
419
|
+
return `${comment.objectId ?? comment.blockId ?? "unknown"}:${comment.placement}`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function isScrollKey(event: KeyboardEvent) {
|
|
423
|
+
return event.key === " "
|
|
424
|
+
|| event.key === "Spacebar"
|
|
425
|
+
|| event.key === "PageDown"
|
|
426
|
+
|| event.key === "PageUp"
|
|
427
|
+
|| event.key === "Home"
|
|
428
|
+
|| event.key === "End"
|
|
429
|
+
|| event.key === "ArrowDown"
|
|
430
|
+
|| event.key === "ArrowUp";
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function compareMarkerRects(left: InspectorLayerRect, right: InspectorLayerRect) {
|
|
434
|
+
const topDelta = left.top - right.top;
|
|
435
|
+
if (Math.abs(topDelta) > 1) return topDelta;
|
|
436
|
+
return left.left - right.left;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function markerLabelForEntry(
|
|
440
|
+
markerEntry: InlineSavedCommentMarkerEntry,
|
|
441
|
+
savedCommentLabels: Map<string, string>,
|
|
442
|
+
savedCommentCount: number,
|
|
443
|
+
) {
|
|
444
|
+
const firstComment = markerEntry.comments[0];
|
|
445
|
+
if (!firstComment) return String(savedCommentCount + 1);
|
|
446
|
+
if (firstComment.markerLabel) return firstComment.markerLabel;
|
|
447
|
+
return savedCommentLabels.get(firstComment.id) ?? String(savedCommentCount + 1);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function isMarkerRectNearViewport(rect: InspectorLayerRect, margin = 48) {
|
|
451
|
+
if (typeof window === "undefined") return true;
|
|
452
|
+
return rect.top + rect.height >= -margin
|
|
453
|
+
&& rect.top <= window.innerHeight + margin
|
|
454
|
+
&& rect.left + rect.width >= -margin
|
|
455
|
+
&& rect.left <= window.innerWidth + margin;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export const InlineInspectorLayer = memo(InlineInspectorLayerImpl);
|
|
459
|
+
InlineInspectorLayer.displayName = "InlineInspectorLayer";
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { ObjectSelection, PendingComment } from "./inspectorModel";
|
|
2
|
+
import type { SourceBlock } from "../../document-model";
|
|
3
|
+
import { parseCommentHint } from "../workbenchFormatters";
|
|
4
|
+
import type { InlineSavedComment, InlineSavedCommentMarkerEntry } from "../workbenchTypes";
|
|
5
|
+
|
|
6
|
+
export function getInlineSavedCommentForTarget(
|
|
7
|
+
comments: InlineSavedComment[],
|
|
8
|
+
target: ObjectSelection | null,
|
|
9
|
+
preferredId?: string | null,
|
|
10
|
+
) {
|
|
11
|
+
if (!target) return null;
|
|
12
|
+
const targetKey = objectSelectionKey(target);
|
|
13
|
+
const targetComments = comments.filter((comment) => inlineCommentTargetKey(comment) === targetKey);
|
|
14
|
+
if (!targetComments.length) return null;
|
|
15
|
+
if (preferredId) {
|
|
16
|
+
const preferred = targetComments.find((comment) => comment.id === preferredId);
|
|
17
|
+
if (preferred) return preferred;
|
|
18
|
+
}
|
|
19
|
+
return targetComments[0] ?? null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getInlineSavedCommentMarkers(comments: InlineSavedComment[]) {
|
|
23
|
+
const markerMap = new Map<string, InlineSavedCommentMarkerEntry>();
|
|
24
|
+
|
|
25
|
+
for (const comment of comments) {
|
|
26
|
+
const target: ObjectSelection = {
|
|
27
|
+
objectId: comment.objectId,
|
|
28
|
+
blockId: comment.blockId,
|
|
29
|
+
placement: comment.placement,
|
|
30
|
+
};
|
|
31
|
+
const key = objectSelectionKey(target);
|
|
32
|
+
if (!key) continue;
|
|
33
|
+
const bucket = markerMap.get(key);
|
|
34
|
+
if (bucket) {
|
|
35
|
+
bucket.comments.push(comment);
|
|
36
|
+
} else {
|
|
37
|
+
markerMap.set(key, { target, comments: [comment] });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return Array.from(markerMap.values()).map((entry) => ({
|
|
42
|
+
...entry,
|
|
43
|
+
comments: [...entry.comments],
|
|
44
|
+
})).sort((left, right) => {
|
|
45
|
+
const leftId = left.target.objectId ?? left.target.blockId ?? "";
|
|
46
|
+
const rightId = right.target.objectId ?? right.target.blockId ?? "";
|
|
47
|
+
if (leftId === rightId) {
|
|
48
|
+
if (left.target.placement === right.target.placement) return 0;
|
|
49
|
+
return left.target.placement === "before" ? -1 : 1;
|
|
50
|
+
}
|
|
51
|
+
return leftId.localeCompare(rightId, "zh-Hant");
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveInlineSavedComment(comment: PendingComment, sourceBlocksByPath: Record<string, SourceBlock[]>) {
|
|
56
|
+
const target = resolveInlineSavedCommentTarget(comment, sourceBlocksByPath);
|
|
57
|
+
if (!target) return [];
|
|
58
|
+
const hintMeta = parseCommentHint(comment.hint);
|
|
59
|
+
return [{
|
|
60
|
+
id: comment.id,
|
|
61
|
+
objectId: hintMeta?.targetObjectId,
|
|
62
|
+
blockId: target.id,
|
|
63
|
+
placement: hintMeta?.placement ?? "block",
|
|
64
|
+
note: comment.note,
|
|
65
|
+
path: comment.path,
|
|
66
|
+
line: comment.line,
|
|
67
|
+
timestamp: comment.timestamp,
|
|
68
|
+
}];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function objectSelectionKey(target: ObjectSelection | null) {
|
|
72
|
+
if (!target) return null;
|
|
73
|
+
return `${target.objectId ?? target.blockId ?? "unknown"}\u0000${target.placement}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function inlineCommentTargetKey(comment: InlineSavedComment) {
|
|
77
|
+
return `${comment.objectId ?? comment.blockId ?? "unknown"}\u0000${comment.placement}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function groupSourceBlocksByPath(sourceBlockMap: Record<string, SourceBlock>) {
|
|
81
|
+
const grouped = Object.values(sourceBlockMap).reduce<Record<string, SourceBlock[]>>((accumulator, sourceBlock) => {
|
|
82
|
+
const path = normalizeSourcePath(sourceBlock.path);
|
|
83
|
+
if (!path) return accumulator;
|
|
84
|
+
if (!accumulator[path]) accumulator[path] = [];
|
|
85
|
+
accumulator[path].push(sourceBlock);
|
|
86
|
+
return accumulator;
|
|
87
|
+
}, {});
|
|
88
|
+
|
|
89
|
+
Object.values(grouped).forEach((blocks) => {
|
|
90
|
+
blocks.sort((left, right) => {
|
|
91
|
+
const leftLine = left.source?.line ?? Number.POSITIVE_INFINITY;
|
|
92
|
+
const rightLine = right.source?.line ?? Number.POSITIVE_INFINITY;
|
|
93
|
+
if (leftLine === rightLine) return left.id.localeCompare(right.id, "zh-Hant");
|
|
94
|
+
return leftLine - rightLine;
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return grouped;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveInlineSavedCommentTarget(comment: PendingComment, sourceBlocksByPath: Record<string, SourceBlock[]>) {
|
|
102
|
+
if (!comment.path || !comment.line) return null;
|
|
103
|
+
const commentPath = normalizeSourcePath(comment.path);
|
|
104
|
+
const candidateBlocks = sourceBlocksByPath[commentPath];
|
|
105
|
+
if (!candidateBlocks?.length) return null;
|
|
106
|
+
|
|
107
|
+
const normalizedLine = Number(comment.line);
|
|
108
|
+
if (!Number.isInteger(normalizedLine) || normalizedLine < 1) return null;
|
|
109
|
+
|
|
110
|
+
let selectedBlock: SourceBlock | null = null;
|
|
111
|
+
for (const block of candidateBlocks) {
|
|
112
|
+
if (typeof block.source?.line !== "number") continue;
|
|
113
|
+
if (block.source.line <= normalizedLine) {
|
|
114
|
+
selectedBlock = block;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
if (selectedBlock) return selectedBlock;
|
|
120
|
+
return candidateBlocks[0] ?? null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeSourcePath(value: string) {
|
|
124
|
+
return value.trim().replaceAll("\\", "/").replace(/^\.\//, "").replace(/^document\//, "");
|
|
125
|
+
}
|