@dloizides/ui-nav 1.26.1 → 1.27.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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.27.0
4
+
5
+ - INVOICE-5 "Edit invoices before sending, shared background and menu hover" (AC-UI-2, AC-UI-6): `NavBar` takes `hoverFeedback` (`NavHoverFeedback`: `default`, `tint-pill`, `underline`, `lift`, `sliding-highlight`); the new phone `BottomTabBar` takes `tabFeedback` (`TabFeedback`: `default`, `indicator-pill`, `dim-spring`, `sliding-bar`, `icon-lift`). Timings come from the `@dloizides/ui-motion` tokens (<= 300 ms); reduced motion keeps every option still. `default` keeps today's look, so existing apps do not change.
6
+
7
+ ## 1.26.2
8
+
9
+ - KEFI-LAUNCH-1 "Do every open Kefi task before the public launch": a long account name no longer overlaps the `Topbar` center slot at 360 px. The center slot keeps its content width (`flexShrink: 0`); only the account name gives way. The overlap dates from 1.24.0, when the slot was added.
10
+
3
11
  ## 1.26.1
4
12
 
5
13
  - KEFI-LAUNCH-1 "Do every open Kefi task before the public launch": the `Topbar` left slot no longer wraps beside a long account name at 360 px. The left slot keeps its width (`flexShrink: 0`) and the account name truncates to one line.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { ViewStyle } from 'react-native';
2
+ import { ViewStyle, StyleProp } from 'react-native';
3
3
  import { UiTheme } from '@dloizides/ui-feedback';
4
4
  import { DropdownVariant } from '@dloizides/ui-layout';
5
5
  import { resolveAccessibleRoutes, RoleRouteTable, RoleRoute } from '@dloizides/auth-web';
@@ -363,6 +363,14 @@ interface TopbarProps {
363
363
  }
364
364
  declare const Topbar: ({ left, center, language, notificationSlot, user, account, logout, containerStyle, }: TopbarProps) => React.ReactElement;
365
365
 
366
+ declare enum NavHoverFeedback {
367
+ Default = "default",
368
+ TintPill = "tint-pill",
369
+ Underline = "underline",
370
+ Lift = "lift",
371
+ SlidingHighlight = "sliding-highlight"
372
+ }
373
+
366
374
  /**
367
375
  * NavBar — the config-driven HORIZONTAL top navigation bar. It is the top-bar
368
376
  * counterpart to `Sidebar`: it renders the SAME caller-supplied `NavItem[]`
@@ -445,6 +453,7 @@ interface NavBarProps {
445
453
  * theme. Style-only: every colour still flows through the theme — no per-app fork.
446
454
  */
447
455
  barTheme?: UiTheme;
456
+ hoverFeedback?: NavHoverFeedback;
448
457
  }
449
458
  /**
450
459
  * `NavBar` — the horizontal top bar. When `barTheme` is supplied it re-provisions
@@ -1598,4 +1607,25 @@ declare const NAV_ICON_SIZE = 14;
1598
1607
  /** Chevron icon size for expandable sections. */
1599
1608
  declare const CHEVRON_ICON_SIZE = 12;
1600
1609
 
