@the-portland-company/shell 0.60.1 → 0.61.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 +71 -68
- 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-G5DXKB3R.js} +72 -69
- package/dist/chunk-G5DXKB3R.js.map +1 -0
- package/dist/native.cjs +70 -67
- 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,51 @@ 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 { domToBlob } = 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 blob = await domToBlob(document.body, {
|
|
1397
|
+
width: w,
|
|
1398
|
+
height: h,
|
|
1399
|
+
scale: 1,
|
|
1400
|
+
backgroundColor: bg,
|
|
1401
|
+
type: "image/png",
|
|
1402
|
+
// Cross-origin images that refuse CORS render blank instead of failing.
|
|
1403
|
+
fetch: { requestInit: { mode: "cors", credentials: "omit" }, placeholderImage: "" },
|
|
1404
|
+
filter: (node) => !(node instanceof Element && node.hasAttribute(SPEC_OVERLAY_ROOT_ATTR)),
|
|
1405
|
+
style: { transform: `translate(${-window.scrollX}px, ${-window.scrollY}px)` }
|
|
1397
1406
|
});
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
const
|
|
1405
|
-
|
|
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());
|
|
1407
|
+
if (!blob) throw new Error("page raster failed");
|
|
1408
|
+
let crop = null;
|
|
1409
|
+
const el = status.live_selector ? document.querySelector(status.live_selector) : null;
|
|
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
|
+
crop = { x, y, width: Math.min(w - x, Math.round(r.width)), height: Math.min(h - y, Math.round(r.height)) };
|
|
1437
1415
|
}
|
|
1416
|
+
const form = new FormData();
|
|
1417
|
+
form.append("image", blob, "live.png");
|
|
1418
|
+
form.append(
|
|
1419
|
+
"meta",
|
|
1420
|
+
JSON.stringify({ url: window.location.href, theme, org_id: orgId ?? null, org_name: orgName ?? null, crop })
|
|
1421
|
+
);
|
|
1422
|
+
const res = await fetch(status.capture_url ?? `${SPECS_ORIGIN}/api/overlay/capture`, {
|
|
1423
|
+
method: "POST",
|
|
1424
|
+
credentials: "include",
|
|
1425
|
+
headers: { "X-Spec-Capture": "1" },
|
|
1426
|
+
body: form
|
|
1427
|
+
});
|
|
1428
|
+
if (!res.ok) throw new Error(`capture ${res.status}: ${(await res.text()).slice(0, 160)}`);
|
|
1438
1429
|
}
|
|
1439
|
-
function
|
|
1430
|
+
function specOverlayStatusPath() {
|
|
1431
|
+
return `/api/overlay/status?url=${encodeURIComponent(window.location.href)}&theme=${currentChromeTheme()}`;
|
|
1432
|
+
}
|
|
1433
|
+
function SpecOverlayAction({ orgId, orgName } = {}) {
|
|
1440
1434
|
const [access, setAccess] = react.useState(() => readSpecOverlayAccessCache());
|
|
1441
1435
|
const [status, setStatus] = react.useState(null);
|
|
1442
1436
|
const [open, setOpen] = react.useState(false);
|
|
@@ -1464,37 +1458,41 @@ function SpecOverlayAction({ orgId } = {}) {
|
|
|
1464
1458
|
react.useEffect(() => {
|
|
1465
1459
|
if (!access?.allowed || typeof window === "undefined") return;
|
|
1466
1460
|
const timer = window.setTimeout(() => {
|
|
1467
|
-
|
|
1468
|
-
specOverlayFetchJson(url).then((data) => setStatus(data)).catch(() => setStatus(null));
|
|
1461
|
+
specOverlayFetchJson(specOverlayStatusPath()).then((data) => setStatus(data)).catch(() => setStatus(null));
|
|
1469
1462
|
}, 300);
|
|
1470
1463
|
return () => window.clearTimeout(timer);
|
|
1471
1464
|
}, [access?.allowed, href, orgId]);
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
const
|
|
1465
|
+
const captureRun = react.useRef(0);
|
|
1466
|
+
const runCapture = async (s) => {
|
|
1467
|
+
const run = ++captureRun.current;
|
|
1468
|
+
const before = s.live_source === "viewer" ? s.finished_at : null;
|
|
1475
1469
|
setCapture({ state: "busy" });
|
|
1476
|
-
setOpen(false);
|
|
1477
1470
|
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)}`;
|
|
1471
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1472
|
+
await captureMyView(s, orgId, orgName);
|
|
1482
1473
|
for (let i = 0; i < 45; i++) {
|
|
1483
|
-
await new Promise((r) => setTimeout(r,
|
|
1484
|
-
|
|
1485
|
-
|
|
1474
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
1475
|
+
if (run !== captureRun.current) return;
|
|
1476
|
+
const next = await specOverlayFetchJson(specOverlayStatusPath()).catch(() => null);
|
|
1477
|
+
if (next && next.matched && next.live_source === "viewer" && next.finished_at !== before) {
|
|
1486
1478
|
setStatus(next);
|
|
1487
1479
|
setFrameKey((k) => k + 1);
|
|
1488
1480
|
setCapture({ state: "idle" });
|
|
1489
1481
|
return;
|
|
1490
1482
|
}
|
|
1491
1483
|
}
|
|
1492
|
-
setCapture({ state: "error", message: "Compare did not finish in
|
|
1484
|
+
setCapture({ state: "error", message: "Compare did not finish in 2 min." });
|
|
1493
1485
|
} catch (e) {
|
|
1494
|
-
|
|
1495
|
-
setCapture({ state: "error", message: e instanceof Error ? e.message : String(e) });
|
|
1486
|
+
if (run === captureRun.current) setCapture({ state: "error", message: e instanceof Error ? e.message : String(e) });
|
|
1496
1487
|
}
|
|
1497
1488
|
};
|
|
1489
|
+
react.useEffect(() => {
|
|
1490
|
+
if (!open || !status || !status.matched) return;
|
|
1491
|
+
void runCapture(status);
|
|
1492
|
+
return () => {
|
|
1493
|
+
captureRun.current++;
|
|
1494
|
+
};
|
|
1495
|
+
}, [open, status && status.matched ? status.target_id : null]);
|
|
1498
1496
|
if (!access?.allowed) return null;
|
|
1499
1497
|
const badgeLabel = status && status.matched ? `${Math.round(status.mismatch_pct)}%` : status ? "\u2014" : void 0;
|
|
1500
1498
|
const badgeTone = status && status.matched ? status.passed ? "default" : "danger" : "default";
|
|
@@ -1534,6 +1532,7 @@ function SpecOverlayAction({ orgId } = {}) {
|
|
|
1534
1532
|
"div",
|
|
1535
1533
|
{
|
|
1536
1534
|
className: "fixed inset-0 z-50 flex justify-end bg-black/40",
|
|
1535
|
+
...{ [SPEC_OVERLAY_ROOT_ATTR]: "" },
|
|
1537
1536
|
role: "dialog",
|
|
1538
1537
|
"aria-modal": "true",
|
|
1539
1538
|
"aria-label": "Spec overlay",
|
|
@@ -1587,13 +1586,17 @@ function SpecOverlayAction({ orgId } = {}) {
|
|
|
1587
1586
|
{
|
|
1588
1587
|
type: "button",
|
|
1589
1588
|
disabled: capture.state === "busy",
|
|
1590
|
-
onClick:
|
|
1589
|
+
onClick: () => void runCapture(status),
|
|
1591
1590
|
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" ? "
|
|
1591
|
+
title: "Your view is captured when the drawer opens. Recapture after changing the page.",
|
|
1592
|
+
children: capture.state === "busy" ? "Capturing your view\u2026" : "Recapture"
|
|
1594
1593
|
}
|
|
1595
1594
|
)
|
|
1596
1595
|
] }),
|
|
1596
|
+
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: [
|
|
1597
|
+
/* @__PURE__ */ jsxRuntime.jsx(ValueSkeleton, { className: "h-3 w-3 rounded-full" }),
|
|
1598
|
+
"Capturing your view and comparing\u2026"
|
|
1599
|
+
] }) : null,
|
|
1597
1600
|
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
1601
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1599
1602
|
"iframe",
|
|
@@ -4673,7 +4676,7 @@ function ChromeTopBarActions() {
|
|
|
4673
4676
|
children: /* @__PURE__ */ jsxRuntime.jsx(ChromeDevNotesMenu, {})
|
|
4674
4677
|
}
|
|
4675
4678
|
),
|
|
4676
|
-
/* @__PURE__ */ jsxRuntime.jsx(SpecOverlayAction, { orgId: state.orgId }),
|
|
4679
|
+
/* @__PURE__ */ jsxRuntime.jsx(SpecOverlayAction, { orgId: state.orgId, orgName: state.orgs.find((o) => o.id === state.orgId)?.name ?? null }),
|
|
4677
4680
|
/* @__PURE__ */ jsxRuntime.jsx(SupportAction, { orgId: state.orgId }),
|
|
4678
4681
|
/* @__PURE__ */ jsxRuntime.jsx(NotificationsAction, { orgId: state.orgId })
|
|
4679
4682
|
] });
|