@open-press/core 1.2.0 → 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 +1 -1
- package/engine/commands/_shared.mjs +10 -5
- package/engine/commands/deploy.mjs +19 -4
- package/engine/output/static-server.mjs +16 -9
- package/package.json +1 -1
- package/src/openpress/app/OpenPressApp.tsx +4 -1
- package/src/openpress/app/OpenPressRuntime.tsx +26 -1
- package/src/openpress/reader/PageThumbnailsPanel.tsx +28 -5
- package/src/openpress/reader/SlidePresentationPage.tsx +36 -19
- 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/workbench/Workbench.tsx +46 -164
- package/src/openpress/workbench/actions/DeploymentControl.tsx +1 -1
- package/src/openpress/workbench/actions/ExportControl.tsx +267 -0
- package/src/openpress/workbench/actions/index.ts +1 -1
- package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +7 -2
- package/src/openpress/workbench/hooks/useWorkbenchNavigation.ts +42 -0
- package/src/openpress/workbench/project/ProjectEntryPanel.tsx +2 -278
- 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 +16 -9
- package/src/openpress/workbench/actions/ExportImageControl.tsx +0 -96
- package/src/styles/openpress/media-workspace.css +0 -230
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type CSSProperties,
|
|
8
|
+
type MouseEvent as ReactMouseEvent,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { ChevronLeft, ChevronRight, Download, Maximize2, Minimize2, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
|
11
|
+
import { createPageObjectEntityId } from "../document-model";
|
|
12
|
+
import type { DeploymentInfo, HtmlPageBlock, ReaderDocument } from "../document-model";
|
|
13
|
+
import { pageIndexFromHash, replacePageRoute } from "./readerPageRoute";
|
|
14
|
+
import { clampReaderPageIndex, formatReaderPageNumber, normalizeReaderPageCount } from "./readerStateModel";
|
|
15
|
+
import { usePageViewportScale } from "./usePageViewportScale";
|
|
16
|
+
import { PageThumbnails } from "./PageThumbnailsPanel";
|
|
17
|
+
|
|
18
|
+
type SlideUiMode = "chrome" | "immersive";
|
|
19
|
+
|
|
20
|
+
export function SlidePublicViewer({
|
|
21
|
+
document,
|
|
22
|
+
pages,
|
|
23
|
+
style,
|
|
24
|
+
deploymentInfo,
|
|
25
|
+
}: {
|
|
26
|
+
document: ReaderDocument;
|
|
27
|
+
pages: HtmlPageBlock[];
|
|
28
|
+
style: CSSProperties;
|
|
29
|
+
deploymentInfo?: DeploymentInfo;
|
|
30
|
+
}) {
|
|
31
|
+
const stageRef = useRef<HTMLElement | null>(null);
|
|
32
|
+
const sourceContainerRef = useRef<HTMLDivElement | null>(null);
|
|
33
|
+
const currentPageIndexRef = useRef(0);
|
|
34
|
+
const normalizedPageCount = normalizeReaderPageCount(pages.length);
|
|
35
|
+
|
|
36
|
+
const [currentPageIndex, setCurrentPageIndex] = useState(() => {
|
|
37
|
+
if (typeof window === "undefined") return 0;
|
|
38
|
+
return pageIndexFromHash(window.location.hash, normalizedPageCount) ?? 0;
|
|
39
|
+
});
|
|
40
|
+
const [uiMode, setUiMode] = useState<SlideUiMode>("chrome");
|
|
41
|
+
const [thumbPanelOpen, setThumbPanelOpen] = useState(true);
|
|
42
|
+
|
|
43
|
+
currentPageIndexRef.current = currentPageIndex;
|
|
44
|
+
|
|
45
|
+
const currentPage = pages[clampReaderPageIndex(currentPageIndex, normalizedPageCount)];
|
|
46
|
+
const currentPageLabel = formatReaderPageNumber(currentPageIndex + 1);
|
|
47
|
+
const totalPageLabel = formatReaderPageNumber(normalizedPageCount);
|
|
48
|
+
const pageHtml = useMemo(() => currentPage?.html ?? "", [currentPage?.html]);
|
|
49
|
+
|
|
50
|
+
const pageViewport = usePageViewportScale({
|
|
51
|
+
stageRef,
|
|
52
|
+
pageContainerRef: sourceContainerRef,
|
|
53
|
+
pageCount: pages.length,
|
|
54
|
+
layoutMode: "single",
|
|
55
|
+
initialScaleMode: "fit-width",
|
|
56
|
+
maxFitScale: Infinity,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const setPage = useCallback(
|
|
60
|
+
(pageIndex: number) => {
|
|
61
|
+
const target = clampReaderPageIndex(pageIndex, normalizedPageCount);
|
|
62
|
+
setCurrentPageIndex(target);
|
|
63
|
+
replacePageRoute(target);
|
|
64
|
+
},
|
|
65
|
+
[normalizedPageCount],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Clamp on page count change
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
setCurrentPageIndex((idx) => clampReaderPageIndex(idx, normalizedPageCount));
|
|
71
|
+
}, [normalizedPageCount]);
|
|
72
|
+
|
|
73
|
+
// Hash sync
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const sync = () => {
|
|
76
|
+
const fromHash = pageIndexFromHash(window.location.hash, normalizedPageCount);
|
|
77
|
+
if (fromHash !== null) setCurrentPageIndex(fromHash);
|
|
78
|
+
};
|
|
79
|
+
sync();
|
|
80
|
+
window.addEventListener("hashchange", sync);
|
|
81
|
+
return () => window.removeEventListener("hashchange", sync);
|
|
82
|
+
}, [normalizedPageCount]);
|
|
83
|
+
|
|
84
|
+
// Auto-enter immersive when ?fullscreen=1 is in the URL (e.g., launched from workbench play button)
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!shouldStartImmersive()) return;
|
|
87
|
+
setUiMode("immersive");
|
|
88
|
+
const root = globalThis.document.documentElement;
|
|
89
|
+
if (root?.requestFullscreen) {
|
|
90
|
+
void root.requestFullscreen().catch(() => {
|
|
91
|
+
// Fullscreen rejected — keep immersive CSS only.
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
// Fullscreen change
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
const handler = () => {
|
|
99
|
+
setUiMode(globalThis.document.fullscreenElement ? "immersive" : "chrome");
|
|
100
|
+
};
|
|
101
|
+
globalThis.document.addEventListener("fullscreenchange", handler);
|
|
102
|
+
return () => globalThis.document.removeEventListener("fullscreenchange", handler);
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
// Keyboard navigation
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
108
|
+
if (isEditableTarget(event.target)) return;
|
|
109
|
+
|
|
110
|
+
if (event.key === "Escape") {
|
|
111
|
+
const activeDoc = globalThis.document;
|
|
112
|
+
if (activeDoc.fullscreenElement && activeDoc.exitFullscreen) {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
void activeDoc.exitFullscreen();
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (event.key === "f" || event.key === "F") {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
enterImmersive();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (event.key === " " || event.code === "Space" || event.key === "ArrowRight" || event.key === "PageDown") {
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
setPage(currentPageIndexRef.current + 1);
|
|
128
|
+
} else if (event.key === "ArrowLeft" || event.key === "PageUp") {
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
setPage(currentPageIndexRef.current - 1);
|
|
131
|
+
} else if (event.key === "Home") {
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
setPage(0);
|
|
134
|
+
} else if (event.key === "End") {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
setPage(normalizedPageCount - 1);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
141
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
142
|
+
}, [normalizedPageCount, setPage]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
143
|
+
|
|
144
|
+
const enterImmersive = () => {
|
|
145
|
+
setUiMode("immersive");
|
|
146
|
+
const root = globalThis.document.documentElement;
|
|
147
|
+
if (root?.requestFullscreen) {
|
|
148
|
+
void root.requestFullscreen().catch(() => {
|
|
149
|
+
// Fullscreen rejected (e.g. gesture policy) — keep immersive CSS only.
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const exitImmersive = () => {
|
|
155
|
+
const activeDoc = globalThis.document;
|
|
156
|
+
if (activeDoc.fullscreenElement && activeDoc.exitFullscreen) {
|
|
157
|
+
void activeDoc.exitFullscreen();
|
|
158
|
+
} else {
|
|
159
|
+
setUiMode("chrome");
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const handleStageClick = (event: ReactMouseEvent<HTMLElement>) => {
|
|
164
|
+
if (uiMode !== "immersive") return;
|
|
165
|
+
if (event.defaultPrevented) return;
|
|
166
|
+
if (!(event.target instanceof Element)) return;
|
|
167
|
+
if (event.target.closest("a, button, input, textarea, select, [contenteditable]")) return;
|
|
168
|
+
setPage(currentPageIndexRef.current + 1);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const LeftIcon = thumbPanelOpen ? PanelLeftClose : PanelLeftOpen;
|
|
172
|
+
const leftLabel = thumbPanelOpen ? "收合縮圖面板" : "展開縮圖面板";
|
|
173
|
+
const pdfHref = deploymentInfo?.pdf;
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<main
|
|
177
|
+
className="openpress-workbench openpress-reader-app openpress-slide-public"
|
|
178
|
+
style={style}
|
|
179
|
+
data-openpress-react-runtime="true"
|
|
180
|
+
data-openpress-view-mode="paged"
|
|
181
|
+
data-openpress-press-type="slides"
|
|
182
|
+
data-openpress-presentation-mode={uiMode === "immersive" ? "on" : "off"}
|
|
183
|
+
data-openpress-present-ui={uiMode}
|
|
184
|
+
aria-label={`${document.meta.title} 投影片瀏覽`}
|
|
185
|
+
>
|
|
186
|
+
{/* Top toolbar — chrome mode only */}
|
|
187
|
+
<header
|
|
188
|
+
className="openpress-workbench-toolbar openpress-slide-public__toolbar"
|
|
189
|
+
role="toolbar"
|
|
190
|
+
aria-label="投影片操作"
|
|
191
|
+
data-openpress-slide-public-toolbar
|
|
192
|
+
>
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
className="openpress-workbench-toolbar-panel-toggle"
|
|
196
|
+
aria-label={leftLabel}
|
|
197
|
+
title={leftLabel}
|
|
198
|
+
onClick={() => setThumbPanelOpen((v) => !v)}
|
|
199
|
+
>
|
|
200
|
+
<LeftIcon aria-hidden="true" />
|
|
201
|
+
</button>
|
|
202
|
+
|
|
203
|
+
<div className="openpress-slide-public__nav" aria-label="翻頁">
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
className="openpress-workbench-toolbar-action openpress-slide-public__nav-btn"
|
|
207
|
+
onClick={() => setPage(currentPageIndex - 1)}
|
|
208
|
+
disabled={currentPageIndex === 0}
|
|
209
|
+
aria-label="上一頁"
|
|
210
|
+
title="上一頁"
|
|
211
|
+
>
|
|
212
|
+
<ChevronLeft aria-hidden="true" />
|
|
213
|
+
</button>
|
|
214
|
+
<span className="openpress-slide-public__counter" aria-live="polite" aria-label={`第 ${currentPageLabel} 頁,共 ${totalPageLabel} 頁`}>
|
|
215
|
+
{currentPageLabel} / {totalPageLabel}
|
|
216
|
+
</span>
|
|
217
|
+
<button
|
|
218
|
+
type="button"
|
|
219
|
+
className="openpress-workbench-toolbar-action openpress-slide-public__nav-btn"
|
|
220
|
+
onClick={() => setPage(currentPageIndex + 1)}
|
|
221
|
+
disabled={currentPageIndex >= normalizedPageCount - 1}
|
|
222
|
+
aria-label="下一頁"
|
|
223
|
+
title="下一頁"
|
|
224
|
+
>
|
|
225
|
+
<ChevronRight aria-hidden="true" />
|
|
226
|
+
</button>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<div className="openpress-workbench-toolbar__content" />
|
|
230
|
+
|
|
231
|
+
<div className="openpress-workbench-toolbar__group" aria-label="視圖">
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
className="openpress-workbench-toolbar-action"
|
|
235
|
+
onClick={enterImmersive}
|
|
236
|
+
aria-label="進入全螢幕放映"
|
|
237
|
+
title="全螢幕放映 (F)"
|
|
238
|
+
>
|
|
239
|
+
<Maximize2 aria-hidden="true" />
|
|
240
|
+
</button>
|
|
241
|
+
{pdfHref ? (
|
|
242
|
+
<a
|
|
243
|
+
href={pdfHref}
|
|
244
|
+
target="_blank"
|
|
245
|
+
rel="noopener noreferrer"
|
|
246
|
+
className="openpress-workbench-toolbar-action"
|
|
247
|
+
aria-label="下載 PDF"
|
|
248
|
+
title="下載 PDF"
|
|
249
|
+
>
|
|
250
|
+
<Download aria-hidden="true" />
|
|
251
|
+
</a>
|
|
252
|
+
) : null}
|
|
253
|
+
</div>
|
|
254
|
+
</header>
|
|
255
|
+
|
|
256
|
+
{/* Body: thumb panel + stage */}
|
|
257
|
+
<div className="openpress-slide-public__body">
|
|
258
|
+
{/* Thumbnail panel */}
|
|
259
|
+
<aside
|
|
260
|
+
className={`openpress-slide-public__thumbs${thumbPanelOpen ? "" : " is-closed"}`}
|
|
261
|
+
aria-label="投影片縮圖"
|
|
262
|
+
data-openpress-slide-public-thumbs
|
|
263
|
+
>
|
|
264
|
+
<PageThumbnails
|
|
265
|
+
pages={pages}
|
|
266
|
+
currentPageIndex={currentPageIndex}
|
|
267
|
+
onSelectPage={setPage}
|
|
268
|
+
theme={document.theme}
|
|
269
|
+
/>
|
|
270
|
+
</aside>
|
|
271
|
+
|
|
272
|
+
{/* Main slide stage */}
|
|
273
|
+
<section
|
|
274
|
+
className="openpress-slide-public__stage"
|
|
275
|
+
aria-label="投影片檢視區"
|
|
276
|
+
onClick={handleStageClick}
|
|
277
|
+
ref={stageRef}
|
|
278
|
+
>
|
|
279
|
+
<div
|
|
280
|
+
className="reader-pages openpress-public-page openpress-slide-public__pages"
|
|
281
|
+
ref={sourceContainerRef}
|
|
282
|
+
data-openpress-public-page="true"
|
|
283
|
+
data-openpress-page-layout="single"
|
|
284
|
+
>
|
|
285
|
+
{currentPage ? (
|
|
286
|
+
<div
|
|
287
|
+
key={currentPage.id}
|
|
288
|
+
id={`page-${String(currentPage.pageNumber).padStart(2, "0")}`}
|
|
289
|
+
className="openpress-html-page"
|
|
290
|
+
data-openpress-object-id={currentPage.frameKey ? createPageObjectEntityId(currentPage.frameKey) : undefined}
|
|
291
|
+
data-openpress-page-index={currentPage.pageNumber - 1}
|
|
292
|
+
data-openpress-active="true"
|
|
293
|
+
>
|
|
294
|
+
<div className="openpress-html-page__html" dangerouslySetInnerHTML={{ __html: pageHtml }} />
|
|
295
|
+
</div>
|
|
296
|
+
) : null}
|
|
297
|
+
</div>
|
|
298
|
+
</section>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{/* Immersive mini HUD — fullscreen mode only */}
|
|
302
|
+
<div
|
|
303
|
+
className="openpress-slide-public__mini-hud"
|
|
304
|
+
aria-label="放映控制"
|
|
305
|
+
data-openpress-present-scale={pageViewport.scaleMode}
|
|
306
|
+
>
|
|
307
|
+
<span className="openpress-slide-public__mini-counter">
|
|
308
|
+
{currentPageLabel} / {totalPageLabel}
|
|
309
|
+
</span>
|
|
310
|
+
<button
|
|
311
|
+
type="button"
|
|
312
|
+
className="openpress-slide-public__mini-btn"
|
|
313
|
+
onClick={exitImmersive}
|
|
314
|
+
aria-label="離開全螢幕"
|
|
315
|
+
title="離開全螢幕 (Esc)"
|
|
316
|
+
>
|
|
317
|
+
<Minimize2 aria-hidden="true" />
|
|
318
|
+
</button>
|
|
319
|
+
</div>
|
|
320
|
+
</main>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isEditableTarget(target: EventTarget | null) {
|
|
325
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
326
|
+
return Boolean(target.closest("input, textarea, select, button, [contenteditable]"));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function shouldStartImmersive() {
|
|
330
|
+
if (typeof window === "undefined") return false;
|
|
331
|
+
return new URLSearchParams(window.location.search).get("fullscreen") === "1";
|
|
332
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./PageThumbnailsPanel";
|
|
2
2
|
export * from "./PublicReaderPage";
|
|
3
3
|
export * from "./ReaderNavigationPanel";
|
|
4
|
+
export * from "./SlidePublicPage";
|
|
4
5
|
export * from "./SlidePresentationPage";
|
|
5
6
|
export * from "./pageViewportScaleModel";
|
|
6
7
|
export * from "./readerPageRegistry";
|
|
@@ -34,13 +34,15 @@ export function resolvePageViewportScale({
|
|
|
34
34
|
mode,
|
|
35
35
|
fitWidthScale,
|
|
36
36
|
fitPageScale,
|
|
37
|
+
maxFitScale = MAX_FIT_PAGE_VIEWPORT_SCALE,
|
|
37
38
|
}: {
|
|
38
39
|
mode: PageViewportScaleMode;
|
|
39
40
|
fitWidthScale: number;
|
|
40
41
|
fitPageScale: number;
|
|
42
|
+
maxFitScale?: number;
|
|
41
43
|
}) {
|
|
42
|
-
if (mode === "fit-width") return clampPageViewportScale(fitWidthScale,
|
|
43
|
-
if (mode === "fit-page") return clampPageViewportScale(fitPageScale,
|
|
44
|
+
if (mode === "fit-width") return clampPageViewportScale(fitWidthScale, maxFitScale);
|
|
45
|
+
if (mode === "fit-page") return clampPageViewportScale(fitPageScale, maxFitScale);
|
|
44
46
|
return scaleModeToFixedValue(mode);
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -68,6 +70,6 @@ function scaleModeToFixedValue(mode: PageViewportScaleMode) {
|
|
|
68
70
|
|
|
69
71
|
function clampPageViewportScale(value: number, maxScale: number) {
|
|
70
72
|
if (!Number.isFinite(value)) return 1;
|
|
71
|
-
const safeMaxScale =
|
|
73
|
+
const safeMaxScale = maxScale > 0 ? maxScale : MAX_FIXED_PAGE_VIEWPORT_SCALE;
|
|
72
74
|
return Math.min(Math.max(value, MIN_PAGE_VIEWPORT_SCALE), safeMaxScale);
|
|
73
75
|
}
|
|
@@ -13,13 +13,17 @@ export function usePageViewportScale({
|
|
|
13
13
|
pageContainerRef,
|
|
14
14
|
pageCount,
|
|
15
15
|
layoutMode = "single",
|
|
16
|
+
initialScaleMode = "fit-width",
|
|
17
|
+
maxFitScale = 1,
|
|
16
18
|
}: {
|
|
17
19
|
stageRef: RefObject<HTMLElement | null>;
|
|
18
20
|
pageContainerRef: RefObject<HTMLElement | null>;
|
|
19
21
|
pageCount: number;
|
|
20
22
|
layoutMode?: PageLayoutMode;
|
|
23
|
+
initialScaleMode?: PageViewportScaleMode;
|
|
24
|
+
maxFitScale?: number;
|
|
21
25
|
}) {
|
|
22
|
-
const [scaleMode, setScaleMode] = useState<PageViewportScaleMode>(
|
|
26
|
+
const [scaleMode, setScaleMode] = useState<PageViewportScaleMode>(initialScaleMode);
|
|
23
27
|
const [scale, setScale] = useState(1);
|
|
24
28
|
|
|
25
29
|
useLayoutEffect(() => {
|
|
@@ -66,7 +70,7 @@ export function usePageViewportScale({
|
|
|
66
70
|
const fitPageScale = canonicalWidth > 0 && canonicalHeight > 0
|
|
67
71
|
? Math.min(availableWidth / canonicalWidth, availableHeight / canonicalHeight)
|
|
68
72
|
: 1;
|
|
69
|
-
const nextScale = resolvePageViewportScale({ mode: scaleMode, fitWidthScale, fitPageScale });
|
|
73
|
+
const nextScale = resolvePageViewportScale({ mode: scaleMode, fitWidthScale, fitPageScale, maxFitScale });
|
|
70
74
|
const nextScaleValue = formatPageViewportScaleValue(nextScale);
|
|
71
75
|
|
|
72
76
|
container.style.setProperty("--openpress-page-viewport-scale", nextScaleValue);
|
|
@@ -93,16 +97,16 @@ export function usePageViewportScale({
|
|
|
93
97
|
window.removeEventListener("resize", syncScale);
|
|
94
98
|
window.visualViewport?.removeEventListener("resize", syncScale);
|
|
95
99
|
};
|
|
96
|
-
}, [layoutMode, pageContainerRef, pageCount, scaleMode, stageRef]);
|
|
100
|
+
}, [layoutMode, maxFitScale, pageContainerRef, pageCount, scaleMode, stageRef]);
|
|
97
101
|
|
|
98
102
|
const scaleLabel = useMemo(
|
|
99
103
|
() => {
|
|
100
104
|
const labelScale = scaleMode.startsWith("scale-")
|
|
101
|
-
? resolvePageViewportScale({ mode: scaleMode, fitWidthScale: scale, fitPageScale: scale })
|
|
105
|
+
? resolvePageViewportScale({ mode: scaleMode, fitWidthScale: scale, fitPageScale: scale, maxFitScale })
|
|
102
106
|
: scale;
|
|
103
107
|
return formatPageViewportScaleLabel(scaleMode, labelScale);
|
|
104
108
|
},
|
|
105
|
-
[scale, scaleMode],
|
|
109
|
+
[maxFitScale, scale, scaleMode],
|
|
106
110
|
);
|
|
107
111
|
|
|
108
112
|
return {
|
|
@@ -4,10 +4,8 @@ import {
|
|
|
4
4
|
useState,
|
|
5
5
|
type CSSProperties,
|
|
6
6
|
} from "react";
|
|
7
|
-
import { ExternalLink, Home, MousePointer2, Play, Ruler } from "lucide-react";
|
|
8
7
|
import {
|
|
9
8
|
getProjectIdentity,
|
|
10
|
-
resolveAnchorPageIndex,
|
|
11
9
|
type DeploymentInfo,
|
|
12
10
|
type HtmlPageBlock,
|
|
13
11
|
type ReaderDocument,
|
|
@@ -34,14 +32,12 @@ import {
|
|
|
34
32
|
type InlineDocumentSourceTarget,
|
|
35
33
|
} from "./document";
|
|
36
34
|
import {
|
|
37
|
-
DeploymentControl,
|
|
38
|
-
ExportImageControl,
|
|
39
|
-
PageZoomControl,
|
|
40
|
-
SearchControl,
|
|
41
35
|
useDeploymentWorkbench,
|
|
42
36
|
} from "./actions";
|
|
43
37
|
import { PendingCommentsPanel, WorkbenchControlPanel, type WorkbenchPanel } from "./panels";
|
|
44
38
|
import { WorkbenchShell } from "./shell";
|
|
39
|
+
import { WorkbenchToolbarActions } from "./shell/WorkbenchToolbarActions";
|
|
40
|
+
import { useWorkbenchNavigation } from "./hooks/useWorkbenchNavigation";
|
|
45
41
|
import {
|
|
46
42
|
formatPageGeometrySpec,
|
|
47
43
|
formatInspectorSelection,
|
|
@@ -132,23 +128,13 @@ export function HtmlWorkbench({
|
|
|
132
128
|
onDocumentEdited: onDocumentRefresh,
|
|
133
129
|
});
|
|
134
130
|
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
reader.toggleRightPanel();
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const selectWorkspaceAnchor = (anchorId: string, pageIndex?: number) => {
|
|
147
|
-
const targetPageIndex = resolveAnchorPageIndex(anchorPageMap, displayPages.length, anchorId, pageIndex);
|
|
148
|
-
if (targetPageIndex === null) return false;
|
|
149
|
-
selectWorkspacePage(targetPageIndex, { behavior: "smooth" });
|
|
150
|
-
return true;
|
|
151
|
-
};
|
|
131
|
+
const { selectWorkspaceAnchor, selectWorkspacePage } = useWorkbenchNavigation({
|
|
132
|
+
anchorPageMap,
|
|
133
|
+
pages: displayPages,
|
|
134
|
+
rightPanelOpen: reader.rightPanelOpen,
|
|
135
|
+
setPage: reader.setPage,
|
|
136
|
+
toggleRightPanel: reader.toggleRightPanel,
|
|
137
|
+
});
|
|
152
138
|
|
|
153
139
|
const comments = useInspectorComments({
|
|
154
140
|
workspaceMode,
|
|
@@ -244,145 +230,41 @@ export function HtmlWorkbench({
|
|
|
244
230
|
// don't rebuild the toolbar JSX. The toolbar depends on deploy/page/zoom
|
|
245
231
|
// state and inspector mode, but never on the composer draft text.
|
|
246
232
|
const toolbarActions = useMemo(() => (
|
|
247
|
-
|
|
248
|
-
{
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
aria-live="polite"
|
|
283
|
-
>
|
|
284
|
-
<span className="openpress-dev-pdf-status__spinner" aria-hidden="true" />
|
|
285
|
-
<span>{deployment.pdfStatusMessage}</span>
|
|
286
|
-
</span>
|
|
287
|
-
) : null}
|
|
288
|
-
</button>
|
|
289
|
-
<ExportImageControl
|
|
290
|
-
currentPageIndex={reader.currentPageIndex}
|
|
291
|
-
currentPageLabel={reader.currentPageLabel}
|
|
292
|
-
pressTitle={projectIdentity.name}
|
|
293
|
-
/>
|
|
294
|
-
</div>
|
|
295
|
-
<div className="openpress-workbench-toolbar__group openpress-workbench-toolbar__group--page" aria-label="頁面規格">
|
|
296
|
-
{isSlidePress && onOpenPresentation ? (
|
|
297
|
-
<button
|
|
298
|
-
type="button"
|
|
299
|
-
className="openpress-workbench-toolbar-action"
|
|
300
|
-
data-openpress-slide-present
|
|
301
|
-
data-openpress-toolbar-expanded="false"
|
|
302
|
-
data-openpress-toolbar-active="false"
|
|
303
|
-
aria-pressed="false"
|
|
304
|
-
title="進入放映模式"
|
|
305
|
-
aria-label="進入放映模式"
|
|
306
|
-
onClick={() => onOpenPresentation?.(reader.currentPageIndex)}
|
|
307
|
-
>
|
|
308
|
-
<Play aria-hidden="true" />
|
|
309
|
-
<span className="openpress-workbench-toolbar-action__label">放映</span>
|
|
310
|
-
</button>
|
|
311
|
-
) : null}
|
|
312
|
-
<button
|
|
313
|
-
type="button"
|
|
314
|
-
className="openpress-workbench-page-geometry"
|
|
315
|
-
data-openpress-page-geometry
|
|
316
|
-
title={pageGeometry.title}
|
|
317
|
-
aria-label={`頁面規格 ${pageGeometry.title}`}
|
|
318
|
-
>
|
|
319
|
-
<Ruler aria-hidden="true" />
|
|
320
|
-
<span className="openpress-workbench-page-geometry__label">{pageGeometry.label}</span>
|
|
321
|
-
<span className="openpress-workbench-page-geometry__dimensions">{pageGeometry.dimensions}</span>
|
|
322
|
-
</button>
|
|
323
|
-
<PageZoomControl
|
|
324
|
-
scaleMode={pageViewport.scaleMode}
|
|
325
|
-
scaleLabel={pageViewport.scaleLabel}
|
|
326
|
-
pageLayoutMode={pageLayoutMode}
|
|
327
|
-
onScaleModeChange={pageViewport.setScaleMode}
|
|
328
|
-
onPageLayoutModeChange={setPageLayoutMode}
|
|
329
|
-
/>
|
|
330
|
-
</div>
|
|
331
|
-
<div className="openpress-workbench-toolbar__group openpress-workbench-toolbar__group--right" aria-label="工作台狀態與發布">
|
|
332
|
-
{workspaceMode ? (
|
|
333
|
-
<SearchControl
|
|
334
|
-
sourceBlocksByPath={sourceBlocksByPath}
|
|
335
|
-
onSelectPage={selectWorkspacePage}
|
|
336
|
-
/>
|
|
337
|
-
) : null}
|
|
338
|
-
{workspaceMode && editStatusMessage ? (
|
|
339
|
-
<span
|
|
340
|
-
className="openpress-dev-edit-status openpress-dev-edit-status--toolbar"
|
|
341
|
-
data-openpress-edit-status={inlineEditStatus.state}
|
|
342
|
-
role="status"
|
|
343
|
-
aria-live="polite"
|
|
344
|
-
>
|
|
345
|
-
{inlineEditStatus.state === "saving" ? <span className="openpress-dev-edit-status__spinner" aria-hidden="true" /> : null}
|
|
346
|
-
<span>{editStatusMessage}</span>
|
|
347
|
-
</span>
|
|
348
|
-
) : null}
|
|
349
|
-
{workspaceMode ? (
|
|
350
|
-
<button
|
|
351
|
-
type="button"
|
|
352
|
-
className="openpress-workbench-toolbar-action"
|
|
353
|
-
data-openpress-inspector-toggle
|
|
354
|
-
data-openpress-inspector-active={inspector.inspectorMode ? "true" : "false"}
|
|
355
|
-
data-openpress-toolbar-expanded={inspectorToolbarExpanded ? "true" : "false"}
|
|
356
|
-
data-openpress-toolbar-active={inspectorToolbarExpanded ? "true" : "false"}
|
|
357
|
-
onClick={() => inspector.setInspectorMode(!inspector.inspectorMode)}
|
|
358
|
-
aria-pressed={inspector.inspectorMode}
|
|
359
|
-
title={inspector.inspectorMode ? "關閉註解" : "開啟註解"}
|
|
360
|
-
aria-label={inspector.inspectorMode ? "關閉註解" : "開啟註解"}
|
|
361
|
-
>
|
|
362
|
-
<MousePointer2 aria-hidden="true" />
|
|
363
|
-
<span className="openpress-workbench-toolbar-action__label">{inspector.inspectorMode ? "註解中" : "註解"}</span>
|
|
364
|
-
<span className="openpress-dev-inspector-status">{inspectorSelectionLabel}</span>
|
|
365
|
-
</button>
|
|
366
|
-
) : null}
|
|
367
|
-
{workspaceMode && inspector.inspectorMode ? (
|
|
368
|
-
<span
|
|
369
|
-
className="openpress-dev-inspector-status"
|
|
370
|
-
role="status"
|
|
371
|
-
aria-live="polite"
|
|
372
|
-
data-openpress-inspector-comment-status={comments.inspectorCommentStatus}
|
|
373
|
-
>
|
|
374
|
-
{comments.inspectorCommentStatusMessage}
|
|
375
|
-
</span>
|
|
376
|
-
) : null}
|
|
377
|
-
{deployment.localDeployEnabled ? (
|
|
378
|
-
<DeploymentControl
|
|
379
|
-
info={deployment.currentDeploymentInfo}
|
|
380
|
-
status={deployment.status}
|
|
381
|
-
onDeploy={deployment.handleDeploy}
|
|
382
|
-
/>
|
|
383
|
-
) : null}
|
|
384
|
-
</div>
|
|
385
|
-
</>
|
|
233
|
+
<WorkbenchToolbarActions
|
|
234
|
+
pages={displayPages}
|
|
235
|
+
currentPageIndex={reader.currentPageIndex}
|
|
236
|
+
pressTitle={projectIdentity.name}
|
|
237
|
+
theme={document.theme}
|
|
238
|
+
workspaceMode={workspaceMode}
|
|
239
|
+
sourceBlocksByPath={sourceBlocksByPath}
|
|
240
|
+
onSelectPage={selectWorkspacePage}
|
|
241
|
+
onBackToWorkspace={onBackToWorkspace}
|
|
242
|
+
isSlidePress={isSlidePress}
|
|
243
|
+
onOpenPresentation={onOpenPresentation}
|
|
244
|
+
pageGeometry={pageGeometry}
|
|
245
|
+
scaleMode={pageViewport.scaleMode}
|
|
246
|
+
scaleLabel={pageViewport.scaleLabel}
|
|
247
|
+
pageLayoutMode={pageLayoutMode}
|
|
248
|
+
onScaleModeChange={pageViewport.setScaleMode}
|
|
249
|
+
onPageLayoutModeChange={setPageLayoutMode}
|
|
250
|
+
inlineEditStatus={inlineEditStatus}
|
|
251
|
+
editStatusMessage={editStatusMessage}
|
|
252
|
+
inspectorMode={inspector.inspectorMode}
|
|
253
|
+
inspectorToolbarExpanded={inspectorToolbarExpanded}
|
|
254
|
+
inspectorSelectionLabel={inspectorSelectionLabel}
|
|
255
|
+
onInspectorModeChange={inspector.setInspectorMode}
|
|
256
|
+
inspectorCommentStatus={comments.inspectorCommentStatus}
|
|
257
|
+
inspectorCommentStatusMessage={comments.inspectorCommentStatusMessage}
|
|
258
|
+
deploymentInfo={deployment.currentDeploymentInfo}
|
|
259
|
+
deploymentStatus={deployment.status}
|
|
260
|
+
localDeployEnabled={deployment.localDeployEnabled}
|
|
261
|
+
onDeploy={deployment.handleDeploy}
|
|
262
|
+
onExportPdf={deployment.handleOpenWorkbenchPdf}
|
|
263
|
+
pdfDisabled={deployment.pdfButtonDisabled}
|
|
264
|
+
pdfLabel={deployment.pdfButtonText}
|
|
265
|
+
pdfStatusMessage={deployment.pdfStatusMessage}
|
|
266
|
+
pdfActionStatus={deployment.pdfActionStatus}
|
|
267
|
+
/>
|
|
386
268
|
), [
|
|
387
269
|
comments.inspectorCommentStatus,
|
|
388
270
|
comments.inspectorCommentStatusMessage,
|
|
@@ -394,8 +276,9 @@ export function HtmlWorkbench({
|
|
|
394
276
|
deployment.pdfButtonDisabled,
|
|
395
277
|
deployment.pdfButtonText,
|
|
396
278
|
deployment.pdfStatusMessage,
|
|
397
|
-
deployment.pdfToolbarExpanded,
|
|
398
279
|
deployment.status,
|
|
280
|
+
displayPages,
|
|
281
|
+
document.theme,
|
|
399
282
|
workspaceMode,
|
|
400
283
|
editStatusMessage,
|
|
401
284
|
inlineEditStatus.state,
|
|
@@ -416,7 +299,6 @@ export function HtmlWorkbench({
|
|
|
416
299
|
onBackToWorkspace,
|
|
417
300
|
onOpenPresentation,
|
|
418
301
|
reader.currentPageIndex,
|
|
419
|
-
reader.currentPageLabel,
|
|
420
302
|
projectIdentity.name,
|
|
421
303
|
]);
|
|
422
304
|
|
|
@@ -75,9 +75,9 @@ export function DeploymentControl({
|
|
|
75
75
|
className="openpress-workbench-toolbar-action"
|
|
76
76
|
data-openpress-deploy
|
|
77
77
|
data-openpress-deploy-status={kind}
|
|
78
|
+
data-openpress-deploy-state={status}
|
|
78
79
|
data-openpress-toolbar-expanded="false"
|
|
79
80
|
data-openpress-toolbar-active="false"
|
|
80
|
-
data-deploy-status={status}
|
|
81
81
|
aria-busy={busy ? "true" : "false"}
|
|
82
82
|
aria-label={buttonText}
|
|
83
83
|
title={description}
|