flowfig 0.9.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 +309 -61
- package/dist/atlas.d.ts +6 -0
- package/dist/atlas.js +122 -0
- package/dist/auto.d.ts +3 -0
- package/dist/auto.js +397 -0
- package/dist/browser.js +3 -1
- package/dist/check.d.ts +2 -1
- package/dist/check.js +61 -19
- package/dist/cli.js +29 -6
- package/dist/coverage.js +10 -7
- package/dist/diff.d.ts +10 -0
- package/dist/diff.js +137 -7
- package/dist/geometry.d.ts +4 -0
- package/dist/geometry.js +45 -17
- package/dist/index.d.ts +1 -1
- package/dist/index.js +177 -83
- package/dist/load.js +4 -3
- package/dist/mermaid.d.ts +19 -0
- package/dist/mermaid.js +617 -0
- package/dist/model.d.ts +19 -6
- package/dist/model.js +60 -17
- 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 +76 -26
- package/dist/trace.js +10 -3
- package/dist/verify.d.ts +2 -0
- package/dist/verify.js +36 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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, useId, 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, shownFont, textFloor, 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]})`;
|
|
@@ -54,17 +55,37 @@ const HIDDEN = {
|
|
|
54
55
|
* The interactive player for one figure. Give it a `FlowProps` spec.
|
|
55
56
|
* Use `toSvg` from `flowfig/svg` for a static animated SVG.
|
|
56
57
|
*/
|
|
57
|
-
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 > 0 ? { ...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.
|
|
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
|
+
};
|
|
77
|
+
function FlowBody({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, autoplay = true, check = false, rail: withRail = false, lanes, timeline, today, written, wide, setWide, }) {
|
|
58
78
|
const tl = timeline && isLanesLayout(layout);
|
|
59
79
|
const alt = useMemo(() => altText({ layout, edges, steps: stepsIn }), [layout, edges, stepsIn]);
|
|
60
80
|
const descId = useId();
|
|
81
|
+
const tabsId = useId();
|
|
61
82
|
const synthetic = tl && !stepsIn.length;
|
|
62
83
|
const steps = useMemo(() => (synthetic ? timelineBeats({ layout, edges, timeline, today }) : stepsIn), [synthetic, layout, edges, timeline, today, stepsIn]);
|
|
63
84
|
const [axisW, setAxisW] = useState(TL_AXIS_W);
|
|
64
85
|
const area = useRef(null);
|
|
65
86
|
const root = useRef(null);
|
|
66
87
|
const outer = useRef(null);
|
|
67
|
-
const [fit, setFit] = useState({ scale: 1, height: 0 });
|
|
88
|
+
const [fit, setFit] = useState({ scale: 1, height: 0, scroll: false });
|
|
68
89
|
const [full, setFull] = useState(false);
|
|
69
90
|
useEffect(() => {
|
|
70
91
|
if (!full)
|
|
@@ -82,6 +103,8 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
82
103
|
const firstLabel = useRef('');
|
|
83
104
|
const paths = useRef({});
|
|
84
105
|
const [routed, setRouted] = useState([]);
|
|
106
|
+
const routedOn = useRef(new WeakMap());
|
|
107
|
+
const markSides = new Set(nodes(layout).flatMap((n) => (n.mark ? [markSide(n.id, n.mark, routed)] : [])));
|
|
85
108
|
const [extraTall, setExtraTall] = useState(new Set());
|
|
86
109
|
const lanePlan = useMemo(() => lanes && !tl && isLanesLayout(layout)
|
|
87
110
|
? planLanes({ layout, edges, steps, lanes }, { floor: withRail ? (layoutRail({ layout, edges, steps, rail: true }, 560)?.width ?? 0) : 0 })
|
|
@@ -129,6 +152,13 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
129
152
|
const railOnly = withRail === 'only';
|
|
130
153
|
const rail = useMemo(() => (withRail ? layoutRail({ layout, edges, steps, rail: true }, railOnly ? 560 : mapW) : null), [withRail, railOnly, layout, edges, steps, mapW]);
|
|
131
154
|
const noMap = railOnly && rail != null;
|
|
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;
|
|
158
|
+
const follow = useRef(true);
|
|
159
|
+
const user = useRef(false);
|
|
160
|
+
const left = useRef(0);
|
|
161
|
+
const busy = useRef(0);
|
|
132
162
|
const jump = useRef(null);
|
|
133
163
|
const [hoverEdge, setHoverEdge] = useState(null);
|
|
134
164
|
const [still, setStill] = useState(false);
|
|
@@ -148,15 +178,20 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
148
178
|
const el = root.current, box = outer.current;
|
|
149
179
|
if (!el || !box)
|
|
150
180
|
return;
|
|
181
|
+
let last = '';
|
|
151
182
|
const measure = () => {
|
|
152
|
-
|
|
153
|
-
|
|
183
|
+
if (!el.offsetWidth)
|
|
184
|
+
return;
|
|
185
|
+
const goal = nextWide(wide, box.clientWidth, el.offsetWidth);
|
|
186
|
+
if (setWide && goal !== wide)
|
|
187
|
+
return setWide(goal);
|
|
188
|
+
const scale = fitScale(wide, box.clientWidth, el.offsetWidth);
|
|
189
|
+
setFit({ scale, height: el.offsetHeight * scale, scroll: scale > box.clientWidth / el.offsetWidth });
|
|
154
190
|
setMapW(el.offsetWidth);
|
|
155
191
|
if (area.current)
|
|
156
192
|
setAxisW(area.current.offsetWidth);
|
|
157
193
|
const base = el.getBoundingClientRect();
|
|
158
|
-
|
|
159
|
-
const k = base.width / el.offsetWidth;
|
|
194
|
+
const k = shownScale(el);
|
|
160
195
|
const rects = {};
|
|
161
196
|
el.querySelectorAll('[data-fig]').forEach((n) => {
|
|
162
197
|
const r = n.getBoundingClientRect();
|
|
@@ -202,6 +237,10 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
202
237
|
const li = layout.children.findIndex((l) => isGroup(l) && (l.id === id || l.children.some((b) => b.id === id)));
|
|
203
238
|
return laneBands[`${li}@${lanePlan?.ends.get(eid)?.[j]}`];
|
|
204
239
|
};
|
|
240
|
+
const key = JSON.stringify([rects, extra, bands, laneBands, pillArea], (_k, v) => (typeof v === 'number' ? Math.round(v) : v));
|
|
241
|
+
if (key === last)
|
|
242
|
+
return;
|
|
243
|
+
last = key;
|
|
205
244
|
const end = (eid, id, start) => lanePlan ? laneEnd(lanePlan, eid, id, start, (lane, b) => bands[`${lane}@${b}`], rects) : id;
|
|
206
245
|
const next = route(edges.map((e, i) => ({
|
|
207
246
|
id: ids[i],
|
|
@@ -209,12 +248,14 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
209
248
|
to: end(ids[i], e.to, false),
|
|
210
249
|
around: e.around ?? (lanePlan?.around.has(ids[i]) ? 'below' : undefined),
|
|
211
250
|
...(tl && { sides: ['r', 'l'], elbow: true }),
|
|
251
|
+
...(rowBreaks.has(e) && { drop: true }),
|
|
212
252
|
...(stubs.has(ids[i]) && {
|
|
213
253
|
stub: stubs.get(ids[i]).map(labelPillW),
|
|
214
254
|
bands: [bandOf(ids[i], e.from, 0), bandOf(ids[i], e.to, 1)],
|
|
215
255
|
}),
|
|
216
256
|
...(!tl && e.label != null && { labelW: labelPillW(str(e.label)) }),
|
|
217
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);
|
|
218
259
|
setRouted(next);
|
|
219
260
|
if (lanePlan) {
|
|
220
261
|
const more = [...tightCopies(layout.children, lanePlan.ends, edges, next)].filter((k) => !lanePlan.tall.has(k) && !extraTall.has(k));
|
|
@@ -224,19 +265,44 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
224
265
|
};
|
|
225
266
|
measure();
|
|
226
267
|
const ro = new ResizeObserver(measure);
|
|
227
|
-
ro.observe(el);
|
|
268
|
+
ro.observe(el, { box: 'border-box' });
|
|
228
269
|
ro.observe(box);
|
|
229
270
|
el.querySelectorAll('[data-fig]').forEach((n) => ro.observe(n));
|
|
230
271
|
return () => ro.disconnect();
|
|
231
|
-
}, [edges, ids, layout, tips, noMap, lanes, tl, axisW, lanePlan, extraTall]);
|
|
272
|
+
}, [edges, ids, layout, tips, noMap, lanes, tl, axisW, lanePlan, extraTall, wide, setWide]);
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
const pane = railPane.current;
|
|
275
|
+
const n = rail?.rows.flatMap((r) => (r.kind === 'message' && r.step === active && r.beat === beat ? [r.n] : []))[0];
|
|
276
|
+
const now = zoom && follow.current && n != null ? pane?.querySelector(`[data-rail-row="${n}"] path`) : null;
|
|
277
|
+
if (!pane || !now)
|
|
278
|
+
return;
|
|
279
|
+
const [p, r] = [pane.getBoundingClientRect(), now.getBoundingClientRect()];
|
|
280
|
+
user.current = false;
|
|
281
|
+
// Some browsers fire no scrollend event.
|
|
282
|
+
busy.current = performance.now() + 1000;
|
|
283
|
+
pane.scrollTo({ left: pane.scrollLeft + (r.left + r.right) / 2 - p.left - pane.clientWidth / 2, behavior: still ? 'auto' : 'smooth' });
|
|
284
|
+
}, [zoom, active, beat, still, rail]);
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
const pane = railPane.current;
|
|
287
|
+
if (!pane)
|
|
288
|
+
return;
|
|
289
|
+
const end = () => (busy.current = 0);
|
|
290
|
+
pane.addEventListener('scrollend', end);
|
|
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]);
|
|
232
296
|
const reported = useRef(new Set());
|
|
233
297
|
useEffect(() => {
|
|
234
298
|
const el = root.current, box = outer.current;
|
|
235
299
|
const fig = figure.current;
|
|
236
|
-
if (!check || !fig || (
|
|
300
|
+
if (!check || !fig || (noMap ? !railBox : !el || !box || !el.offsetWidth))
|
|
301
|
+
return;
|
|
302
|
+
if (setWide && el && box && nextWide(wide, box.clientWidth, el.offsetWidth) !== wide)
|
|
237
303
|
return;
|
|
238
304
|
const base = (el ?? fig).getBoundingClientRect();
|
|
239
|
-
const k = el ?
|
|
305
|
+
const k = el ? shownScale(el) : 1;
|
|
240
306
|
const rel = (n) => {
|
|
241
307
|
const r = n.getBoundingClientRect();
|
|
242
308
|
return { x: (r.left - base.left) / k, y: (r.top - base.top) / k, w: r.width / k, h: r.height / k };
|
|
@@ -253,7 +319,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
253
319
|
.filter((t) => !t.children.length && t.textContent?.trim() && t.clientWidth > 0 && !t.closest('[aria-hidden]'))
|
|
254
320
|
.map((t) => {
|
|
255
321
|
const fontSize = parseFloat(getComputedStyle(t).fontSize);
|
|
256
|
-
if (!n.dataset.diamond)
|
|
322
|
+
if (!n.dataset.diamond || t.closest('[data-fig-outside]'))
|
|
257
323
|
return { text: t.textContent, fontSize, room: t.clientWidth, need: t.scrollWidth };
|
|
258
324
|
const [r, range] = [rel(t), document.createRange()];
|
|
259
325
|
range.selectNodeContents(t);
|
|
@@ -267,17 +333,28 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
267
333
|
}),
|
|
268
334
|
};
|
|
269
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;
|
|
270
342
|
const fonts = [...fig.querySelectorAll('*')]
|
|
271
343
|
.filter((t) => !t.children.length &&
|
|
272
344
|
t.textContent?.trim() &&
|
|
273
345
|
// An SVG <text> has no CSS box, so skip clientWidth.
|
|
274
346
|
(t instanceof SVGElement || t.clientWidth > 0) &&
|
|
275
347
|
!t.closest('[data-fig-tag],[aria-hidden]'))
|
|
276
|
-
.map((t) =>
|
|
348
|
+
.map((t) => {
|
|
349
|
+
const svg = railPane.current?.firstElementChild;
|
|
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 });
|
|
353
|
+
});
|
|
277
354
|
const labels = Object.fromEntries([...fig.querySelectorAll('[data-fig-label]')].map((n) => [n.dataset.figLabel, rel(n)]));
|
|
278
355
|
const lanesIn = el ? [...el.querySelectorAll('[data-fig-band]')] : [];
|
|
279
356
|
const scene = {
|
|
280
|
-
width:
|
|
357
|
+
width: (box ?? fig).clientWidth,
|
|
281
358
|
boxes,
|
|
282
359
|
...(lanePlan?.stubs.size && {
|
|
283
360
|
lanes: lanesIn.map((n) => ({
|
|
@@ -322,8 +399,8 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
322
399
|
minFont: Math.min(...fonts),
|
|
323
400
|
};
|
|
324
401
|
for (const f of [
|
|
325
|
-
...checkSpec(
|
|
326
|
-
...checkScene(scene, { width: (box ?? fig).clientWidth }),
|
|
402
|
+
...checkSpec(written),
|
|
403
|
+
...checkScene(scene, { width: (box ?? fig).clientWidth, minText: textFloor((box ?? fig).clientWidth), auto: written.layout.auto }),
|
|
327
404
|
...checkTheme(theme),
|
|
328
405
|
]) {
|
|
329
406
|
const key = f.rule + ':' + f.ids.join() + ':' + f.message;
|
|
@@ -333,7 +410,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
333
410
|
// oxlint-disable-next-line no-console
|
|
334
411
|
console.warn(`flowfig check: ${f.severity} ${f.rule}: ${f.message}`);
|
|
335
412
|
}
|
|
336
|
-
}, [check, routed, layout, edges, steps, ids, theme, rail, noMap, lanePlan]);
|
|
413
|
+
}, [check, routed, written, layout, edges, steps, ids, theme, rail, noMap, railBox, lanePlan, wide, setWide]);
|
|
337
414
|
useEffect(() => {
|
|
338
415
|
const gs = dots.current, cs = chips.current, edgeOf = Object.fromEntries(edges.map((e, i) => [ids[i], e]));
|
|
339
416
|
if (clock.current.beats !== beats) {
|
|
@@ -453,6 +530,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
453
530
|
return t && TONES[t];
|
|
454
531
|
};
|
|
455
532
|
const goTo = (step, b) => {
|
|
533
|
+
follow.current = true;
|
|
456
534
|
setPlaying(true);
|
|
457
535
|
if (step === active) {
|
|
458
536
|
clock.current.elapsed = beats.slice(0, b).reduce((t, x) => t + beatMs(x, speed), 0);
|
|
@@ -648,15 +726,9 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
648
726
|
display: 'flex',
|
|
649
727
|
flexDirection: item.direction ?? 'row',
|
|
650
728
|
gap: groupGap(item, edges),
|
|
651
|
-
alignItems: item.align
|
|
652
|
-
? item.align === 'center'
|
|
653
|
-
? 'center'
|
|
654
|
-
: 'flex-' + item.align
|
|
655
|
-
: item.direction === 'column'
|
|
656
|
-
? 'stretch'
|
|
657
|
-
: 'center',
|
|
729
|
+
alignItems: !item.align || item.align === 'center' ? 'center' : 'flex-' + item.align,
|
|
658
730
|
justifyContent: 'center',
|
|
659
|
-
}, children: item.children.map((c) => renderItem(c, depth + 1)) })] }, item.id ??
|
|
731
|
+
}, children: item.children.map((c) => renderItem(c, depth + 1)) })] }, item.id ?? `group:${nodes(item)[0]?.id}`));
|
|
660
732
|
}
|
|
661
733
|
const lit = litNodes.has(item.id);
|
|
662
734
|
const diamond = item.shape === 'decision';
|
|
@@ -664,7 +736,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
664
736
|
const card = carded.has(item.id);
|
|
665
737
|
const dl = diamond && !card ? diamondLines(item, nodeWidth(item, false) + 70) : null;
|
|
666
738
|
const w = diamond || card || item.width != null || str(item.label) ? nodeWidth(item, card, cap) + (diamond ? 70 : 0) : undefined;
|
|
667
|
-
const lines = w != null &&
|
|
739
|
+
const lines = w != null && str(item.label) ? labelLines(item, w) : null;
|
|
668
740
|
const bt = item.tone && TONES[item.tone];
|
|
669
741
|
return (_jsxs("div", { "data-fig": item.id, title: item.source, "data-diamond": diamond || undefined, onMouseEnter: () => setHover(item.id), onMouseLeave: () => setHover(null), style: {
|
|
670
742
|
position: 'relative',
|
|
@@ -711,14 +783,12 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
711
783
|
transition: 'border-color .25s',
|
|
712
784
|
} })), item.mark && (_jsx("span", { "aria-hidden": true, style: {
|
|
713
785
|
position: 'absolute',
|
|
714
|
-
|
|
715
|
-
transform: 'translateY(-50%)',
|
|
786
|
+
...markAt(markSide(item.id, item.mark, routed), item.mark === 'start' ? -18 : -20),
|
|
716
787
|
boxSizing: 'border-box',
|
|
717
788
|
borderRadius: '50%',
|
|
718
789
|
...(item.mark === 'start'
|
|
719
|
-
? {
|
|
790
|
+
? { width: 10, height: 10, background: bt ?? v('accent') }
|
|
720
791
|
: {
|
|
721
|
-
right: -20,
|
|
722
792
|
width: 14,
|
|
723
793
|
height: 14,
|
|
724
794
|
border: `1.5px solid ${bt ?? v('accent')}`,
|
|
@@ -788,8 +858,8 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
788
858
|
borderBottom: `1px solid ${v('border')}`,
|
|
789
859
|
backgroundImage: `radial-gradient(color-mix(in srgb, ${v('fg')} 16%, transparent) 1px, transparent 1.2px)`,
|
|
790
860
|
backgroundSize: '14px 14px',
|
|
791
|
-
}, children: _jsx("div", { ref: outer, style: {
|
|
792
|
-
overflow:
|
|
861
|
+
}, children: _jsx("div", { ref: outer, role: "tabpanel", id: `${tabsId}-panel`, "aria-labelledby": `${tabsId}-tab-${active ?? 0}`, style: {
|
|
862
|
+
overflow: wide || fit.scroll ? 'auto' : 'hidden',
|
|
793
863
|
height: fit.scale < 1 ? fit.height : undefined,
|
|
794
864
|
}, children: _jsxs("div", { ref: root, style: {
|
|
795
865
|
position: 'relative',
|
|
@@ -797,8 +867,8 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
797
867
|
margin: '0 auto',
|
|
798
868
|
transform: fit.scale < 1 ? `scale(${fit.scale})` : undefined,
|
|
799
869
|
transformOrigin: 'top left',
|
|
800
|
-
paddingTop: arcRoom(routed, 'above') || 4,
|
|
801
|
-
paddingBottom: arcRoom(routed, 'below') || 4,
|
|
870
|
+
paddingTop: arcRoom(routed, 'above') || (markSides.has('t') ? 20 : 4),
|
|
871
|
+
paddingBottom: arcRoom(routed, 'below') || (markSides.has('b') ? 20 : 4),
|
|
802
872
|
paddingLeft: arcRoom(routed, 'left') || 4,
|
|
803
873
|
paddingRight: arcRoom(routed, 'right') || 4,
|
|
804
874
|
}, children: [renderItem(layout, 0), _jsxs("svg", { style: {
|
|
@@ -879,52 +949,72 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
879
949
|
fontFamily: MONO,
|
|
880
950
|
transition: 'background .25s, color .25s, opacity .25s',
|
|
881
951
|
}, children: p.text }, p.key)));
|
|
882
|
-
})] }) }) }) })), rail && (
|
|
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
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
952
|
+
})] }) }) }) })), rail && (_jsx("div", { ref: railPane, onWheel: () => (user.current = true), onPointerDown: () => (user.current = true), onTouchStart: () => (user.current = true), onScroll: (e) => {
|
|
953
|
+
const x = e.currentTarget.scrollLeft;
|
|
954
|
+
if (user.current && x !== left.current && performance.now() > busy.current)
|
|
955
|
+
follow.current = false;
|
|
956
|
+
left.current = x;
|
|
957
|
+
}, style: { overflowX: zoom ? 'auto' : undefined }, children: _jsxs("svg", { viewBox: `0 0 ${rail.width} ${rail.height}`, style: {
|
|
958
|
+
display: 'block',
|
|
959
|
+
width: zoom ? rail.width * zoom : '100%',
|
|
960
|
+
maxWidth: zoom ? undefined : rail.width,
|
|
961
|
+
height: 'auto',
|
|
962
|
+
margin: `${noMap ? 0 : RAIL.gap}px auto 0`,
|
|
963
|
+
overflow: 'visible',
|
|
964
|
+
fontFamily: v('font'),
|
|
965
|
+
}, 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))), (() => {
|
|
966
|
+
const st = railState(rail, active ?? 0);
|
|
967
|
+
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}`) : []);
|
|
968
|
+
return _jsx("path", { d: lines.join(' '), stroke: v('border'), strokeDasharray: "2 3" });
|
|
969
|
+
})(), (() => {
|
|
970
|
+
const st = railState(rail, active ?? 0);
|
|
971
|
+
return rail.rows.map((row, i) => {
|
|
972
|
+
if (!st[i].shown)
|
|
973
|
+
return null;
|
|
974
|
+
const y = st[i].y;
|
|
975
|
+
if (row.kind === 'phase') {
|
|
976
|
+
const open = !rail.folds || row.step === active;
|
|
977
|
+
const playing = row.step === active ? rail.rows.find((r) => r.kind === 'message' && r.step === active && r.beat === beat) : undefined;
|
|
978
|
+
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}`));
|
|
979
|
+
}
|
|
980
|
+
const state = still
|
|
981
|
+
? 'done'
|
|
982
|
+
: active == null || row.step > active || (row.step === active && row.beat > beat)
|
|
983
|
+
? 'next'
|
|
984
|
+
: row.step === active && row.beat === beat
|
|
985
|
+
? 'now'
|
|
986
|
+
: 'done';
|
|
987
|
+
const on = state === 'now' ||
|
|
988
|
+
hoverEdge === row.edge ||
|
|
989
|
+
(hover != null && [rail.columns[row.from].id, rail.columns[row.to].id].includes(hover));
|
|
990
|
+
const [x1, x2] = [rail.columns[row.from].x, rail.columns[row.to].x];
|
|
991
|
+
const ly = y + RAIL.row / 2;
|
|
992
|
+
const tone = state === 'now' && row.tone ? TONES[row.tone] : undefined;
|
|
993
|
+
const g = row.group != null ? rail.groups[row.group] : null;
|
|
994
|
+
const tip = edgeTip(row.edge, edges[ids.indexOf(row.edge)].source, allBeats);
|
|
995
|
+
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 &&
|
|
996
|
+
(() => {
|
|
997
|
+
const tagW = ASYNC_TAG_W;
|
|
998
|
+
const tx = !row.pill ? (x1 + x2) / 2 - tagW / 2 : x2 > x1 ? row.pill.x - tagW - 4 : row.pill.x + row.pill.w + 4;
|
|
999
|
+
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" })] }));
|
|
1000
|
+
})(), 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}`));
|
|
1001
|
+
});
|
|
1002
|
+
})(), _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: () => {
|
|
1003
|
+
follow.current ||= !playing;
|
|
1004
|
+
setPlaying((p) => !p);
|
|
1005
|
+
}, 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) => {
|
|
1006
|
+
const to = { ArrowRight: (active ?? 0) + 1, ArrowLeft: (active ?? 0) - 1, Home: 0, End: steps.length - 1 }[e.key];
|
|
1007
|
+
if (to === undefined)
|
|
1008
|
+
return;
|
|
1009
|
+
e.preventDefault();
|
|
1010
|
+
const n = (to + steps.length) % steps.length;
|
|
1011
|
+
clock.current.elapsed = 0;
|
|
1012
|
+
follow.current = true;
|
|
1013
|
+
setActive(n);
|
|
1014
|
+
setPlaying(true);
|
|
1015
|
+
setBeat(0);
|
|
1016
|
+
document.getElementById(`${tabsId}-tab-${n}`)?.focus();
|
|
1017
|
+
}, style: {
|
|
928
1018
|
display: 'inline-flex',
|
|
929
1019
|
gap: 2,
|
|
930
1020
|
padding: 3,
|
|
@@ -933,8 +1023,9 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
933
1023
|
border: `1px solid ${v('border')}`,
|
|
934
1024
|
}, children: steps.map((s, i) => {
|
|
935
1025
|
const on = active === i;
|
|
936
|
-
return (_jsxs("button", { type: "button", role: "tab", "aria-selected": on, onClick: () => {
|
|
1026
|
+
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: () => {
|
|
937
1027
|
clock.current.elapsed = 0;
|
|
1028
|
+
follow.current = true;
|
|
938
1029
|
setActive(i);
|
|
939
1030
|
setPlaying(true);
|
|
940
1031
|
setBeat(0);
|
|
@@ -983,6 +1074,9 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
983
1074
|
animation: 'flowfig-in .35s ease-out',
|
|
984
1075
|
}, children: said }, `${active}-${saidAt}`))] }))] }));
|
|
985
1076
|
}
|
|
1077
|
+
const markAt = (side, off) => side === 'l' || side === 'r'
|
|
1078
|
+
? { [side === 'l' ? 'left' : 'right']: off, top: '50%', transform: 'translateY(-50%)' }
|
|
1079
|
+
: { [side === 't' ? 'top' : 'bottom']: off, left: '50%', transform: 'translateX(-50%)' };
|
|
986
1080
|
function Icon({ d, fill = false }) {
|
|
987
1081
|
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 }) }));
|
|
988
1082
|
}
|
package/dist/load.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { altText, counts, toBeat } from './model.js';
|
|
3
|
-
import { coverageLine, owners, unsureLines, verifyReport } from './verify.js';
|
|
3
|
+
import { coverageLine, detailFindings, owners, unsureLines, verifyReport } from './verify.js';
|
|
4
4
|
import { toSvg } from './svg.js';
|
|
5
5
|
const SPEC_OPEN = '<metadata id="figure-spec"><![CDATA[';
|
|
6
6
|
const SPEC_CLOSE = ']]></metadata>';
|
|
@@ -68,8 +68,9 @@ export function summaryLines(props) {
|
|
|
68
68
|
return [...edges, ...steps, `alt: ${altText(props).title}`];
|
|
69
69
|
}
|
|
70
70
|
export function verifyLines(props, figure, root = process.cwd()) {
|
|
71
|
-
if (!owners(props).some(([, source, via]) => source != null || via != null))
|
|
72
|
-
return
|
|
71
|
+
if (!owners(props).some(([, source, via]) => source != null || via != null)) {
|
|
72
|
+
return detailFindings(props, root).map((f) => `${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${figure}: ${f.message}`);
|
|
73
|
+
}
|
|
73
74
|
const { findings, coverage } = verifyReport(props, { root });
|
|
74
75
|
return [
|
|
75
76
|
...findings.map((f) => `${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${figure}: ${f.message}`),
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type FlowProps } from './model.ts';
|
|
2
|
+
export type MermaidFault = {
|
|
3
|
+
line: number;
|
|
4
|
+
reason: string;
|
|
5
|
+
};
|
|
6
|
+
type Result = {
|
|
7
|
+
spec: FlowProps;
|
|
8
|
+
faults: [];
|
|
9
|
+
} | {
|
|
10
|
+
spec?: undefined;
|
|
11
|
+
faults: MermaidFault[];
|
|
12
|
+
};
|
|
13
|
+
export declare function fromMermaid(text: string, first?: number): Result;
|
|
14
|
+
export declare function markdownBlocks(text: string): {
|
|
15
|
+
text: string;
|
|
16
|
+
line: number;
|
|
17
|
+
}[];
|
|
18
|
+
export declare function runFromMermaid(argv: string[]): number;
|
|
19
|
+
export {};
|