@open-press/core 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/engine/commands/dev.mjs +2 -2
  2. package/engine/commands/upgrade.mjs +47 -5
  3. package/engine/output/chrome-pdf.mjs +18 -3
  4. package/engine/output/static-server.mjs +39 -0
  5. package/engine/react/comment-endpoint.mjs +13 -39
  6. package/engine/react/comment-marker.mjs +30 -6
  7. package/engine/react/document-entry.mjs +11 -0
  8. package/engine/react/document-export.mjs +45 -5
  9. package/engine/react/http-json.mjs +24 -0
  10. package/engine/react/mdx-compile.mjs +187 -3
  11. package/engine/react/measurement-css.mjs +93 -1
  12. package/engine/react/object-entities.mjs +119 -0
  13. package/engine/react/pipeline/allocate.mjs +10 -7
  14. package/engine/react/pipeline/frame-measurement.mjs +40 -9
  15. package/engine/react/project-asset-endpoint.mjs +6 -24
  16. package/engine/react/source-edit-endpoint.d.mts +10 -0
  17. package/engine/react/source-edit-endpoint.mjs +75 -0
  18. package/engine/react/sources/mdx-resolver.mjs +12 -14
  19. package/engine/react/style-discovery.mjs +1 -4
  20. package/engine/runtime/file-walk.mjs +22 -0
  21. package/engine/runtime/inspection.mjs +1 -20
  22. package/engine/runtime/path-utils.mjs +20 -0
  23. package/engine/runtime/source-text-tools.d.mts +102 -0
  24. package/engine/runtime/source-text-tools.mjs +551 -16
  25. package/engine/runtime/source-workspace.mjs +4 -31
  26. package/package.json +1 -1
  27. package/src/openpress/{App.tsx → app/OpenPressApp.tsx} +25 -12
  28. package/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +10 -7
  29. package/src/openpress/app/index.ts +2 -0
  30. package/src/openpress/core/Frame.tsx +9 -11
  31. package/src/openpress/core/FrameContext.tsx +8 -3
  32. package/src/openpress/core/MdxArea.tsx +11 -12
  33. package/src/openpress/core/cn.ts +4 -0
  34. package/src/openpress/core/index.tsx +2 -1
  35. package/src/openpress/core/primitives.tsx +29 -8
  36. package/src/openpress/core/types.ts +8 -0
  37. package/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
  38. package/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
  39. package/src/openpress/{types.ts → document-model/documentTypes.ts} +42 -0
  40. package/src/openpress/document-model/index.ts +6 -0
  41. package/src/openpress/document-model/objectEntityModel.ts +51 -0
  42. package/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
  43. package/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
  44. package/src/openpress/manuscript/index.tsx +49 -7
  45. package/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
  46. package/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
  47. package/src/openpress/reader/index.ts +10 -0
  48. package/src/openpress/reader/pageViewportScaleModel.ts +73 -0
  49. package/src/openpress/reader/readerTypes.ts +4 -0
  50. package/src/openpress/reader/usePageViewportScale.ts +119 -0
  51. package/src/openpress/reader/usePanelState.ts +56 -0
  52. package/src/openpress/reader/useReaderHashSync.ts +61 -0
  53. package/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
  54. package/src/openpress/reader/useReaderRuntime.ts +146 -0
  55. package/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
  56. package/src/openpress/shared/Panel.tsx +77 -0
  57. package/src/openpress/shared/index.ts +4 -0
  58. package/src/openpress/shared/numberUtils.ts +3 -0
  59. package/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
  60. package/src/openpress/workbench/Workbench.tsx +407 -0
  61. package/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
  62. package/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
  63. package/src/openpress/workbench/actions/SearchControl.tsx +345 -0
  64. package/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
  65. package/src/openpress/workbench/actions/index.ts +5 -0
  66. package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
  67. package/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
  68. package/src/openpress/workbench/dialog/index.ts +1 -0
  69. package/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
  70. package/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
  71. package/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
  72. package/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
  73. package/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
  74. package/src/openpress/workbench/document/index.ts +10 -0
  75. package/src/openpress/workbench/index.ts +2 -0
  76. package/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
  77. package/src/openpress/workbench/inspector/index.ts +5 -0
  78. package/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
  79. package/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
  80. package/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
  81. package/src/openpress/workbench/inspector/useInspectorComments.ts +248 -0
  82. package/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
  83. package/src/openpress/workbench/mentions/index.ts +2 -0
  84. package/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
  85. package/src/openpress/workbench/panels/Panel.tsx +1 -0
  86. package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +76 -0
  87. package/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
  88. package/src/openpress/workbench/panels/index.ts +3 -0
  89. package/src/openpress/workbench/project/ProjectEntryPanel.tsx +523 -0
  90. package/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
  91. package/src/openpress/workbench/project/index.ts +2 -0
  92. package/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
  93. package/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
  94. package/src/openpress/workbench/shell/index.ts +1 -0
  95. package/src/openpress/workbench/workbenchFormatters.ts +120 -0
  96. package/src/openpress/workbench/workbenchTypes.ts +35 -0
  97. package/src/styles/openpress/print-route.css +0 -2
  98. package/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
  99. package/src/styles/openpress/public-viewer.css +25 -320
  100. package/src/styles/openpress/reader-runtime.css +243 -55
  101. package/src/styles/openpress/responsive.css +145 -270
  102. package/src/styles/openpress/workbench-panels.css +214 -178
  103. package/src/styles/openpress/workbench.css +986 -451
  104. package/src/styles/openpress.css +1 -1
  105. package/vite.config.ts +50 -0
  106. package/src/openpress/inspector.ts +0 -282
  107. package/src/openpress/projectWorkspace.tsx +0 -919
  108. package/src/openpress/readerRuntime.ts +0 -230
  109. package/src/openpress/workbench.tsx +0 -1265
  110. package/src/openpress/workbenchTypes.ts +0 -4
  111. /package/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
  112. /package/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
  113. /package/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
  114. /package/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
  115. /package/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
  116. /package/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
