@hjmds/react-native 1.10.0 → 1.11.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.
Files changed (150) hide show
  1. package/README.md +41 -32
  2. package/THIRD_PARTY_NOTICES.md +78 -0
  3. package/dist/actions.d.ts +29 -46
  4. package/dist/actions.d.ts.map +1 -1
  5. package/dist/actions.js +16 -58
  6. package/dist/actions.js.map +1 -1
  7. package/dist/activity-heatmap.d.ts +9 -0
  8. package/dist/activity-heatmap.d.ts.map +1 -0
  9. package/dist/activity-heatmap.js +10 -0
  10. package/dist/activity-heatmap.js.map +1 -0
  11. package/dist/auth-screen.d.ts +5 -4
  12. package/dist/auth-screen.d.ts.map +1 -1
  13. package/dist/auth-screen.js +14 -4
  14. package/dist/auth-screen.js.map +1 -1
  15. package/dist/avatar-blobatar-motion.d.ts +5 -0
  16. package/dist/avatar-blobatar-motion.d.ts.map +1 -0
  17. package/dist/avatar-blobatar-motion.js +33 -0
  18. package/dist/avatar-blobatar-motion.js.map +1 -0
  19. package/dist/avatar-blobatar.d.ts +6 -0
  20. package/dist/avatar-blobatar.d.ts.map +1 -0
  21. package/dist/avatar-blobatar.js +10 -0
  22. package/dist/avatar-blobatar.js.map +1 -0
  23. package/dist/carousel-motion.d.ts +4 -1
  24. package/dist/carousel-motion.d.ts.map +1 -1
  25. package/dist/carousel-motion.js +16 -7
  26. package/dist/carousel-motion.js.map +1 -1
  27. package/dist/code-block.d.ts +8 -0
  28. package/dist/code-block.d.ts.map +1 -0
  29. package/dist/code-block.js +17 -0
  30. package/dist/code-block.js.map +1 -0
  31. package/dist/content-transition.d.ts +5 -2
  32. package/dist/content-transition.d.ts.map +1 -1
  33. package/dist/content-transition.js +14 -8
  34. package/dist/content-transition.js.map +1 -1
  35. package/dist/data-display.d.ts +9 -46
  36. package/dist/data-display.d.ts.map +1 -1
  37. package/dist/data-display.js +50 -108
  38. package/dist/data-display.js.map +1 -1
  39. package/dist/duration-field.d.ts +9 -0
  40. package/dist/duration-field.d.ts.map +1 -0
  41. package/dist/duration-field.js +15 -0
  42. package/dist/duration-field.js.map +1 -0
  43. package/dist/effect-surface.d.ts +11 -0
  44. package/dist/effect-surface.d.ts.map +1 -0
  45. package/dist/effect-surface.js +61 -0
  46. package/dist/effect-surface.js.map +1 -0
  47. package/dist/evidence.d.ts +1 -1
  48. package/dist/evidence.js +1 -1
  49. package/dist/feedback.d.ts +1 -5
  50. package/dist/feedback.d.ts.map +1 -1
  51. package/dist/feedback.js +41 -24
  52. package/dist/feedback.js.map +1 -1
  53. package/dist/floating-action-button.d.ts +2 -2
  54. package/dist/floating-action-button.d.ts.map +1 -1
  55. package/dist/floating-action-button.js +7 -7
  56. package/dist/floating-action-button.js.map +1 -1
  57. package/dist/folder-preview.d.ts +11 -0
  58. package/dist/folder-preview.d.ts.map +1 -0
  59. package/dist/folder-preview.js +10 -0
  60. package/dist/folder-preview.js.map +1 -0
  61. package/dist/forms.d.ts +2 -20
  62. package/dist/forms.d.ts.map +1 -1
  63. package/dist/forms.js +14 -27
  64. package/dist/forms.js.map +1 -1
  65. package/dist/gravity-letters.d.ts +8 -0
  66. package/dist/gravity-letters.d.ts.map +1 -0
  67. package/dist/gravity-letters.js +34 -0
  68. package/dist/gravity-letters.js.map +1 -0
  69. package/dist/grid-reveal.d.ts +9 -0
  70. package/dist/grid-reveal.d.ts.map +1 -0
  71. package/dist/grid-reveal.js +24 -0
  72. package/dist/grid-reveal.js.map +1 -0
  73. package/dist/icon-lucide.d.ts +19 -0
  74. package/dist/icon-lucide.d.ts.map +1 -0
  75. package/dist/icon-lucide.js +11 -0
  76. package/dist/icon-lucide.js.map +1 -0
  77. package/dist/index.d.ts +6 -6
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js.map +1 -1
  80. package/dist/inline-confirm.d.ts +13 -0
  81. package/dist/inline-confirm.d.ts.map +1 -0
  82. package/dist/inline-confirm.js +40 -0
  83. package/dist/inline-confirm.js.map +1 -0
  84. package/dist/inputs.d.ts +7 -53
  85. package/dist/inputs.d.ts.map +1 -1
  86. package/dist/inputs.js +77 -81
  87. package/dist/inputs.js.map +1 -1
  88. package/dist/internal/field-frame.d.ts +16 -0
  89. package/dist/internal/field-frame.d.ts.map +1 -0
  90. package/dist/internal/field-frame.js +19 -0
  91. package/dist/internal/field-frame.js.map +1 -0
  92. package/dist/internal/recipe-button.d.ts +28 -0
  93. package/dist/internal/recipe-button.d.ts.map +1 -0
  94. package/dist/internal/recipe-button.js +64 -0
  95. package/dist/internal/recipe-button.js.map +1 -0
  96. package/dist/navigation-bar.d.ts +10 -0
  97. package/dist/navigation-bar.d.ts.map +1 -0
  98. package/dist/navigation-bar.js +14 -0
  99. package/dist/navigation-bar.js.map +1 -0
  100. package/dist/navigation.d.ts +11 -48
  101. package/dist/navigation.d.ts.map +1 -1
  102. package/dist/navigation.js +189 -132
  103. package/dist/navigation.js.map +1 -1
  104. package/dist/notification-bell.d.ts +11 -0
  105. package/dist/notification-bell.d.ts.map +1 -0
  106. package/dist/notification-bell.js +30 -0
  107. package/dist/notification-bell.js.map +1 -0
  108. package/dist/overlays.d.ts.map +1 -1
  109. package/dist/overlays.js +10 -4
  110. package/dist/overlays.js.map +1 -1
  111. package/dist/primitives.d.ts +9 -45
  112. package/dist/primitives.d.ts.map +1 -1
  113. package/dist/primitives.js +11 -24
  114. package/dist/primitives.js.map +1 -1
  115. package/dist/reaction-picker.d.ts +10 -0
  116. package/dist/reaction-picker.d.ts.map +1 -0
  117. package/dist/reaction-picker.js +11 -0
  118. package/dist/reaction-picker.js.map +1 -0
  119. package/dist/scroll-progress.d.ts +9 -0
  120. package/dist/scroll-progress.d.ts.map +1 -0
  121. package/dist/scroll-progress.js +6 -0
  122. package/dist/scroll-progress.js.map +1 -0
  123. package/dist/sheet-gesture.d.ts.map +1 -1
  124. package/dist/sheet-gesture.js +17 -3
  125. package/dist/sheet-gesture.js.map +1 -1
  126. package/dist/statistic-motion.d.ts +11 -0
  127. package/dist/statistic-motion.d.ts.map +1 -0
  128. package/dist/statistic-motion.js +13 -0
  129. package/dist/statistic-motion.js.map +1 -0
  130. package/dist/step-player.d.ts +24 -0
  131. package/dist/step-player.d.ts.map +1 -0
  132. package/dist/step-player.js +16 -0
  133. package/dist/step-player.js.map +1 -0
  134. package/dist/task-list.d.ts +16 -0
  135. package/dist/task-list.d.ts.map +1 -0
  136. package/dist/task-list.js +19 -0
  137. package/dist/task-list.js.map +1 -0
  138. package/dist/thinking-orb.d.ts +1 -1
  139. package/dist/thinking-orb.d.ts.map +1 -1
  140. package/dist/thinking-orb.js +4 -4
  141. package/dist/thinking-orb.js.map +1 -1
  142. package/dist/toast-liquid.d.ts.map +1 -1
  143. package/dist/toast-liquid.js +14 -10
  144. package/dist/toast-liquid.js.map +1 -1
  145. package/dist/voice-note.d.ts +24 -0
  146. package/dist/voice-note.d.ts.map +1 -0
  147. package/dist/voice-note.js +13 -0
  148. package/dist/voice-note.js.map +1 -0
  149. package/docs/liquid-toast.md +48 -2
  150. package/package.json +136 -7
