@astacinco/rn-primitives 0.1.0 → 0.2.0

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 (48) hide show
  1. package/README.md +195 -0
  2. package/__tests__/Tabs.test.tsx +194 -0
  3. package/__tests__/Tag.test.tsx +123 -0
  4. package/__tests__/Timer.test.tsx +208 -0
  5. package/package.json +10 -6
  6. package/src/AppFooter/AppFooter.tsx +113 -0
  7. package/src/AppFooter/index.ts +2 -0
  8. package/src/AppFooter/types.ts +39 -0
  9. package/src/AppHeader/AppHeader.tsx +165 -0
  10. package/src/AppHeader/index.ts +2 -0
  11. package/src/AppHeader/types.ts +82 -0
  12. package/src/Avatar/Avatar.tsx +111 -0
  13. package/src/Avatar/index.ts +2 -0
  14. package/src/Avatar/types.ts +63 -0
  15. package/src/Badge/Badge.tsx +150 -0
  16. package/src/Badge/index.ts +2 -0
  17. package/src/Badge/types.ts +93 -0
  18. package/src/Button/Button.tsx +34 -20
  19. package/src/Button/types.ts +1 -1
  20. package/src/FloatingTierBadge/FloatingTierBadge.tsx +100 -0
  21. package/src/FloatingTierBadge/index.ts +2 -0
  22. package/src/FloatingTierBadge/types.ts +29 -0
  23. package/src/Input/Input.tsx +8 -23
  24. package/src/MarkdownViewer/MarkdownViewer.tsx +185 -0
  25. package/src/MarkdownViewer/index.ts +2 -0
  26. package/src/MarkdownViewer/types.ts +18 -0
  27. package/src/Modal/Modal.tsx +136 -0
  28. package/src/Modal/index.ts +2 -0
  29. package/src/Modal/types.ts +68 -0
  30. package/src/ProBadge/ProBadge.tsx +59 -0
  31. package/src/ProBadge/index.ts +2 -0
  32. package/src/ProBadge/types.ts +13 -0
  33. package/src/ProLockOverlay/ProLockOverlay.tsx +106 -0
  34. package/src/ProLockOverlay/index.ts +2 -0
  35. package/src/ProLockOverlay/types.ts +22 -0
  36. package/src/Switch/Switch.tsx +120 -0
  37. package/src/Switch/index.ts +2 -0
  38. package/src/Switch/types.ts +58 -0
  39. package/src/Tabs/Tabs.tsx +137 -0
  40. package/src/Tabs/index.ts +2 -0
  41. package/src/Tabs/types.ts +66 -0
  42. package/src/Tag/Tag.tsx +100 -0
  43. package/src/Tag/index.ts +2 -0
  44. package/src/Tag/types.ts +42 -0
  45. package/src/Timer/Timer.tsx +170 -0
  46. package/src/Timer/index.ts +2 -0
  47. package/src/Timer/types.ts +69 -0
  48. package/src/index.ts +52 -0
package/README.md CHANGED
@@ -172,6 +172,201 @@ Themed horizontal divider.
172
172
  - `variant` - `'thin' | 'thick'` (default: `'thin'`)
173
173
  - `color` - Override divider color
174
174
 
