@boostdev/design-system-components 1.2.3 → 1.2.4

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 (191) hide show
  1. package/README.md +29 -1
  2. package/dist/client.cjs +50 -50
  3. package/dist/client.css +470 -470
  4. package/dist/client.js +50 -50
  5. package/dist/index.cjs +50 -50
  6. package/dist/index.css +470 -470
  7. package/dist/index.js +50 -50
  8. package/dist/native/index.cjs +3692 -352
  9. package/dist/native/index.d.cts +359 -3
  10. package/dist/native/index.d.ts +359 -3
  11. package/dist/native/index.js +3811 -364
  12. package/package.json +1 -1
  13. package/src/components/interaction/Button/Button.native.stories.tsx +10 -12
  14. package/src/components/interaction/Button/Button.native.tsx +3 -3
  15. package/src/components/interaction/Dialog/Dialog.native.mdx +61 -0
  16. package/src/components/interaction/Dialog/Dialog.native.spec.tsx +73 -0
  17. package/src/components/interaction/Dialog/Dialog.native.stories.tsx +53 -0
  18. package/src/components/interaction/Dialog/Dialog.native.tsx +128 -0
  19. package/src/components/interaction/Drawer/Drawer.native.mdx +58 -0
  20. package/src/components/interaction/Drawer/Drawer.native.spec.tsx +81 -0
  21. package/src/components/interaction/Drawer/Drawer.native.stories.tsx +33 -0
  22. package/src/components/interaction/Drawer/Drawer.native.tsx +175 -0
  23. package/src/components/interaction/DropdownMenu/DropdownMenu.native.mdx +74 -0
  24. package/src/components/interaction/DropdownMenu/DropdownMenu.native.spec.tsx +78 -0
  25. package/src/components/interaction/DropdownMenu/DropdownMenu.native.stories.tsx +51 -0
  26. package/src/components/interaction/DropdownMenu/DropdownMenu.native.tsx +254 -0
  27. package/src/components/interaction/Popover/Popover.native.mdx +61 -0
  28. package/src/components/interaction/Popover/Popover.native.spec.tsx +73 -0
  29. package/src/components/interaction/Popover/Popover.native.stories.tsx +44 -0
  30. package/src/components/interaction/Popover/Popover.native.tsx +87 -0
  31. package/src/components/interaction/Rating/Rating.native.mdx +55 -0
  32. package/src/components/interaction/Rating/Rating.native.spec.tsx +38 -0
  33. package/src/components/interaction/Rating/Rating.native.stories.tsx +37 -0
  34. package/src/components/interaction/Rating/Rating.native.tsx +50 -0
  35. package/src/components/interaction/Toast/Toast.native.mdx +81 -0
  36. package/src/components/interaction/Toast/Toast.native.spec.tsx +80 -0
  37. package/src/components/interaction/Toast/Toast.native.stories.tsx +47 -0
  38. package/src/components/interaction/Toast/Toast.native.tsx +202 -0
  39. package/src/components/interaction/form/Checkbox/Checkbox.native.stories.tsx +2 -4
  40. package/src/components/interaction/form/Checkbox/Checkbox.native.tsx +1 -1
  41. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.mdx +78 -0
  42. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.spec.tsx +57 -0
  43. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.stories.tsx +96 -0
  44. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.native.tsx +61 -0
  45. package/src/components/interaction/form/Combobox/Combobox.native.mdx +86 -0
  46. package/src/components/interaction/form/Combobox/Combobox.native.spec.tsx +60 -0
  47. package/src/components/interaction/form/Combobox/Combobox.native.stories.tsx +99 -0
  48. package/src/components/interaction/form/Combobox/Combobox.native.tsx +211 -0
  49. package/src/components/interaction/form/FileInput/FileInput.native.mdx +86 -0
  50. package/src/components/interaction/form/FileInput/FileInput.native.spec.tsx +73 -0
  51. package/src/components/interaction/form/FileInput/FileInput.native.stories.tsx +69 -0
  52. package/src/components/interaction/form/FileInput/FileInput.native.tsx +129 -0
  53. package/src/components/interaction/form/FormInput/FormInput.native.mdx +81 -0
  54. package/src/components/interaction/form/FormInput/FormInput.native.spec.tsx +51 -0
  55. package/src/components/interaction/form/FormInput/FormInput.native.stories.tsx +54 -0
  56. package/src/components/interaction/form/FormInput/FormInput.native.tsx +122 -0
  57. package/src/components/interaction/form/NumberInput/NumberInput.native.mdx +87 -0
  58. package/src/components/interaction/form/NumberInput/NumberInput.native.spec.tsx +63 -0
  59. package/src/components/interaction/form/NumberInput/NumberInput.native.stories.tsx +63 -0
  60. package/src/components/interaction/form/NumberInput/NumberInput.native.tsx +198 -0
  61. package/src/components/interaction/form/Radio/Radio.native.spec.tsx +79 -0
  62. package/src/components/interaction/form/Radio/Radio.native.stories.tsx +12 -14
  63. package/src/components/interaction/form/Radio/Radio.native.tsx +1 -1
  64. package/src/components/interaction/form/RadioGroup/RadioGroup.native.mdx +79 -0
  65. package/src/components/interaction/form/RadioGroup/RadioGroup.native.spec.tsx +66 -0
  66. package/src/components/interaction/form/RadioGroup/RadioGroup.native.stories.tsx +96 -0
  67. package/src/components/interaction/form/RadioGroup/RadioGroup.native.tsx +61 -0
  68. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.mdx +78 -0
  69. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.spec.tsx +82 -0
  70. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.stories.tsx +99 -0
  71. package/src/components/interaction/form/SegmentedControl/SegmentedControl.native.tsx +121 -0
  72. package/src/components/interaction/form/Select/Select.native.mdx +85 -0
  73. package/src/components/interaction/form/Select/Select.native.spec.tsx +64 -0
  74. package/src/components/interaction/form/Select/Select.native.stories.tsx +75 -0
  75. package/src/components/interaction/form/Select/Select.native.tsx +252 -0
  76. package/src/components/interaction/form/Slider/Slider.native.mdx +83 -0
  77. package/src/components/interaction/form/Slider/Slider.native.spec.tsx +51 -0
  78. package/src/components/interaction/form/Slider/Slider.native.stories.tsx +58 -0
  79. package/src/components/interaction/form/Slider/Slider.native.tsx +188 -0
  80. package/src/components/interaction/form/Switch/Switch.native.stories.tsx +2 -4
  81. package/src/components/interaction/form/Textarea/Textarea.native.mdx +76 -0
  82. package/src/components/interaction/form/Textarea/Textarea.native.spec.tsx +50 -0
  83. package/src/components/interaction/form/Textarea/Textarea.native.stories.tsx +50 -0
  84. package/src/components/interaction/form/Textarea/Textarea.native.tsx +108 -0
  85. package/src/components/interaction/form/atoms/Label.native.spec.tsx +39 -0
  86. package/src/components/interaction/form/atoms/Label.native.stories.tsx +1 -1
  87. package/src/components/interaction/form/atoms/Message.native.stories.tsx +1 -1
  88. package/src/components/layout/ButtonGroup/ButtonGroup.native.spec.tsx +83 -0
  89. package/src/components/layout/ButtonGroup/ButtonGroup.native.stories.tsx +13 -19
  90. package/src/components/layout/Card/Card.native.spec.tsx +102 -0
  91. package/src/components/layout/Card/Card.native.stories.tsx +9 -11
  92. package/src/components/layout/Card/Card.native.tsx +1 -1
  93. package/src/components/layout/IconWrapper/IconWrapper.native.stories.tsx +7 -11
  94. package/src/components/layout/SectionHeader/SectionHeader.native.spec.tsx +51 -0
  95. package/src/components/layout/SectionHeader/SectionHeader.native.stories.tsx +6 -8
  96. package/src/components/ui/Accordion/Accordion.native.mdx +65 -0
  97. package/src/components/ui/Accordion/Accordion.native.spec.tsx +69 -0
  98. package/src/components/ui/Accordion/Accordion.native.stories.tsx +52 -0
  99. package/src/components/ui/Accordion/Accordion.native.tsx +141 -0
  100. package/src/components/ui/Alert/Alert.native.spec.tsx +76 -0
  101. package/src/components/ui/Alert/Alert.native.stories.tsx +8 -10
  102. package/src/components/ui/Alert/Alert.native.tsx +7 -7
  103. package/src/components/ui/Avatar/Avatar.native.stories.tsx +6 -8
  104. package/src/components/ui/Avatar/Avatar.native.tsx +2 -2
  105. package/src/components/ui/Badge/Badge.native.spec.tsx +45 -0
  106. package/src/components/ui/Badge/Badge.native.stories.tsx +6 -8
  107. package/src/components/ui/Badge/Badge.native.tsx +2 -2
  108. package/src/components/ui/Breadcrumb/Breadcrumb.native.mdx +52 -0
  109. package/src/components/ui/Breadcrumb/Breadcrumb.native.spec.tsx +49 -0
  110. package/src/components/ui/Breadcrumb/Breadcrumb.native.stories.tsx +46 -0
  111. package/src/components/ui/Breadcrumb/Breadcrumb.native.tsx +83 -0
  112. package/src/components/ui/Calendar/Calendar.native.mdx +58 -0
  113. package/src/components/ui/Calendar/Calendar.native.spec.tsx +92 -0
  114. package/src/components/ui/Calendar/Calendar.native.stories.tsx +45 -0
  115. package/src/components/ui/Calendar/Calendar.native.tsx +294 -0
  116. package/src/components/ui/Carousel/Carousel.native.mdx +55 -0
  117. package/src/components/ui/Carousel/Carousel.native.spec.tsx +55 -0
  118. package/src/components/ui/Carousel/Carousel.native.stories.tsx +59 -0
  119. package/src/components/ui/Carousel/Carousel.native.tsx +184 -0
  120. package/src/components/ui/Collapsible/Collapsible.native.mdx +53 -0
  121. package/src/components/ui/Collapsible/Collapsible.native.spec.tsx +68 -0
  122. package/src/components/ui/Collapsible/Collapsible.native.stories.tsx +48 -0
  123. package/src/components/ui/Collapsible/Collapsible.native.tsx +139 -0
  124. package/src/components/ui/DescriptionList/DescriptionList.native.mdx +50 -0
  125. package/src/components/ui/DescriptionList/DescriptionList.native.spec.tsx +37 -0
  126. package/src/components/ui/DescriptionList/DescriptionList.native.stories.tsx +48 -0
  127. package/src/components/ui/DescriptionList/DescriptionList.native.tsx +54 -0
  128. package/src/components/ui/Link/Link.native.mdx +53 -0
  129. package/src/components/ui/Link/Link.native.spec.tsx +35 -0
  130. package/src/components/ui/Link/Link.native.stories.tsx +46 -0
  131. package/src/components/ui/Link/Link.native.tsx +48 -0
  132. package/src/components/ui/Loading/Loading.native.stories.tsx +6 -8
  133. package/src/components/ui/NotificationBanner/NotificationBanner.native.spec.tsx +85 -0
  134. package/src/components/ui/NotificationBanner/NotificationBanner.native.stories.tsx +8 -10
  135. package/src/components/ui/NotificationBanner/NotificationBanner.native.tsx +7 -7
  136. package/src/components/ui/Pagination/Pagination.native.mdx +62 -0
  137. package/src/components/ui/Pagination/Pagination.native.spec.tsx +86 -0
  138. package/src/components/ui/Pagination/Pagination.native.stories.tsx +47 -0
  139. package/src/components/ui/Pagination/Pagination.native.tsx +157 -0
  140. package/src/components/ui/Progress/Progress.native.spec.tsx +56 -0
  141. package/src/components/ui/Progress/Progress.native.stories.tsx +1 -1
  142. package/src/components/ui/Progress/Progress.native.tsx +2 -2
  143. package/src/components/ui/ProgressCircle/ProgressCircle.native.mdx +63 -0
  144. package/src/components/ui/ProgressCircle/ProgressCircle.native.spec.tsx +39 -0
  145. package/src/components/ui/ProgressCircle/ProgressCircle.native.stories.tsx +36 -0
  146. package/src/components/ui/ProgressCircle/ProgressCircle.native.tsx +176 -0
  147. package/src/components/ui/Separator/Separator.native.spec.tsx +39 -0
  148. package/src/components/ui/Separator/Separator.native.stories.tsx +10 -14
  149. package/src/components/ui/Skeleton/Skeleton.native.spec.tsx +39 -0
  150. package/src/components/ui/Skeleton/Skeleton.native.stories.tsx +6 -8
  151. package/src/components/ui/Skeleton/Skeleton.native.tsx +2 -1
  152. package/src/components/ui/Table/Table.native.mdx +72 -0
  153. package/src/components/ui/Table/Table.native.spec.tsx +83 -0
  154. package/src/components/ui/Table/Table.native.stories.tsx +55 -0
  155. package/src/components/ui/Table/Table.native.tsx +196 -0
  156. package/src/components/ui/Tabs/Tabs.native.mdx +65 -0
  157. package/src/components/ui/Tabs/Tabs.native.spec.tsx +65 -0
  158. package/src/components/ui/Tabs/Tabs.native.stories.tsx +57 -0
  159. package/src/components/ui/Tabs/Tabs.native.tsx +133 -0
  160. package/src/components/ui/Tooltip/Tooltip.native.mdx +47 -0
  161. package/src/components/ui/Tooltip/Tooltip.native.spec.tsx +37 -0
  162. package/src/components/ui/Tooltip/Tooltip.native.stories.tsx +38 -0
  163. package/src/components/ui/Tooltip/Tooltip.native.tsx +57 -0
  164. package/src/components/ui/Typography/Typography.native.spec.tsx +49 -0
  165. package/src/components/ui/Typography/Typography.native.stories.tsx +8 -10
  166. package/src/native/ThemeContext.tsx +3 -3
  167. package/src/native.ts +37 -0
  168. package/src/stories/ReactNative.mdx +48 -13
  169. package/src/web-components/interaction/BdsAccordion.stories.tsx +1 -1
  170. package/src/web-components/interaction/BdsCollapsible.stories.tsx +1 -1
  171. package/src/web-components/interaction/BdsSkipLink.stories.tsx +1 -1
  172. package/src/web-components/interaction/BdsTabs.stories.tsx +1 -1
  173. package/src/web-components/interaction/BdsTooltip.stories.tsx +1 -1
  174. package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +1 -1
  175. package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +1 -1
  176. package/src/web-components/interaction/form/BdsCombobox.stories.tsx +1 -1
  177. package/src/web-components/interaction/form/BdsFileInput.stories.tsx +1 -1
  178. package/src/web-components/interaction/form/BdsFormInput.stories.tsx +1 -1
  179. package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +1 -1
  180. package/src/web-components/interaction/form/BdsRadio.stories.tsx +1 -1
  181. package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +1 -1
  182. package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +1 -1
  183. package/src/web-components/interaction/form/BdsSelect.stories.tsx +1 -1
  184. package/src/web-components/interaction/form/BdsSlider.stories.tsx +1 -1
  185. package/src/web-components/interaction/form/BdsSwitch.stories.tsx +1 -1
  186. package/src/web-components/interaction/form/BdsTextarea.stories.tsx +1 -1
  187. package/src/web-components/ui/BdsButtonGroup.stories.tsx +1 -1
  188. package/src/web-components/ui/BdsCard.stories.tsx +1 -1
  189. package/src/web-components/ui/BdsIconWrapper.stories.tsx +1 -1
  190. package/src/web-components/ui/BdsRating.stories.tsx +1 -1
  191. package/src/web-components/ui/BdsSectionHeader.stories.tsx +1 -1
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider } from '../../../native/ThemeContext';
4
+ import { ProgressCircle } from './ProgressCircle.native';
5
+
6
+ const meta = {
7
+ tags: ['!stable', 'alpha'],
8
+ title: 'React Native/UI/ProgressCircle',
9
+ component: ProgressCircle,
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
+ parameters: { layout: 'centered' },
12
+ argTypes: {
13
+ size: { control: 'select', options: ['small', 'medium', 'large'] },
14
+ value: { control: { type: 'range', min: 0, max: 100, step: 1 } },
15
+ },
16
+ } satisfies Meta<typeof ProgressCircle>;
17
+
18
+ export default meta;
19
+ type Story = StoryObj<typeof meta>;
20
+
21
+ export const Default: Story = { args: { value: 60, label: 'Progress' } };
22
+ export const WithValue: Story = { args: { value: 75, label: 'Upload progress', showValue: true } };
23
+ export const Small: Story = { args: { value: 50, label: 'Small progress', size: 'small' } };
24
+ export const Large: Story = { args: { value: 80, label: 'Large progress', size: 'large', showValue: true } };
25
+ export const Complete: Story = { args: { value: 100, label: 'Complete', showValue: true } };
26
+ export const Empty: Story = { args: { value: 0, label: 'Not started', showValue: true } };
27
+
28
+ export const AllSizes: Story = {
29
+ render: () => (
30
+ <div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
31
+ {(['small', 'medium', 'large'] as const).map(s => (
32
+ <ProgressCircle key={s} value={65} label={`${s} progress`} size={s} showValue={s !== 'small'} />
33
+ ))}
34
+ </div>
35
+ ),
36
+ };
@@ -0,0 +1,176 @@
1
+ import { StyleSheet, View, Text, StyleProp, ViewStyle } from 'react-native';
2
+ import { font } from '../../../native/tokens';
3
+ import { useTheme } from '../../../native/ThemeContext';
4
+
5
+ type ProgressCircleSize = 'small' | 'medium' | 'large';
6
+
7
+ interface ProgressCircleProps {
8
+ value: number;
9
+ max?: number;
10
+ size?: ProgressCircleSize;
11
+ label: string;
12
+ showValue?: boolean;
13
+ style?: StyleProp<ViewStyle>;
14
+ }
15
+
16
+ const SIZE_PX: Record<ProgressCircleSize, number> = {
17
+ small: 40,
18
+ medium: 64,
19
+ large: 96,
20
+ };
21
+
22
+ const STROKE_WIDTH: Record<ProgressCircleSize, number> = {
23
+ small: 3,
24
+ medium: 4,
25
+ large: 5,
26
+ };
27
+
28
+ const FONT_SIZE: Record<ProgressCircleSize, number> = {
29
+ small: font.size.bodyS - 2,
30
+ medium: font.size.bodyS,
31
+ large: font.size.body,
32
+ };
33
+
34
+ const styles = StyleSheet.create({
35
+ wrapper: {
36
+ alignItems: 'center',
37
+ justifyContent: 'center',
38
+ },
39
+ valueText: {
40
+ position: 'absolute',
41
+ fontFamily: font.family.body,
42
+ fontWeight: font.weight.semibold,
43
+ },
44
+ });
45
+
46
+ function HalfCircle({
47
+ size,
48
+ stroke,
49
+ color,
50
+ rotation,
51
+ }: {
52
+ size: number;
53
+ stroke: number;
54
+ color: string;
55
+ rotation: number;
56
+ }) {
57
+ return (
58
+ <View
59
+ style={{
60
+ width: size / 2,
61
+ height: size,
62
+ overflow: 'hidden',
63
+ }}
64
+ >
65
+ <View
66
+ style={{
67
+ width: size,
68
+ height: size,
69
+ borderRadius: size / 2,
70
+ borderWidth: stroke,
71
+ borderColor: color,
72
+ transform: [{ rotate: `${rotation}deg` }],
73
+ }}
74
+ />
75
+ </View>
76
+ );
77
+ }
78
+
79
+ export function ProgressCircle({
80
+ value,
81
+ max = 100,
82
+ size = 'medium',
83
+ label,
84
+ showValue = false,
85
+ style,
86
+ }: Readonly<ProgressCircleProps>) {
87
+ const { colors } = useTheme();
88
+ const percentage = Math.min(100, Math.max(0, (value / max) * 100));
89
+ const px = SIZE_PX[size];
90
+ const stroke = STROKE_WIDTH[size];
91
+ const fillColor = colors.colorInteractive;
92
+ const trackColor = colors.colorBgSubtle;
93
+
94
+ // Convert percentage to degrees (0-360)
95
+ const degrees = (percentage / 100) * 360;
96
+
97
+ return (
98
+ <View
99
+ style={[
100
+ styles.wrapper,
101
+ { width: px, height: px },
102
+ style,
103
+ ]}
104
+ accessibilityRole="progressbar"
105
+ accessibilityLabel={label}
106
+ accessibilityValue={{ min: 0, max, now: value }}
107
+ >
108
+ {/* Track: full circle background */}
109
+ <View
110
+ style={{
111
+ position: 'absolute',
112
+ width: px,
113
+ height: px,
114
+ borderRadius: px / 2,
115
+ borderWidth: stroke,
116
+ borderColor: trackColor,
117
+ }}
118
+ />
119
+
120
+ {/* Fill: right half (0-180°) */}
121
+ <View
122
+ style={{
123
+ position: 'absolute',
124
+ width: px / 2,
125
+ height: px,
126
+ start: px / 2,
127
+ overflow: 'hidden',
128
+ }}
129
+ >
130
+ <HalfCircle
131
+ size={px}
132
+ stroke={stroke}
133
+ color={percentage > 0 ? fillColor : 'transparent'}
134
+ rotation={Math.min(degrees, 180) - 180}
135
+ />
136
+ </View>
137
+
138
+ {/* Fill: left half (180-360°) */}
139
+ {degrees > 180 && (
140
+ <View
141
+ style={{
142
+ position: 'absolute',
143
+ width: px / 2,
144
+ height: px,
145
+ start: 0,
146
+ overflow: 'hidden',
147
+ }}
148
+ >
149
+ <View
150
+ style={{
151
+ position: 'absolute',
152
+ end: 0,
153
+ }}
154
+ >
155
+ <HalfCircle
156
+ size={px}
157
+ stroke={stroke}
158
+ color={fillColor}
159
+ rotation={Math.min(degrees - 180, 180)}
160
+ />
161
+ </View>
162
+ </View>
163
+ )}
164
+
165
+ {showValue && (
166
+ <Text
167
+ style={[styles.valueText, { fontSize: FONT_SIZE[size], color: colors.colorOnBg }]}
168
+ importantForAccessibility="no"
169
+ accessibilityElementsHidden={true}
170
+ >
171
+ {Math.round(percentage)}%
172
+ </Text>
173
+ )}
174
+ </View>
175
+ );
176
+ }
@@ -0,0 +1,39 @@
1
+ import { render } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { Separator } from './Separator.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('Separator (native)', () => {
10
+ it('renders with presentation role', () => {
11
+ const { container } = renderWithTheme(<Separator />);
12
+ const element = container.firstChild as HTMLElement;
13
+ // react-native-web maps accessibilityRole="none" to role="presentation"
14
+ expect(element).toHaveAttribute('role', 'presentation');
15
+ });
16
+
17
+ it('renders a single element', () => {
18
+ const { container } = renderWithTheme(<Separator />);
19
+ expect(container.firstChild).toBeInTheDocument();
20
+ expect(container.childElementCount).toBe(1);
21
+ });
22
+
23
+ it('renders differently for vertical orientation', () => {
24
+ const { container: hContainer } = renderWithTheme(<Separator />);
25
+ const { container: vContainer } = renderWithTheme(<Separator orientation="vertical" />);
26
+ const hEl = hContainer.firstChild as HTMLElement;
27
+ const vEl = vContainer.firstChild as HTMLElement;
28
+ // Both render but with distinct class/style sets
29
+ expect(hEl.className).not.toBe(vEl.className);
30
+ });
31
+
32
+ it('applies custom style', () => {
33
+ const { container } = renderWithTheme(
34
+ <Separator style={{ marginTop: 16 }} />,
35
+ );
36
+ const element = container.firstChild as HTMLElement;
37
+ expect(element.style.marginTop).toBe('16px');
38
+ });
39
+ });
@@ -7,7 +7,7 @@ const meta = {
7
7
  tags: ['!stable', 'alpha'],
8
8
  title: 'React Native/UI/Separator',
9
9
  component: Separator,
10
- decorators: [(Story) => <ThemeProvider><Story /></ThemeProvider>],
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
11
  parameters: { layout: 'padded' },
12
12
  argTypes: {
13
13
  orientation: { control: 'select', options: ['horizontal', 'vertical'] },
@@ -19,24 +19,20 @@ type Story = StoryObj<typeof meta>;
19
19
 
20
20
  export const Horizontal: Story = {
21
21
  render: (args) => (
22
- <ThemeProvider>
23
- <div style={{ width: 300 }}>
24
- <Separator {...args} orientation="horizontal" />
25
- </div>
26
- </ThemeProvider>
22
+ <div style={{ width: 300 }}>
23
+ <Separator {...args} orientation="horizontal" />
24
+ </div>
27
25
  ),
28
26
  };
29
27
 
30
28
  export const Vertical: Story = {
31
29
  render: (args) => (
32
- <ThemeProvider>
33
- <div style={{ display: 'flex', height: 60, alignItems: 'stretch' }}>
34
- <span>Left</span>
35
- <div style={{ display: 'flex', margin: '0 8px' }}>
36
- <Separator {...args} orientation="vertical" />
37
- </div>
38
- <span>Right</span>
30
+ <div style={{ display: 'flex', height: 60, alignItems: 'stretch' }}>
31
+ <span>Left</span>
32
+ <div style={{ display: 'flex', margin: '0 8px' }}>
33
+ <Separator {...args} orientation="vertical" />
39
34
  </div>
40
- </ThemeProvider>
35
+ <span>Right</span>
36
+ </div>
41
37
  ),
42
38
  };
@@ -0,0 +1,39 @@
1
+ import { render } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { Skeleton } from './Skeleton.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ describe('Skeleton (native)', () => {
10
+ it('renders without crashing', () => {
11
+ const { container } = renderWithTheme(<Skeleton />);
12
+ expect(container.firstChild).toBeInTheDocument();
13
+ });
14
+
15
+ it('marks content as not important for accessibility', () => {
16
+ const { container } = renderWithTheme(<Skeleton />);
17
+ const element = container.firstChild as HTMLElement;
18
+ // importantForAccessibility="no-hide-descendants" hides descendants from a11y tree
19
+ // In react-native-web this renders as aria-hidden on the element
20
+ expect(element).toBeTruthy();
21
+ // The element should not have any accessible role
22
+ expect(element.getAttribute('role')).toBeNull();
23
+ });
24
+
25
+ it('applies custom style', () => {
26
+ const { container } = renderWithTheme(
27
+ <Skeleton style={{ width: 200, height: 20 }} />,
28
+ );
29
+ const element = container.firstChild as HTMLElement;
30
+ expect(element.style.width).toBe('200px');
31
+ expect(element.style.height).toBe('20px');
32
+ });
33
+
34
+ it('has a background color from the theme', () => {
35
+ const { container } = renderWithTheme(<Skeleton />);
36
+ const element = container.firstChild as HTMLElement;
37
+ expect(element.style.backgroundColor).toBeTruthy();
38
+ });
39
+ });
@@ -7,7 +7,7 @@ const meta = {
7
7
  tags: ['!stable', 'alpha'],
8
8
  title: 'React Native/UI/Skeleton',
9
9
  component: Skeleton,
10
- decorators: [(Story) => <ThemeProvider><Story /></ThemeProvider>],
10
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
11
11
  parameters: { layout: 'padded' },
12
12
  } satisfies Meta<typeof Skeleton>;
