@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,278 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
3
+
4
+ import { borders, button as buttonTokens, colors, radius, spacing, typography } from '../../theme';
5
+ import { Icon, IconName } from '../Icon';
6
+
7
+ export const ToastType = {
8
+ Default: 'default',
9
+ Success: 'success',
10
+ Error: 'error',
11
+ Warning: 'warning',
12
+ Info: 'info',
13
+ } as const;
14
+
15
+ export type ToastType = (typeof ToastType)[keyof typeof ToastType];
16
+
17
+ export type ToastItem = {
18
+ id: string;
19
+ header?: string;
20
+ body: string;
21
+ type: ToastType;
22
+ delay: number;
23
+ autohide: boolean;
24
+ };
25
+
26
+ export type ToastShowOptions = {
27
+ header?: string;
28
+ body: string;
29
+ type?: ToastType;
30
+ delay?: number;
31
+ autohide?: boolean;
32
+ };
33
+
34
+ export type ToastProps = {
35
+ style?: StyleProp<ViewStyle>;
36
+ };
37
+
38
+ const DEFAULT_DELAY_MS = 5000;
39
+
40
+ const TOAST_DELAY_BY_TYPE: Record<ToastType, number> = {
41
+ [ToastType.Default]: DEFAULT_DELAY_MS,
42
+ [ToastType.Success]: DEFAULT_DELAY_MS,
43
+ [ToastType.Error]: 8000,
44
+ [ToastType.Warning]: DEFAULT_DELAY_MS,
45
+ [ToastType.Info]: DEFAULT_DELAY_MS,
46
+ };
47
+
48
+ const TOAST_HEADER_BY_TYPE: Record<ToastType, string> = {
49
+ [ToastType.Default]: 'Notification',
50
+ [ToastType.Success]: 'Success',
51
+ [ToastType.Error]: 'Error',
52
+ [ToastType.Warning]: 'Warning',
53
+ [ToastType.Info]: 'Info',
54
+ };
55
+
56
+ type Listener = (toasts: ToastItem[]) => void;
57
+
58
+ let toasts: ToastItem[] = [];
59
+ const listeners = new Set<Listener>();
60
+ const hideTimers = new Map<string, ReturnType<typeof setTimeout>>();
61
+
62
+ function emit() {
63
+ const snapshot = [...toasts];
64
+ listeners.forEach((listen) => listen(snapshot));
65
+ }
66
+
67
+ function scheduleHide(toast: ToastItem) {
68
+ if (!toast.autohide) {
69
+ return;
70
+ }
71
+ const timer = setTimeout(() => {
72
+ hideTimers.delete(toast.id);
73
+ removeToast(toast);
74
+ }, toast.delay);
75
+ hideTimers.set(toast.id, timer);
76
+ }
77
+
78
+ function clearTimer(id: string) {
79
+ const timer = hideTimers.get(id);
80
+ if (timer) {
81
+ clearTimeout(timer);
82
+ hideTimers.delete(id);
83
+ }
84
+ }
85
+
86
+ function subscribeToasts(listen: Listener) {
87
+ listeners.add(listen);
88
+ listen([...toasts]);
89
+ return () => {
90
+ listeners.delete(listen);
91
+ };
92
+ }
93
+
94
+ export function getToasts() {
95
+ return [...toasts];
96
+ }
97
+
98
+ export function showToast(options: ToastShowOptions): ToastItem {
99
+ const type = options.type ?? ToastType.Default;
100
+ const toast: ToastItem = {
101
+ id: `toast_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
102
+ header: options.header ?? TOAST_HEADER_BY_TYPE[type],
103
+ body: options.body,
104
+ type,
105
+ delay: options.delay ?? TOAST_DELAY_BY_TYPE[type],
106
+ autohide: options.autohide !== false,
107
+ };
108
+ toasts = [...toasts, toast];
109
+ emit();
110
+ scheduleHide(toast);
111
+ return toast;
112
+ }
113
+
114
+ export function removeToast(toast: ToastItem) {
115
+ clearTimer(toast.id);
116
+ toasts = toasts.filter((item) => item.id !== toast.id);
117
+ emit();
118
+ }
119
+
120
+ export function clearToasts() {
121
+ hideTimers.forEach((timer) => clearTimeout(timer));
122
+ hideTimers.clear();
123
+ toasts = [];
124
+ emit();
125
+ }
126
+
127
+ export const toastService = {
128
+ show: showToast,
129
+ success: (message: string, header = TOAST_HEADER_BY_TYPE[ToastType.Success]) =>
130
+ showToast({ header, body: message, type: ToastType.Success }),
131
+ error: (message: string, header = TOAST_HEADER_BY_TYPE[ToastType.Error]) =>
132
+ showToast({ header, body: message, type: ToastType.Error }),
133
+ warning: (message: string, header = TOAST_HEADER_BY_TYPE[ToastType.Warning]) =>
134
+ showToast({ header, body: message, type: ToastType.Warning }),
135
+ info: (message: string, header = TOAST_HEADER_BY_TYPE[ToastType.Info]) =>
136
+ showToast({ header, body: message, type: ToastType.Info }),
137
+ remove: removeToast,
138
+ clear: clearToasts,
139
+ getToasts,
140
+ };
141
+
142
+ const TYPE_CHROME: Record<
143
+ ToastType,
144
+ { background: string; text: string; border: string; header: string }
145
+ > = {
146
+ [ToastType.Default]: {
147
+ background: colors.white,
148
+ text: colors.text.primary,
149
+ border: colors.borderSubtle,
150
+ header: colors.gray.lighter,
151
+ },
152
+ [ToastType.Success]: {
153
+ background: colors.success.base,
154
+ text: colors.white,
155
+ border: colors.success.base,
156
+ header: colors.alert.successSurface,
157
+ },
158
+ [ToastType.Error]: {
159
+ background: colors.danger.dark,
160
+ text: colors.white,
161
+ border: colors.danger.dark,
162
+ header: colors.alert.errorSurface,
163
+ },
164
+ [ToastType.Warning]: {
165
+ background: colors.alert.warningText,
166
+ text: colors.text.primary,
167
+ border: colors.alert.warningBorder,
168
+ header: colors.alert.warningSurface,
169
+ },
170
+ [ToastType.Info]: {
171
+ background: colors.primary.base,
172
+ text: colors.white,
173
+ border: colors.primary.base,
174
+ header: colors.alert.infoSurface,
175
+ },
176
+ };
177
+
178
+ function ToastCard({ toast }: { toast: ToastItem }) {
179
+ const chrome = TYPE_CHROME[toast.type];
180
+ const compactLine = typography.fontSize.sm * typography.lineHeight.tight;
181
+
182
+ return (
183
+ <View accessibilityRole="alert" style={[styles.card, { borderColor: chrome.border }]}>
184
+ <View style={[styles.header, { backgroundColor: chrome.header }]}>
185
+ {toast.header ? (
186
+ <Text style={[styles.headerText, { lineHeight: compactLine }]}>{toast.header}</Text>
187
+ ) : (
188
+ <View style={styles.headerGap} />
189
+ )}
190
+ <Pressable
191
+ accessibilityRole="button"
192
+ accessibilityLabel="Dismiss"
193
+ hitSlop={spacing.md}
194
+ onPress={() => removeToast(toast)}
195
+ style={({ pressed }) => [styles.close, pressed && styles.closePressed]}>
196
+ <Icon name={IconName.Close} size={buttonTokens.icon.sm} color={colors.gray.darker} decorative />
197
+ </Pressable>
198
+ </View>
199
+ <View style={[styles.bodyWrap, { backgroundColor: chrome.background }]}>
200
+ <Text style={[styles.body, { color: chrome.text, lineHeight: compactLine }]}>{toast.body}</Text>
201
+ </View>
202
+ </View>
203
+ );
204
+ }
205
+
206
+ export function Toast({ style }: ToastProps) {
207
+ const [items, setItems] = useState<ToastItem[]>([]);
208
+
209
+ useEffect(() => subscribeToasts(setItems), []);
210
+
211
+ if (items.length === 0) {
212
+ return null;
213
+ }
214
+
215
+ return (
216
+ <View pointerEvents="box-none" style={[styles.viewport, style]}>
217
+ {items.map((toast) => (
218
+ <ToastCard key={toast.id} toast={toast} />
219
+ ))}
220
+ </View>
221
+ );
222
+ }
223
+
224
+ const styles = StyleSheet.create({
225
+ viewport: {
226
+ position: 'absolute',
227
+ top: spacing['4xl'],
228
+ right: spacing.md,
229
+ alignItems: 'flex-end',
230
+ gap: spacing.sm,
231
+ zIndex: 1090,
232
+ maxWidth: '100%',
233
+ },
234
+ card: {
235
+ width: spacing['2xl'] * 12,
236
+ maxWidth: '100%',
237
+ overflow: 'hidden',
238
+ borderRadius: radius.md,
239
+ borderWidth: borders.width.thin,
240
+ backgroundColor: colors.white,
241
+ },
242
+ header: {
243
+ flexDirection: 'row',
244
+ alignItems: 'center',
245
+ gap: spacing.sm,
246
+ paddingHorizontal: spacing.md,
247
+ paddingVertical: spacing.sm,
248
+ },
249
+ headerText: {
250
+ flex: 1,
251
+ minWidth: spacing.none,
252
+ fontFamily: typography.fontFamily.base,
253
+ fontSize: typography.fontSize.sm,
254
+ fontWeight: typography.fontWeight.semibold,
255
+ color: colors.text.primary,
256
+ },
257
+ headerGap: {
258
+ flex: 1,
259
+ },
260
+ bodyWrap: {
261
+ paddingHorizontal: spacing.md,
262
+ paddingVertical: spacing.md,
263
+ },
264
+ body: {
265
+ fontFamily: typography.fontFamily.base,
266
+ fontSize: typography.fontSize.sm,
267
+ fontWeight: typography.fontWeight.regular,
268
+ },
269
+ close: {
270
+ width: spacing['2xl'],
271
+ height: spacing['2xl'],
272
+ alignItems: 'center',
273
+ justifyContent: 'center',
274
+ },
275
+ closePressed: {
276
+ opacity: 0.85,
277
+ },
278
+ });
@@ -0,0 +1,2 @@
1
+ export { Toast, ToastType, showToast, removeToast, clearToasts, toastService } from './Toast';
2
+ export type { ToastProps, ToastItem, ToastShowOptions } from './Toast';
@@ -0,0 +1,296 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Platform,
4
+ Pressable,
5
+ StyleSheet,
6
+ View,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ } from 'react-native';
10
+
11
+ import { borders, button as buttonTokens, colors, radius, shadows, spacing, typography } from '../../theme';
12
+ import { Label } from '../Label';
13
+
14
+ export const ToggleSize = {
15
+ Small: 'sm',
16
+ Medium: 'md',
17
+ Large: 'lg',
18
+ } as const;
19
+
20
+ export type ToggleSize = (typeof ToggleSize)[keyof typeof ToggleSize];
21
+
22
+ export const ToggleState = {
23
+ Default: 'default',
24
+ Hover: 'hover',
25
+ Focused: 'focused',
26
+ Disabled: 'disabled',
27
+ } as const;
28
+
29
+ export type ToggleState = (typeof ToggleState)[keyof typeof ToggleState];
30
+
31
+ export type ToggleProps = {
32
+ checked?: boolean;
33
+ size?: ToggleSize;
34
+ text?: string;
35
+ supportingText?: string;
36
+ state?: ToggleState;
37
+ onCheckedChange?: (checked: boolean) => void;
38
+ style?: StyleProp<ViewStyle>;
39
+ };
40
+
41
+ const isNative = Platform.OS !== 'web';
42
+
43
+ const TRACK = {
44
+ [ToggleSize.Small]: {
45
+ width: spacing['3xl'],
46
+ height: spacing.lg,
47
+ thumb: spacing.base,
48
+ },
49
+ [ToggleSize.Medium]: {
50
+ width: spacing['4xl'],
51
+ height: spacing.xl,
52
+ thumb: spacing.lg,
53
+ },
54
+ [ToggleSize.Large]: {
55
+ width: spacing['6xl'],
56
+ height: spacing['2xl'],
57
+ thumb: spacing.xl,
58
+ },
59
+ } as const;
60
+
61
+ function hitSlopFor(trackHeight: number) {
62
+ const extra = Math.max(spacing.none, (buttonTokens.minTouch - trackHeight) / 2);
63
+ if (extra === spacing.none) {
64
+ return undefined;
65
+ }
66
+ return { top: extra, bottom: extra, left: extra, right: extra };
67
+ }
68
+
69
+ function webThumbShadow(): ViewStyle {
70
+ return {
71
+ boxShadow: `0 ${borders.width.thin}px ${borders.width.medium}px ${spacing.none} ${shadows.toggleThumb.color}, 0 ${borders.width.thin}px ${spacing.xs + borders.width.thin}px ${spacing.none} ${shadows.toggleThumbMd.color}`,
72
+ } as ViewStyle;
73
+ }
74
+
75
+ function webFocusRing(): ViewStyle {
76
+ return {
77
+ outlineWidth: borders.focusRing.width,
78
+ outlineStyle: 'solid',
79
+ outlineColor: colors.toggle.onFocusRing,
80
+ outlineOffset: borders.focusRing.offset,
81
+ } as ViewStyle;
82
+ }
83
+
84
+ function nativeFocusRing(): ViewStyle {
85
+ return {
86
+ position: 'absolute',
87
+ top: -borders.focusRing.width,
88
+ right: -borders.focusRing.width,
89
+ bottom: -borders.focusRing.width,
90
+ left: -borders.focusRing.width,
91
+ borderRadius: radius.lg + borders.focusRing.width,
92
+ borderWidth: borders.focusRing.width,
93
+ borderColor: colors.toggle.onFocusRing,
94
+ zIndex: 0,
95
+ };
96
+ }
97
+
98
+ function trackColor(checked: boolean, disabled: boolean, hover: boolean) {
99
+ if (disabled) {
100
+ return checked ? colors.toggle.onDisabled : colors.toggle.offDisabled;
101
+ }
102
+ if (hover) {
103
+ return checked ? colors.toggle.onHover : colors.toggle.offHover;
104
+ }
105
+ return checked ? colors.toggle.on : colors.toggle.off;
106
+ }
107
+
108
+ type FocusListener = (source: symbol | null) => void;
109
+
110
+ const focusListeners = new Set<FocusListener>();
111
+
112
+ export function dismissToggleFocus() {
113
+ focusListeners.forEach((listener) => listener(null));
114
+ }
115
+
116
+ function subscribeToggleFocus(listener: FocusListener) {
117
+ focusListeners.add(listener);
118
+ return () => {
119
+ focusListeners.delete(listener);
120
+ };
121
+ }
122
+
123
+ function claimToggleFocus(source: symbol) {
124
+ focusListeners.forEach((listener) => listener(source));
125
+ }
126
+
127
+ export function Toggle({
128
+ checked = false,
129
+ size = ToggleSize.Small,
130
+ text = '',
131
+ supportingText = '',
132
+ state = ToggleState.Default,
133
+ onCheckedChange,
134
+ style,
135
+ }: ToggleProps) {
136
+ const [internalChecked, setInternalChecked] = useState(checked);
137
+ const [focused, setFocused] = useState(false);
138
+ const [hovered, setHovered] = useState(false);
139
+ const focusId = useRef(Symbol('toggle-focus'));
140
+
141
+ useEffect(() => {
142
+ setInternalChecked(checked);
143
+ }, [checked]);
144
+
145
+ useEffect(() => {
146
+ return subscribeToggleFocus((source) => {
147
+ if (source !== focusId.current && state !== ToggleState.Focused) {
148
+ setFocused(false);
149
+ }
150
+ });
151
+ }, [state]);
152
+
153
+ const isDisabled = state === ToggleState.Disabled;
154
+ const isChecked = internalChecked;
155
+ const hasText = Boolean(text) || Boolean(supportingText?.trim());
156
+ const hasSupporting = Boolean(supportingText?.trim());
157
+ const showHover = !isDisabled && (state === ToggleState.Hover || hovered);
158
+ const showFocus = !isDisabled && (state === ToggleState.Focused || focused);
159
+ const metrics = TRACK[size];
160
+ const thumbInset = spacing.xs;
161
+ const thumbLeft = isChecked ? metrics.width - metrics.thumb - thumbInset : thumbInset;
162
+
163
+ const toggle = () => {
164
+ if (isDisabled) {
165
+ return;
166
+ }
167
+
168
+ claimToggleFocus(focusId.current);
169
+ setFocused(true);
170
+ const next = !isChecked;
171
+ setInternalChecked(next);
172
+ onCheckedChange?.(next);
173
+ };
174
+
175
+ return (
176
+ <Pressable
177
+ accessibilityRole="switch"
178
+ accessibilityState={{ checked: isChecked, disabled: isDisabled }}
179
+ accessibilityLabel={text || supportingText || undefined}
180
+ disabled={isDisabled}
181
+ hitSlop={hasText ? undefined : hitSlopFor(metrics.height)}
182
+ onHoverIn={() => setHovered(true)}
183
+ onHoverOut={() => setHovered(false)}
184
+ onFocus={() => {
185
+ claimToggleFocus(focusId.current);
186
+ setFocused(true);
187
+ }}
188
+ onBlur={() => {
189
+ if (state !== ToggleState.Focused) {
190
+ setFocused(false);
191
+ }
192
+ }}
193
+ onPress={toggle}
194
+ style={[
195
+ styles.wrapper,
196
+ hasText && styles.wrapperWithText,
197
+ hasSupporting && styles.wrapperFill,
198
+ isDisabled && styles.wrapperDisabled,
199
+ style,
200
+ ]}>
201
+ <View style={styles.trackHost}>
202
+ {showFocus && isNative ? <View pointerEvents="none" style={nativeFocusRing()} /> : null}
203
+ <View
204
+ style={[
205
+ styles.track,
206
+ {
207
+ width: metrics.width,
208
+ height: metrics.height,
209
+ backgroundColor: trackColor(isChecked, isDisabled, showHover),
210
+ },
211
+ showFocus && !isNative && webFocusRing(),
212
+ ]}>
213
+ <View
214
+ style={[
215
+ styles.thumb,
216
+ {
217
+ width: metrics.thumb,
218
+ height: metrics.thumb,
219
+ top: thumbInset,
220
+ left: thumbLeft,
221
+ },
222
+ !isDisabled && !isNative && webThumbShadow(),
223
+ ]}
224
+ />
225
+ </View>
226
+ </View>
227
+
228
+ {hasText ? (
229
+ <Label
230
+ embedded
231
+ text={text}
232
+ supportingText={supportingText}
233
+ style={[styles.content, hasSupporting && styles.contentFill]}
234
+ labelStyle={[styles.label, isDisabled && styles.labelDisabled]}
235
+ supportingStyle={[styles.supporting, isDisabled && styles.labelDisabled]}
236
+ />
237
+ ) : null}
238
+ </Pressable>
239
+ );
240
+ }
241
+
242
+ const styles = StyleSheet.create({
243
+ wrapper: {
244
+ flexDirection: 'row',
245
+ alignItems: 'flex-start',
246
+ alignSelf: 'center',
247
+ maxWidth: '100%',
248
+ },
249
+ wrapperWithText: {
250
+ gap: spacing.md,
251
+ maxWidth: '100%',
252
+ },
253
+ wrapperFill: {
254
+ maxWidth: '100%',
255
+ },
256
+ wrapperDisabled: {
257
+ opacity: 1,
258
+ },
259
+ trackHost: {
260
+ position: 'relative',
261
+ overflow: 'visible',
262
+ flexShrink: 0,
263
+ },
264
+ track: {
265
+ borderRadius: radius.lg,
266
+ overflow: 'visible',
267
+ },
268
+ thumb: {
269
+ position: 'absolute',
270
+ borderRadius: radius.full,
271
+ backgroundColor: colors.toggle.thumb,
272
+ },
273
+ content: {
274
+ flexGrow: 0,
275
+ flexShrink: 1,
276
+ minWidth: spacing.none,
277
+ maxWidth: '100%',
278
+ alignItems: 'stretch',
279
+ gap: spacing.sm,
280
+ },
281
+ contentFill: {
282
+ flexGrow: 1,
283
+ },
284
+ label: {
285
+ color: colors.toggle.label,
286
+ fontWeight: typography.fontWeight.medium,
287
+ },
288
+ supporting: {
289
+ color: colors.toggle.supporting,
290
+ fontWeight: typography.fontWeight.regular,
291
+ lineHeight: typography.fontSize.sm * typography.lineHeight.normal,
292
+ },
293
+ labelDisabled: {
294
+ color: colors.toggle.labelDisabled,
295
+ },
296
+ });
@@ -0,0 +1,2 @@
1
+ export { Toggle, ToggleSize, ToggleState, dismissToggleFocus } from './Toggle';
2
+ export type { ToggleProps } from './Toggle';