waygraph 0.16.2 → 0.16.4

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.
Files changed (123) hide show
  1. package/README.md +12 -0
  2. package/dist/analysis/practices-check.d.ts +1 -1
  3. package/dist/analysis/practices-check.js +36 -0
  4. package/dist/beta.d.ts +6 -0
  5. package/dist/beta.js +5 -0
  6. package/dist/cli/commands/codemod-flows.js +2 -2
  7. package/dist/cli/commands/codemod-stubs.js +43 -1
  8. package/dist/cli/commands/pilot.js +3 -16
  9. package/dist/cli/commands/session-ops.js +26 -1
  10. package/dist/cli/flags.d.ts +1 -1
  11. package/dist/cli/flags.js +1 -1
  12. package/dist/cli/usage.js +6 -0
  13. package/dist/demo/narration/stub-ctx.js +126 -124
  14. package/dist/demo/overlay/cursor.js +1 -1
  15. package/dist/demo/overlay/inpage/banner.js +5 -0
  16. package/dist/demo/overlay/inpage/core.js +1 -0
  17. package/dist/demo/overlay/inpage/network.js +1 -1
  18. package/dist/demo/overlay/inpage/todos.js +0 -15
  19. package/dist/demo/overlay/network-capture.js +2 -2
  20. package/dist/demo/overlay/overlay-css.js +18 -21
  21. package/dist/demo/overlay/overlay-stage.js +1 -1
  22. package/dist/demo/overlay/phase-fixtures.js +5 -5
  23. package/dist/demo/overlay/pilot/shell.js +7 -5
  24. package/dist/demo/overlay/ui/components/ring.js +2 -2
  25. package/dist/demo/overlay/ui/overlay.css +98 -35
  26. package/dist/demo/overlay/ui/shadow.js +69 -2
  27. package/dist/demo/stepper/demo-log.js +8 -8
  28. package/dist/demo/stepper/slides.d.ts +1 -1
  29. package/dist/demo/stepper/slides.js +1 -1
  30. package/dist/demo/stepper/stepper.js +1 -1
  31. package/dist/engine/block/kind.d.ts +3 -0
  32. package/dist/engine/block/kind.js +11 -0
  33. package/dist/engine/errors.d.ts +12 -0
  34. package/dist/engine/errors.js +12 -0
  35. package/dist/engine/read/read-block.d.ts +61 -0
  36. package/dist/engine/read/read-block.js +94 -0
  37. package/dist/engine/read/read-key.d.ts +32 -0
  38. package/dist/engine/read/read-key.js +34 -0
  39. package/dist/engine/read/read-page.d.ts +19 -0
  40. package/dist/engine/read/read-page.js +50 -0
  41. package/dist/engine/read/readers.d.ts +82 -0
  42. package/dist/engine/read/readers.js +43 -0
  43. package/dist/engine/read/run-read.d.ts +17 -0
  44. package/dist/engine/read/run-read.js +156 -0
  45. package/dist/engine/run/connect.js +3 -2
  46. package/dist/engine/run/events.d.ts +28 -10
  47. package/dist/engine/run/events.js +52 -38
  48. package/dist/engine/run/run.js +25 -13
  49. package/dist/engine/stub/stub.d.ts +26 -21
  50. package/dist/index.d.ts +7 -4
  51. package/dist/index.js +9 -4
  52. package/dist/live/auto/auto.js +2 -3
  53. package/dist/live/auto/menu.js +3 -2
  54. package/dist/live/auto-session/auto-session.d.ts +9 -2
  55. package/dist/live/auto-session/auto-session.js +23 -20
  56. package/dist/live/auto-session/feed.d.ts +16 -0
  57. package/dist/live/auto-session/feed.js +71 -0
  58. package/dist/live/auto-session/helpers.d.ts +7 -0
  59. package/dist/live/auto-session/helpers.js +9 -0
  60. package/dist/live/auto-session/ipc.d.ts +15 -47
  61. package/dist/live/auto-session/ipc.js +34 -4
  62. package/dist/live/auto-session/protocol.d.ts +127 -0
  63. package/dist/live/auto-session/protocol.js +6 -0
  64. package/dist/live/auto-session/read.d.ts +25 -0
  65. package/dist/live/auto-session/read.js +50 -0
  66. package/dist/live/traverse/worker.d.ts +1 -0
  67. package/dist/live/traverse/worker.js +6 -1
  68. package/dist/manifest/manifest.d.ts +22 -2
  69. package/dist/manifest/manifest.js +27 -1
  70. package/dist/manifest/project.d.ts +10 -1
  71. package/dist/manifest/project.js +9 -12
  72. package/dist/manifest/read-blocks.d.ts +2 -0
  73. package/dist/manifest/read-blocks.js +23 -0
  74. package/dist/manifest/scan.d.ts +3 -2
  75. package/dist/manifest/scan.js +8 -4
  76. package/dist/runner/runner.d.ts +0 -1
  77. package/dist/runner/runner.js +6 -6
  78. package/dist/runtime/full-launch.d.ts +2 -1
  79. package/dist/runtime/full-launch.js +29 -27
  80. package/dist/runtime/run-log.d.ts +2 -0
  81. package/dist/runtime/run-log.js +13 -6
  82. package/examples/saucedemo/README.md +2 -2
  83. package/examples/saucedemo/src/blocks/SITE-MAP.md +3 -0
  84. package/examples/saucedemo/src/blocks/saucedemo-web/cart/cart.read.block.ts +26 -0
  85. package/examples/saucedemo/src/blocks/saucedemo-web/header.read.block.ts +25 -0
  86. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.read.block.ts +28 -0
  87. package/examples/saucedemo/src/flows/api-demo.flow.ts +1 -1
  88. package/examples/saucedemo/src/flows/cart-bulk.flow.ts +1 -1
  89. package/examples/saucedemo/src/flows/cart-continue.flow.ts +1 -1
  90. package/examples/saucedemo/src/flows/catalog-logout.flow.ts +1 -1
  91. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +1 -1
  92. package/examples/saucedemo/src/flows/checkout.flow.ts +11 -11
  93. package/examples/saucedemo/src/flows/login.flow.ts +1 -1
  94. package/examples/saucedemo/src/flows/shop.flow.ts +1 -1
  95. package/examples/saucedemo/src/flows/stub-on-error.flow.ts +1 -1
  96. package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +1 -1
  97. package/examples/saucedemo/src/states/checkout.reads.ts +9 -0
  98. package/examples/saucedemo/src/stubs/device-showcase.ts +37 -37
  99. package/examples/saucedemo/src/stubs/finish-order.ts +2 -2
  100. package/examples/saucedemo/src/stubs/login.ts +5 -5
  101. package/package.json +5 -1
  102. package/templates/quickstart/src/flows/api-demo.flow.ts +1 -1
  103. package/templates/quickstart/src/flows/cart-bulk.flow.ts +1 -1
  104. package/templates/quickstart/src/flows/cart-continue.flow.ts +1 -1
  105. package/templates/quickstart/src/flows/catalog-logout.flow.ts +1 -1
  106. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +1 -1
  107. package/templates/quickstart/src/flows/checkout.flow.ts +11 -11
  108. package/templates/quickstart/src/flows/login.flow.ts +1 -1
  109. package/templates/quickstart/src/flows/shop.flow.ts +1 -1
  110. package/templates/quickstart/src/flows/stub-on-error.flow.ts +1 -1
  111. package/templates/quickstart/src/flows/viewer-blocked.flow.ts +1 -1
  112. package/templates/quickstart/src/stubs/device-showcase.ts +37 -37
  113. package/templates/quickstart/src/stubs/finish-order.ts +2 -2
  114. package/templates/quickstart/src/stubs/login.ts +5 -5
  115. package/templates/scaffold/src/flows/(external)/hoppscotch/auth.flow.ts +1 -1
  116. package/templates/scaffold/src/flows/(external)/hoppscotch/get.flow.ts +1 -1
  117. package/templates/scaffold/src/flows/(external)/hoppscotch/post.flow.ts +1 -1
  118. package/templates/scaffold/src/flows/(user-stories)/email-verification.flow.ts +4 -4
  119. package/templates/scaffold/src/flows/README.md +1 -1
  120. package/templates/scaffold/src/flows/home/cart.flow.ts +7 -7
  121. package/templates/scaffold/src/flows/home/hello.flow.ts +8 -8
  122. package/templates/skills/waygraph-convention.skill.md +10 -2
  123. package/templates/skills/waygraph-pilot.skill.md +7 -0
