@dloizides/ui-nav 1.9.0 → 1.10.1

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,62 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.10.1
4
+
5
+ - **Fix (a plain-string slot logged `console.error` on EVERY render, in all seven portals).**
6
+ Every caller-supplied slot in the kit is typed `React.ReactNode`, which legitimately includes
7
+ `string` — and callers pass exactly that (`brand: FM('app.name')`, a plain localized string).
8
+ The slot was dropped straight into a `<View>`, and react-native-web refuses to render a bare
9
+ text node there: `Unexpected text node: <s>. A text node cannot be a child of a <View>.`
10
+ Nothing in the type system objected, so the defect only ever surfaced as browser console
11
+ noise — which is why it survived to 1.10.0 across the whole fleet.
12
+ - Fixed once, in the kit, via the new internal `renderTextSlot` helper: a `string`/`number`
13
+ slot is wrapped in `<Text>` (routed through the theme's `colors.text`, so it honours dark
14
+ mode rather than defaulting to black); an element slot passes through **untouched**, so
15
+ every existing caller renders byte-identically. Array slots have each string member wrapped
16
+ individually, keys preserved.
17
+ - Applied to **all 12 slot render sites**, not just the reported one — `NavBar` (`brand`,
18
+ `right`), `Topbar` (`left`, `notificationSlot`), `Sidebar` (`header`, `footer`),
19
+ `CollapsedRail` (`header`, `footer`), `AppShell` (`header`, `nav`, `sidebar`,
20
+ `collapsedSidebar`, `banner`) and `MobileDrawer` (`sidebar`).
21
+ - Guarded by `src/rawTextSlots.test.tsx`, which spies on `console.error` and asserts silence.
22
+ Unit tests that only assert "the slot rendered" pass against the bug; this one does not.
23
+ - **No API change.** Consumers need no edit — the `<Text>` wrapper they were expected to
24
+ remember is now the kit's job.
25
+
26
+ ## 1.10.0
27
+
28
+ - **Fix (`layout` was not authoritative — `layout="side"` rendered a top bar anyway).**
29
+ `wantsTopBar` carried an extra `(layout === 'side' && hasTopBar)` clause, so passing a
30
+ `topBar` **config** with `layout="side"` mounted the bar regardless — making `'side'` and
31
+ `'both'` render **pixel-identically** and `layout` unobservable. `layout` now decides which
32
+ REGIONS exist; `topBar` is only the CONTENT of the top region. A `'side'` app that also wants
33
+ a bar asks for `layout="both"`, which is what that combination always meant.
34
+ - Found by `apps/ui-showcase`, which renders every orientation side by side — a product app
35
+ picks ONE orientation and never sees the other, which is why this survived to 1.9.0.
36
+ - **Not breaking in this fleet:** no app passed `layout="side"` + `topBar`. The four `'side'`
37
+ portals (erevna, katalogos, zygos, agora) pass a `header` NODE, not a `topBar` config.
38
+ - The `hasHeader` clause is **deliberately kept**: `header` is the documented escape hatch by
39
+ which a `'side'` app supplies its own ready-made header (the structured `Topbar` with
40
+ language / notification / user slots). It is an explicit "render THIS node", not a config
41
+ whose presence is silently read as an orientation change.
42
+ - **a11y: `AccountState.accountHint`** — the account-name button's `accessibilityHint` was
43
+ hardcoded to `displayName`, byte-identical to its `accessibilityLabel`, so a screen reader
44
+ announced the name twice and never said what pressing it does — and the consumer had no way
45
+ to correct it. The sibling `upgrade?: TopbarAction` on the same interface has always carried
46
+ a `hint`; this closes that inconsistency. Falls back to `displayName`, so existing callers
47
+ are unaffected.
48
+ - **a11y: `AccountState.accountTestID`** — the account-name button's testID was hardcoded, with
49
+ no override, unlike `TopbarAction.testID?` / `NavItem.testID?` elsewhere in the package.
50
+ - **a11y: `Topbar.language.testID`** — the inline `language` prop type declared no `testID`, so
51
+ the repo's testID+label+hint standard was **structurally unsatisfiable** for that one button
52
+ while every other Topbar action (all `TopbarAction`) accepted one.
53
+
54
+ > Note for consumers: this react-native-web build does **not** forward `accessibilityHint` to
55
+ > the DOM at all — rendered buttons carry `aria-label` but no hint attribute. The hint is a
56
+ > NATIVE-only guarantee today. That is pre-existing and unchanged here, but it means DOM-level
57
+ > tests cannot distinguish a threaded hint from a dropped one; `topbarA11y.test.tsx` therefore
58
+ > pins the prop threading directly.
59
+
3
60
  ## 1.9.0
