@titan-design/react-ui 0.0.2 → 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 (206) hide show
  1. package/dist/{chunk-NA3EES23.mjs → chunk-MGW37MPO.mjs} +26 -4
  2. package/dist/chunk-MGW37MPO.mjs.map +1 -0
  3. package/dist/index.d.mts +115 -2
  4. package/dist/index.d.ts +115 -2
  5. package/dist/index.js +262 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +225 -3
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/theme/index.d.mts +124 -109
  10. package/dist/theme/index.d.ts +124 -109
  11. package/dist/theme/index.js +24 -1
  12. package/dist/theme/index.js.map +1 -1
  13. package/dist/theme/index.mjs +1 -1
  14. package/package.json +12 -5
  15. package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
  16. package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
  17. package/src/components/custom/DateTime/DateTime.tsx +235 -0
  18. package/src/components/custom/DateTime/index.ts +2 -0
  19. package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
  20. package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
  21. package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
  22. package/src/components/custom/EmptyState/index.ts +2 -0
  23. package/src/components/custom/Metric/Metric.stories.tsx +133 -0
  24. package/src/components/custom/Metric/Metric.test.tsx +120 -0
  25. package/src/components/custom/Metric/Metric.tsx +92 -0
  26. package/src/components/custom/Metric/index.ts +2 -0
  27. package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
  28. package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
  29. package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
  30. package/src/components/custom/Sidebar/index.ts +18 -0
  31. package/src/components/custom/Table/Table.stories.tsx +481 -0
  32. package/src/components/custom/Table/Table.test.tsx +531 -0
  33. package/src/components/custom/Table/Table.tsx +696 -0
  34. package/src/components/custom/Table/index.ts +22 -0
  35. package/src/components/custom/Typography/Typography.stories.tsx +153 -0
  36. package/src/components/custom/Typography/Typography.test.tsx +107 -0
  37. package/src/components/custom/Typography/Typography.tsx +218 -0
  38. package/src/components/custom/Typography/index.ts +19 -0
  39. package/src/components/custom/index.ts +8 -0
  40. package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
  41. package/src/components/custom/stepper/Stepper.test.tsx +230 -0
  42. package/src/components/custom/stepper/Stepper.tsx +250 -0
  43. package/src/components/custom/stepper/index.ts +10 -0
  44. package/src/components/index.ts +5 -0
  45. package/src/components/ui/alert/Alert.stories.tsx +154 -0
  46. package/src/components/ui/alert/Alert.test.tsx +202 -0
  47. package/src/components/ui/alert/Alert.tsx +170 -0
  48. package/src/components/ui/alert/index.ts +8 -0
  49. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
  50. package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
  51. package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
  52. package/src/components/ui/autocomplete/index.ts +2 -0
  53. package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
  54. package/src/components/ui/avatar/Avatar.test.tsx +135 -0
  55. package/src/components/ui/avatar/Avatar.tsx +168 -0
  56. package/src/components/ui/avatar/index.ts +2 -0
  57. package/src/components/ui/badge/Badge.stories.tsx +100 -0
  58. package/src/components/ui/badge/Badge.test.tsx +86 -0
  59. package/src/components/ui/badge/Badge.tsx +105 -0
  60. package/src/components/ui/badge/index.ts +2 -0
  61. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
  62. package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
  63. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
  64. package/src/components/ui/breadcrumbs/index.ts +2 -0
  65. package/src/components/ui/button/Button.stories.tsx +216 -0
  66. package/src/components/ui/button/Button.test.tsx +132 -0
  67. package/src/components/ui/button/Button.tsx +242 -0
  68. package/src/components/ui/button/index.ts +10 -0
  69. package/src/components/ui/card/Card.stories.tsx +443 -0
  70. package/src/components/ui/card/Card.test.tsx +265 -0
  71. package/src/components/ui/card/Card.tsx +380 -0
  72. package/src/components/ui/card/index.ts +20 -0
  73. package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
  74. package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
  75. package/src/components/ui/checkbox/Checkbox.tsx +163 -0
  76. package/src/components/ui/checkbox/index.ts +2 -0
  77. package/src/components/ui/chip/Chip.stories.tsx +112 -0
  78. package/src/components/ui/chip/Chip.test.tsx +119 -0
  79. package/src/components/ui/chip/Chip.tsx +151 -0
  80. package/src/components/ui/chip/index.ts +2 -0
  81. package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
  82. package/src/components/ui/collapse/Collapse.test.tsx +298 -0
  83. package/src/components/ui/collapse/Collapse.tsx +275 -0
  84. package/src/components/ui/collapse/index.ts +18 -0
  85. package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
  86. package/src/components/ui/data-row/DataRow.test.tsx +78 -0
  87. package/src/components/ui/data-row/DataRow.tsx +29 -0
  88. package/src/components/ui/data-row/index.ts +2 -0
  89. package/src/components/ui/divider/Divider.stories.tsx +71 -0
  90. package/src/components/ui/divider/Divider.test.tsx +55 -0
  91. package/src/components/ui/divider/Divider.tsx +37 -0
  92. package/src/components/ui/divider/index.ts +2 -0
  93. package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
  94. package/src/components/ui/drawer/Drawer.test.tsx +142 -0
  95. package/src/components/ui/drawer/Drawer.tsx +228 -0
  96. package/src/components/ui/drawer/index.ts +9 -0
  97. package/src/components/ui/form-field/FormField.stories.tsx +260 -0
  98. package/src/components/ui/form-field/FormField.test.tsx +280 -0
  99. package/src/components/ui/form-field/FormField.tsx +308 -0
  100. package/src/components/ui/form-field/index.ts +2 -0
  101. package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
  102. package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
  103. package/src/components/ui/help-tip/HelpTip.tsx +229 -0
  104. package/src/components/ui/help-tip/index.ts +2 -0
  105. package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
  106. package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
  107. package/src/components/ui/icon-box/IconBox.tsx +57 -0
  108. package/src/components/ui/icon-box/index.ts +1 -0
  109. package/src/components/ui/index.ts +36 -0
  110. package/src/components/ui/input/Input.stories.tsx +192 -0
  111. package/src/components/ui/input/Input.test.tsx +86 -0
  112. package/src/components/ui/input/Input.tsx +273 -0
  113. package/src/components/ui/input/index.ts +2 -0
  114. package/src/components/ui/link/Link.stories.tsx +78 -0
  115. package/src/components/ui/link/Link.test.tsx +94 -0
  116. package/src/components/ui/link/Link.tsx +86 -0
  117. package/src/components/ui/link/index.ts +2 -0
  118. package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
  119. package/src/components/ui/list-item/ListItem.test.tsx +128 -0
  120. package/src/components/ui/list-item/ListItem.tsx +89 -0
  121. package/src/components/ui/list-item/index.ts +14 -0
  122. package/src/components/ui/menu/Menu.stories.tsx +180 -0
  123. package/src/components/ui/menu/Menu.test.tsx +353 -0
  124. package/src/components/ui/menu/Menu.tsx +222 -0
  125. package/src/components/ui/menu/index.ts +2 -0
  126. package/src/components/ui/modal/Modal.stories.tsx +350 -0
  127. package/src/components/ui/modal/Modal.test.tsx +159 -0
  128. package/src/components/ui/modal/Modal.tsx +254 -0
  129. package/src/components/ui/modal/index.ts +19 -0
  130. package/src/components/ui/popover/Popover.stories.tsx +190 -0
  131. package/src/components/ui/popover/Popover.test.tsx +277 -0
  132. package/src/components/ui/popover/Popover.tsx +162 -0
  133. package/src/components/ui/popover/index.ts +2 -0
  134. package/src/components/ui/progress/Progress.stories.tsx +190 -0
  135. package/src/components/ui/progress/Progress.test.tsx +181 -0
  136. package/src/components/ui/progress/Progress.tsx +320 -0
  137. package/src/components/ui/progress/index.ts +9 -0
  138. package/src/components/ui/radio/Radio.stories.tsx +199 -0
  139. package/src/components/ui/radio/Radio.test.tsx +166 -0
  140. package/src/components/ui/radio/Radio.tsx +200 -0
  141. package/src/components/ui/radio/index.ts +2 -0
  142. package/src/components/ui/section/Section.stories.tsx +111 -0
  143. package/src/components/ui/section/Section.test.tsx +121 -0
  144. package/src/components/ui/section/Section.tsx +52 -0
  145. package/src/components/ui/section/index.ts +6 -0
  146. package/src/components/ui/select/Select.stories.tsx +286 -0
  147. package/src/components/ui/select/Select.test.tsx +180 -0
  148. package/src/components/ui/select/Select.tsx +236 -0
  149. package/src/components/ui/select/index.ts +2 -0
  150. package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
  151. package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
  152. package/src/components/ui/skeleton/Skeleton.tsx +229 -0
  153. package/src/components/ui/skeleton/index.ts +16 -0
  154. package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
  155. package/src/components/ui/spinner/Spinner.test.tsx +67 -0
  156. package/src/components/ui/spinner/Spinner.tsx +72 -0
  157. package/src/components/ui/spinner/index.ts +2 -0
  158. package/src/components/ui/stack/Stack.stories.tsx +127 -0
  159. package/src/components/ui/stack/Stack.test.tsx +184 -0
  160. package/src/components/ui/stack/Stack.tsx +76 -0
  161. package/src/components/ui/stack/index.ts +2 -0
  162. package/src/components/ui/surface/Surface.stories.tsx +91 -0
  163. package/src/components/ui/surface/Surface.test.tsx +63 -0
  164. package/src/components/ui/surface/Surface.tsx +51 -0
  165. package/src/components/ui/surface/index.ts +1 -0
  166. package/src/components/ui/switch/Switch.stories.tsx +111 -0
  167. package/src/components/ui/switch/Switch.test.tsx +112 -0
  168. package/src/components/ui/switch/Switch.tsx +103 -0
  169. package/src/components/ui/switch/index.ts +2 -0
  170. package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
  171. package/src/components/ui/tabs/Tabs.test.tsx +192 -0
  172. package/src/components/ui/tabs/Tabs.tsx +253 -0
  173. package/src/components/ui/tabs/index.ts +16 -0
  174. package/src/components/ui/toast/Toast.stories.tsx +255 -0
  175. package/src/components/ui/toast/Toast.test.tsx +207 -0
  176. package/src/components/ui/toast/Toast.tsx +327 -0
  177. package/src/components/ui/toast/index.ts +8 -0
  178. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
  179. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
  180. package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
  181. package/src/components/ui/toolbar-button/index.ts +7 -0
  182. package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
  183. package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
  184. package/src/components/ui/tooltip/Tooltip.tsx +143 -0
  185. package/src/components/ui/tooltip/index.ts +2 -0
  186. package/src/index.ts +10 -0
  187. package/src/test/setup.ts +19 -0
  188. package/src/theme/Colors.stories.tsx +258 -0
  189. package/src/theme/config.ts +193 -0
  190. package/src/theme/elevation.stories.tsx +346 -0
  191. package/src/theme/elevation.test.ts +27 -0
  192. package/src/theme/elevation.ts +567 -0
  193. package/src/theme/index.ts +39 -0
  194. package/src/theme/shadows.stories.tsx +523 -0
  195. package/src/theme/shadows.ts +439 -0
  196. package/src/theme/tokens/index.ts +2 -0
  197. package/src/theme/tokens/primitives.ts +354 -0
  198. package/src/theme/tokens/semantic.ts +381 -0
  199. package/src/types/jest-axe.d.ts +16 -0
  200. package/src/types/nativewind.d.ts +54 -0
  201. package/src/utils/cn.ts +14 -0
  202. package/src/utils/colors.ts +140 -0
  203. package/src/utils/form.ts +188 -0
  204. package/src/utils/index.ts +29 -0
  205. package/src/utils/useTheme.ts +72 -0
  206. package/dist/chunk-NA3EES23.mjs.map +0 -1