@@ -0,0 +1,407 @@
1
+ import {
2
+ useMemo,
3
+ useRef,
4
+ useState,
5
+ type CSSProperties,
6
+ } from "react";
7
+ import { ExternalLink, MousePointer2, Ruler } from "lucide-react";
8
+ import {
9
+ getProjectIdentity,
10
+ resolveAnchorPageIndex,
11
+ type DeploymentInfo,
12
+ type HtmlPageBlock,
13
+ type ReaderDocument,
14
+ } from "../document-model";
15
+ import { InlineInspectorLayer, useInspector, useInspectorComments } from "./inspector";
16
+ import { ProjectEntryPanel } from "./project";
17
+ import {
18
+ Bookmarks,
19
+ CurrentPagePanel,
20
+ PUBLIC_DRAWER_BREAKPOINT,
21
+ PublicPage,
22
+ useReaderRuntime,
23
+ usePageViewportScale,
24
+ useViewMode,
25
+ type PageLayoutMode,
26
+ } from "../reader";
27
+ import {
28
+ ReaderStage,
29
+ InlineSourceEditorLayer,
30
+ useDocumentWorkbenchModel,
31
+ useInlineDocumentEditor,
32
+ type InlineDocumentEditStatus,
33
+ type InlineDocumentSourceTarget,
34
+ } from "./document";
35
+ import {
36
+ DeploymentControl,
37
+ PageZoomControl,
38
+ SearchControl,
39
+ useDeploymentWorkbench,
40
+ } from "./actions";
41
+ import { PendingCommentsPanel, WorkbenchControlPanel, type WorkbenchPanel } from "./panels";
42
+ import { WorkbenchShell } from "./shell";
43
+ import {
44
+ formatPageGeometrySpec,
45
+ formatInspectorSelection,
46
+ } from "./workbenchFormatters";
47
+
48
+ export function HtmlWorkbench({
49
+ document,
50
+ pages,
51
+ style,
52
+ devMode,
53
+ deploymentInfo,
54
+ onDocumentRefresh,
55
+ extraControlPanels,
56
+ }: {
57
+ document: ReaderDocument;
58
+ pages: Array<HtmlPageBlock>;
59
+ style: CSSProperties;
60
+ devMode: boolean;
61
+ deploymentInfo: DeploymentInfo;
62
+ onDocumentRefresh?: () => void | Promise<void>;
63
+ // Append extra panels into the right-side control panel. Built-in panels
64
+ // (pending comments + project entry) render first; extra panels render
65
+ // after them in the supplied order.
66
+ extraControlPanels?: WorkbenchPanel[];
67
+ }) {
68
+ const sourceContainerRef = useRef<HTMLDivElement | null>(null);
69
+ const displayPages = pages;
70
+ const { viewMode } = useViewMode();
71
+ const {
72
+ mediaAssets,
73
+ anchorPageMap,
74
+ projectComponentUsages,
75
+ bookmarks,
76
+ sourceBlockMap,
77
+ sourceBlocksByPath,
78
+ projectMentionItems,
79
+ } = useDocumentWorkbenchModel(document, displayPages);
80
+ const inspector = useInspector(document, { enabled: devMode });
81
+ const reader = useReaderRuntime({
82
+ pageCount: Math.max(displayPages.length, 1),
83
+ leftPanelBreakpoint: PUBLIC_DRAWER_BREAKPOINT,
84
+ rightPanelBreakpoint: PUBLIC_DRAWER_BREAKPOINT,
85
+ });
86
+ const [pageLayoutMode, setPageLayoutMode] = useState<PageLayoutMode>("single");
87
+ const pageViewport = usePageViewportScale({
88
+ stageRef: reader.stageRef,
89
+ pageContainerRef: sourceContainerRef,
90
+ pageCount: displayPages.length,
91
+ layoutMode: pageLayoutMode,
92
+ });
93
+ const deployment = useDeploymentWorkbench({ deploymentInfo });
94
+ const [inlineEditStatus, setInlineEditStatus] = useState<InlineDocumentEditStatus>({ state: "idle" });
95
+ const [sourceEditorTarget, setSourceEditorTarget] = useState<InlineDocumentSourceTarget | null>(null);
96
+
97
+ const projectIdentity = getProjectIdentity(document.meta);
98
+ const pageGeometry = formatPageGeometrySpec(document.theme);
99
+ const inspectorSelectionLabel = formatInspectorSelection(
100
+ inspector.selectedBlock,
101
+ inspector.selectedObjectEntity,
102
+ );
103
+ const inspectorToolbarExpanded = inspector.inspectorMode;
104
+ const editStatusMessage = formatInlineEditStatus(inlineEditStatus);
105
+
106
+ useInlineDocumentEditor({
107
+ enabled: devMode,
108
+ sourceContainerRef,
109
+ sourceBlockMap,
110
+ onStatusChange: setInlineEditStatus,
111
+ onOpenSourceBlock: setSourceEditorTarget,
112
+ onDocumentEdited: onDocumentRefresh,
113
+ });
114
+
115
+ const selectWorkspacePage = (pageIndex: number, options?: { behavior?: ScrollBehavior }) => {
116
+ reader.setPage(pageIndex, options);
117
+ if (
118
+ typeof window !== "undefined"
119
+ && window.innerWidth < PUBLIC_DRAWER_BREAKPOINT
120
+ && reader.rightPanelOpen
121
+ ) {
122
+ reader.toggleRightPanel();
123
+ }
124
+ };
125
+
126
+ const selectWorkspaceAnchor = (anchorId: string, pageIndex?: number) => {
127
+ const targetPageIndex = resolveAnchorPageIndex(anchorPageMap, displayPages.length, anchorId, pageIndex);
128
+ if (targetPageIndex === null) return false;
129
+ selectWorkspacePage(targetPageIndex, { behavior: "smooth" });
130
+ return true;
131
+ };
132
+
133
+ const comments = useInspectorComments({
134
+ devMode,
135
+ inspector,
136
+ sourceBlockMap,
137
+ sourceBlocksByPath,
138
+ sourceContainerRef,
139
+ onSelectWorkspacePage: selectWorkspacePage,
140
+ });
141
+
142
+ // Stabilize the controller objects so memoized InlineInspectorLayer can skip
143
+ // re-rendering when nothing observable changed.
144
+ const inspectorLayerComments = useMemo(() => ({
145
+ saved: comments.inlineSavedComments,
146
+ active: comments.activeInlineSavedComment ?? null,
147
+ status: comments.inspectorCommentStatus,
148
+ statusMessage: comments.inspectorCommentStatusMessage,
149
+ totalCount: comments.pendingComments.length,
150
+ onOpenSaved: comments.handleOpenInlineSavedComment,
151
+ onRemoveSaved: comments.handleRemoveInlineSavedComment,
152
+ }), [
153
+ comments.activeInlineSavedComment,
154
+ comments.handleOpenInlineSavedComment,
155
+ comments.handleRemoveInlineSavedComment,
156
+ comments.inlineSavedComments,
157
+ comments.inspectorCommentStatus,
158
+ comments.inspectorCommentStatusMessage,
159
+ comments.pendingComments.length,
160
+ ]);
161
+ const inspectorLayerComposer = useMemo(() => ({
162
+ text: comments.inspectorCommentText,
163
+ submitDisabled: comments.inspectorCommentDisabled,
164
+ mentionItems: projectMentionItems,
165
+ onTextChange: comments.setInspectorCommentText,
166
+ onSubmit: comments.handleSubmitInspectorComment,
167
+ }), [
168
+ comments.handleSubmitInspectorComment,
169
+ comments.inspectorCommentDisabled,
170
+ comments.inspectorCommentText,
171
+ comments.setInspectorCommentText,
172
+ projectMentionItems,
173
+ ]);
174
+
175
+ const currentSourcePath = displayPages[reader.currentPageIndex]?.source;
176
+ const builtInControlPanels: WorkbenchPanel[] = [
177
+ {
178
+ id: "pending-comments",
179
+ render: () => (
180
+ <PendingCommentsPanel
181
+ comments={comments.pendingComments}
182
+ status={comments.commentsStatus}
183
+ error={comments.commentsError}
184
+ onClear={comments.clearPendingComment}
185
+ onSelect={comments.handleSelectPendingComment}
186
+ />
187
+ ),
188
+ },
189
+ {
190
+ id: "project-entry",
191
+ render: () => (
192
+ <ProjectEntryPanel
193
+ mediaAssets={mediaAssets}
194
+ componentUsages={projectComponentUsages}
195
+ mentionItems={projectMentionItems}
196
+ currentSource={currentSourcePath}
197
+ onCommentSubmitted={comments.refreshPendingComments}
198
+ />
199
+ ),
200
+ },
201
+ ];
202
+ const controlPanels = extraControlPanels
203
+ ? [...builtInControlPanels, ...extraControlPanels]
204
+ : builtInControlPanels;
205
+
206
+ const toolbarActions = (
207
+ <>
208
+ <div className="openpress-workbench-toolbar__group" aria-label="輸出">
209
+ <button
210
+ type="button"
211
+ className="openpress-workbench-toolbar-action"
212
+ data-openpress-public-export
213
+ data-openpress-toolbar-expanded={deployment.pdfToolbarExpanded ? "true" : "false"}
214
+ data-openpress-toolbar-active={deployment.pdfToolbarExpanded ? "true" : "false"}
215
+ disabled={deployment.pdfButtonDisabled}
216
+ onClick={deployment.handleOpenWorkbenchPdf}
217
+ title={deployment.pdfButtonText}
218
+ aria-label={deployment.pdfButtonText}
219
+ >
220
+ <ExternalLink aria-hidden="true" />
221
+ <span className="openpress-workbench-toolbar-action__label">{deployment.pdfButtonText}</span>
222
+ {deployment.pdfStatusMessage ? (
223
+ <span
224
+ className="openpress-dev-pdf-status"
225
+ data-openpress-pdf-status={deployment.pdfActionStatus}
226
+ role="status"
227
+ aria-live="polite"
228
+ >
229
+ <span className="openpress-dev-pdf-status__spinner" aria-hidden="true" />
230
+ <span>{deployment.pdfStatusMessage}</span>
231
+ </span>
232
+ ) : null}
233
+ </button>
234
+ </div>
235
+ <div className="openpress-workbench-toolbar__group openpress-workbench-toolbar__group--page" aria-label="頁面規格">
236
+ <button
237
+ type="button"
238
+ className="openpress-workbench-page-geometry"
239
+ data-openpress-page-geometry
240
+ title={pageGeometry.title}
241
+ aria-label={`頁面規格 ${pageGeometry.title}`}
242
+ >
243
+ <Ruler aria-hidden="true" />
244
+ <span className="openpress-workbench-page-geometry__label">{pageGeometry.label}</span>
245
+ <span className="openpress-workbench-page-geometry__dimensions">{pageGeometry.dimensions}</span>
246
+ </button>
247
+ <PageZoomControl
248
+ scaleMode={pageViewport.scaleMode}
249
+ scaleLabel={pageViewport.scaleLabel}
250
+ pageLayoutMode={pageLayoutMode}
251
+ onScaleModeChange={pageViewport.setScaleMode}
252
+ onPageLayoutModeChange={setPageLayoutMode}
253
+ />
254
+ </div>
255
+ <div className="openpress-workbench-toolbar__group openpress-workbench-toolbar__group--right" aria-label="工作台狀態與發布">
256
+ {devMode ? (
257
+ <SearchControl
258
+ sourceBlocksByPath={sourceBlocksByPath}
259
+ onSelectPage={selectWorkspacePage}
260
+ />
261
+ ) : null}
262
+ {devMode && editStatusMessage ? (
263
+ <span
264
+ className="openpress-dev-edit-status openpress-dev-edit-status--toolbar"
265
+ data-openpress-edit-status={inlineEditStatus.state}
266
+ role="status"
267
+ aria-live="polite"
268
+ >
269
+ {inlineEditStatus.state === "saving" ? <span className="openpress-dev-edit-status__spinner" aria-hidden="true" /> : null}
270
+ <span>{editStatusMessage}</span>
271
+ </span>
272
+ ) : null}
273
+ {devMode ? (
274
+ <button
275
+ type="button"
276
+ className="openpress-workbench-toolbar-action"
277
+ data-openpress-inspector-toggle
278
+ data-openpress-inspector-active={inspector.inspectorMode ? "true" : "false"}
279
+ data-openpress-toolbar-expanded={inspectorToolbarExpanded ? "true" : "false"}
280
+ data-openpress-toolbar-active={inspectorToolbarExpanded ? "true" : "false"}
281
+ onClick={() => inspector.setInspectorMode(!inspector.inspectorMode)}
282
+ aria-pressed={inspector.inspectorMode}
283
+ title={inspector.inspectorMode ? "關閉註解" : "開啟註解"}
284
+ aria-label={inspector.inspectorMode ? "關閉註解" : "開啟註解"}
285
+ >
286
+ <MousePointer2 aria-hidden="true" />
287
+ <span className="openpress-workbench-toolbar-action__label">{inspector.inspectorMode ? "註解中" : "註解"}</span>
288
+ <span className="openpress-dev-inspector-status">{inspectorSelectionLabel}</span>
289
+ </button>
290
+ ) : null}
291
+ {devMode && inspector.inspectorMode ? (
292
+ <span
293
+ className="openpress-dev-inspector-status"
294
+ role="status"
295
+ aria-live="polite"
296
+ data-openpress-inspector-comment-status={comments.inspectorCommentStatus}
297
+ >
298
+ {comments.inspectorCommentStatusMessage}
299
+ </span>
300
+ ) : null}
301
+ {deployment.localDeployEnabled ? (
302
+ <DeploymentControl
303
+ info={deployment.currentDeploymentInfo}
304
+ status={deployment.status}
305
+ onDeploy={deployment.handleDeploy}
306
+ />
307
+ ) : null}
308
+ </div>
309
+ </>
310
+ );
311
+
312
+ return (
313
+ <WorkbenchShell
314
+ style={style}
315
+ devMode={devMode}
316
+ viewMode={viewMode}
317
+ inspectorMode={inspector.inspectorMode}
318
+ editMode={devMode}
319
+ leftPanelOpen={reader.leftPanelOpen}
320
+ rightPanelOpen={reader.rightPanelOpen}
321
+ onToggleLeftPanel={reader.toggleLeftPanel}
322
+ onToggleRightPanel={reader.toggleRightPanel}
323
+ >
324
+ <WorkbenchShell.Toolbar>
325
+ {toolbarActions}
326
+ </WorkbenchShell.Toolbar>
327
+
328
+ <WorkbenchShell.LeftPanel>
329
+ <section className="openpress-public-identity" aria-label="文件資訊">
330
+ <strong>
331
+ <span className="openpress-public-title-main">{projectIdentity.name}</span>
332
+ {projectIdentity.subtitle ? <span className="openpress-public-title-sub">{projectIdentity.subtitle}</span> : null}
333
+ </strong>
334
+ {projectIdentity.label ? <span>{projectIdentity.label}</span> : null}
335
+ </section>
336
+
337
+ <section
338
+ id="openpress-bookmarks"
339
+ className="openpress-panel-section openpress-panel-section--bookmarks"
340
+ aria-label="章節書籤"
341
+ >
342
+ <nav className="reader-bookmarks" aria-label="章節導覽" data-openpress-react-bookmarks="true">
343
+ <div className="reader-bookmarks-rail" aria-hidden="true" />
344
+ <Bookmarks
345
+ items={bookmarks}
346
+ currentPageIndex={reader.currentPageIndex}
347
+ onSelectPage={selectWorkspacePage}
348
+ />
349
+ </nav>
350
+ </section>
351
+ <CurrentPagePanel
352
+ currentPageLabel={reader.currentPageLabel}
353
+ totalPageLabel={reader.totalPageLabel}
354
+ progressPercent={reader.progressPercent}
355
+ title={displayPages[reader.currentPageIndex]?.title || document.meta.title}
356
+ pageLabelPrefix="頁"
357
+ showHeading={false}
358
+ showTitle={false}
359
+ />
360
+ </WorkbenchShell.LeftPanel>
361
+
362
+ <WorkbenchShell.RightPanel>
363
+ <WorkbenchControlPanel panels={controlPanels} />
364
+ </WorkbenchShell.RightPanel>
365
+
366
+ <WorkbenchShell.MainContent>
367
+ <ReaderStage ref={reader.stageRef}>
368
+ <PublicPage
369
+ pages={displayPages}
370
+ currentPageIndex={reader.currentPageIndex}
371
+ devMode={devMode}
372
+ sourceContainerRef={sourceContainerRef}
373
+ registerPage={reader.registerPage}
374
+ exposeSourceData={devMode}
375
+ inspector={inspector}
376
+ onInternalAnchorNavigate={selectWorkspaceAnchor}
377
+ pageLayoutMode={pageLayoutMode}
378
+ />
379
+ {devMode ? (
380
+ <InlineInspectorLayer
381
+ sourceContainerRef={sourceContainerRef}
382
+ inspector={inspector}
383
+ comments={inspectorLayerComments}
384
+ composer={inspectorLayerComposer}
385
+ geometryVersion={`${pageViewport.scaleMode}:${pageViewport.scale}:${pageLayoutMode}`}
386
+ />
387
+ ) : null}
388
+ {devMode ? (
389
+ <InlineSourceEditorLayer
390
+ target={sourceEditorTarget}
391
+ onClose={() => setSourceEditorTarget(null)}
392
+ onStatusChange={setInlineEditStatus}
393
+ geometryVersion={`${pageViewport.scaleMode}:${pageViewport.scale}:${pageLayoutMode}`}
394
+ />
395
+ ) : null}
396
+ </ReaderStage>
397
+ </WorkbenchShell.MainContent>
398
+ </WorkbenchShell>
399
+ );
400
+ }
401
+
402
+ function formatInlineEditStatus(status: InlineDocumentEditStatus) {
403
+ if (status.state === "saving") return "儲存中";
404
+ if (status.state === "saved") return "已儲存";
405
+ if (status.state === "failed") return "儲存失敗";
406
+ return "";
407
+ }
@@ -0,0 +1,157 @@
1
+ import { useId, useState } from "react";
2
+ import { Check, Rocket } from "lucide-react";
3
+ import type { DeploymentInfo } from "../../document-model";
4
+ import { WorkbenchDialog } from "../dialog";
5
+ import type { DeployStatus } from "../workbenchTypes";
6
+ import {
7
+ deployButtonText,
8
+ deploymentStatusKind,
9
+ deploymentStatusSummary,
10
+ deploymentStatusText,
11
+ } from "./deploymentStatusModel";
12
+
13
+ export function DeploymentControl({
14
+ info,
15
+ status,
16
+ onDeploy,
17
+ }: {
18
+ info: DeploymentInfo;
19
+ status: DeployStatus;
20
+ onDeploy: () => void | Promise<void>;
21
+ }) {
22
+ const titleId = useId();
23
+ const [dialogOpen, setDialogOpen] = useState(false);
24
+ const kind = deploymentStatusKind(info, status);
25
+ const buttonText = deployButtonText(info, status);
26
+ const description = deploymentStatusText(info, status);
27
+ const summary = deploymentStatusSummary(info, status);
28
+ const sourceLabel = deploymentSourceLabel(info);
29
+ const busy = status === "deploying";
30
+ const confirmDisabled = busy || status === "unavailable" || info.configured === false;
31
+
32
+ const confirmDeploy = () => {
33
+ if (confirmDisabled) return;
34
+ setDialogOpen(false);
35
+ void onDeploy();
36
+ };
37
+
38
+ const dialog = dialogOpen ? (
39
+ <WorkbenchDialog
40
+ titleId={titleId}
41
+ title="部署資訊"
42
+ eyebrow="Deployment"
43
+ titleMeta={<span className="openpress-deploy-dialog__source">{sourceLabel}</span>}
44
+ className="openpress-deploy-dialog"
45
+ backdropClassName="openpress-deploy-dialog-backdrop"
46
+ closeLabel="關閉部署資訊"
47
+ onClose={() => setDialogOpen(false)}
48
+ footer={(
49
+ <>
50
+ <button type="button" onClick={() => setDialogOpen(false)}>取消</button>
51
+ <button type="button" disabled={confirmDisabled} onClick={confirmDeploy}>
52
+ <Check aria-hidden="true" />
53
+ <span>{busy ? "部署中" : "確認部署"}</span>
54
+ </button>
55
+ </>
56
+ )}
57
+ >
58
+ <dl data-openpress-deploy-align="left-values">
59
+ <DeployStatusRow label="狀態" value={summary} kind={kind} />
60
+ <DeployLinkRow label="公開頁面" url={info.publicUrl} />
61
+ <DeployLinkRow label="PDF" url={info.pdf} />
62
+ </dl>
63
+ {info.configured === false ? (
64
+ <p className="openpress-deploy-dialog__message" role="status">
65
+ {info.setupMessage ?? "部署設定尚未完成。"}
66
+ </p>
67
+ ) : null}
68
+ </WorkbenchDialog>
69
+ ) : null;
70
+
71
+ return (
72
+ <>
73
+ <button
74
+ type="button"
75
+ className="openpress-workbench-toolbar-action"
76
+ data-openpress-deploy
77
+ data-openpress-deploy-status={kind}
78
+ data-openpress-toolbar-expanded="false"
79
+ data-openpress-toolbar-active="false"
80
+ data-deploy-status={status}
81
+ aria-busy={busy ? "true" : "false"}
82
+ aria-label={buttonText}
83
+ title={description}
84
+ onClick={() => setDialogOpen(true)}
85
+ >
86
+ <Rocket aria-hidden="true" />
87
+ </button>
88
+ {busy ? (
89
+ <span
90
+ className="openpress-dev-deploy-status openpress-dev-deploy-status--toolbar"
91
+ data-openpress-deploy-status={kind}
92
+ role="status"
93
+ aria-live="polite"
94
+ >
95
+ <span className="openpress-dev-deploy-status__dot" aria-hidden="true" />
96
+ <span>部署中</span>
97
+ </span>
98
+ ) : null}
99
+ {dialog}
100
+ </>
101
+ );
102
+ }
103
+
104
+ function DeployStatusRow({ label, value, kind }: { label: string; value: string; kind: string }) {
105
+ return (
106
+ <div>
107
+ <dt>{label}</dt>
108
+ <dd>
109
+ <span className="openpress-deploy-dialog__status" data-openpress-deploy-status={kind}>
110
+ {value}
111
+ </span>
112
+ </dd>
113
+ </div>
114
+ );
115
+ }
116
+
117
+ function DeployLinkRow({ label, url }: { label: string; url?: string }) {
118
+ return (
119
+ <div>
120
+ <dt>{label}</dt>
121
+ <dd>
122
+ {url ? (
123
+ <a href={url} target="_blank" rel="noreferrer">
124
+ {formatDeployUrl(url)}
125
+ </a>
126
+ ) : (
127
+ "尚未產生"
128
+ )}
129
+ </dd>
130
+ </div>
131
+ );
132
+ }
133
+
134
+ function deploymentSourceLabel(info: DeploymentInfo) {
135
+ const adapter = info.adapter?.trim().toLowerCase();
136
+
137
+ if (adapter === "cloudflare-pages" || adapter === "cloudflare") return "Cloudflare Pages";
138
+ if (adapter === "github-pages" || adapter === "github") return "GitHub Pages";
139
+ if (adapter === "zeabur" || adapter === "zebur") return "Zeabur";
140
+
141
+ return info.projectName?.trim() || info.source?.trim() || info.adapter?.trim() || "本機工作區";
142
+ }
143
+
144
+ function formatDeployUrl(value: string) {
145
+ try {
146
+ const url = new URL(value);
147
+ const pathname = url.pathname === "/" ? "" : url.pathname.replace(/\/$/, "");
148
+ return shortenDeployUrl(`${url.host}${pathname}`);
149
+ } catch {
150
+ return shortenDeployUrl(value);
151
+ }
152
+ }
153
+
154
+ function shortenDeployUrl(value: string) {
155
+ if (value.length <= 48) return value;
156
+ return `${value.slice(0, 30)}...${value.slice(-14)}`;
157
+ }