@open-press/cli 1.0.0 → 1.1.1

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,168 +0,0 @@
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,267 +0,0 @@
1
- import {
2
- useMemo,
3
- useRef,
4
- type CSSProperties,
5
- type MouseEvent as ReactMouseEvent,
6
- type RefCallback,
7
- type RefObject,
8
- } from "react";
9
- import { BookOpen, ExternalLink, X } from "lucide-react";
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";
26
-
27
- export const PUBLIC_DRAWER_BREAKPOINT = 1185;
28
- export type ViewMode = "paged";
29
- export type PageInspector = Pick<InspectorState, "enabled" | "handleClick">;
30
-
31
- export function PublicViewer({
32
- document,
33
- pages,
34
- style,
35
- deploymentInfo = { online: false },
36
- }: {
37
- document: ReaderDocument;
38
- pages: Array<HtmlPageBlock>;
39
- style: CSSProperties;
40
- deploymentInfo?: DeploymentInfo;
41
- }) {
42
- const sourceContainerRef = useRef<HTMLDivElement | null>(null);
43
- const displayPages = pages;
44
- const viewModeState = useViewMode();
45
- const { viewMode } = viewModeState;
46
- const bookmarks = collectBookmarkIndex(displayPages);
47
- const anchorPageMap = useMemo(() => createAnchorPageMap(displayPages), [displayPages]);
48
- const reader = useReaderRuntime({
49
- pageCount: displayPages.length,
50
- rightPanelBreakpoint: PUBLIC_DRAWER_BREAKPOINT,
51
- });
52
- usePageViewportScale({
53
- stageRef: reader.stageRef,
54
- pageContainerRef: sourceContainerRef,
55
- pageCount: displayPages.length,
56
- layoutMode: "single",
57
- });
58
- const currentPage = displayPages[reader.currentPageIndex];
59
- const staticPdfHref = deploymentInfo.pdf;
60
- const projectIdentity = getProjectIdentity(document.meta);
61
-
62
- const drawerOpen = reader.rightPanelOpen;
63
-
64
- const selectPublicPage = (pageIndex: number, options?: { behavior?: ScrollBehavior }) => {
65
- reader.setPage(pageIndex, options);
66
- if (window.innerWidth < PUBLIC_DRAWER_BREAKPOINT && drawerOpen) reader.toggleRightPanel();
67
- };
68
-
69
- const selectPublicAnchor = (anchorId: string, pageIndex?: number) => {
70
- const targetPageIndex = resolveAnchorPageIndex(anchorPageMap, displayPages.length, anchorId, pageIndex);
71
- if (targetPageIndex === null) return false;
72
- selectPublicPage(targetPageIndex, { behavior: "smooth" });
73
- return true;
74
- };
75
-
76
- const appClassName = [
77
- "reader-app openpress-reader-app openpress-public-viewer is-ready",
78
- drawerOpen ? "" : "is-closed-right",
79
- ].filter(Boolean).join(" ");
80
-
81
- const handleOpenStaticPdf = () => {
82
- if (!staticPdfHref) return;
83
- window.open(staticPdfHref, "_blank", "noopener,noreferrer");
84
- };
85
-
86
- return (
87
- <main className="openpress-workbench openpress-public-shell" style={style} data-openpress-public-viewer="true" aria-label={`${document.meta.title} 公開頁`}>
88
- <div
89
- className={appClassName}
90
- data-openpress-react-runtime="true"
91
- data-openpress-view-mode={viewMode}
92
- >
93
- {drawerOpen && (
94
- <div className="openpress-public-scrim" aria-hidden="true" onClick={reader.toggleRightPanel} />
95
- )}
96
- <button type="button" className="openpress-public-fab" aria-label="開啟目錄" onClick={reader.toggleRightPanel}>
97
- <BookOpen size={20} aria-hidden="true" />
98
- </button>
99
-
100
- <section className="openpress-workbench__stage openpress-public-viewer__stage" aria-label="公開文件頁面">
101
- <main className="reader-stage" tabIndex={-1} ref={reader.stageRef}>
102
- <PublicPage
103
- pages={displayPages}
104
- currentPageIndex={reader.currentPageIndex}
105
- devMode={false}
106
- sourceContainerRef={sourceContainerRef}
107
- registerPage={reader.registerPage}
108
- onInternalAnchorNavigate={selectPublicAnchor}
109
- />
110
- </main>
111
- </section>
112
-
113
- <aside className="reader-side-nav openpress-workspace-panel openpress-public-navigation" aria-label="文件導覽">
114
- <button type="button" className="openpress-public-drawer-close" aria-label="關閉目錄" onClick={reader.toggleRightPanel}>
115
- <X size={16} aria-hidden="true" />
116
- </button>
117
- <section className="openpress-public-identity" aria-label="文件資訊">
118
- <strong>
119
- <span className="openpress-public-title-main">{projectIdentity.name}</span>
120
- {projectIdentity.subtitle ? <span className="openpress-public-title-sub">{projectIdentity.subtitle}</span> : null}
121
- </strong>
122
- {projectIdentity.label ? <span>{projectIdentity.label}</span> : null}
123
- </section>
124
- <div className="openpress-public-actions" aria-label="文件操作">
125
- <button
126
- type="button"
127
- className="openpress-public-export-button"
128
- data-openpress-public-export
129
- disabled={!staticPdfHref}
130
- onClick={handleOpenStaticPdf}
131
- >
132
- <ExternalLink aria-hidden="true" />
133
- {!staticPdfHref ? "PDF 未部署" : "開啟 PDF"}
134
- </button>
135
- </div>
136
- <section id="openpress-bookmarks" className="openpress-panel-section openpress-panel-section--bookmarks" aria-label="章節書籤">
137
- <nav className="reader-bookmarks" aria-label="章節導覽" data-openpress-react-bookmarks="true">
138
- <div className="reader-bookmarks-rail" aria-hidden="true" />
139
- <Bookmarks items={bookmarks} currentPageIndex={reader.currentPageIndex} onSelectPage={selectPublicPage} />
140
- </nav>
141
- </section>
142
- <CurrentPagePanel
143
- currentPageLabel={reader.currentPageLabel}
144
- totalPageLabel={reader.totalPageLabel}
145
- progressPercent={reader.progressPercent}
146
- title={currentPage?.title || document.meta.title}
147
- pageLabelPrefix="頁"
148
- showHeading={false}
149
- showTitle={false}
150
- />
151
- </aside>
152
- </div>
153
- </main>
154
- );
155
- }
156
-
157
- export function useViewMode(): { viewMode: ViewMode } {
158
- return { viewMode: "paged" };
159
- }
160
-
161
- export function PrintDocument({
162
- document,
163
- pages,
164
- style,
165
- }: {
166
- document: ReaderDocument;
167
- pages: Array<HtmlPageBlock>;
168
- style: CSSProperties;
169
- }) {
170
- const sourceContainerRef = useRef<HTMLDivElement | null>(null);
171
- const displayPages = pages;
172
- const registerPage = () => () => undefined;
173
-
174
- return (
175
- <main
176
- className="openpress-print-document"
177
- style={style}
178
- data-openpress-print-document="true"
179
- aria-label={`${document.meta.title} PDF 輸出`}
180
- >
181
- <PublicPage
182
- pages={displayPages}
183
- currentPageIndex={0}
184
- devMode={false}
185
- sourceContainerRef={sourceContainerRef}
186
- registerPage={registerPage}
187
- exposeSourceData
188
- />
189
- </main>
190
- );
191
- }
192
-
193
- export function PublicPage({
194
- pages,
195
- currentPageIndex,
196
- devMode,
197
- sourceContainerRef,
198
- registerPage,
199
- exposeSourceData = false,
200
- inspector,
201
- onInternalAnchorNavigate,
202
- pageLayoutMode = "single",
203
- }: {
204
- pages: DisplayPage[];
205
- currentPageIndex: number;
206
- devMode: boolean;
207
- sourceContainerRef: RefObject<HTMLDivElement | null>;
208
- registerPage: (pageIndex: number) => RefCallback<HTMLElement>;
209
- exposeSourceData?: boolean;
210
- inspector?: PageInspector;
211
- onInternalAnchorNavigate?: (anchorId: string, pageIndex?: number) => boolean;
212
- pageLayoutMode?: PageLayoutMode;
213
- }) {
214
- const handlePageClick = (event: ReactMouseEvent<HTMLDivElement>) => {
215
- if (inspector?.enabled && inspector.handleClick(event)) return;
216
- if (!onInternalAnchorNavigate || event.defaultPrevented || event.button !== 0) return;
217
- if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;
218
- if (!(event.target instanceof Element)) return;
219
-
220
- const link = event.target.closest<HTMLAnchorElement>('a[href^="#"]');
221
- if (!link) return;
222
-
223
- const href = link.getAttribute("href") ?? "";
224
- const anchorId = link.dataset.openpressAnchor || safeDecodeAnchor(href.slice(1));
225
- if (!anchorId) return;
226
-
227
- const pageIndex = Number.parseInt(link.dataset.openpressTargetPageIndex ?? "", 10);
228
- const handled = onInternalAnchorNavigate(anchorId, Number.isFinite(pageIndex) ? pageIndex : undefined);
229
- if (handled) event.preventDefault();
230
- };
231
-
232
- return (
233
- <div
234
- className="reader-pages openpress-public-page"
235
- ref={sourceContainerRef}
236
- data-openpress-public-page="true"
237
- data-openpress-page-layout={pageLayoutMode}
238
- onClick={handlePageClick}
239
- >
240
- {pages.map((page) => (
241
- <div
242
- key={page.id}
243
- ref={registerPage(page.pageNumber - 1)}
244
- id={`page-${String(page.pageNumber).padStart(2, "0")}`}
245
- className="openpress-html-page"
246
- data-openpress-object-id={page.frameKey ? createPageObjectEntityId(page.frameKey) : undefined}
247
- data-openpress-page-index={page.pageNumber - 1}
248
- data-openpress-page-spread-side={pageLayoutMode === "spread" ? ((page.pageNumber - 1) % 2 === 0 ? "left" : "right") : undefined}
249
- data-openpress-active={currentPageIndex === page.pageNumber - 1 ? "true" : "false"}
250
- data-source-path={exposeSourceData ? page.source?.path : undefined}
251
- data-source-file={exposeSourceData ? page.source?.file : undefined}
252
- >
253
- <div className="openpress-html-page__html" dangerouslySetInnerHTML={{ __html: page.html }} />
254
- </div>
255
- ))}
256
- </div>
257
- );
258
- }
259
-
260
- function safeDecodeAnchor(value: string) {
261
- if (!value) return "";
262
- try {
263
- return decodeURIComponent(value);
264
- } catch {
265
- return value;
266
- }
267
- }
@@ -1,123 +0,0 @@
1
- import { type CSSProperties, type MouseEvent as ReactMouseEvent } from "react";
2
- import type { BookmarkItem } from "../document-model";
3
- import { Panel } from "../shared";
4
-
5
- type BookmarkSelectOptions = {
6
- behavior?: ScrollBehavior;
7
- };
8
-
9
- export function Bookmarks({
10
- items,
11
- currentPageIndex,
12
- onSelectPage,
13
- }: {
14
- items: BookmarkItem[];
15
- currentPageIndex: number;
16
- onSelectPage: (pageIndex: number, options?: BookmarkSelectOptions) => void;
17
- }) {
18
- const goToPage = (event: ReactMouseEvent<HTMLButtonElement>, pageIndex: number) => {
19
- event.preventDefault();
20
- onSelectPage(pageIndex, { behavior: "smooth" });
21
- };
22
-
23
- if (items.length === 0) {
24
- return <Panel.Empty className="openpress-asset-empty" role="status">尚無書籤</Panel.Empty>;
25
- }
26
-
27
- return (
28
- <>
29
- {items.map((item, index) => {
30
- const groupActive = currentPageIndex >= item.pageIndex && currentPageIndex <= item.endPageIndex;
31
- const activeSub = item.subs.find((sub) => currentPageIndex >= sub.pageIndex && currentPageIndex <= sub.endPageIndex);
32
- const h2SelfActive = groupActive && !activeSub;
33
- const itemLabel = item.label ?? String(index + 1).padStart(2, "0");
34
- return (
35
- <div className={`bookmark-group${groupActive ? " is-open" : ""}`} key={item.id}>
36
- <button
37
- type="button"
38
- className={`bookmark-item bookmark-h2${h2SelfActive ? " is-active" : ""}`}
39
- data-openpress-page-index={item.pageIndex}
40
- onClick={(event) => goToPage(event, item.pageIndex)}
41
- >
42
- <span className="bookmark-index">{itemLabel}</span>
43
- <span className="bookmark-title">{item.title}</span>
44
- </button>
45
- <div className="bookmark-subs">
46
- {item.subs.map((sub, subIndex) => {
47
- const subActive = currentPageIndex >= sub.pageIndex && currentPageIndex <= sub.endPageIndex;
48
- const activeTopic = sub.subs.find((topic) => currentPageIndex >= topic.pageIndex && currentPageIndex <= topic.endPageIndex);
49
- const subSelfActive = subActive && !activeTopic;
50
- const subLabel = sub.label ?? `${itemLabel}.${subIndex + 1}`;
51
- return (
52
- <div className="bookmark-subgroup" key={sub.id}>
53
- <button
54
- type="button"
55
- className={`bookmark-item bookmark-h3${subSelfActive ? " is-active" : ""}`}
56
- data-openpress-page-index={sub.pageIndex}
57
- onClick={(event) => goToPage(event, sub.pageIndex)}
58
- >
59
- <span className="bookmark-index">{subLabel}</span>
60
- <span className="bookmark-title">{sub.title}</span>
61
- </button>
62
- {sub.subs.map((topic, topicIndex) => {
63
- const topicActive = currentPageIndex >= topic.pageIndex && currentPageIndex <= topic.endPageIndex;
64
- const topicLabel = topic.label ?? `${subLabel}.${topicIndex + 1}`;
65
- return (
66
- <button
67
- type="button"
68
- className={`bookmark-item bookmark-h4${topicActive ? " is-active" : ""}`}
69
- data-openpress-page-index={topic.pageIndex}
70
- onClick={(event) => goToPage(event, topic.pageIndex)}
71
- key={topic.id}
72
- >
73
- <span className="bookmark-index">{topicLabel}</span>
74
- <span className="bookmark-title">{topic.title}</span>
75
- </button>
76
- );
77
- })}
78
- </div>
79
- );
80
- })}
81
- </div>
82
- </div>
83
- );
84
- })}
85
- </>
86
- );
87
- }
88
-
89
- export function CurrentPagePanel({
90
- currentPageLabel,
91
- totalPageLabel,
92
- progressPercent,
93
- title,
94
- pageLabelPrefix,
95
- showHeading = true,
96
- showTitle = true,
97
- }: {
98
- currentPageLabel: string;
99
- totalPageLabel: string;
100
- progressPercent: number;
101
- title: string;
102
- pageLabelPrefix?: string;
103
- showHeading?: boolean;
104
- showTitle?: boolean;
105
- }) {
106
- return (
107
- <Panel.Section className="openpress-panel-section--current" aria-label="目前頁面">
108
- {showHeading ? <Panel.SectionTitle className="openpress-panel-heading">目前頁面</Panel.SectionTitle> : null}
109
- <div className="openpress-current-page-card">
110
- <div className="openpress-current-page-card__number" aria-label="目前頁數">
111
- {pageLabelPrefix ? <span className="openpress-current-page-card__prefix">{pageLabelPrefix}</span> : null}
112
- <span data-openpress-current-page>{currentPageLabel}</span>
113
- <span className="sep">/</span>
114
- <span data-openpress-total-pages>{totalPageLabel}</span>
115
- </div>
116
- {showTitle ? <div className="openpress-current-page-card__title">{title}</div> : null}
117
- <div className="openpress-current-page-card__progress" aria-hidden="true">
118
- <span style={{ "--progress": `${progressPercent}%` } as CSSProperties} />
119
- </div>
120
- </div>
121
- </Panel.Section>
122
- );
123
- }
@@ -1,11 +0,0 @@
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";
@@ -1,73 +0,0 @@
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
- }
@@ -1,41 +0,0 @@
1
- export interface ReaderPageRegistry<TNode> {
2
- refs: Array<TNode | null>;
3
- registerPage: (pageIndex: number) => (node: TNode | null) => void;
4
- trim: (pageCount: number) => void;
5
- }
6
-
7
- export function createReaderPageRegistry<TNode = HTMLElement>(
8
- onChange: (version: number) => void,
9
- ): ReaderPageRegistry<TNode> {
10
- const refs: Array<TNode | null> = [];
11
- const callbacks = new Map<number, (node: TNode | null) => void>();
12
- let version = 0;
13
-
14
- const bump = () => {
15
- version += 1;
16
- onChange(version);
17
- };
18
-
19
- return {
20
- refs,
21
- registerPage(pageIndex: number) {
22
- const existing = callbacks.get(pageIndex);
23
- if (existing) return existing;
24
-
25
- const callback = (node: TNode | null) => {
26
- if (refs[pageIndex] === node) return;
27
- refs[pageIndex] = node;
28
- bump();
29
- };
30
-
31
- callbacks.set(pageIndex, callback);
32
- return callback;
33
- },
34
- trim(pageCount: number) {
35
- refs.length = Math.max(pageCount, 0);
36
- for (const pageIndex of callbacks.keys()) {
37
- if (pageIndex >= pageCount) callbacks.delete(pageIndex);
38
- }
39
- },
40
- };
41
- }