@mrmeg/expo-ui 0.25.0 → 0.26.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 (35) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/LLM_USAGE.md +13 -1
  3. package/README.md +89 -18
  4. package/dist/components/Avatar.d.ts +1 -1
  5. package/dist/components/BottomSheet.d.ts +1 -1
  6. package/dist/components/BottomSheet.js +28 -51
  7. package/dist/components/DismissKeyboard.d.ts +8 -6
  8. package/dist/components/DismissKeyboard.js +9 -7
  9. package/dist/components/DropdownMenu.js +1 -1
  10. package/dist/components/Icon.d.ts +24 -22
  11. package/dist/components/Icon.js +26 -3
  12. package/dist/components/Notification.js +3 -3
  13. package/dist/components/Popover.js +1 -1
  14. package/dist/components/Select.js +1 -1
  15. package/dist/components/StatCard.d.ts +1 -1
  16. package/dist/components/TextInput.d.ts +1 -1
  17. package/dist/components/TextInput.js +55 -35
  18. package/dist/components/Tooltip.js +1 -1
  19. package/dist/components/iconRegistry.generated.d.ts +148 -0
  20. package/dist/components/iconRegistry.generated.js +296 -0
  21. package/dist/components/keyboardDismiss.js +126 -55
  22. package/dist/components/keyboardFocusRegistry.d.ts +14 -4
  23. package/dist/components/keyboardFocusRegistry.js +13 -5
  24. package/dist/components/nativeTextField.android.d.ts +3 -0
  25. package/dist/components/nativeTextField.android.js +226 -0
  26. package/dist/components/nativeTextField.d.ts +14 -0
  27. package/dist/components/nativeTextField.js +14 -0
  28. package/dist/constants/spacing.d.ts +2 -2
  29. package/dist/constants/spacing.js +2 -2
  30. package/dist/design-system.json +152 -3
  31. package/dist/hooks/useResources.d.ts +5 -18
  32. package/dist/hooks/useResources.js +5 -43
  33. package/dist/state/themeStore.d.ts +1 -1
  34. package/llms-full.md +3 -3
  35. package/package.json +4 -3
@@ -1,7 +1,14 @@
1
1
  import * as React from "react";
2
- import type { StyleProp, TextProps, TextStyle } from "react-native";
3
- import Feather from "@expo/vector-icons/Feather";
2
+ import type { StyleProp, TextStyle, ViewProps, ViewStyle } from "react-native";
4
3
  import type { ThemeColors } from "../constants/colors";
4
+ import { type IconName } from "./iconRegistry.generated";
5
+ /**
6
+ * Kebab-case Lucide icon names the package bundles, served from
7
+ * `icon-names.json` through the generated registry. Add a name there and run
8
+ * `bun run ui:icons` to extend the union; pass any other Lucide component
9
+ * through `component` without touching the registry.
10
+ */
11
+ export type { IconName };
5
12
  /**
6
13
  * Theme color names that can be used as shortcuts.
7
14
  *
@@ -9,7 +16,6 @@ import type { ThemeColors } from "../constants/colors";
9
16
  * (`foreground`, `accent`, `border`, …) and can never drift from the theme.
10
17
  */
11
18
  export type ThemeColorName = keyof ThemeColors;