@@ -209,6 +209,7 @@ export function installCore({ envAutoplay, todoDockUi }) {
209
209
  continue;
210
210
  const cur = dock.dataset.pos === "right" ? "right" : "left";
211
211
  dock.dataset.pos = cur === "right" ? "left" : "right";
212
+ __wgAdd(dock);
212
213
  try {
213
214
  localStorage.setItem("wg-todo-pos", dock.dataset.pos);
214
215
  }
@@ -145,7 +145,7 @@ export function addNetworkEntry(entry) {
145
145
  const countEl = panel.querySelector(".wg-net-count");
146
146
  if (countEl)
147
147
  countEl.textContent = String(w.__wgNetworkEntries.length);
148
- // Always captured, only shown when explicitly turned on (ctx.showNetworkTab(); default hidden -
148
+ // Always captured, only shown when explicitly turned on (ctx.ui.showNetworkTab(); default hidden -
149
149
  // see StubCtx's own doc comment) - the DOM/row still gets built either way, so toggling visible
150
150
  // later shows the full history immediately, not just entries captured after that point.
151
151
  if (w.__wgNetworkTabVisible) {
@@ -38,19 +38,6 @@ export function installTodos({ todoPos }) {
38
38
  // Drop any in-panel leftover (old builds put #wg-todos in .wg-body).
39
39
  __wgQA("#wg-panel #wg-todos").forEach((el) => el.remove());
40
40
  const allDocks = () => [...__wgQA(".wg-todo-dock, #wg-todo-dock")].filter((el, i, arr) => arr.indexOf(el) === i);
41
- const relayoutTodoDocks = () => {
42
- const docks = allDocks();
43
- // Must match .wg-todo-dock/#wg-todo-dock's own CSS `top` (demo/overlay/overlay-css.ts,
44
- // ui/css/dock.css) - this inline style OVERRIDES that CSS the instant more than one
45
- // dock exists (multi-todo stacking), so a CSS-only change here is silently underwater;
46
- // real bug this was: the CSS top was bumped 72 -> 104 for banner clearance, but this
47
- // hardcoded 72 kept winning at runtime and nothing actually moved.
48
- let top = 104;
49
- for (const el of docks) {
50
- el.style.top = top + "px";
51
- top += Math.max(48, el.getBoundingClientRect().height) + 10;
52
- }
53
- };
54
41
  const findDock = (key) => {
55
42
  if (key === "_default") {
56
43
  return (__wgQ('.wg-todo-dock[data-wg-todo-key="_default"]') ||
@@ -88,7 +75,6 @@ export function installTodos({ todoPos }) {
88
75
  else {
89
76
  allDocks().forEach((el) => el.remove());
90
77
  }
91
- relayoutTodoDocks();
92
78
  return;
93
79
  }
94
80
  if (sync === "set" && replace) {
@@ -285,7 +271,6 @@ export function installTodos({ todoPos }) {
285
271
  });
286
272
  }
287
273
  dock._wgLastPayload = syncPayload;
288
- relayoutTodoDocks();
289
274
  if (window.__wgStampModal) {
290
275
  window.__wgStampModal(dock, "todos", { ready: true });
291
276
  }
@@ -55,6 +55,8 @@ export async function restoreNetworkHistory(page) {
55
55
  }
56
56
  }
57
57
  export function installNetworkCapture(page) {
58
+ // defineApiBlock reports its calls through the engine's hook; show them in the same panel.
59
+ setApiCallRecorder((p, entry) => recordNetworkEntry(p, entry));
58
60
  if (page.__wgNetworkCaptureInstalled)
59
61
  return;
60
62
  page.__wgNetworkCaptureInstalled = true;
@@ -123,5 +125,3 @@ export function installNetworkCapture(page) {
123
125
  });
124
126
  });
125
127
  }
126
- // defineApiBlock reports its calls through core's hook; show them in the same panel.
127
- setApiCallRecorder((page, entry) => recordNetworkEntry(page, entry));
@@ -15,12 +15,12 @@ import { SURFACE } from "./ui/tokens.js";
15
15
  // never collides with that bundle.
16
16
  export const RUNNER_EXTRA_CSS =
17
17
  // Spotlight: dim everything except the highlight target (focus: true).
18
- "#wg-focus-veil{position:fixed;z-index:2147483644;pointer-events:none;" +
18
+ "#wg-focus-veil{position:fixed;z-index:var(--wg-z-focus);pointer-events:none;" +
19
19
  "border-radius:12px;box-shadow:0 0 0 9999px rgba(8,4,20,.62);" +
20
20
  "opacity:0;transition:opacity .3s ease,left .3s ease,top .3s ease,width .3s ease,height .3s ease;}" +
21
21
  "#wg-focus-veil.wg-in{opacity:1;}" +
22
22
  // Touch swipe trail (mobile/tablet orientation + device theater).
23
- "#wg-swipe-layer{position:fixed;inset:0;z-index:2147483646;pointer-events:none;" +
23
+ "#wg-swipe-layer{position:fixed;inset:0;z-index:var(--wg-z-cursor);pointer-events:none;" +
24
24
  "overflow:hidden;}" +
25
25
  "#wg-swipe-layer .wg-swipe-dot{position:absolute;width:14px;height:14px;margin:-7px 0 0 -7px;" +
26
26
  "border-radius:50%;background:rgba(147,197,253,.55);border:2px solid rgba(255,255,255,.85);" +
@@ -34,8 +34,8 @@ export const RUNNER_EXTRA_CSS =
34
34
  "border:1px solid rgba(56,189,248,.55);font:700 12px/1.2 system-ui,sans-serif;" +
35
35
  "letter-spacing:.04em;text-transform:uppercase;opacity:0;transition:opacity .25s ease;}" +
36
36
  "#wg-swipe-layer.wg-in .wg-swipe-label{opacity:1;}" +
37
- "#wg-panel{position:fixed;z-index:2147483647;left:50%;bottom:12px;transform:translateX(-50%);" +
38
- "max-width:min(92vw,640px);max-height:calc(100vh - 24px);overflow-y:auto;box-sizing:border-box;" +
37
+ "#wg-panel{z-index:var(--wg-z-top);" +
38
+ "width:min(92vw,640px);overflow-y:auto;box-sizing:border-box;" +
39
39
  "background:" + SURFACE + ";color:#fff;border-radius:14px;" +
40
40
  "padding:16px 20px;font:14px/1.4 system-ui,sans-serif;box-shadow:0 12px 30px rgba(0,0,0,.35);" +
41
41
  // .06s was tuned back when the panel was removed and recreated on
@@ -78,16 +78,14 @@ export const RUNNER_EXTRA_CSS =
78
78
  // Todo checklist(s) float outside #wg-panel so --mini / Hide never hide them.
79
79
  // Multiple docks (one per todoId) are supported - stacked on the same side.
80
80
  // Click a dock to slide left <-> right (WAYGRAPH_TODO_POS / --todo-left|right).
81
- ".wg-todo-dock,#wg-todo-dock{position:fixed;z-index:2147483646;top:104px;left:14px;" +
82
- "width:min(280px,42vw);max-height:calc(100vh - 100px);overflow:auto;box-sizing:border-box;" +
81
+ ".wg-todo-dock,#wg-todo-dock{z-index:var(--wg-z-dock);" +
82
+ "width:min(280px,42vw);overflow:auto;box-sizing:border-box;" +
83
83
  "padding:10px 12px;background:" + SURFACE + ";color:#fff;border-radius:12px;" +
84
84
  "border:1px solid rgba(124,58,237,.45);box-shadow:0 8px 24px rgba(0,0,0,.35);" +
85
85
  "pointer-events:auto;cursor:pointer;" +
86
- "transition:transform .4s cubic-bezier(.22,1,.36,1),top .35s ease,opacity .25s ease,z-index 0s;transform:translateX(0);}" +
87
- ".wg-todo-dock[data-pos=right],#wg-todo-dock[data-pos=right]{transform:translateX(calc(100vw - 100% - 28px));}" +
88
- ".wg-todo-dock[data-pos=left],#wg-todo-dock[data-pos=left]{transform:translateX(0);}" +
86
+ "transition:opacity .25s ease,z-index 0s;}" +
89
87
  /* While a highlight ring is up: sit under the ring so captions stay readable. */
90
- ".wg-todo-dock.wg-todo-behind,#wg-todo-dock.wg-todo-behind{z-index:2147483643;opacity:.42;}" +
88
+ ".wg-todo-dock.wg-todo-behind,#wg-todo-dock.wg-todo-behind{z-index:var(--wg-z-focus);opacity:.42;}" +
91
89
  /* Collision tuck: shrink so a mid-page ring can still breathe. */
92
90
  ".wg-todo-dock[data-tucked=\"1\"],#wg-todo-dock[data-tucked=\"1\"]{max-height:min(28vh,220px);}" +
93
91
  /* Compact + cap: fold overflow rows; hover / data-expanded reveals more. */
@@ -103,7 +101,7 @@ export const RUNNER_EXTRA_CSS =
103
101
  // Always-on zoom HUD - bottom-right (top-left is crowded: todos + banners). Violet, matching the
104
102
  // rest of the overlay's design tokens - this used to be a lone amber/yellow badge, the one
105
103
  // surface that never got pulled into the shared palette when everything else did.
106
- "#wg-zoom-badge{position:fixed;z-index:2147483646;bottom:14px;right:14px;" +
104
+ "#wg-zoom-badge{z-index:var(--wg-z-chrome);" +
107
105
  "display:flex;align-items:center;gap:7px;padding:6px 11px 6px 8px;" +
108
106
  "border-radius:999px;background:" + SURFACE + ";color:#f0e8ff;" +
109
107
  "border:1px solid rgba(124,58,237,.55);box-shadow:0 6px 18px rgba(0,0,0,.4);" +
@@ -115,16 +113,16 @@ export const RUNNER_EXTRA_CSS =
115
113
  "#wg-zoom-badge .wg-zoom-val{color:#c9a6ff;font-variant-numeric:tabular-nums;font-weight:800;min-width:3.2em;}" +
116
114
  "#wg-zoom-badge[data-zoomed=\"1\"]{border-color:#7C3AED;}" +
117
115
  // Typing chip (method fills: username / password / etc.)
118
- "#wg-typing-badge{position:fixed;z-index:2147483646;bottom:88px;left:50%;transform:translateX(-50%);" +
116
+ "#wg-typing-badge{z-index:var(--wg-z-chrome);" +
119
117
  "display:flex;align-items:center;gap:8px;padding:8px 14px;" +
120
118
  "border-radius:999px;background:rgba(20,10,40,.95);color:#e0f2fe;" +
121
119
  "border:1px solid rgba(56,189,248,.55);box-shadow:0 8px 22px rgba(0,0,0,.4);" +
122
120
  "font:700 12px/1.2 system-ui,sans-serif;pointer-events:none;" +
123
- "opacity:0;transition:opacity .2s ease;}" +
121
+ "opacity:0;transition-duration:.2s;}" +
124
122
  "#wg-typing-badge.wg-in{opacity:1;}" +
125
123
  "#wg-typing-badge .wg-ty-dots{letter-spacing:.15em;color:#7dd3fc;}" +
126
124
  // Device toast + chip (0.13.2+) - icon toast on fixture change, then compact chip.
127
- "#wg-device-toast{position:fixed;z-index:2147483647;top:14px;right:14px;" +
125
+ "#wg-device-toast{z-index:var(--wg-z-toast);" +
128
126
  "display:flex;align-items:center;gap:10px;min-width:200px;max-width:min(92vw,320px);" +
129
127
  "padding:12px 14px;border-radius:14px;background:rgba(20,10,40,.96);color:#f0e8ff;" +
130
128
  "border:1px solid rgba(124,58,237,.55);box-shadow:0 10px 28px rgba(0,0,0,.4);" +
@@ -146,20 +144,21 @@ export const RUNNER_EXTRA_CSS =
146
144
  "#wg-device-toast .wg-dev-title{font:800 13px/1.2 system-ui,sans-serif;color:#fff;}" +
147
145
  "#wg-device-toast .wg-dev-sub{margin-top:3px;font:600 11px/1.3 system-ui,sans-serif;" +
148
146
  "color:#c9a6ff;letter-spacing:.02em;}" +
149
- "#wg-device-badge{position:fixed;z-index:2147483646;top:14px;right:14px;" +
147
+ "#wg-device-badge{z-index:var(--wg-z-chrome);" +
150
148
  "display:inline-flex;align-items:center;gap:6px;padding:6px 10px 6px 8px;" +
151
149
  "border-radius:999px;background:rgba(20,10,40,.92);color:#f0e8ff;" +
152
150
  "border:1px solid rgba(124,58,237,.5);font:700 11px/1.2 system-ui,sans-serif;" +
153
151
  "letter-spacing:.04em;text-transform:uppercase;pointer-events:none;" +
154
152
  "box-shadow:0 4px 14px rgba(0,0,0,.3);opacity:0;transform:translateY(-4px);" +
155
- "transition:opacity .3s ease,transform .35s cubic-bezier(.22,1,.36,1);}" +
153
+ "transition-duration:.35s;transition-timing-function:cubic-bezier(.22,1,.36,1);}" +
156
154
  "#wg-device-badge.wg-in{opacity:1;transform:translateY(0);}" +
157
155
  "#wg-device-badge[data-preset=mobile]{border-color:#3B82F6;}" +
158
156
  "#wg-device-badge[data-preset=tablet]{border-color:#22C55E;}" +
159
157
  "#wg-device-badge[data-preset=desktop]{border-color:#9CA3AF;}" +
160
158
  "#wg-device-badge .wg-dev-icon{width:16px;height:16px;display:flex;align-items:center;justify-content:center;}" +
161
159
  "#wg-device-badge .wg-dev-icon svg{width:14px;height:14px;display:block;}" +
162
- "#wg-device-badge .wg-dev-label{white-space:nowrap;}" +
160
+ "#wg-device-badge{overflow:hidden;}" +
161
+ "#wg-device-badge .wg-dev-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;}" +
163
162
  "#wg-cursor[data-touch=1]{width:28px;height:28px;}" +
164
163
  "#wg-todos,.wg-todos-list{list-style:none;margin:0 0 12px;padding:8px 10px;background:#0f0620;border-radius:8px;" +
165
164
  "border:1px solid #3a2a60;}" +
@@ -270,16 +269,14 @@ export const RUNNER_EXTRA_CSS =
270
269
  "#wg-panel button.wg-mini-next{background:#2a9d6f;border-color:#22855e;color:#fff;display:none;}" +
271
270
  "#wg-panel button.wg-mini-next:hover{background:#22855e;}" +
272
271
  "#wg-panel button.wg-mini-next:disabled{opacity:.55;cursor:default;}" +
273
- "#wg-panel.wg-collapsed{width:auto;max-width:92vw;padding:8px 12px;max-height:none;overflow:hidden;}" +
272
+ "#wg-panel.wg-collapsed{width:auto;padding:8px 12px;overflow:hidden;}" +
274
273
  "#wg-panel.wg-collapsed .wg-body{display:none;}" +
275
274
  "#wg-panel.wg-collapsed .wg-chrome{margin:0;}" +
276
275
  "#wg-panel.wg-collapsed .wg-chrome-title{font:700 13px/1.25 system-ui,sans-serif;color:#fff;" +
277
276
  "letter-spacing:0;text-transform:none;}" +
278
277
  "#wg-panel.wg-collapsed button.wg-mini-next.wg-mini-next-show{display:inline-block;}" +
279
278
  "@media (max-width:640px){" +
280
- "#wg-panel{left:8px;right:8px;bottom:8px;transform:none;max-width:none;width:auto;" +
281
- "max-height:min(55vh,calc(100vh - 16px));padding:12px 14px;border-radius:12px;}" +
282
- "#wg-panel.wg-collapsed{left:50%;right:auto;transform:translateX(-50%);width:auto;}" +
279
+ "#wg-panel{width:auto;padding:12px 14px;border-radius:12px;}" +
283
280
  "#wg-episodes{overflow-x:auto;-webkit-overflow-scrolling:touch;flex-wrap:nowrap;}" +
284
281
  "#wg-panel .wg-narration{font-size:13px;}" +
285
282
  "#wg-panel button{width:100%;}" +
@@ -49,7 +49,7 @@ export function createOverlayStage(page, { todoDockRef, deviceRef }) {
49
49
  if (!docks.length) {
50
50
  // Empty registry: do NOT clear the DOM. renderBefore/After may have
51
51
  // just painted from lastTodoDock (keep). Clearing here made todos
52
- // vanish until the next ctx.todos() set (PIA #15).
52
+ // vanish until the next ctx.ui.todos() set (PIA #15).
53
53
  return;
54
54
  }
55
55
  await page
@@ -43,17 +43,17 @@ export function phaseToOverlayFixtures(phase, holdMs = 6000) {
43
43
  export function describeOverlayFixtures(tag, f) {
44
44
  const out = [];
45
45
  if (f.title)
46
- out.push(`${tag} ctx.title(${JSON.stringify(f.title)})${f.titlePos ? ` pos=${f.titlePos}` : ""}`);
46
+ out.push(`${tag} ctx.ui.title(${JSON.stringify(f.title)})${f.titlePos ? ` pos=${f.titlePos}` : ""}`);
47
47
  if (f.bannerUi)
48
- out.push(`${tag} ctx.bannerUi(${JSON.stringify(f.bannerUi)})`);
48
+ out.push(`${tag} ctx.ui.bannerUi(${JSON.stringify(f.bannerUi)})`);
49
49
  if (f.todos?.length)
50
- out.push(`${tag} ctx.todos n=${f.todos.length} index=${f.todoIndex ?? 0}${f.todoPos ? ` pos=${f.todoPos}` : ""}`);
50
+ out.push(`${tag} ctx.ui.todos n=${f.todos.length} index=${f.todoIndex ?? 0}${f.todoPos ? ` pos=${f.todoPos}` : ""}`);
51
51
  if (f.todoUi)
52
52
  out.push(`${tag} todoUi ${JSON.stringify(f.todoUi)}`);
53
53
  if (f.device)
54
- out.push(`${tag} ctx.device(${typeof f.device === "string" ? f.device : f.device.preset ?? "custom"})`);
54
+ out.push(`${tag} ctx.ui.device(${typeof f.device === "string" ? f.device : f.device.preset ?? "custom"})`);
55
55
  if (f.zoom)
56
- out.push(`${tag} ctx.zoom(${f.zoom})`);
56
+ out.push(`${tag} ctx.ui.zoom(${f.zoom})`);
57
57
  for (const r of f.rings ?? [])
58
58
  out.push(`${tag} ring ${r.selector} tone=${r.tone ?? "planned"} label=${JSON.stringify(r.label)}`);
59
59
  return out;
@@ -13,8 +13,10 @@ import { ignorePageGone } from "../../../engine/page-gone.js";
13
13
  // re-invented) - genuinely the same visual language, not just the ring CSS
14
14
  // this file already shared via WAYGRAPH_RING_CSS.
15
15
  const OVERLAY_CSS = `
16
+ /* Every element here sits in a slot of the overlay layout (ui/css/layout.css) - bottom-right, bottom-left, top-right -
17
+ or on its stage (the spotlight); none is positioned by hand. */
16
18
  #wg-pilot-overlay {
17
- position: fixed; z-index: 2147483000; bottom: 12px; right: 12px;
19
+ z-index: var(--wg-z-top); display: flex; flex-direction: column; align-items: flex-end;
18
20
  font: 12px/1.4 ui-monospace, "SF Mono", Consolas, monospace;
19
21
  color: #fff; pointer-events: auto;
20
22
  }
@@ -56,7 +58,7 @@ const OVERLAY_CSS = `
56
58
  #wg-pilot-panel-graph .wg-pilot-graph-edge { padding: 2px 0 2px 14px; border-left: 2px solid rgba(255,255,255,.15); font-size: 11px; }
57
59
  #wg-pilot-panel-graph .wg-pilot-graph-edge .wg-pilot-kind { color: #c9a6ff; }
58
60
  #wg-pilot-activity {
59
- position: fixed; z-index: 2147483000; bottom: 12px; left: 12px;
61
+ z-index: var(--wg-z-chrome);
60
62
  font: 12px/1.4 ui-monospace, "SF Mono", Consolas, monospace;
61
63
  background: ${SURFACE}; color: #fff; padding: 6px 10px; border-radius: 8px;
62
64
  box-shadow: 0 2px 8px rgba(0,0,0,0.25); white-space: nowrap; pointer-events: none;
@@ -68,12 +70,12 @@ const OVERLAY_CSS = `
68
70
  #wg-banner all come from the ONE shared bundle ensureShadowRoot installs (ui/css/*.css) - not
69
71
  redefined here. */
70
72
  /* Spotlight (demo focus:true) - dim page, cutout around target. */
71
- #wg-pilot-fx-focus{position:fixed;z-index:2147483644;pointer-events:none;
73
+ #wg-pilot-fx-focus{position:fixed;z-index:var(--wg-z-focus);pointer-events:none;
72
74
  border-radius:12px;box-shadow:0 0 0 9999px rgba(8,4,20,.62);
73
75
  opacity:0;transition:opacity .3s ease,left .3s ease,top .3s ease,width .3s ease,height .3s ease;}
74
76
  #wg-pilot-fx-focus.wg-in{opacity:1;}
75
77
  /* Zoom HUD (demo parity - badge only; no CSS scale of the page). */
76
- #wg-pilot-fx-zoom{position:fixed;z-index:2147483646;bottom:14px;right:14px;
78
+ #wg-pilot-fx-zoom{z-index:var(--wg-z-chrome);
77
79
  display:flex;align-items:center;gap:7px;padding:6px 11px 6px 8px;
78
80
  border-radius:999px;background:${SURFACE};color:#f0e8ff;
79
81
  border:1px solid rgba(250,204,21,.6);box-shadow:0 6px 18px rgba(0,0,0,.4);
@@ -83,7 +85,7 @@ const OVERLAY_CSS = `
83
85
  #wg-pilot-fx-zoom .wg-pilot-fx-zoom-ico svg{width:14px;height:14px;display:block;}
84
86
  #wg-pilot-fx-zoom .wg-pilot-fx-zoom-val{color:#fde68a;font-variant-numeric:tabular-nums;font-weight:800;min-width:3.2em;}
85
87
  #wg-pilot-fx-zoom[data-zoomed="1"]{border-color:#fbbf24;}
86
- #wg-pilot-fx-device{position:fixed;z-index:2147483646;top:14px;right:14px;
88
+ #wg-pilot-fx-device{z-index:var(--wg-z-chrome);
87
89
  padding:8px 12px;border-radius:10px;background:${SURFACE};color:#f0e8ff;
88
90
  border:1px solid rgba(124,58,237,.55);font:700 12px/1.2 system-ui,sans-serif;
89
91
  pointer-events:none;box-shadow:0 6px 18px rgba(0,0,0,.35);}
@@ -11,13 +11,13 @@ function toneRules(sel, decl, only) {
11
11
  /** The highlight ring + its label (`#wg-ring`, `#wg-ring-label`): tone / size / weight variants. */
12
12
  export function ringCss() {
13
13
  const p = TONES.planned;
14
- return ("#wg-ring{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
14
+ return ("#wg-ring{position:fixed;z-index:var(--wg-z-ring,2147483646);pointer-events:none;opacity:0;" +
15
15
  `border:2.5px solid ${p.ring};border-radius:10px;` +
16
16
  // Opacity fade only - NEVER transition border-color/box-shadow: tone swaps must snap.
17
17
  `box-shadow:0 0 0 4px ${p.glow};transition:opacity .3s ease;}` +
18
18
  toneRules("#wg-ring", (t) => `border-color:${t.ring};box-shadow:0 0 0 4px ${t.glow};`) +
19
19
  // A real element (not ::after) so JS can measure it and clamp it back onto the screen.
20
- "#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
20
+ "#wg-ring-label{position:fixed;z-index:var(--wg-z-ring,2147483646);pointer-events:none;opacity:0;" +
21
21
  `max-width:min(360px,70vw);white-space:normal;padding:6px 10px;border-radius:7px;background:${p.labelBg};color:${p.labelFg};` +
22
22
  "font:600 12px/1.35 system-ui,sans-serif;transition:opacity .3s ease;}" +
23
23
  "#wg-ring-label::after{content:attr(data-label);}" +
@@ -31,16 +31,21 @@
31
31
  --wg-font: system-ui, sans-serif;
32
32
  --wg-font-mono: ui-monospace, "SF Mono", Consolas, monospace;
33
33
 
34
- /* Stacking: Open Props' --layer-important is the one legitimate use of a max z-index (a
35
- floating control that must sit above literally anything the host page can throw at it). */
36
- --wg-z-top: var(--layer-important, 2147483647);
37
- --wg-z-ring: 2147483646;
38
- --wg-z-fx: 2147483645;
39
- --wg-z-focus: 2147483644;
40
- --wg-z-dock: 2147483000;
41
- /* Banner sits BELOW rings/labels/todos - a title card must never shadow a highlight. */
42
- --wg-z-banner: 2147483000;
43
-
34
+ /* Stacking. All overlay UI lives in one fixed parent (#wg-ui, layout.css) that sits above the host page with the one
35
+ max z-index; these values only order the layers inside it, lowest first. */
36
+ --wg-z-ui: var(--layer-important, 2147483647);
37
+ --wg-z-focus: 1; /* the spotlight veil; chrome stepping back while a ring is the focus */
38
+ --wg-z-chrome: 2; /* banner, todo docks, caption, chips, network tab */
39
+ --wg-z-dock: 2;
40
+ --wg-z-banner: 2;
41
+ --wg-z-fx: 4; /* rings and their labels: a highlight is never covered by chrome */
42
+ --wg-z-ring: 4;
43
+ --wg-z-top: 6; /* the step panel and other controls a person clicks */
44
+ --wg-z-toast: 7; /* short-lived notices (device change) */
45
+ --wg-z-cursor: 8; /* the cursor, click pulse and swipe trail: the pointer is always on top */
46
+ /* Spacing of the chrome layout: between items in a slot, and from the viewport edge. */
47
+ --wg-gap: 10px;
48
+ --wg-edge: 14px;
44
49
  --wg-ease: var(--ease-3, cubic-bezier(.22,1,.36,1));
45
50
  --wg-shadow-dock: var(--shadow-3, 0 2px 10px rgba(0,0,0,.3));
46
51
  --wg-shadow-banner: var(--shadow-4, 0 8px 20px rgba(0,0,0,.3));
@@ -50,6 +55,74 @@
50
55
  }
51
56
 
52
57
 
58
+ /*
59
+ * The overlay's one layout. Every piece of waygraph UI lives in #wg-ui, a fixed layer over the page, in one of two
60
+ * places:
61
+ *
62
+ * #wg-stage things anchored to the page: ring, label, cursor, click pulse, spotlight veil, swipe trail. They follow
63
+ * a target's coordinates (position: fixed against the viewport).
64
+ * #wg-chrome everything else, placed in a slot. Nothing in it is positioned by hand: a slot is a flex column with
65
+ * a gap, so what shares a place stacks instead of overlapping, and a slot never grows past its row.
66
+ *
67
+ * top-left | top-center | top-right banner (by its side) · device chip / toast
68
+ * left | | right todo docks (by side)
69
+ * bottom-left | bottom-center | bottom-right network tab · typing, caption, step panel · zoom
70
+ *
71
+ * The slot an element goes in is decided once, in ui/shadow.ts (by id and data-pos), and a data-pos change moves it.
72
+ * Each row lays out on its own, so a wide step panel never squeezes the side docks.
73
+ */
74
+ #wg-ui { position: fixed; inset: 0; z-index: var(--wg-z-ui); pointer-events: none; }
75
+ #wg-stage { display: contents; }
76
+ #wg-chrome {
77
+ position: absolute; inset: 0; box-sizing: border-box;
78
+ display: flex; flex-direction: column; gap: var(--wg-gap); padding: var(--wg-edge);
79
+ }
80
+ .wg-row { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); gap: var(--wg-gap); min-height: 0; }
81
+ .wg-row-top { flex: 0 0 auto; align-items: start; }
82
+ .wg-row-middle { flex: 1 1 0; display: flex; justify-content: space-between; align-items: stretch; }
83
+ .wg-row-bottom { flex: 0 1 auto; align-items: end; max-height: 62vh; }
84
+ .wg-slot {
85
+ display: flex; flex-direction: column; gap: var(--wg-gap);
86
+ min-width: 0; min-height: 0; max-height: 100%; max-width: 100%;
87
+ }
88
+ .wg-slot[data-slot$="left"] { align-items: flex-start; }
89
+ .wg-slot[data-slot$="center"] { align-items: center; }
90
+ .wg-slot[data-slot$="right"] { align-items: flex-end; }
91
+ .wg-row-bottom .wg-slot { justify-content: flex-end; }
92
+ /* A slotted element shrinks (and scrolls inside) rather than spilling out of its slot: a min-width of its own (the
93
+ network tab's 260px) must not push it past the slot into its neighbour. */
94
+ .wg-slot > * { position: relative; flex: 0 1 auto; box-sizing: border-box; pointer-events: auto; }
95
+ /* The slot's limits win over an element's own sizing (an #id rule's min-width / max-height), hence the id here. */
96
+ #wg-chrome .wg-slot > * { min-width: 0; min-height: 0; max-width: 100%; max-height: 100%; }
97
+
98
+ /* Order inside a shared slot: what is being typed, what the narration says, then the controls. */
99
+ #wg-typing-badge { order: 1; }
100
+ #wg-caption-bar { order: 2; }
101
+ #wg-panel, #wg-auto-panel { order: 3; }
102
+ #wg-device-toast { order: 1; }
103
+ #wg-device-badge, #wg-pilot-fx-device { order: 2; }
104
+
105
+ /*
106
+ * Chrome that comes and goes takes no space while hidden (an empty gap above the step panel would read as a bug), and
107
+ * still fades: display switches with the transition (allow-discrete), and @starting-style is where the fade starts.
108
+ */
109
+ #wg-caption-bar, #wg-typing-badge, #wg-network-log, #wg-device-badge {
110
+ transition-property: opacity, transform, display; transition-behavior: allow-discrete;
111
+ }
112
+ #wg-caption-bar:not(.wg-in), #wg-typing-badge:not(.wg-in), #wg-network-log:not(.wg-in), #wg-device-badge:not(.wg-in) {
113
+ display: none;
114
+ }
115
+ @starting-style {
116
+ #wg-caption-bar.wg-in, #wg-typing-badge.wg-in, #wg-network-log.wg-in, #wg-device-badge.wg-in { opacity: 0; }
117
+ }
118
+
119
+ /* Narrow viewports: the bottom row's three slots stack (network, then the step panel full width, then zoom). */
120
+ @media (max-width: 640px) {
121
+ .wg-row-bottom { grid-template-columns: minmax(0, 1fr); }
122
+ .wg-slot[data-slot="bottom-center"] { align-items: stretch; }
123
+ }
124
+
125
+
53
126
  /*
54
127
  * ONE ring implementation, shared by every surface (demo/auto's singleton #wg-ring, and Pilot's
55
128
  * multiple simultaneous fixture rings) - `.wg-ring-el`/`.wg-ring-el-label` is the real class every
@@ -110,14 +183,14 @@
110
183
 
111
184
  /* Travelling mouse cursor + click ripple (#wg-cursor, #wg-click-pulse). */
112
185
  #wg-cursor {
113
- position: fixed; z-index: var(--wg-z-top); width: 24px; height: 24px; pointer-events: none;
186
+ position: fixed; z-index: var(--wg-z-cursor); width: 24px; height: 24px; pointer-events: none;
114
187
  left: 0; top: 0; opacity: 0; margin: 0;
115
188
  /* Travel time is JS-driven via --wg-cursor-ms (a hardcoded duration would make speed a no-op). */
116
189
  transition: transform var(--wg-cursor-ms, 600ms) var(--wg-ease), opacity .2s ease;
117
190
  filter: drop-shadow(0 2px 4px rgba(12,12,26,.4));
118
191
  }
119
192
  #wg-click-pulse {
120
- position: fixed; z-index: var(--wg-z-top); width: 14px; height: 14px;
193
+ position: fixed; z-index: var(--wg-z-cursor); width: 14px; height: 14px;
121
194
  margin-left: -7px; margin-top: -7px; border-radius: 50%; pointer-events: none; opacity: 0;
122
195
  border: 2px solid var(--wg-tone-ring, var(--wg-tone-planned-ring)); background: var(--wg-tone-pulse, var(--wg-tone-planned-glow));
123
196
  }
@@ -130,19 +203,16 @@
130
203
  @keyframes wg-pulse { 0% { opacity: .9; transform: scale(.4); } 100% { opacity: 0; transform: scale(2.4); } }
131
204
 
132
205
 
133
- /* Top title banner (#wg-banner). Sits BELOW the ring/label/dock layers (--wg-z-banner) - a title
134
- * card must never shadow a highlight; see tokens.css. Moves out of the way of a ring it overlaps
135
- * (data-collision, driven from inpage/banner.ts's __wgBannerAvoidRing) unless authored off. */
206
+ /* Top title banner (#wg-banner), in the top-left / top-center / top-right slot by its data-pos (layout.css). Sits
207
+ * below rings and labels (--wg-z-banner): a title card never shadows a highlight. Moves to another slot when a ring
208
+ * overlaps it (inpage/banner.ts's __wgBannerAvoidRing) unless authored off. */
136
209
  #wg-banner {
137
- position: fixed; z-index: var(--wg-z-banner); top: 14px; left: 14px;
210
+ z-index: var(--wg-z-banner);
138
211
  background: var(--wg-surface); color: #fff; border-radius: var(--wg-radius-lg); padding: 10px 16px;
139
212
  font: 14px/1.4 var(--wg-font); box-shadow: var(--wg-shadow-banner);
140
- border: 1px solid rgba(124,58,237,.4); cursor: pointer;
141
- transition: left .3s var(--wg-ease), right .3s var(--wg-ease), transform .3s var(--wg-ease);
213
+ border: 1px solid rgba(124,58,237,.4); cursor: pointer; max-width: min(92vw, 520px);
214
+ transition: opacity .3s var(--wg-ease);
142
215
  }
