cpk-ui 0.5.2 → 0.5.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.
@@ -0,0 +1,31 @@
1
+ import React, { type ReactElement } from 'react';
2
+ import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
3
+ export type SegmentedControlSizeType = 'small' | 'medium' | 'large' | number;
4
+ export type SegmentedControlColorType = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light';
5
+ export type SegmentedControlItem = {
6
+ value: string | number;
7
+ text: string | ReactElement;
8
+ };
9
+ type Styles = {
10
+ container?: StyleProp<ViewStyle>;
11
+ segment?: StyleProp<ViewStyle>;
12
+ selectedSegment?: StyleProp<ViewStyle>;
13
+ text?: StyleProp<TextStyle>;
14
+ selectedText?: StyleProp<TextStyle>;
15
+ };
16
+ export type SegmentedControlProps = {
17
+ testID?: string;
18
+ values: SegmentedControlItem[];
19
+ selectedValue: string | number;
20
+ onValueChange?: (value: string | number) => void;
21
+ color?: SegmentedControlColorType;
22
+ size?: SegmentedControlSizeType;
23
+ disabled?: boolean;
24
+ style?: StyleProp<ViewStyle>;
25
+ styles?: Styles;
26
+ borderRadius?: number;
27
+ };
28
+ declare function SegmentedControlContainer({ testID, values, selectedValue, onValueChange, color, size, disabled, style, styles, borderRadius, }: SegmentedControlProps): ReactElement;
29
+ export declare const SegmentedControl: React.MemoExoticComponent<typeof SegmentedControlContainer>;
30
+ export default SegmentedControl;
31
+ //# sourceMappingURL=SegmentedControl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SegmentedControl.d.ts","sourceRoot":"","sources":["../../../../src/components/uis/SegmentedControl/SegmentedControl.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAuB,KAAK,YAAY,EAAC,MAAM,OAAO,CAAC;AACrE,OAAO,KAAK,EAAC,SAAS,EAAE,SAAS,EAAE,SAAS,EAAC,MAAM,cAAc,CAAC;AAQlE,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;AAC7E,MAAM,MAAM,yBAAyB,GACjC,SAAS,GACT,WAAW,GACX,SAAS,GACT,QAAQ,GACR,SAAS,GACT,MAAM,GACN,OAAO,CAAC;AAEZ,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,YAAY,CAAC;CAC7B,CAAC;AAEF,KAAK,MAAM,GAAG;IACZ,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/B,eAAe,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACvC,IAAI,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC5B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,oBAAoB,EAAE,CAAC;IAC/B,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;IACjD,KAAK,CAAC,EAAE,yBAAyB,CAAC;IAClC,IAAI,CAAC,EAAE,wBAAwB,CAAC;IAChC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AA6EF,iBAAS,yBAAyB,CAAC,EACjC,MAAM,EACN,MAAM,EACN,aAAa,EACb,aAAa,EACb,KAAiB,EACjB,IAAe,EACf,QAAgB,EAChB,KAAK,EACL,MAAM,EACN,YAAgB,GACjB,EAAE,qBAAqB,GAAG,YAAY,CA0FtC;AAGD,eAAO,MAAM,gBAAgB,6DAE5B,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,118 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React, { useCallback, useMemo } from 'react';
3
+ import { TouchableOpacity } from 'react-native';
4
+ import { styled, css } from 'kstyled';
5
+ import { useTheme } from '../../../providers/ThemeProvider';
6
+ import { Typography } from '../Typography/Typography';
7
+ const Container = styled.View `
8
+ overflow: hidden;
9
+ `;
10
+ // Calculate styles based on theme and props
11
+ const calculateStyles = ({ theme, color, size, borderRadius, styles, }) => {
12
+ const padding = typeof size === 'number'
13
+ ? `${size * 0.4}px ${size * 0.8}px`
14
+ : size === 'large'
15
+ ? '12px 16px'
16
+ : size === 'medium'
17
+ ? '8px 12px'
18
+ : size === 'small'
19
+ ? '6px 8px'
20
+ : '8px 12px';
21
+ return {
22
+ wrapper: [
23
+ css `
24
+ flex-direction: row;
25
+ background-color: ${theme.bg.paper};
26
+ border-top-color: ${theme.bg.disabled};
27
+ border-bottom-color: ${theme.bg.disabled};
28
+ border-left-color: ${theme.bg.disabled};
29
+ border-right-color: ${theme.bg.disabled};
30
+ border-width: 1px;
31
+ border-radius: ${borderRadius}px;
32
+ `,
33
+ ],
34
+ container: styles?.container,
35
+ segment: [
36
+ css `
37
+ flex: 1;
38
+ align-items: center;
39
+ justify-content: center;
40
+ padding: ${padding};
41
+ background-color: transparent;
42
+ `,
43
+ styles?.segment,
44
+ ],
45
+ selectedSegment: [
46
+ css `
47
+ background-color: ${theme.button[color].bg};
48
+ `,
49
+ styles?.selectedSegment,
50
+ ],
51
+ text: [
52
+ css `
53
+ color: ${theme.text.basic};
54
+ `,
55
+ styles?.text,
56
+ ],
57
+ selectedText: [
58
+ css `
59
+ color: ${theme.button[color].text};
60
+ font-weight: 600;
61
+ `,
62
+ styles?.selectedText,
63
+ ],
64
+ };
65
+ };
66
+ function SegmentedControlContainer({ testID, values, selectedValue, onValueChange, color = 'primary', size = 'medium', disabled = false, style, styles, borderRadius = 8, }) {
67
+ const { theme } = useTheme();
68
+ // Memoize styles calculation
69
+ const compositeStyles = useMemo(() => calculateStyles({
70
+ theme,
71
+ color,
72
+ size,
73
+ borderRadius,
74
+ styles,
75
+ }), [theme, color, size, borderRadius, styles]);
76
+ // Memoize segment press handler
77
+ const handlePress = useCallback((value) => {
78
+ if (!disabled) {
79
+ onValueChange?.(value);
80
+ }
81
+ }, [disabled, onValueChange]);
82
+ // Memoize segments rendering
83
+ const segments = useMemo(() => {
84
+ const cornerRadius = Math.max(borderRadius - 1, 0);
85
+ return values.map((item, index) => {
86
+ const isSelected = selectedValue === item.value;
87
+ const isFirst = index === 0;
88
+ const isLast = index === values.length - 1;
89
+ return (_jsx(TouchableOpacity, { testID: `segment-${index}`, activeOpacity: 0.7, disabled: disabled, onPress: () => handlePress(item.value), style: [
90
+ compositeStyles.segment,
91
+ isSelected && compositeStyles.selectedSegment,
92
+ {
93
+ borderTopLeftRadius: isFirst ? cornerRadius : 0,
94
+ borderBottomLeftRadius: isFirst ? cornerRadius : 0,
95
+ borderTopRightRadius: isLast ? cornerRadius : 0,
96
+ borderBottomRightRadius: isLast ? cornerRadius : 0,
97
+ borderRightWidth: isLast ? 0 : 1,
98
+ borderRightColor: theme.bg.disabled,
99
+ },
100
+ ], children: typeof item.text === 'string' ? (_jsx(Typography.Body2, { style: [
101
+ compositeStyles.text,
102
+ isSelected && compositeStyles.selectedText,
103
+ ], children: item.text })) : (item.text) }, `segment-${index}`));
104
+ });
105
+ }, [
106
+ values,
107
+ selectedValue,
108
+ disabled,
109
+ handlePress,
110
+ compositeStyles,
111
+ borderRadius,
112
+ theme.bg.disabled,
113
+ ]);
114
+ return (_jsx(Container, { style: style, testID: testID, children: _jsx(Container, { style: [compositeStyles.wrapper, compositeStyles.container], children: segments }) }));
115
+ }
116
+ // Export memoized component for better performance
117
+ export const SegmentedControl = React.memo(SegmentedControlContainer);
118
+ export default SegmentedControl;
package/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export * from './components/uis/LoadingIndicator/LoadingIndicator';
16
16
  export * from './components/uis/PinchZoom/PinchZoom';
