@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,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,5 @@
1
+ export * from "./InlineInspectorLayer";
2
+ export * from "./inlineCommentModel";
3
+ export * from "./inspectorGeometryModel";
4
+ export * from "./inspectorModel";
5
+ export * from "./useInspectorComments";
@@ -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
+ }