@elvora/react-native 1.0.0-rc.1

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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -0
  3. package/dist/index.cjs +5785 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +1253 -0
  6. package/dist/index.d.ts +1253 -0
  7. package/dist/index.js +5683 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +88 -0
  10. package/src/Accordion.tsx +11 -0
  11. package/src/Affix.tsx +20 -0
  12. package/src/Alert.tsx +102 -0
  13. package/src/Anchor.tsx +58 -0
  14. package/src/AutoComplete.tsx +122 -0
  15. package/src/Avatar.tsx +58 -0
  16. package/src/BackTop.tsx +71 -0
  17. package/src/Backdrop.tsx +32 -0
  18. package/src/Badge.tsx +87 -0
  19. package/src/Box.tsx +67 -0
  20. package/src/Breadcrumb.tsx +46 -0
  21. package/src/Button.test.tsx +39 -0
  22. package/src/Button.tsx +127 -0
  23. package/src/ButtonGroup.tsx +74 -0
  24. package/src/Calendar.tsx +165 -0
  25. package/src/Card.tsx +69 -0
  26. package/src/Carousel.tsx +99 -0
  27. package/src/Cascader.tsx +160 -0
  28. package/src/Checkbox.tsx +85 -0
  29. package/src/ChipInput.tsx +130 -0
  30. package/src/Collapse.tsx +120 -0
  31. package/src/ColorPicker.tsx +114 -0
  32. package/src/Container.tsx +22 -0
  33. package/src/DataGrid.tsx +170 -0
  34. package/src/DatePicker.tsx +195 -0
  35. package/src/DateRangePicker.tsx +249 -0
  36. package/src/Descriptions.tsx +98 -0
  37. package/src/Divider.tsx +32 -0
  38. package/src/Drawer.tsx +103 -0
  39. package/src/Dropdown.tsx +15 -0
  40. package/src/ElvoraProvider.tsx +31 -0
  41. package/src/Empty.tsx +34 -0
  42. package/src/FloatButton.tsx +78 -0
  43. package/src/Form.tsx +119 -0
  44. package/src/Grid.tsx +68 -0
  45. package/src/Icon.tsx +49 -0
  46. package/src/IconButton.tsx +28 -0
  47. package/src/Image.tsx +68 -0
  48. package/src/ImageList.tsx +58 -0
  49. package/src/Input.tsx +87 -0
  50. package/src/Label.tsx +46 -0
  51. package/src/List.tsx +82 -0
  52. package/src/Mentions.tsx +148 -0
  53. package/src/Menu.tsx +77 -0
  54. package/src/Modal.tsx +114 -0
  55. package/src/NumberInput.tsx +156 -0
  56. package/src/Pagination.tsx +148 -0
  57. package/src/PaginationVariants.tsx +64 -0
  58. package/src/Popover.tsx +74 -0
  59. package/src/ProForm.tsx +219 -0
  60. package/src/ProLayout.tsx +151 -0
  61. package/src/ProTable.tsx +91 -0
  62. package/src/Progress.tsx +92 -0
  63. package/src/QRCode.tsx +65 -0
  64. package/src/Radio.tsx +98 -0
  65. package/src/Rate.tsx +66 -0
  66. package/src/Result.tsx +64 -0
  67. package/src/Segmented.tsx +75 -0
  68. package/src/Select.tsx +146 -0
  69. package/src/Skeleton.tsx +49 -0
  70. package/src/Slider.tsx +122 -0
  71. package/src/SpeedDial.tsx +87 -0
  72. package/src/Spinner.tsx +29 -0
  73. package/src/Splitter.tsx +91 -0
  74. package/src/Stack.tsx +38 -0
  75. package/src/Statistic.tsx +60 -0
  76. package/src/Stepper.tsx +113 -0
  77. package/src/Steps.tsx +146 -0
  78. package/src/Switch.tsx +52 -0
  79. package/src/Table.tsx +178 -0
  80. package/src/Tabs.tsx +122 -0
  81. package/src/Tag.tsx +83 -0
  82. package/src/Textarea.tsx +22 -0
  83. package/src/TimePicker.tsx +187 -0
  84. package/src/Timeline.tsx +92 -0
  85. package/src/Toast.tsx +140 -0
  86. package/src/ToggleButton.tsx +66 -0
  87. package/src/Tooltip.tsx +56 -0
  88. package/src/Tour.tsx +118 -0
  89. package/src/Transfer.tsx +219 -0
  90. package/src/Tree.tsx +144 -0
  91. package/src/TreeSelect.tsx +221 -0
  92. package/src/Upload.tsx +109 -0
  93. package/src/Watermark.tsx +76 -0
  94. package/src/index.ts +221 -0
  95. package/src/smoke.test.tsx +113 -0
  96. package/src/test/react-native-stub.tsx +413 -0
  97. package/src/test/react-native-svg-stub.tsx +33 -0
  98. package/src/test/setup.ts +7 -0
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "@elvora/react-native",
3
+ "version": "1.0.0-rc.1",
4
+ "description": "Elvora UI components for React Native — same headless API as the web adapters, native rendering, WCAG 2.1 AA.",
5
+ "license": "MIT",
6
+ "author": "Elvora UI Contributors",
7
+ "homepage": "https://github.com/elvora-ui/elvora/tree/main/packages/react-native#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/elvora-ui/elvora.git",
11
+ "directory": "packages/react-native"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/elvora-ui/elvora/issues"
15
+ },
16
+ "keywords": [
17
+ "react-native",
18
+ "ios",
19
+ "android",
20
+ "ui",
21
+ "design-system",
22
+ "headless",
23
+ "accessibility",
24
+ "elvora"
25
+ ],
26
+ "type": "module",
27
+ "main": "./dist/index.cjs",
28
+ "module": "./dist/index.js",
29
+ "react-native": "./src/index.ts",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "react-native": "./src/index.ts",
34
+ "types": "./dist/index.d.ts",
35
+ "import": "./dist/index.js",
36
+ "require": "./dist/index.cjs"
37
+ }
38
+ },
39
+ "files": [
40
+ "dist",
41
+ "src"
42
+ ],
43
+ "sideEffects": false,
44
+ "peerDependencies": {
45
+ "react": ">=18.0.0",
46
+ "react-native": ">=0.74.0",
47
+ "react-native-svg": ">=15.0.0"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "react-native-svg": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "dependencies": {
55
+ "@elvora/icons": "1.0.0-rc.1",
56
+ "@elvora/core": "1.0.0-rc.1",
57
+ "@elvora/themes": "1.0.0-rc.1",
58
+ "@elvora/tokens": "1.0.0-rc.1"
59
+ },
60
+ "devDependencies": {
61
+ "@testing-library/jest-dom": "^6.6.3",
62
+ "@testing-library/react": "^16.1.0",
63
+ "@types/react": "^18.3.18",
64
+ "@types/react-dom": "^18.3.5",
65
+ "jsdom": "^25.0.1",
66
+ "react": "^18.3.1",
67
+ "react-dom": "^18.3.1",
68
+ "react-native": "0.76.5",
69
+ "react-native-svg": "^15.8.0",
70
+ "react-test-renderer": "^18.3.1",
71
+ "rimraf": "^6.0.1",
72
+ "tsup": "^8.3.5",
73
+ "typescript": "^5.7.2",
74
+ "vitest": "^2.1.8"
75
+ },
76
+ "publishConfig": {
77
+ "access": "public"
78
+ },
79
+ "scripts": {
80
+ "build": "tsup",
81
+ "dev": "tsup --watch",
82
+ "typecheck": "tsc --noEmit",
83
+ "lint": "echo 'no lint configured'",
84
+ "test": "vitest run",
85
+ "test:watch": "vitest",
86
+ "clean": "rimraf dist"
87
+ }
88
+ }
@@ -0,0 +1,11 @@
1
+ import { Collapse, type CollapseItem, type CollapseProps } from './Collapse';
2
+
3
+ export interface AccordionItem extends CollapseItem {}
4
+
5
+ export interface AccordionProps extends Omit<CollapseProps, 'accordion' | 'items'> {
6
+ items: AccordionItem[];
7
+ }
8
+
9
+ export function Accordion(props: AccordionProps) {
10
+ return <Collapse {...props} accordion />;
11
+ }
package/src/Affix.tsx ADDED
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+
4
+ export interface AffixProps extends ViewProps {
5
+ /** No-op on RN — use sticky list headers instead. Pass-through wrapper. */
6
+ offsetTop?: number;
7
+ offsetBottom?: number;
8
+ onChange?: (affixed: boolean) => void;
9
+ children?: ReactNode;
10
+ }
11
+
12
+ /**
13
+ * Affix — on React Native there is no global scroll viewport so this is a
14
+ * pass-through wrapper. For sticky list headers use `FlatList`'s
15
+ * `stickyHeaderIndices` or `ScrollView`'s `stickyHeaderIndices` directly.
16
+ */
17
+ export function Affix(props: AffixProps) {
18
+ const { children, offsetTop: _o, offsetBottom: _b, onChange: _c, ...rest } = props;
19
+ return <View {...rest}>{children}</View>;
20
+ }
package/src/Alert.tsx ADDED
@@ -0,0 +1,102 @@
1
+ import { forwardRef, type ReactNode } from 'react';
2
+ import { View, Text, type ViewProps, type ViewStyle } from 'react-native';
3
+ import type { ElvoraStatus, ElvoraTone } from '@elvora/core';
4
+ import { useTheme } from './ElvoraProvider';
5
+ import { Icon } from './Icon';
6
+ import { IconButton } from './IconButton';
7
+ import type { IconName } from '@elvora/icons';
8
+
9
+ export interface AlertProps extends ViewProps {
10
+ status?: ElvoraStatus;
11
+ tone?: ElvoraTone;
12
+ title?: ReactNode;
13
+ description?: ReactNode;
14
+ icon?: ReactNode;
15
+ onClose?: () => void;
16
+ closeLabel?: string;
17
+ }
18
+
19
+ const statusIntent = {
20
+ neutral: 'neutral',
21
+ info: 'info',
22
+ success: 'success',
23
+ warning: 'warning',
24
+ error: 'danger',
25
+ } as const;
26
+
27
+ const defaultIcon: Record<ElvoraStatus, IconName> = {
28
+ neutral: 'info',
29
+ info: 'info',
30
+ success: 'checkCircle',
31
+ warning: 'alertCircle',
32
+ error: 'x',
33
+ };
34
+
35
+ /** Alert — banner-style feedback for RN. */
36
+ export const Alert = forwardRef<View, AlertProps>(function Alert(props, ref) {
37
+ const {
38
+ status = 'info',
39
+ tone = 'subtle',
40
+ title,
41
+ description,
42
+ icon,
43
+ onClose,
44
+ closeLabel = 'Dismiss',
45
+ children,
46
+ style,
47
+ ...rest
48
+ } = props;
49
+ const theme = useTheme();
50
+ const intent = theme.colors.intent[statusIntent[status]];
51
+ let bg = intent.subtle;
52
+ let fg = intent.fg;
53
+ let borderColor = 'transparent';
54
+ if (tone === 'solid') {
55
+ bg = intent.solid;
56
+ fg = intent.solidFg;
57
+ } else if (tone === 'outline') {
58
+ bg = 'transparent';
59
+ fg = intent.fg;
60
+ borderColor = intent.border;
61
+ }
62
+ const base: ViewStyle = {
63
+ flexDirection: 'row',
64
+ alignItems: 'flex-start',
65
+ gap: 12,
66
+ paddingHorizontal: 16,
67
+ paddingVertical: 12,
68
+ borderRadius: theme.radii.md,
69
+ backgroundColor: bg,
70
+ borderWidth: tone === 'outline' ? 1 : 0,
71
+ borderColor,
72
+ };
73
+ return (
74
+ <View
75
+ ref={ref}
76
+ accessibilityRole={status === 'error' || status === 'warning' ? 'alert' : undefined}
77
+ accessibilityLiveRegion="polite"
78
+ style={[base, style]}
79
+ {...rest}
80
+ >
81
+ <View style={{ marginTop: 2 }}>{icon ?? <Icon name={defaultIcon[status]} size={18} color={fg} />}</View>
82
+ <View style={{ flex: 1, minWidth: 0 }}>
83
+ {title ? (
84
+ <Text style={{ color: fg, fontWeight: '600', fontSize: 14, marginBottom: description || children ? 2 : 0 }}>
85
+ {title}
86
+ </Text>
87
+ ) : null}
88
+ {description ? <Text style={{ color: fg, fontSize: 13, lineHeight: 18 }}>{description}</Text> : null}
89
+ {children}
90
+ </View>
91
+ {onClose ? (
92
+ <IconButton
93
+ accessibilityLabel={closeLabel}
94
+ size="xs"
95
+ variant="ghost"
96
+ onPress={onClose}
97
+ icon={<Icon name="x" size={14} color={fg} />}
98
+ />
99
+ ) : null}
100
+ </View>
101
+ );
102
+ });
package/src/Anchor.tsx ADDED
@@ -0,0 +1,58 @@
1
+ import { type ReactNode, type RefObject } from 'react';
2
+ import { Pressable, Text, View, type ViewProps, type ScrollView } from 'react-native';
3
+ import { useTheme } from './ElvoraProvider';
4
+
5
+ export interface AnchorLink {
6
+ /** Vertical scroll offset (px) for this section. */
7
+ offset: number;
8
+ label: ReactNode;
9
+ }
10
+
11
+ export interface AnchorProps extends ViewProps {
12
+ links: AnchorLink[];
13
+ /** Ref to the parent ScrollView used to scroll to each section. */
14
+ scrollRef: RefObject<ScrollView>;
15
+ activeIndex?: number;
16
+ smooth?: boolean;
17
+ }
18
+
19
+ /**
20
+ * Anchor — section jumper for RN. Caller passes a ref to their parent
21
+ * `<ScrollView>` and pre-computed offsets for each section. Active highlighting
22
+ * is opt-in via `activeIndex`.
23
+ */
24
+ export function Anchor(props: AnchorProps) {
25
+ const { links, scrollRef, activeIndex, smooth = true, style, ...rest } = props;
26
+ const theme = useTheme();
27
+ return (
28
+ <View accessibilityRole="menu" style={style} {...rest}>
29
+ {links.map((link, idx) => {
30
+ const isActive = activeIndex === idx;
31
+ return (
32
+ <Pressable
33
+ key={idx}
34
+ accessibilityRole="link"
35
+ accessibilityState={{ selected: isActive }}
36
+ onPress={() => scrollRef.current?.scrollTo({ y: link.offset, animated: smooth })}
37
+ style={{
38
+ paddingVertical: 6,
39
+ paddingLeft: 12,
40
+ borderLeftWidth: 2,
41
+ borderLeftColor: isActive ? theme.colors.intent.primary.solid : theme.colors.border,
42
+ backgroundColor: isActive ? theme.colors.intent.primary.subtle : 'transparent',
43
+ }}
44
+ >
45
+ <Text
46
+ style={{
47
+ fontSize: 13,
48
+ color: isActive ? theme.colors.intent.primary.fg : theme.colors.fgMuted,
49
+ }}
50
+ >
51
+ {link.label}
52
+ </Text>
53
+ </Pressable>
54
+ );
55
+ })}
56
+ </View>
57
+ );
58
+ }
@@ -0,0 +1,122 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { FlatList, Pressable, TextInput, View, Text, type ViewProps } from 'react-native';
3
+ import { useTheme } from './ElvoraProvider';
4
+
5
+ export interface AutoCompleteOption {
6
+ value: string;
7
+ label?: string;
8
+ }
9
+
10
+ export interface AutoCompleteProps extends Omit<ViewProps, 'children'> {
11
+ value?: string;
12
+ defaultValue?: string;
13
+ onChangeValue?: (value: string) => void;
14
+ options?: AutoCompleteOption[];
15
+ loadOptions?: (query: string) => Promise<AutoCompleteOption[]>;
16
+ onSelectOption?: (opt: AutoCompleteOption) => void;
17
+ placeholder?: string;
18
+ isDisabled?: boolean;
19
+ }
20
+
21
+ export function AutoComplete(props: AutoCompleteProps) {
22
+ const {
23
+ value: valueProp,
24
+ defaultValue = '',
25
+ onChangeValue,
26
+ options = [],
27
+ loadOptions,
28
+ onSelectOption,
29
+ placeholder,
30
+ isDisabled,
31
+ style,
32
+ ...rest
33
+ } = props;
34
+ const theme = useTheme();
35
+ const [internal, setInternal] = useState(defaultValue);
36
+ const query = valueProp ?? internal;
37
+ const [isOpen, setOpen] = useState(false);
38
+ const [asyncOptions, setAsyncOptions] = useState<AutoCompleteOption[]>([]);
39
+
40
+ useEffect(() => {
41
+ if (!loadOptions) return;
42
+ let cancelled = false;
43
+ loadOptions(query).then((opts) => {
44
+ if (!cancelled) setAsyncOptions(opts);
45
+ });
46
+ return () => {
47
+ cancelled = true;
48
+ };
49
+ }, [query, loadOptions]);
50
+
51
+ const filtered = useMemo(() => {
52
+ const source = loadOptions ? asyncOptions : options;
53
+ if (!query) return source;
54
+ return source.filter((o) => o.value.toLowerCase().includes(query.toLowerCase()));
55
+ }, [asyncOptions, options, loadOptions, query]);
56
+
57
+ const setQuery = (next: string) => {
58
+ if (valueProp === undefined) setInternal(next);
59
+ onChangeValue?.(next);
60
+ };
61
+
62
+ return (
63
+ <View style={style} {...rest}>
64
+ <TextInput
65
+ value={query}
66
+ placeholder={placeholder}
67
+ placeholderTextColor={theme.colors.fgMuted}
68
+ editable={!isDisabled}
69
+ onChangeText={(t) => {
70
+ setQuery(t);
71
+ setOpen(true);
72
+ }}
73
+ onFocus={() => setOpen(true)}
74
+ onBlur={() => setTimeout(() => setOpen(false), 150)}
75
+ style={{
76
+ paddingHorizontal: 12,
77
+ paddingVertical: 8,
78
+ borderWidth: 1,
79
+ borderColor: theme.colors.border,
80
+ borderRadius: Number(theme.radii.md),
81
+ backgroundColor: isDisabled ? theme.colors.surface : theme.colors.surfaceElevated,
82
+ color: theme.colors.fg,
83
+ fontSize: 14,
84
+ }}
85
+ />
86
+ {isOpen && filtered.length > 0 ? (
87
+ <View
88
+ style={{
89
+ marginTop: 4,
90
+ borderRadius: Number(theme.radii.md),
91
+ borderWidth: 1,
92
+ borderColor: theme.colors.border,
93
+ backgroundColor: theme.colors.surfaceElevated,
94
+ maxHeight: 200,
95
+ }}
96
+ >
97
+ <FlatList
98
+ data={filtered}
99
+ keyExtractor={(o) => o.value}
100
+ keyboardShouldPersistTaps="handled"
101
+ renderItem={({ item }) => (
102
+ <Pressable
103
+ onPress={() => {
104
+ setQuery(item.value);
105
+ onSelectOption?.(item);
106
+ setOpen(false);
107
+ }}
108
+ style={({ pressed }) => ({
109
+ paddingHorizontal: 12,
110
+ paddingVertical: 10,
111
+ backgroundColor: pressed ? theme.colors.intent.neutral.subtle : 'transparent',
112
+ })}
113
+ >
114
+ <Text style={{ color: theme.colors.fg }}>{item.label ?? item.value}</Text>
115
+ </Pressable>
116
+ )}
117
+ />
118
+ </View>
119
+ ) : null}
120
+ </View>
121
+ );
122
+ }
package/src/Avatar.tsx ADDED
@@ -0,0 +1,58 @@
1
+ import { forwardRef, useState, type ReactNode } from 'react';
2
+ import { Image, Text, View, type ImageSourcePropType, type StyleProp, type ViewStyle } from 'react-native';
3
+ import type { ElvoraSize } from '@elvora/core';
4
+ import { useTheme } from './ElvoraProvider';
5
+
6
+ export interface AvatarProps {
7
+ src?: string | ImageSourcePropType;
8
+ alt?: string;
9
+ name?: string;
10
+ size?: ElvoraSize;
11
+ shape?: 'circle' | 'square';
12
+ fallback?: ReactNode;
13
+ style?: StyleProp<ViewStyle>;
14
+ }
15
+
16
+ const sizeMap: Record<ElvoraSize, number> = { xs: 20, sm: 28, md: 36, lg: 48, xl: 64 };
17
+
18
+ export const Avatar = forwardRef<View, AvatarProps>(function Avatar(props, ref) {
19
+ const { src, alt, name, size = 'md', shape = 'circle', fallback, style } = props;
20
+ const theme = useTheme();
21
+ const [imgFailed, setImgFailed] = useState(false);
22
+ const px = sizeMap[size];
23
+ const initials = (name ?? '')
24
+ .split(' ')
25
+ .map((p) => p[0])
26
+ .filter(Boolean)
27
+ .slice(0, 2)
28
+ .join('')
29
+ .toUpperCase();
30
+
31
+ const container: ViewStyle = {
32
+ width: px,
33
+ height: px,
34
+ borderRadius: shape === 'circle' ? px / 2 : Number(theme.radii.md),
35
+ backgroundColor: theme.colors.intent.neutral.subtle,
36
+ alignItems: 'center',
37
+ justifyContent: 'center',
38
+ overflow: 'hidden',
39
+ };
40
+
41
+ const showImage = src && !imgFailed;
42
+ const source: ImageSourcePropType | undefined =
43
+ typeof src === 'string' ? { uri: src } : (src as ImageSourcePropType | undefined);
44
+
45
+ return (
46
+ <View ref={ref} accessibilityLabel={alt ?? name} style={[container, style]}>
47
+ {showImage && source ? (
48
+ <Image source={source} onError={() => setImgFailed(true)} style={{ width: '100%', height: '100%' }} />
49
+ ) : initials ? (
50
+ <Text style={{ color: theme.colors.intent.neutral.fg, fontWeight: '600', fontSize: Math.max(10, Math.floor(px * 0.4)) }}>
51
+ {initials}
52
+ </Text>
53
+ ) : (
54
+ fallback ?? <Text style={{ color: theme.colors.intent.neutral.fg }}>?</Text>
55
+ )}
56
+ </View>
57
+ );
58
+ });
@@ -0,0 +1,71 @@
1
+ import { useEffect, useState, type RefObject } from 'react';
2
+ import { Animated, Pressable, type ScrollView } from 'react-native';
3
+ import { useTheme } from './ElvoraProvider';
4
+ import { Icon } from './Icon';
5
+
6
+ export interface BackTopProps {
7
+ /** Ref to the ScrollView this button scrolls to top. */
8
+ scrollRef: RefObject<ScrollView>;
9
+ /** Current Y offset of the parent ScrollView (caller wires up `onScroll`). */
10
+ scrollY: number;
11
+ /** Show after the user has scrolled past this offset (default 400). */
12
+ visibilityHeight?: number;
13
+ bottom?: number;
14
+ right?: number;
15
+ label?: string;
16
+ }
17
+
18
+ /**
19
+ * BackTop — floating "scroll to top" button for RN. Caller passes a ref to the
20
+ * parent ScrollView plus the current scroll offset (e.g. from `onScroll` event).
21
+ */
22
+ export function BackTop(props: BackTopProps) {
23
+ const { scrollRef, scrollY, visibilityHeight = 400, bottom = 24, right = 24, label = 'Back to top' } = props;
24
+ const theme = useTheme();
25
+ const [visible, setVisible] = useState(false);
26
+ const opacity = useState(new Animated.Value(0))[0];
27
+
28
+ useEffect(() => {
29
+ setVisible(scrollY > visibilityHeight);
30
+ }, [scrollY, visibilityHeight]);
31
+
32
+ useEffect(() => {
33
+ Animated.timing(opacity, { toValue: visible ? 1 : 0, duration: 200, useNativeDriver: true }).start();
34
+ }, [visible, opacity]);
35
+
36
+ if (!visible) return null;
37
+ return (
38
+ <Animated.View
39
+ pointerEvents={visible ? 'auto' : 'none'}
40
+ style={{
41
+ position: 'absolute',
42
+ bottom,
43
+ right,
44
+ opacity,
45
+ }}
46
+ >
47
+ <Pressable
48
+ accessibilityRole="button"
49
+ accessibilityLabel={label}
50
+ onPress={() => scrollRef.current?.scrollTo({ y: 0, animated: true })}
51
+ style={{
52
+ width: 44,
53
+ height: 44,
54
+ borderRadius: 22,
55
+ borderWidth: 1,
56
+ borderColor: theme.colors.border,
57
+ backgroundColor: theme.colors.surfaceElevated,
58
+ alignItems: 'center',
59
+ justifyContent: 'center',
60
+ shadowColor: '#000',
61
+ shadowOpacity: 0.15,
62
+ shadowRadius: 8,
63
+ shadowOffset: { width: 0, height: 4 },
64
+ elevation: 4,
65
+ }}
66
+ >
67
+ <Icon name="chevronUp" size={18} color={theme.colors.fg} />
68
+ </Pressable>
69
+ </Animated.View>
70
+ );
71
+ }
@@ -0,0 +1,32 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Modal, Pressable, View } from 'react-native';
3
+
4
+ export interface BackdropProps {
5
+ open: boolean;
6
+ onClose?: () => void;
7
+ tint?: string;
8
+ invisible?: boolean;
9
+ children?: ReactNode;
10
+ }
11
+
12
+ export function Backdrop(props: BackdropProps) {
13
+ const { open, onClose, tint, invisible, children } = props;
14
+ if (!open) return null;
15
+ return (
16
+ <Modal transparent visible animationType="fade" onRequestClose={onClose}>
17
+ <Pressable
18
+ accessibilityRole="button"
19
+ accessibilityLabel="Close"
20
+ onPress={onClose}
21
+ style={{
22
+ flex: 1,
23
+ backgroundColor: invisible ? 'transparent' : tint ?? 'rgba(0,0,0,0.55)',
24
+ alignItems: 'center',
25
+ justifyContent: 'center',
26
+ }}
27
+ >
28
+ <View>{children}</View>
29
+ </Pressable>
30
+ </Modal>
31
+ );
32
+ }
package/src/Badge.tsx ADDED
@@ -0,0 +1,87 @@
1
+ import { forwardRef, type ReactNode } from 'react';
2
+ import { Text, View, type StyleProp, type ViewStyle } from 'react-native';
3
+ import type { ElvoraSize, ElvoraStatus, ElvoraTone } from '@elvora/core';
4
+ import { useTheme } from './ElvoraProvider';
5
+
6
+ export interface BadgeProps {
7
+ status?: ElvoraStatus;
8
+ size?: Exclude<ElvoraSize, 'xl'>;
9
+ tone?: ElvoraTone;
10
+ isDot?: boolean;
11
+ children?: ReactNode;
12
+ style?: StyleProp<ViewStyle>;
13
+ }
14
+
15
+ const sizeMap = {
16
+ xs: { padX: 4, padY: 1, font: 10, dotSize: 6 },
17
+ sm: { padX: 6, padY: 2, font: 11, dotSize: 8 },
18
+ md: { padX: 8, padY: 2, font: 12, dotSize: 10 },
19
+ lg: { padX: 10, padY: 4, font: 13, dotSize: 12 },
20
+ };
21
+
22
+ const statusIntent = {
23
+ neutral: 'neutral',
24
+ info: 'info',
25
+ success: 'success',
26
+ warning: 'warning',
27
+ error: 'danger',
28
+ } as const;
29
+
30
+ export const Badge = forwardRef<View, BadgeProps>(function Badge(props, ref) {
31
+ const { status = 'neutral', size = 'sm', tone = 'subtle', isDot = false, children, style } = props;
32
+ const theme = useTheme();
33
+ const intent = theme.colors.intent[statusIntent[status]];
34
+ const dims = sizeMap[size];
35
+
36
+ let bg = intent.subtle;
37
+ let fg = intent.fg;
38
+ let border = 'transparent';
39
+ if (tone === 'solid') {
40
+ bg = intent.solid;
41
+ fg = intent.solidFg;
42
+ } else if (tone === 'outline') {
43
+ bg = 'transparent';
44
+ fg = intent.fg;
45
+ border = intent.border;
46
+ }
47
+
48
+ if (isDot) {
49
+ return (
50
+ <View
51
+ ref={ref}
52
+ accessibilityRole="image"
53
+ style={[
54
+ {
55
+ width: dims.dotSize,
56
+ height: dims.dotSize,
57
+ borderRadius: dims.dotSize / 2,
58
+ backgroundColor: bg,
59
+ borderWidth: tone === 'outline' ? 1 : 0,
60
+ borderColor: border,
61
+ },
62
+ style,
63
+ ]}
64
+ />
65
+ );
66
+ }
67
+
68
+ return (
69
+ <View
70
+ ref={ref}
71
+ style={[
72
+ {
73
+ paddingHorizontal: dims.padX,
74
+ paddingVertical: dims.padY,
75
+ borderRadius: Number(theme.radii.md),
76
+ backgroundColor: bg,
77
+ borderWidth: tone === 'outline' ? 1 : 0,
78
+ borderColor: border,
79
+ alignSelf: 'flex-start',
80
+ },
81
+ style,
82
+ ]}
83
+ >
84
+ <Text style={{ color: fg, fontSize: dims.font, fontWeight: '500' }}>{children}</Text>
85
+ </View>
86
+ );
87
+ });