@dloizides/ui-nav 1.26.2 → 1.28.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/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
2
- import { StyleSheet, Platform, Pressable, Text, View, TextInput, useWindowDimensions, ScrollView, ActivityIndicator } from 'react-native';
2
+ import { StyleSheet, Platform, Pressable, Text, View, TextInput, useWindowDimensions, ScrollView, ActivityIndicator, Animated } from 'react-native';
3
3
  import { useUi, UiProvider } from '@dloizides/ui-feedback';
4
4
  import { jsxs, jsx } from 'react/jsx-runtime';
5
- import { Collapse } from '@dloizides/ui-motion';
5
+ import { Collapse, motionDurations, motionEasings } from '@dloizides/ui-motion';
6
6
  import { LAYOUT_COLLAPSE_BREAKPOINT, ModalDropdown, DropdownVariant, SelectedMarker } from '@dloizides/ui-layout';
7
7
  import { Z_INDEX } from '@dloizides/design-tokens';
8
8
  import { resolveAccessibleRoutes } from '@dloizides/auth-web';
@@ -897,7 +897,8 @@ var APP_SHELL_SUFFIX = {
897
897
  pending: "-pending",
898
898
  loading: "-loading",
899
899
  error: "-error",
900
- forbidden: "-forbidden"
900
+ forbidden: "-forbidden",
901
+ band: "-backdrop-band"
901
902
  };
