@the-portland-company/shell 0.40.0 → 0.41.1

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,83 @@
1
+ ## 0.41.1 — 2026-08-03
2
+
3
+ ### Fixed — the Chakra tooltip override in 0.41.0 did not actually take
4
+
5
+ 0.41.0 pinned `Tooltip.baseStyle.bg`, but Chakra's own baseStyle sets
6
+ `bg: var(--tooltip-bg)` and then assigns that VARIABLE from its palette — and it
7
+ ships a `_dark` block that reassigns `--tooltip-bg` to `colors.gray.300`,
8
+ deliberately inverting in dark mode. Overriding `bg` alone therefore lost, and
9
+ the bubble still measured `#E8E8E8` live after the 0.41.0 deploy. Both the base
10
+ and the `_dark` block now pin `--tooltip-bg` / `--tooltip-fg` /
11
+ `--popper-arrow-bg` to the shared panel surface, with a test asserting it.
12
+
13
+ ## 0.41.0 — 2026-08-03
14
+
15
+ ### Fixed — the expanded sub-menu was the same colour as the left bar
16
+
17
+ Reference/UI-Platform → Styling pins the nav ramp (page `#0A0A0A` → left/top bar
18
+ `#161616` → **expanded sub-menu `#212121`** → row hover `#2B2B2B` → borders
19
+ `#333333`). The expanded group painted `--panel`, which is `#161616` in dark —
20
+ the same value as the bar behind it — so nesting read as flat. New tokens
21
+ `--nav-submenu-bg` and `--nav-row-hover` carry the ramp's steps; the nested
22
+ sub-group's hover no longer reuses `--panel-elevated` (`#1F1F1F`), which was
23
+ DARKER than the panel it sits on and made a hovered row recede.
24
+
25
+ ### Fixed — quarantined-contact rows looked clickable and were not
26
+
27
+ The rows in the Alerts panel carried a hover highlight on a non-interactive
28
+ `<li>`, so a row advertised interactivity and then did nothing. Each row is now
29
+ a real `<a>` — keyboard focusable, Enter-activatable, with a visible
30
+ focus-visible ring — opening the quarantined-contacts view filtered to that
31
+ number (`?phone=`), the same destination as "View all" but scoped. Counts cap at
32
+ `99+` and match the shared badge geometry.
33
+
34
+ ### Fixed — the `.dark` block was missing whole token groups
35
+
36
+ Global-Rules → Dark Mode: "A var can never exist in one block and not the
37
+ other." The form-control, app-field, floating-label and `--app-color-gray-*`
38
+ groups were declared in `:root` only, so a control in dark mode kept its LIGHT
39
+ border, placeholder and label values — measured live as slate `#64748B` /
40
+ `#475569` against black, which is also the forbidden blue-tinted family. All are
41
+ now overridden, with the gray ramp INVERTED (pale surface → dark surface, dark
42
+ text → light text).
43
+
44
+ ### Fixed — Chakra tooltips and subtle alerts in dark mode
45
+
46
+ Chakra's stock `Tooltip` inverts, so in dark mode every `div#tooltip-*` rendered
47
+ a near-white `#E8E8E8` slab against black chrome (15 of 29 pages). It is now
48
+ pinned to the shared panel surface, so a Chakra tooltip and the native chrome's
49
+ `TopBarTooltip` are one treatment, per UI-Platform. Chakra's `subtle` /
50
+ `left-accent` alerts resolved `<scheme>.200` for BOTH the surface and the icon,
51
+ so an error alert painted `#FEB2B2`; in dark they now use a neutral raised
52
+ surface and carry meaning in the icon/text accent, with red reconciled to the
53
+ brand ramp rather than Chakra's `#E53E3E`.
54
+
55
+ ### Fixed — `gen:tokens` silently deleted hand-written chrome rules
56
+
57
+ `scripts/gen-tokens.mjs` rewrites `chrome.css` wholesale, but the widget-slot
58
+ badge normalization and the beta-banner defaults had been appended to the
59
+ committed file by hand. The next regeneration would have deleted them — 0.40.0's
60
+ badge fix included. They now live in the generator (`STATIC_CHROME_CSS`), so the
61
+ file regenerates losslessly.
62
+
63
+ ### Added
64
+
65
+ `__tests__/darkTokenCompleteness.test.ts` asserts that every colour-valued
66
+ `:root` var has a `.dark` override and that every dark chrome hex is hue-free
67
+ (R = G = B) — the arithmetic check the spec asks for, rather than an eyeball.
68
+
69
+ ## 0.40.1 — 2026-08-03
70
+
71
+ ### Fixed — a top-bar nudge could still cover an open panel
72
+
73
+ 0.40.0 stopped the new-reply toast covering the notifications panel, but the
74
+ same collision existed from a NEIGHBOURING trigger: the low-fuel nudge is wider
75
+ than its own trigger and the panels are right-aligned, so with Notifications
76
+ open the nudge sat on its header (measured live on contacts.politogyvrm.com
77
+ after the 0.40.0 deploy). The top bar now keeps one module-scoped count of open
78
+ panels (`useAnyTopBarPanelOpen`); both transient nudges yield while any panel is
79
+ open and return when it closes. Still no z-index changes.
80
+
1
81
  ## 0.40.0 — 2026-08-03
