@open-press/core 1.2.0 → 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 (40) hide show
  1. package/engine/cli.mjs +1 -1
  2. package/engine/commands/_shared.mjs +10 -5
  3. package/engine/commands/deploy.mjs +19 -4
  4. package/engine/output/static-server.mjs +16 -9
  5. package/package.json +1 -1
  6. package/src/openpress/app/OpenPressApp.tsx +4 -1
  7. package/src/openpress/app/OpenPressRuntime.tsx +26 -1
  8. package/src/openpress/reader/PageThumbnailsPanel.tsx +28 -5
  9. package/src/openpress/reader/SlidePresentationPage.tsx +36 -19
  10. package/src/openpress/reader/SlidePublicPage.tsx +332 -0
  11. package/src/openpress/reader/index.ts +1 -0
  12. package/src/openpress/reader/pageViewportScaleModel.ts +5 -3
  13. package/src/openpress/reader/usePageViewportScale.ts +9 -5
  14. package/src/openpress/workbench/Workbench.tsx +46 -164
  15. package/src/openpress/workbench/actions/DeploymentControl.tsx +1 -1
  16. package/src/openpress/workbench/actions/ExportControl.tsx +267 -0
  17. package/src/openpress/workbench/actions/index.ts +1 -1
  18. package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +7 -2
  19. package/src/openpress/workbench/hooks/useWorkbenchNavigation.ts +42 -0
  20. package/src/openpress/workbench/project/ProjectEntryPanel.tsx +2 -278
  21. package/src/openpress/workbench/shell/WorkbenchToolbarActions.tsx +206 -0
  22. package/src/styles/openpress/app-shell.css +0 -83
  23. package/src/styles/openpress/print-route.css +1 -3
  24. package/src/styles/openpress/project-preview-panel.css +5 -783
  25. package/src/styles/openpress/public-viewer.css +7 -249
  26. package/src/styles/openpress/reader-runtime.css +0 -274
  27. package/src/styles/openpress/slide-presenter.css +150 -0
  28. package/src/styles/openpress/slide-public-viewer.css +222 -0
  29. package/src/styles/openpress/workbench-dialog.css +267 -0
  30. package/src/styles/openpress/workbench-export.css +154 -0
  31. package/src/styles/openpress/workbench-inline-editor.css +128 -0
  32. package/src/styles/openpress/workbench-panels.css +0 -88
  33. package/src/styles/openpress/workbench-search.css +257 -0
  34. package/src/styles/openpress/workbench-toolbar.css +422 -0
  35. package/src/styles/openpress/workbench.css +34 -1263
  36. package/src/styles/openpress/workspace-gallery.css +0 -5
  37. package/src/styles/openpress.css +7 -1
  38. package/vite.config.ts +16 -9
  39. package/src/openpress/workbench/actions/ExportImageControl.tsx +0 -96
  40. 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
@@ -361,6 +361,11 @@ async function handleLocalDeployRequest(req: IncomingMessage, res: ServerRespons
361
361
  return;
362
362
  }
363
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
+
364
369
  if (!isLocalDeployConfigured()) {
365
370
  writeJson(res, 400, {
366
371
  ok: false,
@@ -370,15 +375,15 @@ async function handleLocalDeployRequest(req: IncomingMessage, res: ServerRespons
370
375
  deploy_adapter: openpressConfig.deploy.adapter,
371
376
  deploy_source: openpressConfig.deploy.source,
372
377
  deploy_project_name: openpressConfig.deploy.projectName,
373
- command: openpressCliCommand(["deploy", ".", "--confirm"]),
378
+ command: openpressCliCommand(cliArgs),
374
379
  });
375
380
  return;
376
381
  }
377
382
 
378
- const result = await runLocalDeploy();
383
+ const result = await runLocalDeploy(slug);
379
384
  const deployedUrl = extractDeployUrl(result.stdout);
380
385
  if (result.code === 0 && deployedUrl) {
381
- await writeLocalDeploymentPublicUrl(deployedUrl);
386
+ await writeLocalDeploymentPublicUrl(deployedUrl, pdfFilename);
382
387
  }
383
388
  const deploymentInfo = await readLocalDeploymentInfo();
384
389
  const publicUrl = deployedUrl ?? deploymentInfo.public_url;
@@ -386,10 +391,10 @@ async function handleLocalDeployRequest(req: IncomingMessage, res: ServerRespons
386
391
  ok: result.code === 0,
387
392
  code: result.code,
388
393
  deployed_at: deploymentInfo.deployed_at,
389
- pdf: deployedUrl ? `${deployedUrl}/${openpressConfig.pdf.filename}` : deploymentInfo.pdf,
394
+ pdf: deployedUrl ? `${deployedUrl}/${pdfFilename}` : deploymentInfo.pdf,
390
395
  public_url: publicUrl,
391
396
  dirty: false,
392
- command: openpressCliCommand(["deploy", ".", "--confirm"]),
397
+ command: openpressCliCommand(cliArgs),
393
398
  stdout: result.stdout,
394
399
  stderr: result.stderr,
395
400
  });
@@ -420,9 +425,11 @@ function runLocalPdfExport(slug = "") {
420
425
  });
421
426
  }
422
427
 
423
- function runLocalDeploy() {
428
+ function runLocalDeploy(slug = "") {
429
+ const args = [openpressCliPath, "deploy", ".", "--confirm"];
430
+ if (slug) args.push("--press", slug);
424
431
  return new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => {
425
- const child = spawn("node", [openpressCliPath, "deploy", ".", "--confirm"], {
432
+ const child = spawn("node", args, {
426
433
  cwd: workspaceRoot,
427
434
  shell: false,
428
435
  });
@@ -485,7 +492,7 @@ async function readLocalDeploymentInfo() {
485
492
  }
486
493
  }
487
494
 
488
- async function writeLocalDeploymentPublicUrl(publicUrl: string) {
495
+ async function writeLocalDeploymentPublicUrl(publicUrl: string, pdfFilename = openpressConfig.pdf.filename) {
489
496
  let deployConfig: Record<string, unknown> = {};
490
497
  try {
491
498
  deployConfig = JSON.parse(await fs.readFile(openpressConfig.paths.deployMetadata, "utf8")) as Record<string, unknown>;
@@ -495,7 +502,7 @@ async function writeLocalDeploymentPublicUrl(publicUrl: string) {
495
502
  await fs.mkdir(path.dirname(openpressConfig.paths.deployMetadata), { recursive: true });
496
503
  await fs.writeFile(
497
504
  openpressConfig.paths.deployMetadata,
498
- `${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`,
499
506
  "utf8",
500
507
  );
501
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
- }