waygraph 0.15.5 → 0.15.8

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.
@@ -682,6 +682,11 @@ export class AutoSession {
682
682
  : [
683
683
  fixtures.rings?.length ? `${fixtures.rings.length} ring(s)` : null,
684
684
  fixtures.todos?.length ? `${fixtures.todos.length} todo(s)` : null,
685
+ fixtures.zoom && fixtures.zoom > 1.001 ? `zoom ${fixtures.zoom}` : null,
686
+ fixtures.device
687
+ ? `device ${typeof fixtures.device === "string" ? fixtures.device : fixtures.device.preset || "custom"}`
688
+ : null,
689
+ fixtures.rings?.some((r) => r.focus) ? "focus" : null,
685
690
  ]
686
691
  .filter(Boolean)
687
692
  .join(", ") || "fixtures";
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
  }
@@ -6782,6 +6970,12 @@ function applyRunFlags(flags, opts) {
6782
6970
  if (flags.todoPos === "left" || flags.todoPos === "right") {
6783
6971
  process.env.WAYGRAPH_TODO_POS = flags.todoPos;
6784
6972
  }
6973
+ if (flags.todoUiFull) {
6974
+ process.env.WAYGRAPH_TODO_UI = "full";
6975
+ }
6976
+ else if (flags.todoUiSmart) {
6977
+ process.env.WAYGRAPH_TODO_UI = "smart";
6978
+ }
6785
6979
  if (flags.ffExpand) {
6786
6980
  process.env.WAYGRAPH_FF_EXPAND = "1";
6787
6981
  }
@@ -7030,6 +7224,8 @@ Primary (less is more):
7030
7224
  --full Classic wrap-all block chips (default: carousel)
7031
7225
  --mini Force compact mini panel (Hide pill + Next); alias --stepper-mini
7032
7226
  --todo-left|--todo-right Floating checklist dock side (also ctx.todoPos / WAYGRAPH_TODO_POS)
7227
+ --todo-full Opt out: full checklist, no compact/collision/behind-ring
7228
+ --todo-smart Opt in: compact + collision + behind-ring (default)
7033
7229
  --ff-expand Expand fastForwardComposeBlock inners as separate steps
7034
7230
  --ff-disabled Dispute: expand FF (alias --no-ff); blitz kept on those inners
7035
7231
  --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 } 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
