@dloizides/ui-nav 1.6.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,10 +2,15 @@
2
2
 
3
3
  var reactNative = require('react-native');
4
4
  var uiFeedback = require('@dloizides/ui-feedback');
5
- var react = require('react');
5
+ var React5 = require('react');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
+ var uiLayout = require('@dloizides/ui-layout');
7
8
  var authWeb = require('@dloizides/auth-web');
8
9
 
10
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
+
12
+ var React5__default = /*#__PURE__*/_interopDefault(React5);
13
+
9
14
  // src/Sidebar.tsx
10
15
 
11
16
  // src/isRouteActive.ts
@@ -14,6 +19,7 @@ function isRouteActive(pathname, route) {
14
19
  return pathname === route || pathname.startsWith(`${route}/`);
15
20
  }
16
21
  var ACTIVE_BORDER_RADIUS = 4;
22
+ var NAV_LINK_GAP = 4;
17
23
  var navStyles = reactNative.StyleSheet.create({
18
24
  // --- Sidebar ---
19
25
  sidebarContainer: {
@@ -96,22 +102,26 @@ var navBarStyles = reactNative.StyleSheet.create({
96
102
  justifyContent: "center"
97
103
  },
98
104
  toggleGlyph: { fontSize: 20, fontWeight: "700" },
99
- // Expanded links live inside a horizontal ScrollView so the bar stays ONE ROW no matter how many
100
- // items an app passes: they never wrap onto a second line any overflow scrolls instead, with the
101
- // brand (left) and right slot pinned. `linksScroll` is the ScrollView's own box (a shrinkable,
102
- // growable flex child that may collapse to 0 and scroll); `links` is its non-wrapping content row.
103
- linksScroll: {
105
+ // Expanded links live on ONE ROW that never wraps: it is a shrinkable/growable flex child that
106
+ // fills the space between the pinned brand (left) and right slot, and CLIPS overflow. A measure
107
+ // pass (priority+) then keeps only the leading items that fit inline and collapses the rest behind
108
+ // the "More ▾" overflow trigger so no item is ever cut off; the surplus moves into a dropdown.
109
+ // `overflow:'hidden'` only masks the single unmeasured first frame before the fit resolves.
110
+ linksRow: {
104
111
  flexGrow: 1,
105
112
  flexShrink: 1,
106
- minWidth: 0
107
- },
108
- links: {
113
+ minWidth: 0,
109
114
  flexDirection: "row",
110
115
  alignItems: "center",
111
116
  flexWrap: "nowrap",
112
- columnGap: 4,
113
- rowGap: 4
117
+ columnGap: NAV_LINK_GAP,
118
+ overflow: "hidden"
114
119
  },
120
+ // Each inline link + the overflow trigger sit in a natural-width cell that never shrinks, so their
121
+ // measured widths are their true content widths (what the priority+ fit computation reasons about).
122
+ linkCell: { flexShrink: 0 },
123
+ overflowTrigger: { columnGap: 4 },
124
+ overflowChevron: { fontSize: 12, fontWeight: "700" },
115
125
  // Collapsed (below breakpoint): links drop onto their own full-width line,
116
126
  // stacked vertically — mirrors the v1 `.gn-links` responsive behaviour.
117
127
  linksStacked: {
@@ -151,6 +161,86 @@ var navBarStyles = reactNative.StyleSheet.create({
151
161
  columnGap: 8
152
162
  }
153
163
  });
164
+ var pillNavStyles = reactNative.StyleSheet.create({
165
+ // Cross-nav pill row (kefi coral dashboard switcher): fully-rounded pills, wraps.
166
+ container: {
167
+ flexDirection: "row",
168
+ flexWrap: "wrap",
169
+ alignItems: "center",
170
+ columnGap: 8,
171
+ rowGap: 8,
172
+ paddingVertical: 8
173
+ },
174
+ pill: {
175
+ paddingHorizontal: 16,
176
+ paddingVertical: 8,
177
+ borderRadius: 999,
178
+ minHeight: 36,
179
+ flexDirection: "row",
180
+ alignItems: "center",
181
+ columnGap: 6,
182
+ justifyContent: "center"
183
+ },
184
+ pillText: { fontSize: 14, fontWeight: "600" }
185
+ });
186
+ var darkModeStyles = reactNative.StyleSheet.create({
187
+ // Segmented Light / Dark / System control.
188
+ segmentRow: {
189
+ flexDirection: "row",
190
+ alignItems: "center",
191
+ borderWidth: 1,
192
+ borderRadius: 8,
193
+ overflow: "hidden",
194
+ alignSelf: "flex-start"
195
+ },
196
+ segment: {
197
+ paddingHorizontal: 12,
198
+ paddingVertical: 8,
199
+ minHeight: 36,
200
+ flexDirection: "row",
201
+ alignItems: "center",
202
+ columnGap: 6,
203
+ justifyContent: "center"
204
+ },
205
+ segmentText: { fontSize: 13, fontWeight: "600" },
206
+ // Cycle-button variant.
207
+ cycle: {
208
+ paddingHorizontal: 12,
209
+ paddingVertical: 8,
210
+ minHeight: 44,
211
+ minWidth: 44,
212
+ borderRadius: 8,
213
+ flexDirection: "row",
214
+ alignItems: "center",
215
+ columnGap: 6,
216
+ justifyContent: "center"
217
+ },
218
+ cycleText: { fontSize: 13, fontWeight: "600" }
219
+ });
220
+ var collapsedRailStyles = reactNative.StyleSheet.create({
221
+ // Intermediate (tablet) icon-only rail: a narrow vertical strip of icon buttons.
222
+ container: {
223
+ width: 64,
224
+ paddingTop: 16,
225
+ paddingHorizontal: 8,
226
+ borderRightWidth: 1,
227
+ height: "100%",
228
+ alignItems: "center"
229
+ },
230
+ // Each item is a ≥44×44 centred touch target with a rounded active/hover pill.
231
+ item: {
232
+ width: 48,
233
+ height: 48,
234
+ marginVertical: 4,
235
+ borderRadius: 8,
236
+ alignItems: "center",
237
+ justifyContent: "center"
238
+ },
239
+ // Glyph fallback (the label's first character) when an item has no renderIcon.
240
+ glyph: { fontSize: 18, fontWeight: "700" },
241
+ spacer: { flex: 1 },
242
+ slot: { alignItems: "center" }
243
+ });
154
244
  var expandableStyles = reactNative.StyleSheet.create({
155
245
  childItem: {
156
246
  borderRadius: 4,
@@ -209,21 +299,21 @@ var NavExpandableItem = ({
209
299
  renderChevron,
210
300
  depth = 0
211
301
  }) => {
212
- const [expanded, setExpanded] = react.useState(false);
213
- const [focused, setFocused] = react.useState(false);
302
+ const [expanded, setExpanded] = React5.useState(false);
303
+ const [focused, setFocused] = React5.useState(false);
214
304
  const { theme } = uiFeedback.useUi();
215
305
  const colors = theme.colors;
216
306
  const primaryColor = theme.palette.primary["500"];
217
- const toggle = react.useCallback(() => setExpanded((v) => !v), []);
307
+ const toggle = React5.useCallback(() => setExpanded((v) => !v), []);
218
308
  const indent = depth * BASE_INDENT;
219
309
  const hasChildren = Array.isArray(item.children) && item.children.length > 0;
220
310
  const isActive = isRouteActive(pathname, item.route);
221
- const activeItemStyle = react.useMemo(
311
+ const activeItemStyle = React5.useMemo(
222
312
  () => ({ backgroundColor: colors.border, borderRadius: ACTIVE_BORDER_RADIUS }),
223
313
  [colors.border]
224
314
  );
225
- const paddingStyle = react.useMemo(() => ({ paddingLeft: indent + BASE_INDENT }), [indent]);
226
- const headerPaddingStyle = react.useMemo(() => ({ paddingLeft: indent }), [indent]);
315
+ const paddingStyle = React5.useMemo(() => ({ paddingLeft: indent + BASE_INDENT }), [indent]);
316
+ const headerPaddingStyle = React5.useMemo(() => ({ paddingLeft: indent }), [indent]);
227
317
  if (!hasChildren)
228
318
  return /* @__PURE__ */ jsxRuntime.jsxs(
229
319
  reactNative.TouchableOpacity,
@@ -357,7 +447,9 @@ var NAV_TEST_IDS = {
357
447
  /** responsive hamburger toggle in the horizontal NavBar (shown below the breakpoint). */
358
448
  navBarToggle: "navbar-toggle",
359
449
  /** links container in the horizontal NavBar. */
360
- navBarLinks: "navbar-links"
450
+ navBarLinks: "navbar-links",
451
+ /** the "More ▾" priority+ overflow trigger in the horizontal NavBar (shown when items spill). */
452
+ navBarOverflow: "navbar-overflow"
361
453
  };
362
454
  var APP_SHELL_SUFFIX = {
363
455
  content: "-content",
@@ -365,6 +457,8 @@ var APP_SHELL_SUFFIX = {
365
457
  nav: "-nav",
366
458
  /** The persistent left rail of the back-office layout (only when `sidebar` is supplied). */
367
459
  sidebar: "-sidebar",
460
+ /** The intermediate persistent COLLAPSED (icon-only) rail (3-tier layout, tablet band). */
461
+ collapsedSidebar: "-collapsed-sidebar",
368
462
  /** Mobile hamburger that opens the nav drawer (back-office layout, below the breakpoint). */
369
463
  menuToggle: "-menu-toggle",
370
464
  /** The mobile nav drawer panel (holds the `sidebar` slot when opened on a narrow viewport). */
@@ -387,7 +481,7 @@ var FocusableTouchable = ({
387
481
  testID,
388
482
  children
389
483
  }) => {
390
- const [focused, setFocused] = react.useState(false);
484
+ const [focused, setFocused] = React5.useState(false);
391
485
  return /* @__PURE__ */ jsxRuntime.jsx(
392
486
  reactNative.Pressable,
393
487
  {
@@ -539,8 +633,8 @@ var NavBarLink = ({
539
633
  navigateHint,
540
634
  onPress
541
635
  }) => {
542
- const [hovered, setHovered] = react.useState(false);
543
- const [focused, setFocused] = react.useState(false);
636
+ const [hovered, setHovered] = React5.useState(false);
637
+ const [focused, setFocused] = React5.useState(false);
544
638
  const isHovered = hovered && !isActive;
545
639
  const textColor = isActive ? TEXT_ON_PRIMARY2 : isHovered ? colors.hoverText : colors.rest;
546
640
  const pillStyle = isActive ? { backgroundColor: colors.activeBg } : isHovered ? { backgroundColor: colors.hoverBg } : void 0;
@@ -572,14 +666,116 @@ var NavBarLink = ({
572
666
  }
573
667
  );
574
668
  };
669
+ var TEXT_ON_PRIMARY3 = "#ffffff";
670
+ var NO_ACTIVE_ROUTE = "";
671
+ var NavOverflowMenu = ({
672
+ items,
673
+ pathname,
674
+ onNavigate,
675
+ colors,
676
+ label,
677
+ hint,
678
+ testID,
679
+ variant
680
+ }) => {
681
+ const options = React5.useMemo(() => items.map((item) => ({ label: item.label, value: item.route })), [items]);
682
+ const activeRoute = React5.useMemo(
683
+ () => items.find((item) => isRouteActive(pathname, item.route))?.route ?? NO_ACTIVE_ROUTE,
684
+ [items, pathname]
685
+ );
686
+ const optionTestID = React5.useMemo(() => {
687
+ const byRoute = new Map(items.map((item) => [item.route, item.testID ?? item.key]));
688
+ return (route) => byRoute.get(route) ?? `${testID}-option-${route}`;
689
+ }, [items, testID]);
690
+ const hasActive = activeRoute !== NO_ACTIVE_ROUTE;
691
+ const textColor = hasActive ? TEXT_ON_PRIMARY3 : colors.rest;
692
+ const pillStyle = hasActive ? { backgroundColor: colors.activeBg } : void 0;
693
+ return /* @__PURE__ */ jsxRuntime.jsx(
694
+ uiLayout.ModalDropdown,
695
+ {
696
+ accessibilityHint: hint,
697
+ accessibilityLabel: label,
698
+ optionTestID,
699
+ options,
700
+ testID,
701
+ value: activeRoute,
702
+ variant,
703
+ onChange: onNavigate,
704
+ renderTrigger: ({ isOpen }) => /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [navBarStyles.link, navBarStyles.overflowTrigger, pillStyle], children: [
705
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navBarStyles.linkText, { color: textColor }], children: label }),
706
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navBarStyles.overflowChevron, { color: textColor }], children: isOpen ? "\u25B4" : "\u25BE" })
707
+ ] })
708
+ }
709
+ );
710
+ };
711
+
712
+ // src/computeVisibleCount.ts
713
+ function sum(values) {
714
+ return values.reduce((acc, value) => acc + value, 0);
715
+ }
716
+ function computeVisibleCount({ availableWidth, itemWidths, moreWidth, gap }) {
717
+ const total = itemWidths.length;
718
+ if (total === 0) return 0;
719
+ if (availableWidth <= 0 || itemWidths.some((width) => width <= 0)) return total;
720
+ const fullRowWidth = sum(itemWidths) + gap * (total - 1);
721
+ if (fullRowWidth <= availableWidth) return total;
722
+ const reservedForMore = moreWidth + gap;
723
+ let used = 0;
724
+ let count = 0;
725
+ for (let index = 0; index < total; index += 1) {
726
+ const width = itemWidths[index] ?? 0;
727
+ const withGap = used + (count > 0 ? gap : 0) + width;
728
+ if (withGap + reservedForMore > availableWidth) break;
729
+ used = withGap;
730
+ count += 1;
731
+ }
732
+ return count;
733
+ }
734
+
735
+ // src/useNavOverflow.ts
736
+ function resizeWidthBuffer(previous, count) {
737
+ const next = new Array(count).fill(0);
738
+ const shared = Math.min(previous.length, count);
739
+ for (let index = 0; index < shared; index += 1) next[index] = previous[index] ?? 0;
740
+ return next;
741
+ }
742
+ function useNavOverflow(itemCount, gap) {
743
+ const [availableWidth, setAvailableWidthState] = React5.useState(0);
744
+ const [moreWidth, setMoreWidthState] = React5.useState(0);
745
+ const [itemWidths, setItemWidths] = React5.useState(() => new Array(itemCount).fill(0));
746
+ React5.useEffect(() => {
747
+ setItemWidths((previous) => previous.length === itemCount ? previous : resizeWidthBuffer(previous, itemCount));
748
+ }, [itemCount]);
749
+ const setItemWidth = React5.useCallback((index, width) => {
750
+ setItemWidths((previous) => {
751
+ if (index < 0 || index >= previous.length || previous[index] === width) return previous;
752
+ const next = previous.slice();
753
+ next[index] = width;
754
+ return next;
755
+ });
756
+ }, []);
757
+ const setAvailableWidth = React5.useCallback(
758
+ (width) => setAvailableWidthState((previous) => previous === width ? previous : width),
759
+ []
760
+ );
761
+ const setMoreWidth = React5.useCallback(
762
+ (width) => setMoreWidthState((previous) => previous === width ? previous : width),
763
+ []
764
+ );
765
+ const visibleCount = React5.useMemo(
766
+ () => computeVisibleCount({ availableWidth, itemWidths, moreWidth, gap }),
767
+ [availableWidth, itemWidths, moreWidth, gap]
768
+ );
769
+ return { visibleCount, setAvailableWidth, setItemWidth, setMoreWidth };
770
+ }
575
771
  var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
576
772
  function getReducedMotionQuery() {
577
773
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return void 0;
578
774
  return window.matchMedia(REDUCED_MOTION_QUERY);
579
775
  }
580
776
  function useReducedMotion() {
581
- const [reduced, setReduced] = react.useState(() => getReducedMotionQuery()?.matches ?? false);
582
- react.useEffect(() => {
777
+ const [reduced, setReduced] = React5.useState(() => getReducedMotionQuery()?.matches ?? false);
778
+ React5.useEffect(() => {
583
779
  const mql = getReducedMotionQuery();
584
780
  if (mql === void 0 || mql.addEventListener === void 0) return void 0;
585
781
  const onChange = () => setReduced(mql.matches);
@@ -596,7 +792,7 @@ var MENU_GLYPH = "\u2630";
596
792
  function ariaControlsProps(id) {
597
793
  return { "aria-controls": id };
598
794
  }
599
- var NavBar = ({
795
+ var NavBarInner = ({
600
796
  items,
601
797
  pathname,
602
798
  onNavigate,
@@ -608,6 +804,8 @@ var NavBar = ({
608
804
  menuHint = "",
609
805
  renderMenuIcon,
610
806
  collapseBelow = DEFAULT_COLLAPSE_BELOW,
807
+ overflowLabel = "More",
808
+ overflowHint = "",
611
809
  containerStyle
612
810
  }) => {
613
811
  const { theme } = uiFeedback.useUi();
@@ -615,19 +813,20 @@ var NavBar = ({
615
813
  const primaryColor = theme.palette.primary["500"];
616
814
  const { width } = reactNative.useWindowDimensions();
617
815
  const reducedMotion = useReducedMotion();
618
- const [open, setOpen] = react.useState(false);
619
- const [toggleFocused, setToggleFocused] = react.useState(false);
816
+ const [open, setOpen] = React5.useState(false);
817
+ const [toggleFocused, setToggleFocused] = React5.useState(false);
818
+ const { visibleCount, setAvailableWidth, setItemWidth, setMoreWidth } = useNavOverflow(items.length, NAV_LINK_GAP);
620
819
  const collapsed = width < collapseBelow;
621
820
  const showLinks = !collapsed || open;
622
- const toggleMenu = react.useCallback(() => setOpen((v) => !v), []);
623
- const handlePress = react.useCallback(
821
+ const toggleMenu = React5.useCallback(() => setOpen((v) => !v), []);
822
+ const handlePress = React5.useCallback(
624
823
  (route) => {
625
824
  setOpen(false);
626
825
  onNavigate(route);
627
826
  },
628
827
  [onNavigate]
629
828
  );
630
- const linkColors = react.useMemo(
829
+ const linkColors = React5.useMemo(
631
830
  () => ({
632
831
  rest: colors.textSecondary,
633
832
  hoverText: colors.text,
@@ -668,7 +867,7 @@ var NavBar = ({
668
867
  }
669
868
  ) : null,
670
869
  showLinks ? (() => {
671
- const links = items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
870
+ const renderLink = (item) => /* @__PURE__ */ jsxRuntime.jsx(
672
871
  NavBarLink,
673
872
  {
674
873
  collapsed,
@@ -679,19 +878,50 @@ var NavBar = ({
679
878
  navigateHint,
680
879
  reducedMotion,
681
880
  onPress: handlePress
682
- },
683
- item.key
684
- ));
685
- return collapsed ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { nativeID: LINKS_REGION_ID, style: navBarStyles.linksStacked, testID: NAV_TEST_IDS.navBarLinks, children: links }) : /* @__PURE__ */ jsxRuntime.jsx(
686
- reactNative.ScrollView,
881
+ }
882
+ );
883
+ if (collapsed) {
884
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { nativeID: LINKS_REGION_ID, style: navBarStyles.linksStacked, testID: NAV_TEST_IDS.navBarLinks, children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(React5__default.default.Fragment, { children: renderLink(item) }, item.key)) });
885
+ }
886
+ const visibleItems = items.slice(0, visibleCount);
887
+ const overflowItems = items.slice(visibleCount);
888
+ return /* @__PURE__ */ jsxRuntime.jsxs(
889
+ reactNative.View,
687
890
  {
688
- horizontal: true,
689
- showsHorizontalScrollIndicator: false,
690
891
  nativeID: LINKS_REGION_ID,
691
- style: navBarStyles.linksScroll,
692
- contentContainerStyle: navBarStyles.links,
892
+ style: navBarStyles.linksRow,
693
893
  testID: NAV_TEST_IDS.navBarLinks,
694
- children: links
894
+ onLayout: (event) => setAvailableWidth(event.nativeEvent.layout.width),
895
+ children: [
896
+ visibleItems.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
897
+ reactNative.View,
898
+ {
899
+ style: navBarStyles.linkCell,
900
+ onLayout: (event) => setItemWidth(index, event.nativeEvent.layout.width),
901
+ children: renderLink(item)
902
+ },
903
+ item.key
904
+ )),
905
+ overflowItems.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
906
+ reactNative.View,
907
+ {
908
+ style: navBarStyles.linkCell,
909
+ onLayout: (event) => setMoreWidth(event.nativeEvent.layout.width),
910
+ children: /* @__PURE__ */ jsxRuntime.jsx(
911
+ NavOverflowMenu,
912
+ {
913
+ colors: linkColors,
914
+ hint: overflowHint,
915
+ items: overflowItems,
916
+ label: overflowLabel,
917
+ pathname,
918
+ testID: NAV_TEST_IDS.navBarOverflow,
919
+ onNavigate: handlePress
920
+ }
921
+ )
922
+ }
923
+ ) : null
924
+ ]
695
925
  }
696
926
  );
697
927
  })() : null,
@@ -700,7 +930,57 @@ var NavBar = ({
700
930
  }
701
931
  );
702
932
  };
703
- var MOBILE_BREAKPOINT = 768;
933
+ var NavBar = ({ barTheme, ...props }) => {
934
+ const { t, navigate } = uiFeedback.useUi();
935
+ if (barTheme === void 0) return /* @__PURE__ */ jsxRuntime.jsx(NavBarInner, { ...props });
936
+ return /* @__PURE__ */ jsxRuntime.jsx(uiFeedback.UiProvider, { navigate, t, theme: barTheme, children: /* @__PURE__ */ jsxRuntime.jsx(NavBarInner, { ...props }) });
937
+ };
938
+ var Nav = (props) => {
939
+ if (props.orientation === "vertical") {
940
+ return /* @__PURE__ */ jsxRuntime.jsx(Sidebar, { ...props });
941
+ }
942
+ return /* @__PURE__ */ jsxRuntime.jsx(NavBar, { ...props });
943
+ };
944
+ var CARD_PADDING = 20;
945
+ var CARD_BORDER_RADIUS = 12;
946
+ var CARD_BORDER_WIDTH = 1;
947
+ var CARD_TITLE_FONT_SIZE = 16;
948
+ var CARD_MESSAGE_FONT_SIZE = 14;
949
+ var CARD_TITLE_MARGIN_BOTTOM = 8;
950
+ var styles = reactNative.StyleSheet.create({
951
+ centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
952
+ card: {
953
+ padding: CARD_PADDING,
954
+ borderRadius: CARD_BORDER_RADIUS,
955
+ borderWidth: CARD_BORDER_WIDTH
956
+ },
957
+ cardTitle: { fontSize: CARD_TITLE_FONT_SIZE, fontWeight: "700", marginBottom: CARD_TITLE_MARGIN_BOTTOM },
958
+ cardMessage: { fontSize: CARD_MESSAGE_FONT_SIZE }
959
+ });
960
+ function MessageCard({
961
+ message,
962
+ accentColor,
963
+ testID
964
+ }) {
965
+ const { theme } = uiFeedback.useUi();
966
+ const colors = theme.colors;
967
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.card, { backgroundColor: colors.surface, borderColor: accentColor }], testID, children: [
968
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.cardTitle, { color: accentColor }], children: message.titleText }),
969
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.cardMessage, { color: colors.textSecondary }], children: message.messageText })
970
+ ] });
971
+ }
972
+ function useContentBody(state, children, testID) {
973
+ const { theme } = uiFeedback.useUi();
974
+ const primary = theme.palette.primary["500"];
975
+ const errorColor = theme.semantic.error["500"];
976
+ if (state?.forbidden)
977
+ return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.forbidden, testID: `${testID}${APP_SHELL_SUFFIX.forbidden}` });
978
+ if (state?.error)
979
+ return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
980
+ if (state?.loading === true)
981
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.centerFill, testID: `${testID}${APP_SHELL_SUFFIX.loading}`, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: primary, size: "large" }) });
982
+ return children;
983
+ }
704
984
  var SCRIM_COLOR = "rgba(0, 0, 0, 0.5)";
705
985
  var MENU_GLYPH2 = "\u2630";
706
986
  var MENU_MIN_TARGET = 44;
@@ -714,7 +994,7 @@ var DEFAULT_DRAWER_LABELS = {
714
994
  closeLabel: "Close menu",
715
995
  closeHint: "Close the navigation menu"
716
996
  };
717
- var styles = reactNative.StyleSheet.create({
997
+ var styles2 = reactNative.StyleSheet.create({
718
998
  overlay: { ...reactNative.StyleSheet.absoluteFillObject, zIndex: DRAWER_Z_INDEX },
719
999
  scrim: { ...reactNative.StyleSheet.absoluteFillObject, backgroundColor: SCRIM_COLOR },
720
1000
  panel: { position: "absolute", top: 0, bottom: 0, left: 0 },
@@ -735,10 +1015,10 @@ var MenuToggle = ({ label, hint, onOpen, testID }) => {
735
1015
  accessibilityHint: hint,
736
1016
  accessibilityLabel: label,
737
1017
  ringColor: theme.palette.primary["500"],
738
- style: styles.menuToggle,
1018
+ style: styles2.menuToggle,
739
1019
  testID,
740
1020
  onPress: onOpen,
741
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.menuToggleGlyph, { color: theme.colors.text }], children: MENU_GLYPH2 })
1021
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.menuToggleGlyph, { color: theme.colors.text }], children: MENU_GLYPH2 })
742
1022
  }
743
1023
  );
