@open-press/cli 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +6 -1
  2. package/package.json +1 -1
  3. package/template/core/AGENTS.md +126 -0
  4. package/template/core/CHANGELOG.md +65 -0
  5. package/template/core/engine/commands/dev.mjs +2 -2
  6. package/template/core/engine/commands/upgrade.mjs +47 -5
  7. package/template/core/engine/output/chrome-pdf.mjs +18 -3
  8. package/template/core/engine/output/static-server.mjs +39 -0
  9. package/template/core/engine/react/comment-endpoint.mjs +13 -39
  10. package/template/core/engine/react/comment-marker.mjs +30 -6
  11. package/template/core/engine/react/document-entry.mjs +11 -0
  12. package/template/core/engine/react/document-export.mjs +45 -5
  13. package/template/core/engine/react/http-json.mjs +24 -0
  14. package/template/core/engine/react/mdx-compile.mjs +187 -3
  15. package/template/core/engine/react/measurement-css.mjs +93 -1
  16. package/template/core/engine/react/object-entities.mjs +119 -0
  17. package/template/core/engine/react/pipeline/allocate.mjs +10 -7
  18. package/template/core/engine/react/pipeline/frame-measurement.mjs +40 -9
  19. package/template/core/engine/react/project-asset-endpoint.mjs +6 -24
  20. package/template/core/engine/react/source-edit-endpoint.d.mts +10 -0
  21. package/template/core/engine/react/source-edit-endpoint.mjs +75 -0
  22. package/template/core/engine/react/sources/mdx-resolver.mjs +12 -14
  23. package/template/core/engine/react/style-discovery.mjs +1 -4
  24. package/template/core/engine/runtime/file-walk.mjs +22 -0
  25. package/template/core/engine/runtime/inspection.mjs +1 -20
  26. package/template/core/engine/runtime/path-utils.mjs +20 -0
  27. package/template/core/engine/runtime/source-text-tools.d.mts +102 -0
  28. package/template/core/engine/runtime/source-text-tools.mjs +551 -16
  29. package/template/core/engine/runtime/source-workspace.mjs +4 -31
  30. package/template/core/package.json +1 -1
  31. package/template/core/src/main.tsx +2 -2
  32. package/template/core/src/openpress/{App.tsx → app/OpenPressApp.tsx} +25 -12
  33. package/template/core/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +10 -7
  34. package/template/core/src/openpress/app/index.ts +2 -0
  35. package/template/core/src/openpress/core/Frame.tsx +9 -11
  36. package/template/core/src/openpress/core/FrameContext.tsx +8 -3
  37. package/template/core/src/openpress/core/MdxArea.tsx +11 -12
  38. package/template/core/src/openpress/core/cn.ts +4 -0
  39. package/template/core/src/openpress/core/index.tsx +2 -1
  40. package/template/core/src/openpress/core/primitives.tsx +29 -8
  41. package/template/core/src/openpress/core/types.ts +8 -0
  42. package/template/core/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
  43. package/template/core/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
  44. package/template/core/src/openpress/{types.ts → document-model/documentTypes.ts} +42 -0
  45. package/template/core/src/openpress/document-model/index.ts +6 -0
  46. package/template/core/src/openpress/document-model/objectEntityModel.ts +51 -0
  47. package/template/core/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
  48. package/template/core/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
  49. package/template/core/src/openpress/manuscript/index.tsx +49 -7
  50. package/template/core/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
  51. package/template/core/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
  52. package/template/core/src/openpress/reader/index.ts +10 -0
  53. package/template/core/src/openpress/reader/pageViewportScaleModel.ts +73 -0
  54. package/template/core/src/openpress/reader/readerTypes.ts +4 -0
  55. package/template/core/src/openpress/reader/usePageViewportScale.ts +119 -0
  56. package/template/core/src/openpress/reader/usePanelState.ts +56 -0
  57. package/template/core/src/openpress/reader/useReaderHashSync.ts +61 -0
  58. package/template/core/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
  59. package/template/core/src/openpress/reader/useReaderRuntime.ts +146 -0
  60. package/template/core/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
  61. package/template/core/src/openpress/shared/Panel.tsx +77 -0
  62. package/template/core/src/openpress/shared/index.ts +4 -0
  63. package/template/core/src/openpress/shared/numberUtils.ts +3 -0
  64. package/template/core/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
  65. package/template/core/src/openpress/workbench/Workbench.tsx +407 -0
  66. package/template/core/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
  67. package/template/core/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
  68. package/template/core/src/openpress/workbench/actions/SearchControl.tsx +345 -0
  69. package/template/core/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
  70. package/template/core/src/openpress/workbench/actions/index.ts +5 -0
  71. package/template/core/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
  72. package/template/core/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
  73. package/template/core/src/openpress/workbench/dialog/index.ts +1 -0
  74. package/template/core/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
  75. package/template/core/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
  76. package/template/core/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
  77. package/template/core/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
  78. package/template/core/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
  79. package/template/core/src/openpress/workbench/document/index.ts +10 -0
  80. package/template/core/src/openpress/workbench/index.ts +2 -0
  81. package/template/core/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
  82. package/template/core/src/openpress/workbench/inspector/index.ts +5 -0
  83. package/template/core/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
  84. package/template/core/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
  85. package/template/core/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
  86. package/template/core/src/openpress/workbench/inspector/useInspectorComments.ts +248 -0
  87. package/template/core/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
  88. package/template/core/src/openpress/workbench/mentions/index.ts +2 -0
  89. package/template/core/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
  90. package/template/core/src/openpress/workbench/panels/Panel.tsx +1 -0
  91. package/template/core/src/openpress/workbench/panels/PendingCommentsPanel.tsx +76 -0
  92. package/template/core/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
  93. package/template/core/src/openpress/workbench/panels/index.ts +3 -0
  94. package/template/core/src/openpress/workbench/project/ProjectEntryPanel.tsx +523 -0
  95. package/template/core/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
  96. package/template/core/src/openpress/workbench/project/index.ts +2 -0
  97. package/template/core/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
  98. package/template/core/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
  99. package/template/core/src/openpress/workbench/shell/index.ts +1 -0
  100. package/template/core/src/openpress/workbench/workbenchFormatters.ts +120 -0
  101. package/template/core/src/openpress/workbench/workbenchTypes.ts +35 -0
  102. package/template/core/src/styles/openpress/print-route.css +0 -2
  103. package/template/core/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
  104. package/template/core/src/styles/openpress/public-viewer.css +25 -320
  105. package/template/core/src/styles/openpress/reader-runtime.css +243 -55
  106. package/template/core/src/styles/openpress/responsive.css +145 -270
  107. package/template/core/src/styles/openpress/workbench-panels.css +214 -178
  108. package/template/core/src/styles/openpress/workbench.css +986 -451
  109. package/template/core/src/styles/openpress.css +1 -1
  110. package/template/core/vite.config.ts +50 -0
  111. package/template/packs/academic-paper/document/chapters/01-introduction/content/01-introduction.mdx +26 -12
  112. package/template/packs/academic-paper/document/chapters/02-methods/content/01-methods.mdx +37 -17
  113. package/template/packs/academic-paper/document/chapters/03-results-and-discussion/content/01-results.mdx +34 -16
  114. package/template/packs/academic-paper/document/chapters/04-acknowledgment/content/01-acknowledgment.mdx +22 -8
  115. package/template/packs/academic-paper/document/chapters/05-references/content/01-references.mdx +20 -15
  116. package/template/packs/academic-paper/document/components/Page.tsx +26 -3
  117. package/template/packs/academic-paper/document/index.tsx +51 -59
  118. package/template/packs/academic-paper/document/media/figure-placeholder.svg +9 -0
  119. package/template/packs/academic-paper/document/theme/base/page-contract.css +30 -13
  120. package/template/packs/academic-paper/document/theme/base/typography.css +30 -33
  121. package/template/packs/academic-paper/document/theme/page-surfaces/cover.css +74 -47
  122. package/template/core/src/openpress/inspector.ts +0 -282
  123. package/template/core/src/openpress/projectWorkspace.tsx +0 -919
  124. package/template/core/src/openpress/readerRuntime.ts +0 -230
  125. package/template/core/src/openpress/workbench.tsx +0 -1265
  126. package/template/core/src/openpress/workbenchTypes.ts +0 -4
  127. /package/template/core/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
  128. /package/template/core/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
  129. /package/template/core/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
  130. /package/template/core/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
  131. /package/template/core/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
  132. /package/template/core/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  Scaffolder for [open-press](https://github.com/quan0715/open-press) — an AI-first fixed-layout document workspace.
4
4
 
5
+ ## Prerequisite
6
+
7
+ Node.js 20 or newer with `npm` / `npx`.
8
+
5
9
  ## Quick start
6
10
 
7
11
  ```bash
@@ -20,7 +24,7 @@ npx @open-press/cli init <target> [flags]
20
24
 
21
25
  | Flag | Description |
22
26
  | -------------------- | --------------------------------------------------------------------------- |
23
- | `--pack <name>` | Style pack starter: `editorial-monograph` or `claude-document` |
27
+ | `--pack <name>` | Style pack starter: `editorial-monograph`, `claude-document`, or `academic-paper` |
24
28
  | `--title <s>` | Document title (written to `openpress.config.mjs`) |
25
29
  | `--subtitle <s>` | Document subtitle |
26
30
  | `--organization <s>` | Organization name |
@@ -46,6 +50,7 @@ Workspace commands (run via `npm run` or `node engine/cli.mjs`):
46
50
 
47
51
  ```
48
52
  npm run dev # start workbench
53
+ npm run openpress:export # refresh public/openpress/document.json
49
54
  npm run build # render production output (dist-react/)
50
55
  npm run preview # preview production build
51
56
  npm run openpress:validate # structural checks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-press/cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "Scaffolder for open-press — AI-first fixed-layout document workspaces.",
6
6
  "license": "MIT",
@@ -0,0 +1,126 @@
1
+ # Working on this open-press workspace
2
+
3
+ This directory is an **open-press workspace** scaffolded by
4
+ `@open-press/cli`. You (the agent) own:
5
+
6
+ - `document/` — chapters (MDX), components, theme, media. The actual content.
7
+ - `package.json` / `openpress.config.mjs` — project metadata + build config.
8
+ - `.agents/skills/` — agent skills installed by `npx skills` (auto-refreshed via `npx open-press upgrade`).
9
+
10
+ The rest of the tree is the open-press framework copied at scaffold time
11
+ (`engine/`, `src/`, `vite.config.ts`, etc). Treat it as vendored — don't
12
+ hand-edit unless the user explicitly asks; the next `npx open-press upgrade`
13
+ will rewrite it.
14
+
15
+ ## Quick reference
16
+
17
+ ```bash
18
+ npm run dev # workbench at http://127.0.0.1:5173/?dev=1
19
+ npm run openpress:validate # structural checks
20
+ npm run openpress:render # full render through chromium
21
+ npm run openpress:pdf # write document.pdf
22
+ npm run openpress:export # write public/openpress/document.json
23
+ npm run openpress:deploy # deploy via the configured adapter
24
+ npx open-press doctor # current vs latest version + pending migrations
25
+ npx open-press upgrade # apply the upgrade flow (see below)
26
+ ```
27
+
28
+ ## When the user asks to upgrade
29
+
30
+ Triggers: "升級 / 套件更新 / upgrade open-press / apply latest design /
31
+ update to vX.Y.Z" etc.
32
+
33
+ **Follow the upgrade SOP, do NOT manually diff template versions:**
34
+
35
+ 1. `npx open-press doctor` — confirm current vs latest, count pending
36
+ migrations.
37
+ 2. Ask the user "go ahead?" — briefly mention what will change (deps,
38
+ skills, migrations to read).
39
+ 3. `npx open-press upgrade` — refreshes `@open-press/core`, refreshes
40
+ installed skills, **fetches migration notes for each pending version
41
+ into `.openpress/migrations/<version>.md`** and lists the paths.
42
+ 4. For each migration file listed, read it fully. Each has a
43
+ `Document-level changes` section with `rg` find + rewrite rules.
44
+ Apply to `document/` with user confirmation.
45
+ 5. Verify:
46
+ ```bash
47
+ npm run openpress:validate
48
+ npm run openpress:render
49
+ ```
50
+ Fix anything broken using the migration notes.
51
+ 6. Report to the user: starting version → ending version, what was
52
+ applied, anything that needed manual judgement.
53
+
54
+ **Anti-pattern**: running `npx @open-press/cli@latest init` somewhere
55
+ and manually diffing against the workspace. The migration notes are the
56
+ authoritative source for what changed; fresh templates ship default
57
+ content that does not apply to a customised workspace.
58
+
59
+ ## When the user says "I changed X but the page didn't update"
60
+
61
+ Common cause: **the reader renders from a static `public/openpress/document.json`,
62
+ not from your live MDX / theme files.** Vite Hot Reload covers React UI
63
+ chrome (workbench panels, inspector, navigation) but it does **not**
64
+ regenerate `document.json`. So edits to:
65
+
66
+ - `document/chapters/**/*.mdx` (prose)
67
+ - `document/index.tsx` (Press tree, Cover/BackCover JSX)
68
+ - `document/components/**/*.tsx` (Page, openers, custom components)
69
+ - `document/theme/**` style files that affect pagination capacity
70
+ - `openpress.config.mjs` metadata (title, captionNumbering, …)
71
+
72
+ …all need a re-export before the workbench / public viewer reflect
73
+ the change:
74
+
75
+ ```bash
76
+ npm run openpress:export # regenerate public/openpress/document.json
77
+ # then refresh the browser
78
+ ```
79
+
80
+ Quick rules of thumb:
81
+
82
+ - Pure CSS edits under `document/theme/` that don't move blocks → HMR
83
+ is enough (CSS is hot-replaced).
84
+ - Anything that affects content, pagination, or metadata → re-export.
85
+ - `npm run openpress:render` is `export` + extra asset sync; either
86
+ works to refresh the JSON.
87
+
88
+ **Agent SOP**: after applying any non-CSS edit to `document/`, run
89
+ `npm run openpress:export` before telling the user "done". If they ask
90
+ "why didn't my change show up?", check whether `document.json` was
91
+ regenerated since the edit.
92
+
93
+ ## When the user reports a render / paginate issue
94
+
95
+ Press Tree paginates at build time. Common things to check:
96
+
97
+ 1. `npm run openpress:export` then inspect
98
+ `public/openpress/document.json` for `source.warnings` (chain
99
+ overflow, missing chains, etc.).
100
+ 2. `npm run openpress:validate` for structural issues
101
+ (missing entries, broken anchors).
102
+ 3. `npm run dev` and use the workbench inspector to find which MDX
103
+ block / Frame element is misbehaving — comments and inline
104
+ annotations work directly from there.
105
+
106
+ ## Skills
107
+
108
+ This workspace ships agent skills under `.agents/skills/`. If your
109
+ platform supports skills (Claude Code, Cursor, Codex, Cline, Gemini
110
+ CLI, …), prefer invoking them over re-reading this file:
111
+
112
+ - `openpress` — operate the workspace (CLI, validate, export, render,
113
+ PDF, deploy, search/replace, comments, upgrades, routing).
114
+ - `openpress-writing` — writing-time rules for MDX prose.
115
+ - `openpress-design` — theme tokens, layout, page surfaces.
116
+ - `openpress-deploy` — deployment workflows.
117
+ - `openpress-init` — scaffolding new workspaces.
118
+ - Plus any style-pack-specific skills installed by the user.
119
+
120
+ Skills are kept in sync by `npx skills upgrade` (run automatically
121
+ inside `npx open-press upgrade`).
122
+
123
+ ## Reporting issues
124
+
125
+ - Issues / questions: https://github.com/quan0715/open-press/issues
126
+ - Source: https://github.com/quan0715/open-press
@@ -1,5 +1,70 @@
1
1
  # @open-press/core
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **Workbench architecture**: the monolithic `workbench.tsx` is split into a modular structure under `workbench/{shell,panels,actions,inspector,mentions,project,document}/`. New panels: `DeploymentControl`, `SearchControl`, `PendingCommentsPanel`, `DocumentPanel`, `ProjectEntryPanel`, plus `WorkbenchShell` and `InlineInspectorLayer`.
8
+ - **Module reorganization**: source tree split into typed subdirectories:
9
+ - `app/`: `OpenPressApp`, `OpenPressRuntime` (replaces the old `App.tsx` + `renderer.tsx`)
10
+ - `document-model/`: `anchorMap`, `documentIndexes`, `documentTypes`, `objectEntityModel`, `projectIdentity`, `reactDocumentMetadata`
11
+ - `reader/`: `PublicReaderPage`, `ReaderNavigationPanel`, `useReaderRuntime`, registry/route/scroll/state helpers
12
+ - `shared/`: `frameScheduler`, `runtimeMode`, `Panel`, `numberUtils`
13
+ - **Object-entity model**: `Frame` and `MdxArea` now expose `data-openpress-object-id`. New `document-model/objectEntityModel` defines the id format.
14
+ - **`MediaFigure` / `ImageFigure`**: new core primitives that accept `src/alt/caption` and resolve relative paths to `/openpress/media/...` automatically.
15
+ - **`<Sections>` default page**: `page` prop becomes optional; when omitted the built-in `DefaultSectionPage` renders the standard manuscript frame.
16
+ - **Engine helpers**: new `engine/react/{http-json,object-entities,source-edit-endpoint}.mjs` and `engine/runtime/{file-walk,path-utils}.mjs` runtime helpers. `engine/runtime/source-text-tools` exports TypeScript definitions.
17
+ - **Dev endpoints**: vite plugin wires `/__openpress/search` and `/__openpress/source-edit` middlewares for the new workbench search + inline editing flows.
18
+ - **Inline source editor**: ships the `InlineSourceEditorLayer` UI on top of `useInlineDocumentEditor` + `/__openpress/source-edit`. The hook now uses a `MutationObserver` so newly inserted blocks become editable, and routes mouse clicks through `focus()` to preserve selection on `contenteditable` boundaries. Workbench wires `sourceEditorTarget` state into the layer.
19
+ - **Table editing in the source pipeline**: table captions are emitted as standalone source blocks (`kind=element`, `name=caption`, `layout="attached"`) with `data-openpress-block-id`/`data-openpress-object-id` markers and preserved source positions; the allocator treats `layout="attached"` blocks as non-paginable. `applySourceBlockTableCellEditToText` (in `engine/runtime/source-text-tools`) accepts a `cellIndex` so the inline source editor can target a single `<td>`.
20
+ - **Reader pagination**: arrow-key pagination now defers to the user's active text selection. Shift-arrow / mouse-drag selections no longer get swallowed by the page-turn shortcut.
21
+ - **Page zoom + spread layout**: new `reader/pageViewportScaleModel` + `usePageViewportScale` hook drive a `--openpress-page-viewport-scale` CSS variable on the page container; the workbench toolbar exposes a `PageZoomControl` dropdown with fit-width / fit-page / fixed percentages plus a one-page / two-page spread toggle.
22
+ - **Inspector cell-precision comments**: `CommentDraft` gains an optional `targetObjectId` so a comment can point at a sub-block (e.g. a single table cell) while still attributing the source position to its enclosing block. `formatInspectorHint` carries the value through to the wire hint.
23
+ - **Shared `WorkbenchDialog` shell**: portal + backdrop + header (eyebrow / title / title-meta / close) + optional footer. `DeploymentControl`, `SearchControl`, and `ProjectPreviewDialog` all render through this shell now, replacing the prior per-dialog scaffolding under `openpress-deploy-dialog-*` / `openpress-search-dialog-*` / `openpress-project-preview-dialog__*`.
24
+ - **`WorkbenchControlPanel` registry**: `HtmlWorkbench` now accepts an `extraControlPanels?: WorkbenchPanel[]` prop and renders the right-side panel from a `{ id, render }` registry. Built-in panels (pending comments, project entry) ship as the first entries.
25
+ - **Workbench state hooks**: extract `useDeploymentWorkbench` and `useInspectorComments` from `HtmlWorkbench`; `useReaderRuntime` is split into focused sub-hooks (`usePanelState`, `useReaderScrollAnchor`, `useReaderHashSync`, `useReaderKeyboardNav`).
26
+ - **`InlineInspectorLayer` memoization**: now wrapped in `React.memo` with a stable `geometryVersion` prop so the geometry / event listeners no longer rebuild on every parent render.
27
+ - **Panels open lazily**: `usePanelState` now defaults both panels closed, so the reader opens with a clean stage; resize never auto-opens them.
28
+
29
+ ### Patch Changes
30
+
31
+ - Inspector: fix comment-marker count and multi-target marker rendering.
32
+ - Inspector: object-entity id helpers consolidate in `document-model/objectEntityModel` instead of being duplicated inside `Frame`, `MdxArea`, manuscript `TocArea`, and `PublicReaderPage`.
33
+ - Inline editor: `useInlineDocumentEditor` exposes `onDocumentEdited`; `OpenPressApp` re-loads `/openpress/document.json` after a successful inline edit so derived indexes stay in sync.
34
+ - Dev: reset Vite optimizer cache so workspace-side dependencies are picked up.
35
+ - Workbench dialog: viewport-aware width + max-height so a big media preview doesn't blow up the dialog to full screen.
36
+ - Project composer: add `/apply-comments` to the skill mention list so pending comment resolution can be invoked from the workbench.
37
+ - Carries forward the 0.7.1 measurement + pagination fixes (font/image readiness, relative media src inlining, list-per-item paging, `OPENPRESS_DEBUG_ALLOC`, academic-paper starter body overflow).
38
+
39
+ ### Breaking Changes
40
+
41
+ - `FrameContext.consumeArea(chainId)` return type changes from `ReactNode | null` to `{ indexInFrame: number; blocks: ReactNode | null }`. Custom `Frame` consumers must read `.blocks`.
42
+ - `App` export is renamed to `OpenPressApp` and now lives under `@open-press/core/app`. The old `renderer.tsx` is replaced by `OpenPressRuntime`.
43
+ - `data-openpress-mdx-area-empty` is now always emitted (`"true"` / `"false"`). Selectors that relied on the attribute being absent need updating.
44
+ - Reader `ViewMode` collapses to `"paged"` only — the legacy `"reading"` flow mode is removed. Use `usePageViewportScale` for free-scaling instead.
45
+ - Several internal module paths moved into subdirectories (`document-model/`, `reader/`, `shared/`, `workbench/...`). Consumers that deep-imported from the openpress source must switch to the new barrels.
46
+ - Shared dialog scaffolding (backdrop, container, header, close button) moved from per-dialog class families (`openpress-deploy-dialog-backdrop`, `__panel`, `__panel header`, `__close`, etc.) to the shared `openpress-workbench-dialog*` family. Per-dialog modifier classes (`openpress-deploy-dialog`, `openpress-search-dialog`, `openpress-project-preview-dialog`) are still applied for dialog-specific styling. Selectors that targeted the old scaffolding names (notably `*-backdrop` and `__panel`) need updating; selectors that combine the modifier class with new `__heading` / `__footer` / `__close` modifiers continue to work.
47
+
48
+ ## 0.7.1
49
+
50
+ ### Patch Changes
51
+
52
+ - Measurement pipeline + pagination fixes:
53
+
54
+ - **Measurement**: wait on `document.fonts.ready`, image `load`/`error` + `decode()`,
55
+ and two `requestAnimationFrame` ticks before sampling block heights so figures
56
+ no longer under-measure on cold loads.
57
+ - **Measurement**: inline relative `media/`, `./media/`, and `/openpress/media/`
58
+ image sources during the SSR measurement pass (previously only the absolute
59
+ `/openpress/media/...` form was rewritten, leaving relative refs as broken).
60
+ - **MDX compile**: split bullet/numbered lists into per-item paginable blocks
61
+ so long lists can break across pages without losing ordered numbering.
62
+ - **Debug**: new `OPENPRESS_DEBUG_ALLOC` env var prints per-iteration allocator
63
+ state (mdxArea capacities, block heights, pagination hints, warnings).
64
+ - **Academic-paper starter**: `<MdxArea overflow="extend">` on the body and the
65
+ single-column `.reader-page--content .page-body` override removed so content
66
+ paginates naturally with the new allocator.
67
+
3
68
  ## 0.7.0
4
69
 
5
70
  ### Minor Changes
@@ -16,7 +16,7 @@ export async function run({ root, options }) {
16
16
  if (!options.noBuild) {
17
17
  console.log(`Command: ${formatNodeScriptCommand(root, CLI_ENTRY)} export .`);
18
18
  }
19
- console.log(`Command: npx vite --config vite.config.ts --host ${host} --port ${port}`);
19
+ console.log(`Command: npx vite --force --config vite.config.ts --host ${host} --port ${port}`);
20
20
  return 0;
21
21
  }
22
22
  if (!options.noBuild) {
@@ -27,7 +27,7 @@ export async function run({ root, options }) {
27
27
  await printDoctorNoticeIfStale(root);
28
28
 
29
29
  console.log(`OpenPress dev: ${url}`);
30
- return runCommand("npx", ["vite", "--config", "vite.config.ts", "--host", host, "--port", port], root);
30
+ return runCommand("npx", ["vite", "--force", "--config", "vite.config.ts", "--host", host, "--port", port], root);
31
31
  }
32
32
 
33
33
  async function printDoctorNoticeIfStale(root) {
@@ -1,9 +1,15 @@
1
1
  import { existsSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { diagnose } from "./doctor.mjs";
5
5
  import { runCommand } from "./_shared.mjs";
6
6
 
7
+ // Migration notes live in the framework repo, not in scaffolded workspaces.
8
+ // `npx open-press upgrade` fetches the notes for each pending version and
9
+ // caches them under `.openpress/migrations/` so agents can read locally.
10
+ const MIGRATION_REMOTE_BASE = "https://raw.githubusercontent.com/quan0715/open-press/main/docs/migrations";
11
+ const MIGRATION_CACHE_DIR = path.join(".openpress", "migrations");
12
+
7
13
  export async function run({ root, options }) {
8
14
  const dryRun = Boolean(options?.dryRun);
9
15
  const skipSkills = Boolean(options?.noSkills);
@@ -85,7 +91,11 @@ export async function run({ root, options }) {
85
91
  process.stdout.write(" (no migration docs in this version range)\n\n");
86
92
  } else {
87
93
  for (const m of migrationContents) {
88
- process.stdout.write(` ─ ${m.path}\n`);
94
+ if (m.path) {
95
+ process.stdout.write(` ─ ${m.path}${m.fetched ? " (fetched from github)" : ""}\n`);
96
+ } else {
97
+ process.stdout.write(` ─ ${m.version}.md (not found locally or on github — check the repo manually)\n`);
98
+ }
89
99
  }
90
100
  process.stdout.write(
91
101
  "\nAgent: open each file, identify document-level changes, grep document/ for affected patterns, propose edits before applying.\n",
@@ -107,11 +117,43 @@ async function hasCoreDep(root) {
107
117
 
108
118
  async function loadMigrations(root, versions) {
109
119
  const results = [];
120
+ const cacheDir = path.join(root, MIGRATION_CACHE_DIR);
121
+ await mkdir(cacheDir, { recursive: true });
122
+
110
123
  for (const v of versions) {
111
- const p = path.join(root, "docs", "migrations", `${v}.md`);
112
- if (existsSync(p)) {
113
- results.push({ version: v, path: path.relative(root, p) });
124
+ // Framework repo has docs/migrations/ at root — prefer local if present
125
+ // (covers the open-press framework repo itself acting as a workspace).
126
+ const localDocsPath = path.join(root, "docs", "migrations", `${v}.md`);
127
+ if (existsSync(localDocsPath)) {
128
+ results.push({ version: v, path: path.relative(root, localDocsPath), fetched: false });
129
+ continue;
130
+ }
131
+
132
+ // Otherwise fetch from GitHub raw and cache to .openpress/migrations/.
133
+ const cachedPath = path.join(cacheDir, `${v}.md`);
134
+ if (existsSync(cachedPath)) {
135
+ results.push({ version: v, path: path.relative(root, cachedPath), fetched: false });
136
+ continue;
137
+ }
138
+
139
+ const body = await fetchMigration(v);
140
+ if (body) {
141
+ await writeFile(cachedPath, body, "utf8");
142
+ results.push({ version: v, path: path.relative(root, cachedPath), fetched: true });
143
+ } else {
144
+ results.push({ version: v, path: null, fetched: false });
114
145
  }
115
146
  }
116
147
  return results;
117
148
  }
149
+
150
+ async function fetchMigration(version) {
151
+ const url = `${MIGRATION_REMOTE_BASE}/${version}.md`;
152
+ try {
153
+ const res = await fetch(url, { headers: { Accept: "text/plain" } });
154
+ if (!res.ok) return null;
155
+ return await res.text();
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
@@ -91,6 +91,13 @@ const DEFAULT_PRINT_OPTIONS = {
91
91
  marginLeft: 0,
92
92
  };
93
93
 
94
+ export const DEFAULT_PRINT_VIEWPORT = Object.freeze({
95
+ width: 1200,
96
+ height: 1698,
97
+ deviceScaleFactor: 1,
98
+ mobile: false,
99
+ });
100
+
94
101
  export async function printUrlToPdf({
95
102
  root,
96
103
  url,
@@ -98,6 +105,7 @@ export async function printUrlToPdf({
98
105
  chrome,
99
106
  waitForReady = waitForPrintReady,
100
107
  printOptions = {},
108
+ viewport = DEFAULT_PRINT_VIEWPORT,
101
109
  debuggingPortBase = 9600,
102
110
  debuggingPortRange = 300,
103
111
  profilePrefix = "chrome-pdf",
@@ -126,9 +134,7 @@ export async function printUrlToPdf({
126
134
  const tab = await waitForChromeTab(debuggingPort);
127
135
  const client = await connectChromeDevTools(tab.webSocketDebuggerUrl);
128
136
  try {
129
- await client.send("Page.enable");
130
- await client.send("Runtime.enable");
131
- await client.send("Emulation.setEmulatedMedia", { media: "print" });
137
+ await preparePdfPage(client, { viewport });
132
138
  await client.send("Page.navigate", { url });
133
139
  const readyResult = await waitForReady(client);
134
140
  const result = await client.send("Page.printToPDF", {
@@ -146,6 +152,15 @@ export async function printUrlToPdf({
146
152
  }
147
153
  }
148
154
 
155
+ export async function preparePdfPage(client, { viewport = DEFAULT_PRINT_VIEWPORT } = {}) {
156
+ await client.send("Page.enable");
157
+ await client.send("Runtime.enable");
158
+ if (viewport) {
159
+ await client.send("Emulation.setDeviceMetricsOverride", viewport);
160
+ }
161
+ await client.send("Emulation.setEmulatedMedia", { media: "print" });
162
+ }
163
+
149
164
  export async function evaluateUrlWithChrome({
150
165
  root,
151
166
  url,
@@ -3,7 +3,9 @@ import http from "node:http";
3
3
  import path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
  import { loadConfig, publicPdfHref } from "../runtime/config.mjs";
6
+ import { searchSourceText } from "../runtime/source-text-tools.mjs";
6
7
  import { handleProjectAssetRequest } from "../react/project-asset-endpoint.mjs";
8
+ import { handleSourceEditRequest } from "../react/source-edit-endpoint.mjs";
7
9
 
8
10
  const [rootArg = "dist", ...rest] = process.argv.slice(2);
9
11
  const host = valueAfter(rest, "--host") ?? "127.0.0.1";
@@ -32,6 +34,14 @@ const server = http.createServer(async (req, res) => {
32
34
  await handleStatusRequest(req, res);
33
35
  return;
34
36
  }
37
+ if (url.pathname === "/__openpress/search") {
38
+ await handleSearchRequest(req, res, url);
39
+ return;
40
+ }
41
+ if (url.pathname === "/__openpress/source-edit") {
42
+ await handleSourceEditRequest(req, res, { root: workspace });
43
+ return;
44
+ }
35
45
  if (url.pathname === "/__openpress/local-pdf-export") {
36
46
  await handleLocalPdfExportRequest(req, res);
37
47
  return;
@@ -103,6 +113,35 @@ async function handleStatusRequest(req, res) {
103
113
  });
104
114
  }
105
115
 
116
+ async function handleSearchRequest(req, res, url) {
117
+ if (req.method !== "GET") {
118
+ writeJson(res, 405, { ok: false, message: "Search endpoint requires GET." });
119
+ return;
120
+ }
121
+
122
+ const query = (url.searchParams.get("q") ?? "").trim();
123
+ if (!query) {
124
+ writeJson(res, 400, { ok: false, message: "Search query is required." });
125
+ return;
126
+ }
127
+
128
+ try {
129
+ const report = await searchSourceText({
130
+ config,
131
+ query,
132
+ scope: searchScopeFrom(url),
133
+ caseSensitive: url.searchParams.get("caseSensitive") === "true",
134
+ });
135
+ writeJson(res, 200, { ok: true, ...report });
136
+ } catch (error) {
137
+ writeJson(res, 500, { ok: false, message: error instanceof Error ? error.message : String(error) });
138
+ }
139
+ }
140
+
141
+ function searchScopeFrom(url) {
142
+ return url.searchParams.get("scope") === "all" ? "all" : "content";
143
+ }
144
+
106
145
  function valueAfter(args, flag) {
107
146
  const index = args.indexOf(flag);
108
147
  return index >= 0 ? args[index + 1] : undefined;
@@ -4,8 +4,7 @@ import {
4
4
  listCommentMarkers,
5
5
  updateCommentMarker,
6
6
  } from "./comment-marker.mjs";
7
-
8
- const MAX_COMMENT_BODY_BYTES = 64 * 1024;
7
+ import { readJsonBody, writeJson } from "./http-json.mjs";
9
8
 
10
9
  export async function handleCommentRequest(req, res, {
11
10
  root = ".",
@@ -16,17 +15,14 @@ export async function handleCommentRequest(req, res, {
16
15
  try {
17
16
  writeJson(res, 200, { ok: true, comments: await listCommentMarkers({ root }) });
18
17
  } catch (error) {
19
- writeJson(res, 400, {
20
- ok: false,
21
- message: error instanceof Error ? error.message : String(error),
22
- });
18
+ writeErrorJson(res, error);
23
19
  }
24
20
  return;
25
21
  }
26
22
 
27
23
  if (req.method === "DELETE") {
28
24
  try {
29
- const body = await readJsonBody(req);
25
+ const body = await readJsonBody(req, { bodyLabel: "OpenPress comment request" });
30
26
  const result = await clearCommentMarkers({
31
27
  root,
32
28
  id: body?.id,
@@ -34,17 +30,14 @@ export async function handleCommentRequest(req, res, {
34
30
  });
35
31
  writeJson(res, 200, { ok: true, ...result });
36
32
  } catch (error) {
37
- writeJson(res, 400, {
38
- ok: false,
39
- message: error instanceof Error ? error.message : String(error),
40
- });
33
+ writeErrorJson(res, error);
41
34
  }
42
35
  return;
43
36
  }
44
37
 
45
38
  if (req.method === "PATCH") {
46
39
  try {
47
- const body = await readJsonBody(req);
40
+ const body = await readJsonBody(req, { bodyLabel: "OpenPress comment request" });
48
41
  const result = await updateCommentMarker({
49
42
  root,
50
43
  id: body?.id,
@@ -64,10 +57,7 @@ export async function handleCommentRequest(req, res, {
64
57
  },
65
58
  });
66
59
  } catch (error) {
67
- writeJson(res, 400, {
68
- ok: false,
69
- message: error instanceof Error ? error.message : String(error),
70
- });
60
+ writeErrorJson(res, error);
71
61
  }
72
62
  return;
73
63
  }
@@ -78,7 +68,7 @@ export async function handleCommentRequest(req, res, {
78
68
  }
79
69
 
80
70
  try {
81
- const body = await readJsonBody(req);
71
+ const body = await readJsonBody(req, { bodyLabel: "OpenPress comment request" });
82
72
  const target = body?.target ?? {};
83
73
  const result = await insertCommentMarker({
84
74
  root,
@@ -100,29 +90,13 @@ export async function handleCommentRequest(req, res, {
100
90
  },
101
91
  });
102
92
  } catch (error) {
103
- writeJson(res, 400, {
104
- ok: false,
105
- message: error instanceof Error ? error.message : String(error),
106
- });
107
- }
108
- }
109
-
110
- async function readJsonBody(req) {
111
- let body = "";
112
- for await (const chunk of req) {
113
- body += String(chunk);
114
- if (Buffer.byteLength(body, "utf8") > MAX_COMMENT_BODY_BYTES) {
115
- throw new Error("OpenPress comment request body is too large.");
116
- }
117
- }
118
- try {
119
- return JSON.parse(body || "{}");
120
- } catch {
121
- throw new Error("OpenPress comment request body must be valid JSON.");
93
+ writeErrorJson(res, error);
122
94
  }
123
95
  }
124
96
 
125
- function writeJson(res, status, body) {
126
- res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
127
- res.end(`${JSON.stringify(body, null, 2)}\n`);
97
+ function writeErrorJson(res, error) {
98
+ writeJson(res, 400, {
99
+ ok: false,
100
+ message: error instanceof Error ? error.message : String(error),
101
+ });
128
102
  }
@@ -14,8 +14,8 @@ const EDITABLE_COMMENT_SOURCE_PATTERNS = [
14
14
  /^document\/.+\.mdx$/,
15
15
  /^document\/.+\.tsx$/,
16
16
  ];
17
- const COMMENT_MARKER_RE = /\{\/\*\s*@openpress-comment\b(?<attrs>[^*]*)\*\/\}/g;
18
- const COMMENT_LINE_RE = /^\s*\{\/\*\s*@openpress-comment\b[^*]*\*\/\}\s*$/;
17
+ const COMMENT_MARKER_RE = /(?:\{\/\*|\/\*)\s*@openpress-comment\b(?<attrs>[^*]*)\*\/\}?/g;
18
+ const COMMENT_LINE_RE = /^\s*(?:\{\/\*|\/\*)\s*@openpress-comment\b[^*]*\*\/\}?\s*$/;
19
19
 
20
20
  export async function insertCommentMarker({
21
21
  root = ".",
@@ -35,9 +35,15 @@ export async function insertCommentMarker({
35
35
  }
36
36
 
37
37
  const noteText = normalizedNote(note);
38
- const marker = createCommentMarker({ id, timestamp, note: noteText, hint });
39
38
  const line = normalizeLineNumber(source?.line);
40
39
  const text = await fs.readFile(absolutePath, "utf8");
40
+ const marker = createCommentMarker({
41
+ id,
42
+ timestamp,
43
+ note: noteText,
44
+ hint,
45
+ syntax: commentMarkerSyntaxForInsert(text, line, relativePath),
46
+ });
41
47
  const nextText = insertLineBefore(text, line, marker);
42
48
  await fs.writeFile(absolutePath, nextText, "utf8");
43
49
 
@@ -51,9 +57,10 @@ export async function insertCommentMarker({
51
57
  };
52
58
  }
53
59
 
54
- export function createCommentMarker({ id = createCommentId(), timestamp = new Date().toISOString(), note, hint } = {}) {
60
+ export function createCommentMarker({ id = createCommentId(), timestamp = new Date().toISOString(), note, hint, syntax = "jsx" } = {}) {
55
61
  const payload = { note: normalizedNote(note), ...(typeof hint === "string" && hint.trim() ? { hint: hint.trim() } : {}) };
56
- return `{/* @openpress-comment id="${escapeMarkerAttribute(id)}" ts="${escapeMarkerAttribute(timestamp)}" text="${encodeCommentPayload(payload)}" */}`;
62
+ const body = `@openpress-comment id="${escapeMarkerAttribute(id)}" ts="${escapeMarkerAttribute(timestamp)}" text="${encodeCommentPayload(payload)}"`;
63
+ return syntax === "block" ? `/* ${body} */` : `{/* ${body} */}`;
57
64
  }
58
65
 
59
66
  export function decodeCommentMarkerText(marker) {
@@ -285,7 +292,7 @@ function replaceCommentMarkerLine(text, { id, note, hint, timestamp }) {
285
292
  if (!COMMENT_LINE_RE.test(line)) continue;
286
293
  const attrs = parseMarkerAttributes(line);
287
294
  if (attrs.id !== id) continue;
288
- const marker = createCommentMarker({ id, timestamp, note, hint });
295
+ const marker = createCommentMarker({ id, timestamp, note, hint, syntax: commentMarkerSyntaxForLine(line) });
289
296
  lines[index] = `${line.match(/^\s*/)?.[0] ?? ""}${marker}`;
290
297
  return {
291
298
  text: `${lines.join(newline)}${hasTrailingNewline ? newline : ""}`,
@@ -311,6 +318,23 @@ function parseMarkerAttributes(value) {
311
318
  return attrs;
312
319
  }
313
320
 
321
+ function commentMarkerSyntaxForInsert(text, line, relativePath) {
322
+ if (!String(relativePath).endsWith(".tsx")) return "jsx";
323
+ const lines = String(text ?? "").split(/\r?\n/);
324
+ const index = Math.min(Math.max(line - 1, 0), lines.length);
325
+ const priorContent = lines.slice(0, index).some((entry) => entry.trim().length > 0);
326
+ if (!priorContent) return "block";
327
+ const targetLine = lines[index] ?? "";
328
+ if (/^\s*(import\b|export\b|function\b|const\b|let\b|var\b|type\b|interface\b|return\b)/.test(targetLine)) {
329
+ return "block";
330
+ }
331
+ return "jsx";
332
+ }
333
+
334
+ function commentMarkerSyntaxForLine(line) {
335
+ return /^\s*\/\*/.test(line) ? "block" : "jsx";
336
+ }
337
+
314
338
  function lineStartOffsets(text) {
315
339
  const starts = [0];
316
340
  for (let index = 0; index < text.length; index += 1) {
@@ -68,6 +68,7 @@ export async function createReactSsrServer(workspaceRoot = ".") {
68
68
  return createViteServer({
69
69
  configFile: false,
70
70
  root: FRAMEWORK_ROOT,
71
+ cacheDir: path.join(resolvedWorkspaceRoot, ".openpress", "vite-ssr"),
71
72
  appType: "custom",
72
73
  logLevel: "silent",
73
74
  plugins: [reactRuntimePlugin(), react()],
@@ -82,6 +83,16 @@ export async function createReactSsrServer(workspaceRoot = ".") {
82
83
  { find: "@/components", replacement: path.join(resolvedWorkspaceRoot, "document", "components") },
83
84
  ],
84
85
  },
86
+ optimizeDeps: {
87
+ include: [
88
+ "@mdx-js/react",
89
+ "react",
90
+ "react-dom",
91
+ "react-dom/server",
92
+ "react/jsx-dev-runtime",
93
+ "react/jsx-runtime",
94
+ ],
95
+ },
85
96
  server: {
86
97
  middlewareMode: true,
87
98
  fs: {