12
- export type IconName = React.ComponentProps<typeof Feather>["name"];
13
19
  type IconBaseProps = {
14
20
  /** Size of the icon in pixels */
15
21
  size?: number;
@@ -20,33 +26,29 @@ type IconBaseProps = {
20
26
  /** When true, hides the icon from the accessibility tree. @default false */
21
27
  decorative?: boolean;
22
28
  };
23
- type IconAccessibilityProps = Pick<TextProps, "accessible" | "importantForAccessibility" | "accessibilityElementsHidden" | "aria-hidden">;
29
+ type IconAccessibilityProps = Pick<ViewProps, "accessible" | "importantForAccessibility" | "accessibilityElementsHidden" | "aria-hidden">;
30
+ /**
31
+ * Props handed to a `component`. Wide enough that any `lucide-react-native`
32
+ * component type-checks as-is (`LucideProps` extends `SvgProps`, whose `style`
33
+ * is a view style), and any component that accepts `size` and `color` works.
34
+ */
24
35
  type CustomIconComponentProps = {
25
36
  size: number;
26
37
  color: string;
27
- style?: StyleProp<TextStyle>;
38
+ style?: StyleProp<ViewStyle>;
28
39
  } & Partial<IconAccessibilityProps>;
29
- type FeatherIconProps = IconBaseProps & {
30
- /** The icon name to render (Feather icons) */
40
+ type RegistryIconProps = IconBaseProps & {
41
+ /** The icon name to render (a Lucide name from the package registry) */
31
42
  name: IconName;
32
43
  component?: never;
33
44
  };
34
45
  type CustomIconProps = IconBaseProps & {
35
46
  name?: never;
36
- /** Custom component to render instead of Feather. Receives size and color as props. */
47
+ /**
48
+ * Custom component to render instead of a registry icon. Receives size and
49
+ * color as props — any `lucide-react-native` icon import works here.
50
+ */
37
51
  component: React.ComponentType<CustomIconComponentProps>;
38
52
  };
39
- export type IconProps = FeatherIconProps | CustomIconProps;
40
- /**
41
- * Universal Icon Component
42
- * Renders @expo/vector-icons Feather with theme integration and style support
43
- *
44
- * Usage:
45
- * ```tsx
46
- * <Icon name="check" color="primary" size={16} />
47
- * <Icon name="calendar" color="#FF0000" size={24} />
48
- * <Icon name="terminal" style={{ marginRight: 8 }} />
49
- * ```
50
- */
51
- export declare function Icon(props: IconProps): React.JSX.Element;
52
- export {};
53
+ export type IconProps = RegistryIconProps | CustomIconProps;
54
+ export declare function Icon(props: IconProps): React.JSX.Element | null;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useTheme } from "../hooks/useTheme.js";
3
- import Feather from "@expo/vector-icons/Feather";
3
+ import { ICONS } from "./iconRegistry.generated";
4
4
  /**
5
5
  * Resolve an icon color against the active theme.
6
6
  *
@@ -20,15 +20,26 @@ function resolveIconColor(color, themeColors) {
20
20
  }
21
21
  /**
22
22
  * Universal Icon Component
23
- * Renders @expo/vector-icons Feather with theme integration and style support
23
+ * Renders `lucide-react-native` SVG icons with theme integration and style
24
+ * support. Icons are SVG on every platform: no font to load, and server markup
25
+ * carries the glyph.
24
26
  *
25
27
  * Usage:
26
28
  * ```tsx
27
29
  * <Icon name="check" color="primary" size={16} />
28
30
  * <Icon name="calendar" color="#FF0000" size={24} />
29
31
  * <Icon name="terminal" style={{ marginRight: 8 }} />
32
+ * <Icon component={Rocket} color="accent" /> // any Lucide import
30
33
  * ```
31
34
  */
35
+ const warnedUnknownIconNames = new Set();
36
+ function warnUnknownIconName(name) {
37
+ if (!__DEV__ || warnedUnknownIconNames.has(name))
38
+ return;
39
+ warnedUnknownIconNames.add(name);
40
+ console.warn(`Icon: "${name}" is not in the package icon registry, so nothing was rendered. ` +
41
+ "Add it to icon-names.json and run `bun run ui:icons`, or pass a Lucide component through `component`.");
42
+ }
32
43
  export function Icon(props) {
33
44
  const { size = 24, color, style, decorative = false } = props;
34
45
  const { theme } = useTheme();
@@ -42,9 +53,21 @@ export function Icon(props) {
42
53
  "aria-hidden": true,
43
54
  }
44
55
  : { accessible: true };
56
+ // `style` stays a text style on the public prop for source compatibility
57
+ // with the font-icon era; SVG roots take a view style, and every layout
58
+ // property callers actually pass (margins, transforms) belongs to both.
45
59
  const iconStyle = [style, { pointerEvents: "none" }];
46
60
  if (CustomComponent) {
47
61
  return (_jsx(CustomComponent, { size: size, color: iconColor, style: iconStyle, ...accessibilityProps }));
48
62
  }
49
- return (_jsx(Feather, { name: props.name, size: size, color: iconColor, style: iconStyle, ...accessibilityProps }));
63
+ const name = props.name;
64
+ const Lucide = ICONS[name];
65
+ // `IconName` makes an unknown name a type error, but names also arrive at
66
+ // runtime (a database field, a config file). Rendering `undefined` would
67
+ // throw; draw nothing and say so once per name in development instead.
68
+ if (!Lucide) {
69
+ warnUnknownIconName(name);
70
+ return null;
71
+ }
72
+ return (_jsx(Lucide, { size: size, color: iconColor, style: iconStyle, ...accessibilityProps }));
50
73
  }
@@ -156,19 +156,19 @@ export const Notification = () => {
156
156
  switch (alert?.type) {
157
157
  case "error":
158
158
  return {
159
- icon: "alert-circle",
159
+ icon: "circle-alert",
160
160
  color: theme.colors.destructive,
161
161
  bgColor: withAlpha(theme.colors.destructive, 0.08),
162
162
  };
163
163
  case "success":
164
164
  return {
165
- icon: "check-circle",
165
+ icon: "circle-check-big",
166
166
  color: theme.colors.success,
167
167
  bgColor: withAlpha(theme.colors.success, 0.08),
168
168
  };
169
169
  case "warning":
170
170
  return {
171
- icon: "alert-triangle",
171
+ icon: "triangle-alert",
172
172
  color: theme.colors.warning,
173
173
  bgColor: withAlpha(theme.colors.warning, 0.08),
174
174
  };
@@ -41,7 +41,7 @@ function PopoverContent({ side, align = "center", sideOffset = 4, portalHost, ..
41
41
  ...getShadowStyle("soft"),
42
42
  },
43
43
  ]) ?? undefined;
44
- return (_jsx(PopoverPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(PopoverPrimitive.Overlay, { style: Platform.select({ native: StyleSheet.absoluteFill }), children: _jsx(AnimatedView, { type: "fade", enterDuration: 200, children: _jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(PopoverPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, style: contentStyle, ...props }) }) }) }) }) }) }));
44
+ return (_jsx(PopoverPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(PopoverPrimitive.Overlay, { style: Platform.select({ native: StyleSheet.absoluteFill }), children: _jsx(AnimatedView, { type: "fade", enterDuration: 200, style: StyleSheet.absoluteFill, pointerEvents: "box-none", children: _jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(PopoverPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, style: contentStyle, ...props }) }) }) }) }) }) }));
45
45
  }
