@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,314 @@
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ Dimensions,
4
+ Linking,
5
+ Modal,
6
+ Platform,
7
+ Pressable,
8
+ ScrollView,
9
+ StyleSheet,
10
+ Text,
11
+ View,
12
+ type StyleProp,
13
+ type ViewStyle,
14
+ } from 'react-native';
15
+
16
+ import { borders, colors, radius, spacing, typography } from '../../theme';
17
+ import { Button, ButtonSize, ButtonVariant, IconPosition, dismissButtonFocus } from '../Button';
18
+ import { Icon, IconName } from '../Icon';
19
+
20
+ export type ActionMenuItemVariant = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info';
21
+
22
+ export type ActionMenuItem<T = Record<string, unknown>> = {
23
+ label: string;
24
+ icon?: string;
25
+ action: (row: T) => void;
26
+ visible?: (row: T) => boolean;
27
+ disabled?: (row: T) => boolean;
28
+ variant?: ActionMenuItemVariant;
29
+ dividerBefore?: boolean;
30
+ };
31
+
32
+ export type ActionsMenuProps<T = Record<string, unknown>> = {
33
+ rowData: T;
34
+ actions?: ActionMenuItem<T>[];
35
+ menuAriaLabel?: string;
36
+ menuPanelAriaLabel?: string;
37
+ triggerText?: string;
38
+ triggerHref?: string;
39
+ triggerHrefTarget?: '_blank' | '_self' | '_parent' | '_top';
40
+ disabled?: boolean;
41
+ resolveLabel?: (label: string, row: T) => string;
42
+ onActionClick?: (payload: { action: ActionMenuItem<T>; row: T }) => void;
43
+ style?: StyleProp<ViewStyle>;
44
+ };
45
+
46
+ function itemColor(variant?: ActionMenuItemVariant) {
47
+ if (variant === 'danger') {
48
+ return colors.danger.dark;
49
+ }
50
+ if (variant === 'primary') {
51
+ return colors.interactive.accent;
52
+ }
53
+ return colors.gray.darker;
54
+ }
55
+
56
+ function panelPosition(
57
+ trigger: { x: number; y: number; width: number; height: number },
58
+ menu: { width: number; height: number },
59
+ ) {
60
+ const window = Dimensions.get('window');
61
+ const pad = spacing.lg;
62
+ const spaceBelow = window.height - (trigger.y + trigger.height);
63
+ const placeUp = spaceBelow < menu.height + spacing.xs && trigger.y > menu.height + spacing.xs;
64
+
65
+ let top = placeUp ? trigger.y - menu.height - spacing.xs : trigger.y + trigger.height + spacing.xs;
66
+ let left = trigger.x + trigger.width - menu.width;
67
+
68
+ left = Math.min(Math.max(pad, left), window.width - menu.width - pad);
69
+ top = Math.min(Math.max(pad, top), window.height - menu.height - pad);
70
+
71
+ return { top, left };
72
+ }
73
+
74
+ function openHref(href: string, target: ActionsMenuProps['triggerHrefTarget']) {
75
+ if (Platform.OS === 'web' && typeof globalThis.open === 'function' && target === '_blank') {
76
+ globalThis.open(href, '_blank', 'noopener,noreferrer');
77
+ return;
78
+ }
79
+ void Linking.openURL(href);
80
+ }
81
+
82
+ export function ActionsMenu<T = Record<string, unknown>>({
83
+ rowData,
84
+ actions = [],
85
+ menuAriaLabel = 'Actions menu',
86
+ menuPanelAriaLabel,
87
+ triggerText,
88
+ triggerHref,
89
+ triggerHrefTarget = '_blank',
90
+ disabled = false,
91
+ resolveLabel,
92
+ onActionClick,
93
+ style,
94
+ }: ActionsMenuProps<T>) {
95
+ const triggerRef = useRef<View>(null);
96
+ const [open, setOpen] = useState(false);
97
+ const [hovered, setHovered] = useState<number | null>(null);
98
+ const [triggerBox, setTriggerBox] = useState({ x: 0, y: 0, width: 0, height: 0 });
99
+ const [menuSize, setMenuSize] = useState({ width: spacing['4xl'] * 4, height: 0 });
100
+ const position = panelPosition(triggerBox, menuSize);
101
+ const hasTextTrigger = Boolean(triggerText?.trim());
102
+
103
+ const visibleActions = useMemo(
104
+ () => actions.filter((action) => !action.visible || action.visible(rowData)),
105
+ [actions, rowData],
106
+ );
107
+
108
+ useEffect(() => {
109
+ if (disabled && open) {
110
+ setOpen(false);
111
+ }
112
+ }, [disabled, open]);
113
+
114
+ function close() {
115
+ setOpen(false);
116
+ setHovered(null);
117
+ dismissButtonFocus();
118
+ setTimeout(() => dismissButtonFocus(), 220);
119
+ }
120
+
121
+ function toggle() {
122
+ if (disabled) {
123
+ close();
124
+ return;
125
+ }
126
+ if (open) {
127
+ close();
128
+ return;
129
+ }
130
+ triggerRef.current?.measureInWindow((x, y, width, height) => {
131
+ setTriggerBox({ x, y, width, height });
132
+ setOpen(true);
133
+ });
134
+ }
135
+
136
+ function onItemPress(action: ActionMenuItem<T>) {
137
+ const isDisabled = Boolean(action.disabled?.(rowData));
138
+ if (disabled || isDisabled) {
139
+ return;
140
+ }
141
+ action.action(rowData);
142
+ onActionClick?.({ action, row: rowData });
143
+ close();
144
+ }
145
+
146
+ if (triggerHref) {
147
+ return (
148
+ <Pressable
149
+ accessibilityRole="link"
150
+ accessibilityLabel={triggerText || menuAriaLabel}
151
+ onPress={() => openHref(triggerHref, triggerHrefTarget)}
152
+ style={[styles.host, styles.linkTrigger, style]}>
153
+ <Text style={styles.linkLabel}>{triggerText || menuAriaLabel}</Text>
154
+ </Pressable>
155
+ );
156
+ }
157
+
158
+ return (
159
+ <View ref={triggerRef} collapsable={false} style={[styles.host, style]}>
160
+ <View style={[styles.triggerFrame, open && styles.triggerFrameOpen]}>
161
+ <Button
162
+ label={hasTextTrigger ? triggerText : undefined}
163
+ variant={ButtonVariant.TertiaryGray}
164
+ size={ButtonSize.Small}
165
+ iconPosition={hasTextTrigger ? IconPosition.None : IconPosition.Only}
166
+ iconName={hasTextTrigger ? undefined : open ? IconName.VerticalDotsActive : IconName.VerticalThreeDots}
167
+ iconDecorative
168
+ disabled={disabled}
169
+ accessibilityLabel={menuAriaLabel}
170
+ onPress={toggle}
171
+ contentStyle={hasTextTrigger ? undefined : styles.dotsContent}
172
+ />
173
+ </View>
174
+
175
+ <Modal visible={open} transparent animationType="none" onRequestClose={close}>
176
+ <Pressable accessible={false} style={styles.backdrop} onPress={close}>
177
+ <Pressable
178
+ accessible={false}
179
+ accessibilityRole="menu"
180
+ accessibilityLabel={menuPanelAriaLabel || menuAriaLabel}
181
+ onPress={(event) => {
182
+ if ('stopPropagation' in event && typeof event.stopPropagation === 'function') {
183
+ event.stopPropagation();
184
+ }
185
+ }}
186
+ onLayout={(event) => {
187
+ const { width, height } = event.nativeEvent.layout;
188
+ if (width !== menuSize.width || height !== menuSize.height) {
189
+ setMenuSize({ width, height });
190
+ }
191
+ }}
192
+ style={[styles.panel, { top: position.top, left: position.left }]}>
193
+ <ScrollView
194
+ style={styles.list}
195
+ contentContainerStyle={styles.listContent}
196
+ keyboardShouldPersistTaps="handled"
197
+ bounces={false}>
198
+ {visibleActions.map((action, index) => {
199
+ const label = resolveLabel ? resolveLabel(action.label, rowData) : action.label;
200
+ const isDisabled = Boolean(action.disabled?.(rowData));
201
+ const tint = itemColor(action.variant);
202
+ const isHovered = hovered === index && !isDisabled;
203
+
204
+ return (
205
+ <View key={`${action.label}-${index}`}>
206
+ {action.dividerBefore ? <View style={styles.divider} /> : null}
207
+ <Pressable
208
+ accessibilityRole="menuitem"
209
+ accessibilityState={{ disabled: isDisabled }}
210
+ accessibilityLabel={label}
211
+ disabled={isDisabled}
212
+ onHoverIn={() => setHovered(index)}
213
+ onHoverOut={() => setHovered((current) => (current === index ? null : current))}
214
+ onPress={() => onItemPress(action)}
215
+ style={[styles.item, isHovered && styles.itemHovered, isDisabled && styles.itemDisabled]}>
216
+ {action.icon ? (
217
+ <Icon name={action.icon} size={spacing.base} color={tint} decorative />
218
+ ) : null}
219
+ <Text style={[styles.itemLabel, { color: tint }, isDisabled && styles.itemLabelDisabled]}>
220
+ {label}
221
+ </Text>
222
+ </Pressable>
223
+ </View>
224
+ );
225
+ })}
226
+ </ScrollView>
227
+ </Pressable>
228
+ </Pressable>
229
+ </Modal>
230
+ </View>
231
+ );
232
+ }
233
+
234
+ const styles = StyleSheet.create({
235
+ host: {
236
+ alignSelf: 'flex-start',
237
+ },
238
+ triggerFrame: {
239
+ borderRadius: radius.md,
240
+ borderWidth: borders.width.medium,
241
+ borderColor: colors.transparent,
242
+ },
243
+ triggerFrameOpen: {
244
+ backgroundColor: colors.actionsMenu.triggerOpenBg,
245
+ borderColor: colors.actionsMenu.triggerBorder,
246
+ },
247
+ dotsContent: {
248
+ width: spacing['3xl'],
249
+ height: spacing['3xl'],
250
+ minHeight: spacing['3xl'],
251
+ borderWidth: borders.width.none,
252
+ padding: spacing.none,
253
+ },
254
+ backdrop: {
255
+ flex: 1,
256
+ },
257
+ panel: {
258
+ position: 'absolute',
259
+ minWidth: spacing['4xl'] * 4,
260
+ maxWidth: spacing['4xl'] * 6,
261
+ maxHeight: spacing['4xl'] * 8,
262
+ backgroundColor: colors.actionsMenu.panelBg,
263
+ borderWidth: borders.width.thin,
264
+ borderColor: colors.actionsMenu.panelBorder,
265
+ borderRadius: radius.md,
266
+ overflow: 'hidden',
267
+ },
268
+ list: {
269
+ maxHeight: spacing['4xl'] * 8,
270
+ },
271
+ listContent: {
272
+ paddingVertical: spacing.xs,
273
+ },
274
+ item: {
275
+ minHeight: spacing['4xl'],
276
+ paddingVertical: spacing.xs,
277
+ paddingHorizontal: spacing.base,
278
+ flexDirection: 'row',
279
+ alignItems: 'center',
280
+ gap: spacing.sm,
281
+ },
282
+ itemHovered: {
283
+ backgroundColor: colors.actionsMenu.itemHoverBg,
284
+ },
285
+ itemDisabled: {
286
+ opacity: 0.5,
287
+ },
288
+ itemLabel: {
289
+ flexShrink: 1,
290
+ fontFamily: typography.fontFamily.base,
291
+ fontWeight: typography.fontWeight.regular,
292
+ fontSize: typography.fontSize.sm,
293
+ lineHeight: typography.buttonLineHeight.sm,
294
+ },
295
+ itemLabelDisabled: {
296
+ color: colors.gray.dark,
297
+ },
298
+ divider: {
299
+ height: borders.width.thin,
300
+ backgroundColor: colors.actionsMenu.divider,
301
+ marginVertical: spacing.none,
302
+ },
303
+ linkTrigger: {
304
+ paddingVertical: spacing.sm,
305
+ paddingHorizontal: spacing.base,
306
+ borderRadius: radius.md,
307
+ },
308
+ linkLabel: {
309
+ fontFamily: typography.fontFamily.base,
310
+ fontSize: typography.fontSize.sm,
311
+ fontWeight: typography.fontWeight.medium,
312
+ color: colors.gray.dark,
313
+ },
314
+ });
@@ -0,0 +1,2 @@
1
+ export type { ActionMenuItem, ActionMenuItemVariant, ActionsMenuProps } from './ActionsMenu';
2
+ export { ActionsMenu } from './ActionsMenu';
@@ -0,0 +1,291 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
3
+ import * as Linking from 'expo-linking';
4
+
5
+ import { borders, colors, radius, spacing, typography } from '../../theme';
6
+ import { Button, ButtonSize, ButtonVariant, IconPosition, dismissButtonFocus } from '../Button';
7
+ import { Icon, IconName } from '../Icon';
8
+
9
+ export const AlertType = {
10
+ Success: 'success',
11
+ Error: 'error',
12
+ Warning: 'warning',
13
+ Hint: 'hint',
14
+ } as const;
15
+
16
+ export type AlertType = (typeof AlertType)[keyof typeof AlertType];
17
+
18
+ export const AlertLayout = {
19
+ Wide: 'wide',
20
+ Narrow: 'narrow',
21
+ } as const;
22
+
23
+ export type AlertLayout = (typeof AlertLayout)[keyof typeof AlertLayout];
24
+
25
+ export const AlertTextTone = {
26
+ Neutral: 'neutral',
27
+ Themed: 'themed',
28
+ } as const;
29
+
30
+ export type AlertTextTone = (typeof AlertTextTone)[keyof typeof AlertTextTone];
31
+
32
+ export type AlertProps = {
33
+ type?: AlertType;
34
+ layout?: AlertLayout;
35
+ textTone?: AlertTextTone;
36
+ title?: string;
37
+ bodyText?: string;
38
+ dismissible?: boolean;
39
+ showIcon?: boolean;
40
+ learnMoreLabel?: string;
41
+ learnMoreHref?: string | null;
42
+ closeAriaLabel?: string;
43
+ onClosed?: () => void;
44
+ onLearnMore?: () => void;
45
+ style?: StyleProp<ViewStyle>;
46
+ };
47
+
48
+ const TYPE_CHROME: Record<AlertType, { surface: string; border: string; themedText: string; icon: IconName }> = {
49
+ [AlertType.Success]: {
50
+ surface: colors.alert.successSurface,
51
+ border: colors.alert.successBorder,
52
+ themedText: colors.alert.successText,
53
+ icon: IconName.Success,
54
+ },
55
+ [AlertType.Hint]: {
56
+ surface: colors.alert.infoSurface,
57
+ border: colors.alert.infoBorder,
58
+ themedText: colors.alert.hintText,
59
+ icon: IconName.Info,
60
+ },
61
+ [AlertType.Warning]: {
62
+ surface: colors.alert.warningSurface,
63
+ border: colors.alert.warningBorder,
64
+ themedText: colors.alert.warningText,
65
+ icon: IconName.Warning,
66
+ },
67
+ [AlertType.Error]: {
68
+ surface: colors.alert.errorSurface,
69
+ border: colors.alert.errorBorder,
70
+ themedText: colors.alert.errorText,
71
+ icon: IconName.Error,
72
+ },
73
+ };
74
+
75
+ function isOpenableHref(href: string | null | undefined) {
76
+ return Boolean(href && href !== '#');
77
+ }
78
+
79
+ export function Alert({
80
+ type = AlertType.Hint,
81
+ layout = AlertLayout.Wide,
82
+ textTone = AlertTextTone.Neutral,
83
+ title = '',
84
+ bodyText = '',
85
+ dismissible,
86
+ showIcon = true,
87
+ learnMoreLabel = '',
88
+ learnMoreHref = null,
89
+ closeAriaLabel = 'Close',
90
+ onClosed,
91
+ onLearnMore,
92
+ style,
93
+ }: AlertProps) {
94
+ const [dismissed, setDismissed] = useState(false);
95
+ const isWide = layout === AlertLayout.Wide;
96
+ const showClose = !isWide && dismissible !== false;
97
+ const chrome = TYPE_CHROME[type];
98
+ const isThemed = textTone === AlertTextTone.Themed;
99
+ const titleColor = isThemed ? chrome.themedText : colors.alert.title;
100
+ const messageColor = isThemed ? chrome.themedText : colors.alert.description;
101
+ const linkColor = isThemed ? chrome.themedText : colors.alert.description;
102
+
103
+ useEffect(() => {
104
+ setDismissed(false);
105
+ }, [type, layout, title, bodyText, dismissible, showIcon]);
106
+
107
+ if (dismissed) {
108
+ return null;
109
+ }
110
+
111
+ const closeAlert = () => {
112
+ setDismissed(true);
113
+ dismissButtonFocus();
114
+ onClosed?.();
115
+ };
116
+
117
+ const pressLearnMore = () => {
118
+ onLearnMore?.();
119
+ if (isOpenableHref(learnMoreHref)) {
120
+ void Linking.openURL(learnMoreHref as string);
121
+ }
122
+ };
123
+
124
+ const icon = showIcon ? (
125
+ <Icon name={chrome.icon} size={spacing.xl} decorative />
126
+ ) : null;
127
+
128
+ const learnMore = !learnMoreLabel
129
+ ? null
130
+ : learnMoreHref != null && learnMoreHref !== ''
131
+ ? (
132
+ <Pressable
133
+ accessibilityRole="link"
134
+ accessibilityLabel={learnMoreLabel}
135
+ onPress={pressLearnMore}
136
+ style={({ pressed }) => [styles.learnMore, pressed && styles.learnMorePressed]}>
137
+ <Text style={[styles.learnMoreText, { color: linkColor, borderBottomColor: linkColor }]}>{learnMoreLabel}</Text>
138
+ </Pressable>
139
+ )
140
+ : (
141
+ <Button
142
+ label={learnMoreLabel}
143
+ variant={ButtonVariant.LinkGray}
144
+ size={ButtonSize.Small}
145
+ accessibilityLabel={learnMoreLabel}
146
+ onPress={pressLearnMore}
147
+ contentStyle={[styles.learnMoreButton, { borderBottomColor: linkColor }]}
148
+ />
149
+ );
150
+
151
+ const closeButton = showClose ? (
152
+ <Button
153
+ label=""
154
+ variant={ButtonVariant.TertiaryGray}
155
+ size={ButtonSize.Small}
156
+ iconPosition={IconPosition.Only}
157
+ iconName={IconName.Close}
158
+ iconDecorative
159
+ accessibilityLabel={closeAriaLabel || 'Close'}
160
+ onPress={closeAlert}
161
+ contentStyle={styles.closeBtn}
162
+ />
163
+ ) : null;
164
+
165
+ return (
166
+ <View
167
+ accessibilityRole="alert"
168
+ style={[
169
+ styles.host,
170
+ isWide ? styles.hostWide : styles.hostNarrow,
171
+ { backgroundColor: chrome.surface, borderColor: chrome.border },
172
+ style,
173
+ ]}>
174
+ <View style={styles.inner}>
175
+ {isWide ? (
176
+ showIcon || title ? (
177
+ <View style={styles.headerWide}>
178
+ {icon}
179
+ {title ? (
180
+ <Text style={[styles.title, styles.titleInline, { color: titleColor }]}>{title}</Text>
181
+ ) : null}
182
+ </View>
183
+ ) : null
184
+ ) : showIcon || showClose ? (
185
+ <View style={styles.headerNarrow}>
186
+ {icon}
187
+ <View style={styles.headerGap} />
188
+ {closeButton}
189
+ </View>
190
+ ) : null}
191
+
192
+ <View style={[styles.body, isWide && showIcon && styles.bodyIndented]}>
193
+ {!isWide && title ? <Text style={[styles.title, { color: titleColor }]}>{title}</Text> : null}
194
+ {bodyText ? <Text style={[styles.message, { color: messageColor }]}>{bodyText}</Text> : null}
195
+ {learnMore}
196
+ </View>
197
+ </View>
198
+ </View>
199
+ );
200
+ }
201
+
202
+ const compactLineHeight = typography.fontSize.sm * typography.lineHeight.tight;
203
+
204
+ const styles = StyleSheet.create({
205
+ host: {
206
+ width: '100%',
207
+ padding: spacing.base,
208
+ borderWidth: borders.width.thin,
209
+ borderRadius: radius.md,
210
+ },
211
+ hostWide: {
212
+ alignSelf: 'center',
213
+ maxWidth: spacing['2xl'] * 12,
214
+ },
215
+ hostNarrow: {
216
+ alignSelf: 'center',
217
+ maxWidth: spacing['2xl'] * 10,
218
+ },
219
+ inner: {
220
+ width: '100%',
221
+ gap: spacing.sm,
222
+ },
223
+ headerWide: {
224
+ flexDirection: 'row',
225
+ alignItems: 'center',
226
+ gap: spacing.sm,
227
+ width: '100%',
228
+ minHeight: spacing.xl,
229
+ },
230
+ headerNarrow: {
231
+ flexDirection: 'row',
232
+ alignItems: 'center',
233
+ gap: spacing.sm,
234
+ width: '100%',
235
+ minHeight: spacing.xl,
236
+ },
237
+ headerGap: {
238
+ flex: 1,
239
+ minWidth: spacing.none,
240
+ },
241
+ body: {
242
+ gap: spacing.sm,
243
+ minWidth: spacing.none,
244
+ },
245
+ bodyIndented: {
246
+ paddingLeft: spacing.xl + spacing.sm,
247
+ },
248
+ title: {
249
+ fontFamily: typography.fontFamily.base,
250
+ fontSize: typography.fontSize.sm,
251
+ fontWeight: typography.fontWeight.semibold,
252
+ lineHeight: compactLineHeight,
253
+ },
254
+ titleInline: {
255
+ flex: 1,
256
+ minWidth: spacing.none,
257
+ },
258
+ message: {
259
+ fontFamily: typography.fontFamily.base,
260
+ fontSize: typography.fontSize.sm,
261
+ fontWeight: typography.fontWeight.regular,
262
+ lineHeight: compactLineHeight,
263
+ },
264
+ learnMore: {
265
+ alignSelf: 'flex-start',
266
+ },
267
+ learnMorePressed: {
268
+ opacity: 0.85,
269
+ },
270
+ learnMoreText: {
271
+ alignSelf: 'flex-start',
272
+ fontFamily: typography.fontFamily.base,
273
+ fontSize: typography.fontSize.sm,
274
+ fontWeight: typography.fontWeight.regular,
275
+ lineHeight: compactLineHeight,
276
+ borderBottomWidth: borders.width.thin,
277
+ },
278
+ learnMoreButton: {
279
+ alignSelf: 'flex-start',
280
+ paddingHorizontal: spacing.none,
281
+ paddingVertical: spacing.none,
282
+ minHeight: compactLineHeight,
283
+ borderBottomWidth: borders.width.thin,
284
+ boxShadow: 'none',
285
+ },
286
+ closeBtn: {
287
+ minWidth: spacing['3xl'],
288
+ minHeight: spacing['3xl'],
289
+ padding: spacing.xs,
290
+ },
291
+ });
@@ -0,0 +1,2 @@
1
+ export { Alert, AlertType, AlertLayout, AlertTextTone } from './Alert';
2
+ export type { AlertProps } from './Alert';