@dloizides/ui-nav 1.16.1 → 1.18.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/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.18.0
4
+
5
+ Root-cause fix for sidebar leaf co-activation. Behavioral/additive — no breaking API.
6
+
7
+ - **Fix (two sidebar leaves showed `aria-current="page"` at once).** On the live finreg
8
+ `/accounting/journals`, BOTH "Chart of Accounts" (route `/accounting`) and "Journals"
9
+ (route `/accounting/journals`) were marked active, because the active predicate was purely
10
+ prefix-based (`pathname === route || pathname.startsWith(route + '/')`) — a module-home leaf
11
+ routed at `/accounting` is a prefix of every `/accounting/*` sibling, so it co-activated on all
12
+ of them. (Second occurrence of this class; the first, CRM Contacts at `/crm`, was worked around
13
+ by moving the route.) Now resolution is **most-specific (longest) match wins**: among a nav
14
+ model's LEAF items, at most ONE is active for any path — the leaf whose route is the longest
15
+ prefix match. On `/accounting/journals` only Journals is active; on `/accounting` exactly only
16
+ Chart of Accounts is. For nav models with no parent/child route overlap (the common case)
17
+ behaviour is unchanged.
18
+ - **New export** `resolveActiveRoute(items, pathname)`: flattens the tree to its leaf routes and
19
+ returns the single longest matching route (honouring per-leaf `exact`), or `undefined`.
20
+ - `Sidebar` resolves the winning route once over the full leaf set and passes `activeRoute` to
21
+ each row; `NavExpandableItem` marks a leaf active iff `item.route === activeRoute` (falls back
22
+ to the per-item predicate when used standalone — backward compatible). Group HEADER expansion
23
+ (`hasActiveDescendant`) is a separate concern and is unchanged.
24
+ - The same winner-based resolution now backs `NavBar`, `PillNav`, `CollapsedRail` and
25
+ `NavOverflowMenu`, so the latent prefix co-activation cannot resurface in the horizontal navs.
26
+ - **Additive API**: new optional `NavItem.exact?: boolean` — a leaf active ONLY on its exact
27
+ path (never a nested one). A complementary escape hatch; the longest-match default is the real
28
+ fix, so no consumer needs to set it.
29
+ - `isRouteActive(pathname, route, exact?)` gains an optional third arg; the two-arg call is
30
+ unchanged.
31
+
32
+ ## 1.17.0
33
+
34
+ Two live-defect fixes to the shared sidebar chrome. Behavioral/additive — no breaking API.
35
+
36
+ - **Fix (two search bars on desktop → mutually exclusive by affordance).** 1.16.1 shipped the
37
+ desktop inline `SidebarSearch` field, but the ⌘K `CommandPaletteTrigger` could be present at
38
+ the same time, so a wide viewport showed BOTH. `Sidebar` now owns both affordances and renders
39
+ exactly ONE, chosen by its own viewport width via `resolveSearchAffordance`:
40
+ - inline affordance (≥768) → the inline field ONLY; the ⌘K trigger is suppressed.
41
+ - palette affordance (<768) → the ⌘K trigger ONLY; the inline field is suppressed.
42
+ - The ⌘K keyboard shortcut (`useCommandPaletteHotkey`) is untouched and stays bound at every width.
43
+ - **Additive API**: new `Sidebar.paletteTrigger?: SidebarPaletteTriggerConfig`
44
+ (`{ label, hint, shortcut, onOpen, testID? }`), forwarded from `NavShellSideRail.paletteTrigger`.
45
+ Consumers that wire neither, or only the inline field, are unchanged (the inline field simply
46
+ no longer renders below 768, where a persistent field would steal vertical space).
47
+
48
+ - **Fix (hover wash never appeared on RN-web).** The nav rows were `TouchableOpacity`, which in
49
+ react-native-web has NO hover support and silently drops `onHoverIn`/`onHoverOut` — so the row's
50
+ `hovered` state never toggled and the primary-tinted overlay stayed at opacity 0. The leaf and
51
+ group-header rows in `NavExpandableItem` are now `Pressable`, whose `useHover` responder actually
52
+ drives `hovered`, lifting the token-tinted overlay to the hover wash (`HOVER_TINT_OPACITY`, ~160ms
53
+ reduced-motion-gated transition) — distinct from and lighter than the active tint. Token-only
54
+ (colours via the theme; no raw hex/rgb/hsl). aria wiring (`aria-current` / `aria-expanded`, which
55
+ `MobileDrawer` depends on) is preserved across the switch.
56
+
3
57
  ## 1.16.1
