@ledgerhq/lumen-ui-rnative 0.1.5 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ledgerhq/lumen-ui-rnative",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "license": "Apache-2.0",
5
5
  "keywords": [
6
6
  "react-native",
@@ -3,7 +3,7 @@ export declare function SegmentedControlButton({ value, children, icon: Icon, on
3
3
  export declare namespace SegmentedControlButton {
4
4
  var displayName: string;
5
5
  }
6
- export declare function SegmentedControl({ selectedValue, onSelectedChange, accessibilityLabel, children, ...props }: SegmentedControlProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function SegmentedControl({ selectedValue, onSelectedChange, accessibilityLabel, children, disabled, appearance, ...props }: SegmentedControlProps): import("react/jsx-runtime").JSX.Element;
7
7
  export declare namespace SegmentedControl {
8
8
  var displayName: string;
9
9
  }
@@ -1 +1 @@
1
- {"version":3,"file":"SegmentedControl.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/SegmentedControl/SegmentedControl.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,2BAA2B,EAC3B,qBAAqB,EACtB,MAAM,SAAS,CAAC;AAIjB,wBAAgB,sBAAsB,CAAC,EACrC,KAAK,EACL,QAAQ,EACR,IAAI,EAAE,IAAI,EACV,OAAO,EACP,GAAG,KAAK,EACT,EAAE,2BAA2B,2CAkC7B;yBAxCe,sBAAsB;;;AA4EtC,wBAAgB,gBAAgB,CAAC,EAC/B,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,qBAAqB,2CAwEvB;yBA9Ee,gBAAgB"}
1
+ {"version":3,"file":"SegmentedControl.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/SegmentedControl/SegmentedControl.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,2BAA2B,EAC3B,qBAAqB,EACtB,MAAM,SAAS,CAAC;AAMjB,wBAAgB,sBAAsB,CAAC,EACrC,KAAK,EACL,QAAQ,EACR,IAAI,EAAE,IAAI,EACV,OAAO,EACP,GAAG,KAAK,EACT,EAAE,2BAA2B,2CAqC7B;yBA3Ce,sBAAsB;;;AA2FtC,wBAAgB,gBAAgB,CAAC,EAC/B,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,QAAQ,EACR,QAAQ,EACR,UAAyB,EACzB,GAAG,KAAK,EACT,EAAE,qBAAqB,2CAkCvB;yBA1Ce,gBAAgB"}
@@ -1,24 +1,24 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useCallback, useEffect, useRef } from 'react';
3
- import Animated, { useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated';
2
+ import Animated from 'react-native-reanimated';
4
3
  import { useStyleSheet } from '../../../styles';
5
- import { durations, easingCurves } from '../../Animations/constants';
6
4
  import { Box, Pressable, Text } from '../Utility';
7
5
  import { SegmentedControlContextProvider, useSegmentedControlContext, } from './SegmentedControlContext';
8
- const ICON_SIZE = 16;
6
+ import { usePillLayout, useSegmentedControlSelectedIndex, } from './usePillLayout';
9
7
  export function SegmentedControlButton({ value, children, icon: Icon, onPress, ...props }) {
10
- const styles = useButtonStyles();
11
- const { selectedValue, onSelectedChange } = useSegmentedControlContext();
8
+ const { selectedValue, onSelectedChange, disabled } = useSegmentedControlContext();
12
9
  const selected = selectedValue === value;
10
+ const styles = useButtonStyles({ selected, disabled });
13
11
  function handlePress() {
14
- onSelectedChange(value);
15
- onPress?.();
12
+ if (!disabled) {
13
+ onSelectedChange(value);
14
+ onPress?.();
15
+ }
16
16
  }
17
- return (_jsx(Pressable, { onPress: handlePress, accessibilityState: { selected }, style: styles.button, ...props, children: _jsxs(Box, { style: styles.content, children: [Icon && (_jsx(Box, { style: styles.iconWrap, children: _jsx(Icon, { size: ICON_SIZE }) })), _jsx(Text, { typography: selected ? 'body2SemiBold' : 'body2', lx: { color: 'base' }, style: styles.label, children: children })] }) }));
17
+ return (_jsx(Pressable, { onPress: handlePress, disabled: disabled, accessibilityState: { selected, disabled }, style: styles.button, ...props, children: _jsxs(Box, { style: styles.content, children: [Icon && (_jsx(Box, { style: styles.iconWrap, children: _jsx(Icon, { size: 16, color: styles.textColor }) })), _jsx(Text, { typography: styles.typography, lx: { color: styles.textColor }, style: styles.label, children: children })] }) }));
18
18
  }
19
19
  SegmentedControlButton.displayName = 'SegmentedControlButton';
20
- function useButtonStyles() {
21
- return useStyleSheet((t) => ({
20
+ function useButtonStyles({ selected, disabled, }) {
21
+ const styles = useStyleSheet((t) => ({
22
22
  button: {
23
23
  flex: 1,
24
24
  flexDirection: 'row',
@@ -44,71 +44,44 @@ function useButtonStyles() {
44
44
  alignItems: 'center',
45
45
  },
46
46
  }), []);
47
+ const typography = selected
48
+ ? 'body2SemiBold'
49
+ : 'body2';
50
+ const textColor = selected && !disabled ? 'base' : 'muted';
51
+ return { ...styles, typography, textColor };
47
52
  }
48
- export function SegmentedControl({ selectedValue, onSelectedChange, accessibilityLabel, children, ...props }) {
49
- const styles = useRootStyles();
50
- const pillTranslateX = useSharedValue(0);
51
- const pillWidth = useSharedValue(0);
52
- const pillHeight = useSharedValue(0);
53
- const hasLayoutRef = useRef(false);
54
- const getSelectedIndex = useCallback(() => {
55
- return React.Children.toArray(children).findIndex((child) => {
56
- if (React.isValidElement(child) && child.props != null) {
57
- return child.props.value === selectedValue;
58
- }
59
- return false;
60
- });
61
- }, [selectedValue, children]);
62
- function onLayout(e) {
63
- const { width, height } = e.nativeEvent.layout;
64
- const count = React.Children.count(children);
65
- const slotWidth = count > 0 ? width / count : 0;
66
- pillWidth.value = slotWidth;
67
- pillHeight.value = height;
68
- if (!hasLayoutRef.current) {
69
- hasLayoutRef.current = true;
70
- const index = getSelectedIndex();
71
- if (index >= 0) {
72
- pillTranslateX.value = index * slotWidth;
73
- }
74
- }
75
- }
76
- useEffect(() => {
77
- if (!hasLayoutRef.current)
78
- return;
79
- const index = getSelectedIndex();
80
- if (index >= 0 && pillWidth.value > 0) {
81
- pillTranslateX.value = withTiming(index * pillWidth.value, {
82
- duration: durations['250'],
83
- easing: easingCurves.bezier.default,
84
- });
85
- }
86
- }, [pillWidth, pillTranslateX, getSelectedIndex]);
87
- const animatedPillStyle = useAnimatedStyle(() => ({
88
- transform: [{ translateX: pillTranslateX.value }],
89
- width: pillWidth.value,
90
- height: pillHeight.value,
91
- }), [pillTranslateX, pillWidth, pillHeight]);
92
- return (_jsx(SegmentedControlContextProvider, { value: { selectedValue, onSelectedChange }, children: _jsxs(Box, { accessibilityRole: 'radiogroup', accessibilityLabel: accessibilityLabel, onLayout: onLayout, style: styles.container, ...props, children: [children, _jsx(Animated.View, { style: [styles.pill, animatedPillStyle], pointerEvents: 'none' })] }) }));
53
+ export function SegmentedControl({ selectedValue, onSelectedChange, accessibilityLabel, children, disabled, appearance = 'background', ...props }) {
54
+ const styles = useRootStyles({
55
+ disabled: Boolean(disabled),
56
+ appearance,
57
+ });
58
+ const selectedIndex = useSegmentedControlSelectedIndex(selectedValue, children);
59
+ const { onLayout, animatedPillStyle } = usePillLayout({
60
+ selectedIndex,
61
+ children,
62
+ });
63
+ return (_jsx(SegmentedControlContextProvider, { value: { selectedValue, onSelectedChange, disabled }, children: _jsxs(Box, { accessibilityRole: 'radiogroup', accessibilityLabel: accessibilityLabel, accessibilityState: { disabled }, onLayout: onLayout, style: styles.container, ...props, children: [children, _jsx(Animated.View, { style: [styles.pill, animatedPillStyle], pointerEvents: 'none' })] }) }));
93
64
  }
94
65
  SegmentedControl.displayName = 'SegmentedControl';
95
- function useRootStyles() {
66
+ function useRootStyles({ disabled, appearance, }) {
96
67
  return useStyleSheet((t) => ({
97
68
  container: {
98
69
  flexDirection: 'row',
99
70
  alignItems: 'center',
100
71
  position: 'relative',
101
72
  width: '100%',
102
- borderRadius: t.borderRadius.full,
103
- backgroundColor: t.colors.bg.baseTransparent,
73
+ borderRadius: t.borderRadius.md,
74
+ backgroundColor: appearance === 'background' ? t.colors.bg.surface : 'transparent',
104
75
  },
105
76
  pill: {
106
77
  position: 'absolute',
107
78
  top: 0,
108
79
  left: 0,
109
80
  borderRadius: t.borderRadius.sm,
110
- backgroundColor: t.colors.bg.muted,
81
+ backgroundColor: disabled
82
+ ? t.colors.bg.baseTransparentPressed
83
+ : t.colors.bg.mutedTransparent,
111
84
  zIndex: 0,
112
85
  },
113
- }), []);
86
+ }), [disabled, appearance]);
114
87
  }
@@ -1,6 +1,7 @@
1
1
  export type SegmentedControlContextValue = {
2
2
  selectedValue: string;
3
3
  onSelectedChange: (value: string) => void;
4
+ disabled?: boolean;
4
5
  };
5
6
  declare const SegmentedControlContextProvider: import("react").FC<{
6
7
  children: import("react").ReactNode;
@@ -1 +1 @@
1
- {"version":3,"file":"SegmentedControlContext.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/SegmentedControl/SegmentedControlContext.tsx"],"names":[],"mappings":"AAEA,MAAM,MAAM,4BAA4B,GAAG;IACzC,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC3C,CAAC;AAEF,QAAA,MAAO,+BAA+B;;;EAC+B,CAAC;AAEtE,eAAO,MAAM,0BAA0B,oCAInC,CAAC;AAEL,OAAO,EAAE,+BAA+B,EAAE,CAAC"}
1
+ {"version":3,"file":"SegmentedControlContext.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/SegmentedControl/SegmentedControlContext.tsx"],"names":[],"mappings":"AAEA,MAAM,MAAM,4BAA4B,GAAG;IACzC,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,QAAA,MAAO,+BAA+B;;;EAC+B,CAAC;AAEtE,eAAO,MAAM,0BAA0B,oCAInC,CAAC;AAEL,OAAO,EAAE,+BAA+B,EAAE,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { ComponentType, ReactNode } from 'react';
2
- import { StyledPressableProps } from '../../../styles';
2
+ import { LumenTextStyle, StyledPressableProps } from '../../../styles';
3
3
  import { IconSize } from '../Icon';
4
4
  import { BoxProps } from '../Utility';
5
5
  export type SegmentedControlProps = {
@@ -11,17 +11,27 @@ export type SegmentedControlProps = {
11
11
  * Callback when the selected segment value changes.
12
12
  */
13
13
  onSelectedChange: (value: string) => void;
14
+ /**
15
+ * When true, the entire control is disabled (no interaction, selected uses muted styling).
16
+ */
17
+ disabled?: boolean;
18
+ /**
19
+ * Visual style of the control container: "background" shows the surface bg, "no-background" is transparent.
20
+ * @default 'background'
21
+ */
22
+ appearance?: 'background' | 'no-background';
14
23
  /**
15
24
  * Accessible label for the control (e.g. "File view").
16
25
  */
17
26
  accessibilityLabel?: string;
18
27
  /**
19
- * Segment buttons (SegmentedControlButton). Can be wrapped (e.g. in Tooltip).
28
+ * Segment buttons (SegmentedControlButton).
20
29
  */
21
30
  children: ReactNode;
22
31
  } & Omit<BoxProps, 'children'>;
23
32
  type IconComponent = ComponentType<{
24
33
  size?: IconSize;
34
+ color?: LumenTextStyle['color'];
25
35
  }>;
26
36
  export type SegmentedControlButtonProps = {
27
37
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/SegmentedControl/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,MAAM,MAAM,qBAAqB,GAAG;IAClC;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C;;OAEG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB,GAAG,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AAE/B,KAAK,aAAa,GAAG,aAAa,CAAC;IACjC,IAAI,CAAC,EAAE,QAAQ,CAAC;CACjB,CAAC,CAAC;AAEH,MAAM,MAAM,2BAA2B,GAAG;IACxC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IACpB;;OAEG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB,GAAG,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/SegmentedControl/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,MAAM,MAAM,qBAAqB,GAAG;IAClC;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,YAAY,GAAG,eAAe,CAAC;IAC5C;;OAEG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB,GAAG,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AAE/B,KAAK,aAAa,GAAG,aAAa,CAAC;IACjC,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;CACjC,CAAC,CAAC;AAEH,MAAM,MAAM,2BAA2B,GAAG;IACxC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IACpB;;OAEG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB,GAAG,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC"}
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { LayoutChangeEvent } from 'react-native';
3
+ export declare function useSegmentedControlSelectedIndex(selectedValue: string, children: React.ReactNode): number;
4
+ type UsePillLayoutParams = {
5
+ selectedIndex: number;
6
+ children: React.ReactNode;
7
+ };
8
+ export declare function usePillLayout({ selectedIndex, children, }: UsePillLayoutParams): {
9
+ onLayout: (e: LayoutChangeEvent) => void;
10
+ animatedPillStyle: {
11
+ transform: {
12
+ translateX: number;
13
+ }[];
14
+ width: number;
15
+ height: number;
16
+ };
17
+ };
18
+ export {};
19
+ //# sourceMappingURL=usePillLayout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePillLayout.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/SegmentedControl/usePillLayout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAqC,MAAM,OAAO,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAQjD,wBAAgB,gCAAgC,CAC9C,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,KAAK,CAAC,SAAS,GACxB,MAAM,CAWR;AAED,KAAK,mBAAmB,GAAG;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAEF,wBAAgB,aAAa,CAAC,EAC5B,aAAa,EACb,QAAQ,GACT,EAAE,mBAAmB;kBAMC,iBAAiB,KAAG,IAAI;;;;;;;;EAoC9C"}
@@ -0,0 +1,46 @@
1
+ import React, { useEffect, useMemo, useRef } from 'react';
2
+ import { useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated';
3
+ import { durations, easingCurves } from '../../Animations/constants';
4
+ export function useSegmentedControlSelectedIndex(selectedValue, children) {
5
+ return useMemo(() => React.Children.toArray(children).findIndex((child) => {
6
+ if (React.isValidElement(child) && child.props != null) {
7
+ return child.props.value === selectedValue;
8
+ }
9
+ return false;
10
+ }), [selectedValue, children]);
11
+ }
12
+ export function usePillLayout({ selectedIndex, children, }) {
13
+ const pillTranslateX = useSharedValue(0);
14
+ const pillWidth = useSharedValue(0);
15
+ const pillHeight = useSharedValue(0);
16
+ const hasLayoutRef = useRef(false);
17
+ const onLayout = (e) => {
18
+ const { width, height } = e.nativeEvent.layout;
19
+ const count = React.Children.count(children);
20
+ const slotWidth = count > 0 ? width / count : 0;
21
+ pillWidth.value = slotWidth;
22
+ pillHeight.value = height;
23
+ if (!hasLayoutRef.current) {
24
+ hasLayoutRef.current = true;
25
+ if (selectedIndex >= 0) {
26
+ pillTranslateX.value = selectedIndex * slotWidth;
27
+ }
28
+ }
29
+ };
30
+ useEffect(() => {
31
+ if (!hasLayoutRef.current)
32
+ return;
33
+ if (selectedIndex >= 0 && pillWidth.value > 0) {
34
+ pillTranslateX.value = withTiming(selectedIndex * pillWidth.value, {
35
+ duration: durations['250'],
36
+ easing: easingCurves.bezier.default,
37
+ });
38
+ }
39
+ }, [selectedIndex, pillWidth, pillTranslateX]);
40
+ const animatedPillStyle = useAnimatedStyle(() => ({
41
+ transform: [{ translateX: pillTranslateX.value }],
42
+ width: pillWidth.value,
43
+ height: pillHeight.value,
44
+ }), [pillTranslateX, pillWidth, pillHeight]);
45
+ return { onLayout, animatedPillStyle };
46
+ }
@@ -21,10 +21,10 @@ const useStyles = ({ disabled, pressed, isFull, }) => {
21
21
  gap: t.spacings.s8,
22
22
  padding: t.spacings.s12,
23
23
  borderRadius: t.borderRadius.md,
24
- backgroundColor: t.colors.bg.muted,
24
+ backgroundColor: t.colors.bg.surface,
25
25
  },
26
26
  isFull && { width: t.sizes.full },
27
- pressed && !disabled && { backgroundColor: t.colors.bg.mutedPressed },
27
+ pressed && !disabled && { backgroundColor: t.colors.bg.surfacePressed },
28
28
  disabled && { backgroundColor: t.colors.bg.disabled },
29
29
  ]),
30
30
  label: StyleSheet.flatten([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ledgerhq/lumen-ui-rnative",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "license": "Apache-2.0",
5
5
  "keywords": [
6
6
  "react-native",
@@ -37,7 +37,7 @@
37
37
  "peerDependencies": {
38
38
  "@types/react": "^19.0.0",
39
39
  "@gorhom/bottom-sheet": "^5.0.0",
40
- "@ledgerhq/lumen-design-core": "0.1.1",
40
+ "@ledgerhq/lumen-design-core": "0.1.2",
41
41
  "react": "^19.0.0",
42
42
  "react-native": "~0.79.7",
43
43
  "react-native-reanimated": "^3.0.0",
@@ -192,7 +192,7 @@ export const VariantsShowcase: Story = {
192
192
  </ListItemContent>
193
193
  </ListItemLeading>
194
194
  <ListItemTrailing>
195
- <ListItemIcon icon={ChevronRight} />
195
+ <ListItemIcon color='muted' icon={ChevronRight} />
196
196
  </ListItemTrailing>
197
197
  </ListItem>
198
198
 
@@ -205,7 +205,7 @@ export const VariantsShowcase: Story = {
205
205
  </ListItemContent>
206
206
  </ListItemLeading>
207
207
  <ListItemTrailing>
208
- <ListItemIcon icon={ChevronRight} />
208
+ <ListItemIcon color='muted' icon={ChevronRight} />
209
209
  </ListItemTrailing>
210
210
  </ListItem>
211
211
 
@@ -284,7 +284,7 @@ export const DisabledState: Story = {
284
284
  </ListItemContent>
285
285
  </ListItemLeading>
286
286
  <ListItemTrailing>
287
- <ListItemIcon icon={ChevronRight} />
287
+ <ListItemIcon color='muted' icon={ChevronRight} />
288
288
  </ListItemTrailing>
289
289
  </ListItem>
290
290
 
@@ -357,7 +357,7 @@ export const ResponsiveLayout: Story = {
357
357
  </ListItemContent>
358
358
  </ListItemLeading>
359
359
  <ListItemTrailing>
360
- <ListItemIcon icon={ChevronRight} />
360
+ <ListItemIcon color='muted' icon={ChevronRight} />
361
361
  </ListItemTrailing>
362
362
  </ListItem>
363
363
 
@@ -375,7 +375,7 @@ export const ResponsiveLayout: Story = {
375
375
  </ListItemContent>
376
376
  </ListItemLeading>
377
377
  <ListItemTrailing>
378
- <ListItemIcon icon={ChevronRight} />
378
+ <ListItemIcon color='muted' icon={ChevronRight} />
379
379
  </ListItemTrailing>
380
380
  </ListItem>
381
381
 
@@ -25,6 +25,7 @@ SegmentedControl is a tab bar–style component for switching between mutually e
25
25
  - **Segments**: Individual options the user can select.
26
26
  - **Selected state**: The active segment (sliding pill + semi-bold label).
27
27
  - **Optional icon**: Icon to the left of the label (from Symbols).
28
+ - **Appearance**: Use `appearance="background"` (default) for a surface background, or `appearance="no-background"` for a transparent container.
28
29
 
29
30
  ## Properties
30
31
 
@@ -55,44 +56,6 @@ SegmentedControl lays out segments in a horizontal row with equal width per segm
55
56
 
56
57
  Use the library as part of `@ledgerhq/lumen-ui-rnative`. See the [Setup Guide →](?path=/docs/getting-started-setup--docs).
57
58
 
58
- ## SegmentedControlButton
59
-
60
- Use as a direct child of SegmentedControl (or inside wrappers such as Tooltip). Required: value (string) and children (label). Optional: icon (component from symbols), onPress (runs in addition to parent onSelectedChange).
61
-
62
- <div className='my-24 overflow-hidden rounded-lg'>
63
- <table className='w-full'>
64
- <thead>
65
- <tr className='border-b border-muted bg-muted'>
66
- <th className='p-12 text-left text-on-accent body-2'>Prop</th>
67
- <th className='p-12 text-left text-on-accent body-2'>Type</th>
68
- <th className='p-12 text-left text-on-accent body-2'>Description</th>
69
- </tr>
70
- </thead>
71
- <tbody className='bg-canvas'>
72
- <tr className='border-b border-muted'>
73
- <td className='text-accent p-12'>value</td>
74
- <td className='p-12 text-muted'>string</td>
75
- <td className='p-12 text-muted'>Unique value for this segment (e.g. "send", "receive")</td>
76
- </tr>
77
- <tr className='border-b border-muted'>
78
- <td className='text-accent p-12'>children</td>
79
- <td className='p-12 text-muted'>ReactNode</td>
80
- <td className='p-12 text-muted'>Button label (e.g. "Send", "Tokens")</td>
81
- </tr>
82
- <tr className='border-b border-muted'>
83
- <td className='text-accent p-12'>icon</td>
84
- <td className='p-12 text-muted'>ComponentType</td>
85
- <td className='p-12 text-muted'>Optional icon to the left of the label (from Symbols)</td>
86
- </tr>
87
- <tr>
88
- <td className='text-accent p-12'>onPress</td>
89
- <td className='p-12 text-muted'>() =&gt; void</td>
90
- <td className='p-12 text-muted'>Optional callback when the button is pressed (in addition to parent onSelectedChange)</td>
91
- </tr>
92
- </tbody>
93
- </table>
94
- </div>
95
-
96
59
  ## Basic Usage
97
60
 
98
61
  Control selected value in state at the top level and pass it to SegmentedControl. Buttons use value to identify themselves; selected state is derived from context.
@@ -17,34 +17,27 @@ const meta = {
17
17
  argTypes: {
18
18
  onSelectedChange: {
19
19
  action: 'change',
20
- description: 'Callback when the selected value changes',
21
- table: {
22
- type: { summary: '(value: string) => void' },
23
- },
20
+ },
21
+ disabled: {
22
+ control: 'boolean',
23
+ },
24
+ appearance: {
25
+ options: ['background', 'no-background'],
26
+ control: 'radio',
24
27
  },
25
28
  accessibilityLabel: {
26
29
  control: 'text',
27
- description: 'Accessible label for the control',
28
- table: {
29
- type: { summary: 'string' },
30
- },
31
30
  },
32
31
  selectedValue: {
33
32
  control: 'text',
34
- description:
35
- 'The value of the currently selected segment (drives the sliding pill)',
36
- table: {
37
- type: { summary: 'string' },
38
- },
39
33
  },
40
34
  children: {
41
35
  control: false,
42
- description: 'SegmentedControlButton elements',
43
- table: {
44
- type: { summary: 'ReactNode' },
45
- },
46
36
  },
47
37
  },
38
+ args: {
39
+ appearance: 'background',
40
+ },
48
41
  } satisfies Meta<typeof SegmentedControl>;
49
42
 
50
43
  export default meta;
@@ -52,12 +45,13 @@ type Story = StoryObj<typeof meta>;
52
45
 
53
46
  export const Base: Story = {
54
47
  args: {} as React.ComponentProps<typeof SegmentedControl>,
55
- render: () => {
48
+ render: (args) => {
56
49
  const [state, setState] = useState('send');
57
50
 
58
51
  return (
59
52
  <Box lx={{ width: 's256' }}>
60
53
  <SegmentedControl
54
+ {...args}
61
55
  selectedValue={state}
62
56
  onSelectedChange={setState}
63
57
  accessibilityLabel='Transaction type'
@@ -75,11 +69,12 @@ export const Base: Story = {
75
69
 
76
70
  export const WithIcons: Story = {
77
71
  args: {} as React.ComponentProps<typeof SegmentedControl>,
78
- render: () => {
72
+ render: (args) => {
79
73
  const [state, setState] = useState('tokens');
80
74
 
81
75
  return (
82
76
  <SegmentedControl
77
+ {...args}
83
78
  selectedValue={state}
84
79
  onSelectedChange={setState}
85
80
  accessibilityLabel='Asset section'
@@ -100,3 +95,24 @@ export const WithIcons: Story = {
100
95
  );
101
96
  },
102
97
  };
98
+
99
+ export const Disabled: Story = {
100
+ args: {} as React.ComponentProps<typeof SegmentedControl>,
101
+ render: (args) => (
102
+ <Box lx={{ width: 's256' }}>
103
+ <SegmentedControl
104
+ {...args}
105
+ selectedValue='receive'
106
+ onSelectedChange={() => {
107
+ /* empty */
108
+ }}
109
+ accessibilityLabel='Transaction type (disabled)'
110
+ disabled
111
+ >
112
+ <SegmentedControlButton value='send'>Send</SegmentedControlButton>
113
+ <SegmentedControlButton value='receive'>Receive</SegmentedControlButton>
114
+ <SegmentedControlButton value='buy'>Buy</SegmentedControlButton>
115
+ </SegmentedControl>
116
+ </Box>
117
+ ),
118
+ };
@@ -1,12 +1,6 @@
1
- import React, { useCallback, useEffect, useRef } from 'react';
2
- import { LayoutChangeEvent } from 'react-native';
3
- import Animated, {
4
- useAnimatedStyle,
5
- useSharedValue,
6
- withTiming,
7
- } from 'react-native-reanimated';
1
+ import Animated from 'react-native-reanimated';
8
2
  import { useStyleSheet } from '../../../styles';
9
- import { durations, easingCurves } from '../../Animations/constants';
3
+ import type { LumenTextStyle, LumenTypographyTokenName } from '../../../styles';
10
4
  import { Box, Pressable, Text } from '../Utility';
11
5
  import {
12
6
  SegmentedControlContextProvider,
@@ -16,8 +10,10 @@ import type {
16
10
  SegmentedControlButtonProps,
17
11
  SegmentedControlProps,
18
12
  } from './types';
19
-
20
- const ICON_SIZE = 16;
13
+ import {
14
+ usePillLayout,
15
+ useSegmentedControlSelectedIndex,
16
+ } from './usePillLayout';
21
17
 
22
18
  export function SegmentedControlButton({
23
19
  value,
@@ -26,32 +22,35 @@ export function SegmentedControlButton({
26
22
  onPress,
27
23
  ...props
28
24
  }: SegmentedControlButtonProps) {
29
- const styles = useButtonStyles();
30
- const { selectedValue, onSelectedChange } = useSegmentedControlContext();
31
-
25
+ const { selectedValue, onSelectedChange, disabled } =
26
+ useSegmentedControlContext();
32
27
  const selected = selectedValue === value;
28
+ const styles = useButtonStyles({ selected, disabled });
33
29
 
34
30
  function handlePress() {
35
- onSelectedChange(value);
36
- onPress?.();
31
+ if (!disabled) {
32
+ onSelectedChange(value);
33
+ onPress?.();
34
+ }
37
35
  }
38
36
 
39
37
  return (
40
38
  <Pressable
41
39
  onPress={handlePress}
42
- accessibilityState={{ selected }}
40
+ disabled={disabled}
41
+ accessibilityState={{ selected, disabled }}
43
42
  style={styles.button}
44
43
  {...props}
45
44
  >
46
45
  <Box style={styles.content}>
47
46
  {Icon && (
48
47
  <Box style={styles.iconWrap}>
49
- <Icon size={ICON_SIZE} />
48
+ <Icon size={16} color={styles.textColor} />
50
49
  </Box>
51
50
  )}
52
51
  <Text
53
- typography={selected ? 'body2SemiBold' : 'body2'}
54
- lx={{ color: 'base' }}
52
+ typography={styles.typography}
53
+ lx={{ color: styles.textColor }}
55
54
  style={styles.label}
56
55
  >
57
56
  {children}
@@ -63,8 +62,14 @@ export function SegmentedControlButton({
63
62
 
64
63
  SegmentedControlButton.displayName = 'SegmentedControlButton';
65
64
 
66
- function useButtonStyles() {
67
- return useStyleSheet(
65
+ function useButtonStyles({
66
+ selected,
67
+ disabled,
68
+ }: {
69
+ selected: boolean;
70
+ disabled?: boolean;
71
+ }) {
72
+ const styles = useStyleSheet(
68
73
  (t) => ({
69
74
  button: {
70
75
  flex: 1,
@@ -93,6 +98,12 @@ function useButtonStyles() {
93
98
  }),
94
99
  [],
95
100
  );
101
+ const typography: LumenTypographyTokenName = selected
102
+ ? 'body2SemiBold'
103
+ : 'body2';
104
+ const textColor: LumenTextStyle['color'] =
105
+ selected && !disabled ? 'base' : 'muted';
106
+ return { ...styles, typography, textColor };
96
107
  }
97
108
 
98
109
  export function SegmentedControl({
@@ -100,67 +111,31 @@ export function SegmentedControl({
100
111
  onSelectedChange,
101
112
  accessibilityLabel,
102
113
  children,
114
+ disabled,
115
+ appearance = 'background',
103
116
  ...props
104
117
  }: SegmentedControlProps) {
105
- const styles = useRootStyles();
106
- const pillTranslateX = useSharedValue(0);
107
- const pillWidth = useSharedValue(0);
108
- const pillHeight = useSharedValue(0);
109
- const hasLayoutRef = useRef(false);
110
-
111
- const getSelectedIndex = useCallback((): number => {
112
- return React.Children.toArray(children).findIndex((child) => {
113
- if (React.isValidElement(child) && child.props != null) {
114
- return (child.props as { value?: string }).value === selectedValue;
115
- }
116
- return false;
117
- });
118
- }, [selectedValue, children]);
119
-
120
- function onLayout(e: LayoutChangeEvent) {
121
- const { width, height } = e.nativeEvent.layout;
122
- const count = React.Children.count(children);
123
- const slotWidth = count > 0 ? width / count : 0;
124
-
125
- pillWidth.value = slotWidth;
126
- pillHeight.value = height;
127
-
128
- if (!hasLayoutRef.current) {
129
- hasLayoutRef.current = true;
130
- const index = getSelectedIndex();
131
- if (index >= 0) {
132
- pillTranslateX.value = index * slotWidth;
133
- }
134
- }
135
- }
136
-
137
- useEffect(() => {
138
- if (!hasLayoutRef.current) return;
139
- const index = getSelectedIndex();
140
- if (index >= 0 && pillWidth.value > 0) {
141
- pillTranslateX.value = withTiming(index * pillWidth.value, {
142
- duration: durations['250'],
143
- easing: easingCurves.bezier.default,
144
- });
145
- }
146
- }, [pillWidth, pillTranslateX, getSelectedIndex]);
147
-
148
- const animatedPillStyle = useAnimatedStyle(
149
- () => ({
150
- transform: [{ translateX: pillTranslateX.value }],
151
- width: pillWidth.value,
152
- height: pillHeight.value,
153
- }),
154
- [pillTranslateX, pillWidth, pillHeight],
118
+ const styles = useRootStyles({
119
+ disabled: Boolean(disabled),
120
+ appearance,
121
+ });
122
+ const selectedIndex = useSegmentedControlSelectedIndex(
123
+ selectedValue,
124
+ children,
155
125
  );
126
+ const { onLayout, animatedPillStyle } = usePillLayout({
127
+ selectedIndex,
128
+ children,
129
+ });
156
130
 
157
131
  return (
158
132
  <SegmentedControlContextProvider
159
- value={{ selectedValue, onSelectedChange }}
133
+ value={{ selectedValue, onSelectedChange, disabled }}
160
134
  >
161
135
  <Box
162
136
  accessibilityRole='radiogroup'
163
137
  accessibilityLabel={accessibilityLabel}
138
+ accessibilityState={{ disabled }}
164
139
  onLayout={onLayout}
165
140
  style={styles.container}
166
141
  {...props}
@@ -177,7 +152,13 @@ export function SegmentedControl({
177
152
 
178
153
  SegmentedControl.displayName = 'SegmentedControl';
179
154
 
180
- function useRootStyles() {
155
+ function useRootStyles({
156
+ disabled,
157
+ appearance,
158
+ }: {
159
+ disabled: boolean;
160
+ appearance: 'background' | 'no-background';
161
+ }) {
181
162
  return useStyleSheet(
182
163
  (t) => ({
183
164
  container: {
@@ -185,18 +166,21 @@ function useRootStyles() {
185
166
  alignItems: 'center',
186
167
  position: 'relative',
187
168
  width: '100%',
188
- borderRadius: t.borderRadius.full,
189
- backgroundColor: t.colors.bg.baseTransparent,
169
+ borderRadius: t.borderRadius.md,
170
+ backgroundColor:
171
+ appearance === 'background' ? t.colors.bg.surface : 'transparent',
190
172
  },
191
173
  pill: {
192
174
  position: 'absolute',
193
175
  top: 0,
194
176
  left: 0,
195
177
  borderRadius: t.borderRadius.sm,
196
- backgroundColor: t.colors.bg.muted,
178
+ backgroundColor: disabled
179
+ ? t.colors.bg.baseTransparentPressed
180
+ : t.colors.bg.mutedTransparent,
197
181
  zIndex: 0,
198
182
  },
199
183
  }),
200
- [],
184
+ [disabled, appearance],
201
185
  );
202
186
  }
@@ -3,6 +3,7 @@ import { createSafeContext } from '@ledgerhq/lumen-utils-shared';
3
3
  export type SegmentedControlContextValue = {
4
4
  selectedValue: string;
5
5
  onSelectedChange: (value: string) => void;
6
+ disabled?: boolean;
6
7
  };
7
8
 
8
9
  const [SegmentedControlContextProvider, _useSegmentedControlSafeContext] =
@@ -1,5 +1,5 @@
1
1
  import { ComponentType, ReactNode } from 'react';
2
- import { StyledPressableProps } from '../../../styles';
2
+ import { LumenTextStyle, StyledPressableProps } from '../../../styles';
3
3
  import { IconSize } from '../Icon';
4
4
  import { BoxProps } from '../Utility';
5
5
 
@@ -12,18 +12,28 @@ export type SegmentedControlProps = {
12
12
  * Callback when the selected segment value changes.
13
13
  */
14
14
  onSelectedChange: (value: string) => void;
15
+ /**
16
+ * When true, the entire control is disabled (no interaction, selected uses muted styling).
17
+ */
18
+ disabled?: boolean;
19
+ /**
20
+ * Visual style of the control container: "background" shows the surface bg, "no-background" is transparent.
21
+ * @default 'background'
22
+ */
23
+ appearance?: 'background' | 'no-background';
15
24
  /**
16
25
  * Accessible label for the control (e.g. "File view").
17
26
  */
18
27
  accessibilityLabel?: string;
19
28
  /**
20
- * Segment buttons (SegmentedControlButton). Can be wrapped (e.g. in Tooltip).
29
+ * Segment buttons (SegmentedControlButton).
21
30
  */
22
31
  children: ReactNode;
23
32
  } & Omit<BoxProps, 'children'>;
24
33
 
25
34
  type IconComponent = ComponentType<{
26
35
  size?: IconSize;
36
+ color?: LumenTextStyle['color'];
27
37
  }>;
28
38
 
29
39
  export type SegmentedControlButtonProps = {
@@ -0,0 +1,76 @@
1
+ import React, { useEffect, useMemo, useRef } from 'react';
2
+ import { LayoutChangeEvent } from 'react-native';
3
+ import {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withTiming,
7
+ } from 'react-native-reanimated';
8
+ import { durations, easingCurves } from '../../Animations/constants';
9
+
10
+ export function useSegmentedControlSelectedIndex(
11
+ selectedValue: string,
12
+ children: React.ReactNode,
13
+ ): number {
14
+ return useMemo(
15
+ () =>
16
+ React.Children.toArray(children).findIndex((child) => {
17
+ if (React.isValidElement(child) && child.props != null) {
18
+ return (child.props as { value?: string }).value === selectedValue;
19
+ }
20
+ return false;
21
+ }),
22
+ [selectedValue, children],
23
+ );
24
+ }
25
+
26
+ type UsePillLayoutParams = {
27
+ selectedIndex: number;
28
+ children: React.ReactNode;
29
+ };
30
+
31
+ export function usePillLayout({
32
+ selectedIndex,
33
+ children,
34
+ }: UsePillLayoutParams) {
35
+ const pillTranslateX = useSharedValue(0);
36
+ const pillWidth = useSharedValue(0);
37
+ const pillHeight = useSharedValue(0);
38
+ const hasLayoutRef = useRef(false);
39
+
40
+ const onLayout = (e: LayoutChangeEvent): void => {
41
+ const { width, height } = e.nativeEvent.layout;
42
+ const count = React.Children.count(children);
43
+ const slotWidth = count > 0 ? width / count : 0;
44
+
45
+ pillWidth.value = slotWidth;
46
+ pillHeight.value = height;
47
+
48
+ if (!hasLayoutRef.current) {
49
+ hasLayoutRef.current = true;
50
+ if (selectedIndex >= 0) {
51
+ pillTranslateX.value = selectedIndex * slotWidth;
52
+ }
53
+ }
54
+ };
55
+
56
+ useEffect(() => {
57
+ if (!hasLayoutRef.current) return;
58
+ if (selectedIndex >= 0 && pillWidth.value > 0) {
59
+ pillTranslateX.value = withTiming(selectedIndex * pillWidth.value, {
60
+ duration: durations['250'],
61
+ easing: easingCurves.bezier.default,
62
+ });
63
+ }
64
+ }, [selectedIndex, pillWidth, pillTranslateX]);
65
+
66
+ const animatedPillStyle = useAnimatedStyle(
67
+ () => ({
68
+ transform: [{ translateX: pillTranslateX.value }],
69
+ width: pillWidth.value,
70
+ height: pillHeight.value,
71
+ }),
72
+ [pillTranslateX, pillWidth, pillHeight],
73
+ );
74
+
75
+ return { onLayout, animatedPillStyle };
76
+ }
@@ -38,10 +38,10 @@ const useStyles = ({
38
38
  gap: t.spacings.s8,
39
39
  padding: t.spacings.s12,
40
40
  borderRadius: t.borderRadius.md,
41
- backgroundColor: t.colors.bg.muted,
41
+ backgroundColor: t.colors.bg.surface,
42
42
  },
43
43
  isFull && { width: t.sizes.full },
44
- pressed && !disabled && { backgroundColor: t.colors.bg.mutedPressed },
44
+ pressed && !disabled && { backgroundColor: t.colors.bg.surfacePressed },
45
45
  disabled && { backgroundColor: t.colors.bg.disabled },
46
46
  ]),
47
47
  label: StyleSheet.flatten([