143
- #wg-banner[data-pos=left] { left: 14px; right: auto; transform: none; }
144
- #wg-banner[data-pos=center] { left: 50%; right: auto; transform: translateX(-50%); }
145
- #wg-banner[data-pos=right] { right: 14px; left: auto; transform: none; }
146
216
  /* Same "auto opacity" step-back the todo dock already has (.wg-todo-behind) while a ring caption
147
217
  is the current focus - added via __wgBannerSetBehind, not just the collision-driven move. */
148
218
  #wg-banner.wg-banner-behind { opacity: .42; }
@@ -161,14 +231,11 @@
161
231
  * duplicate of this file's own rules, is consolidated.
162
232
  */
163
233
  #wg-pilot-fx-todos {
164
- position: fixed; z-index: var(--wg-z-fx); top: 104px; max-width: 280px; max-height: calc(100vh - 132px);
165
- overflow: auto; font: 12px/1.4 ui-sans-serif, system-ui, sans-serif; color: #fff;
234
+ z-index: var(--wg-z-dock); width: min(280px, 42vw); overflow: auto; font: 12px/1.4 ui-sans-serif, system-ui, sans-serif; color: #fff;
166
235
  background: var(--wg-surface); border-radius: var(--wg-radius-md); padding: 10px 12px;