744
1024
  };
@@ -749,8 +1029,8 @@ var MobileDrawer = ({
749
1029
  drawerTestID,
750
1030
  scrimTestID
751
1031
  }) => {
752
- const panelRef = react.useRef(null);
753
- react.useEffect(() => {
1032
+ const panelRef = React5.useRef(null);
1033
+ React5.useEffect(() => {
754
1034
  const node = panelRef.current;
755
1035
  if (node === null || typeof node.addEventListener !== "function") return void 0;
756
1036
  const handleClick = (event) => {
@@ -761,21 +1041,34 @@ var MobileDrawer = ({
761
1041
  node.addEventListener("click", handleClick);
762
1042
  return () => node.removeEventListener("click", handleClick);
763
1043
  }, [onClose]);
764
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.overlay, children: [
1044
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.overlay, children: [
765
1045
  /* @__PURE__ */ jsxRuntime.jsx(
766
1046
  reactNative.Pressable,
767
1047
  {
768
1048
  accessibilityHint: labels.closeHint,
769
1049
  accessibilityLabel: labels.closeLabel,
770
1050
  accessibilityRole: "button",
771
- style: styles.scrim,
1051
+ style: styles2.scrim,
772
1052
  testID: scrimTestID,
773
1053
  onPress: onClose
774
1054
  }
775
1055
  ),
776
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ref: panelRef, "aria-modal": true, role: "dialog", style: styles.panel, testID: drawerTestID, children: sidebar })
1056
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ref: panelRef, "aria-modal": true, role: "dialog", style: styles2.panel, testID: drawerTestID, children: sidebar })
777
1057
  ] });
778
1058
  };