1601
- export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, type CommandItem, CommandPalette, type CommandPaletteLabels, type CommandPaletteProps, CommandPaletteTrigger, type CommandPaletteTriggerProps, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, NAV_TEST_IDS, Nav, NavBar, type NavBarProps, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavOrientation, NavOverflowMenu, type NavOverflowMenuProps, type NavProps, NavShell, type NavShellCollapsedRail, type NavShellLayout, type NavShellProps, type NavShellSideRail, type NavShellTopBar, type NavUser, PillNav, type PillNavProps, PillNavVariant, RAIL_FULL_BREAKPOINT, type RailMode, type ResolveRailModeArgs, type SearchAffordance, type ShellMessage, Sidebar, SidebarChevron, type SidebarChevronProps, type SidebarPaletteTriggerConfig, type SidebarProps, SidebarSearch, type SidebarSearchConfig, type SidebarSearchLabels, type SidebarSearchProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveActiveRoute, resolveContentMaxWidth, resolvePillMenuLabel, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };
1610
+ declare enum TabFeedback {
1611
+ Default = "default",
1612
+ IndicatorPill = "indicator-pill",
1613
+ DimSpring = "dim-spring",
1614
+ SlidingBar = "sliding-bar",
1615
+ IconLift = "icon-lift"
1616
+ }
1617
+
1618
+ interface BottomTabBarProps {
1619
+ items: NavItem[];
1620
+ pathname: string;
1621
+ onNavigate: (route: string) => void;
1622
+ tabFeedback?: TabFeedback;
1623
+ navigateHint?: (label: string) => string;
1624
+ regionLabel?: string;
1625
+ containerStyle?: StyleProp<ViewStyle>;
1626
+ testID?: string;
1627
+ }
1628
+ /** Phone tab bar; `tabFeedback` picks the press feedback, `Default` adds no motion. */
1629
+ declare const BottomTabBar: ({ items, pathname, onNavigate, tabFeedback, navigateHint, regionLabel, containerStyle, testID, }: BottomTabBarProps) => React.ReactElement;
1630
+
1631
+ export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, BottomTabBar, type BottomTabBarProps, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, type CommandItem, CommandPalette, type CommandPaletteLabels, type CommandPaletteProps, CommandPaletteTrigger, type CommandPaletteTriggerProps, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, NAV_TEST_IDS, Nav, NavBar, type NavBarProps, NavExpandableItem, type NavExpandableItemProps, NavHoverFeedback, type NavItem, type NavOrientation, NavOverflowMenu, type NavOverflowMenuProps, type NavProps, NavShell, type NavShellCollapsedRail, type NavShellLayout, type NavShellProps, type NavShellSideRail, type NavShellTopBar, type NavUser, PillNav, type PillNavProps, PillNavVariant, RAIL_FULL_BREAKPOINT, type RailMode, type ResolveRailModeArgs, type SearchAffordance, type ShellMessage, Sidebar, SidebarChevron, type SidebarChevronProps, type SidebarPaletteTriggerConfig, type SidebarProps, SidebarSearch, type SidebarSearchConfig, type SidebarSearchLabels, type SidebarSearchProps, TabFeedback, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveActiveRoute, resolveContentMaxWidth, resolvePillMenuLabel, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { ViewStyle } from 'react-native';
2
+ import { ViewStyle, StyleProp } from 'react-native';
3
3
  import { UiTheme } from '@dloizides/ui-feedback';
4
4
  import { DropdownVariant } from '@dloizides/ui-layout';
5
5
  import { resolveAccessibleRoutes, RoleRouteTable, RoleRoute } from '@dloizides/auth-web';
@@ -363,6 +363,14 @@ interface TopbarProps {
363
363
  }
364
364
  declare const Topbar: ({ left, center, language, notificationSlot, user, account, logout, containerStyle, }: TopbarProps) => React.ReactElement;
365
365
 
366
+ declare enum NavHoverFeedback {
367
+ Default = "default",
368
+ TintPill = "tint-pill",
369
+ Underline = "underline",
370
+ Lift = "lift",
371
+ SlidingHighlight = "sliding-highlight"
372
+ }
373
+
366
374
  /**
367
375
  * NavBar — the config-driven HORIZONTAL top navigation bar. It is the top-bar
368
376
  * counterpart to `Sidebar`: it renders the SAME caller-supplied `NavItem[]`
@@ -445,6 +453,7 @@ interface NavBarProps {
445
453
  * theme. Style-only: every colour still flows through the theme — no per-app fork.
446
454
  */
447
455
  barTheme?: UiTheme;
456
+ hoverFeedback?: NavHoverFeedback;
448
457
  }
449
458
  /**
450
459
  * `NavBar` — the horizontal top bar. When `barTheme` is supplied it re-provisions
@@ -1598,4 +1607,25 @@ declare const NAV_ICON_SIZE = 14;
1598
1607
  /** Chevron icon size for expandable sections. */
1599
1608
  declare const CHEVRON_ICON_SIZE = 12;
1600
1609
 