167
236
  box-shadow: var(--wg-shadow-dock); pointer-events: auto; transition: opacity .25s ease;
168
237
  }
169
238
  #wg-pilot-fx-todos.wg-todo-behind { z-index: var(--wg-z-focus); opacity: .42; pointer-events: none; }
170
- #wg-pilot-fx-todos[data-pos=left] { left: 14px; right: auto; }
171
- #wg-pilot-fx-todos[data-pos=right] { right: 14px; left: auto; }
172
239
  #wg-pilot-fx-todos .wg-pilot-fx-todo-title {
173
240
  font: 700 11px/1.2 system-ui, sans-serif; color: var(--wg-accent-soft);
174
241
  letter-spacing: .04em; text-transform: uppercase; margin: 0 0 8px;
@@ -199,29 +266,25 @@
199
266
  * chrome, this narrates the PAGE itself so it reads as a caption card, not more chrome).
200
267
  */
201
268
  #wg-caption-bar {
202
- position: fixed; z-index: var(--wg-z-focus); left: 0; right: 0; bottom: 0;
203
- /* Bottom padding clears the collapsed step-pill (#wg-panel, bottom:12px, ~40px tall,
204
- z-index:2147483647 - always on top) - without it the pill sat right over the caption card,
205
- both centered at the bottom of the viewport (see #wg-typing-badge's own bottom:88px, same
206
- reason). */
207
- padding: 0 6% 64px; display: flex; justify-content: center;
269
+ /* In the bottom-center slot, above the step panel (layout.css): the slot's gap keeps them apart. */
270
+ z-index: var(--wg-z-chrome); display: flex; justify-content: center; max-width: min(820px, 92vw);
208
271
  pointer-events: none; opacity: 0; transform: translateY(6px);