4
58
 
5
59
  - **Fix (NavShell forwards inline-search props to the inner `Sidebar`).** 1.16.0 added
package/dist/index.d.mts CHANGED
@@ -5,12 +5,71 @@ import { DropdownVariant } from '@dloizides/ui-layout';
5
5
  import { resolveAccessibleRoutes, RoleRouteTable, RoleRoute } from '@dloizides/auth-web';
6
6
 
7
7
  /**
8
- * Active-route matcher shared by the sidebar entries. Ported verbatim from the
9
- * twin app sidebars: an item is active when the current pathname equals its
10
- * route or is nested under it (`/foo` matches `/foo` and `/foo/bar`, but `/`
11
- * only matches `/`).
8
+ * Public prop types for the `@dloizides/ui-nav` config-driven navigation shell.
9
+ *
10
+ * The rendering chrome (sidebar + topbar) was byte-identical across erevna-web
11
+ * and katalogos-web; only the *item data* (which routes, how they group) differs
12
+ * per app. So this package renders a caller-supplied `NavItem[]` — labels are
13
+ * pre-localized strings and icons are render slots, keeping the package free of
14
+ * any app's i18n helper, icon set, router, or store.
12
15
  */
13
- declare function isRouteActive(pathname: string, route: string): boolean;
16
+
17
+ /** One navigation entry. Labels are already localized by the caller. */
18
+ interface NavItem {
19
+ /** Stable key + default testID. */
20
+ key: string;
21
+ /** Localized display label. */
22
+ label: string;
23
+ /** Route/path this item navigates to (passed back to `onNavigate`). */
24
+ route: string;
25
+ /** Optional testID override (defaults to `key`). */
26
+ testID?: string;
27
+ /**
28
+ * Optional leading icon. Receives the resolved foreground colour and a size,
29
+ * so the app supplies its own icon component without this package importing
30
+ * an icon set.
31
+ */
32
+ renderIcon?: (color: string, size: number) => React.ReactNode;
33
+ /** Optional nested items — rendered as an expandable section. */
34
+ children?: NavItem[];
35
+ /**
36
+ * Opt this leaf out of prefix matching: when `true` it is active ONLY when the
37
+ * pathname equals its route exactly (never for a nested `route/child` path). A
38
+ * complementary escape hatch — the default most-specific/longest-match resolution
39
+ * (see `resolveActiveRoute`) already prevents a module-home leaf like `/accounting`
40
+ * from co-activating with `/accounting/journals`, so most consumers never need
41
+ * this. Reach for it only when a leaf must NEVER light up on any descendant path
42
+ * even when no longer sibling route exists.
43
+ */
44
+ exact?: boolean;
45
+ }
46
+
47
+ /**
48
+ * Active-route matching for the nav shells.
49
+ *
50
+ * `isRouteActive` is the low-level per-route predicate (prefix-based, ported
51
+ * verbatim from the twin app sidebars): a route is active when the current
52
+ * pathname equals it or is nested under it (`/foo` matches `/foo` and `/foo/bar`,
53
+ * but `/` only matches `/`). With `exact`, only an exact path matches.
54
+ *
55
+ * `resolveActiveRoute` is the CROSS-ITEM resolver that fixes the sidebar
56
+ * co-activation bug: because a module-home leaf (`/accounting`) is a prefix of
57
+ * every sibling (`/accounting/journals`), the naive per-item predicate lights up
58
+ * BOTH on `/accounting/journals`. `resolveActiveRoute` instead returns the ONE
59
+ * most-specific (longest) leaf route that matches — so at most one leaf is ever
60
+ * active, the longest wins on nested paths, and the module-home leaf is active
61
+ * only on its own exact path. For a nav model with no parent/child route overlap
62
+ * (the common case) this is identical to the old behaviour.
63
+ */
64
+
65
+ declare function isRouteActive(pathname: string, route: string, exact?: boolean): boolean;
66
+ /**
67
+ * The single active leaf route for `pathname`: the LONGEST leaf route that
68
+ * matches (most-specific wins). Returns `undefined` when no leaf matches. Marking
69
+ * a leaf active iff `item.route === resolveActiveRoute(items, pathname)` guarantees
70
+ * at most one active leaf — the whack-a-mole prefix co-activation fix.
71
+ */
72
+ declare function resolveActiveRoute(items: NavItem[], pathname: string): string | undefined;
14
73
 
