@the-portland-company/shell 0.47.0 → 0.48.0-rc.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,71 @@
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
+
37
+ ## 0.47.1 — 2026-08-13
38
+
39
+ Two fixes found by running a consuming app's WCAG contrast gate against 0.47.0.
40
+ Both are 0.47.0 defects; nothing from before 0.47.0 is affected.
41
+
42
+ ### Fixed — `--color-danger` collided with apps' own Tailwind theme colour
43
+
44
+ 0.47.0 introduced `--color-danger` at `:root` to separate "attention/error" from
45
+ "accent". The name was a bad choice: Tailwind v4 derives utilities from
46
+ `--color-*` theme keys, and Petitions already defined
47
+ `--color-danger: var(--tone-danger)` (`#c53030`) to back its `bg-danger` badges.
48
+ The shell's declaration won the cascade, so every `bg-danger` element repainted
49
+ from `#c53030` to `#E63946` — dropping white-on-danger from 6.2:1 to **4.17:1**
50
+ and failing WCAG AA on eight badges in one view.
51
+
52
+ Renamed to **`--chrome-danger`**. The shell should not squat a generic
53
+ `--color-*` name that apps legitimately own; `--color-accent` stays as it is,
54
+ because that one is a documented chrome alias apps already consume.
55
+
56
+ ### Fixed — a Mode accent must not carry into dark
57
+
58
+ 0.47.0 let a Mode's accent through to dark on the theory that a foreground
59
+ colour is safe there. Measured: Petition Mode's `#15803D` on the dark elevated
60
+ panel `#1F1F1F` is **3.29:1**, and it regressed fourteen routes at once.
61
+
62
+ A Mode's palette is authored against white. Nothing makes it legible against
63
+ black, and deriving a dark variant would be inventing a dark theme for a Mode
64
+ whose design never had one — which the brief explicitly ruled out. So a Mode
65
+ accent is now reset under `.dark`, and dark keeps the platform's neutral ramp
66
+ exactly as it did on 0.46.0. This is chrome POLICY, in `chrome.css`, so no Mode
67
+ can opt out of it by authoring a value.
68
+
1
69
  ## 0.47.0 — 2026-08-13
2
70
 
3
71
  Petition Mode has been green since V1 and has rendered red for the whole of the
@@ -20,7 +88,7 @@ Besides mis-signalling, it made the accent unoverridable — apps reasonably set
20
88
 
21
89
  Those 32 references now resolve a semantic token, split by what the colour
22
90
  actually means: `--color-accent` for genuine accent furniture, and a new
23
- `--color-danger` for attention and error (count badges, the low-fuel meter and
91
+ `--chrome-danger` for attention and error (count badges, the low-fuel meter and
24
92
  its nudge, validation messages). The danger set stays red in every Mode — a
25
93
  green "3 unread" badge is worse than a red one.
26
94
 
@@ -165,7 +165,7 @@ function resolveOrgLogoUrl(url) {
165
165
  return `${CORE_APP_ORIGIN}${raw.startsWith("/") ? "" : "/"}${raw}`;
166
166
  }
167
167
  function topBarIconButtonClass(tone = "default") {
168
- const toneClass = tone === "danger" || tone === "warning" ? "text-[color:var(--color-danger)] hover:text-[color:var(--color-danger)]" : "text-[color:var(--topbar-icon-color,var(--muted))] hover:text-[color:var(--topbar-icon-hover-color,var(--foreground))] hover:bg-[color:var(--topbar-icon-hover-bg,transparent)]";
168
+ const toneClass = tone === "danger" || tone === "warning" ? "text-[color:var(--chrome-danger)] hover:text-[color:var(--chrome-danger)]" : "text-[color:var(--topbar-icon-color,var(--muted))] hover:text-[color:var(--topbar-icon-hover-color,var(--foreground))] hover:bg-[color:var(--topbar-icon-hover-bg,transparent)]";
169
169
  return [
170
170
  // `group/tbtip` powers the shared <TopBarTooltip> rendered inside the trigger.
171
171
  "group/tbtip relative flex items-center justify-center rounded-full border border-[color:var(--line)] transition",
@@ -192,7 +192,7 @@ function useAnyTopBarPanelOpen() {
192
192
  }
193
193
  function TopBarCountBadge({ count }) {
194
194
  if (!(count > 0)) return null;
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-danger)] px-1 text-[10px] font-normal leading-none 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(--chrome-danger)] px-1 text-[10px] font-normal leading-none text-white", children: count > 99 ? "99+" : count });
196
196
  }