1601
- export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, type CommandItem, CommandPalette, type CommandPaletteLabels, type CommandPaletteProps, CommandPaletteTrigger, type CommandPaletteTriggerProps, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, NAV_TEST_IDS, Nav, NavBar, type NavBarProps, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavOrientation, NavOverflowMenu, type NavOverflowMenuProps, type NavProps, NavShell, type NavShellCollapsedRail, type NavShellLayout, type NavShellProps, type NavShellSideRail, type NavShellTopBar, type NavUser, PillNav, type PillNavProps, PillNavVariant, RAIL_FULL_BREAKPOINT, type RailMode, type ResolveRailModeArgs, type SearchAffordance, type ShellMessage, Sidebar, SidebarChevron, type SidebarChevronProps, type SidebarPaletteTriggerConfig, type SidebarProps, SidebarSearch, type SidebarSearchConfig, type SidebarSearchLabels, type SidebarSearchProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveActiveRoute, resolveContentMaxWidth, resolvePillMenuLabel, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };
1610
+ declare enum TabFeedback {
1611
+ Default = "default",
1612
+ IndicatorPill = "indicator-pill",
1613
+ DimSpring = "dim-spring",
1614
+ SlidingBar = "sliding-bar",
1615
+ IconLift = "icon-lift"
1616
+ }
1617
+
1618
+ interface BottomTabBarProps {
1619
+ items: NavItem[];
1620
+ pathname: string;
1621
+ onNavigate: (route: string) => void;
1622
+ tabFeedback?: TabFeedback;
1623
+ navigateHint?: (label: string) => string;
1624
+ regionLabel?: string;
1625
+ containerStyle?: StyleProp<ViewStyle>;
1626
+ testID?: string;
1627
+ }
1628
+ /** Phone tab bar; `tabFeedback` picks the press feedback, `Default` adds no motion. */
1629
+ declare const BottomTabBar: ({ items, pathname, onNavigate, tabFeedback, navigateHint, regionLabel, containerStyle, testID, }: BottomTabBarProps) => React.ReactElement;
1630
+
1631
+ export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, BottomTabBar, type BottomTabBarProps, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, type CommandItem, CommandPalette, type CommandPaletteLabels, type CommandPaletteProps, CommandPaletteTrigger, type CommandPaletteTriggerProps, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, NAV_TEST_IDS, Nav, NavBar, type NavBarProps, NavExpandableItem, type NavExpandableItemProps, NavHoverFeedback, type NavItem, type NavOrientation, NavOverflowMenu, type NavOverflowMenuProps, type NavProps, NavShell, type NavShellCollapsedRail, type NavShellLayout, type NavShellProps, type NavShellSideRail, type NavShellTopBar, type NavUser, PillNav, type PillNavProps, PillNavVariant, RAIL_FULL_BREAKPOINT, type RailMode, type ResolveRailModeArgs, type SearchAffordance, type ShellMessage, Sidebar, SidebarChevron, type SidebarChevronProps, type SidebarPaletteTriggerConfig, type SidebarProps, SidebarSearch, type SidebarSearchConfig, type SidebarSearchLabels, type SidebarSearchProps, TabFeedback, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveActiveRoute, resolveContentMaxWidth, resolvePillMenuLabel, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };
package/dist/index.js CHANGED
@@ -71,7 +71,7 @@ var navStyles = reactNative.StyleSheet.create({
71
71
  justifyContent: "space-between"
72
72
  },
73
73
  topbarLeft: { flexGrow: 1, flexShrink: 0 },
74
- topbarCenter: { flexShrink: 1, minWidth: 0, flexDirection: "row", alignItems: "center", marginHorizontal: 8 },
74
+ topbarCenter: { flexShrink: 0, minWidth: 0, flexDirection: "row", alignItems: "center", marginHorizontal: 8 },
75
75
  topbarRight: { flexDirection: "row", alignItems: "center", flexShrink: 1, minWidth: 0 },
76
76
  topbarRowItem: {
77
77
  marginHorizontal: 8,
@@ -1057,10 +1057,88 @@ var Topbar = ({
1057
1057
  }
1058
1058
  );
1059
1059
  };
1060
+
1061
+ // src/NavHoverFeedback.ts
1062
+ var NavHoverFeedback = /* @__PURE__ */ ((NavHoverFeedback2) => {
1063
+ NavHoverFeedback2["Default"] = "default";
1064
+ NavHoverFeedback2["TintPill"] = "tint-pill";
1065
+ NavHoverFeedback2["Underline"] = "underline";
1066
+ NavHoverFeedback2["Lift"] = "lift";
1067
+ NavHoverFeedback2["SlidingHighlight"] = "sliding-highlight";
1068
+ return NavHoverFeedback2;
1069
+ })(NavHoverFeedback || {});
1070
+
1071
+ // src/feedback/navLinkFeedback.ts
1072
+ var DEFAULT_FADE_MS = 150;
1073
+ var PRESS_SCALE = 0.97;
1074
+ var PRESS_NUDGE_PX = 1;
1075
+ var LIFT_PX = -2;
1076
+ var PILL_KINDS = /* @__PURE__ */ new Set([
1077
+ "default" /* Default */,
1078
+ "tint-pill" /* TintPill */,
1079
+ "lift" /* Lift */
1080
+ ]);
1081
+ function pillBackground(kind, isHovered, active, colors) {
1082
+ if (!PILL_KINDS.has(kind)) return void 0;
1083
+ if (active) return colors.activeBg;
1084
+ return isHovered ? colors.hoverBg : void 0;
1085
+ }
1086
+ function linkTextColor(kind, isHovered, active, colors) {
1087
+ if (active) return PILL_KINDS.has(kind) ? colors.onPrimary : colors.activeBg;
1088
+ return isHovered ? colors.hoverText : colors.rest;
1089
+ }
1090
+ function pressTransform(kind, pressed, isHovered) {
1091
+ if (kind === "tint-pill" /* TintPill */) return pressed ? [{ scale: PRESS_SCALE }] : void 0;
1092
+ if (kind === "underline" /* Underline */) return pressed ? [{ translateY: PRESS_NUDGE_PX }] : void 0;
1093
+ if (kind !== "lift" /* Lift */) return void 0;
1094
+ if (pressed) return [{ translateY: 0 }];
1095
+ return isHovered ? [{ translateY: LIFT_PX }] : void 0;
1096
+ }
1097
+ function feedbackDuration(kind) {
1098
+ if (kind === "default" /* Default */) return DEFAULT_FADE_MS;
1099
+ return kind === "tint-pill" /* TintPill */ ? uiMotion.motionDurations.fast : uiMotion.motionDurations.base;
1100
+ }
1101
+ function resolveLinkFeedback(kind, state, colors, reducedMotion) {
1102
+ const isHovered = state.hovered && !state.active;
1103
+ const underlined = state.active || isHovered;
1104
+ return {
1105
+ background: pillBackground(kind, isHovered, state.active, colors),
1106
+ textColor: linkTextColor(kind, isHovered, state.active, colors),
1107
+ transform: reducedMotion ? void 0 : pressTransform(kind, state.pressed, isHovered),
1108
+ shadow: kind === "lift" /* Lift */ && isHovered,
1109
+ underline: kind === "underline" /* Underline */ ? { color: colors.activeBg, scale: underlined ? 1 : 0 } : null,
1110
+ durationMs: reducedMotion ? uiMotion.motionDurations.instant : feedbackDuration(kind)
1111
+ };
1112
+ }
1060
1113
  var TEXT_ON_PRIMARY2 = "#ffffff";
1061
1114
  function ariaCurrentProps2(isActive) {
1062
1115
  return isActive ? { "aria-current": "page" } : {};
1063
1116
  }
1117
+ var FEEDBACK_PROPERTIES = "color, background-color, transform, box-shadow";
1118
+ var UNDERLINE_INSET = 14;
1119
+ var UNDERLINE_HEIGHT = 2;
1120
+ var SHADOW_OPACITY = 0.18;
1121
+ var SHADOW_RADIUS = 6;
1122
+ var SHADOW_OFFSET = { width: 0, height: 2 };
1123
+ function feedbackStyle(kind, feedback, shadowColor, reducedMotion) {
1124
+ const background = feedback.background === void 0 ? {} : { backgroundColor: feedback.background };
1125
+ const shadow = feedback.shadow ? { shadowColor, shadowOpacity: SHADOW_OPACITY, shadowRadius: SHADOW_RADIUS, shadowOffset: SHADOW_OFFSET } : {};
1126
+ const transition = kind === "default" /* Default */ ? hoverTransitionStyle(reducedMotion) : webTransition(FEEDBACK_PROPERTIES, feedback.durationMs, reducedMotion);
1127
+ return [background, shadow, { transform: feedback.transform }, transition ?? {}];
1128
+ }
1129
+ function underlineStyle(feedback, reducedMotion) {
1130
+ if (feedback.underline === null) return null;
1131
+ const line = {
1132
+ position: "absolute",
1133
+ left: UNDERLINE_INSET,
1134
+ right: UNDERLINE_INSET,
1135
+ bottom: 0,
1136
+ height: UNDERLINE_HEIGHT,
1137
+ backgroundColor: feedback.underline.color,
1138
+ transform: [{ scaleX: feedback.underline.scale }]
1139
+ };
1140
+ return [line, webTransition("transform", feedback.durationMs, reducedMotion) ?? {}];
1141
+ }
1064
1142
  var NavBarLink = ({
1065
1143
  item,
1066
1144
  isActive,
@@ -1069,13 +1147,20 @@ var NavBarLink = ({
1069
1147
  reducedMotion,
1070
1148
  colors,
1071
1149
  navigateHint,
1072
- onPress
1150
+ onPress,
1151
+ hoverFeedback = "default" /* Default */,
1152
+ onHoverChange
1073
1153
  }) => {
1074
1154
  const [hovered, setHovered] = React.useState(false);
1075
1155
  const [focused, setFocused] = React.useState(false);
1076
- const isHovered = hovered && !isActive;
1077
- const textColor = isActive ? TEXT_ON_PRIMARY2 : isHovered ? colors.hoverText : colors.rest;
1078
- const pillStyle = isActive ? { backgroundColor: colors.activeBg } : isHovered ? { backgroundColor: colors.hoverBg } : void 0;
1156
+ const linkColors = { ...colors, onPrimary: TEXT_ON_PRIMARY2 };
1157
+ const resolve = (pressed) => resolveLinkFeedback(hoverFeedback, { hovered, pressed, active: isActive }, linkColors, reducedMotion);
1158
+ const textColor = resolve(false).textColor;
1159
+ const underline = underlineStyle(resolve(false), reducedMotion);
1160
+ const setHover = (next) => {
1161
+ setHovered(next);
1162
+ onHoverChange?.(item.key, next);
1163
+ };
1079
1164
  return /* @__PURE__ */ jsxRuntime.jsxs(
1080
1165
  reactNative.Pressable,
1081
1166
  {
@@ -1083,27 +1168,61 @@ var NavBarLink = ({
1083
1168
  accessibilityLabel: item.label,
1084
1169
  accessibilityRole: "link",
1085
1170
  accessibilityState: { selected: isActive },
1086
- style: [
1171
+ style: ({ pressed }) => [
1087
1172
  navBarStyles.link,
1088
1173
  collapsed ? navBarStyles.linkStacked : void 0,
1089
- pillStyle,
1090
- hoverTransitionStyle(reducedMotion),
1174
+ ...feedbackStyle(hoverFeedback, resolve(pressed), colors.rest, reducedMotion),
1091
1175
  focusRingStyle(focused, colors.ring)
1092
1176
  ],
1093
1177
  testID: item.testID ?? item.key,
1094
1178
  onBlur: () => setFocused(false),
1095
1179
  onFocus: () => setFocused(true),
1096
- onHoverIn: () => setHovered(true),
1097
- onHoverOut: () => setHovered(false),
1180
+ onHoverIn: () => setHover(true),
1181
+ onHoverOut: () => setHover(false),
1098
1182
  onPress: () => onPress(item.route),
1099
1183
  ...ariaCurrentProps2(isActive),
1100
1184
  children: [
1101
1185
  typeof item.renderIcon === "function" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: navBarStyles.linkIcon, children: item.renderIcon(textColor, iconSize) }) : null,
1186
+ underline === null ? null : /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { pointerEvents: "none", style: underline }),
1102
1187
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navBarStyles.linkText, { color: textColor }], children: item.label })
1103
1188
  ]
1104
1189
  }
1105
1190
  );
1106
1191
  };