@@ -0,0 +1,126 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { Checkbox, CheckboxGroup } from './Checkbox'
4
+
5
+ const meta: Meta<typeof Checkbox> = {
6
+ title: 'Components/Checkbox',
7
+ component: Checkbox,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ isChecked: {
11
+ control: 'boolean',
12
+ description: 'Whether the checkbox is checked',
13
+ },
14
+ isIndeterminate: {
15
+ control: 'boolean',
16
+ description: 'Whether the checkbox is in indeterminate state',
17
+ },
18
+ isDisabled: {
19
+ control: 'boolean',
20
+ description: 'Whether the checkbox is disabled',
21
+ },
22
+ isInvalid: {
23
+ control: 'boolean',
24
+ description: 'Whether the checkbox is invalid',
25
+ },
26
+ size: {
27
+ control: 'select',
28
+ options: ['sm', 'md', 'lg'],
29
+ description: 'Checkbox size',
30
+ },
31
+ label: {
32
+ control: 'text',
33
+ description: 'Label text',
34
+ },
35
+ helperText: {
36
+ control: 'text',
37
+ description: 'Helper text below the label',
38
+ },
39
+ },
40
+ }
41
+
42
+ export default meta
43
+ type Story = StoryObj<typeof Checkbox>
44
+
45
+ export const Default: Story = {
46
+ args: {
47
+ label: 'Accept terms and conditions',
48
+ size: 'md',
49
+ },
50
+ render: (args) => <Checkbox {...args} />,
51
+ }
52
+
53
+ export const Checked: Story = {
54
+ args: {
55
+ isChecked: true,
56
+ label: 'Checked checkbox',
57
+ size: 'md',
58
+ },
59
+ render: (args) => <Checkbox {...args} />,
60
+ }
61
+
62
+ export const Indeterminate: Story = {
63
+ args: {
64
+ isChecked: true,
65
+ isIndeterminate: true,
66
+ label: 'Indeterminate state',
67
+ size: 'md',
68
+ },
69
+ render: (args) => <Checkbox {...args} />,
70
+ }
71
+
72
+ export const AllSizes: Story = {
73
+ render: () => (
74
+ <View style={{ gap: 12 }}>
75
+ <Checkbox isChecked size="sm" label="Small checkbox" />
76
+ <Checkbox isChecked size="md" label="Medium checkbox" />
77
+ <Checkbox isChecked size="lg" label="Large checkbox" />
78
+ </View>
79
+ ),
80
+ }
81
+
82
+ export const WithHelperText: Story = {
83
+ args: {
84
+ label: 'Subscribe to newsletter',
85
+ helperText: 'You can unsubscribe at any time.',
86
+ size: 'md',
87
+ },
88
+ render: (args) => <Checkbox {...args} />,
89
+ }
90
+
91
+ export const Disabled: Story = {
92
+ render: () => (
93
+ <View style={{ gap: 12 }}>
94
+ <Checkbox isDisabled label="Disabled unchecked" />
95
+ <Checkbox isDisabled isChecked label="Disabled checked" />
96
+ <Checkbox isDisabled isIndeterminate isChecked label="Disabled indeterminate" />
97
+ </View>
98
+ ),
99
+ }
100
+
101
+ export const Invalid: Story = {
102
+ args: {
103
+ isInvalid: true,
104
+ label: 'You must accept the terms',
105
+ helperText: 'This field is required.',
106
+ size: 'md',
107
+ },
108
+ render: (args) => <Checkbox {...args} />,
109
+ }
110
+
111
+ export const Group: Story = {
112
+ render: () => (
113
+ <View style={{ gap: 16 }}>
114
+ <CheckboxGroup label="Notifications" orientation="vertical">
115
+ <Checkbox isChecked label="Email notifications" />
116
+ <Checkbox label="Push notifications" />
117
+ <Checkbox isChecked label="SMS notifications" />
118
+ </CheckboxGroup>
119
+ <CheckboxGroup label="Horizontal Group" orientation="horizontal">
120
+ <Checkbox isChecked label="Option A" />
121
+ <Checkbox label="Option B" />
122
+ <Checkbox label="Option C" />
123
+ </CheckboxGroup>
124
+ </View>
125
+ ),
126
+ }
@@ -0,0 +1,152 @@
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 { Checkbox, CheckboxGroup } from './Checkbox'
5
+
6
+ describe('Checkbox', () => {
7
+ it('renders correctly', () => {
8
+ render(<Checkbox label="Accept terms" />)
9
+ expect(screen.getByRole('checkbox')).toBeInTheDocument()
10
+ })
11
+
12
+ it('renders label text', () => {
13
+ render(<Checkbox label="Accept terms" />)
14
+ expect(screen.getByText('Accept terms')).toBeInTheDocument()
15
+ })
16
+
17
+ it('renders helper text', () => {
18
+ render(<Checkbox label="Terms" helperText="Please read carefully" />)
19
+ expect(screen.getByText('Please read carefully')).toBeInTheDocument()
20
+ })
21
+
22
+ it('handles checked state change', () => {
23
+ const onCheckedChange = vi.fn()
24
+ render(<Checkbox label="Check me" onCheckedChange={onCheckedChange} />)
25
+ fireEvent.click(screen.getByRole('checkbox'))
26
+ expect(onCheckedChange).toHaveBeenCalledWith(true)
27
+ })
28
+
29
+ it('toggles from checked to unchecked', () => {
30
+ const onCheckedChange = vi.fn()
31
+ render(<Checkbox label="Check me" isChecked onCheckedChange={onCheckedChange} />)
32
+ fireEvent.click(screen.getByRole('checkbox'))
33
+ expect(onCheckedChange).toHaveBeenCalledWith(false)
34
+ })
35
+
36
+ it('does not fire onCheckedChange when disabled', () => {
37
+ const onCheckedChange = vi.fn()
38
+ render(<Checkbox label="Disabled" isDisabled onCheckedChange={onCheckedChange} />)
39
+ fireEvent.click(screen.getByRole('checkbox'))
40
+ expect(onCheckedChange).not.toHaveBeenCalled()
41
+ })
42
+
43
+ it('shows checkmark when checked', () => {
44
+ render(<Checkbox isChecked label="Checked" />)
45
+ expect(screen.getByText('\u2713')).toBeInTheDocument()
46
+ })
47
+
48
+ it('renders indeterminate state', () => {
49
+ render(<Checkbox isIndeterminate label="Mixed" />)
50
+ const checkbox = screen.getByRole('checkbox')
51
+ // react-native-web does not map accessibilityState.checked to aria-checked
52
+ expect(checkbox).toBeInTheDocument()
53
+ })
54
+
55
+ it('communicates disabled state', () => {
56
+ render(<Checkbox isDisabled label="Disabled" />)
57
+ // react-native-web renders aria-disabled="true" on a div, but toBeDisabled() only works on form elements
58
+ expect(screen.getByRole('checkbox')).toHaveAttribute('aria-disabled', 'true')
59
+ })
60
+
61
+ it('renders with all size options', () => {
62
+ const sizes = ['sm', 'md', 'lg'] as const
63
+ sizes.forEach((size) => {
64
+ const { unmount } = render(<Checkbox size={size} label="Test" />)
65
+ expect(screen.getByRole('checkbox')).toBeInTheDocument()
66
+ unmount()
67
+ })
68
+ })
69
+
70
+ it('applies custom className', () => {
71
+ render(<Checkbox className="extra" label="Test" />)
72
+ expect(screen.getByRole('checkbox')).toBeInTheDocument()
73
+ })
74
+
75
+ it('sets accessibility label from label prop', () => {
76
+ render(<Checkbox label="My checkbox" />)
77
+ expect(screen.getByRole('checkbox')).toHaveAttribute('aria-label', 'My checkbox')
78
+ })
79
+
80
+ describe('CheckboxGroup', () => {
81
+ it('renders children', () => {
82
+ render(
83
+ <CheckboxGroup>
84
+ <Checkbox label="Option A" />
85
+ <Checkbox label="Option B" />
86
+ </CheckboxGroup>
87
+ )
88
+ expect(screen.getByText('Option A')).toBeInTheDocument()
89
+ expect(screen.getByText('Option B')).toBeInTheDocument()
90
+ })
91
+
92
+ it('renders with label', () => {
93
+ render(
94
+ <CheckboxGroup label="Select options">
95
+ <Checkbox label="A" />
96
+ </CheckboxGroup>
97
+ )
98
+ expect(screen.getByText('Select options')).toBeInTheDocument()
99
+ })
100
+
101
+ it('supports horizontal orientation', () => {
102
+ const { container } = render(
103
+ <CheckboxGroup orientation="horizontal">
104
+ <Checkbox label="A" />
105
+ <Checkbox label="B" />
106
+ </CheckboxGroup>
107
+ )
108
+ expect(container.firstChild).toBeInTheDocument()
109
+ })
110
+
111
+ it('supports vertical orientation', () => {
112
+ const { container } = render(
113
+ <CheckboxGroup orientation="vertical">
114
+ <Checkbox label="A" />
115
+ <Checkbox label="B" />
116
+ </CheckboxGroup>
117
+ )
118
+ expect(container.firstChild).toBeInTheDocument()
119
+ })
120
+ })
121
+
122
+ describe('accessibility', () => {
123
+ it('has no accessibility violations', async () => {
124
+ const { container } = render(<Checkbox label="Accept terms" />)
125
+ const results = await axe(container, {
126
+ rules: { 'aria-required-attr': { enabled: false } },
127
+ })
128
+ expect(results).toHaveNoViolations()
129
+ })
130
+
131
+ it('has no accessibility violations when checked', async () => {
132
+ const { container } = render(<Checkbox label="Checked" isChecked />)
133
+ const results = await axe(container, {
134
+ rules: { 'aria-required-attr': { enabled: false } },
135
+ })
136
+ expect(results).toHaveNoViolations()
137
+ })
138
+
139
+ it('has no accessibility violations for CheckboxGroup', async () => {
140
+ const { container } = render(
141
+ <CheckboxGroup label="Options">
142
+ <Checkbox label="A" />
143
+ <Checkbox label="B" />
144
+ </CheckboxGroup>
145
+ )
146
+ const results = await axe(container, {
147
+ rules: { 'aria-required-attr': { enabled: false } },
148
+ })
149
+ expect(results).toHaveNoViolations()
150
+ })
151
+ })
152
+ })
@@ -0,0 +1,163 @@
1
+ import React, { forwardRef } from 'react'
2
+ import { Pressable, View, Text, type PressableProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ export type CheckboxSize = 'sm' | 'md' | 'lg'
6
+
7
+ export interface CheckboxProps extends Omit<PressableProps, 'children'> {
8
+ /** Whether the checkbox is checked */
9
+ isChecked?: boolean
10
+ /** Whether the checkbox is in indeterminate state */
11
+ isIndeterminate?: boolean
12
+ /** Whether the checkbox is disabled */
13
+ isDisabled?: boolean
14
+ /** Whether the checkbox is invalid */
15
+ isInvalid?: boolean
16
+ /** Checkbox size */
17
+ size?: CheckboxSize
18
+ /** Label text */
19
+ label?: string
20
+ /** Helper text */
21
+ helperText?: string
22
+ /** Value for forms */
23
+ value?: string
24
+ /** Callback when checked state changes */
25
+ onCheckedChange?: (checked: boolean) => void
26
+ /** Additional className */
27
+ className?: string
28
+ }
29
+
30
+ const sizeStyles: Record<CheckboxSize, { box: string; icon: string; label: string }> = {
31
+ sm: { box: 'w-4 h-4', icon: 'w-2.5 h-2.5', label: 'text-sm' },
32
+ md: { box: 'w-5 h-5', icon: 'w-3 h-3', label: 'text-base' },
33
+ lg: { box: 'w-6 h-6', icon: 'w-3.5 h-3.5', label: 'text-lg' },
34
+ }
35
+
36
+ /**
37
+ * Checkbox component for boolean inputs.
38
+ *
39
+ * @example
40
+ * <Checkbox
41
+ * isChecked={checked}
42
+ * onCheckedChange={setChecked}
43
+ * label="Accept terms and conditions"
44
+ * />
45
+ */
46
+ export const Checkbox = forwardRef<View, CheckboxProps>(function Checkbox(
47
+ {
48
+ isChecked = false,
49
+ isIndeterminate = false,
50
+ isDisabled = false,
51
+ isInvalid = false,
52
+ size = 'md',
53
+ label,
54
+ helperText,
55
+ value,
56
+ onCheckedChange,
57
+ className,
58
+ ...props
59
+ },
60
+ ref
61
+ ) {
62
+ const styles = sizeStyles[size]
63
+
64
+ const handlePress = () => {
65
+ if (!isDisabled) {
66
+ onCheckedChange?.(!isChecked)
67
+ }
68
+ }
69
+
70
+ return (
71
+ <Pressable
72
+ ref={ref}
73
+ disabled={isDisabled}
74
+ onPress={handlePress}
75
+ accessibilityRole="checkbox"
76
+ accessibilityState={{
77
+ checked: isIndeterminate ? 'mixed' : isChecked,
78
+ disabled: isDisabled,
79
+ }}
80
+ accessibilityLabel={label}
81
+ className={cn(
82
+ 'flex-row items-start gap-2',
83
+ isDisabled && 'opacity-50',
84
+ className
85
+ )}
86
+ {...props}
87
+ >
88
+ <View
89
+ className={cn(
90
+ 'items-center justify-center rounded border-2 transition-colors',
91
+ styles.box,
92
+ isChecked || isIndeterminate
93
+ ? 'bg-brand-primary border-brand-primary'
94
+ : 'bg-transparent border-border-default',
95
+ isInvalid && 'border-status-error',
96
+ !isDisabled && 'web:hover:border-brand-primary'
97
+ )}
98
+ >
99
+ {isChecked && !isIndeterminate && (
100
+ <View className={cn('bg-white rounded-sm', styles.icon)}>
101
+ {/* Checkmark icon - using a simple view as placeholder */}
102
+ <Text className="text-brand-primary font-bold text-center leading-none">✓</Text>
103
+ </View>
104
+ )}
105
+ {isIndeterminate && (
106
+ <View className={cn('bg-white h-0.5 w-2/3 rounded')} />
107
+ )}
108
+ </View>
109
+
110
+ {(label || helperText) && (
111
+ <View className="flex-1">
112
+ {label && (
113
+ <Text className={cn('text-text-primary font-medium', styles.label)}>
114
+ {label}
115
+ </Text>
116
+ )}
117
+ {helperText && (
118
+ <Text className="text-xs text-text-secondary mt-0.5">
119
+ {helperText}
120
+ </Text>
121
+ )}
122
+ </View>
123
+ )}
124
+ </Pressable>
125
+ )
126
+ })
127
+
128
+ export interface CheckboxGroupProps {
129
+ children: React.ReactNode
130
+ /** Label for the group */
131
+ label?: string
132
+ /** Orientation of checkboxes */
133
+ orientation?: 'horizontal' | 'vertical'
134
+ /** Additional className */
135
+ className?: string
136
+ }
137
+
138
+ /**
139
+ * Container for grouping related checkboxes.
140
+ */
141
+ export function CheckboxGroup({
142
+ children,
143
+ label,
144
+ orientation = 'vertical',
145
+ className,
146
+ }: CheckboxGroupProps) {
147
+ return (
148
+ <View className={cn('gap-1', className)}>
149
+ {label && (
150
+ <Text className="text-sm font-medium text-text-primary mb-2">
151
+ {label}
152
+ </Text>
153
+ )}
154
+ <View
155
+ className={cn(
156
+ orientation === 'vertical' ? 'flex-col gap-2' : 'flex-row gap-4'
157
+ )}
158
+ >
159
+ {children}
160
+ </View>
161
+ </View>
162
+ )
163
+ }
@@ -0,0 +1,2 @@
1
+ export { Checkbox, CheckboxGroup } from './Checkbox'
2
+ export type { CheckboxProps, CheckboxGroupProps, CheckboxSize } from './Checkbox'
@@ -0,0 +1,112 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { Chip } from './Chip'
4
+
5
+ const meta: Meta<typeof Chip> = {
6
+ title: 'Components/Chip',
7
+ component: Chip,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ variant: {
11
+ control: 'select',
12
+ options: ['solid', 'subtle', 'outline'],
13
+ },
14
+ color: {
15
+ control: 'select',
16
+ options: ['default', 'primary', 'secondary', 'success', 'error', 'warning', 'info'],
17
+ },
18
+ size: {
19
+ control: 'select',
20
+ options: ['sm', 'md', 'lg'],
21
+ },
22
+ isDisabled: { control: 'boolean' },
23
+ },
24
+ }
25
+
26
+ export default meta
27
+ type Story = StoryObj<typeof Chip>
28
+
29
+ export const Default: Story = {
30
+ args: {
31
+ children: 'Chip',
32
+ variant: 'subtle',
33
+ color: 'default',
34
+ size: 'md',
35
+ },
36
+ }
37
+
38
+ export const AllColors: Story = {
39
+ render: () => (
40
+ <View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap' }}>
41
+ <Chip color="default">Default</Chip>
42
+ <Chip color="primary">Primary</Chip>
43
+ <Chip color="secondary">Secondary</Chip>
44
+ <Chip color="success">Success</Chip>
45
+ <Chip color="error">Error</Chip>
46
+ <Chip color="warning">Warning</Chip>
47
+ <Chip color="info">Info</Chip>
48
+ </View>
49
+ ),
50
+ }
51
+
52
+ export const AllVariants: Story = {
53
+ render: () => (
54
+ <View style={{ gap: 12 }}>
55
+ <View style={{ flexDirection: 'row', gap: 8 }}>
56
+ <Chip variant="solid" color="primary">Solid</Chip>
57
+ <Chip variant="subtle" color="primary">Subtle</Chip>
58
+ <Chip variant="outline" color="primary">Outline</Chip>
59
+ </View>
60
+ </View>
61
+ ),
62
+ }
63
+
64
+ export const AllSizes: Story = {
65
+ render: () => (
66
+ <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
67
+ <Chip size="sm">Small</Chip>
68
+ <Chip size="md">Medium</Chip>
69
+ <Chip size="lg">Large</Chip>
70
+ </View>
71
+ ),
72
+ }
73
+
74
+ export const Dismissible: Story = {
75
+ render: () => (
76
+ <View style={{ flexDirection: 'row', gap: 8 }}>
77
+ <Chip onDelete={() => console.log('delete')}>Remove me</Chip>
78
+ <Chip color="primary" onDelete={() => console.log('delete')}>Primary</Chip>
79
+ <Chip color="error" onDelete={() => console.log('delete')}>Error</Chip>
80
+ </View>
81
+ ),
82
+ }
83
+
84
+ export const Clickable: Story = {
85
+ render: () => (
86
+ <View style={{ flexDirection: 'row', gap: 8 }}>
87
+ <Chip onPress={() => console.log('clicked')}>Click me</Chip>
88
+ <Chip color="primary" onPress={() => console.log('clicked')}>Primary</Chip>
89
+ </View>
90
+ ),
91
+ }
92
+
93
+ export const Disabled: Story = {
94
+ render: () => (
95
+ <View style={{ flexDirection: 'row', gap: 8 }}>
96
+ <Chip isDisabled>Disabled</Chip>
97
+ <Chip isDisabled onDelete={() => {}}>Disabled Dismissible</Chip>
98
+ <Chip isDisabled onPress={() => {}}>Disabled Clickable</Chip>
99
+ </View>
100
+ ),
101
+ }
102
+
103
+ export const AsTagList: Story = {
104
+ render: () => (
105
+ <View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap', maxWidth: 300 }}>
106
+ <Chip size="sm" color="primary" onDelete={() => {}}>React</Chip>
107
+ <Chip size="sm" color="secondary" onDelete={() => {}}>TypeScript</Chip>
108
+ <Chip size="sm" color="info" onDelete={() => {}}>Tailwind</Chip>
109
+ <Chip size="sm" color="success" onDelete={() => {}}>NativeWind</Chip>
110
+ </View>
111
+ ),
112
+ }
@@ -0,0 +1,119 @@
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 { Chip } from './Chip'
5
+
6
+ describe('Chip', () => {
7
+ it('renders children correctly', () => {
8
+ render(<Chip>Label</Chip>)
9
+ expect(screen.getByText('Label')).toBeInTheDocument()
10
+ })
11
+
12
+ it('renders as a View when not clickable', () => {
13
+ const { container } = render(<Chip>Static</Chip>)
14
+ expect(container.firstChild).toBeInTheDocument()
15
+ })
16
+
17
+ it('renders as a Pressable when onPress is provided', () => {
18
+ render(<Chip onPress={() => {}}>Clickable</Chip>)
19
+ expect(screen.getByRole('button')).toBeInTheDocument()
20
+ })
21
+
22
+ it('handles press events', () => {
23
+ const onPress = vi.fn()
24
+ render(<Chip onPress={onPress}>Click me</Chip>)
25
+ fireEvent.click(screen.getByRole('button'))
26
+ expect(onPress).toHaveBeenCalledTimes(1)
27
+ })
28
+
29
+ it('does not fire onPress when disabled', () => {
30
+ const onPress = vi.fn()
31
+ render(
32
+ <Chip onPress={onPress} isDisabled>
33
+ Disabled
34
+ </Chip>
35
+ )
36
+ const button = screen.getByRole('button')
37
+ expect(button).toBeDisabled()
38
+ fireEvent.click(button)
39
+ expect(onPress).not.toHaveBeenCalled()
40
+ })
41
+
42
+ it('renders delete button when onDelete is provided', () => {
43
+ const onDelete = vi.fn()
44
+ render(<Chip onDelete={onDelete}>Dismissible</Chip>)
45
+ expect(screen.getByLabelText('Remove')).toBeInTheDocument()
46
+ })
47
+
48
+ it('handles delete events', () => {
49
+ const onDelete = vi.fn()
50
+ render(<Chip onDelete={onDelete}>Dismissible</Chip>)
51
+ fireEvent.click(screen.getByLabelText('Remove'))
52
+ expect(onDelete).toHaveBeenCalledTimes(1)
53
+ })
54
+
55
+ it('renders leftElement', () => {
56
+ render(
57
+ <Chip leftElement={<span data-testid="icon">icon</span>}>
58
+ With Icon
59
+ </Chip>
60
+ )
61
+ expect(screen.getByTestId('icon')).toBeInTheDocument()
62
+ })
63
+
64
+ it('renders with all variant options', () => {
65
+ const variants = ['solid', 'subtle', 'outline'] as const
66
+ variants.forEach((variant) => {
67
+ const { unmount } = render(<Chip variant={variant}>Test</Chip>)
68
+ expect(screen.getByText('Test')).toBeInTheDocument()
69
+ unmount()
70
+ })
71
+ })
72
+
73
+ it('renders with all color options', () => {
74
+ const colors = ['default', 'primary', 'secondary', 'success', 'error', 'warning', 'info'] as const
75
+ colors.forEach((color) => {
76
+ const { unmount } = render(<Chip color={color}>Test</Chip>)
77
+ expect(screen.getByText('Test')).toBeInTheDocument()
78
+ unmount()
79
+ })
80
+ })
81
+
82
+ it('renders with all size options', () => {
83
+ const sizes = ['sm', 'md', 'lg'] as const
84
+ sizes.forEach((size) => {
85
+ const { unmount } = render(<Chip size={size}>Test</Chip>)
86
+ expect(screen.getByText('Test')).toBeInTheDocument()
87
+ unmount()
88
+ })
89
+ })
90
+
91
+ it('applies custom className', () => {
92
+ const { container } = render(<Chip className="extra">Test</Chip>)
93
+ expect(container.firstChild).toBeInTheDocument()
94
+ })
95
+
96
+ describe('accessibility', () => {
97
+ it('has no accessibility violations for static chip', async () => {
98
+ const { container } = render(<Chip>Label</Chip>)
99
+ const results = await axe(container)
100
+ expect(results).toHaveNoViolations()
101
+ })
102
+
103
+ it('has no accessibility violations for clickable chip', async () => {
104
+ const { container } = render(<Chip onPress={() => {}}>Clickable</Chip>)
105
+ const results = await axe(container)
106
+ expect(results).toHaveNoViolations()
107
+ })
108
+
109
+ it('has button role when clickable', () => {
110
+ render(<Chip onPress={() => {}}>Clickable</Chip>)
111
+ expect(screen.getByRole('button')).toBeInTheDocument()
112
+ })
113
+
114
+ it('delete button has accessible label', () => {
115
+ render(<Chip onDelete={() => {}}>Tag</Chip>)
116
+ expect(screen.getByLabelText('Remove')).toBeInTheDocument()
117
+ })
118
+ })
119
+ })