@dloizides/ui-nav 1.8.0 → 1.10.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,5 +1,54 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.10.0
4
+
5
+ - **Fix (`layout` was not authoritative — `layout="side"` rendered a top bar anyway).**
6
+ `wantsTopBar` carried an extra `(layout === 'side' && hasTopBar)` clause, so passing a
7
+ `topBar` **config** with `layout="side"` mounted the bar regardless — making `'side'` and
8
+ `'both'` render **pixel-identically** and `layout` unobservable. `layout` now decides which
9
+ REGIONS exist; `topBar` is only the CONTENT of the top region. A `'side'` app that also wants
10
+ a bar asks for `layout="both"`, which is what that combination always meant.
11
+ - Found by `apps/ui-showcase`, which renders every orientation side by side — a product app
12
+ picks ONE orientation and never sees the other, which is why this survived to 1.9.0.
13
+ - **Not breaking in this fleet:** no app passed `layout="side"` + `topBar`. The four `'side'`
14
+ portals (erevna, katalogos, zygos, agora) pass a `header` NODE, not a `topBar` config.
15
+ - The `hasHeader` clause is **deliberately kept**: `header` is the documented escape hatch by
16
+ which a `'side'` app supplies its own ready-made header (the structured `Topbar` with
17
+ language / notification / user slots). It is an explicit "render THIS node", not a config
18
+ whose presence is silently read as an orientation change.
19
+ - **a11y: `AccountState.accountHint`** — the account-name button's `accessibilityHint` was
20
+ hardcoded to `displayName`, byte-identical to its `accessibilityLabel`, so a screen reader
21
+ announced the name twice and never said what pressing it does — and the consumer had no way
22
+ to correct it. The sibling `upgrade?: TopbarAction` on the same interface has always carried
23
+ a `hint`; this closes that inconsistency. Falls back to `displayName`, so existing callers
24
+ are unaffected.
25
+ - **a11y: `AccountState.accountTestID`** — the account-name button's testID was hardcoded, with
26
+ no override, unlike `TopbarAction.testID?` / `NavItem.testID?` elsewhere in the package.
27
+ - **a11y: `Topbar.language.testID`** — the inline `language` prop type declared no `testID`, so
28
+ the repo's testID+label+hint standard was **structurally unsatisfiable** for that one button
29
+ while every other Topbar action (all `TopbarAction`) accepted one.
30
+
31
+ > Note for consumers: this react-native-web build does **not** forward `accessibilityHint` to
32
+ > the DOM at all — rendered buttons carry `aria-label` but no hint attribute. The hint is a
33
+ > NATIVE-only guarantee today. That is pre-existing and unchanged here, but it means DOM-level
34
+ > tests cannot distinguish a threaded hint from a dropped one; `topbarA11y.test.tsx` therefore
35
+ > pins the prop threading directly.
36
+
37
+ ## 1.9.0
38
+
39
+ **`NavBar.contentMaxWidth` — a full-bleed bar with a centred content column.**
40
+ An optional content-column cap (px), forwarded via `NavShell.topBar.contentMaxWidth`.
41
+ When set, the OUTER `NavBar` container — which carries the bar background + bottom
42
+ border (from `barTheme` / theme surface + any `containerStyle`, e.g. the AML
43
+ radial-glow appbar) — stays FULL WIDTH (full-bleed), while the INNER row (brand +
44
+ links + right) is capped to `contentMaxWidth` and centred. This restores the v1-console
45
+ `.ui-appbar` (full-bleed) + `.ui-appbar-inner { max-width; margin: 0 auto }` shape:
46
+ previously an app had to put the cap on the container's `containerStyle`, which also
47
+ capped the dark background and left light gutters on wide viewports. Omit it and the
48
+ inner spans the full container (unchanged — byte-identical to before). The priority+
49
+ overflow "…" fit measures the capped inner's rendered width, so it keeps working
50
+ against the content column, not the full-bleed container. Purely additive.
51
+
3
52
  ## 1.8.0
