@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
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { View, Text } from 'react-native'
|
|
3
|
+
import { SupersetWrapper } from './SupersetWrapper'
|
|
4
|
+
import { ExerciseCard } from './ExerciseCard'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof SupersetWrapper> = {
|
|
7
|
+
title: 'Custom/Workout/SupersetWrapper',
|
|
8
|
+
component: SupersetWrapper,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
decorators: [
|
|
11
|
+
(Story) => (
|
|
12
|
+
<View style={{ maxWidth: 400, padding: 16, backgroundColor: '#121212' }}>
|
|
13
|
+
<Story />
|
|
14
|
+
</View>
|
|
15
|
+
),
|
|
16
|
+
],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default meta
|
|
20
|
+
type Story = StoryObj<typeof SupersetWrapper>
|
|
21
|
+
|
|
22
|
+
export const Default: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
children: (
|
|
25
|
+
<>
|
|
26
|
+
<View
|
|
27
|
+
style={{
|
|
28
|
+
padding: 12,
|
|
29
|
+
backgroundColor: '#1C1C1C',
|
|
30
|
+
borderRadius: 8,
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
|
|
34
|
+
Exercise A
|
|
35
|
+
</Text>
|
|
36
|
+
</View>
|
|
37
|
+
<View
|
|
38
|
+
style={{
|
|
39
|
+
padding: 12,
|
|
40
|
+
backgroundColor: '#1C1C1C',
|
|
41
|
+
borderRadius: 8,
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
|
|
45
|
+
Exercise B
|
|
46
|
+
</Text>
|
|
47
|
+
</View>
|
|
48
|
+
</>
|
|
49
|
+
),
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const CustomLabel: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
label: 'A1/A2',
|
|
56
|
+
children: (
|
|
57
|
+
<>
|
|
58
|
+
<View
|
|
59
|
+
style={{
|
|
60
|
+
padding: 12,
|
|
61
|
+
backgroundColor: '#1C1C1C',
|
|
62
|
+
borderRadius: 8,
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
|
|
66
|
+
Exercise A1
|
|
67
|
+
</Text>
|
|
68
|
+
</View>
|
|
69
|
+
<View
|
|
70
|
+
style={{
|
|
71
|
+
padding: 12,
|
|
72
|
+
backgroundColor: '#1C1C1C',
|
|
73
|
+
borderRadius: 8,
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
|
|
77
|
+
Exercise A2
|
|
78
|
+
</Text>
|
|
79
|
+
</View>
|
|
80
|
+
</>
|
|
81
|
+
),
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const CustomColor: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
color: '#22C55E',
|
|
88
|
+
children: (
|
|
89
|
+
<>
|
|
90
|
+
<View
|
|
91
|
+
style={{
|
|
92
|
+
padding: 12,
|
|
93
|
+
backgroundColor: '#1C1C1C',
|
|
94
|
+
borderRadius: 8,
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
|
|
98
|
+
Exercise A
|
|
99
|
+
</Text>
|
|
100
|
+
</View>
|
|
101
|
+
<View
|
|
102
|
+
style={{
|
|
103
|
+
padding: 12,
|
|
104
|
+
backgroundColor: '#1C1C1C',
|
|
105
|
+
borderRadius: 8,
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<Text style={{ color: '#F3F4F6', fontFamily: 'Inter, sans-serif' }}>
|
|
109
|
+
Exercise B
|
|
110
|
+
</Text>
|
|
111
|
+
</View>
|
|
112
|
+
</>
|
|
113
|
+
),
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const exerciseA = {
|
|
118
|
+
name: 'Bench Press',
|
|
119
|
+
state: 'collapsed' as const,
|
|
120
|
+
onToggle: () => {},
|
|
121
|
+
summary: { sets: 3, reps: 8, weight: 185, unit: 'lbs' as const },
|
|
122
|
+
supersetPosition: 'first' as const,
|
|
123
|
+
supersetColor: '#FF7900',
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const exerciseB = {
|
|
127
|
+
name: 'Bent Over Row',
|
|
128
|
+
state: 'collapsed' as const,
|
|
129
|
+
onToggle: () => {},
|
|
130
|
+
summary: { sets: 3, reps: 8, weight: 155, unit: 'lbs' as const },
|
|
131
|
+
supersetPosition: 'last' as const,
|
|
132
|
+
supersetColor: '#FF7900',
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const WithExerciseCards: Story = {
|
|
136
|
+
args: {
|
|
137
|
+
children: (
|
|
138
|
+
<>
|
|
139
|
+
<ExerciseCard {...exerciseA} />
|
|
140
|
+
<ExerciseCard {...exerciseB} />
|
|
141
|
+
</>
|
|
142
|
+
),
|
|
143
|
+
},
|
|
144
|
+
}
|
|
@@ -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(
|