@himakarinv/nativex 1.0.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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/images.d.ts +14 -0
  4. package/index.js +8 -0
  5. package/nativewind-preset.js +18 -0
  6. package/package.json +120 -0
  7. package/src/assets/branding/logo.jpg +0 -0
  8. package/src/assets/branding/softech-logo.png +0 -0
  9. package/src/assets/icons/2-layers.svg +3 -0
  10. package/src/assets/icons/Forbidden.svg +211 -0
  11. package/src/assets/icons/Internal-server-error.svg +119 -0
  12. package/src/assets/icons/Log-out-nav.svg +4 -0
  13. package/src/assets/icons/Not-Found.svg +282 -0
  14. package/src/assets/icons/Unauthorized.svg +314 -0
  15. package/src/assets/icons/User-Frame.svg +5 -0
  16. package/src/assets/icons/alert.svg +4 -0
  17. package/src/assets/icons/arrow-right.svg +3 -0
  18. package/src/assets/icons/avatar-female.svg +10 -0
  19. package/src/assets/icons/avatar-male.svg +10 -0
  20. package/src/assets/icons/bell.svg +3 -0
  21. package/src/assets/icons/calendar-event.svg +11 -0
  22. package/src/assets/icons/calendar.svg +6 -0
  23. package/src/assets/icons/card.svg +4 -0
  24. package/src/assets/icons/caret-right.svg +3 -0
  25. package/src/assets/icons/chat.svg +11 -0
  26. package/src/assets/icons/check.svg +3 -0
  27. package/src/assets/icons/checkbox-minus.svg +3 -0
  28. package/src/assets/icons/checkmark.svg +3 -0
  29. package/src/assets/icons/chevron-down.svg +3 -0
  30. package/src/assets/icons/chevron-left.svg +9 -0
  31. package/src/assets/icons/chevron-right.svg +9 -0
  32. package/src/assets/icons/circle-radio-active.svg +10 -0
  33. package/src/assets/icons/circle-radio.svg +10 -0
  34. package/src/assets/icons/clock.svg +3 -0
  35. package/src/assets/icons/close.svg +3 -0
  36. package/src/assets/icons/code.svg +3 -0
  37. package/src/assets/icons/country.svg +20 -0
  38. package/src/assets/icons/dot.svg +3 -0
  39. package/src/assets/icons/download.svg +1 -0
  40. package/src/assets/icons/dropdown-arrow.svg +3 -0
  41. package/src/assets/icons/dropdown-avatar.svg +3 -0
  42. package/src/assets/icons/email.svg +3 -0
  43. package/src/assets/icons/error.svg +3 -0
  44. package/src/assets/icons/eye-off.svg +1 -0
  45. package/src/assets/icons/eye.svg +1 -0
  46. package/src/assets/icons/hashtag-icon.svg +3 -0
  47. package/src/assets/icons/help-circle.svg +10 -0
  48. package/src/assets/icons/help.svg +4 -0
  49. package/src/assets/icons/hide-checkbox.svg +3 -0
  50. package/src/assets/icons/home-smile.svg +3 -0
  51. package/src/assets/icons/home.svg +3 -0
  52. package/src/assets/icons/info.svg +5 -0
  53. package/src/assets/icons/left-arrow.svg +3 -0
  54. package/src/assets/icons/log-out.svg +3 -0
  55. package/src/assets/icons/notification-box.svg +4 -0
  56. package/src/assets/icons/pencil.svg +1 -0
  57. package/src/assets/icons/plus.svg +3 -0
  58. package/src/assets/icons/radio-dot.svg +3 -0
  59. package/src/assets/icons/right-arrow.svg +3 -0
  60. package/src/assets/icons/search.svg +3 -0
  61. package/src/assets/icons/settings.svg +11 -0
  62. package/src/assets/icons/slack.svg +17 -0
  63. package/src/assets/icons/slash.svg +3 -0
  64. package/src/assets/icons/success.svg +3 -0
  65. package/src/assets/icons/successicon.svg +5 -0
  66. package/src/assets/icons/three-dot-vertical.svg +5 -0
  67. package/src/assets/icons/trash.svg +1 -0
  68. package/src/assets/icons/trend-down-01.svg +3 -0
  69. package/src/assets/icons/trend-up-01.svg +3 -0
  70. package/src/assets/icons/up-arrow.svg +3 -0
  71. package/src/assets/icons/user-plus.svg +10 -0
  72. package/src/assets/icons/user.svg +3 -0
  73. package/src/assets/icons/users.svg +3 -0
  74. package/src/assets/icons/vertical-dots-true-icon.svg +5 -0
  75. package/src/assets/icons/vertical-line.svg +3 -0
  76. package/src/assets/icons/vertical-three-dots-icon.svg +5 -0
  77. package/src/assets/icons/warning.svg +3 -0
  78. package/src/assets/icons/zap.svg +3 -0
  79. package/src/components/ActionsMenu/ActionsMenu.tsx +314 -0
  80. package/src/components/ActionsMenu/index.ts +2 -0
  81. package/src/components/Alert/Alert.tsx +291 -0
  82. package/src/components/Alert/index.ts +2 -0
  83. package/src/components/Badge/Badge.tsx +230 -0
  84. package/src/components/Badge/index.ts +2 -0
  85. package/src/components/Breadcrumb/Breadcrumb.tsx +364 -0
  86. package/src/components/Breadcrumb/index.ts +7 -0
  87. package/src/components/Button/Button.tsx +584 -0
  88. package/src/components/Button/index.ts +9 -0
  89. package/src/components/Checkbox/Checkbox.tsx +336 -0
  90. package/src/components/Checkbox/index.ts +2 -0
  91. package/src/components/CreatePassword/CreatePassword.tsx +161 -0
  92. package/src/components/CreatePassword/index.ts +2 -0
  93. package/src/components/DataTable/DataTable.tsx +822 -0
  94. package/src/components/DataTable/index.ts +10 -0
  95. package/src/components/Datepicker/Datepicker.tsx +797 -0
  96. package/src/components/Datepicker/index.ts +2 -0
  97. package/src/components/Dropdown/Dropdown.tsx +277 -0
  98. package/src/components/Dropdown/index.ts +7 -0
  99. package/src/components/DropdownMenu/DropdownMenu.tsx +471 -0
  100. package/src/components/DropdownMenu/index.ts +11 -0
  101. package/src/components/DynamicForm/DynamicForm.tsx +741 -0
  102. package/src/components/DynamicForm/index.ts +13 -0
  103. package/src/components/ErrorPages/ErrorPages.tsx +135 -0
  104. package/src/components/ErrorPages/index.ts +2 -0
  105. package/src/components/ForgotPassword/ForgotPassword.tsx +121 -0
  106. package/src/components/ForgotPassword/index.ts +2 -0
  107. package/src/components/HintError/HintError.tsx +37 -0
  108. package/src/components/HintError/index.ts +2 -0
  109. package/src/components/Icon/Icon.tsx +43 -0
  110. package/src/components/Icon/icon-registry.ts +227 -0
  111. package/src/components/Icon/index.ts +3 -0
  112. package/src/components/InputDropdown/InputDropdown.tsx +662 -0
  113. package/src/components/InputDropdown/index.ts +9 -0
  114. package/src/components/InputField/InputField.tsx +527 -0
  115. package/src/components/InputField/index.ts +2 -0
  116. package/src/components/Label/Label.tsx +88 -0
  117. package/src/components/Label/index.ts +2 -0
  118. package/src/components/LoginForm/LoginForm.tsx +158 -0
  119. package/src/components/LoginForm/index.ts +2 -0
  120. package/src/components/LoginShell/LoginShell.tsx +97 -0
  121. package/src/components/LoginShell/RoleCard.tsx +24 -0
  122. package/src/components/LoginShell/SoftechLogo.tsx +24 -0
  123. package/src/components/LoginShell/branding-assets.ts +2 -0
  124. package/src/components/LoginShell/index.ts +20 -0
  125. package/src/components/LoginShell/login-copy.ts +45 -0
  126. package/src/components/LoginShell/loginPageStyles.ts +210 -0
  127. package/src/components/Modals/Modals.tsx +313 -0
  128. package/src/components/Modals/index.ts +2 -0
  129. package/src/components/Offcanvas/Offcanvas.tsx +404 -0
  130. package/src/components/Offcanvas/index.ts +13 -0
  131. package/src/components/PageHeader/PageHeader.tsx +128 -0
  132. package/src/components/PageHeader/index.ts +2 -0
  133. package/src/components/Pagination/Pagination.tsx +540 -0
  134. package/src/components/Pagination/index.ts +8 -0
  135. package/src/components/ResetPassword/ResetPassword.tsx +172 -0
  136. package/src/components/ResetPassword/index.ts +2 -0
  137. package/src/components/RoleSelection/RoleSelection.tsx +111 -0
  138. package/src/components/RoleSelection/index.ts +2 -0
  139. package/src/components/SendOtp/SendOtp.tsx +149 -0
  140. package/src/components/SendOtp/index.ts +2 -0
  141. package/src/components/Sidenavbar/Sidenavbar.tsx +358 -0
  142. package/src/components/Sidenavbar/index.ts +2 -0
  143. package/src/components/StatCard/StatCard.tsx +393 -0
  144. package/src/components/StatCard/index.ts +6 -0
  145. package/src/components/Tabs/Tabs.tsx +239 -0
  146. package/src/components/Tabs/index.ts +2 -0
  147. package/src/components/Textarea/Textarea.tsx +217 -0
  148. package/src/components/Textarea/index.ts +2 -0
  149. package/src/components/Timepicker/Timepicker.tsx +384 -0
  150. package/src/components/Timepicker/index.ts +2 -0
  151. package/src/components/Toast/Toast.tsx +278 -0
  152. package/src/components/Toast/index.ts +2 -0
  153. package/src/components/Toggle/Toggle.tsx +296 -0
  154. package/src/components/Toggle/index.ts +2 -0
  155. package/src/components/Typeahead/Typeahead.tsx +521 -0
  156. package/src/components/Typeahead/index.ts +2 -0
  157. package/src/components/Verification/Verification.tsx +374 -0
  158. package/src/components/Verification/index.ts +2 -0
  159. package/src/index.ts +39 -0
  160. package/src/theme/borders.ts +19 -0
  161. package/src/theme/button.ts +41 -0
  162. package/src/theme/colors.ts +273 -0
  163. package/src/theme/index.ts +14 -0
  164. package/src/theme/nativewind-theme.js +182 -0
  165. package/src/theme/radius.ts +12 -0
  166. package/src/theme/shadows.ts +40 -0
  167. package/src/theme/spacing.ts +18 -0
  168. package/src/theme/typography.ts +35 -0
  169. package/src/utils/a11y.ts +21 -0
  170. package/svg.d.ts +7 -0