17
17
  export * from './components/uis/Rating/Rating';
18
18
  export * from './components/uis/RadioGroup/RadioGroup';
19
+ export * from './components/uis/SegmentedControl/SegmentedControl';
19
20
  export * from './components/uis/StatusbarBrightness/StatusBarBrightness';
20
21
  export * from './components/uis/SwitchToggle/SwitchToggle';
21
22
  export * from './components/uis/Typography/Typography';
package/index.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AACA,cAAc,6CAA6C,CAAC;AAC5D,cAAc,uCAAuC,CAAC;AAGtD,cAAc,iDAAiD,CAAC;AAGhE,cAAc,aAAa,CAAC;AAG5B,cAAc,sCAAsC,CAAC;AACrD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oCAAoC,CAAC;AACnD,cAAc,kDAAkD,CAAC;AACjE,cAAc,oCAAoC,CAAC;AACnD,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wCAAwC,CAAC;AACvD,cAAc,oDAAoD,CAAC;AACnE,cAAc,sCAAsC,CAAC;AACrD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,wCAAwC,CAAC;AACvD,cAAc,0DAA0D,CAAC;AACzE,cAAc,4CAA4C,CAAC;AAC3D,cAAc,wCAAwC,CAAC;AAGvD,cAAc,SAAS,CAAC;AAGxB,cAAc,SAAS,CAAC;AAGxB,cAAc,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AACA,cAAc,6CAA6C,CAAC;AAC5D,cAAc,uCAAuC,CAAC;AAGtD,cAAc,iDAAiD,CAAC;AAGhE,cAAc,aAAa,CAAC;AAG5B,cAAc,sCAAsC,CAAC;AACrD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oCAAoC,CAAC;AACnD,cAAc,kDAAkD,CAAC;AACjE,cAAc,oCAAoC,CAAC;AACnD,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wCAAwC,CAAC;AACvD,cAAc,oDAAoD,CAAC;AACnE,cAAc,sCAAsC,CAAC;AACrD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,wCAAwC,CAAC;AACvD,cAAc,oDAAoD,CAAC;AACnE,cAAc,0DAA0D,CAAC;AACzE,cAAc,4CAA4C,CAAC;AAC3D,cAAc,wCAAwC,CAAC;AAGvD,cAAc,SAAS,CAAC;AAGxB,cAAc,SAAS,CAAC;AAGxB,cAAc,SAAS,CAAC"}
package/index.js CHANGED
@@ -20,6 +20,7 @@ export * from './components/uis/LoadingIndicator/LoadingIndicator';
20
20
  export * from './components/uis/PinchZoom/PinchZoom';
21
21
  export * from './components/uis/Rating/Rating';
22
22
  export * from './components/uis/RadioGroup/RadioGroup';
23
+ export * from './components/uis/SegmentedControl/SegmentedControl';
23
24
  export * from './components/uis/StatusbarBrightness/StatusBarBrightness';
24
25
  export * from './components/uis/SwitchToggle/SwitchToggle';
25
26
  export * from './components/uis/Typography/Typography';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cpk-ui",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "main": "index",
5
5
  "react-native": "index",
6
6
  "module": "index",
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import {type ReactElement} from 'react';
3
- import {View} from 'react-native';
3
+ import {StyleSheet, View} from 'react-native';
4
4
  import type {RenderAPI} from '@testing-library/react-native';
5
5
  import {act, fireEvent, render, waitFor} from '@testing-library/react-native';
6
6
 
