@the-portland-company/shell 0.96.1 → 0.97.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.
@@ -4,9 +4,9 @@
4
4
  var react = require('react');
5
5
  var clsx = require('clsx');
6
6
  var fi = require('react-icons/fi');
7
- var reactDom = require('react-dom');
8
- var jsxRuntime = require('react/jsx-runtime');
7
+ require('react-dom');
9
8
  var lu = require('react-icons/lu');
9
+ var jsxRuntime = require('react/jsx-runtime');
10
10
  var devnotes = require('@the-portland-company/devnotes');
11
11
 
12
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -575,7 +575,6 @@ var SETTINGS_APP_ORIGIN = "https://settings.politogyvrm.com";
575
575
  var CORE_BILLING_PATH = "/organization-settings/subscription-billing";
576
576
  var CORE_BILLING_URL = `${SETTINGS_APP_ORIGIN}${CORE_BILLING_PATH}`;
577
577
  var QUARANTINED_CONTACTS_URL = `https://contacts.politogyvrm.com/duplicates`;
578
- var SPECS_ORIGIN = "https://specs.politogyvrm.com";
579
578
  function withOrgParam(url, orgId) {
580
579
  if (!orgId) return url;
581
580
  return `${url}${url.includes("?") ? "&" : "?"}org=${orgId}`;
@@ -653,16 +652,6 @@ function TopBarPanelArrow() {
653
652
  }
654
653
  );
655
654
  }
656
- function TopBarWidgetSlot({
657
- title,
658
- description,
659
- children
660
- }) {
661
- return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "topbar-widget-slot group/tbtip relative inline-flex", children: [
662
- children,
663
- /* @__PURE__ */ jsxRuntime.jsx(TopBarTooltip, { title, description })
664
- ] });
665
- }
666
655
  var THEME_OPTIONS = [
667
656
  { value: "light", label: "Light" }
668
657
  ];
@@ -946,23 +935,6 @@ function BellOffGlyph({ size = 14, className }) {
946
935
  /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "1", y1: "1", x2: "23", y2: "23" })
947
936
  ] });
948
937
  }
949
- function LifeBuoyGlyph({ size = TOPBAR_GLYPH_SIZE, className }) {
950
- return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className, "aria-hidden": true, children: [
951
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10" }),
952
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "4" }),
953
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4.93", y1: "4.93", x2: "9.17", y2: "9.17" }),
954
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "14.83", y1: "14.83", x2: "19.07", y2: "19.07" }),
955
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "14.83", y1: "9.17", x2: "19.07", y2: "4.93" }),
956
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4.93", y1: "19.07", x2: "9.17", y2: "14.83" })
957
- ] });
958
- }
959
- function LayoutGlyph({ size = TOPBAR_GLYPH_SIZE, className }) {
960
- return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className, "aria-hidden": true, children: [
961
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
962
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "9", x2: "21", y2: "9" }),
963
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "21", x2: "9", y2: "9" })
964
- ] });
965
- }
966
938
  function PlusGlyph({ size = 14, className }) {
967
939
  return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", className, "aria-hidden": true, children: [
968
940
  /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "5", x2: "12", y2: "19" }),
@@ -1596,586 +1568,39 @@ function NotificationsAction({ orgId }) {
1596
1568
  ] })
1597
1569
  );
1598
1570
  }
