@titan-design/react-ui 0.0.2 → 0.1.1

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 (206) hide show
  1. package/dist/{chunk-NA3EES23.mjs → chunk-MGW37MPO.mjs} +26 -4
  2. package/dist/chunk-MGW37MPO.mjs.map +1 -0
  3. package/dist/index.d.mts +115 -2
  4. package/dist/index.d.ts +115 -2
  5. package/dist/index.js +262 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +225 -3
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/theme/index.d.mts +124 -109
  10. package/dist/theme/index.d.ts +124 -109
  11. package/dist/theme/index.js +24 -1
  12. package/dist/theme/index.js.map +1 -1
  13. package/dist/theme/index.mjs +1 -1
  14. package/package.json +12 -5
  15. package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
  16. package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
  17. package/src/components/custom/DateTime/DateTime.tsx +235 -0
  18. package/src/components/custom/DateTime/index.ts +2 -0
  19. package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
  20. package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
  21. package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
  22. package/src/components/custom/EmptyState/index.ts +2 -0
  23. package/src/components/custom/Metric/Metric.stories.tsx +133 -0
  24. package/src/components/custom/Metric/Metric.test.tsx +120 -0
  25. package/src/components/custom/Metric/Metric.tsx +92 -0
  26. package/src/components/custom/Metric/index.ts +2 -0
  27. package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
  28. package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
  29. package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
  30. package/src/components/custom/Sidebar/index.ts +18 -0
  31. package/src/components/custom/Table/Table.stories.tsx +481 -0
  32. package/src/components/custom/Table/Table.test.tsx +531 -0
  33. package/src/components/custom/Table/Table.tsx +696 -0
  34. package/src/components/custom/Table/index.ts +22 -0
  35. package/src/components/custom/Typography/Typography.stories.tsx +153 -0
  36. package/src/components/custom/Typography/Typography.test.tsx +107 -0
  37. package/src/components/custom/Typography/Typography.tsx +218 -0
  38. package/src/components/custom/Typography/index.ts +19 -0
  39. package/src/components/custom/index.ts +8 -0
  40. package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
  41. package/src/components/custom/stepper/Stepper.test.tsx +230 -0
  42. package/src/components/custom/stepper/Stepper.tsx +250 -0
  43. package/src/components/custom/stepper/index.ts +10 -0
  44. package/src/components/index.ts +5 -0
  45. package/src/components/ui/alert/Alert.stories.tsx +154 -0
  46. package/src/components/ui/alert/Alert.test.tsx +202 -0
  47. package/src/components/ui/alert/Alert.tsx +170 -0
  48. package/src/components/ui/alert/index.ts +8 -0
  49. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
  50. package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
  51. package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
  52. package/src/components/ui/autocomplete/index.ts +2 -0
  53. package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
  54. package/src/components/ui/avatar/Avatar.test.tsx +135 -0
  55. package/src/components/ui/avatar/Avatar.tsx +168 -0
  56. package/src/components/ui/avatar/index.ts +2 -0
  57. package/src/components/ui/badge/Badge.stories.tsx +100 -0
  58. package/src/components/ui/badge/Badge.test.tsx +86 -0
  59. package/src/components/ui/badge/Badge.tsx +105 -0
  60. package/src/components/ui/badge/index.ts +2 -0
  61. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
  62. package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
  63. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
  64. package/src/components/ui/breadcrumbs/index.ts +2 -0
  65. package/src/components/ui/button/Button.stories.tsx +216 -0
  66. package/src/components/ui/button/Button.test.tsx +132 -0
  67. package/src/components/ui/button/Button.tsx +242 -0
  68. package/src/components/ui/button/index.ts +10 -0
  69. package/src/components/ui/card/Card.stories.tsx +443 -0
  70. package/src/components/ui/card/Card.test.tsx +265 -0
  71. package/src/components/ui/card/Card.tsx +380 -0
  72. package/src/components/ui/card/index.ts +20 -0
  73. package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
  74. package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
  75. package/src/components/ui/checkbox/Checkbox.tsx +163 -0
  76. package/src/components/ui/checkbox/index.ts +2 -0
  77. package/src/components/ui/chip/Chip.stories.tsx +112 -0
  78. package/src/components/ui/chip/Chip.test.tsx +119 -0
  79. package/src/components/ui/chip/Chip.tsx +151 -0
  80. package/src/components/ui/chip/index.ts +2 -0
  81. package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
  82. package/src/components/ui/collapse/Collapse.test.tsx +298 -0
  83. package/src/components/ui/collapse/Collapse.tsx +275 -0
  84. package/src/components/ui/collapse/index.ts +18 -0
  85. package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
  86. package/src/components/ui/data-row/DataRow.test.tsx +78 -0
  87. package/src/components/ui/data-row/DataRow.tsx +29 -0
  88. package/src/components/ui/data-row/index.ts +2 -0
  89. package/src/components/ui/divider/Divider.stories.tsx +71 -0
  90. package/src/components/ui/divider/Divider.test.tsx +55 -0
  91. package/src/components/ui/divider/Divider.tsx +37 -0
  92. package/src/components/ui/divider/index.ts +2 -0
  93. package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
  94. package/src/components/ui/drawer/Drawer.test.tsx +142 -0
  95. package/src/components/ui/drawer/Drawer.tsx +228 -0
  96. package/src/components/ui/drawer/index.ts +9 -0
  97. package/src/components/ui/form-field/FormField.stories.tsx +260 -0
  98. package/src/components/ui/form-field/FormField.test.tsx +280 -0
  99. package/src/components/ui/form-field/FormField.tsx +308 -0
  100. package/src/components/ui/form-field/index.ts +2 -0
  101. package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
  102. package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
  103. package/src/components/ui/help-tip/HelpTip.tsx +229 -0
  104. package/src/components/ui/help-tip/index.ts +2 -0
  105. package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
  106. package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
  107. package/src/components/ui/icon-box/IconBox.tsx +57 -0
  108. package/src/components/ui/icon-box/index.ts +1 -0
  109. package/src/components/ui/index.ts +36 -0
  110. package/src/components/ui/input/Input.stories.tsx +192 -0
  111. package/src/components/ui/input/Input.test.tsx +86 -0
  112. package/src/components/ui/input/Input.tsx +273 -0
  113. package/src/components/ui/input/index.ts +2 -0
  114. package/src/components/ui/link/Link.stories.tsx +78 -0
  115. package/src/components/ui/link/Link.test.tsx +94 -0
  116. package/src/components/ui/link/Link.tsx +86 -0
  117. package/src/components/ui/link/index.ts +2 -0
  118. package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
  119. package/src/components/ui/list-item/ListItem.test.tsx +128 -0
  120. package/src/components/ui/list-item/ListItem.tsx +89 -0
  121. package/src/components/ui/list-item/index.ts +14 -0
  122. package/src/components/ui/menu/Menu.stories.tsx +180 -0
  123. package/src/components/ui/menu/Menu.test.tsx +353 -0
  124. package/src/components/ui/menu/Menu.tsx +222 -0
  125. package/src/components/ui/menu/index.ts +2 -0
  126. package/src/components/ui/modal/Modal.stories.tsx +350 -0
  127. package/src/components/ui/modal/Modal.test.tsx +159 -0
  128. package/src/components/ui/modal/Modal.tsx +254 -0
  129. package/src/components/ui/modal/index.ts +19 -0
  130. package/src/components/ui/popover/Popover.stories.tsx +190 -0
  131. package/src/components/ui/popover/Popover.test.tsx +277 -0
  132. package/src/components/ui/popover/Popover.tsx +162 -0
  133. package/src/components/ui/popover/index.ts +2 -0
  134. package/src/components/ui/progress/Progress.stories.tsx +190 -0
  135. package/src/components/ui/progress/Progress.test.tsx +181 -0
  136. package/src/components/ui/progress/Progress.tsx +320 -0
  137. package/src/components/ui/progress/index.ts +9 -0
  138. package/src/components/ui/radio/Radio.stories.tsx +199 -0
  139. package/src/components/ui/radio/Radio.test.tsx +166 -0
  140. package/src/components/ui/radio/Radio.tsx +200 -0
  141. package/src/components/ui/radio/index.ts +2 -0
  142. package/src/components/ui/section/Section.stories.tsx +111 -0
  143. package/src/components/ui/section/Section.test.tsx +121 -0
  144. package/src/components/ui/section/Section.tsx +52 -0
  145. package/src/components/ui/section/index.ts +6 -0
  146. package/src/components/ui/select/Select.stories.tsx +286 -0
  147. package/src/components/ui/select/Select.test.tsx +180 -0
  148. package/src/components/ui/select/Select.tsx +236 -0
  149. package/src/components/ui/select/index.ts +2 -0
  150. package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
  151. package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
  152. package/src/components/ui/skeleton/Skeleton.tsx +229 -0
  153. package/src/components/ui/skeleton/index.ts +16 -0
  154. package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
  155. package/src/components/ui/spinner/Spinner.test.tsx +67 -0
  156. package/src/components/ui/spinner/Spinner.tsx +72 -0
  157. package/src/components/ui/spinner/index.ts +2 -0
  158. package/src/components/ui/stack/Stack.stories.tsx +127 -0
  159. package/src/components/ui/stack/Stack.test.tsx +184 -0
  160. package/src/components/ui/stack/Stack.tsx +76 -0
  161. package/src/components/ui/stack/index.ts +2 -0
  162. package/src/components/ui/surface/Surface.stories.tsx +91 -0
  163. package/src/components/ui/surface/Surface.test.tsx +63 -0
  164. package/src/components/ui/surface/Surface.tsx +51 -0
  165. package/src/components/ui/surface/index.ts +1 -0
  166. package/src/components/ui/switch/Switch.stories.tsx +111 -0
  167. package/src/components/ui/switch/Switch.test.tsx +112 -0
  168. package/src/components/ui/switch/Switch.tsx +103 -0
  169. package/src/components/ui/switch/index.ts +2 -0
  170. package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
  171. package/src/components/ui/tabs/Tabs.test.tsx +192 -0
  172. package/src/components/ui/tabs/Tabs.tsx +253 -0
  173. package/src/components/ui/tabs/index.ts +16 -0
  174. package/src/components/ui/toast/Toast.stories.tsx +255 -0
  175. package/src/components/ui/toast/Toast.test.tsx +207 -0
  176. package/src/components/ui/toast/Toast.tsx +327 -0
  177. package/src/components/ui/toast/index.ts +8 -0
  178. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
  179. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
  180. package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
  181. package/src/components/ui/toolbar-button/index.ts +7 -0
  182. package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
  183. package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
  184. package/src/components/ui/tooltip/Tooltip.tsx +143 -0
  185. package/src/components/ui/tooltip/index.ts +2 -0
  186. package/src/index.ts +10 -0
  187. package/src/test/setup.ts +19 -0
  188. package/src/theme/Colors.stories.tsx +258 -0
  189. package/src/theme/config.ts +193 -0
  190. package/src/theme/elevation.stories.tsx +346 -0
  191. package/src/theme/elevation.test.ts +27 -0
  192. package/src/theme/elevation.ts +567 -0
  193. package/src/theme/index.ts +39 -0
  194. package/src/theme/shadows.stories.tsx +523 -0
  195. package/src/theme/shadows.ts +439 -0
  196. package/src/theme/tokens/index.ts +2 -0
  197. package/src/theme/tokens/primitives.ts +354 -0
  198. package/src/theme/tokens/semantic.ts +381 -0
  199. package/src/types/jest-axe.d.ts +16 -0
  200. package/src/types/nativewind.d.ts +54 -0
  201. package/src/utils/cn.ts +14 -0
  202. package/src/utils/colors.ts +140 -0
  203. package/src/utils/form.ts +188 -0
  204. package/src/utils/index.ts +29 -0
  205. package/src/utils/useTheme.ts +72 -0
  206. package/dist/chunk-NA3EES23.mjs.map +0 -1
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { Avatar, AvatarBadge, AvatarGroup } from './Avatar'
5
+
6
+ describe('Avatar', () => {
7
+ it('renders correctly with default props', () => {
8
+ render(<Avatar />)
9
+ expect(screen.getByRole('img')).toBeInTheDocument()
10
+ })
11
+
12
+ it('renders fallback text', () => {
13
+ render(<Avatar fallback="JD" />)
14
+ expect(screen.getByText('JD')).toBeInTheDocument()
15
+ })
16
+
17
+ it('uses fallback as accessibility label when no alt provided', () => {
18
+ render(<Avatar fallback="JD" />)
19
+ expect(screen.getByRole('img')).toHaveAttribute('aria-label', 'JD')
20
+ })
21
+
22
+ it('uses alt as accessibility label', () => {
23
+ render(<Avatar alt="John Doe" fallback="JD" />)
24
+ expect(screen.getByRole('img')).toHaveAttribute('aria-label', 'John Doe')
25
+ })
26
+
27
+ it('uses default accessibility label when no alt or fallback', () => {
28
+ render(<Avatar />)
29
+ expect(screen.getByRole('img')).toHaveAttribute('aria-label', 'Avatar')
30
+ })
31
+
32
+ it('renders with image source', () => {
33
+ render(<Avatar source={{ uri: 'https://example.com/avatar.jpg' }} alt="User" />)
34
+ const imgs = screen.getAllByRole('img')
35
+ expect(imgs.length).toBeGreaterThan(0)
36
+ })
37
+
38
+ it('renders with all size options', () => {
39
+ const sizes = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'] as const
40
+ sizes.forEach((size) => {
41
+ const { unmount } = render(<Avatar size={size} fallback="AB" />)
42
+ expect(screen.getByRole('img')).toBeInTheDocument()
43
+ unmount()
44
+ })
45
+ })
46
+
47
+ it('renders empty placeholder when no source or fallback', () => {
48
+ const { container } = render(<Avatar />)
49
+ expect(container.firstChild).toBeInTheDocument()
50
+ })
51
+
52
+ it('applies custom className', () => {
53
+ render(<Avatar className="extra" />)
54
+ expect(screen.getByRole('img')).toBeInTheDocument()
55
+ })
56
+
57
+ describe('AvatarBadge', () => {
58
+ it('renders with default success color', () => {
59
+ const { container } = render(<AvatarBadge />)
60
+ expect(container.firstChild).toBeInTheDocument()
61
+ })
62
+
63
+ it('renders with all color options', () => {
64
+ const colors = ['success', 'error', 'warning', 'default'] as const
65
+ colors.forEach((color) => {
66
+ const { unmount } = render(<AvatarBadge color={color} />)
67
+ unmount()
68
+ })
69
+ })
70
+
71
+ it('accepts custom className', () => {
72
+ const { container } = render(<AvatarBadge className="extra" />)
73
+ expect(container.firstChild).toBeInTheDocument()
74
+ })
75
+ })
76
+
77
+ describe('AvatarGroup', () => {
78
+ it('renders children avatars', () => {
79
+ render(
80
+ <AvatarGroup>
81
+ <Avatar fallback="A" />
82
+ <Avatar fallback="B" />
83
+ </AvatarGroup>
84
+ )
85
+ expect(screen.getByText('A')).toBeInTheDocument()
86
+ expect(screen.getByText('B')).toBeInTheDocument()
87
+ })
88
+
89
+ it('limits visible avatars with max prop', () => {
90
+ render(
91
+ <AvatarGroup max={2}>
92
+ <Avatar fallback="A" />
93
+ <Avatar fallback="B" />
94
+ <Avatar fallback="C" />
95
+ <Avatar fallback="D" />
96
+ </AvatarGroup>
97
+ )
98
+ expect(screen.getByText('A')).toBeInTheDocument()
99
+ expect(screen.getByText('B')).toBeInTheDocument()
100
+ expect(screen.getByText('+2')).toBeInTheDocument()
101
+ })
102
+
103
+ it('shows no +N indicator when all avatars fit', () => {
104
+ render(
105
+ <AvatarGroup max={5}>
106
+ <Avatar fallback="A" />
107
+ <Avatar fallback="B" />
108
+ </AvatarGroup>
109
+ )
110
+ expect(screen.queryByText(/\+/)).not.toBeInTheDocument()
111
+ })
112
+
113
+ it('applies custom className', () => {
114
+ const { container } = render(
115
+ <AvatarGroup className="extra">
116
+ <Avatar fallback="A" />
117
+ </AvatarGroup>
118
+ )
119
+ expect(container.firstChild).toBeInTheDocument()
120
+ })
121
+ })
122
+
123
+ describe('accessibility', () => {
124
+ it('has no accessibility violations', async () => {
125
+ const { container } = render(<Avatar fallback="JD" alt="John Doe" />)
126
+ const results = await axe(container)
127
+ expect(results).toHaveNoViolations()
128
+ })
129
+
130
+ it('has correct image role', () => {
131
+ render(<Avatar fallback="JD" />)
132
+ expect(screen.getByRole('img')).toBeInTheDocument()
133
+ })
134
+ })
135
+ })
@@ -0,0 +1,168 @@
1
+ import React from 'react'
2
+ import { View, Text, Image, type ViewProps, type ImageSourcePropType } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
6
+
7
+ export interface AvatarProps extends ViewProps {
8
+ /** Avatar size */
9
+ size?: AvatarSize
10
+ /** Image source */
11
+ source?: ImageSourcePropType
12
+ /** Fallback text (usually initials) */
13
+ fallback?: string
14
+ /** Alt text for accessibility */
15
+ alt?: string
16
+ /** Additional className */
17
+ className?: string
18
+ }
19
+
20
+ const sizeStyles: Record<AvatarSize, { container: string; text: string; image: string }> = {
21
+ xs: { container: 'w-6 h-6', text: 'text-xs', image: 'w-6 h-6' },
22
+ sm: { container: 'w-8 h-8', text: 'text-xs', image: 'w-8 h-8' },
23
+ md: { container: 'w-10 h-10', text: 'text-sm', image: 'w-10 h-10' },
24
+ lg: { container: 'w-12 h-12', text: 'text-base', image: 'w-12 h-12' },
25
+ xl: { container: 'w-16 h-16', text: 'text-lg', image: 'w-16 h-16' },
26
+ '2xl': { container: 'w-20 h-20', text: 'text-xl', image: 'w-20 h-20' },
27
+ }
28
+
29
+ /**
30
+ * Avatar component for user/entity representation.
31
+ *
32
+ * @example
33
+ * // With image
34
+ * <Avatar source={{ uri: 'https://example.com/avatar.jpg' }} alt="User" />
35
+ *
36
+ * // With fallback initials
37
+ * <Avatar fallback="JD" />
38
+ *
39
+ * // With badge
40
+ * <AvatarGroup>
41
+ * <Avatar source={source1} />
42
+ * <Avatar source={source2} />
43
+ * <AvatarBadge />
44
+ * </AvatarGroup>
45
+ */
46
+ export function Avatar({
47
+ size = 'md',
48
+ source,
49
+ fallback,
50
+ alt,
51
+ className,
52
+ ...props
53
+ }: AvatarProps) {
54
+ const styles = sizeStyles[size]
55
+ const hasImage = source !== undefined
56
+
57
+ return (
58
+ <View
59
+ accessibilityRole="image"
60
+ accessibilityLabel={alt || fallback || 'Avatar'}
61
+ className={cn(
62
+ 'items-center justify-center rounded-full overflow-hidden',
63
+ 'bg-border-strong',
64
+ styles.container,
65
+ className
66
+ )}
67
+ {...props}
68
+ >
69
+ {hasImage ? (
70
+ <Image
71
+ source={source}
72
+ className={cn('rounded-full', styles.image)}
73
+ accessibilityLabel={alt}
74
+ />
75
+ ) : fallback ? (
76
+ <Text className={cn('font-semibold text-text-primary', styles.text)}>
77
+ {fallback}
78
+ </Text>
79
+ ) : (
80
+ <View className="w-full h-full bg-surface-raised" />
81
+ )}
82
+ </View>
83
+ )
84
+ }
85
+
86
+ export interface AvatarBadgeProps {
87
+ /** Badge color */
88
+ color?: 'success' | 'error' | 'warning' | 'default'
89
+ /** Additional className */
90
+ className?: string
91
+ }
92
+
93
+ const badgeColors: Record<string, string> = {
94
+ success: 'bg-status-success',
95
+ error: 'bg-status-error',
96
+ warning: 'bg-status-warning',
97
+ default: 'bg-border-strong',
98
+ }
99
+
100
+ /**
101
+ * Status badge for Avatar (e.g., online indicator).
102
+ */
103
+ export function AvatarBadge({ color = 'success', className }: AvatarBadgeProps) {
104
+ return (
105
+ <View
106
+ className={cn(
107
+ 'absolute bottom-0 right-0 w-3 h-3 rounded-full border-2 border-surface-base',
108
+ badgeColors[color],
109
+ className
110
+ )}
111
+ />
112
+ )
113
+ }
114
+
115
+ export interface AvatarGroupProps {
116
+ children: React.ReactNode
117
+ /** Maximum avatars to show before +N indicator */
118
+ max?: number
119
+ /** Size of avatars */
120
+ size?: AvatarSize
121
+ /** Additional className */
122
+ className?: string
123
+ }
124
+
125
+ /**
126
+ * Group of avatars with stacking effect.
127
+ */
128
+ export function AvatarGroup({
129
+ children,
130
+ max,
131
+ size = 'md',
132
+ className,
133
+ }: AvatarGroupProps) {
134
+ const childArray = React.Children.toArray(children)
135
+ const visibleChildren = max ? childArray.slice(0, max) : childArray
136
+ const remainingCount = max ? Math.max(0, childArray.length - max) : 0
137
+
138
+ return (
139
+ <View className={cn('flex-row', className)}>
140
+ {visibleChildren.map((child, index) => (
141
+ <View
142
+ key={index}
143
+ className={cn(
144
+ 'border-2 border-surface-base rounded-full',
145
+ index > 0 && '-ml-2'
146
+ )}
147
+ >
148
+ {React.isValidElement(child)
149
+ ? React.cloneElement(child as React.ReactElement<any>, { size })
150
+ : child}
151
+ </View>
152
+ ))}
153
+ {remainingCount > 0 && (
154
+ <View
155
+ className={cn(
156
+ 'border-2 border-surface-base rounded-full -ml-2',
157
+ 'items-center justify-center bg-surface-raised',
158
+ sizeStyles[size].container
159
+ )}
160
+ >
161
+ <Text className={cn('font-semibold text-text-secondary', sizeStyles[size].text)}>
162
+ +{remainingCount}
163
+ </Text>
164
+ </View>
165
+ )}
166
+ </View>
167
+ )
168
+ }
@@ -0,0 +1,2 @@
1
+ export { Avatar, AvatarBadge, AvatarGroup } from './Avatar'
2
+ export type { AvatarProps, AvatarBadgeProps, AvatarGroupProps, AvatarSize } from './Avatar'
@@ -0,0 +1,100 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { Badge, BadgeText } from './Badge'
4
+
5
+ const meta: Meta<typeof Badge> = {
6
+ title: 'Components/Badge',
7
+ component: Badge,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ variant: {
11
+ control: 'select',
12
+ options: ['solid', 'subtle', 'outline'],
13
+ description: 'Visual variant',
14
+ },
15
+ color: {
16
+ control: 'select',
17
+ options: ['default', 'primary', 'secondary', 'success', 'error', 'warning', 'info'],
18
+ description: 'Color scheme',
19
+ },
20
+ size: {
21
+ control: 'select',
22
+ options: ['sm', 'md', 'lg'],
23
+ description: 'Badge size',
24
+ },
25
+ },
26
+ }
27
+
28
+ export default meta
29
+ type Story = StoryObj<typeof Badge>
30
+
31
+ export const Default: Story = {
32
+ args: {
33
+ variant: 'subtle',
34
+ color: 'default',
35
+ size: 'md',
36
+ children: 'Badge',
37
+ },
38
+ }
39
+
40
+ export const AllVariants: Story = {
41
+ render: () => (
42
+ <View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap' }}>
43
+ <Badge variant="solid" color="primary">Solid</Badge>
44
+ <Badge variant="subtle" color="primary">Subtle</Badge>
45
+ <Badge variant="outline" color="primary">Outline</Badge>
46
+ </View>
47
+ ),
48
+ }
49
+
50
+ export const AllColors: Story = {
51
+ render: () => (
52
+ <View style={{ gap: 12 }}>
53
+ <View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap' }}>
54
+ <Badge variant="solid" color="default">Default</Badge>
55
+ <Badge variant="solid" color="primary">Primary</Badge>
56
+ <Badge variant="solid" color="secondary">Secondary</Badge>
57
+ <Badge variant="solid" color="success">Success</Badge>
58
+ <Badge variant="solid" color="error">Error</Badge>
59
+ <Badge variant="solid" color="warning">Warning</Badge>
60
+ <Badge variant="solid" color="info">Info</Badge>
61
+ </View>
62
+ <View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap' }}>
63
+ <Badge variant="subtle" color="default">Default</Badge>
64
+ <Badge variant="subtle" color="primary">Primary</Badge>
65
+ <Badge variant="subtle" color="secondary">Secondary</Badge>
66
+ <Badge variant="subtle" color="success">Success</Badge>
67
+ <Badge variant="subtle" color="error">Error</Badge>
68
+ <Badge variant="subtle" color="warning">Warning</Badge>
69
+ <Badge variant="subtle" color="info">Info</Badge>
70
+ </View>
71
+ <View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap' }}>
72
+ <Badge variant="outline" color="default">Default</Badge>
73
+ <Badge variant="outline" color="primary">Primary</Badge>
74
+ <Badge variant="outline" color="secondary">Secondary</Badge>
75
+ <Badge variant="outline" color="success">Success</Badge>
76
+ <Badge variant="outline" color="error">Error</Badge>
77
+ <Badge variant="outline" color="warning">Warning</Badge>
78
+ <Badge variant="outline" color="info">Info</Badge>
79
+ </View>
80
+ </View>
81
+ ),
82
+ }
83
+
84
+ export const AllSizes: Story = {
85
+ render: () => (
86
+ <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
87
+ <Badge size="sm" color="primary">Small</Badge>
88
+ <Badge size="md" color="primary">Medium</Badge>
89
+ <Badge size="lg" color="primary">Large</Badge>
90
+ </View>
91
+ ),
92
+ }
93
+
94
+ export const WithBadgeText: Story = {
95
+ render: () => (
96
+ <Badge variant="solid" color="success">
97
+ <BadgeText>Active</BadgeText>
98
+ </Badge>
99
+ ),
100
+ }
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { Badge, BadgeText } from './Badge'
5
+
6
+ describe('Badge', () => {
7
+ it('renders string children correctly', () => {
8
+ render(<Badge>Active</Badge>)
9
+ expect(screen.getByText('Active')).toBeInTheDocument()
10
+ })
11
+
12
+ it('renders BadgeText compound component', () => {
13
+ render(
14
+ <Badge>
15
+ <BadgeText>Status</BadgeText>
16
+ </Badge>
17
+ )
18
+ expect(screen.getByText('Status')).toBeInTheDocument()
19
+ })
20
+
21
+ it('applies default variant (subtle) and color (default)', () => {
22
+ const { container } = render(<Badge>Default</Badge>)
23
+ expect(container.firstChild).toBeInTheDocument()
24
+ })
25
+
26
+ it('renders with all variant options', () => {
27
+ const variants = ['solid', 'subtle', 'outline'] as const
28
+ variants.forEach((variant) => {
29
+ const { unmount } = render(<Badge variant={variant}>Test</Badge>)
30
+ expect(screen.getByText('Test')).toBeInTheDocument()
31
+ unmount()
32
+ })
33
+ })
34
+
35
+ it('renders with all color options', () => {
36
+ const colors = ['default', 'primary', 'secondary', 'success', 'error', 'warning', 'info'] as const
37
+ colors.forEach((color) => {
38
+ const { unmount } = render(<Badge color={color}>Test</Badge>)
39
+ expect(screen.getByText('Test')).toBeInTheDocument()
40
+ unmount()
41
+ })
42
+ })
43
+
44
+ it('renders with all size options', () => {
45
+ const sizes = ['sm', 'md', 'lg'] as const
46
+ sizes.forEach((size) => {
47
+ const { unmount } = render(<Badge size={size}>Test</Badge>)
48
+ expect(screen.getByText('Test')).toBeInTheDocument()
49
+ unmount()
50
+ })
51
+ })
52
+
53
+ it('applies custom className', () => {
54
+ const { container } = render(<Badge className="custom-class">Test</Badge>)
55
+ expect(container.firstChild).toBeInTheDocument()
56
+ })
57
+
58
+ it('passes additional props through', () => {
59
+ render(<Badge testID="badge-test">Test</Badge>)
60
+ expect(screen.getByText('Test')).toBeInTheDocument()
61
+ })
62
+
63
+ describe('BadgeText', () => {
64
+ it('renders children', () => {
65
+ render(<BadgeText>Label</BadgeText>)
66
+ expect(screen.getByText('Label')).toBeInTheDocument()
67
+ })
68
+
69
+ it('accepts custom className', () => {
70
+ render(<BadgeText className="extra">Label</BadgeText>)
71
+ expect(screen.getByText('Label')).toBeInTheDocument()
72
+ })
73
+ })
74
+
75
+ describe('accessibility', () => {
76
+ it('has no accessibility violations', async () => {
77
+ const { container } = render(
78
+ <Badge color="success">
79
+ <BadgeText>Active</BadgeText>
80
+ </Badge>
81
+ )
82
+ const results = await axe(container)
83
+ expect(results).toHaveNoViolations()
84
+ })
85
+ })
86
+ })
@@ -0,0 +1,105 @@
1
+ import React from 'react'
2
+ import { View, Text, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export type BadgeVariant = 'solid' | 'subtle' | 'outline'
6
+ export type BadgeColor = 'default' | 'primary' | 'secondary' | 'success' | 'error' | 'warning' | 'info'
7
+ export type BadgeSize = 'sm' | 'md' | 'lg'
8
+
9
+ export interface BadgeProps extends ViewProps {
10
+ /** Visual variant */
11
+ variant?: BadgeVariant
12
+ /** Color scheme */
13
+ color?: BadgeColor
14
+ /** Size */
15
+ size?: BadgeSize
16
+ /** Additional className */
17
+ className?: string
18
+ children?: React.ReactNode
19
+ }
20
+
21
+ const variantColorStyles: Record<BadgeVariant, Record<BadgeColor, string>> = {
22
+ solid: {
23
+ default: 'bg-border-strong text-text-inverse',
24
+ primary: 'bg-brand-primary text-text-inverse',
25
+ secondary: 'bg-brand-secondary text-text-inverse',
26
+ success: 'bg-status-success text-text-inverse',
27
+ error: 'bg-status-error text-text-inverse',
28
+ warning: 'bg-status-warning text-text-inverse',
29
+ info: 'bg-status-info text-text-inverse',
30
+ },
31
+ subtle: {
32
+ default: 'bg-surface-raised text-text-secondary',
33
+ primary: 'bg-brand-primary-subtle text-brand-primary',
34
+ secondary: 'bg-brand-secondary-subtle text-brand-secondary',
35
+ success: 'bg-status-success-subtle text-status-success',
36
+ error: 'bg-status-error-subtle text-status-error',
37
+ warning: 'bg-status-warning-subtle text-status-warning',
38
+ info: 'bg-status-info-subtle text-status-info',
39
+ },
40
+ outline: {
41
+ default: 'border border-border-strong text-text-secondary',
42
+ primary: 'border border-brand-primary text-brand-primary',
43
+ secondary: 'border border-brand-secondary text-brand-secondary',
44
+ success: 'border border-status-success text-status-success',
45
+ error: 'border border-status-error text-status-error',
46
+ warning: 'border border-status-warning text-status-warning',
47
+ info: 'border border-status-info text-status-info',
48
+ },
49
+ }
50
+
51
+ const sizeStyles: Record<BadgeSize, string> = {
52
+ sm: 'px-1.5 py-0.5 text-xs',
53
+ md: 'px-2 py-0.5 text-xs',
54
+ lg: 'px-2.5 py-1 text-sm',
55
+ }
56
+
57
+ /**
58
+ * Badge component for status indicators and labels.
59
+ *
60
+ * @example
61
+ * <Badge color="success">Active</Badge>
62
+ * <Badge variant="outline" color="warning">Pending</Badge>
63
+ */
64
+ export function Badge({
65
+ variant = 'subtle',
66
+ color = 'default',
67
+ size = 'md',
68
+ className,
69
+ children,
70
+ ...props
71
+ }: BadgeProps) {
72
+ return (
73
+ <View
74
+ className={cn(
75
+ 'inline-flex items-center justify-center rounded-full font-medium',
76
+ variantColorStyles[variant][color],
77
+ sizeStyles[size],
78
+ className
79
+ )}
80
+ {...props}
81
+ >
82
+ {typeof children === 'string' ? (
83
+ <Text className="text-inherit font-medium">{children}</Text>
84
+ ) : (
85
+ children
86
+ )}
87
+ </View>
88
+ )
89
+ }
90
+
91
+ export interface BadgeTextProps {
92
+ children: React.ReactNode
93
+ className?: string
94
+ }
95
+
96
+ /**
97
+ * Text component for Badge content.
98
+ */
99
+ export function BadgeText({ children, className }: BadgeTextProps) {
100
+ return (
101
+ <Text className={cn('text-inherit font-medium', className)}>
102
+ {children}
103
+ </Text>
104
+ )
105
+ }
@@ -0,0 +1,2 @@
1
+ export { Badge, BadgeText } from './Badge'
2
+ export type { BadgeProps, BadgeTextProps, BadgeVariant, BadgeColor, BadgeSize } from './Badge'
@@ -0,0 +1,70 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { Text } from 'react-native'
3
+ import { Breadcrumbs, BreadcrumbItem } from './Breadcrumbs'
4
+
5
+ const meta: Meta<typeof Breadcrumbs> = {
6
+ title: 'Components/Breadcrumbs',
7
+ component: Breadcrumbs,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ maxItems: {
11
+ control: 'number',
12
+ description: 'Maximum items to show (with collapse)',
13
+ },
14
+ },
15
+ }
16
+
17
+ export default meta
18
+ type Story = StoryObj<typeof Breadcrumbs>
19
+
20
+ export const Default: Story = {
21
+ render: () => (
22
+ <Breadcrumbs>
23
+ <BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
24
+ <BreadcrumbItem onPress={() => {}}>Products</BreadcrumbItem>
25
+ <BreadcrumbItem isCurrentPage>Widget</BreadcrumbItem>
26
+ </Breadcrumbs>
27
+ ),
28
+ }
29
+
30
+ export const CustomSeparator: Story = {
31
+ render: () => (
32
+ <Breadcrumbs separator=">">
33
+ <BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
34
+ <BreadcrumbItem onPress={() => {}}>Category</BreadcrumbItem>
35
+ <BreadcrumbItem isCurrentPage>Item</BreadcrumbItem>
36
+ </Breadcrumbs>
37
+ ),
38
+ }
39
+
40
+ export const WithMaxItems: Story = {
41
+ render: () => (
42
+ <Breadcrumbs maxItems={3}>
43
+ <BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
44
+ <BreadcrumbItem onPress={() => {}}>Category</BreadcrumbItem>
45
+ <BreadcrumbItem onPress={() => {}}>Subcategory</BreadcrumbItem>
46
+ <BreadcrumbItem onPress={() => {}}>Section</BreadcrumbItem>
47
+ <BreadcrumbItem isCurrentPage>Current Page</BreadcrumbItem>
48
+ </Breadcrumbs>
49
+ ),
50
+ }
51
+
52
+ export const SingleItem: Story = {
53
+ render: () => (
54
+ <Breadcrumbs>
55
+ <BreadcrumbItem isCurrentPage>Home</BreadcrumbItem>
56
+ </Breadcrumbs>
57
+ ),
58
+ }
59
+
60
+ export const LongPath: Story = {
61
+ render: () => (
62
+ <Breadcrumbs>
63
+ <BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
64
+ <BreadcrumbItem onPress={() => {}}>Settings</BreadcrumbItem>
65
+ <BreadcrumbItem onPress={() => {}}>Account</BreadcrumbItem>
66
+ <BreadcrumbItem onPress={() => {}}>Security</BreadcrumbItem>
67
+ <BreadcrumbItem isCurrentPage>Two-Factor Authentication</BreadcrumbItem>
68
+ </Breadcrumbs>
69
+ ),
70
+ }