1192
+ var HIGHLIGHT_RADIUS = 8;
1193
+ var SLIDE_PROPERTIES = "left, top, width, height";
1194
+ function slideHighlightRect(layouts, activeKey, hoveredKey) {
1195
+ const hoveredRect = hoveredKey === null ? void 0 : layouts[hoveredKey];
1196
+ const activeRect = activeKey === void 0 ? void 0 : layouts[activeKey];
1197
+ return hoveredRect ?? activeRect ?? null;
1198
+ }
1199
+ function useSlidingHighlight(enabled, activeKey, color, reducedMotion) {
1200
+ const [layouts, setLayouts] = React.useState({});
1201
+ const [hoveredKey, setHoveredKey] = React.useState(null);
1202
+ const measure = React.useCallback(
1203
+ (key, layout) => {
1204
+ if (enabled) setLayouts((prev) => ({ ...prev, [key]: layout }));
1205
+ },
1206
+ [enabled]
1207
+ );
1208
+ const hover = React.useCallback((key, hovered) => {
1209
+ setHoveredKey((prev) => hovered ? key : prev === key ? null : prev);
1210
+ }, []);
1211
+ const rect = enabled ? slideHighlightRect(layouts, activeKey, hoveredKey) : null;
1212
+ const style = rect === null ? null : [
1213
+ {
1214
+ position: "absolute",
1215
+ left: rect.x,
1216
+ top: rect.y,
1217
+ width: rect.width,
1218
+ height: rect.height,
1219
+ borderRadius: HIGHLIGHT_RADIUS,
1220
+ backgroundColor: color
1221
+ },
1222
+ webTransition(SLIDE_PROPERTIES, uiMotion.motionDurations.base, reducedMotion)
1223
+ ];
1224
+ return { style, measure, hover };
1225
+ }
1107
1226
  var TEXT_ON_PRIMARY3 = "#ffffff";