1599
- function composeSupportMessage(input) {
1600
- const ctx = [`Page: ${input.pageTitle || "Unknown"}`, `URL: ${input.url}`];
1601
- if (input.orgName) ctx.push(`Org: ${input.orgName}`);
1602
- const reply = [input.replyEmail?.trim(), input.replyPhone?.trim()].filter(Boolean).join(" / ");
1603
- if (reply) ctx.push(`Reply-to: ${reply}`);
1604
- return `${input.note.trim()}
1605
-
1606
- ---
1607
- ${ctx.join("\n")}`;
1608
- }
1609
- function SupportAction({
1610
- orgId,
1611
- orgs: knownOrgs
1612
- } = {}) {
1613
- const { fetchJson } = useNavChrome("SupportAction");
1614
- const { me, loaded } = useChromeMe();
1615
- const { orgs: fetchedOrgs } = useChromeOrgs({ enabled: !knownOrgs });
1616
- const orgs = knownOrgs ?? fetchedOrgs;
1617
- const [open, setOpen] = react.useState(false);
1618
- const [message, setMessage] = react.useState("");
1619
- const [email, setEmail] = react.useState("");
1620
- const [phone, setPhone] = react.useState("");
1621
- const [showEmailError, setShowEmailError] = react.useState(false);
1622
- const [state, setState] = react.useState("idle");
1623
- const meEmail = me?.email ?? null;
1624
- const mePhone = me?.phone ?? null;
1625
- const missingEmail = loaded && !meEmail;
1626
- const missingPhone = loaded && !mePhone;
1627
- const activeOrg = (orgId ? orgs?.find((o) => o.id === orgId) : null) ?? (orgs && orgs.length === 1 ? orgs[0] : null);
1628
- const orgName = activeOrg?.name ?? null;
1629
- const close = react.useCallback(() => {
1630
- setOpen(false);
1631
- setState("idle");
1632
- setMessage("");
1633
- setShowEmailError(false);
1634
- }, []);
1635
- react.useEffect(() => {
1636
- if (!open) return;
1637
- setEmail(meEmail ?? "");
1638
- setPhone(mePhone ?? "");
1639
- setShowEmailError(false);
1640
- }, [open, meEmail, mePhone]);
1641
- react.useEffect(() => {
1642
- if (!open) return;
1643
- const onKey = (e) => {
1644
- if (e.key === "Escape") close();
1645
- };
1646
- document.addEventListener("keydown", onKey);
1647
- return () => document.removeEventListener("keydown", onKey);
1648
- }, [open, close]);
1649
- const submit = async () => {
1650
- if (!message.trim() || state === "sending") return;
1651
- const resolvedEmail = (missingEmail ? email : meEmail ?? "").trim();
1652
- const resolvedPhone = (missingPhone ? phone : mePhone ?? "").trim();
1653
- if (missingEmail && !resolvedEmail) {
1654
- setShowEmailError(true);
1655
- return;
1656
- }
1657
- setShowEmailError(false);
1658
- const collectedContact = missingEmail || missingPhone;
1659
- const fullMessage = composeSupportMessage({
1660
- note: message,
1661
- pageTitle: typeof document !== "undefined" ? document.title : "",
1662
- url: typeof window !== "undefined" ? window.location.href : "",
1663
- orgName,
1664
- replyEmail: collectedContact ? resolvedEmail : void 0,
1665
- replyPhone: collectedContact ? resolvedPhone : void 0
1666
- });
1667
- setState("sending");
1668
- try {
1669
- await fetchJson("support", {
1670
- method: "POST",
1671
- headers: { "Content-Type": "application/json" },
1672
- body: JSON.stringify({ message: fullMessage })
1673
- });
1674
- setState("sent");
1675
- } catch {
1676
- setState("error");
1677
- }
1678
- };
1679
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1680
- /* @__PURE__ */ jsxRuntime.jsxs(
1681
- "button",
1682
- {
1683
- type: "button",
1684
- "aria-label": "Support",
1685
- className: topBarIconButtonClass(),
1686
- onClick: () => setOpen(true),
1687
- children: [
1688
- /* @__PURE__ */ jsxRuntime.jsx(SlotGlyph, { slot: "support", fallback: LifeBuoyGlyph }),
1689
- /* @__PURE__ */ jsxRuntime.jsx(
1690
- TopBarTooltip,
1691
- {
1692
- title: "Support",
1693
- description: "Send a support request to the Politogy team."
1694
- }
1695
- )
1696
- ]
1697
- }
1698
- ),
1699
- open && typeof document !== "undefined" ? reactDom.createPortal(
1700
- /* @__PURE__ */ jsxRuntime.jsx(
1701
- "div",
1702
- {
1703
- className: "fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4",
1704
- role: "dialog",
1705
- "aria-modal": "true",
1706
- "aria-label": "Support request",
1707
- onMouseDown: (e) => {
1708
- if (e.target === e.currentTarget) close();
1709
- },
1710
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full max-w-md rounded-2xl border border-[color:var(--line)] bg-[color:var(--panel-elevated)] p-6 shadow-[var(--shadow-soft)]", children: [
1711
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold tracking-[-0.01em] text-[color:var(--foreground)]", children: "Contact Support" }),
1712
- state === "sent" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1713
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-3 text-sm text-[color:var(--muted)]", children: "Your request was sent. The team will reply by email." }),
1714
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-5 flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(
1715
- "button",
1716
- {
1717
- type: "button",
1718
- onClick: close,
1719
- className: "rounded-full bg-[color:var(--color-accent-bg-safe,var(--color-accent))] px-4 py-2 text-sm font-semibold text-[#FFFFFF] hover:opacity-90",
1720
- children: "Done"
1721
- }
1722
- ) })
1723
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1724
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-[color:var(--muted)]", children: "Describe the problem or question. Your account details ride along automatically." }),
1725
- missingEmail ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "mt-4 block", children: [
1726
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mb-1 block text-xs font-semibold text-[color:var(--foreground)]", children: "Your email" }),
1727
- /* @__PURE__ */ jsxRuntime.jsx(
1728
- "input",
1729
- {
1730
- type: "email",
1731
- value: email,
1732
- onChange: (e) => setEmail(e.target.value),
1733
- placeholder: "you@example.com",
1734
- className: "w-full rounded-lg border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2 text-sm text-[color:var(--foreground)] placeholder:text-[color:var(--muted)] focus:outline-none focus:ring-2 focus:ring-[color:var(--color-accent)]"
1735
- }
1736
- ),
1737
- showEmailError ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-1 block text-xs text-[color:var(--chrome-danger)]", children: "Enter an email so support can reply." }) : null
1738
- ] }) : null,
1739
- missingPhone ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "mt-3 block", children: [
1740
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mb-1 block text-xs font-semibold text-[color:var(--foreground)]", children: "Your phone \u2014 optional" }),
1741
- /* @__PURE__ */ jsxRuntime.jsx(
1742
- "input",
1743
- {
1744
- type: "tel",
1745
- value: phone,
1746
- onChange: (e) => setPhone(e.target.value),
1747
- placeholder: "(503) 555-1212",
1748
- className: "w-full rounded-lg border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2 text-sm text-[color:var(--foreground)] placeholder:text-[color:var(--muted)] focus:outline-none focus:ring-2 focus:ring-[color:var(--color-accent)]"
1749
- }
1750
- )
1751
- ] }) : null,
1752
- /* @__PURE__ */ jsxRuntime.jsx(
1753
- "textarea",
1754
- {
1755
- value: message,
1756
- onChange: (e) => setMessage(e.target.value),
1757
- rows: 5,
1758
- placeholder: "How can we help?",
1759
- className: "mt-4 w-full resize-y rounded-lg border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2 text-sm text-[color:var(--foreground)] placeholder:text-[color:var(--muted)] focus:outline-none focus:ring-2 focus:ring-[color:var(--color-accent)]"
1760
- }
1761
- ),
1762
- state === "error" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-[color:var(--chrome-danger)]", children: "Sending failed \u2014 try again." }) : null,
1763
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [
1764
- /* @__PURE__ */ jsxRuntime.jsx(
1765
- "button",
1766
- {
1767
- type: "button",
1768
- onClick: close,
1769
- className: "rounded-full border border-[color:var(--line)] px-4 py-2 text-sm font-semibold text-[color:var(--foreground)] hover:bg-[color:var(--panel)]",
1770
- children: "Cancel"
1771
- }
1772
- ),
1773
- /* @__PURE__ */ jsxRuntime.jsx(
1774
- "button",
1775
- {
1776
- type: "button",
1777
- onClick: submit,
1778
- disabled: !message.trim() || state === "sending",
1779
- className: "rounded-full bg-[color:var(--color-accent-bg-safe,var(--color-accent))] px-4 py-2 text-sm font-semibold text-[#FFFFFF] transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50",
1780
- children: state === "sending" ? "Sending\u2026" : "Send request"
1781
- }
1782
- )
1783
- ] })
1784
- ] })
1785
- ] })
1786
- }
1787
- ),
1788
- document.body
1789
- ) : null
1571
+ var NAV_DISPLAY_OPTIONS = [
1572
+ { value: "menu", label: "Sub Menu" },
1573
+ { value: "tabs", label: "Tabs Only" },
1574
+ { value: "both", label: "Both" }
1575
+ ];
1576
+ function TopBarLinkAction({ label, description, href, icon: Icon }) {
1577
+ return /* @__PURE__ */ jsxRuntime.jsxs("a", { href, "aria-label": label, className: topBarIconButtonClass(), children: [
1578
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: TOPBAR_GLYPH_SIZE, className: "text-current" }),
1579
+ /* @__PURE__ */ jsxRuntime.jsx(TopBarTooltip, { title: label, description })
1790
1580
  ] });
