@bacon-pos/ui 0.5.1 → 0.6.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 (140) hide show
  1. package/lib/commonjs/components/Dropdown/Dropdown.js +282 -0
  2. package/lib/commonjs/components/Dropdown/Dropdown.js.map +1 -0
  3. package/lib/commonjs/components/Dropdown/Dropdown.stories.js +421 -0
  4. package/lib/commonjs/components/Dropdown/Dropdown.stories.js.map +1 -0
  5. package/lib/commonjs/components/Dropdown/Dropdown.test.js +113 -0
  6. package/lib/commonjs/components/Dropdown/Dropdown.test.js.map +1 -0
  7. package/lib/commonjs/components/Dropdown/index.js +19 -0
  8. package/lib/commonjs/components/Dropdown/index.js.map +1 -0
  9. package/lib/commonjs/components/Menu/Menu.js +214 -0
  10. package/lib/commonjs/components/Menu/Menu.js.map +1 -0
  11. package/lib/commonjs/components/Menu/Menu.test.js +71 -0
  12. package/lib/commonjs/components/Menu/Menu.test.js.map +1 -0
  13. package/lib/commonjs/components/Menu/MenuButton.js +73 -0
  14. package/lib/commonjs/components/Menu/MenuButton.js.map +1 -0
  15. package/lib/commonjs/components/Menu/MenuButton.stories.js +166 -0
  16. package/lib/commonjs/components/Menu/MenuButton.stories.js.map +1 -0
  17. package/lib/commonjs/components/Menu/SelectButton.js +143 -0
  18. package/lib/commonjs/components/Menu/SelectButton.js.map +1 -0
  19. package/lib/commonjs/components/Menu/SelectButton.stories.js +116 -0
  20. package/lib/commonjs/components/Menu/SelectButton.stories.js.map +1 -0
  21. package/lib/commonjs/components/Menu/SelectButton.test.js +80 -0
  22. package/lib/commonjs/components/Menu/SelectButton.test.js.map +1 -0
  23. package/lib/commonjs/components/Menu/index.js +27 -0
  24. package/lib/commonjs/components/Menu/index.js.map +1 -0
  25. package/lib/commonjs/components/Select/Select.js +246 -0
  26. package/lib/commonjs/components/Select/Select.js.map +1 -0
  27. package/lib/commonjs/components/Select/Select.stories.js +221 -0
  28. package/lib/commonjs/components/Select/Select.stories.js.map +1 -0
  29. package/lib/commonjs/components/Select/Select.test.js +165 -0
  30. package/lib/commonjs/components/Select/Select.test.js.map +1 -0
  31. package/lib/commonjs/components/Select/index.js +13 -0
  32. package/lib/commonjs/components/Select/index.js.map +1 -0
  33. package/lib/commonjs/components/UserChip/UserChip.js +121 -0
  34. package/lib/commonjs/components/UserChip/UserChip.js.map +1 -0
  35. package/lib/commonjs/components/UserChip/UserChip.stories.js +153 -0
  36. package/lib/commonjs/components/UserChip/UserChip.stories.js.map +1 -0
  37. package/lib/commonjs/components/UserChip/UserChip.test.js +59 -0
  38. package/lib/commonjs/components/UserChip/UserChip.test.js.map +1 -0
  39. package/lib/commonjs/components/UserChip/index.js +13 -0
  40. package/lib/commonjs/components/UserChip/index.js.map +1 -0
  41. package/lib/commonjs/components/index.js +57 -13
  42. package/lib/commonjs/components/index.js.map +1 -1
  43. package/lib/module/components/Dropdown/Dropdown.js +277 -0
  44. package/lib/module/components/Dropdown/Dropdown.js.map +1 -0
  45. package/lib/module/components/Dropdown/Dropdown.stories.js +417 -0
  46. package/lib/module/components/Dropdown/Dropdown.stories.js.map +1 -0
  47. package/lib/module/components/Dropdown/Dropdown.test.js +113 -0
  48. package/lib/module/components/Dropdown/Dropdown.test.js.map +1 -0
  49. package/lib/module/components/Dropdown/index.js +4 -0
  50. package/lib/module/components/Dropdown/index.js.map +1 -0
  51. package/lib/module/components/Menu/Menu.js +210 -0
  52. package/lib/module/components/Menu/Menu.js.map +1 -0
  53. package/lib/module/components/Menu/Menu.test.js +71 -0
  54. package/lib/module/components/Menu/Menu.test.js.map +1 -0
  55. package/lib/module/components/Menu/MenuButton.js +69 -0
  56. package/lib/module/components/Menu/MenuButton.js.map +1 -0
  57. package/lib/module/components/Menu/MenuButton.stories.js +162 -0
  58. package/lib/module/components/Menu/MenuButton.stories.js.map +1 -0
  59. package/lib/module/components/Menu/SelectButton.js +139 -0
  60. package/lib/module/components/Menu/SelectButton.js.map +1 -0
  61. package/lib/module/components/Menu/SelectButton.stories.js +112 -0
  62. package/lib/module/components/Menu/SelectButton.stories.js.map +1 -0
  63. package/lib/module/components/Menu/SelectButton.test.js +80 -0
  64. package/lib/module/components/Menu/SelectButton.test.js.map +1 -0
  65. package/lib/module/components/Menu/index.js +6 -0
  66. package/lib/module/components/Menu/index.js.map +1 -0
  67. package/lib/module/components/Select/Select.js +244 -0
  68. package/lib/module/components/Select/Select.js.map +1 -0
  69. package/lib/module/components/Select/Select.stories.js +217 -0
  70. package/lib/module/components/Select/Select.stories.js.map +1 -0
  71. package/lib/module/components/Select/Select.test.js +165 -0
  72. package/lib/module/components/Select/Select.test.js.map +1 -0
  73. package/lib/module/components/Select/index.js +4 -0
  74. package/lib/module/components/Select/index.js.map +1 -0
  75. package/lib/module/components/UserChip/UserChip.js +117 -0
  76. package/lib/module/components/UserChip/UserChip.js.map +1 -0
  77. package/lib/module/components/UserChip/UserChip.stories.js +149 -0
  78. package/lib/module/components/UserChip/UserChip.stories.js.map +1 -0
  79. package/lib/module/components/UserChip/UserChip.test.js +59 -0
  80. package/lib/module/components/UserChip/UserChip.test.js.map +1 -0
  81. package/lib/module/components/UserChip/index.js +4 -0
  82. package/lib/module/components/UserChip/index.js.map +1 -0
  83. package/lib/module/components/index.js +4 -0
  84. package/lib/module/components/index.js.map +1 -1
  85. package/lib/typescript/commonjs/components/Dropdown/Dropdown.d.ts +105 -0
  86. package/lib/typescript/commonjs/components/Dropdown/Dropdown.d.ts.map +1 -0
  87. package/lib/typescript/commonjs/components/Dropdown/index.d.ts +3 -0
  88. package/lib/typescript/commonjs/components/Dropdown/index.d.ts.map +1 -0
  89. package/lib/typescript/commonjs/components/Menu/Menu.d.ts +66 -0
  90. package/lib/typescript/commonjs/components/Menu/Menu.d.ts.map +1 -0
  91. package/lib/typescript/commonjs/components/Menu/MenuButton.d.ts +46 -0
  92. package/lib/typescript/commonjs/components/Menu/MenuButton.d.ts.map +1 -0
  93. package/lib/typescript/commonjs/components/Menu/SelectButton.d.ts +60 -0
  94. package/lib/typescript/commonjs/components/Menu/SelectButton.d.ts.map +1 -0
  95. package/lib/typescript/commonjs/components/Menu/index.d.ts +7 -0
  96. package/lib/typescript/commonjs/components/Menu/index.d.ts.map +1 -0
  97. package/lib/typescript/commonjs/components/Select/Select.d.ts +73 -0
  98. package/lib/typescript/commonjs/components/Select/Select.d.ts.map +1 -0
  99. package/lib/typescript/commonjs/components/Select/index.d.ts +3 -0
  100. package/lib/typescript/commonjs/components/Select/index.d.ts.map +1 -0
  101. package/lib/typescript/commonjs/components/UserChip/UserChip.d.ts +33 -0
  102. package/lib/typescript/commonjs/components/UserChip/UserChip.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/components/UserChip/index.d.ts +3 -0
  104. package/lib/typescript/commonjs/components/UserChip/index.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/components/index.d.ts +4 -0
  106. package/lib/typescript/commonjs/components/index.d.ts.map +1 -1
  107. package/lib/typescript/module/components/Dropdown/Dropdown.d.ts +105 -0
  108. package/lib/typescript/module/components/Dropdown/Dropdown.d.ts.map +1 -0
  109. package/lib/typescript/module/components/Dropdown/index.d.ts +3 -0
  110. package/lib/typescript/module/components/Dropdown/index.d.ts.map +1 -0
  111. package/lib/typescript/module/components/Menu/Menu.d.ts +66 -0
  112. package/lib/typescript/module/components/Menu/Menu.d.ts.map +1 -0
  113. package/lib/typescript/module/components/Menu/MenuButton.d.ts +46 -0
  114. package/lib/typescript/module/components/Menu/MenuButton.d.ts.map +1 -0
  115. package/lib/typescript/module/components/Menu/SelectButton.d.ts +60 -0
  116. package/lib/typescript/module/components/Menu/SelectButton.d.ts.map +1 -0
  117. package/lib/typescript/module/components/Menu/index.d.ts +7 -0
  118. package/lib/typescript/module/components/Menu/index.d.ts.map +1 -0
  119. package/lib/typescript/module/components/Select/Select.d.ts +73 -0
  120. package/lib/typescript/module/components/Select/Select.d.ts.map +1 -0
  121. package/lib/typescript/module/components/Select/index.d.ts +3 -0
  122. package/lib/typescript/module/components/Select/index.d.ts.map +1 -0
  123. package/lib/typescript/module/components/UserChip/UserChip.d.ts +33 -0
  124. package/lib/typescript/module/components/UserChip/UserChip.d.ts.map +1 -0
  125. package/lib/typescript/module/components/UserChip/index.d.ts +3 -0
  126. package/lib/typescript/module/components/UserChip/index.d.ts.map +1 -0
  127. package/lib/typescript/module/components/index.d.ts +4 -0
  128. package/lib/typescript/module/components/index.d.ts.map +1 -1
  129. package/package.json +1 -1
  130. package/src/components/Dropdown/Dropdown.tsx +319 -0
  131. package/src/components/Dropdown/index.ts +2 -0
  132. package/src/components/Menu/Menu.tsx +249 -0
  133. package/src/components/Menu/MenuButton.tsx +98 -0
  134. package/src/components/Menu/SelectButton.tsx +171 -0
  135. package/src/components/Menu/index.ts +12 -0
  136. package/src/components/Select/Select.tsx +323 -0
  137. package/src/components/Select/index.ts +2 -0
  138. package/src/components/UserChip/UserChip.tsx +138 -0
  139. package/src/components/UserChip/index.ts +2 -0
  140. package/src/components/index.ts +4 -0
