@the-portland-company/shell 0.39.1 → 0.41.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,100 @@
1
+ ## 0.41.0 — 2026-08-03
2
+
3
+ ### Fixed — the expanded sub-menu was the same colour as the left bar
4
+
5
+ Reference/UI-Platform → Styling pins the nav ramp (page `#0A0A0A` → left/top bar
6
+ `#161616` → **expanded sub-menu `#212121`** → row hover `#2B2B2B` → borders
7
+ `#333333`). The expanded group painted `--panel`, which is `#161616` in dark —
8
+ the same value as the bar behind it — so nesting read as flat. New tokens
9
+ `--nav-submenu-bg` and `--nav-row-hover` carry the ramp's steps; the nested
10
+ sub-group's hover no longer reuses `--panel-elevated` (`#1F1F1F`), which was
11
+ DARKER than the panel it sits on and made a hovered row recede.
12
+
13
+ ### Fixed — quarantined-contact rows looked clickable and were not
14
+
15
+ The rows in the Alerts panel carried a hover highlight on a non-interactive
16
+ `<li>`, so a row advertised interactivity and then did nothing. Each row is now
17
+ a real `<a>` — keyboard focusable, Enter-activatable, with a visible
18
+ focus-visible ring — opening the quarantined-contacts view filtered to that
19
+ number (`?phone=`), the same destination as "View all" but scoped. Counts cap at
20
+ `99+` and match the shared badge geometry.
21
+
22
+ ### Fixed — the `.dark` block was missing whole token groups
23
+
24
+ Global-Rules → Dark Mode: "A var can never exist in one block and not the
25
+ other." The form-control, app-field, floating-label and `--app-color-gray-*`
26
+ groups were declared in `:root` only, so a control in dark mode kept its LIGHT
27
+ border, placeholder and label values — measured live as slate `#64748B` /
28
+ `#475569` against black, which is also the forbidden blue-tinted family. All are
29
+ now overridden, with the gray ramp INVERTED (pale surface → dark surface, dark
30
+ text → light text).
31
+
32
+ ### Fixed — Chakra tooltips and subtle alerts in dark mode
33
+
34
+ Chakra's stock `Tooltip` inverts, so in dark mode every `div#tooltip-*` rendered
35
+ a near-white `#E8E8E8` slab against black chrome (15 of 29 pages). It is now
36
+ pinned to the shared panel surface, so a Chakra tooltip and the native chrome's
37
+ `TopBarTooltip` are one treatment, per UI-Platform. Chakra's `subtle` /
38
+ `left-accent` alerts resolved `<scheme>.200` for BOTH the surface and the icon,
39
+ so an error alert painted `#FEB2B2`; in dark they now use a neutral raised
40
+ surface and carry meaning in the icon/text accent, with red reconciled to the
41
+ brand ramp rather than Chakra's `#E53E3E`.
42
+
43
+ ### Fixed — `gen:tokens` silently deleted hand-written chrome rules
44
+
45
+ `scripts/gen-tokens.mjs` rewrites `chrome.css` wholesale, but the widget-slot
46
+ badge normalization and the beta-banner defaults had been appended to the
47
+ committed file by hand. The next regeneration would have deleted them — 0.40.0's
48
+ badge fix included. They now live in the generator (`STATIC_CHROME_CSS`), so the
49
+ file regenerates losslessly.
50
+
51
+ ### Added
52
+
53
+ `__tests__/darkTokenCompleteness.test.ts` asserts that every colour-valued
54
+ `:root` var has a `.dark` override and that every dark chrome hex is hue-free
55
+ (R = G = B) — the arithmetic check the spec asks for, rather than an eyeball.
56
+
57
+ ## 0.40.1 — 2026-08-03
58
+
59
+ ### Fixed — a top-bar nudge could still cover an open panel
60
+
61
+ 0.40.0 stopped the new-reply toast covering the notifications panel, but the
62
+ same collision existed from a NEIGHBOURING trigger: the low-fuel nudge is wider
63
+ than its own trigger and the panels are right-aligned, so with Notifications
64
+ open the nudge sat on its header (measured live on contacts.politogyvrm.com
65
+ after the 0.40.0 deploy). The top bar now keeps one module-scoped count of open
66
+ panels (`useAnyTopBarPanelOpen`); both transient nudges yield while any panel is
67
+ open and return when it closes. Still no z-index changes.
68
+
69
+ ## 0.40.0 — 2026-08-03
70
+
71
+ ### Fixed — the DevNotes badge was a second badge definition
72
+
73
+ Reference/UI-Platform "Badge Counters" allows exactly ONE badge definition, and
74
+ `.topbar-widget-slot` in `chrome.css` is supposed to restate it for a launcher
75
+ that brings its own badge markup. It did not: the only rule was
76
+ `.topbar-widget-slot button > span.relative { position: static }`, and DevNotes
77
+ 0.6.x writes that wrapper with an INLINE `position: relative` and **no class**,
78
+ so the selector matched nothing. The badge kept anchoring to the glyph instead
79
+ of the 32px trigger, and its own inline styles (`top:-4 right:-8`, `#dc2626`,
80
+ `font-weight:700`, no `height`) rendered a visibly different bubble — measured
81
+ different on app.politogyvrm.com vs contacts.politogyvrm.com.
82
+
83
+ The slot CSS now (a) matches every direct span child of the launcher, not a
84
+ class, and (b) restates the whole badge definition — 16px circle, `#E63946`
85
+ via `--color-race-red`, 10px / weight 400 / line-height 1, `top:-4px`
86
+ `right:-4px` — with `!important` so inline styles cannot win. `TopBarCountBadge`
87
+ pins `font-weight: 400` and `line-height: 1` explicitly rather than inheriting.
88
+
89
+ ### Fixed — the new-reply toast rendered on top of the notifications panel
90
+
91
+ Both the toast and the panel anchor to the bell trigger, and nothing suppressed
92
+ one while the other was up, so opening Notifications put a message preview over
93
+ the panel's "NOTIFICATIONS" header and first row. `TopBarPopover` gained
94
+ `onOpenChange`; `NotificationsAction` clears the toast when the panel opens,
95
+ refuses to raise a new one while it is open, and does not render it when open.
96
+ One surface per trigger, per Reference/UI-Platform. No z-index was changed.
97
+
1
98
  ## 0.37.1 — 2026-07-27
