@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/dist/index.d.mts CHANGED
@@ -1,5 +1,7 @@
1
1
  import React from 'react';
2
2
  import { ViewStyle } from 'react-native';
3
+ import { UiTheme } from '@dloizides/ui-feedback';
4
+ import { DropdownVariant } from '@dloizides/ui-layout';
3
5
  import { resolveAccessibleRoutes, RoleRouteTable, RoleRoute } from '@dloizides/auth-web';
4
6
 
5
7
  /**
@@ -161,10 +163,19 @@ declare const Topbar: ({ left, language, notificationSlot, user, account, logout
161
163
  * hover bg=`surfaceElevated`, active=`palette.primary`, ring=`palette.primary`).
162
164
  * Hover/focus are web-only (react-native-web); native stays at the rest style.
163
165
  *
166
+ * Overflow ("…" priority+ menu). Above the `collapseBelow` breakpoint the links sit
167
+ * on ONE row; when they don't all fit, a measure pass ({@link useNavOverflow} +
168
+ * {@link computeVisibleCount}) keeps the leading items that fit inline and collapses
169
+ * the rest behind a "More ▾" trigger — the shared `ModalDropdown` (see
170
+ * {@link NavOverflowMenu}) — so no item is ever cut off. BELOW `collapseBelow` the
171
+ * whole set still collapses into the responsive hamburger drawer (all items stacked),
172
+ * so the two mechanisms don't overlap: hamburger for true mobile, "…" for the
173
+ * in-between desktop case where only SOME items spill.
174
+ *
164
175
  * Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM, router,
165
176
  * icon set, or store imports. Labels are pre-localized strings, icons are render
166
177
  * slots, and every colour is read from the theme. This is purely additive —
167
- * `Sidebar`/`Topbar` are unchanged.
178
+ * `Sidebar`/`Topbar` are unchanged and the new overflow labels are optional.
168
179
  */
169
180
 
