@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/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { ViewStyle } from 'react-native';
3
+ import { UiTheme } from '@dloizides/ui-feedback';
3
4
  import { DropdownVariant } from '@dloizides/ui-layout';
4
5
  import { resolveAccessibleRoutes, RoleRouteTable, RoleRoute } from '@dloizides/auth-web';
5
6
 
@@ -209,8 +210,23 @@ interface NavBarProps {
209
210
  overflowHint?: string;
210
211
  /** Extra container style overrides. */
211
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;
212
222
  }
213
- declare const NavBar: ({ items, pathname, onNavigate, regionLabel, navigateHint, brand, right, menuLabel, menuHint, renderMenuIcon, collapseBelow, overflowLabel, overflowHint, containerStyle, }: NavBarProps) => React.ReactElement;
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;
214
230
 
215
231
  /**
216
232
  * `Nav` — ONE component that renders EITHER the horizontal top bar (`NavBar`) or the
@@ -242,76 +258,24 @@ type NavProps = ({
242
258
  declare const Nav: (props: NavProps) => React.ReactElement;
243
259
 
244
260
  /**
245
- * NavBarLink a SINGLE horizontal `NavBar` link, extracted so each link owns its
246
- * own hover/focus state (React hooks can't be per-item inside a map otherwise).
247
- *
248
- * It renders the v1 `.gn-links a` affordance, THEME-DRIVEN (no hard-coded colours
249
- * beyond white ink on the accent pill):
250
- * - rest → muted text (`colors.rest`), no background
251
- * - hover → text brightens to full contrast (`colors.hoverText`) + a subtle
252
- * rounded-pill background (`colors.hoverBg`, e.g. v1 `#16223a`)
253
- * - active → solid accent pill (`colors.activeBg`) with white ink + aria-current
254
- * - focus → a themed keyboard focus ring (`colors.ring`)
255
- * Hover/focus are WEB-only (react-native-web fires `onHoverIn/Out`); on native
256
- * those handlers never fire, so the link stays at its rest style.
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`.
257
266
  */
258
267
 