1108
1227
  var NO_ACTIVE_ROUTE = "";
1109
1228
  var OVERFLOW_MENU_MIN_WIDTH = 200;
@@ -1244,7 +1363,8 @@ var NavBarInner = ({
1244
1363
  overflowLabel = "More",
1245
1364
  overflowHint = "",
1246
1365
  containerStyle,
1247
- contentMaxWidth
1366
+ contentMaxWidth,
1367
+ hoverFeedback = "default" /* Default */
1248
1368
  }) => {
1249
1369
  const { theme } = uiFeedback.useUi();
1250
1370
  const colors = theme.colors;
@@ -1278,6 +1398,12 @@ var NavBarInner = ({
1278
1398
  [onNavigate]
1279
1399
  );
1280
1400
  const activeRoute = resolveActiveRoute(items, pathname);
1401
+ const slide = useSlidingHighlight(
1402
+ hoverFeedback === "sliding-highlight" /* SlidingHighlight */,
1403
+ items.find((item) => item.route === activeRoute)?.key,
1404
+ colors.surfaceElevated,
1405
+ reducedMotion
1406
+ );
1281
1407
  const linkColors = React.useMemo(
1282
1408
  () => ({
1283
1409
  rest: colors.textSecondary,
@@ -1327,9 +1453,11 @@ var NavBarInner = ({
1327
1453
  colors: linkColors,
1328
1454
  iconSize: NAV_ICON_SIZE,
1329
1455
  isActive: item.route === activeRoute,
1456
+ hoverFeedback,
1330
1457
  item,
1331
1458
  navigateHint,
1332
1459
  reducedMotion,
1460
+ onHoverChange: slide.hover,
1333
1461
  onPress: handlePress
1334
1462
  }
1335
1463
  );
@@ -1355,11 +1483,15 @@ var NavBarInner = ({
1355
1483
  testID: NAV_TEST_IDS.navBarLinks,
1356
1484
  onLayout: (event) => setAvailableWidth(event.nativeEvent.layout.width),
1357
1485
  children: [
1486
+ slide.style === null ? null : /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { pointerEvents: "none", style: slide.style }),
1358
1487
  visibleItems.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
1359
1488
  reactNative.View,
1360
1489
  {
1361
1490
  style: navBarStyles.linkCell,
1362
- onLayout: (event) => setItemWidth(index, event.nativeEvent.layout.width),
1491
+ onLayout: (event) => {
1492
+ setItemWidth(index, event.nativeEvent.layout.width);
1493
+ slide.measure(item.key, event.nativeEvent.layout);
1494
+ },
1363
1495
  children: renderLink(item)
1364
1496
  },
1365
1497
  item.key
@@ -2220,11 +2352,202 @@ function accessibleNavItems(user, table, translate) {
2220
2352
  return roleRoutesToNavItems(authWeb.resolveAccessibleRoutes(user, table), translate);
2221
2353
  }
2222
2354
 
2355
+ // src/TabFeedback.ts
2356
+ var TabFeedback = /* @__PURE__ */ ((TabFeedback2) => {
2357
+ TabFeedback2["Default"] = "default";
2358
+ TabFeedback2["IndicatorPill"] = "indicator-pill";
2359
+ TabFeedback2["DimSpring"] = "dim-spring";
2360
+ TabFeedback2["SlidingBar"] = "sliding-bar";
2361
+ TabFeedback2["IconLift"] = "icon-lift";
2362
+ return TabFeedback2;
2363
+ })(TabFeedback || {});
2364
+
2365
+ // src/feedback/tabPressFeedback.ts
2366
+ var FULL = 1;
2367
+ var PERCENT = 100;
2368
+ var PRESS_ICON_SCALE = 0.92;
2369
+ var DIM_OPACITY = 0.6;
2370
+ var ICON_LIFT_PX = -3;
2371
+ var SPRING_OVERSHOOT = 1.06;
2372
+ function tabIconTransform(kind, state) {
2373
+ if (kind === "indicator-pill" /* IndicatorPill */ && state.pressed) return [{ scale: PRESS_ICON_SCALE }];
2374
+ const lifted = state.active || state.pressed;
2375
+ return kind === "icon-lift" /* IconLift */ && lifted ? [{ translateY: ICON_LIFT_PX }] : void 0;
2376
+ }
2377
+ function tabDuration(kind) {
2378
+ if (kind === "default" /* Default */) return uiMotion.motionDurations.instant;
2379
+ const slow = kind === "dim-spring" /* DimSpring */ || kind === "sliding-bar" /* SlidingBar */;
2380
+ return slow ? uiMotion.motionDurations.slow : uiMotion.motionDurations.base;
2381
+ }
2382
+ function resolveTabFeedback(kind, state, reducedMotion) {
2383
+ const pillScale = state.active ? FULL : 0;
2384
+ return {
2385
+ opacity: kind === "dim-spring" /* DimSpring */ && state.pressed ? DIM_OPACITY : FULL,
2386
+ iconTransform: reducedMotion ? void 0 : tabIconTransform(kind, state),
2387
+ pillScale: kind === "indicator-pill" /* IndicatorPill */ ? pillScale : null,
2388
+ spring: kind === "dim-spring" /* DimSpring */ && !reducedMotion,
2389
+ durationMs: reducedMotion ? uiMotion.motionDurations.instant : tabDuration(kind)
2390
+ };
2391
+ }
2392
+ function slidingBarPosition(activeIndex, count) {
2393
+ if (activeIndex < 0 || count <= 0) return null;
2394
+ const share = PERCENT / count;
2395
+ return { left: `${activeIndex * share}%`, width: `${share}%` };
2396
+ }
2397
+ function springSteps() {
2398
+ return [
2399
+ { toValue: SPRING_OVERSHOOT, durationMs: uiMotion.motionDurations.fast },
2400
+ { toValue: FULL, durationMs: uiMotion.motionDurations.slow - uiMotion.motionDurations.fast }
2401
+ ];
2402
+ }
2403
+ function useSettleSpring(active, enabled) {
2404
+ const scale = React.useRef(new reactNative.Animated.Value(FULL)).current;
2405
+ React.useEffect(() => {
2406
+ if (!enabled || !active) return void 0;
2407
+ const steps = springSteps().map(
2408
+ (step) => reactNative.Animated.timing(scale, {
2409
+ toValue: step.toValue,
2410
+ duration: step.durationMs,
2411
+ easing: uiMotion.motionEasings.standard,
2412
+ useNativeDriver: false
2413
+ })
2414
+ );
2415
+ const animation = reactNative.Animated.sequence(steps);
2416
+ animation.start();
2417
+ return () => animation.stop();
2418
+ }, [active, enabled, scale]);
2419
+ return scale;
2420
+ }
2421
+ var ICON_SIZE = 24;
2422
+ var PILL_WIDTH = 56;
2423
+ var PILL_HEIGHT = 32;
2424
+ var PILL_RADIUS = 16;
2425
+ var TAB_MIN_HEIGHT = 56;
2426
+ var LABEL_SIZE = 12;
2427
+ var LABEL_GAP = 2;
2428
+ var TAB_PADDING = 4;
2429
+ var styles7 = reactNative.StyleSheet.create({
2430
+ tab: {
2431
+ flex: 1,
2432
+ minHeight: TAB_MIN_HEIGHT,
2433
+ alignItems: "center",
2434
+ justifyContent: "center",
2435
+ paddingVertical: TAB_PADDING,
2436
+ rowGap: LABEL_GAP
2437
+ },
2438
+ iconWrap: { width: PILL_WIDTH, height: PILL_HEIGHT, alignItems: "center", justifyContent: "center" },
2439
+ pill: { ...reactNative.StyleSheet.absoluteFillObject, borderRadius: PILL_RADIUS },
2440
+ label: { fontSize: LABEL_SIZE, textAlign: "center" }
2441
+ });
2442
+ function ariaCurrentProps3(isActive) {
2443
+ return isActive ? { "aria-current": "page" } : {};
2444
+ }
2445
+ var BottomTab = ({
2446
+ item,
2447
+ isActive,
2448
+ kind,
2449
+ colors,
2450
+ reducedMotion,
2451
+ navigateHint,
2452
+ onPress
2453
+ }) => {
2454
+ const [pressed, setPressed] = React.useState(false);
2455
+ const feedback = resolveTabFeedback(kind, { active: isActive, pressed }, reducedMotion);
2456
+ const scale = useSettleSpring(isActive, feedback.spring);
2457
+ const color = isActive ? colors.active : colors.rest;
2458
+ const motion = webTransition("transform", feedback.durationMs, reducedMotion);
2459
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2460
+ reactNative.Pressable,
2461
+ {
2462
+ accessibilityHint: navigateHint(item.label),
2463
+ accessibilityLabel: item.label,
2464
+ accessibilityRole: "link",
2465
+ accessibilityState: { selected: isActive },
2466
+ style: [styles7.tab, { opacity: feedback.opacity }],
2467
+ testID: item.testID ?? item.key,
2468
+ onPress: () => onPress(item.route),
2469
+ onPressIn: () => setPressed(true),
2470
+ onPressOut: () => setPressed(false),
2471
+ ...ariaCurrentProps3(isActive),
2472
+ children: [
2473
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Animated.View, { style: [styles7.iconWrap, { transform: [{ scale }] }], children: [
2474
+ feedback.pillScale === null ? null : /* @__PURE__ */ jsxRuntime.jsx(
2475
+ reactNative.View,
2476
+ {
2477
+ pointerEvents: "none",
2478
+ style: [styles7.pill, { backgroundColor: colors.pill, transform: [{ scale: feedback.pillScale }] }, motion]
2479
+ }
2480
+ ),
2481
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [{ transform: feedback.iconTransform }, motion], children: item.renderIcon?.(color, ICON_SIZE) })
2482
+ ] }),
2483
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.label, { color }], children: item.label })
2484
+ ]
2485
+ }
2486
+ );
2487
+ };
2488
+ var DEFAULT_TEST_ID = "bottom-tab-bar";
2489
+ var SLIDER_HEIGHT = 3;
2490
+ var styles8 = reactNative.StyleSheet.create({
2491
+ bar: { flexDirection: "row", borderTopWidth: reactNative.StyleSheet.hairlineWidth },
2492
+ slider: { position: "absolute", top: 0, height: SLIDER_HEIGHT }
2493
+ });
2494
+ var BottomTabBar = ({
2495
+ items,
2496
+ pathname,
2497
+ onNavigate,
2498
+ tabFeedback = "default" /* Default */,
2499
+ navigateHint = (label) => label,
2500
+ regionLabel = "",
2501
+ containerStyle,
2502
+ testID = DEFAULT_TEST_ID
2503
+ }) => {
2504
+ const { theme } = uiFeedback.useUi();
2505
+ const reducedMotion = useReducedMotion();
2506
+ const primary = theme.palette.primary["500"];
2507
+ const activeRoute = resolveActiveRoute(items, pathname);
2508
+ const activeIndex = items.findIndex((item) => item.route === activeRoute);
2509
+ const bar = tabFeedback === "sliding-bar" /* SlidingBar */ ? slidingBarPosition(activeIndex, items.length) : null;
2510
+ const barMs = resolveTabFeedback(tabFeedback, { active: true, pressed: false }, reducedMotion).durationMs;
2511
+ const colors = { active: primary, rest: theme.colors.textSecondary, pill: theme.colors.surfaceElevated };
2512
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2513
+ reactNative.View,
2514
+ {
2515
+ "aria-label": regionLabel,
2516
+ role: "navigation",
2517
+ style: [styles8.bar, { backgroundColor: theme.colors.surface, borderTopColor: theme.colors.border }, containerStyle],
2518
+ testID,
2519
+ children: [
2520
+ bar === null ? null : /* @__PURE__ */ jsxRuntime.jsx(
2521
+ reactNative.View,
2522
+ {
2523
+ pointerEvents: "none",
2524
+ style: [styles8.slider, { left: bar.left, width: bar.width, backgroundColor: primary }, webTransition("left", barMs, reducedMotion)]
2525
+ }
2526
+ ),
2527
+ items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
2528
+ BottomTab,
2529
+ {
2530
+ colors,
2531
+ isActive: item.route === activeRoute,
2532
+ item,
2533
+ kind: tabFeedback,
2534
+ navigateHint,
2535
+ reducedMotion,
2536
+ onPress: onNavigate
2537
+ },
2538
+ item.key
2539
+ ))
2540
+ ]
2541
+ }
2542
+ );
2543
+ };
2544
+
2223
2545
  exports.ACTIVE_BORDER_RADIUS = ACTIVE_BORDER_RADIUS;
