@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,230 @@
1
+ import { Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
2
+
3
+ import { colors, radius, typography } from '../../theme';
4
+ import { Icon, IconName, isIconName } from '../Icon';
5
+
6
+ export const BadgeSize = {
7
+ Small: 'sm',
8
+ Medium: 'md',
9
+ Large: 'lg',
10
+ } as const;
11
+
12
+ export type BadgeSize = (typeof BadgeSize)[keyof typeof BadgeSize];
13
+
14
+ export const BadgeColor = {
15
+ Gray: 'gray',
16
+ Primary: 'primary',
17
+ Error: 'error',
18
+ Warning: 'warning',
19
+ Success: 'success',
20
+ BlueGray: 'blue-gray',
21
+ BlueLight: 'blue-light',
22
+ Blue: 'blue',
23
+ Indigo: 'indigo',
24
+ Purple: 'purple',
25
+ Pink: 'pink',
26
+ Rose: 'rose',
27
+ Orange: 'orange',
28
+ } as const;
29
+
30
+ export type BadgeColor = (typeof BadgeColor)[keyof typeof BadgeColor];
31
+
32
+ export const BadgeIconPosition = {
33
+ Left: 'left',
34
+ Right: 'right',
35
+ } as const;
36
+
37
+ export type BadgeIconPosition = (typeof BadgeIconPosition)[keyof typeof BadgeIconPosition];
38
+
39
+ export type BadgeIconColor = BadgeColor;
40
+ export type BadgeIcon = 'none' | IconName;
41
+
42
+ export type BadgeProps = {
43
+ label?: string;
44
+ size?: BadgeSize;
45
+ color?: BadgeColor;
46
+ icon?: BadgeIcon;
47
+ iconPosition?: BadgeIconPosition;
48
+ iconColor?: BadgeIconColor;
49
+ removable?: boolean;
50
+ onRemove?: () => void;
51
+ style?: StyleProp<ViewStyle>;
52
+ };
53
+
54
+ const COLOR_TOKENS: Record<BadgeColor, { bg: string; text: string; icon: string }> = {
55
+ gray: colors.badge.gray,
56
+ primary: colors.badge.primary,
57
+ error: colors.badge.error,
58
+ warning: colors.badge.warning,
59
+ success: colors.badge.success,
60
+ 'blue-gray': colors.badge.blueGray,
61
+ 'blue-light': colors.badge.blueLight,
62
+ blue: colors.badge.blue,
63
+ indigo: colors.badge.indigo,
64
+ purple: colors.badge.purple,
65
+ pink: colors.badge.pink,
66
+ rose: colors.badge.rose,
67
+ orange: colors.badge.orange,
68
+ };
69
+
70
+ function BadgeGlyph({
71
+ icon,
72
+ iconColor,
73
+ position,
74
+ }: {
75
+ icon: BadgeIcon;
76
+ iconColor: BadgeIconColor;
77
+ position: BadgeIconPosition;
78
+ }) {
79
+ if (icon === 'none') {
80
+ return null;
81
+ }
82
+
83
+ if (icon === IconName.Country) {
84
+ return (
85
+ <View style={position === BadgeIconPosition.Right ? styles.countryRight : styles.country}>
86
+ <Icon name={IconName.Country} size={16} decorative />
87
+ </View>
88
+ );
89
+ }
90
+
91
+ if (!isIconName(icon)) {
92
+ return null;
93
+ }
94
+
95
+ const isDot = icon === IconName.Dot;
96
+ const tint = COLOR_TOKENS[iconColor].icon;
97
+
98
+ return (
99
+ <View style={position === BadgeIconPosition.Right ? styles.maskRight : undefined}>
100
+ <Icon name={icon} size={isDot ? 8 : 12} color={tint} decorative accessibilityLabel={icon} />
101
+ </View>
102
+ );
103
+ }
104
+
105
+ export function Badge({
106
+ label = 'Label',
107
+ size = BadgeSize.Medium,
108
+ color = BadgeColor.Gray,
109
+ icon = 'none',
110
+ iconPosition = BadgeIconPosition.Left,
111
+ iconColor = BadgeColor.Gray,
112
+ removable = false,
113
+ onRemove,
114
+ style,
115
+ }: BadgeProps) {
116
+ const palette = COLOR_TOKENS[color];
117
+ const showIcon = icon !== 'none';
118
+ const isDotOrCountry = icon === IconName.Dot || icon === IconName.Country;
119
+ const gapStyle = showIcon || removable ? (isDotOrCountry ? styles.gap6 : styles.gap4) : undefined;
120
+
121
+ return (
122
+ <View
123
+ style={[
124
+ styles.badge,
125
+ SIZE_STYLES[size],
126
+ { backgroundColor: palette.bg },
127
+ gapStyle,
128
+ removable && styles.removable,
129
+ style,
130
+ ]}>
131
+ {showIcon && iconPosition === BadgeIconPosition.Left ? (
132
+ <BadgeGlyph icon={icon} iconColor={iconColor} position={iconPosition} />
133
+ ) : null}
134
+ <Text
135
+ style={[styles.label, SIZE_LABEL[size], { color: palette.text, fontFamily: typography.fontFamily.base }]}
136
+ numberOfLines={1}>
137
+ {label}
138
+ </Text>
139
+ {showIcon && iconPosition === BadgeIconPosition.Right ? (
140
+ <BadgeGlyph icon={icon} iconColor={iconColor} position={iconPosition} />
141
+ ) : null}
142
+ {removable ? (
143
+ <Pressable
144
+ accessibilityRole="button"
145
+ accessibilityLabel="Remove"
146
+ hitSlop={8}
147
+ onPress={onRemove}
148
+ style={styles.close}>
149
+ <Icon name={IconName.Close} size={6} color={palette.text} decorative />
150
+ </Pressable>
151
+ ) : null}
152
+ </View>
153
+ );
154
+ }
155
+
156
+ const SIZE_STYLES = StyleSheet.create({
157
+ sm: {
158
+ minWidth: 48,
159
+ height: 22,
160
+ paddingVertical: 2,
161
+ paddingHorizontal: 8,
162
+ },
163
+ md: {
164
+ minWidth: 57,
165
+ height: 24,
166
+ paddingVertical: 2,
167
+ paddingHorizontal: 10,
168
+ },
169
+ lg: {
170
+ minWidth: 61,
171
+ height: 28,
172
+ paddingVertical: 4,
173
+ paddingHorizontal: 12,
174
+ },
175
+ });
176
+
177
+ const SIZE_LABEL = StyleSheet.create({
178
+ sm: {
179
+ fontSize: 12,
180
+ lineHeight: 18,
181
+ },
182
+ md: {
183
+ fontSize: typography.fontSize.sm,
184
+ lineHeight: 20,
185
+ },
186
+ lg: {
187
+ fontSize: typography.fontSize.sm,
188
+ lineHeight: 20,
189
+ },
190
+ });
191
+
192
+ const styles = StyleSheet.create({
193
+ badge: {
194
+ flexDirection: 'row',
195
+ alignItems: 'center',
196
+ justifyContent: 'center',
197
+ alignSelf: 'flex-start',
198
+ borderRadius: radius.xl,
199
+ },
200
+ gap4: {
201
+ gap: 4,
202
+ },
203
+ gap6: {
204
+ gap: 6,
205
+ },
206
+ removable: {
207
+ paddingEnd: 2,
208
+ },
209
+ label: {
210
+ fontWeight: typography.fontWeight.medium,
211
+ textAlign: 'center',
212
+ },
213
+ country: {
214
+ marginLeft: -2,
215
+ marginRight: -5,
216
+ transform: [{ translateY: -2 }],
217
+ },
218
+ countryRight: {
219
+ transform: [{ translateY: -3 }],
220
+ },
221
+ maskRight: {
222
+ transform: [{ translateY: -1 }],
223
+ },
224
+ close: {
225
+ width: 12,
226
+ height: 12,
227
+ alignItems: 'center',
228
+ justifyContent: 'center',
229
+ },
230
+ });
@@ -0,0 +1,2 @@
1
+ export { Badge, BadgeSize, BadgeColor, BadgeIconPosition } from './Badge';
2
+ export type { BadgeProps, BadgeIcon, BadgeIconColor } from './Badge';
@@ -0,0 +1,364 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ Pressable,
4
+ ScrollView,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ type StyleProp,
9
+ type ViewStyle,
10
+ } from 'react-native';
11
+
12
+ import { button as buttonTokens, colors, radius, spacing, typography } from '../../theme';
13
+ import { a11yHiddenProps } from '../../utils/a11y';
14
+ import { Icon, IconName } from '../Icon';
15
+
16
+ export const BreadcrumbVariant = {
17
+ Default: 'default',
18
+ Card: 'card',
19
+ } as const;
20
+
21
+ export type BreadcrumbVariant = (typeof BreadcrumbVariant)[keyof typeof BreadcrumbVariant];
22
+
23
+ export const BreadcrumbSeparator = {
24
+ Chevron: 'chevron',
25
+ Slash: 'slash',
26
+ Arrow: 'arrow',
27
+ Caret: 'caret',
28
+ } as const;
29
+
30
+ export type BreadcrumbSeparator = (typeof BreadcrumbSeparator)[keyof typeof BreadcrumbSeparator];
31
+
32
+ export const BreadcrumbSlotKind = {
33
+ Crumb: 'crumb',
34
+ Ellipsis: 'ellipsis',
35
+ } as const;
36
+
37
+ export type BreadcrumbSlotKind = (typeof BreadcrumbSlotKind)[keyof typeof BreadcrumbSlotKind];
38
+
39
+ export type BreadcrumbItem = {
40
+ label: string;
41
+ routerLink?: unknown[] | string | null;
42
+ href?: string | null;
43
+ };
44
+
45
+ export type BreadcrumbRow = {
46
+ kind: BreadcrumbSlotKind;
47
+ label: string;
48
+ linkHref: string | null;
49
+ isCurrent: boolean;
50
+ showLeadingSep: boolean;
51
+ };
52
+
53
+ export type BreadcrumbProps = {
54
+ items?: BreadcrumbItem[];
55
+ maxLeadingSegments?: number;
56
+ variant?: BreadcrumbVariant;
57
+ separator?: BreadcrumbSeparator;
58
+ showItemIcon?: boolean;
59
+ onItemPress?: (item: { label: string; href: string }) => void;
60
+ style?: StyleProp<ViewStyle>;
61
+ };
62
+
63
+ const SEPARATOR_ICON: Record<BreadcrumbSeparator, IconName> = {
64
+ [BreadcrumbSeparator.Chevron]: IconName.ChevronRight,
65
+ [BreadcrumbSeparator.Slash]: IconName.Slash,
66
+ [BreadcrumbSeparator.Arrow]: IconName.ArrowRight,
67
+ [BreadcrumbSeparator.Caret]: IconName.CaretRight,
68
+ };
69
+
70
+ const SEPARATOR_SIZE: Record<BreadcrumbSeparator, number> = {
71
+ [BreadcrumbSeparator.Chevron]: spacing.base,
72
+ [BreadcrumbSeparator.Slash]: spacing.lg,
73
+ [BreadcrumbSeparator.Arrow]: spacing.lg,
74
+ [BreadcrumbSeparator.Caret]: spacing.lg,
75
+ };
76
+
77
+ const ITEM_ICON_SIZE = spacing.lg;
78
+
79
+ type Slot =
80
+ | { kind: typeof BreadcrumbSlotKind.Crumb; item: BreadcrumbItem; isCurrent: boolean }
81
+ | { kind: typeof BreadcrumbSlotKind.Ellipsis };
82
+
83
+ function normalizeItems(items: BreadcrumbItem[]): BreadcrumbItem[] {
84
+ if (!Array.isArray(items)) {
85
+ return [];
86
+ }
87
+ return items.map((entry) => {
88
+ const o = entry as Record<string, unknown>;
89
+ const rawLabel = o.label ?? o.name ?? o.text ?? '';
90
+ return {
91
+ label: typeof rawLabel === 'string' ? rawLabel : String(rawLabel ?? ''),
92
+ routerLink: (o.routerLink as BreadcrumbItem['routerLink']) ?? null,
93
+ href: typeof o.href === 'string' && o.href.length ? o.href : null,
94
+ };
95
+ });
96
+ }
97
+
98
+ function resolveHref(item: BreadcrumbItem, isCurrent: boolean): string | null {
99
+ if (isCurrent) {
100
+ return null;
101
+ }
102
+ if (item.href) {
103
+ return item.href;
104
+ }
105
+ const rl = item.routerLink;
106
+ if (rl === null || rl === undefined || rl === '') {
107
+ return null;
108
+ }
109
+ if (Array.isArray(rl)) {
110
+ if (!rl.length) {
111
+ return null;
112
+ }
113
+ return rl
114
+ .map((segment, index) => {
115
+ const str = String(segment ?? '');
116
+ return index === 0 ? str : str.replace(/^\//, '');
117
+ })
118
+ .join('/');
119
+ }
120
+ return String(rl);
121
+ }
122
+
123
+ function buildSlots(items: BreadcrumbItem[], maxLeadingSegments: number): Slot[] {
124
+ if (!items.length) {
125
+ return [];
126
+ }
127
+ const maxLead = Math.max(1, maxLeadingSegments);
128
+ const maxItemsWithoutEllipsis = maxLead + 1;
129
+ if (items.length <= maxItemsWithoutEllipsis) {
130
+ return items.map((item, index) => ({
131
+ kind: BreadcrumbSlotKind.Crumb,
132
+ item,
133
+ isCurrent: index === items.length - 1,
134
+ }));
135
+ }
136
+ const head: Slot[] = items.slice(0, maxLead).map((item) => ({
137
+ kind: BreadcrumbSlotKind.Crumb,
138
+ item,
139
+ isCurrent: false,
140
+ }));
141
+ const tail: Slot = {
142
+ kind: BreadcrumbSlotKind.Crumb,
143
+ item: items[items.length - 1],
144
+ isCurrent: true,
145
+ };
146
+ return [...head, { kind: BreadcrumbSlotKind.Ellipsis }, tail];
147
+ }
148
+
149
+ function buildRows(items: BreadcrumbItem[], maxLeadingSegments: number): BreadcrumbRow[] {
150
+ return buildSlots(items, maxLeadingSegments).map((slot, index) => {
151
+ if (slot.kind === BreadcrumbSlotKind.Ellipsis) {
152
+ return {
153
+ kind: BreadcrumbSlotKind.Ellipsis,
154
+ label: '…',
155
+ linkHref: null,
156
+ isCurrent: false,
157
+ showLeadingSep: index > 0,
158
+ };
159
+ }
160
+ return {
161
+ kind: BreadcrumbSlotKind.Crumb,
162
+ label: slot.item.label,
163
+ linkHref: resolveHref(slot.item, slot.isCurrent),
164
+ isCurrent: slot.isCurrent,
165
+ showLeadingSep: index > 0,
166
+ };
167
+ });
168
+ }
169
+
170
+ function asCurrentOnly(item: BreadcrumbItem): BreadcrumbItem {
171
+ return { label: item.label };
172
+ }
173
+
174
+ export function Breadcrumb({
175
+ items = [],
176
+ maxLeadingSegments = 3,
177
+ variant = BreadcrumbVariant.Default,
178
+ separator = BreadcrumbSeparator.Chevron,
179
+ showItemIcon = false,
180
+ onItemPress,
181
+ style,
182
+ }: BreadcrumbProps) {
183
+ const [trail, setTrail] = useState(() => normalizeItems(items));
184
+
185
+ useEffect(() => {
186
+ setTrail(normalizeItems(items));
187
+ }, [items]);
188
+
189
+ const rows = useMemo(() => buildRows(trail, maxLeadingSegments), [trail, maxLeadingSegments]);
190
+ const isSoloItem = trail.length === 1;
191
+ const isCard = variant === BreadcrumbVariant.Card;
192
+ const fontSize = isCard ? typography.fontSize.sm : typography.fontSize.md;
193
+ const lineHeight = isCard ? spacing['2xl'] : Math.round(fontSize * 1.3);
194
+ const linkColor = isCard
195
+ ? colors.breadcrumb.cardText
196
+ : showItemIcon
197
+ ? colors.breadcrumb.withIconsText
198
+ : colors.breadcrumb.normalText;
199
+ const currentColor = isCard ? colors.breadcrumb.cardCurrent : colors.breadcrumb.link;
200
+ const soloColor = isCard
201
+ ? colors.breadcrumb.cardText
202
+ : showItemIcon
203
+ ? colors.breadcrumb.withIconsText
204
+ : colors.breadcrumb.normalText;
205
+ const ellipsisColor = isCard
206
+ ? colors.breadcrumb.cardText
207
+ : showItemIcon
208
+ ? colors.breadcrumb.withIconsText
209
+ : colors.breadcrumb.normalText;
210
+ const sepIcon = SEPARATOR_ICON[separator];
211
+ const sepSize = SEPARATOR_SIZE[separator];
212
+ const hitSlop = crumbHitSlop(lineHeight);
213
+
214
+ function circleIconFor(isCurrent: boolean) {
215
+ return isCurrent && !isSoloItem ? IconName.CircleRadioActive : IconName.CircleRadio;
216
+ }
217
+
218
+ function onCrumbPress(row: BreadcrumbRow) {
219
+ if (!row.linkHref) {
220
+ return;
221
+ }
222
+ const index = trail.findIndex((item, itemIndex) => {
223
+ const href = resolveHref(item, itemIndex === trail.length - 1);
224
+ return item.label === row.label && href === row.linkHref;
225
+ });
226
+ if (index >= 0) {
227
+ setTrail(trail.slice(0, index + 1).map((item, itemIndex, list) => (itemIndex === list.length - 1 ? asCurrentOnly(item) : item)));
228
+ }
229
+ onItemPress?.({ label: row.label, href: row.linkHref });
230
+ }
231
+
232
+ return (
233
+ <View accessibilityLabel="breadcrumb" style={[styles.nav, isCard && styles.navCard, style]}>
234
+ <ScrollView
235
+ horizontal
236
+ bounces={false}
237
+ showsHorizontalScrollIndicator={false}
238
+ contentContainerStyle={styles.list}>
239
+ {rows.map((row, index) => {
240
+ const labelColor = row.isCurrent ? (isSoloItem ? soloColor : currentColor) : linkColor;
241
+ const itemIcon = showItemIcon ? (
242
+ <Icon name={circleIconFor(row.isCurrent)} size={ITEM_ICON_SIZE} color={labelColor} decorative />
243
+ ) : null;
244
+
245
+ return (
246
+ <View key={`${row.kind}-${index}-${row.label}`} style={styles.item} accessibilityState={{ selected: row.isCurrent }}>
247
+ <View style={styles.cell}>
248
+ {row.showLeadingSep ? (
249
+ <View style={styles.sep} {...a11yHiddenProps()}>
250
+ <Icon name={sepIcon} size={sepSize} color={colors.breadcrumb.normalText} decorative />
251
+ </View>
252
+ ) : null}
253
+
254
+ {row.kind === BreadcrumbSlotKind.Ellipsis ? (
255
+ <Text
256
+ {...a11yHiddenProps(true, 'no')}
257
+ style={[styles.ellipsis, { fontSize, color: ellipsisColor, lineHeight }]}>
258
+ {row.label}
259
+ </Text>
260
+ ) : row.linkHref ? (
261
+ <Pressable
262
+ accessibilityRole="link"
263
+ accessibilityLabel={row.label}
264
+ hitSlop={hitSlop}
265
+ onPress={() => onCrumbPress(row)}
266
+ style={styles.crumb}>
267
+ {({ pressed }) => (
268
+ <>
269
+ {itemIcon}
270
+ <Text
271
+ style={[
272
+ styles.label,
273
+ {
274
+ fontSize,
275
+ lineHeight,
276
+ color: pressed ? currentColor : labelColor,
277
+ textDecorationLine: pressed ? 'underline' : 'none',
278
+ },
279
+ ]}
280
+ numberOfLines={1}>
281
+ {row.label}
282
+ </Text>
283
+ </>
284
+ )}
285
+ </Pressable>
286
+ ) : (
287
+ <View
288
+ style={styles.crumb}
289
+ accessibilityRole="text"
290
+ accessibilityLabel={row.label}
291
+ accessibilityState={{ selected: row.isCurrent }}>
292
+ {itemIcon}
293
+ <Text numberOfLines={1} style={[styles.label, { fontSize, lineHeight, color: labelColor }]}>
294
+ {row.label}
295
+ </Text>
296
+ </View>
297
+ )}
298
+ </View>
299
+ </View>
300
+ );
301
+ })}
302
+ </ScrollView>
303
+ </View>
304
+ );
305
+ }
306
+
307
+ function crumbHitSlop(lineHeight: number) {
308
+ const extra = Math.max(0, (buttonTokens.minTouch - lineHeight) / 2);
309
+ if (extra === 0) {
310
+ return undefined;
311
+ }
312
+ return { top: extra, bottom: extra, left: extra, right: extra };
313
+ }
314
+
315
+ const styles = StyleSheet.create({
316
+ nav: {
317
+ minHeight: spacing['2xl'],
318
+ alignSelf: 'stretch',
319
+ width: '100%',
320
+ maxWidth: '100%',
321
+ },
322
+ navCard: {
323
+ backgroundColor: colors.breadcrumb.cardBg,
324
+ borderRadius: radius.md,
325
+ paddingVertical: spacing.sm,
326
+ paddingHorizontal: spacing.base,
327
+ },
328
+ list: {
329
+ flexDirection: 'row',
330
+ flexWrap: 'nowrap',
331
+ alignItems: 'center',
332
+ gap: spacing.xs,
333
+ flexGrow: 1,
334
+ },
335
+ item: {
336
+ flexDirection: 'row',
337
+ alignItems: 'center',
338
+ flexShrink: 0,
339
+ },
340
+ cell: {
341
+ flexDirection: 'row',
342
+ alignItems: 'center',
343
+ gap: spacing.xs,
344
+ },
345
+ sep: {
346
+ width: spacing.lg,
347
+ height: spacing.lg,
348
+ alignItems: 'center',
349
+ justifyContent: 'center',
350
+ },
351
+ crumb: {
352
+ flexDirection: 'row',
353
+ alignItems: 'center',
354
+ gap: spacing.sm,
355
+ },
356
+ label: {
357
+ fontFamily: typography.fontFamily.base,
358
+ fontWeight: typography.fontWeight.medium,
359
+ },
360
+ ellipsis: {
361
+ fontFamily: typography.fontFamily.base,
362
+ fontWeight: typography.fontWeight.medium,
363
+ },
364
+ });
@@ -0,0 +1,7 @@
1
+ export {
2
+ Breadcrumb,
3
+ BreadcrumbVariant,
4
+ BreadcrumbSeparator,
5
+ BreadcrumbSlotKind,
6
+ } from './Breadcrumb';
7
+ export type { BreadcrumbProps, BreadcrumbItem, BreadcrumbRow } from './Breadcrumb';