@dloizides/ui-nav 1.34.0 → 1.36.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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.35.0
4
+
5
+ - NAV-SCROLL-1 "Opening sidebar groups never hides the items below" (AC-UI-1, AC-UI-2): `Sidebar` items (header slot + list) scroll in a `ScrollView` between the pinned title/search and the pinned footer, with a `theme.colors.surface` fade on each edge that hides items. Inside `MobileDrawer` opening a top-level group closes the other (through `NavAccordionContext`, which `MobileDrawer` provides; no new `Sidebar` prop). `NavExpandableItem` takes optional controlled `expanded` + `onToggle`. No prop changes for existing consumers.
6
+
3
7
  ## 1.28.0
4
8
 
5
9
  - INVOICE-5 "Edit invoices before sending, shared background and menu hover" (AC-UI-3, AC-UI-6): `AppShell` draws `theme.backdrop` (the `@dloizides/theme-web` 1.5.0 `resolveBackdrop` shape, typed here as `AppShellBackdrop`): its colour and web image on the root, and for `horizon` a brand band behind header and nav that the content overlaps by `overlap.compact` (< 768 px) or `overlap.wide`. No `theme.backdrop` keeps today's flat background.
package/dist/index.d.mts CHANGED
@@ -1209,8 +1209,10 @@ interface NavExpandableItemProps {
1209
1209
  */
1210
1210
  renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
1211
1211
  depth?: number;
1212
+ expanded?: boolean;
1213
+ onToggle?: (key: string) => void;
1212
1214
  }
1213
- declare const NavExpandableItem: ({ item, pathname, onNavigate, navigateHint, expandHint, collapseHint, renderChevron, activeRoute, depth, }: NavExpandableItemProps) => React.ReactElement;
1215
+ declare const NavExpandableItem: ({ item, pathname, onNavigate, navigateHint, expandHint, collapseHint, renderChevron, activeRoute, depth, expanded: expandedProp, onToggle, }: NavExpandableItemProps) => React.ReactElement;
1214
1216
 
1215
1217
  /**
1216
1218
  * Pure matching logic for the {@link CommandPalette}. Kept framework-free so the
package/dist/index.d.ts CHANGED
@@ -1209,8 +1209,10 @@ interface NavExpandableItemProps {
1209
1209
  */
1210
1210
  renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
1211
1211
  depth?: number;
1212
+ expanded?: boolean;
1213
+ onToggle?: (key: string) => void;
1212
1214
  }
1213
- declare const NavExpandableItem: ({ item, pathname, onNavigate, navigateHint, expandHint, collapseHint, renderChevron, activeRoute, depth, }: NavExpandableItemProps) => React.ReactElement;
1215
+ declare const NavExpandableItem: ({ item, pathname, onNavigate, navigateHint, expandHint, collapseHint, renderChevron, activeRoute, depth, expanded: expandedProp, onToggle, }: NavExpandableItemProps) => React.ReactElement;
1214
1216
 
