mellos-mapping 0.14.0 → 0.15.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/server.mjs CHANGED
@@ -21390,7 +21390,7 @@ function removeLayer(map, id) {
21390
21390
 
21391
21391
  // src/render/render.ts
21392
21392
  var ZOOM_MIN = -4;
21393
- var ZOOM_MAX = 1;
21393
+ var ZOOM_MAX = 2;
21394
21394
  var ZOOM_DEFAULT = 0;
21395
21395
  function clampZoom(n) {
21396
21396
  return Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, Math.round(n)));
@@ -21668,10 +21668,14 @@ function neutralSkin(unicode) {
21668
21668
  var BOX_H = 3;
21669
21669
  var BOX_GAP = 2;
21670
21670
  var LEFT_MARGIN = 2;
21671
+ var DETAIL_BUDGET = { innerMin: 22, innerMax: 32, noteRows: 3 };
21672
+ var DETAIL_PLUS_BUDGET = { innerMin: 30, innerMax: 48, noteRows: 12 };
21671
21673
  function zoomGeometry(zoom) {
21672
21674
  switch (zoom) {
21675
+ case 2:
21676
+ return { mode: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_PLUS_BUDGET };
21673
21677
  case 1:
21674
- return { mode: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
21678
+ return { mode: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_BUDGET };
21675
21679
  case 0:
21676
21680
  return { mode: "boxes", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
21677
21681
  case -1:
@@ -21684,9 +21688,6 @@ function zoomGeometry(zoom) {
21684
21688
  return { mode: "constellation", scale: 0, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
21685
21689
  }
21686
21690
  }
21687
- var DETAIL_INNER_MIN = 22;
21688
- var DETAIL_INNER_MAX = 32;
21689
- var DETAIL_NOTE_ROWS = 3;
21690
21691
  var LABEL_BUDGET_MIN = 4;
21691
21692
  function boxSpec(node, geo, unicode, neutral) {
21692
21693
  const glyph = node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0;
@@ -21696,14 +21697,15 @@ function boxSpec(node, geo, unicode, neutral) {
21696
21697
  if (geo.mode === "constellation") {
21697
21698
  return { w: 3, h: 1, label: "", pad: 0, borderless: true, extra: [] };
21698
21699
  }
21699
- if (geo.mode === "detail") {
21700
- const innerW = Math.min(Math.max(displayWidth(text2) + badgeW + 4, DETAIL_INNER_MIN), DETAIL_INNER_MAX);
21700
+ if (geo.mode === "detail" && geo.detail !== void 0) {
21701
+ const budget2 = geo.detail;
21702
+ const innerW = Math.min(Math.max(displayWidth(text2) + badgeW + 4, budget2.innerMin), budget2.innerMax);
21701
21703
  const extra = [];
21702
21704
  if (node.evidence !== void 0) extra.push({ text: fitWidth(` ${node.evidence}`, innerW), style: "faint" });
21703
21705
  if (node.detail !== void 0) {
21704
21706
  const wrapped = wrapWidth(node.detail, innerW - 2);
21705
- for (let i = 0; i < Math.min(wrapped.length, DETAIL_NOTE_ROWS); i++) {
21706
- const cut = i === DETAIL_NOTE_ROWS - 1 && wrapped.length > DETAIL_NOTE_ROWS;
21707
+ for (let i = 0; i < Math.min(wrapped.length, budget2.noteRows); i++) {
21708
+ const cut = i === budget2.noteRows - 1 && wrapped.length > budget2.noteRows;
21707
21709
  extra.push({ text: ` ${cut ? fitWidth(wrapped[i] + "\u2026", innerW - 2) : wrapped[i]}`, style: "none" });
21708
21710
  }
21709
21711
  }
@@ -22513,7 +22515,7 @@ function summarize(map) {
22513
22515
 
22514
22516
  // src/server/server.ts
22515
22517
  var SERVER_NAME = "mellos-mapping";
22516
- var SERVER_VERSION = "0.14.0";
22518
+ var SERVER_VERSION = "0.15.0";
22517
22519
  var ID = external_exports.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "lowercase letters, digits and dashes, 1-64 chars").describe("stable kebab-case identifier");
22518
22520
  var PAGE = ID.optional().describe(
22519
22521
  "page (parallel map) this call targets; omit for the default page. One effort = one page: start a NEW effort on its own page named after the effort, so concurrent sessions never write over each other and the pane can switch between pages."
package/dist/watch.mjs CHANGED
@@ -201,13 +201,15 @@ function updateNode(map, input) {
201
201
 
202
202
  // src/render/render.ts
203
203
  var ZOOM_MIN = -4;
204
- var ZOOM_MAX = 1;
204
+ var ZOOM_MAX = 2;
205
205
  var ZOOM_DEFAULT = 0;
206
206
  function clampZoom(n) {
207
207
  return Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, Math.round(n)));
208
208
  }
209
209
  function zoomLabel(zoom) {
210
210
  switch (zoom) {
211
+ case 2:
212
+ return "detail+";
211
213
  case 1:
212
214
  return "detail";
213
215
  case 0:
@@ -495,10 +497,14 @@ function neutralSkin(unicode) {
495
497
  var BOX_H = 3;
496
498
  var BOX_GAP = 2;
497
499
  var LEFT_MARGIN = 2;
500
+ var DETAIL_BUDGET = { innerMin: 22, innerMax: 32, noteRows: 3 };
501
+ var DETAIL_PLUS_BUDGET = { innerMin: 30, innerMax: 48, noteRows: 12 };
498
502
  function zoomGeometry(zoom) {
499
503
  switch (zoom) {
504
+ case 2:
505
+ return { mode: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_PLUS_BUDGET };
500
506
  case 1:
501
- return { mode: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
507
+ return { mode: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_BUDGET };
502
508
  case 0:
503
509
  return { mode: "boxes", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
504
510
  case -1:
@@ -511,9 +517,6 @@ function zoomGeometry(zoom) {
511
517
  return { mode: "constellation", scale: 0, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
512
518
  }
513
519
  }
514
- var DETAIL_INNER_MIN = 22;
515
- var DETAIL_INNER_MAX = 32;
516
- var DETAIL_NOTE_ROWS = 3;
517
520
  var LABEL_BUDGET_MIN = 4;
518
521
  function boxSpec(node, geo, unicode, neutral) {
519
522
  const glyph = node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0;
@@ -523,14 +526,15 @@ function boxSpec(node, geo, unicode, neutral) {
523
526
  if (geo.mode === "constellation") {
524
527
  return { w: 3, h: 1, label: "", pad: 0, borderless: true, extra: [] };
525
528
  }
526
- if (geo.mode === "detail") {
527
- const innerW = Math.min(Math.max(displayWidth(text) + badgeW + 4, DETAIL_INNER_MIN), DETAIL_INNER_MAX);
529
+ if (geo.mode === "detail" && geo.detail !== void 0) {
530
+ const budget2 = geo.detail;
531
+ const innerW = Math.min(Math.max(displayWidth(text) + badgeW + 4, budget2.innerMin), budget2.innerMax);
528
532
  const extra = [];
529
533
  if (node.evidence !== void 0) extra.push({ text: fitWidth(` ${node.evidence}`, innerW), style: "faint" });
530
534
  if (node.detail !== void 0) {
531
535
  const wrapped = wrapWidth(node.detail, innerW - 2);
532
- for (let i = 0; i < Math.min(wrapped.length, DETAIL_NOTE_ROWS); i++) {
533
- const cut = i === DETAIL_NOTE_ROWS - 1 && wrapped.length > DETAIL_NOTE_ROWS;
536
+ for (let i = 0; i < Math.min(wrapped.length, budget2.noteRows); i++) {
537
+ const cut = i === budget2.noteRows - 1 && wrapped.length > budget2.noteRows;
534
538
  extra.push({ text: ` ${cut ? fitWidth(wrapped[i] + "\u2026", innerW - 2) : wrapped[i]}`, style: "none" });
535
539
  }
536
540
  }
@@ -1175,7 +1179,7 @@ var KEY_PAN = {
1175
1179
  function mouseEvent(code, x, y, final) {
1176
1180
  if (code & WHEEL) {
1177
1181
  const down = (code & 1) !== 0;
1178
- return code & SHIFT ? { kind: "pan", dx: 0, dy: (down ? 1 : -1) * WHEEL_V_STEP } : { kind: "zoom", delta: down ? -1 : 1 };
1182
+ return code & SHIFT ? { kind: "pan", dx: 0, dy: (down ? 1 : -1) * WHEEL_V_STEP } : { kind: "zoom", delta: down ? -1 : 1, at: { x, y } };
1179
1183
  }
1180
1184
  const buttons = code & BUTTON_BITS;
1181
1185
  if (final === "m") return buttons === 0 ? { kind: "mouse-up", x, y } : void 0;
@@ -1271,6 +1275,9 @@ var RESET = "\x1B[0m";
1271
1275
  var PANEL_CONTENT_ROWS = 6;
1272
1276
  var PANEL_ROWS_MIN = 2;
1273
1277
  var MAP_ROWS_MIN = 4;
1278
+ function usableColumns(cols) {
1279
+ return Math.max(1, cols - 1);
1280
+ }
1274
1281
  function clampPanelRows(wanted, totalRows, tabRows) {
1275
1282
  const largest = totalRows - tabRows - MAP_ROWS_MIN - 2;
1276
1283
  return Math.max(PANEL_ROWS_MIN, Math.min(wanted, largest));
@@ -1302,27 +1309,46 @@ function anchorOffsets(anchor, offset, before, after) {
1302
1309
  y: before.h > 0 ? Math.round(offset.y * after.h / before.h) : 0
1303
1310
  };
1304
1311
  }
1305
- function pageTabRow(tabs, width, unicode) {
1306
- const segments = [];
1307
- let col = 1;
1308
- for (const [index, tab] of tabs.entries()) {
1309
- const room = width - (col - 1);
1310
- if (room <= 3) break;
1312
+ var TAB_INDICATOR_W = 3;
1313
+ function pageTabRow(tabs, width, unicode, scroll = 0) {
1314
+ const texts = tabs.map((tab) => {
1311
1315
  const marker = tab.active ? unicode ? "\u25CF" : "*" : unicode ? "\u25CB" : "o";
1312
1316
  const glyph = STATUS_GLYPH[tab.status][unicode ? 0 : 1];
1313
- const text = fitWidth(tab.neutral === true ? ` ${marker} ${tab.title} ` : ` ${marker} ${glyph} ${tab.title} `, room);
1317
+ return tab.neutral === true ? ` ${marker} ${tab.title} ` : ` ${marker} ${glyph} ${tab.title} `;
1318
+ });
1319
+ const sgrOf = (tab) => tab.neutral === true ? tab.active ? "1" : tab.fresh ? "36" : "90" : tab.active ? `${STATUS_SGR[tab.status]};1` : tab.fresh ? STATUS_SGR[tab.status] : "90";
1320
+ const widths = texts.map(displayWidth);
1321
+ const count = tabs.length;
1322
+ let lo = 0;
1323
+ let hi = count - 1;
1324
+ if (widths.reduce((a, b) => a + b, 0) > width) {
1325
+ lo = Math.max(0, Math.min(scroll, count - 1));
1326
+ hi = lo;
1327
+ const cost = (l, h) => widths.slice(l, h + 1).reduce((a, b) => a + b, 0) + (l > 0 ? TAB_INDICATOR_W : 0) + (h < count - 1 ? TAB_INDICATOR_W : 0);
1328
+ while (hi + 1 < count && cost(lo, hi + 1) <= width) hi++;
1329
+ }
1330
+ const segments = [];
1331
+ let col = 1;
1332
+ const push = (text, sgr, action) => {
1314
1333
  const w = displayWidth(text);
1315
- segments.push({
1316
- text,
1317
- sgr: tab.neutral === true ? tab.active ? "1" : tab.fresh ? "36" : "90" : tab.active ? `${STATUS_SGR[tab.status]};1` : tab.fresh ? STATUS_SGR[tab.status] : "90",
1318
- lo: col,
1319
- hi: col + w - 1,
1320
- index
1321
- });
1334
+ segments.push({ text, sgr, lo: col, hi: col + w - 1, action });
1322
1335
  col += w;
1336
+ };
1337
+ if (lo > 0) push(unicode ? " \u2039 " : " < ", "90", { kind: "scroll", delta: -1 });
1338
+ const tail = hi < count - 1 ? TAB_INDICATOR_W : 0;
1339
+ for (let i = lo; i <= hi; i++) {
1340
+ push(fitWidth(texts[i], Math.max(1, width - (col - 1) - tail)), sgrOf(tabs[i]), { kind: "switch", index: i });
1323
1341
  }
1342
+ if (hi < count - 1) push(unicode ? " \u203A " : " > ", "90", { kind: "scroll", delta: 1 });
1324
1343
  return segments;
1325
1344
  }
1345
+ function tabScrollFor(tabs, width, unicode, scroll, index) {
1346
+ if (index <= scroll) return Math.max(0, index);
1347
+ const visibleAt = (s2) => pageTabRow(tabs, width, unicode, s2).some((seg) => seg.action.kind === "switch" && seg.action.index === index);
1348
+ let s = Math.max(0, Math.min(scroll, tabs.length - 1));
1349
+ while (s < index && !visibleAt(s)) s++;
1350
+ return s;
1351
+ }
1326
1352
  function topLevelFiles(defaultFile, files, mapOf) {
1327
1353
  const refs = /* @__PURE__ */ new Set();
1328
1354
  for (const m of mapOf.values()) {
@@ -1602,6 +1628,7 @@ function main() {
1602
1628
  let activeFile;
1603
1629
  let firstScan = true;
1604
1630
  let lastTabSegments = [];
1631
+ let tabScroll = 0;
1605
1632
  let offsetX = 0;
1606
1633
  let offsetY = 0;
1607
1634
  let zoom = ZOOM_DEFAULT;
@@ -1634,8 +1661,19 @@ function main() {
1634
1661
  }
1635
1662
  return false;
1636
1663
  };
1664
+ const viewWidth = () => usableColumns(process.stdout.columns ?? 100);
1637
1665
  const viewHeight = () => Math.max(1, (process.stdout.rows ?? 30) - (1 + panelContentRows) - 1 - tabRows());
1638
1666
  const dividerY = () => tabRows() + viewHeight() + 1;
1667
+ const pageTabsOf = (files) => files.map((f) => {
1668
+ const m = pageData.get(f)?.map;
1669
+ return {
1670
+ title: m?.title ?? (pageIdOfFile(cfg.file, f) ?? "main"),
1671
+ status: m !== void 0 ? mapStatus(m) : "planned",
1672
+ active: f === activeFile,
1673
+ fresh: pageData.get(f)?.fresh ?? false,
1674
+ neutral: m !== void 0 && isNeutralKind(m)
1675
+ };
1676
+ });
1639
1677
  const switchPage = (file) => {
1640
1678
  if (activeFile !== void 0) pageViews.set(activeFile, { offsetX, offsetY, zoom, selectedId });
1641
1679
  activeFile = file;
@@ -1649,10 +1687,14 @@ function main() {
1649
1687
  if (entry !== void 0 && entry.fresh) pageData.set(file, { ...entry, fresh: false });
1650
1688
  map = entry?.map;
1651
1689
  notice = map === void 0 ? `waiting for ${file} ...` : "";
1690
+ const top = topFiles();
1691
+ const tabIndex = top.indexOf(file);
1692
+ if (tabIndex >= 0) tabScroll = tabScrollFor(pageTabsOf(top), viewWidth(), cfg.unicode, tabScroll, tabIndex);
1652
1693
  };
1653
1694
  const hitTest = (termX, termY) => {
1654
1695
  const sx = termX - 1;
1655
1696
  const sy = termY - 1 - tabRows();
1697
+ if (sx < 0 || sx >= viewWidth()) return void 0;
1656
1698
  if (sy < 0 || sy >= viewHeight()) return void 0;
1657
1699
  const cx = sx + offsetX;
1658
1700
  const cy = sy + offsetY;
@@ -1667,6 +1709,7 @@ function main() {
1667
1709
  process.on("SIGTERM", restore);
1668
1710
  const paint = () => {
1669
1711
  const cols = process.stdout.columns ?? 100;
1712
+ const viewW = viewWidth();
1670
1713
  panelContentRows = clampPanelRows(panelContentRows, process.stdout.rows ?? 30, tabRows());
1671
1714
  const viewH = viewHeight();
1672
1715
  const focus = hoverId ?? selectedId;
@@ -1677,9 +1720,9 @@ function main() {
1677
1720
  const windowed = renderMapWindow(
1678
1721
  map,
1679
1722
  { color: cfg.color, unicode: cfg.unicode, spinnerFrame, focus, zoom },
1680
- { x: offsetX, y: offsetY, width: cols, height: viewH }
1723
+ { x: offsetX, y: offsetY, width: viewW, height: viewH }
1681
1724
  );
1682
- const maxX = Math.max(0, windowed.contentWidth - cols);
1725
+ const maxX = Math.max(0, windowed.contentWidth - viewW);
1683
1726
  const maxY = Math.max(0, windowed.contentHeight - viewH);
1684
1727
  if (offsetX > maxX || offsetY > maxY || offsetX < 0 || offsetY < 0) {
1685
1728
  offsetX = Math.min(Math.max(0, offsetX), maxX);
@@ -1693,10 +1736,10 @@ function main() {
1693
1736
  lastContent = { w: windowed.contentWidth, h: windowed.contentHeight };
1694
1737
  if (offsetX !== 0 || offsetY !== 0) panned = ` (+${offsetX},+${offsetY})`;
1695
1738
  } else {
1696
- body = (interactive ? splashFrame(notice, splashTick, cols, viewH, cfg.unicode, cfg.color) : void 0) ?? [fitWidth(notice, Math.max(1, cols - 1))];
1739
+ body = (interactive ? splashFrame(notice, splashTick, viewW, viewH, cfg.unicode, cfg.color) : void 0) ?? [fitWidth(notice, viewW)];
1697
1740
  }
1698
1741
  if (notice !== "" && map !== void 0) {
1699
- body[body.length - 1] = fitWidth(` ${notice}`, Math.max(1, cols - 1));
1742
+ body[body.length - 1] = fitWidth(` ${notice}`, viewW);
1700
1743
  }
1701
1744
  const panelWidth = Math.max(10, cols - 2);
1702
1745
  let panel;
@@ -1708,9 +1751,9 @@ function main() {
1708
1751
  panel = mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
1709
1752
  }
1710
1753
  const grip = cfg.unicode ? " \u22EF " : " ~ ";
1711
- const bar = (cfg.unicode ? "\u2500" : "-").repeat(cols);
1712
- const gripAt = Math.max(0, Math.floor((cols - grip.length) / 2));
1713
- const separator = cols > grip.length + 2 ? bar.slice(0, gripAt) + grip + bar.slice(gripAt + grip.length) : bar;
1754
+ const bar = (cfg.unicode ? "\u2500" : "-").repeat(viewW);
1755
+ const gripAt = Math.max(0, Math.floor((viewW - grip.length) / 2));
1756
+ const separator = viewW > grip.length + 2 ? bar.slice(0, gripAt) + grip + bar.slice(gripAt + grip.length) : bar;
1714
1757
  const panelRows = [
1715
1758
  cfg.color ? `\x1B[90m${separator}${RESET}` : separator,
1716
1759
  ...panel.map(
@@ -1726,29 +1769,19 @@ function main() {
1726
1769
  const parentTitle = parentFile !== void 0 ? pageData.get(parentFile)?.map?.title ?? (pageIdOfFile(cfg.file, parentFile) ?? "main") : "main";
1727
1770
  const nodeLabel = scanned?.label ?? map?.title ?? "";
1728
1771
  const crumbHead = ` ${cfg.unicode ? "\u232B" : "<"} ${parentTitle} ${cfg.unicode ? "\u25B8" : ">"} `;
1729
- const head = { text: crumbHead, sgr: "90", lo: 1, hi: displayWidth(crumbHead), index: -1 };
1730
- const tailText = fitWidth(`${nodeLabel} `, Math.max(1, cols - displayWidth(crumbHead)));
1772
+ const head = { text: crumbHead, sgr: "90", lo: 1, hi: displayWidth(crumbHead), action: { kind: "back" } };
1773
+ const tailText = fitWidth(`${nodeLabel} `, Math.max(1, viewW - displayWidth(crumbHead)));
1731
1774
  const tail = {
1732
1775
  text: tailText,
1733
1776
  sgr: "1",
1734
1777
  lo: head.hi + 1,
1735
1778
  hi: head.hi + displayWidth(tailText),
1736
- index: -1
1779
+ action: { kind: "back" }
1737
1780
  };
1738
1781
  lastTabSegments = [head, tail];
1739
1782
  tabLine = lastTabSegments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET}` : s.text).join("");
1740
1783
  } else if (tabRows() > 0) {
1741
- const tabs = lastTabFiles.map((f) => {
1742
- const m = pageData.get(f)?.map;
1743
- return {
1744
- title: m?.title ?? (pageIdOfFile(cfg.file, f) ?? "main"),
1745
- status: m !== void 0 ? mapStatus(m) : "planned",
1746
- active: f === activeFile,
1747
- fresh: pageData.get(f)?.fresh ?? false,
1748
- neutral: m !== void 0 && isNeutralKind(m)
1749
- };
1750
- });
1751
- const segments = pageTabRow(tabs, cols, cfg.unicode);
1784
+ const segments = pageTabRow(pageTabsOf(lastTabFiles), viewW, cfg.unicode, tabScroll);
1752
1785
  lastTabSegments = segments;
1753
1786
  tabLine = segments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET}` : s.text).join("");
1754
1787
  } else {
@@ -1756,7 +1789,7 @@ function main() {
1756
1789
  }
1757
1790
  const zoomTag = `${cfg.unicode ? "\u2295" : "zoom"} ${zoomLabel(zoom)}`;
1758
1791
  const hint = !interactive ? cfg.file : (flash !== void 0 ? `${flash.text} \xB7 ` : "") + `${zoomTag} \xB7 wheel zoom \xB7 ` + (pannable ? "drag pan \xB7 " : "") + "hover/click \xB7 0 reset \xB7 q quit";
1759
- const footerText = fitWidth(` ${hint}${panned}`, Math.max(1, cols - 1));
1792
+ const footerText = fitWidth(` ${hint}${panned}`, viewW);
1760
1793
  const footer = cfg.color ? `\x1B[90m${footerText}${RESET}` : footerText;
1761
1794
  let frame = HOME;
1762
1795
  if (tabLine !== void 0) frame += tabLine + ERASE_LINE_END + "\n";
@@ -1849,10 +1882,14 @@ function main() {
1849
1882
  dirty = true;
1850
1883
  break;
1851
1884
  case "zoom": {
1885
+ if (event.at !== void 0 && event.at.y === 1 && tabRows() > 0 && !inSubmap()) {
1886
+ tabScroll = Math.max(0, Math.min(tabScroll + (event.delta === 1 ? -1 : 1), topFiles().length - 1));
1887
+ dirty = true;
1888
+ break;
1889
+ }
1852
1890
  const next = clampZoom(zoom + event.delta);
1853
1891
  if (next === zoom || map === void 0) break;
1854
- const cols = process.stdout.columns ?? 100;
1855
- const anchorId = hoverId ?? selectedId ?? nearestHit(lastHits, offsetX + cols / 2, offsetY + viewHeight() / 2)?.id;
1892
+ const anchorId = hoverId ?? selectedId ?? nearestHit(lastHits, offsetX + viewWidth() / 2, offsetY + viewHeight() / 2)?.id;
1856
1893
  const before = lastHits.find((h) => h.id === anchorId);
1857
1894
  zoom = next;
1858
1895
  const sized = renderMapWindow(
@@ -1916,10 +1953,12 @@ function main() {
1916
1953
  if (press && !press.moved) {
1917
1954
  const tabHit = tabRows() > 0 && event.y === 1 ? lastTabSegments.find((s) => event.x >= s.lo && event.x <= s.hi) : void 0;
1918
1955
  if (tabHit !== void 0) {
1919
- if (tabHit.index === -1) {
1956
+ if (tabHit.action.kind === "back") {
1920
1957
  climbBack();
1958
+ } else if (tabHit.action.kind === "scroll") {
1959
+ tabScroll = Math.max(0, Math.min(tabScroll + tabHit.action.delta, lastTabFiles.length - 1));
1921
1960
  } else {
1922
- const target = lastTabFiles[tabHit.index];
1961
+ const target = lastTabFiles[tabHit.action.index];
1923
1962
  if (target !== void 0 && target !== activeFile) switchPage(target);
1924
1963
  }
1925
1964
  } else {
@@ -2017,7 +2056,9 @@ export {
2017
2056
  parseArgs,
2018
2057
  splashArt,
2019
2058
  splashFrame,
2059
+ tabScrollFor,
2020
2060
  topLevelFiles,
2061
+ usableColumns,
2021
2062
  waveAt,
2022
2063
  waveHash,
2023
2064
  waveLevel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mellos-mapping",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "mcpName": "io.github.GuangminJu/mellos-mapping",
5
5
  "description": "A live layered dependency map for bottom-up development — MCP server + terminal pane. Ghost the design first, then light nodes up from the bottom as they are built and verified.",
6
6
  "type": "module",