flowfig 0.10.0 → 0.11.0
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 +6 -4
- package/dist/auto.js +104 -9
- package/dist/check.d.ts +2 -1
- package/dist/check.js +9 -4
- package/dist/cli.js +7 -1
- package/dist/index.js +48 -24
- package/dist/model.d.ts +8 -3
- package/dist/model.js +39 -20
- package/dist/svg.js +10 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ jobs:
|
|
|
65
65
|
- uses: actions/setup-node@v4
|
|
66
66
|
with:
|
|
67
67
|
node-version: 22
|
|
68
|
-
- uses: iamalvisng/flowfig@v0.
|
|
68
|
+
- uses: iamalvisng/flowfig@v0.11.0
|
|
69
69
|
with:
|
|
70
70
|
figures: 'docs/**/*.svg' # default **/*.svg
|
|
71
71
|
# coverage: true # also fail the job if a figure is stale
|
|
@@ -530,7 +530,7 @@ jobs:
|
|
|
530
530
|
- uses: actions/setup-node@v4
|
|
531
531
|
with:
|
|
532
532
|
node-version: 22
|
|
533
|
-
- uses: iamalvisng/flowfig@v0.
|
|
533
|
+
- uses: iamalvisng/flowfig@v0.11.0
|
|
534
534
|
with:
|
|
535
535
|
figures: 'docs/**/*.svg' # default **/*.svg
|
|
536
536
|
```
|
|
@@ -850,8 +850,10 @@ The player has these controls:
|
|
|
850
850
|
- A hover on a box, an edge or a rail row with a `source` shows the `source`. The SVG shows it as a tooltip too.
|
|
851
851
|
|
|
852
852
|
The player props are the spec fields. `theme` sets the colors, `speed` sets the packet time, and `autoplay={false}` stops the
|
|
853
|
-
auto start. `check` runs the check rules. If a figure is wider than its container, the player
|
|
854
|
-
|
|
853
|
+
auto start. `check` runs the check rules. If a figure is wider than its container, the player changes the figure to fit.
|
|
854
|
+
In a box under 600 px, the player keeps reader text at 11 px or more. If the map does not fit, the player draws the map as a column.
|
|
855
|
+
In a box of 600 px or more, the player folds the map only when text would fall under 9 px.
|
|
856
|
+
A lanes map or a timeline map stops at that size and scrolls sideways in its pane. If the reader asks for reduced motion, the player shows the last beat and does not move packets.
|
|
855
857
|
|
|
856
858
|
The player has no dark theme of its own. For dark mode, set the `--fig-*` CSS variables on `.flowfig`:
|
|
857
859
|
|
package/dist/auto.js
CHANGED
|
@@ -3,6 +3,71 @@ const PAGE = 830, PAD = 24, SWEEPS = 8, TALL = 2.5,
|
|
|
3
3
|
// A 20-box chain measures 105 px for each rank.
|
|
4
4
|
RANK_H = 105, MIN_TEXT = 10;
|
|
5
5
|
const flat = (it) => (isGroup(it) && it.label == null && it.id == null ? it.children.flatMap(flat) : [it]);
|
|
6
|
+
export const rowBreaks = new WeakSet();
|
|
7
|
+
const less = (p, q) => {
|
|
8
|
+
const k = p.findIndex((v, i) => v !== q[i]);
|
|
9
|
+
return k >= 0 && p[k] < q[k];
|
|
10
|
+
};
|
|
11
|
+
function arcFold(run, long, pos, carded, edges) {
|
|
12
|
+
const m = run.length;
|
|
13
|
+
const spans = long.map((e) => [pos.get(e.from), pos.get(e.to)].sort((p, q) => p - q));
|
|
14
|
+
const wide = new Map();
|
|
15
|
+
const width = (i, j) => wide.get(i * m + j) ?? wide.set(i * m + j, itemWidth({ direction: 'row', children: run.slice(i, j) }, carded, edges)).get(i * m + j);
|
|
16
|
+
let pick;
|
|
17
|
+
for (let code = 0; long.length <= 6 && code < 3 ** long.length; code++) {
|
|
18
|
+
const sides = spans.map((_, k) => Math.floor(code / 3 ** k) % 3);
|
|
19
|
+
const need = new Set(), full = new Set(), ban = new Set();
|
|
20
|
+
spans.forEach(([a, b], k) => {
|
|
21
|
+
if (!sides[k])
|
|
22
|
+
for (let c = a + 1; c <= b; c++)
|
|
23
|
+
ban.add(c);
|
|
24
|
+
else
|
|
25
|
+
for (const c of sides[k] === 1 ? [a + 1, b + 1] : [a, b]) {
|
|
26
|
+
need.add(c);
|
|
27
|
+
if (sides[k] === 1)
|
|
28
|
+
full.add(c);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const plans = [{ cost: [0, 0], from: -1 }];
|
|
32
|
+
for (let j = 1; j <= m; j++)
|
|
33
|
+
for (let i = j - 1; i >= Math.max(0, j - 4); i--) {
|
|
34
|
+
if ((i < j - 1 && need.has(i + 1)) || width(i, j) > PAGE - 2 * PAD)
|
|
35
|
+
break;
|
|
36
|
+
const prev = plans[i];
|
|
37
|
+
if (!prev || (i && ban.has(i)) || (full.has(j) && j - i < 4))
|
|
38
|
+
continue;
|
|
39
|
+
const cost = [prev.cost[0] + 1, Math.max(prev.cost[1], width(i, j))];
|
|
40
|
+
if (!plans[j] || less(cost, plans[j].cost))
|
|
41
|
+
plans[j] = { cost, from: i };
|
|
42
|
+
}
|
|
43
|
+
if (!plans[m] || (pick && !less(plans[m].cost, pick.cost)))
|
|
44
|
+
continue;
|
|
45
|
+
const cuts = [];
|
|
46
|
+
for (let j = m; j > 0; j = plans[j].from)
|
|
47
|
+
cuts.unshift(plans[j].from);
|
|
48
|
+
const line = (k) => cuts.findLastIndex((c) => c <= k);
|
|
49
|
+
const rowW = (r) => width(cuts[r], cuts[r + 1] ?? m);
|
|
50
|
+
const clash = spans.some(([a, b], k) => {
|
|
51
|
+
const [ra, rb] = [line(a), line(b)];
|
|
52
|
+
const inner = Array.from({ length: rb - ra + 1 }, (_, r) => rowW(ra + r));
|
|
53
|
+
return ((!sides[k] && ra > 0 && ra < cuts.length - 1) ||
|
|
54
|
+
(sides[k] === 1 && Math.min(rowW(ra), rowW(rb)) < Math.max(...inner)) ||
|
|
55
|
+
spans.some(([p, q], l) => l > k && sides[k] && sides[k] === sides[l] && a < q && p < b));
|
|
56
|
+
});
|
|
57
|
+
if (!clash)
|
|
58
|
+
pick = { cost: plans[m].cost, cuts, sides };
|
|
59
|
+
}
|
|
60
|
+
if (!pick)
|
|
61
|
+
return undefined;
|
|
62
|
+
const top = pick.cuts[1] ?? m;
|
|
63
|
+
long.forEach((e, k) => (e.around = [pos.get(e.from) < top ? 'above' : 'below', 'right', 'left'][pick.sides[k]]));
|
|
64
|
+
const rows = pick.cuts.map((c, k) => ({ direction: 'row', children: run.slice(c, pick.cuts[k + 1] ?? m) }));
|
|
65
|
+
const line = (id) => rows.findIndex((l) => idsIn(l).includes(id));
|
|
66
|
+
for (const e of edges)
|
|
67
|
+
if (!long.includes(e) && line(e.from) >= 0 && line(e.from) < line(e.to))
|
|
68
|
+
rowBreaks.add(e);
|
|
69
|
+
return { direction: 'column', align: 'start', children: rows };
|
|
70
|
+
}
|
|
6
71
|
function seenOrder(fig) {
|
|
7
72
|
const at = new Map();
|
|
8
73
|
const byId = new Map(fig.edges.map((e) => [edgeId(e), e]));
|
|
@@ -206,18 +271,46 @@ function level(items, dir, ctx) {
|
|
|
206
271
|
return rows;
|
|
207
272
|
const out = [];
|
|
208
273
|
let run = [];
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
274
|
+
let runAt = [];
|
|
275
|
+
const owner = new Map(via.flatMap((vs, i) => vs.map((v) => [v, pairs[i][2]])));
|
|
276
|
+
const lines = (part, per) => {
|
|
277
|
+
const k = Math.ceil(part.length / per), q = Math.ceil(part.length / k);
|
|
278
|
+
return Array.from({ length: k }, (_, i) => ({ direction: 'row', children: part.slice(i * q, (i + 1) * q) }));
|
|
279
|
+
};
|
|
280
|
+
const plain = (part) => {
|
|
281
|
+
const folded = part.length < 4
|
|
282
|
+
? undefined
|
|
283
|
+
: [4, 3].map((per) => lines(part, per)).find((ls) => ls.every((l) => itemWidth(l, ctx.carded, ctx.edges) <= PAGE - 2 * PAD));
|
|
284
|
+
out.push(...(folded ?? part));
|
|
212
285
|
};
|
|
213
286
|
const flush = () => {
|
|
214
|
-
const
|
|
215
|
-
|
|
287
|
+
const pos = new Map(run.map((r, k) => [r.id, k]));
|
|
288
|
+
const long = ctx.edges.filter((e) => pos.has(e.from) && pos.has(e.to) && Math.abs(pos.get(e.from) - pos.get(e.to)) > 1);
|
|
289
|
+
const local = runAt.every((i) => kept[i].every((x) => x < n || long.includes(owner.get(x))));
|
|
290
|
+
const folded = run.length >= 4 && long.length && local ? arcFold(run, long, pos, ctx.carded, ctx.edges) : undefined;
|
|
291
|
+
if (folded)
|
|
292
|
+
out.push(folded);
|
|
293
|
+
else {
|
|
294
|
+
let part = [];
|
|
295
|
+
run.forEach((r, k) => {
|
|
296
|
+
if (kept[runAt[k]].length === 1)
|
|
297
|
+
part.push(r);
|
|
298
|
+
else {
|
|
299
|
+
plain(part);
|
|
300
|
+
part = [];
|
|
301
|
+
out.push(r);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
plain(part);
|
|
305
|
+
}
|
|
216
306
|
run = [];
|
|
307
|
+
runAt = [];
|
|
217
308
|
};
|
|
218
309
|
rows.forEach((r, i) => {
|
|
219
|
-
if (kept[i].length === 1 && !isGroup(r))
|
|
310
|
+
if (kept[i].filter((x) => x < n).length === 1 && !isGroup(r)) {
|
|
220
311
|
run.push(r);
|
|
312
|
+
runAt.push(i);
|
|
313
|
+
}
|
|
221
314
|
else {
|
|
222
315
|
flush();
|
|
223
316
|
out.push(r);
|
|
@@ -243,7 +336,6 @@ function smallFont(f, carded) {
|
|
|
243
336
|
return 12;
|
|
244
337
|
return f.steps?.length ? 13.5 : 14;
|
|
245
338
|
}
|
|
246
|
-
export const rowBreaks = new WeakSet();
|
|
247
339
|
function fold(f, carded) {
|
|
248
340
|
const room = (PAGE * smallFont(f, carded)) / MIN_TEXT - 2 * PAD;
|
|
249
341
|
if (f.rail || itemWidth(f.layout, carded, f.edges, fitCap(f, PAGE)) <= room)
|
|
@@ -268,8 +360,11 @@ function fold(f, carded) {
|
|
|
268
360
|
if (!plans[j] || better(cost, plans[j].cost) < 0)
|
|
269
361
|
plans[j] = { cost, from: i };
|
|
270
362
|
}
|
|
271
|
-
if (!plans[n] || plans[n].cost[0] < 2)
|
|
272
|
-
|
|
363
|
+
if (!plans[n] || plans[n].cost[0] < 2) {
|
|
364
|
+
const long = f.edges.filter((e) => unit.has(e.from) && unit.has(e.to) && (e.around || Math.abs(unit.get(e.from) - unit.get(e.to)) > 1));
|
|
365
|
+
const layout = n >= 4 && long.some((e) => e.around) && !ranks.some(isGroup) ? arcFold(ranks, long, unit, carded, f.edges) : undefined;
|
|
366
|
+
return layout ? { ...f, layout } : f;
|
|
367
|
+
}
|
|
273
368
|
const cuts = [];
|
|
274
369
|
for (let j = n; j > 0; j = plans[j].from)
|
|
275
370
|
cuts.unshift(plans[j].from);
|
package/dist/check.d.ts
CHANGED
|
@@ -6,8 +6,9 @@ export declare function checkSpec(fig: FlowProps): Finding[];
|
|
|
6
6
|
export type CheckOptions = {
|
|
7
7
|
width?: number;
|
|
8
8
|
minText?: number;
|
|
9
|
+
auto?: boolean;
|
|
9
10
|
};
|
|
10
|
-
export declare function checkScene(scene: Scene, { width, minText }?: CheckOptions): Finding[];
|
|
11
|
+
export declare function checkScene(scene: Scene, { width, minText, auto }?: CheckOptions): Finding[];
|
|
11
12
|
/** The WCAG contrast ratio of two colors, rounded to 2 places; null if either cannot be read. */
|
|
12
13
|
export declare function contrast(a: string, b: string): number | null;
|
|
13
14
|
/** Text the reader cannot read: each text and background pair, in light, dark and any custom theme, needs 4.5:1 (WCAG AA). */
|
package/dist/check.js
CHANGED
|
@@ -209,7 +209,7 @@ const overlap = (a, b, pad) => a.x + pad < b.x + b.w - pad && b.x + pad < a.x +
|
|
|
209
209
|
const px = (n) => Math.round(n * 10) / 10;
|
|
210
210
|
// Measured corner touches go 3.5 px deep; real crossings go 24 px or more.
|
|
211
211
|
const TOUCH = 6;
|
|
212
|
-
export function checkScene(scene, { width = 830, minText = 10 } = {}) {
|
|
212
|
+
export function checkScene(scene, { width = 830, minText = 10, auto } = {}) {
|
|
213
213
|
const out = [];
|
|
214
214
|
for (const b of scene.boxes)
|
|
215
215
|
for (const t of b.texts) {
|
|
@@ -223,7 +223,7 @@ export function checkScene(scene, { width = 830, minText = 10 } = {}) {
|
|
|
223
223
|
const pts = path(e);
|
|
224
224
|
for (const b of scene.boxes)
|
|
225
225
|
if (b.id !== e.from && b.id !== e.to && pts.some((p) => inside(p, b.rect, TOUCH)))
|
|
226
|
-
out.push(err('edge-crosses-box', [e.id, b.id], `edge "${e.id}" passes through box "${b.id}": move a box, or set "auto": true on the root layout`));
|
|
226
|
+
out.push(err('edge-crosses-box', [e.id, b.id], `edge "${e.id}" passes through box "${b.id}": move a box${auto ? '' : ', or set "auto": true on the root layout'}`));
|
|
227
227
|
}
|
|
228
228
|
for (const e of scene.edges) {
|
|
229
229
|
const pts = path(e);
|
|
@@ -231,7 +231,7 @@ export function checkScene(scene, { width = 830, minText = 10 } = {}) {
|
|
|
231
231
|
const ratio = drawn / Math.max(1, Math.hypot(pts.at(-1).x - pts[0].x, pts.at(-1).y - pts[0].y));
|
|
232
232
|
// The README figures have a good edge at ratio 1.77 and 425 px.
|
|
233
233
|
if (ratio >= 1.6 && drawn >= 600)
|
|
234
|
-
out.push(warn('long-edge', [e.id], `edge "${e.id}" is ${px(ratio)} times the straight distance between its ends: put its ends in one row or column, route it with around, or set "auto": true on the root layout`));
|
|
234
|
+
out.push(warn('long-edge', [e.id], `edge "${e.id}" is ${px(ratio)} times the straight distance between its ends: put its ends in one row or column${auto ? ', or route it with around' : ', route it with around, or set "auto": true on the root layout'}`));
|
|
235
235
|
}
|
|
236
236
|
const pills = scene.edges.filter((e) => e.pts && e.label);
|
|
237
237
|
const under = new Set();
|
|
@@ -383,5 +383,10 @@ export function checkRendered(fig, opts, scene) {
|
|
|
383
383
|
ids: [id],
|
|
384
384
|
message: `edge "${id}" ends at a lane that no block on its side shows; the edge uses the nearest block`,
|
|
385
385
|
}));
|
|
386
|
-
return [
|
|
386
|
+
return [
|
|
387
|
+
...checkSpec(fig),
|
|
388
|
+
...checkScene(scene, { ...opts, auto: opts.auto ?? fig.layout.auto }),
|
|
389
|
+
...lost,
|
|
390
|
+
...checkTheme({ ...fig.theme, ...opts.theme }),
|
|
391
|
+
];
|
|
387
392
|
}
|
package/dist/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ import { loadSpec, reportLines, sortFindings, specOf, summaryLines, svgWithSpec,
|
|
|
28
28
|
import { check, render, toSvg } from './svg.js';
|
|
29
29
|
import { checkRendered } from './check.js';
|
|
30
30
|
import { coverageLine, links, parseSource, unsureLines, verifyReport } from './verify.js';
|
|
31
|
+
import { VERSION } from './version.js';
|
|
31
32
|
import { trace, traceLines } from './trace.js';
|
|
32
33
|
import { coverageLines, coverageReport } from './coverage.js';
|
|
33
34
|
const USAGE = `usage: flowfig <-|spec.json|figure.ts> [out.svg] [--open] render a figure; a spec on stdin with -
|
|
@@ -45,7 +46,8 @@ const USAGE = `usage: flowfig <-|spec.json|figure.ts> [out.svg] [--open] rende
|
|
|
45
46
|
flowfig open <figure.svg> [--html <path>] show the figure in the default browser
|
|
46
47
|
flowfig from-mermaid <file.mmd|file.md|-> [--out <dir>] convert Mermaid diagrams to specs
|
|
47
48
|
flowfig gif <figure.svg> [out.gif] [--step <n>] [--dark] [--fps <n>] [--scale <n>] [--mp4] write an animated GIF
|
|
48
|
-
flags for render and check: --strict (warnings are errors), --json, --width <px>, --min-text <px>, --no-check (render only), --no-verify (render only)
|
|
49
|
+
flags for render and check: --strict (warnings are errors), --json, --width <px>, --min-text <px>, --no-check (render only), --no-verify (render only)
|
|
50
|
+
flowfig --version, -v print the version`;
|
|
49
51
|
function usage(message) {
|
|
50
52
|
console.error(message);
|
|
51
53
|
process.exit(2);
|
|
@@ -73,6 +75,10 @@ if (!args[0] || ['help', '--help', '-h'].includes(args[0])) {
|
|
|
73
75
|
usage(USAGE);
|
|
74
76
|
process.exit(0);
|
|
75
77
|
}
|
|
78
|
+
if (['--version', '-v'].includes(args[0])) {
|
|
79
|
+
console.log(VERSION);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
76
82
|
if (args[0] === 'docs') {
|
|
77
83
|
const topic = args[1];
|
|
78
84
|
if (topic && !Object.hasOwn(TOPICS, topic))
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { foldedLabel, groupBox, layoutRail, railState, RAIL } from './rail.js';
|
|
|
6
6
|
import { textWidth } from './text.js';
|
|
7
7
|
import { checkScene, checkSpec, checkTheme } from './check.js';
|
|
8
8
|
import { autoLayout, rowBreaks } from './auto.js';
|
|
9
|
-
import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD, EDGE_OFF, EDGE_ON, LIGHT, TONES, ON_ACCENT, toneFill, toneTint, decisions, edgeId, groupGap, isGroup, isRows, lanePlan as planLanes, laneEnd, laneBlock, STUB_ROOM, labelPillW, str, altText, LANE_BLOCK_GAP, LANE_PAD, LANE_ROW_GAP, isLanesLayout, nodes, timelineBeats, timelineLayout, TL_AXIS_H, TL_AXIS_W, TL_BAR_H, TL_ROW_GAP, toBeat, diamondLines, diamondRoom, tightCopies, nodeWidth, labelLines, fitCap, LABEL_LINE, beatMs, STEP_HOLD_MS, playheadItem, loopStartItem, dateLabelRaised, edgeTip, fitScale, specKey, nextWide, } from './model.js';
|
|
9
|
+
import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD, EDGE_OFF, EDGE_ON, LIGHT, TONES, ON_ACCENT, toneFill, toneTint, decisions, edgeId, groupGap, isGroup, isRows, lanePlan as planLanes, laneEnd, laneBlock, STUB_ROOM, labelPillW, str, altText, LANE_BLOCK_GAP, LANE_PAD, LANE_ROW_GAP, isLanesLayout, nodes, timelineBeats, timelineLayout, TL_AXIS_H, TL_AXIS_W, TL_BAR_H, TL_ROW_GAP, toBeat, diamondLines, diamondRoom, tightCopies, nodeWidth, labelLines, fitCap, LABEL_LINE, beatMs, STEP_HOLD_MS, playheadItem, loopStartItem, dateLabelRaised, edgeTip, fitScale, shownFont, textFloor, specKey, nextWide, } from './model.js';
|
|
10
10
|
export { LIGHT, DARK } from './model.js';
|
|
11
11
|
const DEFAULTS = { ...LIGHT, font: 'inherit' };
|
|
12
12
|
const v = (k) => `var(--fig-${k}, ${DEFAULTS[k]})`;
|
|
@@ -62,11 +62,18 @@ export function Flow(props) {
|
|
|
62
62
|
const fold = !lanes && !timeline && props.rail !== 'only';
|
|
63
63
|
const wide = fold && narrow.key === key ? narrow.wide : 0;
|
|
64
64
|
const setWide = useCallback((w) => setNarrow({ wide: w, key }), [key]);
|
|
65
|
-
const placed = useMemo(() => autoLayout({ layout: wide ? { ...layout, auto: true, direction: 'column' } : layout, edges, steps, lanes, timeline }), [wide, layout, edges, steps, lanes, timeline]);
|
|
65
|
+
const placed = useMemo(() => autoLayout({ layout: wide > 0 ? { ...layout, auto: true, direction: 'column' } : layout, edges, steps, lanes, timeline }), [wide, layout, edges, steps, lanes, timeline]);
|
|
66
66
|
return (_jsx(FlowBody, { ...props, layout: placed.layout, edges: placed.edges, written: props, wide: wide, setWide: fold ? setWide : undefined }));
|
|
67
67
|
}
|
|
68
|
-
// The ASYNC tag is 9 px
|
|
69
|
-
const
|
|
68
|
+
// The ASYNC tag is 9 px.
|
|
69
|
+
const RAIL_TEXT = 9;
|
|
70
|
+
// offsetWidth is rounded, so a rect ratio reads scale 1 as 0.999.
|
|
71
|
+
const shownScale = (el) => {
|
|
72
|
+
let k = 1;
|
|
73
|
+
for (let n = el; n; n = n.parentElement)
|
|
74
|
+
k *= new DOMMatrix(getComputedStyle(n).transform).a;
|
|
75
|
+
return k;
|
|
76
|
+
};
|
|
70
77
|
function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, autoplay = true, check = false, rail: withRail = false, lanes, timeline, today, written, wide, setWide, }) {
|
|
71
78
|
const tl = timeline && isLanesLayout(layout);
|
|
72
79
|
const alt = useMemo(() => altText({ layout, edges, steps: stepsIn }), [layout, edges, stepsIn]);
|
|
@@ -78,7 +85,7 @@ function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, au
|
|
|
78
85
|
const area = useRef(null);
|
|
79
86
|
const root = useRef(null);
|
|
80
87
|
const outer = useRef(null);
|
|
81
|
-
const [fit, setFit] = useState({ scale: 1, height: 0 });
|
|
88
|
+
const [fit, setFit] = useState({ scale: 1, height: 0, scroll: false });
|
|
82
89
|
const [full, setFull] = useState(false);
|
|
83
90
|
useEffect(() => {
|
|
84
91
|
if (!full)
|
|
@@ -96,6 +103,7 @@ function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, au
|
|
|
96
103
|
const firstLabel = useRef('');
|
|
97
104
|
const paths = useRef({});
|
|
98
105
|
const [routed, setRouted] = useState([]);
|
|
106
|
+
const routedOn = useRef(new WeakMap());
|
|
99
107
|
const markSides = new Set(nodes(layout).flatMap((n) => (n.mark ? [markSide(n.id, n.mark, routed)] : [])));
|
|
100
108
|
const [extraTall, setExtraTall] = useState(new Set());
|
|
101
109
|
const lanePlan = useMemo(() => lanes && !tl && isLanesLayout(layout)
|
|
@@ -145,6 +153,8 @@ function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, au
|
|
|
145
153
|
const rail = useMemo(() => (withRail ? layoutRail({ layout, edges, steps, rail: true }, railOnly ? 560 : mapW) : null), [withRail, railOnly, layout, edges, steps, mapW]);
|
|
146
154
|
const noMap = railOnly && rail != null;
|
|
147
155
|
const railPane = useRef(null);
|
|
156
|
+
const [railBox, setRailBox] = useState(0);
|
|
157
|
+
const zoom = wide ? 11 / RAIL_TEXT : noMap && railBox ? textFloor(railBox) / RAIL_TEXT : 0;
|
|
148
158
|
const follow = useRef(true);
|
|
149
159
|
const user = useRef(false);
|
|
150
160
|
const left = useRef(0);
|
|
@@ -176,13 +186,12 @@ function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, au
|
|
|
176
186
|
if (setWide && goal !== wide)
|
|
177
187
|
return setWide(goal);
|
|
178
188
|
const scale = fitScale(wide, box.clientWidth, el.offsetWidth);
|
|
179
|
-
setFit({ scale, height: el.offsetHeight * scale });
|
|
189
|
+
setFit({ scale, height: el.offsetHeight * scale, scroll: scale > box.clientWidth / el.offsetWidth });
|
|
180
190
|
setMapW(el.offsetWidth);
|
|
181
191
|
if (area.current)
|
|
182
192
|
setAxisW(area.current.offsetWidth);
|
|
183
193
|
const base = el.getBoundingClientRect();
|
|
184
|
-
|
|
185
|
-
const k = base.width / el.offsetWidth;
|
|
194
|
+
const k = shownScale(el);
|
|
186
195
|
const rects = {};
|
|
187
196
|
el.querySelectorAll('[data-fig]').forEach((n) => {
|
|
188
197
|
const r = n.getBoundingClientRect();
|
|
@@ -246,6 +255,7 @@ function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, au
|
|
|
246
255
|
}),
|
|
247
256
|
...(!tl && e.label != null && { labelW: labelPillW(str(e.label)) }),
|
|
248
257
|
})), rects, tips, avoid, pillArea, lanePlan ? { bands: Object.values(laneBands), boxes: nodes(layout).flatMap((n) => (rects[n.id] ? [rects[n.id]] : [])) } : undefined);
|
|
258
|
+
routedOn.current.set(next, rects);
|
|
249
259
|
setRouted(next);
|
|
250
260
|
if (lanePlan) {
|
|
251
261
|
const more = [...tightCopies(layout.children, lanePlan.ends, edges, next)].filter((k) => !lanePlan.tall.has(k) && !extraTall.has(k));
|
|
@@ -263,7 +273,7 @@ function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, au
|
|
|
263
273
|
useEffect(() => {
|
|
264
274
|
const pane = railPane.current;
|
|
265
275
|
const n = rail?.rows.flatMap((r) => (r.kind === 'message' && r.step === active && r.beat === beat ? [r.n] : []))[0];
|
|
266
|
-
const now =
|
|
276
|
+
const now = zoom && follow.current && n != null ? pane?.querySelector(`[data-rail-row="${n}"] path`) : null;
|
|
267
277
|
if (!pane || !now)
|
|
268
278
|
return;
|
|
269
279
|
const [p, r] = [pane.getBoundingClientRect(), now.getBoundingClientRect()];
|
|
@@ -271,25 +281,28 @@ function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, au
|
|
|
271
281
|
// Some browsers fire no scrollend event.
|
|
272
282
|
busy.current = performance.now() + 1000;
|
|
273
283
|
pane.scrollTo({ left: pane.scrollLeft + (r.left + r.right) / 2 - p.left - pane.clientWidth / 2, behavior: still ? 'auto' : 'smooth' });
|
|
274
|
-
}, [
|
|
284
|
+
}, [zoom, active, beat, still, rail]);
|
|
275
285
|
useEffect(() => {
|
|
276
286
|
const pane = railPane.current;
|
|
277
287
|
if (!pane)
|
|
278
288
|
return;
|
|
279
289
|
const end = () => (busy.current = 0);
|
|
280
290
|
pane.addEventListener('scrollend', end);
|
|
281
|
-
|
|
282
|
-
|
|
291
|
+
const ro = new ResizeObserver(() => setRailBox(pane.clientWidth));
|
|
292
|
+
if (noMap)
|
|
293
|
+
ro.observe(pane);
|
|
294
|
+
return () => (pane.removeEventListener('scrollend', end), ro.disconnect());
|
|
295
|
+
}, [rail, noMap]);
|
|
283
296
|
const reported = useRef(new Set());
|
|
284
297
|
useEffect(() => {
|
|
285
298
|
const el = root.current, box = outer.current;
|
|
286
299
|
const fig = figure.current;
|
|
287
|
-
if (!check || !fig || (
|
|
300
|
+
if (!check || !fig || (noMap ? !railBox : !el || !box || !el.offsetWidth))
|
|
288
301
|
return;
|
|
289
302
|
if (setWide && el && box && nextWide(wide, box.clientWidth, el.offsetWidth) !== wide)
|
|
290
303
|
return;
|
|
291
304
|
const base = (el ?? fig).getBoundingClientRect();
|
|
292
|
-
const k = el ?
|
|
305
|
+
const k = el ? shownScale(el) : 1;
|
|
293
306
|
const rel = (n) => {
|
|
294
307
|
const r = n.getBoundingClientRect();
|
|
295
308
|
return { x: (r.left - base.left) / k, y: (r.top - base.top) / k, w: r.width / k, h: r.height / k };
|
|
@@ -306,7 +319,7 @@ function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, au
|
|
|
306
319
|
.filter((t) => !t.children.length && t.textContent?.trim() && t.clientWidth > 0 && !t.closest('[aria-hidden]'))
|
|
307
320
|
.map((t) => {
|
|
308
321
|
const fontSize = parseFloat(getComputedStyle(t).fontSize);
|
|
309
|
-
if (!n.dataset.diamond)
|
|
322
|
+
if (!n.dataset.diamond || t.closest('[data-fig-outside]'))
|
|
310
323
|
return { text: t.textContent, fontSize, room: t.clientWidth, need: t.scrollWidth };
|
|
311
324
|
const [r, range] = [rel(t), document.createRange()];
|
|
312
325
|
range.selectNodeContents(t);
|
|
@@ -320,6 +333,12 @@ function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, au
|
|
|
320
333
|
}),
|
|
321
334
|
};
|
|
322
335
|
});
|
|
336
|
+
const moved = (b) => {
|
|
337
|
+
const r = routedOn.current.get(routed)?.[b.id];
|
|
338
|
+
return !r || Math.abs(r.x - b.rect.x) > 1 || Math.abs(r.y - b.rect.y) > 1;
|
|
339
|
+
};
|
|
340
|
+
if (el && boxes.some(moved))
|
|
341
|
+
return;
|
|
323
342
|
const fonts = [...fig.querySelectorAll('*')]
|
|
324
343
|
.filter((t) => !t.children.length &&
|
|
325
344
|
t.textContent?.trim() &&
|
|
@@ -328,8 +347,9 @@ function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, au
|
|
|
328
347
|
!t.closest('[data-fig-tag],[aria-hidden]'))
|
|
329
348
|
.map((t) => {
|
|
330
349
|
const svg = railPane.current?.firstElementChild;
|
|
331
|
-
const
|
|
332
|
-
|
|
350
|
+
const place = el?.contains(t) ? 'map' : svg?.contains(t) && rail ? 'rail' : 'plain';
|
|
351
|
+
const railScale = svg && rail ? svg.getBoundingClientRect().width / rail.width : 1;
|
|
352
|
+
return shownFont(parseFloat(getComputedStyle(t).fontSize), place, { map: k, rail: railScale });
|
|
333
353
|
});
|
|
334
354
|
const labels = Object.fromEntries([...fig.querySelectorAll('[data-fig-label]')].map((n) => [n.dataset.figLabel, rel(n)]));
|
|
335
355
|
const lanesIn = el ? [...el.querySelectorAll('[data-fig-band]')] : [];
|
|
@@ -378,7 +398,11 @@ function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, au
|
|
|
378
398
|
],
|
|
379
399
|
minFont: Math.min(...fonts),
|
|
380
400
|
};
|
|
381
|
-
for (const f of [
|
|
401
|
+
for (const f of [
|
|
402
|
+
...checkSpec(written),
|
|
403
|
+
...checkScene(scene, { width: (box ?? fig).clientWidth, minText: textFloor((box ?? fig).clientWidth), auto: written.layout.auto }),
|
|
404
|
+
...checkTheme(theme),
|
|
405
|
+
]) {
|
|
382
406
|
const key = f.rule + ':' + f.ids.join() + ':' + f.message;
|
|
383
407
|
if (reported.current.has(key))
|
|
384
408
|
continue;
|
|
@@ -386,7 +410,7 @@ function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, au
|
|
|
386
410
|
// oxlint-disable-next-line no-console
|
|
387
411
|
console.warn(`flowfig check: ${f.severity} ${f.rule}: ${f.message}`);
|
|
388
412
|
}
|
|
389
|
-
}, [check, routed, written, layout, edges, steps, ids, theme, rail, noMap, lanePlan, wide, setWide]);
|
|
413
|
+
}, [check, routed, written, layout, edges, steps, ids, theme, rail, noMap, railBox, lanePlan, wide, setWide]);
|
|
390
414
|
useEffect(() => {
|
|
391
415
|
const gs = dots.current, cs = chips.current, edgeOf = Object.fromEntries(edges.map((e, i) => [ids[i], e]));
|
|
392
416
|
if (clock.current.beats !== beats) {
|
|
@@ -712,7 +736,7 @@ function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, au
|
|
|
712
736
|
const card = carded.has(item.id);
|
|
713
737
|
const dl = diamond && !card ? diamondLines(item, nodeWidth(item, false) + 70) : null;
|
|
714
738
|
const w = diamond || card || item.width != null || str(item.label) ? nodeWidth(item, card, cap) + (diamond ? 70 : 0) : undefined;
|
|
715
|
-
const lines = w != null &&
|
|
739
|
+
const lines = w != null && str(item.label) ? labelLines(item, w) : null;
|
|
716
740
|
const bt = item.tone && TONES[item.tone];
|
|
717
741
|
return (_jsxs("div", { "data-fig": item.id, title: item.source, "data-diamond": diamond || undefined, onMouseEnter: () => setHover(item.id), onMouseLeave: () => setHover(null), style: {
|
|
718
742
|
position: 'relative',
|
|
@@ -835,7 +859,7 @@ function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, au
|
|
|
835
859
|
backgroundImage: `radial-gradient(color-mix(in srgb, ${v('fg')} 16%, transparent) 1px, transparent 1.2px)`,
|
|
836
860
|
backgroundSize: '14px 14px',
|
|
837
861
|
}, children: _jsx("div", { ref: outer, role: "tabpanel", id: `${tabsId}-panel`, "aria-labelledby": `${tabsId}-tab-${active ?? 0}`, style: {
|
|
838
|
-
overflow: wide || fit.
|
|
862
|
+
overflow: wide || fit.scroll ? 'auto' : 'hidden',
|
|
839
863
|
height: fit.scale < 1 ? fit.height : undefined,
|
|
840
864
|
}, children: _jsxs("div", { ref: root, style: {
|
|
841
865
|
position: 'relative',
|
|
@@ -930,10 +954,10 @@ function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, au
|
|
|
930
954
|
if (user.current && x !== left.current && performance.now() > busy.current)
|
|
931
955
|
follow.current = false;
|
|
932
956
|
left.current = x;
|
|
933
|
-
}, style: { overflowX:
|
|
957
|
+
}, style: { overflowX: zoom ? 'auto' : undefined }, children: _jsxs("svg", { viewBox: `0 0 ${rail.width} ${rail.height}`, style: {
|
|
934
958
|
display: 'block',
|
|
935
|
-
width:
|
|
936
|
-
maxWidth:
|
|
959
|
+
width: zoom ? rail.width * zoom : '100%',
|
|
960
|
+
maxWidth: zoom ? undefined : rail.width,
|
|
937
961
|
height: 'auto',
|
|
938
962
|
margin: `${noMap ? 0 : RAIL.gap}px auto 0`,
|
|
939
963
|
overflow: 'visible',
|
package/dist/model.d.ts
CHANGED
|
@@ -238,7 +238,7 @@ export declare const validAt: (at: unknown) => at is number;
|
|
|
238
238
|
/** The time column of each box: first use in the steps, then layout order. `at` wins. */
|
|
239
239
|
export declare function laneColumns(fig: FlowProps): Map<string, number>;
|
|
240
240
|
export declare const LABEL_LINE = 18;
|
|
241
|
-
export declare const FRAME_SIDE = 18, NODE_MIN_W = 100, NODE_MAX_W = 190;
|
|
241
|
+
export declare const FRAME_SIDE = 18, NODE_MIN_W = 100, NODE_MAX_W = 190, DIAMOND_MAX_W = 170;
|
|
242
242
|
export declare const LANE_BLOCK_GAP = 40;
|
|
243
243
|
export declare const STUB = 12, STUB_CLEAR = 8, STUB_ROOM = 30;
|
|
244
244
|
/** The width of a box as the SVG draws it, before a diamond adds its 70 px. */
|
|
@@ -249,13 +249,13 @@ export declare function itemWidth(item: FigNode | FigGroup, carded: {
|
|
|
249
249
|
}, edges: FigEdge[], cap?: number): number;
|
|
250
250
|
export declare function fitCap(fig: FlowProps, width: number, padding?: number): number;
|
|
251
251
|
export declare const diamondRoom: (w: number, h: number, far: number) => number;
|
|
252
|
-
/** The lines of a diamond with no card. The diamond grows 15 px for each sub line that wraps. */
|
|
253
252
|
export declare function diamondLines(item: FigNode, w: number): {
|
|
254
253
|
h: number;
|
|
255
254
|
label: {
|
|
255
|
+
text: string;
|
|
256
256
|
dy: number;
|
|
257
257
|
far: number;
|
|
258
|
-
};
|
|
258
|
+
}[];
|
|
259
259
|
subs: {
|
|
260
260
|
text: string;
|
|
261
261
|
dy: number;
|
|
@@ -361,5 +361,10 @@ export declare const altText: (fig: FlowProps) => {
|
|
|
361
361
|
desc: string;
|
|
362
362
|
};
|
|
363
363
|
export declare const specKey: (...parts: unknown[]) => string;
|
|
364
|
+
export declare const textFloor: (box: number) => 9 | 11;
|
|
364
365
|
export declare const fitScale: (wide: number, box: number, map: number) => number;
|
|
366
|
+
export declare const shownFont: (size: number, place: 'map' | 'rail' | 'plain', scale: {
|
|
367
|
+
map: number;
|
|
368
|
+
rail: number;
|
|
369
|
+
}) => number;
|
|
365
370
|
export declare const nextWide: (wide: number, box: number, map: number) => number;
|
package/dist/model.js
CHANGED
|
@@ -95,7 +95,7 @@ export function laneColumns(fig) {
|
|
|
95
95
|
return cols;
|
|
96
96
|
}
|
|
97
97
|
export const LABEL_LINE = 18;
|
|
98
|
-
export const FRAME_SIDE = 18, NODE_MIN_W = 100, NODE_MAX_W = 190;
|
|
98
|
+
export const FRAME_SIDE = 18, NODE_MIN_W = 100, NODE_MAX_W = 190, DIAMOND_MAX_W = 170;
|
|
99
99
|
export const LANE_BLOCK_GAP = 40;
|
|
100
100
|
export const STUB = 12, STUB_CLEAR = 8, STUB_ROOM = 30;
|
|
101
101
|
/** The width of a box as the SVG draws it, before a diamond adds its 70 px. */
|
|
@@ -106,14 +106,16 @@ export function nodeWidth(item, carded, cap = NODE_MAX_W) {
|
|
|
106
106
|
return CARD_WIDTH;
|
|
107
107
|
const label = textWidth(str(item.label), 14) + 32;
|
|
108
108
|
const sub = textWidth(str(item.sub), 12) + 32;
|
|
109
|
-
return Math.min(item.shape === 'decision' ?
|
|
109
|
+
return Math.min(item.shape === 'decision' ? DIAMOND_MAX_W : cap, Math.max(NODE_MIN_W, label, sub));
|
|
110
110
|
}
|
|
111
111
|
export function labelLines(item, w) {
|
|
112
112
|
const label = str(item.label);
|
|
113
|
-
if (item.shape === 'decision'
|
|
113
|
+
if (item.shape === 'decision')
|
|
114
|
+
return wrap(label, w - 102 + 0.5, 14);
|
|
115
|
+
if (textWidth(label, 14) <= w - 32 + 0.5)
|
|
114
116
|
return [label];
|
|
115
117
|
const lines = wrap(label, w - 32, 14);
|
|
116
|
-
return lines.
|
|
118
|
+
return lines.every((l) => textWidth(l, 14) <= w - 32) ? lines : [label];
|
|
117
119
|
}
|
|
118
120
|
export function itemWidth(item, carded, edges, cap = NODE_MAX_W) {
|
|
119
121
|
if (!isGroup(item))
|
|
@@ -137,21 +139,28 @@ export function fitCap(fig, width, padding = 24) {
|
|
|
137
139
|
return cap;
|
|
138
140
|
}
|
|
139
141
|
export const diamondRoom = (w, h, far) => w * Math.max(0, 1 - far / (h / 2)) - 8;
|
|
140
|
-
/** The lines of a diamond with no card. The diamond grows 15 px for each sub line that wraps. */
|
|
141
142
|
export function diamondLines(item, w) {
|
|
142
143
|
const sub = item.sub ? str(item.sub) : '';
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
const label = labelLines(item, w);
|
|
145
|
+
const k = label.length - 1;
|
|
146
|
+
const at = (dy, size) => Math.max(Math.abs(dy - 0.75 * size), dy + 0.25 * size);
|
|
147
|
+
for (let n = sub ? 1 : 0, h = 0;;) {
|
|
148
|
+
h = Math.max(h, 62 + 15 * n + LABEL_LINE * k);
|
|
149
|
+
const dy0 = (n ? 5.5 : 5) - 7.5 * n - (LABEL_LINE / 2) * k;
|
|
150
|
+
const last = n ? at(dy0 + LABEL_LINE * k + 15 * n, 12) : 0;
|
|
148
151
|
const lines = n ? wrap(sub, diamondRoom(w, h, last), 12) : [];
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
const labels = label.map((text, i) => ({ text, dy: dy0 + LABEL_LINE * i, far: at(dy0 + LABEL_LINE * i, 14) }));
|
|
153
|
+
const subs = lines.map((text, j) => ({
|
|
154
|
+
text,
|
|
155
|
+
dy: dy0 + LABEL_LINE * k + 15 * (j + 1),
|
|
156
|
+
far: at(dy0 + LABEL_LINE * k + 15 * (j + 1), 12),
|
|
157
|
+
}));
|
|
158
|
+
if (lines.length > n && n < 6)
|
|
159
|
+
n++;
|
|
160
|
+
else if (h < w && labels.some((l) => textWidth(l.text, 14) > diamondRoom(w, h, l.far)))
|
|
161
|
+
h += 4;
|
|
162
|
+
else
|
|
163
|
+
return { h, label: labels, subs };
|
|
155
164
|
}
|
|
156
165
|
}
|
|
157
166
|
/** The lane copies whose stub end has no clear place. The renderer grows each by STUB_ROOM. */
|
|
@@ -523,7 +532,17 @@ const keyOf = (v, path) => {
|
|
|
523
532
|
return Object.fromEntries(Object.entries(src).flatMap(([k, x]) => (k === 'ref' || k.startsWith('_') ? [] : [[k, keyOf(x, at)]])));
|
|
524
533
|
};
|
|
525
534
|
export const specKey = (...parts) => JSON.stringify(keyOf(parts, []));
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
const
|
|
529
|
-
|
|
535
|
+
// Edge labels are 11 px, the smallest reader text at scale 1.
|
|
536
|
+
const READER_PX = 11;
|
|
537
|
+
const DESKTOP_PX = 9;
|
|
538
|
+
const DESKTOP_BOX = 600;
|
|
539
|
+
export const textFloor = (box) => (box < DESKTOP_BOX ? READER_PX : DESKTOP_PX);
|
|
540
|
+
export const fitScale = (wide, box, map) => Math.max(wide ? 1 : textFloor(box) / READER_PX, Math.min(1, box / map));
|
|
541
|
+
export const shownFont = (size, place, scale) => size * (place === 'map' ? scale.map : place === 'rail' ? scale.rail : 1);
|
|
542
|
+
const folds = (box, row) => box * READER_PX < row * textFloor(box);
|
|
543
|
+
export const nextWide = (wide, box, map) => {
|
|
544
|
+
const row = Math.abs(wide) || map;
|
|
545
|
+
if (!folds(box, row))
|
|
546
|
+
return 0;
|
|
547
|
+
return wide > 0 && map >= wide ? -wide : wide || map;
|
|
548
|
+
};
|
package/dist/svg.js
CHANGED
|
@@ -12,8 +12,12 @@ const LINE = CARD_LINE, CARD_SIDE = 8, ROW_GAP = 4;
|
|
|
12
12
|
const SUB_LINE = 15;
|
|
13
13
|
const MIN_W = 320;
|
|
14
14
|
const FRAME_TOP = 37, FRAME_BOTTOM = 18;
|
|
15
|
-
|
|
16
|
-
const
|
|
15
|
+
const color = (t, k) => {
|
|
16
|
+
const v = String(t[k]);
|
|
17
|
+
const depth = [...v].reduce((d, c) => (d < 0 ? d : d + (c === '(' ? 1 : c === ')' ? -1 : 0)), 0);
|
|
18
|
+
return /^[#a-zA-Z0-9 ,.%()/+-]+$/.test(v) && !/url\(/i.test(v) && depth === 0 ? v : LIGHT[k];
|
|
19
|
+
};
|
|
20
|
+
const vars = (t, cardOn) => `--accent:${color(t, 'accent')}; --fg:${color(t, 'fg')}; --muted:${color(t, 'muted')}; --bg:${color(t, 'bg')}; --surface:${color(t, 'surface')}; --border:${color(t, 'border')}; --card-on:${cardOn}; --tint:color-mix(in srgb, var(--accent) 10%, var(--surface));`;
|
|
17
21
|
const content = (c) => (isRows(c) ? c : str(c));
|
|
18
22
|
const esc = (s) => s.replace(/[<>&"]/g, (c) => ({ '<': '<', '>': '>', '&': '&', '"': '"' })[c]);
|
|
19
23
|
const n2 = (v) => Math.round(v * 10) / 10;
|
|
@@ -293,10 +297,10 @@ export function render(spec, opts = {}) {
|
|
|
293
297
|
const dl = dia ? diamondLines(p.item, p.w) : null;
|
|
294
298
|
const room = (far) => diamondRoom(p.w, p.h, far);
|
|
295
299
|
const flat = (p.item.shape === 'decision' ? p.w - 70 : p.w) - 16;
|
|
296
|
-
const texts = labelLines(p.item, p.w).map((text) => ({
|
|
300
|
+
const texts = labelLines(p.item, p.w).map((text, i) => ({
|
|
297
301
|
text,
|
|
298
302
|
fontSize: 14,
|
|
299
|
-
room: dl ? room(dl.label.far) : flat,
|
|
303
|
+
room: dl ? room(dl.label[i].far) : flat,
|
|
300
304
|
}));
|
|
301
305
|
if (dl)
|
|
302
306
|
for (const l of dl.subs)
|
|
@@ -507,7 +511,7 @@ export function render(spec, opts = {}) {
|
|
|
507
511
|
const contents = cards.get(item.id);
|
|
508
512
|
const cardTop = p.y + p.h - 10 - (contents ? cardH.get(item.id) : 0);
|
|
509
513
|
const dl = item.shape === 'decision' && !contents ? diamondLines(item, p.w) : null;
|
|
510
|
-
const labelY = item.shape === 'store' ? p.y + 24 + 13 : contents ? p.y + 10 + 13 : p.y + p.h / 2 + (dl ? dl.label.dy : item.sub ? -2 : 5);
|
|
514
|
+
const labelY = item.shape === 'store' ? p.y + 24 + 13 : contents ? p.y + 10 + 13 : p.y + p.h / 2 + (dl ? dl.label[0].dy : item.sub ? -2 : 5);
|
|
511
515
|
const shape = item.shape === 'decision'
|
|
512
516
|
? `<polygon points="${n2(cx)},${n2(p.y)} ${n2(p.x + p.w)},${n2(p.y + p.h / 2)} ${n2(cx)},${n2(p.y + p.h)} ${n2(p.x)},${n2(p.y + p.h / 2)}" fill="${fill0}" stroke="${stroke0}"${stroke}/>`
|
|
513
517
|
: item.shape === 'store'
|
|
@@ -516,7 +520,7 @@ export function render(spec, opts = {}) {
|
|
|
516
520
|
: `<rect x="${n2(p.x)}" y="${n2(p.y)}" width="${n2(p.w)}" height="${n2(p.h)}" rx="10" fill="${fill0}" stroke="${stroke0}"${stroke}/>`;
|
|
517
521
|
const lines = labelLines(item, p.w);
|
|
518
522
|
const extra = (lines.length - 1) * LABEL_LINE;
|
|
519
|
-
const firstY = item.shape === 'store' || contents ? labelY : labelY - extra / 2;
|
|
523
|
+
const firstY = item.shape === 'store' || contents || dl ? labelY : labelY - extra / 2;
|
|
520
524
|
const label = lines.map((l, i) => `<text x="${n2(cx)}" y="${n2(firstY + i * LABEL_LINE)}" class="label">${esc(l)}</text>`).join('');
|
|
521
525
|
const sub = dl
|
|
522
526
|
? dl.subs.map((l) => `<text x="${n2(cx)}" y="${n2(p.y + p.h / 2 + l.dy)}" class="sub">${esc(l.text)}</text>`).join('')
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.11.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// The release list bumps this with package.json.
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '0.11.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowfig",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Your coding agent draws animated diagrams from your code, and flowfig checks each box and arrow against that code. CI fails when a diagram drifts from the code. The output is one animated SVG with no script, for a README, PR or issue. Works with Claude Code, Cursor, GitHub Copilot, Codex, Gemini CLI, Windsurf and Kiro.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|