@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,393 @@
1
+ import { Platform, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
2
+ import Svg, { Circle, Defs, LinearGradient, Path, Stop } from 'react-native-svg';
3
+
4
+ import { borders, colors, radius, segmentActiveShadow, spacing, typography } from '../../theme';
5
+ import { a11yHiddenProps } from '../../utils/a11y';
6
+ import { Icon, IconName, isIconName } from '../Icon';
7
+
8
+ export const StatCardIconTone = {
9
+ Success: 'success',
10
+ Primary: 'primary',
11
+ Warning: 'warning',
12
+ Error: 'error',
13
+ Info: 'info',
14
+ Gray: 'gray',
15
+ } as const;
16
+
17
+ export type StatCardIconTone = (typeof StatCardIconTone)[keyof typeof StatCardIconTone];
18
+
19
+ export const StatCardTrendDirection = {
20
+ Up: 'up',
21
+ Down: 'down',
22
+ Neutral: 'neutral',
23
+ } as const;
24
+
25
+ export type StatCardTrendDirection =
26
+ (typeof StatCardTrendDirection)[keyof typeof StatCardTrendDirection];
27
+
28
+ export type StatCardProps = {
29
+ label: string;
30
+ value: string | number;
31
+ valueSuffix?: string;
32
+ subtitle?: string;
33
+ icon?: IconName | string;
34
+ iconTone?: StatCardIconTone;
35
+ footerIcon?: IconName | string;
36
+ trend?: string;
37
+ trendDirection?: StatCardTrendDirection;
38
+ sparkline?: number[] | false;
39
+ style?: StyleProp<ViewStyle>;
40
+ };
41
+
42
+ const FONT = {
43
+ family: Platform.select({
44
+ web: 'Inter, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
45
+ default: typography.fontFamily.base,
46
+ }),
47
+ label: {
48
+ fontSize: typography.fontSize.sm,
49
+ fontWeight: typography.fontWeight.medium,
50
+ lineHeight: Math.round(typography.fontSize.sm * 1.35),
51
+ },
52
+ value: {
53
+ fontSize: typography.fontSize['2xl'],
54
+ fontWeight: typography.fontWeight.bold,
55
+ lineHeight: Math.round(typography.fontSize['2xl'] * 1.15),
56
+ },
57
+ valueSuffix: {
58
+ fontSize: typography.fontSize.sm,
59
+ fontWeight: typography.fontWeight.regular,
60
+ lineHeight: Math.round(typography.fontSize.sm * 1.35),
61
+ },
62
+ subtitle: {
63
+ fontSize: typography.fontSize.sm,
64
+ fontWeight: typography.fontWeight.regular,
65
+ lineHeight: Math.round(typography.fontSize.sm * 1.4),
66
+ },
67
+ trend: {
68
+ fontSize: typography.fontSize.sm,
69
+ fontWeight: typography.fontWeight.medium,
70
+ lineHeight: Math.round(typography.fontSize.sm * 1.35),
71
+ },
72
+ } as const;
73
+
74
+ const LAYOUT = {
75
+ maxWidth: 232,
76
+ padding: spacing.base,
77
+ iconWrap: spacing['2xl'],
78
+ icon: spacing.base,
79
+ footerIcon: spacing.base,
80
+ sparklineW: 88,
81
+ sparklineH: 36,
82
+ } as const;
83
+
84
+ const DEFAULT_SPARKLINE = [0.35, 0.28, 0.42, 0.38, 0.55, 0.48, 0.62, 0.58, 0.72, 0.68, 0.82, 0.9];
85
+
86
+ const ICON_TONE_BG: Record<StatCardIconTone, string> = {
87
+ success: colors.statCard.iconSuccessBg,
88
+ primary: colors.statCard.iconBg,
89
+ warning: colors.statCard.iconWarningBg,
90
+ error: colors.statCard.iconErrorBg,
91
+ info: colors.statCard.iconInfoBg,
92
+ gray: colors.statCard.iconGrayBg,
93
+ };
94
+
95
+ function resolveTrendDirection(
96
+ trend: string | undefined,
97
+ trendDirection: StatCardTrendDirection | undefined,
98
+ ): StatCardTrendDirection {
99
+ const raw = (trend ?? '').trim();
100
+ if (raw.startsWith('-')) {
101
+ return StatCardTrendDirection.Down;
102
+ }
103
+ if (raw.startsWith('+')) {
104
+ return StatCardTrendDirection.Up;
105
+ }
106
+ if (trendDirection) {
107
+ return trendDirection;
108
+ }
109
+ return StatCardTrendDirection.Neutral;
110
+ }
111
+
112
+ function trendPillColors(direction: StatCardTrendDirection) {
113
+ switch (direction) {
114
+ case StatCardTrendDirection.Up:
115
+ return { bg: colors.badge.success.bg, text: colors.badge.success.text };
116
+ case StatCardTrendDirection.Down:
117
+ return { bg: colors.badge.error.bg, text: colors.badge.error.text };
118
+ default:
119
+ return { bg: colors.badge.gray.bg, text: colors.badge.gray.text };
120
+ }
121
+ }
122
+
123
+ function trendArrow(direction: StatCardTrendDirection) {
124
+ switch (direction) {
125
+ case StatCardTrendDirection.Up:
126
+ return '↑';
127
+ case StatCardTrendDirection.Down:
128
+ return '↓';
129
+ default:
130
+ return '→';
131
+ }
132
+ }
133
+
134
+ const SPARK_DOT_R = 3.5;
135
+ const SPARK_PAD = SPARK_DOT_R + 1;
136
+
137
+ function buildSparklinePath(points: number[], width: number, height: number) {
138
+ if (points.length < 2) {
139
+ return { line: '', fill: '', last: { x: width / 2, y: height / 2 } };
140
+ }
141
+
142
+ const innerW = width - SPARK_PAD * 2;
143
+ const innerH = height - SPARK_PAD * 2;
144
+ const stepX = innerW / (points.length - 1);
145
+ const coords = points.map((p, i) => {
146
+ const clamped = Math.min(1, Math.max(0, p));
147
+ return {
148
+ x: SPARK_PAD + i * stepX,
149
+ y: SPARK_PAD + (1 - clamped) * innerH,
150
+ };
151
+ });
152
+
153
+ let line = `M ${coords[0].x} ${coords[0].y}`;
154
+ for (let i = 1; i < coords.length; i += 1) {
155
+ const prev = coords[i - 1];
156
+ const curr = coords[i];
157
+ const cpx = (prev.x + curr.x) / 2;
158
+ line += ` C ${cpx} ${prev.y}, ${cpx} ${curr.y}, ${curr.x} ${curr.y}`;
159
+ }
160
+
161
+ const last = coords[coords.length - 1];
162
+ const fill = `${line} L ${last.x} ${height} L ${coords[0].x} ${height} Z`;
163
+
164
+ return { line, fill, last };
165
+ }
166
+
167
+ function Sparkline({ points, color }: { points: number[]; color: string }) {
168
+ const { line, fill, last } = buildSparklinePath(points, LAYOUT.sparklineW, LAYOUT.sparklineH);
169
+
170
+ return (
171
+ <Svg
172
+ width={LAYOUT.sparklineW}
173
+ height={LAYOUT.sparklineH}
174
+ viewBox={`0 0 ${LAYOUT.sparklineW} ${LAYOUT.sparklineH}`}
175
+ {...a11yHiddenProps()}>
176
+ <Defs>
177
+ <LinearGradient id="statSparkFill" x1="0" y1="0" x2="0" y2="1">
178
+ <Stop offset="0" stopColor={color} stopOpacity="0.22" />
179
+ <Stop offset="1" stopColor={color} stopOpacity="0.02" />
180
+ </LinearGradient>
181
+ </Defs>
182
+ <Path d={fill} fill="url(#statSparkFill)" />
183
+ <Path
184
+ d={line}
185
+ stroke={color}
186
+ strokeWidth={2.25}
187
+ fill="none"
188
+ strokeLinecap="round"
189
+ strokeLinejoin="round"
190
+ />
191
+ <Circle cx={last.x} cy={last.y} r={SPARK_DOT_R} fill={color} />
192
+ </Svg>
193
+ );
194
+ }
195
+
196
+ const softShadow: ViewStyle =
197
+ Platform.OS === 'web'
198
+ ? ({ boxShadow: `0 8px 24px ${colors.boxShadowLight}` } as ViewStyle)
199
+ : {
200
+ ...segmentActiveShadow(),
201
+ shadowOpacity: 0.06,
202
+ shadowRadius: 12,
203
+ shadowOffset: { width: 0, height: 6 },
204
+ };
205
+
206
+ export function StatCard({
207
+ label,
208
+ value,
209
+ valueSuffix,
210
+ subtitle,
211
+ icon,
212
+ iconTone = StatCardIconTone.Primary,
213
+ footerIcon = IconName.Success,
214
+ trend,
215
+ trendDirection,
216
+ sparkline,
217
+ style,
218
+ }: StatCardProps) {
219
+ const showIcon = Boolean(icon && isIconName(icon));
220
+ const showTrend = Boolean(trend);
221
+ const showFooter = Boolean(subtitle);
222
+ const showFooterIcon = Boolean(footerIcon && isIconName(footerIcon));
223
+ const direction = resolveTrendDirection(trend, trendDirection);
224
+ const pill = trendPillColors(direction);
225
+ const accent = ICON_TONE_BG[iconTone];
226
+ const sparkPoints = sparkline === false ? null : (sparkline ?? DEFAULT_SPARKLINE);
227
+ const iconColor = colors.statCard.iconColor;
228
+
229
+ return (
230
+ <View
231
+ accessibilityRole="summary"
232
+ accessibilityLabel={label}
233
+ style={[styles.card, style]}>
234
+ <View style={styles.body}>
235
+ <View style={styles.topRow}>
236
+ {showIcon ? (
237
+ <View
238
+ style={[styles.iconWrap, { backgroundColor: accent }]}
239
+ {...a11yHiddenProps()}>
240
+ <Icon name={icon as string} size={LAYOUT.icon} color={iconColor} decorative />
241
+ </View>
242
+ ) : (
243
+ <View style={styles.topSpacer} />
244
+ )}
245
+
246
+ {showTrend ? (
247
+ <View style={[styles.trendPill, { backgroundColor: pill.bg }]}>
248
+ <Text style={[styles.trendPillText, { color: pill.text }]}>
249
+ {trendArrow(direction)} {trend}
250
+ </Text>
251
+ </View>
252
+ ) : null}
253
+ </View>
254
+
255
+ <View style={styles.metricRow}>
256
+ <View style={styles.metricText}>
257
+ <Text style={styles.label} numberOfLines={2}>
258
+ {label}
259
+ </Text>
260
+ <View style={styles.valueRow}>
261
+ <Text style={[styles.valueMain, { color: accent }]}>{value}</Text>
262
+ {valueSuffix ? <Text style={styles.valueSuffix}>{valueSuffix}</Text> : null}
263
+ </View>
264
+ </View>
265
+
266
+ {sparkPoints ? (
267
+ <View style={styles.sparklineWrap}>
268
+ <Sparkline points={sparkPoints} color={accent} />
269
+ </View>
270
+ ) : null}
271
+ </View>
272
+ </View>
273
+
274
+ {showFooter ? (
275
+ <View style={styles.footer}>
276
+ {showFooterIcon ? (
277
+ <Icon name={footerIcon as string} size={LAYOUT.footerIcon} color={accent} decorative />
278
+ ) : null}
279
+ <Text style={styles.subtitle} numberOfLines={1}>
280
+ {subtitle}
281
+ </Text>
282
+ </View>
283
+ ) : null}
284
+ </View>
285
+ );
286
+ }
287
+
288
+ const styles = StyleSheet.create({
289
+ card: {
290
+ width: '100%',
291
+ maxWidth: LAYOUT.maxWidth,
292
+ minWidth: 0,
293
+ backgroundColor: colors.statCard.surface,
294
+ borderRadius: radius.xl,
295
+ borderWidth: borders.width.thin,
296
+ borderColor: colors.borderSubtle,
297
+ overflow: 'hidden',
298
+ ...softShadow,
299
+ },
300
+ body: {
301
+ gap: spacing.sm,
302
+ padding: LAYOUT.padding,
303
+ },
304
+ topRow: {
305
+ flexDirection: 'row',
306
+ alignItems: 'center',
307
+ justifyContent: 'space-between',
308
+ gap: spacing.sm,
309
+ },
310
+ topSpacer: {
311
+ flex: 1,
312
+ },
313
+ iconWrap: {
314
+ alignItems: 'center',
315
+ justifyContent: 'center',
316
+ width: LAYOUT.iconWrap,
317
+ height: LAYOUT.iconWrap,
318
+ borderRadius: radius.md,
319
+ },
320
+ trendPill: {
321
+ flexDirection: 'row',
322
+ alignItems: 'center',
323
+ paddingHorizontal: spacing.sm,
324
+ paddingVertical: spacing.xs,
325
+ borderRadius: radius.pill,
326
+ },
327
+ trendPillText: {
328
+ fontFamily: FONT.family,
329
+ fontSize: FONT.trend.fontSize,
330
+ fontWeight: FONT.trend.fontWeight,
331
+ lineHeight: FONT.trend.lineHeight,
332
+ },
333
+ metricRow: {
334
+ flexDirection: 'row',
335
+ alignItems: 'flex-end',
336
+ justifyContent: 'space-between',
337
+ gap: spacing.sm,
338
+ minWidth: 0,
339
+ },
340
+ metricText: {
341
+ flex: 1,
342
+ minWidth: 0,
343
+ gap: spacing.xs,
344
+ },
345
+ label: {
346
+ fontFamily: FONT.family,
347
+ fontSize: FONT.label.fontSize,
348
+ fontWeight: FONT.label.fontWeight,
349
+ color: colors.statCard.label,
350
+ lineHeight: FONT.label.lineHeight,
351
+ },
352
+ valueRow: {
353
+ flexDirection: 'row',
354
+ alignItems: 'baseline',
355
+ gap: spacing.xs,
356
+ },
357
+ valueMain: {
358
+ fontFamily: FONT.family,
359
+ fontSize: FONT.value.fontSize,
360
+ fontWeight: FONT.value.fontWeight,
361
+ lineHeight: FONT.value.lineHeight,
362
+ },
363
+ valueSuffix: {
364
+ fontFamily: FONT.family,
365
+ fontSize: FONT.valueSuffix.fontSize,
366
+ fontWeight: FONT.valueSuffix.fontWeight,
367
+ color: colors.statCard.subtitle,
368
+ lineHeight: FONT.valueSuffix.lineHeight,
369
+ },
370
+ sparklineWrap: {
371
+ flexShrink: 0,
372
+ marginBottom: spacing.none,
373
+ },
374
+ footer: {
375
+ flexDirection: 'row',
376
+ alignItems: 'center',
377
+ gap: spacing.sm,
378
+ paddingHorizontal: LAYOUT.padding,
379
+ paddingVertical: spacing.sm,
380
+ borderTopWidth: borders.width.thin,
381
+ borderTopColor: colors.borderSubtle,
382
+ backgroundColor: colors.surfaceMuted,
383
+ },
384
+ subtitle: {
385
+ flex: 1,
386
+ minWidth: 0,
387
+ fontFamily: FONT.family,
388
+ fontSize: FONT.subtitle.fontSize,
389
+ fontWeight: FONT.subtitle.fontWeight,
390
+ color: colors.statCard.subtitle,
391
+ lineHeight: FONT.subtitle.lineHeight,
392
+ },
393
+ });
@@ -0,0 +1,6 @@
1
+ export {
2
+ StatCard,
3
+ StatCardIconTone,
4
+ StatCardTrendDirection,
5
+ } from './StatCard';
6
+ export type { StatCardProps } from './StatCard';
@@ -0,0 +1,239 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { Pressable, ScrollView, 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 { Button, ButtonSize, ButtonVariant } from '../Button';
6
+
7
+ export const TabsVariant = {
8
+ Container: 'container',
9
+ Undergo: 'undergo',
10
+ } as const;
11
+
12
+ export type TabsVariant = (typeof TabsVariant)[keyof typeof TabsVariant];
13
+
14
+ export type TabsProps = {
15
+ tabCount?: number;
16
+ tabLabels?: string[];
17
+ activeIndex?: number;
18
+ variant?: TabsVariant;
19
+ onActiveIndexChange?: (index: number) => void;
20
+ style?: StyleProp<ViewStyle>;
21
+ };
22
+
23
+ const MIN_TABS = 2;
24
+ const MAX_TABS = 6;
25
+
26
+ function tabCountInRange(tabCount: number) {
27
+ return Math.min(MAX_TABS, Math.max(MIN_TABS, Math.floor(tabCount)));
28
+ }
29
+
30
+ function clampIndex(index: number, length: number) {
31
+ const max = Math.max(0, length - 1);
32
+ return Math.min(max, Math.max(0, Math.floor(index)));
33
+ }
34
+
35
+ function tabLabel(index: number, tabLabels: string[]) {
36
+ const fromInput = tabLabels[index];
37
+ if (fromInput !== undefined && fromInput !== '') {
38
+ return fromInput;
39
+ }
40
+ return `Tab ${index + 1}`;
41
+ }
42
+
43
+ export function Tabs({
44
+ tabCount = MIN_TABS,
45
+ tabLabels = [],
46
+ activeIndex = 0,
47
+ variant = TabsVariant.Container,
48
+ onActiveIndexChange,
49
+ style,
50
+ }: TabsProps) {
51
+ const length = tabCountInRange(tabCount);
52
+ const tabs = useMemo(() => Array.from({ length }, (_, index) => index), [length]);
53
+ const [selected, setSelected] = useState(() => clampIndex(activeIndex, length));
54
+
55
+ useEffect(() => {
56
+ setSelected(clampIndex(activeIndex, length));
57
+ }, [activeIndex, length]);
58
+
59
+ const isUndergo = variant === TabsVariant.Undergo;
60
+ const lineHeight = Math.round(typography.fontSize.sm * 1.3);
61
+ const hitExtra = Math.max(0, (buttonTokens.minTouch - lineHeight) / 2);
62
+ const tabHitSlop = hitExtra
63
+ ? { top: hitExtra, bottom: hitExtra, left: hitExtra, right: hitExtra }
64
+ : undefined;
65
+
66
+ function selectTab(index: number) {
67
+ const next = clampIndex(index, length);
68
+ if (next === selected) {
69
+ return;
70
+ }
71
+ setSelected(next);
72
+ onActiveIndexChange?.(next);
73
+ }
74
+
75
+ return (
76
+ <View
77
+ accessibilityRole="tablist"
78
+ style={[styles.host, isUndergo ? styles.hostUndergo : styles.hostContainer, style]}>
79
+ <View style={isUndergo ? styles.undergoFrame : styles.containerRule}>
80
+ <ScrollView
81
+ horizontal
82
+ bounces={false}
83
+ showsHorizontalScrollIndicator={false}
84
+ style={styles.scroller}
85
+ contentContainerStyle={[styles.list, isUndergo ? styles.listUndergo : styles.listContainer]}>
86
+ {tabs.map((index) => {
87
+ const active = index === selected;
88
+ const label = tabLabel(index, tabLabels);
89
+ return (
90
+ <View
91
+ key={index}
92
+ style={[
93
+ styles.item,
94
+ isUndergo && styles.itemUndergo,
95
+ isUndergo && active && styles.itemUndergoActive,
96
+ !isUndergo && styles.itemContainer,
97
+ ]}>
98
+ {isUndergo ? (
99
+ <Button
100
+ label={label}
101
+ variant={ButtonVariant.TertiaryGray}
102
+ size={ButtonSize.Medium}
103
+ accessibilityLabel={label}
104
+ fullWidth
105
+ onPress={() => selectTab(index)}
106
+ style={styles.triggerFill}
107
+ contentStyle={[
108
+ styles.triggerContent,
109
+ styles.triggerUndergo,
110
+ active && styles.triggerUndergoActive,
111
+ ]}
112
+ />
113
+ ) : (
114
+ <Pressable
115
+ accessibilityRole="tab"
116
+ accessibilityState={{ selected: active }}
117
+ accessibilityLabel={label}
118
+ hitSlop={tabHitSlop}
119
+ onPress={() => selectTab(index)}
120
+ style={styles.containerPress}>
121
+ <Text
122
+ style={[
123
+ styles.containerLabel,
124
+ {
125
+ lineHeight,
126
+ color: active ? colors.primary.base : colors.tabs.inactiveLabel,
127
+ fontWeight: active ? typography.fontWeight.large : typography.fontWeight.medium,
128
+ },
129
+ ]}>
130
+ {label}
131
+ </Text>
132
+ </Pressable>
133
+ )}
134
+ {!isUndergo ? (
135
+ <View style={[styles.containerIndicator, active && styles.containerIndicatorActive]} />
136
+ ) : null}
137
+ </View>
138
+ );
139
+ })}
140
+ </ScrollView>
141
+ </View>
142
+ </View>
143
+ );
144
+ }
145
+
146
+ const styles = StyleSheet.create({
147
+ host: {
148
+ maxWidth: '100%',
149
+ alignSelf: 'center',
150
+ },
151
+ hostContainer: {
152
+ alignSelf: 'center',
153
+ maxWidth: '100%',
154
+ },
155
+ hostUndergo: {
156
+ alignSelf: 'center',
157
+ maxWidth: '100%',
158
+ },
159
+ containerRule: {
160
+ borderBottomWidth: borders.width.medium,
161
+ borderBottomColor: colors.tabs.rule,
162
+ overflow: 'visible',
163
+ },
164
+ undergoFrame: {
165
+ padding: spacing.xs,
166
+ borderRadius: radius.md,
167
+ borderWidth: borders.width.thin,
168
+ borderColor: colors.tabs.undergoFrameBorder,
169
+ backgroundColor: colors.tabs.undergoFrameBackground,
170
+ },
171
+ scroller: {
172
+ flexGrow: 0,
173
+ overflow: 'visible',
174
+ },
175
+ list: {
176
+ flexDirection: 'row',
177
+ flexWrap: 'nowrap',
178
+ alignItems: 'stretch',
179
+ },
180
+ listContainer: {
181
+ gap: spacing.md,
182
+ alignItems: 'flex-end',
183
+ },
184
+ listUndergo: {
185
+ gap: spacing.xs,
186
+ minHeight: buttonTokens.minTouch,
187
+ },
188
+ item: {
189
+ flexShrink: 0,
190
+ },
191
+ itemContainer: {
192
+ minHeight: buttonTokens.minTouch,
193
+ justifyContent: 'flex-end',
194
+ overflow: 'visible',
195
+ },
196
+ containerPress: {
197
+ minHeight: buttonTokens.minTouch,
198
+ paddingHorizontal: spacing.xs,
199
+ justifyContent: 'center',
200
+ },
201
+ containerLabel: {
202
+ fontFamily: typography.fontFamily.base,
203
+ fontSize: typography.fontSize.sm,
204
+ },
205
+ containerIndicator: {
206
+ height: borders.width.medium,
207
+ marginBottom: -borders.width.medium,
208
+ backgroundColor: colors.transparent,
209
+ },
210
+ containerIndicatorActive: {
211
+ height: borders.width.medium,
212
+ backgroundColor: colors.primary.base,
213
+ },
214
+ itemUndergo: {
215
+ minHeight: buttonTokens.minTouch,
216
+ justifyContent: 'center',
217
+ borderRadius: radius.md,
218
+ },
219
+ itemUndergoActive: {
220
+ backgroundColor: colors.tabs.undergoActiveSurface,
221
+ borderRadius: radius.md,
222
+ },
223
+ triggerFill: {
224
+ width: '100%',
225
+ alignSelf: 'stretch',
226
+ },
227
+ triggerContent: {
228
+ minHeight: buttonTokens.minTouch,
229
+ justifyContent: 'center',
230
+ },
231
+ triggerUndergo: {
232
+ paddingHorizontal: spacing.md,
233
+ backgroundColor: colors.transparent,
234
+ borderRadius: radius.md,
235
+ },
236
+ triggerUndergoActive: {
237
+ backgroundColor: colors.tabs.undergoActiveSurface,
238
+ },
239
+ });
@@ -0,0 +1,2 @@
1
+ export { Tabs, TabsVariant } from './Tabs';
2
+ export type { TabsProps } from './Tabs';