@@ -0,0 +1,323 @@
1
+ import { useState } from 'react';
2
+ import {
3
+ Pressable,
4
+ View,
5
+ type StyleProp,
6
+ type TextStyle,
7
+ type ViewStyle,
8
+ } from 'react-native';
9
+ import { useTheme } from '../../theme';
10
+ import type { TypographyVariant } from '../../theme/typography';
11
+ import { Dropdown, type DropdownEntry } from '../Dropdown/Dropdown';
12
+ import { Icon } from '../Icon/Icon';
13
+ import type { IconName } from '../Icon/icons.generated';
14
+ import { Menu, type EdgeInsets, type MenuOrigin } from '../Menu/Menu';
15
+ import { Text } from '../Text/Text';
16
+
17
+ /** Width of the focus ring, in px (Figma "Focus" effect: spread 6). */
18
+ const FOCUS_RING_SPREAD = 6;
19
+ const FOCUS_RING_ALPHA = 0.15;
20
+
21
+ /** Convert a `#RRGGBB` (or `#RGB`) token to an `rgba()` string at the given alpha. */
22
+ function hexToRgba(hex: string, alpha: number): string {
23
+ const h = hex.replace('#', '');
24
+ const full = h.length === 3 ? h.replace(/./g, (c) => c + c) : h;
25
+ const r = parseInt(full.slice(0, 2), 16);
26
+ const g = parseInt(full.slice(2, 4), 16);
27
+ const b = parseInt(full.slice(4, 6), 16);
28
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
29
+ }
30
+
31
+ export type SelectSize = 'md' | 'lg';
32
+
33
+ const SIZES: Record<
34
+ SelectSize,
35
+ { height: number; gap: number; iconSize: number; textVariant: TypographyVariant }
36
+ > = {
37
+ md: { height: 48, gap: 8, iconSize: 20, textVariant: 'bodyMedium' },
38
+ lg: { height: 58, gap: 10, iconSize: 24, textVariant: 'bodyLarge' },
39
+ };
40
+
41
+ /** An option in a `Select`. */
42
+ export interface SelectOption {
43
+ key: string;
44
+ label: string;
45
+ /** Leading icon shown in the option row. */
46
+ icon?: IconName;
47
+ disabled?: boolean;
48
+ }
49
+
50
+ export interface SelectProps {
51
+ /** The options to choose from. */
52
+ options: SelectOption[];
53
+ /** Allow choosing several options (checkbox rows; value is an array). */
54
+ multiple?: boolean;
55
+ /** Selected key(s) — a string (single) or string[] (multiple). Controlled. */
56
+ value?: string | string[];
57
+ /** Initial selection for the uncontrolled case. */
58
+ defaultValue?: string | string[];
59
+ /** Called with the new selection (string for single, string[] for multiple). */
60
+ onChange?: (value: string | string[]) => void;
61
+
62
+ /** Label above the field. */
63
+ label?: string;
64
+ /** Helper text below the field. Replaced by the error message when `error` is a string. */
65
+ helperText?: string;
66
+ /** Mark invalid. Pass a string to show it as the error message. */
67
+ error?: boolean | string;
68
+ /** Text shown when nothing is selected. Defaults to "Select option". */
69
+ placeholder?: string;
70
+ /** Fixed leading icon on the field. */
71
+ leftIcon?: IconName;
72
+ /** Field height: `md` = 48, `lg` = 58. Defaults to `lg`. */
73
+ size?: SelectSize;
74
+ /** Fixed field width. Omit to use `fullWidth` (the Figma default). */
75
+ width?: number;
76
+ /** Stretch to the full width of the parent. Defaults to true. Ignored when `width` is set. */
77
+ fullWidth?: boolean;
78
+ /** Disable the field. */
79
+ disabled?: boolean;
80
+ /** Custom display for the selected value(s). */
81
+ renderValue?: (selected: SelectOption[]) => string;
82
+
83
+ /** Match the menu width to the field width. Defaults to true. */
84
+ matchTriggerWidth?: boolean;
85
+ /** Menu width when `matchTriggerWidth` is false. */
86
+ menuWidth?: number;
87
+ /** Max menu height; scrolls beyond it. */
88
+ maxHeight?: number;
89
+ /** Point on the field the menu latches to. Defaults to `{ bottom, left }`. */
90
+ anchorOrigin?: MenuOrigin;
91
+ /** Point on the menu placed at the anchor. Defaults to `{ top, left }`. */
92
+ menuOrigin?: MenuOrigin;
93
+ /** Gap between field and menu. Defaults to 8. */
94
+ offset?: number;
95
+ /** Auto-fit the menu height and flip direction near screen edges. Defaults to true. */
96
+ autoFit?: boolean;
97
+ /** Safe-area insets to keep clear. */
98
+ insets?: EdgeInsets;
99
+ /** Close after selecting. Defaults to `!multiple`. */
100
+ closeOnSelect?: boolean;
101
+ /** Container style. */
102
+ style?: StyleProp<ViewStyle>;
103
+ }
104
+
105
+ /**
106
+ * Select — a form field that opens a `Dropdown` of options. The field mirrors
107
+ * `Input` (pill, label, helper/error, lime focus ring, red error ring); it is
108
+ * not editable — pressing it opens the menu, which matches the field width. The
109
+ * current option is marked with a check. With `multiple`, rows become checkboxes,
110
+ * the value is an array, and the menu stays open while picking.
111
+ */
112
+ export function Select({
113
+ options,
114
+ multiple = false,
115
+ value,
116
+ defaultValue,
117
+ onChange,
118
+ label,
119
+ helperText,
120
+ error,
121
+ placeholder = 'Select option',
122
+ leftIcon,
123
+ size = 'lg',
124
+ width,
125
+ fullWidth = true,
126
+ disabled = false,
127
+ renderValue,
128
+ matchTriggerWidth = true,
129
+ menuWidth,
130
+ maxHeight,
131
+ anchorOrigin = { vertical: 'bottom', horizontal: 'left' },
132
+ menuOrigin = { vertical: 'top', horizontal: 'left' },
133
+ offset = 8,
134
+ autoFit = true,
135
+ insets,
136
+ closeOnSelect,
137
+ style,
138
+ }: SelectProps) {
139
+ const theme = useTheme();
140
+ const sizeConfig = SIZES[size];
141
+
142
+ const isControlled = value !== undefined;
143
+ const [internal, setInternal] = useState<string | string[] | undefined>(
144
+ defaultValue ?? (multiple ? [] : undefined),
145
+ );
146
+ const current = isControlled ? value : internal;
147
+ const selectedKeys = multiple
148
+ ? ((current as string[] | undefined) ?? [])
149
+ : current != null
150
+ ? [current as string]
151
+ : [];
152
+
153
+ const commit = (next: string | string[]) => {
154
+ if (!isControlled) setInternal(next);
155
+ onChange?.(next);
156
+ };
157
+
158
+ const selectSingle = (key: string) => commit(key);
159
+ const toggleMulti = (key: string) => {
160
+ const set = new Set(selectedKeys);
161
+ if (set.has(key)) set.delete(key);
162
+ else set.add(key);
163
+ commit([...set]);
164
+ };
165
+
166
+ const selectedOptions = options.filter((o) => selectedKeys.includes(o.key));
167
+ const isEmpty = selectedOptions.length === 0;
168
+ const displayText = renderValue
169
+ ? renderValue(selectedOptions)
170
+ : isEmpty
171
+ ? placeholder
172
+ : selectedOptions.map((o) => o.label).join(', ');
173
+
174
+ const hasError = Boolean(error);
175
+ const message = typeof error === 'string' ? error : helperText;
176
+
177
+ const rows: DropdownEntry[] = options.map((o) => ({
178
+ key: o.key,
179
+ label: o.label,
180
+ disabled: o.disabled,
181
+ leading: o.icon ? { kind: 'icon', name: o.icon } : undefined,
182
+ trailing: multiple
183
+ ? { kind: 'checkbox', checked: selectedKeys.includes(o.key), onChange: () => toggleMulti(o.key) }
184
+ : selectedKeys.includes(o.key)
185
+ ? { kind: 'icon', name: 'check', color: theme.colors.feedback.success }
186
+ : undefined,
187
+ }));
188
+
189
+ const shouldClose = closeOnSelect ?? !multiple;
190
+
191
+ const iconBox: ViewStyle = {
192
+ width: sizeConfig.iconSize,
193
+ height: sizeConfig.iconSize,
194
+ alignItems: 'center',
195
+ justifyContent: 'center',
196
+ };
197
+
198
+ const containerWidth: ViewStyle =
199
+ width != null
200
+ ? { width, alignSelf: 'flex-start' }
201
+ : fullWidth
202
+ ? { alignSelf: 'stretch' }
203
+ : { alignSelf: 'flex-start' };
204
+
205
+ return (
206
+ <View style={[containerWidth, style]}>
207
+ {label ? (
208
+ <Text variant="bodySmall" color="secondary" style={{ marginBottom: 4, marginLeft: 8 }}>
209
+ {label}
210
+ </Text>
211
+ ) : null}
212
+
213
+ <Menu
214
+ anchorOrigin={anchorOrigin}
215
+ menuOrigin={menuOrigin}
216
+ offset={offset}
217
+ autoFit={autoFit}
218
+ insets={insets}
219
+ triggerStyle={{ alignSelf: 'stretch' }}
220
+ renderTrigger={({ open, toggle }) => {
221
+ const showRing = open && !disabled;
222
+ const borderColor = disabled
223
+ ? theme.colors.disabled.border
224
+ : hasError
225
+ ? theme.colors.feedback.error
226
+ : open
227
+ ? theme.colors.border.focus
228
+ : theme.colors.border.default;
229
+ const ringColor = hasError ? theme.colors.feedback.error : theme.colors.border.focus;
230
+ const boxShadow: ViewStyle['boxShadow'] = showRing
231
+ ? [
232
+ {
233
+ offsetX: 0,
234
+ offsetY: 0,
235
+ blurRadius: 0,
236
+ spreadDistance: FOCUS_RING_SPREAD,
237
+ color: hexToRgba(ringColor, FOCUS_RING_ALPHA),
238
+ },
239
+ ]
240
+ : undefined;
241
+ return (
242
+ <Pressable
243
+ onPress={toggle}
244
+ disabled={disabled}
245
+ accessibilityRole="button"
246
+ accessibilityState={{ expanded: open, disabled }}
247
+ accessibilityLabel={label ?? placeholder}
248
+ style={{
249
+ flexDirection: 'row',
250
+ alignItems: 'center',
251
+ gap: sizeConfig.gap,
252
+ height: sizeConfig.height,
253
+ paddingHorizontal: 16,
254
+ borderWidth: 1,
255
+ borderColor,
256
+ borderRadius: theme.radii.pill,
257
+ backgroundColor: disabled
258
+ ? theme.colors.disabled.background
259
+ : theme.colors.background.default,
260
+ boxShadow,
261
+ }}
262
+ >
263
+ {leftIcon ? (
264
+ <View style={iconBox}>
265
+ <Icon
266
+ name={leftIcon}
267
+ size={sizeConfig.iconSize}
268
+ color={disabled ? theme.colors.disabled.foreground : 'secondary'}
269
+ />
270
+ </View>
271
+ ) : null}
272
+
273
+ <Text
274
+ variant={sizeConfig.textVariant}
275
+ color={disabled ? 'tertiary' : isEmpty ? 'tertiary' : 'primary'}
276
+ numberOfLines={1}
277
+ style={{ flex: 1, minWidth: 0 } as TextStyle}
278
+ >
279
+ {displayText}
280
+ </Text>
281
+
282
+ <View
283
+ style={[iconBox, { transform: [{ rotate: open ? '180deg' : '0deg' }] }]}
284
+ >
285
+ <Icon
286
+ name="chevron-down"
287
+ size={sizeConfig.iconSize}
288
+ color={disabled ? theme.colors.disabled.foreground : 'secondary'}
289
+ />
290
+ </View>
291
+ </Pressable>
292
+ );
293
+ }}
294
+ >
295
+ {({ close, triggerWidth, maxHeight: autoMaxHeight }) => (
296
+ <Dropdown
297
+ options={rows}
298
+ width={matchTriggerWidth ? triggerWidth : menuWidth}
299
+ maxHeight={maxHeight ?? autoMaxHeight}
300
+ onSelect={
301
+ multiple
302
+ ? undefined
303
+ : (key) => {
304
+ selectSingle(key);
305
+ if (shouldClose) close();
306
+ }
307
+ }
308
+ />
309
+ )}
310
+ </Menu>
311
+
312
+ {message ? (
313
+ <Text
314
+ variant="bodySmall"
315
+ color={hasError ? theme.colors.feedback.error : 'tertiary'}
316
+ style={{ marginTop: 8, marginLeft: 8 }}
317
+ >
318
+ {message}
319
+ </Text>
320
+ ) : null}
321
+ </View>
322
+ );
323
+ }
@@ -0,0 +1,2 @@
1
+ export { Select } from './Select';
2
+ export type { SelectProps, SelectOption, SelectSize } from './Select';
@@ -0,0 +1,138 @@
1
+ import {
2
+ Pressable,
3
+ View,
4
+ type ImageSourcePropType,
5
+ type StyleProp,
6
+ type ViewStyle,
7
+ } from 'react-native';
8
+ import { useTheme } from '../../theme';
9
+ import type { TypographyVariant } from '../../theme/typography';
10
+ import { Avatar } from '../Avatar/Avatar';
11
+ import { Text } from '../Text/Text';
12
+
13
+ export type UserChipSize = 'sm' | 'md' | 'lg';
14
+
15
+ export interface UserChipProps {
16
+ /** Avatar image; when omitted or it fails to load, initials from `name` show. */
17
+ source?: ImageSourcePropType;
18
+ /**
19
+ * Name shown as the label and used for the avatar initials. When omitted, only
20
+ * the avatar renders (no pill) — the label's presence is driven by this prop.
21
+ */
22
+ name?: string;
23
+ /** Explicit initials for the avatar fallback (overrides `name`). */
24
+ initials?: string;
25
+ /** Size preset — scales the avatar, text and the fixed pill width. Defaults to `md`. */
26
+ size?: UserChipSize;
27
+ /** Pill background. Defaults to `action.primary` (lime). */
28
+ backgroundColor?: string;
29
+ /** Text color. Defaults to `action.secondaryForeground`. */
30
+ color?: string;
31
+ /** Make the chip pressable. */
32
+ onPress?: () => void;
33
+ /** Override the container style. */
34
+ style?: StyleProp<ViewStyle>;
35
+ /** Accessibility label. Defaults to `name`. */
36
+ accessibilityLabel?: string;
37
+ }
38
+
39
+ /**
40
+ * Per-size config. `width` is the fixed total pill width (the chip never grows or
41
+ * shrinks with the text — long names truncate). Avatar diameter equals the pill
42
+ * height, so it sits flush in the left circle.
43
+ */
44
+ const SIZES: Record<
45
+ UserChipSize,
46
+ { diameter: number; gap: number; paddingRight: number; width: number; textVariant: TypographyVariant }
47
+ > = {
48
+ sm: { diameter: 40, gap: 8, paddingRight: 16, width: 160, textVariant: 'bodyMedium' },
49
+ md: { diameter: 48, gap: 10, paddingRight: 20, width: 188, textVariant: 'bodyLarge' },
50
+ lg: { diameter: 58, gap: 12, paddingRight: 24, width: 212, textVariant: 'bodyLarge' },
51
+ };
52
+
53
+ /**
54
+ * User chip — an `Avatar` with an optional name. With a `name` it renders the
55
+ * lime pill from Figma "User": the avatar flush on the left and the name in a
56
+ * fixed-width box that is centered when short and truncates with an ellipsis when
57
+ * long. Without a `name` it is just the avatar. The pill width is fixed per size.
58
+ */
59
+ export function UserChip({
60
+ source,
61
+ name,
62
+ initials,
63
+ size = 'md',
64
+ backgroundColor,
65
+ color,
66
+ onPress,
67
+ style,
68
+ accessibilityLabel,
69
+ }: UserChipProps) {
70
+ const theme = useTheme();
71
+ const cfg = SIZES[size];
72
+ const hasName = Boolean(name && name.trim());
73
+
74
+ const avatar = (
75
+ <Avatar
76
+ source={source}
77
+ name={name}
78
+ initials={initials}
79
+ diameter={cfg.diameter}
80
+ accessibilityLabel={accessibilityLabel ?? name}
81
+ />
82
+ );
83
+
84
+ // No name → just the avatar, exactly like using <Avatar> directly.
85
+ if (!hasName) {
86
+ if (!onPress) return <View style={style}>{avatar}</View>;
87
+ return (
88
+ <Pressable onPress={onPress} accessibilityRole="button" accessibilityLabel={accessibilityLabel} style={style}>
89
+ {avatar}
90
+ </Pressable>
91
+ );
92
+ }
93
+
94
+ const pillStyle: ViewStyle = {
95
+ flexDirection: 'row',
96
+ alignItems: 'center',
97
+ gap: cfg.gap,
98
+ width: cfg.width,
99
+ height: cfg.diameter,
100
+ borderRadius: cfg.diameter / 2,
101
+ paddingRight: cfg.paddingRight,
102
+ backgroundColor: backgroundColor ?? theme.colors.action.primary,
103
+ };
104
+
105
+ const inner = (
106
+ <>
107
+ {avatar}
108
+ <Text
109
+ variant={cfg.textVariant}
110
+ weight="medium"
111
+ align="center"
112
+ color={color ?? theme.colors.action.secondaryForeground}
113
+ numberOfLines={1}
114
+ style={{ flex: 1 }}
115
+ >
116
+ {name}
117
+ </Text>
118
+ </>
119
+ );
120
+
121
+ if (!onPress) {
122
+ return (
123
+ <View accessibilityLabel={accessibilityLabel ?? name} style={[pillStyle, style]}>
124
+ {inner}
125
+ </View>
126
+ );
127
+ }
128
+ return (
129
+ <Pressable
130
+ onPress={onPress}
131
+ accessibilityRole="button"
132
+ accessibilityLabel={accessibilityLabel ?? name}
133
+ style={[pillStyle, style]}
134
+ >
135
+ {inner}
136
+ </Pressable>
137
+ );
138
+ }
@@ -0,0 +1,2 @@
1
+ export { UserChip } from './UserChip';
2
+ export type { UserChipProps, UserChipSize } from './UserChip';
@@ -7,18 +7,22 @@ export * from './Chip';
7
7
  export * from './CodeField';
8
8
  export * from './CodeInput';
9
9
  export * from './Dialog';
10
+ export * from './Dropdown';
10
11
  export * from './Icon';
11
12
  export * from './IconButton';
12
13
  export * from './Input';
13
14
  export * from './Keypad';
14
15
  export * from './LinkButton';
16
+ export * from './Menu';
15
17
  export * from './ProductCard';
16
18
  export * from './QuantityStepper';
17
19
  export * from './Radio';
20
+ export * from './Select';
18
21
  export * from './Spinner';
19
22
  export * from './Surface';
20
23
  export * from './Switch';
21
24
  export * from './Text';
22
25
  export * from './Toast';
26
+ export * from './UserChip';
23
27
 
24
28
  export type { LabelPosition } from './_internal/Labeled';