@1agh/maude 0.16.0 → 0.17.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 +4 -0
- package/cli/commands/design.mjs +108 -2
- package/package.json +12 -18
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +8 -8
- package/plugins/design/dev-server/annotations-layer.tsx +8 -10
- package/plugins/design/dev-server/api.ts +41 -0
- package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +40 -0
- package/plugins/design/dev-server/bin/_html-playwright.mjs +129 -0
- package/plugins/design/dev-server/bin/_pdf-playwright.mjs +105 -0
- package/plugins/design/dev-server/bin/_png-playwright.mjs +143 -0
- package/plugins/design/dev-server/bin/_pptx-playwright.mjs +98 -0
- package/plugins/design/dev-server/bin/_svg-playwright.mjs +141 -0
- package/plugins/design/dev-server/canvas-lib.tsx +12 -13
- package/plugins/design/dev-server/canvas-shell.tsx +32 -4
- package/plugins/design/dev-server/client/app.jsx +18 -1
- package/plugins/design/dev-server/context-menu.tsx +36 -9
- package/plugins/design/dev-server/dist/client.bundle.js +11 -3
- package/plugins/design/dev-server/export-dialog.tsx +401 -0
- package/plugins/design/dev-server/exporters/_browser-bundles.ts +89 -0
- package/plugins/design/dev-server/exporters/canva-handoff-prompt.ts +74 -0
- package/plugins/design/dev-server/exporters/canva.ts +126 -0
- package/plugins/design/dev-server/exporters/html.ts +103 -0
- package/plugins/design/dev-server/exporters/index.ts +135 -0
- package/plugins/design/dev-server/exporters/pdf.ts +109 -0
- package/plugins/design/dev-server/exporters/png.ts +136 -0
- package/plugins/design/dev-server/exporters/pptx.ts +263 -0
- package/plugins/design/dev-server/exporters/scope.ts +196 -0
- package/plugins/design/dev-server/exporters/svg.ts +122 -0
- package/plugins/design/dev-server/exporters/zip.ts +109 -0
- package/plugins/design/dev-server/http.ts +80 -0
- package/plugins/design/dev-server/inspect.ts +1 -1
- package/plugins/design/dev-server/server.mjs +1 -1
- package/plugins/design/dev-server/test/exporters/canva.test.ts +64 -0
- package/plugins/design/dev-server/test/exporters/endpoint.test.ts +121 -0
- package/plugins/design/dev-server/test/exporters/history.test.ts +79 -0
- package/plugins/design/dev-server/test/exporters/html.test.ts +26 -0
- package/plugins/design/dev-server/test/exporters/pdf.test.ts +53 -0
- package/plugins/design/dev-server/test/exporters/png.test.ts +32 -0
- package/plugins/design/dev-server/test/exporters/pptx.test.ts +31 -0
- package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
- package/plugins/design/dev-server/test/exporters/svg.test.ts +29 -0
- package/plugins/design/dev-server/test/exporters/zip.test.ts +105 -0
- package/plugins/design/dev-server/tool-palette.tsx +34 -16
- package/plugins/design/templates/_shell.html +33 -0
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
A personal marketplace of Claude Code plugins. Two plugins today, plus a `maude` CLI for scaffolding and running the bundled dev tooling.
|
|
4
4
|
|
|
5
|
+
<!-- Demo video lives at https://github.com/1aGh/maude/releases/download/v0.16.0/demo.mp4 once uploaded.
|
|
6
|
+
Until then the inline tag below stays a soft 404; landing page at https://maude.iagh.cz autoplays the same file. -->
|
|
7
|
+
<video src="https://github.com/1aGh/maude/releases/download/v0.16.0/demo.mp4" controls muted playsinline poster="https://maude.iagh.cz/demo-poster.jpg" width="800"></video>
|
|
8
|
+
|
|
5
9
|
> **📚 Full docs: https://maude.iagh.cz** (or browse the source under [`site/content/docs/`](./site/content/docs/) until the public URL lands).
|
|
6
10
|
> Contributing? See [CONTRIBUTING.md](./CONTRIBUTING.md). Security? See [SECURITY.md](./SECURITY.md).
|
|
7
11
|
|
package/cli/commands/design.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import { basename, join, resolve } from 'node:path';
|
|
4
4
|
import { parseArgs } from '../lib/argv.mjs';
|
|
5
5
|
|
|
6
|
-
const SUBCOMMANDS = new Set(['serve', 'init', 'help']);
|
|
6
|
+
const SUBCOMMANDS = new Set(['serve', 'init', 'export', 'help']);
|
|
7
7
|
|
|
8
8
|
export async function run({ args, pkgRoot }) {
|
|
9
9
|
const { positional } = parseArgs(args);
|
|
@@ -25,10 +25,13 @@ export async function run({ args, pkgRoot }) {
|
|
|
25
25
|
if (sub === 'init') {
|
|
26
26
|
return runInit({ args, pkgRoot });
|
|
27
27
|
}
|
|
28
|
+
if (sub === 'export') {
|
|
29
|
+
return runExport({ args });
|
|
30
|
+
}
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
function usage() {
|
|
31
|
-
return `maude design <serve|init> [options]
|
|
34
|
+
return `maude design <serve|init|export> [options]
|
|
32
35
|
|
|
33
36
|
serve [--port N] [--root PATH]
|
|
34
37
|
Start the design plugin's dev server in the current repo. Equivalent
|
|
@@ -45,6 +48,13 @@ function usage() {
|
|
|
45
48
|
--discovery-payload <path> reads a JSON file with answers + tokens and
|
|
46
49
|
scaffolds Core + the derived specimens deterministically (this is the
|
|
47
50
|
path skill 'design-system' uses when shelling out from Claude Code).
|
|
51
|
+
|
|
52
|
+
export <format> [--scope selection|artboard|canvas-as-separate|project-raw]
|
|
53
|
+
[--port N] [--out <path>] [--option key=value ...]
|
|
54
|
+
Drive the same POST /_api/export endpoint the UI uses. Auto-detects
|
|
55
|
+
port from .design/_server.json; requires a running dev server. The
|
|
56
|
+
response body is written to --out (default: current dir, server-
|
|
57
|
+
supplied filename). Formats: png pdf svg html pptx canva zip.
|
|
48
58
|
`;
|
|
49
59
|
}
|
|
50
60
|
|
|
@@ -98,6 +108,102 @@ async function runServe({ args, pkgRoot }) {
|
|
|
98
108
|
});
|
|
99
109
|
}
|
|
100
110
|
|
|
111
|
+
async function runExport({ args }) {
|
|
112
|
+
// `maude design export <format> [--scope ...] [--port N] [--out <path>] [--option key=value]`
|
|
113
|
+
const subArgs = args.slice(args.indexOf('export') + 1);
|
|
114
|
+
const { positional, flags } = parseArgs(subArgs, {
|
|
115
|
+
booleans: ['help'],
|
|
116
|
+
});
|
|
117
|
+
if (flags.help) {
|
|
118
|
+
process.stdout.write(usage());
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const format = positional[0];
|
|
123
|
+
const VALID_FORMATS = new Set(['png', 'pdf', 'svg', 'html', 'pptx', 'canva', 'zip']);
|
|
124
|
+
if (!format || !VALID_FORMATS.has(format)) {
|
|
125
|
+
process.stderr.write(
|
|
126
|
+
`maude design export: missing or unknown <format>. Try one of: ${Array.from(VALID_FORMATS).join(', ')}\n`
|
|
127
|
+
);
|
|
128
|
+
process.exit(2);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const scope = flags.scope ?? 'canvas-as-separate';
|
|
132
|
+
const VALID_SCOPES = new Set(['selection', 'artboard', 'canvas-as-separate', 'project-raw']);
|
|
133
|
+
if (!VALID_SCOPES.has(scope)) {
|
|
134
|
+
process.stderr.write(`maude design export: unknown --scope "${scope}"\n`);
|
|
135
|
+
process.exit(2);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Resolve port: --port > .design/_server.json
|
|
139
|
+
let port = flags.port ? Number(flags.port) : null;
|
|
140
|
+
if (!port) {
|
|
141
|
+
try {
|
|
142
|
+
const raw = await readFile(resolve(process.cwd(), '.design', '_server.json'), 'utf8');
|
|
143
|
+
port = JSON.parse(raw).port;
|
|
144
|
+
} catch {
|
|
145
|
+
/* no server.json — handled below */
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!port) {
|
|
149
|
+
process.stderr.write(
|
|
150
|
+
'maude design export: no --port given and .design/_server.json not found. Start the dev server first (`maude design serve`).\n'
|
|
151
|
+
);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Collect `--option key=value` repeated flags into an object.
|
|
156
|
+
const options = {};
|
|
157
|
+
const repeated = collectRepeatedFlag(subArgs, '--option');
|
|
158
|
+
for (const item of repeated) {
|
|
159
|
+
const eq = item.indexOf('=');
|
|
160
|
+
if (eq < 0) {
|
|
161
|
+
process.stderr.write(
|
|
162
|
+
`maude design export: invalid --option "${item}" (expected key=value)\n`
|
|
163
|
+
);
|
|
164
|
+
process.exit(2);
|
|
165
|
+
}
|
|
166
|
+
const key = item.slice(0, eq);
|
|
167
|
+
const value = item.slice(eq + 1);
|
|
168
|
+
// Coerce common JSON-ish values: true/false, numbers, arrays via comma.
|
|
169
|
+
options[key] = value === 'true' ? true : value === 'false' ? false : value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const url = `http://localhost:${port}/_api/export`;
|
|
173
|
+
const r = await fetch(url, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: { 'content-type': 'application/json' },
|
|
176
|
+
body: JSON.stringify({ format, scope, options }),
|
|
177
|
+
});
|
|
178
|
+
if (!r.ok) {
|
|
179
|
+
const text = await r.text();
|
|
180
|
+
process.stderr.write(`maude design export: server returned ${r.status}: ${text}\n`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const disp = r.headers.get('content-disposition') ?? '';
|
|
185
|
+
const serverFilename = /filename="([^"]+)"/.exec(disp)?.[1] ?? `export.${format}`;
|
|
186
|
+
const outPath = flags.out
|
|
187
|
+
? resolve(process.cwd(), flags.out)
|
|
188
|
+
: resolve(process.cwd(), serverFilename);
|
|
189
|
+
const bytes = new Uint8Array(await r.arrayBuffer());
|
|
190
|
+
await writeFile(outPath, bytes);
|
|
191
|
+
process.stdout.write(`maude design export: wrote ${outPath} (${bytes.byteLength} bytes)\n`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function collectRepeatedFlag(argv, name) {
|
|
195
|
+
const out = [];
|
|
196
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
197
|
+
if (argv[i] === name && argv[i + 1] !== undefined) {
|
|
198
|
+
out.push(argv[i + 1]);
|
|
199
|
+
i += 1;
|
|
200
|
+
} else if (argv[i].startsWith(`${name}=`)) {
|
|
201
|
+
out.push(argv[i].slice(name.length + 1));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
101
207
|
async function runInit({ args, pkgRoot }) {
|
|
102
208
|
const subArgs = args.slice(args.indexOf('init') + 1);
|
|
103
209
|
const { flags } = parseArgs(subArgs, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1agh/maude",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -34,18 +34,20 @@
|
|
|
34
34
|
"video:smoke": "bash scripts/video/smoke/run.sh",
|
|
35
35
|
"video:smoke:terminal": "vhs scripts/video/smoke/terminal.tape",
|
|
36
36
|
"video:smoke:browser": "playwright test --config scripts/video/smoke/playwright.config.ts",
|
|
37
|
-
"video:smoke:card": "
|
|
37
|
+
"video:smoke:card": "cd scripts/video/final && pnpm run render SmokeCard ../.work/smoke/card.mp4 --mute",
|
|
38
|
+
"video:render": "cd scripts/video/final && pnpm run render",
|
|
39
|
+
"video:studio": "cd scripts/video/final && pnpm run studio",
|
|
38
40
|
"postinstall": "node cli/install.cjs",
|
|
39
41
|
"prepublishOnly": "bash scripts/check-version-parity.sh"
|
|
40
42
|
},
|
|
41
43
|
"optionalDependencies": {
|
|
42
|
-
"@1agh/maude-darwin-arm64": "0.
|
|
43
|
-
"@1agh/maude-darwin-x64": "0.
|
|
44
|
-
"@1agh/maude-linux-arm64": "0.
|
|
45
|
-
"@1agh/maude-linux-arm64-musl": "0.
|
|
46
|
-
"@1agh/maude-linux-x64": "0.
|
|
47
|
-
"@1agh/maude-linux-x64-musl": "0.
|
|
48
|
-
"@1agh/maude-win32-x64": "0.
|
|
44
|
+
"@1agh/maude-darwin-arm64": "0.17.1",
|
|
45
|
+
"@1agh/maude-darwin-x64": "0.17.1",
|
|
46
|
+
"@1agh/maude-linux-arm64": "0.17.1",
|
|
47
|
+
"@1agh/maude-linux-arm64-musl": "0.17.1",
|
|
48
|
+
"@1agh/maude-linux-x64": "0.17.1",
|
|
49
|
+
"@1agh/maude-linux-x64-musl": "0.17.1",
|
|
50
|
+
"@1agh/maude-win32-x64": "0.17.1"
|
|
49
51
|
},
|
|
50
52
|
"files": [
|
|
51
53
|
"cli",
|
|
@@ -81,14 +83,6 @@
|
|
|
81
83
|
"devDependencies": {
|
|
82
84
|
"@biomejs/biome": "^1.9.4",
|
|
83
85
|
"@changesets/cli": "^2.27.10",
|
|
84
|
-
"@playwright/test": "^1.60.0"
|
|
85
|
-
"@remotion/bundler": "^4.0.463",
|
|
86
|
-
"@remotion/cli": "^4.0.463",
|
|
87
|
-
"@remotion/renderer": "^4.0.463",
|
|
88
|
-
"@types/react": "^19.2.14",
|
|
89
|
-
"@types/react-dom": "^19.2.3",
|
|
90
|
-
"react": "^19.2.6",
|
|
91
|
-
"react-dom": "^19.2.6",
|
|
92
|
-
"remotion": "^4.0.463"
|
|
86
|
+
"@playwright/test": "^1.60.0"
|
|
93
87
|
}
|
|
94
88
|
}
|
|
@@ -36,14 +36,14 @@ const TOOLBAR_CSS = `
|
|
|
36
36
|
display: flex;
|
|
37
37
|
align-items: center;
|
|
38
38
|
gap: 6px;
|
|
39
|
-
background: var(--bg-1, rgba(255,255,255,0.98));
|
|
40
|
-
border: 1px solid var(--u-
|
|
41
|
-
border-radius:
|
|
39
|
+
background: var(--u-bg-2, var(--bg-1, rgba(255,255,255,0.98)));
|
|
40
|
+
border: 1px solid var(--u-fg-0, #1c1917);
|
|
41
|
+
border-radius: 0;
|
|
42
42
|
padding: 6px 8px;
|
|
43
|
-
box-shadow:
|
|
44
|
-
font-family:
|
|
43
|
+
box-shadow: 4px 4px 0 var(--u-fg-0, #1c1917);
|
|
44
|
+
font-family: var(--u-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
45
45
|
font-size: 12px;
|
|
46
|
-
color: var(--fg-0, #1a1a1a);
|
|
46
|
+
color: var(--u-fg-0, var(--fg-0, #1a1a1a));
|
|
47
47
|
user-select: none;
|
|
48
48
|
pointer-events: auto;
|
|
49
49
|
}
|
|
@@ -79,8 +79,8 @@ const TOOLBAR_CSS = `
|
|
|
79
79
|
.dc-annot-ctx-btn {
|
|
80
80
|
appearance: none;
|
|
81
81
|
background: transparent;
|
|
82
|
-
border: 1px solid rgba(0,0,0,0.
|
|
83
|
-
border-radius:
|
|
82
|
+
border: 1px solid var(--u-fg-0, rgba(0,0,0,0.6));
|
|
83
|
+
border-radius: 0;
|
|
84
84
|
padding: 3px 8px;
|
|
85
85
|
font: inherit;
|
|
86
86
|
color: inherit;
|
|
@@ -118,9 +118,7 @@ const STROKE_WIDTH_THIN = 2;
|
|
|
118
118
|
const STROKE_WIDTH_THICK = 6;
|
|
119
119
|
type Thickness = typeof STROKE_WIDTH_THIN | typeof STROKE_WIDTH_THICK;
|
|
120
120
|
|
|
121
|
-
const FONT_SIZE_SMALL = 12;
|
|
122
121
|
const FONT_SIZE_MEDIUM = 14;
|
|
123
|
-
const FONT_SIZE_LARGE = 20;
|
|
124
122
|
const DEFAULT_FONT_SIZE = FONT_SIZE_MEDIUM;
|
|
125
123
|
|
|
126
124
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -459,15 +457,15 @@ const ANNOT_CSS = `
|
|
|
459
457
|
display: flex;
|
|
460
458
|
align-items: center;
|
|
461
459
|
gap: 8px;
|
|
462
|
-
background: var(--bg-1, rgba(255,255,255,0.98));
|
|
463
|
-
border: 1px solid var(--u-
|
|
464
|
-
border-radius:
|
|
460
|
+
background: var(--u-bg-2, var(--bg-1, rgba(255,255,255,0.98)));
|
|
461
|
+
border: 1px solid var(--u-fg-0, #1c1917);
|
|
462
|
+
border-radius: 0;
|
|
465
463
|
padding: 6px 10px;
|
|
466
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
464
|
+
font-family: var(--u-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
467
465
|
font-size: 11px;
|
|
468
|
-
color: rgba(40,30,20,0.85);
|
|
466
|
+
color: var(--u-fg-0, rgba(40,30,20,0.85));
|
|
469
467
|
z-index: 6;
|
|
470
|
-
box-shadow:
|
|
468
|
+
box-shadow: 4px 4px 0 var(--u-fg-0, #1c1917);
|
|
471
469
|
user-select: none;
|
|
472
470
|
}
|
|
473
471
|
.dc-annot-chrome .dc-annot-swatches { display: flex; gap: 4px; }
|
|
@@ -1481,7 +1479,7 @@ function TextEditor({
|
|
|
1481
1479
|
textAlign: 'center',
|
|
1482
1480
|
color: existing?.color ?? '#1a1a1a',
|
|
1483
1481
|
fontSize: `${fontSize}px`,
|
|
1484
|
-
fontFamily: '
|
|
1482
|
+
fontFamily: 'var(--u-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace)',
|
|
1485
1483
|
lineHeight: 1.25,
|
|
1486
1484
|
outline: 'none',
|
|
1487
1485
|
background: 'transparent',
|
|
@@ -1568,7 +1566,7 @@ function StrokeNode({
|
|
|
1568
1566
|
fontSize={stroke.fontSize}
|
|
1569
1567
|
textAnchor="middle"
|
|
1570
1568
|
dominantBaseline="middle"
|
|
1571
|
-
style={{ fontFamily: '
|
|
1569
|
+
style={{ fontFamily: 'var(--u-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace)' }}
|
|
1572
1570
|
>
|
|
1573
1571
|
{stroke.text}
|
|
1574
1572
|
</text>
|
|
@@ -119,6 +119,16 @@ export interface GitCommitter {
|
|
|
119
119
|
commits: number;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// Phase 6.5 T10 — export history. Five-deep ring buffer of recent exports
|
|
123
|
+
// surfaced by the dialog's "Recent" tab + ⌘⇧E re-run.
|
|
124
|
+
export interface ExportHistoryEntry {
|
|
125
|
+
format: string;
|
|
126
|
+
scope: string;
|
|
127
|
+
options?: Record<string, unknown>;
|
|
128
|
+
filename: string;
|
|
129
|
+
at: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
122
132
|
export interface Api {
|
|
123
133
|
// File tree
|
|
124
134
|
fileSlug(file: string): string;
|
|
@@ -146,6 +156,9 @@ export interface Api {
|
|
|
146
156
|
// Aggregate data
|
|
147
157
|
buildIndexData(): Promise<unknown>;
|
|
148
158
|
buildSystemData(): Promise<unknown>;
|
|
159
|
+
// Export history (Phase 6.5 T10)
|
|
160
|
+
loadExportHistory(): Promise<ExportHistoryEntry[]>;
|
|
161
|
+
appendExportHistory(entry: ExportHistoryEntry): Promise<void>;
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
export function createApi(ctx: Context, onCommentsChanged: (file: string) => void): Api {
|
|
@@ -694,6 +707,32 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
|
|
|
694
707
|
};
|
|
695
708
|
}
|
|
696
709
|
|
|
710
|
+
// ---------- Export history (Phase 6.5 T10) ----------
|
|
711
|
+
//
|
|
712
|
+
// 5-deep ring buffer persisted at `<designRoot>/_export-history.json`.
|
|
713
|
+
// Reads tolerate missing / malformed files (returns []). Writes truncate
|
|
714
|
+
// to most-recent-first.
|
|
715
|
+
|
|
716
|
+
const HISTORY_PATH = path.join(paths.designRoot, '_export-history.json');
|
|
717
|
+
const HISTORY_DEPTH = 5;
|
|
718
|
+
|
|
719
|
+
async function loadExportHistory(): Promise<ExportHistoryEntry[]> {
|
|
720
|
+
try {
|
|
721
|
+
const raw = await Bun.file(HISTORY_PATH).text();
|
|
722
|
+
const arr = JSON.parse(raw);
|
|
723
|
+
if (!Array.isArray(arr)) return [];
|
|
724
|
+
return arr.slice(0, HISTORY_DEPTH);
|
|
725
|
+
} catch {
|
|
726
|
+
return [];
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async function appendExportHistory(entry: ExportHistoryEntry): Promise<void> {
|
|
731
|
+
const prev = await loadExportHistory();
|
|
732
|
+
const next = [entry, ...prev].slice(0, HISTORY_DEPTH);
|
|
733
|
+
await Bun.write(HISTORY_PATH, JSON.stringify(next, null, 2));
|
|
734
|
+
}
|
|
735
|
+
|
|
697
736
|
function tokenKind(name: string, value: string): string {
|
|
698
737
|
const n = name.toLowerCase();
|
|
699
738
|
const v = String(value).trim();
|
|
@@ -853,5 +892,7 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
|
|
|
853
892
|
saveAnnotations,
|
|
854
893
|
buildIndexData,
|
|
855
894
|
buildSystemData,
|
|
895
|
+
loadExportHistory,
|
|
896
|
+
appendExportHistory,
|
|
856
897
|
};
|
|
857
898
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tiny playwright shim that lists `[data-dc-screen]` IDs on a canvas-shell URL,
|
|
3
|
+
// one per line on stdout. Spawned by `exporters/pptx.ts` for canvas-as-separate
|
|
4
|
+
// merge. Lives as a subprocess (not a direct import) so `bun build --compile`
|
|
5
|
+
// of the dev-server binary doesn't pull in playwright + chromium-bidi deep deps.
|
|
6
|
+
|
|
7
|
+
import { chromium } from 'playwright';
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
let url;
|
|
11
|
+
let timeoutSec = 20;
|
|
12
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
13
|
+
if (args[i] === '--url') {
|
|
14
|
+
i += 1;
|
|
15
|
+
url = args[i];
|
|
16
|
+
} else if (args[i] === '--timeout') {
|
|
17
|
+
i += 1;
|
|
18
|
+
timeoutSec = Number(args[i]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (!url) {
|
|
22
|
+
console.error('usage: _enumerate-artboards-playwright.mjs --url <url> [--timeout <sec>]');
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const timeoutMs = timeoutSec * 1000;
|
|
27
|
+
const browser = await chromium.launch();
|
|
28
|
+
try {
|
|
29
|
+
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
30
|
+
const page = await ctx.newPage();
|
|
31
|
+
await page.goto(url, { waitUntil: 'networkidle', timeout: timeoutMs });
|
|
32
|
+
const ids = await page.evaluate(() =>
|
|
33
|
+
Array.from(document.querySelectorAll('[data-dc-screen]')).map(
|
|
34
|
+
(el) => el.getAttribute('data-dc-screen') ?? ''
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
for (const id of ids.filter(Boolean)) console.log(id);
|
|
38
|
+
} finally {
|
|
39
|
+
await browser.close();
|
|
40
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// _html-playwright.mjs — playwright shim for the HTML exporter.
|
|
2
|
+
//
|
|
3
|
+
// Walks the rendered DOM, serializes the full document with stylesheets
|
|
4
|
+
// inlined, and emits a standalone `index.html`. Web fonts + remote images
|
|
5
|
+
// are NOT inlined in v1 — the doc references them by absolute URL so the
|
|
6
|
+
// resulting file works under file:// when the user has those origins
|
|
7
|
+
// available. Full asset inlining is a follow-up; see plan T5.
|
|
8
|
+
//
|
|
9
|
+
// Invocation (Bun.spawn from exporters/html.ts — not invoked directly):
|
|
10
|
+
// npm exec --package=playwright -- node _html-playwright.mjs \
|
|
11
|
+
// --url <url> --selector <css> --out <path> \
|
|
12
|
+
// [--widen-to-artboard] [--multi] [--out-dir <dir>] [--timeout 8]
|
|
13
|
+
|
|
14
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { chromium } from 'playwright';
|
|
17
|
+
|
|
18
|
+
const args = Object.fromEntries(
|
|
19
|
+
process.argv.slice(2).reduce((acc, cur, i, all) => {
|
|
20
|
+
if (cur.startsWith('--')) acc.push([cur.slice(2), all[i + 1] ?? '1']);
|
|
21
|
+
return acc;
|
|
22
|
+
}, [])
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
url,
|
|
27
|
+
selector,
|
|
28
|
+
out,
|
|
29
|
+
'out-dir': outDir,
|
|
30
|
+
'widen-to-artboard': widenFlag,
|
|
31
|
+
multi: multiFlag,
|
|
32
|
+
timeout = '8',
|
|
33
|
+
} = args;
|
|
34
|
+
|
|
35
|
+
if (!url) {
|
|
36
|
+
console.error('usage: _html-playwright.mjs --url <url> --selector <css> --out <path>');
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const widen = widenFlag !== undefined;
|
|
41
|
+
const multi = multiFlag !== undefined;
|
|
42
|
+
const timeoutMs = Number(timeout) * 1000;
|
|
43
|
+
|
|
44
|
+
const browser = await chromium.launch();
|
|
45
|
+
try {
|
|
46
|
+
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
47
|
+
const page = await ctx.newPage();
|
|
48
|
+
await page.goto(url, { waitUntil: 'networkidle', timeout: timeoutMs });
|
|
49
|
+
// Reset the world plane's CSS zoom + transform so the captured artboard
|
|
50
|
+
// outerHTML carries 1440×900 dimensions instead of the pan-zoomed thumb.
|
|
51
|
+
await page.evaluate(() => {
|
|
52
|
+
const world = document.querySelector('.dc-world');
|
|
53
|
+
if (world) {
|
|
54
|
+
world.style.zoom = '1';
|
|
55
|
+
world.style.transform = 'none';
|
|
56
|
+
}
|
|
57
|
+
for (const el of document.querySelectorAll('[data-dc-screen]')) {
|
|
58
|
+
el.style.left = '0px';
|
|
59
|
+
el.style.top = '0px';
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const written = [];
|
|
64
|
+
|
|
65
|
+
if (multi) {
|
|
66
|
+
if (!outDir) {
|
|
67
|
+
console.error('_html-playwright: --multi requires --out-dir');
|
|
68
|
+
process.exit(2);
|
|
69
|
+
}
|
|
70
|
+
mkdirSync(outDir, { recursive: true });
|
|
71
|
+
const screens = await page.locator(selector ?? '[data-dc-screen]').all();
|
|
72
|
+
for (let i = 0; i < screens.length; i += 1) {
|
|
73
|
+
const handle = screens[i];
|
|
74
|
+
const id = (await handle.getAttribute('data-dc-screen')) ?? `artboard-${i + 1}`;
|
|
75
|
+
const html = await serializeOne(handle, false);
|
|
76
|
+
const target = join(outDir, `${id}.html`);
|
|
77
|
+
writeFileSync(target, html, 'utf8');
|
|
78
|
+
written.push(target);
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
if (!out) {
|
|
82
|
+
console.error('_html-playwright: --out required when --multi not set');
|
|
83
|
+
process.exit(2);
|
|
84
|
+
}
|
|
85
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
86
|
+
const handle = page.locator(selector ?? '[data-dc-screen]:first-of-type').first();
|
|
87
|
+
await handle.waitFor({ state: 'visible', timeout: timeoutMs });
|
|
88
|
+
const html = await serializeOne(handle, widen);
|
|
89
|
+
writeFileSync(out, html, 'utf8');
|
|
90
|
+
written.push(out);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const w of written) console.log(w);
|
|
94
|
+
console.error(`✓ playwright wrote ${written.length} html file(s)`);
|
|
95
|
+
} finally {
|
|
96
|
+
await browser.close();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function serializeOne(locator, widenToArtboard) {
|
|
100
|
+
return await locator.evaluate(
|
|
101
|
+
(el, opts) => {
|
|
102
|
+
const target = opts.widenToArtboard ? (el.closest('[data-dc-screen]') ?? el) : el;
|
|
103
|
+
const cssChunks = [];
|
|
104
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
105
|
+
try {
|
|
106
|
+
for (const rule of Array.from(sheet.cssRules)) {
|
|
107
|
+
cssChunks.push(rule.cssText);
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// Cross-origin sheet — skip.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const styleBlock = `<style>${cssChunks.join('\n')}</style>`;
|
|
114
|
+
const innerHtml = target.outerHTML;
|
|
115
|
+
return `<!doctype html>
|
|
116
|
+
<html lang="en">
|
|
117
|
+
<head>
|
|
118
|
+
<meta charset="utf-8" />
|
|
119
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
120
|
+
<title>${document.title || 'Maude export'}</title>
|
|
121
|
+
<base href="${location.origin}/" />
|
|
122
|
+
${styleBlock}
|
|
123
|
+
</head>
|
|
124
|
+
<body>${innerHtml}</body>
|
|
125
|
+
</html>`;
|
|
126
|
+
},
|
|
127
|
+
{ widenToArtboard }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// _pdf-playwright.mjs — playwright shim for the PDF exporter.
|
|
2
|
+
//
|
|
3
|
+
// Drives Chromium's print-to-PDF (`page.pdf()`) directly so the output is a
|
|
4
|
+
// true vector PDF with selectable text — NOT a PNG embedded in a PDF wrapper.
|
|
5
|
+
// Per DDR-041 (PDF via page.pdf()): print-media emulation, font readiness
|
|
6
|
+
// wait, explicit page size matching the artboard rect.
|
|
7
|
+
//
|
|
8
|
+
// Invocation (Bun.spawn from exporters/pdf.ts — not invoked directly):
|
|
9
|
+
// node _pdf-playwright.mjs --url <url> --selector <css> --out <path>
|
|
10
|
+
// [--multi] [--out-dir <dir>] [--timeout 12]
|
|
11
|
+
|
|
12
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { chromium } from 'playwright';
|
|
15
|
+
|
|
16
|
+
const args = Object.fromEntries(
|
|
17
|
+
process.argv.slice(2).reduce((acc, cur, i, all) => {
|
|
18
|
+
if (cur.startsWith('--')) acc.push([cur.slice(2), all[i + 1] ?? '1']);
|
|
19
|
+
return acc;
|
|
20
|
+
}, [])
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const { url, selector, out, 'out-dir': outDir, multi: multiFlag, timeout = '12' } = args;
|
|
24
|
+
|
|
25
|
+
if (!url) {
|
|
26
|
+
console.error('usage: _pdf-playwright.mjs --url <url> --selector <css> --out <path>');
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const multi = multiFlag !== undefined;
|
|
31
|
+
const timeoutMs = Number(timeout) * 1000;
|
|
32
|
+
|
|
33
|
+
const browser = await chromium.launch();
|
|
34
|
+
try {
|
|
35
|
+
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
36
|
+
const page = await ctx.newPage();
|
|
37
|
+
await page.goto(url, { waitUntil: 'networkidle', timeout: timeoutMs });
|
|
38
|
+
// Wait for web fonts so `@font-face` glyphs land in the PDF instead of
|
|
39
|
+
// fallback Latin (puppeteer #3183 / playwright equivalent).
|
|
40
|
+
await page.evaluate(() => document.fonts.ready);
|
|
41
|
+
// Print-media emulation — Chromium's PDF output otherwise applies *screen*
|
|
42
|
+
// CSS and ignores `@media print` rules entirely.
|
|
43
|
+
await page.emulateMedia({ media: 'print' });
|
|
44
|
+
|
|
45
|
+
const written = [];
|
|
46
|
+
const screens = multi
|
|
47
|
+
? await page.locator(selector ?? '[data-dc-screen]').all()
|
|
48
|
+
: [page.locator(selector ?? '[data-dc-screen]:first-of-type').first()];
|
|
49
|
+
|
|
50
|
+
if (multi && outDir) mkdirSync(outDir, { recursive: true });
|
|
51
|
+
else if (out) mkdirSync(dirname(out), { recursive: true });
|
|
52
|
+
|
|
53
|
+
// Reset the world plane's pan/zoom + transform so artboards render at
|
|
54
|
+
// their declared dimensions. CSS `zoom` on .dc-world actually shrinks
|
|
55
|
+
// layout, so getBoundingClientRect returns the post-zoom size unless
|
|
56
|
+
// we zero this. Done once per page; affects every artboard read below.
|
|
57
|
+
await page.evaluate(() => {
|
|
58
|
+
const world = document.querySelector('.dc-world');
|
|
59
|
+
if (world) {
|
|
60
|
+
world.style.zoom = '1';
|
|
61
|
+
world.style.transform = 'none';
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < screens.length; i += 1) {
|
|
66
|
+
const handle = screens[i];
|
|
67
|
+
// Pin each artboard to (0,0) right before its capture so the world's
|
|
68
|
+
// multi-artboard layout doesn't push the bbox off the viewport.
|
|
69
|
+
await handle.evaluate((el) => {
|
|
70
|
+
el.style.left = '0px';
|
|
71
|
+
el.style.top = '0px';
|
|
72
|
+
});
|
|
73
|
+
const rect = await handle.evaluate((el) => {
|
|
74
|
+
const r = el.getBoundingClientRect();
|
|
75
|
+
return { w: r.width, h: r.height, x: r.left, y: r.top };
|
|
76
|
+
});
|
|
77
|
+
// Set the page size to the artboard's pixel dimensions so the resulting
|
|
78
|
+
// PDF is exactly one artboard per page with no margin.
|
|
79
|
+
const targetPath = multi ? join(outDir, `artboard-${i + 1}.pdf`) : out;
|
|
80
|
+
// Crop trick: set the viewport to the artboard rect, scroll it into the
|
|
81
|
+
// top-left corner, then page.pdf() with matching width/height.
|
|
82
|
+
await page.setViewportSize({
|
|
83
|
+
width: Math.ceil(rect.w),
|
|
84
|
+
height: Math.ceil(rect.h),
|
|
85
|
+
});
|
|
86
|
+
await handle.evaluate((el) => {
|
|
87
|
+
el.scrollIntoView({ block: 'start', inline: 'start' });
|
|
88
|
+
window.scrollTo(0, 0);
|
|
89
|
+
});
|
|
90
|
+
const pdf = await page.pdf({
|
|
91
|
+
width: `${Math.ceil(rect.w)}px`,
|
|
92
|
+
height: `${Math.ceil(rect.h)}px`,
|
|
93
|
+
printBackground: true,
|
|
94
|
+
preferCSSPageSize: false,
|
|
95
|
+
margin: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
96
|
+
});
|
|
97
|
+
writeFileSync(targetPath, pdf);
|
|
98
|
+
written.push(targetPath);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const w of written) console.log(w);
|
|
102
|
+
console.error(`✓ page.pdf wrote ${written.length} file(s)`);
|
|
103
|
+
} finally {
|
|
104
|
+
await browser.close();
|
|
105
|
+
}
|