@open-press/core 0.3.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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -0
  3. package/engine/chrome-pdf.d.mts +34 -0
  4. package/engine/chrome-pdf.mjs +344 -0
  5. package/engine/cli.mjs +93 -0
  6. package/engine/commands/_shared.mjs +170 -0
  7. package/engine/commands/deploy.mjs +31 -0
  8. package/engine/commands/dev.mjs +26 -0
  9. package/engine/commands/export.mjs +8 -0
  10. package/engine/commands/init.mjs +24 -0
  11. package/engine/commands/inspect.mjs +35 -0
  12. package/engine/commands/migrate-to-react.mjs +27 -0
  13. package/engine/commands/pdf.mjs +26 -0
  14. package/engine/commands/preview.mjs +26 -0
  15. package/engine/commands/render.mjs +17 -0
  16. package/engine/commands/replace.mjs +41 -0
  17. package/engine/commands/search.mjs +33 -0
  18. package/engine/commands/typecheck.mjs +5 -0
  19. package/engine/commands/validate.mjs +17 -0
  20. package/engine/config.d.mts +40 -0
  21. package/engine/config.mjs +160 -0
  22. package/engine/deploy-sync.mjs +15 -0
  23. package/engine/document-export.mjs +15 -0
  24. package/engine/file-utils.mjs +106 -0
  25. package/engine/fonts.mjs +62 -0
  26. package/engine/init.mjs +90 -0
  27. package/engine/inspection.mjs +348 -0
  28. package/engine/issue-report.mjs +44 -0
  29. package/engine/katex-assets.mjs +45 -0
  30. package/engine/page-block.mjs +30 -0
  31. package/engine/page-renderer.mjs +217 -0
  32. package/engine/pdf-media.mjs +45 -0
  33. package/engine/public-assets.mjs +19 -0
  34. package/engine/react/chapter-css.mjs +53 -0
  35. package/engine/react/comment-endpoint.d.mts +11 -0
  36. package/engine/react/comment-endpoint.mjs +128 -0
  37. package/engine/react/comment-marker.mjs +306 -0
  38. package/engine/react/document-entry.mjs +253 -0
  39. package/engine/react/document-export.mjs +392 -0
  40. package/engine/react/mdx-compile.mjs +295 -0
  41. package/engine/react/measurement-css.mjs +44 -0
  42. package/engine/react/migrate-to-react.mjs +355 -0
  43. package/engine/react/pagination-constants.mjs +3 -0
  44. package/engine/react/pagination.mjs +121 -0
  45. package/engine/react/project-asset-endpoint.d.mts +10 -0
  46. package/engine/react/project-asset-endpoint.mjs +379 -0
  47. package/engine/react/workspace-discovery.mjs +156 -0
  48. package/engine/source-text-tools.mjs +280 -0
  49. package/engine/source-workspace.mjs +76 -0
  50. package/engine/static-server.mjs +493 -0
  51. package/engine/validation.mjs +172 -0
  52. package/index.html +13 -0
  53. package/package.json +86 -0
  54. package/src/openpress/App.tsx +127 -0
  55. package/src/openpress/composerMentions.ts +188 -0
  56. package/src/openpress/core/basePages.tsx +87 -0
  57. package/src/openpress/core/index.tsx +20 -0
  58. package/src/openpress/core/types.ts +71 -0
  59. package/src/openpress/frameScheduler.ts +32 -0
  60. package/src/openpress/indexes.ts +329 -0
  61. package/src/openpress/inspector.ts +282 -0
  62. package/src/openpress/pageRoute.ts +21 -0
  63. package/src/openpress/pagination.ts +845 -0
  64. package/src/openpress/projectIdentity.ts +15 -0
  65. package/src/openpress/projectSources.ts +24 -0
  66. package/src/openpress/projectWorkspace.tsx +919 -0
  67. package/src/openpress/publicPage.tsx +469 -0
  68. package/src/openpress/reactDocumentMetadata.ts +41 -0
  69. package/src/openpress/readerPageRegistry.ts +41 -0
  70. package/src/openpress/readerRuntime.ts +230 -0
  71. package/src/openpress/readerScroll.ts +92 -0
  72. package/src/openpress/readerState.ts +15 -0
  73. package/src/openpress/renderer.tsx +91 -0
  74. package/src/openpress/runtimeMode.ts +22 -0
  75. package/src/openpress/types.ts +112 -0
  76. package/src/openpress/workbench.tsx +1299 -0
  77. package/src/openpress/workbenchPanels.tsx +122 -0
  78. package/src/openpress/workbenchTypes.ts +4 -0
  79. package/src/styles/openpress/app-shell.css +251 -0
  80. package/src/styles/openpress/media-workspace.css +230 -0
  81. package/src/styles/openpress/print-route.css +186 -0
  82. package/src/styles/openpress/project-workspace.css +1318 -0
  83. package/src/styles/openpress/public-viewer.css +983 -0
  84. package/src/styles/openpress/reader-runtime.css +792 -0
  85. package/src/styles/openpress/responsive.css +384 -0
  86. package/src/styles/openpress/workbench-panels.css +558 -0
  87. package/src/styles/openpress/workbench.css +720 -0
  88. package/src/styles/openpress.css +14 -0
  89. package/tsconfig.json +37 -0
  90. package/vite.config.ts +512 -0