@@ -1,29 +1,23 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { backdrop, glyph, radius, spacing } from "@hjmds/design-contracts/foundations";
2
+ import { getTabNavigationTarget, resolveInitialTabValue, tabsBehaviorDefaults, } from "@hjmds/design-contracts/behaviors";
3
3
  import { resolveColorReference } from "@hjmds/design-contracts/color-references";
4
- import { bottomNavigationRecipe, counterBadgeRecipe, loadMoreRecipe, menuRecipe, spinnerRecipe, tabsRecipe, topBarRecipe, } from "@hjmds/design-contracts/recipes";
5
- import { createLoadMoreController, validateLoadMoreDescriptor, } from "@hjmds/design-contracts/components/load-more";
6
4
  import { resolveBottomNavigationActivation, resolveBottomNavigationConfiguration, resolveBottomNavigationDescriptor, } from "@hjmds/design-contracts/components/bottom-navigation";
7
- import { getTabNavigationTarget, resolveInitialTabValue, tabsBehaviorDefaults, } from "@hjmds/design-contracts/behaviors";
8
5
  import { flattenCollectionItems, validateCollection, } from "@hjmds/design-contracts/components/collection";
9
- import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react";
10
- import { ActivityIndicator, AccessibilityInfo, Keyboard, Modal, Platform, Pressable, ScrollView, View, findNodeHandle, } from "react-native";
6
+ import { createLoadMoreController, validateLoadMoreDescriptor, } from "@hjmds/design-contracts/components/load-more";
7
+ import { backdrop, glyph, radius, spacing, } from "@hjmds/design-contracts/foundations";
8
+ import { resolveGooeyIndicator, } from "@hjmds/design-contracts/gooey-navigation";
9
+ import { bottomNavigationRecipe, counterBadgeRecipe, loadMoreRecipe, menuRecipe, spinnerRecipe, tabsRecipe, topBarRecipe, } from "@hjmds/design-contracts/recipes";
10
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, } from "react";
11
+ import { AccessibilityInfo, ActivityIndicator, Animated, AppState, Keyboard, Modal, Platform, Pressable, ScrollView, View, findNodeHandle, } from "react-native";
12
+ import { RecipeButton } from "./internal/recipe-button.js";
11
13
  import { isWebRenderer, webOnly, webTabProps } from "./internal/web-a11y.js";
12
14
  import { Button } from "./actions.js";
13
- import { useControllableState } from "./internal/state.js";
15
+ import { Spinner } from "./feedback.js";
14
16
  import { scheduleAfterNativeModalTeardown, shouldAwaitNativeModalDismiss, } from "./internal/modal-lifecycle.js";
17
+ import { useControllableState } from "./internal/state.js";
15
18
  import { minimumTargetStyle } from "./internal/styles.js";
16
19
  import { Text } from "./primitives.js";
17
20
  import { useHjmNativeTheme } from "./provider.js";
18
- import { Spinner } from "./feedback.js";
19
- function resolveTabItems(items, options) {
20
- if ((items === undefined) === (options === undefined)) {
21
- throw new TypeError("Tabs requires exactly one of items or options");
22
- }
23
- if (items !== undefined)
24
- return items;
25
- return options.map(({ value, ...option }) => ({ ...option, id: value }));
26
- }
27
21
  function encodedTabId(value) {
28
22
  return encodeURIComponent(value);
29
23
  }