209
- transition: opacity .3s ease, transform .3s ease;
272
+ transition-duration: .3s; transition-timing-function: ease;
210
273
  }
211
274
  #wg-caption-bar.wg-in { opacity: 1; transform: translateY(0); }
212
275
  #wg-caption-bar .wg-caption-text {
213
276
  font: 500 15px/1.4 var(--wg-font); color: #1c1917; text-align: center;
214
- max-width: 82%; text-wrap: balance;
277
+ text-wrap: balance;
215
278
  background: #fff; border-radius: var(--wg-radius-lg);
216
279
  padding: 10px 18px; box-shadow: var(--wg-shadow-banner);
217
280
  }
218
281
 
219
282
 
220
283
  /* The network tab: real page traffic (network-capture.ts) and defineApiBlock's own calls, bottom-left (the one corner
221
- * free of the zoom HUD and the typing chip). Hidden until ctx.showNetworkTab(); stays up once shown. In the shared
284
+ * free of the zoom HUD and the typing chip). Hidden until ctx.ui.showNetworkTab(); stays up once shown. In the shared
222
285
  * bundle, not the runner CSS: a navigation rebuilds the panel the moment the shadow root is back, before any later
223
286
  * stylesheet, and without its CSS it painted unstyled and visible, then faded out. */
