@the-portland-company/shell 0.56.0 → 0.57.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,51 @@
1
+ ## 0.57.0 — 2026-09-30
2
+
3
+ ### Fixed — 10DLC alert no longer auto-redirects on every login (`src/native/chromeActions.tsx`)
4
+
5
+ The once-per-session `10dlc_registration` alert redirect fired on every
6
+ login because every login is a new browser session (a fresh
7
+ `sessionStorage`), so the full-page `window.location.assign` re-booted the
8
+ app on every single sign-in — visible to the user as a second loading-screen
9
+ blink that always lands on
10
+ `/organization-settings/subscription-billing/text-email`. The auto-redirect
11
+ is removed entirely. The alert still surfaces in the notifications
12
+ bell/panel (unread, like any other alert) — that panel is now the only
13
+ user-visible surface for it.
14
+
15
+ ### Added — Spec overlay top-bar action, Spencer + Ben only (`./native`, `./app-chrome`)
16
+
17
+ A new `SpecOverlayAction` top-bar control (wired into `ChromeTopBarActions`,
18
+ between DevNotes and Support) shows the live spec/visual-regression status
19
+ of the current page, sourced from the Specs app's overlay API
20
+ (`https://specs.politogyvrm.com/api/overlay/access` and `/api/overlay/status`).
21
+ It renders nothing — no icon, no network noise — for anyone the access
22
+ check doesn't allow; the allow/deny answer is cached in `sessionStorage`
23
+ for the session. When allowed, a "Spec" button shows a rounded
24
+ mismatch-percentage badge (green when the compare passed, red when it
25
+ didn't, a neutral "—" when the page has no compare target yet) and opens a
26
+ 480px right-hand drawer with the spec title, "Open spec" / "Compare tab"
27
+ links, and an iframe of the live overlay. Status re-fetches (debounced
28
+ 300ms) on route change and org change. All calls are plain
29
+ `fetch(..., { credentials: 'include' })` against the Specs origin — this
30
+ does NOT go through the same-origin chrome proxy `fetchJson`.
31
+
32
+ ### Fixed — de-duped chrome fetches + lazy DevNotes boot fetch (`src/native/chromeActions.tsx`, `src/app-chrome/index.tsx`)
33
+
34
+ `useChromeOrgs`, `me`, `FuelAction`'s `communications/fuel-status`, and
35
+ `NotificationsAction`'s `notifications` were each fetched 2-4x per page load
36
+ (consumers mounting independently), costing 1-3.5s per redundant call. Added
37
+ a provider-scoped in-flight-promise + 30s-TTL cache
38
+ (`dedupedFetch`/`invalidateChromeCache` on `useNavChrome`) keyed by
39
+ `(path, orgId)`, so concurrent consumers share one request and org switches
40
+ naturally miss the cache. Separately, DevNotes' boot-time reads (tasks,
41
+ task types, task lists, collaborators, app-link status, capabilities — up to
42
+ 6.8s cold) are deferred until the DevNotes panel is first opened by wrapping
43
+ the adapter passed into `@the-portland-company/devnotes`'s
44
+ `DevNotesProvider`; the launcher button renders instantly and unblocks the
45
+ gated calls via `onPointerDownCapture` before the click fires. The unread
46
+ badge count still resolves on boot, via `requestIdleCallback` instead of
47
+ blocking it.
48
+
1
49
  ## 0.56.0 — 2026-09-30
2
50
 
3
51
  ### 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" },
@@ -4210,18 +4432,71 @@ function AppDevNotes({
4210
4432
  }),
4211
4433
  [storagePrefix, identity.role]
