@dloizides/ui-nav 1.10.0 → 1.10.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.10.2
4
+
5
+ - **Fix (the mobile nav DRAWER was not clickable — a tap on a nav item did not navigate, it
6
+ just closed the drawer / did nothing, in all seven portals below 768px).**
7
+ `MobileDrawer` rendered a full-bleed scrim `Pressable` (`StyleSheet.absoluteFillObject`) and a
8
+ panel `View` with `{ position: 'absolute', top: 0, bottom: 0, left: 0 }` — **no width and no
9
+ zIndex**. Reproduced structurally: the panel's computed `width` was empty and its `zIndex` was
10
+ `0`; the scrim's computed `left`/`right` were both `0px` at `zIndex` `0`. So the scrim FULLY
11
+ overlapped the panel at the SAME stacking level, and on react-native-web the equal-zIndex scrim
12
+ won the pointer (the estate's known "RN-web Pressable swallows a sibling/child press" trap):
13
+ the tap closed the drawer and the nav-item's `TouchableOpacity` `onPress` (→ the app's router)
14
+ never fired.
15
+ - Fixed once, in the kit: the panel now has an explicit drawer **width** (`280px`), a solid
16
+ surface background, and a `zIndex` **above** the scrim; the scrim is anchored BESIDE the panel
17
+ (`left: DRAWER_WIDTH`) so its tap-to-close hit area never overlaps the panel. Nav-item taps
18
+ reach the `Sidebar` leaves; taps beside the drawer still close it.
19
+ - Guarded by `src/mobileDrawer.test.tsx` (RED-first): three structural assertions read the
20
+ resolved geometry via `getComputedStyle` and FAIL against the pre-fix panel/scrim (no width,
21
+ equal zIndex, overlapping scrim); two behavioural assertions lock the contract (nav-item tap
22
+ navigates + closes; scrim tap only closes).
23
+ - **No API change.** Consumers need no edit — bump the dependency and the drawer works.
24
+
25
+ ## 1.10.1
26
+
27
+ - **Fix (a plain-string slot logged `console.error` on EVERY render, in all seven portals).**
28
+ Every caller-supplied slot in the kit is typed `React.ReactNode`, which legitimately includes
29
+ `string` — and callers pass exactly that (`brand: FM('app.name')`, a plain localized string).
30
+ The slot was dropped straight into a `<View>`, and react-native-web refuses to render a bare
31
+ text node there: `Unexpected text node: <s>. A text node cannot be a child of a <View>.`
32
+ Nothing in the type system objected, so the defect only ever surfaced as browser console
33
+ noise — which is why it survived to 1.10.0 across the whole fleet.
34
+ - Fixed once, in the kit, via the new internal `renderTextSlot` helper: a `string`/`number`
35
+ slot is wrapped in `<Text>` (routed through the theme's `colors.text`, so it honours dark
36
+ mode rather than defaulting to black); an element slot passes through **untouched**, so
37
+ every existing caller renders byte-identically. Array slots have each string member wrapped
38
+ individually, keys preserved.
39
+ - Applied to **all 12 slot render sites**, not just the reported one — `NavBar` (`brand`,
40
+ `right`), `Topbar` (`left`, `notificationSlot`), `Sidebar` (`header`, `footer`),
41
+ `CollapsedRail` (`header`, `footer`), `AppShell` (`header`, `nav`, `sidebar`,
42
+ `collapsedSidebar`, `banner`) and `MobileDrawer` (`sidebar`).
43
+ - Guarded by `src/rawTextSlots.test.tsx`, which spies on `console.error` and asserts silence.
44
+ Unit tests that only assert "the slot rendered" pass against the bug; this one does not.
45
+ - **No API change.** Consumers need no edit — the `<Text>` wrapper they were expected to
46
+ remember is now the kit's job.
47
+
3
48
  ## 1.10.0
4
49
 
5
50
  - **Fix (`layout` was not authoritative — `layout="side"` rendered a top bar anyway).**
package/dist/index.js CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  var reactNative = require('react-native');
4
4
  var uiFeedback = require('@dloizides/ui-feedback');
5
- var React5 = require('react');
5
+ var React2 = require('react');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
7
  var uiLayout = require('@dloizides/ui-layout');
