@dloizides/ui-nav 1.17.0 → 1.19.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,48 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.19.0
4
+
5
+ **The "More ▾" overflow menu now opens at a readable width instead of cramping onto the trigger's tiny footprint.**
6
+
7
+ `NavOverflowMenu` rendered the shared `@dloizides/ui-layout` `ModalDropdown` without a `menuMinWidth`,
8
+ so the popover matched the width of the compact "More ▾" pill (~its own label). Option labels
9
+ ("Settings", "API docs") wrapped onto two lines, and the resulting tall rows overflowed the menu's
10
+ max-height — so the lower items scrolled out of view and read as missing.
11
+
12
+ - **`NavOverflowMenu`** now passes a 200px `menuMinWidth` floor, so the overflow menu opens wide
13
+ enough to show each label on one line. `ModalDropdown`/`InlineMenu` clamp a floored menu inside the
14
+ viewport (ui-layout ≥ 1.24.0, now the peer floor), so the extra width never pushes it off the right
15
+ edge. No public API changed.
16
+
17
+ ## 1.18.0
18
+
19
+ Root-cause fix for sidebar leaf co-activation. Behavioral/additive — no breaking API.
20
+
21
+ - **Fix (two sidebar leaves showed `aria-current="page"` at once).** On the live finreg
22
+ `/accounting/journals`, BOTH "Chart of Accounts" (route `/accounting`) and "Journals"
23
+ (route `/accounting/journals`) were marked active, because the active predicate was purely
24
+ prefix-based (`pathname === route || pathname.startsWith(route + '/')`) — a module-home leaf
25
+ routed at `/accounting` is a prefix of every `/accounting/*` sibling, so it co-activated on all
26
+ of them. (Second occurrence of this class; the first, CRM Contacts at `/crm`, was worked around
27
+ by moving the route.) Now resolution is **most-specific (longest) match wins**: among a nav
28
+ model's LEAF items, at most ONE is active for any path — the leaf whose route is the longest
29
+ prefix match. On `/accounting/journals` only Journals is active; on `/accounting` exactly only
30
+ Chart of Accounts is. For nav models with no parent/child route overlap (the common case)
31
+ behaviour is unchanged.
32
+ - **New export** `resolveActiveRoute(items, pathname)`: flattens the tree to its leaf routes and
33
+ returns the single longest matching route (honouring per-leaf `exact`), or `undefined`.
34
+ - `Sidebar` resolves the winning route once over the full leaf set and passes `activeRoute` to
35
+ each row; `NavExpandableItem` marks a leaf active iff `item.route === activeRoute` (falls back
36
+ to the per-item predicate when used standalone — backward compatible). Group HEADER expansion
37
+ (`hasActiveDescendant`) is a separate concern and is unchanged.
38
+ - The same winner-based resolution now backs `NavBar`, `PillNav`, `CollapsedRail` and
39
+ `NavOverflowMenu`, so the latent prefix co-activation cannot resurface in the horizontal navs.
40
+ - **Additive API**: new optional `NavItem.exact?: boolean` — a leaf active ONLY on its exact
41
+ path (never a nested one). A complementary escape hatch; the longest-match default is the real
42
+ fix, so no consumer needs to set it.
43
+ - `isRouteActive(pathname, route, exact?)` gains an optional third arg; the two-arg call is
44
+ unchanged.
45
+
3
46
  ## 1.17.0
4
47
 
