@dloizides/ui-nav 1.21.0 → 1.23.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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.23.0
4
+
5
+ **The NavBar's stacked mobile menu now scrolls within the viewport (SHOWCASE-1).**
6
+
7
+ Below `collapseBelow` the open hamburger menu grew to its natural height (734px with ~15 items on
8
+ ui.dloizides.com at 360x640). Portals set `body { overflow: hidden }`, so the last items sat below
9
+ the fold and neither wheel nor touch could reach them — in every portal with more than ~10 items.
10
+
11
+ - The stacked links region is now a `ScrollView` capped to `windowHeight - closedBarHeight -
12
+ stackedRightSlot - chrome` (pure `computeStackedMenuMaxHeight`, unit-tested; floor of three 44px
13
+ rows). Rows keep their 44px minimum; nothing depends on hover.
14
+
3
15
  ## 1.20.0
4
16
 
5
17
  **The auth-pending and page-loading spinners now OVERLAY stably-mounted children instead of
package/dist/index.d.mts CHANGED
@@ -1359,6 +1359,9 @@ declare const navStyles: {
1359
1359
  paddingHorizontal: number;
1360
1360
  paddingVertical: number;
1361
1361
  borderRadius: number;
1362
+ minHeight: number;
1363
+ minWidth: number;
1364
+ justifyContent: "center";
1362
1365
  };
