@open-press/core 0.7.1 → 1.0.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 (144) hide show
  1. package/README.md +6 -3
  2. package/engine/cli.mjs +8 -8
  3. package/engine/commands/_shared.mjs +37 -15
  4. package/engine/commands/dev.mjs +2 -2
  5. package/engine/commands/image.mjs +29 -0
  6. package/engine/commands/skills-sync.mjs +71 -0
  7. package/engine/commands/typecheck.mjs +63 -1
  8. package/engine/commands/upgrade.mjs +3 -3
  9. package/engine/document-export.mjs +1 -1
  10. package/engine/output/chrome-pdf.mjs +110 -3
  11. package/engine/output/static-server.mjs +87 -9
  12. package/engine/react/comment-endpoint.mjs +13 -39
  13. package/engine/react/comment-marker.mjs +43 -19
  14. package/engine/react/document-entry.mjs +46 -28
  15. package/engine/react/document-export.mjs +328 -164
  16. package/engine/react/http-json.mjs +24 -0
  17. package/engine/react/mdx-compile.mjs +126 -3
  18. package/engine/react/measurement-css.mjs +114 -1
  19. package/engine/react/object-entities.mjs +204 -0
  20. package/engine/react/pagination/allocator.mjs +48 -3
  21. package/engine/react/pagination.mjs +1 -1
  22. package/engine/react/pipeline/allocate.mjs +41 -72
  23. package/engine/react/pipeline/frame-measurement.mjs +6 -0
  24. package/engine/react/press-tree-inspection.mjs +172 -0
  25. package/engine/react/project-asset-endpoint.mjs +6 -24
  26. package/engine/react/source-edit-endpoint.d.mts +10 -0
  27. package/engine/react/source-edit-endpoint.mjs +75 -0
  28. package/engine/react/sources/mdx-resolver.mjs +13 -15
  29. package/engine/react/style-discovery.mjs +23 -8
  30. package/engine/runtime/config.d.mts +8 -0
  31. package/engine/runtime/config.mjs +57 -60
  32. package/engine/runtime/file-utils.mjs +9 -1
  33. package/engine/runtime/file-walk.mjs +22 -0
  34. package/engine/runtime/inspection.mjs +1 -20
  35. package/engine/runtime/page-geometry.mjs +131 -0
  36. package/engine/runtime/path-utils.mjs +20 -0
  37. package/engine/runtime/source-text-tools.d.mts +102 -0
  38. package/engine/runtime/source-text-tools.mjs +551 -16
  39. package/engine/runtime/source-workspace.mjs +16 -34
  40. package/engine/runtime/validation.mjs +19 -10
  41. package/package.json +3 -5
  42. package/src/openpress/app/OpenPressApp.tsx +296 -0
  43. package/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +20 -9
  44. package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
  45. package/src/openpress/app/index.ts +2 -0
  46. package/src/openpress/core/Frame.tsx +26 -15
  47. package/src/openpress/core/FrameContext.tsx +10 -3
  48. package/src/openpress/core/MdxArea.tsx +11 -12
  49. package/src/openpress/core/Press.tsx +25 -4
  50. package/src/openpress/core/Workspace.tsx +36 -0
  51. package/src/openpress/core/cn.ts +4 -0
  52. package/src/openpress/core/index.tsx +11 -3
  53. package/src/openpress/core/primitives.tsx +74 -6
  54. package/src/openpress/core/types.ts +94 -41
  55. package/src/openpress/core/useSource.ts +1 -1
  56. package/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
  57. package/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
  58. package/src/openpress/{types.ts → document-model/documentTypes.ts} +51 -0
  59. package/src/openpress/document-model/index.ts +7 -0
  60. package/src/openpress/document-model/objectEntityModel.ts +55 -0
  61. package/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
  62. package/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
  63. package/src/openpress/document-model/workspaceManifestModel.ts +57 -0
  64. package/src/openpress/manuscript/index.tsx +49 -7
  65. package/src/openpress/mdx/index.ts +15 -7
  66. package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
  67. package/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
  68. package/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
  69. package/src/openpress/reader/index.ts +11 -0
  70. package/src/openpress/reader/pageViewportScaleModel.ts +73 -0
  71. package/src/openpress/reader/readerTypes.ts +4 -0
  72. package/src/openpress/reader/usePageViewportScale.ts +119 -0
  73. package/src/openpress/reader/usePanelState.ts +56 -0
  74. package/src/openpress/reader/useReaderHashSync.ts +61 -0
  75. package/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
  76. package/src/openpress/reader/useReaderRuntime.ts +146 -0
  77. package/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
  78. package/src/openpress/shared/Panel.tsx +77 -0
  79. package/src/openpress/shared/index.ts +4 -0
  80. package/src/openpress/shared/numberUtils.ts +3 -0
  81. package/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
  82. package/src/openpress/workbench/Workbench.tsx +506 -0
  83. package/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
  84. package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
  85. package/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
  86. package/src/openpress/workbench/actions/SearchControl.tsx +345 -0
  87. package/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
  88. package/src/openpress/workbench/actions/index.ts +6 -0
  89. package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
  90. package/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
  91. package/src/openpress/workbench/dialog/index.ts +1 -0
  92. package/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
  93. package/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
  94. package/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
  95. package/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
  96. package/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
  97. package/src/openpress/workbench/document/index.ts +10 -0
  98. package/src/openpress/workbench/index.ts +2 -0
  99. package/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
  100. package/src/openpress/workbench/inspector/index.ts +5 -0
  101. package/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
  102. package/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
  103. package/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
  104. package/src/openpress/workbench/inspector/useInspectorComments.ts +254 -0
  105. package/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
  106. package/src/openpress/workbench/mentions/index.ts +2 -0
  107. package/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
  108. package/src/openpress/workbench/panels/Panel.tsx +1 -0
  109. package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +80 -0
  110. package/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
  111. package/src/openpress/workbench/panels/index.ts +3 -0
  112. package/src/openpress/workbench/project/ProjectEntryPanel.tsx +525 -0
  113. package/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
  114. package/src/openpress/workbench/project/index.ts +2 -0
  115. package/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
  116. package/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
  117. package/src/openpress/workbench/shell/index.ts +1 -0
  118. package/src/openpress/workbench/workbenchFormatters.ts +120 -0
  119. package/src/openpress/workbench/workbenchTypes.ts +35 -0
  120. package/src/styles/openpress/print-route.css +0 -2
  121. package/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
  122. package/src/styles/openpress/public-viewer.css +25 -320
  123. package/src/styles/openpress/reader-runtime.css +252 -55
  124. package/src/styles/openpress/responsive.css +145 -270
  125. package/src/styles/openpress/workbench-panels.css +327 -178
  126. package/src/styles/openpress/workbench.css +986 -451
  127. package/src/styles/openpress/workspace-gallery.css +300 -0
  128. package/src/styles/openpress.css +2 -1
  129. package/tsconfig.json +1 -1
  130. package/vite.config.ts +50 -0
  131. package/engine/commands/init.mjs +0 -24
  132. package/engine/init.mjs +0 -90
  133. package/src/openpress/App.tsx +0 -127
  134. package/src/openpress/inspector.ts +0 -282
  135. package/src/openpress/projectWorkspace.tsx +0 -919
  136. package/src/openpress/readerRuntime.ts +0 -230
  137. package/src/openpress/workbench.tsx +0 -1265
  138. package/src/openpress/workbenchTypes.ts +0 -4
  139. /package/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
  140. /package/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
  141. /package/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
  142. /package/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
  143. /package/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
  144. /package/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