197
197
  function TopBarTooltip({
198
198
  title,
@@ -516,7 +516,7 @@ function FuelRow({
516
516
  {
517
517
  className: [
518
518
  "text-xs font-semibold tabular-nums",
519
- low ? "text-[color:var(--color-danger)]" : "text-[color:var(--muted)]"
519
+ low ? "text-[color:var(--chrome-danger)]" : "text-[color:var(--muted)]"
520
520
  ].join(" "),
521
521
  children: [
522
522
  remaining.toLocaleString(),
@@ -540,7 +540,7 @@ function FuelRow({
540
540
  {
541
541
  className: [
542
542
  "h-full rounded-full transition-[width]",
543
- low ? "bg-[color:var(--color-danger)]" : "bg-[color:var(--color-ok,#00A651)]"
543
+ low ? "bg-[color:var(--chrome-danger)]" : "bg-[color:var(--color-ok,#00A651)]"
544
544
  ].join(" "),
545
545
  style: { width: `${ratio}%` }
546
546
  }
@@ -576,14 +576,14 @@ function LowFuelHint({ lowFuel, orgId }) {
576
576
  "div",
577
577
  {
578
578
  role: "status",
579
- className: "absolute right-0 top-[calc(100%+10px)] z-50 w-60 rounded-lg border border-[color:var(--color-danger)] bg-[color:var(--color-danger)] px-3 py-2 text-white shadow-[var(--shadow-soft)]",
579
+ className: "absolute right-0 top-[calc(100%+10px)] z-50 w-60 rounded-lg border border-[color:var(--chrome-danger)] bg-[color:var(--chrome-danger)] px-3 py-2 text-white shadow-[var(--shadow-soft)]",
580
580
  onClick: (e) => e.stopPropagation(),
581
581
  children: [
582
582
  /* @__PURE__ */ jsxRuntime.jsx(
583
583
  "span",
584
584
  {
585
585
  "aria-hidden": true,
586
- className: "absolute -top-[5px] right-[11px] h-2.5 w-2.5 rotate-45 bg-[color:var(--color-danger)]"
586
+ className: "absolute -top-[5px] right-[11px] h-2.5 w-2.5 rotate-45 bg-[color:var(--chrome-danger)]"
587
587
  }
588
588
  ),
589
589
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2", children: [
@@ -1237,7 +1237,7 @@ function SupportAction({ orgId } = {}) {
1237
1237
  className: "w-full rounded-lg border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2 text-sm text-[color:var(--foreground)] placeholder:text-[color:var(--muted)] focus:outline-none focus:ring-2 focus:ring-[color:var(--color-accent)]"
1238
1238
  }
1239
1239
  ),
1240
- showEmailError ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-1 block text-xs text-[color:var(--color-danger)]", children: "Enter an email so support can reply." }) : null
1240
+ showEmailError ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-1 block text-xs text-[color:var(--chrome-danger)]", children: "Enter an email so support can reply." }) : null
1241
1241
  ] }) : null,
1242
1242
  missingPhone ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "mt-3 block", children: [
1243
1243
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mb-1 block text-xs font-semibold text-[color:var(--foreground)]", children: "Your phone \u2014 optional" }),
@@ -1262,7 +1262,7 @@ function SupportAction({ orgId } = {}) {
1262
1262
  className: "mt-4 w-full resize-y rounded-lg border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2 text-sm text-[color:var(--foreground)] placeholder:text-[color:var(--muted)] focus:outline-none focus:ring-2 focus:ring-[color:var(--color-accent)]"
1263
1263
  }
1264
1264
  ),
1265
- state === "error" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-[color:var(--color-danger)]", children: "Sending failed \u2014 try again." }) : null,
1265
+ state === "error" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-[color:var(--chrome-danger)]", children: "Sending failed \u2014 try again." }) : null,
1266
1266
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [
1267
1267
  /* @__PURE__ */ jsxRuntime.jsx(
1268
1268
  "button",
@@ -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,