@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,139 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View } from 'react-native'
3
+ import { IconBox } from './IconBox'
4
+
5
+ function StarIcon({ size = 20, className }: { size?: number; className?: string }) {
6
+ return (
7
+ <View
8
+ className={className}
9
+ style={{
10
+ width: size,
11
+ height: size,
12
+ justifyContent: 'center',
13
+ alignItems: 'center',
14
+ }}
15
+ >
16
+ <View
17
+ style={{
18
+ width: size * 0.8,
19
+ height: size * 0.8,
20
+ borderRadius: size * 0.4,
21
+ backgroundColor: 'currentColor',
22
+ }}
23
+ />
24
+ </View>
25
+ )
26
+ }
27
+
28
+ function BoltIcon({ size = 20, className }: { size?: number; className?: string }) {
29
+ return (
30
+ <View
31
+ className={className}
32
+ style={{
33
+ width: size,
34
+ height: size,
35
+ justifyContent: 'center',
36
+ alignItems: 'center',
37
+ }}
38
+ >
39
+ <View
40
+ style={{
41
+ width: 0,
42
+ height: 0,
43
+ borderLeftWidth: size * 0.3,
44
+ borderRightWidth: size * 0.3,
45
+ borderBottomWidth: size * 0.6,
46
+ borderLeftColor: 'transparent',
47
+ borderRightColor: 'transparent',
48
+ borderBottomColor: 'currentColor',
49
+ }}
50
+ />
51
+ </View>
52
+ )
53
+ }
54
+
55
+ const meta: Meta<typeof IconBox> = {
56
+ title: 'Components/IconBox',
57
+ component: IconBox,
58
+ tags: ['autodocs'],
59
+ argTypes: {
60
+ color: {
61
+ control: 'select',
62
+ options: ['primary', 'secondary', 'success', 'error', 'warning', 'info', 'neutral'],
63
+ description: 'Color variant for the icon box',
64
+ },
65
+ size: {
66
+ control: 'select',
67
+ options: ['sm', 'md', 'lg'],
68
+ description: 'Size variant',
69
+ },
70
+ },
71
+ }
72
+
73
+ export default meta
74
+ type Story = StoryObj<typeof IconBox>
75
+
76
+ export const Default: Story = {
77
+ args: {
78
+ icon: StarIcon,
79
+ color: 'neutral',
80
+ size: 'md',
81
+ },
82
+ }
83
+
84
+ export const Primary: Story = {
85
+ args: {
86
+ icon: StarIcon,
87
+ color: 'primary',
88
+ size: 'md',
89
+ },
90
+ }
91
+
92
+ export const Small: Story = {
93
+ args: {
94
+ icon: StarIcon,
95
+ color: 'primary',
96
+ size: 'sm',
97
+ },
98
+ }
99
+
100
+ export const Large: Story = {
101
+ args: {
102
+ icon: StarIcon,
103
+ color: 'primary',
104
+ size: 'lg',
105
+ },
106
+ }
107
+
108
+ export const AllColors: Story = {
109
+ render: () => (
110
+ <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap' }}>
111
+ <IconBox icon={StarIcon} color="primary" />
112
+ <IconBox icon={StarIcon} color="secondary" />
113
+ <IconBox icon={StarIcon} color="success" />
114
+ <IconBox icon={StarIcon} color="error" />
115
+ <IconBox icon={StarIcon} color="warning" />
116
+ <IconBox icon={StarIcon} color="info" />
117
+ <IconBox icon={StarIcon} color="neutral" />
118
+ </View>
119
+ ),
120
+ }
121
+
122
+ export const AllSizes: Story = {
123
+ render: () => (
124
+ <View style={{ flexDirection: 'row', gap: 12, alignItems: 'center' }}>
125
+ <IconBox icon={StarIcon} color="primary" size="sm" />
126
+ <IconBox icon={StarIcon} color="primary" size="md" />
127
+ <IconBox icon={StarIcon} color="primary" size="lg" />
128
+ </View>
129
+ ),
130
+ }
131
+
132
+ export const DifferentIcons: Story = {
133
+ render: () => (
134
+ <View style={{ flexDirection: 'row', gap: 12 }}>
135
+ <IconBox icon={StarIcon} color="success" />
136
+ <IconBox icon={BoltIcon} color="warning" />
137
+ </View>
138
+ ),
139
+ }
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import { IconBox } from './IconBox'
5
+
6
+ function MockIcon({ size, className }: { size?: number; className?: string }) {
7
+ return (
8
+ <span data-testid="icon" data-size={size} className={className}>
9
+ icon
10
+ </span>
11
+ )
12
+ }
13
+
14
+ describe('IconBox', () => {
15
+ it('renders the icon component', () => {
16
+ render(<IconBox icon={MockIcon} />)
17
+ expect(screen.getByTestId('icon')).toBeInTheDocument()
18
+ })
19
+
20
+ it('passes correct default size to icon (md = 20)', () => {
21
+ render(<IconBox icon={MockIcon} />)
22
+ expect(screen.getByTestId('icon')).toHaveAttribute('data-size', '20')
23
+ })
24
+
25
+ it('passes correct size to icon for sm variant', () => {
26
+ render(<IconBox icon={MockIcon} size="sm" />)
27
+ expect(screen.getByTestId('icon')).toHaveAttribute('data-size', '16')
28
+ })
29
+
30
+ it('passes correct size to icon for lg variant', () => {
31
+ render(<IconBox icon={MockIcon} size="lg" />)
32
+ expect(screen.getByTestId('icon')).toHaveAttribute('data-size', '24')
33
+ })
34
+
35
+ it('renders with size sm without crashing', () => {
36
+ const { container } = render(<IconBox icon={MockIcon} size="sm" />)
37
+ expect(container.firstChild).toBeTruthy()
38
+ expect(screen.getByTestId('icon')).toBeInTheDocument()
39
+ })
40
+
41
+ it('renders with size md by default without crashing', () => {
42
+ const { container } = render(<IconBox icon={MockIcon} />)
43
+ expect(container.firstChild).toBeTruthy()
44
+ expect(screen.getByTestId('icon')).toBeInTheDocument()
45
+ })
46
+
47
+ it('renders with size lg without crashing', () => {
48
+ const { container } = render(<IconBox icon={MockIcon} size="lg" />)
49
+ expect(container.firstChild).toBeTruthy()
50
+ expect(screen.getByTestId('icon')).toBeInTheDocument()
51
+ })
52
+
53
+ it('renders with color primary without crashing', () => {
54
+ const { container } = render(<IconBox icon={MockIcon} color="primary" />)
55
+ expect(container.firstChild).toBeTruthy()
56
+ })
57
+
58
+ it('renders with neutral color by default', () => {
59
+ const { container } = render(<IconBox icon={MockIcon} />)
60
+ expect(container.firstChild).toBeTruthy()
61
+ })
62
+
63
+ it('renders with className prop without crashing', () => {
64
+ const { container } = render(<IconBox icon={MockIcon} className="mt-4" />)
65
+ expect(container.firstChild).toBeTruthy()
66
+ })
67
+
68
+ it('passes through testID prop', () => {
69
+ render(<IconBox icon={MockIcon} testID="my-icon-box" />)
70
+ expect(screen.getByTestId('my-icon-box')).toBeInTheDocument()
71
+ })
72
+
73
+ it('supports all color variants without errors', () => {
74
+ const colors = [
75
+ 'primary',
76
+ 'secondary',
77
+ 'success',
78
+ 'error',
79
+ 'warning',
80
+ 'info',
81
+ 'neutral',
82
+ ] as const
83
+ for (const color of colors) {
84
+ const { unmount } = render(<IconBox icon={MockIcon} color={color} />)
85
+ expect(screen.getByTestId('icon')).toBeInTheDocument()
86
+ unmount()
87
+ }
88
+ })
89
+
90
+ it('supports all size variants without errors', () => {
91
+ const sizes = ['sm', 'md', 'lg'] as const
92
+ for (const size of sizes) {
93
+ const { unmount } = render(<IconBox icon={MockIcon} size={size} />)
94
+ expect(screen.getByTestId('icon')).toBeInTheDocument()
95
+ unmount()
96
+ }
97
+ })
98
+
99
+ it('renders with all props combined', () => {
100
+ render(
101
+ <IconBox
102
+ icon={MockIcon}
103
+ color="success"
104
+ size="lg"
105
+ className="mt-2"
106
+ testID="full-icon-box"
107
+ />,
108
+ )
109
+ expect(screen.getByTestId('full-icon-box')).toBeInTheDocument()
110
+ expect(screen.getByTestId('icon')).toHaveAttribute('data-size', '24')
111
+ })
112
+
113
+ describe('accessibility', () => {
114
+ it('has no accessibility violations', async () => {
115
+ const { container } = render(<IconBox icon={MockIcon} color="primary" />)
116
+ const results = await axe(container)
117
+ expect(results).toHaveNoViolations()
118
+ })
119
+ })
120
+ })
@@ -0,0 +1,57 @@
1
+ import React from 'react'
2
+ import { View, type ViewProps } from 'react-native'
3
+ import { cn } from '../../../utils/cn'
4
+
5
+ type IconBoxColor =
6
+ | 'primary'
7
+ | 'secondary'
8
+ | 'success'
9
+ | 'error'
10
+ | 'warning'
11
+ | 'info'
12
+ | 'neutral'
13
+
14
+ type IconBoxSize = 'sm' | 'md' | 'lg'
15
+
16
+ export interface IconBoxProps extends ViewProps {
17
+ icon: React.ComponentType<{ size?: number; className?: string }>
18
+ color?: IconBoxColor
19
+ size?: IconBoxSize
20
+ className?: string
21
+ }
22
+
23
+ const colorClasses: Record<IconBoxColor, { bg: string; icon: string }> = {
24
+ primary: { bg: 'bg-brand-primary/10', icon: 'text-brand-primary' },
25
+ secondary: { bg: 'bg-brand-secondary/10', icon: 'text-brand-secondary' },
26
+ success: { bg: 'bg-status-success/10', icon: 'text-status-success' },
27
+ error: { bg: 'bg-status-error/10', icon: 'text-status-error' },
28
+ warning: { bg: 'bg-status-warning/10', icon: 'text-status-warning' },
29
+ info: { bg: 'bg-status-info/10', icon: 'text-status-info' },
30
+ neutral: { bg: 'bg-surface-elevated', icon: 'text-text-secondary' },
31
+ }
32
+
33
+ const sizeClasses: Record<IconBoxSize, { box: string; iconSize: number }> = {
34
+ sm: { box: 'w-8 h-8 rounded-lg', iconSize: 16 },
35
+ md: { box: 'w-10 h-10 rounded-xl', iconSize: 20 },
36
+ lg: { box: 'w-12 h-12 rounded-xl', iconSize: 24 },
37
+ }
38
+
39
+ export function IconBox({
40
+ icon: Icon,
41
+ color = 'neutral',
42
+ size = 'md',
43
+ className,
44
+ ...props
45
+ }: IconBoxProps) {
46
+ const colors = colorClasses[color]
47
+ const sizes = sizeClasses[size]
48
+
49
+ return (
50
+ <View
51
+ className={cn('items-center justify-center', colors.bg, sizes.box, className)}
52
+ {...props}
53
+ >
54
+ <Icon size={sizes.iconSize} className={colors.icon} />
55
+ </View>
56
+ )
57
+ }
@@ -0,0 +1 @@
1
+ export { IconBox, type IconBoxProps } from './IconBox'
@@ -0,0 +1,36 @@
1
+ // Core UI components
2
+ export * from './alert'
3
+ export * from './button'
4
+ export * from './input'
5
+ export * from './card'
6
+ export * from './badge'
7
+ export * from './spinner'
8
+ export * from './avatar'
9
+ export * from './divider'
10
+ export * from './checkbox'
11
+ export * from './switch'
12
+ export * from './modal'
13
+ export * from './skeleton'
14
+ export * from './tabs'
15
+ export * from './chip'
16
+ export * from './link'
17
+ export * from './tooltip'
18
+ export * from './collapse'
19
+ export * from './breadcrumbs'
20
+ export * from './popover'
21
+ export * from './menu'
22
+ export * from './select'
23
+ export * from './toolbar-button'
24
+ export * from './radio'
25
+ export * from './drawer'
26
+ export * from './progress'
27
+ export * from './toast'
28
+ export * from './autocomplete'
29
+ export * from './help-tip'
30
+ export * from './form-field'
31
+ export * from './stack'
32
+ export * from './section'
33
+ export * from './data-row'
34
+ export * from './icon-box'
35
+ export * from './surface'
36
+ export * from './list-item'
@@ -0,0 +1,192 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { View, Text } from 'react-native'
3
+ import { Input, InputGroup, PasswordInput } from './Input'
4
+
5
+ const meta: Meta<typeof Input> = {
6
+ title: 'Components/Input',
7
+ component: Input,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ variant: {
11
+ control: 'select',
12
+ options: ['outline', 'filled', 'underline'],
13
+ },
14
+ size: {
15
+ control: 'select',
16
+ options: ['sm', 'md', 'lg'],
17
+ },
18
+ multiline: { control: 'boolean' },
19
+ isDisabled: { control: 'boolean' },
20
+ isInvalid: { control: 'boolean' },
21
+ isReadOnly: { control: 'boolean' },
22
+ isRequired: { control: 'boolean' },
23
+ },
24
+ }
25
+
26
+ export default meta
27
+ type Story = StoryObj<typeof Input>
28
+
29
+ export const Default: Story = {
30
+ args: {
31
+ placeholder: 'Enter text...',
32
+ },
33
+ }
34
+
35
+ export const WithLabel: Story = {
36
+ args: {
37
+ label: 'Email',
38
+ placeholder: 'Enter your email',
39
+ },
40
+ }
41
+
42
+ export const WithHelperText: Story = {
43
+ args: {
44
+ label: 'Password',
45
+ placeholder: 'Enter your password',
46
+ helperText: 'Must be at least 8 characters',
47
+ },
48
+ }
49
+
50
+ export const WithError: Story = {
51
+ args: {
52
+ label: 'Email',
53
+ placeholder: 'Enter your email',
54
+ isInvalid: true,
55
+ errorMessage: 'Please enter a valid email address',
56
+ defaultValue: 'invalid-email',
57
+ },
58
+ }
59
+
60
+ export const Required: Story = {
61
+ args: {
62
+ label: 'Username',
63
+ placeholder: 'Choose a username',
64
+ isRequired: true,
65
+ },
66
+ }
67
+
68
+ export const Disabled: Story = {
69
+ args: {
70
+ label: 'Disabled Input',
71
+ placeholder: 'This is disabled',
72
+ isDisabled: true,
73
+ defaultValue: 'Disabled value',
74
+ },
75
+ }
76
+
77
+ export const ReadOnly: Story = {
78
+ args: {
79
+ label: 'Read Only',
80
+ isReadOnly: true,
81
+ defaultValue: 'This value cannot be changed',
82
+ },
83
+ }
84
+
85
+ export const AllVariants: Story = {
86
+ render: () => (
87
+ <View style={{ gap: 16, width: 300 }}>
88
+ <Input variant="outline" label="Outline" placeholder="Outline variant" />
89
+ <Input variant="filled" label="Filled" placeholder="Filled variant" />
90
+ <Input variant="underline" label="Underline" placeholder="Underline variant" />
91
+ </View>
92
+ ),
93
+ }
94
+
95
+ export const AllSizes: Story = {
96
+ render: () => (
97
+ <View style={{ gap: 16, width: 300 }}>
98
+ <Input size="sm" placeholder="Small input" />
99
+ <Input size="md" placeholder="Medium input" />
100
+ <Input size="lg" placeholder="Large input" />
101
+ </View>
102
+ ),
103
+ }
104
+
105
+ export const Multiline: Story = {
106
+ args: {
107
+ label: 'Description',
108
+ placeholder: 'Enter a description...',
109
+ multiline: true,
110
+ numberOfLines: 4,
111
+ helperText: 'Maximum 500 characters',
112
+ },
113
+ }
114
+
115
+ export const MultilineSizes: Story = {
116
+ render: () => (
117
+ <View style={{ gap: 16, width: 400 }}>
118
+ <Input size="sm" multiline label="Small" placeholder="Small textarea" />
119
+ <Input size="md" multiline label="Medium" placeholder="Medium textarea" />
120
+ <Input size="lg" multiline label="Large" placeholder="Large textarea" />
121
+ </View>
122
+ ),
123
+ }
124
+
125
+ export const Password: Story = {
126
+ render: () => (
127
+ <View style={{ gap: 16, width: 300 }}>
128
+ <PasswordInput
129
+ label="Password"
130
+ placeholder="Enter your password"
131
+ helperText="Must be at least 8 characters"
132
+ />
133
+ </View>
134
+ ),
135
+ }
136
+
137
+ export const PasswordWithError: Story = {
138
+ render: () => (
139
+ <View style={{ gap: 16, width: 300 }}>
140
+ <PasswordInput
141
+ label="Password"
142
+ placeholder="Enter your password"
143
+ isInvalid
144
+ errorMessage="Password must be at least 8 characters"
145
+ defaultValue="short"
146
+ />
147
+ </View>
148
+ ),
149
+ }
150
+
151
+ // Simple search icon for demo
152
+ function SearchIcon() {
153
+ return (
154
+ <View className="w-4 h-4 items-center justify-center">
155
+ <View className="w-3 h-3 rounded-full border-2 border-text-secondary" />
156
+ <View className="absolute bottom-0 right-0 w-1.5 h-0.5 bg-text-secondary rotate-45 origin-left" />
157
+ </View>
158
+ )
159
+ }
160
+
161
+ export const WithLeftElement: Story = {
162
+ render: () => (
163
+ <View style={{ gap: 16, width: 300 }}>
164
+ <Input
165
+ placeholder="Search..."
166
+ leftElement={<SearchIcon />}
167
+ />
168
+ </View>
169
+ ),
170
+ }
171
+
172
+ export const FormExample: Story = {
173
+ render: () => (
174
+ <InputGroup>
175
+ <Input label="First Name" placeholder="John" isRequired />
176
+ <Input label="Last Name" placeholder="Doe" isRequired />
177
+ <Input label="Email" placeholder="john@example.com" isRequired />
178
+ <PasswordInput
179
+ label="Password"
180
+ placeholder="••••••••"
181
+ helperText="Must be at least 8 characters"
182
+ isRequired
183
+ />
184
+ <Input
185
+ label="Bio"
186
+ placeholder="Tell us about yourself..."
187
+ multiline
188
+ numberOfLines={3}
189
+ />
190
+ </InputGroup>
191
+ ),
192
+ }
@@ -0,0 +1,86 @@
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 { Input } from './Input'
5
+
6
+ describe('Input', () => {
7
+ it('renders correctly', () => {
8
+ render(<Input placeholder="Enter text" />)
9
+ expect(screen.getByPlaceholderText('Enter text')).toBeInTheDocument()
10
+ })
11
+
12
+ it('displays label when provided', () => {
13
+ render(<Input label="Email" placeholder="Enter email" />)
14
+ expect(screen.getByText('Email')).toBeInTheDocument()
15
+ })
16
+
17
+ it('displays required indicator when isRequired', () => {
18
+ render(<Input label="Email" isRequired />)
19
+ expect(screen.getByText('*')).toBeInTheDocument()
20
+ })
21
+
22
+ it('displays helper text when provided', () => {
23
+ render(<Input helperText="This is helper text" />)
24
+ expect(screen.getByText('This is helper text')).toBeInTheDocument()
25
+ })
26
+
27
+ it('displays error message when isInvalid', () => {
28
+ render(
29
+ <Input
30
+ isInvalid
31
+ errorMessage="Invalid input"
32
+ helperText="This should not show"
33
+ />
34
+ )
35
+ expect(screen.getByText('Invalid input')).toBeInTheDocument()
36
+ expect(screen.queryByText('This should not show')).not.toBeInTheDocument()
37
+ })
38
+
39
+ it('handles text changes', () => {
40
+ const onChangeText = vi.fn()
41
+ render(<Input onChangeText={onChangeText} placeholder="Type here" />)
42
+
43
+ const input = screen.getByPlaceholderText('Type here')
44
+ // react-native-web renders as HTML input, use change event
45
+ fireEvent.change(input, { target: { value: 'Hello' } })
46
+
47
+ expect(onChangeText).toHaveBeenCalled()
48
+ })
49
+
50
+ it('respects isDisabled prop', () => {
51
+ render(<Input isDisabled placeholder="Disabled" />)
52
+ const input = screen.getByPlaceholderText('Disabled')
53
+ // react-native-web renders isDisabled as readonly, not disabled
54
+ expect(input).toHaveAttribute('readonly')
55
+ })
56
+
57
+ it('respects isReadOnly prop', () => {
58
+ render(<Input isReadOnly defaultValue="Read only" />)
59
+ const input = screen.getByDisplayValue('Read only')
60
+ expect(input).toHaveAttribute('readonly')
61
+ })
62
+
63
+ describe('accessibility', () => {
64
+ it('has no accessibility violations', async () => {
65
+ const { container } = render(
66
+ <Input label="Email" placeholder="Enter your email" />
67
+ )
68
+
69
+ const results = await axe(container)
70
+ expect(results).toHaveNoViolations()
71
+ })
72
+
73
+ it('associates label with input', () => {
74
+ render(<Input label="Username" placeholder="Enter username" />)
75
+ const input = screen.getByPlaceholderText('Enter username')
76
+ expect(input).toHaveAccessibleName('Username')
77
+ })
78
+
79
+ it('communicates disabled state', () => {
80
+ render(<Input isDisabled placeholder="Disabled input" />)
81
+ const input = screen.getByPlaceholderText('Disabled input')
82
+ // react-native-web renders isDisabled as readonly
83
+ expect(input).toHaveAttribute('readonly')
84
+ })
85
+ })
86
+ })