1791
1581
  }
1792
- var SPEC_OVERLAY_ACCESS_CACHE_KEY = "politogy_spec_overlay_access";
1793
- function readSpecOverlayAccessCache() {
1794
- if (typeof window === "undefined") return null;
1795
- try {
1796
- const raw = window.sessionStorage.getItem(SPEC_OVERLAY_ACCESS_CACHE_KEY);
1797
- if (!raw) return null;
1798
- const parsed = JSON.parse(raw);
1799
- if (typeof parsed?.allowed === "boolean") return parsed;
1800
- } catch {
1801
- }
1802
- return null;
1803
- }
1804
- function writeSpecOverlayAccessCache(access) {
1805
- if (typeof window === "undefined") return;
1806
- try {
1807
- window.sessionStorage.setItem(SPEC_OVERLAY_ACCESS_CACHE_KEY, JSON.stringify(access));
1808
- } catch {
1809
- }
1810
- }
1811
- var locationChangeListeners = /* @__PURE__ */ new Set();
1812
- var historyPatched = false;
1813
- function ensureHistoryPatched() {
1814
- if (historyPatched || typeof window === "undefined") return;
1815
- historyPatched = true;
1816
- const notify = () => locationChangeListeners.forEach((fn) => fn());
1817
- const originalPushState = window.history.pushState.bind(window.history);
1818
- const originalReplaceState = window.history.replaceState.bind(window.history);
1819
- window.history.pushState = (...args) => {
1820
- originalPushState(...args);
1821
- notify();
1822
- };
1823
- window.history.replaceState = (...args) => {
1824
- originalReplaceState(...args);
1825
- notify();
1826
- };
1827
- window.addEventListener("popstate", notify);
1828
- }
1829
- function useLocationHref() {
1830
- const [href, setHref] = react.useState(
1831
- () => typeof window === "undefined" ? "" : window.location.href
1832
- );
1833
- react.useEffect(() => {
1834
- ensureHistoryPatched();
1835
- const onChange = () => setHref(window.location.href);
1836
- locationChangeListeners.add(onChange);
1837
- return () => {
1838
- locationChangeListeners.delete(onChange);
1839
- };
1840
- }, []);
1841
- return href;
1842
- }
1843
- function specOverlayFetchJson(path) {
1844
- return fetch(`${SPECS_ORIGIN}${path}`, { credentials: "include", signal: AbortSignal.timeout(1e4) }).then((res) => {
1845
- if (!res.ok) throw new Error(`spec overlay ${path}: ${res.status}`);
1846
- return res.json();
1847
- });
1848
- }
1849
- function isAutomatedCaptureSession() {
1850
- return typeof navigator !== "undefined" && navigator.webdriver === true;
1851
- }
1852
- function currentChromeTheme() {
1853
- const root = document.documentElement;
1854
- if (root.classList.contains("dark") || root.getAttribute("data-theme") === "dark") return "dark";
1855
- if (root.classList.contains("light") || root.getAttribute("data-theme") === "light") return "light";
1856
- return "light";
1857
- }
1858
- var SPEC_OVERLAY_ROOT_ATTR = "data-spec-overlay-root";
1859
- async function captureMyView(status, orgId, orgName) {
1860
- const { domToCanvas } = await import('modern-screenshot');
1861
- const w = window.innerWidth;
1862
- const h = window.innerHeight;
1863
- const theme = currentChromeTheme();
1864
- const bg = getComputedStyle(document.body).backgroundColor || (theme === "dark" ? "#0b0b0c" : "#ffffff");
1865
- const full = await domToCanvas(document.body, {
1866
- width: w,
1867
- height: h,
1868
- scale: 1,
1869
- backgroundColor: bg,
1870
- // Cross-origin images that refuse CORS render blank instead of failing.
1871
- fetch: { requestInit: { mode: "cors", credentials: "omit" }, placeholderImage: "" },
1872
- // Drop the Spec drawer and everything fixed (sidebar, toasts, popovers):
1873
- // the design is the page body only, so chrome would only add noise.
1874
- filter: (node) => !(node instanceof Element) || !node.hasAttribute(SPEC_OVERLAY_ROOT_ATTR) && getComputedStyle(node).position !== "fixed",
1875
- style: { transform: `translate(${-window.scrollX}px, ${-window.scrollY}px)` }
1876
- });
1877
- const el = (status.live_selector ? document.querySelector(status.live_selector) : null) ?? document.querySelector("[data-shell-content]");
1878
- let region = { x: 0, y: 0, width: w, height: h };
1879
- if (el) {
1880
- const r = el.getBoundingClientRect();
1881
- const x = Math.max(0, Math.round(r.left));
1882
- const y = Math.max(0, Math.round(r.top));
1883
- region = { x, y, width: Math.min(w - x, Math.round(r.width)), height: Math.min(h - y, Math.round(r.height)) };
1884
- }
1885
- const out = document.createElement("canvas");
1886
- out.width = region.width;
1887
- out.height = region.height;
1888
- const ctx = out.getContext("2d");
1889
- if (!ctx) throw new Error("page raster failed");
1890
- ctx.drawImage(full, region.x, region.y, region.width, region.height, 0, 0, region.width, region.height);
1891
- const blob = await new Promise((resolve) => out.toBlob(resolve, "image/png"));
1892
- if (!blob) throw new Error("page raster failed");
1893
- const form = new FormData();
1894
- form.append("image", blob, "live.png");
1895
- form.append(
1896
- "meta",
1897
- JSON.stringify({ url: window.location.href, theme, org_id: orgId ?? null, org_name: orgName ?? null, crop: null })
1582
+ function OrganizationSettingsAction({ href }) {
1583
+ return /* @__PURE__ */ jsxRuntime.jsx(
1584
+ TopBarLinkAction,
1585
+ {
1586
+ label: "Organization settings",
1587
+ description: "Users, Brand Kit, modes and billing for this organization.",
1588
+ href,
1589
+ icon: lu.LuSettings
1590
+ }
1898
1591
  );
1899
- const res = await fetch(status.capture_url ?? `${SPECS_ORIGIN}/api/overlay/capture`, {
1900
- method: "POST",
1901
- credentials: "include",
1902
- headers: { "X-Spec-Capture": "1" },
1903
- body: form,
1904
- signal: AbortSignal.timeout(1e4)
1905
- });
1906
- if (!res.ok) throw new Error(`capture ${res.status}: ${(await res.text()).slice(0, 160)}`);
1907
1592
  }
1908
- function SpecCaptureLoading() {
1909
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-spec-capture-loading": "", role: "status", "aria-live": "polite", className: "flex-1 overflow-hidden px-4 py-4", children: [
1910
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-4 flex items-center gap-3 text-sm text-[color:var(--foreground)]", children: [
1911
- /* @__PURE__ */ jsxRuntime.jsx(
1912
- "span",
1913
- {
1914
- "aria-hidden": "true",
1915
- 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"
1916
- }
1917
- ),
1918
- "Capturing your view and comparing\u2026"
1919
- ] }),
1920
- ["Live \u2014 your view", "Diff"].map((label) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-4", children: [
1921
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-1 text-xs font-semibold uppercase tracking-wide text-[color:var(--muted)]", children: label }),
1922
- /* @__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" })
1923
- ] }, label))
1924
- ] });
1925
- }
1926
- function specOverlayStatusPath() {
1927
- return `/api/overlay/status?url=${encodeURIComponent(window.location.href)}&theme=${currentChromeTheme()}`;
1928
- }
1929
- function SpecOverlayAction({ orgId, orgName } = {}) {
1930
- const [access, setAccess] = react.useState(() => readSpecOverlayAccessCache());
1931
- const [status, setStatus] = react.useState(null);
1932
- const [open, setOpen] = react.useState(false);
1933
- const [capture, setCapture] = react.useState({ state: "idle" });
1934
- const [frameKey, setFrameKey] = react.useState(0);
1935
- const [copied, setCopied] = react.useState("idle");
1936
- const href = useLocationHref();
1937
- const automated = isAutomatedCaptureSession();
1938
- react.useEffect(() => {
1939
- if (access || automated) return;
1940
- let cancelled = false;
1941
- specOverlayFetchJson("/api/overlay/access").then((data) => {
1942
- if (cancelled) return;
1943
- const next = data && typeof data.allowed === "boolean" ? data : { allowed: false };
1944
- setAccess(next);
1945
- writeSpecOverlayAccessCache(next);
1946
- }).catch(() => {
1947
- if (cancelled) return;
1948
- const next = { allowed: false };
1949
- setAccess(next);
1950
- writeSpecOverlayAccessCache(next);
1951
- });
1952
- return () => {
1953
- cancelled = true;
1954
- };
1955
- }, [access]);
1956
- react.useEffect(() => {
1957
- if (automated || !access?.allowed || typeof window === "undefined") return;
1958
- const timer = window.setTimeout(() => {
1959
- specOverlayFetchJson(specOverlayStatusPath()).then((data) => setStatus(data)).catch(() => setStatus(null));
1960
- }, 300);
1961
- return () => window.clearTimeout(timer);
1962
- }, [access?.allowed, href, orgId]);
1963
- const captureRun = react.useRef(0);
1964
- const accessRef = react.useRef(access);
1965
- accessRef.current = access;
1966
- const runCapture = async (s) => {
1967
- const run = ++captureRun.current;
1968
- const before = s.live_source === "viewer" ? s.finished_at : null;
1969
- setCapture({ state: "busy" });
1970
- try {
1971
- await new Promise((r) => setTimeout(r, 150));
1972
- await captureMyView(s, orgId, orgName);
1973
- for (let i = 0; i < 45; i++) {
1974
- await new Promise((r) => setTimeout(r, 3e3));
1975
- if (run !== captureRun.current) return;
1976
- if (!accessRef.current?.allowed) {
1977
- setCapture({ state: "idle" });
1978
- return;
1979
- }
1980
- const next = await specOverlayFetchJson(specOverlayStatusPath()).catch(() => null);
1981
- if (next && next.matched && next.live_source === "viewer" && next.finished_at !== before) {
1982
- setStatus(next);
1983
- setFrameKey((k) => k + 1);
1984
- setCapture({ state: "idle" });
1985
- return;
1986
- }
1987
- }
1988
- setCapture({ state: "error", message: "Compare did not finish in 2 min." });
1989
- } catch (e) {
1990
- if (run === captureRun.current) setCapture({ state: "error", message: e instanceof Error ? e.message : String(e) });
1593
+ function MyProfileAction({ href }) {
1594
+ return /* @__PURE__ */ jsxRuntime.jsx(
1595
+ TopBarLinkAction,
1596
+ {
1597
+ label: "My profile",
1598
+ description: "Your photo, organizations, security and notifications.",
1599
+ href,
1600
+ icon: lu.LuCircleUser
1991
1601
  }
1992
- };
1993
- react.useEffect(() => {
1994
- if (automated || !open || !status || !status.matched) return;
1995
- void runCapture(status);
1996
- return () => {
1997
- captureRun.current++;
1998
- };
1999
- }, [open, status && status.matched ? status.target_id : null]);
2000
- if (automated || !access?.allowed) return null;
2001
- const hasScore = status && status.matched && typeof status.mismatch_pct === "number";
2002
- const badgeLabel = hasScore ? `${Math.round(status.mismatch_pct)}%` : status ? "\u2014" : void 0;
2003
- const badgeTone = hasScore ? status && status.matched && status.passed ? "default" : "danger" : "default";
2004
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2005
- /* @__PURE__ */ jsxRuntime.jsxs(
2006
- "button",
2007
- {
2008
- type: "button",
2009
- "aria-label": "Spec",
2010
- "aria-expanded": open,
2011
- className: topBarIconButtonClass(badgeTone),
2012
- onClick: () => setOpen(true),
2013
- children: [
2014
- /* @__PURE__ */ jsxRuntime.jsx(LayoutGlyph, { size: TOPBAR_GLYPH_SIZE, className: "text-current" }),
2015
- badgeLabel ? /* @__PURE__ */ jsxRuntime.jsx(
2016
- "span",
2017
- {
2018
- className: [
2019
- "absolute -top-1 -right-1 flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-normal leading-none text-[#FFFFFF]",
2020
- hasScore ? status && status.matched && status.passed ? "bg-[color:var(--chrome-success,#2f855a)]" : "bg-[color:var(--chrome-danger)]" : "bg-[color:var(--muted)]"
2021
- ].join(" "),
2022
- children: badgeLabel
2023
- }
2024
- ) : null,
2025
- !open ? /* @__PURE__ */ jsxRuntime.jsx(
2026
- TopBarTooltip,
2027
- {
2028
- title: "Spec",
2029
- description: "Spec design + visual regression for this page."
2030
- }
2031
- ) : null
2032
- ]
2033
- }
2034
- ),
2035
- open && typeof document !== "undefined" ? reactDom.createPortal(
2036
- /* @__PURE__ */ jsxRuntime.jsx(
2037
- "div",
2038
- {
2039
- className: "fixed inset-0 z-50 flex justify-end bg-black/40",
2040
- ...{ [SPEC_OVERLAY_ROOT_ATTR]: "" },
2041
- role: "dialog",
2042
- "aria-modal": "true",
2043
- "aria-label": "Spec overlay",
2044
- onMouseDown: (e) => {
2045
- if (e.target === e.currentTarget) setOpen(false);
2046
- },
2047
- onKeyDown: (e) => {
2048
- if (e.key === "Escape") setOpen(false);
2049
- },
2050
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full w-full max-w-[480px] flex-col border-l border-[color:var(--line)] bg-[color:var(--panel-elevated)] shadow-[var(--shadow-soft)]", children: [
2051
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-[color:var(--line)] px-4 py-3", children: [
2052
- /* @__PURE__ */ jsxRuntime.jsxs("h2", { className: "truncate text-sm font-bold tracking-[-0.01em] text-[color:var(--foreground)]", children: [
2053
- "Spec",
2054
- status && status.matched ? `: ${status.spec_title}` : ""
2055
- ] }),
2056
- /* @__PURE__ */ jsxRuntime.jsx(
2057
- "button",
2058
- {
2059
- type: "button",
2060
- "aria-label": "Close",
2061
- onClick: () => setOpen(false),
2062
- className: "rounded-full p-1 text-[color:var(--muted)] hover:bg-[color:var(--panel)] hover:text-[color:var(--foreground)]",
2063
- children: /* @__PURE__ */ jsxRuntime.jsx(CloseGlyph, { size: 16 })
2064
- }
2065
- )
2066
- ] }),
2067
- status && status.matched ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2068
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 border-b border-[color:var(--line)] px-4 py-2 text-xs font-semibold", children: [
2069
- /* @__PURE__ */ jsxRuntime.jsx(
2070
- "a",
2071
- {
2072
- href: status.spec_url,
2073
- target: "_blank",
2074
- rel: "noreferrer",
2075
- className: "text-[color:var(--color-accent-fg,var(--color-accent))] hover:underline",
2076
- children: "Open spec \u2197"
2077
- }
2078
- ),
2079
- /* @__PURE__ */ jsxRuntime.jsx(
2080
- "a",
2081
- {
2082
- href: status.compare_url,
2083
- target: "_blank",
2084
- rel: "noreferrer",
2085
- className: "text-[color:var(--color-accent-fg,var(--color-accent))] hover:underline",
2086
- children: "Compare tab \u2197"
2087
- }
2088
- ),
2089
- /* @__PURE__ */ jsxRuntime.jsx(
2090
- "button",
2091
- {
2092
- type: "button",
2093
- disabled: capture.state === "busy",
2094
- onClick: () => void runCapture(status),
2095
- 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",
2096
- title: "Your view is captured when the drawer opens. Recapture after changing the page.",
2097
- children: capture.state === "busy" ? "Capturing\u2026" : "Recapture"
2098
- }
2099
- ),
2100
- /* @__PURE__ */ jsxRuntime.jsx(
2101
- "button",
2102
- {
2103
- type: "button",
2104
- "data-spec-copy-prompt": "",
2105
- disabled: capture.state === "busy" || !status.prompt,
2106
- onClick: () => {
2107
- const text = status.prompt;
2108
- if (!text) return;
2109
- navigator.clipboard.writeText(text).then(() => setCopied("copied")).catch(() => setCopied("failed")).finally(() => window.setTimeout(() => setCopied("idle"), 2e3));
2110
- },
2111
- className: "rounded-md border border-[color:var(--line)] px-2 py-1 text-[color:var(--foreground)] hover:bg-[color:var(--panel)] disabled:opacity-60",
2112
- title: "Copy a repair prompt for an AI agent: spec, score, image links, top mismatched regions and instructions.",
2113
- children: copied === "copied" ? "Copied \u2713" : copied === "failed" ? "Copy failed" : "Copy prompt"
2114
- }
2115
- )
2116
- ] }),
2117
- status.live_size && status.design_size && capture.state !== "busy" ? /* @__PURE__ */ jsxRuntime.jsxs(
2118
- "p",
2119
- {
2120
- "data-spec-sizes": "",
2121
- className: [
2122
- "border-b border-[color:var(--line)] px-4 py-1.5 text-xs",
2123
- 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)]"
2124
- ].join(" "),
2125
- children: [
2126
- "Live ",
2127
- status.live_size.width,
2128
- "\xD7",
2129
- status.live_size.height,
2130
- " \xB7 Design ",
2131
- status.design_size.width,
2132
- "\xD7",
2133
- status.design_size.height,
2134
- status.live_size.width === status.design_size.width && status.live_size.height === status.design_size.height ? " \xB7 sizes match" : " \xB7 SIZE MISMATCH"
2135
- ]
2136
- }
2137
- ) : null,
2138
- 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,
2139
- capture.state === "busy" ? /* @__PURE__ */ jsxRuntime.jsx(SpecCaptureLoading, {}) : /* @__PURE__ */ jsxRuntime.jsx(
2140
- "iframe",
2141
- {
2142
- src: status.overlay_url,
2143
- title: "Spec overlay",
2144
- className: "h-full w-full flex-1 border-0"
2145
- },
2146
- frameKey
2147
- )
2148
- ] }) : status && !status.matched ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 text-sm text-[color:var(--muted)]", children: [
2149
- /* @__PURE__ */ jsxRuntime.jsx("p", { children: "No compare target covers this page." }),
2150
- /* @__PURE__ */ jsxRuntime.jsx(
2151
- "a",
2152
- {
2153
- href: status.search_url,
2154
- target: "_blank",
2155
- rel: "noreferrer",
2156
- className: "mt-2 inline-block font-semibold text-[color:var(--color-accent-fg,var(--color-accent))] hover:underline",
2157
- children: "Search specs \u2197"
2158
- }
2159
- )
2160
- ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4", children: /* @__PURE__ */ jsxRuntime.jsx(ValueSkeleton, { className: "h-4 w-full" }) })
2161
- ] })
2162
- }
2163
- ),
2164
- document.body
2165
- ) : null
2166
- ] });
2167
- }
2168
- function CloseGlyph({ size = 16, className }) {
2169
- return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className, "aria-hidden": true, children: [
2170
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
2171
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
2172
- ] });
1602
+ );
2173
1603
  }