4
53
 
5
54
  The navigation shell becomes the ONE shell for every portal: a first-class
package/README.md CHANGED
@@ -126,6 +126,7 @@ import { NavShell, type NavItem } from '@dloizides/ui-nav';
126
126
  right: <><LanguageSwitcher /><UserChip /></>,
127
127
  overflowLabel: FM('menu.more'),
128
128
  barTheme: darkChromeTheme, // optional: dark bar over a light page
129
+ contentMaxWidth: 1180, // optional: full-bleed bar, inner capped + centred
129
130
  }}
130
131
  sideRail={{
131
132
  items: sectionItems, // side sections (leaf + expandable)
@@ -169,6 +170,19 @@ are re-passed so labels keep localizing; every colour still flows through the th
169
170
  (style-only). Custom appbar backgrounds (e.g. a radial-glow gradient) stay
170
171
  expressible via `containerStyle`.
171
172
 
173
+ ### Full-bleed bar with a centred content column (`NavBar.contentMaxWidth`)
174
+
175
+ Pass `contentMaxWidth` (px) — via `NavBar.contentMaxWidth` or
176
+ `NavShell.topBar.contentMaxWidth` — to make the bar **full-bleed** while its content
177
+ stays in a centred column. The OUTER container (which carries the bar background +
178
+ bottom border from `barTheme` / theme surface + any `containerStyle`, e.g. AML's
179
+ radial-glow appbar) spans the full viewport width; the INNER row (brand + links +
180
+ right) is capped to `contentMaxWidth` and centred — the v1-console `.ui-appbar`
181
+ (full-bleed) + `.ui-appbar-inner { max-width; margin: 0 auto }` shape. Omit it and
182
+ the inner spans the full container (unchanged). The priority+ overflow "…" fit
183
+ measures the capped inner's rendered width, so it keeps working against the content
184
+ column, not the full-bleed container.
185
+
172
186
  ### Cross-navigation pills (`PillNav`)
173
187
 
174
188
  A row of fully-rounded pills from a `NavItem[]` — kefi's coral dashboard switcher.
package/dist/index.d.mts CHANGED
@@ -120,16 +120,34 @@ interface AccountState {
120
120
  plan?: AccountPlan;
121
121
  onLogout: () => void;
122
122
  onAccount?: () => void;
123
+ /**
124
+ * a11y hint for the account-name button (what pressing it DOES).
125
+ *
126
+ * Without this the hint fell back to `displayName` — byte-identical to the
127
+ * accessibility LABEL, so a screen reader announced the name twice and never
128
+ * said where the button goes. The sibling `upgrade?: TopbarAction` on this very
129
+ * interface has always carried a `hint`; this closes that inconsistency. The
130
+ * `displayName` fallback is retained so existing callers are unaffected.
131
+ */
132
+ accountHint?: string;
133
+ /** testID for the account-name button. Defaults to `NAV_TEST_IDS.accountName`. */
134
+ accountTestID?: string;
123
135
  upgrade?: TopbarAction;
124
136
  }
125
137
  interface TopbarProps {
126
138
  /** Left slot — typically the tenant logo. */
127
139
  left?: React.ReactNode;
128
- /** Optional language toggle. */
140
+ /**
141
+ * Optional language toggle. `testID` is optional but the repo standard wants
142
+ * one — before 1.10.0 this inline type declared none, so a testID was
143
+ * structurally unreachable for this button while every other Topbar action
144
+ * (which use `TopbarAction`) accepted one.
145
+ */
129
146
  language?: {
130
147
  label: string;
131
148
  hint: string;
132
149
  onPress: () => void;
150
+ testID?: string;
133
151
  };
134
152
  /** Optional notification slot (e.g. a notification bell). */
135
153
  notificationSlot?: React.ReactNode;
@@ -210,6 +228,16 @@ interface NavBarProps {
210
228
  overflowHint?: string;
211
229
  /** Extra container style overrides. */
212
230
  containerStyle?: ViewStyle | ViewStyle[];
231
+ /**
232
+ * Optional content-column cap (px). When set, the OUTER container — which carries
233
+ * the background + bottom border (`barTheme`/theme surface + any `containerStyle`,
234
+ * e.g. the AML radial-glow appbar) — stays FULL WIDTH (full-bleed), while the INNER
235
+ * row (brand + links + right) is capped to this width and centred. Omit for the
236
+ * default where the inner spans the full container. The priority+ overflow "…" fit
237
+ * measures the (now capped) inner's rendered width via `onLayout`, so it keeps
238
+ * working against the capped column, not the full-bleed container.
239
+ */
240
+ contentMaxWidth?: number;
213
241
  /**
214
242
  * Optional CHROME theme override — re-provisions the `@dloizides/ui-feedback`
215
243
  * theme for the bar ONLY, so the top bar can be dark while the page below keeps
@@ -508,6 +536,13 @@ interface NavShellTopBar {
508
536
  * `ViewStyle`, the AML radial-glow appbar). Style-only.
509
537
  */
510
538
  containerStyle?: ViewStyle | ViewStyle[];
539
+ /**
540
+ * Optional content-column cap (px) for the top bar. When set, the bar's
541
+ * background + bottom border (from `containerStyle` / `barTheme`) stay FULL WIDTH
542
+ * (full-bleed) while the inner brand + links + right row is capped to this width
543
+ * and centred — the AML v1-console appbar shape. Forwarded to `NavBar.contentMaxWidth`.
544
+ */
545
+ contentMaxWidth?: number;
511
546
  /**
512
547
  * Optional CHROME theme override for the top bar only (dark bar over a light
513
548
  * page). Forwarded to `NavBar.barTheme`. Style-only — see `NavBar`.
@@ -574,8 +609,9 @@ interface NavShellProps extends ForwardedShellProps {
574
609
  navigateHint?: (label: string) => string;
575
610
  /**
576
611
  * Top-bar config → a `NavBar` in the header slot. Required for `'top'` /
577
- * `'both'`; ignored for `'side'` unless supplied (a `'side'` app may still pass
578
- * a `topBar` with empty `items` for its brand + user header).
612
+ * `'both'`; **ignored for `'side'`** `layout` decides which regions exist, so
613
+ * a `'side'` app that also wants a bar asks for `layout="both"`. A `'side'` app
614
+ * that wants its OWN header node uses the `header` escape hatch instead.
579
615
  */
580
616
  topBar?: NavShellTopBar;
581
617
  /**
package/dist/index.d.ts CHANGED
@@ -120,16 +120,34 @@ interface AccountState {
120
120
  plan?: AccountPlan;
121
121
  onLogout: () => void;
122
122
  onAccount?: () => void;
123
+ /**
124
+ * a11y hint for the account-name button (what pressing it DOES).
125
+ *
126
+ * Without this the hint fell back to `displayName` — byte-identical to the
127
+ * accessibility LABEL, so a screen reader announced the name twice and never
128
+ * said where the button goes. The sibling `upgrade?: TopbarAction` on this very
129
+ * interface has always carried a `hint`; this closes that inconsistency. The
130
+ * `displayName` fallback is retained so existing callers are unaffected.
131
+ */
132
+ accountHint?: string;
133
+ /** testID for the account-name button. Defaults to `NAV_TEST_IDS.accountName`. */
134
+ accountTestID?: string;
123
135
  upgrade?: TopbarAction;
124
136
  }
125
137
  interface TopbarProps {
126
138
  /** Left slot — typically the tenant logo. */
127
139
  left?: React.ReactNode;
128
- /** Optional language toggle. */
140
+ /**
141
+ * Optional language toggle. `testID` is optional but the repo standard wants
142
+ * one — before 1.10.0 this inline type declared none, so a testID was
143
+ * structurally unreachable for this button while every other Topbar action
144
+ * (which use `TopbarAction`) accepted one.
145
+ */
129
146
  language?: {
130
147
  label: string;
131
148
  hint: string;
132
149
  onPress: () => void;
150
+ testID?: string;
133
151
  };
134
152
  /** Optional notification slot (e.g. a notification bell). */
135
153
  notificationSlot?: React.ReactNode;
@@ -210,6 +228,16 @@ interface NavBarProps {
210
228
  overflowHint?: string;
211
229
  /** Extra container style overrides. */
212
230
  containerStyle?: ViewStyle | ViewStyle[];
231
+ /**
232
+ * Optional content-column cap (px). When set, the OUTER container — which carries
233
+ * the background + bottom border (`barTheme`/theme surface + any `containerStyle`,
234
+ * e.g. the AML radial-glow appbar) — stays FULL WIDTH (full-bleed), while the INNER
235
+ * row (brand + links + right) is capped to this width and centred. Omit for the
236
+ * default where the inner spans the full container. The priority+ overflow "…" fit
237
+ * measures the (now capped) inner's rendered width via `onLayout`, so it keeps
238
+ * working against the capped column, not the full-bleed container.
239
+ */
240
+ contentMaxWidth?: number;
213
241
  /**
214
242
  * Optional CHROME theme override — re-provisions the `@dloizides/ui-feedback`
215
243
  * theme for the bar ONLY, so the top bar can be dark while the page below keeps
@@ -508,6 +536,13 @@ interface NavShellTopBar {
508
536
  * `ViewStyle`, the AML radial-glow appbar). Style-only.
509
537
  */
510
538
  containerStyle?: ViewStyle | ViewStyle[];
539
+ /**
540
+ * Optional content-column cap (px) for the top bar. When set, the bar's
541
+ * background + bottom border (from `containerStyle` / `barTheme`) stay FULL WIDTH
542
+ * (full-bleed) while the inner brand + links + right row is capped to this width
543
+ * and centred — the AML v1-console appbar shape. Forwarded to `NavBar.contentMaxWidth`.
544
+ */
545
+ contentMaxWidth?: number;
511
546
  /**
512
547
  * Optional CHROME theme override for the top bar only (dark bar over a light
513
548
  * page). Forwarded to `NavBar.barTheme`. Style-only — see `NavBar`.
@@ -574,8 +609,9 @@ interface NavShellProps extends ForwardedShellProps {
574
609
  navigateHint?: (label: string) => string;
575
610
  /**
576
611
  * Top-bar config → a `NavBar` in the header slot. Required for `'top'` /
577
- * `'both'`; ignored for `'side'` unless supplied (a `'side'` app may still pass
578
- * a `topBar` with empty `items` for its brand + user header).
612
+ * `'both'`; **ignored for `'side'`** `layout` decides which regions exist, so
613
+ * a `'side'` app that also wants a bar asks for `layout="both"`. A `'side'` app
614
+ * that wants its OWN header node uses the `header` escape hatch instead.
579
615
  */
580
616
  topBar?: NavShellTopBar;
581
617
  /**
package/dist/index.js CHANGED
@@ -540,6 +540,7 @@ var Topbar = ({
540
540
  accessibilityLabel: language.label,
541
541
  ringColor: primaryColor,
542
542
  style: navStyles.topbarRowItem,
543
+ testID: language.testID,
543
544
  onPress: language.onPress,
544
545
  children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.topbarLabel, { color: colors.text }], children: language.label })
545
546
  }
@@ -553,10 +554,10 @@ var Topbar = ({
553
554
  richAccount.onAccount ? /* @__PURE__ */ jsxRuntime.jsx(
554
555
  FocusableTouchable,
555
556
  {
556
- accessibilityHint: richAccount.displayName,
557
+ accessibilityHint: richAccount.accountHint ?? richAccount.displayName,
557
558
  accessibilityLabel: richAccount.displayName,
558
559
  ringColor: primaryColor,
559
- testID: NAV_TEST_IDS.accountName,
560
+ testID: richAccount.accountTestID ?? NAV_TEST_IDS.accountName,
560
561
  onPress: richAccount.onAccount,
561
562
  children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.userName, { color: colors.text }], children: richAccount.displayName })
562
563
  }
@@ -806,7 +807,8 @@ var NavBarInner = ({
806
807
  collapseBelow = DEFAULT_COLLAPSE_BELOW,
807
808
  overflowLabel = "More",
808
809
  overflowHint = "",
809
- containerStyle
810
+ containerStyle,
811
+ contentMaxWidth
810
812
  }) => {
811
813
  const { theme } = uiFeedback.useUi();
812
814
  const colors = theme.colors;
@@ -818,6 +820,10 @@ var NavBarInner = ({
818
820
  const { visibleCount, setAvailableWidth, setItemWidth, setMoreWidth } = useNavOverflow(items.length, NAV_LINK_GAP);
819
821
  const collapsed = width < collapseBelow;
820
822
  const showLinks = !collapsed || open;
823
+ const innerCapStyle = React5.useMemo(
824
+ () => contentMaxWidth === void 0 ? null : { maxWidth: contentMaxWidth, width: "100%", alignSelf: "center" },
825
+ [contentMaxWidth]
826
+ );
821
827
  const toggleMenu = React5.useCallback(() => setOpen((v) => !v), []);
822
828
  const handlePress = React5.useCallback(
823
829
  (route) => {
@@ -847,7 +853,7 @@ var NavBarInner = ({
847
853
  { backgroundColor: colors.surface, borderBottomColor: colors.border },
848
854
  containerStyle
849
855
  ],
850
- children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [navBarStyles.inner, collapsed ? null : navBarStyles.innerNoWrap], children: [
856
+ children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [navBarStyles.inner, collapsed ? null : navBarStyles.innerNoWrap, innerCapStyle], children: [
851
857
  brand !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: navBarStyles.brand, children: brand }) : null,
852
858
  collapsed ? /* @__PURE__ */ jsxRuntime.jsx(
853
859
  reactNative.Pressable,
@@ -1236,8 +1242,8 @@ var CollapsedRail = ({
1236
1242
  }
1237
1243
  );
1238
1244
  };
1239
- function wantsTopBar(layout, hasHeader, hasTopBar) {
1240
- return layout === "top" || layout === "both" || hasHeader || layout === "side" && hasTopBar;
1245
+ function wantsTopBar(layout, hasHeader) {
1246
+ return layout === "top" || layout === "both" || hasHeader;
1241
1247
  }
1242
1248
  function wantsSideRail(layout, hasSideRail) {
1243
1249
  return (layout === "side" || layout === "both") && hasSideRail;
@@ -1256,7 +1262,7 @@ var NavShell = ({
1256
1262
  collapsedRailRange: collapsedRailRangeProp,
1257
1263
  ...shellProps
1258
1264
  }) => {
1259
- const showTopBar = wantsTopBar(layout, header !== void 0, topBar !== void 0);
1265
+ const showTopBar = wantsTopBar(layout, header !== void 0);
1260
1266
  const showSideRail = wantsSideRail(layout, sideRail !== void 0);
1261
1267
  const headerNode = header !== void 0 ? header : showTopBar && topBar !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1262
1268
  NavBar,
@@ -1265,6 +1271,7 @@ var NavShell = ({
1265
1271
  brand: topBar.brand,
1266
1272
  collapseBelow: topBar.collapseBelow,
1267
1273
  containerStyle: topBar.containerStyle,
1274
+ contentMaxWidth: topBar.contentMaxWidth,
1268
1275
  items: topBar.items,
1269
1276
  menuHint: topBar.menuHint,
1270
1277
  menuLabel: topBar.menuLabel,