@titan-design/react-ui 0.1.1 → 0.2.5
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/package.json +13 -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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { View, Text } from 'react-native'
|
|
5
|
+
import { SupersetWrapper } from './SupersetWrapper'
|
|
6
|
+
|
|
7
|
+
describe('SupersetWrapper', () => {
|
|
8
|
+
describe('label', () => {
|
|
9
|
+
it('renders default label "SS"', () => {
|
|
10
|
+
render(
|
|
11
|
+
<SupersetWrapper>
|
|
12
|
+
<View />
|
|
13
|
+
</SupersetWrapper>,
|
|
14
|
+
)
|
|
15
|
+
expect(screen.getByTestId('superset-label')).toHaveTextContent('SS')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('renders custom label', () => {
|
|
19
|
+
render(
|
|
20
|
+
<SupersetWrapper label="A1/A2">
|
|
21
|
+
<View />
|
|
22
|
+
</SupersetWrapper>,
|
|
23
|
+
)
|
|
24
|
+
expect(screen.getByTestId('superset-label')).toHaveTextContent('A1/A2')
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('color', () => {
|
|
29
|
+
it('applies default color #FF7900 to border and label background', () => {
|
|
30
|
+
render(
|
|
31
|
+
<SupersetWrapper>
|
|
32
|
+
<View />
|
|
33
|
+
</SupersetWrapper>,
|
|
34
|
+
)
|
|
35
|
+
const wrapper = screen.getByTestId('superset-wrapper')
|
|
36
|
+
expect(wrapper).toHaveStyle({ borderLeftColor: '#FF7900' })
|
|
37
|
+
|
|
38
|
+
const label = screen.getByTestId('superset-label')
|
|
39
|
+
expect(label).toHaveStyle({ backgroundColor: '#FF7900' })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('applies custom color to border and label background', () => {
|
|
43
|
+
render(
|
|
44
|
+
<SupersetWrapper color="#22C55E">
|
|
45
|
+
<View />
|
|
46
|
+
</SupersetWrapper>,
|
|
47
|
+
)
|
|
48
|
+
const wrapper = screen.getByTestId('superset-wrapper')
|
|
49
|
+
expect(wrapper).toHaveStyle({ borderLeftColor: '#22C55E' })
|
|
50
|
+
|
|
51
|
+
const label = screen.getByTestId('superset-label')
|
|
52
|
+
expect(label).toHaveStyle({ backgroundColor: '#22C55E' })
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('children', () => {
|
|
57
|
+
it('renders children', () => {
|
|
58
|
+
render(
|
|
59
|
+
<SupersetWrapper>
|
|
60
|
+
<Text testID="child-1">Exercise A</Text>
|
|
61
|
+
<Text testID="child-2">Exercise B</Text>
|
|
62
|
+
</SupersetWrapper>,
|
|
63
|
+
)
|
|
64
|
+
expect(screen.getByTestId('child-1')).toHaveTextContent('Exercise A')
|
|
65
|
+
expect(screen.getByTestId('child-2')).toHaveTextContent('Exercise B')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('accessibility', () => {
|
|
70
|
+
it('has group role', () => {
|
|
71
|
+
render(
|
|
72
|
+
<SupersetWrapper>
|
|
73
|
+
<View />
|
|
74
|
+
</SupersetWrapper>,
|
|
75
|
+
)
|
|
76
|
+
expect(screen.getByRole('group')).toBeInTheDocument()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('has correct accessibility label with default label', () => {
|
|
80
|
+
render(
|
|
81
|
+
<SupersetWrapper>
|
|
82
|
+
<View />
|
|
83
|
+
</SupersetWrapper>,
|
|
84
|
+
)
|
|
85
|
+
expect(screen.getByLabelText('Superset: SS')).toBeInTheDocument()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('has correct accessibility label with custom label', () => {
|
|
89
|
+
render(
|
|
90
|
+
<SupersetWrapper label="A1/A2">
|
|
91
|
+
<View />
|
|
92
|
+
</SupersetWrapper>,
|
|
93
|
+
)
|
|
94
|
+
expect(screen.getByLabelText('Superset: A1/A2')).toBeInTheDocument()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('has no accessibility violations', async () => {
|
|
98
|
+
const { container } = render(
|
|
99
|
+
<SupersetWrapper>
|
|
100
|
+
<Text>Exercise A</Text>
|
|
101
|
+
<Text>Exercise B</Text>
|
|
102
|
+
</SupersetWrapper>,
|
|
103
|
+
)
|
|
104
|
+
const results = await axe(container)
|
|
105
|
+
expect(results).toHaveNoViolations()
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('label positioning', () => {
|
|
110
|
+
it('positions label absolutely', () => {
|
|
111
|
+
render(
|
|
112
|
+
<SupersetWrapper>
|
|
113
|
+
<View />
|
|
114
|
+
</SupersetWrapper>,
|
|
115
|
+
)
|
|
116
|
+
const label = screen.getByTestId('superset-label')
|
|
117
|
+
expect(label).toHaveStyle({ position: 'absolute' })
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('positions label at top -1 and left -3', () => {
|
|
121
|
+
render(
|
|
122
|
+
<SupersetWrapper>
|
|
123
|
+
<View />
|
|
124
|
+
</SupersetWrapper>,
|
|
125
|
+
)
|
|
126
|
+
const label = screen.getByTestId('superset-label')
|
|
127
|
+
expect(label).toHaveStyle({ top: '-1px', left: '-3px' })
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Font mapping: font-heading=Space Grotesk, font-body=Nunito Sans (UI), font-sans=Inter (body)
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { View, Text } from 'react-native'
|
|
4
|
+
|
|
5
|
+
export interface SupersetWrapperProps {
|
|
6
|
+
label?: string
|
|
7
|
+
color?: string
|
|
8
|
+
children: React.ReactNode
|
|
9
|
+
syncExpansion?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULTS = {
|
|
13
|
+
label: 'SS',
|
|
14
|
+
color: '#FF7900',
|
|
15
|
+
bgBase: '#101010',
|
|
16
|
+
} as const
|
|
17
|
+
|
|
18
|
+
export function SupersetWrapper({
|
|
19
|
+
label = DEFAULTS.label,
|
|
20
|
+
color = DEFAULTS.color,
|
|
21
|
+
children,
|
|
22
|
+
}: SupersetWrapperProps) {
|
|
23
|
+
return (
|
|
24
|
+
<View
|
|
25
|
+
style={{
|
|
26
|
+
position: 'relative',
|
|
27
|
+
borderLeftWidth: 3,
|
|
28
|
+
borderLeftColor: color,
|
|
29
|
+
paddingLeft: 8,
|
|
30
|
+
marginHorizontal: 12,
|
|
31
|
+
marginBottom: 8,
|
|
32
|
+
overflow: 'visible',
|
|
33
|
+
}}
|
|
34
|
+
accessibilityRole={"group" as any}
|
|
35
|
+
accessibilityLabel={`Superset: ${label}`}
|
|
36
|
+
testID="superset-wrapper"
|
|
37
|
+
>
|
|
38
|
+
<Text
|
|
39
|
+
style={{
|
|
40
|
+
position: 'absolute',
|
|
41
|
+
top: -1,
|
|
42
|
+
left: -3,
|
|
43
|
+
fontSize: 9,
|
|
44
|
+
fontFamily: 'Inter, sans-serif',
|
|
45
|
+
fontWeight: '700',
|
|
46
|
+
backgroundColor: color,
|
|
47
|
+
color: DEFAULTS.bgBase,
|
|
48
|
+
paddingVertical: 2,
|
|
49
|
+
paddingHorizontal: 6,
|
|
50
|
+
borderBottomRightRadius: 4,
|
|
51
|
+
letterSpacing: 0.5,
|
|
52
|
+
zIndex: 2,
|
|
53
|
+
}}
|
|
54
|
+
testID="superset-label"
|
|
55
|
+
>
|
|
56
|
+
{label}
|
|
57
|
+
</Text>
|
|
58
|
+
<View style={{ gap: 2 }} testID="superset-children">
|
|
59
|
+
{children}
|
|
60
|
+
</View>
|
|
61
|
+
</View>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { Stepper, Step, StepIndicator, StepLabel, StepContent } from './Stepper'
|
|
1
|
+
export { Stepper, Step as StepperStep, StepIndicator, StepLabel, StepContent } from './Stepper'
|
|
2
2
|
export type {
|
|
3
3
|
StepperProps,
|
|
4
|
-
StepProps,
|
|
4
|
+
StepProps as StepperStepProps,
|
|
5
5
|
StepIndicatorProps,
|
|
6
6
|
StepLabelProps,
|
|
7
7
|
StepContentProps,
|
|
@@ -54,6 +54,23 @@ describe('Avatar', () => {
|
|
|
54
54
|
expect(screen.getByRole('img')).toBeInTheDocument()
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
+
describe('colorFromName', () => {
|
|
58
|
+
it('renders initials from colorFromName', () => {
|
|
59
|
+
render(<Avatar colorFromName="John Doe" />)
|
|
60
|
+
expect(screen.getByText('JD')).toBeInTheDocument()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('generates deterministic color from name', () => {
|
|
64
|
+
const { container } = render(<Avatar colorFromName="Alice" />)
|
|
65
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('colorFromName fallback is overridden by explicit fallback', () => {
|
|
69
|
+
render(<Avatar colorFromName="John Doe" fallback="XX" />)
|
|
70
|
+
expect(screen.getByText('XX')).toBeInTheDocument()
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
57
74
|
describe('AvatarBadge', () => {
|
|
58
75
|
it('renders with default success color', () => {
|
|
59
76
|
const { container } = render(<AvatarBadge />)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { View, Text, Image, type ViewProps, type ImageSourcePropType } from 'react-native'
|
|
3
3
|
import { cn } from '../../../utils/cn'
|
|
4
|
+
import { avatarColor, getInitials } from '../../../utils/avatar-color'
|
|
4
5
|
|
|
5
6
|
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
|
6
7
|
|
|
@@ -13,6 +14,8 @@ export interface AvatarProps extends ViewProps {
|
|
|
13
14
|
fallback?: string
|
|
14
15
|
/** Alt text for accessibility */
|
|
15
16
|
alt?: string
|
|
17
|
+
/** Generate background color and initials from a name */
|
|
18
|
+
colorFromName?: string
|
|
16
19
|
/** Additional className */
|
|
17
20
|
className?: string
|
|
18
21
|
}
|
|
@@ -48,22 +51,27 @@ export function Avatar({
|
|
|
48
51
|
source,
|
|
49
52
|
fallback,
|
|
50
53
|
alt,
|
|
54
|
+
colorFromName,
|
|
51
55
|
className,
|
|
52
56
|
...props
|
|
53
57
|
}: AvatarProps) {
|
|
54
58
|
const styles = sizeStyles[size]
|
|
55
59
|
const hasImage = source !== undefined
|
|
56
60
|
|
|
61
|
+
const resolvedFallback = fallback ?? (colorFromName ? getInitials(colorFromName) : undefined)
|
|
62
|
+
const nameColor = colorFromName ? avatarColor(colorFromName) : undefined
|
|
63
|
+
|
|
57
64
|
return (
|
|
58
65
|
<View
|
|
59
66
|
accessibilityRole="image"
|
|
60
|
-
accessibilityLabel={alt ||
|
|
67
|
+
accessibilityLabel={alt || resolvedFallback || 'Avatar'}
|
|
61
68
|
className={cn(
|
|
62
69
|
'items-center justify-center rounded-full overflow-hidden',
|
|
63
|
-
'bg-border-strong',
|
|
70
|
+
!nameColor && 'bg-border-strong',
|
|
64
71
|
styles.container,
|
|
65
72
|
className
|
|
66
73
|
)}
|
|
74
|
+
style={nameColor ? { backgroundColor: nameColor } : undefined}
|
|
67
75
|
{...props}
|
|
68
76
|
>
|
|
69
77
|
{hasImage ? (
|
|
@@ -72,9 +80,9 @@ export function Avatar({
|
|
|
72
80
|
className={cn('rounded-full', styles.image)}
|
|
73
81
|
accessibilityLabel={alt}
|
|
74
82
|
/>
|
|
75
|
-
) :
|
|
76
|
-
<Text className={cn('font-semibold text-text-
|
|
77
|
-
{
|
|
83
|
+
) : resolvedFallback ? (
|
|
84
|
+
<Text className={cn('font-semibold text-text-inverse', styles.text)}>
|
|
85
|
+
{resolvedFallback}
|
|
78
86
|
</Text>
|
|
79
87
|
) : (
|
|
80
88
|
<View className="w-full h-full bg-surface-raised" />
|
|
@@ -72,6 +72,23 @@ describe('Badge', () => {
|
|
|
72
72
|
})
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
+
describe('dot indicator', () => {
|
|
76
|
+
it('renders dot when dot prop is true', () => {
|
|
77
|
+
const { container } = render(<Badge dot color="success">Active</Badge>)
|
|
78
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('renders dot with explicit dotColor', () => {
|
|
82
|
+
const { container } = render(<Badge dot dotColor="warning">Pending</Badge>)
|
|
83
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('does not render dot by default', () => {
|
|
87
|
+
const { container } = render(<Badge>No dot</Badge>)
|
|
88
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
75
92
|
describe('accessibility', () => {
|
|
76
93
|
it('has no accessibility violations', async () => {
|
|
77
94
|
const { container } = render(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { View, Text, type ViewProps } from 'react-native'
|
|
3
3
|
import { cn } from '../../../utils/cn'
|
|
4
|
+
import { Indicator, type IndicatorColor } from '../indicator'
|
|
4
5
|
|
|
5
6
|
export type BadgeVariant = 'solid' | 'subtle' | 'outline'
|
|
6
7
|
export type BadgeColor = 'default' | 'primary' | 'secondary' | 'success' | 'error' | 'warning' | 'info'
|
|
@@ -13,6 +14,10 @@ export interface BadgeProps extends ViewProps {
|
|
|
13
14
|
color?: BadgeColor
|
|
14
15
|
/** Size */
|
|
15
16
|
size?: BadgeSize
|
|
17
|
+
/** Show a leading indicator dot */
|
|
18
|
+
dot?: boolean
|
|
19
|
+
/** Color for the dot indicator (defaults to match badge color) */
|
|
20
|
+
dotColor?: IndicatorColor
|
|
16
21
|
/** Additional className */
|
|
17
22
|
className?: string
|
|
18
23
|
children?: React.ReactNode
|
|
@@ -65,6 +70,8 @@ export function Badge({
|
|
|
65
70
|
variant = 'subtle',
|
|
66
71
|
color = 'default',
|
|
67
72
|
size = 'md',
|
|
73
|
+
dot,
|
|
74
|
+
dotColor,
|
|
68
75
|
className,
|
|
69
76
|
children,
|
|
70
77
|
...props
|
|
@@ -79,6 +86,13 @@ export function Badge({
|
|
|
79
86
|
)}
|
|
80
87
|
{...props}
|
|
81
88
|
>
|
|
89
|
+
{dot && (
|
|
90
|
+
<Indicator
|
|
91
|
+
size="xs"
|
|
92
|
+
color={dotColor ?? (color === 'default' || color === 'secondary' ? 'default' : color as IndicatorColor)}
|
|
93
|
+
className="mr-0.5"
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
82
96
|
{typeof children === 'string' ? (
|
|
83
97
|
<Text className="text-inherit font-medium">{children}</Text>
|
|
84
98
|
) : (
|
|
@@ -7,6 +7,52 @@ export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'link'
|
|
|
7
7
|
export type ButtonSize = 'sm' | 'md' | 'lg'
|
|
8
8
|
export type ButtonColor = 'primary' | 'secondary' | 'success' | 'error' | 'warning' | 'info'
|
|
9
9
|
|
|
10
|
+
/** Inline color map for RNW where Tailwind text classes get dropped */
|
|
11
|
+
const textColorMap: Record<ButtonVariant, Record<ButtonColor, string>> = {
|
|
12
|
+
solid: {
|
|
13
|
+
primary: '#FFFFFF',
|
|
14
|
+
secondary: '#FFFFFF',
|
|
15
|
+
success: '#FFFFFF',
|
|
16
|
+
error: '#FFFFFF',
|
|
17
|
+
warning: '#FFFFFF',
|
|
18
|
+
info: '#FFFFFF',
|
|
19
|
+
},
|
|
20
|
+
outline: {
|
|
21
|
+
primary: semanticColorsDark['brand-primary'],
|
|
22
|
+
secondary: semanticColorsDark['brand-secondary'],
|
|
23
|
+
success: semanticColorsDark['status-success'],
|
|
24
|
+
error: semanticColorsDark['status-error'],
|
|
25
|
+
warning: semanticColorsDark['status-warning'],
|
|
26
|
+
info: semanticColorsDark['status-info'],
|
|
27
|
+
},
|
|
28
|
+
ghost: {
|
|
29
|
+
primary: semanticColorsDark['brand-primary'],
|
|
30
|
+
secondary: semanticColorsDark['brand-secondary'],
|
|
31
|
+
success: semanticColorsDark['status-success'],
|
|
32
|
+
error: semanticColorsDark['status-error'],
|
|
33
|
+
warning: semanticColorsDark['status-warning'],
|
|
34
|
+
info: semanticColorsDark['status-info'],
|
|
35
|
+
},
|
|
36
|
+
link: {
|
|
37
|
+
primary: semanticColorsDark['brand-primary'],
|
|
38
|
+
secondary: semanticColorsDark['brand-secondary'],
|
|
39
|
+
success: semanticColorsDark['status-success'],
|
|
40
|
+
error: semanticColorsDark['status-error'],
|
|
41
|
+
warning: semanticColorsDark['status-warning'],
|
|
42
|
+
info: semanticColorsDark['status-info'],
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Inline border color map for outline variant */
|
|
47
|
+
const borderColorMap: Record<ButtonColor, string> = {
|
|
48
|
+
primary: semanticColorsDark['brand-primary'],
|
|
49
|
+
secondary: semanticColorsDark['brand-secondary'],
|
|
50
|
+
success: semanticColorsDark['status-success'],
|
|
51
|
+
error: semanticColorsDark['status-error'],
|
|
52
|
+
warning: semanticColorsDark['status-warning'],
|
|
53
|
+
info: semanticColorsDark['status-info'],
|
|
54
|
+
}
|
|
55
|
+
|
|
10
56
|
export interface ButtonProps extends Omit<PressableProps, 'children'> {
|
|
11
57
|
/** Visual style variant */
|
|
12
58
|
variant?: ButtonVariant
|
|
@@ -151,6 +197,13 @@ export const Button = forwardRef<View, ButtonProps>(function Button(
|
|
|
151
197
|
) {
|
|
152
198
|
const disabled = isDisabled || isLoading
|
|
153
199
|
|
|
200
|
+
const inlineStyle: Record<string, string> = {
|
|
201
|
+
color: textColorMap[variant][color],
|
|
202
|
+
}
|
|
203
|
+
if (variant === 'outline') {
|
|
204
|
+
inlineStyle.borderColor = borderColorMap[color]
|
|
205
|
+
}
|
|
206
|
+
|
|
154
207
|
return (
|
|
155
208
|
<Pressable
|
|
156
209
|
ref={ref}
|
|
@@ -172,6 +225,7 @@ export const Button = forwardRef<View, ButtonProps>(function Button(
|
|
|
172
225
|
variant === 'link' && 'px-0 py-0 min-h-0',
|
|
173
226
|
className
|
|
174
227
|
)}
|
|
228
|
+
style={inlineStyle}
|
|
175
229
|
{...props}
|
|
176
230
|
>
|
|
177
231
|
{isLoading && (
|
|
@@ -188,6 +242,7 @@ export const Button = forwardRef<View, ButtonProps>(function Button(
|
|
|
188
242
|
textSizeStyles[size],
|
|
189
243
|
textStyles[variant][color]
|
|
190
244
|
)}
|
|
245
|
+
style={{ color: textColorMap[variant][color] }}
|
|
191
246
|
>
|
|
192
247
|
{loadingText}
|
|
193
248
|
</Text>
|
|
@@ -216,6 +216,35 @@ describe('Card', () => {
|
|
|
216
216
|
})
|
|
217
217
|
})
|
|
218
218
|
|
|
219
|
+
describe('accent and subtle variants', () => {
|
|
220
|
+
it('renders accent variant', () => {
|
|
221
|
+
const { container } = render(
|
|
222
|
+
<Card variant="accent">
|
|
223
|
+
<CardContent>Accent card</CardContent>
|
|
224
|
+
</Card>
|
|
225
|
+
)
|
|
226
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('renders subtle variant', () => {
|
|
230
|
+
const { container } = render(
|
|
231
|
+
<Card variant="subtle">
|
|
232
|
+
<CardContent>Subtle card</CardContent>
|
|
233
|
+
</Card>
|
|
234
|
+
)
|
|
235
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('renders accent variant with custom accentColor and accentWidth', () => {
|
|
239
|
+
const { container } = render(
|
|
240
|
+
<Card variant="accent" accentColor="#FF0000" accentWidth={5}>
|
|
241
|
+
<CardContent>Custom accent</CardContent>
|
|
242
|
+
</Card>
|
|
243
|
+
)
|
|
244
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
|
|
219
248
|
describe('custom styling', () => {
|
|
220
249
|
it('applies custom borderColor', () => {
|
|
221
250
|
const { container } = render(
|
|
@@ -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
|
+
}
|