15
74
  /**
16
75
  * SidebarSearch — the persistent, DESKTOP inline search field that lives at the
@@ -51,36 +110,6 @@ interface SidebarSearchProps {
51
110
  /** The inline sidebar search field (desktop) with a clear button. */
52
111
  declare const SidebarSearch: ({ value, onChangeText, onClear, labels, testID, }: SidebarSearchProps) => React.ReactElement;
53
112
 
54
- /**
55
- * Public prop types for the `@dloizides/ui-nav` config-driven navigation shell.
56
- *
57
- * The rendering chrome (sidebar + topbar) was byte-identical across erevna-web
58
- * and katalogos-web; only the *item data* (which routes, how they group) differs
59
- * per app. So this package renders a caller-supplied `NavItem[]` — labels are
60
- * pre-localized strings and icons are render slots, keeping the package free of
61
- * any app's i18n helper, icon set, router, or store.
62
- */
63
-
64
- /** One navigation entry. Labels are already localized by the caller. */
65
- interface NavItem {
66
- /** Stable key + default testID. */
67
- key: string;
68
- /** Localized display label. */
69
- label: string;
70
- /** Route/path this item navigates to (passed back to `onNavigate`). */
71
- route: string;
72
- /** Optional testID override (defaults to `key`). */
73
- testID?: string;
74
- /**
75
- * Optional leading icon. Receives the resolved foreground colour and a size,
76
- * so the app supplies its own icon component without this package importing
77
- * an icon set.
78
- */
79
- renderIcon?: (color: string, size: number) => React.ReactNode;
80
- /** Optional nested items — rendered as an expandable section. */
81
- children?: NavItem[];
82
- }
83
-
84
113
  /**
85
114
  * Sidebar — the config-driven left navigation shell promoted from the
86
115
  * byte-identical erevna-web / katalogos-web `Sidebar`. It renders a caller
@@ -100,6 +129,24 @@ interface SidebarSearchConfig extends SidebarSearchLabels {
100
129
  /** Shown in place of the list when the query matches nothing. */
101
130
  emptyText: string;
102
131
  }