@@ -59,13 +53,15 @@ export function TabPanel(props) {
59
53
  return (_jsx(View, { nativeID: getTabPanelId(tabsId, value, dynamic ? "dynamic" : "keyed"), accessibilityLabel: label, accessibilityLabelledBy: getTabId(tabsId, dynamic ? activeValue : value), accessibilityElementsHidden: !selected, importantForAccessibility: selected ? "auto" : "no-hide-descendants", pointerEvents: selected ? "auto" : "none", role: "tabpanel", style: [style, selected ? null : { display: "none" }], children: children }));
60
54
  }
61
55
  export function Tabs(props) {
62
- const { id, label, items, options, value: valueProp, defaultValue, onValueChange, activationMode = tabsBehaviorDefaults.activationMode, mountPolicy = tabsBehaviorDefaults.mountPolicy, panelMode = tabsBehaviorDefaults.panelMode, orientation = tabsBehaviorDefaults.orientation, direction: directionProp, loop = tabsBehaviorDefaults.loop, size = tabsRecipe.defaults.size, layout = tabsRecipe.defaults.layout, overflow = tabsRecipe.defaults.overflow, renderPanels = true, children, style, tabListStyle, } = props;
56
+ const { id, label, items, value: valueProp, defaultValue, onValueChange, activationMode = tabsBehaviorDefaults.activationMode, mountPolicy = tabsBehaviorDefaults.mountPolicy, panelMode = tabsBehaviorDefaults.panelMode, appearance = "standard", orientation = tabsBehaviorDefaults.orientation, direction: directionProp, loop = tabsBehaviorDefaults.loop, size = tabsRecipe.defaults.size, layout = tabsRecipe.defaults.layout, overflow = tabsRecipe.defaults.overflow, renderPanels = true, children, style, tabListStyle, } = props;
63
57
  if (!label.trim())
64
58
  throw new TypeError("Tabs label must not be empty");
65
59
  if (panelMode === "dynamic" && mountPolicy !== "active") {
66
60
  throw new TypeError("Tabs dynamic panelMode requires active mountPolicy");
67
61
  }
68
- const tabItems = useMemo(() => resolveTabItems(items, options), [items, options]);
62
+ if ("options" in props || !Array.isArray(items))
63
+ throw new TypeError("Tabs requires items; options was removed");
64
+ const tabItems = items;
69
65
  if (tabItems.length === 0)
70
66
  throw new TypeError("Tabs requires at least one option");
71
67
  const theme = useHjmNativeTheme();
@@ -102,6 +98,46 @@ export function Tabs(props) {
102
98
  const [focusValue, setFocusValue] = useState(selected);
103
99
  const [visited, setVisited] = useState(() => new Set([selected]));
104
100
  const tabRefs = useRef(new Map());
101
+ const gooey = appearance === "gooey" && orientation === "horizontal";
102
+ const [indicatorRects, setIndicatorRects] = useState({});
103
+ const tabScroll = useRef(null);
104
+ const [tabViewport, setTabViewport] = useState(0);
105
+ const [tabContentWidth, setTabContentWidth] = useState(0);
106
+ const destination = indicatorRects[selected];
107
+ useLayoutEffect(() => {
108
+ if (orientation !== "horizontal" || !scrollable || fitted || !destination || !tabViewport)
109
+ return;
110
+ // Physical RTL offset keeps selected labels visible at 200% scale (iOS showcase).
111
+ const x = Math.max(0, Math.min(destination.x + destination.width / 2 - tabViewport / 2, tabContentWidth - tabViewport));
112
+ tabScroll.current?.scrollTo({ x, animated: false });
113
+ }, [selected, destination, tabViewport, tabContentWidth, direction, orientation, scrollable, fitted]);
114
+ const previousIndicator = useRef(null);
115
+ const progress = useRef(new Animated.Value(1)).current;
116
+ const [indicatorRecipe, setIndicatorRecipe] = useState(null);
117
+ useLayoutEffect(() => {
118
+ progress.stopAnimation();
119
+ progress.setValue(1);
120
+ if (!gooey || !destination) {
121
+ previousIndicator.current = null;
122
+ setIndicatorRecipe(null);
123
+ return;
124
+ }
125
+ const previous = previousIndicator.current;
126
+ const animate = previous && previous.value !== selected && !environment.reducedMotion && AppState.currentState === "active";
127
+ const recipe = resolveGooeyIndicator(animate ? previous.rect : destination, destination);
128
+ setIndicatorRecipe(recipe);
129
+ previousIndicator.current = { value: selected, rect: destination };
130
+ if (!animate)
131
+ return;
132
+ progress.setValue(0);
133
+ // Width morphing must use the JS driver; transforms alone would distort the pill corners.
134
+ const animation = Animated.timing(progress, { toValue: 1, duration: recipe.duration, useNativeDriver: false });
135
+ animation.start();
136
+ const stop = () => { animation.stop(); progress.setValue(1); };
137
+ const subscription = AppState.addEventListener("change", state => { if (state !== "active")
138
+ stop(); });
139
+ return () => { stop(); subscription.remove(); };
140
+ }, [gooey, selected, destination, environment.reducedMotion, progress]);
105
141
  useEffect(() => {
106
142
  if (!controlled && !storedValueValid)
107
143
  setSelected(collectionFallback);
@@ -145,6 +181,7 @@ export function Tabs(props) {
145
181
  if (target !== undefined)
146
182
  focusTab(target);
147
183
  };
184
+ // Panel flexGrow preserves intrinsic height in auto-sized stacks; flex:1 collapsed it to zero.
148
185
  const hasPanels = renderPanels &&
149
186
  (children !== undefined || tabItems.some((item) => item.panel !== undefined));
150
187
  return (_jsxs(View, { accessibilityLabel: label, style: [
@@ -154,7 +191,7 @@ export function Tabs(props) {
154
191
  gap: spacing.md,
155
192
  },
156
193
  style,
157
- ], children: [_jsx(ScrollView, { nativeID: id, accessibilityLabel: label, accessibilityRole: "tablist", horizontal: orientation === "horizontal", scrollEnabled: scrollable && !fitted, showsHorizontalScrollIndicator: false, showsVerticalScrollIndicator: false, style: tabListStyle, contentContainerStyle: [
194
+ ], children: [_jsxs(ScrollView, { ref: tabScroll, onLayout: event => setTabViewport(event.nativeEvent.layout.width), onContentSizeChange: width => setTabContentWidth(width), nativeID: id, accessibilityLabel: label, accessibilityRole: "tablist", horizontal: orientation === "horizontal", scrollEnabled: scrollable && !fitted, showsHorizontalScrollIndicator: false, showsVerticalScrollIndicator: false, style: tabListStyle, contentContainerStyle: [
158
195
  {
159
196
  borderBottomColor: orientation === "horizontal"
160
197
  ? resolveColorReference(tabsRecipe.colors.divider, theme.palette)
@@ -172,105 +209,114 @@ export function Tabs(props) {
172
209
  flexGrow: fitted ? 1 : 0,
173
210
  flexDirection: orientation === "vertical" ? "column" : "row",
174
211
  },
175
- ], children: tabItems.map((item) => {
176
- const active = selected === item.id;
177
- return (_jsxs(Pressable, { nativeID: id ? getTabId(id, item.id) : undefined, role: Platform.OS === "ios" ? "button" : "tab", ref: (node) => {
178
- if (node)
179
- tabRefs.current.set(item.id, node);
180
- else
181
- tabRefs.current.delete(item.id);
182
- }, accessibilityActions: [
183
- { name: "activate" },
184
- { name: "increment" },
185
- { name: "decrement" },
186
- ], accessibilityLabel: item.badge
187
- ? `${item.label}, ${item.badgeAccessibilityLabel ?? item.badge}`
188
- : item.label, accessibilityRole: "tab", accessibilityState: { disabled: item.disabled === true, selected: active }, ...webOnly(webTabProps({
189
- selected: active,
190
- disabled: item.disabled === true,
191
- // aria-controls must name a rendered panel; a Tabs without
192
- // panels has nothing for it to point at.
193
- controls: id && hasPanels ? getTabPanelId(id, item.id, panelMode) : undefined,
194
- focused: focusValue === item.id,
195
- orientation,
196
- direction,
197
- onActivate: () => {
212
+ ], children: [gooey && indicatorRecipe ? _jsx(Animated.View, { pointerEvents: "none", accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", style: {
213
+ position: "absolute", bottom: 0, height: 6, borderRadius: radius.full,
214
+ backgroundColor: resolveColorReference(tabsRecipe.colors.indicator, theme.palette),
215
+ left: progress.interpolate({ inputRange: [...indicatorRecipe.input], outputRange: [...indicatorRecipe.x] }),
216
+ width: progress.interpolate({ inputRange: [...indicatorRecipe.input], outputRange: [...indicatorRecipe.width] }),
217
+ } }) : null, tabItems.map((item) => {
218
+ const active = selected === item.id;
219
+ return (_jsxs(Pressable, { onLayout: orientation === "horizontal" ? event => {
220
+ const { x, width } = event.nativeEvent.layout;
221
+ if (width <= 0)
222
+ return;
223
+ setIndicatorRects(current => current[item.id]?.x === x && current[item.id]?.width === width ? current : { ...current, [item.id]: { x, width } });
224
+ } : undefined, nativeID: id ? getTabId(id, item.id) : undefined, role: Platform.OS === "ios" ? "button" : "tab", ref: (node) => {
225
+ if (node)
226
+ tabRefs.current.set(item.id, node);
227
+ else
228
+ tabRefs.current.delete(item.id);
229
+ }, accessibilityActions: [
230
+ { name: "activate" },
231
+ { name: "increment" },
232
+ { name: "decrement" },
233
+ ], accessibilityLabel: item.badge
234
+ ? `${item.label}, ${item.badgeAccessibilityLabel ?? item.badge}`
235
+ : item.label, accessibilityRole: "tab", accessibilityState: { disabled: item.disabled === true, selected: active }, ...webOnly(webTabProps({
236
+ selected: active,
237
+ disabled: item.disabled === true,
238
+ // aria-controls must name a rendered panel; a Tabs without
239
+ // panels has nothing for it to point at.
240
+ controls: id && hasPanels ? getTabPanelId(id, item.id, panelMode) : undefined,
241
+ focused: focusValue === item.id,
242
+ orientation,
243
+ direction,
244
+ onActivate: () => {
245
+ setFocusValue(item.id);
246
+ setSelected(item.id);
247
+ },
248
+ onMoveFocus: (intent) => moveFocus(item.id, intent),
249
+ })), disabled: item.disabled, onAccessibilityAction: (event) => {
250
+ const action = event.nativeEvent.actionName;
251
+ if (action === "activate")
252
+ setSelected(item.id);
253
+ else if (action === "increment")
254
+ moveFocus(item.id, "next");
255
+ else if (action === "decrement")
256
+ moveFocus(item.id, "previous");
257
+ }, onFocus: () => {
258
+ setFocusValue(item.id);
259
+ if (activationMode === "automatic")
260
+ setSelected(item.id);
261
+ }, onPress: () => {
198
262
  setFocusValue(item.id);
199
263
  setSelected(item.id);
200
- },
201
- onMoveFocus: (intent) => moveFocus(item.id, intent),
202
- })), disabled: item.disabled, onAccessibilityAction: (event) => {
203
- const action = event.nativeEvent.actionName;
204
- if (action === "activate")
205
- setSelected(item.id);
206
- else if (action === "increment")
207
- moveFocus(item.id, "next");
208
- else if (action === "decrement")
209
- moveFocus(item.id, "previous");
210
- }, onFocus: () => {
211
- setFocusValue(item.id);
212
- if (activationMode === "automatic")
213
- setSelected(item.id);
214
- }, onPress: () => {
215
- setFocusValue(item.id);
216
- setSelected(item.id);
217
- }, style: ({ pressed }) => [
218
- minimumTargetStyle,
219
- {
220
- alignItems: "center",
221
- backgroundColor: pressed
222
- ? resolveColorReference(tabsRecipe.states.pressedBackground, theme.palette)
223
- : "transparent",
224
- direction,
225
- flex: fitted ? 1 : undefined,
226
- flexDirection: "row",
227
- gap: tabsRecipe.gap,
228
- justifyContent: "center",
229
- minHeight: sizeContract.minHeight,
230
- opacity: item.disabled ? tabsRecipe.states.disabledOpacity : 1,
231
- paddingHorizontal: sizeContract.paddingHorizontal,
232
- position: "relative",
233
- },
234
- ], children: [item.renderLeading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", children: item.renderLeading({
235
- selected: active,
236
- disabled: item.disabled === true,
237
- color: resolveColorReference(active ? tabsRecipe.colors.selected : tabsRecipe.colors.idle, theme.palette),
238
- size: glyph[tabsRecipe.icon.glyph],
239
- glyphSize: glyph[tabsRecipe.icon.glyph],
240
- }) })) : null, _jsx(Text, { align: "center", style: {
241
- color: resolveColorReference(active ? tabsRecipe.colors.selected : tabsRecipe.colors.idle, theme.palette),
242
- fontWeight: active
243
- ? tabsRecipe.label.selectedFontWeight
244
- : tabsRecipe.label.fontWeight,
245
- }, variant: sizeContract.textVariant, children: item.label }), item.badge ? (_jsx(View, { accessible: false, style: {
246
- backgroundColor: colors.bg,
247
- borderRadius: radius.full,
248
- paddingHorizontal: spacing.xs,
249
- }, children: _jsx(Text, { align: "center", tone: "brand", variant: "caption", children: item.badge }) })) : null, active ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", style: {
250
- backgroundColor: resolveColorReference(tabsRecipe.colors.indicator, theme.palette),
251
- ...(orientation === "horizontal"
252
- ? {
253
- bottom: -tabsRecipe.indicatorHeight / 2,
254
- height: tabsRecipe.indicatorHeight,
255
- left: 0,
256
- right: 0,
257
- }
258
- : {
259
- bottom: 0,
260
- end: -tabsRecipe.indicatorHeight / 2,
261
- top: 0,
262
- width: tabsRecipe.indicatorHeight,
263
- }),
264
- position: "absolute",
265
- } })) : null] }, item.id));
266
- }) }), hasPanels ? panelMode === "dynamic" ? (_jsx(View, { accessibilityLabel: tabItems.find((item) => item.id === selected)?.panelAccessibilityLabel, accessibilityLabelledBy: id ? getTabId(id, selected) : undefined, nativeID: id ? getDynamicTabPanelId(id) : undefined, importantForAccessibility: "yes", role: "tabpanel", style: { flex: 1 }, children: tabItems.find((item) => item.id === selected)?.panel ?? children?.(selected) })) : tabItems.map((item) => {
264
+ }, style: ({ pressed }) => [
265
+ minimumTargetStyle,
266
+ {
267
+ alignItems: "center",
268
+ backgroundColor: pressed
269
+ ? resolveColorReference(tabsRecipe.states.pressedBackground, theme.palette)
270
+ : "transparent",
271
+ direction,
272
+ flex: fitted ? 1 : undefined,
273
+ flexDirection: "row",
274
+ gap: tabsRecipe.gap,
275
+ justifyContent: "center",
276
+ minHeight: sizeContract.minHeight,
277
+ opacity: item.disabled ? tabsRecipe.states.disabledOpacity : 1,
278
+ paddingHorizontal: sizeContract.paddingHorizontal,
279
+ position: "relative",
280
+ },
281
+ ], children: [item.renderLeading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", children: item.renderLeading({
282
+ selected: active,
283
+ disabled: item.disabled === true,
284
+ color: resolveColorReference(active ? tabsRecipe.colors.selected : tabsRecipe.colors.idle, theme.palette),
285
+ size: glyph[tabsRecipe.icon.glyph],
286
+ }) })) : null, _jsx(Text, { align: "center", style: {
287
+ color: resolveColorReference(active ? tabsRecipe.colors.selected : tabsRecipe.colors.idle, theme.palette),
288
+ fontWeight: active
289
+ ? tabsRecipe.label.selectedFontWeight
290
+ : tabsRecipe.label.fontWeight,
291
+ }, variant: sizeContract.textVariant, children: item.label }), item.badge ? (_jsx(View, { accessible: false, style: {
292
+ backgroundColor: colors.bg,
293
+ borderRadius: radius.full,
294
+ paddingHorizontal: spacing.xs,
295
+ }, children: _jsx(Text, { align: "center", tone: "brand", variant: "caption", children: item.badge }) })) : null, active && !gooey ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", style: {
296
+ backgroundColor: resolveColorReference(tabsRecipe.colors.indicator, theme.palette),
297
+ ...(orientation === "horizontal"
298
+ ? {
299
+ bottom: -tabsRecipe.indicatorHeight / 2,
300
+ height: tabsRecipe.indicatorHeight,
301
+ left: 0,
302
+ right: 0,
303
+ }
304
+ : {
305
+ bottom: 0,
306
+ end: -tabsRecipe.indicatorHeight / 2,
307
+ top: 0,
308
+ width: tabsRecipe.indicatorHeight,
309
+ }),
310
+ position: "absolute",
311
+ } })) : null] }, item.id));
312
+ })] }), hasPanels ? panelMode === "dynamic" ? (_jsx(View, { accessibilityLabel: tabItems.find((item) => item.id === selected)?.panelAccessibilityLabel, accessibilityLabelledBy: id ? getTabId(id, selected) : undefined, nativeID: id ? getDynamicTabPanelId(id) : undefined, importantForAccessibility: "yes", role: "tabpanel", style: { flexGrow: 1, flexShrink: 1 }, children: tabItems.find((item) => item.id === selected)?.panel ?? children?.(selected) })) : tabItems.map((item) => {
267
313
  const active = item.id === selected;
268
314
  const mounted = mountPolicy === "always" ||
269
315
  (mountPolicy === "visited" && (visited.has(item.id) || active)) ||
270
316
  (mountPolicy === "active" && active);
271
317
  if (!mounted)
272
318
  return null;
273
- return (_jsx(View, { accessibilityLabel: item.panelAccessibilityLabel, accessibilityLabelledBy: id ? getTabId(id, item.id) : undefined, nativeID: id ? getTabPanelId(id, item.id) : undefined, importantForAccessibility: active ? "yes" : "no-hide-descendants", role: "tabpanel", style: { display: active ? "flex" : "none", flex: 1 }, children: item.panel ?? children?.(item.id) }, item.id));
319
+ return (_jsx(View, { accessibilityLabel: item.panelAccessibilityLabel, accessibilityLabelledBy: id ? getTabId(id, item.id) : undefined, nativeID: id ? getTabPanelId(id, item.id) : undefined, importantForAccessibility: active ? "yes" : "no-hide-descendants", role: "tabpanel", style: { display: active ? "flex" : "none", flexGrow: 1, flexShrink: 1 }, children: item.panel ?? children?.(item.id) }, item.id));
274
320
  }) : null] }));
