@titan-design/react-ui 0.1.0 → 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.
- package/package.json +10 -5
- package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
- package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
- package/src/components/custom/DateTime/DateTime.tsx +235 -0
- package/src/components/custom/DateTime/index.ts +2 -0
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
- package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
- package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
- package/src/components/custom/EmptyState/index.ts +2 -0
- package/src/components/custom/Metric/Metric.stories.tsx +133 -0
- package/src/components/custom/Metric/Metric.test.tsx +120 -0
- package/src/components/custom/Metric/Metric.tsx +92 -0
- package/src/components/custom/Metric/index.ts +2 -0
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
- package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
- package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
- package/src/components/custom/Sidebar/index.ts +18 -0
- package/src/components/custom/Table/Table.stories.tsx +481 -0
- package/src/components/custom/Table/Table.test.tsx +531 -0
- package/src/components/custom/Table/Table.tsx +696 -0
- package/src/components/custom/Table/index.ts +22 -0
- package/src/components/custom/Typography/Typography.stories.tsx +153 -0
- package/src/components/custom/Typography/Typography.test.tsx +107 -0
- package/src/components/custom/Typography/Typography.tsx +218 -0
- package/src/components/custom/Typography/index.ts +19 -0
- package/src/components/custom/index.ts +8 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
- package/src/components/custom/stepper/Stepper.test.tsx +230 -0
- package/src/components/custom/stepper/Stepper.tsx +250 -0
- package/src/components/custom/stepper/index.ts +10 -0
- package/src/components/index.ts +5 -0
- package/src/components/ui/alert/Alert.stories.tsx +154 -0
- package/src/components/ui/alert/Alert.test.tsx +202 -0
- package/src/components/ui/alert/Alert.tsx +170 -0
- package/src/components/ui/alert/index.ts +8 -0
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
- package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
- package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
- package/src/components/ui/autocomplete/index.ts +2 -0
- package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
- package/src/components/ui/avatar/Avatar.test.tsx +135 -0
- package/src/components/ui/avatar/Avatar.tsx +168 -0
- package/src/components/ui/avatar/index.ts +2 -0
- package/src/components/ui/badge/Badge.stories.tsx +100 -0
- package/src/components/ui/badge/Badge.test.tsx +86 -0
- package/src/components/ui/badge/Badge.tsx +105 -0
- package/src/components/ui/badge/index.ts +2 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
- package/src/components/ui/breadcrumbs/index.ts +2 -0
- package/src/components/ui/button/Button.stories.tsx +216 -0
- package/src/components/ui/button/Button.test.tsx +132 -0
- package/src/components/ui/button/Button.tsx +242 -0
- package/src/components/ui/button/index.ts +10 -0
- package/src/components/ui/card/Card.stories.tsx +443 -0
- package/src/components/ui/card/Card.test.tsx +265 -0
- package/src/components/ui/card/Card.tsx +380 -0
- package/src/components/ui/card/index.ts +20 -0
- package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
- package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
- package/src/components/ui/checkbox/Checkbox.tsx +163 -0
- package/src/components/ui/checkbox/index.ts +2 -0
- package/src/components/ui/chip/Chip.stories.tsx +112 -0
- package/src/components/ui/chip/Chip.test.tsx +119 -0
- package/src/components/ui/chip/Chip.tsx +151 -0
- package/src/components/ui/chip/index.ts +2 -0
- package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
- package/src/components/ui/collapse/Collapse.test.tsx +298 -0
- package/src/components/ui/collapse/Collapse.tsx +275 -0
- package/src/components/ui/collapse/index.ts +18 -0
- package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
- package/src/components/ui/data-row/DataRow.test.tsx +78 -0
- package/src/components/ui/data-row/DataRow.tsx +29 -0
- package/src/components/ui/data-row/index.ts +2 -0
- package/src/components/ui/divider/Divider.stories.tsx +71 -0
- package/src/components/ui/divider/Divider.test.tsx +55 -0
- package/src/components/ui/divider/Divider.tsx +37 -0
- package/src/components/ui/divider/index.ts +2 -0
- package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
- package/src/components/ui/drawer/Drawer.test.tsx +142 -0
- package/src/components/ui/drawer/Drawer.tsx +228 -0
- package/src/components/ui/drawer/index.ts +9 -0
- package/src/components/ui/form-field/FormField.stories.tsx +260 -0
- package/src/components/ui/form-field/FormField.test.tsx +280 -0
- package/src/components/ui/form-field/FormField.tsx +308 -0
- package/src/components/ui/form-field/index.ts +2 -0
- package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
- package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
- package/src/components/ui/help-tip/HelpTip.tsx +229 -0
- package/src/components/ui/help-tip/index.ts +2 -0
- package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
- package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
- package/src/components/ui/icon-box/IconBox.tsx +57 -0
- package/src/components/ui/icon-box/index.ts +1 -0
- package/src/components/ui/index.ts +36 -0
- package/src/components/ui/input/Input.stories.tsx +192 -0
- package/src/components/ui/input/Input.test.tsx +86 -0
- package/src/components/ui/input/Input.tsx +273 -0
- package/src/components/ui/input/index.ts +2 -0
- package/src/components/ui/link/Link.stories.tsx +78 -0
- package/src/components/ui/link/Link.test.tsx +94 -0
- package/src/components/ui/link/Link.tsx +86 -0
- package/src/components/ui/link/index.ts +2 -0
- package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
- package/src/components/ui/list-item/ListItem.test.tsx +128 -0
- package/src/components/ui/list-item/ListItem.tsx +89 -0
- package/src/components/ui/list-item/index.ts +14 -0
- package/src/components/ui/menu/Menu.stories.tsx +180 -0
- package/src/components/ui/menu/Menu.test.tsx +353 -0
- package/src/components/ui/menu/Menu.tsx +222 -0
- package/src/components/ui/menu/index.ts +2 -0
- package/src/components/ui/modal/Modal.stories.tsx +350 -0
- package/src/components/ui/modal/Modal.test.tsx +159 -0
- package/src/components/ui/modal/Modal.tsx +254 -0
- package/src/components/ui/modal/index.ts +19 -0
- package/src/components/ui/popover/Popover.stories.tsx +190 -0
- package/src/components/ui/popover/Popover.test.tsx +277 -0
- package/src/components/ui/popover/Popover.tsx +162 -0
- package/src/components/ui/popover/index.ts +2 -0
- package/src/components/ui/progress/Progress.stories.tsx +190 -0
- package/src/components/ui/progress/Progress.test.tsx +181 -0
- package/src/components/ui/progress/Progress.tsx +320 -0
- package/src/components/ui/progress/index.ts +9 -0
- package/src/components/ui/radio/Radio.stories.tsx +199 -0
- package/src/components/ui/radio/Radio.test.tsx +166 -0
- package/src/components/ui/radio/Radio.tsx +200 -0
- package/src/components/ui/radio/index.ts +2 -0
- package/src/components/ui/section/Section.stories.tsx +111 -0
- package/src/components/ui/section/Section.test.tsx +121 -0
- package/src/components/ui/section/Section.tsx +52 -0
- package/src/components/ui/section/index.ts +6 -0
- package/src/components/ui/select/Select.stories.tsx +286 -0
- package/src/components/ui/select/Select.test.tsx +180 -0
- package/src/components/ui/select/Select.tsx +236 -0
- package/src/components/ui/select/index.ts +2 -0
- package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
- package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
- package/src/components/ui/skeleton/Skeleton.tsx +229 -0
- package/src/components/ui/skeleton/index.ts +16 -0
- package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
- package/src/components/ui/spinner/Spinner.test.tsx +67 -0
- package/src/components/ui/spinner/Spinner.tsx +72 -0
- package/src/components/ui/spinner/index.ts +2 -0
- package/src/components/ui/stack/Stack.stories.tsx +127 -0
- package/src/components/ui/stack/Stack.test.tsx +184 -0
- package/src/components/ui/stack/Stack.tsx +76 -0
- package/src/components/ui/stack/index.ts +2 -0
- package/src/components/ui/surface/Surface.stories.tsx +91 -0
- package/src/components/ui/surface/Surface.test.tsx +63 -0
- package/src/components/ui/surface/Surface.tsx +51 -0
- package/src/components/ui/surface/index.ts +1 -0
- package/src/components/ui/switch/Switch.stories.tsx +111 -0
- package/src/components/ui/switch/Switch.test.tsx +112 -0
- package/src/components/ui/switch/Switch.tsx +103 -0
- package/src/components/ui/switch/index.ts +2 -0
- package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
- package/src/components/ui/tabs/Tabs.test.tsx +192 -0
- package/src/components/ui/tabs/Tabs.tsx +253 -0
- package/src/components/ui/tabs/index.ts +16 -0
- package/src/components/ui/toast/Toast.stories.tsx +255 -0
- package/src/components/ui/toast/Toast.test.tsx +207 -0
- package/src/components/ui/toast/Toast.tsx +327 -0
- package/src/components/ui/toast/index.ts +8 -0
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
- package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
- package/src/components/ui/toolbar-button/index.ts +7 -0
- package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
- package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
- package/src/components/ui/tooltip/Tooltip.tsx +143 -0
- package/src/components/ui/tooltip/index.ts +2 -0
- package/src/index.ts +10 -0
- package/src/test/setup.ts +19 -0
- package/src/theme/Colors.stories.tsx +258 -0
- package/src/theme/config.ts +193 -0
- package/src/theme/elevation.stories.tsx +346 -0
- package/src/theme/elevation.test.ts +27 -0
- package/src/theme/elevation.ts +567 -0
- package/src/theme/index.ts +39 -0
- package/src/theme/shadows.stories.tsx +523 -0
- package/src/theme/shadows.ts +439 -0
- package/src/theme/tokens/index.ts +2 -0
- package/src/theme/tokens/primitives.ts +354 -0
- package/src/theme/tokens/semantic.ts +381 -0
- package/src/types/jest-axe.d.ts +16 -0
- package/src/types/nativewind.d.ts +54 -0
- package/src/utils/cn.ts +14 -0
- package/src/utils/colors.ts +140 -0
- package/src/utils/form.ts +188 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/useTheme.ts +72 -0
|
@@ -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,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,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
|
+
}
|