1215
1217
  /**
1216
1218
  * Pure matching logic for the {@link CommandPalette}. Kept framework-free so the
package/dist/index.js CHANGED
@@ -486,6 +486,18 @@ function resolveActiveRoute(items, pathname) {
486
486
  }
487
487
  return winner;
488
488
  }
489
+ var NavAccordionContext = React.createContext(false);
490
+ function hasActiveDescendant(item, pathname) {
491
+ if (isRouteActive(pathname, item.route)) return true;
492
+ return (item.children ?? []).some((child) => hasActiveDescendant(child, pathname));
493
+ }
494
+ function initialOpenGroup(items, pathname) {
495
+ const hasChildren = (item) => (item.children ?? []).length > 0;
496
+ return items.find((item) => hasChildren(item) && hasActiveDescendant(item, pathname))?.key;
497
+ }
498
+ function nextOpenGroup(current, pressed) {
499
+ return current === pressed ? void 0 : pressed;
500
+ }
489
501
  var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
490
502
  function getReducedMotionQuery() {
491
503
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return void 0;
@@ -535,10 +547,6 @@ function ariaExpandedProps(expanded) {
535
547
  function hoverHandlerProps(onIn, onOut) {
536
548
  return IS_WEB ? { onHoverIn: onIn, onHoverOut: onOut } : {};
537
549
  }
538
- function hasActiveDescendant(item, pathname) {
539
- if (isRouteActive(pathname, item.route)) return true;
540
- return (item.children ?? []).some((child) => hasActiveDescendant(child, pathname));
541
- }
542
550
  function TintOverlay({
543
551
  opacity,
544
552
  color,
@@ -562,15 +570,21 @@ var NavExpandableItem = ({
562
570
  collapseHint,
563
571
  renderChevron,
564
572
  activeRoute,
565
- depth = 0
573
+ depth = 0,
574
+ expanded: expandedProp,
575
+ onToggle
566
576
  }) => {
567
- const [expanded, setExpanded] = React.useState(() => hasActiveDescendant(item, pathname));
577
+ const [internalExpanded, setExpanded] = React.useState(() => hasActiveDescendant(item, pathname));
578
+ const expanded = expandedProp ?? internalExpanded;
568
579
  const [focused, setFocused] = React.useState(false);
569
580
  const [hovered, setHovered] = React.useState(false);
570
581
  const { theme } = uiFeedback.useUi();
571
582
  const colors = theme.colors;
572
583
  const primaryColor = theme.palette.primary["500"];
573
- const toggle = React.useCallback(() => setExpanded((v) => !v), []);
584
+ const toggle = React.useCallback(() => {
585
+ if (onToggle !== void 0) onToggle(item.key);
586
+ else setExpanded((v) => !v);
587
+ }, [onToggle, item.key]);
574
588
  const onHoverIn = React.useCallback(() => setHovered(true), []);
575
589
  const onHoverOut = React.useCallback(() => setHovered(false), []);
576
590
  const indent = depth * BASE_INDENT;
@@ -681,9 +695,72 @@ var DEFAULT_SEARCH_BREAKPOINT = uiLayout.LAYOUT_COLLAPSE_BREAKPOINT;
681
695
  function resolveSearchAffordance(viewport, breakpoint = DEFAULT_SEARCH_BREAKPOINT) {
682
696
  return viewport >= breakpoint ? "inline" : "palette";
683
697
  }
698
+
699
+ // src/sidebarEdgeFade.ts
700
+ var EDGE_TOLERANCE = 1;
701
+ function resolveEdgeFades({ offsetY, viewportHeight, contentHeight }) {
702
+ const hiddenBelow = contentHeight - (offsetY + viewportHeight);
703
+ return { top: offsetY > EDGE_TOLERANCE, bottom: hiddenBelow > EDGE_TOLERANCE };
704
+ }
705
+ var FADE_BAND_HEIGHT = 6;
706
+ var FADE_BAND_OPACITIES = [0.9, 0.65, 0.4, 0.15];
707
+ var SCROLL_EVENT_THROTTLE_MS = 16;
708
+ var styles2 = reactNative.StyleSheet.create({
709
+ area: { flex: 1, minHeight: 0 },
710
+ fade: { position: "absolute", left: 0, right: 0 },
711
+ fadeTop: { top: 0 },
712
+ fadeBottom: { bottom: 0, flexDirection: "column-reverse" },
713
+ band: { height: FADE_BAND_HEIGHT }
714
+ });
715
+ function EdgeFade({ edge, color }) {
716
+ return /* @__PURE__ */ jsxRuntime.jsx(
717
+ reactNative.View,
718
+ {
719
+ pointerEvents: "none",
720
+ style: [styles2.fade, edge === "top" ? styles2.fadeTop : styles2.fadeBottom],
721
+ testID: `sidebar-fade-${edge}`,
722
+ children: FADE_BAND_OPACITIES.map((opacity) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles2.band, { backgroundColor: color, opacity }] }, opacity))
723
+ }
724
+ );
725
+ }
726
+ var SidebarScrollArea = ({ children }) => {
727
+ const { theme } = uiFeedback.useUi();
728
+ const metrics = React.useRef({ offsetY: 0, viewportHeight: 0, contentHeight: 0 });
729
+ const [fades, setFades] = React.useState({ top: false, bottom: false });
730
+ const update = React.useCallback((next) => {
731
+ metrics.current = { ...metrics.current, ...next };
732
+ const resolved = resolveEdgeFades(metrics.current);
733
+ setFades((prev) => prev.top === resolved.top && prev.bottom === resolved.bottom ? prev : resolved);
734
+ }, []);
735
+ const onLayout = React.useCallback(
736
+ (e) => update({ viewportHeight: e.nativeEvent.layout.height }),
737
+ [update]
738
+ );
739
+ const onContentSizeChange = React.useCallback((_w, h) => update({ contentHeight: h }), [update]);
740
+ const onScroll = React.useCallback(
741
+ (e) => update({ offsetY: e.nativeEvent.contentOffset.y }),
742
+ [update]
743
+ );
744
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.area, children: [
745
+ /* @__PURE__ */ jsxRuntime.jsx(
746
+ reactNative.ScrollView,
747
+ {
748
+ scrollEventThrottle: SCROLL_EVENT_THROTTLE_MS,
749
+ style: styles2.area,
750
+ testID: "sidebar-scroll",
751
+ onContentSizeChange,
752
+ onLayout,
753
+ onScroll,
754
+ children
755
+ }
756
+ ),
757
+ fades.top ? /* @__PURE__ */ jsxRuntime.jsx(EdgeFade, { color: theme.colors.surface, edge: "top" }) : null,
758
+ fades.bottom ? /* @__PURE__ */ jsxRuntime.jsx(EdgeFade, { color: theme.colors.surface, edge: "bottom" }) : null
759
+ ] });
760
+ };
684
761
  var FIELD_MIN_HEIGHT = 38;