902
903
  var FocusableTouchable = ({
903
904
  onPress,
@@ -1051,10 +1052,88 @@ var Topbar = ({
1051
1052
  }
1052
1053
  );
1053
1054
  };
1055
+
1056
+ // src/NavHoverFeedback.ts
1057
+ var NavHoverFeedback = /* @__PURE__ */ ((NavHoverFeedback2) => {
1058
+ NavHoverFeedback2["Default"] = "default";
1059
+ NavHoverFeedback2["TintPill"] = "tint-pill";
1060
+ NavHoverFeedback2["Underline"] = "underline";
1061
+ NavHoverFeedback2["Lift"] = "lift";
1062
+ NavHoverFeedback2["SlidingHighlight"] = "sliding-highlight";
1063
+ return NavHoverFeedback2;
1064
+ })(NavHoverFeedback || {});
1065
+
1066
+ // src/feedback/navLinkFeedback.ts
1067
+ var DEFAULT_FADE_MS = 150;
1068
+ var PRESS_SCALE = 0.97;
1069
+ var PRESS_NUDGE_PX = 1;
1070
+ var LIFT_PX = -2;
1071
+ var PILL_KINDS = /* @__PURE__ */ new Set([
1072
+ "default" /* Default */,
1073
+ "tint-pill" /* TintPill */,
1074
+ "lift" /* Lift */
1075
+ ]);
1076
+ function pillBackground(kind, isHovered, active, colors) {
1077
+ if (!PILL_KINDS.has(kind)) return void 0;
1078
+ if (active) return colors.activeBg;
1079
+ return isHovered ? colors.hoverBg : void 0;
1080
+ }
1081
+ function linkTextColor(kind, isHovered, active, colors) {
1082
+ if (active) return PILL_KINDS.has(kind) ? colors.onPrimary : colors.activeBg;
1083
+ return isHovered ? colors.hoverText : colors.rest;
1084
+ }
1085
+ function pressTransform(kind, pressed, isHovered) {
1086
+ if (kind === "tint-pill" /* TintPill */) return pressed ? [{ scale: PRESS_SCALE }] : void 0;
1087
+ if (kind === "underline" /* Underline */) return pressed ? [{ translateY: PRESS_NUDGE_PX }] : void 0;
1088
+ if (kind !== "lift" /* Lift */) return void 0;
1089
+ if (pressed) return [{ translateY: 0 }];
1090
+ return isHovered ? [{ translateY: LIFT_PX }] : void 0;
1091
+ }
1092
+ function feedbackDuration(kind) {
1093
+ if (kind === "default" /* Default */) return DEFAULT_FADE_MS;
1094
+ return kind === "tint-pill" /* TintPill */ ? motionDurations.fast : motionDurations.base;
1095
+ }
1096
+ function resolveLinkFeedback(kind, state, colors, reducedMotion) {
1097
+ const isHovered = state.hovered && !state.active;
1098
+ const underlined = state.active || isHovered;
1099
+ return {
1100
+ background: pillBackground(kind, isHovered, state.active, colors),
1101
+ textColor: linkTextColor(kind, isHovered, state.active, colors),
1102
+ transform: reducedMotion ? void 0 : pressTransform(kind, state.pressed, isHovered),
1103
+ shadow: kind === "lift" /* Lift */ && isHovered,
1104
+ underline: kind === "underline" /* Underline */ ? { color: colors.activeBg, scale: underlined ? 1 : 0 } : null,
1105
+ durationMs: reducedMotion ? motionDurations.instant : feedbackDuration(kind)
1106
+ };
1107
+ }
1054
1108
  var TEXT_ON_PRIMARY2 = "#ffffff";
1055
1109
  function ariaCurrentProps2(isActive) {
1056
1110
  return isActive ? { "aria-current": "page" } : {};
1057
1111
  }
1112
+ var FEEDBACK_PROPERTIES = "color, background-color, transform, box-shadow";
1113
+ var UNDERLINE_INSET = 14;
1114
+ var UNDERLINE_HEIGHT = 2;
1115
+ var SHADOW_OPACITY = 0.18;
1116
+ var SHADOW_RADIUS = 6;
1117
+ var SHADOW_OFFSET = { width: 0, height: 2 };
1118
+ function feedbackStyle(kind, feedback, shadowColor, reducedMotion) {
1119
+ const background = feedback.background === void 0 ? {} : { backgroundColor: feedback.background };
1120
+ const shadow = feedback.shadow ? { shadowColor, shadowOpacity: SHADOW_OPACITY, shadowRadius: SHADOW_RADIUS, shadowOffset: SHADOW_OFFSET } : {};
1121
+ const transition = kind === "default" /* Default */ ? hoverTransitionStyle(reducedMotion) : webTransition(FEEDBACK_PROPERTIES, feedback.durationMs, reducedMotion);
1122
+ return [background, shadow, { transform: feedback.transform }, transition ?? {}];
1123
+ }
1124
+ function underlineStyle(feedback, reducedMotion) {
1125
+ if (feedback.underline === null) return null;
1126
+ const line = {
1127
+ position: "absolute",
1128
+ left: UNDERLINE_INSET,
1129
+ right: UNDERLINE_INSET,
1130
+ bottom: 0,
1131
+ height: UNDERLINE_HEIGHT,
1132
+ backgroundColor: feedback.underline.color,
1133
+ transform: [{ scaleX: feedback.underline.scale }]
1134
+ };
1135
+ return [line, webTransition("transform", feedback.durationMs, reducedMotion) ?? {}];
1136
+ }
1058
1137
  var NavBarLink = ({
1059
1138
  item,
1060
1139
  isActive,
@@ -1063,13 +1142,20 @@ var NavBarLink = ({
1063
1142
  reducedMotion,
1064
1143
  colors,
1065
1144
  navigateHint,
1066
- onPress
1145
+ onPress,
1146
+ hoverFeedback = "default" /* Default */,
1147
+ onHoverChange
1067
1148
  }) => {
1068
1149
  const [hovered, setHovered] = useState(false);
1069
1150
  const [focused, setFocused] = useState(false);
1070
- const isHovered = hovered && !isActive;
1071
- const textColor = isActive ? TEXT_ON_PRIMARY2 : isHovered ? colors.hoverText : colors.rest;
1072
- const pillStyle = isActive ? { backgroundColor: colors.activeBg } : isHovered ? { backgroundColor: colors.hoverBg } : void 0;
1151
+ const linkColors = { ...colors, onPrimary: TEXT_ON_PRIMARY2 };
1152
+ const resolve = (pressed) => resolveLinkFeedback(hoverFeedback, { hovered, pressed, active: isActive }, linkColors, reducedMotion);
1153
+ const textColor = resolve(false).textColor;
1154
+ const underline = underlineStyle(resolve(false), reducedMotion);
1155
+ const setHover = (next) => {
1156
+ setHovered(next);
1157
+ onHoverChange?.(item.key, next);
1158
+ };
1073
1159
  return /* @__PURE__ */ jsxs(
1074
1160
  Pressable,
1075
1161
  {
@@ -1077,27 +1163,61 @@ var NavBarLink = ({
1077
1163
  accessibilityLabel: item.label,
1078
1164
  accessibilityRole: "link",
1079
1165
  accessibilityState: { selected: isActive },
1080
- style: [
1166
+ style: ({ pressed }) => [
1081
1167
  navBarStyles.link,
1082
1168
  collapsed ? navBarStyles.linkStacked : void 0,
1083
- pillStyle,
1084
- hoverTransitionStyle(reducedMotion),
1169
+ ...feedbackStyle(hoverFeedback, resolve(pressed), colors.rest, reducedMotion),
1085
1170
  focusRingStyle(focused, colors.ring)
1086
1171
  ],
1087
1172
  testID: item.testID ?? item.key,
1088
1173
  onBlur: () => setFocused(false),
1089
1174
  onFocus: () => setFocused(true),
1090
- onHoverIn: () => setHovered(true),
1091
- onHoverOut: () => setHovered(false),
1175
+ onHoverIn: () => setHover(true),
1176
+ onHoverOut: () => setHover(false),
1092
1177
  onPress: () => onPress(item.route),
1093
1178
  ...ariaCurrentProps2(isActive),
1094
1179
  children: [
1095
1180
  typeof item.renderIcon === "function" ? /* @__PURE__ */ jsx(View, { style: navBarStyles.linkIcon, children: item.renderIcon(textColor, iconSize) }) : null,
1181
+ underline === null ? null : /* @__PURE__ */ jsx(View, { pointerEvents: "none", style: underline }),
1096
1182
  /* @__PURE__ */ jsx(Text, { style: [navBarStyles.linkText, { color: textColor }], children: item.label })
1097
1183
  ]
1098
1184
  }
1099
1185
  );
1100
1186
  };
1187
+ var HIGHLIGHT_RADIUS = 8;
1188
+ var SLIDE_PROPERTIES = "left, top, width, height";
1189
+ function slideHighlightRect(layouts, activeKey, hoveredKey) {
1190
+ const hoveredRect = hoveredKey === null ? void 0 : layouts[hoveredKey];
1191
+ const activeRect = activeKey === void 0 ? void 0 : layouts[activeKey];
1192
+ return hoveredRect ?? activeRect ?? null;
1193
+ }
1194
+ function useSlidingHighlight(enabled, activeKey, color, reducedMotion) {
1195
+ const [layouts, setLayouts] = useState({});
1196
+ const [hoveredKey, setHoveredKey] = useState(null);
1197
+ const measure = useCallback(
1198
+ (key, layout) => {
1199
+ if (enabled) setLayouts((prev) => ({ ...prev, [key]: layout }));
1200
+ },
1201
+ [enabled]
1202
+ );
1203
+ const hover = useCallback((key, hovered) => {
1204
+ setHoveredKey((prev) => hovered ? key : prev === key ? null : prev);
1205
+ }, []);
1206
+ const rect = enabled ? slideHighlightRect(layouts, activeKey, hoveredKey) : null;
1207
+ const style = rect === null ? null : [
1208
+ {
1209
+ position: "absolute",
1210
+ left: rect.x,
1211
+ top: rect.y,
1212
+ width: rect.width,
1213
+ height: rect.height,
1214
+ borderRadius: HIGHLIGHT_RADIUS,
1215
+ backgroundColor: color
1216
+ },
1217
+ webTransition(SLIDE_PROPERTIES, motionDurations.base, reducedMotion)
1218
+ ];
1219
+ return { style, measure, hover };
1220
+ }
1101
1221
  var TEXT_ON_PRIMARY3 = "#ffffff";
1102
1222
  var NO_ACTIVE_ROUTE = "";
1103
1223
  var OVERFLOW_MENU_MIN_WIDTH = 200;
@@ -1238,7 +1358,8 @@ var NavBarInner = ({
1238
1358
  overflowLabel = "More",
1239
1359
  overflowHint = "",
1240
1360
  containerStyle,
1241
- contentMaxWidth
1361
+ contentMaxWidth,
1362
+ hoverFeedback = "default" /* Default */
1242
1363
  }) => {
1243
1364
  const { theme } = useUi();
1244
1365
  const colors = theme.colors;
@@ -1272,6 +1393,12 @@ var NavBarInner = ({
1272
1393
  [onNavigate]
1273
1394
  );
1274
1395
  const activeRoute = resolveActiveRoute(items, pathname);
1396
+ const slide = useSlidingHighlight(
1397
+ hoverFeedback === "sliding-highlight" /* SlidingHighlight */,
1398
+ items.find((item) => item.route === activeRoute)?.key,
1399
+ colors.surfaceElevated,
1400
+ reducedMotion
1401
+ );
1275
1402
  const linkColors = useMemo(
1276
1403
  () => ({
1277
1404
  rest: colors.textSecondary,
@@ -1321,9 +1448,11 @@ var NavBarInner = ({
1321
1448
  colors: linkColors,
1322
1449
  iconSize: NAV_ICON_SIZE,
1323
1450
  isActive: item.route === activeRoute,
1451
+ hoverFeedback,
1324
1452
  item,
1325
1453
  navigateHint,
1326
1454
  reducedMotion,
1455
+ onHoverChange: slide.hover,
1327
1456
  onPress: handlePress
1328
1457
  }
1329
1458
  );
@@ -1349,11 +1478,15 @@ var NavBarInner = ({
1349
1478
  testID: NAV_TEST_IDS.navBarLinks,
1350
1479
  onLayout: (event) => setAvailableWidth(event.nativeEvent.layout.width),
1351
1480
  children: [
1481
+ slide.style === null ? null : /* @__PURE__ */ jsx(View, { pointerEvents: "none", style: slide.style }),
1352
1482
  visibleItems.map((item, index) => /* @__PURE__ */ jsx(
1353
1483
  View,
1354
1484
  {
1355
1485
  style: navBarStyles.linkCell,
1356
- onLayout: (event) => setItemWidth(index, event.nativeEvent.layout.width),
1486
+ onLayout: (event) => {
1487
+ setItemWidth(index, event.nativeEvent.layout.width);
1488
+ slide.measure(item.key, event.nativeEvent.layout);
1489
+ },
1357
1490
  children: renderLink(item)
1358
1491
  },
1359
1492
  item.key
@@ -1404,13 +1537,53 @@ var Nav = (props) => {
1404
1537
  }
1405
1538
  return /* @__PURE__ */ jsx(NavBar, { ...props });
1406
1539
  };
1540
+ var RAIL_FULL_BREAKPOINT = LAYOUT_COLLAPSE_BREAKPOINT;
1541
+ var DEFAULT_COLLAPSED_RAIL_MAX = 1024;
1542
+ function resolveRailMode({ viewport, hasSidebar, hasCollapsed, range }) {
1543
+ if (!hasSidebar) return "none";
1544
+ if (hasCollapsed) {
1545
+ if (viewport >= range.max) return "full";
1546
+ if (viewport >= range.min) return "collapsed";
1547
+ return "drawer";
1548
+ }
1549
+ return viewport >= RAIL_FULL_BREAKPOINT ? "full" : "drawer";
1550
+ }
1551
+ var styles3 = StyleSheet.create({
1552
+ band: { left: 0, position: "absolute", right: 0, top: 0 }
1553
+ });
1554
+ function readBackdrop(theme) {
1555
+ return theme.backdrop;
1556
+ }
1557
+ function rootStyle(backdrop, fallback) {
1558
+ if (backdrop === void 0) return { backgroundColor: fallback };
1559
+ const base = { backgroundColor: backdrop.color };
1560
+ if (!IS_WEB || backdrop.image === null) return base;
1561
+ const web = { backgroundImage: backdrop.image, backgroundSize: backdrop.imageSize ?? "auto" };
1562
+ return { ...base, ...web };
1563
+ }
1564
+ function resolveShellBackdrop(theme, viewport, contentPadding, testID) {
1565
+ const backdrop = readBackdrop(theme);
1566
+ const band = backdrop?.band ?? null;
1567
+ const root = rootStyle(backdrop, theme.colors.background);
1568
+ if (band === null) return { root, chrome: void 0, chromeText: theme.colors.text, band: null };
1569
+ const overlap = viewport < RAIL_FULL_BREAKPOINT ? band.overlap.compact : band.overlap.wide;
1570
+ const element = /* @__PURE__ */ jsx(
1571
+ View,
1572
+ {
1573
+ pointerEvents: "none",
1574
+ style: [styles3.band, { backgroundColor: band.color, height: contentPadding + overlap }],
1575
+ testID: `${testID}${APP_SHELL_SUFFIX.band}`
1576
+ }
1577
+ );
1578
+ return { root, chrome: { backgroundColor: band.color }, chromeText: band.text, band: element };
1579
+ }
1407
1580
  var CARD_PADDING = 20;
1408
1581
  var CARD_BORDER_RADIUS = 12;
1409
1582
  var CARD_BORDER_WIDTH = 1;
1410
1583
  var CARD_TITLE_FONT_SIZE = 16;
1411
1584
  var CARD_MESSAGE_FONT_SIZE = 14;
1412
1585
  var CARD_TITLE_MARGIN_BOTTOM = 8;
1413
- var styles3 = StyleSheet.create({
1586
+ var styles4 = StyleSheet.create({
1414
1587
  // Loading is a TRANSITIONAL state: the spinner is an absolute overlay ON TOP of
1415
1588
  // stably-mounted children, never a replacement for them. Replacing children here
1416
1589
  // would unmount whatever they host (e.g. a navigator), and on remount React
@@ -1442,9 +1615,9 @@ function MessageCard({
1442
1615
  }) {
1443
1616
  const { theme } = useUi();
1444
1617
  const colors = theme.colors;
1445
- return /* @__PURE__ */ jsxs(View, { style: [styles3.card, { backgroundColor: colors.surface, borderColor: accentColor }], testID, children: [
1446
- /* @__PURE__ */ jsx(Text, { style: [styles3.cardTitle, { color: accentColor }], children: message.titleText }),
1447
- /* @__PURE__ */ jsx(Text, { style: [styles3.cardMessage, { color: colors.textSecondary }], children: message.messageText })
1618
+ return /* @__PURE__ */ jsxs(View, { style: [styles4.card, { backgroundColor: colors.surface, borderColor: accentColor }], testID, children: [
1619
+ /* @__PURE__ */ jsx(Text, { style: [styles4.cardTitle, { color: accentColor }], children: message.titleText }),
1620
+ /* @__PURE__ */ jsx(Text, { style: [styles4.cardMessage, { color: colors.textSecondary }], children: message.messageText })
1448
1621
  ] });
1449
1622
  }
1450
1623
  function useContentBody(state, children, testID) {
@@ -1456,12 +1629,12 @@ function useContentBody(state, children, testID) {
1456
1629
  return /* @__PURE__ */ jsx(MessageCard, { accentColor: errorColor, message: state.forbidden, testID: `${testID}${APP_SHELL_SUFFIX.forbidden}` });
1457
1630
  if (state.error)
1458
1631
  return /* @__PURE__ */ jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
1459
- return /* @__PURE__ */ jsxs(View, { style: styles3.loadingHost, children: [
1632
+ return /* @__PURE__ */ jsxs(View, { style: styles4.loadingHost, children: [
1460
1633
  children,
1461
1634
  state.loading === true ? /* @__PURE__ */ jsx(
1462
1635
  View,
1463
1636
  {
1464
- style: [styles3.loadingOverlay, { backgroundColor: theme.colors.background }],
1637
+ style: [styles4.loadingOverlay, { backgroundColor: theme.colors.background }],
1465
1638
  testID: `${testID}${APP_SHELL_SUFFIX.loading}`,
1466
1639
  children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" })
1467
1640
  }
@@ -1488,7 +1661,7 @@ var DEFAULT_DRAWER_LABELS = {
1488
1661
  closeLabel: "Close menu",
1489
1662
  closeHint: "Close the navigation menu"
1490
1663
  };
1491
- var styles4 = StyleSheet.create({
1664
+ var styles5 = StyleSheet.create({
1492
1665
  overlay: { ...StyleSheet.absoluteFillObject, zIndex: DRAWER_Z_INDEX },
1493
1666
  // The scrim's tap-to-close hit area is anchored to the RIGHT of the panel
1494
1667
  // (`left: DRAWER_WIDTH`), so it NEVER overlaps the panel: nav-item taps always
@@ -1523,10 +1696,10 @@ var MenuToggle = ({ label, hint, onOpen, testID }) => {
1523
1696
  accessibilityHint: hint,
1524
1697
  accessibilityLabel: label,
1525
1698
  ringColor: theme.palette.primary["500"],
1526
- style: styles4.menuToggle,
1699
+ style: styles5.menuToggle,
1527
1700
  testID,
1528
1701
  onPress: onOpen,
1529
- children: /* @__PURE__ */ jsx(Text, { style: [styles4.menuToggleGlyph, { color: theme.colors.text }], children: MENU_GLYPH2 })
1702
+ children: /* @__PURE__ */ jsx(Text, { style: [styles5.menuToggleGlyph, { color: theme.colors.text }], children: MENU_GLYPH2 })
1530
1703
  }
1531
1704
  );
1532
1705
  };
@@ -1552,14 +1725,14 @@ var MobileDrawer = ({
1552
1725
  node.addEventListener("click", handleClick);
1553
1726
  return () => node.removeEventListener("click", handleClick);
1554
1727
  }, [onClose]);
1555
- return /* @__PURE__ */ jsxs(View, { style: styles4.overlay, children: [
1728
+ return /* @__PURE__ */ jsxs(View, { style: styles5.overlay, children: [
1556
1729
  /* @__PURE__ */ jsx(
1557
1730
  Pressable,
1558
1731
  {
1559
1732
  accessibilityHint: labels.closeHint,
1560
1733
  accessibilityLabel: labels.closeLabel,
1561
1734
  accessibilityRole: "button",
1562
- style: styles4.scrim,
1735
+ style: styles5.scrim,
1563
1736
  testID: scrimTestID,
1564
1737
  onPress: onClose
1565
1738
  }
@@ -1570,24 +1743,13 @@ var MobileDrawer = ({
1570
1743
  ref: panelRef,
1571
1744
  "aria-modal": true,
1572
1745
  role: "dialog",
1573
- style: [styles4.panel, { backgroundColor: theme.colors.surface }],
1746
+ style: [styles5.panel, { backgroundColor: theme.colors.surface }],
1574
1747
  testID: drawerTestID,
1575
1748
  children: renderTextSlot(sidebar, { color: theme.colors.text })
1576
1749
  }
1577
1750
  )
1578
1751
  ] });
1579
1752
  };
1580
- var RAIL_FULL_BREAKPOINT = LAYOUT_COLLAPSE_BREAKPOINT;
1581
- var DEFAULT_COLLAPSED_RAIL_MAX = 1024;
1582
- function resolveRailMode({ viewport, hasSidebar, hasCollapsed, range }) {
1583
- if (!hasSidebar) return "none";
1584
- if (hasCollapsed) {
1585
- if (viewport >= range.max) return "full";
1586
- if (viewport >= range.min) return "collapsed";
1587
- return "drawer";
1588
- }
1589
- return viewport >= RAIL_FULL_BREAKPOINT ? "full" : "drawer";
1590
- }
1591
1753
  function resolveContentMaxWidth(width, viewport) {
1592
1754
  if (width === "full") return "full";
1593
1755
  const wideMinViewport = width.wideMinViewport ?? Number.POSITIVE_INFINITY;
@@ -1599,7 +1761,7 @@ function useContentMaxWidth(width) {
1599
1761
  return resolveContentMaxWidth(width, viewport);
1600
1762
  }
1601
1763
  var DEFAULT_CONTENT_PADDING = 24;
1602
- var styles5 = StyleSheet.create({
1764
+ var styles6 = StyleSheet.create({
1603
1765
  root: { flex: 1 },
1604
1766
  // Auth-pending spinner: an opaque absolute overlay over the mounted shell, so
1605
1767
  // `children` (and any navigator they host) are never unmounted while the gate
@@ -1639,6 +1801,7 @@ var AppShell = ({
1639
1801
  const maxWidth = useContentMaxWidth(width);
1640
1802
  const body = useContentBody(state, children, testID);
1641
1803
  const { width: viewportWidth } = useWindowDimensions();
1804
+ const look = resolveShellBackdrop(theme, viewportWidth, contentPadding, testID);
1642
1805
  const railRange = collapsedRailRange ?? { min: RAIL_FULL_BREAKPOINT, max: DEFAULT_COLLAPSED_RAIL_MAX };
1643
1806
  const railMode = resolveRailMode({
1644
1807
  viewport: viewportWidth,
@@ -1660,18 +1823,21 @@ var AppShell = ({
1660
1823
  }, [isUnauthenticated, gate]);
1661
1824
  const gatePending = gate?.pending === true;
1662
1825
  if (isUnauthenticated) return null;
1663
- const columnStyle = maxWidth === "full" ? styles5.columnFull : [styles5.columnCapped, { maxWidth }];
1664
- const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles5.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
1665
- const scroller = /* @__PURE__ */ jsx(
1826
+ const columnStyle = maxWidth === "full" ? styles6.columnFull : [styles6.columnCapped, { maxWidth }];
1827
+ const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles6.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
1828
+ const scroller = /* @__PURE__ */ jsxs(
1666
1829
  ScrollView,
1667
1830
  {
1668
- contentContainerStyle: [styles5.scrollContent, { padding: contentPadding }],
1669
- style: styles5.scroll,
1831
+ contentContainerStyle: [styles6.scrollContent, { padding: contentPadding }],
1832
+ style: styles6.scroll,
1670
1833
  testID: `${testID}${APP_SHELL_SUFFIX.content}`,
1671
- children: /* @__PURE__ */ jsx(View, { style: columnStyle, children: body })
1834
+ children: [
1835
+ look.band,
1836
+ /* @__PURE__ */ jsx(View, { style: columnStyle, children: body })
1837
+ ]
1672
1838
  }
1673
1839
  );
1674
- const headerRegion = useDrawer ? /* @__PURE__ */ jsxs(View, { style: styles5.headerRow, testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: [
1840
+ const headerRegion = useDrawer ? /* @__PURE__ */ jsxs(View, { style: [styles6.headerRow, look.chrome], testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: [
1675
1841
  /* @__PURE__ */ jsx(
1676
1842
  MenuToggle,
1677
1843
  {
@@ -1681,14 +1847,14 @@ var AppShell = ({
1681
1847
  onOpen: openDrawer
1682
1848
  }
1683
1849
  ),
1684
- /* @__PURE__ */ jsx(View, { style: styles5.headerFill, children: renderTextSlot(header, { color: theme.colors.text }) })
1685
- ] }) : /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: renderTextSlot(header, { color: theme.colors.text }) });
1850
+ /* @__PURE__ */ jsx(View, { style: styles6.headerFill, children: renderTextSlot(header, { color: look.chromeText }) })
1851
+ ] }) : /* @__PURE__ */ jsx(View, { style: look.chrome, testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: renderTextSlot(header, { color: look.chromeText }) });
1686
1852
  const railRegion = railMode === "full" ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.sidebar}`, children: renderTextSlot(sidebar, { color: theme.colors.text }) }) : railMode === "collapsed" ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.collapsedSidebar}`, children: renderTextSlot(collapsedSidebar, { color: theme.colors.text }) }) : null;
1687
- return /* @__PURE__ */ jsxs(View, { style: [styles5.root, { backgroundColor: theme.colors.background }], testID, children: [
1853
+ return /* @__PURE__ */ jsxs(View, { style: [styles6.root, look.root], testID, children: [
1688
1854
  headerRegion,
1689
- nav !== void 0 ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.nav}`, children: navInnerStyle !== void 0 ? /* @__PURE__ */ jsx(View, { style: navInnerStyle, children: renderTextSlot(nav, { color: theme.colors.text }) }) : renderTextSlot(nav, { color: theme.colors.text }) }) : null,
1855
+ nav !== void 0 ? /* @__PURE__ */ jsx(View, { style: look.chrome, testID: `${testID}${APP_SHELL_SUFFIX.nav}`, children: navInnerStyle !== void 0 ? /* @__PURE__ */ jsx(View, { style: navInnerStyle, children: renderTextSlot(nav, { color: look.chromeText }) }) : renderTextSlot(nav, { color: look.chromeText }) }) : null,
1690
1856
  banner !== void 0 ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: renderTextSlot(banner, { color: theme.colors.text }) }) : null,
1691
- railRegion !== null ? /* @__PURE__ */ jsxs(View, { style: styles5.bodyRow, children: [
1857
+ railRegion !== null ? /* @__PURE__ */ jsxs(View, { style: styles6.bodyRow, children: [
1692
1858
  railRegion,
1693
1859
  scroller
1694
1860
  ] }) : scroller,
@@ -1705,7 +1871,7 @@ var AppShell = ({
1705
1871
  gatePending ? /* @__PURE__ */ jsx(
1706
1872
  View,
1707
1873
  {
1708
- style: [styles5.pendingOverlay, { backgroundColor: theme.colors.background }],
1874
+ style: [styles6.pendingOverlay, { backgroundColor: theme.colors.background }],
1709
1875
  testID: `${testID}${APP_SHELL_SUFFIX.pending}`,
1710
1876
  children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" })
1711
1877
  }
@@ -2072,7 +2238,7 @@ var KEY_DOWN = "ArrowDown";
2072
2238
  var KEY_UP = "ArrowUp";
2073
2239
  var KEY_ENTER = "Enter";
2074
2240
  var KEY_ESCAPE = "Escape";
2075
- var styles6 = StyleSheet.create({
2241
+ var styles7 = StyleSheet.create({
2076
2242
  overlay: { ...StyleSheet.absoluteFillObject, zIndex: OVERLAY_Z_INDEX, alignItems: "center" },
2077
2243
  scrim: { ...StyleSheet.absoluteFillObject, backgroundColor: SCRIM_COLOR2 },
2078
2244
  panel: {
@@ -2101,14 +2267,14 @@ var PaletteRow = ({ item, selected, onChoose }) => {
2101
2267
  accessibilityLabel: item.label,
2102
2268
  accessibilityRole: "button",
2103
2269
  accessibilityState: { selected },
2104
- style: [styles6.row, selected ? { backgroundColor: colors.border } : void 0],
2270
+ style: [styles7.row, selected ? { backgroundColor: colors.border } : void 0],
2105
2271
  testID: item.testID ?? item.key,
2106
2272
  onPress: () => onChoose(item),
2107
2273
  children: [
2108
2274
  typeof item.renderIcon === "function" ? item.renderIcon(colors.textSecondary, NAV_ICON_SIZE) : null,
2109
- /* @__PURE__ */ jsxs(View, { style: styles6.rowText, children: [
2110
- /* @__PURE__ */ jsx(Text, { style: [styles6.rowLabel, { color: colors.text }], children: item.label }),
2111
- item.hint ? /* @__PURE__ */ jsx(Text, { style: [styles6.rowHint, { color: colors.textSecondary }], children: item.hint }) : null
2275
+ /* @__PURE__ */ jsxs(View, { style: styles7.rowText, children: [
2276
+ /* @__PURE__ */ jsx(Text, { style: [styles7.rowLabel, { color: colors.text }], children: item.label }),
2277
+ item.hint ? /* @__PURE__ */ jsx(Text, { style: [styles7.rowHint, { color: colors.textSecondary }], children: item.hint }) : null
2112
2278
  ] })
2113
2279
  ]
2114
2280
  }
@@ -2148,14 +2314,14 @@ var CommandPalette = ({ open, items, onClose, labels, testID }) => {
2148
2314
  [results, selected, choose, onClose]
2149
2315
  );
2150
2316
  if (!open) return null;
2151
- return /* @__PURE__ */ jsxs(View, { style: styles6.overlay, children: [
2317
+ return /* @__PURE__ */ jsxs(View, { style: styles7.overlay, children: [
2152
2318
  /* @__PURE__ */ jsx(
2153
2319
  Pressable,
2154
2320
  {
2155
2321
  accessibilityHint: labels.closeHint,
2156
2322
  accessibilityLabel: labels.closeLabel,
2157
2323
  accessibilityRole: "button",
2158
- style: styles6.scrim,
2324
+ style: styles7.scrim,
2159
2325
  testID: `${testID ?? "command-palette"}-scrim`,
2160
2326
  onPress: onClose
2161
2327
  }
@@ -2166,7 +2332,7 @@ var CommandPalette = ({ open, items, onClose, labels, testID }) => {
2166
2332
  "aria-modal": true,
2167
2333
  "aria-label": labels.regionLabel,
2168
2334
  role: "dialog",
2169
- style: [styles6.panel, { backgroundColor: colors.surface, borderColor: colors.border }],
2335
+ style: [styles7.panel, { backgroundColor: colors.surface, borderColor: colors.border }],
2170
2336
  testID: testID ?? "command-palette",
2171
2337
  children: [
2172
2338
  /* @__PURE__ */ jsx(
@@ -2176,14 +2342,14 @@ var CommandPalette = ({ open, items, onClose, labels, testID }) => {
2176
2342
  accessibilityLabel: labels.placeholder,
2177
2343
  placeholder: labels.placeholder,
2178
2344
  placeholderTextColor: colors.textSecondary,
2179
- style: [styles6.input, { color: colors.text, borderBottomColor: colors.border }],
2345
+ style: [styles7.input, { color: colors.text, borderBottomColor: colors.border }],
2180
2346
  testID: `${testID ?? "command-palette"}-input`,
2181
2347
  value: query,
2182
2348
  onChangeText: setQuery,
2183
2349
  onKeyPress
2184
2350
  }
2185
2351
  ),
2186
- results.length === 0 ? /* @__PURE__ */ jsx(Text, { style: [styles6.empty, { color: colors.textSecondary }], children: labels.emptyText }) : /* @__PURE__ */ jsx(ScrollView, { keyboardShouldPersistTaps: "handled", style: styles6.list, children: results.map((item, index) => /* @__PURE__ */ jsx(PaletteRow, { item, selected: index === selected, onChoose: choose }, item.key)) })
2352
+ results.length === 0 ? /* @__PURE__ */ jsx(Text, { style: [styles7.empty, { color: colors.textSecondary }], children: labels.emptyText }) : /* @__PURE__ */ jsx(ScrollView, { keyboardShouldPersistTaps: "handled", style: styles7.list, children: results.map((item, index) => /* @__PURE__ */ jsx(PaletteRow, { item, selected: index === selected, onChoose: choose }, item.key)) })
2187
2353
  ]
2188
2354
  }
2189
2355
  )
@@ -2214,6 +2380,196 @@ function accessibleNavItems(user, table, translate) {
2214
2380
  return roleRoutesToNavItems(resolveAccessibleRoutes(user, table), translate);
2215
2381
  }
2216
2382
 
2217
- export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, AppShell, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, CommandPalette, CommandPaletteTrigger, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, NAV_TEST_IDS, Nav, NavBar, NavExpandableItem, NavOverflowMenu, NavShell, PillNav, PillNavVariant, RAIL_FULL_BREAKPOINT, Sidebar, SidebarChevron, SidebarSearch, Topbar, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveActiveRoute, resolveContentMaxWidth, resolvePillMenuLabel, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };
2383
+ // src/TabFeedback.ts
2384
+ var TabFeedback = /* @__PURE__ */ ((TabFeedback2) => {
2385
+ TabFeedback2["Default"] = "default";
2386
+ TabFeedback2["IndicatorPill"] = "indicator-pill";
2387
+ TabFeedback2["DimSpring"] = "dim-spring";
2388
+ TabFeedback2["SlidingBar"] = "sliding-bar";
2389
+ TabFeedback2["IconLift"] = "icon-lift";
2390
+ return TabFeedback2;
2391
+ })(TabFeedback || {});
2392
+
2393
+ // src/feedback/tabPressFeedback.ts
2394
+ var FULL = 1;
2395
+ var PERCENT = 100;
2396
+ var PRESS_ICON_SCALE = 0.92;
2397
+ var DIM_OPACITY = 0.6;
2398
+ var ICON_LIFT_PX = -3;
2399
+ var SPRING_OVERSHOOT = 1.06;
2400
+ function tabIconTransform(kind, state) {
2401
+ if (kind === "indicator-pill" /* IndicatorPill */ && state.pressed) return [{ scale: PRESS_ICON_SCALE }];
2402
+ const lifted = state.active || state.pressed;
2403
+ return kind === "icon-lift" /* IconLift */ && lifted ? [{ translateY: ICON_LIFT_PX }] : void 0;
2404
+ }
2405
+ function tabDuration(kind) {
2406
+ if (kind === "default" /* Default */) return motionDurations.instant;
2407
+ const slow = kind === "dim-spring" /* DimSpring */ || kind === "sliding-bar" /* SlidingBar */;
2408
+ return slow ? motionDurations.slow : motionDurations.base;
2409
+ }
2410
+ function resolveTabFeedback(kind, state, reducedMotion) {
2411
+ const pillScale = state.active ? FULL : 0;
2412
+ return {
2413
+ opacity: kind === "dim-spring" /* DimSpring */ && state.pressed ? DIM_OPACITY : FULL,
2414
+ iconTransform: reducedMotion ? void 0 : tabIconTransform(kind, state),
2415
+ pillScale: kind === "indicator-pill" /* IndicatorPill */ ? pillScale : null,
2416
+ spring: kind === "dim-spring" /* DimSpring */ && !reducedMotion,
2417
+ durationMs: reducedMotion ? motionDurations.instant : tabDuration(kind)
2418
+ };
2419
+ }
2420
+ function slidingBarPosition(activeIndex, count) {
2421
+ if (activeIndex < 0 || count <= 0) return null;
2422
+ const share = PERCENT / count;
2423
+ return { left: `${activeIndex * share}%`, width: `${share}%` };
2424
+ }
2425
+ function springSteps() {
2426
+ return [
2427
+ { toValue: SPRING_OVERSHOOT, durationMs: motionDurations.fast },
2428
+ { toValue: FULL, durationMs: motionDurations.slow - motionDurations.fast }
2429
+ ];
2430
+ }
2431
+ function useSettleSpring(active, enabled) {
2432
+ const scale = useRef(new Animated.Value(FULL)).current;
2433
+ useEffect(() => {
2434
+ if (!enabled || !active) return void 0;
2435
+ const steps = springSteps().map(
2436
+ (step) => Animated.timing(scale, {
2437
+ toValue: step.toValue,
2438
+ duration: step.durationMs,
2439
+ easing: motionEasings.standard,
2440
+ useNativeDriver: false
2441
+ })
2442
+ );
2443
+ const animation = Animated.sequence(steps);
2444
+ animation.start();
2445
+ return () => animation.stop();
2446
+ }, [active, enabled, scale]);
2447
+ return scale;
2448
+ }
2449
+ var ICON_SIZE = 24;
2450
+ var PILL_WIDTH = 56;
2451
+ var PILL_HEIGHT = 32;
2452
+ var PILL_RADIUS = 16;
2453
+ var TAB_MIN_HEIGHT = 56;
2454
+ var LABEL_SIZE = 12;
2455
+ var LABEL_GAP = 2;
2456
+ var TAB_PADDING = 4;
2457
+ var styles8 = StyleSheet.create({
2458
+ tab: {
2459
+ flex: 1,
2460
+ minHeight: TAB_MIN_HEIGHT,
2461
+ alignItems: "center",
2462
+ justifyContent: "center",
2463
+ paddingVertical: TAB_PADDING,
2464
+ rowGap: LABEL_GAP
2465
+ },
2466
+ iconWrap: { width: PILL_WIDTH, height: PILL_HEIGHT, alignItems: "center", justifyContent: "center" },
2467
+ pill: { ...StyleSheet.absoluteFillObject, borderRadius: PILL_RADIUS },
2468
+ label: { fontSize: LABEL_SIZE, textAlign: "center" }
2469
+ });
2470
+ function ariaCurrentProps3(isActive) {
2471
+ return isActive ? { "aria-current": "page" } : {};
2472
+ }
2473
+ var BottomTab = ({
2474
+ item,
2475
+ isActive,
2476
+ kind,
2477
+ colors,
2478
+ reducedMotion,
2479
+ navigateHint,
2480
+ onPress
2481
+ }) => {
2482
+ const [pressed, setPressed] = useState(false);
2483
+ const feedback = resolveTabFeedback(kind, { active: isActive, pressed }, reducedMotion);
2484
+ const scale = useSettleSpring(isActive, feedback.spring);
2485
+ const color = isActive ? colors.active : colors.rest;
2486
+ const motion = webTransition("transform", feedback.durationMs, reducedMotion);
2487
+ return /* @__PURE__ */ jsxs(
2488
+ Pressable,
2489
+ {
2490
+ accessibilityHint: navigateHint(item.label),
2491
+ accessibilityLabel: item.label,
2492
+ accessibilityRole: "link",
2493
+ accessibilityState: { selected: isActive },
2494
+ style: [styles8.tab, { opacity: feedback.opacity }],
2495
+ testID: item.testID ?? item.key,
2496
+ onPress: () => onPress(item.route),
2497
+ onPressIn: () => setPressed(true),
2498
+ onPressOut: () => setPressed(false),
2499
+ ...ariaCurrentProps3(isActive),
2500
+ children: [
2501
+ /* @__PURE__ */ jsxs(Animated.View, { style: [styles8.iconWrap, { transform: [{ scale }] }], children: [
2502
+ feedback.pillScale === null ? null : /* @__PURE__ */ jsx(
2503
+ View,
2504
+ {
2505
+ pointerEvents: "none",
2506
+ style: [styles8.pill, { backgroundColor: colors.pill, transform: [{ scale: feedback.pillScale }] }, motion]
2507
+ }
2508
+ ),
2509
+ /* @__PURE__ */ jsx(View, { style: [{ transform: feedback.iconTransform }, motion], children: item.renderIcon?.(color, ICON_SIZE) })
2510
+ ] }),
2511
+ /* @__PURE__ */ jsx(Text, { style: [styles8.label, { color }], children: item.label })
2512
+ ]
2513
+ }
2514
+ );
2515
+ };
2516
+ var DEFAULT_TEST_ID = "bottom-tab-bar";
2517
+ var SLIDER_HEIGHT = 3;
2518
+ var styles9 = StyleSheet.create({
2519
+ bar: { flexDirection: "row", borderTopWidth: StyleSheet.hairlineWidth },
2520
+ slider: { position: "absolute", top: 0, height: SLIDER_HEIGHT }
2521
+ });
2522
+ var BottomTabBar = ({
2523
+ items,
2524
+ pathname,
2525
+ onNavigate,
2526
+ tabFeedback = "default" /* Default */,
2527
+ navigateHint = (label) => label,
2528
+ regionLabel = "",
2529
+ containerStyle,
2530
+ testID = DEFAULT_TEST_ID
2531
+ }) => {
2532
+ const { theme } = useUi();
2533
+ const reducedMotion = useReducedMotion();
2534
+ const primary = theme.palette.primary["500"];
2535
+ const activeRoute = resolveActiveRoute(items, pathname);
2536
+ const activeIndex = items.findIndex((item) => item.route === activeRoute);
2537
+ const bar = tabFeedback === "sliding-bar" /* SlidingBar */ ? slidingBarPosition(activeIndex, items.length) : null;
2538
+ const barMs = resolveTabFeedback(tabFeedback, { active: true, pressed: false }, reducedMotion).durationMs;
2539
+ const colors = { active: primary, rest: theme.colors.textSecondary, pill: theme.colors.surfaceElevated };
2540
+ return /* @__PURE__ */ jsxs(
2541
+ View,
2542
+ {
2543
+ "aria-label": regionLabel,
2544
+ role: "navigation",
2545
+ style: [styles9.bar, { backgroundColor: theme.colors.surface, borderTopColor: theme.colors.border }, containerStyle],
2546
+ testID,
2547
+ children: [
2548
+ bar === null ? null : /* @__PURE__ */ jsx(
2549
+ View,
2550
+ {
2551
+ pointerEvents: "none",
2552
+ style: [styles9.slider, { left: bar.left, width: bar.width, backgroundColor: primary }, webTransition("left", barMs, reducedMotion)]
2553
+ }
2554
+ ),
2555
+ items.map((item) => /* @__PURE__ */ jsx(
2556
+ BottomTab,
2557
+ {
2558
+ colors,
2559
+ isActive: item.route === activeRoute,
2560
+ item,
2561
+ kind: tabFeedback,
2562
+ navigateHint,
2563
+ reducedMotion,
2564
+ onPress: onNavigate
2565
+ },
2566
+ item.key
2567
+ ))
2568
+ ]
2569
+ }
2570
+ );
2571
+ };
2572
+
2573
+ export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, AppShell, BASE_INDENT, BottomTabBar, CHEVRON_ICON_SIZE, CollapsedRail, CommandPalette, CommandPaletteTrigger, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, NAV_TEST_IDS, Nav, NavBar, NavExpandableItem, NavHoverFeedback, NavOverflowMenu, NavShell, PillNav, PillNavVariant, RAIL_FULL_BREAKPOINT, Sidebar, SidebarChevron, SidebarSearch, TabFeedback, Topbar, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveActiveRoute, resolveContentMaxWidth, resolvePillMenuLabel, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };
2218
2574
  //# sourceMappingURL=index.mjs.map
2219
2575
  //# sourceMappingURL=index.mjs.map