1363
1366
  accountText: {
1364
1367
  fontWeight: "600";
@@ -1415,6 +1418,7 @@ declare const darkModeStyles: {
1415
1418
  paddingHorizontal: number;
1416
1419
  paddingVertical: number;
1417
1420
  minHeight: number;
1421
+ minWidth: number;
1418
1422
  flexDirection: "row";
1419
1423
  alignItems: "center";
1420
1424
  columnGap: number;
@@ -1476,6 +1480,7 @@ declare const expandableStyles: {
1476
1480
  flexDirection: "row";
1477
1481
  alignItems: "center";
1478
1482
  paddingVertical: number;
1483
+ minHeight: number;
1479
1484
  borderLeftWidth: number;
1480
1485
  borderLeftColor: string;
1481
1486
  };
@@ -1509,6 +1514,7 @@ declare const expandableStyles: {
1509
1514
  flexDirection: "row";
1510
1515
  alignItems: "center";
1511
1516
  paddingVertical: number;
1517
+ minHeight: number;
1512
1518
  };
1513
1519
  sectionHeader: {
1514
1520
  marginTop: number;
package/dist/index.d.ts CHANGED
@@ -1359,6 +1359,9 @@ declare const navStyles: {
1359
1359
  paddingHorizontal: number;
1360
1360
  paddingVertical: number;
1361
1361
  borderRadius: number;
1362
+ minHeight: number;
1363
+ minWidth: number;
1364
+ justifyContent: "center";
1362
1365
  };
1363
1366
  accountText: {
1364
1367
  fontWeight: "600";
@@ -1415,6 +1418,7 @@ declare const darkModeStyles: {
1415
1418
  paddingHorizontal: number;
1416
1419
  paddingVertical: number;
1417
1420
  minHeight: number;
1421
+ minWidth: number;
1418
1422
  flexDirection: "row";
1419
1423
  alignItems: "center";
1420
1424
  columnGap: number;
@@ -1476,6 +1480,7 @@ declare const expandableStyles: {
1476
1480
  flexDirection: "row";
1477
1481
  alignItems: "center";
1478
1482
  paddingVertical: number;
1483
+ minHeight: number;
1479
1484
  borderLeftWidth: number;
1480
1485
  borderLeftColor: string;
1481
1486
  };
@@ -1509,6 +1514,7 @@ declare const expandableStyles: {
1509
1514
  flexDirection: "row";
1510
1515
  alignItems: "center";
1511
1516
  paddingVertical: number;
1517
+ minHeight: number;
1512
1518
  };
1513
1519
  sectionHeader: {
1514
1520
  marginTop: number;
package/dist/index.js CHANGED
@@ -13,146 +13,7 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
13
  var React__default = /*#__PURE__*/_interopDefault(React);
14
14
 
15
15
  // src/Sidebar.tsx
16
- var IS_WEB = reactNative.Platform.OS === "web";
17
- var FOCUS_RING_WIDTH = 2;
18
- var FOCUS_RING_OFFSET = 2;
19
- var HOVER_TRANSITION_MS = 150;
20
- var TINT_TRANSITION_MS = 160;
21
- function focusRingStyle(focused, ringColor) {
22
- if (!IS_WEB || !focused) return void 0;
23
- const ring = {
24
- outlineWidth: FOCUS_RING_WIDTH,
25
- outlineStyle: "solid",
26
- outlineColor: ringColor,
27
- outlineOffset: FOCUS_RING_OFFSET
28
- };
29
- return ring;
30
- }
31
- function webTransition(properties, durationMs, reducedMotion) {
32
- if (!IS_WEB) return void 0;
33
- const transition = {
34
- transitionProperty: properties,
35
- transitionDuration: reducedMotion ? "0ms" : `${durationMs}ms`
36
- };
37
- return transition;
38
- }
39
- function hoverTransitionStyle(reducedMotion) {
40
- return webTransition("color, background-color", HOVER_TRANSITION_MS, reducedMotion);
41
- }
42
- function tintTransitionStyle(reducedMotion) {
43
- return webTransition("opacity", TINT_TRANSITION_MS, reducedMotion);
44
- }
45
- function rotateTransitionStyle(reducedMotion) {
46
- return webTransition("transform", TINT_TRANSITION_MS, reducedMotion);
47
- }
48
- var MIN_TARGET = 36;
49
- var styles = reactNative.StyleSheet.create({
50
- trigger: {
51
- flexDirection: "row",
52
- alignItems: "center",
53
- justifyContent: "space-between",
54
- columnGap: 8,
55
- paddingHorizontal: 12,
56
- minHeight: MIN_TARGET,
57
- borderWidth: 1,
58
- borderRadius: 8
59
- },
60
- label: { fontSize: 14 },
61
- badge: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, borderWidth: 1 },
62
- badgeText: { fontSize: 11, fontWeight: "700" }
63
- });
64
- var CommandPaletteTrigger = ({
65
- label,
66
- hint,
67
- shortcut,
68
- onPress,
69
- testID
70
- }) => {
71
- const { theme } = uiFeedback.useUi();
72
- const colors = theme.colors;
73
- const primary = theme.palette.primary["500"];
74
- const [focused, setFocused] = React__default.default.useState(false);
75
- return /* @__PURE__ */ jsxRuntime.jsxs(
76
- reactNative.Pressable,
77
- {
78
- accessibilityHint: hint,
79
- accessibilityLabel: label,
80
- accessibilityRole: "button",
81
- style: [
82
- styles.trigger,
83
- { backgroundColor: colors.surface, borderColor: colors.border },
84
- focusRingStyle(focused, primary)
85
- ],
86
- testID,
87
- onBlur: () => setFocused(false),
88
- onFocus: () => setFocused(true),
89
- onPress,
90
- children: [
91
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.label, { color: colors.textSecondary }], children: label }),
92
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles.badge, { borderColor: colors.border }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.badgeText, { color: colors.textSecondary }], children: shortcut }) })
93
- ]
94
- }
95
- );
96
- };
97
-
98
- // src/sidebarFilter.ts
99
- function searchTokens(query) {
100
- return query.trim().toLowerCase().split(/\s+/).filter(Boolean);
101
- }
102
- function labelMatches(item, tokens) {
103
- const label = item.label.toLowerCase();
104
- return tokens.every((token) => label.includes(token));
105
- }
106
- function filterNavItems(items, query) {
107
- const tokens = searchTokens(query);
108
- if (tokens.length === 0) return [...items];
109
- return filterByTokens(items, tokens);
110
- }
111
- function filterByTokens(items, tokens) {
112
- const kept = [];
113
- for (const item of items) {
114
- const selfMatch = labelMatches(item, tokens);
115
- const children = item.children ?? [];
116
- if (selfMatch) {
117
- kept.push(item);
118
- continue;
119
- }
120
- if (children.length === 0) continue;
121
- const prunedChildren = filterByTokens(children, tokens);
122
- if (prunedChildren.length > 0) kept.push({ ...item, children: prunedChildren });
123
- }
124
- return kept;
125
- }
126
- function isFilterActive(query) {
127
- return searchTokens(query).length > 0;
128
- }
129
-
130
- // src/isRouteActive.ts
131
- function isRouteActive(pathname, route, exact = false) {
132
- if (exact) return pathname === route;
133
- if (route === "/") return pathname === "/";
134
- return pathname === route || pathname.startsWith(`${route}/`);
135
- }
136
- function leafRoutes(items) {
137
- return items.flatMap((item) => {
138
- const children = item.children ?? [];
139
- const isGroup = children.length > 0;
140
- return isGroup ? leafRoutes(children) : [{ route: item.route, exact: item.exact === true }];
141
- });
142
- }
143
- function resolveActiveRoute(items, pathname) {
144
- let winner;
145
- let winnerLength = -1;
146
- for (const leaf of leafRoutes(items)) {
147
- const matches = isRouteActive(pathname, leaf.route, leaf.exact);
148
- const isLonger = leaf.route.length > winnerLength;
149
- if (matches && isLonger) {
150
- winner = leaf.route;
151
- winnerLength = leaf.route.length;
152
- }
153
- }
154
- return winner;
155
- }
16
+ var MIN_TOUCH_TARGET = 44;
156
17
  var ACTIVE_BORDER_RADIUS = 4;
