@titan-design/react-ui 0.1.1 → 0.2.6
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/README.md +47 -1
- package/dist/{chunk-MGW37MPO.mjs → chunk-UXVRPOME.mjs} +173 -3
- package/dist/chunk-UXVRPOME.mjs.map +1 -0
- package/dist/index.d.mts +101 -12
- package/dist/index.d.ts +101 -12
- package/dist/index.js +1267 -468
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +734 -112
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +86 -1
- package/dist/theme/index.d.ts +86 -1
- package/dist/theme/index.js +166 -0
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +1 -1
- package/docs/WEB_SETUP.md +217 -0
- package/package.json +14 -2
- package/src/components/custom/Workout/RestTimer.stories.tsx +71 -0
- package/src/components/custom/Workout/RestTimer.test.tsx +145 -0
- package/src/components/custom/Workout/RestTimer.tsx +172 -0
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +144 -0
- package/src/components/custom/Workout/SupersetWrapper.test.tsx +130 -0
- package/src/components/custom/Workout/SupersetWrapper.tsx +63 -0
- package/src/components/custom/Workout/index.ts +2 -0
- package/src/components/custom/index.ts +1 -0
- package/src/components/custom/stepper/index.ts +2 -2
- package/src/components/ui/avatar/Avatar.test.tsx +17 -0
- package/src/components/ui/avatar/Avatar.tsx +13 -5
- package/src/components/ui/badge/Badge.test.tsx +17 -0
- package/src/components/ui/badge/Badge.tsx +14 -0
- package/src/components/ui/badge/index.ts +1 -0
- package/src/components/ui/button/Button.tsx +55 -0
- package/src/components/ui/card/Card.test.tsx +29 -0
- package/src/components/ui/card/Card.tsx +32 -11
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/indicator/Indicator.stories.tsx +74 -0
- package/src/components/ui/indicator/Indicator.test.tsx +55 -0
- package/src/components/ui/indicator/Indicator.tsx +73 -0
- package/src/components/ui/indicator/index.ts +2 -0
- package/src/components/ui/pill/Pill.stories.tsx +71 -0
- package/src/components/ui/pill/Pill.test.tsx +69 -0
- package/src/components/ui/pill/Pill.tsx +104 -0
- package/src/components/ui/pill/index.ts +2 -0
- package/src/components/ui/popover/Popover.test.tsx +144 -2
- package/src/components/ui/popover/Popover.tsx +81 -6
- package/src/components/ui/progress/Progress.test.tsx +29 -0
- package/src/components/ui/progress/Progress.tsx +54 -35
- package/src/components/ui/select/Select.test.tsx +20 -0
- package/src/components/ui/select/Select.tsx +9 -2
- package/src/components/ui/toast/index.ts +1 -0
- package/src/components/ui/tooltip/Tooltip.test.tsx +83 -0
- package/src/components/ui/tooltip/Tooltip.tsx +117 -26
- package/src/stories/PresetShowcase.stories.tsx +462 -0
- package/src/stories/ThemePresets.stories.tsx +85 -0
- package/src/theme/global.css +129 -1
- package/src/theme/index.ts +1 -0
- package/src/theme/presets/apply.ts +97 -0
- package/src/theme/presets/audiobook.ts +93 -0
- package/src/theme/presets/default.ts +6 -0
- package/src/theme/presets/index.ts +4 -0
- package/src/theme/presets/presets.test.ts +51 -0
- package/src/theme/presets/types.ts +76 -0
- package/src/utils/avatar-color.test.ts +30 -0
- package/src/utils/avatar-color.ts +20 -0
- package/src/web-jsx/jsx-dev-runtime.ts +28 -0
- package/src/web-jsx/jsx-runtime.ts +41 -0
- package/tailwind.config.js +53 -3
- package/dist/chunk-MGW37MPO.mjs.map +0 -1
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from '../../../theme'
|
|
11
11
|
import { useTheme } from '../../../utils/useTheme'
|
|
12
12
|
|
|
13
|
-
export type CardVariant = 'elevated' | 'outline' | 'filled'
|
|
13
|
+
export type CardVariant = 'elevated' | 'outline' | 'filled' | 'accent' | 'subtle'
|
|
14
14
|
export type CardElevation = 1 | 2 | 3 // subtle, standard, prominent
|
|
15
15
|
|
|
16
16
|
export interface CardProps extends ViewProps {
|
|
@@ -28,6 +28,10 @@ export interface CardProps extends ViewProps {
|
|
|
28
28
|
borderColor?: string
|
|
29
29
|
/** Custom background color (hex, rgb, or CSS color). Useful for colored cards. */
|
|
30
30
|
bgColor?: string
|
|
31
|
+
/** Accent stripe color for accent variant (CSS color or hex) */
|
|
32
|
+
accentColor?: string
|
|
33
|
+
/** Accent stripe width for accent variant in pixels (default: 3) */
|
|
34
|
+
accentWidth?: number
|
|
31
35
|
/** Additional className */
|
|
32
36
|
className?: string
|
|
33
37
|
children?: React.ReactNode
|
|
@@ -43,6 +47,8 @@ const variantStyles: Record<CardVariant, string> = {
|
|
|
43
47
|
elevated: '', // Will be set dynamically via elevation system
|
|
44
48
|
outline: 'border-2 border-border-strong', // Thicker border with stronger contrast
|
|
45
49
|
filled: '', // Will be set dynamically via elevation system
|
|
50
|
+
accent: 'border border-border-default',
|
|
51
|
+
subtle: 'border border-border-subtle',
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
/**
|
|
@@ -75,6 +81,8 @@ export function Card({
|
|
|
75
81
|
onPress,
|
|
76
82
|
borderColor,
|
|
77
83
|
bgColor,
|
|
84
|
+
accentColor,
|
|
85
|
+
accentWidth,
|
|
78
86
|
className,
|
|
79
87
|
children,
|
|
80
88
|
style,
|
|
@@ -91,8 +99,8 @@ export function Card({
|
|
|
91
99
|
const elevationLevel = useMemo(() => {
|
|
92
100
|
const validated = getValidatedElevation('card', elevation as ElevationLevel)
|
|
93
101
|
// Map variant to elevation if needed
|
|
94
|
-
if (variant === 'outline') {
|
|
95
|
-
return 1 as ElevationLevel //
|
|
102
|
+
if (variant === 'outline' || variant === 'accent' || variant === 'subtle') {
|
|
103
|
+
return 1 as ElevationLevel // These border variants use subtle elevation
|
|
96
104
|
}
|
|
97
105
|
return validated
|
|
98
106
|
}, [variant, elevation])
|
|
@@ -103,9 +111,13 @@ export function Card({
|
|
|
103
111
|
// Calculate surface color and shadow from elevation
|
|
104
112
|
const surfaceColor = useMemo(() => {
|
|
105
113
|
// Outline variant in light mode uses a specific off-white for clear visibility
|
|
106
|
-
if (variant === 'outline' && theme === 'light') {
|
|
114
|
+
if ((variant === 'outline' || variant === 'accent') && theme === 'light') {
|
|
107
115
|
return '#FAFAFA' // Matches --color-surface-elevated in light mode
|
|
108
116
|
}
|
|
117
|
+
// Subtle variant uses the base surface color without elevation lift
|
|
118
|
+
if (variant === 'subtle') {
|
|
119
|
+
return getElevationSurface(baseColor, 0 as ElevationLevel, theme)
|
|
120
|
+
}
|
|
109
121
|
return getElevationSurface(baseColor, elevationLevel, theme)
|
|
110
122
|
}, [baseColor, elevationLevel, theme, variant])
|
|
111
123
|
|
|
@@ -148,8 +160,8 @@ export function Card({
|
|
|
148
160
|
const baseClassName = cn(
|
|
149
161
|
'rounded-lg overflow-hidden relative',
|
|
150
162
|
// Apply variant styles, but only use default border color if no custom borderColor
|
|
151
|
-
variant === 'outline'
|
|
152
|
-
? borderColor
|
|
163
|
+
variant === 'outline'
|
|
164
|
+
? borderColor
|
|
153
165
|
? 'border-2' // Just the border width, color via style
|
|
154
166
|
: variantStyles[variant] // Full variant styles including color
|
|
155
167
|
: variantStyles[variant],
|
|
@@ -160,22 +172,31 @@ export function Card({
|
|
|
160
172
|
className
|
|
161
173
|
)
|
|
162
174
|
|
|
175
|
+
// Accent variant: left stripe via borderLeft override
|
|
176
|
+
const accentStyle = useMemo(() => variant === 'accent' ? {
|
|
177
|
+
borderLeftWidth: accentWidth ?? 3,
|
|
178
|
+
borderLeftColor: accentColor ?? 'var(--color-brand-primary)',
|
|
179
|
+
} : {}, [variant, accentWidth, accentColor])
|
|
180
|
+
|
|
163
181
|
// Merge styles: background color from elevation + shadow style + custom colors + custom style
|
|
164
182
|
const mergedStyle = useMemo(() => {
|
|
165
183
|
const elevationStyle: Record<string, any> = {
|
|
166
184
|
backgroundColor: bgColor || surfaceColor,
|
|
185
|
+
borderRadius: 8,
|
|
186
|
+
overflow: 'hidden' as const,
|
|
167
187
|
...shadowStyle,
|
|
188
|
+
...accentStyle,
|
|
168
189
|
}
|
|
169
|
-
|
|
190
|
+
|
|
170
191
|
// Add custom border color if provided
|
|
171
192
|
if (borderColor) {
|
|
172
193
|
elevationStyle.borderColor = borderColor
|
|
173
194
|
}
|
|
174
|
-
|
|
195
|
+
|
|
175
196
|
if (!style) return elevationStyle
|
|
176
|
-
//
|
|
177
|
-
return [
|
|
178
|
-
}, [style, surfaceColor, shadowStyle, borderColor, bgColor])
|
|
197
|
+
// User style takes precedence over elevation defaults (e.g. maxWidth, custom bg)
|
|
198
|
+
return [elevationStyle, style]
|
|
199
|
+
}, [style, surfaceColor, shadowStyle, borderColor, bgColor, accentStyle])
|
|
179
200
|
|
|
180
201
|
if (isClickable) {
|
|
181
202
|
return (
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { Indicator } from './Indicator'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Indicator> = {
|
|
6
|
+
title: 'Components/Indicator',
|
|
7
|
+
component: Indicator,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
size: { control: 'select', options: ['xs', 'sm', 'md', 'lg'] },
|
|
11
|
+
color: { control: 'select', options: ['default', 'primary', 'success', 'error', 'warning', 'info'] },
|
|
12
|
+
glow: { control: 'boolean' },
|
|
13
|
+
ring: { control: 'boolean' },
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
export default meta
|
|
17
|
+
type Story = StoryObj<typeof Indicator>
|
|
18
|
+
|
|
19
|
+
export const Default: Story = { args: { color: 'primary', size: 'md' } }
|
|
20
|
+
|
|
21
|
+
export const AllSizes: Story = {
|
|
22
|
+
render: () => (
|
|
23
|
+
<View className="flex-row gap-4 items-center">
|
|
24
|
+
<Indicator size="xs" color="primary" />
|
|
25
|
+
<Indicator size="sm" color="primary" />
|
|
26
|
+
<Indicator size="md" color="primary" />
|
|
27
|
+
<Indicator size="lg" color="primary" />
|
|
28
|
+
</View>
|
|
29
|
+
),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const AllColors: Story = {
|
|
33
|
+
render: () => (
|
|
34
|
+
<View className="flex-row gap-4 items-center">
|
|
35
|
+
<Indicator size="md" color="default" />
|
|
36
|
+
<Indicator size="md" color="primary" />
|
|
37
|
+
<Indicator size="md" color="success" />
|
|
38
|
+
<Indicator size="md" color="error" />
|
|
39
|
+
<Indicator size="md" color="warning" />
|
|
40
|
+
<Indicator size="md" color="info" />
|
|
41
|
+
</View>
|
|
42
|
+
),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const WithGlow: Story = {
|
|
46
|
+
render: () => (
|
|
47
|
+
<View className="flex-row gap-6 items-center p-4 bg-surface-base">
|
|
48
|
+
<Indicator size="md" color="success" glow />
|
|
49
|
+
<Indicator size="md" color="error" glow />
|
|
50
|
+
<Indicator size="md" color="warning" glow />
|
|
51
|
+
<Indicator size="md" color="primary" glow />
|
|
52
|
+
</View>
|
|
53
|
+
),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const WithRing: Story = {
|
|
57
|
+
render: () => (
|
|
58
|
+
<View className="flex-row gap-4 items-center">
|
|
59
|
+
<Indicator size="md" color="success" ring />
|
|
60
|
+
<Indicator size="md" color="error" ring />
|
|
61
|
+
<Indicator size="lg" color="primary" ring />
|
|
62
|
+
</View>
|
|
63
|
+
),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const CustomColor: Story = {
|
|
67
|
+
render: () => (
|
|
68
|
+
<View className="flex-row gap-4 items-center">
|
|
69
|
+
<Indicator size="md" customColor="#FF6B6B" />
|
|
70
|
+
<Indicator size="md" customColor="#4ECDC4" />
|
|
71
|
+
<Indicator size="md" customColor="#45B7D1" glow />
|
|
72
|
+
</View>
|
|
73
|
+
),
|
|
74
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { Indicator } from './Indicator'
|
|
5
|
+
|
|
6
|
+
describe('Indicator', () => {
|
|
7
|
+
it('renders with default props', () => {
|
|
8
|
+
const { container } = render(<Indicator />)
|
|
9
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('renders with all size options', () => {
|
|
13
|
+
const sizes = ['xs', 'sm', 'md', 'lg'] as const
|
|
14
|
+
sizes.forEach((size) => {
|
|
15
|
+
const { unmount } = render(<Indicator size={size} />)
|
|
16
|
+
unmount()
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('renders with all color options', () => {
|
|
21
|
+
const colors = ['default', 'primary', 'success', 'error', 'warning', 'info'] as const
|
|
22
|
+
colors.forEach((color) => {
|
|
23
|
+
const { unmount } = render(<Indicator color={color} />)
|
|
24
|
+
unmount()
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('applies custom className', () => {
|
|
29
|
+
const { container } = render(<Indicator className="custom-class" />)
|
|
30
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('supports customColor via inline style', () => {
|
|
34
|
+
const { container } = render(<Indicator customColor="#FF0000" />)
|
|
35
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('supports ring prop', () => {
|
|
39
|
+
const { container } = render(<Indicator ring />)
|
|
40
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('supports glow prop', () => {
|
|
44
|
+
const { container } = render(<Indicator glow color="success" />)
|
|
45
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('accessibility', () => {
|
|
49
|
+
it('has no accessibility violations', async () => {
|
|
50
|
+
const { container } = render(<Indicator color="success" />)
|
|
51
|
+
const results = await axe(container)
|
|
52
|
+
expect(results).toHaveNoViolations()
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, type ViewProps } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
|
|
5
|
+
export type IndicatorSize = 'xs' | 'sm' | 'md' | 'lg'
|
|
6
|
+
export type IndicatorColor = 'default' | 'primary' | 'success' | 'error' | 'warning' | 'info'
|
|
7
|
+
|
|
8
|
+
export interface IndicatorProps extends ViewProps {
|
|
9
|
+
/** Dot size */
|
|
10
|
+
size?: IndicatorSize
|
|
11
|
+
/** Semantic color */
|
|
12
|
+
color?: IndicatorColor
|
|
13
|
+
/** Custom hex color (overrides color prop) */
|
|
14
|
+
customColor?: string
|
|
15
|
+
/** Add a glow shadow effect */
|
|
16
|
+
glow?: boolean
|
|
17
|
+
/** Add a ring border */
|
|
18
|
+
ring?: boolean
|
|
19
|
+
/** Additional className */
|
|
20
|
+
className?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sizeStyles: Record<IndicatorSize, string> = {
|
|
24
|
+
xs: 'w-1 h-1',
|
|
25
|
+
sm: 'w-1.5 h-1.5',
|
|
26
|
+
md: 'w-2 h-2',
|
|
27
|
+
lg: 'w-2.5 h-2.5',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const colorStyles: Record<IndicatorColor, string> = {
|
|
31
|
+
default: 'bg-text-tertiary',
|
|
32
|
+
primary: 'bg-brand-primary',
|
|
33
|
+
success: 'bg-status-success',
|
|
34
|
+
error: 'bg-status-error',
|
|
35
|
+
warning: 'bg-status-warning',
|
|
36
|
+
info: 'bg-status-info',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function Indicator({
|
|
40
|
+
size = 'sm',
|
|
41
|
+
color = 'default',
|
|
42
|
+
customColor,
|
|
43
|
+
glow = false,
|
|
44
|
+
ring = false,
|
|
45
|
+
className,
|
|
46
|
+
style,
|
|
47
|
+
...props
|
|
48
|
+
}: IndicatorProps) {
|
|
49
|
+
const dynamicStyle = {
|
|
50
|
+
...(typeof style === 'object' ? style : {}),
|
|
51
|
+
...(customColor ? { backgroundColor: customColor } : {}),
|
|
52
|
+
...(glow && !customColor ? {} : {}),
|
|
53
|
+
...(glow && customColor ? { boxShadow: `0 0 6px ${customColor}` } : {}),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<View
|
|
58
|
+
className={cn(
|
|
59
|
+
'rounded-full shrink-0',
|
|
60
|
+
sizeStyles[size],
|
|
61
|
+
!customColor && colorStyles[color],
|
|
62
|
+
ring && 'border-2 border-background-base',
|
|
63
|
+
glow && !customColor && color === 'success' && 'shadow-[0_0_6px_rgba(20,184,166,0.6)]',
|
|
64
|
+
glow && !customColor && color === 'error' && 'shadow-[0_0_6px_rgba(239,68,68,0.6)]',
|
|
65
|
+
glow && !customColor && color === 'warning' && 'shadow-[0_0_6px_rgba(245,158,11,0.6)]',
|
|
66
|
+
glow && !customColor && color === 'primary' && 'shadow-[0_0_6px_rgba(255,121,0,0.6)]',
|
|
67
|
+
className
|
|
68
|
+
)}
|
|
69
|
+
style={Object.keys(dynamicStyle).length > 0 ? dynamicStyle : style}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { Pill } from './Pill'
|
|
4
|
+
import { Indicator } from '../indicator'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Pill> = {
|
|
7
|
+
title: 'Components/Pill',
|
|
8
|
+
component: Pill,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
argTypes: {
|
|
11
|
+
variant: { control: 'select', options: ['subtle', 'outline'] },
|
|
12
|
+
color: { control: 'select', options: ['default', 'primary', 'secondary', 'success', 'error', 'warning', 'info'] },
|
|
13
|
+
size: { control: 'select', options: ['xs', 'sm', 'md'] },
|
|
14
|
+
rounded: { control: 'boolean' },
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
export default meta
|
|
18
|
+
type Story = StoryObj<typeof Pill>
|
|
19
|
+
|
|
20
|
+
export const Default: Story = { args: { children: 'Label', color: 'primary' } }
|
|
21
|
+
|
|
22
|
+
export const AllVariants: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<View className="flex-row gap-2">
|
|
25
|
+
<Pill variant="subtle" color="primary">Subtle</Pill>
|
|
26
|
+
<Pill variant="outline" color="primary">Outline</Pill>
|
|
27
|
+
</View>
|
|
28
|
+
),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const AllColors: Story = {
|
|
32
|
+
render: () => (
|
|
33
|
+
<View className="flex-row gap-2 flex-wrap">
|
|
34
|
+
<Pill color="default">Default</Pill>
|
|
35
|
+
<Pill color="primary">Primary</Pill>
|
|
36
|
+
<Pill color="success">Success</Pill>
|
|
37
|
+
<Pill color="error">Error</Pill>
|
|
38
|
+
<Pill color="warning">Warning</Pill>
|
|
39
|
+
<Pill color="info">Info</Pill>
|
|
40
|
+
</View>
|
|
41
|
+
),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const AllSizes: Story = {
|
|
45
|
+
render: () => (
|
|
46
|
+
<View className="flex-row gap-2 items-center">
|
|
47
|
+
<Pill size="xs" color="primary">XS</Pill>
|
|
48
|
+
<Pill size="sm" color="primary">SM</Pill>
|
|
49
|
+
<Pill size="md" color="primary">MD</Pill>
|
|
50
|
+
</View>
|
|
51
|
+
),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const WithDot: Story = {
|
|
55
|
+
render: () => (
|
|
56
|
+
<View className="flex-row gap-2">
|
|
57
|
+
<Pill color="success" leftElement={<Indicator size="xs" color="success" />}>Active</Pill>
|
|
58
|
+
<Pill color="error" leftElement={<Indicator size="xs" color="error" />}>Failed</Pill>
|
|
59
|
+
<Pill color="warning" leftElement={<Indicator size="xs" color="warning" />}>Pending</Pill>
|
|
60
|
+
</View>
|
|
61
|
+
),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const SquareCorners: Story = {
|
|
65
|
+
render: () => (
|
|
66
|
+
<View className="flex-row gap-2">
|
|
67
|
+
<Pill rounded={false} color="primary">Tag</Pill>
|
|
68
|
+
<Pill rounded={false} color="success">Done</Pill>
|
|
69
|
+
</View>
|
|
70
|
+
),
|
|
71
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { Pill } from './Pill'
|
|
5
|
+
|
|
6
|
+
describe('Pill', () => {
|
|
7
|
+
it('renders string children', () => {
|
|
8
|
+
render(<Pill>Active</Pill>)
|
|
9
|
+
expect(screen.getByText('Active')).toBeInTheDocument()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('renders with all variants', () => {
|
|
13
|
+
const variants = ['subtle', 'outline'] as const
|
|
14
|
+
variants.forEach((variant) => {
|
|
15
|
+
const { unmount } = render(<Pill variant={variant}>Test</Pill>)
|
|
16
|
+
expect(screen.getByText('Test')).toBeInTheDocument()
|
|
17
|
+
unmount()
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('renders with all colors', () => {
|
|
22
|
+
const colors = ['default', 'primary', 'secondary', 'success', 'error', 'warning', 'info'] as const
|
|
23
|
+
colors.forEach((color) => {
|
|
24
|
+
const { unmount } = render(<Pill color={color}>Test</Pill>)
|
|
25
|
+
expect(screen.getByText('Test')).toBeInTheDocument()
|
|
26
|
+
unmount()
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('renders with all sizes', () => {
|
|
31
|
+
const sizes = ['xs', 'sm', 'md'] as const
|
|
32
|
+
sizes.forEach((size) => {
|
|
33
|
+
const { unmount } = render(<Pill size={size}>Test</Pill>)
|
|
34
|
+
expect(screen.getByText('Test')).toBeInTheDocument()
|
|
35
|
+
unmount()
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('applies rounded-full by default', () => {
|
|
40
|
+
const { container } = render(<Pill>Test</Pill>)
|
|
41
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('supports leftElement', () => {
|
|
45
|
+
render(<Pill leftElement={<span data-testid="dot" />}>Label</Pill>)
|
|
46
|
+
expect(screen.getByTestId('dot')).toBeInTheDocument()
|
|
47
|
+
expect(screen.getByText('Label')).toBeInTheDocument()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('handles onPress', () => {
|
|
51
|
+
const handler = vi.fn()
|
|
52
|
+
render(<Pill onPress={handler}>Click</Pill>)
|
|
53
|
+
fireEvent.click(screen.getByText('Click'))
|
|
54
|
+
expect(handler).toHaveBeenCalled()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('applies custom className', () => {
|
|
58
|
+
const { container } = render(<Pill className="custom">Test</Pill>)
|
|
59
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('accessibility', () => {
|
|
63
|
+
it('has no accessibility violations', async () => {
|
|
64
|
+
const { container } = render(<Pill>Active</Pill>)
|
|
65
|
+
const results = await axe(container)
|
|
66
|
+
expect(results).toHaveNoViolations()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, Pressable, type ViewProps } from 'react-native'
|
|
3
|
+
import { cn } from '../../../utils/cn'
|
|
4
|
+
|
|
5
|
+
export type PillVariant = 'subtle' | 'outline'
|
|
6
|
+
export type PillColor = 'default' | 'primary' | 'secondary' | 'success' | 'error' | 'warning' | 'info'
|
|
7
|
+
export type PillSize = 'xs' | 'sm' | 'md'
|
|
8
|
+
|
|
9
|
+
export interface PillProps extends ViewProps {
|
|
10
|
+
/** Pill content */
|
|
11
|
+
children: React.ReactNode
|
|
12
|
+
/** Visual variant */
|
|
13
|
+
variant?: PillVariant
|
|
14
|
+
/** Color scheme */
|
|
15
|
+
color?: PillColor
|
|
16
|
+
/** Size */
|
|
17
|
+
size?: PillSize
|
|
18
|
+
/** Fully rounded (default true) or slight radius */
|
|
19
|
+
rounded?: boolean
|
|
20
|
+
/** Element before the label (e.g., Indicator dot) */
|
|
21
|
+
leftElement?: React.ReactNode
|
|
22
|
+
/** Press handler (makes pill interactive) */
|
|
23
|
+
onPress?: () => void
|
|
24
|
+
/** Additional className */
|
|
25
|
+
className?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const variantColorStyles: Record<PillVariant, Record<PillColor, string>> = {
|
|
29
|
+
subtle: {
|
|
30
|
+
default: 'bg-surface-raised border-border-subtle text-text-secondary',
|
|
31
|
+
primary: 'bg-brand-primary/10 border-brand-primary/25 text-brand-primary',
|
|
32
|
+
secondary: 'bg-brand-secondary/10 border-brand-secondary/25 text-brand-secondary',
|
|
33
|
+
success: 'bg-status-success/10 border-status-success/25 text-status-success',
|
|
34
|
+
error: 'bg-status-error/10 border-status-error/25 text-status-error',
|
|
35
|
+
warning: 'bg-status-warning/10 border-status-warning/25 text-status-warning',
|
|
36
|
+
info: 'bg-status-info/10 border-status-info/25 text-status-info',
|
|
37
|
+
},
|
|
38
|
+
outline: {
|
|
39
|
+
default: 'border-border-default text-text-secondary',
|
|
40
|
+
primary: 'border-brand-primary text-brand-primary',
|
|
41
|
+
secondary: 'border-brand-secondary text-brand-secondary',
|
|
42
|
+
success: 'border-status-success text-status-success',
|
|
43
|
+
error: 'border-status-error text-status-error',
|
|
44
|
+
warning: 'border-status-warning text-status-warning',
|
|
45
|
+
info: 'border-status-info text-status-info',
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const sizeStyles: Record<PillSize, { container: string; text: string }> = {
|
|
50
|
+
xs: { container: 'px-1 py-px', text: 'text-[9px]' },
|
|
51
|
+
sm: { container: 'px-2 py-0.5', text: 'text-[10px]' },
|
|
52
|
+
md: { container: 'px-2 py-1', text: 'text-[10px]' },
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function Pill({
|
|
56
|
+
children,
|
|
57
|
+
variant = 'subtle',
|
|
58
|
+
color = 'default',
|
|
59
|
+
size = 'sm',
|
|
60
|
+
rounded = true,
|
|
61
|
+
leftElement,
|
|
62
|
+
onPress,
|
|
63
|
+
className,
|
|
64
|
+
...props
|
|
65
|
+
}: PillProps) {
|
|
66
|
+
const containerClasses = cn(
|
|
67
|
+
'flex-row items-center gap-1 border self-start shrink-0',
|
|
68
|
+
rounded ? 'rounded-full' : 'rounded',
|
|
69
|
+
variantColorStyles[variant][color],
|
|
70
|
+
sizeStyles[size].container,
|
|
71
|
+
className
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const textClasses = cn(
|
|
75
|
+
'font-heading font-semibold',
|
|
76
|
+
sizeStyles[size].text,
|
|
77
|
+
'text-inherit'
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const content = (
|
|
81
|
+
<>
|
|
82
|
+
{leftElement}
|
|
83
|
+
{typeof children === 'string' ? (
|
|
84
|
+
<Text className={textClasses}>{children}</Text>
|
|
85
|
+
) : (
|
|
86
|
+
children
|
|
87
|
+
)}
|
|
88
|
+
</>
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if (onPress) {
|
|
92
|
+
return (
|
|
93
|
+
<Pressable onPress={onPress} className={containerClasses} {...props}>
|
|
94
|
+
{content}
|
|
95
|
+
</Pressable>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<View className={containerClasses} {...props}>
|
|
101
|
+
{content}
|
|
102
|
+
</View>
|
|
103
|
+
)
|
|
104
|
+
}
|