@dloizides/ui-nav 1.4.0 → 1.5.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,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.0
4
+
5
+ Make the `AppShell` back-office `sidebar` layout **responsive** — a mobile drawer.
6
+
7
+ The `sidebar` slot (1.4.0) always rendered a persistent fixed-width left rail in a
8
+ `flexDirection:'row'` body with no width breakpoint, so below ~768px the content column
9
+ starved (headings wrapped a character per line, inputs truncated) — the merchant admin was
10
+ unusable on a phone. Now `AppShell` reads `useWindowDimensions`:
11
+
12
+ - **Desktop (≥768px): IDENTICAL to 1.4.0** — the persistent rail + row layout, untouched.
13
+ Purely additive; no consuming product's desktop layout changes.
14
+ - **Below 768px:** the rail is not rendered beside the content; instead a **hamburger**
15
+ (in the header) opens the same `Sidebar` as an overlay **drawer** (a tap-to-close scrim +
16
+ a left panel) and the content takes the full width. The drawer closes on a scrim tap and
17
+ on a nav-item press (web: a bubbled click on a `role="button"` / link inside the panel).
18
+
19
+ Accessibility: the hamburger has a testID + `accessibilityLabel` + `accessibilityHint` and a
20
+ themed focus ring; the drawer panel is `role="dialog"` `aria-modal`; the scrim is a labelled
21
+ button. New optional prop `mobileMenu?: Partial<DrawerLabels>` supplies pre-localized
22
+ open/close labels (English defaults otherwise — this package has no i18n). New testID
23
+ suffixes: `-menu-toggle`, `-drawer`, `-drawer-scrim`.
24
+
25
+ **Backwards compatible.** No breaking prop changes — consumers pass the same `sidebar` slot.
26
+ Omit `sidebar` and the tree is byte-identical to 1.4.0 (erevna / katalogos / kefi untouched).
27
+ Only a `sidebar` consumer on a narrow viewport sees the drawer. Pinned by tests asserting the
28
+ rail is present on desktop and absent (hamburger present) on a narrow viewport.
29
+
3
30
  ## 1.4.0
4
31
 
5
32
  Add an optional **`sidebar`** slot to `AppShell` — the back-office layout.
package/dist/index.d.mts CHANGED
@@ -195,6 +195,33 @@ interface NavBarProps {
195
195
  }
196
196
  declare const NavBar: ({ items, pathname, onNavigate, regionLabel, navigateHint, brand, right, menuLabel, menuHint, renderMenuIcon, collapseBelow, containerStyle, }: NavBarProps) => React.ReactElement;
197
197
 
198
+ /**
199
+ * MobileDrawer — the below-the-breakpoint face of `AppShell`'s back-office
200
+ * `sidebar` layout. On a narrow viewport (<768px) the persistent left rail would
201
+ * starve the content column, so instead of the rail we render a hamburger toggle
202
+ * (`MenuToggle`) in the header and slide the same `Sidebar` in as an overlay
203
+ * drawer (a scrim + a left panel). The drawer closes on a scrim tap and on any
204
+ * nav-item press inside the panel (web: a bubbled click on a `role="button"` /
205
+ * link element). Desktop is untouched — AppShell never mounts this above 768px.
206
+ *
207
+ * Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM / router /
208
+ * store imports. Labels are pre-localized strings (English defaults, overridable
209
+ * by the consumer via `AppShell`'s `mobileMenu` prop); every colour is read from
210
+ * the `@dloizides/ui-feedback` UiProvider theme.
211
+ */
212
+
213
+ /** Pre-localized labels for the drawer's hamburger + scrim. */
214
+ interface DrawerLabels {
215
+ /** Accessible name of the hamburger that opens the drawer. */
216
+ openLabel: string;
217
+ /** Accessible hint of the hamburger that opens the drawer. */
218
+ openHint: string;
219
+ /** Accessible name of the scrim (tap to close). */
220
+ closeLabel: string;
221
+ /** Accessible hint of the scrim (tap to close). */
222
+ closeHint: string;
223
+ }
224
+
198
225
  /** Content-width policy: a capped column, optionally wider past a viewport breakpoint, or full-bleed. */
