@open-press/core 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/engine/cli.mjs +1 -1
- package/engine/commands/_shared.mjs +10 -5
- package/engine/commands/deploy.mjs +19 -4
- package/engine/commands/typecheck.mjs +1 -1
- package/engine/document-export.mjs +1 -1
- package/engine/output/page-block.mjs +11 -2
- package/engine/output/public-assets.mjs +41 -6
- package/engine/output/static-server.mjs +84 -24
- package/engine/react/caption-numbering.mjs +2 -2
- package/engine/react/comment-marker.mjs +1 -2
- package/engine/react/document-entry.mjs +64 -11
- package/engine/react/document-export.d.mts +6 -0
- package/engine/react/document-export.mjs +158 -28
- package/engine/react/mdx-compile.mjs +4 -4
- package/engine/react/measurement-css.mjs +3 -3
- package/engine/react/page-folio.mjs +37 -0
- package/engine/react/pagination/allocator.mjs +4 -4
- package/engine/react/pipeline/frame-measurement.mjs +34 -16
- package/engine/react/press-tree-inspection.mjs +43 -13
- package/engine/react/project-asset-endpoint.mjs +45 -11
- package/engine/react/sources/heading-numbering.mjs +2 -2
- package/engine/react/sources/mdx-resolver.mjs +3 -3
- package/engine/react/style-discovery.mjs +60 -11
- package/engine/react/text-source-transform.mjs +18 -4
- package/engine/runtime/config.mjs +22 -22
- package/engine/runtime/file-utils.mjs +57 -13
- package/engine/runtime/inspection.mjs +40 -15
- package/engine/runtime/page-geometry.mjs +6 -6
- package/engine/runtime/source-text-tools.mjs +28 -4
- package/engine/runtime/source-workspace.mjs +6 -9
- package/engine/runtime/validation.mjs +42 -24
- package/package.json +1 -1
- package/src/openpress/app/OpenPressApp.tsx +10 -16
- package/src/openpress/app/OpenPressRuntime.tsx +29 -4
- package/src/openpress/app/WorkspaceGalleryPage.tsx +1 -1
- package/src/openpress/core/PageFolio.tsx +115 -0
- package/src/openpress/core/Press.tsx +5 -10
- package/src/openpress/core/Slide.tsx +11 -0
- package/src/openpress/core/index.tsx +4 -0
- package/src/openpress/core/types.ts +21 -13
- package/src/openpress/core/useSource.ts +1 -1
- package/src/openpress/document-model/workspaceManifestModel.ts +4 -9
- package/src/openpress/reader/PageThumbnailsPanel.tsx +28 -5
- package/src/openpress/reader/SlidePresentationPage.tsx +36 -19
- package/src/openpress/reader/SlidePublicPage.tsx +332 -0
- package/src/openpress/reader/index.ts +1 -0
- package/src/openpress/reader/pageViewportScaleModel.ts +5 -3
- package/src/openpress/reader/usePageViewportScale.ts +9 -5
- package/src/openpress/workbench/Workbench.tsx +46 -164
- package/src/openpress/workbench/actions/DeploymentControl.tsx +1 -1
- package/src/openpress/workbench/actions/ExportControl.tsx +267 -0
- package/src/openpress/workbench/actions/index.ts +1 -1
- package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +7 -2
- package/src/openpress/workbench/hooks/useWorkbenchNavigation.ts +42 -0
- package/src/openpress/workbench/project/ProjectEntryPanel.tsx +2 -278
- package/src/openpress/workbench/shell/WorkbenchToolbarActions.tsx +206 -0
- package/src/styles/openpress/app-shell.css +0 -83
- package/src/styles/openpress/print-route.css +1 -3
- package/src/styles/openpress/project-preview-panel.css +5 -783
- package/src/styles/openpress/public-viewer.css +7 -249
- package/src/styles/openpress/reader-runtime.css +0 -274
- package/src/styles/openpress/slide-presenter.css +150 -0
- package/src/styles/openpress/slide-public-viewer.css +222 -0
- package/src/styles/openpress/workbench-dialog.css +267 -0
- package/src/styles/openpress/workbench-export.css +154 -0
- package/src/styles/openpress/workbench-inline-editor.css +128 -0
- package/src/styles/openpress/workbench-panels.css +0 -88
- package/src/styles/openpress/workbench-search.css +257 -0
- package/src/styles/openpress/workbench-toolbar.css +422 -0
- package/src/styles/openpress/workbench.css +34 -1263
- package/src/styles/openpress/workspace-gallery.css +0 -5
- package/src/styles/openpress.css +7 -1
- package/vite.config.ts +98 -25
- package/src/openpress/workbench/actions/ExportImageControl.tsx +0 -96
- package/src/styles/openpress/media-workspace.css +0 -230
|
@@ -262,11 +262,6 @@
|
|
|
262
262
|
white-space: nowrap;
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
.openpress-workspace-gallery__dot {
|
|
266
|
-
color: color-mix(in srgb, var(--workspace-card-muted) 55%, transparent);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
.openpress-workspace-gallery__pages,
|
|
270
265
|
.openpress-workspace-gallery__geom {
|
|
271
266
|
color: var(--workspace-card-muted);
|
|
272
267
|
}
|
package/src/styles/openpress.css
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
@import "./openpress/app-shell.css";
|
|
2
2
|
@import "./openpress/workspace-gallery.css";
|
|
3
3
|
@import "./openpress/workbench.css";
|
|
4
|
+
@import "./openpress/slide-presenter.css";
|
|
5
|
+
@import "./openpress/workbench-toolbar.css";
|
|
6
|
+
@import "./openpress/slide-public-viewer.css";
|
|
7
|
+
@import "./openpress/workbench-inline-editor.css";
|
|
8
|
+
@import "./openpress/workbench-dialog.css";
|
|
9
|
+
@import "./openpress/workbench-search.css";
|
|
10
|
+
@import "./openpress/workbench-export.css";
|
|
4
11
|
@import "./openpress/workbench-panels.css";
|
|
5
12
|
@import "./openpress/project-preview-panel.css";
|
|
6
|
-
@import "./openpress/media-workspace.css";
|
|
7
13
|
@import "./openpress/reader-runtime.css";
|
|
8
14
|
@import "./openpress/public-viewer.css";
|
|
9
15
|
@import "./openpress/responsive.css";
|
package/vite.config.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { searchSourceText } from "./engine/runtime/source-text-tools.mjs";
|
|
|
10
10
|
import { handleCommentRequest } from "./engine/react/comment-endpoint.mjs";
|
|
11
11
|
import { handleProjectAssetRequest } from "./engine/react/project-asset-endpoint.mjs";
|
|
12
12
|
import { handleSourceEditRequest } from "./engine/react/source-edit-endpoint.mjs";
|
|
13
|
+
import { exportReactDocument } from "./engine/react/document-export.mjs";
|
|
13
14
|
|
|
14
15
|
const frameworkRoot = fileURLToPath(new URL("./", import.meta.url));
|
|
15
16
|
const workspaceRoot = process.env.OPENPRESS_WORKSPACE_ROOT
|
|
@@ -26,10 +27,7 @@ const openpressConfig = await loadConfig(workspaceRoot);
|
|
|
26
27
|
const outputDir = openpressConfig.paths.outputDir;
|
|
27
28
|
const reactDocumentRoot = openpressConfig.paths.documentRoot;
|
|
28
29
|
const reactDocumentComponentsRoot = openpressConfig.paths.componentsDir;
|
|
29
|
-
const
|
|
30
|
-
const activeContentDir = await fileExists(reactDocumentEntry)
|
|
31
|
-
? path.join(reactDocumentRoot, "chapters")
|
|
32
|
-
: openpressConfig.paths.sourceDir;
|
|
30
|
+
const activeContentDir = reactDocumentRoot;
|
|
33
31
|
|
|
34
32
|
// Workspace directories — Vite resolves these at build time so that
|
|
35
33
|
// `import.meta.glob("@workspace/content/**")` and friends follow the active
|
|
@@ -112,6 +110,11 @@ export default defineConfig({
|
|
|
112
110
|
});
|
|
113
111
|
|
|
114
112
|
function openpressLocalDeployPlugin() {
|
|
113
|
+
// Suppress auto-reload when source-edit endpoint triggers an export (avoids double reload).
|
|
114
|
+
let watcherSuppressedUntil = 0;
|
|
115
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
116
|
+
let exporting = false;
|
|
117
|
+
|
|
115
118
|
return {
|
|
116
119
|
name: "openpress-local-deploy-endpoint",
|
|
117
120
|
configureServer(server: { middlewares: { use: (path: string, handler: (req: IncomingMessage, res: ServerResponse) => void) => void } }) {
|
|
@@ -128,6 +131,7 @@ function openpressLocalDeployPlugin() {
|
|
|
128
131
|
void handleLocalSearchRequest(req, res);
|
|
129
132
|
});
|
|
130
133
|
server.middlewares.use("/__openpress/source-edit", (req, res) => {
|
|
134
|
+
if (req.method === "POST") watcherSuppressedUntil = Date.now() + 5000;
|
|
131
135
|
void handleSourceEditRequest(req, res, { root: workspaceRoot });
|
|
132
136
|
});
|
|
133
137
|
server.middlewares.use("/__openpress/deploy", (req, res) => {
|
|
@@ -146,6 +150,31 @@ function openpressLocalDeployPlugin() {
|
|
|
146
150
|
void handleLocalMediaFileRequest(req, res);
|
|
147
151
|
});
|
|
148
152
|
},
|
|
153
|
+
async handleHotUpdate({ file, server }: { file: string; server: { ws: { send: (payload: unknown) => void } } }) {
|
|
154
|
+
// Only react to changes inside the press/document directory.
|
|
155
|
+
const inDocumentRoot = file.startsWith(reactDocumentRoot + path.sep) || file === reactDocumentRoot;
|
|
156
|
+
const inContentDir = file.startsWith(activeContentDir + path.sep) || file === activeContentDir;
|
|
157
|
+
if (!inDocumentRoot && !inContentDir) return;
|
|
158
|
+
|
|
159
|
+
// Skip when source-edit already handled the export to avoid a double reload.
|
|
160
|
+
if (Date.now() < watcherSuppressedUntil) return [];
|
|
161
|
+
|
|
162
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
163
|
+
debounceTimer = setTimeout(async () => {
|
|
164
|
+
if (exporting) return;
|
|
165
|
+
exporting = true;
|
|
166
|
+
try {
|
|
167
|
+
await exportReactDocument(workspaceRoot, { syncAssets: false });
|
|
168
|
+
} catch {
|
|
169
|
+
// Export failure must not crash the dev server.
|
|
170
|
+
} finally {
|
|
171
|
+
exporting = false;
|
|
172
|
+
}
|
|
173
|
+
server.ws.send({ type: "full-reload" });
|
|
174
|
+
}, 300);
|
|
175
|
+
|
|
176
|
+
return []; // Suppress Vite's premature HMR until our export finishes.
|
|
177
|
+
},
|
|
149
178
|
};
|
|
150
179
|
}
|
|
151
180
|
|
|
@@ -205,14 +234,12 @@ async function handleLocalMediaFileRequest(req: IncomingMessage, res: ServerResp
|
|
|
205
234
|
writeJson(res, 404, { ok: false, message: "Media file not found." });
|
|
206
235
|
return;
|
|
207
236
|
}
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (!resolvedTarget.startsWith(`${mediaRoot}${path.sep}`) && resolvedTarget !== mediaRoot) {
|
|
212
|
-
writeJson(res, 403, { ok: false, message: "Forbidden." });
|
|
237
|
+
const mediaPath = await findLocalMediaFile(fileName);
|
|
238
|
+
if (!mediaPath) {
|
|
239
|
+
writeJson(res, 404, { ok: false, message: "Media file not found." });
|
|
213
240
|
return;
|
|
214
241
|
}
|
|
215
|
-
const body = await fs.readFile(
|
|
242
|
+
const body = await fs.readFile(mediaPath);
|
|
216
243
|
res.writeHead(200, {
|
|
217
244
|
"Content-Type": mediaMimeType(fileName),
|
|
218
245
|
"Cache-Control": "no-store",
|
|
@@ -361,6 +388,11 @@ async function handleLocalDeployRequest(req: IncomingMessage, res: ServerRespons
|
|
|
361
388
|
return;
|
|
362
389
|
}
|
|
363
390
|
|
|
391
|
+
const body = await readJsonRequestBody(req);
|
|
392
|
+
const slug = normalizePressSlug(body?.press);
|
|
393
|
+
const cliArgs = slug ? ["deploy", ".", "--confirm", "--press", slug] : ["deploy", ".", "--confirm"];
|
|
394
|
+
const pdfFilename = pressFilename(openpressConfig.pdf.filename, slug);
|
|
395
|
+
|
|
364
396
|
if (!isLocalDeployConfigured()) {
|
|
365
397
|
writeJson(res, 400, {
|
|
366
398
|
ok: false,
|
|
@@ -370,15 +402,15 @@ async function handleLocalDeployRequest(req: IncomingMessage, res: ServerRespons
|
|
|
370
402
|
deploy_adapter: openpressConfig.deploy.adapter,
|
|
371
403
|
deploy_source: openpressConfig.deploy.source,
|
|
372
404
|
deploy_project_name: openpressConfig.deploy.projectName,
|
|
373
|
-
command: openpressCliCommand(
|
|
405
|
+
command: openpressCliCommand(cliArgs),
|
|
374
406
|
});
|
|
375
407
|
return;
|
|
376
408
|
}
|
|
377
409
|
|
|
378
|
-
const result = await runLocalDeploy();
|
|
410
|
+
const result = await runLocalDeploy(slug);
|
|
379
411
|
const deployedUrl = extractDeployUrl(result.stdout);
|
|
380
412
|
if (result.code === 0 && deployedUrl) {
|
|
381
|
-
await writeLocalDeploymentPublicUrl(deployedUrl);
|
|
413
|
+
await writeLocalDeploymentPublicUrl(deployedUrl, pdfFilename);
|
|
382
414
|
}
|
|
383
415
|
const deploymentInfo = await readLocalDeploymentInfo();
|
|
384
416
|
const publicUrl = deployedUrl ?? deploymentInfo.public_url;
|
|
@@ -386,10 +418,10 @@ async function handleLocalDeployRequest(req: IncomingMessage, res: ServerRespons
|
|
|
386
418
|
ok: result.code === 0,
|
|
387
419
|
code: result.code,
|
|
388
420
|
deployed_at: deploymentInfo.deployed_at,
|
|
389
|
-
pdf: deployedUrl ? `${deployedUrl}/${
|
|
421
|
+
pdf: deployedUrl ? `${deployedUrl}/${pdfFilename}` : deploymentInfo.pdf,
|
|
390
422
|
public_url: publicUrl,
|
|
391
423
|
dirty: false,
|
|
392
|
-
command: openpressCliCommand(
|
|
424
|
+
command: openpressCliCommand(cliArgs),
|
|
393
425
|
stdout: result.stdout,
|
|
394
426
|
stderr: result.stderr,
|
|
395
427
|
});
|
|
@@ -420,9 +452,11 @@ function runLocalPdfExport(slug = "") {
|
|
|
420
452
|
});
|
|
421
453
|
}
|
|
422
454
|
|
|
423
|
-
function runLocalDeploy() {
|
|
455
|
+
function runLocalDeploy(slug = "") {
|
|
456
|
+
const args = [openpressCliPath, "deploy", ".", "--confirm"];
|
|
457
|
+
if (slug) args.push("--press", slug);
|
|
424
458
|
return new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => {
|
|
425
|
-
const child = spawn("node",
|
|
459
|
+
const child = spawn("node", args, {
|
|
426
460
|
cwd: workspaceRoot,
|
|
427
461
|
shell: false,
|
|
428
462
|
});
|
|
@@ -485,7 +519,7 @@ async function readLocalDeploymentInfo() {
|
|
|
485
519
|
}
|
|
486
520
|
}
|
|
487
521
|
|
|
488
|
-
async function writeLocalDeploymentPublicUrl(publicUrl: string) {
|
|
522
|
+
async function writeLocalDeploymentPublicUrl(publicUrl: string, pdfFilename = openpressConfig.pdf.filename) {
|
|
489
523
|
let deployConfig: Record<string, unknown> = {};
|
|
490
524
|
try {
|
|
491
525
|
deployConfig = JSON.parse(await fs.readFile(openpressConfig.paths.deployMetadata, "utf8")) as Record<string, unknown>;
|
|
@@ -495,7 +529,7 @@ async function writeLocalDeploymentPublicUrl(publicUrl: string) {
|
|
|
495
529
|
await fs.mkdir(path.dirname(openpressConfig.paths.deployMetadata), { recursive: true });
|
|
496
530
|
await fs.writeFile(
|
|
497
531
|
openpressConfig.paths.deployMetadata,
|
|
498
|
-
`${JSON.stringify({ ...deployConfig, pdf: `${publicUrl}/${
|
|
532
|
+
`${JSON.stringify({ ...deployConfig, pdf: `${publicUrl}/${pdfFilename}`, public_url: publicUrl }, null, 2)}\n`,
|
|
499
533
|
"utf8",
|
|
500
534
|
);
|
|
501
535
|
}
|
|
@@ -510,16 +544,11 @@ async function isLocalDeploymentDirty(deployedAt: string | undefined) {
|
|
|
510
544
|
|
|
511
545
|
function getLocalDeploymentSourcePaths() {
|
|
512
546
|
return [
|
|
513
|
-
openpressConfig.paths.
|
|
514
|
-
openpressConfig.paths.mediaDir,
|
|
515
|
-
openpressConfig.paths.themeDir,
|
|
516
|
-
openpressConfig.paths.designDoc,
|
|
517
|
-
openpressConfig.paths.componentsDir,
|
|
547
|
+
openpressConfig.paths.documentRoot,
|
|
518
548
|
path.join(frameworkRoot, "src"),
|
|
519
549
|
path.join(frameworkRoot, "index.html"),
|
|
520
550
|
path.join(frameworkRoot, "vite.config.ts"),
|
|
521
551
|
path.join(workspaceRoot, "package.json"),
|
|
522
|
-
path.join(workspaceRoot, "openpress.config.mjs"),
|
|
523
552
|
openpressConfig.configPath,
|
|
524
553
|
];
|
|
525
554
|
}
|
|
@@ -596,6 +625,50 @@ async function uniqueMediaFileName(mediaDir: string, fileName: string) {
|
|
|
596
625
|
return candidate;
|
|
597
626
|
}
|
|
598
627
|
|
|
628
|
+
async function findLocalMediaFile(fileName: string): Promise<string | null> {
|
|
629
|
+
for (const mediaRoot of await collectLocalMediaRoots()) {
|
|
630
|
+
const resolvedRoot = path.resolve(mediaRoot);
|
|
631
|
+
const candidate = path.resolve(mediaRoot, fileName);
|
|
632
|
+
if (!isInsideRoot(candidate, resolvedRoot)) continue;
|
|
633
|
+
if (await fileExists(candidate)) return candidate;
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function collectLocalMediaRoots(): Promise<string[]> {
|
|
639
|
+
const roots = [
|
|
640
|
+
openpressConfig.paths.mediaDir,
|
|
641
|
+
path.join(openpressConfig.paths.publicDir, "media"),
|
|
642
|
+
];
|
|
643
|
+
try {
|
|
644
|
+
const entries = await fs.readdir(openpressConfig.paths.documentRoot, { withFileTypes: true });
|
|
645
|
+
for (const entry of entries) {
|
|
646
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "shared") continue;
|
|
647
|
+
roots.push(path.join(openpressConfig.paths.documentRoot, entry.name, "media"));
|
|
648
|
+
}
|
|
649
|
+
} catch {
|
|
650
|
+
// Missing press/ is handled by the render/validate commands.
|
|
651
|
+
}
|
|
652
|
+
return uniquePaths(roots);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function uniquePaths(paths: string[]): string[] {
|
|
656
|
+
const out: string[] = [];
|
|
657
|
+
const seen = new Set<string>();
|
|
658
|
+
for (const candidate of paths) {
|
|
659
|
+
const normalized = path.resolve(candidate);
|
|
660
|
+
if (seen.has(normalized)) continue;
|
|
661
|
+
seen.add(normalized);
|
|
662
|
+
out.push(normalized);
|
|
663
|
+
}
|
|
664
|
+
return out;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function isInsideRoot(candidate: string, rootDir: string): boolean {
|
|
668
|
+
const relative = path.relative(rootDir, candidate);
|
|
669
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
670
|
+
}
|
|
671
|
+
|
|
599
672
|
function readRequestBuffer(req: IncomingMessage, maxBytes: number) {
|
|
600
673
|
return new Promise<Buffer>((resolve, reject) => {
|
|
601
674
|
const chunks: Buffer[] = [];
|
|
@@ -1,96 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
.openpress-media-assets {
|
|
2
|
-
display: grid;
|
|
3
|
-
grid-template-rows: auto minmax(120px, 1fr) auto auto auto;
|
|
4
|
-
min-height: 0;
|
|
5
|
-
overflow: hidden;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
.openpress-media-workspace-page {
|
|
9
|
-
position: absolute;
|
|
10
|
-
inset: 0;
|
|
11
|
-
z-index: 8;
|
|
12
|
-
padding: 28px clamp(24px, 4vw, 48px) 40px;
|
|
13
|
-
background: #141414;
|
|
14
|
-
color: #dfe1dd;
|
|
15
|
-
overflow: auto;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.openpress-media-workspace-header {
|
|
19
|
-
display: flex;
|
|
20
|
-
gap: 18px;
|
|
21
|
-
align-items: start;
|
|
22
|
-
justify-content: space-between;
|
|
23
|
-
border-bottom: 1px solid rgb(255 255 255 / 9%);
|
|
24
|
-
padding-bottom: 18px;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.openpress-media-workspace-header .openpress-panel-heading {
|
|
28
|
-
padding: 0 0 8px;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.openpress-media-workspace-header h2 {
|
|
32
|
-
margin: 0;
|
|
33
|
-
color: #f2f2f0;
|
|
34
|
-
font-size: 22px;
|
|
35
|
-
font-weight: 500;
|
|
36
|
-
line-height: 1.2;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.openpress-media-workspace-header p {
|
|
40
|
-
margin: 8px 0 0;
|
|
41
|
-
color: #858c93;
|
|
42
|
-
font-size: 12px;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.openpress-media-workspace-back {
|
|
46
|
-
flex: 0 0 auto;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.openpress-media-asset-list {
|
|
50
|
-
display: grid;
|
|
51
|
-
min-height: 0;
|
|
52
|
-
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
53
|
-
gap: 10px;
|
|
54
|
-
overflow: auto;
|
|
55
|
-
padding: 18px 0;
|
|
56
|
-
scrollbar-width: none;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.openpress-media-asset-list::-webkit-scrollbar {
|
|
60
|
-
width: 0;
|
|
61
|
-
height: 0;
|
|
62
|
-
display: none;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.openpress-media-asset {
|
|
66
|
-
display: grid;
|
|
67
|
-
grid-template-columns: 54px minmax(0, 1fr);
|
|
68
|
-
gap: 12px;
|
|
69
|
-
align-items: center;
|
|
70
|
-
min-height: 66px;
|
|
71
|
-
border: 1px solid rgb(255 255 255 / 9%);
|
|
72
|
-
padding: 10px;
|
|
73
|
-
background: rgb(255 255 255 / 3%);
|
|
74
|
-
color: #a2a8ae;
|
|
75
|
-
text-decoration: none;
|
|
76
|
-
transition:
|
|
77
|
-
border-color 160ms ease,
|
|
78
|
-
background 160ms ease,
|
|
79
|
-
color 160ms ease;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.openpress-media-asset:hover {
|
|
83
|
-
border-color: rgb(255 255 255 / 18%);
|
|
84
|
-
background: rgb(255 255 255 / 6%);
|
|
85
|
-
color: #f3f3ef;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.openpress-media-asset__thumb {
|
|
89
|
-
display: grid;
|
|
90
|
-
width: 54px;
|
|
91
|
-
height: 54px;
|
|
92
|
-
place-items: center;
|
|
93
|
-
overflow: hidden;
|
|
94
|
-
border: 1px solid rgb(255 255 255 / 10%);
|
|
95
|
-
background: #101010;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
.openpress-media-asset__thumb img {
|
|
99
|
-
display: block;
|
|
100
|
-
width: 100%;
|
|
101
|
-
height: 100%;
|
|
102
|
-
object-fit: cover;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.openpress-media-asset--svg .openpress-media-asset__thumb img {
|
|
106
|
-
object-fit: contain;
|
|
107
|
-
padding: 5px;
|
|
108
|
-
background: #f4f4f0;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
.openpress-media-asset__meta {
|
|
112
|
-
min-width: 0;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.openpress-media-asset__meta strong {
|
|
116
|
-
display: block;
|
|
117
|
-
overflow: hidden;
|
|
118
|
-
color: #e2e4e1;
|
|
119
|
-
font-size: 12px;
|
|
120
|
-
font-weight: 500;
|
|
121
|
-
line-height: 1.2;
|
|
122
|
-
text-overflow: ellipsis;
|
|
123
|
-
white-space: nowrap;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
.openpress-media-asset__meta small {
|
|
127
|
-
display: block;
|
|
128
|
-
overflow: hidden;
|
|
129
|
-
margin-top: 4px;
|
|
130
|
-
color: #6b7178;
|
|
131
|
-
font-size: 10px;
|
|
132
|
-
line-height: 1.2;
|
|
133
|
-
text-overflow: ellipsis;
|
|
134
|
-
white-space: nowrap;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
.openpress-media-dropzone {
|
|
138
|
-
display: grid;
|
|
139
|
-
gap: 7px;
|
|
140
|
-
margin: 0 0 14px;
|
|
141
|
-
border: 1px dashed rgb(255 255 255 / 18%);
|
|
142
|
-
padding: 12px;
|
|
143
|
-
background: rgb(255 255 255 / 2%);
|
|
144
|
-
color: #7f868d;
|
|
145
|
-
font-size: 11px;
|
|
146
|
-
line-height: 1.45;
|
|
147
|
-
transition:
|
|
148
|
-
border-color 160ms ease,
|
|
149
|
-
background 160ms ease,
|
|
150
|
-
color 160ms ease;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
.openpress-media-dropzone[data-drag-active="true"] {
|
|
154
|
-
border-color: rgb(223 75 33 / 70%);
|
|
155
|
-
background: rgb(223 75 33 / 9%);
|
|
156
|
-
color: #ece7df;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
.openpress-media-dropzone__action,
|
|
160
|
-
.openpress-staged-asset button {
|
|
161
|
-
width: fit-content;
|
|
162
|
-
border: 1px solid rgb(255 255 255 / 14%);
|
|
163
|
-
padding: 6px 9px;
|
|
164
|
-
background: rgb(255 255 255 / 4%);
|
|
165
|
-
color: #ececea;
|
|
166
|
-
font: inherit;
|
|
167
|
-
font-size: 11px;
|
|
168
|
-
cursor: pointer;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
.openpress-media-dropzone__action:hover,
|
|
172
|
-
.openpress-staged-asset button:hover {
|
|
173
|
-
border-color: rgb(255 255 255 / 24%);
|
|
174
|
-
background: rgb(255 255 255 / 8%);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
.openpress-staged-assets {
|
|
178
|
-
display: grid;
|
|
179
|
-
max-height: 180px;
|
|
180
|
-
gap: 8px;
|
|
181
|
-
overflow: auto;
|
|
182
|
-
padding: 0 0 12px;
|
|
183
|
-
scrollbar-width: none;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
.openpress-staged-asset {
|
|
187
|
-
display: grid;
|
|
188
|
-
grid-template-columns: 34px minmax(0, 1fr) auto;
|
|
189
|
-
gap: 9px;
|
|
190
|
-
align-items: center;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
.openpress-staged-asset img {
|
|
194
|
-
width: 34px;
|
|
195
|
-
height: 34px;
|
|
196
|
-
object-fit: cover;
|
|
197
|
-
border: 1px solid rgb(255 255 255 / 10%);
|
|
198
|
-
background: #111;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
.openpress-staged-asset span {
|
|
202
|
-
min-width: 0;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
.openpress-staged-asset strong,
|
|
206
|
-
.openpress-staged-asset small {
|
|
207
|
-
display: block;
|
|
208
|
-
overflow: hidden;
|
|
209
|
-
text-overflow: ellipsis;
|
|
210
|
-
white-space: nowrap;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
.openpress-staged-asset strong {
|
|
214
|
-
color: #dedfdd;
|
|
215
|
-
font-size: 11px;
|
|
216
|
-
font-weight: 500;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
.openpress-staged-asset small {
|
|
220
|
-
margin-top: 3px;
|
|
221
|
-
color: #70767d;
|
|
222
|
-
font-size: 10px;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
body::-webkit-scrollbar,
|
|
226
|
-
html::-webkit-scrollbar {
|
|
227
|
-
width: 0;
|
|
228
|
-
height: 0;
|
|
229
|
-
display: none;
|
|
230
|
-
}
|