@open-press/core 1.1.4 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/engine/cli.mjs +3 -3
  2. package/engine/commands/_shared.mjs +89 -13
  3. package/engine/commands/deploy.mjs +19 -4
  4. package/engine/commands/image.mjs +9 -3
  5. package/engine/commands/pdf.mjs +4 -1
  6. package/engine/output/chrome-pdf.mjs +102 -0
  7. package/engine/output/static-server.mjs +64 -17
  8. package/engine/react/document-export.mjs +22 -0
  9. package/package.json +1 -1
  10. package/src/openpress/app/OpenPressApp.tsx +5 -1
  11. package/src/openpress/app/OpenPressRuntime.tsx +85 -6
  12. package/src/openpress/reader/PageThumbnailsPanel.tsx +28 -5
  13. package/src/openpress/reader/PublicReaderPage.tsx +163 -74
  14. package/src/openpress/reader/SlidePresentationPage.tsx +37 -15
  15. package/src/openpress/reader/SlidePublicPage.tsx +332 -0
  16. package/src/openpress/reader/index.ts +1 -0
  17. package/src/openpress/reader/pageViewportScaleModel.ts +5 -3
  18. package/src/openpress/reader/usePageViewportScale.ts +9 -5
  19. package/src/openpress/reader/usePanelState.ts +14 -5
  20. package/src/openpress/shared/index.ts +1 -0
  21. package/src/openpress/shared/staticSearch.ts +174 -0
  22. package/src/openpress/workbench/Workbench.tsx +61 -176
  23. package/src/openpress/workbench/actions/DeploymentControl.tsx +1 -1
  24. package/src/openpress/workbench/actions/ExportControl.tsx +267 -0
  25. package/src/openpress/workbench/actions/SearchControl.tsx +32 -43
  26. package/src/openpress/workbench/actions/index.ts +1 -1
  27. package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +21 -5
  28. package/src/openpress/workbench/hooks/useWorkbenchNavigation.ts +42 -0
  29. package/src/openpress/workbench/inspector/useInspectorComments.ts +6 -6
  30. package/src/openpress/workbench/project/ProjectEntryPanel.tsx +2 -278
  31. package/src/openpress/workbench/shell/WorkbenchShell.tsx +44 -18
  32. package/src/openpress/workbench/shell/WorkbenchToolbarActions.tsx +206 -0
  33. package/src/styles/openpress/app-shell.css +0 -83
  34. package/src/styles/openpress/print-route.css +1 -3
  35. package/src/styles/openpress/project-preview-panel.css +5 -783
  36. package/src/styles/openpress/public-viewer.css +7 -249
  37. package/src/styles/openpress/reader-runtime.css +0 -274
  38. package/src/styles/openpress/slide-presenter.css +150 -0
  39. package/src/styles/openpress/slide-public-viewer.css +222 -0
  40. package/src/styles/openpress/workbench-dialog.css +267 -0
  41. package/src/styles/openpress/workbench-export.css +154 -0
  42. package/src/styles/openpress/workbench-inline-editor.css +128 -0
  43. package/src/styles/openpress/workbench-panels.css +0 -88
  44. package/src/styles/openpress/workbench-search.css +257 -0
  45. package/src/styles/openpress/workbench-toolbar.css +422 -0
  46. package/src/styles/openpress/workbench.css +34 -1263
  47. package/src/styles/openpress/workspace-gallery.css +0 -5
  48. package/src/styles/openpress.css +7 -1
  49. package/vite.config.ts +66 -17
  50. package/src/openpress/workbench/actions/ExportImageControl.tsx +0 -96
  51. 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
  }
@@ -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
@@ -233,13 +233,18 @@ async function handleLocalPdfExportRequest(req: IncomingMessage, res: ServerResp
233
233
  return;
234
234
  }
235
235
 
