@dloizides/ui-nav 1.7.0 → 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,63 @@
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
+
3
61
  ## 1.7.0
4
62
 
5
63
  Two additive navigation capabilities on the horizontal bar, plus a unifying component.
package/README.md CHANGED
@@ -101,6 +101,89 @@ 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
+ }}
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
+
104
187
  An app that renders a `Sidebar` today switches to a top bar by swapping the
105
188
  `Sidebar` element for `NavBar` (same `items` / `pathname` / `onNavigate`) — e.g.
106
189
  the AML v2 app drops `NavBar` into the `AppShell` `header` (or `nav`) slot so its