@utilitywarehouse/hearth-react-native 0.9.0 → 0.11.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 (106) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/CHANGELOG.md +16 -0
  4. package/build/components/Avatar/Avatar.d.ts +6 -0
  5. package/build/components/Avatar/Avatar.js +80 -0
  6. package/build/components/Avatar/Avatar.props.d.ts +28 -0
  7. package/build/components/Avatar/Avatar.props.js +1 -0
  8. package/build/components/Avatar/index.d.ts +2 -0
  9. package/build/components/Avatar/index.js +1 -0
  10. package/build/components/DateInput/DateInput.d.ts +6 -0
  11. package/build/components/DateInput/DateInput.js +19 -0
  12. package/build/components/DateInput/DateInput.props.d.ts +79 -0
  13. package/build/components/DateInput/DateInput.props.js +1 -0
  14. package/build/components/DateInput/DateInputSegment.d.ts +20 -0
  15. package/build/components/DateInput/DateInputSegment.js +31 -0
  16. package/build/components/DateInput/index.d.ts +2 -0
  17. package/build/components/DateInput/index.js +1 -0
  18. package/build/components/PillGroup/Pill.d.ts +16 -0
  19. package/build/components/PillGroup/Pill.js +94 -0
  20. package/build/components/PillGroup/Pill.props.d.ts +10 -0
  21. package/build/components/PillGroup/Pill.props.js +1 -0
  22. package/build/components/PillGroup/PillGroup.context.d.ts +6 -0
  23. package/build/components/PillGroup/PillGroup.context.js +5 -0
  24. package/build/components/PillGroup/PillGroup.d.ts +5 -0
  25. package/build/components/PillGroup/PillGroup.js +34 -0
  26. package/build/components/PillGroup/PillGroup.props.d.ts +15 -0
  27. package/build/components/PillGroup/PillGroup.props.js +1 -0
  28. package/build/components/PillGroup/index.d.ts +4 -0
  29. package/build/components/PillGroup/index.js +2 -0
  30. package/build/components/Select/Select.js +2 -1
  31. package/build/components/Toast/Toast.context.d.ts +9 -0
  32. package/build/components/Toast/Toast.context.js +90 -0
  33. package/build/components/Toast/Toast.props.d.ts +29 -0
  34. package/build/components/Toast/Toast.props.js +1 -0
  35. package/build/components/Toast/ToastItem.d.ts +10 -0
  36. package/build/components/Toast/ToastItem.js +129 -0
  37. package/build/components/Toast/index.d.ts +3 -0
  38. package/build/components/Toast/index.js +2 -0
  39. package/build/components/index.d.ts +4 -0
  40. package/build/components/index.js +4 -0
  41. package/build/tokens/components/dark/checkbox.d.ts +3 -0
  42. package/build/tokens/components/dark/checkbox.js +3 -0
  43. package/build/tokens/components/dark/input.d.ts +6 -0
  44. package/build/tokens/components/dark/input.js +6 -0
  45. package/build/tokens/components/dark/radio.d.ts +3 -0
  46. package/build/tokens/components/dark/radio.js +3 -0
  47. package/build/tokens/components/dark/table.d.ts +2 -0
  48. package/build/tokens/components/dark/table.js +2 -0
  49. package/build/tokens/components/dark/toast.d.ts +6 -2
  50. package/build/tokens/components/dark/toast.js +6 -2
  51. package/build/tokens/components/light/checkbox.d.ts +3 -0
  52. package/build/tokens/components/light/checkbox.js +3 -0
  53. package/build/tokens/components/light/input.d.ts +6 -0
  54. package/build/tokens/components/light/input.js +6 -0
  55. package/build/tokens/components/light/radio.d.ts +3 -0
  56. package/build/tokens/components/light/radio.js +3 -0
  57. package/build/tokens/components/light/table.d.ts +2 -0
  58. package/build/tokens/components/light/table.js +2 -0
  59. package/build/tokens/components/light/toast.d.ts +6 -2
  60. package/build/tokens/components/light/toast.js +6 -2
  61. package/build/utils/getInitials.d.ts +1 -0
  62. package/build/utils/getInitials.js +8 -0
  63. package/build/utils/index.d.ts +1 -0
  64. package/build/utils/index.js +1 -0
  65. package/docs/assets/toast-ios.MP4 +0 -0
  66. package/docs/components/AllComponents.web.tsx +43 -0
  67. package/package.json +3 -3
  68. package/src/components/Avatar/Avatar.docs.mdx +105 -0
  69. package/src/components/Avatar/Avatar.props.ts +31 -0
  70. package/src/components/Avatar/Avatar.stories.tsx +77 -0
  71. package/src/components/Avatar/Avatar.tsx +136 -0
  72. package/src/components/Avatar/index.ts +2 -0
  73. package/src/components/DateInput/DateInput.docs.mdx +163 -0
  74. package/src/components/DateInput/DateInput.props.ts +80 -0
  75. package/src/components/DateInput/DateInput.stories.tsx +269 -0
  76. package/src/components/DateInput/DateInput.tsx +117 -0
  77. package/src/components/DateInput/DateInputSegment.tsx +83 -0
  78. package/src/components/DateInput/index.ts +2 -0
  79. package/src/components/PillGroup/Pill.props.ts +13 -0
  80. package/src/components/PillGroup/Pill.tsx +120 -0
  81. package/src/components/PillGroup/PillGroup.context.tsx +12 -0
  82. package/src/components/PillGroup/PillGroup.docs.mdx +96 -0
  83. package/src/components/PillGroup/PillGroup.props.ts +22 -0
  84. package/src/components/PillGroup/PillGroup.stories.tsx +159 -0
  85. package/src/components/PillGroup/PillGroup.tsx +66 -0
  86. package/src/components/PillGroup/index.ts +4 -0
  87. package/src/components/Select/Select.tsx +2 -0
  88. package/src/components/Toast/Toast.context.tsx +118 -0
  89. package/src/components/Toast/Toast.docs.mdx +164 -0
  90. package/src/components/Toast/Toast.props.ts +33 -0
  91. package/src/components/Toast/Toast.stories.tsx +356 -0
  92. package/src/components/Toast/ToastItem.tsx +200 -0
  93. package/src/components/Toast/index.ts +3 -0
  94. package/src/components/index.ts +4 -0
  95. package/src/tokens/components/dark/checkbox.ts +3 -0
  96. package/src/tokens/components/dark/input.ts +6 -0
  97. package/src/tokens/components/dark/radio.ts +3 -0
  98. package/src/tokens/components/dark/table.ts +2 -0
  99. package/src/tokens/components/dark/toast.ts +6 -2
  100. package/src/tokens/components/light/checkbox.ts +3 -0
  101. package/src/tokens/components/light/input.ts +6 -0
  102. package/src/tokens/components/light/radio.ts +3 -0
  103. package/src/tokens/components/light/table.ts +2 -0
  104. package/src/tokens/components/light/toast.ts +6 -2
  105. package/src/utils/getInitials.ts +7 -0
  106. package/src/utils/index.ts +1 -0
