@the-portland-company/shell 0.61.1 → 0.63.0

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.
@@ -1388,36 +1388,44 @@ function currentChromeTheme() {
1388
1388
  }
1389
1389
  var SPEC_OVERLAY_ROOT_ATTR = "data-spec-overlay-root";
1390
1390
  async function captureMyView(status, orgId, orgName) {
1391
- const { domToBlob } = await import('modern-screenshot');
1391
+ const { domToCanvas } = await import('modern-screenshot');
1392
1392
  const w = window.innerWidth;
1393
1393
  const h = window.innerHeight;
1394
1394
  const theme = currentChromeTheme();
1395
1395
  const bg = getComputedStyle(document.body).backgroundColor || (theme === "dark" ? "#0b0b0c" : "#ffffff");
1396
- const blob = await domToBlob(document.body, {
1396
+ const full = await domToCanvas(document.body, {
1397
1397
  width: w,
1398
1398
  height: h,
1399
1399
  scale: 1,
1400
1400
  backgroundColor: bg,
1401
- type: "image/png",
1402
1401
  // Cross-origin images that refuse CORS render blank instead of failing.
1403
1402
  fetch: { requestInit: { mode: "cors", credentials: "omit" }, placeholderImage: "" },
1404
- filter: (node) => !(node instanceof Element && node.hasAttribute(SPEC_OVERLAY_ROOT_ATTR)),
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",
1405
1406
  style: { transform: `translate(${-window.scrollX}px, ${-window.scrollY}px)` }
1406
1407
  });
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;
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
1410
  if (el) {
1411
1411
  const r = el.getBoundingClientRect();
1412
1412
  const x = Math.max(0, Math.round(r.left));
1413
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)) };
1414
+ region = { x, y, width: Math.min(w - x, Math.round(r.width)), height: Math.min(h - y, Math.round(r.height)) };
1415
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");
1416
1424
  const form = new FormData();
1417
1425
  form.append("image", blob, "live.png");
1418
1426
  form.append(
1419
1427
  "meta",
1420
- JSON.stringify({ url: window.location.href, theme, org_id: orgId ?? null, org_name: orgName ?? null, crop })
1428
+ JSON.stringify({ url: window.location.href, theme, org_id: orgId ?? null, org_name: orgName ?? null, crop: null })
1421
1429
  );
1422
1430
  const res = await fetch(status.capture_url ?? `${SPECS_ORIGIN}/api/overlay/capture`, {
1423
1431
  method: "POST",
@@ -1427,6 +1435,24 @@ async function captureMyView(status, orgId, orgName) {
1427
1435
  });
1428
1436
  if (!res.ok) throw new Error(`capture ${res.status}: ${(await res.text()).slice(0, 160)}`);
1429
1437
  }
1438
+ function SpecCaptureLoading() {
1439
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-spec-capture-loading": "", role: "status", "aria-live": "polite", className: "flex-1 overflow-hidden px-4 py-4", children: [
1440
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-4 flex items-center gap-3 text-sm text-[color:var(--foreground)]", children: [
1441
+ /* @__PURE__ */ jsxRuntime.jsx(
1442
+ "span",
1443
+ {
1444
+ "aria-hidden": "true",
1445
+ className: "inline-block h-5 w-5 animate-spin rounded-full border-2 border-[color:var(--line)] border-t-[color:var(--color-accent)] motion-reduce:animate-none"
1446
+ }
1447
+ ),
1448
+ "Capturing your view and comparing\u2026"
1449
+ ] }),
1450
+ ["Live \u2014 your view", "Diff"].map((label) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-4", children: [
1451
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-1 text-xs font-semibold uppercase tracking-wide text-[color:var(--muted)]", children: label }),
1452
+ /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", className: "h-40 w-full animate-pulse rounded border border-[color:var(--line)] bg-[color:var(--panel)] motion-reduce:animate-none" })
1453
+ ] }, label))
1454
+ ] });
1455
+ }
1430
1456
  function specOverlayStatusPath() {
1431
1457
  return `/api/overlay/status?url=${encodeURIComponent(window.location.href)}&theme=${currentChromeTheme()}`;
1432
1458
  }
