@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.
Files changed (132) hide show
  1. package/README.md +6 -1
  2. package/package.json +1 -1
  3. package/template/core/AGENTS.md +126 -0
  4. package/template/core/CHANGELOG.md +65 -0
  5. package/template/core/engine/commands/dev.mjs +2 -2
  6. package/template/core/engine/commands/upgrade.mjs +47 -5
  7. package/template/core/engine/output/chrome-pdf.mjs +18 -3
  8. package/template/core/engine/output/static-server.mjs +39 -0
  9. package/template/core/engine/react/comment-endpoint.mjs +13 -39
  10. package/template/core/engine/react/comment-marker.mjs +30 -6
  11. package/template/core/engine/react/document-entry.mjs +11 -0
  12. package/template/core/engine/react/document-export.mjs +45 -5
  13. package/template/core/engine/react/http-json.mjs +24 -0
  14. package/template/core/engine/react/mdx-compile.mjs +187 -3
  15. package/template/core/engine/react/measurement-css.mjs +93 -1
  16. package/template/core/engine/react/object-entities.mjs +119 -0
  17. package/template/core/engine/react/pipeline/allocate.mjs +10 -7
  18. package/template/core/engine/react/pipeline/frame-measurement.mjs +40 -9
  19. package/template/core/engine/react/project-asset-endpoint.mjs +6 -24
  20. package/template/core/engine/react/source-edit-endpoint.d.mts +10 -0
  21. package/template/core/engine/react/source-edit-endpoint.mjs +75 -0
  22. package/template/core/engine/react/sources/mdx-resolver.mjs +12 -14
  23. package/template/core/engine/react/style-discovery.mjs +1 -4
  24. package/template/core/engine/runtime/file-walk.mjs +22 -0
  25. package/template/core/engine/runtime/inspection.mjs +1 -20
  26. package/template/core/engine/runtime/path-utils.mjs +20 -0
  27. package/template/core/engine/runtime/source-text-tools.d.mts +102 -0
  28. package/template/core/engine/runtime/source-text-tools.mjs +551 -16
  29. package/template/core/engine/runtime/source-workspace.mjs +4 -31
  30. package/template/core/package.json +1 -1
  31. package/template/core/src/main.tsx +2 -2
  32. package/template/core/src/openpress/{App.tsx → app/OpenPressApp.tsx} +25 -12
  33. package/template/core/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +10 -7
  34. package/template/core/src/openpress/app/index.ts +2 -0
  35. package/template/core/src/openpress/core/Frame.tsx +9 -11
  36. package/template/core/src/openpress/core/FrameContext.tsx +8 -3
  37. package/template/core/src/openpress/core/MdxArea.tsx +11 -12
  38. package/template/core/src/openpress/core/cn.ts +4 -0
  39. package/template/core/src/openpress/core/index.tsx +2 -1
  40. package/template/core/src/openpress/core/primitives.tsx +29 -8
  41. package/template/core/src/openpress/core/types.ts +8 -0
  42. package/template/core/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
  43. package/template/core/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
  44. package/template/core/src/openpress/{types.ts → document-model/documentTypes.ts} +42 -0
  45. package/template/core/src/openpress/document-model/index.ts +6 -0
  46. package/template/core/src/openpress/document-model/objectEntityModel.ts +51 -0
  47. package/template/core/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
  48. package/template/core/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
  49. package/template/core/src/openpress/manuscript/index.tsx +49 -7
  50. package/template/core/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
  51. package/template/core/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
  52. package/template/core/src/openpress/reader/index.ts +10 -0
  53. package/template/core/src/openpress/reader/pageViewportScaleModel.ts +73 -0
  54. package/template/core/src/openpress/reader/readerTypes.ts +4 -0
  55. package/template/core/src/openpress/reader/usePageViewportScale.ts +119 -0
  56. package/template/core/src/openpress/reader/usePanelState.ts +56 -0
  57. package/template/core/src/openpress/reader/useReaderHashSync.ts +61 -0
  58. package/template/core/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
  59. package/template/core/src/openpress/reader/useReaderRuntime.ts +146 -0
  60. package/template/core/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
  61. package/template/core/src/openpress/shared/Panel.tsx +77 -0
  62. package/template/core/src/openpress/shared/index.ts +4 -0
  63. package/template/core/src/openpress/shared/numberUtils.ts +3 -0
  64. package/template/core/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
  65. package/template/core/src/openpress/workbench/Workbench.tsx +407 -0
  66. package/template/core/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
  67. package/template/core/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
  68. package/template/core/src/openpress/workbench/actions/SearchControl.tsx +345 -0
  69. package/template/core/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
  70. package/template/core/src/openpress/workbench/actions/index.ts +5 -0
  71. package/template/core/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
  72. package/template/core/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
  73. package/template/core/src/openpress/workbench/dialog/index.ts +1 -0
  74. package/template/core/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
  75. package/template/core/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
  76. package/template/core/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
  77. package/template/core/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
  78. package/template/core/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
  79. package/template/core/src/openpress/workbench/document/index.ts +10 -0
  80. package/template/core/src/openpress/workbench/index.ts +2 -0
  81. package/template/core/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
  82. package/template/core/src/openpress/workbench/inspector/index.ts +5 -0
  83. package/template/core/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
  84. package/template/core/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
  85. package/template/core/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
  86. package/template/core/src/openpress/workbench/inspector/useInspectorComments.ts +248 -0
  87. package/template/core/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
  88. package/template/core/src/openpress/workbench/mentions/index.ts +2 -0
  89. package/template/core/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
  90. package/template/core/src/openpress/workbench/panels/Panel.tsx +1 -0
  91. package/template/core/src/openpress/workbench/panels/PendingCommentsPanel.tsx +76 -0
  92. package/template/core/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
  93. package/template/core/src/openpress/workbench/panels/index.ts +3 -0
  94. package/template/core/src/openpress/workbench/project/ProjectEntryPanel.tsx +523 -0
  95. package/template/core/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
  96. package/template/core/src/openpress/workbench/project/index.ts +2 -0
  97. package/template/core/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
  98. package/template/core/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
  99. package/template/core/src/openpress/workbench/shell/index.ts +1 -0
  100. package/template/core/src/openpress/workbench/workbenchFormatters.ts +120 -0
  101. package/template/core/src/openpress/workbench/workbenchTypes.ts +35 -0
  102. package/template/core/src/styles/openpress/print-route.css +0 -2
  103. package/template/core/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
  104. package/template/core/src/styles/openpress/public-viewer.css +25 -320
  105. package/template/core/src/styles/openpress/reader-runtime.css +243 -55
  106. package/template/core/src/styles/openpress/responsive.css +145 -270
  107. package/template/core/src/styles/openpress/workbench-panels.css +214 -178
  108. package/template/core/src/styles/openpress/workbench.css +986 -451
  109. package/template/core/src/styles/openpress.css +1 -1
  110. package/template/core/vite.config.ts +50 -0
  111. package/template/packs/academic-paper/document/chapters/01-introduction/content/01-introduction.mdx +26 -12
  112. package/template/packs/academic-paper/document/chapters/02-methods/content/01-methods.mdx +37 -17
  113. package/template/packs/academic-paper/document/chapters/03-results-and-discussion/content/01-results.mdx +34 -16
  114. package/template/packs/academic-paper/document/chapters/04-acknowledgment/content/01-acknowledgment.mdx +22 -8
  115. package/template/packs/academic-paper/document/chapters/05-references/content/01-references.mdx +20 -15
  116. package/template/packs/academic-paper/document/components/Page.tsx +26 -3
  117. package/template/packs/academic-paper/document/index.tsx +51 -59
  118. package/template/packs/academic-paper/document/media/figure-placeholder.svg +9 -0
  119. package/template/packs/academic-paper/document/theme/base/page-contract.css +30 -13
  120. package/template/packs/academic-paper/document/theme/base/typography.css +30 -33
  121. package/template/packs/academic-paper/document/theme/page-surfaces/cover.css +74 -47
  122. package/template/core/src/openpress/inspector.ts +0 -282
  123. package/template/core/src/openpress/projectWorkspace.tsx +0 -919
  124. package/template/core/src/openpress/readerRuntime.ts +0 -230
  125. package/template/core/src/openpress/workbench.tsx +0 -1265
  126. package/template/core/src/openpress/workbenchTypes.ts +0 -4
  127. /package/template/core/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
  128. /package/template/core/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
  129. /package/template/core/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
  130. /package/template/core/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
  131. /package/template/core/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
  132. /package/template/core/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