4
61
 
5
62
  **`NavBar.contentMaxWidth` — a full-bleed bar with a centred content column.**
package/dist/index.d.mts CHANGED
@@ -120,16 +120,34 @@ interface AccountState {
120
120
  plan?: AccountPlan;
121
121
  onLogout: () => void;
122
122
  onAccount?: () => void;
123
+ /**
124
+ * a11y hint for the account-name button (what pressing it DOES).
125
+ *
126
+ * Without this the hint fell back to `displayName` — byte-identical to the
127
+ * accessibility LABEL, so a screen reader announced the name twice and never
128
+ * said where the button goes. The sibling `upgrade?: TopbarAction` on this very
129
+ * interface has always carried a `hint`; this closes that inconsistency. The
130
+ * `displayName` fallback is retained so existing callers are unaffected.
131
+ */
132
+ accountHint?: string;
133
+ /** testID for the account-name button. Defaults to `NAV_TEST_IDS.accountName`. */
134
+ accountTestID?: string;
123
135
  upgrade?: TopbarAction;
124
136
  }
125
137
  interface TopbarProps {
126
138
  /** Left slot — typically the tenant logo. */
127
139
  left?: React.ReactNode;
128
- /** Optional language toggle. */
140
+ /**
141
+ * Optional language toggle. `testID` is optional but the repo standard wants
142
+ * one — before 1.10.0 this inline type declared none, so a testID was
143
+ * structurally unreachable for this button while every other Topbar action
144
+ * (which use `TopbarAction`) accepted one.
145
+ */
129
146
  language?: {
130
147
  label: string;
131
148
  hint: string;
132
149
  onPress: () => void;
150
+ testID?: string;
133
151
  };
134
152
  /** Optional notification slot (e.g. a notification bell). */
135
153
  notificationSlot?: React.ReactNode;
@@ -591,8 +609,9 @@ interface NavShellProps extends ForwardedShellProps {
591
609
  navigateHint?: (label: string) => string;
592
610
  /**
593
611
  * Top-bar config → a `NavBar` in the header slot. Required for `'top'` /
594
- * `'both'`; ignored for `'side'` unless supplied (a `'side'` app may still pass
595
- * a `topBar` with empty `items` for its brand + user header).
612
+ * `'both'`; **ignored for `'side'`** `layout` decides which regions exist, so
613
+ * a `'side'` app that also wants a bar asks for `layout="both"`. A `'side'` app
614
+ * that wants its OWN header node uses the `header` escape hatch instead.
596
615
  */
597
616
  topBar?: NavShellTopBar;
598
617
  /**
package/dist/index.d.ts CHANGED
@@ -120,16 +120,34 @@ interface AccountState {
120
120
  plan?: AccountPlan;
121
121
  onLogout: () => void;
122
122
  onAccount?: () => void;
123
+ /**
124
+ * a11y hint for the account-name button (what pressing it DOES).
125
+ *
126
+ * Without this the hint fell back to `displayName` — byte-identical to the
127
+ * accessibility LABEL, so a screen reader announced the name twice and never
128
+ * said where the button goes. The sibling `upgrade?: TopbarAction` on this very
129
+ * interface has always carried a `hint`; this closes that inconsistency. The
130
+ * `displayName` fallback is retained so existing callers are unaffected.
131
+ */
132
+ accountHint?: string;
133
+ /** testID for the account-name button. Defaults to `NAV_TEST_IDS.accountName`. */
134
+ accountTestID?: string;
123
135
  upgrade?: TopbarAction;
124
136
  }
125
137
  interface TopbarProps {
126
138
  /** Left slot — typically the tenant logo. */
127
139
  left?: React.ReactNode;
128
- /** Optional language toggle. */
140
+ /**
141
+ * Optional language toggle. `testID` is optional but the repo standard wants
142
+ * one — before 1.10.0 this inline type declared none, so a testID was
143
+ * structurally unreachable for this button while every other Topbar action
144
+ * (which use `TopbarAction`) accepted one.
145
+ */
129
146
  language?: {
130
147
  label: string;
131
148
  hint: string;
132
149
  onPress: () => void;
150
+ testID?: string;
133
151
  };
134
152
  /** Optional notification slot (e.g. a notification bell). */
135
153
  notificationSlot?: React.ReactNode;
@@ -591,8 +609,9 @@ interface NavShellProps extends ForwardedShellProps {
591
609
  navigateHint?: (label: string) => string;
592
610
  /**
593
611
  * Top-bar config → a `NavBar` in the header slot. Required for `'top'` /
594
- * `'both'`; ignored for `'side'` unless supplied (a `'side'` app may still pass
595
- * a `topBar` with empty `items` for its brand + user header).
612
+ * `'both'`; **ignored for `'side'`** `layout` decides which regions exist, so
613
+ * a `'side'` app that also wants a bar asks for `layout="both"`. A `'side'` app
614
+ * that wants its OWN header node uses the `header` escape hatch instead.
596
615
  */
597
616
  topBar?: NavShellTopBar;
598
617
  /**
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,
@@ -540,11 +551,12 @@ var Topbar = ({
540
551
  accessibilityLabel: language.label,
541
552
  ringColor: primaryColor,
542
553
  style: navStyles.topbarRowItem,
554
+ testID: language.testID,
543
555
  onPress: language.onPress,
544
556
  children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.topbarLabel, { color: colors.text }], children: language.label })
545
557
  }
546
558
  ) : null,
547
- notificationSlot,
559
+ renderTextSlot(notificationSlot, { color: colors.text }),
548
560
  user ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: navStyles.userBlock, children: [
549
561
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.userName, { color: colors.text }], children: user.name }),
550
562
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.userEmail, { color: colors.textSecondary }], children: user.email })
@@ -553,10 +565,10 @@ var Topbar = ({
553
565
  richAccount.onAccount ? /* @__PURE__ */ jsxRuntime.jsx(
554
566
  FocusableTouchable,
555
567
  {
556
- accessibilityHint: richAccount.displayName,
568
+ accessibilityHint: richAccount.accountHint ?? richAccount.displayName,
557
569
  accessibilityLabel: richAccount.displayName,
558
570
  ringColor: primaryColor,
559
- testID: NAV_TEST_IDS.accountName,
571
+ testID: richAccount.accountTestID ?? NAV_TEST_IDS.accountName,
560
572
  onPress: richAccount.onAccount,
561
573
  children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.userName, { color: colors.text }], children: richAccount.displayName })
562
574
  }
@@ -633,8 +645,8 @@ var NavBarLink = ({
633
645
  navigateHint,
634
646
  onPress
635
647
  }) => {
636
- const [hovered, setHovered] = React5.useState(false);
637
- const [focused, setFocused] = React5.useState(false);
648
+ const [hovered, setHovered] = React2.useState(false);
649
+ const [focused, setFocused] = React2.useState(false);
638
650
  const isHovered = hovered && !isActive;
639
651
  const textColor = isActive ? TEXT_ON_PRIMARY2 : isHovered ? colors.hoverText : colors.rest;
640
652
  const pillStyle = isActive ? { backgroundColor: colors.activeBg } : isHovered ? { backgroundColor: colors.hoverBg } : void 0;
@@ -678,12 +690,12 @@ var NavOverflowMenu = ({
678
690
  testID,
679
691
  variant
680
692
  }) => {
681
- const options = React5.useMemo(() => items.map((item) => ({ label: item.label, value: item.route })), [items]);
682
- const activeRoute = React5.useMemo(
693
+ const options = React2.useMemo(() => items.map((item) => ({ label: item.label, value: item.route })), [items]);
694
+ const activeRoute = React2.useMemo(
683
695
  () => items.find((item) => isRouteActive(pathname, item.route))?.route ?? NO_ACTIVE_ROUTE,
684
696
  [items, pathname]
685
697
  );
686
- const optionTestID = React5.useMemo(() => {
698
+ const optionTestID = React2.useMemo(() => {
687
699
  const byRoute = new Map(items.map((item) => [item.route, item.testID ?? item.key]));
688
700
  return (route) => byRoute.get(route) ?? `${testID}-option-${route}`;
689
701
  }, [items, testID]);
@@ -740,13 +752,13 @@ function resizeWidthBuffer(previous, count) {
740
752
  return next;
741
753
  }
742
754
  function useNavOverflow(itemCount, gap) {
743
- const [availableWidth, setAvailableWidthState] = React5.useState(0);
744
- const [moreWidth, setMoreWidthState] = React5.useState(0);
745
- const [itemWidths, setItemWidths] = React5.useState(() => new Array(itemCount).fill(0));
746
- React5.useEffect(() => {
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(() => {
747
759
  setItemWidths((previous) => previous.length === itemCount ? previous : resizeWidthBuffer(previous, itemCount));
748
760
  }, [itemCount]);
749
- const setItemWidth = React5.useCallback((index, width) => {
761
+ const setItemWidth = React2.useCallback((index, width) => {
750
762
  setItemWidths((previous) => {
751
763
  if (index < 0 || index >= previous.length || previous[index] === width) return previous;
752
764
  const next = previous.slice();
@@ -754,15 +766,15 @@ function useNavOverflow(itemCount, gap) {
754
766
  return next;
755
767
  });
756
768
  }, []);
757
- const setAvailableWidth = React5.useCallback(
769
+ const setAvailableWidth = React2.useCallback(
758
770
  (width) => setAvailableWidthState((previous) => previous === width ? previous : width),
759
771
  []
760
772
  );
761
- const setMoreWidth = React5.useCallback(
773
+ const setMoreWidth = React2.useCallback(
762
774
  (width) => setMoreWidthState((previous) => previous === width ? previous : width),
763
775
  []
764
776
  );
765
- const visibleCount = React5.useMemo(
777
+ const visibleCount = React2.useMemo(
766
778
  () => computeVisibleCount({ availableWidth, itemWidths, moreWidth, gap }),
767
779
  [availableWidth, itemWidths, moreWidth, gap]
768
780
  );
@@ -774,8 +786,8 @@ function getReducedMotionQuery() {
774
786
  return window.matchMedia(REDUCED_MOTION_QUERY);
775
787
  }
776
788
  function useReducedMotion() {
777
- const [reduced, setReduced] = React5.useState(() => getReducedMotionQuery()?.matches ?? false);
778
- React5.useEffect(() => {
789
+ const [reduced, setReduced] = React2.useState(() => getReducedMotionQuery()?.matches ?? false);
790
+ React2.useEffect(() => {
779
791
  const mql = getReducedMotionQuery();
780
792
  if (mql === void 0 || mql.addEventListener === void 0) return void 0;
781
793
  const onChange = () => setReduced(mql.matches);
@@ -814,24 +826,24 @@ var NavBarInner = ({
814
826
  const primaryColor = theme.palette.primary["500"];
815
827
  const { width } = reactNative.useWindowDimensions();
816
828
  const reducedMotion = useReducedMotion();
817
- const [open, setOpen] = React5.useState(false);
818
- const [toggleFocused, setToggleFocused] = React5.useState(false);
829
+ const [open, setOpen] = React2.useState(false);
830
+ const [toggleFocused, setToggleFocused] = React2.useState(false);
819
831
  const { visibleCount, setAvailableWidth, setItemWidth, setMoreWidth } = useNavOverflow(items.length, NAV_LINK_GAP);
820
832
  const collapsed = width < collapseBelow;
821
833
  const showLinks = !collapsed || open;
822
- const innerCapStyle = React5.useMemo(
834
+ const innerCapStyle = React2.useMemo(
823
835
  () => contentMaxWidth === void 0 ? null : { maxWidth: contentMaxWidth, width: "100%", alignSelf: "center" },
824
836
  [contentMaxWidth]
825
837
  );
826
- const toggleMenu = React5.useCallback(() => setOpen((v) => !v), []);
827
- const handlePress = React5.useCallback(
838
+ const toggleMenu = React2.useCallback(() => setOpen((v) => !v), []);
839
+ const handlePress = React2.useCallback(
828
840
  (route) => {
829
841
  setOpen(false);
830
842
  onNavigate(route);
831
843
  },
832
844
  [onNavigate]
833
845
  );
834
- const linkColors = React5.useMemo(
846
+ const linkColors = React2.useMemo(
835
847
  () => ({
836
848
  rest: colors.textSecondary,
837
849
  hoverText: colors.text,
@@ -853,7 +865,7 @@ var NavBarInner = ({
853
865
  containerStyle
854
866
  ],
855
867
  children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [navBarStyles.inner, collapsed ? null : navBarStyles.innerNoWrap, innerCapStyle], children: [
856
- 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,
857
869
  collapsed ? /* @__PURE__ */ jsxRuntime.jsx(
858
870
  reactNative.Pressable,
859
871
  {
@@ -886,7 +898,7 @@ var NavBarInner = ({
886
898
  }
887
899
  );
888
900
  if (collapsed) {
889
- 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)) });
890
902
  }
891
903
  const visibleItems = items.slice(0, visibleCount);
892
904
  const overflowItems = items.slice(visibleCount);
@@ -930,7 +942,7 @@ var NavBarInner = ({
930
942
  }
931
943
  );
932
944
  })() : null,
933
- 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
934
946
  ] })
935
947
  }
936
948
  );
@@ -1034,8 +1046,9 @@ var MobileDrawer = ({
1034
1046
  drawerTestID,
1035
1047
  scrimTestID
1036
1048
  }) => {
1037
- const panelRef = React5.useRef(null);
1038
- React5.useEffect(() => {
1049
+ const { theme } = uiFeedback.useUi();
1050
+ const panelRef = React2.useRef(null);
1051
+ React2.useEffect(() => {
1039
1052
  const node = panelRef.current;
1040
1053
  if (node === null || typeof node.addEventListener !== "function") return void 0;
1041
1054
  const handleClick = (event) => {
@@ -1058,7 +1071,7 @@ var MobileDrawer = ({
1058
1071
  onPress: onClose
1059
1072
  }
1060
1073
  ),
1061
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ref: panelRef, "aria-modal": true, role: "dialog", style: styles2.panel, testID: drawerTestID, children: sidebar })
1074
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ref: panelRef, "aria-modal": true, role: "dialog", style: styles2.panel, testID: drawerTestID, children: renderTextSlot(sidebar, { color: theme.colors.text }) })
1062
1075
  ] });
1063
1076
  };
1064
1077
 
@@ -1130,15 +1143,15 @@ var AppShell = ({
1130
1143
  range: railRange
1131
1144
  });
1132
1145
  const useDrawer = railMode === "drawer";
1133
- const [drawerOpen, setDrawerOpen] = React5.useState(false);
1134
- const openDrawer = React5.useCallback(() => setDrawerOpen(true), []);
1135
- const closeDrawer = React5.useCallback(() => setDrawerOpen(false), []);
1136
- React5.useEffect(() => {
1146
+ const [drawerOpen, setDrawerOpen] = React2.useState(false);
1147
+ const openDrawer = React2.useCallback(() => setDrawerOpen(true), []);
1148
+ const closeDrawer = React2.useCallback(() => setDrawerOpen(false), []);
1149
+ React2.useEffect(() => {
1137
1150
  if (!useDrawer && drawerOpen) setDrawerOpen(false);
1138
1151
  }, [useDrawer, drawerOpen]);
1139
1152
  const drawerLabels = { ...DEFAULT_DRAWER_LABELS, ...mobileMenu };
1140
1153
  const isUnauthenticated = gate !== void 0 && !gate.pending && !gate.authenticated;
1141
- React5.useEffect(() => {
1154
+ React2.useEffect(() => {
1142
1155
  if (isUnauthenticated && gate !== void 0) gate.onRedirect();
1143
1156
  }, [isUnauthenticated, gate]);
1144
1157
  if (gate?.pending === true)
@@ -1165,13 +1178,13 @@ var AppShell = ({
1165
1178
  onOpen: openDrawer
1166
1179
  }
1167
1180
  ),
1168
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.headerFill, children: header })
1169
- ] }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: header });
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;
1181
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.headerFill, children: renderTextSlot(header, { color: theme.colors.text }) })
1182
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: renderTextSlot(header, { color: theme.colors.text }) });
1183
+ 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;
1171
1184
  return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles3.root, { backgroundColor: theme.colors.background }], testID, children: [
1172
1185
  headerRegion,
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,
1174
- banner !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: banner }) : null,
1186
+ 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,
1187
+ banner !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: renderTextSlot(banner, { color: theme.colors.text }) }) : null,
1175
1188
  railRegion !== null ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.bodyRow, children: [
1176
1189
  railRegion,
1177
1190
  scroller
@@ -1217,7 +1230,7 @@ var CollapsedRail = ({
1217
1230
  containerStyle
1218
1231
  ],
1219
1232
  children: [
1220
- header !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.slot, children: header }) : null,
1233
+ header !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.slot, children: renderTextSlot(header, { color: colors.text }) }) : null,
1221
1234
  items.map((item) => {
1222
1235
  const active = isRouteActive(pathname, item.route);
1223
1236
  const iconColor = active ? primaryColor : colors.textSecondary;
@@ -1236,13 +1249,13 @@ var CollapsedRail = ({
1236
1249
  );
1237
1250
  }),
1238
1251
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.spacer }),
1239
- footer !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.slot, children: footer }) : null
1252
+ footer !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.slot, children: renderTextSlot(footer, { color: colors.text }) }) : null
1240
1253
  ]
1241
1254
  }
1242
1255
  );