157
18
  var ACTIVE_ACCENT_WIDTH = 3;
158
19
  var NAV_ROW_RADIUS = 8;
@@ -215,7 +76,15 @@ var navStyles = reactNative.StyleSheet.create({
215
76
  userBlock: { marginHorizontal: 12, alignItems: "flex-end" },
216
77
  userName: { fontWeight: "600" },
217
78
  userEmail: { fontSize: 12 },
218
- accountBtn: { marginLeft: 8, paddingHorizontal: 10, paddingVertical: 6, borderRadius: 4 },
79
+ accountBtn: {
80
+ marginLeft: 8,
81
+ paddingHorizontal: 10,
82
+ paddingVertical: 6,
83
+ borderRadius: 4,
84
+ minHeight: MIN_TOUCH_TARGET,
85
+ minWidth: MIN_TOUCH_TARGET,
86
+ justifyContent: "center"
87
+ },
219
88
  accountText: { fontWeight: "600" },
220
89
  // --- Rich account-state header (Move 1c) ---
221
90
  accountTenant: { fontSize: 12 },
@@ -251,8 +120,8 @@ var navBarStyles = reactNative.StyleSheet.create({
251
120
  paddingVertical: 6,
252
121
  borderRadius: 4,
253
122
  // a11y: keep the hamburger a comfortable ≥44×44 touch target.
254
- minWidth: 44,
255
- minHeight: 44,
123
+ minWidth: MIN_TOUCH_TARGET,
124
+ minHeight: MIN_TOUCH_TARGET,
256
125
  alignItems: "center",
257
126
  justifyContent: "center"
258
127
  },
@@ -279,13 +148,19 @@ var navBarStyles = reactNative.StyleSheet.create({
279
148
  overflowChevron: { fontSize: 12, fontWeight: "700" },
280
149
  // Collapsed (below breakpoint): links drop onto their own full-width line,
281
150
  // stacked vertically — mirrors the v1 `.gn-links` responsive behaviour.
151
+ // SHOWCASE-1: this is a ScrollView — `maxHeight` (viewport minus bar) is applied at render time,
152
+ // so a long menu scrolls within the viewport instead of running off a body:overflow:hidden page.
282
153
  linksStacked: {
283
154
  width: "100%",
284
155
  flexBasis: "100%",
156
+ flexGrow: 0,
157
+ flexShrink: 0,
158
+ marginTop: 8
159
+ },
160
+ linksStackedContent: {
285
161
  flexDirection: "column",
286
162
  alignItems: "stretch",
287
- rowGap: 2,
288
- marginTop: 8
163
+ rowGap: 2
289
164
  },
290
165
  link: {
291
166
  paddingVertical: 8,
@@ -294,7 +169,7 @@ var navBarStyles = reactNative.StyleSheet.create({
294
169
  flexDirection: "row",
295
170
  alignItems: "center",
296
171
  // a11y: comfortable ≥44 tall touch target (rounded-pill hover/active affordance).
297
- minHeight: 44,
172
+ minHeight: MIN_TOUCH_TARGET,
298
173
  justifyContent: "center"
299
174
  },
300
175
  linkStacked: { paddingVertical: 10 },
@@ -330,7 +205,7 @@ var pillNavStyles = reactNative.StyleSheet.create({
330
205
  paddingHorizontal: 16,
331
206
  paddingVertical: 8,
332
207
  borderRadius: 999,
333
- minHeight: 36,
208
+ minHeight: MIN_TOUCH_TARGET,
334
209
  flexDirection: "row",
335
210
  alignItems: "center",
336
211
  columnGap: 6,
@@ -351,7 +226,8 @@ var darkModeStyles = reactNative.StyleSheet.create({
351
226
  segment: {
352
227
  paddingHorizontal: 12,
353
228
  paddingVertical: 8,
354
- minHeight: 36,
229
+ minHeight: MIN_TOUCH_TARGET,
230
+ minWidth: MIN_TOUCH_TARGET,
355
231
  flexDirection: "row",
356
232
  alignItems: "center",
357
233
  columnGap: 6,
@@ -362,8 +238,8 @@ var darkModeStyles = reactNative.StyleSheet.create({
362
238
  cycle: {
363
239
  paddingHorizontal: 12,
364
240
  paddingVertical: 8,
365
- minHeight: 44,
366
- minWidth: 44,
241
+ minHeight: MIN_TOUCH_TARGET,
242
+ minWidth: MIN_TOUCH_TARGET,
367
243
  borderRadius: 8,
368
244
  flexDirection: "row",
369
245
  alignItems: "center",
@@ -408,6 +284,7 @@ var expandableStyles = reactNative.StyleSheet.create({
408
284
  flexDirection: "row",
409
285
  alignItems: "center",
410
286
  paddingVertical: 9,
287
+ minHeight: MIN_TOUCH_TARGET,
411
288
  borderLeftWidth: ACTIVE_ACCENT_WIDTH,
412
289
  borderLeftColor: "transparent"
413
290
  },
@@ -435,7 +312,8 @@ var expandableStyles = reactNative.StyleSheet.create({
435
312
  borderRadius: NAV_ROW_RADIUS,
436
313
  flexDirection: "row",
437
314
  alignItems: "center",
438
- paddingVertical: 8
315
+ paddingVertical: 8,
316
+ minHeight: MIN_TOUCH_TARGET
439
317
  },
440
318
  // Depth-0 module groups render as docs-style SECTION HEADERS: extra top rhythm
441
319
  // to separate one module from the next.
@@ -455,6 +333,145 @@ var expandableStyles = reactNative.StyleSheet.create({
455
333
  var BASE_INDENT = 12;
456
334
  var NAV_ICON_SIZE = 14;
457
335
  var CHEVRON_ICON_SIZE = 12;
336
+ var IS_WEB = reactNative.Platform.OS === "web";
337
+ var FOCUS_RING_WIDTH = 2;
338
+ var FOCUS_RING_OFFSET = 2;
339
+ var HOVER_TRANSITION_MS = 150;
340
+ var TINT_TRANSITION_MS = 160;
341
+ function focusRingStyle(focused, ringColor) {
342
+ if (!IS_WEB || !focused) return void 0;
343
+ const ring = {
344
+ outlineWidth: FOCUS_RING_WIDTH,
345
+ outlineStyle: "solid",
346
+ outlineColor: ringColor,
347
+ outlineOffset: FOCUS_RING_OFFSET
348
+ };
349
+ return ring;
350
+ }
351
+ function webTransition(properties, durationMs, reducedMotion) {
352
+ if (!IS_WEB) return void 0;
353
+ const transition = {
354
+ transitionProperty: properties,
355
+ transitionDuration: reducedMotion ? "0ms" : `${durationMs}ms`
356
+ };
357
+ return transition;
358
+ }
359
+ function hoverTransitionStyle(reducedMotion) {
360
+ return webTransition("color, background-color", HOVER_TRANSITION_MS, reducedMotion);
361
+ }
362
+ function tintTransitionStyle(reducedMotion) {
363
+ return webTransition("opacity", TINT_TRANSITION_MS, reducedMotion);
364
+ }
365
+ function rotateTransitionStyle(reducedMotion) {
366
+ return webTransition("transform", TINT_TRANSITION_MS, reducedMotion);
367
+ }
368
+ var styles = reactNative.StyleSheet.create({
369
+ trigger: {
370
+ flexDirection: "row",
371
+ alignItems: "center",
372
+ justifyContent: "space-between",
373
+ columnGap: 8,
374
+ paddingHorizontal: 12,
375
+ minHeight: MIN_TOUCH_TARGET,
376
+ borderWidth: 1,
377
+ borderRadius: 8
378
+ },
379
+ label: { fontSize: 14 },
380
+ badge: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, borderWidth: 1 },
381
+ badgeText: { fontSize: 11, fontWeight: "700" }
382
+ });
383
+ var CommandPaletteTrigger = ({
384
+ label,
385
+ hint,
386
+ shortcut,
387
+ onPress,
388
+ testID
389
+ }) => {
390
+ const { theme } = uiFeedback.useUi();
391
+ const colors = theme.colors;
392
+ const primary = theme.palette.primary["500"];
393
+ const [focused, setFocused] = React__default.default.useState(false);
394
+ return /* @__PURE__ */ jsxRuntime.jsxs(
395
+ reactNative.Pressable,
396
+ {
397
+ accessibilityHint: hint,
398
+ accessibilityLabel: label,
399
+ accessibilityRole: "button",
400
+ style: [
401
+ styles.trigger,
402
+ { backgroundColor: colors.surface, borderColor: colors.border },
403
+ focusRingStyle(focused, primary)
404
+ ],
405
+ testID,
406
+ onBlur: () => setFocused(false),
407
+ onFocus: () => setFocused(true),
408
+ onPress,
409
+ children: [
410
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.label, { color: colors.textSecondary }], children: label }),
411
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles.badge, { borderColor: colors.border }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.badgeText, { color: colors.textSecondary }], children: shortcut }) })
412
+ ]
413
+ }
414
+ );
415
+ };
416
+
417
+ // src/sidebarFilter.ts
418
+ function searchTokens(query) {
419
+ return query.trim().toLowerCase().split(/\s+/).filter(Boolean);
420
+ }
421
+ function labelMatches(item, tokens) {
422
+ const label = item.label.toLowerCase();
423
+ return tokens.every((token) => label.includes(token));
424
+ }
425
+ function filterNavItems(items, query) {
426
+ const tokens = searchTokens(query);
427
+ if (tokens.length === 0) return [...items];
428
+ return filterByTokens(items, tokens);
429
+ }
430
+ function filterByTokens(items, tokens) {
431
+ const kept = [];
432
+ for (const item of items) {
433
+ const selfMatch = labelMatches(item, tokens);
434
+ const children = item.children ?? [];
435
+ if (selfMatch) {
436
+ kept.push(item);
437
+ continue;
438
+ }
439
+ if (children.length === 0) continue;
440
+ const prunedChildren = filterByTokens(children, tokens);
441
+ if (prunedChildren.length > 0) kept.push({ ...item, children: prunedChildren });
442
+ }
443
+ return kept;
444
+ }
445
+ function isFilterActive(query) {
446
+ return searchTokens(query).length > 0;
447
+ }
448
+
449
+ // src/isRouteActive.ts
450
+ function isRouteActive(pathname, route, exact = false) {
451
+ if (exact) return pathname === route;
452
+ if (route === "/") return pathname === "/";
453
+ return pathname === route || pathname.startsWith(`${route}/`);
454
+ }
455
+ function leafRoutes(items) {
456
+ return items.flatMap((item) => {
457
+ const children = item.children ?? [];
458
+ const isGroup = children.length > 0;
459
+ return isGroup ? leafRoutes(children) : [{ route: item.route, exact: item.exact === true }];
460
+ });
461
+ }
462
+ function resolveActiveRoute(items, pathname) {
463
+ let winner;
464
+ let winnerLength = -1;
465
+ for (const leaf of leafRoutes(items)) {
466
+ const matches = isRouteActive(pathname, leaf.route, leaf.exact);
467
+ const isLonger = leaf.route.length > winnerLength;
468
+ if (matches && isLonger) {
469
+ winner = leaf.route;
470
+ winnerLength = leaf.route.length;
471
+ }
472
+ }
473
+ return winner;
474
+ }
458
475
  var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
459
476
  function getReducedMotionQuery() {
460
477
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return void 0;
@@ -652,7 +669,6 @@ function resolveSearchAffordance(viewport, breakpoint = DEFAULT_SEARCH_BREAKPOIN
652
669
  }
653
670
  var FIELD_MIN_HEIGHT = 38;
654
671
  var CLEAR_GLYPH = "\u2715";
655
- var CLEAR_MIN_TARGET = 28;
656
672
  var styles2 = reactNative.StyleSheet.create({
657
673
  wrap: {
658
674
  flexDirection: "row",
@@ -670,8 +686,8 @@ var styles2 = reactNative.StyleSheet.create({
670
686
  minHeight: FIELD_MIN_HEIGHT
671
687
  },
672
688
  clear: {
673
- minWidth: CLEAR_MIN_TARGET,
674
- minHeight: CLEAR_MIN_TARGET,
689
+ minWidth: MIN_TOUCH_TARGET,
690
+ minHeight: MIN_TOUCH_TARGET,
675
691
  alignItems: "center",
676
692
  justifyContent: "center",
677
693
  borderRadius: 6
@@ -1174,9 +1190,24 @@ function useNavOverflow(itemCount, gap) {
1174
1190
  );
1175
1191
  return { visibleCount, setAvailableWidth, setItemWidth, setMoreWidth };
1176
1192
  }
1193
+
1194
+ // src/stackedMenuMaxHeight.ts
1195
+ var DEFAULT_BAR_HEIGHT = 64;
1196
+ var MIN_STACKED_MENU_HEIGHT = 3 * 44;
1197
+ function computeStackedMenuMaxHeight({
1198
+ windowHeight,
1199
+ barHeight,
1200
+ reservedBelow
1201
+ }) {
1202
+ const bar = barHeight > 0 ? barHeight : DEFAULT_BAR_HEIGHT;
1203
+ const reserved = Math.max(0, reservedBelow);
1204
+ return Math.max(MIN_STACKED_MENU_HEIGHT, windowHeight - bar - reserved);
1205
+ }
1177
1206
  var LINKS_REGION_ID = "navbar-links-region";
1178
1207
  var DEFAULT_COLLAPSE_BELOW = 760;
1179
1208
  var MENU_GLYPH = "\u2630";
1209
+ var STACKED_MENU_CHROME = 12;
1210
+ var STACKED_RIGHT_CHROME = 8;
1180
1211
  function ariaControlsProps(id) {
1181
1212
  return { "aria-controls": id };
1182
1213
  }
@@ -1200,13 +1231,22 @@ var NavBarInner = ({
1200
1231
  const { theme } = uiFeedback.useUi();
1201
1232
  const colors = theme.colors;
1202
1233
  const primaryColor = theme.palette.primary["500"];
1203
- const { width } = reactNative.useWindowDimensions();
1234
+ const { width, height } = reactNative.useWindowDimensions();
1204
1235
  const reducedMotion = useReducedMotion();
1205
1236
  const [open, setOpen] = React.useState(false);
1206
1237
  const [toggleFocused, setToggleFocused] = React.useState(false);
1207
1238
  const { visibleCount, setAvailableWidth, setItemWidth, setMoreWidth } = useNavOverflow(items.length, NAV_LINK_GAP);
1208
1239
  const collapsed = width < collapseBelow;
1209
1240
  const showLinks = !collapsed || open;
1241
+ const [barHeight, setBarHeight] = React.useState(0);
1242
+ const [rightHeight, setRightHeight] = React.useState(0);
1243
+ const hasRight = right !== void 0;
1244
+ const stackedMaxHeight = computeStackedMenuMaxHeight({
1245
+ windowHeight: height,
1246
+ barHeight,
1247
+ reservedBelow: STACKED_MENU_CHROME + (hasRight ? STACKED_RIGHT_CHROME + rightHeight : 0)
1248
+ });
1249
+ const measureBar = collapsed && !open;
1210
1250
  const innerCapStyle = React.useMemo(
1211
1251
  () => contentMaxWidth === void 0 ? null : { maxWidth: contentMaxWidth, width: "100%", alignSelf: "center" },
1212
1252
  [contentMaxWidth]
@@ -1241,6 +1281,7 @@ var NavBarInner = ({
1241
1281
  { backgroundColor: colors.surface, borderBottomColor: colors.border },
1242
1282
  containerStyle
1243
1283
  ],
1284
+ onLayout: measureBar ? (event) => setBarHeight(event.nativeEvent.layout.height) : void 0,
1244
1285
  children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [navBarStyles.inner, collapsed ? null : navBarStyles.innerNoWrap, innerCapStyle], children: [
1245
1286
  brand !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: navBarStyles.brand, children: renderTextSlot(brand, { color: colors.text }) }) : null,
1246
1287
  collapsed ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -1275,7 +1316,16 @@ var NavBarInner = ({
1275
1316
  }
1276
1317
  );
1277
1318
  if (collapsed) {
1278
- 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(React__default.default.Fragment, { children: renderLink(item) }, item.key)) });
1319
+ return /* @__PURE__ */ jsxRuntime.jsx(
1320
+ reactNative.ScrollView,
1321
+ {
1322
+ contentContainerStyle: navBarStyles.linksStackedContent,
1323
+ nativeID: LINKS_REGION_ID,
1324
+ style: [navBarStyles.linksStacked, { maxHeight: stackedMaxHeight }],
1325
+ testID: NAV_TEST_IDS.navBarLinks,
1326
+ children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(React__default.default.Fragment, { children: renderLink(item) }, item.key))
1327
+ }
1328
+ );
1279
1329
  }
1280
1330
  const visibleItems = items.slice(0, visibleCount);
1281
1331
  const overflowItems = items.slice(visibleCount);
@@ -1319,7 +1369,14 @@ var NavBarInner = ({
1319
1369
  }
1320
1370
  );
1321
1371
  })() : null,
1322
- showLinks && right !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsed ? navBarStyles.rightStacked : navBarStyles.right, children: renderTextSlot(right, { color: colors.text }) }) : null
1372
+ showLinks && hasRight ? /* @__PURE__ */ jsxRuntime.jsx(
1373
+ reactNative.View,
1374
+ {
1375
+ style: collapsed ? navBarStyles.rightStacked : navBarStyles.right,
1376
+ onLayout: collapsed ? (event) => setRightHeight(event.nativeEvent.layout.height) : void 0,
1377
+ children: renderTextSlot(right, { color: colors.text })
1378
+ }
1379
+ ) : null
1323
1380
  ] })
1324
1381
  }
1325
1382
  );
@@ -1401,7 +1458,6 @@ function useContentBody(state, children, testID) {
1401
1458
  }
1402
1459
  var SCRIM_COLOR = "rgba(0, 0, 0, 0.5)";
1403
1460
  var MENU_GLYPH2 = "\u2630";
1404
- var MENU_MIN_TARGET = 44;
1405
1461
  var MENU_GLYPH_FONT_SIZE = 20;
1406
1462
  var MENU_TOGGLE_PADDING = 12;
1407
1463
  var DRAWER_Z_INDEX = 50;
@@ -1439,8 +1495,8 @@ var styles4 = reactNative.StyleSheet.create({
1439
1495
  // and receive presses instead of the scrim swallowing them.
1440
1496
  panel: { position: "absolute", top: 0, bottom: 0, left: 0, width: DRAWER_WIDTH, zIndex: PANEL_Z_INDEX },
1441
1497
  menuToggle: {
1442
- minWidth: MENU_MIN_TARGET,
1443
- minHeight: MENU_MIN_TARGET,
1498
+ minWidth: MIN_TOUCH_TARGET,
1499
+ minHeight: MIN_TOUCH_TARGET,
1444
1500
  alignItems: "center",
1445
1501
  justifyContent: "center",
1446
1502
  paddingHorizontal: MENU_TOGGLE_PADDING