@dloizides/ui-nav 1.7.0 → 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
@@ -161,6 +161,86 @@ var navBarStyles = reactNative.StyleSheet.create({
161
161
  columnGap: 8
162
162
  }
163
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
+ });
164
244
  var expandableStyles = reactNative.StyleSheet.create({
165
245
  childItem: {
166
246
  borderRadius: 4,
@@ -377,6 +457,8 @@ var APP_SHELL_SUFFIX = {
377
457
  nav: "-nav",
378
458
  /** The persistent left rail of the back-office layout (only when `sidebar` is supplied). */
379
459
  sidebar: "-sidebar",
460
+ /** The intermediate persistent COLLAPSED (icon-only) rail (3-tier layout, tablet band). */
461
+ collapsedSidebar: "-collapsed-sidebar",
380
462
  /** Mobile hamburger that opens the nav drawer (back-office layout, below the breakpoint). */
381
463
  menuToggle: "-menu-toggle",
382
464
  /** The mobile nav drawer panel (holds the `sidebar` slot when opened on a narrow viewport). */
@@ -710,7 +792,7 @@ var MENU_GLYPH = "\u2630";
710
792
  function ariaControlsProps(id) {
711
793
  return { "aria-controls": id };
712
794
  }
713
- var NavBar = ({
795
+ var NavBarInner = ({
714
796
  items,
715
797
  pathname,
716
798
  onNavigate,
@@ -848,13 +930,57 @@ var NavBar = ({
848
930
  }
849
931
  );
850
932
  };
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
+ };
851
938
  var Nav = (props) => {
852
939
  if (props.orientation === "vertical") {
853
940
  return /* @__PURE__ */ jsxRuntime.jsx(Sidebar, { ...props });
854
941
  }
855
942
  return /* @__PURE__ */ jsxRuntime.jsx(NavBar, { ...props });
856
943
  };
857
- var MOBILE_BREAKPOINT = 768;
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
+ }
858
984
  var SCRIM_COLOR = "rgba(0, 0, 0, 0.5)";
859
985
  var MENU_GLYPH2 = "\u2630";
860
986
  var MENU_MIN_TARGET = 44;
@@ -868,7 +994,7 @@ var DEFAULT_DRAWER_LABELS = {
868
994
  closeLabel: "Close menu",
869
995
  closeHint: "Close the navigation menu"
870
996
  };
871
- var styles = reactNative.StyleSheet.create({
997
+ var styles2 = reactNative.StyleSheet.create({
872
998
  overlay: { ...reactNative.StyleSheet.absoluteFillObject, zIndex: DRAWER_Z_INDEX },
873
999
  scrim: { ...reactNative.StyleSheet.absoluteFillObject, backgroundColor: SCRIM_COLOR },
874
1000
  panel: { position: "absolute", top: 0, bottom: 0, left: 0 },
@@ -889,10 +1015,10 @@ var MenuToggle = ({ label, hint, onOpen, testID }) => {
889
1015
  accessibilityHint: hint,
890
1016
  accessibilityLabel: label,
891
1017
  ringColor: theme.palette.primary["500"],
892
- style: styles.menuToggle,
1018
+ style: styles2.menuToggle,
893
1019
  testID,
894
1020
  onPress: onOpen,
895
- 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 })
896
1022
  }
897
1023
  );
898
1024
  };
@@ -915,21 +1041,34 @@ var MobileDrawer = ({
915
1041
  node.addEventListener("click", handleClick);
916
1042
  return () => node.removeEventListener("click", handleClick);
917
1043
  }, [onClose]);
918
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.overlay, children: [
1044
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.overlay, children: [
919
1045
  /* @__PURE__ */ jsxRuntime.jsx(
920
1046
  reactNative.Pressable,
921
1047
  {
922
1048
  accessibilityHint: labels.closeHint,
923
1049
  accessibilityLabel: labels.closeLabel,
924
1050
  accessibilityRole: "button",
925
- style: styles.scrim,
1051
+ style: styles2.scrim,
926
1052
  testID: scrimTestID,
927
1053
  onPress: onClose
928
1054
  }
929
1055
  ),
930
- /* @__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 })
931
1057
  ] });
932
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
+ }
933
1072
  function resolveContentMaxWidth(width, viewport) {
934
1073
  if (width === "full") return "full";
935
1074
  const wideMinViewport = width.wideMinViewport ?? Number.POSITIVE_INFINITY;
@@ -941,13 +1080,7 @@ function useContentMaxWidth(width) {
941
1080
  return resolveContentMaxWidth(width, viewport);
942
1081
  }
943
1082
  var DEFAULT_CONTENT_PADDING = 24;
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 styles2 = reactNative.StyleSheet.create({
1083
+ var styles3 = reactNative.StyleSheet.create({
951
1084
  root: { flex: 1 },
952
1085
  centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
953
1086
  scroll: { flex: 1 },
@@ -961,50 +1094,14 @@ var styles2 = reactNative.StyleSheet.create({
961
1094
  columnCapped: { flex: 1, width: "100%", alignSelf: "center" },
962
1095
  // Non-flex variant used to cap the nav strip's inner content to the content
963
1096
  // column so the nav aligns with the page content instead of running full-bleed.
964
- chromeCapped: { width: "100%", alignSelf: "center" },
965
- card: {
966
- padding: CARD_PADDING,
967
- borderRadius: CARD_BORDER_RADIUS,
968
- borderWidth: CARD_BORDER_WIDTH
969
- },
970
- cardTitle: { fontSize: CARD_TITLE_FONT_SIZE, fontWeight: "700", marginBottom: CARD_TITLE_MARGIN_BOTTOM },
971
- cardMessage: { fontSize: CARD_MESSAGE_FONT_SIZE }
1097
+ chromeCapped: { width: "100%", alignSelf: "center" }
972
1098
  });
973
- function MessageCard({
974
- message,
975
- accentColor,
976
- testID
977
- }) {
978
- const { theme } = uiFeedback.useUi();
979
- const colors = theme.colors;
980
- return /* @__PURE__ */ jsxRuntime.jsxs(
981
- reactNative.View,
982
- {
983
- style: [styles2.card, { backgroundColor: colors.surface, borderColor: accentColor }],
984
- testID,
985
- children: [
986
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.cardTitle, { color: accentColor }], children: message.titleText }),
987
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.cardMessage, { color: colors.textSecondary }], children: message.messageText })
988
- ]
989
- }
990
- );
991
- }
992
- function useContentBody(state, children, testID) {
993
- const { theme } = uiFeedback.useUi();
994
- const primary = theme.palette.primary["500"];
995
- const errorColor = theme.semantic.error["500"];
996
- if (state?.forbidden)
997
- return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.forbidden, testID: `${testID}${APP_SHELL_SUFFIX.forbidden}` });
998
- if (state?.error)
999
- return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
1000
- if (state?.loading === true)
1001
- 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" }) });
1002
- return children;
1003
- }
1004
1099
  var AppShell = ({
1005
1100
  header,
1006
1101
  nav,
1007
1102
  sidebar,
1103
+ collapsedSidebar,
1104
+ collapsedRailRange,
1008
1105
  mobileMenu,
1009
1106
  banner,
1010
1107
  width = "full",
@@ -1020,34 +1117,40 @@ var AppShell = ({
1020
1117
  const maxWidth = useContentMaxWidth(width);
1021
1118
  const body = useContentBody(state, children, testID);
1022
1119
  const { width: viewportWidth } = reactNative.useWindowDimensions();
1023
- const isNarrow = viewportWidth < MOBILE_BREAKPOINT;
1024
- const useDrawer = sidebar !== void 0 && isNarrow;
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";
1025
1128
  const [drawerOpen, setDrawerOpen] = React5.useState(false);
1026
1129
  const openDrawer = React5.useCallback(() => setDrawerOpen(true), []);
1027
1130
  const closeDrawer = React5.useCallback(() => setDrawerOpen(false), []);
1028
1131
  React5.useEffect(() => {
1029
- if (!isNarrow && drawerOpen) setDrawerOpen(false);
1030
- }, [isNarrow, drawerOpen]);
1132
+ if (!useDrawer && drawerOpen) setDrawerOpen(false);
1133
+ }, [useDrawer, drawerOpen]);
1031
1134
  const drawerLabels = { ...DEFAULT_DRAWER_LABELS, ...mobileMenu };
1032
1135
  const isUnauthenticated = gate !== void 0 && !gate.pending && !gate.authenticated;
1033
1136
  React5.useEffect(() => {
1034
1137
  if (isUnauthenticated && gate !== void 0) gate.onRedirect();
1035
1138
  }, [isUnauthenticated, gate]);
1036
1139
  if (gate?.pending === true)
1037
- 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" }) });
1038
1141
  if (isUnauthenticated) return null;
1039
- const columnStyle = maxWidth === "full" ? styles2.columnFull : [styles2.columnCapped, { maxWidth }];
1040
- 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;
1041
1144
  const scroller = /* @__PURE__ */ jsxRuntime.jsx(
1042
1145
  reactNative.ScrollView,
1043
1146
  {
1044
- contentContainerStyle: [styles2.scrollContent, { padding: contentPadding }],
1045
- style: styles2.scroll,
1147
+ contentContainerStyle: [styles3.scrollContent, { padding: contentPadding }],
1148
+ style: styles3.scroll,
1046
1149
  testID: `${testID}${APP_SHELL_SUFFIX.content}`,
1047
1150
  children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: columnStyle, children: body })
1048
1151
  }
1049
1152
  );