@@ -1436,6 +1462,7 @@ function SpecOverlayAction({ orgId, orgName } = {}) {
1436
1462
  const [open, setOpen] = react.useState(false);
1437
1463
  const [capture, setCapture] = react.useState({ state: "idle" });
1438
1464
  const [frameKey, setFrameKey] = react.useState(0);
1465
+ const [copied, setCopied] = react.useState("idle");
1439
1466
  const href = useLocationHref();
1440
1467
  react.useEffect(() => {
1441
1468
  if (access) return;
@@ -1589,16 +1616,49 @@ function SpecOverlayAction({ orgId, orgName } = {}) {
1589
1616
  onClick: () => void runCapture(status),
1590
1617
  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",
1591
1618
  title: "Your view is captured when the drawer opens. Recapture after changing the page.",
1592
- children: capture.state === "busy" ? "Capturing your view\u2026" : "Recapture"
1619
+ children: capture.state === "busy" ? "Capturing\u2026" : "Recapture"
1620
+ }
1621
+ ),
1622
+ /* @__PURE__ */ jsxRuntime.jsx(
1623
+ "button",
1624
+ {
1625
+ type: "button",
1626
+ "data-spec-copy-prompt": "",
1627
+ disabled: capture.state === "busy" || !status.prompt,
1628
+ onClick: () => {
1629
+ const text = status.prompt;
1630
+ if (!text) return;
1631
+ navigator.clipboard.writeText(text).then(() => setCopied("copied")).catch(() => setCopied("failed")).finally(() => window.setTimeout(() => setCopied("idle"), 2e3));
1632
+ },
1633
+ className: "rounded-md border border-[color:var(--line)] px-2 py-1 text-[color:var(--foreground)] hover:bg-[color:var(--panel)] disabled:opacity-60",
1634
+ title: "Copy a repair prompt for an AI agent: spec, score, image links, top mismatched regions and instructions.",
1635
+ children: copied === "copied" ? "Copied \u2713" : copied === "failed" ? "Copy failed" : "Copy prompt"
1593
1636
  }
1594
1637
  )
1595
1638
  ] }),
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,
1639
+ status.live_size && status.design_size && capture.state !== "busy" ? /* @__PURE__ */ jsxRuntime.jsxs(
1640
+ "p",
1641
+ {
1642
+ "data-spec-sizes": "",
1643
+ className: [
1644
+ "border-b border-[color:var(--line)] px-4 py-1.5 text-xs",
1645
+ status.live_size.width === status.design_size.width && status.live_size.height === status.design_size.height ? "text-[color:var(--muted)]" : "font-semibold text-[color:var(--chrome-danger)]"
1646
+ ].join(" "),
1647
+ children: [
1648
+ "Live ",
1649
+ status.live_size.width,
1650
+ "\xD7",
1651
+ status.live_size.height,
1652
+ " \xB7 Design ",
1653
+ status.design_size.width,
1654
+ "\xD7",
1655
+ status.design_size.height,
1656
+ status.live_size.width === status.design_size.width && status.live_size.height === status.design_size.height ? " \xB7 sizes match" : " \xB7 SIZE MISMATCH"
1657
+ ]
1658
+ }
1659
+ ) : null,
1600
1660
  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,
1601
- /* @__PURE__ */ jsxRuntime.jsx(
1661
+ capture.state === "busy" ? /* @__PURE__ */ jsxRuntime.jsx(SpecCaptureLoading, {}) : /* @__PURE__ */ jsxRuntime.jsx(
1602
1662
  "iframe",
1603
1663
  {
1604
1664
  src: status.overlay_url,
@@ -3321,7 +3381,7 @@ function NavAppShell({
3321
3381
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lg:pl-[240px]", children: [
3322
3382
  /* @__PURE__ */ jsxRuntime.jsx(NavTopBar, { ...topBar, onOpenNav: () => setNavOpen(true), navOpen }),
3323
3383
  betaBanner ? /* @__PURE__ */ jsxRuntime.jsx(NavBetaBanner, { ...betaBanner }) : null,
3324
- contentWrapper === "bare" ? children : /* @__PURE__ */ jsxRuntime.jsx("main", { className: "px-6 py-7", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto max-w-7xl", children }) })
3384
+ 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 }) })
3325
3385
  ] })
3326
3386
  ]
3327
3387
  }