4212
4434
  );
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 }) });
4435
+ const activatedRef = react.useRef(false);
4436
+ const activationWaitersRef = react.useRef([]);
4437
+ const activate = react.useCallback(() => {
4438
+ if (activatedRef.current) return;
4439
+ activatedRef.current = true;
4440
+ const waiters = activationWaitersRef.current;
4441
+ activationWaitersRef.current = [];
4442
+ waiters.forEach((resolve) => resolve());
4443
+ }, []);
4444
+ const waitForActivation = react.useCallback(() => {
4445
+ if (activatedRef.current) return Promise.resolve();
4446
+ return new Promise((resolve) => {
4447
+ activationWaitersRef.current.push(resolve);
4448
+ });
4449
+ }, []);
4450
+ const lazyAdapter = react.useMemo(() => {
4451
+ const GATED_METHODS = [
4452
+ "fetchTasks",
4453
+ "fetchTaskTypes",
4454
+ "fetchTaskLists",
4455
+ "fetchCollaborators",
4456
+ "fetchCapabilities",
4457
+ "getAppLinkStatus",
4458
+ "fetchBugReports",
4459
+ "fetchBugReportTypes"
4460
+ ];
4461
+ const wrapped = { ...adapter };
4462
+ for (const method of GATED_METHODS) {
4463
+ const original = adapter[method].bind(adapter);
4464
+ wrapped[method] = (...args) => waitForActivation().then(() => original(...args));
4465
+ }
4466
+ let idleScheduled = false;
4467
+ const originalUnreadCounts = adapter.fetchUnreadCounts.bind(adapter);
4468
+ wrapped.fetchUnreadCounts = () => {
4469
+ if (idleScheduled || typeof window === "undefined") return originalUnreadCounts();
4470
+ idleScheduled = true;
4471
+ return new Promise((resolve, reject) => {
4472
+ const run = () => originalUnreadCounts().then(resolve, reject);
4473
+ const ric = window.requestIdleCallback;
4474
+ if (ric) ric(run);
4475
+ else setTimeout(run, 0);
4476
+ });
4477
+ };
4478
+ return wrapped;
4479
+ }, [adapter, waitForActivation]);
4480
+ if (devNotes?.providedByApp) return /* @__PURE__ */ jsxRuntime.jsx(DevNotesSurface, { activate, children });
4481
+ return /* @__PURE__ */ jsxRuntime.jsx(devnotes.DevNotesProvider, { adapter: lazyAdapter, user, config, children: /* @__PURE__ */ jsxRuntime.jsx(DevNotesSurface, { activate, children }) });
4215
4482
  }
4216
4483
  var DevNotesTasksContext = react.createContext({
4217
4484
  ready: false,
4218
4485
  openTasks: () => {
4486
+ },
4487
+ activate: () => {
4219
4488
  }
4220
4489
  });
4221
- function DevNotesSurface({ children }) {
4490
+ function DevNotesSurface({
4491
+ activate,
4492
+ children
4493
+ }) {
4222
4494
  const [tasksOpen, setTasksOpen] = react.useState(false);
4223
- const openTasks = react.useCallback(() => setTasksOpen(true), []);
4224
- const tasksCtx = react.useMemo(() => ({ ready: true, openTasks }), [openTasks]);
4495
+ const openTasks = react.useCallback(() => {
4496
+ activate();
4497
+ setTasksOpen(true);
4498
+ }, [activate]);
4499
+ const tasksCtx = react.useMemo(() => ({ ready: true, openTasks, activate }), [openTasks, activate]);
4225
4500
  return /* @__PURE__ */ jsxRuntime.jsxs(DevNotesTasksContext.Provider, { value: tasksCtx, children: [
4226
4501
  children,
4227
4502
  tasksOpen ? /* @__PURE__ */ jsxRuntime.jsx(devnotes.DevNotesTaskListModal, { open: true, onClose: () => setTasksOpen(false) }) : null,
@@ -4277,14 +4552,15 @@ function ChromeTopBarActions() {
4277
4552
  children: /* @__PURE__ */ jsxRuntime.jsx(ChromeDevNotesMenu, {})
4278
4553
  }
4279
4554
  ),
4555
+ /* @__PURE__ */ jsxRuntime.jsx(SpecOverlayAction, { orgId: state.orgId }),
4280
4556
  /* @__PURE__ */ jsxRuntime.jsx(SupportAction, { orgId: state.orgId }),
4281
4557
  /* @__PURE__ */ jsxRuntime.jsx(NotificationsAction, { orgId: state.orgId })
4282
4558
  ] });
4283
4559
  }
4284
4560
  function ChromeDevNotesMenu() {
4285
- const { ready, openTasks } = react.useContext(DevNotesTasksContext);
4561
+ const { ready, openTasks, activate } = react.useContext(DevNotesTasksContext);
4286
4562
  if (!ready) return null;
4287
- return /* @__PURE__ */ jsxRuntime.jsx(devnotes.DevNotesMenu, { position: "top-right", onViewTasks: openTasks });
4563
+ 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
4564
  }
4289
4565
  function useIsSuperAdmin() {
4290
4566
  const state = useChromeState();