275
321
  }
276
322
  function useBottomNavigationKeyboardVisible() {
@@ -296,6 +342,8 @@ export function BottomNavigation({ descriptor, onActivate, onLongActivate, rende
296
342
  ...configuration,
297
343
  direction: configuration.direction ?? theme.environment.direction,
298
344
  }, resolved.items.length);
345
+ const capsule = presentation.presentation === "capsule";
346
+ const expandedLabels = theme.environment.textScale >= 1.5 || resolved.items.length > 4;
299
347
  const keyboardVisible = useBottomNavigationKeyboardVisible();
300
348
  const density = bottomNavigationRecipe.density[presentation.density];
301
349
  const presentationRecipe = bottomNavigationRecipe.presentations[presentation.presentation];
@@ -326,7 +374,10 @@ export function BottomNavigation({ descriptor, onActivate, onLongActivate, rende
326
374
  ], children: _jsxs(View, { style: [
327
375
  {
328
376
  alignSelf: "center",
329
- backgroundColor: surfaceBackground,
377
+ flexDirection: capsule ? "row" : "column",
378
+ alignItems: capsule ? "center" : undefined,
379
+ gap: capsule ? spacing.xs : 0,
380
+ backgroundColor: capsule ? "transparent" : surfaceBackground,
330
381
  borderColor: surfaceBorder,
331
382
  borderRadius: presentationRecipe.radius
332
383
  ? radius[presentationRecipe.radius]
@@ -347,10 +398,17 @@ export function BottomNavigation({ descriptor, onActivate, onLongActivate, rende
347
398
  surfaceStyle,
348
399
  ], children: [_jsx(View, { accessibilityLabel: resolved.accessibilityLabel, accessibilityRole: "tablist", style: [
349
400
  {
350
- alignItems: "flex-start",
401
+ alignItems: capsule ? "stretch" : "flex-start",
351
402
  direction: presentation.direction,
352
403
  flexDirection: "row",
353
- width: "100%",
404
+ width: capsule ? undefined : "100%",
405
+ flex: capsule ? 1 : undefined,
406
+ minWidth: 0,
407
+ padding: capsule ? spacing.xxs : 0,
408
+ backgroundColor: capsule ? surfaceBackground : undefined,
409
+ borderRadius: capsule ? radius.full : undefined,
410
+ borderWidth: capsule ? presentationRecipe.borderWidth : 0,
411
+ borderColor: surfaceBorder,
354
412
  },
355
413
  listStyle,
356
414
  ], children: resolved.items.map((item, index) => {
@@ -378,15 +436,16 @@ export function BottomNavigation({ descriptor, onActivate, onLongActivate, rende
378
436
  minimumTargetStyle,
379
437
  {
380
438
  alignItems: "center",
381
- backgroundColor: pressed ? pressedBackground : "transparent",
382
- borderRadius: radius.lg,
383
- flex: 1,
439
+ backgroundColor: pressed ? pressedBackground : capsule && selected ? theme.colors.surfaceAccent : "transparent",
440
+ borderRadius: capsule ? radius.full : radius.lg,
441
+ flexDirection: capsule && !expandedLabels ? "row" : "column",
442
+ flex: capsule && selected && !expandedLabels ? 2 : 1,
384
443
  flexShrink: 1,
385
444
  gap: density.gap,
386
- justifyContent: "flex-start",
445
+ justifyContent: capsule ? "center" : "flex-start",
387
446
  marginEnd: index === middleIndex ? centerGap : 0,
388
447
  minHeight: density.itemMinHeight,
389
- minWidth: density.itemMinWidth,
448
+ minWidth: capsule ? 44 : density.itemMinWidth,
390
449
  opacity: item.disabled
391
450
  ? bottomNavigationRecipe.states.disabledOpacity
392
451
  : 1,
@@ -449,7 +508,7 @@ export function BottomNavigation({ descriptor, onActivate, onLongActivate, rende
449
508
  }, children: _jsx(Text, { accessible: false, align: "center", style: {
450
509
  color: resolveColorReference(badgeTone.content, theme.palette),
451
510
  fontWeight: counterBadgeRecipe.fontWeight,
452
- }, variant: badgeMetrics.textVariant, children: item.badge.visibleLabel }) })) })) : null] }), _jsx(Text, { align: "center", allowFontScaling: bottomNavigationRecipe.largeText.allowFontScaling, maxFontSizeMultiplier: bottomNavigationRecipe.largeText.maxFontSizeMultiplier, style: {
511
+ }, variant: badgeMetrics.textVariant, children: item.badge.visibleLabel }) })) })) : null] }), (!capsule || expandedLabels || selected) && _jsx(Text, { align: "center", allowFontScaling: bottomNavigationRecipe.largeText.allowFontScaling, maxFontSizeMultiplier: bottomNavigationRecipe.largeText.maxFontSizeMultiplier, style: {
453
512
  color: selected ? selectedLabelColor : idleColor,
454
513
  flexShrink: 1,
455
514
  fontWeight: selected
@@ -467,6 +526,8 @@ export function BottomNavigation({ descriptor, onActivate, onLongActivate, rende
467
526
  right: 0,
468
527
  top: 0,
469
528
  },
529
+ // Keep creation outside route destinations; absolute center placement overlaps capsule labels.
530
+ capsule ? { position: "relative", left: undefined, right: undefined, top: undefined, bottom: undefined, flexShrink: 0 } : undefined,
470
531
  primaryActionStyle,
471
532
  ], children: primaryAction })) : null] }) }));
