@the-portland-company/shell 0.56.0 → 0.58.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,66 @@
1
+ ## 0.58.0 — 2026-09-30
2
+
3
+ ### Fixed — Meta build-metadata row renders for every viewer (`src/app-chrome/index.tsx`)
4
+
5
+ `useChromeFooter` gated the `env | commit | version` / `date | author` rows
6
+ behind Super Administrator. Reference/UI-Platform "Left bar — required
7
+ elements" and Specs/Left-Navigation §10 list Meta unconditionally, so every
8
+ non-admin viewer was missing a required rail element (DevNotes 83a64713).
9
+
10
+ ### Fixed — rail font pinned to the Brand token (`src/native/index.tsx`)
11
+
12
+ `NavSidebar` now sets `font-family: var(--font-family, …)` directly instead
13
+ of inheriting `body`, so an app-level font (Contacts) can no longer leak into
14
+ the left rail (DevNotes 64a4961e).
15
+
16
+ ## 0.57.0 — 2026-09-30
17
+
18
+ ### Fixed — 10DLC alert no longer auto-redirects on every login (`src/native/chromeActions.tsx`)
19
+
20
+ The once-per-session `10dlc_registration` alert redirect fired on every
21
+ login because every login is a new browser session (a fresh
22
+ `sessionStorage`), so the full-page `window.location.assign` re-booted the
23
+ app on every single sign-in — visible to the user as a second loading-screen
24
+ blink that always lands on
25
+ `/organization-settings/subscription-billing/text-email`. The auto-redirect
26
+ is removed entirely. The alert still surfaces in the notifications
27
+ bell/panel (unread, like any other alert) — that panel is now the only
28
+ user-visible surface for it.
29
+
30
+ ### Added — Spec overlay top-bar action, Spencer + Ben only (`./native`, `./app-chrome`)
31
+
32
+ A new `SpecOverlayAction` top-bar control (wired into `ChromeTopBarActions`,
33
+ between DevNotes and Support) shows the live spec/visual-regression status
34
+ of the current page, sourced from the Specs app's overlay API
35
+ (`https://specs.politogyvrm.com/api/overlay/access` and `/api/overlay/status`).
36
+ It renders nothing — no icon, no network noise — for anyone the access
37
+ check doesn't allow; the allow/deny answer is cached in `sessionStorage`
38
+ for the session. When allowed, a "Spec" button shows a rounded
39
+ mismatch-percentage badge (green when the compare passed, red when it
40
+ didn't, a neutral "—" when the page has no compare target yet) and opens a
41
+ 480px right-hand drawer with the spec title, "Open spec" / "Compare tab"
42
+ links, and an iframe of the live overlay. Status re-fetches (debounced
43
+ 300ms) on route change and org change. All calls are plain
44
+ `fetch(..., { credentials: 'include' })` against the Specs origin — this
45
+ does NOT go through the same-origin chrome proxy `fetchJson`.
46
+
47
+ ### Fixed — de-duped chrome fetches + lazy DevNotes boot fetch (`src/native/chromeActions.tsx`, `src/app-chrome/index.tsx`)
48
+
49
+ `useChromeOrgs`, `me`, `FuelAction`'s `communications/fuel-status`, and
50
+ `NotificationsAction`'s `notifications` were each fetched 2-4x per page load
51
+ (consumers mounting independently), costing 1-3.5s per redundant call. Added
52
+ a provider-scoped in-flight-promise + 30s-TTL cache
53
+ (`dedupedFetch`/`invalidateChromeCache` on `useNavChrome`) keyed by
54
+ `(path, orgId)`, so concurrent consumers share one request and org switches
55
+ naturally miss the cache. Separately, DevNotes' boot-time reads (tasks,
56
+ task types, task lists, collaborators, app-link status, capabilities — up to
57
+ 6.8s cold) are deferred until the DevNotes panel is first opened by wrapping
58
+ the adapter passed into `@the-portland-company/devnotes`'s
59
+ `DevNotesProvider`; the launcher button renders instantly and unblocks the
60
+ gated calls via `onPointerDownCapture` before the click fires. The unread
61
+ badge count still resolves on boot, via `requestIdleCallback` instead of
62
+ blocking it.
63
+
1
64
  ## 0.56.0 — 2026-09-30
