@the-portland-company/shell 0.60.1 → 0.62.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/app-chrome.cjs +80 -69
- package/dist/app-chrome.cjs.map +1 -1
- package/dist/app-chrome.js +2 -2
- package/dist/app-chrome.js.map +1 -1
- package/dist/{chunk-655XPQLJ.js → chunk-CFSA73PR.js} +81 -70
- package/dist/chunk-CFSA73PR.js.map +1 -0
- package/dist/native.cjs +79 -68
- package/dist/native.cjs.map +1 -1
- package/dist/native.d.cts +2 -1
- package/dist/native.d.ts +2 -1
- package/dist/native.js +1 -1
- package/package.json +3 -2
- package/dist/chunk-655XPQLJ.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 0.61.0 — 2026-09-30
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- **Spec drawer shows YOUR view on open.** Opening the drawer now rasterizes the current page in the browser (`modern-screenshot`, DOM → PNG, viewport size, drawer excluded) and sends it as the LIVE side of the compare — your org, data, theme and full frame. No click and no screen-share prompt (the 0.60.0 `getDisplayMedia` path is removed). Status is requested with `&theme=` so Specs renders the design in your theme; the active org name is sent for the label. "Recapture" re-runs it.
|
|
6
|
+
|
|
1
7
|
## 0.60.1 — 2026-09-30
|
|
2
8
|
|
|
3
9
|
### Fixed
|
package/dist/app-chrome.cjs
CHANGED
|
@@ -1386,57 +1386,59 @@ function currentChromeTheme() {
|
|
|
1386
1386
|
if (root.classList.contains("light") || root.getAttribute("data-theme") === "light") return "light";
|
|
1387
1387
|
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
1388
1388
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1389
|
+
var SPEC_OVERLAY_ROOT_ATTR = "data-spec-overlay-root";
|
|
1390
|
+
async function captureMyView(status, orgId, orgName) {
|
|
1391
|
+
const { domToCanvas } = await import('modern-screenshot');
|
|
1392
|
+
const w = window.innerWidth;
|
|
1393
|
+
const h = window.innerHeight;
|
|
1394
|
+
const theme = currentChromeTheme();
|
|
1395
|
+
const bg = getComputedStyle(document.body).backgroundColor || (theme === "dark" ? "#0b0b0c" : "#ffffff");
|
|
1396
|
+
const full = await domToCanvas(document.body, {
|
|
1397
|
+
width: w,
|
|
1398
|
+
height: h,
|
|
1399
|
+
scale: 1,
|
|
1400
|
+
backgroundColor: bg,
|
|
1401
|
+
// Cross-origin images that refuse CORS render blank instead of failing.
|
|
1402
|
+
fetch: { requestInit: { mode: "cors", credentials: "omit" }, placeholderImage: "" },
|
|
1403
|
+
// Drop the Spec drawer and everything fixed (sidebar, toasts, popovers):
|
|
1404
|
+
// the design is the page body only, so chrome would only add noise.
|
|
1405
|
+
filter: (node) => !(node instanceof Element) || !node.hasAttribute(SPEC_OVERLAY_ROOT_ATTR) && getComputedStyle(node).position !== "fixed",
|
|
1406
|
+
style: { transform: `translate(${-window.scrollX}px, ${-window.scrollY}px)` }
|
|
1397
1407
|
});
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
const h = window.innerHeight;
|
|
1406
|
-
const canvas = document.createElement("canvas");
|
|
1407
|
-
canvas.width = w;
|
|
1408
|
-
canvas.height = h;
|
|
1409
|
-
const ctx = canvas.getContext("2d");
|
|
1410
|
-
if (!ctx) throw new Error("canvas unavailable");
|
|
1411
|
-
ctx.drawImage(video, 0, 0, w, h);
|
|
1412
|
-
const blob = await new Promise((r) => canvas.toBlob(r, "image/png"));
|
|
1413
|
-
if (!blob) throw new Error("frame encode failed");
|
|
1414
|
-
let crop = null;
|
|
1415
|
-
const el = status.live_selector ? document.querySelector(status.live_selector) : null;
|
|
1416
|
-
if (el) {
|
|
1417
|
-
const b = el.getBoundingClientRect();
|
|
1418
|
-
const x = Math.max(0, Math.round(b.left));
|
|
1419
|
-
const y = Math.max(0, Math.round(b.top));
|
|
1420
|
-
crop = { x, y, width: Math.min(w - x, Math.round(b.width)), height: Math.min(h - y, Math.round(b.height)) };
|
|
1421
|
-
}
|
|
1422
|
-
const form = new FormData();
|
|
1423
|
-
form.append("image", blob, "live.png");
|
|
1424
|
-
form.append(
|
|
1425
|
-
"meta",
|
|
1426
|
-
JSON.stringify({ url: window.location.href, theme: currentChromeTheme(), org_id: orgId ?? null, crop })
|
|
1427
|
-
);
|
|
1428
|
-
const res = await fetch(status.capture_url ?? `${SPECS_ORIGIN}/api/overlay/capture`, {
|
|
1429
|
-
method: "POST",
|
|
1430
|
-
credentials: "include",
|
|
1431
|
-
headers: { "X-Spec-Capture": "1" },
|
|
1432
|
-
body: form
|
|
1433
|
-
});
|
|
1434
|
-
if (!res.ok) throw new Error(`capture ${res.status}: ${(await res.text()).slice(0, 160)}`);
|
|
1435
|
-
} finally {
|
|
1436
|
-
stream.getTracks().forEach((t) => t.stop());
|
|
1408
|
+
const el = (status.live_selector ? document.querySelector(status.live_selector) : null) ?? document.querySelector("[data-shell-content]");
|
|
1409
|
+
let region = { x: 0, y: 0, width: w, height: h };
|
|
1410
|
+
if (el) {
|
|
1411
|
+
const r = el.getBoundingClientRect();
|
|
1412
|
+
const x = Math.max(0, Math.round(r.left));
|
|
1413
|
+
const y = Math.max(0, Math.round(r.top));
|
|
1414
|
+
region = { x, y, width: Math.min(w - x, Math.round(r.width)), height: Math.min(h - y, Math.round(r.height)) };
|
|
1437
1415
|
}
|
|
1416
|
+
const out = document.createElement("canvas");
|
|
1417
|
+
out.width = region.width;
|
|
1418
|
+
out.height = region.height;
|
|
1419
|
+
const ctx = out.getContext("2d");
|
|
1420
|
+
if (!ctx) throw new Error("page raster failed");
|
|
1421
|
+
ctx.drawImage(full, region.x, region.y, region.width, region.height, 0, 0, region.width, region.height);
|
|
1422
|
+
const blob = await new Promise((resolve) => out.toBlob(resolve, "image/png"));
|
|
1423
|
+
if (!blob) throw new Error("page raster failed");
|
|
1424
|
+
const form = new FormData();
|
|
1425
|
+
form.append("image", blob, "live.png");
|
|
1426
|
+
form.append(
|
|
1427
|
+
"meta",
|
|
1428
|
+
JSON.stringify({ url: window.location.href, theme, org_id: orgId ?? null, org_name: orgName ?? null, crop: null })
|
|
1429
|
+
);
|
|
1430
|
+
const res = await fetch(status.capture_url ?? `${SPECS_ORIGIN}/api/overlay/capture`, {
|
|
1431
|
+
method: "POST",
|
|
1432
|
+
credentials: "include",
|
|
1433
|
+
headers: { "X-Spec-Capture": "1" },
|
|
1434
|
+
body: form
|
|
1435
|
+
});
|
|
1436
|
+
if (!res.ok) throw new Error(`capture ${res.status}: ${(await res.text()).slice(0, 160)}`);
|
|
1438
1437
|
}
|
|
1439
|
-
function
|
|
1438
|
+
function specOverlayStatusPath() {
|
|
1439
|
+
return `/api/overlay/status?url=${encodeURIComponent(window.location.href)}&theme=${currentChromeTheme()}`;
|
|
1440
|
+
}
|
|
1441
|
+
function SpecOverlayAction({ orgId, orgName } = {}) {
|
|
1440
1442
|
const [access, setAccess] = react.useState(() => readSpecOverlayAccessCache());
|
|
1441
1443
|
const [status, setStatus] = react.useState(null);
|
|
1442
1444
|
const [open, setOpen] = react.useState(false);
|
|
@@ -1464,37 +1466,41 @@ function SpecOverlayAction({ orgId } = {}) {
|
|
|
1464
1466
|
react.useEffect(() => {
|
|
1465
1467
|
if (!access?.allowed || typeof window === "undefined") return;
|
|
1466
1468
|
const timer = window.setTimeout(() => {
|
|
1467
|
-
|
|
1468
|
-
specOverlayFetchJson(url).then((data) => setStatus(data)).catch(() => setStatus(null));
|
|
1469
|
+
specOverlayFetchJson(specOverlayStatusPath()).then((data) => setStatus(data)).catch(() => setStatus(null));
|
|
1469
1470
|
}, 300);
|
|
1470
1471
|
return () => window.clearTimeout(timer);
|
|
1471
1472
|
}, [access?.allowed, href, orgId]);
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
const
|
|
1473
|
+
const captureRun = react.useRef(0);
|
|
1474
|
+
const runCapture = async (s) => {
|
|
1475
|
+
const run = ++captureRun.current;
|
|
1476
|
+
const before = s.live_source === "viewer" ? s.finished_at : null;
|
|
1475
1477
|
setCapture({ state: "busy" });
|
|
1476
|
-
setOpen(false);
|
|
1477
1478
|
try {
|
|
1478
|
-
await new Promise((r) => setTimeout(r,
|
|
1479
|
-
await captureMyView(
|
|
1480
|
-
setOpen(true);
|
|
1481
|
-
const url = `/api/overlay/status?url=${encodeURIComponent(window.location.href)}`;
|
|
1479
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1480
|
+
await captureMyView(s, orgId, orgName);
|
|
1482
1481
|
for (let i = 0; i < 45; i++) {
|
|
1483
|
-
await new Promise((r) => setTimeout(r,
|
|
1484
|
-
|
|
1485
|
-
|
|
1482
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
1483
|
+
if (run !== captureRun.current) return;
|
|
1484
|
+
const next = await specOverlayFetchJson(specOverlayStatusPath()).catch(() => null);
|
|
1485
|
+
if (next && next.matched && next.live_source === "viewer" && next.finished_at !== before) {
|
|
1486
1486
|
setStatus(next);
|
|
1487
1487
|
setFrameKey((k) => k + 1);
|
|
1488
1488
|
setCapture({ state: "idle" });
|
|
1489
1489
|
return;
|
|
1490
1490
|
}
|
|
1491
1491
|
}
|
|
1492
|
-
setCapture({ state: "error", message: "Compare did not finish in
|
|
1492
|
+
setCapture({ state: "error", message: "Compare did not finish in 2 min." });
|
|
1493
1493
|
} catch (e) {
|
|
1494
|
-
|
|
1495
|
-
setCapture({ state: "error", message: e instanceof Error ? e.message : String(e) });
|
|
1494
|
+
if (run === captureRun.current) setCapture({ state: "error", message: e instanceof Error ? e.message : String(e) });
|
|
1496
1495
|
}
|
|
1497
1496
|
};
|
|
1497
|
+
react.useEffect(() => {
|
|
1498
|
+
if (!open || !status || !status.matched) return;
|
|
1499
|
+
void runCapture(status);
|
|
1500
|
+
return () => {
|
|
1501
|
+
captureRun.current++;
|
|
1502
|
+
};
|
|
1503
|
+
}, [open, status && status.matched ? status.target_id : null]);
|
|
1498
1504
|
if (!access?.allowed) return null;
|
|
1499
1505
|
const badgeLabel = status && status.matched ? `${Math.round(status.mismatch_pct)}%` : status ? "\u2014" : void 0;
|
|
1500
1506
|
const badgeTone = status && status.matched ? status.passed ? "default" : "danger" : "default";
|
|
@@ -1534,6 +1540,7 @@ function SpecOverlayAction({ orgId } = {}) {
|
|
|
1534
1540
|
"div",
|
|
1535
1541
|
{
|
|
1536
1542
|
className: "fixed inset-0 z-50 flex justify-end bg-black/40",
|
|
1543
|
+
...{ [SPEC_OVERLAY_ROOT_ATTR]: "" },
|
|
1537
1544
|
role: "dialog",
|
|
1538
1545
|
"aria-modal": "true",
|
|
1539
1546
|
"aria-label": "Spec overlay",
|
|
@@ -1587,13 +1594,17 @@ function SpecOverlayAction({ orgId } = {}) {
|
|
|
1587
1594
|
{
|
|
1588
1595
|
type: "button",
|
|
1589
1596
|
disabled: capture.state === "busy",
|
|
1590
|
-
onClick:
|
|
1597
|
+
onClick: () => void runCapture(status),
|
|
1591
1598
|
className: "ml-auto rounded-md border border-[color:var(--line)] px-2 py-1 text-[color:var(--foreground)] hover:bg-[color:var(--panel)] disabled:opacity-60",
|
|
1592
|
-
title: "
|
|
1593
|
-
children: capture.state === "busy" ? "
|
|
1599
|
+
title: "Your view is captured when the drawer opens. Recapture after changing the page.",
|
|
1600
|
+
children: capture.state === "busy" ? "Capturing your view\u2026" : "Recapture"
|
|
1594
1601
|
}
|
|
1595
1602
|
)
|
|
1596
1603
|
] }),
|
|
1604
|
+
capture.state === "busy" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 border-b border-[color:var(--line)] px-4 py-2 text-xs text-[color:var(--muted)]", children: [
|
|
1605
|
+
/* @__PURE__ */ jsxRuntime.jsx(ValueSkeleton, { className: "h-3 w-3 rounded-full" }),
|
|
1606
|
+
"Capturing your view and comparing\u2026"
|
|
1607
|
+
] }) : null,
|
|
1597
1608
|
capture.state === "error" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "border-b border-[color:var(--line)] px-4 py-2 text-xs text-[color:var(--chrome-danger)]", children: capture.message }) : null,
|
|
1598
1609
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1599
1610
|
"iframe",
|
|
@@ -3318,7 +3329,7 @@ function NavAppShell({
|
|
|
3318
3329
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lg:pl-[240px]", children: [
|
|
3319
3330
|
/* @__PURE__ */ jsxRuntime.jsx(NavTopBar, { ...topBar, onOpenNav: () => setNavOpen(true), navOpen }),
|
|
3320
3331
|
betaBanner ? /* @__PURE__ */ jsxRuntime.jsx(NavBetaBanner, { ...betaBanner }) : null,
|
|
3321
|
-
contentWrapper === "bare" ? children : /* @__PURE__ */ jsxRuntime.jsx("main", { className: "px-6 py-7", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto max-w-7xl", children }) })
|
|
3332
|
+
contentWrapper === "bare" ? /* @__PURE__ */ jsxRuntime.jsx("div", { "data-shell-content": "", children }) : /* @__PURE__ */ jsxRuntime.jsx("main", { "data-shell-content": "", className: "px-6 py-7", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto max-w-7xl", children }) })
|
|
3322
3333
|
] })
|
|
3323
3334
|
]
|
|
3324
3335
|
}
|
|
@@ -4673,7 +4684,7 @@ function ChromeTopBarActions() {
|
|
|
4673
4684
|
children: /* @__PURE__ */ jsxRuntime.jsx(ChromeDevNotesMenu, {})
|
|
4674
4685
|
}
|
|
4675
4686
|
),
|
|
4676
|
-
/* @__PURE__ */ jsxRuntime.jsx(SpecOverlayAction, { orgId: state.orgId }),
|
|
4687
|
+
/* @__PURE__ */ jsxRuntime.jsx(SpecOverlayAction, { orgId: state.orgId, orgName: state.orgs.find((o) => o.id === state.orgId)?.name ?? null }),
|
|
4677
4688
|
/* @__PURE__ */ jsxRuntime.jsx(SupportAction, { orgId: state.orgId }),
|
|
4678
4689
|
/* @__PURE__ */ jsxRuntime.jsx(NotificationsAction, { orgId: state.orgId })
|
|
4679
4690
|
] });
|