472
533
  }
@@ -712,7 +773,7 @@ function useMenuAfterDismiss(visible) {
712
773
  };
713
774
  }
714
775
  /** Sectioned Native action/selection menu with teardown-safe action callbacks. */
715
- export function Menu({ triggerLabel, title = triggerLabel, items, sections, source: sourceProp, selection = { mode: "none" }, onSelect, onAction, onActionAfterDismiss, onSelectionAfterDismiss, open, defaultOpen = false, onOpenChange, onDismiss, disabled = false, readOnly = false, busy = false, readOnlyLabel, asyncState = { status: "idle" }, onRetry, retryLabel, density = menuRecipe.defaults.density, renderLeading, renderTrailing, dismissLabel, trigger, renderTrigger, style, ...modalProps }) {
776
+ export function Menu({ triggerLabel, title = triggerLabel, items, sections, source: sourceProp, selection = { mode: "none" }, onAction, onActionAfterDismiss, onSelectionAfterDismiss, open, defaultOpen = false, onOpenChange, onDismiss, disabled = false, readOnly = false, busy = false, readOnlyLabel, asyncState = { status: "idle" }, onRetry, retryLabel, density = menuRecipe.defaults.density, renderLeading, renderTrailing, dismissLabel, trigger, renderTrigger, style, ...modalProps }) {
716
777
  const providedSources = [sourceProp, items, sections].filter((candidate) => candidate !== undefined).length;
717
778
  if (providedSources !== 1) {
718
779
  throw new TypeError("Menu requires exactly one of source, items, or sections");
@@ -721,7 +782,7 @@ export function Menu({ triggerLabel, title = triggerLabel, items, sections, sour
721
782
  ? { sections }
722
783
  : {
723
784
  items: (items ?? []).map((item) => ({
724
- id: item.value,
785
+ id: item.id,
725
786
  label: item.label,
726
787
  textValue: item.textValue ?? item.label,
727
788
  ...(item.description === undefined ? {} : { description: item.description }),
@@ -734,7 +795,6 @@ export function Menu({ triggerLabel, title = triggerLabel, items, sections, sour
734
795
  });
735
796
  validateCollection(source);
736
797
  const collectionItems = flattenCollectionItems(source);
737
- const legacyItems = new Map((items ?? []).map((item) => [item.value, item]));
738
798
  if (collectionItems.length === 0 && asyncState.status === "idle") {
739
799
  throw new Error("Menu requires an item or a non-idle asyncState");
740
800
  }
@@ -818,12 +878,10 @@ export function Menu({ triggerLabel, title = triggerLabel, items, sections, sour
818
878
  selection.onSelectionChange?.(next);
819
879
  return;
820
880
  }
821
- void onSelect?.(item.id);
822
881
  void onAction?.(item.id);
823
882
  close("selection", onActionAfterDismiss ? () => onActionAfterDismiss(item.id) : null);
824
883
  };
825
884
  const renderItem = (item) => {
826
- const legacyItem = legacyItems.get(item.id);
827
885
  const selected = selection.mode === "single"
828
886
  ? selectedSingle === item.id
829
887
  : selection.mode === "multiple"
@@ -836,14 +894,14 @@ export function Menu({ triggerLabel, title = triggerLabel, items, sections, sour
836
894
  disabled: itemDisabled,
837
895
  color: resolveColorReference(menuRecipe.leading.color, theme.palette),
838
896
  size: glyph[menuRecipe.leading.glyph],
839
- }) ?? legacyItem?.icon;
897
+ });
840
898
  const trailing = renderTrailing?.(item);
841
899
  return (_jsxs(Pressable, { ref: (node) => {
842
900
  if (node)
843
901
  itemRefs.current.set(item.id, node);
844
902
  else
845
903
  itemRefs.current.delete(item.id);
846
- }, accessibilityHint: legacyItem?.accessibilityHint ?? item.description, accessibilityLabel: item.label, accessibilityRole: "menuitem", accessibilityState: {
904
+ }, accessibilityHint: item.description, accessibilityLabel: item.label, accessibilityRole: "menuitem", accessibilityState: {
847
905
  disabled: itemDisabled,
848
906
  ...(selection.mode === "single" ? { selected } : {}),
849
907
  ...(selection.mode === "multiple" ? { checked: selected } : {}),
@@ -895,7 +953,7 @@ export function Menu({ triggerLabel, title = triggerLabel, items, sections, sour
895
953
  position: "absolute",
896
954
  right: 0,
897
955
  top: 0,
898
- } }), _jsxs(View, { accessibilityLabel: title, accessibilityRole: "menu", accessibilityViewIsModal: true, style: {
956
+ } }), _jsxs(View, { accessibilityLabel: title.trim() || triggerLabel, accessibilityRole: "menu", accessibilityViewIsModal: true, style: {
899
957
  alignSelf: "center",
900
958
  backgroundColor: colors.bg,
901
959
  borderRadius: radius.lg,
@@ -904,7 +962,7 @@ export function Menu({ triggerLabel, title = triggerLabel, items, sections, sour
904
962
  maxWidth: 520,
905
963
  padding: spacing.md,
906
964
  width: "100%",
907
- }, children: [_jsx(Text, { tone: "primary", variant: "title", children: title }), _jsxs(ScrollView, { children: [asyncState.status === "loading" || asyncState.status === "error" || asyncState.status === "empty" ? (_jsxs(View, { style: { gap: spacing.sm, minHeight: densityContract.minHeight }, children: [asyncState.status === "loading" ? _jsx(Spinner, { label: asyncState.message }) : (_jsx(Text, { accessibilityLiveRegion: asyncState.status === "error" ? "assertive" : "polite", accessibilityRole: asyncState.status === "error" ? "alert" : undefined, tone: asyncState.status === "error" ? "danger" : "muted", children: asyncState.message })), asyncState.status === "error" && onRetry ? (_jsx(Button, { onPress: onRetry, tone: "secondary", children: retryLabel ?? dismissLabel })) : null] })) : collection, asyncState.status === "loadingMore" ? _jsx(Spinner, { label: asyncState.message }) : null] }), _jsx(Button, { onPress: () => close("programmatic"), tone: "secondary", children: dismissLabel })] })] }) })] }));
965
+ }, children: [title.trim() ? _jsx(Text, { tone: "primary", variant: "title", children: title }) : null, _jsxs(ScrollView, { children: [asyncState.status === "loading" || asyncState.status === "error" || asyncState.status === "empty" ? (_jsxs(View, { style: { gap: spacing.sm, minHeight: densityContract.minHeight }, children: [asyncState.status === "loading" ? _jsx(Spinner, { label: asyncState.message }) : (_jsx(Text, { accessibilityLiveRegion: asyncState.status === "error" ? "assertive" : "polite", accessibilityRole: asyncState.status === "error" ? "alert" : undefined, tone: asyncState.status === "error" ? "danger" : "muted", children: asyncState.message })), asyncState.status === "error" && onRetry ? (_jsx(Button, { onPress: onRetry, tone: "secondary", children: retryLabel ?? dismissLabel })) : null] })) : collection, asyncState.status === "loadingMore" ? _jsx(Spinner, { label: asyncState.message }) : null] }), _jsx(Button, { onPress: () => close("programmatic"), tone: "secondary", children: dismissLabel })] })] }) })] }));
908
966
  }
