flowfig 0.5.0 → 0.7.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 +185 -114
- package/dist/browser.d.ts +1 -3
- package/dist/browser.js +72 -60
- package/dist/check.d.ts +5 -3
- package/dist/check.js +28 -21
- package/dist/cli.js +33 -47
- package/dist/diff.js +0 -4
- package/dist/draw.d.ts +1 -3
- package/dist/draw.js +3 -7
- package/dist/geometry.d.ts +13 -5
- package/dist/geometry.js +164 -70
- package/dist/gif.d.ts +1 -2
- package/dist/gif.js +115 -42
- package/dist/guide.d.ts +1 -4
- package/dist/guide.js +7 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +69 -105
- package/dist/init.d.ts +3 -13
- package/dist/init.js +17 -19
- package/dist/load.d.ts +1 -3
- package/dist/load.js +2 -5
- package/dist/mcp.d.ts +1 -2
- package/dist/mcp.js +9 -12
- package/dist/model.d.ts +57 -108
- package/dist/model.js +21 -88
- package/dist/open.d.ts +2 -5
- package/dist/open.js +6 -11
- package/dist/page.d.ts +0 -1
- package/dist/page.js +1 -3
- package/dist/picker.d.ts +2 -4
- package/dist/picker.js +11 -14
- package/dist/png.js +9 -10
- package/dist/rail.d.ts +1 -3
- package/dist/rail.js +2 -16
- package/dist/scene.d.ts +9 -7
- package/dist/source.d.ts +0 -1
- package/dist/source.js +2 -2
- package/dist/svg.d.ts +5 -8
- package/dist/svg.js +81 -121
- package/dist/text.d.ts +0 -2
- package/dist/text.js +1 -4
- package/dist/verify.d.ts +1 -1
- package/dist/verify.js +3 -6
- package/dist/version.d.ts +1 -2
- package/dist/version.js +2 -2
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -5,14 +5,13 @@ import { 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 { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD, CARD_WIDTH, EDGE_OFF, EDGE_ON, LIGHT, TONES, ON_ACCENT, toneFill, toneTint, decisions, edgeId, groupGap, isGroup, isRows, lanePlan as planLanes, laneEnd, laneBlock, STUB_ROOM, labelPillW, str, 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, beatMs, STEP_HOLD_MS, playheadItem, dateLabelRaised, } from './model.js';
|
|
8
|
+
import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD, CARD_WIDTH, EDGE_OFF, EDGE_ON, LIGHT, TONES, ON_ACCENT, toneFill, toneTint, decisions, edgeId, groupGap, isGroup, isRows, lanePlan as planLanes, laneEnd, laneBlock, STUB_ROOM, labelPillW, str, 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, beatMs, STEP_HOLD_MS, playheadItem, loopStartItem, dateLabelRaised, } from './model.js';
|
|
9
9
|
export { LIGHT, DARK } from './model.js';
|
|
10
10
|
const DEFAULTS = { ...LIGHT, font: 'inherit' };
|
|
11
11
|
const v = (k) => `var(--fig-${k}, ${DEFAULTS[k]})`;
|
|
12
12
|
const MONO = 'var(--ifm-font-family-monospace, ui-monospace, SFMono-Regular, Menlo, monospace)';
|
|
13
|
-
const NONE = [];
|
|
14
|
-
const ACTIVE = 'flowfig-active';
|
|
15
|
-
// The active box reads --ff-hop (the tone of the hop that arrived), then --ff-box (the tone of the box), then the accent.
|
|
13
|
+
const NONE = [];
|
|
14
|
+
const ACTIVE = 'flowfig-active';
|
|
16
15
|
const hue = `var(--ff-hop, var(--ff-box, ${v('accent')}))`;
|
|
17
16
|
const glow = `0 0 0 3px color-mix(in srgb, ${hue} 18%, transparent)`;
|
|
18
17
|
const cardBody = (c) => {
|
|
@@ -40,20 +39,17 @@ const cardBody = (c) => {
|
|
|
40
39
|
fontFamily: r.mono ? MONO : undefined,
|
|
41
40
|
fontSize: r.mono ? 10.5 : undefined,
|
|
42
41
|
}, children: [r.text, r.meta != null && _jsxs("span", { style: { color: v('muted') }, children: [" \u00B7 ", r.meta] })] }));
|
|
43
|
-
// A word-sized tag heads its row so the text keeps the full width; a number or no tag sits inline.
|
|
44
42
|
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));
|
|
45
43
|
}) }));
|
|
46
44
|
};
|
|
47
45
|
/**
|
|
48
|
-
* The interactive player for one figure
|
|
49
|
-
*
|
|
46
|
+
* The interactive player for one figure. Give it a `FlowProps` spec.
|
|
47
|
+
* Use `toSvg` from `flowfig/svg` for a static animated SVG.
|
|
50
48
|
*/
|
|
51
49
|
export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, autoplay = true, check = false, rail: withRail = false, lanes, timeline, today, }) {
|
|
52
50
|
const tl = timeline && isLanesLayout(layout);
|
|
53
|
-
// A timeline with no steps of its own walks its dated items in date order, with no tab row.
|
|
54
51
|
const synthetic = tl && !stepsIn.length;
|
|
55
52
|
const steps = useMemo(() => (synthetic ? timelineBeats({ layout, edges, timeline, today }) : stepsIn), [synthetic, layout, edges, timeline, today, stepsIn]);
|
|
56
|
-
// The width of the timeline area in px: the server draws TL_AXIS_W, and the browser measures the real width after mount.
|
|
57
53
|
const [axisW, setAxisW] = useState(TL_AXIS_W);
|
|
58
54
|
const area = useRef(null);
|
|
59
55
|
const root = useRef(null);
|
|
@@ -69,22 +65,19 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
69
65
|
}, [full]);
|
|
70
66
|
const dots = useRef([]);
|
|
71
67
|
const chips = useRef([]);
|
|
72
|
-
const bar = useRef(null);
|
|
68
|
+
const bar = useRef(null);
|
|
73
69
|
const [beat, setBeat] = useState(0);
|
|
74
|
-
const [holding, setHolding] = useState(false);
|
|
70
|
+
const [holding, setHolding] = useState(false);
|
|
71
|
+
const [returning, setReturning] = useState(false);
|
|
72
|
+
const firstLabel = useRef('');
|
|
75
73
|
const paths = useRef({});
|
|
76
74
|
const [routed, setRouted] = useState([]);
|
|
77
|
-
// Lane copies that grow because a stub there had no place clear of every edge path (see `tightCopies`).
|
|
78
75
|
const [extraTall, setExtraTall] = useState(new Set());
|
|
79
|
-
// Lanes: the shared wrap plan (blocks, gaps, stubs), the same as the SVG at the default 830 px width.
|
|
80
76
|
const lanePlan = useMemo(() => lanes && !tl && isLanesLayout(layout)
|
|
81
|
-
? planLanes({ layout, edges, steps, lanes },
|
|
82
|
-
// A rail sets a least width, as in the SVG.
|
|
83
|
-
{ floor: withRail ? (layoutRail({ layout, edges, steps, rail: true }, 560)?.width ?? 0) : 0 })
|
|
77
|
+
? planLanes({ layout, edges, steps, lanes }, { floor: withRail ? (layoutRail({ layout, edges, steps, rail: true }, 560)?.width ?? 0) : 0 })
|
|
84
78
|
: null, [lanes, tl, layout, edges, steps, withRail]);
|
|
85
79
|
const [active, setActive] = useState(steps.length ? 0 : null);
|
|
86
80
|
const [playing, setPlaying] = useState(autoplay);
|
|
87
|
-
// The step's clock lives outside React: pausing freezes it, resizing keeps it, only a new step resets it.
|
|
88
81
|
const playingRef = useRef(playing);
|
|
89
82
|
playingRef.current = playing;
|
|
90
83
|
const [rate, setRate] = useState(1);
|
|
@@ -93,7 +86,6 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
93
86
|
const clock = useRef({ beats: null, elapsed: 0 });
|
|
94
87
|
const [hover, setHover] = useState(null);
|
|
95
88
|
const ids = useMemo(() => edges.map(edgeId), [edges]);
|
|
96
|
-
// A milestone is a diamond too, so its edges meet the tips.
|
|
97
89
|
const tips = useMemo(() => new Set([
|
|
98
90
|
...decisions(layout),
|
|
99
91
|
...(tl
|
|
@@ -104,7 +96,6 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
104
96
|
]), [layout, tl, edges, timeline, today]);
|
|
105
97
|
const step = active == null ? null : steps[active];
|
|
106
98
|
const beats = useMemo(() => (step?.flow ?? []).map(toBeat), [step]);
|
|
107
|
-
// Every content each box's card will ever show. The card is sized to the largest, so text never gets cut and nothing jumps.
|
|
108
99
|
const carded = useMemo(() => {
|
|
109
100
|
const all = new Map();
|
|
110
101
|
for (const b of steps.flatMap((s) => s.flow.map(toBeat)))
|
|
@@ -113,30 +104,23 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
113
104
|
return all;
|
|
114
105
|
}, [steps]);
|
|
115
106
|
const cur = beats[beat];
|
|
116
|
-
// The narration: the latest `say` so far (a beat without one keeps the line before), else the step caption.
|
|
117
107
|
const saidAt = beats.slice(0, beat + 1).findLastIndex((b) => b.say != null);
|
|
118
108
|
const said = saidAt === -1 ? step?.caption : beats[saidAt].say;
|
|
119
|
-
// Content cards: everything the step has shown so far, latest wins.
|
|
120
109
|
const shown = useMemo(() => Object.assign({}, ...beats.slice(0, beat + 1).map((b) => b.show)), [beats, beat]);
|
|
121
|
-
// The beat that last changed each card, so only a card whose content just changed animates in.
|
|
122
110
|
const shownAt = useMemo(() => {
|
|
123
111
|
const at = {};
|
|
124
112
|
beats.slice(0, beat + 1).forEach((b, i) => Object.keys(b.show ?? {}).forEach((k) => (at[k] = i)));
|
|
125
113
|
return at;
|
|
126
114
|
}, [beats, beat]);
|
|
127
|
-
// The rail lays out in node terms, like the SVG, so both renderers draw the same numbers. Its width follows the measured map.
|
|
128
115
|
const figure = useRef(null);
|
|
129
116
|
const [mapW, setMapW] = useState(0);
|
|
130
117
|
const railOnly = withRail === 'only';
|
|
131
118
|
const rail = useMemo(() => (withRail ? layoutRail({ layout, edges, steps, rail: true }, railOnly ? 560 : mapW) : null), [withRail, railOnly, layout, edges, steps, mapW]);
|
|
132
|
-
// 'only' drops the map, but only when the rail has a hop to draw; a figure never renders empty.
|
|
133
119
|
const noMap = railOnly && rail != null;
|
|
134
|
-
const jump = useRef(null);
|
|
120
|
+
const jump = useRef(null);
|
|
135
121
|
const [hoverEdge, setHoverEdge] = useState(null);
|
|
136
|
-
// With reduced motion the player shows the last beat, so the rail marks every row as done.
|
|
137
122
|
const [still, setStill] = useState(false);
|
|
138
123
|
useEffect(() => setStill(window.matchMedia('(prefers-reduced-motion: reduce)').matches), []);
|
|
139
|
-
// Boxes with many edges on one side get taller so the edges and their labels have room.
|
|
140
124
|
const minHeight = useMemo(() => {
|
|
141
125
|
const out = {}, inn = {};
|
|
142
126
|
for (const e of edges) {
|
|
@@ -148,20 +132,19 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
148
132
|
return n > 2 ? n * 30 : undefined;
|
|
149
133
|
};
|
|
150
134
|
}, [edges]);
|
|
151
|
-
// Measure every box and redraw the edges whenever anything changes size.
|
|
152
135
|
useEffect(() => {
|
|
153
136
|
const el = root.current, box = outer.current;
|
|
154
137
|
if (!el || !box)
|
|
155
|
-
return;
|
|
138
|
+
return;
|
|
156
139
|
const measure = () => {
|
|
157
|
-
// Too wide for its container? Shrink it so the whole figure stays in view. Below half size, scroll instead.
|
|
158
140
|
const scale = Math.max(0.5, Math.min(1, box.clientWidth / el.offsetWidth));
|
|
159
141
|
setFit({ scale, height: el.offsetHeight * scale });
|
|
160
142
|
setMapW(el.offsetWidth);
|
|
161
143
|
if (area.current)
|
|
162
144
|
setAxisW(area.current.offsetWidth);
|
|
163
145
|
const base = el.getBoundingClientRect();
|
|
164
|
-
|
|
146
|
+
// Rects are measured unscaled: k is the scale on screen.
|
|
147
|
+
const k = base.width / el.offsetWidth;
|
|
165
148
|
const rects = {};
|
|
166
149
|
el.querySelectorAll('[data-fig]').forEach((n) => {
|
|
167
150
|
const r = n.getBoundingClientRect();
|
|
@@ -177,7 +160,10 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
177
160
|
const r = n.getBoundingClientRect();
|
|
178
161
|
avoid.push({ x: (r.left - base.left) / k, y: (r.top - base.top) / k, w: r.width / k, h: r.height / k });
|
|
179
162
|
});
|
|
180
|
-
|
|
163
|
+
if (tl)
|
|
164
|
+
for (const n of nodes(layout))
|
|
165
|
+
if (rects[n.id])
|
|
166
|
+
avoid.push({ ...rects[n.id], box: true });
|
|
181
167
|
const stubs = lanePlan?.stubs ?? new Map();
|
|
182
168
|
if (stubs.size) {
|
|
183
169
|
for (const n of nodes(layout))
|
|
@@ -188,13 +174,11 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
188
174
|
avoid.push({ x: (r.left - base.left) / k, y: (r.top - base.top) / k, w: r.width / k, h: r.height / k });
|
|
189
175
|
});
|
|
190
176
|
}
|
|
191
|
-
// A wrapped lane end routes to the band of its block (`data-fig-copy`), as in the SVG.
|
|
192
177
|
const bands = {};
|
|
193
178
|
el.querySelectorAll('[data-fig-copy]').forEach((n) => {
|
|
194
179
|
const r = n.getBoundingClientRect();
|
|
195
180
|
bands[n.dataset.figCopy] = { x: (r.left - base.left) / k, y: (r.top - base.top) / k, w: r.width / k, h: r.height / k };
|
|
196
181
|
});
|
|
197
|
-
// A stub pill stays in the lanes, right of the gutter, as in the SVG.
|
|
198
182
|
const blockRects = [...el.querySelectorAll('[data-fig-block]')].map((n) => n.getBoundingClientRect());
|
|
199
183
|
const pillArea = stubs.size && blockRects.length
|
|
200
184
|
? (() => {
|
|
@@ -204,7 +188,6 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
204
188
|
return { x, y: (y0 - base.top) / k, w: (x1 - base.left) / k - x, h: (y1 - y0) / k };
|
|
205
189
|
})()
|
|
206
190
|
: undefined;
|
|
207
|
-
// The band of an edge end, in the block of that end, as in the SVG.
|
|
208
191
|
const laneBands = {};
|
|
209
192
|
el.querySelectorAll('[data-fig-band]').forEach((n) => {
|
|
210
193
|
const r = n.getBoundingClientRect();
|
|
@@ -225,8 +208,8 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
225
208
|
stub: stubs.get(ids[i]).map(labelPillW),
|
|
226
209
|
bands: [bandOf(ids[i], e.from, 0), bandOf(ids[i], e.to, 1)],
|
|
227
210
|
}),
|
|
228
|
-
...(stubs.size && e.label != null && { labelW: labelPillW(str(e.label)) }),
|
|
229
|
-
})), rects, tips, avoid, pillArea);
|
|
211
|
+
...((stubs.size || lanePlan) && e.label != null && { labelW: labelPillW(str(e.label)) }),
|
|
212
|
+
})), rects, tips, avoid, pillArea, lanePlan ? { bands: Object.values(laneBands), boxes: nodes(layout).flatMap((n) => (rects[n.id] ? [rects[n.id]] : [])) } : undefined);
|
|
230
213
|
setRouted(next);
|
|
231
214
|
if (lanePlan) {
|
|
232
215
|
const more = [...tightCopies(layout.children, lanePlan.ends, edges, next)].filter((k) => !lanePlan.tall.has(k) && !extraTall.has(k));
|
|
@@ -241,12 +224,10 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
241
224
|
el.querySelectorAll('[data-fig]').forEach((n) => ro.observe(n));
|
|
242
225
|
return () => ro.disconnect();
|
|
243
226
|
}, [edges, ids, layout, tips, noMap, lanes, tl, axisW, lanePlan, extraTall]);
|
|
244
|
-
// The same rules as `flowfig check`, on what the browser actually drew: real fonts, real wrapping. Each fault prints once.
|
|
245
227
|
const reported = useRef(new Set());
|
|
246
228
|
useEffect(() => {
|
|
247
229
|
const el = root.current, box = outer.current;
|
|
248
230
|
const fig = figure.current;
|
|
249
|
-
// With rail: 'only' there is no map: the figure is the frame and the scene holds only the rail.
|
|
250
231
|
if (!check || !fig || (!noMap && (!el || !box)))
|
|
251
232
|
return;
|
|
252
233
|
const base = (el ?? fig).getBoundingClientRect();
|
|
@@ -269,7 +250,6 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
269
250
|
const fontSize = parseFloat(getComputedStyle(t).fontSize);
|
|
270
251
|
if (!n.dataset.diamond)
|
|
271
252
|
return { text: t.textContent, fontSize, room: t.clientWidth, need: t.scrollWidth };
|
|
272
|
-
// A diamond gives a line the width of its outline at the far edge of the line, and the line is as wide as its text.
|
|
273
253
|
const [r, range] = [rel(t), document.createRange()];
|
|
274
254
|
range.selectNodeContents(t);
|
|
275
255
|
const far = Math.max(Math.abs(r.y - rect.y - rect.h / 2), Math.abs(r.y + r.h - rect.y - rect.h / 2));
|
|
@@ -282,11 +262,10 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
282
262
|
}),
|
|
283
263
|
};
|
|
284
264
|
});
|
|
285
|
-
// Every visible leaf text counts, as in the SVG scene; the 9 px tag pills do not.
|
|
286
|
-
// The rail is a sibling of the map, so the rail queries start at the figure. An SVG <text> has no CSS box, so it skips clientWidth.
|
|
287
265
|
const fonts = [...fig.querySelectorAll('*')]
|
|
288
266
|
.filter((t) => !t.children.length &&
|
|
289
267
|
t.textContent?.trim() &&
|
|
268
|
+
// An SVG <text> has no CSS box, so skip clientWidth.
|
|
290
269
|
(t instanceof SVGElement || t.clientWidth > 0) &&
|
|
291
270
|
!t.closest('[data-fig-tag],[aria-hidden]'))
|
|
292
271
|
.map((t) => parseFloat(getComputedStyle(t).fontSize));
|
|
@@ -304,12 +283,10 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
304
283
|
edges: [
|
|
305
284
|
...routed.flatMap((r) => {
|
|
306
285
|
const e = edges[ids.indexOf(r.id)];
|
|
307
|
-
// A cross-block edge is two scene edges: each stub with its pill.
|
|
308
286
|
if (r.stub)
|
|
309
287
|
return r.stub.pts.map((pts, j) => ({ id: r.id, from: e.from, to: e.to, curve: r.curve, pts, label: labels[`${r.id}:${j}`] }));
|
|
310
288
|
return [{ id: r.id, from: e.from, to: e.to, curve: r.curve, label: labels[r.id], ...(tl && { behind: true }) }];
|
|
311
289
|
}),
|
|
312
|
-
// A rail pill is a label for label-overlap; its curve is a point at the pill center, so it can cross no box.
|
|
313
290
|
...(rail?.rows ?? []).flatMap((row) => {
|
|
314
291
|
const r = row.kind === 'message' ? labels[`rail:${row.n}`] : undefined;
|
|
315
292
|
if (row.kind !== 'message' || !r)
|
|
@@ -338,14 +315,13 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
338
315
|
if (reported.current.has(key))
|
|
339
316
|
continue;
|
|
340
317
|
reported.current.add(key);
|
|
318
|
+
// oxlint-disable-next-line no-console
|
|
341
319
|
console.warn(`flowfig check: ${f.severity} ${f.rule}: ${f.message}`);
|
|
342
320
|
}
|
|
343
321
|
}, [check, routed, layout, edges, steps, ids, theme, rail, noMap, lanePlan]);
|
|
344
|
-
// Play the step's beats: each moves its packets (and their data cards) along its edges, then the next step starts.
|
|
345
322
|
useEffect(() => {
|
|
346
323
|
const gs = dots.current, cs = chips.current, edgeOf = Object.fromEntries(edges.map((e, i) => [ids[i], e]));
|
|
347
324
|
if (clock.current.beats !== beats) {
|
|
348
|
-
// A rail click can open another step at a later beat; start the clock at that beat, not at 0.
|
|
349
325
|
const j = jump.current ?? 0;
|
|
350
326
|
jump.current = null;
|
|
351
327
|
clock.current = { beats, elapsed: beats.slice(0, j).reduce((t, b) => t + beatMs(b, speed), 0) };
|
|
@@ -359,7 +335,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
359
335
|
return void setBeat(beats.length - 1);
|
|
360
336
|
}
|
|
361
337
|
const ends = beats.reduce((acc, b) => [...acc, (acc.at(-1) ?? 0) + beatMs(b, speed)], []);
|
|
362
|
-
const total = ends.at(-1) + STEP_HOLD_MS;
|
|
338
|
+
const total = ends.at(-1) + STEP_HOLD_MS;
|
|
363
339
|
let raf = 0, last = performance.now(), shownBeat = -1;
|
|
364
340
|
const tick = (now) => {
|
|
365
341
|
if (playingRef.current)
|
|
@@ -378,8 +354,9 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
378
354
|
if (i !== shownBeat)
|
|
379
355
|
setBeat((shownBeat = i));
|
|
380
356
|
setHolding(t >= ends.at(-1));
|
|
357
|
+
setReturning(active === steps.length - 1 && t >= total - 400 * BASE_RATE);
|
|
381
358
|
const start = i ? ends[i - 1] : 0;
|
|
382
|
-
const f = Math.min(1, (t - start) / speed);
|
|
359
|
+
const f = Math.min(1, (t - start) / speed);
|
|
383
360
|
const eased = f < 0.5 ? 2 * f * f : 1 - (-2 * f + 2) ** 2 / 2;
|
|
384
361
|
const hops = t < ends.at(-1) ? beats[i].hops : [];
|
|
385
362
|
gs.forEach((g, j) => {
|
|
@@ -405,14 +382,12 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
405
382
|
c.style.opacity = hops[j].data == null ? '0' : '1';
|
|
406
383
|
}
|
|
407
384
|
});
|
|
408
|
-
// A box is active from the packet arrival to the end of the step hold. The tick toggles the class and skips React.
|
|
409
385
|
const arrived = new Map();
|
|
410
386
|
if (f >= 1)
|
|
411
387
|
for (const h of beats[i].hops) {
|
|
412
388
|
const to = h.back ? edgeOf[h.edge]?.from : edgeOf[h.edge]?.to;
|
|
413
389
|
arrived.set(to, arrived.get(to) ?? (h.tone && TONES[h.tone]));
|
|
414
390
|
}
|
|
415
|
-
// A focused box turns active at the beat start; in a timeline, when the today line arrives.
|
|
416
391
|
if (t - start >= (tl ? 400 * BASE_RATE : 0))
|
|
417
392
|
for (const id of beats[i].focus ?? [])
|
|
418
393
|
if (!arrived.has(id))
|
|
@@ -429,8 +404,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
429
404
|
if (on === n.classList.contains(ACTIVE))
|
|
430
405
|
return;
|
|
431
406
|
if (!on)
|
|
432
|
-
return void n.classList.remove(ACTIVE);
|
|
433
|
-
// The box turns active at once: no transition while the class arrives.
|
|
407
|
+
return void n.classList.remove(ACTIVE);
|
|
434
408
|
const keep = n.style.transition;
|
|
435
409
|
n.style.transition = 'none';
|
|
436
410
|
n.classList.add(ACTIVE);
|
|
@@ -447,8 +421,6 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
447
421
|
cs.forEach((c) => c && (c.style.opacity = '0'));
|
|
448
422
|
};
|
|
449
423
|
}, [beats, routed, noMap, speed, steps.length]);
|
|
450
|
-
// What to light up: the step's edges and their ends, or whatever touches the hovered box.
|
|
451
|
-
// A step lights the trail so far: every edge its beats have crossed, and every box it has filled.
|
|
452
424
|
const trail = beats.slice(0, beat + 1).flatMap((b) => b.hops.map((h) => h.edge));
|
|
453
425
|
const hoverEdgeIdx = hoverEdge == null ? -1 : ids.indexOf(hoverEdge);
|
|
454
426
|
const litEdges = new Set(hoverEdge ? [hoverEdge] : hover ? ids.filter((id, i) => edges[i].from === hover || edges[i].to === hover) : trail);
|
|
@@ -461,7 +433,6 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
461
433
|
if (litEdges.has(ids[i]))
|
|
462
434
|
litNodes.add(e.from).add(e.to);
|
|
463
435
|
});
|
|
464
|
-
// A toned hop colors its edge for its own beat only; the trail after it is the accent.
|
|
465
436
|
const hopTone = (edge) => {
|
|
466
437
|
const t = cur?.hops.find((h) => h.edge === edge && h.tone)?.tone;
|
|
467
438
|
return t && TONES[t];
|
|
@@ -480,7 +451,6 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
480
451
|
const focus = hover != null || hoverEdge != null || step != null;
|
|
481
452
|
const vars = Object.fromEntries(Object.entries(theme ?? {}).map(([k, val]) => [`--fig-${k}`, val]));
|
|
482
453
|
const timelineFig = useMemo(() => (tl ? timelineLayout({ layout, edges, timeline, today }, axisW) : null), [tl, layout, edges, timeline, today, axisW]);
|
|
483
|
-
// A bar or a milestone of a timeline: placed by the shared layout, with the look of a box.
|
|
484
454
|
const renderBar = (n, it) => {
|
|
485
455
|
const lit = litNodes.has(n.id);
|
|
486
456
|
const bt = n.tone && TONES[n.tone];
|
|
@@ -514,20 +484,56 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
514
484
|
const renderItem = (item, depth) => {
|
|
515
485
|
if (timelineFig && item === layout) {
|
|
516
486
|
const lanesList = layout.children;
|
|
517
|
-
|
|
518
|
-
|
|
487
|
+
const focused = still
|
|
488
|
+
? undefined
|
|
489
|
+
: returning
|
|
490
|
+
? loopStartItem(timelineFig.items, steps)
|
|
491
|
+
: holding
|
|
492
|
+
? undefined
|
|
493
|
+
: playheadItem(timelineFig.items, beats, beat);
|
|
519
494
|
const home = timelineFig.last ?? 0;
|
|
520
495
|
const at = focused ? focused.x + (focused.milestone ? focused.w / 2 : 0) : home;
|
|
521
496
|
const dateText = focused ? focused.date : timelineFig.lastDate;
|
|
522
497
|
const rowsPx = timelineFig.rows.map((r) => r * TL_BAR_H + (r - 1) * TL_ROW_GAP + LANE_PAD * 2);
|
|
498
|
+
if (firstLabel.current === '')
|
|
499
|
+
firstLabel.current = dateText;
|
|
500
|
+
else if (firstLabel.current !== dateText)
|
|
501
|
+
firstLabel.current = null;
|
|
502
|
+
const marks = (front) => timelineFig.last != null && (_jsxs("div", { style: { gridColumn: '2 / 4', gridRow: '1 / -1', position: 'relative', zIndex: front ? 2 : 1, pointerEvents: 'none' }, children: [timelineFig.today != null && (_jsx("div", { "data-fig-today": front ? undefined : '', style: { position: 'absolute', left: timelineFig.today, top: 12, bottom: 0, width: 0 }, children: front ? (_jsx("span", { style: {
|
|
503
|
+
position: 'absolute',
|
|
504
|
+
right: 3,
|
|
505
|
+
top: -1,
|
|
506
|
+
fontSize: 11,
|
|
507
|
+
lineHeight: '11px',
|
|
508
|
+
fontWeight: 600,
|
|
509
|
+
color: v('accent'),
|
|
510
|
+
}, children: "today" })) : (_jsx("div", { style: { position: 'absolute', top: 0, bottom: 0, borderLeft: `1px dashed ${v('accent')}`, opacity: 0.6 } })) })), _jsx("div", { "data-fig-playhead": front ? undefined : '', style: {
|
|
511
|
+
position: 'absolute',
|
|
512
|
+
left: 0,
|
|
513
|
+
top: 12,
|
|
514
|
+
bottom: 0,
|
|
515
|
+
width: front ? 0 : 1.5,
|
|
516
|
+
background: front ? undefined : v('accent'),
|
|
517
|
+
transform: `translateX(${at}px)`,
|
|
518
|
+
transition: still ? 'none' : 'transform .4s linear',
|
|
519
|
+
}, children: front && (_jsx("span", { style: {
|
|
520
|
+
position: 'absolute',
|
|
521
|
+
left: 3,
|
|
522
|
+
top: dateLabelRaised(at, timelineFig.today, textWidth(dateText, 11), textWidth('today', 11)) ? -13 : -1,
|
|
523
|
+
fontSize: 11,
|
|
524
|
+
lineHeight: '11px',
|
|
525
|
+
fontWeight: 600,
|
|
526
|
+
whiteSpace: 'nowrap',
|
|
527
|
+
color: v('accent'),
|
|
528
|
+
animation: still || firstLabel.current === dateText ? undefined : 'flowfig-label .4s step-end',
|
|
529
|
+
}, children: dateText }, dateText)) })] }));
|
|
523
530
|
return (_jsxs("div", { style: {
|
|
524
531
|
display: 'grid',
|
|
525
532
|
gridTemplateColumns: `max-content minmax(${TL_AXIS_W}px, 1fr) 18px`,
|
|
526
|
-
// The axis strip sits over the gap above the first band, as in the SVG.
|
|
527
533
|
gridTemplateRows: `${TL_AXIS_H - LANE_ROW_GAP}px ${rowsPx.map((h) => h + 'px').join(' ')}`,
|
|
528
534
|
rowGap: LANE_ROW_GAP,
|
|
529
535
|
position: 'relative',
|
|
530
|
-
}, children: [_jsxs("div", { "data-fig-axis": "", style: { gridColumn: '2 / 4', gridRow: 1, position: 'relative', height: TL_AXIS_H, alignSelf: 'start' }, children: [_jsx("div", { style: { position: 'absolute', left: 0, right: 0, top: TL_AXIS_H - 1, height: 1, background: v('border') } }), timelineFig.ticks.map((k) => (_jsxs(Fragment, { children: [_jsx("div", { style: { position: 'absolute', left: k.x, top: TL_AXIS_H - 5, width: 1, height: 4, background: v('border') } }), _jsx("div", { style: { position: 'absolute', left: k.x + 3, top: 0, fontSize: 11, lineHeight: '13px', color: v('muted') }, children: k.label })] }, k.x)))] }), lanesList.map((lane, li) => {
|
|
536
|
+
}, children: [marks(false), _jsxs("div", { "data-fig-axis": "", style: { gridColumn: '2 / 4', gridRow: 1, position: 'relative', height: TL_AXIS_H, alignSelf: 'start' }, children: [_jsx("div", { style: { position: 'absolute', left: 0, right: 0, top: TL_AXIS_H - 1, height: 1, background: v('border') } }), timelineFig.ticks.map((k) => (_jsxs(Fragment, { children: [_jsx("div", { style: { position: 'absolute', left: k.x, top: TL_AXIS_H - 5, width: 1, height: 4, background: v('border') } }), _jsx("div", { style: { position: 'absolute', left: k.x + 3, top: 0, fontSize: 11, lineHeight: '13px', color: v('muted') }, children: k.label })] }, k.x)))] }), lanesList.map((lane, li) => {
|
|
531
537
|
const lit = lane.id != null && litNodes.has(lane.id);
|
|
532
538
|
return (_jsxs(Fragment, { children: [_jsx("div", { "data-fig": lane.id, "data-fig-lane": "", style: {
|
|
533
539
|
gridColumn: '1 / -1',
|
|
@@ -552,50 +558,18 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
552
558
|
}, children: lane.label }), _jsx("div", { ref: li === 0 ? area : undefined, style: { gridColumn: 2, gridRow: li + 2, position: 'relative', zIndex: 1 }, children: timelineFig.items
|
|
553
559
|
.filter((it) => it.track === li)
|
|
554
560
|
.map((it) => renderBar(lane.children.find((k) => k.id === it.id), it)) })] }, lane.id ?? String(lane.label)));
|
|
555
|
-
}),
|
|
556
|
-
position: 'absolute',
|
|
557
|
-
right: 3,
|
|
558
|
-
top: -1,
|
|
559
|
-
fontSize: 11,
|
|
560
|
-
lineHeight: '11px',
|
|
561
|
-
fontWeight: 600,
|
|
562
|
-
color: v('accent'),
|
|
563
|
-
}, children: "today" })] })), _jsx("div", { "data-fig-playhead": "", style: {
|
|
564
|
-
position: 'absolute',
|
|
565
|
-
left: 0,
|
|
566
|
-
top: 12,
|
|
567
|
-
bottom: 0,
|
|
568
|
-
width: 1.5,
|
|
569
|
-
background: v('accent'),
|
|
570
|
-
transform: `translateX(${at}px)`,
|
|
571
|
-
transition: still ? 'none' : 'transform .4s linear',
|
|
572
|
-
}, children: _jsx("span", { style: {
|
|
573
|
-
position: 'absolute',
|
|
574
|
-
left: 3,
|
|
575
|
-
top: dateLabelRaised(at, timelineFig.today, textWidth(dateText, 11), textWidth('today', 11)) ? -13 : -1,
|
|
576
|
-
fontSize: 11,
|
|
577
|
-
lineHeight: '11px',
|
|
578
|
-
fontWeight: 600,
|
|
579
|
-
whiteSpace: 'nowrap',
|
|
580
|
-
color: v('accent'),
|
|
581
|
-
animation: still ? undefined : 'flowfig-label .4s step-end',
|
|
582
|
-
}, children: dateText }, dateText) })] }))] }));
|
|
561
|
+
}), marks(true)] }));
|
|
583
562
|
}
|
|
584
563
|
if (lanes && item === layout && isLanesLayout(layout)) {
|
|
585
564
|
const { cols, starts, gaps, lead, blocks, gutter, tall } = lanePlan;
|
|
586
|
-
const n = Math.max(0, ...cols.values()) + 1;
|
|
587
565
|
const count = blocks.length;
|
|
588
|
-
// One grid per block of `per` time columns, as in the SVG. A wrapped block shows only the lanes with a box in it.
|
|
589
566
|
const grid = (bk) => {
|
|
590
567
|
const m = starts[bk + 1] - starts[bk];
|
|
591
568
|
const inBlock = (b) => laneBlock(starts, cols.get(b.id)) === bk;
|
|
592
569
|
const shown = layout.children.filter((lane) => count < 2 || lane.children.some(inBlock));
|
|
593
570
|
return (_jsx("div", { "data-fig-block": bk, style: {
|
|
594
571
|
display: 'grid',
|
|
595
|
-
// A wrapped block stretches to the widest block, so all bands share one width.
|
|
596
|
-
// Every block has the gutter of the plan, so the labels and the columns line up as in the SVG.
|
|
597
572
|
gridTemplateColumns: `${gutter}px repeat(${m}, max-content)${count > 1 ? ' 1fr' : ''}`,
|
|
598
|
-
// An empty lane keeps the SVG's least band height: a 38 px label line plus the lane padding.
|
|
599
573
|
gridAutoRows: `minmax(${38 + LANE_PAD * 2}px, auto)`,
|
|
600
574
|
columnGap: 0,
|
|
601
575
|
rowGap: LANE_ROW_GAP,
|
|
@@ -628,7 +602,6 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
628
602
|
gridRow: li + 1,
|
|
629
603
|
alignSelf: 'center',
|
|
630
604
|
zIndex: 1,
|
|
631
|
-
// The space after each column and before a block's first column comes from the plan, as in the SVG.
|
|
632
605
|
padding: `${LANE_PAD}px ${gaps[cols.get(b.id)]}px ${LANE_PAD + (tall.has(`${layout.children.indexOf(lane)}@${bk}`) || extraTall.has(`${layout.children.indexOf(lane)}@${bk}`) ? STUB_ROOM : 0)}px ${cols.get(b.id) === starts[bk] ? lead[bk] : 0}px`,
|
|
633
606
|
}, children: renderItem(b, 1) }, b.id)))] }, lane.id ?? String(lane.label)));
|
|
634
607
|
}) }, bk));
|
|
@@ -674,7 +647,6 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
674
647
|
const diamond = item.shape === 'decision';
|
|
675
648
|
const store = item.shape === 'store';
|
|
676
649
|
const card = carded.has(item.id);
|
|
677
|
-
// The sub of a diamond wraps as in the SVG, one div per line, so a line stays inside the outline.
|
|
678
650
|
const dl = diamond && !card ? diamondLines(item, nodeWidth(item, false) + 70) : null;
|
|
679
651
|
const bt = item.tone && TONES[item.tone];
|
|
680
652
|
return (_jsxs("div", { "data-fig": item.id, "data-diamond": diamond || undefined, onMouseEnter: () => setHover(item.id), onMouseLeave: () => setHover(null), style: {
|
|
@@ -790,7 +762,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
790
762
|
width: 28,
|
|
791
763
|
height: 28,
|
|
792
764
|
}, children: _jsx(Icon, { d: full ? 'M4 4l8 8M12 4l-8 8' : 'M9 3h4v4M7 13H3V9M13 3L9 7M3 13l4-4' }) }), _jsx("style", { children: '@keyframes flowfig-label{from{opacity:0}to{opacity:0}}@keyframes flowfig-in{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}' +
|
|
793
|
-
// The
|
|
765
|
+
// The 2 px active border takes 1 px of padding, so the box size stays.
|
|
794
766
|
`.${ACTIVE}:not([data-diamond]){--ff-b:1px;border-width:2px!important;background:color-mix(in srgb, ${hue} 10%, ${v('surface')})!important;box-shadow:${glow}!important;border-color:${hue}!important}` +
|
|
795
767
|
`.${ACTIVE} polygon{fill:color-mix(in srgb, ${hue} 10%, ${v('surface')});stroke:${hue};stroke-width:2px}` }), !noMap && (_jsx(_Fragment, { children: _jsx("div", { style: {
|
|
796
768
|
margin: full ? '-48px -24px 0' : '-20px -16px 0',
|
|
@@ -809,8 +781,6 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
809
781
|
transform: fit.scale < 1 ? `scale(${fit.scale})` : undefined,
|
|
810
782
|
transformOrigin: 'top left',
|
|
811
783
|
padding: 4,
|
|
812
|
-
// room for edges that arc over or under the boxes
|
|
813
|
-
// A stub does not arc.
|
|
814
784
|
paddingTop: edges.some((e, i) => e.around === 'above' && !lanePlan?.stubs.has(ids[i])) ? 44 : 4,
|
|
815
785
|
paddingBottom: edges.some((e, i) => (e.around === 'below' || (!e.around && lanePlan?.around.has(ids[i]))) && !lanePlan?.stubs.has(ids[i]))
|
|
816
786
|
? 44
|
|
@@ -822,7 +792,6 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
822
792
|
height: '100%',
|
|
823
793
|
overflow: 'visible',
|
|
824
794
|
pointerEvents: 'none',
|
|
825
|
-
// A timeline dependency runs under the bars (z-index 1) and over the bands (0).
|
|
826
795
|
zIndex: tl ? 0 : undefined,
|
|
827
796
|
}, children: [_jsx("defs", { children: ['off', 'on'].map((k) => (_jsx("marker", { id: `fig-arrow-${k}`, 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: "context-stroke" }) }, k))) }), routed.map((r) => {
|
|
828
797
|
const on = litEdges.has(r.id);
|
|
@@ -836,7 +805,6 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
836
805
|
const guide = (_jsx("path", { ref: (p) => {
|
|
837
806
|
paths.current[r.id] = p;
|
|
838
807
|
}, d: r.d, fill: "none", stroke: "none" }, r.id));
|
|
839
|
-
// A cross-block edge draws its two stubs; its full path only guides the packet, which jumps between the blocks.
|
|
840
808
|
if (r.stub)
|
|
841
809
|
return [...r.stub.parts.map((d, j) => look(d, `${r.id}:${j}`)), guide];
|
|
842
810
|
return look(r.d, r.id, true);
|
|
@@ -868,7 +836,6 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
868
836
|
return [];
|
|
869
837
|
const on = litEdges.has(r.id);
|
|
870
838
|
const tone = hopTone(r.id);
|
|
871
|
-
// A cross-block edge has two pills, one per stub, in place of its label.
|
|
872
839
|
const pills = texts
|
|
873
840
|
? r.stub.pills.map((p, j) => ({
|
|
874
841
|
key: `${r.id}:${j}`,
|
|
@@ -905,7 +872,6 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
905
872
|
fontFamily: v('font'),
|
|
906
873
|
}, 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))), (() => {
|
|
907
874
|
const st = railState(rail, active ?? 0);
|
|
908
|
-
// The lifelines run through the message rows only, so a phase header reads as one clear divider.
|
|
909
875
|
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}`) : []);
|
|
910
876
|
return _jsx("path", { d: lines.join(' '), stroke: v('border'), strokeDasharray: "2 3" });
|
|
911
877
|
})(), (() => {
|
|
@@ -936,7 +902,6 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
936
902
|
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: [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 &&
|
|
937
903
|
(() => {
|
|
938
904
|
const tagW = ASYNC_TAG_W;
|
|
939
|
-
// The tag sits on the tail side of the pill, so it never hides the arrowhead.
|
|
940
905
|
const tx = !row.pill ? (x1 + x2) / 2 - tagW / 2 : x2 > x1 ? row.pill.x - tagW - 4 : row.pill.x + row.pill.w + 4;
|
|
941
906
|
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" })] }));
|
|
942
907
|
})(), 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}`));
|
|
@@ -951,7 +916,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
951
916
|
}, children: steps.map((s, i) => {
|
|
952
917
|
const on = active === i;
|
|
953
918
|
return (_jsxs("button", { type: "button", role: "tab", "aria-selected": on, onClick: () => {
|
|
954
|
-
clock.current.elapsed = 0;
|
|
919
|
+
clock.current.elapsed = 0;
|
|
955
920
|
setActive(i);
|
|
956
921
|
setPlaying(true);
|
|
957
922
|
setBeat(0);
|
|
@@ -1000,7 +965,6 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
1000
965
|
animation: 'flowfig-in .35s ease-out',
|
|
1001
966
|
}, children: said }, `${active}-${saidAt}`))] }))] }));
|
|
1002
967
|
}
|
|
1003
|
-
/** A 16px stroke icon drawn from one SVG path. */
|
|
1004
968
|
function Icon({ d, fill = false }) {
|
|
1005
969
|
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 }) }));
|
|
1006
970
|
}
|
package/dist/init.d.ts
CHANGED
|
@@ -1,25 +1,18 @@
|
|
|
1
1
|
type Agent = {
|
|
2
2
|
id: string;
|
|
3
3
|
name: string;
|
|
4
|
-
/** Paths whose presence means the repo uses the agent. A trailing `/` means a directory. */
|
|
5
4
|
marks: string[];
|
|
6
|
-
/** The file init writes, relative to the repo. */
|
|
7
5
|
file: string;
|
|
8
|
-
/** `section` edits a file the user owns; `whole` owns the file. */
|
|
9
6
|
kind: 'section' | 'whole';
|
|
10
|
-
/** Text before the marked block in a whole file. */
|
|
11
7
|
head?: string;
|
|
12
8
|
note?: string;
|
|
13
|
-
/** The MCP config file the agent reads, and the top-level key that holds the servers. `type` adds `"type": "stdio"`. */
|
|
14
9
|
mcp?: {
|
|
15
10
|
file: string;
|
|
16
11
|
key: 'mcpServers' | 'servers';
|
|
17
12
|
type?: true;
|
|
18
13
|
global?: string;
|
|
19
14
|
};
|
|
20
|
-
/** Shown in place of the MCP file when init cannot write the config. */
|
|
21
15
|
mcpNote?: string;
|
|
22
|
-
/** A second file, a marked section in a file the user owns. Written only in a repo run, never with `--global`. */
|
|
23
16
|
also?: {
|
|
24
17
|
file: string;
|
|
25
18
|
text: string;
|
|
@@ -30,16 +23,13 @@ export declare const MCP_ENTRY: {
|
|
|
30
23
|
args: string[];
|
|
31
24
|
};
|
|
32
25
|
export declare const AGENTS: Agent[];
|
|
33
|
-
/** The ids of the agents that the repo at `dir` already uses. */
|
|
34
26
|
export declare const detect: (dir: string) => string[];
|
|
35
|
-
/** Toggle the agents named by numbers ("1 3" or "1,3").
|
|
27
|
+
/** Toggle the agents named by numbers ("1 3" or "1,3"). Ignore bad input. */
|
|
36
28
|
export declare function toggle(input: string, selected: Set<string>, count: number): Set<string>;
|
|
37
|
-
/** The picker screen. */
|
|
38
29
|
export declare function renderList(selected: Set<string>, detected: string[], dir: string): string;
|
|
39
|
-
/** The new file text, or `undefined` if the
|
|
30
|
+
/** The new file text, or `undefined` if the user owns the file. */
|
|
40
31
|
export declare function place(a: Agent, old: string | undefined): string | undefined;
|
|
41
|
-
/** The MCP file with the flowfig entry
|
|
32
|
+
/** The MCP file with the flowfig entry, or `undefined` if the file cannot hold it. */
|
|
42
33
|
export declare function registerMcp(a: Agent, old: string | undefined): string | undefined;
|
|
43
|
-
/** Run `flowfig init`. Returns the exit code. */
|
|
44
34
|
export declare function runInit(argv: string[]): Promise<number>;
|
|
45
35
|
export {};
|