waygraph 0.13.0 → 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 +243 -50
- package/dist/highlights.d.ts +9 -7
- package/dist/highlights.js +17 -3
- package/examples/saucedemo/.tmp/device-demo/device-desktop.png +0 -0
- package/examples/saucedemo/.tmp/device-demo/device-mobile.png +0 -0
- package/examples/saucedemo/.tmp/device-demo/device-tablet.png +0 -0
- package/examples/saucedemo/scripts/demo-device-presets.mjs +188 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +6 -0
- package/package.json +1 -1
- package/templates/quickstart/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +6 -0
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
|
|
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
|
-
"
|
|
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;
|
|
541
|
-
"
|
|
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
|
|
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;}" +
|
|
@@ -552,6 +578,9 @@ const RING_CSS =
|
|
|
552
578
|
"#wg-todos .wg-todo-done{color:#6ee7b7;text-decoration:line-through;opacity:.85;}" +
|
|
553
579
|
"#wg-todos .wg-todo-current{color:#fff;}" +
|
|
554
580
|
"#wg-todos .wg-todo-pending{color:#9a7ad1;}" +
|
|
581
|
+
"#wg-todos[data-wg-todo-style=bullets] .wg-todo-pending," +
|
|
582
|
+
"#wg-todos[data-wg-todo-style=bullets] .wg-todo-current{color:#e8dcff;}" +
|
|
583
|
+
"#wg-todos[data-wg-todo-style=bullets] .wg-todo-mark{color:#c9a6ff;}" +
|
|
555
584
|
"#wg-progress{height:4px;background:#2a1650;border-radius:2px;margin:0 0 12px;overflow:hidden;}" +
|
|
556
585
|
"#wg-progress-bar{height:100%;background:#7C3AED;border-radius:2px;transition:width .3s ease;}" +
|
|
557
586
|
// A real tab bar for episodes - the currently-active episode reads as
|
|
@@ -1196,21 +1225,39 @@ async function installOverlay(page, title) {
|
|
|
1196
1225
|
return id ? ' data-wg-todo-item="' + esc(id) + '"' : "";
|
|
1197
1226
|
};
|
|
1198
1227
|
const renderItems = (items, style) => {
|
|
1199
|
-
const
|
|
1228
|
+
const rawStyle = String(style || "sequential").toLowerCase();
|
|
1229
|
+
const st =
|
|
1230
|
+
rawStyle === "checklist"
|
|
1231
|
+
? "checklist"
|
|
1232
|
+
: rawStyle === "bullets" ||
|
|
1233
|
+
rawStyle === "bullet" ||
|
|
1234
|
+
rawStyle === "list" ||
|
|
1235
|
+
rawStyle === "ul" ||
|
|
1236
|
+
rawStyle === "points" ||
|
|
1237
|
+
rawStyle === "plain"
|
|
1238
|
+
? "bullets"
|
|
1239
|
+
: "sequential";
|
|
1200
1240
|
return (
|
|
1201
1241
|
'<ul id="wg-todos" data-wg-todo-style="' +
|
|
1202
1242
|
st +
|
|
1203
1243
|
'">' +
|
|
1204
1244
|
(items || [])
|
|
1205
1245
|
.map((t) => {
|
|
1206
|
-
const cls =
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1246
|
+
const cls =
|
|
1247
|
+
st === "bullets"
|
|
1248
|
+
? t.done
|
|
1249
|
+
? "wg-todo-done"
|
|
1250
|
+
: "wg-todo-pending"
|
|
1251
|
+
: t.current
|
|
1252
|
+
? "wg-todo-current"
|
|
1253
|
+
: t.done
|
|
1254
|
+
? "wg-todo-done"
|
|
1255
|
+
: "wg-todo-pending";
|
|
1211
1256
|
let mark;
|
|
1212
1257
|
if (st === "checklist") {
|
|
1213
1258
|
mark = t.done ? "\u2611" : "\u2610";
|
|
1259
|
+
} else if (st === "bullets") {
|
|
1260
|
+
mark = "\u2022";
|
|
1214
1261
|
} else {
|
|
1215
1262
|
mark = t.done ? "\u2713" : t.current ? "\u2192" : "\u25CB";
|
|
1216
1263
|
}
|
|
@@ -1266,15 +1313,14 @@ async function installOverlay(page, title) {
|
|
|
1266
1313
|
window.__wgStampModal(dock, "todos", { ready: true });
|
|
1267
1314
|
}
|
|
1268
1315
|
};
|
|
1269
|
-
// Device
|
|
1270
|
-
// 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? }
|
|
1271
1318
|
window.__wgSyncDevice = (payload) => {
|
|
1272
1319
|
const sync =
|
|
1273
1320
|
payload && (payload.sync === "clear" || payload.sync === "keep" || payload.sync === "set")
|
|
1274
1321
|
? payload.sync
|
|
1275
1322
|
: "keep";
|
|
1276
1323
|
const device = payload && payload.device ? payload.device : null;
|
|
1277
|
-
let badge = document.getElementById("wg-device-badge");
|
|
1278
1324
|
const cursor = document.getElementById("wg-cursor");
|
|
1279
1325
|
const MOUSE_SVG =
|
|
1280
1326
|
"<svg viewBox='0 0 32 32' width='24' height='24'>" +
|
|
@@ -1285,43 +1331,158 @@ async function installOverlay(page, title) {
|
|
|
1285
1331
|
"<ellipse cx='16' cy='22' rx='7' ry='8' fill='#0C0C1A' stroke='#fff' stroke-width='1.4'/>" +
|
|
1286
1332
|
"<rect x='12' y='6' width='8' height='16' rx='4' fill='#0C0C1A' stroke='#fff' stroke-width='1.4'/>" +
|
|
1287
1333
|
"</svg>";
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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 {
|
|
1292
1354
|
cursor.removeAttribute("data-touch");
|
|
1293
1355
|
cursor.innerHTML = MOUSE_SVG;
|
|
1294
1356
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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;
|
|
1298
1459
|
}
|
|
1460
|
+
|
|
1299
1461
|
if (!device) return;
|
|
1300
1462
|
const preset = device.preset || "desktop";
|
|
1301
1463
|
const touch = !!device.touchMode;
|
|
1302
1464
|
const remain = payload.remain !== false && sync !== "clear";
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
}
|
|
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);
|
|
1325
1486
|
}
|
|
1326
1487
|
};
|
|
1327
1488
|
},
|
|
@@ -2755,8 +2916,7 @@ async function clickPulseAt(page, x, y, tone) {
|
|
|
2755
2916
|
|
|
2756
2917
|
/**
|
|
2757
2918
|
* Apply authored device fixture to the live page (0.13+).
|
|
2758
|
-
*
|
|
2759
|
-
* 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).
|
|
2760
2920
|
*/
|
|
2761
2921
|
async function applyDeviceToPage(page, device, sync) {
|
|
2762
2922
|
const mode = sync || (device ? "set" : "keep");
|
|
@@ -2774,14 +2934,46 @@ async function applyDeviceToPage(page, device, sync) {
|
|
|
2774
2934
|
typeof page.__wgRealSetViewportSize === "function"
|
|
2775
2935
|
? page.__wgRealSetViewportSize
|
|
2776
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;
|
|
2777
2942
|
try {
|
|
2778
|
-
|
|
2779
|
-
width: d.viewport.width,
|
|
2780
|
-
height: d.viewport.height,
|
|
2781
|
-
});
|
|
2943
|
+
from = page.viewportSize();
|
|
2782
2944
|
} catch {
|
|
2783
|
-
|
|
2945
|
+
from = null;
|
|
2784
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.
|
|
2785
2977
|
await page
|
|
2786
2978
|
.evaluate(
|
|
2787
2979
|
(payload) => {
|
|
@@ -2791,6 +2983,7 @@ async function applyDeviceToPage(page, device, sync) {
|
|
|
2791
2983
|
sync: mode,
|
|
2792
2984
|
device: d,
|
|
2793
2985
|
remain: mode === "keep" || mode === "set",
|
|
2986
|
+
announce,
|
|
2794
2987
|
},
|
|
2795
2988
|
)
|
|
2796
2989
|
.catch(() => {});
|
package/dist/highlights.d.ts
CHANGED
|
@@ -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 `
|
|
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[];
|
|
@@ -280,8 +281,8 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
|
|
|
280
281
|
*/
|
|
281
282
|
todoId(id: string): void;
|
|
282
283
|
/**
|
|
283
|
-
* Dock presentation: `sequential` (arrow walkthrough)
|
|
284
|
-
* (independent checkboxes, no
|
|
284
|
+
* Dock presentation: `sequential` (arrow walkthrough), `checklist`
|
|
285
|
+
* (independent checkboxes), or `bullets` (flat • list, no progress marks).
|
|
285
286
|
*/
|
|
286
287
|
todoStyle(style: TodoListStyle): void;
|
|
287
288
|
/**
|
|
@@ -510,7 +511,8 @@ export declare function normalizeTodoPos(raw: unknown): "left" | "right" | undef
|
|
|
510
511
|
export declare function normalizeTodoStyle(raw: unknown): TodoListStyle | undefined;
|
|
511
512
|
/**
|
|
512
513
|
* Normalize checklist rows; apply todoIndex for sequential done/current.
|
|
513
|
-
* 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).
|
|
514
516
|
*/
|
|
515
517
|
export declare function normalizeTodos(todos: readonly WaygraphTodoInput[] | undefined, todoIndex?: number, style?: TodoListStyle): WaygraphTodoItem[];
|
|
516
518
|
/** Build the floating dock from bag / phase fields. */
|
|
@@ -540,13 +542,13 @@ export declare function applyTodoPhase(prev: TodoDockState | undefined, phase: {
|
|
|
540
542
|
sync: "set" | "clear" | "keep";
|
|
541
543
|
};
|
|
542
544
|
/**
|
|
543
|
-
* Advance every sequential group to `index` (capped). Checklist
|
|
545
|
+
* Advance every sequential group to `index` (capped). Checklist / bullets unchanged.
|
|
544
546
|
* Used while cycling rings / fill/click stubs so the walkthrough moves.
|
|
545
547
|
*/
|
|
546
548
|
export declare function advanceTodoDock(dock: TodoDockState | undefined, index: number): TodoDockState | undefined;
|
|
547
549
|
/**
|
|
548
550
|
* Mark sequential groups complete (all done, none current) - e.g. end of step.
|
|
549
|
-
* Checklist groups keep explicit done flags.
|
|
551
|
+
* Checklist / bullets groups keep explicit done flags.
|
|
550
552
|
*/
|
|
551
553
|
export declare function completeSequentialTodoDock(dock: TodoDockState | undefined): TodoDockState | undefined;
|
|
552
554
|
/**
|
package/dist/highlights.js
CHANGED
|
@@ -495,11 +495,20 @@ export function normalizeTodoStyle(raw) {
|
|
|
495
495
|
return "sequential";
|
|
496
496
|
if (s === "checklist" || s === "check" || s === "checks" || s === "box")
|
|
497
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
|
+
}
|
|
498
506
|
return undefined;
|
|
499
507
|
}
|
|
500
508
|
/**
|
|
501
509
|
* Normalize checklist rows; apply todoIndex for sequential done/current.
|
|
502
|
-
* 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).
|
|
503
512
|
*/
|
|
504
513
|
export function normalizeTodos(todos, todoIndex, style = "sequential") {
|
|
505
514
|
if (!todos || todos.length === 0)
|
|
@@ -523,6 +532,11 @@ export function normalizeTodos(todos, todoIndex, style = "sequential") {
|
|
|
523
532
|
item.current = false;
|
|
524
533
|
return item;
|
|
525
534
|
}
|
|
535
|
+
if (style === "bullets") {
|
|
536
|
+
// Flat list: no walkthrough current; keep explicit done if authored.
|
|
537
|
+
item.current = false;
|
|
538
|
+
return item;
|
|
539
|
+
}
|
|
526
540
|
if (idx !== undefined) {
|
|
527
541
|
if (item.done === undefined)
|
|
528
542
|
item.done = i < idx;
|
|
@@ -606,7 +620,7 @@ export function applyTodoPhase(prev, phase) {
|
|
|
606
620
|
return { dock: prev, sync: "keep" };
|
|
607
621
|
}
|
|
608
622
|
/**
|
|
609
|
-
* Advance every sequential group to `index` (capped). Checklist
|
|
623
|
+
* Advance every sequential group to `index` (capped). Checklist / bullets unchanged.
|
|
610
624
|
* Used while cycling rings / fill/click stubs so the walkthrough moves.
|
|
611
625
|
*/
|
|
612
626
|
export function advanceTodoDock(dock, index) {
|
|
@@ -636,7 +650,7 @@ export function advanceTodoDock(dock, index) {
|
|
|
636
650
|
}
|
|
637
651
|
/**
|
|
638
652
|
* Mark sequential groups complete (all done, none current) - e.g. end of step.
|
|
639
|
-
* Checklist groups keep explicit done flags.
|
|
653
|
+
* Checklist / bullets groups keep explicit done flags.
|
|
640
654
|
*/
|
|
641
655
|
export function completeSequentialTodoDock(dock) {
|
|
642
656
|
if (!dock)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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,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,
|