1059
+
1060
+ // src/railMode.ts
1061
+ var RAIL_FULL_BREAKPOINT = 768;
1062
+ var DEFAULT_COLLAPSED_RAIL_MAX = 1024;
1063
+ function resolveRailMode({ viewport, hasSidebar, hasCollapsed, range }) {
1064
+ if (!hasSidebar) return "none";
1065
+ if (hasCollapsed) {
1066
+ if (viewport >= range.max) return "full";
1067
+ if (viewport >= range.min) return "collapsed";
1068
+ return "drawer";
1069
+ }
1070
+ return viewport >= RAIL_FULL_BREAKPOINT ? "full" : "drawer";
1071
+ }
779
1072
  function resolveContentMaxWidth(width, viewport) {
780
1073
  if (width === "full") return "full";
781
1074
  const wideMinViewport = width.wideMinViewport ?? Number.POSITIVE_INFINITY;
@@ -787,13 +1080,7 @@ function useContentMaxWidth(width) {
787
1080
  return resolveContentMaxWidth(width, viewport);
788
1081
  }
789
1082
  var DEFAULT_CONTENT_PADDING = 24;
790
- var CARD_PADDING = 20;
791
- var CARD_BORDER_RADIUS = 12;
792
- var CARD_BORDER_WIDTH = 1;
793
- var CARD_TITLE_FONT_SIZE = 16;
794
- var CARD_MESSAGE_FONT_SIZE = 14;
795
- var CARD_TITLE_MARGIN_BOTTOM = 8;
796
- var styles2 = reactNative.StyleSheet.create({
1083
+ var styles3 = reactNative.StyleSheet.create({
797
1084
  root: { flex: 1 },
798
1085
  centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
799
1086
  scroll: { flex: 1 },
@@ -807,50 +1094,14 @@ var styles2 = reactNative.StyleSheet.create({
807
1094
  columnCapped: { flex: 1, width: "100%", alignSelf: "center" },
808
1095
  // Non-flex variant used to cap the nav strip's inner content to the content
809
1096
  // column so the nav aligns with the page content instead of running full-bleed.
810
- chromeCapped: { width: "100%", alignSelf: "center" },
811
- card: {
812
- padding: CARD_PADDING,
813
- borderRadius: CARD_BORDER_RADIUS,
814
- borderWidth: CARD_BORDER_WIDTH
815
- },
816
- cardTitle: { fontSize: CARD_TITLE_FONT_SIZE, fontWeight: "700", marginBottom: CARD_TITLE_MARGIN_BOTTOM },
817
- cardMessage: { fontSize: CARD_MESSAGE_FONT_SIZE }
1097
+ chromeCapped: { width: "100%", alignSelf: "center" }
818
1098
  });
819
- function MessageCard({
820
- message,
821
- accentColor,
822
- testID
823
- }) {
824
- const { theme } = uiFeedback.useUi();
825
- const colors = theme.colors;
826
- return /* @__PURE__ */ jsxRuntime.jsxs(
827
- reactNative.View,
828
- {
829
- style: [styles2.card, { backgroundColor: colors.surface, borderColor: accentColor }],
830
- testID,
831
- children: [
832
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.cardTitle, { color: accentColor }], children: message.titleText }),
833
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.cardMessage, { color: colors.textSecondary }], children: message.messageText })
834
- ]
835
- }
836
- );
837
- }
838
- function useContentBody(state, children, testID) {
839
- const { theme } = uiFeedback.useUi();
840
- const primary = theme.palette.primary["500"];
841
- const errorColor = theme.semantic.error["500"];
842
- if (state?.forbidden)
843
- return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.forbidden, testID: `${testID}${APP_SHELL_SUFFIX.forbidden}` });
844
- if (state?.error)
845
- return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
846
- if (state?.loading === true)
847
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.centerFill, testID: `${testID}${APP_SHELL_SUFFIX.loading}`, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: primary, size: "large" }) });
848
- return children;
849
- }
850
1099
  var AppShell = ({
851
1100
  header,
852
1101
  nav,
853
1102
  sidebar,
1103
+ collapsedSidebar,
1104
+ collapsedRailRange,
854
1105
  mobileMenu,
855
1106
  banner,
856
1107
  width = "full",
@@ -866,34 +1117,40 @@ var AppShell = ({
866
1117
  const maxWidth = useContentMaxWidth(width);
867
1118
  const body = useContentBody(state, children, testID);
868
1119
  const { width: viewportWidth } = reactNative.useWindowDimensions();
869
- const isNarrow = viewportWidth < MOBILE_BREAKPOINT;
870
- const useDrawer = sidebar !== void 0 && isNarrow;
871
- const [drawerOpen, setDrawerOpen] = react.useState(false);
872
- const openDrawer = react.useCallback(() => setDrawerOpen(true), []);
873
- const closeDrawer = react.useCallback(() => setDrawerOpen(false), []);
874
- react.useEffect(() => {
875
- if (!isNarrow && drawerOpen) setDrawerOpen(false);
876
- }, [isNarrow, drawerOpen]);
1120
+ const railRange = collapsedRailRange ?? { min: RAIL_FULL_BREAKPOINT, max: DEFAULT_COLLAPSED_RAIL_MAX };
1121
+ const railMode = resolveRailMode({
1122
+ viewport: viewportWidth,
1123
+ hasSidebar: sidebar !== void 0,
1124
+ hasCollapsed: collapsedSidebar !== void 0,
1125
+ range: railRange
1126
+ });
1127
+ const useDrawer = railMode === "drawer";
1128
+ const [drawerOpen, setDrawerOpen] = React5.useState(false);
1129
+ const openDrawer = React5.useCallback(() => setDrawerOpen(true), []);
1130
+ const closeDrawer = React5.useCallback(() => setDrawerOpen(false), []);
1131
+ React5.useEffect(() => {
1132
+ if (!useDrawer && drawerOpen) setDrawerOpen(false);
1133
+ }, [useDrawer, drawerOpen]);
877
1134
  const drawerLabels = { ...DEFAULT_DRAWER_LABELS, ...mobileMenu };
878
1135
  const isUnauthenticated = gate !== void 0 && !gate.pending && !gate.authenticated;
879
- react.useEffect(() => {
1136
+ React5.useEffect(() => {
880
1137
  if (isUnauthenticated && gate !== void 0) gate.onRedirect();
881
1138
  }, [isUnauthenticated, gate]);
882
1139
  if (gate?.pending === true)
883
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles2.root, styles2.centerFill, { backgroundColor: theme.colors.background }], testID: `${testID}${APP_SHELL_SUFFIX.pending}`, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: primary, size: "large" }) });
1140
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles3.root, styles3.centerFill, { backgroundColor: theme.colors.background }], testID: `${testID}${APP_SHELL_SUFFIX.pending}`, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: primary, size: "large" }) });
884
1141
  if (isUnauthenticated) return null;