13
13
 
@@ -22,12 +22,10 @@ export const Avatar: Story = {
22
22
  };
23
23
  export const Card: Story = {
24
24
  render: () => (
25
- <ThemeProvider>
26
- <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', width: 280 }}>
27
- <Skeleton style={{ width: 280, height: 160 }} />
28
- <Skeleton style={{ width: 200, height: 16 }} />
29
- <Skeleton style={{ width: 140, height: 14 }} />
30
- </div>
31
- </ThemeProvider>
25
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', width: 280 }}>
26
+ <Skeleton style={{ width: 280, height: 160 }} />
27
+ <Skeleton style={{ width: 200, height: 16 }} />
28
+ <Skeleton style={{ width: 140, height: 14 }} />
29
+ </div>
32
30
  ),
33
31
  };
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
  import { Animated, StyleSheet, StyleProp, ViewStyle } from 'react-native';
3
+ import { border } from '../../../native/tokens';
3
4
  import { useTheme } from '../../../native/ThemeContext';
4
5
 
5
6
  interface SkeletonProps {
@@ -8,7 +9,7 @@ interface SkeletonProps {
8
9
 
9
10
  const styles = StyleSheet.create({
10
11
  skeleton: {
11
- borderRadius: 4,
12
+ borderRadius: border.radius.xs,
12
13
  overflow: 'hidden',
13
14
  },
14
15
  });
@@ -0,0 +1,72 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './Table.native.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # Table (React Native)
7
+
8
+ React Native table component for displaying structured data in rows and columns. Supports optional sortable columns, captions, and horizontal scrolling for wide tables. Alternating row backgrounds improve readability.
9
+
10
+ > **Status: alpha** -- API may change before stable release.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Table } from '@boostdev/components';
16
+
17
+ <Table
18
+ columns={[
19
+ { key: 'name', header: 'Name' },
20
+ { key: 'email', header: 'Email' },
21
+ ]}
22
+ rows={[
23
+ { name: 'Alice', email: 'alice@example.com' },
24
+ { name: 'Bob', email: 'bob@example.com' },
25
+ ]}
26
+ />
27
+ ```
28
+
29
+ ## Examples
30
+
31
+ ### Default
32
+ <Canvas of={Stories.Default} />
33
+
34
+ ### With caption
35
+ <Canvas of={Stories.WithCaption} />
36
+
37
+ ### Sortable columns
38
+ <Canvas of={Stories.Sortable} />
39
+
40
+ ### Empty table
41
+ <Canvas of={Stories.Empty} />
42
+
43
+ ## Props
44
+
45
+ <ArgTypes of={Stories} />
46
+
47
+ ## Props reference
48
+
49
+ | Prop | Type | Default | Description |
50
+ |------|------|---------|-------------|
51
+ | `columns` | `TableColumn<Row>[]` | -- | Column definitions |
52
+ | `rows` | `Row[]` | -- | Data rows |
53
+ | `caption` | `string` | -- | Optional table caption |
54
+ | `sortKey` | `string` | -- | Currently sorted column key |
55
+ | `sortDirection` | `"asc" \| "desc"` | -- | Current sort direction |
56
+ | `onSort` | `(key: string, direction: "asc" \| "desc") => void` | -- | Called when a sortable header is pressed |
57
+ | `style` | `StyleProp<ViewStyle>` | -- | Custom style for the scroll wrapper |
58
+
59
+ ### TableColumn
60
+
61
+ | Prop | Type | Description |
62
+ |------|------|-------------|
63
+ | `key` | `string` | Column key matching row property |
64
+ | `header` | `ReactNode` | Header cell content |
65
+ | `sortable` | `boolean` | Enables sort button on header |
66
+ | `render` | `(row: Row) => ReactNode` | Custom cell renderer |
67
+
68
+ ## Accessibility
69
+
70
+ - Sortable headers have `button` role with descriptive labels
71
+ - Sort indicators are hidden from the accessibility tree
72
+ - Table scrolls horizontally when content exceeds the viewport width
@@ -0,0 +1,83 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { ThemeProvider } from '../../../native/ThemeContext';
3
+ import { Table } from './Table.native';
4
+
5
+ function renderWithTheme(ui: React.ReactElement) {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ }
8
+
9
+ const columns = [
10
+ { key: 'name', header: 'Name' },
11
+ { key: 'age', header: 'Age' },
12
+ ];
13
+
14
+ const rows = [
15
+ { name: 'Alice', age: 29 },
16
+ { name: 'Bob', age: 34 },
17
+ ];
18
+
19
+ describe('Table (native)', () => {
20
+ it('renders column headers', () => {
21
+ renderWithTheme(<Table columns={columns} rows={rows} />);
22
+ expect(screen.getByText('Name')).toBeInTheDocument();
23
+ expect(screen.getByText('Age')).toBeInTheDocument();
24
+ });
25
+
26
+ it('renders row data', () => {
27
+ renderWithTheme(<Table columns={columns} rows={rows} />);
28
+ expect(screen.getByText('Alice')).toBeInTheDocument();
29
+ expect(screen.getByText('29')).toBeInTheDocument();
30
+ expect(screen.getByText('Bob')).toBeInTheDocument();
31
+ expect(screen.getByText('34')).toBeInTheDocument();
32
+ });
33
+
34
+ it('renders a caption when provided', () => {
35
+ renderWithTheme(<Table columns={columns} rows={rows} caption="Team" />);
36
+ expect(screen.getByText('Team')).toBeInTheDocument();
37
+ });
38
+
39
+ it('renders an empty table without errors', () => {
40
+ renderWithTheme(<Table columns={columns} rows={[]} />);
41
+ expect(screen.getByText('Name')).toBeInTheDocument();
42
+ expect(screen.getByText('Age')).toBeInTheDocument();
43
+ });
44
+
45
+ it('calls onSort when a sortable column header is pressed', () => {
46
+ const onSort = vi.fn();
47
+ const sortableColumns = [
48
+ { key: 'name', header: 'Name', sortable: true },
49
+ { key: 'age', header: 'Age' },
50
+ ];
51
+ renderWithTheme(
52
+ <Table columns={sortableColumns} rows={rows} onSort={onSort} />,
53
+ );
54
+ fireEvent.click(screen.getByRole('button', { name: /Sort by Name/ }));
55
+ expect(onSort).toHaveBeenCalledWith('name', 'asc');
56
+ });
57
+
58
+ it('toggles sort direction on subsequent presses', () => {
59
+ const onSort = vi.fn();
60
+ const sortableColumns = [
61
+ { key: 'name', header: 'Name', sortable: true },
62
+ ];
63
+ renderWithTheme(
64
+ <Table
65
+ columns={sortableColumns}
66
+ rows={rows}
67
+ sortKey="name"
68
+ sortDirection="asc"
69
+ onSort={onSort}
70
+ />,
71
+ );
72
+ fireEvent.click(screen.getByRole('button', { name: /Sort by Name/ }));
73
+ expect(onSort).toHaveBeenCalledWith('name', 'desc');
74
+ });
75
+
76
+ it('uses custom render function for cells', () => {
77
+ const customColumns = [
78
+ { key: 'name', header: 'Name', render: (row: { name: string }) => `Mr. ${row.name}` },
79
+ ];
80
+ renderWithTheme(<Table columns={customColumns} rows={[{ name: 'Alice' }]} />);
81
+ expect(screen.getByText('Mr. Alice')).toBeInTheDocument();
82
+ });
83
+ });
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider } from '../../../native/ThemeContext';
4
+ import { Table } from './Table.native';
5
+
6
+ interface Person { name: string; role: string; age: number }
7
+
8
+ const meta = {
9
+ tags: ['!stable', 'alpha'],
10
+ title: 'React Native/UI/Table',
11
+ component: Table<Person>,
12
+ decorators: [(Story) => <ThemeProvider colorScheme="light"><Story /></ThemeProvider>],
13
+ parameters: { layout: 'padded' },
14
+ } satisfies Meta<typeof Table<Person>>;
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof meta>;
18
+
19
+ const columns = [
20
+ { key: 'name', header: 'Name' },
21
+ { key: 'role', header: 'Role' },
22
+ { key: 'age', header: 'Age' },
23
+ ];
24
+
25
+ const rows: Person[] = [
26
+ { name: 'Alice Johnson', role: 'Engineer', age: 29 },
27
+ { name: 'Bob Smith', role: 'Designer', age: 34 },
28
+ { name: 'Carol White', role: 'Manager', age: 41 },
29
+ { name: 'Dave Brown', role: 'Analyst', age: 26 },
30
+ ];
31
+
32
+ export const Default: Story = {
33
+ args: { columns, rows },
34
+ };
35
+
36
+ export const WithCaption: Story = {
37
+ args: { columns, rows, caption: 'Team Members' },
38
+ };
39
+
40
+ export const Sortable: Story = {
41
+ args: {
42
+ columns: [
43
+ { key: 'name', header: 'Name', sortable: true },
44
+ { key: 'role', header: 'Role' },
45
+ { key: 'age', header: 'Age', sortable: true },
46
+ ],
47
+ rows,
48
+ sortKey: 'name',
49
+ sortDirection: 'asc',
50
+ },
51
+ };
52
+
53
+ export const Empty: Story = {
54
+ args: { columns, rows: [] },
55
+ };