132
+ /**
133
+ * The ⌘K command-palette trigger the sidebar renders as its NARROW-viewport search
134
+ * affordance (a persistent inline field would steal scarce vertical space on mobile
135
+ * — see {@link resolveSearchAffordance}). Pre-localized strings + the open handler;
136
+ * wire `onOpen` to the same handler as the ⌘K hotkey ({@link useCommandPaletteHotkey}).
137
+ */
138
+ interface SidebarPaletteTriggerConfig {
139
+ /** Placeholder-style label, e.g. "Search". */
140
+ label: string;
141
+ /** a11y hint describing what activating it does. */
142
+ hint: string;
143
+ /** The shortcut badge text, e.g. "⌘K". */
144
+ shortcut: string;
145
+ /** Open the palette (same handler as the ⌘K hotkey). */
146
+ onOpen: () => void;
147
+ /** testID for the trigger. Defaults to `sidebar-command-palette-trigger`. */
148
+ testID?: string;
149
+ }
103
150
  interface SidebarProps {
104
151
  /** Nav entries — already role-filtered / grouped by the app. */
105
152
  items: NavItem[];
@@ -121,12 +168,21 @@ interface SidebarProps {
121
168
  renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
122
169
  /**
123
170
  * Opt into the persistent inline search field (desktop). Requires `search`
124
- * labels; when omitted the sidebar renders exactly as before. Prefer this on
125
- * the wide/full rail and the ⌘K palette on the mobile drawer.
171
+ * labels; when omitted the sidebar renders exactly as before. The field shows
172
+ * ONLY at the inline affordance (viewport 768, {@link resolveSearchAffordance});
173
+ * on a narrow viewport the ⌘K `paletteTrigger` is the search affordance instead.
126
174
  */
127
175
  enableInlineSearch?: boolean;
128
176
  /** Pre-localized inline-search strings — required for `enableInlineSearch`. */
129
177
  search?: SidebarSearchConfig;
178
+ /**
179
+ * Opt into the ⌘K {@link CommandPaletteTrigger} as the NARROW-viewport search
180
+ * affordance. Rendered ONLY at the palette affordance (viewport < 768); on a wide
181
+ * viewport it is suppressed so the inline field and the trigger are never both
182
+ * shown — they are mutually exclusive by affordance. The ⌘K hotkey stays bound at
183
+ * every width independently ({@link useCommandPaletteHotkey}).
184
+ */
185
+ paletteTrigger?: SidebarPaletteTriggerConfig;
130
186
  /** Controlled search query (optional — the sidebar owns the state when omitted). */
131
187
  searchQuery?: string;
132
188
  /** Called on each query change (with either controlled or internal state). */
@@ -138,7 +194,7 @@ interface SidebarProps {
138
194
  /** Extra container style overrides. */
139
195
  containerStyle?: ViewStyle | ViewStyle[];
140
196
  }
141
- declare const Sidebar: ({ items, pathname, onNavigate, title, regionLabel, navigateHint, expandHint, collapseHint, renderChevron, enableInlineSearch, search, searchQuery, onSearchChange, header, footer, containerStyle, }: SidebarProps) => React.ReactElement;
197
+ declare const Sidebar: ({ items, pathname, onNavigate, title, regionLabel, navigateHint, expandHint, collapseHint, renderChevron, enableInlineSearch, search, paletteTrigger, searchQuery, onSearchChange, header, footer, containerStyle, }: SidebarProps) => React.ReactElement;
142
198
 
143
199
  /**
144
200
  * SidebarChevron — the built-in disclosure caret for an expandable sidebar
@@ -737,6 +793,12 @@ interface NavShellSideRail {
737
793
  enableInlineSearch?: boolean;
738
794
  /** Pre-localized inline-search strings — required for `enableInlineSearch`. Forwarded to `Sidebar.search`. */
739
795
  search?: SidebarSearchConfig;
796
+ /**
797
+ * Opt the inner `Sidebar` into the ⌘K palette trigger as its narrow-viewport
798
+ * search affordance. Mutually exclusive with the inline field by viewport width
799
+ * (never both shown). Forwarded to `Sidebar.paletteTrigger`.
800
+ */
801
+ paletteTrigger?: SidebarPaletteTriggerConfig;
740
802
  /** Controlled search query (optional — the Sidebar owns the state when omitted). Forwarded to `Sidebar.searchQuery`. */
741
803
  searchQuery?: string;
742
804
  /** Called on each query change (controlled or internal). Forwarded to `Sidebar.onSearchChange`. */
@@ -1047,6 +1109,15 @@ interface NavExpandableItemProps {
1047
1109
  item: NavItem;
1048
1110
  pathname: string;
1049
1111
  onNavigate: (route: string) => void;
1112
+ /**
1113
+ * The single winning active leaf route for the whole nav model, resolved once by
1114
+ * the parent via `resolveActiveRoute` (most-specific/longest match wins). A leaf
1115
+ * is active iff `item.route === activeRoute`, so at most one leaf ever carries
1116
+ * `aria-current="page"` — the fix for `/accounting` co-activating with
1117
+ * `/accounting/journals`. When omitted (standalone use) each leaf falls back to
1118
+ * the per-item prefix predicate, preserving the pre-1.18 behaviour.
1119
+ */
1120
+ activeRoute?: string;
1050
1121
  /** a11y hint for a leaf item, given its label. */
1051
1122
  navigateHint: (label: string) => string;
1052
1123
  /** a11y hint shown when the section is collapsed (press expands). */
@@ -1061,7 +1132,7 @@ interface NavExpandableItemProps {
1061
1132
  renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
1062
1133
  depth?: number;
1063
1134
  }
1064
- declare const NavExpandableItem: ({ item, pathname, onNavigate, navigateHint, expandHint, collapseHint, renderChevron, depth, }: NavExpandableItemProps) => React.ReactElement;
1135
+ declare const NavExpandableItem: ({ item, pathname, onNavigate, navigateHint, expandHint, collapseHint, renderChevron, activeRoute, depth, }: NavExpandableItemProps) => React.ReactElement;
1065
1136
 
1066
1137
  /**
1067
1138
  * Pure matching logic for the {@link CommandPalette}. Kept framework-free so the
@@ -1233,6 +1304,9 @@ declare const navStyles: {
1233
1304
  sidebarSpacer: {
1234
1305
  flex: number;
1235
1306
  };
1307
+ sidebarTriggerSlot: {
1308
+ marginBottom: number;
1309
+ };
1236
1310
  sidebarEmpty: {
1237
1311
  fontSize: number;
1238
1312
  paddingVertical: number;
@@ -1453,4 +1527,4 @@ declare const NAV_ICON_SIZE = 14;
1453
1527
  /** Chevron icon size for expandable sections. */
1454
1528
  declare const CHEVRON_ICON_SIZE = 12;
1455
1529
 
1456
- export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, type CommandItem, CommandPalette, type CommandPaletteLabels, type CommandPaletteProps, CommandPaletteTrigger, type CommandPaletteTriggerProps, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, 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 SearchAffordance, type ShellMessage, Sidebar, SidebarChevron, type SidebarChevronProps, type SidebarProps, SidebarSearch, type SidebarSearchConfig, type SidebarSearchLabels, type SidebarSearchProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveContentMaxWidth, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };
1530
+ export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, type CommandItem, CommandPalette, type CommandPaletteLabels, type CommandPaletteProps, CommandPaletteTrigger, type CommandPaletteTriggerProps, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, 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 SearchAffordance, type ShellMessage, Sidebar, SidebarChevron, type SidebarChevronProps, type SidebarPaletteTriggerConfig, type SidebarProps, SidebarSearch, type SidebarSearchConfig, type SidebarSearchLabels, type SidebarSearchProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveActiveRoute, resolveContentMaxWidth, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };
package/dist/index.d.ts CHANGED
@@ -5,12 +5,71 @@ import { DropdownVariant } from '@dloizides/ui-layout';
5
5
  import { resolveAccessibleRoutes, RoleRouteTable, RoleRoute } from '@dloizides/auth-web';
6
6
 
7
7
  /**
8
- * Active-route matcher shared by the sidebar entries. Ported verbatim from the
9
- * twin app sidebars: an item is active when the current pathname equals its
10
- * route or is nested under it (`/foo` matches `/foo` and `/foo/bar`, but `/`
11
- * only matches `/`).
8
+ * Public prop types for the `@dloizides/ui-nav` config-driven navigation shell.
9
+ *
10
+ * The rendering chrome (sidebar + topbar) was byte-identical across erevna-web
11
+ * and katalogos-web; only the *item data* (which routes, how they group) differs
12
+ * per app. So this package renders a caller-supplied `NavItem[]` — labels are
13
+ * pre-localized strings and icons are render slots, keeping the package free of
14
+ * any app's i18n helper, icon set, router, or store.
12
15
  */
13
- declare function isRouteActive(pathname: string, route: string): boolean;
16
+
17
+ /** One navigation entry. Labels are already localized by the caller. */
18
+ interface NavItem {
19
+ /** Stable key + default testID. */
20
+ key: string;
21
+ /** Localized display label. */
22
+ label: string;
23
+ /** Route/path this item navigates to (passed back to `onNavigate`). */
24
+ route: string;
25
+ /** Optional testID override (defaults to `key`). */
26
+ testID?: string;
27
+ /**
28
+ * Optional leading icon. Receives the resolved foreground colour and a size,
29
+ * so the app supplies its own icon component without this package importing
30
+ * an icon set.
31
+ */
32
+ renderIcon?: (color: string, size: number) => React.ReactNode;
33
+ /** Optional nested items — rendered as an expandable section. */
34
+ children?: NavItem[];
35
+ /**
36
+ * Opt this leaf out of prefix matching: when `true` it is active ONLY when the
37
+ * pathname equals its route exactly (never for a nested `route/child` path). A
38
+ * complementary escape hatch — the default most-specific/longest-match resolution
39
+ * (see `resolveActiveRoute`) already prevents a module-home leaf like `/accounting`
40
+ * from co-activating with `/accounting/journals`, so most consumers never need
41
+ * this. Reach for it only when a leaf must NEVER light up on any descendant path
42
+ * even when no longer sibling route exists.
43
+ */
44
+ exact?: boolean;
45
+ }
46
+
47
+ /**
48
+ * Active-route matching for the nav shells.
49
+ *
50
+ * `isRouteActive` is the low-level per-route predicate (prefix-based, ported
51
+ * verbatim from the twin app sidebars): a route is active when the current
52
+ * pathname equals it or is nested under it (`/foo` matches `/foo` and `/foo/bar`,
53
+ * but `/` only matches `/`). With `exact`, only an exact path matches.
54
+ *
55
+ * `resolveActiveRoute` is the CROSS-ITEM resolver that fixes the sidebar
56
+ * co-activation bug: because a module-home leaf (`/accounting`) is a prefix of
57
+ * every sibling (`/accounting/journals`), the naive per-item predicate lights up
58
+ * BOTH on `/accounting/journals`. `resolveActiveRoute` instead returns the ONE
59
+ * most-specific (longest) leaf route that matches — so at most one leaf is ever
60
+ * active, the longest wins on nested paths, and the module-home leaf is active
61
+ * only on its own exact path. For a nav model with no parent/child route overlap
62
+ * (the common case) this is identical to the old behaviour.
63
+ */
64
+
65
+ declare function isRouteActive(pathname: string, route: string, exact?: boolean): boolean;
66
+ /**
67
+ * The single active leaf route for `pathname`: the LONGEST leaf route that
68
+ * matches (most-specific wins). Returns `undefined` when no leaf matches. Marking
69
+ * a leaf active iff `item.route === resolveActiveRoute(items, pathname)` guarantees
70
+ * at most one active leaf — the whack-a-mole prefix co-activation fix.
71
+ */
72
+ declare function resolveActiveRoute(items: NavItem[], pathname: string): string | undefined;
14
73
 
15
74
  /**
16
75
  * SidebarSearch — the persistent, DESKTOP inline search field that lives at the
@@ -51,36 +110,6 @@ interface SidebarSearchProps {
51
110
  /** The inline sidebar search field (desktop) with a clear button. */
52
111
  declare const SidebarSearch: ({ value, onChangeText, onClear, labels, testID, }: SidebarSearchProps) => React.ReactElement;
53
112
 
54
- /**
55
- * Public prop types for the `@dloizides/ui-nav` config-driven navigation shell.
56
- *
57
- * The rendering chrome (sidebar + topbar) was byte-identical across erevna-web
58
- * and katalogos-web; only the *item data* (which routes, how they group) differs
59
- * per app. So this package renders a caller-supplied `NavItem[]` — labels are
60
- * pre-localized strings and icons are render slots, keeping the package free of
61
- * any app's i18n helper, icon set, router, or store.
62
- */
63
-
64
- /** One navigation entry. Labels are already localized by the caller. */
65
- interface NavItem {
66
- /** Stable key + default testID. */
67
- key: string;
68
- /** Localized display label. */
69
- label: string;
70
- /** Route/path this item navigates to (passed back to `onNavigate`). */
71
- route: string;
72
- /** Optional testID override (defaults to `key`). */
73
- testID?: string;
74
- /**
75
- * Optional leading icon. Receives the resolved foreground colour and a size,
76
- * so the app supplies its own icon component without this package importing
77
- * an icon set.
78
- */
79
- renderIcon?: (color: string, size: number) => React.ReactNode;
80
- /** Optional nested items — rendered as an expandable section. */
81
- children?: NavItem[];
82
- }
83
-
84
113
  /**
85
114
  * Sidebar — the config-driven left navigation shell promoted from the
86
115
  * byte-identical erevna-web / katalogos-web `Sidebar`. It renders a caller
@@ -100,6 +129,24 @@ interface SidebarSearchConfig extends SidebarSearchLabels {
100
129
  /** Shown in place of the list when the query matches nothing. */
101
130
  emptyText: string;
102
131
  }