685
762
  var CLEAR_GLYPH = "\u2715";
686
- var styles2 = reactNative.StyleSheet.create({
763
+ var styles3 = reactNative.StyleSheet.create({
687
764
  wrap: {
688
765
  flexDirection: "row",
689
766
  alignItems: "center",
@@ -724,7 +801,7 @@ var SidebarSearch = ({
724
801
  reactNative.View,
725
802
  {
726
803
  style: [
727
- styles2.wrap,
804
+ styles3.wrap,
728
805
  { backgroundColor: colors.surface, borderColor: focused ? primary : colors.border },
729
806
  focusRingStyle(focused, primary)
730
807
  ],
@@ -736,7 +813,7 @@ var SidebarSearch = ({
736
813
  accessibilityLabel: labels.placeholder,
737
814
  placeholder: labels.placeholder,
738
815
  placeholderTextColor: colors.textSecondary,
739
- style: [styles2.input, { color: colors.text }],
816
+ style: [styles3.input, { color: colors.text }],
740
817
  testID,
741
818
  value,
742
819
  onBlur: () => setFocused(false),
@@ -750,10 +827,10 @@ var SidebarSearch = ({
750
827
  accessibilityHint: labels.clearHint,
751
828
  accessibilityLabel: labels.clearLabel,
752
829
  accessibilityRole: "button",
753
- style: styles2.clear,
830
+ style: styles3.clear,
754
831
  testID: `${testID}-clear`,
755
832
  onPress: onClear,
756
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.clearGlyph, { color: colors.textSecondary }], children: CLEAR_GLYPH })
833
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles3.clearGlyph, { color: colors.textSecondary }], children: CLEAR_GLYPH })
757
834
  }
758
835
  ) : null
759
836
  ]
@@ -812,6 +889,10 @@ var Sidebar = ({
812
889
  const visibleItems = showInlineSearch ? filterNavItems(items, query) : items;
813
890
  const showEmpty = showInlineSearch && isFilterActive(query) && visibleItems.length === 0;
814
891
  const activeRoute = resolveActiveRoute(items, pathname);
892
+ const accordion = React.useContext(NavAccordionContext);
893
+ const [openGroup, setOpenGroup] = React.useState(() => initialOpenGroup(items, pathname));
894
+ const toggleGroup = React.useCallback((key) => setOpenGroup((current) => nextOpenGroup(current, key)), []);
895
+ const accordionProps = (item) => accordion && (item.children ?? []).length > 0 ? { expanded: openGroup === item.key, onToggle: toggleGroup } : {};
815
896
  return /* @__PURE__ */ jsxRuntime.jsxs(
816
897
  reactNative.View,
817
898
  {
@@ -845,22 +926,24 @@ var Sidebar = ({
845
926
  onPress: paletteTrigger.onOpen
846
927
  }
847
928
  ) }) : null,
848
- renderTextSlot(header, { color: colors.text }),
849
- showEmpty && search !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.sidebarEmpty, { color: colors.textSecondary }], testID: "sidebar-search-empty", children: search.emptyText }) : visibleItems.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
850
- NavExpandableItem,
851
- {
852
- activeRoute,
853
- collapseHint,
854
- expandHint,
855
- item,
856
- navigateHint,
857
- pathname,
858
- renderChevron,
859
- onNavigate
860
- },
861
- item.key
862
- )),
863
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: navStyles.sidebarSpacer }),
929
+ /* @__PURE__ */ jsxRuntime.jsxs(SidebarScrollArea, { children: [
930
+ renderTextSlot(header, { color: colors.text }),
931
+ showEmpty && search !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.sidebarEmpty, { color: colors.textSecondary }], testID: "sidebar-search-empty", children: search.emptyText }) : visibleItems.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
932
+ NavExpandableItem,
933
+ {
934
+ activeRoute,
935
+ collapseHint,
936
+ expandHint,
937
+ item,
938
+ navigateHint,
939
+ pathname,
940
+ renderChevron,
941
+ onNavigate,
942
+ ...accordionProps(item)
943
+ },
944
+ item.key
945
+ ))
946
+ ] }),
864
947
  renderTextSlot(footer, { color: colors.text })