@@ -133,12 +133,14 @@ describe('[RadioButton]', () => {
133
133
  testingLib = render(component);
134
134
 
135
135
  const circleRadio = testingLib.getByTestId('circle-radio-0');
136
-
137
136
  // Get initial styles before layout
138
- const initialStyle = circleRadio.props.style;
139
- const initialMargin = Array.isArray(initialStyle)
140
- ? initialStyle.find((s: any) => s && s.margin !== undefined)?.margin
141
- : initialStyle?.margin;
137
+ const initialStyle = StyleSheet.flatten(circleRadio.props.style) ?? {};
138
+ const initialMargin =
139
+ initialStyle.margin ??
140
+ initialStyle.marginTop ??
141
+ initialStyle.marginRight ??
142
+ initialStyle.marginBottom ??
143
+ initialStyle.marginLeft;
142
144
 
143
145
  // Initially margin should be 0 since innerLayout is not set
144
146
  expect(initialMargin).toBe(0);
@@ -156,13 +158,18 @@ describe('[RadioButton]', () => {
156
158
 
157
159
  // After layout, the styles should reflect the innerLayout values
158
160
  // margin should be 2 and borderRadius should be width/2 = 20
159
- const updatedStyle = testingLib.getByTestId('circle-radio-0').props.style;
160
- const styleWithLayout = Array.isArray(updatedStyle)
161
- ? updatedStyle.find((s: any) => s && s.margin !== undefined)
162
- : updatedStyle;
163
-
164
- expect(styleWithLayout.margin).toBe(2);
165
- expect(styleWithLayout.borderRadius).toBe(20); // 40 / 2
161
+ const updatedStyle =
162
+ StyleSheet.flatten(testingLib.getByTestId('circle-radio-0').props.style) ??
163
+ {};
164
+ const updatedMargin =
165
+ updatedStyle.margin ??
166
+ updatedStyle.marginTop ??
167
+ updatedStyle.marginRight ??
168
+ updatedStyle.marginBottom ??
169
+ updatedStyle.marginLeft;
170
+
171
+ expect(updatedMargin).toBe(2);
172
+ expect(updatedStyle.borderRadius).toBe(20); // 40 / 2
166
173
  });
167
174
 
168
175
  describe('colors', () => {
@@ -0,0 +1,546 @@
1
+ import {useState} from 'react';
2
+ import type {Meta, StoryObj} from '@storybook/react';
3
+ import {View} from 'react-native';
4
+
5
+ import {withThemeProvider} from '../../../../.storybook/decorators';
6
+ import {SegmentedControl, SegmentedControlColorType, SegmentedControlSizeType} from './SegmentedControl';
7
+ import {Typography} from '../Typography/Typography';
8
+
9
+ const meta = {
10
+ title: 'SegmentedControl',
11
+ component: (props) => <SegmentedControl {...props} />,
12
+ parameters: {
13
+ notes: `
14
+ A iOS-style SegmentedControl component that allows users to select a single option from a set of choices.
15
+
16
+ ## Features
17
+ - **iOS-style Design**: Native iOS segmented control appearance
18
+ - **Flexible Sizing**: Supports preset sizes (small, medium, large) and custom numeric values
19
+ - **Color Variants**: Multiple color options for different contexts
20
+ - **Disabled State**: Can disable the entire control
21
+ - **Smooth Transitions**: Animated selection with visual feedback
22
+
23
+ ## Size Options
24
+ - \`small\`: 6px/8px padding
25
+ - \`medium\`: 8px/12px padding (default)
26
+ - \`large\`: 12px/16px padding
27
+ - Custom number: Any pixel value for custom padding
28
+
29
+ ## Usage
30
+ \`\`\`tsx
31
+ <SegmentedControl
32
+ values={[
33
+ {value: 'day', text: 'Day'},
34
+ {value: 'week', text: 'Week'},
35
+ {value: 'month', text: 'Month'},
36
+ ]}
37
+ selectedValue="day"
38
+ onValueChange={setSelectedValue}
39
+ size="medium"
40
+ color="primary"
41
+ />
42
+ \`\`\`
43
+ `,
44
+ docs: {
45
+ description: {
46
+ component: `
47
+ A iOS-style SegmentedControl component that allows users to select a single option from a set of choices.
48
+
49
+ ## Features
50
+ - **iOS-style Design**: Native iOS segmented control appearance
51
+ - **Flexible Sizing**: Supports preset sizes (small, medium, large) and custom numeric values
52
+ - **Color Variants**: Multiple color options for different contexts
53
+ - **Disabled State**: Can disable the entire control
54
+ - **Smooth Transitions**: Animated selection with visual feedback
55
+
56
+ ## Size Options
57
+ - \`small\`: 6px/8px padding
58
+ - \`medium\`: 8px/12px padding (default)
59
+ - \`large\`: 12px/16px padding
60
+ - Custom number: Any pixel value for custom padding
61
+
62
+ ## Usage
63
+ \`\`\`tsx
64
+ <SegmentedControl
65
+ values={[
66
+ {value: 'day', text: 'Day'},
67
+ {value: 'week', text: 'Week'},
68
+ {value: 'month', text: 'Month'},
69
+ ]}
70
+ selectedValue="day"
71
+ onValueChange={setSelectedValue}
72
+ size="medium"
73
+ color="primary"
74
+ />
75
+ \`\`\`
76
+ `,
77
+ },
78
+ },
79
+ },
80
+ argTypes: {
81
+ color: {
82
+ control: 'select',
83
+ options: [
84
+ 'primary',
85
+ 'secondary',
86
+ 'success',
87
+ 'danger',
88
+ 'warning',
89
+ 'info',
90
+ 'light',
91
+ ] as SegmentedControlColorType[],
92
+ description: 'Color variant of the selected segment',
93
+ },
94
+ size: {
95
+ control: 'radio',
96
+ options: ['small', 'medium', 'large', 16, 20, 24, 32],
97
+ description: 'Size can be "small", "medium", "large" or a custom number in pixels',
98
+ },
99
+ disabled: {
100
+ control: 'boolean',
101
+ description: 'Disables the entire segmented control',
102
+ },
103
+ borderRadius: {
104
+ control: {type: 'number', min: 0, max: 20, step: 1},
105
+ description: 'Border radius of the control',
106
+ },
107
+ },
108
+ decorators: [
109
+ (Story, context) =>
110
+ withThemeProvider(
111
+ Story,
112
+ context,
113
+ // @ts-ignore
114
+ context.args.theme,
115
+ ),
116
+ ],
117
+ } satisfies Meta<typeof SegmentedControl>;
118
+
119
+ export default meta;
120
+
121
+ type Story = StoryObj<typeof meta>;
122
+
123
+ export const Basic: Story = {
124
+ render: (args) => {
125
+ const [selectedValue, setSelectedValue] = useState<string | number>('day');
126
+
127
+ return (
128
+ <View style={{gap: 20}}>
129
+ <SegmentedControl
130
+ values={[
131
+ {value: 'day', text: 'Day'},
132
+ {value: 'week', text: 'Week'},
133
+ {value: 'month', text: 'Month'},
134
+ ]}
135
+ color={args.color}
136
+ size={args.size}
137
+ disabled={args.disabled}
138
+ borderRadius={args.borderRadius}
139
+ selectedValue={selectedValue}
140
+ onValueChange={setSelectedValue}
141
+ />
142
+ <View style={{padding: 16, alignItems: 'center'}}>
143
+ <Typography.Heading2>Selected: {selectedValue}</Typography.Heading2>
144
+ </View>
145
+ </View>
146
+ );
147
+ },
148
+ args: {
149
+ theme: 'light',
150
+ color: 'primary',
151
+ size: 'medium',
152
+ disabled: false,
153
+ borderRadius: 8,
154
+ },
155
+ argTypes: {
156
+ theme: {
157
+ control: 'select',
158
+ options: ['light', 'dark'],
159
+ },
160
+ },
161
+ };
162
+
163
+ export const WithDynamicContent: Story = {
164
+ render: (args) => {
165
+ const [selectedValue, setSelectedValue] = useState<string | number>('day');
166
+
167
+ const contentMap = {
168
+ day: {
169
+ title: 'Daily View',
170
+ description: 'View your daily activities and statistics',
171
+ emoji: '📅',
172
+ },
173
+ week: {
174
+ title: 'Weekly View',
175
+ description: 'See your weekly progress and trends',
176
+ emoji: '📊',
177
+ },
178
+ month: {
179
+ title: 'Monthly View',
180
+ description: 'Analyze your monthly performance',
181
+ emoji: '📈',
182
+ },
183
+ };
184
+
185
+ const content = contentMap[selectedValue as keyof typeof contentMap];
186
+
187
+ return (
188
+ <View style={{gap: 20}}>
189
+ <SegmentedControl
190
+ values={[
191
+ {value: 'day', text: 'Day'},
192
+ {value: 'week', text: 'Week'},
193
+ {value: 'month', text: 'Month'},
194
+ ]}
195
+ color={args.color}
196
+ size={args.size}
197
+ disabled={args.disabled}
198
+ borderRadius={args.borderRadius}
199
+ selectedValue={selectedValue}
200
+ onValueChange={setSelectedValue}
201
+ />
202
+ <View style={{padding: 24, alignItems: 'center', gap: 12}}>
203
+ <Typography.Heading1 style={{fontSize: 48}}>
204
+ {content.emoji}
205
+ </Typography.Heading1>
206
+ <Typography.Heading2>{content.title}</Typography.Heading2>
207
+ <Typography.Body1 style={{textAlign: 'center', opacity: 0.7}}>
208
+ {content.description}
209
+ </Typography.Body1>
210
+ </View>
211
+ </View>
212
+ );
213
+ },
214
+ args: {
215
+ theme: 'light',
216
+ color: 'primary',
217
+ size: 'medium',
218
+ disabled: false,
219
+ borderRadius: 8,
220
+ },
221
+ argTypes: {
222
+ theme: {
223
+ control: 'select',
224
+ options: ['light', 'dark'],
225
+ },
226
+ },
227
+ };
228
+
229
+ export const PlatformSelector: Story = {
230
+ render: (args) => {
231
+ const [selectedValue, setSelectedValue] = useState<string | number>('ios');
232
+
233
+ const platformInfo = {
234
+ ios: {
235
+ icon: '🍎',
236
+ name: 'iOS',
237
+ version: 'iOS 17+',
238
+ features: ['Swift', 'SwiftUI', 'UIKit'],
239
+ },
240
+ android: {
241
+ icon: '🤖',
242
+ name: 'Android',
243
+ version: 'Android 14+',
244
+ features: ['Kotlin', 'Jetpack Compose', 'Material Design'],
245
+ },
246
+ web: {
247
+ icon: '🌐',
248
+ name: 'Web',
249
+ version: 'Modern Browsers',
250
+ features: ['React', 'TypeScript', 'Responsive Design'],
251
+ },
252
+ };
253
+
254
+ const info = platformInfo[selectedValue as keyof typeof platformInfo];
255
+
256
+ return (
257
+ <View style={{gap: 20}}>
258
+ <SegmentedControl
259
+ values={[
260
+ {value: 'ios', text: 'iOS'},
261
+ {value: 'android', text: 'Android'},
262
+ {value: 'web', text: 'Web'},
263
+ ]}
264
+ color={args.color}
265
+ size={args.size}
266
+ disabled={args.disabled}
267
+ borderRadius={args.borderRadius}
268
+ selectedValue={selectedValue}
269
+ onValueChange={setSelectedValue}
270
+ />
271
+ <View style={{padding: 24, gap: 16}}>
272
+ <View style={{alignItems: 'center', gap: 8}}>
273
+ <Typography.Heading1 style={{fontSize: 56}}>
274
+ {info.icon}
275
+ </Typography.Heading1>
276
+ <Typography.Heading2>{info.name}</Typography.Heading2>
277
+ <Typography.Body2 style={{opacity: 0.6}}>
278
+ {info.version}
279
+ </Typography.Body2>
280
+ </View>
281
+ <View style={{gap: 8, marginTop: 8}}>
282
+ <Typography.Heading3>Key Technologies:</Typography.Heading3>
283
+ {info.features.map((feature) => (
284
+ <Typography.Body1 key={feature} style={{paddingLeft: 12}}>
285
+ • {feature}
286
+ </Typography.Body1>
287
+ ))}
288
+ </View>
289
+ </View>
290
+ </View>
291
+ );
292
+ },
293
+ args: {
294
+ theme: 'light',
295
+ color: 'primary',
296
+ size: 'medium',
297
+ disabled: false,
298
+ borderRadius: 8,
299
+ },
300
+ argTypes: {
301
+ theme: {
302
+ control: 'select',
303
+ options: ['light', 'dark'],
304
+ },
305
+ },
306
+ };
307
+
308
+ export const AllSizes: Story = {
309
+ render: (args) => {
310
+ const [selectedSmall, setSelectedSmall] = useState<string | number>('day');
311
+ const [selectedMedium, setSelectedMedium] = useState<string | number>('day');
312
+ const [selectedLarge, setSelectedLarge] = useState<string | number>('day');
313
+
314
+ const values = [
315
+ {value: 'day', text: 'Day'},
316
+ {value: 'week', text: 'Week'},
317
+ {value: 'month', text: 'Month'},
318
+ ];
319
+
320
+ return (
321
+ <View style={{gap: 24}}>
322
+ <View style={{gap: 8}}>
323
+ <Typography.Heading3>Small</Typography.Heading3>
324
+ <SegmentedControl
325
+ values={values}
326
+ color="primary"
327
+ size="small"
328
+ borderRadius={8}
329
+ selectedValue={selectedSmall}
330
+ onValueChange={setSelectedSmall}
331
+ />
332
+ </View>
333
+ <View style={{gap: 8}}>
334
+ <Typography.Heading3>Medium</Typography.Heading3>
335
+ <SegmentedControl
336
+ values={values}
337
+ color="primary"
338
+ size="medium"
339
+ borderRadius={8}
340
+ selectedValue={selectedMedium}
341
+ onValueChange={setSelectedMedium}
342
+ />
343
+ </View>
344
+ <View style={{gap: 8}}>
345
+ <Typography.Heading3>Large</Typography.Heading3>
346
+ <SegmentedControl
347
+ values={values}
348
+ color="primary"
349
+ size="large"
350
+ borderRadius={8}
351
+ selectedValue={selectedLarge}
352
+ onValueChange={setSelectedLarge}
353
+ />
354
+ </View>
355
+ </View>
356
+ );
357
+ },
358
+ args: {
359
+ theme: 'light',
360
+ },
361
+ argTypes: {
362
+ theme: {
363
+ control: 'select',
364
+ options: ['light', 'dark'],
365
+ },
366
+ },
367
+ };
368
+
369
+ export const AllColors: Story = {
370
+ render: (args) => {
371
+ const [selected1, setSelected1] = useState<string | number>(1);
372
+ const [selected2, setSelected2] = useState<string | number>(1);
373
+ const [selected3, setSelected3] = useState<string | number>(1);
374
+ const [selected4, setSelected4] = useState<string | number>(1);
375
+ const [selected5, setSelected5] = useState<string | number>(1);
376
+ const [selected6, setSelected6] = useState<string | number>(1);
377
+
378
+ const values = [
379
+ {value: 1, text: 'Option 1'},
380
+ {value: 2, text: 'Option 2'},
381
+ {value: 3, text: 'Option 3'},
382
+ ];
383
+
384
+ return (
385
+ <View style={{gap: 20}}>
386
+ <View style={{gap: 8}}>
387
+ <Typography.Heading3>Primary</Typography.Heading3>
388
+ <SegmentedControl
389
+ values={values}
390
+ color="primary"
391
+ size="medium"
392
+ borderRadius={8}
393
+ selectedValue={selected1}
394
+ onValueChange={setSelected1}
395
+ />
396
+ </View>
397
+ <View style={{gap: 8}}>
398
+ <Typography.Heading3>Secondary</Typography.Heading3>
399
+ <SegmentedControl
400
+ values={values}
401
+ color="secondary"
402
+ size="medium"
403
+ borderRadius={8}
404
+ selectedValue={selected2}
405
+ onValueChange={setSelected2}
406
+ />
407
+ </View>
408
+ <View style={{gap: 8}}>
409
+ <Typography.Heading3>Success</Typography.Heading3>
410
+ <SegmentedControl
411
+ values={values}
412
+ color="success"
413
+ size="medium"
414
+ borderRadius={8}
415
+ selectedValue={selected3}
416
+ onValueChange={setSelected3}
417
+ />
418
+ </View>
419
+ <View style={{gap: 8}}>
420
+ <Typography.Heading3>Danger</Typography.Heading3>
421
+ <SegmentedControl
422
+ values={values}
423
+ color="danger"
424
+ size="medium"
425
+ borderRadius={8}
426
+ selectedValue={selected4}
427
+ onValueChange={setSelected4}
428
+ />
429
+ </View>
430
+ <View style={{gap: 8}}>
431
+ <Typography.Heading3>Warning</Typography.Heading3>
432
+ <SegmentedControl
433
+ values={values}
434
+ color="warning"
435
+ size="medium"
436
+ borderRadius={8}
437
+ selectedValue={selected5}
438
+ onValueChange={setSelected5}
439
+ />
440
+ </View>
441
+ <View style={{gap: 8}}>
442
+ <Typography.Heading3>Info</Typography.Heading3>
443
+ <SegmentedControl
444
+ values={values}
445
+ color="info"
446
+ size="medium"
447
+ borderRadius={8}
448
+ selectedValue={selected6}
449
+ onValueChange={setSelected6}
450
+ />
451
+ </View>
452
+ </View>
453
+ );
454
+ },
455
+ args: {
456
+ theme: 'light',
457
+ },
458
+ argTypes: {
459
+ theme: {
460
+ control: 'select',
461
+ options: ['light', 'dark'],
462
+ },
463
+ },
464
+ };
465
+
466
+ export const WithIcons: Story = {
467
+ render: (args) => {
468
+ const [selectedValue, setSelectedValue] = useState<string | number>('list');
469
+
470
+ return (
471
+ <View style={{gap: 20}}>
472
+ <SegmentedControl
473
+ values={[
474
+ {value: 'grid', text: <Typography.Body2>📱 Grid</Typography.Body2>},
475
+ {value: 'list', text: <Typography.Body2>📋 List</Typography.Body2>},
476
+ {value: 'map', text: <Typography.Body2>🗺️ Map</Typography.Body2>},
477
+ ]}
478
+ color={args.color}
479
+ size={args.size}
480
+ disabled={args.disabled}
481
+ borderRadius={args.borderRadius}
482
+ selectedValue={selectedValue}
483
+ onValueChange={setSelectedValue}
484
+ />
485
+ <View style={{padding: 16, alignItems: 'center'}}>
486
+ <Typography.Body1>View mode: {selectedValue}</Typography.Body1>
487
+ </View>
488
+ </View>
489
+ );
490
+ },
491
+ args: {
492
+ theme: 'light',
493
+ color: 'primary',
494
+ size: 'medium',
495
+ disabled: false,
496
+ borderRadius: 8,
497
+ },
498
+ argTypes: {
499
+ theme: {
500
+ control: 'select',
501
+ options: ['light', 'dark'],
502
+ },
503
+ },
504
+ };
505
+
506
+ export const Disabled: Story = {
507
+ render: (args) => {
508
+ const [selectedValue, setSelectedValue] = useState<string | number>('second');
509
+
510
+ return (
511
+ <View style={{gap: 20}}>
512
+ <SegmentedControl
513
+ values={[
514
+ {value: 'first', text: 'First'},
515
+ {value: 'second', text: 'Second'},
516
+ {value: 'third', text: 'Third'},
517
+ ]}
518
+ color={args.color}
519
+ size={args.size}
520
+ disabled={args.disabled}
521
+ borderRadius={args.borderRadius}
522
+ selectedValue={selectedValue}
523
+ onValueChange={setSelectedValue}
524
+ />
525
+ <View style={{padding: 16, alignItems: 'center'}}>
526
+ <Typography.Body1 style={{opacity: 0.5}}>
527
+ This control is disabled
528
+ </Typography.Body1>
529
+ </View>
530
+ </View>
531
+ );
532
+ },
533
+ args: {
534
+ theme: 'light',
535
+ color: 'primary',
536
+ size: 'medium',
537
+ disabled: true,
538
+ borderRadius: 8,
539
+ },
540
+ argTypes: {
541
+ theme: {
542
+ control: 'select',
543
+ options: ['light', 'dark'],
544
+ },
545
+ },
546
+ };
@@ -0,0 +1,274 @@
1
+ import * as React from 'react';
2
+ import {type ReactElement} from 'react';
3
+ import {View, Text} from 'react-native';
4
+ import type {RenderAPI} from '@testing-library/react-native';
5
+ import {fireEvent, render, waitFor} from '@testing-library/react-native';
6
+
7
+ import {createComponent, createTestProps} from '../../../../test/testUtils';
8
+ import {type SegmentedControlColorType, SegmentedControl} from './SegmentedControl';
9
+ import {Typography} from '../Typography/Typography';
10
+
11
+ let props: any;
12
+ let component: ReactElement;
13
+ let testingLib: RenderAPI;
14
+
15
+ const values = [
16
+ {value: 'day', text: 'Day'},
17
+ {value: 'week', text: 'Week'},
18
+ {value: 'month', text: 'Month'},
19
+ ];
20
+
21
+ describe('[SegmentedControl] render', () => {
22
+ it('should render without crashing', async () => {
23
+ props = createTestProps({
24
+ selectedValue: 'day',
25
+ onValueChange: (value: string | number) => (props.selectedValue = value),
26
+ });
27
+
28
+ component = createComponent(
29
+ <View>
30
+ <SegmentedControl
31
+ values={values}
32
+ onValueChange={props.onValueChange}
33
+ selectedValue={props.selectedValue}
34
+ />
35
+ </View>,
36
+ );
37
+
38
+ testingLib = render(component);
39
+
40
+ const baseElement = await waitFor(() => testingLib.toJSON());
41
+ expect(baseElement).toBeTruthy();
42
+ });
43
+
44
+ it('should trigger `onValueChange` and change `selectedValue` props', () => {
45
+ props = createTestProps({
46
+ selectedValue: 'day',
47
+ onValueChange: (value: string | number) => (props.selectedValue = value),
48
+ });
49
+
50
+ component = createComponent(
51
+ <View>
52
+ <SegmentedControl
53
+ values={values}
54
+ onValueChange={props.onValueChange}
55
+ selectedValue={props.selectedValue}
56
+ />
57
+ </View>,
58
+ );
59
+
60
+ testingLib = render(component);
61
+
62
+ const secondOption = testingLib.getByTestId('segment-1');
63
+ expect(props.selectedValue).toEqual('day');
64
+
65
+ fireEvent.press(secondOption);
66
+ expect(props.selectedValue).toEqual('week');
67
+ });
68
+
69
+ it('should not trigger `onValueChange` when disabled', () => {
70
+ props = createTestProps({
71
+ selectedValue: 'day',
72
+ onValueChange: jest.fn(),
73
+ });
74
+
75
+ component = createComponent(
76
+ <View>
77
+ <SegmentedControl
78
+ values={values}
79
+ disabled={true}
80
+ onValueChange={props.onValueChange}
81
+ selectedValue={props.selectedValue}
82
+ />
83
+ </View>,
84
+ );
85
+
86
+ testingLib = render(component);
87
+
88
+ const secondOption = testingLib.getByTestId('segment-1');
89
+ fireEvent.press(secondOption);
90
+
91
+ expect(props.onValueChange).not.toHaveBeenCalled();
92
+ });
93
+ });
94
+
95
+ describe('[SegmentedControl] colors', () => {
96
+ const colors = [
97
+ 'primary',
98
+ 'secondary',
99
+ 'success',
100
+ 'info',
101
+ 'warning',
102
+ 'danger',
103
+ 'light',
104
+ ];
105
+
106
+ it('should render all colors', async () => {
107
+ component = createComponent(
108
+ <View>
109
+ {colors.map((color) => {
110
+ return (
111
+ <View key={color} style={{marginTop: 24}}>
112
+ <SegmentedControl
113
+ color={color as SegmentedControlColorType}
114
+ values={values}
115
+ selectedValue="day"
116
+ />
117
+ </View>
118
+ );
119
+ })}
120
+ </View>,
121
+ );
122
+
123
+ testingLib = render(component);
124
+
125
+ const baseElement = await waitFor(() => testingLib.toJSON());
126
+ expect(baseElement).toBeTruthy();
127
+ });
128
+ });
129
+
130
+ describe('[SegmentedControl] sizes', () => {
131
+ it('should render with small size', async () => {
132
+ component = createComponent(
133
+ <SegmentedControl
134
+ values={values}
135
+ selectedValue="day"
136
+ size="small"
137
+ />,
138
+ );
139
+
140
+ testingLib = render(component);
141
+
142
+ const baseElement = await waitFor(() => testingLib.toJSON());
143
+ expect(baseElement).toBeTruthy();
144
+ });
145
+
146
+ it('should render with medium size', async () => {
147
+ component = createComponent(
148
+ <SegmentedControl
149
+ values={values}
150
+ selectedValue="day"
151
+ size="medium"
152
+ />,
153
+ );
154
+
155
+ testingLib = render(component);
156
+
157
+ const baseElement = await waitFor(() => testingLib.toJSON());
158
+ expect(baseElement).toBeTruthy();
159
+ });
160
+
161
+ it('should render with large size', async () => {
162
+ component = createComponent(
163
+ <SegmentedControl
164
+ values={values}
165
+ selectedValue="day"
166
+ size="large"
167
+ />,
168
+ );
169
+
170
+ testingLib = render(component);
171
+
172
+ const baseElement = await waitFor(() => testingLib.toJSON());
173
+ expect(baseElement).toBeTruthy();
174
+ });
175
+
176
+ it('should render with custom numeric size', async () => {
177
+ component = createComponent(
178
+ <SegmentedControl
179
+ values={values}
180
+ selectedValue="day"
181
+ size={28}
182
+ />,
183
+ );
184
+
185
+ testingLib = render(component);
186
+
187
+ const baseElement = await waitFor(() => testingLib.toJSON());
188
+ expect(baseElement).toBeTruthy();
189
+ });
190
+ });
191
+
192
+ describe('[SegmentedControl] with different data lengths', () => {
193
+ it('should render with 2 segments', async () => {
194
+ const twoSegments = [
195
+ {value: 'first', text: 'First'},
196
+ {value: 'second', text: 'Second'},
197
+ ];
198
+ component = createComponent(
199
+ <SegmentedControl
200
+ values={twoSegments}
201
+ selectedValue="first"
202
+ />,
203
+ );
204
+
205
+ testingLib = render(component);
206
+
207
+ const baseElement = await waitFor(() => testingLib.toJSON());
208
+ expect(baseElement).toBeTruthy();
209
+ expect(testingLib.getByTestId('segment-0')).toBeTruthy();
210
+ expect(testingLib.getByTestId('segment-1')).toBeTruthy();
211
+ });
212
+
213
+ it('should render with 4 segments', async () => {
214
+ const fourSegments = [
215
+ {value: 1, text: 'One'},
216
+ {value: 2, text: 'Two'},
217
+ {value: 3, text: 'Three'},
218
+ {value: 4, text: 'Four'},
219
+ ];
220
+ component = createComponent(
221
+ <SegmentedControl
222
+ values={fourSegments}
223
+ selectedValue={1}
224
+ />,
225
+ );
226
+
227
+ testingLib = render(component);
228
+
229
+ const baseElement = await waitFor(() => testingLib.toJSON());
230
+ expect(baseElement).toBeTruthy();
231
+ expect(testingLib.getByTestId('segment-0')).toBeTruthy();
232
+ expect(testingLib.getByTestId('segment-1')).toBeTruthy();
233
+ expect(testingLib.getByTestId('segment-2')).toBeTruthy();
234
+ expect(testingLib.getByTestId('segment-3')).toBeTruthy();
235
+ });
236
+ });
237
+
238
+ describe('[SegmentedControl] custom borderRadius', () => {
239
+ it('should render with custom border radius', async () => {
240
+ component = createComponent(
241
+ <SegmentedControl
242
+ borderRadius={16}
243
+ values={values}
244
+ selectedValue="day"
245
+ />,
246
+ );
247
+
248
+ testingLib = render(component);
249
+
250
+ const baseElement = await waitFor(() => testingLib.toJSON());
251
+ expect(baseElement).toBeTruthy();
252
+ });
253
+ });
254
+
255
+ describe('[SegmentedControl] with ReactElement text', () => {
256
+ it('should render with React elements as text', async () => {
257
+ const valuesWithElements = [
258
+ {value: 'grid', text: <View><Text>Grid</Text></View>},
259
+ {value: 'list', text: <View><Text>List</Text></View>},
260
+ ];
261
+
262
+ component = createComponent(
263
+ <SegmentedControl
264
+ values={valuesWithElements}
265
+ selectedValue="grid"
266
+ />,
267
+ );
268
+
269
+ testingLib = render(component);
270
+
271
+ const baseElement = await waitFor(() => testingLib.toJSON());
272
+ expect(baseElement).toBeTruthy();
273
+ });
274
+ });
@@ -0,0 +1,229 @@
1
+ import React, {useCallback, useMemo, type ReactElement} from 'react';
2
+ import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
3
+ import {TouchableOpacity} from 'react-native';
4
+ import {styled, css} from 'kstyled';
5
+
6
+ import {useTheme} from '../../../providers/ThemeProvider';
7
+ import {Typography} from '../Typography/Typography';
8
+ import type {CpkTheme} from '../../../utils/theme';
9
+
10
+ export type SegmentedControlSizeType = 'small' | 'medium' | 'large' | number;
11
+ export type SegmentedControlColorType =
12
+ | 'primary'
13
+ | 'secondary'
14
+ | 'success'
15
+ | 'danger'
16
+ | 'warning'
17
+ | 'info'
18
+ | 'light';
19
+
20
+ export type SegmentedControlItem = {
21
+ value: string | number;
22
+ text: string | ReactElement;
23
+ };
24
+
25
+ type Styles = {
26
+ container?: StyleProp<ViewStyle>;
27
+ segment?: StyleProp<ViewStyle>;
28
+ selectedSegment?: StyleProp<ViewStyle>;
29
+ text?: StyleProp<TextStyle>;
30
+ selectedText?: StyleProp<TextStyle>;
31
+ };
32
+
33
+ export type SegmentedControlProps = {
34
+ testID?: string;
35
+ values: SegmentedControlItem[];
36
+ selectedValue: string | number;
37
+ onValueChange?: (value: string | number) => void;
38
+ color?: SegmentedControlColorType;
39
+ size?: SegmentedControlSizeType;
40
+ disabled?: boolean;
41
+ style?: StyleProp<ViewStyle>;
42
+ styles?: Styles;
43
+ borderRadius?: number;
44
+ };
45
+
46
+ const Container = styled.View`
47
+ overflow: hidden;
48
+ `;
49
+
50
+ // Calculate styles based on theme and props
51
+ const calculateStyles = ({
52
+ theme,
53
+ color,
54
+ size,
55
+ borderRadius,
56
+ styles,
57
+ }: {
58
+ theme: CpkTheme;
59
+ color: SegmentedControlColorType;
60
+ size: SegmentedControlSizeType;
61
+ borderRadius: number;
62
+ styles?: Styles;
63
+ }) => {
64
+ const padding =
65
+ typeof size === 'number'
66
+ ? `${size * 0.4}px ${size * 0.8}px`
67
+ : size === 'large'
68
+ ? '12px 16px'
69
+ : size === 'medium'
70
+ ? '8px 12px'
71
+ : size === 'small'
72
+ ? '6px 8px'
73
+ : '8px 12px';
74
+
75
+ return {
76
+ wrapper: [
77
+ css`
78
+ flex-direction: row;
79
+ background-color: ${theme.bg.paper};
80
+ border-top-color: ${theme.bg.disabled};
81
+ border-bottom-color: ${theme.bg.disabled};
82
+ border-left-color: ${theme.bg.disabled};
83
+ border-right-color: ${theme.bg.disabled};
84
+ border-width: 1px;
85
+ border-radius: ${borderRadius}px;
86
+ `,
87
+ ],
88
+ container: styles?.container,
89
+ segment: [
90
+ css`
91
+ flex: 1;
92
+ align-items: center;
93
+ justify-content: center;
94
+ padding: ${padding};
95
+ background-color: transparent;
96
+ `,
97
+ styles?.segment,
98
+ ],
99
+ selectedSegment: [
100
+ css`
101
+ background-color: ${theme.button[color].bg};
102
+ `,
103
+ styles?.selectedSegment,
104
+ ],
105
+ text: [
106
+ css`
107
+ color: ${theme.text.basic};
108
+ `,
109
+ styles?.text,
110
+ ],
111
+ selectedText: [
112
+ css`
113
+ color: ${theme.button[color].text};
114
+ font-weight: 600;
115
+ `,
116
+ styles?.selectedText,
117
+ ],
118
+ };
119
+ };
120
+
121
+ function SegmentedControlContainer({
122
+ testID,
123
+ values,
124
+ selectedValue,
125
+ onValueChange,
126
+ color = 'primary',
127
+ size = 'medium',
128
+ disabled = false,
129
+ style,
130
+ styles,
131
+ borderRadius = 8,
132
+ }: SegmentedControlProps): ReactElement {
133
+ const {theme} = useTheme();
134
+
135
+ // Memoize styles calculation
136
+ const compositeStyles = useMemo(
137
+ () =>
138
+ calculateStyles({
139
+ theme,
140
+ color,
141
+ size,
142
+ borderRadius,
143
+ styles,
144
+ }),
145
+ [theme, color, size, borderRadius, styles],
146
+ );
147
+
148
+ // Memoize segment press handler
149
+ const handlePress = useCallback(
150
+ (value: string | number) => {
151
+ if (!disabled) {
152
+ onValueChange?.(value);
153
+ }
154
+ },
155
+ [disabled, onValueChange],
156
+ );
157
+
158
+ // Memoize segments rendering
159
+ const segments = useMemo(
160
+ () => {
161
+ const cornerRadius = Math.max(borderRadius - 1, 0);
162
+
163
+ return values.map((item: SegmentedControlItem, index: number) => {
164
+ const isSelected = selectedValue === item.value;
165
+ const isFirst = index === 0;
166
+ const isLast = index === values.length - 1;
167
+
168
+ return (
169
+ <TouchableOpacity
170
+ key={`segment-${index}`}
171
+ testID={`segment-${index}`}
172
+ activeOpacity={0.7}
173
+ disabled={disabled}
174
+ onPress={() => handlePress(item.value)}
175
+ style={[
176
+ compositeStyles.segment,
177
+ isSelected && compositeStyles.selectedSegment,
178
+ {
179
+ borderTopLeftRadius: isFirst ? cornerRadius : 0,
180
+ borderBottomLeftRadius: isFirst ? cornerRadius : 0,
181
+ borderTopRightRadius: isLast ? cornerRadius : 0,
182
+ borderBottomRightRadius: isLast ? cornerRadius : 0,
183
+ borderRightWidth: isLast ? 0 : 1,
184
+ borderRightColor: theme.bg.disabled,
185
+ },
186
+ ]}
187
+ >
188
+ {typeof item.text === 'string' ? (
189
+ <Typography.Body2
190
+ style={[
191
+ compositeStyles.text,
192
+ isSelected && compositeStyles.selectedText,
193
+ ]}
194
+ >
195
+ {item.text}
196
+ </Typography.Body2>
197
+ ) : (
198
+ item.text
199
+ )}
200
+ </TouchableOpacity>
201
+ );
202
+ });
203
+ },
204
+ [
205
+ values,
206
+ selectedValue,
207
+ disabled,
208
+ handlePress,
209
+ compositeStyles,
210
+ borderRadius,
211
+ theme.bg.disabled,
212
+ ],
213
+ );
214
+
215
+ return (
216
+ <Container style={style} testID={testID}>
217
+ <Container style={[compositeStyles.wrapper, compositeStyles.container]}>
218
+ {segments}
219
+ </Container>
220
+ </Container>
221
+ );
222
+ }
223
+
224
+ // Export memoized component for better performance
225
+ export const SegmentedControl = React.memo(
226
+ SegmentedControlContainer,
227
+ );
228
+
229
+ export default SegmentedControl;
package/src/index.tsx CHANGED
@@ -23,6 +23,7 @@ export * from './components/uis/LoadingIndicator/LoadingIndicator';
23
23
  export * from './components/uis/PinchZoom/PinchZoom';
24
24
  export * from './components/uis/Rating/Rating';
25
25
  export * from './components/uis/RadioGroup/RadioGroup';
26
+ export * from './components/uis/SegmentedControl/SegmentedControl';
26
27
  export * from './components/uis/StatusbarBrightness/StatusBarBrightness';
27
28
  export * from './components/uis/SwitchToggle/SwitchToggle';
28
29
  export * from './components/uis/Typography/Typography';