46
46
  function PopoverHeader({ children, style, ...props }) {
47
47
  const { theme } = useTheme();
@@ -100,7 +100,7 @@ function SelectContent({ side, align = "start", sideOffset = 4, portalHost, styl
100
100
  return (_jsx(SelectPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(SelectPrimitive.Overlay, { style: Platform.select({
101
101
  native: StyleSheet.absoluteFill,
102
102
  default: undefined,
103
- }), children: _jsx(AnimatedView, { type: "fade", children: _jsx(TextColorContext.Provider, { value: theme.colors.popoverForeground, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(SelectPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, insets: insets, avoidCollisions: true, ...props, style: {
103
+ }), children: _jsx(AnimatedView, { type: "fade", style: StyleSheet.absoluteFill, pointerEvents: "box-none", children: _jsx(TextColorContext.Provider, { value: theme.colors.popoverForeground, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(SelectPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, insets: insets, avoidCollisions: true, ...props, style: {
104
104
  backgroundColor: theme.colors.popover,
105
105
  borderWidth: 1,
106
106
  borderColor: theme.colors.border,
@@ -17,7 +17,7 @@ export interface StatCardProps {
17
17
  unit?: string;
18
18
  /** Optional signed change line below the value. */
19
19
  change?: StatCardChange;
20
- /** Optional leading icon name (Feather), rendered beside the label. */
20
+ /** Optional leading icon name (Lucide), rendered beside the label. */
21
21
  icon?: IconName;
22
22
  /** Makes the card pressable with scale feedback (delegates to Card). */
23
23
  onPress?: () => void;
@@ -10,7 +10,7 @@ export type TextInputSize = "sm" | "md" | "lg";
10
10
  export type TextInputVariant = "outline" | "filled" | "underlined";
11
11
  interface TextInputCustomProps extends TextInputProps {
12
12
  /**
13
- * Forwarded ref to the underlying RNTextInput element.
13
+ * RNTextInput ref on web; an RN-compatible imperative handle on native.
14
14
  *
15
15
  * Written as `ComponentRef<typeof RNTextInput>` rather than RN 0.88's
16
16
  * `TextInputInstance` alias so the emitted declarations stay valid on
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useCallback, useImperativeHandle, useMemo, useRef, useState, } from "react";
3
3
  import { StyleSheet, TextInput as RNTextInput, Platform, View, Pressable, } from "react-native";
4
- import { Host, TextInput as ExpoTextInput, useNativeState, } from "@expo/ui";
4
+ import { Host, useNativeState, } from "@expo/ui";
5
+ import { NativeTextField } from "./nativeTextField";
5
6
  import { useTheme } from "../hooks/useTheme.js";
6
7
  import { spacing } from "../constants/spacing.js";
7
8
  import { useFontStyle } from "../hooks/useFontStyle.js";
@@ -179,23 +180,33 @@ function WebTextInput({ variant = "outline", size = "md", label, helperText, err
179
180
  }, accessibilityLabel: "Clear input", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: spacing.iconSm, color: "textDim", decorative: true }) })), showClearButton && rightElement && !hasSecureToggle && (_jsxs(View, { style: styles.rightElements, children: [_jsx(Pressable, { onPress: () => {
180
181
  hapticLight();
181
182
  onChangeText?.("");
182
- }, accessibilityLabel: "Clear input", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: spacing.iconSm, color: "textDim", decorative: true }) }), rightElement] })), !showClearButton && rightElement && !hasSecureToggle && (_jsx(View, { style: styles.rightElement, children: rightElement })), secureTextEntry && showSecureEntryToggle && (_jsx(Pressable, { style: styles.passwordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) })), showErrorIcon && (_jsx(View, { style: [styles.errorIcon, { pointerEvents: "none" }], accessibilityLabel: "Error", children: _jsx(Icon, { name: "alert-circle", size: spacing.iconSm, color: "destructive", decorative: true }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [
183
+ }, accessibilityLabel: "Clear input", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: spacing.iconSm, color: "textDim", decorative: true }) }), rightElement] })), !showClearButton && rightElement && !hasSecureToggle && (_jsx(View, { style: styles.rightElement, children: rightElement })), secureTextEntry && showSecureEntryToggle && (_jsx(Pressable, { style: styles.passwordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) })), showErrorIcon && (_jsx(View, { style: [styles.errorIcon, { pointerEvents: "none" }], accessibilityLabel: "Error", children: _jsx(Icon, { name: "circle-alert", size: spacing.iconSm, color: "destructive", decorative: true }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [
183
184
  styles.helperText,
184
185
  hasError && styles.errorText,
185
186
  ], children: errorText || helperText }))] }));
186
187
  }
187
188
  /**
188
- * Native (iOS / Android) implementation backed by `@expo/ui`'s TextInput, which
189
- * bridges to SwiftUI's `TextField`/`SecureField` and Jetpack Compose's
190
- * `TextField`. The text buffer lives natively (via `useNativeState`), so typing
191
- * never round-trips through React state — eliminating the cursor flicker seen on
192
- * controlled RN inputs.
189
+ * Native (iOS / Android) implementation backed by a native text field rendered
190
+ * inside an `@expo/ui` `Host`. The text buffer lives natively (via
191
+ * `useNativeState`), so typing never round-trips through React state —
192
+ * eliminating the cursor flicker seen on controlled RN inputs.
193
193
  *
194
- * By design (reliability over feature-parity) this path renders the field plus
195
- * sibling label / helper / error text only. The in-field overlays from the web
196
- * implementation — password visibility toggle, clear button, left/right
197
- * elements, and error icon — are intentionally omitted on native to avoid
198
- * layering RN views over the native host view.
194
+ * The field comes from `./nativeTextField`, a platform-split module. iOS gets
195
+ * `@expo/ui`'s universal TextInput (SwiftUI `TextField`/`SecureField`, which
196
+ * already reports a password field to the keyboard). Android gets a
197
+ * package-owned port of that universal field built on
198
+ * `@expo/ui/jetpack-compose`'s `BasicTextField` (`nativeTextField.android.tsx`),
199
+ * because the universal Android field maps `secureTextEntry` to a masking
200
+ * transformation only and offers no way to set Compose's `KeyboardType.Password`
201
+ * — the IME saw a plain text field, kept its suggestion strip and learned the
202
+ * password. The port declares a password keyboard for secure fields and keeps it
203
+ * across the eye toggle, so revealing the text never restarts the IME. Metro
204
+ * picks the platform file; TypeScript types both through the default file.
205
+ *
206
+ * This path supports label / helper / error text and a password visibility
207
+ * toggle laid out beside the native Host. Clear buttons, left/right elements,
208
+ * and the error icon remain web-only; RN overlays are not layered over the
209
+ * native host view.
199
210
  */
200
211
  function NativeTextInput({ variant = "outline", size = "md", label, helperText, errorText, error, required, rows, forceLight, inputMode, onChangeText, onFocus, onBlur, value, defaultValue, editable = true, multiline, secureTextEntry, showSecureEntryToggle, ref, wrapperStyle,
201
212
  // Web-only affordances. Destructured out of `rest` so they're NOT forwarded to
@@ -231,9 +242,9 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
231
242
  // parity) once it settles.
232
243
  const queuedTogglesRef = useRef(0);
233
244
  const toggleRef = useRef(() => { });
234
- // Per-flavour mount generation. Bumped whenever a flavour becomes the incoming
235
- // view so React always mounts a FRESH native view (whose `autoFocus` will run)
236
- // instead of reusing one that is still unmounting from the previous handoff.
245
+ // iOS-only per-flavour mount generation. Bumped whenever a flavour becomes
246
+ // the incoming view so React mounts a FRESH native view (whose `autoFocus`
247
+ // will run) instead of reusing one still unmounting from the previous handoff.
237
248
  const generationRef = useRef({ secure: 0, plain: 0 });
238
249
  // Last text the JS side knows about; see handleChangeText.
239
250
  const lastTextRef = useRef(value ?? defaultValue ?? "");
@@ -256,12 +267,18 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
256
267
  // RNTextInput because that's what the public TextInputCustomProps declares.
257
268
  // We expose the subset consumers use, plus a `setNativeProps` shim so the
258
269
  // uncontrolled AuthTextField can push corrected text into the native buffer.
259
- // One ref per native view flavour; only the active one (or, mid-handoff on
260
- // iOS, both) is mounted.
270
+ // Android keeps one field/ref as visualTransformation changes in place.
271
+ // iOS has one ref per flavour; both are mounted only during a handoff.
272
+ const androidRef = useRef(null);
261
273
  const secureRef = useRef(null);
262
274
  const plainRef = useRef(null);
263
- const activeInput = useCallback(() => (activeSecureRef.current ? secureRef : plainRef).current, []);
275
+ const activeInput = useCallback(() => {
276
+ if (Platform.OS === "android")
277
+ return androidRef.current;
278
+ return (activeSecureRef.current ? secureRef : plainRef).current;
279
+ }, []);
264
280
  const blurAll = useCallback(() => {
281
+ androidRef.current?.blur();
265
282
  secureRef.current?.blur();
266
283
  plainRef.current?.blur();
267
284
  }, []);
@@ -282,19 +299,16 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
282
299
  }, []);
283
300
  const parentOnFocus = onFocus;
284
301
  const parentOnBlur = onBlur;
285
- // Focus/blur from a view that is not the active flavour is ignored: that is
286
- // the outgoing view resigning during a handoff, or a stale event from a view
287
- // React is unmounting.
302
+ // On iOS, ignore focus/blur from the outgoing flavour during a handoff or
303
+ // unmount. Android events always belong to the same mounted field.
288
304
  const handleFocus = useCallback((secure) => {
289
- if (secure !== activeSecureRef.current)
305
+ if (Platform.OS === "ios" && secure !== activeSecureRef.current)
290
306
  return;
291
307
  isFocusedRef.current = true;
292
- // Register a blur handle so tap-away dismissal (`keyboardDismiss.ts`) and
293
- // the BottomSheet overlay can resign this field: RN's `TextInputState`
294
- // never sees SwiftUI / Compose fields, so `Keyboard.dismiss()` cannot.
295
- // Blurring through the field's own ref is window-independent — inside a
296
- // native bottom sheet `KeyboardController.dismiss()` can miss, the ref
297
- // never does.
308
+ // Register the field's own blur handle for tap-away dismissal
309
+ // (`DismissKeyboard`, `BottomSheet.Content`). RN supports Expo-hosted field
310
+ // interop; this direct ref also targets the actual field inside a native
311
+ // sheet's window.
298
312
  setKeyboardFocusedInput(focusRegistryToken, blurAll);
299
313
  if (handoffRef.current) {
300
314
  // The incoming view took first responder; the parent never saw focus leave.
@@ -305,7 +319,7 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
305
319
  parentOnFocus?.();
306
320
  }, [blurAll, finishHandoff, focusRegistryToken, parentOnFocus]);
307
321
  const handleBlur = useCallback((secure) => {
308
- if (secure !== activeSecureRef.current)
322
+ if (Platform.OS === "ios" && secure !== activeSecureRef.current)
309
323
  return;
310
324
  isFocusedRef.current = false;
311
325
  restoreAfterSecureHandoffRef.current = false;
@@ -321,10 +335,12 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
321
335
  }
322
336
  restoreAfterSecureHandoffRef.current = false;
323
337
  const outgoingSecure = activeSecureRef.current;
324
- if (outgoingSecure)
325
- generationRef.current.plain += 1;
326
- else
327
- generationRef.current.secure += 1;
338
+ if (Platform.OS === "ios") {
339
+ if (outgoingSecure)
340
+ generationRef.current.plain += 1;
341
+ else
342
+ generationRef.current.secure += 1;
343
+ }
328
344
  if (Platform.OS === "ios" && isFocusedRef.current) {
329
345
  const timer = setTimeout(() => {
330
346
  // The incoming view never took first responder. Rather than unmount
@@ -460,7 +476,7 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
460
476
  fontSize: sizeConfig.fontSize,
461
477
  ...(nativeFontFamily ? { fontFamily: nativeFontFamily } : null),
462
478
  };
463
- return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, size: "base", fontWeight: "medium", style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) })), _jsxs(View, { style: [surfaceStyle, hasSecureToggle && styles.nativeRow], hitSlop: { top: surfaceHitSlop, bottom: surfaceHitSlop }, ...surfaceResponderProps, children: [[true, false].map((secure) => {
479
+ return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, size: "base", fontWeight: "medium", style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) })), _jsxs(View, { style: [surfaceStyle, hasSecureToggle && styles.nativeRow], hitSlop: { top: surfaceHitSlop, bottom: surfaceHitSlop }, ...surfaceResponderProps, children: [(Platform.OS === "android" ? [effectiveSecureTextEntry] : [true, false]).map((secure) => {
464
480
  const active = secure === effectiveSecureTextEntry;
465
481
  const outgoing = handoff != null && secure === handoff.outgoingSecure;
466
482
  if (!active && !outgoing)
@@ -472,7 +488,11 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
472
488
  ? hasSecureToggle
473
489
  ? styles.nativeHostFlex
474
490
  : styles.nativeHost
475
- : styles.nativeHostHandoff, pointerEvents: inLayout ? "auto" : "none", accessibilityElementsHidden: !inLayout, importantForAccessibility: inLayout ? "auto" : "no-hide-descendants", children: _jsx(ExpoTextInput, { ...rest, autoFocus: rest.autoFocus || (handoff != null && active), ref: secure ? secureRef : plainRef, value: state, defaultValue: defaultValue, onChangeText: handleChangeText, onFocus: () => handleFocus(secure), onBlur: () => handleBlur(secure), editable: editable, multiline: multiline, rows: rows, inputMode: inputMode, secureTextEntry: secure, placeholderTextColor: theme.colors.textDim, style: boxStyle, textStyle: textStyle }) }, secure ? `secure-${generationRef.current.secure}` : `plain-${generationRef.current.plain}`));
491
+ : styles.nativeHostHandoff, pointerEvents: inLayout ? "auto" : "none", accessibilityElementsHidden: !inLayout, importantForAccessibility: inLayout ? "auto" : "no-hide-descendants", children: _jsx(NativeTextField, { ...rest, autoFocus: rest.autoFocus || (handoff != null && active), ref: Platform.OS === "android" ? androidRef : secure ? secureRef : plainRef, value: state, defaultValue: defaultValue, onChangeText: handleChangeText, onFocus: () => handleFocus(secure), onBlur: () => handleBlur(secure), editable: editable, multiline: multiline, rows: rows, inputMode: inputMode, secureTextEntry: secure, placeholderTextColor: theme.colors.textDim, style: boxStyle, textStyle: textStyle }) }, Platform.OS === "android"
492
+ ? "android"
493
+ : secure
494
+ ? `secure-${generationRef.current.secure}`
495
+ : `plain-${generationRef.current.plain}`));
476
496
  }), hasSecureToggle && (_jsx(Pressable, { style: styles.nativePasswordToggle, onPress: togglePasswordVisible, accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, size: "sm", style: [styles.helperText, hasError && styles.errorText], children: errorText || helperText }))] }));
477
497
  }
478
498
  const createStyles = (theme, variant, size) => StyleSheet.create({
@@ -71,7 +71,7 @@ function TooltipContent({ side = "top", align = "center", sideOffset = 4, portal
71
71
  ...getShadowStyle("soft"),
72
72
  },
73
73
  ]) ?? undefined;
74
- return (_jsx(TooltipPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(TooltipPrimitive.Overlay, { style: Platform.select({ native: StyleSheet.absoluteFill }), children: _jsx(AnimatedView, { type: "fade", enterDuration: 150, children: _jsx(TextColorContext.Provider, { value: colors.text, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(TooltipPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, style: contentStyle, ...props }) }) }) }) }) }) }) }));
74
+ return (_jsx(TooltipPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(TooltipPrimitive.Overlay, { style: Platform.select({ native: StyleSheet.absoluteFill }), children: _jsx(AnimatedView, { type: "fade", enterDuration: 150, style: StyleSheet.absoluteFill, pointerEvents: "box-none", children: _jsx(TextColorContext.Provider, { value: colors.text, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(TooltipPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, style: contentStyle, ...props }) }) }) }) }) }) }) }));
75
75
  }
