@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/dist/index.d.ts 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,33 @@ interface NavBarProps {
209
210
  overflowHint?: string;
210
211
  /** Extra container style overrides. */
211
212
  containerStyle?: ViewStyle | ViewStyle[];
213
+ /**
214
+ * Optional content-column cap (px). When set, the OUTER container — which carries
215
+ * the background + bottom border (`barTheme`/theme surface + any `containerStyle`,
216
+ * e.g. the AML radial-glow appbar) — stays FULL WIDTH (full-bleed), while the INNER
217
+ * row (brand + links + right) is capped to this width and centred. Omit for the
218
+ * default where the inner spans the full container. The priority+ overflow "…" fit
219
+ * measures the (now capped) inner's rendered width via `onLayout`, so it keeps
220
+ * working against the capped column, not the full-bleed container.
221
+ */
222
+ contentMaxWidth?: number;
223
+ /**
224
+ * Optional CHROME theme override — re-provisions the `@dloizides/ui-feedback`
225
+ * theme for the bar ONLY, so the top bar can be dark while the page below keeps
226
+ * the light workspace theme (the AML v1-console "dark appbar over light content"
227
+ * pattern). The live `t` (+ `navigate`) from the surrounding provider are
228
+ * re-passed, so nav labels keep localizing. Omit for a bar that shares the page
229
+ * theme. Style-only: every colour still flows through the theme — no per-app fork.
230
+ */
231
+ barTheme?: UiTheme;
212
232
  }
213
- declare const NavBar: ({ items, pathname, onNavigate, regionLabel, navigateHint, brand, right, menuLabel, menuHint, renderMenuIcon, collapseBelow, overflowLabel, overflowHint, containerStyle, }: NavBarProps) => React.ReactElement;
233
+ /**
234
+ * `NavBar` — the horizontal top bar. When `barTheme` is supplied it re-provisions
235
+ * the `@dloizides/ui-feedback` theme around the bar (dark bar over a light page),
236
+ * re-passing the live `t` / `navigate` so labels keep localizing; otherwise it
237
+ * renders directly against the surrounding theme (byte-identical to before).
238
+ */
239
+ declare const NavBar: ({ barTheme, ...props }: NavBarProps) => React.ReactElement;
214
240
 
215
241
  /**
216
242
  * `Nav` — ONE component that renders EITHER the horizontal top bar (`NavBar`) or the
@@ -242,76 +268,24 @@ type NavProps = ({
242
268
  declare const Nav: (props: NavProps) => React.ReactElement;
243
269
 
244
270
  /**
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.
271
+ * AppShell content-body helpers the page-state cards (loading / error /
272
+ * forbidden) and the precedence rule that picks what renders inside the content
273
+ * column. Split out of `AppShell` so the shell file stays focused on layout /
274
+ * responsive composition. Colours are routed through the `@dloizides/ui-feedback`
275
+ * theme, same as the rest of `@dloizides/ui-nav`.
257
276
  */
258
277
 
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;
278
+ /** A titled message card (error / forbidden). Colours come from the theme. */
279
+ interface ShellMessage {
280
+ titleText: string;
281
+ messageText: string;
271
282
  }
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;
283
+ /** Page-state model — a loading spinner card, or a titled error / forbidden card. */
284
+ interface ShellState {
285
+ loading?: boolean;
286
+ error?: ShellMessage | null;
287
+ forbidden?: ShellMessage | null;
313
288
  }
314
- declare const NavOverflowMenu: ({ items, pathname, onNavigate, colors, label, hint, testID, variant, }: NavOverflowMenuProps) => React.ReactElement;
315
289
 
