@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,662 @@
1
+ import { useEffect, useMemo, useRef, useState, type RefObject } from 'react';
2
+ import {
3
+ Dimensions,
4
+ Modal,
5
+ Platform,
6
+ Pressable,
7
+ ScrollView,
8
+ StyleSheet,
9
+ Text,
10
+ TextInput,
11
+ View,
12
+ type StyleProp,
13
+ type TextStyle,
14
+ type ViewStyle,
15
+ } from 'react-native';
16
+
17
+ import { borders, colors, radius, shadows, spacing, typography } from '../../theme';
18
+ import { Icon, IconName } from '../Icon';
19
+ import { Label } from '../Label';
20
+
21
+ export const InputDropdownLayout = {
22
+ Default: 'default',
23
+ YearMonth: 'year-month',
24
+ } as const;
25
+
26
+ export type InputDropdownLayout = (typeof InputDropdownLayout)[keyof typeof InputDropdownLayout];
27
+
28
+ export const InputDropdownSize = {
29
+ XSmall: 'xs',
30
+ Small: 'sm',
31
+ Medium: 'md',
32
+ Large: 'lg',
33
+ } as const;
34
+
35
+ export type InputDropdownSize = (typeof InputDropdownSize)[keyof typeof InputDropdownSize];
36
+
37
+ export const InputDropdownIconType = {
38
+ Default: 'default',
39
+ User: 'user',
40
+ Search: 'search',
41
+ Dot: 'dot',
42
+ } as const;
43
+
44
+ export type InputDropdownIconType =
45
+ (typeof InputDropdownIconType)[keyof typeof InputDropdownIconType];
46
+
47
+ export type InputDropdownItem = {
48
+ label: string;
49
+ value?: string;
50
+ handle?: string;
51
+ };
52
+
53
+ export type MonthYearValue = {
54
+ year: string;
55
+ month: string;
56
+ };
57
+
58
+ export type InputDropdownProps = {
59
+ layout?: InputDropdownLayout;
60
+ label?: string;
61
+ required?: boolean;
62
+ suffix?: boolean;
63
+ iconType?: InputDropdownIconType;
64
+ options?: InputDropdownItem[];
65
+ placeholder?: string;
66
+ searchable?: boolean;
67
+ size?: InputDropdownSize;
68
+ selectedValue?: string;
69
+ disabled?: boolean;
70
+ yearLabel?: string;
71
+ monthLabel?: string;
72
+ yearPlaceholder?: string;
73
+ monthPlaceholder?: string;
74
+ yearOptions?: InputDropdownItem[];
75
+ monthOptions?: InputDropdownItem[];
76
+ selectedYear?: string;
77
+ selectedMonth?: string;
78
+ onSelectionChange?: (item: InputDropdownItem | null) => void;
79
+ onYearChange?: (item: InputDropdownItem | null) => void;
80
+ onMonthChange?: (item: InputDropdownItem | null) => void;
81
+ onValueChange?: (value: MonthYearValue) => void;
82
+ style?: StyleProp<ViewStyle>;
83
+ };
84
+
85
+ type MonthYearField = 'year' | 'month';
86
+
87
+ const isNative = Platform.OS !== 'web';
88
+ const MENU_MAX_HEIGHT = spacing['7xl'] * 7 + spacing.md;
89
+ const MENU_GAP = spacing.sm + spacing.xs;
90
+
91
+ export function buildMonthDropdownOptions(): InputDropdownItem[] {
92
+ return [
93
+ { label: 'January', value: '01' },
94
+ { label: 'February', value: '02' },
95
+ { label: 'March', value: '03' },
96
+ { label: 'April', value: '04' },
97
+ { label: 'May', value: '05' },
98
+ { label: 'June', value: '06' },
99
+ { label: 'July', value: '07' },
100
+ { label: 'August', value: '08' },
101
+ { label: 'September', value: '09' },
102
+ { label: 'October', value: '10' },
103
+ { label: 'November', value: '11' },
104
+ { label: 'December', value: '12' },
105
+ ];
106
+ }
107
+
108
+ export function buildYearDropdownOptions(
109
+ startYear = new Date().getFullYear() - 5,
110
+ endYear = new Date().getFullYear() + 5,
111
+ ): InputDropdownItem[] {
112
+ const from = Math.min(startYear, endYear);
113
+ const to = Math.max(startYear, endYear);
114
+ const options: InputDropdownItem[] = [];
115
+ for (let year = from; year <= to; year += 1) {
116
+ options.push({ label: String(year), value: String(year) });
117
+ }
118
+ return options;
119
+ }
120
+
121
+ function sizeTokens(size: InputDropdownSize) {
122
+ const compact = size === InputDropdownSize.XSmall || size === InputDropdownSize.Small;
123
+ const large = size === InputDropdownSize.Large;
124
+
125
+ return {
126
+ width:
127
+ size === InputDropdownSize.XSmall
128
+ ? spacing['7xl'] * 2 + spacing['2xl']
129
+ : size === InputDropdownSize.Small
130
+ ? spacing['7xl'] * 3
131
+ : size === InputDropdownSize.Medium
132
+ ? spacing['7xl'] * 4
133
+ : spacing['7xl'] * 5,
134
+ height: compact ? spacing['3xl'] : large ? spacing['5xl'] : spacing['4xl'],
135
+ paddingX: compact ? spacing.md : spacing.base,
136
+ fontSize: compact ? typography.fontSize.sm : large ? typography.fontSize.base : typography.fontSize.md,
137
+ icon: compact ? spacing.base : spacing.lg,
138
+ };
139
+ }
140
+
141
+ function fieldHitSlop(height: number) {
142
+ const extra = Math.max(spacing.none, (spacing['7xl'] - height) / 2);
143
+ return { top: extra, bottom: extra, left: spacing.none, right: spacing.none };
144
+ }
145
+
146
+ function prefixIconName(iconType: InputDropdownIconType) {
147
+ if (iconType === InputDropdownIconType.User) {
148
+ return IconName.User;
149
+ }
150
+ if (iconType === InputDropdownIconType.Search) {
151
+ return IconName.Search;
152
+ }
153
+ if (iconType === InputDropdownIconType.Dot) {
154
+ return IconName.Dot;
155
+ }
156
+ return '';
157
+ }
158
+
159
+ function itemSuffix(item: InputDropdownItem, showGenerated: boolean) {
160
+ if (item.handle) {
161
+ return item.handle;
162
+ }
163
+ if (!showGenerated) {
164
+ return '';
165
+ }
166
+ const first = item.label.trim().split(/\s+/)[0] || item.label.trim();
167
+ return first ? `@${first}` : '';
168
+ }
169
+
170
+ function sameItem(left: InputDropdownItem | null, right: InputDropdownItem) {
171
+ if (!left) {
172
+ return false;
173
+ }
174
+ return left.value === right.value || (left.label === right.label && !left.value && !right.value);
175
+ }
176
+
177
+ function findByValue(options: InputDropdownItem[], value?: string) {
178
+ if (value === undefined || value === '') {
179
+ return null;
180
+ }
181
+ return options.find((item) => item.value === value) ?? null;
182
+ }
183
+
184
+ function webFocusRing(): ViewStyle {
185
+ return {
186
+ boxShadow: `0 0 0 ${borders.focusRing.width}px ${colors.focus}`,
187
+ } as ViewStyle;
188
+ }
189
+
190
+ function webMenuShadow(): ViewStyle {
191
+ return {
192
+ boxShadow: `0 ${shadows.boxLight.offsetY}px ${shadows.boxLight.radius}px ${colors.boxShadowLight}`,
193
+ } as ViewStyle;
194
+ }
195
+
196
+ function nativeRingStyle(): ViewStyle {
197
+ return {
198
+ position: 'absolute',
199
+ top: -borders.focusRing.width,
200
+ right: -borders.focusRing.width,
201
+ bottom: -borders.focusRing.width,
202
+ left: -borders.focusRing.width,
203
+ borderRadius: radius.md + borders.focusRing.width,
204
+ borderWidth: borders.focusRing.width,
205
+ borderColor: colors.focus,
206
+ zIndex: 0,
207
+ };
208
+ }
209
+
210
+ function menuPosition(
211
+ trigger: { x: number; y: number; width: number; height: number },
212
+ menu: { width: number; height: number },
213
+ ) {
214
+ const window = Dimensions.get('window');
215
+ const pad = spacing.lg;
216
+ const left = Math.min(Math.max(pad, trigger.x), window.width - menu.width - pad);
217
+ const top = Math.min(
218
+ Math.max(pad, trigger.y + trigger.height + MENU_GAP),
219
+ window.height - menu.height - pad,
220
+ );
221
+ return { top, left };
222
+ }
223
+
224
+ export function InputDropdown({
225
+ layout = InputDropdownLayout.Default,
226
+ label = 'Team member',
227
+ required = false,
228
+ suffix = true,
229
+ iconType = InputDropdownIconType.Default,
230
+ options = [],
231
+ placeholder = 'Select team member',
232
+ searchable = false,
233
+ size = InputDropdownSize.Medium,
234
+ selectedValue,
235
+ disabled = false,
236
+ yearLabel = 'Year',
237
+ monthLabel = 'Month',
238
+ yearPlaceholder = 'Select year',
239
+ monthPlaceholder = 'Select month',
240
+ yearOptions,
241
+ monthOptions,
242
+ selectedYear,
243
+ selectedMonth,
244
+ onSelectionChange,
245
+ onYearChange,
246
+ onMonthChange,
247
+ onValueChange,
248
+ style,
249
+ }: InputDropdownProps) {
250
+ const isYearMonth = layout === InputDropdownLayout.YearMonth;
251
+ const years = yearOptions ?? buildYearDropdownOptions();
252
+ const months = monthOptions ?? buildMonthDropdownOptions();
253
+ const now = useMemo(() => new Date(), []);
254
+ const defaultYear = String(now.getFullYear());
255
+ const defaultMonth = String(now.getMonth() + 1).padStart(2, '0');
256
+
257
+ const [open, setOpen] = useState(false);
258
+ const [openField, setOpenField] = useState<MonthYearField | null>(null);
259
+ const [selected, setSelected] = useState<InputDropdownItem | null>(() => findByValue(options, selectedValue));
260
+ const [yearItem, setYearItem] = useState<InputDropdownItem | null>(() =>
261
+ findByValue(years, selectedYear ?? defaultYear),
262
+ );
263
+ const [monthItem, setMonthItem] = useState<InputDropdownItem | null>(() =>
264
+ findByValue(months, selectedMonth ?? defaultMonth),
265
+ );
266
+ const [searchText, setSearchText] = useState('');
267
+ const [hovered, setHovered] = useState<string | null>(null);
268
+ const [triggerBox, setTriggerBox] = useState({ x: 0, y: 0, width: 0, height: 0 });
269
+ const [menuSize, setMenuSize] = useState({ width: 0, height: 0 });
270
+
271
+ const defaultRef = useRef<View>(null);
272
+ const yearRef = useRef<View>(null);
273
+ const monthRef = useRef<View>(null);
274
+ const tokens = sizeTokens(size);
275
+ const prefix = prefixIconName(iconType);
276
+ const showItemIcon = iconType === InputDropdownIconType.User || iconType === InputDropdownIconType.Dot;
277
+ const filtered = useMemo(() => {
278
+ if (!searchable || !searchText.trim()) {
279
+ return options;
280
+ }
281
+ const term = searchText.toLowerCase();
282
+ return options.filter((item) => item.label.toLowerCase().includes(term));
283
+ }, [options, searchable, searchText]);
284
+
285
+ useEffect(() => {
286
+ if (selectedValue !== undefined) {
287
+ setSelected(findByValue(options, selectedValue));
288
+ }
289
+ }, [options, selectedValue]);
290
+
291
+ useEffect(() => {
292
+ if (selectedYear !== undefined) {
293
+ setYearItem(findByValue(years, selectedYear));
294
+ }
295
+ }, [selectedYear, years]);
296
+
297
+ useEffect(() => {
298
+ if (selectedMonth !== undefined) {
299
+ setMonthItem(findByValue(months, selectedMonth));
300
+ }
301
+ }, [selectedMonth, months]);
302
+
303
+ const close = () => {
304
+ setOpen(false);
305
+ setOpenField(null);
306
+ setHovered(null);
307
+ };
308
+
309
+ const openAt = (host: View | null, field: MonthYearField | null) => {
310
+ host?.measureInWindow((x, y, width, height) => {
311
+ setTriggerBox({ x, y, width, height });
312
+ setOpenField(field);
313
+ setOpen(true);
314
+ });
315
+ };
316
+
317
+ const toggleDefault = () => {
318
+ if (disabled) {
319
+ return;
320
+ }
321
+ if (open && !openField) {
322
+ close();
323
+ return;
324
+ }
325
+ openAt(defaultRef.current, null);
326
+ };
327
+
328
+ const toggleField = (field: MonthYearField) => {
329
+ if (disabled) {
330
+ return;
331
+ }
332
+ if (openField === field) {
333
+ close();
334
+ return;
335
+ }
336
+ openAt(field === 'year' ? yearRef.current : monthRef.current, field);
337
+ };
338
+
339
+ const selectDefault = (item: InputDropdownItem) => {
340
+ setSelected(item);
341
+ if (searchable) {
342
+ setSearchText(item.label);
343
+ }
344
+ onSelectionChange?.(item);
345
+ close();
346
+ };
347
+
348
+ const selectField = (field: MonthYearField, item: InputDropdownItem) => {
349
+ const nextYear = field === 'year' ? (item.value ?? '') : (yearItem?.value ?? '');
350
+ const nextMonth = field === 'month' ? (item.value ?? '') : (monthItem?.value ?? '');
351
+ if (field === 'year') {
352
+ setYearItem(item);
353
+ onYearChange?.(item);
354
+ } else {
355
+ setMonthItem(item);
356
+ onMonthChange?.(item);
357
+ }
358
+ close();
359
+ onValueChange?.({ year: nextYear, month: nextMonth });
360
+ };
361
+
362
+ const activeOptions = openField === 'year' ? years : openField === 'month' ? months : filtered;
363
+ const activeSelected =
364
+ openField === 'year' ? yearItem : openField === 'month' ? monthItem : selected;
365
+ const position = menuPosition(triggerBox, {
366
+ width: menuSize.width || triggerBox.width,
367
+ height: menuSize.height,
368
+ });
369
+
370
+ const renderMenu = () => (
371
+ <Modal visible={open} transparent animationType="none" onRequestClose={close}>
372
+ <Pressable accessible={false} style={styles.backdrop} onPress={close}>
373
+ <Pressable
374
+ accessible={false}
375
+ accessibilityRole="menu"
376
+ onPress={(event) => {
377
+ if ('stopPropagation' in event && typeof event.stopPropagation === 'function') {
378
+ event.stopPropagation();
379
+ }
380
+ }}
381
+ onLayout={(event) => {
382
+ const { width, height } = event.nativeEvent.layout;
383
+ if (width !== menuSize.width || height !== menuSize.height) {
384
+ setMenuSize({ width, height });
385
+ }
386
+ }}
387
+ style={[
388
+ styles.panel,
389
+ {
390
+ top: position.top,
391
+ left: position.left,
392
+ width: Math.max(tokens.width, triggerBox.width),
393
+ maxHeight: MENU_MAX_HEIGHT,
394
+ },
395
+ !isNative && webMenuShadow(),
396
+ ]}>
397
+ <ScrollView keyboardShouldPersistTaps="handled">
398
+ {activeOptions.map((item) => {
399
+ const key = item.value ?? item.label;
400
+ const selectedItem = sameItem(activeSelected, item);
401
+ const isHovered = hovered === key && !selectedItem;
402
+ const extra = itemSuffix(item, suffix && !openField);
403
+
404
+ return (
405
+ <Pressable
406
+ key={key}
407
+ accessibilityRole="menuitem"
408
+ accessibilityState={{ selected: selectedItem }}
409
+ accessibilityLabel={item.label}
410
+ onHoverIn={() => setHovered(key)}
411
+ onHoverOut={() => setHovered((current) => (current === key ? null : current))}
412
+ onPress={() => {
413
+ if (openField) {
414
+ selectField(openField, item);
415
+ } else {
416
+ selectDefault(item);
417
+ }
418
+ }}
419
+ style={[
420
+ styles.item,
421
+ {
422
+ minHeight: tokens.height,
423
+ paddingHorizontal: tokens.paddingX,
424
+ },
425
+ selectedItem && styles.itemSelected,
426
+ isHovered && styles.itemHovered,
427
+ ]}>
428
+ {showItemIcon && !openField && prefix ? (
429
+ <Icon name={prefix} size={tokens.icon} color={colors.inputDropdown.text} decorative />
430
+ ) : null}
431
+ <Text style={[styles.itemLabel, { fontSize: tokens.fontSize }]} numberOfLines={1}>
432
+ {item.label}
433
+ {extra ? <Text style={styles.itemSuffix}>{` ${extra}`}</Text> : null}
434
+ </Text>
435
+ {selectedItem && (suffix || openField) ? (
436
+ <Icon name={IconName.Check} size={tokens.icon} color={colors.inputDropdown.text} decorative />
437
+ ) : null}
438
+ </Pressable>
439
+ );
440
+ })}
441
+ </ScrollView>
442
+ </Pressable>
443
+ </Pressable>
444
+ </Modal>
445
+ );
446
+
447
+ const renderField = ({
448
+ hostRef,
449
+ fieldLabel,
450
+ fieldPlaceholder,
451
+ selectedItem,
452
+ fieldOpen,
453
+ showSearch,
454
+ onPress,
455
+ }: {
456
+ hostRef: RefObject<View | null>;
457
+ fieldLabel: string;
458
+ fieldPlaceholder: string;
459
+ selectedItem: InputDropdownItem | null;
460
+ fieldOpen: boolean;
461
+ showSearch: boolean;
462
+ onPress: () => void;
463
+ }) => {
464
+ const extra = selectedItem ? itemSuffix(selectedItem, suffix) : '';
465
+ const showRing = fieldOpen && !disabled;
466
+
467
+ return (
468
+ <View ref={hostRef} collapsable={false} style={[styles.field, { width: tokens.width }]}>
469
+ {fieldLabel ? <Label text={fieldLabel} required={required && !isYearMonth} /> : null}
470
+ <View style={styles.triggerHost}>
471
+ {showRing && isNative ? <View pointerEvents="none" style={nativeRingStyle()} /> : null}
472
+ <Pressable
473
+ accessibilityRole={showSearch ? 'none' : 'button'}
474
+ accessibilityState={{ disabled, expanded: fieldOpen }}
475
+ accessibilityLabel={fieldLabel || placeholder}
476
+ disabled={disabled}
477
+ hitSlop={fieldHitSlop(tokens.height)}
478
+ onPress={showSearch ? undefined : onPress}
479
+ style={[
480
+ styles.trigger,
481
+ {
482
+ height: tokens.height,
483
+ paddingHorizontal: tokens.paddingX,
484
+ },
485
+ showRing && !isNative && webFocusRing(),
486
+ disabled && styles.triggerDisabled,
487
+ ]}>
488
+ {prefix ? <Icon name={prefix} size={tokens.icon} color={colors.inputDropdown.text} decorative /> : null}
489
+ {showSearch ? (
490
+ <TextInput
491
+ value={searchText}
492
+ onChangeText={(next) => {
493
+ setSearchText(next);
494
+ if (next.trim() || !searchText) {
495
+ openAt(defaultRef.current, null);
496
+ } else {
497
+ close();
498
+ }
499
+ }}
500
+ onFocus={() => openAt(defaultRef.current, null)}
501
+ placeholder={fieldPlaceholder}
502
+ placeholderTextColor={colors.inputDropdown.mutedText}
503
+ editable={!disabled}
504
+ style={[styles.searchInput, { fontSize: tokens.fontSize }]}
505
+ />
506
+ ) : (
507
+ <>
508
+ <Text
509
+ numberOfLines={1}
510
+ style={[
511
+ styles.triggerText,
512
+ { fontSize: tokens.fontSize },
513
+ !selectedItem && styles.triggerPlaceholder,
514
+ ]}>
515
+ {selectedItem ? selectedItem.label : fieldPlaceholder}
516
+ {selectedItem && extra ? <Text style={styles.triggerSuffix}>{` ${extra}`}</Text> : null}
517
+ </Text>
518
+ <View style={fieldOpen ? styles.chevronOpen : undefined}>
519
+ <Icon name={IconName.ChevronDown} size={tokens.icon} color={colors.inputDropdown.mutedText} decorative />
520
+ </View>
521
+ </>
522
+ )}
523
+ </Pressable>
524
+ </View>
525
+ </View>
526
+ );
527
+ };
528
+
529
+ if (isYearMonth) {
530
+ return (
531
+ <View style={[styles.yearMonth, style]}>
532
+ {renderField({
533
+ hostRef: yearRef,
534
+ fieldLabel: yearLabel,
535
+ fieldPlaceholder: yearPlaceholder,
536
+ selectedItem: yearItem,
537
+ fieldOpen: openField === 'year',
538
+ showSearch: false,
539
+ onPress: () => toggleField('year'),
540
+ })}
541
+ {renderField({
542
+ hostRef: monthRef,
543
+ fieldLabel: monthLabel,
544
+ fieldPlaceholder: monthPlaceholder,
545
+ selectedItem: monthItem,
546
+ fieldOpen: openField === 'month',
547
+ showSearch: false,
548
+ onPress: () => toggleField('month'),
549
+ })}
550
+ {renderMenu()}
551
+ </View>
552
+ );
553
+ }
554
+
555
+ return (
556
+ <View style={style}>
557
+ {renderField({
558
+ hostRef: defaultRef,
559
+ fieldLabel: label,
560
+ fieldPlaceholder: placeholder,
561
+ selectedItem: selected,
562
+ fieldOpen: open && !openField,
563
+ showSearch: searchable,
564
+ onPress: toggleDefault,
565
+ })}
566
+ {renderMenu()}
567
+ </View>
568
+ );
569
+ }
570
+
571
+ const webInputReset = {
572
+ outlineStyle: 'solid',
573
+ outlineWidth: borders.width.none,
574
+ outlineColor: colors.transparent,
575
+ } as TextStyle;
576
+
577
+ const styles = StyleSheet.create({
578
+ yearMonth: {
579
+ flexDirection: 'row',
580
+ flexWrap: 'wrap',
581
+ alignItems: 'flex-start',
582
+ gap: spacing.md,
583
+ },
584
+ field: {
585
+ gap: spacing.sm + spacing.xs,
586
+ },
587
+ triggerHost: {
588
+ position: 'relative',
589
+ overflow: 'visible',
590
+ },
591
+ trigger: {
592
+ width: '100%',
593
+ flexDirection: 'row',
594
+ alignItems: 'center',
595
+ gap: spacing.sm,
596
+ borderWidth: borders.width.thin,
597
+ borderColor: colors.gray.base,
598
+ borderRadius: radius.md,
599
+ backgroundColor: colors.white,
600
+ },
601
+ triggerDisabled: {
602
+ opacity: 0.6,
603
+ },
604
+ triggerText: {
605
+ flex: 1,
606
+ minWidth: spacing.none,
607
+ color: colors.inputDropdown.text,
608
+ fontFamily: typography.fontFamily.base,
609
+ lineHeight: typography.fontSize.base * typography.lineHeight.normal,
610
+ },
611
+ triggerPlaceholder: {
612
+ color: colors.inputDropdown.mutedText,
613
+ },
614
+ triggerSuffix: {
615
+ color: colors.gray.darker,
616
+ },
617
+ searchInput: {
618
+ flex: 1,
619
+ minWidth: spacing.none,
620
+ padding: spacing.none,
621
+ color: colors.inputDropdown.text,
622
+ fontFamily: typography.fontFamily.base,
623
+ ...webInputReset,
624
+ },
625
+ chevronOpen: {
626
+ transform: [{ rotate: '180deg' }],
627
+ },
628
+ backdrop: {
629
+ flex: 1,
630
+ },
631
+ panel: {
632
+ position: 'absolute',
633
+ backgroundColor: colors.white,
634
+ borderWidth: borders.width.thin,
635
+ borderColor: colors.input.border,
636
+ borderRadius: radius.sm,
637
+ overflow: 'hidden',
638
+ },
639
+ item: {
640
+ width: '100%',
641
+ flexDirection: 'row',
642
+ alignItems: 'center',
643
+ gap: spacing.sm,
644
+ backgroundColor: colors.white,
645
+ },
646
+ itemSelected: {
647
+ backgroundColor: colors.primary.light,
648
+ },
649
+ itemHovered: {
650
+ backgroundColor: colors.gray.light,
651
+ },
652
+ itemLabel: {
653
+ flex: 1,
654
+ minWidth: spacing.none,
655
+ color: colors.inputDropdown.text,
656
+ fontFamily: typography.fontFamily.base,
657
+ },
658
+ itemSuffix: {
659
+ color: colors.gray.darker,
660
+ fontWeight: typography.fontWeight.regular,
661
+ },
662
+ });
@@ -0,0 +1,9 @@
1
+ export {
2
+ InputDropdown,
3
+ InputDropdownLayout,
4
+ InputDropdownSize,
5
+ InputDropdownIconType,
6
+ buildMonthDropdownOptions,
7
+ buildYearDropdownOptions,
8
+ } from './InputDropdown';
9
+ export type { InputDropdownProps, InputDropdownItem, MonthYearValue } from './InputDropdown';