199
226
  type AppShellWidth = {
200
227
  max: number;
@@ -245,8 +272,18 @@ interface AppShellProps {
245
272
  * supplied, the region below header/nav/banner lays out as a row: the rail on the
246
273
  * left, the scrolling content column on the right. Omit for the default vertical
247
274
  * stack — the tree is then byte-identical to the pre-`sidebar` AppShell.
275
+ *
276
+ * Responsive: at/under ~768px the persistent rail is REPLACED by a hamburger (in
277
+ * the header) that opens the same `sidebar` as an overlay drawer, so the content
278
+ * takes the full width. Desktop (≥768px) is unchanged. No effect without `sidebar`.
248
279
  */
249
280
  sidebar?: React.ReactNode;
281
+ /**
282
+ * Pre-localized labels for the mobile nav drawer (only used in the `sidebar`
283
+ * layout, below the breakpoint): the hamburger's open label/hint and the scrim's
284
+ * close label/hint. Optional — English defaults apply. NO i18n in this package.
285
+ */
286
+ mobileMenu?: Partial<DrawerLabels>;
250
287
  /** Banner slot (e.g. verification-pending). */
251
288
  banner?: React.ReactNode;
252
289
  /** Content-width policy. Defaults to full-bleed. */
@@ -275,7 +312,7 @@ interface AppShellProps {
275
312
  testID: string;
276
313
  children: React.ReactNode;
277
314
  }
278
- declare const AppShell: ({ header, nav, sidebar, banner, width, contentPadding, chromeAlignment, gate, state, testID, children, }: AppShellProps) => React.ReactElement | null;
315
+ declare const AppShell: ({ header, nav, sidebar, mobileMenu, banner, width, contentPadding, chromeAlignment, gate, state, testID, children, }: AppShellProps) => React.ReactElement | null;
279
316
 
280
317
  /**
281
318
  * Default testIDs for `@dloizides/ui-nav`. Kept as a small central map (mirrors
@@ -305,6 +342,12 @@ declare const APP_SHELL_SUFFIX: {
305
342
  readonly nav: "-nav";
306
343
  /** The persistent left rail of the back-office layout (only when `sidebar` is supplied). */
307
344
  readonly sidebar: "-sidebar";
345
+ /** Mobile hamburger that opens the nav drawer (back-office layout, below the breakpoint). */
346
+ readonly menuToggle: "-menu-toggle";
347
+ /** The mobile nav drawer panel (holds the `sidebar` slot when opened on a narrow viewport). */
348
+ readonly drawer: "-drawer";
349
+ /** The mobile nav drawer scrim (tap to close). */
350
+ readonly scrim: "-drawer-scrim";
308
351
  readonly banner: "-banner";
309
352
  readonly pending: "-pending";
310
353
  readonly loading: "-loading";
package/dist/index.d.ts CHANGED
@@ -195,6 +195,33 @@ interface NavBarProps {
195
195
  }
196
196
  declare const NavBar: ({ items, pathname, onNavigate, regionLabel, navigateHint, brand, right, menuLabel, menuHint, renderMenuIcon, collapseBelow, containerStyle, }: NavBarProps) => React.ReactElement;
197
197
 
198
+ /**
199
+ * MobileDrawer — the below-the-breakpoint face of `AppShell`'s back-office
200
+ * `sidebar` layout. On a narrow viewport (<768px) the persistent left rail would
201
+ * starve the content column, so instead of the rail we render a hamburger toggle
202
+ * (`MenuToggle`) in the header and slide the same `Sidebar` in as an overlay
203
+ * drawer (a scrim + a left panel). The drawer closes on a scrim tap and on any
204
+ * nav-item press inside the panel (web: a bubbled click on a `role="button"` /
205
+ * link element). Desktop is untouched — AppShell never mounts this above 768px.
206
+ *
207
+ * Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM / router /
208
+ * store imports. Labels are pre-localized strings (English defaults, overridable
209
+ * by the consumer via `AppShell`'s `mobileMenu` prop); every colour is read from
210
+ * the `@dloizides/ui-feedback` UiProvider theme.
211
+ */
212
+
213
+ /** Pre-localized labels for the drawer's hamburger + scrim. */
214
+ interface DrawerLabels {
215
+ /** Accessible name of the hamburger that opens the drawer. */
216
+ openLabel: string;
217
+ /** Accessible hint of the hamburger that opens the drawer. */
218
+ openHint: string;
219
+ /** Accessible name of the scrim (tap to close). */
220
+ closeLabel: string;
221
+ /** Accessible hint of the scrim (tap to close). */
222
+ closeHint: string;
223
+ }
224
+
198
225
  /** Content-width policy: a capped column, optionally wider past a viewport breakpoint, or full-bleed. */
199
226
  type AppShellWidth = {
200
227
  max: number;
@@ -245,8 +272,18 @@ interface AppShellProps {
245
272
  * supplied, the region below header/nav/banner lays out as a row: the rail on the
246
273
  * left, the scrolling content column on the right. Omit for the default vertical
247
274
  * stack — the tree is then byte-identical to the pre-`sidebar` AppShell.
275
+ *
276
+ * Responsive: at/under ~768px the persistent rail is REPLACED by a hamburger (in
277
+ * the header) that opens the same `sidebar` as an overlay drawer, so the content
278
+ * takes the full width. Desktop (≥768px) is unchanged. No effect without `sidebar`.
248
279
  */
249
280
  sidebar?: React.ReactNode;
281
+ /**
282
+ * Pre-localized labels for the mobile nav drawer (only used in the `sidebar`
283
+ * layout, below the breakpoint): the hamburger's open label/hint and the scrim's
284
+ * close label/hint. Optional — English defaults apply. NO i18n in this package.
285
+ */
286
+ mobileMenu?: Partial<DrawerLabels>;
250
287
  /** Banner slot (e.g. verification-pending). */
251
288
  banner?: React.ReactNode;
252
289
  /** Content-width policy. Defaults to full-bleed. */
@@ -275,7 +312,7 @@ interface AppShellProps {
275
312
  testID: string;
276
313
  children: React.ReactNode;
277
314
  }
278
- declare const AppShell: ({ header, nav, sidebar, banner, width, contentPadding, chromeAlignment, gate, state, testID, children, }: AppShellProps) => React.ReactElement | null;
315
+ declare const AppShell: ({ header, nav, sidebar, mobileMenu, banner, width, contentPadding, chromeAlignment, gate, state, testID, children, }: AppShellProps) => React.ReactElement | null;
279
316
 
280
317
  /**
281
318
  * Default testIDs for `@dloizides/ui-nav`. Kept as a small central map (mirrors
@@ -305,6 +342,12 @@ declare const APP_SHELL_SUFFIX: {
305
342
  readonly nav: "-nav";
306
343
  /** The persistent left rail of the back-office layout (only when `sidebar` is supplied). */
307
344
  readonly sidebar: "-sidebar";
345
+ /** Mobile hamburger that opens the nav drawer (back-office layout, below the breakpoint). */
346
+ readonly menuToggle: "-menu-toggle";
347
+ /** The mobile nav drawer panel (holds the `sidebar` slot when opened on a narrow viewport). */
348
+ readonly drawer: "-drawer";
349
+ /** The mobile nav drawer scrim (tap to close). */
350
+ readonly scrim: "-drawer-scrim";
308
351
  readonly banner: "-banner";
309
352
  readonly pending: "-pending";
310
353
  readonly loading: "-loading";
package/dist/index.js CHANGED
@@ -348,6 +348,12 @@ var APP_SHELL_SUFFIX = {
348
348
  nav: "-nav",
349
349
  /** The persistent left rail of the back-office layout (only when `sidebar` is supplied). */
350
350
  sidebar: "-sidebar",
351
+ /** Mobile hamburger that opens the nav drawer (back-office layout, below the breakpoint). */
352
+ menuToggle: "-menu-toggle",
353
+ /** The mobile nav drawer panel (holds the `sidebar` slot when opened on a narrow viewport). */
354
+ drawer: "-drawer",
355
+ /** The mobile nav drawer scrim (tap to close). */
356
+ scrim: "-drawer-scrim",
351
357
  banner: "-banner",
352
358
  pending: "-pending",
353
359
  loading: "-loading",
@@ -671,6 +677,82 @@ var NavBar = ({
671
677
  }
672
678
  );
673
679
  };
680
+ var MOBILE_BREAKPOINT = 768;
681
+ var SCRIM_COLOR = "rgba(0, 0, 0, 0.5)";
682
+ var MENU_GLYPH2 = "\u2630";
683
+ var MENU_MIN_TARGET = 44;
684
+ var MENU_GLYPH_FONT_SIZE = 20;
685
+ var MENU_TOGGLE_PADDING = 12;
686
+ var DRAWER_Z_INDEX = 50;
687
+ var NAV_ACTIVATABLE_SELECTOR = 'a, [role="button"], [role="link"]';
688
+ var DEFAULT_DRAWER_LABELS = {
689
+ openLabel: "Menu",
690
+ openHint: "Open the navigation menu",
691
+ closeLabel: "Close menu",
692
+ closeHint: "Close the navigation menu"
693
+ };
694
+ var styles = reactNative.StyleSheet.create({
695
+ overlay: { ...reactNative.StyleSheet.absoluteFillObject, zIndex: DRAWER_Z_INDEX },
696
+ scrim: { ...reactNative.StyleSheet.absoluteFillObject, backgroundColor: SCRIM_COLOR },
697
+ panel: { position: "absolute", top: 0, bottom: 0, left: 0 },
698
+ menuToggle: {
699
+ minWidth: MENU_MIN_TARGET,
700
+ minHeight: MENU_MIN_TARGET,
701
+ alignItems: "center",
702
+ justifyContent: "center",
703
+ paddingHorizontal: MENU_TOGGLE_PADDING
704
+ },
705
+ menuToggleGlyph: { fontSize: MENU_GLYPH_FONT_SIZE, fontWeight: "700" }
706
+ });
707
+ var MenuToggle = ({ label, hint, onOpen, testID }) => {
708
+ const { theme } = uiFeedback.useUi();
709
+ return /* @__PURE__ */ jsxRuntime.jsx(
710
+ FocusableTouchable,
711
+ {
712
+ accessibilityHint: hint,
713
+ accessibilityLabel: label,
714
+ ringColor: theme.palette.primary["500"],
715
+ style: styles.menuToggle,
716
+ testID,
717
+ onPress: onOpen,
718
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.menuToggleGlyph, { color: theme.colors.text }], children: MENU_GLYPH2 })
719
+ }
720
+ );
721
+ };
722
+ var MobileDrawer = ({
723
+ sidebar,
724
+ labels,
725
+ onClose,
726
+ drawerTestID,
727
+ scrimTestID
728
+ }) => {
729
+ const panelRef = react.useRef(null);
730
+ react.useEffect(() => {
731
+ const node = panelRef.current;
732
+ if (node === null || typeof node.addEventListener !== "function") return void 0;
733
+ const handleClick = (event) => {
734
+ const target = event.target;
735
+ const onNavItem = target !== null && target.closest(NAV_ACTIVATABLE_SELECTOR) !== null;
736
+ if (onNavItem) onClose();
737
+ };
738
+ node.addEventListener("click", handleClick);
739
+ return () => node.removeEventListener("click", handleClick);
740
+ }, [onClose]);
741
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.overlay, children: [
742
+ /* @__PURE__ */ jsxRuntime.jsx(
743
+ reactNative.Pressable,
744
+ {
745
+ accessibilityHint: labels.closeHint,
746
+ accessibilityLabel: labels.closeLabel,
747
+ accessibilityRole: "button",
748
+ style: styles.scrim,
749
+ testID: scrimTestID,
750
+ onPress: onClose
751
+ }
752
+ ),
753
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ref: panelRef, "aria-modal": true, role: "dialog", style: styles.panel, testID: drawerTestID, children: sidebar })
754
+ ] });
755
+ };
674
756
  function resolveContentMaxWidth(width, viewport) {
675
757
  if (width === "full") return "full";
676
758
  const wideMinViewport = width.wideMinViewport ?? Number.POSITIVE_INFINITY;
@@ -688,11 +770,14 @@ var CARD_BORDER_WIDTH = 1;
688
770
  var CARD_TITLE_FONT_SIZE = 16;
689
771
  var CARD_MESSAGE_FONT_SIZE = 14;
690
772
  var CARD_TITLE_MARGIN_BOTTOM = 8;
691
- var styles = reactNative.StyleSheet.create({
773
+ var styles2 = reactNative.StyleSheet.create({
692
774
  root: { flex: 1 },
693
775
  centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
694
776
  scroll: { flex: 1 },
695
777
  scrollContent: { flexGrow: 1 },
778
+ // Mobile back-office header: the hamburger sits inline before the header slot.
779
+ headerRow: { flexDirection: "row", alignItems: "center" },
780
+ headerFill: { flex: 1 },
696
781
  // Back-office layout: a persistent left rail beside the scrolling content column.
697
782
  bodyRow: { flex: 1, flexDirection: "row" },
698
783
  columnFull: { flex: 1 },
@@ -718,11 +803,11 @@ function MessageCard({
718
803
  return /* @__PURE__ */ jsxRuntime.jsxs(
719
804
  reactNative.View,
720
805
  {
721
- style: [styles.card, { backgroundColor: colors.surface, borderColor: accentColor }],
806
+ style: [styles2.card, { backgroundColor: colors.surface, borderColor: accentColor }],
722
807
  testID,
723
808
  children: [
724
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.cardTitle, { color: accentColor }], children: message.titleText }),
725
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.cardMessage, { color: colors.textSecondary }], children: message.messageText })
809
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.cardTitle, { color: accentColor }], children: message.titleText }),
810
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.cardMessage, { color: colors.textSecondary }], children: message.messageText })
726
811
  ]
