waygraph 0.13.1 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +216 -44
- 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;}" +
|
|
@@ -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,158 @@ async function installOverlay(page, title) {
|
|
|
1306
1331
|
"<ellipse cx='16' cy='22' rx='7' ry='8' fill='#0C0C1A' stroke='#fff' stroke-width='1.4'/>" +
|
|
1307
1332
|
"<rect x='12' y='6' width='8' height='16' rx='4' fill='#0C0C1A' stroke='#fff' stroke-width='1.4'/>" +
|
|
1308
1333
|
"</svg>";
|
|
1309
|
-
|
|
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) => {
|
|
1359
|
+
let badge = document.getElementById("wg-device-badge");
|
|
1360
|
+
if (preset === "desktop" && !touch) {
|
|
1361
|
+
if (badge) {
|
|
1362
|
+
badge.classList.remove("wg-in");
|
|
1363
|
+
setTimeout(() => {
|
|
1364
|
+
const b = document.getElementById("wg-device-badge");
|
|
1365
|
+
if (b) b.remove();
|
|
1366
|
+
}, 320);
|
|
1367
|
+
}
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
if (!badge) {
|
|
1371
|
+
badge = document.createElement("div");
|
|
1372
|
+
badge.id = "wg-device-badge";
|
|
1373
|
+
badge.setAttribute("data-wg-ui", "1");
|
|
1374
|
+
document.documentElement.appendChild(badge);
|
|
1375
|
+
}
|
|
1376
|
+
badge.dataset.preset = preset;
|
|
1377
|
+
badge.dataset.touch = touch ? "1" : "0";
|
|
1378
|
+
const chipLabel =
|
|
1379
|
+
preset === "desktop" && !touch
|
|
1380
|
+
? "desktop"
|
|
1381
|
+
: preset + (remain ? " · remain" : "");
|
|
1382
|
+
badge.innerHTML =
|
|
1383
|
+
'<span class="wg-dev-icon">' +
|
|
1384
|
+
(ICONS[preset] || ICONS.desktop) +
|
|
1385
|
+
'</span><span class="wg-dev-label">' +
|
|
1386
|
+
chipLabel +
|
|
1387
|
+
"</span>";
|
|
1388
|
+
void badge.offsetWidth;
|
|
1389
|
+
badge.classList.add("wg-in");
|
|
1390
|
+
};
|
|
1391
|
+
const showToast = (preset, touch, title, sub) => {
|
|
1392
|
+
let toast = document.getElementById("wg-device-toast");
|
|
1393
|
+
if (toast && toast._wgTimer) {
|
|
1394
|
+
clearTimeout(toast._wgTimer);
|
|
1395
|
+
toast._wgTimer = null;
|
|
1396
|
+
}
|
|
1397
|
+
if (!toast) {
|
|
1398
|
+
toast = document.createElement("div");
|
|
1399
|
+
toast.id = "wg-device-toast";
|
|
1400
|
+
toast.setAttribute("data-wg-ui", "1");
|
|
1401
|
+
document.documentElement.appendChild(toast);
|
|
1402
|
+
}
|
|
1403
|
+
// Hide chip while toast is up (same corner).
|
|
1404
|
+
const badge = document.getElementById("wg-device-badge");
|
|
1405
|
+
if (badge) badge.classList.remove("wg-in");
|
|
1406
|
+
toast.dataset.preset = preset;
|
|
1407
|
+
toast.innerHTML =
|
|
1408
|
+
'<span class="wg-dev-icon">' +
|
|
1409
|
+
(ICONS[preset] || ICONS.desktop) +
|
|
1410
|
+
'</span><span class="wg-dev-copy"><div class="wg-dev-title">' +
|
|
1411
|
+
title +
|
|
1412
|
+
'</div><div class="wg-dev-sub">' +
|
|
1413
|
+
sub +
|
|
1414
|
+
"</div></span>";
|
|
1415
|
+
toast.classList.remove("wg-out");
|
|
1416
|
+
void toast.offsetWidth;
|
|
1417
|
+
toast.classList.add("wg-in");
|
|
1418
|
+
toast._wgTimer = setTimeout(() => {
|
|
1419
|
+
toast.classList.add("wg-out");
|
|
1420
|
+
toast.classList.remove("wg-in");
|
|
1421
|
+
setTimeout(() => {
|
|
1422
|
+
const t = document.getElementById("wg-device-toast");
|
|
1423
|
+
if (t) t.remove();
|
|
1424
|
+
if (preset !== "desktop" || touch) {
|
|
1425
|
+
ensureChip(preset, touch, true);
|
|
1426
|
+
}
|
|
1427
|
+
}, 380);
|
|
1428
|
+
}, 2200);
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1431
|
+
if (sync === "keep" && !device) return;
|
|
1432
|
+
|
|
1433
|
+
const isDesktopClear =
|
|
1434
|
+
sync === "clear" ||
|
|
1435
|
+
(device && device.preset === "desktop" && !device.touchMode);
|
|
1436
|
+
if (isDesktopClear) {
|
|
1437
|
+
setCursor(false);
|
|
1438
|
+
const announce = payload.announce !== false && sync !== "keep";
|
|
1439
|
+
if (announce) {
|
|
1440
|
+
const vp = device && device.viewport ? device.viewport : { width: 1280, height: 720 };
|
|
1441
|
+
showToast(
|
|
1442
|
+
"desktop",
|
|
1443
|
+
false,
|
|
1444
|
+
"Back to desktop",
|
|
1445
|
+
vp.width + "\u00d7" + vp.height + " \u00b7 mouse",
|
|
1446
|
+
);
|
|
1447
|
+
setTimeout(() => {
|
|
1448
|
+
const badge = document.getElementById("wg-device-badge");
|
|
1449
|
+
if (badge) badge.remove();
|
|
1450
|
+
}, 2600);
|
|
1451
|
+
} else {
|
|
1452
|
+
const badge = document.getElementById("wg-device-badge");
|
|
1453
|
+
if (badge) badge.remove();
|
|
1454
|
+
const toast = document.getElementById("wg-device-toast");
|
|
1455
|
+
if (toast) toast.remove();
|
|
1456
|
+
}
|
|
1457
|
+
window.__wgDevicePreset = "desktop";
|
|
1458
|
+
return;
|
|
1319
1459
|
}
|
|
1460
|
+
|
|
1320
1461
|
if (!device) return;
|
|
1321
1462
|
const preset = device.preset || "desktop";
|
|
1322
1463
|
const touch = !!device.touchMode;
|
|
1323
1464
|
const remain = payload.remain !== false && sync !== "clear";
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
}
|
|
1465
|
+
const prev = window.__wgDevicePreset || "";
|
|
1466
|
+
const changed = prev !== preset || (touch && prev === "desktop");
|
|
1467
|
+
setCursor(touch);
|
|
1468
|
+
window.__wgDevicePreset = preset;
|
|
1469
|
+
|
|
1470
|
+
const announce =
|
|
1471
|
+
payload.announce === true ||
|
|
1472
|
+
(payload.announce !== false && (sync === "set" || sync === "clear") && changed);
|
|
1473
|
+
|
|
1474
|
+
const vp = device.viewport || {};
|
|
1475
|
+
const size =
|
|
1476
|
+
(vp.width || "?") + "\u00d7" + (vp.height || "?") + (touch ? " \u00b7 touch" : "");
|
|
1477
|
+
const titles = {
|
|
1478
|
+
mobile: "Now in mobile mode",
|
|
1479
|
+
tablet: "Now in tablet mode",
|
|
1480
|
+
desktop: "Now in desktop mode",
|
|
1481
|
+
};
|
|
1482
|
+
if (announce) {
|
|
1483
|
+
showToast(preset, touch, titles[preset] || "Device updated", size);
|
|
1484
|
+
} else {
|
|
1485
|
+
ensureChip(preset, touch, remain);
|
|
1346
1486
|
}
|
|
1347
1487
|
};
|
|
1348
1488
|
},
|
|
@@ -2776,8 +2916,7 @@ async function clickPulseAt(page, x, y, tone) {
|
|
|
2776
2916
|
|
|
2777
2917
|
/**
|
|
2778
2918
|
* Apply authored device fixture to the live page (0.13+).
|
|
2779
|
-
*
|
|
2780
|
-
* sync keep with no device = no-op; clear / desktop = restore main feel.
|
|
2919
|
+
* Seamless viewport lerp + toast on set/clear (quiet chip recreate on keep).
|
|
2781
2920
|
*/
|
|
2782
2921
|
async function applyDeviceToPage(page, device, sync) {
|
|
2783
2922
|
const mode = sync || (device ? "set" : "keep");
|
|
@@ -2795,14 +2934,46 @@ async function applyDeviceToPage(page, device, sync) {
|
|
|
2795
2934
|
typeof page.__wgRealSetViewportSize === "function"
|
|
2796
2935
|
? page.__wgRealSetViewportSize
|
|
2797
2936
|
: page.setViewportSize.bind(page);
|
|
2937
|
+
const target = {
|
|
2938
|
+
width: Math.max(200, Math.floor(d.viewport.width)),
|
|
2939
|
+
height: Math.max(200, Math.floor(d.viewport.height)),
|
|
2940
|
+
};
|
|
2941
|
+
let from = null;
|
|
2798
2942
|
try {
|
|
2799
|
-
|
|
2800
|
-
width: d.viewport.width,
|
|
2801
|
-
height: d.viewport.height,
|
|
2802
|
-
});
|
|
2943
|
+
from = page.viewportSize();
|
|
2803
2944
|
} catch {
|
|
2804
|
-
|
|
2945
|
+
from = null;
|
|
2805
2946
|
}
|
|
2947
|
+
const announce = mode === "set" || mode === "clear";
|
|
2948
|
+
const shouldAnimate =
|
|
2949
|
+
announce &&
|
|
2950
|
+
from &&
|
|
2951
|
+
from.width > 0 &&
|
|
2952
|
+
from.height > 0 &&
|
|
2953
|
+
(from.width !== target.width || from.height !== target.height);
|
|
2954
|
+
|
|
2955
|
+
try {
|
|
2956
|
+
if (shouldAnimate) {
|
|
2957
|
+
const steps = 14;
|
|
2958
|
+
const ms = Number(process.env.WAYGRAPH_DEVICE_TRANSITION_MS || 520);
|
|
2959
|
+
const stepMs = Math.max(12, Math.floor(ms / steps));
|
|
2960
|
+
for (let i = 1; i <= steps; i++) {
|
|
2961
|
+
const t = i / steps;
|
|
2962
|
+
const e = t * t * (3 - 2 * t); // smoothstep
|
|
2963
|
+
await realSet({
|
|
2964
|
+
width: Math.round(from.width + (target.width - from.width) * e),
|
|
2965
|
+
height: Math.round(from.height + (target.height - from.height) * e),
|
|
2966
|
+
});
|
|
2967
|
+
if (i < steps) await new Promise((r) => setTimeout(r, stepMs));
|
|
2968
|
+
}
|
|
2969
|
+
} else {
|
|
2970
|
+
await realSet(target);
|
|
2971
|
+
}
|
|
2972
|
+
} catch {
|
|
2973
|
+
/* maximized / null viewport hosts may reject - toast still updates */
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
// Fire toast near the end of the lerp so it reads as one seamless beat.
|
|
2806
2977
|
await page
|
|
2807
2978
|
.evaluate(
|
|
2808
2979
|
(payload) => {
|
|
@@ -2812,6 +2983,7 @@ async function applyDeviceToPage(page, device, sync) {
|
|
|
2812
2983
|
sync: mode,
|
|
2813
2984
|
device: d,
|
|
2814
2985
|
remain: mode === "keep" || mode === "set",
|
|
2986
|
+
announce,
|
|
2815
2987
|
},
|
|
2816
2988
|
)
|
|
2817
2989
|
.catch(() => {});
|
|
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,
|