waygraph 0.12.48 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -230,6 +230,8 @@ import {
230
230
  applyTodoPhase,
231
231
  advanceTodoDock,
232
232
  completeSequentialTodoDock,
233
+ applyDevicePhase,
234
+ resolveDeviceState,
233
235
  } from "waygraph";
234
236
 
235
237
  function walkDir(dir, pattern) {
@@ -530,6 +532,18 @@ const RING_CSS =
530
532
  "#wg-todo-dock[data-pos=right]{transform:translateX(calc(100vw - 100% - 28px));}" +
531
533
  "#wg-todo-dock[data-pos=left]{transform:translateX(0);}" +
532
534
  "#wg-todo-dock #wg-todos{list-style:none;margin:0;padding:0;background:transparent;border:none;}" +
535
+ // Device / touch badge (0.13+) - shows preset + remain|desktop reset.
536
+ "#wg-device-badge{position:fixed;z-index:2147483646;top:14px;right:14px;" +
537
+ "padding:6px 10px;border-radius:999px;background:rgba(20,10,40,.92);color:#f0e8ff;" +
538
+ "border:1px solid rgba(124,58,237,.5);font:700 11px/1.2 system-ui,sans-serif;" +
539
+ "letter-spacing:.04em;text-transform:uppercase;pointer-events:none;" +
540
+ "box-shadow:0 4px 14px rgba(0,0,0,.3);opacity:0;transition:opacity .25s ease;}" +
541
+ "#wg-device-badge.wg-in{opacity:1;}" +
542
+ "#wg-device-badge[data-preset=mobile]{border-color:#3B82F6;}" +
543
+ "#wg-device-badge[data-preset=tablet]{border-color:#22C55E;}" +
544
+ "#wg-device-badge[data-preset=desktop]{border-color:#9CA3AF;}" +
545
+ "#wg-device-badge[data-touch=1]::after{content:' · touch';opacity:.85;}" +
546
+ "#wg-cursor[data-touch=1]{width:28px;height:28px;}" +
533
547
  "#wg-todos{list-style:none;margin:0 0 12px;padding:8px 10px;background:#0f0620;border-radius:8px;" +
534
548
  "border:1px solid #3a2a60;}" +
535
549
  "#wg-todos li{display:flex;gap:8px;align-items:flex-start;margin:0 0 6px;font:600 12.5px/1.35 system-ui,sans-serif;}" +
@@ -538,6 +552,9 @@ const RING_CSS =
538
552
  "#wg-todos .wg-todo-done{color:#6ee7b7;text-decoration:line-through;opacity:.85;}" +
539
553
  "#wg-todos .wg-todo-current{color:#fff;}" +
540
554
  "#wg-todos .wg-todo-pending{color:#9a7ad1;}" +
555
+ "#wg-todos[data-wg-todo-style=bullets] .wg-todo-pending," +
556
+ "#wg-todos[data-wg-todo-style=bullets] .wg-todo-current{color:#e8dcff;}" +
557
+ "#wg-todos[data-wg-todo-style=bullets] .wg-todo-mark{color:#c9a6ff;}" +
541
558
  "#wg-progress{height:4px;background:#2a1650;border-radius:2px;margin:0 0 12px;overflow:hidden;}" +
542
559
  "#wg-progress-bar{height:100%;background:#7C3AED;border-radius:2px;transition:width .3s ease;}" +
543
560
  // A real tab bar for episodes - the currently-active episode reads as
@@ -1182,21 +1199,39 @@ async function installOverlay(page, title) {
1182
1199
  return id ? ' data-wg-todo-item="' + esc(id) + '"' : "";
1183
1200
  };
1184
1201
  const renderItems = (items, style) => {
1185
- const st = style === "checklist" ? "checklist" : "sequential";
1202
+ const rawStyle = String(style || "sequential").toLowerCase();
1203
+ const st =
1204
+ rawStyle === "checklist"
1205
+ ? "checklist"
1206
+ : rawStyle === "bullets" ||
1207
+ rawStyle === "bullet" ||
1208
+ rawStyle === "list" ||
1209
+ rawStyle === "ul" ||
1210
+ rawStyle === "points" ||
1211
+ rawStyle === "plain"
1212
+ ? "bullets"
1213
+ : "sequential";
1186
1214
  return (
1187
1215
  '<ul id="wg-todos" data-wg-todo-style="' +
1188
1216
  st +
1189
1217
  '">' +
1190
1218
  (items || [])
1191
1219
  .map((t) => {
1192
- const cls = t.current
1193
- ? "wg-todo-current"
1194
- : t.done
1195
- ? "wg-todo-done"
1196
- : "wg-todo-pending";
1220
+ const cls =
1221
+ st === "bullets"
1222
+ ? t.done
1223
+ ? "wg-todo-done"
1224
+ : "wg-todo-pending"
1225
+ : t.current
1226
+ ? "wg-todo-current"
1227
+ : t.done
1228
+ ? "wg-todo-done"
1229
+ : "wg-todo-pending";
1197
1230
  let mark;
1198
1231
  if (st === "checklist") {
1199
1232
  mark = t.done ? "\u2611" : "\u2610";
1233
+ } else if (st === "bullets") {
1234
+ mark = "\u2022";
1200
1235
  } else {
1201
1236
  mark = t.done ? "\u2713" : t.current ? "\u2192" : "\u25CB";
1202
1237
  }
@@ -1252,6 +1287,64 @@ async function installOverlay(page, title) {
1252
1287
  window.__wgStampModal(dock, "todos", { ready: true });
1253
1288
  }
1254
1289
  };
1290
+ // Device / touch badge (0.13+) - mirror todo sync keep/set/clear.
1291
+ // Payload: { sync, device?, remain? }
1292
+ window.__wgSyncDevice = (payload) => {
1293
+ const sync =
1294
+ payload && (payload.sync === "clear" || payload.sync === "keep" || payload.sync === "set")
1295
+ ? payload.sync
1296
+ : "keep";
1297
+ const device = payload && payload.device ? payload.device : null;
1298
+ let badge = document.getElementById("wg-device-badge");
1299
+ const cursor = document.getElementById("wg-cursor");
1300
+ const MOUSE_SVG =
1301
+ "<svg viewBox='0 0 32 32' width='24' height='24'>" +
1302
+ "<path fill='#0C0C1A' stroke='#fff' stroke-width='1.4' stroke-linejoin='round' " +
1303
+ "d='M6 3.5l1.4 22.5 5.8-5.4 4.2 9.4 3.6-1.6-4.2-9.2H26z'/></svg>";
1304
+ const FINGER_SVG =
1305
+ "<svg viewBox='0 0 32 32' width='28' height='28'>" +
1306
+ "<ellipse cx='16' cy='22' rx='7' ry='8' fill='#0C0C1A' stroke='#fff' stroke-width='1.4'/>" +
1307
+ "<rect x='12' y='6' width='8' height='16' rx='4' fill='#0C0C1A' stroke='#fff' stroke-width='1.4'/>" +
1308
+ "</svg>";
1309
+ if (sync === "keep" && !device) return;
1310
+ if (sync === "clear" || (device && device.preset === "desktop" && !device.touchMode)) {
1311
+ if (badge) badge.remove();
1312
+ if (cursor) {
1313
+ cursor.removeAttribute("data-touch");
1314
+ cursor.innerHTML = MOUSE_SVG;
1315
+ }
1316
+ if (sync === "clear" && !device) return;
1317
+ // clear with desktop device: still hide badge (back to main)
1318
+ if (device && device.preset === "desktop" && !device.touchMode) return;
1319
+ }
1320
+ if (!device) return;
1321
+ const preset = device.preset || "desktop";
1322
+ const touch = !!device.touchMode;
1323
+ const remain = payload.remain !== false && sync !== "clear";
1324
+ if (!badge) {
1325
+ badge = document.createElement("div");
1326
+ badge.id = "wg-device-badge";
1327
+ badge.setAttribute("data-wg-ui", "1");
1328
+ document.documentElement.appendChild(badge);
1329
+ }
1330
+ badge.dataset.preset = preset;
1331
+ badge.dataset.touch = touch ? "1" : "0";
1332
+ const label =
1333
+ preset === "desktop" && !touch
1334
+ ? "desktop"
1335
+ : preset + (remain ? " · remain" : " · set");
1336
+ badge.textContent = label;
1337
+ badge.classList.add("wg-in");
1338
+ if (cursor) {
1339
+ if (touch) {
1340
+ cursor.setAttribute("data-touch", "1");
1341
+ cursor.innerHTML = FINGER_SVG;
1342
+ } else {
1343
+ cursor.removeAttribute("data-touch");
1344
+ cursor.innerHTML = MOUSE_SVG;
1345
+ }
1346
+ }
1347
+ };
1255
1348
  },
1256
1349
  { title, favicon: WAYGRAPH_FAVICON, bannerPos, todoPos, envAutoplay },
1257
1350
  )
@@ -2681,6 +2774,49 @@ async function clickPulseAt(page, x, y, tone) {
2681
2774
  .catch(() => {});
2682
2775
  }
2683
2776
 
2777
+ /**
2778
+ * Apply authored device fixture to the live page (0.13+).
2779
+ * Uses the real setViewportSize (step mode noops block-authored calls).
2780
+ * sync keep with no device = no-op; clear / desktop = restore main feel.
2781
+ */
2782
+ async function applyDeviceToPage(page, device, sync) {
2783
+ const mode = sync || (device ? "set" : "keep");
2784
+ if (mode === "keep" && !device) return;
2785
+ const d =
2786
+ device ||
2787
+ resolveDeviceState("desktop", false) || {
2788
+ preset: "desktop",
2789
+ viewport: { width: 1280, height: 720, deviceScaleFactor: 1 },
2790
+ isMobile: false,
2791
+ hasTouch: false,
2792
+ touchMode: false,
2793
+ };
2794
+ const realSet =
2795
+ typeof page.__wgRealSetViewportSize === "function"
2796
+ ? page.__wgRealSetViewportSize
2797
+ : page.setViewportSize.bind(page);
2798
+ try {
2799
+ await realSet({
2800
+ width: d.viewport.width,
2801
+ height: d.viewport.height,
2802
+ });
2803
+ } catch {
2804
+ /* maximized / null viewport hosts may reject - badge still updates */
2805
+ }
2806
+ await page
2807
+ .evaluate(
2808
+ (payload) => {
2809
+ if (window.__wgSyncDevice) window.__wgSyncDevice(payload);
2810
+ },
2811
+ {
2812
+ sync: mode,
2813
+ device: d,
2814
+ remain: mode === "keep" || mode === "set",
2815
+ },
2816
+ )
2817
+ .catch(() => {});
2818
+ }
2819
+
2684
2820
  /**
2685
2821
  * True if the engine's own narrate() (waygraph's public export, for a Block
2686
2822
  * author explicitly captioning one action) JUST positioned the ring for
@@ -2697,7 +2833,7 @@ async function wasJustNarrated(page) {
2697
2833
  .catch(() => false);
2698
2834
  }
2699
2835
 
2700
- function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef, todoDockRef) {
2836
+ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef, todoDockRef, deviceRef) {
2701
2837
  // Playwright's own slowMo ALREADY pauses after every single low-level
2702
2838
  // action it dispatches - and pressSequentially() fires one such action
2703
2839
  // PER CHARACTER. Also giving pressSequentially its own fixed delay
@@ -2861,7 +2997,36 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2861
2997
  // mock click visually completes before the real one fires.
2862
2998
  await new Promise((res) => setTimeout(res, skipTheater() ? 0 : 500));
2863
2999
  }
2864
- const result = await originalClick.call(this, options);
3000
+ // Touch theater (0.13+): prefer Locator.tap / long-press when device
3001
+ // touchMode is on (context launched with hasTouch). gesture on stub
3002
+ // wins: tap | hold | click.
3003
+ let result;
3004
+ const stubsForGesture = (stubBeforeRef && stubBeforeRef.current) || [];
3005
+ let gesture = "click";
3006
+ try {
3007
+ const idx = await matchStubIndexForLocator(page, this, stubsForGesture);
3008
+ const stub = idx >= 0 ? stubsForGesture[idx] : null;
3009
+ if (stub && stub.gesture) gesture = stub.gesture;
3010
+ else if (deviceRef && deviceRef.current && deviceRef.current.touchMode) gesture = "tap";
3011
+ } catch {
3012
+ if (deviceRef && deviceRef.current && deviceRef.current.touchMode) gesture = "tap";
3013
+ }
3014
+ if (gesture === "tap" || gesture === "hold") {
3015
+ try {
3016
+ if (gesture === "hold") {
3017
+ result = await this.tap({
3018
+ ...(options || {}),
3019
+ delay: skipTheater() ? 0 : 600,
3020
+ });
3021
+ } else {
3022
+ result = await this.tap(options);
3023
+ }
3024
+ } catch {
3025
+ result = await originalClick.call(this, options);
3026
+ }
3027
+ } else {
3028
+ result = await originalClick.call(this, options);
3029
+ }
2865
3030
  await new Promise((res) => setTimeout(res, clickPostPop()));
2866
3031
  await hideRing(page);
2867
3032
  return result;
@@ -2971,7 +3136,9 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2971
3136
  const stubBeforeRef = { current: [] };
2972
3137
  /** Live todo dock for Method fill/click advance (same carry as lastTodoDock). */
2973
3138
  const todoDockRef = { current: undefined };
2974
- instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef, todoDockRef);
3139
+ /** Live device fixture for touch theater (same carry as lastDevice). */
3140
+ const deviceRef = { current: undefined };
3141
+ instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef, todoDockRef, deviceRef);
2975
3142
  // Force the panel checkbox from this process's flags/env at run start.
