@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.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,
@@ -724,7 +806,8 @@ var NavBar = ({
724
806
  collapseBelow = DEFAULT_COLLAPSE_BELOW,
725
807
  overflowLabel = "More",
726
808
  overflowHint = "",
727
- containerStyle
809
+ containerStyle,
810
+ contentMaxWidth
728
811
  }) => {
729
812
  const { theme } = uiFeedback.useUi();
730
813
  const colors = theme.colors;
@@ -736,6 +819,10 @@ var NavBar = ({
736
819
  const { visibleCount, setAvailableWidth, setItemWidth, setMoreWidth } = useNavOverflow(items.length, NAV_LINK_GAP);
737
820
  const collapsed = width < collapseBelow;
738
821
  const showLinks = !collapsed || open;
822
+ const innerCapStyle = React5.useMemo(
823
+ () => contentMaxWidth === void 0 ? null : { maxWidth: contentMaxWidth, width: "100%", alignSelf: "center" },
824
+ [contentMaxWidth]
825
+ );
739
826
  const toggleMenu = React5.useCallback(() => setOpen((v) => !v), []);
740
827
  const handlePress = React5.useCallback(
741
828
  (route) => {
@@ -765,7 +852,7 @@ var NavBar = ({
765
852
  { backgroundColor: colors.surface, borderBottomColor: colors.border },
766
853
  containerStyle
767
854
  ],
768
- children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [navBarStyles.inner, collapsed ? null : navBarStyles.innerNoWrap], children: [
855
+ children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [navBarStyles.inner, collapsed ? null : navBarStyles.innerNoWrap, innerCapStyle], children: [
769
856
  brand !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: navBarStyles.brand, children: brand }) : null,
770
857
  collapsed ? /* @__PURE__ */ jsxRuntime.jsx(
771
858
  reactNative.Pressable,
@@ -848,13 +935,57 @@ var NavBar = ({
848
935
  }
849
936
  );
850
937
  };
938
+ var NavBar = ({ barTheme, ...props }) => {
939
+ const { t, navigate } = uiFeedback.useUi();
940
+ if (barTheme === void 0) return /* @__PURE__ */ jsxRuntime.jsx(NavBarInner, { ...props });
941
+ return /* @__PURE__ */ jsxRuntime.jsx(uiFeedback.UiProvider, { navigate, t, theme: barTheme, children: /* @__PURE__ */ jsxRuntime.jsx(NavBarInner, { ...props }) });
942
+ };
851
943
  var Nav = (props) => {
852
944
  if (props.orientation === "vertical") {
853
945
  return /* @__PURE__ */ jsxRuntime.jsx(Sidebar, { ...props });
854
946
  }
855
947
  return /* @__PURE__ */ jsxRuntime.jsx(NavBar, { ...props });
856
948
  };
857
- var MOBILE_BREAKPOINT = 768;
949
+ var CARD_PADDING = 20;
950
+ var CARD_BORDER_RADIUS = 12;
951
+ var CARD_BORDER_WIDTH = 1;
952
+ var CARD_TITLE_FONT_SIZE = 16;
953
+ var CARD_MESSAGE_FONT_SIZE = 14;
954
+ var CARD_TITLE_MARGIN_BOTTOM = 8;
955
+ var styles = reactNative.StyleSheet.create({
956
+ centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
957
+ card: {
958
+ padding: CARD_PADDING,
959
+ borderRadius: CARD_BORDER_RADIUS,
960
+ borderWidth: CARD_BORDER_WIDTH
961
+ },
962
+ cardTitle: { fontSize: CARD_TITLE_FONT_SIZE, fontWeight: "700", marginBottom: CARD_TITLE_MARGIN_BOTTOM },
963
+ cardMessage: { fontSize: CARD_MESSAGE_FONT_SIZE }
964
+ });
965
+ function MessageCard({
966
+ message,
967
+ accentColor,
968
+ testID
969
+ }) {
970
+ const { theme } = uiFeedback.useUi();
971
+ const colors = theme.colors;
972
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.card, { backgroundColor: colors.surface, borderColor: accentColor }], testID, children: [
973
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.cardTitle, { color: accentColor }], children: message.titleText }),
974
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.cardMessage, { color: colors.textSecondary }], children: message.messageText })
975
+ ] });
976
+ }
977
+ function useContentBody(state, children, testID) {
978
+ const { theme } = uiFeedback.useUi();
979
+ const primary = theme.palette.primary["500"];
980
+ const errorColor = theme.semantic.error["500"];
981
+ if (state?.forbidden)
982
+ return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.forbidden, testID: `${testID}${APP_SHELL_SUFFIX.forbidden}` });
983
+ if (state?.error)
984
+ return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
985
+ if (state?.loading === true)
986
+ 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" }) });
987
+ return children;
988
+ }
858
989
  var SCRIM_COLOR = "rgba(0, 0, 0, 0.5)";
