flowfig 0.3.0 → 0.5.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 +70 -41
- package/dist/browser.d.ts +33 -0
- package/dist/browser.js +201 -0
- package/dist/check.js +104 -4
- package/dist/cli.js +177 -7
- package/dist/diff.js +1 -1
- package/dist/draw.js +20 -3
- package/dist/geometry.d.ts +20 -1
- package/dist/geometry.js +204 -14
- package/dist/gif.d.ts +12 -0
- package/dist/gif.js +245 -0
- package/dist/guide.d.ts +3 -3
- package/dist/guide.js +28 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.js +357 -39
- package/dist/init.d.ts +5 -0
- package/dist/init.js +58 -22
- package/dist/mcp.js +1 -1
- package/dist/model.d.ts +180 -0
- package/dist/model.js +420 -8
- package/dist/open.d.ts +15 -0
- package/dist/open.js +82 -0
- package/dist/page.d.ts +2 -0
- package/dist/page.js +16 -0
- package/dist/picker.d.ts +33 -0
- package/dist/picker.js +148 -0
- package/dist/png.d.ts +7 -0
- package/dist/png.js +70 -0
- package/dist/scene.d.ts +9 -0
- package/dist/svg.d.ts +3 -0
- package/dist/svg.js +334 -76
- package/dist/text.d.ts +2 -0
- package/dist/text.js +19 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { route } from './geometry.js';
|
|
5
5
|
import { foldedLabel, groupBox, layoutRail, railState, RAIL } from './rail.js';
|
|
6
|
+
import { textWidth } from './text.js';
|
|
6
7
|
import { checkScene, checkSpec, checkTheme } from './check.js';
|
|
7
|
-
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, nodes, toBeat, beatMs, STEP_HOLD_MS, } 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, dateLabelRaised, } from './model.js';
|
|
8
9
|
export { LIGHT, DARK } from './model.js';
|
|
9
10
|
const DEFAULTS = { ...LIGHT, font: 'inherit' };
|
|
10
11
|
const v = (k) => `var(--fig-${k}, ${DEFAULTS[k]})`;
|
|
11
12
|
const MONO = 'var(--ifm-font-family-monospace, ui-monospace, SFMono-Regular, Menlo, monospace)';
|
|
13
|
+
const NONE = []; // a stable default, so a memo keyed on it does not run every render
|
|
12
14
|
const ACTIVE = 'flowfig-active'; // the box a packet has just reached
|
|
13
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.
|
|
14
16
|
const hue = `var(--ff-hop, var(--ff-box, ${v('accent')}))`;
|
|
@@ -46,7 +48,14 @@ const cardBody = (c) => {
|
|
|
46
48
|
* The interactive player for one figure: tabs for the steps, play and pause, speed, hover and full screen.
|
|
47
49
|
* Give it a `FlowProps` spec. Use `toSvg` from `flowfig/svg` for a static animated SVG instead.
|
|
48
50
|
*/
|
|
49
|
-
export function Flow({ layout, edges, steps =
|
|
51
|
+
export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900, autoplay = true, check = false, rail: withRail = false, lanes, timeline, today, }) {
|
|
52
|
+
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
|
+
const synthetic = tl && !stepsIn.length;
|
|
55
|
+
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
|
+
const [axisW, setAxisW] = useState(TL_AXIS_W);
|
|
58
|
+
const area = useRef(null);
|
|
50
59
|
const root = useRef(null);
|
|
51
60
|
const outer = useRef(null);
|
|
52
61
|
const [fit, setFit] = useState({ scale: 1, height: 0 });
|
|
@@ -62,8 +71,17 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
62
71
|
const chips = useRef([]);
|
|
63
72
|
const bar = useRef(null); // the active tab's progress line
|
|
64
73
|
const [beat, setBeat] = useState(0);
|
|
74
|
+
const [holding, setHolding] = useState(false); // the step hold: a timeline's today line rests at today
|
|
65
75
|
const paths = useRef({});
|
|
66
76
|
const [routed, setRouted] = useState([]);
|
|
77
|
+
// Lane copies that grow because a stub there had no place clear of every edge path (see `tightCopies`).
|
|
78
|
+
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
|
+
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 })
|
|
84
|
+
: null, [lanes, tl, layout, edges, steps, withRail]);
|
|
67
85
|
const [active, setActive] = useState(steps.length ? 0 : null);
|
|
68
86
|
const [playing, setPlaying] = useState(autoplay);
|
|
69
87
|
// The step's clock lives outside React: pausing freezes it, resizing keeps it, only a new step resets it.
|
|
@@ -75,7 +93,15 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
75
93
|
const clock = useRef({ beats: null, elapsed: 0 });
|
|
76
94
|
const [hover, setHover] = useState(null);
|
|
77
95
|
const ids = useMemo(() => edges.map(edgeId), [edges]);
|
|
78
|
-
|
|
96
|
+
// A milestone is a diamond too, so its edges meet the tips.
|
|
97
|
+
const tips = useMemo(() => new Set([
|
|
98
|
+
...decisions(layout),
|
|
99
|
+
...(tl
|
|
100
|
+
? timelineLayout({ layout, edges, timeline, today }, TL_AXIS_W)
|
|
101
|
+
.items.filter((i) => i.milestone)
|
|
102
|
+
.map((i) => i.id)
|
|
103
|
+
: []),
|
|
104
|
+
]), [layout, tl, edges, timeline, today]);
|
|
79
105
|
const step = active == null ? null : steps[active];
|
|
80
106
|
const beats = useMemo(() => (step?.flow ?? []).map(toBeat), [step]);
|
|
81
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.
|
|
@@ -132,6 +158,8 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
132
158
|
const scale = Math.max(0.5, Math.min(1, box.clientWidth / el.offsetWidth));
|
|
133
159
|
setFit({ scale, height: el.offsetHeight * scale });
|
|
134
160
|
setMapW(el.offsetWidth);
|
|
161
|
+
if (area.current)
|
|
162
|
+
setAxisW(area.current.offsetWidth);
|
|
135
163
|
const base = el.getBoundingClientRect();
|
|
136
164
|
const k = base.width / el.offsetWidth; // the scale currently on screen; rects are measured unscaled
|
|
137
165
|
const rects = {};
|
|
@@ -144,7 +172,67 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
144
172
|
h: r.height / k,
|
|
145
173
|
};
|
|
146
174
|
});
|
|
147
|
-
|
|
175
|
+
const avoid = [];
|
|
176
|
+
el.querySelectorAll('[data-fig-outside]').forEach((n) => {
|
|
177
|
+
const r = n.getBoundingClientRect();
|
|
178
|
+
avoid.push({ x: (r.left - base.left) / k, y: (r.top - base.top) / k, w: r.width / k, h: r.height / k });
|
|
179
|
+
});
|
|
180
|
+
// Wrapped lanes: an edge between two blocks is two stubs with pills, clear of the boxes and the lane labels.
|
|
181
|
+
const stubs = lanePlan?.stubs ?? new Map();
|
|
182
|
+
if (stubs.size) {
|
|
183
|
+
for (const n of nodes(layout))
|
|
184
|
+
if (rects[n.id])
|
|
185
|
+
avoid.push(rects[n.id]);
|
|
186
|
+
el.querySelectorAll('[data-fig-gutter]').forEach((n) => {
|
|
187
|
+
const r = n.getBoundingClientRect();
|
|
188
|
+
avoid.push({ x: (r.left - base.left) / k, y: (r.top - base.top) / k, w: r.width / k, h: r.height / k });
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
// A wrapped lane end routes to the band of its block (`data-fig-copy`), as in the SVG.
|
|
192
|
+
const bands = {};
|
|
193
|
+
el.querySelectorAll('[data-fig-copy]').forEach((n) => {
|
|
194
|
+
const r = n.getBoundingClientRect();
|
|
195
|
+
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
|
+
});
|
|
197
|
+
// A stub pill stays in the lanes, right of the gutter, as in the SVG.
|
|
198
|
+
const blockRects = [...el.querySelectorAll('[data-fig-block]')].map((n) => n.getBoundingClientRect());
|
|
199
|
+
const pillArea = stubs.size && blockRects.length
|
|
200
|
+
? (() => {
|
|
201
|
+
const [x0, y0] = [Math.min(...blockRects.map((r) => r.left)), Math.min(...blockRects.map((r) => r.top))];
|
|
202
|
+
const [x1, y1] = [Math.max(...blockRects.map((r) => r.right)), Math.max(...blockRects.map((r) => r.bottom))];
|
|
203
|
+
const x = (x0 - base.left) / k + lanePlan.gutter;
|
|
204
|
+
return { x, y: (y0 - base.top) / k, w: (x1 - base.left) / k - x, h: (y1 - y0) / k };
|
|
205
|
+
})()
|
|
206
|
+
: undefined;
|
|
207
|
+
// The band of an edge end, in the block of that end, as in the SVG.
|
|
208
|
+
const laneBands = {};
|
|
209
|
+
el.querySelectorAll('[data-fig-band]').forEach((n) => {
|
|
210
|
+
const r = n.getBoundingClientRect();
|
|
211
|
+
laneBands[n.dataset.figBand] = { x: (r.left - base.left) / k, y: (r.top - base.top) / k, w: r.width / k, h: r.height / k };
|
|
212
|
+
});
|
|
213
|
+
const bandOf = (eid, id, j) => {
|
|
214
|
+
const li = layout.children.findIndex((l) => isGroup(l) && (l.id === id || l.children.some((b) => b.id === id)));
|
|
215
|
+
return laneBands[`${li}@${lanePlan?.ends.get(eid)?.[j]}`];
|
|
216
|
+
};
|
|
217
|
+
const end = (eid, id, start) => lanePlan ? laneEnd(lanePlan, eid, id, start, (lane, b) => bands[`${lane}@${b}`], rects) : id;
|
|
218
|
+
const next = route(edges.map((e, i) => ({
|
|
219
|
+
id: ids[i],
|
|
220
|
+
from: end(ids[i], e.from, true),
|
|
221
|
+
to: end(ids[i], e.to, false),
|
|
222
|
+
around: e.around ?? (lanePlan?.around.has(ids[i]) ? 'below' : undefined),
|
|
223
|
+
...(tl && { sides: ['r', 'l'], elbow: true }),
|
|
224
|
+
...(stubs.has(ids[i]) && {
|
|
225
|
+
stub: stubs.get(ids[i]).map(labelPillW),
|
|
226
|
+
bands: [bandOf(ids[i], e.from, 0), bandOf(ids[i], e.to, 1)],
|
|
227
|
+
}),
|
|
228
|
+
...(stubs.size && e.label != null && { labelW: labelPillW(str(e.label)) }),
|
|
229
|
+
})), rects, tips, avoid, pillArea);
|
|
230
|
+
setRouted(next);
|
|
231
|
+
if (lanePlan) {
|
|
232
|
+
const more = [...tightCopies(layout.children, lanePlan.ends, edges, next)].filter((k) => !lanePlan.tall.has(k) && !extraTall.has(k));
|
|
233
|
+
if (more.length)
|
|
234
|
+
setExtraTall(new Set([...extraTall, ...more]));
|
|
235
|
+
}
|
|
148
236
|
};
|
|
149
237
|
measure();
|
|
150
238
|
const ro = new ResizeObserver(measure);
|
|
@@ -152,7 +240,7 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
152
240
|
ro.observe(box);
|
|
153
241
|
el.querySelectorAll('[data-fig]').forEach((n) => ro.observe(n));
|
|
154
242
|
return () => ro.disconnect();
|
|
155
|
-
}, [edges, ids, layout, tips, noMap]);
|
|
243
|
+
}, [edges, ids, layout, tips, noMap, lanes, tl, axisW, lanePlan, extraTall]);
|
|
156
244
|
// The same rules as `flowfig check`, on what the browser actually drew: real fonts, real wrapping. Each fault prints once.
|
|
157
245
|
const reported = useRef(new Set());
|
|
158
246
|
useEffect(() => {
|
|
@@ -170,16 +258,30 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
170
258
|
const boxIds = new Set(nodes(layout).map((n) => n.id));
|
|
171
259
|
const boxes = [...(el ?? fig).querySelectorAll('[data-fig]')]
|
|
172
260
|
.filter((n) => boxIds.has(n.dataset.fig))
|
|
173
|
-
.map((n) =>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
261
|
+
.map((n) => {
|
|
262
|
+
const rect = rel(n);
|
|
263
|
+
return {
|
|
264
|
+
id: n.dataset.fig,
|
|
265
|
+
rect,
|
|
266
|
+
texts: [...n.querySelectorAll('*')]
|
|
267
|
+
.filter((t) => !t.children.length && t.textContent?.trim() && t.clientWidth > 0 && !t.closest('[aria-hidden]'))
|
|
268
|
+
.map((t) => {
|
|
269
|
+
const fontSize = parseFloat(getComputedStyle(t).fontSize);
|
|
270
|
+
if (!n.dataset.diamond)
|
|
271
|
+
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
|
+
const [r, range] = [rel(t), document.createRange()];
|
|
274
|
+
range.selectNodeContents(t);
|
|
275
|
+
const far = Math.max(Math.abs(r.y - rect.y - rect.h / 2), Math.abs(r.y + r.h - rect.y - rect.h / 2));
|
|
276
|
+
return {
|
|
277
|
+
text: t.textContent,
|
|
278
|
+
fontSize,
|
|
279
|
+
room: diamondRoom(rect.w, rect.h, far),
|
|
280
|
+
need: range.getBoundingClientRect().width / k,
|
|
281
|
+
};
|
|
282
|
+
}),
|
|
283
|
+
};
|
|
284
|
+
});
|
|
183
285
|
// Every visible leaf text counts, as in the SVG scene; the 9 px tag pills do not.
|
|
184
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.
|
|
185
287
|
const fonts = [...fig.querySelectorAll('*')]
|
|
@@ -189,13 +291,23 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
189
291
|
!t.closest('[data-fig-tag],[aria-hidden]'))
|
|
190
292
|
.map((t) => parseFloat(getComputedStyle(t).fontSize));
|
|
191
293
|
const labels = Object.fromEntries([...fig.querySelectorAll('[data-fig-label]')].map((n) => [n.dataset.figLabel, rel(n)]));
|
|
294
|
+
const lanesIn = el ? [...el.querySelectorAll('[data-fig-band]')] : [];
|
|
192
295
|
const scene = {
|
|
193
296
|
width: Math.max(el?.offsetWidth ?? 0, rail?.width ?? 0),
|
|
194
297
|
boxes,
|
|
298
|
+
...(lanePlan?.stubs.size && {
|
|
299
|
+
lanes: lanesIn.map((n) => ({
|
|
300
|
+
id: str(layout.children[Number(n.dataset.figBand.split('@')[0])].label),
|
|
301
|
+
rect: rel(n),
|
|
302
|
+
})),
|
|
303
|
+
}),
|
|
195
304
|
edges: [
|
|
196
|
-
...routed.
|
|
305
|
+
...routed.flatMap((r) => {
|
|
197
306
|
const e = edges[ids.indexOf(r.id)];
|
|
198
|
-
|
|
307
|
+
// A cross-block edge is two scene edges: each stub with its pill.
|
|
308
|
+
if (r.stub)
|
|
309
|
+
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
|
+
return [{ id: r.id, from: e.from, to: e.to, curve: r.curve, label: labels[r.id], ...(tl && { behind: true }) }];
|
|
199
311
|
}),
|
|
200
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.
|
|
201
313
|
...(rail?.rows ?? []).flatMap((row) => {
|
|
@@ -228,7 +340,7 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
228
340
|
reported.current.add(key);
|
|
229
341
|
console.warn(`flowfig check: ${f.severity} ${f.rule}: ${f.message}`);
|
|
230
342
|
}
|
|
231
|
-
}, [check, routed, layout, edges, steps, ids, theme, rail, noMap]);
|
|
343
|
+
}, [check, routed, layout, edges, steps, ids, theme, rail, noMap, lanePlan]);
|
|
232
344
|
// Play the step's beats: each moves its packets (and their data cards) along its edges, then the next step starts.
|
|
233
345
|
useEffect(() => {
|
|
234
346
|
const gs = dots.current, cs = chips.current, edgeOf = Object.fromEntries(edges.map((e, i) => [ids[i], e]));
|
|
@@ -239,7 +351,7 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
239
351
|
clock.current = { beats, elapsed: beats.slice(0, j).reduce((t, b) => t + beatMs(b, speed), 0) };
|
|
240
352
|
setBeat(j);
|
|
241
353
|
}
|
|
242
|
-
if (!beats.length || (!noMap && !routed.length))
|
|
354
|
+
if (!beats.length || (!noMap && !routed.length && !(tl && !edges.length)))
|
|
243
355
|
return;
|
|
244
356
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
245
357
|
if (bar.current)
|
|
@@ -265,6 +377,7 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
265
377
|
const i = next === -1 ? beats.length - 1 : next;
|
|
266
378
|
if (i !== shownBeat)
|
|
267
379
|
setBeat((shownBeat = i));
|
|
380
|
+
setHolding(t >= ends.at(-1));
|
|
268
381
|
const start = i ? ends[i - 1] : 0;
|
|
269
382
|
const f = Math.min(1, (t - start) / speed); // the packet crosses in `speed`; the hold gives the rest
|
|
270
383
|
const eased = f < 0.5 ? 2 * f * f : 1 - (-2 * f + 2) ** 2 / 2;
|
|
@@ -299,6 +412,11 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
299
412
|
const to = h.back ? edgeOf[h.edge]?.from : edgeOf[h.edge]?.to;
|
|
300
413
|
arrived.set(to, arrived.get(to) ?? (h.tone && TONES[h.tone]));
|
|
301
414
|
}
|
|
415
|
+
// A focused box turns active at the beat start; in a timeline, when the today line arrives.
|
|
416
|
+
if (t - start >= (tl ? 400 * BASE_RATE : 0))
|
|
417
|
+
for (const id of beats[i].focus ?? [])
|
|
418
|
+
if (!arrived.has(id))
|
|
419
|
+
arrived.set(id, undefined);
|
|
302
420
|
root.current?.querySelectorAll('[data-fig]').forEach((n) => {
|
|
303
421
|
const on = arrived.has(n.dataset.fig ?? '');
|
|
304
422
|
if (on) {
|
|
@@ -338,7 +456,7 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
338
456
|
? [edges[hoverEdgeIdx].from, edges[hoverEdgeIdx].to]
|
|
339
457
|
: hover
|
|
340
458
|
? [hover]
|
|
341
|
-
: [...(step?.nodes ?? []), ...Object.keys(shown), ...(cur?.light ?? [])]);
|
|
459
|
+
: [...(step?.nodes ?? []), ...Object.keys(shown), ...(cur?.light ?? []), ...(cur?.focus ?? [])]);
|
|
342
460
|
edges.forEach((e, i) => {
|
|
343
461
|
if (litEdges.has(ids[i]))
|
|
344
462
|
litNodes.add(e.from).add(e.to);
|
|
@@ -361,7 +479,164 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
361
479
|
};
|
|
362
480
|
const focus = hover != null || hoverEdge != null || step != null;
|
|
363
481
|
const vars = Object.fromEntries(Object.entries(theme ?? {}).map(([k, val]) => [`--fig-${k}`, val]));
|
|
482
|
+
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
|
+
const renderBar = (n, it) => {
|
|
485
|
+
const lit = litNodes.has(n.id);
|
|
486
|
+
const bt = n.tone && TONES[n.tone];
|
|
487
|
+
const label = String(n.label);
|
|
488
|
+
const inside = it.labelInside;
|
|
489
|
+
const outside = (_jsx("span", { "data-fig-outside": "", style: { position: 'absolute', left: '100%', marginLeft: 6, whiteSpace: 'nowrap', fontSize: 13, fontWeight: 500, color: v('fg') }, children: label }));
|
|
490
|
+
const top = LANE_PAD + it.row * (TL_BAR_H + TL_ROW_GAP);
|
|
491
|
+
return (_jsxs("div", { "data-fig": n.id, "data-diamond": it.milestone || undefined, onMouseEnter: () => setHover(n.id), onMouseLeave: () => setHover(null), style: {
|
|
492
|
+
position: 'absolute',
|
|
493
|
+
left: it.x,
|
|
494
|
+
top: it.milestone ? top + (TL_BAR_H - it.w) / 2 : top,
|
|
495
|
+
width: it.w,
|
|
496
|
+
height: it.milestone ? it.w : TL_BAR_H,
|
|
497
|
+
boxSizing: 'border-box',
|
|
498
|
+
display: 'flex',
|
|
499
|
+
alignItems: 'center',
|
|
500
|
+
padding: it.milestone ? 0 : '0 calc(8px - var(--ff-b, 0px))',
|
|
501
|
+
color: v('fg'),
|
|
502
|
+
background: it.milestone ? undefined : bt ? toneTint(bt, v('bg')) : v('bg'),
|
|
503
|
+
border: it.milestone ? undefined : `1px solid ${bt ?? (lit ? v('accent') : v('border'))}`,
|
|
504
|
+
...(bt && { '--ff-box': bt }),
|
|
505
|
+
borderRadius: 6,
|
|
506
|
+
fontSize: 13,
|
|
507
|
+
fontWeight: 500,
|
|
508
|
+
whiteSpace: 'nowrap',
|
|
509
|
+
opacity: focus && !lit ? 0.7 : 1,
|
|
510
|
+
transition: 'border-color .4s, background .4s, box-shadow .4s, border-width .4s, padding .4s, opacity .25s',
|
|
511
|
+
cursor: 'default',
|
|
512
|
+
}, children: [it.milestone && (_jsx("svg", { viewBox: "0 0 100 100", preserveAspectRatio: "none", style: { position: 'absolute', inset: 0, width: '100%', height: '100%', overflow: 'visible' }, children: _jsx("polygon", { points: "50,0 100,50 50,100 0,50", fill: bt ? toneTint(bt, v('bg')) : v('bg'), stroke: bt ?? (lit ? v('accent') : v('border')), strokeWidth: lit ? 1.6 : 1, vectorEffect: "non-scaling-stroke", style: { transition: 'stroke .4s, fill .4s, stroke-width .4s' } }) })), inside ? _jsx("span", { style: { position: 'relative' }, children: label }) : outside] }, n.id));
|
|
513
|
+
};
|
|
364
514
|
const renderItem = (item, depth) => {
|
|
515
|
+
if (timelineFig && item === layout) {
|
|
516
|
+
const lanesList = layout.children;
|
|
517
|
+
// The playhead follows the latest dated focus in the step, and rests at the last date before one and in the hold.
|
|
518
|
+
const focused = holding || still ? undefined : playheadItem(timelineFig.items, beats, beat);
|
|
519
|
+
const home = timelineFig.last ?? 0;
|
|
520
|
+
const at = focused ? focused.x + (focused.milestone ? focused.w / 2 : 0) : home;
|
|
521
|
+
const dateText = focused ? focused.date : timelineFig.lastDate;
|
|
522
|
+
const rowsPx = timelineFig.rows.map((r) => r * TL_BAR_H + (r - 1) * TL_ROW_GAP + LANE_PAD * 2);
|
|
523
|
+
return (_jsxs("div", { style: {
|
|
524
|
+
display: 'grid',
|
|
525
|
+
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
|
+
gridTemplateRows: `${TL_AXIS_H - LANE_ROW_GAP}px ${rowsPx.map((h) => h + 'px').join(' ')}`,
|
|
528
|
+
rowGap: LANE_ROW_GAP,
|
|
529
|
+
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) => {
|
|
531
|
+
const lit = lane.id != null && litNodes.has(lane.id);
|
|
532
|
+
return (_jsxs(Fragment, { children: [_jsx("div", { "data-fig": lane.id, "data-fig-lane": "", style: {
|
|
533
|
+
gridColumn: '1 / -1',
|
|
534
|
+
gridRow: li + 2,
|
|
535
|
+
background: v('surface'),
|
|
536
|
+
border: `1px solid ${lit ? v('accent') : v('border')}`,
|
|
537
|
+
boxShadow: lit ? glow : undefined,
|
|
538
|
+
transition: 'border-color .25s, box-shadow .25s',
|
|
539
|
+
borderRadius: 14,
|
|
540
|
+
zIndex: 0,
|
|
541
|
+
} }), _jsx("div", { style: {
|
|
542
|
+
gridColumn: 1,
|
|
543
|
+
gridRow: li + 2,
|
|
544
|
+
alignSelf: 'center',
|
|
545
|
+
zIndex: 1,
|
|
546
|
+
padding: '0 18px',
|
|
547
|
+
fontSize: 12,
|
|
548
|
+
fontWeight: 600,
|
|
549
|
+
letterSpacing: '.04em',
|
|
550
|
+
textTransform: 'uppercase',
|
|
551
|
+
color: v('muted'),
|
|
552
|
+
}, children: lane.label }), _jsx("div", { ref: li === 0 ? area : undefined, style: { gridColumn: 2, gridRow: li + 2, position: 'relative', zIndex: 1 }, children: timelineFig.items
|
|
553
|
+
.filter((it) => it.track === li)
|
|
554
|
+
.map((it) => renderBar(lane.children.find((k) => k.id === it.id), it)) })] }, lane.id ?? String(lane.label)));
|
|
555
|
+
}), timelineFig.last != null && (_jsxs("div", { style: { gridColumn: '2 / 4', gridRow: '1 / -1', position: 'relative', zIndex: 2, pointerEvents: 'none' }, children: [timelineFig.today != null && (_jsxs("div", { "data-fig-today": "", style: { position: 'absolute', left: timelineFig.today, top: 12, bottom: 0, width: 0 }, children: [_jsx("div", { style: { position: 'absolute', top: 0, bottom: 0, borderLeft: `1px dashed ${v('accent')}`, opacity: 0.6 } }), _jsx("span", { style: {
|
|
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) })] }))] }));
|
|
583
|
+
}
|
|
584
|
+
if (lanes && item === layout && isLanesLayout(layout)) {
|
|
585
|
+
const { cols, starts, gaps, lead, blocks, gutter, tall } = lanePlan;
|
|
586
|
+
const n = Math.max(0, ...cols.values()) + 1;
|
|
587
|
+
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
|
+
const grid = (bk) => {
|
|
590
|
+
const m = starts[bk + 1] - starts[bk];
|
|
591
|
+
const inBlock = (b) => laneBlock(starts, cols.get(b.id)) === bk;
|
|
592
|
+
const shown = layout.children.filter((lane) => count < 2 || lane.children.some(inBlock));
|
|
593
|
+
return (_jsx("div", { "data-fig-block": bk, style: {
|
|
594
|
+
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
|
+
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
|
+
gridAutoRows: `minmax(${38 + LANE_PAD * 2}px, auto)`,
|
|
600
|
+
columnGap: 0,
|
|
601
|
+
rowGap: LANE_ROW_GAP,
|
|
602
|
+
position: 'relative',
|
|
603
|
+
}, children: shown.map((lane, li) => {
|
|
604
|
+
const lit = lane.id != null && litNodes.has(lane.id);
|
|
605
|
+
return (_jsxs(Fragment, { children: [_jsx("div", { "data-fig": lane.id, "data-fig-copy": lane.id != null ? `${lane.id}@${bk}` : undefined, "data-fig-band": `${layout.children.indexOf(lane)}@${bk}`, "data-fig-lane": "", style: {
|
|
606
|
+
gridColumn: '1 / -1',
|
|
607
|
+
gridRow: li + 1,
|
|
608
|
+
background: v('surface'),
|
|
609
|
+
border: `1px solid ${lit ? v('accent') : v('border')}`,
|
|
610
|
+
boxShadow: lit ? glow : undefined,
|
|
611
|
+
transition: 'border-color .25s, box-shadow .25s',
|
|
612
|
+
borderRadius: 14,
|
|
613
|
+
zIndex: 0,
|
|
614
|
+
} }), _jsx("div", { "data-fig-gutter": "", style: {
|
|
615
|
+
gridColumn: 1,
|
|
616
|
+
gridRow: li + 1,
|
|
617
|
+
alignSelf: 'center',
|
|
618
|
+
zIndex: 1,
|
|
619
|
+
padding: '0 18px',
|
|
620
|
+
whiteSpace: 'nowrap',
|
|
621
|
+
fontSize: 12,
|
|
622
|
+
fontWeight: 600,
|
|
623
|
+
letterSpacing: '.04em',
|
|
624
|
+
textTransform: 'uppercase',
|
|
625
|
+
color: v('muted'),
|
|
626
|
+
}, children: lane.label }), lane.children.filter(inBlock).map((b) => (_jsx("div", { style: {
|
|
627
|
+
gridColumn: cols.get(b.id) - starts[bk] + 2,
|
|
628
|
+
gridRow: li + 1,
|
|
629
|
+
alignSelf: 'center',
|
|
630
|
+
zIndex: 1,
|
|
631
|
+
// The space after each column and before a block's first column comes from the plan, as in the SVG.
|
|
632
|
+
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
|
+
}, children: renderItem(b, 1) }, b.id)))] }, lane.id ?? String(lane.label)));
|
|
634
|
+
}) }, bk));
|
|
635
|
+
};
|
|
636
|
+
if (count < 2)
|
|
637
|
+
return grid(blocks[0]);
|
|
638
|
+
return _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: LANE_BLOCK_GAP }, children: blocks.map(grid) });
|
|
639
|
+
}
|
|
365
640
|
if (isGroup(item)) {
|
|
366
641
|
const lit = item.id != null && litNodes.has(item.id);
|
|
367
642
|
const framed = item.label != null;
|
|
@@ -399,14 +674,16 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
399
674
|
const diamond = item.shape === 'decision';
|
|
400
675
|
const store = item.shape === 'store';
|
|
401
676
|
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
|
+
const dl = diamond && !card ? diamondLines(item, nodeWidth(item, false) + 70) : null;
|
|
402
679
|
const bt = item.tone && TONES[item.tone];
|
|
403
680
|
return (_jsxs("div", { "data-fig": item.id, "data-diamond": diamond || undefined, onMouseEnter: () => setHover(item.id), onMouseLeave: () => setHover(null), style: {
|
|
404
681
|
position: 'relative',
|
|
405
682
|
isolation: 'isolate',
|
|
406
683
|
minWidth: 100,
|
|
407
|
-
maxWidth: card ? undefined : 190,
|
|
408
|
-
width: item.width ?? (card ? CARD_WIDTH : undefined),
|
|
409
|
-
minHeight: minHeight(item.id),
|
|
684
|
+
maxWidth: card || diamond ? undefined : 190,
|
|
685
|
+
width: diamond ? nodeWidth(item, card) + 70 : (item.width ?? (card ? CARD_WIDTH : undefined)),
|
|
686
|
+
minHeight: dl ? Math.max(dl.h, (minHeight(item.id) ?? 0) + 24) : minHeight(item.id),
|
|
410
687
|
boxSizing: 'border-box',
|
|
411
688
|
display: 'flex',
|
|
412
689
|
flexDirection: 'column',
|
|
@@ -443,7 +720,24 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
443
720
|
borderRadius: '50%',
|
|
444
721
|
border: `1px solid ${bt ?? (lit ? v('accent') : v('border'))}`,
|
|
445
722
|
transition: 'border-color .25s',
|
|
446
|
-
} })),
|
|
723
|
+
} })), item.mark && (_jsx("span", { "aria-hidden": true, style: {
|
|
724
|
+
position: 'absolute',
|
|
725
|
+
top: '50%',
|
|
726
|
+
transform: 'translateY(-50%)',
|
|
727
|
+
boxSizing: 'border-box',
|
|
728
|
+
borderRadius: '50%',
|
|
729
|
+
...(item.mark === 'start'
|
|
730
|
+
? { left: -18, width: 10, height: 10, background: bt ?? v('accent') }
|
|
731
|
+
: {
|
|
732
|
+
right: -20,
|
|
733
|
+
width: 14,
|
|
734
|
+
height: 14,
|
|
735
|
+
border: `1.5px solid ${bt ?? v('accent')}`,
|
|
736
|
+
background: `radial-gradient(circle, ${bt ?? v('accent')} 0 4px, transparent 4.5px)`,
|
|
737
|
+
}),
|
|
738
|
+
} })), _jsx("div", { children: item.label }), dl
|
|
739
|
+
? dl.subs.map((l, i) => (_jsx("div", { style: { fontSize: 12, fontWeight: 400, color: v('muted'), lineHeight: '15px', whiteSpace: 'nowrap' }, children: l.text }, i)))
|
|
740
|
+
: item.sub != null && _jsx("div", { style: { fontSize: 12, fontWeight: 400, color: v('muted'), marginTop: 2 }, children: item.sub }), card && (_jsxs("div", { style: {
|
|
447
741
|
marginTop: 8,
|
|
448
742
|
padding: '6px 8px',
|
|
449
743
|
minHeight: item.lines != null ? item.lines * CARD_LINE + CARD_PAD * 2 + 1 : undefined,
|
|
@@ -495,7 +789,7 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
495
789
|
zIndex: 3,
|
|
496
790
|
width: 28,
|
|
497
791
|
height: 28,
|
|
498
|
-
}, children: _jsx(Icon, { d: full ? 'M4 4l8 8M12 4l-8 8' : 'M9 3h4v4M7 13H3V9M13 3L9 7M3 13l4-4' }) }), _jsx("style", { children: '@keyframes flowfig-in{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}' +
|
|
792
|
+
}, 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}}' +
|
|
499
793
|
// The active box has a 2 px border, a tint and a glow. The padding shrinks by 1 px. The box size stays the same, so the edges keep their route.
|
|
500
794
|
`.${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}` +
|
|
501
795
|
`.${ACTIVE} polygon{fill:color-mix(in srgb, ${hue} 10%, ${v('surface')});stroke:${hue};stroke-width:2px}` }), !noMap && (_jsx(_Fragment, { children: _jsx("div", { style: {
|
|
@@ -516,8 +810,11 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
516
810
|
transformOrigin: 'top left',
|
|
517
811
|
padding: 4,
|
|
518
812
|
// room for edges that arc over or under the boxes
|
|
519
|
-
|
|
520
|
-
|
|
813
|
+
// A stub does not arc.
|
|
814
|
+
paddingTop: edges.some((e, i) => e.around === 'above' && !lanePlan?.stubs.has(ids[i])) ? 44 : 4,
|
|
815
|
+
paddingBottom: edges.some((e, i) => (e.around === 'below' || (!e.around && lanePlan?.around.has(ids[i]))) && !lanePlan?.stubs.has(ids[i]))
|
|
816
|
+
? 44
|
|
817
|
+
: 4,
|
|
521
818
|
}, children: [renderItem(layout, 0), _jsxs("svg", { style: {
|
|
522
819
|
position: 'absolute',
|
|
523
820
|
inset: 0,
|
|
@@ -525,13 +822,24 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
525
822
|
height: '100%',
|
|
526
823
|
overflow: 'visible',
|
|
527
824
|
pointerEvents: 'none',
|
|
825
|
+
// A timeline dependency runs under the bars (z-index 1) and over the bands (0).
|
|
826
|
+
zIndex: tl ? 0 : undefined,
|
|
528
827
|
}, 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) => {
|
|
529
828
|
const on = litEdges.has(r.id);
|
|
530
829
|
const tone = hopTone(r.id);
|
|
531
830
|
const hidden = !on && edges[ids.indexOf(r.id)].quiet;
|
|
532
|
-
|
|
831
|
+
const look = (d, key, guides = false) => (_jsx("path", { ref: guides
|
|
832
|
+
? (p) => {
|
|
833
|
+
paths.current[r.id] = p;
|
|
834
|
+
}
|
|
835
|
+
: undefined, d: d, fill: "none", stroke: on ? (tone ?? v('accent')) : v('muted'), strokeWidth: on ? EDGE_ON : EDGE_OFF, strokeOpacity: hidden ? 0 : focus && !on ? 0.35 : 1, markerEnd: hidden ? undefined : `url(#fig-arrow-${on ? 'on' : 'off'})`, style: { transition: 'stroke .25s, stroke-opacity .25s' } }, key));
|
|
836
|
+
const guide = (_jsx("path", { ref: (p) => {
|
|
533
837
|
paths.current[r.id] = p;
|
|
534
|
-
}, d: r.d, fill: "none", stroke:
|
|
838
|
+
}, 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
|
+
if (r.stub)
|
|
841
|
+
return [...r.stub.parts.map((d, j) => look(d, `${r.id}:${j}`)), guide];
|
|
842
|
+
return look(r.d, r.id, true);
|
|
535
843
|
}), Array.from({ length: Math.max(1, ...beats.map((b) => b.hops.length)) }, (_, j) => (_jsxs("g", { ref: (g) => {
|
|
536
844
|
dots.current[j] = g;
|
|
537
845
|
}, style: { opacity: 0, color: v('accent') }, children: [_jsx("circle", { r: 10, fill: "currentColor", opacity: 0.2 }), _jsx("circle", { r: 4.5, fill: "currentColor" })] }, j)))] }), Array.from({ length: Math.max(1, ...beats.map((b) => b.hops.length)) }, (_, j) => (_jsx("div", { ref: (c) => {
|
|
@@ -553,16 +861,26 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
553
861
|
color: ON_ACCENT,
|
|
554
862
|
boxShadow: '0 4px 14px rgba(0,0,0,.18)',
|
|
555
863
|
transition: 'opacity .2s',
|
|
556
|
-
}, children: cur?.hops[j]?.data }, j))), routed.
|
|
864
|
+
}, children: cur?.hops[j]?.data }, j))), routed.flatMap((r) => {
|
|
557
865
|
const e = edges[ids.indexOf(r.id)];
|
|
558
|
-
|
|
559
|
-
|
|
866
|
+
const texts = r.stub && lanePlan?.stubs.get(r.id);
|
|
867
|
+
if (!texts && (e.label == null || tl))
|
|
868
|
+
return [];
|
|
560
869
|
const on = litEdges.has(r.id);
|
|
561
870
|
const tone = hopTone(r.id);
|
|
562
|
-
|
|
871
|
+
// A cross-block edge has two pills, one per stub, in place of its label.
|
|
872
|
+
const pills = texts
|
|
873
|
+
? r.stub.pills.map((p, j) => ({
|
|
874
|
+
key: `${r.id}:${j}`,
|
|
875
|
+
x: p.x + p.w / 2,
|
|
876
|
+
y: p.y + p.h / 2,
|
|
877
|
+
text: (j === 0 && r.stub.short ? texts[2] : texts[j]),
|
|
878
|
+
}))
|
|
879
|
+
: [{ key: r.id, x: r.mid.x, y: r.mid.y, text: e.label }];
|
|
880
|
+
return pills.map((p) => (_jsx("div", { "data-fig-label": p.key, style: {
|
|
563
881
|
position: 'absolute',
|
|
564
|
-
left:
|
|
565
|
-
top:
|
|
882
|
+
left: p.x,
|
|
883
|
+
top: p.y,
|
|
566
884
|
transform: 'translate(-50%, -50%)',
|
|
567
885
|
fontSize: 11,
|
|
568
886
|
lineHeight: '16px',
|
|
@@ -576,7 +894,7 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
576
894
|
opacity: !on && e.quiet ? 0 : focus && !on ? 0.6 : 1,
|
|
577
895
|
fontFamily: MONO,
|
|
578
896
|
transition: 'background .25s, color .25s, opacity .25s',
|
|
579
|
-
}, children:
|
|
897
|
+
}, children: p.text }, p.key)));
|
|
580
898
|
})] }) }) }) })), rail && (_jsxs("svg", { viewBox: `0 0 ${rail.width} ${rail.height}`, style: {
|
|
581
899
|
display: 'block',
|
|
582
900
|
width: '100%',
|
|
@@ -623,7 +941,7 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
623
941
|
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" })] }));
|
|
624
942
|
})(), 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}`));
|
|
625
943
|
});
|
|
626
|
-
})(), _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: () => setPlaying((p) => !p), children: _jsx(Icon, { d: playing ? 'M5.5 4v8M10.5 4v8' : 'M5 3.5v9l7.5-4.5z', fill: !playing }) }), _jsx("div", { role: "tablist", style: {
|
|
944
|
+
})(), _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: () => setPlaying((p) => !p), children: _jsx(Icon, { d: playing ? 'M5.5 4v8M10.5 4v8' : 'M5 3.5v9l7.5-4.5z', fill: !playing }) }), !synthetic && (_jsx("div", { role: "tablist", style: {
|
|
627
945
|
display: 'inline-flex',
|
|
628
946
|
gap: 2,
|
|
629
947
|
padding: 3,
|
|
@@ -662,7 +980,7 @@ export function Flow({ layout, edges, steps = [], theme, speed = 900, autoplay =
|
|
|
662
980
|
transform: 'scaleX(0)',
|
|
663
981
|
opacity: playing ? 1 : 0.35,
|
|
664
982
|
} }))] }, i));
|
|
665
|
-
}) }), _jsxs("button", { type: "button", "aria-label": `Speed ${rate}×, switch to ${rate === 1 ? 2 : 1}×`, title: "Playback speed", style: {
|
|
983
|
+
}) })), _jsxs("button", { type: "button", "aria-label": `Speed ${rate}×, switch to ${rate === 1 ? 2 : 1}×`, title: "Playback speed", style: {
|
|
666
984
|
...iconBtn,
|
|
667
985
|
width: 'auto',
|
|
668
986
|
padding: '0 10px',
|
package/dist/init.d.ts
CHANGED
|
@@ -19,6 +19,11 @@ type Agent = {
|
|
|
19
19
|
};
|
|
20
20
|
/** Shown in place of the MCP file when init cannot write the config. */
|
|
21
21
|
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
|
+
also?: {
|
|
24
|
+
file: string;
|
|
25
|
+
text: string;
|
|
26
|
+
};
|
|
22
27
|
};
|
|
23
28
|
export declare const MCP_ENTRY: {
|
|
24
29
|
command: string;
|