865
948
  ]
866
949
  }
@@ -1582,7 +1665,7 @@ var NavBar = ({ barTheme, ...props }) => {
1582
1665
  if (barTheme === void 0) return /* @__PURE__ */ jsxRuntime.jsx(NavBarInner, { ...props });
1583
1666
  return /* @__PURE__ */ jsxRuntime.jsx(uiFeedback.UiProvider, { navigate, t, theme: barTheme, children: /* @__PURE__ */ jsxRuntime.jsx(NavBarInner, { ...props }) });
1584
1667
  };
1585
- var styles3 = reactNative.StyleSheet.create({
1668
+ var styles4 = reactNative.StyleSheet.create({
1586
1669
  hitBox: { minHeight: MIN_TOUCH_TARGET, minWidth: MIN_TOUCH_TARGET, justifyContent: "center" }
1587
1670
  });
1588
1671
  function anchorProps(route) {
@@ -1605,7 +1688,7 @@ var BrandHomeLink = ({ route, label, onNavigate, children }) => {
1605
1688
  {
1606
1689
  accessibilityLabel: label,
1607
1690
  accessibilityRole: "link",
1608
- style: [styles3.hitBox, focusRingStyle(focused, theme.palette.primary["500"])],
1691
+ style: [styles4.hitBox, focusRingStyle(focused, theme.palette.primary["500"])],
1609
1692
  testID: NAV_TEST_IDS.brandHome,
1610
1693
  onBlur: () => setFocused(false),
1611
1694
  onFocus: () => setFocused(true),
@@ -1618,7 +1701,7 @@ var BrandHomeLink = ({ route, label, onNavigate, children }) => {
1618
1701
  var DEFAULT_ICON_SIZE = 32;
1619
1702
  var NAME_FONT_SIZE = 16;
1620
1703
  var NAME_GAP = 8;
1621
- var styles4 = reactNative.StyleSheet.create({
1704
+ var styles5 = reactNative.StyleSheet.create({
1622
1705
  name: { flexShrink: 1, fontSize: NAME_FONT_SIZE, fontWeight: "700" },
1623
1706
  row: { alignItems: "center", flexDirection: "row", gap: NAME_GAP }
1624
1707
  });
@@ -1632,7 +1715,7 @@ var BrandMark = ({
1632
1715
  iconOnly = false
1633
1716
  }) => {
1634
1717
  const { theme } = uiFeedback.useUi();
1635
- return /* @__PURE__ */ jsxRuntime.jsx(BrandHomeLink, { label, route, onNavigate, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles4.row, children: [
1718
+ return /* @__PURE__ */ jsxRuntime.jsx(BrandHomeLink, { label, route, onNavigate, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles5.row, children: [
1636
1719
  /* @__PURE__ */ jsxRuntime.jsx(
1637
1720
  reactNative.Image,
1638
1721
  {
@@ -1646,7 +1729,7 @@ var BrandMark = ({
1646
1729
  reactNative.Text,
1647
1730
  {
1648
1731
  numberOfLines: 1,
1649
- style: [styles4.name, { color: theme.colors.text }],
1732
+ style: [styles5.name, { color: theme.colors.text }],
1650
1733
  testID: NAV_TEST_IDS.brandMarkName,
1651
1734
  children: name
1652
1735
  }
@@ -1659,7 +1742,7 @@ var Nav = (props) => {
1659
1742
  }
1660
1743
  return /* @__PURE__ */ jsxRuntime.jsx(NavBar, { ...props });
1661
1744
  };
1662
- var styles5 = reactNative.StyleSheet.create({
1745
+ var styles6 = reactNative.StyleSheet.create({
1663
1746
  band: { left: 0, position: "absolute", right: 0, top: 0 }
1664
1747
  });
1665
1748
  function readBackdrop(theme) {
@@ -1682,7 +1765,7 @@ function resolveShellBackdrop(theme, viewport, contentPadding, testID) {
1682
1765
  reactNative.View,
1683
1766
  {
1684
1767
  pointerEvents: "none",
1685
- style: [styles5.band, { backgroundColor: band.color, height: contentPadding + overlap }],
1768
+ style: [styles6.band, { backgroundColor: band.color, height: contentPadding + overlap }],
1686
1769
  testID: `${testID}${APP_SHELL_SUFFIX.band}`
1687
1770
  }
1688
1771
  );
@@ -1694,7 +1777,7 @@ var CARD_BORDER_WIDTH = 1;
1694
1777
  var CARD_TITLE_FONT_SIZE = 16;
1695
1778
  var CARD_MESSAGE_FONT_SIZE = 14;
1696
1779
  var CARD_TITLE_MARGIN_BOTTOM = 8;
1697
- var styles6 = reactNative.StyleSheet.create({
1780
+ var styles7 = reactNative.StyleSheet.create({
1698
1781
  // Loading is a TRANSITIONAL state: the spinner is an absolute overlay ON TOP of
1699
1782
  // stably-mounted children, never a replacement for them. Replacing children here
1700
1783
  // would unmount whatever they host (e.g. a navigator), and on remount React
@@ -1726,9 +1809,9 @@ function MessageCard({
1726
1809
  }) {
1727
1810
  const { theme } = uiFeedback.useUi();
1728
1811
  const colors = theme.colors;
1729
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles6.card, { backgroundColor: colors.surface, borderColor: accentColor }], testID, children: [
1730
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles6.cardTitle, { color: accentColor }], children: message.titleText }),
1731
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles6.cardMessage, { color: colors.textSecondary }], children: message.messageText })
1812
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles7.card, { backgroundColor: colors.surface, borderColor: accentColor }], testID, children: [
1813
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.cardTitle, { color: accentColor }], children: message.titleText }),
1814
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.cardMessage, { color: colors.textSecondary }], children: message.messageText })
1732
1815
  ] });
1733
1816
  }
1734
1817
  function useContentBody(state, children, testID) {
@@ -1740,12 +1823,12 @@ function useContentBody(state, children, testID) {
1740
1823
  return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.forbidden, testID: `${testID}${APP_SHELL_SUFFIX.forbidden}` });
1741
1824
  if (state.error)
1742
1825
  return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
1743
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles6.loadingHost, children: [
1826
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles7.loadingHost, children: [
1744
1827
  children,
1745
1828
  state.loading === true ? /* @__PURE__ */ jsxRuntime.jsx(
1746
1829
  reactNative.View,
1747
1830
  {
1748
- style: [styles6.loadingOverlay, { backgroundColor: theme.colors.background }],
1831
+ style: [styles7.loadingOverlay, { backgroundColor: theme.colors.background }],
1749
1832
  testID: `${testID}${APP_SHELL_SUFFIX.loading}`,
1750
1833
  children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: primary, size: "large" })
1751
1834
  }
@@ -1772,7 +1855,7 @@ var DEFAULT_DRAWER_LABELS = {
1772
1855
  closeLabel: "Close menu",
1773
1856
  closeHint: "Close the navigation menu"
1774
1857
  };
1775
- var styles7 = reactNative.StyleSheet.create({
1858
+ var styles8 = reactNative.StyleSheet.create({
1776
1859
  overlay: { ...reactNative.StyleSheet.absoluteFillObject, zIndex: DRAWER_Z_INDEX },
1777
1860
  // The scrim's tap-to-close hit area is anchored to the RIGHT of the panel
1778
1861
  // (`left: DRAWER_WIDTH`), so it NEVER overlaps the panel: nav-item taps always
@@ -1807,10 +1890,10 @@ var MenuToggle = ({ label, hint, onOpen, testID }) => {
1807
1890
  accessibilityHint: hint,
1808
1891
  accessibilityLabel: label,
1809
1892
  ringColor: theme.palette.primary["500"],
1810
- style: styles7.menuToggle,
1893
+ style: styles8.menuToggle,
1811
1894
  testID,
1812
1895
  onPress: onOpen,
1813
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.menuToggleGlyph, { color: theme.colors.text }], children: MENU_GLYPH2 })
1896
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles8.menuToggleGlyph, { color: theme.colors.text }], children: MENU_GLYPH2 })
1814
1897
  }
1815
1898
  );
1816
1899
  };
@@ -1836,14 +1919,14 @@ var MobileDrawer = ({
1836
1919
  node.addEventListener("click", handleClick);
1837
1920
  return () => node.removeEventListener("click", handleClick);
1838
1921
  }, [onClose]);
1839
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles7.overlay, children: [
1922
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles8.overlay, children: [
1840
1923
  /* @__PURE__ */ jsxRuntime.jsx(
1841
1924
  reactNative.Pressable,
1842
1925
  {
1843
1926
  accessibilityHint: labels.closeHint,
1844
1927
  accessibilityLabel: labels.closeLabel,
1845
1928
  accessibilityRole: "button",
1846
- style: styles7.scrim,
1929
+ style: styles8.scrim,
1847
1930
  testID: scrimTestID,
1848
1931
  onPress: onClose
1849
1932
  }
@@ -1854,9 +1937,9 @@ var MobileDrawer = ({
1854
1937
  ref: panelRef,
1855
1938
  "aria-modal": true,
1856
1939
  role: "dialog",
1857
- style: [styles7.panel, { backgroundColor: theme.colors.surface }],
1940
+ style: [styles8.panel, { backgroundColor: theme.colors.surface }],
1858
1941
  testID: drawerTestID,
1859
- children: renderTextSlot(sidebar, { color: theme.colors.text })
1942
+ children: /* @__PURE__ */ jsxRuntime.jsx(NavAccordionContext.Provider, { value: true, children: renderTextSlot(sidebar, { color: theme.colors.text }) })
1860
1943
  }
1861
1944
  )
1862
1945
  ] });
@@ -1872,7 +1955,7 @@ function useContentMaxWidth(width) {
1872
1955
  return resolveContentMaxWidth(width, viewport);
1873
1956
  }
1874
1957
  var DEFAULT_CONTENT_PADDING = 24;
1875
- var styles8 = reactNative.StyleSheet.create({
1958
+ var styles9 = reactNative.StyleSheet.create({
1876
1959
  root: { flex: 1 },
1877
1960
  // Auth-pending spinner: an opaque absolute overlay over the mounted shell, so
1878
1961
  // `children` (and any navigator they host) are never unmounted while the gate
@@ -1934,13 +2017,13 @@ var AppShell = ({
1934
2017
  }, [isUnauthenticated, gate]);
1935
2018
  const gatePending = gate?.pending === true;
1936
2019
  if (isUnauthenticated) return null;
1937
- const columnStyle = maxWidth === "full" ? styles8.columnFull : [styles8.columnCapped, { maxWidth }];
1938
- const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles8.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
2020
+ const columnStyle = maxWidth === "full" ? styles9.columnFull : [styles9.columnCapped, { maxWidth }];
2021
+ const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles9.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
1939
2022
  const scroller = /* @__PURE__ */ jsxRuntime.jsxs(
1940
2023
  reactNative.ScrollView,
1941
2024
  {
1942
- contentContainerStyle: [styles8.scrollContent, { padding: contentPadding }],
1943
- style: styles8.scroll,
2025
+ contentContainerStyle: [styles9.scrollContent, { padding: contentPadding }],
2026
+ style: styles9.scroll,
1944
2027
  testID: `${testID}${APP_SHELL_SUFFIX.content}`,
1945
2028
  children: [
1946
2029
  look.band,
@@ -1948,7 +2031,7 @@ var AppShell = ({
1948
2031
  ]
1949
2032
  }
1950
2033
  );
1951
- const headerRegion = useDrawer ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles8.headerRow, look.chrome], testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: [
2034
+ const headerRegion = useDrawer ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles9.headerRow, look.chrome], testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: [
1952
2035
  /* @__PURE__ */ jsxRuntime.jsx(
1953
2036
  MenuToggle,
1954
2037
  {
@@ -1958,14 +2041,14 @@ var AppShell = ({
1958
2041
  onOpen: openDrawer
1959
2042
  }
1960
2043
  ),
1961
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles8.headerFill, children: renderTextSlot(header, { color: look.chromeText }) })
2044
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles9.headerFill, children: renderTextSlot(header, { color: look.chromeText }) })
1962
2045
  ] }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: look.chrome, testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: renderTextSlot(header, { color: look.chromeText }) });
1963
2046
  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;
1964
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles8.root, look.root], testID, children: [
2047
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles9.root, look.root], testID, children: [
1965
2048
  headerRegion,
1966
2049
  nav !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: look.chrome, testID: `${testID}${APP_SHELL_SUFFIX.nav}`, children: navInnerStyle !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: navInnerStyle, children: renderTextSlot(nav, { color: look.chromeText }) }) : renderTextSlot(nav, { color: look.chromeText }) }) : null,
1967
2050
  banner !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: renderTextSlot(banner, { color: theme.colors.text }) }) : null,
1968
- railRegion !== null ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles8.bodyRow, children: [
2051
+ railRegion !== null ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles9.bodyRow, children: [
1969
2052
  railRegion,
1970
2053
  scroller
1971
2054
  ] }) : scroller,
