@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 +63 -0
- package/dist/app-chrome.cjs +325 -49
- package/dist/app-chrome.cjs.map +1 -1
- package/dist/app-chrome.d.cts +7 -3
- package/dist/app-chrome.d.ts +7 -3
- package/dist/app-chrome.js +64 -11
- package/dist/app-chrome.js.map +1 -1
- package/dist/{chunk-MR6TLHEE.js → chunk-BV2QF7NY.js} +266 -43
- package/dist/chunk-BV2QF7NY.js.map +1 -0
- package/dist/native.cjs +264 -39
- package/dist/native.cjs.map +1 -1
- package/dist/native.d.cts +6 -1
- package/dist/native.d.ts +6 -1
- package/dist/native.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-MR6TLHEE.js.map +0 -1
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)
|
package/dist/app-chrome.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
}, [
|
|
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 {
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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 {
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
}, [
|
|
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 {
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
4214
|
-
|
|
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({
|
|
4491
|
+
function DevNotesSurface({
|
|
4492
|
+
activate,
|
|
4493
|
+
children
|
|
4494
|
+
}) {
|
|
4222
4495
|
const [tasksOpen, setTasksOpen] = react.useState(false);
|
|
4223
|
-
const openTasks = react.useCallback(() =>
|
|
4224
|
-
|
|
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
|
-
() =>
|
|
4306
|
-
[
|
|
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) {
|