@dloizides/ui-nav 1.6.1 → 1.8.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/ADOPTION.md ADDED
@@ -0,0 +1,83 @@
1
+ # Adopting `@dloizides/ui-nav` ≥ 1.8.0 — per-portal migration map
2
+
3
+ This is the map the migration agents follow to move each RN-web portal off its
4
+ bespoke nav/shell onto the shared package. **Only STYLE varies per app** — layout
5
+ structure and behaviour are shared. Nothing from the current shells is lost:
6
+ everything is either a component, an optional prop, or a render slot.
7
+
8
+ > Migrations are a **separate wave** — do NOT edit the apps as part of building the
9
+ > package. This doc tells each app which `layout`, which slots, and which
10
+ > components to use.
11
+
12
+ ## The one decision: which `layout`?
13
+
14
+ | Portal | Current shell | `NavShell` `layout` | Notes |
15
+ |--------|---------------|---------------------|-------|
16
+ | **AML v2** (`consoleChrome` / AuthedShell / PublicShell) | top bar only (dark, radial glow) | `"top"` | `topBar.barTheme` = dark chrome; brand/user/language in `topBar.brand` / `topBar.right`; overflow "…" built-in |
17
+ | **Agora** (`AdminShell`) | side rail + top bar (already `AppShell`) | `"side"` | `sideRail` = sections; `header` = `<Topbar logout=… />` |
18
+ | **Zygos** (`ZygosShell`) | side rail + top bar (already `AppShell`) | `"side"` | as Agora, plus `Topbar.user` block |
19
+ | **Kefi** (`DashboardShell` / `AppHeader` / `DashboardNav`) | top bar + coral pill strip | `"top"` | `header`/`topBar` = brand + account; put `<PillNav minItems={2} />` in the `nav` slot |
20
+ | **Erevna** (`ProtectedLayout` + Sidebar/Topbar) | 3-tier rail + top bar | `"side"` **+** `sideRail.collapsed` | full rail → collapsed icon rail → drawer |
21
+ | **Katalogos** | byte-identical to erevna | `"side"` **+** `sideRail.collapsed` | same as erevna |
22
+
23
+ ## Slot mapping (what fills each slot)
24
+
25
+ ### AML v2 — `layout="top"`
26
+ - `topBar.items` ← `editionNavItems(visibleNavItems(isAdmin), selfHosted)` → `NavItem[]`
27
+ (keep the app-side edition/self-host filtering; pass the result).
28
+ - `topBar.brand` ← the `ConsoleBrand` block (brand→marketing-vs-console-home routing
29
+ stays in the app; `brand` is a free-form node).
30
+ - `topBar.right` ← `<ShellLanguageSwitcher />` + `<UserChip />` (or the "Sign in" pill
31
+ for anonymous `PublicShell`). Both are already `right`-slot nodes today.
32
+ - `topBar.barTheme` ← the navy `useNavTheme()` clone (dark bar over light page).
33
+ - `topBar.containerStyle` ← the 1180px cap + the radial-glow `appbarBarStyle` (a web
34
+ `background`/`backgroundImage` gradient cast through `ViewStyle`). **Style only.**
35
+ - `topBar.overflowLabel` / `collapseBelow={760}` ← as today.
36
+ - External-route split (`isExternalRoute` → `Linking.openURL` vs `router.navigate`)
37
+ stays in the app's `onNavigate`. `ToastHost` stays app-side (it is `@dloizides/ui-feedback`).
38
+
39
+ ### Agora / Zygos — `layout="side"`
40
+ - `sideRail.items` ← the localized `NAV_SECTIONS` (`renderIcon` per item).
41
+ - `sideRail.title` / `regionLabel` / `navigateHint` ← as today.
42
+ - `header` ← `<Topbar logout={…} user={…?} />` (Zygos passes `user`; Agora does not).
43
+ - `mobileMenu` ← the drawer open/close labels (Agora passes them; Zygos gets English defaults).
44
+ - Nothing else changes — these already consume `AppShell` + `Sidebar` + `Topbar`.
45
+
46
+ ### Kefi — `layout="top"` + `nav` slot
47
+ - `header` ← `<Topbar account={{ displayName, onLogout }} left={<Wordmark/>} logout={…} />`
48
+ (the rich `AccountState`; keep omitting tenant/plan to avoid the round-trip).
49
+ - `nav` ← `<PillNav items={accessibleNavItems(user, postLoginRouteTable, FM)} minItems={2}
50
+ pathname={…} onNavigate={…} regionLabel={…} />` — this folds in the coral pills AND
51
+ the ≥2-dashboards rule (no more app-local `MIN_DASHBOARDS_FOR_NAV`).
52
+ - `gate` / `state` / `banner` / `width` / `chromeAlignment` / `contentPadding` ← exactly
53
+ the `AppShell` props kefi already passes (unchanged).
54
+ - Coral colour = theme `palette.primary`; pill radius/font = theme/`containerStyle`. **Style only.**
55
+
56
+ ### Erevna / Katalogos — `layout="side"` + `sideRail.collapsed`
57
+ - `sideRail.items` ← `toNavItems(groupSidebarItems(moduleRegistry.getSidebarItemsForRoles(roles)))`
58
+ (role-gated, grouped/expandable — unchanged pipeline).
59
+ - `sideRail.header` ← the Home shortcut; `sideRail.footer` ← `<DarkModeControl variant="segmented"
60
+ options={[Light, Dark, System]} value=… onChange={setDarkModePreference} />` + Logout
61
+ (replaces the bespoke `DarkModeToggle`). `sideRail.renderChevron` ← chevronUp/Down.
62
+ - `sideRail.collapsed` ← `{ items, header, footer, range: { min, max } }` → the tablet
63
+ **collapsed icon rail** (replaces `MobileSidebarCollapsed`). Its footer can hold the
64
+ `DarkModeControl variant="cycle"` (replaces `MobileDarkModeButton`).
65
+ - `header` ← `<Topbar left={<TenantLogo/>} language={…} notificationSlot={<SafeNotificationBell/>}
66
+ user={…} logout={…} account={…?} />`.
67
+ - `mobileMenu` ← the drawer open/close labels (the phone drawer re-hosts the full rail).
68
+ - `gate` (auth) / `banner` (`VerificationPendingBanner`) / `state` ← the `AppShell` props.
69
+ - **Still app-side (not lost, not shared):** the `!isUser` role-conditional rail hide
70
+ (just pass no `sideRail` for plain users), the `SkipNavLink` (compose in `children`),
71
+ and — if the exact right-anchored mobile *account* drawer (user+dark+lang+account) is
72
+ wanted verbatim — keep `MobileTopbar` in the `header` slot; the shared left drawer
73
+ re-hosts the rail, which covers the section-navigation case.
74
+ - **Style only:** scrim colour/opacity, slide offsets/durations, segmented-toggle
75
+ highlight, drawer panel colours → theme tokens / `containerStyle`.
76
+
77
+ ## Contract reminders (unchanged)
78
+
79
+ - No FM / router / icon-set / store imports in the package. Labels are pre-localized
80
+ strings; icons are `renderIcon` render slots; every colour flows through the
81
+ `@dloizides/ui-feedback` `UiProvider` theme.
82
+ - `Sidebar` / `Topbar` / `NavBar` / `Nav` / `AppShell` and the role helpers keep their
83
+ exact 1.7.0 signatures — migrate incrementally; a partial migration compiles.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,105 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.8.0
4
+
5
+ The navigation shell becomes the ONE shell for every portal: a first-class
6
+ **top | side | both** layout, plus the capabilities harvested from every RN-web
7
+ portal's bespoke nav/shell so nothing is lost when they migrate. Purely additive —
8
+ `Sidebar`, `Topbar`, `NavBar`, `Nav`, `AppShell`, and the role helpers keep their
9
+ exact signatures; every new prop/component is optional.
10
+
11
+ **`NavShell` — top bar, side rail, or BOTH.** One component owns the whole page
12
+ composition, chosen by a single `layout` prop (`'top' | 'side' | 'both'`). It wires
13
+ a top bar into `AppShell`'s `header` slot and a side rail into its `sidebar` slot,
14
+ and inherits every `AppShell` capability (auth gate, loading/error/forbidden state
15
+ cards, width policy, banner, the responsive hamburger + overlay drawer). `'both'`
16
+ is the AML-console shape — a top bar (brand / global links + overflow "…" / user)
17
+ AND a side rail (sections) together — now first-class instead of hand-wired. The
18
+ top bar is built from a `topBar` config (a `NavBar`), the side rail from a
19
+ `sideRail` config (a `Sidebar`); an explicit `header` node overrides the built top
20
+ bar for apps that want a structured `Topbar`.
21
+
22
+ **Three-tier responsive rail (`AppShell.collapsedSidebar`).** Opts the `sidebar`
23
+ layout into the erevna/katalogos scheme: the full rail on desktop, an intermediate
24
+ persistent **collapsed (icon-only) rail** on a tablet band, and the overlay drawer
25
+ on a phone. Supply a `collapsedSidebar` node (and optionally `collapsedRailRange`,
26
+ default `{ min: 768, max: 1024 }`) — omit it and the layout stays the unchanged
27
+ two-tier scheme. New `CollapsedRail` component renders an icon-only rail from the
28
+ same `NavItem[]` (icon or first-letter glyph fallback, header/footer slots). New
29
+ `resolveRailMode` pure resolver + `RAIL_FULL_BREAKPOINT` / `DEFAULT_COLLAPSED_RAIL_MAX`.
30
+ `NavShell.sideRail.collapsed` builds it config-driven. New testID suffix
31
+ `-collapsed-sidebar`.
32
+
33
+ **`NavBar.barTheme` — dark bar over a light page.** An optional chrome-theme
34
+ override re-provisions the `@dloizides/ui-feedback` theme for the bar only (the AML
35
+ v1-console dark appbar over light content), re-passing the live `t` / `navigate` so
36
+ labels keep localizing. Forwarded via `NavShell.topBar.barTheme`. Style-only —
37
+ colours still flow through the theme. (Custom appbar backgrounds like AML's radial
38
+ glow stay expressible via `containerStyle`.)
39
+
40
+ **`PillNav` — coral cross-navigation (kefi).** A row of fully-rounded pills from a
41
+ `NavItem[]`, the active one wearing the accent (theme `palette.primary` = kefi's
42
+ coral). Folds in kefi's **visibility rule**: hides (renders `null`) unless there
43
+ are at least `minItems` entries (default 1; kefi passes 2).
44
+
45
+ **`DarkModeControl` — 3-way colour scheme (erevna/katalogos).** A controlled
46
+ Light / Dark / System control in two variants: `"segmented"` (a highlighted
47
+ segment row) and `"cycle"` (an icon+label button that advances to the next option
48
+ on press). Value-agnostic string options, pre-localized labels/hints.
49
+
50
+ **Refactor (no behaviour change).** `AppShell`'s page-state cards moved to
51
+ `appShellContent.tsx` (`useContentBody` / `ShellMessage` / `ShellState`) to keep
52
+ `AppShell` focused on layout; the shell's render tree is unchanged.
53
+
54
+ New exports: `NavShell` (+ `NavShellProps` / `NavShellLayout` / `NavShellTopBar` /
55
+ `NavShellSideRail` / `NavShellCollapsedRail`), `CollapsedRail` (+ `CollapsedRailProps`),
56
+ `PillNav` (+ `PillNavProps`), `DarkModeControl` (+ `DarkModeControlProps` /
57
+ `DarkModeOption` / `DarkModeVariant`), `resolveRailMode` (+ `RailMode` /
58
+ `CollapsedRailRange` / `ResolveRailModeArgs`), `RAIL_FULL_BREAKPOINT`,
59
+ `DEFAULT_COLLAPSED_RAIL_MAX`, `pillNavStyles`, `darkModeStyles`, `collapsedRailStyles`.
60
+
61
+ ## 1.7.0
62
+
63
+ Two additive navigation capabilities on the horizontal bar, plus a unifying component.
64
+
65
+ **Priority+ overflow ("…" menu) on `NavBar`.** Above the `collapseBelow` breakpoint the
66
+ inline links sit on ONE non-wrapping row; when they don't all fit, a measure pass
67
+ (`useNavOverflow` + the pure `computeVisibleCount`) keeps the leading items that fit inline
68
+ and collapses the rest behind a **"More ▾"** trigger — the shared `@dloizides/ui-layout`
69
+ `ModalDropdown`, so the overflow menu is portalled, keyboard-navigable, and dismissed on
70
+ outside-click / Escape like every other menu. No item is ever cut off; the surplus simply
71
+ moves into the dropdown. When the active route lives in the overflow set the trigger wears
72
+ the accent pill so it stays visibly reachable. BELOW `collapseBelow` the whole set still
73
+ collapses into the responsive hamburger drawer (the two mechanisms never overlap). Two new
74
+ optional props localize the trigger: `overflowLabel` (visible + accessible, default `"More"`)
75
+ and `overflowHint`. New `NAV_TEST_IDS.navBarOverflow` (`navbar-overflow`).
76
+
77
+ **`Nav` — one component, two orientations.** `<Nav orientation="horizontal" />` renders the
78
+ `NavBar`; `<Nav orientation="vertical" />` renders the `Sidebar` — from the SAME `NavItem[]`,
79
+ so an app flips between a top bar and a side rail without swapping components. `orientation`
80
+ defaults to `"horizontal"`; the remaining props are a discriminated union of the delegate's
81
+ props (TypeScript enforces the right prop set per orientation). Vertical nav is the existing
82
+ `Sidebar` (stacked, vertically scrolling — no overflow menu needed).
83
+
84
+ **Backwards compatible.** Purely additive — `Sidebar`, `Topbar`, `NavBar`, `AppShell`, and
85
+ the role-gating helpers keep their exact signatures; the new overflow props are optional
86
+ (default `"More"` / `""`) so aml-v2 (the only `NavBar` consumer) and every `Sidebar`/`Topbar`
87
+ consumer (erevna / katalogos / kefi / agora) are untouched. New exports (`Nav`, `NavProps`,
88
+ `NavOrientation`, `NavOverflowMenu`, `NavOverflowMenuProps`, `NAV_LINK_GAP`) are additive.
89
+
90
+ ## 1.6.1
91
+
92
+ Keep the expanded links on ONE row — the row must be `nowrap` so `flex-shrink` distributes
93
+ negative space (a `wrap` row drops the right slot onto a 2nd line instead of shrinking the
94
+ links). Renames the internal expanded links style to `linksRow` (grow + shrink-to-0,
95
+ `minWidth:0`, `overflow:hidden`) with a per-link `linkCell` (never shrinks, so each link keeps
96
+ its natural measured width). Brand + right stay pinned (`flexShrink:0`).
97
+
98
+ ## 1.6.0
99
+
100
+ Keep the bar to ONE row — horizontal-scroll / clip the links instead of wrapping the bar
101
+ onto a second line when brand + links + right slot overflow the available width.
102
+
3
103
  ## 1.5.0