2
65
 
3
66
  ### Fixed — mode continuity for dual-mode apps (a2p)
@@ -169,6 +169,7 @@ var CORE_APP_ORIGIN = "https://app.politogyvrm.com";
169
169
  var CORE_BILLING_PATH = "/organization-settings/subscription-billing/text-email";
170
170
  var CORE_BILLING_URL = `${CORE_APP_ORIGIN}${CORE_BILLING_PATH}`;
171
171
  var QUARANTINED_CONTACTS_URL = `https://contacts.politogyvrm.com/duplicates`;
172
+ var SPECS_ORIGIN = "https://specs.politogyvrm.com";
172
173
  function withOrgParam(url, orgId) {
173
174
  if (!orgId) return url;
174
175
  return `${url}${url.includes("?") ? "&" : "?"}org=${orgId}`;
@@ -291,16 +292,46 @@ function NavThemeControl({
291
292
  )
292
293
  ] });
293
294
  }
295
+ var CHROME_FETCH_CACHE_TTL_MS = 3e4;
296
+ function chromeCacheKey(path, orgId) {
297
+ return orgId === void 0 ? path : `${path}::${orgId ?? ""}`;
298
+ }
299
+ function createChromeFetchCache(getFetchJson) {
300
+ const cache = /* @__PURE__ */ new Map();
301
+ function dedupedFetch(path, init, opts = {}) {
302
+ const key = chromeCacheKey(path, opts.orgId);
303
+ const now = Date.now();
304
+ const hit = cache.get(key);
305
+ if (hit && hit.expiresAt > now) return hit.promise;
306
+ const promise = getFetchJson()(path, init);
307
+ promise.catch(() => cache.delete(key));
308
+ cache.set(key, { promise, expiresAt: now + (opts.ttlMs ?? CHROME_FETCH_CACHE_TTL_MS) });
309
+ return promise;
310
+ }
311
+ function invalidate(path) {
312
+ for (const key of cache.keys()) {
313
+ if (key === path || key.startsWith(`${path}::`)) cache.delete(key);
314
+ }
315
+ }
316
+ return { dedupedFetch, invalidate };
317
+ }
294
318
  var NavChromeContext = react.createContext(null);
