@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
@@ -0,0 +1,160 @@
1
+ import { useMemo, useState, type ReactNode } from 'react';
2
+ import { Modal, Pressable, ScrollView, View, Text, type ViewProps } from 'react-native';
3
+ import { useTheme } from './ElvoraProvider';
4
+
5
+ export interface CascaderOption {
6
+ value: string;
7
+ label: ReactNode;
8
+ children?: CascaderOption[];
9
+ isDisabled?: boolean;
10
+ }
11
+
12
+ export interface CascaderProps extends Omit<ViewProps, 'onChange'> {
13
+ options: CascaderOption[];
14
+ value?: string[];
15
+ defaultValue?: string[];
16
+ onChange?: (value: string[], path: CascaderOption[]) => void;
17
+ placeholder?: string;
18
+ displaySeparator?: string;
19
+ isDisabled?: boolean;
20
+ }
21
+
22
+ function findPath(options: CascaderOption[], value: string[]): CascaderOption[] {
23
+ const path: CascaderOption[] = [];
24
+ let level = options;
25
+ for (const v of value) {
26
+ const found = level.find((o) => o.value === v);
27
+ if (!found) break;
28
+ path.push(found);
29
+ level = found.children ?? [];
30
+ }
31
+ return path;
32
+ }
33
+
34
+ export function Cascader(props: CascaderProps) {
35
+ const {
36
+ options,
37
+ value: valueProp,
38
+ defaultValue,
39
+ onChange,
40
+ placeholder = 'Please select',
41
+ displaySeparator = ' / ',
42
+ isDisabled,
43
+ style,
44
+ ...rest
45
+ } = props;
46
+ const theme = useTheme();
47
+ const [internal, setInternal] = useState<string[]>(defaultValue ?? []);
48
+ const value = valueProp ?? internal;
49
+ const [isOpen, setOpen] = useState(false);
50
+ const [activePath, setActivePath] = useState<string[]>(value);
51
+
52
+ const columns = useMemo(() => {
53
+ const cols: CascaderOption[][] = [options];
54
+ let level = options;
55
+ for (const v of activePath) {
56
+ const found = level.find((o) => o.value === v);
57
+ if (!found || !found.children?.length) break;
58
+ cols.push(found.children);
59
+ level = found.children;
60
+ }
61
+ return cols;
62
+ }, [options, activePath]);
63
+
64
+ const onSelectAtLevel = (level: number, opt: CascaderOption) => {
65
+ if (opt.isDisabled) return;
66
+ const nextPath = [...activePath.slice(0, level), opt.value];
67
+ setActivePath(nextPath);
68
+ if (!opt.children?.length) {
69
+ const fullPath = findPath(options, nextPath);
70
+ if (valueProp === undefined) setInternal(nextPath);
71
+ onChange?.(nextPath, fullPath);
72
+ setOpen(false);
73
+ }
74
+ };
75
+
76
+ const displayed = value.length > 0 ? findPath(options, value).map((o) => o.label).join(displaySeparator) : '';
77
+
78
+ return (
79
+ <View style={style} {...rest}>
80
+ <Pressable
81
+ accessibilityRole="button"
82
+ disabled={isDisabled}
83
+ onPress={() => {
84
+ setActivePath(value);
85
+ setOpen(true);
86
+ }}
87
+ style={({ pressed }) => ({
88
+ paddingHorizontal: 12,
89
+ paddingVertical: 8,
90
+ borderWidth: 1,
91
+ borderColor: theme.colors.border,
92
+ borderRadius: Number(theme.radii.md),
93
+ backgroundColor: pressed ? theme.colors.intent.neutral.subtle : theme.colors.surfaceElevated,
94
+ opacity: isDisabled ? 0.6 : 1,
95
+ })}
96
+ >
97
+ <Text style={{ color: displayed ? theme.colors.fg : theme.colors.fgMuted, fontSize: 14 }}>
98
+ {displayed || placeholder}
99
+ </Text>
100
+ </Pressable>
101
+ <Modal transparent visible={isOpen} animationType="fade" onRequestClose={() => setOpen(false)}>
102
+ <Pressable
103
+ style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.4)' }}
104
+ onPress={() => setOpen(false)}
105
+ >
106
+ <Pressable
107
+ onPress={() => undefined}
108
+ style={{
109
+ backgroundColor: theme.colors.surfaceElevated,
110
+ borderRadius: Number(theme.radii.md),
111
+ flexDirection: 'row',
112
+ maxWidth: '95%',
113
+ maxHeight: 360,
114
+ }}
115
+ >
116
+ {columns.map((col, i) => (
117
+ <ScrollView
118
+ key={i}
119
+ style={{
120
+ width: 160,
121
+ borderRightWidth: i < columns.length - 1 ? 1 : 0,
122
+ borderRightColor: theme.colors.border,
123
+ }}
124
+ >
125
+ {col.map((opt) => {
126
+ const isActive = activePath[i] === opt.value;
127
+ const hasChildren = !!opt.children?.length;
128
+ return (
129
+ <Pressable
130
+ key={opt.value}
131
+ disabled={opt.isDisabled}
132
+ onPress={() => onSelectAtLevel(i, opt)}
133
+ style={({ pressed }) => ({
134
+ flexDirection: 'row',
135
+ alignItems: 'center',
136
+ justifyContent: 'space-between',
137
+ paddingHorizontal: 12,
138
+ paddingVertical: 8,
139
+ backgroundColor: isActive
140
+ ? theme.colors.intent.primary.subtle
141
+ : pressed
142
+ ? theme.colors.intent.neutral.subtle
143
+ : 'transparent',
144
+ })}
145
+ >
146
+ <Text style={{ color: opt.isDisabled ? theme.colors.fgSubtle : theme.colors.fg, fontSize: 13 }}>
147
+ {opt.label}
148
+ </Text>
149
+ {hasChildren ? <Text style={{ color: theme.colors.fgMuted }}>›</Text> : null}
150
+ </Pressable>
151
+ );
152
+ })}
153
+ </ScrollView>
154
+ ))}
155
+ </Pressable>
156
+ </Pressable>
157
+ </Modal>
158
+ </View>
159
+ );
160
+ }
@@ -0,0 +1,85 @@
1
+ import { forwardRef, type ReactNode } from 'react';
2
+ import { Pressable, Text, View, type PressableProps, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { useControllableState } from '@elvora/core/react';
4
+ import { useTheme } from './ElvoraProvider';
5
+ import { Icon } from './Icon';
6
+
7
+ export interface CheckboxProps extends Omit<PressableProps, 'onPress' | 'children'> {
8
+ size?: 'sm' | 'md' | 'lg';
9
+ isDisabled?: boolean;
10
+ isInvalid?: boolean;
11
+ isChecked?: boolean;
12
+ defaultChecked?: boolean;
13
+ isIndeterminate?: boolean;
14
+ onChange?: (checked: boolean) => void;
15
+ children?: ReactNode;
16
+ style?: StyleProp<ViewStyle>;
17
+ }
18
+
19
+ const sizeMap = {
20
+ sm: { box: 16, icon: 12, font: 13 },
21
+ md: { box: 20, icon: 14, font: 14 },
22
+ lg: { box: 24, icon: 16, font: 16 },
23
+ };
24
+
25
+ export const Checkbox = forwardRef<View, CheckboxProps>(function Checkbox(props, ref) {
26
+ const {
27
+ size = 'md',
28
+ isDisabled,
29
+ isInvalid,
30
+ isChecked,
31
+ defaultChecked = false,
32
+ isIndeterminate = false,
33
+ onChange,
34
+ children,
35
+ style,
36
+ ...rest
37
+ } = props;
38
+ const theme = useTheme();
39
+ const [checked, setChecked] = useControllableState({
40
+ value: isChecked,
41
+ defaultValue: defaultChecked,
42
+ onChange,
43
+ });
44
+ const dims = sizeMap[size];
45
+ const isActive = Boolean(checked) || isIndeterminate;
46
+
47
+ return (
48
+ <Pressable
49
+ ref={ref}
50
+ accessibilityRole="checkbox"
51
+ accessibilityState={{ checked: isIndeterminate ? 'mixed' : Boolean(checked), disabled: isDisabled }}
52
+ disabled={isDisabled}
53
+ onPress={() => setChecked(!checked)}
54
+ style={[
55
+ { flexDirection: 'row', alignItems: 'center', gap: 8, opacity: isDisabled ? 0.5 : 1 },
56
+ style,
57
+ ]}
58
+ {...rest}
59
+ >
60
+ <View
61
+ style={{
62
+ width: dims.box,
63
+ height: dims.box,
64
+ borderWidth: 1.5,
65
+ borderColor: isInvalid
66
+ ? theme.colors.intent.danger.border
67
+ : isActive
68
+ ? theme.colors.intent.primary.solid
69
+ : theme.colors.borderStrong,
70
+ borderRadius: 4,
71
+ backgroundColor: isActive ? theme.colors.intent.primary.solid : theme.colors.surfaceElevated,
72
+ alignItems: 'center',
73
+ justifyContent: 'center',
74
+ }}
75
+ >
76
+ {isIndeterminate ? (
77
+ <Icon name="minus" size={dims.icon} color={theme.colors.intent.primary.solidFg} />
78
+ ) : checked ? (
79
+ <Icon name="check" size={dims.icon} color={theme.colors.intent.primary.solidFg} />
80
+ ) : null}
81
+ </View>
82
+ {children ? <Text style={{ color: theme.colors.fg, fontSize: dims.font }}>{children}</Text> : null}
83
+ </Pressable>
84
+ );
85
+ });
@@ -0,0 +1,130 @@
1
+ import { useCallback, useRef, useState } from 'react';
2
+ import { TextInput, View, type NativeSyntheticEvent, type TextInputKeyPressEventData, type ViewStyle } from 'react-native';
3
+ import { useTheme } from './ElvoraProvider';
4
+ import { Tag } from './Tag';
5
+
6
+ export interface ChipInputProps {
7
+ value?: string[];
8
+ defaultValue?: string[];
9
+ onChange?: (chips: string[]) => void;
10
+ placeholder?: string;
11
+ validate?: (value: string) => boolean;
12
+ unique?: boolean;
13
+ isDisabled?: boolean;
14
+ maxChips?: number;
15
+ style?: ViewStyle;
16
+ accessibilityLabel?: string;
17
+ }
18
+
19
+ export function ChipInput(props: ChipInputProps) {
20
+ const {
21
+ value: valueProp,
22
+ defaultValue = [],
23
+ onChange,
24
+ placeholder,
25
+ validate,
26
+ unique = true,
27
+ isDisabled,
28
+ maxChips,
29
+ style,
30
+ accessibilityLabel,
31
+ } = props;
32
+ const theme = useTheme();
33
+ const [internal, setInternal] = useState<string[]>(defaultValue);
34
+ const value = valueProp ?? internal;
35
+ const [draft, setDraft] = useState('');
36
+ const inputRef = useRef<TextInput | null>(null);
37
+
38
+ const setChips = useCallback(
39
+ (next: string[]) => {
40
+ if (valueProp === undefined) setInternal(next);
41
+ onChange?.(next);
42
+ },
43
+ [valueProp, onChange],
44
+ );
45
+
46
+ const addChip = useCallback(
47
+ (raw: string) => {
48
+ const trimmed = raw.trim();
49
+ if (!trimmed) return;
50
+ if (validate && !validate(trimmed)) return;
51
+ if (maxChips !== undefined && value.length >= maxChips) return;
52
+ if (unique && value.includes(trimmed)) {
53
+ setDraft('');
54
+ return;
55
+ }
56
+ setChips([...value, trimmed]);
57
+ setDraft('');
58
+ },
59
+ [validate, maxChips, unique, value, setChips],
60
+ );
61
+
62
+ const removeChip = useCallback(
63
+ (idx: number) => {
64
+ const next = value.slice();
65
+ next.splice(idx, 1);
66
+ setChips(next);
67
+ },
68
+ [value, setChips],
69
+ );
70
+
71
+ const handleKey = (e: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
72
+ const key = e.nativeEvent.key;
73
+ if (key === 'Backspace' && !draft && value.length > 0) {
74
+ removeChip(value.length - 1);
75
+ }
76
+ };
77
+
78
+ return (
79
+ <View
80
+ style={[
81
+ {
82
+ flexDirection: 'row',
83
+ flexWrap: 'wrap',
84
+ alignItems: 'center',
85
+ gap: 6,
86
+ minHeight: 40,
87
+ paddingHorizontal: 8,
88
+ paddingVertical: 4,
89
+ borderWidth: 1,
90
+ borderColor: theme.colors.border,
91
+ borderRadius: 6,
92
+ backgroundColor: theme.colors.surfaceElevated,
93
+ opacity: isDisabled ? 0.6 : 1,
94
+ },
95
+ style,
96
+ ]}
97
+ >
98
+ {value.map((chip, idx) => (
99
+ <Tag key={`${chip}-${idx}`} onClose={isDisabled ? undefined : () => removeChip(idx)}>
100
+ {chip}
101
+ </Tag>
102
+ ))}
103
+ <TextInput
104
+ ref={inputRef}
105
+ accessibilityLabel={accessibilityLabel ?? 'Add tag'}
106
+ editable={!isDisabled}
107
+ value={draft}
108
+ onChangeText={(v) => {
109
+ if (v.endsWith(',')) {
110
+ addChip(v.slice(0, -1));
111
+ } else {
112
+ setDraft(v);
113
+ }
114
+ }}
115
+ onSubmitEditing={() => addChip(draft)}
116
+ onKeyPress={handleKey}
117
+ onBlur={() => addChip(draft)}
118
+ placeholder={value.length === 0 ? placeholder : undefined}
119
+ placeholderTextColor={theme.colors.fgSubtle}
120
+ style={{
121
+ flex: 1,
122
+ minWidth: 80,
123
+ color: theme.colors.fg,
124
+ fontSize: 14,
125
+ paddingVertical: 4,
126
+ }}
127
+ />
128
+ </View>
129
+ );
130
+ }
@@ -0,0 +1,120 @@
1
+ import { useCallback, useState, type ReactNode } from 'react';
2
+ import { Pressable, View, Text, type ViewProps } from 'react-native';
3
+ import { useTheme } from './ElvoraProvider';
4
+
5
+ export interface CollapseItem {
6
+ key: string;
7
+ header: ReactNode;
8
+ children: ReactNode;
9
+ extra?: ReactNode;
10
+ isDisabled?: boolean;
11
+ }
12
+
13
+ export interface CollapseProps extends Omit<ViewProps, 'children' | 'onChange'> {
14
+ items: CollapseItem[];
15
+ activeKeys?: string[];
16
+ defaultActiveKeys?: string[];
17
+ onChange?: (keys: string[]) => void;
18
+ accordion?: boolean;
19
+ bordered?: boolean;
20
+ size?: 'sm' | 'md' | 'lg';
21
+ }
22
+
23
+ export function Collapse(props: CollapseProps) {
24
+ const {
25
+ items,
26
+ activeKeys: activeProp,
27
+ defaultActiveKeys,
28
+ onChange,
29
+ accordion,
30
+ bordered = true,
31
+ size = 'md',
32
+ style,
33
+ ...rest
34
+ } = props;
35
+ const theme = useTheme();
36
+ const [internal, setInternal] = useState<string[]>(defaultActiveKeys ?? []);
37
+ const active = new Set(activeProp ?? internal);
38
+
39
+ const padding = size === 'sm' ? 10 : size === 'lg' ? 18 : 14;
40
+
41
+ const togglePanel = useCallback(
42
+ (key: string) => {
43
+ const next = new Set(active);
44
+ if (next.has(key)) next.delete(key);
45
+ else if (accordion) {
46
+ next.clear();
47
+ next.add(key);
48
+ } else next.add(key);
49
+ const list = Array.from(next);
50
+ if (activeProp === undefined) setInternal(list);
51
+ onChange?.(list);
52
+ },
53
+ [active, accordion, activeProp, onChange],
54
+ );
55
+
56
+ return (
57
+ <View
58
+ style={[
59
+ {
60
+ borderWidth: bordered ? 1 : 0,
61
+ borderColor: theme.colors.border,
62
+ borderRadius: 8,
63
+ backgroundColor: theme.colors.surfaceElevated,
64
+ overflow: 'hidden',
65
+ },
66
+ style,
67
+ ]}
68
+ {...rest}
69
+ >
70
+ {items.map((item, idx) => {
71
+ const isOpen = active.has(item.key);
72
+ const isLast = idx === items.length - 1;
73
+ return (
74
+ <View key={item.key}>
75
+ <Pressable
76
+ accessibilityRole="button"
77
+ accessibilityState={{ expanded: isOpen, disabled: item.isDisabled }}
78
+ disabled={item.isDisabled}
79
+ onPress={() => togglePanel(item.key)}
80
+ style={{
81
+ flexDirection: 'row',
82
+ alignItems: 'center',
83
+ padding,
84
+ borderBottomWidth: !isLast || isOpen ? 1 : 0,
85
+ borderBottomColor: theme.colors.border,
86
+ opacity: item.isDisabled ? 0.6 : 1,
87
+ }}
88
+ >
89
+ <Text style={{ width: 14, color: theme.colors.fg }}>{isOpen ? '▾' : '▸'}</Text>
90
+ <Text style={{ flex: 1, color: theme.colors.fg, fontWeight: '500' }}>
91
+ {typeof item.header === 'string' ? item.header : null}
92
+ </Text>
93
+ {item.extra ? (
94
+ <Text style={{ color: theme.colors.fgSubtle }}>
95
+ {typeof item.extra === 'string' ? item.extra : ''}
96
+ </Text>
97
+ ) : null}
98
+ </Pressable>
99
+ {isOpen ? (
100
+ <View
101
+ style={{
102
+ padding,
103
+ borderBottomWidth: !isLast ? 1 : 0,
104
+ borderBottomColor: theme.colors.border,
105
+ backgroundColor: theme.colors.surface,
106
+ }}
107
+ >
108
+ {typeof item.children === 'string' || typeof item.children === 'number' ? (
109
+ <Text style={{ color: theme.colors.fg }}>{String(item.children)}</Text>
110
+ ) : (
111
+ item.children
112
+ )}
113
+ </View>
114
+ ) : null}
115
+ </View>
116
+ );
117
+ })}
118
+ </View>
119
+ );
120
+ }
@@ -0,0 +1,114 @@
1
+ import { useState, type ReactNode } from 'react';
2
+ import { Modal, Pressable, View, Text, TextInput, type ViewProps } from 'react-native';
3
+ import { useTheme } from './ElvoraProvider';
4
+
5
+ export interface ColorPickerProps extends Omit<ViewProps, 'onChange'> {
6
+ value?: string;
7
+ defaultValue?: string;
8
+ onChange?: (hex: string) => void;
9
+ presets?: string[];
10
+ isDisabled?: boolean;
11
+ /** Custom trigger node (otherwise a swatch button is rendered). */
12
+ trigger?: ReactNode;
13
+ }
14
+
15
+ const DEFAULT_PRESETS = ['#0F172A', '#1D4ED8', '#16A34A', '#F59E0B', '#DC2626', '#7C3AED', '#0EA5E9', '#FFFFFF'];
16
+
17
+ function normalize(v: string): string {
18
+ if (!v) return '#000000';
19
+ return v.startsWith('#') ? v.toUpperCase() : `#${v.toUpperCase()}`;
20
+ }
21
+
22
+ export function ColorPicker(props: ColorPickerProps) {
23
+ const { value: valueProp, defaultValue = '#0F172A', onChange, presets = DEFAULT_PRESETS, isDisabled, trigger, style, ...rest } = props;
24
+ const theme = useTheme();
25
+ const [internal, setInternal] = useState(normalize(defaultValue));
26
+ const value = normalize(valueProp ?? internal);
27
+ const [isOpen, setOpen] = useState(false);
28
+ const [hexInput, setHexInput] = useState(value);
29
+
30
+ const setValue = (next: string) => {
31
+ const n = normalize(next);
32
+ if (valueProp === undefined) setInternal(n);
33
+ onChange?.(n);
34
+ setHexInput(n);
35
+ };
36
+
37
+ return (
38
+ <View style={style} {...rest}>
39
+ <Pressable
40
+ accessibilityRole="button"
41
+ disabled={isDisabled}
42
+ onPress={() => setOpen(true)}
43
+ style={({ pressed }) => ({
44
+ flexDirection: 'row',
45
+ alignItems: 'center',
46
+ padding: 6,
47
+ borderRadius: Number(theme.radii.md),
48
+ borderWidth: 1,
49
+ borderColor: theme.colors.border,
50
+ backgroundColor: pressed ? theme.colors.intent.neutral.subtle : theme.colors.surfaceElevated,
51
+ opacity: isDisabled ? 0.6 : 1,
52
+ })}
53
+ >
54
+ {trigger ?? (
55
+ <>
56
+ <View style={{ width: 24, height: 24, borderRadius: Number(theme.radii.sm), backgroundColor: value, borderWidth: 1, borderColor: theme.colors.border }} />
57
+ <Text style={{ marginLeft: 8, color: theme.colors.fg, fontSize: 13 }}>{value}</Text>
58
+ </>
59
+ )}
60
+ </Pressable>
61
+ <Modal transparent visible={isOpen} animationType="fade" onRequestClose={() => setOpen(false)}>
62
+ <Pressable
63
+ style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.4)' }}
64
+ onPress={() => setOpen(false)}
65
+ >
66
+ <Pressable
67
+ onPress={() => undefined}
68
+ style={{
69
+ padding: 16,
70
+ backgroundColor: theme.colors.surfaceElevated,
71
+ borderRadius: Number(theme.radii.md),
72
+ minWidth: 260,
73
+ }}
74
+ >
75
+ <Text style={{ fontSize: 14, fontWeight: '500', marginBottom: 12, color: theme.colors.fg }}>Pick color</Text>
76
+ <View style={{ width: '100%', height: 60, borderRadius: Number(theme.radii.sm), backgroundColor: value, marginBottom: 12, borderWidth: 1, borderColor: theme.colors.border }} />
77
+ <TextInput
78
+ value={hexInput}
79
+ onChangeText={setHexInput}
80
+ onSubmitEditing={() => setValue(hexInput)}
81
+ autoCapitalize="characters"
82
+ placeholder="#RRGGBB"
83
+ style={{
84
+ paddingHorizontal: 10,
85
+ paddingVertical: 8,
86
+ borderWidth: 1,
87
+ borderColor: theme.colors.border,
88
+ borderRadius: Number(theme.radii.sm),
89
+ color: theme.colors.fg,
90
+ marginBottom: 12,
91
+ }}
92
+ />
93
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6 }}>
94
+ {presets.map((p) => (
95
+ <Pressable
96
+ key={p}
97
+ onPress={() => setValue(p)}
98
+ style={{
99
+ width: 28,
100
+ height: 28,
101
+ borderRadius: Number(theme.radii.sm),
102
+ backgroundColor: p,
103
+ borderWidth: 1,
104
+ borderColor: theme.colors.border,
105
+ }}
106
+ />
107
+ ))}
108
+ </View>
109
+ </Pressable>
110
+ </Pressable>
111
+ </Modal>
112
+ </View>
113
+ );
114
+ }
@@ -0,0 +1,22 @@
1
+ import { forwardRef, type ReactNode } from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+
4
+ export interface ContainerProps extends ViewProps {
5
+ maxWidth?: number;
6
+ padding?: number;
7
+ children?: ReactNode;
8
+ }
9
+
10
+ /** Container — centered, max-width container for RN. */
11
+ export const Container = forwardRef<View, ContainerProps>(function Container(props, ref) {
12
+ const { maxWidth = 1080, padding = 16, style, children, ...rest } = props;
13
+ return (
14
+ <View
15
+ ref={ref}
16
+ style={[{ width: '100%', maxWidth, alignSelf: 'center', paddingHorizontal: padding }, style]}
17
+ {...rest}
18
+ >
19
+ {children}
20
+ </View>
21
+ );
22
+ });