@@ -0,0 +1,248 @@
1
+ import { useCallback, useEffect, useMemo, useState, type FormEvent, type RefObject } from "react";
2
+ import type { SourceBlock } from "../../document-model";
3
+ import type { InlineSavedComment, InspectorCommentStatus, PendingCommentsStatus } from "../workbenchTypes";
4
+ import { formatInspectorCommentStatus } from "../workbenchFormatters";
5
+ import { clearInspectorComment, fetchInspectorComments } from "./inspectorModel";
6
+ import { createInspectorCommentDraft, submitInspectorComment, updateInspectorComment } from "./inspectorModel";
7
+ import type { InspectorState, PendingComment } from "./inspectorModel";
8
+ import { getInlineSavedCommentForTarget, resolveInlineSavedComment } from "./inlineCommentModel";
9
+
10
+ export interface UseInspectorCommentsOptions {
11
+ devMode: boolean;
12
+ inspector: InspectorState;
13
+ sourceBlockMap: Record<string, SourceBlock>;
14
+ sourceBlocksByPath: Record<string, SourceBlock[]>;
15
+ sourceContainerRef: RefObject<HTMLDivElement | null>;
16
+ onSelectWorkspacePage: (pageIndex: number, options?: { behavior?: ScrollBehavior }) => void;
17
+ }
18
+
19
+ export interface InspectorComments {
20
+ pendingComments: PendingComment[];
21
+ commentsStatus: PendingCommentsStatus;
22
+ commentsError: string;
23
+ inspectorCommentText: string;
24
+ inspectorCommentStatus: InspectorCommentStatus;
25
+ inspectorCommentStatusMessage: string;
26
+ inspectorCommentDisabled: boolean;
27
+ inlineSavedComments: InlineSavedComment[];
28
+ activeInlineSavedComment: InlineSavedComment | null;
29
+ setInspectorCommentText: (value: string) => void;
30
+ refreshPendingComments: () => Promise<void>;
31
+ clearPendingComment: (id: string) => Promise<void>;
32
+ handleSubmitInspectorComment: (event?: FormEvent<HTMLFormElement>) => Promise<void>;
33
+ handleOpenInlineSavedComment: (comment: InlineSavedComment) => void;
34
+ handleRemoveInlineSavedComment: (comment: InlineSavedComment) => Promise<void>;
35
+ handleSelectPendingComment: (comment: PendingComment) => void;
36
+ }
37
+
38
+ export function useInspectorComments({
39
+ devMode,
40
+ inspector,
41
+ sourceBlockMap,
42
+ sourceBlocksByPath,
43
+ sourceContainerRef,
44
+ onSelectWorkspacePage,
45
+ }: UseInspectorCommentsOptions): InspectorComments {
46
+ const [inspectorCommentText, setInspectorCommentText] = useState("");
47
+ const [inspectorCommentStatus, setInspectorCommentStatus] = useState<InspectorCommentStatus>("idle");
48
+ const [inspectorCommentError, setInspectorCommentError] = useState("");
49
+ const [inlineSavedCommentId, setInlineSavedCommentId] = useState<string | null>(null);
50
+ const [pendingComments, setPendingComments] = useState<PendingComment[]>([]);
51
+ const [commentsStatus, setCommentsStatus] = useState<PendingCommentsStatus>("idle");
52
+ const [commentsError, setCommentsError] = useState("");
53
+
54
+ const inlineSavedComments = useMemo(
55
+ () => pendingComments.flatMap((comment, index) => (
56
+ resolveInlineSavedComment(comment, sourceBlocksByPath)
57
+ .map((inlineComment) => ({ ...inlineComment, markerLabel: String(index + 1) }))
58
+ )),
59
+ [pendingComments, sourceBlocksByPath],
60
+ );
61
+
62
+ const activeInlineSavedComment = getInlineSavedCommentForTarget(
63
+ inlineSavedComments,
64
+ inspector.selectedTarget,
65
+ inlineSavedCommentId,
66
+ );
67
+
68
+ const inspectorCommentDisabled =
69
+ !inspector.selectedBlock || !inspectorCommentText.trim() || inspectorCommentStatus === "submitting";
70
+ const inspectorCommentStatusMessage = formatInspectorCommentStatus(inspectorCommentStatus, inspectorCommentError);
71
+
72
+ const refreshPendingComments = useCallback(async () => {
73
+ if (!devMode) return;
74
+ setCommentsStatus("loading");
75
+ setCommentsError("");
76
+ try {
77
+ const comments = await fetchInspectorComments();
78
+ setPendingComments(comments);
79
+ setCommentsStatus("ready");
80
+ } catch (error) {
81
+ setCommentsStatus("failed");
82
+ setCommentsError(error instanceof Error ? error.message : String(error));
83
+ }
84
+ }, [devMode]);
85
+
86
+ const clearPendingComment = useCallback(async (id: string) => {
87
+ setCommentsStatus("clearing");
88
+ setCommentsError("");
89
+ try {
90
+ await clearInspectorComment({ id });
91
+ setPendingComments((comments) => comments.filter((comment) => comment.id !== id));
92
+ setInlineSavedCommentId((currentId) => (currentId === id ? null : currentId));
93
+ setCommentsStatus("ready");
94
+ } catch (error) {
95
+ setCommentsStatus("failed");
96
+ setCommentsError(error instanceof Error ? error.message : String(error));
97
+ }
98
+ }, []);
99
+
100
+ const handleSubmitInspectorComment = useCallback(async (event?: FormEvent<HTMLFormElement>) => {
101
+ event?.preventDefault();
102
+ if (inspectorCommentDisabled || !inspector.selectedBlock) return;
103
+ setInspectorCommentStatus("submitting");
104
+ setInspectorCommentError("");
105
+ try {
106
+ const note = inspectorCommentText.trim();
107
+ const placement = inspector.selectedTarget?.placement ?? "block";
108
+ if (activeInlineSavedComment) {
109
+ const result = await updateInspectorComment({
110
+ id: activeInlineSavedComment.id,
111
+ note,
112
+ placement,
113
+ });
114
+ setInlineSavedCommentId(result.comment?.id ?? activeInlineSavedComment.id);
115
+ } else {
116
+ const draft = createInspectorCommentDraft({
117
+ block: inspector.selectedBlock,
118
+ entity: inspector.selectedObjectEntity,
119
+ target: inspector.selectedTarget,
120
+ note,
121
+ placement,
122
+ });
123
+ const result = await submitInspectorComment({ draft });
124
+ if (result.comment?.id) {
125
+ setInlineSavedCommentId(result.comment.id);
126
+ }
127
+ }
128
+ setInspectorCommentText("");
129
+ setInspectorCommentStatus("saved");
130
+ void refreshPendingComments();
131
+ } catch (error) {
132
+ setInspectorCommentStatus("failed");
133
+ setInspectorCommentError(error instanceof Error ? error.message : String(error));
134
+ }
135
+ }, [
136
+ activeInlineSavedComment,
137
+ inspector.selectedBlock,
138
+ inspector.selectedObjectEntity,
139
+ inspector.selectedTarget,
140
+ inspector.selectedTarget?.placement,
141
+ inspectorCommentDisabled,
142
+ inspectorCommentText,
143
+ refreshPendingComments,
144
+ ]);
145
+
146
+ const handleOpenInlineSavedComment = useCallback((comment: InlineSavedComment) => {
147
+ setInlineSavedCommentId(comment.id);
148
+ setInspectorCommentText(comment.note);
149
+ setInspectorCommentStatus("idle");
150
+ setInspectorCommentError("");
151
+ }, []);
152
+
153
+ const handleRemoveInlineSavedComment = useCallback(async (comment: InlineSavedComment) => {
154
+ setInspectorCommentStatus("submitting");
155
+ setInspectorCommentError("");
156
+ try {
157
+ await clearInspectorComment({ id: comment.id });
158
+ setPendingComments((comments) => comments.filter((item) => item.id !== comment.id));
159
+ setInlineSavedCommentId((currentId) => (currentId === comment.id ? null : currentId));
160
+ setInspectorCommentText("");
161
+ setInspectorCommentStatus("idle");
162
+ inspector.selectTarget(null);
163
+ void refreshPendingComments();
164
+ } catch (error) {
165
+ setInspectorCommentStatus("failed");
166
+ setInspectorCommentError(error instanceof Error ? error.message : String(error));
167
+ }
168
+ }, [inspector, refreshPendingComments]);
169
+
170
+ const handleSelectPendingComment = useCallback((comment: PendingComment) => {
171
+ const inlineComment = inlineSavedComments.find((item) => item.id === comment.id)
172
+ ?? resolveInlineSavedComment(comment, sourceBlocksByPath)[0];
173
+ if (!inlineComment?.blockId) return;
174
+
175
+ const sourceBlock = sourceBlockMap[inlineComment.blockId];
176
+ if (typeof sourceBlock?.pageIndex === "number") {
177
+ onSelectWorkspacePage(sourceBlock.pageIndex, { behavior: "smooth" });
178
+ }
179
+
180
+ inspector.selectSelection({
181
+ objectId: inlineComment.objectId,
182
+ blockId: inlineComment.blockId,
183
+ placement: inlineComment.placement,
184
+ });
185
+ handleOpenInlineSavedComment(inlineComment);
186
+
187
+ window.requestAnimationFrame(() => {
188
+ const selector = inlineComment.objectId
189
+ ? `[data-openpress-object-id="${cssEscape(inlineComment.objectId)}"]`
190
+ : `[data-openpress-block-id="${cssEscape(inlineComment.blockId!)}"]`;
191
+ sourceContainerRef.current?.querySelector<HTMLElement>(selector)?.scrollIntoView({
192
+ behavior: "smooth",
193
+ block: "center",
194
+ });
195
+ });
196
+ }, [
197
+ handleOpenInlineSavedComment,
198
+ inlineSavedComments,
199
+ inspector,
200
+ onSelectWorkspacePage,
201
+ sourceBlockMap,
202
+ sourceBlocksByPath,
203
+ sourceContainerRef,
204
+ ]);
205
+
206
+ // Reset composer state when the inspector selection changes.
207
+ useEffect(() => {
208
+ setInspectorCommentStatus("idle");
209
+ setInspectorCommentError("");
210
+ setInspectorCommentText("");
211
+ }, [inspector.selectedBlockId, inspector.selectedTarget?.placement]);
212
+
213
+ // Drop the inline saved id if its comment is no longer reachable.
214
+ useEffect(() => {
215
+ if (inlineSavedCommentId && !activeInlineSavedComment) {
216
+ setInlineSavedCommentId(null);
217
+ }
218
+ }, [activeInlineSavedComment, inlineSavedCommentId]);
219
+
220
+ // Initial + dev-mode refresh of pending comments.
221
+ useEffect(() => {
222
+ if (!devMode) return;
223
+ void refreshPendingComments();
224
+ }, [devMode, refreshPendingComments]);
225
+
226
+ return {
227
+ pendingComments,
228
+ commentsStatus,
229
+ commentsError,
230
+ inspectorCommentText,
231
+ inspectorCommentStatus,
232
+ inspectorCommentStatusMessage,
233
+ inspectorCommentDisabled,
234
+ inlineSavedComments,
235
+ activeInlineSavedComment,
236
+ setInspectorCommentText,
237
+ refreshPendingComments,
238
+ clearPendingComment,
239
+ handleSubmitInspectorComment,
240
+ handleOpenInlineSavedComment,
241
+ handleRemoveInlineSavedComment,
242
+ handleSelectPendingComment,
243
+ };
244
+ }
245
+
246
+ function cssEscape(value: string) {
247
+ return globalThis.CSS?.escape ? globalThis.CSS.escape(value) : value.replace(/["\\]/g, "\\$&");
248
+ }
@@ -0,0 +1,41 @@
1
+ import type { ComposerMentionItem } from "./useComposerMentions";
2
+
3
+ export interface MentionSuggestionListProps {
4
+ className: string;
5
+ suggestions: ComposerMentionItem[];
6
+ highlightedIndex: number;
7
+ ariaLabel: string;
8
+ onHighlight: (index: number) => void;
9
+ onSelect: (item: ComposerMentionItem) => void;
10
+ }
11
+
12
+ export function MentionSuggestionList({
13
+ className,
14
+ suggestions,
15
+ highlightedIndex,
16
+ ariaLabel,
17
+ onHighlight,
18
+ onSelect,
19
+ }: MentionSuggestionListProps) {
20
+ if (suggestions.length === 0) return null;
21
+
22
+ return (
23
+ <div className={className} role="listbox" aria-label={ariaLabel}>
24
+ {suggestions.map((item, index) => (
25
+ <button
26
+ type="button"
27
+ role="option"
28
+ aria-selected={index === highlightedIndex}
29
+ data-highlighted={index === highlightedIndex ? "true" : undefined}
30
+ key={`${item.kind}-${item.value}`}
31
+ onMouseDown={(event) => event.preventDefault()}
32
+ onMouseEnter={() => onHighlight(index)}
33
+ onClick={() => onSelect(item)}
34
+ >
35
+ <span>{item.label}</span>
36
+ <small>{item.meta}</small>
37
+ </button>
38
+ ))}
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./useComposerMentions";
2
+ export * from "./MentionSuggestionList";
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useMemo, useState, type KeyboardEvent, type RefObject } from "react";
2
+ import { clampNumber } from "../../shared";
2
3
 
3
4
  export type ComposerMentionItem = {
4
5
  trigger: "@" | "/";
@@ -176,10 +177,6 @@ function mentionMatches(item: ComposerMentionItem, query: string) {
176
177
  || item.meta.toLowerCase().includes(normalizedQuery);
177
178
  }
178
179
 
179
- function clampNumber(value: number, min: number, max: number) {
180
- return Math.min(Math.max(value, min), Math.max(min, max));
181
- }
182
-
183
180
  const MENTION_PREFIX_DEFINITIONS: Array<{ kind: "media" | "chapter" | "section" | "component"; meta: string }> = [
184
181
  { kind: "media", meta: "prefix · images" },
185
182
  { kind: "chapter", meta: "prefix · chapters" },
@@ -0,0 +1 @@
1
+ export { Panel } from "../../shared/Panel";
@@ -0,0 +1,76 @@
1
+ import { Trash2 } from "lucide-react";
2
+ import type { PendingComment } from "../inspector";
3
+ import {
4
+ formatCommentTimestamp,
5
+ formatCommentsCount,
6
+ } from "../workbenchFormatters";
7
+ import type { PendingCommentsStatus } from "../workbenchTypes";
8
+ import { Panel } from "./Panel";
9
+
10
+ export function PendingCommentsPanel({
11
+ comments,
12
+ status,
13
+ error,
14
+ onClear,
15
+ onSelect,
16
+ }: {
17
+ comments: PendingComment[];
18
+ status: PendingCommentsStatus;
19
+ error: string;
20
+ onClear: (id: string) => Promise<void>;
21
+ onSelect?: (comment: PendingComment) => void;
22
+ }) {
23
+ const busy = status === "loading" || status === "clearing";
24
+
25
+ return (
26
+ <Panel
27
+ className="openpress-comments-panel openpress-comments-panel--embedded openpress-panel--compact"
28
+ data-openpress-comments-panel
29
+ aria-label="待處理註解"
30
+ >
31
+ <Panel.Header>
32
+ <div className="openpress-panel-heading-stack">
33
+ <Panel.Kicker>Comments</Panel.Kicker>
34
+ <Panel.Title>待處理註解</Panel.Title>
35
+ <Panel.Description>{formatCommentsCount(comments.length, status)}</Panel.Description>
36
+ </div>
37
+ </Panel.Header>
38
+
39
+ <Panel.Body>
40
+ {error ? <Panel.Error>{error}</Panel.Error> : null}
41
+
42
+ {comments.length === 0 && status !== "loading" ? (
43
+ <Panel.Empty role="status">目前沒有註解</Panel.Empty>
44
+ ) : (
45
+ <ol className="openpress-comments-list" aria-label="待處理註解列表">
46
+ {comments.map((comment) => (
47
+ <li className="openpress-comment-entry" data-openpress-comment-id={comment.id} key={comment.id}>
48
+ <button
49
+ type="button"
50
+ className="openpress-comment-entry__jump"
51
+ onClick={() => onSelect?.(comment)}
52
+ aria-label={`跳到註解 ${comment.id}`}
53
+ >
54
+ <p className="openpress-comment-entry__note" title={comment.note}>{comment.note}</p>
55
+ <p className="openpress-comment-entry__meta">
56
+ <code>{comment.path}:{comment.line}</code>
57
+ {comment.timestamp ? <span>{formatCommentTimestamp(comment.timestamp)}</span> : null}
58
+ </p>
59
+ </button>
60
+ <button
61
+ type="button"
62
+ className="openpress-comment-entry__clear"
63
+ onClick={() => void onClear(comment.id)}
64
+ disabled={busy}
65
+ aria-label={`清除註解 ${comment.id}`}
66
+ >
67
+ <Trash2 aria-hidden="true" />
68
+ </button>
69
+ </li>
70
+ ))}
71
+ </ol>
72
+ )}
73
+ </Panel.Body>
74
+ </Panel>
75
+ );
76
+ }
@@ -0,0 +1,29 @@
1
+ import { Fragment, type ReactNode } from "react";
2
+
3
+ // A WorkbenchPanel is a self-contained renderable slot in the workbench
4
+ // control surface. Each panel owns its own props by capturing them in render;
5
+ // the host (HtmlWorkbench) supplies the list and decides ordering.
6
+ export interface WorkbenchPanel {
7
+ id: string;
8
+ render: () => ReactNode;
9
+ }
10
+
11
+ export interface WorkbenchControlPanelProps {
12
+ panels: WorkbenchPanel[];
13
+ className?: string;
14
+ ariaLabel?: string;
15
+ }
16
+
17
+ export function WorkbenchControlPanel({
18
+ panels,
19
+ className = "openpress-control-panel",
20
+ ariaLabel = "控制面板",
21
+ }: WorkbenchControlPanelProps) {
22
+ return (
23
+ <div className={className} data-openpress-control-panel aria-label={ariaLabel}>
24
+ {panels.map((panel) => (
25
+ <Fragment key={panel.id}>{panel.render()}</Fragment>
26
+ ))}
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./Panel";
2
+ export * from "./PendingCommentsPanel";
3
+ export * from "./WorkbenchControlPanel";