859
990
  var MENU_GLYPH2 = "\u2630";
860
991
  var MENU_MIN_TARGET = 44;
@@ -868,7 +999,7 @@ var DEFAULT_DRAWER_LABELS = {
868
999
  closeLabel: "Close menu",
869
1000
  closeHint: "Close the navigation menu"
870
1001
  };
871
- var styles = reactNative.StyleSheet.create({
1002
+ var styles2 = reactNative.StyleSheet.create({
872
1003
  overlay: { ...reactNative.StyleSheet.absoluteFillObject, zIndex: DRAWER_Z_INDEX },
873
1004
  scrim: { ...reactNative.StyleSheet.absoluteFillObject, backgroundColor: SCRIM_COLOR },
874
1005
  panel: { position: "absolute", top: 0, bottom: 0, left: 0 },
@@ -889,10 +1020,10 @@ var MenuToggle = ({ label, hint, onOpen, testID }) => {
889
1020
  accessibilityHint: hint,
890
1021
  accessibilityLabel: label,
891
1022
  ringColor: theme.palette.primary["500"],
892
- style: styles.menuToggle,
1023
+ style: styles2.menuToggle,
893
1024
  testID,
894
1025
  onPress: onOpen,
895
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.menuToggleGlyph, { color: theme.colors.text }], children: MENU_GLYPH2 })
1026
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.menuToggleGlyph, { color: theme.colors.text }], children: MENU_GLYPH2 })
896
1027
  }
897
1028
  );
898
1029
  };
@@ -915,21 +1046,34 @@ var MobileDrawer = ({
915
1046
  node.addEventListener("click", handleClick);
916
1047
  return () => node.removeEventListener("click", handleClick);
917
1048
  }, [onClose]);
918
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.overlay, children: [
1049
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.overlay, children: [
919
1050
  /* @__PURE__ */ jsxRuntime.jsx(
920
1051
  reactNative.Pressable,
921
1052
  {
922
1053
  accessibilityHint: labels.closeHint,
923
1054
  accessibilityLabel: labels.closeLabel,
924
1055
  accessibilityRole: "button",
925
- style: styles.scrim,
1056
+ style: styles2.scrim,
926
1057
  testID: scrimTestID,
927
1058
  onPress: onClose
928
1059
  }
929
1060
  ),
930
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ref: panelRef, "aria-modal": true, role: "dialog", style: styles.panel, testID: drawerTestID, children: sidebar })
1061
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ref: panelRef, "aria-modal": true, role: "dialog", style: styles2.panel, testID: drawerTestID, children: sidebar })
931
1062
  ] });
932
1063
  };
