@dloizides/ui-nav 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { StyleSheet, Platform, TouchableOpacity, View, Text, useWindowDimensions, Pressable, ActivityIndicator, ScrollView } from 'react-native';
2
- import { useUi } from '@dloizides/ui-feedback';
1
+ import { StyleSheet, Platform, TouchableOpacity, View, Text, useWindowDimensions, ActivityIndicator, ScrollView, Pressable } from 'react-native';
2
+ import { useUi, UiProvider } from '@dloizides/ui-feedback';
3
3
  import React5, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
4
4
  import { jsxs, jsx } from 'react/jsx-runtime';
5
5
  import { ModalDropdown } from '@dloizides/ui-layout';
@@ -155,6 +155,86 @@ var navBarStyles = StyleSheet.create({
155
155
  columnGap: 8
156
156
  }
157
157
  });
158
+ var pillNavStyles = StyleSheet.create({
159
+ // Cross-nav pill row (kefi coral dashboard switcher): fully-rounded pills, wraps.
160
+ container: {
161
+ flexDirection: "row",
162
+ flexWrap: "wrap",
163
+ alignItems: "center",
164
+ columnGap: 8,
165
+ rowGap: 8,
166
+ paddingVertical: 8
167
+ },
168
+ pill: {
169
+ paddingHorizontal: 16,
170
+ paddingVertical: 8,
171
+ borderRadius: 999,
172
+ minHeight: 36,
173
+ flexDirection: "row",
174
+ alignItems: "center",
175
+ columnGap: 6,
176
+ justifyContent: "center"
177
+ },
178
+ pillText: { fontSize: 14, fontWeight: "600" }
179
+ });
180
+ var darkModeStyles = StyleSheet.create({
181
+ // Segmented Light / Dark / System control.
182
+ segmentRow: {
183
+ flexDirection: "row",
184
+ alignItems: "center",
185
+ borderWidth: 1,
186
+ borderRadius: 8,
187
+ overflow: "hidden",
188
+ alignSelf: "flex-start"
189
+ },
190
+ segment: {
191
+ paddingHorizontal: 12,
192
+ paddingVertical: 8,
193
+ minHeight: 36,
194
+ flexDirection: "row",
195
+ alignItems: "center",
196
+ columnGap: 6,
197
+ justifyContent: "center"
198
+ },
199
+ segmentText: { fontSize: 13, fontWeight: "600" },
200
+ // Cycle-button variant.
201
+ cycle: {
202
+ paddingHorizontal: 12,
203
+ paddingVertical: 8,
204
+ minHeight: 44,
205
+ minWidth: 44,
206
+ borderRadius: 8,
207
+ flexDirection: "row",
208
+ alignItems: "center",
209
+ columnGap: 6,
210
+ justifyContent: "center"
211
+ },
212
+ cycleText: { fontSize: 13, fontWeight: "600" }
213
+ });
214
+ var collapsedRailStyles = StyleSheet.create({
215
+ // Intermediate (tablet) icon-only rail: a narrow vertical strip of icon buttons.
216
+ container: {
217
+ width: 64,
218
+ paddingTop: 16,
219
+ paddingHorizontal: 8,
220
+ borderRightWidth: 1,
221
+ height: "100%",
222
+ alignItems: "center"
223
+ },
224
+ // Each item is a ≥44×44 centred touch target with a rounded active/hover pill.
225
+ item: {
226
+ width: 48,
227
+ height: 48,
228
+ marginVertical: 4,
229
+ borderRadius: 8,
230
+ alignItems: "center",
231
+ justifyContent: "center"
232
+ },
233
+ // Glyph fallback (the label's first character) when an item has no renderIcon.
234
+ glyph: { fontSize: 18, fontWeight: "700" },
235
+ spacer: { flex: 1 },
236
+ slot: { alignItems: "center" }
237
+ });
158
238
  var expandableStyles = StyleSheet.create({
159
239
  childItem: {
160
240
  borderRadius: 4,
@@ -371,6 +451,8 @@ var APP_SHELL_SUFFIX = {
371
451
  nav: "-nav",
372
452
  /** The persistent left rail of the back-office layout (only when `sidebar` is supplied). */
373
453
  sidebar: "-sidebar",
454
+ /** The intermediate persistent COLLAPSED (icon-only) rail (3-tier layout, tablet band). */
455
+ collapsedSidebar: "-collapsed-sidebar",
374
456
  /** Mobile hamburger that opens the nav drawer (back-office layout, below the breakpoint). */
375
457
  menuToggle: "-menu-toggle",
376
458
  /** The mobile nav drawer panel (holds the `sidebar` slot when opened on a narrow viewport). */
@@ -704,7 +786,7 @@ var MENU_GLYPH = "\u2630";
704
786
  function ariaControlsProps(id) {
705
787
  return { "aria-controls": id };
706
788
  }
707
- var NavBar = ({
789
+ var NavBarInner = ({
708
790
  items,
709
791
  pathname,
710
792
  onNavigate,
@@ -718,7 +800,8 @@ var NavBar = ({
718
800
  collapseBelow = DEFAULT_COLLAPSE_BELOW,
719
801
  overflowLabel = "More",
720
802
  overflowHint = "",
721
- containerStyle
803
+ containerStyle,
804
+ contentMaxWidth
722
805
  }) => {
723
806
  const { theme } = useUi();
724
807
  const colors = theme.colors;
@@ -730,6 +813,10 @@ var NavBar = ({
730
813
  const { visibleCount, setAvailableWidth, setItemWidth, setMoreWidth } = useNavOverflow(items.length, NAV_LINK_GAP);
731
814
  const collapsed = width < collapseBelow;
732
815
  const showLinks = !collapsed || open;
816
+ const innerCapStyle = useMemo(
817
+ () => contentMaxWidth === void 0 ? null : { maxWidth: contentMaxWidth, width: "100%", alignSelf: "center" },
818
+ [contentMaxWidth]
819
+ );
733
820
  const toggleMenu = useCallback(() => setOpen((v) => !v), []);
734
821
  const handlePress = useCallback(
735
822
  (route) => {
@@ -759,7 +846,7 @@ var NavBar = ({
759
846
  { backgroundColor: colors.surface, borderBottomColor: colors.border },
760
847
  containerStyle
761
848
  ],
762
- children: /* @__PURE__ */ jsxs(View, { style: [navBarStyles.inner, collapsed ? null : navBarStyles.innerNoWrap], children: [
849
+ children: /* @__PURE__ */ jsxs(View, { style: [navBarStyles.inner, collapsed ? null : navBarStyles.innerNoWrap, innerCapStyle], children: [
763
850
  brand !== void 0 ? /* @__PURE__ */ jsx(View, { style: navBarStyles.brand, children: brand }) : null,
764
851
  collapsed ? /* @__PURE__ */ jsx(
765
852
  Pressable,
@@ -842,13 +929,57 @@ var NavBar = ({
842
929
  }
843
930
  );
844
931
  };
932
+ var NavBar = ({ barTheme, ...props }) => {
933
+ const { t, navigate } = useUi();
934
+ if (barTheme === void 0) return /* @__PURE__ */ jsx(NavBarInner, { ...props });
935
+ return /* @__PURE__ */ jsx(UiProvider, { navigate, t, theme: barTheme, children: /* @__PURE__ */ jsx(NavBarInner, { ...props }) });
936
+ };
845
937
  var Nav = (props) => {
846
938
  if (props.orientation === "vertical") {
847
939
  return /* @__PURE__ */ jsx(Sidebar, { ...props });
848
940
  }
849
941
  return /* @__PURE__ */ jsx(NavBar, { ...props });
850
942
  };
851
- var MOBILE_BREAKPOINT = 768;
943
+ var CARD_PADDING = 20;
944
+ var CARD_BORDER_RADIUS = 12;
945
+ var CARD_BORDER_WIDTH = 1;
946
+ var CARD_TITLE_FONT_SIZE = 16;
947
+ var CARD_MESSAGE_FONT_SIZE = 14;
948
+ var CARD_TITLE_MARGIN_BOTTOM = 8;
949
+ var styles = StyleSheet.create({
950
+ centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
951
+ card: {
952
+ padding: CARD_PADDING,
953
+ borderRadius: CARD_BORDER_RADIUS,
954
+ borderWidth: CARD_BORDER_WIDTH
955
+ },
956
+ cardTitle: { fontSize: CARD_TITLE_FONT_SIZE, fontWeight: "700", marginBottom: CARD_TITLE_MARGIN_BOTTOM },
957
+ cardMessage: { fontSize: CARD_MESSAGE_FONT_SIZE }
958
+ });
959
+ function MessageCard({
960
+ message,
961
+ accentColor,
962
+ testID
963
+ }) {
964
+ const { theme } = useUi();
965
+ const colors = theme.colors;
966
+ return /* @__PURE__ */ jsxs(View, { style: [styles.card, { backgroundColor: colors.surface, borderColor: accentColor }], testID, children: [
967
+ /* @__PURE__ */ jsx(Text, { style: [styles.cardTitle, { color: accentColor }], children: message.titleText }),
968
+ /* @__PURE__ */ jsx(Text, { style: [styles.cardMessage, { color: colors.textSecondary }], children: message.messageText })
969
+ ] });
970
+ }
971
+ function useContentBody(state, children, testID) {
972
+ const { theme } = useUi();
973
+ const primary = theme.palette.primary["500"];
974
+ const errorColor = theme.semantic.error["500"];
975
+ if (state?.forbidden)
976
+ return /* @__PURE__ */ jsx(MessageCard, { accentColor: errorColor, message: state.forbidden, testID: `${testID}${APP_SHELL_SUFFIX.forbidden}` });
977
+ if (state?.error)
978
+ return /* @__PURE__ */ jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
979
+ if (state?.loading === true)
980
+ return /* @__PURE__ */ jsx(View, { style: styles.centerFill, testID: `${testID}${APP_SHELL_SUFFIX.loading}`, children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" }) });
981
+ return children;
982
+ }
852
983
  var SCRIM_COLOR = "rgba(0, 0, 0, 0.5)";
853
984
  var MENU_GLYPH2 = "\u2630";
854
985
  var MENU_MIN_TARGET = 44;
@@ -862,7 +993,7 @@ var DEFAULT_DRAWER_LABELS = {
862
993
  closeLabel: "Close menu",
863
994
  closeHint: "Close the navigation menu"
864
995
  };
865
- var styles = StyleSheet.create({
996
+ var styles2 = StyleSheet.create({
866
997
  overlay: { ...StyleSheet.absoluteFillObject, zIndex: DRAWER_Z_INDEX },
867
998
  scrim: { ...StyleSheet.absoluteFillObject, backgroundColor: SCRIM_COLOR },
868
999
  panel: { position: "absolute", top: 0, bottom: 0, left: 0 },
@@ -883,10 +1014,10 @@ var MenuToggle = ({ label, hint, onOpen, testID }) => {
883
1014
  accessibilityHint: hint,
884
1015
  accessibilityLabel: label,
885
1016
  ringColor: theme.palette.primary["500"],
886
- style: styles.menuToggle,
1017
+ style: styles2.menuToggle,
887
1018
  testID,
888
1019
  onPress: onOpen,
889
- children: /* @__PURE__ */ jsx(Text, { style: [styles.menuToggleGlyph, { color: theme.colors.text }], children: MENU_GLYPH2 })
1020
+ children: /* @__PURE__ */ jsx(Text, { style: [styles2.menuToggleGlyph, { color: theme.colors.text }], children: MENU_GLYPH2 })
890
1021
  }
891
1022
  );
892
1023
  };
@@ -909,21 +1040,34 @@ var MobileDrawer = ({
909
1040
  node.addEventListener("click", handleClick);
910
1041
  return () => node.removeEventListener("click", handleClick);
911
1042
  }, [onClose]);
912
- return /* @__PURE__ */ jsxs(View, { style: styles.overlay, children: [
1043
+ return /* @__PURE__ */ jsxs(View, { style: styles2.overlay, children: [
913
1044
  /* @__PURE__ */ jsx(
914
1045
  Pressable,
915
1046
  {
916
1047
  accessibilityHint: labels.closeHint,
917
1048
  accessibilityLabel: labels.closeLabel,
918
1049
  accessibilityRole: "button",
919
- style: styles.scrim,
1050
+ style: styles2.scrim,
920
1051
  testID: scrimTestID,
921
1052
  onPress: onClose
922
1053
  }
923
1054
  ),
924
- /* @__PURE__ */ jsx(View, { ref: panelRef, "aria-modal": true, role: "dialog", style: styles.panel, testID: drawerTestID, children: sidebar })
1055
+ /* @__PURE__ */ jsx(View, { ref: panelRef, "aria-modal": true, role: "dialog", style: styles2.panel, testID: drawerTestID, children: sidebar })
925
1056
  ] });
926
1057
  };
1058
+
1059
+ // src/railMode.ts
1060
+ var RAIL_FULL_BREAKPOINT = 768;
1061
+ var DEFAULT_COLLAPSED_RAIL_MAX = 1024;
1062
+ function resolveRailMode({ viewport, hasSidebar, hasCollapsed, range }) {
1063
+ if (!hasSidebar) return "none";
1064
+ if (hasCollapsed) {
1065
+ if (viewport >= range.max) return "full";
1066
+ if (viewport >= range.min) return "collapsed";
1067
+ return "drawer";
1068
+ }
1069
+ return viewport >= RAIL_FULL_BREAKPOINT ? "full" : "drawer";
1070
+ }
927
1071
  function resolveContentMaxWidth(width, viewport) {
928
1072
  if (width === "full") return "full";
929
1073
  const wideMinViewport = width.wideMinViewport ?? Number.POSITIVE_INFINITY;
@@ -935,13 +1079,7 @@ function useContentMaxWidth(width) {
935
1079
  return resolveContentMaxWidth(width, viewport);
936
1080
  }
937
1081
  var DEFAULT_CONTENT_PADDING = 24;
938
- var CARD_PADDING = 20;
939
- var CARD_BORDER_RADIUS = 12;
940
- var CARD_BORDER_WIDTH = 1;
941
- var CARD_TITLE_FONT_SIZE = 16;
942
- var CARD_MESSAGE_FONT_SIZE = 14;
943
- var CARD_TITLE_MARGIN_BOTTOM = 8;
944
- var styles2 = StyleSheet.create({
1082
+ var styles3 = StyleSheet.create({
945
1083
  root: { flex: 1 },
946
1084
  centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
947
1085
  scroll: { flex: 1 },
@@ -955,50 +1093,14 @@ var styles2 = StyleSheet.create({
955
1093
  columnCapped: { flex: 1, width: "100%", alignSelf: "center" },
956
1094
  // Non-flex variant used to cap the nav strip's inner content to the content
957
1095
  // column so the nav aligns with the page content instead of running full-bleed.
958
- chromeCapped: { width: "100%", alignSelf: "center" },
959
- card: {
960
- padding: CARD_PADDING,
961
- borderRadius: CARD_BORDER_RADIUS,
962
- borderWidth: CARD_BORDER_WIDTH
963
- },
964
- cardTitle: { fontSize: CARD_TITLE_FONT_SIZE, fontWeight: "700", marginBottom: CARD_TITLE_MARGIN_BOTTOM },
965
- cardMessage: { fontSize: CARD_MESSAGE_FONT_SIZE }
1096
+ chromeCapped: { width: "100%", alignSelf: "center" }
966
1097
  });
967
- function MessageCard({
968
- message,
969
- accentColor,
970
- testID
971
- }) {
972
- const { theme } = useUi();
973
- const colors = theme.colors;
974
- return /* @__PURE__ */ jsxs(
975
- View,
976
- {
977
- style: [styles2.card, { backgroundColor: colors.surface, borderColor: accentColor }],
978
- testID,
979
- children: [
980
- /* @__PURE__ */ jsx(Text, { style: [styles2.cardTitle, { color: accentColor }], children: message.titleText }),
981
- /* @__PURE__ */ jsx(Text, { style: [styles2.cardMessage, { color: colors.textSecondary }], children: message.messageText })
982
- ]
983
- }
984
- );
985
- }
986
- function useContentBody(state, children, testID) {
987
- const { theme } = useUi();
988
- const primary = theme.palette.primary["500"];
989
- const errorColor = theme.semantic.error["500"];
990
- if (state?.forbidden)
991
- return /* @__PURE__ */ jsx(MessageCard, { accentColor: errorColor, message: state.forbidden, testID: `${testID}${APP_SHELL_SUFFIX.forbidden}` });
992
- if (state?.error)
993
- return /* @__PURE__ */ jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
994
- if (state?.loading === true)
995
- return /* @__PURE__ */ jsx(View, { style: styles2.centerFill, testID: `${testID}${APP_SHELL_SUFFIX.loading}`, children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" }) });
996
- return children;
997
- }
998
1098
  var AppShell = ({
999
1099
  header,
1000
1100
  nav,
1001
1101
  sidebar,
1102
+ collapsedSidebar,
1103
+ collapsedRailRange,
1002
1104
  mobileMenu,
1003
1105
  banner,
1004
1106
  width = "full",
@@ -1014,34 +1116,40 @@ var AppShell = ({
1014
1116
  const maxWidth = useContentMaxWidth(width);
1015
1117
  const body = useContentBody(state, children, testID);
1016
1118
  const { width: viewportWidth } = useWindowDimensions();
1017
- const isNarrow = viewportWidth < MOBILE_BREAKPOINT;
1018
- const useDrawer = sidebar !== void 0 && isNarrow;
1119
+ const railRange = collapsedRailRange ?? { min: RAIL_FULL_BREAKPOINT, max: DEFAULT_COLLAPSED_RAIL_MAX };
1120
+ const railMode = resolveRailMode({
1121
+ viewport: viewportWidth,
1122
+ hasSidebar: sidebar !== void 0,
1123
+ hasCollapsed: collapsedSidebar !== void 0,
1124
+ range: railRange
1125
+ });
1126
+ const useDrawer = railMode === "drawer";
1019
1127
  const [drawerOpen, setDrawerOpen] = useState(false);
1020
1128
  const openDrawer = useCallback(() => setDrawerOpen(true), []);
1021
1129
  const closeDrawer = useCallback(() => setDrawerOpen(false), []);
1022
1130
  useEffect(() => {
1023
- if (!isNarrow && drawerOpen) setDrawerOpen(false);
1024
- }, [isNarrow, drawerOpen]);
1131
+ if (!useDrawer && drawerOpen) setDrawerOpen(false);
1132
+ }, [useDrawer, drawerOpen]);
1025
1133
  const drawerLabels = { ...DEFAULT_DRAWER_LABELS, ...mobileMenu };
1026
1134
  const isUnauthenticated = gate !== void 0 && !gate.pending && !gate.authenticated;
1027
1135
  useEffect(() => {
1028
1136
  if (isUnauthenticated && gate !== void 0) gate.onRedirect();
1029
1137
  }, [isUnauthenticated, gate]);
1030
1138
  if (gate?.pending === true)
1031
- return /* @__PURE__ */ jsx(View, { style: [styles2.root, styles2.centerFill, { backgroundColor: theme.colors.background }], testID: `${testID}${APP_SHELL_SUFFIX.pending}`, children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" }) });
1139
+ return /* @__PURE__ */ jsx(View, { style: [styles3.root, styles3.centerFill, { backgroundColor: theme.colors.background }], testID: `${testID}${APP_SHELL_SUFFIX.pending}`, children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" }) });
1032
1140
  if (isUnauthenticated) return null;
1033
- const columnStyle = maxWidth === "full" ? styles2.columnFull : [styles2.columnCapped, { maxWidth }];
1034
- const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles2.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
1141
+ const columnStyle = maxWidth === "full" ? styles3.columnFull : [styles3.columnCapped, { maxWidth }];
1142
+ const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles3.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
1035
1143
  const scroller = /* @__PURE__ */ jsx(
1036
1144
  ScrollView,
1037
1145
  {
1038
- contentContainerStyle: [styles2.scrollContent, { padding: contentPadding }],
1039
- style: styles2.scroll,
1146
+ contentContainerStyle: [styles3.scrollContent, { padding: contentPadding }],
1147
+ style: styles3.scroll,
1040
1148
  testID: `${testID}${APP_SHELL_SUFFIX.content}`,
1041
1149
  children: /* @__PURE__ */ jsx(View, { style: columnStyle, children: body })
1042
1150
  }
1043
1151
  );
1044
- const headerRegion = useDrawer ? /* @__PURE__ */ jsxs(View, { style: styles2.headerRow, testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: [
1152
+ const headerRegion = useDrawer ? /* @__PURE__ */ jsxs(View, { style: styles3.headerRow, testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: [
1045
1153
  /* @__PURE__ */ jsx(
1046
1154
  MenuToggle,
1047
1155
  {
@@ -1051,15 +1159,15 @@ var AppShell = ({
1051
1159
  onOpen: openDrawer
1052
1160
  }
1053
1161
  ),
1054
- /* @__PURE__ */ jsx(View, { style: styles2.headerFill, children: header })
1162
+ /* @__PURE__ */ jsx(View, { style: styles3.headerFill, children: header })
1055
1163
  ] }) : /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: header });
1056
- const showRail = sidebar !== void 0 && !isNarrow;
1057
- return /* @__PURE__ */ jsxs(View, { style: [styles2.root, { backgroundColor: theme.colors.background }], testID, children: [
1164
+ const railRegion = railMode === "full" ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.sidebar}`, children: sidebar }) : railMode === "collapsed" ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.collapsedSidebar}`, children: collapsedSidebar }) : null;
1165
+ return /* @__PURE__ */ jsxs(View, { style: [styles3.root, { backgroundColor: theme.colors.background }], testID, children: [
1058
1166
  headerRegion,
1059
1167
  nav !== void 0 ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.nav}`, children: navInnerStyle !== void 0 ? /* @__PURE__ */ jsx(View, { style: navInnerStyle, children: nav }) : nav }) : null,
1060
1168
  banner !== void 0 ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: banner }) : null,
1061
- showRail ? /* @__PURE__ */ jsxs(View, { style: styles2.bodyRow, children: [
1062
- /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.sidebar}`, children: sidebar }),
1169
+ railRegion !== null ? /* @__PURE__ */ jsxs(View, { style: styles3.bodyRow, children: [
1170
+ railRegion,
1063
1171
  scroller
1064
1172
  ] }) : scroller,
1065
1173
  useDrawer && drawerOpen ? /* @__PURE__ */ jsx(
@@ -1074,6 +1182,262 @@ var AppShell = ({
1074
1182
  ) : null
1075
1183
  ] });
1076
1184
  };
1185
+ var COLLAPSED_ICON_SIZE = 22;
1186
+ function glyphFor(label) {
1187
+ return label.trim().charAt(0).toUpperCase();
1188
+ }
1189
+ var CollapsedRail = ({
1190
+ items,
1191
+ pathname,
1192
+ onNavigate,
1193
+ regionLabel,
1194
+ navigateHint = (label) => label,
1195
+ header,
1196
+ footer,
1197
+ containerStyle
1198
+ }) => {
1199
+ const { theme } = useUi();
1200
+ const colors = theme.colors;
1201
+ const primaryColor = theme.palette.primary["500"];
1202
+ return /* @__PURE__ */ jsxs(
1203
+ View,
1204
+ {
1205
+ accessibilityRole: "none",
1206
+ "aria-label": regionLabel,
1207
+ role: "navigation",
1208
+ style: [
1209
+ collapsedRailStyles.container,
1210
+ { backgroundColor: colors.surface, borderRightColor: colors.border },
1211
+ containerStyle
1212
+ ],
1213
+ children: [
1214
+ header !== void 0 ? /* @__PURE__ */ jsx(View, { style: collapsedRailStyles.slot, children: header }) : null,
1215
+ items.map((item) => {
1216
+ const active = isRouteActive(pathname, item.route);
1217
+ const iconColor = active ? primaryColor : colors.textSecondary;
1218
+ return /* @__PURE__ */ jsx(
1219
+ FocusableTouchable,
1220
+ {
1221
+ accessibilityHint: navigateHint(item.label),
1222
+ accessibilityLabel: item.label,
1223
+ ringColor: primaryColor,
1224
+ style: [collapsedRailStyles.item, active ? { backgroundColor: colors.border } : void 0],
1225
+ testID: item.testID ?? item.key,
1226
+ onPress: () => onNavigate(item.route),
1227
+ children: typeof item.renderIcon === "function" ? item.renderIcon(iconColor, COLLAPSED_ICON_SIZE) : /* @__PURE__ */ jsx(Text, { style: [collapsedRailStyles.glyph, { color: iconColor }], children: glyphFor(item.label) })
1228
+ },
1229
+ item.key
1230
+ );
1231
+ }),
1232
+ /* @__PURE__ */ jsx(View, { style: collapsedRailStyles.spacer }),
1233
+ footer !== void 0 ? /* @__PURE__ */ jsx(View, { style: collapsedRailStyles.slot, children: footer }) : null
1234
+ ]
1235
+ }
1236
+ );
1237
+ };
1238
+ function wantsTopBar(layout, hasHeader, hasTopBar) {
1239
+ return layout === "top" || layout === "both" || hasHeader || layout === "side" && hasTopBar;
1240
+ }
1241
+ function wantsSideRail(layout, hasSideRail) {
1242
+ return (layout === "side" || layout === "both") && hasSideRail;
1243
+ }
1244
+ var NavShell = ({
1245
+ layout,
1246
+ pathname,
1247
+ onNavigate,
1248
+ regionLabel,
1249
+ navigateHint,
1250
+ topBar,
1251
+ header,
1252
+ sideRail,
1253
+ children,
1254
+ collapsedSidebar: collapsedSidebarProp,
1255
+ collapsedRailRange: collapsedRailRangeProp,
1256
+ ...shellProps
1257
+ }) => {
1258
+ const showTopBar = wantsTopBar(layout, header !== void 0, topBar !== void 0);
1259
+ const showSideRail = wantsSideRail(layout, sideRail !== void 0);
1260
+ const headerNode = header !== void 0 ? header : showTopBar && topBar !== void 0 ? /* @__PURE__ */ jsx(
1261
+ NavBar,
1262
+ {
1263
+ barTheme: topBar.barTheme,
1264
+ brand: topBar.brand,
1265
+ collapseBelow: topBar.collapseBelow,
1266
+ containerStyle: topBar.containerStyle,
1267
+ contentMaxWidth: topBar.contentMaxWidth,
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__ */ 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__ */ 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__ */ 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 } = useUi();
1336
+ const colors = theme.colors;
1337
+ const primaryColor = theme.palette.primary["500"];
1338
+ if (items.length < minItems) return null;
1339
+ return /* @__PURE__ */ jsx(
1340
+ 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__ */ 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__ */ jsx(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 } = 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 = 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__ */ jsx(View, { "aria-label": regionLabel, style: containerStyle, children: /* @__PURE__ */ 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__ */ jsx(Text, { style: [darkModeStyles.cycleText, { color: colors.text }], children: current.label })
1408
+ ]
1409
+ }
1410
+ ) });
1411
+ }
1412
+ return /* @__PURE__ */ jsx(
1413
+ 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__ */ 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__ */ jsx(Text, { style: [darkModeStyles.segmentText, { color: textColor }], children: option.label })
1433
+ ]
1434
+ },
1435
+ option.value
1436
+ );
1437
+ })
1438
+ }
1439
+ );
1440
+ };
1077
1441
  function roleRoutesToNavItems(routes, translate) {
1078
1442
  return routes.map((entry) => ({
1079
1443
  key: entry.role,
@@ -1085,6 +1449,6 @@ function accessibleNavItems(user, table, translate) {
1085
1449
  return roleRoutesToNavItems(resolveAccessibleRoutes(user, table), translate);
1086
1450
  }
1087
1451
 
1088
- export { ACTIVE_BORDER_RADIUS, APP_SHELL_SUFFIX, AppShell, BASE_INDENT, CHEVRON_ICON_SIZE, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_TEST_IDS, Nav, NavBar, NavExpandableItem, NavOverflowMenu, Sidebar, Topbar, accessibleNavItems, expandableStyles, isRouteActive, navStyles, resolveContentMaxWidth, roleRoutesToNavItems, useContentMaxWidth };
1452
+ export { ACTIVE_BORDER_RADIUS, APP_SHELL_SUFFIX, AppShell, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, DEFAULT_COLLAPSED_RAIL_MAX, DarkModeControl, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_TEST_IDS, Nav, NavBar, NavExpandableItem, NavOverflowMenu, NavShell, PillNav, RAIL_FULL_BREAKPOINT, Sidebar, Topbar, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, isRouteActive, navStyles, pillNavStyles, resolveContentMaxWidth, resolveRailMode, roleRoutesToNavItems, useContentMaxWidth };
1089
1453
  //# sourceMappingURL=index.mjs.map
1090
1454
  //# sourceMappingURL=index.mjs.map