2976
3143
  // installOverlay only seeds localStorage when the key is null (so mid-run
2977
3144
  // checkbox clicks survive navigations). Without this, a prior --autoplay
@@ -3002,7 +3169,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3002
3169
  // setViewportSize entirely for the duration of step mode. Blocks that
3003
3170
  // call it don't need it to actually do anything here - their own
3004
3171
  // selectors/layouts still work fine at whatever size the real window
3005
- // already is.
3172
+ // already is. Authored ctx.device() still uses __wgRealSetViewportSize.
3173
+ page.__wgRealSetViewportSize = page.setViewportSize.bind(page);
3006
3174
  page.setViewportSize = async () => {};
3007
3175
  let resolveNext = null;
3008
3176
  // Render-to-gate race: a slide/step panel's Next button is wired to
@@ -3100,6 +3268,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3100
3268
  let result;
3101
3269
  /** Carry floating todo dock across steps (empty stubBefore must not wipe). */
3102
3270
  let lastTodoDock = undefined;
3271
+ /** Carry device / touch fixture across steps (omit = keep, like todos). */
3272
+ let lastDevice = undefined;
3103
3273
  for (let i = 0; i < resolved.length; i++) {
3104
3274
  const r = resolved[i];
3105
3275
  // The block breadcrumb is scoped to THIS step's own episode, not the
@@ -3193,6 +3363,15 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3193
3363
  });
