waygraph 0.13.1 → 0.13.2

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
@@ -532,17 +532,43 @@ const RING_CSS =
532
532
  "#wg-todo-dock[data-pos=right]{transform:translateX(calc(100vw - 100% - 28px));}" +
533
533
  "#wg-todo-dock[data-pos=left]{transform:translateX(0);}" +
534
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.
535
+ // Device toast + chip (0.13.2+) - icon toast on fixture change, then compact chip.
536
+ "#wg-device-toast{position:fixed;z-index:2147483647;top:14px;right:14px;" +
537
+ "display:flex;align-items:center;gap:10px;min-width:200px;max-width:min(92vw,320px);" +
538
+ "padding:12px 14px;border-radius:14px;background:rgba(20,10,40,.96);color:#f0e8ff;" +
539
+ "border:1px solid rgba(124,58,237,.55);box-shadow:0 10px 28px rgba(0,0,0,.4);" +
540
+ "font:600 13px/1.35 system-ui,sans-serif;pointer-events:none;" +
541
+ "opacity:0;transform:translateX(18px) scale(.96);" +
542
+ "transition:opacity .35s ease,transform .45s cubic-bezier(.22,1,.36,1);}" +
543
+ "#wg-device-toast.wg-in{opacity:1;transform:translateX(0) scale(1);}" +
544
+ "#wg-device-toast.wg-out{opacity:0;transform:translateX(12px) scale(.98);}" +
545
+ "#wg-device-toast[data-preset=mobile]{border-color:#3B82F6;}" +
546
+ "#wg-device-toast[data-preset=tablet]{border-color:#22C55E;}" +
547
+ "#wg-device-toast[data-preset=desktop]{border-color:#9CA3AF;}" +
548
+ "#wg-device-toast .wg-dev-icon{flex:0 0 auto;width:36px;height:36px;border-radius:10px;" +
549
+ "display:flex;align-items:center;justify-content:center;background:rgba(124,58,237,.28);}" +
550
+ "#wg-device-toast[data-preset=mobile] .wg-dev-icon{background:rgba(59,130,246,.28);}" +
551
+ "#wg-device-toast[data-preset=tablet] .wg-dev-icon{background:rgba(34,197,94,.28);}" +
552
+ "#wg-device-toast[data-preset=desktop] .wg-dev-icon{background:rgba(156,163,175,.28);}" +
553
+ "#wg-device-toast .wg-dev-icon svg{width:22px;height:22px;display:block;}" +
554
+ "#wg-device-toast .wg-dev-copy{flex:1 1 auto;min-width:0;}" +
555
+ "#wg-device-toast .wg-dev-title{font:800 13px/1.2 system-ui,sans-serif;color:#fff;}" +
556
+ "#wg-device-toast .wg-dev-sub{margin-top:3px;font:600 11px/1.3 system-ui,sans-serif;" +
557
+ "color:#c9a6ff;letter-spacing:.02em;}" +
536
558
  "#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;" +
559
+ "display:inline-flex;align-items:center;gap:6px;padding:6px 10px 6px 8px;" +
560
+ "border-radius:999px;background:rgba(20,10,40,.92);color:#f0e8ff;" +
538
561
  "border:1px solid rgba(124,58,237,.5);font:700 11px/1.2 system-ui,sans-serif;" +
539
562
  "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;}" +
563
+ "box-shadow:0 4px 14px rgba(0,0,0,.3);opacity:0;transform:translateY(-4px);" +
564
+ "transition:opacity .3s ease,transform .35s cubic-bezier(.22,1,.36,1);}" +
565
+ "#wg-device-badge.wg-in{opacity:1;transform:translateY(0);}" +
542
566
  "#wg-device-badge[data-preset=mobile]{border-color:#3B82F6;}" +
543
567
  "#wg-device-badge[data-preset=tablet]{border-color:#22C55E;}" +
544
568
  "#wg-device-badge[data-preset=desktop]{border-color:#9CA3AF;}" +