1243
1256
  };
1244
- function wantsTopBar(layout, hasHeader, hasTopBar) {
1245
- return layout === "top" || layout === "both" || hasHeader || layout === "side" && hasTopBar;
1257
+ function wantsTopBar(layout, hasHeader) {
1258
+ return layout === "top" || layout === "both" || hasHeader;
1246
1259
  }
1247
1260
  function wantsSideRail(layout, hasSideRail) {
1248
1261
  return (layout === "side" || layout === "both") && hasSideRail;
@@ -1261,7 +1274,7 @@ var NavShell = ({
1261
1274
  collapsedRailRange: collapsedRailRangeProp,
1262
1275
  ...shellProps
1263
1276
  }) => {
1264
- const showTopBar = wantsTopBar(layout, header !== void 0, topBar !== void 0);
1277
+ const showTopBar = wantsTopBar(layout, header !== void 0);
1265
1278
  const showSideRail = wantsSideRail(layout, sideRail !== void 0);
1266
1279
  const headerNode = header !== void 0 ? header : showTopBar && topBar !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1267
1280
  NavBar,
@@ -1390,7 +1403,7 @@ var DarkModeControl = ({
1390
1403
  const primaryColor = theme.palette.primary["500"];
1391
1404
  const currentIndex = options.findIndex((option) => option.value === value);
1392
1405
  const current = currentIndex >= 0 ? options[currentIndex] : options[0];
1393
- const advance = React5.useCallback(() => {
1406
+ const advance = React2.useCallback(() => {
1394
1407
  if (options.length === 0) return;
1395
1408
  const from = currentIndex >= 0 ? currentIndex : 0;
1396
1409
  const next = options[(from + 1) % options.length];