885
- const columnStyle = maxWidth === "full" ? styles2.columnFull : [styles2.columnCapped, { maxWidth }];
886
- const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles2.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
1142
+ const columnStyle = maxWidth === "full" ? styles3.columnFull : [styles3.columnCapped, { maxWidth }];
1143
+ const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles3.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
887
1144
  const scroller = /* @__PURE__ */ jsxRuntime.jsx(
888
1145
  reactNative.ScrollView,
889
1146
  {
890
- contentContainerStyle: [styles2.scrollContent, { padding: contentPadding }],
891
- style: styles2.scroll,
1147
+ contentContainerStyle: [styles3.scrollContent, { padding: contentPadding }],
1148
+ style: styles3.scroll,
892
1149
  testID: `${testID}${APP_SHELL_SUFFIX.content}`,
893
1150
  children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: columnStyle, children: body })
894
1151
  }
895
1152
  );
896
- const headerRegion = useDrawer ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.headerRow, testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: [
1153
+ const headerRegion = useDrawer ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.headerRow, testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: [
897
1154
  /* @__PURE__ */ jsxRuntime.jsx(
898
1155
  MenuToggle,
899
1156
  {
@@ -903,15 +1160,15 @@ var AppShell = ({
903
1160
  onOpen: openDrawer
904
1161
  }
905
1162
  ),
906
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.headerFill, children: header })
1163
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.headerFill, children: header })
907
1164
  ] }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: header });
