@sudobility/components-rn 1.0.21 → 1.0.23
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/index.cjs.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/lib/utils.d.ts +7 -2
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/ui/Alert/Alert.d.ts +8 -0
- package/dist/ui/Alert/Alert.d.ts.map +1 -1
- package/dist/ui/Button/Button.shared.d.ts +18 -3
- package/dist/ui/Button/Button.shared.d.ts.map +1 -1
- package/dist/ui/Card/Card.d.ts +32 -0
- package/dist/ui/Card/Card.d.ts.map +1 -1
- package/dist/ui/{ChainBadge.d.ts → ChainBadge/ChainBadge.d.ts} +1 -0
- package/dist/ui/ChainBadge/ChainBadge.d.ts.map +1 -0
- package/dist/ui/ChainBadge/index.d.ts +2 -0
- package/dist/ui/ChainBadge/index.d.ts.map +1 -0
- package/dist/ui/Spinner/Spinner.d.ts +8 -0
- package/dist/ui/Spinner/Spinner.d.ts.map +1 -1
- package/dist/ui/Toast/Toast.d.ts +11 -0
- package/dist/ui/Toast/Toast.d.ts.map +1 -1
- package/package.json +23 -12
- package/src/__tests__/alert.test.tsx +95 -0
- package/src/__tests__/badge.test.tsx +121 -0
- package/src/__tests__/button.test.tsx +107 -0
- package/src/__tests__/card.test.tsx +149 -0
- package/src/__tests__/dialog.test.tsx +76 -0
- package/src/__tests__/input.test.tsx +80 -0
- package/src/__tests__/modal.test.tsx +125 -0
- package/src/__tests__/sheet.test.tsx +113 -0
- package/src/__tests__/tabs.test.tsx +213 -0
- package/src/__tests__/toast.test.tsx +181 -0
- package/src/__tests__/utils.test.ts +47 -0
- package/src/index.ts +4 -1
- package/src/lib/utils.ts +7 -2
- package/src/ui/Alert/Alert.tsx +8 -0
- package/src/ui/Button/Button.shared.ts +18 -3
- package/src/ui/Card/Card.tsx +32 -0
- package/src/ui/{ChainBadge.tsx → ChainBadge/ChainBadge.tsx} +2 -1
- package/src/ui/ChainBadge/index.ts +1 -0
- package/src/ui/Spinner/Spinner.tsx +8 -0
- package/src/ui/Toast/Toast.tsx +11 -0
- package/dist/ui/ChainBadge.d.ts.map +0 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react-native';
|
|
3
|
+
import { Text } from 'react-native';
|
|
4
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/Tabs';
|
|
5
|
+
|
|
6
|
+
describe('Tabs', () => {
|
|
7
|
+
const renderTabs = (props = {}) =>
|
|
8
|
+
render(
|
|
9
|
+
<Tabs defaultValue='tab1' {...props}>
|
|
10
|
+
<TabsList>
|
|
11
|
+
<TabsTrigger value='tab1'>Tab 1</TabsTrigger>
|
|
12
|
+
<TabsTrigger value='tab2'>Tab 2</TabsTrigger>
|
|
13
|
+
<TabsTrigger value='tab3'>Tab 3</TabsTrigger>
|
|
14
|
+
</TabsList>
|
|
15
|
+
<TabsContent value='tab1'>
|
|
16
|
+
<Text>Content 1</Text>
|
|
17
|
+
</TabsContent>
|
|
18
|
+
<TabsContent value='tab2'>
|
|
19
|
+
<Text>Content 2</Text>
|
|
20
|
+
</TabsContent>
|
|
21
|
+
<TabsContent value='tab3'>
|
|
22
|
+
<Text>Content 3</Text>
|
|
23
|
+
</TabsContent>
|
|
24
|
+
</Tabs>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
it('renders tab triggers', () => {
|
|
28
|
+
renderTabs();
|
|
29
|
+
expect(screen.getByText('Tab 1')).toBeTruthy();
|
|
30
|
+
expect(screen.getByText('Tab 2')).toBeTruthy();
|
|
31
|
+
expect(screen.getByText('Tab 3')).toBeTruthy();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('shows content for default tab', () => {
|
|
35
|
+
renderTabs();
|
|
36
|
+
expect(screen.getByText('Content 1')).toBeTruthy();
|
|
37
|
+
expect(screen.queryByText('Content 2')).toBeNull();
|
|
38
|
+
expect(screen.queryByText('Content 3')).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('switches content when a tab is pressed', () => {
|
|
42
|
+
renderTabs();
|
|
43
|
+
|
|
44
|
+
// Initially tab 1 content is shown
|
|
45
|
+
expect(screen.getByText('Content 1')).toBeTruthy();
|
|
46
|
+
|
|
47
|
+
// Press tab 2
|
|
48
|
+
fireEvent.press(screen.getByText('Tab 2'));
|
|
49
|
+
expect(screen.queryByText('Content 1')).toBeNull();
|
|
50
|
+
expect(screen.getByText('Content 2')).toBeTruthy();
|
|
51
|
+
|
|
52
|
+
// Press tab 3
|
|
53
|
+
fireEvent.press(screen.getByText('Tab 3'));
|
|
54
|
+
expect(screen.queryByText('Content 2')).toBeNull();
|
|
55
|
+
expect(screen.getByText('Content 3')).toBeTruthy();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('calls onValueChange when tab changes', () => {
|
|
59
|
+
const onValueChange = jest.fn();
|
|
60
|
+
renderTabs({ onValueChange });
|
|
61
|
+
|
|
62
|
+
fireEvent.press(screen.getByText('Tab 2'));
|
|
63
|
+
expect(onValueChange).toHaveBeenCalledWith('tab2');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('works in controlled mode', () => {
|
|
67
|
+
const onValueChange = jest.fn();
|
|
68
|
+
const { rerender } = render(
|
|
69
|
+
<Tabs value='tab1' onValueChange={onValueChange}>
|
|
70
|
+
<TabsList>
|
|
71
|
+
<TabsTrigger value='tab1'>Tab 1</TabsTrigger>
|
|
72
|
+
<TabsTrigger value='tab2'>Tab 2</TabsTrigger>
|
|
73
|
+
</TabsList>
|
|
74
|
+
<TabsContent value='tab1'>
|
|
75
|
+
<Text>Content 1</Text>
|
|
76
|
+
</TabsContent>
|
|
77
|
+
<TabsContent value='tab2'>
|
|
78
|
+
<Text>Content 2</Text>
|
|
79
|
+
</TabsContent>
|
|
80
|
+
</Tabs>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(screen.getByText('Content 1')).toBeTruthy();
|
|
84
|
+
|
|
85
|
+
// Press tab 2 - in controlled mode the parent decides
|
|
86
|
+
fireEvent.press(screen.getByText('Tab 2'));
|
|
87
|
+
expect(onValueChange).toHaveBeenCalledWith('tab2');
|
|
88
|
+
|
|
89
|
+
// Content doesn't change until the parent updates the value prop
|
|
90
|
+
expect(screen.getByText('Content 1')).toBeTruthy();
|
|
91
|
+
|
|
92
|
+
// Rerender with updated value
|
|
93
|
+
rerender(
|
|
94
|
+
<Tabs value='tab2' onValueChange={onValueChange}>
|
|
95
|
+
<TabsList>
|
|
96
|
+
<TabsTrigger value='tab1'>Tab 1</TabsTrigger>
|
|
97
|
+
<TabsTrigger value='tab2'>Tab 2</TabsTrigger>
|
|
98
|
+
</TabsList>
|
|
99
|
+
<TabsContent value='tab1'>
|
|
100
|
+
<Text>Content 1</Text>
|
|
101
|
+
</TabsContent>
|
|
102
|
+
<TabsContent value='tab2'>
|
|
103
|
+
<Text>Content 2</Text>
|
|
104
|
+
</TabsContent>
|
|
105
|
+
</Tabs>
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(screen.getByText('Content 2')).toBeTruthy();
|
|
109
|
+
expect(screen.queryByText('Content 1')).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('tab triggers have tab accessibility role', () => {
|
|
113
|
+
renderTabs();
|
|
114
|
+
const tabs = screen.getAllByRole('tab');
|
|
115
|
+
expect(tabs.length).toBe(3);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('selected tab has selected accessibility state', () => {
|
|
119
|
+
renderTabs();
|
|
120
|
+
const tabs = screen.getAllByRole('tab');
|
|
121
|
+
// First tab should be selected
|
|
122
|
+
expect(tabs[0].props.accessibilityState).toEqual({
|
|
123
|
+
selected: true,
|
|
124
|
+
disabled: false,
|
|
125
|
+
});
|
|
126
|
+
// Second tab should not be selected
|
|
127
|
+
expect(tabs[1].props.accessibilityState).toEqual({
|
|
128
|
+
selected: false,
|
|
129
|
+
disabled: false,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('disabled tab has disabled accessibility state', () => {
|
|
134
|
+
render(
|
|
135
|
+
<Tabs defaultValue='tab1'>
|
|
136
|
+
<TabsList>
|
|
137
|
+
<TabsTrigger value='tab1'>Tab 1</TabsTrigger>
|
|
138
|
+
<TabsTrigger value='tab2' disabled>
|
|
139
|
+
Tab 2
|
|
140
|
+
</TabsTrigger>
|
|
141
|
+
</TabsList>
|
|
142
|
+
<TabsContent value='tab1'>
|
|
143
|
+
<Text>Content 1</Text>
|
|
144
|
+
</TabsContent>
|
|
145
|
+
<TabsContent value='tab2'>
|
|
146
|
+
<Text>Content 2</Text>
|
|
147
|
+
</TabsContent>
|
|
148
|
+
</Tabs>
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const tabs = screen.getAllByRole('tab');
|
|
152
|
+
expect(tabs[1].props.accessibilityState).toEqual({
|
|
153
|
+
selected: false,
|
|
154
|
+
disabled: true,
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('disabled tab does not switch content when pressed', () => {
|
|
159
|
+
render(
|
|
160
|
+
<Tabs defaultValue='tab1'>
|
|
161
|
+
<TabsList>
|
|
162
|
+
<TabsTrigger value='tab1'>Tab 1</TabsTrigger>
|
|
163
|
+
<TabsTrigger value='tab2' disabled>
|
|
164
|
+
Tab 2
|
|
165
|
+
</TabsTrigger>
|
|
166
|
+
</TabsList>
|
|
167
|
+
<TabsContent value='tab1'>
|
|
168
|
+
<Text>Content 1</Text>
|
|
169
|
+
</TabsContent>
|
|
170
|
+
<TabsContent value='tab2'>
|
|
171
|
+
<Text>Content 2</Text>
|
|
172
|
+
</TabsContent>
|
|
173
|
+
</Tabs>
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
fireEvent.press(screen.getByText('Tab 2'));
|
|
177
|
+
// Content 1 should still be visible
|
|
178
|
+
expect(screen.getByText('Content 1')).toBeTruthy();
|
|
179
|
+
expect(screen.queryByText('Content 2')).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('TabsTrigger outside Tabs context', () => {
|
|
184
|
+
it('throws when used outside Tabs', () => {
|
|
185
|
+
const consoleSpy = jest
|
|
186
|
+
.spyOn(console, 'error')
|
|
187
|
+
.mockImplementation(() => {});
|
|
188
|
+
|
|
189
|
+
expect(() => render(<TabsTrigger value='tab1'>Tab 1</TabsTrigger>)).toThrow(
|
|
190
|
+
'Tabs components must be used within a Tabs provider'
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
consoleSpy.mockRestore();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('TabsContent outside Tabs context', () => {
|
|
198
|
+
it('throws when used outside Tabs', () => {
|
|
199
|
+
const consoleSpy = jest
|
|
200
|
+
.spyOn(console, 'error')
|
|
201
|
+
.mockImplementation(() => {});
|
|
202
|
+
|
|
203
|
+
expect(() =>
|
|
204
|
+
render(
|
|
205
|
+
<TabsContent value='tab1'>
|
|
206
|
+
<Text>Content</Text>
|
|
207
|
+
</TabsContent>
|
|
208
|
+
)
|
|
209
|
+
).toThrow('Tabs components must be used within a Tabs provider');
|
|
210
|
+
|
|
211
|
+
consoleSpy.mockRestore();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, act } from '@testing-library/react-native';
|
|
3
|
+
import { Text, Pressable } from 'react-native';
|
|
4
|
+
import { Toast, ToastProvider, useToast } from '../ui/Toast';
|
|
5
|
+
import type { ToastMessage } from '../ui/Toast';
|
|
6
|
+
|
|
7
|
+
describe('Toast', () => {
|
|
8
|
+
const baseToast: ToastMessage = {
|
|
9
|
+
id: 'test-1',
|
|
10
|
+
title: 'Test Toast',
|
|
11
|
+
description: 'This is a test toast',
|
|
12
|
+
variant: 'default',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
it('renders title', () => {
|
|
16
|
+
render(<Toast toast={baseToast} onRemove={jest.fn()} />);
|
|
17
|
+
expect(screen.getByText('Test Toast')).toBeTruthy();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders description', () => {
|
|
21
|
+
render(<Toast toast={baseToast} onRemove={jest.fn()} />);
|
|
22
|
+
expect(screen.getByText('This is a test toast')).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('renders close button', () => {
|
|
26
|
+
render(<Toast toast={baseToast} onRemove={jest.fn()} />);
|
|
27
|
+
const closeButton = screen.getByLabelText('Close notification');
|
|
28
|
+
expect(closeButton).toBeTruthy();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('calls onRemove when close button is pressed', () => {
|
|
32
|
+
const onRemove = jest.fn();
|
|
33
|
+
render(<Toast toast={baseToast} onRemove={onRemove} />);
|
|
34
|
+
fireEvent.press(screen.getByLabelText('Close notification'));
|
|
35
|
+
expect(onRemove).toHaveBeenCalledWith('test-1');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders action button when action is provided', () => {
|
|
39
|
+
const onAction = jest.fn();
|
|
40
|
+
const toastWithAction: ToastMessage = {
|
|
41
|
+
...baseToast,
|
|
42
|
+
action: { label: 'Undo', onPress: onAction },
|
|
43
|
+
};
|
|
44
|
+
render(<Toast toast={toastWithAction} onRemove={jest.fn()} />);
|
|
45
|
+
expect(screen.getByText('Undo')).toBeTruthy();
|
|
46
|
+
fireEvent.press(screen.getByText('Undo'));
|
|
47
|
+
expect(onAction).toHaveBeenCalledTimes(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('renders with different variants without crashing', () => {
|
|
51
|
+
const variants = [
|
|
52
|
+
'default',
|
|
53
|
+
'success',
|
|
54
|
+
'error',
|
|
55
|
+
'warning',
|
|
56
|
+
'info',
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
variants.forEach(variant => {
|
|
60
|
+
const toast: ToastMessage = { ...baseToast, variant, id: variant };
|
|
61
|
+
const { unmount } = render(<Toast toast={toast} onRemove={jest.fn()} />);
|
|
62
|
+
expect(screen.getByText('Test Toast')).toBeTruthy();
|
|
63
|
+
unmount();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('renders toast without title', () => {
|
|
68
|
+
const toast: ToastMessage = {
|
|
69
|
+
id: 'no-title',
|
|
70
|
+
description: 'Description only',
|
|
71
|
+
};
|
|
72
|
+
render(<Toast toast={toast} onRemove={jest.fn()} />);
|
|
73
|
+
expect(screen.getByText('Description only')).toBeTruthy();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('renders toast without description', () => {
|
|
77
|
+
const toast: ToastMessage = {
|
|
78
|
+
id: 'no-desc',
|
|
79
|
+
title: 'Title only',
|
|
80
|
+
};
|
|
81
|
+
render(<Toast toast={toast} onRemove={jest.fn()} />);
|
|
82
|
+
expect(screen.getByText('Title only')).toBeTruthy();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('ToastProvider', () => {
|
|
87
|
+
it('renders children', () => {
|
|
88
|
+
render(
|
|
89
|
+
<ToastProvider>
|
|
90
|
+
<Text>App content</Text>
|
|
91
|
+
</ToastProvider>
|
|
92
|
+
);
|
|
93
|
+
expect(screen.getByText('App content')).toBeTruthy();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('useToast', () => {
|
|
98
|
+
it('throws when used outside ToastProvider', () => {
|
|
99
|
+
// Suppress console.error for this test since we expect an error
|
|
100
|
+
const consoleSpy = jest
|
|
101
|
+
.spyOn(console, 'error')
|
|
102
|
+
.mockImplementation(() => {});
|
|
103
|
+
|
|
104
|
+
const BadComponent = () => {
|
|
105
|
+
useToast();
|
|
106
|
+
return null;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
expect(() => render(<BadComponent />)).toThrow(
|
|
110
|
+
'useToast must be used within ToastProvider'
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
consoleSpy.mockRestore();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('addToast adds a toast that renders', () => {
|
|
117
|
+
const TestComponent = () => {
|
|
118
|
+
const { addToast } = useToast();
|
|
119
|
+
return (
|
|
120
|
+
<Pressable
|
|
121
|
+
testID='add-toast'
|
|
122
|
+
onPress={() =>
|
|
123
|
+
addToast({
|
|
124
|
+
title: 'New Toast',
|
|
125
|
+
description: 'Toast was added',
|
|
126
|
+
variant: 'success',
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
>
|
|
130
|
+
<Text>Add Toast</Text>
|
|
131
|
+
</Pressable>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
render(
|
|
136
|
+
<ToastProvider>
|
|
137
|
+
<TestComponent />
|
|
138
|
+
</ToastProvider>
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
act(() => {
|
|
142
|
+
fireEvent.press(screen.getByTestId('add-toast'));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(screen.getByText('New Toast')).toBeTruthy();
|
|
146
|
+
expect(screen.getByText('Toast was added')).toBeTruthy();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('removeToast removes a toast', () => {
|
|
150
|
+
let toastContext: ReturnType<typeof useToast>;
|
|
151
|
+
|
|
152
|
+
const TestComponent = () => {
|
|
153
|
+
toastContext = useToast();
|
|
154
|
+
return <Text>Test</Text>;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
render(
|
|
158
|
+
<ToastProvider>
|
|
159
|
+
<TestComponent />
|
|
160
|
+
</ToastProvider>
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Add a toast first
|
|
164
|
+
act(() => {
|
|
165
|
+
toastContext.addToast({
|
|
166
|
+
title: 'Removable Toast',
|
|
167
|
+
variant: 'info',
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(screen.getByText('Removable Toast')).toBeTruthy();
|
|
172
|
+
|
|
173
|
+
// Now remove it
|
|
174
|
+
act(() => {
|
|
175
|
+
const toastId = toastContext.toasts[0].id;
|
|
176
|
+
toastContext.removeToast(toastId);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(screen.queryByText('Removable Toast')).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cn } from '../lib/utils';
|
|
2
|
+
|
|
3
|
+
describe('cn utility', () => {
|
|
4
|
+
it('merges class names', () => {
|
|
5
|
+
expect(cn('foo', 'bar')).toBe('foo bar');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('handles undefined and null values', () => {
|
|
9
|
+
expect(cn('foo', undefined, 'bar', null)).toBe('foo bar');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('handles empty string', () => {
|
|
13
|
+
expect(cn('')).toBe('');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('handles no arguments', () => {
|
|
17
|
+
expect(cn()).toBe('');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('handles conditional classes via objects', () => {
|
|
21
|
+
expect(cn({ foo: true, bar: false, baz: true })).toBe('foo baz');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('handles arrays', () => {
|
|
25
|
+
expect(cn(['foo', 'bar'])).toBe('foo bar');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('handles mixed arguments', () => {
|
|
29
|
+
expect(cn('base', { active: true, hidden: false }, ['extra'])).toBe(
|
|
30
|
+
'base active extra'
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('handles false and zero values', () => {
|
|
35
|
+
expect(cn('foo', false, 0, 'bar')).toBe('foo bar');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('combines Tailwind-like classes', () => {
|
|
39
|
+
expect(cn('rounded-lg p-4', 'bg-white', 'mt-2')).toBe(
|
|
40
|
+
'rounded-lg p-4 bg-white mt-2'
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('handles nested arrays', () => {
|
|
45
|
+
expect(cn(['a', ['b', 'c']])).toBe('a b c');
|
|
46
|
+
});
|
|
47
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -187,7 +187,10 @@ export * from './ui/ScrollSpy';
|
|
|
187
187
|
export * from './ui/SplitPane';
|
|
188
188
|
export * from './ui/VirtualList';
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
/**
|
|
191
|
+
* @deprecated Import directly from `@sudobility/design` instead.
|
|
192
|
+
* These re-exports will be removed in a future major version.
|
|
193
|
+
*/
|
|
191
194
|
export {
|
|
192
195
|
variants,
|
|
193
196
|
textVariants,
|
package/src/lib/utils.ts
CHANGED
|
@@ -2,8 +2,13 @@ import { type ClassValue, clsx } from 'clsx';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Merges class names for React Native with NativeWind.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* Unlike the web version (`@sudobility/components`), this does NOT use
|
|
7
|
+
* `tailwind-merge` since NativeWind processes Tailwind classes at build
|
|
8
|
+
* time and handles conflict resolution internally.
|
|
9
|
+
*
|
|
10
|
+
* @param inputs - Class values to merge (strings, arrays, objects, etc.)
|
|
11
|
+
* @returns A single merged class name string
|
|
7
12
|
*/
|
|
8
13
|
export function cn(...inputs: ClassValue[]): string {
|
|
9
14
|
return clsx(inputs);
|
package/src/ui/Alert/Alert.tsx
CHANGED
|
@@ -3,6 +3,12 @@ import { View, Text, type ViewProps } from 'react-native';
|
|
|
3
3
|
import { cn } from '../../lib/utils';
|
|
4
4
|
import { variants as v } from '@sudobility/design';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Props for the Alert component.
|
|
8
|
+
*
|
|
9
|
+
* Supports semantic variants (info, success, warning, attention, error)
|
|
10
|
+
* with design system styling, optional custom icons, and compound content.
|
|
11
|
+
*/
|
|
6
12
|
export interface AlertProps extends ViewProps {
|
|
7
13
|
variant?: 'info' | 'success' | 'warning' | 'attention' | 'error';
|
|
8
14
|
title?: string;
|
|
@@ -20,6 +26,7 @@ const defaultIcons: Record<string, string> = {
|
|
|
20
26
|
error: '✕',
|
|
21
27
|
};
|
|
22
28
|
|
|
29
|
+
/** Alert title sub-component with bold font styling. */
|
|
23
30
|
export const AlertTitle: React.FC<{
|
|
24
31
|
children: React.ReactNode;
|
|
25
32
|
className?: string;
|
|
@@ -27,6 +34,7 @@ export const AlertTitle: React.FC<{
|
|
|
27
34
|
<Text className={cn('font-medium mb-1', className)}>{children}</Text>
|
|
28
35
|
);
|
|
29
36
|
|
|
37
|
+
/** Alert description sub-component with smaller text styling. */
|
|
30
38
|
export const AlertDescription: React.FC<{
|
|
31
39
|
children: React.ReactNode;
|
|
32
40
|
className?: string;
|
|
@@ -38,7 +38,10 @@ export interface ButtonBaseProps {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* Map size abbreviations to design system variant keys
|
|
41
|
+
* Map size abbreviations to design system variant keys.
|
|
42
|
+
*
|
|
43
|
+
* @param size - The abbreviated size key ('sm', 'lg', or 'default')
|
|
44
|
+
* @returns The full design system variant key ('small', 'large', or 'default')
|
|
42
45
|
*/
|
|
43
46
|
export const mapSizeToVariantKey = (size: string | undefined): string => {
|
|
44
47
|
if (!size) return 'default';
|
|
@@ -51,7 +54,15 @@ export const mapSizeToVariantKey = (size: string | undefined): string => {
|
|
|
51
54
|
};
|
|
52
55
|
|
|
53
56
|
/**
|
|
54
|
-
* Get button variant class from design system
|
|
57
|
+
* Get button variant class string from the design system.
|
|
58
|
+
*
|
|
59
|
+
* Handles gradient variants, web3 variants (wallet/connect/disconnect),
|
|
60
|
+
* and standard variants with size modifiers.
|
|
61
|
+
*
|
|
62
|
+
* @param variantName - The button variant name (e.g., 'primary', 'gradient', 'wallet')
|
|
63
|
+
* @param sizeName - Optional size abbreviation ('sm' or 'lg')
|
|
64
|
+
* @param v - The design system variants object
|
|
65
|
+
* @returns A Tailwind class string from the design system
|
|
55
66
|
*/
|
|
56
67
|
export const getButtonVariantClass = (
|
|
57
68
|
variantName: string,
|
|
@@ -72,7 +83,11 @@ export const getButtonVariantClass = (
|
|
|
72
83
|
};
|
|
73
84
|
|
|
74
85
|
/**
|
|
75
|
-
* Shared button state logic
|
|
86
|
+
* Shared button state logic for determining disabled and spinner visibility.
|
|
87
|
+
*
|
|
88
|
+
* @param loading - Whether the button is in a loading state
|
|
89
|
+
* @param disabled - Whether the button is explicitly disabled
|
|
90
|
+
* @returns An object with `isDisabled` and `showSpinner` booleans
|
|
76
91
|
*/
|
|
77
92
|
export const useButtonState = (
|
|
78
93
|
loading: boolean | undefined,
|
package/src/ui/Card/Card.tsx
CHANGED
|
@@ -3,6 +3,13 @@ import { View, Text, Pressable, type ViewProps } from 'react-native';
|
|
|
3
3
|
import { cn } from '../../lib/utils';
|
|
4
4
|
import { textVariants, getCardVariantColors } from '@sudobility/design';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Props for the Card component.
|
|
8
|
+
*
|
|
9
|
+
* Supports multiple visual variants (bordered, elevated, info, success, etc.),
|
|
10
|
+
* configurable padding, optional icon display, and a close button for
|
|
11
|
+
* info-type variants.
|
|
12
|
+
*/
|
|
6
13
|
export interface CardProps extends ViewProps {
|
|
7
14
|
variant?:
|
|
8
15
|
| 'default'
|
|
@@ -32,6 +39,22 @@ const paddingStyles = {
|
|
|
32
39
|
lg: 'p-8',
|
|
33
40
|
};
|
|
34
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Card component for React Native.
|
|
44
|
+
*
|
|
45
|
+
* A versatile container with variant-based styling from the design system.
|
|
46
|
+
* Info-type variants (info, success, warning, error) support an optional
|
|
47
|
+
* icon and close button.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* <Card variant="elevated" padding="md">
|
|
52
|
+
* <CardHeader title="Title" description="Subtitle" />
|
|
53
|
+
* <CardContent><Text>Body</Text></CardContent>
|
|
54
|
+
* <CardFooter><Button>Action</Button></CardFooter>
|
|
55
|
+
* </Card>
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
35
58
|
export const Card: React.FC<CardProps> = ({
|
|
36
59
|
variant = 'elevated',
|
|
37
60
|
padding = 'md',
|
|
@@ -85,11 +108,18 @@ export const Card: React.FC<CardProps> = ({
|
|
|
85
108
|
);
|
|
86
109
|
};
|
|
87
110
|
|
|
111
|
+
/** Props for the CardHeader sub-component. */
|
|
88
112
|
interface CardHeaderProps extends ViewProps {
|
|
113
|
+
/** Card title rendered as a heading. */
|
|
89
114
|
title?: string;
|
|
115
|
+
/** Card description rendered below the title. */
|
|
90
116
|
description?: string;
|
|
91
117
|
}
|
|
92
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Card header sub-component with optional title and description.
|
|
121
|
+
* Uses design system text variants for consistent typography.
|
|
122
|
+
*/
|
|
93
123
|
export const CardHeader: React.FC<CardHeaderProps> = ({
|
|
94
124
|
title,
|
|
95
125
|
description,
|
|
@@ -108,6 +138,7 @@ export const CardHeader: React.FC<CardHeaderProps> = ({
|
|
|
108
138
|
);
|
|
109
139
|
};
|
|
110
140
|
|
|
141
|
+
/** Card body content area. */
|
|
111
142
|
export const CardContent: React.FC<ViewProps> = ({
|
|
112
143
|
className,
|
|
113
144
|
children,
|
|
@@ -120,6 +151,7 @@ export const CardContent: React.FC<ViewProps> = ({
|
|
|
120
151
|
);
|
|
121
152
|
};
|
|
122
153
|
|
|
154
|
+
/** Card footer area, rendered as a horizontal row with top padding. */
|
|
123
155
|
export const CardFooter: React.FC<ViewProps> = ({
|
|
124
156
|
className,
|
|
125
157
|
children,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { View, Text, type ViewProps } from 'react-native';
|
|
3
|
-
import { cn } from '
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
4
|
|
|
5
5
|
export type ChainType = 'evm' | 'solana' | 'bitcoin' | 'cosmos';
|
|
6
6
|
|
|
7
|
+
/** Badge displaying a blockchain network identifier with chain-specific color and icon */
|
|
7
8
|
export interface ChainBadgeProps extends ViewProps {
|
|
8
9
|
chainType: ChainType;
|
|
9
10
|
size?: 'sm' | 'md' | 'lg';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ChainBadge, type ChainBadgeProps, type ChainType } from './ChainBadge';
|
|
@@ -2,11 +2,19 @@ import React from 'react';
|
|
|
2
2
|
import { View, ActivityIndicator, Text, type ViewProps } from 'react-native';
|
|
3
3
|
import { cn } from '../../lib/utils';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Props for the Spinner loading indicator component.
|
|
7
|
+
*/
|
|
5
8
|
export interface SpinnerProps extends ViewProps {
|
|
9
|
+
/** Size of the activity indicator. */
|
|
6
10
|
size?: 'small' | 'default' | 'large' | 'extraLarge';
|
|
11
|
+
/** Color variant for the spinner. */
|
|
7
12
|
variant?: 'default' | 'white' | 'success' | 'warning' | 'error';
|
|
13
|
+
/** Accessibility label for screen readers. */
|
|
8
14
|
accessibilityLabel?: string;
|
|
15
|
+
/** Text shown below the spinner when showText is true. */
|
|
9
16
|
loadingText?: string;
|
|
17
|
+
/** Whether to display loading text below the spinner. */
|
|
10
18
|
showText?: boolean;
|
|
11
19
|
}
|
|
12
20
|
|
package/src/ui/Toast/Toast.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import React, {
|
|
|
9
9
|
import { View, Text, Pressable, Animated, SafeAreaView } from 'react-native';
|
|
10
10
|
import { cn } from '../../lib/utils';
|
|
11
11
|
|
|
12
|
+
/** Data structure representing a single toast notification. */
|
|
12
13
|
export interface ToastMessage {
|
|
13
14
|
id: string;
|
|
14
15
|
title?: string;
|
|
@@ -28,6 +29,7 @@ export interface ToastProps {
|
|
|
28
29
|
onRemove: (id: string) => void;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/** Context value provided by ToastProvider for managing toast notifications. */
|
|
31
33
|
export interface ToastContextValue {
|
|
32
34
|
toasts: ToastMessage[];
|
|
33
35
|
addToast: (toast: Omit<ToastMessage, 'id'>) => void;
|
|
@@ -36,6 +38,15 @@ export interface ToastContextValue {
|
|
|
36
38
|
|
|
37
39
|
const ToastContext = createContext<ToastContextValue | undefined>(undefined);
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Hook to access the toast notification system.
|
|
43
|
+
*
|
|
44
|
+
* Must be used within a ToastProvider. Returns functions to add and remove
|
|
45
|
+
* toast notifications, plus the current list of active toasts.
|
|
46
|
+
*
|
|
47
|
+
* @returns The toast context value with addToast, removeToast, and toasts
|
|
48
|
+
* @throws Error if used outside of a ToastProvider
|
|
49
|
+
*/
|
|
39
50
|
export const useToast = () => {
|
|
40
51
|
const context = useContext(ToastContext);
|
|
41
52
|
if (!context) {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ChainBadge.d.ts","sourceRoot":"","sources":["../../src/ui/ChainBadge.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAG1D,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEhE,MAAM,WAAW,eAAgB,SAAQ,SAAS;IAChD,SAAS,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AA0CD,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAgChD,CAAC"}
|