@@ -0,0 +1,230 @@
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
+ }
@@ -0,0 +1,92 @@
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
+ }
@@ -0,0 +1,15 @@
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
+ }
@@ -0,0 +1,91 @@
1
+ import { useMemo, type CSSProperties } from "react";
2
+ import { PrintDocument, PublicViewer } from "./publicPage";
3
+ import { isPrintModeLocation, isWorkspaceModeLocation } from "./runtimeMode";
4
+ import { HtmlWorkbench } from "./workbench";
5
+ import type {
6
+ DeploymentInfo,
7
+ ReaderDocument,
8
+ HtmlPageBlock,
9
+ Theme,
10
+ } from "./types";
11
+
12
+ interface RendererProps {
13
+ document: ReaderDocument;
14
+ deploymentInfo?: DeploymentInfo;
15
+ }
16
+
17
+ export function Renderer({
18
+ document,
19
+ deploymentInfo = { online: false },
20
+ }: RendererProps) {
21
+ const style = themeToCssVariables(document.theme);
22
+ const htmlPages = document.blocks.filter((block): block is HtmlPageBlock => block.kind === "htmlPage");
23
+ const workspaceMode = useMemo(() => {
24
+ if (typeof window === "undefined") return false;
25
+ return isWorkspaceModeLocation(window.location);
26
+ }, []);
27
+ const printMode = useMemo(() => {
28
+ if (typeof window === "undefined") return false;
29
+ return isPrintModeLocation(window.location);
30
+ }, []);
31
+
32
+ if (htmlPages.length > 0) {
33
+ if (printMode) {
34
+ return <PrintDocument document={document} pages={htmlPages} style={style} />;
35
+ }
36
+
37
+ if (!workspaceMode) {
38
+ return <PublicViewer document={document} pages={htmlPages} style={style} deploymentInfo={deploymentInfo} />;
39
+ }
40
+
41
+ return (
42
+ <HtmlWorkbench
43
+ document={document}
44
+ pages={htmlPages}
45
+ style={style}
46
+ devMode={workspaceMode}
47
+ deploymentInfo={deploymentInfo}
48
+ />
49
+ );
50
+ }
51
+
52
+ return <EmptyState style={style} workspaceMode={workspaceMode} />;
53
+ }
54
+
55
+ function EmptyState({ style, workspaceMode }: { style: CSSProperties; workspaceMode: boolean }) {
56
+ return (
57
+ <main className="openpress-shell openpress-empty-state" style={style}>
58
+ <section className="openpress-empty-state__panel">
59
+ <p className="openpress-empty-state__eyebrow">OpenPress</p>
60
+ <h1 className="openpress-empty-state__title">This document has no content yet.</h1>
61
+ <p className="openpress-empty-state__body">
62
+ Add React MDX chapter files under <code>document/chapters/**/content/</code>, then re-export.
63
+ </p>
64
+ {workspaceMode ? (
65
+ <ol className="openpress-empty-state__steps">
66
+ <li><code>npm run openpress:export</code> &nbsp;— refreshes <code>public/openpress/document.json</code></li>
67
+ <li>Reload this page</li>
68
+ </ol>
69
+ ) : (
70
+ <p className="openpress-empty-state__body">
71
+ (If you are the document author, run <code>npm run dev</code> locally to edit.)
72
+ </p>
73
+ )}
74
+ </section>
75
+ </main>
76
+ );
77
+ }
78
+
79
+ function themeToCssVariables(theme?: Theme) {
80
+ const style: CSSProperties & Record<`--${string}`, string> = {
81
+ "--openpress-font-family": theme?.fontFamily ?? "'Noto Sans TC', 'PingFang TC', sans-serif",
82
+ "--openpress-accent": theme?.accentColor ?? "#df4b21",
83
+ "--openpress-text": theme?.textColor ?? "#20242a",
84
+ };
85
+
86
+ if (theme?.pageWidth) style["--openpress-page-width"] = theme.pageWidth;
87
+ if (theme?.pageHeight) style["--openpress-page-height"] = theme.pageHeight;
88
+ if (theme?.pagePadding) style["--openpress-page-padding"] = theme.pagePadding;
89
+
90
+ return style;
91
+ }
@@ -0,0 +1,22 @@
1
+ export function isLocalWorkspaceHost(hostname: string) {
2
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
3
+ }
4
+
5
+ export function isWorkspaceModeLocation(location: Pick<Location, "hostname" | "search">) {
6
+ return isLocalWorkspaceHost(location.hostname) && new URLSearchParams(location.search).has("dev");
7
+ }
8
+
9
+ export function isPrintModeLocation(location: Pick<Location, "search">) {
10
+ return new URLSearchParams(location.search).has("print");
11
+ }
12
+
13
+ export function buildPublicPreviewHref(currentHref: string, pageIndex?: number) {
14
+ const url = new URL(currentHref);
15
+ url.searchParams.delete("dev");
16
+ url.searchParams.delete("workspace");
17
+ url.searchParams.delete("fontPreview");
18
+ if (typeof pageIndex === "number") {
19
+ url.hash = `page-${String(pageIndex + 1).padStart(2, "0")}`;
20
+ }
21
+ return url.toString();
22
+ }
@@ -0,0 +1,112 @@
1
+ export interface DeploymentInfo {
2
+ online: boolean;
3
+ deployedAt?: string;
4
+ pdf?: string;
5
+ publicUrl?: string;
6
+ dirty?: boolean;
7
+ configured?: boolean;
8
+ adapter?: string;
9
+ source?: string;
10
+ projectName?: string;
11
+ setupMessage?: string;
12
+ }
13
+
14
+ export interface ReaderDocument {
15
+ meta: DocumentMeta;
16
+ source?: DocumentSource;
17
+ theme?: Theme;
18
+ blocks: HtmlPageBlock[];
19
+ }
20
+
21
+ export interface DocumentSource {
22
+ type: string;
23
+ contentDir?: string;
24
+ editable?: boolean;
25
+ editMode?: string;
26
+ styles?: DocumentStyle[];
27
+ blockMap?: Record<string, SourceBlock>;
28
+ pagination?: BuildPagination;
29
+ }
30
+
31
+ export interface DocumentStyle {
32
+ kind: string;
33
+ href?: string;
34
+ path?: string;
35
+ }
36
+
37
+ export interface SourceLocation {
38
+ line: number;
39
+ column: number;
40
+ endLine?: number;
41
+ endColumn?: number;
42
+ }
43
+
44
+ export interface SourceBlock {
45
+ id: string;
46
+ kind?: string;
47
+ name?: string;
48
+ chapterSlug?: string;
49
+ path: string;
50
+ pageIndex?: number;
51
+ pageNumber?: number;
52
+ source?: SourceLocation;
53
+ }
54
+
55
+ export interface BuildPagination {
56
+ mode: string;
57
+ pageSafeHeightPx?: number;
58
+ warnings?: PaginationWarning[];
59
+ }
60
+
61
+ export interface PaginationWarning {
62
+ code: string;
63
+ blockId?: string;
64
+ height?: number;
65
+ pageSafeHeightPx?: number;
66
+ path?: string;
67
+ source?: SourceLocation;
68
+ }
69
+
70
+ export interface DocumentMeta {
71
+ title: string;
72
+ subtitle?: string;
73
+ organization?: string;
74
+ version?: string;
75
+ footer?: string;
76
+ workspaceLabel?: string;
77
+ }
78
+
79
+ export interface Theme {
80
+ pageWidth?: string;
81
+ pageHeight?: string;
82
+ pagePadding?: string;
83
+ fontFamily?: string;
84
+ accentColor?: string;
85
+ textColor?: string;
86
+ }
87
+
88
+ export interface BlockSource {
89
+ file: string;
90
+ path: string;
91
+ kind?: string;
92
+ chapter?: number;
93
+ slug?: string;
94
+ sectionIndex?: number;
95
+ }
96
+
97
+ // The engine currently emits one block kind only: a fully-rendered HTML page.
98
+ // All historical structured-block variants (cover / section / paragraph /
99
+ // list / figure / table / callout) were aspirational and never instantiated;
100
+ // they were removed along with the unused client-side block model. If the
101
+ // engine ever emits structured blocks again, reintroduce a discriminated
102
+ // union here.
103
+ export interface HtmlPageBlock {
104
+ id: string;
105
+ kind: "htmlPage";
106
+ title: string;
107
+ pageNumber: number;
108
+ html: string;
109
+ anchors?: string[];
110
+ className?: string;
111
+ source?: BlockSource;
112
+ }