@the-portland-company/shell 0.76.15 → 0.77.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 +39 -0
- package/dist/app-chrome.cjs +131 -195
- package/dist/app-chrome.cjs.map +1 -1
- package/dist/app-chrome.d.cts +9 -4
- package/dist/app-chrome.d.ts +9 -4
- package/dist/app-chrome.js +8 -6
- package/dist/app-chrome.js.map +1 -1
- package/dist/{chunk-HSC6EYRR.js → chunk-GGM4AD43.js} +6 -1769
- package/dist/chunk-GGM4AD43.js.map +1 -0
- package/dist/chunk-TUK2IEL5.js +2002 -0
- package/dist/chunk-TUK2IEL5.js.map +1 -0
- package/dist/index-Cu5frXRQ.d.cts +990 -0
- package/dist/index-DJ3qBzfQ.d.ts +990 -0
- package/dist/index.cjs +170 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +37 -36
- package/dist/index.js.map +1 -1
- package/dist/native.cjs +131 -15
- package/dist/native.cjs.map +1 -1
- package/dist/native.d.cts +4 -929
- package/dist/native.d.ts +4 -929
- package/dist/native.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-HSC6EYRR.js.map +0 -1
- package/dist/chunk-MY6G3LMN.js +0 -125
- package/dist/chunk-MY6G3LMN.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
## 0.77.0
|
|
2
|
+
|
|
3
|
+
No theme control in the account menu. Specs cited: Reference/UI-Platform "Theme — light only (Dark Mode retired)" and "There is no theme control … Apps must not render any theme control"; Reference/Global-Rules "Theming — Light Only".
|
|
4
|
+
|
|
5
|
+
- **Breaking for the eye only.** `PolitogyAppFrame` no longer passes `theme` to the account menu, so the "Appearance" radio group (one option, "Light", since 0.73.0 made the theme light only) is gone from every app. `NavUserMenu.theme` stays optional for a caller that still passes it; nothing in the shell does.
|
|
6
|
+
- `./theme-preference` and `useThemePreference` are unchanged: they still resolve every stored preference to light and keep the cross-app cookie contract for the apps that import them.
|
|
7
|
+
- **Audit note (closeout 2026-10-12, item 1).** The "Known shell defect — dark theme restored in 0.68.0" paragraph in Reference/UI-Platform is stale: 0.73.0 (#116, 2026-10-06) removed dark rendering, `chrome.css` ships no `.dark` block (the remaining mentions are comments), and the pre-paint script hardcodes light. The runtime Brand token sheet (specs app, `lib/brand-authoring.ts`) still emits `.dark {}` blocks that nothing applies.
|
|
8
|
+
|
|
9
|
+
## 0.76.18
|
|
10
|
+
|
|
11
|
+
Consolidates 0.76.16 and 0.76.17, neither of which reached npm (their publish gates failed). Specs cited: Global-Rules v38, UI-Platform v46, Specs/Left-Navigation (no Tribes row, §9.1), Specs/System-Communications §6.3 and §12.2 (Binding, 2026-10-03).
|
|
12
|
+
|
|
13
|
+
- **`NotificationsAdapter` (comms bell).** `src/notifications/adapter.ts`: `list(orgId)`, `markRead`, `archive`, `resolve`. The default reads `me/notifications` through the chrome proxy with the viewer's session (comms `GET /v1/me/notifications`, `POST /v1/me/notifications/:id/{read,archive,resolve}`, politogy-comms #14). Any failure (401, 404, network, malformed body) is an empty bell, never an error; owner actions resolve `false` instead of throwing. `NavChromeProvider` and `PolitogyAppChrome` take `notificationsAdapter`: omitted = comms (default); `null` = legacy contacts-shaped feed (replies, imports, quarantine). Alert rows read the server `read_at`; the badge is the server count.
|
|
14
|
+
- **Behavior change for consumers.** Apps that rely on the legacy contacts replies, imports or quarantine rows in the bell must pass `notificationsAdapter={null}`.
|
|
15
|
+
- **Tests.** `__tests__/notificationsAdapter.test.ts` (new). `__tests__/chromeFetchDedupe.test.tsx` counts `me/notifications`. `__tests__/chromeActions.test.tsx` pins its legacy `NotificationsAction` tests to `notificationsAdapter={null}`; this is what fixed the 8 CI-only failures that blocked the 0.76.16 publish gate.
|
|
16
|
+
- **Tribes.** No `tribes` registry row in this release. Tribes lives inside Contacts; a `tribes.politogyvrm.com` entry would point at an empty scaffold.
|
|
17
|
+
- **Tab containers are left-aligned and fluid** (Spencer, 2026-10-11: "all apps, all tabs, all screens"; Reference/Global-Rules "Tabs & UI Styling": the strip spans the full width of the content area, tabs keep their natural width, left-aligned). `HeaderTabBar` (deprecated) no longer stretches each tab with `flex-1`: tabs sit at natural width packed from the left on a full-width strip (40px hit area). `NavTabStrip` already met this (full-width block, `flex` with default start alignment, natural-width tabs) and is unchanged; `__tests__/tabsLeftAligned.test.tsx` (new) pins both so neither grows `mx-auto`, `justify-center` or a fixed width.
|
|
18
|
+
|
|
19
|
+
## 0.76.17
|
|
20
|
+
|
|
21
|
+
Tooltips on the Spec tooltip theme, and the new-reply toast no longer paints over the bell's tooltip. Specs cited: Brand guideline "Tooltips point at their trigger" (`tooltips-point-at-their-trigger`, Spencer 2026-10-06), Reference/UI-Platform "Top-bar tooltips and panels - one shared treatment", Reference/Global-Rules "Tooltip on every tab / UX", tokens v150 `shellChrome.button.tooltipBg/Fg`.
|
|
22
|
+
|
|
23
|
+
- **`TopBarTooltip` is styled on the tooltip tokens.** Body, border and arrow use `--app-btn-tooltip-bg` (v150: #2F3136), title and description use `--app-btn-tooltip-fg` (#FFFFFF, description at 75% opacity). It was on the panel surface (`--panel-elevated` / `--line`), which is why it rendered as a light card. Opens on keyboard focus (`group-focus-visible/tbtip`) as well as hover, per the guideline.
|
|
24
|
+
- **Toast/tooltip overlap (a2p dashboard, reported 2026-10-11).** The new-reply toast and the bell's hover tooltip are both z-50 under the same trigger; hovering the bell while a toast was up painted the toast over "Alerts & Notifications". `TopBarPopover` takes `hideTooltip`; `NotificationsAction` passes it while the toast is visible and `FuelAction` while the low-fuel hint is up. One surface per trigger.
|
|
25
|
+
- **`HeaderTabBar` (deprecated) uses the shared tooltip** instead of Chakra's `Tooltip`; the shell no longer ships a second tooltip.
|
|
26
|
+
- Tests: `__tests__/tooltipTheme.test.tsx` (new).
|
|
27
|
+
- **Not changed:** `src/css/chrome.css` still carries the pre-v150 fallbacks (`--app-btn-tooltip-bg: #0F172A`, mode primaries). `npm run gen:tokens` against Specs v150 produces a 508-line diff including `--mode-relationship-primary` #D32F3B -> #2F3136, which is a Brand-token question, not this fix; the runtime token sheet (`ensureRuntimeBrandTokens`) already overrides every fallback live.
|
|
28
|
+
- **Still open:** `aria-describedby` from trigger to tooltip (guideline) is not wired; apps outside the chrome still use native `title=` attributes rather than `TopBarTooltip` (exported from `./native`).
|
|
29
|
+
|
|
30
|
+
## 0.76.16
|
|
31
|
+
|
|
32
|
+
The shell's Notifications bell reads the comms feed through a `NotificationsAdapter`. Specs cited: Global-Rules v38, UI-Platform v46, Specs/Left-Navigation (no Tribes row, §9.1), Specs/System-Communications §6.3 and §12.2 (Binding, 2026-10-03).
|
|
33
|
+
|
|
34
|
+
- **`NotificationsAdapter` (bell).** New `src/notifications/adapter.ts`: `list(orgId)`, `markRead`, `archive`, `resolve`. The default reads `me/notifications` through the chrome proxy with the viewer's session, which is the comms `GET /v1/me/notifications` and `POST /v1/me/notifications/:id/{read,archive,resolve}` (politogy-comms #14). Any failure (401, 404, network, malformed body) is an empty bell, never an error state; owner actions resolve `false` instead of throwing.
|
|
35
|
+
- **Bell wiring.** `NavChromeProvider` and `PolitogyAppChrome` take `notificationsAdapter`. Omitted, the comms adapter is used. `null` keeps the legacy contacts-shaped feed (replies, imports, quarantine) for any app that needs it. Alert rows read the server `read_at` state; the badge is the server's single count.
|
|
36
|
+
- Exports: `createCommsNotificationsAdapter`, `normalizeNotificationsFeed`, `emptyNotificationsFeed` and the adapter types from the package root.
|
|
37
|
+
- Tests: `__tests__/notificationsAdapter.test.ts` (new); `__tests__/chromeFetchDedupe.test.tsx` now counts `me/notifications`. The 41 pre-existing localStorage/cookie failures are unchanged and not addressed.
|
|
38
|
+
- **Still open:** `subscribe` (realtime) is not implemented; the bell refreshes on open and after a mark-read. Resolve and archive are on the adapter but have no panel control yet.
|
|
39
|
+
|
|
1
40
|
## 0.76.15
|
|
2
41
|
|
|
3
42
|
TITLEBAR: the chrome no longer draws the 56px page-title `<h1>` strip (for example "A2P Messaging" above the "CAMPAIGN MODE" eyebrow) for feature apps. The Design shows the page opening on its own eyebrow + H2, so `PolitogyAppFrame` now passes only core's own page title as `topBar.title`. The `title` grant is kept but inert; removing it is a follow-up.
|
package/dist/app-chrome.cjs
CHANGED
|
@@ -415,91 +415,78 @@ function SpeculationRulesScript({ items }) {
|
|
|
415
415
|
return null;
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
// src/
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
);
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
)}; Path=/; Max-Age=31536000; SameSite=Lax${domainAttr}${secure}`;
|
|
447
|
-
}
|
|
448
|
-
function writeSharedCookie(name, value, maxAgeSeconds) {
|
|
449
|
-
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
450
|
-
const host = window.location.hostname;
|
|
451
|
-
const domainAttr = /(^|\.)politogyvrm\.com$/i.test(host) ? "; Domain=.politogyvrm.com" : "";
|
|
452
|
-
const secure = window.location.protocol === "https:" ? "; Secure" : "";
|
|
453
|
-
document.cookie = `${name}=${encodeURIComponent(
|
|
454
|
-
value
|
|
455
|
-
)}; Path=/; Max-Age=${maxAgeSeconds}; SameSite=Lax${domainAttr}${secure}`;
|
|
456
|
-
}
|
|
457
|
-
function markThemeChoicePending(pref) {
|
|
458
|
-
writeSharedCookie(THEME_PENDING_COOKIE, pref, 31536e3);
|
|
459
|
-
}
|
|
460
|
-
function readPendingThemeChoice() {
|
|
461
|
-
if (typeof document === "undefined") return null;
|
|
462
|
-
try {
|
|
463
|
-
const m = document.cookie.match(
|
|
464
|
-
new RegExp(`(?:^|;\\s*)${THEME_PENDING_COOKIE}=([^;]+)`)
|
|
465
|
-
);
|
|
466
|
-
const v = m ? decodeURIComponent(m[1]) : null;
|
|
467
|
-
return isThemePreference(v) ? v : null;
|
|
468
|
-
} catch {
|
|
469
|
-
return null;
|
|
418
|
+
// src/notifications/adapter.ts
|
|
419
|
+
function emptyNotificationsFeed() {
|
|
420
|
+
return { notifications: [], badge: 0 };
|
|
421
|
+
}
|
|
422
|
+
function asString(value) {
|
|
423
|
+
return typeof value === "string" ? value : null;
|
|
424
|
+
}
|
|
425
|
+
function normalizeNotificationsFeed(raw) {
|
|
426
|
+
if (!raw || typeof raw !== "object") return emptyNotificationsFeed();
|
|
427
|
+
const body = raw;
|
|
428
|
+
const rows = Array.isArray(body.notifications) ? body.notifications : [];
|
|
429
|
+
const notifications = [];
|
|
430
|
+
for (const row of rows) {
|
|
431
|
+
if (!row || typeof row !== "object") continue;
|
|
432
|
+
const r = row;
|
|
433
|
+
const id = asString(r.id);
|
|
434
|
+
if (!id || asString(r.archived_at)) continue;
|
|
435
|
+
notifications.push({
|
|
436
|
+
id,
|
|
437
|
+
kind: r.kind === "alert" ? "alert" : "notification",
|
|
438
|
+
title: asString(r.title) ?? "",
|
|
439
|
+
body: asString(r.body) ?? "",
|
|
440
|
+
icon: asString(r.icon),
|
|
441
|
+
link: asString(r.link),
|
|
442
|
+
readAt: asString(r.read_at),
|
|
443
|
+
orgId: asString(r.org_id),
|
|
444
|
+
createdAt: asString(r.created_at)
|
|
445
|
+
});
|
|
470
446
|
}
|
|
447
|
+
const badge = typeof body.badge === "number" && Number.isFinite(body.badge) ? Math.max(0, Math.floor(body.badge)) : notifications.filter((n) => n.kind === "alert" || n.readAt === null).length;
|
|
448
|
+
return { notifications, badge };
|
|
471
449
|
}
|
|
472
|
-
function
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
450
|
+
function createCommsNotificationsAdapter(fetchJson) {
|
|
451
|
+
const act = async (id, action) => {
|
|
452
|
+
if (!fetchJson || !id) return false;
|
|
453
|
+
try {
|
|
454
|
+
await fetchJson(`me/notifications/${encodeURIComponent(id)}/${action}`, { method: "POST" });
|
|
455
|
+
return true;
|
|
456
|
+
} catch {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
return {
|
|
461
|
+
async list(orgId) {
|
|
462
|
+
if (!fetchJson) return emptyNotificationsFeed();
|
|
463
|
+
try {
|
|
464
|
+
const query = orgId ? `?org_id=${encodeURIComponent(orgId)}` : "";
|
|
465
|
+
return normalizeNotificationsFeed(await fetchJson(`me/notifications${query}`));
|
|
466
|
+
} catch {
|
|
467
|
+
return emptyNotificationsFeed();
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
markRead: (id) => act(id, "read"),
|
|
471
|
+
archive: (id) => act(id, "archive"),
|
|
472
|
+
resolve: (id) => act(id, "resolve")
|
|
473
|
+
};
|
|
477
474
|
}
|
|
478
|
-
function
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
475
|
+
function notificationsFeedToPayload(feed, orgId) {
|
|
476
|
+
return {
|
|
477
|
+
organizationId: orgId ?? "",
|
|
478
|
+
alerts: feed.notifications.map((n) => ({
|
|
479
|
+
id: n.id,
|
|
480
|
+
title: n.title,
|
|
481
|
+
description: n.body,
|
|
482
|
+
href: n.link,
|
|
483
|
+
kind: n.kind,
|
|
484
|
+
read: n.readAt !== null
|
|
485
|
+
})),
|
|
486
|
+
replies: [],
|
|
487
|
+
badge: feed.badge
|
|
488
|
+
};
|
|
488
489
|
}
|
|
489
|
-
`(function(){try{
|
|
490
|
-
var KEY=${JSON.stringify(THEME_STORAGE_KEY)};
|
|
491
|
-
var VALID=["light","dark","system"];
|
|
492
|
-
function fromCookie(){try{var m=document.cookie.match(/(?:^|;\\s*)${THEME_COOKIE}=([^;]+)/);return m?decodeURIComponent(m[1]):null;}catch(e){return null;}}
|
|
493
|
-
var cookie=fromCookie();
|
|
494
|
-
var ls=null;try{ls=localStorage.getItem(KEY);}catch(e){}
|
|
495
|
-
var pref=cookie||ls||"system";
|
|
496
|
-
if(VALID.indexOf(pref)===-1)pref="system";
|
|
497
|
-
if(cookie&&cookie!==ls){try{localStorage.setItem(KEY,cookie);}catch(e){}}
|
|
498
|
-
var dark=false;
|
|
499
|
-
var d=document.documentElement;
|
|
500
|
-
if(dark)d.classList.add("dark");else d.classList.remove("dark");
|
|
501
|
-
try{d.setAttribute("data-theme",dark?"dark":"light");d.style.colorScheme=dark?"dark":"light";}catch(e){}
|
|
502
|
-
}catch(e){}})();`;
|
|
503
490
|
var CORE_APP_ORIGIN = "https://app.politogyvrm.com";
|
|
504
491
|
var CORE_BILLING_PATH = "/organization-settings/subscription-billing/text-email";
|
|
505
492
|
var CORE_BILLING_URL = `${CORE_APP_ORIGIN}${CORE_BILLING_PATH}`;
|
|
@@ -558,17 +545,17 @@ function TopBarTooltip({
|
|
|
558
545
|
"span",
|
|
559
546
|
{
|
|
560
547
|
role: "tooltip",
|
|
561
|
-
className: "pointer-events-none absolute left-1/2 top-full z-50 mt-2 hidden w-max max-w-56 -translate-x-1/2 flex-col rounded-lg border border-[color:var(--
|
|
548
|
+
className: "pointer-events-none absolute left-1/2 top-full z-50 mt-2 hidden w-max max-w-56 -translate-x-1/2 flex-col rounded-lg border border-[color:var(--app-btn-tooltip-bg,#2F3136)] bg-[color:var(--app-btn-tooltip-bg,#2F3136)] px-3 py-2 text-left shadow-[var(--shadow-soft)] group-hover/tbtip:flex group-focus-visible/tbtip:flex",
|
|
562
549
|
children: [
|
|
563
550
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
564
551
|
"span",
|
|
565
552
|
{
|
|
566
553
|
"aria-hidden": true,
|
|
567
|
-
className: "absolute -top-[5px] left-1/2 h-2.5 w-2.5 -translate-x-1/2 rotate-45 border-l border-t border-[color:var(--
|
|
554
|
+
className: "absolute -top-[5px] left-1/2 h-2.5 w-2.5 -translate-x-1/2 rotate-45 border-l border-t border-[color:var(--app-btn-tooltip-bg,#2F3136)] bg-[color:var(--app-btn-tooltip-bg,#2F3136)]"
|
|
568
555
|
}
|
|
569
556
|
),
|
|
570
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-semibold text-[color:var(--
|
|
571
|
-
description ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-0.5 text-[11px] leading-snug text-[color:var(--
|
|
557
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-semibold text-[color:var(--app-btn-tooltip-fg,#FFFFFF)]", children: title }),
|
|
558
|
+
description ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-0.5 text-[11px] leading-snug text-[color:var(--app-btn-tooltip-fg,#FFFFFF)] opacity-75", children: description }) : null
|
|
572
559
|
]
|
|
573
560
|
}
|
|
574
561
|
);
|
|
@@ -652,6 +639,7 @@ function createChromeFetchCache(getFetchJson) {
|
|
|
652
639
|
var NavChromeContext = react.createContext(null);
|
|
653
640
|
function NavChromeProvider({
|
|
654
641
|
fetchJson,
|
|
642
|
+
notificationsAdapter,
|
|
655
643
|
children
|
|
656
644
|
}) {
|
|
657
645
|
const fetchJsonRef = react.useRef(fetchJson);
|
|
@@ -661,6 +649,13 @@ function NavChromeProvider({
|
|
|
661
649
|
cacheRef.current = createChromeFetchCache(() => fetchJsonRef.current);
|
|
662
650
|
}
|
|
663
651
|
const { dedupedFetch, invalidate: invalidateChromeCache } = cacheRef.current;
|
|
652
|
+
const defaultAdapterRef = react.useRef(null);
|
|
653
|
+
if (!defaultAdapterRef.current) {
|
|
654
|
+
defaultAdapterRef.current = createCommsNotificationsAdapter(
|
|
655
|
+
(path, init) => fetchJsonRef.current(path, init)
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
const adapter = notificationsAdapter === void 0 ? defaultAdapterRef.current : notificationsAdapter;
|
|
664
659
|
const [me, setMe] = react.useState(null);
|
|
665
660
|
const [meLoaded, setMeLoaded] = react.useState(false);
|
|
666
661
|
react.useEffect(() => {
|
|
@@ -675,7 +670,13 @@ function NavChromeProvider({
|
|
|
675
670
|
cancelled = true;
|
|
676
671
|
};
|
|
677
672
|
}, [dedupedFetch]);
|
|
678
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
673
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
674
|
+
NavChromeContext.Provider,
|
|
675
|
+
{
|
|
676
|
+
value: { fetchJson, dedupedFetch, invalidateChromeCache, me, meLoaded, notificationsAdapter: adapter },
|
|
677
|
+
children
|
|
678
|
+
}
|
|
679
|
+
);
|
|
679
680
|
}
|
|
680
681
|
function useChromeMe() {
|
|
681
682
|
const ctx = react.useContext(NavChromeContext);
|
|
@@ -756,6 +757,7 @@ function TopBarPopover({
|
|
|
756
757
|
onOpen,
|
|
757
758
|
openToken,
|
|
758
759
|
onOpenChange,
|
|
760
|
+
hideTooltip = false,
|
|
759
761
|
children,
|
|
760
762
|
panelClassName
|
|
761
763
|
}) {
|
|
@@ -812,7 +814,7 @@ function TopBarPopover({
|
|
|
812
814
|
children: [
|
|
813
815
|
/* @__PURE__ */ jsxRuntime.jsx(SlotGlyph, { slot, fallback: fallbackIcon }),
|
|
814
816
|
typeof badge === "number" ? /* @__PURE__ */ jsxRuntime.jsx(TopBarCountBadge, { count: badge }) : null,
|
|
815
|
-
!open ? /* @__PURE__ */ jsxRuntime.jsx(TopBarTooltip, { title: label, description }) : null
|
|
817
|
+
!open && !hideTooltip ? /* @__PURE__ */ jsxRuntime.jsx(TopBarTooltip, { title: label, description }) : null
|
|
816
818
|
]
|
|
817
819
|
}
|
|
818
820
|
),
|
|
@@ -950,7 +952,11 @@ function subscribeLowFuelHintVisible(onChange) {
|
|
|
950
952
|
return () => lowFuelHintListeners.delete(onChange);
|
|
951
953
|
}
|
|
952
954
|
function useLowFuelHintVisible() {
|
|
953
|
-
return react.useSyncExternalStore(
|
|
955
|
+
return react.useSyncExternalStore(
|
|
956
|
+
subscribeLowFuelHintVisible,
|
|
957
|
+
() => lowFuelHintVisible,
|
|
958
|
+
() => false
|
|
959
|
+
);
|
|
954
960
|
}
|
|
955
961
|
var LOW_FUEL_HINT_CLEARANCE_PX = 96;
|
|
956
962
|
function LowFuelHint({ lowFuel, orgId }) {
|
|
@@ -1055,6 +1061,7 @@ function FuelAction({ orgId }) {
|
|
|
1055
1061
|
};
|
|
1056
1062
|
}, [dedupedFetch, orgId]);
|
|
1057
1063
|
const low = !!fuel && (fuel.signal.lowFuel || fuel.broadcast.lowFuel);
|
|
1064
|
+
const lowFuelHintUp = useLowFuelHintVisible();
|
|
1058
1065
|
return (
|
|
1059
1066
|
// The nudge anchors to the trigger, so it sits OUTSIDE the popover panel in
|
|
1060
1067
|
// its own relative wrapper (same shape the VRM widget used).
|
|
@@ -1067,6 +1074,7 @@ function FuelAction({ orgId }) {
|
|
|
1067
1074
|
slot: "comms-fuel",
|
|
1068
1075
|
fallbackIcon: BatteryGlyph,
|
|
1069
1076
|
tone: low ? "warning" : "default",
|
|
1077
|
+
hideTooltip: lowFuelHintUp,
|
|
1070
1078
|
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
|
|
1071
1079
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Communications Fuel" }),
|
|
1072
1080
|
error || !loading && !fuel ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-[color:var(--muted)]", children: "Fuel balance is unavailable right now." }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
@@ -1256,7 +1264,7 @@ function NewReplyToast({
|
|
|
1256
1264
|
);
|
|
1257
1265
|
}
|
|
1258
1266
|
function NotificationsAction({ orgId }) {
|
|
1259
|
-
const { dedupedFetch } = useNavChrome("NotificationsAction");
|
|
1267
|
+
const { dedupedFetch, notificationsAdapter } = useNavChrome("NotificationsAction");
|
|
1260
1268
|
const [payload, setPayload] = react.useState(null);
|
|
1261
1269
|
const [loading, setLoading] = react.useState(true);
|
|
1262
1270
|
const [error, setError] = react.useState(false);
|
|
@@ -1265,6 +1273,7 @@ function NotificationsAction({ orgId }) {
|
|
|
1265
1273
|
const [toastsMuted, setToastsMuted] = react.useState(() => readToastsMuted());
|
|
1266
1274
|
const [openToken, setOpenToken] = react.useState(0);
|
|
1267
1275
|
const [panelOpen, setPanelOpen] = react.useState(false);
|
|
1276
|
+
const [reloadToken, setReloadToken] = react.useState(0);
|
|
1268
1277
|
const anyPanelOpen = useAnyTopBarPanelOpen();
|
|
1269
1278
|
react.useEffect(() => {
|
|
1270
1279
|
setNotif(readNotifState(orgId));
|
|
@@ -1276,6 +1285,19 @@ function NotificationsAction({ orgId }) {
|
|
|
1276
1285
|
setLoading(false);
|
|
1277
1286
|
return;
|
|
1278
1287
|
}
|
|
1288
|
+
if (notificationsAdapter) {
|
|
1289
|
+
setError(false);
|
|
1290
|
+
notificationsAdapter.list(orgId).then((feed) => {
|
|
1291
|
+
if (!cancelled) setPayload(notificationsFeedToPayload(feed, orgId));
|
|
1292
|
+
}).catch(() => {
|
|
1293
|
+
if (!cancelled) setPayload(notificationsFeedToPayload(emptyNotificationsFeed(), orgId));
|
|
1294
|
+
}).finally(() => {
|
|
1295
|
+
if (!cancelled) setLoading(false);
|
|
1296
|
+
});
|
|
1297
|
+
return () => {
|
|
1298
|
+
cancelled = true;
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1279
1301
|
setLoading(true);
|
|
1280
1302
|
setError(false);
|
|
1281
1303
|
setPayload(null);
|
|
@@ -1293,9 +1315,14 @@ function NotificationsAction({ orgId }) {
|
|
|
1293
1315
|
return () => {
|
|
1294
1316
|
cancelled = true;
|
|
1295
1317
|
};
|
|
1296
|
-
}, [dedupedFetch, orgId]);
|
|
1318
|
+
}, [dedupedFetch, notificationsAdapter, orgId, reloadToken]);
|
|
1297
1319
|
const markRead = react.useCallback(
|
|
1298
1320
|
(id) => {
|
|
1321
|
+
if (notificationsAdapter) {
|
|
1322
|
+
void notificationsAdapter.markRead(id).then((ok) => {
|
|
1323
|
+
if (ok) setReloadToken((t) => t + 1);
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1299
1326
|
setNotif((prev) => {
|
|
1300
1327
|
if (prev.readAlertIds.includes(id)) return prev;
|
|
1301
1328
|
const next = { ...prev, readAlertIds: [...prev.readAlertIds, id] };
|
|
@@ -1303,7 +1330,7 @@ function NotificationsAction({ orgId }) {
|
|
|
1303
1330
|
return next;
|
|
1304
1331
|
});
|
|
1305
1332
|
},
|
|
1306
|
-
[orgId]
|
|
1333
|
+
[orgId, notificationsAdapter]
|
|
1307
1334
|
);
|
|
1308
1335
|
const markUnread = react.useCallback(
|
|
1309
1336
|
(id) => {
|
|
@@ -1373,14 +1400,16 @@ function NotificationsAction({ orgId }) {
|
|
|
1373
1400
|
}
|
|
1374
1401
|
setOpenToken((t) => t + 1);
|
|
1375
1402
|
}, [toastReply]);
|
|
1376
|
-
const
|
|
1403
|
+
const isAlertRead = (a) => a.read === true || notif.readAlertIds.includes(a.id);
|
|
1404
|
+
const unreadAlertCount = alerts.filter((a) => !isAlertRead(a)).length;
|
|
1377
1405
|
const unseenReplyCount = replies.filter((r) => {
|
|
1378
1406
|
if (!notif.lastSeenAt) return true;
|
|
1379
1407
|
if (!r.createdAt) return false;
|
|
1380
1408
|
return r.createdAt > notif.lastSeenAt;
|
|
1381
1409
|
}).length;
|
|
1382
|
-
const badgeCount = unreadAlertCount + unseenReplyCount;
|
|
1410
|
+
const badgeCount = notificationsAdapter && payload?.badge !== void 0 ? payload.badge : unreadAlertCount + unseenReplyCount;
|
|
1383
1411
|
const isEmpty = alerts.length === 0 && replies.length === 0 && imports.length === 0 && quarantined.length === 0;
|
|
1412
|
+
const toastVisible = toastReply !== null && !panelOpen && !anyPanelOpen;
|
|
1384
1413
|
return (
|
|
1385
1414
|
// Relative wrapper so the new-reply toast anchors to the bell trigger,
|
|
1386
1415
|
// outside the popover panel (same pattern as the fuel low-fuel nudge).
|
|
@@ -1396,6 +1425,7 @@ function NotificationsAction({ orgId }) {
|
|
|
1396
1425
|
onOpen,
|
|
1397
1426
|
openToken,
|
|
1398
1427
|
onOpenChange: setPanelOpen,
|
|
1428
|
+
hideTooltip: toastVisible,
|
|
1399
1429
|
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
|
|
1400
1430
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
|
|
1401
1431
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Notifications" }),
|
|
@@ -1415,7 +1445,7 @@ function NotificationsAction({ orgId }) {
|
|
|
1415
1445
|
] }) : error ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-[color:var(--muted)]", children: "Notifications are unavailable right now." }) : isEmpty ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "py-1 text-sm text-[color:var(--muted)]", children: "You\u2019re all caught up." }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex max-h-80 flex-col gap-2 overflow-y-auto", children: [
|
|
1416
1446
|
alerts.length > 0 || replies.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "flex flex-col gap-1", children: [
|
|
1417
1447
|
alerts.map((a) => {
|
|
1418
|
-
const isRead =
|
|
1448
|
+
const isRead = isAlertRead(a);
|
|
1419
1449
|
return /* @__PURE__ */ jsxRuntime.jsx("li", { className: isRead ? "opacity-50" : void 0, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-1 rounded-lg px-2 py-2 hover:bg-[color:var(--panel)]", children: [
|
|
1420
1450
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1421
1451
|
"a",
|
|
@@ -1434,7 +1464,7 @@ function NotificationsAction({ orgId }) {
|
|
|
1434
1464
|
{
|
|
1435
1465
|
type: "button",
|
|
1436
1466
|
"aria-label": isRead ? "Mark as unread" : "Mark as read",
|
|
1437
|
-
onClick: () => isRead ? markUnread(a.id) : markRead(a.id),
|
|
1467
|
+
onClick: () => isRead && !notificationsAdapter ? markUnread(a.id) : markRead(a.id),
|
|
1438
1468
|
className: "mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[color:var(--muted)] transition hover:bg-[color:var(--panel-elevated)] hover:text-[color:var(--foreground)]",
|
|
1439
1469
|
children: /* @__PURE__ */ jsxRuntime.jsx(CheckGlyph, {})
|
|
1440
1470
|
}
|
|
@@ -1493,7 +1523,7 @@ function NotificationsAction({ orgId }) {
|
|
|
1493
1523
|
] })
|
|
1494
1524
|
}
|
|
1495
1525
|
),
|
|
1496
|
-
|
|
1526
|
+
toastVisible && toastReply ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1497
1527
|
NewReplyToast,
|
|
1498
1528
|
{
|
|
1499
1529
|
reply: toastReply,
|
|
@@ -2085,102 +2115,6 @@ var NAV_DISPLAY_OPTIONS = [
|
|
|
2085
2115
|
{ value: "tabs", label: "Tabs Only" },
|
|
2086
2116
|
{ value: "both", label: "Both" }
|
|
2087
2117
|
];
|
|
2088
|
-
function useThemePreference() {
|
|
2089
|
-
const ctx = react.useContext(NavChromeContext);
|
|
2090
|
-
const fetchJson = ctx?.fetchJson ?? null;
|
|
2091
|
-
const [value, setValueState] = react.useState("system");
|
|
2092
|
-
const reconciled = react.useRef(false);
|
|
2093
|
-
react.useEffect(() => {
|
|
2094
|
-
const cookie = readThemeCookie();
|
|
2095
|
-
let ls = null;
|
|
2096
|
-
try {
|
|
2097
|
-
ls = localStorage.getItem(THEME_STORAGE_KEY);
|
|
2098
|
-
} catch {
|
|
2099
|
-
}
|
|
2100
|
-
const pref = cookie ?? (isThemePreference(ls) ? ls : null) ?? "system";
|
|
2101
|
-
setValueState(pref);
|
|
2102
|
-
applyThemePreference();
|
|
2103
|
-
}, []);
|
|
2104
|
-
const meCtx = react.useContext(NavChromeContext);
|
|
2105
|
-
react.useEffect(() => {
|
|
2106
|
-
if (reconciled.current) return;
|
|
2107
|
-
const pending = readPendingThemeChoice();
|
|
2108
|
-
if (pending) {
|
|
2109
|
-
if (meCtx?.me) reconciled.current = true;
|
|
2110
|
-
setValueState(pending);
|
|
2111
|
-
applyThemePreference();
|
|
2112
|
-
writeThemeCookie(pending);
|
|
2113
|
-
try {
|
|
2114
|
-
localStorage.setItem(THEME_STORAGE_KEY, pending);
|
|
2115
|
-
} catch {
|
|
2116
|
-
}
|
|
2117
|
-
if (!meCtx?.me) return;
|
|
2118
|
-
fetchJson?.("me/preferences", {
|
|
2119
|
-
method: "PATCH",
|
|
2120
|
-
headers: { "Content-Type": "application/json" },
|
|
2121
|
-
body: JSON.stringify({ themePreference: pending })
|
|
2122
|
-
}).then(() => clearPendingThemeChoice()).catch(() => {
|
|
2123
|
-
});
|
|
2124
|
-
return;
|
|
2125
|
-
}
|
|
2126
|
-
const pref = meCtx?.me?.themePreference;
|
|
2127
|
-
if (isThemePreference(pref)) {
|
|
2128
|
-
reconciled.current = true;
|
|
2129
|
-
setValueState(pref);
|
|
2130
|
-
applyThemePreference();
|
|
2131
|
-
writeThemeCookie(pref);
|
|
2132
|
-
try {
|
|
2133
|
-
localStorage.setItem(THEME_STORAGE_KEY, pref);
|
|
2134
|
-
} catch {
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
}, [meCtx?.me]);
|
|
2138
|
-
react.useEffect(() => {
|
|
2139
|
-
const onStorage = (e) => {
|
|
2140
|
-
if (e.key === THEME_STORAGE_KEY && isThemePreference(e.newValue)) {
|
|
2141
|
-
setValueState(e.newValue);
|
|
2142
|
-
applyThemePreference(e.newValue);
|
|
2143
|
-
}
|
|
2144
|
-
};
|
|
2145
|
-
const onWake = () => {
|
|
2146
|
-
const cookie = readThemeCookie();
|
|
2147
|
-
if (cookie) {
|
|
2148
|
-
setValueState((prev) => {
|
|
2149
|
-
if (cookie !== prev) applyThemePreference();
|
|
2150
|
-
return cookie;
|
|
2151
|
-
});
|
|
2152
|
-
}
|
|
2153
|
-
};
|
|
2154
|
-
window.addEventListener("storage", onStorage);
|
|
2155
|
-
window.addEventListener("focus", onWake);
|
|
2156
|
-
document.addEventListener("visibilitychange", onWake);
|
|
2157
|
-
return () => {
|
|
2158
|
-
window.removeEventListener("storage", onStorage);
|
|
2159
|
-
window.removeEventListener("focus", onWake);
|
|
2160
|
-
document.removeEventListener("visibilitychange", onWake);
|
|
2161
|
-
};
|
|
2162
|
-
}, []);
|
|
2163
|
-
const setValue = react.useCallback(
|
|
2164
|
-
(next) => {
|
|
2165
|
-
setValueState(next);
|
|
2166
|
-
applyThemePreference();
|
|
2167
|
-
writeThemeCookie(next);
|
|
2168
|
-
try {
|
|
2169
|
-
localStorage.setItem(THEME_STORAGE_KEY, next);
|
|
2170
|
-
} catch {
|
|
2171
|
-
}
|
|
2172
|
-
markThemeChoicePending(next);
|
|
2173
|
-
fetchJson?.("me/preferences", {
|
|
2174
|
-
method: "PATCH",
|
|
2175
|
-
headers: { "Content-Type": "application/json" },
|
|
2176
|
-
body: JSON.stringify({ themePreference: next })
|
|
2177
|
-
}).then(() => clearPendingThemeChoice()).catch(() => {
|
|
2178
|
-
});
|
|
2179
|
-
},
|
|
2180
|
-
[fetchJson]
|
|
2181
|
-
);
|
|
2182
|
-
return { value, setValue };
|
|
2183
|
-
}
|
|
2184
2118
|
var standardNavIcons = {
|
|
2185
2119
|
home: lu.LuHouse,
|
|
2186
2120
|
users: lu.LuUsers,
|
|
@@ -5388,6 +5322,7 @@ function PolitogyAppChrome({
|
|
|
5388
5322
|
storagePrefix,
|
|
5389
5323
|
proxyBasePath = "/api/chrome",
|
|
5390
5324
|
fetchJson: fetchJsonOverride,
|
|
5325
|
+
notificationsAdapter,
|
|
5391
5326
|
devNotes,
|
|
5392
5327
|
org,
|
|
5393
5328
|
tokenOrg,
|
|
@@ -5419,7 +5354,7 @@ function PolitogyAppChrome({
|
|
|
5419
5354
|
children
|
|
5420
5355
|
}
|
|
5421
5356
|
);
|
|
5422
|
-
return /* @__PURE__ */ jsxRuntime.jsx(NavChromeProvider, { fetchJson, children: tokenOrg ? /* @__PURE__ */ jsxRuntime.jsx(TokenChromeStateProvider, { tokenOrg, children: devNotesTree }) : /* @__PURE__ */ jsxRuntime.jsx(ChromeStateProvider, { controlledOrg: org, children: devNotesTree }) });
|
|
5357
|
+
return /* @__PURE__ */ jsxRuntime.jsx(NavChromeProvider, { fetchJson, notificationsAdapter, children: tokenOrg ? /* @__PURE__ */ jsxRuntime.jsx(TokenChromeStateProvider, { tokenOrg, children: devNotesTree }) : /* @__PURE__ */ jsxRuntime.jsx(ChromeStateProvider, { controlledOrg: org, children: devNotesTree }) });
|
|
5423
5358
|
}
|
|
5424
5359
|
function ChromeTopBarActions() {
|
|
5425
5360
|
const state = useChromeState();
|
|
@@ -5480,7 +5415,6 @@ function useChromeUserMenu({
|
|
|
5480
5415
|
email
|
|
5481
5416
|
}) {
|
|
5482
5417
|
const state = useChromeState();
|
|
5483
|
-
const theme = useThemePreference();
|
|
5484
5418
|
const activeOrg = state?.orgs.find((o) => o.id === state.orgId) ?? null;
|
|
5485
5419
|
const orgSuffix = activeOrg ? `?org=${activeOrg.id}` : "";
|
|
5486
5420
|
const coreBase = getCoreBaseUrl();
|
|
@@ -5491,7 +5425,9 @@ function useChromeUserMenu({
|
|
|
5491
5425
|
roles: (state?.orgs ?? []).filter((o) => o.rawRole || o.role).map((o) => ({ role: o.rawRole ?? o.role, orgName: o.name })),
|
|
5492
5426
|
accounts: [{ id: userId, name: userName, email, avatarUrl: null, active: true }],
|
|
5493
5427
|
addAccountHref: `${coreBase}/my-profile#accounts`,
|
|
5494
|
-
theme:
|
|
5428
|
+
// No theme control: the product is light only (Reference/UI-Platform
|
|
5429
|
+
// "Theme — light only"; shell 0.73.0 stopped applying dark). The account
|
|
5430
|
+
// menu rendered an "Appearance" radio with one option until 0.77.0.
|
|
5495
5431
|
navDisplay: state ? { value: state.navDisplay, onChange: state.setNavDisplay } : void 0
|
|
5496
5432
|
};
|
|
5497
5433
|
}
|