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/init.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// `flowfig init`: write flowfig instructions for the coding agents of a repo. Pure functions where possible, so the tests need no TTY.
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
6
|
import { AGENT_TEXT, SKILL_DESCRIPTION } from './guide.js';
|
|
7
|
+
import { runPicker } from './picker.js';
|
|
7
8
|
const START = '<!-- flowfig:start -->';
|
|
8
9
|
const END = '<!-- flowfig:end -->';
|
|
9
10
|
export const MCP_ENTRY = { command: 'npx', args: ['flowfig', 'mcp'] };
|
|
@@ -15,6 +16,10 @@ export const AGENTS = [
|
|
|
15
16
|
file: '.claude/skills/figure/SKILL.md',
|
|
16
17
|
kind: 'whole',
|
|
17
18
|
mcp: { file: '.mcp.json', key: 'mcpServers', type: true, global: '.claude.json' },
|
|
19
|
+
also: {
|
|
20
|
+
file: 'CLAUDE.md',
|
|
21
|
+
text: '## Diagrams\nDraw every diagram with the `figure` skill (flowfig). The user can run `/figure <question>`. Do not answer a diagram request with a Mermaid block or ASCII art, unless the user asks for that tool.',
|
|
22
|
+
},
|
|
18
23
|
head: `---\nname: figure\ndescription: ${JSON.stringify(SKILL_DESCRIPTION)}\nuser_invocable: true\n---\n\n`,
|
|
19
24
|
},
|
|
20
25
|
{ id: 'agents', name: 'AGENTS.md', marks: ['AGENTS.md'], file: 'AGENTS.md', kind: 'section', note: 'Codex, Amp, Jules, ...' },
|
|
@@ -87,12 +92,16 @@ export function renderList(selected, detected, dir) {
|
|
|
87
92
|
'Type numbers to toggle (for example "1 3"), Enter to write, q to quit.',
|
|
88
93
|
].join('\n');
|
|
89
94
|
}
|
|
90
|
-
const block = () => `${START}\n${
|
|
95
|
+
const block = (text = AGENT_TEXT) => `${START}\n${text.trimEnd()}\n${END}\n`;
|
|
91
96
|
/** The new file text, or `undefined` if the file belongs to the user and init must leave it. `old` is `undefined` for a missing file. */
|
|
92
97
|
export function place(a, old) {
|
|
93
98
|
const mine = block();
|
|
94
99
|
if (a.kind === 'whole')
|
|
95
100
|
return old === undefined || old.includes(START) ? `${a.head ?? ''}${mine}` : undefined;
|
|
101
|
+
return section(old, mine);
|
|
102
|
+
}
|
|
103
|
+
/** `old` with the marked block `mine` added or replaced. `undefined` if the block is open. */
|
|
104
|
+
function section(old, mine) {
|
|
96
105
|
if (old === undefined)
|
|
97
106
|
return mine;
|
|
98
107
|
const s = old.indexOf(START), e = old.indexOf(END, s);
|
|
@@ -124,12 +133,17 @@ export function registerMcp(a, old) {
|
|
|
124
133
|
json[a.mcp.key] = { ...servers, flowfig: entry };
|
|
125
134
|
return JSON.stringify(json, null, 2) + '\n';
|
|
126
135
|
}
|
|
136
|
+
/** `✓ ` before a finished write. */
|
|
137
|
+
const mark = (status) => (/^(created|updated|unchanged)$/.test(status) ? '✓ ' : '');
|
|
138
|
+
/** The path to show: relative to the repo folder, or `~` for the home folder. */
|
|
139
|
+
const shown = (path, base, global) => (global ? `~/${relative(base, path)}` : relative(base, path));
|
|
127
140
|
const HINT = 'run with -y or --agents <ids>';
|
|
128
141
|
/** Run `flowfig init`. Returns the exit code. */
|
|
129
142
|
export async function runInit(argv) {
|
|
130
143
|
const args = [...argv];
|
|
131
144
|
const flag = (n, ...alias) => [n, ...alias].some((x) => args.includes(x) && args.splice(args.indexOf(x), 1).length > 0);
|
|
132
|
-
const listAgents = flag('--list-agents'), all = flag('--all-agents'), yes = flag('-y', '--yes'), global = flag('--global'), dry = flag('--dry-run')
|
|
145
|
+
const listAgents = flag('--list-agents'), all = flag('--all-agents'), yes = flag('-y', '--yes'), global = flag('--global'), dry = flag('--dry-run');
|
|
146
|
+
let noMcp = flag('--no-mcp');
|
|
133
147
|
let ids;
|
|
134
148
|
const at = args.indexOf('--agents');
|
|
135
149
|
if (at !== -1)
|
|
@@ -165,30 +179,50 @@ export async function runInit(argv) {
|
|
|
165
179
|
return fail(HINT);
|
|
166
180
|
}
|
|
167
181
|
else {
|
|
168
|
-
const
|
|
169
|
-
|
|
182
|
+
const rows = AGENTS.map((a) => ({
|
|
183
|
+
id: a.id,
|
|
184
|
+
name: a.name,
|
|
185
|
+
detected: detected.includes(a.id),
|
|
186
|
+
files: [...new Set([a.file, a.also?.file, a.mcp?.file].filter((f) => !!f))],
|
|
187
|
+
}));
|
|
188
|
+
const got = await runPicker(rows, auto, !noMcp);
|
|
189
|
+
if (got === 'fallback') {
|
|
190
|
+
const picked = await pick(auto, detected, dir);
|
|
191
|
+
if (!picked)
|
|
192
|
+
return 0;
|
|
193
|
+
chosen = picked;
|
|
194
|
+
}
|
|
195
|
+
else if (!got)
|
|
170
196
|
return 0;
|
|
171
|
-
|
|
197
|
+
else {
|
|
198
|
+
chosen = got.ids;
|
|
199
|
+
noMcp = !got.mcp;
|
|
200
|
+
}
|
|
172
201
|
}
|
|
173
202
|
for (const a of AGENTS.filter((x) => chosen.has(x.id))) {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
status
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
status =
|
|
203
|
+
const files = [{ file: a.file, place: (o) => place(a, o) }];
|
|
204
|
+
if (a.also && !global)
|
|
205
|
+
files.push({ file: a.also.file, place: (o) => section(o, block(a.also.text)) });
|
|
206
|
+
for (const f of files) {
|
|
207
|
+
const path = global ? join(homedir(), f.file) : join(dir, f.file);
|
|
208
|
+
const old = existsSync(path) ? readFileSync(path, 'utf8') : undefined;
|
|
209
|
+
const next = f.place(old);
|
|
210
|
+
let status;
|
|
211
|
+
if (next === undefined)
|
|
212
|
+
status = old?.includes(START) ? 'skipped (flowfig:start without flowfig:end)' : 'skipped (the file exists and is not from flowfig)';
|
|
213
|
+
else if (next === old)
|
|
214
|
+
status = 'unchanged';
|
|
186
215
|
else {
|
|
187
|
-
|
|
188
|
-
|
|
216
|
+
status = old === undefined ? 'created' : 'updated';
|
|
217
|
+
if (dry)
|
|
218
|
+
status = `would ${status === 'created' ? 'create' : 'update'}`;
|
|
219
|
+
else {
|
|
220
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
221
|
+
writeFileSync(path, next);
|
|
222
|
+
}
|
|
189
223
|
}
|
|
224
|
+
console.log(`${mark(status)}${status.padEnd(9)} ${shown(path, global ? homedir() : dir, global)}`);
|
|
190
225
|
}
|
|
191
|
-
console.log(`${status.padEnd(9)} ${path}`);
|
|
192
226
|
if (noMcp)
|
|
193
227
|
continue;
|
|
194
228
|
if (a.mcpNote) {
|
|
@@ -227,8 +261,10 @@ export async function runInit(argv) {
|
|
|
227
261
|
}
|
|
228
262
|
}
|
|
229
263
|
}
|
|
230
|
-
console.log(`${mcpStatus.padEnd(9)} ${mcpPath}`);
|
|
264
|
+
console.log(`${mark(mcpStatus)}${mcpStatus.padEnd(9)} ${shown(mcpPath, global ? homedir() : dir, global)}`);
|
|
231
265
|
}
|
|
266
|
+
if (!dry)
|
|
267
|
+
console.log(`Next: start a new agent session.${chosen.has('claude') ? ' In Claude Code, run /figure how does login work.' : ''}`);
|
|
232
268
|
return 0;
|
|
233
269
|
}
|
|
234
270
|
function fail(message) {
|
package/dist/mcp.js
CHANGED
|
@@ -100,7 +100,7 @@ function run(name, args) {
|
|
|
100
100
|
if (name === 'check' || errors)
|
|
101
101
|
return ok(lines.join('\n'), errors);
|
|
102
102
|
const out = String(args.out);
|
|
103
|
-
const svg = svgWithSpec(props);
|
|
103
|
+
const svg = svgWithSpec(props, opts);
|
|
104
104
|
try {
|
|
105
105
|
mkdirSync(dirname(out), { recursive: true });
|
|
106
106
|
writeFileSync(out, svg);
|
package/dist/model.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Rect } from './geometry.ts';
|
|
2
3
|
/** A box in the figure. A box with a `show` in some step becomes a content card: it grows to fit the most any step puts in it. */
|
|
3
4
|
export type FigNode = {
|
|
4
5
|
/** Name that edges, steps and beats use to point at this box. Must be unique in the figure. */
|
|
@@ -13,8 +14,16 @@ export type FigNode = {
|
|
|
13
14
|
lines?: number;
|
|
14
15
|
/** Width in px. Overrides the width the layout picks. */
|
|
15
16
|
width?: number;
|
|
17
|
+
/** In a `lanes` figure: the time column of the box, 0-based. Default: the box's first appearance in the steps. */
|
|
18
|
+
at?: number;
|
|
19
|
+
/** In a `timeline` figure: the start of the item, or the date of a milestone, as YYYY-MM-DD. */
|
|
20
|
+
from?: string;
|
|
21
|
+
/** In a `timeline` figure: the last day of the item, as YYYY-MM-DD. The day counts. Without it the box is a milestone. */
|
|
22
|
+
to?: string;
|
|
16
23
|
/** A permanent state color for the box, such as a failing part. It gives a 1 px border and a light tint. An arrival with no hop tone uses it too. */
|
|
17
24
|
tone?: FigTone;
|
|
25
|
+
/** A lifecycle mark: `start` draws a filled dot before the box, `end` a ringed dot after it. No layout change. */
|
|
26
|
+
mark?: 'start' | 'end';
|
|
18
27
|
/** The code this draws, `path` or `path#symbol`, relative to the repo root. `flowfig verify` checks it. */
|
|
19
28
|
source?: string;
|
|
20
29
|
};
|
|
@@ -75,6 +84,8 @@ export type FigBeat = {
|
|
|
75
84
|
show?: Record<string, FigContent>;
|
|
76
85
|
/** Ids of boxes to highlight for this beat, without a packet or a content card. */
|
|
77
86
|
light?: string[];
|
|
87
|
+
/** Ids of boxes that take the active look for this beat: a 2 px border, a tint and a glow. The boxes in `light` keep the trail look. */
|
|
88
|
+
focus?: string[];
|
|
78
89
|
/** How long the beat lasts in ms. The packet crosses in `speed`; the rest is a hold. An explicit `ms` wins as written. Default: `speed` plus the time to read `say`. */
|
|
79
90
|
ms?: number;
|
|
80
91
|
};
|
|
@@ -128,6 +139,12 @@ export type FlowProps = {
|
|
|
128
139
|
check?: boolean;
|
|
129
140
|
/** Draw the steps as a lifeline rail under the map: one row for each message, with its payload. `'only'` draws the rail without the map. Default: `false`. */
|
|
130
141
|
rail?: boolean | 'only';
|
|
142
|
+
/** Draw the layout as swimlanes: a `column` group of labeled groups, one per role, with the boxes in time order left to right. Default: off. */
|
|
143
|
+
lanes?: true;
|
|
144
|
+
/** Draw the layout as a timeline: one labeled group per track, with the boxes placed by their `from` and `to` dates. Default: off. */
|
|
145
|
+
timeline?: true;
|
|
146
|
+
/** In a `timeline` figure: the date of the fixed today marker, as YYYY-MM-DD. With no `today`, the figure has no marker. */
|
|
147
|
+
today?: string;
|
|
131
148
|
};
|
|
132
149
|
/** A figure file's default export: a title, a source note and the props. */
|
|
133
150
|
export type Figure = {
|
|
@@ -195,9 +212,172 @@ export declare const str: (n: unknown) => string;
|
|
|
195
212
|
export declare const readMs: (say: unknown) => number;
|
|
196
213
|
/** How long a beat lasts. An explicit `ms` wins as written. Else the beat lasts `speed` plus the time to read `say`. */
|
|
197
214
|
export declare const beatMs: (b: Beat, speed: number) => number;
|
|
215
|
+
/** The item the playhead rests on at beat `i`: the first dated id in the latest focus up to `i`, or undefined for home. */
|
|
216
|
+
export declare const playheadItem: <T extends {
|
|
217
|
+
id: string;
|
|
218
|
+
}>(items: T[], beats: {
|
|
219
|
+
focus?: string[];
|
|
220
|
+
}[], i: number) => T | undefined;
|
|
221
|
+
/** The box of a timeline label that sits beside its bar, in the bar's coordinates. */
|
|
222
|
+
export declare const outsideLabelRect: (bar: {
|
|
223
|
+
x: number;
|
|
224
|
+
y: number;
|
|
225
|
+
w: number;
|
|
226
|
+
h: number;
|
|
227
|
+
}, label: string) => {
|
|
228
|
+
x: number;
|
|
229
|
+
y: number;
|
|
230
|
+
w: number;
|
|
231
|
+
h: number;
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* The time span in which the playhead label shows its date. The label waits for the line to arrive: it stays hidden for
|
|
235
|
+
* the ramp (the move), so it never shows a date at a place that is not that date, and it never meets "today" on the way.
|
|
236
|
+
*/
|
|
237
|
+
export declare const labelSpan: (t0: number, end: number, ramp: number) => [number, number];
|
|
238
|
+
/** True if a playhead date label at x would overlap the "today" label, so it moves to the top row. Widths are in px. */
|
|
239
|
+
export declare const dateLabelRaised: (x: number, today: number | null | undefined, w: number, todayW: number) => boolean;
|
|
198
240
|
/** The hold at the end of each step, before the next step starts. */
|
|
199
241
|
export declare const STEP_HOLD_MS = 2000;
|
|
200
242
|
export declare const nodes: (g: FigGroup) => FigNode[];
|
|
243
|
+
/** Lane layout constants, shared by both renderers: the gap between time columns, the padding inside a lane, and the gap between lanes. */
|
|
244
|
+
export declare const LANE_GAP = 56, LANE_PAD = 24, LANE_ROW_GAP = 20;
|
|
245
|
+
/** True if the layout can draw as lanes: a `column` group whose every child is a labeled group with boxes only. */
|
|
246
|
+
export declare const isLanesLayout: (g: FigGroup) => boolean;
|
|
247
|
+
/** True if `at` is a usable time column: an integer of 0 or more. */
|
|
248
|
+
export declare const validAt: (at: unknown) => at is number;
|
|
249
|
+
/** The time column of each box in a lanes figure: its first appearance in the steps (from, then to; a back hop to, then from),
|
|
250
|
+
* then the boxes no step touches in layout order. `at` overrides. Two boxes may share a column. */
|
|
251
|
+
export declare function laneColumns(fig: FlowProps): Map<string, number>;
|
|
252
|
+
/** The side of an SVG frame, and the width limits of a box with no card. The lane split measures with them. */
|
|
253
|
+
export declare const FRAME_SIDE = 18, NODE_MIN_W = 100, NODE_MAX_W = 190;
|
|
254
|
+
/** The gap between two blocks of wrapped lanes. */
|
|
255
|
+
export declare const LANE_BLOCK_GAP = 40;
|
|
256
|
+
/** The length of a stub line between a box and its pill, the clear space past a pill, and the room a lane grows by for a pill. */
|
|
257
|
+
export declare const STUB = 12, STUB_CLEAR = 8, STUB_ROOM = 30;
|
|
258
|
+
/** The width of a box as the SVG draws it, before a diamond adds its 70 px. */
|
|
259
|
+
export declare function nodeWidth(item: FigNode, carded: boolean): number;
|
|
260
|
+
/** The room a diamond of size w by h gives a text line whose far edge sits `far` px from the center: the width of the outline there, less 8 px. */
|
|
261
|
+
export declare const diamondRoom: (w: number, h: number, far: number) => number;
|
|
262
|
+
/** The lines of a diamond with no card. The sub wraps to the room the outline gives its last line; the diamond grows taller by 15 px
|
|
263
|
+
* for each line until the lines fit. `dy` is the baseline of each line below the center, as the SVG draws it: the label sits 2 px
|
|
264
|
+
* above the center over a one-line sub, and 5 px below it with no sub. `far` is the distance from the center to the far edge of the text. */
|
|
265
|
+
export declare function diamondLines(item: FigNode, w: number): {
|
|
266
|
+
h: number;
|
|
267
|
+
label: {
|
|
268
|
+
dy: number;
|
|
269
|
+
far: number;
|
|
270
|
+
};
|
|
271
|
+
subs: {
|
|
272
|
+
text: string;
|
|
273
|
+
dy: number;
|
|
274
|
+
far: number;
|
|
275
|
+
}[];
|
|
276
|
+
};
|
|
277
|
+
/** The lane copies (`laneIndex@block`) that hold a stub end with no place clear of every edge path. A renderer grows each by
|
|
278
|
+
* STUB_ROOM and routes again: the pill then fits under or over its box, inside the band. Both renderers call it with their routes. */
|
|
279
|
+
export declare function tightCopies(lanes: FigGroup[], ends: Map<string, [number, number]>, edges: FlowProps['edges'], routed: {
|
|
280
|
+
id: string;
|
|
281
|
+
stub?: {
|
|
282
|
+
tight: [boolean, boolean];
|
|
283
|
+
};
|
|
284
|
+
}[]): Set<string>;
|
|
285
|
+
/** The width of the label gutter of lanes and of a timeline: the widest track label plus the frame sides. */
|
|
286
|
+
export declare const laneGutter: (lanes: FigGroup[]) => number;
|
|
287
|
+
/** How a lanes figure wraps. Both renderers lay out with it, so the player shows the blocks and stubs of the SVG. */
|
|
288
|
+
export type LanePlan = {
|
|
289
|
+
/** The time column of each box. */
|
|
290
|
+
cols: Map<string, number>;
|
|
291
|
+
/** The first column of each block, then the column count: block k holds columns starts[k] to starts[k + 1] - 1. */
|
|
292
|
+
starts: number[];
|
|
293
|
+
/** The most columns in one block. */
|
|
294
|
+
per: number;
|
|
295
|
+
/** The blocks that hold a box, in order. A block with no box is not drawn. A figure with no box keeps block 0. */
|
|
296
|
+
blocks: number[];
|
|
297
|
+
/** The width of the label gutter, the same in every block. */
|
|
298
|
+
gutter: number;
|
|
299
|
+
/** The space after each column: the gap to the next column of its block, or the right margin of the band for the last one. */
|
|
300
|
+
gaps: number[];
|
|
301
|
+
/** The room before the first column of each block, after the gutter. */
|
|
302
|
+
lead: number[];
|
|
303
|
+
/** The block of the source and of the target of each edge, by edge id. A lane end names the copy of the lane in that block. */
|
|
304
|
+
ends: Map<string, [number, number]>;
|
|
305
|
+
/** The edges whose ends sit in two blocks, by edge id: the pill texts at the source and at the target, then a shorter source
|
|
306
|
+
* text without the edge label, if the edge has one. */
|
|
307
|
+
stubs: Map<string, string[]>;
|
|
308
|
+
/** Edges between two boxes of one lane and one block, with a box of that lane between them and no `around` of their own.
|
|
309
|
+
* A straight route would cross that box, so the renderers route them around below. */
|
|
310
|
+
around: Set<string>;
|
|
311
|
+
/** The lane copies (`laneIndex@block`) that grow by STUB_ROOM at the bottom: a stub pill there has no room beside its box. */
|
|
312
|
+
tall: Set<string>;
|
|
313
|
+
/** Edges with a lane end that has no block on the side of the other end. They use the nearest block; `check` reports them. */
|
|
314
|
+
lost: string[];
|
|
315
|
+
};
|
|
316
|
+
/** The block of a time column, from the block starts of a plan. */
|
|
317
|
+
export declare const laneBlock: (starts: number[], c: number) => number;
|
|
318
|
+
/** The pill texts of a cross-block edge: "label → Target" at the source, "from Source" at the target, and "→ Target", a
|
|
319
|
+
* shorter source text for a pill that has no place for the long one. */
|
|
320
|
+
export declare const stubTexts: (label: unknown, from: unknown, to: unknown) => string[];
|
|
321
|
+
/**
|
|
322
|
+
* The wrap of a lanes figure. If all the time columns do not fit `width` with text at `minText` or more, the lanes wrap like a
|
|
323
|
+
* line of text: block 1 holds columns 0..n-1, block 2 holds n..2n-1. n is the largest count that fits; it is 1 at least.
|
|
324
|
+
* The text test is the one `check` runs: the smallest text (11 px, or 10.5 px with a mono card row) times width / figure width.
|
|
325
|
+
* If the wrap cannot make the text readable, the lanes do not wrap: the smallest font is under `minText`, or `floor` (the least
|
|
326
|
+
* width of the rest of the figure, such as the rail) is already too wide.
|
|
327
|
+
* The blocks share the columns as evenly as they can, and a block holds 2 columns at least: if 2 do not fit, the lanes keep
|
|
328
|
+
* one block. An edge between two blocks becomes two stubs with pills. A pill takes no room of its own, so n depends on the boxes.
|
|
329
|
+
*/
|
|
330
|
+
export declare function lanePlan(fig: FlowProps, { width, minText, padding, floor }?: {
|
|
331
|
+
floor?: number | undefined;
|
|
332
|
+
minText?: number | undefined;
|
|
333
|
+
padding?: number | undefined;
|
|
334
|
+
width?: number | undefined;
|
|
335
|
+
}): LanePlan;
|
|
336
|
+
/**
|
|
337
|
+
* The rect id that an edge end routes to in wrapped lanes. A box keeps its id. A lane has one band per block, so a lane end names
|
|
338
|
+
* the band in the block from the plan (`band` finds it). A stub end at a lane is a point inside that band: the source at the band
|
|
339
|
+
* end, past the room for its pill, and the target at the first column of the block. The function adds the rect it names to `rects`.
|
|
340
|
+
*/
|
|
341
|
+
export declare function laneEnd(plan: LanePlan, edge: string, id: string, start: boolean, band: (lane: string, block: number) => Rect | undefined, rects: Record<string, Rect>): string;
|
|
342
|
+
/** Timeline constants, shared by both renderers: the bar height, the gap between rows, the least bar width, the milestone width,
|
|
343
|
+
* the axis strip height and the axis width of the SVG. */
|
|
344
|
+
export declare const TL_BAR_H = 28, TL_ROW_GAP = 8, TL_MIN_BAR = 24, TL_DIAMOND = 14, TL_AXIS_H = 28, TL_AXIS_W = 640;
|
|
345
|
+
/** The days since 1970-01-01 (UTC) of a YYYY-MM-DD date. It is null for any other text and for a date that does not exist. */
|
|
346
|
+
export declare const dayOf: (iso: string) => number | null;
|
|
347
|
+
export type TimelineItem = {
|
|
348
|
+
id: string;
|
|
349
|
+
track: number;
|
|
350
|
+
row: number;
|
|
351
|
+
x: number;
|
|
352
|
+
w: number;
|
|
353
|
+
milestone: boolean;
|
|
354
|
+
labelInside: boolean;
|
|
355
|
+
/** The `from` date in the d MMM form, for the playhead label. */
|
|
356
|
+
date: string;
|
|
357
|
+
};
|
|
358
|
+
export type TimelineLayout = {
|
|
359
|
+
/** The range in days, rounded out to whole weeks: the Monday of the first week and the Sunday of the last week. */
|
|
360
|
+
start: number;
|
|
361
|
+
end: number;
|
|
362
|
+
/** The week ticks (`W41`) or the month ticks (`Oct`). */
|
|
363
|
+
ticks: {
|
|
364
|
+
x: number;
|
|
365
|
+
label: string;
|
|
366
|
+
}[];
|
|
367
|
+
/** Bars and milestones. `x` and `w` are px inside the axis; `row` is the stack row inside the track. */
|
|
368
|
+
items: TimelineItem[];
|
|
369
|
+
/** The row count of each track. */
|
|
370
|
+
rows: number[];
|
|
371
|
+
/** The x of the today marker. It is null when the figure has no valid `today`. */
|
|
372
|
+
today: number | null;
|
|
373
|
+
/** The x and the d MMM date of the last day: the playhead rests there in the step hold. The x is null when the figure has no dated item. */
|
|
374
|
+
last: number | null;
|
|
375
|
+
lastDate: string;
|
|
376
|
+
};
|
|
377
|
+
/** Where each dated item, tick and the today line sit on the axis, `axisWidth` px wide. Both renderers call it. */
|
|
378
|
+
export declare function timelineLayout(fig: FlowProps, axisWidth: number): TimelineLayout;
|
|
379
|
+
/** One step, "timeline", with one beat for each dated item in date order. The beat lights the item and says "label, d MMM to d MMM" (a milestone: "label, d MMM"), then " · sub". */
|
|
380
|
+
export declare function timelineBeats(fig: FlowProps): FigStep[];
|
|
201
381
|
/** The part counts of a spec, for the line that `flowfig check` prints. An agent copies the line into its reply. */
|
|
202
382
|
export declare const counts: (p: FlowProps) => {
|
|
203
383
|
boxes: number;
|