259
- /** Theme-resolved colours a link needs, computed once by `NavBar`. */
260
- interface NavBarLinkColors {
261
- /** Muted link text at rest. */
262
- rest: string;
263
- /** Brightened link text on hover. */
264
- hoverText: string;
265
- /** Subtle hover-pill background. */
266
- hoverBg: string;
267
- /** Active-pill background (accent). */
268
- activeBg: string;
269
- /** Focus-ring colour. */
270
- ring: string;
268
+ /** A titled message card (error / forbidden). Colours come from the theme. */
269
+ interface ShellMessage {
270
+ titleText: string;
271
+ messageText: string;
271
272
  }
272
-
273
- /**
274
- * NavOverflowMenu — the "More ▾" trigger + dropdown that holds the `NavBar` items
275
- * which don't fit inline (the priority+ overflow set).
276
- *
277
- * It REUSES the shared `@dloizides/ui-layout` `ModalDropdown` — the exact component
278
- * every other menu in the app uses — so the overflow menu is portalled, keyboard
279
- * navigable, dismissed on outside-click / Escape, and opens on click, looking
280
- * identical to the rest of the app's menus. The overflow entries are the same
281
- * `NavItem`s (label + route + active state); selecting one navigates via the same
282
- * `onNavigate` the inline links use.
283
- *
284
- * The trigger is a rounded pill matching a `NavBarLink`: muted at rest, and — when
285
- * the ACTIVE route lives in the overflow set — it wears the accent pill so the
286
- * active item stays visibly reachable behind the "…". The dropdown marks that same
287
- * item selected (its `value` is the active overflow route). a11y: `ModalDropdown`
288
- * owns the trigger's `role="button"` + `aria-expanded` + caller-supplied label/hint,
289
- * and gives the options keyboard navigation.
290
- *
291
- * Contract discipline (as the rest of `@dloizides/ui-nav`): no FM / router / icon
292
- * set / store; the label + hint are pre-localized strings from the caller and every
293
- * colour comes from the theme (via the `NavBarLinkColors` the `NavBar` computed).
294
- */
295
-
296
- interface NavOverflowMenuProps {
297
- /** The items that spilled out of the inline row. */
298
- items: NavItem[];
299
- /** Current active route/path — decides the active-pill + selected option. */
300
- pathname: string;
301
- /** Navigation callback — receives the chosen item's `route`. */
302
- onNavigate: (route: string) => void;
303
- /** Theme-resolved link colours (same set the inline links use). */
304
- colors: NavBarLinkColors;
305
- /** Localized visible + accessible label for the trigger (e.g. "More"). */
306
- label: string;
307
- /** Localized accessibility hint for the trigger. */
308
- hint: string;
309
- /** testID for the trigger (options default to `` `${testID}-option-${route}` ``). */
310
- testID: string;
311
- /** Force a dropdown variant (default: responsive — inline menu on desktop). */
312
- variant?: DropdownVariant;
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;
313
278
  }
314
- declare const NavOverflowMenu: ({ items, pathname, onNavigate, colors, label, hint, testID, variant, }: NavOverflowMenuProps) => React.ReactElement;
315
279
 
316
280
  /**
317
281
  * MobileDrawer — the below-the-breakpoint face of `AppShell`'s back-office
@@ -340,6 +304,50 @@ interface DrawerLabels {
340
304
  closeHint: string;
341
305
  }
342
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
+
343
351
  /** Content-width policy: a capped column, optionally wider past a viewport breakpoint, or full-bleed. */
344
352
  type AppShellWidth = {
345
353
  max: number;
@@ -375,11 +383,6 @@ declare function useContentMaxWidth(width: AppShellWidth): number | 'full';
375
383
  * callbacks; every colour is routed through the `@dloizides/ui-feedback` theme.
376
384
  */
377
385
 
378
- /** A titled message card (error / forbidden). Colours come from the theme. */
379
- interface ShellMessage {
380
- titleText: string;
381
- messageText: string;
382
- }
383
386
  interface AppShellProps {
384
387
  /** App-supplied wired header (Topbar / AppHeader), rendered full-bleed. */
385
388
  header: React.ReactNode;
@@ -396,6 +399,21 @@ interface AppShellProps {
396
399
  * takes the full width. Desktop (≥768px) is unchanged. No effect without `sidebar`.
397
400
  */
398
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;
399
417
  /**
400
418
  * Pre-localized labels for the mobile nav drawer (only used in the `sidebar`
401
419
  * layout, below the breakpoint): the hamburger's open label/hint and the scrim's
@@ -421,16 +439,360 @@ interface AppShellProps {
421
439
  onRedirect: () => void;
422
440
  };
423
441
  /** Page state — loading spinner card, or a titled error / forbidden card. */
424
- state?: {
425
- loading?: boolean;
426
- error?: ShellMessage | null;
427
- forbidden?: ShellMessage | null;
428
- };
442
+ state?: ShellState;
429
443
  /** Root testID; the content region is exposed as `${testID}-content`. */
430
444
  testID: string;
431
445
  children: React.ReactNode;
432
446
  }
433
- 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;
434
796
 
435
797
  /**
436
798
  * Default testIDs for `@dloizides/ui-nav`. Kept as a small central map (mirrors
@@ -462,6 +824,8 @@ declare const APP_SHELL_SUFFIX: {
462
824
  readonly nav: "-nav";
463
825
  /** The persistent left rail of the back-office layout (only when `sidebar` is supplied). */
464
826
  readonly sidebar: "-sidebar";
827
+ /** The intermediate persistent COLLAPSED (icon-only) rail (3-tier layout, tablet band). */
828
+ readonly collapsedSidebar: "-collapsed-sidebar";
465
829
  /** Mobile hamburger that opens the nav drawer (back-office layout, below the breakpoint). */
466
830
  readonly menuToggle: "-menu-toggle";
467
831
  /** The mobile nav drawer panel (holds the `sidebar` slot when opened on a narrow viewport). */
@@ -605,6 +969,96 @@ declare const navStyles: {
605
969
  fontWeight: "700";
606
970
  };
607
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
+ };
608
1062
  declare const expandableStyles: {
609
1063
  childItem: {
610
1064
  borderRadius: number;
@@ -648,4 +1102,4 @@ declare const NAV_ICON_SIZE = 14;
648
1102
  /** Chevron icon size for expandable sections. */
649
1103
  declare const CHEVRON_ICON_SIZE = 12;
650
1104
 
651
- 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_LINK_GAP, NAV_TEST_IDS, Nav, NavBar, type NavBarProps, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavOrientation, NavOverflowMenu, type NavOverflowMenuProps, type NavProps, 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 };