2224
2546
  exports.ACTIVE_TINT_OPACITY = ACTIVE_TINT_OPACITY;
2225
2547
  exports.APP_SHELL_SUFFIX = APP_SHELL_SUFFIX;
2226
2548
  exports.AppShell = AppShell;
2227
2549
  exports.BASE_INDENT = BASE_INDENT;
2550
+ exports.BottomTabBar = BottomTabBar;
2228
2551
  exports.CHEVRON_ICON_SIZE = CHEVRON_ICON_SIZE;
2229
2552
  exports.CollapsedRail = CollapsedRail;
2230
2553
  exports.CommandPalette = CommandPalette;
@@ -2240,6 +2563,7 @@ exports.NAV_TEST_IDS = NAV_TEST_IDS;
2240
2563
  exports.Nav = Nav;
2241
2564
  exports.NavBar = NavBar;
2242
2565
  exports.NavExpandableItem = NavExpandableItem;
2566
+ exports.NavHoverFeedback = NavHoverFeedback;
2243
2567
  exports.NavOverflowMenu = NavOverflowMenu;
2244
2568
  exports.NavShell = NavShell;
2245
2569
  exports.PillNav = PillNav;
@@ -2248,6 +2572,7 @@ exports.RAIL_FULL_BREAKPOINT = RAIL_FULL_BREAKPOINT;
2248
2572
  exports.Sidebar = Sidebar;
2249
2573
  exports.SidebarChevron = SidebarChevron;
2250
2574
  exports.SidebarSearch = SidebarSearch;
2575
+ exports.TabFeedback = TabFeedback;
2251
2576
  exports.Topbar = Topbar;
2252
2577
  exports.accessibleNavItems = accessibleNavItems;
2253
2578
  exports.collapsedRailStyles = collapsedRailStyles;