@open-press/cli 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 (234) hide show
  1. package/README.md +29 -13
  2. package/dist/cli.js +44 -195
  3. package/package.json +4 -5
  4. package/template/core/AGENTS.md +18 -14
  5. package/template/core/CHANGELOG.md +57 -9
  6. package/template/core/README.md +6 -3
  7. package/template/core/engine/cli.mjs +8 -8
  8. package/template/core/engine/commands/_shared.mjs +37 -15
  9. package/template/core/engine/commands/dev.mjs +2 -2
  10. package/template/core/engine/commands/image.mjs +29 -0
  11. package/template/core/engine/commands/skills-sync.mjs +71 -0
  12. package/template/core/engine/commands/typecheck.mjs +63 -1
  13. package/template/core/engine/commands/upgrade.mjs +3 -3
  14. package/template/core/engine/document-export.mjs +1 -1
  15. package/template/core/engine/output/chrome-pdf.mjs +110 -3
  16. package/template/core/engine/output/static-server.mjs +87 -9
  17. package/template/core/engine/react/comment-endpoint.mjs +13 -39
  18. package/template/core/engine/react/comment-marker.mjs +43 -19
  19. package/template/core/engine/react/document-entry.mjs +46 -28
  20. package/template/core/engine/react/document-export.mjs +328 -164
  21. package/template/core/engine/react/http-json.mjs +24 -0
  22. package/template/core/engine/react/mdx-compile.mjs +126 -3
  23. package/template/core/engine/react/measurement-css.mjs +114 -1
  24. package/template/core/engine/react/object-entities.mjs +204 -0
  25. package/template/core/engine/react/pagination/allocator.mjs +48 -3
  26. package/template/core/engine/react/pagination.mjs +1 -1
  27. package/template/core/engine/react/pipeline/allocate.mjs +41 -72
  28. package/template/core/engine/react/pipeline/frame-measurement.mjs +6 -0
  29. package/template/core/engine/react/press-tree-inspection.mjs +172 -0
  30. package/template/core/engine/react/project-asset-endpoint.mjs +6 -24
  31. package/template/core/engine/react/source-edit-endpoint.d.mts +10 -0
  32. package/template/core/engine/react/source-edit-endpoint.mjs +75 -0
  33. package/template/core/engine/react/sources/mdx-resolver.mjs +13 -15
  34. package/template/core/engine/react/style-discovery.mjs +23 -8
  35. package/template/core/engine/runtime/config.d.mts +8 -0
  36. package/template/core/engine/runtime/config.mjs +57 -60
  37. package/template/core/engine/runtime/file-utils.mjs +9 -1
  38. package/template/core/engine/runtime/file-walk.mjs +22 -0
  39. package/template/core/engine/runtime/inspection.mjs +1 -20
  40. package/template/core/engine/runtime/page-geometry.mjs +131 -0
  41. package/template/core/engine/runtime/path-utils.mjs +20 -0
  42. package/template/core/engine/runtime/source-text-tools.d.mts +102 -0
  43. package/template/core/engine/runtime/source-text-tools.mjs +551 -16
  44. package/template/core/engine/runtime/source-workspace.mjs +16 -34
  45. package/template/core/engine/runtime/validation.mjs +19 -10
  46. package/template/core/openpress.config.mjs +3 -7
  47. package/template/core/package.json +3 -5
  48. package/template/core/src/main.tsx +2 -2
  49. package/template/core/src/openpress/app/OpenPressApp.tsx +296 -0
  50. package/template/core/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +20 -9
  51. package/template/core/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
  52. package/template/core/src/openpress/app/index.ts +2 -0
  53. package/template/core/src/openpress/core/Frame.tsx +26 -15
  54. package/template/core/src/openpress/core/FrameContext.tsx +10 -3
  55. package/template/core/src/openpress/core/MdxArea.tsx +11 -12
  56. package/template/core/src/openpress/core/Press.tsx +25 -4
  57. package/template/core/src/openpress/core/Workspace.tsx +36 -0
  58. package/template/core/src/openpress/core/cn.ts +4 -0
  59. package/template/core/src/openpress/core/index.tsx +11 -3
  60. package/template/core/src/openpress/core/primitives.tsx +74 -6
  61. package/template/core/src/openpress/core/types.ts +94 -41
  62. package/template/core/src/openpress/core/useSource.ts +1 -1
  63. package/template/core/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
  64. package/template/core/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
  65. package/template/core/src/openpress/{types.ts → document-model/documentTypes.ts} +51 -0
  66. package/template/core/src/openpress/document-model/index.ts +7 -0
  67. package/template/core/src/openpress/document-model/objectEntityModel.ts +55 -0
  68. package/template/core/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
  69. package/template/core/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
  70. package/template/core/src/openpress/document-model/workspaceManifestModel.ts +57 -0
  71. package/template/core/src/openpress/manuscript/index.tsx +49 -7
  72. package/template/core/src/openpress/mdx/index.ts +15 -7
  73. package/template/core/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
  74. package/template/core/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
  75. package/template/core/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
  76. package/template/core/src/openpress/reader/index.ts +11 -0
  77. package/template/core/src/openpress/reader/pageViewportScaleModel.ts +73 -0
  78. package/template/core/src/openpress/reader/readerTypes.ts +4 -0
  79. package/template/core/src/openpress/reader/usePageViewportScale.ts +119 -0
  80. package/template/core/src/openpress/reader/usePanelState.ts +56 -0
  81. package/template/core/src/openpress/reader/useReaderHashSync.ts +61 -0
  82. package/template/core/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
  83. package/template/core/src/openpress/reader/useReaderRuntime.ts +146 -0
  84. package/template/core/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
  85. package/template/core/src/openpress/shared/Panel.tsx +77 -0
  86. package/template/core/src/openpress/shared/index.ts +4 -0
  87. package/template/core/src/openpress/shared/numberUtils.ts +3 -0
  88. package/template/core/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
  89. package/template/core/src/openpress/workbench/Workbench.tsx +506 -0
  90. package/template/core/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
  91. package/template/core/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
  92. package/template/core/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
  93. package/template/core/src/openpress/workbench/actions/SearchControl.tsx +345 -0
  94. package/template/core/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
  95. package/template/core/src/openpress/workbench/actions/index.ts +6 -0
  96. package/template/core/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
  97. package/template/core/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
  98. package/template/core/src/openpress/workbench/dialog/index.ts +1 -0
  99. package/template/core/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
  100. package/template/core/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
  101. package/template/core/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
  102. package/template/core/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
  103. package/template/core/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
  104. package/template/core/src/openpress/workbench/document/index.ts +10 -0
  105. package/template/core/src/openpress/workbench/index.ts +2 -0
  106. package/template/core/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
  107. package/template/core/src/openpress/workbench/inspector/index.ts +5 -0
  108. package/template/core/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
  109. package/template/core/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
  110. package/template/core/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
  111. package/template/core/src/openpress/workbench/inspector/useInspectorComments.ts +254 -0
  112. package/template/core/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
  113. package/template/core/src/openpress/workbench/mentions/index.ts +2 -0
  114. package/template/core/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
  115. package/template/core/src/openpress/workbench/panels/Panel.tsx +1 -0
  116. package/template/core/src/openpress/workbench/panels/PendingCommentsPanel.tsx +80 -0
  117. package/template/core/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
  118. package/template/core/src/openpress/workbench/panels/index.ts +3 -0
  119. package/template/core/src/openpress/workbench/project/ProjectEntryPanel.tsx +525 -0
  120. package/template/core/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
  121. package/template/core/src/openpress/workbench/project/index.ts +2 -0
  122. package/template/core/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
  123. package/template/core/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
  124. package/template/core/src/openpress/workbench/shell/index.ts +1 -0
  125. package/template/core/src/openpress/workbench/workbenchFormatters.ts +120 -0
  126. package/template/core/src/openpress/workbench/workbenchTypes.ts +35 -0
  127. package/template/core/src/styles/openpress/print-route.css +0 -2
  128. package/template/core/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
  129. package/template/core/src/styles/openpress/public-viewer.css +25 -320
  130. package/template/core/src/styles/openpress/reader-runtime.css +252 -55
  131. package/template/core/src/styles/openpress/responsive.css +145 -270
  132. package/template/core/src/styles/openpress/workbench-panels.css +327 -178
  133. package/template/core/src/styles/openpress/workbench.css +986 -451
  134. package/template/core/src/styles/openpress/workspace-gallery.css +300 -0
  135. package/template/core/src/styles/openpress.css +2 -1
  136. package/template/core/tsconfig.json +1 -1
  137. package/template/core/vite.config.ts +50 -0
  138. package/template/core/engine/commands/init.mjs +0 -24
  139. package/template/core/engine/init.mjs +0 -90
  140. package/template/core/src/openpress/App.tsx +0 -127
  141. package/template/core/src/openpress/inspector.ts +0 -282
  142. package/template/core/src/openpress/projectWorkspace.tsx +0 -919
  143. package/template/core/src/openpress/readerRuntime.ts +0 -230
  144. package/template/core/src/openpress/workbench.tsx +0 -1265
  145. package/template/core/src/openpress/workbenchTypes.ts +0 -4
  146. package/template/packs/academic-paper/document/chapters/01-introduction/content/01-introduction.mdx +0 -35
  147. package/template/packs/academic-paper/document/chapters/02-methods/content/01-methods.mdx +0 -50
  148. package/template/packs/academic-paper/document/chapters/03-results-and-discussion/content/01-results.mdx +0 -47
  149. package/template/packs/academic-paper/document/chapters/04-acknowledgment/content/01-acknowledgment.mdx +0 -26
  150. package/template/packs/academic-paper/document/chapters/05-references/content/01-references.mdx +0 -32
  151. package/template/packs/academic-paper/document/components/ChapterOpenerVisual/index.tsx +0 -76
  152. package/template/packs/academic-paper/document/components/Page.tsx +0 -60
  153. package/template/packs/academic-paper/document/components/TokenSwatchGrid/index.tsx +0 -46
  154. package/template/packs/academic-paper/document/components/TokenSwatchGrid/style.css +0 -63
  155. package/template/packs/academic-paper/document/components/TypeSpecimen/index.tsx +0 -38
  156. package/template/packs/academic-paper/document/components/TypeSpecimen/style.css +0 -111
  157. package/template/packs/academic-paper/document/design.md +0 -279
  158. package/template/packs/academic-paper/document/index.tsx +0 -123
  159. package/template/packs/academic-paper/document/media/README.md +0 -13
  160. package/template/packs/academic-paper/document/media/figure-placeholder.svg +0 -9
  161. package/template/packs/academic-paper/document/openpress.config.mjs +0 -26
  162. package/template/packs/academic-paper/document/theme/README.md +0 -11
  163. package/template/packs/academic-paper/document/theme/base/page-contract.css +0 -522
  164. package/template/packs/academic-paper/document/theme/base/print.css +0 -93
  165. package/template/packs/academic-paper/document/theme/base/typography.css +0 -333
  166. package/template/packs/academic-paper/document/theme/fonts.css +0 -3
  167. package/template/packs/academic-paper/document/theme/page-surfaces/back-cover.css +0 -43
  168. package/template/packs/academic-paper/document/theme/page-surfaces/chapter-opener.css +0 -205
  169. package/template/packs/academic-paper/document/theme/page-surfaces/cover.css +0 -294
  170. package/template/packs/academic-paper/document/theme/page-surfaces/toc.css +0 -149
  171. package/template/packs/academic-paper/document/theme/patterns/_chart-frame.css +0 -49
  172. package/template/packs/academic-paper/document/theme/patterns/figure-grid.css +0 -68
  173. package/template/packs/academic-paper/document/theme/patterns/table-utilities.css +0 -66
  174. package/template/packs/academic-paper/document/theme/shell/reader-controls.css +0 -761
  175. package/template/packs/academic-paper/document/theme/tokens.css +0 -80
  176. package/template/packs/academic-paper/openpress.config.mjs +0 -5
  177. package/template/packs/claude-document/document/chapters/01-document-shape/content/01-document-shape.mdx +0 -51
  178. package/template/packs/claude-document/document/chapters/02-review-loop/content/01-review-loop.mdx +0 -31
  179. package/template/packs/claude-document/document/components/ChapterOpenerVisual.tsx +0 -96
  180. package/template/packs/claude-document/document/components/Page.tsx +0 -37
  181. package/template/packs/claude-document/document/design.md +0 -142
  182. package/template/packs/claude-document/document/index.tsx +0 -94
  183. package/template/packs/claude-document/document/media/README.md +0 -13
  184. package/template/packs/claude-document/document/openpress.config.mjs +0 -26
  185. package/template/packs/claude-document/document/theme/README.md +0 -15
  186. package/template/packs/claude-document/document/theme/base/page-contract.css +0 -525
  187. package/template/packs/claude-document/document/theme/base/print.css +0 -93
  188. package/template/packs/claude-document/document/theme/base/typography.css +0 -612
  189. package/template/packs/claude-document/document/theme/fonts.css +0 -4
  190. package/template/packs/claude-document/document/theme/page-surfaces/back-cover.css +0 -72
  191. package/template/packs/claude-document/document/theme/page-surfaces/chapter-opener.css +0 -236
  192. package/template/packs/claude-document/document/theme/page-surfaces/cover.css +0 -309
  193. package/template/packs/claude-document/document/theme/page-surfaces/toc.css +0 -225
  194. package/template/packs/claude-document/document/theme/patterns/_chart-frame.css +0 -53
  195. package/template/packs/claude-document/document/theme/patterns/figure-grid.css +0 -68
  196. package/template/packs/claude-document/document/theme/patterns/table-utilities.css +0 -66
  197. package/template/packs/claude-document/document/theme/shell/reader-controls.css +0 -789
  198. package/template/packs/claude-document/document/theme/tokens.css +0 -89
  199. package/template/packs/claude-document/openpress.config.mjs +0 -5
  200. package/template/packs/editorial-monograph/document/chapters/01-product-and-use-cases/content/01-product-and-use-cases.mdx +0 -31
  201. package/template/packs/editorial-monograph/document/chapters/02-workflow/content/01-workflow.mdx +0 -89
  202. package/template/packs/editorial-monograph/document/chapters/03-agent-skills-contributors/content/01-agent-skills-contributors.mdx +0 -51
  203. package/template/packs/editorial-monograph/document/chapters/04-validation-deploy/content/01-validation-deploy.mdx +0 -39
  204. package/template/packs/editorial-monograph/document/components/ChapterOpenerVisual/index.tsx +0 -76
  205. package/template/packs/editorial-monograph/document/components/Page.tsx +0 -37
  206. package/template/packs/editorial-monograph/document/components/TokenSwatchGrid/index.tsx +0 -46
  207. package/template/packs/editorial-monograph/document/components/TokenSwatchGrid/style.css +0 -63
  208. package/template/packs/editorial-monograph/document/components/TypeSpecimen/index.tsx +0 -38
  209. package/template/packs/editorial-monograph/document/components/TypeSpecimen/style.css +0 -111
  210. package/template/packs/editorial-monograph/document/design.md +0 -279
  211. package/template/packs/editorial-monograph/document/index.tsx +0 -97
  212. package/template/packs/editorial-monograph/document/media/README.md +0 -13
  213. package/template/packs/editorial-monograph/document/openpress.config.mjs +0 -26
  214. package/template/packs/editorial-monograph/document/theme/README.md +0 -11
  215. package/template/packs/editorial-monograph/document/theme/base/page-contract.css +0 -505
  216. package/template/packs/editorial-monograph/document/theme/base/print.css +0 -93
  217. package/template/packs/editorial-monograph/document/theme/base/typography.css +0 -336
  218. package/template/packs/editorial-monograph/document/theme/fonts.css +0 -3
  219. package/template/packs/editorial-monograph/document/theme/page-surfaces/back-cover.css +0 -43
  220. package/template/packs/editorial-monograph/document/theme/page-surfaces/chapter-opener.css +0 -205
  221. package/template/packs/editorial-monograph/document/theme/page-surfaces/cover.css +0 -147
  222. package/template/packs/editorial-monograph/document/theme/page-surfaces/toc.css +0 -149
  223. package/template/packs/editorial-monograph/document/theme/patterns/_chart-frame.css +0 -49
  224. package/template/packs/editorial-monograph/document/theme/patterns/figure-grid.css +0 -68
  225. package/template/packs/editorial-monograph/document/theme/patterns/table-utilities.css +0 -66
  226. package/template/packs/editorial-monograph/document/theme/shell/reader-controls.css +0 -761
  227. package/template/packs/editorial-monograph/document/theme/tokens.css +0 -80
  228. package/template/packs/editorial-monograph/openpress.config.mjs +0 -5
  229. /package/template/core/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
  230. /package/template/core/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
  231. /package/template/core/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
  232. /package/template/core/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
  233. /package/template/core/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
  234. /package/template/core/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";