8
8
  var authWeb = require('@dloizides/auth-web');
9
9
 
10
10
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
11
 
12
- var React5__default = /*#__PURE__*/_interopDefault(React5);
12
+ var React2__default = /*#__PURE__*/_interopDefault(React2);
13
13
 
14
14
  // src/Sidebar.tsx
15
15
 
@@ -299,21 +299,21 @@ var NavExpandableItem = ({
299
299
  renderChevron,
300
300
  depth = 0
301
301
  }) => {
302
- const [expanded, setExpanded] = React5.useState(false);
303
- const [focused, setFocused] = React5.useState(false);
302
+ const [expanded, setExpanded] = React2.useState(false);
303
+ const [focused, setFocused] = React2.useState(false);
304
304
  const { theme } = uiFeedback.useUi();
305
305
  const colors = theme.colors;
306
306
  const primaryColor = theme.palette.primary["500"];
307
- const toggle = React5.useCallback(() => setExpanded((v) => !v), []);
307
+ const toggle = React2.useCallback(() => setExpanded((v) => !v), []);
308
308
  const indent = depth * BASE_INDENT;
309
309
  const hasChildren = Array.isArray(item.children) && item.children.length > 0;
310
310
  const isActive = isRouteActive(pathname, item.route);
311
- const activeItemStyle = React5.useMemo(
311
+ const activeItemStyle = React2.useMemo(
312
312
  () => ({ backgroundColor: colors.border, borderRadius: ACTIVE_BORDER_RADIUS }),
313
313
  [colors.border]
314
314
  );
315
- const paddingStyle = React5.useMemo(() => ({ paddingLeft: indent + BASE_INDENT }), [indent]);
316
- const headerPaddingStyle = React5.useMemo(() => ({ paddingLeft: indent }), [indent]);
315
+ const paddingStyle = React2.useMemo(() => ({ paddingLeft: indent + BASE_INDENT }), [indent]);
316
+ const headerPaddingStyle = React2.useMemo(() => ({ paddingLeft: indent }), [indent]);
317
317
  if (!hasChildren)
318
318
  return /* @__PURE__ */ jsxRuntime.jsxs(
319
319
  reactNative.TouchableOpacity,
@@ -384,6 +384,17 @@ var NavExpandableItem = ({
384
384
  )) }) : null
385
385
  ] });
386
386
  };
387
+ function isBareText(node) {
388
+ return typeof node === "string" || typeof node === "number";
389
+ }
390
+ function renderTextSlot(node, style) {
391
+ if (node === void 0 || node === null) return node;
392
+ if (isBareText(node)) return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style, children: node });
393
+ return React2__default.default.Children.map(
394
+ node,
395
+ (child) => isBareText(child) ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style, children: child }) : child
396
+ );
397
+ }
387
398
  var Sidebar = ({
388
399
  items,
389
400
  pathname,
@@ -413,7 +424,7 @@ var Sidebar = ({
413
424
  ],
414
425
  children: [
415
426
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { accessibilityRole: "header", style: [navStyles.sidebarTitle, { color: colors.text }], children: title }),
416
- header,
427
+ renderTextSlot(header, { color: colors.text }),
417
428
  items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
418
429
  NavExpandableItem,
419
430
  {
@@ -428,7 +439,7 @@ var Sidebar = ({
428
439
  item.key
429
440
  )),
430
441
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: navStyles.sidebarSpacer }),
431
- footer
442
+ renderTextSlot(footer, { color: colors.text })
432
443
  ]
433
444
  }
434
445
  );
@@ -481,7 +492,7 @@ var FocusableTouchable = ({
481
492
  testID,
482
493
  children
483
494
  }) => {
484
- const [focused, setFocused] = React5.useState(false);
495
+ const [focused, setFocused] = React2.useState(false);
485
496
  return /* @__PURE__ */ jsxRuntime.jsx(
486
497
  reactNative.Pressable,
487
498
  {
@@ -531,7 +542,7 @@ var Topbar = ({
531
542
  containerStyle
532
543
  ],
533
544
  children: [
534
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: navStyles.topbarLeft, children: left }),
545
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: navStyles.topbarLeft, children: renderTextSlot(left, { color: colors.text }) }),
535
546
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: navStyles.topbarRight, children: [
536
547
  language ? /* @__PURE__ */ jsxRuntime.jsx(
537
548
  FocusableTouchable,
@@ -545,7 +556,7 @@ var Topbar = ({
545
556
  children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.topbarLabel, { color: colors.text }], children: language.label })
546
557
  }
547
558
  ) : null,