@@ -1982,7 +2065,7 @@ var AppShell = ({
1982
2065
  gatePending ? /* @__PURE__ */ jsxRuntime.jsx(
1983
2066
  reactNative.View,
1984
2067
  {
1985
- style: [styles8.pendingOverlay, { backgroundColor: theme.colors.background }],
2068
+ style: [styles9.pendingOverlay, { backgroundColor: theme.colors.background }],
1986
2069
  testID: `${testID}${APP_SHELL_SUFFIX.pending}`,
1987
2070
  children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: primary, size: "large" })
1988
2071
  }
@@ -2351,7 +2434,7 @@ var KEY_DOWN = "ArrowDown";
2351
2434
  var KEY_UP = "ArrowUp";
2352
2435
  var KEY_ENTER = "Enter";
2353
2436
  var KEY_ESCAPE = "Escape";
2354
- var styles9 = reactNative.StyleSheet.create({
2437
+ var styles10 = reactNative.StyleSheet.create({
2355
2438
  overlay: { ...reactNative.StyleSheet.absoluteFillObject, zIndex: OVERLAY_Z_INDEX, alignItems: "center" },
2356
2439
  scrim: { ...reactNative.StyleSheet.absoluteFillObject, backgroundColor: SCRIM_COLOR2 },
2357
2440
  panel: {
@@ -2380,14 +2463,14 @@ var PaletteRow = ({ item, selected, onChoose }) => {
2380
2463
  accessibilityLabel: item.label,
2381
2464
  accessibilityRole: "button",
2382
2465
  accessibilityState: { selected },
2383
- style: [styles9.row, selected ? { backgroundColor: colors.border } : void 0],
2466
+ style: [styles10.row, selected ? { backgroundColor: colors.border } : void 0],
2384
2467
  testID: item.testID ?? item.key,
2385
2468
  onPress: () => onChoose(item),
2386
2469
  children: [
2387
2470
  typeof item.renderIcon === "function" ? item.renderIcon(colors.textSecondary, NAV_ICON_SIZE) : null,
2388
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles9.rowText, children: [
2389
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles9.rowLabel, { color: colors.text }], children: item.label }),
2390
- item.hint ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles9.rowHint, { color: colors.textSecondary }], children: item.hint }) : null
2471
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles10.rowText, children: [
2472
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles10.rowLabel, { color: colors.text }], children: item.label }),
2473
+ item.hint ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles10.rowHint, { color: colors.textSecondary }], children: item.hint }) : null
2391
2474
  ] })
