flowfig 0.8.4 → 0.10.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 +375 -41
- package/dist/atlas.d.ts +6 -0
- package/dist/atlas.js +122 -0
- package/dist/auto.d.ts +3 -0
- package/dist/auto.js +302 -0
- package/dist/browser.js +3 -1
- package/dist/check.js +54 -17
- package/dist/cli.js +69 -6
- package/dist/code.d.ts +2 -0
- package/dist/code.js +9 -4
- package/dist/coverage.d.ts +27 -0
- package/dist/coverage.js +116 -0
- package/dist/diff.d.ts +10 -0
- package/dist/diff.js +137 -7
- package/dist/edges.d.ts +3 -0
- package/dist/edges.js +3 -3
- package/dist/geometry.d.ts +4 -0
- package/dist/geometry.js +45 -17
- package/dist/imports.d.ts +1 -0
- package/dist/imports.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +161 -81
- package/dist/load.js +6 -5
- package/dist/mermaid.d.ts +19 -0
- package/dist/mermaid.js +617 -0
- package/dist/model.d.ts +15 -3
- package/dist/model.js +56 -1
- package/dist/open.d.ts +1 -0
- package/dist/open.js +11 -8
- package/dist/page.d.ts +2 -1
- package/dist/page.js +5 -4
- package/dist/svg.d.ts +7 -2
- package/dist/svg.js +73 -22
- package/dist/trace.d.ts +30 -0
- package/dist/trace.js +278 -0
- package/dist/verify.d.ts +5 -2
- package/dist/verify.js +39 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/geometry.js
CHANGED
|
@@ -52,9 +52,18 @@ const spanOf = (a, b, around) => {
|
|
|
52
52
|
return [Math.min(p, q), Math.max(p, q)];
|
|
53
53
|
};
|
|
54
54
|
const SIDES = { above: ['t', 't'], below: ['b', 'b'], left: ['l', 'l'], right: ['r', 'r'] };
|
|
55
|
+
export const samples = (curve) => {
|
|
56
|
+
const hull = curve.slice(1).reduce((s, q, k) => s + Math.hypot(q.x - curve[k].x, q.y - curve[k].y), 0);
|
|
57
|
+
const n = Math.max(32, Math.ceil(hull / 4));
|
|
58
|
+
return Array.from({ length: n + 1 }, (_, i) => bezier(curve, i / n));
|
|
59
|
+
};
|
|
55
60
|
export const hit = (curve, boxes, m) => {
|
|
56
|
-
const
|
|
57
|
-
|
|
61
|
+
const xs = curve.map((q) => q.x), ys = curve.map((q) => q.y);
|
|
62
|
+
const near = boxes.filter((r) => r.x + r.w + m > Math.min(...xs) && r.x - m < Math.max(...xs) && r.y + r.h + m > Math.min(...ys) && r.y - m < Math.max(...ys));
|
|
63
|
+
if (!near.length)
|
|
64
|
+
return [];
|
|
65
|
+
const pts = samples(curve);
|
|
66
|
+
return near.filter((r) => pts.some((q) => q.x > r.x - m && q.x < r.x + r.w + m && q.y > r.y - m && q.y < r.y + r.h + m));
|
|
58
67
|
};
|
|
59
68
|
/** Routes edges between measured boxes as curved SVG paths. `elbow` draws right-angle paths; `stub` splits a cross-lane edge in two. */
|
|
60
69
|
export function route(edges, rects, tips = new Set(), avoid = [], area, lanes, pad = 24) {
|
|
@@ -68,7 +77,7 @@ export function route(edges, rects, tips = new Set(), avoid = [], area, lanes, p
|
|
|
68
77
|
const others = avoid.filter((r) => r.box && !same(r, a) && !same(r, b));
|
|
69
78
|
const plain = stacked ? (a.y < b.y ? ['b', 't'] : ['t', 'b']) : a.x < b.x ? ['r', 'l'] : ['l', 'r'];
|
|
70
79
|
const past = hit(bend(sideMid(a, plain[0]), sideMid(b, plain[1]), plain[0]), others, -2);
|
|
71
|
-
const misses = (way, at) => [0.2, 0.5, 0.8].filter((f) => hit(arcLine(sideAt(a, SIDES[way][0], f), sideAt(b, SIDES[way][1], f), way, at), others, -
|
|
80
|
+
const misses = (way, at) => [0.2, 0.5, 0.8].filter((f) => hit(arcLine(sideAt(a, SIDES[way][0], f), sideAt(b, SIDES[way][1], f), way, at), others, -6).length > 0)
|
|
72
81
|
.length;
|
|
73
82
|
const blocked = (way, at) => misses(way, at) === 3;
|
|
74
83
|
let around = e.around;
|
|
@@ -81,7 +90,10 @@ export function route(edges, rects, tips = new Set(), avoid = [], area, lanes, p
|
|
|
81
90
|
if (blocked(around, arc))
|
|
82
91
|
around = undefined;
|
|
83
92
|
}
|
|
84
|
-
|
|
93
|
+
const down = e.drop && !hit(bend(sideMid(a, 'b'), sideMid(b, 't'), 'b'), others, -2).length;
|
|
94
|
+
if (e.drop && !around)
|
|
95
|
+
elbow = !down;
|
|
96
|
+
else if (!around && !e.sides && !e.stub && !e.elbow && past.length) {
|
|
85
97
|
const ways = stacked ? ['left', 'right'] : ['above', 'below'];
|
|
86
98
|
const tries = [];
|
|
87
99
|
for (const share of [false, true])
|
|
@@ -107,7 +119,7 @@ export function route(edges, rects, tips = new Set(), avoid = [], area, lanes, p
|
|
|
107
119
|
const [lo, hi] = spanOf(a, b, around);
|
|
108
120
|
arcs.push({ way: around, lo, hi, at: arc });
|
|
109
121
|
}
|
|
110
|
-
const [sa, sb] = e.sides ?? (e.stub ? ['r', 'l'] : around ? SIDES[around] : plain);
|
|
122
|
+
const [sa, sb] = e.sides ?? (e.stub ? ['r', 'l'] : down ? ['b', 't'] : around ? SIDES[around] : plain);
|
|
111
123
|
picks.push({ ...e, plain, a, b, sa, sb, around, arc, elbow });
|
|
112
124
|
}
|
|
113
125
|
let anchor = new Map();
|
|
@@ -152,10 +164,10 @@ export function route(edges, rects, tips = new Set(), avoid = [], area, lanes, p
|
|
|
152
164
|
if (!p.around || !s || !e)
|
|
153
165
|
continue;
|
|
154
166
|
const others = avoid.filter((r) => r.box && !same(r, p.a) && !same(r, p.b));
|
|
155
|
-
if (!hit(arcLine(s, e, p.around, p.arc), others, -
|
|
167
|
+
if (!hit(arcLine(s, e, p.around, p.arc), others, -6).length)
|
|
156
168
|
continue;
|
|
157
169
|
const [pa, pb] = p.plain;
|
|
158
|
-
const straight = hit(bend(sideMid(p.a, pa), sideMid(p.b, pb), pa), others, -
|
|
170
|
+
const straight = hit(bend(sideMid(p.a, pa), sideMid(p.b, pb), pa), others, -6).length > 0;
|
|
159
171
|
[p.sa, p.sb, p.around, p.arc, p.elbow, moved] = [pa, pb, undefined, undefined, p.elbow || straight, true];
|
|
160
172
|
}
|
|
161
173
|
if (moved)
|
|
@@ -183,7 +195,15 @@ export function route(edges, rects, tips = new Set(), avoid = [], area, lanes, p
|
|
|
183
195
|
const outside = (way, { x, y }) => (way === 'above' ? y0 - y : way === 'below' ? y - y1 : way === 'left' ? x0 - x : x - x1);
|
|
184
196
|
const roomOf = (way, at, curve, labelW) => Math.max(ARC - 6, lanes || !labelW ? outside(way, { x: at, y: at }) - 6 : outside(way, bezier(curve, 0.5)) + (upright(way) ? labelW / 2 : 9));
|
|
185
197
|
const one = (p) => {
|
|
186
|
-
const
|
|
198
|
+
const turn = !p.stub && p.elbow && p.sa === 'l';
|
|
199
|
+
const [s, e] = [anchor.get(p.id + (turn ? ':e' : ':s')), anchor.get(p.id + (turn ? ':s' : ':e'))];
|
|
200
|
+
const flip = (r) => {
|
|
201
|
+
if (!turn)
|
|
202
|
+
return r;
|
|
203
|
+
const pts = [...r.elbow].reverse();
|
|
204
|
+
const d = pts.slice(1).reduce((d, q, k) => d + (q.y === pts[k].y ? ` H ${q.x}` : ` V ${q.y}`), `M ${pts[0].x} ${pts[0].y}`);
|
|
205
|
+
return { ...r, d, curve: [...r.curve].reverse(), elbow: pts };
|
|
206
|
+
};
|
|
187
207
|
if (p.stub) {
|
|
188
208
|
const [ow, iw, sw] = p.stub;
|
|
189
209
|
const [sb, tb] = p.bands ?? [];
|
|
@@ -336,7 +356,7 @@ export function route(edges, rects, tips = new Set(), avoid = [], area, lanes, p
|
|
|
336
356
|
}
|
|
337
357
|
if (p.elbow) {
|
|
338
358
|
if (e.x - s.x < 16 && s.y === e.y) {
|
|
339
|
-
return { id: p.id, d: `M ${s.x} ${s.y} H ${e.x}`, mid: { x: (s.x + e.x) / 2, y: s.y }, curve: [s, s, e, e], elbow: [s, e] };
|
|
359
|
+
return flip({ id: p.id, d: `M ${s.x} ${s.y} H ${e.x}`, mid: { x: (s.x + e.x) / 2, y: s.y }, curve: [s, s, e, e], elbow: [s, e] });
|
|
340
360
|
}
|
|
341
361
|
const own = (r) => [p.a, p.b].some((q) => same(q, r));
|
|
342
362
|
const all = avoid.filter((r) => !own(r));
|
|
@@ -347,14 +367,14 @@ export function route(edges, rects, tips = new Set(), avoid = [], area, lanes, p
|
|
|
347
367
|
const mx = s.x + 8, ex = e.x - 8, mid = (s.y + e.y) / 2;
|
|
348
368
|
const ys = Array.from({ length: Math.floor((y1 - y0 + 24) / 4) + 1 }, (_, i) => y0 - 12 + i * 4).sort((u, w) => Math.abs(u - mid) - Math.abs(w - mid));
|
|
349
369
|
const my = ys.find((y) => vClear(mx, s.y, y) && hClear(y, ex, mx, all) && vClear(ex, y, e.y)) ?? mid;
|
|
350
|
-
return {
|
|
370
|
+
return flip({
|
|
351
371
|
id: p.id,
|
|
352
372
|
d: `M ${s.x} ${s.y} H ${mx} V ${my} H ${ex} V ${e.y} H ${e.x}`,
|
|
353
373
|
mid: { x: (mx + ex) / 2, y: my },
|
|
354
374
|
curve: [s, { x: mx, y: my }, { x: ex, y: my }, e],
|
|
355
375
|
elbow: [s, { x: mx, y: s.y }, { x: mx, y: my }, { x: ex, y: my }, { x: ex, y: e.y }, e],
|
|
356
376
|
bleed: { above: room(y0 - my), below: room(my - y1) },
|
|
357
|
-
};
|
|
377
|
+
});
|
|
358
378
|
}
|
|
359
379
|
const mid = (s.x + e.x) / 2;
|
|
360
380
|
const gap = Array.from({ length: Math.floor((e.x - s.x - 16) / 4) + 1 }, (_, i) => s.x + 8 + i * 4).sort((u, w) => Math.abs(u - mid) - Math.abs(w - mid));
|
|
@@ -383,7 +403,7 @@ export function route(edges, rects, tips = new Set(), avoid = [], area, lanes, p
|
|
|
383
403
|
}
|
|
384
404
|
if (best) {
|
|
385
405
|
const { x, y, ex } = best;
|
|
386
|
-
return {
|
|
406
|
+
return flip({
|
|
387
407
|
id: p.id,
|
|
388
408
|
d: `M ${s.x} ${s.y} H ${x} V ${y} H ${ex} V ${e.y} H ${e.x}`,
|
|
389
409
|
mid: { x: (x + ex) / 2, y },
|
|
@@ -395,18 +415,18 @@ export function route(edges, rects, tips = new Set(), avoid = [], area, lanes, p
|
|
|
395
415
|
above: room(y0 - y),
|
|
396
416
|
below: room(y - y1),
|
|
397
417
|
},
|
|
398
|
-
};
|
|
418
|
+
});
|
|
399
419
|
}
|
|
400
420
|
}
|
|
401
421
|
const x = mx ?? mid;
|
|
402
422
|
const c1 = { x, y: s.y }, c2 = { x, y: e.y };
|
|
403
|
-
return {
|
|
423
|
+
return flip({
|
|
404
424
|
id: p.id,
|
|
405
425
|
d: `M ${s.x} ${s.y} H ${x} V ${e.y} H ${e.x}`,
|
|
406
426
|
mid: { x, y: (s.y + e.y) / 2 },
|
|
407
427
|
curve: [s, c1, c2, e],
|
|
408
428
|
elbow: [s, c1, c2, e],
|
|
409
|
-
};
|
|
429
|
+
});
|
|
410
430
|
}
|
|
411
431
|
if (p.around) {
|
|
412
432
|
const curve = arcLine(s, e, p.around, p.arc);
|
|
@@ -456,15 +476,23 @@ export function route(edges, rects, tips = new Set(), avoid = [], area, lanes, p
|
|
|
456
476
|
pills.push(...use.out);
|
|
457
477
|
let k = 0;
|
|
458
478
|
for (const [p, r] of done) {
|
|
459
|
-
if (p.labelW)
|
|
479
|
+
if (p.labelW) {
|
|
460
480
|
r.mid = use.mids[k++];
|
|
481
|
+
const [l, rt] = [x0 - r.mid.x + p.labelW / 2, r.mid.x + p.labelW / 2 - x1];
|
|
482
|
+
r.bleed = { ...r.bleed, left: Math.max(r.bleed?.left ?? 0, l, 0), right: Math.max(r.bleed?.right ?? 0, rt, 0) };
|
|
483
|
+
}
|
|
461
484
|
track(Array.from({ length: 33 }, (_, i) => bezier(r.curve, i / 32)));
|
|
462
485
|
}
|
|
463
486
|
for (const p of picks)
|
|
464
487
|
if (p.stub)
|
|
465
488
|
done.set(p, one(p));
|
|
466
|
-
return picks.map((p) => done.get(p));
|
|
489
|
+
return picks.map((p) => ({ ...done.get(p), ends: [`${p.from}:${p.sa}`, `${p.to}:${p.sb}`] }));
|
|
467
490
|
}
|
|
491
|
+
export const markSide = (id, mark, routed) => {
|
|
492
|
+
const used = new Set(routed.flatMap((r) => r.ends ?? []));
|
|
493
|
+
const order = mark === 'start' ? ['l', 't', 'b', 'r'] : ['r', 'b', 't', 'l'];
|
|
494
|
+
return order.find((s) => !used.has(`${id}:${s}`)) ?? order[0];
|
|
495
|
+
};
|
|
468
496
|
export const arcRoom = (routed, way) => Math.max(0, ...routed.map((r) => (r.around === way ? r.room : (r.bleed?.[way] ?? 0))));
|
|
469
497
|
// The player pads its root by 4 px on a side with no detour.
|
|
470
498
|
const PLAYER_PAD = 4;
|
package/dist/imports.d.ts
CHANGED
|
@@ -8,4 +8,5 @@ export declare const OUTSIDE = "#outside";
|
|
|
8
8
|
export declare function resolveTs(root: string, from: string, spec: string, read: Read): string | null;
|
|
9
9
|
export declare function resolvePy(root: string, from: string, mod: string, read: Read, top?: string): string | null;
|
|
10
10
|
export declare function importsOf(root: string, file: CodeFile, read: Read): Import[];
|
|
11
|
+
export declare function reexports(root: string, file: CodeFile, name: string, read: Read): [string | null, string][];
|
|
11
12
|
export declare function leadsTo(root: string, fromPath: string, toPath: string, name: string, read?: Read, cache?: Map<string, CodeFile | null>, depth?: number): string | null;
|
package/dist/imports.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ export { LIGHT, DARK } from './model.ts';
|
|
|
5
5
|
* The interactive player for one figure. Give it a `FlowProps` spec.
|
|
6
6
|
* Use `toSvg` from `flowfig/svg` for a static animated SVG.
|
|
7
7
|
*/
|
|
8
|
-
export declare function Flow(
|
|
8
|
+
export declare function Flow(props: FlowProps): import("react").JSX.Element;
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import { arcRoom, avoidOf, route } from './geometry.js';
|
|
3
|
+
import { Fragment, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { arcRoom, avoidOf, markSide, route } from './geometry.js';
|
|
5
5
|
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
|
-
import {
|
|
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
10
|
export { LIGHT, DARK } from './model.js';
|
|
10
11
|
const DEFAULTS = { ...LIGHT, font: 'inherit' };
|
|
11
12
|
const v = (k) => `var(--fig-${k}, ${DEFAULTS[k]})`;
|
|
@@ -42,12 +43,35 @@ const cardBody = (c) => {
|
|
|
42
43
|
return (r.tag?.length ?? 0) > 2 ? (_jsxs("div", { children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 5, marginBottom: 1 }, children: [tag, mark] }), text] }, i)) : (_jsxs("div", { style: { display: 'flex', gap: 5, alignItems: 'baseline' }, children: [tag, text, mark] }, i));
|
|
43
44
|
}) }));
|
|
44
45
|
};
|
|
46
|
+
const HIDDEN = {
|
|
47
|
+
position: 'absolute',
|
|
48
|
+
width: 1,
|
|
49
|
+
height: 1,
|
|
50
|
+
overflow: 'hidden',
|
|
51
|
+
clip: 'rect(0 0 0 0)',
|
|
52
|
+
whiteSpace: 'pre-line',
|
|
53
|
+
};
|
|
45
54
|
/**
|
|
46
55
|
* The interactive player for one figure. Give it a `FlowProps` spec.
|
|
47
56
|
* Use `toSvg` from `flowfig/svg` for a static animated SVG.
|
|
48
57
|
*/
|
|
49
|
-
export function Flow(
|
|
58
|
+
export function Flow(props) {
|
|
59
|
+
const { layout, edges, steps, lanes, timeline } = props;
|
|
60
|
+
const key = useMemo(() => specKey(layout, edges, steps), [layout, edges, steps]);
|
|
61
|
+
const [narrow, setNarrow] = useState({ wide: 0, key: '' });
|
|
62
|
+
const fold = !lanes && !timeline && props.rail !== 'only';
|
|
63
|
+
const wide = fold && narrow.key === key ? narrow.wide : 0;
|
|
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]);
|
|
66
|
+
return (_jsx(FlowBody, { ...props, layout: placed.layout, edges: placed.edges, written: props, wide: wide, setWide: fold ? setWide : undefined }));
|
|
67
|
+
}
|
|
68
|
+
// The ASYNC tag is 9 px; phone text needs 11 px.
|
|
69
|
+
const RAIL_ZOOM = 11 / 9;
|
|
70
|
+
function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, autoplay = true, check = false, rail: withRail = false, lanes, timeline, today, written, wide, setWide, }) {
|
|
50
71
|
const tl = timeline && isLanesLayout(layout);
|
|
72
|
+
const alt = useMemo(() => altText({ layout, edges, steps: stepsIn }), [layout, edges, stepsIn]);
|
|
73
|
+
const descId = useId();
|
|
74
|
+
const tabsId = useId();
|
|
51
75
|
const synthetic = tl && !stepsIn.length;
|
|
52
76
|
const steps = useMemo(() => (synthetic ? timelineBeats({ layout, edges, timeline, today }) : stepsIn), [synthetic, layout, edges, timeline, today, stepsIn]);
|
|
53
77
|
const [axisW, setAxisW] = useState(TL_AXIS_W);
|
|
@@ -72,6 +96,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
72
96
|
const firstLabel = useRef('');
|
|
73
97
|
const paths = useRef({});
|
|
74
98
|
const [routed, setRouted] = useState([]);
|
|
99
|
+
const markSides = new Set(nodes(layout).flatMap((n) => (n.mark ? [markSide(n.id, n.mark, routed)] : [])));
|
|
75
100
|
const [extraTall, setExtraTall] = useState(new Set());
|
|
76
101
|
const lanePlan = useMemo(() => lanes && !tl && isLanesLayout(layout)
|
|
77
102
|
? planLanes({ layout, edges, steps, lanes }, { floor: withRail ? (layoutRail({ layout, edges, steps, rail: true }, 560)?.width ?? 0) : 0 })
|
|
@@ -119,6 +144,11 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
119
144
|
const railOnly = withRail === 'only';
|
|
120
145
|
const rail = useMemo(() => (withRail ? layoutRail({ layout, edges, steps, rail: true }, railOnly ? 560 : mapW) : null), [withRail, railOnly, layout, edges, steps, mapW]);
|
|
121
146
|
const noMap = railOnly && rail != null;
|
|
147
|
+
const railPane = useRef(null);
|
|
148
|
+
const follow = useRef(true);
|
|
149
|
+
const user = useRef(false);
|
|
150
|
+
const left = useRef(0);
|
|
151
|
+
const busy = useRef(0);
|
|
122
152
|
const jump = useRef(null);
|
|
123
153
|
const [hoverEdge, setHoverEdge] = useState(null);
|
|
124
154
|
const [still, setStill] = useState(false);
|
|
@@ -138,8 +168,14 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
138
168
|
const el = root.current, box = outer.current;
|
|
139
169
|
if (!el || !box)
|
|
140
170
|
return;
|
|
171
|
+
let last = '';
|
|
141
172
|
const measure = () => {
|
|
142
|
-
|
|
173
|
+
if (!el.offsetWidth)
|
|
174
|
+
return;
|
|
175
|
+
const goal = nextWide(wide, box.clientWidth, el.offsetWidth);
|
|
176
|
+
if (setWide && goal !== wide)
|
|
177
|
+
return setWide(goal);
|
|
178
|
+
const scale = fitScale(wide, box.clientWidth, el.offsetWidth);
|
|
143
179
|
setFit({ scale, height: el.offsetHeight * scale });
|
|
144
180
|
setMapW(el.offsetWidth);
|
|
145
181
|
if (area.current)
|
|
@@ -192,6 +228,10 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
192
228
|
const li = layout.children.findIndex((l) => isGroup(l) && (l.id === id || l.children.some((b) => b.id === id)));
|
|
193
229
|
return laneBands[`${li}@${lanePlan?.ends.get(eid)?.[j]}`];
|
|
194
230
|
};
|
|
231
|
+
const key = JSON.stringify([rects, extra, bands, laneBands, pillArea], (_k, v) => (typeof v === 'number' ? Math.round(v) : v));
|
|
232
|
+
if (key === last)
|
|
233
|
+
return;
|
|
234
|
+
last = key;
|
|
195
235
|
const end = (eid, id, start) => lanePlan ? laneEnd(lanePlan, eid, id, start, (lane, b) => bands[`${lane}@${b}`], rects) : id;
|
|
196
236
|
const next = route(edges.map((e, i) => ({
|
|
197
237
|
id: ids[i],
|
|
@@ -199,6 +239,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
199
239
|
to: end(ids[i], e.to, false),
|
|
200
240
|
around: e.around ?? (lanePlan?.around.has(ids[i]) ? 'below' : undefined),
|
|
201
241
|
...(tl && { sides: ['r', 'l'], elbow: true }),
|
|
242
|
+
...(rowBreaks.has(e) && { drop: true }),
|
|
202
243
|
...(stubs.has(ids[i]) && {
|
|
203
244
|
stub: stubs.get(ids[i]).map(labelPillW),
|
|
204
245
|
bands: [bandOf(ids[i], e.from, 0), bandOf(ids[i], e.to, 1)],
|
|
@@ -214,16 +255,38 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
214
255
|
};
|
|
215
256
|
measure();
|
|
216
257
|
const ro = new ResizeObserver(measure);
|
|
217
|
-
ro.observe(el);
|
|
258
|
+
ro.observe(el, { box: 'border-box' });
|
|
218
259
|
ro.observe(box);
|
|
219
260
|
el.querySelectorAll('[data-fig]').forEach((n) => ro.observe(n));
|
|
220
261
|
return () => ro.disconnect();
|
|
221
|
-
}, [edges, ids, layout, tips, noMap, lanes, tl, axisW, lanePlan, extraTall]);
|
|
262
|
+
}, [edges, ids, layout, tips, noMap, lanes, tl, axisW, lanePlan, extraTall, wide, setWide]);
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
const pane = railPane.current;
|
|
265
|
+
const n = rail?.rows.flatMap((r) => (r.kind === 'message' && r.step === active && r.beat === beat ? [r.n] : []))[0];
|
|
266
|
+
const now = wide && follow.current && n != null ? pane?.querySelector(`[data-rail-row="${n}"] path`) : null;
|
|
267
|
+
if (!pane || !now)
|
|
268
|
+
return;
|
|
269
|
+
const [p, r] = [pane.getBoundingClientRect(), now.getBoundingClientRect()];
|
|
270
|
+
user.current = false;
|
|
271
|
+
// Some browsers fire no scrollend event.
|
|
272
|
+
busy.current = performance.now() + 1000;
|
|
273
|
+
pane.scrollTo({ left: pane.scrollLeft + (r.left + r.right) / 2 - p.left - pane.clientWidth / 2, behavior: still ? 'auto' : 'smooth' });
|
|
274
|
+
}, [wide, active, beat, still, rail]);
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
const pane = railPane.current;
|
|
277
|
+
if (!pane)
|
|
278
|
+
return;
|
|
279
|
+
const end = () => (busy.current = 0);
|
|
280
|
+
pane.addEventListener('scrollend', end);
|
|
281
|
+
return () => pane.removeEventListener('scrollend', end);
|
|
282
|
+
}, [rail]);
|
|
222
283
|
const reported = useRef(new Set());
|
|
223
284
|
useEffect(() => {
|
|
224
285
|
const el = root.current, box = outer.current;
|
|
225
286
|
const fig = figure.current;
|
|
226
|
-
if (!check || !fig || (!noMap && (!el || !box)))
|
|
287
|
+
if (!check || !fig || (!noMap && (!el || !box || !el.offsetWidth)))
|
|
288
|
+
return;
|
|
289
|
+
if (setWide && el && box && nextWide(wide, box.clientWidth, el.offsetWidth) !== wide)
|
|
227
290
|
return;
|
|
228
291
|
const base = (el ?? fig).getBoundingClientRect();
|
|
229
292
|
const k = el ? base.width / el.offsetWidth : 1;
|
|
@@ -263,11 +326,15 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
263
326
|
// An SVG <text> has no CSS box, so skip clientWidth.
|
|
264
327
|
(t instanceof SVGElement || t.clientWidth > 0) &&
|
|
265
328
|
!t.closest('[data-fig-tag],[aria-hidden]'))
|
|
266
|
-
.map((t) =>
|
|
329
|
+
.map((t) => {
|
|
330
|
+
const svg = railPane.current?.firstElementChild;
|
|
331
|
+
const at = el?.contains(t) ? k : svg?.contains(t) && rail ? svg.getBoundingClientRect().width / rail.width : 1;
|
|
332
|
+
return parseFloat(getComputedStyle(t).fontSize) * at;
|
|
333
|
+
});
|
|
267
334
|
const labels = Object.fromEntries([...fig.querySelectorAll('[data-fig-label]')].map((n) => [n.dataset.figLabel, rel(n)]));
|
|
268
335
|
const lanesIn = el ? [...el.querySelectorAll('[data-fig-band]')] : [];
|
|
269
336
|
const scene = {
|
|
270
|
-
width:
|
|
337
|
+
width: (box ?? fig).clientWidth,
|
|
271
338
|
boxes,
|
|
272
339
|
...(lanePlan?.stubs.size && {
|
|
273
340
|
lanes: lanesIn.map((n) => ({
|
|
@@ -311,11 +378,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
311
378
|
],
|
|
312
379
|
minFont: Math.min(...fonts),
|
|
313
380
|
};
|
|
314
|
-
for (const f of [
|
|
315
|
-
...checkSpec({ layout, edges, steps }),
|
|
316
|
-
...checkScene(scene, { width: (box ?? fig).clientWidth }),
|
|
317
|
-
...checkTheme(theme),
|
|
318
|
-
]) {
|
|
381
|
+
for (const f of [...checkSpec(written), ...checkScene(scene, { width: (box ?? fig).clientWidth }), ...checkTheme(theme)]) {
|
|
319
382
|
const key = f.rule + ':' + f.ids.join() + ':' + f.message;
|
|
320
383
|
if (reported.current.has(key))
|
|
321
384
|
continue;
|
|
@@ -323,7 +386,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
323
386
|
// oxlint-disable-next-line no-console
|
|
324
387
|
console.warn(`flowfig check: ${f.severity} ${f.rule}: ${f.message}`);
|
|
325
388
|
}
|
|
326
|
-
}, [check, routed, layout, edges, steps, ids, theme, rail, noMap, lanePlan]);
|
|
389
|
+
}, [check, routed, written, layout, edges, steps, ids, theme, rail, noMap, lanePlan, wide, setWide]);
|
|
327
390
|
useEffect(() => {
|
|
328
391
|
const gs = dots.current, cs = chips.current, edgeOf = Object.fromEntries(edges.map((e, i) => [ids[i], e]));
|
|
329
392
|
if (clock.current.beats !== beats) {
|
|
@@ -443,6 +506,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
443
506
|
return t && TONES[t];
|
|
444
507
|
};
|
|
445
508
|
const goTo = (step, b) => {
|
|
509
|
+
follow.current = true;
|
|
446
510
|
setPlaying(true);
|
|
447
511
|
if (step === active) {
|
|
448
512
|
clock.current.elapsed = beats.slice(0, b).reduce((t, x) => t + beatMs(x, speed), 0);
|
|
@@ -638,15 +702,9 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
638
702
|
display: 'flex',
|
|
639
703
|
flexDirection: item.direction ?? 'row',
|
|
640
704
|
gap: groupGap(item, edges),
|
|
641
|
-
alignItems: item.align
|
|
642
|
-
? item.align === 'center'
|
|
643
|
-
? 'center'
|
|
644
|
-
: 'flex-' + item.align
|
|
645
|
-
: item.direction === 'column'
|
|
646
|
-
? 'stretch'
|
|
647
|
-
: 'center',
|
|
705
|
+
alignItems: !item.align || item.align === 'center' ? 'center' : 'flex-' + item.align,
|
|
648
706
|
justifyContent: 'center',
|
|
649
|
-
}, children: item.children.map((c) => renderItem(c, depth + 1)) })] }, item.id ??
|
|
707
|
+
}, children: item.children.map((c) => renderItem(c, depth + 1)) })] }, item.id ?? `group:${nodes(item)[0]?.id}`));
|
|
650
708
|
}
|
|
651
709
|
const lit = litNodes.has(item.id);
|
|
652
710
|
const diamond = item.shape === 'decision';
|
|
@@ -701,14 +759,12 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
701
759
|
transition: 'border-color .25s',
|
|
702
760
|
} })), item.mark && (_jsx("span", { "aria-hidden": true, style: {
|
|
703
761
|
position: 'absolute',
|
|
704
|
-
|
|
705
|
-
transform: 'translateY(-50%)',
|
|
762
|
+
...markAt(markSide(item.id, item.mark, routed), item.mark === 'start' ? -18 : -20),
|
|
706
763
|
boxSizing: 'border-box',
|
|
707
764
|
borderRadius: '50%',
|
|
708
765
|
...(item.mark === 'start'
|
|
709
|
-
? {
|
|
766
|
+
? { width: 10, height: 10, background: bt ?? v('accent') }
|
|
710
767
|
: {
|
|
711
|
-
right: -20,
|
|
712
768
|
width: 14,
|
|
713
769
|
height: 14,
|
|
714
770
|
border: `1.5px solid ${bt ?? v('accent')}`,
|
|
@@ -747,7 +803,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
747
803
|
background: v('bg'),
|
|
748
804
|
color: v('muted'),
|
|
749
805
|
};
|
|
750
|
-
return (_jsxs("figure", { ref: figure, className: "flowfig", style: {
|
|
806
|
+
return (_jsxs("figure", { ref: figure, className: "flowfig", "aria-label": alt.title, "aria-describedby": descId, style: {
|
|
751
807
|
...vars,
|
|
752
808
|
position: full ? 'fixed' : 'relative',
|
|
753
809
|
inset: full ? 0 : undefined,
|
|
@@ -760,7 +816,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
760
816
|
background: v('bg'),
|
|
761
817
|
fontFamily: v('font'),
|
|
762
818
|
color: v('fg'),
|
|
763
|
-
}, children: [_jsx("button", { type: "button", "aria-label": full ? 'Close full screen' : 'Full screen', title: full ? 'Close (Esc)' : 'Full screen', onClick: () => setFull((f) => !f), style: {
|
|
819
|
+
}, children: [_jsx("div", { id: descId, style: HIDDEN, children: alt.desc }), _jsx("button", { type: "button", "aria-label": full ? 'Close full screen' : 'Full screen', title: full ? 'Close (Esc)' : 'Full screen', onClick: () => setFull((f) => !f), style: {
|
|
764
820
|
...iconBtn,
|
|
765
821
|
position: 'absolute',
|
|
766
822
|
top: 10,
|
|
@@ -778,8 +834,8 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
778
834
|
borderBottom: `1px solid ${v('border')}`,
|
|
779
835
|
backgroundImage: `radial-gradient(color-mix(in srgb, ${v('fg')} 16%, transparent) 1px, transparent 1.2px)`,
|
|
780
836
|
backgroundSize: '14px 14px',
|
|
781
|
-
}, children: _jsx("div", { ref: outer, style: {
|
|
782
|
-
overflow: fit.scale
|
|
837
|
+
}, children: _jsx("div", { ref: outer, role: "tabpanel", id: `${tabsId}-panel`, "aria-labelledby": `${tabsId}-tab-${active ?? 0}`, style: {
|
|
838
|
+
overflow: wide || fit.scale <= 0.5 ? 'auto' : 'hidden',
|
|
783
839
|
height: fit.scale < 1 ? fit.height : undefined,
|
|
784
840
|
}, children: _jsxs("div", { ref: root, style: {
|
|
785
841
|
position: 'relative',
|
|
@@ -787,8 +843,8 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
787
843
|
margin: '0 auto',
|
|
788
844
|
transform: fit.scale < 1 ? `scale(${fit.scale})` : undefined,
|
|
789
845
|
transformOrigin: 'top left',
|
|
790
|
-
paddingTop: arcRoom(routed, 'above') || 4,
|
|
791
|
-
paddingBottom: arcRoom(routed, 'below') || 4,
|
|
846
|
+
paddingTop: arcRoom(routed, 'above') || (markSides.has('t') ? 20 : 4),
|
|
847
|
+
paddingBottom: arcRoom(routed, 'below') || (markSides.has('b') ? 20 : 4),
|
|
792
848
|
paddingLeft: arcRoom(routed, 'left') || 4,
|
|
793
849
|
paddingRight: arcRoom(routed, 'right') || 4,
|
|
794
850
|
}, children: [renderItem(layout, 0), _jsxs("svg", { style: {
|
|
@@ -869,52 +925,72 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
869
925
|
fontFamily: MONO,
|
|
870
926
|
transition: 'background .25s, color .25s, opacity .25s',
|
|
871
927
|
}, children: p.text }, p.key)));
|
|
872
|
-
})] }) }) }) })), rail && (
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
? '
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
928
|
+
})] }) }) }) })), rail && (_jsx("div", { ref: railPane, onWheel: () => (user.current = true), onPointerDown: () => (user.current = true), onTouchStart: () => (user.current = true), onScroll: (e) => {
|
|
929
|
+
const x = e.currentTarget.scrollLeft;
|
|
930
|
+
if (user.current && x !== left.current && performance.now() > busy.current)
|
|
931
|
+
follow.current = false;
|
|
932
|
+
left.current = x;
|
|
933
|
+
}, style: { overflowX: wide ? 'auto' : undefined }, children: _jsxs("svg", { viewBox: `0 0 ${rail.width} ${rail.height}`, style: {
|
|
934
|
+
display: 'block',
|
|
935
|
+
width: wide ? rail.width * RAIL_ZOOM : '100%',
|
|
936
|
+
maxWidth: wide ? undefined : rail.width,
|
|
937
|
+
height: 'auto',
|
|
938
|
+
margin: `${noMap ? 0 : RAIL.gap}px auto 0`,
|
|
939
|
+
overflow: 'visible',
|
|
940
|
+
fontFamily: v('font'),
|
|
941
|
+
}, children: [rail.bands.map((b) => (_jsxs("g", { children: [_jsx("rect", { x: b.rect.x, y: b.rect.y, width: b.rect.w, height: b.rect.h, rx: 10, fill: v('surface'), stroke: v('border') }), _jsx("text", { x: b.rect.x + 10, y: b.rect.y + 14, fill: v('muted'), fontSize: 11, fontWeight: 600, letterSpacing: ".04em", children: b.label.toUpperCase() })] }, b.id))), rail.columns.map((c) => (_jsx("text", { x: c.x, y: rail.head - RAIL.cols / 2 + 4, fill: v('fg'), fontSize: 12, fontWeight: 500, textAnchor: "middle", children: c.label }, c.id))), (() => {
|
|
942
|
+
const st = railState(rail, active ?? 0);
|
|
943
|
+
const lines = rail.rows.flatMap((row, i) => row.kind === 'message' && st[i].shown ? rail.columns.map((c) => `M ${c.x} ${st[i].y} V ${st[i].y + RAIL.row}`) : []);
|
|
944
|
+
return _jsx("path", { d: lines.join(' '), stroke: v('border'), strokeDasharray: "2 3" });
|
|
945
|
+
})(), (() => {
|
|
946
|
+
const st = railState(rail, active ?? 0);
|
|
947
|
+
return rail.rows.map((row, i) => {
|
|
948
|
+
if (!st[i].shown)
|
|
949
|
+
return null;
|
|
950
|
+
const y = st[i].y;
|
|
951
|
+
if (row.kind === 'phase') {
|
|
952
|
+
const open = !rail.folds || row.step === active;
|
|
953
|
+
const playing = row.step === active ? rail.rows.find((r) => r.kind === 'message' && r.step === active && r.beat === beat) : undefined;
|
|
954
|
+
return (_jsxs("g", { style: { cursor: open ? undefined : 'pointer' }, onClick: open ? undefined : () => goTo(row.step, 0), children: [_jsx("text", { x: RAIL.pad, y: y + 19, fill: open ? v('fg') : v('muted'), fontSize: 13, fontWeight: 600, fontFamily: MONO, children: open ? row.label : foldedLabel(row) }), _jsx("path", { d: `M ${open ? row.line.open : row.line.folded} ${y + 15} H ${row.line.end}`, stroke: v('border') }), playing?.kind === 'message' && (_jsxs("text", { x: rail.width - RAIL.pad, y: y + 19, fill: v('muted'), fontSize: 11, fontWeight: 600, textAnchor: "end", children: [still ? rail.total : playing.n, " of ", rail.total] }))] }, `p${i}`));
|
|
955
|
+
}
|
|
956
|
+
const state = still
|
|
957
|
+
? 'done'
|
|
958
|
+
: active == null || row.step > active || (row.step === active && row.beat > beat)
|
|
959
|
+
? 'next'
|
|
960
|
+
: row.step === active && row.beat === beat
|
|
961
|
+
? 'now'
|
|
962
|
+
: 'done';
|
|
963
|
+
const on = state === 'now' ||
|
|
964
|
+
hoverEdge === row.edge ||
|
|
965
|
+
(hover != null && [rail.columns[row.from].id, rail.columns[row.to].id].includes(hover));
|
|
966
|
+
const [x1, x2] = [rail.columns[row.from].x, rail.columns[row.to].x];
|
|
967
|
+
const ly = y + RAIL.row / 2;
|
|
968
|
+
const tone = state === 'now' && row.tone ? TONES[row.tone] : undefined;
|
|
969
|
+
const g = row.group != null ? rail.groups[row.group] : null;
|
|
970
|
+
const tip = edgeTip(row.edge, edges[ids.indexOf(row.edge)].source, allBeats);
|
|
971
|
+
return (_jsxs("g", { "data-rail-row": row.n, "data-state": state, opacity: state === 'next' && !on ? 0.45 : 1, style: { cursor: 'pointer' }, onClick: () => goTo(row.step, row.beat), onMouseEnter: () => setHoverEdge(row.edge), onMouseLeave: () => setHoverEdge(null), children: [tip && _jsx("title", { children: tip }), g && g.rows[0] === i && (_jsx("rect", { x: groupBox(rail, g).x, y: y + groupBox(rail, g).y, width: groupBox(rail, g).w, height: groupBox(rail, g).h, rx: 6, fill: `color-mix(in srgb, ${v('accent')} 8%, ${v('bg')})`, stroke: v('accent'), strokeDasharray: "3 3" })), _jsx("path", { d: `M ${x1} ${ly} H ${x2}`, fill: "none", stroke: on ? (tone ?? v('accent')) : v('muted'), strokeWidth: on ? EDGE_ON : EDGE_OFF, strokeDasharray: row.async ? '4 3' : undefined, markerEnd: "url(#flowfig-rail-arrow)" }), row.async &&
|
|
972
|
+
(() => {
|
|
973
|
+
const tagW = ASYNC_TAG_W;
|
|
974
|
+
const tx = !row.pill ? (x1 + x2) / 2 - tagW / 2 : x2 > x1 ? row.pill.x - tagW - 4 : row.pill.x + row.pill.w + 4;
|
|
975
|
+
return (_jsxs(_Fragment, { children: [_jsx("rect", { x: tx, y: ly - 6, width: tagW, height: 12, rx: 4, fill: v('bg') }), _jsx("rect", { x: tx, y: ly - 6, width: tagW, height: 12, rx: 4, fill: TONES.gray, fillOpacity: 0.15 }), _jsx("text", { x: tx + tagW / 2, y: ly + 3, fill: TONES.gray, fontSize: 9, fontWeight: 600, letterSpacing: ".03em", textAnchor: "middle", "data-fig-tag": true, children: "ASYNC" })] }));
|
|
976
|
+
})(), row.pill && (_jsxs("g", { "data-fig-label": `rail:${row.n}`, children: [_jsx("rect", { x: row.pill.x, y: ly - 9, width: row.pill.w, height: 18, rx: 9, fill: on ? (tone ? toneFill(tone) : v('accent')) : v('bg'), stroke: on ? (tone ?? v('accent')) : v('border') }), _jsx("text", { x: row.pill.x + row.pill.w / 2, y: ly + 4, fill: on ? ON_ACCENT : v('muted'), fontSize: 11, fontFamily: MONO, textAnchor: "middle", children: row.text })] }))] }, `m${i}`));
|
|
977
|
+
});
|
|
978
|
+
})(), _jsx("defs", { children: _jsx("marker", { id: "flowfig-rail-arrow", viewBox: "0 0 10 10", refX: "9", refY: "5", markerWidth: "7", markerHeight: "7", orient: "auto-start-reverse", children: _jsx("path", { d: "M 0 1 L 9 5 L 0 9 z", fill: v('muted') }) }) })] }) })), steps.length > 0 && (_jsxs("figcaption", { style: { marginTop: 14, textAlign: 'center' }, children: [_jsxs("div", { style: { display: 'flex', gap: 10, justifyContent: 'center', alignItems: 'center' }, children: [_jsx("button", { type: "button", "aria-label": playing ? 'Pause' : 'Play', title: playing ? 'Pause' : 'Play', style: iconBtn, onClick: () => {
|
|
979
|
+
follow.current ||= !playing;
|
|
980
|
+
setPlaying((p) => !p);
|
|
981
|
+
}, children: _jsx(Icon, { d: playing ? 'M5.5 4v8M10.5 4v8' : 'M5 3.5v9l7.5-4.5z', fill: !playing }) }), !synthetic && (_jsx("div", { role: "tablist", onKeyDown: (e) => {
|
|
982
|
+
const to = { ArrowRight: (active ?? 0) + 1, ArrowLeft: (active ?? 0) - 1, Home: 0, End: steps.length - 1 }[e.key];
|
|
983
|
+
if (to === undefined)
|
|
984
|
+
return;
|
|
985
|
+
e.preventDefault();
|
|
986
|
+
const n = (to + steps.length) % steps.length;
|
|
987
|
+
clock.current.elapsed = 0;
|
|
988
|
+
follow.current = true;
|
|
989
|
+
setActive(n);
|
|
990
|
+
setPlaying(true);
|
|
991
|
+
setBeat(0);
|
|
992
|
+
document.getElementById(`${tabsId}-tab-${n}`)?.focus();
|
|
993
|
+
}, style: {
|
|
918
994
|
display: 'inline-flex',
|
|
919
995
|
gap: 2,
|
|
920
996
|
padding: 3,
|
|
@@ -923,8 +999,9 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
923
999
|
border: `1px solid ${v('border')}`,
|
|
924
1000
|
}, children: steps.map((s, i) => {
|
|
925
1001
|
const on = active === i;
|
|
926
|
-
return (_jsxs("button", { type: "button", role: "tab", "aria-selected": on, onClick: () => {
|
|
1002
|
+
return (_jsxs("button", { type: "button", role: "tab", "aria-selected": on, id: `${tabsId}-tab-${i}`, "aria-controls": noMap ? undefined : `${tabsId}-panel`, tabIndex: on ? 0 : -1, onClick: () => {
|
|
927
1003
|
clock.current.elapsed = 0;
|
|
1004
|
+
follow.current = true;
|
|
928
1005
|
setActive(i);
|
|
929
1006
|
setPlaying(true);
|
|
930
1007
|
setBeat(0);
|
|
@@ -973,6 +1050,9 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
973
1050
|
animation: 'flowfig-in .35s ease-out',
|
|
974
1051
|
}, children: said }, `${active}-${saidAt}`))] }))] }));
|
|
975
1052
|
}
|
|
1053
|
+
const markAt = (side, off) => side === 'l' || side === 'r'
|
|
1054
|
+
? { [side === 'l' ? 'left' : 'right']: off, top: '50%', transform: 'translateY(-50%)' }
|
|
1055
|
+
: { [side === 't' ? 'top' : 'bottom']: off, left: '50%', transform: 'translateX(-50%)' };
|
|
976
1056
|
function Icon({ d, fill = false }) {
|
|
977
1057
|
return (_jsx("svg", { width: 14, height: 14, viewBox: "0 0 16 16", fill: fill ? 'currentColor' : 'none', stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: _jsx("path", { d: d }) }));
|
|
978
1058
|
}
|