@@ -0,0 +1,521 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Dimensions,
4
+ Modal,
5
+ Platform,
6
+ Pressable,
7
+ ScrollView,
8
+ StyleSheet,
9
+ Text,
10
+ View,
11
+ type NativeSyntheticEvent,
12
+ type StyleProp,
13
+ type TextInputKeyPressEventData,
14
+ type ViewStyle,
15
+ } from 'react-native';
16
+
17
+ import { borders, colors, radius, shadows, spacing, typography } from '../../theme';
18
+ import { Button, ButtonSize, ButtonVariant, IconPosition } from '../Button';
19
+ import { IconName } from '../Icon';
20
+ import { InputField, InputState } from '../InputField';
21
+
22
+ export type TypeaheadOption = {
23
+ id: number | string;
24
+ label: string;
25
+ email?: string;
26
+ };
27
+
28
+ export type TypeaheadValue = number | number[] | TypeaheadOption | TypeaheadOption[] | null;
29
+
30
+ export type TypeaheadProps = {
31
+ label?: string;
32
+ placeholder?: string;
33
+ required?: boolean;
34
+ disabled?: boolean;
35
+ fullWidth?: boolean;
36
+ helpText?: string;
37
+ error?: string;
38
+ state?: InputState;
39
+ multiple?: boolean;
40
+ inputId?: string;
41
+ seedLabel?: string;
42
+ searchFn?: (query: string) => Promise<TypeaheadOption[]>;
43
+ getOptionsByIdsFn?: (ids: number[]) => Promise<TypeaheadOption[]>;
44
+ value?: TypeaheadValue;
45
+ onValueChange?: (value: TypeaheadValue) => void;
46
+ style?: StyleProp<ViewStyle>;
47
+ };
48
+
49
+ const isNative = Platform.OS !== 'web';
50
+ const MIN_QUERY = 2;
51
+ const SEARCH_DELAY = 300;
52
+ const MENU_MAX_HEIGHT = spacing['7xl'] * 4 + spacing['2xl'];
53
+ const MENU_GAP = spacing.xs;
54
+ const FIELD_WIDTH = isNative ? 255 : 320;
55
+
56
+ function webMenuShadow(): ViewStyle {
57
+ return {
58
+ boxShadow: `0 ${spacing.xs}px ${spacing.md}px ${colors.boxShadowLight}`,
59
+ } as ViewStyle;
60
+ }
61
+
62
+ function fieldMenuBox(host: { x: number; y: number; width: number; height: number }, fullWidth: boolean) {
63
+ const width = fullWidth
64
+ ? Math.max(spacing.none, host.width - (borders.width.thick + borders.focusRing.width) * 2)
65
+ : FIELD_WIDTH;
66
+ const x = host.x + Math.max(spacing.none, (host.width - width) / 2);
67
+ return { x, y: host.y, width, height: host.height };
68
+ }
69
+
70
+ function menuPosition(
71
+ field: { x: number; y: number; width: number; height: number },
72
+ menuHeight: number,
73
+ ) {
74
+ const window = Dimensions.get('window');
75
+ const pad = spacing.lg;
76
+ const left = Math.min(Math.max(pad, field.x), window.width - field.width - pad);
77
+ const below = field.y + field.height + MENU_GAP;
78
+ const above = field.y - menuHeight - MENU_GAP;
79
+ const top = below + menuHeight + pad <= window.height ? below : Math.max(pad, above);
80
+ return { top, left };
81
+ }
82
+
83
+ function optionIds(value: TypeaheadValue): number[] | null {
84
+ if (value == null) {
85
+ return null;
86
+ }
87
+ if (Array.isArray(value)) {
88
+ if (value.length === 0) {
89
+ return [];
90
+ }
91
+ if (typeof value[0] === 'object' && value[0] && 'id' in value[0]) {
92
+ return null;
93
+ }
94
+ return value as number[];
95
+ }
96
+ if (typeof value === 'object' && 'id' in value) {
97
+ return null;
98
+ }
99
+ if (typeof value === 'number') {
100
+ return [value];
101
+ }
102
+ return null;
103
+ }
104
+
105
+ function optionsFromValue(value: TypeaheadValue): TypeaheadOption[] {
106
+ if (value == null) {
107
+ return [];
108
+ }
109
+ if (Array.isArray(value)) {
110
+ if (value.length > 0 && typeof value[0] === 'object' && value[0] && 'id' in value[0]) {
111
+ return value as TypeaheadOption[];
112
+ }
113
+ return [];
114
+ }
115
+ if (typeof value === 'object' && 'id' in value) {
116
+ return [value];
117
+ }
118
+ return [];
119
+ }
120
+
121
+ function sameOption(left: TypeaheadOption, right: TypeaheadOption) {
122
+ return String(left.id) === String(right.id);
123
+ }
124
+
125
+ function emitValue(options: TypeaheadOption[], multiple: boolean): TypeaheadValue {
126
+ if (options.length === 0) {
127
+ return null;
128
+ }
129
+ const numeric = options.every((item) => typeof item.id === 'number' || !Number.isNaN(Number(item.id)));
130
+ if (multiple) {
131
+ return numeric ? options.map((item) => Number(item.id)) : options;
132
+ }
133
+ const first = options[0];
134
+ const numericId = Number(first.id);
135
+ return Number.isFinite(numericId) ? numericId : first;
136
+ }
137
+
138
+ export function Typeahead({
139
+ label = '',
140
+ placeholder = '',
141
+ required = false,
142
+ disabled = false,
143
+ fullWidth = false,
144
+ helpText = '',
145
+ error = '',
146
+ state = InputState.Default,
147
+ multiple = false,
148
+ inputId,
149
+ seedLabel = '',
150
+ searchFn,
151
+ getOptionsByIdsFn,
152
+ value,
153
+ onValueChange,
154
+ style,
155
+ }: TypeaheadProps) {
156
+ const [inputValue, setInputValue] = useState('');
157
+ const [results, setResults] = useState<TypeaheadOption[]>([]);
158
+ const [open, setOpen] = useState(false);
159
+ const [activeIndex, setActiveIndex] = useState(-1);
160
+ const [selectedOptions, setSelectedOptions] = useState<TypeaheadOption[]>([]);
161
+ const [triggerBox, setTriggerBox] = useState({ x: 0, y: 0, width: 0, height: 0 });
162
+ const [menuHeight, setMenuHeight] = useState(0);
163
+
164
+ const hostRef = useRef<View>(null);
165
+ const searchTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
166
+ const searchGeneration = useRef(0);
167
+ const pendingIds = useRef<number[] | null>(null);
168
+
169
+ const effectiveState = error ? InputState.Error : state;
170
+
171
+ useEffect(() => {
172
+ if (value === undefined) {
173
+ return;
174
+ }
175
+
176
+ if (value == null) {
177
+ pendingIds.current = null;
178
+ setSelectedOptions([]);
179
+ setInputValue('');
180
+ return;
181
+ }
182
+
183
+ const fromValue = optionsFromValue(value);
184
+ if (fromValue.length > 0) {
185
+ pendingIds.current = null;
186
+ setSelectedOptions(fromValue);
187
+ setInputValue(multiple ? '' : fromValue[0].label);
188
+ return;
189
+ }
190
+
191
+ const ids = optionIds(value);
192
+ if (!ids) {
193
+ return;
194
+ }
195
+
196
+ pendingIds.current = ids;
197
+ const seed = seedLabel.trim();
198
+ if (!multiple && seed) {
199
+ setInputValue(seed);
200
+ } else if (multiple) {
201
+ setInputValue('');
202
+ }
203
+
204
+ if (!ids.length || !getOptionsByIdsFn) {
205
+ setSelectedOptions([]);
206
+ return;
207
+ }
208
+
209
+ let cancelled = false;
210
+ getOptionsByIdsFn(ids)
211
+ .then((options) => {
212
+ if (cancelled || JSON.stringify(pendingIds.current) !== JSON.stringify(ids)) {
213
+ return;
214
+ }
215
+ setSelectedOptions(options);
216
+ if (!multiple && options[0]) {
217
+ setInputValue(options[0].label);
218
+ }
219
+ })
220
+ .catch(() => {
221
+ if (!cancelled) {
222
+ setSelectedOptions([]);
223
+ }
224
+ });
225
+
226
+ return () => {
227
+ cancelled = true;
228
+ };
229
+ }, [getOptionsByIdsFn, multiple, seedLabel, value]);
230
+
231
+ useEffect(() => {
232
+ return () => {
233
+ if (searchTimer.current) {
234
+ clearTimeout(searchTimer.current);
235
+ }
236
+ };
237
+ }, []);
238
+
239
+ const close = () => {
240
+ setOpen(false);
241
+ setActiveIndex(-1);
242
+ };
243
+
244
+ const openMenu = () => {
245
+ hostRef.current?.measureInWindow((x, y, width, height) => {
246
+ setTriggerBox({ x, y, width, height });
247
+ setOpen(true);
248
+ });
249
+ };
250
+
251
+ const search = (term: string) => {
252
+ if (searchTimer.current) {
253
+ clearTimeout(searchTimer.current);
254
+ searchTimer.current = null;
255
+ }
256
+
257
+ if (!term || term.length < MIN_QUERY || !searchFn) {
258
+ searchGeneration.current += 1;
259
+ setResults([]);
260
+ close();
261
+ return;
262
+ }
263
+
264
+ const generation = (searchGeneration.current += 1);
265
+ searchTimer.current = setTimeout(() => {
266
+ searchFn(term)
267
+ .then((next) => {
268
+ if (generation !== searchGeneration.current) {
269
+ return;
270
+ }
271
+ setResults(next);
272
+ setActiveIndex(-1);
273
+ if (next.length > 0) {
274
+ openMenu();
275
+ } else {
276
+ close();
277
+ }
278
+ })
279
+ .catch(() => {
280
+ if (generation !== searchGeneration.current) {
281
+ return;
282
+ }
283
+ setResults([]);
284
+ close();
285
+ });
286
+ }, SEARCH_DELAY);
287
+ };
288
+
289
+ const commit = (next: TypeaheadOption[]) => {
290
+ setSelectedOptions(next);
291
+ onValueChange?.(emitValue(next, multiple));
292
+ };
293
+
294
+ const selectOption = (option: TypeaheadOption) => {
295
+ if (multiple) {
296
+ if (!selectedOptions.some((item) => sameOption(item, option))) {
297
+ commit([...selectedOptions, option]);
298
+ }
299
+ setInputValue('');
300
+ } else {
301
+ commit([option]);
302
+ setInputValue(option.label);
303
+ }
304
+ setResults([]);
305
+ close();
306
+ };
307
+
308
+ const removeOption = (option: TypeaheadOption) => {
309
+ commit(selectedOptions.filter((item) => !sameOption(item, option)));
310
+ };
311
+
312
+ const onKeyPress = (event: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
313
+ if (!open) {
314
+ return;
315
+ }
316
+
317
+ const key = event.nativeEvent.key;
318
+ if (key === 'ArrowDown') {
319
+ event.preventDefault?.();
320
+ setActiveIndex((current) => Math.min(current + 1, results.length - 1));
321
+ return;
322
+ }
323
+ if (key === 'ArrowUp') {
324
+ event.preventDefault?.();
325
+ setActiveIndex((current) => Math.max(current - 1, 0));
326
+ return;
327
+ }
328
+ if (key === 'Enter' && activeIndex >= 0 && results[activeIndex]) {
329
+ event.preventDefault?.();
330
+ selectOption(results[activeIndex]);
331
+ return;
332
+ }
333
+ if (key === 'Escape') {
334
+ close();
335
+ }
336
+ };
337
+
338
+ const fieldBox = fieldMenuBox(triggerBox, fullWidth);
339
+ const position = menuPosition(fieldBox, menuHeight);
340
+ const menuWidth = fieldBox.width;
341
+
342
+ return (
343
+ <View
344
+ ref={hostRef}
345
+ collapsable={false}
346
+ style={[styles.host, fullWidth ? styles.fullWidth : styles.fixedWidth, style]}>
347
+ <InputField
348
+ label={label}
349
+ required={required}
350
+ placeholder={placeholder}
351
+ hint={helpText}
352
+ error={error}
353
+ state={effectiveState}
354
+ disabled={disabled}
355
+ value={inputValue}
356
+ inputId={inputId}
357
+ fullWidth={fullWidth}
358
+ autoComplete="off"
359
+ accessibilityLabel={label || placeholder}
360
+ onValueChange={(next) => {
361
+ setInputValue(next);
362
+ search(next);
363
+ }}
364
+ onKeyPress={onKeyPress}
365
+ />
366
+
367
+ {multiple && selectedOptions.length > 0 ? (
368
+ <View style={styles.tags}>
369
+ {selectedOptions.map((option) => (
370
+ <View key={String(option.id)} style={styles.tag}>
371
+ <Text style={styles.tagLabel}>{option.label}</Text>
372
+ <Button
373
+ label=""
374
+ variant={ButtonVariant.TertiaryGray}
375
+ size={ButtonSize.Small}
376
+ iconPosition={IconPosition.Only}
377
+ iconName={IconName.Close}
378
+ iconDecorative
379
+ disabled={disabled}
380
+ accessibilityLabel={`Remove ${option.label}`}
381
+ onPress={() => removeOption(option)}
382
+ contentStyle={styles.tagClose}
383
+ />
384
+ </View>
385
+ ))}
386
+ </View>
387
+ ) : null}
388
+
389
+ <Modal visible={open && results.length > 0} transparent animationType="none" onRequestClose={close}>
390
+ <Pressable accessible={false} style={styles.backdrop} onPress={close}>
391
+ <Pressable
392
+ accessible={false}
393
+ accessibilityRole="menu"
394
+ onPress={(event) => {
395
+ if ('stopPropagation' in event && typeof event.stopPropagation === 'function') {
396
+ event.stopPropagation();
397
+ }
398
+ }}
399
+ onLayout={(event) => {
400
+ const nextHeight = event.nativeEvent.layout.height;
401
+ if (nextHeight !== menuHeight) {
402
+ setMenuHeight(nextHeight);
403
+ }
404
+ }}
405
+ style={[
406
+ styles.menu,
407
+ {
408
+ top: position.top,
409
+ left: position.left,
410
+ width: menuWidth,
411
+ maxWidth: menuWidth,
412
+ maxHeight: MENU_MAX_HEIGHT,
413
+ },
414
+ !isNative && webMenuShadow(),
415
+ ]}>
416
+ <ScrollView keyboardShouldPersistTaps="handled">
417
+ {results.map((option, index) => {
418
+ const active = index === activeIndex;
419
+ return (
420
+ <Pressable
421
+ key={String(option.id)}
422
+ accessibilityRole="menuitem"
423
+ accessibilityLabel={option.email ? `${option.label}, ${option.email}` : option.label}
424
+ onHoverIn={() => setActiveIndex(index)}
425
+ onPress={() => selectOption(option)}
426
+ style={[styles.option, active && styles.optionActive]}>
427
+ <Text style={styles.optionLabel} numberOfLines={1}>
428
+ {option.label}
429
+ </Text>
430
+ {option.email ? (
431
+ <Text style={styles.optionEmail} numberOfLines={1}>
432
+ {option.email}
433
+ </Text>
434
+ ) : null}
435
+ </Pressable>
436
+ );
437
+ })}
438
+ </ScrollView>
439
+ </Pressable>
440
+ </Pressable>
441
+ </Modal>
442
+ </View>
443
+ );
444
+ }
445
+
446
+ const styles = StyleSheet.create({
447
+ host: {
448
+ position: 'relative',
449
+ alignSelf: 'flex-start',
450
+ overflow: 'visible',
451
+ gap: spacing.xs,
452
+ },
453
+ fixedWidth: {
454
+ maxWidth: '100%',
455
+ },
456
+ fullWidth: {
457
+ width: '100%',
458
+ maxWidth: '100%',
459
+ },
460
+ backdrop: {
461
+ flex: 1,
462
+ },
463
+ menu: {
464
+ position: 'absolute',
465
+ backgroundColor: colors.white,
466
+ borderWidth: borders.width.thin,
467
+ borderColor: colors.input.border,
468
+ borderRadius: radius.md,
469
+ overflow: 'hidden',
470
+ },
471
+ option: {
472
+ width: '100%',
473
+ minHeight: spacing['4xl'],
474
+ justifyContent: 'center',
475
+ paddingVertical: spacing.sm,
476
+ paddingHorizontal: spacing.base,
477
+ },
478
+ optionActive: {
479
+ backgroundColor: colors.primary.background,
480
+ },
481
+ optionLabel: {
482
+ color: colors.input.text,
483
+ fontFamily: typography.fontFamily.base,
484
+ fontSize: typography.fontSize.md,
485
+ lineHeight: typography.fontSize.md * typography.lineHeight.normal,
486
+ },
487
+ optionEmail: {
488
+ color: colors.text.muted,
489
+ fontFamily: typography.fontFamily.base,
490
+ fontSize: typography.fontSize.sm,
491
+ lineHeight: typography.fontSize.sm * typography.lineHeight.normal,
492
+ },
493
+ tags: {
494
+ flexDirection: 'row',
495
+ flexWrap: 'wrap',
496
+ gap: spacing.xs,
497
+ },
498
+ tag: {
499
+ flexDirection: 'row',
500
+ alignItems: 'center',
501
+ gap: spacing.xs,
502
+ paddingVertical: spacing.xs,
503
+ paddingHorizontal: spacing.sm,
504
+ backgroundColor: colors.primary.background,
505
+ borderWidth: borders.width.thin,
506
+ borderColor: colors.input.border,
507
+ borderRadius: radius.pill,
508
+ },
509
+ tagLabel: {
510
+ color: colors.interactive.accent,
511
+ fontFamily: typography.fontFamily.base,
512
+ fontSize: typography.fontSize.sm,
513
+ lineHeight: typography.fontSize.sm * typography.lineHeight.normal,
514
+ },
515
+ tagClose: {
516
+ width: spacing.xl,
517
+ height: spacing.xl,
518
+ minHeight: spacing.xl,
519
+ padding: spacing.none,
520
+ },
521
+ });
@@ -0,0 +1,2 @@
1
+ export { Typeahead } from './Typeahead';
2
+ export type { TypeaheadOption, TypeaheadProps, TypeaheadValue } from './Typeahead';