2392
2475
  ]
2393
2476
  }
@@ -2427,14 +2510,14 @@ var CommandPalette = ({ open, items, onClose, labels, testID }) => {
2427
2510
  [results, selected, choose, onClose]
2428
2511
  );
2429
2512
  if (!open) return null;
2430
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles9.overlay, children: [
2513
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles10.overlay, children: [
2431
2514
  /* @__PURE__ */ jsxRuntime.jsx(
2432
2515
  reactNative.Pressable,
2433
2516
  {
2434
2517
  accessibilityHint: labels.closeHint,
2435
2518
  accessibilityLabel: labels.closeLabel,
2436
2519
  accessibilityRole: "button",
2437
- style: styles9.scrim,
2520
+ style: styles10.scrim,
2438
2521
  testID: `${testID ?? "command-palette"}-scrim`,
2439
2522
  onPress: onClose
2440
2523
  }
@@ -2445,7 +2528,7 @@ var CommandPalette = ({ open, items, onClose, labels, testID }) => {
2445
2528
  "aria-modal": true,
2446
2529
  "aria-label": labels.regionLabel,
2447
2530
  role: "dialog",
2448
- style: [styles9.panel, { backgroundColor: colors.surface, borderColor: colors.border }],
2531
+ style: [styles10.panel, { backgroundColor: colors.surface, borderColor: colors.border }],
2449
2532
  testID: testID ?? "command-palette",
2450
2533
  children: [
2451
2534
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2455,14 +2538,14 @@ var CommandPalette = ({ open, items, onClose, labels, testID }) => {
2455
2538
  accessibilityLabel: labels.placeholder,
2456
2539
  placeholder: labels.placeholder,
2457
2540
  placeholderTextColor: colors.textSecondary,
2458
- style: [styles9.input, { color: colors.text, borderBottomColor: colors.border }],
2541
+ style: [styles10.input, { color: colors.text, borderBottomColor: colors.border }],
2459
2542
  testID: `${testID ?? "command-palette"}-input`,
2460
2543
  value: query,
2461
2544
  onChangeText: setQuery,
2462
2545
  onKeyPress
2463
2546
  }
2464
2547
  ),
2465
- results.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles9.empty, { color: colors.textSecondary }], children: labels.emptyText }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.ScrollView, { keyboardShouldPersistTaps: "handled", style: styles9.list, children: results.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(PaletteRow, { item, selected: index === selected, onChoose: choose }, item.key)) })
2548
+ results.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles10.empty, { color: colors.textSecondary }], children: labels.emptyText }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.ScrollView, { keyboardShouldPersistTaps: "handled", style: styles10.list, children: results.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(PaletteRow, { item, selected: index === selected, onChoose: choose }, item.key)) })
2466
2549
  ]