1050
- 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: [
1051
1154
  /* @__PURE__ */ jsxRuntime.jsx(
1052
1155
  MenuToggle,
1053
1156
  {
@@ -1057,15 +1160,15 @@ var AppShell = ({
1057
1160
  onOpen: openDrawer
1058
1161
  }
1059
1162
  ),
1060
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.headerFill, children: header })
1163
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.headerFill, children: header })
1061
1164
  ] }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: header });
1062
- const showRail = sidebar !== void 0 && !isNarrow;
1063
- 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: [
1064
1167
  headerRegion,
1065
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,
1066
1169
  banner !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: banner }) : null,
1067
- showRail ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.bodyRow, children: [
1068
- /* @__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,
1069
1172
  scroller
1070
1173
  ] }) : scroller,
1071
1174
  useDrawer && drawerOpen ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -1080,6 +1183,261 @@ var AppShell = ({
1080
1183
  ) : null
1081
1184
  ] });
1082
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
+ };
1083
1441
  function roleRoutesToNavItems(routes, translate) {
1084
1442
  return routes.map((entry) => ({
1085
1443
  key: entry.role,
@@ -1096,6 +1454,9 @@ exports.APP_SHELL_SUFFIX = APP_SHELL_SUFFIX;
1096
1454
  exports.AppShell = AppShell;
1097
1455
  exports.BASE_INDENT = BASE_INDENT;
1098
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;
1099
1460
  exports.NAV_ICON_SIZE = NAV_ICON_SIZE;
1100
1461
  exports.NAV_LINK_GAP = NAV_LINK_GAP;
1101
1462
  exports.NAV_TEST_IDS = NAV_TEST_IDS;
@@ -1103,13 +1464,20 @@ exports.Nav = Nav;
1103
1464
  exports.NavBar = NavBar;
1104
1465
  exports.NavExpandableItem = NavExpandableItem;
1105
1466
  exports.NavOverflowMenu = NavOverflowMenu;
1467
+ exports.NavShell = NavShell;
1468
+ exports.PillNav = PillNav;
1469
+ exports.RAIL_FULL_BREAKPOINT = RAIL_FULL_BREAKPOINT;
1106
1470
  exports.Sidebar = Sidebar;
1107
1471
  exports.Topbar = Topbar;
1108
1472
  exports.accessibleNavItems = accessibleNavItems;
1473
+ exports.collapsedRailStyles = collapsedRailStyles;
1474
+ exports.darkModeStyles = darkModeStyles;
1109
1475
  exports.expandableStyles = expandableStyles;
1110
1476
  exports.isRouteActive = isRouteActive;
1111
1477
  exports.navStyles = navStyles;
1478
+ exports.pillNavStyles = pillNavStyles;
1112
1479
  exports.resolveContentMaxWidth = resolveContentMaxWidth;
1480
+ exports.resolveRailMode = resolveRailMode;
1113
1481
  exports.roleRoutesToNavItems = roleRoutesToNavItems;
1114
1482
  exports.useContentMaxWidth = useContentMaxWidth;
1115
1483
  //# sourceMappingURL=index.js.map