@dloizides/ui-nav 1.7.0 → 1.9.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,78 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.0
4
+
5
+ **`NavBar.contentMaxWidth` — a full-bleed bar with a centred content column.**
6
+ An optional content-column cap (px), forwarded via `NavShell.topBar.contentMaxWidth`.
7
+ When set, the OUTER `NavBar` container — which carries the bar background + bottom
8
+ border (from `barTheme` / theme surface + any `containerStyle`, e.g. the AML
9
+ radial-glow appbar) — stays FULL WIDTH (full-bleed), while the INNER row (brand +
10
+ links + right) is capped to `contentMaxWidth` and centred. This restores the v1-console
11
+ `.ui-appbar` (full-bleed) + `.ui-appbar-inner { max-width; margin: 0 auto }` shape:
12
+ previously an app had to put the cap on the container's `containerStyle`, which also
13
+ capped the dark background and left light gutters on wide viewports. Omit it and the
14
+ inner spans the full container (unchanged — byte-identical to before). The priority+
15
+ overflow "…" fit measures the capped inner's rendered width, so it keeps working
16
+ against the content column, not the full-bleed container. Purely additive.
17
+
18
+ ## 1.8.0
19
+
20
+ The navigation shell becomes the ONE shell for every portal: a first-class
21
+ **top | side | both** layout, plus the capabilities harvested from every RN-web
22
+ portal's bespoke nav/shell so nothing is lost when they migrate. Purely additive —
23
+ `Sidebar`, `Topbar`, `NavBar`, `Nav`, `AppShell`, and the role helpers keep their
24
+ exact signatures; every new prop/component is optional.
25
+
26
+ **`NavShell` — top bar, side rail, or BOTH.** One component owns the whole page
27
+ composition, chosen by a single `layout` prop (`'top' | 'side' | 'both'`). It wires
28
+ a top bar into `AppShell`'s `header` slot and a side rail into its `sidebar` slot,
29
+ and inherits every `AppShell` capability (auth gate, loading/error/forbidden state
30
+ cards, width policy, banner, the responsive hamburger + overlay drawer). `'both'`
31
+ is the AML-console shape — a top bar (brand / global links + overflow "…" / user)
32
+ AND a side rail (sections) together — now first-class instead of hand-wired. The
33
+ top bar is built from a `topBar` config (a `NavBar`), the side rail from a
34
+ `sideRail` config (a `Sidebar`); an explicit `header` node overrides the built top
35
+ bar for apps that want a structured `Topbar`.
36
+
37
+ **Three-tier responsive rail (`AppShell.collapsedSidebar`).** Opts the `sidebar`
38
+ layout into the erevna/katalogos scheme: the full rail on desktop, an intermediate
39
+ persistent **collapsed (icon-only) rail** on a tablet band, and the overlay drawer
40
+ on a phone. Supply a `collapsedSidebar` node (and optionally `collapsedRailRange`,
41
+ default `{ min: 768, max: 1024 }`) — omit it and the layout stays the unchanged
42
+ two-tier scheme. New `CollapsedRail` component renders an icon-only rail from the
43
+ same `NavItem[]` (icon or first-letter glyph fallback, header/footer slots). New
44
+ `resolveRailMode` pure resolver + `RAIL_FULL_BREAKPOINT` / `DEFAULT_COLLAPSED_RAIL_MAX`.
45
+ `NavShell.sideRail.collapsed` builds it config-driven. New testID suffix
46
+ `-collapsed-sidebar`.
47
+
48
+ **`NavBar.barTheme` — dark bar over a light page.** An optional chrome-theme
49
+ override re-provisions the `@dloizides/ui-feedback` theme for the bar only (the AML
50
+ v1-console dark appbar over light content), re-passing the live `t` / `navigate` so
51
+ labels keep localizing. Forwarded via `NavShell.topBar.barTheme`. Style-only —
52
+ colours still flow through the theme. (Custom appbar backgrounds like AML's radial
53
+ glow stay expressible via `containerStyle`.)
54
+
55
+ **`PillNav` — coral cross-navigation (kefi).** A row of fully-rounded pills from a
56
+ `NavItem[]`, the active one wearing the accent (theme `palette.primary` = kefi's
57
+ coral). Folds in kefi's **visibility rule**: hides (renders `null`) unless there
58
+ are at least `minItems` entries (default 1; kefi passes 2).
59
+
60
+ **`DarkModeControl` — 3-way colour scheme (erevna/katalogos).** A controlled
61
+ Light / Dark / System control in two variants: `"segmented"` (a highlighted
62
+ segment row) and `"cycle"` (an icon+label button that advances to the next option
63
+ on press). Value-agnostic string options, pre-localized labels/hints.
64
+
65
+ **Refactor (no behaviour change).** `AppShell`'s page-state cards moved to
66
+ `appShellContent.tsx` (`useContentBody` / `ShellMessage` / `ShellState`) to keep
67
+ `AppShell` focused on layout; the shell's render tree is unchanged.
68
+
69
+ New exports: `NavShell` (+ `NavShellProps` / `NavShellLayout` / `NavShellTopBar` /
70
+ `NavShellSideRail` / `NavShellCollapsedRail`), `CollapsedRail` (+ `CollapsedRailProps`),
71
+ `PillNav` (+ `PillNavProps`), `DarkModeControl` (+ `DarkModeControlProps` /
72
+ `DarkModeOption` / `DarkModeVariant`), `resolveRailMode` (+ `RailMode` /
73
+ `CollapsedRailRange` / `ResolveRailModeArgs`), `RAIL_FULL_BREAKPOINT`,
74
+ `DEFAULT_COLLAPSED_RAIL_MAX`, `pillNavStyles`, `darkModeStyles`, `collapsedRailStyles`.
75
+
3
76
  ## 1.7.0