132
+ /**
133
+ * The ⌘K command-palette trigger the sidebar renders as its NARROW-viewport search
134
+ * affordance (a persistent inline field would steal scarce vertical space on mobile
135
+ * — see {@link resolveSearchAffordance}). Pre-localized strings + the open handler;
136
+ * wire `onOpen` to the same handler as the ⌘K hotkey ({@link useCommandPaletteHotkey}).
137
+ */
138
+ interface SidebarPaletteTriggerConfig {
139
+ /** Placeholder-style label, e.g. "Search". */
140
+ label: string;
141
+ /** a11y hint describing what activating it does. */
142
+ hint: string;
143
+ /** The shortcut badge text, e.g. "⌘K". */
144
+ shortcut: string;
145
+ /** Open the palette (same handler as the ⌘K hotkey). */
146
+ onOpen: () => void;
147
+ /** testID for the trigger. Defaults to `sidebar-command-palette-trigger`. */
148
+ testID?: string;
149
+ }
103
150
  interface SidebarProps {
104
151
  /** Nav entries — already role-filtered / grouped by the app. */
105
152
  items: NavItem[];
@@ -121,12 +168,21 @@ interface SidebarProps {
121
168
  renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
122
169
  /**
123
170
  * Opt into the persistent inline search field (desktop). Requires `search`
124
- * labels; when omitted the sidebar renders exactly as before. Prefer this on
125
- * the wide/full rail and the ⌘K palette on the mobile drawer.
171
+ * labels; when omitted the sidebar renders exactly as before. The field shows
172
+ * ONLY at the inline affordance (viewport 768, {@link resolveSearchAffordance});
173
+ * on a narrow viewport the ⌘K `paletteTrigger` is the search affordance instead.
126
174
  */
127
175
  enableInlineSearch?: boolean;
128
176
  /** Pre-localized inline-search strings — required for `enableInlineSearch`. */
129
177
  search?: SidebarSearchConfig;
178
+ /**
179
+ * Opt into the ⌘K {@link CommandPaletteTrigger} as the NARROW-viewport search
180
+ * affordance. Rendered ONLY at the palette affordance (viewport < 768); on a wide
181
+ * viewport it is suppressed so the inline field and the trigger are never both
182
+ * shown — they are mutually exclusive by affordance. The ⌘K hotkey stays bound at
183
+ * every width independently ({@link useCommandPaletteHotkey}).
184
+ */
185
+ paletteTrigger?: SidebarPaletteTriggerConfig;
130
186
  /** Controlled search query (optional — the sidebar owns the state when omitted). */
131
187
  searchQuery?: string;
132
188
  /** Called on each query change (with either controlled or internal state). */
@@ -138,7 +194,7 @@ interface SidebarProps {
138
194
  /** Extra container style overrides. */
139
195
  containerStyle?: ViewStyle | ViewStyle[];
140
196
  }
141
- declare const Sidebar: ({ items, pathname, onNavigate, title, regionLabel, navigateHint, expandHint, collapseHint, renderChevron, enableInlineSearch, search, searchQuery, onSearchChange, header, footer, containerStyle, }: SidebarProps) => React.ReactElement;
197
+ declare const Sidebar: ({ items, pathname, onNavigate, title, regionLabel, navigateHint, expandHint, collapseHint, renderChevron, enableInlineSearch, search, paletteTrigger, searchQuery, onSearchChange, header, footer, containerStyle, }: SidebarProps) => React.ReactElement;
142
198
 
143
199
  /**
144
200
  * SidebarChevron — the built-in disclosure caret for an expandable sidebar
@@ -737,6 +793,12 @@ interface NavShellSideRail {
737
793
  enableInlineSearch?: boolean;
738
794
  /** Pre-localized inline-search strings — required for `enableInlineSearch`. Forwarded to `Sidebar.search`. */
739
795
  search?: SidebarSearchConfig;
796
+ /**
797
+ * Opt the inner `Sidebar` into the ⌘K palette trigger as its narrow-viewport
798
+ * search affordance. Mutually exclusive with the inline field by viewport width
799
+ * (never both shown). Forwarded to `Sidebar.paletteTrigger`.
800
+ */
801
+ paletteTrigger?: SidebarPaletteTriggerConfig;
740
802
  /** Controlled search query (optional — the Sidebar owns the state when omitted). Forwarded to `Sidebar.searchQuery`. */
741
803
  searchQuery?: string;
742
804
  /** Called on each query change (controlled or internal). Forwarded to `Sidebar.onSearchChange`. */
@@ -1047,6 +1109,15 @@ interface NavExpandableItemProps {
1047
1109
  item: NavItem;
1048
1110
  pathname: string;
1049
1111
  onNavigate: (route: string) => void;
1112
+ /**
1113
+ * The single winning active leaf route for the whole nav model, resolved once by
1114
+ * the parent via `resolveActiveRoute` (most-specific/longest match wins). A leaf
1115
+ * is active iff `item.route === activeRoute`, so at most one leaf ever carries
1116
+ * `aria-current="page"` — the fix for `/accounting` co-activating with
1117
+ * `/accounting/journals`. When omitted (standalone use) each leaf falls back to
1118
+ * the per-item prefix predicate, preserving the pre-1.18 behaviour.
1119
+ */
1120
+ activeRoute?: string;
1050
1121
  /** a11y hint for a leaf item, given its label. */
1051
1122
  navigateHint: (label: string) => string;
1052
1123
  /** a11y hint shown when the section is collapsed (press expands). */
@@ -1061,7 +1132,7 @@ interface NavExpandableItemProps {
1061
1132
  renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
1062
1133
  depth?: number;
1063
1134
  }
1064
- declare const NavExpandableItem: ({ item, pathname, onNavigate, navigateHint, expandHint, collapseHint, renderChevron, depth, }: NavExpandableItemProps) => React.ReactElement;
1135
+ declare const NavExpandableItem: ({ item, pathname, onNavigate, navigateHint, expandHint, collapseHint, renderChevron, activeRoute, depth, }: NavExpandableItemProps) => React.ReactElement;
1065
1136
 
1066
1137
  /**
1067
1138
  * Pure matching logic for the {@link CommandPalette}. Kept framework-free so the
@@ -1233,6 +1304,9 @@ declare const navStyles: {
1233
1304
  sidebarSpacer: {
1234
1305
  flex: number;
1235
1306
  };
1307
+ sidebarTriggerSlot: {
1308
+ marginBottom: number;
1309
+ };
1236
1310
  sidebarEmpty: {
1237
1311
  fontSize: number;
1238
1312
  paddingVertical: number;
@@ -1453,4 +1527,4 @@ declare const NAV_ICON_SIZE = 14;
1453
1527
  /** Chevron icon size for expandable sections. */
1454
1528
  declare const CHEVRON_ICON_SIZE = 12;
1455
1529
 
1456
- export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, type CommandItem, CommandPalette, type CommandPaletteLabels, type CommandPaletteProps, CommandPaletteTrigger, type CommandPaletteTriggerProps, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, 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 SearchAffordance, type ShellMessage, Sidebar, SidebarChevron, type SidebarChevronProps, type SidebarProps, SidebarSearch, type SidebarSearchConfig, type SidebarSearchLabels, type SidebarSearchProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveContentMaxWidth, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };
1530
+ export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, type CommandItem, CommandPalette, type CommandPaletteLabels, type CommandPaletteProps, CommandPaletteTrigger, type CommandPaletteTriggerProps, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, 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 SearchAffordance, type ShellMessage, Sidebar, SidebarChevron, type SidebarChevronProps, type SidebarPaletteTriggerConfig, type SidebarProps, SidebarSearch, type SidebarSearchConfig, type SidebarSearchLabels, type SidebarSearchProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveActiveRoute, resolveContentMaxWidth, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };