@mrmeg/expo-ui 0.27.1 → 0.29.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 (155) hide show
  1. package/CHANGELOG.md +666 -0
  2. package/LLM_USAGE.md +241 -37
  3. package/README.md +591 -136
  4. package/bin/expo-ui-icons.mjs +509 -0
  5. package/dist/components/Accordion.js +12 -6
  6. package/dist/components/AnimatedView.d.ts +1 -1
  7. package/dist/components/AnimatedView.js +5 -2
  8. package/dist/components/Avatar.d.ts +1 -1
  9. package/dist/components/Avatar.js +2 -2
  10. package/dist/components/Badge.js +5 -1
  11. package/dist/components/BottomSheet.d.ts +29 -1
  12. package/dist/components/BottomSheet.js +107 -30
  13. package/dist/components/Button.d.ts +20 -4
  14. package/dist/components/Button.js +60 -43
  15. package/dist/components/Card.d.ts +1 -1
  16. package/dist/components/Card.js +24 -4
  17. package/dist/components/Carousel.js +2 -2
  18. package/dist/components/Checkbox.js +51 -44
  19. package/dist/components/Collapsible.js +7 -3
  20. package/dist/components/Dialog.js +87 -16
  21. package/dist/components/Drawer.d.ts +1 -1
  22. package/dist/components/Drawer.js +65 -55
  23. package/dist/components/DropdownMenu.js +11 -7
  24. package/dist/components/EmptyState.d.ts +2 -2
  25. package/dist/components/EmptyState.js +5 -2
  26. package/dist/components/Hydrated.d.ts +31 -0
  27. package/dist/components/Hydrated.js +30 -0
  28. package/dist/components/Icon.d.ts +71 -28
  29. package/dist/components/Icon.js +70 -28
  30. package/dist/components/IconGlyph.d.ts +15 -0
  31. package/dist/components/IconGlyph.js +32 -0
  32. package/dist/components/IconGlyph.native.d.ts +10 -0
  33. package/dist/components/IconGlyph.native.js +30 -0
  34. package/dist/components/InputOTP.js +5 -2
  35. package/dist/components/Item.d.ts +103 -4
  36. package/dist/components/Item.js +168 -15
  37. package/dist/components/KeyboardAvoidingView.d.ts +1 -1
  38. package/dist/components/KeyboardAvoidingView.js +4 -1
  39. package/dist/components/Label.js +3 -2
  40. package/dist/components/MaxWidthContainer.js +1 -1
  41. package/dist/components/Notification.js +45 -27
  42. package/dist/components/Popover.d.ts +18 -5
  43. package/dist/components/Popover.js +72 -8
  44. package/dist/components/Progress.js +8 -6
  45. package/dist/components/RadioGroup.js +52 -49
  46. package/dist/components/Screen.d.ts +61 -0
  47. package/dist/components/Screen.js +62 -0
  48. package/dist/components/SectionHeader.d.ts +1 -1
  49. package/dist/components/SegmentedControl.d.ts +12 -4
  50. package/dist/components/SegmentedControl.js +94 -4
  51. package/dist/components/Select.js +21 -26
  52. package/dist/components/Skeleton.js +24 -10
  53. package/dist/components/Slider.d.ts +11 -3
  54. package/dist/components/Slider.js +73 -6
  55. package/dist/components/StatCard.d.ts +4 -2
  56. package/dist/components/StatCard.js +3 -1
  57. package/dist/components/StyledText.d.ts +7 -1
  58. package/dist/components/StyledText.js +5 -3
  59. package/dist/components/Switch.d.ts +1 -1
  60. package/dist/components/Switch.js +20 -30
  61. package/dist/components/Tabs.d.ts +2 -2
  62. package/dist/components/Tabs.js +13 -24
  63. package/dist/components/TextInput.d.ts +1 -1
  64. package/dist/components/TextInput.js +71 -26
  65. package/dist/components/ThemeSelector.d.ts +52 -0
  66. package/dist/components/ThemeSelector.js +90 -0
  67. package/dist/components/Toggle.d.ts +5 -49
  68. package/dist/components/Toggle.js +53 -29
  69. package/dist/components/ToggleGroup.d.ts +4 -8
  70. package/dist/components/ToggleGroup.js +52 -34
  71. package/dist/components/Tooltip.d.ts +1 -1
  72. package/dist/components/Tooltip.js +9 -3
  73. package/dist/components/UIProvider.d.ts +22 -2
  74. package/dist/components/UIProvider.js +15 -3
  75. package/dist/components/bottomSheetDismiss.d.ts +19 -0
  76. package/dist/components/bottomSheetDismiss.js +44 -0
  77. package/dist/components/iconBuiltins.generated.d.ts +143 -0
  78. package/dist/components/iconBuiltins.generated.js +25 -0
  79. package/dist/components/iconTypes.d.ts +37 -0
  80. package/dist/components/iconTypes.js +1 -0
  81. package/dist/components/index.d.ts +47 -44
  82. package/dist/components/index.js +3 -0
  83. package/dist/components/keyboardDismiss.js +12 -4
  84. package/dist/components/nativeTextField.android.js +13 -9
  85. package/dist/constants/colors.d.ts +40 -5
  86. package/dist/constants/colors.js +26 -8
  87. package/dist/constants/fonts.d.ts +41 -2
  88. package/dist/constants/fonts.js +46 -7
  89. package/dist/constants/index.d.ts +5 -4
  90. package/dist/constants/index.js +1 -0
  91. package/dist/constants/interaction.d.ts +18 -0
  92. package/dist/constants/interaction.js +18 -0
  93. package/dist/constants/spacing.d.ts +2 -1
  94. package/dist/constants/spacing.js +2 -1
  95. package/dist/design-system.json +429 -132
  96. package/dist/hooks/index.d.ts +12 -7
  97. package/dist/hooks/index.js +5 -0
  98. package/dist/hooks/useDimensions.d.ts +13 -8
  99. package/dist/hooks/useDimensions.js +123 -48
  100. package/dist/hooks/useFocusVisible.d.ts +30 -0
  101. package/dist/hooks/useFocusVisible.js +45 -0
  102. package/dist/hooks/useFontStyle.d.ts +2 -2
  103. package/dist/hooks/useFontStyle.js +3 -2
  104. package/dist/hooks/useHydrated.d.ts +13 -0
  105. package/dist/hooks/useHydrated.js +19 -0
  106. package/dist/hooks/useNotificationOffset.d.ts +20 -0
  107. package/dist/hooks/useNotificationOffset.js +34 -0
  108. package/dist/hooks/useResources.d.ts +21 -20
  109. package/dist/hooks/useResources.js +72 -68
  110. package/dist/hooks/useScalePress.d.ts +8 -5
  111. package/dist/hooks/useScalePress.js +16 -9
  112. package/dist/hooks/useShape.d.ts +24 -0
  113. package/dist/hooks/useShape.js +24 -0
  114. package/dist/hooks/useStaggeredEntrance.js +9 -7
  115. package/dist/hooks/useTheme.d.ts +3 -3
  116. package/dist/hooks/useTheme.js +177 -66
  117. package/dist/hooks/useWindowInsets.d.ts +13 -0
  118. package/dist/hooks/useWindowInsets.js +23 -0
  119. package/dist/icons/LICENSE.lucide +43 -0
  120. package/dist/icons/lucide.json +2144 -0
  121. package/dist/index.d.ts +5 -5
  122. package/dist/lib/composeRefs.d.ts +8 -0
  123. package/dist/lib/composeRefs.js +20 -0
  124. package/dist/lib/haptics.d.ts +12 -0
  125. package/dist/lib/haptics.js +21 -0
  126. package/dist/lib/index.d.ts +6 -5
  127. package/dist/lib/index.js +1 -0
  128. package/dist/lib/interFonts.d.ts +15 -0
  129. package/dist/lib/interFonts.js +1 -0
  130. package/dist/lib/interFonts.native.d.ts +3 -0
  131. package/dist/lib/interFonts.native.js +20 -0
  132. package/dist/lib/popoverPlacement.d.ts +36 -0
  133. package/dist/lib/popoverPlacement.js +29 -0
  134. package/dist/lib/themedStyles.d.ts +1 -1
  135. package/dist/lib/useAnimatedValue.d.ts +15 -0
  136. package/dist/lib/useAnimatedValue.js +19 -0
  137. package/dist/lib/webHitTarget.d.ts +40 -0
  138. package/dist/lib/webHitTarget.js +37 -0
  139. package/dist/state/feedbackStore.d.ts +24 -0
  140. package/dist/state/feedbackStore.js +16 -0
  141. package/dist/state/globalUIStore.d.ts +27 -0
  142. package/dist/state/globalUIStore.js +23 -0
  143. package/dist/state/index.d.ts +7 -5
  144. package/dist/state/index.js +2 -0
  145. package/dist/state/initialScheme.d.ts +27 -0
  146. package/dist/state/initialScheme.js +29 -0
  147. package/dist/state/notify.d.ts +1 -1
  148. package/dist/state/themeColorScope.d.ts +1 -1
  149. package/dist/state/themeStore.d.ts +69 -16
  150. package/dist/state/themeStore.js +65 -38
  151. package/llms-full.md +59 -10
  152. package/llms.txt +10 -1
  153. package/package.json +79 -4
  154. package/dist/components/iconRegistry.generated.d.ts +0 -148
  155. package/dist/components/iconRegistry.generated.js +0 -296
@@ -3,9 +3,10 @@ import * as React from "react";
3
3
  import { Animated, Platform, StyleSheet, Text, View } from "react-native";
4
4
  import { Icon } from "./Icon.js";
5
5
  import { AnimatedView } from "./AnimatedView.js";
6
- import { TextClassContext, TextSelectabilityContext } from "./StyledText.context";
6
+ import { TextClassContext, TextSelectabilityContext } from "./StyledText.context.js";
7
7
  import { useTheme } from "../hooks/useTheme.js";
8
8
  import { spacing } from "../constants/spacing.js";
9
+ import { interaction } from "../constants/interaction.js";
9
10
  import { useScalePress } from "../hooks/useScalePress.js";
10
11
  import * as DropdownMenuPrimitive from "@rn-primitives/dropdown-menu";
11
12
  import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
@@ -76,7 +77,7 @@ function DropdownMenuContent({ side, align = "start", sideOffset = 4, portalHost
76
77
  return (_jsx(DropdownMenuPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(DropdownMenuPrimitive.Overlay, { style: Platform.select({
77
78
  native: StyleSheet.absoluteFill,
78
79
  default: undefined,
79
- }), children: _jsx(AnimatedView, { type: "fade", style: StyleSheet.absoluteFill, pointerEvents: "box-none", children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(DropdownMenuPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, insets: insets, avoidCollisions: true, ...props, style: {
80
+ }), children: _jsx(AnimatedView, { type: "fade", style: [StyleSheet.absoluteFill, styles.passThrough], children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(DropdownMenuPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, insets: insets, avoidCollisions: true, ...props, style: {
80
81
  backgroundColor: theme.colors.popover,
81
82
  borderWidth: 1,
82
83
  borderColor: theme.colors.border,
@@ -109,7 +110,7 @@ function DropdownMenuItem({ inset = false, variant = "default", style: styleOver
109
110
  outlineStyle: "none",
110
111
  userSelect: "none",
111
112
  }),
112
- ...(props.disabled && { opacity: 0.5 }),
113
+ ...(props.disabled && { opacity: interaction.disabledOpacity }),
113
114
  ...(inset && { paddingLeft: spacing.xl }),
114
115
  ...(styleOverride && typeof styleOverride !== "function"
115
116
  ? StyleSheet.flatten(styleOverride)
@@ -130,7 +131,7 @@ function DropdownMenuCheckboxItem({ children, style: styleOverride, ...props })
130
131
  outlineStyle: "none",
131
132
  userSelect: "none",
132
133
  }),
133
- ...(props.disabled && { opacity: 0.5 }),
134
+ ...(props.disabled && { opacity: interaction.disabledOpacity }),
134
135
  ...(styleOverride && typeof styleOverride !== "function"
135
136
  ? StyleSheet.flatten(styleOverride)
136
137
  : {}),
@@ -157,7 +158,7 @@ function DropdownMenuRadioItem({ children, style: styleOverride, ...props }) {
157
158
  outlineStyle: "none",
158
159
  userSelect: "none",
159
160
  }),
160
- ...(props.disabled && { opacity: 0.5 }),
161
+ ...(props.disabled && { opacity: interaction.disabledOpacity }),
161
162
  ...(styleOverride && typeof styleOverride !== "function"
162
163
  ? StyleSheet.flatten(styleOverride)
163
164
  : {}),
@@ -172,7 +173,7 @@ function DropdownMenuRadioItem({ children, style: styleOverride, ...props }) {
172
173
  backgroundColor: theme.colors.text,
173
174
  height: 8,
174
175
  width: 8,
175
- borderRadius: 4,
176
+ borderRadius: spacing.radiusFull,
176
177
  } }) }) }), typeof children === "function" ? null : children] }) }) }) }));
