flowfig 0.8.0 → 0.8.1

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/dist/svg.js CHANGED
@@ -1,14 +1,14 @@
1
1
  // Wrapping is approximate: text is measured by character class, not by a browser.
2
- import { route } from './geometry.js';
2
+ import { arcRoom, avoidOf, route } from './geometry.js';
3
3
  import { foldedLabel, groupBox, layoutRail, railState, RAIL } from './rail.js';
4
4
  import { textWidth, wrap } from './text.js';
5
5
  import { checkRendered, planFor } from './check.js';
6
6
  export { parseSource, links } from './source.js';
7
7
  export { diff, formatDiff } from './diff.js';
8
- import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD, CARD_WIDTH, DARK, EDGE_OFF, EDGE_ON, LIGHT, TONES, toneFill, toneTint, labelPillW, ON_ACCENT, str, edgeId, groupGap, isGroup, isRows, LANE_PAD, LANE_ROW_GAP, LANE_BLOCK_GAP, FRAME_SIDE, laneGutter as gutterOf, STUB_ROOM, laneBlock, laneEnd, tightCopies, nodeWidth, diamondLines, diamondRoom, isLanesLayout, timelineBeats, timelineLayout, TL_AXIS_H, TL_AXIS_W, TL_BAR_H, TL_ROW_GAP, toBeat, beatMs, STEP_HOLD_MS, playheadItem, loopStartItem, dateLabelRaised, labelSpan, outsideLabelRect, } from './model.js';
8
+ import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD, CARD_WIDTH, DARK, EDGE_OFF, EDGE_ON, LIGHT, TONES, toneFill, toneTint, labelPillW, ON_ACCENT, str, edgeId, groupGap, isGroup, isRows, LANE_PAD, LANE_ROW_GAP, LANE_BLOCK_GAP, FRAME_SIDE, laneGutter as gutterOf, STUB_ROOM, laneBlock, laneEnd, tightCopies, LABEL_LINE, itemWidth, labelLines, fitCap, diamondLines, diamondRoom, isLanesLayout, timelineBeats, timelineLayout, TL_AXIS_H, TL_AXIS_W, TL_BAR_H, TL_ROW_GAP, toBeat, beatMs, STEP_HOLD_MS, playheadItem, loopStartItem, dateLabelRaised, labelSpan, outsideLabelRect, edgeTip, } from './model.js';
9
9
  export { LIGHT, DARK } from './model.js';
10
10
  const LINE = CARD_LINE, CARD_SIDE = 8, ROW_GAP = 4;
11
- const LABEL_LINE = 18, SUB_LINE = 15;
11
+ const SUB_LINE = 15;
12
12
  const FRAME_TOP = 37, FRAME_BOTTOM = 18;
13
13
  // The card-on fills approximate the player's 8 % accent tint.
14
14
  const vars = (t, cardOn) => `--accent:${t.accent}; --fg:${t.fg}; --muted:${t.muted}; --bg:${t.bg}; --surface:${t.surface}; --border:${t.border}; --card-on:${cardOn}; --tint:color-mix(in srgb, var(--accent) 10%, var(--surface));`;