@@ -61,14 +61,30 @@ export declare function showPilotVision(page: Page, selector: string, label: str
61
61
  export type PilotFixtureRing = {
62
62
  selector: string;
63
63
  label: string;
64
+ /** Extra caption line (demo `detail`). */
65
+ detail?: string;
66
+ /** Short badge, e.g. AC / BUG / GATE. */
67
+ tag?: string;
64
68
  /** Any {@link HighlightTone} or alias (`error`/`blue`/…). Normalized on paint. */
65
69
  tone?: HighlightTone | string;
66
70
  size?: "sm" | "md" | "lg" | string;
67
71
  weight?: "normal" | "bold" | string;
72
+ /** Caption text color (CSS). Overrides tone label color when set. */
73
+ color?: string;
74
+ /**
75
+ * Camera zoom while this ring is up (demo parity: scroll + zoom badge;
76
+ * does not CSS-scale the page). Typical `1.25`..`2`.
77
+ */
78
+ zoom?: number;
79
+ /** When false, keep zoom after this paint / holdMs. Default true. */
80
+ zoomOut?: boolean;
81
+ /** Spotlight: dim the rest of the page around this target. */
82
+ focus?: boolean;
68
83
  };
69
84
  /**
70
85
  * Agent-sent highlight fixtures for a live Pilot / auto session.
71
86
  * Painted by {@link showPilotFixtures}; driven via IPC `op: "highlight"`.
87
+ * Mirrors demo stub surface: rings, todos, zoom, focus, device.
72
88
  */
73
89
  export type PilotHighlightFixtures = {
74
90
  /** Replace current fixture rings with these (empty + no todos = clear). */
@@ -79,18 +95,41 @@ export type PilotHighlightFixtures = {
79
95
  todoIndex?: number;
80
96
  /** Optional title above the todo list. */
81
97
  todoTitle?: string;
98
+ /** Todo dock side. Default `right`. */
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;
82
105
  /**
83
106
  * How long fixtures stay visible (ms). `0` = until the next highlight /
84
- * clear. Default `12000`. Vision rings (`showPilotVision`) stay separate.
107
+ * clear. Default `30000` (30s) unless specified.
85
108
  */
86
109
  holdMs?: number;
87
- /** Drop every agent fixture ring/todo immediately. */
110
+ /**
111
+ * Camera zoom toward a selector (demo parity: scrollIntoView + zoom badge).
112
+ * Defaults the zoom target to {@link zoomSelector}, else the last ring's
113
+ * selector (or a ring that sets its own `zoom`). Cleared on `clear: true`
114
+ * or when holdMs expires (if zoomOut).
115
+ */
116
+ zoom?: number;
117
+ /** Element to zoom toward when {@link zoom} is set (defaults to last ring). */
118
+ zoomSelector?: string;
119
+ /** When false, keep zoom after fixtures clear/expire. Default true. */
120
+ zoomOut?: boolean;
121
+ /**
122
+ * Viewport / device fixture (`mobile` | `tablet` | `desktop`, or a full
123
+ * {@link DeviceState}). Applied via Playwright `setViewportSize`.
124
+ */
125
+ device?: DevicePreset | DeviceState;
126
+ /** Drop every agent fixture ring/todo/focus/zoom immediately. */
88
127
  clear?: boolean;
89
128
  };
90
129
  /**
91
130
  * Paint (or clear) agent-authored highlight fixtures on the live page.
92
- * Multi-ring + optional todo strip - what Block `stubBefore` does in
93
- * `waygraph demo`, available to the Pilot agent without going through demo.
131
+ * Multi-ring + optional todo strip + zoom/focus/device - the Pilot equivalent
132
+ * of Block `stubBefore` narration in `waygraph demo`.
94
133
  * Does not import `cli.ts` (see file header).
95
134
  */
96
135
  export declare function showPilotFixtures(page: Page, fixtures: PilotHighlightFixtures): Promise<{
@@ -1,4 +1,4 @@
1
- import { WAYGRAPH_RING_CSS, normalizeHighlightTone, normalizeHighlightSize, normalizeHighlightWeight, toneIconPrefix } 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,16 +88,46 @@ 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;left:14px;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;}
98
+ #wg-pilot-fx-todos[data-pos=left]{left:14px;right:auto;}
99
+ #wg-pilot-fx-todos[data-pos=right]{right:14px;left:auto;}
96
100
  #wg-pilot-fx-todos .wg-pilot-fx-todo-title{font-weight:700;color:#c9a6ff;margin-bottom:6px;}
97
101
  #wg-pilot-fx-todos ol{margin:0;padding-left:18px;}
98
102
  #wg-pilot-fx-todos li{margin:3px 0;color:#ddd;}
99
103
  #wg-pilot-fx-todos li.wg-pilot-fx-todo-done{color:#888;text-decoration:line-through;}
100
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;}
108
+ /* Spotlight (demo focus:true) - dim page, cutout around target. */
109
+ #wg-pilot-fx-focus{position:fixed;z-index:2147483644;pointer-events:none;
110
+ border-radius:12px;box-shadow:0 0 0 9999px rgba(8,4,20,.62);
111
+ opacity:0;transition:opacity .3s ease,left .3s ease,top .3s ease,width .3s ease,height .3s ease;}
112
+ #wg-pilot-fx-focus.wg-in{opacity:1;}
113
+ /* Zoom HUD (demo parity - badge only; no CSS scale of the page). */
114
+ #wg-pilot-fx-zoom{position:fixed;z-index:2147483646;bottom:14px;right:14px;
115
+ display:flex;align-items:center;gap:7px;padding:6px 11px 6px 8px;
116
+ border-radius:999px;background:rgba(20,10,40,.94);color:#f0e8ff;
117
+ border:1px solid rgba(250,204,21,.6);box-shadow:0 6px 18px rgba(0,0,0,.4);
118
+ font:700 12px/1.2 system-ui,sans-serif;pointer-events:none;}
119
+ #wg-pilot-fx-zoom .wg-pilot-fx-zoom-ico{width:18px;height:18px;display:flex;align-items:center;justify-content:center;
120
+ border-radius:6px;background:rgba(250,204,21,.22);}
121
+ #wg-pilot-fx-zoom .wg-pilot-fx-zoom-ico svg{width:14px;height:14px;display:block;}
122
+ #wg-pilot-fx-zoom .wg-pilot-fx-zoom-val{color:#fde68a;font-variant-numeric:tabular-nums;font-weight:800;min-width:3.2em;}
123
+ #wg-pilot-fx-zoom[data-zoomed="1"]{border-color:#fbbf24;}
124
+ #wg-pilot-fx-device{position:fixed;z-index:2147483646;top:14px;right:14px;
125
+ padding:8px 12px;border-radius:10px;background:rgba(20,10,40,.94);color:#f0e8ff;
126
+ border:1px solid rgba(124,58,237,.55);font:700 12px/1.2 system-ui,sans-serif;
127
+ pointer-events:none;box-shadow:0 6px 18px rgba(0,0,0,.35);}
128
+ #wg-pilot-fx-device[data-preset=mobile]{border-color:#3B82F6;}
129
+ #wg-pilot-fx-device[data-preset=tablet]{border-color:#22C55E;}
130
+ #wg-pilot-fx-device[data-preset=desktop]{border-color:#9CA3AF;}
101
131
  ` + WAYGRAPH_RING_CSS;
102
132
  /** Idempotent - safe to call before every update, matching narrate mode's own precedent. */
103
133
  async function ensureInstalled(page) {
@@ -254,8 +284,8 @@ export async function showPilotVision(page, selector, label, tone = "auto") {
254
284
  }
255
285
  /**
256
286
  * Paint (or clear) agent-authored highlight fixtures on the live page.
257
- * Multi-ring + optional todo strip - what Block `stubBefore` does in
258
- * `waygraph demo`, available to the Pilot agent without going through demo.
287
+ * Multi-ring + optional todo strip + zoom/focus/device - the Pilot equivalent
288
+ * of Block `stubBefore` narration in `waygraph demo`.
259
289
  * Does not import `cli.ts` (see file header).
260
290
  */
261
291
  export async function showPilotFixtures(page, fixtures) {
@@ -263,38 +293,169 @@ export async function showPilotFixtures(page, fixtures) {
263
293
  const holdMs = fixtures.clear === true
264
294
  ? 0
265
295
  : fixtures.holdMs === undefined
266
- ? 12_000
296
+ ? 30_000
267
297
  : Math.max(0, fixtures.holdMs);
268
298
  const rings = (fixtures.clear ? [] : (fixtures.rings ?? [])).map((r) => {
269
299
  const tone = normalizeHighlightTone(r.tone);
270
300
  const size = normalizeHighlightSize(r.size);
271
301
  const weight = normalizeHighlightWeight(r.weight);
272
- const icon = toneIconPrefix(tone);
273
- const label = icon && !r.label.startsWith(icon) ? `${icon} ${r.label}` : r.label;
302
+ const caption = { label: r.label, tone };
303
+ if (r.detail)
304
+ caption.detail = r.detail;
305
+ if (r.tag)
306
+ caption.tag = r.tag;
307
+ const label = formatHighlightCaption(caption);
274
308
  return {
275
309
  selector: r.selector,
276
310
  label,
277
311
  tone,
278
312
  size,
279
313
  weight,
314
+ color: typeof r.color === "string" && r.color.trim() ? r.color.trim() : "",
315
+ zoom: typeof r.zoom === "number" && Number.isFinite(r.zoom) && r.zoom > 0
316
+ ? r.zoom
317
+ : undefined,
318
+ zoomOut: r.zoomOut,
319
+ focus: r.focus === true,
280
320
  };
281
321
  });
282
322
  const todos = fixtures.clear ? [] : (fixtures.todos ?? []);
283
323
  const todoIndex = fixtures.todoIndex ?? 0;
284
324
  const todoTitle = fixtures.todoTitle ?? "Plan";
325
+ const todoPos = fixtures.todoPos === "left" ? "left" : "right";
326
+ const todoUi = resolveTodoDockUi(fixtures.todoUi ?? null);
327
+ // Effective zoom: phase zoom, else last ring that authored zoom.
328
+ let zoom = fixtures.clear === true
329
+ ? 1
330
+ : typeof fixtures.zoom === "number" && Number.isFinite(fixtures.zoom)
331
+ ? fixtures.zoom
332
+ : 1;
333
+ let zoomSelector = fixtures.zoomSelector ??
334
+ (rings.length > 0 ? rings[rings.length - 1].selector : undefined);
335
+ let zoomOut = fixtures.zoomOut !== false;
336
+ if (!fixtures.clear && zoom <= 1.001) {
337
+ for (let i = rings.length - 1; i >= 0; i--) {
338
+ const r = rings[i];
339
+ if (r.zoom !== undefined && r.zoom > 1.001) {
340
+ zoom = r.zoom;
341
+ zoomSelector = r.selector;
342
+ if (r.zoomOut !== undefined)
343
+ zoomOut = r.zoomOut !== false;
344
+ break;
345
+ }
346
+ }
347
+ }
348
+ else if (!fixtures.clear) {
349
+ // Phase zoomOut can still be overridden by the targeted ring.
350
+ const hit = rings.find((r) => r.selector === zoomSelector);
351
+ if (hit?.zoomOut !== undefined)
352
+ zoomOut = hit.zoomOut !== false;
353
+ }
354
+ // Device viewport (Node-side) before paint so rings land on the new size.
355
+ if (fixtures.clear === true) {
356
+ // Leave viewport alone on clear - only drop overlays.
357
+ }
358
+ else if (fixtures.device !== undefined) {
359
+ const d = resolveDeviceState(fixtures.device);
360
+ if (d) {
361
+ await page
362
+ .setViewportSize({
363
+ width: Math.max(200, Math.floor(d.viewport.width)),
364
+ height: Math.max(200, Math.floor(d.viewport.height)),
365
+ })
366
+ .catch(() => { });
367
+ }
368
+ }
369
+ const deviceLabel = fixtures.clear === true
370
+ ? ""
371
+ : fixtures.device === undefined
372
+ ? ""
373
+ : typeof fixtures.device === "string"
374
+ ? fixtures.device
375
+ : fixtures.device.preset || "device";
285
376
  return page
286
- .evaluate(({ rings, todos, todoIndex, todoTitle, holdMs }) => {
377
+ .evaluate(({ rings, todos, todoIndex, todoTitle, todoPos, holdMs, zoom, zoomSelector, zoomOut, clearAll, deviceLabel, todoUi, }) => {
287
378
  const w = window;
288
379
  if (w.__wgPilotFxHideTimer)
289
380
  clearTimeout(w.__wgPilotFxHideTimer);
381
+ const clearFocus = () => {
382
+ const veil = document.getElementById("wg-pilot-fx-focus");
383
+ if (!veil)
384
+ return;
385
+ veil.classList.remove("wg-in");
386
+ setTimeout(() => {
387
+ const v = document.getElementById("wg-pilot-fx-focus");
388
+ if (v && !v.classList.contains("wg-in"))
389
+ v.remove();
390
+ }, 320);
391
+ };
392
+ const applyFocus = (box) => {
393
+ let veil = document.getElementById("wg-pilot-fx-focus");
394
+ if (!veil) {
395
+ veil = document.createElement("div");
396
+ veil.id = "wg-pilot-fx-focus";
397
+ document.documentElement.appendChild(veil);
398
+ }
399
+ const pad = 10;
400
+ veil.style.left = `${Math.max(0, box.x - pad)}px`;
401
+ veil.style.top = `${Math.max(0, box.y - pad)}px`;
402
+ veil.style.width = `${Math.max(8, box.width + pad * 2)}px`;
403
+ veil.style.height = `${Math.max(8, box.height + pad * 2)}px`;
404
+ void veil.offsetWidth;
405
+ veil.classList.add("wg-in");
406
+ };
407
+ const setZoomBadge = (scale) => {
408
+ const level = Number.isFinite(scale) && scale > 0 ? scale : 1;
409
+ let badge = document.getElementById("wg-pilot-fx-zoom");
410
+ if (!badge) {
411
+ badge = document.createElement("div");
412
+ badge.id = "wg-pilot-fx-zoom";
413
+ document.documentElement.appendChild(badge);
414
+ }
415
+ const zoomed = level > 1.001 || level < 0.999;
416
+ badge.dataset.zoomed = zoomed ? "1" : "0";
417
+ badge.innerHTML =
418
+ '<span class="wg-pilot-fx-zoom-ico"><svg viewBox="0 0 24 24" fill="none" stroke="#fde68a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg></span>' +
419
+ `<span class="wg-pilot-fx-zoom-val">${level.toFixed(2)}\u00d7</span>`;
420
+ };
421
+ const clearZoom = () => {
422
+ // Undo any leftover CSS-scale from older Pilot zoom path.
423
+ const root = document.documentElement;
424
+ root.style.removeProperty("transform");
425
+ root.style.removeProperty("transform-origin");
426
+ root.style.removeProperty("transition");
427
+ document.getElementById("wg-pilot-fx-zoom")?.remove();
428
+ };
429
+ const setDeviceChip = (label) => {
430
+ document.getElementById("wg-pilot-fx-device")?.remove();
431
+ if (!label)
432
+ return;
433
+ const chip = document.createElement("div");
434
+ chip.id = "wg-pilot-fx-device";
435
+ chip.dataset.preset = label;
436
+ chip.textContent = `device \u00b7 ${label}`;
437
+ document.documentElement.appendChild(chip);
438
+ };
439
+ w.__wgPilotFxZoomOutOnHide = zoomOut;
290
440
  const clearFx = () => {
291
441
  document.querySelectorAll(".wg-pilot-fx-ring, .wg-pilot-fx-label").forEach((el) => el.remove());
292
442
  document.getElementById("wg-pilot-fx-todos")?.remove();
443
+ clearFocus();
444
+ if (w.__wgPilotFxZoomOutOnHide !== false || clearAll)
445
+ clearZoom();
446
+ if (clearAll) {
447
+ document.getElementById("wg-pilot-fx-device")?.remove();
448
+ clearZoom();
449
+ }
293
450
  };
294
451
  w.__wgPilotFxClear = clearFx;
295
452
  clearFx();
453
+ if (clearAll) {
454
+ return { painted: 0, missing: [] };
455
+ }
296
456
  const missing = [];
297
457
  let painted = 0;
458
+ let focusBox = null;
298
459
  for (const ring of rings) {
299
460
  const el = document.querySelector(ring.selector);
300
461
  if (!el) {
@@ -306,14 +467,11 @@ export async function showPilotFixtures(page, fixtures) {
306
467
  missing.push(ring.selector);
307
468
  continue;
308
469
  }
309
- const tone = ring.tone;
310
- const size = ring.size;
311
- const weight = ring.weight;
312
- const pad = size === "sm" ? 3 : size === "lg" ? 10 : 6;
470
+ const pad = ring.size === "sm" ? 3 : ring.size === "lg" ? 10 : 6;
313
471
  const ringEl = document.createElement("div");
314
472
  ringEl.className = "wg-pilot-fx-ring";
315
- ringEl.dataset.tone = tone;
316
- ringEl.dataset.size = size;
473
+ ringEl.dataset.tone = ring.tone;
474
+ ringEl.dataset.size = ring.size;
317
475
  ringEl.style.left = `${rect.left - pad}px`;
318
476
  ringEl.style.top = `${rect.top - pad}px`;
319
477
  ringEl.style.width = `${Math.max(4, rect.width + pad * 2)}px`;
@@ -321,11 +479,14 @@ export async function showPilotFixtures(page, fixtures) {
321
479
  ringEl.style.opacity = "1";
322
480
  const labelEl = document.createElement("div");
323
481
  labelEl.className = "wg-pilot-fx-label";
324
- labelEl.dataset.tone = tone;
325
- labelEl.dataset.size = size;
326
- labelEl.dataset.weight = weight;
482
+ labelEl.dataset.tone = ring.tone;
483
+ labelEl.dataset.size = ring.size;
484
+ labelEl.dataset.weight = ring.weight;
327
485
  labelEl.textContent = ring.label;
328
486
  labelEl.style.opacity = "1";
487
+ if (ring.color) {
488
+ labelEl.style.color = ring.color;
489
+ }
329
490
  document.documentElement.appendChild(ringEl);
330
491
  document.documentElement.appendChild(labelEl);
331
492
  const lw = labelEl.offsetWidth;
@@ -344,14 +505,29 @@ export async function showPilotFixtures(page, fixtures) {
344
505
  labelEl.style.left = `${labelLeft}px`;
345
506
  labelEl.style.top = `${labelTop}px`;
346
507
  painted += 1;
508
+ if (ring.focus) {
509
+ focusBox = { x: rect.left, y: rect.top, width: rect.width, height: rect.height };
510
+ }
347
511
  }
348
512
  if (todos.length > 0) {
349
513
  const dock = document.createElement("div");
350
514
  dock.id = "wg-pilot-fx-todos";
515
+ dock.dataset.pos = todoPos;
351
516
  const title = document.createElement("div");
352
517
  title.className = "wg-pilot-fx-todo-title";
353
518
  title.textContent = todoTitle;
354
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
+ }
355
531
  const ol = document.createElement("ol");
356
532
  todos.forEach((text, i) => {
357
533
  const li = document.createElement("li");
@@ -360,16 +536,81 @@ export async function showPilotFixtures(page, fixtures) {
360
536
  li.className = "wg-pilot-fx-todo-done";
361
537
  else if (i === todoIndex)
362
538
  li.className = "wg-pilot-fx-todo-now";
539
+ if (compact && (i < start || i >= end))
540
+ li.classList.add("wg-todo-fold");
363
541
  ol.appendChild(li);
364
542
  });
365
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
+ }
366
550
  document.documentElement.appendChild(dock);
367
551
  }
368
- if (holdMs > 0 && (painted > 0 || todos.length > 0)) {
552
+ if (focusBox)
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
+ }
571
+ if (zoom > 1.001 && zoomSelector) {
572
+ const target = document.querySelector(zoomSelector);
573
+ if (target) {
574
+ target.scrollIntoView({
575
+ block: "center",
576
+ inline: "center",
577
+ behavior: "instant",
578
+ });
579
+ // Re-apply focus after scroll so the cutout tracks the new rect.
580
+ if (focusBox) {
581
+ const r = target.getBoundingClientRect();
582
+ applyFocus({ x: r.left, y: r.top, width: r.width, height: r.height });
583
+ }
584
+ setZoomBadge(zoom);
585
+ }
586
+ else {
587
+ missing.push(zoomSelector);
588
+ }
589
+ }
590
+ else if (zoom > 1.001) {
591
+ setZoomBadge(zoom);
592
+ }
593
+ if (deviceLabel)
594
+ setDeviceChip(deviceLabel);
595
+ if (holdMs > 0 &&
596
+ (painted > 0 || todos.length > 0 || zoom > 1.001 || !!deviceLabel)) {
369
597
  w.__wgPilotFxHideTimer = setTimeout(() => clearFx(), holdMs);
370
598
  }
371
599
  return { painted, missing };
372
- }, { rings, todos, todoIndex, todoTitle, holdMs })
600
+ }, {
601
+ rings,
602
+ todos,
603
+ todoIndex,
604
+ todoTitle,
605
+ todoPos,
606
+ holdMs,
607
+ zoom,
608
+ zoomSelector: zoomSelector ?? "",
609
+ zoomOut,
610
+ clearAll: fixtures.clear === true,
611
+ deviceLabel,
612
+ todoUi,
613
+ })
373
614
  .catch(() => ({ painted: 0, missing: rings.map((r) => r.selector) }));
374
615
  }
375
616
  /**
@@ -0,0 +1,8 @@
1
+ {
2
+ "sessionId": "03710cfc",
3
+ "socketPath": "/tmp/waygraph-auto/03710cfc.sock",
4
+ "pid": 255515,
5
+ "projectDir": "/home/dan/Desktop/Work/zsign/services/waygraph/examples/saucedemo",
6
+ "startedAt": "2026-09-21T09:37:58.750Z",
7
+ "headless": false
8
+ }
File without changes
File without changes
File without changes
@@ -0,0 +1,8 @@
1
+ {
2
+ "sessionId": "605d965b",
3
+ "socketPath": "/tmp/waygraph-auto/605d965b.sock",
4
+ "pid": 247729,
5
+ "projectDir": "/home/dan/Desktop/Work/zsign/services/waygraph/examples/saucedemo",
6
+ "startedAt": "2026-09-21T09:34:04.318Z",
7
+ "headless": false
8
+ }
File without changes
File without changes
@@ -0,0 +1,8 @@
1
+ {
2
+ "sessionId": "6b667734",
3
+ "socketPath": "/tmp/waygraph-auto/6b667734.sock",
4
+ "pid": 245167,
5
+ "projectDir": "/home/dan/Desktop/Work/zsign/services/waygraph/examples/saucedemo",
6
+ "startedAt": "2026-09-21T09:33:13.876Z",
7
+ "headless": false
8
+ }
File without changes
@@ -0,0 +1,4 @@
1
+ (node:254673) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
2
+ (Use `node --trace-warnings ...` to show where the warning was created)
3
+ (node:254673) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
4
+ (Use `node --trace-warnings ...` to show where the warning was created)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.15.5",
3
+ "version": "0.15.8",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -27,15 +27,25 @@ sending picks.
27
27
 
28
28
  Block `stubBefore` / `withHighlightFixtures` only paint in `waygraph demo`. In Pilot they
29
29
  land in `auto trace` as metadata but do **not** draw on the page. When a human is watching
30
- a headful session (or you want to narrate intent before a send), paint yourself:
30
+ a headful session (or you want to narrate intent before a send), paint yourself with the
31
+ same fixture surface:
31
32
 
32
33
  ```