224
- #wg-network-log{position:fixed;z-index:2147483646;bottom:14px;left:14px;min-width:260px;max-width:min(92vw,420px);max-height:min(60vh,420px);display:flex;flex-direction:column;border-radius:10px;background:var(--wg-surface);color:#f0e8ff;border:1px solid rgba(124,58,237,.45);box-shadow:0 8px 22px rgba(0,0,0,.4);font:12px/1.4 ui-monospace,"SF Mono",Consolas,monospace;opacity:0;transform:translateY(8px);transition:opacity .25s ease,transform .25s ease;overflow:hidden;}
287
+ #wg-network-log{z-index:var(--wg-z-chrome);min-width:260px;max-width:min(92vw,420px);max-height:min(60vh,420px);display:flex;flex-direction:column;border-radius:10px;background:var(--wg-surface);color:#f0e8ff;border:1px solid rgba(124,58,237,.45);box-shadow:0 8px 22px rgba(0,0,0,.4);font:12px/1.4 ui-monospace,"SF Mono",Consolas,monospace;opacity:0;transform:translateY(8px);transition-duration:.25s;transition-timing-function:ease;overflow:hidden;}
225
288
  #wg-network-log.wg-in{opacity:1;transform:translateY(0);}
226
289
  #wg-network-log.wg-net-collapsed .wg-net-list{display:none;}