@@ -42,20 +42,18 @@ function layoutCard(c, width) {
42
42
  function size(item, s) {
43
43
  if (!isGroup(item)) {
44
44
  const contents = s.cards.get(item.id);
45
- const w = nodeWidth(item, contents != null) + (item.shape === 'decision' ? 70 : 0);
45
+ const w = itemWidth(item, s.cards, s.fig.edges, s.cap);
46
46
  const top = item.shape === 'store' ? 24 : 10;
47
47
  const card = contents ? 8 + s.cardH.get(item.id) : 0;
48
- const h = Math.max(top + LABEL_LINE + (item.sub ? SUB_LINE : 0) + card + 10, s.minH(item.id));
48
+ const label = labelLines(item, w).length * LABEL_LINE;
49
+ const h = Math.max(top + label + (item.sub ? SUB_LINE : 0) + card + 10, s.minH(item.id));
49
50
  if (item.shape !== 'decision')
50
51
  return { w, h };
51
52
  return { w, h: contents ? h + 24 : Math.max(diamondLines(item, w).h, s.minH(item.id) + 24) };
52
53
  }
53
54
  const kids = item.children.map((c) => size(c, s));
54
- const gap = s.gap(item);
55
- const along = kids.reduce((n, k) => n + (item.direction === 'column' ? k.h : k.w), 0) + gap * (kids.length - 1);
56
- const across = Math.max(...kids.map((k) => (item.direction === 'column' ? k.w : k.h)));
57
- const inner = item.direction === 'column' ? { w: across, h: along } : { w: along, h: across };
58
- return item.label != null ? { w: inner.w + FRAME_SIDE * 2, h: inner.h + FRAME_TOP + FRAME_BOTTOM } : inner;
55
+ const h = item.direction === 'column' ? kids.reduce((n, k) => n + k.h, 0) + s.gap(item) * (kids.length - 1) : Math.max(...kids.map((k) => k.h));
56
+ return { w: itemWidth(item, s.cards, s.fig.edges, s.cap), h: h + (item.label != null ? FRAME_TOP + FRAME_BOTTOM : 0) };
59
57
  }
60
58
  function placeLanes(fig, x, y, s, out) {
61
59
  const lanes = fig.layout.children;
@@ -186,7 +184,16 @@ export function render(fig, opts = {}) {
186
184
  const cardH = new Map();
187
185
  const lanes = !tl && fig.lanes && isLanesLayout(fig.layout);
188
186
  const plan = lanes ? planFor(fig, opts) : null;
189
- const sizes = { cards, cardH, minH, gap: (g) => groupGap(g, fig.edges), fig, plan, tall: new Set(plan?.tall) };
187
+ const sizes = {
188
+ cards,
189
+ cardH,
190
+ minH,
191
+ gap: (g) => groupGap(g, fig.edges),
192
+ fig,
193
+ plan,
194
+ tall: new Set(plan?.tall),
195
+ cap: fitCap(fig, opts.width ?? 830, pad),
196
+ };
190
197
  for (const [id, contents] of cards) {
191
198
  const width = CARD_WIDTH;
192
199
  const widths = [width];
@@ -227,13 +234,10 @@ export function render(fig, opts = {}) {
227
234
  around: e.around ?? (plan?.around.has(ids[i]) ? 'below' : undefined),
228
235
  ...(tl && { sides: ['r', 'l'], elbow: true }),
229
236
  ...(stubs.has(ids[i]) && { stub: stubs.get(ids[i]).map(labelPillW), bands: [bandOf(ids[i], e.from, 0), bandOf(ids[i], e.to, 1)] }),
230
- ...((stubs.size || lanes) && e.label != null && { labelW: labelPillW(str(e.label)) }),
231
- })), rects, tips, stubs.size
232
- ? placed.flatMap((p) => (!isGroup(p.item) ? [p] : p.lane ? [{ x: p.x, y: p.y, w: gutter, h: p.h }] : []))
233
- : [
234
- ...placed.filter((p) => p.tl && !p.tl.labelInside).map((p) => outsideLabelRect(p, str(p.item.label))),
235
- ...placed.filter((p) => p.tl).map((p) => ({ x: p.x, y: p.y, w: p.w, h: p.h, box: true })),
236
- ], stubs.size ? { x: placed[0].x + gutter, y: placed[0].y, w: placed[0].w - gutter, h: placed[0].h } : undefined, lanes ? { bands: placed.filter((p) => p.lane), boxes: placed.filter((p) => !isGroup(p.item)) } : undefined);
237
+ ...(!tl && e.label != null && { labelW: labelPillW(str(e.label)) }),
238
+ })), rects, tips, avoidOf(placed.filter((p) => !isGroup(p.item)), stubs.size
239
+ ? placed.filter((p) => p.lane).map((p) => ({ x: p.x, y: p.y, w: gutter, h: p.h }))
240
+ : placed.filter((p) => p.tl && !p.tl.labelInside).map((p) => outsideLabelRect(p, str(p.item.label))), stubs.size > 0), stubs.size ? { x: placed[0].x + gutter, y: placed[0].y, w: placed[0].w - gutter, h: placed[0].h } : undefined, lanes ? { bands: placed.filter((p) => p.lane), boxes: placed.filter((p) => !isGroup(p.item)) } : undefined, pad);
237
241
  };
238
242
  let routed = go();
