@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,584 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Platform,
4
+ Pressable,
5
+ Text,
6
+ View,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ } from 'react-native';
10
+
11
+ import { borders, button as buttonTokens, colors, radius } from '../../theme';
12
+ import { Icon } from '../Icon';
13
+
14
+ export const ButtonVariant = {
15
+ Primary: 'primary',
16
+ Secondary: 'secondary',
17
+ Success: 'success',
18
+ Danger: 'danger',
19
+ Warning: 'warning',
20
+ Info: 'info',
21
+ SecondaryGray: 'secondary-gray',
22
+ Tertiary: 'tertiary',
23
+ TertiaryGray: 'tertiary-gray',
24
+ LinkPrimary: 'link-primary',
25
+ LinkGray: 'link-gray',
26
+ PrimaryDanger: 'primary-danger',
27
+ SecondaryDanger: 'secondary-danger',
28
+ SecondaryDangerGray: 'secondary-danger-gray',
29
+ TertiaryDanger: 'tertiary-danger',
30
+ TertiaryDangerGray: 'tertiary-danger-gray',
31
+ LinkDangerPrimary: 'link-danger-primary',
32
+ LinkDangerGray: 'link-danger-gray',
33
+ } as const;
34
+
35
+ export type ButtonVariant = (typeof ButtonVariant)[keyof typeof ButtonVariant];
36
+
37
+ export const ButtonSize = {
38
+ Small: 'sm',
39
+ Medium: 'md',
40
+ Large: 'lg',
41
+ ExtraLarge: 'xl',
42
+ ExtraExtraLarge: '2xl',
43
+ } as const;
44
+
45
+ export type ButtonSize = (typeof ButtonSize)[keyof typeof ButtonSize];
46
+
47
+ export const IconPosition = {
48
+ None: 'none',
49
+ Before: 'before',
50
+ After: 'after',
51
+ Only: 'only',
52
+ } as const;
53
+
54
+ export type IconPosition = (typeof IconPosition)[keyof typeof IconPosition];
55
+
56
+ export const IconType = {
57
+ Circle: 'circle',
58
+ Dot: 'dot',
59
+ } as const;
60
+
61
+ export type IconType = (typeof IconType)[keyof typeof IconType];
62
+
63
+ export type ButtonProps = {
64
+ label?: string;
65
+ variant?: ButtonVariant;
66
+ size?: ButtonSize;
67
+ disabled?: boolean;
68
+ iconPosition?: IconPosition;
69
+ iconType?: IconType;
70
+ iconName?: string;
71
+ iconDecorative?: boolean;
72
+ accessibilityLabel?: string;
73
+ onPress?: () => void;
74
+ fullWidth?: boolean;
75
+ style?: StyleProp<ViewStyle>;
76
+ contentStyle?: StyleProp<ViewStyle>;
77
+ };
78
+
79
+ type VisualSize = 'sm' | 'md' | 'lg' | 'xl';
80
+
81
+ type VariantClasses = {
82
+ container: string;
83
+ pressed: string;
84
+ disabled: string;
85
+ label: string;
86
+ labelPressed: string;
87
+ labelDisabled: string;
88
+ focused?: string;
89
+ labelFocused?: string;
90
+ circle: string;
91
+ dot: string;
92
+ isLink: boolean;
93
+ };
94
+
95
+ const focusDismissers = new Set<() => void>();
96
+ let retainFocusUntil = 0;
97
+
98
+ function clearAllButtonFocus() {
99
+ focusDismissers.forEach((dismiss) => dismiss());
100
+ }
101
+
102
+ export function dismissButtonFocus() {
103
+ if (Date.now() < retainFocusUntil) {
104
+ return;
105
+ }
106
+ clearAllButtonFocus();
107
+ }
108
+
109
+ const LINK_VARIANTS = new Set<ButtonVariant>([
110
+ ButtonVariant.LinkPrimary,
111
+ ButtonVariant.LinkGray,
112
+ ButtonVariant.LinkDangerPrimary,
113
+ ButtonVariant.LinkDangerGray,
114
+ ]);
115
+
116
+ function cx(...parts: (string | false | null | undefined)[]) {
117
+ return parts.filter(Boolean).join(' ');
118
+ }
119
+
120
+ function visualSize(size: ButtonSize): VisualSize {
121
+ return size === ButtonSize.ExtraExtraLarge ? 'xl' : size;
122
+ }
123
+
124
+ const SIZE_CLASS: Record<VisualSize, string> = {
125
+ sm: 'h-btn-sm py-btn-sm-y px-btn-sm-x',
126
+ md: 'h-btn-md py-btn-md-y px-btn-md-x',
127
+ lg: 'h-btn-lg py-btn-lg-y px-btn-lg-x',
128
+ xl: 'h-btn-xl py-btn-xl-y px-btn-xl-x',
129
+ };
130
+
131
+ const ICON_ONLY_CLASS: Record<VisualSize, string> = {
132
+ sm: 'h-btn-sm w-btn-sm p-0',
133
+ md: 'h-btn-md w-btn-md p-0',
134
+ lg: 'h-btn-lg w-btn-lg p-0',
135
+ xl: 'h-btn-xl w-btn-xl p-0',
136
+ };
137
+
138
+ const LABEL_CLASS: Record<VisualSize, string> = {
139
+ sm: 'text-sm leading-5',
140
+ md: 'text-md',
141
+ lg: 'text-base',
142
+ xl: 'text-lg',
143
+ };
144
+
145
+ const VARIANT_CLASS: Record<ButtonVariant, VariantClasses> = {
146
+ primary: {
147
+ container: 'bg-primary-base border border-thin border-primary-base',
148
+ pressed: 'bg-primary-dark-hover border-primary-base',
149
+ disabled: 'bg-primary-lighter border-transparent',
150
+ label: 'text-white',
151
+ labelPressed: 'text-white',
152
+ labelDisabled: 'text-white',
153
+ circle: 'border-white',
154
+ dot: 'bg-white',
155
+ isLink: false,
156
+ },
157
+ secondary: {
158
+ container: 'bg-primary-lightest border border-thin border-primary-base',
159
+ pressed: 'bg-primary-light-hover border-primary-base',
160
+ disabled: 'bg-primary-lighter border-primary-base',
161
+ label: 'text-primary-base',
162
+ labelPressed: 'text-primary-base',
163
+ labelDisabled: 'text-primary-dark',
164
+ circle: 'border-primary-base',
165
+ dot: 'bg-success-base',
166
+ isLink: false,
167
+ },
168
+ success: {
169
+ container: 'bg-success-base border border-thin border-success-base',
170
+ pressed: 'bg-alert-success-border border-alert-success-border',
171
+ disabled: 'bg-badge-success-bg border-transparent',
172
+ label: 'text-white',
173
+ labelPressed: 'text-white',
174
+ labelDisabled: 'text-white',
175
+ circle: 'border-white',
176
+ dot: 'bg-white',
177
+ isLink: false,
178
+ },
179
+ danger: {
180
+ container: 'bg-danger-dark border border-thin border-danger-dark',
181
+ pressed: 'bg-danger-darker border-danger-darker',
182
+ disabled: 'bg-danger-medium border-transparent',
183
+ label: 'text-white',
184
+ labelPressed: 'text-white',
185
+ labelDisabled: 'text-white',
186
+ circle: 'border-white',
187
+ dot: 'bg-white',
188
+ isLink: false,
189
+ },
190
+ warning: {
191
+ container: 'bg-badge-warning-icon border border-thin border-badge-warning-icon',
192
+ pressed: 'bg-badge-warning-text border-badge-warning-text',
193
+ disabled: 'bg-badge-warning-bg border-transparent',
194
+ label: 'text-white',
195
+ labelPressed: 'text-white',
196
+ labelDisabled: 'text-white',
197
+ circle: 'border-white',
198
+ dot: 'bg-white',
199
+ isLink: false,
200
+ },
201
+ info: {
202
+ container: 'bg-primary-base border border-thin border-primary-base',
203
+ pressed: 'bg-primary-darker border-primary-darker',
204
+ disabled: 'bg-primary-lighter border-transparent',
205
+ label: 'text-white',
206
+ labelPressed: 'text-white',
207
+ labelDisabled: 'text-white',
208
+ circle: 'border-white',
209
+ dot: 'bg-white',
210
+ isLink: false,
211
+ },
212
+ 'secondary-gray': {
213
+ container: 'bg-white border border-thin border-gray-base',
214
+ pressed: 'bg-gray-light-hover border-gray-medium',
215
+ disabled: 'bg-gray-lighter border-gray-medium',
216
+ focused: 'bg-white border border-thin border-gray-medium',
217
+ label: 'text-gray-darker',
218
+ labelPressed: 'text-gray-darker',
219
+ labelFocused: 'text-gray-darkest',
220
+ labelDisabled: 'text-gray-medium',
221
+ circle: 'border-gray-darker',
222
+ dot: 'bg-success-base',
223
+ isLink: false,
224
+ },
225
+ tertiary: {
226
+ container: 'bg-transparent border-0',
227
+ pressed: 'bg-primary-lightest-hover',
228
+ disabled: 'bg-transparent',
229
+ label: 'text-primary-base',
230
+ labelPressed: 'text-primary-base',
231
+ labelDisabled: 'text-gray-base',
232
+ circle: 'border-primary-base',
233
+ dot: 'bg-success-base',
234
+ isLink: false,
235
+ },
236
+ 'tertiary-gray': {
237
+ container: 'bg-transparent border-0',
238
+ pressed: 'bg-gray-lightest-hover',
239
+ disabled: 'bg-transparent',
240
+ label: 'text-gray-dark',
241
+ labelPressed: 'text-gray-darker',
242
+ labelDisabled: 'text-gray-base',
243
+ circle: 'border-gray-dark',
244
+ dot: 'bg-success-base',
245
+ isLink: false,
246
+ },
247
+ 'link-primary': {
248
+ container: 'bg-transparent border-0',
249
+ pressed: 'bg-transparent',
250
+ disabled: 'bg-transparent',
251
+ label: 'text-primary-base',
252
+ labelPressed: 'text-primary-darker',
253
+ labelDisabled: 'text-gray-base',
254
+ circle: 'border-primary-base',
255
+ dot: 'bg-success-base',
256
+ isLink: true,
257
+ },
258
+ 'link-gray': {
259
+ container: 'bg-transparent border-0',
260
+ pressed: 'bg-transparent',
261
+ disabled: 'bg-transparent',
262
+ label: 'text-gray-dark',
263
+ labelPressed: 'text-gray-darkest',
264
+ labelDisabled: 'text-gray-base',
265
+ circle: 'border-gray-dark',
266
+ dot: 'bg-success-base',
267
+ isLink: true,
268
+ },
269
+ 'primary-danger': {
270
+ container: 'bg-danger-dark border border-thin border-danger-darker',
271
+ pressed: 'bg-danger-darker border-danger-darker',
272
+ disabled: 'bg-danger-medium border-transparent',
273
+ label: 'text-white',
274
+ labelPressed: 'text-white',
275
+ labelDisabled: 'text-white',
276
+ circle: 'border-white',
277
+ dot: 'bg-white',
278
+ isLink: false,
279
+ },
280
+ 'secondary-danger': {
281
+ container: 'bg-danger-lighter border border-thin border-danger-lighter',
282
+ pressed: 'bg-danger-light border-danger-light',
283
+ disabled: 'bg-danger-lightest border-danger-lightest',
284
+ label: 'text-danger-darker',
285
+ labelPressed: 'text-danger-darker',
286
+ labelDisabled: 'text-danger-base',
287
+ circle: 'border-danger-darker',
288
+ dot: 'bg-success-base',
289
+ isLink: false,
290
+ },
291
+ 'secondary-danger-gray': {
292
+ container: 'bg-white border border-thin border-danger-base',
293
+ pressed: 'bg-danger-lightest-hover border-danger-base',
294
+ disabled: 'bg-white border-danger-medium',
295
+ label: 'text-danger-darker',
296
+ labelPressed: 'text-danger-darkest',
297
+ labelDisabled: 'text-danger-base',
298
+ circle: 'border-danger-darker',
299
+ dot: 'bg-success-base',
300
+ isLink: false,
301
+ },
302
+ 'tertiary-danger': {
303
+ container: 'bg-transparent border-0',
304
+ pressed: 'bg-danger-light-hover',
305
+ disabled: 'bg-transparent',
306
+ label: 'text-danger-darker',
307
+ labelPressed: 'text-danger-darker',
308
+ labelFocused: 'text-danger-darkest',
309
+ labelDisabled: 'text-danger-base',
310
+ circle: 'border-danger-darker',
311
+ dot: 'bg-success-base',
312
+ isLink: false,
313
+ },
314
+ 'tertiary-danger-gray': {
315
+ container: 'bg-transparent border-0',
316
+ pressed: 'bg-danger-light-hover',
317
+ disabled: 'bg-transparent',
318
+ label: 'text-danger-darker',
319
+ labelPressed: 'text-danger-darkest',
320
+ labelDisabled: 'text-danger-base',
321
+ circle: 'border-danger-darker',
322
+ dot: 'bg-success-base',
323
+ isLink: false,
324
+ },
325
+ 'link-danger-primary': {
326
+ container: 'bg-transparent border-0',
327
+ pressed: 'bg-transparent',
328
+ disabled: 'bg-transparent',
329
+ label: 'text-danger-darker',
330
+ labelPressed: 'text-danger-darkest',
331
+ labelDisabled: 'text-danger-base',
332
+ circle: 'border-danger-darker',
333
+ dot: 'bg-success-base',
334
+ isLink: true,
335
+ },
336
+ 'link-danger-gray': {
337
+ container: 'bg-transparent border-0',
338
+ pressed: 'bg-transparent',
339
+ disabled: 'bg-transparent',
340
+ label: 'text-danger-darker',
341
+ labelPressed: 'text-danger-darkest',
342
+ labelDisabled: 'text-danger-base',
343
+ circle: 'border-danger-darker',
344
+ dot: 'bg-success-base',
345
+ isLink: true,
346
+ },
347
+ };
348
+
349
+ const ICON_TINT: Record<
350
+ ButtonVariant,
351
+ { default: string; pressed: string; disabled: string; focused?: string }
352
+ > = {
353
+ primary: { default: colors.white, pressed: colors.white, disabled: colors.white },
354
+ secondary: { default: colors.primary.base, pressed: colors.primary.base, disabled: colors.primary.dark },
355
+ success: { default: colors.white, pressed: colors.white, disabled: colors.white },
356
+ danger: { default: colors.white, pressed: colors.white, disabled: colors.white },
357
+ warning: { default: colors.white, pressed: colors.white, disabled: colors.white },
358
+ info: { default: colors.white, pressed: colors.white, disabled: colors.white },
359
+ 'secondary-gray': { default: colors.gray.darker, pressed: colors.gray.darker, disabled: colors.gray.medium },
360
+ tertiary: { default: colors.primary.base, pressed: colors.primary.base, disabled: colors.gray.base },
361
+ 'tertiary-gray': { default: colors.gray.dark, pressed: colors.gray.darker, disabled: colors.gray.base },
362
+ 'link-primary': { default: colors.primary.base, pressed: colors.primary.darker, disabled: colors.gray.base },
363
+ 'link-gray': { default: colors.gray.dark, pressed: colors.gray.darkest, disabled: colors.gray.base },
364
+ 'primary-danger': { default: colors.white, pressed: colors.white, disabled: colors.white },
365
+ 'secondary-danger': { default: colors.danger.darker, pressed: colors.danger.darker, disabled: colors.danger.base },
366
+ 'secondary-danger-gray': {
367
+ default: colors.danger.darker,
368
+ pressed: colors.danger.darkest,
369
+ disabled: colors.danger.base,
370
+ },
371
+ 'tertiary-danger': { default: colors.danger.darker, pressed: colors.danger.darker, focused: colors.danger.darkest, disabled: colors.danger.base },
372
+ 'tertiary-danger-gray': {
373
+ default: colors.danger.darker,
374
+ pressed: colors.danger.darkest,
375
+ disabled: colors.danger.base,
376
+ },
377
+ 'link-danger-primary': {
378
+ default: colors.danger.darker,
379
+ pressed: colors.danger.darkest,
380
+ disabled: colors.danger.base,
381
+ },
382
+ 'link-danger-gray': { default: colors.danger.darker, pressed: colors.danger.darkest, disabled: colors.danger.base },
383
+ };
384
+
385
+ function Decoration({ kind, circleClass, dotClass }: { kind: IconType; circleClass: string; dotClass: string }) {
386
+ if (kind === IconType.Dot) {
387
+ return <View className={cx('h-md w-md rounded-full', dotClass)} />;
388
+ }
389
+
390
+ return <View className={cx('h-lg w-lg rounded-full border-thin', circleClass)} />;
391
+ }
392
+
393
+ function hitSlopFor(visualHeight: number) {
394
+ const extra = Math.max(0, (buttonTokens.minTouch - visualHeight) / 2);
395
+ if (extra === 0) {
396
+ return undefined;
397
+ }
398
+ return { top: extra, bottom: extra, left: extra, right: extra };
399
+ }
400
+
401
+ function containsNode(host: unknown, target: EventTarget | null) {
402
+ if (!host || !target || typeof host !== 'object') {
403
+ return false;
404
+ }
405
+ const node = host as { contains?: (other: EventTarget) => boolean };
406
+ return typeof node.contains === 'function' ? node.contains(target) : false;
407
+ }
408
+
409
+ /** Flush 2px ring that follows `border-radius` (CSS `outline` stays square on RN web). */
410
+ function angularFocusRing(visible: boolean): ViewStyle {
411
+ return {
412
+ boxShadow: visible ? `0px 0px 0px ${borders.focusRing.width}px ${colors.focus}` : 'none',
413
+ } as ViewStyle;
414
+ }
415
+
416
+ const PRESSABLE_RESET = {
417
+ outlineWidth: 0,
418
+ outlineStyle: 'solid' as const,
419
+ outlineColor: 'transparent',
420
+ outlineOffset: 0,
421
+ borderRadius: radius.button,
422
+ overflow: 'visible' as const,
423
+ } satisfies ViewStyle;
424
+
425
+ export function Button({
426
+ label = 'Button',
427
+ variant = ButtonVariant.Primary,
428
+ size = ButtonSize.Medium,
429
+ disabled = false,
430
+ iconPosition = IconPosition.None,
431
+ iconType = IconType.Circle,
432
+ iconName,
433
+ iconDecorative = false,
434
+ accessibilityLabel,
435
+ onPress,
436
+ fullWidth = false,
437
+ style,
438
+ contentStyle,
439
+ }: ButtonProps) {
440
+ const hostRef = useRef<View>(null);
441
+ const [focused, setFocused] = useState(false);
442
+ const [hovered, setHovered] = useState(false);
443
+ const visual = visualSize(size);
444
+ const palette = VARIANT_CLASS[variant];
445
+ const hasSvg = Boolean(iconName);
446
+ const showSvgBefore = hasSvg && iconPosition === IconPosition.Before;
447
+ const showSvgAfter = hasSvg && iconPosition === IconPosition.After;
448
+ const isSvgOnly = hasSvg && !showSvgBefore && !showSvgAfter;
449
+ const isIconOnly = isSvgOnly || iconPosition === IconPosition.Only;
450
+ const isLink = palette.isLink || LINK_VARIANTS.has(variant);
451
+ const showLabel = !isIconOnly;
452
+ const showBefore = !hasSvg && iconPosition === IconPosition.Before;
453
+ const showAfter = !hasSvg && iconPosition === IconPosition.After;
454
+ const showOnlyDecoration = !hasSvg && iconPosition === IconPosition.Only;
455
+ const visualHeight = isLink ? buttonTokens.lineHeight[visual] : buttonTokens.height[visual];
456
+
457
+ useEffect(() => {
458
+ const dismiss = () => setFocused(false);
459
+ focusDismissers.add(dismiss);
460
+ return () => {
461
+ focusDismissers.delete(dismiss);
462
+ };
463
+ }, []);
464
+
465
+ useEffect(() => {
466
+ if (Platform.OS !== 'web' || !focused) {
467
+ return;
468
+ }
469
+
470
+ const doc = globalThis.document as Document | undefined;
471
+ if (!doc) {
472
+ return;
473
+ }
474
+
475
+ const onPointerDown = (event: Event) => {
476
+ if (!containsNode(hostRef.current, event.target)) {
477
+ setFocused(false);
478
+ }
479
+ };
480
+
481
+ doc.addEventListener('mousedown', onPointerDown);
482
+ doc.addEventListener('touchstart', onPointerDown);
483
+ return () => {
484
+ doc.removeEventListener('mousedown', onPointerDown);
485
+ doc.removeEventListener('touchstart', onPointerDown);
486
+ };
487
+ }, [focused]);
488
+
489
+ return (
490
+ <View
491
+ className={fullWidth ? 'w-full self-stretch' : 'self-start'}
492
+ style={fullWidth ? { width: '100%', alignSelf: 'stretch' } : undefined}>
493
+ <Pressable
494
+ ref={hostRef}
495
+ accessibilityRole="button"
496
+ accessibilityLabel={accessibilityLabel || (showLabel ? label : iconName || label)}
497
+ accessibilityState={{ disabled }}
498
+ disabled={disabled}
499
+ focusable={!disabled}
500
+ hitSlop={hitSlopFor(visualHeight)}
501
+ className="overflow-visible rounded-button"
502
+ onHoverIn={() => setHovered(true)}
503
+ onHoverOut={() => setHovered(false)}
504
+ onFocus={() => {
505
+ if (!isLink && !disabled) {
506
+ setFocused(true);
507
+ }
508
+ }}
509
+ onBlur={() => setFocused(false)}
510
+ onPress={(event) => {
511
+ if ('stopPropagation' in event && typeof event.stopPropagation === 'function') {
512
+ event.stopPropagation();
513
+ }
514
+ clearAllButtonFocus();
515
+ retainFocusUntil = Date.now() + 200;
516
+ if (!isLink && !disabled) {
517
+ setFocused(true);
518
+ }
519
+ onPress?.();
520
+ }}
521
+ style={[PRESSABLE_RESET, fullWidth && { width: '100%' }, style]}>
522
+ {({ pressed }) => {
523
+ const showRing = !disabled && !isLink && (pressed || focused);
524
+ const showHover = !disabled && !showRing && hovered;
525
+ const containerState = disabled
526
+ ? palette.disabled
527
+ : showHover
528
+ ? palette.pressed
529
+ : showRing
530
+ ? (palette.focused ?? palette.container)
531
+ : palette.container;
532
+ const labelState = disabled
533
+ ? palette.labelDisabled
534
+ : showHover
535
+ ? palette.labelPressed
536
+ : showRing
537
+ ? (palette.labelFocused ?? palette.label)
538
+ : palette.label;
539
+ const tints = ICON_TINT[variant];
540
+ const tint = disabled
541
+ ? tints.disabled
542
+ : showHover
543
+ ? tints.pressed
544
+ : showRing
545
+ ? (tints.focused ?? tints.default)
546
+ : tints.default;
547
+
548
+ return (
549
+ <View
550
+ className={cx(
551
+ 'flex-row items-center justify-center gap-sm rounded-button',
552
+ isLink ? 'px-0 py-0' : isIconOnly ? ICON_ONLY_CLASS[visual] : SIZE_CLASS[visual],
553
+ containerState,
554
+ disabled && 'opacity-60',
555
+ )}
556
+ style={[angularFocusRing(showRing), fullWidth && { width: '100%' }, contentStyle]}>
557
+ {showSvgBefore && iconName ? (
558
+ <Icon name={iconName} size={buttonTokens.icon[visual]} color={tint} decorative={iconDecorative} />
559
+ ) : null}
560
+ {isSvgOnly && iconName ? (
561
+ <Icon name={iconName} size={buttonTokens.icon[visual]} color={tint} decorative={iconDecorative} />
562
+ ) : null}
563
+ {showBefore ? (
564
+ <Decoration kind={iconType} circleClass={palette.circle} dotClass={palette.dot} />
565
+ ) : null}
566
+ {showOnlyDecoration ? (
567
+ <Decoration kind={iconType} circleClass={palette.circle} dotClass={palette.dot} />
568
+ ) : null}
569
+ {showLabel ? (
570
+ <Text className={cx('font-sans font-semibold', LABEL_CLASS[visual], labelState)}>{label}</Text>
571
+ ) : null}
572
+ {showAfter ? (
573
+ <Decoration kind={iconType} circleClass={palette.circle} dotClass={palette.dot} />
574
+ ) : null}
575
+ {showSvgAfter && iconName ? (
576
+ <Icon name={iconName} size={buttonTokens.icon[visual]} color={tint} decorative={iconDecorative} />
577
+ ) : null}
578
+ </View>
579
+ );
580
+ }}
581
+ </Pressable>
582
+ </View>
583
+ );
584
+ }
@@ -0,0 +1,9 @@
1
+ export {
2
+ Button,
3
+ ButtonVariant,
4
+ ButtonSize,
5
+ IconPosition,
6
+ IconType,
7
+ dismissButtonFocus,
8
+ } from './Button';
9
+ export type { ButtonProps } from './Button';