227
290
  #wg-network-log .wg-net-header{display:flex;align-items:center;gap:6px;padding:8px 10px;border-bottom:1px solid rgba(124,58,237,.25);cursor:default;flex:0 0 auto;}
@@ -15,18 +15,85 @@ export function installShadowRoot(overlaySheet) {
15
15
  host?.remove();
16
16
  host = document.createElement("div");
17
17
  host.id = "wg-root";
18
- // No box of its own: children are position:fixed, so the host must never affect page layout.
18
+ // No box of its own: everything inside is in the fixed #wg-ui layer, so the host never affects page layout.
19
19
  host.style.cssText = "display:contents";
20
20
  host.attachShadow({ mode: "open" });
21
21
  document.documentElement.appendChild(host);
22
22
  }
23
23
  return host.shadowRoot;
24
24
  };
25
+ // The one layout (ui/css/layout.css): #wg-ui holds the stage (anchored things) and the chrome's slots. Where a chrome
26
+ // element goes is decided here, once, by its id and data-pos; anything not in the table is anchored (the stage).
27
+ const SLOTS = ["top-left", "top-center", "top-right", "left", "right", "bottom-left", "bottom-center", "bottom-right"];
28
+ const SLOT_OF = {
29
+ "wg-typing-badge": "bottom-center",
30
+ "wg-caption-bar": "bottom-center",
31
+ "wg-panel": "bottom-center",
32
+ "wg-auto-panel": "bottom-center",
33
+ "wg-network-log": "bottom-left",
34
+ "wg-pilot-activity": "bottom-left",
35
+ "wg-zoom-badge": "bottom-right",
36
+ "wg-pilot-fx-zoom": "bottom-right",
37
+ "wg-pilot-overlay": "bottom-right",
38
+ "wg-device-toast": "top-right",
39
+ "wg-device-badge": "top-right",
40
+ "wg-pilot-fx-device": "top-right",
41
+ };
42
+ const slotOf = (el) => {
43
+ if (!(el instanceof HTMLElement))
44
+ return undefined;
45
+ const pos = el.dataset.pos;
46
+ if (el.id === "wg-banner")
47
+ return pos === "center" || pos === "right" ? `top-${pos}` : "top-left";
48
+ if (el.classList.contains("wg-todo-dock") || el.id === "wg-todo-dock" || el.id === "wg-pilot-fx-todos")
49
+ return pos === "right" ? "right" : "left";
50
+ return SLOT_OF[el.id];
51
+ };
52
+ const ui = () => {
53
+ const r = root();
54
+ let layer = r.getElementById("wg-ui");
55
+ if (layer)
56
+ return layer;
57
+ layer = document.createElement("div");
58
+ layer.id = "wg-ui";
59
+ const stage = document.createElement("div");
60
+ stage.id = "wg-stage";
61
+ const chrome = document.createElement("div");
62
+ chrome.id = "wg-chrome";
63
+ for (const [row, slots] of [["top", SLOTS.slice(0, 3)], ["middle", SLOTS.slice(3, 5)], ["bottom", SLOTS.slice(5)]]) {
64
+ const rowEl = document.createElement("div");
65
+ rowEl.className = `wg-row wg-row-${row}`;
66
+ for (const name of slots) {
67
+ const slot = document.createElement("div");
68
+ slot.className = "wg-slot";
69
+ slot.dataset.slot = name;
70
+ rowEl.appendChild(slot);
71
+ }
72
+ chrome.appendChild(rowEl);
73
+ }
74
+ layer.append(stage, chrome);
75
+ r.appendChild(layer);
76
+ // A data-pos change (banner click, a dock flipping away from a ring) moves the element to its new slot.
77
+ new MutationObserver((records) => {
78
+ for (const rec of records)
79
+ if (rec.target instanceof Element && rec.target.parentElement?.classList.contains("wg-slot"))
80
+ place(rec.target);
81
+ }).observe(chrome, { subtree: true, attributes: true, attributeFilter: ["data-pos"] });
82
+ return layer;
83
+ };
84
+ const place = (node) => {
85
+ const layer = ui();
86
+ const slot = node instanceof Element ? slotOf(node) : undefined;
87
+ const parent = slot ? layer.querySelector(`[data-slot="${slot}"]`) : layer.querySelector("#wg-stage");
88
+ if (node.parentNode !== parent)
89
+ parent.appendChild(node);
90
+ return node;
91
+ };
25
92
  w.__wgRoot = root;