@@ -0,0 +1,80 @@
1
+ import type { TextInputProps } from 'react-native';
2
+ import type { FormFieldBaseProps } from '../FormField/FormField.props';
3
+
4
+ export interface DateInputProps extends FormFieldBaseProps {
5
+ /**
6
+ * Whether the day segment is hidden.
7
+ * @default false
8
+ */
9
+ hideDay?: boolean;
10
+ /**
11
+ * Whether the month segment is hidden.
12
+ * @default false
13
+ */
14
+ hideMonth?: boolean;
15
+ /**
16
+ * Whether the year segment is hidden.
17
+ * @default false
18
+ */
19
+ hideYear?: boolean;
20
+ /**
21
+ * Placeholder text for the day segment.
22
+ */
23
+ dayPlaceholder?: string;
24
+ /**
25
+ * Placeholder text for the month segment.
26
+ */
27
+ monthPlaceholder?: string;
28
+ /**
29
+ * Placeholder text for the year segment.
30
+ */
31
+ yearPlaceholder?: string;
32
+ /**
33
+ * The controlled value for the day segment. Must be used with an `onDayChange` handler.
34
+ */
35
+ dayValue?: string;
36
+ /**
37
+ * The controlled value for the month segment. Must be used with an `onMonthChange` handler.
38
+ */
39
+ monthValue?: string;
40
+ /**
41
+ * The controlled value for the year segment. Must be used with an `onYearChange` handler.
42
+ */
43
+ yearValue?: string;
44
+ /**
45
+ * Callback fired when the day value changes.
46
+ */
47
+ onDayChange?: (text: string) => void;
48
+ /**
49
+ * Callback fired when the month value changes.
50
+ */
51
+ onMonthChange?: (text: string) => void;
52
+ /**
53
+ * Callback fired when the year value changes.
54
+ */
55
+ onYearChange?: (text: string) => void;
56
+ /**
57
+ * Callback fired when the day segment receives focus.
58
+ */
59
+ onDayFocus?: TextInputProps['onFocus'];
60
+ /**
61
+ * Callback fired when the month segment receives focus.
62
+ */
63
+ onMonthFocus?: TextInputProps['onFocus'];
64
+ /**
65
+ * Callback fired when the year segment receives focus.
66
+ */
67
+ onYearFocus?: TextInputProps['onFocus'];
68
+ /**
69
+ * Callback fired when the day segment loses focus.
70
+ */
71
+ onDayBlur?: TextInputProps['onBlur'];
72
+ /**
73
+ * Callback fired when the month segment loses focus.
74
+ */
75
+ onMonthBlur?: TextInputProps['onBlur'];
76
+ /**
77
+ * Callback fired when the year segment loses focus.
78
+ */
79
+ onYearBlur?: TextInputProps['onBlur'];
80
+ }
@@ -0,0 +1,269 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-native';
2
+ import { TickSmallIcon, WarningSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
3
+ import { useState } from 'react';
4
+ import { View } from 'react-native';
5
+ import { Button, Card, Flex, Heading } from '../../components';
6
+ import { DateInput } from './';
7
+
8
+ const DateInputMeta: Meta<typeof DateInput> = {
9
+ title: 'Stories / DateInput',
10
+ component: DateInput,
11
+ parameters: {
12
+ // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
13
+ layout: 'centered',
14
+ },
15
+ argTypes: {
16
+ label: {
17
+ control: 'text',
18
+ },
19
+ helperText: {
20
+ control: 'text',
21
+ },
22
+ validationStatus: {
23
+ control: 'select',
24
+ options: ['initial', 'valid', 'invalid'],
25
+ },
26
+ disabled: {
27
+ control: 'boolean',
28
+ },
29
+ required: {
30
+ control: 'boolean',
31
+ },
32
+ hideDay: {
33
+ control: 'boolean',
34
+ },
35
+ hideMonth: {
36
+ control: 'boolean',
37
+ },
38
+ hideYear: {
39
+ control: 'boolean',
40
+ },
41
+ },
42
+ };
43
+
44
+ export default DateInputMeta;
45
+
46
+ type Story = StoryObj<typeof DateInput>;
47
+
48
+ export const Playground: Story = {
49
+ args: {
50
+ label: 'Date',
51
+ helperText: 'Helper text',
52
+ },
53
+ };
54
+
55
+ export const DateOfBirth: Story = {
56
+ render: () => {
57
+ return <DateInput label="Date of birth" helperText="Enter your date of birth" required />;
58
+ },
59
+ };
60
+
61
+ export const CardExpiry: Story = {
62
+ render: () => {
63
+ const [month, setMonth] = useState('');
64
+ const [year, setYear] = useState('');
65
+ return (
66
+ <DateInput
67
+ label="Card expiry"
68
+ helperText="Enter the expiry month and year"
69
+ monthValue={month}
70
+ yearValue={year}
71
+ onMonthChange={setMonth}
72
+ onYearChange={setYear}
73
+ hideDay
74
+ required
75
+ />
76
+ );
77
+ },
78
+ };
79
+
80
+ export const YearOnly: Story = {
81
+ render: () => {
82
+ return <DateInput label="Year" helperText="Enter the year" hideDay hideMonth />;
83
+ },
84
+ };
85
+
86
+ export const Validation: Story = {
87
+ render: () => {
88
+ const [day, setDay] = useState('01');
89
+ const [month, setMonth] = useState('02');
90
+ const [year, setYear] = useState('2025');
91
+ return (
92
+ <View style={{ gap: 16 }}>
93
+ <DateInput
94
+ label="Valid date"
95
+ dayValue={day}
96
+ monthValue={month}
97
+ yearValue={year}
98
+ onDayChange={setDay}
99
+ onMonthChange={setMonth}
100
+ onYearChange={setYear}
101
+ validationStatus="valid"
102
+ validText="Date is valid"
103
+ helperIcon={TickSmallIcon}
104
+ required
105
+ />
106
+ <DateInput
107
+ label="Invalid date"
108
+ dayValue="32"
109
+ monthValue="13"
110
+ yearValue="2025"
111
+ validationStatus="invalid"
112
+ invalidText="Please enter a valid date"
113
+ helperIcon={WarningSmallIcon}
114
+ required
115
+ />
116
+ </View>
117
+ );
118
+ },
119
+ };
120
+
121
+ export const Disabled: Story = {
122
+ render: () => {
123
+ return (
124
+ <DateInput
125
+ label="Date of birth"
126
+ helperText="This field is disabled"
127
+ dayValue="15"
128
+ monthValue="06"
129
+ yearValue="1990"
130
+ disabled
131
+ />
132
+ );
133
+ },
134
+ };
135
+
136
+ export const DefaultValue: Story = {
137
+ render: () => {
138
+ return <DateInput label="Date of birth" dayValue="01" monthValue="01" yearValue="2000" />;
139
+ },
140
+ };
141
+
142
+ export const WithCustomValidation: Story = {
143
+ render: () => {
144
+ const [day, setDay] = useState('');
145
+ const [month, setMonth] = useState('');
146
+ const [year, setYear] = useState('');
147
+
148
+ const validateDate = () => {
149
+ if (!day || !month || !year) return { status: 'initial' as const, message: '' };
150
+
151
+ const dayNum = parseInt(day, 10);
152
+ const monthNum = parseInt(month, 10);
153
+ const yearNum = parseInt(year, 10);
154
+
155
+ // Basic validation
156
+ if (dayNum < 1 || dayNum > 31) {
157
+ return { status: 'invalid' as const, message: 'Day must be between 1 and 31' };
158
+ }
159
+ if (monthNum < 1 || monthNum > 12) {
160
+ return { status: 'invalid' as const, message: 'Month must be between 1 and 12' };
161
+ }
162
+ if (yearNum < 1900 || yearNum > new Date().getFullYear()) {
163
+ return {
164
+ status: 'invalid' as const,
165
+ message: `Year must be between 1900 and ${new Date().getFullYear()}`,
166
+ };
167
+ }
168
+
169
+ // Check valid date
170
+ const date = new Date(yearNum, monthNum - 1, dayNum);
171
+ if (
172
+ date.getDate() !== dayNum ||
173
+ date.getMonth() !== monthNum - 1 ||
174
+ date.getFullYear() !== yearNum
175
+ ) {
176
+ return { status: 'invalid' as const, message: 'Please enter a valid date' };
177
+ }
178
+
179
+ return { status: 'valid' as const, message: 'Valid date' };
180
+ };
181
+
182
+ const validation = validateDate();
183
+
184
+ return (
185
+ <DateInput
186
+ label="Date of birth"
187
+ helperText="Enter a valid date between 1900 and today"
188
+ dayValue={day}
189
+ monthValue={month}
190
+ yearValue={year}
191
+ onDayChange={setDay}
192
+ onMonthChange={setMonth}
193
+ onYearChange={setYear}
194
+ validationStatus={validation.status}
195
+ validText={validation.status === 'valid' ? validation.message : undefined}
196
+ invalidText={validation.status === 'invalid' ? validation.message : undefined}
197
+ required
198
+ />
199
+ );
200
+ },
201
+ };
202
+
203
+ export const FlexibleSegments: Story = {
204
+ render: () => (
205
+ <View style={{ gap: 16 }}>
206
+ <DateInput label="Full date" helperText="DD/MM/YYYY" />
207
+ <DateInput label="Month and year" helperText="MM/YYYY" hideDay required />
208
+ <DateInput label="Year only" helperText="YYYY" hideDay hideMonth required />
209
+ </View>
210
+ ),
211
+ };
212
+
213
+ export const GroupingInputs: Story = {
214
+ render: () => (
215
+ <Flex space="sm">
216
+ <Heading size="lg">Event Registration</Heading>
217
+ <Card variant="subtle" gap="250">
218
+ <DateInput label="Date of birth" helperText="Enter your date of birth" required />
219
+ <DateInput
220
+ label="Event date preference"
221
+ helperText="Select your preferred date"
222
+ required={false}
223
+ />
224
+ </Card>
225
+ </Flex>
226
+ ),
227
+ };
228
+
229
+ export const WithState: Story = {
230
+ render: () => {
231
+ const [day, setDay] = useState('');
232
+ const [month, setMonth] = useState('');
233
+ const [year, setYear] = useState('');
234
+
235
+ const handleReset = () => {
236
+ setDay('');
237
+ setMonth('');
238
+ setYear('');
239
+ };
240
+
241
+ const handleSetToday = () => {
242
+ const today = new Date();
243
+ setDay(String(today.getDate()).padStart(2, '0'));
244
+ setMonth(String(today.getMonth() + 1).padStart(2, '0'));
245
+ setYear(String(today.getFullYear()));
246
+ };
247
+
248
+ return (
249
+ <Flex space="md">
250
+ <DateInput
251
+ label="Date"
252
+ helperText="Select or enter a date"
253
+ dayValue={day}
254
+ monthValue={month}
255
+ yearValue={year}
256
+ onDayChange={setDay}
257
+ onMonthChange={setMonth}
258
+ onYearChange={setYear}
259
+ />
260
+ <Flex space="xs">
261
+ <Button onPress={handleSetToday}>Set to Today</Button>
262
+ <Button onPress={handleReset} variant="solid">
263
+ Reset
264
+ </Button>
265
+ </Flex>
266
+ </Flex>
267
+ );
268
+ },
269
+ };
@@ -0,0 +1,117 @@
1
+ import { View } from 'react-native';
2
+ import { StyleSheet } from 'react-native-unistyles';
3
+ import { FormField } from '../FormField';
4
+ import type { DateInputProps } from './DateInput.props';
5
+ import DateInputSegment from './DateInputSegment';
6
+
7
+ const DateInput = ({
8
+ label,
9
+ helperText,
10
+ helperIcon,
11
+ validationStatus = 'initial',
12
+ validText,
13
+ invalidText,
14
+ disabled,
15
+ readonly,
16
+ required,
17
+ hideDay = false,
18
+ hideMonth = false,
19
+ hideYear = false,
20
+ dayPlaceholder = 'DD',
21
+ monthPlaceholder = 'MM',
22
+ yearPlaceholder = 'YYYY',
23
+ dayValue,
24
+ monthValue,
25
+ yearValue,
26
+ onDayChange,
27
+ onMonthChange,
28
+ onYearChange,
29
+ onDayFocus,
30
+ onMonthFocus,
31
+ onYearFocus,
32
+ onDayBlur,
33
+ onMonthBlur,
34
+ onYearBlur,
35
+ ...props
36
+ }: DateInputProps) => {
37
+ return (
38
+ <FormField
39
+ label={label}
40
+ helperText={helperText}
41
+ helperIcon={helperIcon}
42
+ validationStatus={validationStatus}
43
+ validText={validText}
44
+ invalidText={invalidText}
45
+ disabled={disabled}
46
+ readonly={readonly}
47
+ required={required}
48
+ style={styles.wrap}
49
+ {...props}
50
+ >
51
+ <View style={styles.container}>
52
+ {!hideDay && (
53
+ <DateInputSegment
54
+ label="Day"
55
+ placeholder={dayPlaceholder}
56
+ value={dayValue}
57
+ onChange={onDayChange}
58
+ onFocus={onDayFocus}
59
+ onBlur={onDayBlur}
60
+ disabled={disabled}
61
+ required={required}
62
+ readonly={readonly}
63
+ validationStatus={validationStatus}
64
+ maxLength={2}
65
+ testID="date-input-day"
66
+ />
67
+ )}
68
+ {!hideMonth && (
69
+ <DateInputSegment
70
+ label="Month"
71
+ placeholder={monthPlaceholder}
72
+ value={monthValue}
73
+ onChange={onMonthChange}
74
+ onFocus={onMonthFocus}
75
+ onBlur={onMonthBlur}
76
+ disabled={disabled}
77
+ required={required}
78
+ readonly={readonly}
79
+ validationStatus={validationStatus}
80
+ maxLength={2}
81
+ testID="date-input-month"
82
+ />
83
+ )}
84
+ {!hideYear && (
85
+ <DateInputSegment
86
+ label="Year"
87
+ placeholder={yearPlaceholder}
88
+ value={yearValue}
89
+ onChange={onYearChange}
90
+ onFocus={onYearFocus}
91
+ onBlur={onYearBlur}
92
+ disabled={disabled}
93
+ required={required}
94
+ readonly={readonly}
95
+ validationStatus={validationStatus}
96
+ maxLength={4}
97
+ testID="date-input-year"
98
+ />
99
+ )}
100
+ </View>
101
+ </FormField>
102
+ );
103
+ };
104
+
105
+ DateInput.displayName = 'DateInput';
106
+
107
+ const styles = StyleSheet.create(theme => ({
108
+ wrap: {
109
+ gap: theme.components.input.gap,
110
+ },
111
+ container: {
112
+ flexDirection: 'row',
113
+ gap: theme.components.input.date.gap,
114
+ },
115
+ }));
116
+
117
+ export default DateInput;
@@ -0,0 +1,83 @@
1
+ import { View } from 'react-native';
2
+ import { StyleSheet } from 'react-native-unistyles';
3
+ import { BodyText } from '../BodyText';
4
+ import { Input } from '../Input';
5
+ import type { DateInputProps } from './DateInput.props';
6
+
7
+ interface DateInputSegmentProps {
8
+ label: string;
9
+ placeholder?: string;
10
+ value?: string;
11
+ onChange?: (text: string) => void;
12
+ onFocus?: DateInputProps['onDayFocus'];
13
+ onBlur?: DateInputProps['onDayBlur'];
14
+ disabled?: boolean;
15
+ required?: boolean;
16
+ validationStatus?: DateInputProps['validationStatus'];
17
+ maxLength?: number;
18
+ readonly?: boolean;
19
+ testID?: string;
20
+ }
21
+
22
+ const DateInputSegment = ({
23
+ label,
24
+ placeholder,
25
+ value,
26
+ onChange,
27
+ onFocus,
28
+ onBlur,
29
+ disabled,
30
+ validationStatus,
31
+ maxLength,
32
+ readonly,
33
+ testID,
34
+ }: DateInputSegmentProps) => {
35
+ styles.useVariants({ disabled });
36
+ return (
37
+ <View style={styles.container}>
38
+ <BodyText size="md" style={styles.label}>
39
+ {label}
40
+ </BodyText>
41
+ <Input
42
+ value={value}
43
+ onChangeText={onChange}
44
+ onFocus={onFocus}
45
+ onBlur={onBlur}
46
+ placeholder={disabled ? undefined : placeholder}
47
+ keyboardType="number-pad"
48
+ maxLength={maxLength}
49
+ testID={testID}
50
+ accessibilityLabel={label}
51
+ disabled={disabled}
52
+ validationStatus={validationStatus}
53
+ readonly={readonly}
54
+ style={styles.input}
55
+ />
56
+ </View>
57
+ );
58
+ };
59
+
60
+ DateInputSegment.displayName = 'DateInputSegment';
61
+
62
+ const styles = StyleSheet.create(theme => ({
63
+ container: {
64
+ flex: 1,
65
+ gap: theme.components.input.gap,
66
+ maxWidth: 96,
67
+ },
68
+ label: {
69
+ variants: {
70
+ disabled: {
71
+ true: {
72
+ opacity: theme.opacity.disabled,
73
+ },
74
+ },
75
+ },
76
+ },
77
+ input: {
78
+ flex: 1,
79
+ maxWidth: 96,
80
+ },
81
+ }));
82
+
83
+ export default DateInputSegment;
@@ -0,0 +1,2 @@
1
+ export { default as DateInput } from './DateInput';
2
+ export type { DateInputProps } from './DateInput.props';
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { PressableProps } from 'react-native';
3
+
4
+ export interface PillProps extends Omit<PressableProps, 'children'> {
5
+ /** Value returned when selected */
6
+ value: string;
7
+
8
+ /** Text label shown inside the pill */
9
+ label: string;
10
+
11
+ /** Left icon */
12
+ icon?: React.ComponentType<any>;
13
+ }
@@ -0,0 +1,120 @@
1
+ import { createPressable } from '@gluestack-ui/pressable';
2
+ import { Pressable } from 'react-native';
3
+ import { StyleSheet } from 'react-native-unistyles';
4
+ import { Icon } from '../Icon';
5
+ import { BodyText } from '../BodyText';
6
+ import { usePillGroupContext } from './PillGroup.context';
7
+ import type { PillProps } from './Pill.props';
8
+
9
+ const PillRoot = ({
10
+ value,
11
+ label,
12
+ icon,
13
+ states = {},
14
+ ...props
15
+ }: PillProps & { states?: { active?: boolean } }) => {
16
+ const { active } = states;
17
+ const context = usePillGroupContext();
18
+ const isSelected = context?.value.includes(value) ?? false;
19
+
20
+ styles.useVariants({ selected: isSelected, active });
21
+
22
+ const handlePress = () => {
23
+ context?.onChange(value);
24
+ };
25
+
26
+ return (
27
+ <Pressable
28
+ {...props}
29
+ style={styles.pill}
30
+ accessibilityRole="button"
31
+ accessibilityState={{ selected: isSelected }}
32
+ onPress={handlePress}
33
+ >
34
+ {icon && <Icon as={icon} size="sm" style={styles.icon} />}
35
+ <BodyText weight="semibold" style={styles.text}>
36
+ {label}
37
+ </BodyText>
38
+ </Pressable>
39
+ );
40
+ };
41
+
42
+ export const Pill = createPressable({ Root: PillRoot });
43
+
44
+ Pill.displayName = 'Pill';
45
+
46
+ const styles = StyleSheet.create(theme => ({
47
+ pill: {
48
+ flexDirection: 'row',
49
+ alignItems: 'center',
50
+ justifyContent: 'center',
51
+ height: theme.components.pill.height,
52
+ minWidth: theme.components.pill.minWidth,
53
+ gap: theme.components.pill.gap,
54
+ paddingHorizontal: theme.components.pill.paddingHorizontal,
55
+ paddingVertical: theme.components.pill.paddingVertical,
56
+ borderRadius: theme.components.pill.borderRadius,
57
+ borderWidth: theme.components.pill.borderWidth,
58
+ borderColor: theme.color.interactive.neutral.border.subtle,
59
+ backgroundColor: 'transparent',
60
+ _web: {
61
+ _hover: {
62
+ backgroundColor: theme.color.interactive.neutral.surface.subtle.hover,
63
+ },
64
+ '_focus-visible': theme.helpers.focusVisible,
65
+ },
66
+ variants: {
67
+ active: {
68
+ true: {
69
+ backgroundColor: theme.color.interactive.neutral.surface.subtle.active,
70
+ },
71
+ },
72
+ selected: {
73
+ true: {
74
+ backgroundColor: theme.color.interactive.brand.surface.strong.default,
75
+ borderColor: theme.color.interactive.brand.surface.strong.default,
76
+ _web: {
77
+ _hover: {
78
+ backgroundColor: theme.color.interactive.brand.surface.strong.hover,
79
+ borderColor: theme.color.interactive.brand.surface.strong.hover,
80
+ },
81
+ },
82
+ },
83
+ },
84
+ },
85
+ compoundVariants: [
86
+ {
87
+ selected: true,
88
+ active: true,
89
+ styles: {
90
+ backgroundColor: theme.color.interactive.brand.surface.strong.active,
91
+ borderColor: theme.color.interactive.brand.surface.strong.active,
92
+ },
93
+ },
94
+ ],
95
+ },
96
+ text: {
97
+ variants: {
98
+ selected: {
99
+ true: {
100
+ color: theme.color.text.inverted,
101
+ },
102
+ false: {
103
+ color: theme.color.text.primary,
104
+ },
105
+ },
106
+ },
107
+ },
108
+ icon: {
109
+ variants: {
110
+ selected: {
111
+ true: {
112
+ color: theme.color.icon.inverted,
113
+ },
114
+ false: {
115
+ color: theme.color.icon.primary,
116
+ },
117
+ },
118
+ },
119
+ },
120
+ }));
@@ -0,0 +1,12 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ export interface PillGroupContextValue {
4
+ value: string[];
5
+ onChange: (value: string) => void;
6
+ }
7
+
8
+ export const PillGroupContext = createContext<PillGroupContextValue | null>(null);
9
+
10
+ export const usePillGroupContext = () => {
11
+ return useContext(PillGroupContext);
12
+ };