@the-portland-company/shell 0.47.1 → 0.48.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,39 @@
1
+ ## 0.48.0 — 2026-08-13
2
+
3
+ V1-parity round 2, driven by a measured element-by-element audit of a live V1
4
+ build against 0.47.1. Two kinds of change: one global conformance fix, and a
5
+ set of registry/Mode opt-ins that are byte-identical for apps that declare
6
+ nothing.
7
+
8
+ ### Fixed — the sidebar now consumes its own Brand tokens (GLOBAL)
9
+
10
+ "Sidebar width, colors, item radius, and spacing come from `--sidebar-*`"
11
+ (Reference/UI-Platform) — but the chrome painted the bar from `--surface-1`
12
+ and rows from `--muted`, so `--sidebar-bg: #FFFFFF` and
13
+ `--sidebar-text: #2F3136` were dead tokens and a Brand edit to them did
14
+ nothing. Consuming them changes every app toward what the Brand editor
15
+ declares: white rail, dark rows in light; the same #161616 ramp in dark
16
+ (dark values added to chrome.css's own `.dark` block so stylesheet order
17
+ between chrome.css and the runtime sheet cannot flip them). This also ends
18
+ the app-wide 3.65:1 AA failure on every inactive row (now 12.6:1).
19
+
20
+ ### Added — opt-in, byte-identical defaults
21
+
22
+ - **Org pill takes the Mode accent** (`--mode-accent` / `--mode-on-accent`,
23
+ the latter luminance-derived by the Brand API). Neutral everywhere a Mode
24
+ declares nothing, and in dark.
25
+ - **`navShape: 'flat'` (registry)** — a rail whose whole tree nests under one
26
+ entry renders the children at the root: no parent "Dashboard" row, no
27
+ accordion; rows take the wash active treatment (V1's rail rows).
28
+ - **`beta: false` (registry)** — omits the Beta banner strip for that app.
29
+ - **`'title'` grant** — a granted app's pages may publish the top-bar title
30
+ ("Batches", not the app name on every page).
31
+ - **Granted breadcrumbs now REPLACE the derived trail** (like core): a page
32
+ that publishes none shows none, instead of inheriting
33
+ "Dashboard › <page>" from a tree it opted out of.
34
+
35
+ Petitions is the only registry row using any of these.
36
+
1
37
  ## 0.47.1 — 2026-08-13
2
38
 
3
39
  Two fixes found by running a consuming app's WCAG contrast gate against 0.47.0.
@@ -1766,7 +1766,7 @@ function NavSidebar({
1766
1766
  setOpenGroup(activeGroupKey);
1767
1767
  }, [activeGroupKey]);
1768
1768
  const toggleGroup = (key) => setOpenGroup((prev) => prev === key ? null : key);
1769
- return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: "fixed inset-y-0 left-0 z-30 hidden w-[240px] flex-col border-r border-[color:var(--line)] bg-[color:var(--nav-surface)] lg:flex", children: [
1769
+ return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: "fixed inset-y-0 left-0 z-30 hidden w-[240px] flex-col border-r border-[color:var(--line)] bg-[color:var(--sidebar-bg,var(--nav-surface))] lg:flex", children: [
1770
1770
  /* @__PURE__ */ jsxRuntime.jsx(
1771
1771
  Link,
1772
1772
  {
@@ -1792,7 +1792,7 @@ function NavSidebar({
1792
1792
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col overflow-y-auto px-4 pb-2", children: [
1793
1793
  org ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-3", children: [
1794
1794
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center justify-between px-1", children: [
1795
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Organization" }),
1795
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--sidebar-text-muted,var(--muted))]", children: "Organization" }),
1796
1796
  org.id && !org.loading ? /* @__PURE__ */ jsxRuntime.jsx(OrgIdCopyButton, { id: org.id }) : null
1797
1797
  ] }),
1798
1798
  org.loading ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex w-full items-center gap-2.5 rounded-md border border-[color:var(--line)] bg-[color:var(--panel-elevated)] px-2.5 py-2 shadow-sm", children: [
@@ -1812,7 +1812,7 @@ function NavSidebar({
1812
1812
  ),
1813
1813
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: "Loading organization" })
1814
1814
  ] }) : org.organizations && org.organizations.length > 0 && org.onSwitch ? /* @__PURE__ */ jsxRuntime.jsxs("details", { "data-nav-details": true, className: "group relative", children: [
1815
- /* @__PURE__ */ jsxRuntime.jsxs("summary", { className: "flex w-full cursor-pointer list-none items-center justify-between gap-2 rounded-md border border-[color:var(--line)] bg-[color:var(--panel-elevated)] px-2.5 py-2 text-left text-[color:var(--foreground)] shadow-sm transition hover:bg-[color:var(--panel)] [&::-webkit-details-marker]:hidden", children: [
1815
+ /* @__PURE__ */ jsxRuntime.jsxs("summary", { className: "flex w-full cursor-pointer list-none items-center justify-between gap-2 rounded-md border border-[color:var(--mode-accent,var(--line))] bg-[color:var(--mode-accent,var(--panel-elevated))] px-2.5 py-2 text-left text-[color:var(--mode-on-accent,var(--foreground))] shadow-sm transition hover:bg-[color:var(--mode-accent-strong,var(--panel))] [&::-webkit-details-marker]:hidden", children: [
1816
1816
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 items-center gap-2.5", children: [
1817
1817
  /* @__PURE__ */ jsxRuntime.jsx(
1818
1818
  "span",
@@ -1875,7 +1875,7 @@ function NavSidebar({
1875
1875
  {
1876
1876
  type: "button",
1877
1877
  onClick: org.onClick,
1878
- className: "flex w-full items-center justify-between gap-2 rounded-md border border-[color:var(--line)] bg-[color:var(--panel-elevated)] px-2.5 py-2 text-left text-[color:var(--foreground)] shadow-sm transition hover:bg-[color:var(--panel)]",
1878
+ className: "flex w-full items-center justify-between gap-2 rounded-md border border-[color:var(--mode-accent,var(--line))] bg-[color:var(--mode-accent,var(--panel-elevated))] px-2.5 py-2 text-left text-[color:var(--mode-on-accent,var(--foreground))] shadow-sm transition hover:bg-[color:var(--mode-accent-strong,var(--panel))]",
1879
1879
  children: [
1880
1880
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 items-center gap-2.5", children: [
1881
1881
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1940,7 +1940,7 @@ function NavSidebar({
1940
1940
  }
1941
1941
  ) }) : null,
1942
1942
  /* @__PURE__ */ jsxRuntime.jsxs("nav", { className: "flex flex-col gap-1", children: [
1943
- nav.groupLabel ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-2 pb-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: nav.groupLabel }) : null,
1943
+ nav.groupLabel ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-2 pb-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--sidebar-text-muted,var(--muted))]", children: nav.groupLabel }) : null,
1944
1944
  nav.items.map((item, idx) => {
1945
1945
  if (item.kind === "section") {
1946
1946
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -1952,6 +1952,30 @@ function NavSidebar({
1952
1952
  `section-${idx}-${item.label}`
1953
1953
  );
1954
1954
  }
1955
+ if (nav.flatten && item.children && item.children.length > 0) {
1956
+ return item.children.map((child, i) => {
1957
+ if (child.kind === "section") {
1958
+ return /* @__PURE__ */ jsxRuntime.jsx(
1959
+ NavSectionRow,
1960
+ {
1961
+ label: child.label,
1962
+ first: idx === 0 && i === 0
1963
+ },
1964
+ `flat-section-${idx}-${i}-${child.label}`
1965
+ );
1966
+ }
1967
+ return /* @__PURE__ */ jsxRuntime.jsx(
1968
+ NavRow,
1969
+ {
1970
+ item: child,
1971
+ active: child.href ? activeFn(pathname, child.href) : false,
1972
+ Link,
1973
+ variant: "flat"
1974
+ },
1975
+ child.href ?? `flat-${idx}-${i}-${child.label}`
1976
+ );
1977
+ });
1978
+ }
1955
1979
  if (item.children && item.children.length > 0) {
1956
1980
  const key = groupKeyOf(item, idx);
1957
1981
  const selfActive = item.href ? activeFn(pathname, item.href) : false;
@@ -2059,7 +2083,7 @@ function NavSectionRow({
2059
2083
  "p",
2060
2084
  {
2061
2085
  className: [
2062
- "text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]",
2086
+ "text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--sidebar-text-muted,var(--muted))]",
2063
2087
  variant === "sub" ? "px-3 pb-0.5" : "px-2 pb-1",
2064
2088
  first ? "" : "mt-3"
2065
2089
  ].join(" "),
@@ -2075,14 +2099,15 @@ function NavRow({
2075
2099
  }) {
2076
2100
  const Icon = item.icon;
2077
2101
  const isSub = variant === "sub";
2078
- const activeClasses = isSub ? "bg-[color:var(--mode-accent-surface,var(--panel-elevated))] text-[color:var(--color-accent)] shadow-sm" : "bg-[color:var(--color-accent)] text-white shadow-sm";
2102
+ const washActive = variant !== "top";
2103
+ const activeClasses = washActive ? "bg-[color:var(--mode-accent-surface,var(--panel-elevated))] text-[color:var(--color-accent)] shadow-sm" : "bg-[color:var(--color-accent)] text-white shadow-sm";
2079
2104
  const wrapperClasses = [
2080
2105
  "group relative block overflow-hidden rounded-md transition",
2081
- active ? activeClasses : "text-[color:var(--muted)] hover:text-white"
2106
+ active ? activeClasses : "text-[color:var(--sidebar-text,var(--muted))] hover:text-white"
2082
2107
  ].join(" ");
2083
2108
  const rowPadding = isSub ? "px-3 py-1.5 text-[13px]" : "px-3 py-2 text-sm";
2084
2109
  const iconSize = isSub ? 16 : 18;
2085
- const iconClass = active && !isSub ? "text-white" : "text-current";
2110
+ const iconClass = active && !washActive ? "text-white" : "text-current";
2086
2111
  const inner = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2087
2112
  !active ? /* @__PURE__ */ jsxRuntime.jsx(
2088
2113
  "span",
@@ -2147,7 +2172,7 @@ function NavGroup({
2147
2172
  const headerClasses = [
2148
2173
  "group relative block w-full overflow-hidden text-left transition",
2149
2174
  open ? "rounded-t-md" : "rounded-md",
2150
- active ? "bg-[color:var(--color-accent)] text-white shadow-sm" : open ? "bg-[color:var(--panel)] text-[color:var(--foreground)]" : "text-[color:var(--muted)] hover:text-white"
2175
+ active ? "bg-[color:var(--color-accent)] text-white shadow-sm" : open ? "bg-[color:var(--panel)] text-[color:var(--foreground)]" : "text-[color:var(--sidebar-text,var(--muted))] hover:text-white"
2151
2176
  ].join(" ");
2152
2177
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2153
2178
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -2244,7 +2269,7 @@ function NavSubGroup({
2244
2269
  "aria-expanded": open,
2245
2270
  className: [
2246
2271
  "flex w-full items-center gap-2.5 rounded px-3 py-1.5 text-left text-[13px] font-medium transition",
2247
- ownsActive ? "text-[color:var(--color-accent)]" : "text-[color:var(--muted)] hover:bg-[color:var(--nav-row-hover)] hover:text-[color:var(--foreground)]"
2272
+ ownsActive ? "text-[color:var(--color-accent)]" : "text-[color:var(--sidebar-text,var(--muted))] hover:bg-[color:var(--nav-row-hover)] hover:text-[color:var(--foreground)]"
2248
2273
  ].join(" "),
2249
2274
  children: [
2250
2275
  Icon ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: 15, className: "shrink-0 text-current" }) : null,
@@ -3037,7 +3062,11 @@ var POLITOGY_APPS = {
3037
3062
  // <main>), and its adjudication case is addressed per sheet and per line —
3038
3063
  // a trail derived from the nav tree reads "Dashboard › Adjudication" on
3039
3064
  // every one of those URLs and cannot name the record you are actually on.
3040
- grants: ["contentWrapper", "breadcrumbs"]
3065
+ // 'title' for the same reason: its pages have real names ("Batches"),
3066
+ // where the registry can only say "Petitions".
3067
+ grants: ["contentWrapper", "breadcrumbs", "title"],
3068
+ navShape: "flat",
3069
+ beta: false
3041
3070
  },
3042
3071
  // Campaign Mode apps — each scoped to Campaign, so they render the Campaign
3043
3072
  // tree and a static Campaign pill. Not yet deployed; hidden from the Campaign
@@ -3178,7 +3207,7 @@ function FrameBody({
3178
3207
  () => deriveBreadcrumbs(navItems, pathname, { search, origin: meta.origin }),
3179
3208
  [navItems, pathname, search, meta.origin]
3180
3209
  );
3181
- const breadcrumbs = core ? core.page?.breadcrumbs : granted("breadcrumbs") ?? derivedBreadcrumbs;
3210
+ const breadcrumbs = core ? core.page?.breadcrumbs : grants?.includes("breadcrumbs") ? granted("breadcrumbs") : derivedBreadcrumbs;
3182
3211
  return /* @__PURE__ */ jsxRuntime.jsx(
3183
3212
  NavAppShell,
3184
3213
  {
@@ -3191,7 +3220,11 @@ function FrameBody({
3191
3220
  title: "Politogy"
3192
3221
  },
3193
3222
  nav: {
3194
- items: navDisplay === "tabs" ? navItems.map(({ children: _sub, ...item }) => item) : navItems
3223
+ // A flat rail's sub-menu IS the nav, so the 'tabs only' preference
3224
+ // (which strips children) would leave one childless parent row —
3225
+ // flat apps ignore the stripping.
3226
+ items: navDisplay === "tabs" && meta.navShape !== "flat" ? navItems.map(({ children: _sub, ...item }) => item) : navItems,
3227
+ flatten: meta.navShape === "flat"
3195
3228
  },
3196
3229
  // The app's own workspace is NOT the organization — it is a per-app
3197
3230
  // bootstrap account with a different id in every app. Showing it while
@@ -3210,13 +3243,13 @@ function FrameBody({
3210
3243
  footer,
3211
3244
  linkComponent
3212
3245
  },
3213
- betaBanner: {},
3246
+ betaBanner: meta.beta === false ? void 0 : {},
3214
3247
  modeId: appMode,
3215
3248
  contentWrapper: core?.contentWrapper ?? granted("contentWrapper"),
3216
3249
  topBar: {
3217
3250
  // Core's pages publish their own identity; every other app is one
3218
3251
  // product surface and takes its registry title.
3219
- title: core?.page?.title ?? meta.title,
3252
+ title: core?.page?.title ?? (grants?.includes("title") ? data?.frame?.title : void 0) ?? meta.title,
3220
3253
  breadcrumbs,
3221
3254
  user: {
3222
3255
  name: identity.userName,