316
290
  /**
317
291
  * MobileDrawer — the below-the-breakpoint face of `AppShell`'s back-office
@@ -340,6 +314,50 @@ interface DrawerLabels {
340
314
  closeHint: string;
341
315
  }
342
316
 
317
+ /**
318
+ * Rail responsive mode — the pure (viewport → which rail face) rule behind
319
+ * `AppShell`'s optional THREE-tier layout, split out so it is directly
320
+ * unit-testable.
321
+ *
322
+ * Two-tier (default, back-compat): a persistent full rail at/above
323
+ * {@link RAIL_FULL_BREAKPOINT}, an overlay drawer below it. This is what every
324
+ * existing `AppShell` `sidebar` consumer already gets.
325
+ *
326
+ * Three-tier (opt-in — supply a `collapsedSidebar`): a persistent full rail on
327
+ * desktop, an intermediate persistent COLLAPSED (icon-only) rail on tablet, and
328
+ * the overlay drawer on a phone — the erevna / katalogos responsive scheme.
329
+ */
330
+ /** At/above this viewport width the persistent FULL rail shows (two-tier boundary). */
331
+ declare const RAIL_FULL_BREAKPOINT = 768;
332
+ /** Default upper bound of the collapsed-rail band (full rail at/above this). */
333
+ declare const DEFAULT_COLLAPSED_RAIL_MAX = 1024;
334
+ /** Which face the rail takes at a given viewport. */
335
+ type RailMode = 'none' | 'full' | 'collapsed' | 'drawer';
336
+ /**
337
+ * Viewport band in which the intermediate COLLAPSED icon-rail shows: `[min, max)`.
338
+ * At/above `max` the full rail shows; below `min` the overlay drawer shows.
339
+ */
340
+ interface CollapsedRailRange {
341
+ min: number;
342
+ max: number;
343
+ }
344
+ interface ResolveRailModeArgs {
345
+ viewport: number;
346
+ /** Whether a `sidebar` (full rail) node was supplied at all. */
347
+ hasSidebar: boolean;
348
+ /** Whether an intermediate `collapsedSidebar` node was supplied (enables 3-tier). */
349
+ hasCollapsed: boolean;
350
+ /** The collapsed-rail band; only consulted in 3-tier mode. */
351
+ range: CollapsedRailRange;
352
+ }
353
+ /**
354
+ * Resolve the rail mode. No sidebar ⇒ `'none'`. With a `collapsedSidebar`
355
+ * (3-tier): full at/above `range.max`, collapsed in `[range.min, range.max)`,
356
+ * drawer below `range.min`. Without one (2-tier, unchanged): full at/above
357
+ * {@link RAIL_FULL_BREAKPOINT}, drawer below.
358
+ */
359
+ declare function resolveRailMode({ viewport, hasSidebar, hasCollapsed, range }: ResolveRailModeArgs): RailMode;
360
+
343
361
  /** Content-width policy: a capped column, optionally wider past a viewport breakpoint, or full-bleed. */