3194
3364
  lastTodoDock = appliedBefore.dock;
3195
3365
  todoDockRef.current = lastTodoDock;
3366
+ const appliedDeviceBefore = applyDevicePhase(lastDevice, {
3367
+ deviceSync: stubBeforePhase.deviceSync,
3368
+ device: stubBeforePhase.device,
3369
+ });
3370
+ lastDevice = appliedDeviceBefore.device;
3371
+ deviceRef.current = lastDevice;
3372
+ if (appliedDeviceBefore.sync !== "keep" || lastDevice) {
3373
+ await applyDeviceToPage(page, lastDevice, appliedDeviceBefore.sync);
3374
+ }
3196
3375
  const stubBeforeTodos =
3197
3376
  (lastTodoDock && lastTodoDock.groups[0] && lastTodoDock.groups[0].items) ||
3198
3377
  stubBeforePhase.todos ||
@@ -3418,6 +3597,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3418
3597
  let afterOverlayTitle = overlayTitle;
3419
3598
  let afterTodoPos = overlayTodoPos;
3420
3599
  let appliedAfter = { dock: lastTodoDock, sync: "keep" };
3600
+ let appliedDeviceAfter = { device: lastDevice, sync: "keep" };
3421
3601
  if (hasAuthoredStubAfter(r.block, result, fixturesAfter)) {
3422
3602
  const afterPhase = await runStubPhase(r.block, "stubAfter", {
3423
3603
  out: result,
@@ -3431,6 +3611,15 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3431
3611
  });
3432
3612
  lastTodoDock = appliedAfter.dock;
3433
3613
  todoDockRef.current = lastTodoDock;
3614
+ appliedDeviceAfter = applyDevicePhase(lastDevice, {
3615
+ deviceSync: afterPhase.deviceSync,
3616
+ device: afterPhase.device,
3617
+ });
3618
+ lastDevice = appliedDeviceAfter.device;
3619
+ deviceRef.current = lastDevice;
3620
+ if (appliedDeviceAfter.sync !== "keep" || (appliedDeviceAfter.sync === "set" && lastDevice)) {
3621
+ await applyDeviceToPage(page, lastDevice, appliedDeviceAfter.sync);
3622
+ }
3434
3623
  stubAfterTodos =
3435
3624
  (lastTodoDock && lastTodoDock.groups[0] && lastTodoDock.groups[0].items) ||
3436
3625
  afterPhase.todos ||
@@ -3880,10 +4069,11 @@ async function main() {
3880
4069
  // Lock DPR so --video-viewport 800x450 stays 800x450 in the .webm
3881
4070
  // (HiDPI hosts otherwise scale recordVideo to e.g. 1920x1080).
3882
4071
  deviceScaleFactor: 1,
4072
+ hasTouch: true,
3883
4073
  }
3884
4074
  : step
3885
- ? { baseURL, viewport: null }
3886
- : { baseURL, viewport: { width: 1280, height: 720 } };
4075
+ ? { baseURL, viewport: null, hasTouch: true }
4076
+ : { baseURL, viewport: { width: 1280, height: 720 }, hasTouch: true };
3887
4077
  if (videoDir) {
3888
4078
  contextOpts.recordVideo = fixedVideoViewport
3889
4079
  ? { dir: videoDir, size: fixedVideoViewport }
@@ -51,8 +51,9 @@ export type WaygraphTodoInput = string | WaygraphTodoItem;
51
51
  * Dock list presentation:
52
52
  * - `sequential` - arrow on current, strike done (demo walkthrough)
53
53
  * - `checklist` - checkbox done/pending only (no current arrow; independent checks)
54
+ * - `bullets` - flat bullet list (no arrow/checkbox; open notes / trimmed dock)
54
55
  */
55
- export type TodoListStyle = "sequential" | "checklist";
56
+ export type TodoListStyle = "sequential" | "checklist" | "bullets";
56
57
  /** One titled group in the floating todo dock (FR / Scenarios / ACs, etc.). */
57
58
  export type WaygraphTodoGroupInput = {
58
59
  /** Stable group id -> `data-wg-todo-group`. */
@@ -99,7 +100,7 @@ export type StubPhaseFixtures = {
99
100
  * (e.g. Mailhog -> back to app and still find the checklist).
100
101
  */
101
102
  todoId?: string;
102
- /** `sequential` (default) or `checklist`. */
103
+ /** `sequential` (default), `checklist`, or `bullets`. */
103
104
  todoStyle?: TodoListStyle;
104
105
  /** Multiple titled lists (FR / Scenarios / ACs). Wins over bare todos when set. */
105
106
  todoGroups?: readonly WaygraphTodoGroupInput[];
@@ -117,6 +118,57 @@ export type StubPhaseFixtures = {
117
118
  * Authored control (also click / WAYGRAPH_TODO_POS / --todo-left|right).
118
119
  */
119
120
  todoPos?: "left" | "right";
121
+ /**
122
+ * Viewport preset: `mobile` | `tablet` | `desktop` (default / clear).
123
+ * Omit on later blocks = **keep** (same persist pattern as todos).
124
+ */
125
+ device?: DevicePreset | DeviceState;
126
+ /** Explicit pixel viewport (wins over preset width/height when both set). */
127
+ viewport?: DeviceViewport;
128
+ /** Touch theater + Playwright touchscreen when true. */
129
+ touch?: boolean;
130
+ };
131
+ /** Named viewport presets (0.13+). */
132
+ export type DevicePreset = "mobile" | "tablet" | "desktop";
133
+ /** Explicit viewport box. */
134
+ export type DeviceViewport = {
135
+ width: number;
136
+ height: number;
137
+ deviceScaleFactor?: number;
138
+ };
139
+ /**
140
+ * Carried device fixture (todo-dock shaped persist).
141
+ * Omit on a later step = keep; hideDevice/clearDevice/desktop = clear to desktop.
142
+ */
143
+ export type DeviceState = {
144
+ preset: DevicePreset;
145
+ viewport: DeviceViewport;
146
+ /** Playwright-ish: treat as mobile layout. */
147
+ isMobile: boolean;
148
+ /** Context needs hasTouch for touchscreen.tap (demo launches with hasTouch). */
149
+ hasTouch: boolean;
150
+ /** Demo gesture theater: finger cursor, tap/hold timing, prefer touchscreen. */
151
+ touchMode: boolean;
152
+ };
153
+ /** Built-in preset sizes (CSS pixels). */
154
+ export declare const DEVICE_PRESETS: Record<DevicePreset, Omit<DeviceState, "touchMode"> & {
155
+ touchMode?: boolean;
156
+ }>;
157
+ export declare function normalizeDevicePreset(raw: unknown): DevicePreset | undefined;
158
+ /** Resolve a preset name or partial state into a full DeviceState. */
159
+ export declare function resolveDeviceState(input: DevicePreset | DeviceState | DeviceViewport | undefined, touchOverride?: boolean): DeviceState | undefined;
160
+ /**
161
+ * Carry-forward for device fixtures (mirror {@link applyTodoPhase}).
162
+ * - clear: hideDevice / clearDevice / device("desktop") with clear intent
163
+ * - set: author set device/viewport/touch this phase
164
+ * - keep: omit - preserve previous (whole episode remain)
165
+ */
166
+ export declare function applyDevicePhase(prev: DeviceState | undefined, phase: {
167
+ deviceSync?: "set" | "clear" | "keep";
168
+ device?: DeviceState;
169
+ }): {
170
+ device: DeviceState | undefined;
171
+ sync: "set" | "clear" | "keep";
120
172
  };
121
173
  /** One named highlight slot on a block (selector + caption). */
122
174
  export interface WaygraphHighlightStub extends FixtureDurationFields {
@@ -176,6 +228,13 @@ export interface WaygraphHighlightStub extends FixtureDurationFields {
176
228
  * for authored stub queues; set false to keep cursor hidden).
177
229
  */
178
230
  cursor?: boolean;
231
+ /**
232
+ * Pointer feel when touchMode is on (0.13+):
233
+ * - `tap` - short press via touchscreen (default in touch mode)
234
+ * - `hold` - long-press (~600ms) then release
235
+ * - `click` - mouse click even in touch mode
236
+ */
237
+ gesture?: "tap" | "hold" | "click";
179
238
  }
180
239
  /** Flow fixture patch: label required; selector optional (inherits from stub). */
181
240
  export type WaygraphHighlightFixture = Partial<Pick<WaygraphHighlightStub, "selector">> & Required<Pick<WaygraphHighlightStub, "label">> & Pick<WaygraphHighlightStub, "detail" | "tag" | "tone" | "size" | "weight" | "zoom" | "zoomOut" | "focus" | "followMouse" | "follow" | "color" | "cursor"> & FixtureDurationFields;
@@ -222,8 +281,8 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
222
281
  */
223
282
  todoId(id: string): void;
224
283
  /**
225
- * Dock presentation: `sequential` (arrow walkthrough) or `checklist`
226
- * (independent checkboxes, no current arrow).
284
+ * Dock presentation: `sequential` (arrow walkthrough), `checklist`
285
+ * (independent checkboxes), or `bullets` (flat • list, no progress marks).
227
286
  */
228
287
  todoStyle(style: TodoListStyle): void;
229
288
  /**
@@ -238,6 +297,23 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
238
297
  hideTodos(): void;
239
298
  /** Alias for {@link StubCtx.hideTodos}. */
240
299
  clearTodos(): void;
300
+ /**
301
+ * Viewport / device fixture (0.13+). `mobile` | `tablet` | `desktop`.
302
+ * Omit on later blocks = **keep** (same persist as todos). Use
303
+ * {@link StubCtx.hideDevice} / {@link StubCtx.clearDevice} to return to desktop.
304
+ */
305
+ device(preset: DevicePreset | DeviceState): void;
306
+ /** Explicit CSS-pixel viewport (implies mobile-ish unless desktop-sized). */
307
+ viewport(box: DeviceViewport): void;
308
+ /** Enable / disable touch theater + touchscreen gestures. */
309
+ touch(on?: boolean): void;
310
+ /**
311
+ * Reset to desktop viewport and clear touch mode. Omit device on a later
312
+ * block = **keep**; only hideDevice / clearDevice / device("desktop") clears.
313
+ */
314
+ hideDevice(): void;
315
+ /** Alias for {@link StubCtx.hideDevice}. */
316
+ clearDevice(): void;
241
317
  /** Default camera zoom for rings without their own zoom. */
242
318
  zoom(n: number): void;
243
319
  /**
@@ -280,6 +356,15 @@ export type StubPhaseResult = {
280
356
  * - `keep` - author omitted todos; demo must leave previous dock alone
281
357
  */
282
358
  todoSync?: "set" | "clear" | "keep";
359
+ /** Resolved device after this phase (undefined when keep with no prior). */
360
+ device?: DeviceState;
361
+ /**
362
+ * Device intent (mirror todoSync):
363
+ * - `set` - apply {@link device}
364
+ * - `clear` - reset to desktop (hideDevice / clearDevice / device desktop)
365
+ * - `keep` - author omitted device; demo must leave previous alone
366
+ */
367
+ deviceSync?: "set" | "clear" | "keep";
283
368
  zoom?: number;
284
369
  zoomOut?: boolean;
285
370
  title?: string;
@@ -426,7 +511,8 @@ export declare function normalizeTodoPos(raw: unknown): "left" | "right" | undef
426
511
  export declare function normalizeTodoStyle(raw: unknown): TodoListStyle | undefined;
427
512
  /**
428
513
  * Normalize checklist rows; apply todoIndex for sequential done/current.
429
- * In checklist mode, todoIndex is ignored unless items omit explicit done.
514
+ * In checklist / bullets mode, todoIndex is ignored unless items omit explicit done
515
+ * (bullets never auto-mark current).
430
516
  */
431
517
  export declare function normalizeTodos(todos: readonly WaygraphTodoInput[] | undefined, todoIndex?: number, style?: TodoListStyle): WaygraphTodoItem[];
432
518
  /** Build the floating dock from bag / phase fields. */
@@ -456,13 +542,13 @@ export declare function applyTodoPhase(prev: TodoDockState | undefined, phase: {
456
542
  sync: "set" | "clear" | "keep";
457
543
  };
458
544
  /**
459
- * Advance every sequential group to `index` (capped). Checklist groups unchanged.
545
+ * Advance every sequential group to `index` (capped). Checklist / bullets unchanged.
460
546
  * Used while cycling rings / fill/click stubs so the walkthrough moves.
461
547
  */
462
548
  export declare function advanceTodoDock(dock: TodoDockState | undefined, index: number): TodoDockState | undefined;
463
549
  /**
464
550
  * Mark sequential groups complete (all done, none current) - e.g. end of step.
465
- * Checklist groups keep explicit done flags.
551
+ * Checklist / bullets groups keep explicit done flags.
466
552
  */
467
553
  export declare function completeSequentialTodoDock(dock: TodoDockState | undefined): TodoDockState | undefined;
468
554
  /**
@@ -12,6 +12,123 @@
12
12
  * All fixture shapes share optional {@link FixtureDurationFields} so demo dwell
13
13
  * before advancing is authored the same way on stubs, flow fixtures, and slides.
14
14
  */
15
+ /** Built-in preset sizes (CSS pixels). */
16
+ export const DEVICE_PRESETS = {
17
+ mobile: {
18
+ preset: "mobile",
19
+ viewport: { width: 390, height: 844, deviceScaleFactor: 2 },
20
+ isMobile: true,
21
+ hasTouch: true,
22
+ touchMode: true,
23
+ },
24
+ tablet: {
25
+ preset: "tablet",
26
+ viewport: { width: 768, height: 1024, deviceScaleFactor: 2 },
27
+ isMobile: true,
28
+ hasTouch: true,
29
+ touchMode: true,
30
+ },
31
+ desktop: {
32
+ preset: "desktop",
33
+ viewport: { width: 1280, height: 720, deviceScaleFactor: 1 },
34
+ isMobile: false,
35
+ hasTouch: false,
36
+ touchMode: false,
37
+ },
38
+ };
39
+ export function normalizeDevicePreset(raw) {
40
+ const s = String(raw ?? "")
41
+ .trim()
42
+ .toLowerCase();
43
+ if (s === "mobile" || s === "phone" || s === "m")
44
+ return "mobile";
45
+ if (s === "tablet" || s === "pad" || s === "t")
46
+ return "tablet";
47
+ if (s === "desktop" || s === "desk" || s === "d" || s === "main")
48
+ return "desktop";
49
+ return undefined;
50
+ }
51
+ /** Resolve a preset name or partial state into a full DeviceState. */
52
+ export function resolveDeviceState(input, touchOverride) {
53
+ if (input === undefined)
54
+ return undefined;
55
+ if (typeof input === "string") {
56
+ const p = normalizeDevicePreset(input);
57
+ if (!p)
58
+ return undefined;
59
+ const base = DEVICE_PRESETS[p];
60
+ const touchMode = touchOverride !== undefined ? !!touchOverride : !!base.touchMode;
61
+ return {
62
+ preset: base.preset,
63
+ viewport: { ...base.viewport },
64
+ isMobile: base.isMobile,
65
+ hasTouch: base.hasTouch || touchMode,
66
+ touchMode,
67
+ };
68
+ }
69
+ if ("width" in input && "height" in input && !("preset" in input)) {
70
+ const vp = input;
71
+ const touchMode = touchOverride !== undefined ? !!touchOverride : true;
72
+ return {
73
+ preset: "mobile",
74
+ viewport: {
75
+ width: Math.max(200, Math.floor(vp.width)),
76
+ height: Math.max(200, Math.floor(vp.height)),
77
+ ...(vp.deviceScaleFactor !== undefined
78
+ ? { deviceScaleFactor: vp.deviceScaleFactor }
79
+ : { deviceScaleFactor: 2 }),
80
+ },
81
+ isMobile: true,
82
+ hasTouch: true,
83
+ touchMode,
84
+ };
85
+ }
86
+ const d = input;
87
+ const preset = normalizeDevicePreset(d.preset) || "desktop";
88
+ const base = DEVICE_PRESETS[preset];
89
+ const touchMode = touchOverride !== undefined
90
+ ? !!touchOverride
91
+ : d.touchMode !== undefined
92
+ ? !!d.touchMode
93
+ : !!base.touchMode;
94
+ const viewport = d.viewport
95
+ ? {
96
+ width: Math.max(200, Math.floor(d.viewport.width)),
97
+ height: Math.max(200, Math.floor(d.viewport.height)),
98
+ ...(d.viewport.deviceScaleFactor !== undefined
99
+ ? { deviceScaleFactor: d.viewport.deviceScaleFactor }
100
+ : base.viewport.deviceScaleFactor !== undefined
101
+ ? { deviceScaleFactor: base.viewport.deviceScaleFactor }
102
+ : {}),
103
+ }
104
+ : { ...base.viewport };
105
+ return {
106
+ preset,
107
+ viewport,
108
+ isMobile: d.isMobile !== undefined ? !!d.isMobile : base.isMobile,
109
+ hasTouch: d.hasTouch !== undefined ? !!d.hasTouch : base.hasTouch || touchMode,
110
+ touchMode,
111
+ };
112
+ }
113
+ /**
114
+ * Carry-forward for device fixtures (mirror {@link applyTodoPhase}).
115
+ * - clear: hideDevice / clearDevice / device("desktop") with clear intent
116
+ * - set: author set device/viewport/touch this phase
117
+ * - keep: omit - preserve previous (whole episode remain)
118
+ */
119
+ export function applyDevicePhase(prev, phase) {
120
+ const sync = phase.deviceSync ||
121
+ (phase.device ? "set" : "keep");
122
+ if (sync === "clear") {
123
+ return { device: resolveDeviceState("desktop", false), sync: "clear" };
124
+ }
125
+ if (sync === "set") {
126
+ if (phase.device)
127
+ return { device: phase.device, sync: "set" };
128
+ return { device: resolveDeviceState("desktop", false), sync: "clear" };
129
+ }
130
+ return { device: prev, sync: "keep" };
131
+ }
15
132
  /** Apply Flow defaults under per-slot authored values (slot wins when set). */
16
133
  export function applyHighlightStyleDefaults(slot, defaults) {
17
134
  const tone = slot.tone !== undefined && slot.tone !== ""
@@ -378,11 +495,20 @@ export function normalizeTodoStyle(raw) {
378
495
  return "sequential";
379
496
  if (s === "checklist" || s === "check" || s === "checks" || s === "box")
380
497
  return "checklist";
498
+ if (s === "bullets" ||
499
+ s === "bullet" ||
500
+ s === "list" ||
501
+ s === "ul" ||
502
+ s === "points" ||
503
+ s === "plain") {
504
+ return "bullets";
505
+ }
381
506
  return undefined;
382
507
  }
383
508
  /**
384
509
  * Normalize checklist rows; apply todoIndex for sequential done/current.
385
- * In checklist mode, todoIndex is ignored unless items omit explicit done.
510
+ * In checklist / bullets mode, todoIndex is ignored unless items omit explicit done
511
+ * (bullets never auto-mark current).
386
512
  */
387
513
  export function normalizeTodos(todos, todoIndex, style = "sequential") {
388
514
  if (!todos || todos.length === 0)
@@ -406,6 +532,11 @@ export function normalizeTodos(todos, todoIndex, style = "sequential") {
406
532
  item.current = false;
407
533
  return item;
408
534
  }
535
+ if (style === "bullets") {
536
+ // Flat list: no walkthrough current; keep explicit done if authored.
537
+ item.current = false;
538
+ return item;
539
+ }
409
540
  if (idx !== undefined) {
410
541
  if (item.done === undefined)
411
542
  item.done = i < idx;
@@ -489,7 +620,7 @@ export function applyTodoPhase(prev, phase) {
489
620
  return { dock: prev, sync: "keep" };
490
621
  }
491
622
  /**
492
- * Advance every sequential group to `index` (capped). Checklist groups unchanged.
623
+ * Advance every sequential group to `index` (capped). Checklist / bullets unchanged.
493
624
  * Used while cycling rings / fill/click stubs so the walkthrough moves.
494
625
  */
495
626
  export function advanceTodoDock(dock, index) {
@@ -519,7 +650,7 @@ export function advanceTodoDock(dock, index) {
519
650
  }
520
651
  /**
521
652
  * Mark sequential groups complete (all done, none current) - e.g. end of step.
522
- * Checklist groups keep explicit done flags.
653
+ * Checklist / bullets groups keep explicit done flags.
523
654
  */
524
655
  export function completeSequentialTodoDock(dock) {
525
656
  if (!dock)
@@ -600,6 +731,57 @@ function createStubCtx(bag, opts) {
600
731
  bag.todos = [];
601
732
  delete bag.todoGroups;
602
733
  },
734
+ device(preset) {
735
+ bag.deviceTouched = true;
736
+ bag.hideDevice = false;
737
+ const resolved = resolveDeviceState(preset, bag.touchOverride);
738
+ if (resolved) {
739
+ bag.device = resolved;
740
+ if (resolved.preset === "desktop" && !resolved.touchMode) {
741
+ bag.hideDevice = true;
742
+ }
743
+ }
744
+ },
745
+ viewport(box) {
746
+ bag.deviceTouched = true;
747
+ bag.hideDevice = false;
748
+ const resolved = resolveDeviceState(box, bag.touchOverride);
749
+ if (resolved)
750
+ bag.device = resolved;
751
+ },
752
+ touch(on) {
753
+ bag.deviceTouched = true;
754
+ const enabled = on === undefined ? true : !!on;
755
+ bag.touchOverride = enabled;
756
+ if (enabled)
757
+ bag.hideDevice = false;
758
+ if (bag.device) {
759
+ bag.device = {
760
+ ...bag.device,
761
+ touchMode: enabled,
762
+ hasTouch: bag.device.hasTouch || enabled,
763
+ };
764
+ if (enabled)
765
+ bag.hideDevice = false;
766
+ }
767
+ else if (enabled) {
768
+ const base = resolveDeviceState("desktop", true);
769
+ if (base)
770
+ bag.device = { ...base, touchMode: true, hasTouch: true };
771
+ }
772
+ },
773
+ hideDevice() {
774
+ bag.deviceTouched = true;
775
+ bag.hideDevice = true;
776
+ bag.device = resolveDeviceState("desktop", false);
777
+ bag.touchOverride = false;
778
+ },
779
+ clearDevice() {
780
+ bag.deviceTouched = true;
781
+ bag.hideDevice = true;
782
+ bag.device = resolveDeviceState("desktop", false);
783
+ bag.touchOverride = false;
784
+ },
603
785
  zoom(n) {
604
786
  if (Number.isFinite(n) && n > 0)
605
787
  bag.zoom = n;
@@ -648,6 +830,27 @@ function createStubCtx(bag, opts) {
648
830
  bag.hideTodos = !partial.todoGroups || !partial.todoGroups.length;
649
831
  bag.todoGroups = partial.todoGroups;
650
832
  }
833
+ if (partial.touch !== undefined) {
834
+ bag.deviceTouched = true;
835
+ bag.touchOverride = !!partial.touch;
836
+ }
837
+ if (partial.device !== undefined) {
838
+ bag.deviceTouched = true;
839
+ bag.hideDevice = false;
840
+ const resolved = resolveDeviceState(partial.device, bag.touchOverride);
841
+ if (resolved) {
842
+ bag.device = resolved;
843
+ if (resolved.preset === "desktop" && !resolved.touchMode)
844
+ bag.hideDevice = true;
845
+ }
846
+ }
847
+ if (partial.viewport !== undefined) {
848
+ bag.deviceTouched = true;
849
+ bag.hideDevice = false;
850
+ const resolved = resolveDeviceState(partial.viewport, bag.touchOverride);
851
+ if (resolved)
852
+ bag.device = resolved;
853
+ }
651
854
  if (partial.zoom !== undefined && Number.isFinite(partial.zoom) && partial.zoom > 0) {
652
855
  bag.zoom = partial.zoom;
653
856
  }
@@ -812,12 +1015,37 @@ export async function runStubPhase(block, phase, opts) {
812
1015
  : bag.todoTouched && !todoDock
813
1016
  ? "clear"
814
1017
  : "keep";
1018
+ let deviceState;
1019
+ if (bag.hideDevice) {
1020
+ deviceState = resolveDeviceState("desktop", false);
1021
+ }
1022
+ else if (bag.device) {
1023
+ deviceState = bag.touchOverride !== undefined
1024
+ ? {
1025
+ ...bag.device,
1026
+ touchMode: bag.touchOverride,
1027
+ hasTouch: bag.device.hasTouch || bag.touchOverride,
1028
+ }
1029
+ : bag.device;
1030
+ }
1031
+ else if (bag.touchOverride !== undefined) {
1032
+ deviceState = resolveDeviceState("desktop", bag.touchOverride);
1033
+ }
1034
+ const deviceSync = bag.hideDevice
1035
+ ? "clear"
1036
+ : bag.deviceTouched && deviceState
1037
+ ? "set"
1038
+ : bag.deviceTouched && !deviceState
1039
+ ? "clear"
1040
+ : "keep";
815
1041
  return {
816
1042
  highlights,
817
1043
  todos,
818
1044
  todoSync,
1045
+ deviceSync,
819
1046
  ...(bag.todoIndex !== undefined ? { todoIndex: bag.todoIndex } : {}),
820
1047
  ...(todoDock ? { todoDock } : {}),
1048
+ ...(deviceState ? { device: deviceState } : {}),
821
1049
  ...(bag.zoom !== undefined ? { zoom: bag.zoom } : {}),
822
1050
  ...(bag.zoomOut !== undefined ? { zoomOut: bag.zoomOut } : {}),
823
1051
  ...(bag.title !== undefined && bag.title !== "" ? { title: bag.title } : {}),
package/dist/index.d.ts CHANGED
@@ -3,8 +3,8 @@ export { connect, checkpoint } from "./types.js";
3
3
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
4
4
  export { Trait, urlMatches, textEquals, visible } from "./trait.js";
5
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, withHighlightStyle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
6
- export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, 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, WaygraphTodoItem, WaygraphTodoInput, WaygraphTodoGroup, WaygraphTodoGroupInput, TodoDockState, TodoListStyle, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, StubCtx, StubLifecycleFn, StubPhaseResult, StubPhaseFixtures, } from "./highlights.js";
6
+ export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, DEVICE_PRESETS, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
7
+ export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, WaygraphTodoItem, WaygraphTodoInput, WaygraphTodoGroup, WaygraphTodoGroupInput, TodoDockState, TodoListStyle, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, StubCtx, StubLifecycleFn, StubPhaseResult, StubPhaseFixtures, DevicePreset, DeviceViewport, DeviceState, } 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
@@ -2,7 +2,7 @@ export { connect, checkpoint } from "./types.js";
2
2
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
3
3
  export { Trait, urlMatches, textEquals, visible } from "./trait.js";
4
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, withHighlightStyle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
5
- export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, 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";
5
+ export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, DEVICE_PRESETS, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } 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";
@@ -37,6 +37,8 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
37
37
  // Demo: zoom product -> add (stay zoomed), then badge after act.
38
38
  stubBefore: (ctx: StubCtx) => {
39
39
  ctx.title("Pick a product");
40
+ // Back to desktop after mobile login (explicit clear - not omit/remain).
41
+ ctx.clearDevice();
40
42
  ctx.todoId("saucedemo-cart");
41
43
  ctx.todos([
42
44
  { id: "cart-find", text: "Find the product" },
@@ -51,11 +51,13 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
51
51
  // sequential highlight queue (appear / dwell / fade), focus + color.
52
52
  stubBefore: (ctx: StubCtx) => {
53
53
  ctx.title("Signing in");
54
+ // 0.13+: mobile viewport + touch theater; omit on later blocks = remain.
55
+ ctx.device("mobile");
54
56
  ctx.todoId("saucedemo-login");
55
57
  ctx.todos([
56
58
  { id: "login-user", text: "Enter username" },
57
59
  { id: "login-pass", text: "Enter password" },
58
- { id: "login-submit", text: "Click Login" },
60
+ { id: "login-submit", text: "Tap Login" },
59
61
  ]);
60
62
  ctx.todoIndex(0);
61
63
  ctx.zoom(1.35);
@@ -81,6 +83,7 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
81
83
  color: "#86efac",
82
84
  zoom: 1.55,
83
85
  weight: "bold",
86
+ gesture: "tap",
84
87
  },
85
88
  });
86
89
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.48",
3
+ "version": "0.13.1",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -36,6 +36,8 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
36
36
  // Demo: zoom product -> add (stay zoomed), then badge after act.
37
37
  stubBefore: (ctx) => {
38
38
  ctx.title("Pick a product");
39
+ // Back to desktop after mobile login (explicit clear - not omit/remain).
40
+ ctx.clearDevice();
39
41
  ctx.todoId("saucedemo-cart");
40
42
  ctx.todos([
41
43
  { id: "cart-find", text: "Find the product" },
@@ -51,11 +51,13 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
51
51
  // sequential highlight queue (appear / dwell / fade), focus + color.
52
52
  stubBefore: (ctx: StubCtx) => {
53
53
  ctx.title("Signing in");
54
+ // 0.13+: mobile viewport + touch theater; omit on later blocks = remain.
55
+ ctx.device("mobile");
54
56
  ctx.todoId("saucedemo-login");
55
57
  ctx.todos([
56
58
  { id: "login-user", text: "Enter username" },
57
59
  { id: "login-pass", text: "Enter password" },
58
- { id: "login-submit", text: "Click Login" },
60
+ { id: "login-submit", text: "Tap Login" },
59
61
  ]);
60
62
  ctx.todoIndex(0);
61
63
  ctx.zoom(1.35);
@@ -81,6 +83,7 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
81
83
  color: "#86efac",
82
84
  zoom: 1.55,
83
85
  weight: "bold",
86
+ gesture: "tap",
84
87
  },
85
88
  });
86
89
  },