2174
- var NAV_DISPLAY_OPTIONS = [
2175
- { value: "menu", label: "Sub Menu" },
2176
- { value: "tabs", label: "Tabs Only" },
2177
- { value: "both", label: "Both" }
2178
- ];
2179
1604
  function parseCssUrl(value) {
2180
1605
  if (!value) return null;
2181
1606
  const m = /^url\(\s*(['"]?)(.*?)\1\s*\)$/i.exec(value.trim());
@@ -2455,13 +1880,14 @@ function standardPolitogyNav(opts) {
2455
1880
  });
2456
1881
  };
2457
1882
  const accountRows = () => [
2458
- { kind: "section", label: "Account" },
1883
+ { kind: "section", label: "Account", railHidden: true },
2459
1884
  // My Profile moved to the Politogy ID account app (auth.politogyvrm.com)
2460
1885
  // with the retirement of app.politogyvrm.com.
2461
- { label: "My profile", icon: icon("circleUser"), href: withOrg(`${AUTH_ORIGIN}my-profile`) },
1886
+ { label: "My profile", icon: icon("circleUser"), href: withOrg(`${AUTH_ORIGIN}my-profile`), railHidden: true },
2462
1887
  {
2463
1888
  label: "Organization settings",
2464
1889
  icon: icon("settings"),
1890
+ railHidden: true,
2465
1891
  // Organization settings moved to the standalone Settings app
2466
1892
  // (settings.politogyvrm.com, Specs/Organization-Settings v12).
2467
1893
  href: withOrg(`${SETTINGS_ORIGIN}organization-settings`),
@@ -2985,6 +2411,8 @@ function NavSidebar({
2985
2411
  ] }) : null,
2986
2412
  filterOrgs(org.organizations, orgFilter).map((o) => {
2987
2413
  const selected = o.id === org.id;
2414
+ const iconSrc = resolveOrgLogoUrl(o.iconUrl);
2415
+ const logoSrc = iconSrc ? void 0 : resolveOrgLogoUrl(o.logoUrl);
2988
2416
  return /* @__PURE__ */ jsxRuntime.jsxs(
2989
2417
  "button",
2990
2418
  {
@@ -3003,12 +2431,13 @@ function NavSidebar({
3003
2431
  /* @__PURE__ */ jsxRuntime.jsx(
3004
2432
  "span",
3005
2433
  {
3006
- style: { backgroundColor: o.brandColor || ORG_PILL_DEFAULT },
3007
- className: "flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden rounded-full text-[10px] font-bold text-[#FFFFFF]",
3008
- children: resolveOrgLogoUrl(o.logoUrl) ? /* @__PURE__ */ jsxRuntime.jsx(
2434
+ "data-org-option-mark": iconSrc ? "icon" : logoSrc ? "logo" : "initials",
2435
+ style: iconSrc ? void 0 : { backgroundColor: o.brandColor || ORG_PILL_DEFAULT },
2436
+ className: `flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden text-[10px] font-bold text-[#FFFFFF] ${iconSrc ? "rounded-[4px] bg-[#FFFFFF]" : "rounded-full"}`,
2437
+ children: iconSrc ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: iconSrc, alt: "", className: "max-h-full max-w-full object-contain" }) : logoSrc ? /* @__PURE__ */ jsxRuntime.jsx(
3009
2438
  "img",
3010
2439
  {
3011
- src: resolveOrgLogoUrl(o.logoUrl),
2440
+ src: logoSrc,
3012
2441
  alt: "",
3013
2442
  className: "h-full w-full object-cover"
3014
2443
  }
@@ -3105,7 +2534,7 @@ function NavSidebar({
3105
2534
  ] }),
3106
2535
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-1 flex-col overflow-y-auto px-[10px] py-3", children: /* @__PURE__ */ jsxRuntime.jsxs("nav", { className: "flex flex-col gap-0.5", children: [
3107
2536
  nav.groupLabel ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: `${NAV_EYEBROW_CLASS} px-2 pb-1.5 pt-1 ${rc("max-md:hidden")}`, children: nav.groupLabel }) : null,
3108
- nav.items.map((item, idx) => {
2537
+ nav.items.filter((item) => !item.railHidden).map((item, idx) => {
3109
2538
  if (item.kind === "section") {
3110
2539
  return /* @__PURE__ */ jsxRuntime.jsx(
3111
2540
  NavSectionRow,
@@ -5072,6 +4501,7 @@ function FrameBody({
5072
4501
  name: activeCoreOrg.name,
5073
4502
  id: activeCoreOrg.id,
5074
4503
  logoUrl: activeCoreOrg.logoUrl,
4504
+ iconUrl: activeCoreOrg.iconUrl ?? null,
5075
4505
  brandColor: activeCoreOrg.brandColor,
5076
4506
  organizations: coreOrgs,
5077
4507
  onSwitch: chrome?.setOrgId
@@ -5175,6 +4605,7 @@ function TokenChromeStateProvider({
5175
4605
  id: o.id,
5176
4606
  name: o.name,
5177
4607
  logoUrl: o.logoUrl,
4608
+ iconUrl: o.iconUrl ?? null,
5178
4609
  brandColor: o.brandColor,
5179
4610
  role: o.role,
5180
4611
  rawRole: o.rawRole ?? null
@@ -5435,6 +4866,7 @@ function ChromeStateProvider({
5435
4866
  id: o.id,
5436
4867
  name: o.name,
5437
4868
  logoUrl: o.logoUrl,
4869
+ iconUrl: o.iconUrl ?? null,
5438
4870
  brandColor: o.brandColor,
5439
4871
  role: o.role,
5440
4872
  rawRole: o.rawRole ?? null
@@ -5452,6 +4884,7 @@ function ChromeStateProvider({
5452
4884
  id: o.id,
5453
4885
  name: o.name,
5454
4886
  logoUrl: o.logoUrl,
4887
+ iconUrl: o.iconUrl ?? null,
5455
4888
  brandColor: o.brandColor,
5456
4889
  role: o.role,
5457
4890
  rawRole: o.rawRole ?? null
@@ -5644,26 +5077,15 @@ function ChromeTopBarActions() {
5644
5077
  const state = useChromeState();
5645
5078
  if (!state) return null;
5646
5079
  const settledOrgId = state.orgsLoading ? null : state.orgId;
5080
+ const tail = orgIdTail(state.orgId);
5081
+ const orgQuery = tail ? `?${ORG_ID_QUERY_PARAM}=${tail}` : "";
5647
5082
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5648
- /* @__PURE__ */ jsxRuntime.jsx(
5649
- TopBarWidgetSlot,
5650
- {
5651
- title: "DevNotes",
5652
- description: "Capture bugs and notes, filed straight to Forge.",
5653
- children: /* @__PURE__ */ jsxRuntime.jsx(ChromeDevNotesMenu, {})
5654
- }
5655
- ),
5656
5083
  /* @__PURE__ */ jsxRuntime.jsx(FuelAction, { orgId: settledOrgId }),
5657
- /* @__PURE__ */ jsxRuntime.jsx(SpecOverlayAction, { orgId: state.orgId, orgName: state.orgs.find((o) => o.id === state.orgId)?.name ?? null }),
5658
5084
  /* @__PURE__ */ jsxRuntime.jsx(NotificationsAction, { orgId: settledOrgId }),
5659
- /* @__PURE__ */ jsxRuntime.jsx(SupportAction, { orgId: state.orgId, orgs: state.orgs.length ? state.orgs : void 0 })
5085
+ /* @__PURE__ */ jsxRuntime.jsx(OrganizationSettingsAction, { href: `${settingsBaseUrl()}/organization-settings${orgQuery}` }),
5086
+ /* @__PURE__ */ jsxRuntime.jsx(MyProfileAction, { href: `${authBaseUrl()}/my-profile${orgQuery}` })
5660
5087
  ] });
5661
5088
  }
5662
- function ChromeDevNotesMenu() {
5663
- const { ready, openTasks, activate } = react.useContext(DevNotesTasksContext);
5664
- if (!ready) return null;
5665
- return /* @__PURE__ */ jsxRuntime.jsx("span", { style: { display: "contents" }, onPointerDownCapture: activate, onFocusCapture: activate, onKeyDownCapture: activate, children: /* @__PURE__ */ jsxRuntime.jsx(devnotes.DevNotesMenu, { position: "top-right", icon: lu.LuNotebookPen, onViewTasks: openTasks }) });
5666
- }
5667
5089
  function useIsSuperAdmin() {
5668
5090
  const state = useChromeState();
5669
5091
  return (state?.orgs ?? []).some((o) => /super/i.test(o.rawRole ?? ""));
@@ -5713,11 +5135,13 @@ function useChromeUserMenu({
5713
5135
  organizationSettingsHref: `${settings}/organization-settings${orgSuffix}`,
5714
5136
  roles: (state?.orgs ?? []).filter((o) => o.rawRole || o.role).map((o) => ({ role: o.rawRole ?? o.role, orgName: o.name })),
5715
5137
  accounts: [{ id: userId, name: userName, email, avatarUrl: null, active: true }],
5716
- addAccountHref: `${auth}/my-profile#accounts`,
5138
+ addAccountHref: `${auth}/my-profile#accounts`
5717
5139
  // No theme control: the product is light only (Reference/UI-Platform
5718
5140
  // "Theme — light only"; shell 0.73.0 stopped applying dark). The account
5719
5141
  // menu rendered an "Appearance" radio with one option until 0.77.0.
5720
- navDisplay: state ? { value: state.navDisplay, onChange: state.setNavDisplay } : void 0
5142
+ // Ben 2026-11-04: no Navigation display option in the account menu. The
5143
+ // preference itself (state.navDisplay, its storage and the frame's use of
5144
+ // it) is unchanged; only the menu control is gone.
5721
5145
  };
5722
5146
  }
5723
5147
  var MODE_DEFS = [