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/dist/rail.js CHANGED
@@ -1,8 +1,5 @@
1
1
  import { edgeId, isGroup, labelPillW, str, toBeat } from './model.js';
2
2
  import { textWidth } from './text.js';
3
- // band: the band-label line; cols: the column-label row; bandInset: a band's inset from its outer column edges, so two adjacent
4
- // bands keep 16 px between them; clear: the space between a phase label or counter and a line; inset: the space a parallel band
5
- // leaves around its arrows and pills.
6
3
  export const RAIL = {
7
4
  gap: 24,
8
5
  band: 20,
@@ -16,9 +13,7 @@ export const RAIL = {
16
13
  inset: 6,
17
14
  maxOpen: 10,
18
15
  };
19
- /** The folded phase text. Both renderers and the layout use it, so the measured width matches the drawn width. */
20
16
  export const foldedLabel = (row) => `▸ ${row.label} · ${row.messages} messages`;
21
- /** Rail columns follow this order, so a reader meets the boxes in the same order on the rail as on the map. */
22
17
  const order = (item) => isGroup(item)
23
18
  ? [...(item.id ? [{ id: item.id, label: str(item.label) }] : []), ...item.children.flatMap(order)]
24
19
  : [{ id: item.id, label: str(item.label) }];
@@ -51,15 +46,12 @@ export function layoutRail(fig, mapWidth) {
51
46
  const used = new Set(msgs.flatMap((m) => [edges.get(m.edge).from, edges.get(m.edge).to]));
52
47
  const cols = order(fig.layout).filter((c) => used.has(c.id));
53
48
  const index = new Map(cols.map((c, i) => [c.id, i]));
54
- // Every column gets the same width: at least RAIL.minCol, wide enough for each label and pill, and together as wide as the map.
55
- // A column label also needs room for the band inset on each side.
56
49
  let need = RAIL.minCol;
57
50
  for (const c of cols)
58
51
  need = Math.max(need, textWidth(c.label, 12) + 16 + RAIL.bandInset * 2);
59
52
  for (const m of msgs)
60
53
  if (m.text)
61
54
  need = Math.max(need, labelPillW(m.text) + 16);
62
- // The phase labels start at RAIL.pad and the counters end at width - RAIL.pad. No band border comes within RAIL.clear of them.
63
55
  const folds = msgs.length > RAIL.maxOpen;
64
56
  const labelW = (text) => textWidth(text, 13, true);
65
57
  const counterW = (() => {
@@ -70,8 +62,6 @@ export function layoutRail(fig, mapWidth) {
70
62
  const label = str(steps[si].label);
71
63
  return labelW(folds ? foldedLabel({ label, messages: b.flat().length }) : label);
72
64
  }));
73
- // Bands: each labeled group around the columns it holds, RAIL.bandInset inside their outer edges; a nested group sits 4 px inside
74
- // its parent and one label line (RAIL.band) below it. The height comes later.
75
65
  const place = (left, right, colW) => {
76
66
  const width = left + colW * cols.length + right;
77
67
  const columns = cols.map((c, i) => ({ id: c.id, label: c.label, x: left + colW * (i + 0.5), w: colW }));
@@ -90,7 +80,6 @@ export function layoutRail(fig, mapWidth) {
90
80
  };
91
81
  walk(fig.layout, 0);
92
82
  const borders = bands.flatMap((b) => [b.rect.x, b.rect.x + b.rect.w]);
93
- // How far the columns must move in from each side, so every band border clears the label and the counter.
94
83
  const dl = Math.max(0, RAIL.pad + widest + RAIL.clear - Math.min(Infinity, ...borders));
95
84
  const dr = Math.max(0, Math.max(-Infinity, ...borders) - (width - RAIL.pad - counterW - RAIL.clear));
96
85
  return { width, columns, bands, dl, dr };
@@ -102,14 +91,12 @@ export function layoutRail(fig, mapWidth) {
102
91
  for (let k = 0; placed.dl > 0.01 || placed.dr > 0.01; k++) {
103
92
  left += placed.dl;
104
93
  right += placed.dr;
105
- // Narrower columns keep the rail as wide as the map. After a few tries the columns keep their width, and the rail grows.
106
94
  if (k < 8)
107
95
  colW = spread(left, right);
108
96
  placed = place(left, right, colW);
109
97
  }
110
98
  const { width, columns, bands } = placed;
111
99
  const head = (bands.length ? Math.max(...bands.map((b) => b.rect.y)) + RAIL.band : 0) + RAIL.cols;
112
- // Rows: a phase header per step, then one row per message.
113
100
  const rows = [];
114
101
  const groups = [];
115
102
  let n = 0;
@@ -145,7 +132,6 @@ export function layoutRail(fig, mapWidth) {
145
132
  }
146
133
  }
147
134
  });
148
- // Above maxOpen messages, only the playing step shows its rows; the others shrink to their phase header.
149
135
  const states = steps.map((_, open) => {
150
136
  let y = head;
151
137
  return rows.map((row) => {
@@ -159,7 +145,7 @@ export function layoutRail(fig, mapWidth) {
159
145
  const bottom = (s) => s.reduce((m, st, i) => (st.shown ? Math.max(m, st.y + (rows[i].kind === 'phase' ? RAIL.phase : RAIL.row)) : m), 0);
160
146
  const height = Math.max(...states.map(bottom)) + 8;
161
147
  for (const b of bands)
162
- b.rect.h = height - b.rect.y - (b.rect.y / RAIL.band) * 4; // the bottom edge keeps the 4 px nesting inset
148
+ b.rect.h = height - b.rect.y - (b.rect.y / RAIL.band) * 4;
163
149
  return { width, head, height, columns, bands, rows, groups, total: n, folds, states };
164
150
  }
165
151
  /** A parallel band around its rows, relative to the y of its first row: RAIL.inset around every arrow and pill it holds. */
@@ -171,6 +157,6 @@ export function groupBox(rail, g) {
171
157
  return [rail.columns[r.from].x, rail.columns[r.to].x, ...(r.pill ? [r.pill.x, r.pill.x + r.pill.w] : [])];
172
158
  });
173
159
  const x = Math.min(...xs) - RAIL.inset;
174
- const top = (RAIL.row - 18) / 2 - RAIL.inset; // 18: the pill height
160
+ const top = (RAIL.row - 18) / 2 - RAIL.inset;
175
161
  return { x, y: top, w: Math.max(...xs) + RAIL.inset - x, h: g.rows.length * RAIL.row - top * 2 };
176
162
  }
package/dist/scene.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Pt, Rect } from './geometry.ts';
2
- /** One line of text in a box. `need` is the measured width; without it, the check estimates the width. */
2
+ /** `need` is the measured width; without it, `check` estimates it. */
3
3
  export type TextRun = {
4
4
  text: string;
5
5
  fontSize: number;
@@ -12,32 +12,34 @@ export type SceneBox = {
12
12
  rect: Rect;
13
13
  texts: TextRun[];
14
14
  };
15
- /** `step` marks a rail row: rows of two steps never show at the same time, so their labels cannot overlap. */
16
- /** `behind` marks an edge drawn under the boxes (a timeline dependency): `check` skips its crossing test. */
17
- /** `pts` are the corners of an edge drawn as straight runs (a cross-block lanes edge): `check` tests them in place of `curve`. */
18
15
  export type SceneEdge = {
19
16
  id: string;
20
17
  from: string;
21
18
  to: string;
22
19
  curve: [Pt, Pt, Pt, Pt];
20
+ /** Corners of a straight-run edge (cross-block lanes). `check` tests them, not `curve`. */
23
21
  pts?: Pt[];
24
22
  label?: Rect;
23
+ /** A rail row: rows of two steps never show together, so labels cannot overlap. */
25
24
  step?: number;
25
+ /** Drawn under the boxes (a timeline dependency): `check` skips the crossing test. */
26
26
  behind?: true;
27
+ /** Timeline elbow corners. `check` tests a `behind` edge only when it has them. */
28
+ elbow?: Pt[];
27
29
  };
28
- /** `minFont` is the smallest reading text in px, before any scale. Group frames are not boxes. */
29
- /** `lanes` are the lane bands of a swimlanes figure, one per lane and block, named by the lane label. A stub pill stays inside one. */
30
30
  export type Scene = {
31
31
  width: number;
32
32
  boxes: SceneBox[];
33
33
  edges: SceneEdge[];
34
+ /** The smallest reading text in px, before any scale. */
34
35
  minFont: number;
36
+ /** The lane bands of a swimlanes figure. A stub pill stays inside one. */
35
37
  lanes?: {
36
38
  id: string;
37
39
  rect: Rect;
38
40
  }[];
39
41
  };
40
- /** One fault that `flowfig check` found. `rule` names the check, for example `unknown-id`, `text-overflow`, `edge-crosses-box`, `label-overlap`, `small-text`, `low-contrast`. `ids` are the boxes or edges it names. */
42
+ /** One fault that `flowfig check` found. `ids` name the boxes or edges. */
41
43
  export type Finding = {
42
44
  rule: string;
43
45
  severity: 'error' | 'warning';
package/dist/source.d.ts CHANGED
@@ -4,7 +4,6 @@ export declare function parseSource(s: string): {
4
4
  path: string;
5
5
  symbol?: string;
6
6
  } | null;
7
- /** The GitHub anchor of a heading: lower case, only letters, digits, spaces and hyphens, spaces to hyphens. */
8
7
  export declare const headingSlug: (text: string) => string;
9
8
  export type Link = {
10
9
  owner: string;
package/dist/source.js CHANGED
@@ -1,4 +1,4 @@
1
- // Code links, pure: no Node import, so the React and SVG entries stay browser-safe. `verify` in verify.ts reads the disk.
1
+ // No Node import: the React and SVG entries must stay browser-safe.
2
2
  import { edgeId, nodes, toBeat } from './model.js';
3
3
  /** `path` or `path#symbol`. No spaces, one `#` at most, both parts non-empty. `null` for a bad form. */
4
4
  export function parseSource(s) {
@@ -7,7 +7,7 @@ export function parseSource(s) {
7
7
  return null;
8
8
  return m[2] ? { path: m[1], symbol: m[2] } : { path: m[1] };
9
9
  }
10
- /** The GitHub anchor of a heading: lower case, only letters, digits, spaces and hyphens, spaces to hyphens. */
10
+ // GitHub anchor rules: drop other characters, then spaces become hyphens.
11
11
  export const headingSlug = (text) => text
12
12
  .toLowerCase()
13
13
  .replace(/[^\p{L}\p{N} -]/gu, '')
package/dist/svg.d.ts CHANGED
@@ -9,25 +9,22 @@ import { type FigTheme, type FlowProps } from './model.ts';
9
9
  export { LIGHT, DARK } from './model.ts';
10
10
  /** Options for `toSvg`, `render` and `check`. */
11
11
  export type SvgOptions = {
12
- /** Milliseconds a packet takes to cross one edge, in place of `FlowProps.speed`. Default: `FlowProps.speed`, else 900. */
12
+ /** Milliseconds a packet takes to cross one edge. Default: `FlowProps.speed`, else 900. */
13
13
  speed?: number;
14
14
  /** Space around the figure in px. Default: 24. */
15
15
  padding?: number;
16
- /** Colors, in place of `FlowProps.theme`. Default: `FlowProps.theme`. */
16
+ /** Colors. Default: `FlowProps.theme`. */
17
17
  theme?: FigTheme;
18
- /** Lanes: the page width and the smallest text that decide when the time columns wrap into blocks. Default: 830 and 10, as in `check`. */
18
+ /** Lanes: page width and smallest text size that set the wrap. Default: 830 and 10. */
19
19
  width?: number;
20
20
  minText?: number;
21
21
  };
22
- /**
23
- * The SVG string, and the scene it drew. The scene lets `flowfig check` test the same layout the SVG uses.
24
- * The scene is for tools, and its shape may change. Use `toSvg` if you only need the SVG.
25
- */
22
+ /** The SVG string and the scene it drew. The scene shape may change; use `toSvg` for the SVG only. */
26
23
  export declare function render(fig: FlowProps, opts?: SvgOptions): {
27
24
  svg: string;
28
25
  scene: Scene;
29
26
  };
30
27
  /** One self-contained animated SVG string for the figure. Needs no React and no browser. */
31
28
  export declare function toSvg(fig: FlowProps, opts?: SvgOptions): string;
32
- /** Every fault `flowfig check` knows about, for this figure as the SVG lays it out. A `Finding` has a `rule` name, a `severity`, the ids it names and a message. */
29
+ /** Every fault `flowfig check` knows about, for this figure as the SVG lays it out. */
33
30
  export declare function check(fig: FlowProps, opts?: SvgOptions & CheckOptions): Finding[];