waygraph 0.13.1 → 0.13.3
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 -43
- package/dist/highlights.d.ts +25 -2
- package/dist/highlights.js +131 -43
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- 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/scripts/demo-orientation-gestures-video.mjs +262 -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;}" +
|
|
@@ -1287,15 +1313,14 @@ async function installOverlay(page, title) {
|
|
|
1287
1313
|
window.__wgStampModal(dock, "todos", { ready: true });
|
|
1288
1314
|
}
|
|
1289
1315
|
};
|
|
1290
|
-
// Device
|
|
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,186 @@ 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
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
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
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1357
|
+
};
|
|
1358
|
+
const ensureChip = (preset, touch, remain, orientation) => {
|
|
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 orient =
|
|
1379
|
+
orientation === "landscape" || orientation === "portrait"
|
|
1380
|
+
? orientation
|
|
1381
|
+
: "";
|
|
1382
|
+
if (orient) badge.dataset.orientation = orient;
|
|
1383
|
+
const chipLabel =
|
|
1384
|
+
preset === "desktop" && !touch
|
|
1385
|
+
? "desktop"
|
|
1386
|
+
: preset +
|
|
1387
|
+
(orient ? " \u00b7 " + (orient === "landscape" ? "land" : "port") : "") +
|
|
1388
|
+
(remain ? " \u00b7 remain" : "");
|
|
1389
|
+
badge.innerHTML =
|
|
1390
|
+
'<span class="wg-dev-icon">' +
|
|
1391
|
+
(ICONS[preset] || ICONS.desktop) +
|
|
1392
|
+
'</span><span class="wg-dev-label">' +
|
|
1393
|
+
chipLabel +
|
|
1394
|
+
"</span>";
|
|
1395
|
+
void badge.offsetWidth;
|
|
1396
|
+
badge.classList.add("wg-in");
|
|
1397
|
+
};
|
|
1398
|
+
const showToast = (preset, touch, title, sub, orientation) => {
|
|
1399
|
+
let toast = document.getElementById("wg-device-toast");
|
|
1400
|
+
if (toast && toast._wgTimer) {
|
|
1401
|
+
clearTimeout(toast._wgTimer);
|
|
1402
|
+
toast._wgTimer = null;
|
|
1403
|
+
}
|
|
1404
|
+
if (!toast) {
|
|
1405
|
+
toast = document.createElement("div");
|
|
1406
|
+
toast.id = "wg-device-toast";
|
|
1407
|
+
toast.setAttribute("data-wg-ui", "1");
|
|
1408
|
+
document.documentElement.appendChild(toast);
|
|
1409
|
+
}
|
|
1410
|
+
// Hide chip while toast is up (same corner).
|
|
1411
|
+
const badge = document.getElementById("wg-device-badge");
|
|
1412
|
+
if (badge) badge.classList.remove("wg-in");
|
|
1413
|
+
toast.dataset.preset = preset;
|
|
1414
|
+
toast.innerHTML =
|
|
1415
|
+
'<span class="wg-dev-icon">' +
|
|
1416
|
+
(ICONS[preset] || ICONS.desktop) +
|
|
1417
|
+
'</span><span class="wg-dev-copy"><div class="wg-dev-title">' +
|
|
1418
|
+
title +
|
|
1419
|
+
'</div><div class="wg-dev-sub">' +
|
|
1420
|
+
sub +
|
|
1421
|
+
"</div></span>";
|
|
1422
|
+
toast.classList.remove("wg-out");
|
|
1423
|
+
void toast.offsetWidth;
|
|
1424
|
+
toast.classList.add("wg-in");
|
|
1425
|
+
toast._wgTimer = setTimeout(() => {
|
|
1426
|
+
toast.classList.add("wg-out");
|
|
1427
|
+
toast.classList.remove("wg-in");
|
|
1428
|
+
setTimeout(() => {
|
|
1429
|
+
const t = document.getElementById("wg-device-toast");
|
|
1430
|
+
if (t) t.remove();
|
|
1431
|
+
if (preset !== "desktop" || touch) {
|
|
1432
|
+
ensureChip(preset, touch, true, orientation);
|
|
1433
|
+
}
|
|
1434
|
+
}, 380);
|
|
1435
|
+
}, 2200);
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1438
|
+
if (sync === "keep" && !device) return;
|
|
1439
|
+
|
|
1440
|
+
const isDesktopClear =
|
|
1441
|
+
sync === "clear" ||
|
|
1442
|
+
(device && device.preset === "desktop" && !device.touchMode);
|
|
1443
|
+
if (isDesktopClear) {
|
|
1444
|
+
setCursor(false);
|
|
1445
|
+
const announce = payload.announce !== false && sync !== "keep";
|
|
1446
|
+
if (announce) {
|
|
1447
|
+
const vp = device && device.viewport ? device.viewport : { width: 1280, height: 720 };
|
|
1448
|
+
showToast(
|
|
1449
|
+
"desktop",
|
|
1450
|
+
false,
|
|
1451
|
+
"Back to desktop",
|
|
1452
|
+
vp.width + "\u00d7" + vp.height + " \u00b7 mouse",
|
|
1453
|
+
"landscape",
|
|
1454
|
+
);
|
|
1455
|
+
setTimeout(() => {
|
|
1456
|
+
const badge = document.getElementById("wg-device-badge");
|
|
1457
|
+
if (badge) badge.remove();
|
|
1458
|
+
}, 2600);
|
|
1459
|
+
} else {
|
|
1460
|
+
const badge = document.getElementById("wg-device-badge");
|
|
1461
|
+
if (badge) badge.remove();
|
|
1462
|
+
const toast = document.getElementById("wg-device-toast");
|
|
1463
|
+
if (toast) toast.remove();
|
|
1464
|
+
}
|
|
1465
|
+
window.__wgDevicePreset = "desktop";
|
|
1466
|
+
window.__wgDeviceOrientation = "landscape";
|
|
1467
|
+
return;
|
|
1319
1468
|
}
|
|
1469
|
+
|
|
1320
1470
|
if (!device) return;
|
|
1321
1471
|
const preset = device.preset || "desktop";
|
|
1322
1472
|
const touch = !!device.touchMode;
|
|
1323
1473
|
const remain = payload.remain !== false && sync !== "clear";
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1474
|
+
const vp = device.viewport || {};
|
|
1475
|
+
const orient =
|
|
1476
|
+
device.orientation === "landscape" || device.orientation === "portrait"
|
|
1477
|
+
? device.orientation
|
|
1478
|
+
: vp.height > vp.width
|
|
1479
|
+
? "portrait"
|
|
1480
|
+
: "landscape";
|
|
1481
|
+
const prev = window.__wgDevicePreset || "";
|
|
1482
|
+
const prevOrient = window.__wgDeviceOrientation || "";
|
|
1483
|
+
const presetChanged = prev !== preset || (touch && prev === "desktop");
|
|
1484
|
+
const orientChanged = prevOrient !== "" && prevOrient !== orient;
|
|
1485
|
+
const changed = presetChanged || orientChanged || prev === "";
|
|
1486
|
+
setCursor(touch);
|
|
1487
|
+
window.__wgDevicePreset = preset;
|
|
1488
|
+
window.__wgDeviceOrientation = orient;
|
|
1489
|
+
|
|
1490
|
+
const announce =
|
|
1491
|
+
payload.announce === true ||
|
|
1492
|
+
(payload.announce !== false && (sync === "set" || sync === "clear") && changed);
|
|
1493
|
+
|
|
1494
|
+
const size =
|
|
1495
|
+
(vp.width || "?") +
|
|
1496
|
+
"\u00d7" +
|
|
1497
|
+
(vp.height || "?") +
|
|
1498
|
+
" \u00b7 " +
|
|
1499
|
+
orient +
|
|
1500
|
+
(touch ? " \u00b7 touch" : "");
|
|
1501
|
+
let titleText = {
|
|
1502
|
+
mobile: "Now in mobile mode",
|
|
1503
|
+
tablet: "Now in tablet mode",
|
|
1504
|
+
desktop: "Now in desktop mode",
|
|
1505
|
+
}[preset] || "Device updated";
|
|
1506
|
+
if (orientChanged && !presetChanged) {
|
|
1507
|
+
titleText =
|
|
1508
|
+
orient === "landscape" ? "Rotated to landscape" : "Rotated to portrait";
|
|
1329
1509
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
preset
|
|
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
|
-
}
|
|
1510
|
+
if (announce) {
|
|
1511
|
+
showToast(preset, touch, titleText, size, orient);
|
|
1512
|
+
} else {
|
|
1513
|
+
ensureChip(preset, touch, remain, orient);
|
|
1346
1514
|
}
|
|
1347
1515
|
};
|
|
1348
1516
|
},
|
|
@@ -2776,8 +2944,7 @@ async function clickPulseAt(page, x, y, tone) {
|
|
|
2776
2944
|
|
|
2777
2945
|
/**
|
|
2778
2946
|
* Apply authored device fixture to the live page (0.13+).
|
|
2779
|
-
*
|
|
2780
|
-
* sync keep with no device = no-op; clear / desktop = restore main feel.
|
|
2947
|
+
* Seamless viewport lerp + toast on set/clear (quiet chip recreate on keep).
|
|
2781
2948
|
*/
|
|
2782
2949
|
async function applyDeviceToPage(page, device, sync) {
|
|
2783
2950
|
const mode = sync || (device ? "set" : "keep");
|
|
@@ -2795,14 +2962,46 @@ async function applyDeviceToPage(page, device, sync) {
|
|
|
2795
2962
|
typeof page.__wgRealSetViewportSize === "function"
|
|
2796
2963
|
? page.__wgRealSetViewportSize
|
|
2797
2964
|
: page.setViewportSize.bind(page);
|
|
2965
|
+
const target = {
|
|
2966
|
+
width: Math.max(200, Math.floor(d.viewport.width)),
|
|
2967
|
+
height: Math.max(200, Math.floor(d.viewport.height)),
|
|
2968
|
+
};
|
|
2969
|
+
let from = null;
|
|
2798
2970
|
try {
|
|
2799
|
-
|
|
2800
|
-
width: d.viewport.width,
|
|
2801
|
-
height: d.viewport.height,
|
|
2802
|
-
});
|
|
2971
|
+
from = page.viewportSize();
|
|
2803
2972
|
} catch {
|
|
2804
|
-
|
|
2973
|
+
from = null;
|
|
2805
2974
|
}
|
|
2975
|
+
const announce = mode === "set" || mode === "clear";
|
|
2976
|
+
const shouldAnimate =
|
|
2977
|
+
announce &&
|
|
2978
|
+
from &&
|
|
2979
|
+
from.width > 0 &&
|
|
2980
|
+
from.height > 0 &&
|
|
2981
|
+
(from.width !== target.width || from.height !== target.height);
|
|
2982
|
+
|
|
2983
|
+
try {
|
|
2984
|
+
if (shouldAnimate) {
|
|
2985
|
+
const steps = 14;
|
|
2986
|
+
const ms = Number(process.env.WAYGRAPH_DEVICE_TRANSITION_MS || 520);
|
|
2987
|
+
const stepMs = Math.max(12, Math.floor(ms / steps));
|
|
2988
|
+
for (let i = 1; i <= steps; i++) {
|
|
2989
|
+
const t = i / steps;
|
|
2990
|
+
const e = t * t * (3 - 2 * t); // smoothstep
|
|
2991
|
+
await realSet({
|
|
2992
|
+
width: Math.round(from.width + (target.width - from.width) * e),
|
|
2993
|
+
height: Math.round(from.height + (target.height - from.height) * e),
|
|
2994
|
+
});
|
|
2995
|
+
if (i < steps) await new Promise((r) => setTimeout(r, stepMs));
|
|
2996
|
+
}
|
|
2997
|
+
} else {
|
|
2998
|
+
await realSet(target);
|
|
2999
|
+
}
|
|
3000
|
+
} catch {
|
|
3001
|
+
/* maximized / null viewport hosts may reject - toast still updates */
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
// Fire toast near the end of the lerp so it reads as one seamless beat.
|
|
2806
3005
|
await page
|
|
2807
3006
|
.evaluate(
|
|
2808
3007
|
(payload) => {
|
|
@@ -2812,6 +3011,7 @@ async function applyDeviceToPage(page, device, sync) {
|
|
|
2812
3011
|
sync: mode,
|
|
2813
3012
|
device: d,
|
|
2814
3013
|
remain: mode === "keep" || mode === "set",
|
|
3014
|
+
announce,
|
|
2815
3015
|
},
|
|
2816
3016
|
)
|
|
2817
3017
|
.catch(() => {});
|
package/dist/highlights.d.ts
CHANGED
|
@@ -127,6 +127,8 @@ export type StubPhaseFixtures = {
|
|
|
127
127
|
viewport?: DeviceViewport;
|
|
128
128
|
/** Touch theater + Playwright touchscreen when true. */
|
|
129
129
|
touch?: boolean;
|
|
130
|
+
/** Portrait / landscape (swaps preset width/height). */
|
|
131
|
+
orientation?: DeviceOrientation;
|
|
130
132
|
};
|
|
131
133
|
/** Named viewport presets (0.13+). */
|
|
132
134
|
export type DevicePreset = "mobile" | "tablet" | "desktop";
|
|
@@ -149,14 +151,26 @@ export type DeviceState = {
|
|
|
149
151
|
hasTouch: boolean;
|
|
150
152
|
/** Demo gesture theater: finger cursor, tap/hold timing, prefer touchscreen. */
|
|
151
153
|
touchMode: boolean;
|
|
154
|
+
/** Viewport orientation (0.13.3+). Default derived from width/height. */
|
|
155
|
+
orientation?: DeviceOrientation;
|
|
152
156
|
};
|
|
153
|
-
/**
|
|
157
|
+
/** Portrait (tall) or landscape (wide). */
|
|
158
|
+
export type DeviceOrientation = "portrait" | "landscape";
|
|
159
|
+
/** Built-in preset sizes (CSS pixels) — stored in portrait for mobile/tablet. */
|
|
154
160
|
export declare const DEVICE_PRESETS: Record<DevicePreset, Omit<DeviceState, "touchMode"> & {
|
|
155
161
|
touchMode?: boolean;
|
|
156
162
|
}>;
|
|
157
163
|
export declare function normalizeDevicePreset(raw: unknown): DevicePreset | undefined;
|
|
164
|
+
export declare function normalizeDeviceOrientation(raw: unknown): DeviceOrientation | undefined;
|
|
165
|
+
/** Infer orientation from box (square counts as landscape). */
|
|
166
|
+
export declare function orientationFromViewport(vp: DeviceViewport): DeviceOrientation;
|
|
167
|
+
/**
|
|
168
|
+
* Swap width/height so the viewport matches the requested orientation.
|
|
169
|
+
* Preserves deviceScaleFactor. No-op when already oriented.
|
|
170
|
+
*/
|
|
171
|
+
export declare function applyOrientation(state: DeviceState, orientation: DeviceOrientation): DeviceState;
|
|
158
172
|
/** 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;
|
|
173
|
+
export declare function resolveDeviceState(input: DevicePreset | DeviceState | DeviceViewport | undefined, touchOverride?: boolean, orientationOverride?: DeviceOrientation): DeviceState | undefined;
|
|
160
174
|
/**
|
|
161
175
|
* Carry-forward for device fixtures (mirror {@link applyTodoPhase}).
|
|
162
176
|
* - clear: hideDevice / clearDevice / device("desktop") with clear intent
|
|
@@ -307,6 +321,15 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
|
|
|
307
321
|
viewport(box: DeviceViewport): void;
|
|
308
322
|
/** Enable / disable touch theater + touchscreen gestures. */
|
|
309
323
|
touch(on?: boolean): void;
|
|
324
|
+
/**
|
|
325
|
+
* Rotate viewport to portrait or landscape (0.13.3+).
|
|
326
|
+
* Keeps current preset; swaps width/height. Toast + lerp like device().
|
|
327
|
+
*/
|
|
328
|
+
orientation(o: DeviceOrientation): void;
|
|
329
|
+
/** Alias for {@link StubCtx.orientation}("landscape"). */
|
|
330
|
+
landscape(): void;
|
|
331
|
+
/** Alias for {@link StubCtx.orientation}("portrait"). */
|
|
332
|
+
portrait(): void;
|
|
310
333
|
/**
|
|
311
334
|
* Reset to desktop viewport and clear touch mode. Omit device on a later
|
|
312
335
|
* block = **keep**; only hideDevice / clearDevice / device("desktop") clears.
|
package/dist/highlights.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
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). */
|
|
15
|
+
/** Built-in preset sizes (CSS pixels) — stored in portrait for mobile/tablet. */
|
|
16
16
|
export const DEVICE_PRESETS = {
|
|
17
17
|
mobile: {
|
|
18
18
|
preset: "mobile",
|
|
@@ -20,6 +20,7 @@ export const DEVICE_PRESETS = {
|
|
|
20
20
|
isMobile: true,
|
|
21
21
|
hasTouch: true,
|
|
22
22
|
touchMode: true,
|
|
23
|
+
orientation: "portrait",
|
|
23
24
|
},
|
|
24
25
|
tablet: {
|
|
25
26
|
preset: "tablet",
|
|
@@ -27,6 +28,7 @@ export const DEVICE_PRESETS = {
|
|
|
27
28
|
isMobile: true,
|
|
28
29
|
hasTouch: true,
|
|
29
30
|
touchMode: true,
|
|
31
|
+
orientation: "portrait",
|
|
30
32
|
},
|
|
31
33
|
desktop: {
|
|
32
34
|
preset: "desktop",
|
|
@@ -34,6 +36,7 @@ export const DEVICE_PRESETS = {
|
|
|
34
36
|
isMobile: false,
|
|
35
37
|
hasTouch: false,
|
|
36
38
|
touchMode: false,
|
|
39
|
+
orientation: "landscape",
|
|
37
40
|
},
|
|
38
41
|
};
|
|
39
42
|
export function normalizeDevicePreset(raw) {
|
|
@@ -48,67 +51,118 @@ export function normalizeDevicePreset(raw) {
|
|
|
48
51
|
return "desktop";
|
|
49
52
|
return undefined;
|
|
50
53
|
}
|
|
54
|
+
export function normalizeDeviceOrientation(raw) {
|
|
55
|
+
const s = String(raw ?? "")
|
|
56
|
+
.trim()
|
|
57
|
+
.toLowerCase();
|
|
58
|
+
if (s === "portrait" || s === "port" || s === "tall" || s === "p")
|
|
59
|
+
return "portrait";
|
|
60
|
+
if (s === "landscape" || s === "land" || s === "wide" || s === "l")
|
|
61
|
+
return "landscape";
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
/** Infer orientation from box (square counts as landscape). */
|
|
65
|
+
export function orientationFromViewport(vp) {
|
|
66
|
+
return vp.height > vp.width ? "portrait" : "landscape";
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Swap width/height so the viewport matches the requested orientation.
|
|
70
|
+
* Preserves deviceScaleFactor. No-op when already oriented.
|
|
71
|
+
*/
|
|
72
|
+
export function applyOrientation(state, orientation) {
|
|
73
|
+
const vp = { ...state.viewport };
|
|
74
|
+
const current = orientationFromViewport(vp);
|
|
75
|
+
if (current !== orientation) {
|
|
76
|
+
const w = vp.width;
|
|
77
|
+
vp.width = vp.height;
|
|
78
|
+
vp.height = w;
|
|
79
|
+
}
|
|
80
|
+
return { ...state, viewport: vp, orientation };
|
|
81
|
+
}
|
|
51
82
|
/** Resolve a preset name or partial state into a full DeviceState. */
|
|
52
|
-
export function resolveDeviceState(input, touchOverride) {
|
|
83
|
+
export function resolveDeviceState(input, touchOverride, orientationOverride) {
|
|
53
84
|
if (input === undefined)
|
|
54
85
|
return undefined;
|
|
86
|
+
let resolved;
|
|
55
87
|
if (typeof input === "string") {
|
|
56
88
|
const p = normalizeDevicePreset(input);
|
|
57
89
|
if (!p)
|
|
58
90
|
return undefined;
|
|
59
91
|
const base = DEVICE_PRESETS[p];
|
|
60
92
|
const touchMode = touchOverride !== undefined ? !!touchOverride : !!base.touchMode;
|
|
61
|
-
|
|
93
|
+
resolved = {
|
|
62
94
|
preset: base.preset,
|
|
63
95
|
viewport: { ...base.viewport },
|
|
64
96
|
isMobile: base.isMobile,
|
|
65
97
|
hasTouch: base.hasTouch || touchMode,
|
|
66
98
|
touchMode,
|
|
99
|
+
orientation: base.orientation || orientationFromViewport(base.viewport),
|
|
67
100
|
};
|
|
68
101
|
}
|
|
69
|
-
if ("width" in input && "height" in input && !("preset" in input)) {
|
|
102
|
+
else if ("width" in input && "height" in input && !("preset" in input)) {
|
|
70
103
|
const vp = input;
|
|
71
104
|
const touchMode = touchOverride !== undefined ? !!touchOverride : true;
|
|
72
|
-
|
|
105
|
+
const box = {
|
|
106
|
+
width: Math.max(200, Math.floor(vp.width)),
|
|
107
|
+
height: Math.max(200, Math.floor(vp.height)),
|
|
108
|
+
...(vp.deviceScaleFactor !== undefined
|
|
109
|
+
? { deviceScaleFactor: vp.deviceScaleFactor }
|
|
110
|
+
: { deviceScaleFactor: 2 }),
|
|
111
|
+
};
|
|
112
|
+
resolved = {
|
|
73
113
|
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
|
-
},
|
|
114
|
+
viewport: box,
|
|
81
115
|
isMobile: true,
|
|
82
116
|
hasTouch: true,
|
|
83
117
|
touchMode,
|
|
118
|
+
orientation: orientationFromViewport(box),
|
|
84
119
|
};
|
|
85
120
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
else {
|
|
122
|
+
const d = input;
|
|
123
|
+
const preset = normalizeDevicePreset(d.preset) || "desktop";
|
|
124
|
+
const base = DEVICE_PRESETS[preset];
|
|
125
|
+
const touchMode = touchOverride !== undefined
|
|
126
|
+
? !!touchOverride
|
|
127
|
+
: d.touchMode !== undefined
|
|
128
|
+
? !!d.touchMode
|
|
129
|
+
: !!base.touchMode;
|
|
130
|
+
const viewport = d.viewport
|
|
131
|
+
? {
|
|
132
|
+
width: Math.max(200, Math.floor(d.viewport.width)),
|
|
133
|
+
height: Math.max(200, Math.floor(d.viewport.height)),
|
|
134
|
+
...(d.viewport.deviceScaleFactor !== undefined
|
|
135
|
+
? { deviceScaleFactor: d.viewport.deviceScaleFactor }
|
|
136
|
+
: base.viewport.deviceScaleFactor !== undefined
|
|
137
|
+
? { deviceScaleFactor: base.viewport.deviceScaleFactor }
|
|
138
|
+
: {}),
|
|
139
|
+
}
|
|
140
|
+
: { ...base.viewport };
|
|
141
|
+
resolved = {
|
|
142
|
+
preset,
|
|
143
|
+
viewport,
|
|
144
|
+
isMobile: d.isMobile !== undefined ? !!d.isMobile : base.isMobile,
|
|
145
|
+
hasTouch: d.hasTouch !== undefined ? !!d.hasTouch : base.hasTouch || touchMode,
|
|
146
|
+
touchMode,
|
|
147
|
+
orientation: normalizeDeviceOrientation(d.orientation) ||
|
|
148
|
+
orientationFromViewport(viewport),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (!resolved)
|
|
152
|
+
return undefined;
|
|
153
|
+
const want = orientationOverride ||
|
|
154
|
+
(typeof input === "object" && input && "orientation" in input
|
|
155
|
+
? normalizeDeviceOrientation(input.orientation)
|
|
156
|
+
: undefined);
|
|
157
|
+
if (want)
|
|
158
|
+
return applyOrientation(resolved, want);
|
|
159
|
+
if (!resolved.orientation) {
|
|
160
|
+
resolved = {
|
|
161
|
+
...resolved,
|
|
162
|
+
orientation: orientationFromViewport(resolved.viewport),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return resolved;
|
|
112
166
|
}
|
|
113
167
|
/**
|
|
114
168
|
* Carry-forward for device fixtures (mirror {@link applyTodoPhase}).
|
|
@@ -734,7 +788,7 @@ function createStubCtx(bag, opts) {
|
|
|
734
788
|
device(preset) {
|
|
735
789
|
bag.deviceTouched = true;
|
|
736
790
|
bag.hideDevice = false;
|
|
737
|
-
const resolved = resolveDeviceState(preset, bag.touchOverride);
|
|
791
|
+
const resolved = resolveDeviceState(preset, bag.touchOverride, bag.orientationOverride);
|
|
738
792
|
if (resolved) {
|
|
739
793
|
bag.device = resolved;
|
|
740
794
|
if (resolved.preset === "desktop" && !resolved.touchMode) {
|
|
@@ -745,7 +799,7 @@ function createStubCtx(bag, opts) {
|
|
|
745
799
|
viewport(box) {
|
|
746
800
|
bag.deviceTouched = true;
|
|
747
801
|
bag.hideDevice = false;
|
|
748
|
-
const resolved = resolveDeviceState(box, bag.touchOverride);
|
|
802
|
+
const resolved = resolveDeviceState(box, bag.touchOverride, bag.orientationOverride);
|
|
749
803
|
if (resolved)
|
|
750
804
|
bag.device = resolved;
|
|
751
805
|
},
|
|
@@ -765,22 +819,43 @@ function createStubCtx(bag, opts) {
|
|
|
765
819
|
bag.hideDevice = false;
|
|
766
820
|
}
|
|
767
821
|
else if (enabled) {
|
|
768
|
-
const base = resolveDeviceState("desktop", true);
|
|
822
|
+
const base = resolveDeviceState("desktop", true, bag.orientationOverride);
|
|
769
823
|
if (base)
|
|
770
824
|
bag.device = { ...base, touchMode: true, hasTouch: true };
|
|
771
825
|
}
|
|
772
826
|
},
|
|
827
|
+
orientation(o) {
|
|
828
|
+
const want = normalizeDeviceOrientation(o);
|
|
829
|
+
if (!want)
|
|
830
|
+
return;
|
|
831
|
+
bag.deviceTouched = true;
|
|
832
|
+
bag.hideDevice = false;
|
|
833
|
+
bag.orientationOverride = want;
|
|
834
|
+
const base = bag.device ||
|
|
835
|
+
resolveDeviceState("mobile", bag.touchOverride) ||
|
|
836
|
+
resolveDeviceState("mobile");
|
|
837
|
+
if (base)
|
|
838
|
+
bag.device = applyOrientation(base, want);
|
|
839
|
+
},
|
|
840
|
+
landscape() {
|
|
841
|
+
this.orientation("landscape");
|
|
842
|
+
},
|
|
843
|
+
portrait() {
|
|
844
|
+
this.orientation("portrait");
|
|
845
|
+
},
|
|
773
846
|
hideDevice() {
|
|
774
847
|
bag.deviceTouched = true;
|
|
775
848
|
bag.hideDevice = true;
|
|
776
849
|
bag.device = resolveDeviceState("desktop", false);
|
|
777
850
|
bag.touchOverride = false;
|
|
851
|
+
bag.orientationOverride = undefined;
|
|
778
852
|
},
|
|
779
853
|
clearDevice() {
|
|
780
854
|
bag.deviceTouched = true;
|
|
781
855
|
bag.hideDevice = true;
|
|
782
856
|
bag.device = resolveDeviceState("desktop", false);
|
|
783
857
|
bag.touchOverride = false;
|
|
858
|
+
bag.orientationOverride = undefined;
|
|
784
859
|
},
|
|
785
860
|
zoom(n) {
|
|
786
861
|
if (Number.isFinite(n) && n > 0)
|
|
@@ -834,10 +909,23 @@ function createStubCtx(bag, opts) {
|
|
|
834
909
|
bag.deviceTouched = true;
|
|
835
910
|
bag.touchOverride = !!partial.touch;
|
|
836
911
|
}
|
|
912
|
+
if (partial.orientation !== undefined) {
|
|
913
|
+
const want = normalizeDeviceOrientation(partial.orientation);
|
|
914
|
+
if (want) {
|
|
915
|
+
bag.deviceTouched = true;
|
|
916
|
+
bag.hideDevice = false;
|
|
917
|
+
bag.orientationOverride = want;
|
|
918
|
+
const base = bag.device ||
|
|
919
|
+
resolveDeviceState("mobile", bag.touchOverride) ||
|
|
920
|
+
resolveDeviceState("mobile");
|
|
921
|
+
if (base)
|
|
922
|
+
bag.device = applyOrientation(base, want);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
837
925
|
if (partial.device !== undefined) {
|
|
838
926
|
bag.deviceTouched = true;
|
|
839
927
|
bag.hideDevice = false;
|
|
840
|
-
const resolved = resolveDeviceState(partial.device, bag.touchOverride);
|
|
928
|
+
const resolved = resolveDeviceState(partial.device, bag.touchOverride, bag.orientationOverride);
|
|
841
929
|
if (resolved) {
|
|
842
930
|
bag.device = resolved;
|
|
843
931
|
if (resolved.preset === "desktop" && !resolved.touchMode)
|
|
@@ -847,7 +935,7 @@ function createStubCtx(bag, opts) {
|
|
|
847
935
|
if (partial.viewport !== undefined) {
|
|
848
936
|
bag.deviceTouched = true;
|
|
849
937
|
bag.hideDevice = false;
|
|
850
|
-
const resolved = resolveDeviceState(partial.viewport, bag.touchOverride);
|
|
938
|
+
const resolved = resolveDeviceState(partial.viewport, bag.touchOverride, bag.orientationOverride);
|
|
851
939
|
if (resolved)
|
|
852
940
|
bag.device = resolved;
|
|
853
941
|
}
|
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, 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";
|
|
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, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, 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, DeviceOrientation, } 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, 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";
|
|
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, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, 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";
|
|
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();
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record a .webm of: portrait/landscape + click / tap / hold theater.
|
|
3
|
+
* Square capture so orientation changes are obvious in the video frame.
|
|
4
|
+
*
|
|
5
|
+
* From examples/saucedemo:
|
|
6
|
+
* node scripts/demo-orientation-gestures-video.mjs
|
|
7
|
+
*/
|
|
8
|
+
import { chromium } from "playwright";
|
|
9
|
+
import { mkdirSync, copyFileSync, readdirSync } from "node:fs";
|
|
10
|
+
import { join, dirname } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const outDir = join(__dirname, "../.tmp/device-demo");
|
|
15
|
+
mkdirSync(outDir, { recursive: true });
|
|
16
|
+
|
|
17
|
+
const CAPTURE = { width: 1080, height: 1080 };
|
|
18
|
+
|
|
19
|
+
const PRESETS = {
|
|
20
|
+
mobilePortrait: { w: 390, h: 844, preset: "mobile", orient: "portrait", touch: true },
|
|
21
|
+
mobileLandscape: { w: 844, h: 390, preset: "mobile", orient: "landscape", touch: true },
|
|
22
|
+
tabletPortrait: { w: 768, h: 1024, preset: "tablet", orient: "portrait", touch: true },
|
|
23
|
+
tabletLandscape: { w: 1024, h: 768, preset: "tablet", orient: "landscape", touch: true },
|
|
24
|
+
desktop: { w: 1280, h: 720, preset: "desktop", orient: "landscape", touch: false },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const CSS = `
|
|
28
|
+
html,body{margin:0;background:#0b1220;}
|
|
29
|
+
#wg-stage{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;
|
|
30
|
+
background:radial-gradient(ellipse at 50% 30%,#1a1030 0%,#0b1220 70%);}
|
|
31
|
+
#wg-frame{position:relative;background:#fff;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.55);
|
|
32
|
+
border:3px solid #2a1650;border-radius:18px;transition:width .5s cubic-bezier(.22,1,.36,1),
|
|
33
|
+
height .5s cubic-bezier(.22,1,.36,1),border-radius .4s ease;}
|
|
34
|
+
#wg-frame iframe{border:0;width:100%;height:100%;display:block;}
|
|
35
|
+
#wg-device-toast{position:fixed;z-index:2147483647;top:18px;right:18px;
|
|
36
|
+
display:flex;align-items:center;gap:10px;min-width:220px;max-width:340px;
|
|
37
|
+
padding:12px 14px;border-radius:14px;background:rgba(20,10,40,.96);color:#f0e8ff;
|
|
38
|
+
border:1px solid rgba(124,58,237,.55);box-shadow:0 10px 28px rgba(0,0,0,.45);
|
|
39
|
+
font:600 13px/1.35 system-ui,sans-serif;pointer-events:none;
|
|
40
|
+
opacity:0;transform:translateX(18px);transition:opacity .35s ease,transform .45s cubic-bezier(.22,1,.36,1);}
|
|
41
|
+
#wg-device-toast.wg-in{opacity:1;transform:translateX(0);}
|
|
42
|
+
#wg-device-toast[data-preset=mobile]{border-color:#3B82F6;}
|
|
43
|
+
#wg-device-toast[data-preset=tablet]{border-color:#22C55E;}
|
|
44
|
+
#wg-device-toast[data-preset=desktop]{border-color:#9CA3AF;}
|
|
45
|
+
#wg-device-toast .wg-dev-icon{width:36px;height:36px;border-radius:10px;display:flex;
|
|
46
|
+
align-items:center;justify-content:center;background:rgba(124,58,237,.28);}
|
|
47
|
+
#wg-device-toast .wg-dev-title{font:800 13px/1.2 system-ui,sans-serif;color:#fff;}
|
|
48
|
+
#wg-device-toast .wg-dev-sub{margin-top:3px;font:600 11px/1.3 system-ui,sans-serif;color:#c9a6ff;}
|
|
49
|
+
#wg-gesture-banner{position:fixed;z-index:2147483646;left:18px;bottom:18px;
|
|
50
|
+
padding:10px 14px;border-radius:12px;background:rgba(20,10,40,.94);color:#fff;
|
|
51
|
+
font:700 13px/1.3 system-ui,sans-serif;border:1px solid rgba(124,58,237,.45);}
|
|
52
|
+
#wg-cursor{position:fixed;z-index:2147483647;width:28px;height:28px;pointer-events:none;
|
|
53
|
+
left:0;top:0;opacity:0;margin:0;transition:transform .55s cubic-bezier(.22,1,.36,1),opacity .2s;}
|
|
54
|
+
#wg-click-pulse{position:fixed;z-index:2147483647;width:18px;height:18px;margin-left:-9px;
|
|
55
|
+
margin-top:-9px;border-radius:50%;pointer-events:none;opacity:0;border:2px solid #7C3AED;
|
|
56
|
+
background:rgba(124,58,237,.3);}
|
|
57
|
+
#wg-click-pulse.wg-pulse{animation:wg-pulse .55s ease-out;}
|
|
58
|
+
#wg-click-pulse[data-hold=1]{border-color:#EAB308;background:rgba(234,179,8,.35);
|
|
59
|
+
width:28px;height:28px;margin-left:-14px;margin-top:-14px;}
|
|
60
|
+
@keyframes wg-pulse{0%{opacity:.95;transform:scale(.35);}100%{opacity:0;transform:scale(2.6);}}
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
const ICONS = {
|
|
64
|
+
mobile:
|
|
65
|
+
'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="#fff" stroke-width="1.8"><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>',
|
|
66
|
+
tablet:
|
|
67
|
+
'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="#fff" stroke-width="1.8"><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>',
|
|
68
|
+
desktop:
|
|
69
|
+
'<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="#fff" stroke-width="1.8"><rect x="2.5" y="3.5" width="19" height="12.5" rx="1.5"/><path d="M8 20h8M12 16v4"/></svg>',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const MOUSE =
|
|
73
|
+
"<svg viewBox='0 0 32 32' width='24' height='24'><path fill='#0C0C1A' stroke='#fff' stroke-width='1.4' d='M6 3.5l1.4 22.5 5.8-5.4 4.2 9.4 3.6-1.6-4.2-9.2H26z'/></svg>";
|
|
74
|
+
const FINGER =
|
|
75
|
+
"<svg viewBox='0 0 32 32' width='28' height='28'><ellipse cx='16' cy='22' rx='7' ry='8' fill='#0C0C1A' stroke='#fff' stroke-width='1.4'/><rect x='12' y='6' width='8' height='16' rx='4' fill='#0C0C1A' stroke='#fff' stroke-width='1.4'/></svg>";
|
|
76
|
+
|
|
77
|
+
const executablePath = process.env.CHROME_PATH || process.env.CHROMIUM_PATH || undefined;
|
|
78
|
+
|
|
79
|
+
const browser = await chromium.launch({
|
|
80
|
+
headless: false,
|
|
81
|
+
...(executablePath ? { executablePath } : {}),
|
|
82
|
+
});
|
|
83
|
+
const context = await browser.newContext({
|
|
84
|
+
hasTouch: true,
|
|
85
|
+
viewport: CAPTURE,
|
|
86
|
+
recordVideo: { dir: outDir, size: CAPTURE },
|
|
87
|
+
deviceScaleFactor: 1,
|
|
88
|
+
});
|
|
89
|
+
const page = await context.newPage();
|
|
90
|
+
|
|
91
|
+
await page.setContent(`<!doctype html><html><head><style>${CSS}</style></head>
|
|
92
|
+
<body>
|
|
93
|
+
<div id="wg-stage"><div id="wg-frame" style="width:390px;height:844px">
|
|
94
|
+
<iframe id="wg-app" src="https://www.saucedemo.com/"></iframe>
|
|
95
|
+
</div></div>
|
|
96
|
+
<div id="wg-gesture-banner">waygraph · orientation + gestures</div>
|
|
97
|
+
<div id="wg-cursor">${FINGER}</div>
|
|
98
|
+
<div id="wg-click-pulse"></div>
|
|
99
|
+
</body></html>`);
|
|
100
|
+
|
|
101
|
+
await page.waitForTimeout(2500);
|
|
102
|
+
|
|
103
|
+
async function toast(title, sub, preset) {
|
|
104
|
+
await page.evaluate(
|
|
105
|
+
({ title, sub, preset, icons }) => {
|
|
106
|
+
let t = document.getElementById("wg-device-toast");
|
|
107
|
+
if (!t) {
|
|
108
|
+
t = document.createElement("div");
|
|
109
|
+
t.id = "wg-device-toast";
|
|
110
|
+
document.body.appendChild(t);
|
|
111
|
+
}
|
|
112
|
+
t.dataset.preset = preset;
|
|
113
|
+
t.innerHTML =
|
|
114
|
+
'<span class="wg-dev-icon">' +
|
|
115
|
+
icons[preset] +
|
|
116
|
+
'</span><span><div class="wg-dev-title">' +
|
|
117
|
+
title +
|
|
118
|
+
'</div><div class="wg-dev-sub">' +
|
|
119
|
+
sub +
|
|
120
|
+
"</div></span>";
|
|
121
|
+
t.classList.remove("wg-in");
|
|
122
|
+
void t.offsetWidth;
|
|
123
|
+
t.classList.add("wg-in");
|
|
124
|
+
},
|
|
125
|
+
{ title, sub, preset, icons: ICONS },
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function setFrame(key) {
|
|
130
|
+
const p = PRESETS[key];
|
|
131
|
+
const title =
|
|
132
|
+
key.includes("Landscape") || p.orient === "landscape"
|
|
133
|
+
? p.preset === "desktop"
|
|
134
|
+
? "Back to desktop"
|
|
135
|
+
: "Rotated to landscape"
|
|
136
|
+
: key.includes("Portrait") || p.orient === "portrait"
|
|
137
|
+
? p.preset === "mobile" && key === "mobilePortrait"
|
|
138
|
+
? "Now in mobile mode"
|
|
139
|
+
: "Rotated to portrait"
|
|
140
|
+
: "Device updated";
|
|
141
|
+
// First switch: mobile portrait uses "Now in mobile"
|
|
142
|
+
const t =
|
|
143
|
+
key === "mobilePortrait"
|
|
144
|
+
? "Now in mobile mode"
|
|
145
|
+
: key === "desktop"
|
|
146
|
+
? "Back to desktop"
|
|
147
|
+
: key.includes("Landscape")
|
|
148
|
+
? "Rotated to landscape"
|
|
149
|
+
: key.includes("Portrait")
|
|
150
|
+
? key.startsWith("tablet")
|
|
151
|
+
? "Now in tablet mode"
|
|
152
|
+
: "Rotated to portrait"
|
|
153
|
+
: title;
|
|
154
|
+
|
|
155
|
+
await page.evaluate(
|
|
156
|
+
({ w, h, touch, finger, mouse }) => {
|
|
157
|
+
const frame = document.getElementById("wg-frame");
|
|
158
|
+
frame.style.width = w + "px";
|
|
159
|
+
frame.style.height = h + "px";
|
|
160
|
+
frame.style.borderRadius = touch ? (w < h ? "28px" : "18px") : "10px";
|
|
161
|
+
const c = document.getElementById("wg-cursor");
|
|
162
|
+
c.innerHTML = touch ? finger : mouse;
|
|
163
|
+
},
|
|
164
|
+
{ w: p.w, h: p.h, touch: p.touch, finger: FINGER, mouse: MOUSE },
|
|
165
|
+
);
|
|
166
|
+
await toast(t, `${p.w}×${p.h} · ${p.orient}${p.touch ? " · touch" : " · mouse"}`, p.preset);
|
|
167
|
+
await page.waitForTimeout(900);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function banner(text) {
|
|
171
|
+
await page.evaluate((text) => {
|
|
172
|
+
const b = document.getElementById("wg-gesture-banner");
|
|
173
|
+
if (b) b.textContent = text;
|
|
174
|
+
}, text);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function gestureAt(kind) {
|
|
178
|
+
// Aim at center of the device frame (login button-ish on saucedemo).
|
|
179
|
+
const box = await page.locator("#wg-frame").boundingBox();
|
|
180
|
+
if (!box) return;
|
|
181
|
+
const x = box.x + box.width * 0.5;
|
|
182
|
+
const y = box.y + box.height * 0.58;
|
|
183
|
+
await banner(
|
|
184
|
+
kind === "click"
|
|
185
|
+
? "Gesture: CLICK (mouse)"
|
|
186
|
+
: kind === "tap"
|
|
187
|
+
? "Gesture: TAP (touch)"
|
|
188
|
+
: "Gesture: HOLD (long-press)",
|
|
189
|
+
);
|
|
190
|
+
await page.evaluate(
|
|
191
|
+
({ x, y, kind, ms }) => {
|
|
192
|
+
const c = document.getElementById("wg-cursor");
|
|
193
|
+
c.style.setProperty("--wg-cursor-ms", ms + "ms");
|
|
194
|
+
c.style.opacity = "1";
|
|
195
|
+
c.style.transform = `translate(${x - 8}px,${y - 8}px)`;
|
|
196
|
+
},
|
|
197
|
+
{ x, y, kind, ms: 550 },
|
|
198
|
+
);
|
|
199
|
+
await page.waitForTimeout(600);
|
|
200
|
+
await page.evaluate(
|
|
201
|
+
({ x, y, kind }) => {
|
|
202
|
+
const p = document.getElementById("wg-click-pulse");
|
|
203
|
+
p.style.left = x + "px";
|
|
204
|
+
p.style.top = y + "px";
|
|
205
|
+
p.dataset.hold = kind === "hold" ? "1" : "0";
|
|
206
|
+
p.classList.remove("wg-pulse");
|
|
207
|
+
void p.offsetWidth;
|
|
208
|
+
p.classList.add("wg-pulse");
|
|
209
|
+
},
|
|
210
|
+
{ x, y, kind },
|
|
211
|
+
);
|
|
212
|
+
await page.waitForTimeout(kind === "hold" ? 900 : 550);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Sequence: portrait -> landscape (shows in square video) + gestures
|
|
216
|
+
await setFrame("mobilePortrait");
|
|
217
|
+
await page.waitForTimeout(1200);
|
|
218
|
+
await gestureAt("tap");
|
|
219
|
+
await page.waitForTimeout(700);
|
|
220
|
+
|
|
221
|
+
await setFrame("mobileLandscape");
|
|
222
|
+
await page.waitForTimeout(1400);
|
|
223
|
+
await gestureAt("hold");
|
|
224
|
+
await page.waitForTimeout(700);
|
|
225
|
+
|
|
226
|
+
await setFrame("tabletPortrait");
|
|
227
|
+
await page.waitForTimeout(1200);
|
|
228
|
+
await gestureAt("tap");
|
|
229
|
+
await page.waitForTimeout(600);
|
|
230
|
+
|
|
231
|
+
await setFrame("tabletLandscape");
|
|
232
|
+
await page.waitForTimeout(1400);
|
|
233
|
+
await gestureAt("click");
|
|
234
|
+
await page.waitForTimeout(800);
|
|
235
|
+
|
|
236
|
+
await setFrame("desktop");
|
|
237
|
+
await page.waitForTimeout(1500);
|
|
238
|
+
|
|
239
|
+
await banner("waygraph · orientation + gestures · done");
|
|
240
|
+
await page.waitForTimeout(1000);
|
|
241
|
+
|
|
242
|
+
const vid = page.video();
|
|
243
|
+
await context.close();
|
|
244
|
+
await browser.close();
|
|
245
|
+
|
|
246
|
+
let saved = null;
|
|
247
|
+
if (vid) {
|
|
248
|
+
const raw = await vid.path();
|
|
249
|
+
saved = join(outDir, "orientation-gestures.webm");
|
|
250
|
+
try {
|
|
251
|
+
copyFileSync(raw, saved);
|
|
252
|
+
} catch {
|
|
253
|
+
saved = raw;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
console.log("ORIENTATION_GESTURES_VIDEO", saved || outDir);
|
|
257
|
+
console.log(
|
|
258
|
+
"files",
|
|
259
|
+
readdirSync(outDir)
|
|
260
|
+
.filter((f) => f.endsWith(".webm"))
|
|
261
|
+
.join(", "),
|
|
262
|
+
);
|
|
@@ -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,
|