177
178
  }
178
179
  function DropdownMenuLabel({ inset = false, style: styleOverride, ...props }) {
@@ -216,7 +217,10 @@ function DropdownMenuShortcut({ children, text, style: styleOverride, ...props }
216
217
  styleOverride,
217
218
  ], children: text ?? children }));
218
219
  }
219
- const styles = StyleSheet.create({
220
+ const styles = /*#__PURE__*/ StyleSheet.create({
221
+ // Overlay layer: covers the screen without swallowing touches (style, not the
222
+ // deprecated `pointerEvents` prop).
223
+ passThrough: { pointerEvents: "box-none" },
220
224
  item: {
221
225
  position: "relative",
222
226
  flexDirection: "row",
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { StyleProp, ViewStyle } from "react-native";
3
- import { type ButtonProps } from "./Button";
4
- import { type IconName } from "./Icon";
3
+ import { type ButtonProps } from "./Button.js";
4
+ import { type IconName } from "./Icon.js";
5
5
  export interface EmptyStateProps {
6
6
  /** Icon name to display */
7
7
  icon?: IconName;
@@ -4,6 +4,7 @@ import { SansSerifBoldText, SansSerifText } from "./StyledText.js";
4
4
  import { Button } from "./Button.js";
5
5
  import { Icon } from "./Icon.js";
6
6
  import { useTheme } from "../hooks/useTheme.js";
7
+ import { shapeRadius, useShape } from "../hooks/useShape.js";
7
8
  import { spacing } from "../constants/spacing.js";
8
9
  import { createThemedStyles } from "../lib/themedStyles.js";
9
10
  /**
@@ -26,7 +27,9 @@ import { createThemedStyles } from "../lib/themedStyles.js";
26
27
  export function EmptyState({ icon, iconSize = spacing.iconMd, title, description, actionLabel, onAction, actionPreset = "default", style, children, bordered = false, }) {
27
28
  const { theme } = useTheme();
28
29
  const styles = themedStyles(theme);
29
- return (_jsxs(View, { style: [styles.container, bordered && styles.bordered, style], children: [!!icon && (_jsx(View, { style: styles.iconWrapper, children: _jsx(Icon, { name: icon, size: iconSize, color: theme.colors.mutedForeground }) })), _jsx(SansSerifBoldText, { selectable: false, size: "lg", style: styles.title, children: title }), !!description && (_jsx(SansSerifText, { selectable: false, size: "base", style: styles.description, children: description })), children, !!actionLabel && onAction && (_jsx(Button, { preset: actionPreset, onPress: onAction, text: actionLabel, style: styles.action }))] }));
30
+ // Host-app `setShape({ card })` shapes the bordered frame like a Card.
31
+ const cardRadius = shapeRadius(useShape("card"));
32
+ return (_jsxs(View, { style: [styles.container, bordered && styles.bordered, bordered && cardRadius, style], children: [!!icon && (_jsx(View, { style: styles.iconWrapper, children: _jsx(Icon, { name: icon, size: iconSize, color: theme.colors.mutedForeground }) })), _jsx(SansSerifBoldText, { selectable: false, size: "lg", style: styles.title, children: title }), !!description && (_jsx(SansSerifText, { selectable: false, size: "base", style: styles.description, children: description })), children, !!actionLabel && onAction && (_jsx(Button, { preset: actionPreset, onPress: onAction, text: actionLabel, style: styles.action }))] }));
30
33
  }
31
34
  const MEDIA_SIZE = 56;
32
35
  const createStyles = (theme) => StyleSheet.create({
@@ -74,4 +77,4 @@ const createStyles = (theme) => StyleSheet.create({
74
77
  alignSelf: "center",
75
78
  },
76
79
  });
77
- const themedStyles = createThemedStyles(createStyles);
80
+ const themedStyles = /*#__PURE__*/ createThemedStyles(createStyles);
@@ -0,0 +1,31 @@
1
+ import type { ReactNode } from "react";
2
+ export interface HydratedProps {
3
+ /** What the server HTML and the hydration pass show in place of the children. @default null */
4
+ fallback?: ReactNode;
5
+ children: ReactNode;
6
+ }
7
+ /**
8
+ * Hydrated
9
+ *
10
+ * Renders `fallback` on the server and through the hydration pass, then its
11
+ * children in the first client render — the gate for markup the server cannot
12
+ * reproduce. Under Expo Router's SSR the server renders the app inside
13
+ * `+html.tsx` while the client hydrates `#root`, so every `useId()` differs
14
+ * between the two trees; the kit defers its own ids, but a Radix-backed
15
+ * component (`Tabs`, `Accordion`, `Collapsible`, `Select`, `DropdownMenu`,
16
+ * `Popover`) emits ids on both sides and logs a hydration mismatch when it is
17
+ * part of a server-rendered route. Wrap that subtree, not the page: the rest of
18
+ * the screen keeps its server HTML. Give it a fallback that holds the layout
19
+ * (a placeholder the same height) so nothing shifts when the children mount.
20
+ *
21
+ * Do not use it for content that must be in the server HTML (a FAQ's questions,
22
+ * a product's description); accept the development-only log there instead. On
23
+ * native there is no hydration pass, so children render immediately.
24
+ *
25
+ * ```tsx
26
+ * <Hydrated fallback={<View style={{ height: 240 }} />}>
27
+ * <Tabs defaultValue="day">…</Tabs>
28
+ * </Hydrated>
29
+ * ```
30
+ */
31
+ export declare function Hydrated({ fallback, children }: HydratedProps): import("react").JSX.Element;
@@ -0,0 +1,30 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useHydrated } from "../hooks/useHydrated.js";
3
+ /**
4
+ * Hydrated
5
+ *
6
+ * Renders `fallback` on the server and through the hydration pass, then its
7
+ * children in the first client render — the gate for markup the server cannot
8
+ * reproduce. Under Expo Router's SSR the server renders the app inside
9
+ * `+html.tsx` while the client hydrates `#root`, so every `useId()` differs
10
+ * between the two trees; the kit defers its own ids, but a Radix-backed
11
+ * component (`Tabs`, `Accordion`, `Collapsible`, `Select`, `DropdownMenu`,
12
+ * `Popover`) emits ids on both sides and logs a hydration mismatch when it is
13
+ * part of a server-rendered route. Wrap that subtree, not the page: the rest of
14
+ * the screen keeps its server HTML. Give it a fallback that holds the layout
15
+ * (a placeholder the same height) so nothing shifts when the children mount.
16
+ *
17
+ * Do not use it for content that must be in the server HTML (a FAQ's questions,
18
+ * a product's description); accept the development-only log there instead. On
19
+ * native there is no hydration pass, so children render immediately.
20
+ *
21
+ * ```tsx
22
+ * <Hydrated fallback={<View style={{ height: 240 }} />}>
23
+ * <Tabs defaultValue="day">…</Tabs>
24
+ * </Hydrated>
25
+ * ```
26
+ */
27
+ export function Hydrated({ fallback = null, children }) {
28
+ const hydrated = useHydrated();
29
+ return _jsx(_Fragment, { children: hydrated ? children : fallback });
30
+ }
@@ -1,14 +1,40 @@
1
1
  import * as React from "react";
2
- import type { StyleProp, TextStyle, ViewProps, ViewStyle } from "react-native";
3
- import type { ThemeColors } from "../constants/colors";
4
- import { type IconName } from "./iconRegistry.generated";
2
+ import { type StyleProp, type TextStyle } from "react-native";
3
+ import type { ThemeColors } from "../constants/colors.js";
4
+ import { type BuiltinIconName } from "./iconBuiltins.generated.js";
5
+ import type { IconComponent, IconGlyph } from "./iconTypes.js";
6
+ export type { IconAccessibilityProps, IconComponent, IconGlyph, IconTag } from "./iconTypes.js";
5
7
  /**
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.
8
+ * Every name `Icon` accepts, as keys. The package registers the names its own
9
+ * components draw (`icon-names.json`); an app adds the rest by listing them in
10
+ * `client/icons.json` and running `expo-ui-icons`, whose generated module
11
+ * augments this interface and exports the glyphs for `registerIcons`:
12
+ *
13
+ * ```ts
14
+ * declare module "@mrmeg/expo-ui/components/Icon" {
15
+ * interface IconRegistry { "archive": true }
16
+ * }
17
+ * ```
18
+ */
19
+ export interface IconRegistry extends Record<BuiltinIconName, true> {
20
+ }
21
+ /**
22
+ * Kebab-case Lucide names `Icon` can draw: the package builtins plus every
23
+ * name the app registered. An unregistered literal is a type error.
24
+ */
25
+ export type IconName = Extract<keyof IconRegistry, string>;
26
+ /**
27
+ * Make glyphs drawable by name. Call it once at module scope in the root
28
+ * layout with the module `expo-ui-icons` generates
29
+ * (`registerIcons(appIcons)`), so it has run before any route renders, on the
30
+ * server too. Idempotent; a later call wins for a name it repeats.
10
31
  */
11
- export type { IconName };
32
+ export declare function registerIcons(icons: Readonly<Record<string, IconGlyph>>): void;
33
+ /**
34
+ * Whether a runtime value (a database field, a server payload) names a
35
+ * registered icon. Narrows it to `IconName` for `<Icon name>`.
36
+ */
37
+ export declare function isIconName(value: unknown): value is IconName;
12
38
  /**
13
39
  * Theme color names that can be used as shortcuts.
14
40
  *
@@ -23,32 +49,49 @@ type IconBaseProps = {
23
49
  color?: string | ThemeColorName;
24
50
  /** Additional styles for positioning, transforms, etc. */
25
51
  style?: StyleProp<TextStyle>;
26
- /** When true, hides the icon from the accessibility tree. @default false */
52
+ /**
53
+ * Hides the icon from the accessibility tree. An icon with no
54
+ * `accessibilityLabel` is already hidden (see below), so this is only
55
+ * needed to silence a labelled icon. @default false
56
+ */
27
57
  decorative?: boolean;
58
+ /**
59
+ * What the icon means, for an icon that stands alone (no visible label next
60
+ * to it): `role="img"` + `aria-label` on web, `accessible` +
61
+ * `accessibilityLabel` on native. Omit it when the icon sits beside text
62
+ * that already says it — an unlabeled icon is hidden from assistive tech,
63
+ * which would otherwise announce an unnamed image.
64
+ */
65
+ accessibilityLabel?: string;
66
+ /** Test id for a named icon's SVG root. Defaults to `icon-<name>` in development. */
67
+ testID?: string;
28
68
  };
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
- */
35
- type CustomIconComponentProps = {
36
- size: number;
37
- color: string;
38
- style?: StyleProp<ViewStyle>;
39
- } & Partial<IconAccessibilityProps>;
40
- type RegistryIconProps = IconBaseProps & {
41
- /** The icon name to render (a Lucide name from the package registry) */
69
+ export type NamedIconProps = IconBaseProps & {
70
+ /** A registered icon name: a package builtin or one listed in the app's `client/icons.json`. */
42
71
  name: IconName;
43
72
  component?: never;
44
73
  };
45
- type CustomIconProps = IconBaseProps & {
74
+ export type IconProps = NamedIconProps | (IconBaseProps & {
46
75
  name?: never;
47
76
  /**
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.
77
+ * A component to render instead of a registered glyph, for art that is
78
+ * not a Lucide icon. Receives the resolved size, color, style, and
79
+ * accessibility props, exactly like a named icon.
50
80
  */
51
- component: React.ComponentType<CustomIconComponentProps>;
52
- };
53
- export type IconProps = RegistryIconProps | CustomIconProps;
81
+ component: IconComponent;
82
+ });
83
+ /**
84
+ * Universal Icon Component
85
+ * Draws a registered glyph as SVG with theme integration and style support:
86
+ * an inline `<svg>` on web and the server, `react-native-svg` on iOS and
87
+ * Android. No font to load, and server markup carries the glyph.
88
+ *
89
+ * Usage:
90
+ * ```tsx
91
+ * <Icon name="check" color="primary" size={16} />
92
+ * <Icon name="calendar" color="#FF0000" size={24} />
93
+ * <Icon name="terminal" style={{ marginRight: 8 }} />
94
+ * <Icon component={BrandMark} color="accent" /> // non-Lucide art
95
+ * ```
96
+ */
54
97
  export declare function Icon(props: IconProps): React.JSX.Element | null;
@@ -1,6 +1,27 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Platform } from "react-native";
2
3
  import { useTheme } from "../hooks/useTheme.js";
3
- import { ICONS } from "./iconRegistry.generated";
4
+ import { BUILTIN_ICONS } from "./iconBuiltins.generated.js";
5
+ import { IconGlyph as GlyphRenderer } from "./IconGlyph";
6
+ /** Glyphs by name: the builtins, then whatever `registerIcons` adds. */
7
+ const registry = new Map(Object.entries(BUILTIN_ICONS));
8
+ /**
9
+ * Make glyphs drawable by name. Call it once at module scope in the root
10
+ * layout with the module `expo-ui-icons` generates
11
+ * (`registerIcons(appIcons)`), so it has run before any route renders, on the
12
+ * server too. Idempotent; a later call wins for a name it repeats.
13
+ */
14
+ export function registerIcons(icons) {
15
+ for (const [name, glyph] of Object.entries(icons))
16
+ registry.set(name, glyph);
17
+ }
18
+ /**
19
+ * Whether a runtime value (a database field, a server payload) names a
20
+ * registered icon. Narrows it to `IconName` for `<Icon name>`.
21
+ */
22
+ export function isIconName(value) {
23
+ return typeof value === "string" && registry.has(value);
24
+ }
4
25
  /**
5
26
  * Resolve an icon color against the active theme.
6
27
  *
@@ -18,41 +39,62 @@ function resolveIconColor(color, themeColors) {
18
39
  }
19
40
  return color;
20
41
  }
42
+ /**
43
+ * On web the SVG root is a DOM element and React forwards every prop to it,
44
+ * so the RN-only accessibility props (`accessible`,
45
+ * `importantForAccessibility`, `accessibilityElementsHidden`) surface as
46
+ * "unknown prop" warnings on every page. Web gets ARIA only; native keeps
47
+ * the RN props.
48
+ *
49
+ * Only a labelled icon is exposed. An icon without a label used to render as
50
+ * an unnamed `role="img"` (web) or an `accessible` element with nothing to
51
+ * say (native) — dozens per screen, each a WCAG 1.1.1 failure — so no label
52
+ * now means decorative.
53
+ */
54
+ function iconAccessibilityProps(decorative, label) {
55
+ const hidden = decorative || label === undefined;
56
+ if (Platform.OS === "web") {
57
+ return hidden ? { "aria-hidden": true } : { role: "img", "aria-label": label };
58
+ }
59
+ if (hidden) {
60
+ return {
61
+ accessible: false,
62
+ importantForAccessibility: "no-hide-descendants",
63
+ accessibilityElementsHidden: true,
64
+ "aria-hidden": true,
65
+ };
66
+ }
67
+ return { accessible: true, accessibilityLabel: label };
68
+ }
69
+ const warnedUnknownIconNames = new Set();
70
+ function warnUnknownIconName(name) {
71
+ if (!__DEV__ || warnedUnknownIconNames.has(name))
72
+ return;
73
+ warnedUnknownIconNames.add(name);
74
+ console.warn(`Icon: "${name}" is not a registered icon, so nothing was rendered. ` +
75
+ "Add it to client/icons.json, run `bun run icons`, and make sure " +
76
+ "`registerIcons(appIcons)` runs in the root layout.");
77
+ }
21
78
  /**
22
79
  * Universal Icon Component
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.
80
+ * Draws a registered glyph as SVG with theme integration and style support:
81
+ * an inline `<svg>` on web and the server, `react-native-svg` on iOS and
82
+ * Android. No font to load, and server markup carries the glyph.
26
83
  *
27
84
  * Usage:
28
85
  * ```tsx
29
86
  * <Icon name="check" color="primary" size={16} />
30
87
  * <Icon name="calendar" color="#FF0000" size={24} />
31
88
  * <Icon name="terminal" style={{ marginRight: 8 }} />
32
- * <Icon component={Rocket} color="accent" /> // any Lucide import
89
+ * <Icon component={BrandMark} color="accent" /> // non-Lucide art
33
90
  * ```
34
91
  */
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
- }
43
92
  export function Icon(props) {
44
- const { size = 24, color, style, decorative = false } = props;
93
+ const { size = 24, color, style, decorative = false, accessibilityLabel } = props;
45
94
  const { theme } = useTheme();
46
95
  const iconColor = resolveIconColor(color, theme.colors);
47
96
  const CustomComponent = "component" in props ? props.component : undefined;
48
- const accessibilityProps = decorative
49
- ? {
50
- accessible: false,
51
- importantForAccessibility: "no-hide-descendants",
52
- accessibilityElementsHidden: true,
53
- "aria-hidden": true,
54
- }
55
- : { accessible: true };
97
+ const accessibilityProps = iconAccessibilityProps(decorative, accessibilityLabel);
56
98
  // `style` stays a text style on the public prop for source compatibility
57
99
  // with the font-icon era; SVG roots take a view style, and every layout
58
100
  // property callers actually pass (margins, transforms) belongs to both.
@@ -60,14 +102,14 @@ export function Icon(props) {
60
102
  if (CustomComponent) {
61
103
  return (_jsx(CustomComponent, { size: size, color: iconColor, style: iconStyle, ...accessibilityProps }));
62
104
  }
63
- const name = props.name;
64
- const Lucide = ICONS[name];
105
+ const { name, testID } = props;
106
+ const glyph = registry.get(name);
65
107
  // `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) {
108
+ // runtime (a database field, a config file) and an app can forget to
109
+ // register. Draw nothing and say so once per name in development.
110
+ if (!glyph) {
69
111
  warnUnknownIconName(name);
70
112
  return null;
71
113
  }
72
- return (_jsx(Lucide, { size: size, color: iconColor, style: iconStyle, ...accessibilityProps }));
114
+ return (_jsx(GlyphRenderer, { glyph: glyph, size: size, color: iconColor, style: iconStyle, testID: testID ?? (__DEV__ ? `icon-${name}` : undefined), ...accessibilityProps }));
73
115
  }
@@ -0,0 +1,15 @@
1
+ import { type ReactElement } from "react";
2
+ import type { IconGlyphProps } from "./iconTypes.js";
3
+ /**
4
+ * Web and server renderer for a glyph: one inline `<svg>` built with
5
+ * react-native-web's `unstable_createElement`, so the style array, `testID`
6
+ * (`data-testid`), and ARIA props convert exactly as they do on a `View`, and
7
+ * `react-dom/server` emits the full markup with no DOM.
8
+ *
9
+ * The SVG strokes `currentColor` and carries the color as a `color` style. On
10
+ * web every theme color is a `var(--c-*)` string that is identical in both
11
+ * schemes, so server and client markup match and a theme change recolors the
12
+ * icon through CSS alone. No hooks and no ids: nothing here can differ
13
+ * between the server render and hydration.
14
+ */
15
+ export declare function IconGlyph({ glyph, size, color, strokeWidth, style, testID, ...accessibility }: IconGlyphProps): ReactElement;
@@ -0,0 +1,32 @@
1
+ import { createElement } from "react";
2
+ import { unstable_createElement } from "react-native-web";
3
+ /**
4
+ * Web and server renderer for a glyph: one inline `<svg>` built with
5
+ * react-native-web's `unstable_createElement`, so the style array, `testID`
6
+ * (`data-testid`), and ARIA props convert exactly as they do on a `View`, and
7
+ * `react-dom/server` emits the full markup with no DOM.
8
+ *
9
+ * The SVG strokes `currentColor` and carries the color as a `color` style. On
10
+ * web every theme color is a `var(--c-*)` string that is identical in both
11
+ * schemes, so server and client markup match and a theme change recolors the
12
+ * icon through CSS alone. No hooks and no ids: nothing here can differ
13
+ * between the server render and hydration.
14
+ */
15
+ export function IconGlyph({ glyph, size, color, strokeWidth = 2, style, testID, ...accessibility }) {
16
+ return unstable_createElement("svg", {
17
+ xmlns: "http://www.w3.org/2000/svg",
18
+ width: size,
19
+ height: size,
20
+ viewBox: "0 0 24 24",
21
+ fill: "none",
22
+ stroke: "currentColor",
23
+ strokeWidth,
24
+ strokeLinecap: "round",
25
+ strokeLinejoin: "round",
26
+ style: [{ color }, style, { pointerEvents: "none" }],
27
+ ...accessibility,
28
+ testID,
29
+ // react-native-web reads children from props; a third argument is options.
30
+ children: glyph.map(([tag, attrs], index) => createElement(tag, { key: index, ...attrs })),
31
+ });
32
+ }
@@ -0,0 +1,10 @@
1
+ import { type ReactElement } from "react";
2
+ import type { IconGlyphProps } from "./iconTypes.js";
3
+ /**
4
+ * iOS and Android renderer for a glyph, drawn with `react-native-svg` the way
5
+ * Lucide's own native component draws it, so the pixels match: the root sets
6
+ * the 24×24 view box, no fill, a round 2 px stroke in `color`, and `color` itself
7
+ * (so a `fill="currentColor"` node resolves), and every child repeats the fill,
8
+ * stroke, width, cap, and join before its own attributes.
9
+ */
10
+ export declare function IconGlyph({ glyph, size, color, strokeWidth, style, testID, ...accessibility }: IconGlyphProps): ReactElement;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createElement } from "react";
3
+ import { Circle, Ellipse, Line, Path, Polygon, Polyline, Rect, Svg } from "react-native-svg";
4
+ const ELEMENTS = {
5
+ path: Path,
6
+ circle: Circle,
7
+ rect: Rect,
8
+ line: Line,
9
+ polyline: Polyline,
10
+ polygon: Polygon,
11
+ ellipse: Ellipse,
12
+ };
13
+ /**
14
+ * iOS and Android renderer for a glyph, drawn with `react-native-svg` the way
15
+ * Lucide's own native component draws it, so the pixels match: the root sets
16
+ * the 24×24 view box, no fill, a round 2 px stroke in `color`, and `color` itself
17
+ * (so a `fill="currentColor"` node resolves), and every child repeats the fill,
18
+ * stroke, width, cap, and join before its own attributes.
19
+ */
20
+ export function IconGlyph({ glyph, size, color, strokeWidth = 2, style, testID, ...accessibility }) {
21
+ return (_jsx(Svg, { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, color: color, strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", style: style, testID: testID, ...accessibility, children: glyph.map(([tag, attrs], index) => createElement(ELEMENTS[tag], {
22
+ key: index,
23
+ fill: "none",
24
+ stroke: color,
25
+ strokeWidth,
26
+ strokeLinecap: "round",
27
+ strokeLinejoin: "round",
28
+ ...attrs,
29
+ })) }));
30
+ }
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useRef, useState, useCallback } from "react";
3
3
  import { View, TextInput as RNTextInput, Pressable, StyleSheet, Platform, } from "react-native";