236
- const result = await runLocalPdfExport();
237
- const exists = await fileExists(openpressConfig.paths.pdf);
236
+ const body = await readJsonRequestBody(req);
237
+ const slug = normalizePressSlug(body?.press);
238
+ const result = await runLocalPdfExport(slug);
239
+ const pdfPath = pressPdfAbsolutePath(slug);
240
+ const exists = await fileExists(pdfPath);
241
+ const cliArgs = slug ? ["pdf", ".", "--press", slug] : ["pdf", "."];
242
+ const pdfUrl = `/__openpress/local-pdf-file?${slug ? `press=${encodeURIComponent(slug)}&` : ""}ts=${Date.now()}`;
238
243
  writeJson(res, result.code === 0 && exists ? 200 : 500, {
239
244
  ok: result.code === 0 && exists,
240
245
  code: result.code,
241
- pdf: `/__openpress/local-pdf-file?ts=${Date.now()}`,
242
- command: openpressCliCommand(["pdf", "."]),
246
+ pdf: pdfUrl,
247
+ command: openpressCliCommand(cliArgs),
243
248
  stdout: result.stdout,
244
249
  stderr: result.stderr,
245
250
  });
@@ -251,11 +256,15 @@ async function handleLocalPdfFileRequest(req: IncomingMessage, res: ServerRespon
251
256
  return;
252
257
  }
253
258
 
259
+ const requestUrl = new URL(req.url ?? "/", "http://localhost");
260
+ const slug = normalizePressSlug(requestUrl.searchParams.get("press"));
261
+ const pdfPath = pressPdfAbsolutePath(slug);
262
+ const filename = pressFilename(openpressConfig.pdf.filename, slug);
254
263
  try {
255
- const body = await fs.readFile(openpressConfig.paths.pdf);
264
+ const body = await fs.readFile(pdfPath);
256
265
  res.writeHead(200, {
257
266
  "Content-Type": "application/pdf",
258
- "Content-Disposition": `inline; filename="${openpressConfig.pdf.filename}"`,
267
+ "Content-Disposition": `inline; filename="${filename}"`,
259
268
  "Cache-Control": "no-store",
260
269
  });
261
270
  res.end(body);
@@ -264,6 +273,37 @@ async function handleLocalPdfFileRequest(req: IncomingMessage, res: ServerRespon
264
273
  }
265
274
  }
266
275
 