5
48
  Two live-defect fixes to the shared sidebar chrome. Behavioral/additive — no breaking API.
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
@@ -1080,6 +1109,15 @@ interface NavExpandableItemProps {
1080
1109
  item: NavItem;
1081
1110
  pathname: string;
1082
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;
1083
1121
  /** a11y hint for a leaf item, given its label. */
1084
1122
  navigateHint: (label: string) => string;
1085
1123
  /** a11y hint shown when the section is collapsed (press expands). */
@@ -1094,7 +1132,7 @@ interface NavExpandableItemProps {
1094
1132
  renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
1095
1133
  depth?: number;
1096
1134
  }
1097
- 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;
1098
1136
 
1099
1137
  /**
1100
1138
  * Pure matching logic for the {@link CommandPalette}. Kept framework-free so the
@@ -1489,4 +1527,4 @@ declare const NAV_ICON_SIZE = 14;
1489
1527
  /** Chevron icon size for expandable sections. */
1490
1528
  declare const CHEVRON_ICON_SIZE = 12;
1491
1529
 
1492
- 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, 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
@@ -1080,6 +1109,15 @@ interface NavExpandableItemProps {
1080
1109
  item: NavItem;
1081
1110
  pathname: string;
1082
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;
1083
1121
  /** a11y hint for a leaf item, given its label. */
1084
1122
  navigateHint: (label: string) => string;
1085
1123
  /** a11y hint shown when the section is collapsed (press expands). */
@@ -1094,7 +1132,7 @@ interface NavExpandableItemProps {
1094
1132
  renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
1095
1133
  depth?: number;
1096
1134
  }
1097
- 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;
1098
1136
 
1099
1137
  /**
1100
1138
  * Pure matching logic for the {@link CommandPalette}. Kept framework-free so the
@@ -1489,4 +1527,4 @@ declare const NAV_ICON_SIZE = 14;
1489
1527
  /** Chevron icon size for expandable sections. */
1490
1528
  declare const CHEVRON_ICON_SIZE = 12;
1491
1529
 
1492
- 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, 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.js CHANGED
@@ -128,10 +128,31 @@ function isFilterActive(query) {
128
128
  }
129
129
 
130
130
  // src/isRouteActive.ts
131
- function isRouteActive(pathname, route) {
131
+ function isRouteActive(pathname, route, exact = false) {
132
+ if (exact) return pathname === route;
132
133
  if (route === "/") return pathname === "/";
133
134
  return pathname === route || pathname.startsWith(`${route}/`);
134
135
  }
136
+ function leafRoutes(items) {
137
+ return items.flatMap((item) => {
138
+ const children = item.children ?? [];
139
+ const isGroup = children.length > 0;
140
+ return isGroup ? leafRoutes(children) : [{ route: item.route, exact: item.exact === true }];
141
+ });
142
+ }
143
+ function resolveActiveRoute(items, pathname) {
144
+ let winner;
145
+ let winnerLength = -1;
146
+ for (const leaf of leafRoutes(items)) {
147
+ const matches = isRouteActive(pathname, leaf.route, leaf.exact);
148
+ const isLonger = leaf.route.length > winnerLength;
149
+ if (matches && isLonger) {
150
+ winner = leaf.route;
151
+ winnerLength = leaf.route.length;
152
+ }
153
+ }
154
+ return winner;
155
+ }
135
156
  var ACTIVE_BORDER_RADIUS = 4;
136
157
  var ACTIVE_ACCENT_WIDTH = 3;
137
158
  var NAV_ROW_RADIUS = 8;
@@ -509,6 +530,7 @@ var NavExpandableItem = ({
509
530
  expandHint,
510
531
  collapseHint,
511
532
  renderChevron,
533
+ activeRoute,
512
534
  depth = 0
513
535
  }) => {
514
536
  const [expanded, setExpanded] = React.useState(() => hasActiveDescendant(item, pathname));
@@ -522,7 +544,7 @@ var NavExpandableItem = ({
522
544
  const onHoverOut = React.useCallback(() => setHovered(false), []);
523
545
  const indent = depth * BASE_INDENT;
524
546
  const hasChildren = Array.isArray(item.children) && item.children.length > 0;
525
- const isActive = isRouteActive(pathname, item.route);
547
+ const isActive = activeRoute !== void 0 ? item.route === activeRoute : isRouteActive(pathname, item.route, item.exact === true);
526
548
  const isSectionHeader = hasChildren && depth === 0;
527
549
  const activeAccentStyle = React.useMemo(
528
550
  () => ({ borderLeftWidth: ACTIVE_ACCENT_WIDTH, borderLeftColor: primaryColor }),
@@ -610,6 +632,7 @@ var NavExpandableItem = ({
610
632
  /* @__PURE__ */ jsxRuntime.jsx(uiMotion.Collapse, { open: expanded, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: expandableStyles.childrenContainer, children: item.children?.map((child) => /* @__PURE__ */ jsxRuntime.jsx(
611
633
  NavExpandableItem,
612
634
  {
635
+ activeRoute,
613
636
  collapseHint,
614
637
  depth: depth + 1,
615
638
  expandHint,
@@ -760,6 +783,7 @@ var Sidebar = ({
760
783
  const showPaletteTrigger = paletteTrigger !== void 0 && !isInlineAffordance;
761
784
  const visibleItems = showInlineSearch ? filterNavItems(items, query) : items;
762
785
  const showEmpty = showInlineSearch && isFilterActive(query) && visibleItems.length === 0;
786
+ const activeRoute = resolveActiveRoute(items, pathname);
763
787
  return /* @__PURE__ */ jsxRuntime.jsxs(
764
788
  reactNative.View,
765
789
  {
@@ -797,6 +821,7 @@ var Sidebar = ({
797
821
  showEmpty && search !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.sidebarEmpty, { color: colors.textSecondary }], testID: "sidebar-search-empty", children: search.emptyText }) : visibleItems.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
798
822
  NavExpandableItem,
799
823
  {
824
+ activeRoute,
800
825
  collapseHint,
801
826
  expandHint,
802
827
  item,
@@ -1049,6 +1074,7 @@ var NavBarLink = ({
1049
1074
  };
1050
1075
  var TEXT_ON_PRIMARY3 = "#ffffff";
1051
1076
  var NO_ACTIVE_ROUTE = "";
1077
+ var OVERFLOW_MENU_MIN_WIDTH = 200;
1052
1078
  var NavOverflowMenu = ({
1053
1079
  items,
1054
1080
  pathname,
@@ -1061,7 +1087,7 @@ var NavOverflowMenu = ({
1061
1087
  }) => {
1062
1088
  const options = React.useMemo(() => items.map((item) => ({ label: item.label, value: item.route })), [items]);
1063
1089
  const activeRoute = React.useMemo(
1064
- () => items.find((item) => isRouteActive(pathname, item.route))?.route ?? NO_ACTIVE_ROUTE,
1090
+ () => resolveActiveRoute(items, pathname) ?? NO_ACTIVE_ROUTE,
1065
1091
  [items, pathname]
1066
1092
  );
1067
1093
  const optionTestID = React.useMemo(() => {
@@ -1076,6 +1102,7 @@ var NavOverflowMenu = ({
1076
1102
  {
1077
1103
  accessibilityHint: hint,
1078
1104
  accessibilityLabel: label,
1105
+ menuMinWidth: OVERFLOW_MENU_MIN_WIDTH,
1079
1106
  optionTestID,
1080
1107
  options,
1081
1108
  testID,
@@ -1194,6 +1221,7 @@ var NavBarInner = ({
1194
1221
  },
1195
1222
  [onNavigate]
1196
1223
  );
1224
+ const activeRoute = resolveActiveRoute(items, pathname);
1197
1225
  const linkColors = React.useMemo(
1198
1226
  () => ({
1199
1227
  rest: colors.textSecondary,
@@ -1241,7 +1269,7 @@ var NavBarInner = ({
1241
1269
  collapsed,
1242
1270
  colors: linkColors,
1243
1271
  iconSize: NAV_ICON_SIZE,
1244
- isActive: isRouteActive(pathname, item.route),
1272
+ isActive: item.route === activeRoute,
1245
1273
  item,
1246
1274
  navigateHint,
1247
1275
  reducedMotion,
@@ -1603,6 +1631,7 @@ var CollapsedRail = ({
1603
1631
  const { theme } = uiFeedback.useUi();
1604
1632
  const colors = theme.colors;
1605
1633
  const primaryColor = theme.palette.primary["500"];
1634
+ const activeRoute = resolveActiveRoute(items, pathname);
1606
1635
  return /* @__PURE__ */ jsxRuntime.jsxs(
1607
1636
  reactNative.View,
1608
1637
  {
@@ -1617,7 +1646,7 @@ var CollapsedRail = ({
1617
1646
  children: [
1618
1647
  header !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.slot, children: renderTextSlot(header, { color: colors.text }) }) : null,
1619
1648
  items.map((item) => {
1620
- const active = isRouteActive(pathname, item.route);
1649
+ const active = item.route === activeRoute;
1621
1650
  const iconColor = active ? primaryColor : colors.textSecondary;
1622
1651
  return /* @__PURE__ */ jsxRuntime.jsx(
1623
1652
  FocusableTouchable,
@@ -1745,6 +1774,7 @@ var PillNav = ({
1745
1774
  const colors = theme.colors;
1746
1775
  const primaryColor = theme.palette.primary["500"];
1747
1776
  if (items.length < minItems) return null;
1777
+ const activeRoute = resolveActiveRoute(items, pathname);
1748
1778
  return /* @__PURE__ */ jsxRuntime.jsx(
1749
1779
  reactNative.View,
1750
1780
  {
@@ -1753,7 +1783,7 @@ var PillNav = ({
1753
1783
  role: "navigation",
1754
1784
  style: [pillNavStyles.container, containerStyle],
1755
1785
  children: items.map((item) => {
1756
- const active = isRouteActive(pathname, item.route);
1786
+ const active = item.route === activeRoute;
1757
1787
  const textColor = active ? TEXT_ON_PRIMARY4 : colors.textSecondary;
1758
1788
  const pillStyle = active ? { backgroundColor: primaryColor } : { backgroundColor: colors.surfaceElevated };
1759
1789
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -2049,6 +2079,7 @@ exports.isFilterActive = isFilterActive;
2049
2079
  exports.isRouteActive = isRouteActive;
2050
2080
  exports.navStyles = navStyles;
2051
2081
  exports.pillNavStyles = pillNavStyles;
2082
+ exports.resolveActiveRoute = resolveActiveRoute;
2052
2083
  exports.resolveContentMaxWidth = resolveContentMaxWidth;
2053
2084
  exports.resolveRailMode = resolveRailMode;
2054
2085
  exports.resolveSearchAffordance = resolveSearchAffordance;