4
4
  import { useTheme } from "../hooks/useTheme.js";
5
+ import { useShape } from "../hooks/useShape.js";
5
6
  import { spacing } from "../constants/spacing.js";
6
7
  import { StyledText } from "./StyledText.js";
7
8
  import { createThemedStyles } from "../lib/themedStyles.js";
@@ -85,6 +86,8 @@ function OTPCell({ index, total, char, isActive, hasError, disabled, theme, onPr
85
86
  // pass every frame. The 1↔2px and color changes read as instant for OTP
86
87
  // cells, so compute both during render.
87
88
  const borderWidth = isActive && !hasError ? 2 : 1;
89
+ // Host-app `setShape({ input })`: cells share the TextInput radius.
90
+ const inputShape = useShape("input");
88
91
  const borderColor = hasError
89
92
  ? theme.colors.destructive
90
93
  : isActive
@@ -94,7 +97,7 @@ function OTPCell({ index, total, char, isActive, hasError, disabled, theme, onPr
94
97
  {
95
98
  width: CELL_WIDTH,
96
99
  height: CELL_HEIGHT,
97
- borderRadius: spacing.radiusMd,
100
+ borderRadius: inputShape?.borderRadius ?? spacing.radiusMd,
98
101
  borderWidth,
99
102
  justifyContent: "center",
100
103
  alignItems: "center",
@@ -132,5 +135,5 @@ const createStyles = (theme) => StyleSheet.create({
132
135
  marginTop: spacing.xs,
133
136
  },
134
137
  });
135
- const themedStyles = createThemedStyles(createStyles);
138
+ const themedStyles = /*#__PURE__*/ createThemedStyles(createStyles);
136
139
  export { InputOTP };