2467
2550
  }
2468
2551
  )
@@ -2567,7 +2650,7 @@ var TAB_MIN_HEIGHT = 56;
2567
2650
  var LABEL_SIZE = 12;
2568
2651
  var LABEL_GAP = 2;
2569
2652
  var TAB_PADDING = 4;
2570
- var styles10 = reactNative.StyleSheet.create({
2653
+ var styles11 = reactNative.StyleSheet.create({
2571
2654
  tab: {
2572
2655
  flex: 1,
2573
2656
  minHeight: TAB_MIN_HEIGHT,
@@ -2604,31 +2687,31 @@ var BottomTab = ({
2604
2687
  accessibilityLabel: item.label,
2605
2688
  accessibilityRole: "link",
2606
2689
  accessibilityState: { selected: isActive },
2607
- style: [styles10.tab, { opacity: feedback.opacity }],
2690
+ style: [styles11.tab, { opacity: feedback.opacity }],
2608
2691
  testID: item.testID ?? item.key,
2609
2692
  onPress: () => onPress(item.route),
2610
2693
  onPressIn: () => setPressed(true),
2611
2694
  onPressOut: () => setPressed(false),
2612
2695
  ...ariaCurrentProps3(isActive),
2613
2696
  children: [
2614
- /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Animated.View, { style: [styles10.iconWrap, { transform: [{ scale }] }], children: [
2697
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Animated.View, { style: [styles11.iconWrap, { transform: [{ scale }] }], children: [
2615
2698
  feedback.pillScale === null ? null : /* @__PURE__ */ jsxRuntime.jsx(
2616
2699
  reactNative.View,
2617
2700
  {
2618
2701
  pointerEvents: "none",
2619
- style: [styles10.pill, { backgroundColor: colors.pill, transform: [{ scale: feedback.pillScale }] }, motion]
2702
+ style: [styles11.pill, { backgroundColor: colors.pill, transform: [{ scale: feedback.pillScale }] }, motion]
2620
2703
  }
2621
2704
  ),
2622
2705
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [{ transform: feedback.iconTransform }, motion], children: item.renderIcon?.(color, ICON_SIZE) })
2623
2706
  ] }),
2624
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles10.label, { color }], children: item.label })
2707
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles11.label, { color }], children: item.label })
2625
2708
  ]