344
362
  type AppShellWidth = {
345
363
  max: number;
@@ -375,11 +393,6 @@ declare function useContentMaxWidth(width: AppShellWidth): number | 'full';
375
393
  * callbacks; every colour is routed through the `@dloizides/ui-feedback` theme.
376
394
  */
377
395
 
378
- /** A titled message card (error / forbidden). Colours come from the theme. */
379
- interface ShellMessage {
380
- titleText: string;
381
- messageText: string;
382
- }
383
396
  interface AppShellProps {
384
397
  /** App-supplied wired header (Topbar / AppHeader), rendered full-bleed. */
385
398
  header: React.ReactNode;
@@ -396,6 +409,21 @@ interface AppShellProps {
396
409
  * takes the full width. Desktop (≥768px) is unchanged. No effect without `sidebar`.
397
410
  */
398
411
  sidebar?: React.ReactNode;
412
+ /**
413
+ * Optional intermediate COLLAPSED (icon-only) rail — opts the `sidebar` layout
414
+ * into a THREE-tier responsive scheme (erevna / katalogos): the full `sidebar`
415
+ * on desktop, this collapsed rail on a tablet-width band, and the overlay drawer
416
+ * (hosting the full `sidebar`) on a phone. Omit it and the layout stays the
417
+ * unchanged two-tier scheme (full rail ≥768px, drawer below). No effect without
418
+ * `sidebar`. Exposed as `${testID}${APP_SHELL_SUFFIX.collapsedSidebar}`.
419
+ */
420
+ collapsedSidebar?: React.ReactNode;
421
+ /**
422
+ * The viewport band `[min, max)` in which `collapsedSidebar` shows: at/above
423
+ * `max` the full rail shows, below `min` the drawer shows. Defaults to
424
+ * `{ min: 768, max: 1024 }`. Only consulted when `collapsedSidebar` is supplied.
425
+ */
426
+ collapsedRailRange?: CollapsedRailRange;
399
427
  /**
400
428
  * Pre-localized labels for the mobile nav drawer (only used in the `sidebar`
401
429
  * layout, below the breakpoint): the hamburger's open label/hint and the scrim's
@@ -421,16 +449,367 @@ interface AppShellProps {
421
449
  onRedirect: () => void;
422
450
  };
423
451
  /** 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
- };
452
+ state?: ShellState;
429
453
  /** Root testID; the content region is exposed as `${testID}-content`. */
430
454
  testID: string;
431
455
  children: React.ReactNode;
432
456
  }
433
- declare const AppShell: ({ header, nav, sidebar, mobileMenu, banner, width, contentPadding, chromeAlignment, gate, state, testID, children, }: AppShellProps) => React.ReactElement | null;
457
+ declare const AppShell: ({ header, nav, sidebar, collapsedSidebar, collapsedRailRange, mobileMenu, banner, width, contentPadding, chromeAlignment, gate, state, testID, children, }: AppShellProps) => React.ReactElement | null;
458
+
459
+ /**
460
+ * NavShell — the ONE first-class navigation shell that renders a **top bar**, a
461
+ * **side rail**, or **BOTH together**, chosen by a single `layout` prop
462
+ * (`'top' | 'side' | 'both'`). It is the shell-level answer to `Nav`'s
463
+ * single-axis question: where `Nav` returns one element (a `NavBar` OR a
464
+ * `Sidebar`), `NavShell` owns the whole page composition — it wires a top bar
465
+ * into `AppShell`'s `header` slot and a side rail into `AppShell`'s `sidebar`
466
+ * slot, and inherits every `AppShell` capability (auth gate, loading / error /
467
+ * forbidden state cards, width discipline, banner, the responsive
468
+ * hamburger + overlay drawer for the rail).
469
+ *
470
+ * layout="top" → a horizontal top bar only (brand + links + overflow "…" + right).
471
+ * layout="side" → a header + a persistent left rail of sections (the erevna / katalogos shape).
472
+ * layout="both" → a top bar (brand / global links + overflow "…" / user) AND a side rail
473
+ * (sections), together — the AML-console shape, now first-class.
474
+ *
475
+ * "both" is inherently a shell composition (a top region + a side region + a
476
+ * content column), which a single-element `Nav` cannot own; `NavShell` is that
477
+ * composition, so no app hand-wires `Topbar` + `Sidebar` + a `ScrollView` row
478
+ * again. Customizability is STYLE-only: every colour flows through the
479
+ * `@dloizides/ui-feedback` UiProvider theme, and brand / user / language /
480
+ * notification / section content are render slots. Structure + behaviour are
481
+ * shared.
482
+ *
483
+ * Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM, router,
484
+ * icon-set, or store imports. Labels are pre-localized strings, icons are render
485
+ * slots, and every colour is read from the theme.
486
+ */
487
+
488
+ /** Which regions the shell renders: a top bar, a side rail, or both together. */
489
+ type NavShellLayout = 'top' | 'side' | 'both';
490
+ /**
491
+ * Config for the TOP bar (rendered as a `NavBar` in `AppShell`'s header slot).
492
+ * Used when `layout` is `'top'` or `'both'`. Every field is optional except the
493
+ * items — an empty `items` array renders a bar with just the brand + right slots
494
+ * (a plain top bar with no nav links), which is the natural `'side'`-mode header.
495
+ */
496
+ interface NavShellTopBar {
497
+ /** Top-bar nav entries (global links). Empty ⇒ brand + right only. */
498
+ items: NavItem[];
499
+ /** Left slot — typically the tenant brand / logo. */
500
+ brand?: React.ReactNode;
501
+ /** Right slot — free-form (language toggle, notification bell, user chip, logout). */
502
+ right?: React.ReactNode;
503
+ /** Collapse the links behind a hamburger below this viewport width (default 760). */
504
+ collapseBelow?: number;
505
+ /** Localized visible + accessible label for the "More ▾" overflow trigger (default "More"). */
506
+ overflowLabel?: string;
507
+ /** Localized accessibility hint for the overflow trigger. */
508
+ overflowHint?: string;
509
+ /** Localized accessibility label for the responsive menu toggle. */
510
+ menuLabel?: string;
511
+ /** Localized accessibility hint for the responsive menu toggle. */
512
+ menuHint?: string;
513
+ /** Optional custom glyph renderer for the responsive toggle; defaults to ☰. */
514
+ renderMenuIcon?: (color: string, open: boolean) => React.ReactNode;
515
+ /**
516
+ * Extra container style overrides (e.g. an app-specific appbar background —
517
+ * including a web `background`/`backgroundImage` gradient cast through
518
+ * `ViewStyle`, the AML radial-glow appbar). Style-only.
519
+ */
520
+ containerStyle?: ViewStyle | ViewStyle[];
521
+ /**
522
+ * Optional content-column cap (px) for the top bar. When set, the bar's
523
+ * background + bottom border (from `containerStyle` / `barTheme`) stay FULL WIDTH
524
+ * (full-bleed) while the inner brand + links + right row is capped to this width
525
+ * and centred — the AML v1-console appbar shape. Forwarded to `NavBar.contentMaxWidth`.
526
+ */
527
+ contentMaxWidth?: number;
528
+ /**
529
+ * Optional CHROME theme override for the top bar only (dark bar over a light
530
+ * page). Forwarded to `NavBar.barTheme`. Style-only — see `NavBar`.
531
+ */
532
+ barTheme?: UiTheme;
533
+ }
534
+ /**
535
+ * Config for the intermediate COLLAPSED (icon-only) rail — opts the side layout
536
+ * into a THREE-tier responsive scheme (full rail → collapsed rail → drawer). When
537
+ * present, NavShell builds a `<CollapsedRail>` and hands it to `AppShell` for the
538
+ * tablet band; omit it and the layout stays two-tier.
539
+ */
540
+ interface NavShellCollapsedRail {
541
+ /** Items for the icon rail. Defaults to the full rail's `items`. */
542
+ items?: NavItem[];
543
+ /** Optional header slot (e.g. a home shortcut). */
544
+ header?: React.ReactNode;
545
+ /** Optional footer slot (dark-mode toggle, logout). */
546
+ footer?: React.ReactNode;
547
+ /** The `[min, max)` viewport band for the collapsed rail (default `{ min: 768, max: 1024 }`). */
548
+ range?: CollapsedRailRange;
549
+ }
550
+ /**
551
+ * Config for the SIDE rail (rendered as a `Sidebar` in `AppShell`'s sidebar
552
+ * slot). Used when `layout` is `'side'` or `'both'`.
553
+ */
554
+ interface NavShellSideRail {
555
+ /** Side-rail nav entries (sections — leaf + expandable). */
556
+ items: NavItem[];
557
+ /** Localized menu title (heading). */
558
+ title: string;
559
+ /** Optional header slot rendered above the items (e.g. a Home shortcut). */
560
+ header?: React.ReactNode;
561
+ /** Optional footer slot rendered after a flex spacer (dark-mode toggle, logout). */
562
+ footer?: React.ReactNode;
563
+ /** a11y hint shown when an expandable section is collapsed. */
564
+ expandHint?: string;
565
+ /** a11y hint shown when an expandable section is expanded. */
566
+ collapseHint?: string;
567
+ /** Optional chevron renderer for expandable sections. */
568
+ renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
569
+ /** Extra container style overrides. */
570
+ containerStyle?: ViewStyle | ViewStyle[];
571
+ /** Optional collapsed (icon-only) rail config → the intermediate tablet tier. */
572
+ collapsed?: NavShellCollapsedRail;
573
+ }
574
+ /**
575
+ * `AppShell` props NavShell forwards straight through. It composes the `header`
576
+ * and `sidebar` (+ `collapsedSidebar`) regions itself, so those are handled here;
577
+ * everything else — including the optional `nav` strip (e.g. a `PillNav`), the
578
+ * auth gate, state cards, banner, and width policy — passes through untouched.
579
+ */
580
+ type ForwardedShellProps = Omit<AppShellProps, 'header' | 'sidebar' | 'children'>;
581
+ interface NavShellProps extends ForwardedShellProps {
582
+ /** Which regions to render: a top bar, a side rail, or both. */
583
+ layout: NavShellLayout;
584
+ /** Current active route/path — drives active highlighting on both regions. */
585
+ pathname: string;
586
+ /** Navigation callback — receives a `NavItem.route`. */
587
+ onNavigate: (route: string) => void;
588
+ /** Localized accessibility label for the navigation landmark(s). */
589
+ regionLabel: string;
590
+ /** a11y hint for a nav item, given its label. Defaults to the label. */
591
+ navigateHint?: (label: string) => string;
592
+ /**
593
+ * Top-bar config → a `NavBar` in the header slot. Required for `'top'` /
594
+ * `'both'`; ignored for `'side'` unless supplied (a `'side'` app may still pass
595
+ * a `topBar` with empty `items` for its brand + user header).
596
+ */
597
+ topBar?: NavShellTopBar;
598
+ /**
599
+ * Escape hatch: a ready-made header node (e.g. an app-composed `<Topbar>`).
600
+ * When supplied it REPLACES the `topBar`-built `NavBar` — used by `'side'`
601
+ * apps that want the structured `Topbar` (language / notification / user
602
+ * slots) instead of a `NavBar` header.
603
+ */
604
+ header?: React.ReactNode;
605
+ /**
606
+ * Side-rail config → a `Sidebar` in the sidebar slot. Required for `'side'` /
607
+ * `'both'`; ignored for `'top'`.
608
+ */
609
+ sideRail?: NavShellSideRail;
610
+ children: React.ReactNode;
611
+ }
612
+ declare const NavShell: ({ layout, pathname, onNavigate, regionLabel, navigateHint, topBar, header, sideRail, children, collapsedSidebar: collapsedSidebarProp, collapsedRailRange: collapsedRailRangeProp, ...shellProps }: NavShellProps) => React.ReactElement | null;
613
+
614
+ /**
615
+ * CollapsedRail — the icon-only vertical rail for the intermediate (tablet) tier
616
+ * of `AppShell`'s three-tier responsive layout (the erevna / katalogos collapsed
617
+ * rail). It renders the SAME caller-supplied `NavItem[]` as `Sidebar`, but as a
618
+ * narrow strip of icon buttons: each item shows its `renderIcon` (or a first-letter
619
+ * glyph fallback), labelled for assistive tech, with the active route highlighted.
620
+ * Optional `header` / `footer` slots carry app chrome (a home shortcut, a
621
+ * dark-mode toggle, logout) top and bottom.
622
+ *
623
+ * A collapsed rail cannot expand a section inline, so pressing ANY item — leaf or
624
+ * parent — navigates to its `route`; an app that wants a parent to open the full
625
+ * drawer instead can special-case that in its `onNavigate`.
626
+ *
627
+ * Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM, router,
628
+ * icon-set, or store imports. Labels are pre-localized strings, icons are render
629
+ * slots, and every colour is read from the `@dloizides/ui-feedback` theme.
630
+ */
631
+
632
+ interface CollapsedRailProps {
633
+ /** Nav entries — the same role-filtered `NavItem[]` the full `Sidebar` renders. */
634
+ items: NavItem[];
635
+ /** Current active route/path. */
636
+ pathname: string;
637
+ /** Navigation callback — receives a `NavItem.route`. */
638
+ onNavigate: (route: string) => void;
639
+ /** Localized accessibility label for the navigation landmark. */
640
+ regionLabel: string;
641
+ /** a11y hint for an item, given its label. Defaults to the label. */
642
+ navigateHint?: (label: string) => string;
643
+ /** Optional header slot rendered above the items (e.g. a home shortcut). */
644
+ header?: React.ReactNode;
645
+ /** Optional footer slot rendered after a flex spacer (dark-mode toggle, logout). */
646
+ footer?: React.ReactNode;
647
+ /** Extra container style overrides. */
648
+ containerStyle?: ViewStyle | ViewStyle[];
649
+ }
650
+ declare const CollapsedRail: ({ items, pathname, onNavigate, regionLabel, navigateHint, header, footer, containerStyle, }: CollapsedRailProps) => React.ReactElement;
651
+
652
+ /**
653
+ * PillNav — a horizontal row of fully-rounded "pills", the cross-navigation
654
+ * switcher promoted from kefi's coral dashboard nav. It renders the same
655
+ * caller-supplied `NavItem[]` as the other nav components, as pills that wrap,
656
+ * with the active route wearing the accent pill.
657
+ *
658
+ * It also folds in kefi's **visibility rule**: the whole nav hides (renders
659
+ * `null`) unless there are at least `minItems` entries — a single destination is
660
+ * not worth a switcher. Default `minItems` is 1 (always show when there is at
661
+ * least one item); kefi passes `2`.
662
+ *
663
+ * "Coral pills" is STYLE, not structure: the accent colour is the theme
664
+ * `palette.primary` (kefi's coral), the resting pill is `surfaceElevated`, and
665
+ * `containerStyle` / theme tokens carry any per-app tweak — no layout fork.
666
+ *
667
+ * Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM, router,
668
+ * icon-set, or store imports. Labels are pre-localized strings, icons are render
669
+ * slots, and every colour is read from the `@dloizides/ui-feedback` theme.
670
+ */
671
+
672
+ interface PillNavProps {
673
+ /** Nav entries — the cross-navigation destinations. */
674
+ items: NavItem[];
675
+ /** Current active route/path. */
676
+ pathname: string;
677
+ /** Navigation callback — receives a `NavItem.route`. */
678
+ onNavigate: (route: string) => void;
679
+ /** Localized accessibility label for the navigation landmark. */
680
+ regionLabel: string;
681
+ /** a11y hint for a pill, given its label. Defaults to the label. */
682
+ navigateHint?: (label: string) => string;
683
+ /**
684
+ * Hide the whole switcher unless there are at least this many items. Default 1
685
+ * (show when non-empty); kefi passes 2 (a lone dashboard needs no switcher).
686
+ */
687
+ minItems?: number;
688
+ /** Extra container style overrides. */
689
+ containerStyle?: ViewStyle | ViewStyle[];
690
+ }
691
+ declare const PillNav: ({ items, pathname, onNavigate, regionLabel, navigateHint, minItems, containerStyle, }: PillNavProps) => React.ReactElement | null;
692
+
693
+ /**
694
+ * DarkModeControl — the shared colour-scheme control promoted from erevna /
695
+ * katalogos (their Light / Dark / System toggle + the icon-only "cycle" button).
696
+ * It is a CONTROLLED component: the app owns the current `value` and applies the
697
+ * chosen scheme; this component only renders the choices and reports changes.
698
+ *
699
+ * Two variants encode the two behaviours the apps shipped:
700
+ * - `"segmented"` (default): a row of segments (Light / Dark / System), the
701
+ * active one highlighted — the desktop control.
702
+ * - `"cycle"`: a single icon+label button that advances to the NEXT option on
703
+ * each press (wrapping) — the compact mobile control.
704
+ *
705
+ * The options are pre-localized (`label` + `hint`) and value-agnostic (a string),
706
+ * so the app maps its own `DarkModePreference` enum to strings without this
707
+ * package importing it. Every colour flows through the `@dloizides/ui-feedback`
708
+ * theme; STYLE is the only per-app variation.
709
+ */
710
+
711
+ /** Which control shape to render. */
712
+ type DarkModeVariant = 'segmented' | 'cycle';
713
+ /** One pre-localized colour-scheme choice (e.g. Light / Dark / System). */
714
+ interface DarkModeOption {
715
+ /** App-defined value (its own preference enum mapped to a string). */
716
+ value: string;
717
+ /** Localized visible + accessible label. */
718
+ label: string;
719
+ /** Localized accessibility hint. */
720
+ hint: string;
721
+ /** Optional leading icon. Receives the resolved colour + size. */
722
+ renderIcon?: (color: string, size: number) => React.ReactNode;
723
+ }
724
+ interface DarkModeControlProps {
725
+ /** Current selected value — must match one of `options[].value`. */
726
+ value: string;
727
+ /** The pre-localized choices (typically Light / Dark / System). */
728
+ options: DarkModeOption[];
729
+ /** Change callback — receives the newly-selected value. */
730
+ onChange: (value: string) => void;
731
+ /** Localized accessibility label for the control group. */
732
+ regionLabel: string;
733
+ /** Control shape — a segmented row (default) or a single cycling button. */
734
+ variant?: DarkModeVariant;
735
+ /** testID for the control; segments are `${testID}-${value}`. */
736
+ testID?: string;
737
+ /** Extra container style overrides. */
738
+ containerStyle?: ViewStyle | ViewStyle[];
739
+ }
740
+ declare const DarkModeControl: ({ value, options, onChange, regionLabel, variant, testID, containerStyle, }: DarkModeControlProps) => React.ReactElement | null;
741
+
742
+ /**
743
+ * NavBarLink — a SINGLE horizontal `NavBar` link, extracted so each link owns its
744
+ * own hover/focus state (React hooks can't be per-item inside a map otherwise).
745
+ *
746
+ * It renders the v1 `.gn-links a` affordance, THEME-DRIVEN (no hard-coded colours
747
+ * beyond white ink on the accent pill):
748
+ * - rest → muted text (`colors.rest`), no background
749
+ * - hover → text brightens to full contrast (`colors.hoverText`) + a subtle
750
+ * rounded-pill background (`colors.hoverBg`, e.g. v1 `#16223a`)
751
+ * - active → solid accent pill (`colors.activeBg`) with white ink + aria-current
752
+ * - focus → a themed keyboard focus ring (`colors.ring`)
753
+ * Hover/focus are WEB-only (react-native-web fires `onHoverIn/Out`); on native
754
+ * those handlers never fire, so the link stays at its rest style.
755
+ */
756
+
757
+ /** Theme-resolved colours a link needs, computed once by `NavBar`. */
758
+ interface NavBarLinkColors {
759
+ /** Muted link text at rest. */
760
+ rest: string;
761
+ /** Brightened link text on hover. */
762
+ hoverText: string;
763
+ /** Subtle hover-pill background. */
764
+ hoverBg: string;
765
+ /** Active-pill background (accent). */
766
+ activeBg: string;
767
+ /** Focus-ring colour. */
768
+ ring: string;
769
+ }
770
+
771
+ /**
772
+ * NavOverflowMenu — the "More ▾" trigger + dropdown that holds the `NavBar` items
773
+ * which don't fit inline (the priority+ overflow set).
774
+ *
775
+ * It REUSES the shared `@dloizides/ui-layout` `ModalDropdown` — the exact component
776
+ * every other menu in the app uses — so the overflow menu is portalled, keyboard
777
+ * navigable, dismissed on outside-click / Escape, and opens on click, looking
778
+ * identical to the rest of the app's menus. The overflow entries are the same
779
+ * `NavItem`s (label + route + active state); selecting one navigates via the same
780
+ * `onNavigate` the inline links use.
781
+ *
782
+ * The trigger is a rounded pill matching a `NavBarLink`: muted at rest, and — when
783
+ * the ACTIVE route lives in the overflow set — it wears the accent pill so the
784
+ * active item stays visibly reachable behind the "…". The dropdown marks that same
785
+ * item selected (its `value` is the active overflow route). a11y: `ModalDropdown`
786
+ * owns the trigger's `role="button"` + `aria-expanded` + caller-supplied label/hint,
787
+ * and gives the options keyboard navigation.
788
+ *
789
+ * Contract discipline (as the rest of `@dloizides/ui-nav`): no FM / router / icon
790
+ * set / store; the label + hint are pre-localized strings from the caller and every
791
+ * colour comes from the theme (via the `NavBarLinkColors` the `NavBar` computed).
792
+ */
793
+
794
+ interface NavOverflowMenuProps {
795
+ /** The items that spilled out of the inline row. */
796
+ items: NavItem[];
797
+ /** Current active route/path — decides the active-pill + selected option. */
798
+ pathname: string;
799
+ /** Navigation callback — receives the chosen item's `route`. */
800
+ onNavigate: (route: string) => void;
801
+ /** Theme-resolved link colours (same set the inline links use). */
802
+ colors: NavBarLinkColors;
803
+ /** Localized visible + accessible label for the trigger (e.g. "More"). */
804
+ label: string;
805
+ /** Localized accessibility hint for the trigger. */
806
+ hint: string;
807
+ /** testID for the trigger (options default to `` `${testID}-option-${route}` ``). */
808
+ testID: string;
809
+ /** Force a dropdown variant (default: responsive — inline menu on desktop). */
810
+ variant?: DropdownVariant;
811
+ }
812
+ declare const NavOverflowMenu: ({ items, pathname, onNavigate, colors, label, hint, testID, variant, }: NavOverflowMenuProps) => React.ReactElement;
434
813
 
435
814
  /**
436
815
  * Default testIDs for `@dloizides/ui-nav`. Kept as a small central map (mirrors
@@ -462,6 +841,8 @@ declare const APP_SHELL_SUFFIX: {
462
841
  readonly nav: "-nav";
463
842
  /** The persistent left rail of the back-office layout (only when `sidebar` is supplied). */
464
843
  readonly sidebar: "-sidebar";
844
+ /** The intermediate persistent COLLAPSED (icon-only) rail (3-tier layout, tablet band). */
845
+ readonly collapsedSidebar: "-collapsed-sidebar";
465
846
  /** Mobile hamburger that opens the nav drawer (back-office layout, below the breakpoint). */
466
847
  readonly menuToggle: "-menu-toggle";
467
848
  /** The mobile nav drawer panel (holds the `sidebar` slot when opened on a narrow viewport). */
@@ -605,6 +986,96 @@ declare const navStyles: {
605
986
  fontWeight: "700";
606
987
  };
607
988
  };
989
+ declare const pillNavStyles: {
990
+ container: {
991
+ flexDirection: "row";
992
+ flexWrap: "wrap";
993
+ alignItems: "center";
994
+ columnGap: number;
995
+ rowGap: number;
996
+ paddingVertical: number;
997
+ };
998
+ pill: {
999
+ paddingHorizontal: number;
1000
+ paddingVertical: number;
1001
+ borderRadius: number;
1002
+ minHeight: number;
1003
+ flexDirection: "row";
1004
+ alignItems: "center";
1005
+ columnGap: number;
1006
+ justifyContent: "center";
1007
+ };
1008
+ pillText: {
1009
+ fontSize: number;
1010
+ fontWeight: "600";
1011
+ };
1012
+ };
1013
+ declare const darkModeStyles: {
1014
+ segmentRow: {
1015
+ flexDirection: "row";
1016
+ alignItems: "center";
1017
+ borderWidth: number;
1018
+ borderRadius: number;
1019
+ overflow: "hidden";
1020
+ alignSelf: "flex-start";
1021
+ };
1022
+ segment: {
1023
+ paddingHorizontal: number;
1024
+ paddingVertical: number;
1025
+ minHeight: number;
1026
+ flexDirection: "row";
1027
+ alignItems: "center";
1028
+ columnGap: number;
1029
+ justifyContent: "center";
1030
+ };
1031
+ segmentText: {
1032
+ fontSize: number;
1033
+ fontWeight: "600";
1034
+ };
1035
+ cycle: {
1036
+ paddingHorizontal: number;
1037
+ paddingVertical: number;
1038
+ minHeight: number;
1039
+ minWidth: number;
1040
+ borderRadius: number;
1041
+ flexDirection: "row";
1042
+ alignItems: "center";
1043
+ columnGap: number;
1044
+ justifyContent: "center";
1045
+ };
1046
+ cycleText: {
1047
+ fontSize: number;
1048
+ fontWeight: "600";
1049
+ };
1050
+ };
1051
+ declare const collapsedRailStyles: {
1052
+ container: {
1053
+ width: number;
1054
+ paddingTop: number;
1055
+ paddingHorizontal: number;
1056
+ borderRightWidth: number;
1057
+ height: "100%";
1058
+ alignItems: "center";
1059
+ };
1060
+ item: {
1061
+ width: number;
1062
+ height: number;
1063
+ marginVertical: number;
1064
+ borderRadius: number;
1065
+ alignItems: "center";
1066
+ justifyContent: "center";
1067
+ };
1068
+ glyph: {
1069
+ fontSize: number;
1070
+ fontWeight: "700";
1071
+ };
1072
+ spacer: {
1073
+ flex: number;
1074
+ };
1075
+ slot: {
1076
+ alignItems: "center";
1077
+ };
1078
+ };
608
1079
  declare const expandableStyles: {
609
1080
  childItem: {
610
1081
  borderRadius: number;
@@ -648,4 +1119,4 @@ declare const NAV_ICON_SIZE = 14;
648
1119
  /** Chevron icon size for expandable sections. */
649
1120
  declare const CHEVRON_ICON_SIZE = 12;
650
1121
 
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 };
1122
+ 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 };