@hjmds/react-native 0.8.2

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 (94) hide show
  1. package/README.md +310 -0
  2. package/dist/actions.d.ts +128 -0
  3. package/dist/actions.d.ts.map +1 -0
  4. package/dist/actions.js +157 -0
  5. package/dist/actions.js.map +1 -0
  6. package/dist/data-display.d.ts +285 -0
  7. package/dist/data-display.d.ts.map +1 -0
  8. package/dist/data-display.js +775 -0
  9. package/dist/data-display.js.map +1 -0
  10. package/dist/date-picker.d.ts +25 -0
  11. package/dist/date-picker.d.ts.map +1 -0
  12. package/dist/date-picker.js +39 -0
  13. package/dist/date-picker.js.map +1 -0
  14. package/dist/evidence.d.ts +954 -0
  15. package/dist/evidence.d.ts.map +1 -0
  16. package/dist/evidence.js +98 -0
  17. package/dist/evidence.js.map +1 -0
  18. package/dist/feedback.d.ts +148 -0
  19. package/dist/feedback.d.ts.map +1 -0
  20. package/dist/feedback.js +559 -0
  21. package/dist/feedback.js.map +1 -0
  22. package/dist/file-picker.d.ts +20 -0
  23. package/dist/file-picker.d.ts.map +1 -0
  24. package/dist/file-picker.js +37 -0
  25. package/dist/file-picker.js.map +1 -0
  26. package/dist/forms.d.ts +170 -0
  27. package/dist/forms.d.ts.map +1 -0
  28. package/dist/forms.js +595 -0
  29. package/dist/forms.js.map +1 -0
  30. package/dist/index.d.ts +16 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +16 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/inputs-public.d.ts +6 -0
  35. package/dist/inputs-public.d.ts.map +1 -0
  36. package/dist/inputs-public.js +6 -0
  37. package/dist/inputs-public.js.map +1 -0
  38. package/dist/inputs.d.ts +258 -0
  39. package/dist/inputs.d.ts.map +1 -0
  40. package/dist/inputs.js +681 -0
  41. package/dist/inputs.js.map +1 -0
  42. package/dist/internal/modal-lifecycle.d.ts +15 -0
  43. package/dist/internal/modal-lifecycle.d.ts.map +1 -0
  44. package/dist/internal/modal-lifecycle.js +17 -0
  45. package/dist/internal/modal-lifecycle.js.map +1 -0
  46. package/dist/internal/state.d.ts +8 -0
  47. package/dist/internal/state.d.ts.map +1 -0
  48. package/dist/internal/state.js +18 -0
  49. package/dist/internal/state.js.map +1 -0
  50. package/dist/internal/styles.d.ts +29 -0
  51. package/dist/internal/styles.d.ts.map +1 -0
  52. package/dist/internal/styles.js +51 -0
  53. package/dist/internal/styles.js.map +1 -0
  54. package/dist/navigation.d.ts +271 -0
  55. package/dist/navigation.d.ts.map +1 -0
  56. package/dist/navigation.js +978 -0
  57. package/dist/navigation.js.map +1 -0
  58. package/dist/number-field.d.ts +58 -0
  59. package/dist/number-field.d.ts.map +1 -0
  60. package/dist/number-field.js +172 -0
  61. package/dist/number-field.js.map +1 -0
  62. package/dist/otp-field.d.ts +2 -0
  63. package/dist/otp-field.d.ts.map +1 -0
  64. package/dist/otp-field.js +2 -0
  65. package/dist/otp-field.js.map +1 -0
  66. package/dist/overlays.d.ts +68 -0
  67. package/dist/overlays.d.ts.map +1 -0
  68. package/dist/overlays.js +757 -0
  69. package/dist/overlays.js.map +1 -0
  70. package/dist/password-field.d.ts +2 -0
  71. package/dist/password-field.d.ts.map +1 -0
  72. package/dist/password-field.js +2 -0
  73. package/dist/password-field.js.map +1 -0
  74. package/dist/primitives.d.ts +132 -0
  75. package/dist/primitives.d.ts.map +1 -0
  76. package/dist/primitives.js +238 -0
  77. package/dist/primitives.js.map +1 -0
  78. package/dist/provider.d.ts +41 -0
  79. package/dist/provider.d.ts.map +1 -0
  80. package/dist/provider.js +78 -0
  81. package/dist/provider.js.map +1 -0
  82. package/dist/slider.d.ts +45 -0
  83. package/dist/slider.d.ts.map +1 -0
  84. package/dist/slider.js +200 -0
  85. package/dist/slider.js.map +1 -0
  86. package/dist/steps.d.ts +13 -0
  87. package/dist/steps.d.ts.map +1 -0
  88. package/dist/steps.js +13 -0
  89. package/dist/steps.js.map +1 -0
  90. package/dist/upload-item.d.ts +13 -0
  91. package/dist/upload-item.d.ts.map +1 -0
  92. package/dist/upload-item.js +25 -0
  93. package/dist/upload-item.js.map +1 -0
  94. package/package.json +166 -0