2626
2709
  }
2627
2710
  );
2628
2711
  };
2629
2712
  var DEFAULT_TEST_ID = "bottom-tab-bar";
2630
2713
  var SLIDER_HEIGHT = 3;
2631
- var styles11 = reactNative.StyleSheet.create({
2714
+ var styles12 = reactNative.StyleSheet.create({
2632
2715
  bar: { flexDirection: "row", borderTopWidth: reactNative.StyleSheet.hairlineWidth },
2633
2716
  slider: { position: "absolute", top: 0, height: SLIDER_HEIGHT }
2634
2717
  });
@@ -2655,14 +2738,14 @@ var BottomTabBar = ({
2655
2738
  {
2656
2739
  "aria-label": regionLabel,
2657
2740
  role: "navigation",
2658
- style: [styles11.bar, { backgroundColor: theme.colors.surface, borderTopColor: theme.colors.border }, containerStyle],
2741
+ style: [styles12.bar, { backgroundColor: theme.colors.surface, borderTopColor: theme.colors.border }, containerStyle],
2659
2742
  testID,
2660
2743
  children: [
2661
2744
  bar === null ? null : /* @__PURE__ */ jsxRuntime.jsx(
2662
2745
  reactNative.View,
2663
2746
  {
2664
2747
  pointerEvents: "none",
2665
- style: [styles11.slider, { left: bar.left, width: bar.width, backgroundColor: primary }, webTransition("left", barMs, reducedMotion)]
2748
+ style: [styles12.slider, { left: bar.left, width: bar.width, backgroundColor: primary }, webTransition("left", barMs, reducedMotion)]
2666
2749
  }
2667
2750
  ),
2668
2751
  items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(