2
99
 
3
100
  ### Fixed — cross-app navigation kept the user's active organization
@@ -154,6 +154,10 @@ function withOrgParam(url, orgId) {
154
154
  if (!orgId) return url;
155
155
  return `${url}${url.includes("?") ? "&" : "?"}org=${orgId}`;
156
156
  }
157
+ function withPhoneParam(url, phone) {
158
+ if (!phone) return url;
159
+ return `${url}${url.includes("?") ? "&" : "?"}phone=${encodeURIComponent(phone)}`;
160
+ }
157
161
  function resolveOrgLogoUrl(url) {
158
162
  const raw = url?.trim();
159
163
  if (!raw) return void 0;
@@ -169,9 +173,26 @@ function topBarIconButtonClass(tone = "default") {
169
173
  toneClass
170
174
  ].join(" ");
171
175
  }
176
+ var openPanelCount = 0;
177
+ var panelListeners = /* @__PURE__ */ new Set();
178
+ var emitPanelChange = () => panelListeners.forEach((l) => l());
179
+ function setPanelOpenGlobal(open) {
180
+ openPanelCount = Math.max(0, openPanelCount + (open ? 1 : -1));
181
+ emitPanelChange();
182
+ }
183
+ function useAnyTopBarPanelOpen() {
184
+ return react.useSyncExternalStore(
185
+ (cb) => {
186
+ panelListeners.add(cb);
187
+ return () => panelListeners.delete(cb);
188
+ },
189
+ () => openPanelCount > 0,
190
+ () => false
191
+ );
192
+ }
172
193
  function TopBarCountBadge({ count }) {
173
194
  if (!(count > 0)) return null;
174
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute -top-1 -right-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-[color:var(--color-race-red)] px-1 text-[10px] text-white", children: count > 99 ? "99+" : count });
195
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute -top-1 -right-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-[color:var(--color-race-red)] px-1 text-[10px] font-normal leading-none text-white", children: count > 99 ? "99+" : count });
175
196
  }