@@ -0,0 +1,978 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { glyph, radius, spacing } from "@hjmds/design-contracts/foundations";
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
+ import { resolveBottomNavigationActivation, resolveBottomNavigationConfiguration, resolveBottomNavigationDescriptor, } from "@hjmds/design-contracts/components/bottom-navigation";
7
+ import { getTabNavigationTarget, resolveInitialTabValue, tabsBehaviorDefaults, } from "@hjmds/design-contracts/behaviors";
8
+ import { flattenCollectionItems, validateCollection, } from "@hjmds/design-contracts/components/collection";
9
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, } from "react";
10
+ import { ActivityIndicator, AccessibilityInfo, Keyboard, Modal, Platform, Pressable, ScrollView, View, findNodeHandle, } from "react-native";
11
+ import { Button } from "./actions.js";
12
+ import { useControllableState } from "./internal/state.js";
13
+ import { scheduleAfterNativeModalTeardown, shouldAwaitNativeModalDismiss, } from "./internal/modal-lifecycle.js";
14
+ import { minimumTargetStyle } from "./internal/styles.js";
15
+ import { Text } from "./primitives.js";
16
+ import { useHjmNativeTheme } from "./provider.js";
17
+ import { Spinner } from "./feedback.js";
18
+ function encodedTabId(value) {
19
+ return encodeURIComponent(value);
20
+ }
21
+ export function getTabId(tabsId, value) {
22
+ return `${tabsId}-tab-${encodedTabId(value)}`;
23
+ }
24
+ export function getTabPanelId(tabsId, value, mode = "keyed") {
25
+ return mode === "dynamic"
26
+ ? `${tabsId}-panel`
27
+ : `${tabsId}-panel-${encodedTabId(value)}`;
28
+ }
29
+ export function getDynamicTabPanelId(tabsId) {
30
+ return getTabPanelId(tabsId, "", "dynamic");
31
+ }
32
+ /** External panel host for products that keep routing, query, or list state outside Tabs. */
33
+ export function TabPanel(props) {
34
+ const { tabsId, activeValue, label, children, style } = props;
35
+ const dynamic = props.mode === "dynamic";
36
+ const value = dynamic ? activeValue : props.value;
37
+ const selected = value === activeValue;
38
+ const mountPolicy = dynamic
39
+ ? "active"
40
+ : props.mountPolicy ?? tabsBehaviorDefaults.mountPolicy;
41
+ const [visited, setVisited] = useState(selected);
42
+ useEffect(() => {
43
+ if (selected)
44
+ setVisited(true);
45
+ }, [selected]);
46
+ const mounted = dynamic || selected || mountPolicy === "always" ||
47
+ (mountPolicy === "visited" && visited);
48
+ if (!mounted)
49
+ return null;
50
+ 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 }));
51
+ }
52
+ export function Tabs(props) {
53
+ const { id, label, 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;
54
+ if (!label.trim())
55
+ throw new TypeError("Tabs label must not be empty");
56
+ if (options.length === 0)
57
+ throw new TypeError("Tabs requires at least one option");
58
+ if (panelMode === "dynamic" && mountPolicy !== "active") {
59
+ throw new TypeError("Tabs dynamic panelMode requires active mountPolicy");
60
+ }
61
+ const theme = useHjmNativeTheme();
62
+ const { colors, environment } = theme;
63
+ const direction = directionProp ?? environment.direction;
64
+ const sizeContract = tabsRecipe.sizes[size];
65
+ const fitted = tabsRecipe.layouts[layout].fitted;
66
+ const scrollable = tabsRecipe.overflow[overflow].scrollable;
67
+ const descriptors = options.map((option) => ({
68
+ id: option.value,
69
+ label: option.label,
70
+ ...(option.disabled === undefined ? {} : { disabled: option.disabled }),
71
+ }));
72
+ const collectionFallback = resolveInitialTabValue(descriptors);
73
+ if (collectionFallback === undefined)
74
+ throw new TypeError("Tabs requires an enabled option");
75
+ if (valueProp !== undefined)
76
+ resolveInitialTabValue(descriptors, valueProp);
77
+ const initialRef = useRef(null);
78
+ if (initialRef.current === null) {
79
+ const initial = resolveInitialTabValue(descriptors, valueProp ?? defaultValue);
80
+ if (initial === undefined)
81
+ throw new TypeError("Tabs requires an enabled option");
82
+ initialRef.current = { value: initial };
83
+ }
84
+ const [storedValue, setSelected] = useControllableState({
85
+ ...(valueProp === undefined ? {} : { value: valueProp }),
86
+ defaultValue: initialRef.current.value,
87
+ ...(onValueChange === undefined ? {} : { onChange: onValueChange }),
88
+ });
89
+ const controlled = valueProp !== undefined;
90
+ const storedValueValid = descriptors.some((item) => item.id === storedValue && !item.disabled);
91
+ const selected = storedValueValid ? storedValue : collectionFallback;
92
+ const [focusValue, setFocusValue] = useState(selected);
93
+ const [visited, setVisited] = useState(() => new Set([selected]));
94
+ const tabRefs = useRef(new Map());
95
+ useEffect(() => {
96
+ if (!controlled && !storedValueValid)
97
+ setSelected(collectionFallback);
98
+ }, [collectionFallback, controlled, setSelected, storedValueValid]);
99
+ useEffect(() => {
100
+ if (!descriptors.some((item) => item.id === focusValue && !item.disabled)) {
101
+ setFocusValue(selected);
102
+ }
103
+ }, [descriptors, focusValue, selected]);
104
+ useEffect(() => {
105
+ setVisited((current) => {
106
+ const known = new Set(options.map((option) => option.value));
107
+ const next = new Set([...current].filter((id) => known.has(id)));
108
+ next.add(selected);
109
+ if (next.size === current.size && [...next].every((id) => current.has(id)))
110
+ return current;
111
+ return next;
112
+ });
113
+ }, [options, selected]);
114
+ const focusTab = (target) => {
115
+ setFocusValue(target);
116
+ if (activationMode === "automatic")
117
+ setSelected(target);
118
+ queueMicrotask(() => {
119
+ const node = tabRefs.current.get(target);
120
+ if (!node)
121
+ return;
122
+ const handle = findNodeHandle(node);
123
+ if (handle !== null)
124
+ AccessibilityInfo.setAccessibilityFocus(handle);
125
+ });
126
+ };
127
+ const moveFocus = (from, intent) => {
128
+ const target = getTabNavigationTarget(descriptors, from, intent, loop);
129
+ if (target !== undefined)
130
+ focusTab(target);
131
+ };
132
+ const hasPanels = renderPanels &&
133
+ (children !== undefined || options.some((option) => option.panel !== undefined));
134
+ return (_jsxs(View, { accessibilityLabel: label, style: [
135
+ {
136
+ direction,
137
+ flexDirection: orientation === "vertical" ? "row" : "column",
138
+ gap: spacing.md,
139
+ },
140
+ style,
141
+ ], children: [_jsx(ScrollView, { nativeID: id, accessibilityLabel: label, accessibilityRole: "tablist", horizontal: orientation === "horizontal", scrollEnabled: scrollable && !fitted, showsHorizontalScrollIndicator: false, showsVerticalScrollIndicator: false, style: tabListStyle, contentContainerStyle: [
142
+ {
143
+ borderBottomColor: orientation === "horizontal"
144
+ ? resolveColorReference(tabsRecipe.colors.divider, theme.palette)
145
+ : undefined,
146
+ borderBottomWidth: orientation === "horizontal"
147
+ ? tabsRecipe.indicatorHeight / 2
148
+ : 0,
149
+ borderEndColor: orientation === "vertical"
150
+ ? resolveColorReference(tabsRecipe.colors.divider, theme.palette)
151
+ : undefined,
152
+ borderEndWidth: orientation === "vertical"
153
+ ? tabsRecipe.indicatorHeight / 2
154
+ : 0,
155
+ direction,
156
+ flexGrow: fitted ? 1 : 0,
157
+ flexDirection: orientation === "vertical" ? "column" : "row",
158
+ },
159
+ ], children: options.map((option) => {
160
+ const active = selected === option.value;
161
+ return (_jsxs(Pressable, { nativeID: id ? getTabId(id, option.value) : undefined, role: Platform.OS === "ios" ? "button" : "tab", ref: (node) => {
162
+ if (node)
163
+ tabRefs.current.set(option.value, node);
164
+ else
165
+ tabRefs.current.delete(option.value);
166
+ }, accessibilityActions: [
167
+ { name: "activate" },
168
+ { name: "increment" },
169
+ { name: "decrement" },
170
+ ], accessibilityLabel: option.badge
171
+ ? `${option.label}, ${option.badgeAccessibilityLabel ?? option.badge}`
172
+ : option.label, accessibilityRole: "tab", accessibilityState: { disabled: option.disabled === true, selected: active }, disabled: option.disabled, onAccessibilityAction: (event) => {
173
+ const action = event.nativeEvent.actionName;
174
+ if (action === "activate")
175
+ setSelected(option.value);
176
+ else if (action === "increment")
177
+ moveFocus(option.value, "next");
178
+ else if (action === "decrement")
179
+ moveFocus(option.value, "previous");
180
+ }, onFocus: () => {
181
+ setFocusValue(option.value);
182
+ if (activationMode === "automatic")
183
+ setSelected(option.value);
184
+ }, onPress: () => {
185
+ setFocusValue(option.value);
186
+ setSelected(option.value);
187
+ }, style: ({ pressed }) => [
188
+ minimumTargetStyle,
189
+ {
190
+ alignItems: "center",
191
+ backgroundColor: pressed
192
+ ? resolveColorReference(tabsRecipe.states.pressedBackground, theme.palette)
193
+ : "transparent",
194
+ direction,
195
+ flex: fitted ? 1 : undefined,
196
+ flexDirection: "row",
197
+ gap: tabsRecipe.gap,
198
+ justifyContent: "center",
199
+ minHeight: sizeContract.minHeight,
200
+ opacity: option.disabled ? tabsRecipe.states.disabledOpacity : 1,
201
+ paddingHorizontal: sizeContract.paddingHorizontal,
202
+ position: "relative",
203
+ },
204
+ ], children: [option.renderLeading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", children: option.renderLeading({
205
+ selected: active,
206
+ disabled: option.disabled === true,
207
+ color: resolveColorReference(active ? tabsRecipe.colors.selected : tabsRecipe.colors.idle, theme.palette),
208
+ size: glyph[tabsRecipe.icon.glyph],
209
+ glyphSize: glyph[tabsRecipe.icon.glyph],
210
+ }) })) : null, _jsx(Text, { align: "center", style: {
211
+ color: resolveColorReference(active ? tabsRecipe.colors.selected : tabsRecipe.colors.idle, theme.palette),
212
+ fontWeight: active
213
+ ? tabsRecipe.label.selectedFontWeight
214
+ : tabsRecipe.label.fontWeight,
215
+ }, variant: sizeContract.textVariant, children: option.label }), option.badge ? (_jsx(View, { accessible: false, style: {
216
+ backgroundColor: colors.surfaceAccent,
217
+ borderRadius: radius.full,
218
+ paddingHorizontal: spacing.xs,
219
+ }, children: _jsx(Text, { align: "center", tone: "brand", variant: "caption", children: option.badge }) })) : null, active ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", style: {
220
+ backgroundColor: resolveColorReference(tabsRecipe.colors.indicator, theme.palette),
221
+ ...(orientation === "horizontal"
222
+ ? {
223
+ bottom: -tabsRecipe.indicatorHeight / 2,
224
+ height: tabsRecipe.indicatorHeight,
225
+ left: 0,
226
+ right: 0,
227
+ }
228
+ : {
229
+ bottom: 0,
230
+ end: -tabsRecipe.indicatorHeight / 2,
231
+ top: 0,
232
+ width: tabsRecipe.indicatorHeight,
233
+ }),
234
+ position: "absolute",
235
+ } })) : null] }, option.value));
236
+ }) }), hasPanels ? panelMode === "dynamic" ? (_jsx(View, { accessibilityLabel: options.find((option) => option.value === selected)?.panelAccessibilityLabel, accessibilityLabelledBy: id ? getTabId(id, selected) : undefined, nativeID: id ? getDynamicTabPanelId(id) : undefined, importantForAccessibility: "yes", role: "tabpanel", style: { flex: 1 }, children: options.find((option) => option.value === selected)?.panel ?? children?.(selected) })) : options.map((option) => {
237
+ const active = option.value === selected;
238
+ const mounted = mountPolicy === "always" ||
239
+ (mountPolicy === "visited" && (visited.has(option.value) || active)) ||
240
+ (mountPolicy === "active" && active);
241
+ if (!mounted)
242
+ return null;
243
+ return (_jsx(View, { accessibilityLabel: option.panelAccessibilityLabel, accessibilityLabelledBy: id ? getTabId(id, option.value) : undefined, nativeID: id ? getTabPanelId(id, option.value) : undefined, importantForAccessibility: active ? "yes" : "no-hide-descendants", role: "tabpanel", style: { display: active ? "flex" : "none", flex: 1 }, children: option.panel ?? children?.(option.value) }, option.value));
244
+ }) : null] }));
245
+ }
246
+ function useBottomNavigationKeyboardVisible() {
247
+ const [visible, setVisible] = useState(() => Keyboard.isVisible());
248
+ useEffect(() => {
249
+ const show = Keyboard.addListener("keyboardDidShow", () => setVisible(true));
250
+ const hide = Keyboard.addListener("keyboardDidHide", () => setVisible(false));
251
+ return () => {
252
+ show.remove();
253
+ hide.remove();
254
+ };
255
+ }, []);
256
+ return visible;
257
+ }
258
+ /** Router-owned persistent destinations; activation emits intent without mutating selection. */
259
+ export function BottomNavigation({ descriptor, onActivate, onLongActivate, renderIcon, renderBadge, getItemTestID, primaryAction, configuration = {}, safeAreaBottom = 0, style, surfaceStyle, listStyle, primaryActionStyle, }) {
260
+ const resolved = resolveBottomNavigationDescriptor(descriptor);
261
+ if (!Number.isFinite(safeAreaBottom) || safeAreaBottom < 0) {
262
+ throw new RangeError("BottomNavigation safeAreaBottom must be non-negative");
263
+ }
264
+ const theme = useHjmNativeTheme();
265
+ const presentation = resolveBottomNavigationConfiguration({
266
+ ...configuration,
267
+ direction: configuration.direction ?? theme.environment.direction,
268
+ }, resolved.items.length);
269
+ const keyboardVisible = useBottomNavigationKeyboardVisible();
270
+ const density = bottomNavigationRecipe.density[presentation.density];
271
+ const presentationRecipe = bottomNavigationRecipe.presentations[presentation.presentation];
272
+ const centerGap = bottomNavigationRecipe.distributions[presentation.distribution].centerGap;
273
+ const middleIndex = resolved.items.length / 2 - 1;
274
+ const idleColor = resolveColorReference(bottomNavigationRecipe.colors.idle, theme.palette);
275
+ const selectedIconColor = resolveColorReference(bottomNavigationRecipe.colors.selectedIcon, theme.palette);
276
+ const selectedLabelColor = resolveColorReference(bottomNavigationRecipe.colors.selectedLabel, theme.palette);
277
+ const pressedBackground = resolveColorReference(bottomNavigationRecipe.states.pressedBackground, theme.palette);
278
+ const surfaceBackground = resolveColorReference(presentationRecipe.background, theme.palette);
279
+ const surfaceBorder = resolveColorReference(presentationRecipe.border, theme.palette);
280
+ const badgeMetrics = counterBadgeRecipe.sizes[bottomNavigationRecipe.badge.size];
281
+ const badgeTone = counterBadgeRecipe.tones.danger;
282
+ const badgeVariant = counterBadgeRecipe.variants[bottomNavigationRecipe.badge.variant];
283
+ const [focusedKey, setFocusedKey] = useState(null);
284
+ if (presentation.keyboardBehavior === "hide" && keyboardVisible)
285
+ return null;
286
+ return (_jsx(View, { style: [
287
+ {
288
+ backgroundColor: presentation.presentation === "bar" ? surfaceBackground : "transparent",
289
+ direction: presentation.direction,
290
+ paddingBottom: safeAreaBottom + bottomNavigationRecipe.safeArea.minimumBottomPadding,
291
+ paddingHorizontal: presentationRecipe.outerPaddingHorizontal,
292
+ paddingTop: presentationRecipe.outerPaddingTop,
293
+ width: "100%",
294
+ },
295
+ style,
296
+ ], children: _jsxs(View, { style: [
297
+ {
298
+ alignSelf: "center",
299
+ backgroundColor: surfaceBackground,
300
+ borderColor: surfaceBorder,
301
+ borderRadius: presentationRecipe.radius
302
+ ? radius[presentationRecipe.radius]
303
+ : 0,
304
+ borderTopWidth: presentation.presentation === "bar" ? presentationRecipe.borderWidth : 0,
305
+ borderWidth: presentation.presentation === "floating" ? presentationRecipe.borderWidth : 0,
306
+ elevation: presentationRecipe.shadow ? 8 : 0,
307
+ maxWidth: presentationRecipe.maxWidth ?? undefined,
308
+ position: "relative",
309
+ shadowColor: presentationRecipe.shadow?.color,
310
+ shadowOffset: presentationRecipe.shadow
311
+ ? { width: 0, height: presentationRecipe.shadow.offsetY }
312
+ : undefined,
313
+ shadowOpacity: presentationRecipe.shadow?.opacity,
314
+ shadowRadius: presentationRecipe.shadow?.radius,
315
+ width: "100%",
316
+ },
317
+ surfaceStyle,
318
+ ], children: [_jsx(View, { accessibilityLabel: resolved.accessibilityLabel, accessibilityRole: "tablist", style: [
319
+ {
320
+ alignItems: "flex-start",
321
+ direction: presentation.direction,
322
+ flexDirection: "row",
323
+ width: "100%",
324
+ },
325
+ listStyle,
326
+ ], children: resolved.items.map((item, index) => {
327
+ const selected = item.id === resolved.selectedKey;
328
+ const focused = focusedKey === item.id;
329
+ const sourceBadge = descriptor.items[index]?.badge;
330
+ return (_jsxs(Pressable, { accessibilityLabel: item.resolvedAccessibilityLabel, accessibilityRole: "tab", accessibilityState: { disabled: item.disabled, selected }, disabled: item.disabled, onBlur: () => {
331
+ setFocusedKey((current) => current === item.id ? null : current);
332
+ }, onFocus: () => setFocusedKey(item.id), onLongPress: onLongActivate
333
+ ? () => {
334
+ const activation = resolveBottomNavigationActivation(descriptor, item.id);
335
+ if (activation)
336
+ onLongActivate(activation);
337
+ }
338
+ : undefined, onPress: () => {
339
+ const activation = resolveBottomNavigationActivation(descriptor, item.id);
340
+ if (activation)
341
+ onActivate(activation);
342
+ }, testID: getItemTestID?.(item), style: ({ pressed }) => [
343
+ minimumTargetStyle,
344
+ {
345
+ alignItems: "center",
346
+ backgroundColor: pressed ? pressedBackground : "transparent",
347
+ borderRadius: radius.lg,
348
+ flex: 1,
349
+ flexShrink: 1,
350
+ gap: density.gap,
351
+ justifyContent: "flex-start",
352
+ marginEnd: index === middleIndex ? centerGap : 0,
353
+ minHeight: density.itemMinHeight,
354
+ minWidth: density.itemMinWidth,
355
+ opacity: item.disabled
356
+ ? bottomNavigationRecipe.states.disabledOpacity
357
+ : 1,
358
+ outlineColor: focused
359
+ ? resolveColorReference(bottomNavigationRecipe.states.focus.color, theme.palette)
360
+ : "transparent",
361
+ outlineOffset: focused
362
+ ? bottomNavigationRecipe.states.focus.offset
363
+ : 0,
364
+ outlineStyle: focused ? "solid" : undefined,
365
+ outlineWidth: focused
366
+ ? bottomNavigationRecipe.states.focus.width
367
+ : 0,
368
+ padding: density.padding,
369
+ },
370
+ ], children: [_jsxs(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", style: {
371
+ alignItems: "center",
372
+ borderRadius: radius[bottomNavigationRecipe.indicator.radius],
373
+ borderWidth: bottomNavigationRecipe.indicator.borderWidth,
374
+ justifyContent: "center",
375
+ minHeight: bottomNavigationRecipe.indicator.minHeight,
376
+ minWidth: bottomNavigationRecipe.indicator.minWidth,
377
+ position: "relative",
378
+ transform: [{
379
+ scale: selected
380
+ ? bottomNavigationRecipe.icon.selectedEmphasis.scale.selected
381
+ : bottomNavigationRecipe.icon.selectedEmphasis.scale.idle,
382
+ }],
383
+ }, children: [renderIcon({
384
+ item,
385
+ name: item.icon.name,
386
+ selected,
387
+ color: selected ? selectedIconColor : idleColor,
388
+ size: glyph[density.icon],
389
+ strokeWidth: selected
390
+ ? bottomNavigationRecipe.icon.selectedEmphasis.strokeWidth.selected
391
+ : bottomNavigationRecipe.icon.selectedEmphasis.strokeWidth.idle,
392
+ }), item.badge && sourceBadge ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", style: {
393
+ end: bottomNavigationRecipe.badge.anchor.inlineEnd,
394
+ position: "absolute",
395
+ top: bottomNavigationRecipe.badge.anchor.blockStart,
396
+ }, children: renderBadge ? renderBadge({
397
+ item,
398
+ badge: item.badge,
399
+ count: sourceBadge.count,
400
+ ...(sourceBadge.max === undefined ? {} : { max: sourceBadge.max }),
401
+ selected,
402
+ }) : (_jsx(View, { style: {
403
+ alignItems: "center",
404
+ backgroundColor: resolveColorReference(badgeTone.background, theme.palette),
405
+ borderColor: badgeVariant.border
406
+ ? resolveColorReference(badgeVariant.border, theme.palette)
407
+ : "transparent",
408
+ borderRadius: radius[counterBadgeRecipe.radius],
409
+ borderWidth: badgeVariant.borderWidth,
410
+ justifyContent: "center",
411
+ minHeight: badgeMetrics.height,
412
+ minWidth: badgeMetrics.minWidth,
413
+ paddingHorizontal: badgeMetrics.paddingHorizontal,
414
+ }, children: _jsx(Text, { accessible: false, align: "center", style: {
415
+ color: resolveColorReference(badgeTone.content, theme.palette),
416
+ fontWeight: counterBadgeRecipe.fontWeight,
417
+ }, variant: badgeMetrics.textVariant, children: item.badge.visibleLabel }) })) })) : null] }), _jsx(Text, { align: "center", allowFontScaling: bottomNavigationRecipe.largeText.allowFontScaling, maxFontSizeMultiplier: bottomNavigationRecipe.largeText.maxFontSizeMultiplier, style: {
418
+ color: selected ? selectedLabelColor : idleColor,
419
+ flexShrink: 1,
420
+ fontWeight: selected
421
+ ? bottomNavigationRecipe.label.selectedFontWeight
422
+ : bottomNavigationRecipe.label.fontWeight,
423
+ minWidth: 0,
424
+ }, variant: density.label, children: item.label })] }, item.id));
425
+ }) }), primaryAction ? (_jsx(View, { pointerEvents: "box-none", style: [
426
+ {
427
+ alignItems: "center",
428
+ bottom: 0,
429
+ justifyContent: "center",
430
+ left: 0,
431
+ position: "absolute",
432
+ right: 0,
433
+ top: 0,
434
+ },
435
+ primaryActionStyle,
436
+ ], children: primaryAction })) : null] }) }));
437
+ }
438
+ /** Recipe-owned icon-over-micro-label action for Native screen chrome. */
439
+ export function TopBarAction(props) {
440
+ const { label, accessibilityLabel, accessibilityState, children, disabled = false, labelVisibility = "visible", labelStyle, style, ...intentAndHostProps } = props;
441
+ if (!label.trim())
442
+ throw new TypeError("TopBarAction label must not be empty");
443
+ const resolvedAccessibilityLabel = accessibilityLabel ?? label;
444
+ if (!resolvedAccessibilityLabel.trim()) {
445
+ throw new TypeError("TopBarAction accessibilityLabel must not be empty");
446
+ }
447
+ if (children === undefined || children === null || children === false) {
448
+ throw new TypeError("TopBarAction requires icon or visual children");
449
+ }
450
+ const theme = useHjmNativeTheme();
451
+ const intent = props.intent ?? "button";
452
+ const hostProps = intent === "link"
453
+ ? (() => {
454
+ const { intent: _intent, destination: _destination, onNavigate: _onNavigate, renderLink: _renderLink, ...rest } = intentAndHostProps;
455
+ return rest;
456
+ })()
457
+ : (() => {
458
+ const { intent: _intent, onPress: _onPress, renderAction: _renderAction, ...rest } = intentAndHostProps;
459
+ return rest;
460
+ })();
461
+ const handlePress = (event) => {
462
+ if (disabled)
463
+ return;
464
+ if (props.intent === "link") {
465
+ void props.onNavigate?.(props.destination);
466
+ }
467
+ else {
468
+ props.onPress(event);
469
+ }
470
+ };
471
+ const controlProps = {
472
+ ...hostProps,
473
+ accessible: true,
474
+ accessibilityLabel: resolvedAccessibilityLabel,
475
+ accessibilityRole: intent,
476
+ accessibilityState: { ...accessibilityState, disabled },
477
+ children: (_jsxs(_Fragment, { children: [_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", pointerEvents: "none", children: children }), labelVisibility === "visible" ? (_jsx(Text, { accessible: false, style: [
478
+ {
479
+ color: resolveColorReference(topBarRecipe.actionLabel.color, theme.palette),
480
+ fontWeight: topBarRecipe.actionLabel.fontWeight,
481
+ },
482
+ labelStyle,
483
+ ], variant: topBarRecipe.actionLabel.textVariant, children: label })) : null] })),
484
+ disabled,
485
+ onPress: handlePress,
486
+ role: intent,
487
+ style: ({ pressed }) => [
488
+ {
489
+ alignItems: "center",
490
+ direction: theme.environment.direction,
491
+ gap: topBarRecipe.action.gap,
492
+ justifyContent: "center",
493
+ minHeight: topBarRecipe.action.minHeight,
494
+ minWidth: topBarRecipe.action.minWidth,
495
+ opacity: disabled
496
+ ? topBarRecipe.action.disabledOpacity
497
+ : pressed
498
+ ? topBarRecipe.action.pressedOpacity
499
+ : 1,
500
+ paddingHorizontal: topBarRecipe.action.paddingHorizontal,
501
+ },
502
+ style,
503
+ ],
504
+ };
505
+ if (props.intent === "link") {
506
+ if (!props.renderLink && !props.onNavigate) {
507
+ throw new TypeError("TopBarAction link intent requires renderLink or onNavigate");
508
+ }
509
+ const linkProps = { ...controlProps, destination: props.destination };
510
+ return props.renderLink?.(linkProps) ?? _jsx(Pressable, { ...controlProps });
511
+ }
512
+ return props.renderAction?.(controlProps) ?? _jsx(Pressable, { ...controlProps });
513
+ }
514
+ /** Native screen top bar with logical action slots and large-text reflow. */
515
+ export function TopBar({ title, titleLeading, onTitlePress, titleAccessibilityLabel, titleAccessibilityHint, leading, trailing, actions, centered = topBarRecipe.defaults.centered, safeAreaTop = 0, style, leadingStyle, titleStyle, trailingStyle, }) {
516
+ if (!Number.isFinite(safeAreaTop) || safeAreaTop < 0) {
517
+ throw new RangeError("TopBar safeAreaTop must be non-negative");
518
+ }
519
+ if (title !== undefined && !title.trim()) {
520
+ throw new TypeError("TopBar title must be omitted or contain non-whitespace copy");
521
+ }
522
+ if (titleAccessibilityLabel !== undefined && !titleAccessibilityLabel.trim()) {
523
+ throw new TypeError("TopBar titleAccessibilityLabel must not be empty");
524
+ }
525
+ const hasLeading = leading !== undefined && leading !== null && leading !== false;
526
+ const hasTrailing = trailing !== undefined && trailing !== null && trailing !== false;
527
+ const hasActions = actions !== undefined && actions !== null && actions !== false;
528
+ if (hasTrailing && hasActions) {
529
+ throw new TypeError("TopBar accepts either trailing or actions, not both");
530
+ }
531
+ const trailingContent = hasTrailing ? trailing : actions;
532
+ const hasTrailingContent = hasTrailing || hasActions;
533
+ const hasTitle = title !== undefined;
534
+ const resolvedTitle = title ?? "";
535
+ if (!hasTitle && (titleLeading !== undefined ||
536
+ onTitlePress !== undefined ||
537
+ titleAccessibilityLabel !== undefined ||
538
+ titleAccessibilityHint !== undefined)) {
539
+ throw new TypeError("TopBar title affordance props require a title");
540
+ }
541
+ if (titleAccessibilityHint !== undefined && onTitlePress === undefined) {
542
+ throw new TypeError("TopBar titleAccessibilityHint requires onTitlePress");
543
+ }
544
+ const renderCompactLeadingSlot = hasLeading || (hasTitle && centered);
545
+ const renderCompactTrailingSlot = hasTrailingContent || (hasTitle && centered);
546
+ const theme = useHjmNativeTheme();
547
+ const largeText = theme.environment.textScale >= topBarRecipe.largeTextThreshold;
548
+ const resolvedTitleStyle = [
549
+ {
550
+ color: resolveColorReference(topBarRecipe.title.color, theme.palette),
551
+ fontWeight: topBarRecipe.title.fontWeight,
552
+ },
553
+ titleStyle,
554
+ ];
555
+ const renderTitle = (containerStyle, numberOfLines) => {
556
+ const titleContent = (_jsxs(_Fragment, { children: [titleLeading !== undefined && titleLeading !== null && titleLeading !== false ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", pointerEvents: "none", children: titleLeading })) : null, _jsx(Text, { ...(onTitlePress
557
+ ? { accessible: false }
558
+ : {
559
+ accessibilityLabel: titleAccessibilityLabel,
560
+ accessibilityRole: "header",
561
+ }), numberOfLines: numberOfLines, style: [{ flexShrink: 1, minWidth: 0 }, resolvedTitleStyle], variant: topBarRecipe.title.textVariant, children: resolvedTitle })] }));
562
+ const commonStyle = [
563
+ containerStyle,
564
+ {
565
+ alignItems: "center",
566
+ direction: theme.environment.direction,
567
+ flexDirection: "row",
568
+ gap: topBarRecipe.titleAction.gap,
569
+ justifyContent: centered ? "center" : "flex-start",
570
+ minHeight: topBarRecipe.titleAction.minHeight,
571
+ minWidth: topBarRecipe.titleAction.minWidth,
572
+ },
573
+ ];
574
+ if (onTitlePress) {
575
+ return (_jsx(Pressable, { accessible: true, accessibilityHint: titleAccessibilityHint, accessibilityLabel: titleAccessibilityLabel ?? resolvedTitle, accessibilityRole: "button", onPress: onTitlePress, style: ({ pressed }) => [
576
+ commonStyle,
577
+ { opacity: pressed ? topBarRecipe.titleAction.pressedOpacity : 1 },
578
+ ], children: titleContent }));
579
+ }
580
+ return (_jsx(View, { accessible: false, style: commonStyle, children: titleContent }));
581
+ };
582
+ return (_jsx(View, { accessibilityRole: "toolbar", style: [
583
+ {
584
+ alignItems: largeText ? "stretch" : "center",
585
+ backgroundColor: resolveColorReference(topBarRecipe.background, theme.palette),
586
+ direction: theme.environment.direction,
587
+ flexDirection: largeText ? "column" : "row",
588
+ gap: topBarRecipe.gap,
589
+ minHeight: topBarRecipe.minHeight + safeAreaTop,
590
+ paddingHorizontal: topBarRecipe.paddingHorizontal,
591
+ paddingTop: safeAreaTop,
592
+ },
593
+ style,
594
+ ], children: largeText ? (_jsxs(_Fragment, { children: [hasLeading || hasTitle ? (_jsxs(View, { style: {
595
+ alignItems: "center",
596
+ direction: theme.environment.direction,
597
+ flexDirection: "row",
598
+ gap: topBarRecipe.gap,
599
+ width: "100%",
600
+ }, children: [hasLeading ? (_jsx(View, { style: [
601
+ {
602
+ alignItems: "flex-start",
603
+ direction: theme.environment.direction,
604
+ flexDirection: "row",
605
+ minWidth: topBarRecipe.sideMinWidth,
606
+ },
607
+ leadingStyle,
608
+ ], children: leading })) : null, hasTitle ? (renderTitle({ flex: 1, flexShrink: 1, minWidth: 0 })) : null, hasTitle && centered && hasLeading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", pointerEvents: "none", style: { minWidth: topBarRecipe.sideMinWidth } })) : null] })) : null, hasTrailingContent ? (_jsx(View, { style: [
609
+ {
610
+ direction: theme.environment.direction,
611
+ flexDirection: "row",
612
+ flexWrap: "wrap",
613
+ gap: topBarRecipe.gap,
614
+ justifyContent: "flex-end",
615
+ minWidth: topBarRecipe.sideMinWidth,
616
+ width: "100%",
617
+ },
618
+ trailingStyle,
619
+ ], children: trailingContent })) : null] })) : (_jsxs(_Fragment, { children: [renderCompactLeadingSlot ? (_jsx(View, { style: [
620
+ {
621
+ alignItems: "center",
622
+ direction: theme.environment.direction,
623
+ flex: hasTitle ? 1 : undefined,
624
+ flexDirection: "row",
625
+ justifyContent: "flex-start",
626
+ minWidth: topBarRecipe.sideMinWidth,
627
+ },
628
+ leadingStyle,
629
+ ], children: hasLeading ? leading : null })) : null, hasTitle ? (renderTitle({ flex: 2, flexShrink: 1, minWidth: 0 }, 1)) : null, renderCompactTrailingSlot ? (_jsx(View, { style: [
630
+ {
631
+ alignItems: "center",
632
+ direction: theme.environment.direction,
633
+ flex: hasTitle ? 1 : undefined,
634
+ flexDirection: "row",
635
+ flexWrap: "wrap",
636
+ gap: topBarRecipe.gap,
637
+ justifyContent: "flex-end",
638
+ marginStart: hasTitle ? undefined : "auto",
639
+ minWidth: topBarRecipe.sideMinWidth,
640
+ },
641
+ trailingStyle,
642
+ ], children: hasTrailingContent ? trailingContent : null })) : null] })) }));
643
+ }
644
+ function useMenuAfterDismiss(visible) {
645
+ const shownRef = useRef(false);
646
+ const previousVisibleRef = useRef(visible);
647
+ const pendingRef = useRef(null);
648
+ const taskRef = useRef(null);
649
+ const complete = useCallback(() => {
650
+ taskRef.current?.cancel();
651
+ taskRef.current = null;
652
+ shownRef.current = false;
653
+ const pending = pendingRef.current;
654
+ pendingRef.current = null;
655
+ if (pending)
656
+ void pending();
657
+ }, []);
658
+ useLayoutEffect(() => {
659
+ const wasVisible = previousVisibleRef.current;
660
+ previousVisibleRef.current = visible;
661
+ if (!wasVisible || visible || shouldAwaitNativeModalDismiss(shownRef.current))
662
+ return;
663
+ taskRef.current?.cancel();
664
+ taskRef.current = scheduleAfterNativeModalTeardown(complete);
665
+ }, [complete, visible]);
666
+ useEffect(() => () => taskRef.current?.cancel(), []);
667
+ return {
668
+ queue(callback) {
669
+ pendingRef.current = callback;
670
+ },
671
+ onDismiss: complete,
672
+ onShow() {
673
+ shownRef.current = true;
674
+ },
675
+ };
676
+ }
677
+ /** Sectioned Native action/selection menu with teardown-safe action callbacks. */
678
+ 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 }) {
679
+ const providedSources = [sourceProp, items, sections].filter((candidate) => candidate !== undefined).length;
680
+ if (providedSources !== 1) {
681
+ throw new TypeError("Menu requires exactly one of source, items, or sections");
682
+ }
683
+ const source = sourceProp ?? (sections
684
+ ? { sections }
685
+ : {
686
+ items: (items ?? []).map((item) => ({
687
+ id: item.value,
688
+ label: item.label,
689
+ textValue: item.textValue ?? item.label,
690
+ ...(item.description === undefined ? {} : { description: item.description }),
691
+ ...(item.shortcut === undefined ? {} : { shortcut: item.shortcut }),
692
+ ...(item.tone === undefined
693
+ ? {}
694
+ : { tone: item.tone === "danger" ? "danger" : "neutral" }),
695
+ ...(item.disabled === undefined ? {} : { disabled: item.disabled }),
696
+ })),
697
+ });
698
+ validateCollection(source);
699
+ const collectionItems = flattenCollectionItems(source);
700
+ const legacyItems = new Map((items ?? []).map((item) => [item.value, item]));
701
+ if (collectionItems.length === 0 && asyncState.status === "idle") {
702
+ throw new Error("Menu requires an item or a non-idle asyncState");
703
+ }
704
+ const theme = useHjmNativeTheme();
705
+ const { colors, environment } = theme;
706
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
707
+ const visible = open ?? uncontrolledOpen;
708
+ const [uncontrolledSingle, setUncontrolledSingle] = useState(selection.mode === "single" ? selection.defaultSelectedKey ?? null : null);
709
+ const [uncontrolledMultiple, setUncontrolledMultiple] = useState(selection.mode === "multiple"
710
+ ? selection.defaultSelectedKeys ?? new Set()
711
+ : new Set());
712
+ const selectedSingle = selection.mode === "single"
713
+ ? selection.selectedKey ?? uncontrolledSingle
714
+ : null;
715
+ const selectedMultiple = selection.mode === "multiple"
716
+ ? selection.selectedKeys ?? uncontrolledMultiple
717
+ : new Set();
718
+ for (const key of selection.mode === "multiple"
719
+ ? selectedMultiple
720
+ : selectedSingle === null ? [] : [selectedSingle]) {
721
+ if (!collectionItems.some((item) => item.id === key)) {
722
+ throw new RangeError(`Menu selection must identify an item: ${key}`);
723
+ }
724
+ }
725
+ const triggerRef = useRef(null);
726
+ const itemRefs = useRef(new Map());
727
+ const modalDismiss = useMenuAfterDismiss(visible);
728
+ const densityContract = menuRecipe.density[density];
729
+ const requestOpen = useCallback((next, reason) => {
730
+ if (next === visible)
731
+ return;
732
+ if (open === undefined)
733
+ setUncontrolledOpen(next);
734
+ onOpenChange?.(next, reason);
735
+ }, [onOpenChange, open, visible]);
736
+ const close = useCallback((reason, after) => {
737
+ modalDismiss.queue(async () => {
738
+ const handle = findNodeHandle(triggerRef.current);
739
+ if (handle !== null)
740
+ AccessibilityInfo.setAccessibilityFocus(handle);
741
+ onDismiss?.(reason);
742
+ await after?.();
743
+ });
744
+ requestOpen(false, reason);
745
+ }, [modalDismiss, onDismiss, requestOpen]);
746
+ useEffect(() => {
747
+ if (visible && (disabled || readOnly))
748
+ close("programmatic");
749
+ }, [close, disabled, readOnly, visible]);
750
+ const focusFirstItem = () => {
751
+ const first = collectionItems.find((item) => !item.disabled) ?? collectionItems[0];
752
+ if (!first)
753
+ return;
754
+ const target = itemRefs.current.get(first.id);
755
+ if (target) {
756
+ const handle = findNodeHandle(target);
757
+ if (handle !== null)
758
+ AccessibilityInfo.setAccessibilityFocus(handle);
759
+ }
760
+ };
761
+ const activate = (item) => {
762
+ if (item.disabled || disabled || readOnly || busy)
763
+ return;
764
+ if (selection.mode === "single") {
765
+ if (selection.selectedKey === undefined)
766
+ setUncontrolledSingle(item.id);
767
+ selection.onSelectionChange?.(item.id);
768
+ close("selection", onSelectionAfterDismiss
769
+ ? () => onSelectionAfterDismiss(item.id)
770
+ : null);
771
+ return;
772
+ }
773
+ if (selection.mode === "multiple") {
774
+ const next = new Set(selectedMultiple);
775
+ if (next.has(item.id))
776
+ next.delete(item.id);
777
+ else
778
+ next.add(item.id);
779
+ if (selection.selectedKeys === undefined)
780
+ setUncontrolledMultiple(next);
781
+ selection.onSelectionChange?.(next);
782
+ return;
783
+ }
784
+ void onSelect?.(item.id);
785
+ void onAction?.(item.id);
786
+ close("selection", onActionAfterDismiss ? () => onActionAfterDismiss(item.id) : null);
787
+ };
788
+ const renderItem = (item) => {
789
+ const legacyItem = legacyItems.get(item.id);
790
+ const selected = selection.mode === "single"
791
+ ? selectedSingle === item.id
792
+ : selection.mode === "multiple"
793
+ ? selectedMultiple.has(item.id)
794
+ : false;
795
+ const itemDisabled = item.disabled === true || disabled || readOnly || busy;
796
+ const contentColor = resolveColorReference(item.tone === "danger" ? menuRecipe.tones.danger : menuRecipe.tones.neutral, theme.palette);
797
+ const leading = renderLeading?.(item, {
798
+ selected,
799
+ disabled: itemDisabled,
800
+ color: resolveColorReference(menuRecipe.leading.color, theme.palette),
801
+ size: glyph[menuRecipe.leading.glyph],
802
+ }) ?? legacyItem?.icon;
803
+ const trailing = renderTrailing?.(item);
804
+ return (_jsxs(Pressable, { ref: (node) => {
805
+ if (node)
806
+ itemRefs.current.set(item.id, node);
807
+ else
808
+ itemRefs.current.delete(item.id);
809
+ }, accessibilityHint: legacyItem?.accessibilityHint ?? item.description, accessibilityLabel: item.label, accessibilityRole: "menuitem", accessibilityState: {
810
+ disabled: itemDisabled,
811
+ ...(selection.mode === "single" ? { selected } : {}),
812
+ ...(selection.mode === "multiple" ? { checked: selected } : {}),
813
+ }, disabled: itemDisabled, onPress: () => activate(item), style: ({ pressed }) => [
814
+ minimumTargetStyle,
815
+ {
816
+ alignItems: "center",
817
+ backgroundColor: selected
818
+ ? resolveColorReference(densityContract.selectedBackground, theme.palette)
819
+ : pressed
820
+ ? resolveColorReference(densityContract.highlightedBackground, theme.palette)
821
+ : "transparent",
822
+ borderRadius: radius[densityContract.radius],
823
+ direction: environment.direction,
824
+ flexDirection: "row",
825
+ gap: densityContract.gap,
826
+ minHeight: densityContract.minHeight,
827
+ opacity: itemDisabled ? menuRecipe.states.disabledOpacity : 1,
828
+ paddingHorizontal: densityContract.paddingHorizontal,
829
+ },
830
+ ], children: [leading ? (_jsx(View, { accessibilityElementsHidden: true, accessible: false, importantForAccessibility: "no-hide-descendants", children: leading })) : null, _jsxs(View, { style: { flex: 1, gap: spacing.xxs, minWidth: 0 }, children: [_jsx(Text, { style: { color: contentColor }, variant: densityContract.label.textVariant, children: item.label }), item.description ? _jsx(Text, { tone: "muted", variant: densityContract.description.textVariant, children: item.description }) : null] }), item.shortcut ? _jsx(Text, { accessible: false, tone: "muted", variant: menuRecipe.shortcut.textVariant, children: item.shortcut }) : null, trailing ? _jsx(View, { accessibilityElementsHidden: true, accessible: false, children: trailing }) : null, selection.mode !== "none" && selected ? _jsx(Text, { accessible: false, tone: "brand", children: "\u2713" }) : null] }, item.id));
831
+ };
832
+ const collection = source.sections ? source.sections.map((section) => (_jsxs(View, { accessibilityLabel: section.accessibilityLabel ?? section.label, children: [section.label ? (_jsx(Text, { style: {
833
+ color: resolveColorReference(menuRecipe.sectionLabel.color, theme.palette),
834
+ paddingHorizontal: menuRecipe.sectionLabel.paddingHorizontal,
835
+ paddingVertical: menuRecipe.sectionLabel.paddingVertical,
836
+ }, variant: menuRecipe.sectionLabel.textVariant, children: section.label })) : null, section.items.map(renderItem)] }, section.id))) : collectionItems.map(renderItem);
837
+ const triggerProps = {
838
+ accessibilityState: { busy, disabled: disabled || readOnly || busy, expanded: visible },
839
+ onPress: () => {
840
+ if (!disabled && !readOnly && !busy)
841
+ requestOpen(!visible, "trigger");
842
+ },
843
+ };
844
+ return (_jsxs(View, { style: style, children: [_jsx(View, { ref: triggerRef, children: renderTrigger ? renderTrigger(triggerProps) : (_jsx(Pressable, { accessibilityHint: readOnly ? readOnlyLabel : undefined, accessibilityLabel: triggerLabel, accessibilityRole: "button", accessibilityState: triggerProps.accessibilityState, disabled: disabled || readOnly || busy, onPress: triggerProps.onPress, style: ({ pressed }) => [
845
+ minimumTargetStyle,
846
+ {
847
+ alignItems: "center",
848
+ justifyContent: "center",
849
+ opacity: disabled || busy ? menuRecipe.states.disabledOpacity : pressed ? 0.86 : 1,
850
+ },
851
+ ], children: trigger ?? _jsx(Text, { tone: "brand", variant: "label", children: triggerLabel }) })) }), _jsx(Modal, { ...modalProps, animationType: "none", onDismiss: modalDismiss.onDismiss, onRequestClose: () => close("escape"), onShow: () => {
852
+ modalDismiss.onShow();
853
+ focusFirstItem();
854
+ }, transparent: true, visible: visible, children: _jsxs(View, { style: { flex: 1, justifyContent: "center", padding: spacing.md }, children: [_jsx(Pressable, { accessibilityLabel: dismissLabel, accessibilityRole: "button", onPress: () => close("outside"), style: {
855
+ backgroundColor: "#00000088",
856
+ bottom: 0,
857
+ left: 0,
858
+ position: "absolute",
859
+ right: 0,
860
+ top: 0,
861
+ } }), _jsxs(View, { accessibilityLabel: title, accessibilityRole: "menu", accessibilityViewIsModal: true, style: {
862
+ alignSelf: "center",
863
+ backgroundColor: colors.bg,
864
+ borderRadius: radius.lg,
865
+ gap: spacing.sm,
866
+ maxHeight: "75%",
867
+ maxWidth: 520,
868
+ padding: spacing.md,
869
+ width: "100%",
870
+ }, 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 })] })] }) })] }));
871
+ }
872
+ function createNativeLoadMoreControllerFacade() {
873
+ let active = null;
874
+ return {
875
+ request(state, reason) {
876
+ if (!active)
877
+ throw new Error("Cannot use a detached LoadMore controller");
878
+ return active.request(state, reason);
879
+ },
880
+ attach(controller) {
881
+ active = controller;
882
+ return () => {
883
+ if (active === controller)
884
+ active = null;
885
+ };
886
+ },
887
+ };
888
+ }
889
+ /** Collection footer that de-duplicates automatic and manual page requests. */
890
+ export const LoadMore = forwardRef(function LoadMore({ descriptor, onLoadMore, mode = loadMoreRecipe.defaults.mode, density = loadMoreRecipe.defaults.density, onRequestOutcome, onRequestError, style, }, ref) {
891
+ validateLoadMoreDescriptor(descriptor);
892
+ const stateRef = useRef(descriptor.state);
893
+ const handlerRef = useRef(onLoadMore);
894
+ stateRef.current = descriptor.state;
895
+ handlerRef.current = onLoadMore;
896
+ const [controllerFacade] = useState(createNativeLoadMoreControllerFacade);
897
+ const lastViewportRequest = useRef(null);
898
+ useLayoutEffect(() => {
899
+ const controller = createLoadMoreController({
900
+ mode,
901
+ onLoadMore: (request) => handlerRef.current(request),
902
+ });
903
+ const detach = controllerFacade.attach(controller);
904
+ lastViewportRequest.current = null;
905
+ return () => {
906
+ detach();
907
+ controller.dispose();
908
+ };
909
+ }, [controllerFacade, mode]);
910
+ const request = useCallback(async (reason) => {
911
+ const state = stateRef.current;
912
+ if (reason === "viewport" && mode === "automatic" && state.status === "ready") {
913
+ const previous = lastViewportRequest.current;
914
+ if (previous?.mode === mode &&
915
+ previous.requestKey === state.requestKey) {
916
+ const outcome = "already-requesting";
917
+ onRequestOutcome?.(outcome, reason);
918
+ return outcome;
919
+ }
920
+ lastViewportRequest.current = {
921
+ mode,
922
+ requestKey: state.requestKey,
923
+ };
924
+ }
925
+ try {
926
+ const outcome = await controllerFacade.request(state, reason);
927
+ onRequestOutcome?.(outcome, reason);
928
+ return outcome;
929
+ }
930
+ catch (error) {
931
+ onRequestError?.(error, reason);
932
+ // The request was accepted and started; product error state arrives
933
+ // through the controlled descriptor and optional callback. Keep the
934
+ // FlatList imperative bridge rejection-free like the Web observer.
935
+ return "started";
936
+ }
937
+ }, [controllerFacade, mode, onRequestError, onRequestOutcome]);
938
+ useImperativeHandle(ref, () => ({
939
+ onEndReached: () => request("viewport"),
940
+ }), [request]);
941
+ const { state, labels } = descriptor;
942
+ const densityContract = loadMoreRecipe.density[density];
943
+ const theme = useHjmNativeTheme();
944
+ return (_jsx(View, { style: [
945
+ {
946
+ alignItems: "center",
947
+ gap: densityContract.gap,
948
+ paddingVertical: densityContract.paddingVertical,
949
+ },
950
+ style,
951
+ ], children: state.status === "ready" ? (_jsx(Button, { onPress: () => {
952
+ void request("manual").catch(() => undefined);
953
+ }, labelStyle: {
954
+ color: resolveColorReference(loadMoreRecipe.trigger.color, theme.palette),
955
+ fontWeight: loadMoreRecipe.trigger.fontWeight,
956
+ }, size: "small", style: {
957
+ borderRadius: radius[loadMoreRecipe.trigger.radius],
958
+ minHeight: loadMoreRecipe.trigger.minHeight,
959
+ paddingHorizontal: loadMoreRecipe.trigger.paddingHorizontal,
960
+ }, tone: "link", children: labels.loadMore })) : state.status === "loading" ? (_jsxs(View, { accessibilityLabel: labels.loading, accessibilityRole: "progressbar", accessibilityState: { busy: true }, accessible: true, style: {
961
+ alignItems: "center",
962
+ flexDirection: "row",
963
+ gap: densityContract.gap,
964
+ justifyContent: "center",
965
+ }, children: [_jsx(ActivityIndicator, { color: resolveColorReference(spinnerRecipe.tones[loadMoreRecipe.spinner.tone], theme.palette), size: loadMoreRecipe.spinner.size }), _jsx(Text, { accessible: false, style: {
966
+ color: resolveColorReference(loadMoreRecipe.status.color, theme.palette),
967
+ }, 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: () => {
968
+ void request("retry").catch(() => undefined);
969
+ }, labelStyle: {
970
+ color: resolveColorReference(loadMoreRecipe.trigger.color, theme.palette),
971
+ fontWeight: loadMoreRecipe.trigger.fontWeight,
972
+ }, size: "small", style: {
973
+ borderRadius: radius[loadMoreRecipe.trigger.radius],
974
+ minHeight: loadMoreRecipe.trigger.minHeight,
975
+ paddingHorizontal: loadMoreRecipe.trigger.paddingHorizontal,
976
+ }, tone: "link", children: labels.retry })] })) : (_jsx(Text, { accessibilityLiveRegion: "polite", style: { color: resolveColorReference(loadMoreRecipe.end.color, theme.palette) }, variant: loadMoreRecipe.end.textVariant, children: labels.complete })) }));
977
+ });
978
+ //# sourceMappingURL=navigation.js.map