@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.
- package/CHANGELOG.md +666 -0
- package/LLM_USAGE.md +241 -37
- package/README.md +591 -136
- package/bin/expo-ui-icons.mjs +509 -0
- package/dist/components/Accordion.js +12 -6
- package/dist/components/AnimatedView.d.ts +1 -1
- package/dist/components/AnimatedView.js +5 -2
- package/dist/components/Avatar.d.ts +1 -1
- package/dist/components/Avatar.js +2 -2
- package/dist/components/Badge.js +5 -1
- package/dist/components/BottomSheet.d.ts +29 -1
- package/dist/components/BottomSheet.js +107 -30
- package/dist/components/Button.d.ts +20 -4
- package/dist/components/Button.js +60 -43
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +24 -4
- package/dist/components/Carousel.js +2 -2
- package/dist/components/Checkbox.js +51 -44
- package/dist/components/Collapsible.js +7 -3
- package/dist/components/Dialog.js +87 -16
- package/dist/components/Drawer.d.ts +1 -1
- package/dist/components/Drawer.js +65 -55
- package/dist/components/DropdownMenu.js +11 -7
- package/dist/components/EmptyState.d.ts +2 -2
- package/dist/components/EmptyState.js +5 -2
- package/dist/components/Hydrated.d.ts +31 -0
- package/dist/components/Hydrated.js +30 -0
- package/dist/components/Icon.d.ts +71 -28
- package/dist/components/Icon.js +70 -28
- package/dist/components/IconGlyph.d.ts +15 -0
- package/dist/components/IconGlyph.js +32 -0
- package/dist/components/IconGlyph.native.d.ts +10 -0
- package/dist/components/IconGlyph.native.js +30 -0
- package/dist/components/InputOTP.js +5 -2
- package/dist/components/Item.d.ts +103 -4
- package/dist/components/Item.js +168 -15
- package/dist/components/KeyboardAvoidingView.d.ts +1 -1
- package/dist/components/KeyboardAvoidingView.js +4 -1
- package/dist/components/Label.js +3 -2
- package/dist/components/MaxWidthContainer.js +1 -1
- package/dist/components/Notification.js +45 -27
- package/dist/components/Popover.d.ts +18 -5
- package/dist/components/Popover.js +72 -8
- package/dist/components/Progress.js +8 -6
- package/dist/components/RadioGroup.js +52 -49
- package/dist/components/Screen.d.ts +61 -0
- package/dist/components/Screen.js +62 -0
- package/dist/components/SectionHeader.d.ts +1 -1
- package/dist/components/SegmentedControl.d.ts +12 -4
- package/dist/components/SegmentedControl.js +94 -4
- package/dist/components/Select.js +21 -26
- package/dist/components/Skeleton.js +24 -10
- package/dist/components/Slider.d.ts +11 -3
- package/dist/components/Slider.js +73 -6
- package/dist/components/StatCard.d.ts +4 -2
- package/dist/components/StatCard.js +3 -1
- package/dist/components/StyledText.d.ts +7 -1
- package/dist/components/StyledText.js +5 -3
- package/dist/components/Switch.d.ts +1 -1
- package/dist/components/Switch.js +20 -30
- package/dist/components/Tabs.d.ts +2 -2
- package/dist/components/Tabs.js +13 -24
- package/dist/components/TextInput.d.ts +1 -1
- package/dist/components/TextInput.js +71 -26
- package/dist/components/ThemeSelector.d.ts +52 -0
- package/dist/components/ThemeSelector.js +90 -0
- package/dist/components/Toggle.d.ts +5 -49
- package/dist/components/Toggle.js +53 -29
- package/dist/components/ToggleGroup.d.ts +4 -8
- package/dist/components/ToggleGroup.js +52 -34
- package/dist/components/Tooltip.d.ts +1 -1
- package/dist/components/Tooltip.js +9 -3
- package/dist/components/UIProvider.d.ts +22 -2
- package/dist/components/UIProvider.js +15 -3
- package/dist/components/bottomSheetDismiss.d.ts +19 -0
- package/dist/components/bottomSheetDismiss.js +44 -0
- package/dist/components/iconBuiltins.generated.d.ts +143 -0
- package/dist/components/iconBuiltins.generated.js +25 -0
- package/dist/components/iconTypes.d.ts +37 -0
- package/dist/components/iconTypes.js +1 -0
- package/dist/components/index.d.ts +47 -44
- package/dist/components/index.js +3 -0
- package/dist/components/keyboardDismiss.js +12 -4
- package/dist/components/nativeTextField.android.js +13 -9
- package/dist/constants/colors.d.ts +40 -5
- package/dist/constants/colors.js +26 -8
- package/dist/constants/fonts.d.ts +41 -2
- package/dist/constants/fonts.js +46 -7
- package/dist/constants/index.d.ts +5 -4
- package/dist/constants/index.js +1 -0
- package/dist/constants/interaction.d.ts +18 -0
- package/dist/constants/interaction.js +18 -0
- package/dist/constants/spacing.d.ts +2 -1
- package/dist/constants/spacing.js +2 -1
- package/dist/design-system.json +429 -132
- package/dist/hooks/index.d.ts +12 -7
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/useDimensions.d.ts +13 -8
- package/dist/hooks/useDimensions.js +123 -48
- package/dist/hooks/useFocusVisible.d.ts +30 -0
- package/dist/hooks/useFocusVisible.js +45 -0
- package/dist/hooks/useFontStyle.d.ts +2 -2
- package/dist/hooks/useFontStyle.js +3 -2
- package/dist/hooks/useHydrated.d.ts +13 -0
- package/dist/hooks/useHydrated.js +19 -0
- package/dist/hooks/useNotificationOffset.d.ts +20 -0
- package/dist/hooks/useNotificationOffset.js +34 -0
- package/dist/hooks/useResources.d.ts +21 -20
- package/dist/hooks/useResources.js +72 -68
- package/dist/hooks/useScalePress.d.ts +8 -5
- package/dist/hooks/useScalePress.js +16 -9
- package/dist/hooks/useShape.d.ts +24 -0
- package/dist/hooks/useShape.js +24 -0
- package/dist/hooks/useStaggeredEntrance.js +9 -7
- package/dist/hooks/useTheme.d.ts +3 -3
- package/dist/hooks/useTheme.js +177 -66
- package/dist/hooks/useWindowInsets.d.ts +13 -0
- package/dist/hooks/useWindowInsets.js +23 -0
- package/dist/icons/LICENSE.lucide +43 -0
- package/dist/icons/lucide.json +2144 -0
- package/dist/index.d.ts +5 -5
- package/dist/lib/composeRefs.d.ts +8 -0
- package/dist/lib/composeRefs.js +20 -0
- package/dist/lib/haptics.d.ts +12 -0
- package/dist/lib/haptics.js +21 -0
- package/dist/lib/index.d.ts +6 -5
- package/dist/lib/index.js +1 -0
- package/dist/lib/interFonts.d.ts +15 -0
- package/dist/lib/interFonts.js +1 -0
- package/dist/lib/interFonts.native.d.ts +3 -0
- package/dist/lib/interFonts.native.js +20 -0
- package/dist/lib/popoverPlacement.d.ts +36 -0
- package/dist/lib/popoverPlacement.js +29 -0
- package/dist/lib/themedStyles.d.ts +1 -1
- package/dist/lib/useAnimatedValue.d.ts +15 -0
- package/dist/lib/useAnimatedValue.js +19 -0
- package/dist/lib/webHitTarget.d.ts +40 -0
- package/dist/lib/webHitTarget.js +37 -0
- package/dist/state/feedbackStore.d.ts +24 -0
- package/dist/state/feedbackStore.js +16 -0
- package/dist/state/globalUIStore.d.ts +27 -0
- package/dist/state/globalUIStore.js +23 -0
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +2 -0
- package/dist/state/initialScheme.d.ts +27 -0
- package/dist/state/initialScheme.js +29 -0
- package/dist/state/notify.d.ts +1 -1
- package/dist/state/themeColorScope.d.ts +1 -1
- package/dist/state/themeStore.d.ts +69 -16
- package/dist/state/themeStore.js +65 -38
- package/llms-full.md +59 -10
- package/llms.txt +10 -1
- package/package.json +79 -4
- package/dist/components/iconRegistry.generated.d.ts +0 -148
- 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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
3
|
-
import type { ThemeColors } from "../constants/colors";
|
|
4
|
-
import { type
|
|
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
|
-
*
|
|
7
|
-
* `icon-names.json`
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
74
|
+
export type IconProps = NamedIconProps | (IconBaseProps & {
|
|
46
75
|
name?: never;
|
|
47
76
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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:
|
|
52
|
-
};
|
|
53
|
-
|
|
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;
|
package/dist/components/Icon.js
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
24
|
-
*
|
|
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={
|
|
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
|
|
64
|
-
const
|
|
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)
|
|
67
|
-
//
|
|
68
|
-
if (!
|
|
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(
|
|
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 };
|