@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
@@ -1,230 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState, type RefCallback } from "react";
2
- import { pageIndexFromHash, replacePageRoute } from "./pageRoute";
3
- import { createReaderPageRegistry } from "./readerPageRegistry";
4
- import { clampReaderPageIndex, formatReaderPageNumber, normalizeReaderPageCount } from "./readerState";
5
- import { createPageVisibilityObserver, scrollToPage } from "./readerScroll";
6
-
7
- export interface SetPageOptions {
8
- behavior?: ScrollBehavior;
9
- }
10
-
11
- interface UseReaderRuntimeOptions {
12
- pageCount: number;
13
- rightPanelBreakpoint?: number;
14
- }
15
-
16
- // Generous upper bound on a smooth scrollIntoView. If the target ref is gone or
17
- // the browser never settles on it, clear the guard so the IO observer regains
18
- // authority over currentPageIndex.
19
- const PROGRAMMATIC_SCROLL_FALLBACK_MS = 2500;
20
-
21
- export function useReaderRuntime({ pageCount, rightPanelBreakpoint = 1000 }: UseReaderRuntimeOptions) {
22
- const normalizedPageCount = normalizeReaderPageCount(pageCount);
23
- const stageRef = useRef<HTMLElement | null>(null);
24
- const [pageRegistrationVersion, setPageRegistrationVersion] = useState(0);
25
- const pageRegistry = useRef<ReturnType<typeof createReaderPageRegistry<HTMLElement>> | null>(null);
26
- if (!pageRegistry.current) {
27
- pageRegistry.current = createReaderPageRegistry<HTMLElement>(setPageRegistrationVersion);
28
- }
29
-
30
- const [currentPageIndex, setCurrentPageIndex] = useState(() => {
31
- if (typeof window === "undefined") return 0;
32
- const fromHash = pageIndexFromHash(window.location.hash, normalizedPageCount);
33
- return fromHash ?? 0;
34
- });
35
- const [rightPanelOpen, setRightPanelOpen] = useState(() =>
36
- typeof window === "undefined" ? true : window.innerWidth >= rightPanelBreakpoint,
37
- );
38
-
39
- const currentPageIndexRef = useRef(currentPageIndex);
40
- currentPageIndexRef.current = currentPageIndex;
41
-
42
- // While a programmatic scroll is in flight, the IntersectionObserver should
43
- // only accept the destination page (not the intermediates we sweep past).
44
- // The ref clears as soon as the destination becomes visible.
45
- const pendingScrollTargetRef = useRef<number | null>(null);
46
- const pendingScrollClearTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
47
-
48
- const armPendingScrollTarget = useCallback((target: number) => {
49
- pendingScrollTargetRef.current = target;
50
- if (pendingScrollClearTimerRef.current !== null) clearTimeout(pendingScrollClearTimerRef.current);
51
- pendingScrollClearTimerRef.current = setTimeout(() => {
52
- pendingScrollTargetRef.current = null;
53
- pendingScrollClearTimerRef.current = null;
54
- }, PROGRAMMATIC_SCROLL_FALLBACK_MS);
55
- }, []);
56
-
57
- const clearPendingScrollTarget = useCallback(() => {
58
- pendingScrollTargetRef.current = null;
59
- if (pendingScrollClearTimerRef.current !== null) {
60
- clearTimeout(pendingScrollClearTimerRef.current);
61
- pendingScrollClearTimerRef.current = null;
62
- }
63
- }, []);
64
-
65
- useEffect(() => () => clearPendingScrollTarget(), [clearPendingScrollTarget]);
66
-
67
- useEffect(() => {
68
- pageRegistry.current?.trim(normalizedPageCount);
69
- setCurrentPageIndex((idx) => clampReaderPageIndex(idx, normalizedPageCount));
70
- }, [normalizedPageCount]);
71
-
72
- useEffect(() => {
73
- const stage = stageRef.current;
74
- if (!stage) return undefined;
75
- const observer = createPageVisibilityObserver(stage, (pageIndex) => {
76
- // During a programmatic scroll, ignore intermediate pages the browser
77
- // sweeps past; only the destination counts as the new current page.
78
- if (pendingScrollTargetRef.current !== null) {
79
- if (pageIndex !== pendingScrollTargetRef.current) return;
80
- clearPendingScrollTarget();
81
- }
82
- setCurrentPageIndex((prev) => (prev === pageIndex ? prev : pageIndex));
83
- });
84
- if (!observer) return undefined;
85
- pageRegistry.current?.refs.forEach((el) => el && observer.observe(el));
86
- return () => observer.disconnect();
87
- }, [clearPendingScrollTarget, normalizedPageCount, pageRegistrationVersion]);
88
-
89
- useEffect(() => {
90
- if (typeof window === "undefined") return;
91
- replacePageRoute(currentPageIndex);
92
- }, [currentPageIndex]);
93
-
94
- // When refs change (initial mount, pagination kicks in), re-anchor the
95
- // stage to the page we already believe we're on. Otherwise scroll-snap
96
- // mandatory snaps to whichever page happens to sit closest to the current
97
- // scroll position. Only fire when we have somewhere non-default to land,
98
- // so the IO observer stays free to drive state during ordinary navigation.
99
- useEffect(() => {
100
- const refs = pageRegistry.current?.refs ?? [];
101
- const idx = currentPageIndexRef.current;
102
- if (idx === 0) return;
103
- if (!refs[idx]) return;
104
- armPendingScrollTarget(idx);
105
- scrollToPage(refs, idx, "instant", stageRef.current);
106
- }, [armPendingScrollTarget, pageRegistrationVersion]);
107
-
108
- useEffect(() => {
109
- if (typeof window === "undefined") return undefined;
110
-
111
- const syncFromHash = (behavior: ScrollBehavior) => {
112
- const refs = pageRegistry.current?.refs ?? [];
113
- const hashPage = pageIndexFromHash(window.location.hash, normalizedPageCount);
114
- if (hashPage === null) return;
115
- // Our own replacePageRoute call writes the hash to mirror state; skip
116
- // if the hash already matches so we don't fight ourselves.
117
- if (hashPage === currentPageIndexRef.current) return;
118
- armPendingScrollTarget(hashPage);
119
- setCurrentPageIndex(hashPage);
120
- scrollToPage(refs, hashPage, behavior, stageRef.current);
121
- };
122
-
123
- const onHashChange = () => syncFromHash("smooth");
124
- window.addEventListener("hashchange", onHashChange);
125
- window.addEventListener("popstate", onHashChange);
126
- return () => {
127
- window.removeEventListener("hashchange", onHashChange);
128
- window.removeEventListener("popstate", onHashChange);
129
- };
130
- }, [armPendingScrollTarget, normalizedPageCount]);
131
-
132
- useEffect(() => {
133
- if (typeof window === "undefined") return undefined;
134
-
135
- let frame: number | null = null;
136
- const reAnchorAfterPaint = () => {
137
- if (frame !== null) window.cancelAnimationFrame(frame);
138
- frame = window.requestAnimationFrame(() => {
139
- frame = null;
140
- const refs = pageRegistry.current?.refs ?? [];
141
- // If a programmatic scroll is in flight, re-anchor to its destination
142
- // so the snap doesn't pull us back to where we were before clicking.
143
- const target = pendingScrollTargetRef.current ?? currentPageIndexRef.current;
144
- scrollToPage(refs, target, "instant", stageRef.current);
145
- });
146
- };
147
-
148
- const handleResize = () => {
149
- setRightPanelOpen(window.innerWidth >= rightPanelBreakpoint);
150
- // scroll-snap-type: y mandatory re-aligns to the closest snap point on
151
- // viewport change, which can land one page off from where the reader was.
152
- // Pin to the IO-confirmed current page (or active programmatic target).
153
- reAnchorAfterPaint();
154
- };
155
-
156
- handleResize();
157
- window.addEventListener("resize", handleResize);
158
- window.visualViewport?.addEventListener("resize", handleResize);
159
- return () => {
160
- window.removeEventListener("resize", handleResize);
161
- window.visualViewport?.removeEventListener("resize", handleResize);
162
- if (frame !== null) window.cancelAnimationFrame(frame);
163
- };
164
- }, [rightPanelBreakpoint]);
165
-
166
- const setPage = useCallback(
167
- (pageIndex: number, options: SetPageOptions = {}) => {
168
- const refs = pageRegistry.current?.refs ?? [];
169
- const target = clampReaderPageIndex(pageIndex, normalizedPageCount);
170
- armPendingScrollTarget(target);
171
- setCurrentPageIndex(target);
172
- scrollToPage(refs, target, options.behavior ?? "smooth", stageRef.current);
173
- },
174
- [armPendingScrollTarget, normalizedPageCount],
175
- );
176
-
177
- const nextPage = useCallback(() => {
178
- setPage(currentPageIndexRef.current + 1);
179
- }, [setPage]);
180
-
181
- const prevPage = useCallback(() => {
182
- setPage(currentPageIndexRef.current - 1);
183
- }, [setPage]);
184
-
185
- useEffect(() => {
186
- const handleKeyDown = (event: KeyboardEvent) => {
187
- if (isEditableTarget(event.target)) return;
188
- if (event.key === "ArrowRight" || event.key === "PageDown") {
189
- event.preventDefault();
190
- nextPage();
191
- } else if (event.key === "ArrowLeft" || event.key === "PageUp") {
192
- event.preventDefault();
193
- prevPage();
194
- } else if (event.key === "Home") {
195
- event.preventDefault();
196
- setPage(0);
197
- } else if (event.key === "End") {
198
- event.preventDefault();
199
- setPage(Math.max(0, normalizedPageCount - 1));
200
- }
201
- };
202
- window.addEventListener("keydown", handleKeyDown);
203
- return () => window.removeEventListener("keydown", handleKeyDown);
204
- }, [nextPage, prevPage, setPage, normalizedPageCount]);
205
-
206
- const registerPage = useCallback<(pageIndex: number) => RefCallback<HTMLElement>>(
207
- (pageIndex) => pageRegistry.current?.registerPage(pageIndex) ?? (() => undefined),
208
- [],
209
- );
210
-
211
- const progressPercent =
212
- normalizedPageCount <= 1 ? 100 : ((currentPageIndex + 1) / normalizedPageCount) * 100;
213
-
214
- return {
215
- stageRef,
216
- currentPageIndex,
217
- currentPageLabel: formatReaderPageNumber(currentPageIndex + 1),
218
- totalPageLabel: formatReaderPageNumber(normalizedPageCount),
219
- progressPercent,
220
- rightPanelOpen,
221
- registerPage,
222
- setPage,
223
- toggleRightPanel: () => setRightPanelOpen((open) => !open),
224
- };
225
- }
226
-
227
- function isEditableTarget(target: EventTarget | null) {
228
- if (!(target instanceof HTMLElement)) return false;
229
- return Boolean(target.closest("input, textarea, select, button, [contenteditable='true']"));
230
- }