276
+ function normalizePressSlug(value: unknown): string {
277
+ if (typeof value !== "string") return "";
278
+ return value.trim().replace(/^\/+|\/+$/g, "");
279
+ }
280
+
281
+ function pressFilename(baseFilename: string, slug: string): string {
282
+ if (!slug) return baseFilename;
283
+ const ext = path.extname(baseFilename);
284
+ const stem = ext ? baseFilename.slice(0, -ext.length) : baseFilename;
285
+ return `${stem}-${slug}${ext}`;
286
+ }
287
+
288
+ function pressPdfAbsolutePath(slug: string): string {
289
+ return path.join(openpressConfig.outputDir, pressFilename(openpressConfig.pdf.filename, slug));
290
+ }
291
+
292
+ async function readJsonRequestBody(req: IncomingMessage): Promise<{ press?: unknown } | null> {
293
+ try {
294
+ const chunks: Buffer[] = [];
295
+ for await (const chunk of req) {
296
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : (chunk as Buffer));
297
+ }
298
+ if (chunks.length === 0) return null;
299
+ const text = Buffer.concat(chunks).toString("utf8");
300
+ if (!text.trim()) return null;
301
+ return JSON.parse(text);
302
+ } catch {
303
+ return null;
304
+ }
305
+ }
306
+
267
307
  async function handleLocalStatusRequest(req: IncomingMessage, res: ServerResponse) {
268
308
  if (req.method !== "GET") {
269
309
  writeJson(res, 405, { ok: false, message: "Status endpoint requires GET." });
@@ -321,6 +361,11 @@ async function handleLocalDeployRequest(req: IncomingMessage, res: ServerRespons
321
361
  return;
322
362
  }
323
363
 
364
+ const body = await readJsonRequestBody(req);
365
+ const slug = normalizePressSlug(body?.press);
366
+ const cliArgs = slug ? ["deploy", ".", "--confirm", "--press", slug] : ["deploy", ".", "--confirm"];
367
+ const pdfFilename = pressFilename(openpressConfig.pdf.filename, slug);
368
+
324
369
  if (!isLocalDeployConfigured()) {
325
370
  writeJson(res, 400, {
326
371
  ok: false,
@@ -330,15 +375,15 @@ async function handleLocalDeployRequest(req: IncomingMessage, res: ServerRespons
330
375
  deploy_adapter: openpressConfig.deploy.adapter,
331
376
  deploy_source: openpressConfig.deploy.source,
332
377
  deploy_project_name: openpressConfig.deploy.projectName,
333
- command: openpressCliCommand(["deploy", ".", "--confirm"]),
378
+ command: openpressCliCommand(cliArgs),
334
379
  });
335
380
  return;
336
381
  }
337
382
 
338
- const result = await runLocalDeploy();
383
+ const result = await runLocalDeploy(slug);
339
384
  const deployedUrl = extractDeployUrl(result.stdout);
340
385
  if (result.code === 0 && deployedUrl) {
341
- await writeLocalDeploymentPublicUrl(deployedUrl);
386
+ await writeLocalDeploymentPublicUrl(deployedUrl, pdfFilename);
342
387
  }
343
388
  const deploymentInfo = await readLocalDeploymentInfo();
344
389
  const publicUrl = deployedUrl ?? deploymentInfo.public_url;
@@ -346,18 +391,20 @@ async function handleLocalDeployRequest(req: IncomingMessage, res: ServerRespons
346
391
  ok: result.code === 0,
347
392
  code: result.code,
348
393
  deployed_at: deploymentInfo.deployed_at,
349
- pdf: deployedUrl ? `${deployedUrl}/${openpressConfig.pdf.filename}` : deploymentInfo.pdf,
394
+ pdf: deployedUrl ? `${deployedUrl}/${pdfFilename}` : deploymentInfo.pdf,
350
395
  public_url: publicUrl,
351
396
  dirty: false,
352
- command: openpressCliCommand(["deploy", ".", "--confirm"]),
397
+ command: openpressCliCommand(cliArgs),
353
398
  stdout: result.stdout,
354
399
  stderr: result.stderr,
355
400
  });
356
401
  }
357
402
 
358
- function runLocalPdfExport() {
403
+ function runLocalPdfExport(slug = "") {
404
+ const args = [openpressCliPath, "pdf", "."];
405
+ if (slug) args.push("--press", slug);
359
406
  return new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => {
360
- const child = spawn("node", [openpressCliPath, "pdf", "."], {
407
+ const child = spawn("node", args, {
361
408
  cwd: workspaceRoot,
362
409
  shell: false,
363
410
  });
@@ -378,9 +425,11 @@ function runLocalPdfExport() {
378
425
  });
379
426
  }
380
427
 
381
- function runLocalDeploy() {
428
+ function runLocalDeploy(slug = "") {
429
+ const args = [openpressCliPath, "deploy", ".", "--confirm"];
430
+ if (slug) args.push("--press", slug);
382
431
  return new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => {
383
- const child = spawn("node", [openpressCliPath, "deploy", ".", "--confirm"], {
432
+ const child = spawn("node", args, {
384
433
  cwd: workspaceRoot,
385
434
  shell: false,
386
435
  });
@@ -443,7 +492,7 @@ async function readLocalDeploymentInfo() {
443
492
  }
444
493
  }
445
494
 
446
- async function writeLocalDeploymentPublicUrl(publicUrl: string) {
495
+ async function writeLocalDeploymentPublicUrl(publicUrl: string, pdfFilename = openpressConfig.pdf.filename) {
447
496
  let deployConfig: Record<string, unknown> = {};
448
497
  try {
449
498
  deployConfig = JSON.parse(await fs.readFile(openpressConfig.paths.deployMetadata, "utf8")) as Record<string, unknown>;
@@ -453,7 +502,7 @@ async function writeLocalDeploymentPublicUrl(publicUrl: string) {
453
502
  await fs.mkdir(path.dirname(openpressConfig.paths.deployMetadata), { recursive: true });
454
503
  await fs.writeFile(
455
504
  openpressConfig.paths.deployMetadata,
456
- `${JSON.stringify({ ...deployConfig, pdf: `${publicUrl}/${openpressConfig.pdf.filename}`, public_url: publicUrl }, null, 2)}\n`,
505
+ `${JSON.stringify({ ...deployConfig, pdf: `${publicUrl}/${pdfFilename}`, public_url: publicUrl }, null, 2)}\n`,
457
506
  "utf8",
458
507
  );
459
508
  }
@@ -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
- }