@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.
Files changed (67) hide show
  1. package/README.md +47 -1
  2. package/dist/{chunk-MGW37MPO.mjs → chunk-UXVRPOME.mjs} +173 -3
  3. package/dist/chunk-UXVRPOME.mjs.map +1 -0
  4. package/dist/index.d.mts +101 -12
  5. package/dist/index.d.ts +101 -12
  6. package/dist/index.js +1267 -468
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +734 -112
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/theme/index.d.mts +86 -1
  11. package/dist/theme/index.d.ts +86 -1
  12. package/dist/theme/index.js +166 -0
  13. package/dist/theme/index.js.map +1 -1
  14. package/dist/theme/index.mjs +1 -1
  15. package/docs/WEB_SETUP.md +217 -0
  16. package/package.json +14 -2
  17. package/src/components/custom/Workout/RestTimer.stories.tsx +71 -0
  18. package/src/components/custom/Workout/RestTimer.test.tsx +145 -0
  19. package/src/components/custom/Workout/RestTimer.tsx +172 -0
  20. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +144 -0
  21. package/src/components/custom/Workout/SupersetWrapper.test.tsx +130 -0
  22. package/src/components/custom/Workout/SupersetWrapper.tsx +63 -0
  23. package/src/components/custom/Workout/index.ts +2 -0
  24. package/src/components/custom/index.ts +1 -0
  25. package/src/components/custom/stepper/index.ts +2 -2
  26. package/src/components/ui/avatar/Avatar.test.tsx +17 -0
  27. package/src/components/ui/avatar/Avatar.tsx +13 -5
  28. package/src/components/ui/badge/Badge.test.tsx +17 -0
  29. package/src/components/ui/badge/Badge.tsx +14 -0
  30. package/src/components/ui/badge/index.ts +1 -0
  31. package/src/components/ui/button/Button.tsx +55 -0
  32. package/src/components/ui/card/Card.test.tsx +29 -0
  33. package/src/components/ui/card/Card.tsx +32 -11
  34. package/src/components/ui/index.ts +2 -0
  35. package/src/components/ui/indicator/Indicator.stories.tsx +74 -0
  36. package/src/components/ui/indicator/Indicator.test.tsx +55 -0
  37. package/src/components/ui/indicator/Indicator.tsx +73 -0
  38. package/src/components/ui/indicator/index.ts +2 -0
  39. package/src/components/ui/pill/Pill.stories.tsx +71 -0
  40. package/src/components/ui/pill/Pill.test.tsx +69 -0
  41. package/src/components/ui/pill/Pill.tsx +104 -0
  42. package/src/components/ui/pill/index.ts +2 -0
  43. package/src/components/ui/popover/Popover.test.tsx +144 -2
  44. package/src/components/ui/popover/Popover.tsx +81 -6
  45. package/src/components/ui/progress/Progress.test.tsx +29 -0
  46. package/src/components/ui/progress/Progress.tsx +54 -35
  47. package/src/components/ui/select/Select.test.tsx +20 -0
  48. package/src/components/ui/select/Select.tsx +9 -2
  49. package/src/components/ui/toast/index.ts +1 -0
  50. package/src/components/ui/tooltip/Tooltip.test.tsx +83 -0
  51. package/src/components/ui/tooltip/Tooltip.tsx +117 -26
  52. package/src/stories/PresetShowcase.stories.tsx +462 -0
  53. package/src/stories/ThemePresets.stories.tsx +85 -0
  54. package/src/theme/global.css +129 -1
  55. package/src/theme/index.ts +1 -0
  56. package/src/theme/presets/apply.ts +97 -0
  57. package/src/theme/presets/audiobook.ts +93 -0
  58. package/src/theme/presets/default.ts +6 -0
  59. package/src/theme/presets/index.ts +4 -0
  60. package/src/theme/presets/presets.test.ts +51 -0
  61. package/src/theme/presets/types.ts +76 -0
  62. package/src/utils/avatar-color.test.ts +30 -0
  63. package/src/utils/avatar-color.ts +20 -0
  64. package/src/web-jsx/jsx-dev-runtime.ts +28 -0
  65. package/src/web-jsx/jsx-runtime.ts +41 -0
  66. package/tailwind.config.js +53 -3
  67. 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
+ }
@@ -0,0 +1,2 @@
1
+ export { RestTimer, type RestTimerProps } from './RestTimer'
2
+ export { SupersetWrapper, type SupersetWrapperProps } from './SupersetWrapper'
@@ -6,3 +6,4 @@ export * from './EmptyState'
6
6
  export * from './stepper'
7
7
  export * from './DateTime'
8
8
  export * from './Metric'
9
+ export * from './Workout'
@@ -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 || fallback || 'Avatar'}
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
- ) : fallback ? (
76
- <Text className={cn('font-semibold text-text-primary', styles.text)}>
77
- {fallback}
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
  ) : (
@@ -1,2 +1,3 @@
1
1
  export { Badge, BadgeText } from './Badge'
2
2
  export type { BadgeProps, BadgeTextProps, BadgeVariant, BadgeColor, BadgeSize } from './Badge'
3
+ export type { IndicatorColor } from '../indicator'
@@ -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(