548
- notificationSlot,
559
+ renderTextSlot(notificationSlot, { color: colors.text }),
549
560
  user ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: navStyles.userBlock, children: [
550
561
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.userName, { color: colors.text }], children: user.name }),
551
562
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.userEmail, { color: colors.textSecondary }], children: user.email })
@@ -634,8 +645,8 @@ var NavBarLink = ({
634
645
  navigateHint,
635
646
  onPress
636
647
  }) => {
637
- const [hovered, setHovered] = React5.useState(false);
638
- const [focused, setFocused] = React5.useState(false);
648
+ const [hovered, setHovered] = React2.useState(false);
649
+ const [focused, setFocused] = React2.useState(false);
639
650
  const isHovered = hovered && !isActive;
640
651
  const textColor = isActive ? TEXT_ON_PRIMARY2 : isHovered ? colors.hoverText : colors.rest;
641
652
  const pillStyle = isActive ? { backgroundColor: colors.activeBg } : isHovered ? { backgroundColor: colors.hoverBg } : void 0;
@@ -679,12 +690,12 @@ var NavOverflowMenu = ({
679
690
  testID,
680
691
  variant
681
692
  }) => {
682
- const options = React5.useMemo(() => items.map((item) => ({ label: item.label, value: item.route })), [items]);
683
- const activeRoute = React5.useMemo(
693
+ const options = React2.useMemo(() => items.map((item) => ({ label: item.label, value: item.route })), [items]);
694
+ const activeRoute = React2.useMemo(
684
695
  () => items.find((item) => isRouteActive(pathname, item.route))?.route ?? NO_ACTIVE_ROUTE,
685
696
  [items, pathname]
686
697
  );
687
- const optionTestID = React5.useMemo(() => {
698
+ const optionTestID = React2.useMemo(() => {
688
699
  const byRoute = new Map(items.map((item) => [item.route, item.testID ?? item.key]));
689
700
  return (route) => byRoute.get(route) ?? `${testID}-option-${route}`;
690
701
  }, [items, testID]);
@@ -741,13 +752,13 @@ function resizeWidthBuffer(previous, count) {
741
752
  return next;
742
753
  }
743
754
  function useNavOverflow(itemCount, gap) {
744
- const [availableWidth, setAvailableWidthState] = React5.useState(0);
745
- const [moreWidth, setMoreWidthState] = React5.useState(0);
746
- const [itemWidths, setItemWidths] = React5.useState(() => new Array(itemCount).fill(0));
747
- React5.useEffect(() => {
755
+ const [availableWidth, setAvailableWidthState] = React2.useState(0);
756
+ const [moreWidth, setMoreWidthState] = React2.useState(0);
757
+ const [itemWidths, setItemWidths] = React2.useState(() => new Array(itemCount).fill(0));
758
+ React2.useEffect(() => {
748
759
  setItemWidths((previous) => previous.length === itemCount ? previous : resizeWidthBuffer(previous, itemCount));
749
760
  }, [itemCount]);
750
- const setItemWidth = React5.useCallback((index, width) => {
761
+ const setItemWidth = React2.useCallback((index, width) => {
751
762
  setItemWidths((previous) => {
752
763
  if (index < 0 || index >= previous.length || previous[index] === width) return previous;
753
764
  const next = previous.slice();
@@ -755,15 +766,15 @@ function useNavOverflow(itemCount, gap) {
755
766
  return next;
756
767
  });
757
768
  }, []);
758
- const setAvailableWidth = React5.useCallback(
769
+ const setAvailableWidth = React2.useCallback(
759
770
  (width) => setAvailableWidthState((previous) => previous === width ? previous : width),
760
771
  []
761
772
  );
762
- const setMoreWidth = React5.useCallback(
773
+ const setMoreWidth = React2.useCallback(
763
774
  (width) => setMoreWidthState((previous) => previous === width ? previous : width),
764
775
  []
765
776
  );
766
- const visibleCount = React5.useMemo(
777
+ const visibleCount = React2.useMemo(
767
778
  () => computeVisibleCount({ availableWidth, itemWidths, moreWidth, gap }),
768
779
  [availableWidth, itemWidths, moreWidth, gap]
769
780
  );
@@ -775,8 +786,8 @@ function getReducedMotionQuery() {
775
786
  return window.matchMedia(REDUCED_MOTION_QUERY);
776
787
  }
777
788
  function useReducedMotion() {
778
- const [reduced, setReduced] = React5.useState(() => getReducedMotionQuery()?.matches ?? false);
779
- React5.useEffect(() => {
789
+ const [reduced, setReduced] = React2.useState(() => getReducedMotionQuery()?.matches ?? false);
790
+ React2.useEffect(() => {
780
791
  const mql = getReducedMotionQuery();
781
792
  if (mql === void 0 || mql.addEventListener === void 0) return void 0;
782
793
  const onChange = () => setReduced(mql.matches);
@@ -815,24 +826,24 @@ var NavBarInner = ({
815
826
  const primaryColor = theme.palette.primary["500"];
816
827
  const { width } = reactNative.useWindowDimensions();
817
828
  const reducedMotion = useReducedMotion();
818
- const [open, setOpen] = React5.useState(false);
819
- const [toggleFocused, setToggleFocused] = React5.useState(false);
829
+ const [open, setOpen] = React2.useState(false);
830
+ const [toggleFocused, setToggleFocused] = React2.useState(false);
820
831
  const { visibleCount, setAvailableWidth, setItemWidth, setMoreWidth } = useNavOverflow(items.length, NAV_LINK_GAP);
821
832
  const collapsed = width < collapseBelow;
822
833
  const showLinks = !collapsed || open;
823
- const innerCapStyle = React5.useMemo(
834
+ const innerCapStyle = React2.useMemo(
824
835
  () => contentMaxWidth === void 0 ? null : { maxWidth: contentMaxWidth, width: "100%", alignSelf: "center" },
825
836
  [contentMaxWidth]
826
837
  );
827
- const toggleMenu = React5.useCallback(() => setOpen((v) => !v), []);
828
- const handlePress = React5.useCallback(
838
+ const toggleMenu = React2.useCallback(() => setOpen((v) => !v), []);
839
+ const handlePress = React2.useCallback(
829
840
  (route) => {
830
841
  setOpen(false);
831
842
  onNavigate(route);
832
843
  },
833
844
  [onNavigate]
834
845
  );
835
- const linkColors = React5.useMemo(
846
+ const linkColors = React2.useMemo(
836
847
  () => ({
837
848
  rest: colors.textSecondary,
838
849
  hoverText: colors.text,
@@ -854,7 +865,7 @@ var NavBarInner = ({
854
865
  containerStyle
855
866
  ],
856
867
  children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [navBarStyles.inner, collapsed ? null : navBarStyles.innerNoWrap, innerCapStyle], children: [
857
- brand !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: navBarStyles.brand, children: brand }) : null,
868
+ brand !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: navBarStyles.brand, children: renderTextSlot(brand, { color: colors.text }) }) : null,
858
869
  collapsed ? /* @__PURE__ */ jsxRuntime.jsx(
859
870
  reactNative.Pressable,
860
871
  {
@@ -887,7 +898,7 @@ var NavBarInner = ({
887
898
  }
888
899
  );
889
900
  if (collapsed) {
890
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { nativeID: LINKS_REGION_ID, style: navBarStyles.linksStacked, testID: NAV_TEST_IDS.navBarLinks, children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(React5__default.default.Fragment, { children: renderLink(item) }, item.key)) });
901
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { nativeID: LINKS_REGION_ID, style: navBarStyles.linksStacked, testID: NAV_TEST_IDS.navBarLinks, children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(React2__default.default.Fragment, { children: renderLink(item) }, item.key)) });
891
902
  }
892
903
  const visibleItems = items.slice(0, visibleCount);
893
904
  const overflowItems = items.slice(visibleCount);
@@ -931,7 +942,7 @@ var NavBarInner = ({
931
942
  }
932
943
  );
933
944
  })() : null,
934
- showLinks && right !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsed ? navBarStyles.rightStacked : navBarStyles.right, children: right }) : null
945
+ showLinks && right !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsed ? navBarStyles.rightStacked : navBarStyles.right, children: renderTextSlot(right, { color: colors.text }) }) : null
935
946
  ] })
936
947
  }
937
948
  );
@@ -993,6 +1004,9 @@ var MENU_MIN_TARGET = 44;
993
1004
  var MENU_GLYPH_FONT_SIZE = 20;
994
1005
  var MENU_TOGGLE_PADDING = 12;
995
1006
  var DRAWER_Z_INDEX = 50;
1007
+ var DRAWER_WIDTH = 280;
1008
+ var SCRIM_Z_INDEX = 1;
1009
+ var PANEL_Z_INDEX = 2;
996
1010
  var NAV_ACTIVATABLE_SELECTOR = 'a, [role="button"], [role="link"]';
997
1011
  var DEFAULT_DRAWER_LABELS = {
998
1012
  openLabel: "Menu",
@@ -1002,8 +1016,22 @@ var DEFAULT_DRAWER_LABELS = {
1002
1016
  };
1003
1017
  var styles2 = reactNative.StyleSheet.create({
1004
1018
  overlay: { ...reactNative.StyleSheet.absoluteFillObject, zIndex: DRAWER_Z_INDEX },
1005
- scrim: { ...reactNative.StyleSheet.absoluteFillObject, backgroundColor: SCRIM_COLOR },
1006
- panel: { position: "absolute", top: 0, bottom: 0, left: 0 },
1019
+ // The scrim's tap-to-close hit area is anchored to the RIGHT of the panel
1020
+ // (`left: DRAWER_WIDTH`), so it NEVER overlaps the panel: nav-item taps always
1021
+ // reach the Sidebar's TouchableOpacity, while taps beside the drawer still close.
1022
+ scrim: {
1023
+ position: "absolute",
1024
+ top: 0,
1025
+ bottom: 0,
1026
+ left: DRAWER_WIDTH,
1027
+ right: 0,
1028
+ backgroundColor: SCRIM_COLOR,
1029
+ zIndex: SCRIM_Z_INDEX
1030
+ },
1031
+ // The panel gets an explicit width (a real drawer) AND a zIndex above the scrim,
1032
+ // so on react-native-web it — and the nav leaves inside it — sit above the scrim
1033
+ // and receive presses instead of the scrim swallowing them.
1034
+ panel: { position: "absolute", top: 0, bottom: 0, left: 0, width: DRAWER_WIDTH, zIndex: PANEL_Z_INDEX },
1007
1035
  menuToggle: {
1008
1036
  minWidth: MENU_MIN_TARGET,
1009
1037
  minHeight: MENU_MIN_TARGET,
@@ -1035,8 +1063,9 @@ var MobileDrawer = ({
1035
1063
  drawerTestID,
1036
1064
  scrimTestID
1037
1065
  }) => {
1038
- const panelRef = React5.useRef(null);
1039
- React5.useEffect(() => {
1066
+ const { theme } = uiFeedback.useUi();
1067
+ const panelRef = React2.useRef(null);
1068
+ React2.useEffect(() => {
1040
1069
  const node = panelRef.current;
1041
1070
  if (node === null || typeof node.addEventListener !== "function") return void 0;
1042
1071
  const handleClick = (event) => {
@@ -1059,7 +1088,17 @@ var MobileDrawer = ({
1059
1088
  onPress: onClose
1060
1089
  }
1061
1090
  ),
1062
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ref: panelRef, "aria-modal": true, role: "dialog", style: styles2.panel, testID: drawerTestID, children: sidebar })
1091
+ /* @__PURE__ */ jsxRuntime.jsx(
1092
+ reactNative.View,
1093
+ {
1094
+ ref: panelRef,
1095
+ "aria-modal": true,
1096
+ role: "dialog",
1097
+ style: [styles2.panel, { backgroundColor: theme.colors.surface }],
1098
+ testID: drawerTestID,
1099
+ children: renderTextSlot(sidebar, { color: theme.colors.text })
1100
+ }
1101
+ )
1063
1102
  ] });
1064
1103
  };
1065
1104
 
@@ -1131,15 +1170,15 @@ var AppShell = ({
1131
1170
  range: railRange
1132
1171
  });
1133
1172
  const useDrawer = railMode === "drawer";
1134
- const [drawerOpen, setDrawerOpen] = React5.useState(false);
1135
- const openDrawer = React5.useCallback(() => setDrawerOpen(true), []);
1136
- const closeDrawer = React5.useCallback(() => setDrawerOpen(false), []);
1137
- React5.useEffect(() => {
1173
+ const [drawerOpen, setDrawerOpen] = React2.useState(false);
1174
+ const openDrawer = React2.useCallback(() => setDrawerOpen(true), []);
1175
+ const closeDrawer = React2.useCallback(() => setDrawerOpen(false), []);
1176
+ React2.useEffect(() => {
1138
1177
  if (!useDrawer && drawerOpen) setDrawerOpen(false);
1139
1178
  }, [useDrawer, drawerOpen]);
1140
1179
  const drawerLabels = { ...DEFAULT_DRAWER_LABELS, ...mobileMenu };
1141
1180
  const isUnauthenticated = gate !== void 0 && !gate.pending && !gate.authenticated;
1142
- React5.useEffect(() => {
1181
+ React2.useEffect(() => {
1143
1182
  if (isUnauthenticated && gate !== void 0) gate.onRedirect();
1144
1183
  }, [isUnauthenticated, gate]);
1145
1184
  if (gate?.pending === true)
@@ -1166,13 +1205,13 @@ var AppShell = ({
1166
1205
  onOpen: openDrawer
1167
1206
  }
1168
1207
  ),
1169
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.headerFill, children: header })
1170
- ] }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: header });
1171
- 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;
1208
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.headerFill, children: renderTextSlot(header, { color: theme.colors.text }) })
1209
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: renderTextSlot(header, { color: theme.colors.text }) });
1210
+ const railRegion = railMode === "full" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.sidebar}`, children: renderTextSlot(sidebar, { color: theme.colors.text }) }) : railMode === "collapsed" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.collapsedSidebar}`, children: renderTextSlot(collapsedSidebar, { color: theme.colors.text }) }) : null;
1172
1211
  return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles3.root, { backgroundColor: theme.colors.background }], testID, children: [
1173
1212
  headerRegion,
1174
- 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,
1175
- banner !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: banner }) : null,
1213
+ 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: renderTextSlot(nav, { color: theme.colors.text }) }) : renderTextSlot(nav, { color: theme.colors.text }) }) : null,
1214
+ banner !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: renderTextSlot(banner, { color: theme.colors.text }) }) : null,
1176
1215
  railRegion !== null ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.bodyRow, children: [
1177
1216
  railRegion,
1178
1217
  scroller
@@ -1218,7 +1257,7 @@ var CollapsedRail = ({
1218
1257
  containerStyle
1219
1258
  ],
1220
1259
  children: [
1221
- header !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.slot, children: header }) : null,
1260
+ header !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.slot, children: renderTextSlot(header, { color: colors.text }) }) : null,
1222
1261
  items.map((item) => {
1223
1262
  const active = isRouteActive(pathname, item.route);
1224
1263
  const iconColor = active ? primaryColor : colors.textSecondary;
@@ -1237,7 +1276,7 @@ var CollapsedRail = ({
1237
1276
  );
1238
1277
  }),
1239
1278
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.spacer }),
1240
- footer !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.slot, children: footer }) : null
1279
+ footer !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.slot, children: renderTextSlot(footer, { color: colors.text }) }) : null
1241
1280
  ]
1242
1281
  }
1243
1282
  );
@@ -1391,7 +1430,7 @@ var DarkModeControl = ({
1391
1430
  const primaryColor = theme.palette.primary["500"];
1392
1431
  const currentIndex = options.findIndex((option) => option.value === value);
1393
1432
  const current = currentIndex >= 0 ? options[currentIndex] : options[0];
1394
- const advance = React5.useCallback(() => {
1433
+ const advance = React2.useCallback(() => {
1395
1434
  if (options.length === 0) return;
1396
1435
  const from = currentIndex >= 0 ? currentIndex : 0;
1397
1436
  const next = options[(from + 1) % options.length];