727
812
  }
728
813
  );
@@ -736,13 +821,14 @@ function useContentBody(state, children, testID) {
736
821
  if (state?.error)
737
822
  return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
738
823
  if (state?.loading === true)
739
- 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" }) });
824
+ 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" }) });
740
825
  return children;
741
826
  }
742
827
  var AppShell = ({
743
828
  header,
744
829
  nav,
745
830
  sidebar,
831
+ mobileMenu,
746
832
  banner,
747
833
  width = "full",
748
834
  contentPadding = DEFAULT_CONTENT_PADDING,
@@ -756,32 +842,65 @@ var AppShell = ({
756
842
  const primary = theme.palette.primary["500"];
757
843
  const maxWidth = useContentMaxWidth(width);
758
844
  const body = useContentBody(state, children, testID);
845
+ const { width: viewportWidth } = reactNative.useWindowDimensions();
846
+ const isNarrow = viewportWidth < MOBILE_BREAKPOINT;
847
+ const useDrawer = sidebar !== void 0 && isNarrow;
848
+ const [drawerOpen, setDrawerOpen] = react.useState(false);
849
+ const openDrawer = react.useCallback(() => setDrawerOpen(true), []);
850
+ const closeDrawer = react.useCallback(() => setDrawerOpen(false), []);
851
+ react.useEffect(() => {
852
+ if (!isNarrow && drawerOpen) setDrawerOpen(false);
853
+ }, [isNarrow, drawerOpen]);
854
+ const drawerLabels = { ...DEFAULT_DRAWER_LABELS, ...mobileMenu };
759
855
  const isUnauthenticated = gate !== void 0 && !gate.pending && !gate.authenticated;
760
856
  react.useEffect(() => {
761
857
  if (isUnauthenticated && gate !== void 0) gate.onRedirect();
762
858
  }, [isUnauthenticated, gate]);
763
859
  if (gate?.pending === true)
764
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles.root, styles.centerFill, { backgroundColor: theme.colors.background }], testID: `${testID}${APP_SHELL_SUFFIX.pending}`, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: primary, size: "large" }) });
860
+ 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" }) });
765
861
  if (isUnauthenticated) return null;
