@open-press/cli 1.0.0 → 1.1.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 (175) hide show
  1. package/README.md +11 -12
  2. package/dist/cli.js +298 -79
  3. package/package.json +9 -7
  4. package/template/core/AGENTS.md +0 -130
  5. package/template/core/CHANGELOG.md +0 -218
  6. package/template/core/README.md +0 -43
  7. package/template/core/engine/cli.mjs +0 -96
  8. package/template/core/engine/commands/_shared.mjs +0 -199
  9. package/template/core/engine/commands/deploy.mjs +0 -31
  10. package/template/core/engine/commands/dev.mjs +0 -49
  11. package/template/core/engine/commands/doctor.mjs +0 -229
  12. package/template/core/engine/commands/export.mjs +0 -8
  13. package/template/core/engine/commands/image.mjs +0 -29
  14. package/template/core/engine/commands/inspect.mjs +0 -35
  15. package/template/core/engine/commands/pdf.mjs +0 -26
  16. package/template/core/engine/commands/preview.mjs +0 -26
  17. package/template/core/engine/commands/render.mjs +0 -17
  18. package/template/core/engine/commands/replace.mjs +0 -41
  19. package/template/core/engine/commands/search.mjs +0 -33
  20. package/template/core/engine/commands/skills-sync.mjs +0 -71
  21. package/template/core/engine/commands/typecheck.mjs +0 -67
  22. package/template/core/engine/commands/upgrade.mjs +0 -159
  23. package/template/core/engine/commands/validate.mjs +0 -17
  24. package/template/core/engine/document-export.mjs +0 -15
  25. package/template/core/engine/output/chrome-pdf.d.mts +0 -34
  26. package/template/core/engine/output/chrome-pdf.mjs +0 -450
  27. package/template/core/engine/output/deploy-sync.mjs +0 -15
  28. package/template/core/engine/output/fonts.mjs +0 -62
  29. package/template/core/engine/output/katex-assets.mjs +0 -45
  30. package/template/core/engine/output/page-block.mjs +0 -30
  31. package/template/core/engine/output/pdf-media.mjs +0 -45
  32. package/template/core/engine/output/public-assets.mjs +0 -19
  33. package/template/core/engine/output/static-server.mjs +0 -571
  34. package/template/core/engine/react/caption-numbering.mjs +0 -73
  35. package/template/core/engine/react/comment-endpoint.d.mts +0 -11
  36. package/template/core/engine/react/comment-endpoint.mjs +0 -102
  37. package/template/core/engine/react/comment-marker.mjs +0 -374
  38. package/template/core/engine/react/document-entry.mjs +0 -331
  39. package/template/core/engine/react/document-export.mjs +0 -512
  40. package/template/core/engine/react/http-json.mjs +0 -24
  41. package/template/core/engine/react/mdx-compile.mjs +0 -629
  42. package/template/core/engine/react/measurement-css.mjs +0 -157
  43. package/template/core/engine/react/object-entities.mjs +0 -204
  44. package/template/core/engine/react/pagination/allocator.mjs +0 -167
  45. package/template/core/engine/react/pagination/regions.mjs +0 -81
  46. package/template/core/engine/react/pagination-constants.mjs +0 -3
  47. package/template/core/engine/react/pagination.mjs +0 -9
  48. package/template/core/engine/react/pipeline/allocate.mjs +0 -217
  49. package/template/core/engine/react/pipeline/final-render.mjs +0 -94
  50. package/template/core/engine/react/pipeline/frame-measurement.mjs +0 -306
  51. package/template/core/engine/react/pipeline/press-tree.mjs +0 -135
  52. package/template/core/engine/react/press-tree-inspection.mjs +0 -172
  53. package/template/core/engine/react/project-asset-endpoint.d.mts +0 -10
  54. package/template/core/engine/react/project-asset-endpoint.mjs +0 -361
  55. package/template/core/engine/react/section-css.mjs +0 -56
  56. package/template/core/engine/react/source-edit-endpoint.d.mts +0 -10
  57. package/template/core/engine/react/source-edit-endpoint.mjs +0 -75
  58. package/template/core/engine/react/sources/heading-numbering.mjs +0 -132
  59. package/template/core/engine/react/sources/mdx-resolver.mjs +0 -439
  60. package/template/core/engine/react/style-discovery.mjs +0 -160
  61. package/template/core/engine/runtime/config.d.mts +0 -48
  62. package/template/core/engine/runtime/config.mjs +0 -172
  63. package/template/core/engine/runtime/file-utils.mjs +0 -114
  64. package/template/core/engine/runtime/file-walk.mjs +0 -22
  65. package/template/core/engine/runtime/inspection.mjs +0 -328
  66. package/template/core/engine/runtime/issue-report.mjs +0 -44
  67. package/template/core/engine/runtime/page-geometry.mjs +0 -131
  68. package/template/core/engine/runtime/path-utils.mjs +0 -20
  69. package/template/core/engine/runtime/source-text-tools.d.mts +0 -102
  70. package/template/core/engine/runtime/source-text-tools.mjs +0 -832
  71. package/template/core/engine/runtime/source-workspace.mjs +0 -168
  72. package/template/core/engine/runtime/validation.mjs +0 -183
  73. package/template/core/index.html +0 -13
  74. package/template/core/openpress.config.mjs +0 -8
  75. package/template/core/package.json +0 -89
  76. package/template/core/src/main.tsx +0 -16
  77. package/template/core/src/openpress/app/OpenPressApp.tsx +0 -296
  78. package/template/core/src/openpress/app/OpenPressRuntime.tsx +0 -102
  79. package/template/core/src/openpress/app/WorkspaceGalleryPage.tsx +0 -219
  80. package/template/core/src/openpress/app/index.ts +0 -2
  81. package/template/core/src/openpress/core/Frame.tsx +0 -91
  82. package/template/core/src/openpress/core/FrameContext.tsx +0 -26
  83. package/template/core/src/openpress/core/MdxArea.tsx +0 -34
  84. package/template/core/src/openpress/core/Press.tsx +0 -55
  85. package/template/core/src/openpress/core/Workspace.tsx +0 -36
  86. package/template/core/src/openpress/core/cn.ts +0 -4
  87. package/template/core/src/openpress/core/index.tsx +0 -47
  88. package/template/core/src/openpress/core/primitives.tsx +0 -91
  89. package/template/core/src/openpress/core/types.ts +0 -236
  90. package/template/core/src/openpress/core/useSource.ts +0 -28
  91. package/template/core/src/openpress/document-model/anchorMapModel.ts +0 -27
  92. package/template/core/src/openpress/document-model/documentIndexes.ts +0 -329
  93. package/template/core/src/openpress/document-model/documentTypes.ts +0 -147
  94. package/template/core/src/openpress/document-model/index.ts +0 -7
  95. package/template/core/src/openpress/document-model/objectEntityModel.ts +0 -55
  96. package/template/core/src/openpress/document-model/projectIdentityModel.ts +0 -15
  97. package/template/core/src/openpress/document-model/reactDocumentMetadataModel.ts +0 -27
  98. package/template/core/src/openpress/document-model/workspaceManifestModel.ts +0 -57
  99. package/template/core/src/openpress/manuscript/index.tsx +0 -238
  100. package/template/core/src/openpress/mdx/index.ts +0 -96
  101. package/template/core/src/openpress/numbering/index.ts +0 -294
  102. package/template/core/src/openpress/reader/PageThumbnailsPanel.tsx +0 -168
  103. package/template/core/src/openpress/reader/PublicReaderPage.tsx +0 -267
  104. package/template/core/src/openpress/reader/ReaderNavigationPanel.tsx +0 -123
  105. package/template/core/src/openpress/reader/index.ts +0 -11
  106. package/template/core/src/openpress/reader/pageViewportScaleModel.ts +0 -73
  107. package/template/core/src/openpress/reader/readerPageRegistry.ts +0 -41
  108. package/template/core/src/openpress/reader/readerPageRoute.ts +0 -21
  109. package/template/core/src/openpress/reader/readerScroll.ts +0 -92
  110. package/template/core/src/openpress/reader/readerStateModel.ts +0 -15
  111. package/template/core/src/openpress/reader/readerTypes.ts +0 -4
  112. package/template/core/src/openpress/reader/usePageViewportScale.ts +0 -119
  113. package/template/core/src/openpress/reader/usePanelState.ts +0 -56
  114. package/template/core/src/openpress/reader/useReaderHashSync.ts +0 -61
  115. package/template/core/src/openpress/reader/useReaderKeyboardNav.ts +0 -48
  116. package/template/core/src/openpress/reader/useReaderRuntime.ts +0 -146
  117. package/template/core/src/openpress/reader/useReaderScrollAnchor.ts +0 -64
  118. package/template/core/src/openpress/shared/Panel.tsx +0 -77
  119. package/template/core/src/openpress/shared/frameScheduler.ts +0 -32
  120. package/template/core/src/openpress/shared/index.ts +0 -4
  121. package/template/core/src/openpress/shared/numberUtils.ts +0 -3
  122. package/template/core/src/openpress/shared/runtimeMode.ts +0 -11
  123. package/template/core/src/openpress/workbench/Workbench.tsx +0 -506
  124. package/template/core/src/openpress/workbench/actions/DeploymentControl.tsx +0 -157
  125. package/template/core/src/openpress/workbench/actions/ExportImageControl.tsx +0 -96
  126. package/template/core/src/openpress/workbench/actions/PageZoomControl.tsx +0 -182
  127. package/template/core/src/openpress/workbench/actions/SearchControl.tsx +0 -345
  128. package/template/core/src/openpress/workbench/actions/deploymentStatusModel.ts +0 -112
  129. package/template/core/src/openpress/workbench/actions/index.ts +0 -6
  130. package/template/core/src/openpress/workbench/actions/useDeploymentWorkbench.ts +0 -136
  131. package/template/core/src/openpress/workbench/dialog/WorkbenchDialog.tsx +0 -72
  132. package/template/core/src/openpress/workbench/dialog/index.ts +0 -1
  133. package/template/core/src/openpress/workbench/document/components/DocumentPanel.tsx +0 -127
  134. package/template/core/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +0 -207
  135. package/template/core/src/openpress/workbench/document/components/ReaderStage.tsx +0 -9
  136. package/template/core/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +0 -34
  137. package/template/core/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +0 -525
  138. package/template/core/src/openpress/workbench/document/index.ts +0 -10
  139. package/template/core/src/openpress/workbench/index.ts +0 -2
  140. package/template/core/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +0 -459
  141. package/template/core/src/openpress/workbench/inspector/index.ts +0 -5
  142. package/template/core/src/openpress/workbench/inspector/inlineCommentModel.ts +0 -125
  143. package/template/core/src/openpress/workbench/inspector/inspectorGeometryModel.ts +0 -160
  144. package/template/core/src/openpress/workbench/inspector/inspectorModel.ts +0 -408
  145. package/template/core/src/openpress/workbench/inspector/useInspectorComments.ts +0 -254
  146. package/template/core/src/openpress/workbench/mentions/MentionSuggestionList.tsx +0 -41
  147. package/template/core/src/openpress/workbench/mentions/index.ts +0 -2
  148. package/template/core/src/openpress/workbench/mentions/useComposerMentions.ts +0 -185
  149. package/template/core/src/openpress/workbench/panels/Panel.tsx +0 -1
  150. package/template/core/src/openpress/workbench/panels/PendingCommentsPanel.tsx +0 -80
  151. package/template/core/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +0 -29
  152. package/template/core/src/openpress/workbench/panels/index.ts +0 -3
  153. package/template/core/src/openpress/workbench/project/ProjectEntryPanel.tsx +0 -525
  154. package/template/core/src/openpress/workbench/project/ProjectPreviewDialog.tsx +0 -35
  155. package/template/core/src/openpress/workbench/project/index.ts +0 -2
  156. package/template/core/src/openpress/workbench/project/projectPreviewTypes.ts +0 -11
  157. package/template/core/src/openpress/workbench/project/projectSourceModel.ts +0 -24
  158. package/template/core/src/openpress/workbench/shell/WorkbenchShell.tsx +0 -167
  159. package/template/core/src/openpress/workbench/shell/index.ts +0 -1
  160. package/template/core/src/openpress/workbench/workbenchFormatters.ts +0 -120
  161. package/template/core/src/openpress/workbench/workbenchTypes.ts +0 -35
  162. package/template/core/src/styles/openpress/app-shell.css +0 -251
  163. package/template/core/src/styles/openpress/media-workspace.css +0 -230
  164. package/template/core/src/styles/openpress/print-route.css +0 -184
  165. package/template/core/src/styles/openpress/project-preview-panel.css +0 -924
  166. package/template/core/src/styles/openpress/public-viewer.css +0 -688
  167. package/template/core/src/styles/openpress/reader-runtime.css +0 -989
  168. package/template/core/src/styles/openpress/responsive.css +0 -245
  169. package/template/core/src/styles/openpress/workbench-panels.css +0 -707
  170. package/template/core/src/styles/openpress/workbench.css +0 -1255
  171. package/template/core/src/styles/openpress/workspace-gallery.css +0 -300
  172. package/template/core/src/styles/openpress.css +0 -15
  173. package/template/core/src/vite-env.d.ts +0 -9
  174. package/template/core/tsconfig.json +0 -40
  175. package/template/core/vite.config.ts +0 -584