176
197
  function TopBarTooltip({
177
198
  title,
@@ -343,10 +364,19 @@ function TopBarPopover({
343
364
  tone,
344
365
  onOpen,
345
366
  openToken,
367
+ onOpenChange,
346
368
  children,
347
369
  panelClassName
348
370
  }) {
349
371
  const [open, setOpen] = react.useState(false);
372
+ const onOpenChangeRef = react.useRef(onOpenChange);
373
+ onOpenChangeRef.current = onOpenChange;
374
+ react.useEffect(() => {
375
+ onOpenChangeRef.current?.(open);
376
+ if (!open) return;
377
+ setPanelOpenGlobal(true);
378
+ return () => setPanelOpenGlobal(false);
379
+ }, [open]);
350
380
  const rootRef = react.useRef(null);
351
381
  const lastOpenTokenRef = react.useRef(openToken);
352
382
  react.useEffect(() => {
@@ -524,7 +554,8 @@ function LowFuelHint({ lowFuel, orgId }) {
524
554
  } catch {
525
555
  }
526
556
  }, [lowFuel]);
527
- if (!lowFuel || dismissed) return null;
557
+ const panelOpen = useAnyTopBarPanelOpen();
558
+ if (!lowFuel || dismissed || panelOpen) return null;
528
559
  const dismiss = () => {
529
560
  try {
530
561
  window.sessionStorage.setItem(LOW_FUEL_HINT_DISMISSED_KEY, "1");
@@ -757,6 +788,8 @@ function NotificationsAction({ orgId }) {
757
788
  const [notif, setNotif] = react.useState(() => readNotifState(orgId));
758
789
  const [toastReply, setToastReply] = react.useState(null);
759
790
  const [openToken, setOpenToken] = react.useState(0);
791
+ const [panelOpen, setPanelOpen] = react.useState(false);
792
+ const anyPanelOpen = useAnyTopBarPanelOpen();
760
793
  react.useEffect(() => {
761
794
  setNotif(readNotifState(orgId));
762
795
  setToastReply(null);
@@ -824,6 +857,7 @@ function NotificationsAction({ orgId }) {
824
857
  [orgId]
825
858
  );
826
859
  const onOpen = react.useCallback(() => {
860
+ setToastReply(null);
827
861
  setNotif((prev) => {
828
862
  const next = { ...prev, lastSeenAt: (/* @__PURE__ */ new Date()).toISOString() };
829
863
  writeNotifState(orgId, next);
@@ -835,7 +869,7 @@ function NotificationsAction({ orgId }) {
835
869
  const imports = payload?.imports ?? [];
836
870
  const quarantined = payload?.quarantined ?? [];
837
871
  react.useEffect(() => {
838
- if (!payload || toastReply) return;
872
+ if (!payload || toastReply || panelOpen) return;
839
873
  const candidate = replies.filter((r) => {
840
874
  if (!r.createdAt) return false;
841
875
  if (notif.lastSeenAt && r.createdAt <= notif.lastSeenAt) return false;
@@ -851,7 +885,7 @@ function NotificationsAction({ orgId }) {
851
885
  writeNotifState(orgId, next);
852
886
  return next;
853
887
  });
854
- }, [payload, replies, toastReply, notif.lastSeenAt, notif.toastedReplyIds, orgId]);
888
+ }, [payload, replies, toastReply, panelOpen, notif.lastSeenAt, notif.toastedReplyIds, orgId]);
855
889
  const onToastActivate = react.useCallback(() => {
856
890
  const href = toastReply?.href;
857
891
  setToastReply(null);
@@ -883,6 +917,7 @@ function NotificationsAction({ orgId }) {
883
917
  badge: loading ? 0 : badgeCount,
884
918
  onOpen,
885
919
  openToken,
920
+ onOpenChange: setPanelOpen,
886
921
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
887
922
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Notifications" }),
888
923
  loading ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 py-1", children: [
@@ -938,19 +973,27 @@ function NotificationsAction({ orgId }) {
938
973
  }
939
974
  )
940
975
  ] }),
941
- /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col gap-1", children: quarantined.map((q) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "rounded-lg px-2 py-2 hover:bg-[color:var(--panel)]", children: [
942
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
943
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold text-[color:var(--foreground)]", children: formatPhone(q.phone) }),
944
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-4 min-w-4 items-center justify-center rounded-full bg-[color:var(--panel)] px-1 text-[10px] tabular-nums text-[color:var(--muted)]", children: q.count })
945
- ] }),
946
- q.latestAt ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-[color:var(--muted)]", children: new Date(q.latestAt).toLocaleString() }) : null
947
- ] }, q.phone)) })
976
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex flex-col gap-1", children: quarantined.map((q) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsxs(
977
+ "a",
978
+ {
979
+ href: withPhoneParam(withOrgParam(QUARANTINED_CONTACTS_URL, orgId), q.phone),
980
+ "aria-label": `Review ${q.count} quarantined message${q.count === 1 ? "" : "s"} from ${formatPhone(q.phone)}`,
981
+ className: "block rounded-lg px-2 py-2 transition hover:bg-[color:var(--nav-row-hover)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--color-race-red)]",
982
+ children: [
983
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
984
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold text-[color:var(--foreground)]", children: formatPhone(q.phone) }),
985
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-4 min-w-4 items-center justify-center rounded-full bg-[color:var(--panel)] px-1 text-[10px] font-normal leading-none tabular-nums text-[color:var(--muted)]", children: q.count > 99 ? "99+" : q.count })
986
+ ] }),
987
+ q.latestAt ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-[color:var(--muted)]", children: new Date(q.latestAt).toLocaleString() }) : null
988
+ ]
989
+ }
990
+ ) }, q.phone)) })
948
991
  ] }) : null
