waygraph 0.15.7 → 0.15.9

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
@@ -1015,6 +1015,7 @@ Flows are files (0.10.5+):
1015
1015
  | Manual watch | `waygraph demo src/flows/shop.flow.ts` |
1016
1016
  | Auto-advance | `waygraph demo --blocks shopFlow --auto-next` (nav auto-hides strip) |
1017
1017
  | Fast / full strip | `waygraph demo ... --fast` (shorter gates; keeps cursor) · `--full` (classic chips; default = carousel) |
1018
+ | Todo dock UX | `waygraph demo ... --todo-smart` (default) · `--todo-full` (no compact/collision/behind) |
1018
1019
  | QA watch + record | `waygraph demo --blocks shopFlow --auto-play-video` |
1019
1020
  | Ad-hoc Blocks | `waygraph run --blocks "login then nav-cart" --data '{...}'` |
1020
1021
  | Headed execute | `waygraph run --blocks shopFlow --non-headless --video` |
@@ -1040,6 +1041,20 @@ Flows are files (0.10.5+):
1040
1041
  `--auto-next` (alias `--autoplay`) = panel Auto-advance. `--auto-play-video` is **demo
1041
1042
  only**. `chain` remains a compat alias for `run --blocks` / `auto --blocks`.
1042
1043
 
1044
+ ### Todo dock UI (demo, 0.15.8+)
1045
+
1046
+ Long FR/AC checklists used to bury highlight rings. **Smart defaults are on:**
1047
+
1048
+ | Behavior | Default | Opt out |
1049
+ |----------|---------|---------|
1050
+ | Compact fold (~5 rows around current; hover expands; `+N more`) | on | `ctx.todoDockFull()` / `ctx.todoDockUi({ compact: false })` / `--todo-full` / `WAYGRAPH_TODO_UI=full` |
1051
+ | Collision flip (dock L/R when a ring overlaps) | on | `ctx.todoDockUi({ collision: false })` / `WAYGRAPH_TODO_COLLISION=0` |
1052
+ | Behind ring (dim + lower z-index while a ring is up) | on | `ctx.todoDockUi({ behindRing: false })` / `WAYGRAPH_TODO_BEHIND=0` |
1053
+
1054
+ Per-knob env also: `WAYGRAPH_TODO_COMPACT`, `WAYGRAPH_TODO_CAP` (default 5),
1055
+ `WAYGRAPH_TODO_EXPAND_CAP` (default 14). Author API: `ctx.todoDockUi({ ... })`.
1056
+ Pilot `auto highlight` accepts the same knobs as `todoUi` on the JSON body.
1057
+
1043
1058
  Run `waygraph --help` (or any subcommand with no args) for the full flag reference kept
1044
1059
  in `src/cli.ts`'s own `usage()` - that's the source of truth for flags, this table is the
1045
1060
  scannable summary.
package/dist/cli.js CHANGED
@@ -235,6 +235,7 @@ import {
235
235
  completeSequentialTodoDock,
236
236
  applyDevicePhase,
237
237
  resolveDeviceState,
238
+ resolveTodoDockUi,
238
239
  WAYGRAPH_RING_CSS,
239
240
  } from "waygraph";
240
241
 
@@ -517,9 +518,22 @@ const RING_CSS =
517
518
  "padding:10px 12px;background:rgba(20,10,40,.94);color:#fff;border-radius:12px;" +
518
519
  "border:1px solid rgba(124,58,237,.45);box-shadow:0 8px 24px rgba(0,0,0,.35);" +
519
520
  "pointer-events:auto;cursor:pointer;" +
520
- "transition:transform .4s cubic-bezier(.22,1,.36,1),top .35s ease;transform:translateX(0);}" +
521
+ "transition:transform .4s cubic-bezier(.22,1,.36,1),top .35s ease,opacity .25s ease,z-index 0s;transform:translateX(0);}" +
521
522
  ".wg-todo-dock[data-pos=right],#wg-todo-dock[data-pos=right]{transform:translateX(calc(100vw - 100% - 28px));}" +
522
523
  ".wg-todo-dock[data-pos=left],#wg-todo-dock[data-pos=left]{transform:translateX(0);}" +
524
+ /* While a highlight ring is up: sit under the ring so captions stay readable. */
525
+ ".wg-todo-dock.wg-todo-behind,#wg-todo-dock.wg-todo-behind{z-index:2147483643;opacity:.42;}" +
526
+ /* Collision tuck: shrink so a mid-page ring can still breathe. */
527
+ ".wg-todo-dock[data-tucked=\\"1\\"],#wg-todo-dock[data-tucked=\\"1\\"]{max-height:min(28vh,220px);}" +
528
+ /* Compact + cap: fold overflow rows; hover / data-expanded reveals more. */
529
+ ".wg-todo-dock[data-compact=\\"1\\"]:not(:hover):not([data-expanded=\\"1\\"]) li.wg-todo-fold{display:none;}" +
530
+ ".wg-todo-dock[data-compact=\\"1\\"]:is(:hover,[data-expanded=\\"1\\"]) li.wg-todo-fold-deep{display:none;}" +
531
+ ".wg-todo-dock .wg-todo-more{display:flex;align-items:center;gap:6px;margin-top:6px;padding:5px 8px;" +
532
+ "border-radius:8px;background:rgba(124,58,237,.22);border:1px solid rgba(124,58,237,.4);" +
533
+ "font:700 11px/1.2 system-ui,sans-serif;color:#e8dcff;cursor:pointer;user-select:none;}" +
534
+ ".wg-todo-dock .wg-todo-more:hover{background:rgba(124,58,237,.35);}" +
535
+ ".wg-todo-dock .wg-todo-dock-title{font:700 11px/1.2 system-ui,sans-serif;color:#c9a6ff;" +
536
+ "letter-spacing:.04em;text-transform:uppercase;margin:0 0 8px;}" +
523
537
  ".wg-todo-dock .wg-todos-list,#wg-todo-dock #wg-todos,.wg-todo-dock #wg-todos{list-style:none;margin:0;padding:0;background:transparent;border:none;}" +
