@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
@@ -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 // Outline uses subtle elevation
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
- // Merge styles, with elevation style taking precedence for background/shadow
177
- return [style, elevationStyle]
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 (
@@ -34,3 +34,5 @@ export * from './data-row'
34
34
  export * from './icon-box'
35
35
  export * from './surface'
36
36
  export * from './list-item'
37
+ export * from './indicator'
38
+ export * from './pill'
@@ -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,2 @@
1
+ export { Indicator } from './Indicator'
2
+ export type { IndicatorProps, IndicatorSize, IndicatorColor } from './Indicator'
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export { Pill } from './Pill'
2
+ export type { PillProps, PillVariant, PillColor, PillSize } from './Pill'