2
82
 
3
83
  ### Fixed — the DevNotes badge was a second badge definition
@@ -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,6 +173,23 @@ 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
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 });
@@ -352,6 +373,9 @@ function TopBarPopover({
352
373
  onOpenChangeRef.current = onOpenChange;
353
374
  react.useEffect(() => {
354
375
  onOpenChangeRef.current?.(open);
376
+ if (!open) return;
377
+ setPanelOpenGlobal(true);
378
+ return () => setPanelOpenGlobal(false);
355
379
  }, [open]);
356
380
  const rootRef = react.useRef(null);
357
381
  const lastOpenTokenRef = react.useRef(openToken);
@@ -530,7 +554,8 @@ function LowFuelHint({ lowFuel, orgId }) {
530
554
  } catch {
531
555
  }
532
556
  }, [lowFuel]);
533
- if (!lowFuel || dismissed) return null;
557
+ const panelOpen = useAnyTopBarPanelOpen();
558
+ if (!lowFuel || dismissed || panelOpen) return null;
534
559
  const dismiss = () => {
535
560
  try {
536
561
  window.sessionStorage.setItem(LOW_FUEL_HINT_DISMISSED_KEY, "1");
@@ -764,6 +789,7 @@ function NotificationsAction({ orgId }) {
764
789
  const [toastReply, setToastReply] = react.useState(null);
765
790
  const [openToken, setOpenToken] = react.useState(0);
766
791
  const [panelOpen, setPanelOpen] = react.useState(false);
792
+ const anyPanelOpen = useAnyTopBarPanelOpen();
767
793
  react.useEffect(() => {
768
794
  setNotif(readNotifState(orgId));
769
795
  setToastReply(null);
@@ -947,19 +973,27 @@ function NotificationsAction({ orgId }) {
947
973
  }
948
974
  )
949
975
  ] }),
950
- /* @__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: [
951
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
952
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold text-[color:var(--foreground)]", children: formatPhone(q.phone) }),
953
- /* @__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 })
954
- ] }),
955
- q.latestAt ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-[color:var(--muted)]", children: new Date(q.latestAt).toLocaleString() }) : null
956
- ] }, 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)) })
957
991
  ] }) : null
958
992
  ] })
959
993
  ] })
960
994
  }
961
995
  ),
962
- toastReply && !panelOpen ? /* @__PURE__ */ jsxRuntime.jsx(
996
+ toastReply && !panelOpen && !anyPanelOpen ? /* @__PURE__ */ jsxRuntime.jsx(
963
997
  NewReplyToast,
964
998
  {
965
999
  reply: toastReply,
@@ -2001,7 +2035,7 @@ function NavGroup({
2001
2035
  ]
2002
2036
  }
2003
2037
  ),
2004
- 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) => {
2005
2039
  const childActive = child.href ? activeFn(pathname, child.href) : false;
2006
2040
  if (child.children && child.children.length > 0) {
2007
2041
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -2049,7 +2083,7 @@ function NavSubGroup({
2049
2083
  "aria-expanded": open,
2050
2084
  className: [
2051
2085
  "flex w-full items-center gap-2.5 rounded px-3 py-1.5 text-left text-[13px] font-medium transition",
2052
- 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)]"
2053
2087
  ].join(" "),
2054
2088
  children: [
2055
2089
  Icon ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: 15, className: "shrink-0 text-current" }) : null,