@flyingrobots/bijou 0.2.0 → 0.4.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 CHANGED
@@ -4,11 +4,14 @@ Themed terminal components for CLIs, loggers, and scripts — graceful degradati
4
4
 
5
5
  **Zero dependencies. Hexagonal architecture. Works everywhere.**
6
6
 
7
- ## What's New in 0.2.0?
7
+ ## What's New in 0.4.0?
8
8
 
9
- - **`IOPort.onResize()`** — new port method for terminal resize events, enabling TUI apps to reflow layout when the terminal is resized
9
+ - **`textarea()`** — multi-line text input form with cursor navigation, line numbers, placeholder, maxLength
10
+ - **`filter()`** — fuzzy-filter select form with real-time search by label and keywords
11
+ - **`dagLayout()`** — returns node position map alongside rendered output, for interactive DAG navigation
12
+ - **`dag()` `selectedId`** — cursor-style node highlighting with highest priority over highlight path
10
13
 
11
- See the [CHANGELOG](https://github.com/flyingrobots/bijou/blob/main/CHANGELOG.md) for the full release history.
14
+ See the [CHANGELOG](https://github.com/flyingrobots/bijou/blob/main/docs/CHANGELOG.md) for the full release history.
12
15
 
13
16
  ## Install
14
17
 
@@ -0,0 +1,43 @@
1
+ import type { BijouContext } from '../../ports/context.js';
2
+ import type { TokenValue } from '../theme/tokens.js';
3
+ export interface DagNode {
4
+ id: string;
5
+ label: string;
6
+ edges?: string[];
7
+ badge?: string;
8
+ token?: TokenValue;
9
+ /** @internal Used by dagSlice to mark ghost boundary nodes */
10
+ _ghost?: boolean;
11
+ _ghostLabel?: string;
12
+ }
13
+ export interface DagOptions {
14
+ nodeToken?: TokenValue;
15
+ edgeToken?: TokenValue;
16
+ highlightPath?: string[];
17
+ highlightToken?: TokenValue;
18
+ selectedId?: string;
19
+ selectedToken?: TokenValue;
20
+ nodeWidth?: number;
21
+ maxWidth?: number;
22
+ direction?: 'down' | 'right';
23
+ ctx?: BijouContext;
24
+ }
25
+ export interface DagNodePosition {
26
+ readonly row: number;
27
+ readonly col: number;
28
+ readonly width: number;
29
+ readonly height: number;
30
+ }
31
+ export interface DagLayout {
32
+ readonly output: string;
33
+ readonly nodes: ReadonlyMap<string, DagNodePosition>;
34
+ readonly width: number;
35
+ readonly height: number;
36
+ }
37
+ export declare function dagSlice(nodes: DagNode[], focus: string, opts?: {
38
+ direction?: 'ancestors' | 'descendants' | 'both';
39
+ depth?: number;
40
+ }): DagNode[];
41
+ export declare function dagLayout(nodes: DagNode[], options?: DagOptions): DagLayout;
42
+ export declare function dag(nodes: DagNode[], options?: DagOptions): string;
43
+ //# sourceMappingURL=dag.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dag.d.ts","sourceRoot":"","sources":["../../../src/core/components/dag.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAKrD,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,EAAE,UAAU,CAAC;IACvB,SAAS,CAAC,EAAE,UAAU,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,cAAc,CAAC,EAAE,UAAU,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,UAAU,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC7B,GAAG,CAAC,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACrD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAiiBD,wBAAgB,QAAQ,CACtB,KAAK,EAAE,OAAO,EAAE,EAChB,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IACL,SAAS,CAAC,EAAE,WAAW,GAAG,aAAa,GAAG,MAAM,CAAC;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,OAAO,EAAE,CAuHX;AAID,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,OAAO,GAAE,UAAe,GAAG,SAAS,CAK/E;AAID,wBAAgB,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,OAAO,GAAE,UAAe,GAAG,MAAM,CAgBtE"}
@@ -0,0 +1,626 @@
1
+ import { getDefaultContext } from '../../context.js';
2
+ // ── Helpers ────────────────────────────────────────────────────────
3
+ function resolveCtx(ctx) {
4
+ if (ctx)
5
+ return ctx;
6
+ return getDefaultContext();
7
+ }
8
+ function visibleLength(str) {
9
+ // eslint-disable-next-line no-control-regex
10
+ return str.replace(/\x1b\[[0-9;]*m/g, '').length;
11
+ }
12
+ function truncateLabel(text, maxLen) {
13
+ if (maxLen <= 0)
14
+ return '';
15
+ if (visibleLength(text) <= maxLen)
16
+ return text;
17
+ return text.slice(0, maxLen - 1) + '\u2026';
18
+ }
19
+ // ── Layout: Layer Assignment ───────────────────────────────────────
20
+ function assignLayers(nodes) {
21
+ const children = new Map();
22
+ const parents = new Map();
23
+ const inDegree = new Map();
24
+ const nodeIds = new Set(nodes.map(n => n.id));
25
+ for (const n of nodes) {
26
+ // Filter edges to only include targets that exist in the graph
27
+ children.set(n.id, (n.edges ?? []).filter(e => nodeIds.has(e)));
28
+ inDegree.set(n.id, 0);
29
+ if (!parents.has(n.id))
30
+ parents.set(n.id, []);
31
+ }
32
+ for (const n of nodes) {
33
+ for (const childId of children.get(n.id) ?? []) {
34
+ if (!parents.has(childId))
35
+ parents.set(childId, []);
36
+ parents.get(childId).push(n.id);
37
+ inDegree.set(childId, (inDegree.get(childId) ?? 0) + 1);
38
+ }
39
+ }
40
+ // Kahn's topological sort
41
+ const queue = [];
42
+ for (const [id, deg] of inDegree) {
43
+ if (deg === 0)
44
+ queue.push(id);
45
+ }
46
+ const topoOrder = [];
47
+ const visited = new Set();
48
+ while (queue.length > 0) {
49
+ const id = queue.shift();
50
+ if (visited.has(id))
51
+ continue;
52
+ visited.add(id);
53
+ topoOrder.push(id);
54
+ for (const childId of children.get(id) ?? []) {
55
+ const newDeg = (inDegree.get(childId) ?? 1) - 1;
56
+ inDegree.set(childId, newDeg);
57
+ if (newDeg === 0)
58
+ queue.push(childId);
59
+ }
60
+ }
61
+ if (topoOrder.length !== nodes.length) {
62
+ throw new Error('[bijou] dag(): cycle detected in graph');
63
+ }
64
+ // Longest-path layer assignment
65
+ const layerMap = new Map();
66
+ for (const id of topoOrder) {
67
+ const pars = parents.get(id) ?? [];
68
+ if (pars.length === 0) {
69
+ layerMap.set(id, 0);
70
+ }
71
+ else {
72
+ let maxParent = 0;
73
+ for (const p of pars) {
74
+ maxParent = Math.max(maxParent, layerMap.get(p) ?? 0);
75
+ }
76
+ layerMap.set(id, maxParent + 1);
77
+ }
78
+ }
79
+ return layerMap;
80
+ }
81
+ // ── Layout: Column Ordering ────────────────────────────────────────
82
+ function buildLayerArrays(nodes, layerMap) {
83
+ let maxLayer = 0;
84
+ for (const v of layerMap.values()) {
85
+ if (v > maxLayer)
86
+ maxLayer = v;
87
+ }
88
+ const layers = Array.from({ length: maxLayer + 1 }, () => []);
89
+ for (const n of nodes) {
90
+ const l = layerMap.get(n.id);
91
+ if (l !== undefined)
92
+ layers[l].push(n.id);
93
+ }
94
+ return layers;
95
+ }
96
+ function orderColumns(layers, nodes) {
97
+ const childrenMap = new Map();
98
+ const parentsMap = new Map();
99
+ for (const n of nodes) {
100
+ childrenMap.set(n.id, n.edges ?? []);
101
+ if (!parentsMap.has(n.id))
102
+ parentsMap.set(n.id, []);
103
+ }
104
+ for (const n of nodes) {
105
+ for (const c of n.edges ?? []) {
106
+ if (!parentsMap.has(c))
107
+ parentsMap.set(c, []);
108
+ parentsMap.get(c).push(n.id);
109
+ }
110
+ }
111
+ // Top-down pass
112
+ for (let l = 1; l < layers.length; l++) {
113
+ const prevLayer = layers[l - 1];
114
+ const curLayer = layers[l];
115
+ const prevIndex = new Map();
116
+ for (let i = 0; i < prevLayer.length; i++) {
117
+ prevIndex.set(prevLayer[i], i);
118
+ }
119
+ const bary = new Map();
120
+ for (const id of curLayer) {
121
+ const pars = (parentsMap.get(id) ?? []).filter(p => prevIndex.has(p));
122
+ if (pars.length === 0) {
123
+ bary.set(id, Infinity);
124
+ }
125
+ else {
126
+ const avg = pars.reduce((s, p) => s + (prevIndex.get(p) ?? 0), 0) / pars.length;
127
+ bary.set(id, avg);
128
+ }
129
+ }
130
+ curLayer.sort((a, b) => (bary.get(a) ?? Infinity) - (bary.get(b) ?? Infinity));
131
+ }
132
+ // Bottom-up pass
133
+ for (let l = layers.length - 2; l >= 0; l--) {
134
+ const nextLayer = layers[l + 1];
135
+ const curLayer = layers[l];
136
+ const nextIndex = new Map();
137
+ for (let i = 0; i < nextLayer.length; i++) {
138
+ nextIndex.set(nextLayer[i], i);
139
+ }
140
+ const bary = new Map();
141
+ for (const id of curLayer) {
142
+ const chlds = (childrenMap.get(id) ?? []).filter(c => nextIndex.has(c));
143
+ if (chlds.length === 0) {
144
+ bary.set(id, Infinity);
145
+ }
146
+ else {
147
+ const avg = chlds.reduce((s, c) => s + (nextIndex.get(c) ?? 0), 0) / chlds.length;
148
+ bary.set(id, avg);
149
+ }
150
+ }
151
+ curLayer.sort((a, b) => (bary.get(a) ?? Infinity) - (bary.get(b) ?? Infinity));
152
+ }
153
+ }
154
+ const JUNCTION = {
155
+ 'D': '\u2502', 'U': '\u2502', 'DU': '\u2502',
156
+ 'L': '\u2500', 'R': '\u2500', 'LR': '\u2500',
157
+ 'DR': '\u250c', 'DL': '\u2510', 'RU': '\u2514', 'LU': '\u2518',
158
+ 'DRU': '\u251c', 'DLU': '\u2524', 'DLR': '\u252c', 'LRU': '\u2534',
159
+ 'DLRU': '\u253c',
160
+ };
161
+ function junctionChar(dirs) {
162
+ const key = [...dirs].sort().join('');
163
+ return JUNCTION[key] ?? '\u253c';
164
+ }
165
+ function createGrid(rows, cols) {
166
+ const dirs = [];
167
+ for (let r = 0; r < rows; r++) {
168
+ const row = [];
169
+ for (let c = 0; c < cols; c++) {
170
+ row.push(new Set());
171
+ }
172
+ dirs.push(row);
173
+ }
174
+ return { dirs, arrows: new Set(), rows, cols };
175
+ }
176
+ function markDir(g, r, c, ...ds) {
177
+ if (r >= 0 && r < g.rows && c >= 0 && c < g.cols) {
178
+ const cell = g.dirs[r][c];
179
+ for (const d of ds)
180
+ cell.add(d);
181
+ }
182
+ }
183
+ function markEdge(g, fromCol, fromLayer, toCol, toLayer, RS, colCenter) {
184
+ const srcC = colCenter(fromCol);
185
+ const dstC = colCenter(toCol);
186
+ const sRow = fromLayer * RS + 3;
187
+ const dRow = toLayer * RS - 1; // one row above dest box
188
+ const mid = sRow + 1;
189
+ if (srcC === dstC) {
190
+ for (let r = sRow; r < dRow; r++)
191
+ markDir(g, r, srcC, 'D', 'U');
192
+ }
193
+ else {
194
+ markDir(g, sRow, srcC, 'D', 'U');
195
+ markDir(g, mid, srcC, srcC < dstC ? 'R' : 'L', 'U');
196
+ const minC = Math.min(srcC, dstC);
197
+ const maxC = Math.max(srcC, dstC);
198
+ for (let c = minC + 1; c < maxC; c++)
199
+ markDir(g, mid, c, 'L', 'R');
200
+ markDir(g, mid, dstC, srcC < dstC ? 'L' : 'R', 'D');
201
+ for (let r = mid + 1; r < dRow; r++)
202
+ markDir(g, r, dstC, 'D', 'U');
203
+ }
204
+ g.arrows.add(dRow * 10000 + dstC);
205
+ }
206
+ // ── Node Box Rendering ─────────────────────────────────────────────
207
+ function renderNodeBox(label, badgeText, width, ghost) {
208
+ const h = ghost ? '\u254c' : '\u2500';
209
+ const v = ghost ? '\u254e' : '\u2502';
210
+ const innerW = width - 2;
211
+ const contentW = innerW - 2;
212
+ let content;
213
+ if (badgeText) {
214
+ const maxLabelW = contentW - visibleLength(badgeText) - 1;
215
+ const tLabel = truncateLabel(label, maxLabelW);
216
+ const gap = Math.max(1, contentW - visibleLength(tLabel) - visibleLength(badgeText));
217
+ content = tLabel + ' '.repeat(gap) + badgeText;
218
+ }
219
+ else {
220
+ content = truncateLabel(label, contentW);
221
+ }
222
+ const padRight = Math.max(0, contentW - visibleLength(content));
223
+ const top = '\u256d' + h.repeat(innerW) + '\u256e';
224
+ const mid = v + ' ' + content + ' '.repeat(padRight) + ' ' + v;
225
+ const bot = '\u2570' + h.repeat(innerW) + '\u256f';
226
+ return [top, mid, bot];
227
+ }
228
+ // ── Interactive Renderer ───────────────────────────────────────────
229
+ function renderInteractiveLayout(nodes, options, ctx) {
230
+ if (nodes.length === 0)
231
+ return { output: '', nodes: new Map(), width: 0, height: 0 };
232
+ const nodeMap = new Map();
233
+ for (const n of nodes)
234
+ nodeMap.set(n.id, n);
235
+ const layerMap = assignLayers(nodes);
236
+ const layers = buildLayerArrays(nodes, layerMap);
237
+ orderColumns(layers, nodes);
238
+ const colIndex = new Map();
239
+ for (const layer of layers) {
240
+ for (let i = 0; i < layer.length; i++) {
241
+ colIndex.set(layer[i], i);
242
+ }
243
+ }
244
+ let maxNodesPerLayer = 1;
245
+ for (const layer of layers) {
246
+ if (layer.length > maxNodesPerLayer)
247
+ maxNodesPerLayer = layer.length;
248
+ }
249
+ const maxWidth = options.maxWidth ?? ctx.runtime.columns;
250
+ let nodeWidth = options.nodeWidth ?? Math.max(...nodes.map(n => visibleLength(n.label) + (n.badge ? visibleLength(n.badge) + 2 : 0) + 4), 16);
251
+ let gap = 4;
252
+ let colStride = nodeWidth + gap;
253
+ let totalWidth = maxNodesPerLayer * colStride;
254
+ if (totalWidth > maxWidth && !options.nodeWidth) {
255
+ gap = 2;
256
+ colStride = nodeWidth + gap;
257
+ totalWidth = maxNodesPerLayer * colStride;
258
+ }
259
+ if (totalWidth > maxWidth && !options.nodeWidth) {
260
+ nodeWidth = Math.max(16, Math.floor((maxWidth - gap) / maxNodesPerLayer) - gap);
261
+ colStride = nodeWidth + gap;
262
+ totalWidth = maxNodesPerLayer * colStride;
263
+ }
264
+ const RS = 6;
265
+ const gridRows = layers.length * RS;
266
+ const gridCols = totalWidth;
267
+ const colCenter = (c) => c * colStride + Math.floor(nodeWidth / 2);
268
+ const g = createGrid(gridRows, gridCols);
269
+ // Mark edges
270
+ for (const n of nodes) {
271
+ const fLayer = layerMap.get(n.id);
272
+ const fCol = colIndex.get(n.id);
273
+ if (fLayer === undefined || fCol === undefined)
274
+ continue;
275
+ for (const childId of n.edges ?? []) {
276
+ const tLayer = layerMap.get(childId);
277
+ const tCol = colIndex.get(childId);
278
+ if (tLayer === undefined || tCol === undefined)
279
+ continue;
280
+ markEdge(g, fCol, fLayer, tCol, tLayer, RS, colCenter);
281
+ }
282
+ }
283
+ // Build output grids
284
+ const charGrid = [];
285
+ const tokenGrid = [];
286
+ for (let r = 0; r < gridRows; r++) {
287
+ const charRow = [];
288
+ const tokenRow = [];
289
+ for (let c = 0; c < gridCols; c++) {
290
+ charRow.push(' ');
291
+ tokenRow.push(null);
292
+ }
293
+ charGrid.push(charRow);
294
+ tokenGrid.push(tokenRow);
295
+ }
296
+ // Write edge characters
297
+ const edgeToken = options.edgeToken ?? ctx.theme.theme.border.muted;
298
+ for (let r = 0; r < gridRows; r++) {
299
+ for (let c = 0; c < gridCols; c++) {
300
+ const cell = g.dirs[r][c];
301
+ if (cell.size > 0) {
302
+ charGrid[r][c] = junctionChar(cell);
303
+ tokenGrid[r][c] = edgeToken;
304
+ }
305
+ }
306
+ }
307
+ // Write arrowheads
308
+ for (const encoded of g.arrows) {
309
+ const r = Math.floor(encoded / 10000);
310
+ const c = encoded % 10000;
311
+ if (r >= 0 && r < gridRows && c >= 0 && c < gridCols) {
312
+ charGrid[r][c] = '\u25bc';
313
+ tokenGrid[r][c] = edgeToken;
314
+ }
315
+ }
316
+ // Highlight edges
317
+ const highlightSet = new Set(options.highlightPath ?? []);
318
+ if (options.highlightPath && options.highlightToken) {
319
+ const hlToken = options.highlightToken;
320
+ const path = options.highlightPath;
321
+ for (let i = 0; i < path.length - 1; i++) {
322
+ const fromId = path[i];
323
+ const toId = path[i + 1];
324
+ const fLayer = layerMap.get(fromId);
325
+ const tLayer = layerMap.get(toId);
326
+ const fCol = colIndex.get(fromId);
327
+ const tCol = colIndex.get(toId);
328
+ if (fLayer === undefined || tLayer === undefined || fCol === undefined || tCol === undefined)
329
+ continue;
330
+ const srcC = colCenter(fCol);
331
+ const dstC = colCenter(tCol);
332
+ const sRow = fLayer * RS + 3;
333
+ const dRow = tLayer * RS - 1;
334
+ const midRow = sRow + 1;
335
+ if (srcC === dstC) {
336
+ for (let r = sRow; r <= dRow && r < gridRows; r++) {
337
+ if (srcC < gridCols)
338
+ tokenGrid[r][srcC] = hlToken;
339
+ }
340
+ }
341
+ else {
342
+ if (sRow < gridRows && srcC < gridCols)
343
+ tokenGrid[sRow][srcC] = hlToken;
344
+ const minC = Math.min(srcC, dstC);
345
+ const maxC2 = Math.max(srcC, dstC);
346
+ if (midRow < gridRows) {
347
+ for (let c = minC; c <= maxC2 && c < gridCols; c++) {
348
+ tokenGrid[midRow][c] = hlToken;
349
+ }
350
+ }
351
+ for (let r = midRow; r <= dRow && r < gridRows; r++) {
352
+ if (dstC < gridCols)
353
+ tokenGrid[r][dstC] = hlToken;
354
+ }
355
+ }
356
+ }
357
+ }
358
+ // Write node boxes
359
+ const positions = new Map();
360
+ for (const n of nodes) {
361
+ const layer = layerMap.get(n.id);
362
+ const col = colIndex.get(n.id);
363
+ if (layer === undefined || col === undefined)
364
+ continue;
365
+ const startCol = col * colStride;
366
+ const startRow = layer * RS;
367
+ positions.set(n.id, { row: startRow, col: startCol, width: nodeWidth, height: 3 });
368
+ const boxLines = renderNodeBox(n.label, n.badge, nodeWidth, n._ghost === true);
369
+ let nToken;
370
+ if (options.selectedId === n.id) {
371
+ nToken = options.selectedToken ?? ctx.theme.theme.ui.cursor;
372
+ }
373
+ else if (highlightSet.has(n.id) && options.highlightToken) {
374
+ nToken = options.highlightToken;
375
+ }
376
+ else if (n.token) {
377
+ nToken = n.token;
378
+ }
379
+ else {
380
+ nToken = options.nodeToken ?? ctx.theme.theme.border.primary;
381
+ }
382
+ for (let lineIdx = 0; lineIdx < boxLines.length; lineIdx++) {
383
+ const row = startRow + lineIdx;
384
+ if (row >= gridRows)
385
+ continue;
386
+ const line = boxLines[lineIdx];
387
+ const chars = [...line];
388
+ for (let ci = 0; ci < chars.length; ci++) {
389
+ const gc = startCol + ci;
390
+ if (gc < gridCols) {
391
+ charGrid[row][gc] = chars[ci];
392
+ tokenGrid[row][gc] = nToken;
393
+ }
394
+ }
395
+ }
396
+ }
397
+ // Serialize
398
+ const lines = [];
399
+ for (let r = 0; r < gridRows; r++) {
400
+ let line = '';
401
+ let prevToken = null;
402
+ let run = '';
403
+ for (let c = 0; c < gridCols; c++) {
404
+ const ch = charGrid[r][c];
405
+ const tk = tokenGrid[r][c];
406
+ if (tk === prevToken || (tk === null && prevToken === null)) {
407
+ run += ch;
408
+ }
409
+ else {
410
+ if (run) {
411
+ line += prevToken ? ctx.style.styled(prevToken, run) : run;
412
+ }
413
+ run = ch;
414
+ prevToken = tk;
415
+ }
416
+ }
417
+ if (run) {
418
+ line += prevToken ? ctx.style.styled(prevToken, run) : run;
419
+ }
420
+ lines.push(line.replace(/\s+$/, ''));
421
+ }
422
+ while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
423
+ lines.pop();
424
+ }
425
+ return { output: lines.join('\n'), nodes: positions, width: gridCols, height: gridRows };
426
+ }
427
+ // ── Pipe Renderer ──────────────────────────────────────────────────
428
+ function renderPipe(nodes) {
429
+ if (nodes.length === 0)
430
+ return '';
431
+ const lines = [];
432
+ for (const n of nodes) {
433
+ const edges = n.edges ?? [];
434
+ const badgePart = n.badge ? ` (${n.badge})` : '';
435
+ if (edges.length > 0) {
436
+ const targets = edges
437
+ .map(id => {
438
+ const target = nodes.find(t => t.id === id);
439
+ return target ? target.label : id;
440
+ })
441
+ .join(', ');
442
+ lines.push(`${n.label}${badgePart} -> ${targets}`);
443
+ }
444
+ else {
445
+ lines.push(`${n.label}${badgePart}`);
446
+ }
447
+ }
448
+ return lines.join('\n');
449
+ }
450
+ // ── Accessible Renderer ────────────────────────────────────────────
451
+ function renderAccessible(nodes, layerMap) {
452
+ if (nodes.length === 0)
453
+ return 'Graph: 0 nodes, 0 edges';
454
+ const totalEdges = nodes.reduce((s, n) => s + (n.edges?.length ?? 0), 0);
455
+ const lines = [`Graph: ${nodes.length} nodes, ${totalEdges} edges`, ''];
456
+ const layers = buildLayerArrays(nodes, layerMap);
457
+ const nodeMap = new Map();
458
+ for (const n of nodes)
459
+ nodeMap.set(n.id, n);
460
+ for (let l = 0; l < layers.length; l++) {
461
+ lines.push(`Layer ${l + 1}:`);
462
+ for (const id of layers[l]) {
463
+ const n = nodeMap.get(id);
464
+ if (!n)
465
+ continue;
466
+ const badgePart = n.badge ? ` (${n.badge})` : '';
467
+ const edges = (n.edges ?? []).filter(e => nodeMap.has(e));
468
+ if (edges.length > 0) {
469
+ const targets = edges.map(e => nodeMap.get(e)?.label ?? e).join(', ');
470
+ lines.push(` ${n.label}${badgePart} -> ${targets}`);
471
+ }
472
+ else {
473
+ lines.push(` ${n.label}${badgePart} (end)`);
474
+ }
475
+ }
476
+ lines.push('');
477
+ }
478
+ while (lines.length > 0 && lines[lines.length - 1] === '')
479
+ lines.pop();
480
+ return lines.join('\n');
481
+ }
482
+ // ── dagSlice ───────────────────────────────────────────────────────
483
+ export function dagSlice(nodes, focus, opts) {
484
+ const direction = opts?.direction ?? 'both';
485
+ const maxDepth = opts?.depth ?? Infinity;
486
+ const nodeMap = new Map();
487
+ for (const n of nodes)
488
+ nodeMap.set(n.id, n);
489
+ if (!nodeMap.has(focus))
490
+ return [];
491
+ const included = new Set();
492
+ const ghostIds = new Set();
493
+ // Build parent map
494
+ const parentsMap = new Map();
495
+ for (const n of nodes) {
496
+ if (!parentsMap.has(n.id))
497
+ parentsMap.set(n.id, []);
498
+ for (const c of n.edges ?? []) {
499
+ if (!parentsMap.has(c))
500
+ parentsMap.set(c, []);
501
+ parentsMap.get(c).push(n.id);
502
+ }
503
+ }
504
+ // BFS ancestors
505
+ if (direction === 'ancestors' || direction === 'both') {
506
+ const queue = [[focus, 0]];
507
+ const visited = new Set();
508
+ while (queue.length > 0) {
509
+ const entry = queue.shift();
510
+ const id = entry[0];
511
+ const depth = entry[1];
512
+ if (visited.has(id))
513
+ continue;
514
+ visited.add(id);
515
+ included.add(id);
516
+ if (depth < maxDepth) {
517
+ for (const p of parentsMap.get(id) ?? []) {
518
+ if (!visited.has(p))
519
+ queue.push([p, depth + 1]);
520
+ }
521
+ }
522
+ else {
523
+ const boundaryParents = (parentsMap.get(id) ?? []).filter(p => !visited.has(p));
524
+ if (boundaryParents.length > 0) {
525
+ const ghostId = `__ghost_ancestors_${id}`;
526
+ ghostIds.add(ghostId);
527
+ included.add(ghostId);
528
+ }
529
+ }
530
+ }
531
+ }
532
+ // BFS descendants
533
+ if (direction === 'descendants' || direction === 'both') {
534
+ const queue = [[focus, 0]];
535
+ const visited = new Set();
536
+ while (queue.length > 0) {
537
+ const entry = queue.shift();
538
+ const id = entry[0];
539
+ const depth = entry[1];
540
+ if (visited.has(id))
541
+ continue;
542
+ visited.add(id);
543
+ included.add(id);
544
+ const ch = nodeMap.get(id)?.edges ?? [];
545
+ if (depth < maxDepth) {
546
+ for (const c of ch) {
547
+ if (!visited.has(c) && nodeMap.has(c))
548
+ queue.push([c, depth + 1]);
549
+ }
550
+ }
551
+ else {
552
+ const boundaryChildren = ch.filter(c => !visited.has(c) && nodeMap.has(c));
553
+ if (boundaryChildren.length > 0) {
554
+ const ghostId = `__ghost_descendants_${id}`;
555
+ ghostIds.add(ghostId);
556
+ included.add(ghostId);
557
+ }
558
+ }
559
+ }
560
+ }
561
+ // Build result
562
+ const result = [];
563
+ for (const gid of ghostIds) {
564
+ if (gid.startsWith('__ghost_ancestors_')) {
565
+ const boundaryId = gid.replace('__ghost_ancestors_', '');
566
+ const allParents = (parentsMap.get(boundaryId) ?? []).filter(p => !included.has(p));
567
+ const count = allParents.length;
568
+ result.push({
569
+ id: gid,
570
+ label: `... ${count} ancestor${count !== 1 ? 's' : ''}`,
571
+ edges: [boundaryId],
572
+ _ghost: true,
573
+ _ghostLabel: `... ${count} ancestor${count !== 1 ? 's' : ''}`,
574
+ });
575
+ }
576
+ }
577
+ for (const n of nodes) {
578
+ if (!included.has(n.id))
579
+ continue;
580
+ const filteredEdges = (n.edges ?? []).filter(e => included.has(e));
581
+ const descGhostId = `__ghost_descendants_${n.id}`;
582
+ if (ghostIds.has(descGhostId)) {
583
+ filteredEdges.push(descGhostId);
584
+ }
585
+ result.push({ ...n, edges: filteredEdges.length > 0 ? filteredEdges : undefined });
586
+ }
587
+ for (const gid of ghostIds) {
588
+ if (gid.startsWith('__ghost_descendants_')) {
589
+ const boundaryId = gid.replace('__ghost_descendants_', '');
590
+ const boundaryNode = nodeMap.get(boundaryId);
591
+ const allChildren = (boundaryNode?.edges ?? []).filter(c => !included.has(c) && nodeMap.has(c));
592
+ const count = allChildren.length;
593
+ result.push({
594
+ id: gid,
595
+ label: `... ${count} descendant${count !== 1 ? 's' : ''}`,
596
+ _ghost: true,
597
+ _ghostLabel: `... ${count} descendant${count !== 1 ? 's' : ''}`,
598
+ });
599
+ }
600
+ }
601
+ return result;
602
+ }
603
+ // ── dagLayout ──────────────────────────────────────────────────────
604
+ export function dagLayout(nodes, options = {}) {
605
+ const ctx = resolveCtx(options.ctx);
606
+ if (nodes.length === 0)
607
+ return { output: '', nodes: new Map(), width: 0, height: 0 };
608
+ const result = renderInteractiveLayout(nodes, options, ctx);
609
+ return { output: result.output, nodes: result.nodes, width: result.width, height: result.height };
610
+ }
611
+ // ── Main Entry Point ───────────────────────────────────────────────
612
+ export function dag(nodes, options = {}) {
613
+ const ctx = resolveCtx(options.ctx);
614
+ const mode = ctx.mode;
615
+ if (nodes.length === 0)
616
+ return '';
617
+ if (mode === 'pipe') {
618
+ return renderPipe(nodes);
619
+ }
620
+ if (mode === 'accessible') {
621
+ const layerMap = assignLayers(nodes);
622
+ return renderAccessible(nodes, layerMap);
623
+ }
624
+ return renderInteractiveLayout(nodes, options, ctx).output;
625
+ }
626
+ //# sourceMappingURL=dag.js.map