@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.
Files changed (40) hide show
  1. package/engine/cli.mjs +1 -1
  2. package/engine/commands/_shared.mjs +10 -5
  3. package/engine/commands/deploy.mjs +19 -4
  4. package/engine/output/static-server.mjs +16 -9
  5. package/package.json +1 -1
  6. package/src/openpress/app/OpenPressApp.tsx +4 -1
  7. package/src/openpress/app/OpenPressRuntime.tsx +26 -1
  8. package/src/openpress/reader/PageThumbnailsPanel.tsx +28 -5
  9. package/src/openpress/reader/SlidePresentationPage.tsx +36 -19
  10. package/src/openpress/reader/SlidePublicPage.tsx +332 -0
  11. package/src/openpress/reader/index.ts +1 -0
  12. package/src/openpress/reader/pageViewportScaleModel.ts +5 -3
  13. package/src/openpress/reader/usePageViewportScale.ts +9 -5
  14. package/src/openpress/workbench/Workbench.tsx +46 -164
  15. package/src/openpress/workbench/actions/DeploymentControl.tsx +1 -1
  16. package/src/openpress/workbench/actions/ExportControl.tsx +267 -0
  17. package/src/openpress/workbench/actions/index.ts +1 -1
  18. package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +7 -2
  19. package/src/openpress/workbench/hooks/useWorkbenchNavigation.ts +42 -0
  20. package/src/openpress/workbench/project/ProjectEntryPanel.tsx +2 -278
  21. package/src/openpress/workbench/shell/WorkbenchToolbarActions.tsx +206 -0
  22. package/src/styles/openpress/app-shell.css +0 -83
  23. package/src/styles/openpress/print-route.css +1 -3
  24. package/src/styles/openpress/project-preview-panel.css +5 -783
  25. package/src/styles/openpress/public-viewer.css +7 -249
  26. package/src/styles/openpress/reader-runtime.css +0 -274
  27. package/src/styles/openpress/slide-presenter.css +150 -0
  28. package/src/styles/openpress/slide-public-viewer.css +222 -0
  29. package/src/styles/openpress/workbench-dialog.css +267 -0
  30. package/src/styles/openpress/workbench-export.css +154 -0
  31. package/src/styles/openpress/workbench-inline-editor.css +128 -0
  32. package/src/styles/openpress/workbench-panels.css +0 -88
  33. package/src/styles/openpress/workbench-search.css +257 -0
  34. package/src/styles/openpress/workbench-toolbar.css +422 -0
  35. package/src/styles/openpress/workbench.css +34 -1263
  36. package/src/styles/openpress/workspace-gallery.css +0 -5
  37. package/src/styles/openpress.css +7 -1
  38. package/vite.config.ts +16 -9
  39. package/src/openpress/workbench/actions/ExportImageControl.tsx +0 -96
  40. 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, MAX_FIT_PAGE_VIEWPORT_SCALE);
43
- if (mode === "fit-page") return clampPageViewportScale(fitPageScale, MAX_FIT_PAGE_VIEWPORT_SCALE);
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 = Number.isFinite(maxScale) && maxScale > 0 ? maxScale : MAX_FIXED_PAGE_VIEWPORT_SCALE;
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>("fit-width");
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 selectWorkspacePage = (pageIndex: number, options?: { behavior?: ScrollBehavior }) => {
136
- reader.setPage(pageIndex, options);
137
- if (
138
- typeof window !== "undefined"
139
- && window.innerWidth < PUBLIC_DRAWER_BREAKPOINT
140
- && reader.rightPanelOpen
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
- {onBackToWorkspace ? (
249
- <div className="openpress-workbench-toolbar__group" aria-label="工作台導覽">
250
- <button
251
- type="button"
252
- className="openpress-workbench-toolbar-action openpress-workbench-toolbar-action--back"
253
- data-openpress-back-to-workspace
254
- onClick={onBackToWorkspace}
255
- title="回到工作台"
256
- aria-label="回到工作台"
257
- >
258
- <Home aria-hidden="true" />
259
- <span className="openpress-workbench-toolbar-action__label">工作台</span>
260
- </button>
261
- </div>
262
- ) : null}
263
- <div className="openpress-workbench-toolbar__group" aria-label="輸出">
264
- <button
265
- type="button"
266
- className="openpress-workbench-toolbar-action"
267
- data-openpress-public-export
268
- data-openpress-toolbar-expanded={deployment.pdfToolbarExpanded ? "true" : "false"}
269
- data-openpress-toolbar-active={deployment.pdfToolbarExpanded ? "true" : "false"}
270
- disabled={deployment.pdfButtonDisabled}
271
- onClick={deployment.handleOpenWorkbenchPdf}
272
- title={deployment.pdfButtonText}
273
- aria-label={deployment.pdfButtonText}
274
- >
275
- <ExternalLink aria-hidden="true" />
276
- <span className="openpress-workbench-toolbar-action__label">{deployment.pdfButtonText}</span>
277
- {deployment.pdfStatusMessage ? (
278
- <span
279
- className="openpress-dev-pdf-status"
280
- data-openpress-pdf-status={deployment.pdfActionStatus}
281
- role="status"
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}