@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.
- package/dist/{chunk-NA3EES23.mjs → chunk-MGW37MPO.mjs} +26 -4
- package/dist/chunk-MGW37MPO.mjs.map +1 -0
- package/dist/index.d.mts +115 -2
- package/dist/index.d.ts +115 -2
- package/dist/index.js +262 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +225 -3
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +124 -109
- package/dist/theme/index.d.ts +124 -109
- package/dist/theme/index.js +24 -1
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +1 -1
- package/package.json +12 -5
- package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
- package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
- package/src/components/custom/DateTime/DateTime.tsx +235 -0
- package/src/components/custom/DateTime/index.ts +2 -0
- package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
- package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
- package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
- package/src/components/custom/EmptyState/index.ts +2 -0
- package/src/components/custom/Metric/Metric.stories.tsx +133 -0
- package/src/components/custom/Metric/Metric.test.tsx +120 -0
- package/src/components/custom/Metric/Metric.tsx +92 -0
- package/src/components/custom/Metric/index.ts +2 -0
- package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
- package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
- package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
- package/src/components/custom/Sidebar/index.ts +18 -0
- package/src/components/custom/Table/Table.stories.tsx +481 -0
- package/src/components/custom/Table/Table.test.tsx +531 -0
- package/src/components/custom/Table/Table.tsx +696 -0
- package/src/components/custom/Table/index.ts +22 -0
- package/src/components/custom/Typography/Typography.stories.tsx +153 -0
- package/src/components/custom/Typography/Typography.test.tsx +107 -0
- package/src/components/custom/Typography/Typography.tsx +218 -0
- package/src/components/custom/Typography/index.ts +19 -0
- package/src/components/custom/index.ts +8 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
- package/src/components/custom/stepper/Stepper.test.tsx +230 -0
- package/src/components/custom/stepper/Stepper.tsx +250 -0
- package/src/components/custom/stepper/index.ts +10 -0
- package/src/components/index.ts +5 -0
- package/src/components/ui/alert/Alert.stories.tsx +154 -0
- package/src/components/ui/alert/Alert.test.tsx +202 -0
- package/src/components/ui/alert/Alert.tsx +170 -0
- package/src/components/ui/alert/index.ts +8 -0
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
- package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
- package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
- package/src/components/ui/autocomplete/index.ts +2 -0
- package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
- package/src/components/ui/avatar/Avatar.test.tsx +135 -0
- package/src/components/ui/avatar/Avatar.tsx +168 -0
- package/src/components/ui/avatar/index.ts +2 -0
- package/src/components/ui/badge/Badge.stories.tsx +100 -0
- package/src/components/ui/badge/Badge.test.tsx +86 -0
- package/src/components/ui/badge/Badge.tsx +105 -0
- package/src/components/ui/badge/index.ts +2 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
- package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
- package/src/components/ui/breadcrumbs/index.ts +2 -0
- package/src/components/ui/button/Button.stories.tsx +216 -0
- package/src/components/ui/button/Button.test.tsx +132 -0
- package/src/components/ui/button/Button.tsx +242 -0
- package/src/components/ui/button/index.ts +10 -0
- package/src/components/ui/card/Card.stories.tsx +443 -0
- package/src/components/ui/card/Card.test.tsx +265 -0
- package/src/components/ui/card/Card.tsx +380 -0
- package/src/components/ui/card/index.ts +20 -0
- package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
- package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
- package/src/components/ui/checkbox/Checkbox.tsx +163 -0
- package/src/components/ui/checkbox/index.ts +2 -0
- package/src/components/ui/chip/Chip.stories.tsx +112 -0
- package/src/components/ui/chip/Chip.test.tsx +119 -0
- package/src/components/ui/chip/Chip.tsx +151 -0
- package/src/components/ui/chip/index.ts +2 -0
- package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
- package/src/components/ui/collapse/Collapse.test.tsx +298 -0
- package/src/components/ui/collapse/Collapse.tsx +275 -0
- package/src/components/ui/collapse/index.ts +18 -0
- package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
- package/src/components/ui/data-row/DataRow.test.tsx +78 -0
- package/src/components/ui/data-row/DataRow.tsx +29 -0
- package/src/components/ui/data-row/index.ts +2 -0
- package/src/components/ui/divider/Divider.stories.tsx +71 -0
- package/src/components/ui/divider/Divider.test.tsx +55 -0
- package/src/components/ui/divider/Divider.tsx +37 -0
- package/src/components/ui/divider/index.ts +2 -0
- package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
- package/src/components/ui/drawer/Drawer.test.tsx +142 -0
- package/src/components/ui/drawer/Drawer.tsx +228 -0
- package/src/components/ui/drawer/index.ts +9 -0
- package/src/components/ui/form-field/FormField.stories.tsx +260 -0
- package/src/components/ui/form-field/FormField.test.tsx +280 -0
- package/src/components/ui/form-field/FormField.tsx +308 -0
- package/src/components/ui/form-field/index.ts +2 -0
- package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
- package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
- package/src/components/ui/help-tip/HelpTip.tsx +229 -0
- package/src/components/ui/help-tip/index.ts +2 -0
- package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
- package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
- package/src/components/ui/icon-box/IconBox.tsx +57 -0
- package/src/components/ui/icon-box/index.ts +1 -0
- package/src/components/ui/index.ts +36 -0
- package/src/components/ui/input/Input.stories.tsx +192 -0
- package/src/components/ui/input/Input.test.tsx +86 -0
- package/src/components/ui/input/Input.tsx +273 -0
- package/src/components/ui/input/index.ts +2 -0
- package/src/components/ui/link/Link.stories.tsx +78 -0
- package/src/components/ui/link/Link.test.tsx +94 -0
- package/src/components/ui/link/Link.tsx +86 -0
- package/src/components/ui/link/index.ts +2 -0
- package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
- package/src/components/ui/list-item/ListItem.test.tsx +128 -0
- package/src/components/ui/list-item/ListItem.tsx +89 -0
- package/src/components/ui/list-item/index.ts +14 -0
- package/src/components/ui/menu/Menu.stories.tsx +180 -0
- package/src/components/ui/menu/Menu.test.tsx +353 -0
- package/src/components/ui/menu/Menu.tsx +222 -0
- package/src/components/ui/menu/index.ts +2 -0
- package/src/components/ui/modal/Modal.stories.tsx +350 -0
- package/src/components/ui/modal/Modal.test.tsx +159 -0
- package/src/components/ui/modal/Modal.tsx +254 -0
- package/src/components/ui/modal/index.ts +19 -0
- package/src/components/ui/popover/Popover.stories.tsx +190 -0
- package/src/components/ui/popover/Popover.test.tsx +277 -0
- package/src/components/ui/popover/Popover.tsx +162 -0
- package/src/components/ui/popover/index.ts +2 -0
- package/src/components/ui/progress/Progress.stories.tsx +190 -0
- package/src/components/ui/progress/Progress.test.tsx +181 -0
- package/src/components/ui/progress/Progress.tsx +320 -0
- package/src/components/ui/progress/index.ts +9 -0
- package/src/components/ui/radio/Radio.stories.tsx +199 -0
- package/src/components/ui/radio/Radio.test.tsx +166 -0
- package/src/components/ui/radio/Radio.tsx +200 -0
- package/src/components/ui/radio/index.ts +2 -0
- package/src/components/ui/section/Section.stories.tsx +111 -0
- package/src/components/ui/section/Section.test.tsx +121 -0
- package/src/components/ui/section/Section.tsx +52 -0
- package/src/components/ui/section/index.ts +6 -0
- package/src/components/ui/select/Select.stories.tsx +286 -0
- package/src/components/ui/select/Select.test.tsx +180 -0
- package/src/components/ui/select/Select.tsx +236 -0
- package/src/components/ui/select/index.ts +2 -0
- package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
- package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
- package/src/components/ui/skeleton/Skeleton.tsx +229 -0
- package/src/components/ui/skeleton/index.ts +16 -0
- package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
- package/src/components/ui/spinner/Spinner.test.tsx +67 -0
- package/src/components/ui/spinner/Spinner.tsx +72 -0
- package/src/components/ui/spinner/index.ts +2 -0
- package/src/components/ui/stack/Stack.stories.tsx +127 -0
- package/src/components/ui/stack/Stack.test.tsx +184 -0
- package/src/components/ui/stack/Stack.tsx +76 -0
- package/src/components/ui/stack/index.ts +2 -0
- package/src/components/ui/surface/Surface.stories.tsx +91 -0
- package/src/components/ui/surface/Surface.test.tsx +63 -0
- package/src/components/ui/surface/Surface.tsx +51 -0
- package/src/components/ui/surface/index.ts +1 -0
- package/src/components/ui/switch/Switch.stories.tsx +111 -0
- package/src/components/ui/switch/Switch.test.tsx +112 -0
- package/src/components/ui/switch/Switch.tsx +103 -0
- package/src/components/ui/switch/index.ts +2 -0
- package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
- package/src/components/ui/tabs/Tabs.test.tsx +192 -0
- package/src/components/ui/tabs/Tabs.tsx +253 -0
- package/src/components/ui/tabs/index.ts +16 -0
- package/src/components/ui/toast/Toast.stories.tsx +255 -0
- package/src/components/ui/toast/Toast.test.tsx +207 -0
- package/src/components/ui/toast/Toast.tsx +327 -0
- package/src/components/ui/toast/index.ts +8 -0
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
- package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
- package/src/components/ui/toolbar-button/index.ts +7 -0
- package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
- package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
- package/src/components/ui/tooltip/Tooltip.tsx +143 -0
- package/src/components/ui/tooltip/index.ts +2 -0
- package/src/index.ts +10 -0
- package/src/test/setup.ts +19 -0
- package/src/theme/Colors.stories.tsx +258 -0
- package/src/theme/config.ts +193 -0
- package/src/theme/elevation.stories.tsx +346 -0
- package/src/theme/elevation.test.ts +27 -0
- package/src/theme/elevation.ts +567 -0
- package/src/theme/index.ts +39 -0
- package/src/theme/shadows.stories.tsx +523 -0
- package/src/theme/shadows.ts +439 -0
- package/src/theme/tokens/index.ts +2 -0
- package/src/theme/tokens/primitives.ts +354 -0
- package/src/theme/tokens/semantic.ts +381 -0
- package/src/types/jest-axe.d.ts +16 -0
- package/src/types/nativewind.d.ts +54 -0
- package/src/utils/cn.ts +14 -0
- package/src/utils/colors.ts +140 -0
- package/src/utils/form.ts +188 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/useTheme.ts +72 -0
- package/dist/chunk-NA3EES23.mjs.map +0 -1
|
@@ -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
|
+
})
|