239
243
  for (let pass = 0; plan && pass < 3; pass++) {
@@ -260,7 +264,11 @@ export function render(fig, opts = {}) {
260
264
  const dl = dia ? diamondLines(p.item, p.w) : null;
261
265
  const room = (far) => diamondRoom(p.w, p.h, far);
262
266
  const flat = (p.item.shape === 'decision' ? p.w - 70 : p.w) - 16;
263
- const texts = [{ text: str(p.item.label), fontSize: 14, room: dl ? room(dl.label.far) : flat }];
267
+ const texts = labelLines(p.item, p.w).map((text) => ({
268
+ text,
269
+ fontSize: 14,
270
+ room: dl ? room(dl.label.far) : flat,
271
+ }));
264
272
  if (dl)
265
273
  for (const l of dl.subs)
266
274
  texts.push({ text: l.text, fontSize: 12, room: room(l.far) });
@@ -443,6 +451,7 @@ export function render(fig, opts = {}) {
443
451
  cls(p.lane && 'lane', anim(on, 'stroke: var(--accent)', 'stroke: var(--border)', 'n')) +
444
452
  `/><text x="${n2(p.x + FRAME_SIDE)}" y="${n2(p.lane ? p.y + p.h / 2 + 4 : p.y + 20)}" class="frame">${esc(str(item.label).toUpperCase())}</text>`);
445
453
  }
454
+ const tip = (inner) => (item.source ? `<g><title>${esc(item.source)}</title>${inner}</g>` : inner);
446
455
  const bt = item.tone && TONES[item.tone];
447
456
  const stroke = cls(boxAnim(item.id, true, bt));
448
457
  if (p.tl) {
@@ -455,7 +464,7 @@ export function render(fig, opts = {}) {
455
464
  ? `<polygon points="${n2(cx)},${n2(p.y)} ${n2(p.x + p.w)},${n2(cy)} ${n2(cx)},${n2(p.y + p.h)} ${n2(p.x)},${n2(cy)}" fill="${fill0}" stroke="${stroke0}"${stroke}/>`
456
465
  : `<rect x="${n2(p.x)}" y="${n2(p.y)}" width="${n2(p.w)}" height="${n2(p.h)}" rx="6" fill="${fill0}" stroke="${stroke0}"${stroke}/>`;
457
466
  const tx = inside ? p.x + 8 : p.x + p.w + 6;
458
- return shape + `<text x="${n2(tx)}" y="${n2(cy + 4.5)}" class="bar">${esc(label)}</text>`;
467
+ return tip(shape + `<text x="${n2(tx)}" y="${n2(cy + 4.5)}" class="bar">${esc(label)}</text>`);
459
468
  }
460
469
  const rim = item.shape === 'store' ? cls(boxAnim(item.id, false, bt)) : '';
461
470
  const fill0 = bt ? toneTint(bt, 'var(--bg)') : 'var(--bg)';
@@ -471,11 +480,14 @@ export function render(fig, opts = {}) {
471
480
  ? `<path d="M${n2(p.x)} ${n2(p.y + 12)} a ${n2(p.w / 2)} 12 0 0 1 ${n2(p.w)} 0 v ${n2(p.h - 24)} a ${n2(p.w / 2)} 12 0 0 1 ${n2(-p.w)} 0 z" fill="${fill0}" stroke="${stroke0}"${stroke}/>` +
472
481
  `<path d="M${n2(p.x)} ${n2(p.y + 12)} a ${n2(p.w / 2)} 12 0 0 0 ${n2(p.w)} 0" fill="none" stroke="${stroke0}"${rim}/>`
473
482
  : `<rect x="${n2(p.x)}" y="${n2(p.y)}" width="${n2(p.w)}" height="${n2(p.h)}" rx="10" fill="${fill0}" stroke="${stroke0}"${stroke}/>`;
474
- const label = `<text x="${n2(cx)}" y="${n2(labelY)}" class="label">${esc(str(item.label))}</text>`;
483
+ const lines = labelLines(item, p.w);
484
+ const extra = (lines.length - 1) * LABEL_LINE;
485
+ const firstY = item.shape === 'store' || contents ? labelY : labelY - extra / 2;
486
+ const label = lines.map((l, i) => `<text x="${n2(cx)}" y="${n2(firstY + i * LABEL_LINE)}" class="label">${esc(l)}</text>`).join('');
475
487
  const sub = dl
476
488
  ? dl.subs.map((l) => `<text x="${n2(cx)}" y="${n2(p.y + p.h / 2 + l.dy)}" class="sub">${esc(l.text)}</text>`).join('')
477
489
  : item.sub
478
- ? `<text x="${n2(cx)}" y="${n2(labelY + SUB_LINE)}" class="sub">${esc(str(item.sub))}</text>`
490
+ ? `<text x="${n2(cx)}" y="${n2(firstY + extra + SUB_LINE)}" class="sub">${esc(str(item.sub))}</text>`
479
491
  : '';
480
492
  const dot = bt ?? 'var(--accent)';
481
493
  const my = n2(p.y + p.h / 2);
@@ -484,7 +496,7 @@ export function render(fig, opts = {}) {
484
496
  : item.mark === 'end'
485
497
  ? `<circle cx="${n2(p.x + p.w + 12)}" cy="${my}" r="6.25" fill="none" stroke="${dot}" stroke-width="1.5"/><circle cx="${n2(p.x + p.w + 12)}" cy="${my}" r="4" fill="${dot}"/>`
486
498
  : '';
487
- return shape + label + sub + mark + (contents ? card(p, cardTop, contents) : '');
499
+ return tip(shape + label + sub + mark + (contents ? card(p, cardTop, contents) : ''));
488
500
  });
489
501
  function card(p, top, contents) {
490
502
  const id = p.item.id;
@@ -550,11 +562,13 @@ export function render(fig, opts = {}) {
550
562
  const lit = cls(toned
551
563
  ? frames(on.map((x, i) => (x ? `stroke: ${col[i]}; stroke-width: ${EDGE_ON}` : off)), 'e', off)
552
564
  : anim(on, `stroke: var(--accent); stroke-width: ${EDGE_ON}`, off, 'e'));
553
- const path = r.stub
565
+ const tip = edgeTip(r.id, e.source, beats.flat());
566
+ const path0 = r.stub
554
567
  ? r.stub.parts
555
568
  .map((d) => `<path d="${d}" fill="none" stroke="var(--muted)" stroke-width="${EDGE_OFF}" marker-end="url(#arrow)"${lit}/>`)
556
569
  .join('') + `<path id="p-${esc(r.id)}" d="${r.d}" fill="none" stroke="none"/>`
557
570
  : `<path id="p-${esc(r.id)}" d="${r.d}" fill="none" stroke="var(--muted)" stroke-width="${EDGE_OFF}" marker-end="url(#arrow)"${lit}/>`;
571
+ const path = tip ? `<g><title>${esc(tip)}</title>${path0}</g>` : path0;
558
572
  const pill = (x, y, lw, text) => `<rect x="${n2(x - lw / 2)}" y="${n2(y - 9)}" width="${n2(lw)}" height="18" rx="9" fill="var(--bg)" stroke="var(--border)"` +
559
573
  cls(toned
560
574
  ? frames(on.map((x, i) => x ? `fill: ${tone[i] ? toneFill(col[i]) : col[i]}; stroke: ${col[i]}` : 'fill: var(--bg); stroke: var(--border)'), 'l', 'fill: var(--bg); stroke: var(--border)')
@@ -668,18 +682,18 @@ export function render(fig, opts = {}) {
668
682
  }
669
683
  }
670
684
  const bounds = placed[0];
671
- const arcs = fig.edges.some((e, i) => (e.around || plan?.around.has(ids[i])) && !stubs.has(ids[i]));
685
+ const [arcT, arcB, arcL, arcR] = ['above', 'below', 'left', 'right'].map((w) => arcRoom(routed, w));
672
686
  const capLines = [...new Set(captions)].flatMap((c) => wrap(c, Math.max(560, bounds.w), 13.5).length);
673
- const mapW = Math.max(bounds.w + pad * 2, 560);
687
+ const mapW = Math.max(bounds.w + pad * 2 + arcL + arcR, 560);
674
688
  const rail = fig.rail ? layoutRail(fig, fig.rail === 'only' ? 560 : mapW) : null;
675
689
  const only = fig.rail === 'only' && rail != null;
676
690
  const capTop = rail ? 20 : 26;
677
691
  const capH = steps.length ? capTop + 4 + Math.max(0, ...capLines) * 20 : 0;
678
692
  const W = only ? Math.max(560, rail.width) : Math.max(mapW, rail?.width ?? 0);
679
- const mapH = only ? 0 : bounds.h + pad * 2 + (arcs ? 44 : 0);
693
+ const mapH = only ? 0 : bounds.h + pad * 2 + arcT + arcB;
680
694
  const top = only ? pad : mapH + RAIL.gap;
681
695
  const H = only ? pad + rail.height + capH : mapH + (rail ? RAIL.gap + rail.height : 0) + capH;
682
- const shift = (W - (bounds.w + pad * 2)) / 2;
696
+ const shift = (W - (bounds.w + pad * 2 + arcL + arcR)) / 2 + arcL;
683
697
  const railX = rail ? (W - rail.width) / 2 : 0;
684
698
  const labels = rail || synthetic
685
699
  ? []
@@ -758,7 +772,8 @@ export function render(fig, opts = {}) {
758
772
  ? `<rect x="${n2(row.pill.x)}" y="${n2(ly - 9)}" width="${n2(row.pill.w)}" height="18" rx="9" fill="var(--bg)" stroke="var(--border)"${cls(anim(now, `fill: ${row.tone ? toneFill(col) : col}; stroke: ${col}`, 'fill: var(--bg); stroke: var(--border)', row.tone ? 'l' + col : 'l'))}/>` +
759
773
  `<text x="${n2(row.pill.x + row.pill.w / 2)}" y="${n2(ly + 4)}"${cls('edgelabel', anim(now, `fill: ${ON_ACCENT}`, 'fill: var(--muted)', 'x'))}>${esc(row.text)}</text>`
760
774
  : '';
761
- parts.push(`<g${cls('railrow', moveRow(i))}><g${cls(anim(next, 'opacity: .45', 'opacity: 1', 'r'))}>` +
775
+ const tip = edgeTip(row.edge, fig.edges[ids.indexOf(row.edge)].source, beats.flat());
776
+ parts.push(`<g${cls('railrow', moveRow(i))}>${tip ? `<title>${esc(tip)}</title>` : ''}<g${cls(anim(next, 'opacity: .45', 'opacity: 1', 'r'))}>` +
762
777
  `<path d="M ${n2(x1)} ${n2(ly)} H ${n2(x2)}" fill="none" stroke="var(--muted)" stroke-width="${EDGE_OFF}"${row.async ? ' stroke-dasharray="4 3"' : ''} marker-end="url(#arrow)"${cls(lit)}/>` +
763
778
  tag +
764
779
  pill +
@@ -796,7 +811,7 @@ ${css.join('\n')}
796
811
  <rect width="100%" height="100%" fill="var(--bg)"/>
797
812
  ${only
798
813
  ? ''
799
- : `<g transform="translate(${n2(shift)} ${arcs ? 44 : 0})">
814
+ : `<g transform="translate(${n2(shift)} ${arcT})">
800
815
  ${axisSvg}
801
816
  ${tl ? boxes.filter((b, i) => b && isGroup(placed[i].item)).join('\n') : ''}${lineSvg && `\n${lineSvg}`}
802
817
  ${tl ? edgeSvg.join('\n') : ''}
@@ -814,6 +829,7 @@ ${said.join('\n')}
814
829
  fonts.push(12, 13);
815
830
  const scene = {
816
831
  width: n2(W),
832
+ ...(!only && { area: { x: -shift, y: -arcT, w: W, h: mapH } }),
817
833
  boxes: only ? [] : sceneBoxes,
818
834
  edges: [
819
835
  ...(only ? [] : routed).flatMap((r) => {
@@ -827,14 +843,15 @@ ${said.join('\n')}
827
843
  to: e.to,
828
844
  curve: r.curve,
829
845
  label: labelRects[r.id],
830
- ...(tl && { behind: true, elbow: r.elbow }),
846
+ ...(tl && { behind: true }),
847
+ ...(r.elbow && { elbow: r.elbow }),
831
848
  },
832
849
  ];
833
850
  }),
834
851
  ...(rail?.rows ?? []).flatMap((row, i) => {
835
852
  if (row.kind !== 'message')
836
853
  return [];
837
- const dx = only ? railX : railX - shift, dy = only ? top : top - (arcs ? 44 : 0);
854
+ const dx = only ? railX : railX - shift, dy = only ? top : top - arcT;
838
855
  const y = dy + railState(rail, row.step)[i].y + RAIL.row / 2;
839
856
  const [x1, x2] = [dx + rail.columns[row.from].x, dx + rail.columns[row.to].x];
840
857
  const p = { x: x1, y }, q = { x: x2, y };
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.8.0";
1
+ export declare const VERSION = "0.8.1";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // The release list bumps this with package.json.
2
- export const VERSION = '0.8.0';
2
+ export const VERSION = '0.8.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowfig",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Your coding agent draws animated diagrams from your code, and flowfig checks them. One animated SVG for a GitHub README, PR or issue, and a React player. Set up Claude Code, Cursor, GitHub Copilot, Codex, Gemini CLI, Windsurf and Kiro with npx flowfig init.",
5
5
  "license": "MIT",
6
6
  "repository": {