175
+ ### Switch
176
+
177
+ Themed toggle switch.
178
+
179
+ ```typescript
180
+ <Switch
181
+ value={isEnabled}
182
+ onValueChange={setIsEnabled}
183
+ />
184
+
185
+ <Switch
186
+ value={isEnabled}
187
+ onValueChange={setIsEnabled}
188
+ label="Enable notifications"
189
+ labelPosition="right"
190
+ size="lg"
191
+ />
192
+ ```
193
+
194
+ **Props:**
195
+ - `value` - Current switch state (required)
196
+ - `onValueChange` - Callback when toggled (required)
197
+ - `label` - Optional label text
198
+ - `labelPosition` - `'left' | 'right'` (default: `'right'`)
199
+ - `size` - `'sm' | 'md' | 'lg'` (default: `'md'`)
200
+ - `disabled` - Disable the switch
201
+ - `activeColor` - Override active track color
202
+ - `inactiveColor` - Override inactive track color
203
+
204
+ ### Avatar
205
+
206
+ User avatar with image or fallback initials.
207
+
208
+ ```typescript
209
+ // With image
210
+ <Avatar
211
+ source={{ uri: 'https://example.com/photo.jpg' }}
212
+ size="md"
213
+ />
214
+
215
+ // With fallback initials
216
+ <Avatar
217
+ fallback="John Doe"
218
+ size="lg"
219
+ />
220
+ ```
221
+
222
+ **Props:**
223
+ - `source` - Image source (same as RN Image)
224
+ - `fallback` - Name to generate initials from
225
+ - `size` - `'xs' | 'sm' | 'md' | 'lg' | 'xl'` (default: `'md'`)
226
+ - `rounded` - Circular shape (default: `true`)
227
+ - `borderWidth` - Border width in pixels
228
+ - `customSize` - Override size with exact pixels
229
+
230
+ ### Badge
231
+
232
+ Notification badge, positioned on children or standalone.
233
+
234
+ ```typescript
235
+ // Count badge on avatar
236
+ <Badge count={5} position="top-right">
237
+ <Avatar source={...} />
238
+ </Badge>
239
+
240
+ // Dot badge
241
+ <Badge dot variant="error">
242
+ <Icon name="bell" />
243
+ </Badge>
244
+
245
+ // Standalone badge
246
+ <Badge count={99} maxCount={99} standalone />
247
+ ```
248
+
249
+ **Props:**
250
+ - `count` - Number to display
251
+ - `dot` - Show as dot instead of count
252
+ - `variant` - `'default' | 'primary' | 'error' | 'success' | 'warning'`
253
+ - `position` - `'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'`
254
+ - `size` - `'sm' | 'md' | 'lg'`
255
+ - `maxCount` - Max number before showing "99+"
256
+ - `showZero` - Show badge when count is 0
257
+ - `standalone` - Render without positioning on children
258
+ - `offset` - `[x, y]` fine-tune position
259
+
260
+ ### Tag
261
+
262
+ Inline label for categories, status, or attributes.
263
+
264
+ ```typescript
265
+ // Basic usage
266
+ <Tag label="New" />
267
+
268
+ // With color
269
+ <Tag label="Success" color="success" />
270
+ <Tag label="Warning" color="warning" />
271
+ <Tag label="Error" color="error" />
272
+
273
+ // Filled variant
274
+ <Tag label="Active" color="primary" variant="filled" />
275
+
276
+ // Sizes
277
+ <Tag label="Small" size="sm" />
278
+ <Tag label="Large" size="lg" />
279
+ ```
280
+
281
+ **Props:**
282
+ - `label` - Tag text (required)
283
+ - `color` - `'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info'`
284
+ - `variant` - `'outlined' | 'filled'` (default: `'outlined'`)
285
+ - `size` - `'sm' | 'md' | 'lg'` (default: `'md'`)
286
+
287
+ ### Timer
288
+
289
+ Countdown timer with controls.
290
+
291
+ ```typescript
292
+ // Basic timer
293
+ <Timer durationMinutes={5} />
294
+
295
+ // With callbacks
296
+ <Timer
297
+ durationMinutes={90}
298
+ onStart={() => console.log('Started')}
299
+ onComplete={() => console.log('Done!')}
300
+ onPause={() => console.log('Paused')}
301
+ showProgress
302
+ showControls
303
+ />
304
+
305
+ // Auto-start timer
306
+ <Timer durationMinutes={60} autoStart />
307
+ ```
308
+
309
+ **Props:**
310
+ - `durationMinutes` - Timer duration in minutes (required)
311
+ - `autoStart` - Start automatically (default: `false`)
312
+ - `showControls` - Show start/pause/reset buttons (default: `true`)
313
+ - `showProgress` - Show progress bar (default: `true`)
314
+ - `lowTimeThreshold` - Seconds remaining for low-time warning (default: `300`)
315
+ - `onStart` - Callback when timer starts
316
+ - `onPause` - Callback when timer pauses
317
+ - `onReset` - Callback when timer resets
318
+ - `onComplete` - Callback when timer reaches zero
319
+ - `onTick` - Callback each second with remaining time
320
+
321
+ ### Tabs
322
+
323
+ Horizontal tab selector.
324
+
325
+ ```typescript
326
+ const options = [
327
+ { value: 'all', label: 'All' },
328
+ { value: 'active', label: 'Active' },
329
+ { value: 'completed', label: 'Completed' },
330
+ ];
331
+
332
+ <Tabs
333
+ options={options}
334
+ selected={selectedTab}
335
+ onSelect={setSelectedTab}
336
+ />
337
+
338
+ // Variants
339
+ <Tabs options={options} selected={tab} onSelect={setTab} variant="pills" />
340
+ <Tabs options={options} selected={tab} onSelect={setTab} variant="outlined" />
341
+ <Tabs options={options} selected={tab} onSelect={setTab} variant="filled" />
342
+
343
+ // Sizes
344
+ <Tabs options={options} selected={tab} onSelect={setTab} size="sm" />
345
+ <Tabs options={options} selected={tab} onSelect={setTab} size="lg" />
346
+ ```
347
+
348
+ **Props:**
349
+ - `options` - Array of `{ value: T, label: string }` (required)
350
+ - `selected` - Currently selected value (required)
351
+ - `onSelect` - Callback when tab is selected (required)
352
+ - `variant` - `'pills' | 'outlined' | 'filled'` (default: `'pills'`)
353
+ - `size` - `'sm' | 'md' | 'lg'` (default: `'md'`)
354
+ - `scrollable` - Enable horizontal scrolling (default: `true`)
355
+
356
+ ### MarkdownViewer
357
+
358
+ Render markdown content with theme styling.
359
+
360
+ ```typescript
361
+ <MarkdownViewer content="# Hello\n\nThis is **bold** text." />
362
+ ```
363
+
364
+ **Props:**
365
+ - `content` - Markdown string to render (required)
366
+ - `style` - Override container style
367
+
368
+ **Note:** Requires `react-native-markdown-display` as an optional peer dependency. Falls back to plain text if not installed.
369
+
175
370
  ## Theme Integration