170
181
  interface NavBarProps {
@@ -190,10 +201,81 @@ interface NavBarProps {
190
201
  renderMenuIcon?: (color: string, open: boolean) => React.ReactNode;
191
202
  /** Collapse the links behind a hamburger below this viewport width (default 760). */
192
203
  collapseBelow?: number;
204
+ /**
205
+ * Localized visible + accessible label for the "More ▾" overflow trigger shown when
206
+ * the inline links don't all fit on one row (priority+ menu). Defaults to `"More"`.
207
+ */
208
+ overflowLabel?: string;
209
+ /** Localized accessibility hint for the overflow trigger. */
210
+ overflowHint?: string;
193
211
  /** Extra container style overrides. */
194
212
  containerStyle?: ViewStyle | ViewStyle[];
213
+ /**
214
+ * Optional CHROME theme override — re-provisions the `@dloizides/ui-feedback`
215
+ * theme for the bar ONLY, so the top bar can be dark while the page below keeps
216
+ * the light workspace theme (the AML v1-console "dark appbar over light content"
217
+ * pattern). The live `t` (+ `navigate`) from the surrounding provider are
218
+ * re-passed, so nav labels keep localizing. Omit for a bar that shares the page
219
+ * theme. Style-only: every colour still flows through the theme — no per-app fork.
220
+ */
221
+ barTheme?: UiTheme;
222
+ }
223
+ /**
224
+ * `NavBar` — the horizontal top bar. When `barTheme` is supplied it re-provisions
225
+ * the `@dloizides/ui-feedback` theme around the bar (dark bar over a light page),
226
+ * re-passing the live `t` / `navigate` so labels keep localizing; otherwise it
227
+ * renders directly against the surrounding theme (byte-identical to before).
228
+ */
229
+ declare const NavBar: ({ barTheme, ...props }: NavBarProps) => React.ReactElement;
230
+
231
+ /**
232
+ * `Nav` — ONE component that renders EITHER the horizontal top bar (`NavBar`) or the
233
+ * vertical rail (`Sidebar`) from the same `NavItem[]`, chosen by a single
234
+ * `orientation` prop. It lets an app flip its navigation between a top bar and a side
235
+ * menu without swapping components or re-plumbing props:
236
+ *
237
+ * <Nav orientation="horizontal" items={items} pathname={p} onNavigate={go} .../> // NavBar
238
+ * <Nav orientation="vertical" items={items} pathname={p} onNavigate={go} .../> // Sidebar
239
+ *
240
+ * `orientation` defaults to `"horizontal"`. The remaining props are exactly the
241
+ * delegate's props — a discriminated union — so TypeScript enforces the right prop
242
+ * set per orientation (e.g. `title` is required only for the vertical/Sidebar side,
243
+ * `collapseBelow` / `overflowLabel` only for the horizontal/NavBar side).
244
+ *
245
+ * `Nav` is purely additive: `NavBar` and `Sidebar` remain exported and unchanged, so
246
+ * every existing consumer (erevna / katalogos / kefi / agora) is untouched. Vertical
247
+ * navigation is the existing Sidebar — stacked links that scroll vertically, so it
248
+ * needs no overflow menu; the priority+ "…" overflow is a horizontal-bar concern.
249
+ */
250
+
251
+ /** Which axis the navigation renders on. */
252
+ type NavOrientation = 'horizontal' | 'vertical';
253
+ type NavProps = ({
254
+ orientation?: 'horizontal';
255
+ } & NavBarProps) | ({
256
+ orientation: 'vertical';
257
+ } & SidebarProps);
258
+ declare const Nav: (props: NavProps) => React.ReactElement;
259
+
260
+ /**
261
+ * AppShell content-body helpers — the page-state cards (loading / error /
262
+ * forbidden) and the precedence rule that picks what renders inside the content
263
+ * column. Split out of `AppShell` so the shell file stays focused on layout /
264
+ * responsive composition. Colours are routed through the `@dloizides/ui-feedback`
265
+ * theme, same as the rest of `@dloizides/ui-nav`.
266
+ */
267
+
268
+ /** A titled message card (error / forbidden). Colours come from the theme. */
269
+ interface ShellMessage {
270
+ titleText: string;
271
+ messageText: string;
272
+ }
273
+ /** Page-state model — a loading spinner card, or a titled error / forbidden card. */
274
+ interface ShellState {
275
+ loading?: boolean;
276
+ error?: ShellMessage | null;
277
+ forbidden?: ShellMessage | null;
195
278
  }
196
- declare const NavBar: ({ items, pathname, onNavigate, regionLabel, navigateHint, brand, right, menuLabel, menuHint, renderMenuIcon, collapseBelow, containerStyle, }: NavBarProps) => React.ReactElement;
197
279
 
198
280
  /**
199
281
  * MobileDrawer — the below-the-breakpoint face of `AppShell`'s back-office
@@ -222,6 +304,50 @@ interface DrawerLabels {
222
304
  closeHint: string;
223
305
  }
224
306
 
307
+ /**
308
+ * Rail responsive mode — the pure (viewport → which rail face) rule behind
309
+ * `AppShell`'s optional THREE-tier layout, split out so it is directly
310
+ * unit-testable.
311
+ *
312
+ * Two-tier (default, back-compat): a persistent full rail at/above
313
+ * {@link RAIL_FULL_BREAKPOINT}, an overlay drawer below it. This is what every
314
+ * existing `AppShell` `sidebar` consumer already gets.
315
+ *
316
+ * Three-tier (opt-in — supply a `collapsedSidebar`): a persistent full rail on
317
+ * desktop, an intermediate persistent COLLAPSED (icon-only) rail on tablet, and
318
+ * the overlay drawer on a phone — the erevna / katalogos responsive scheme.
319
+ */
320
+ /** At/above this viewport width the persistent FULL rail shows (two-tier boundary). */
321
+ declare const RAIL_FULL_BREAKPOINT = 768;
322
+ /** Default upper bound of the collapsed-rail band (full rail at/above this). */
323
+ declare const DEFAULT_COLLAPSED_RAIL_MAX = 1024;
324
+ /** Which face the rail takes at a given viewport. */
325
+ type RailMode = 'none' | 'full' | 'collapsed' | 'drawer';
326
+ /**
327
+ * Viewport band in which the intermediate COLLAPSED icon-rail shows: `[min, max)`.
328
+ * At/above `max` the full rail shows; below `min` the overlay drawer shows.
329
+ */
330
+ interface CollapsedRailRange {
331
+ min: number;
332
+ max: number;
333
+ }
334
+ interface ResolveRailModeArgs {
335
+ viewport: number;
336
+ /** Whether a `sidebar` (full rail) node was supplied at all. */
337
+ hasSidebar: boolean;
338
+ /** Whether an intermediate `collapsedSidebar` node was supplied (enables 3-tier). */
339
+ hasCollapsed: boolean;
340
+ /** The collapsed-rail band; only consulted in 3-tier mode. */
341
+ range: CollapsedRailRange;
342
+ }
343
+ /**
344
+ * Resolve the rail mode. No sidebar ⇒ `'none'`. With a `collapsedSidebar`
345
+ * (3-tier): full at/above `range.max`, collapsed in `[range.min, range.max)`,
346
+ * drawer below `range.min`. Without one (2-tier, unchanged): full at/above
347
+ * {@link RAIL_FULL_BREAKPOINT}, drawer below.
348
+ */
349
+ declare function resolveRailMode({ viewport, hasSidebar, hasCollapsed, range }: ResolveRailModeArgs): RailMode;
350
+
225
351
  /** Content-width policy: a capped column, optionally wider past a viewport breakpoint, or full-bleed. */
226
352
  type AppShellWidth = {
227
353
  max: number;
@@ -257,11 +383,6 @@ declare function useContentMaxWidth(width: AppShellWidth): number | 'full';
257
383
  * callbacks; every colour is routed through the `@dloizides/ui-feedback` theme.
258
384
  */
259
385
 
260
- /** A titled message card (error / forbidden). Colours come from the theme. */
261
- interface ShellMessage {
262
- titleText: string;
263
- messageText: string;
264
- }
265
386
  interface AppShellProps {
266
387
  /** App-supplied wired header (Topbar / AppHeader), rendered full-bleed. */
267
388
  header: React.ReactNode;
@@ -278,6 +399,21 @@ interface AppShellProps {
278
399
  * takes the full width. Desktop (≥768px) is unchanged. No effect without `sidebar`.
279
400
  */
280
401
  sidebar?: React.ReactNode;
402
+ /**
403
+ * Optional intermediate COLLAPSED (icon-only) rail — opts the `sidebar` layout
404
+ * into a THREE-tier responsive scheme (erevna / katalogos): the full `sidebar`
405
+ * on desktop, this collapsed rail on a tablet-width band, and the overlay drawer
406
+ * (hosting the full `sidebar`) on a phone. Omit it and the layout stays the
407
+ * unchanged two-tier scheme (full rail ≥768px, drawer below). No effect without
408
+ * `sidebar`. Exposed as `${testID}${APP_SHELL_SUFFIX.collapsedSidebar}`.
409
+ */
410
+ collapsedSidebar?: React.ReactNode;
411
+ /**
412
+ * The viewport band `[min, max)` in which `collapsedSidebar` shows: at/above
413
+ * `max` the full rail shows, below `min` the drawer shows. Defaults to
414
+ * `{ min: 768, max: 1024 }`. Only consulted when `collapsedSidebar` is supplied.
415
+ */
416
+ collapsedRailRange?: CollapsedRailRange;
281
417
  /**
282
418
  * Pre-localized labels for the mobile nav drawer (only used in the `sidebar`
283
419
  * layout, below the breakpoint): the hamburger's open label/hint and the scrim's
@@ -303,16 +439,360 @@ interface AppShellProps {
303
439
  onRedirect: () => void;
304
440
  };
305
441
  /** Page state — loading spinner card, or a titled error / forbidden card. */
306
- state?: {
307
- loading?: boolean;
308
- error?: ShellMessage | null;
309
- forbidden?: ShellMessage | null;
310
- };
442
+ state?: ShellState;
311
443
  /** Root testID; the content region is exposed as `${testID}-content`. */
312
444
  testID: string;
313
445
  children: React.ReactNode;
314
446
  }
315
- declare const AppShell: ({ header, nav, sidebar, mobileMenu, banner, width, contentPadding, chromeAlignment, gate, state, testID, children, }: AppShellProps) => React.ReactElement | null;
447
+ declare const AppShell: ({ header, nav, sidebar, collapsedSidebar, collapsedRailRange, mobileMenu, banner, width, contentPadding, chromeAlignment, gate, state, testID, children, }: AppShellProps) => React.ReactElement | null;
448
+
449
+ /**
450
+ * NavShell — the ONE first-class navigation shell that renders a **top bar**, a
451
+ * **side rail**, or **BOTH together**, chosen by a single `layout` prop
452
+ * (`'top' | 'side' | 'both'`). It is the shell-level answer to `Nav`'s
453
+ * single-axis question: where `Nav` returns one element (a `NavBar` OR a
454
+ * `Sidebar`), `NavShell` owns the whole page composition — it wires a top bar
455
+ * into `AppShell`'s `header` slot and a side rail into `AppShell`'s `sidebar`
456
+ * slot, and inherits every `AppShell` capability (auth gate, loading / error /
457
+ * forbidden state cards, width discipline, banner, the responsive
458
+ * hamburger + overlay drawer for the rail).
459
+ *
460
+ * layout="top" → a horizontal top bar only (brand + links + overflow "…" + right).
461
+ * layout="side" → a header + a persistent left rail of sections (the erevna / katalogos shape).
462
+ * layout="both" → a top bar (brand / global links + overflow "…" / user) AND a side rail
463
+ * (sections), together — the AML-console shape, now first-class.
464
+ *
465
+ * "both" is inherently a shell composition (a top region + a side region + a
466
+ * content column), which a single-element `Nav` cannot own; `NavShell` is that
467
+ * composition, so no app hand-wires `Topbar` + `Sidebar` + a `ScrollView` row
468
+ * again. Customizability is STYLE-only: every colour flows through the
469
+ * `@dloizides/ui-feedback` UiProvider theme, and brand / user / language /
470
+ * notification / section content are render slots. Structure + behaviour are
471
+ * shared.
472
+ *
473
+ * Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM, router,
474
+ * icon-set, or store imports. Labels are pre-localized strings, icons are render
475
+ * slots, and every colour is read from the theme.
476
+ */
477
+
478
+ /** Which regions the shell renders: a top bar, a side rail, or both together. */
479
+ type NavShellLayout = 'top' | 'side' | 'both';
480
+ /**
481
+ * Config for the TOP bar (rendered as a `NavBar` in `AppShell`'s header slot).
482
+ * Used when `layout` is `'top'` or `'both'`. Every field is optional except the
483
+ * items — an empty `items` array renders a bar with just the brand + right slots
484
+ * (a plain top bar with no nav links), which is the natural `'side'`-mode header.
485
+ */
486
+ interface NavShellTopBar {
487
+ /** Top-bar nav entries (global links). Empty ⇒ brand + right only. */
488
+ items: NavItem[];
489
+ /** Left slot — typically the tenant brand / logo. */
490
+ brand?: React.ReactNode;
491
+ /** Right slot — free-form (language toggle, notification bell, user chip, logout). */
492
+ right?: React.ReactNode;
493
+ /** Collapse the links behind a hamburger below this viewport width (default 760). */
494
+ collapseBelow?: number;
495
+ /** Localized visible + accessible label for the "More ▾" overflow trigger (default "More"). */
496
+ overflowLabel?: string;
497
+ /** Localized accessibility hint for the overflow trigger. */
498
+ overflowHint?: string;
499
+ /** Localized accessibility label for the responsive menu toggle. */
500
+ menuLabel?: string;
501
+ /** Localized accessibility hint for the responsive menu toggle. */
502
+ menuHint?: string;
503
+ /** Optional custom glyph renderer for the responsive toggle; defaults to ☰. */
504
+ renderMenuIcon?: (color: string, open: boolean) => React.ReactNode;
505
+ /**
506
+ * Extra container style overrides (e.g. an app-specific appbar background —
507
+ * including a web `background`/`backgroundImage` gradient cast through
508
+ * `ViewStyle`, the AML radial-glow appbar). Style-only.
509
+ */
510
+ containerStyle?: ViewStyle | ViewStyle[];
511
+ /**
512
+ * Optional CHROME theme override for the top bar only (dark bar over a light
513
+ * page). Forwarded to `NavBar.barTheme`. Style-only — see `NavBar`.
514
+ */
515
+ barTheme?: UiTheme;
516
+ }
517
+ /**
518
+ * Config for the intermediate COLLAPSED (icon-only) rail — opts the side layout
519
+ * into a THREE-tier responsive scheme (full rail → collapsed rail → drawer). When
520
+ * present, NavShell builds a `<CollapsedRail>` and hands it to `AppShell` for the
521
+ * tablet band; omit it and the layout stays two-tier.
522
+ */
523
+ interface NavShellCollapsedRail {
524
+ /** Items for the icon rail. Defaults to the full rail's `items`. */
525
+ items?: NavItem[];
526
+ /** Optional header slot (e.g. a home shortcut). */
527
+ header?: React.ReactNode;
528
+ /** Optional footer slot (dark-mode toggle, logout). */
529
+ footer?: React.ReactNode;
530
+ /** The `[min, max)` viewport band for the collapsed rail (default `{ min: 768, max: 1024 }`). */
531
+ range?: CollapsedRailRange;
532
+ }
533
+ /**
534
+ * Config for the SIDE rail (rendered as a `Sidebar` in `AppShell`'s sidebar
535
+ * slot). Used when `layout` is `'side'` or `'both'`.
536
+ */
537
+ interface NavShellSideRail {
538
+ /** Side-rail nav entries (sections — leaf + expandable). */
539
+ items: NavItem[];
540
+ /** Localized menu title (heading). */
541
+ title: string;
542
+ /** Optional header slot rendered above the items (e.g. a Home shortcut). */
543
+ header?: React.ReactNode;
544
+ /** Optional footer slot rendered after a flex spacer (dark-mode toggle, logout). */
545
+ footer?: React.ReactNode;
546
+ /** a11y hint shown when an expandable section is collapsed. */
547
+ expandHint?: string;
548
+ /** a11y hint shown when an expandable section is expanded. */
549
+ collapseHint?: string;
550
+ /** Optional chevron renderer for expandable sections. */
551
+ renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
552
+ /** Extra container style overrides. */
553
+ containerStyle?: ViewStyle | ViewStyle[];
554
+ /** Optional collapsed (icon-only) rail config → the intermediate tablet tier. */
555
+ collapsed?: NavShellCollapsedRail;
556
+ }
557
+ /**
558
+ * `AppShell` props NavShell forwards straight through. It composes the `header`
559
+ * and `sidebar` (+ `collapsedSidebar`) regions itself, so those are handled here;
560
+ * everything else — including the optional `nav` strip (e.g. a `PillNav`), the
561
+ * auth gate, state cards, banner, and width policy — passes through untouched.
562
+ */
563
+ type ForwardedShellProps = Omit<AppShellProps, 'header' | 'sidebar' | 'children'>;
564
+ interface NavShellProps extends ForwardedShellProps {
565
+ /** Which regions to render: a top bar, a side rail, or both. */
566
+ layout: NavShellLayout;
567
+ /** Current active route/path — drives active highlighting on both regions. */
568
+ pathname: string;
569
+ /** Navigation callback — receives a `NavItem.route`. */
570
+ onNavigate: (route: string) => void;
571
+ /** Localized accessibility label for the navigation landmark(s). */
572
+ regionLabel: string;
573
+ /** a11y hint for a nav item, given its label. Defaults to the label. */
574
+ navigateHint?: (label: string) => string;
575
+ /**
576
+ * Top-bar config → a `NavBar` in the header slot. Required for `'top'` /
577
+ * `'both'`; ignored for `'side'` unless supplied (a `'side'` app may still pass
578
+ * a `topBar` with empty `items` for its brand + user header).
579
+ */
580
+ topBar?: NavShellTopBar;
581
+ /**
582
+ * Escape hatch: a ready-made header node (e.g. an app-composed `<Topbar>`).
583
+ * When supplied it REPLACES the `topBar`-built `NavBar` — used by `'side'`
584
+ * apps that want the structured `Topbar` (language / notification / user
585
+ * slots) instead of a `NavBar` header.
586
+ */
587
+ header?: React.ReactNode;
588
+ /**
589
+ * Side-rail config → a `Sidebar` in the sidebar slot. Required for `'side'` /
590
+ * `'both'`; ignored for `'top'`.
591
+ */
592
+ sideRail?: NavShellSideRail;
593
+ children: React.ReactNode;
594
+ }
595
+ declare const NavShell: ({ layout, pathname, onNavigate, regionLabel, navigateHint, topBar, header, sideRail, children, collapsedSidebar: collapsedSidebarProp, collapsedRailRange: collapsedRailRangeProp, ...shellProps }: NavShellProps) => React.ReactElement | null;
596
+
597
+ /**
598
+ * CollapsedRail — the icon-only vertical rail for the intermediate (tablet) tier
599
+ * of `AppShell`'s three-tier responsive layout (the erevna / katalogos collapsed
600
+ * rail). It renders the SAME caller-supplied `NavItem[]` as `Sidebar`, but as a
601
+ * narrow strip of icon buttons: each item shows its `renderIcon` (or a first-letter
602
+ * glyph fallback), labelled for assistive tech, with the active route highlighted.
603
+ * Optional `header` / `footer` slots carry app chrome (a home shortcut, a
604
+ * dark-mode toggle, logout) top and bottom.
605
+ *
606
+ * A collapsed rail cannot expand a section inline, so pressing ANY item — leaf or
607
+ * parent — navigates to its `route`; an app that wants a parent to open the full
608
+ * drawer instead can special-case that in its `onNavigate`.
609
+ *
610
+ * Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM, router,
611
+ * icon-set, or store imports. Labels are pre-localized strings, icons are render
612
+ * slots, and every colour is read from the `@dloizides/ui-feedback` theme.
613
+ */
614
+
615
+ interface CollapsedRailProps {
616
+ /** Nav entries — the same role-filtered `NavItem[]` the full `Sidebar` renders. */
617
+ items: NavItem[];
618
+ /** Current active route/path. */
619
+ pathname: string;
620
+ /** Navigation callback — receives a `NavItem.route`. */
621
+ onNavigate: (route: string) => void;
622
+ /** Localized accessibility label for the navigation landmark. */
623
+ regionLabel: string;
624
+ /** a11y hint for an item, given its label. Defaults to the label. */
625
+ navigateHint?: (label: string) => string;
626
+ /** Optional header slot rendered above the items (e.g. a home shortcut). */
627
+ header?: React.ReactNode;
628
+ /** Optional footer slot rendered after a flex spacer (dark-mode toggle, logout). */
629
+ footer?: React.ReactNode;
630
+ /** Extra container style overrides. */
631
+ containerStyle?: ViewStyle | ViewStyle[];
632
+ }
633
+ declare const CollapsedRail: ({ items, pathname, onNavigate, regionLabel, navigateHint, header, footer, containerStyle, }: CollapsedRailProps) => React.ReactElement;
634
+
635
+ /**
636
+ * PillNav — a horizontal row of fully-rounded "pills", the cross-navigation
637
+ * switcher promoted from kefi's coral dashboard nav. It renders the same
638
+ * caller-supplied `NavItem[]` as the other nav components, as pills that wrap,
639
+ * with the active route wearing the accent pill.
640
+ *
641
+ * It also folds in kefi's **visibility rule**: the whole nav hides (renders
642
+ * `null`) unless there are at least `minItems` entries — a single destination is
643
+ * not worth a switcher. Default `minItems` is 1 (always show when there is at
644
+ * least one item); kefi passes `2`.
645
+ *
646
+ * "Coral pills" is STYLE, not structure: the accent colour is the theme
647
+ * `palette.primary` (kefi's coral), the resting pill is `surfaceElevated`, and
648
+ * `containerStyle` / theme tokens carry any per-app tweak — no layout fork.
649
+ *
650
+ * Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM, router,
651
+ * icon-set, or store imports. Labels are pre-localized strings, icons are render
652
+ * slots, and every colour is read from the `@dloizides/ui-feedback` theme.
653
+ */
654
+
655
+ interface PillNavProps {
656
+ /** Nav entries — the cross-navigation destinations. */
657
+ items: NavItem[];
658
+ /** Current active route/path. */
659
+ pathname: string;
660
+ /** Navigation callback — receives a `NavItem.route`. */
661
+ onNavigate: (route: string) => void;
662
+ /** Localized accessibility label for the navigation landmark. */
663
+ regionLabel: string;
664
+ /** a11y hint for a pill, given its label. Defaults to the label. */
665
+ navigateHint?: (label: string) => string;
666
+ /**
667
+ * Hide the whole switcher unless there are at least this many items. Default 1
668
+ * (show when non-empty); kefi passes 2 (a lone dashboard needs no switcher).
669
+ */
670
+ minItems?: number;
671
+ /** Extra container style overrides. */
672
+ containerStyle?: ViewStyle | ViewStyle[];
673
+ }
674
+ declare const PillNav: ({ items, pathname, onNavigate, regionLabel, navigateHint, minItems, containerStyle, }: PillNavProps) => React.ReactElement | null;
675
+
676
+ /**
677
+ * DarkModeControl — the shared colour-scheme control promoted from erevna /
678
+ * katalogos (their Light / Dark / System toggle + the icon-only "cycle" button).
679
+ * It is a CONTROLLED component: the app owns the current `value` and applies the
680
+ * chosen scheme; this component only renders the choices and reports changes.
681
+ *
682
+ * Two variants encode the two behaviours the apps shipped:
683
+ * - `"segmented"` (default): a row of segments (Light / Dark / System), the
684
+ * active one highlighted — the desktop control.
685
+ * - `"cycle"`: a single icon+label button that advances to the NEXT option on
686
+ * each press (wrapping) — the compact mobile control.
687
+ *
688
+ * The options are pre-localized (`label` + `hint`) and value-agnostic (a string),
689
+ * so the app maps its own `DarkModePreference` enum to strings without this
690
+ * package importing it. Every colour flows through the `@dloizides/ui-feedback`
691
+ * theme; STYLE is the only per-app variation.
692
+ */
693
+
694
+ /** Which control shape to render. */
695
+ type DarkModeVariant = 'segmented' | 'cycle';
696
+ /** One pre-localized colour-scheme choice (e.g. Light / Dark / System). */
697
+ interface DarkModeOption {
698
+ /** App-defined value (its own preference enum mapped to a string). */
699
+ value: string;
700
+ /** Localized visible + accessible label. */
701
+ label: string;
702
+ /** Localized accessibility hint. */
703
+ hint: string;
704
+ /** Optional leading icon. Receives the resolved colour + size. */
705
+ renderIcon?: (color: string, size: number) => React.ReactNode;
706
+ }
707
+ interface DarkModeControlProps {
708
+ /** Current selected value — must match one of `options[].value`. */
709
+ value: string;
710
+ /** The pre-localized choices (typically Light / Dark / System). */
711
+ options: DarkModeOption[];
712
+ /** Change callback — receives the newly-selected value. */
713
+ onChange: (value: string) => void;
714
+ /** Localized accessibility label for the control group. */
715
+ regionLabel: string;
716
+ /** Control shape — a segmented row (default) or a single cycling button. */
717
+ variant?: DarkModeVariant;
718
+ /** testID for the control; segments are `${testID}-${value}`. */
719
+ testID?: string;
720
+ /** Extra container style overrides. */
721
+ containerStyle?: ViewStyle | ViewStyle[];
722
+ }
723
+ declare const DarkModeControl: ({ value, options, onChange, regionLabel, variant, testID, containerStyle, }: DarkModeControlProps) => React.ReactElement | null;
724
+
725
+ /**
726
+ * NavBarLink — a SINGLE horizontal `NavBar` link, extracted so each link owns its
727
+ * own hover/focus state (React hooks can't be per-item inside a map otherwise).
728
+ *
729
+ * It renders the v1 `.gn-links a` affordance, THEME-DRIVEN (no hard-coded colours
730
+ * beyond white ink on the accent pill):
731
+ * - rest → muted text (`colors.rest`), no background
732
+ * - hover → text brightens to full contrast (`colors.hoverText`) + a subtle
733
+ * rounded-pill background (`colors.hoverBg`, e.g. v1 `#16223a`)
734
+ * - active → solid accent pill (`colors.activeBg`) with white ink + aria-current
735
+ * - focus → a themed keyboard focus ring (`colors.ring`)
736
+ * Hover/focus are WEB-only (react-native-web fires `onHoverIn/Out`); on native
737
+ * those handlers never fire, so the link stays at its rest style.
738
+ */
739
+
740
+ /** Theme-resolved colours a link needs, computed once by `NavBar`. */
741
+ interface NavBarLinkColors {
742
+ /** Muted link text at rest. */
743
+ rest: string;
744
+ /** Brightened link text on hover. */
745
+ hoverText: string;
746
+ /** Subtle hover-pill background. */
747
+ hoverBg: string;
748
+ /** Active-pill background (accent). */
749
+ activeBg: string;
750
+ /** Focus-ring colour. */
751
+ ring: string;
752
+ }
753
+
754
+ /**
755
+ * NavOverflowMenu — the "More ▾" trigger + dropdown that holds the `NavBar` items
756
+ * which don't fit inline (the priority+ overflow set).
757
+ *
758
+ * It REUSES the shared `@dloizides/ui-layout` `ModalDropdown` — the exact component
759
+ * every other menu in the app uses — so the overflow menu is portalled, keyboard
760
+ * navigable, dismissed on outside-click / Escape, and opens on click, looking
761
+ * identical to the rest of the app's menus. The overflow entries are the same
762
+ * `NavItem`s (label + route + active state); selecting one navigates via the same
763
+ * `onNavigate` the inline links use.
764
+ *
765
+ * The trigger is a rounded pill matching a `NavBarLink`: muted at rest, and — when
766
+ * the ACTIVE route lives in the overflow set — it wears the accent pill so the
767
+ * active item stays visibly reachable behind the "…". The dropdown marks that same
768
+ * item selected (its `value` is the active overflow route). a11y: `ModalDropdown`
769
+ * owns the trigger's `role="button"` + `aria-expanded` + caller-supplied label/hint,
770
+ * and gives the options keyboard navigation.
771
+ *
772
+ * Contract discipline (as the rest of `@dloizides/ui-nav`): no FM / router / icon
773
+ * set / store; the label + hint are pre-localized strings from the caller and every
774
+ * colour comes from the theme (via the `NavBarLinkColors` the `NavBar` computed).
775
+ */
776
+
777
+ interface NavOverflowMenuProps {
778
+ /** The items that spilled out of the inline row. */
779
+ items: NavItem[];
780
+ /** Current active route/path — decides the active-pill + selected option. */
781
+ pathname: string;
782
+ /** Navigation callback — receives the chosen item's `route`. */
783
+ onNavigate: (route: string) => void;
784
+ /** Theme-resolved link colours (same set the inline links use). */
785
+ colors: NavBarLinkColors;
786
+ /** Localized visible + accessible label for the trigger (e.g. "More"). */
787
+ label: string;
788
+ /** Localized accessibility hint for the trigger. */
789
+ hint: string;
790
+ /** testID for the trigger (options default to `` `${testID}-option-${route}` ``). */
791
+ testID: string;
792
+ /** Force a dropdown variant (default: responsive — inline menu on desktop). */
793
+ variant?: DropdownVariant;
794
+ }
795
+ declare const NavOverflowMenu: ({ items, pathname, onNavigate, colors, label, hint, testID, variant, }: NavOverflowMenuProps) => React.ReactElement;
316
796
 
317
797
  /**
318
798
  * Default testIDs for `@dloizides/ui-nav`. Kept as a small central map (mirrors
@@ -334,6 +814,8 @@ declare const NAV_TEST_IDS: {
334
814
  readonly navBarToggle: "navbar-toggle";
335
815
  /** links container in the horizontal NavBar. */
336
816
  readonly navBarLinks: "navbar-links";
817
+ /** the "More ▾" priority+ overflow trigger in the horizontal NavBar (shown when items spill). */
818
+ readonly navBarOverflow: "navbar-overflow";
337
819
  };
338
820
  /** Suffixes appended to an `AppShell`'s required `testID` to name its regions. */
339
821
  declare const APP_SHELL_SUFFIX: {
@@ -342,6 +824,8 @@ declare const APP_SHELL_SUFFIX: {
342
824
  readonly nav: "-nav";
343
825
  /** The persistent left rail of the back-office layout (only when `sidebar` is supplied). */
344
826
  readonly sidebar: "-sidebar";
827
+ /** The intermediate persistent COLLAPSED (icon-only) rail (3-tier layout, tablet band). */
828
+ readonly collapsedSidebar: "-collapsed-sidebar";
345
829
  /** Mobile hamburger that opens the nav drawer (back-office layout, below the breakpoint). */
346
830
  readonly menuToggle: "-menu-toggle";
347
831
  /** The mobile nav drawer panel (holds the `sidebar` slot when opened on a narrow viewport). */
@@ -399,6 +883,13 @@ declare function roleRoutesToNavItems(routes: RoleRoute[], translate: (key: stri
399
883
  declare function accessibleNavItems(user: NavUser, table: RoleRouteTable, translate: (key: string) => string): NavItem[];
400
884
 
401
885
  declare const ACTIVE_BORDER_RADIUS = 4;
886
+ /**
887
+ * Horizontal gap (px) between adjacent inline items in the `NavBar` links row.
888
+ * Single source of truth: it is both the row's `columnGap` AND the gap the
889
+ * priority+ overflow fit ({@link computeVisibleCount}) reasons about, so the two
890
+ * can never drift apart.
891
+ */
892
+ declare const NAV_LINK_GAP = 4;
402
893
  declare const navStyles: {
403
894
  sidebarContainer: {
404
895
  width: number;
@@ -478,6 +969,96 @@ declare const navStyles: {
478
969
  fontWeight: "700";
479
970
  };
480
971
  };
972
+ declare const pillNavStyles: {
973
+ container: {
974
+ flexDirection: "row";
975
+ flexWrap: "wrap";
976
+ alignItems: "center";
977
+ columnGap: number;
978
+ rowGap: number;
979
+ paddingVertical: number;
980
+ };
981
+ pill: {
982
+ paddingHorizontal: number;
983
+ paddingVertical: number;
984
+ borderRadius: number;
985
+ minHeight: number;
986
+ flexDirection: "row";
987
+ alignItems: "center";
988
+ columnGap: number;
989
+ justifyContent: "center";
990
+ };
991
+ pillText: {
992
+ fontSize: number;
993
+ fontWeight: "600";
994
+ };
995
+ };
996
+ declare const darkModeStyles: {
997
+ segmentRow: {
998
+ flexDirection: "row";
999
+ alignItems: "center";
1000
+ borderWidth: number;
1001
+ borderRadius: number;
1002
+ overflow: "hidden";
1003
+ alignSelf: "flex-start";
1004
+ };
1005
+ segment: {
1006
+ paddingHorizontal: number;
1007
+ paddingVertical: number;
1008
+ minHeight: number;
1009
+ flexDirection: "row";
1010
+ alignItems: "center";
1011
+ columnGap: number;
1012
+ justifyContent: "center";
1013
+ };
1014
+ segmentText: {
1015
+ fontSize: number;
1016
+ fontWeight: "600";
1017
+ };
1018
+ cycle: {
1019
+ paddingHorizontal: number;
1020
+ paddingVertical: number;
1021
+ minHeight: number;
1022
+ minWidth: number;
1023
+ borderRadius: number;
1024
+ flexDirection: "row";
1025
+ alignItems: "center";
1026
+ columnGap: number;
1027
+ justifyContent: "center";
1028
+ };
1029
+ cycleText: {
1030
+ fontSize: number;
1031
+ fontWeight: "600";
1032
+ };
1033
+ };
1034
+ declare const collapsedRailStyles: {
1035
+ container: {
1036
+ width: number;
1037
+ paddingTop: number;
1038
+ paddingHorizontal: number;
1039
+ borderRightWidth: number;
1040
+ height: "100%";
1041
+ alignItems: "center";
1042
+ };
1043
+ item: {
1044
+ width: number;
1045
+ height: number;
1046
+ marginVertical: number;
1047
+ borderRadius: number;
1048
+ alignItems: "center";
1049
+ justifyContent: "center";
1050
+ };
1051
+ glyph: {
1052
+ fontSize: number;
1053
+ fontWeight: "700";
1054
+ };
1055
+ spacer: {
1056
+ flex: number;
1057
+ };
1058
+ slot: {
1059
+ alignItems: "center";
1060
+ };
1061
+ };
481
1062
  declare const expandableStyles: {
482
1063
  childItem: {
483
1064
  borderRadius: number;
@@ -521,4 +1102,4 @@ declare const NAV_ICON_SIZE = 14;
521
1102
  /** Chevron icon size for expandable sections. */
522
1103
  declare const CHEVRON_ICON_SIZE = 12;
523
1104
 
524
- export { ACTIVE_BORDER_RADIUS, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, NAV_ICON_SIZE, NAV_TEST_IDS, NavBar, type NavBarProps, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavUser, type ShellMessage, Sidebar, type SidebarProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, expandableStyles, isRouteActive, navStyles, resolveContentMaxWidth, roleRoutesToNavItems, useContentMaxWidth };
1105
+ export { ACTIVE_BORDER_RADIUS, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, DEFAULT_COLLAPSED_RAIL_MAX, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_TEST_IDS, Nav, NavBar, type NavBarProps, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavOrientation, NavOverflowMenu, type NavOverflowMenuProps, type NavProps, NavShell, type NavShellCollapsedRail, type NavShellLayout, type NavShellProps, type NavShellSideRail, type NavShellTopBar, type NavUser, PillNav, type PillNavProps, RAIL_FULL_BREAKPOINT, type RailMode, type ResolveRailModeArgs, type ShellMessage, Sidebar, type SidebarProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, isRouteActive, navStyles, pillNavStyles, resolveContentMaxWidth, resolveRailMode, roleRoutesToNavItems, useContentMaxWidth };