waygraph 0.12.14 → 0.12.16

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
@@ -233,6 +233,12 @@ Sign In narration.
233
233
  snapshot, partitions edges by hash, and claims via in-process edge leases (audit under
234
234
  `.waygraph-traverse/`). `--session inherit` is refused when `parallel > 1`. See
235
235
  `docs/proposals/traverse-ffcompose-rfc.md`.
236
+
237
+ **Richer demo narration:** automation rings are **gray**; authored stubs/slides can use
238
+ `tone: "info" | "warning" | "danger" | "success"` (iconified captions), plus
239
+ `size: "sm" | "md" | "lg"` and `weight: "normal" | "bold"`. Episode pacing:
240
+ `withDemoPace(flow, "fast" | "slow" | "normal" | "blitz")` or `withBlockPace(block, ...)`
241
+ (FFCompose stays blitz). Living objectives: `docs/proposals/OBJECTIVES.md`.
236
242
  `spawnTab(entry, page, mem)` drives a genuinely separate second tab through its own
237
243
  Block/Flow, in the same browser context an existing `page` already belongs to - the
238
244
  pattern `observe()` could already reach for (it's the only phase allowed to touch
package/dist/cli.js CHANGED
@@ -211,6 +211,11 @@ import {
211
211
  formatHighlightCaption,
212
212
  hasAuthoredStubAfter,
213
213
  hasAuthoredStubOnError,
214
+ normalizeHighlightTone,
215
+ normalizeDemoPace,
216
+ normalizeHighlightSize,
217
+ normalizeHighlightWeight,
218
+ resolveStepDemoPace,
214
219
  } from "waygraph";
215
220
 
216
221
  function walkDir(dir, pattern) {
@@ -399,9 +404,13 @@ const RING_CSS =
399
404
  "border:2.5px solid #7C3AED;border-radius:10px;" +
400
405
  "box-shadow:0 0 0 4px rgba(124,58,237,.16);transition:opacity .3s ease,border-color .15s,box-shadow .15s;}" +
401
406
  // Planned (stubs / fixtures / YAP / instruction.highlights) = purple.
402
- // Automation (verify fallback, unmatched fill/click) = yellow - so operators
403
- // can ignore engine checks and only watch purple narration.
404
- "#wg-ring[data-tone=auto]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
407
+ // Automation (verify fallback, unmatched fill/click) = gray - operators
408
+ // can ignore engine checks and watch purple + semantic tones.
409
+ "#wg-ring[data-tone=auto]{border-color:#9CA3AF;box-shadow:0 0 0 4px rgba(156,163,175,.28);}" +
410
+ "#wg-ring[data-tone=info]{border-color:#3B82F6;box-shadow:0 0 0 4px rgba(59,130,246,.22);}" +
411
+ "#wg-ring[data-tone=warning]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
412
+ "#wg-ring[data-tone=danger]{border-color:#EF4444;box-shadow:0 0 0 4px rgba(239,68,68,.22);}" +
413
+ "#wg-ring[data-tone=success]{border-color:#22C55E;box-shadow:0 0 0 4px rgba(34,197,94,.22);}" +
405
414
  // A real element, not a ::after pseudo-element - a pseudo-element's
406
415
  // position is CSS-relative to the ring's own box (left:0 always meant
407
416
  // "the ring's own left edge"), so it had no way to be clamped back onto
@@ -413,7 +422,18 @@ const RING_CSS =
413
422
  "#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
414
423
  "max-width:min(360px,70vw);white-space:normal;padding:6px 10px;border-radius:7px;background:#7C3AED;color:#fff;" +
415
424
  "font:600 12px/1.35 system-ui,sans-serif;transition:opacity .3s ease,background .15s,color .15s;}" +
416
- "#wg-ring-label[data-tone=auto]{background:#EAB308;color:#1c1917;}" +
425
+ "#wg-ring-label[data-tone=auto]{background:#6B7280;color:#fff;}" +
426
+ "#wg-ring-label[data-tone=info]{background:#2563EB;color:#fff;}" +
427
+ "#wg-ring-label[data-tone=warning]{background:#EAB308;color:#1c1917;}" +
428
+ "#wg-ring-label[data-tone=danger]{background:#DC2626;color:#fff;}" +
429
+ "#wg-ring-label[data-tone=success]{background:#16A34A;color:#fff;}" +
430
+ // size: ring pad + label font; weight: label boldness
431
+ "#wg-ring[data-size=sm]{border-width:1.5px;border-radius:8px;}" +
432
+ "#wg-ring[data-size=lg]{border-width:4px;border-radius:12px;}" +
433
+ "#wg-ring-label[data-size=sm]{font-size:10px;line-height:1.25;padding:4px 7px;border-radius:5px;}" +
434
+ "#wg-ring-label[data-size=lg]{font-size:16px;line-height:1.35;padding:8px 14px;border-radius:9px;max-width:min(480px,85vw);}" +
435
+ "#wg-ring-label[data-weight=bold]{font-weight:800;}" +
436
+ "#wg-ring-label[data-weight=normal]{font-weight:600;}" +
417
437
  // Mouse cursor icon that travels to a target before it's acted on, plus a
418
438
  // quick expanding ripple at the moment of a click - same idea as
419
439
  // help-center-clip-engine's #clip-cursor/#clip-ring (video-pipeline). The
@@ -431,7 +451,11 @@ const RING_CSS =
431
451
  "#wg-click-pulse{position:fixed;z-index:2147483647;width:14px;height:14px;" +
432
452
  "margin-left:-7px;margin-top:-7px;border-radius:50%;pointer-events:none;opacity:0;" +
433
453
  "border:2px solid #7C3AED;background:rgba(124,58,237,.25);}" +
434
- "#wg-click-pulse[data-tone=auto]{border-color:#EAB308;background:rgba(234,179,8,.28);}" +
454
+ "#wg-click-pulse[data-tone=auto]{border-color:#9CA3AF;background:rgba(156,163,175,.28);}" +
455
+ "#wg-click-pulse[data-tone=info]{border-color:#3B82F6;background:rgba(59,130,246,.28);}" +
456
+ "#wg-click-pulse[data-tone=warning]{border-color:#EAB308;background:rgba(234,179,8,.28);}" +
457
+ "#wg-click-pulse[data-tone=danger]{border-color:#EF4444;background:rgba(239,68,68,.28);}" +
458
+ "#wg-click-pulse[data-tone=success]{border-color:#22C55E;background:rgba(34,197,94,.28);}" +
435
459
  "#wg-click-pulse.wg-pulse{animation:wg-pulse .5s ease-out;}" +
436
460
  "@keyframes wg-pulse{0%{opacity:.9;transform:scale(.4);}100%{opacity:0;transform:scale(2.4);}}" +
437
461
  "#wg-panel{position:fixed;z-index:2147483647;left:50%;bottom:12px;transform:translateX(-50%);" +
@@ -647,20 +671,34 @@ async function installOverlay(page, title) {
647
671
  const cursor = document.getElementById("wg-cursor");
648
672
  if (cursor) cursor.style.opacity = "0";
649
673
  };
650
- window.__wgPositionRing = (box, label, tone) => {
674
+ window.__wgPositionRing = (box, label, tone, style) => {
651
675
  const ring = document.getElementById("wg-ring");
652
676
  const ringLabel = document.getElementById("wg-ring-label");
653
677
  if (!ring || !ringLabel) return;
654
- const t = tone === "auto" ? "auto" : "planned";
678
+ const raw = (tone || "planned") + "";
679
+ const t =
680
+ raw === "auto" || raw === "info" || raw === "warning" || raw === "danger" || raw === "success"
681
+ ? raw
682
+ : "planned";
683
+ const st = style && typeof style === "object" ? style : {};
684
+ const sizeRaw = (st.size || "md") + "";
685
+ const size =
686
+ sizeRaw === "sm" || sizeRaw === "lg" ? sizeRaw : "md";
687
+ const weightRaw = (st.weight || "normal") + "";
688
+ const weight = weightRaw === "bold" ? "bold" : "normal";
655
689
  ring.dataset.tone = t;
656
690
  ringLabel.dataset.tone = t;
691
+ ring.dataset.size = size;
692
+ ringLabel.dataset.size = size;
693
+ ringLabel.dataset.weight = weight;
694
+ const pad = size === "sm" ? 3 : size === "lg" ? 10 : 6;
657
695
  const margin = 6;
658
696
  const vw = window.innerWidth;
659
697
  const vh = window.innerHeight;
660
- let left = box.x - 6;
661
- let top = box.y - 6;
662
- const width = box.width + 12;
663
- const height = box.height + 12;
698
+ let left = box.x - pad;
699
+ let top = box.y - pad;
700
+ const width = box.width + pad * 2;
701
+ const height = box.height + pad * 2;
664
702
  if (left < margin) left = margin;
665
703
  if (top < margin) top = margin;
666
704
  if (left + width > vw - margin) left = Math.max(margin, vw - margin - width);
@@ -688,7 +726,11 @@ async function installOverlay(page, title) {
688
726
  window.__wgClickPulse = (x, y, tone) => {
689
727
  const pulse = document.getElementById("wg-click-pulse");
690
728
  if (!pulse) return;
691
- pulse.dataset.tone = tone === "auto" ? "auto" : "planned";
729
+ const raw = (tone || "planned") + "";
730
+ pulse.dataset.tone =
731
+ raw === "auto" || raw === "info" || raw === "warning" || raw === "danger" || raw === "success"
732
+ ? raw
733
+ : "planned";
692
734
  pulse.style.left = x + "px";
693
735
  pulse.style.top = y + "px";
694
736
  pulse.classList.remove("wg-pulse");
@@ -1035,7 +1077,10 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1035
1077
  try {
1036
1078
  const box = await page.locator(h.selector).first().boundingBox();
1037
1079
  if (box) {
1038
- await showRing(page, box, h.label, h.tone || "planned");
1080
+ await showRing(page, box, h.label, h.tone || "planned", {
1081
+ size: h.size,
1082
+ weight: h.weight,
1083
+ });
1039
1084
  const authored = resolveFixtureDwellMs(h, { gatesFast });
1040
1085
  const legacyMs = i === list.length - 1 ? 200 : 900;
1041
1086
  const holdMs =
@@ -1057,7 +1102,10 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1057
1102
  if (window.__wgHideRing) window.__wgHideRing();
1058
1103
  return;
1059
1104
  }
1060
- window.__wgPositionRing(el.getBoundingClientRect(), h.label, h.tone || "planned");
1105
+ window.__wgPositionRing(el.getBoundingClientRect(), h.label, h.tone || "planned", {
1106
+ size: h.size || "md",
1107
+ weight: h.weight || "normal",
1108
+ });
1061
1109
  };
1062
1110
  reposition();
1063
1111
  window.__wgRingTrack = reposition;
@@ -1303,7 +1351,7 @@ function resolveDeclaredHighlights(block, resultTag) {
1303
1351
  if (!Array.isArray(highlights)) return [];
1304
1352
  return highlights.filter((h) => h && typeof h.selector === "string" && typeof h.label === "string").map((h) => ({
1305
1353
  ...h,
1306
- tone: h.tone === "auto" ? "auto" : "planned",
1354
+ tone: normalizeHighlightTone(h.tone),
1307
1355
  }));
1308
1356
  }
1309
1357
 
@@ -1402,13 +1450,17 @@ async function markStepRunning(page, _opts) {
1402
1450
  .catch(() => {});
1403
1451
  }
1404
1452
 
1405
- async function showRing(page, box, label, tone) {
1453
+ async function showRing(page, box, label, tone, style) {
1454
+ const size = normalizeHighlightSize(style && style.size);
1455
+ const weight = normalizeHighlightWeight(style && style.weight);
1406
1456
  await page
1407
1457
  .evaluate(
1408
- ({ box, label, tone }) => {
1409
- if (window.__wgPositionRing) window.__wgPositionRing(box, label, tone || "planned");
1458
+ ({ box, label, tone, size, weight }) => {
1459
+ if (window.__wgPositionRing) {
1460
+ window.__wgPositionRing(box, label, tone || "planned", { size, weight });
1461
+ }
1410
1462
  },
1411
- { box, label, tone: tone || "planned" },
1463
+ { box, label, tone: tone || "planned", size, weight },
1412
1464
  )
1413
1465
  .catch(() => {});
1414
1466
  }
@@ -1438,14 +1490,24 @@ async function matchStubForLocator(page, locator, stubs) {
1438
1490
 
1439
1491
  async function captionForLocator(page, locator, stubs, fallback) {
1440
1492
  const matched = await matchStubForLocator(page, locator, stubs);
1441
- if (matched) return { label: formatHighlightCaption(matched), tone: "planned" };
1442
- return { label: fallback, tone: "auto" };
1493
+ if (matched) {
1494
+ return {
1495
+ label: formatHighlightCaption(matched),
1496
+ tone: normalizeHighlightTone(matched.tone),
1497
+ size: normalizeHighlightSize(matched.size),
1498
+ weight: normalizeHighlightWeight(matched.weight),
1499
+ };
1500
+ }
1501
+ return { label: fallback, tone: "auto", size: "md", weight: "normal" };
1443
1502
  }
1444
1503
 
1445
1504
  async function dwellMatchedStub(page, locator, stubs, pacing) {
1446
1505
  const matched = await matchStubForLocator(page, locator, stubs);
1447
1506
  if (!matched) return;
1448
- const ms = resolveFixtureDwellMs(matched, { gatesFast: !!(pacing && pacing.gatesFast) });
1507
+ const ms = resolveFixtureDwellMs(matched, {
1508
+ gatesFast: !!(pacing && pacing.gatesFast),
1509
+ pace: pacing && pacing.demoPace,
1510
+ });
1449
1511
  if (ms != null && ms > 0) {
1450
1512
  await new Promise((res) => setTimeout(res, ms));
1451
1513
  }
@@ -1466,22 +1528,30 @@ async function presentSlides(page, slides, _gate, opts) {
1466
1528
  const episodeNumber = opts && opts.episodeNumber;
1467
1529
  const episodeTitle = (opts && opts.episodeTitle) || "";
1468
1530
  const gatesFast = !!(opts && opts.fast);
1531
+ const demoPace = normalizeDemoPace((opts && opts.pace) || (gatesFast ? "fast" : "normal"));
1469
1532
  const autoplayMs =
1470
1533
  opts && opts.autoplayMs
1471
1534
  ? Number(opts.autoplayMs)
1472
1535
  : process.env.WAYGRAPH_AUTOPLAY_MS
1473
1536
  ? Number(process.env.WAYGRAPH_AUTOPLAY_MS)
1474
- : 1800;
1537
+ : demoPace === "slow"
1538
+ ? 3500
1539
+ : 1800;
1475
1540
  for (let i = 0; i < slides.length; i++) {
1476
1541
  const s = slides[i];
1477
1542
  const caption = formatHighlightCaption(s);
1543
+ const slideTone = normalizeHighlightTone(s.tone);
1544
+ const slideStyle = {
1545
+ size: normalizeHighlightSize(s.size),
1546
+ weight: normalizeHighlightWeight(s.weight),
1547
+ };
1478
1548
  const isLast = i === slides.length - 1;
1479
- const dwellMs = resolveFixtureDwellMs(s, { gatesFast });
1549
+ const dwellMs = resolveFixtureDwellMs(s, { gatesFast, pace: demoPace });
1480
1550
  await installOverlay(page, title);
1481
1551
  if (s.selector) {
1482
1552
  try {
1483
1553
  const box = await page.locator(s.selector).first().boundingBox();
1484
- if (box) await showRing(page, box, caption, "planned");
1554
+ if (box) await showRing(page, box, caption, slideTone, slideStyle);
1485
1555
  } catch {
1486
1556
  await hideRing(page);
1487
1557
  }
@@ -1793,7 +1863,10 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
1793
1863
  const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
1794
1864
  const cap = await captionForLocator(page, this, stubs, fallback);
1795
1865
  await moveCursorTo(page, box, cursorMs(500));
1796
- await showRing(page, box, cap.label, cap.tone);
1866
+ await showRing(page, box, cap.label, cap.tone, {
1867
+ size: cap.size,
1868
+ weight: cap.weight,
1869
+ });
1797
1870
  await dwellMatchedStub(page, this, stubs, pacing);
1798
1871
  await new Promise((res) => setTimeout(res, skipTheater() ? 0 : 200));
1799
1872
  } else if (box) {
@@ -1865,7 +1938,10 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
1865
1938
  const cap = await captionForLocator(page, this, stubs, fallback);
1866
1939
  clickTone = cap.tone;
1867
1940
  clickPoint = await moveCursorTo(page, box, cursorMs(600));
1868
- await showRing(page, box, cap.label, cap.tone);
1941
+ await showRing(page, box, cap.label, cap.tone, {
1942
+ size: cap.size,
1943
+ weight: cap.weight,
1944
+ });
1869
1945
  await dwellMatchedStub(page, this, stubs, pacing);
1870
1946
  // "pop for a few seconds" - Dan's own phrase, matching the
1871
1947
  // zsign demo-engine's ring-before-click pattern in
@@ -1996,7 +2072,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
1996
2072
  // inside the FF (that was the "30s login / cursor gone but still slow" bug).
1997
2073
  const demoFast = process.env.WAYGRAPH_DEMO_FAST === "1";
1998
2074
  const stepperMode = process.env.WAYGRAPH_STEPPER === "full" ? "full" : "carousel";
1999
- const pacing = { gatesFast: demoFast, skipTheater: false };
2075
+ const pacing = { gatesFast: demoFast, gatesSlow: false, skipTheater: false, demoPace: "normal" };
2000
2076
  const stubBeforeRef = { current: [] };
2001
2077
  instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef);
2002
2078
  // Force the panel checkbox from this process's flags/env at run start.
@@ -2087,7 +2163,9 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2087
2163
  ? Math.min(80, autoplayMs)
2088
2164
  : pacing.gatesFast
2089
2165
  ? Math.min(400, autoplayMs)
2090
- : autoplayMs;
2166
+ : pacing.gatesSlow
2167
+ ? Math.max(autoplayMs * 2, 3500)
2168
+ : autoplayMs;
2091
2169
  let elapsed = 0;
2092
2170
  for (;;) {
2093
2171
  // Re-read the checkbox EVERY loop tick, not once up front - a human
@@ -2171,10 +2249,33 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2171
2249
  await resetPageState(context, page, baseURL);
2172
2250
  }
2173
2251
  // gatesFast / skipTheater: opaque FF, former-FF inners (--ff-disabled/--ff-expand),
2174
- // or WAYGRAPH_FAST_BLOCKS. --fast alone = shorter gates only (keeps theater).
2175
- pacing.gatesFast = demoFast || !!r.block.fastForward || !!r.wasFastForward;
2252
+ // WAYGRAPH_FAST_BLOCKS, or withDemoPace/withBlockPace. --fast alone = shorter
2253
+ // gates only (keeps theater). Episode pace: block > flow > CLI --fast.
2254
+ const stepPace = resolveStepDemoPace({
2255
+ blockPace: r.block.demoPace,
2256
+ flowPace: r.demoPace,
2257
+ fastForward: !!r.block.fastForward,
2258
+ wasFastForward: !!r.wasFastForward,
2259
+ });
2260
+ const cliFast = demoFast || fastBlockNames.has(r.block.name);
2261
+ pacing.demoPace =
2262
+ stepPace === "normal" && cliFast
2263
+ ? fastBlockNames.has(r.block.name)
2264
+ ? "blitz"
2265
+ : "fast"
2266
+ : stepPace;
2267
+ pacing.gatesFast =
2268
+ pacing.demoPace === "blitz" ||
2269
+ pacing.demoPace === "fast" ||
2270
+ !!r.block.fastForward ||
2271
+ !!r.wasFastForward ||
2272
+ demoFast;
2273
+ pacing.gatesSlow = pacing.demoPace === "slow";
2176
2274
  pacing.skipTheater =
2177
- fastBlockNames.has(r.block.name) || !!r.block.fastForward || !!r.wasFastForward;
2275
+ pacing.demoPace === "blitz" ||
2276
+ fastBlockNames.has(r.block.name) ||
2277
+ !!r.block.fastForward ||
2278
+ !!r.wasFastForward;
2178
2279
  const fixtures = r.highlightFixtures;
2179
2280
  stubBeforeRef.current = resolveHighlightSlots(r.block, "stubBefore", { fixtures });
2180
2281
  const isNavBlock = r.block.__waygraphKind === "nav";
@@ -2273,7 +2374,9 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2273
2374
  label: formatHighlightCaption(h),
2274
2375
  duration: h.duration,
2275
2376
  fastMode: h.fastMode,
2276
- tone: "planned",
2377
+ tone: normalizeHighlightTone(h.tone),
2378
+ size: normalizeHighlightSize(h.size),
2379
+ weight: normalizeHighlightWeight(h.weight),
2277
2380
  }));
2278
2381
  await cycleHighlightRings(page, errHighlights, !!pacing.gatesFast, {
2279
2382
  defaultHoldMs: 2000,
@@ -2333,6 +2436,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2333
2436
  title,
2334
2437
  blockName: r.block.name,
2335
2438
  fast: pacing.gatesFast,
2439
+ pace: pacing.demoPace,
2336
2440
  episodeNumber: r.episodeNumber,
2337
2441
  episodeTitle: r.episodeTitle,
2338
2442
  autoplayMs,
@@ -2346,7 +2450,9 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2346
2450
  label: formatHighlightCaption(h),
2347
2451
  duration: h.duration,
2348
2452
  fastMode: h.fastMode,
2349
- tone: "planned",
2453
+ tone: normalizeHighlightTone(h.tone),
2454
+ size: normalizeHighlightSize(h.size),
2455
+ weight: normalizeHighlightWeight(h.weight),
2350
2456
  }),
2351
2457
  );
2352
2458
  } else {
@@ -2531,6 +2637,7 @@ async function main() {
2531
2637
  exportName: bi.name,
2532
2638
  seedMem: idx === 0 ? seedMem : undefined,
2533
2639
  highlightFixtures: flow.highlightFixtures,
2640
+ demoPace: flow.demoPace,
2534
2641
  expectedFailureReason:
2535
2642
  flow.expectedFailureReason && idx === blockInfos.length - 1
2536
2643
  ? flow.expectedFailureReason
@@ -2599,6 +2706,7 @@ async function main() {
2599
2706
  episodeTitle: flow.title || seg.ref,
2600
2707
  expectedFailureReason: flow.expectedFailureReason,
2601
2708
  highlightFixtures: flow.highlightFixtures,
2709
+ demoPace: flow.demoPace,
2602
2710
  seedMem: () => seedMemForFlow(mem, flow.blocks(), seg.json, seg.ref),
2603
2711
  });
2604
2712
  } else {
@@ -2608,6 +2716,7 @@ async function main() {
2608
2716
  episodeNumber: undefined,
2609
2717
  episodeTitle: undefined,
2610
2718
  highlightFixtures: undefined,
2719
+ demoPace: undefined,
2611
2720
  seedMem: () => seedMemForBlock(mem, r, seg.json),
2612
2721
  });
2613
2722
  }
@@ -2632,6 +2741,7 @@ async function main() {
2632
2741
  episodeNumber: meta.episodeNumber,
2633
2742
  episodeTitle: meta.episodeTitle,
2634
2743
  highlightFixtures: meta.highlightFixtures,
2744
+ demoPace: meta.demoPace,
2635
2745
  expectedFailureReason:
2636
2746
  meta.expectedFailureReason && remainingInFlow === 1 ? meta.expectedFailureReason : undefined,
2637
2747
  seedMem: isFirstOfSegment ? meta.seedMem : undefined,
package/dist/engine.d.ts CHANGED
@@ -186,6 +186,11 @@ export interface Flow<Out extends Checkpoint<string>> {
186
186
  * block.name. `run()` never reads this; step/`demo` merges onto block stubs.
187
187
  */
188
188
  readonly highlightFixtures?: import("./highlights.js").HighlightFixtureMap;
189
+ /**
190
+ * Demo pacing for this Flow's episode (`withDemoPace`).
191
+ * `run()` ignores; step/demo applies per Block in the episode.
192
+ */
193
+ readonly demoPace?: import("./highlights.js").DemoPace;
189
194
  /**
190
195
  * This Flow's constituent Blocks, in order between `start`/`end`, as plain
191
196
  * data - for introspection/visualization tools, without running anything.
@@ -253,6 +258,18 @@ export declare function withSessionReset<Out extends Checkpoint<string>>(flow: F
253
258
  * @example const ticket123 = withTitle(loginFlow, "Ticket #123 - Sales rep signs in");
254
259
  */
255
260
  export declare function withTitle<Out extends Checkpoint<string>>(flow: Flow<Out>, title: string): Flow<Out>;
261
+ /**
262
+ * Demo pacing for a whole Flow episode (fast review vs slow gap/wrong walkthrough).
263
+ * Non-destructive. `fastForwardComposeBlock` is always blitz for its own step;
264
+ * use this when an entire episode should be fast or slow.
265
+ * @example withDemoPace(withTitle(gapFlow, "Episode 2 - gaps"), "slow")
266
+ */
267
+ export declare function withDemoPace<Out extends Checkpoint<string>>(flow: Flow<Out>, pace: import("./highlights.js").DemoPace): Flow<Out>;
268
+ /**
269
+ * Demo pacing for one Block or compose unit (not FF - use fastForwardComposeBlock for blitz).
270
+ * @example withBlockPace(composeBlock("gap-review", [...]), "slow")
271
+ */
272
+ export declare function withBlockPace<In extends Checkpoint<string>, Out extends Checkpoint<string>>(block: Block<In, Out>, pace: import("./highlights.js").DemoPace): Block<In, Out>;
256
273
  /**
257
274
  * Marks a Flow as one that's SUPPOSED to throw - e.g. a locked-out user's
258
275
  * login attempt genuinely failing, demonstrating the app's own real
package/dist/engine.js CHANGED
@@ -351,6 +351,31 @@ export function withTitle(flow, title) {
351
351
  modBlockVerify: (block, nameOrIndex, newCheck) => withTitle(flow.modBlockVerify(block, nameOrIndex, newCheck), title),
352
352
  };
353
353
  }
354
+ /**
355
+ * Demo pacing for a whole Flow episode (fast review vs slow gap/wrong walkthrough).
356
+ * Non-destructive. `fastForwardComposeBlock` is always blitz for its own step;
357
+ * use this when an entire episode should be fast or slow.
358
+ * @example withDemoPace(withTitle(gapFlow, "Episode 2 - gaps"), "slow")
359
+ */
360
+ export function withDemoPace(flow, pace) {
361
+ const demoPace = pace;
362
+ return {
363
+ ...flow,
364
+ demoPace,
365
+ withBlockVerify: (block, verify) => withDemoPace(flow.withBlockVerify(block, verify), demoPace),
366
+ modBlockVerify: (block, nameOrIndex, newCheck) => withDemoPace(flow.modBlockVerify(block, nameOrIndex, newCheck), demoPace),
367
+ };
368
+ }
369
+ /**
370
+ * Demo pacing for one Block or compose unit (not FF - use fastForwardComposeBlock for blitz).
371
+ * @example withBlockPace(composeBlock("gap-review", [...]), "slow")
372
+ */
373
+ export function withBlockPace(block, pace) {
374
+ return {
375
+ ...block,
376
+ demoPace: pace,
377
+ };
378
+ }
354
379
  /**
355
380
  * Marks a Flow as one that's SUPPOSED to throw - e.g. a locked-out user's
356
381
  * login attempt genuinely failing, demonstrating the app's own real
@@ -33,9 +33,26 @@ export interface WaygraphHighlightStub extends FixtureDurationFields {
33
33
  detail?: string;
34
34
  /** Short badge, e.g. AC / BUG / GATE / YAP. */
35
35
  tag?: string;
36
+ /**
37
+ * Demo ring color + icon:
38
+ * - `planned` purple (authored narration, default)
39
+ * - `auto` gray (engine automation / verify fallback)
40
+ * - `info` blue · `warning` yellow · `danger` red · `success` green
41
+ */
42
+ tone?: HighlightTone;
43
+ /**
44
+ * Ring padding + label font scale: `sm` | `md` (default) | `lg`.
45
+ * Aliases: small/medium/large, s/m/l.
46
+ */
47
+ size?: HighlightSize;
48
+ /**
49
+ * Label font weight: `normal` (default) | `bold`.
50
+ * Aliases: regular, strong.
51
+ */
52
+ weight?: HighlightWeight;
36
53
  }
37
54
  /** Flow fixture patch: label required; selector optional (inherits from stub). */
38
- export type WaygraphHighlightFixture = Partial<Pick<WaygraphHighlightStub, "selector">> & Required<Pick<WaygraphHighlightStub, "label">> & Pick<WaygraphHighlightStub, "detail" | "tag"> & FixtureDurationFields;
55
+ export type WaygraphHighlightFixture = Partial<Pick<WaygraphHighlightStub, "selector">> & Required<Pick<WaygraphHighlightStub, "label">> & Pick<WaygraphHighlightStub, "detail" | "tag" | "tone" | "size" | "weight"> & FixtureDurationFields;
39
56
  export type HighlightStubPhase = Record<string, WaygraphHighlightStub>;
40
57
  export type HighlightStubPhaseOrFn<Out extends Checkpoint<string>> = HighlightStubPhase | ((out: Out) => HighlightStubPhase);
41
58
  /**
@@ -49,11 +66,42 @@ export interface WaygraphSlide extends FixtureDurationFields {
49
66
  tag?: string;
50
67
  /** Optional ring while this slide is showing. */
51
68
  selector?: string;
69
+ tone?: HighlightTone;
70
+ size?: HighlightSize;
71
+ weight?: HighlightWeight;
52
72
  }
73
+ /**
74
+ * Demo ring tones (iconified).
75
+ * Automation (`auto`) is gray so authored planned/info/warning/danger/success stand out.
76
+ */
77
+ export type HighlightTone = "planned" | "auto" | "info" | "warning" | "danger" | "success";
78
+ /** Ring + label scale. */
79
+ export type HighlightSize = "sm" | "md" | "lg";
80
+ /** Label font weight. */
81
+ export type HighlightWeight = "normal" | "bold";
82
+ /** Map author aliases (`error` -> danger) to a canonical tone. */
83
+ export declare function normalizeHighlightTone(raw: unknown): HighlightTone;
84
+ /** Map size aliases to sm/md/lg. */
85
+ export declare function normalizeHighlightSize(raw: unknown): HighlightSize;
86
+ /** Map weight aliases to normal/bold. */
87
+ export declare function normalizeHighlightWeight(raw: unknown): HighlightWeight;
88
+ /** Prefix label with a short ASCII icon for semantic tones. */
89
+ export declare function toneIconPrefix(tone: HighlightTone): string;
90
+ /**
91
+ * Demo pacing for a Flow episode or a Block/compose group.
92
+ * - `blitz` - FF-like: skip theater + short gates (same as fastForwardComposeBlock)
93
+ * - `fast` - shorter gates, keep cursor theater
94
+ * - `normal` - default demo pace
95
+ * - `slow` - longer autoplay + fixture dwell (gaps / wrongs review)
96
+ */
97
+ export type DemoPace = "blitz" | "fast" | "normal" | "slow";
98
+ export declare function normalizeDemoPace(raw: unknown): DemoPace;
53
99
  /** Default fixture dwell when `duration: true`. */
54
100
  export declare const FIXTURE_DURATION_MS_DEFAULT = 2000;
55
101
  /** Default fixture dwell under `--fast` when duration is enabled. */
56
102
  export declare const FIXTURE_DURATION_FAST_MS_DEFAULT = 600;
103
+ /** Default fixture dwell under demo pace `slow` (gaps / wrongs review). */
104
+ export declare const FIXTURE_DURATION_SLOW_MS_DEFAULT = 4000;
57
105
  /** @deprecated Prefer FIXTURE_DURATION_MS_DEFAULT */
58
106
  export declare const SLIDE_DURATION_MS_DEFAULT = 2000;
59
107
  /** @deprecated Prefer FIXTURE_DURATION_FAST_MS_DEFAULT */
@@ -64,11 +112,23 @@ export declare const SLIDE_DURATION_FAST_MS_DEFAULT = 600;
64
112
  */
65
113
  export declare function resolveFixtureDwellMs(fixture: FixtureDurationFields, opts?: {
66
114
  gatesFast?: boolean;
115
+ pace?: DemoPace;
67
116
  }): number | null;
68
117
  /** @deprecated Prefer {@link resolveFixtureDwellMs} */
69
118
  export declare function resolveSlideDwellMs(slide: FixtureDurationFields, opts?: {
70
119
  gatesFast?: boolean;
120
+ pace?: DemoPace;
71
121
  }): number | null;
122
+ /**
123
+ * Effective demo pace for a step: block override > flow episode > normal.
124
+ * Opaque FF / wasFastForward always win as blitz.
125
+ */
126
+ export declare function resolveStepDemoPace(opts: {
127
+ blockPace?: DemoPace | string | null;
128
+ flowPace?: DemoPace | string | null;
129
+ fastForward?: boolean;
130
+ wasFastForward?: boolean;
131
+ }): DemoPace;
72
132
  export type WaygraphSlidesOrFn<Out extends Checkpoint<string>> = readonly WaygraphSlide[] | ((out: Out) => readonly WaygraphSlide[]);
73
133
  /** Demo stub phase names (block instruction + flow fixtures). */
74
134
  export type HighlightStubPhaseName = "stubBefore" | "stubAfter" | "stubOnError";
@@ -85,6 +145,7 @@ export type HighlightFixtureMap = Record<string, {
85
145
  export interface WaygraphHighlight {
86
146
  selector: string;
87
147
  label: string;
148
+ tone?: HighlightTone;
88
149
  }
89
150
  export type ResolvedHighlight = WaygraphHighlightStub;
90
151
  /**
@@ -104,12 +165,13 @@ export declare function resolveSlides(block: Block<any, any>, opts?: {
104
165
  out?: Checkpoint<string>;
105
166
  fixtures?: HighlightFixtureMap;
106
167
  }): WaygraphSlide[];
107
- /** Caption for ring / slide UI (label|caption + optional detail/tag). */
168
+ /** Caption for ring / slide UI (label|caption + optional detail/tag + tone icon). */
108
169
  export declare function formatHighlightCaption(h: {
109
170
  label?: string;
110
171
  caption?: string;
111
172
  detail?: string;
112
173
  tag?: string;
174
+ tone?: HighlightTone | string;
113
175
  }): string;
114
176
  /**
115
177
  * True when the block authored a non-empty stubAfter (after resolve + shim),
@@ -8,10 +8,76 @@
8
8
  * All fixture shapes share optional {@link FixtureDurationFields} so demo dwell
9
9
  * before advancing is authored the same way on stubs, flow fixtures, and slides.
10
10
  */
11
+ const TONE_ICONS = {
12
+ planned: "",
13
+ auto: "",
14
+ info: "i",
15
+ warning: "!",
16
+ danger: "x",
17
+ success: "+",
18
+ };
19
+ /** Map author aliases (`error` -> danger) to a canonical tone. */
20
+ export function normalizeHighlightTone(raw) {
21
+ if (typeof raw !== "string")
22
+ return "planned";
23
+ const t = raw.trim().toLowerCase();
24
+ if (t === "auto" || t === "automation" || t === "engine")
25
+ return "auto";
26
+ if (t === "info" || t === "blue")
27
+ return "info";
28
+ if (t === "warning" || t === "warn" || t === "yellow")
29
+ return "warning";
30
+ if (t === "danger" || t === "error" || t === "red")
31
+ return "danger";
32
+ if (t === "success" || t === "ok" || t === "green")
33
+ return "success";
34
+ if (t === "planned" || t === "purple" || t === "authored")
35
+ return "planned";
36
+ return "planned";
37
+ }
38
+ /** Map size aliases to sm/md/lg. */
39
+ export function normalizeHighlightSize(raw) {
40
+ if (typeof raw !== "string")
41
+ return "md";
42
+ const t = raw.trim().toLowerCase();
43
+ if (t === "sm" || t === "s" || t === "small" || t === "tiny")
44
+ return "sm";
45
+ if (t === "lg" || t === "l" || t === "large" || t === "big" || t === "xl")
46
+ return "lg";
47
+ return "md";
48
+ }
49
+ /** Map weight aliases to normal/bold. */
50
+ export function normalizeHighlightWeight(raw) {
51
+ if (typeof raw !== "string")
52
+ return "normal";
53
+ const t = raw.trim().toLowerCase();
54
+ if (t === "bold" || t === "strong" || t === "heavy" || t === "b")
55
+ return "bold";
56
+ return "normal";
57
+ }
58
+ /** Prefix label with a short ASCII icon for semantic tones. */
59
+ export function toneIconPrefix(tone) {
60
+ const icon = TONE_ICONS[tone];
61
+ return icon ? `[${icon}] ` : "";
62
+ }
63
+ export function normalizeDemoPace(raw) {
64
+ if (typeof raw !== "string")
65
+ return "normal";
66
+ const t = raw.trim().toLowerCase();
67
+ if (t === "blitz" || t === "ff")
68
+ return "blitz";
69
+ if (t === "fast" || t === "quick")
70
+ return "fast";
71
+ if (t === "slow" || t === "careful" || t === "review")
72
+ return "slow";
73
+ return "normal";
74
+ }
11
75
  /** Default fixture dwell when `duration: true`. */
12
76
  export const FIXTURE_DURATION_MS_DEFAULT = 2000;
13
77
  /** Default fixture dwell under `--fast` when duration is enabled. */
14
78
  export const FIXTURE_DURATION_FAST_MS_DEFAULT = 600;
79
+ /** Default fixture dwell under demo pace `slow` (gaps / wrongs review). */
80
+ export const FIXTURE_DURATION_SLOW_MS_DEFAULT = 4000;
15
81
  /** @deprecated Prefer FIXTURE_DURATION_MS_DEFAULT */
16
82
  export const SLIDE_DURATION_MS_DEFAULT = FIXTURE_DURATION_MS_DEFAULT;
17
83
  /** @deprecated Prefer FIXTURE_DURATION_FAST_MS_DEFAULT */
@@ -23,23 +89,40 @@ export const SLIDE_DURATION_FAST_MS_DEFAULT = FIXTURE_DURATION_FAST_MS_DEFAULT;
23
89
  export function resolveFixtureDwellMs(fixture, opts) {
24
90
  if (fixture.duration === undefined || fixture.duration === false)
25
91
  return null;
26
- const fast = !!opts?.gatesFast;
92
+ const pace = normalizeDemoPace(opts?.pace ?? (opts?.gatesFast ? "fast" : "normal"));
27
93
  const fastMs = typeof fixture.fastMode === "number" && Number.isFinite(fixture.fastMode) && fixture.fastMode >= 0
28
94
  ? fixture.fastMode
29
95
  : FIXTURE_DURATION_FAST_MS_DEFAULT;
30
- if (fast)
96
+ if (pace === "blitz" || pace === "fast" || opts?.gatesFast)
31
97
  return fastMs;
32
- if (fixture.duration === true)
33
- return FIXTURE_DURATION_MS_DEFAULT;
34
- if (typeof fixture.duration === "number" && Number.isFinite(fixture.duration) && fixture.duration >= 0) {
35
- return fixture.duration;
36
- }
37
- return FIXTURE_DURATION_MS_DEFAULT;
98
+ const normalMs = fixture.duration === true
99
+ ? FIXTURE_DURATION_MS_DEFAULT
100
+ : typeof fixture.duration === "number" && Number.isFinite(fixture.duration) && fixture.duration >= 0
101
+ ? fixture.duration
102
+ : FIXTURE_DURATION_MS_DEFAULT;
103
+ if (pace === "slow")
104
+ return Math.max(normalMs, FIXTURE_DURATION_SLOW_MS_DEFAULT);
105
+ return normalMs;
38
106
  }
39
107
  /** @deprecated Prefer {@link resolveFixtureDwellMs} */
40
108
  export function resolveSlideDwellMs(slide, opts) {
41
109
  return resolveFixtureDwellMs(slide, opts);
42
110
  }
111
+ /**
112
+ * Effective demo pace for a step: block override > flow episode > normal.
113
+ * Opaque FF / wasFastForward always win as blitz.
114
+ */
115
+ export function resolveStepDemoPace(opts) {
116
+ if (opts.fastForward || opts.wasFastForward)
117
+ return "blitz";
118
+ if (opts.blockPace != null && opts.blockPace !== "") {
119
+ return normalizeDemoPace(opts.blockPace);
120
+ }
121
+ if (opts.flowPace != null && opts.flowPace !== "") {
122
+ return normalizeDemoPace(opts.flowPace);
123
+ }
124
+ return "normal";
125
+ }
43
126
  function isStubPhase(v) {
44
127
  return !!v && typeof v === "object" && !Array.isArray(v);
45
128
  }
@@ -53,10 +136,24 @@ function pickDurationFields(base, patch) {
53
136
  out.fastMode = fastMode;
54
137
  return out;
55
138
  }
139
+ function pickStyleFields(base, patch) {
140
+ const out = {};
141
+ const tone = patch?.tone !== undefined ? patch.tone : base?.tone;
142
+ const size = patch?.size !== undefined ? patch.size : base?.size;
143
+ const weight = patch?.weight !== undefined ? patch.weight : base?.weight;
144
+ if (tone !== undefined)
145
+ out.tone = normalizeHighlightTone(tone);
146
+ if (size !== undefined)
147
+ out.size = normalizeHighlightSize(size);
148
+ if (weight !== undefined)
149
+ out.weight = normalizeHighlightWeight(weight);
150
+ return out;
151
+ }
56
152
  function mergeSlot(base, patch) {
57
153
  if (!base && !patch)
58
154
  return null;
59
155
  const dwell = pickDurationFields(base, patch);
156
+ const style = pickStyleFields(base, patch);
60
157
  if (!base && patch) {
61
158
  if (!patch.selector)
62
159
  return null;
@@ -65,11 +162,18 @@ function mergeSlot(base, patch) {
65
162
  label: patch.label,
66
163
  ...(patch.detail ? { detail: patch.detail } : {}),
67
164
  ...(patch.tag ? { tag: patch.tag } : {}),
165
+ ...style,
68
166
  ...dwell,
69
167
  };
70
168
  }
71
- if (base && !patch)
72
- return { ...base };
169
+ if (base && !patch) {
170
+ return {
171
+ ...base,
172
+ ...(base.tone ? { tone: normalizeHighlightTone(base.tone) } : {}),
173
+ ...(base.size ? { size: normalizeHighlightSize(base.size) } : {}),
174
+ ...(base.weight ? { weight: normalizeHighlightWeight(base.weight) } : {}),
175
+ };
176
+ }
73
177
  return {
74
178
  selector: patch.selector ?? base.selector,
75
179
  label: patch.label,
@@ -79,6 +183,7 @@ function mergeSlot(base, patch) {
79
183
  ? { detail: base.detail }
80
184
  : {}),
81
185
  ...(patch.tag !== undefined ? { tag: patch.tag } : base.tag ? { tag: base.tag } : {}),
186
+ ...style,
82
187
  ...dwell,
83
188
  };
84
189
  }
@@ -88,7 +193,11 @@ function phaseFromLegacyHighlights(highlights) {
88
193
  return out;
89
194
  highlights.forEach((h, i) => {
90
195
  if (h && typeof h.selector === "string" && typeof h.label === "string") {
91
- out[String(i)] = { selector: h.selector, label: h.label };
196
+ out[String(i)] = {
197
+ selector: h.selector,
198
+ label: h.label,
199
+ ...(h.tone ? { tone: normalizeHighlightTone(h.tone) } : {}),
200
+ };
92
201
  }
93
202
  });
94
203
  return out;
@@ -169,13 +278,15 @@ export function resolveSlides(block, opts) {
169
278
  return [];
170
279
  return raw.filter((s) => s && typeof s.caption === "string" && s.caption.length > 0);
171
280
  }
172
- /** Caption for ring / slide UI (label|caption + optional detail/tag). */
281
+ /** Caption for ring / slide UI (label|caption + optional detail/tag + tone icon). */
173
282
  export function formatHighlightCaption(h) {
174
- const primary = h.caption ?? h.label ?? "";
283
+ const tone = normalizeHighlightTone(h.tone);
284
+ const icon = toneIconPrefix(tone);
285
+ const primary = (h.label ?? h.caption ?? "").trim();
175
286
  const tag = h.tag ? `[${h.tag}] ` : "";
176
287
  if (h.detail)
177
- return `${tag}${primary} - ${h.detail}`;
178
- return `${tag}${primary}`;
288
+ return `${icon}${tag}${primary} - ${h.detail}`;
289
+ return `${icon}${tag}${primary}`;
179
290
  }
180
291
  /**
181
292
  * True when the block authored a non-empty stubAfter (after resolve + shim),
package/dist/index.d.ts CHANGED
@@ -2,9 +2,9 @@ export type { Checkpoint, Instruction, Block, DefinedBlock, Start, WaygraphInsta
2
2
  export { connect, checkpoint } from "./types.js";
3
3
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
4
4
  export { Trait, urlMatches, textEquals, visible } from "./trait.js";
5
- export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
6
- export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
7
- export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, } from "./highlights.js";
5
+ export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
6
+ export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, toneIconPrefix, resolveStepDemoPace, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
7
+ export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, DemoPace, } from "./highlights.js";
8
8
  export type { Flow, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, } 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
@@ -1,8 +1,8 @@
1
1
  export { connect, checkpoint } from "./types.js";
2
2
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
3
3
  export { Trait, urlMatches, textEquals, visible } from "./trait.js";
4
- export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
5
- export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
4
+ export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
5
+ export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, toneIconPrefix, resolveStepDemoPace, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } 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";
@@ -3,7 +3,7 @@ import type { MemPage } from "./mem-page.js";
3
3
  export declare function installDemoChrome(page: Page, title?: string, options?: {
4
4
  banner?: boolean;
5
5
  }): Promise<void>;
6
- /** Demo cursor + ring on fill/click during auto explore headful runs (automation = yellow). */
6
+ /** Demo cursor + ring on fill/click during auto explore headful runs (automation = gray). */
7
7
  export declare function instrumentInteractionHighlighting(page: Page, mem: MemPage, slowMo: number): void;
8
8
  declare global {
9
9
  interface Window {
@@ -1,11 +1,26 @@
1
1
  const RING_CSS = "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
2
2
  "border:2.5px solid #7C3AED;border-radius:10px;box-shadow:0 0 0 4px rgba(124,58,237,.16);" +
3
3
  "transition:opacity .3s ease,border-color .15s,box-shadow .15s;}" +
4
- "#wg-ring[data-tone=auto]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
4
+ // Automation = gray; semantic tones for authored highlights.
5
+ "#wg-ring[data-tone=auto]{border-color:#9CA3AF;box-shadow:0 0 0 4px rgba(156,163,175,.28);}" +
6
+ "#wg-ring[data-tone=info]{border-color:#3B82F6;box-shadow:0 0 0 4px rgba(59,130,246,.22);}" +
7
+ "#wg-ring[data-tone=warning]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
8
+ "#wg-ring[data-tone=danger]{border-color:#EF4444;box-shadow:0 0 0 4px rgba(239,68,68,.22);}" +
9
+ "#wg-ring[data-tone=success]{border-color:#22C55E;box-shadow:0 0 0 4px rgba(34,197,94,.22);}" +
5
10
  "#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
6
11
  "white-space:nowrap;padding:4px 9px;border-radius:7px;background:#7C3AED;color:#fff;" +
7
12
  "font:600 12px/1.2 system-ui,sans-serif;transition:opacity .3s ease,background .15s,color .15s;}" +
8
- "#wg-ring-label[data-tone=auto]{background:#EAB308;color:#1c1917;}" +
13
+ "#wg-ring-label[data-tone=auto]{background:#6B7280;color:#fff;}" +
14
+ "#wg-ring-label[data-tone=info]{background:#2563EB;color:#fff;}" +
15
+ "#wg-ring-label[data-tone=warning]{background:#EAB308;color:#1c1917;}" +
16
+ "#wg-ring-label[data-tone=danger]{background:#DC2626;color:#fff;}" +
17
+ "#wg-ring-label[data-tone=success]{background:#16A34A;color:#fff;}" +
18
+ "#wg-ring[data-size=sm]{border-width:1.5px;border-radius:8px;}" +
19
+ "#wg-ring[data-size=lg]{border-width:4px;border-radius:12px;}" +
20
+ "#wg-ring-label[data-size=sm]{font-size:10px;line-height:1.25;padding:4px 7px;border-radius:5px;}" +
21
+ "#wg-ring-label[data-size=lg]{font-size:16px;line-height:1.35;padding:8px 14px;border-radius:9px;}" +
22
+ "#wg-ring-label[data-weight=bold]{font-weight:800;}" +
23
+ "#wg-ring-label[data-weight=normal]{font-weight:600;}" +
9
24
  "#wg-cursor{position:fixed;z-index:2147483647;width:24px;height:24px;pointer-events:none;" +
10
25
  "left:0;top:0;opacity:0;margin:0;" +
11
26
  "transition:transform var(--wg-cursor-ms,600ms) cubic-bezier(.22,1,.36,1),opacity .2s ease;" +
@@ -13,7 +28,11 @@ const RING_CSS = "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none
13
28
  "#wg-click-pulse{position:fixed;z-index:2147483647;width:14px;height:14px;" +
14
29
  "margin-left:-7px;margin-top:-7px;border-radius:50%;pointer-events:none;opacity:0;" +
15
30
  "border:2px solid #7C3AED;background:rgba(124,58,237,.25);}" +
16
- "#wg-click-pulse[data-tone=auto]{border-color:#EAB308;background:rgba(234,179,8,.28);}" +
31
+ "#wg-click-pulse[data-tone=auto]{border-color:#9CA3AF;background:rgba(156,163,175,.28);}" +
32
+ "#wg-click-pulse[data-tone=info]{border-color:#3B82F6;background:rgba(59,130,246,.28);}" +
33
+ "#wg-click-pulse[data-tone=warning]{border-color:#EAB308;background:rgba(234,179,8,.28);}" +
34
+ "#wg-click-pulse[data-tone=danger]{border-color:#EF4444;background:rgba(239,68,68,.28);}" +
35
+ "#wg-click-pulse[data-tone=success]{border-color:#22C55E;background:rgba(34,197,94,.28);}" +
17
36
  "#wg-click-pulse.wg-pulse{animation:wg-pulse .5s ease-out;}" +
18
37
  "@keyframes wg-pulse{0%{opacity:.9;transform:scale(.4);}100%{opacity:0;transform:scale(2.4);}}" +
19
38
  "#wg-banner{position:fixed;z-index:2147483647;top:14px;left:14px;" +
@@ -65,25 +84,48 @@ export async function installDemoChrome(page, title = "waygraph auto", options =
65
84
  const pulse = document.getElementById("wg-click-pulse");
66
85
  if (!pulse)
67
86
  return;
68
- pulse.dataset.tone = tone === "auto" ? "auto" : "planned";
87
+ const raw = (tone || "planned") + "";
88
+ pulse.dataset.tone =
89
+ raw === "auto" ||
90
+ raw === "info" ||
91
+ raw === "warning" ||
92
+ raw === "danger" ||
93
+ raw === "success"
94
+ ? raw
95
+ : "planned";
69
96
  pulse.style.left = x + "px";
70
97
  pulse.style.top = y + "px";
71
98
  pulse.classList.remove("wg-pulse");
72
99
  void pulse.offsetWidth;
73
100
  pulse.classList.add("wg-pulse");
74
101
  };
75
- window.__wgPositionRing = (box, label, tone) => {
102
+ window.__wgPositionRing = (box, label, tone, style) => {
76
103
  const ring = document.getElementById("wg-ring");
77
104
  const ringLabel = document.getElementById("wg-ring-label");
78
105
  if (!ring || !ringLabel)
79
106
  return;
80
- const t = tone === "auto" ? "auto" : "planned";
107
+ const raw = (tone || "planned") + "";
108
+ const t = raw === "auto" ||
109
+ raw === "info" ||
110
+ raw === "warning" ||
111
+ raw === "danger" ||
112
+ raw === "success"
113
+ ? raw
114
+ : "planned";
115
+ const sizeRaw = ((style && style.size) || "md") + "";
116
+ const size = sizeRaw === "sm" || sizeRaw === "lg" ? sizeRaw : "md";
117
+ const weightRaw = ((style && style.weight) || "normal") + "";
118
+ const weight = weightRaw === "bold" ? "bold" : "normal";
81
119
  ring.dataset.tone = t;
82
120
  ringLabel.dataset.tone = t;
83
- ring.style.left = box.x - 6 + "px";
84
- ring.style.top = box.y - 6 + "px";
85
- ring.style.width = box.width + 12 + "px";
86
- ring.style.height = box.height + 12 + "px";
121
+ ring.dataset.size = size;
122
+ ringLabel.dataset.size = size;
123
+ ringLabel.dataset.weight = weight;
124
+ const pad = size === "sm" ? 3 : size === "lg" ? 10 : 6;
125
+ ring.style.left = box.x - pad + "px";
126
+ ring.style.top = box.y - pad + "px";
127
+ ring.style.width = box.width + pad * 2 + "px";
128
+ ring.style.height = box.height + pad * 2 + "px";
87
129
  ring.style.opacity = "1";
88
130
  ringLabel.textContent = label;
89
131
  ringLabel.style.left = box.x + "px";
@@ -136,7 +178,7 @@ async function clickPulseAt(page, x, y, tone = "auto") {
136
178
  .evaluate(({ x, y, tone }) => window.__wgClickPulse?.(x, y, tone), { x, y, tone })
137
179
  .catch(() => { });
138
180
  }
139
- /** Demo cursor + ring on fill/click during auto explore headful runs (automation = yellow). */
181
+ /** Demo cursor + ring on fill/click during auto explore headful runs (automation = gray). */
140
182
  export function instrumentInteractionHighlighting(page, mem, slowMo) {
141
183
  const typeDelay = () => (slowMo ? 0 : 30);
142
184
  const clickPrePop = () => (slowMo ? 300 : 700);
package/dist/types.d.ts CHANGED
@@ -209,6 +209,11 @@ export interface Block<In extends Checkpoint<string>, Out extends Checkpoint<str
209
209
  * @example requires: [LoginCreds.key]
210
210
  */
211
211
  requires?: readonly MemKey<any>[];
212
+ /**
213
+ * Demo pacing for this Block or compose group (`withBlockPace`).
214
+ * Demo/step only - `runGraph` ignores. Prefer `fastForwardComposeBlock` for blitz.
215
+ */
216
+ demoPace?: import("./highlights.js").DemoPace;
212
217
  /**
213
218
  * Optional. When this Block's real choices depend on the live page
214
219
  * (which of several items to add to cart, which of several rows to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.14",
3
+ "version": "0.12.16",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",