766
- const columnStyle = maxWidth === "full" ? styles.columnFull : [styles.columnCapped, { maxWidth }];
767
- const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
862
+ const columnStyle = maxWidth === "full" ? styles2.columnFull : [styles2.columnCapped, { maxWidth }];
863
+ const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles2.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
768
864
  const scroller = /* @__PURE__ */ jsxRuntime.jsx(
769
865
  reactNative.ScrollView,
770
866
  {
771
- contentContainerStyle: [styles.scrollContent, { padding: contentPadding }],
772
- style: styles.scroll,
867
+ contentContainerStyle: [styles2.scrollContent, { padding: contentPadding }],
868
+ style: styles2.scroll,
773
869
  testID: `${testID}${APP_SHELL_SUFFIX.content}`,
774
870
  children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: columnStyle, children: body })
775
871
  }
776
872
  );
777
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.root, { backgroundColor: theme.colors.background }], testID, children: [
778
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: header }),
873
+ const headerRegion = useDrawer ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.headerRow, testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: [
874
+ /* @__PURE__ */ jsxRuntime.jsx(
875
+ MenuToggle,
876
+ {
877
+ hint: drawerLabels.openHint,
878
+ label: drawerLabels.openLabel,
879
+ testID: `${testID}${APP_SHELL_SUFFIX.menuToggle}`,
880
+ onOpen: openDrawer
881
+ }
882
+ ),
883
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.headerFill, children: header })
884
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: header });
885
+ const showRail = sidebar !== void 0 && !isNarrow;
886
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles2.root, { backgroundColor: theme.colors.background }], testID, children: [
887
+ headerRegion,
779
888
  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,
780
889
  banner !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: banner }) : null,
781
- sidebar !== void 0 ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.bodyRow, children: [
890
+ showRail ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.bodyRow, children: [
782
891
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.sidebar}`, children: sidebar }),
783
892
  scroller
784
- ] }) : scroller
893
+ ] }) : scroller,
894
+ useDrawer && drawerOpen ? /* @__PURE__ */ jsxRuntime.jsx(
895
+ MobileDrawer,
896
+ {
897
+ drawerTestID: `${testID}${APP_SHELL_SUFFIX.drawer}`,
898
+ labels: drawerLabels,
899
+ scrimTestID: `${testID}${APP_SHELL_SUFFIX.scrim}`,
900
+ sidebar,
901
+ onClose: closeDrawer
902
+ }
903
+ ) : null
785
904
  ] });
786
905
  };
787
906
  function roleRoutesToNavItems(routes, translate) {