@@ -1,21 +0,0 @@
1
- const PAGE_HASH_PATTERN = /^#page-(\d+)$/;
2
-
3
- export function pageHashFromIndex(pageIndex: number) {
4
- return `#page-${String(Math.max(1, pageIndex + 1)).padStart(2, "0")}`;
5
- }
6
-
7
- export function pageIndexFromHash(hash: string, pageCount: number) {
8
- const match = hash.match(PAGE_HASH_PATTERN);
9
- if (!match) return null;
10
-
11
- const pageNumber = Number.parseInt(match[1], 10);
12
- if (!Number.isFinite(pageNumber) || pageNumber < 1 || pageNumber > pageCount) return null;
13
- return pageNumber - 1;
14
- }
15
-
16
- export function replacePageRoute(pageIndex: number) {
17
- if (typeof window === "undefined") return;
18
- const hash = pageHashFromIndex(pageIndex);
19
- if (window.location.hash === hash) return;
20
- window.history.replaceState(null, "", hash);
21
- }
@@ -1,92 +0,0 @@
1
- // Single place that touches scrollIntoView and IntersectionObserver. Keeping
2
- // these together makes it obvious which DOM APIs the reader depends on and
3
- // keeps the React runtime free of imperative scroll bookkeeping.
4
-
5
- const DEBOUNCE_MS = 100;
6
-
7
- const OBSERVER_THRESHOLDS = [0, 0.25, 0.5, 0.75, 1];
8
-
9
- export function scrollToPage(
10
- refs: Array<HTMLElement | null>,
11
- pageIndex: number,
12
- behavior: ScrollBehavior = "smooth",
13
- root?: HTMLElement | null,
14
- ) {
15
- const target = refs[pageIndex];
16
- if (!target) return false;
17
-
18
- if (root && root.contains(target) && typeof root.scrollTo === "function") {
19
- const rootRect = root.getBoundingClientRect();
20
- const targetRect = target.getBoundingClientRect();
21
- const scrollMarginTop = readScrollMarginTop(target);
22
- root.scrollTo({
23
- top: Math.max(0, root.scrollTop + targetRect.top - rootRect.top - scrollMarginTop),
24
- behavior,
25
- });
26
- return true;
27
- }
28
-
29
- target.scrollIntoView({ behavior, block: "start" });
30
- return true;
31
- }
32
-
33
- function readScrollMarginTop(target: HTMLElement) {
34
- if (typeof window === "undefined") return 0;
35
- const value = Number.parseFloat(window.getComputedStyle(target).scrollMarginTop);
36
- return Number.isFinite(value) ? value : 0;
37
- }
38
-
39
- export interface PageVisibilityObserver {
40
- observe: (element: Element) => void;
41
- disconnect: () => void;
42
- }
43
-
44
- export function createPageVisibilityObserver(
45
- root: Element,
46
- onVisiblePageChange: (pageIndex: number) => void,
47
- ): PageVisibilityObserver | null {
48
- if (typeof IntersectionObserver === "undefined") return null;
49
-
50
- const ratios = new Map<Element, number>();
51
- let debounceTimer: ReturnType<typeof setTimeout> | null = null;
52
-
53
- const flush = () => {
54
- debounceTimer = null;
55
- let bestEl: Element | null = null;
56
- let bestRatio = -1;
57
- for (const [el, ratio] of ratios) {
58
- if (ratio > bestRatio) {
59
- bestRatio = ratio;
60
- bestEl = el;
61
- }
62
- }
63
- if (!bestEl || bestRatio <= 0) return;
64
- const raw = bestEl.getAttribute("data-openpress-page-index");
65
- if (raw === null) return;
66
- const parsed = Number.parseInt(raw, 10);
67
- if (Number.isFinite(parsed)) onVisiblePageChange(parsed);
68
- };
69
-
70
- const observer = new IntersectionObserver(
71
- (entries) => {
72
- for (const entry of entries) {
73
- ratios.set(entry.target, entry.isIntersecting ? entry.intersectionRatio : 0);
74
- }
75
- if (debounceTimer !== null) clearTimeout(debounceTimer);
76
- debounceTimer = setTimeout(flush, DEBOUNCE_MS);
77
- },
78
- { root, threshold: OBSERVER_THRESHOLDS },
79
- );
80
-
81
- return {
82
- observe: (element) => observer.observe(element),
83
- disconnect: () => {
84
- observer.disconnect();
85
- if (debounceTimer !== null) {
86
- clearTimeout(debounceTimer);
87
- debounceTimer = null;
88
- }
89
- ratios.clear();
90
- },
91
- };
92
- }
@@ -1,15 +0,0 @@
1
- export function clampReaderPageIndex(value: number, pageCount: number) {
2
- const normalizedPageCount = normalizeReaderPageCount(pageCount);
3
- if (normalizedPageCount <= 0) return 0;
4
- if (!Number.isFinite(value)) return 0;
5
- return Math.min(Math.max(Math.trunc(value), 0), normalizedPageCount - 1);
6
- }
7
-
8
- export function formatReaderPageNumber(value: number) {
9
- return String(Math.max(Math.trunc(value), 1)).padStart(2, "0");
10
- }
11
-
12
- export function normalizeReaderPageCount(value: number) {
13
- if (!Number.isFinite(value)) return 0;
14
- return Math.max(Math.trunc(value), 0);
15
- }
@@ -1,4 +0,0 @@
1
- import type { IndexedHtmlPage } from "../document-model";
2
- import type { HtmlPageBlock } from "../document-model";
3
-
4
- export type DisplayPage = IndexedHtmlPage & Pick<HtmlPageBlock, "source" | "frameKey">;
@@ -1,119 +0,0 @@
1
- import { useLayoutEffect, useMemo, useState, type RefObject } from "react";
2
- import { scheduleBrowserFrame } from "../shared";
3
- import {
4
- formatPageViewportScaleLabel,
5
- formatPageViewportScaleValue,
6
- resolvePageViewportScale,
7
- type PageLayoutMode,
8
- type PageViewportScaleMode,
9
- } from "./pageViewportScaleModel";
10
-
11
- export function usePageViewportScale({
12
- stageRef,
13
- pageContainerRef,
14
- pageCount,
15
- layoutMode = "single",
16
- }: {
17
- stageRef: RefObject<HTMLElement | null>;
18
- pageContainerRef: RefObject<HTMLElement | null>;
19
- pageCount: number;
20
- layoutMode?: PageLayoutMode;
21
- }) {
22
- const [scaleMode, setScaleMode] = useState<PageViewportScaleMode>("fit-width");
23
- const [scale, setScale] = useState(1);
24
-
25
- useLayoutEffect(() => {
26
- if (typeof window === "undefined") return undefined;
27
-
28
- let cancelFrame: (() => void) | null = null;
29
-
30
- const syncScale = () => {
31
- cancelFrame?.();
32
- cancelFrame = scheduleBrowserFrame(() => {
33
- cancelFrame = null;
34
- const container = pageContainerRef.current;
35
- if (!container) return;
36
-
37
- const pageSurface = container.querySelector<HTMLElement>(".openpress-html-page__html");
38
- if (!pageSurface) {
39
- container.style.setProperty("--openpress-page-viewport-scale", "1");
40
- container.dataset.openpressPageScaleMode = scaleMode;
41
- container.dataset.openpressPageScale = "1";
42
- setScale(1);
43
- return;
44
- }
45
-
46
- const stage = stageRef.current ?? container.parentElement;
47
- const containerStyle = window.getComputedStyle(container);
48
- const paddingLeft = parseCssPixelValue(containerStyle.paddingLeft);
49
- const paddingRight = parseCssPixelValue(containerStyle.paddingRight);
50
- const paddingTop = parseCssPixelValue(containerStyle.paddingTop);
51
- const paddingBottom = parseCssPixelValue(containerStyle.paddingBottom);
52
- const columnGap = parseCssPixelValue(containerStyle.columnGap || containerStyle.gap);
53
- const availableWidth = Math.max(
54
- 1,
55
- (stage?.clientWidth || container.clientWidth || window.innerWidth) - paddingLeft - paddingRight,
56
- );
57
- const availableHeight = Math.max(
58
- 1,
59
- (stage?.clientHeight || container.clientHeight || window.innerHeight) - paddingTop - paddingBottom,
60
- );
61
- const pageWidth = pageSurface.offsetWidth;
62
- const pageHeight = pageSurface.offsetHeight;
63
- const canonicalWidth = layoutMode === "spread" ? (pageWidth * 2) + columnGap : pageWidth;
64
- const canonicalHeight = pageHeight;
65
- const fitWidthScale = canonicalWidth > 0 ? availableWidth / canonicalWidth : 1;
66
- const fitPageScale = canonicalWidth > 0 && canonicalHeight > 0
67
- ? Math.min(availableWidth / canonicalWidth, availableHeight / canonicalHeight)
68
- : 1;
69
- const nextScale = resolvePageViewportScale({ mode: scaleMode, fitWidthScale, fitPageScale });
70
- const nextScaleValue = formatPageViewportScaleValue(nextScale);
71
-
72
- container.style.setProperty("--openpress-page-viewport-scale", nextScaleValue);
73
- container.dataset.openpressPageScaleMode = scaleMode;
74
- container.dataset.openpressPageScale = nextScaleValue;
75
- setScale((current) => (current === nextScale ? current : nextScale));
76
- });
77
- };
78
-
79
- syncScale();
80
-
81
- const ResizeObserverCtor = window.ResizeObserver;
82
- const observer = ResizeObserverCtor ? new ResizeObserverCtor(syncScale) : null;
83
- const stage = stageRef.current;
84
- const container = pageContainerRef.current;
85
- if (stage) observer?.observe(stage);
86
- if (container) observer?.observe(container);
87
-
88
- window.addEventListener("resize", syncScale);
89
- window.visualViewport?.addEventListener("resize", syncScale);
90
- return () => {
91
- cancelFrame?.();
92
- observer?.disconnect();
93
- window.removeEventListener("resize", syncScale);
94
- window.visualViewport?.removeEventListener("resize", syncScale);
95
- };
96
- }, [layoutMode, pageContainerRef, pageCount, scaleMode, stageRef]);
97
-
98
- const scaleLabel = useMemo(
99
- () => {
100
- const labelScale = scaleMode.startsWith("scale-")
101
- ? resolvePageViewportScale({ mode: scaleMode, fitWidthScale: scale, fitPageScale: scale })
102
- : scale;
103
- return formatPageViewportScaleLabel(scaleMode, labelScale);
104
- },
105
- [scale, scaleMode],
106
- );
107
-
108
- return {
109
- scale,
110
- scaleMode,
111
- scaleLabel,
112
- setScaleMode,
113
- };
114
- }
115
-
116
- function parseCssPixelValue(value: string) {
117
- const parsed = Number.parseFloat(value);
118
- return Number.isFinite(parsed) ? parsed : 0;
119
- }
@@ -1,56 +0,0 @@
1
- import { useCallback, useEffect, useState } from "react";
2
-
3
- export interface UsePanelStateOptions {
4
- leftPanelBreakpoint?: number;
5
- rightPanelBreakpoint?: number;
6
- onAfterResize?: () => void;
7
- }
8
-
9
- export interface PanelState {
10
- leftPanelOpen: boolean;
11
- rightPanelOpen: boolean;
12
- toggleLeftPanel: () => void;
13
- toggleRightPanel: () => void;
14
- }
15
-
16
- export function usePanelState({
17
- leftPanelBreakpoint,
18
- rightPanelBreakpoint = 1000,
19
- onAfterResize,
20
- }: UsePanelStateOptions = {}): PanelState {
21
- const shouldOpenLeftPanel = useCallback(
22
- () =>
23
- leftPanelBreakpoint === undefined || typeof window === "undefined" || window.innerWidth >= leftPanelBreakpoint,
24
- [leftPanelBreakpoint],
25
- );
26
- const shouldOpenRightPanel = useCallback(
27
- () => typeof window === "undefined" || window.innerWidth >= rightPanelBreakpoint,
28
- [rightPanelBreakpoint],
29
- );
30
-
31
- const [rightPanelOpen, setRightPanelOpen] = useState(false);
32
- const [leftPanelOpen, setLeftPanelOpen] = useState(false);
33
-
34
- useEffect(() => {
35
- if (typeof window === "undefined") return undefined;
36
-
37
- const handleResize = () => {
38
- setLeftPanelOpen((open) => (open && !shouldOpenLeftPanel() ? false : open));
39
- setRightPanelOpen((open) => (open && !shouldOpenRightPanel() ? false : open));
40
- onAfterResize?.();
41
- };
42
-
43
- handleResize();
44
- window.addEventListener("resize", handleResize);
45
- window.visualViewport?.addEventListener("resize", handleResize);
46
- return () => {
47
- window.removeEventListener("resize", handleResize);
48
- window.visualViewport?.removeEventListener("resize", handleResize);
49
- };
50
- }, [shouldOpenLeftPanel, shouldOpenRightPanel, onAfterResize]);
51
-
52
- const toggleLeftPanel = useCallback(() => setLeftPanelOpen((open) => !open), []);
53
- const toggleRightPanel = useCallback(() => setRightPanelOpen((open) => !open), []);
54
-
55
- return { leftPanelOpen, rightPanelOpen, toggleLeftPanel, toggleRightPanel };
56
- }
@@ -1,61 +0,0 @@
1
- import { useEffect, type MutableRefObject } from "react";
2
- import { pageIndexFromHash, replacePageRoute } from "./readerPageRoute";
3
- import { scrollToPage } from "./readerScroll";
4
-
5
- export interface UseReaderHashSyncOptions {
6
- stageRef: MutableRefObject<HTMLElement | null>;
7
- pageRefs: MutableRefObject<Array<HTMLElement | null>>;
8
- currentPageIndex: number;
9
- currentPageIndexRef: MutableRefObject<number>;
10
- normalizedPageCount: number;
11
- setCurrentPageIndex: (index: number) => void;
12
- armPendingScrollTarget: (target: number) => void;
13
- }
14
-
15
- export function useReaderHashSync({
16
- stageRef,
17
- pageRefs,
18
- currentPageIndex,
19
- currentPageIndexRef,
20
- normalizedPageCount,
21
- setCurrentPageIndex,
22
- armPendingScrollTarget,
23
- }: UseReaderHashSyncOptions) {
24
- // Mirror currentPageIndex into the URL hash so deep links + history work.
25
- useEffect(() => {
26
- if (typeof window === "undefined") return;
27
- replacePageRoute(currentPageIndex);
28
- }, [currentPageIndex]);
29
-
30
- // Listen for hash/back/forward navigation and drive scroll to match.
31
- useEffect(() => {
32
- if (typeof window === "undefined") return undefined;
33
-
34
- const syncFromHash = (behavior: ScrollBehavior) => {
35
- const refs = pageRefs.current;
36
- const hashPage = pageIndexFromHash(window.location.hash, normalizedPageCount);
37
- if (hashPage === null) return;
38
- // replacePageRoute writes the hash to mirror state; skip if it already
39
- // matches so we don't fight ourselves.
40
- if (hashPage === currentPageIndexRef.current) return;
41
- armPendingScrollTarget(hashPage);
42
- setCurrentPageIndex(hashPage);
43
- scrollToPage(refs, hashPage, behavior, stageRef.current);
44
- };
45
-
46
- const onHashChange = () => syncFromHash("smooth");
47
- window.addEventListener("hashchange", onHashChange);
48
- window.addEventListener("popstate", onHashChange);
49
- return () => {
50
- window.removeEventListener("hashchange", onHashChange);
51
- window.removeEventListener("popstate", onHashChange);
52
- };
53
- }, [
54
- armPendingScrollTarget,
55
- currentPageIndexRef,
56
- normalizedPageCount,
57
- pageRefs,
58
- setCurrentPageIndex,
59
- stageRef,
60
- ]);
61
- }
@@ -1,48 +0,0 @@
1
- import { useEffect } from "react";
2
-
3
- export interface UseReaderKeyboardNavOptions {
4
- nextPage: () => void;
5
- prevPage: () => void;
6
- setPage: (pageIndex: number) => void;
7
- normalizedPageCount: number;
8
- }
9
-
10
- export function useReaderKeyboardNav({
11
- nextPage,
12
- prevPage,
13
- setPage,
14
- normalizedPageCount,
15
- }: UseReaderKeyboardNavOptions) {
16
- useEffect(() => {
17
- const handleKeyDown = (event: KeyboardEvent) => {
18
- if (isEditableTarget(event.target)) return;
19
- if (hasActiveTextSelection()) return;
20
- if (event.key === "ArrowRight" || event.key === "PageDown") {
21
- event.preventDefault();
22
- nextPage();
23
- } else if (event.key === "ArrowLeft" || event.key === "PageUp") {
24
- event.preventDefault();
25
- prevPage();
26
- } else if (event.key === "Home") {
27
- event.preventDefault();
28
- setPage(0);
29
- } else if (event.key === "End") {
30
- event.preventDefault();
31
- setPage(Math.max(0, normalizedPageCount - 1));
32
- }
33
- };
34
- window.addEventListener("keydown", handleKeyDown);
35
- return () => window.removeEventListener("keydown", handleKeyDown);
36
- }, [nextPage, prevPage, setPage, normalizedPageCount]);
37
- }
38
-
39
- function isEditableTarget(target: EventTarget | null) {
40
- if (!(target instanceof HTMLElement)) return false;
41
- return Boolean(target.closest("input, textarea, select, button, [contenteditable]"));
42
- }
43
-
44
- function hasActiveTextSelection() {
45
- const selection = window.getSelection?.();
46
- if (!selection || selection.isCollapsed) return false;
47
- return Boolean(selection.toString().trim());
48
- }
@@ -1,146 +0,0 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState, type RefCallback } from "react";
2
- import { pageIndexFromHash } from "./readerPageRoute";
3
- import { createReaderPageRegistry } from "./readerPageRegistry";
4
- import { clampReaderPageIndex, formatReaderPageNumber, normalizeReaderPageCount } from "./readerStateModel";
5
- import { createPageVisibilityObserver, scrollToPage } from "./readerScroll";
6
- import { usePanelState } from "./usePanelState";
7
- import { useReaderScrollAnchor } from "./useReaderScrollAnchor";
8
- import { useReaderHashSync } from "./useReaderHashSync";
9
- import { useReaderKeyboardNav } from "./useReaderKeyboardNav";
10
-
11
- export interface SetPageOptions {
12
- behavior?: ScrollBehavior;
13
- }
14
-
15
- interface UseReaderRuntimeOptions {
16
- pageCount: number;
17
- leftPanelBreakpoint?: number;
18
- rightPanelBreakpoint?: number;
19
- }
20
-
21
- export function useReaderRuntime({
22
- pageCount,
23
- leftPanelBreakpoint,
24
- rightPanelBreakpoint = 1000,
25
- }: UseReaderRuntimeOptions) {
26
- const normalizedPageCount = normalizeReaderPageCount(pageCount);
27
- const stageRef = useRef<HTMLElement | null>(null);
28
- const [pageRegistrationVersion, setPageRegistrationVersion] = useState(0);
29
- const pageRegistry = useRef<ReturnType<typeof createReaderPageRegistry<HTMLElement>> | null>(null);
30
- if (!pageRegistry.current) {
31
- pageRegistry.current = createReaderPageRegistry<HTMLElement>(setPageRegistrationVersion);
32
- }
33
- const pageRefs = useMemo(() => ({
34
- get current() {
35
- return pageRegistry.current?.refs ?? [];
36
- },
37
- }), []) as { current: Array<HTMLElement | null> };
38
-
39
- const [currentPageIndex, setCurrentPageIndex] = useState(() => {
40
- if (typeof window === "undefined") return 0;
41
- const fromHash = pageIndexFromHash(window.location.hash, normalizedPageCount);
42
- return fromHash ?? 0;
43
- });
44
-
45
- const currentPageIndexRef = useRef(currentPageIndex);
46
- currentPageIndexRef.current = currentPageIndex;
47
-
48
- const { pendingScrollTargetRef, armPendingScrollTarget, clearPendingScrollTarget, reAnchorAfterPaint } =
49
- useReaderScrollAnchor({ stageRef, pageRefs, currentPageIndexRef });
50
-
51
- const { leftPanelOpen, rightPanelOpen, toggleLeftPanel, toggleRightPanel } = usePanelState({
52
- leftPanelBreakpoint,
53
- rightPanelBreakpoint,
54
- // scroll-snap-type: y mandatory re-aligns to the closest snap point on
55
- // viewport change, which can land one page off from where the reader was.
56
- onAfterResize: reAnchorAfterPaint,
57
- });
58
-
59
- // Trim the registry + clamp current page when the page count shrinks.
60
- useEffect(() => {
61
- pageRegistry.current?.trim(normalizedPageCount);
62
- setCurrentPageIndex((idx) => clampReaderPageIndex(idx, normalizedPageCount));
63
- }, [normalizedPageCount]);
64
-
65
- // Drive currentPageIndex from visible pages. Suppress intermediates while a
66
- // programmatic scroll is in flight.
67
- useEffect(() => {
68
- const stage = stageRef.current;
69
- if (!stage) return undefined;
70
- const observer = createPageVisibilityObserver(stage, (pageIndex) => {
71
- if (pendingScrollTargetRef.current !== null) {
72
- if (pageIndex !== pendingScrollTargetRef.current) return;
73
- clearPendingScrollTarget();
74
- }
75
- setCurrentPageIndex((prev) => (prev === pageIndex ? prev : pageIndex));
76
- });
77
- if (!observer) return undefined;
78
- pageRegistry.current?.refs.forEach((el) => el && observer.observe(el));
79
- return () => observer.disconnect();
80
- }, [clearPendingScrollTarget, normalizedPageCount, pageRegistrationVersion, pendingScrollTargetRef]);
81
-
82
- // When refs change (initial mount, pagination kicks in), re-anchor the stage
83
- // to the page we already believe we're on so scroll-snap mandatory doesn't
84
- // pull us to whichever page is closest.
85
- useEffect(() => {
86
- const refs = pageRegistry.current?.refs ?? [];
87
- const idx = currentPageIndexRef.current;
88
- if (idx === 0) return;
89
- if (!refs[idx]) return;
90
- armPendingScrollTarget(idx);
91
- scrollToPage(refs, idx, "instant", stageRef.current);
92
- }, [armPendingScrollTarget, pageRegistrationVersion]);
93
-
94
- const setPage = useCallback(
95
- (pageIndex: number, options: SetPageOptions = {}) => {
96
- const refs = pageRegistry.current?.refs ?? [];
97
- const target = clampReaderPageIndex(pageIndex, normalizedPageCount);
98
- armPendingScrollTarget(target);
99
- setCurrentPageIndex(target);
100
- scrollToPage(refs, target, options.behavior ?? "smooth", stageRef.current);
101
- },
102
- [armPendingScrollTarget, normalizedPageCount],
103
- );
104
-
105
- const nextPage = useCallback(() => {
106
- setPage(currentPageIndexRef.current + 1);
107
- }, [setPage]);
108
-
109
- const prevPage = useCallback(() => {
110
- setPage(currentPageIndexRef.current - 1);
111
- }, [setPage]);
112
-
113
- useReaderHashSync({
114
- stageRef,
115
- pageRefs,
116
- currentPageIndex,
117
- currentPageIndexRef,
118
- normalizedPageCount,
119
- setCurrentPageIndex,
120
- armPendingScrollTarget,
121
- });
122
-
123
- useReaderKeyboardNav({ nextPage, prevPage, setPage, normalizedPageCount });
124
-
125
- const registerPage = useCallback<(pageIndex: number) => RefCallback<HTMLElement>>(
126
- (pageIndex) => pageRegistry.current?.registerPage(pageIndex) ?? (() => undefined),
127
- [],
128
- );
129
-
130
- const progressPercent =
131
- normalizedPageCount <= 1 ? 100 : ((currentPageIndex + 1) / normalizedPageCount) * 100;
132
-
133
- return {
134
- stageRef,
135
- currentPageIndex,
136
- currentPageLabel: formatReaderPageNumber(currentPageIndex + 1),
137
- totalPageLabel: formatReaderPageNumber(normalizedPageCount),
138
- progressPercent,
139
- leftPanelOpen,
140
- rightPanelOpen,
141
- registerPage,
142
- setPage,
143
- toggleLeftPanel,
144
- toggleRightPanel,
145
- };
146
- }
@@ -1,64 +0,0 @@
1
- import { useCallback, useEffect, useRef, type MutableRefObject } from "react";
2
- import { scrollToPage } from "./readerScroll";
3
-
4
- // Generous upper bound on a smooth scrollIntoView. If the target ref is gone or
5
- // the browser never settles on it, clear the guard so the IO observer regains
6
- // authority over currentPageIndex.
7
- const PROGRAMMATIC_SCROLL_FALLBACK_MS = 2500;
8
-
9
- export interface UseReaderScrollAnchorOptions {
10
- stageRef: MutableRefObject<HTMLElement | null>;
11
- pageRefs: MutableRefObject<Array<HTMLElement | null>>;
12
- currentPageIndexRef: MutableRefObject<number>;
13
- }
14
-
15
- export interface ReaderScrollAnchor {
16
- pendingScrollTargetRef: MutableRefObject<number | null>;
17
- armPendingScrollTarget: (target: number) => void;
18
- clearPendingScrollTarget: () => void;
19
- reAnchorAfterPaint: () => void;
20
- }
21
-
22
- export function useReaderScrollAnchor({
23
- stageRef,
24
- pageRefs,
25
- currentPageIndexRef,
26
- }: UseReaderScrollAnchorOptions): ReaderScrollAnchor {
27
- // While a programmatic scroll is in flight, the IntersectionObserver should
28
- // only accept the destination page (not the intermediates we sweep past).
29
- const pendingScrollTargetRef = useRef<number | null>(null);
30
- const pendingScrollClearTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
31
-
32
- const armPendingScrollTarget = useCallback((target: number) => {
33
- pendingScrollTargetRef.current = target;
34
- if (pendingScrollClearTimerRef.current !== null) clearTimeout(pendingScrollClearTimerRef.current);
35
- pendingScrollClearTimerRef.current = setTimeout(() => {
36
- pendingScrollTargetRef.current = null;
37
- pendingScrollClearTimerRef.current = null;
38
- }, PROGRAMMATIC_SCROLL_FALLBACK_MS);
39
- }, []);
40
-
41
- const clearPendingScrollTarget = useCallback(() => {
42
- pendingScrollTargetRef.current = null;
43
- if (pendingScrollClearTimerRef.current !== null) {
44
- clearTimeout(pendingScrollClearTimerRef.current);
45
- pendingScrollClearTimerRef.current = null;
46
- }
47
- }, []);
48
-
49
- useEffect(() => () => clearPendingScrollTarget(), [clearPendingScrollTarget]);
50
-
51
- // Re-anchor the stage to the page we already believe we're on. scroll-snap
52
- // mandatory would otherwise snap to whichever page is closest after a layout
53
- // change. Pin to the active programmatic target if there is one.
54
- const reAnchorAfterPaint = useCallback(() => {
55
- if (typeof window === "undefined") return;
56
- window.requestAnimationFrame(() => {
57
- const refs = pageRefs.current;
58
- const target = pendingScrollTargetRef.current ?? currentPageIndexRef.current;
59
- scrollToPage(refs, target, "instant", stageRef.current);
60
- });
61
- }, [currentPageIndexRef, pageRefs, stageRef]);
62
-
63
- return { pendingScrollTargetRef, armPendingScrollTarget, clearPendingScrollTarget, reAnchorAfterPaint };
64
- }