@open-press/core 0.7.1 → 1.0.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.
- package/README.md +6 -3
- package/engine/cli.mjs +8 -8
- package/engine/commands/_shared.mjs +37 -15
- package/engine/commands/dev.mjs +2 -2
- package/engine/commands/image.mjs +29 -0
- package/engine/commands/skills-sync.mjs +71 -0
- package/engine/commands/typecheck.mjs +63 -1
- package/engine/commands/upgrade.mjs +3 -3
- package/engine/document-export.mjs +1 -1
- package/engine/output/chrome-pdf.mjs +110 -3
- package/engine/output/static-server.mjs +87 -9
- package/engine/react/comment-endpoint.mjs +13 -39
- package/engine/react/comment-marker.mjs +43 -19
- package/engine/react/document-entry.mjs +46 -28
- package/engine/react/document-export.mjs +328 -164
- package/engine/react/http-json.mjs +24 -0
- package/engine/react/mdx-compile.mjs +126 -3
- package/engine/react/measurement-css.mjs +114 -1
- package/engine/react/object-entities.mjs +204 -0
- package/engine/react/pagination/allocator.mjs +48 -3
- package/engine/react/pagination.mjs +1 -1
- package/engine/react/pipeline/allocate.mjs +41 -72
- package/engine/react/pipeline/frame-measurement.mjs +6 -0
- package/engine/react/press-tree-inspection.mjs +172 -0
- package/engine/react/project-asset-endpoint.mjs +6 -24
- package/engine/react/source-edit-endpoint.d.mts +10 -0
- package/engine/react/source-edit-endpoint.mjs +75 -0
- package/engine/react/sources/mdx-resolver.mjs +13 -15
- package/engine/react/style-discovery.mjs +23 -8
- package/engine/runtime/config.d.mts +8 -0
- package/engine/runtime/config.mjs +57 -60
- package/engine/runtime/file-utils.mjs +9 -1
- package/engine/runtime/file-walk.mjs +22 -0
- package/engine/runtime/inspection.mjs +1 -20
- package/engine/runtime/page-geometry.mjs +131 -0
- package/engine/runtime/path-utils.mjs +20 -0
- package/engine/runtime/source-text-tools.d.mts +102 -0
- package/engine/runtime/source-text-tools.mjs +551 -16
- package/engine/runtime/source-workspace.mjs +16 -34
- package/engine/runtime/validation.mjs +19 -10
- package/package.json +3 -5
- package/src/openpress/app/OpenPressApp.tsx +296 -0
- package/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +20 -9
- package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
- package/src/openpress/app/index.ts +2 -0
- package/src/openpress/core/Frame.tsx +26 -15
- package/src/openpress/core/FrameContext.tsx +10 -3
- package/src/openpress/core/MdxArea.tsx +11 -12
- package/src/openpress/core/Press.tsx +25 -4
- package/src/openpress/core/Workspace.tsx +36 -0
- package/src/openpress/core/cn.ts +4 -0
- package/src/openpress/core/index.tsx +11 -3
- package/src/openpress/core/primitives.tsx +74 -6
- package/src/openpress/core/types.ts +94 -41
- package/src/openpress/core/useSource.ts +1 -1
- package/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
- package/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
- package/src/openpress/{types.ts → document-model/documentTypes.ts} +51 -0
- package/src/openpress/document-model/index.ts +7 -0
- package/src/openpress/document-model/objectEntityModel.ts +55 -0
- package/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
- package/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
- package/src/openpress/document-model/workspaceManifestModel.ts +57 -0
- package/src/openpress/manuscript/index.tsx +49 -7
- package/src/openpress/mdx/index.ts +15 -7
- package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
- package/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
- package/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
- package/src/openpress/reader/index.ts +11 -0
- package/src/openpress/reader/pageViewportScaleModel.ts +73 -0
- package/src/openpress/reader/readerTypes.ts +4 -0
- package/src/openpress/reader/usePageViewportScale.ts +119 -0
- package/src/openpress/reader/usePanelState.ts +56 -0
- package/src/openpress/reader/useReaderHashSync.ts +61 -0
- package/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
- package/src/openpress/reader/useReaderRuntime.ts +146 -0
- package/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
- package/src/openpress/shared/Panel.tsx +77 -0
- package/src/openpress/shared/index.ts +4 -0
- package/src/openpress/shared/numberUtils.ts +3 -0
- package/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
- package/src/openpress/workbench/Workbench.tsx +506 -0
- package/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
- package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
- package/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
- package/src/openpress/workbench/actions/SearchControl.tsx +345 -0
- package/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
- package/src/openpress/workbench/actions/index.ts +6 -0
- package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
- package/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
- package/src/openpress/workbench/dialog/index.ts +1 -0
- package/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
- package/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
- package/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
- package/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
- package/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
- package/src/openpress/workbench/document/index.ts +10 -0
- package/src/openpress/workbench/index.ts +2 -0
- package/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
- package/src/openpress/workbench/inspector/index.ts +5 -0
- package/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
- package/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
- package/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
- package/src/openpress/workbench/inspector/useInspectorComments.ts +254 -0
- package/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
- package/src/openpress/workbench/mentions/index.ts +2 -0
- package/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
- package/src/openpress/workbench/panels/Panel.tsx +1 -0
- package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +80 -0
- package/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
- package/src/openpress/workbench/panels/index.ts +3 -0
- package/src/openpress/workbench/project/ProjectEntryPanel.tsx +525 -0
- package/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
- package/src/openpress/workbench/project/index.ts +2 -0
- package/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
- package/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
- package/src/openpress/workbench/shell/index.ts +1 -0
- package/src/openpress/workbench/workbenchFormatters.ts +120 -0
- package/src/openpress/workbench/workbenchTypes.ts +35 -0
- package/src/styles/openpress/print-route.css +0 -2
- package/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
- package/src/styles/openpress/public-viewer.css +25 -320
- package/src/styles/openpress/reader-runtime.css +252 -55
- package/src/styles/openpress/responsive.css +145 -270
- package/src/styles/openpress/workbench-panels.css +327 -178
- package/src/styles/openpress/workbench.css +986 -451
- package/src/styles/openpress/workspace-gallery.css +300 -0
- package/src/styles/openpress.css +2 -1
- package/tsconfig.json +1 -1
- package/vite.config.ts +50 -0
- package/engine/commands/init.mjs +0 -24
- package/engine/init.mjs +0 -90
- package/src/openpress/App.tsx +0 -127
- package/src/openpress/inspector.ts +0 -282
- package/src/openpress/projectWorkspace.tsx +0 -919
- package/src/openpress/readerRuntime.ts +0 -230
- package/src/openpress/workbench.tsx +0 -1265
- package/src/openpress/workbenchTypes.ts +0 -4
- /package/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
- /package/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
- /package/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
- /package/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
- /package/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
- /package/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useMemo,
|
|
3
|
+
useRef,
|
|
4
|
+
useState,
|
|
5
|
+
type CSSProperties,
|
|
6
|
+
} from "react";
|
|
7
|
+
import { ExternalLink, Home, 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
|
+
PageThumbnails,
|
|
21
|
+
PUBLIC_DRAWER_BREAKPOINT,
|
|
22
|
+
PublicPage,
|
|
23
|
+
useReaderRuntime,
|
|
24
|
+
usePageViewportScale,
|
|
25
|
+
useViewMode,
|
|
26
|
+
type PageLayoutMode,
|
|
27
|
+
} from "../reader";
|
|
28
|
+
import {
|
|
29
|
+
ReaderStage,
|
|
30
|
+
InlineSourceEditorLayer,
|
|
31
|
+
useDocumentWorkbenchModel,
|
|
32
|
+
useInlineDocumentEditor,
|
|
33
|
+
type InlineDocumentEditStatus,
|
|
34
|
+
type InlineDocumentSourceTarget,
|
|
35
|
+
} from "./document";
|
|
36
|
+
import {
|
|
37
|
+
DeploymentControl,
|
|
38
|
+
ExportImageControl,
|
|
39
|
+
PageZoomControl,
|
|
40
|
+
SearchControl,
|
|
41
|
+
useDeploymentWorkbench,
|
|
42
|
+
} from "./actions";
|
|
43
|
+
import { PendingCommentsPanel, WorkbenchControlPanel, type WorkbenchPanel } from "./panels";
|
|
44
|
+
import { WorkbenchShell } from "./shell";
|
|
45
|
+
import {
|
|
46
|
+
formatPageGeometrySpec,
|
|
47
|
+
formatInspectorSelection,
|
|
48
|
+
} from "./workbenchFormatters";
|
|
49
|
+
|
|
50
|
+
export function HtmlWorkbench({
|
|
51
|
+
document,
|
|
52
|
+
pages,
|
|
53
|
+
style,
|
|
54
|
+
devMode,
|
|
55
|
+
deploymentInfo,
|
|
56
|
+
onDocumentRefresh,
|
|
57
|
+
onBackToWorkspace,
|
|
58
|
+
extraControlPanels,
|
|
59
|
+
}: {
|
|
60
|
+
document: ReaderDocument;
|
|
61
|
+
pages: Array<HtmlPageBlock>;
|
|
62
|
+
style: CSSProperties;
|
|
63
|
+
devMode: boolean;
|
|
64
|
+
deploymentInfo: DeploymentInfo;
|
|
65
|
+
onDocumentRefresh?: () => void | Promise<void>;
|
|
66
|
+
onBackToWorkspace?: () => void;
|
|
67
|
+
// Append extra panels into the right-side control panel. Built-in panels
|
|
68
|
+
// (pending comments + project entry) render first; extra panels render
|
|
69
|
+
// after them in the supplied order.
|
|
70
|
+
extraControlPanels?: WorkbenchPanel[];
|
|
71
|
+
}) {
|
|
72
|
+
const sourceContainerRef = useRef<HTMLDivElement | null>(null);
|
|
73
|
+
const displayPages = pages;
|
|
74
|
+
const { viewMode } = useViewMode();
|
|
75
|
+
const {
|
|
76
|
+
mediaAssets,
|
|
77
|
+
anchorPageMap,
|
|
78
|
+
projectComponentUsages,
|
|
79
|
+
bookmarks,
|
|
80
|
+
sourceBlockMap,
|
|
81
|
+
sourceBlocksByPath,
|
|
82
|
+
projectMentionItems,
|
|
83
|
+
} = useDocumentWorkbenchModel(document, displayPages);
|
|
84
|
+
const inspector = useInspector(document, { enabled: devMode });
|
|
85
|
+
const reader = useReaderRuntime({
|
|
86
|
+
pageCount: Math.max(displayPages.length, 1),
|
|
87
|
+
leftPanelBreakpoint: PUBLIC_DRAWER_BREAKPOINT,
|
|
88
|
+
rightPanelBreakpoint: PUBLIC_DRAWER_BREAKPOINT,
|
|
89
|
+
});
|
|
90
|
+
const [pageLayoutMode, setPageLayoutMode] = useState<PageLayoutMode>("single");
|
|
91
|
+
const pageViewport = usePageViewportScale({
|
|
92
|
+
stageRef: reader.stageRef,
|
|
93
|
+
pageContainerRef: sourceContainerRef,
|
|
94
|
+
pageCount: displayPages.length,
|
|
95
|
+
layoutMode: pageLayoutMode,
|
|
96
|
+
});
|
|
97
|
+
const deployment = useDeploymentWorkbench({ deploymentInfo });
|
|
98
|
+
const [inlineEditStatus, setInlineEditStatus] = useState<InlineDocumentEditStatus>({ state: "idle" });
|
|
99
|
+
const [sourceEditorTarget, setSourceEditorTarget] = useState<InlineDocumentSourceTarget | null>(null);
|
|
100
|
+
|
|
101
|
+
const projectIdentity = getProjectIdentity(document.meta);
|
|
102
|
+
const pageGeometry = formatPageGeometrySpec(document.theme);
|
|
103
|
+
const inspectorSelectionLabel = formatInspectorSelection(
|
|
104
|
+
inspector.selectedBlock,
|
|
105
|
+
inspector.selectedObjectEntity,
|
|
106
|
+
);
|
|
107
|
+
const inspectorToolbarExpanded = inspector.inspectorMode;
|
|
108
|
+
const editStatusMessage = formatInlineEditStatus(inlineEditStatus);
|
|
109
|
+
|
|
110
|
+
// Inline source editing and inspector commenting are mutually exclusive
|
|
111
|
+
// interaction modes on the same blocks. While inspector mode is on, the
|
|
112
|
+
// user is selecting blocks to comment on — keeping contenteditable + the
|
|
113
|
+
// text cursor active would (a) show the I-beam instead of the inspector
|
|
114
|
+
// crosshair, (b) allow accidental text selection that paints the whole
|
|
115
|
+
// page (notably covers) with the browser ::selection color.
|
|
116
|
+
const inlineEditEnabled = devMode && !inspector.inspectorMode;
|
|
117
|
+
useInlineDocumentEditor({
|
|
118
|
+
enabled: inlineEditEnabled,
|
|
119
|
+
sourceContainerRef,
|
|
120
|
+
sourceBlockMap,
|
|
121
|
+
onStatusChange: setInlineEditStatus,
|
|
122
|
+
onOpenSourceBlock: setSourceEditorTarget,
|
|
123
|
+
onDocumentEdited: onDocumentRefresh,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const selectWorkspacePage = (pageIndex: number, options?: { behavior?: ScrollBehavior }) => {
|
|
127
|
+
reader.setPage(pageIndex, options);
|
|
128
|
+
if (
|
|
129
|
+
typeof window !== "undefined"
|
|
130
|
+
&& window.innerWidth < PUBLIC_DRAWER_BREAKPOINT
|
|
131
|
+
&& reader.rightPanelOpen
|
|
132
|
+
) {
|
|
133
|
+
reader.toggleRightPanel();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const selectWorkspaceAnchor = (anchorId: string, pageIndex?: number) => {
|
|
138
|
+
const targetPageIndex = resolveAnchorPageIndex(anchorPageMap, displayPages.length, anchorId, pageIndex);
|
|
139
|
+
if (targetPageIndex === null) return false;
|
|
140
|
+
selectWorkspacePage(targetPageIndex, { behavior: "smooth" });
|
|
141
|
+
return true;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const comments = useInspectorComments({
|
|
145
|
+
devMode,
|
|
146
|
+
inspector,
|
|
147
|
+
sourceBlockMap,
|
|
148
|
+
sourceBlocksByPath,
|
|
149
|
+
sourceContainerRef,
|
|
150
|
+
onSelectWorkspacePage: selectWorkspacePage,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Stabilize the controller objects so memoized InlineInspectorLayer can skip
|
|
154
|
+
// re-rendering when nothing observable changed.
|
|
155
|
+
const inspectorLayerComments = useMemo(() => ({
|
|
156
|
+
saved: comments.inlineSavedComments,
|
|
157
|
+
active: comments.activeInlineSavedComment ?? null,
|
|
158
|
+
status: comments.inspectorCommentStatus,
|
|
159
|
+
statusMessage: comments.inspectorCommentStatusMessage,
|
|
160
|
+
totalCount: comments.pendingComments.length,
|
|
161
|
+
onOpenSaved: comments.handleOpenInlineSavedComment,
|
|
162
|
+
onRemoveSaved: comments.handleRemoveInlineSavedComment,
|
|
163
|
+
}), [
|
|
164
|
+
comments.activeInlineSavedComment,
|
|
165
|
+
comments.handleOpenInlineSavedComment,
|
|
166
|
+
comments.handleRemoveInlineSavedComment,
|
|
167
|
+
comments.inlineSavedComments,
|
|
168
|
+
comments.inspectorCommentStatus,
|
|
169
|
+
comments.inspectorCommentStatusMessage,
|
|
170
|
+
comments.pendingComments.length,
|
|
171
|
+
]);
|
|
172
|
+
const inspectorLayerComposer = useMemo(() => ({
|
|
173
|
+
text: comments.inspectorCommentText,
|
|
174
|
+
submitDisabled: comments.inspectorCommentDisabled,
|
|
175
|
+
mentionItems: projectMentionItems,
|
|
176
|
+
onTextChange: comments.setInspectorCommentText,
|
|
177
|
+
onSubmit: comments.handleSubmitInspectorComment,
|
|
178
|
+
}), [
|
|
179
|
+
comments.handleSubmitInspectorComment,
|
|
180
|
+
comments.inspectorCommentDisabled,
|
|
181
|
+
comments.inspectorCommentText,
|
|
182
|
+
comments.setInspectorCommentText,
|
|
183
|
+
projectMentionItems,
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
const currentSourcePath = displayPages[reader.currentPageIndex]?.source;
|
|
187
|
+
// Stabilize the panel registry across keystrokes in the inspector
|
|
188
|
+
// composer. Without `useMemo` the registry array (and the JSX closures
|
|
189
|
+
// inside) would be recreated on every Workbench render, so typing a
|
|
190
|
+
// single character would force WorkbenchControlPanel + every panel to
|
|
191
|
+
// diff fresh React elements.
|
|
192
|
+
const builtInControlPanels = useMemo<WorkbenchPanel[]>(() => [
|
|
193
|
+
{
|
|
194
|
+
id: "pending-comments",
|
|
195
|
+
render: () => (
|
|
196
|
+
<PendingCommentsPanel
|
|
197
|
+
comments={comments.pendingComments}
|
|
198
|
+
status={comments.commentsStatus}
|
|
199
|
+
error={comments.commentsError}
|
|
200
|
+
onClear={comments.clearPendingComment}
|
|
201
|
+
onSelect={comments.handleSelectPendingComment}
|
|
202
|
+
/>
|
|
203
|
+
),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "project-entry",
|
|
207
|
+
render: () => (
|
|
208
|
+
<ProjectEntryPanel
|
|
209
|
+
mediaAssets={mediaAssets}
|
|
210
|
+
componentUsages={projectComponentUsages}
|
|
211
|
+
mentionItems={projectMentionItems}
|
|
212
|
+
currentSource={currentSourcePath}
|
|
213
|
+
onCommentSubmitted={comments.refreshPendingComments}
|
|
214
|
+
/>
|
|
215
|
+
),
|
|
216
|
+
},
|
|
217
|
+
], [
|
|
218
|
+
comments.clearPendingComment,
|
|
219
|
+
comments.commentsError,
|
|
220
|
+
comments.commentsStatus,
|
|
221
|
+
comments.handleSelectPendingComment,
|
|
222
|
+
comments.pendingComments,
|
|
223
|
+
comments.refreshPendingComments,
|
|
224
|
+
currentSourcePath,
|
|
225
|
+
mediaAssets,
|
|
226
|
+
projectComponentUsages,
|
|
227
|
+
projectMentionItems,
|
|
228
|
+
]);
|
|
229
|
+
const controlPanels = useMemo(
|
|
230
|
+
() => (extraControlPanels ? [...builtInControlPanels, ...extraControlPanels] : builtInControlPanels),
|
|
231
|
+
[builtInControlPanels, extraControlPanels],
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Memoize so composer keystrokes (which only flip `comments.inspectorCommentText`)
|
|
235
|
+
// don't rebuild the toolbar JSX. The toolbar depends on deploy/page/zoom
|
|
236
|
+
// state and inspector mode, but never on the composer draft text.
|
|
237
|
+
const toolbarActions = useMemo(() => (
|
|
238
|
+
<>
|
|
239
|
+
{onBackToWorkspace ? (
|
|
240
|
+
<div className="openpress-workbench-toolbar__group" aria-label="工作台導覽">
|
|
241
|
+
<button
|
|
242
|
+
type="button"
|
|
243
|
+
className="openpress-workbench-toolbar-action openpress-workbench-toolbar-action--back"
|
|
244
|
+
data-openpress-back-to-workspace
|
|
245
|
+
onClick={onBackToWorkspace}
|
|
246
|
+
title="回到工作台"
|
|
247
|
+
aria-label="回到工作台"
|
|
248
|
+
>
|
|
249
|
+
<Home aria-hidden="true" />
|
|
250
|
+
<span className="openpress-workbench-toolbar-action__label">工作台</span>
|
|
251
|
+
</button>
|
|
252
|
+
</div>
|
|
253
|
+
) : null}
|
|
254
|
+
<div className="openpress-workbench-toolbar__group" aria-label="輸出">
|
|
255
|
+
<button
|
|
256
|
+
type="button"
|
|
257
|
+
className="openpress-workbench-toolbar-action"
|
|
258
|
+
data-openpress-public-export
|
|
259
|
+
data-openpress-toolbar-expanded={deployment.pdfToolbarExpanded ? "true" : "false"}
|
|
260
|
+
data-openpress-toolbar-active={deployment.pdfToolbarExpanded ? "true" : "false"}
|
|
261
|
+
disabled={deployment.pdfButtonDisabled}
|
|
262
|
+
onClick={deployment.handleOpenWorkbenchPdf}
|
|
263
|
+
title={deployment.pdfButtonText}
|
|
264
|
+
aria-label={deployment.pdfButtonText}
|
|
265
|
+
>
|
|
266
|
+
<ExternalLink aria-hidden="true" />
|
|
267
|
+
<span className="openpress-workbench-toolbar-action__label">{deployment.pdfButtonText}</span>
|
|
268
|
+
{deployment.pdfStatusMessage ? (
|
|
269
|
+
<span
|
|
270
|
+
className="openpress-dev-pdf-status"
|
|
271
|
+
data-openpress-pdf-status={deployment.pdfActionStatus}
|
|
272
|
+
role="status"
|
|
273
|
+
aria-live="polite"
|
|
274
|
+
>
|
|
275
|
+
<span className="openpress-dev-pdf-status__spinner" aria-hidden="true" />
|
|
276
|
+
<span>{deployment.pdfStatusMessage}</span>
|
|
277
|
+
</span>
|
|
278
|
+
) : null}
|
|
279
|
+
</button>
|
|
280
|
+
<ExportImageControl
|
|
281
|
+
currentPageIndex={reader.currentPageIndex}
|
|
282
|
+
currentPageLabel={reader.currentPageLabel}
|
|
283
|
+
pressTitle={projectIdentity.name}
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
<div className="openpress-workbench-toolbar__group openpress-workbench-toolbar__group--page" aria-label="頁面規格">
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
className="openpress-workbench-page-geometry"
|
|
290
|
+
data-openpress-page-geometry
|
|
291
|
+
title={pageGeometry.title}
|
|
292
|
+
aria-label={`頁面規格 ${pageGeometry.title}`}
|
|
293
|
+
>
|
|
294
|
+
<Ruler aria-hidden="true" />
|
|
295
|
+
<span className="openpress-workbench-page-geometry__label">{pageGeometry.label}</span>
|
|
296
|
+
<span className="openpress-workbench-page-geometry__dimensions">{pageGeometry.dimensions}</span>
|
|
297
|
+
</button>
|
|
298
|
+
<PageZoomControl
|
|
299
|
+
scaleMode={pageViewport.scaleMode}
|
|
300
|
+
scaleLabel={pageViewport.scaleLabel}
|
|
301
|
+
pageLayoutMode={pageLayoutMode}
|
|
302
|
+
onScaleModeChange={pageViewport.setScaleMode}
|
|
303
|
+
onPageLayoutModeChange={setPageLayoutMode}
|
|
304
|
+
/>
|
|
305
|
+
</div>
|
|
306
|
+
<div className="openpress-workbench-toolbar__group openpress-workbench-toolbar__group--right" aria-label="工作台狀態與發布">
|
|
307
|
+
{devMode ? (
|
|
308
|
+
<SearchControl
|
|
309
|
+
sourceBlocksByPath={sourceBlocksByPath}
|
|
310
|
+
onSelectPage={selectWorkspacePage}
|
|
311
|
+
/>
|
|
312
|
+
) : null}
|
|
313
|
+
{devMode && editStatusMessage ? (
|
|
314
|
+
<span
|
|
315
|
+
className="openpress-dev-edit-status openpress-dev-edit-status--toolbar"
|
|
316
|
+
data-openpress-edit-status={inlineEditStatus.state}
|
|
317
|
+
role="status"
|
|
318
|
+
aria-live="polite"
|
|
319
|
+
>
|
|
320
|
+
{inlineEditStatus.state === "saving" ? <span className="openpress-dev-edit-status__spinner" aria-hidden="true" /> : null}
|
|
321
|
+
<span>{editStatusMessage}</span>
|
|
322
|
+
</span>
|
|
323
|
+
) : null}
|
|
324
|
+
{devMode ? (
|
|
325
|
+
<button
|
|
326
|
+
type="button"
|
|
327
|
+
className="openpress-workbench-toolbar-action"
|
|
328
|
+
data-openpress-inspector-toggle
|
|
329
|
+
data-openpress-inspector-active={inspector.inspectorMode ? "true" : "false"}
|
|
330
|
+
data-openpress-toolbar-expanded={inspectorToolbarExpanded ? "true" : "false"}
|
|
331
|
+
data-openpress-toolbar-active={inspectorToolbarExpanded ? "true" : "false"}
|
|
332
|
+
onClick={() => inspector.setInspectorMode(!inspector.inspectorMode)}
|
|
333
|
+
aria-pressed={inspector.inspectorMode}
|
|
334
|
+
title={inspector.inspectorMode ? "關閉註解" : "開啟註解"}
|
|
335
|
+
aria-label={inspector.inspectorMode ? "關閉註解" : "開啟註解"}
|
|
336
|
+
>
|
|
337
|
+
<MousePointer2 aria-hidden="true" />
|
|
338
|
+
<span className="openpress-workbench-toolbar-action__label">{inspector.inspectorMode ? "註解中" : "註解"}</span>
|
|
339
|
+
<span className="openpress-dev-inspector-status">{inspectorSelectionLabel}</span>
|
|
340
|
+
</button>
|
|
341
|
+
) : null}
|
|
342
|
+
{devMode && inspector.inspectorMode ? (
|
|
343
|
+
<span
|
|
344
|
+
className="openpress-dev-inspector-status"
|
|
345
|
+
role="status"
|
|
346
|
+
aria-live="polite"
|
|
347
|
+
data-openpress-inspector-comment-status={comments.inspectorCommentStatus}
|
|
348
|
+
>
|
|
349
|
+
{comments.inspectorCommentStatusMessage}
|
|
350
|
+
</span>
|
|
351
|
+
) : null}
|
|
352
|
+
{deployment.localDeployEnabled ? (
|
|
353
|
+
<DeploymentControl
|
|
354
|
+
info={deployment.currentDeploymentInfo}
|
|
355
|
+
status={deployment.status}
|
|
356
|
+
onDeploy={deployment.handleDeploy}
|
|
357
|
+
/>
|
|
358
|
+
) : null}
|
|
359
|
+
</div>
|
|
360
|
+
</>
|
|
361
|
+
), [
|
|
362
|
+
comments.inspectorCommentStatus,
|
|
363
|
+
comments.inspectorCommentStatusMessage,
|
|
364
|
+
deployment.currentDeploymentInfo,
|
|
365
|
+
deployment.handleDeploy,
|
|
366
|
+
deployment.handleOpenWorkbenchPdf,
|
|
367
|
+
deployment.localDeployEnabled,
|
|
368
|
+
deployment.pdfActionStatus,
|
|
369
|
+
deployment.pdfButtonDisabled,
|
|
370
|
+
deployment.pdfButtonText,
|
|
371
|
+
deployment.pdfStatusMessage,
|
|
372
|
+
deployment.pdfToolbarExpanded,
|
|
373
|
+
deployment.status,
|
|
374
|
+
devMode,
|
|
375
|
+
editStatusMessage,
|
|
376
|
+
inlineEditStatus.state,
|
|
377
|
+
inspector.inspectorMode,
|
|
378
|
+
inspector.setInspectorMode,
|
|
379
|
+
inspectorSelectionLabel,
|
|
380
|
+
inspectorToolbarExpanded,
|
|
381
|
+
pageGeometry.dimensions,
|
|
382
|
+
pageGeometry.label,
|
|
383
|
+
pageGeometry.title,
|
|
384
|
+
pageLayoutMode,
|
|
385
|
+
pageViewport.scaleLabel,
|
|
386
|
+
pageViewport.scaleMode,
|
|
387
|
+
pageViewport.setScaleMode,
|
|
388
|
+
selectWorkspacePage,
|
|
389
|
+
sourceBlocksByPath,
|
|
390
|
+
onBackToWorkspace,
|
|
391
|
+
reader.currentPageIndex,
|
|
392
|
+
reader.currentPageLabel,
|
|
393
|
+
projectIdentity.name,
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
<WorkbenchShell
|
|
398
|
+
style={style}
|
|
399
|
+
devMode={devMode}
|
|
400
|
+
viewMode={viewMode}
|
|
401
|
+
inspectorMode={inspector.inspectorMode}
|
|
402
|
+
editMode={inlineEditEnabled}
|
|
403
|
+
leftPanelOpen={reader.leftPanelOpen}
|
|
404
|
+
rightPanelOpen={reader.rightPanelOpen}
|
|
405
|
+
onToggleLeftPanel={reader.toggleLeftPanel}
|
|
406
|
+
onToggleRightPanel={reader.toggleRightPanel}
|
|
407
|
+
>
|
|
408
|
+
<WorkbenchShell.Toolbar>
|
|
409
|
+
{toolbarActions}
|
|
410
|
+
</WorkbenchShell.Toolbar>
|
|
411
|
+
|
|
412
|
+
<WorkbenchShell.LeftPanel>
|
|
413
|
+
<section className="openpress-public-identity" aria-label="文件資訊">
|
|
414
|
+
<strong>
|
|
415
|
+
<span className="openpress-public-title-main">{projectIdentity.name}</span>
|
|
416
|
+
{projectIdentity.subtitle ? <span className="openpress-public-title-sub">{projectIdentity.subtitle}</span> : null}
|
|
417
|
+
</strong>
|
|
418
|
+
{projectIdentity.label ? <span>{projectIdentity.label}</span> : null}
|
|
419
|
+
</section>
|
|
420
|
+
|
|
421
|
+
{bookmarks.length > 0 ? (
|
|
422
|
+
<section
|
|
423
|
+
id="openpress-bookmarks"
|
|
424
|
+
className="openpress-panel-section openpress-panel-section--bookmarks"
|
|
425
|
+
aria-label="章節書籤"
|
|
426
|
+
>
|
|
427
|
+
<nav className="reader-bookmarks" aria-label="章節導覽" data-openpress-react-bookmarks="true">
|
|
428
|
+
<div className="reader-bookmarks-rail" aria-hidden="true" />
|
|
429
|
+
<Bookmarks
|
|
430
|
+
items={bookmarks}
|
|
431
|
+
currentPageIndex={reader.currentPageIndex}
|
|
432
|
+
onSelectPage={selectWorkspacePage}
|
|
433
|
+
/>
|
|
434
|
+
</nav>
|
|
435
|
+
</section>
|
|
436
|
+
) : (
|
|
437
|
+
<section
|
|
438
|
+
id="openpress-thumbnails"
|
|
439
|
+
className="openpress-panel-section openpress-panel-section--thumbnails"
|
|
440
|
+
aria-label="頁面縮圖"
|
|
441
|
+
>
|
|
442
|
+
<PageThumbnails
|
|
443
|
+
pages={displayPages}
|
|
444
|
+
currentPageIndex={reader.currentPageIndex}
|
|
445
|
+
onSelectPage={selectWorkspacePage}
|
|
446
|
+
theme={document.theme}
|
|
447
|
+
/>
|
|
448
|
+
</section>
|
|
449
|
+
)}
|
|
450
|
+
<CurrentPagePanel
|
|
451
|
+
currentPageLabel={reader.currentPageLabel}
|
|
452
|
+
totalPageLabel={reader.totalPageLabel}
|
|
453
|
+
progressPercent={reader.progressPercent}
|
|
454
|
+
title={displayPages[reader.currentPageIndex]?.title || document.meta.title}
|
|
455
|
+
pageLabelPrefix="頁"
|
|
456
|
+
showHeading={false}
|
|
457
|
+
showTitle={false}
|
|
458
|
+
/>
|
|
459
|
+
</WorkbenchShell.LeftPanel>
|
|
460
|
+
|
|
461
|
+
<WorkbenchShell.RightPanel>
|
|
462
|
+
<WorkbenchControlPanel panels={controlPanels} />
|
|
463
|
+
</WorkbenchShell.RightPanel>
|
|
464
|
+
|
|
465
|
+
<WorkbenchShell.MainContent>
|
|
466
|
+
<ReaderStage ref={reader.stageRef}>
|
|
467
|
+
<PublicPage
|
|
468
|
+
pages={displayPages}
|
|
469
|
+
currentPageIndex={reader.currentPageIndex}
|
|
470
|
+
devMode={devMode}
|
|
471
|
+
sourceContainerRef={sourceContainerRef}
|
|
472
|
+
registerPage={reader.registerPage}
|
|
473
|
+
exposeSourceData={devMode}
|
|
474
|
+
inspector={inspector}
|
|
475
|
+
onInternalAnchorNavigate={selectWorkspaceAnchor}
|
|
476
|
+
pageLayoutMode={pageLayoutMode}
|
|
477
|
+
/>
|
|
478
|
+
{devMode ? (
|
|
479
|
+
<InlineInspectorLayer
|
|
480
|
+
sourceContainerRef={sourceContainerRef}
|
|
481
|
+
inspector={inspector}
|
|
482
|
+
comments={inspectorLayerComments}
|
|
483
|
+
composer={inspectorLayerComposer}
|
|
484
|
+
geometryVersion={`${pageViewport.scaleMode}:${pageViewport.scale}:${pageLayoutMode}`}
|
|
485
|
+
/>
|
|
486
|
+
) : null}
|
|
487
|
+
{devMode ? (
|
|
488
|
+
<InlineSourceEditorLayer
|
|
489
|
+
target={sourceEditorTarget}
|
|
490
|
+
onClose={() => setSourceEditorTarget(null)}
|
|
491
|
+
onStatusChange={setInlineEditStatus}
|
|
492
|
+
geometryVersion={`${pageViewport.scaleMode}:${pageViewport.scale}:${pageLayoutMode}`}
|
|
493
|
+
/>
|
|
494
|
+
) : null}
|
|
495
|
+
</ReaderStage>
|
|
496
|
+
</WorkbenchShell.MainContent>
|
|
497
|
+
</WorkbenchShell>
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function formatInlineEditStatus(status: InlineDocumentEditStatus) {
|
|
502
|
+
if (status.state === "saving") return "儲存中";
|
|
503
|
+
if (status.state === "saved") return "已儲存";
|
|
504
|
+
if (status.state === "failed") return "儲存失敗";
|
|
505
|
+
return "";
|
|
506
|
+
}
|
|
@@ -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
|
+
}
|