@@ -0,0 +1,254 @@
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
+ // Memoize the status message so its identity is stable while only
71
+ // composer text changes — the toolbar and other consumers that depend
72
+ // on it can then memoize without keystrokes invalidating their cache.
73
+ const inspectorCommentStatusMessage = useMemo(
74
+ () => formatInspectorCommentStatus(inspectorCommentStatus, inspectorCommentError),
75
+ [inspectorCommentStatus, inspectorCommentError],
76
+ );
77
+
78
+ const refreshPendingComments = useCallback(async () => {
79
+ if (!devMode) return;
80
+ setCommentsStatus("loading");
81
+ setCommentsError("");
82
+ try {
83
+ const comments = await fetchInspectorComments();
84
+ setPendingComments(comments);
85
+ setCommentsStatus("ready");
86
+ } catch (error) {
87
+ setCommentsStatus("failed");
88
+ setCommentsError(error instanceof Error ? error.message : String(error));
89
+ }
90
+ }, [devMode]);
91
+
92
+ const clearPendingComment = useCallback(async (id: string) => {
93
+ setCommentsStatus("clearing");
94
+ setCommentsError("");
95
+ try {
96
+ await clearInspectorComment({ id });
97
+ setPendingComments((comments) => comments.filter((comment) => comment.id !== id));
98
+ setInlineSavedCommentId((currentId) => (currentId === id ? null : currentId));
99
+ setCommentsStatus("ready");
100
+ } catch (error) {
101
+ setCommentsStatus("failed");
102
+ setCommentsError(error instanceof Error ? error.message : String(error));
103
+ }
104
+ }, []);
105
+
106
+ const handleSubmitInspectorComment = useCallback(async (event?: FormEvent<HTMLFormElement>) => {
107
+ event?.preventDefault();
108
+ if (inspectorCommentDisabled || !inspector.selectedBlock) return;
109
+ setInspectorCommentStatus("submitting");
110
+ setInspectorCommentError("");
111
+ try {
112
+ const note = inspectorCommentText.trim();
113
+ const placement = inspector.selectedTarget?.placement ?? "block";
114
+ if (activeInlineSavedComment) {
115
+ const result = await updateInspectorComment({
116
+ id: activeInlineSavedComment.id,
117
+ note,
118
+ placement,
119
+ });
120
+ setInlineSavedCommentId(result.comment?.id ?? activeInlineSavedComment.id);
121
+ } else {
122
+ const draft = createInspectorCommentDraft({
123
+ block: inspector.selectedBlock,
124
+ entity: inspector.selectedObjectEntity,
125
+ target: inspector.selectedTarget,
126
+ note,
127
+ placement,
128
+ });
129
+ const result = await submitInspectorComment({ draft });
130
+ if (result.comment?.id) {
131
+ setInlineSavedCommentId(result.comment.id);
132
+ }
133
+ }
134
+ setInspectorCommentText("");
135
+ setInspectorCommentStatus("saved");
136
+ void refreshPendingComments();
137
+ } catch (error) {
138
+ setInspectorCommentStatus("failed");
139
+ setInspectorCommentError(error instanceof Error ? error.message : String(error));
140
+ }
141
+ }, [
142
+ activeInlineSavedComment,
143
+ inspector.selectedBlock,
144
+ inspector.selectedObjectEntity,
145
+ inspector.selectedTarget,
146
+ inspector.selectedTarget?.placement,
147
+ inspectorCommentDisabled,
148
+ inspectorCommentText,
149
+ refreshPendingComments,
150
+ ]);
151
+
152
+ const handleOpenInlineSavedComment = useCallback((comment: InlineSavedComment) => {
153
+ setInlineSavedCommentId(comment.id);
154
+ setInspectorCommentText(comment.note);
155
+ setInspectorCommentStatus("idle");
156
+ setInspectorCommentError("");
157
+ }, []);
158
+
159
+ const handleRemoveInlineSavedComment = useCallback(async (comment: InlineSavedComment) => {
160
+ setInspectorCommentStatus("submitting");
161
+ setInspectorCommentError("");
162
+ try {
163
+ await clearInspectorComment({ id: comment.id });
164
+ setPendingComments((comments) => comments.filter((item) => item.id !== comment.id));
165
+ setInlineSavedCommentId((currentId) => (currentId === comment.id ? null : currentId));
166
+ setInspectorCommentText("");
167
+ setInspectorCommentStatus("idle");
168
+ inspector.selectTarget(null);
169
+ void refreshPendingComments();
170
+ } catch (error) {
171
+ setInspectorCommentStatus("failed");
172
+ setInspectorCommentError(error instanceof Error ? error.message : String(error));
173
+ }
174
+ }, [inspector, refreshPendingComments]);
175
+
176
+ const handleSelectPendingComment = useCallback((comment: PendingComment) => {
177
+ const inlineComment = inlineSavedComments.find((item) => item.id === comment.id)
178
+ ?? resolveInlineSavedComment(comment, sourceBlocksByPath)[0];
179
+ if (!inlineComment?.blockId) return;
180
+
181
+ const sourceBlock = sourceBlockMap[inlineComment.blockId];
182
+ if (typeof sourceBlock?.pageIndex === "number") {
183
+ onSelectWorkspacePage(sourceBlock.pageIndex, { behavior: "smooth" });
184
+ }
185
+
186
+ inspector.selectSelection({
187
+ objectId: inlineComment.objectId,
188
+ blockId: inlineComment.blockId,
189
+ placement: inlineComment.placement,
190
+ });
191
+ handleOpenInlineSavedComment(inlineComment);
192
+
193
+ window.requestAnimationFrame(() => {
194
+ const selector = inlineComment.objectId
195
+ ? `[data-openpress-object-id="${cssEscape(inlineComment.objectId)}"]`
196
+ : `[data-openpress-block-id="${cssEscape(inlineComment.blockId!)}"]`;
197
+ sourceContainerRef.current?.querySelector<HTMLElement>(selector)?.scrollIntoView({
198
+ behavior: "smooth",
199
+ block: "center",
200
+ });
201
+ });
202
+ }, [
203
+ handleOpenInlineSavedComment,
204
+ inlineSavedComments,
205
+ inspector,
206
+ onSelectWorkspacePage,
207
+ sourceBlockMap,
208
+ sourceBlocksByPath,
209
+ sourceContainerRef,
210
+ ]);
211
+
212
+ // Reset composer state when the inspector selection changes.
213
+ useEffect(() => {
214
+ setInspectorCommentStatus("idle");
215
+ setInspectorCommentError("");
216
+ setInspectorCommentText("");
217
+ }, [inspector.selectedBlockId, inspector.selectedTarget?.placement]);
218
+
219
+ // Drop the inline saved id if its comment is no longer reachable.
220
+ useEffect(() => {
221
+ if (inlineSavedCommentId && !activeInlineSavedComment) {
222
+ setInlineSavedCommentId(null);
223
+ }
224
+ }, [activeInlineSavedComment, inlineSavedCommentId]);
225
+
226
+ // Initial + dev-mode refresh of pending comments.
227
+ useEffect(() => {
228
+ if (!devMode) return;
229
+ void refreshPendingComments();
230
+ }, [devMode, refreshPendingComments]);
231
+
232
+ return {
233
+ pendingComments,
234
+ commentsStatus,
235
+ commentsError,
236
+ inspectorCommentText,
237
+ inspectorCommentStatus,
238
+ inspectorCommentStatusMessage,
239
+ inspectorCommentDisabled,
240
+ inlineSavedComments,
241
+ activeInlineSavedComment,
242
+ setInspectorCommentText,
243
+ refreshPendingComments,
244
+ clearPendingComment,
245
+ handleSubmitInspectorComment,
246
+ handleOpenInlineSavedComment,
247
+ handleRemoveInlineSavedComment,
248
+ handleSelectPendingComment,
249
+ };
250
+ }
251
+
252
+ function cssEscape(value: string) {
253
+ return globalThis.CSS?.escape ? globalThis.CSS.escape(value) : value.replace(/["\\]/g, "\\$&");
254
+ }
@@ -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,80 @@
1
+ import { memo } from "react";
2
+ import { Trash2 } from "lucide-react";
3
+ import type { PendingComment } from "../inspector";
4
+ import {
5
+ formatCommentTimestamp,
6
+ formatCommentsCount,
7
+ } from "../workbenchFormatters";
8
+ import type { PendingCommentsStatus } from "../workbenchTypes";
9
+ import { Panel } from "./Panel";
10
+
11
+ function PendingCommentsPanelImpl({
12
+ comments,
13
+ status,
14
+ error,
15
+ onClear,
16
+ onSelect,
17
+ }: {
18
+ comments: PendingComment[];
19
+ status: PendingCommentsStatus;
20
+ error: string;
21
+ onClear: (id: string) => Promise<void>;
22
+ onSelect?: (comment: PendingComment) => void;
23
+ }) {
24
+ const busy = status === "loading" || status === "clearing";
25
+
26
+ return (
27
+ <Panel
28
+ className="openpress-comments-panel openpress-comments-panel--embedded openpress-panel--compact"
29
+ data-openpress-comments-panel
30
+ aria-label="待處理註解"
31
+ >
32
+ <Panel.Header>
33
+ <div className="openpress-panel-heading-stack">
34
+ <Panel.Kicker>Comments</Panel.Kicker>
35
+ <Panel.Title>待處理註解</Panel.Title>
36
+ <Panel.Description>{formatCommentsCount(comments.length, status)}</Panel.Description>
37
+ </div>
38
+ </Panel.Header>
39
+
40
+ <Panel.Body>
41
+ {error ? <Panel.Error>{error}</Panel.Error> : null}
42
+
43
+ {comments.length === 0 && status !== "loading" ? (
44
+ <Panel.Empty role="status">目前沒有註解</Panel.Empty>
45
+ ) : (
46
+ <ol className="openpress-comments-list" aria-label="待處理註解列表">
47
+ {comments.map((comment) => (
48
+ <li className="openpress-comment-entry" data-openpress-comment-id={comment.id} key={comment.id}>
49
+ <button
50
+ type="button"
51
+ className="openpress-comment-entry__jump"
52
+ onClick={() => onSelect?.(comment)}
53
+ aria-label={`跳到註解 ${comment.id}`}
54
+ >
55
+ <p className="openpress-comment-entry__note" title={comment.note}>{comment.note}</p>
56
+ <p className="openpress-comment-entry__meta">
57
+ <code>{comment.path}:{comment.line}</code>
58
+ {comment.timestamp ? <span>{formatCommentTimestamp(comment.timestamp)}</span> : null}
59
+ </p>
60
+ </button>
61
+ <button
62
+ type="button"
63
+ className="openpress-comment-entry__clear"
64
+ onClick={() => void onClear(comment.id)}
65
+ disabled={busy}
66
+ aria-label={`清除註解 ${comment.id}`}
67
+ >
68
+ <Trash2 aria-hidden="true" />
69
+ </button>
70
+ </li>
71
+ ))}
72
+ </ol>
73
+ )}
74
+ </Panel.Body>
75
+ </Panel>
76
+ );
77
+ }
78
+
79
+ export const PendingCommentsPanel = memo(PendingCommentsPanelImpl);
80
+ PendingCommentsPanel.displayName = "PendingCommentsPanel";
@@ -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";