1064
+
1065
+ // src/railMode.ts
1066
+ var RAIL_FULL_BREAKPOINT = 768;
1067
+ var DEFAULT_COLLAPSED_RAIL_MAX = 1024;
1068
+ function resolveRailMode({ viewport, hasSidebar, hasCollapsed, range }) {
1069
+ if (!hasSidebar) return "none";
1070
+ if (hasCollapsed) {
1071
+ if (viewport >= range.max) return "full";
1072
+ if (viewport >= range.min) return "collapsed";
1073
+ return "drawer";
1074
+ }
1075
+ return viewport >= RAIL_FULL_BREAKPOINT ? "full" : "drawer";
1076
+ }
933
1077
  function resolveContentMaxWidth(width, viewport) {
934
1078
  if (width === "full") return "full";
935
1079
  const wideMinViewport = width.wideMinViewport ?? Number.POSITIVE_INFINITY;
@@ -941,13 +1085,7 @@ function useContentMaxWidth(width) {
941
1085
  return resolveContentMaxWidth(width, viewport);
942
1086
  }
943
1087
  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({
1088
+ var styles3 = reactNative.StyleSheet.create({
951
1089
  root: { flex: 1 },
952
1090
  centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
953
1091
  scroll: { flex: 1 },
@@ -961,50 +1099,14 @@ var styles2 = reactNative.StyleSheet.create({
961
1099
  columnCapped: { flex: 1, width: "100%", alignSelf: "center" },
962
1100
  // Non-flex variant used to cap the nav strip's inner content to the content
963
1101
  // 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 }
1102
+ chromeCapped: { width: "100%", alignSelf: "center" }
972
1103
  });
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
1104
  var AppShell = ({
1005
1105
  header,
1006
1106
  nav,
1007
1107
  sidebar,
1108
+ collapsedSidebar,
1109
+ collapsedRailRange,
1008
1110
  mobileMenu,
1009
1111
  banner,
1010
1112
  width = "full",
@@ -1020,34 +1122,40 @@ var AppShell = ({
1020
1122
  const maxWidth = useContentMaxWidth(width);
1021
1123
  const body = useContentBody(state, children, testID);
1022
1124
  const { width: viewportWidth } = reactNative.useWindowDimensions();
1023
- const isNarrow = viewportWidth < MOBILE_BREAKPOINT;
1024
- const useDrawer = sidebar !== void 0 && isNarrow;
1125
+ const railRange = collapsedRailRange ?? { min: RAIL_FULL_BREAKPOINT, max: DEFAULT_COLLAPSED_RAIL_MAX };
1126
+ const railMode = resolveRailMode({
1127
+ viewport: viewportWidth,
1128
+ hasSidebar: sidebar !== void 0,
1129
+ hasCollapsed: collapsedSidebar !== void 0,
1130
+ range: railRange
1131
+ });
1132
+ const useDrawer = railMode === "drawer";
1025
1133
  const [drawerOpen, setDrawerOpen] = React5.useState(false);
1026
1134
  const openDrawer = React5.useCallback(() => setDrawerOpen(true), []);
1027
1135
  const closeDrawer = React5.useCallback(() => setDrawerOpen(false), []);
1028
1136
  React5.useEffect(() => {
1029
- if (!isNarrow && drawerOpen) setDrawerOpen(false);
1030
- }, [isNarrow, drawerOpen]);
1137
+ if (!useDrawer && drawerOpen) setDrawerOpen(false);
1138
+ }, [useDrawer, drawerOpen]);
1031
1139
  const drawerLabels = { ...DEFAULT_DRAWER_LABELS, ...mobileMenu };
1032
1140
  const isUnauthenticated = gate !== void 0 && !gate.pending && !gate.authenticated;
1033
1141
  React5.useEffect(() => {
1034
1142
  if (isUnauthenticated && gate !== void 0) gate.onRedirect();
1035
1143
  }, [isUnauthenticated, gate]);
1036
1144
  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" }) });
1145
+ 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
1146
  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;
1147
+ const columnStyle = maxWidth === "full" ? styles3.columnFull : [styles3.columnCapped, { maxWidth }];
1148
+ const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles3.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
1041
1149
  const scroller = /* @__PURE__ */ jsxRuntime.jsx(
1042
1150
  reactNative.ScrollView,
1043
1151
  {
1044
- contentContainerStyle: [styles2.scrollContent, { padding: contentPadding }],
1045
- style: styles2.scroll,
1152
+ contentContainerStyle: [styles3.scrollContent, { padding: contentPadding }],
1153
+ style: styles3.scroll,
1046
1154
  testID: `${testID}${APP_SHELL_SUFFIX.content}`,
1047
1155
  children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: columnStyle, children: body })
1048
1156
  }
1049
1157
  );
1050
- const headerRegion = useDrawer ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.headerRow, testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: [
1158
+ const headerRegion = useDrawer ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.headerRow, testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: [
1051
1159
  /* @__PURE__ */ jsxRuntime.jsx(
1052
1160
  MenuToggle,
1053
1161
  {
@@ -1057,15 +1165,15 @@ var AppShell = ({
1057
1165
  onOpen: openDrawer
1058
1166
  }
1059
1167
  ),
1060
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.headerFill, children: header })
1168
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.headerFill, children: header })
1061
1169
  ] }) : /* @__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: [
1170
+ 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;
1171
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles3.root, { backgroundColor: theme.colors.background }], testID, children: [
1064
1172
  headerRegion,
1065
1173
  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
1174
  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 }),
1175
+ railRegion !== null ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.bodyRow, children: [
1176
+ railRegion,
1069
1177
  scroller
1070
1178
  ] }) : scroller,
1071
1179
  useDrawer && drawerOpen ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -1080,6 +1188,262 @@ var AppShell = ({
1080
1188
  ) : null
1081
1189
  ] });
1082
1190
  };
1191
+ var COLLAPSED_ICON_SIZE = 22;
1192
+ function glyphFor(label) {
1193
+ return label.trim().charAt(0).toUpperCase();
1194
+ }
1195
+ var CollapsedRail = ({
1196
+ items,
1197
+ pathname,
1198
+ onNavigate,
1199
+ regionLabel,
1200
+ navigateHint = (label) => label,
1201
+ header,
1202
+ footer,
1203
+ containerStyle
1204
+ }) => {
1205
+ const { theme } = uiFeedback.useUi();
1206
+ const colors = theme.colors;
1207
+ const primaryColor = theme.palette.primary["500"];
1208
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1209
+ reactNative.View,
1210
+ {
1211
+ accessibilityRole: "none",
1212
+ "aria-label": regionLabel,
1213
+ role: "navigation",
1214
+ style: [
1215
+ collapsedRailStyles.container,
1216
+ { backgroundColor: colors.surface, borderRightColor: colors.border },
1217
+ containerStyle
1218
+ ],
1219
+ children: [
1220
+ header !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.slot, children: header }) : null,
1221
+ items.map((item) => {
1222
+ const active = isRouteActive(pathname, item.route);
1223
+ const iconColor = active ? primaryColor : colors.textSecondary;
1224
+ return /* @__PURE__ */ jsxRuntime.jsx(
1225
+ FocusableTouchable,
1226
+ {
1227
+ accessibilityHint: navigateHint(item.label),
1228
+ accessibilityLabel: item.label,
1229
+ ringColor: primaryColor,
1230
+ style: [collapsedRailStyles.item, active ? { backgroundColor: colors.border } : void 0],
1231
+ testID: item.testID ?? item.key,
1232
+ onPress: () => onNavigate(item.route),
1233
+ 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) })
1234
+ },
1235
+ item.key
1236
+ );
1237
+ }),
1238
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.spacer }),
1239
+ footer !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.slot, children: footer }) : null
1240
+ ]
1241
+ }
1242
+ );
1243
+ };
1244
+ function wantsTopBar(layout, hasHeader, hasTopBar) {
1245
+ return layout === "top" || layout === "both" || hasHeader || layout === "side" && hasTopBar;
1246
+ }
1247
+ function wantsSideRail(layout, hasSideRail) {
1248
+ return (layout === "side" || layout === "both") && hasSideRail;
1249
+ }
1250
+ var NavShell = ({
1251
+ layout,
1252
+ pathname,
1253
+ onNavigate,
1254
+ regionLabel,
1255
+ navigateHint,
1256
+ topBar,
1257
+ header,
1258
+ sideRail,
1259
+ children,
1260
+ collapsedSidebar: collapsedSidebarProp,
1261
+ collapsedRailRange: collapsedRailRangeProp,
1262
+ ...shellProps
1263
+ }) => {
1264
+ const showTopBar = wantsTopBar(layout, header !== void 0, topBar !== void 0);
1265
+ const showSideRail = wantsSideRail(layout, sideRail !== void 0);
1266
+ const headerNode = header !== void 0 ? header : showTopBar && topBar !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1267
+ NavBar,
1268
+ {
1269
+ barTheme: topBar.barTheme,
1270
+ brand: topBar.brand,
1271
+ collapseBelow: topBar.collapseBelow,
1272
+ containerStyle: topBar.containerStyle,
1273
+ contentMaxWidth: topBar.contentMaxWidth,
1274
+ items: topBar.items,
1275
+ menuHint: topBar.menuHint,
1276
+ menuLabel: topBar.menuLabel,
1277
+ navigateHint,
1278
+ overflowHint: topBar.overflowHint,
1279
+ overflowLabel: topBar.overflowLabel,
1280
+ pathname,
1281
+ regionLabel,
1282
+ renderMenuIcon: topBar.renderMenuIcon,
1283
+ right: topBar.right,
1284
+ onNavigate
1285
+ }
1286
+ ) : null;
1287
+ const sidebarNode = showSideRail && sideRail !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1288
+ Sidebar,
1289
+ {
1290
+ collapseHint: sideRail.collapseHint,
1291
+ containerStyle: sideRail.containerStyle,
1292
+ expandHint: sideRail.expandHint,
1293
+ footer: sideRail.footer,
1294
+ header: sideRail.header,
1295
+ items: sideRail.items,
1296
+ navigateHint,
1297
+ pathname,
1298
+ regionLabel,
1299
+ renderChevron: sideRail.renderChevron,
1300
+ title: sideRail.title,
1301
+ onNavigate
1302
+ }
1303
+ ) : void 0;
1304
+ const collapsedCfg = showSideRail ? sideRail?.collapsed : void 0;
1305
+ const collapsedNode = collapsedCfg !== void 0 && sideRail !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1306
+ CollapsedRail,
1307
+ {
1308
+ footer: collapsedCfg.footer,
1309
+ header: collapsedCfg.header,
1310
+ items: collapsedCfg.items ?? sideRail.items,
1311
+ navigateHint,
1312
+ pathname,
1313
+ regionLabel,
1314
+ onNavigate
1315
+ }
1316
+ ) : collapsedSidebarProp;
1317
+ const collapsedRange = collapsedCfg?.range ?? collapsedRailRangeProp;
1318
+ return /* @__PURE__ */ jsxRuntime.jsx(
1319
+ AppShell,
1320
+ {
1321
+ ...shellProps,
1322
+ collapsedRailRange: collapsedRange,
1323
+ collapsedSidebar: collapsedNode,
1324
+ header: headerNode,
1325
+ sidebar: sidebarNode,
1326
+ children
1327
+ }
1328
+ );
1329
+ };
1330
+ var TEXT_ON_PRIMARY4 = "#ffffff";
1331
+ var DEFAULT_MIN_ITEMS = 1;
1332
+ var PillNav = ({
1333
+ items,
1334
+ pathname,
1335
+ onNavigate,
1336
+ regionLabel,
1337
+ navigateHint = (label) => label,
1338
+ minItems = DEFAULT_MIN_ITEMS,
1339
+ containerStyle
1340
+ }) => {
1341
+ const { theme } = uiFeedback.useUi();
1342
+ const colors = theme.colors;
1343
+ const primaryColor = theme.palette.primary["500"];
1344
+ if (items.length < minItems) return null;
1345
+ return /* @__PURE__ */ jsxRuntime.jsx(
1346
+ reactNative.View,
1347
+ {
1348
+ accessibilityRole: "none",
1349
+ "aria-label": regionLabel,
1350
+ role: "navigation",
1351
+ style: [pillNavStyles.container, containerStyle],
1352
+ children: items.map((item) => {
1353
+ const active = isRouteActive(pathname, item.route);
1354
+ const textColor = active ? TEXT_ON_PRIMARY4 : colors.textSecondary;
1355
+ const pillStyle = active ? { backgroundColor: primaryColor } : { backgroundColor: colors.surfaceElevated };
1356
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1357
+ FocusableTouchable,
1358
+ {
1359
+ accessibilityHint: navigateHint(item.label),
1360
+ accessibilityLabel: item.label,
1361
+ accessibilityRole: "link",
1362
+ ringColor: primaryColor,
1363
+ style: [pillNavStyles.pill, pillStyle],
1364
+ testID: item.testID ?? item.key,
1365
+ onPress: () => onNavigate(item.route),
1366
+ children: [
1367
+ typeof item.renderIcon === "function" ? item.renderIcon(textColor, NAV_ICON_SIZE) : null,
1368
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [pillNavStyles.pillText, { color: textColor }], children: item.label })
1369
+ ]
1370
+ },
1371
+ item.key
1372
+ );
1373
+ })
1374
+ }
1375
+ );
1376
+ };
1377
+ var TEXT_ON_PRIMARY5 = "#ffffff";
1378
+ var DARK_MODE_ICON_SIZE = 16;
1379
+ var DarkModeControl = ({
1380
+ value,
1381
+ options,
1382
+ onChange,
1383
+ regionLabel,
1384
+ variant = "segmented",
1385
+ testID,
1386
+ containerStyle
1387
+ }) => {
1388
+ const { theme } = uiFeedback.useUi();
1389
+ const colors = theme.colors;
1390
+ const primaryColor = theme.palette.primary["500"];
1391
+ const currentIndex = options.findIndex((option) => option.value === value);
1392
+ const current = currentIndex >= 0 ? options[currentIndex] : options[0];
1393
+ const advance = React5.useCallback(() => {
1394
+ if (options.length === 0) return;
1395
+ const from = currentIndex >= 0 ? currentIndex : 0;
1396
+ const next = options[(from + 1) % options.length];
1397
+ if (next !== void 0) onChange(next.value);
1398
+ }, [options, currentIndex, onChange]);
1399
+ if (options.length === 0 || current === void 0) return null;
1400
+ if (variant === "cycle") {
1401
+ const iconColor = colors.text;
1402
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { "aria-label": regionLabel, style: containerStyle, children: /* @__PURE__ */ jsxRuntime.jsxs(
1403
+ FocusableTouchable,
1404
+ {
1405
+ accessibilityHint: current.hint,
1406
+ accessibilityLabel: current.label,
1407
+ ringColor: primaryColor,
1408
+ style: [darkModeStyles.cycle, { backgroundColor: colors.surfaceElevated }],
1409
+ testID,
1410
+ onPress: advance,
1411
+ children: [
1412
+ typeof current.renderIcon === "function" ? current.renderIcon(iconColor, DARK_MODE_ICON_SIZE) : null,
1413
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [darkModeStyles.cycleText, { color: colors.text }], children: current.label })
1414
+ ]
1415
+ }
1416
+ ) });
1417
+ }
1418
+ return /* @__PURE__ */ jsxRuntime.jsx(
1419
+ reactNative.View,
1420
+ {
1421
+ "aria-label": regionLabel,
1422
+ role: "group",
1423
+ style: [darkModeStyles.segmentRow, { borderColor: colors.border }, containerStyle],
1424
+ children: options.map((option) => {
1425
+ const active = option.value === value;
1426
+ const textColor = active ? TEXT_ON_PRIMARY5 : colors.textSecondary;
1427
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1428
+ FocusableTouchable,
1429
+ {
1430
+ accessibilityHint: option.hint,
1431
+ accessibilityLabel: option.label,
1432
+ ringColor: primaryColor,
1433
+ style: [darkModeStyles.segment, active ? { backgroundColor: primaryColor } : void 0],
1434
+ testID: testID !== void 0 ? `${testID}-${option.value}` : void 0,
1435
+ onPress: () => onChange(option.value),
1436
+ children: [
1437
+ typeof option.renderIcon === "function" ? option.renderIcon(textColor, DARK_MODE_ICON_SIZE) : null,
1438
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [darkModeStyles.segmentText, { color: textColor }], children: option.label })
1439
+ ]
1440
+ },
1441
+ option.value
1442
+ );
1443
+ })
1444
+ }
1445
+ );
1446
+ };
1083
1447
  function roleRoutesToNavItems(routes, translate) {
1084
1448
  return routes.map((entry) => ({
1085
1449
  key: entry.role,
@@ -1096,6 +1460,9 @@ exports.APP_SHELL_SUFFIX = APP_SHELL_SUFFIX;
1096
1460
  exports.AppShell = AppShell;
1097
1461
  exports.BASE_INDENT = BASE_INDENT;
1098
1462
  exports.CHEVRON_ICON_SIZE = CHEVRON_ICON_SIZE;
1463
+ exports.CollapsedRail = CollapsedRail;
1464
+ exports.DEFAULT_COLLAPSED_RAIL_MAX = DEFAULT_COLLAPSED_RAIL_MAX;
1465
+ exports.DarkModeControl = DarkModeControl;
1099
1466
  exports.NAV_ICON_SIZE = NAV_ICON_SIZE;
1100
1467
  exports.NAV_LINK_GAP = NAV_LINK_GAP;
1101
1468
  exports.NAV_TEST_IDS = NAV_TEST_IDS;
@@ -1103,13 +1470,20 @@ exports.Nav = Nav;
1103
1470
  exports.NavBar = NavBar;
1104
1471
  exports.NavExpandableItem = NavExpandableItem;
1105
1472
  exports.NavOverflowMenu = NavOverflowMenu;
1473
+ exports.NavShell = NavShell;
1474
+ exports.PillNav = PillNav;
1475
+ exports.RAIL_FULL_BREAKPOINT = RAIL_FULL_BREAKPOINT;
1106
1476
  exports.Sidebar = Sidebar;
1107
1477
  exports.Topbar = Topbar;
1108
1478
  exports.accessibleNavItems = accessibleNavItems;
1479
+ exports.collapsedRailStyles = collapsedRailStyles;
1480
+ exports.darkModeStyles = darkModeStyles;
1109
1481
  exports.expandableStyles = expandableStyles;
1110
1482
  exports.isRouteActive = isRouteActive;
1111
1483
  exports.navStyles = navStyles;
1484
+ exports.pillNavStyles = pillNavStyles;
1112
1485
  exports.resolveContentMaxWidth = resolveContentMaxWidth;
1486
+ exports.resolveRailMode = resolveRailMode;
1113
1487
  exports.roleRoutesToNavItems = roleRoutesToNavItems;
1114
1488
  exports.useContentMaxWidth = useContentMaxWidth;
1115
1489
  //# sourceMappingURL=index.js.map