295
319
  function NavChromeProvider({
296
320
  fetchJson,
297
321
  children
298
322
  }) {
323
+ const fetchJsonRef = react.useRef(fetchJson);
324
+ fetchJsonRef.current = fetchJson;
325
+ const cacheRef = react.useRef(null);
326
+ if (!cacheRef.current) {
327
+ cacheRef.current = createChromeFetchCache(() => fetchJsonRef.current);
328
+ }
329
+ const { dedupedFetch, invalidate: invalidateChromeCache } = cacheRef.current;
299
330
  const [me, setMe] = react.useState(null);
300
331
  const [meLoaded, setMeLoaded] = react.useState(false);
301
332
  react.useEffect(() => {
302
333
  let cancelled = false;
303
- fetchJson("me").then((data) => {
334
+ dedupedFetch("me", void 0).then((data) => {
304
335
  if (!cancelled) setMe(data);
305
336
  }).catch(() => {
306
337
  }).finally(() => {
@@ -309,8 +340,8 @@ function NavChromeProvider({
309
340
  return () => {
310
341
  cancelled = true;
311
342
  };
312
- }, [fetchJson]);
313
- return /* @__PURE__ */ jsxRuntime.jsx(NavChromeContext.Provider, { value: { fetchJson, me, meLoaded }, children });
343
+ }, [dedupedFetch]);
344
+ return /* @__PURE__ */ jsxRuntime.jsx(NavChromeContext.Provider, { value: { fetchJson, dedupedFetch, invalidateChromeCache, me, meLoaded }, children });
314
345
  }
315
346
  function useChromeMe() {
316
347
  const ctx = react.useContext(NavChromeContext);
@@ -329,7 +360,7 @@ function useNavChrome(component) {
329
360
  return ctx;
330
361
  }
331
362
  function useChromeOrgs() {
332
- const { fetchJson } = useNavChrome("useChromeOrgs");
363
+ const { dedupedFetch } = useNavChrome("useChromeOrgs");
333
364
  const [orgs, setOrgs] = react.useState(null);
334
365
  const [loading, setLoading] = react.useState(true);
335
366
  const [error, setError] = react.useState(false);
@@ -337,7 +368,7 @@ function useChromeOrgs() {
337
368
  let cancelled = false;
338
369
  setLoading(true);
339
370
  setError(false);
340
- fetchJson("orgs").then((data) => {
371
+ dedupedFetch("orgs", void 0).then((data) => {
341
372
  if (cancelled) return;
342
373
  const list = data?.orgs ?? [];
343
374
  setOrgs(list);
@@ -349,7 +380,7 @@ function useChromeOrgs() {
349
380
  return () => {
350
381
  cancelled = true;
351
382
  };
352
- }, [fetchJson]);
383
+ }, [dedupedFetch]);
353
384
  return { orgs, loading, error };
354
385
  }
355
386
  function ValueSkeleton({ className }) {
@@ -633,7 +664,7 @@ function LowFuelHint({ lowFuel, orgId }) {
633
664
  );
634
665
  }
635
666
  function FuelAction({ orgId }) {
636
- const { fetchJson } = useNavChrome("FuelAction");
667
+ const { dedupedFetch } = useNavChrome("FuelAction");
637
668
  const [fuel, setFuel] = react.useState(null);
638
669
  const [loading, setLoading] = react.useState(true);
639
670
  const [error, setError] = react.useState(false);
@@ -642,9 +673,11 @@ function FuelAction({ orgId }) {
642
673
  setLoading(true);
643
674
  setError(false);
644
675
  setFuel(null);
645
- fetchJson("communications/fuel-status", {
646
- headers: orgId ? { "x-contacts-org": orgId } : void 0
647
- }).then((data) => {
676
+ dedupedFetch(
677
+ "communications/fuel-status",
678
+ { headers: orgId ? { "x-contacts-org": orgId } : void 0 },
679
+ { orgId }
680
+ ).then((data) => {
648
681
  if (cancelled) return;
649
682
  setFuel(data?.fuel ?? null);
650
683
  }).catch(() => {
@@ -655,7 +688,7 @@ function FuelAction({ orgId }) {
655
688
  return () => {
656
689
  cancelled = true;
657
690
  };
658
- }, [fetchJson, orgId]);
691
+ }, [dedupedFetch, orgId]);
659
692
  const low = !!fuel && (fuel.signal.lowFuel || fuel.broadcast.lowFuel);
660
693
  return (
661
694
  // The nudge anchors to the trigger, so it sits OUTSIDE the popover panel in
@@ -726,7 +759,6 @@ function writeNotifState(orgId, state) {
726
759
  } catch {
727
760
  }
728
761
  }
729
- var TEN_DLC_REDIRECTED_KEY = "politogy_10dlc_redirected";
730
762
  var TOASTS_MUTED_KEY = "politogy_chrome_toasts_muted";
731
763
  function readToastsMuted() {
732
764
  if (typeof window === "undefined") return false;
@@ -857,7 +889,7 @@ function NewReplyToast({
857
889
  );
858
890
  }
859
891
  function NotificationsAction({ orgId }) {
860
- const { fetchJson } = useNavChrome("NotificationsAction");
892
+ const { dedupedFetch } = useNavChrome("NotificationsAction");
861
893
  const [payload, setPayload] = react.useState(null);
862
894
  const [loading, setLoading] = react.useState(true);
863
895
  const [error, setError] = react.useState(false);
@@ -876,9 +908,11 @@ function NotificationsAction({ orgId }) {
876
908
  setLoading(true);
877
909
  setError(false);
878
910
  setPayload(null);
879
- fetchJson("notifications", {
880
- headers: orgId ? { "x-contacts-org": orgId } : void 0
881
- }).then((data) => {
911
+ dedupedFetch(
912
+ "notifications",
913
+ { headers: orgId ? { "x-contacts-org": orgId } : void 0 },
914
+ { orgId }
915
+ ).then((data) => {
882
916
  if (!cancelled) setPayload(data);
883
917
  }).catch(() => {
884
918
  if (!cancelled) setError(true);
@@ -888,29 +922,7 @@ function NotificationsAction({ orgId }) {
888
922
  return () => {
889
923
  cancelled = true;
890
924
  };
891
- }, [fetchJson, orgId]);
892
- react.useEffect(() => {
893
- if (!payload || typeof window === "undefined") return;
894
- const alert = payload.alerts.find((a) => a.kind === "10dlc_registration" && a.href);
895
- if (!alert?.href) return;
896
- try {
897
- if (window.sessionStorage.getItem(TEN_DLC_REDIRECTED_KEY) === "1") return;
898
- } catch {
899
- return;
900
- }
901
- let target;
902
- try {
903
- target = new URL(alert.href, window.location.href);
904
- } catch {
905
- return;
906
- }
907
- if (target.origin !== window.location.origin) return;
908
- try {
909
- window.sessionStorage.setItem(TEN_DLC_REDIRECTED_KEY, "1");
910
- } catch {
911
- }
912
- window.location.assign(alert.href);
913
- }, [payload]);
925
+ }, [dedupedFetch, orgId]);
914
926
  const markRead = react.useCallback(
915
927
  (id) => {
916
928
  setNotif((prev) => {
@@ -1311,6 +1323,216 @@ function SupportAction({ orgId } = {}) {
1311
1323
  ) : null
1312
1324
  ] });
1313
1325
  }
1326
+ var SPEC_OVERLAY_ACCESS_CACHE_KEY = "politogy_spec_overlay_access";
1327
+ function readSpecOverlayAccessCache() {
1328
+ if (typeof window === "undefined") return null;
1329
+ try {
1330
+ const raw = window.sessionStorage.getItem(SPEC_OVERLAY_ACCESS_CACHE_KEY);
1331
+ if (!raw) return null;
1332
+ const parsed = JSON.parse(raw);
1333
+ if (typeof parsed?.allowed === "boolean") return parsed;
1334
+ } catch {
1335
+ }
1336
+ return null;
1337
+ }
1338
+ function writeSpecOverlayAccessCache(access) {
1339
+ if (typeof window === "undefined") return;
1340
+ try {
1341
+ window.sessionStorage.setItem(SPEC_OVERLAY_ACCESS_CACHE_KEY, JSON.stringify(access));
1342
+ } catch {
1343
+ }
1344
+ }
1345
+ var locationChangeListeners = /* @__PURE__ */ new Set();
1346
+ var historyPatched = false;
1347
+ function ensureHistoryPatched() {
1348
+ if (historyPatched || typeof window === "undefined") return;
1349
+ historyPatched = true;
1350
+ const notify = () => locationChangeListeners.forEach((fn) => fn());
1351
+ const originalPushState = window.history.pushState.bind(window.history);
1352
+ const originalReplaceState = window.history.replaceState.bind(window.history);
1353
+ window.history.pushState = (...args) => {
1354
+ originalPushState(...args);
1355
+ notify();
1356
+ };
1357
+ window.history.replaceState = (...args) => {
1358
+ originalReplaceState(...args);
1359
+ notify();
1360
+ };
1361
+ window.addEventListener("popstate", notify);
1362
+ }
1363
+ function useLocationHref() {
1364
+ const [href, setHref] = react.useState(
1365
+ () => typeof window === "undefined" ? "" : window.location.href
1366
+ );
1367
+ react.useEffect(() => {
1368
+ ensureHistoryPatched();
1369
+ const onChange = () => setHref(window.location.href);
1370
+ locationChangeListeners.add(onChange);
1371
+ return () => {
1372
+ locationChangeListeners.delete(onChange);
1373
+ };
1374
+ }, []);
1375
+ return href;
1376
+ }
1377
+ function specOverlayFetchJson(path) {
1378
+ return fetch(`${SPECS_ORIGIN}${path}`, { credentials: "include" }).then((res) => {
1379
+ if (!res.ok) throw new Error(`spec overlay ${path}: ${res.status}`);
1380
+ return res.json();
1381
+ });
1382
+ }
1383
+ function SpecOverlayAction({ orgId } = {}) {
1384
+ const [access, setAccess] = react.useState(() => readSpecOverlayAccessCache());
1385
+ const [status, setStatus] = react.useState(null);
1386
+ const [open, setOpen] = react.useState(false);
1387
+ const href = useLocationHref();
1388
+ react.useEffect(() => {
1389
+ if (access) return;
1390
+ let cancelled = false;
1391
+ specOverlayFetchJson("/api/overlay/access").then((data) => {
1392
+ if (cancelled) return;
1393
+ const next = data && typeof data.allowed === "boolean" ? data : { allowed: false };
1394
+ setAccess(next);
1395
+ writeSpecOverlayAccessCache(next);
1396
+ }).catch(() => {
1397
+ if (cancelled) return;
1398
+ const next = { allowed: false };
1399
+ setAccess(next);
1400
+ writeSpecOverlayAccessCache(next);
1401
+ });
1402
+ return () => {
1403
+ cancelled = true;
1404
+ };
1405
+ }, [access]);
1406
+ react.useEffect(() => {
1407
+ if (!access?.allowed || typeof window === "undefined") return;
1408
+ const timer = window.setTimeout(() => {
1409
+ const url = `/api/overlay/status?url=${encodeURIComponent(window.location.href)}`;
1410
+ specOverlayFetchJson(url).then((data) => setStatus(data)).catch(() => setStatus(null));
1411
+ }, 300);
1412
+ return () => window.clearTimeout(timer);
1413
+ }, [access?.allowed, href, orgId]);
1414
+ if (!access?.allowed) return null;
1415
+ const badgeLabel = status && status.matched ? `${Math.round(status.mismatch_pct)}%` : status ? "\u2014" : void 0;
1416
+ const badgeTone = status && status.matched ? status.passed ? "default" : "danger" : "default";
1417
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1418
+ /* @__PURE__ */ jsxRuntime.jsxs(
1419
+ "button",
1420
+ {
1421
+ type: "button",
1422
+ "aria-label": "Spec",
1423
+ "aria-expanded": open,
1424
+ className: topBarIconButtonClass(badgeTone),
1425
+ onClick: () => setOpen(true),
1426
+ children: [
1427
+ /* @__PURE__ */ jsxRuntime.jsx(LayoutGlyph, { size: 20, className: "text-current" }),
1428
+ badgeLabel ? /* @__PURE__ */ jsxRuntime.jsx(
1429
+ "span",
1430
+ {
1431
+ className: [
1432
+ "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-white",
1433
+ status && status.matched ? status.passed ? "bg-[color:var(--chrome-success,#2f855a)]" : "bg-[color:var(--chrome-danger)]" : "bg-[color:var(--muted)]"
1434
+ ].join(" "),
1435
+ children: badgeLabel
1436
+ }
1437
+ ) : null,
1438
+ !open ? /* @__PURE__ */ jsxRuntime.jsx(
1439
+ TopBarTooltip,
1440
+ {
1441
+ title: "Spec",
1442
+ description: "Spec design + visual regression for this page."
1443
+ }
1444
+ ) : null
1445
+ ]
1446
+ }
1447
+ ),
1448
+ open && typeof document !== "undefined" ? reactDom.createPortal(
1449
+ /* @__PURE__ */ jsxRuntime.jsx(
1450
+ "div",
1451
+ {
1452
+ className: "fixed inset-0 z-50 flex justify-end bg-black/40",
1453
+ role: "dialog",
1454
+ "aria-modal": "true",
1455
+ "aria-label": "Spec overlay",
1456
+ onMouseDown: (e) => {
1457
+ if (e.target === e.currentTarget) setOpen(false);
1458
+ },
1459
+ onKeyDown: (e) => {
1460
+ if (e.key === "Escape") setOpen(false);
1461
+ },
1462
+ 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: [
1463
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-[color:var(--line)] px-4 py-3", children: [
1464
+ /* @__PURE__ */ jsxRuntime.jsxs("h2", { className: "truncate text-sm font-bold tracking-[-0.01em] text-[color:var(--foreground)]", children: [
1465
+ "Spec",
1466
+ status && status.matched ? `: ${status.spec_title}` : ""
1467
+ ] }),
1468
+ /* @__PURE__ */ jsxRuntime.jsx(
1469
+ "button",
1470
+ {
1471
+ type: "button",
1472
+ "aria-label": "Close",
1473
+ onClick: () => setOpen(false),
1474
+ className: "rounded-full p-1 text-[color:var(--muted)] hover:bg-[color:var(--panel)] hover:text-[color:var(--foreground)]",
1475
+ children: /* @__PURE__ */ jsxRuntime.jsx(CloseGlyph, { size: 16 })
1476
+ }
1477
+ )
1478
+ ] }),
1479
+ status && status.matched ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1480
+ /* @__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: [
1481
+ /* @__PURE__ */ jsxRuntime.jsx(
1482
+ "a",
1483
+ {
1484
+ href: status.spec_url,
1485
+ target: "_blank",
1486
+ rel: "noreferrer",
1487
+ className: "text-[color:var(--color-accent)] hover:underline",
1488
+ children: "Open spec \u2197"
1489
+ }
1490
+ ),
1491
+ /* @__PURE__ */ jsxRuntime.jsx(
1492
+ "a",
1493
+ {
1494
+ href: status.compare_url,
1495
+ target: "_blank",
1496
+ rel: "noreferrer",
1497
+ className: "text-[color:var(--color-accent)] hover:underline",
1498
+ children: "Compare tab \u2197"
1499
+ }
1500
+ )
1501
+ ] }),
1502
+ /* @__PURE__ */ jsxRuntime.jsx(
1503
+ "iframe",
1504
+ {
1505
+ src: status.overlay_url,
1506
+ title: "Spec overlay",
1507
+ className: "h-full w-full flex-1 border-0"
1508
+ }
1509
+ )
1510
+ ] }) : status && !status.matched ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 text-sm text-[color:var(--muted)]", children: [
1511
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: "No compare target covers this page." }),
1512
+ /* @__PURE__ */ jsxRuntime.jsx(
1513
+ "a",
1514
+ {
1515
+ href: status.search_url,
1516
+ target: "_blank",
1517
+ rel: "noreferrer",
1518
+ className: "mt-2 inline-block font-semibold text-[color:var(--color-accent)] hover:underline",
1519
+ children: "Search specs \u2197"
1520
+ }
1521
+ )
1522
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4", children: /* @__PURE__ */ jsxRuntime.jsx(ValueSkeleton, { className: "h-4 w-full" }) })
1523
+ ] })
1524
+ }
1525
+ ),
1526
+ document.body
1527
+ ) : null
1528
+ ] });
1529
+ }
1530
+ function CloseGlyph({ size = 16, className }) {
1531
+ 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: [
1532
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1533
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1534
+ ] });
1535
+ }
1314
1536
  var NAV_DISPLAY_OPTIONS = [
1315
1537
  { value: "menu", label: "Sub Menu" },
1316
1538
  { value: "tabs", label: "Tabs Only" },
@@ -1872,6 +2094,7 @@ function NavSidebar({
1872
2094
  "fixed inset-y-0 left-0 w-[240px] flex-col border-r border-[color:var(--line)] bg-[color:var(--sidebar-bg,var(--nav-surface))] lg:flex",
1873
2095
  open ? "flex z-50" : "hidden z-30"
1874
2096
  ),
2097
+ style: { fontFamily: 'var(--font-family, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif)' },
1875
2098
  onKeyDown: trapTab,
1876
2099
  onClick: (e) => {
1877
2100
  if (open && e.target.closest("a[href]")) onClose?.();
@@ -4210,18 +4433,71 @@ function AppDevNotes({
4210
4433
  }),
4211
4434
  [storagePrefix, identity.role]
4212
4435
  );
4213
- if (devNotes?.providedByApp) return /* @__PURE__ */ jsxRuntime.jsx(DevNotesSurface, { children });
4214
- return /* @__PURE__ */ jsxRuntime.jsx(devnotes.DevNotesProvider, { adapter, user, config, children: /* @__PURE__ */ jsxRuntime.jsx(DevNotesSurface, { children }) });
4436
+ const activatedRef = react.useRef(false);
4437
+ const activationWaitersRef = react.useRef([]);
4438
+ const activate = react.useCallback(() => {
4439
+ if (activatedRef.current) return;
4440
+ activatedRef.current = true;
4441
+ const waiters = activationWaitersRef.current;
4442
+ activationWaitersRef.current = [];
4443
+ waiters.forEach((resolve) => resolve());
4444
+ }, []);
4445
+ const waitForActivation = react.useCallback(() => {
4446
+ if (activatedRef.current) return Promise.resolve();
4447
+ return new Promise((resolve) => {
4448
+ activationWaitersRef.current.push(resolve);
4449
+ });
4450
+ }, []);
4451
+ const lazyAdapter = react.useMemo(() => {
4452
+ const GATED_METHODS = [
4453
+ "fetchTasks",
4454
+ "fetchTaskTypes",
4455
+ "fetchTaskLists",
4456
+ "fetchCollaborators",
4457
+ "fetchCapabilities",
4458
+ "getAppLinkStatus",
4459
+ "fetchBugReports",
4460
+ "fetchBugReportTypes"
4461
+ ];
4462
+ const wrapped = { ...adapter };
4463
+ for (const method of GATED_METHODS) {
4464
+ const original = adapter[method].bind(adapter);
4465
+ wrapped[method] = (...args) => waitForActivation().then(() => original(...args));
4466
+ }
4467
+ let idleScheduled = false;
4468
+ const originalUnreadCounts = adapter.fetchUnreadCounts.bind(adapter);
4469
+ wrapped.fetchUnreadCounts = () => {
4470
+ if (idleScheduled || typeof window === "undefined") return originalUnreadCounts();
4471
+ idleScheduled = true;
4472
+ return new Promise((resolve, reject) => {
4473
+ const run = () => originalUnreadCounts().then(resolve, reject);
4474
+ const ric = window.requestIdleCallback;
4475
+ if (ric) ric(run);
4476
+ else setTimeout(run, 0);
4477
+ });
4478
+ };
4479
+ return wrapped;
4480
+ }, [adapter, waitForActivation]);
4481
+ if (devNotes?.providedByApp) return /* @__PURE__ */ jsxRuntime.jsx(DevNotesSurface, { activate, children });
4482
+ return /* @__PURE__ */ jsxRuntime.jsx(devnotes.DevNotesProvider, { adapter: lazyAdapter, user, config, children: /* @__PURE__ */ jsxRuntime.jsx(DevNotesSurface, { activate, children }) });
4215
4483
  }
4216
4484
  var DevNotesTasksContext = react.createContext({
4217
4485
  ready: false,
4218
4486
  openTasks: () => {
4487
+ },
4488
+ activate: () => {
4219
4489
  }
4220
4490
  });
4221
- function DevNotesSurface({ children }) {
4491
+ function DevNotesSurface({
4492
+ activate,
4493
+ children
4494
+ }) {
4222
4495
  const [tasksOpen, setTasksOpen] = react.useState(false);
4223
- const openTasks = react.useCallback(() => setTasksOpen(true), []);
4224
- const tasksCtx = react.useMemo(() => ({ ready: true, openTasks }), [openTasks]);
4496
+ const openTasks = react.useCallback(() => {
4497
+ activate();
4498
+ setTasksOpen(true);
4499
+ }, [activate]);
4500
+ const tasksCtx = react.useMemo(() => ({ ready: true, openTasks, activate }), [openTasks, activate]);
4225
4501
  return /* @__PURE__ */ jsxRuntime.jsxs(DevNotesTasksContext.Provider, { value: tasksCtx, children: [
4226
4502
  children,
4227
4503
  tasksOpen ? /* @__PURE__ */ jsxRuntime.jsx(devnotes.DevNotesTaskListModal, { open: true, onClose: () => setTasksOpen(false) }) : null,
@@ -4277,14 +4553,15 @@ function ChromeTopBarActions() {
4277
4553
  children: /* @__PURE__ */ jsxRuntime.jsx(ChromeDevNotesMenu, {})
4278
4554
  }
4279
4555
  ),
4556
+ /* @__PURE__ */ jsxRuntime.jsx(SpecOverlayAction, { orgId: state.orgId }),
4280
4557
  /* @__PURE__ */ jsxRuntime.jsx(SupportAction, { orgId: state.orgId }),
4281
4558
  /* @__PURE__ */ jsxRuntime.jsx(NotificationsAction, { orgId: state.orgId })
4282
4559
  ] });
4283
4560
  }
4284
4561
  function ChromeDevNotesMenu() {
4285
- const { ready, openTasks } = react.useContext(DevNotesTasksContext);
4562
+ const { ready, openTasks, activate } = react.useContext(DevNotesTasksContext);
4286
4563
  if (!ready) return null;
4287
- return /* @__PURE__ */ jsxRuntime.jsx(devnotes.DevNotesMenu, { position: "top-right", onViewTasks: openTasks });
4564
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { style: { display: "contents" }, onPointerDownCapture: activate, onFocusCapture: activate, onKeyDownCapture: activate, children: /* @__PURE__ */ jsxRuntime.jsx(devnotes.DevNotesMenu, { position: "top-right", onViewTasks: openTasks }) });
4288
4565
  }
4289
4566
  function useIsSuperAdmin() {
4290
4567
  const state = useChromeState();
@@ -4300,10 +4577,9 @@ var FOOTER_LINKS = [
4300
4577
  }
4301
4578
  ];
4302
4579
  function useChromeFooter(meta) {
4303
- const isSuperAdmin = useIsSuperAdmin();
4304
4580
  return react.useMemo(
4305
- () => isSuperAdmin ? { links: FOOTER_LINKS, ...meta } : { links: FOOTER_LINKS },
4306
- [isSuperAdmin, meta.envLabel, meta.version, meta.commit, meta.commitDate, meta.commitAuthor]
4581
+ () => ({ links: FOOTER_LINKS, ...meta }),
4582
+ [meta.envLabel, meta.version, meta.commit, meta.commitDate, meta.commitAuthor]
4307
4583
  );
4308
4584
  }
4309
4585
  function useChromeIdentity(fallback) {