@titan-design/react-ui 0.1.0 → 0.1.1

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 (192) hide show
  1. package/package.json +10 -5
  2. package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
  3. package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
  4. package/src/components/custom/DateTime/DateTime.tsx +235 -0
  5. package/src/components/custom/DateTime/index.ts +2 -0
  6. package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
  7. package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
  8. package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
  9. package/src/components/custom/EmptyState/index.ts +2 -0
  10. package/src/components/custom/Metric/Metric.stories.tsx +133 -0
  11. package/src/components/custom/Metric/Metric.test.tsx +120 -0
  12. package/src/components/custom/Metric/Metric.tsx +92 -0
  13. package/src/components/custom/Metric/index.ts +2 -0
  14. package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
  15. package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
  16. package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
  17. package/src/components/custom/Sidebar/index.ts +18 -0
  18. package/src/components/custom/Table/Table.stories.tsx +481 -0
  19. package/src/components/custom/Table/Table.test.tsx +531 -0
  20. package/src/components/custom/Table/Table.tsx +696 -0
  21. package/src/components/custom/Table/index.ts +22 -0
  22. package/src/components/custom/Typography/Typography.stories.tsx +153 -0
  23. package/src/components/custom/Typography/Typography.test.tsx +107 -0
  24. package/src/components/custom/Typography/Typography.tsx +218 -0
  25. package/src/components/custom/Typography/index.ts +19 -0
  26. package/src/components/custom/index.ts +8 -0
  27. package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
  28. package/src/components/custom/stepper/Stepper.test.tsx +230 -0
  29. package/src/components/custom/stepper/Stepper.tsx +250 -0
  30. package/src/components/custom/stepper/index.ts +10 -0
  31. package/src/components/index.ts +5 -0
  32. package/src/components/ui/alert/Alert.stories.tsx +154 -0
  33. package/src/components/ui/alert/Alert.test.tsx +202 -0
  34. package/src/components/ui/alert/Alert.tsx +170 -0
  35. package/src/components/ui/alert/index.ts +8 -0
  36. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
  37. package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
  38. package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
  39. package/src/components/ui/autocomplete/index.ts +2 -0
  40. package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
  41. package/src/components/ui/avatar/Avatar.test.tsx +135 -0
  42. package/src/components/ui/avatar/Avatar.tsx +168 -0
  43. package/src/components/ui/avatar/index.ts +2 -0
  44. package/src/components/ui/badge/Badge.stories.tsx +100 -0
  45. package/src/components/ui/badge/Badge.test.tsx +86 -0
  46. package/src/components/ui/badge/Badge.tsx +105 -0
  47. package/src/components/ui/badge/index.ts +2 -0
  48. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
  49. package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
  50. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
  51. package/src/components/ui/breadcrumbs/index.ts +2 -0
  52. package/src/components/ui/button/Button.stories.tsx +216 -0
  53. package/src/components/ui/button/Button.test.tsx +132 -0
  54. package/src/components/ui/button/Button.tsx +242 -0
  55. package/src/components/ui/button/index.ts +10 -0
  56. package/src/components/ui/card/Card.stories.tsx +443 -0
  57. package/src/components/ui/card/Card.test.tsx +265 -0
  58. package/src/components/ui/card/Card.tsx +380 -0
  59. package/src/components/ui/card/index.ts +20 -0
  60. package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
  61. package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
  62. package/src/components/ui/checkbox/Checkbox.tsx +163 -0
  63. package/src/components/ui/checkbox/index.ts +2 -0
  64. package/src/components/ui/chip/Chip.stories.tsx +112 -0
  65. package/src/components/ui/chip/Chip.test.tsx +119 -0
  66. package/src/components/ui/chip/Chip.tsx +151 -0
  67. package/src/components/ui/chip/index.ts +2 -0
  68. package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
  69. package/src/components/ui/collapse/Collapse.test.tsx +298 -0
  70. package/src/components/ui/collapse/Collapse.tsx +275 -0
  71. package/src/components/ui/collapse/index.ts +18 -0
  72. package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
  73. package/src/components/ui/data-row/DataRow.test.tsx +78 -0
  74. package/src/components/ui/data-row/DataRow.tsx +29 -0
  75. package/src/components/ui/data-row/index.ts +2 -0
  76. package/src/components/ui/divider/Divider.stories.tsx +71 -0
  77. package/src/components/ui/divider/Divider.test.tsx +55 -0
  78. package/src/components/ui/divider/Divider.tsx +37 -0
  79. package/src/components/ui/divider/index.ts +2 -0
  80. package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
  81. package/src/components/ui/drawer/Drawer.test.tsx +142 -0
  82. package/src/components/ui/drawer/Drawer.tsx +228 -0
  83. package/src/components/ui/drawer/index.ts +9 -0
  84. package/src/components/ui/form-field/FormField.stories.tsx +260 -0
  85. package/src/components/ui/form-field/FormField.test.tsx +280 -0
  86. package/src/components/ui/form-field/FormField.tsx +308 -0
  87. package/src/components/ui/form-field/index.ts +2 -0
  88. package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
  89. package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
  90. package/src/components/ui/help-tip/HelpTip.tsx +229 -0
  91. package/src/components/ui/help-tip/index.ts +2 -0
  92. package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
  93. package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
  94. package/src/components/ui/icon-box/IconBox.tsx +57 -0
  95. package/src/components/ui/icon-box/index.ts +1 -0
  96. package/src/components/ui/index.ts +36 -0
  97. package/src/components/ui/input/Input.stories.tsx +192 -0
  98. package/src/components/ui/input/Input.test.tsx +86 -0
  99. package/src/components/ui/input/Input.tsx +273 -0
  100. package/src/components/ui/input/index.ts +2 -0
  101. package/src/components/ui/link/Link.stories.tsx +78 -0
  102. package/src/components/ui/link/Link.test.tsx +94 -0
  103. package/src/components/ui/link/Link.tsx +86 -0
  104. package/src/components/ui/link/index.ts +2 -0
  105. package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
  106. package/src/components/ui/list-item/ListItem.test.tsx +128 -0
  107. package/src/components/ui/list-item/ListItem.tsx +89 -0
  108. package/src/components/ui/list-item/index.ts +14 -0
  109. package/src/components/ui/menu/Menu.stories.tsx +180 -0
  110. package/src/components/ui/menu/Menu.test.tsx +353 -0
  111. package/src/components/ui/menu/Menu.tsx +222 -0
  112. package/src/components/ui/menu/index.ts +2 -0
  113. package/src/components/ui/modal/Modal.stories.tsx +350 -0
  114. package/src/components/ui/modal/Modal.test.tsx +159 -0
  115. package/src/components/ui/modal/Modal.tsx +254 -0
  116. package/src/components/ui/modal/index.ts +19 -0
  117. package/src/components/ui/popover/Popover.stories.tsx +190 -0
  118. package/src/components/ui/popover/Popover.test.tsx +277 -0
  119. package/src/components/ui/popover/Popover.tsx +162 -0
  120. package/src/components/ui/popover/index.ts +2 -0
  121. package/src/components/ui/progress/Progress.stories.tsx +190 -0
  122. package/src/components/ui/progress/Progress.test.tsx +181 -0
  123. package/src/components/ui/progress/Progress.tsx +320 -0
  124. package/src/components/ui/progress/index.ts +9 -0
  125. package/src/components/ui/radio/Radio.stories.tsx +199 -0
  126. package/src/components/ui/radio/Radio.test.tsx +166 -0
  127. package/src/components/ui/radio/Radio.tsx +200 -0
  128. package/src/components/ui/radio/index.ts +2 -0
  129. package/src/components/ui/section/Section.stories.tsx +111 -0
  130. package/src/components/ui/section/Section.test.tsx +121 -0
  131. package/src/components/ui/section/Section.tsx +52 -0
  132. package/src/components/ui/section/index.ts +6 -0
  133. package/src/components/ui/select/Select.stories.tsx +286 -0
  134. package/src/components/ui/select/Select.test.tsx +180 -0
  135. package/src/components/ui/select/Select.tsx +236 -0
  136. package/src/components/ui/select/index.ts +2 -0
  137. package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
  138. package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
  139. package/src/components/ui/skeleton/Skeleton.tsx +229 -0
  140. package/src/components/ui/skeleton/index.ts +16 -0
  141. package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
  142. package/src/components/ui/spinner/Spinner.test.tsx +67 -0
  143. package/src/components/ui/spinner/Spinner.tsx +72 -0
  144. package/src/components/ui/spinner/index.ts +2 -0
  145. package/src/components/ui/stack/Stack.stories.tsx +127 -0
  146. package/src/components/ui/stack/Stack.test.tsx +184 -0
  147. package/src/components/ui/stack/Stack.tsx +76 -0
  148. package/src/components/ui/stack/index.ts +2 -0
  149. package/src/components/ui/surface/Surface.stories.tsx +91 -0
  150. package/src/components/ui/surface/Surface.test.tsx +63 -0
  151. package/src/components/ui/surface/Surface.tsx +51 -0
  152. package/src/components/ui/surface/index.ts +1 -0
  153. package/src/components/ui/switch/Switch.stories.tsx +111 -0
  154. package/src/components/ui/switch/Switch.test.tsx +112 -0
  155. package/src/components/ui/switch/Switch.tsx +103 -0
  156. package/src/components/ui/switch/index.ts +2 -0
  157. package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
  158. package/src/components/ui/tabs/Tabs.test.tsx +192 -0
  159. package/src/components/ui/tabs/Tabs.tsx +253 -0
  160. package/src/components/ui/tabs/index.ts +16 -0
  161. package/src/components/ui/toast/Toast.stories.tsx +255 -0
  162. package/src/components/ui/toast/Toast.test.tsx +207 -0
  163. package/src/components/ui/toast/Toast.tsx +327 -0
  164. package/src/components/ui/toast/index.ts +8 -0
  165. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
  166. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
  167. package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
  168. package/src/components/ui/toolbar-button/index.ts +7 -0
  169. package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
  170. package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
  171. package/src/components/ui/tooltip/Tooltip.tsx +143 -0
  172. package/src/components/ui/tooltip/index.ts +2 -0
  173. package/src/index.ts +10 -0
  174. package/src/test/setup.ts +19 -0
  175. package/src/theme/Colors.stories.tsx +258 -0
  176. package/src/theme/config.ts +193 -0
  177. package/src/theme/elevation.stories.tsx +346 -0
  178. package/src/theme/elevation.test.ts +27 -0
  179. package/src/theme/elevation.ts +567 -0
  180. package/src/theme/index.ts +39 -0
  181. package/src/theme/shadows.stories.tsx +523 -0
  182. package/src/theme/shadows.ts +439 -0
  183. package/src/theme/tokens/index.ts +2 -0
  184. package/src/theme/tokens/primitives.ts +354 -0
  185. package/src/theme/tokens/semantic.ts +381 -0
  186. package/src/types/jest-axe.d.ts +16 -0
  187. package/src/types/nativewind.d.ts +54 -0
  188. package/src/utils/cn.ts +14 -0
  189. package/src/utils/colors.ts +140 -0
  190. package/src/utils/form.ts +188 -0
  191. package/src/utils/index.ts +29 -0
  192. package/src/utils/useTheme.ts +72 -0
