@the-portland-company/shell 0.59.0 → 0.60.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 +12 -0
- package/dist/app-chrome.cjs +138 -34
- package/dist/app-chrome.cjs.map +1 -1
- package/dist/app-chrome.js +1 -1
- package/dist/chrome.css +4 -0
- package/dist/{chunk-BV2QF7NY.js → chunk-655XPQLJ.js} +140 -36
- package/dist/chunk-655XPQLJ.js.map +1 -0
- package/dist/native.cjs +138 -34
- package/dist/native.cjs.map +1 -1
- package/dist/native.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-BV2QF7NY.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## 0.60.1 — 2026-09-30
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **Meta footer contrast (WCAG 2.2 AA).** The commit / version / date / author rows inherited `--muted` (#718096, 4.02:1 on the light rail) and, since 0.58.0 renders Meta for every viewer, tripped Petitions' contrast regression gate (+4 fails per route). New token `--sidebar-meta-text` (light #696D72 = 5.2:1, dark #A3A3A3 = 7.2:1) scoped to the Meta block (`data-shell-meta`); separators and the env chip unchanged.
|
|
6
|
+
|
|
7
|
+
## 0.60.0 — 2026-09-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Spec overlay: Capture my view** — compare the design against this tab (own org/theme) (#79).
|
|
12
|
+
|
|
1
13
|
## 0.59.0 — 2026-09-30
|
|
2
14
|
|
|
3
15
|
### Fixed — parent nav row stays active on child routes when the 'tabs' nav preference strips children (`src/app-chrome/frame.tsx`, DevNotes ebb036a1)
|
package/dist/app-chrome.cjs
CHANGED
|
@@ -1380,10 +1380,68 @@ function specOverlayFetchJson(path) {
|
|
|
1380
1380
|
return res.json();
|
|
1381
1381
|
});
|
|
1382
1382
|
}
|
|
1383
|
+
function currentChromeTheme() {
|
|
1384
|
+
const root = document.documentElement;
|
|
1385
|
+
if (root.classList.contains("dark") || root.getAttribute("data-theme") === "dark") return "dark";
|
|
1386
|
+
if (root.classList.contains("light") || root.getAttribute("data-theme") === "light") return "light";
|
|
1387
|
+
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
1388
|
+
}
|
|
1389
|
+
async function captureMyView(status, orgId) {
|
|
1390
|
+
const md = navigator.mediaDevices;
|
|
1391
|
+
const stream = await md.getDisplayMedia({
|
|
1392
|
+
video: { displaySurface: "browser" },
|
|
1393
|
+
audio: false,
|
|
1394
|
+
preferCurrentTab: true,
|
|
1395
|
+
selfBrowserSurface: "include",
|
|
1396
|
+
surfaceSwitching: "exclude"
|
|
1397
|
+
});
|
|
1398
|
+
try {
|
|
1399
|
+
const video = document.createElement("video");
|
|
1400
|
+
video.muted = true;
|
|
1401
|
+
video.srcObject = stream;
|
|
1402
|
+
await video.play();
|
|
1403
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
1404
|
+
const w = window.innerWidth;
|
|
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());
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1383
1439
|
function SpecOverlayAction({ orgId } = {}) {
|
|
1384
1440
|
const [access, setAccess] = react.useState(() => readSpecOverlayAccessCache());
|
|
1385
1441
|
const [status, setStatus] = react.useState(null);
|
|
1386
1442
|
const [open, setOpen] = react.useState(false);
|
|
1443
|
+
const [capture, setCapture] = react.useState({ state: "idle" });
|
|
1444
|
+
const [frameKey, setFrameKey] = react.useState(0);
|
|
1387
1445
|
const href = useLocationHref();
|
|
1388
1446
|
react.useEffect(() => {
|
|
1389
1447
|
if (access) return;
|
|
@@ -1411,6 +1469,32 @@ function SpecOverlayAction({ orgId } = {}) {
|
|
|
1411
1469
|
}, 300);
|
|
1412
1470
|
return () => window.clearTimeout(timer);
|
|
1413
1471
|
}, [access?.allowed, href, orgId]);
|
|
1472
|
+
const onCaptureMyView = async () => {
|
|
1473
|
+
if (!status || !status.matched) return;
|
|
1474
|
+
const before = status.finished_at;
|
|
1475
|
+
setCapture({ state: "busy" });
|
|
1476
|
+
setOpen(false);
|
|
1477
|
+
try {
|
|
1478
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
1479
|
+
await captureMyView(status, orgId);
|
|
1480
|
+
setOpen(true);
|
|
1481
|
+
const url = `/api/overlay/status?url=${encodeURIComponent(window.location.href)}`;
|
|
1482
|
+
for (let i = 0; i < 45; i++) {
|
|
1483
|
+
await new Promise((r) => setTimeout(r, 4e3));
|
|
1484
|
+
const next = await specOverlayFetchJson(url).catch(() => null);
|
|
1485
|
+
if (next && next.matched && next.finished_at !== before) {
|
|
1486
|
+
setStatus(next);
|
|
1487
|
+
setFrameKey((k) => k + 1);
|
|
1488
|
+
setCapture({ state: "idle" });
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
setCapture({ state: "error", message: "Compare did not finish in 3 min." });
|
|
1493
|
+
} catch (e) {
|
|
1494
|
+
setOpen(true);
|
|
1495
|
+
setCapture({ state: "error", message: e instanceof Error ? e.message : String(e) });
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1414
1498
|
if (!access?.allowed) return null;
|
|
1415
1499
|
const badgeLabel = status && status.matched ? `${Math.round(status.mismatch_pct)}%` : status ? "\u2014" : void 0;
|
|
1416
1500
|
const badgeTone = status && status.matched ? status.passed ? "default" : "danger" : "default";
|
|
@@ -1497,15 +1581,28 @@ function SpecOverlayAction({ orgId } = {}) {
|
|
|
1497
1581
|
className: "text-[color:var(--color-accent)] hover:underline",
|
|
1498
1582
|
children: "Compare tab \u2197"
|
|
1499
1583
|
}
|
|
1584
|
+
),
|
|
1585
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1586
|
+
"button",
|
|
1587
|
+
{
|
|
1588
|
+
type: "button",
|
|
1589
|
+
disabled: capture.state === "busy",
|
|
1590
|
+
onClick: onCaptureMyView,
|
|
1591
|
+
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: "Compare the design against this tab exactly as you see it (your org, data and theme).",
|
|
1593
|
+
children: capture.state === "busy" ? "Comparing\u2026" : "Capture my view"
|
|
1594
|
+
}
|
|
1500
1595
|
)
|
|
1501
1596
|
] }),
|
|
1597
|
+
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,
|
|
1502
1598
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1503
1599
|
"iframe",
|
|
1504
1600
|
{
|
|
1505
1601
|
src: status.overlay_url,
|
|
1506
1602
|
title: "Spec overlay",
|
|
1507
1603
|
className: "h-full w-full flex-1 border-0"
|
|
1508
|
-
}
|
|
1604
|
+
},
|
|
1605
|
+
frameKey
|
|
1509
1606
|
)
|
|
1510
1607
|
] }) : status && !status.matched ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 text-sm text-[color:var(--muted)]", children: [
|
|
1511
1608
|
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "No compare target covers this page." }),
|
|
@@ -2389,39 +2486,46 @@ function NavSidebar({
|
|
|
2389
2486
|
}
|
|
2390
2487
|
)
|
|
2391
2488
|
] }, l.label)) }) : null,
|
|
2392
|
-
footer.envLabel ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2489
|
+
footer.envLabel ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2490
|
+
"div",
|
|
2491
|
+
{
|
|
2492
|
+
"data-shell-meta": true,
|
|
2493
|
+
className: "mt-1.5 flex flex-col items-center gap-0.5 font-[family-name:var(--font-plex-mono)] text-[10px] text-[color:var(--sidebar-meta-text,var(--sidebar-text))]",
|
|
2494
|
+
children: [
|
|
2495
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
2496
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2497
|
+
"span",
|
|
2498
|
+
{
|
|
2499
|
+
className: [
|
|
2500
|
+
"rounded-md px-1.5 py-0.5 text-[10px] font-semibold",
|
|
2501
|
+
envChipClasses(footer.envLabel)
|
|
2502
|
+
].join(" "),
|
|
2503
|
+
children: footer.envLabel
|
|
2504
|
+
}
|
|
2505
|
+
),
|
|
2506
|
+
footer.commit ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2507
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[color:var(--muted)]", children: "|" }),
|
|
2508
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: footer.commit.slice(-4) })
|
|
2509
|
+
] }) : null,
|
|
2510
|
+
footer.version ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2511
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[color:var(--muted)]", children: "|" }),
|
|
2512
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
2513
|
+
"v",
|
|
2514
|
+
footer.version
|
|
2515
|
+
] })
|
|
2516
|
+
] }) : null
|
|
2517
|
+
] }),
|
|
2518
|
+
footer.commitDate || footer.commitAuthor ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
2519
|
+
footer.commitDate ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: new Date(footer.commitDate).toLocaleDateString(void 0, {
|
|
2520
|
+
month: "short",
|
|
2521
|
+
day: "numeric"
|
|
2522
|
+
}) }) : null,
|
|
2523
|
+
footer.commitDate && footer.commitAuthor ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[color:var(--muted)]", children: "|" }) : null,
|
|
2524
|
+
footer.commitAuthor ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: footer.commitAuthor.split(/\s+/)[0] }) : null
|
|
2525
|
+
] }) : null
|
|
2526
|
+
]
|
|
2527
|
+
}
|
|
2528
|
+
) : null
|
|
2425
2529
|
] }) : null
|
|
2426
2530
|
]
|
|
2427
2531
|
}
|