@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/dist/index.mjs CHANGED
@@ -1,12 +1,93 @@
1
- import React3, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
2
- import { StyleSheet, Platform, Text, TouchableOpacity, View, TextInput, Pressable, useWindowDimensions, ActivityIndicator, ScrollView } from 'react-native';
1
+ import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
2
+ import { Platform, StyleSheet, Pressable, Text, View, TextInput, useWindowDimensions, ActivityIndicator, ScrollView } from 'react-native';
3
3
  import { useUi, UiProvider } from '@dloizides/ui-feedback';
4
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
5
  import { Collapse } from '@dloizides/ui-motion';
5
- import { jsx, jsxs } from 'react/jsx-runtime';
6
6
  import { ModalDropdown } from '@dloizides/ui-layout';
7
7
  import { resolveAccessibleRoutes } from '@dloizides/auth-web';
8
8
 
9
9
  // src/Sidebar.tsx
10
+ var IS_WEB = Platform.OS === "web";
11
+ var FOCUS_RING_WIDTH = 2;
12
+ var FOCUS_RING_OFFSET = 2;
13
+ var HOVER_TRANSITION_MS = 150;
14
+ var TINT_TRANSITION_MS = 160;
15
+ function focusRingStyle(focused, ringColor) {
16
+ if (!IS_WEB || !focused) return void 0;
17
+ const ring = {
18
+ outlineWidth: FOCUS_RING_WIDTH,
19
+ outlineStyle: "solid",
20
+ outlineColor: ringColor,
21
+ outlineOffset: FOCUS_RING_OFFSET
22
+ };
23
+ return ring;
24
+ }
25
+ function webTransition(properties, durationMs, reducedMotion) {
26
+ if (!IS_WEB) return void 0;
27
+ const transition = {
28
+ transitionProperty: properties,
29
+ transitionDuration: reducedMotion ? "0ms" : `${durationMs}ms`
30
+ };
31
+ return transition;
32
+ }
33
+ function hoverTransitionStyle(reducedMotion) {
34
+ return webTransition("color, background-color", HOVER_TRANSITION_MS, reducedMotion);
35
+ }
36
+ function tintTransitionStyle(reducedMotion) {
37
+ return webTransition("opacity", TINT_TRANSITION_MS, reducedMotion);
38
+ }
39
+ function rotateTransitionStyle(reducedMotion) {
40
+ return webTransition("transform", TINT_TRANSITION_MS, reducedMotion);
41
+ }
42
+ var MIN_TARGET = 36;
43
+ var styles = StyleSheet.create({
44
+ trigger: {
45
+ flexDirection: "row",
46
+ alignItems: "center",
47
+ justifyContent: "space-between",
48
+ columnGap: 8,
49
+ paddingHorizontal: 12,
50
+ minHeight: MIN_TARGET,
51
+ borderWidth: 1,
52
+ borderRadius: 8
53
+ },
54
+ label: { fontSize: 14 },
55
+ badge: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, borderWidth: 1 },
56
+ badgeText: { fontSize: 11, fontWeight: "700" }
57
+ });
58
+ var CommandPaletteTrigger = ({
59
+ label,
60
+ hint,
61
+ shortcut,
62
+ onPress,
63
+ testID
64
+ }) => {
65
+ const { theme } = useUi();
66
+ const colors = theme.colors;
67
+ const primary = theme.palette.primary["500"];
68
+ const [focused, setFocused] = React.useState(false);
69
+ return /* @__PURE__ */ jsxs(
70
+ Pressable,
71
+ {
72
+ accessibilityHint: hint,
73
+ accessibilityLabel: label,
74
+ accessibilityRole: "button",
75
+ style: [
76
+ styles.trigger,
77
+ { backgroundColor: colors.surface, borderColor: colors.border },
78
+ focusRingStyle(focused, primary)
79
+ ],
80
+ testID,
81
+ onBlur: () => setFocused(false),
82
+ onFocus: () => setFocused(true),
83
+ onPress,
84
+ children: [
85
+ /* @__PURE__ */ jsx(Text, { style: [styles.label, { color: colors.textSecondary }], children: label }),
86
+ /* @__PURE__ */ jsx(View, { style: [styles.badge, { borderColor: colors.border }], children: /* @__PURE__ */ jsx(Text, { style: [styles.badgeText, { color: colors.textSecondary }], children: shortcut }) })
87
+ ]
88
+ }
89
+ );
90
+ };
10
91
 
11
92
  // src/sidebarFilter.ts