76
76
  function TooltipBody({ children, style, ...props }) {
77
77
  return (_jsx(View, { style: [
@@ -0,0 +1,148 @@
1
+ export declare const ICONS: {
2
+ readonly archive: import("lucide-react-native").LucideIcon;
3
+ readonly "arrow-down": import("lucide-react-native").LucideIcon;
4
+ readonly "arrow-left": import("lucide-react-native").LucideIcon;
5
+ readonly "arrow-right": import("lucide-react-native").LucideIcon;
6
+ readonly "arrow-up": import("lucide-react-native").LucideIcon;
7
+ readonly award: import("lucide-react-native").LucideIcon;
8
+ readonly beer: import("lucide-react-native").LucideIcon;
9
+ readonly bell: import("lucide-react-native").LucideIcon;
10
+ readonly "bell-off": import("lucide-react-native").LucideIcon;
11
+ readonly bold: import("lucide-react-native").LucideIcon;
12
+ readonly bookmark: import("lucide-react-native").LucideIcon;
13
+ readonly box: import("lucide-react-native").LucideIcon;
14
+ readonly briefcase: import("lucide-react-native").LucideIcon;
15
+ readonly calendar: import("lucide-react-native").LucideIcon;
16
+ readonly "calendar-check": import("lucide-react-native").LucideIcon;
17
+ readonly camera: import("lucide-react-native").LucideIcon;
18
+ readonly "camera-off": import("lucide-react-native").LucideIcon;
19
+ readonly "chart-no-axes-column": import("lucide-react-native").LucideIcon;
20
+ readonly check: import("lucide-react-native").LucideIcon;
21
+ readonly "chevron-down": import("lucide-react-native").LucideIcon;
22
+ readonly "chevron-left": import("lucide-react-native").LucideIcon;
23
+ readonly "chevron-right": import("lucide-react-native").LucideIcon;
24
+ readonly "chevron-up": import("lucide-react-native").LucideIcon;
25
+ readonly "circle-alert": import("lucide-react-native").LucideIcon;
26
+ readonly "circle-check-big": import("lucide-react-native").LucideIcon;
27
+ readonly "circle-plus": import("lucide-react-native").LucideIcon;
28
+ readonly "circle-question-mark": import("lucide-react-native").LucideIcon;
29
+ readonly "circle-stop": import("lucide-react-native").LucideIcon;
30
+ readonly "circle-x": import("lucide-react-native").LucideIcon;
31
+ readonly clock: import("lucide-react-native").LucideIcon;
32
+ readonly cloud: import("lucide-react-native").LucideIcon;
33
+ readonly "cloud-off": import("lucide-react-native").LucideIcon;
34
+ readonly code: import("lucide-react-native").LucideIcon;
35
+ readonly coffee: import("lucide-react-native").LucideIcon;
36
+ readonly compass: import("lucide-react-native").LucideIcon;
37
+ readonly copy: import("lucide-react-native").LucideIcon;
38
+ readonly "credit-card": import("lucide-react-native").LucideIcon;
39
+ readonly crosshair: import("lucide-react-native").LucideIcon;
40
+ readonly database: import("lucide-react-native").LucideIcon;
41
+ readonly "dollar-sign": import("lucide-react-native").LucideIcon;
42
+ readonly download: import("lucide-react-native").LucideIcon;
43
+ readonly droplet: import("lucide-react-native").LucideIcon;
44
+ readonly "ellipsis-vertical": import("lucide-react-native").LucideIcon;
45
+ readonly "external-link": import("lucide-react-native").LucideIcon;
46
+ readonly eye: import("lucide-react-native").LucideIcon;
47
+ readonly "eye-off": import("lucide-react-native").LucideIcon;
48
+ readonly "face-slightly-smiling": import("lucide-react-native").LucideIcon;
49
+ readonly file: import("lucide-react-native").LucideIcon;
50
+ readonly "file-text": import("lucide-react-native").LucideIcon;
51
+ readonly folder: import("lucide-react-native").LucideIcon;
52
+ readonly gift: import("lucide-react-native").LucideIcon;
53
+ readonly "git-branch": import("lucide-react-native").LucideIcon;
54
+ readonly "git-pull-request": import("lucide-react-native").LucideIcon;
55
+ readonly globe: import("lucide-react-native").LucideIcon;
56
+ readonly "grid-3x3": import("lucide-react-native").LucideIcon;
57
+ readonly hash: import("lucide-react-native").LucideIcon;
58
+ readonly heart: import("lucide-react-native").LucideIcon;
59
+ readonly hexagon: import("lucide-react-native").LucideIcon;
60
+ readonly house: import("lucide-react-native").LucideIcon;
61
+ readonly image: import("lucide-react-native").LucideIcon;
62
+ readonly inbox: import("lucide-react-native").LucideIcon;
63
+ readonly info: import("lucide-react-native").LucideIcon;
64
+ readonly italic: import("lucide-react-native").LucideIcon;
65
+ readonly key: import("lucide-react-native").LucideIcon;
66
+ readonly layers: import("lucide-react-native").LucideIcon;
67
+ readonly "layout-grid": import("lucide-react-native").LucideIcon;
68
+ readonly link: import("lucide-react-native").LucideIcon;
69
+ readonly "link-2": import("lucide-react-native").LucideIcon;
70
+ readonly list: import("lucide-react-native").LucideIcon;
71
+ readonly loader: import("lucide-react-native").LucideIcon;
72
+ readonly lock: import("lucide-react-native").LucideIcon;
73
+ readonly "lock-open": import("lucide-react-native").LucideIcon;
74
+ readonly "log-in": import("lucide-react-native").LucideIcon;
75
+ readonly "log-out": import("lucide-react-native").LucideIcon;
76
+ readonly mail: import("lucide-react-native").LucideIcon;
77
+ readonly map: import("lucide-react-native").LucideIcon;
78
+ readonly "map-pin": import("lucide-react-native").LucideIcon;
79
+ readonly maximize: import("lucide-react-native").LucideIcon;
80
+ readonly "maximize-2": import("lucide-react-native").LucideIcon;
81
+ readonly menu: import("lucide-react-native").LucideIcon;
82
+ readonly "message-circle": import("lucide-react-native").LucideIcon;
83
+ readonly "message-square": import("lucide-react-native").LucideIcon;
84
+ readonly mic: import("lucide-react-native").LucideIcon;
85
+ readonly "mic-off": import("lucide-react-native").LucideIcon;
86
+ readonly minus: import("lucide-react-native").LucideIcon;
87
+ readonly monitor: import("lucide-react-native").LucideIcon;
88
+ readonly moon: import("lucide-react-native").LucideIcon;
89
+ readonly music: import("lucide-react-native").LucideIcon;
90
+ readonly navigation: import("lucide-react-native").LucideIcon;
91
+ readonly package: import("lucide-react-native").LucideIcon;
92
+ readonly "panel-left": import("lucide-react-native").LucideIcon;
93
+ readonly "panels-top-left": import("lucide-react-native").LucideIcon;
94
+ readonly "party-popper": import("lucide-react-native").LucideIcon;
95
+ readonly pen: import("lucide-react-native").LucideIcon;
96
+ readonly "pen-line": import("lucide-react-native").LucideIcon;
97
+ readonly pencil: import("lucide-react-native").LucideIcon;
98
+ readonly percent: import("lucide-react-native").LucideIcon;
99
+ readonly phone: import("lucide-react-native").LucideIcon;
100
+ readonly play: import("lucide-react-native").LucideIcon;
101
+ readonly plus: import("lucide-react-native").LucideIcon;
102
+ readonly printer: import("lucide-react-native").LucideIcon;
103
+ readonly radio: import("lucide-react-native").LucideIcon;
104
+ readonly "refresh-cw": import("lucide-react-native").LucideIcon;
105
+ readonly repeat: import("lucide-react-native").LucideIcon;
106
+ readonly "rotate-cw": import("lucide-react-native").LucideIcon;
107
+ readonly search: import("lucide-react-native").LucideIcon;
108
+ readonly send: import("lucide-react-native").LucideIcon;
109
+ readonly server: import("lucide-react-native").LucideIcon;
110
+ readonly settings: import("lucide-react-native").LucideIcon;
111
+ readonly share: import("lucide-react-native").LucideIcon;
112
+ readonly "share-2": import("lucide-react-native").LucideIcon;
113
+ readonly shield: import("lucide-react-native").LucideIcon;
114
+ readonly "shield-off": import("lucide-react-native").LucideIcon;
115
+ readonly "shopping-bag": import("lucide-react-native").LucideIcon;
116
+ readonly "shopping-cart": import("lucide-react-native").LucideIcon;
117
+ readonly shuffle: import("lucide-react-native").LucideIcon;
118
+ readonly slash: import("lucide-react-native").LucideIcon;
119
+ readonly "sliders-horizontal": import("lucide-react-native").LucideIcon;
120
+ readonly "sliders-vertical": import("lucide-react-native").LucideIcon;
121
+ readonly smartphone: import("lucide-react-native").LucideIcon;
122
+ readonly square: import("lucide-react-native").LucideIcon;
123
+ readonly "square-check-big": import("lucide-react-native").LucideIcon;
124
+ readonly "square-pen": import("lucide-react-native").LucideIcon;
125
+ readonly star: import("lucide-react-native").LucideIcon;
126
+ readonly sun: import("lucide-react-native").LucideIcon;
127
+ readonly tag: import("lucide-react-native").LucideIcon;
128
+ readonly terminal: import("lucide-react-native").LucideIcon;
129
+ readonly "thumbs-up": import("lucide-react-native").LucideIcon;
130
+ readonly trash: import("lucide-react-native").LucideIcon;
131
+ readonly "trending-down": import("lucide-react-native").LucideIcon;
132
+ readonly "trending-up": import("lucide-react-native").LucideIcon;
133
+ readonly "triangle-alert": import("lucide-react-native").LucideIcon;
134
+ readonly truck: import("lucide-react-native").LucideIcon;
135
+ readonly underline: import("lucide-react-native").LucideIcon;
136
+ readonly upload: import("lucide-react-native").LucideIcon;
137
+ readonly user: import("lucide-react-native").LucideIcon;
138
+ readonly "user-check": import("lucide-react-native").LucideIcon;
139
+ readonly "user-minus": import("lucide-react-native").LucideIcon;
140
+ readonly "user-plus": import("lucide-react-native").LucideIcon;
141
+ readonly users: import("lucide-react-native").LucideIcon;
142
+ readonly video: import("lucide-react-native").LucideIcon;
143
+ readonly "wifi-off": import("lucide-react-native").LucideIcon;
144
+ readonly wrench: import("lucide-react-native").LucideIcon;
145
+ readonly x: import("lucide-react-native").LucideIcon;
146
+ readonly zap: import("lucide-react-native").LucideIcon;
147
+ };
148
+ export type IconName = keyof typeof ICONS;