545
- "#wg-device-badge[data-touch=1]::after{content:' · touch';opacity:.85;}" +
569
+ "#wg-device-badge .wg-dev-icon{width:16px;height:16px;display:flex;align-items:center;justify-content:center;}" +
570
+ "#wg-device-badge .wg-dev-icon svg{width:14px;height:14px;display:block;}" +
571
+ "#wg-device-badge .wg-dev-label{white-space:nowrap;}" +
546
572
  "#wg-cursor[data-touch=1]{width:28px;height:28px;}" +
547
573
  "#wg-todos{list-style:none;margin:0 0 12px;padding:8px 10px;background:#0f0620;border-radius:8px;" +
548
574
  "border:1px solid #3a2a60;}" +
@@ -1287,15 +1313,14 @@ async function installOverlay(page, title) {
1287
1313
  window.__wgStampModal(dock, "todos", { ready: true });
1288
1314
  }
1289
1315
  };
1290
- // Device / touch badge (0.13+) - mirror todo sync keep/set/clear.
1291
- // Payload: { sync, device?, remain? }
1316
+ // Device toast + chip (0.13.2+) - announce on set/clear; quiet chip on keep.
1317
+ // Payload: { sync, device?, remain?, announce? }
1292
1318
  window.__wgSyncDevice = (payload) => {
1293
1319
  const sync =
1294
1320
  payload && (payload.sync === "clear" || payload.sync === "keep" || payload.sync === "set")
1295
1321
  ? payload.sync
1296
1322
  : "keep";
1297
1323
  const device = payload && payload.device ? payload.device : null;
1298
- let badge = document.getElementById("wg-device-badge");
1299
1324
  const cursor = document.getElementById("wg-cursor");
1300
1325
  const MOUSE_SVG =
1301
1326
  "<svg viewBox='0 0 32 32' width='24' height='24'>" +
@@ -1306,43 +1331,158 @@ async function installOverlay(page, title) {
1306
1331
  "<ellipse cx='16' cy='22' rx='7' ry='8' fill='#0C0C1A' stroke='#fff' stroke-width='1.4'/>" +
1307
1332
  "<rect x='12' y='6' width='8' height='16' rx='4' fill='#0C0C1A' stroke='#fff' stroke-width='1.4'/>" +
1308
1333
  "</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) {
1334
+ const ICONS = {
1335
+ mobile:
1336
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
1337
+ '<rect x="7" y="2.5" width="10" height="19" rx="2.2"/>' +
1338
+ '<circle cx="12" cy="18.2" r="1.1" fill="#fff" stroke="none"/></svg>',
1339
+ tablet:
1340
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
1341
+ '<rect x="3.5" y="4" width="17" height="16" rx="2"/>' +
1342
+ '<circle cx="12" cy="17.2" r="1" fill="#fff" stroke="none"/></svg>',
1343
+ desktop:
1344
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
1345
+ '<rect x="2.5" y="3.5" width="19" height="12.5" rx="1.5"/>' +
1346
+ '<path d="M8 20h8M12 16v4"/></svg>',
1347
+ };
1348
+ const setCursor = (touch) => {
1349
+ if (!cursor) return;
1350
+ if (touch) {
1351
+ cursor.setAttribute("data-touch", "1");
1352
+ cursor.innerHTML = FINGER_SVG;
1353
+ } else {
1313
1354
  cursor.removeAttribute("data-touch");
1314
1355
  cursor.innerHTML = MOUSE_SVG;
1315
1356
  }
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;
1357
+ };
1358
+ const ensureChip = (preset, touch, remain) => {
1359
+ let badge = document.getElementById("wg-device-badge");
1360
+ if (preset === "desktop" && !touch) {
1361
+ if (badge) {
1362
+ badge.classList.remove("wg-in");
1363
+ setTimeout(() => {
1364
+ const b = document.getElementById("wg-device-badge");
1365
+ if (b) b.remove();
1366
+ }, 320);
1367
+ }
1368
+ return;
1369
+ }
1370
+ if (!badge) {
1371
+ badge = document.createElement("div");
1372
+ badge.id = "wg-device-badge";
1373
+ badge.setAttribute("data-wg-ui", "1");
1374
+ document.documentElement.appendChild(badge);
1375
+ }
1376
+ badge.dataset.preset = preset;
1377
+ badge.dataset.touch = touch ? "1" : "0";
1378
+ const chipLabel =
1379
+ preset === "desktop" && !touch
1380
+ ? "desktop"
1381
+ : preset + (remain ? " · remain" : "");
1382
+ badge.innerHTML =
1383
+ '<span class="wg-dev-icon">' +
1384
+ (ICONS[preset] || ICONS.desktop) +
1385
+ '</span><span class="wg-dev-label">' +
1386
+ chipLabel +
1387
+ "</span>";
1388
+ void badge.offsetWidth;
1389
+ badge.classList.add("wg-in");
1390
+ };
1391
+ const showToast = (preset, touch, title, sub) => {
1392
+ let toast = document.getElementById("wg-device-toast");
1393
+ if (toast && toast._wgTimer) {
1394
+ clearTimeout(toast._wgTimer);
1395
+ toast._wgTimer = null;
1396
+ }
1397
+ if (!toast) {
1398
+ toast = document.createElement("div");
1399
+ toast.id = "wg-device-toast";
1400
+ toast.setAttribute("data-wg-ui", "1");
1401
+ document.documentElement.appendChild(toast);
1402
+ }
1403
+ // Hide chip while toast is up (same corner).
1404
+ const badge = document.getElementById("wg-device-badge");
1405
+ if (badge) badge.classList.remove("wg-in");
1406
+ toast.dataset.preset = preset;
1407
+ toast.innerHTML =
1408
+ '<span class="wg-dev-icon">' +
1409
+ (ICONS[preset] || ICONS.desktop) +
1410
+ '</span><span class="wg-dev-copy"><div class="wg-dev-title">' +
1411
+ title +
1412
+ '</div><div class="wg-dev-sub">' +
1413
+ sub +
1414
+ "</div></span>";
1415
+ toast.classList.remove("wg-out");
1416
+ void toast.offsetWidth;
1417
+ toast.classList.add("wg-in");
1418
+ toast._wgTimer = setTimeout(() => {
1419
+ toast.classList.add("wg-out");
1420
+ toast.classList.remove("wg-in");
1421
+ setTimeout(() => {
1422
+ const t = document.getElementById("wg-device-toast");
1423
+ if (t) t.remove();
1424
+ if (preset !== "desktop" || touch) {
1425
+ ensureChip(preset, touch, true);
1426
+ }
1427
+ }, 380);
1428
+ }, 2200);
1429
+ };
1430
+
1431
+ if (sync === "keep" && !device) return;
1432
+
1433
+ const isDesktopClear =
1434
+ sync === "clear" ||
1435
+ (device && device.preset === "desktop" && !device.touchMode);
1436
+ if (isDesktopClear) {
1437
+ setCursor(false);
1438
+ const announce = payload.announce !== false && sync !== "keep";
1439
+ if (announce) {
1440
+ const vp = device && device.viewport ? device.viewport : { width: 1280, height: 720 };
1441
+ showToast(
1442
+ "desktop",
1443
+ false,
1444
+ "Back to desktop",
1445
+ vp.width + "\u00d7" + vp.height + " \u00b7 mouse",
1446
+ );
1447
+ setTimeout(() => {
1448
+ const badge = document.getElementById("wg-device-badge");
1449
+ if (badge) badge.remove();
1450
+ }, 2600);
1451
+ } else {
1452
+ const badge = document.getElementById("wg-device-badge");
1453
+ if (badge) badge.remove();
1454
+ const toast = document.getElementById("wg-device-toast");
1455
+ if (toast) toast.remove();
1456
+ }
1457
+ window.__wgDevicePreset = "desktop";
1458
+ return;
1319
1459
  }
1460
+
1320
1461
  if (!device) return;
1321
1462
  const preset = device.preset || "desktop";
1322
1463
  const touch = !!device.touchMode;
1323
1464
  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
- }
1465
+ const prev = window.__wgDevicePreset || "";
1466
+ const changed = prev !== preset || (touch && prev === "desktop");
1467
+ setCursor(touch);
1468
+ window.__wgDevicePreset = preset;
1469
+
1470
+ const announce =
1471
+ payload.announce === true ||
1472
+ (payload.announce !== false && (sync === "set" || sync === "clear") && changed);
1473
+
1474
+ const vp = device.viewport || {};
1475
+ const size =
1476
+ (vp.width || "?") + "\u00d7" + (vp.height || "?") + (touch ? " \u00b7 touch" : "");
1477
+ const titles = {
1478
+ mobile: "Now in mobile mode",
1479
+ tablet: "Now in tablet mode",
1480
+ desktop: "Now in desktop mode",
1481
+ };
1482
+ if (announce) {
1483
+ showToast(preset, touch, titles[preset] || "Device updated", size);
1484
+ } else {
1485
+ ensureChip(preset, touch, remain);
1346
1486
  }
1347
1487
  };
