@open-press/core 0.8.0 → 1.1.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 +17 -5
- package/engine/cli.mjs +9 -9
- package/engine/commands/_shared.mjs +70 -18
- package/engine/commands/deploy.mjs +3 -3
- package/engine/commands/dev.mjs +13 -4
- package/engine/commands/image.mjs +29 -0
- package/engine/commands/inspect.mjs +3 -2
- package/engine/commands/pdf.mjs +2 -2
- package/engine/commands/preview.mjs +2 -2
- package/engine/commands/render.mjs +6 -4
- package/engine/commands/replace.mjs +1 -1
- package/engine/commands/search.mjs +1 -1
- package/engine/commands/skills-sync.mjs +71 -0
- package/engine/commands/typecheck.mjs +71 -1
- package/engine/commands/upgrade.mjs +3 -3
- package/engine/document-export.mjs +1 -1
- package/engine/output/chrome-pdf.mjs +92 -0
- package/engine/output/static-server.mjs +60 -17
- package/engine/react/comment-marker.mjs +13 -13
- package/engine/react/document-entry.mjs +35 -28
- package/engine/react/document-export.mjs +309 -170
- package/engine/react/mdx-compile.mjs +30 -0
- package/engine/react/measurement-css.mjs +21 -0
- package/engine/react/object-entities.mjs +85 -0
- package/engine/react/pagination/allocator.mjs +48 -3
- package/engine/react/pagination.mjs +1 -1
- package/engine/react/pipeline/allocate.mjs +31 -65
- package/engine/react/pipeline/frame-measurement.mjs +4 -0
- package/engine/react/press-tree-inspection.mjs +172 -0
- package/engine/react/sources/mdx-resolver.mjs +1 -1
- package/engine/react/style-discovery.mjs +22 -4
- 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/page-geometry.mjs +131 -0
- package/engine/runtime/source-text-tools.mjs +1 -1
- package/engine/runtime/source-workspace.mjs +12 -3
- package/engine/runtime/validation.mjs +19 -10
- package/index.html +4 -0
- package/package.json +9 -12
- package/src/main.tsx +16 -0
- package/src/openpress/app/OpenPressApp.tsx +173 -17
- package/src/openpress/app/OpenPressRuntime.tsx +10 -2
- package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
- package/src/openpress/core/Frame.tsx +20 -7
- package/src/openpress/core/FrameContext.tsx +2 -0
- package/src/openpress/core/Press.tsx +25 -4
- package/src/openpress/core/Workspace.tsx +36 -0
- package/src/openpress/core/index.tsx +10 -3
- package/src/openpress/core/primitives.tsx +48 -1
- package/src/openpress/core/types.ts +86 -41
- package/src/openpress/core/useSource.ts +1 -1
- package/src/openpress/document-model/documentTypes.ts +9 -0
- package/src/openpress/document-model/index.ts +1 -0
- package/src/openpress/document-model/objectEntityModel.ts +4 -0
- package/src/openpress/document-model/workspaceManifestModel.ts +57 -0
- package/src/openpress/mdx/index.ts +15 -7
- package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
- package/src/openpress/reader/index.ts +1 -0
- package/src/openpress/workbench/Workbench.tsx +120 -21
- package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
- package/src/openpress/workbench/actions/SearchControl.tsx +3 -3
- package/src/openpress/workbench/actions/index.ts +1 -0
- package/src/openpress/workbench/inspector/useInspectorComments.ts +7 -1
- package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +5 -1
- package/src/openpress/workbench/project/ProjectEntryPanel.tsx +4 -2
- package/src/openpress/workbench/project/projectSourceModel.ts +2 -2
- package/src/openpress/workbench/workbenchFormatters.ts +2 -2
- package/src/styles/openpress/reader-runtime.css +9 -0
- package/src/styles/openpress/workbench-panels.css +113 -0
- package/src/styles/openpress/workspace-gallery.css +300 -0
- package/src/styles/openpress.css +1 -5
- package/src/vite-env.d.ts +8 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +6 -6
- package/engine/commands/init.mjs +0 -24
- package/engine/init.mjs +0 -90
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
useState,
|
|
5
5
|
type CSSProperties,
|
|
6
6
|
} from "react";
|
|
7
|
-
import { ExternalLink, MousePointer2, Ruler } from "lucide-react";
|
|
7
|
+
import { ExternalLink, Home, MousePointer2, Ruler } from "lucide-react";
|
|
8
8
|
import {
|
|
9
9
|
getProjectIdentity,
|
|
10
10
|
resolveAnchorPageIndex,
|
|
@@ -17,6 +17,7 @@ import { ProjectEntryPanel } from "./project";
|
|
|
17
17
|
import {
|
|
18
18
|
Bookmarks,
|
|
19
19
|
CurrentPagePanel,
|
|
20
|
+
PageThumbnails,
|
|
20
21
|
PUBLIC_DRAWER_BREAKPOINT,
|
|
21
22
|
PublicPage,
|
|
22
23
|
useReaderRuntime,
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
} from "./document";
|
|
35
36
|
import {
|
|
36
37
|
DeploymentControl,
|
|
38
|
+
ExportImageControl,
|
|
37
39
|
PageZoomControl,
|
|
38
40
|
SearchControl,
|
|
39
41
|
useDeploymentWorkbench,
|
|
@@ -52,6 +54,7 @@ export function HtmlWorkbench({
|
|
|
52
54
|
devMode,
|
|
53
55
|
deploymentInfo,
|
|
54
56
|
onDocumentRefresh,
|
|
57
|
+
onBackToWorkspace,
|
|
55
58
|
extraControlPanels,
|
|
56
59
|
}: {
|
|
57
60
|
document: ReaderDocument;
|
|
@@ -60,6 +63,7 @@ export function HtmlWorkbench({
|
|
|
60
63
|
devMode: boolean;
|
|
61
64
|
deploymentInfo: DeploymentInfo;
|
|
62
65
|
onDocumentRefresh?: () => void | Promise<void>;
|
|
66
|
+
onBackToWorkspace?: () => void;
|
|
63
67
|
// Append extra panels into the right-side control panel. Built-in panels
|
|
64
68
|
// (pending comments + project entry) render first; extra panels render
|
|
65
69
|
// after them in the supplied order.
|
|
@@ -103,8 +107,15 @@ export function HtmlWorkbench({
|
|
|
103
107
|
const inspectorToolbarExpanded = inspector.inspectorMode;
|
|
104
108
|
const editStatusMessage = formatInlineEditStatus(inlineEditStatus);
|
|
105
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;
|
|
106
117
|
useInlineDocumentEditor({
|
|
107
|
-
enabled:
|
|
118
|
+
enabled: inlineEditEnabled,
|
|
108
119
|
sourceContainerRef,
|
|
109
120
|
sourceBlockMap,
|
|
110
121
|
onStatusChange: setInlineEditStatus,
|
|
@@ -173,7 +184,12 @@ export function HtmlWorkbench({
|
|
|
173
184
|
]);
|
|
174
185
|
|
|
175
186
|
const currentSourcePath = displayPages[reader.currentPageIndex]?.source;
|
|
176
|
-
|
|
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[]>(() => [
|
|
177
193
|
{
|
|
178
194
|
id: "pending-comments",
|
|
179
195
|
render: () => (
|
|
@@ -198,13 +214,43 @@ export function HtmlWorkbench({
|
|
|
198
214
|
/>
|
|
199
215
|
),
|
|
200
216
|
},
|
|
201
|
-
]
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
);
|
|
205
233
|
|
|
206
|
-
|
|
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(() => (
|
|
207
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}
|
|
208
254
|
<div className="openpress-workbench-toolbar__group" aria-label="輸出">
|
|
209
255
|
<button
|
|
210
256
|
type="button"
|
|
@@ -231,6 +277,11 @@ export function HtmlWorkbench({
|
|
|
231
277
|
</span>
|
|
232
278
|
) : null}
|
|
233
279
|
</button>
|
|
280
|
+
<ExportImageControl
|
|
281
|
+
currentPageIndex={reader.currentPageIndex}
|
|
282
|
+
currentPageLabel={reader.currentPageLabel}
|
|
283
|
+
pressTitle={projectIdentity.name}
|
|
284
|
+
/>
|
|
234
285
|
</div>
|
|
235
286
|
<div className="openpress-workbench-toolbar__group openpress-workbench-toolbar__group--page" aria-label="頁面規格">
|
|
236
287
|
<button
|
|
@@ -307,7 +358,40 @@ export function HtmlWorkbench({
|
|
|
307
358
|
) : null}
|
|
308
359
|
</div>
|
|
309
360
|
</>
|
|
310
|
-
)
|
|
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
|
+
]);
|
|
311
395
|
|
|
312
396
|
return (
|
|
313
397
|
<WorkbenchShell
|
|
@@ -315,7 +399,7 @@ export function HtmlWorkbench({
|
|
|
315
399
|
devMode={devMode}
|
|
316
400
|
viewMode={viewMode}
|
|
317
401
|
inspectorMode={inspector.inspectorMode}
|
|
318
|
-
editMode={
|
|
402
|
+
editMode={inlineEditEnabled}
|
|
319
403
|
leftPanelOpen={reader.leftPanelOpen}
|
|
320
404
|
rightPanelOpen={reader.rightPanelOpen}
|
|
321
405
|
onToggleLeftPanel={reader.toggleLeftPanel}
|
|
@@ -334,20 +418,35 @@ export function HtmlWorkbench({
|
|
|
334
418
|
{projectIdentity.label ? <span>{projectIdentity.label}</span> : null}
|
|
335
419
|
</section>
|
|
336
420
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
<
|
|
344
|
-
|
|
345
|
-
|
|
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}
|
|
346
444
|
currentPageIndex={reader.currentPageIndex}
|
|
347
445
|
onSelectPage={selectWorkspacePage}
|
|
446
|
+
theme={document.theme}
|
|
348
447
|
/>
|
|
349
|
-
</
|
|
350
|
-
|
|
448
|
+
</section>
|
|
449
|
+
)}
|
|
351
450
|
<CurrentPagePanel
|
|
352
451
|
currentPageLabel={reader.currentPageLabel}
|
|
353
452
|
totalPageLabel={reader.totalPageLabel}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { Camera } from "lucide-react";
|
|
3
|
+
import { toPng } from "html-to-image";
|
|
4
|
+
|
|
5
|
+
type ExportStatus = "idle" | "exporting" | "done" | "error";
|
|
6
|
+
|
|
7
|
+
// Exports the currently visible page as a PNG. Locates the page DOM via
|
|
8
|
+
// the data-openpress-page-index attribute (set in PublicReaderPage) and
|
|
9
|
+
// hands it to html-to-image, then triggers a browser download.
|
|
10
|
+
//
|
|
11
|
+
// Lives in the workbench toolbar so it's reachable for any Press shape
|
|
12
|
+
// (manuscript / canvas / slide); for multi-page Press the user navigates
|
|
13
|
+
// to the page first, then exports.
|
|
14
|
+
export function ExportImageControl({
|
|
15
|
+
currentPageIndex,
|
|
16
|
+
currentPageLabel,
|
|
17
|
+
pressTitle,
|
|
18
|
+
}: {
|
|
19
|
+
currentPageIndex: number;
|
|
20
|
+
currentPageLabel: string;
|
|
21
|
+
pressTitle: string;
|
|
22
|
+
}) {
|
|
23
|
+
const [status, setStatus] = useState<ExportStatus>("idle");
|
|
24
|
+
|
|
25
|
+
const handleExport = useCallback(async () => {
|
|
26
|
+
if (status === "exporting") return;
|
|
27
|
+
setStatus("exporting");
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const pageEl = typeof window === "undefined"
|
|
31
|
+
? null
|
|
32
|
+
: window.document.querySelector<HTMLElement>(
|
|
33
|
+
`[data-openpress-page-index="${currentPageIndex}"]`,
|
|
34
|
+
);
|
|
35
|
+
if (!pageEl) throw new Error("找不到目前頁面");
|
|
36
|
+
|
|
37
|
+
// pixelRatio: 2 — retina-ish; keeps text crisp without blowing the file size.
|
|
38
|
+
// cacheBust: true — force re-fetch of images so stale CORS doesn't taint the canvas.
|
|
39
|
+
const dataUrl = await toPng(pageEl, {
|
|
40
|
+
pixelRatio: 2,
|
|
41
|
+
cacheBust: true,
|
|
42
|
+
backgroundColor: "#ffffff",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const safeTitle = sanitizeFilename(pressTitle) || "openpress";
|
|
46
|
+
const safePage = sanitizeFilename(currentPageLabel) || String(currentPageIndex + 1);
|
|
47
|
+
const link = window.document.createElement("a");
|
|
48
|
+
link.href = dataUrl;
|
|
49
|
+
link.download = `${safeTitle}-${safePage}.png`;
|
|
50
|
+
window.document.body.appendChild(link);
|
|
51
|
+
link.click();
|
|
52
|
+
link.remove();
|
|
53
|
+
|
|
54
|
+
setStatus("done");
|
|
55
|
+
window.setTimeout(() => setStatus("idle"), 1600);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error("[openpress] page PNG export failed", error);
|
|
58
|
+
setStatus("error");
|
|
59
|
+
window.setTimeout(() => setStatus("idle"), 2400);
|
|
60
|
+
}
|
|
61
|
+
}, [currentPageIndex, currentPageLabel, pressTitle, status]);
|
|
62
|
+
|
|
63
|
+
const label = status === "exporting"
|
|
64
|
+
? "匯出中…"
|
|
65
|
+
: status === "done"
|
|
66
|
+
? "已下載"
|
|
67
|
+
: status === "error"
|
|
68
|
+
? "匯出失敗"
|
|
69
|
+
: "PNG";
|
|
70
|
+
const title = "將目前頁面匯出為 PNG";
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
className="openpress-workbench-toolbar-action"
|
|
76
|
+
data-openpress-page-png-export
|
|
77
|
+
data-openpress-export-status={status}
|
|
78
|
+
disabled={status === "exporting"}
|
|
79
|
+
onClick={handleExport}
|
|
80
|
+
title={title}
|
|
81
|
+
aria-label={title}
|
|
82
|
+
>
|
|
83
|
+
<Camera aria-hidden="true" />
|
|
84
|
+
<span className="openpress-workbench-toolbar-action__label">{label}</span>
|
|
85
|
+
</button>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sanitizeFilename(value: string): string {
|
|
90
|
+
return value
|
|
91
|
+
.replace(/[\\/:*?"<>|]+/g, "-")
|
|
92
|
+
.replace(/\s+/g, "-")
|
|
93
|
+
.replace(/-+/g, "-")
|
|
94
|
+
.replace(/^-+|-+$/g, "")
|
|
95
|
+
.slice(0, 80);
|
|
96
|
+
}
|
|
@@ -336,10 +336,10 @@ function resolveSearchJumpTarget(
|
|
|
336
336
|
function sourcePathKeys(value: string) {
|
|
337
337
|
const normalized = value.trim().replaceAll("\\", "/").replace(/^\.\//, "");
|
|
338
338
|
const keys = [normalized];
|
|
339
|
-
if (normalized.startsWith("
|
|
340
|
-
keys.push(normalized.replace(/^
|
|
339
|
+
if (normalized.startsWith("press/")) {
|
|
340
|
+
keys.push(normalized.replace(/^press\//, ""));
|
|
341
341
|
} else {
|
|
342
|
-
keys.push(`
|
|
342
|
+
keys.push(`press/${normalized}`);
|
|
343
343
|
}
|
|
344
344
|
return Array.from(new Set(keys));
|
|
345
345
|
}
|
|
@@ -67,7 +67,13 @@ export function useInspectorComments({
|
|
|
67
67
|
|
|
68
68
|
const inspectorCommentDisabled =
|
|
69
69
|
!inspector.selectedBlock || !inspectorCommentText.trim() || inspectorCommentStatus === "submitting";
|
|
70
|
-
|
|
70
|
+
// Memoize the status message so its identity is stable while only
|
|
71
|
+
// composer text changes — the toolbar and other consumers that depend
|
|
72
|
+
// on it can then memoize without keystrokes invalidating their cache.
|
|
73
|
+
const inspectorCommentStatusMessage = useMemo(
|
|
74
|
+
() => formatInspectorCommentStatus(inspectorCommentStatus, inspectorCommentError),
|
|
75
|
+
[inspectorCommentStatus, inspectorCommentError],
|
|
76
|
+
);
|
|
71
77
|
|
|
72
78
|
const refreshPendingComments = useCallback(async () => {
|
|
73
79
|
if (!devMode) return;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { memo } from "react";
|
|
1
2
|
import { Trash2 } from "lucide-react";
|
|
2
3
|
import type { PendingComment } from "../inspector";
|
|
3
4
|
import {
|
|
@@ -7,7 +8,7 @@ import {
|
|
|
7
8
|
import type { PendingCommentsStatus } from "../workbenchTypes";
|
|
8
9
|
import { Panel } from "./Panel";
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
function PendingCommentsPanelImpl({
|
|
11
12
|
comments,
|
|
12
13
|
status,
|
|
13
14
|
error,
|
|
@@ -74,3 +75,6 @@ export function PendingCommentsPanel({
|
|
|
74
75
|
</Panel>
|
|
75
76
|
);
|
|
76
77
|
}
|
|
78
|
+
|
|
79
|
+
export const PendingCommentsPanel = memo(PendingCommentsPanelImpl);
|
|
80
|
+
PendingCommentsPanel.displayName = "PendingCommentsPanel";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, type CSSProperties } from "react";
|
|
1
|
+
import { memo, useState, type CSSProperties } from "react";
|
|
2
2
|
import { Component as ComponentIcon, Images, Palette, type LucideIcon } from "lucide-react";
|
|
3
3
|
import type { BookmarkItem, BookmarkSubItem, MediaAssetItem } from "../../document-model";
|
|
4
4
|
import { projectSourceDirectoryPath, PROJECT_SOURCES } from "./projectSourceModel";
|
|
@@ -77,7 +77,7 @@ export function createProjectMentionItems(
|
|
|
77
77
|
return [...PROJECT_SKILL_MENTIONS, ...referenceItems, ...mediaItems, ...componentItems];
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
function ProjectEntryPanelImpl({
|
|
81
81
|
mediaAssets,
|
|
82
82
|
componentUsages,
|
|
83
83
|
mentionItems,
|
|
@@ -164,6 +164,8 @@ export function ProjectEntryPanel({
|
|
|
164
164
|
);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
export const ProjectEntryPanel = memo(ProjectEntryPanelImpl);
|
|
168
|
+
ProjectEntryPanel.displayName = "ProjectEntryPanel";
|
|
167
169
|
|
|
168
170
|
export function ProjectPreviewPanel({
|
|
169
171
|
mediaAssets,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Source directory paths come from
|
|
2
|
-
//
|
|
1
|
+
// Source directory paths come from package-owned Vite build-time defines,
|
|
2
|
+
// which read the active workspace conventions and package.json config.
|
|
3
3
|
|
|
4
4
|
export const PROJECT_SOURCES = {
|
|
5
5
|
content: {
|
|
@@ -33,11 +33,11 @@ export function formatCommentsCount(count: number, status: PendingCommentsStatus
|
|
|
33
33
|
return `${count} 則待處理`;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export function formatPageGeometrySpec(theme?: Pick<Theme, "pageWidth" | "pageHeight">): PageGeometrySpec {
|
|
36
|
+
export function formatPageGeometrySpec(theme?: Pick<Theme, "pageLabel" | "pageWidth" | "pageHeight">): PageGeometrySpec {
|
|
37
37
|
const width = parseCssLength(theme?.pageWidth ?? DEFAULT_PAGE_GEOMETRY.pageWidth);
|
|
38
38
|
const height = parseCssLength(theme?.pageHeight ?? DEFAULT_PAGE_GEOMETRY.pageHeight);
|
|
39
39
|
const dimensions = formatLengthPair(width, height);
|
|
40
|
-
const label = pageGeometryLabel(width, height);
|
|
40
|
+
const label = theme?.pageLabel?.trim() || pageGeometryLabel(width, height);
|
|
41
41
|
|
|
42
42
|
return {
|
|
43
43
|
label,
|
|
@@ -61,6 +61,15 @@
|
|
|
61
61
|
white-space: nowrap;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
.openpress-reader-app[data-openpress-inspector-mode="on"] .openpress-html-page__html {
|
|
65
|
+
/* Inspector mode is a click-to-select-block flow. Disable text selection
|
|
66
|
+
across the rendered page so drag-selects / Cmd+A don't paint the whole
|
|
67
|
+
page with the browser ::selection color — most visible on covers and
|
|
68
|
+
back-covers where there's little text and lots of background. */
|
|
69
|
+
user-select: none;
|
|
70
|
+
-webkit-user-select: none;
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
.openpress-reader-app[data-openpress-inspector-mode="on"] .openpress-html-page__html [data-openpress-block-id] {
|
|
65
74
|
cursor: crosshair;
|
|
66
75
|
}
|
|
@@ -592,3 +592,116 @@
|
|
|
592
592
|
clip: rect(0 0 0 0);
|
|
593
593
|
white-space: nowrap;
|
|
594
594
|
}
|
|
595
|
+
|
|
596
|
+
/* Page thumbnails — left panel fallback when no MDX bookmarks exist
|
|
597
|
+
(canvas-style Press: social posts, slides). Renders the actual page
|
|
598
|
+
HTML scaled down so the user can navigate by visual recognition. */
|
|
599
|
+
.openpress-reader-app .openpress-panel-section--thumbnails {
|
|
600
|
+
display: flex;
|
|
601
|
+
flex-direction: column;
|
|
602
|
+
gap: 10px;
|
|
603
|
+
padding: 8px 14px 16px;
|
|
604
|
+
min-height: 0;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.openpress-reader-app .openpress-thumb-list {
|
|
608
|
+
display: flex;
|
|
609
|
+
flex-direction: column;
|
|
610
|
+
gap: 10px;
|
|
611
|
+
margin: 0;
|
|
612
|
+
padding: 0;
|
|
613
|
+
list-style: none;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.openpress-reader-app .openpress-thumb-card {
|
|
617
|
+
display: grid;
|
|
618
|
+
grid-template-columns: 20px minmax(0, 1fr);
|
|
619
|
+
align-items: stretch;
|
|
620
|
+
gap: 6px;
|
|
621
|
+
padding: 6px 8px 6px 0;
|
|
622
|
+
border: 1px solid rgb(242 242 240 / 12%);
|
|
623
|
+
border-radius: 7px;
|
|
624
|
+
background: rgb(20 20 20 / 50%);
|
|
625
|
+
color: inherit;
|
|
626
|
+
cursor: pointer;
|
|
627
|
+
text-align: left;
|
|
628
|
+
font: inherit;
|
|
629
|
+
overflow: hidden;
|
|
630
|
+
transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.openpress-reader-app .openpress-thumb-card:hover {
|
|
634
|
+
border-color: rgb(242 242 240 / 26%);
|
|
635
|
+
transform: translateY(-1px);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.openpress-reader-app .openpress-thumb-card.is-active {
|
|
639
|
+
border-color: var(--openpress-accent, #df4b21);
|
|
640
|
+
box-shadow: 0 0 0 1px var(--openpress-accent, #df4b21) inset;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
.openpress-reader-app .openpress-thumb-card__surface {
|
|
644
|
+
position: relative;
|
|
645
|
+
grid-column: 2;
|
|
646
|
+
grid-row: 1;
|
|
647
|
+
display: grid;
|
|
648
|
+
place-items: center;
|
|
649
|
+
width: 100%;
|
|
650
|
+
overflow: hidden;
|
|
651
|
+
background: #fff;
|
|
652
|
+
border: 1px solid rgb(242 242 240 / 12%);
|
|
653
|
+
border-radius: 4px;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.openpress-reader-app .openpress-thumb-card__page-host {
|
|
657
|
+
position: absolute;
|
|
658
|
+
inset: 0;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.openpress-reader-app .openpress-thumb-card__page-host .openpress-public-page {
|
|
662
|
+
display: block;
|
|
663
|
+
pointer-events: none;
|
|
664
|
+
user-select: none;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.openpress-reader-app .openpress-thumb-card__frame {
|
|
668
|
+
position: relative;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.openpress-reader-app .openpress-thumb-card__surface .openpress-html-page {
|
|
672
|
+
pointer-events: none;
|
|
673
|
+
user-select: none;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.openpress-reader-app .openpress-thumb-card__meta {
|
|
677
|
+
display: grid;
|
|
678
|
+
grid-column: 1;
|
|
679
|
+
grid-row: 1;
|
|
680
|
+
grid-template-rows: auto;
|
|
681
|
+
justify-items: center;
|
|
682
|
+
align-items: center;
|
|
683
|
+
padding: 0 0 1px;
|
|
684
|
+
font-size: 11px;
|
|
685
|
+
color: rgb(242 242 240 / 58%);
|
|
686
|
+
min-width: 0;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.openpress-reader-app .openpress-thumb-card__index {
|
|
690
|
+
font-family: var(--openpress-mono, ui-monospace, monospace);
|
|
691
|
+
font-size: 11px;
|
|
692
|
+
letter-spacing: 0;
|
|
693
|
+
color: rgb(242 242 240 / 68%);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
.openpress-reader-app .openpress-thumb-card.is-active .openpress-thumb-card__index {
|
|
697
|
+
color: var(--openpress-accent, #df4b21);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
.openpress-reader-app .openpress-thumb-card__title {
|
|
701
|
+
position: absolute;
|
|
702
|
+
width: 1px;
|
|
703
|
+
height: 1px;
|
|
704
|
+
overflow: hidden;
|
|
705
|
+
clip: rect(0 0 0 0);
|
|
706
|
+
white-space: nowrap;
|
|
707
|
+
}
|