33
- waygraph auto highlight <sessionId> '{"rings":[{"selector":"#user-name","label":"Username","tone":"planned"},{"selector":"#password","label":"Password","tone":"info"}],"todos":["Fill username","Fill password","Submit"],"todoIndex":0,"holdMs":0}'
34
+ waygraph auto highlight <sessionId> '{"rings":[{"selector":"#user-name","label":"Username","tone":"planned","focus":true},{"selector":"#password","label":"Password","tone":"info","detail":"secret"}],"todos":["Fill username","Fill password","Submit"],"todoIndex":0,"zoom":1.5,"holdMs":0}'
35
+ waygraph auto highlight <sessionId> '{"device":"mobile","rings":[{"selector":".inventory_item:last-child","label":"Last item","zoom":1.6,"focus":true}],"holdMs":30000}'
34
36
  waygraph auto highlight <sessionId> '{"clear":true}'
35
37
  ```
36
38
 
39
+ Supported fields (demo parity):
40
+ - `rings[]`: `selector`, `label`, `detail`, `tag`, `tone`, `size`, `weight`, `color`, `zoom`, `zoomOut`, `focus`
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")
43
+ - `zoom` / `zoomSelector` / `zoomOut` - scroll + zoom badge (not CSS page scale)
44
+ - `device`: `mobile` | `tablet` | `desktop` (Playwright viewport)
45
+ - `holdMs`: ms to keep fixtures; `0` = until the next highlight / clear (default 30000)
46
+ - `clear`: true drops rings/todos/focus/zoom badge
47
+
37
48
  - `tone`: all demo tones - `planned` | `auto` | `info` | `warning` | `danger` | `success` | `orange` (aliases like `error`/`blue`/`green` work too)
38
- - `holdMs`: ms to keep fixtures; `0` = until the next highlight / clear (default 12000)
39
49
  - Bottom-left toast shows `Highlighting: …` (same strip as `Running (Dom): …`)
40
50
  - Missing selectors are listed in the JSON response (`missing`) - not a hard failure
41
51
  - Does not replace `auto send` - paint, then run the real Block