909
967
  function createNativeLoadMoreControllerFacade() {
910
968
  let active = null;
@@ -924,7 +982,7 @@ function createNativeLoadMoreControllerFacade() {
924
982
  };
925
983
  }
926
984
  /** Collection footer that de-duplicates automatic and manual page requests. */
927
- export const LoadMore = forwardRef(function LoadMore({ descriptor, onLoadMore, mode = loadMoreRecipe.defaults.mode, density = loadMoreRecipe.defaults.density, onRequestOutcome, onRequestError, layoutStyle, style, }, ref) {
985
+ export const LoadMore = forwardRef(function LoadMore({ descriptor, onLoadMore, mode = loadMoreRecipe.defaults.mode, density = loadMoreRecipe.defaults.density, onRequestOutcome, onRequestError, layoutStyle, }, ref) {
928
986
  validateLoadMoreDescriptor(descriptor);
929
987
  const stateRef = useRef(descriptor.state);
930
988
  const handlerRef = useRef(onLoadMore);
@@ -984,9 +1042,8 @@ export const LoadMore = forwardRef(function LoadMore({ descriptor, onLoadMore, m
984
1042
  gap: densityContract.gap,
985
1043
  paddingVertical: densityContract.paddingVertical,
986
1044
  },
987
- style,
988
1045
  layoutStyle,
989
- ], children: state.status === "ready" ? (_jsx(Button, { onPress: () => {
1046
+ ], children: state.status === "ready" ? (_jsx(RecipeButton, { onPress: () => {
990
1047
  void request("manual").catch(() => undefined);
991
1048
  }, labelStyle: {
992
1049
  color: resolveColorReference(loadMoreRecipe.trigger.color, theme.palette),
@@ -1002,7 +1059,7 @@ export const LoadMore = forwardRef(function LoadMore({ descriptor, onLoadMore, m
1002
1059
  justifyContent: "center",
1003
1060
  }, children: [_jsx(ActivityIndicator, { color: resolveColorReference(spinnerRecipe.tones[loadMoreRecipe.spinner.tone], theme.palette), size: loadMoreRecipe.spinner.size }), _jsx(Text, { accessible: false, style: {
1004
1061
  color: resolveColorReference(loadMoreRecipe.status.color, theme.palette),
1005
- }, variant: loadMoreRecipe.status.textVariant, children: labels.loading })] })) : state.status === "error" ? (_jsxs(_Fragment, { children: [_jsx(Text, { accessibilityLiveRegion: "assertive", style: { color: resolveColorReference(loadMoreRecipe.error.color, theme.palette) }, variant: loadMoreRecipe.error.textVariant, children: state.message }), _jsx(Button, { onPress: () => {
1062
+ }, variant: loadMoreRecipe.status.textVariant, children: labels.loading })] })) : state.status === "error" ? (_jsxs(_Fragment, { children: [_jsx(Text, { accessibilityLiveRegion: "assertive", style: { color: resolveColorReference(loadMoreRecipe.error.color, theme.palette) }, variant: loadMoreRecipe.error.textVariant, children: state.message }), _jsx(RecipeButton, { onPress: () => {
1006
1063
  void request("retry").catch(() => undefined);
1007
1064
  }, labelStyle: {
1008
1065
  color: resolveColorReference(loadMoreRecipe.trigger.color, theme.palette),