1348
1488
  },
@@ -2776,8 +2916,7 @@ async function clickPulseAt(page, x, y, tone) {
2776
2916
 
2777
2917
  /**
2778
2918
  * 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.
2919
+ * Seamless viewport lerp + toast on set/clear (quiet chip recreate on keep).
2781
2920
  */
2782
2921
  async function applyDeviceToPage(page, device, sync) {
2783
2922
  const mode = sync || (device ? "set" : "keep");
@@ -2795,14 +2934,46 @@ async function applyDeviceToPage(page, device, sync) {
2795
2934
  typeof page.__wgRealSetViewportSize === "function"
2796
2935
  ? page.__wgRealSetViewportSize
2797
2936
  : page.setViewportSize.bind(page);
2937
+ const target = {
2938
+ width: Math.max(200, Math.floor(d.viewport.width)),
2939
+ height: Math.max(200, Math.floor(d.viewport.height)),
2940
+ };
2941
+ let from = null;
2798
2942
  try {
2799
- await realSet({
2800
- width: d.viewport.width,
2801
- height: d.viewport.height,
2802
- });
2943
+ from = page.viewportSize();
2803
2944
  } catch {
2804
- /* maximized / null viewport hosts may reject - badge still updates */
2945
+ from = null;
2805
2946
  }
2947
+ const announce = mode === "set" || mode === "clear";
2948
+ const shouldAnimate =
2949
+ announce &&
2950
+ from &&
2951
+ from.width > 0 &&
2952
+ from.height > 0 &&
2953
+ (from.width !== target.width || from.height !== target.height);
2954
+
2955
+ try {
2956
+ if (shouldAnimate) {
2957
+ const steps = 14;
2958
+ const ms = Number(process.env.WAYGRAPH_DEVICE_TRANSITION_MS || 520);
2959
+ const stepMs = Math.max(12, Math.floor(ms / steps));
2960
+ for (let i = 1; i <= steps; i++) {
2961
+ const t = i / steps;
2962
+ const e = t * t * (3 - 2 * t); // smoothstep
2963
+ await realSet({
2964
+ width: Math.round(from.width + (target.width - from.width) * e),
2965
+ height: Math.round(from.height + (target.height - from.height) * e),
2966
+ });
2967
+ if (i < steps) await new Promise((r) => setTimeout(r, stepMs));
2968
+ }
2969
+ } else {
2970
+ await realSet(target);
2971
+ }
2972
+ } catch {
2973
+ /* maximized / null viewport hosts may reject - toast still updates */
2974
+ }
2975
+
2976
+ // Fire toast near the end of the lerp so it reads as one seamless beat.
2806
2977
  await page
2807
2978
  .evaluate(
2808
2979
  (payload) => {
@@ -2812,6 +2983,7 @@ async function applyDeviceToPage(page, device, sync) {
2812
2983
  sync: mode,
2813
2984
  device: d,
2814
2985
  remain: mode === "keep" || mode === "set",
2986
+ announce,
2815
2987
  },
2816
2988
  )
2817
2989
  .catch(() => {});
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Headed visual demo: mobile -> tablet -> desktop with toast + seamless lerp.
3
+ * Usage (from examples/saucedemo): WG_DEVICE_HOLD_MS=5000 node scripts/demo-device-presets.mjs
4
+ */
5
+ import { chromium } from "playwright";
6
+ import { mkdirSync } from "node:fs";
7
+ import { join, dirname } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const outDir = join(__dirname, "../.tmp/device-demo");
12
+ mkdirSync(outDir, { recursive: true });
13
+
14
+ const PRESETS = {
15
+ mobile: {
16
+ preset: "mobile",
17
+ viewport: { width: 390, height: 844 },
18
+ touchMode: true,
19
+ title: "Now in mobile mode",
20
+ },
21
+ tablet: {
22
+ preset: "tablet",
23
+ viewport: { width: 768, height: 1024 },
24
+ touchMode: true,
25
+ title: "Now in tablet mode",
26
+ },
27
+ desktop: {
28
+ preset: "desktop",
29
+ viewport: { width: 1280, height: 720 },
30
+ touchMode: false,
31
+ title: "Back to desktop",
32
+ },
33
+ };
34
+
35
+ const CSS = `
36
+ #wg-device-toast{position:fixed;z-index:2147483647;top:14px;right:14px;
37
+ display:flex;align-items:center;gap:10px;min-width:200px;max-width:min(92vw,320px);
38
+ padding:12px 14px;border-radius:14px;background:rgba(20,10,40,.96);color:#f0e8ff;
39
+ border:1px solid rgba(124,58,237,.55);box-shadow:0 10px 28px rgba(0,0,0,.4);
40
+ font:600 13px/1.35 system-ui,sans-serif;pointer-events:none;
41
+ opacity:0;transform:translateX(18px) scale(.96);
42
+ transition:opacity .35s ease,transform .45s cubic-bezier(.22,1,.36,1);}
43
+ #wg-device-toast.wg-in{opacity:1;transform:translateX(0) scale(1);}
44
+ #wg-device-toast.wg-out{opacity:0;transform:translateX(12px) scale(.98);}
45
+ #wg-device-toast[data-preset=mobile]{border-color:#3B82F6;}
46
+ #wg-device-toast[data-preset=tablet]{border-color:#22C55E;}
47
+ #wg-device-toast[data-preset=desktop]{border-color:#9CA3AF;}
48
+ #wg-device-toast .wg-dev-icon{flex:0 0 auto;width:36px;height:36px;border-radius:10px;
49
+ display:flex;align-items:center;justify-content:center;background:rgba(124,58,237,.28);}
50
+ #wg-device-toast[data-preset=mobile] .wg-dev-icon{background:rgba(59,130,246,.28);}
51
+ #wg-device-toast[data-preset=tablet] .wg-dev-icon{background:rgba(34,197,94,.28);}
52
+ #wg-device-toast[data-preset=desktop] .wg-dev-icon{background:rgba(156,163,175,.28);}
53
+ #wg-device-toast .wg-dev-icon svg{width:22px;height:22px;display:block;}
54
+ #wg-device-toast .wg-dev-title{font:800 13px/1.2 system-ui,sans-serif;color:#fff;}
55
+ #wg-device-toast .wg-dev-sub{margin-top:3px;font:600 11px/1.3 system-ui,sans-serif;color:#c9a6ff;}
56
+ #wg-device-badge{position:fixed;z-index:2147483646;top:14px;right:14px;
57
+ display:inline-flex;align-items:center;gap:6px;padding:6px 10px 6px 8px;
58
+ border-radius:999px;background:rgba(20,10,40,.92);color:#f0e8ff;
59
+ border:1px solid rgba(124,58,237,.5);font:700 11px/1.2 system-ui,sans-serif;
60
+ letter-spacing:.04em;text-transform:uppercase;pointer-events:none;
61
+ box-shadow:0 4px 14px rgba(0,0,0,.3);opacity:0;transition:opacity .3s ease;}
62
+ #wg-device-badge.wg-in{opacity:1;}
63
+ #wg-device-badge[data-preset=mobile]{border-color:#3B82F6;}
64
+ #wg-device-badge[data-preset=tablet]{border-color:#22C55E;}
65
+ #wg-device-badge .wg-dev-icon svg{width:14px;height:14px;display:block;}
66
+ `;
67
+
68
+ const ICONS = {
69
+ mobile:
70
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="2.5" width="10" height="19" rx="2.2"/><circle cx="12" cy="18.2" r="1.1" fill="#fff" stroke="none"/></svg>',
71
+ tablet:
72
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4" width="17" height="16" rx="2"/><circle cx="12" cy="17.2" r="1" fill="#fff" stroke="none"/></svg>',
73
+ desktop:
74
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="3.5" width="19" height="12.5" rx="1.5"/><path d="M8 20h8M12 16v4"/></svg>',
75
+ };
76
+
77
+ async function lerpViewport(page, to, ms = 520) {
78
+ const from = page.viewportSize() || { width: 1280, height: 720 };
79
+ const steps = 14;
80
+ const stepMs = Math.max(12, Math.floor(ms / steps));
81
+ for (let i = 1; i <= steps; i++) {
82
+ const t = i / steps;
83
+ const e = t * t * (3 - 2 * t);
84
+ await page.setViewportSize({
85
+ width: Math.round(from.width + (to.width - from.width) * e),
86
+ height: Math.round(from.height + (to.height - from.height) * e),
87
+ });
88
+ if (i < steps) await page.waitForTimeout(stepMs);
89
+ }
90
+ }
91
+
92
+ async function applyPreset(page, key) {
93
+ const d = PRESETS[key];
94
+ await lerpViewport(page, d.viewport, 520);
95
+ await page.evaluate(
96
+ ({ d, css, icons, key }) => {
97
+ let style = document.getElementById("wg-device-demo-style");
98
+ if (!style) {
99
+ style = document.createElement("style");
100
+ style.id = "wg-device-demo-style";
101
+ style.textContent = css;
102
+ document.documentElement.appendChild(style);
103
+ }
104
+ let toast = document.getElementById("wg-device-toast");
105
+ if (toast && toast._wgTimer) clearTimeout(toast._wgTimer);
106
+ if (!toast) {
107
+ toast = document.createElement("div");
108
+ toast.id = "wg-device-toast";
109
+ document.documentElement.appendChild(toast);
110
+ }
111
+ const badge = document.getElementById("wg-device-badge");
112
+ if (badge) badge.classList.remove("wg-in");
113
+ toast.dataset.preset = d.preset;
114
+ const sub =
115
+ d.viewport.width +
116
+ "\u00d7" +
117
+ d.viewport.height +
118
+ (d.touchMode ? " \u00b7 touch" : " \u00b7 mouse");
119
+ toast.innerHTML =
120
+ '<span class="wg-dev-icon">' +
121
+ icons[d.preset] +
122
+ '</span><span class="wg-dev-copy"><div class="wg-dev-title">' +
123
+ d.title +
124
+ '</div><div class="wg-dev-sub">' +
125
+ sub +
126
+ "</div></span>";
127
+ toast.classList.remove("wg-out");
128
+ void toast.offsetWidth;
129
+ toast.classList.add("wg-in");
130
+ toast._wgTimer = setTimeout(() => {
131
+ toast.classList.add("wg-out");
132
+ toast.classList.remove("wg-in");
133
+ setTimeout(() => {
134
+ if (d.preset === "desktop" && !d.touchMode) {
135
+ const b = document.getElementById("wg-device-badge");
136
+ if (b) b.remove();
137
+ return;
138
+ }
139
+ let chip = document.getElementById("wg-device-badge");
140
+ if (!chip) {
141
+ chip = document.createElement("div");
142
+ chip.id = "wg-device-badge";
143
+ document.documentElement.appendChild(chip);
144
+ }
145
+ chip.dataset.preset = d.preset;
146
+ chip.innerHTML =
147
+ '<span class="wg-dev-icon">' +
148
+ icons[d.preset] +
149
+ '</span><span>' +
150
+ d.preset +
151
+ " \u00b7 remain</span>";
152
+ chip.classList.add("wg-in");
153
+ }, 380);
154
+ }, 2200);
155
+ },
156
+ { d, css: CSS, icons: ICONS, key },
157
+ );
158
+ }
159
+
160
+ const holdMs = Number(process.env.WG_DEVICE_HOLD_MS || 4500);
161
+ const executablePath = process.env.CHROME_PATH || process.env.CHROMIUM_PATH || undefined;
162
+
163
+ const browser = await chromium.launch({
164
+ headless: false,
165
+ ...(executablePath ? { executablePath } : {}),
166
+ });
167
+ const context = await browser.newContext({
168
+ hasTouch: true,
169
+ viewport: { width: 1280, height: 720 },
170
+ });
171
+ const page = await context.newPage();
172
+ await page.goto("https://www.saucedemo.com/", { waitUntil: "domcontentloaded" });
173
+
174
+ const shots = [];
175
+ for (const key of ["mobile", "tablet", "desktop"]) {
176
+ console.log("device demo:", key);
177
+ await applyPreset(page, key);
178
+ await page.waitForTimeout(900);
179
+ const shot = join(outDir, `device-${key}.png`);
180
+ await page.screenshot({ path: shot, fullPage: false });
181
+ shots.push(shot);
182
+ console.log(" shot:", shot);
183
+ await page.waitForTimeout(holdMs);
184
+ }
185
+
186
+ console.log("DEVICE_DEMO_DONE", JSON.stringify(shots));
187
+ await page.waitForTimeout(1500);
188
+ await browser.close();
@@ -1,4 +1,5 @@
1
1
  import { definePageBlock, Trait } from "waygraph";
2
+ import type { StubCtx } from "waygraph";
2
3
  import type { LoggedIn } from "../../../states/checkout.states.js";
3
4
  import { AddToCartBlock } from "./methods/add-to-cart.effect.block.js";
4
5
  import { RemoveFromCartBlock } from "./methods/remove-from-cart.effect.block.js";
@@ -27,6 +28,11 @@ export const InventoryPageBlock = definePageBlock<LoggedIn>({
27
28
  Trait.url({ pathname: "/inventory.html" }),
28
29
  Trait.visible(InventorySel.list),
29
30
  ],
31
+ // Showcase responsive dashboard: tablet after mobile login (toast + lerp).
32
+ stubBefore: (ctx: StubCtx) => {
33
+ ctx.title("Inventory · tablet");
34
+ ctx.device("tablet");
35
+ },
30
36
  methods: {
31
37
  addToCart: () => AddToCartBlock,
32
38
  removeFromCart: () => RemoveFromCartBlock,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -1,4 +1,5 @@
1
1
  import { definePageBlock, Trait } from "waygraph";
2
+ import type { StubCtx } from "waygraph";
2
3
  import type { LoggedIn } from "../../../states/checkout.states.js";
3
4
  import { AddToCartBlock } from "./methods/add-to-cart.effect.block.js";
4
5
  import { RemoveFromCartBlock } from "./methods/remove-from-cart.effect.block.js";
@@ -27,6 +28,11 @@ export const InventoryPageBlock = definePageBlock<LoggedIn>({
27
28
  Trait.url({ pathname: "/inventory.html" }),
28
29
  Trait.visible(InventorySel.list),
29
30
  ],
31
+ // Showcase responsive dashboard: tablet after mobile login (toast + lerp).
32
+ stubBefore: (ctx: StubCtx) => {
33
+ ctx.title("Inventory · tablet");
34
+ ctx.device("tablet");
35
+ },
30
36
  methods: {
31
37
  addToCart: () => AddToCartBlock,
32
38
  removeFromCart: () => RemoveFromCartBlock,