908
- const showRail = sidebar !== void 0 && !isNarrow;
909
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles2.root, { backgroundColor: theme.colors.background }], testID, children: [
1165
+ const railRegion = railMode === "full" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.sidebar}`, children: sidebar }) : railMode === "collapsed" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.collapsedSidebar}`, children: collapsedSidebar }) : null;
1166
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles3.root, { backgroundColor: theme.colors.background }], testID, children: [
910
1167
  headerRegion,
911
1168
  nav !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.nav}`, children: navInnerStyle !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: navInnerStyle, children: nav }) : nav }) : null,
912
1169
  banner !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: banner }) : null,
913
- showRail ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.bodyRow, children: [
914
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.sidebar}`, children: sidebar }),
1170
+ railRegion !== null ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.bodyRow, children: [
1171
+ railRegion,
915
1172
  scroller
916
1173
  ] }) : scroller,
917
1174
  useDrawer && drawerOpen ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -926,6 +1183,261 @@ var AppShell = ({
926
1183
  ) : null
927
1184
  ] });
928
1185
  };
1186
+ var COLLAPSED_ICON_SIZE = 22;
1187
+ function glyphFor(label) {
1188
+ return label.trim().charAt(0).toUpperCase();
1189
+ }
1190
+ var CollapsedRail = ({
1191
+ items,
1192
+ pathname,
1193
+ onNavigate,
1194
+ regionLabel,
1195
+ navigateHint = (label) => label,
1196
+ header,
1197
+ footer,
1198
+ containerStyle
1199
+ }) => {
1200
+ const { theme } = uiFeedback.useUi();
1201
+ const colors = theme.colors;
1202
+ const primaryColor = theme.palette.primary["500"];
1203
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1204
+ reactNative.View,
1205
+ {
1206
+ accessibilityRole: "none",
1207
+ "aria-label": regionLabel,
1208
+ role: "navigation",
1209
+ style: [
1210
+ collapsedRailStyles.container,
1211
+ { backgroundColor: colors.surface, borderRightColor: colors.border },
1212
+ containerStyle
1213
+ ],
1214
+ children: [
1215
+ header !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.slot, children: header }) : null,
1216
+ items.map((item) => {
1217
+ const active = isRouteActive(pathname, item.route);
1218
+ const iconColor = active ? primaryColor : colors.textSecondary;
1219
+ return /* @__PURE__ */ jsxRuntime.jsx(
1220
+ FocusableTouchable,
1221
+ {
1222
+ accessibilityHint: navigateHint(item.label),
1223
+ accessibilityLabel: item.label,
1224
+ ringColor: primaryColor,
1225
+ style: [collapsedRailStyles.item, active ? { backgroundColor: colors.border } : void 0],
1226
+ testID: item.testID ?? item.key,
1227
+ onPress: () => onNavigate(item.route),
1228
+ children: typeof item.renderIcon === "function" ? item.renderIcon(iconColor, COLLAPSED_ICON_SIZE) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [collapsedRailStyles.glyph, { color: iconColor }], children: glyphFor(item.label) })
1229
+ },
1230
+ item.key
1231
+ );
1232
+ }),
1233
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.spacer }),
1234
+ footer !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.slot, children: footer }) : null
1235
+ ]
1236
+ }
1237
+ );
1238
+ };
1239
+ function wantsTopBar(layout, hasHeader, hasTopBar) {
1240
+ return layout === "top" || layout === "both" || hasHeader || layout === "side" && hasTopBar;
1241
+ }
1242
+ function wantsSideRail(layout, hasSideRail) {
1243
+ return (layout === "side" || layout === "both") && hasSideRail;
1244
+ }
1245
+ var NavShell = ({
1246
+ layout,
1247
+ pathname,
1248
+ onNavigate,
1249
+ regionLabel,
1250
+ navigateHint,
1251
+ topBar,
1252
+ header,
1253
+ sideRail,
1254
+ children,
1255
+ collapsedSidebar: collapsedSidebarProp,
1256
+ collapsedRailRange: collapsedRailRangeProp,
1257
+ ...shellProps
1258
+ }) => {
1259
+ const showTopBar = wantsTopBar(layout, header !== void 0, topBar !== void 0);
1260
+ const showSideRail = wantsSideRail(layout, sideRail !== void 0);
1261
+ const headerNode = header !== void 0 ? header : showTopBar && topBar !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1262
+ NavBar,
1263
+ {
1264
+ barTheme: topBar.barTheme,
1265
+ brand: topBar.brand,
1266
+ collapseBelow: topBar.collapseBelow,
1267
+ containerStyle: topBar.containerStyle,
1268
+ items: topBar.items,
1269
+ menuHint: topBar.menuHint,
1270
+ menuLabel: topBar.menuLabel,
1271
+ navigateHint,
1272
+ overflowHint: topBar.overflowHint,
1273
+ overflowLabel: topBar.overflowLabel,
1274
+ pathname,
1275
+ regionLabel,
1276
+ renderMenuIcon: topBar.renderMenuIcon,
1277
+ right: topBar.right,
1278
+ onNavigate
1279
+ }
1280
+ ) : null;
1281
+ const sidebarNode = showSideRail && sideRail !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1282
+ Sidebar,
1283
+ {
1284
+ collapseHint: sideRail.collapseHint,
1285
+ containerStyle: sideRail.containerStyle,
1286
+ expandHint: sideRail.expandHint,
1287
+ footer: sideRail.footer,
1288
+ header: sideRail.header,
1289
+ items: sideRail.items,
1290
+ navigateHint,
1291
+ pathname,
1292
+ regionLabel,
1293
+ renderChevron: sideRail.renderChevron,
1294
+ title: sideRail.title,
1295
+ onNavigate
1296
+ }
1297
+ ) : void 0;
1298
+ const collapsedCfg = showSideRail ? sideRail?.collapsed : void 0;
1299
+ const collapsedNode = collapsedCfg !== void 0 && sideRail !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1300
+ CollapsedRail,
1301
+ {
1302
+ footer: collapsedCfg.footer,
1303
+ header: collapsedCfg.header,
1304
+ items: collapsedCfg.items ?? sideRail.items,
1305
+ navigateHint,
1306
+ pathname,
1307
+ regionLabel,
1308
+ onNavigate
1309
+ }
1310
+ ) : collapsedSidebarProp;
1311
+ const collapsedRange = collapsedCfg?.range ?? collapsedRailRangeProp;
1312
+ return /* @__PURE__ */ jsxRuntime.jsx(
1313
+ AppShell,
1314
+ {
1315
+ ...shellProps,
1316
+ collapsedRailRange: collapsedRange,
1317
+ collapsedSidebar: collapsedNode,
1318
+ header: headerNode,
1319
+ sidebar: sidebarNode,
1320
+ children
1321
+ }
1322
+ );
1323
+ };
1324
+ var TEXT_ON_PRIMARY4 = "#ffffff";
1325
+ var DEFAULT_MIN_ITEMS = 1;
1326
+ var PillNav = ({
1327
+ items,
1328
+ pathname,
1329
+ onNavigate,
1330
+ regionLabel,
1331
+ navigateHint = (label) => label,
1332
+ minItems = DEFAULT_MIN_ITEMS,
1333
+ containerStyle
1334
+ }) => {
1335
+ const { theme } = uiFeedback.useUi();
1336
+ const colors = theme.colors;
1337
+ const primaryColor = theme.palette.primary["500"];
1338
+ if (items.length < minItems) return null;
1339
+ return /* @__PURE__ */ jsxRuntime.jsx(
1340
+ reactNative.View,
1341
+ {
1342
+ accessibilityRole: "none",
1343
+ "aria-label": regionLabel,
1344
+ role: "navigation",
1345
+ style: [pillNavStyles.container, containerStyle],
1346
+ children: items.map((item) => {
1347
+ const active = isRouteActive(pathname, item.route);
1348
+ const textColor = active ? TEXT_ON_PRIMARY4 : colors.textSecondary;
1349
+ const pillStyle = active ? { backgroundColor: primaryColor } : { backgroundColor: colors.surfaceElevated };
1350
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1351
+ FocusableTouchable,
1352
+ {
1353
+ accessibilityHint: navigateHint(item.label),
1354
+ accessibilityLabel: item.label,
1355
+ accessibilityRole: "link",
1356
+ ringColor: primaryColor,
1357
+ style: [pillNavStyles.pill, pillStyle],
1358
+ testID: item.testID ?? item.key,
1359
+ onPress: () => onNavigate(item.route),
1360
+ children: [
1361
+ typeof item.renderIcon === "function" ? item.renderIcon(textColor, NAV_ICON_SIZE) : null,
1362
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [pillNavStyles.pillText, { color: textColor }], children: item.label })
1363
+ ]
1364
+ },
1365
+ item.key
1366
+ );
1367
+ })
1368
+ }
1369
+ );
1370
+ };
1371
+ var TEXT_ON_PRIMARY5 = "#ffffff";
1372
+ var DARK_MODE_ICON_SIZE = 16;
1373
+ var DarkModeControl = ({
1374
+ value,
1375
+ options,
1376
+ onChange,
1377
+ regionLabel,
1378
+ variant = "segmented",
1379
+ testID,
1380
+ containerStyle
1381
+ }) => {
1382
+ const { theme } = uiFeedback.useUi();
1383
+ const colors = theme.colors;
1384
+ const primaryColor = theme.palette.primary["500"];
1385
+ const currentIndex = options.findIndex((option) => option.value === value);
1386
+ const current = currentIndex >= 0 ? options[currentIndex] : options[0];
1387
+ const advance = React5.useCallback(() => {
1388
+ if (options.length === 0) return;
1389
+ const from = currentIndex >= 0 ? currentIndex : 0;
1390
+ const next = options[(from + 1) % options.length];
1391
+ if (next !== void 0) onChange(next.value);
1392
+ }, [options, currentIndex, onChange]);
1393
+ if (options.length === 0 || current === void 0) return null;
1394
+ if (variant === "cycle") {
1395
+ const iconColor = colors.text;
1396
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { "aria-label": regionLabel, style: containerStyle, children: /* @__PURE__ */ jsxRuntime.jsxs(
1397
+ FocusableTouchable,
1398
+ {
1399
+ accessibilityHint: current.hint,
1400
+ accessibilityLabel: current.label,
1401
+ ringColor: primaryColor,
1402
+ style: [darkModeStyles.cycle, { backgroundColor: colors.surfaceElevated }],
1403
+ testID,
1404
+ onPress: advance,
1405
+ children: [
1406
+ typeof current.renderIcon === "function" ? current.renderIcon(iconColor, DARK_MODE_ICON_SIZE) : null,
1407
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [darkModeStyles.cycleText, { color: colors.text }], children: current.label })
1408
+ ]
1409
+ }
1410
+ ) });
1411
+ }
1412
+ return /* @__PURE__ */ jsxRuntime.jsx(
1413
+ reactNative.View,
1414
+ {
1415
+ "aria-label": regionLabel,
1416
+ role: "group",
1417
+ style: [darkModeStyles.segmentRow, { borderColor: colors.border }, containerStyle],
1418
+ children: options.map((option) => {
1419
+ const active = option.value === value;
1420
+ const textColor = active ? TEXT_ON_PRIMARY5 : colors.textSecondary;
1421
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1422
+ FocusableTouchable,
1423
+ {
1424
+ accessibilityHint: option.hint,
1425
+ accessibilityLabel: option.label,
1426
+ ringColor: primaryColor,
1427
+ style: [darkModeStyles.segment, active ? { backgroundColor: primaryColor } : void 0],
1428
+ testID: testID !== void 0 ? `${testID}-${option.value}` : void 0,
1429
+ onPress: () => onChange(option.value),
1430
+ children: [
1431
+ typeof option.renderIcon === "function" ? option.renderIcon(textColor, DARK_MODE_ICON_SIZE) : null,
1432
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [darkModeStyles.segmentText, { color: textColor }], children: option.label })
1433
+ ]
1434
+ },
1435
+ option.value
1436
+ );
1437
+ })
1438
+ }
1439
+ );
1440
+ };
929
1441
  function roleRoutesToNavItems(routes, translate) {
930
1442
  return routes.map((entry) => ({
931
1443
  key: entry.role,
@@ -942,17 +1454,30 @@ exports.APP_SHELL_SUFFIX = APP_SHELL_SUFFIX;
942
1454
  exports.AppShell = AppShell;
943
1455
  exports.BASE_INDENT = BASE_INDENT;
944
1456
  exports.CHEVRON_ICON_SIZE = CHEVRON_ICON_SIZE;
1457
+ exports.CollapsedRail = CollapsedRail;
1458
+ exports.DEFAULT_COLLAPSED_RAIL_MAX = DEFAULT_COLLAPSED_RAIL_MAX;
1459
+ exports.DarkModeControl = DarkModeControl;
945
1460
  exports.NAV_ICON_SIZE = NAV_ICON_SIZE;
1461
+ exports.NAV_LINK_GAP = NAV_LINK_GAP;
946
1462
  exports.NAV_TEST_IDS = NAV_TEST_IDS;
1463
+ exports.Nav = Nav;
947
1464
  exports.NavBar = NavBar;
948
1465
  exports.NavExpandableItem = NavExpandableItem;
1466
+ exports.NavOverflowMenu = NavOverflowMenu;
1467
+ exports.NavShell = NavShell;
1468
+ exports.PillNav = PillNav;
1469
+ exports.RAIL_FULL_BREAKPOINT = RAIL_FULL_BREAKPOINT;
949
1470
  exports.Sidebar = Sidebar;
950
1471
  exports.Topbar = Topbar;
951
1472
  exports.accessibleNavItems = accessibleNavItems;
1473
+ exports.collapsedRailStyles = collapsedRailStyles;
1474
+ exports.darkModeStyles = darkModeStyles;
952
1475
  exports.expandableStyles = expandableStyles;
953
1476
  exports.isRouteActive = isRouteActive;
954
1477
  exports.navStyles = navStyles;
1478
+ exports.pillNavStyles = pillNavStyles;
955
1479
  exports.resolveContentMaxWidth = resolveContentMaxWidth;
1480
+ exports.resolveRailMode = resolveRailMode;
956
1481
  exports.roleRoutesToNavItems = roleRoutesToNavItems;
957
1482
  exports.useContentMaxWidth = useContentMaxWidth;
958
1483
  //# sourceMappingURL=index.js.map