949
992
  ] })
950
993
  ] })
951
994
  }
952
995
  ),
953
- toastReply ? /* @__PURE__ */ jsxRuntime.jsx(
996
+ toastReply && !panelOpen && !anyPanelOpen ? /* @__PURE__ */ jsxRuntime.jsx(
954
997
  NewReplyToast,
955
998
  {
956
999
  reply: toastReply,
@@ -1992,7 +2035,7 @@ function NavGroup({
1992
2035
  ]
1993
2036
  }
1994
2037
  ),
1995
- open ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-b-md bg-[color:var(--panel)] p-1", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-0.5", children: item.children.map((child, i) => {
2038
+ open ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-b-md bg-[color:var(--nav-submenu-bg)] p-1", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-0.5", children: item.children.map((child, i) => {
1996
2039
  const childActive = child.href ? activeFn(pathname, child.href) : false;
1997
2040
  if (child.children && child.children.length > 0) {
1998
2041
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -2040,7 +2083,7 @@ function NavSubGroup({
2040
2083
  "aria-expanded": open,
2041
2084
  className: [
2042
2085
  "flex w-full items-center gap-2.5 rounded px-3 py-1.5 text-left text-[13px] font-medium transition",
2043
- ownsActive ? "text-[color:var(--color-race-red)]" : "text-[color:var(--muted)] hover:bg-[color:var(--panel-elevated)] hover:text-[color:var(--foreground)]"
2086
+ ownsActive ? "text-[color:var(--color-race-red)]" : "text-[color:var(--muted)] hover:bg-[color:var(--nav-row-hover)] hover:text-[color:var(--foreground)]"
2044
2087
  ].join(" "),
2045
2088
  children: [
2046
2089
  Icon ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: 15, className: "shrink-0 text-current" }) : null,