26
93
  w.__wgById = (id) => root().getElementById(id) || document.getElementById(id);
27
94
  w.__wgQ = (sel) => root().querySelector(sel) || document.querySelector(sel);
28
95
  w.__wgQA = (sel) => Array.from(root().querySelectorAll(sel)).concat(Array.from(document.querySelectorAll(sel)));
29
- w.__wgAdd = (node) => root().appendChild(node);
96
+ w.__wgAdd = place;
30
97
  w.__wgCss = (css, key) => {
31
98
  const r = root();
32
99
  let style = Array.from(r.children).find((c) => c.tagName === "STYLE" && c.getAttribute("data-key") === key);
@@ -177,13 +177,13 @@ export function logStubPhaseFixtures(phaseName, phase) {
177
177
  return;
178
178
  const tag = "stub." + phaseName;
179
179
  if (phase.title)
180
- demoFixture(tag, "ctx.title(...)", JSON.stringify(phase.title));
180
+ demoFixture(tag, "ctx.ui.title(...)", JSON.stringify(phase.title));
181
181
  if (phase.titlePos)
182
- demoFixture(tag, "ctx.titlePos(...)", JSON.stringify(phase.titlePos));
182
+ demoFixture(tag, "ctx.ui.titlePos(...)", JSON.stringify(phase.titlePos));
183
183
  if (phase.bannerUi)
184
- demoFixture(tag, "ctx.bannerUi(...)", JSON.stringify(phase.bannerUi));
184
+ demoFixture(tag, "ctx.ui.bannerUi(...)", JSON.stringify(phase.bannerUi));
185
185
  if (phase.deviceSync && phase.deviceSync !== "keep") {
186
- demoFixture(tag, phase.deviceSync === "clear" ? "ctx.hideDevice()" : "ctx.device(...)", phase.device
186
+ demoFixture(tag, phase.deviceSync === "clear" ? "ctx.ui.hideDevice()" : "ctx.ui.device(...)", phase.device
187
187
  ? {
188
188
  preset: phase.device.preset,
189
189
  w: phase.device.viewport && phase.device.viewport.width,
@@ -194,11 +194,11 @@ export function logStubPhaseFixtures(phaseName, phase) {
194
194
  : phase.deviceSync);
195
195
  }
196
196
  if (phase.zoom != null)
197
- demoFixture(tag, "ctx.zoom(" + phase.zoom + ")", phase.zoom + "x");
197
+ demoFixture(tag, "ctx.ui.zoom(" + phase.zoom + ")", phase.zoom + "x");
198
198
  if (phase.zoomOut !== undefined)
199
- demoFixture(tag, "ctx.zoomOut(" + !!phase.zoomOut + ")", !!phase.zoomOut);
199
+ demoFixture(tag, "ctx.ui.zoomOut(" + !!phase.zoomOut + ")", !!phase.zoomOut);
200
200
  if (phase.todoSync && phase.todoSync !== "keep") {
201
- demoFixture(tag, phase.todoSync === "clear" ? "ctx.hideTodos() / clear" : "ctx.todos(...)", phase.todoDock
201
+ demoFixture(tag, phase.todoSync === "clear" ? "ctx.ui.hideTodos() / clear" : "ctx.ui.todos(...)", phase.todoDock
202
202
  ? {
203
203
  id: phase.todoDock.id,
204
204
  pos: phase.todoDock.pos,
@@ -208,7 +208,7 @@ export function logStubPhaseFixtures(phaseName, phase) {
208
208
  : phase.todoSync);
209
209
  }
210
210
  phase.highlights.forEach((h, i) => {
211
- demoFixture(tag, "ctx.ring[" + i + "]", {
211
+ demoFixture(tag, "ctx.ui.ring[" + i + "]", {
212
212
  selector: h.selector,
213
213
  label: h.label,
214
214
  detail: h.detail,
@@ -22,5 +22,5 @@ export interface SlidesOptions {
22
22
  * doing nothing. In-page data-wg-acked + autoplay poll is the source of truth.
23
23
  */
24
24
  export declare function presentSlides(page: WgPage, slides: readonly WaygraphSlide[], opts?: SlidesOptions): Promise<void>;
25
- /** Plays a stub phase's `ctx.slides(...)` for the step on screen, in the flow's highlight style. */
25
+ /** Plays a stub phase's `ctx.ui.slides(...)` for the step on screen, in the flow's highlight style. */
26
26
  export declare function showSlides(ctx: StepperContext, view: StepView, slides: readonly WaygraphSlide[]): Promise<void>;