@dloizides/ui-nav 1.22.0 → 1.23.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,494 +1,515 @@
1
- # Changelog
2
-
3
- ## 1.20.0
4
-
5
- **The auth-pending and page-loading spinners now OVERLAY stably-mounted children instead of
6
- replacing them — closing a latent deep-link-collapse trap in the shared shell.**
7
-
8
- `AppShell`'s `gate.pending` branch and `appShellContent`'s `state.loading` branch each `return`ed a
9
- full-screen spinner *instead of* `children`. Replacing the subtree unmounts whatever `children` host
10
- — in a portal that is a navigator/`<Slot/>` — and on remount React Navigation re-derives its default
11
- route, so a deep-linked sub-route silently collapses to its default on every reload. (This is the
12
- exact class of bug that hit kefi-web's organizer portal, fixed there in its `OnboardingGate`; the
13
- same shape lived dormant here because no consumer passes `gate=`/`state=` yet.)
14
-
15
- - **`AppShell` (`gate.pending`)** now renders the shell tree normally and paints the pending spinner
16
- as an opaque `position:absolute` overlay on top, so `children` mount once and are never torn down
17
- across `pending → authenticated` (proven by a "same child instance, no remount" test).
18
- - **`appShellContent` (`state.loading`)** likewise overlays the loading spinner over mounted
19
- `children` rather than swapping them out. The `loadingHost` wrapper is gated on `state` being
20
- DEFINED, not on `loading`'s live value — so the element type at the mount point is invariant across
21
- a `loading` true↔false toggle and `children` are reconciled in place, never remounted on the edge
22
- (also proven by a "same child instance, no remount" test). Consumers that pass no `state` render
23
- `children` bare, byte-identical to 1.19.0. Error / forbidden cards are TERMINAL and still replace;
24
- unauthenticated still returns null + `onRedirect`.
25
- - **Rule encoded in the docblock:** a *transitional* status overlays; only a *terminal* state
26
- replaces. Corollary for consumers — `children` mount beneath the pending overlay, so gate data
27
- FETCHES on auth state (`enabled: isAuthenticated`), not on mount. No public API changed; behavior
28
- is unchanged for any consumer not passing `gate`/`state`.
29
-
30
- ## 1.19.0
31
-
32
- **The "More ▾" overflow menu now opens at a readable width instead of cramping onto the trigger's tiny footprint.**
33
-
34
- `NavOverflowMenu` rendered the shared `@dloizides/ui-layout` `ModalDropdown` without a `menuMinWidth`,
35
- so the popover matched the width of the compact "More ▾" pill (~its own label). Option labels
36
- ("Settings", "API docs") wrapped onto two lines, and the resulting tall rows overflowed the menu's
37
- max-height — so the lower items scrolled out of view and read as missing.
38
-
39
- - **`NavOverflowMenu`** now passes a 200px `menuMinWidth` floor, so the overflow menu opens wide
40
- enough to show each label on one line. `ModalDropdown`/`InlineMenu` clamp a floored menu inside the
41
- viewport (ui-layout ≥ 1.24.0, now the peer floor), so the extra width never pushes it off the right
42
- edge. No public API changed.
43
-
44
- ## 1.18.0
45
-
46
- Root-cause fix for sidebar leaf co-activation. Behavioral/additive — no breaking API.
47
-
48
- - **Fix (two sidebar leaves showed `aria-current="page"` at once).** On the live finreg
49
- `/accounting/journals`, BOTH "Chart of Accounts" (route `/accounting`) and "Journals"
50
- (route `/accounting/journals`) were marked active, because the active predicate was purely
51
- prefix-based (`pathname === route || pathname.startsWith(route + '/')`) — a module-home leaf
52
- routed at `/accounting` is a prefix of every `/accounting/*` sibling, so it co-activated on all
53
- of them. (Second occurrence of this class; the first, CRM Contacts at `/crm`, was worked around
54
- by moving the route.) Now resolution is **most-specific (longest) match wins**: among a nav
55
- model's LEAF items, at most ONE is active for any path — the leaf whose route is the longest
56
- prefix match. On `/accounting/journals` only Journals is active; on `/accounting` exactly only
57
- Chart of Accounts is. For nav models with no parent/child route overlap (the common case)
58
- behaviour is unchanged.
59
- - **New export** `resolveActiveRoute(items, pathname)`: flattens the tree to its leaf routes and
60
- returns the single longest matching route (honouring per-leaf `exact`), or `undefined`.
61
- - `Sidebar` resolves the winning route once over the full leaf set and passes `activeRoute` to
62
- each row; `NavExpandableItem` marks a leaf active iff `item.route === activeRoute` (falls back
63
- to the per-item predicate when used standalone — backward compatible). Group HEADER expansion
64
- (`hasActiveDescendant`) is a separate concern and is unchanged.
65
- - The same winner-based resolution now backs `NavBar`, `PillNav`, `CollapsedRail` and
66
- `NavOverflowMenu`, so the latent prefix co-activation cannot resurface in the horizontal navs.
67
- - **Additive API**: new optional `NavItem.exact?: boolean` — a leaf active ONLY on its exact
68
- path (never a nested one). A complementary escape hatch; the longest-match default is the real
69
- fix, so no consumer needs to set it.
70
- - `isRouteActive(pathname, route, exact?)` gains an optional third arg; the two-arg call is
71
- unchanged.
72
-
73
- ## 1.17.0
74
-
75
- Two live-defect fixes to the shared sidebar chrome. Behavioral/additive — no breaking API.
76
-
77
- - **Fix (two search bars on desktop → mutually exclusive by affordance).** 1.16.1 shipped the
78
- desktop inline `SidebarSearch` field, but the ⌘K `CommandPaletteTrigger` could be present at
79
- the same time, so a wide viewport showed BOTH. `Sidebar` now owns both affordances and renders
80
- exactly ONE, chosen by its own viewport width via `resolveSearchAffordance`:
81
- - inline affordance (≥768) → the inline field ONLY; the ⌘K trigger is suppressed.
82
- - palette affordance (<768) → the ⌘K trigger ONLY; the inline field is suppressed.
83
- - The ⌘K keyboard shortcut (`useCommandPaletteHotkey`) is untouched and stays bound at every width.
84
- - **Additive API**: new `Sidebar.paletteTrigger?: SidebarPaletteTriggerConfig`
85
- (`{ label, hint, shortcut, onOpen, testID? }`), forwarded from `NavShellSideRail.paletteTrigger`.
86
- Consumers that wire neither, or only the inline field, are unchanged (the inline field simply
87
- no longer renders below 768, where a persistent field would steal vertical space).
88
-
89
- - **Fix (hover wash never appeared on RN-web).** The nav rows were `TouchableOpacity`, which in
90
- react-native-web has NO hover support and silently drops `onHoverIn`/`onHoverOut` — so the row's
91
- `hovered` state never toggled and the primary-tinted overlay stayed at opacity 0. The leaf and
92
- group-header rows in `NavExpandableItem` are now `Pressable`, whose `useHover` responder actually
93
- drives `hovered`, lifting the token-tinted overlay to the hover wash (`HOVER_TINT_OPACITY`, ~160ms
94
- reduced-motion-gated transition) — distinct from and lighter than the active tint. Token-only
95
- (colours via the theme; no raw hex/rgb/hsl). aria wiring (`aria-current` / `aria-expanded`, which
96
- `MobileDrawer` depends on) is preserved across the switch.
97
-
98
- ## 1.16.1
99
-
100
- - **Fix (NavShell forwards inline-search props to the inner `Sidebar`).** 1.16.0 added
101
- desktop inline search to `Sidebar` (`enableInlineSearch`, `search`, controlled
102
- `searchQuery` / `onSearchChange`), but all seven portals consume the higher-level
103
- `NavShell` (via `sideRail`), which did **not** forward those props — so inline search was
104
- unreachable by every real consumer. `NavShellSideRail` now carries the same four fields
105
- (`enableInlineSearch?`, `search?: SidebarSearchConfig`, `searchQuery?`, `onSearchChange?`)
106
- and threads them onto the `<Sidebar>` it builds. Desktop-vs-mobile resolution is unchanged
107
- (inline ≥768, ⌘K palette <768 via `resolveSearchAffordance`).
108
- - **Purely additive / backward compatible**: with the fields unset, `NavShell` renders
109
- exactly as 1.16.0. No breaking change; no styling touched.
110
-
111
- ## 1.16.0
112
-
113
- - **Feature (docs-style sidebar rail — hover wash, tinted active pill, rotating chevron).**
114
- The `Sidebar` / `NavExpandableItem` rows now read like the aml-screening docs navigation:
115
- each row carries a PRIMARY-TINTED overlay whose opacity eases `0 → hover → active` as a
116
- smooth ~160ms CSS cross-fade on web (gated by `prefers-reduced-motion`, omitted on native).
117
- - **Hover** lights a row with a subtle brand wash (`HOVER_TINT_OPACITY` = 0.06); **active**
118
- deepens it to a soft brand pill (`ACTIVE_TINT_OPACITY` = 0.14) over the transparent
119
- left-accent gutter every leaf already reserved, so the active leaf shows a tinted pill + a
120
- coloured left bar + brand-coloured label — no layout shift, all from `theme.palette.primary`.
121
- - **Rotating chevron.** Expandable groups now get a built-in `SidebarChevron` (a `›` glyph
122
- that rotates to `⌄` on expand, tweened on web) when the caller supplies no `renderChevron`.
123
- A caller-supplied `renderChevron` still wins, so existing icon-set chevrons are unaffected.
124
- - Row corner radius softened to `NAV_ROW_RADIUS` (8px) with the rounded overlay clipped to it.
125
- - No public API change to existing props; hover uses RNW's `onHoverIn`/`onHoverOut` (no-op on
126
- native). New exports: `SidebarChevron`, `NAV_ROW_RADIUS`, `ACTIVE_TINT_OPACITY`,
127
- `HOVER_TINT_OPACITY`.
128
-
129
- - **Feature (desktop INLINE sidebar search — opt-in — with the ⌘K palette kept for mobile).**
130
- The owner found the ⌘K command-palette popup inconvenient on desktop. `Sidebar` now accepts
131
- an opt-in persistent inline search field at the top of the rail that narrows the nav list
132
- LIVE as you type.
133
- - **New `Sidebar` props (all additive, default OFF):** `enableInlineSearch` + a pre-localized
134
- `search: SidebarSearchConfig` (`placeholder` / `hint` / `clearLabel` / `clearHint` /
135
- `emptyText`), plus optional controlled `searchQuery` + `onSearchChange` (the sidebar owns the
136
- state when omitted). With the flag off the render tree is byte-identical to 1.15 — the mobile
137
- drawer, which should keep using the ⌘K palette, is untouched.
138
- - **New exports:** `SidebarSearch` (the field), `filterNavItems` / `isFilterActive` /
139
- `searchTokens` (the pure tree-prune, same all-tokens-must-match semantics as `filterCommands`
140
- so inline + ⌘K rank identically), and `resolveSearchAffordance(viewport)` +
141
- `DEFAULT_SEARCH_BREAKPOINT` — the pure `'inline'` (≥768px) vs `'palette'` (<768px) rule so a
142
- shell can wire desktop-inline / mobile-palette from one function. The ⌘K hotkey
143
- (`useCommandPaletteHotkey`) stays bound at every width.
144
-
145
- ## 1.15.0
146
-
147
- - **Feature (an expandable nav group now ANIMATES its expand/collapse instead of snapping).**
148
- `NavExpandableItem` used to conditionally render its children (`{expanded ? … : null}`),
149
- so a sidebar/drawer group's leaves appeared and vanished INSTANTLY on toggle — a hard,
150
- jarring cut with no transition. The group's children are now wrapped in the
151
- `@dloizides/ui-motion` `<Collapse open={expanded}>`, which measures the children's natural
152
- height and animates a real `Animated.Value` height on WEB and native alike (the height code
153
- path RN's native-only `LayoutAnimation` never gave the web build), collapsing to instant
154
- under `prefers-reduced-motion`.
155
- - **Collapsed leaves stay non-interactive.** Per the `Collapse` contract the children remain
156
- MOUNTED at all times (their state and a11y focus order are preserved); when collapsed they
157
- are clipped to zero height under `overflow: hidden`, so a collapsed group's leaves are
158
- neither visible nor tappable in a real browser even though they are in the tree. This is a
159
- deliberate change from the prior "absent from the DOM when collapsed" behaviour.
160
- - **`aria-expanded` and the mobile-drawer contract are intact.** The group header still emits
161
- `aria-expanded` (the `ariaExpandedProps` spread is unchanged), so `MobileDrawer` still tells
162
- a group TOGGLE apart from a navigating LEAF and does not dismiss the drawer when a group is
163
- expanded on mobile. The active-leaf accent bar, docs-style section-header styling, chevron
164
- slot, testIDs, and all a11y roles/labels/hints are unchanged.
165
- - **No public API change.** `NavExpandableItemProps` / `NavItem` are untouched — the animation
166
- is derived from the `expanded` state the component already tracked. `@dloizides/ui-motion`
167
- (`^1.0.1`) is added as a runtime dependency; all 7 portals gain the animated group on their
168
- next bump for free. Consumers with no expandable groups are visually unaffected.
169
-
170
- ## 1.13.1
171
-
172
- - **Fix (mobile drawer: a sub-item / leaf tap dismissed the drawer WITHOUT navigating).**
173
- On a phone-width viewport, tapping a leaf inside the open `MobileDrawer` closed the drawer
174
- but the route never changed — verified live on finreg via Playwright (real Chromium, real
175
- pointer events); every jsdom unit test stayed green because it wired `onClose` to an inert
176
- `jest.fn()` that never unmounts anything.
177
- - **Root cause.** `MobileDrawer` closes on a nav press via a NATIVE
178
- `panel.addEventListener('click', …)`. react-native-web fires a leaf's `onPress` from its
179
- `onClick`, dispatched through REACT's synthetic event system whose root listener sits on the
180
- app-root container — an ANCESTOR of the panel. So on a real click the native panel listener
181
- runs FIRST (during the DOM bubble), and closing synchronously there ran `setDrawerOpen(false)`,
182
- unmounting the panel and tearing the leaf out of the tree BEFORE React could dispatch its
183
- `onPress`. The navigation call never ran.
184
- - **Fix.** The drawer's close is now DEFERRED to the next macrotask (`setTimeout(…, 0)`), so
185
- React finishes dispatching the tapped leaf's `onPress` (the navigation) for the current click;
186
- the panel then unmounts on the following tick, after the route has already changed. Scrim tap
187
- and group-header toggle are unchanged.
188
- - **No API change.** All 7 portals get the fix on their next bump.
189
-
190
- ## 1.11.0
191
-
192
- - **Feature (an expandable nav group now auto-opens when the current route is inside it).**
193
- `NavExpandableItem` used to mount every group collapsed (`useState(false)`), so on a fresh
194
- page load the group containing the active route was closed and its active leaf was hidden.
195
- A persistent-rail console that groups its nav by module (e.g. finreg-web's Payments / CRM /
196
- Accounting rail) needs the active module's group open on load, the others collapsed — with
197
- no way for the caller to express that, since expansion state was internal and uncontrolled.
198
- - The group now initializes its expansion from whether the current route is inside it:
199
- `useState(() => hasActiveDescendant(item, pathname))`, where `hasActiveDescendant` is true
200
- when the item's own route is active (`isRouteActive`) or any descendant's is (recursive, so
201
- it holds at any nesting depth). The group of the active route mounts EXPANDED (active leaf
202
- visible + highlighted); a group with no active descendant mounts COLLAPSED — exactly the
203
- prior behaviour.
204
- - **No API change.** `NavItem` is unchanged — the behaviour is derived from the `pathname`
205
- the component already receives. Consumers with no expandable groups, or whose groups hold no
206
- active route, are byte-for-byte unaffected; all 7 portals gain the auto-open on their next
207
- bump for free.
208
- - Guarded by `src/navExpandableItem.test.tsx` (RED-first): the two "mounts EXPANDED" assertions
209
- FAIL against the pre-change `useState(false)` (the active group's child is absent from the DOM)
210
- and pass after; the "mounts COLLAPSED" + "header always renders" assertions lock the
211
- backward-compatible half.
212
-
213
- ## 1.10.2
214
-
215
- - **Fix (the mobile nav DRAWER was not clickable — a tap on a nav item did not navigate, it
216
- just closed the drawer / did nothing, in all seven portals below 768px).**
217
- `MobileDrawer` rendered a full-bleed scrim `Pressable` (`StyleSheet.absoluteFillObject`) and a
218
- panel `View` with `{ position: 'absolute', top: 0, bottom: 0, left: 0 }` — **no width and no
219
- zIndex**. Reproduced structurally: the panel's computed `width` was empty and its `zIndex` was
220
- `0`; the scrim's computed `left`/`right` were both `0px` at `zIndex` `0`. So the scrim FULLY
221
- overlapped the panel at the SAME stacking level, and on react-native-web the equal-zIndex scrim
222
- won the pointer (the estate's known "RN-web Pressable swallows a sibling/child press" trap):
223
- the tap closed the drawer and the nav-item's `TouchableOpacity` `onPress` (→ the app's router)
224
- never fired.
225
- - Fixed once, in the kit: the panel now has an explicit drawer **width** (`280px`), a solid
226
- surface background, and a `zIndex` **above** the scrim; the scrim is anchored BESIDE the panel
227
- (`left: DRAWER_WIDTH`) so its tap-to-close hit area never overlaps the panel. Nav-item taps
228
- reach the `Sidebar` leaves; taps beside the drawer still close it.
229
- - Guarded by `src/mobileDrawer.test.tsx` (RED-first): three structural assertions read the
230
- resolved geometry via `getComputedStyle` and FAIL against the pre-fix panel/scrim (no width,
231
- equal zIndex, overlapping scrim); two behavioural assertions lock the contract (nav-item tap
232
- navigates + closes; scrim tap only closes).
233
- - **No API change.** Consumers need no edit — bump the dependency and the drawer works.
234
-
235
- ## 1.10.1
236
-
237
- - **Fix (a plain-string slot logged `console.error` on EVERY render, in all seven portals).**
238
- Every caller-supplied slot in the kit is typed `React.ReactNode`, which legitimately includes
239
- `string` — and callers pass exactly that (`brand: FM('app.name')`, a plain localized string).
240
- The slot was dropped straight into a `<View>`, and react-native-web refuses to render a bare
241
- text node there: `Unexpected text node: <s>. A text node cannot be a child of a <View>.`
242
- Nothing in the type system objected, so the defect only ever surfaced as browser console
243
- noise — which is why it survived to 1.10.0 across the whole fleet.
244
- - Fixed once, in the kit, via the new internal `renderTextSlot` helper: a `string`/`number`
245
- slot is wrapped in `<Text>` (routed through the theme's `colors.text`, so it honours dark
246
- mode rather than defaulting to black); an element slot passes through **untouched**, so
247
- every existing caller renders byte-identically. Array slots have each string member wrapped
248
- individually, keys preserved.
249
- - Applied to **all 12 slot render sites**, not just the reported one — `NavBar` (`brand`,
250
- `right`), `Topbar` (`left`, `notificationSlot`), `Sidebar` (`header`, `footer`),
251
- `CollapsedRail` (`header`, `footer`), `AppShell` (`header`, `nav`, `sidebar`,
252
- `collapsedSidebar`, `banner`) and `MobileDrawer` (`sidebar`).
253
- - Guarded by `src/rawTextSlots.test.tsx`, which spies on `console.error` and asserts silence.
254
- Unit tests that only assert "the slot rendered" pass against the bug; this one does not.
255
- - **No API change.** Consumers need no edit — the `<Text>` wrapper they were expected to
256
- remember is now the kit's job.
257
-
258
- ## 1.10.0
259
-
260
- - **Fix (`layout` was not authoritative — `layout="side"` rendered a top bar anyway).**
261
- `wantsTopBar` carried an extra `(layout === 'side' && hasTopBar)` clause, so passing a
262
- `topBar` **config** with `layout="side"` mounted the bar regardless — making `'side'` and
263
- `'both'` render **pixel-identically** and `layout` unobservable. `layout` now decides which
264
- REGIONS exist; `topBar` is only the CONTENT of the top region. A `'side'` app that also wants
265
- a bar asks for `layout="both"`, which is what that combination always meant.
266
- - Found by `apps/ui-showcase`, which renders every orientation side by side — a product app
267
- picks ONE orientation and never sees the other, which is why this survived to 1.9.0.
268
- - **Not breaking in this fleet:** no app passed `layout="side"` + `topBar`. The four `'side'`
269
- portals (erevna, katalogos, zygos, agora) pass a `header` NODE, not a `topBar` config.
270
- - The `hasHeader` clause is **deliberately kept**: `header` is the documented escape hatch by
271
- which a `'side'` app supplies its own ready-made header (the structured `Topbar` with
272
- language / notification / user slots). It is an explicit "render THIS node", not a config
273
- whose presence is silently read as an orientation change.
274
- - **a11y: `AccountState.accountHint`** — the account-name button's `accessibilityHint` was
275
- hardcoded to `displayName`, byte-identical to its `accessibilityLabel`, so a screen reader
276
- announced the name twice and never said what pressing it does — and the consumer had no way
277
- to correct it. The sibling `upgrade?: TopbarAction` on the same interface has always carried
278
- a `hint`; this closes that inconsistency. Falls back to `displayName`, so existing callers
279
- are unaffected.
280
- - **a11y: `AccountState.accountTestID`** — the account-name button's testID was hardcoded, with
281
- no override, unlike `TopbarAction.testID?` / `NavItem.testID?` elsewhere in the package.
282
- - **a11y: `Topbar.language.testID`** — the inline `language` prop type declared no `testID`, so
283
- the repo's testID+label+hint standard was **structurally unsatisfiable** for that one button
284
- while every other Topbar action (all `TopbarAction`) accepted one.
285
-
286
- > Note for consumers: this react-native-web build does **not** forward `accessibilityHint` to
287
- > the DOM at all — rendered buttons carry `aria-label` but no hint attribute. The hint is a
288
- > NATIVE-only guarantee today. That is pre-existing and unchanged here, but it means DOM-level
289
- > tests cannot distinguish a threaded hint from a dropped one; `topbarA11y.test.tsx` therefore
290
- > pins the prop threading directly.
291
-
292
- ## 1.9.0
293
-
294
- **`NavBar.contentMaxWidth` — a full-bleed bar with a centred content column.**
295
- An optional content-column cap (px), forwarded via `NavShell.topBar.contentMaxWidth`.
296
- When set, the OUTER `NavBar` container — which carries the bar background + bottom
297
- border (from `barTheme` / theme surface + any `containerStyle`, e.g. the AML
298
- radial-glow appbar) — stays FULL WIDTH (full-bleed), while the INNER row (brand +
299
- links + right) is capped to `contentMaxWidth` and centred. This restores the v1-console
300
- `.ui-appbar` (full-bleed) + `.ui-appbar-inner { max-width; margin: 0 auto }` shape:
301
- previously an app had to put the cap on the container's `containerStyle`, which also
302
- capped the dark background and left light gutters on wide viewports. Omit it and the
303
- inner spans the full container (unchanged — byte-identical to before). The priority+
304
- overflow "…" fit measures the capped inner's rendered width, so it keeps working
305
- against the content column, not the full-bleed container. Purely additive.
306
-
307
- ## 1.8.0
308
-
309
- The navigation shell becomes the ONE shell for every portal: a first-class
310
- **top | side | both** layout, plus the capabilities harvested from every RN-web
311
- portal's bespoke nav/shell so nothing is lost when they migrate. Purely additive —
312
- `Sidebar`, `Topbar`, `NavBar`, `Nav`, `AppShell`, and the role helpers keep their
313
- exact signatures; every new prop/component is optional.
314
-
315
- **`NavShell` — top bar, side rail, or BOTH.** One component owns the whole page
316
- composition, chosen by a single `layout` prop (`'top' | 'side' | 'both'`). It wires
317
- a top bar into `AppShell`'s `header` slot and a side rail into its `sidebar` slot,
318
- and inherits every `AppShell` capability (auth gate, loading/error/forbidden state
319
- cards, width policy, banner, the responsive hamburger + overlay drawer). `'both'`
320
- is the AML-console shape — a top bar (brand / global links + overflow "…" / user)
321
- AND a side rail (sections) together — now first-class instead of hand-wired. The
322
- top bar is built from a `topBar` config (a `NavBar`), the side rail from a
323
- `sideRail` config (a `Sidebar`); an explicit `header` node overrides the built top
324
- bar for apps that want a structured `Topbar`.
325
-
326
- **Three-tier responsive rail (`AppShell.collapsedSidebar`).** Opts the `sidebar`
327
- layout into the erevna/katalogos scheme: the full rail on desktop, an intermediate
328
- persistent **collapsed (icon-only) rail** on a tablet band, and the overlay drawer
329
- on a phone. Supply a `collapsedSidebar` node (and optionally `collapsedRailRange`,
330
- default `{ min: 768, max: 1024 }`) — omit it and the layout stays the unchanged
331
- two-tier scheme. New `CollapsedRail` component renders an icon-only rail from the
332
- same `NavItem[]` (icon or first-letter glyph fallback, header/footer slots). New
333
- `resolveRailMode` pure resolver + `RAIL_FULL_BREAKPOINT` / `DEFAULT_COLLAPSED_RAIL_MAX`.
334
- `NavShell.sideRail.collapsed` builds it config-driven. New testID suffix
335
- `-collapsed-sidebar`.
336
-
337
- **`NavBar.barTheme` — dark bar over a light page.** An optional chrome-theme
338
- override re-provisions the `@dloizides/ui-feedback` theme for the bar only (the AML
339
- v1-console dark appbar over light content), re-passing the live `t` / `navigate` so
340
- labels keep localizing. Forwarded via `NavShell.topBar.barTheme`. Style-only —
341
- colours still flow through the theme. (Custom appbar backgrounds like AML's radial
342
- glow stay expressible via `containerStyle`.)
343
-
344
- **`PillNav` — coral cross-navigation (kefi).** A row of fully-rounded pills from a
345
- `NavItem[]`, the active one wearing the accent (theme `palette.primary` = kefi's
346
- coral). Folds in kefi's **visibility rule**: hides (renders `null`) unless there
347
- are at least `minItems` entries (default 1; kefi passes 2).
348
-
349
- **`DarkModeControl` — 3-way colour scheme (erevna/katalogos).** A controlled
350
- Light / Dark / System control in two variants: `"segmented"` (a highlighted
351
- segment row) and `"cycle"` (an icon+label button that advances to the next option
352
- on press). Value-agnostic string options, pre-localized labels/hints.
353
-
354
- **Refactor (no behaviour change).** `AppShell`'s page-state cards moved to
355
- `appShellContent.tsx` (`useContentBody` / `ShellMessage` / `ShellState`) to keep
356
- `AppShell` focused on layout; the shell's render tree is unchanged.
357
-
358
- New exports: `NavShell` (+ `NavShellProps` / `NavShellLayout` / `NavShellTopBar` /
359
- `NavShellSideRail` / `NavShellCollapsedRail`), `CollapsedRail` (+ `CollapsedRailProps`),
360
- `PillNav` (+ `PillNavProps`), `DarkModeControl` (+ `DarkModeControlProps` /
361
- `DarkModeOption` / `DarkModeVariant`), `resolveRailMode` (+ `RailMode` /
362
- `CollapsedRailRange` / `ResolveRailModeArgs`), `RAIL_FULL_BREAKPOINT`,
363
- `DEFAULT_COLLAPSED_RAIL_MAX`, `pillNavStyles`, `darkModeStyles`, `collapsedRailStyles`.
364
-
365
- ## 1.7.0
366
-
367
- Two additive navigation capabilities on the horizontal bar, plus a unifying component.
368
-
369
- **Priority+ overflow ("…" menu) on `NavBar`.** Above the `collapseBelow` breakpoint the
370
- inline links sit on ONE non-wrapping row; when they don't all fit, a measure pass
371
- (`useNavOverflow` + the pure `computeVisibleCount`) keeps the leading items that fit inline
372
- and collapses the rest behind a **"More ▾"** trigger — the shared `@dloizides/ui-layout`
373
- `ModalDropdown`, so the overflow menu is portalled, keyboard-navigable, and dismissed on
374
- outside-click / Escape like every other menu. No item is ever cut off; the surplus simply
375
- moves into the dropdown. When the active route lives in the overflow set the trigger wears
376
- the accent pill so it stays visibly reachable. BELOW `collapseBelow` the whole set still
377
- collapses into the responsive hamburger drawer (the two mechanisms never overlap). Two new
378
- optional props localize the trigger: `overflowLabel` (visible + accessible, default `"More"`)
379
- and `overflowHint`. New `NAV_TEST_IDS.navBarOverflow` (`navbar-overflow`).
380
-
381
- **`Nav` — one component, two orientations.** `<Nav orientation="horizontal" />` renders the
382
- `NavBar`; `<Nav orientation="vertical" />` renders the `Sidebar` — from the SAME `NavItem[]`,
383
- so an app flips between a top bar and a side rail without swapping components. `orientation`
384
- defaults to `"horizontal"`; the remaining props are a discriminated union of the delegate's
385
- props (TypeScript enforces the right prop set per orientation). Vertical nav is the existing
386
- `Sidebar` (stacked, vertically scrolling — no overflow menu needed).
387
-
388
- **Backwards compatible.** Purely additive — `Sidebar`, `Topbar`, `NavBar`, `AppShell`, and
389
- the role-gating helpers keep their exact signatures; the new overflow props are optional
390
- (default `"More"` / `""`) so aml-v2 (the only `NavBar` consumer) and every `Sidebar`/`Topbar`
391
- consumer (erevna / katalogos / kefi / agora) are untouched. New exports (`Nav`, `NavProps`,
392
- `NavOrientation`, `NavOverflowMenu`, `NavOverflowMenuProps`, `NAV_LINK_GAP`) are additive.
393
-
394
- ## 1.6.1
395
-
396
- Keep the expanded links on ONE row — the row must be `nowrap` so `flex-shrink` distributes
397
- negative space (a `wrap` row drops the right slot onto a 2nd line instead of shrinking the
398
- links). Renames the internal expanded links style to `linksRow` (grow + shrink-to-0,
399
- `minWidth:0`, `overflow:hidden`) with a per-link `linkCell` (never shrinks, so each link keeps
400
- its natural measured width). Brand + right stay pinned (`flexShrink:0`).
401
-
402
- ## 1.6.0
403
-
404
- Keep the bar to ONE row — horizontal-scroll / clip the links instead of wrapping the bar
405
- onto a second line when brand + links + right slot overflow the available width.
406
-
407
- ## 1.5.0
408
-
409
- Make the `AppShell` back-office `sidebar` layout **responsive** — a mobile drawer.
410
-
411
- The `sidebar` slot (1.4.0) always rendered a persistent fixed-width left rail in a
412
- `flexDirection:'row'` body with no width breakpoint, so below ~768px the content column
413
- starved (headings wrapped a character per line, inputs truncated) — the merchant admin was
414
- unusable on a phone. Now `AppShell` reads `useWindowDimensions`:
415
-
416
- - **Desktop (≥768px): IDENTICAL to 1.4.0** — the persistent rail + row layout, untouched.
417
- Purely additive; no consuming product's desktop layout changes.
418
- - **Below 768px:** the rail is not rendered beside the content; instead a **hamburger**
419
- (in the header) opens the same `Sidebar` as an overlay **drawer** (a tap-to-close scrim +
420
- a left panel) and the content takes the full width. The drawer closes on a scrim tap and
421
- on a nav-item press (web: a bubbled click on a `role="button"` / link inside the panel).
422
-
423
- Accessibility: the hamburger has a testID + `accessibilityLabel` + `accessibilityHint` and a
424
- themed focus ring; the drawer panel is `role="dialog"` `aria-modal`; the scrim is a labelled
425
- button. New optional prop `mobileMenu?: Partial<DrawerLabels>` supplies pre-localized
426
- open/close labels (English defaults otherwise — this package has no i18n). New testID
427
- suffixes: `-menu-toggle`, `-drawer`, `-drawer-scrim`.
428
-
429
- **Backwards compatible.** No breaking prop changes — consumers pass the same `sidebar` slot.
430
- Omit `sidebar` and the tree is byte-identical to 1.4.0 (erevna / katalogos / kefi untouched).
431
- Only a `sidebar` consumer on a narrow viewport sees the drawer. Pinned by tests asserting the
432
- rail is present on desktop and absent (hamburger present) on a narrow viewport.
433
-
434
- ## 1.4.0
435
-
436
- Add an optional **`sidebar`** slot to `AppShell` — the back-office layout.
437
-
438
- `AppShell` could only stack vertically (header → nav strip → banner → scrolling content),
439
- so a product with a persistent LEFT RAIL (Agora's merchant admin: products / categories /
440
- coupons / settings) had to hand-compose `Topbar` + `Sidebar` + a `ScrollView` row and give
441
- up the shell's auth gate and loading/error/forbidden state cards. Supplying `sidebar` now
442
- lays the body out as a ROW — the rail on the left, the width-disciplined scrolling content
443
- column on the right — while every existing feature (gate, state cards, `width`,
444
- `chromeAlignment`) keeps working.
445
-
446
- The new region is exposed as `` `${testID}${APP_SHELL_SUFFIX.sidebar}` `` (`-sidebar`).
447
-
448
- **Backwards compatible.** Omit `sidebar` and the render tree is byte-identical to 1.3.0 —
449
- erevna / katalogos / kefi / aml-v2 are untouched. Pinned by a test asserting no `-sidebar`
450
- region exists when the slot is omitted.
451
-
452
- ## 1.3.0
453
-
454
- Give the `NavBar` the v1 `.gn-links` look + a full accessibility pass (and extend the
455
- same a11y bar to `Sidebar`/`Topbar`).
456
-
457
- **Look & hover (NavBar).** Each link is a rounded-pill target with the v1 hover
458
- affordance — muted at rest, and on HOVER the text brightens to full contrast while a
459
- subtle rounded-pill background appears; the ACTIVE link is a solid accent pill. All
460
- theme-driven (rest = `textSecondary`, hover text = `text`, hover bg = `surfaceElevated`,
461
- active/ring = `palette.primary`). Hover/focus are web-only (`onHoverIn/Out`, `onFocus/Blur`
462
- via `Pressable`); native stays at the rest style. The hover cross-fade is cheap CSS and is
463
- disabled under `prefers-reduced-motion: reduce` (new `useReducedMotion`). Backwards
464
- compatible — the hover pill is the default (aml-v2 is the only `NavBar` consumer) and the
465
- brand/right slots, responsive hamburger collapse, and `containerStyle` width behaviour are
466
- unchanged.
467
-
468
- **Accessibility.** NavBar toggle now carries `aria-controls` pointing at the collapsible
469
- links region (id via `nativeID`), keeps `aria-expanded`, and gains a themed keyboard focus
470
- ring; links keep `aria-current="page"` + `role="link"` and gain a focus ring; link/toggle
471
- touch targets are now ≥44×44. `Sidebar` leaves gain `aria-current="page"` + a focus ring;
472
- `Topbar` action buttons gain a focus ring (new internal `FocusableTouchable`). Focus rings
473
- never remove the outline without a themed replacement.
474
-
475
- ## 1.2.0
476
-
477
- Add `NavBar` — a config-driven **horizontal top navigation bar**, the top-bar
478
- counterpart to `Sidebar`. It renders the same caller-supplied `NavItem[]` as inline
479
- links across the top (brand left slot + inline links + free-form right slot), with
480
- an active-link highlight (`aria-current="page"`) and a responsive collapse to a
481
- hamburger toggle below a configurable breakpoint (`collapseBelow`, default `760`) —
482
- mirroring the v1 AML console `.global-nav`. Theme-driven (UiProvider colours),
483
- `role="navigation"` landmark, keyboard-operable links. Purely additive: `Sidebar`,
484
- `Topbar`, `AppShell`, and the role-gating helpers are unchanged. New `NAV_TEST_IDS`
485
- entries `navBarToggle` / `navBarLinks`.
486
-
487
- ## 1.0.0
488
-
489
- Initial release. `Sidebar` (leaf + expandable items, active-route highlight, header/footer
490
- slots), `Topbar` (logo/language/notification/user/account/logout slots), and the
491
- `accessibleNavItems` / `roleRoutesToNavItems` role-gating helpers (reusing
492
- `resolveAccessibleRoutes` from `@dloizides/auth-web`). Chrome + metrics ported verbatim from
493
- the byte-identical erevna-web / katalogos-web nav twins; every colour reads from the
494
- `@dloizides/ui-feedback` UiProvider theme.
1
+ # Changelog
2
+
3
+ ## 1.23.1
4
+
5
+ **Overlay z-indexes come from the shared `@dloizides/design-tokens` `Z_INDEX` scale (UI-ZINDEX-SCALE-1 "Shared overlay z-index scale").**
6
+
7
+ - `MobileDrawer` overlay uses `Z_INDEX.drawer`; `CommandPalette` overlay uses `Z_INDEX.palette`.
8
+ - Values are unchanged (50 / 60), so stacking behaviour is identical. The drawer's scrim/panel
9
+ order (1 / 2) is local to the overlay and stays as is.
10
+ - New dependency: `@dloizides/design-tokens` `^1.13.0`.
11
+
12
+ ## 1.23.0
13
+
14
+ **The NavBar's stacked mobile menu now scrolls within the viewport (SHOWCASE-1).**
15
+
16
+ Below `collapseBelow` the open hamburger menu grew to its natural height (734px with ~15 items on
17
+ ui.dloizides.com at 360x640). Portals set `body { overflow: hidden }`, so the last items sat below
18
+ the fold and neither wheel nor touch could reach them — in every portal with more than ~10 items.
19
+
20
+ - The stacked links region is now a `ScrollView` capped to `windowHeight - closedBarHeight -
21
+ stackedRightSlot - chrome` (pure `computeStackedMenuMaxHeight`, unit-tested; floor of three 44px
22
+ rows). Rows keep their 44px minimum; nothing depends on hover.
23
+
24
+ ## 1.20.0
25
+
26
+ **The auth-pending and page-loading spinners now OVERLAY stably-mounted children instead of
27
+ replacing them — closing a latent deep-link-collapse trap in the shared shell.**
28
+
29
+ `AppShell`'s `gate.pending` branch and `appShellContent`'s `state.loading` branch each `return`ed a
30
+ full-screen spinner *instead of* `children`. Replacing the subtree unmounts whatever `children` host
31
+ — in a portal that is a navigator/`<Slot/>` — and on remount React Navigation re-derives its default
32
+ route, so a deep-linked sub-route silently collapses to its default on every reload. (This is the
33
+ exact class of bug that hit kefi-web's organizer portal, fixed there in its `OnboardingGate`; the
34
+ same shape lived dormant here because no consumer passes `gate=`/`state=` yet.)
35
+
36
+ - **`AppShell` (`gate.pending`)** now renders the shell tree normally and paints the pending spinner
37
+ as an opaque `position:absolute` overlay on top, so `children` mount once and are never torn down
38
+ across `pending → authenticated` (proven by a "same child instance, no remount" test).
39
+ - **`appShellContent` (`state.loading`)** likewise overlays the loading spinner over mounted
40
+ `children` rather than swapping them out. The `loadingHost` wrapper is gated on `state` being
41
+ DEFINED, not on `loading`'s live value — so the element type at the mount point is invariant across
42
+ a `loading` true↔false toggle and `children` are reconciled in place, never remounted on the edge
43
+ (also proven by a "same child instance, no remount" test). Consumers that pass no `state` render
44
+ `children` bare, byte-identical to 1.19.0. Error / forbidden cards are TERMINAL and still replace;
45
+ unauthenticated still returns null + `onRedirect`.
46
+ - **Rule encoded in the docblock:** a *transitional* status overlays; only a *terminal* state
47
+ replaces. Corollary for consumers — `children` mount beneath the pending overlay, so gate data
48
+ FETCHES on auth state (`enabled: isAuthenticated`), not on mount. No public API changed; behavior
49
+ is unchanged for any consumer not passing `gate`/`state`.
50
+
51
+ ## 1.19.0
52
+
53
+ **The "More ▾" overflow menu now opens at a readable width instead of cramping onto the trigger's tiny footprint.**
54
+
55
+ `NavOverflowMenu` rendered the shared `@dloizides/ui-layout` `ModalDropdown` without a `menuMinWidth`,
56
+ so the popover matched the width of the compact "More ▾" pill (~its own label). Option labels
57
+ ("Settings", "API docs") wrapped onto two lines, and the resulting tall rows overflowed the menu's
58
+ max-height — so the lower items scrolled out of view and read as missing.
59
+
60
+ - **`NavOverflowMenu`** now passes a 200px `menuMinWidth` floor, so the overflow menu opens wide
61
+ enough to show each label on one line. `ModalDropdown`/`InlineMenu` clamp a floored menu inside the
62
+ viewport (ui-layout ≥ 1.24.0, now the peer floor), so the extra width never pushes it off the right
63
+ edge. No public API changed.
64
+
65
+ ## 1.18.0
66
+
67
+ Root-cause fix for sidebar leaf co-activation. Behavioral/additive — no breaking API.
68
+
69
+ - **Fix (two sidebar leaves showed `aria-current="page"` at once).** On the live finreg
70
+ `/accounting/journals`, BOTH "Chart of Accounts" (route `/accounting`) and "Journals"
71
+ (route `/accounting/journals`) were marked active, because the active predicate was purely
72
+ prefix-based (`pathname === route || pathname.startsWith(route + '/')`) — a module-home leaf
73
+ routed at `/accounting` is a prefix of every `/accounting/*` sibling, so it co-activated on all
74
+ of them. (Second occurrence of this class; the first, CRM Contacts at `/crm`, was worked around
75
+ by moving the route.) Now resolution is **most-specific (longest) match wins**: among a nav
76
+ model's LEAF items, at most ONE is active for any path — the leaf whose route is the longest
77
+ prefix match. On `/accounting/journals` only Journals is active; on `/accounting` exactly only
78
+ Chart of Accounts is. For nav models with no parent/child route overlap (the common case)
79
+ behaviour is unchanged.
80
+ - **New export** `resolveActiveRoute(items, pathname)`: flattens the tree to its leaf routes and
81
+ returns the single longest matching route (honouring per-leaf `exact`), or `undefined`.
82
+ - `Sidebar` resolves the winning route once over the full leaf set and passes `activeRoute` to
83
+ each row; `NavExpandableItem` marks a leaf active iff `item.route === activeRoute` (falls back
84
+ to the per-item predicate when used standalone — backward compatible). Group HEADER expansion
85
+ (`hasActiveDescendant`) is a separate concern and is unchanged.
86
+ - The same winner-based resolution now backs `NavBar`, `PillNav`, `CollapsedRail` and
87
+ `NavOverflowMenu`, so the latent prefix co-activation cannot resurface in the horizontal navs.
88
+ - **Additive API**: new optional `NavItem.exact?: boolean` — a leaf active ONLY on its exact
89
+ path (never a nested one). A complementary escape hatch; the longest-match default is the real
90
+ fix, so no consumer needs to set it.
91
+ - `isRouteActive(pathname, route, exact?)` gains an optional third arg; the two-arg call is
92
+ unchanged.
93
+
94
+ ## 1.17.0
95
+
96
+ Two live-defect fixes to the shared sidebar chrome. Behavioral/additive — no breaking API.
97
+
98
+ - **Fix (two search bars on desktop → mutually exclusive by affordance).** 1.16.1 shipped the
99
+ desktop inline `SidebarSearch` field, but the ⌘K `CommandPaletteTrigger` could be present at
100
+ the same time, so a wide viewport showed BOTH. `Sidebar` now owns both affordances and renders
101
+ exactly ONE, chosen by its own viewport width via `resolveSearchAffordance`:
102
+ - inline affordance (≥768) → the inline field ONLY; the ⌘K trigger is suppressed.
103
+ - palette affordance (<768) → the ⌘K trigger ONLY; the inline field is suppressed.
104
+ - The ⌘K keyboard shortcut (`useCommandPaletteHotkey`) is untouched and stays bound at every width.
105
+ - **Additive API**: new `Sidebar.paletteTrigger?: SidebarPaletteTriggerConfig`
106
+ (`{ label, hint, shortcut, onOpen, testID? }`), forwarded from `NavShellSideRail.paletteTrigger`.
107
+ Consumers that wire neither, or only the inline field, are unchanged (the inline field simply
108
+ no longer renders below 768, where a persistent field would steal vertical space).
109
+
110
+ - **Fix (hover wash never appeared on RN-web).** The nav rows were `TouchableOpacity`, which in
111
+ react-native-web has NO hover support and silently drops `onHoverIn`/`onHoverOut` — so the row's
112
+ `hovered` state never toggled and the primary-tinted overlay stayed at opacity 0. The leaf and
113
+ group-header rows in `NavExpandableItem` are now `Pressable`, whose `useHover` responder actually
114
+ drives `hovered`, lifting the token-tinted overlay to the hover wash (`HOVER_TINT_OPACITY`, ~160ms
115
+ reduced-motion-gated transition) — distinct from and lighter than the active tint. Token-only
116
+ (colours via the theme; no raw hex/rgb/hsl). aria wiring (`aria-current` / `aria-expanded`, which
117
+ `MobileDrawer` depends on) is preserved across the switch.
118
+
119
+ ## 1.16.1
120
+
121
+ - **Fix (NavShell forwards inline-search props to the inner `Sidebar`).** 1.16.0 added
122
+ desktop inline search to `Sidebar` (`enableInlineSearch`, `search`, controlled
123
+ `searchQuery` / `onSearchChange`), but all seven portals consume the higher-level
124
+ `NavShell` (via `sideRail`), which did **not** forward those props — so inline search was
125
+ unreachable by every real consumer. `NavShellSideRail` now carries the same four fields
126
+ (`enableInlineSearch?`, `search?: SidebarSearchConfig`, `searchQuery?`, `onSearchChange?`)
127
+ and threads them onto the `<Sidebar>` it builds. Desktop-vs-mobile resolution is unchanged
128
+ (inline ≥768, ⌘K palette <768 via `resolveSearchAffordance`).
129
+ - **Purely additive / backward compatible**: with the fields unset, `NavShell` renders
130
+ exactly as 1.16.0. No breaking change; no styling touched.
131
+
132
+ ## 1.16.0
133
+
134
+ - **Feature (docs-style sidebar rail — hover wash, tinted active pill, rotating chevron).**
135
+ The `Sidebar` / `NavExpandableItem` rows now read like the aml-screening docs navigation:
136
+ each row carries a PRIMARY-TINTED overlay whose opacity eases `0 → hover → active` as a
137
+ smooth ~160ms CSS cross-fade on web (gated by `prefers-reduced-motion`, omitted on native).
138
+ - **Hover** lights a row with a subtle brand wash (`HOVER_TINT_OPACITY` = 0.06); **active**
139
+ deepens it to a soft brand pill (`ACTIVE_TINT_OPACITY` = 0.14) over the transparent
140
+ left-accent gutter every leaf already reserved, so the active leaf shows a tinted pill + a
141
+ coloured left bar + brand-coloured label — no layout shift, all from `theme.palette.primary`.
142
+ - **Rotating chevron.** Expandable groups now get a built-in `SidebarChevron` (a `›` glyph
143
+ that rotates to `⌄` on expand, tweened on web) when the caller supplies no `renderChevron`.
144
+ A caller-supplied `renderChevron` still wins, so existing icon-set chevrons are unaffected.
145
+ - Row corner radius softened to `NAV_ROW_RADIUS` (8px) with the rounded overlay clipped to it.
146
+ - No public API change to existing props; hover uses RNW's `onHoverIn`/`onHoverOut` (no-op on
147
+ native). New exports: `SidebarChevron`, `NAV_ROW_RADIUS`, `ACTIVE_TINT_OPACITY`,
148
+ `HOVER_TINT_OPACITY`.
149
+
150
+ - **Feature (desktop INLINE sidebar search — opt-in — with the ⌘K palette kept for mobile).**
151
+ The owner found the ⌘K command-palette popup inconvenient on desktop. `Sidebar` now accepts
152
+ an opt-in persistent inline search field at the top of the rail that narrows the nav list
153
+ LIVE as you type.
154
+ - **New `Sidebar` props (all additive, default OFF):** `enableInlineSearch` + a pre-localized
155
+ `search: SidebarSearchConfig` (`placeholder` / `hint` / `clearLabel` / `clearHint` /
156
+ `emptyText`), plus optional controlled `searchQuery` + `onSearchChange` (the sidebar owns the
157
+ state when omitted). With the flag off the render tree is byte-identical to 1.15 — the mobile
158
+ drawer, which should keep using the ⌘K palette, is untouched.
159
+ - **New exports:** `SidebarSearch` (the field), `filterNavItems` / `isFilterActive` /
160
+ `searchTokens` (the pure tree-prune, same all-tokens-must-match semantics as `filterCommands`
161
+ so inline + ⌘K rank identically), and `resolveSearchAffordance(viewport)` +
162
+ `DEFAULT_SEARCH_BREAKPOINT` — the pure `'inline'` (≥768px) vs `'palette'` (<768px) rule so a
163
+ shell can wire desktop-inline / mobile-palette from one function. The ⌘K hotkey
164
+ (`useCommandPaletteHotkey`) stays bound at every width.
165
+
166
+ ## 1.15.0
167
+
168
+ - **Feature (an expandable nav group now ANIMATES its expand/collapse instead of snapping).**
169
+ `NavExpandableItem` used to conditionally render its children (`{expanded ? … : null}`),
170
+ so a sidebar/drawer group's leaves appeared and vanished INSTANTLY on toggle — a hard,
171
+ jarring cut with no transition. The group's children are now wrapped in the
172
+ `@dloizides/ui-motion` `<Collapse open={expanded}>`, which measures the children's natural
173
+ height and animates a real `Animated.Value` height on WEB and native alike (the height code
174
+ path RN's native-only `LayoutAnimation` never gave the web build), collapsing to instant
175
+ under `prefers-reduced-motion`.
176
+ - **Collapsed leaves stay non-interactive.** Per the `Collapse` contract the children remain
177
+ MOUNTED at all times (their state and a11y focus order are preserved); when collapsed they
178
+ are clipped to zero height under `overflow: hidden`, so a collapsed group's leaves are
179
+ neither visible nor tappable in a real browser even though they are in the tree. This is a
180
+ deliberate change from the prior "absent from the DOM when collapsed" behaviour.
181
+ - **`aria-expanded` and the mobile-drawer contract are intact.** The group header still emits
182
+ `aria-expanded` (the `ariaExpandedProps` spread is unchanged), so `MobileDrawer` still tells
183
+ a group TOGGLE apart from a navigating LEAF and does not dismiss the drawer when a group is
184
+ expanded on mobile. The active-leaf accent bar, docs-style section-header styling, chevron
185
+ slot, testIDs, and all a11y roles/labels/hints are unchanged.
186
+ - **No public API change.** `NavExpandableItemProps` / `NavItem` are untouched — the animation
187
+ is derived from the `expanded` state the component already tracked. `@dloizides/ui-motion`
188
+ (`^1.0.1`) is added as a runtime dependency; all 7 portals gain the animated group on their
189
+ next bump for free. Consumers with no expandable groups are visually unaffected.
190
+
191
+ ## 1.13.1
192
+
193
+ - **Fix (mobile drawer: a sub-item / leaf tap dismissed the drawer WITHOUT navigating).**
194
+ On a phone-width viewport, tapping a leaf inside the open `MobileDrawer` closed the drawer
195
+ but the route never changed — verified live on finreg via Playwright (real Chromium, real
196
+ pointer events); every jsdom unit test stayed green because it wired `onClose` to an inert
197
+ `jest.fn()` that never unmounts anything.
198
+ - **Root cause.** `MobileDrawer` closes on a nav press via a NATIVE
199
+ `panel.addEventListener('click', …)`. react-native-web fires a leaf's `onPress` from its
200
+ `onClick`, dispatched through REACT's synthetic event system whose root listener sits on the
201
+ app-root container — an ANCESTOR of the panel. So on a real click the native panel listener
202
+ runs FIRST (during the DOM bubble), and closing synchronously there ran `setDrawerOpen(false)`,
203
+ unmounting the panel and tearing the leaf out of the tree BEFORE React could dispatch its
204
+ `onPress`. The navigation call never ran.
205
+ - **Fix.** The drawer's close is now DEFERRED to the next macrotask (`setTimeout(…, 0)`), so
206
+ React finishes dispatching the tapped leaf's `onPress` (the navigation) for the current click;
207
+ the panel then unmounts on the following tick, after the route has already changed. Scrim tap
208
+ and group-header toggle are unchanged.
209
+ - **No API change.** All 7 portals get the fix on their next bump.
210
+
211
+ ## 1.11.0
212
+
213
+ - **Feature (an expandable nav group now auto-opens when the current route is inside it).**
214
+ `NavExpandableItem` used to mount every group collapsed (`useState(false)`), so on a fresh
215
+ page load the group containing the active route was closed and its active leaf was hidden.
216
+ A persistent-rail console that groups its nav by module (e.g. finreg-web's Payments / CRM /
217
+ Accounting rail) needs the active module's group open on load, the others collapsed — with
218
+ no way for the caller to express that, since expansion state was internal and uncontrolled.
219
+ - The group now initializes its expansion from whether the current route is inside it:
220
+ `useState(() => hasActiveDescendant(item, pathname))`, where `hasActiveDescendant` is true
221
+ when the item's own route is active (`isRouteActive`) or any descendant's is (recursive, so
222
+ it holds at any nesting depth). The group of the active route mounts EXPANDED (active leaf
223
+ visible + highlighted); a group with no active descendant mounts COLLAPSED — exactly the
224
+ prior behaviour.
225
+ - **No API change.** `NavItem` is unchanged — the behaviour is derived from the `pathname`
226
+ the component already receives. Consumers with no expandable groups, or whose groups hold no
227
+ active route, are byte-for-byte unaffected; all 7 portals gain the auto-open on their next
228
+ bump for free.
229
+ - Guarded by `src/navExpandableItem.test.tsx` (RED-first): the two "mounts EXPANDED" assertions
230
+ FAIL against the pre-change `useState(false)` (the active group's child is absent from the DOM)
231
+ and pass after; the "mounts COLLAPSED" + "header always renders" assertions lock the
232
+ backward-compatible half.
233
+
234
+ ## 1.10.2
235
+
236
+ - **Fix (the mobile nav DRAWER was not clickable — a tap on a nav item did not navigate, it
237
+ just closed the drawer / did nothing, in all seven portals below 768px).**
238
+ `MobileDrawer` rendered a full-bleed scrim `Pressable` (`StyleSheet.absoluteFillObject`) and a
239
+ panel `View` with `{ position: 'absolute', top: 0, bottom: 0, left: 0 }` — **no width and no
240
+ zIndex**. Reproduced structurally: the panel's computed `width` was empty and its `zIndex` was
241
+ `0`; the scrim's computed `left`/`right` were both `0px` at `zIndex` `0`. So the scrim FULLY
242
+ overlapped the panel at the SAME stacking level, and on react-native-web the equal-zIndex scrim
243
+ won the pointer (the estate's known "RN-web Pressable swallows a sibling/child press" trap):
244
+ the tap closed the drawer and the nav-item's `TouchableOpacity` `onPress` (→ the app's router)
245
+ never fired.
246
+ - Fixed once, in the kit: the panel now has an explicit drawer **width** (`280px`), a solid
247
+ surface background, and a `zIndex` **above** the scrim; the scrim is anchored BESIDE the panel
248
+ (`left: DRAWER_WIDTH`) so its tap-to-close hit area never overlaps the panel. Nav-item taps
249
+ reach the `Sidebar` leaves; taps beside the drawer still close it.
250
+ - Guarded by `src/mobileDrawer.test.tsx` (RED-first): three structural assertions read the
251
+ resolved geometry via `getComputedStyle` and FAIL against the pre-fix panel/scrim (no width,
252
+ equal zIndex, overlapping scrim); two behavioural assertions lock the contract (nav-item tap
253
+ navigates + closes; scrim tap only closes).
254
+ - **No API change.** Consumers need no edit — bump the dependency and the drawer works.
255
+
256
+ ## 1.10.1
257
+
258
+ - **Fix (a plain-string slot logged `console.error` on EVERY render, in all seven portals).**
259
+ Every caller-supplied slot in the kit is typed `React.ReactNode`, which legitimately includes
260
+ `string` — and callers pass exactly that (`brand: FM('app.name')`, a plain localized string).
261
+ The slot was dropped straight into a `<View>`, and react-native-web refuses to render a bare
262
+ text node there: `Unexpected text node: <s>. A text node cannot be a child of a <View>.`
263
+ Nothing in the type system objected, so the defect only ever surfaced as browser console
264
+ noise — which is why it survived to 1.10.0 across the whole fleet.
265
+ - Fixed once, in the kit, via the new internal `renderTextSlot` helper: a `string`/`number`
266
+ slot is wrapped in `<Text>` (routed through the theme's `colors.text`, so it honours dark
267
+ mode rather than defaulting to black); an element slot passes through **untouched**, so
268
+ every existing caller renders byte-identically. Array slots have each string member wrapped
269
+ individually, keys preserved.
270
+ - Applied to **all 12 slot render sites**, not just the reported one — `NavBar` (`brand`,
271
+ `right`), `Topbar` (`left`, `notificationSlot`), `Sidebar` (`header`, `footer`),
272
+ `CollapsedRail` (`header`, `footer`), `AppShell` (`header`, `nav`, `sidebar`,
273
+ `collapsedSidebar`, `banner`) and `MobileDrawer` (`sidebar`).
274
+ - Guarded by `src/rawTextSlots.test.tsx`, which spies on `console.error` and asserts silence.
275
+ Unit tests that only assert "the slot rendered" pass against the bug; this one does not.
276
+ - **No API change.** Consumers need no edit — the `<Text>` wrapper they were expected to
277
+ remember is now the kit's job.
278
+
279
+ ## 1.10.0
280
+
281
+ - **Fix (`layout` was not authoritative — `layout="side"` rendered a top bar anyway).**
282
+ `wantsTopBar` carried an extra `(layout === 'side' && hasTopBar)` clause, so passing a
283
+ `topBar` **config** with `layout="side"` mounted the bar regardless — making `'side'` and
284
+ `'both'` render **pixel-identically** and `layout` unobservable. `layout` now decides which
285
+ REGIONS exist; `topBar` is only the CONTENT of the top region. A `'side'` app that also wants
286
+ a bar asks for `layout="both"`, which is what that combination always meant.
287
+ - Found by `apps/ui-showcase`, which renders every orientation side by side — a product app
288
+ picks ONE orientation and never sees the other, which is why this survived to 1.9.0.
289
+ - **Not breaking in this fleet:** no app passed `layout="side"` + `topBar`. The four `'side'`
290
+ portals (erevna, katalogos, zygos, agora) pass a `header` NODE, not a `topBar` config.
291
+ - The `hasHeader` clause is **deliberately kept**: `header` is the documented escape hatch by
292
+ which a `'side'` app supplies its own ready-made header (the structured `Topbar` with
293
+ language / notification / user slots). It is an explicit "render THIS node", not a config
294
+ whose presence is silently read as an orientation change.
295
+ - **a11y: `AccountState.accountHint`** — the account-name button's `accessibilityHint` was
296
+ hardcoded to `displayName`, byte-identical to its `accessibilityLabel`, so a screen reader
297
+ announced the name twice and never said what pressing it does — and the consumer had no way
298
+ to correct it. The sibling `upgrade?: TopbarAction` on the same interface has always carried
299
+ a `hint`; this closes that inconsistency. Falls back to `displayName`, so existing callers
300
+ are unaffected.
301
+ - **a11y: `AccountState.accountTestID`** — the account-name button's testID was hardcoded, with
302
+ no override, unlike `TopbarAction.testID?` / `NavItem.testID?` elsewhere in the package.
303
+ - **a11y: `Topbar.language.testID`** — the inline `language` prop type declared no `testID`, so
304
+ the repo's testID+label+hint standard was **structurally unsatisfiable** for that one button
305
+ while every other Topbar action (all `TopbarAction`) accepted one.
306
+
307
+ > Note for consumers: this react-native-web build does **not** forward `accessibilityHint` to
308
+ > the DOM at all — rendered buttons carry `aria-label` but no hint attribute. The hint is a
309
+ > NATIVE-only guarantee today. That is pre-existing and unchanged here, but it means DOM-level
310
+ > tests cannot distinguish a threaded hint from a dropped one; `topbarA11y.test.tsx` therefore
311
+ > pins the prop threading directly.
312
+
313
+ ## 1.9.0
314
+
315
+ **`NavBar.contentMaxWidth` — a full-bleed bar with a centred content column.**
316
+ An optional content-column cap (px), forwarded via `NavShell.topBar.contentMaxWidth`.
317
+ When set, the OUTER `NavBar` container — which carries the bar background + bottom
318
+ border (from `barTheme` / theme surface + any `containerStyle`, e.g. the AML
319
+ radial-glow appbar) — stays FULL WIDTH (full-bleed), while the INNER row (brand +
320
+ links + right) is capped to `contentMaxWidth` and centred. This restores the v1-console
321
+ `.ui-appbar` (full-bleed) + `.ui-appbar-inner { max-width; margin: 0 auto }` shape:
322
+ previously an app had to put the cap on the container's `containerStyle`, which also
323
+ capped the dark background and left light gutters on wide viewports. Omit it and the
324
+ inner spans the full container (unchanged — byte-identical to before). The priority+
325
+ overflow "…" fit measures the capped inner's rendered width, so it keeps working
326
+ against the content column, not the full-bleed container. Purely additive.
327
+
328
+ ## 1.8.0
329
+
330
+ The navigation shell becomes the ONE shell for every portal: a first-class
331
+ **top | side | both** layout, plus the capabilities harvested from every RN-web
332
+ portal's bespoke nav/shell so nothing is lost when they migrate. Purely additive —
333
+ `Sidebar`, `Topbar`, `NavBar`, `Nav`, `AppShell`, and the role helpers keep their
334
+ exact signatures; every new prop/component is optional.
335
+
336
+ **`NavShell` — top bar, side rail, or BOTH.** One component owns the whole page
337
+ composition, chosen by a single `layout` prop (`'top' | 'side' | 'both'`). It wires
338
+ a top bar into `AppShell`'s `header` slot and a side rail into its `sidebar` slot,
339
+ and inherits every `AppShell` capability (auth gate, loading/error/forbidden state
340
+ cards, width policy, banner, the responsive hamburger + overlay drawer). `'both'`
341
+ is the AML-console shape — a top bar (brand / global links + overflow "…" / user)
342
+ AND a side rail (sections) together — now first-class instead of hand-wired. The
343
+ top bar is built from a `topBar` config (a `NavBar`), the side rail from a
344
+ `sideRail` config (a `Sidebar`); an explicit `header` node overrides the built top
345
+ bar for apps that want a structured `Topbar`.
346
+
347
+ **Three-tier responsive rail (`AppShell.collapsedSidebar`).** Opts the `sidebar`
348
+ layout into the erevna/katalogos scheme: the full rail on desktop, an intermediate
349
+ persistent **collapsed (icon-only) rail** on a tablet band, and the overlay drawer
350
+ on a phone. Supply a `collapsedSidebar` node (and optionally `collapsedRailRange`,
351
+ default `{ min: 768, max: 1024 }`) — omit it and the layout stays the unchanged
352
+ two-tier scheme. New `CollapsedRail` component renders an icon-only rail from the
353
+ same `NavItem[]` (icon or first-letter glyph fallback, header/footer slots). New
354
+ `resolveRailMode` pure resolver + `RAIL_FULL_BREAKPOINT` / `DEFAULT_COLLAPSED_RAIL_MAX`.
355
+ `NavShell.sideRail.collapsed` builds it config-driven. New testID suffix
356
+ `-collapsed-sidebar`.
357
+
358
+ **`NavBar.barTheme` — dark bar over a light page.** An optional chrome-theme
359
+ override re-provisions the `@dloizides/ui-feedback` theme for the bar only (the AML
360
+ v1-console dark appbar over light content), re-passing the live `t` / `navigate` so
361
+ labels keep localizing. Forwarded via `NavShell.topBar.barTheme`. Style-only —
362
+ colours still flow through the theme. (Custom appbar backgrounds like AML's radial
363
+ glow stay expressible via `containerStyle`.)
364
+
365
+ **`PillNav` — coral cross-navigation (kefi).** A row of fully-rounded pills from a
366
+ `NavItem[]`, the active one wearing the accent (theme `palette.primary` = kefi's
367
+ coral). Folds in kefi's **visibility rule**: hides (renders `null`) unless there
368
+ are at least `minItems` entries (default 1; kefi passes 2).
369
+
370
+ **`DarkModeControl` — 3-way colour scheme (erevna/katalogos).** A controlled
371
+ Light / Dark / System control in two variants: `"segmented"` (a highlighted
372
+ segment row) and `"cycle"` (an icon+label button that advances to the next option
373
+ on press). Value-agnostic string options, pre-localized labels/hints.
374
+
375
+ **Refactor (no behaviour change).** `AppShell`'s page-state cards moved to
376
+ `appShellContent.tsx` (`useContentBody` / `ShellMessage` / `ShellState`) to keep
377
+ `AppShell` focused on layout; the shell's render tree is unchanged.
378
+
379
+ New exports: `NavShell` (+ `NavShellProps` / `NavShellLayout` / `NavShellTopBar` /
380
+ `NavShellSideRail` / `NavShellCollapsedRail`), `CollapsedRail` (+ `CollapsedRailProps`),
381
+ `PillNav` (+ `PillNavProps`), `DarkModeControl` (+ `DarkModeControlProps` /
382
+ `DarkModeOption` / `DarkModeVariant`), `resolveRailMode` (+ `RailMode` /
383
+ `CollapsedRailRange` / `ResolveRailModeArgs`), `RAIL_FULL_BREAKPOINT`,
384
+ `DEFAULT_COLLAPSED_RAIL_MAX`, `pillNavStyles`, `darkModeStyles`, `collapsedRailStyles`.
385
+
386
+ ## 1.7.0
387
+
388
+ Two additive navigation capabilities on the horizontal bar, plus a unifying component.
389
+
390
+ **Priority+ overflow ("…" menu) on `NavBar`.** Above the `collapseBelow` breakpoint the
391
+ inline links sit on ONE non-wrapping row; when they don't all fit, a measure pass
392
+ (`useNavOverflow` + the pure `computeVisibleCount`) keeps the leading items that fit inline
393
+ and collapses the rest behind a **"More ▾"** trigger — the shared `@dloizides/ui-layout`
394
+ `ModalDropdown`, so the overflow menu is portalled, keyboard-navigable, and dismissed on
395
+ outside-click / Escape like every other menu. No item is ever cut off; the surplus simply
396
+ moves into the dropdown. When the active route lives in the overflow set the trigger wears
397
+ the accent pill so it stays visibly reachable. BELOW `collapseBelow` the whole set still
398
+ collapses into the responsive hamburger drawer (the two mechanisms never overlap). Two new
399
+ optional props localize the trigger: `overflowLabel` (visible + accessible, default `"More"`)
400
+ and `overflowHint`. New `NAV_TEST_IDS.navBarOverflow` (`navbar-overflow`).
401
+
402
+ **`Nav` — one component, two orientations.** `<Nav orientation="horizontal" />` renders the
403
+ `NavBar`; `<Nav orientation="vertical" />` renders the `Sidebar` — from the SAME `NavItem[]`,
404
+ so an app flips between a top bar and a side rail without swapping components. `orientation`
405
+ defaults to `"horizontal"`; the remaining props are a discriminated union of the delegate's
406
+ props (TypeScript enforces the right prop set per orientation). Vertical nav is the existing
407
+ `Sidebar` (stacked, vertically scrolling — no overflow menu needed).
408
+
409
+ **Backwards compatible.** Purely additive — `Sidebar`, `Topbar`, `NavBar`, `AppShell`, and
410
+ the role-gating helpers keep their exact signatures; the new overflow props are optional
411
+ (default `"More"` / `""`) so aml-v2 (the only `NavBar` consumer) and every `Sidebar`/`Topbar`
412
+ consumer (erevna / katalogos / kefi / agora) are untouched. New exports (`Nav`, `NavProps`,
413
+ `NavOrientation`, `NavOverflowMenu`, `NavOverflowMenuProps`, `NAV_LINK_GAP`) are additive.
414
+
415
+ ## 1.6.1
416
+
417
+ Keep the expanded links on ONE row — the row must be `nowrap` so `flex-shrink` distributes
418
+ negative space (a `wrap` row drops the right slot onto a 2nd line instead of shrinking the
419
+ links). Renames the internal expanded links style to `linksRow` (grow + shrink-to-0,
420
+ `minWidth:0`, `overflow:hidden`) with a per-link `linkCell` (never shrinks, so each link keeps
421
+ its natural measured width). Brand + right stay pinned (`flexShrink:0`).
422
+
423
+ ## 1.6.0
424
+
425
+ Keep the bar to ONE row — horizontal-scroll / clip the links instead of wrapping the bar
426
+ onto a second line when brand + links + right slot overflow the available width.
427
+
428
+ ## 1.5.0
429
+
430
+ Make the `AppShell` back-office `sidebar` layout **responsive** — a mobile drawer.
431
+
432
+ The `sidebar` slot (1.4.0) always rendered a persistent fixed-width left rail in a
433
+ `flexDirection:'row'` body with no width breakpoint, so below ~768px the content column
434
+ starved (headings wrapped a character per line, inputs truncated) — the merchant admin was
435
+ unusable on a phone. Now `AppShell` reads `useWindowDimensions`:
436
+
437
+ - **Desktop (≥768px): IDENTICAL to 1.4.0** — the persistent rail + row layout, untouched.
438
+ Purely additive; no consuming product's desktop layout changes.
439
+ - **Below 768px:** the rail is not rendered beside the content; instead a **hamburger**
440
+ (in the header) opens the same `Sidebar` as an overlay **drawer** (a tap-to-close scrim +
441
+ a left panel) and the content takes the full width. The drawer closes on a scrim tap and
442
+ on a nav-item press (web: a bubbled click on a `role="button"` / link inside the panel).
443
+
444
+ Accessibility: the hamburger has a testID + `accessibilityLabel` + `accessibilityHint` and a
445
+ themed focus ring; the drawer panel is `role="dialog"` `aria-modal`; the scrim is a labelled
446
+ button. New optional prop `mobileMenu?: Partial<DrawerLabels>` supplies pre-localized
447
+ open/close labels (English defaults otherwise — this package has no i18n). New testID
448
+ suffixes: `-menu-toggle`, `-drawer`, `-drawer-scrim`.
449
+
450
+ **Backwards compatible.** No breaking prop changes — consumers pass the same `sidebar` slot.
451
+ Omit `sidebar` and the tree is byte-identical to 1.4.0 (erevna / katalogos / kefi untouched).
452
+ Only a `sidebar` consumer on a narrow viewport sees the drawer. Pinned by tests asserting the
453
+ rail is present on desktop and absent (hamburger present) on a narrow viewport.
454
+
455
+ ## 1.4.0
456
+
457
+ Add an optional **`sidebar`** slot to `AppShell` — the back-office layout.
458
+
459
+ `AppShell` could only stack vertically (header → nav strip → banner → scrolling content),
460
+ so a product with a persistent LEFT RAIL (Agora's merchant admin: products / categories /
461
+ coupons / settings) had to hand-compose `Topbar` + `Sidebar` + a `ScrollView` row and give
462
+ up the shell's auth gate and loading/error/forbidden state cards. Supplying `sidebar` now
463
+ lays the body out as a ROW — the rail on the left, the width-disciplined scrolling content
464
+ column on the right — while every existing feature (gate, state cards, `width`,
465
+ `chromeAlignment`) keeps working.
466
+
467
+ The new region is exposed as `` `${testID}${APP_SHELL_SUFFIX.sidebar}` `` (`-sidebar`).
468
+
469
+ **Backwards compatible.** Omit `sidebar` and the render tree is byte-identical to 1.3.0 —
470
+ erevna / katalogos / kefi / aml-v2 are untouched. Pinned by a test asserting no `-sidebar`
471
+ region exists when the slot is omitted.
472
+
473
+ ## 1.3.0
474
+
475
+ Give the `NavBar` the v1 `.gn-links` look + a full accessibility pass (and extend the
476
+ same a11y bar to `Sidebar`/`Topbar`).
477
+
478
+ **Look & hover (NavBar).** Each link is a rounded-pill target with the v1 hover
479
+ affordance — muted at rest, and on HOVER the text brightens to full contrast while a
480
+ subtle rounded-pill background appears; the ACTIVE link is a solid accent pill. All
481
+ theme-driven (rest = `textSecondary`, hover text = `text`, hover bg = `surfaceElevated`,
482
+ active/ring = `palette.primary`). Hover/focus are web-only (`onHoverIn/Out`, `onFocus/Blur`
483
+ via `Pressable`); native stays at the rest style. The hover cross-fade is cheap CSS and is
484
+ disabled under `prefers-reduced-motion: reduce` (new `useReducedMotion`). Backwards
485
+ compatible — the hover pill is the default (aml-v2 is the only `NavBar` consumer) and the
486
+ brand/right slots, responsive hamburger collapse, and `containerStyle` width behaviour are
487
+ unchanged.
488
+
489
+ **Accessibility.** NavBar toggle now carries `aria-controls` pointing at the collapsible
490
+ links region (id via `nativeID`), keeps `aria-expanded`, and gains a themed keyboard focus
491
+ ring; links keep `aria-current="page"` + `role="link"` and gain a focus ring; link/toggle
492
+ touch targets are now ≥44×44. `Sidebar` leaves gain `aria-current="page"` + a focus ring;
493
+ `Topbar` action buttons gain a focus ring (new internal `FocusableTouchable`). Focus rings
494
+ never remove the outline without a themed replacement.
495
+
496
+ ## 1.2.0
497
+
498
+ Add `NavBar` — a config-driven **horizontal top navigation bar**, the top-bar
499
+ counterpart to `Sidebar`. It renders the same caller-supplied `NavItem[]` as inline
500
+ links across the top (brand left slot + inline links + free-form right slot), with
501
+ an active-link highlight (`aria-current="page"`) and a responsive collapse to a
502
+ hamburger toggle below a configurable breakpoint (`collapseBelow`, default `760`) —
503
+ mirroring the v1 AML console `.global-nav`. Theme-driven (UiProvider colours),
504
+ `role="navigation"` landmark, keyboard-operable links. Purely additive: `Sidebar`,
505
+ `Topbar`, `AppShell`, and the role-gating helpers are unchanged. New `NAV_TEST_IDS`
506
+ entries `navBarToggle` / `navBarLinks`.
507
+
508
+ ## 1.0.0
509
+
510
+ Initial release. `Sidebar` (leaf + expandable items, active-route highlight, header/footer
511
+ slots), `Topbar` (logo/language/notification/user/account/logout slots), and the
512
+ `accessibleNavItems` / `roleRoutesToNavItems` role-gating helpers (reusing
513
+ `resolveAccessibleRoutes` from `@dloizides/auth-web`). Chrome + metrics ported verbatim from
514
+ the byte-identical erevna-web / katalogos-web nav twins; every colour reads from the
515
+ `@dloizides/ui-feedback` UiProvider theme.