176
371
 
177
372
  All components use the `useTheme()` hook from `@astacinco/rn-theming`. They automatically:
@@ -0,0 +1,194 @@
1
+ import React from 'react';
2
+ import { fireEvent } from '@testing-library/react-native';
3
+ import { renderWithTheme, createThemeSnapshot } from '@astacinco/rn-testing';
4
+ import { Tabs, type TabOption } from '../src/Tabs';
5
+
6
+ const mockOptions: TabOption<string>[] = [
7
+ { value: 'tab1', label: 'Tab 1' },
8
+ { value: 'tab2', label: 'Tab 2' },
9
+ { value: 'tab3', label: 'Tab 3' },
10
+ ];
11
+
12
+ describe('Tabs', () => {
13
+ const mockOnSelect = jest.fn();
14
+
15
+ beforeEach(() => {
16
+ mockOnSelect.mockClear();
17
+ });
18
+
19
+ // Snapshot tests for both themes
20
+ createThemeSnapshot(
21
+ <Tabs
22
+ options={mockOptions}
23
+ selected="tab1"
24
+ onSelect={() => {}}
25
+ testID="tabs"
26
+ />
27
+ );
28
+
29
+ describe('rendering', () => {
30
+ it('renders_all_tabs', () => {
31
+ const { getByText } = renderWithTheme(
32
+ <Tabs
33
+ options={mockOptions}
34
+ selected="tab1"
35
+ onSelect={mockOnSelect}
36
+ testID="tabs"
37
+ />
38
+ );
39
+
40
+ expect(getByText('Tab 1')).toBeTruthy();
41
+ expect(getByText('Tab 2')).toBeTruthy();
42
+ expect(getByText('Tab 3')).toBeTruthy();
43
+ });
44
+
45
+ it('highlights_selected_tab', () => {
46
+ const { getByTestId } = renderWithTheme(
47
+ <Tabs
48
+ options={mockOptions}
49
+ selected="tab2"
50
+ onSelect={mockOnSelect}
51
+ testID="tabs"
52
+ />
53
+ );
54
+
55
+ expect(getByTestId('tabs')).toBeTruthy();
56
+ });
57
+ });
58
+
59
+ describe('variants', () => {
60
+ it('renders_pills_variant_byDefault', () => {
61
+ const { getByTestId } = renderWithTheme(
62
+ <Tabs
63
+ options={mockOptions}
64
+ selected="tab1"
65
+ onSelect={mockOnSelect}
66
+ testID="tabs"
67
+ />
68
+ );
69
+ expect(getByTestId('tabs')).toBeTruthy();
70
+ });
71
+
72
+ it('renders_outlined_variant', () => {
73
+ const { getByTestId } = renderWithTheme(
74
+ <Tabs
75
+ options={mockOptions}
76
+ selected="tab1"
77
+ onSelect={mockOnSelect}
78
+ variant="outlined"
79
+ testID="tabs"
80
+ />
81
+ );
82
+ expect(getByTestId('tabs')).toBeTruthy();
83
+ });
84
+
85
+ it('renders_filled_variant', () => {
86
+ const { getByTestId } = renderWithTheme(
87
+ <Tabs
88
+ options={mockOptions}
89
+ selected="tab1"
90
+ onSelect={mockOnSelect}
91
+ variant="filled"
92
+ testID="tabs"
93
+ />
94
+ );
95
+ expect(getByTestId('tabs')).toBeTruthy();
96
+ });
97
+ });
98
+
99
+ describe('sizes', () => {
100
+ it('renders_sm_size', () => {
101
+ const { getByTestId } = renderWithTheme(
102
+ <Tabs
103
+ options={mockOptions}
104
+ selected="tab1"
105
+ onSelect={mockOnSelect}
106
+ size="sm"
107
+ testID="tabs"
108
+ />
109
+ );
110
+ expect(getByTestId('tabs')).toBeTruthy();
111
+ });
112
+
113
+ it('renders_md_size_byDefault', () => {
114
+ const { getByTestId } = renderWithTheme(
115
+ <Tabs
116
+ options={mockOptions}
117
+ selected="tab1"
118
+ onSelect={mockOnSelect}
119
+ testID="tabs"
120
+ />
121
+ );
122
+ expect(getByTestId('tabs')).toBeTruthy();
123
+ });
124
+
125
+ it('renders_lg_size', () => {
126
+ const { getByTestId } = renderWithTheme(
127
+ <Tabs
128
+ options={mockOptions}
129
+ selected="tab1"
130
+ onSelect={mockOnSelect}
131
+ size="lg"
132
+ testID="tabs"
133
+ />
134
+ );
135
+ expect(getByTestId('tabs')).toBeTruthy();
136
+ });
137
+ });
138
+
139
+ describe('interactions', () => {
140
+ it('calls_onSelect_when_tab_pressed', () => {
141
+ const { getByText } = renderWithTheme(
142
+ <Tabs
143
+ options={mockOptions}
144
+ selected="tab1"
145
+ onSelect={mockOnSelect}
146
+ testID="tabs"
147
+ />
148
+ );
149
+
150
+ fireEvent.press(getByText('Tab 2'));
151
+ expect(mockOnSelect).toHaveBeenCalledWith('tab2');
152
+ });
153
+
154
+ it('calls_onSelect_with_correct_value', () => {
155
+ const { getByText } = renderWithTheme(
156
+ <Tabs
157
+ options={mockOptions}
158
+ selected="tab1"
159
+ onSelect={mockOnSelect}
160
+ testID="tabs"
161
+ />
162
+ );
163
+
164
+ fireEvent.press(getByText('Tab 3'));
165
+ expect(mockOnSelect).toHaveBeenCalledWith('tab3');
166
+ });
167
+ });
168
+
169
+ describe('theming', () => {
170
+ it('uses_different_colors_inDarkMode', () => {
171
+ const lightResult = renderWithTheme(
172
+ <Tabs
173
+ options={mockOptions}
174
+ selected="tab1"
175
+ onSelect={mockOnSelect}
176
+ testID="tabs"
177
+ />,
178
+ 'light'
179
+ );
180
+ const darkResult = renderWithTheme(
181
+ <Tabs
182
+ options={mockOptions}
183
+ selected="tab1"
184
+ onSelect={mockOnSelect}
185
+ testID="tabs"
186
+ />,
187
+ 'dark'
188
+ );
189
+
190
+ expect(lightResult.getByTestId('tabs')).toBeTruthy();
191
+ expect(darkResult.getByTestId('tabs')).toBeTruthy();
192
+ });
193
+ });
194
+ });
@@ -0,0 +1,123 @@
1
+ import React from 'react';
2
+ import { renderWithTheme, createThemeSnapshot } from '@astacinco/rn-testing';
3
+ import { Tag } from '../src/Tag';
4
+
5
+ describe('Tag', () => {
6
+ // Snapshot tests for both themes
7
+ createThemeSnapshot(<Tag label="Default Tag" testID="tag" />);
8
+
9
+ describe('colors', () => {
10
+ it('renders_default_color', () => {
11
+ const { getByTestId } = renderWithTheme(
12
+ <Tag label="Default" color="default" testID="tag" />
13
+ );
14
+ expect(getByTestId('tag')).toBeTruthy();
15
+ });
16
+
17
+ it('renders_primary_color', () => {
18
+ const { getByTestId } = renderWithTheme(
19
+ <Tag label="Primary" color="primary" testID="tag" />
20
+ );
21
+ expect(getByTestId('tag')).toBeTruthy();
22
+ });
23
+
24
+ it('renders_success_color', () => {
25
+ const { getByTestId } = renderWithTheme(
26
+ <Tag label="Success" color="success" testID="tag" />
27
+ );
28
+ expect(getByTestId('tag')).toBeTruthy();
29
+ });
30
+
31
+ it('renders_warning_color', () => {
32
+ const { getByTestId } = renderWithTheme(
33
+ <Tag label="Warning" color="warning" testID="tag" />
34
+ );
35
+ expect(getByTestId('tag')).toBeTruthy();
36
+ });
37
+
38
+ it('renders_error_color', () => {
39
+ const { getByTestId } = renderWithTheme(
40
+ <Tag label="Error" color="error" testID="tag" />
41
+ );
42
+ expect(getByTestId('tag')).toBeTruthy();
43
+ });
44
+
45
+ it('renders_info_color', () => {
46
+ const { getByTestId } = renderWithTheme(
47
+ <Tag label="Info" color="info" testID="tag" />
48
+ );
49
+ expect(getByTestId('tag')).toBeTruthy();
50
+ });
51
+
52
+ it('renders_secondary_color', () => {
53
+ const { getByTestId } = renderWithTheme(
54
+ <Tag label="Secondary" color="secondary" testID="tag" />
55
+ );
56
+ expect(getByTestId('tag')).toBeTruthy();
57
+ });
58
+ });
59
+
60
+ describe('variants', () => {
61
+ it('renders_outlined_variant_byDefault', () => {
62
+ const { getByTestId } = renderWithTheme(
63
+ <Tag label="Outlined" testID="tag" />
64
+ );
65
+ expect(getByTestId('tag')).toBeTruthy();
66
+ });
67
+
68
+ it('renders_filled_variant', () => {
69
+ const { getByTestId } = renderWithTheme(
70
+ <Tag label="Filled" variant="filled" testID="tag" />
71
+ );
72
+ expect(getByTestId('tag')).toBeTruthy();
73
+ });
74
+ });
75
+
76
+ describe('sizes', () => {
77
+ it('renders_sm_size', () => {
78
+ const { getByTestId } = renderWithTheme(
79
+ <Tag label="Small" size="sm" testID="tag" />
80
+ );
81
+ expect(getByTestId('tag')).toBeTruthy();
82
+ });
83
+
84
+ it('renders_md_size_byDefault', () => {
85
+ const { getByTestId } = renderWithTheme(
86
+ <Tag label="Medium" testID="tag" />
87
+ );
88
+ expect(getByTestId('tag')).toBeTruthy();
89
+ });
90
+
91
+ it('renders_lg_size', () => {
92
+ const { getByTestId } = renderWithTheme(
93
+ <Tag label="Large" size="lg" testID="tag" />
94
+ );
95
+ expect(getByTestId('tag')).toBeTruthy();
96
+ });
97
+ });
98
+
99
+ describe('theming', () => {
100
+ it('uses_different_colors_inDarkMode', () => {
101
+ const lightResult = renderWithTheme(
102
+ <Tag label="Test" testID="tag" />,
103
+ 'light'
104
+ );
105
+ const darkResult = renderWithTheme(
106
+ <Tag label="Test" testID="tag" />,
107
+ 'dark'
108
+ );
109
+
110
+ expect(lightResult.getByTestId('tag')).toBeTruthy();
111
+ expect(darkResult.getByTestId('tag')).toBeTruthy();
112
+ });
113
+ });
114
+
115
+ describe('label', () => {
116
+ it('renders_label_text', () => {
117
+ const { getByText } = renderWithTheme(
118
+ <Tag label="My Tag Label" testID="tag" />
119
+ );
120
+ expect(getByText('My Tag Label')).toBeTruthy();
121
+ });
122
+ });
123
+ });