@nonbot/mcp 0.2.4 → 0.3.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.
@@ -1,4 +1,5 @@
1
1
  import { IP_NODE_SIZE } from './ipShape.js';
2
+ import { satelliteRowWidth, satellitePositions, satelliteBandHeight, INNER_LOOP_GAP, INNER_LOOP_Y_OFFSET, HOST_DEFAULT_HEIGHT } from './satellites.js';
2
3
  export const PX_PER_WEEK = 120;
3
4
  const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
4
5
  const ITEM_TOP_HANDLE_X = 8;
@@ -130,9 +131,6 @@ export function layoutTimeline(nodes, opts = {}) {
130
131
  const slot = (id, from) => ({ id, ...geo.get(id), ...(from ? { from } : {}) });
131
132
  const byX = (a, b) => geo.get(a.id).x - geo.get(b.id).x || a.id.localeCompare(b.id);
132
133
  let y = 0;
133
- for (const o of items.filter(n => levelOf(n) === 'objective').sort(byX))
134
- y = placeBand([slot(o.id)], y);
135
- y = placeBand(items.filter(n => levelOf(n) === 'key-result').sort(byX).map(n => slot(n.id)), y);
136
134
  const ipNodes = nodes.filter(isIpNode);
137
135
  const ipFirstSource = new Map();
138
136
  const ipUnplaced = [];
@@ -143,14 +141,13 @@ export function layoutTimeline(nodes, opts = {}) {
143
141
  else
144
142
  ipUnplaced.push(n.id);
145
143
  }
146
- const epics = items.filter(n => levelOf(n) === 'epic').sort(byX);
147
144
  const rest = items.filter(n => !['objective', 'key-result', 'epic'].includes(levelOf(n)));
148
- for (const epic of [...epics.map(e => e.id), null]) {
145
+ const placeEpicBlock = (epic) => {
149
146
  const inBlock = rest.filter(n => epicOf(n) === epic);
150
147
  const blockIds = new Set([...(epic ? [epic] : []), ...inBlock.map(n => n.id)]);
151
148
  const ips = ipNodes.filter(n => blockIds.has(ipFirstSource.get(n.id) ?? ''));
152
149
  if (!epic && !inBlock.length && !ips.length)
153
- continue;
150
+ return;
154
151
  if (y > 0)
155
152
  y += BLOCK_GAP - ROW_GAP;
156
153
  if (epic)
@@ -169,6 +166,59 @@ export function layoutTimeline(nodes, opts = {}) {
169
166
  pool.push(...leaves.filter(n => !featureIds.has(n.data?.parentItemId)).map(n => slot(n.id)));
170
167
  pool.push(...inBlock.filter(n => levelOf(n) === 'milestone').sort(byX).map(n => slot(n.id)));
171
168
  y = placeBand(pool, y);
169
+ };
170
+ const epics = items.filter(n => levelOf(n) === 'epic').sort(byX);
171
+ const objectives = items.filter(n => levelOf(n) === 'objective').sort(byX);
172
+ const krs = items.filter(n => levelOf(n) === 'key-result').sort(byX);
173
+ if (opts.group === 'objective') {
174
+ const epicIds = new Set(epics.map(e => e.id));
175
+ const linked = new Map();
176
+ const link = (kr, ep) => { if (epicIds.has(ep) && !(linked.get(kr) ?? []).includes(ep))
177
+ linked.set(kr, [...(linked.get(kr) ?? []), ep]); };
178
+ for (const e of opts.edges ?? [])
179
+ if (e.dependencyType === 'okr-link')
180
+ link(e.source, e.target);
181
+ for (const kr of krs)
182
+ for (const ep of (Array.isArray(kr.data?.linkedEpicIds) ? kr.data.linkedEpicIds : []))
183
+ link(kr.id, ep);
184
+ const rank = new Map((opts.objectiveOrder ?? []).map((id, i) => [id, i]));
185
+ const ordered = [...objectives].sort((a, b) => (rank.get(a.id) ?? Infinity) - (rank.get(b.id) ?? Infinity) || byX(a, b));
186
+ const placedEpics = new Set();
187
+ const placeGroup = (krList) => {
188
+ if (krList.length)
189
+ y = placeBand(krList.map(n => slot(n.id)), y);
190
+ for (const kr of krList) {
191
+ const own = (linked.get(kr.id) ?? []).filter(id => !placedEpics.has(id)).map(id => byId.get(id)).sort(byX);
192
+ for (const ep of own) {
193
+ placedEpics.add(ep.id);
194
+ placeEpicBlock(ep.id);
195
+ }
196
+ }
197
+ };
198
+ for (const o of ordered) {
199
+ if (y > 0)
200
+ y += BLOCK_GAP - ROW_GAP;
201
+ y = placeBand([slot(o.id)], y);
202
+ placeGroup(krs.filter(k => k.data?.parentItemId === o.id));
203
+ }
204
+ const objectiveIds = new Set(objectives.map(o => o.id));
205
+ const looseKrs = krs.filter(k => !objectiveIds.has(k.data?.parentItemId));
206
+ if (looseKrs.length) {
207
+ if (y > 0)
208
+ y += BLOCK_GAP - ROW_GAP;
209
+ placeGroup(looseKrs);
210
+ }
211
+ for (const ep of epics)
212
+ if (!placedEpics.has(ep.id))
213
+ placeEpicBlock(ep.id);
214
+ placeEpicBlock(null);
215
+ }
216
+ else {
217
+ for (const o of objectives)
218
+ y = placeBand([slot(o.id)], y);
219
+ y = placeBand(krs.map(n => slot(n.id)), y);
220
+ for (const epic of [...epics.map(e => e.id), null])
221
+ placeEpicBlock(epic);
172
222
  }
173
223
  ipUnplaced.forEach((id, k) => positions.set(id, { x: k * (IP_NODE_SIZE.width + LANE_GAP_X), y: y + BLOCK_GAP }));
174
224
  return { positions, widthOverride };
@@ -191,3 +241,471 @@ export function applyLayout(nodes, res) {
191
241
  return out;
192
242
  });
193
243
  }
244
+ export const MAP_WIDTH = { epic: 280, feature: 260, story: 240, enabler: 240, milestone: 120, objective: 300, 'key-result': 260 };
245
+ const MAP_HERO_EXTRA = 260;
246
+ const MAP_GAP = 24;
247
+ const MAP_INDENT = 32;
248
+ export const MAP_GRID_COLS = 3;
249
+ export const MAP_GRID_MAX_COLS = 6;
250
+ const MAP_GRID_ROWS = 10;
251
+ const MAP_COLUMN_GAP = 96;
252
+ const MAP_BAND_GAP = 240;
253
+ export function layoutMap(nodes, opts = {}) {
254
+ return layoutMapDetailed(nodes, opts).positions;
255
+ }
256
+ export function layoutMapDetailed(nodes, opts = {}) {
257
+ const heights = opts.heights ?? {};
258
+ const sizes = opts.sizes ?? {};
259
+ const items = nodes.filter(isItem);
260
+ const byId = new Map(items.map(n => [n.id, n]));
261
+ const out = new Map();
262
+ const measured = (n) => { const z = sizes[n.id]; return z && z.width > 0 && z.height > 0 ? z : null; };
263
+ const w = (n) => {
264
+ const m = measured(n);
265
+ if (m)
266
+ return m.width;
267
+ const cs = n.data?.canvasSize;
268
+ return cs && Number.isFinite(cs.width) ? cs.width : MAP_WIDTH[levelOf(n)];
269
+ };
270
+ const h = (n) => {
271
+ const m = measured(n);
272
+ if (m)
273
+ return m.height;
274
+ if (heights[n.id])
275
+ return heights[n.id];
276
+ const cs = n.data?.canvasSize;
277
+ if (cs && Number.isFinite(cs.height))
278
+ return cs.height;
279
+ const hero = Array.isArray(n.data?.pastedImages) && n.data.pastedImages.length > 0;
280
+ return HEIGHT[levelOf(n)] + (hero ? MAP_HERO_EXTRA : 0);
281
+ };
282
+ const tx = (n) => n.position?.x ?? 0;
283
+ const byTime = (a, b) => tx(a) - tx(b) || a.id.localeCompare(b.id);
284
+ const kids = new Map();
285
+ for (const n of items) {
286
+ const p = n.data?.parentItemId;
287
+ if (p && byId.has(p))
288
+ kids.set(p, [...(kids.get(p) ?? []), n]);
289
+ }
290
+ const childrenOf = (id) => (kids.get(id) ?? []).slice().sort(byTime);
291
+ const isDone = (n) => n.data?.state === 'done';
292
+ const grid = (leaves, x, y) => {
293
+ const cols = Math.min(MAP_GRID_MAX_COLS, Math.max(MAP_GRID_COLS, Math.ceil(leaves.length / MAP_GRID_ROWS)));
294
+ const colW = [];
295
+ leaves.forEach((n, i) => { const c = i % cols; colW[c] = Math.max(colW[c] ?? 0, w(n)); });
296
+ const colX = [];
297
+ colW.forEach((cw, c) => { colX[c] = c === 0 ? x : colX[c - 1] + colW[c - 1] + MAP_GAP; });
298
+ let bottom = y;
299
+ for (let i = 0; i < leaves.length; i += cols) {
300
+ const row = leaves.slice(i, i + cols);
301
+ let rowH = 0;
302
+ row.forEach((n, c) => { out.set(n.id, { x: colX[c], y: bottom }); rowH = Math.max(rowH, h(n)); });
303
+ bottom += rowH + MAP_GAP;
304
+ }
305
+ const right = colW.length ? colX[colW.length - 1] + colW[colW.length - 1] : x;
306
+ return { bottom, right };
307
+ };
308
+ const placed = new Set();
309
+ const colCap = opts.columnHeight ?? Infinity;
310
+ const column = (epic, x, top, loose = []) => {
311
+ let y = top;
312
+ let right = x;
313
+ let colX = x;
314
+ let flowTop = top;
315
+ const maybeWrap = () => { if (y - top > colCap && y > flowTop) {
316
+ colX = right + MAP_COLUMN_GAP / 2;
317
+ y = flowTop;
318
+ } };
319
+ const take = (n, nx, ny) => { out.set(n.id, { x: nx, y: ny }); placed.add(n.id); right = Math.max(right, nx + w(n)); return ny + h(n) + MAP_GAP; };
320
+ const leavesGrid = (list, gx) => {
321
+ if (!list.length)
322
+ return;
323
+ for (const n of list)
324
+ placed.add(n.id);
325
+ const g = grid(list, gx, y);
326
+ y = g.bottom;
327
+ right = Math.max(right, g.right);
328
+ };
329
+ const descend = (id, gx, depth = 1) => {
330
+ const at = () => (depth === 1 ? colX + MAP_INDENT : gx);
331
+ const all = childrenOf(id).filter(n => !placed.has(n.id)).sort((a, b) => Number(isDone(a)) - Number(isDone(b)));
332
+ const containers = all.filter(n => (kids.get(n.id) ?? []).length > 0 || levelOf(n) === 'feature');
333
+ const leaves = all.filter(n => !containers.includes(n) && levelOf(n) !== 'milestone');
334
+ const ms = all.filter(n => !containers.includes(n) && levelOf(n) === 'milestone');
335
+ for (const c of containers) {
336
+ if (depth === 1)
337
+ maybeWrap();
338
+ const cx = at();
339
+ y = take(c, cx, y);
340
+ descend(c.id, cx + MAP_INDENT, depth + 1);
341
+ }
342
+ if (leaves.length && depth === 1)
343
+ maybeWrap();
344
+ leavesGrid(leaves, at());
345
+ for (const m of ms)
346
+ y = take(m, at(), y);
347
+ };
348
+ if (epic) {
349
+ y = take(epic, x, y);
350
+ flowTop = y;
351
+ descend(epic.id, x + MAP_INDENT);
352
+ }
353
+ for (const n of loose)
354
+ if (!placed.has(n.id)) {
355
+ y = take(n, x, y);
356
+ descend(n.id, x + MAP_INDENT);
357
+ }
358
+ return { right, bottom: y };
359
+ };
360
+ const objectives = items.filter(n => levelOf(n) === 'objective');
361
+ const krs = items.filter(n => levelOf(n) === 'key-result').sort(byTime);
362
+ const epics = items.filter(n => levelOf(n) === 'epic' && !byId.has(n.data?.parentItemId)).sort(byTime);
363
+ const epicIds = new Set(epics.map(e => e.id));
364
+ const linked = new Map();
365
+ const link = (kr, ep) => { if (epicIds.has(ep) && !(linked.get(kr) ?? []).includes(ep))
366
+ linked.set(kr, [...(linked.get(kr) ?? []), ep]); };
367
+ for (const e of opts.edges ?? [])
368
+ if (e.dependencyType === 'okr-link')
369
+ link(e.source, e.target);
370
+ for (const kr of krs)
371
+ for (const ep of (Array.isArray(kr.data?.linkedEpicIds) ? kr.data.linkedEpicIds : []))
372
+ link(kr.id, ep);
373
+ const rank = new Map((opts.objectiveOrder ?? []).map((id, i) => [id, i]));
374
+ const ordered = objectives.slice().sort((a, b) => (rank.get(a.id) ?? Infinity) - (rank.get(b.id) ?? Infinity) || byTime(a, b));
375
+ const headerH = opts.headerHeight ?? 0;
376
+ const allById = new Map(nodes.map(n => [n.id, n]));
377
+ const modW = (m) => sizes[m.id]?.width || Number(m.data?.width) || (isIpNode(m) ? IP_NODE_SIZE.width : 400);
378
+ const modH = (m) => sizes[m.id]?.height || Number(m.data?.height) || (isIpNode(m) ? IP_NODE_SIZE.height : 300);
379
+ const anyW = (n) => (n.type === 'rad-module' ? modW(n) : w(n));
380
+ const anyH = (n) => (n.type === 'rad-module' ? modH(n) : h(n));
381
+ const bands = [];
382
+ const capture = (kind, objective, header, fn) => {
383
+ const before = new Set(out.keys());
384
+ fn(header ? headerH : 0);
385
+ const ids = [...out.keys()].filter(k => !before.has(k));
386
+ if (!ids.length)
387
+ return;
388
+ let width = 0;
389
+ let height = header ? headerH : 0;
390
+ for (const id of ids) {
391
+ const p = out.get(id);
392
+ const n = allById.get(id);
393
+ width = Math.max(width, p.x + anyW(n));
394
+ height = Math.max(height, p.y + anyH(n));
395
+ }
396
+ const its = ids.map(id => allById.get(id)).filter(isItem);
397
+ bands.push({ kind, objectiveId: objective?.id ?? null, x: 0, y: 0, width, height, header, items: its.length, done: its.filter(isDone).length, epics: its.filter(n => levelOf(n) === 'epic').length, ids });
398
+ };
399
+ const ipNodes = nodes.filter(isIpNode);
400
+ const firstSource = (m) => (Array.isArray(m.data?.attachedItemIds) ? m.data.attachedItemIds : []).find((id) => byId.has(id));
401
+ const band = (top, lead, bandEpics) => {
402
+ const before = new Set(out.keys());
403
+ let x = 0;
404
+ if (lead.length) {
405
+ let y = top;
406
+ let right = 0;
407
+ for (const n of lead) {
408
+ out.set(n.id, { x: 0, y });
409
+ placed.add(n.id);
410
+ right = Math.max(right, w(n));
411
+ y += h(n) + MAP_GAP;
412
+ }
413
+ x = right + MAP_COLUMN_GAP;
414
+ }
415
+ for (const ep of bandEpics)
416
+ x = column(ep, x, top).right + MAP_COLUMN_GAP;
417
+ let y = top;
418
+ for (const m of ipNodes) {
419
+ const src = firstSource(m);
420
+ if (out.has(m.id) || !src || !out.has(src) || before.has(src))
421
+ continue;
422
+ out.set(m.id, { x, y });
423
+ y += modH(m) + MAP_GAP;
424
+ }
425
+ };
426
+ const placedEpics = new Set();
427
+ const epicsOf = (krList) => {
428
+ const list = [];
429
+ for (const kr of krList)
430
+ for (const id of linked.get(kr.id) ?? [])
431
+ if (!placedEpics.has(id)) {
432
+ placedEpics.add(id);
433
+ list.push(byId.get(id));
434
+ }
435
+ return list;
436
+ };
437
+ for (const o of ordered) {
438
+ const own = krs.filter(k => k.data?.parentItemId === o.id);
439
+ capture('objective', o, true, top => band(top, [o, ...own], epicsOf(own)));
440
+ }
441
+ const objectiveIds = new Set(objectives.map(o => o.id));
442
+ const looseKrs = krs.filter(k => !objectiveIds.has(k.data?.parentItemId));
443
+ const looseEpics = epicsOf(looseKrs);
444
+ const unlinked = epics.filter(e => !placedEpics.has(e.id));
445
+ for (const e of unlinked)
446
+ placedEpics.add(e.id);
447
+ capture('loose', null, true, top => band(top, looseKrs, [...looseEpics, ...unlinked]));
448
+ capture('rest', null, false, () => {
449
+ const rest = items.filter(n => !placed.has(n.id) && !byId.has(n.data?.parentItemId)).sort(byTime);
450
+ let y = rest.length ? column(null, 0, 0, rest).bottom : 0;
451
+ const stray = items.filter(n => !placed.has(n.id));
452
+ if (stray.length) {
453
+ for (const n of stray)
454
+ placed.add(n.id);
455
+ grid(stray, 0, y);
456
+ }
457
+ });
458
+ capture('modules', null, false, () => {
459
+ let mx = 0;
460
+ let my = 0;
461
+ let rowH = 0;
462
+ for (const m of nodes.filter(n => n.type === 'rad-module' && !out.has(n.id))) {
463
+ const mw = modW(m);
464
+ if (mx > 0 && mx + mw > 4 * 520) {
465
+ my += rowH + MAP_GAP;
466
+ mx = 0;
467
+ rowH = 0;
468
+ }
469
+ out.set(m.id, { x: mx, y: my });
470
+ mx += mw + MAP_GAP;
471
+ rowH = Math.max(rowH, modH(m));
472
+ }
473
+ });
474
+ const place = (b, dx, dy) => {
475
+ for (const id of b.ids) {
476
+ const p = out.get(id);
477
+ out.set(id, { x: p.x + dx, y: p.y + dy });
478
+ }
479
+ b.x = dx;
480
+ b.y = dy;
481
+ };
482
+ if (opts.pack === 'grid' && bands.length) {
483
+ const area = bands.reduce((sum, b) => sum + (b.width + MAP_BAND_GAP) * (b.height + MAP_BAND_GAP), 0);
484
+ const rowW = Math.max(...bands.map(b => b.width), Math.sqrt(area * (opts.aspect ?? 16 / 9)));
485
+ let x = 0;
486
+ let y = 0;
487
+ let rowH = 0;
488
+ for (const b of bands) {
489
+ if (x > 0 && x + b.width > rowW) {
490
+ y += rowH + MAP_BAND_GAP;
491
+ x = 0;
492
+ rowH = 0;
493
+ }
494
+ place(b, x, y);
495
+ x += b.width + MAP_BAND_GAP;
496
+ rowH = Math.max(rowH, b.height);
497
+ }
498
+ }
499
+ else {
500
+ let y = 0;
501
+ for (const b of bands) {
502
+ place(b, 0, y);
503
+ y += b.height + MAP_BAND_GAP;
504
+ }
505
+ }
506
+ return { positions: out, bands: bands.map(({ ids: _ids, ...b }) => b) };
507
+ }
508
+ export function applyMapLayout(nodes, positions) {
509
+ return nodes.map(n => {
510
+ const p = positions.get(n.id);
511
+ return p ? { ...n, freePosition: { x: Math.round(p.x), y: Math.round(p.y) } } : n;
512
+ });
513
+ }
514
+ export const FS_ROW = { objective: 0, 'key-result': 400, epic: 600, feature: 1040, milestone: 1320, story: 1480, enabler: 1480 };
515
+ export const FS_EPIC_STAGGER = 20;
516
+ export const FS_LEAF_PITCH = 240;
517
+ export const FS_MILESTONE_LIFT_GAP = 20;
518
+ export const FS_FEATURE_MIN_WIDTH = 600;
519
+ export const FS_GROUP_GAP = 240;
520
+ export function layoutFullStack(nodes, opts = {}) {
521
+ const sizes = opts.sizes ?? {};
522
+ const items = nodes.filter(isItem);
523
+ const byId = new Map(items.map(n => [n.id, n]));
524
+ const out = new Map();
525
+ const w = (n) => {
526
+ const z = sizes[n.id];
527
+ if (z && z.width > 0)
528
+ return z.width;
529
+ const cs = n.data?.canvasSize;
530
+ return cs && Number.isFinite(cs.width) && cs.width > 0 ? cs.width : MAP_WIDTH[levelOf(n)];
531
+ };
532
+ const tx = (n) => n.position?.x ?? 0;
533
+ const order = new Map(items.map((n, i) => [n.id, i]));
534
+ const byTime = (a, b) => tx(a) - tx(b) || order.get(a.id) - order.get(b.id);
535
+ const kids = new Map();
536
+ for (const n of items) {
537
+ const p = n.data?.parentItemId;
538
+ if (p && byId.has(p) && p !== n.id)
539
+ kids.set(p, [...(kids.get(p) ?? []), n]);
540
+ }
541
+ const put = (n, x, y) => out.set(n.id, { x, y });
542
+ const open = (list) => list.filter(n => !out.has(n.id)).sort(byTime);
543
+ const childrenOf = (id) => open(kids.get(id) ?? []);
544
+ const isMs = (n) => levelOf(n) === 'milestone';
545
+ const msH = (n) => {
546
+ const z = sizes[n.id];
547
+ if (z && z.height > 0)
548
+ return z.height;
549
+ const cs = n.data?.canvasSize;
550
+ return cs && Number.isFinite(cs.height) && cs.height > 0 ? cs.height : HEIGHT.milestone;
551
+ };
552
+ let x = 0;
553
+ let epicIndex = 0;
554
+ let epicBandRight = -Infinity;
555
+ let featureBandRight = -Infinity;
556
+ const clear = (cx, card, row, right) => (row > 0 && Number.isFinite(right) ? Math.max(cx, right + INNER_LOOP_GAP - card / 2 + row / 2) : cx);
557
+ const featureColumn = (f, fx0, stagger, under, epicBand) => {
558
+ const row = f ? satelliteRowWidth(f.data) : 0;
559
+ const fx = f ? clear(fx0, w(f), row, featureBandRight) : fx0;
560
+ if (f)
561
+ put(f, fx, FS_ROW.feature);
562
+ let lx = fx;
563
+ for (const l of under.filter(n => !isMs(n))) {
564
+ put(l, lx, FS_ROW.story);
565
+ lx += Math.max(FS_LEAF_PITCH, w(l));
566
+ }
567
+ const ms = under.filter(isMs);
568
+ const msW = ms.reduce((s, m) => s + w(m), 0);
569
+ let width = Math.max(lx - fx + msW, f ? Math.max(FS_FEATURE_MIN_WIDTH, w(f), row) : 0);
570
+ const raised = row > 0 && ms.length > 0;
571
+ const msY = raised ? FS_ROW.feature - Math.max(...ms.map(msH)) - FS_MILESTONE_LIFT_GAP : FS_ROW.milestone + stagger;
572
+ if (raised && epicBand && msY < epicBand.bottom && msY + Math.max(...ms.map(msH)) > epicBand.top) {
573
+ const left = fx + width - msW;
574
+ if (left < epicBand.right + INNER_LOOP_GAP && left + msW > epicBand.left - INNER_LOOP_GAP)
575
+ width = epicBand.right + INNER_LOOP_GAP + msW - fx;
576
+ }
577
+ let mx = fx + width - msW;
578
+ for (const m of ms) {
579
+ put(m, mx, msY);
580
+ mx += w(m);
581
+ }
582
+ if (f && row > 0)
583
+ featureBandRight = Math.max(featureBandRight, fx + w(f) / 2 + row / 2);
584
+ if (ms.length) {
585
+ if (raised)
586
+ epicBandRight = Math.max(epicBandRight, mx);
587
+ else
588
+ featureBandRight = Math.max(featureBandRight, mx);
589
+ }
590
+ return fx + width - fx0;
591
+ };
592
+ const epicColumn = (ep) => {
593
+ const stagger = FS_EPIC_STAGGER * (epicIndex++ % 2);
594
+ const row = satelliteRowWidth(ep.data);
595
+ const ex = clear(x, w(ep), row, epicBandRight);
596
+ const ey = FS_ROW.epic + stagger;
597
+ put(ep, ex, ey);
598
+ const band = row > 0
599
+ ? { left: ex + w(ep) / 2 - row / 2, right: ex + w(ep) / 2 + row / 2, top: ey + HOST_DEFAULT_HEIGHT.epic + INNER_LOOP_Y_OFFSET, bottom: ey + satelliteBandHeight(ep.data) }
600
+ : null;
601
+ if (band)
602
+ epicBandRight = Math.max(epicBandRight, band.right);
603
+ let cx = ex;
604
+ const ch = childrenOf(ep.id);
605
+ for (const f of ch.filter(n => levelOf(n) === 'feature'))
606
+ cx += featureColumn(f, cx, stagger, childrenOf(f.id), band);
607
+ const direct = ch.filter(n => levelOf(n) !== 'feature');
608
+ if (direct.length)
609
+ cx += featureColumn(null, cx, stagger, direct, band);
610
+ x = ex + Math.max(cx - ex, w(ep), row);
611
+ return ex;
612
+ };
613
+ const objectives = items.filter(n => levelOf(n) === 'objective');
614
+ const krs = items.filter(n => levelOf(n) === 'key-result').sort(byTime);
615
+ const epics = items.filter(n => levelOf(n) === 'epic').sort(byTime);
616
+ const epicIds = new Set(epics.map(e => e.id));
617
+ const linked = new Map();
618
+ const link = (kr, ep) => { if (epicIds.has(ep) && !(linked.get(kr) ?? []).includes(ep))
619
+ linked.set(kr, [...(linked.get(kr) ?? []), ep]); };
620
+ for (const e of opts.edges ?? [])
621
+ if (e.dependencyType === 'okr-link')
622
+ link(e.source, e.target);
623
+ for (const kr of krs)
624
+ for (const ep of (Array.isArray(kr.data?.linkedEpicIds) ? kr.data.linkedEpicIds : []))
625
+ link(kr.id, ep);
626
+ const rank = new Map((opts.objectiveOrder ?? []).map((id, i) => [id, i]));
627
+ const ordered = objectives.slice().sort((a, b) => (rank.get(a.id) ?? Infinity) - (rank.get(b.id) ?? Infinity) || byTime(a, b));
628
+ let started = false;
629
+ const group = (fn) => {
630
+ const before = out.size;
631
+ const at = x;
632
+ if (started)
633
+ x += FS_GROUP_GAP;
634
+ fn();
635
+ if (out.size === before)
636
+ x = at;
637
+ else
638
+ started = true;
639
+ };
640
+ const krColumn = (kr) => {
641
+ const own = open((linked.get(kr.id) ?? []).map(id => byId.get(id)));
642
+ if (!own.length) {
643
+ put(kr, x, FS_ROW['key-result']);
644
+ x += w(kr) + MAP_GAP;
645
+ return;
646
+ }
647
+ let first = null;
648
+ for (const ep of own) {
649
+ const ex = epicColumn(ep);
650
+ first ??= ex;
651
+ }
652
+ put(kr, first, FS_ROW['key-result']);
653
+ };
654
+ for (const o of ordered)
655
+ group(() => {
656
+ const ox = x;
657
+ put(o, ox, FS_ROW.objective);
658
+ for (const kr of krs.filter(k => k.data?.parentItemId === o.id))
659
+ krColumn(kr);
660
+ x = Math.max(x, ox + w(o));
661
+ });
662
+ const objectiveIds = new Set(objectives.map(o => o.id));
663
+ group(() => { for (const kr of krs.filter(k => !objectiveIds.has(k.data?.parentItemId)))
664
+ krColumn(kr); });
665
+ group(() => { for (const ep of open(epics))
666
+ epicColumn(ep); });
667
+ group(() => {
668
+ for (const f of open(items.filter(n => levelOf(n) === 'feature')))
669
+ x += featureColumn(f, x, 0, childrenOf(f.id), null);
670
+ const loose = open(items.filter(n => !byId.has(n.data?.parentItemId) && ['story', 'enabler', 'milestone'].includes(levelOf(n))));
671
+ if (loose.length)
672
+ x += featureColumn(null, x, 0, loose, null);
673
+ for (const n of open(items)) {
674
+ put(n, x, FS_ROW[levelOf(n)]);
675
+ x += Math.max(FS_LEAF_PITCH, w(n));
676
+ }
677
+ });
678
+ return out;
679
+ }
680
+ export function applyFullStackTimeline(nodes, positions) {
681
+ return nodes.map(n => {
682
+ const p = positions.get(n.id);
683
+ return p ? { ...n, position: { ...(n.position ?? { x: 0 }), y: Math.round(p.y) } } : n;
684
+ });
685
+ }
686
+ export function timelineRowOverlaps(nodes) {
687
+ const cards = nodes.filter(isItem).map(n => {
688
+ const lv = levelOf(n);
689
+ const weeks = typeof n.data?.durationWeeks === 'number' ? n.data.durationWeeks : DEFAULT_DURATION[lv];
690
+ const width = Number(n.data?.manualWidthOverride) || (weeks > 0 ? weeks * PX_PER_WEEK : DEFAULT_WIDTH[lv]);
691
+ return { x: n.position?.x ?? 0, y: n.position?.y ?? 0, width };
692
+ });
693
+ let count = 0;
694
+ for (let i = 0; i < cards.length; i++)
695
+ for (let j = i + 1; j < cards.length; j++) {
696
+ const a = cards[i], b = cards[j];
697
+ if (Math.abs(a.y - b.y) < 100 && a.x < b.x + b.width && b.x < a.x + a.width)
698
+ count++;
699
+ }
700
+ return count;
701
+ }
702
+ export function mapBoxes(n) {
703
+ const at = n.freePosition ?? n.position ?? { x: 0, y: 0 };
704
+ if (n.type === 'rad-module')
705
+ return [{ x: at.x, y: at.y, width: Number(n.data?.width) || 400, height: Number(n.data?.height) || 300 }];
706
+ const cs = n.data?.canvasSize;
707
+ const width = cs && Number.isFinite(cs.width) && cs.width > 0 ? cs.width : MAP_WIDTH[levelOf(n)];
708
+ const height = cs && Number.isFinite(cs.height) && cs.height > 0 ? cs.height : HEIGHT[levelOf(n)] + (Array.isArray(n.data?.pastedImages) && n.data.pastedImages.length ? MAP_HERO_EXTRA : 0);
709
+ const card = { x: at.x, y: at.y, width, height };
710
+ return [card, ...satellitePositions(n.data, card).map(({ x, y, width: w, height: h }) => ({ x, y, width: w, height: h }))];
711
+ }
@@ -0,0 +1,13 @@
1
+ import type { LayoutNode, MapBand } from './layout.js';
2
+ export declare const HEADER_MARKER = "__layoutHeader";
3
+ export declare const HEADER_TIMELINE_X = -6000;
4
+ export declare function headerText(b: MapBand, title: string): {
5
+ title: string;
6
+ body: string;
7
+ };
8
+ export declare function withMapHeaders<T extends LayoutNode>(nodes: T[], bands: MapBand[], headerHeight: number, now?: number): {
9
+ nodes: T[];
10
+ added: number;
11
+ updated: number;
12
+ removed: number;
13
+ };
@@ -0,0 +1,64 @@
1
+ export const HEADER_MARKER = '__layoutHeader';
2
+ export const HEADER_TIMELINE_X = -6000;
3
+ const MAX_NOTE_WIDTH = 2400;
4
+ const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
5
+ const keyOf = (b) => b.objectiveId ?? b.kind;
6
+ export function headerText(b, title) {
7
+ const pct = b.items ? Math.round((b.done / b.items) * 100) : 0;
8
+ const epics = `${b.epics} epic${b.epics === 1 ? '' : 's'}`;
9
+ const line = `${epics} · ${b.done} of ${b.items} cards done (${pct}%)`;
10
+ return { title, body: `<h2>${esc(title)}</h2><p>${esc(line)}</p>` };
11
+ }
12
+ export function withMapHeaders(nodes, bands, headerHeight, now = Date.now()) {
13
+ const titleOf = new Map(nodes.map(n => [n.id, String(n.data?.title ?? '')]));
14
+ const wanted = new Map(bands.filter(b => b.header).map(b => [keyOf(b), b]));
15
+ let added = 0;
16
+ let updated = 0;
17
+ let removed = 0;
18
+ const timelineY = (b) => {
19
+ const o = b.objectiveId ? nodes.find(n => n.id === b.objectiveId) : undefined;
20
+ return o?.position?.y ?? 0;
21
+ };
22
+ const build = (b, id, prev) => {
23
+ const t = headerText(b, b.objectiveId ? titleOf.get(b.objectiveId) || 'Objective' : 'Not linked to a goal yet');
24
+ const size = { width: Math.min(MAX_NOTE_WIDTH, Math.max(600, Math.round(b.width))), height: Math.max(80, headerHeight - 40) };
25
+ const prevModule = (prev?.data?.moduleData ?? {});
26
+ return {
27
+ ...(prev ?? {}),
28
+ id,
29
+ type: 'rad-module',
30
+ position: { x: HEADER_TIMELINE_X, y: timelineY(b) },
31
+ freePosition: { x: Math.round(b.x), y: Math.round(b.y) },
32
+ data: {
33
+ ...(prev?.data ?? {}),
34
+ subtype: 'document',
35
+ width: size.width,
36
+ height: size.height,
37
+ moduleData: { files: [], images: [], ...prevModule, title: t.title, body: t.body, __chromeCollapsed: true, __noteSize: size, [HEADER_MARKER]: keyOf(b) },
38
+ },
39
+ };
40
+ };
41
+ const out = [];
42
+ for (const n of nodes) {
43
+ const k = n.type === 'rad-module' && n.data?.subtype === 'document' ? n.data?.moduleData?.[HEADER_MARKER] : undefined;
44
+ if (typeof k !== 'string') {
45
+ out.push(n);
46
+ continue;
47
+ }
48
+ const b = wanted.get(k);
49
+ if (!b) {
50
+ removed++;
51
+ continue;
52
+ }
53
+ out.push(build(b, n.id, n));
54
+ updated++;
55
+ wanted.delete(k);
56
+ }
57
+ let i = 0;
58
+ for (const [k, b] of wanted) {
59
+ const slug = k.replace(/[^a-zA-Z0-9]/g, '').slice(-12) || String(i);
60
+ out.push(build(b, `radmod-document-${now}-hdr${slug}${i++}`));
61
+ added++;
62
+ }
63
+ return { nodes: out, added, updated, removed };
64
+ }
@@ -0,0 +1,18 @@
1
+ import { type LayoutNode } from './layout.js';
2
+ export interface PreviewOptions {
3
+ view?: 'map' | 'timeline';
4
+ sizes?: Record<string, {
5
+ width: number;
6
+ height: number;
7
+ }>;
8
+ width?: number;
9
+ }
10
+ export declare function renderPreviewSvg(nodes: LayoutNode[], opts?: PreviewOptions): {
11
+ svg: string;
12
+ cards: number;
13
+ satellites: number;
14
+ extent: {
15
+ width: number;
16
+ height: number;
17
+ };
18
+ };