@@ -0,0 +1,156 @@
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 { Breadcrumbs, BreadcrumbItem } from './Breadcrumbs'
5
+
6
+ describe('Breadcrumbs', () => {
7
+ it('renders breadcrumb items', () => {
8
+ render(
9
+ <Breadcrumbs>
10
+ <BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
11
+ <BreadcrumbItem onPress={() => {}}>Products</BreadcrumbItem>
12
+ <BreadcrumbItem isCurrentPage>Widget</BreadcrumbItem>
13
+ </Breadcrumbs>
14
+ )
15
+
16
+ expect(screen.getByText('Home')).toBeInTheDocument()
17
+ expect(screen.getByText('Products')).toBeInTheDocument()
18
+ expect(screen.getByText('Widget')).toBeInTheDocument()
19
+ })
20
+
21
+ it('renders default separator between items', () => {
22
+ render(
23
+ <Breadcrumbs>
24
+ <BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
25
+ <BreadcrumbItem isCurrentPage>Page</BreadcrumbItem>
26
+ </Breadcrumbs>
27
+ )
28
+
29
+ // Default separator is '/'
30
+ const separators = screen.getAllByText('/')
31
+ expect(separators).toHaveLength(1)
32
+ })
33
+
34
+ it('renders custom separator', () => {
35
+ render(
36
+ <Breadcrumbs separator=">">
37
+ <BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
38
+ <BreadcrumbItem isCurrentPage>Page</BreadcrumbItem>
39
+ </Breadcrumbs>
40
+ )
41
+
42
+ expect(screen.getByText('>')).toBeInTheDocument()
43
+ })
44
+
45
+ it('does not render separator after the last item', () => {
46
+ render(
47
+ <Breadcrumbs>
48
+ <BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
49
+ <BreadcrumbItem onPress={() => {}}>Products</BreadcrumbItem>
50
+ <BreadcrumbItem isCurrentPage>Widget</BreadcrumbItem>
51
+ </Breadcrumbs>
52
+ )
53
+
54
+ const separators = screen.getAllByText('/')
55
+ expect(separators).toHaveLength(2)
56
+ })
57
+
58
+ describe('maxItems with collapse', () => {
59
+ it('collapses items when exceeding maxItems', () => {
60
+ render(
61
+ <Breadcrumbs maxItems={3}>
62
+ <BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
63
+ <BreadcrumbItem onPress={() => {}}>Products</BreadcrumbItem>
64
+ <BreadcrumbItem onPress={() => {}}>Category</BreadcrumbItem>
65
+ <BreadcrumbItem onPress={() => {}}>Subcategory</BreadcrumbItem>
66
+ <BreadcrumbItem isCurrentPage>Widget</BreadcrumbItem>
67
+ </Breadcrumbs>
68
+ )
69
+
70
+ expect(screen.getByText('...')).toBeInTheDocument()
71
+ expect(screen.getByText('Home')).toBeInTheDocument()
72
+ expect(screen.getByText('Widget')).toBeInTheDocument()
73
+ })
74
+
75
+ it('does not collapse when items are within maxItems', () => {
76
+ render(
77
+ <Breadcrumbs maxItems={5}>
78
+ <BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
79
+ <BreadcrumbItem isCurrentPage>Page</BreadcrumbItem>
80
+ </Breadcrumbs>
81
+ )
82
+
83
+ expect(screen.queryByText('...')).not.toBeInTheDocument()
84
+ })
85
+ })
86
+
87
+ describe('BreadcrumbItem', () => {
88
+ it('renders as link when not current page', () => {
89
+ render(
90
+ <Breadcrumbs>
91
+ <BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
92
+ </Breadcrumbs>
93
+ )
94
+
95
+ expect(screen.getByRole('link')).toBeInTheDocument()
96
+ })
97
+
98
+ it('renders as text when isCurrentPage is true', () => {
99
+ render(
100
+ <Breadcrumbs>
101
+ <BreadcrumbItem isCurrentPage>Current</BreadcrumbItem>
102
+ </Breadcrumbs>
103
+ )
104
+
105
+ expect(screen.queryByRole('link')).not.toBeInTheDocument()
106
+ expect(screen.getByText('Current')).toBeInTheDocument()
107
+ })
108
+
109
+ it('calls onPress handler when clicked', () => {
110
+ const onPress = vi.fn()
111
+ render(
112
+ <Breadcrumbs>
113
+ <BreadcrumbItem onPress={onPress}>Home</BreadcrumbItem>
114
+ </Breadcrumbs>
115
+ )
116
+
117
+ fireEvent.click(screen.getByRole('link'))
118
+ expect(onPress).toHaveBeenCalledTimes(1)
119
+ })
120
+
121
+ it('sets aria-current on current page item', () => {
122
+ render(
123
+ <Breadcrumbs>
124
+ <BreadcrumbItem isCurrentPage>Current Page</BreadcrumbItem>
125
+ </Breadcrumbs>
126
+ )
127
+
128
+ expect(screen.getByText('Current Page')).toHaveAttribute('aria-current', 'page')
129
+ })
130
+
131
+ it('accepts custom className', () => {
132
+ render(
133
+ <Breadcrumbs>
134
+ <BreadcrumbItem className="custom-class" isCurrentPage>
135
+ Styled
136
+ </BreadcrumbItem>
137
+ </Breadcrumbs>
138
+ )
139
+ expect(screen.getByText('Styled')).toBeInTheDocument()
140
+ })
141
+ })
142
+
143
+ describe('accessibility', () => {
144
+ it('has no accessibility violations', async () => {
145
+ const { container } = render(
146
+ <Breadcrumbs>
147
+ <BreadcrumbItem onPress={() => {}}>Home</BreadcrumbItem>
148
+ <BreadcrumbItem onPress={() => {}}>Products</BreadcrumbItem>
149
+ <BreadcrumbItem isCurrentPage>Widget</BreadcrumbItem>
150
+ </Breadcrumbs>
151
+ )
152
+ const results = await axe(container)
153
+ expect(results).toHaveNoViolations()
154
+ })
155
+ })
156
+ })
@@ -0,0 +1,127 @@
1
+ import React from 'react'
2
+ import { View, Text, Pressable, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export interface BreadcrumbsProps extends ViewProps {
6
+ /** Custom separator */
7
+ separator?: React.ReactNode
8
+ /** Maximum items to show (with collapse) */
9
+ maxItems?: number
10
+ /** Additional className */
11
+ className?: string
12
+ children?: React.ReactNode
13
+ }
14
+
15
+ /**
16
+ * Breadcrumbs navigation component.
17
+ *
18
+ * @example
19
+ * <Breadcrumbs>
20
+ * <BreadcrumbItem href="/">Home</BreadcrumbItem>
21
+ * <BreadcrumbItem href="/products">Products</BreadcrumbItem>
22
+ * <BreadcrumbItem isCurrentPage>Widget</BreadcrumbItem>
23
+ * </Breadcrumbs>
24
+ */
25
+ export function Breadcrumbs({
26
+ separator = '/',
27
+ maxItems,
28
+ className,
29
+ children,
30
+ ...props
31
+ }: BreadcrumbsProps) {
32
+ const items = React.Children.toArray(children)
33
+ let displayItems = items
34
+
35
+ // Handle max items with ellipsis
36
+ if (maxItems && items.length > maxItems) {
37
+ const firstItem = items[0]
38
+ const lastItems = items.slice(-Math.floor(maxItems / 2))
39
+ displayItems = [
40
+ firstItem,
41
+ <BreadcrumbEllipsis key="ellipsis" />,
42
+ ...lastItems,
43
+ ]
44
+ }
45
+
46
+ return (
47
+ <View
48
+ accessibilityRole="none"
49
+ className={cn('flex-row items-center flex-wrap', className)}
50
+ {...props}
51
+ >
52
+ {displayItems.map((child, index) => (
53
+ <View key={index} className="flex-row items-center">
54
+ {child}
55
+ {index < displayItems.length - 1 && (
56
+ <Text className="mx-2 text-text-tertiary text-sm">
57
+ {separator}
58
+ </Text>
59
+ )}
60
+ </View>
61
+ ))}
62
+ </View>
63
+ )
64
+ }
65
+
66
+ export interface BreadcrumbItemProps {
67
+ /** Navigation URL */
68
+ href?: string
69
+ /** Whether this is the current page */
70
+ isCurrentPage?: boolean
71
+ /** Press handler */
72
+ onPress?: () => void
73
+ /** Additional className */
74
+ className?: string
75
+ children?: React.ReactNode
76
+ }
77
+
78
+ /**
79
+ * Individual breadcrumb item.
80
+ */
81
+ export function BreadcrumbItem({
82
+ href,
83
+ isCurrentPage = false,
84
+ onPress,
85
+ className,
86
+ children,
87
+ }: BreadcrumbItemProps) {
88
+ if (isCurrentPage) {
89
+ return (
90
+ <Text
91
+ accessibilityRole="text"
92
+ aria-current="page"
93
+ className={cn('text-sm font-medium text-text-primary', className)}
94
+ >
95
+ {children}
96
+ </Text>
97
+ )
98
+ }
99
+
100
+ return (
101
+ <Pressable
102
+ onPress={onPress}
103
+ accessibilityRole="link"
104
+ accessibilityHint={href}
105
+ >
106
+ <Text
107
+ className={cn(
108
+ 'text-sm text-text-secondary',
109
+ 'web:hover:text-text-primary web:hover:underline',
110
+ 'active:opacity-70',
111
+ className
112
+ )}
113
+ >
114
+ {children}
115
+ </Text>
116
+ </Pressable>
117
+ )
118
+ }
119
+
120
+ /**
121
+ * Ellipsis for collapsed breadcrumbs.
122
+ */
123
+ function BreadcrumbEllipsis() {
124
+ return (
125
+ <Text className="text-sm text-text-tertiary">...</Text>
126
+ )
127
+ }
@@ -0,0 +1,2 @@
1
+ export { Breadcrumbs, BreadcrumbItem } from './Breadcrumbs'
2
+ export type { BreadcrumbsProps, BreadcrumbItemProps } from './Breadcrumbs'
@@ -0,0 +1,216 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { Button, ButtonText, ButtonIcon } from './Button'
4
+
5
+ const meta: Meta<typeof Button> = {
6
+ title: 'Components/Button',
7
+ component: Button,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ variant: {
11
+ control: 'select',
12
+ options: ['solid', 'outline', 'ghost', 'link'],
13
+ description: 'Visual style variant',
14
+ },
15
+ size: {
16
+ control: 'select',
17
+ options: ['sm', 'md', 'lg'],
18
+ description: 'Button size',
19
+ },
20
+ color: {
21
+ control: 'select',
22
+ options: ['primary', 'secondary', 'accent', 'success', 'error', 'warning', 'info'],
23
+ description: 'Color scheme',
24
+ },
25
+ isIconButton: {
26
+ control: 'boolean',
27
+ description: 'Whether this is an icon-only button (square aspect ratio)',
28
+ },
29
+ isDisabled: {
30
+ control: 'boolean',
31
+ description: 'Whether the button is disabled',
32
+ },
33
+ isLoading: {
34
+ control: 'boolean',
35
+ description: 'Whether the button is in a loading state',
36
+ },
37
+ fullWidth: {
38
+ control: 'boolean',
39
+ description: 'Whether the button should take full width',
40
+ },
41
+ },
42
+ }
43
+
44
+ export default meta
45
+ type Story = StoryObj<typeof Button>
46
+
47
+ export const Primary: Story = {
48
+ args: {
49
+ variant: 'solid',
50
+ color: 'primary',
51
+ size: 'md',
52
+ },
53
+ render: (args) => (
54
+ <Button {...args}>
55
+ <ButtonText>Primary Button</ButtonText>
56
+ </Button>
57
+ ),
58
+ }
59
+
60
+ export const Secondary: Story = {
61
+ args: {
62
+ variant: 'solid',
63
+ color: 'secondary',
64
+ size: 'md',
65
+ },
66
+ render: (args) => (
67
+ <Button {...args}>
68
+ <ButtonText>Secondary Button</ButtonText>
69
+ </Button>
70
+ ),
71
+ }
72
+
73
+ export const Outline: Story = {
74
+ args: {
75
+ variant: 'outline',
76
+ color: 'primary',
77
+ },
78
+ render: (args) => (
79
+ <Button {...args}>
80
+ <ButtonText>Outline Button</ButtonText>
81
+ </Button>
82
+ ),
83
+ }
84
+
85
+ export const Ghost: Story = {
86
+ args: {
87
+ variant: 'ghost',
88
+ color: 'primary',
89
+ },
90
+ render: (args) => (
91
+ <Button {...args}>
92
+ <ButtonText>Ghost Button</ButtonText>
93
+ </Button>
94
+ ),
95
+ }
96
+
97
+ export const Link: Story = {
98
+ args: {
99
+ variant: 'link',
100
+ color: 'primary',
101
+ },
102
+ render: (args) => (
103
+ <Button {...args}>
104
+ <ButtonText>Link Button</ButtonText>
105
+ </Button>
106
+ ),
107
+ }
108
+
109
+ export const Loading: Story = {
110
+ args: {
111
+ isLoading: true,
112
+ loadingText: 'Saving...',
113
+ variant: 'solid',
114
+ color: 'primary',
115
+ },
116
+ render: (args) => (
117
+ <Button {...args}>
118
+ <ButtonText>Save</ButtonText>
119
+ </Button>
120
+ ),
121
+ }
122
+
123
+ export const Disabled: Story = {
124
+ args: {
125
+ isDisabled: true,
126
+ variant: 'solid',
127
+ color: 'primary',
128
+ },
129
+ render: (args) => (
130
+ <Button {...args}>
131
+ <ButtonText>Disabled Button</ButtonText>
132
+ </Button>
133
+ ),
134
+ }
135
+
136
+ export const AllVariants: Story = {
137
+ render: () => (
138
+ <View style={{ gap: 16 }}>
139
+ <View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap' }}>
140
+ <Button variant="solid" color="primary">
141
+ <ButtonText>Solid</ButtonText>
142
+ </Button>
143
+ <Button variant="outline" color="primary">
144
+ <ButtonText>Outline</ButtonText>
145
+ </Button>
146
+ <Button variant="ghost" color="primary">
147
+ <ButtonText>Ghost</ButtonText>
148
+ </Button>
149
+ <Button variant="link" color="primary">
150
+ <ButtonText>Link</ButtonText>
151
+ </Button>
152
+ </View>
153
+ </View>
154
+ ),
155
+ }
156
+
157
+ export const AllColors: Story = {
158
+ render: () => (
159
+ <View style={{ gap: 8 }}>
160
+ <View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap' }}>
161
+ <Button color="primary"><ButtonText>Primary</ButtonText></Button>
162
+ <Button color="secondary"><ButtonText>Secondary</ButtonText></Button>
163
+ <Button color="success"><ButtonText>Success</ButtonText></Button>
164
+ <Button color="error"><ButtonText>Error</ButtonText></Button>
165
+ <Button color="warning"><ButtonText>Warning</ButtonText></Button>
166
+ <Button color="info"><ButtonText>Info</ButtonText></Button>
167
+ </View>
168
+ </View>
169
+ ),
170
+ }
171
+
172
+ export const AllSizes: Story = {
173
+ render: () => (
174
+ <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
175
+ <Button size="sm"><ButtonText>Small</ButtonText></Button>
176
+ <Button size="md"><ButtonText>Medium</ButtonText></Button>
177
+ <Button size="lg"><ButtonText>Large</ButtonText></Button>
178
+ </View>
179
+ ),
180
+ }
181
+
182
+ // Simple plus icon component for demos
183
+ function PlusIcon({ size = 16 }: { size?: number }) {
184
+ return (
185
+ <View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }}>
186
+ <View style={{ position: 'absolute', width: size * 0.6, height: 2, backgroundColor: 'currentColor', borderRadius: 1 }} />
187
+ <View style={{ position: 'absolute', width: 2, height: size * 0.6, backgroundColor: 'currentColor', borderRadius: 1 }} />
188
+ </View>
189
+ )
190
+ }
191
+
192
+ export const IconButton: Story = {
193
+ render: () => (
194
+ <View style={{ gap: 16 }}>
195
+ <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
196
+ <Button isIconButton size="sm" color="primary">
197
+ <ButtonIcon as={PlusIcon} size={14} />
198
+ </Button>
199
+ <Button isIconButton size="md" color="primary">
200
+ <ButtonIcon as={PlusIcon} size={16} />
201
+ </Button>
202
+ <Button isIconButton size="lg" color="primary">
203
+ <ButtonIcon as={PlusIcon} size={20} />
204
+ </Button>
205
+ </View>
206
+ <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
207
+ <Button isIconButton variant="outline" color="primary">
208
+ <ButtonIcon as={PlusIcon} />
209
+ </Button>
210
+ <Button isIconButton variant="ghost" color="primary">
211
+ <ButtonIcon as={PlusIcon} />
212
+ </Button>
213
+ </View>
214
+ </View>
215
+ ),
216
+ }
@@ -0,0 +1,132 @@
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 { Button, ButtonText } from './Button'
5
+
6
+ describe('Button', () => {
7
+ it('renders children correctly', () => {
8
+ render(
9
+ <Button>
10
+ <ButtonText>Click me</ButtonText>
11
+ </Button>
12
+ )
13
+ expect(screen.getByText('Click me')).toBeInTheDocument()
14
+ })
15
+
16
+ it('handles press events', () => {
17
+ const onPress = vi.fn()
18
+ render(
19
+ <Button onPress={onPress}>
20
+ <ButtonText>Click</ButtonText>
21
+ </Button>
22
+ )
23
+
24
+ fireEvent.click(screen.getByRole('button'))
25
+ expect(onPress).toHaveBeenCalledTimes(1)
26
+ })
27
+
28
+ it('respects isDisabled prop', () => {
29
+ const onPress = vi.fn()
30
+ render(
31
+ <Button isDisabled onPress={onPress}>
32
+ <ButtonText>Disabled</ButtonText>
33
+ </Button>
34
+ )
35
+
36
+ const button = screen.getByRole('button')
37
+ expect(button).toBeDisabled()
38
+
39
+ fireEvent.click(button)
40
+ expect(onPress).not.toHaveBeenCalled()
41
+ })
42
+
43
+ it('respects isLoading prop', () => {
44
+ const onPress = vi.fn()
45
+ render(
46
+ <Button isLoading loadingText="Loading..." onPress={onPress}>
47
+ <ButtonText>Submit</ButtonText>
48
+ </Button>
49
+ )
50
+
51
+ expect(screen.getByText('Loading...')).toBeInTheDocument()
52
+
53
+ fireEvent.click(screen.getByRole('button'))
54
+ expect(onPress).not.toHaveBeenCalled()
55
+ })
56
+
57
+ it('applies variant styles correctly', () => {
58
+ const { rerender } = render(
59
+ <Button variant="solid" testID="button">
60
+ <ButtonText>Solid</ButtonText>
61
+ </Button>
62
+ )
63
+ // Button should have solid styles
64
+ expect(screen.getByRole('button')).toBeInTheDocument()
65
+
66
+ rerender(
67
+ <Button variant="outline" testID="button">
68
+ <ButtonText>Outline</ButtonText>
69
+ </Button>
70
+ )
71
+ // Button should have outline styles
72
+ expect(screen.getByRole('button')).toBeInTheDocument()
73
+ })
74
+
75
+ it('applies size styles correctly', () => {
76
+ const { rerender } = render(
77
+ <Button size="sm">
78
+ <ButtonText>Small</ButtonText>
79
+ </Button>
80
+ )
81
+ expect(screen.getByRole('button')).toBeInTheDocument()
82
+
83
+ rerender(
84
+ <Button size="lg">
85
+ <ButtonText>Large</ButtonText>
86
+ </Button>
87
+ )
88
+ expect(screen.getByRole('button')).toBeInTheDocument()
89
+ })
90
+
91
+ it('applies color variants correctly', () => {
92
+ render(
93
+ <Button color="primary">
94
+ <ButtonText>Primary</ButtonText>
95
+ </Button>
96
+ )
97
+ expect(screen.getByRole('button')).toBeInTheDocument()
98
+ })
99
+
100
+ describe('accessibility', () => {
101
+ it('has no accessibility violations', async () => {
102
+ const { container } = render(
103
+ <Button>
104
+ <ButtonText>Accessible Button</ButtonText>
105
+ </Button>
106
+ )
107
+
108
+ const results = await axe(container)
109
+ expect(results).toHaveNoViolations()
110
+ })
111
+
112
+ it('has correct accessibility role', () => {
113
+ render(
114
+ <Button>
115
+ <ButtonText>Button</ButtonText>
116
+ </Button>
117
+ )
118
+ expect(screen.getByRole('button')).toBeInTheDocument()
119
+ })
120
+
121
+ it('communicates disabled state to assistive technology', () => {
122
+ render(
123
+ <Button isDisabled>
124
+ <ButtonText>Disabled</ButtonText>
125
+ </Button>
126
+ )
127
+
128
+ const button = screen.getByRole('button')
129
+ expect(button).toHaveAttribute('aria-disabled', 'true')
130
+ })
131
+ })
132
+ })