4
77
 
5
78
  Two additive navigation capabilities on the horizontal bar, plus a unifying component.
package/README.md CHANGED
@@ -101,6 +101,103 @@ import { Nav, type NavItem } from '@dloizides/ui-nav';
101
101
 
102
102
  `Nav` is purely additive: `Sidebar` / `Topbar` / `NavBar` stay exported and unchanged.
103
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
+ contentMaxWidth: 1180, // optional: full-bleed bar, inner capped + centred
130
+ }}
131
+ sideRail={{
132
+ items: sectionItems, // side sections (leaf + expandable)
133
+ title: FM('menu.title'),
134
+ footer: <><DarkModeControl … /><LogoutButton /></>,
135
+ collapsed: { range: { min: 768, max: 1024 } }, // optional 3-tier icon rail
136
+ }}
137
+ gate={{ pending, authenticated, onRedirect: () => router.push('/login') }}
138
+ state={{ loading, error, forbidden }}
139
+ width={{ max: 1180, wideMax: 1400, wideMinViewport: 1600 }}
140
+ testID="app-shell"
141
+ >
142
+ <PageContent />
143
+ </NavShell>
144
+ ```
145
+
146
+ - `layout="top"` → a top bar only (`topBar`); no rail.
147
+ - `layout="side"` → a header + a persistent left rail of sections (`sideRail`); pass a
148
+ structured `<Topbar>` as `header` for the language/notification/user slots.
149
+ - `layout="both"` → both, together.
150
+
151
+ `NavShell` composes existing components, so it stays purely additive — `AppShell`,
152
+ `NavBar`, and `Sidebar` are unchanged.
153
+
154
+ ### Three-tier responsive rail (`AppShell.collapsedSidebar` / `CollapsedRail`)
155
+
156
+ Supply a `collapsedSidebar` (or `NavShell`'s `sideRail.collapsed`) to opt the rail
157
+ into the erevna/katalogos scheme: the full rail on desktop, an intermediate
158
+ persistent **collapsed (icon-only) rail** on a tablet band `[min, max)` (default
159
+ `{ min: 768, max: 1024 }`), and the overlay drawer on a phone. Omit it and the
160
+ layout stays the unchanged two-tier scheme (full rail ≥768px, drawer below).
161
+ `CollapsedRail` renders the icon rail from the same `NavItem[]` (each item's
162
+ `renderIcon`, or a first-letter glyph fallback), with header/footer slots. The pure
163
+ `resolveRailMode` decides the tier and is directly unit-testable.
164
+
165
+ ### Dark bar over a light page (`NavBar.barTheme`)
166
+
167
+ Pass `barTheme` (a `UiTheme`) to re-provision the theme for the bar **only** — a
168
+ dark appbar above a light workspace (the AML v1 console). The live `t` / `navigate`
169
+ are re-passed so labels keep localizing; every colour still flows through the theme
170
+ (style-only). Custom appbar backgrounds (e.g. a radial-glow gradient) stay
171
+ expressible via `containerStyle`.
172
+
173
+ ### Full-bleed bar with a centred content column (`NavBar.contentMaxWidth`)
174
+
175
+ Pass `contentMaxWidth` (px) — via `NavBar.contentMaxWidth` or
176
+ `NavShell.topBar.contentMaxWidth` — to make the bar **full-bleed** while its content
177
+ stays in a centred column. The OUTER container (which carries the bar background +
178
+ bottom border from `barTheme` / theme surface + any `containerStyle`, e.g. AML's
179
+ radial-glow appbar) spans the full viewport width; the INNER row (brand + links +
180
+ right) is capped to `contentMaxWidth` and centred — the v1-console `.ui-appbar`
181
+ (full-bleed) + `.ui-appbar-inner { max-width; margin: 0 auto }` shape. Omit it and
182
+ the inner spans the full container (unchanged). The priority+ overflow "…" fit
183
+ measures the capped inner's rendered width, so it keeps working against the content
184
+ column, not the full-bleed container.
185
+
186
+ ### Cross-navigation pills (`PillNav`)
187
+
188
+ A row of fully-rounded pills from a `NavItem[]` — kefi's coral dashboard switcher.
189
+ The active pill wears the accent (theme `palette.primary`). Folds in kefi's rule:
190
+ it hides (renders `null`) unless there are at least `minItems` entries (default 1;
191
+ kefi passes `2`). Drop it into `AppShell`/`NavShell`'s `nav` slot.
192
+
193
+ ### Colour-scheme control (`DarkModeControl`)
194
+
195
+ A controlled Light / Dark / System control (erevna/katalogos), in two variants:
196
+ `"segmented"` (a highlighted segment row) and `"cycle"` (an icon+label button that
197
+ advances to the next option per press). Options are pre-localized and value-agnostic
198
+ (strings), so map your own preference enum without this package importing it. Put it
199
+ in a `Sidebar`/`CollapsedRail` footer or a `NavBar` right slot.
200
+
104
201
  An app that renders a `Sidebar` today switches to a top bar by swapping the
105
202
  `Sidebar` element for `NavBar` (same `items` / `pathname` / `onNavigate`) — e.g.
106
203
  the AML v2 app drops `NavBar` into the `AppShell` `header` (or `nav`) slot so its