@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.
- package/README.md +29 -13
- package/dist/cli.js +44 -195
- package/package.json +4 -5
- package/template/core/AGENTS.md +18 -14
- package/template/core/CHANGELOG.md +57 -9
- package/template/core/README.md +6 -3
- package/template/core/engine/cli.mjs +8 -8
- package/template/core/engine/commands/_shared.mjs +37 -15
- package/template/core/engine/commands/dev.mjs +2 -2
- package/template/core/engine/commands/image.mjs +29 -0
- package/template/core/engine/commands/skills-sync.mjs +71 -0
- package/template/core/engine/commands/typecheck.mjs +63 -1
- package/template/core/engine/commands/upgrade.mjs +3 -3
- package/template/core/engine/document-export.mjs +1 -1
- package/template/core/engine/output/chrome-pdf.mjs +110 -3
- package/template/core/engine/output/static-server.mjs +87 -9
- package/template/core/engine/react/comment-endpoint.mjs +13 -39
- package/template/core/engine/react/comment-marker.mjs +43 -19
- package/template/core/engine/react/document-entry.mjs +46 -28
- package/template/core/engine/react/document-export.mjs +328 -164
- package/template/core/engine/react/http-json.mjs +24 -0
- package/template/core/engine/react/mdx-compile.mjs +126 -3
- package/template/core/engine/react/measurement-css.mjs +114 -1
- package/template/core/engine/react/object-entities.mjs +204 -0
- package/template/core/engine/react/pagination/allocator.mjs +48 -3
- package/template/core/engine/react/pagination.mjs +1 -1
- package/template/core/engine/react/pipeline/allocate.mjs +41 -72
- package/template/core/engine/react/pipeline/frame-measurement.mjs +6 -0
- package/template/core/engine/react/press-tree-inspection.mjs +172 -0
- package/template/core/engine/react/project-asset-endpoint.mjs +6 -24
- package/template/core/engine/react/source-edit-endpoint.d.mts +10 -0
- package/template/core/engine/react/source-edit-endpoint.mjs +75 -0
- package/template/core/engine/react/sources/mdx-resolver.mjs +13 -15
- package/template/core/engine/react/style-discovery.mjs +23 -8
- package/template/core/engine/runtime/config.d.mts +8 -0
- package/template/core/engine/runtime/config.mjs +57 -60
- package/template/core/engine/runtime/file-utils.mjs +9 -1
- package/template/core/engine/runtime/file-walk.mjs +22 -0
- package/template/core/engine/runtime/inspection.mjs +1 -20
- package/template/core/engine/runtime/page-geometry.mjs +131 -0
- package/template/core/engine/runtime/path-utils.mjs +20 -0
- package/template/core/engine/runtime/source-text-tools.d.mts +102 -0
- package/template/core/engine/runtime/source-text-tools.mjs +551 -16
- package/template/core/engine/runtime/source-workspace.mjs +16 -34
- package/template/core/engine/runtime/validation.mjs +19 -10
- package/template/core/openpress.config.mjs +3 -7
- package/template/core/package.json +3 -5
- package/template/core/src/main.tsx +2 -2
- package/template/core/src/openpress/app/OpenPressApp.tsx +296 -0
- package/template/core/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +20 -9
- package/template/core/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
- package/template/core/src/openpress/app/index.ts +2 -0
- package/template/core/src/openpress/core/Frame.tsx +26 -15
- package/template/core/src/openpress/core/FrameContext.tsx +10 -3
- package/template/core/src/openpress/core/MdxArea.tsx +11 -12
- package/template/core/src/openpress/core/Press.tsx +25 -4
- package/template/core/src/openpress/core/Workspace.tsx +36 -0
- package/template/core/src/openpress/core/cn.ts +4 -0
- package/template/core/src/openpress/core/index.tsx +11 -3
- package/template/core/src/openpress/core/primitives.tsx +74 -6
- package/template/core/src/openpress/core/types.ts +94 -41
- package/template/core/src/openpress/core/useSource.ts +1 -1
- package/template/core/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
- package/template/core/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
- package/template/core/src/openpress/{types.ts → document-model/documentTypes.ts} +51 -0
- package/template/core/src/openpress/document-model/index.ts +7 -0
- package/template/core/src/openpress/document-model/objectEntityModel.ts +55 -0
- package/template/core/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
- package/template/core/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
- package/template/core/src/openpress/document-model/workspaceManifestModel.ts +57 -0
- package/template/core/src/openpress/manuscript/index.tsx +49 -7
- package/template/core/src/openpress/mdx/index.ts +15 -7
- package/template/core/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
- package/template/core/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
- package/template/core/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
- package/template/core/src/openpress/reader/index.ts +11 -0
- package/template/core/src/openpress/reader/pageViewportScaleModel.ts +73 -0
- package/template/core/src/openpress/reader/readerTypes.ts +4 -0
- package/template/core/src/openpress/reader/usePageViewportScale.ts +119 -0
- package/template/core/src/openpress/reader/usePanelState.ts +56 -0
- package/template/core/src/openpress/reader/useReaderHashSync.ts +61 -0
- package/template/core/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
- package/template/core/src/openpress/reader/useReaderRuntime.ts +146 -0
- package/template/core/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
- package/template/core/src/openpress/shared/Panel.tsx +77 -0
- package/template/core/src/openpress/shared/index.ts +4 -0
- package/template/core/src/openpress/shared/numberUtils.ts +3 -0
- package/template/core/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
- package/template/core/src/openpress/workbench/Workbench.tsx +506 -0
- package/template/core/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
- package/template/core/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
- package/template/core/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
- package/template/core/src/openpress/workbench/actions/SearchControl.tsx +345 -0
- package/template/core/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
- package/template/core/src/openpress/workbench/actions/index.ts +6 -0
- package/template/core/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
- package/template/core/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
- package/template/core/src/openpress/workbench/dialog/index.ts +1 -0
- package/template/core/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
- package/template/core/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
- package/template/core/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
- package/template/core/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
- package/template/core/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
- package/template/core/src/openpress/workbench/document/index.ts +10 -0
- package/template/core/src/openpress/workbench/index.ts +2 -0
- package/template/core/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
- package/template/core/src/openpress/workbench/inspector/index.ts +5 -0
- package/template/core/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
- package/template/core/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
- package/template/core/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
- package/template/core/src/openpress/workbench/inspector/useInspectorComments.ts +254 -0
- package/template/core/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
- package/template/core/src/openpress/workbench/mentions/index.ts +2 -0
- package/template/core/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
- package/template/core/src/openpress/workbench/panels/Panel.tsx +1 -0
- package/template/core/src/openpress/workbench/panels/PendingCommentsPanel.tsx +80 -0
- package/template/core/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
- package/template/core/src/openpress/workbench/panels/index.ts +3 -0
- package/template/core/src/openpress/workbench/project/ProjectEntryPanel.tsx +525 -0
- package/template/core/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
- package/template/core/src/openpress/workbench/project/index.ts +2 -0
- package/template/core/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
- package/template/core/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
- package/template/core/src/openpress/workbench/shell/index.ts +1 -0
- package/template/core/src/openpress/workbench/workbenchFormatters.ts +120 -0
- package/template/core/src/openpress/workbench/workbenchTypes.ts +35 -0
- package/template/core/src/styles/openpress/print-route.css +0 -2
- package/template/core/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
- package/template/core/src/styles/openpress/public-viewer.css +25 -320
- package/template/core/src/styles/openpress/reader-runtime.css +252 -55
- package/template/core/src/styles/openpress/responsive.css +145 -270
- package/template/core/src/styles/openpress/workbench-panels.css +327 -178
- package/template/core/src/styles/openpress/workbench.css +986 -451
- package/template/core/src/styles/openpress/workspace-gallery.css +300 -0
- package/template/core/src/styles/openpress.css +2 -1
- package/template/core/tsconfig.json +1 -1
- package/template/core/vite.config.ts +50 -0
- package/template/core/engine/commands/init.mjs +0 -24
- package/template/core/engine/init.mjs +0 -90
- package/template/core/src/openpress/App.tsx +0 -127
- package/template/core/src/openpress/inspector.ts +0 -282
- package/template/core/src/openpress/projectWorkspace.tsx +0 -919
- package/template/core/src/openpress/readerRuntime.ts +0 -230
- package/template/core/src/openpress/workbench.tsx +0 -1265
- package/template/core/src/openpress/workbenchTypes.ts +0 -4
- package/template/packs/academic-paper/document/chapters/01-introduction/content/01-introduction.mdx +0 -35
- package/template/packs/academic-paper/document/chapters/02-methods/content/01-methods.mdx +0 -50
- package/template/packs/academic-paper/document/chapters/03-results-and-discussion/content/01-results.mdx +0 -47
- package/template/packs/academic-paper/document/chapters/04-acknowledgment/content/01-acknowledgment.mdx +0 -26
- package/template/packs/academic-paper/document/chapters/05-references/content/01-references.mdx +0 -32
- package/template/packs/academic-paper/document/components/ChapterOpenerVisual/index.tsx +0 -76
- package/template/packs/academic-paper/document/components/Page.tsx +0 -60
- package/template/packs/academic-paper/document/components/TokenSwatchGrid/index.tsx +0 -46
- package/template/packs/academic-paper/document/components/TokenSwatchGrid/style.css +0 -63
- package/template/packs/academic-paper/document/components/TypeSpecimen/index.tsx +0 -38
- package/template/packs/academic-paper/document/components/TypeSpecimen/style.css +0 -111
- package/template/packs/academic-paper/document/design.md +0 -279
- package/template/packs/academic-paper/document/index.tsx +0 -123
- package/template/packs/academic-paper/document/media/README.md +0 -13
- package/template/packs/academic-paper/document/media/figure-placeholder.svg +0 -9
- package/template/packs/academic-paper/document/openpress.config.mjs +0 -26
- package/template/packs/academic-paper/document/theme/README.md +0 -11
- package/template/packs/academic-paper/document/theme/base/page-contract.css +0 -522
- package/template/packs/academic-paper/document/theme/base/print.css +0 -93
- package/template/packs/academic-paper/document/theme/base/typography.css +0 -333
- package/template/packs/academic-paper/document/theme/fonts.css +0 -3
- package/template/packs/academic-paper/document/theme/page-surfaces/back-cover.css +0 -43
- package/template/packs/academic-paper/document/theme/page-surfaces/chapter-opener.css +0 -205
- package/template/packs/academic-paper/document/theme/page-surfaces/cover.css +0 -294
- package/template/packs/academic-paper/document/theme/page-surfaces/toc.css +0 -149
- package/template/packs/academic-paper/document/theme/patterns/_chart-frame.css +0 -49
- package/template/packs/academic-paper/document/theme/patterns/figure-grid.css +0 -68
- package/template/packs/academic-paper/document/theme/patterns/table-utilities.css +0 -66
- package/template/packs/academic-paper/document/theme/shell/reader-controls.css +0 -761
- package/template/packs/academic-paper/document/theme/tokens.css +0 -80
- package/template/packs/academic-paper/openpress.config.mjs +0 -5
- package/template/packs/claude-document/document/chapters/01-document-shape/content/01-document-shape.mdx +0 -51
- package/template/packs/claude-document/document/chapters/02-review-loop/content/01-review-loop.mdx +0 -31
- package/template/packs/claude-document/document/components/ChapterOpenerVisual.tsx +0 -96
- package/template/packs/claude-document/document/components/Page.tsx +0 -37
- package/template/packs/claude-document/document/design.md +0 -142
- package/template/packs/claude-document/document/index.tsx +0 -94
- package/template/packs/claude-document/document/media/README.md +0 -13
- package/template/packs/claude-document/document/openpress.config.mjs +0 -26
- package/template/packs/claude-document/document/theme/README.md +0 -15
- package/template/packs/claude-document/document/theme/base/page-contract.css +0 -525
- package/template/packs/claude-document/document/theme/base/print.css +0 -93
- package/template/packs/claude-document/document/theme/base/typography.css +0 -612
- package/template/packs/claude-document/document/theme/fonts.css +0 -4
- package/template/packs/claude-document/document/theme/page-surfaces/back-cover.css +0 -72
- package/template/packs/claude-document/document/theme/page-surfaces/chapter-opener.css +0 -236
- package/template/packs/claude-document/document/theme/page-surfaces/cover.css +0 -309
- package/template/packs/claude-document/document/theme/page-surfaces/toc.css +0 -225
- package/template/packs/claude-document/document/theme/patterns/_chart-frame.css +0 -53
- package/template/packs/claude-document/document/theme/patterns/figure-grid.css +0 -68
- package/template/packs/claude-document/document/theme/patterns/table-utilities.css +0 -66
- package/template/packs/claude-document/document/theme/shell/reader-controls.css +0 -789
- package/template/packs/claude-document/document/theme/tokens.css +0 -89
- package/template/packs/claude-document/openpress.config.mjs +0 -5
- package/template/packs/editorial-monograph/document/chapters/01-product-and-use-cases/content/01-product-and-use-cases.mdx +0 -31
- package/template/packs/editorial-monograph/document/chapters/02-workflow/content/01-workflow.mdx +0 -89
- package/template/packs/editorial-monograph/document/chapters/03-agent-skills-contributors/content/01-agent-skills-contributors.mdx +0 -51
- package/template/packs/editorial-monograph/document/chapters/04-validation-deploy/content/01-validation-deploy.mdx +0 -39
- package/template/packs/editorial-monograph/document/components/ChapterOpenerVisual/index.tsx +0 -76
- package/template/packs/editorial-monograph/document/components/Page.tsx +0 -37
- package/template/packs/editorial-monograph/document/components/TokenSwatchGrid/index.tsx +0 -46
- package/template/packs/editorial-monograph/document/components/TokenSwatchGrid/style.css +0 -63
- package/template/packs/editorial-monograph/document/components/TypeSpecimen/index.tsx +0 -38
- package/template/packs/editorial-monograph/document/components/TypeSpecimen/style.css +0 -111
- package/template/packs/editorial-monograph/document/design.md +0 -279
- package/template/packs/editorial-monograph/document/index.tsx +0 -97
- package/template/packs/editorial-monograph/document/media/README.md +0 -13
- package/template/packs/editorial-monograph/document/openpress.config.mjs +0 -26
- package/template/packs/editorial-monograph/document/theme/README.md +0 -11
- package/template/packs/editorial-monograph/document/theme/base/page-contract.css +0 -505
- package/template/packs/editorial-monograph/document/theme/base/print.css +0 -93
- package/template/packs/editorial-monograph/document/theme/base/typography.css +0 -336
- package/template/packs/editorial-monograph/document/theme/fonts.css +0 -3
- package/template/packs/editorial-monograph/document/theme/page-surfaces/back-cover.css +0 -43
- package/template/packs/editorial-monograph/document/theme/page-surfaces/chapter-opener.css +0 -205
- package/template/packs/editorial-monograph/document/theme/page-surfaces/cover.css +0 -147
- package/template/packs/editorial-monograph/document/theme/page-surfaces/toc.css +0 -149
- package/template/packs/editorial-monograph/document/theme/patterns/_chart-frame.css +0 -49
- package/template/packs/editorial-monograph/document/theme/patterns/figure-grid.css +0 -68
- package/template/packs/editorial-monograph/document/theme/patterns/table-utilities.css +0 -66
- package/template/packs/editorial-monograph/document/theme/shell/reader-controls.css +0 -761
- package/template/packs/editorial-monograph/document/theme/tokens.css +0 -80
- package/template/packs/editorial-monograph/openpress.config.mjs +0 -5
- /package/template/core/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
- /package/template/core/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
- /package/template/core/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
- /package/template/core/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
- /package/template/core/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
- /package/template/core/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type CSSProperties } from "react";
|
|
2
|
+
import type { HtmlPageBlock, Theme } from "../document-model";
|
|
3
|
+
import { Panel } from "../shared";
|
|
4
|
+
|
|
5
|
+
// Used by canvas-style Press (slides, social posts) that don't have an
|
|
6
|
+
// MDX-derived TOC. Renders each page as a clickable miniature so the user
|
|
7
|
+
// can navigate without bookmarks. The miniature embeds the same HTML
|
|
8
|
+
// that the main reader renders, scaled to fit the panel width.
|
|
9
|
+
|
|
10
|
+
const FALLBACK_PAGE_WIDTH_PX = 794; // A4 portrait at 96dpi — matches reader default.
|
|
11
|
+
|
|
12
|
+
export function PageThumbnails({
|
|
13
|
+
pages,
|
|
14
|
+
currentPageIndex,
|
|
15
|
+
onSelectPage,
|
|
16
|
+
theme,
|
|
17
|
+
}: {
|
|
18
|
+
pages: HtmlPageBlock[];
|
|
19
|
+
currentPageIndex: number;
|
|
20
|
+
onSelectPage: (pageIndex: number, options?: { behavior?: ScrollBehavior }) => void;
|
|
21
|
+
theme?: Theme;
|
|
22
|
+
}) {
|
|
23
|
+
const pageWidthPx = parsePxLength(theme?.pageWidth) ?? FALLBACK_PAGE_WIDTH_PX;
|
|
24
|
+
const pageHeightPx = parsePxLength(theme?.pageHeight) ?? pageWidthPx;
|
|
25
|
+
// Compute aspect from the parsed dimensions so it always matches the
|
|
26
|
+
// page render. theme.pageAspectRatio may be missing on per-Press
|
|
27
|
+
// documents in multi-Press workspaces, which is why we don't read it
|
|
28
|
+
// here.
|
|
29
|
+
const aspectRatio = `${pageWidthPx} / ${pageHeightPx}`;
|
|
30
|
+
|
|
31
|
+
if (pages.length === 0) {
|
|
32
|
+
return <Panel.Empty className="openpress-asset-empty" role="status">尚無頁面</Panel.Empty>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<ul className="openpress-thumb-list" aria-label="頁面縮圖">
|
|
37
|
+
{pages.map((page, index) => (
|
|
38
|
+
<li key={page.id}>
|
|
39
|
+
<ThumbnailCard
|
|
40
|
+
page={page}
|
|
41
|
+
index={index}
|
|
42
|
+
active={index === currentPageIndex}
|
|
43
|
+
onClick={() => onSelectPage(index, { behavior: "smooth" })}
|
|
44
|
+
pageWidthPx={pageWidthPx}
|
|
45
|
+
pageHeightPx={pageHeightPx}
|
|
46
|
+
aspectRatio={aspectRatio}
|
|
47
|
+
/>
|
|
48
|
+
</li>
|
|
49
|
+
))}
|
|
50
|
+
</ul>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ThumbnailCard({
|
|
55
|
+
page,
|
|
56
|
+
index,
|
|
57
|
+
active,
|
|
58
|
+
onClick,
|
|
59
|
+
pageWidthPx,
|
|
60
|
+
pageHeightPx,
|
|
61
|
+
aspectRatio,
|
|
62
|
+
}: {
|
|
63
|
+
page: HtmlPageBlock;
|
|
64
|
+
index: number;
|
|
65
|
+
active: boolean;
|
|
66
|
+
onClick: () => void;
|
|
67
|
+
pageWidthPx: number;
|
|
68
|
+
pageHeightPx: number;
|
|
69
|
+
aspectRatio: string;
|
|
70
|
+
}) {
|
|
71
|
+
const surfaceRef = useRef<HTMLDivElement>(null);
|
|
72
|
+
const [scale, setScale] = useState<number | null>(null);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const el = surfaceRef.current;
|
|
76
|
+
if (!el) return;
|
|
77
|
+
const update = () => {
|
|
78
|
+
const w = el.clientWidth;
|
|
79
|
+
const h = el.clientHeight;
|
|
80
|
+
if (w > 0 && h > 0) setScale(Math.min(w / pageWidthPx, h / pageHeightPx));
|
|
81
|
+
};
|
|
82
|
+
update();
|
|
83
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
84
|
+
const ro = new ResizeObserver(update);
|
|
85
|
+
ro.observe(el);
|
|
86
|
+
return () => ro.disconnect();
|
|
87
|
+
}, [pageWidthPx, pageHeightPx]);
|
|
88
|
+
|
|
89
|
+
const className = `openpress-thumb-card${active ? " is-active" : ""}`;
|
|
90
|
+
// Wrap the page HTML using the same class structure as the main
|
|
91
|
+
// reader (`.openpress-html-page > .openpress-html-page__html`) so
|
|
92
|
+
// section-scoped CSS that targets those classes still applies in
|
|
93
|
+
// the miniature.
|
|
94
|
+
const pageClass = page.className
|
|
95
|
+
? `openpress-html-page ${page.className}`
|
|
96
|
+
: "openpress-html-page";
|
|
97
|
+
const scaledWidth = scale ? pageWidthPx * scale : 0;
|
|
98
|
+
const scaledHeight = scale ? pageHeightPx * scale : 0;
|
|
99
|
+
const frameStyle: CSSProperties = {
|
|
100
|
+
width: `${scaledWidth}px`,
|
|
101
|
+
height: `${scaledHeight}px`,
|
|
102
|
+
position: "relative",
|
|
103
|
+
visibility: scale ? "visible" : "hidden",
|
|
104
|
+
};
|
|
105
|
+
const pageStyle: CSSProperties = {
|
|
106
|
+
"--openpress-page-width": `${pageWidthPx}px`,
|
|
107
|
+
"--openpress-page-height": `${pageHeightPx}px`,
|
|
108
|
+
width: `${pageWidthPx}px`,
|
|
109
|
+
height: `${pageHeightPx}px`,
|
|
110
|
+
transform: scale ? `scale(${scale})` : undefined,
|
|
111
|
+
transformOrigin: "top left",
|
|
112
|
+
position: "absolute",
|
|
113
|
+
top: 0,
|
|
114
|
+
left: 0,
|
|
115
|
+
} as CSSProperties;
|
|
116
|
+
const pageTitle = page.title || `Page ${index + 1}`;
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
role="button"
|
|
121
|
+
tabIndex={0}
|
|
122
|
+
className={className}
|
|
123
|
+
data-openpress-thumb-index={index}
|
|
124
|
+
aria-label={`前往第 ${index + 1} 頁:${pageTitle}`}
|
|
125
|
+
aria-current={active ? "page" : undefined}
|
|
126
|
+
onClick={onClick}
|
|
127
|
+
onKeyDown={(event) => {
|
|
128
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
onClick();
|
|
131
|
+
}
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
<div className="openpress-thumb-card__surface" ref={surfaceRef} style={{ aspectRatio }}>
|
|
135
|
+
<div className="openpress-thumb-card__frame" style={frameStyle}>
|
|
136
|
+
<div className={pageClass} style={pageStyle} data-openpress-thumb-page="true">
|
|
137
|
+
<div
|
|
138
|
+
className="openpress-html-page__html"
|
|
139
|
+
// Page HTML comes from the trusted build pipeline (same source
|
|
140
|
+
// as the main reader).
|
|
141
|
+
dangerouslySetInnerHTML={{ __html: page.html }}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<div className="openpress-thumb-card__meta">
|
|
147
|
+
<span className="openpress-thumb-card__index">{String(index + 1).padStart(2, "0")}</span>
|
|
148
|
+
<span className="openpress-thumb-card__title">{pageTitle}</span>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parsePxLength(value: string | undefined): number | null {
|
|
155
|
+
if (!value) return null;
|
|
156
|
+
const match = value.trim().match(/^([\d.]+)\s*(px|mm|cm|in)$/i);
|
|
157
|
+
if (!match) return null;
|
|
158
|
+
const n = Number(match[1]);
|
|
159
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
160
|
+
const unit = match[2].toLowerCase();
|
|
161
|
+
switch (unit) {
|
|
162
|
+
case "px": return n;
|
|
163
|
+
case "mm": return n * (96 / 25.4);
|
|
164
|
+
case "cm": return n * (96 / 2.54);
|
|
165
|
+
case "in": return n * 96;
|
|
166
|
+
default: return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
import {
|
|
2
|
-
useLayoutEffect,
|
|
3
2
|
useMemo,
|
|
4
3
|
useRef,
|
|
5
|
-
useState,
|
|
6
4
|
type CSSProperties,
|
|
7
5
|
type MouseEvent as ReactMouseEvent,
|
|
8
6
|
type RefCallback,
|
|
9
7
|
type RefObject,
|
|
10
8
|
} from "react";
|
|
11
9
|
import { BookOpen, ExternalLink, X } from "lucide-react";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
import {
|
|
11
|
+
collectBookmarkIndex,
|
|
12
|
+
createAnchorPageMap,
|
|
13
|
+
createPageObjectEntityId,
|
|
14
|
+
getProjectIdentity,
|
|
15
|
+
resolveAnchorPageIndex,
|
|
16
|
+
type DeploymentInfo,
|
|
17
|
+
type HtmlPageBlock,
|
|
18
|
+
type ReaderDocument,
|
|
19
|
+
} from "../document-model";
|
|
20
|
+
import type { InspectorState } from "../workbench/inspector";
|
|
21
|
+
import { useReaderRuntime } from "./useReaderRuntime";
|
|
22
|
+
import { Bookmarks, CurrentPagePanel } from "./ReaderNavigationPanel";
|
|
23
|
+
import type { DisplayPage } from "./readerTypes";
|
|
24
|
+
import { usePageViewportScale } from "./usePageViewportScale";
|
|
25
|
+
import type { PageLayoutMode } from "./pageViewportScaleModel";
|
|
21
26
|
|
|
22
27
|
export const PUBLIC_DRAWER_BREAKPOINT = 1185;
|
|
23
|
-
export type ViewMode = "
|
|
28
|
+
export type ViewMode = "paged";
|
|
24
29
|
export type PageInspector = Pick<InspectorState, "enabled" | "handleClick">;
|
|
25
30
|
|
|
26
|
-
const PAGED_VIEW_MIN_WIDTH = 360;
|
|
27
|
-
|
|
28
31
|
export function PublicViewer({
|
|
29
32
|
document,
|
|
30
33
|
pages,
|
|
@@ -46,6 +49,12 @@ export function PublicViewer({
|
|
|
46
49
|
pageCount: displayPages.length,
|
|
47
50
|
rightPanelBreakpoint: PUBLIC_DRAWER_BREAKPOINT,
|
|
48
51
|
});
|
|
52
|
+
usePageViewportScale({
|
|
53
|
+
stageRef: reader.stageRef,
|
|
54
|
+
pageContainerRef: sourceContainerRef,
|
|
55
|
+
pageCount: displayPages.length,
|
|
56
|
+
layoutMode: "single",
|
|
57
|
+
});
|
|
49
58
|
const currentPage = displayPages[reader.currentPageIndex];
|
|
50
59
|
const staticPdfHref = deploymentInfo.pdf;
|
|
51
60
|
const projectIdentity = getProjectIdentity(document.meta);
|
|
@@ -135,7 +144,7 @@ export function PublicViewer({
|
|
|
135
144
|
totalPageLabel={reader.totalPageLabel}
|
|
136
145
|
progressPercent={reader.progressPercent}
|
|
137
146
|
title={currentPage?.title || document.meta.title}
|
|
138
|
-
pageLabelPrefix=
|
|
147
|
+
pageLabelPrefix="頁"
|
|
139
148
|
showHeading={false}
|
|
140
149
|
showTitle={false}
|
|
141
150
|
/>
|
|
@@ -145,37 +154,8 @@ export function PublicViewer({
|
|
|
145
154
|
);
|
|
146
155
|
}
|
|
147
156
|
|
|
148
|
-
export function useViewMode() {
|
|
149
|
-
|
|
150
|
-
if (typeof window === "undefined") return true;
|
|
151
|
-
return viewportAllowsPagedMode();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
useLayoutEffect(() => {
|
|
155
|
-
if (typeof window === "undefined") return undefined;
|
|
156
|
-
|
|
157
|
-
let cancelFrame: (() => void) | null = null;
|
|
158
|
-
const sync = () => {
|
|
159
|
-
cancelFrame?.();
|
|
160
|
-
cancelFrame = scheduleBrowserFrame(() => {
|
|
161
|
-
cancelFrame = null;
|
|
162
|
-
setPagedAllowed(viewportAllowsPagedMode());
|
|
163
|
-
});
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
sync();
|
|
167
|
-
window.addEventListener("resize", sync);
|
|
168
|
-
window.visualViewport?.addEventListener("resize", sync);
|
|
169
|
-
return () => {
|
|
170
|
-
window.removeEventListener("resize", sync);
|
|
171
|
-
window.visualViewport?.removeEventListener("resize", sync);
|
|
172
|
-
cancelFrame?.();
|
|
173
|
-
};
|
|
174
|
-
}, []);
|
|
175
|
-
|
|
176
|
-
const viewMode: ViewMode = pagedAllowed ? "paged" : "reading";
|
|
177
|
-
|
|
178
|
-
return { viewMode };
|
|
157
|
+
export function useViewMode(): { viewMode: ViewMode } {
|
|
158
|
+
return { viewMode: "paged" };
|
|
179
159
|
}
|
|
180
160
|
|
|
181
161
|
export function PrintDocument({
|
|
@@ -210,11 +190,6 @@ export function PrintDocument({
|
|
|
210
190
|
);
|
|
211
191
|
}
|
|
212
192
|
|
|
213
|
-
function viewportAllowsPagedMode() {
|
|
214
|
-
if (typeof window === "undefined") return true;
|
|
215
|
-
return window.innerWidth >= PAGED_VIEW_MIN_WIDTH;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
193
|
export function PublicPage({
|
|
219
194
|
pages,
|
|
220
195
|
currentPageIndex,
|
|
@@ -224,6 +199,7 @@ export function PublicPage({
|
|
|
224
199
|
exposeSourceData = false,
|
|
225
200
|
inspector,
|
|
226
201
|
onInternalAnchorNavigate,
|
|
202
|
+
pageLayoutMode = "single",
|
|
227
203
|
}: {
|
|
228
204
|
pages: DisplayPage[];
|
|
229
205
|
currentPageIndex: number;
|
|
@@ -233,6 +209,7 @@ export function PublicPage({
|
|
|
233
209
|
exposeSourceData?: boolean;
|
|
234
210
|
inspector?: PageInspector;
|
|
235
211
|
onInternalAnchorNavigate?: (anchorId: string, pageIndex?: number) => boolean;
|
|
212
|
+
pageLayoutMode?: PageLayoutMode;
|
|
236
213
|
}) {
|
|
237
214
|
const handlePageClick = (event: ReactMouseEvent<HTMLDivElement>) => {
|
|
238
215
|
if (inspector?.enabled && inspector.handleClick(event)) return;
|
|
@@ -257,6 +234,7 @@ export function PublicPage({
|
|
|
257
234
|
className="reader-pages openpress-public-page"
|
|
258
235
|
ref={sourceContainerRef}
|
|
259
236
|
data-openpress-public-page="true"
|
|
237
|
+
data-openpress-page-layout={pageLayoutMode}
|
|
260
238
|
onClick={handlePageClick}
|
|
261
239
|
>
|
|
262
240
|
{pages.map((page) => (
|
|
@@ -265,7 +243,9 @@ export function PublicPage({
|
|
|
265
243
|
ref={registerPage(page.pageNumber - 1)}
|
|
266
244
|
id={`page-${String(page.pageNumber).padStart(2, "0")}`}
|
|
267
245
|
className="openpress-html-page"
|
|
246
|
+
data-openpress-object-id={page.frameKey ? createPageObjectEntityId(page.frameKey) : undefined}
|
|
268
247
|
data-openpress-page-index={page.pageNumber - 1}
|
|
248
|
+
data-openpress-page-spread-side={pageLayoutMode === "spread" ? ((page.pageNumber - 1) % 2 === 0 ? "left" : "right") : undefined}
|
|
269
249
|
data-openpress-active={currentPageIndex === page.pageNumber - 1 ? "true" : "false"}
|
|
270
250
|
data-source-path={exposeSourceData ? page.source?.path : undefined}
|
|
271
251
|
data-source-file={exposeSourceData ? page.source?.file : undefined}
|
package/template/core/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx}
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type CSSProperties, type MouseEvent as ReactMouseEvent } from "react";
|
|
2
|
-
import type { BookmarkItem } from "
|
|
2
|
+
import type { BookmarkItem } from "../document-model";
|
|
3
|
+
import { Panel } from "../shared";
|
|
3
4
|
|
|
4
5
|
type BookmarkSelectOptions = {
|
|
5
6
|
behavior?: ScrollBehavior;
|
|
@@ -20,7 +21,7 @@ export function Bookmarks({
|
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
if (items.length === 0) {
|
|
23
|
-
return <
|
|
24
|
+
return <Panel.Empty className="openpress-asset-empty" role="status">尚無書籤</Panel.Empty>;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
return (
|
|
@@ -103,8 +104,8 @@ export function CurrentPagePanel({
|
|
|
103
104
|
showTitle?: boolean;
|
|
104
105
|
}) {
|
|
105
106
|
return (
|
|
106
|
-
<
|
|
107
|
-
{showHeading ? <
|
|
107
|
+
<Panel.Section className="openpress-panel-section--current" aria-label="目前頁面">
|
|
108
|
+
{showHeading ? <Panel.SectionTitle className="openpress-panel-heading">目前頁面</Panel.SectionTitle> : null}
|
|
108
109
|
<div className="openpress-current-page-card">
|
|
109
110
|
<div className="openpress-current-page-card__number" aria-label="目前頁數">
|
|
110
111
|
{pageLabelPrefix ? <span className="openpress-current-page-card__prefix">{pageLabelPrefix}</span> : null}
|
|
@@ -117,6 +118,6 @@ export function CurrentPagePanel({
|
|
|
117
118
|
<span style={{ "--progress": `${progressPercent}%` } as CSSProperties} />
|
|
118
119
|
</div>
|
|
119
120
|
</div>
|
|
120
|
-
</
|
|
121
|
+
</Panel.Section>
|
|
121
122
|
);
|
|
122
123
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./PageThumbnailsPanel";
|
|
2
|
+
export * from "./PublicReaderPage";
|
|
3
|
+
export * from "./ReaderNavigationPanel";
|
|
4
|
+
export * from "./pageViewportScaleModel";
|
|
5
|
+
export * from "./readerPageRegistry";
|
|
6
|
+
export * from "./readerPageRoute";
|
|
7
|
+
export * from "./readerScroll";
|
|
8
|
+
export * from "./readerStateModel";
|
|
9
|
+
export * from "./readerTypes";
|
|
10
|
+
export * from "./usePageViewportScale";
|
|
11
|
+
export * from "./useReaderRuntime";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type PageLayoutMode = "single" | "spread";
|
|
2
|
+
|
|
3
|
+
export type PageViewportScaleMode =
|
|
4
|
+
| "fit-width"
|
|
5
|
+
| "fit-page"
|
|
6
|
+
| "scale-25"
|
|
7
|
+
| "scale-50"
|
|
8
|
+
| "scale-75"
|
|
9
|
+
| "scale-100"
|
|
10
|
+
| "scale-125"
|
|
11
|
+
| "scale-150"
|
|
12
|
+
| "scale-200";
|
|
13
|
+
|
|
14
|
+
export const PAGE_VIEWPORT_SCALE_OPTIONS: Array<{
|
|
15
|
+
value: PageViewportScaleMode;
|
|
16
|
+
label: string;
|
|
17
|
+
}> = [
|
|
18
|
+
{ value: "scale-25", label: "25%" },
|
|
19
|
+
{ value: "scale-50", label: "50%" },
|
|
20
|
+
{ value: "scale-75", label: "75%" },
|
|
21
|
+
{ value: "scale-100", label: "100%" },
|
|
22
|
+
{ value: "scale-125", label: "125%" },
|
|
23
|
+
{ value: "scale-150", label: "150%" },
|
|
24
|
+
{ value: "scale-200", label: "200%" },
|
|
25
|
+
{ value: "fit-width", label: "符合頁面寬度" },
|
|
26
|
+
{ value: "fit-page", label: "符合全開頁面" },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const MIN_PAGE_VIEWPORT_SCALE = 0.12;
|
|
30
|
+
const MAX_FIT_PAGE_VIEWPORT_SCALE = 1;
|
|
31
|
+
const MAX_FIXED_PAGE_VIEWPORT_SCALE = 2;
|
|
32
|
+
|
|
33
|
+
export function resolvePageViewportScale({
|
|
34
|
+
mode,
|
|
35
|
+
fitWidthScale,
|
|
36
|
+
fitPageScale,
|
|
37
|
+
}: {
|
|
38
|
+
mode: PageViewportScaleMode;
|
|
39
|
+
fitWidthScale: number;
|
|
40
|
+
fitPageScale: number;
|
|
41
|
+
}) {
|
|
42
|
+
if (mode === "fit-width") return clampPageViewportScale(fitWidthScale, MAX_FIT_PAGE_VIEWPORT_SCALE);
|
|
43
|
+
if (mode === "fit-page") return clampPageViewportScale(fitPageScale, MAX_FIT_PAGE_VIEWPORT_SCALE);
|
|
44
|
+
return scaleModeToFixedValue(mode);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatPageViewportScaleLabel(mode: PageViewportScaleMode, scale: number) {
|
|
48
|
+
void mode;
|
|
49
|
+
return formatPageViewportScalePercent(scale);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatPageViewportScalePercent(scale: number) {
|
|
53
|
+
return `${Math.round(clampPageViewportScale(scale, MAX_FIXED_PAGE_VIEWPORT_SCALE) * 100)}%`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatPageViewportScaleValue(scale: number) {
|
|
57
|
+
return clampPageViewportScale(scale, MAX_FIXED_PAGE_VIEWPORT_SCALE)
|
|
58
|
+
.toFixed(4)
|
|
59
|
+
.replace(/0+$/, "")
|
|
60
|
+
.replace(/\.$/, "");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function scaleModeToFixedValue(mode: PageViewportScaleMode) {
|
|
64
|
+
const match = /^scale-(\d+)$/.exec(mode);
|
|
65
|
+
if (!match) return 1;
|
|
66
|
+
return clampPageViewportScale(Number.parseInt(match[1] ?? "100", 10) / 100, MAX_FIXED_PAGE_VIEWPORT_SCALE);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function clampPageViewportScale(value: number, maxScale: number) {
|
|
70
|
+
if (!Number.isFinite(value)) return 1;
|
|
71
|
+
const safeMaxScale = Number.isFinite(maxScale) && maxScale > 0 ? maxScale : MAX_FIXED_PAGE_VIEWPORT_SCALE;
|
|
72
|
+
return Math.min(Math.max(value, MIN_PAGE_VIEWPORT_SCALE), safeMaxScale);
|
|
73
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
}
|