@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
@@ -17,6 +17,20 @@ const PUBLIC_DEPLOY_ADAPTERS = new Set([
17
17
  "vercel",
18
18
  ]);
19
19
 
20
+ // A directory is an OpenPress workspace if it contains a
21
+ // press/index.tsx entry, or a package.json with an "openpress" field.
22
+ async function isWorkspaceRoot(dir) {
23
+ try {
24
+ await fs.access(path.join(dir, "press", "index.tsx"));
25
+ return true;
26
+ } catch {}
27
+ try {
28
+ const pkg = JSON.parse(await fs.readFile(path.join(dir, "package.json"), "utf8"));
29
+ if (pkg?.openpress && typeof pkg.openpress === "object") return true;
30
+ } catch {}
31
+ return false;
32
+ }
33
+
20
34
  export async function discoverWorkspace(startPath = ".") {
21
35
  let current = path.resolve(startPath);
22
36
  try {
@@ -26,15 +40,10 @@ export async function discoverWorkspace(startPath = ".") {
26
40
  current = path.dirname(current);
27
41
  }
28
42
  while (true) {
29
- const configPath = path.join(current, "openpress.config.mjs");
30
- try {
31
- await fs.access(configPath);
32
- return current;
33
- } catch {
34
- const parent = path.dirname(current);
35
- if (parent === current) throw new Error(`No OpenPress workspace found from ${startPath}`);
36
- current = parent;
37
- }
43
+ if (await isWorkspaceRoot(current)) return current;
44
+ const parent = path.dirname(current);
45
+ if (parent === current) throw new Error(`No OpenPress workspace found from ${startPath}`);
46
+ current = parent;
38
47
  }
39
48
  }
40
49
 
@@ -78,7 +87,7 @@ export async function validateWorkspace(root) {
78
87
 
79
88
  mark(sourceWorkspace.checkedName);
80
89
  if (!(typeof activeConfig.title === "string" && activeConfig.title.trim())) {
81
- add("warning", "config.title", "openpress.config.mjs `title` is empty; the workbench will show the default placeholder.", activeConfig.configPath);
90
+ add("warning", "press.title", "<Press title> is missing in press/index.tsx; the workbench will show the default placeholder.", activeConfig.configPath);
82
91
  }
83
92
  if (!(await sourceDirectoryExists(sourceWorkspace))) {
84
93
  add("warning", sourceWorkspace.missingCode, sourceWorkspace.missingMessage, sourceWorkspace.sourceDir);
@@ -1,10 +1,6 @@
1
- // Root pointer. The real workspace config lives at document/openpress.config.mjs.
2
- //
3
- // document/ is git-ignored in this framework checkout. Populate it locally
4
- // from a style pack's React/MDX document starter:
5
- // cp -R skills/editorial-monograph/starter/document/. document/
6
- // Or create a separate workspace:
7
- // node engine/cli.mjs init ../my-openpress-document
1
+ // Legacy root pointer kept for older local workspaces. Current OpenPress
2
+ // workspaces use package.json "openpress" config plus a press/ source tree.
3
+ // Starter files are supplied by skills, not fetched by the core engine.
8
4
 
9
5
  export default {
10
6
  documentDir: "document",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-press/core",
3
- "version": "0.7.1",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "description": "open-press core — runtime primitives, CLI, and render pipeline for AI-first fixed-layout documents.",
6
6
  "license": "MIT",
@@ -54,17 +54,15 @@
54
54
  "test:e2e:reader": "playwright test --config playwright.reader.config.ts",
55
55
  "test:node": "node --test tests/*.test.mjs",
56
56
  "test:react": "vitest run",
57
- "openpress:validate": "node engine/cli.mjs validate .",
58
- "openpress:export": "node engine/cli.mjs export .",
57
+ "openpress:image": "node engine/cli.mjs image .",
59
58
  "openpress:pdf": "node engine/cli.mjs pdf .",
60
- "openpress:render": "node engine/cli.mjs render . --renderer react",
61
- "openpress:preview": "node engine/cli.mjs preview . --renderer react",
62
59
  "openpress:deploy": "node engine/cli.mjs deploy .",
63
60
  "openpress:deploy:dry-run": "node engine/cli.mjs deploy . --confirm --dry-run"
64
61
  },
65
62
  "dependencies": {
66
63
  "@mdx-js/mdx": "^3.1.1",
67
64
  "@mdx-js/react": "^3.1.1",
65
+ "html-to-image": "^1.11.13",
68
66
  "js-yaml": "^4.1.1",
69
67
  "katex": "^0.16.47",
70
68
  "lucide-react": "^1.16.0",
@@ -1,6 +1,6 @@
1
1
  import { StrictMode } from "react";
2
2
  import { createRoot } from "react-dom/client";
3
- import { App } from "./openpress/App";
3
+ import { OpenPressApp } from "./openpress/app";
4
4
  import "./styles/openpress.css";
5
5
 
6
6
  const rootElement = document.getElementById("root");
@@ -11,6 +11,6 @@ if (!rootElement) {
11
11
 
12
12
  createRoot(rootElement).render(
13
13
  <StrictMode>
14
- <App />
14
+ <OpenPressApp />
15
15
  </StrictMode>,
16
16
  );
@@ -0,0 +1,296 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { OpenPressRuntime } from "./OpenPressRuntime";
3
+ import { WorkspaceGalleryPage } from "./WorkspaceGalleryPage";
4
+ import { isLocalWorkspaceHost } from "../shared";
5
+ import type {
6
+ DeploymentInfo,
7
+ ReaderDocument,
8
+ WorkspaceManifest,
9
+ WorkspaceManifestPress,
10
+ } from "../document-model";
11
+ import { findManifestPress, manifestHasMultiplePresses } from "../document-model";
12
+
13
+ type LoadState =
14
+ | { status: "loading" }
15
+ | {
16
+ // Gallery state — shown for multi-Press workspaces at the root URL.
17
+ // Single-Press workspaces never reach this state.
18
+ status: "gallery";
19
+ manifest: WorkspaceManifest;
20
+ deploymentInfo: DeploymentInfo;
21
+ }
22
+ | {
23
+ status: "ready";
24
+ document: ReaderDocument;
25
+ deploymentInfo: DeploymentInfo;
26
+ manifest: WorkspaceManifest | null;
27
+ // Empty string for single-Press workspaces (no slug routing needed)
28
+ // or for the root entry of a multi-Press workspace. Otherwise the
29
+ // active press's slug — used by refresh/back/forward to re-resolve.
30
+ activeSlug: string;
31
+ }
32
+ | { status: "error"; message: string };
33
+
34
+ interface DeployConfig {
35
+ pdf?: string;
36
+ deployed_at?: string;
37
+ public_url?: string;
38
+ dirty?: boolean;
39
+ deploy_configured?: boolean;
40
+ deploy_adapter?: string;
41
+ deploy_source?: string;
42
+ deploy_project_name?: string | null;
43
+ deploy_setup_message?: string;
44
+ }
45
+
46
+ const offlineDeploymentInfo: DeploymentInfo = { online: false };
47
+
48
+ function LoadingScreen() {
49
+ return (
50
+ <div className="openpress-loading-screen" aria-label="載入中" role="status">
51
+ <div className="openpress-loading-screen__inner">
52
+ <div className="openpress-loading-dots" aria-hidden="true">
53
+ <span /><span /><span />
54
+ </div>
55
+ <span className="openpress-loading-screen__label">載入文件</span>
56
+ </div>
57
+ </div>
58
+ );
59
+ }
60
+
61
+ export function OpenPressApp() {
62
+ const [state, setState] = useState<LoadState>({ status: "loading" });
63
+
64
+ // Single resolution function — same code path for "boot from URL",
65
+ // "click gallery card", and "browser back button". Given a manifest
66
+ // + slug, decides whether to render gallery or load a press.
67
+ const resolveFromSlug = useCallback(async (
68
+ manifest: WorkspaceManifest | null,
69
+ slug: string,
70
+ deploymentInfo: DeploymentInfo,
71
+ ) => {
72
+ // No manifest (legacy deploy): always load /openpress/document.json.
73
+ if (!manifest || manifest.presses.length === 0) {
74
+ const document = await loadReaderDocument("/openpress/document.json");
75
+ setState({ status: "ready", document, deploymentInfo, manifest, activeSlug: "" });
76
+ return;
77
+ }
78
+
79
+ // Empty slug + multi-Press: show gallery. Empty slug + single-Press:
80
+ // load the only press. Same expression handles both — array length
81
+ // is the only thing that matters.
82
+ const normalizedSlug = normalizeSlug(slug);
83
+ if (!normalizedSlug && manifestHasMultiplePresses(manifest)) {
84
+ setState({ status: "gallery", manifest, deploymentInfo });
85
+ return;
86
+ }
87
+
88
+ const press = normalizedSlug
89
+ ? findManifestPress(manifest, normalizedSlug)
90
+ : manifest.presses[0];
91
+ if (!press) {
92
+ setState({
93
+ status: "error",
94
+ message: `Unknown document slug "/${normalizedSlug}". Known: ${manifest.presses.map((p) => `/${p.slug}`).join(", ")}.`,
95
+ });
96
+ return;
97
+ }
98
+ const document = await loadReaderDocument(press.documentUrl);
99
+ setState({ status: "ready", document, deploymentInfo, manifest, activeSlug: press.slug });
100
+ }, []);
101
+
102
+ const refreshDocument = useCallback(async () => {
103
+ if (state.status !== "ready") return;
104
+ const press = state.manifest
105
+ ? findManifestPress(state.manifest, state.activeSlug)
106
+ : null;
107
+ const url = press?.documentUrl ?? "/openpress/document.json";
108
+ const document = await loadReaderDocument(url);
109
+ setState((latest) => {
110
+ if (latest.status !== "ready") return latest;
111
+ return { ...latest, document };
112
+ });
113
+ }, [state]);
114
+
115
+ // Gallery click → pushState + load. Bypasses resolveFromSlug's
116
+ // "empty slug + multi-Press → gallery" branch: an explicit click on
117
+ // the unslugged root Press must enter it, not bounce back to gallery.
118
+ const enterPress = useCallback(async (press: WorkspaceManifestPress) => {
119
+ if (state.status !== "gallery") return;
120
+ pushSlug(press.slug);
121
+ setState({ status: "loading" });
122
+ try {
123
+ const document = await loadReaderDocument(press.documentUrl);
124
+ setState({
125
+ status: "ready",
126
+ document,
127
+ deploymentInfo: state.deploymentInfo,
128
+ manifest: state.manifest,
129
+ activeSlug: press.slug,
130
+ });
131
+ } catch (error) {
132
+ setState({
133
+ status: "error",
134
+ message: error instanceof Error ? error.message : "Unable to load OpenPress document.",
135
+ });
136
+ }
137
+ }, [state]);
138
+
139
+ // Bootstrap: read URL → load manifest + deploy info → resolve.
140
+ useEffect(() => {
141
+ let cancelled = false;
142
+
143
+ async function bootstrap() {
144
+ try {
145
+ const [manifest, deploymentInfo] = await Promise.all([
146
+ loadWorkspaceManifest(),
147
+ loadDeploymentInfo(),
148
+ ]);
149
+ if (cancelled) return;
150
+ await resolveFromSlug(manifest, currentSlugFromLocation(), deploymentInfo);
151
+ } catch (error) {
152
+ if (!cancelled) {
153
+ setState({
154
+ status: "error",
155
+ message: error instanceof Error ? error.message : "Unable to load OpenPress document.",
156
+ });
157
+ }
158
+ }
159
+ }
160
+
161
+ void bootstrap();
162
+ return () => {
163
+ cancelled = true;
164
+ };
165
+ }, [resolveFromSlug]);
166
+
167
+ // Back / forward button — re-resolve from the new URL.
168
+ useEffect(() => {
169
+ function onPopState() {
170
+ if (state.status === "loading") return;
171
+ const manifest = state.status === "gallery"
172
+ ? state.manifest
173
+ : state.status === "ready"
174
+ ? state.manifest
175
+ : null;
176
+ const deploymentInfo = state.status === "gallery" || state.status === "ready"
177
+ ? state.deploymentInfo
178
+ : offlineDeploymentInfo;
179
+ void resolveFromSlug(manifest, currentSlugFromLocation(), deploymentInfo);
180
+ }
181
+ window.addEventListener("popstate", onPopState);
182
+ return () => window.removeEventListener("popstate", onPopState);
183
+ }, [state, resolveFromSlug]);
184
+
185
+ if (state.status === "loading") return <LoadingScreen />;
186
+
187
+ if (state.status === "error") {
188
+ return <div className="openpress-load-state openpress-load-state--error">{state.message}</div>;
189
+ }
190
+
191
+ if (state.status === "gallery") {
192
+ return <WorkspaceGalleryPage manifest={state.manifest} onSelectPress={enterPress} />;
193
+ }
194
+
195
+ // Only multi-Press workspaces have a gallery to go back to. Single-Press
196
+ // workspaces don't render the button (no destination exists).
197
+ const backToWorkspace = state.manifest && manifestHasMultiplePresses(state.manifest)
198
+ ? () => {
199
+ if (state.status !== "ready" || !state.manifest) return;
200
+ pushSlug("");
201
+ setState({
202
+ status: "gallery",
203
+ manifest: state.manifest,
204
+ deploymentInfo: state.deploymentInfo,
205
+ });
206
+ }
207
+ : undefined;
208
+
209
+ return (
210
+ <OpenPressRuntime
211
+ document={state.document}
212
+ deploymentInfo={state.deploymentInfo}
213
+ onDocumentRefresh={refreshDocument}
214
+ onBackToWorkspace={backToWorkspace}
215
+ />
216
+ );
217
+ }
218
+
219
+ function currentSlugFromLocation(): string {
220
+ if (typeof window === "undefined") return "";
221
+ return normalizeSlug(window.location.pathname);
222
+ }
223
+
224
+ function normalizeSlug(raw: string): string {
225
+ return raw.replace(/^\/+|\/+$/g, "");
226
+ }
227
+
228
+ function pushSlug(slug: string) {
229
+ if (typeof window === "undefined") return;
230
+ // Preserve the current query string (e.g. ?dev=1 keeps the workbench
231
+ // chrome alive across gallery navigation). Drop the hash — it's a
232
+ // page anchor that means nothing in a different document.
233
+ const pathname = slug ? `/${normalizeSlug(slug)}` : "/";
234
+ const target = `${pathname}${window.location.search}`;
235
+ if (window.location.pathname === pathname) return;
236
+ window.history.pushState({}, "", target);
237
+ }
238
+
239
+ async function loadWorkspaceManifest(): Promise<WorkspaceManifest | null> {
240
+ // Optional — older deployments don't ship workspace.json. The reader
241
+ // falls back to /openpress/document.json directly when missing, which
242
+ // matches pre-v1.0 behavior.
243
+ try {
244
+ const response = await fetch("/openpress/workspace.json", { cache: "no-store" });
245
+ if (!response.ok) return null;
246
+ return (await response.json()) as WorkspaceManifest;
247
+ } catch {
248
+ return null;
249
+ }
250
+ }
251
+
252
+ async function loadReaderDocument(url: string): Promise<ReaderDocument> {
253
+ const response = await fetch(url, { cache: "no-store" });
254
+ if (!response.ok) {
255
+ throw new Error(`Unable to load ${url} (${response.status})`);
256
+ }
257
+ return (await response.json()) as ReaderDocument;
258
+ }
259
+
260
+ async function loadDeploymentInfo(): Promise<DeploymentInfo> {
261
+ if (typeof window !== "undefined" && isLocalWorkspaceHost(window.location.hostname)) {
262
+ const localInfo = await loadDeploymentInfoFrom("/__openpress/status");
263
+ if (localInfo) return localInfo;
264
+ }
265
+
266
+ return (await loadDeploymentInfoFrom("/openpress/deploy.json")) ?? offlineDeploymentInfo;
267
+ }
268
+
269
+ async function loadDeploymentInfoFrom(path: string): Promise<DeploymentInfo | null> {
270
+ try {
271
+ const response = await fetch(path, { cache: "no-store" });
272
+ if (!response.ok) {
273
+ return null;
274
+ }
275
+ const config = (await response.json()) as DeployConfig;
276
+ return deploymentConfigToInfo(config);
277
+ } catch {
278
+ return null;
279
+ }
280
+ }
281
+
282
+ function deploymentConfigToInfo(config: DeployConfig): DeploymentInfo {
283
+ const configured = config.deploy_configured !== false;
284
+ return {
285
+ online: configured && Boolean(config.deployed_at || config.public_url),
286
+ deployedAt: config.deployed_at,
287
+ pdf: typeof config.pdf === "string" ? config.pdf : undefined,
288
+ publicUrl: typeof config.public_url === "string" ? config.public_url : undefined,
289
+ dirty: config.dirty === true,
290
+ configured,
291
+ adapter: typeof config.deploy_adapter === "string" ? config.deploy_adapter : undefined,
292
+ source: typeof config.deploy_source === "string" ? config.deploy_source : undefined,
293
+ projectName: typeof config.deploy_project_name === "string" ? config.deploy_project_name : undefined,
294
+ setupMessage: typeof config.deploy_setup_message === "string" ? config.deploy_setup_message : undefined,
295
+ };
296
+ }
@@ -1,23 +1,30 @@
1
1
  import { useMemo, type CSSProperties } from "react";
2
- import { PrintDocument, PublicViewer } from "./publicPage";
3
- import { isPrintModeLocation, isWorkspaceModeLocation } from "./runtimeMode";
4
- import { HtmlWorkbench } from "./workbench";
2
+ import { PrintDocument, PublicViewer } from "../reader";
3
+ import { isPrintModeLocation, isWorkspaceModeLocation } from "../shared";
4
+ import { HtmlWorkbench } from "../workbench";
5
5
  import type {
6
6
  DeploymentInfo,
7
7
  ReaderDocument,
8
8
  HtmlPageBlock,
9
9
  Theme,
10
- } from "./types";
10
+ } from "../document-model";
11
11
 
12
- interface RendererProps {
12
+ interface OpenPressRuntimeProps {
13
13
  document: ReaderDocument;
14
14
  deploymentInfo?: DeploymentInfo;
15
+ onDocumentRefresh?: () => void | Promise<void>;
16
+ // Optional — supplied by OpenPressApp when this Press was entered from
17
+ // a multi-Press gallery. Renders a "工作台" home button in the toolbar
18
+ // that returns to the gallery without a full page reload.
19
+ onBackToWorkspace?: () => void;
15
20
  }
16
21
 
17
- export function Renderer({
22
+ export function OpenPressRuntime({
18
23
  document,
19
24
  deploymentInfo = { online: false },
20
- }: RendererProps) {
25
+ onDocumentRefresh,
26
+ onBackToWorkspace,
27
+ }: OpenPressRuntimeProps) {
21
28
  const style = themeToCssVariables(document.theme);
22
29
  const htmlPages = document.blocks.filter((block): block is HtmlPageBlock => block.kind === "htmlPage");
23
30
  const workspaceMode = useMemo(() => {
@@ -45,6 +52,8 @@ export function Renderer({
45
52
  style={style}
46
53
  devMode={workspaceMode}
47
54
  deploymentInfo={deploymentInfo}
55
+ onDocumentRefresh={onDocumentRefresh}
56
+ onBackToWorkspace={onBackToWorkspace}
48
57
  />
49
58
  );
50
59
  }
@@ -59,11 +68,11 @@ function EmptyState({ style, workspaceMode }: { style: CSSProperties; workspaceM
59
68
  <p className="openpress-empty-state__eyebrow">OpenPress</p>
60
69
  <h1 className="openpress-empty-state__title">This document has no content yet.</h1>
61
70
  <p className="openpress-empty-state__body">
62
- Add React MDX chapter files under <code>document/chapters/**/content/</code>, then re-export.
71
+ Add React MDX chapter files under <code>press/chapters/**/content/</code>, then re-build.
63
72
  </p>
64
73
  {workspaceMode ? (
65
74
  <ol className="openpress-empty-state__steps">
66
- <li><code>npm run openpress:export</code> &nbsp;— refreshes <code>public/openpress/document.json</code></li>
75
+ <li><code>npm run build</code> &nbsp;— validates and refreshes <code>public/openpress/document.json</code></li>
67
76
  <li>Reload this page</li>
68
77
  </ol>
69
78
  ) : (
@@ -85,6 +94,8 @@ function themeToCssVariables(theme?: Theme) {
85
94
 
86
95
  if (theme?.pageWidth) style["--openpress-page-width"] = theme.pageWidth;
87
96
  if (theme?.pageHeight) style["--openpress-page-height"] = theme.pageHeight;
97
+ if (theme?.pageAspectRatio) style["--openpress-page-aspect-ratio"] = theme.pageAspectRatio;
98
+ if (theme?.pageHeightRatio) style["--openpress-page-height-ratio"] = theme.pageHeightRatio;
88
99
  if (theme?.pagePadding) style["--openpress-page-padding"] = theme.pagePadding;
89
100
 
90
101
  return style;