12
93
  function searchTokens(query) {
@@ -41,15 +122,40 @@ function isFilterActive(query) {
41
122
  }
42
123
 
43
124
  // src/isRouteActive.ts
44
- function isRouteActive(pathname, route) {
125
+ function isRouteActive(pathname, route, exact = false) {
126
+ if (exact) return pathname === route;
45
127
  if (route === "/") return pathname === "/";
46
128
  return pathname === route || pathname.startsWith(`${route}/`);
47
129
  }
130
+ function leafRoutes(items) {
131
+ return items.flatMap((item) => {
132
+ const children = item.children ?? [];
133
+ const isGroup = children.length > 0;
134
+ return isGroup ? leafRoutes(children) : [{ route: item.route, exact: item.exact === true }];
135
+ });
136
+ }
137
+ function resolveActiveRoute(items, pathname) {
138
+ let winner;
139
+ let winnerLength = -1;
140
+ for (const leaf of leafRoutes(items)) {
141
+ const matches = isRouteActive(pathname, leaf.route, leaf.exact);
142
+ const isLonger = leaf.route.length > winnerLength;
143
+ if (matches && isLonger) {
144
+ winner = leaf.route;
145
+ winnerLength = leaf.route.length;
146
+ }
147
+ }
148
+ return winner;
149
+ }
48
150
  var ACTIVE_BORDER_RADIUS = 4;
49
151
  var ACTIVE_ACCENT_WIDTH = 3;
50
152
  var NAV_ROW_RADIUS = 8;
51
153
  var ACTIVE_TINT_OPACITY = 0.14;
52
154
  var HOVER_TINT_OPACITY = 0.06;
155
+ function rowTintOpacity(isActive, hovered) {
156
+ if (isActive) return ACTIVE_TINT_OPACITY;
157
+ return hovered ? HOVER_TINT_OPACITY : 0;
158
+ }
53
159
  var NAV_LINK_GAP = 4;
54
160
  var navStyles = StyleSheet.create({
55
161
  // --- Sidebar ---
@@ -75,6 +181,12 @@ var navStyles = StyleSheet.create({
75
181
  sidebarSpacer: {
76
182
  flex: 1
77
183
  },
184
+ // Bottom rhythm under the ⌘K palette trigger (the narrow-viewport search
185
+ // affordance), matching the inline field's own `marginBottom` so swapping
186
+ // affordances by width keeps the same gap above the nav list.
187
+ sidebarTriggerSlot: {
188
+ marginBottom: 12
189
+ },
78
190
  // Inline-search no-match text — sits where the nav list would, muted + padded.
79
191
  sidebarEmpty: {
80
192
  fontSize: 13,
@@ -337,38 +449,6 @@ var expandableStyles = StyleSheet.create({
337
449
  var BASE_INDENT = 12;
338
450
  var NAV_ICON_SIZE = 14;
339
451
  var CHEVRON_ICON_SIZE = 12;
340
- var IS_WEB = Platform.OS === "web";
341
- var FOCUS_RING_WIDTH = 2;
342
- var FOCUS_RING_OFFSET = 2;
343
- var HOVER_TRANSITION_MS = 150;
344
- var TINT_TRANSITION_MS = 160;
345
- function focusRingStyle(focused, ringColor) {
346
- if (!IS_WEB || !focused) return void 0;
347
- const ring = {
348
- outlineWidth: FOCUS_RING_WIDTH,
349
- outlineStyle: "solid",
350
- outlineColor: ringColor,
351
- outlineOffset: FOCUS_RING_OFFSET
352
- };
353
- return ring;
354
- }
355
- function webTransition(properties, durationMs, reducedMotion) {
356
- if (!IS_WEB) return void 0;
357
- const transition = {
358
- transitionProperty: properties,
359
- transitionDuration: reducedMotion ? "0ms" : `${durationMs}ms`
360
- };
361
- return transition;
362
- }
363
- function hoverTransitionStyle(reducedMotion) {
364
- return webTransition("color, background-color", HOVER_TRANSITION_MS, reducedMotion);
365
- }
366
- function tintTransitionStyle(reducedMotion) {
367
- return webTransition("opacity", TINT_TRANSITION_MS, reducedMotion);
368
- }
369
- function rotateTransitionStyle(reducedMotion) {
370
- return webTransition("transform", TINT_TRANSITION_MS, reducedMotion);
371
- }
372
452
  var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
373
453
  function getReducedMotionQuery() {
374
454
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return void 0;
@@ -422,9 +502,19 @@ function hasActiveDescendant(item, pathname) {
422
502
  if (isRouteActive(pathname, item.route)) return true;
423
503
  return (item.children ?? []).some((child) => hasActiveDescendant(child, pathname));
424
504
  }
425
- function TintOverlay({ opacity, color }) {
505
+ function TintOverlay({
506
+ opacity,
507
+ color,
508
+ testID
509
+ }) {
426
510
  const reducedMotion = useReducedMotion();
427
- return /* @__PURE__ */ jsx(View, { style: [expandableStyles.tintOverlay, { backgroundColor: color, opacity }, tintTransitionStyle(reducedMotion)] });
511
+ return /* @__PURE__ */ jsx(
512
+ View,
513
+ {
514
+ style: [expandableStyles.tintOverlay, { backgroundColor: color, opacity }, tintTransitionStyle(reducedMotion)],
515
+ testID
516
+ }
517
+ );
428
518
  }
429
519
  var NavExpandableItem = ({
430
520
  item,
@@ -434,6 +524,7 @@ var NavExpandableItem = ({
434
524
  expandHint,
435
525
  collapseHint,
436
526
  renderChevron,
527
+ activeRoute,
437
528
  depth = 0
438
529
  }) => {
439
530
  const [expanded, setExpanded] = useState(() => hasActiveDescendant(item, pathname));
@@ -447,7 +538,7 @@ var NavExpandableItem = ({
447
538
  const onHoverOut = useCallback(() => setHovered(false), []);
448
539
  const indent = depth * BASE_INDENT;
449
540
  const hasChildren = Array.isArray(item.children) && item.children.length > 0;
450
- const isActive = isRouteActive(pathname, item.route);
541
+ const isActive = activeRoute !== void 0 ? item.route === activeRoute : isRouteActive(pathname, item.route, item.exact === true);
451
542
  const isSectionHeader = hasChildren && depth === 0;
452
543
  const activeAccentStyle = useMemo(
453
544
  () => ({ borderLeftWidth: ACTIVE_ACCENT_WIDTH, borderLeftColor: primaryColor }),
@@ -455,11 +546,12 @@ var NavExpandableItem = ({
455
546
  );
456
547
  const paddingStyle = useMemo(() => ({ paddingLeft: indent + BASE_INDENT }), [indent]);
457
548
  const headerPaddingStyle = useMemo(() => ({ paddingLeft: indent }), [indent]);
458
- const leafTintOpacity = isActive ? ACTIVE_TINT_OPACITY : hovered ? HOVER_TINT_OPACITY : 0;
459
- const headerTintOpacity = hovered ? HOVER_TINT_OPACITY : 0;
549
+ const leafTintOpacity = rowTintOpacity(isActive, hovered);
550
+ const headerTintOpacity = rowTintOpacity(false, hovered);
551
+ const rowTestID = item.testID ?? item.key;
460
552
  if (!hasChildren)
461
553
  return /* @__PURE__ */ jsxs(
462
- TouchableOpacity,
554
+ Pressable,
463
555
  {
464
556
  accessibilityHint: navigateHint(item.label),
465
557
  accessibilityLabel: item.label,
@@ -471,14 +563,14 @@ var NavExpandableItem = ({
471
563
  isActive ? activeAccentStyle : void 0,
472
564
  focusRingStyle(focused, primaryColor)
473
565
  ],
474
- testID: item.testID ?? item.key,
566
+ testID: rowTestID,
475
567
  onBlur: () => setFocused(false),
476
568
  onFocus: () => setFocused(true),
477
569
  onPress: () => onNavigate(item.route),
478
570
  ...hoverHandlerProps(onHoverIn, onHoverOut),
479
571
  ...ariaCurrentProps(isActive),
480
572
  children: [
481
- /* @__PURE__ */ jsx(TintOverlay, { color: primaryColor, opacity: leafTintOpacity }),
573
+ /* @__PURE__ */ jsx(TintOverlay, { color: primaryColor, opacity: leafTintOpacity, testID: `${rowTestID}-tint` }),
482
574
  typeof item.renderIcon === "function" ? /* @__PURE__ */ jsx(View, { style: expandableStyles.iconWrapper, children: item.renderIcon(colors.textSecondary, NAV_ICON_SIZE) }) : null,
483
575
  /* @__PURE__ */ jsx(
484
576
  Text,
@@ -496,7 +588,7 @@ var NavExpandableItem = ({
496
588
  const chevronColor = colors.textSecondary;
497
589
  return /* @__PURE__ */ jsxs(View, { children: [
498
590
  /* @__PURE__ */ jsxs(
499
- TouchableOpacity,
591
+ Pressable,
500
592
  {
501
593
  accessibilityHint: expanded ? collapseHint : expandHint,
502
594
  accessibilityLabel: item.label,
@@ -508,14 +600,14 @@ var NavExpandableItem = ({
508
600
  headerPaddingStyle,
509
601
  focusRingStyle(focused, primaryColor)
510
602
  ],
511
- testID: item.testID ?? item.key,
603
+ testID: rowTestID,
512
604
  onBlur: () => setFocused(false),
513
605
  onFocus: () => setFocused(true),
514
606
  onPress: toggle,
515
607
  ...hoverHandlerProps(onHoverIn, onHoverOut),
516
608
  ...ariaExpandedProps(expanded),
517
609
  children: [
518
- /* @__PURE__ */ jsx(TintOverlay, { color: primaryColor, opacity: headerTintOpacity }),
610
+ /* @__PURE__ */ jsx(TintOverlay, { color: primaryColor, opacity: headerTintOpacity, testID: `${rowTestID}-tint` }),
519
611
  typeof item.renderIcon === "function" ? /* @__PURE__ */ jsx(View, { style: expandableStyles.iconWrapper, children: item.renderIcon(isSectionHeader ? colors.textSecondary : colors.text, NAV_ICON_SIZE) }) : null,
520
612
  /* @__PURE__ */ jsx(
521
613
  Text,
@@ -534,6 +626,7 @@ var NavExpandableItem = ({
534
626
  /* @__PURE__ */ jsx(Collapse, { open: expanded, children: /* @__PURE__ */ jsx(View, { style: expandableStyles.childrenContainer, children: item.children?.map((child) => /* @__PURE__ */ jsx(
535
627
  NavExpandableItem,
536
628
  {
629
+ activeRoute,
537
630
  collapseHint,
538
631
  depth: depth + 1,
539
632
  expandHint,
@@ -547,10 +640,16 @@ var NavExpandableItem = ({
547
640
  )) }) })
548
641
  ] });
549
642
  };
643
+
644
+ // src/searchAffordance.ts
645
+ var DEFAULT_SEARCH_BREAKPOINT = 768;
646
+ function resolveSearchAffordance(viewport, breakpoint = DEFAULT_SEARCH_BREAKPOINT) {
647
+ return viewport >= breakpoint ? "inline" : "palette";
648
+ }
550
649
  var FIELD_MIN_HEIGHT = 38;
551
650
  var CLEAR_GLYPH = "\u2715";
552
651
  var CLEAR_MIN_TARGET = 28;
553
- var styles = StyleSheet.create({
652
+ var styles2 = StyleSheet.create({
554
653
  wrap: {
555
654
  flexDirection: "row",
556
655
  alignItems: "center",
@@ -591,7 +690,7 @@ var SidebarSearch = ({
591
690
  View,
592
691
  {
593
692
  style: [
594
- styles.wrap,
693
+ styles2.wrap,
595
694
  { backgroundColor: colors.surface, borderColor: focused ? primary : colors.border },
596
695
  focusRingStyle(focused, primary)
597
696
  ],
@@ -603,7 +702,7 @@ var SidebarSearch = ({
603
702
  accessibilityLabel: labels.placeholder,
604
703
  placeholder: labels.placeholder,
605
704
  placeholderTextColor: colors.textSecondary,
606
- style: [styles.input, { color: colors.text }],
705
+ style: [styles2.input, { color: colors.text }],
607
706
  testID,
608
707
  value,
609
708
  onBlur: () => setFocused(false),
@@ -617,10 +716,10 @@ var SidebarSearch = ({
617
716
  accessibilityHint: labels.clearHint,
618
717
  accessibilityLabel: labels.clearLabel,
619
718
  accessibilityRole: "button",
620
- style: styles.clear,
719
+ style: styles2.clear,
621
720
  testID: `${testID}-clear`,
622
721
  onPress: onClear,
623
- children: /* @__PURE__ */ jsx(Text, { style: [styles.clearGlyph, { color: colors.textSecondary }], children: CLEAR_GLYPH })
722
+ children: /* @__PURE__ */ jsx(Text, { style: [styles2.clearGlyph, { color: colors.textSecondary }], children: CLEAR_GLYPH })
624
723
  }
625
724
  ) : null
626
725
  ]
@@ -633,11 +732,12 @@ function isBareText(node) {
633
732
  function renderTextSlot(node, style) {
634
733
  if (node === void 0 || node === null) return node;
635
734
  if (isBareText(node)) return /* @__PURE__ */ jsx(Text, { style, children: node });
636
- return React3.Children.map(
735
+ return React.Children.map(
637
736
  node,
638
737
  (child) => isBareText(child) ? /* @__PURE__ */ jsx(Text, { style, children: child }) : child
639
738
  );
640
739
  }
740
+ var DEFAULT_TRIGGER_TEST_ID = "sidebar-command-palette-trigger";
641
741
  var Sidebar = ({
642
742
  items,
643
743
  pathname,
@@ -650,6 +750,7 @@ var Sidebar = ({
650
750
  renderChevron,
651
751
  enableInlineSearch = false,
652
752
  search,
753
+ paletteTrigger,
653
754
  searchQuery,
654
755
  onSearchChange,
655
756
  header,
@@ -658,6 +759,7 @@ var Sidebar = ({
658
759
  }) => {
659
760
  const { theme } = useUi();
660
761
  const colors = theme.colors;
762
+ const { width } = useWindowDimensions();
661
763
  const [internalQuery, setInternalQuery] = useState("");
662
764
  const controlled = searchQuery !== void 0;
663
765
  const query = controlled ? searchQuery : internalQuery;
@@ -669,9 +771,13 @@ var Sidebar = ({
669
771
  [controlled, onSearchChange]
670
772
  );
671
773
  const clearQuery = useCallback(() => setQuery(""), [setQuery]);
672
- const searchOn = enableInlineSearch && search !== void 0;
673
- const visibleItems = searchOn ? filterNavItems(items, query) : items;
674
- const showEmpty = searchOn && isFilterActive(query) && visibleItems.length === 0;
774
+ const affordance = resolveSearchAffordance(width);
775
+ const isInlineAffordance = affordance === "inline";
776
+ const showInlineSearch = enableInlineSearch && search !== void 0 && isInlineAffordance;
777
+ const showPaletteTrigger = paletteTrigger !== void 0 && !isInlineAffordance;
778
+ const visibleItems = showInlineSearch ? filterNavItems(items, query) : items;
779
+ const showEmpty = showInlineSearch && isFilterActive(query) && visibleItems.length === 0;
780
+ const activeRoute = resolveActiveRoute(items, pathname);
675
781
  return /* @__PURE__ */ jsxs(
676
782
  View,
677
783
  {
@@ -685,7 +791,7 @@ var Sidebar = ({
685
791
  ],
686
792
  children: [
687
793
  /* @__PURE__ */ jsx(Text, { accessibilityRole: "header", style: [navStyles.sidebarTitle, { color: colors.text }], children: title }),
688
- searchOn && search !== void 0 ? /* @__PURE__ */ jsx(
794
+ showInlineSearch && search !== void 0 ? /* @__PURE__ */ jsx(
689
795
  SidebarSearch,
690
796
  {
691
797
  labels: search,
@@ -695,10 +801,21 @@ var Sidebar = ({
695
801
  onClear: clearQuery
696
802
  }
697
803
  ) : null,
804
+ showPaletteTrigger && paletteTrigger !== void 0 ? /* @__PURE__ */ jsx(View, { style: navStyles.sidebarTriggerSlot, children: /* @__PURE__ */ jsx(
805
+ CommandPaletteTrigger,
806
+ {
807
+ hint: paletteTrigger.hint,
808
+ label: paletteTrigger.label,
809
+ shortcut: paletteTrigger.shortcut,
810
+ testID: paletteTrigger.testID ?? DEFAULT_TRIGGER_TEST_ID,
811
+ onPress: paletteTrigger.onOpen
812
+ }
813
+ ) }) : null,
698
814
  renderTextSlot(header, { color: colors.text }),
699
815
  showEmpty && search !== void 0 ? /* @__PURE__ */ jsx(Text, { style: [navStyles.sidebarEmpty, { color: colors.textSecondary }], testID: "sidebar-search-empty", children: search.emptyText }) : visibleItems.map((item) => /* @__PURE__ */ jsx(
700
816
  NavExpandableItem,
701
817
  {
818
+ activeRoute,
702
819
  collapseHint,
703
820
  expandHint,
704
821
  item,
@@ -716,12 +833,6 @@ var Sidebar = ({
716
833
  );
717
834
  };
718
835
 
719
- // src/searchAffordance.ts
720
- var DEFAULT_SEARCH_BREAKPOINT = 768;
721
- function resolveSearchAffordance(viewport, breakpoint = DEFAULT_SEARCH_BREAKPOINT) {
722
- return viewport >= breakpoint ? "inline" : "palette";
723
- }
724
-
725
836
  // src/constants.ts
726
837
  var NAV_TEST_IDS = {
727
838
  /** displayName line in the rich account header (tappable when `onAccount` set). */
@@ -969,7 +1080,7 @@ var NavOverflowMenu = ({
969
1080
  }) => {
970
1081
  const options = useMemo(() => items.map((item) => ({ label: item.label, value: item.route })), [items]);
971
1082
  const activeRoute = useMemo(
972
- () => items.find((item) => isRouteActive(pathname, item.route))?.route ?? NO_ACTIVE_ROUTE,
1083
+ () => resolveActiveRoute(items, pathname) ?? NO_ACTIVE_ROUTE,
973
1084
  [items, pathname]
974
1085
  );
975
1086
  const optionTestID = useMemo(() => {
@@ -1102,6 +1213,7 @@ var NavBarInner = ({
1102
1213
  },
1103
1214
  [onNavigate]
1104
1215
  );
1216
+ const activeRoute = resolveActiveRoute(items, pathname);
1105
1217
  const linkColors = useMemo(
1106
1218
  () => ({
1107
1219
  rest: colors.textSecondary,
@@ -1149,7 +1261,7 @@ var NavBarInner = ({
1149
1261
  collapsed,
1150
1262
  colors: linkColors,
1151
1263
  iconSize: NAV_ICON_SIZE,
1152
- isActive: isRouteActive(pathname, item.route),
1264
+ isActive: item.route === activeRoute,
1153
1265
  item,
1154
1266
  navigateHint,
1155
1267
  reducedMotion,
@@ -1157,7 +1269,7 @@ var NavBarInner = ({
1157
1269
  }
1158
1270
  );
1159
1271
  if (collapsed) {
1160
- return /* @__PURE__ */ jsx(View, { nativeID: LINKS_REGION_ID, style: navBarStyles.linksStacked, testID: NAV_TEST_IDS.navBarLinks, children: items.map((item) => /* @__PURE__ */ jsx(React3.Fragment, { children: renderLink(item) }, item.key)) });
1272
+ return /* @__PURE__ */ jsx(View, { nativeID: LINKS_REGION_ID, style: navBarStyles.linksStacked, testID: NAV_TEST_IDS.navBarLinks, children: items.map((item) => /* @__PURE__ */ jsx(React.Fragment, { children: renderLink(item) }, item.key)) });
1161
1273
  }
1162
1274
  const visibleItems = items.slice(0, visibleCount);
1163
1275
  const overflowItems = items.slice(visibleCount);
@@ -1223,7 +1335,7 @@ var CARD_BORDER_WIDTH = 1;
1223
1335
  var CARD_TITLE_FONT_SIZE = 16;
1224
1336
  var CARD_MESSAGE_FONT_SIZE = 14;
1225
1337
  var CARD_TITLE_MARGIN_BOTTOM = 8;
1226
- var styles2 = StyleSheet.create({
1338
+ var styles3 = StyleSheet.create({
1227
1339
  centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
1228
1340
  card: {
1229
1341
  padding: CARD_PADDING,
@@ -1240,9 +1352,9 @@ function MessageCard({
1240
1352
  }) {
1241
1353
  const { theme } = useUi();
1242
1354
  const colors = theme.colors;
1243
- return /* @__PURE__ */ jsxs(View, { style: [styles2.card, { backgroundColor: colors.surface, borderColor: accentColor }], testID, children: [
1244
- /* @__PURE__ */ jsx(Text, { style: [styles2.cardTitle, { color: accentColor }], children: message.titleText }),
1245
- /* @__PURE__ */ jsx(Text, { style: [styles2.cardMessage, { color: colors.textSecondary }], children: message.messageText })
1355
+ return /* @__PURE__ */ jsxs(View, { style: [styles3.card, { backgroundColor: colors.surface, borderColor: accentColor }], testID, children: [
1356
+ /* @__PURE__ */ jsx(Text, { style: [styles3.cardTitle, { color: accentColor }], children: message.titleText }),
1357
+ /* @__PURE__ */ jsx(Text, { style: [styles3.cardMessage, { color: colors.textSecondary }], children: message.messageText })
1246
1358
  ] });
1247
1359
  }
1248
1360
  function useContentBody(state, children, testID) {
@@ -1254,7 +1366,7 @@ function useContentBody(state, children, testID) {
1254
1366
  if (state?.error)
1255
1367
  return /* @__PURE__ */ jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
1256
1368
  if (state?.loading === true)
1257
- return /* @__PURE__ */ jsx(View, { style: styles2.centerFill, testID: `${testID}${APP_SHELL_SUFFIX.loading}`, children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" }) });
1369
+ return /* @__PURE__ */ jsx(View, { style: styles3.centerFill, testID: `${testID}${APP_SHELL_SUFFIX.loading}`, children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" }) });
1258
1370
  return children;
1259
1371
  }
1260
1372
  var SCRIM_COLOR = "rgba(0, 0, 0, 0.5)";
@@ -1278,7 +1390,7 @@ var DEFAULT_DRAWER_LABELS = {
1278
1390
  closeLabel: "Close menu",
1279
1391
  closeHint: "Close the navigation menu"
1280
1392
  };
1281
- var styles3 = StyleSheet.create({
1393
+ var styles4 = StyleSheet.create({
1282
1394
  overlay: { ...StyleSheet.absoluteFillObject, zIndex: DRAWER_Z_INDEX },
1283
1395
  // The scrim's tap-to-close hit area is anchored to the RIGHT of the panel
1284
1396
  // (`left: DRAWER_WIDTH`), so it NEVER overlaps the panel: nav-item taps always
@@ -1313,10 +1425,10 @@ var MenuToggle = ({ label, hint, onOpen, testID }) => {
1313
1425
  accessibilityHint: hint,
1314
1426
  accessibilityLabel: label,
1315
1427
  ringColor: theme.palette.primary["500"],
1316
- style: styles3.menuToggle,
1428
+ style: styles4.menuToggle,
1317
1429
  testID,
1318
1430
  onPress: onOpen,
1319
- children: /* @__PURE__ */ jsx(Text, { style: [styles3.menuToggleGlyph, { color: theme.colors.text }], children: MENU_GLYPH2 })
1431
+ children: /* @__PURE__ */ jsx(Text, { style: [styles4.menuToggleGlyph, { color: theme.colors.text }], children: MENU_GLYPH2 })
1320
1432
  }
1321
1433
  );
1322
1434
  };
@@ -1342,14 +1454,14 @@ var MobileDrawer = ({
1342
1454
  node.addEventListener("click", handleClick);
1343
1455
  return () => node.removeEventListener("click", handleClick);
1344
1456
  }, [onClose]);
1345
- return /* @__PURE__ */ jsxs(View, { style: styles3.overlay, children: [
1457
+ return /* @__PURE__ */ jsxs(View, { style: styles4.overlay, children: [
1346
1458
  /* @__PURE__ */ jsx(
1347
1459
  Pressable,
1348
1460
  {
1349
1461
  accessibilityHint: labels.closeHint,
1350
1462
  accessibilityLabel: labels.closeLabel,
1351
1463
  accessibilityRole: "button",
1352
- style: styles3.scrim,
1464
+ style: styles4.scrim,
1353
1465
  testID: scrimTestID,
1354
1466
  onPress: onClose
1355
1467
  }
@@ -1360,7 +1472,7 @@ var MobileDrawer = ({
1360
1472
  ref: panelRef,
1361
1473
  "aria-modal": true,
1362
1474
  role: "dialog",
1363
- style: [styles3.panel, { backgroundColor: theme.colors.surface }],
1475
+ style: [styles4.panel, { backgroundColor: theme.colors.surface }],
1364
1476
  testID: drawerTestID,
1365
1477
  children: renderTextSlot(sidebar, { color: theme.colors.text })
1366
1478
  }
@@ -1391,7 +1503,7 @@ function useContentMaxWidth(width) {
1391
1503
  return resolveContentMaxWidth(width, viewport);
1392
1504
  }
1393
1505
  var DEFAULT_CONTENT_PADDING = 24;
1394
- var styles4 = StyleSheet.create({
1506
+ var styles5 = StyleSheet.create({
1395
1507
  root: { flex: 1 },
1396
1508
  centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
1397
1509
  scroll: { flex: 1 },
@@ -1448,20 +1560,20 @@ var AppShell = ({
1448
1560
  if (isUnauthenticated && gate !== void 0) gate.onRedirect();
1449
1561
  }, [isUnauthenticated, gate]);
1450
1562
  if (gate?.pending === true)
1451
- return /* @__PURE__ */ jsx(View, { style: [styles4.root, styles4.centerFill, { backgroundColor: theme.colors.background }], testID: `${testID}${APP_SHELL_SUFFIX.pending}`, children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" }) });
1563
+ return /* @__PURE__ */ jsx(View, { style: [styles5.root, styles5.centerFill, { backgroundColor: theme.colors.background }], testID: `${testID}${APP_SHELL_SUFFIX.pending}`, children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" }) });
1452
1564
  if (isUnauthenticated) return null;
1453
- const columnStyle = maxWidth === "full" ? styles4.columnFull : [styles4.columnCapped, { maxWidth }];
1454
- const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles4.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
1565
+ const columnStyle = maxWidth === "full" ? styles5.columnFull : [styles5.columnCapped, { maxWidth }];
1566
+ const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles5.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
1455
1567
  const scroller = /* @__PURE__ */ jsx(
1456
1568
  ScrollView,
1457
1569
  {
1458
- contentContainerStyle: [styles4.scrollContent, { padding: contentPadding }],
1459
- style: styles4.scroll,
1570
+ contentContainerStyle: [styles5.scrollContent, { padding: contentPadding }],
1571
+ style: styles5.scroll,
1460
1572
  testID: `${testID}${APP_SHELL_SUFFIX.content}`,
1461
1573
  children: /* @__PURE__ */ jsx(View, { style: columnStyle, children: body })
1462
1574
  }
1463
1575
  );
1464
- const headerRegion = useDrawer ? /* @__PURE__ */ jsxs(View, { style: styles4.headerRow, testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: [
1576
+ const headerRegion = useDrawer ? /* @__PURE__ */ jsxs(View, { style: styles5.headerRow, testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: [
1465
1577
  /* @__PURE__ */ jsx(
1466
1578
  MenuToggle,
1467
1579
  {
@@ -1471,14 +1583,14 @@ var AppShell = ({
1471
1583
  onOpen: openDrawer
1472
1584
  }
1473
1585
  ),
1474
- /* @__PURE__ */ jsx(View, { style: styles4.headerFill, children: renderTextSlot(header, { color: theme.colors.text }) })
1586
+ /* @__PURE__ */ jsx(View, { style: styles5.headerFill, children: renderTextSlot(header, { color: theme.colors.text }) })
1475
1587
  ] }) : /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: renderTextSlot(header, { color: theme.colors.text }) });
1476
1588
  const railRegion = railMode === "full" ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.sidebar}`, children: renderTextSlot(sidebar, { color: theme.colors.text }) }) : railMode === "collapsed" ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.collapsedSidebar}`, children: renderTextSlot(collapsedSidebar, { color: theme.colors.text }) }) : null;
1477
- return /* @__PURE__ */ jsxs(View, { style: [styles4.root, { backgroundColor: theme.colors.background }], testID, children: [
1589
+ return /* @__PURE__ */ jsxs(View, { style: [styles5.root, { backgroundColor: theme.colors.background }], testID, children: [
1478
1590
  headerRegion,
1479
1591
  nav !== void 0 ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.nav}`, children: navInnerStyle !== void 0 ? /* @__PURE__ */ jsx(View, { style: navInnerStyle, children: renderTextSlot(nav, { color: theme.colors.text }) }) : renderTextSlot(nav, { color: theme.colors.text }) }) : null,
1480
1592
  banner !== void 0 ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: renderTextSlot(banner, { color: theme.colors.text }) }) : null,
1481
- railRegion !== null ? /* @__PURE__ */ jsxs(View, { style: styles4.bodyRow, children: [
1593
+ railRegion !== null ? /* @__PURE__ */ jsxs(View, { style: styles5.bodyRow, children: [
1482
1594
  railRegion,
1483
1595
  scroller
1484
1596
  ] }) : scroller,
@@ -1511,6 +1623,7 @@ var CollapsedRail = ({
1511
1623
  const { theme } = useUi();
1512
1624
  const colors = theme.colors;
1513
1625
  const primaryColor = theme.palette.primary["500"];
1626
+ const activeRoute = resolveActiveRoute(items, pathname);
1514
1627
  return /* @__PURE__ */ jsxs(
1515
1628
  View,
1516
1629
  {
@@ -1525,7 +1638,7 @@ var CollapsedRail = ({
1525
1638
  children: [
1526
1639
  header !== void 0 ? /* @__PURE__ */ jsx(View, { style: collapsedRailStyles.slot, children: renderTextSlot(header, { color: colors.text }) }) : null,
1527
1640
  items.map((item) => {
1528
- const active = isRouteActive(pathname, item.route);
1641
+ const active = item.route === activeRoute;
1529
1642
  const iconColor = active ? primaryColor : colors.textSecondary;
1530
1643
  return /* @__PURE__ */ jsx(
1531
1644
  FocusableTouchable,
@@ -1601,6 +1714,7 @@ var NavShell = ({
1601
1714
  header: sideRail.header,
1602
1715
  items: sideRail.items,
1603
1716
  navigateHint,
1717
+ paletteTrigger: sideRail.paletteTrigger,
1604
1718
  pathname,
1605
1719
  regionLabel,
1606
1720
  renderChevron: sideRail.renderChevron,
@@ -1652,6 +1766,7 @@ var PillNav = ({
1652
1766
  const colors = theme.colors;
1653
1767
  const primaryColor = theme.palette.primary["500"];
1654
1768
  if (items.length < minItems) return null;
1769
+ const activeRoute = resolveActiveRoute(items, pathname);
1655
1770
  return /* @__PURE__ */ jsx(
1656
1771
  View,
1657
1772
  {
@@ -1660,7 +1775,7 @@ var PillNav = ({
1660
1775
  role: "navigation",
1661
1776
  style: [pillNavStyles.container, containerStyle],
1662
1777
  children: items.map((item) => {
1663
- const active = isRouteActive(pathname, item.route);
1778
+ const active = item.route === activeRoute;
1664
1779
  const textColor = active ? TEXT_ON_PRIMARY4 : colors.textSecondary;
1665
1780
  const pillStyle = active ? { backgroundColor: primaryColor } : { backgroundColor: colors.surfaceElevated };
1666
1781
  return /* @__PURE__ */ jsxs(
@@ -1776,7 +1891,7 @@ var KEY_DOWN = "ArrowDown";
1776
1891
  var KEY_UP = "ArrowUp";
1777
1892
  var KEY_ENTER = "Enter";
1778
1893
  var KEY_ESCAPE = "Escape";
1779
- var styles5 = StyleSheet.create({
1894
+ var styles6 = StyleSheet.create({
1780
1895
  overlay: { ...StyleSheet.absoluteFillObject, zIndex: OVERLAY_Z_INDEX, alignItems: "center" },
1781
1896
  scrim: { ...StyleSheet.absoluteFillObject, backgroundColor: SCRIM_COLOR2 },
1782
1897
  panel: {
@@ -1805,14 +1920,14 @@ var PaletteRow = ({ item, selected, onChoose }) => {
1805
1920
  accessibilityLabel: item.label,
1806
1921
  accessibilityRole: "button",
1807
1922
  accessibilityState: { selected },
1808
- style: [styles5.row, selected ? { backgroundColor: colors.border } : void 0],
1923
+ style: [styles6.row, selected ? { backgroundColor: colors.border } : void 0],
1809
1924
  testID: item.testID ?? item.key,
1810
1925
  onPress: () => onChoose(item),
1811
1926
  children: [
1812
1927
  typeof item.renderIcon === "function" ? item.renderIcon(colors.textSecondary, NAV_ICON_SIZE) : null,
1813
- /* @__PURE__ */ jsxs(View, { style: styles5.rowText, children: [
1814
- /* @__PURE__ */ jsx(Text, { style: [styles5.rowLabel, { color: colors.text }], children: item.label }),
1815
- item.hint ? /* @__PURE__ */ jsx(Text, { style: [styles5.rowHint, { color: colors.textSecondary }], children: item.hint }) : null
1928
+ /* @__PURE__ */ jsxs(View, { style: styles6.rowText, children: [
1929
+ /* @__PURE__ */ jsx(Text, { style: [styles6.rowLabel, { color: colors.text }], children: item.label }),
1930
+ item.hint ? /* @__PURE__ */ jsx(Text, { style: [styles6.rowHint, { color: colors.textSecondary }], children: item.hint }) : null
1816
1931
  ] })
1817
1932
  ]
1818
1933
  }
@@ -1852,14 +1967,14 @@ var CommandPalette = ({ open, items, onClose, labels, testID }) => {
1852
1967
  [results, selected, choose, onClose]
1853
1968
  );
1854
1969
  if (!open) return null;
1855
- return /* @__PURE__ */ jsxs(View, { style: styles5.overlay, children: [
1970
+ return /* @__PURE__ */ jsxs(View, { style: styles6.overlay, children: [
1856
1971
  /* @__PURE__ */ jsx(
1857
1972
  Pressable,
1858
1973
  {
1859
1974
  accessibilityHint: labels.closeHint,
1860
1975
  accessibilityLabel: labels.closeLabel,
1861
1976
  accessibilityRole: "button",
1862
- style: styles5.scrim,
1977
+ style: styles6.scrim,
1863
1978
  testID: `${testID ?? "command-palette"}-scrim`,
1864
1979
  onPress: onClose
1865
1980
  }
@@ -1870,7 +1985,7 @@ var CommandPalette = ({ open, items, onClose, labels, testID }) => {
1870
1985
  "aria-modal": true,
1871
1986
  "aria-label": labels.regionLabel,
1872
1987
  role: "dialog",
1873
- style: [styles5.panel, { backgroundColor: colors.surface, borderColor: colors.border }],
1988
+ style: [styles6.panel, { backgroundColor: colors.surface, borderColor: colors.border }],
1874
1989
  testID: testID ?? "command-palette",
1875
1990
  children: [
1876
1991
  /* @__PURE__ */ jsx(
@@ -1880,68 +1995,19 @@ var CommandPalette = ({ open, items, onClose, labels, testID }) => {
1880
1995
  accessibilityLabel: labels.placeholder,
1881
1996
  placeholder: labels.placeholder,
1882
1997
  placeholderTextColor: colors.textSecondary,
1883
- style: [styles5.input, { color: colors.text, borderBottomColor: colors.border }],
1998
+ style: [styles6.input, { color: colors.text, borderBottomColor: colors.border }],
1884
1999
  testID: `${testID ?? "command-palette"}-input`,
1885
2000
  value: query,
1886
2001
  onChangeText: setQuery,
1887
2002
  onKeyPress
1888
2003
  }
1889
2004
  ),
1890
- results.length === 0 ? /* @__PURE__ */ jsx(Text, { style: [styles5.empty, { color: colors.textSecondary }], children: labels.emptyText }) : /* @__PURE__ */ jsx(ScrollView, { keyboardShouldPersistTaps: "handled", style: styles5.list, children: results.map((item, index) => /* @__PURE__ */ jsx(PaletteRow, { item, selected: index === selected, onChoose: choose }, item.key)) })
2005
+ results.length === 0 ? /* @__PURE__ */ jsx(Text, { style: [styles6.empty, { color: colors.textSecondary }], children: labels.emptyText }) : /* @__PURE__ */ jsx(ScrollView, { keyboardShouldPersistTaps: "handled", style: styles6.list, children: results.map((item, index) => /* @__PURE__ */ jsx(PaletteRow, { item, selected: index === selected, onChoose: choose }, item.key)) })
1891
2006
  ]
1892
2007
  }
1893
2008
  )
1894
2009
  ] });
1895
2010
  };
1896
- var MIN_TARGET = 36;
1897
- var styles6 = StyleSheet.create({
1898
- trigger: {
1899
- flexDirection: "row",
1900
- alignItems: "center",
1901
- justifyContent: "space-between",
1902
- columnGap: 8,
1903
- paddingHorizontal: 12,
1904
- minHeight: MIN_TARGET,
1905
- borderWidth: 1,
1906
- borderRadius: 8
1907
- },
1908
- label: { fontSize: 14 },
1909
- badge: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, borderWidth: 1 },
1910
- badgeText: { fontSize: 11, fontWeight: "700" }
1911
- });
1912
- var CommandPaletteTrigger = ({
1913
- label,
1914
- hint,
1915
- shortcut,
1916
- onPress,
1917
- testID
1918
- }) => {
1919
- const { theme } = useUi();
1920
- const colors = theme.colors;
1921
- const primary = theme.palette.primary["500"];
1922
- const [focused, setFocused] = React3.useState(false);
1923
- return /* @__PURE__ */ jsxs(
1924
- Pressable,
1925
- {
1926
- accessibilityHint: hint,
1927
- accessibilityLabel: label,
1928
- accessibilityRole: "button",
1929
- style: [
1930
- styles6.trigger,
1931
- { backgroundColor: colors.surface, borderColor: colors.border },
1932
- focusRingStyle(focused, primary)
1933
- ],
1934
- testID,
1935
- onBlur: () => setFocused(false),
1936
- onFocus: () => setFocused(true),
1937
- onPress,
1938
- children: [
1939
- /* @__PURE__ */ jsx(Text, { style: [styles6.label, { color: colors.textSecondary }], children: label }),
1940
- /* @__PURE__ */ jsx(View, { style: [styles6.badge, { borderColor: colors.border }], children: /* @__PURE__ */ jsx(Text, { style: [styles6.badgeText, { color: colors.textSecondary }], children: shortcut }) })
1941
- ]
1942
- }
1943
- );
1944
- };
1945
2011
  var LAUNCH_KEY = "k";
1946
2012
  function useCommandPaletteHotkey(onOpen, enabled = true) {
1947
2013
  useEffect(() => {
@@ -1967,6 +2033,6 @@ function accessibleNavItems(user, table, translate) {
1967
2033
  return roleRoutesToNavItems(resolveAccessibleRoutes(user, table), translate);
1968
2034
  }
1969
2035
 
1970
- export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, AppShell, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, CommandPalette, CommandPaletteTrigger, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, NAV_TEST_IDS, Nav, NavBar, NavExpandableItem, NavOverflowMenu, NavShell, PillNav, RAIL_FULL_BREAKPOINT, Sidebar, SidebarChevron, SidebarSearch, Topbar, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveContentMaxWidth, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };
2036
+ export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, AppShell, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, CommandPalette, CommandPaletteTrigger, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, NAV_TEST_IDS, Nav, NavBar, NavExpandableItem, NavOverflowMenu, NavShell, PillNav, RAIL_FULL_BREAKPOINT, Sidebar, SidebarChevron, SidebarSearch, Topbar, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveActiveRoute, resolveContentMaxWidth, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };
1971
2037
  //# sourceMappingURL=index.mjs.map
1972
2038
  //# sourceMappingURL=index.mjs.map