@itsthecsguy/linework 0.1.2 → 0.1.4
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 +40 -0
- package/dist/api.js +9 -6
- package/dist/cli.js +88 -43
- package/dist/drawio/theme.js +42 -20
- package/dist/drawio/write.js +152 -22
- package/dist/layout/layout.js +225 -17
- package/dist/layout/polish.js +281 -0
- package/dist/layout/sequence.js +7 -4
- package/dist/lint/check.js +6 -1
- package/dist/lint/fixlabels.js +22 -14
- package/dist/render/browser.js +96 -32
- package/dist/render/fonts.js +65 -1
- package/dist/spec/model.js +3 -1
- package/package.json +1 -1
- package/skills/linework/SKILL.md +8 -1
- package/skills/linework/examples/auth-seq.yaml +35 -0
- package/skills/linework/examples/deploy-flow.yaml +32 -0
- package/skills/linework/examples/gcp-ingest.yaml +21 -0
- package/skills/linework/examples/job-state.yaml +25 -0
- package/skills/linework/examples/platform.yaml +45 -0
- package/skills/linework/examples/shop-er.yaml +26 -0
- package/skills/linework/references/style-guide.md +9 -1
package/README.md
CHANGED
|
@@ -102,6 +102,29 @@ Linework lays it out with ELK and draws it with draw.io's own renderer. It draws
|
|
|
102
102
|
|
|
103
103
|
To change a diagram, the agent runs `linework show` to get the spec back, hand edits included, edits it and rebuilds. The rebuild merges into your layout. Your boxes stay where you put them, new ones go next to their neighbours, and only the edges that need it get rerouted (with draw.io's libavoid router).
|
|
104
104
|
|
|
105
|
+
## Examples
|
|
106
|
+
|
|
107
|
+
These are first builds of the specs in [`examples/`](examples), with no hand edits. Each one is a `.drawio.svg`, so the image you see here is also the file you'd open in draw.io. Click one to see its spec.
|
|
108
|
+
|
|
109
|
+
<table>
|
|
110
|
+
<tr>
|
|
111
|
+
<td width="50%" valign="top">Architecture, with AWS icons<br><br><a href="examples/serverless-aws.yaml"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/gallery/serverless-aws-dark.drawio.svg"><img alt="Architecture diagram: an uploader posts to an API Gateway, a Lambda presigns an S3 upload, and S3 events queue resize jobs for a second Lambda." src="docs/gallery/serverless-aws-light.drawio.svg" width="100%"></picture></a></td>
|
|
112
|
+
<td width="50%" valign="top">Flow<br><br><a href="examples/refund-flow.yaml"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/gallery/refund-flow-dark.drawio.svg"><img alt="Flowchart of a refund: look up the order, check the 30-day window and the amount, then auto-approve, queue for review or deny, and email the customer." src="docs/gallery/refund-flow-light.drawio.svg" width="100%"></picture></a></td>
|
|
113
|
+
</tr>
|
|
114
|
+
</table>
|
|
115
|
+
|
|
116
|
+
State
|
|
117
|
+
|
|
118
|
+
<a href="examples/job-state.yaml"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/gallery/job-state-dark.drawio.svg"><img alt="State diagram of a background job: queued, running, paused, waiting to retry, succeeded, failed and cancelled, with the transitions between them." src="docs/gallery/job-state-light.drawio.svg"></picture></a>
|
|
119
|
+
|
|
120
|
+
Sequence
|
|
121
|
+
|
|
122
|
+
<a href="examples/pay-sequence.yaml"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/gallery/pay-sequence-dark.drawio.svg"><img alt="Sequence diagram of a card payment: the web app posts to the checkout API, which writes the order and creates a Stripe PaymentIntent, with an alt frame for success and decline." src="docs/gallery/pay-sequence-light.drawio.svg"></picture></a>
|
|
123
|
+
|
|
124
|
+
ER
|
|
125
|
+
|
|
126
|
+
<a href="examples/shop-er.yaml"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/gallery/shop-er-dark.drawio.svg"><img alt="Entity relationship diagram of a shop: customers, addresses, orders, order items, products, inventory and payments, with keys and crow's-foot cardinality." src="docs/gallery/shop-er-light.drawio.svg"></picture></a>
|
|
127
|
+
|
|
105
128
|
## Styles
|
|
106
129
|
|
|
107
130
|
Set `theme:` in the spec.
|
|
@@ -115,12 +138,29 @@ Set `theme:` in the spec.
|
|
|
115
138
|
| `blueprint` | technical deep dives. Blue ink on pale drafting paper, monospace. |
|
|
116
139
|
| `c4` | C4-model diagrams, in quiet blues and grays |
|
|
117
140
|
|
|
141
|
+
The same spec in each style:
|
|
142
|
+
|
|
143
|
+
<table>
|
|
144
|
+
<tr>
|
|
145
|
+
<td width="33%" align="center" valign="top"><code>clean</code><br><br><img alt="A web app, checkout API and orders database in the clean style" src="docs/gallery/style-clean.drawio.svg"></td>
|
|
146
|
+
<td width="33%" align="center" valign="top"><code>soft</code><br><br><img alt="A web app, checkout API and orders database in the soft style" src="docs/gallery/style-soft.drawio.svg"></td>
|
|
147
|
+
<td width="33%" align="center" valign="top"><code>dark</code><br><br><img alt="A web app, checkout API and orders database in the dark style" src="docs/gallery/style-dark.drawio.svg"></td>
|
|
148
|
+
</tr>
|
|
149
|
+
<tr>
|
|
150
|
+
<td width="33%" align="center" valign="top"><code>sketch</code><br><br><img alt="A web app, checkout API and orders database in the sketch style" src="docs/gallery/style-sketch.drawio.svg"></td>
|
|
151
|
+
<td width="33%" align="center" valign="top"><code>blueprint</code><br><br><img alt="A web app, checkout API and orders database in the blueprint style" src="docs/gallery/style-blueprint.drawio.svg"></td>
|
|
152
|
+
<td width="33%" align="center" valign="top"><code>c4</code><br><br><img alt="A web app, checkout API and orders database in the c4 style" src="docs/gallery/style-c4.drawio.svg"></td>
|
|
153
|
+
</tr>
|
|
154
|
+
</table>
|
|
155
|
+
|
|
118
156
|
Color is for meaning. Nodes stay neutral by default, and errors and successes are red and green in every style. Mark the part the doc is about with `highlight: true` on a node or `==>` on an edge, and it's drawn in the style's accent color. Use `color: red` (or blue, violet, pink, orange, amber, green, teal, gray) for categories.
|
|
119
157
|
|
|
120
158
|
Linework sizes every box from the real width of its text in the font it renders with, wrapping long labels instead of widening boxes. Each SVG carries a subset of that font (about 10 to 15KB), so text fits the same way on GitHub, in a browser or in a doc. `check` flags labels that overflow, overlap a box, another label, another edge or a group border, and `build` moves edge labels until they're clear.
|
|
121
159
|
|
|
122
160
|
Linework needs Node 20+ and a Chromium-based browser, which it uses headless to measure text and render. It tries Playwright's Chromium, then Chrome, then Edge. If none is found, run `linework install-browser`, or set `LINEWORK_CHROME` to a browser binary. If a browser is found but crashes on launch, an agent's sandbox is usually blocking it. Linework then retries Playwright's Chromium in single-process mode, which starts inside Codex's sandbox.
|
|
123
161
|
|
|
162
|
+
Linework keeps the font subsets it embeds in your cache folder (`~/Library/Caches/linework` on macOS, `~/.cache/linework` on Linux, `%LOCALAPPDATA%\linework` on Windows), so rebuilding a diagram doesn't compress the same fonts again. Set `LINEWORK_NO_CACHE=1` to turn that off.
|
|
163
|
+
|
|
124
164
|
## Connect over MCP
|
|
125
165
|
|
|
126
166
|
The MCP server is for chat apps and agents that can't run shell commands. Agents with a shell do better with the skill, since they can run the CLI and see its output directly.
|
package/dist/api.js
CHANGED
|
@@ -97,19 +97,22 @@ export async function build(specText, opts) {
|
|
|
97
97
|
}
|
|
98
98
|
else
|
|
99
99
|
xml = await straightenDetours(b, toMxfile(diagram, l), detourEdges(diagram, l));
|
|
100
|
-
|
|
100
|
+
const fixed = await fixLabels(b, xml);
|
|
101
|
+
xml = fixed.xml;
|
|
101
102
|
xml = stampEdgeRoutes(xml);
|
|
102
|
-
|
|
103
|
+
// Stamping routes changes attributes, not geometry: the cells fixLabels read still hold.
|
|
104
|
+
return finish(b, xml, { mode, warnings, direction: mode === 'merge' ? (old.meta.aiviz_direction ?? l.direction) : l.direction, edits, old, stats, cells: fixed.cells });
|
|
103
105
|
});
|
|
104
106
|
async function finish(b, xml, r) {
|
|
105
107
|
fs.mkdirSync(path.dirname(path.resolve(opts.out)), { recursive: true });
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
// Issued together: the browser renders the preview and lays out the lint pass
|
|
109
|
+
// while node subsets the fonts for the SVG.
|
|
110
|
+
const [file, , cells] = await Promise.all([kind === 'svg' ? b.renderSvg(xml) : xml, opts.png ? b.renderPng(xml, opts.png) : undefined, r.cells ?? b.inspect(xml)]);
|
|
111
|
+
fs.writeFileSync(opts.out, file);
|
|
109
112
|
return {
|
|
110
113
|
out: opts.out,
|
|
111
114
|
warnings: r.warnings,
|
|
112
|
-
check: check(
|
|
115
|
+
check: check(cells),
|
|
113
116
|
direction: r.direction,
|
|
114
117
|
mode: r.mode,
|
|
115
118
|
merge: r.stats,
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import module from 'node:module';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { parseArgs } from 'node:util';
|
|
5
|
-
import { build, checkFile, listIcons, render, show, tidy } from './api.js';
|
|
6
|
-
import { SpecError, jsonSchema } from './spec/parse.js';
|
|
7
|
-
import { KINDS, DIAGRAM_TYPES } from './spec/model.js';
|
|
8
|
-
import { buildLines, checkLines } from './report.js';
|
|
9
6
|
const USAGE = `linework: AI-written, hand-editable diagrams (YAML spec -> .drawio.svg)
|
|
10
7
|
|
|
11
8
|
Usage:
|
|
@@ -53,43 +50,89 @@ Usage:
|
|
|
53
50
|
Download the headless Chromium build that matches this version. Needed only
|
|
54
51
|
when no Chrome or Edge is installed, or to run inside an agent's sandbox.
|
|
55
52
|
`;
|
|
53
|
+
// Node 22.1+ can keep compiled bytecode for linework's modules on disk, which
|
|
54
|
+
// takes about a quarter off loading them on every run after the first. Older
|
|
55
|
+
// Node has no such call. NODE_DISABLE_COMPILE_CACHE=1 turns it off.
|
|
56
|
+
module.enableCompileCache?.();
|
|
56
57
|
async function main(argv) {
|
|
57
58
|
const [cmd, ...rest] = argv;
|
|
58
|
-
const { values, positionals } = parseArgs({
|
|
59
|
-
args: rest,
|
|
60
|
-
allowPositionals: true,
|
|
61
|
-
options: {
|
|
62
|
-
out: { type: 'string', short: 'o' },
|
|
63
|
-
png: { type: 'string' },
|
|
64
|
-
force: { type: 'boolean', default: false },
|
|
65
|
-
relayout: { type: 'boolean', default: false },
|
|
66
|
-
geometry: { type: 'boolean', default: false },
|
|
67
|
-
quiet: { type: 'boolean', short: 'q', default: false },
|
|
68
|
-
block: { type: 'string' },
|
|
69
|
-
scale: { type: 'string' },
|
|
70
|
-
'no-open': { type: 'boolean', default: false },
|
|
71
|
-
port: { type: 'string' },
|
|
72
|
-
root: { type: 'string' },
|
|
73
|
-
to: { type: 'string' },
|
|
74
|
-
pull: { type: 'boolean', default: false },
|
|
75
|
-
help: { type: 'boolean', short: 'h' },
|
|
76
|
-
},
|
|
77
|
-
});
|
|
59
|
+
const { values, positionals } = parseArgs({ args: rest, allowPositionals: true, options: OPTIONS });
|
|
78
60
|
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h' || values.help)
|
|
79
61
|
return void process.stdout.write(USAGE);
|
|
62
|
+
// Commands that draw start Chromium first and load the rest of linework while it
|
|
63
|
+
// boots and loads draw.io: the two take about the same time, so they overlap.
|
|
64
|
+
const needsBrowser = ['build', 'render', 'check', 'tidy', 'icons'].includes(cmd) || (cmd === 'import' && !!values.out);
|
|
65
|
+
const launching = needsBrowser ? import('./render/browser.js').then((m) => m.DrawioBrowser.launch({ scale: cmd === 'render' && values.scale ? Number(values.scale) : 2 })) : undefined;
|
|
66
|
+
// A failure here surfaces where the browser is awaited; don't report it twice.
|
|
67
|
+
launching?.catch(() => { });
|
|
68
|
+
try {
|
|
69
|
+
await run(cmd, values, positionals, launching);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
if (launching)
|
|
73
|
+
await (await launching.catch(() => undefined))?.close();
|
|
74
|
+
}
|
|
75
|
+
// The font subsetter's WebAssembly keeps node alive for ~100ms after the work
|
|
76
|
+
// is done, with nothing left to run. A drawing command is finished here.
|
|
77
|
+
if (launching)
|
|
78
|
+
await exitWhenFlushed();
|
|
79
|
+
}
|
|
80
|
+
/** A small spec with a group, labels and a cycle: enough to exercise ELK's main paths. */
|
|
81
|
+
const WARM_SPEC = 'type: architecture\ngroups: { g: G }\nnodes:\n a: { in: g }\n b: { in: g }\n c: {}\nedges:\n - a -> b: x\n - b -> c: y\n - c -> a\n';
|
|
82
|
+
async function warmUp() {
|
|
83
|
+
const [{ parseSpecText }, { layout }] = await Promise.all([import('./spec/parse.js'), import('./layout/layout.js')]);
|
|
84
|
+
await layout(parseSpecText(WARM_SPEC).diagram);
|
|
85
|
+
}
|
|
86
|
+
function exitWhenFlushed() {
|
|
87
|
+
return new Promise(() => {
|
|
88
|
+
let pending = 2;
|
|
89
|
+
const done = () => --pending === 0 && process.exit();
|
|
90
|
+
process.stdout.write('', done);
|
|
91
|
+
process.stderr.write('', done);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const OPTIONS = {
|
|
95
|
+
out: { type: 'string', short: 'o' },
|
|
96
|
+
png: { type: 'string' },
|
|
97
|
+
force: { type: 'boolean', default: false },
|
|
98
|
+
relayout: { type: 'boolean', default: false },
|
|
99
|
+
geometry: { type: 'boolean', default: false },
|
|
100
|
+
quiet: { type: 'boolean', short: 'q', default: false },
|
|
101
|
+
block: { type: 'string' },
|
|
102
|
+
scale: { type: 'string' },
|
|
103
|
+
'no-open': { type: 'boolean', default: false },
|
|
104
|
+
port: { type: 'string' },
|
|
105
|
+
root: { type: 'string' },
|
|
106
|
+
to: { type: 'string' },
|
|
107
|
+
pull: { type: 'boolean', default: false },
|
|
108
|
+
help: { type: 'boolean', short: 'h' },
|
|
109
|
+
};
|
|
110
|
+
async function run(cmd, values, positionals, launching) {
|
|
111
|
+
const browser = async () => {
|
|
112
|
+
// ELK compiles its code on first use. Laying out a tiny graph while Chromium
|
|
113
|
+
// is still starting takes that cost off the real layout.
|
|
114
|
+
if (launching && ['build', 'import'].includes(cmd))
|
|
115
|
+
await warmUp();
|
|
116
|
+
return launching;
|
|
117
|
+
};
|
|
118
|
+
const { buildLines } = await import('./report.js');
|
|
80
119
|
switch (cmd) {
|
|
81
120
|
case 'build': {
|
|
82
121
|
const src = positionals[0];
|
|
83
122
|
if (!src || !values.out)
|
|
84
123
|
fail('usage: linework build <spec.yaml|-> -o <out.drawio.svg>');
|
|
85
124
|
const text = src === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(src, 'utf8');
|
|
86
|
-
const
|
|
125
|
+
const { build } = await import('./api.js');
|
|
126
|
+
// Spec errors first: they shouldn't wait on, or hide behind, a browser problem.
|
|
127
|
+
(await import('./spec/parse.js')).parseSpecText(text);
|
|
128
|
+
const res = await build(text, { out: values.out, png: values.png, force: values.force, relayout: values.relayout, browser: await browser() });
|
|
87
129
|
for (const line of buildLines(res, values.png))
|
|
88
130
|
console.log(line);
|
|
89
131
|
return;
|
|
90
132
|
}
|
|
91
133
|
case 'show': {
|
|
92
134
|
const file = positionals[0] ?? fail('usage: linework show <file.drawio.svg>');
|
|
135
|
+
const { show } = await import('./api.js');
|
|
93
136
|
const res = show(file, { geometry: values.geometry });
|
|
94
137
|
process.stdout.write(res.yaml);
|
|
95
138
|
if (values.quiet)
|
|
@@ -107,30 +150,26 @@ async function main(argv) {
|
|
|
107
150
|
const file = positionals[0];
|
|
108
151
|
if (!file || !values.out)
|
|
109
152
|
fail('usage: linework render <file.drawio.svg> -o <out.png>');
|
|
110
|
-
const {
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
await render(file, values.out, b);
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
await b.close();
|
|
117
|
-
}
|
|
153
|
+
const { render } = await import('./api.js');
|
|
154
|
+
await render(file, values.out, await browser());
|
|
118
155
|
console.log(`wrote ${values.out}`);
|
|
119
156
|
return;
|
|
120
157
|
}
|
|
121
158
|
case 'check': {
|
|
122
159
|
const file = positionals[0] ?? fail('usage: linework check <file.drawio.svg>');
|
|
123
|
-
const
|
|
124
|
-
|
|
160
|
+
const { checkFile } = await import('./api.js');
|
|
161
|
+
const res = await checkFile(file, await browser());
|
|
162
|
+
await printCheck(res);
|
|
125
163
|
if (res.findings.some((f) => f.level === 'error'))
|
|
126
164
|
process.exitCode = 1;
|
|
127
165
|
return;
|
|
128
166
|
}
|
|
129
167
|
case 'tidy': {
|
|
130
168
|
const file = positionals[0] ?? fail('usage: linework tidy <file.drawio.svg>');
|
|
131
|
-
const
|
|
169
|
+
const { tidy } = await import('./api.js');
|
|
170
|
+
const res = await tidy(file, await browser());
|
|
132
171
|
console.log(res.fixed ? `rerouted ${res.fixed} edges in ${file}` : 'nothing to fix');
|
|
133
|
-
printCheck(res.check);
|
|
172
|
+
await printCheck(res.check);
|
|
134
173
|
return;
|
|
135
174
|
}
|
|
136
175
|
case 'edit': {
|
|
@@ -169,16 +208,18 @@ async function main(argv) {
|
|
|
169
208
|
const yaml = toYaml(res.diagram, 'imported from Mermaid');
|
|
170
209
|
if (!values.out)
|
|
171
210
|
return void process.stdout.write(yaml);
|
|
172
|
-
const
|
|
211
|
+
const { build } = await import('./api.js');
|
|
212
|
+
const built = await build(yaml, { out: values.out, png: values.png, relayout: values.relayout, force: values.force, browser: await browser() });
|
|
173
213
|
console.log(`wrote ${built.out} (${built.check.stats.nodes} nodes, ${built.check.stats.edges} edges, ${built.direction})`);
|
|
174
|
-
printCheck(built.check);
|
|
214
|
+
await printCheck(built.check);
|
|
175
215
|
return;
|
|
176
216
|
}
|
|
177
217
|
case 'icons': {
|
|
178
218
|
const provider = positionals[0];
|
|
179
219
|
if (!provider || !['aws', 'gcp', 'k8s', 'azure'].includes(provider))
|
|
180
220
|
fail('usage: linework icons <aws|gcp|k8s|azure> [filter]');
|
|
181
|
-
const
|
|
221
|
+
const { listIcons } = await import('./api.js');
|
|
222
|
+
const names = await listIcons(provider, positionals[1], await browser());
|
|
182
223
|
console.log(names.map((n) => `${provider}.${n}`).join('\n') || 'no matches');
|
|
183
224
|
return;
|
|
184
225
|
}
|
|
@@ -229,6 +270,7 @@ async function main(argv) {
|
|
|
229
270
|
return;
|
|
230
271
|
}
|
|
231
272
|
case 'kinds': {
|
|
273
|
+
const { KINDS, DIAGRAM_TYPES } = await import('./spec/model.js');
|
|
232
274
|
const types = (positionals.length ? positionals : DIAGRAM_TYPES);
|
|
233
275
|
for (const t of types) {
|
|
234
276
|
if (!KINDS[t])
|
|
@@ -239,9 +281,11 @@ async function main(argv) {
|
|
|
239
281
|
}
|
|
240
282
|
return;
|
|
241
283
|
}
|
|
242
|
-
case 'schema':
|
|
284
|
+
case 'schema': {
|
|
285
|
+
const { jsonSchema } = await import('./spec/parse.js');
|
|
243
286
|
console.log(JSON.stringify(jsonSchema(), null, 2));
|
|
244
287
|
return;
|
|
288
|
+
}
|
|
245
289
|
case 'install-browser': {
|
|
246
290
|
const { browserInstallCommand } = await import('./render/browser.js');
|
|
247
291
|
const { spawnSync } = await import('node:child_process');
|
|
@@ -255,7 +299,8 @@ async function main(argv) {
|
|
|
255
299
|
fail(`unknown command "${cmd}"\n\n${USAGE}`);
|
|
256
300
|
}
|
|
257
301
|
}
|
|
258
|
-
function printCheck(res) {
|
|
302
|
+
async function printCheck(res) {
|
|
303
|
+
const { checkLines } = await import('./report.js');
|
|
259
304
|
for (const line of checkLines(res))
|
|
260
305
|
console.log(line);
|
|
261
306
|
}
|
|
@@ -264,7 +309,7 @@ function fail(msg) {
|
|
|
264
309
|
process.exit(2);
|
|
265
310
|
}
|
|
266
311
|
main(process.argv.slice(2)).catch((e) => {
|
|
267
|
-
if (e
|
|
312
|
+
if (e?.name === 'SpecError') {
|
|
268
313
|
console.error('spec error:');
|
|
269
314
|
for (const i of e.issues)
|
|
270
315
|
console.error(` ${i.path ? i.path + ': ' : ''}${i.message}`);
|
package/dist/drawio/theme.js
CHANGED
|
@@ -76,10 +76,10 @@ function lightPaint(sw, neutral, text, muted, accent) {
|
|
|
76
76
|
return (color, highlight) => {
|
|
77
77
|
if (highlight) {
|
|
78
78
|
const s = sw[color === 'neutral' || color === 'ink' ? accent : color];
|
|
79
|
-
return { fill: s.solid, stroke: s.solid, text: '#FFFFFF', sub: 'rgba(255,255,255,0.
|
|
79
|
+
return { fill: s.solid, stroke: s.solid, text: '#FFFFFF', sub: 'rgba(255,255,255,0.9)', strokeWidth: 1 };
|
|
80
80
|
}
|
|
81
81
|
if (color === 'ink')
|
|
82
|
-
return { fill: text, stroke: text, text: '#FFFFFF', sub:
|
|
82
|
+
return { fill: text, stroke: text, text: '#FFFFFF', sub: 'rgba(255,255,255,0.72)', strokeWidth: 1 };
|
|
83
83
|
if (color === 'neutral')
|
|
84
84
|
return { ...neutral, text, sub: muted, strokeWidth: 1 };
|
|
85
85
|
const s = sw[color];
|
|
@@ -92,16 +92,16 @@ const CLEAN = {
|
|
|
92
92
|
canvas: '#FFFFFF',
|
|
93
93
|
text: '#17171C',
|
|
94
94
|
muted: '#6B6B76',
|
|
95
|
-
edge: '#
|
|
95
|
+
edge: '#868691',
|
|
96
96
|
edgeText: '#52525C',
|
|
97
97
|
edgeWidth: 1.25,
|
|
98
98
|
accent: 'blue',
|
|
99
99
|
kindColors: false,
|
|
100
100
|
radius: 10,
|
|
101
|
-
group: { fill: '#F5F5F7', nested: '#EBEBEF', stroke: 'none', dashed: false, text: '#
|
|
101
|
+
group: { fill: '#F5F5F7', nested: '#EBEBEF', stroke: 'none', dashed: false, text: '#61616C' },
|
|
102
102
|
note: { fill: '#FFF4CC', stroke: 'none', text: '#5C4300' },
|
|
103
103
|
well: '#F3F3F5',
|
|
104
|
-
paint: lightPaint(LIGHT_SW, { fill: '#FFFFFF', stroke: '#
|
|
104
|
+
paint: lightPaint(LIGHT_SW, { fill: '#FFFFFF', stroke: '#C4C4CC' }, '#17171C', '#6B6B76', 'blue'),
|
|
105
105
|
edgePaint: (color, hl) => {
|
|
106
106
|
const s = LIGHT_SW[color ?? 'blue'];
|
|
107
107
|
if (hl)
|
|
@@ -115,12 +115,12 @@ const SOFT = {
|
|
|
115
115
|
name: 'soft',
|
|
116
116
|
kindColors: true,
|
|
117
117
|
radius: 12,
|
|
118
|
-
group: { fill: '#F7F7F9', nested: '#EFEFF3', stroke: 'none', dashed: false, text: '#
|
|
118
|
+
group: { fill: '#F7F7F9', nested: '#EFEFF3', stroke: 'none', dashed: false, text: '#61616C' },
|
|
119
119
|
paint: (color, hl) => {
|
|
120
120
|
if (hl || color === 'ink')
|
|
121
121
|
return CLEAN.paint(color, hl);
|
|
122
122
|
const c = color === 'neutral' ? 'gray' : color;
|
|
123
|
-
return { fill: SOFT_TINT[c], stroke: 'none', text: c === 'gray' ? '#17171C' : LIGHT_SW[c].ink, sub: c === 'gray' ? '#6B6B76' : LIGHT_SW[c].ink + '
|
|
123
|
+
return { fill: SOFT_TINT[c], stroke: 'none', text: c === 'gray' ? '#17171C' : LIGHT_SW[c].ink, sub: c === 'gray' ? '#6B6B76' : LIGHT_SW[c].ink + 'E0', strokeWidth: 1 };
|
|
124
124
|
},
|
|
125
125
|
};
|
|
126
126
|
const DARK = {
|
|
@@ -141,7 +141,7 @@ const DARK = {
|
|
|
141
141
|
paint: (color, hl) => {
|
|
142
142
|
if (hl) {
|
|
143
143
|
const s = DARK_SW[color === 'neutral' || color === 'ink' ? 'blue' : color];
|
|
144
|
-
return { fill: s.solid, stroke: s.solid, text: '#FFFFFF', sub: 'rgba(255,255,255,0.
|
|
144
|
+
return { fill: s.solid, stroke: s.solid, text: '#FFFFFF', sub: 'rgba(255,255,255,0.9)', strokeWidth: 1 };
|
|
145
145
|
}
|
|
146
146
|
if (color === 'ink')
|
|
147
147
|
return { fill: '#E6EDF3', stroke: '#E6EDF3', text: '#0D1117', sub: '#59636E', strokeWidth: 1 };
|
|
@@ -212,7 +212,7 @@ const BLUEPRINT = {
|
|
|
212
212
|
accent: 'blue',
|
|
213
213
|
kindColors: false,
|
|
214
214
|
radius: 4,
|
|
215
|
-
group: { fill: 'none', stroke: '#
|
|
215
|
+
group: { fill: 'none', stroke: '#7489AF', dashed: true, text: '#5A6F92' },
|
|
216
216
|
note: { fill: '#EAF0FA', stroke: '#9DB0D0', text: BP_INK },
|
|
217
217
|
well: '#EAF0FA',
|
|
218
218
|
paint: (color, hl) => {
|
|
@@ -236,7 +236,7 @@ const BLUEPRINT = {
|
|
|
236
236
|
// Other diagram types use `clean`.
|
|
237
237
|
const C4_KIND = {
|
|
238
238
|
person: { fill: '#1E3466', stroke: '#1E3466', text: '#FFFFFF', type: 'Person' },
|
|
239
|
-
external: { fill: '#
|
|
239
|
+
external: { fill: '#626A7A', stroke: '#626A7A', text: '#FFFFFF', type: 'External system' },
|
|
240
240
|
};
|
|
241
241
|
const C4_CONTAINER = { fill: '#456A9E', stroke: '#456A9E', text: '#FFFFFF', type: 'Container' };
|
|
242
242
|
const C4 = { ...CLEAN, name: 'c4', group: { fill: 'none', stroke: '#444444', dashed: true, text: '#444444' }, edge: '#707070', edgeText: '#444444' };
|
|
@@ -250,7 +250,7 @@ export function paintFor(type, kind, theme, look = {}) {
|
|
|
250
250
|
const t = palette(theme);
|
|
251
251
|
if (theme === 'c4' && type === 'architecture' && !look.highlight && !look.color) {
|
|
252
252
|
const c = C4_KIND[kind] ?? C4_CONTAINER;
|
|
253
|
-
return { fill: c.fill, stroke: c.stroke, text: c.text, sub: 'rgba(255,255,255,0.
|
|
253
|
+
return { fill: c.fill, stroke: c.stroke, text: c.text, sub: 'rgba(255,255,255,0.92)', strokeWidth: 1 };
|
|
254
254
|
}
|
|
255
255
|
const k = kindDef(type, kind);
|
|
256
256
|
// A highlight is the style's accent unless the spec names a color.
|
|
@@ -274,7 +274,9 @@ export function nodeStyle(type, kind, theme = 'clean', look = {}) {
|
|
|
274
274
|
const r = t.radius;
|
|
275
275
|
switch (k.shape) {
|
|
276
276
|
case 'cylinder':
|
|
277
|
-
|
|
277
|
+
// With an outline the lid is drawn, and the text centres on the face below
|
|
278
|
+
// it. Without one (soft) the lid is invisible: centre on the whole shape.
|
|
279
|
+
return `shape=cylinder3;boundedLbl=${stroke === 'none' ? 0 : 1};backgroundOutline=1;size=${CYL_SIZE};${base}`;
|
|
278
280
|
case 'pill':
|
|
279
281
|
return `rounded=1;arcSize=50;${base}`;
|
|
280
282
|
case 'diamond':
|
|
@@ -298,6 +300,17 @@ export function nodeStyle(type, kind, theme = 'clean', look = {}) {
|
|
|
298
300
|
return `rounded=1;absoluteArcSize=1;arcSize=${r * 2};${base}`;
|
|
299
301
|
}
|
|
300
302
|
}
|
|
303
|
+
/** Depth of a cylinder's lid and base curve. */
|
|
304
|
+
export const CYL_SIZE = 7;
|
|
305
|
+
/**
|
|
306
|
+
* Vertical centre of a node's text, from the top of its box. draw.io bounds a
|
|
307
|
+
* cylinder's label between its lid (2 x size) and 0.3 x size above the base.
|
|
308
|
+
*/
|
|
309
|
+
export function textMiddle(style, h) {
|
|
310
|
+
if (!/shape=cylinder3/.test(style) || !/boundedLbl=1/.test(style))
|
|
311
|
+
return h / 2;
|
|
312
|
+
return (2 * CYL_SIZE + h - 0.3 * CYL_SIZE) / 2;
|
|
313
|
+
}
|
|
301
314
|
/** Horizontal text padding inside a node, each side. */
|
|
302
315
|
export const PAD_X = 16;
|
|
303
316
|
/** Vertical text padding inside a node, each side. */
|
|
@@ -309,6 +322,8 @@ export function fieldStyle(theme = 'clean') {
|
|
|
309
322
|
return ('text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=0;spacingRight=0;overflow=fill;' +
|
|
310
323
|
`rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;whiteSpace=wrap;${fontPart(t, t.type.size.field, t.text)}`);
|
|
311
324
|
}
|
|
325
|
+
/** Left inset of a group's title. */
|
|
326
|
+
export const GROUP_INSET = 14;
|
|
312
327
|
export function groupStyle(theme = 'clean', look = {}, depth = 0) {
|
|
313
328
|
const t = palette(theme);
|
|
314
329
|
let { fill, stroke, text } = t.group;
|
|
@@ -324,7 +339,7 @@ export function groupStyle(theme = 'clean', look = {}, depth = 0) {
|
|
|
324
339
|
return (`rounded=1;absoluteArcSize=1;arcSize=${t.radius * 2 + 4};html=1;whiteSpace=wrap;container=1;collapsible=0;` +
|
|
325
340
|
`fillColor=${fill};strokeColor=${stroke};strokeWidth=${look.highlight ? 1.5 : 1};` +
|
|
326
341
|
(t.group.dashed ? dashes(t) : '') +
|
|
327
|
-
|
|
342
|
+
`verticalAlign=top;align=left;spacingLeft=${GROUP_INSET};spacingTop=9;` +
|
|
328
343
|
`${fontPart(t, t.type.size.group, text)}fontStyle=1;${t.extra}`);
|
|
329
344
|
}
|
|
330
345
|
/** Group title HTML: sentence case, except blueprint's drafting capitals. */
|
|
@@ -358,8 +373,10 @@ export function lifelineStyle(kind, theme = 'clean', headH = 48, look = {}) {
|
|
|
358
373
|
const t = palette(theme);
|
|
359
374
|
const c = paintFor('sequence', kind, theme, look);
|
|
360
375
|
const k = kindDef('sequence', kind);
|
|
361
|
-
|
|
362
|
-
//
|
|
376
|
+
// The dashed stem takes the stroke color, so a highlighted head keeps the plain
|
|
377
|
+
// participant line: the accent stays on the head and the highlighted messages.
|
|
378
|
+
const quiet = t.paint('neutral', false).stroke;
|
|
379
|
+
const stroke = look.highlight ? (quiet === 'none' ? t.edge : quiet) : c.stroke === 'none' ? (k.dashed ? '#A1A1AA' : c.fill) : c.stroke;
|
|
363
380
|
return (`shape=umlLifeline;perimeter=lifelinePerimeter;container=1;collapsible=0;recursiveResize=0;outlineConnect=0;size=${headH};` +
|
|
364
381
|
`html=1;whiteSpace=wrap;rounded=1;absoluteArcSize=1;arcSize=${t.radius * 2};fillColor=${c.fill};strokeColor=${stroke};strokeWidth=${Math.min(1.5, c.strokeWidth)};` +
|
|
365
382
|
`spacingLeft=${PAD_X - 4};spacingRight=${PAD_X - 4};` +
|
|
@@ -384,12 +401,17 @@ export function messageStyle(style, theme = 'clean', self = false, look = {}) {
|
|
|
384
401
|
(self ? 'edgeStyle=orthogonalEdgeStyle;rounded=1;align=left;spacingLeft=6;verticalAlign=middle;' : '') +
|
|
385
402
|
`${fontPart(t, t.type.size.edge, p.text)}${look.highlight ? 'fontStyle=1;' : ''}labelBackgroundColor=${t.canvas};labelPadding=1 5;${t.extra}`);
|
|
386
403
|
}
|
|
404
|
+
/** Padding of a sequence frame's operator tab, and its inset from the frame's corner. */
|
|
405
|
+
export const FRAME_TAB_X = 9, FRAME_TAB_Y = 3, FRAME_TAB_INSET = 4;
|
|
387
406
|
export function frameStyle(theme = 'clean') {
|
|
388
407
|
const t = palette(theme);
|
|
389
|
-
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
|
|
408
|
+
// The operator (alt, loop, opt) sits in a small tab in the frame's corner: UML's
|
|
409
|
+
// pentagon, drawn as a chip, inset the same from both sides so its square corner
|
|
410
|
+
// stays inside the frame's rounded one. draw.io draws an HTML label's background
|
|
411
|
+
// 1px left of and 3.5px below where its spacing puts it; the +1 and -3.5 cancel that.
|
|
412
|
+
return (`rounded=1;absoluteArcSize=1;arcSize=${t.radius * 2};whiteSpace=wrap;html=1;container=0;verticalAlign=top;align=left;spacing=0;spacingLeft=${FRAME_TAB_X + FRAME_TAB_INSET + 1};spacingTop=${FRAME_TAB_Y + FRAME_TAB_INSET - 3.5};` +
|
|
413
|
+
`labelBackgroundColor=${t.well};labelPadding=${FRAME_TAB_Y} ${FRAME_TAB_X};` +
|
|
414
|
+
`fillColor=none;strokeColor=${t.edge};strokeWidth=1;${fontPart(t, t.type.size.group, t.text)}fontStyle=1;${t.extra}`);
|
|
393
415
|
}
|
|
394
416
|
export function guardStyle(theme = 'clean') {
|
|
395
417
|
const t = palette(theme);
|
|
@@ -397,7 +419,7 @@ export function guardStyle(theme = 'clean') {
|
|
|
397
419
|
}
|
|
398
420
|
export function separatorStyle(theme = 'clean') {
|
|
399
421
|
const t = palette(theme);
|
|
400
|
-
return `endArrow=none;dashed=1;dashPattern=4 4;html=1;strokeColor=${t.
|
|
422
|
+
return `endArrow=none;dashed=1;dashPattern=4 4;html=1;strokeColor=${t.edge};`;
|
|
401
423
|
}
|
|
402
424
|
export function noteStyle(theme = 'clean') {
|
|
403
425
|
const t = palette(theme);
|