524
538
  // Video device stage: keep recordVideo size fixed; center a device-sized shell.
525
539
  "html.wg-video-device-stage{background:#0b1220 !important;}" +
@@ -745,9 +759,17 @@ async function installOverlay(page, title) {
745
759
  const envTodoPos = (process.env.WAYGRAPH_TODO_POS || "left").toLowerCase();
746
760
  const todoPos = envTodoPos === "right" ? "right" : "left";
747
761
  const envAutoplay = process.env.WAYGRAPH_AUTOPLAY === "1";
762
+ const todoDockUi = resolveTodoDockUi();
748
763
  await page
749
764
  .evaluate(
750
- ({ title, favicon, bannerPos, todoPos, envAutoplay }) => {
765
+ ({ title, favicon, bannerPos, todoPos, envAutoplay, todoDockUi }) => {
766
+ window.__wgTodoDockUi = todoDockUi || {
767
+ compact: true,
768
+ cap: 5,
769
+ expandCap: 14,
770
+ collision: true,
771
+ behindRing: true,
772
+ };
751
773
  // Seed the live autoplay toggle from the env default on first ever
752
774
  // load only - a real navigation re-runs this, and re-stamping here
753
775
  // would silently undo a human's mid-run checkbox click.
@@ -851,6 +873,57 @@ async function installOverlay(page, title) {
851
873
  if (labelLeft < margin) labelLeft = margin;
852
874
  ringLabel.style.left = labelLeft + "px";
853
875
  ringLabel.style.top = labelTop + "px";
876
+ if (window.__wgTodosSetBehind) window.__wgTodosSetBehind(true);
877
+ if (window.__wgTodosAvoidRingCollision) window.__wgTodosAvoidRingCollision(box);
878
+ };
879
+ /** Dim/lower floating todo docks while a ring caption is the focus. */
880
+ window.__wgTodosSetBehind = (on) => {
881
+ const ui = window.__wgTodoDockUi || {};
882
+ if (ui.behindRing === false) {
883
+ document.querySelectorAll(".wg-todo-dock, #wg-todo-dock").forEach((el) => {
884
+ el.classList.remove("wg-todo-behind");
885
+ });
886
+ return;
887
+ }
888
+ document.querySelectorAll(".wg-todo-dock, #wg-todo-dock").forEach((el) => {
889
+ el.classList.toggle("wg-todo-behind", !!on);
890
+ });
891
+ };
892
+ /**
893
+ * If the active ring box intersects a todo dock, flip L/R once.
894
+ * Locked until hideRing so follow-rAF does not thrash.
895
+ */
896
+ window.__wgTodosAvoidRingCollision = (box) => {
897
+ const ui = window.__wgTodoDockUi || {};
898
+ if (ui.collision === false) return;
899
+ if (!box || window.__wgTodoCollisionLocked) return;
900
+ const pad = 10;
901
+ const rx1 = box.x - pad;
902
+ const ry1 = box.y - pad;
903
+ const rx2 = box.x + box.width + pad;
904
+ const ry2 = box.y + box.height + pad;
905
+ const overlaps = (el) => {
906
+ const r = el.getBoundingClientRect();
907
+ return !(r.right < rx1 || r.left > rx2 || r.bottom < ry1 || r.top > ry2);
908
+ };
909
+ const docks = [...document.querySelectorAll(".wg-todo-dock, #wg-todo-dock")].filter(
910
+ (el, i, arr) => arr.indexOf(el) === i,
911
+ );
912
+ for (const dock of docks) {
913
+ if (!overlaps(dock)) continue;
914
+ const cur = dock.dataset.pos === "right" ? "right" : "left";
915
+ dock.dataset.pos = cur === "right" ? "left" : "right";
916
+ try {
917
+ localStorage.setItem("wg-todo-pos", dock.dataset.pos);
918
+ } catch {
919
+ /* ignore */
920
+ }
921
+ // Still colliding after flip - tuck (shorter max-height).
922
+ if (overlaps(dock)) dock.dataset.tucked = "1";
923
+ else delete dock.dataset.tucked;
924
+ window.__wgTodoCollisionLocked = true;
925
+ break;
926
+ }
854
927
  };
855
928
  /** Live-follow a selector with rAF (accurate under device shell scale). */
856
929
  window.__wgStopRingFollow = () => {
@@ -907,6 +980,11 @@ async function installOverlay(page, title) {
907
980
  if (ring) ring.style.opacity = "0";
908
981
  if (ringLabel) ringLabel.style.opacity = "0";
909
982
  if (window.__wgClearFocus) window.__wgClearFocus();
983
+ if (window.__wgTodosSetBehind) window.__wgTodosSetBehind(false);
984
+ window.__wgTodoCollisionLocked = false;
985
+ document.querySelectorAll(".wg-todo-dock[data-tucked], #wg-todo-dock[data-tucked]").forEach((el) => {
986
+ delete el.dataset.tucked;
987
+ });
910
988
  // Honor zoomOut:false - keep camera until next zoom / desktop clear.
911
989
  const forceClear = opts && opts.clearZoom === true;
912
990
  const skipClear = opts && opts.clearZoom === false;
@@ -1422,6 +1500,7 @@ async function installOverlay(page, title) {
1422
1500
  // { sync: "set", dock } / { sync:"set", list } -> render / recreate
1423
1501
  // Compat: bare array = set list (empty array = clear - legacy only).
1424
1502
  window.__wgSyncTodos = (payload, pos) => {
1503
+ const syncPayload = payload;
1425
1504
  let sync = "set";
1426
1505
  let list = [];
1427
1506
  let dockState = null;
@@ -1571,7 +1650,35 @@ async function installOverlay(page, title) {
1571
1650
  const id = raw != null && String(raw).trim() ? String(raw).trim() : "";
1572
1651
  return id ? ' data-wg-todo-item="' + esc(id) + '"' : "";
1573
1652
  };
1574
- const renderItems = (items, style) => {
1653
+ // Compact + cap: >cap rows folds overflow; hover shows up to expandCap; "+N more" pins expand.
1654
+ const ui = (payload && !Array.isArray(payload) && payload.ui) || window.__wgTodoDockUi || {};
1655
+ const COMPACT_AT = Number(ui.cap) > 0 ? Number(ui.cap) : 5;
1656
+ const WINDOW = COMPACT_AT;
1657
+ const EXPAND_MAX = Number(ui.expandCap) > 0 ? Number(ui.expandCap) : 14;
1658
+ const compactEnabled = ui.compact !== false;
1659
+ const flattenItems = () => {
1660
+ if (dockState && Array.isArray(dockState.groups) && dockState.groups.length) {
1661
+ const out = [];
1662
+ for (const g of dockState.groups) {
1663
+ for (const t of g.items || []) out.push({ ...t, __style: g.style || dockState.style });
1664
+ }
1665
+ return out;
1666
+ }
1667
+ return (list || []).map((t) => ({ ...t, __style: "sequential" }));
1668
+ };
1669
+ const windowBounds = (items, cap) => {
1670
+ const n = items.length;
1671
+ if (n <= cap) return { start: 0, end: n };
1672
+ let cur = items.findIndex((t) => t.current);
1673
+ if (cur < 0) cur = items.findIndex((t) => !t.done);
1674
+ if (cur < 0) cur = 0;
1675
+ let start = Math.max(0, cur - Math.floor((cap - 1) / 2));
1676
+ let end = Math.min(n, start + cap);
1677
+ start = Math.max(0, end - cap);
1678
+ return { start, end };
1679
+ };
1680
+ const renderItems = (items, style, foldMeta, indexOffset) => {
1681
+ const offset = indexOffset || 0;
1575
1682
  const rawStyle = String(style || "sequential").toLowerCase();
1576
1683
  const st =
1577
1684
  rawStyle === "checklist"
@@ -1584,13 +1691,18 @@ async function installOverlay(page, title) {
1584
1691
  rawStyle === "plain"
1585
1692
  ? "bullets"
1586
1693
  : "sequential";
1694
+ const foldStart = foldMeta ? foldMeta.foldStart : -1;
1695
+ const foldEnd = foldMeta ? foldMeta.foldEnd : -1;
1696
+ const deepStart = foldMeta ? foldMeta.deepStart : -1;
1697
+ const deepEnd = foldMeta ? foldMeta.deepEnd : -1;
1587
1698
  return (
1588
1699
  '<ul class="wg-todos-list" data-wg-todo-style="' +
1589
1700
  st +
1590
1701
  '">' +
1591
1702
  (items || [])
1592
- .map((t) => {
1593
- const cls =
1703
+ .map((t, idx) => {
1704
+ const gi = offset + idx;
1705
+ const clsBase =
1594
1706
  st === "bullets"
1595
1707
  ? t.done
1596
1708
  ? "wg-todo-done"
@@ -1600,6 +1712,9 @@ async function installOverlay(page, title) {
1600
1712
  : t.done
1601
1713
  ? "wg-todo-done"
1602
1714
  : "wg-todo-pending";
1715
+ let foldCls = "";
1716
+ if (foldStart >= 0 && (gi < foldStart || gi >= foldEnd)) foldCls += " wg-todo-fold";
1717
+ if (deepStart >= 0 && (gi < deepStart || gi >= deepEnd)) foldCls += " wg-todo-fold-deep";
1603
1718
  let mark;
1604
1719
  if (st === "checklist") {
1605
1720
  mark = t.done ? "\u2611" : "\u2610";
@@ -1611,7 +1726,8 @@ async function installOverlay(page, title) {
1611
1726
  const rowId = t.id || t.name || "";
1612
1727
  return (
1613
1728
  '<li class="' +
1614
- cls +
1729
+ clsBase +
1730
+ foldCls +
1615
1731
  '"' +
1616
1732
  attrId(rowId) +
1617
1733
  '><span class="wg-todo-mark">' +
@@ -1629,12 +1745,28 @@ async function installOverlay(page, title) {
1629
1745
  if (dockKey !== "_default") dock.setAttribute("data-wg-todo-id", dockKey);
1630
1746
  else dock.removeAttribute("data-wg-todo-id");
1631
1747
 
1748
+ const flat = flattenItems();
1749
+ const compact = compactEnabled && flat.length > COMPACT_AT;
1750
+ const win = windowBounds(flat, WINDOW);
1751
+ const expand = windowBounds(flat, EXPAND_MAX);
1752
+ const foldMeta = compact
1753
+ ? {
1754
+ foldStart: win.start,
1755
+ foldEnd: win.end,
1756
+ deepStart: expand.start,
1757
+ deepEnd: expand.end,
1758
+ }
1759
+ : null;
1760
+ const hiddenCompact = compact ? flat.length - (win.end - win.start) : 0;
1761
+ const hiddenExpand = compact ? flat.length - (expand.end - expand.start) : 0;
1762
+
1632
1763
  let html = "";
1633
1764
  if (dockState && Array.isArray(dockState.groups) && dockState.groups.length) {
1634
1765
  const dockTitle = dockState.title && String(dockState.title).trim();
1635
1766
  if (dockTitle) {
1636
1767
  html += '<div class="wg-todo-dock-title">' + esc(dockTitle) + "</div>";
1637
1768
  }
1769
+ let globalIdx = 0;
1638
1770
  for (const g of dockState.groups) {
1639
1771
  const gTitle = g.title && String(g.title).trim();
1640
1772
  const gid = g.id || g.name || "";
@@ -1645,13 +1777,42 @@ async function installOverlay(page, title) {
1645
1777
  if (gTitle) {
1646
1778
  html += '<div class="wg-todo-group-title">' + esc(gTitle) + "</div>";
1647
1779
  }
1648
- html += renderItems(g.items, g.style || dockState.style);
1780
+ const gItems = g.items || [];
1781
+ html += renderItems(gItems, g.style || dockState.style, foldMeta, globalIdx);
1649
1782
  html += "</div>";
1783
+ globalIdx += gItems.length;
1650
1784
  }
1651
1785
  } else {
1652
- html = renderItems(list, "sequential");
1786
+ html = renderItems(list, "sequential", foldMeta, 0);
1787
+ }
1788
+ if (compact && hiddenCompact > 0) {
1789
+ const n = dock.dataset.expanded === "1" ? hiddenExpand : hiddenCompact;
1790
+ if (n > 0) {
1791
+ html +=
1792
+ '<div class="wg-todo-more" data-wg-todo-more="1" title="Click to expand / collapse">' +
1793
+ "+" +
1794
+ n +
1795
+ " more</div>";
1796
+ }
1797
+ }
1798
+ if (compact) dock.dataset.compact = "1";
1799
+ else {
1800
+ delete dock.dataset.compact;
1801
+ delete dock.dataset.expanded;
1653
1802
  }
1654
1803
  dock.innerHTML = html;
1804
+ const moreBtn = dock.querySelector("[data-wg-todo-more]");
1805
+ if (moreBtn) {
1806
+ moreBtn.addEventListener("click", (e) => {
1807
+ e.stopPropagation();
1808
+ if (dock.dataset.expanded === "1") delete dock.dataset.expanded;
1809
+ else dock.dataset.expanded = "1";
1810
+ if (window.__wgSyncTodos && dock._wgLastPayload) {
1811
+ window.__wgSyncTodos(dock._wgLastPayload);
1812
+ }
1813
+ });
1814
+ }
1815
+ dock._wgLastPayload = syncPayload;
1655
1816
  relayoutTodoDocks();
1656
1817
  if (window.__wgStampModal) {
1657
1818
  window.__wgStampModal(dock, "todos", { ready: true });
@@ -1864,7 +2025,7 @@ async function installOverlay(page, title) {
1864
2025
  }
1865
2026
  };
1866
2027
  },
1867
- { title, favicon: WAYGRAPH_FAVICON, bannerPos, todoPos, envAutoplay },
2028
+ { title, favicon: WAYGRAPH_FAVICON, bannerPos, todoPos, envAutoplay, todoDockUi },
1868
2029
  )
1869
2030
  .catch(() => {});
1870
2031
  // Navigations wipe #wg-device-shell - rebuild when video stage + device are live.
@@ -4970,8 +5131,21 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
4970
5131
  let result;
4971
5132
  /** Carry floating todo dock across steps (empty stubBefore must not wipe). */
4972
5133
  let lastTodoDock = undefined;
5134
+ /** Author todoDockUi patches (merged); resolved against env each step. */
5135
+ let lastTodoDockUiPatch = undefined;
4973
5136
  /** Multi-todo: every dock keyed by todoId (or "_default"), survives navigation. */
4974
5137
  const todoDockById = new Map();
5138
+ const pushTodoDockUi = async (patch) => {
5139
+ if (patch && typeof patch === "object") {
5140
+ lastTodoDockUiPatch = { ...(lastTodoDockUiPatch || {}), ...patch };
5141
+ }
5142
+ const ui = resolveTodoDockUi(lastTodoDockUiPatch);
5143
+ await page
5144
+ .evaluate((u) => {
5145
+ window.__wgTodoDockUi = u;
5146
+ }, ui)
5147
+ .catch(() => {});
5148
+ };
4975
5149
  const dockRegistryKey = (dock) =>
4976
5150
  dock && dock.id && String(dock.id).trim() ? String(dock.id).trim() : "_default";
4977
5151
  const rememberTodoDock = (dock, sync, parallel) => {
@@ -5104,6 +5278,11 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
5104
5278
  const fixtures = r.highlightFixtures;
5105
5279
  const flowStyle = r.highlightStyle;
5106
5280
  const stubBeforePhase = await runStubPhase(r.block, "stubBefore", { fixtures, mem });
5281
+ if (stubBeforePhase.todoDockUi) {
5282
+ await pushTodoDockUi(stubBeforePhase.todoDockUi);
5283
+ } else {
5284
+ await pushTodoDockUi(undefined);
5285
+ }
5107
5286
  stubBeforeRef.current = stubBeforePhase.highlights.map((h) =>
5108
5287
  applyHighlightStyleDefaults(h, flowStyle),
5109
5288
  );
@@ -5422,6 +5601,9 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
5422
5601
  fixtures: fixturesAfter,
5423
5602
  mem,
5424
5603
  });
5604
+ if (afterPhase.todoDockUi) {
5605
+ await pushTodoDockUi(afterPhase.todoDockUi);
5606
+ }
5425
5607
  logStubPhaseFixtures("after", {
5426
5608
  ...afterPhase,
5427
5609
  highlights: (afterPhase.highlights || []).map((h) => {
@@ -6604,6 +6786,12 @@ function parseRunFlags(argv) {
6604
6786
  else if (a === "--todo-right") {
6605
6787
  out.todoPos = "right";
6606
6788
  }
6789
+ else if (a === "--todo-full" || a === "--no-todo-smart") {
6790
+ out.todoUiFull = true;
6791
+ }
6792
+ else if (a === "--todo-smart") {
6793
+ out.todoUiSmart = true;
6794
+ }
6607
6795
  else if (a === "--ff-expand") {
6608
6796
  out.ffExpand = true;
6609
6797
  }
@@ -6727,8 +6915,9 @@ function applyRunFlags(flags, opts) {
6727
6915
  process.env.WAYGRAPH_HEADED = "0";
6728
6916
  }
6729
6917
  }
6730
- if ((flags.fast || flags.fullStepper || flags.miniStepper || flags.todoPos) && !opts?.allowDemoUi) {
6731
- console.error("waygraph: --fast / --full / --mini / --todo-left|--todo-right are demo-only flags");
6918
+ if ((flags.fast || flags.fullStepper || flags.miniStepper || flags.todoPos || flags.todoUiFull || flags.todoUiSmart) &&
6919
+ !opts?.allowDemoUi) {
6920
+ console.error("waygraph: --fast / --full / --mini / --todo-left|--todo-right / --todo-full|--todo-smart are demo-only flags");
6732
6921
  process.exit(1);
6733
6922
  }
6734
6923
  if (flags.step === true) {
@@ -6782,6 +6971,12 @@ function applyRunFlags(flags, opts) {
6782
6971
  if (flags.todoPos === "left" || flags.todoPos === "right") {
6783
6972
  process.env.WAYGRAPH_TODO_POS = flags.todoPos;
6784
6973
  }
6974
+ if (flags.todoUiFull) {
6975
+ process.env.WAYGRAPH_TODO_UI = "full";
6976
+ }
6977
+ else if (flags.todoUiSmart) {
6978
+ process.env.WAYGRAPH_TODO_UI = "smart";
6979
+ }
6785
6980
  if (flags.ffExpand) {
6786
6981
  process.env.WAYGRAPH_FF_EXPAND = "1";
6787
6982
  }
@@ -7030,6 +7225,8 @@ Primary (less is more):
7030
7225
  --full Classic wrap-all block chips (default: carousel)
7031
7226
  --mini Force compact mini panel (Hide pill + Next); alias --stepper-mini
7032
7227
  --todo-left|--todo-right Floating checklist dock side (also ctx.todoPos / WAYGRAPH_TODO_POS)
7228
+ --todo-full Opt out: full checklist, no compact/collision/behind-ring
7229
+ --todo-smart Opt in: compact + collision + behind-ring (default)
7033
7230
  --ff-expand Expand fastForwardComposeBlock inners as separate steps
7034
7231
  --ff-disabled Dispute: expand FF (alias --no-ff); blitz kept on those inners
7035
7232
  --auto-play-video Unattended + recorded: --auto-next + --video (+ step); headless
@@ -105,6 +105,41 @@ export type WaygraphTodosOpts = {
105
105
  */
106
106
  parallel?: boolean;
107
107
  };
108
+ /**
109
+ * Floating todo-dock UX knobs (0.15.8+). Defaults are **smart on** so long
110
+ * FR/AC checklists do not bury highlight rings. Opt out per phase or globally.
111
+ *
112
+ * Defaults: `compact`, `collision`, `behindRing` = true; `cap` = 5; `expandCap` = 14.
113
+ *
114
+ * Global: `WAYGRAPH_TODO_UI=full` / `waygraph demo --todo-full` turns smart off.
115
+ * Per-knob env: `WAYGRAPH_TODO_COMPACT=0`, `WAYGRAPH_TODO_COLLISION=0`,
116
+ * `WAYGRAPH_TODO_BEHIND=0`, `WAYGRAPH_TODO_CAP`, `WAYGRAPH_TODO_EXPAND_CAP`.
117
+ */
118
+ export type TodoDockUiOpts = {
119
+ /** Fold long lists around the current row (+N more / hover). Default true. */
120
+ compact?: boolean;
121
+ /** Visible rows when compact. Default 5. */
122
+ cap?: number;
123
+ /** Visible rows on hover / pinned expand. Default 14. */
124
+ expandCap?: number;
125
+ /** Flip dock L/R when an active ring overlaps it. Default true. */
126
+ collision?: boolean;
127
+ /** Dim + lower z-index while a highlight ring is up. Default true. */
128
+ behindRing?: boolean;
129
+ };
130
+ /** Resolved todo-dock UX (all fields present). */
131
+ export type TodoDockUiResolved = {
132
+ compact: boolean;
133
+ cap: number;
134
+ expandCap: number;
135
+ collision: boolean;
136
+ behindRing: boolean;
137
+ };
138
+ /**
139
+ * Resolve todo-dock UX from env + optional author patch.
140
+ * `WAYGRAPH_TODO_UI=full|off|legacy` forces compact/collision/behindRing off.
141
+ */
142
+ export declare function resolveTodoDockUi(patch?: TodoDockUiOpts | null, env?: NodeJS.ProcessEnv): TodoDockUiResolved;
108
143
  /** Episode-level fixtures authored inside stubBefore/After/OnError(ctx). */
109
144
  export type StubPhaseFixtures = {
110
145
  todos?: readonly WaygraphTodoInput[];
@@ -135,6 +170,11 @@ export type StubPhaseFixtures = {
135
170
  * Authored control (also click / WAYGRAPH_TODO_POS / --todo-left|right).
136
171
  */
137
172
  todoPos?: "left" | "right";
173
+ /**
174
+ * Todo-dock UX (compact / collision / behind-ring). Merged onto env defaults.
175
+ * See {@link TodoDockUiOpts}.
176
+ */
177
+ todoDockUi?: TodoDockUiOpts;
138
178
  /**
139
179
  * Viewport preset: `mobile` | `tablet` | `desktop` (default / clear).
140
180
  * Omit on later blocks = **keep** (same persist pattern as todos).
@@ -403,6 +443,16 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
403
443
  * code; click / env / --todo-left|right are fallbacks.
404
444
  */
405
445
  todoPos(side: "left" | "right"): void;
446
+ /**
447
+ * Todo-dock UX: compact fold, collision flip, dim-behind-ring.
448
+ * Defaults are smart-on; pass false fields to opt out for this phase.
449
+ * @example
450
+ * ctx.todoDockUi({ compact: false }); // always show full list
451
+ * ctx.todoDockFull(); // shorthand: compact+collision+behindRing off
452
+ */
453
+ todoDockUi(opts: TodoDockUiOpts): void;
454
+ /** Shorthand: full checklist, no compact/collision/behind-ring for this phase. */
455
+ todoDockFull(): void;
406
456
  /** Batch-set rings + episode fixtures. */
407
457
  set(partial: {
408
458
  highlights?: HighlightStubPhase;
@@ -430,6 +480,8 @@ export type StubPhaseResult = {
430
480
  * - `keep` - author omitted todos; demo must leave previous dock alone
431
481
  */
432
482
  todoSync?: "set" | "clear" | "keep";
483
+ /** Todo-dock UX for this phase (when author called {@link StubCtx.todoDockUi}). */
484
+ todoDockUi?: TodoDockUiOpts;
433
485
  /** Resolved device after this phase (undefined when keep with no prior). */
434
486
  device?: DeviceState;
435
487
  /**
@@ -12,6 +12,57 @@
12
12
  * All fixture shapes share optional {@link FixtureDurationFields} so demo dwell
13
13
  * before advancing is authored the same way on stubs, flow fixtures, and slides.
14
14
  */
15
+ function envFlagBool(env, key, fallback) {
16
+ const v = env[key];
17
+ if (v === undefined || v === "")
18
+ return fallback;
19
+ const s = String(v).trim().toLowerCase();
20
+ if (s === "0" || s === "false" || s === "off" || s === "no")
21
+ return false;
22
+ if (s === "1" || s === "true" || s === "on" || s === "yes")
23
+ return true;
24
+ return fallback;
25
+ }
26
+ function envFlagInt(env, key, fallback) {
27
+ const n = Number(env[key]);
28
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
29
+ }
30
+ /**
31
+ * Resolve todo-dock UX from env + optional author patch.
32
+ * `WAYGRAPH_TODO_UI=full|off|legacy` forces compact/collision/behindRing off.
33
+ */
34
+ export function resolveTodoDockUi(patch, env = process.env) {
35
+ const mode = String(env.WAYGRAPH_TODO_UI || "smart")
36
+ .trim()
37
+ .toLowerCase();
38
+ const full = mode === "full" || mode === "off" || mode === "0" || mode === "legacy" || mode === "classic";
39
+ const base = {
40
+ compact: full ? false : envFlagBool(env, "WAYGRAPH_TODO_COMPACT", true),
41
+ collision: full ? false : envFlagBool(env, "WAYGRAPH_TODO_COLLISION", true),
42
+ behindRing: full ? false : envFlagBool(env, "WAYGRAPH_TODO_BEHIND", true),
43
+ cap: envFlagInt(env, "WAYGRAPH_TODO_CAP", 5),
44
+ expandCap: envFlagInt(env, "WAYGRAPH_TODO_EXPAND_CAP", 14),
45
+ };
46
+ if (base.expandCap < base.cap)
47
+ base.expandCap = base.cap;
48
+ if (!patch)
49
+ return base;
50
+ const cap = patch.cap !== undefined && Number.isFinite(patch.cap)
51
+ ? Math.max(1, Math.floor(Number(patch.cap)))
52
+ : base.cap;
53
+ let expandCap = patch.expandCap !== undefined && Number.isFinite(patch.expandCap)
54
+ ? Math.max(1, Math.floor(Number(patch.expandCap)))
55
+ : base.expandCap;
56
+ if (expandCap < cap)
57
+ expandCap = cap;
58
+ return {
59
+ compact: patch.compact !== undefined ? !!patch.compact : base.compact,
60
+ collision: patch.collision !== undefined ? !!patch.collision : base.collision,
61
+ behindRing: patch.behindRing !== undefined ? !!patch.behindRing : base.behindRing,
62
+ cap,
63
+ expandCap,
64
+ };
65
+ }
15
66
  /** Built-in preset sizes (CSS pixels) — stored in portrait for mobile/tablet. */
16
67
  export const DEVICE_PRESETS = {
17
68
  mobile: {
@@ -966,6 +1017,18 @@ function createStubCtx(bag, opts) {
966
1017
  if (p)
967
1018
  bag.todoPos = p;
968
1019
  },
1020
+ todoDockUi(opts) {
1021
+ if (!opts || typeof opts !== "object")
1022
+ return;
1023
+ bag.todoDockUi = { ...(bag.todoDockUi || {}), ...opts };
1024
+ },
1025
+ todoDockFull() {
1026
+ bag.todoDockUi = {
1027
+ compact: false,
1028
+ collision: false,
1029
+ behindRing: false,
1030
+ };
1031
+ },
969
1032
  set(partial) {
970
1033
  if (partial.highlights)
971
1034
  bag.highlights = { ...partial.highlights };
@@ -1040,6 +1103,9 @@ function createStubCtx(bag, opts) {
1040
1103
  const tp = normalizeTodoPos(partial.todoPos);
1041
1104
  if (tp)
1042
1105
  bag.todoPos = tp;
1106
+ if (partial.todoDockUi !== undefined && partial.todoDockUi && typeof partial.todoDockUi === "object") {
1107
+ bag.todoDockUi = { ...(bag.todoDockUi || {}), ...partial.todoDockUi };
1108
+ }
1043
1109
  },
1044
1110
  };
1045
1111
  }
@@ -1237,6 +1303,7 @@ export async function runStubPhase(block, phase, opts) {
1237
1303
  ...(bag.zoomOut !== undefined ? { zoomOut: bag.zoomOut } : {}),
1238
1304
  ...(bag.title !== undefined && bag.title !== "" ? { title: bag.title } : {}),
1239
1305
  ...(bag.todoPos !== undefined ? { todoPos: bag.todoPos } : {}),
1306
+ ...(bag.todoDockUi !== undefined ? { todoDockUi: bag.todoDockUi } : {}),
1240
1307
  ...(bag.todoParallel === true ? { todoParallel: true } : {}),
1241
1308
  };
1242
1309
  }
package/dist/index.d.ts CHANGED
@@ -3,8 +3,8 @@ export { connect, checkpoint } from "./types.js";
3
3
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
4
4
  export { Trait, urlMatches, textEquals, visible, frameVisible, frameTextEquals, frameContainsText } from "./trait.js";
5
5
  export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, chainFlow, defineLayout, map, MapBuilder, } from "./engine.js";
6
- export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, DEVICE_PRESETS, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, WAYGRAPH_RING_CSS, } from "./highlights.js";
7
- export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, WaygraphTodoItem, WaygraphTodoInput, WaygraphTodoGroup, WaygraphTodoGroupInput, WaygraphTodosOpts, TodoDockState, TodoListStyle, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, StubCtx, StubLifecycleFn, StubPhaseResult, StubPhaseFixtures, DevicePreset, DeviceViewport, DeviceState, DeviceOrientation, } from "./highlights.js";
6
+ export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, DEVICE_PRESETS, resolveTodoDockUi, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, WAYGRAPH_RING_CSS, } from "./highlights.js";
7
+ export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, WaygraphTodoItem, WaygraphTodoInput, WaygraphTodoGroup, WaygraphTodoGroupInput, WaygraphTodosOpts, TodoDockUiOpts, TodoDockUiResolved, TodoDockState, TodoListStyle, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, StubCtx, StubLifecycleFn, StubPhaseResult, StubPhaseFixtures, DevicePreset, DeviceViewport, DeviceState, DeviceOrientation, } from "./highlights.js";
8
8
  export type { Flow, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, AssertBlock, AssertBlockOptions, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, Layout, LayoutOptions, MapBuilderOptions, } from "./engine.js";
9
9
  export type { ActionPage } from "./types.js";
10
10
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ export { connect, checkpoint } from "./types.js";
2
2
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
3
3
  export { Trait, urlMatches, textEquals, visible, frameVisible, frameTextEquals, frameContainsText } from "./trait.js";
4
4
  export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, chainFlow, defineLayout, map, MapBuilder, } from "./engine.js";
5
- export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, DEVICE_PRESETS, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, WAYGRAPH_RING_CSS, } from "./highlights.js";
5
+ export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, DEVICE_PRESETS, resolveTodoDockUi, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, WAYGRAPH_RING_CSS, } from "./highlights.js";
6
6
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
7
7
  export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp, } from "./blocks-select.js";
8
8
  export { EdgeLeaseCoordinator } from "./traverse-lease.js";
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import type { Page } from "@playwright/test";
20
20
  import type { SessionSnapshot } from "./auto-session.js";
21
- import { type HighlightTone, type DevicePreset, type DeviceState } from "./highlights.js";
21
+ import { type HighlightTone, type DevicePreset, type DeviceState, type TodoDockUiOpts } from "./highlights.js";
22
22
  /**
23
23
  * Bottom-left transient toast, separate from the persistent bottom-right
24
24
  * badge - real, direct user request: something that visibly flags "a
@@ -97,6 +97,11 @@ export type PilotHighlightFixtures = {
97
97
  todoTitle?: string;
98
98
  /** Todo dock side. Default `right`. */
99
99
  todoPos?: "left" | "right";
100
+ /**
101
+ * Todo-dock UX (compact / collision / behind-ring). Defaults smart-on;
102
+ * pass false fields to opt out. See {@link TodoDockUiOpts}.
103
+ */
104
+ todoUi?: TodoDockUiOpts;
100
105
  /**
101
106
  * How long fixtures stay visible (ms). `0` = until the next highlight /
102
107
  * clear. Default `30000` (30s) unless specified.
@@ -1,4 +1,4 @@
1
- import { WAYGRAPH_RING_CSS, normalizeHighlightTone, normalizeHighlightSize, normalizeHighlightWeight, formatHighlightCaption, resolveDeviceState, } from "./highlights.js";
1
+ import { WAYGRAPH_RING_CSS, normalizeHighlightTone, normalizeHighlightSize, normalizeHighlightWeight, formatHighlightCaption, resolveDeviceState, resolveTodoDockUi, } from "./highlights.js";
2
2
  // Real, direct user request: this overlay's own colors were an improvised
3
3
  // purple/white scheme (#a78bfa / #1a1033) that didn't match anything else -
4
4
  // "our design here seems like barely shares with the ui on the stepper
@@ -88,11 +88,13 @@ const OVERLAY_CSS = `
88
88
  .wg-pilot-fx-label[data-size=lg]{font-size:16px;padding:8px 14px;}
89
89
  .wg-pilot-fx-label[data-weight=bold]{font-weight:800;}
90
90
  #wg-pilot-fx-todos{
91
- position:fixed;z-index:2147483645;top:72px;max-width:280px;
91
+ position:fixed;z-index:2147483645;top:72px;max-width:280px;max-height:calc(100vh - 100px);overflow:auto;
92
92
  font:12px/1.4 ui-sans-serif,system-ui,sans-serif;color:#fff;
93
93
  background:rgba(20,10,40,.94);border-radius:10px;padding:10px 12px;
94
- box-shadow:0 2px 10px rgba(0,0,0,.3);pointer-events:none;
94
+ box-shadow:0 2px 10px rgba(0,0,0,.3);pointer-events:auto;
95
+ transition:opacity .25s ease;
95
96
  }
97
+ #wg-pilot-fx-todos.wg-todo-behind{z-index:2147483643;opacity:.42;pointer-events:none;}
96
98
  #wg-pilot-fx-todos[data-pos=left]{left:14px;right:auto;}
97
99
  #wg-pilot-fx-todos[data-pos=right]{right:14px;left:auto;}
98
100
  #wg-pilot-fx-todos .wg-pilot-fx-todo-title{font-weight:700;color:#c9a6ff;margin-bottom:6px;}
@@ -100,6 +102,9 @@ const OVERLAY_CSS = `
100
102
  #wg-pilot-fx-todos li{margin:3px 0;color:#ddd;}
101
103
  #wg-pilot-fx-todos li.wg-pilot-fx-todo-done{color:#888;text-decoration:line-through;}
102
104
  #wg-pilot-fx-todos li.wg-pilot-fx-todo-now{color:#fff;font-weight:700;}
105
+ #wg-pilot-fx-todos[data-compact="1"]:not(:hover) li.wg-todo-fold{display:none;}
106
+ #wg-pilot-fx-todos .wg-todo-more{margin-top:6px;padding:4px 8px;border-radius:8px;
107
+ background:rgba(124,58,237,.22);font:700 11px/1.2 system-ui,sans-serif;color:#e8dcff;}
103
108
  /* Spotlight (demo focus:true) - dim page, cutout around target. */
104
109
  #wg-pilot-fx-focus{position:fixed;z-index:2147483644;pointer-events:none;
105
110
  border-radius:12px;box-shadow:0 0 0 9999px rgba(8,4,20,.62);
@@ -318,6 +323,7 @@ export async function showPilotFixtures(page, fixtures) {
318
323
  const todoIndex = fixtures.todoIndex ?? 0;
319
324
  const todoTitle = fixtures.todoTitle ?? "Plan";
320
325
  const todoPos = fixtures.todoPos === "left" ? "left" : "right";
326
+ const todoUi = resolveTodoDockUi(fixtures.todoUi ?? null);
321
327
  // Effective zoom: phase zoom, else last ring that authored zoom.
322
328
  let zoom = fixtures.clear === true
323
329
  ? 1
@@ -368,7 +374,7 @@ export async function showPilotFixtures(page, fixtures) {
368
374
  ? fixtures.device
369
375
  : fixtures.device.preset || "device";
370
376
  return page
371
- .evaluate(({ rings, todos, todoIndex, todoTitle, todoPos, holdMs, zoom, zoomSelector, zoomOut, clearAll, deviceLabel, }) => {
377
+ .evaluate(({ rings, todos, todoIndex, todoTitle, todoPos, holdMs, zoom, zoomSelector, zoomOut, clearAll, deviceLabel, todoUi, }) => {
372
378
  const w = window;
373
379
  if (w.__wgPilotFxHideTimer)
374
380
  clearTimeout(w.__wgPilotFxHideTimer);
@@ -511,6 +517,17 @@ export async function showPilotFixtures(page, fixtures) {
511
517
  title.className = "wg-pilot-fx-todo-title";
512
518
  title.textContent = todoTitle;
513
519
  dock.appendChild(title);
520
+ const WINDOW = todoUi.cap > 0 ? todoUi.cap : 5;
521
+ const compact = todoUi.compact !== false && todos.length > WINDOW;
522
+ if (compact)
523
+ dock.dataset.compact = "1";
524
+ let start = 0;
525
+ let end = todos.length;
526
+ if (compact) {
527
+ start = Math.max(0, todoIndex - Math.floor((WINDOW - 1) / 2));
528
+ end = Math.min(todos.length, start + WINDOW);
529
+ start = Math.max(0, end - WINDOW);
530
+ }
514
531
  const ol = document.createElement("ol");
515
532
  todos.forEach((text, i) => {
516
533
  const li = document.createElement("li");
@@ -519,13 +536,38 @@ export async function showPilotFixtures(page, fixtures) {
519
536
  li.className = "wg-pilot-fx-todo-done";
520
537
  else if (i === todoIndex)
521
538
  li.className = "wg-pilot-fx-todo-now";
539
+ if (compact && (i < start || i >= end))
540
+ li.classList.add("wg-todo-fold");
522
541
  ol.appendChild(li);
523
542
  });
524
543
  dock.appendChild(ol);
544
+ if (compact && todos.length - (end - start) > 0) {
545
+ const more = document.createElement("div");
546
+ more.className = "wg-todo-more";
547
+ more.textContent = "+" + (todos.length - (end - start)) + " more";
548
+ dock.appendChild(more);
549
+ }
525
550
  document.documentElement.appendChild(dock);
526
551
  }
527
552
  if (focusBox)
528
553
  applyFocus(focusBox);
554
+ // Dim todos under rings / focus so captions stay readable.
555
+ const dockEl = document.getElementById("wg-pilot-fx-todos");
556
+ if (dockEl && todoUi.behindRing !== false && (painted > 0 || focusBox)) {
557
+ dockEl.classList.add("wg-todo-behind");
558
+ }
559
+ // Collision flip: if a painted ring intersects the dock, flip side once.
560
+ if (dockEl && todoUi.collision !== false && painted > 0) {
561
+ const firstRing = document.querySelector(".wg-pilot-fx-ring");
562
+ if (firstRing) {
563
+ const rr = firstRing.getBoundingClientRect();
564
+ const dr = dockEl.getBoundingClientRect();
565
+ const hit = !(rr.right < dr.left || rr.left > dr.right || rr.bottom < dr.top || rr.top > dr.bottom);
566
+ if (hit) {
567
+ dockEl.dataset.pos = dockEl.dataset.pos === "right" ? "left" : "right";
568
+ }
569
+ }
570
+ }
529
571
  if (zoom > 1.001 && zoomSelector) {
530
572
  const target = document.querySelector(zoomSelector);
531
573
  if (target) {
@@ -567,6 +609,7 @@ export async function showPilotFixtures(page, fixtures) {
567
609
  zoomOut,
568
610
  clearAll: fixtures.clear === true,
569
611
  deviceLabel,
612
+ todoUi,
570
613
  })
571
614
  .catch(() => ({ painted: 0, missing: rings.map((r) => r.selector) }));
572
615
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.15.7",
3
+ "version": "0.15.9",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -39,6 +39,7 @@ waygraph auto highlight <sessionId> '{"clear":true}'
39
39
  Supported fields (demo parity):
40
40
  - `rings[]`: `selector`, `label`, `detail`, `tag`, `tone`, `size`, `weight`, `color`, `zoom`, `zoomOut`, `focus`
41
41
  - `todos` / `todoIndex` / `todoTitle` / `todoPos` (`right` default | `left`)
42
+ - `todoUi`: `{ compact?, cap?, expandCap?, collision?, behindRing? }` — same dock UX as demo (defaults smart-on; see README "Todo dock UI")
42
43
  - `zoom` / `zoomSelector` / `zoomOut` - scroll + zoom badge (not CSS page scale)
43
44
  - `device`: `mobile` | `tablet` | `desktop` (Playwright viewport)
44
45
  - `holdMs`: ms to keep fixtures; `0` = until the next highlight / clear (default 30000)