flowfig 0.9.0 → 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 +305 -59
- 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 +22 -5
- 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 +149 -79
- package/dist/load.js +4 -3
- package/dist/mermaid.d.ts +19 -0
- package/dist/mermaid.js +617 -0
- package/dist/model.d.ts +11 -3
- package/dist/model.js +25 -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 +66 -20
- 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, 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,10 +55,23 @@ 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 ? { ...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, }) {
|
|
58
71
|
const tl = timeline && isLanesLayout(layout);
|
|
59
72
|
const alt = useMemo(() => altText({ layout, edges, steps: stepsIn }), [layout, edges, stepsIn]);
|
|
60
73
|
const descId = useId();
|
|
74
|
+
const tabsId = useId();
|
|
61
75
|
const synthetic = tl && !stepsIn.length;
|
|
62
76
|
const steps = useMemo(() => (synthetic ? timelineBeats({ layout, edges, timeline, today }) : stepsIn), [synthetic, layout, edges, timeline, today, stepsIn]);
|
|
63
77
|
const [axisW, setAxisW] = useState(TL_AXIS_W);
|
|
@@ -82,6 +96,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
82
96
|
const firstLabel = useRef('');
|
|
83
97
|
const paths = useRef({});
|
|
84
98
|
const [routed, setRouted] = useState([]);
|
|
99
|
+
const markSides = new Set(nodes(layout).flatMap((n) => (n.mark ? [markSide(n.id, n.mark, routed)] : [])));
|
|
85
100
|
const [extraTall, setExtraTall] = useState(new Set());
|
|
86
101
|
const lanePlan = useMemo(() => lanes && !tl && isLanesLayout(layout)
|
|
87
102
|
? planLanes({ layout, edges, steps, lanes }, { floor: withRail ? (layoutRail({ layout, edges, steps, rail: true }, 560)?.width ?? 0) : 0 })
|
|
@@ -129,6 +144,11 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
129
144
|
const railOnly = withRail === 'only';
|
|
130
145
|
const rail = useMemo(() => (withRail ? layoutRail({ layout, edges, steps, rail: true }, railOnly ? 560 : mapW) : null), [withRail, railOnly, layout, edges, steps, mapW]);
|
|
131
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);
|
|
132
152
|
const jump = useRef(null);
|
|
133
153
|
const [hoverEdge, setHoverEdge] = useState(null);
|
|
134
154
|
const [still, setStill] = useState(false);
|
|
@@ -148,8 +168,14 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
148
168
|
const el = root.current, box = outer.current;
|
|
149
169
|
if (!el || !box)
|
|
150
170
|
return;
|
|
171
|
+
let last = '';
|
|
151
172
|
const measure = () => {
|
|
152
|
-
|
|
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);
|
|
153
179
|
setFit({ scale, height: el.offsetHeight * scale });
|
|
154
180
|
setMapW(el.offsetWidth);
|
|
155
181
|
if (area.current)
|
|
@@ -202,6 +228,10 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
202
228
|
const li = layout.children.findIndex((l) => isGroup(l) && (l.id === id || l.children.some((b) => b.id === id)));
|
|
203
229
|
return laneBands[`${li}@${lanePlan?.ends.get(eid)?.[j]}`];
|
|
204
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;
|
|
205
235
|
const end = (eid, id, start) => lanePlan ? laneEnd(lanePlan, eid, id, start, (lane, b) => bands[`${lane}@${b}`], rects) : id;
|
|
206
236
|
const next = route(edges.map((e, i) => ({
|
|
207
237
|
id: ids[i],
|
|
@@ -209,6 +239,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
209
239
|
to: end(ids[i], e.to, false),
|
|
210
240
|
around: e.around ?? (lanePlan?.around.has(ids[i]) ? 'below' : undefined),
|
|
211
241
|
...(tl && { sides: ['r', 'l'], elbow: true }),
|
|
242
|
+
...(rowBreaks.has(e) && { drop: true }),
|
|
212
243
|
...(stubs.has(ids[i]) && {
|
|
213
244
|
stub: stubs.get(ids[i]).map(labelPillW),
|
|
214
245
|
bands: [bandOf(ids[i], e.from, 0), bandOf(ids[i], e.to, 1)],
|
|
@@ -224,16 +255,38 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
224
255
|
};
|
|
225
256
|
measure();
|
|
226
257
|
const ro = new ResizeObserver(measure);
|
|
227
|
-
ro.observe(el);
|
|
258
|
+
ro.observe(el, { box: 'border-box' });
|
|
228
259
|
ro.observe(box);
|
|
229
260
|
el.querySelectorAll('[data-fig]').forEach((n) => ro.observe(n));
|
|
230
261
|
return () => ro.disconnect();
|
|
231
|
-
}, [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]);
|
|
232
283
|
const reported = useRef(new Set());
|
|
233
284
|
useEffect(() => {
|
|
234
285
|
const el = root.current, box = outer.current;
|
|
235
286
|
const fig = figure.current;
|
|
236
|
-
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)
|
|
237
290
|
return;
|
|
238
291
|
const base = (el ?? fig).getBoundingClientRect();
|
|
239
292
|
const k = el ? base.width / el.offsetWidth : 1;
|
|
@@ -273,11 +326,15 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
273
326
|
// An SVG <text> has no CSS box, so skip clientWidth.
|
|
274
327
|
(t instanceof SVGElement || t.clientWidth > 0) &&
|
|
275
328
|
!t.closest('[data-fig-tag],[aria-hidden]'))
|
|
276
|
-
.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
|
+
});
|
|
277
334
|
const labels = Object.fromEntries([...fig.querySelectorAll('[data-fig-label]')].map((n) => [n.dataset.figLabel, rel(n)]));
|
|
278
335
|
const lanesIn = el ? [...el.querySelectorAll('[data-fig-band]')] : [];
|
|
279
336
|
const scene = {
|
|
280
|
-
width:
|
|
337
|
+
width: (box ?? fig).clientWidth,
|
|
281
338
|
boxes,
|
|
282
339
|
...(lanePlan?.stubs.size && {
|
|
283
340
|
lanes: lanesIn.map((n) => ({
|
|
@@ -321,11 +378,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
321
378
|
],
|
|
322
379
|
minFont: Math.min(...fonts),
|
|
323
380
|
};
|
|
324
|
-
for (const f of [
|
|
325
|
-
...checkSpec({ layout, edges, steps }),
|
|
326
|
-
...checkScene(scene, { width: (box ?? fig).clientWidth }),
|
|
327
|
-
...checkTheme(theme),
|
|
328
|
-
]) {
|
|
381
|
+
for (const f of [...checkSpec(written), ...checkScene(scene, { width: (box ?? fig).clientWidth }), ...checkTheme(theme)]) {
|
|
329
382
|
const key = f.rule + ':' + f.ids.join() + ':' + f.message;
|
|
330
383
|
if (reported.current.has(key))
|
|
331
384
|
continue;
|
|
@@ -333,7 +386,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
333
386
|
// oxlint-disable-next-line no-console
|
|
334
387
|
console.warn(`flowfig check: ${f.severity} ${f.rule}: ${f.message}`);
|
|
335
388
|
}
|
|
336
|
-
}, [check, routed, layout, edges, steps, ids, theme, rail, noMap, lanePlan]);
|
|
389
|
+
}, [check, routed, written, layout, edges, steps, ids, theme, rail, noMap, lanePlan, wide, setWide]);
|
|
337
390
|
useEffect(() => {
|
|
338
391
|
const gs = dots.current, cs = chips.current, edgeOf = Object.fromEntries(edges.map((e, i) => [ids[i], e]));
|
|
339
392
|
if (clock.current.beats !== beats) {
|
|
@@ -453,6 +506,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
453
506
|
return t && TONES[t];
|
|
454
507
|
};
|
|
455
508
|
const goTo = (step, b) => {
|
|
509
|
+
follow.current = true;
|
|
456
510
|
setPlaying(true);
|
|
457
511
|
if (step === active) {
|
|
458
512
|
clock.current.elapsed = beats.slice(0, b).reduce((t, x) => t + beatMs(x, speed), 0);
|
|
@@ -648,15 +702,9 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
648
702
|
display: 'flex',
|
|
649
703
|
flexDirection: item.direction ?? 'row',
|
|
650
704
|
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',
|
|
705
|
+
alignItems: !item.align || item.align === 'center' ? 'center' : 'flex-' + item.align,
|
|
658
706
|
justifyContent: 'center',
|
|
659
|
-
}, 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}`));
|
|
660
708
|
}
|
|
661
709
|
const lit = litNodes.has(item.id);
|
|
662
710
|
const diamond = item.shape === 'decision';
|
|
@@ -711,14 +759,12 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
711
759
|
transition: 'border-color .25s',
|
|
712
760
|
} })), item.mark && (_jsx("span", { "aria-hidden": true, style: {
|
|
713
761
|
position: 'absolute',
|
|
714
|
-
|
|
715
|
-
transform: 'translateY(-50%)',
|
|
762
|
+
...markAt(markSide(item.id, item.mark, routed), item.mark === 'start' ? -18 : -20),
|
|
716
763
|
boxSizing: 'border-box',
|
|
717
764
|
borderRadius: '50%',
|
|
718
765
|
...(item.mark === 'start'
|
|
719
|
-
? {
|
|
766
|
+
? { width: 10, height: 10, background: bt ?? v('accent') }
|
|
720
767
|
: {
|
|
721
|
-
right: -20,
|
|
722
768
|
width: 14,
|
|
723
769
|
height: 14,
|
|
724
770
|
border: `1.5px solid ${bt ?? v('accent')}`,
|
|
@@ -788,8 +834,8 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
788
834
|
borderBottom: `1px solid ${v('border')}`,
|
|
789
835
|
backgroundImage: `radial-gradient(color-mix(in srgb, ${v('fg')} 16%, transparent) 1px, transparent 1.2px)`,
|
|
790
836
|
backgroundSize: '14px 14px',
|
|
791
|
-
}, children: _jsx("div", { ref: outer, style: {
|
|
792
|
-
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',
|
|
793
839
|
height: fit.scale < 1 ? fit.height : undefined,
|
|
794
840
|
}, children: _jsxs("div", { ref: root, style: {
|
|
795
841
|
position: 'relative',
|
|
@@ -797,8 +843,8 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
797
843
|
margin: '0 auto',
|
|
798
844
|
transform: fit.scale < 1 ? `scale(${fit.scale})` : undefined,
|
|
799
845
|
transformOrigin: 'top left',
|
|
800
|
-
paddingTop: arcRoom(routed, 'above') || 4,
|
|
801
|
-
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),
|
|
802
848
|
paddingLeft: arcRoom(routed, 'left') || 4,
|
|
803
849
|
paddingRight: arcRoom(routed, 'right') || 4,
|
|
804
850
|
}, children: [renderItem(layout, 0), _jsxs("svg", { style: {
|
|
@@ -879,52 +925,72 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
879
925
|
fontFamily: MONO,
|
|
880
926
|
transition: 'background .25s, color .25s, opacity .25s',
|
|
881
927
|
}, 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
|
-
|
|
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: {
|
|
928
994
|
display: 'inline-flex',
|
|
929
995
|
gap: 2,
|
|
930
996
|
padding: 3,
|
|
@@ -933,8 +999,9 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
933
999
|
border: `1px solid ${v('border')}`,
|
|
934
1000
|
}, children: steps.map((s, i) => {
|
|
935
1001
|
const on = active === i;
|
|
936
|
-
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: () => {
|
|
937
1003
|
clock.current.elapsed = 0;
|
|
1004
|
+
follow.current = true;
|
|
938
1005
|
setActive(i);
|
|
939
1006
|
setPlaying(true);
|
|
940
1007
|
setBeat(0);
|
|
@@ -983,6 +1050,9 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
983
1050
|
animation: 'flowfig-in .35s ease-out',
|
|
984
1051
|
}, children: said }, `${active}-${saidAt}`))] }))] }));
|
|
985
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%)' };
|
|
986
1056
|
function Icon({ d, fill = false }) {
|
|
987
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 }) }));
|
|
988
1058
|
}
|
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 {};
|