4
104
 
5
105
  Make the `AppShell` back-office `sidebar` layout **responsive** — a mobile drawer.
package/README.md CHANGED
@@ -68,9 +68,122 @@ import { NavBar, type NavItem } from '@dloizides/ui-nav';
68
68
  menuLabel={FM('menu.toggle')} // a11y label for the hamburger
69
69
  menuHint={FM('menu.toggleHint')}
70
70
  collapseBelow={760} // hamburger under this viewport width
71
+ overflowLabel={FM('menu.more')} // "More ▾" priority+ trigger label
72
+ overflowHint={FM('menu.moreHint')}
71
73
  />
72
74
  ```
73
75
 
76
+ **Priority+ overflow ("…" menu).** Above `collapseBelow` the links sit on ONE
77
+ non-wrapping row. When they don't all fit, a measure pass keeps the leading items
78
+ that fit inline and collapses the rest behind a **"More ▾"** trigger — the shared
79
+ `@dloizides/ui-layout` `ModalDropdown` — so no item is ever cut off; the surplus
80
+ moves into a portalled, keyboard-navigable dropdown. If the active route lives in
81
+ the overflow set, the trigger wears the accent pill so it stays reachable. Localize
82
+ the trigger with the optional `overflowLabel` (visible + accessible, default `"More"`)
83
+ and `overflowHint`. Below `collapseBelow` the whole set still collapses into the
84
+ responsive hamburger drawer, so the two mechanisms never overlap.
85
+
86
+ ### One component, two orientations (`Nav`)
87
+
88
+ `Nav` renders EITHER the horizontal `NavBar` or the vertical `Sidebar` from the same
89
+ `NavItem[]`, chosen by a single `orientation` prop — flip between a top bar and a side
90
+ rail without swapping components or re-plumbing props. `orientation` defaults to
91
+ `"horizontal"`; the remaining props are a discriminated union of the delegate's props
92
+ (TypeScript enforces the right set per orientation — e.g. `title` only on vertical,
93
+ `collapseBelow` / `overflowLabel` only on horizontal).
94
+
95
+ ```tsx
96
+ import { Nav, type NavItem } from '@dloizides/ui-nav';
97
+
98
+ <Nav orientation="horizontal" items={items} pathname={p} onNavigate={go} regionLabel={r} /> // NavBar
99
+ <Nav orientation="vertical" items={items} pathname={p} onNavigate={go} title={t} regionLabel={r} /> // Sidebar
100
+ ```
101
+
102
+ `Nav` is purely additive: `Sidebar` / `Topbar` / `NavBar` stay exported and unchanged.
103
+
104
+ ### One shell, three layouts — top | side | **both** (`NavShell`)
105
+
106
+ Where `Nav` answers the single-axis question (a `NavBar` **or** a `Sidebar`),
107
+ `NavShell` owns the whole page composition and renders a **top bar**, a **side
108
+ rail**, or **BOTH together**, chosen by a single `layout` prop. It wires a top bar
109
+ into `AppShell`'s `header` slot and a side rail into its `sidebar` slot, and
110
+ inherits every `AppShell` capability (auth gate, loading/error/forbidden state
111
+ cards, width policy, banner, the responsive hamburger + overlay drawer).
112
+
113
+ ```tsx
114
+ import { NavShell, type NavItem } from '@dloizides/ui-nav';
115
+
116
+ // layout="both" — a top bar (brand / global links + overflow "…" / user) AND a
117
+ // side rail (sections) together (the AML-console shape), now first-class.
118
+ <NavShell
119
+ layout="both"
120
+ pathname={usePathname()}
121
+ onNavigate={(route) => router.push(route)}
122
+ regionLabel={FM('a11y.nav')}
123
+ topBar={{
124
+ items: globalItems, // top links (overflow-capable)
125
+ brand: <BrandLogo />,
126
+ right: <><LanguageSwitcher /><UserChip /></>,
127
+ overflowLabel: FM('menu.more'),
128
+ barTheme: darkChromeTheme, // optional: dark bar over a light page
129
+ }}
130
+ sideRail={{
131
+ items: sectionItems, // side sections (leaf + expandable)
132
+ title: FM('menu.title'),
133
+ footer: <><DarkModeControl … /><LogoutButton /></>,
134
+ collapsed: { range: { min: 768, max: 1024 } }, // optional 3-tier icon rail
135
+ }}
136
+ gate={{ pending, authenticated, onRedirect: () => router.push('/login') }}
137
+ state={{ loading, error, forbidden }}
138
+ width={{ max: 1180, wideMax: 1400, wideMinViewport: 1600 }}
139
+ testID="app-shell"
140
+ >
141
+ <PageContent />
142
+ </NavShell>
143
+ ```
144
+
145
+ - `layout="top"` → a top bar only (`topBar`); no rail.
146
+ - `layout="side"` → a header + a persistent left rail of sections (`sideRail`); pass a
147
+ structured `<Topbar>` as `header` for the language/notification/user slots.
148
+ - `layout="both"` → both, together.
149
+
150
+ `NavShell` composes existing components, so it stays purely additive — `AppShell`,
151
+ `NavBar`, and `Sidebar` are unchanged.
152
+
153
+ ### Three-tier responsive rail (`AppShell.collapsedSidebar` / `CollapsedRail`)
154
+
155
+ Supply a `collapsedSidebar` (or `NavShell`'s `sideRail.collapsed`) to opt the rail
156
+ into the erevna/katalogos scheme: the full rail on desktop, an intermediate
157
+ persistent **collapsed (icon-only) rail** on a tablet band `[min, max)` (default
158
+ `{ min: 768, max: 1024 }`), and the overlay drawer on a phone. Omit it and the
159
+ layout stays the unchanged two-tier scheme (full rail ≥768px, drawer below).
160
+ `CollapsedRail` renders the icon rail from the same `NavItem[]` (each item's
161
+ `renderIcon`, or a first-letter glyph fallback), with header/footer slots. The pure
162
+ `resolveRailMode` decides the tier and is directly unit-testable.
163
+
164
+ ### Dark bar over a light page (`NavBar.barTheme`)
165
+
166
+ Pass `barTheme` (a `UiTheme`) to re-provision the theme for the bar **only** — a
167
+ dark appbar above a light workspace (the AML v1 console). The live `t` / `navigate`
168
+ are re-passed so labels keep localizing; every colour still flows through the theme
169
+ (style-only). Custom appbar backgrounds (e.g. a radial-glow gradient) stay
170
+ expressible via `containerStyle`.
171
+
172
+ ### Cross-navigation pills (`PillNav`)
173
+
174
+ A row of fully-rounded pills from a `NavItem[]` — kefi's coral dashboard switcher.
175
+ The active pill wears the accent (theme `palette.primary`). Folds in kefi's rule:
176
+ it hides (renders `null`) unless there are at least `minItems` entries (default 1;
177
+ kefi passes `2`). Drop it into `AppShell`/`NavShell`'s `nav` slot.
178
+
179
+ ### Colour-scheme control (`DarkModeControl`)
180
+
181
+ A controlled Light / Dark / System control (erevna/katalogos), in two variants:
182
+ `"segmented"` (a highlighted segment row) and `"cycle"` (an icon+label button that
183
+ advances to the next option per press). Options are pre-localized and value-agnostic
184
+ (strings), so map your own preference enum without this package importing it. Put it
185
+ in a `Sidebar`/`CollapsedRail` footer or a `NavBar` right slot.
186
+
74
187
  An app that renders a `Sidebar` today switches to a top bar by swapping the
75
188
  `Sidebar` element for `NavBar` (same `items` / `pathname` / `onNavigate`) — e.g.
76
189
  the AML v2 app drops `NavBar` into the `AppShell` `header` (or `nav`) slot so its