@open-press/core 1.1.4 → 1.2.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.
- package/engine/cli.mjs +3 -3
- package/engine/commands/_shared.mjs +89 -13
- package/engine/commands/deploy.mjs +19 -4
- package/engine/commands/image.mjs +9 -3
- package/engine/commands/pdf.mjs +4 -1
- package/engine/output/chrome-pdf.mjs +102 -0
- package/engine/output/static-server.mjs +64 -17
- package/engine/react/document-export.mjs +22 -0
- package/package.json +1 -1
- package/src/openpress/app/OpenPressApp.tsx +5 -1
- package/src/openpress/app/OpenPressRuntime.tsx +85 -6
- package/src/openpress/reader/PageThumbnailsPanel.tsx +28 -5
- package/src/openpress/reader/PublicReaderPage.tsx +163 -74
- package/src/openpress/reader/SlidePresentationPage.tsx +37 -15
- package/src/openpress/reader/SlidePublicPage.tsx +332 -0
- package/src/openpress/reader/index.ts +1 -0
- package/src/openpress/reader/pageViewportScaleModel.ts +5 -3
- package/src/openpress/reader/usePageViewportScale.ts +9 -5
- package/src/openpress/reader/usePanelState.ts +14 -5
- package/src/openpress/shared/index.ts +1 -0
- package/src/openpress/shared/staticSearch.ts +174 -0
- package/src/openpress/workbench/Workbench.tsx +61 -176
- package/src/openpress/workbench/actions/DeploymentControl.tsx +1 -1
- package/src/openpress/workbench/actions/ExportControl.tsx +267 -0
- package/src/openpress/workbench/actions/SearchControl.tsx +32 -43
- package/src/openpress/workbench/actions/index.ts +1 -1
- package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +21 -5
- package/src/openpress/workbench/hooks/useWorkbenchNavigation.ts +42 -0
- package/src/openpress/workbench/inspector/useInspectorComments.ts +6 -6
- package/src/openpress/workbench/project/ProjectEntryPanel.tsx +2 -278
- package/src/openpress/workbench/shell/WorkbenchShell.tsx +44 -18
- package/src/openpress/workbench/shell/WorkbenchToolbarActions.tsx +206 -0
- package/src/styles/openpress/app-shell.css +0 -83
- package/src/styles/openpress/print-route.css +1 -3
- package/src/styles/openpress/project-preview-panel.css +5 -783
- package/src/styles/openpress/public-viewer.css +7 -249
- package/src/styles/openpress/reader-runtime.css +0 -274
- package/src/styles/openpress/slide-presenter.css +150 -0
- package/src/styles/openpress/slide-public-viewer.css +222 -0
- package/src/styles/openpress/workbench-dialog.css +267 -0
- package/src/styles/openpress/workbench-export.css +154 -0
- package/src/styles/openpress/workbench-inline-editor.css +128 -0
- package/src/styles/openpress/workbench-panels.css +0 -88
- package/src/styles/openpress/workbench-search.css +257 -0
- package/src/styles/openpress/workbench-toolbar.css +422 -0
- package/src/styles/openpress/workbench.css +34 -1263
- package/src/styles/openpress/workspace-gallery.css +0 -5
- package/src/styles/openpress.css +7 -1
- package/vite.config.ts +66 -17
- package/src/openpress/workbench/actions/ExportImageControl.tsx +0 -96
- package/src/styles/openpress/media-workspace.css +0 -230
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useMemo, type CSSProperties } from "react";
|
|
2
|
-
import { PrintDocument, PublicViewer, SlidePresentationPage } from "../reader";
|
|
1
|
+
import { useEffect, useMemo, useState, type CSSProperties } from "react";
|
|
2
|
+
import { PrintDocument, PublicViewer, SlidePublicViewer, SlidePresentationPage } from "../reader";
|
|
3
3
|
import { isPresentationModeLocation, isPrintModeLocation, isWorkspaceModeLocation } from "../shared";
|
|
4
4
|
import { HtmlWorkbench } from "../workbench";
|
|
5
5
|
import type {
|
|
@@ -15,6 +15,12 @@ interface OpenPressRuntimeProps {
|
|
|
15
15
|
document: ReaderDocument;
|
|
16
16
|
runtimeMode?: OpenPressRuntimeMode;
|
|
17
17
|
deploymentInfo?: DeploymentInfo;
|
|
18
|
+
// Active Press slug — supplied by OpenPressApp when the active document
|
|
19
|
+
// came from a multi-Press workspace. The workbench passes this through to
|
|
20
|
+
// useDeploymentWorkbench so the local PDF export endpoint can target the
|
|
21
|
+
// right Press instead of defaulting to the first one and producing a
|
|
22
|
+
// "0 pages observed" timeout.
|
|
23
|
+
activeSlug?: string;
|
|
18
24
|
onDocumentRefresh?: () => void | Promise<void>;
|
|
19
25
|
onOpenPresentation?: (pageIndex: number) => void;
|
|
20
26
|
onExitPresentation?: (pageIndex: number) => void;
|
|
@@ -28,6 +34,7 @@ export function OpenPressRuntime({
|
|
|
28
34
|
document,
|
|
29
35
|
runtimeMode,
|
|
30
36
|
deploymentInfo = { online: false },
|
|
37
|
+
activeSlug,
|
|
31
38
|
onDocumentRefresh,
|
|
32
39
|
onOpenPresentation,
|
|
33
40
|
onExitPresentation,
|
|
@@ -35,19 +42,29 @@ export function OpenPressRuntime({
|
|
|
35
42
|
}: OpenPressRuntimeProps) {
|
|
36
43
|
const style = themeToCssVariables(document.theme);
|
|
37
44
|
const htmlPages = document.blocks.filter((block): block is HtmlPageBlock => block.kind === "htmlPage");
|
|
45
|
+
// The mode flags below all read window.location synchronously. They
|
|
46
|
+
// would otherwise stay frozen at their mount-time values when
|
|
47
|
+
// OpenPressApp re-renders us in response to a client-side URL change
|
|
48
|
+
// (e.g. /<slug>/present -> /<slug>/preview after exiting the slide
|
|
49
|
+
// presenter), so the SlidePresentationPage exits to the wrong
|
|
50
|
+
// route-driven branch (PublicViewer instead of HtmlWorkbench) and the
|
|
51
|
+
// user sees the legacy public-viewer chrome until a hard reload.
|
|
52
|
+
// Bump a version on every pathname/search change so the memos
|
|
53
|
+
// re-evaluate exactly when the URL does.
|
|
54
|
+
const routeVersion = useLocationVersion();
|
|
38
55
|
const activeRuntimeMode = useMemo<OpenPressRuntimeMode>(() => {
|
|
39
56
|
if (runtimeMode) return runtimeMode;
|
|
40
57
|
if (typeof window === "undefined") return "preview";
|
|
41
58
|
return isPresentationModeLocation(window.location) ? "present" : "preview";
|
|
42
|
-
}, [runtimeMode]);
|
|
59
|
+
}, [runtimeMode, routeVersion]);
|
|
43
60
|
const workspaceMode = useMemo(() => {
|
|
44
61
|
if (typeof window === "undefined") return false;
|
|
45
62
|
return isWorkspaceModeLocation(window.location);
|
|
46
|
-
}, []);
|
|
63
|
+
}, [routeVersion]);
|
|
47
64
|
const printMode = useMemo(() => {
|
|
48
65
|
if (typeof window === "undefined") return false;
|
|
49
66
|
return isPrintModeLocation(window.location);
|
|
50
|
-
}, []);
|
|
67
|
+
}, [routeVersion]);
|
|
51
68
|
|
|
52
69
|
if (htmlPages.length > 0) {
|
|
53
70
|
if (printMode) {
|
|
@@ -65,6 +82,20 @@ export function OpenPressRuntime({
|
|
|
65
82
|
);
|
|
66
83
|
}
|
|
67
84
|
|
|
85
|
+
if (!workspaceMode && document.meta.type === "slides") {
|
|
86
|
+
const slideDeploymentInfo = activeSlug
|
|
87
|
+
? { ...deploymentInfo, pdf: resolvePressPdfUrl(deploymentInfo.pdf, activeSlug) }
|
|
88
|
+
: deploymentInfo;
|
|
89
|
+
return (
|
|
90
|
+
<SlidePublicViewer
|
|
91
|
+
document={document}
|
|
92
|
+
pages={htmlPages}
|
|
93
|
+
style={style}
|
|
94
|
+
deploymentInfo={slideDeploymentInfo}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
68
99
|
if (!workspaceMode) {
|
|
69
100
|
return <PublicViewer document={document} pages={htmlPages} style={style} deploymentInfo={deploymentInfo} />;
|
|
70
101
|
}
|
|
@@ -74,8 +105,9 @@ export function OpenPressRuntime({
|
|
|
74
105
|
document={document}
|
|
75
106
|
pages={htmlPages}
|
|
76
107
|
style={style}
|
|
77
|
-
|
|
108
|
+
workspaceMode={workspaceMode}
|
|
78
109
|
deploymentInfo={deploymentInfo}
|
|
110
|
+
pressSlug={activeSlug ?? null}
|
|
79
111
|
onDocumentRefresh={onDocumentRefresh}
|
|
80
112
|
onOpenPresentation={onOpenPresentation}
|
|
81
113
|
onBackToWorkspace={onBackToWorkspace}
|
|
@@ -110,6 +142,53 @@ function EmptyState({ style, workspaceMode }: { style: CSSProperties; workspaceM
|
|
|
110
142
|
);
|
|
111
143
|
}
|
|
112
144
|
|
|
145
|
+
// Bump a counter whenever client-side navigation changes pathname /
|
|
146
|
+
// search / hash, so location-derived memos in OpenPressRuntime
|
|
147
|
+
// re-evaluate. popstate fires on browser back/forward; we also patch
|
|
148
|
+
// pushState / replaceState because the SPA itself calls those when
|
|
149
|
+
// the user opens a Press, exits a slide presenter, or toggles between
|
|
150
|
+
// /<slug>/preview and /<slug>/present.
|
|
151
|
+
function useLocationVersion() {
|
|
152
|
+
const [version, setVersion] = useState(0);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (typeof window === "undefined") return undefined;
|
|
155
|
+
const bump = () => setVersion((value) => value + 1);
|
|
156
|
+
window.addEventListener("popstate", bump);
|
|
157
|
+
window.addEventListener("hashchange", bump);
|
|
158
|
+
const history = window.history;
|
|
159
|
+
const originalPushState = history.pushState.bind(history);
|
|
160
|
+
const originalReplaceState = history.replaceState.bind(history);
|
|
161
|
+
history.pushState = function patchedPushState(...args) {
|
|
162
|
+
const result = originalPushState(...args);
|
|
163
|
+
bump();
|
|
164
|
+
return result;
|
|
165
|
+
} as typeof history.pushState;
|
|
166
|
+
history.replaceState = function patchedReplaceState(...args) {
|
|
167
|
+
const result = originalReplaceState(...args);
|
|
168
|
+
bump();
|
|
169
|
+
return result;
|
|
170
|
+
} as typeof history.replaceState;
|
|
171
|
+
return () => {
|
|
172
|
+
window.removeEventListener("popstate", bump);
|
|
173
|
+
window.removeEventListener("hashchange", bump);
|
|
174
|
+
history.pushState = originalPushState;
|
|
175
|
+
history.replaceState = originalReplaceState;
|
|
176
|
+
};
|
|
177
|
+
}, []);
|
|
178
|
+
return version;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function resolvePressPdfUrl(basePdfUrl: string | undefined, slug: string): string | undefined {
|
|
182
|
+
if (!basePdfUrl) return undefined;
|
|
183
|
+
const lastSlash = basePdfUrl.lastIndexOf("/");
|
|
184
|
+
const dir = lastSlash >= 0 ? basePdfUrl.slice(0, lastSlash + 1) : "";
|
|
185
|
+
const filename = lastSlash >= 0 ? basePdfUrl.slice(lastSlash + 1) : basePdfUrl;
|
|
186
|
+
const dot = filename.lastIndexOf(".");
|
|
187
|
+
const stem = dot >= 0 ? filename.slice(0, dot) : filename;
|
|
188
|
+
const ext = dot >= 0 ? filename.slice(dot) : "";
|
|
189
|
+
return `${dir}${stem}-${slug}${ext}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
113
192
|
function themeToCssVariables(theme?: Theme) {
|
|
114
193
|
const style: CSSProperties & Record<`--${string}`, string> = {
|
|
115
194
|
"--openpress-font-family": theme?.fontFamily ?? "'Noto Sans TC', 'PingFang TC', sans-serif",
|
|
@@ -13,11 +13,15 @@ export function PageThumbnails({
|
|
|
13
13
|
pages,
|
|
14
14
|
currentPageIndex,
|
|
15
15
|
onSelectPage,
|
|
16
|
+
selectedPageIndexes,
|
|
17
|
+
onTogglePage,
|
|
16
18
|
theme,
|
|
17
19
|
}: {
|
|
18
20
|
pages: HtmlPageBlock[];
|
|
19
21
|
currentPageIndex: number;
|
|
20
22
|
onSelectPage: (pageIndex: number, options?: { behavior?: ScrollBehavior }) => void;
|
|
23
|
+
selectedPageIndexes?: ReadonlySet<number>;
|
|
24
|
+
onTogglePage?: (pageIndex: number) => void;
|
|
21
25
|
theme?: Theme;
|
|
22
26
|
}) {
|
|
23
27
|
const pageWidthPx = parsePxLength(theme?.pageWidth) ?? FALLBACK_PAGE_WIDTH_PX;
|
|
@@ -40,7 +44,15 @@ export function PageThumbnails({
|
|
|
40
44
|
page={page}
|
|
41
45
|
index={index}
|
|
42
46
|
active={index === currentPageIndex}
|
|
43
|
-
|
|
47
|
+
selected={selectedPageIndexes?.has(index) ?? false}
|
|
48
|
+
selectionMode={Boolean(selectedPageIndexes && onTogglePage)}
|
|
49
|
+
onClick={() => {
|
|
50
|
+
if (selectedPageIndexes && onTogglePage) {
|
|
51
|
+
onTogglePage(index);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
onSelectPage(index, { behavior: "smooth" });
|
|
55
|
+
}}
|
|
44
56
|
pageWidthPx={pageWidthPx}
|
|
45
57
|
pageHeightPx={pageHeightPx}
|
|
46
58
|
aspectRatio={aspectRatio}
|
|
@@ -55,6 +67,8 @@ function ThumbnailCard({
|
|
|
55
67
|
page,
|
|
56
68
|
index,
|
|
57
69
|
active,
|
|
70
|
+
selected,
|
|
71
|
+
selectionMode,
|
|
58
72
|
onClick,
|
|
59
73
|
pageWidthPx,
|
|
60
74
|
pageHeightPx,
|
|
@@ -63,6 +77,8 @@ function ThumbnailCard({
|
|
|
63
77
|
page: HtmlPageBlock;
|
|
64
78
|
index: number;
|
|
65
79
|
active: boolean;
|
|
80
|
+
selected: boolean;
|
|
81
|
+
selectionMode: boolean;
|
|
66
82
|
onClick: () => void;
|
|
67
83
|
pageWidthPx: number;
|
|
68
84
|
pageHeightPx: number;
|
|
@@ -92,7 +108,7 @@ function ThumbnailCard({
|
|
|
92
108
|
cardRef.current?.scrollIntoView({ block: "nearest" });
|
|
93
109
|
}, [active]);
|
|
94
110
|
|
|
95
|
-
const className = `openpress-thumb-card${active ? " is-active" : ""}`;
|
|
111
|
+
const className = `openpress-thumb-card${active ? " is-active" : ""}${selected ? " is-selected" : ""}`;
|
|
96
112
|
// Wrap the page HTML using the same class structure as the main
|
|
97
113
|
// reader (`.openpress-html-page > .openpress-html-page__html`) so
|
|
98
114
|
// section-scoped CSS that targets those classes still applies in
|
|
@@ -124,12 +140,14 @@ function ThumbnailCard({
|
|
|
124
140
|
return (
|
|
125
141
|
<div
|
|
126
142
|
ref={cardRef}
|
|
127
|
-
role="button"
|
|
143
|
+
role={selectionMode ? "checkbox" : "button"}
|
|
128
144
|
tabIndex={0}
|
|
129
145
|
className={className}
|
|
130
146
|
data-openpress-thumb-index={index}
|
|
131
|
-
|
|
132
|
-
aria-
|
|
147
|
+
data-openpress-thumb-selected={selectionMode ? (selected ? "true" : "false") : undefined}
|
|
148
|
+
aria-label={selectionMode ? `選取第 ${index + 1} 頁:${pageTitle}` : `前往第 ${index + 1} 頁:${pageTitle}`}
|
|
149
|
+
aria-checked={selectionMode ? selected : undefined}
|
|
150
|
+
aria-current={!selectionMode && active ? "page" : undefined}
|
|
133
151
|
onClick={onClick}
|
|
134
152
|
onKeyDown={(event) => {
|
|
135
153
|
if (event.key === "Enter" || event.key === " ") {
|
|
@@ -138,6 +156,11 @@ function ThumbnailCard({
|
|
|
138
156
|
}
|
|
139
157
|
}}
|
|
140
158
|
>
|
|
159
|
+
{selectionMode ? (
|
|
160
|
+
<span className="openpress-thumb-card__check" aria-hidden="true">
|
|
161
|
+
{selected ? "✓" : ""}
|
|
162
|
+
</span>
|
|
163
|
+
) : null}
|
|
141
164
|
<div className="openpress-thumb-card__surface" ref={surfaceRef} style={{ aspectRatio }}>
|
|
142
165
|
<div className="openpress-thumb-card__frame" style={frameStyle}>
|
|
143
166
|
<div className={pageClass} style={pageStyle} data-openpress-thumb-page="true">
|
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
2
4
|
useMemo,
|
|
3
5
|
useRef,
|
|
6
|
+
useState,
|
|
4
7
|
type CSSProperties,
|
|
5
8
|
type MouseEvent as ReactMouseEvent,
|
|
6
9
|
type RefCallback,
|
|
7
10
|
type RefObject,
|
|
8
11
|
} from "react";
|
|
9
|
-
import {
|
|
12
|
+
import { ExternalLink, Ruler } from "lucide-react";
|
|
10
13
|
import {
|
|
11
14
|
collectBookmarkIndex,
|
|
12
15
|
createAnchorPageMap,
|
|
13
16
|
createPageObjectEntityId,
|
|
14
17
|
getProjectIdentity,
|
|
18
|
+
getSourceBlockMap,
|
|
15
19
|
resolveAnchorPageIndex,
|
|
16
20
|
type DeploymentInfo,
|
|
17
21
|
type HtmlPageBlock,
|
|
18
22
|
type ReaderDocument,
|
|
19
23
|
} from "../document-model";
|
|
20
24
|
import type { InspectorState } from "../workbench/inspector";
|
|
25
|
+
import { groupSourceBlocksByPath } from "../workbench/inspector";
|
|
21
26
|
import { useReaderRuntime } from "./useReaderRuntime";
|
|
22
27
|
import { Bookmarks, CurrentPagePanel } from "./ReaderNavigationPanel";
|
|
23
28
|
import type { DisplayPage } from "./readerTypes";
|
|
24
29
|
import { usePageViewportScale } from "./usePageViewportScale";
|
|
25
30
|
import type { PageLayoutMode } from "./pageViewportScaleModel";
|
|
31
|
+
import { PageZoomControl, SearchControl, type SearchControlSearcher } from "../workbench/actions";
|
|
32
|
+
import { WorkbenchShell } from "../workbench/shell";
|
|
33
|
+
import { formatPageGeometrySpec } from "../workbench/workbenchFormatters";
|
|
34
|
+
import { searchCorpus, type SearchCorpus } from "../shared";
|
|
26
35
|
|
|
27
36
|
export const PUBLIC_DRAWER_BREAKPOINT = 1185;
|
|
28
37
|
export type ViewMode = "paged";
|
|
@@ -41,29 +50,60 @@ export function PublicViewer({
|
|
|
41
50
|
}) {
|
|
42
51
|
const sourceContainerRef = useRef<HTMLDivElement | null>(null);
|
|
43
52
|
const displayPages = pages;
|
|
44
|
-
const
|
|
45
|
-
const { viewMode } = viewModeState;
|
|
53
|
+
const { viewMode } = useViewMode();
|
|
46
54
|
const bookmarks = collectBookmarkIndex(displayPages);
|
|
47
55
|
const anchorPageMap = useMemo(() => createAnchorPageMap(displayPages), [displayPages]);
|
|
48
56
|
const reader = useReaderRuntime({
|
|
49
57
|
pageCount: displayPages.length,
|
|
58
|
+
leftPanelBreakpoint: PUBLIC_DRAWER_BREAKPOINT,
|
|
50
59
|
rightPanelBreakpoint: PUBLIC_DRAWER_BREAKPOINT,
|
|
51
60
|
});
|
|
52
|
-
|
|
61
|
+
const [pageLayoutMode, setPageLayoutMode] = useState<PageLayoutMode>("single");
|
|
62
|
+
const pageViewport = usePageViewportScale({
|
|
53
63
|
stageRef: reader.stageRef,
|
|
54
64
|
pageContainerRef: sourceContainerRef,
|
|
55
65
|
pageCount: displayPages.length,
|
|
56
|
-
layoutMode:
|
|
66
|
+
layoutMode: pageLayoutMode,
|
|
57
67
|
});
|
|
58
68
|
const currentPage = displayPages[reader.currentPageIndex];
|
|
59
69
|
const staticPdfHref = deploymentInfo.pdf;
|
|
60
70
|
const projectIdentity = getProjectIdentity(document.meta);
|
|
71
|
+
const pressType = document.meta.type === "slides" ? "slides" : "pages";
|
|
72
|
+
const pageGeometry = formatPageGeometrySpec(document.theme);
|
|
73
|
+
const sourceBlocksByPath = useMemo(
|
|
74
|
+
() => groupSourceBlocksByPath(getSourceBlockMap(document)),
|
|
75
|
+
[document],
|
|
76
|
+
);
|
|
61
77
|
|
|
62
|
-
|
|
78
|
+
// Static searcher: lazy-fetch /openpress/search-corpus.json on first
|
|
79
|
+
// query, cache for subsequent searches, then run the same literal-match
|
|
80
|
+
// logic the dev endpoint uses — no backend required for deployed pages.
|
|
81
|
+
const corpusRef = useRef<SearchCorpus | null>(null);
|
|
82
|
+
const corpusFetchRef = useRef<Promise<SearchCorpus> | null>(null);
|
|
83
|
+
const staticSearcher = useCallback<SearchControlSearcher>(async ({ query, scope, signal }) => {
|
|
84
|
+
if (!corpusRef.current) {
|
|
85
|
+
if (!corpusFetchRef.current) {
|
|
86
|
+
corpusFetchRef.current = fetch("/openpress/search-corpus.json", { cache: "force-cache" })
|
|
87
|
+
.then(async (response) => {
|
|
88
|
+
if (!response.ok) throw new Error(`Failed to load search corpus (${response.status})`);
|
|
89
|
+
return (await response.json()) as SearchCorpus;
|
|
90
|
+
})
|
|
91
|
+
.catch((error) => {
|
|
92
|
+
corpusFetchRef.current = null;
|
|
93
|
+
throw error;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const corpus = await corpusFetchRef.current;
|
|
97
|
+
if (signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
98
|
+
corpusRef.current = corpus;
|
|
99
|
+
}
|
|
100
|
+
if (signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
101
|
+
return searchCorpus(corpusRef.current, { query, scope, caseSensitive: false });
|
|
102
|
+
}, []);
|
|
63
103
|
|
|
64
104
|
const selectPublicPage = (pageIndex: number, options?: { behavior?: ScrollBehavior }) => {
|
|
65
105
|
reader.setPage(pageIndex, options);
|
|
66
|
-
if (window.innerWidth < PUBLIC_DRAWER_BREAKPOINT &&
|
|
106
|
+
if (window.innerWidth < PUBLIC_DRAWER_BREAKPOINT && reader.leftPanelOpen) reader.toggleLeftPanel();
|
|
67
107
|
};
|
|
68
108
|
|
|
69
109
|
const selectPublicAnchor = (anchorId: string, pageIndex?: number) => {
|
|
@@ -73,84 +113,112 @@ export function PublicViewer({
|
|
|
73
113
|
return true;
|
|
74
114
|
};
|
|
75
115
|
|
|
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
116
|
const handleOpenStaticPdf = () => {
|
|
82
117
|
if (!staticPdfHref) return;
|
|
83
118
|
window.open(staticPdfHref, "_blank", "noopener,noreferrer");
|
|
84
119
|
};
|
|
85
120
|
|
|
86
121
|
return (
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
122
|
+
<WorkbenchShell
|
|
123
|
+
style={style}
|
|
124
|
+
viewMode={viewMode}
|
|
125
|
+
pressType={pressType}
|
|
126
|
+
inspectorMode={false}
|
|
127
|
+
leftPanelOpen={reader.leftPanelOpen}
|
|
128
|
+
rightPanelOpen={false}
|
|
129
|
+
onToggleLeftPanel={reader.toggleLeftPanel}
|
|
130
|
+
onToggleRightPanel={reader.toggleLeftPanel}
|
|
131
|
+
withRightPanel={false}
|
|
132
|
+
publicViewer
|
|
133
|
+
>
|
|
134
|
+
<WorkbenchShell.Toolbar>
|
|
135
|
+
<div className="openpress-workbench-toolbar__group" aria-label="輸出">
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
className="openpress-workbench-toolbar-action"
|
|
139
|
+
data-openpress-public-export
|
|
140
|
+
disabled={!staticPdfHref}
|
|
141
|
+
onClick={handleOpenStaticPdf}
|
|
142
|
+
title={staticPdfHref ? "開啟 PDF" : "PDF 未部署"}
|
|
143
|
+
aria-label={staticPdfHref ? "開啟 PDF" : "PDF 未部署"}
|
|
144
|
+
>
|
|
145
|
+
<ExternalLink aria-hidden="true" />
|
|
146
|
+
<span className="openpress-workbench-toolbar-action__label">
|
|
147
|
+
{staticPdfHref ? "開啟 PDF" : "PDF 未部署"}
|
|
148
|
+
</span>
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
<div className="openpress-workbench-toolbar__group openpress-workbench-toolbar__group--page" aria-label="頁面規格">
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
className="openpress-workbench-page-geometry"
|
|
155
|
+
data-openpress-page-geometry
|
|
156
|
+
title={pageGeometry.title}
|
|
157
|
+
aria-label={`頁面規格 ${pageGeometry.title}`}
|
|
158
|
+
>
|
|
159
|
+
<Ruler aria-hidden="true" />
|
|
160
|
+
<span className="openpress-workbench-page-geometry__label">{pageGeometry.label}</span>
|
|
161
|
+
<span className="openpress-workbench-page-geometry__dimensions">{pageGeometry.dimensions}</span>
|
|
162
|
+
</button>
|
|
163
|
+
<PageZoomControl
|
|
164
|
+
scaleMode={pageViewport.scaleMode}
|
|
165
|
+
scaleLabel={pageViewport.scaleLabel}
|
|
166
|
+
pageLayoutMode={pageLayoutMode}
|
|
167
|
+
onScaleModeChange={pageViewport.setScaleMode}
|
|
168
|
+
onPageLayoutModeChange={setPageLayoutMode}
|
|
169
|
+
/>
|
|
170
|
+
<SearchControl
|
|
171
|
+
sourceBlocksByPath={sourceBlocksByPath}
|
|
172
|
+
onSelectPage={selectPublicPage}
|
|
173
|
+
searcher={staticSearcher}
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
</WorkbenchShell.Toolbar>
|
|
99
177
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
registerPage={reader.registerPage}
|
|
108
|
-
onInternalAnchorNavigate={selectPublicAnchor}
|
|
109
|
-
/>
|
|
110
|
-
</main>
|
|
178
|
+
<WorkbenchShell.LeftPanel>
|
|
179
|
+
<section className="openpress-public-identity" aria-label="文件資訊">
|
|
180
|
+
<strong>
|
|
181
|
+
<span className="openpress-public-title-main">{projectIdentity.name}</span>
|
|
182
|
+
{projectIdentity.subtitle ? <span className="openpress-public-title-sub">{projectIdentity.subtitle}</span> : null}
|
|
183
|
+
</strong>
|
|
184
|
+
{projectIdentity.label ? <span>{projectIdentity.label}</span> : null}
|
|
111
185
|
</section>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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="章節書籤">
|
|
186
|
+
{bookmarks.length > 0 ? (
|
|
187
|
+
<section
|
|
188
|
+
id="openpress-bookmarks"
|
|
189
|
+
className="openpress-panel-section openpress-panel-section--bookmarks"
|
|
190
|
+
aria-label="章節書籤"
|
|
191
|
+
>
|
|
137
192
|
<nav className="reader-bookmarks" aria-label="章節導覽" data-openpress-react-bookmarks="true">
|
|
138
193
|
<div className="reader-bookmarks-rail" aria-hidden="true" />
|
|
139
194
|
<Bookmarks items={bookmarks} currentPageIndex={reader.currentPageIndex} onSelectPage={selectPublicPage} />
|
|
140
195
|
</nav>
|
|
141
196
|
</section>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
197
|
+
) : null}
|
|
198
|
+
<CurrentPagePanel
|
|
199
|
+
currentPageLabel={reader.currentPageLabel}
|
|
200
|
+
totalPageLabel={reader.totalPageLabel}
|
|
201
|
+
progressPercent={reader.progressPercent}
|
|
202
|
+
title={currentPage?.title || document.meta.title}
|
|
203
|
+
pageLabelPrefix="頁"
|
|
204
|
+
showHeading={false}
|
|
205
|
+
showTitle={false}
|
|
206
|
+
/>
|
|
207
|
+
</WorkbenchShell.LeftPanel>
|
|
208
|
+
|
|
209
|
+
<WorkbenchShell.MainContent>
|
|
210
|
+
<main className="reader-stage" tabIndex={-1} ref={reader.stageRef}>
|
|
211
|
+
<PublicPage
|
|
212
|
+
pages={displayPages}
|
|
213
|
+
currentPageIndex={reader.currentPageIndex}
|
|
214
|
+
sourceContainerRef={sourceContainerRef}
|
|
215
|
+
registerPage={reader.registerPage}
|
|
216
|
+
onInternalAnchorNavigate={selectPublicAnchor}
|
|
217
|
+
pageLayoutMode={pageLayoutMode}
|
|
150
218
|
/>
|
|
151
|
-
</
|
|
152
|
-
</
|
|
153
|
-
</
|
|
219
|
+
</main>
|
|
220
|
+
</WorkbenchShell.MainContent>
|
|
221
|
+
</WorkbenchShell>
|
|
154
222
|
);
|
|
155
223
|
}
|
|
156
224
|
|
|
@@ -171,6 +239,30 @@ export function PrintDocument({
|
|
|
171
239
|
const displayPages = pages;
|
|
172
240
|
const registerPage = () => () => undefined;
|
|
173
241
|
|
|
242
|
+
// Mirror the per-document page geometry vars onto :root so the @page
|
|
243
|
+
// rule in print-route.css can resolve them. CSS custom properties set
|
|
244
|
+
// on <main> never reach @page in any browser; without this, headless
|
|
245
|
+
// Chrome falls back to the workspace theme default (210mm × 297mm A4)
|
|
246
|
+
// and slide/social/landscape presses print onto the wrong paper.
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if (typeof document === "undefined" || typeof window === "undefined") return undefined;
|
|
249
|
+
const root = window.document.documentElement;
|
|
250
|
+
const overrides: Array<[string, string]> = [];
|
|
251
|
+
for (const [key, value] of Object.entries(style)) {
|
|
252
|
+
if (typeof key === "string" && key.startsWith("--") && typeof value === "string") {
|
|
253
|
+
overrides.push([key, value]);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const previous = overrides.map(([key]) => [key, root.style.getPropertyValue(key)] as const);
|
|
257
|
+
overrides.forEach(([key, value]) => root.style.setProperty(key, value));
|
|
258
|
+
return () => {
|
|
259
|
+
previous.forEach(([key, value]) => {
|
|
260
|
+
if (value) root.style.setProperty(key, value);
|
|
261
|
+
else root.style.removeProperty(key);
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
}, [style]);
|
|
265
|
+
|
|
174
266
|
return (
|
|
175
267
|
<main
|
|
176
268
|
className="openpress-print-document"
|
|
@@ -181,7 +273,6 @@ export function PrintDocument({
|
|
|
181
273
|
<PublicPage
|
|
182
274
|
pages={displayPages}
|
|
183
275
|
currentPageIndex={0}
|
|
184
|
-
devMode={false}
|
|
185
276
|
sourceContainerRef={sourceContainerRef}
|
|
186
277
|
registerPage={registerPage}
|
|
187
278
|
exposeSourceData
|
|
@@ -193,7 +284,6 @@ export function PrintDocument({
|
|
|
193
284
|
export function PublicPage({
|
|
194
285
|
pages,
|
|
195
286
|
currentPageIndex,
|
|
196
|
-
devMode,
|
|
197
287
|
sourceContainerRef,
|
|
198
288
|
registerPage,
|
|
199
289
|
exposeSourceData = false,
|
|
@@ -203,7 +293,6 @@ export function PublicPage({
|
|
|
203
293
|
}: {
|
|
204
294
|
pages: DisplayPage[];
|
|
205
295
|
currentPageIndex: number;
|
|
206
|
-
devMode: boolean;
|
|
207
296
|
sourceContainerRef: RefObject<HTMLDivElement | null>;
|
|
208
297
|
registerPage: (pageIndex: number) => RefCallback<HTMLElement>;
|
|
209
298
|
exposeSourceData?: boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type MouseEvent as ReactMouseEvent } from "react";
|
|
2
|
-
import { Maximize2, X } from "lucide-react";
|
|
2
|
+
import { Download, Maximize2, X } from "lucide-react";
|
|
3
3
|
import { createPageObjectEntityId } from "../document-model";
|
|
4
|
-
import type { HtmlPageBlock, ReaderDocument } from "../document-model";
|
|
4
|
+
import type { DeploymentInfo, HtmlPageBlock, ReaderDocument } from "../document-model";
|
|
5
5
|
import { pageIndexFromHash, replacePageRoute } from "./readerPageRoute";
|
|
6
6
|
import { clampReaderPageIndex, formatReaderPageNumber, normalizeReaderPageCount } from "./readerStateModel";
|
|
7
7
|
import { usePageViewportScale } from "./usePageViewportScale";
|
|
@@ -13,11 +13,13 @@ export function SlidePresentationPage({
|
|
|
13
13
|
pages,
|
|
14
14
|
style,
|
|
15
15
|
onExitPresentation,
|
|
16
|
+
deploymentInfo,
|
|
16
17
|
}: {
|
|
17
18
|
document: ReaderDocument;
|
|
18
19
|
pages: HtmlPageBlock[];
|
|
19
20
|
style: CSSProperties;
|
|
20
21
|
onExitPresentation?: (pageIndex: number) => void;
|
|
22
|
+
deploymentInfo?: DeploymentInfo;
|
|
21
23
|
}) {
|
|
22
24
|
const sourceContainerRef = useRef<HTMLDivElement | null>(null);
|
|
23
25
|
const stageRef = useRef<HTMLElement | null>(null);
|
|
@@ -35,6 +37,8 @@ export function SlidePresentationPage({
|
|
|
35
37
|
pageContainerRef: sourceContainerRef,
|
|
36
38
|
pageCount: pages.length,
|
|
37
39
|
layoutMode: "single",
|
|
40
|
+
initialScaleMode: "fit-page",
|
|
41
|
+
maxFitScale: Infinity,
|
|
38
42
|
});
|
|
39
43
|
const currentPage = pages[clampReaderPageIndex(currentPageIndex, normalizedPageCount)];
|
|
40
44
|
const currentPageLabel = formatReaderPageNumber(currentPageIndex + 1);
|
|
@@ -66,13 +70,11 @@ export function SlidePresentationPage({
|
|
|
66
70
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
67
71
|
if (isEditableTarget(event.target)) return;
|
|
68
72
|
if (event.key === "Escape") {
|
|
69
|
-
event.preventDefault();
|
|
70
73
|
const activeDocument = globalThis.document;
|
|
71
74
|
if (activeDocument.fullscreenElement && activeDocument.exitFullscreen) {
|
|
75
|
+
event.preventDefault();
|
|
72
76
|
void activeDocument.exitFullscreen();
|
|
73
|
-
return;
|
|
74
77
|
}
|
|
75
|
-
onExitPresentation?.(currentPageIndexRef.current);
|
|
76
78
|
return;
|
|
77
79
|
}
|
|
78
80
|
if (event.key === " " || event.code === "Space") {
|
|
@@ -178,6 +180,11 @@ export function SlidePresentationPage({
|
|
|
178
180
|
</section>
|
|
179
181
|
|
|
180
182
|
<div className="openpress-slide-presenter__hud" aria-label="放映控制">
|
|
183
|
+
{deploymentInfo && document.meta.title ? (
|
|
184
|
+
<span className="openpress-slide-presenter__title" aria-label="簡報標題">
|
|
185
|
+
{document.meta.title}
|
|
186
|
+
</span>
|
|
187
|
+
) : null}
|
|
181
188
|
<span
|
|
182
189
|
className="openpress-slide-presenter__progress"
|
|
183
190
|
data-openpress-present-progress
|
|
@@ -195,16 +202,31 @@ export function SlidePresentationPage({
|
|
|
195
202
|
>
|
|
196
203
|
<Maximize2 aria-hidden="true" />
|
|
197
204
|
</button>
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
{deploymentInfo?.pdf ? (
|
|
206
|
+
<a
|
|
207
|
+
href={deploymentInfo.pdf}
|
|
208
|
+
target="_blank"
|
|
209
|
+
rel="noopener noreferrer"
|
|
210
|
+
className="openpress-slide-presenter__button"
|
|
211
|
+
data-openpress-present-pdf
|
|
212
|
+
aria-label="下載 PDF"
|
|
213
|
+
title="下載 PDF"
|
|
214
|
+
>
|
|
215
|
+
<Download aria-hidden="true" />
|
|
216
|
+
</a>
|
|
217
|
+
) : null}
|
|
218
|
+
{onExitPresentation ? (
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
className="openpress-slide-presenter__button"
|
|
222
|
+
data-openpress-present-exit
|
|
223
|
+
onClick={() => onExitPresentation(currentPageIndex)}
|
|
224
|
+
aria-label="回到工作台"
|
|
225
|
+
title="回到工作台"
|
|
226
|
+
>
|
|
227
|
+
<X aria-hidden="true" />
|
|
228
|
+
</button>
|
|
229
|
+
) : null}
|
|
208
230
|
</div>
|
|
209
231
|
</main>
|
|
210
232
|
);
|