@ledgerhq/lumen-ui-rnative 0.1.1 → 0.1.3

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 (101) hide show
  1. package/dist/package.json +3 -3
  2. package/dist/src/i18n/locales/de.json +3 -0
  3. package/dist/src/i18n/locales/en.json +3 -0
  4. package/dist/src/i18n/locales/es.json +3 -0
  5. package/dist/src/i18n/locales/fr.json +3 -0
  6. package/dist/src/i18n/locales/ja.json +3 -0
  7. package/dist/src/i18n/locales/ko.json +3 -0
  8. package/dist/src/i18n/locales/pt.json +3 -0
  9. package/dist/src/i18n/locales/ru.json +3 -0
  10. package/dist/src/i18n/locales/th.json +3 -0
  11. package/dist/src/i18n/locales/tr.json +3 -0
  12. package/dist/src/i18n/locales/zh.json +3 -0
  13. package/dist/src/lib/Animations/constants.d.ts +28 -0
  14. package/dist/src/lib/Animations/constants.d.ts.map +1 -0
  15. package/dist/src/lib/Animations/constants.js +27 -0
  16. package/dist/src/lib/Animations/index.d.ts +1 -0
  17. package/dist/src/lib/Animations/index.d.ts.map +1 -1
  18. package/dist/src/lib/Animations/index.js +1 -0
  19. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.d.ts +1 -1
  20. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.d.ts.map +1 -1
  21. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.js +76 -5
  22. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.d.ts +1 -0
  23. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.d.ts.map +1 -1
  24. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.js +25 -2
  25. package/dist/src/lib/Components/AmountDisplay/types.d.ts +20 -25
  26. package/dist/src/lib/Components/AmountDisplay/types.d.ts.map +1 -1
  27. package/dist/src/lib/Components/AmountDisplay/types.js +1 -1
  28. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.d.ts +10 -0
  29. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.d.ts.map +1 -0
  30. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.js +114 -0
  31. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.stories.d.ts +58 -0
  32. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.stories.d.ts.map +1 -0
  33. package/dist/src/lib/Components/SegmentedControl/SegmentedControl.stories.js +61 -0
  34. package/dist/src/lib/Components/SegmentedControl/SegmentedControlContext.d.ts +11 -0
  35. package/dist/src/lib/Components/SegmentedControl/SegmentedControlContext.d.ts.map +1 -0
  36. package/dist/src/lib/Components/SegmentedControl/SegmentedControlContext.js +7 -0
  37. package/dist/src/lib/Components/SegmentedControl/index.d.ts +3 -0
  38. package/dist/src/lib/Components/SegmentedControl/index.d.ts.map +1 -0
  39. package/dist/src/lib/Components/SegmentedControl/index.js +1 -0
  40. package/dist/src/lib/Components/SegmentedControl/types.d.ts +45 -0
  41. package/dist/src/lib/Components/SegmentedControl/types.d.ts.map +1 -0
  42. package/dist/src/lib/Components/SegmentedControl/types.js +1 -0
  43. package/dist/src/lib/Components/TabBar/TabBar.js +1 -0
  44. package/dist/src/lib/Components/TabBar/types.d.ts +0 -1
  45. package/dist/src/lib/Components/TabBar/types.d.ts.map +1 -1
  46. package/dist/src/lib/Components/index.d.ts +1 -0
  47. package/dist/src/lib/Components/index.d.ts.map +1 -1
  48. package/dist/src/lib/Components/index.js +1 -0
  49. package/dist/src/styles/theme/resolvers/resolveFontWeights.js +1 -1
  50. package/package.json +3 -3
  51. package/src/i18n/locales/de.json +3 -0
  52. package/src/i18n/locales/en.json +3 -0
  53. package/src/i18n/locales/es.json +3 -0
  54. package/src/i18n/locales/fr.json +3 -0
  55. package/src/i18n/locales/ja.json +3 -0
  56. package/src/i18n/locales/ko.json +3 -0
  57. package/src/i18n/locales/pt.json +3 -0
  58. package/src/i18n/locales/ru.json +3 -0
  59. package/src/i18n/locales/th.json +3 -0
  60. package/src/i18n/locales/tr.json +3 -0
  61. package/src/i18n/locales/zh.json +3 -0
  62. package/src/lib/Animations/constants.ts +31 -0
  63. package/src/lib/Animations/index.ts +1 -0
  64. package/src/lib/Components/AmountDisplay/AmountDisplay.mdx +7 -1
  65. package/src/lib/Components/AmountDisplay/AmountDisplay.stories.tsx +29 -2
  66. package/src/lib/Components/AmountDisplay/AmountDisplay.test.tsx +101 -51
  67. package/src/lib/Components/AmountDisplay/AmountDisplay.tsx +175 -24
  68. package/src/lib/Components/AmountDisplay/types.ts +22 -25
  69. package/src/lib/Components/SegmentedControl/SegmentedControl.mdx +159 -0
  70. package/src/lib/Components/SegmentedControl/SegmentedControl.stories.tsx +102 -0
  71. package/src/lib/Components/SegmentedControl/SegmentedControl.test.tsx +57 -0
  72. package/src/lib/Components/SegmentedControl/SegmentedControl.tsx +202 -0
  73. package/src/lib/Components/SegmentedControl/SegmentedControlContext.tsx +17 -0
  74. package/src/lib/Components/SegmentedControl/index.ts +2 -0
  75. package/src/lib/Components/SegmentedControl/types.ts +46 -0
  76. package/src/lib/Components/TabBar/TabBar.tsx +1 -0
  77. package/src/lib/Components/TabBar/types.ts +0 -1
  78. package/src/lib/Components/index.ts +1 -0
  79. package/src/styles/theme/createStylesheetTheme.test.ts +1 -1
  80. package/src/styles/theme/resolvers/resolveFontWeights.test.ts +9 -6
  81. package/src/styles/theme/resolvers/resolveFontWeights.ts +1 -1
  82. package/dist/src/lib/Components/Banner/Banner.figma.d.ts +0 -2
  83. package/dist/src/lib/Components/Banner/Banner.figma.d.ts.map +0 -1
  84. package/dist/src/lib/Components/Banner/Banner.figma.js +0 -45
  85. package/dist/src/lib/Components/Checkbox/Checkbox.figma.d.ts +0 -2
  86. package/dist/src/lib/Components/Checkbox/Checkbox.figma.d.ts.map +0 -1
  87. package/dist/src/lib/Components/Checkbox/Checkbox.figma.js +0 -32
  88. package/dist/src/lib/Components/InteractiveIcon/InteractiveIcon.figma.d.ts +0 -2
  89. package/dist/src/lib/Components/InteractiveIcon/InteractiveIcon.figma.d.ts.map +0 -1
  90. package/dist/src/lib/Components/InteractiveIcon/InteractiveIcon.figma.js +0 -26
  91. package/dist/src/lib/Components/Switch/Switch.figma.d.ts +0 -2
  92. package/dist/src/lib/Components/Switch/Switch.figma.d.ts.map +0 -1
  93. package/dist/src/lib/Components/Switch/Switch.figma.js +0 -32
  94. package/dist/src/lib/Components/Tile/Tile.figma.d.ts +0 -2
  95. package/dist/src/lib/Components/Tile/Tile.figma.d.ts.map +0 -1
  96. package/dist/src/lib/Components/Tile/Tile.figma.js +0 -28
  97. package/src/lib/Components/Banner/Banner.figma.tsx +0 -59
  98. package/src/lib/Components/Checkbox/Checkbox.figma.tsx +0 -49
  99. package/src/lib/Components/InteractiveIcon/InteractiveIcon.figma.tsx +0 -42
  100. package/src/lib/Components/Switch/Switch.figma.tsx +0 -47
  101. package/src/lib/Components/Tile/Tile.figma.tsx +0 -53
@@ -0,0 +1,159 @@
1
+ import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks';
2
+ import * as SegmentedControlStories from './SegmentedControl.stories';
3
+ import { SegmentedControl } from './SegmentedControl';
4
+ import { CustomTabs, Tab } from '../../../../.storybook/components';
5
+ import CommonRulesDoAndDont from '../../../../.storybook/components/DoVsDont/CommonRulesDoAndDont.mdx';
6
+ import { Box } from '../Utility';
7
+
8
+ <Meta title='Navigation/SegmentedControl' of={SegmentedControlStories} />
9
+
10
+ # 📑 SegmentedControl
11
+
12
+ <CustomTabs>
13
+ <Tab label="Overview">
14
+
15
+ ## Introduction
16
+
17
+ SegmentedControl is a tab bar–style component for switching between mutually exclusive options. Use it when the user needs to choose one of a small set of choices (e.g. transaction type, asset section).
18
+
19
+ > View in [Figma](https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7/2.-Components-Library?node-id=1170-2345&m=dev).
20
+
21
+ ## Anatomy
22
+
23
+ <Canvas of={SegmentedControlStories.Base} />
24
+
25
+ - **Segments**: Individual options the user can select.
26
+ - **Selected state**: The active segment (sliding pill + semi-bold label).
27
+ - **Optional icon**: Icon to the left of the label (from Symbols).
28
+
29
+ ## Properties
30
+
31
+ <Canvas of={SegmentedControlStories.Base} />
32
+ <Controls of={SegmentedControlStories.Base} />
33
+
34
+ ### Labels only vs with icons
35
+
36
+ You can use segments with text only (Base), or add an icon to each button (WithIcons). Use one approach consistently for all segments in a single control.
37
+
38
+ <Canvas of={SegmentedControlStories.Base} />
39
+
40
+ <Canvas of={SegmentedControlStories.WithIcons} />
41
+
42
+ ## Responsive Layout
43
+
44
+ SegmentedControl lays out segments in a horizontal row with equal width per segment. The sliding pill animates to the selected segment.
45
+
46
+ ## Accessibility
47
+
48
+ - Use accessibilityLabel on SegmentedControl to describe the control (e.g. "Transaction type", "Asset section").
49
+ - Each segment exposes accessibilityState with a selected value for screen readers.
50
+
51
+ </Tab>
52
+ <Tab label="Implementation">
53
+
54
+ ## Installation
55
+
56
+ Use the library as part of `@ledgerhq/lumen-ui-rnative`. See the [Setup Guide →](?path=/docs/getting-started-setup--docs).
57
+
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
+ ## Basic Usage
97
+
98
+ 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.
99
+
100
+ ```tsx
101
+ import React from 'react';
102
+ import { SegmentedControl, SegmentedControlButton } from '@ledgerhq/lumen-ui-rnative';
103
+
104
+ export default function Example() {
105
+ const [state, setState] = React.useState('send');
106
+
107
+ return (
108
+ <SegmentedControl
109
+ selectedValue={state}
110
+ onSelectedChange={setState}
111
+ accessibilityLabel='Transaction type'
112
+ >
113
+ <SegmentedControlButton value='send'>Send</SegmentedControlButton>
114
+ <SegmentedControlButton value='receive'>Receive</SegmentedControlButton>
115
+ <SegmentedControlButton value='buy'>Buy</SegmentedControlButton>
116
+ </SegmentedControl>
117
+ );
118
+ }
119
+ ```
120
+
121
+ ## With icons
122
+
123
+ Pass an icon from symbols to each button for a left-positioned icon. Use icons on all segments or none for consistency.
124
+
125
+ ```tsx
126
+ import { SegmentedControl, SegmentedControlButton } from '@ledgerhq/lumen-ui-rnative';
127
+ import { Coins, Nft, TransferHorizontal, Settings } from '@ledgerhq/lumen-ui-rnative/symbols';
128
+
129
+ function Example() {
130
+ const [state, setState] = React.useState('tokens');
131
+ return (
132
+ <SegmentedControl
133
+ selectedValue={state}
134
+ onSelectedChange={setState}
135
+ accessibilityLabel='Asset section'
136
+ >
137
+ <SegmentedControlButton value='tokens' icon={Coins}>
138
+ Tokens
139
+ </SegmentedControlButton>
140
+ <SegmentedControlButton value='nfts' icon={Nft}>
141
+ NFTs
142
+ </SegmentedControlButton>
143
+ <SegmentedControlButton value='activity' icon={TransferHorizontal}>
144
+ Activity
145
+ </SegmentedControlButton>
146
+ <SegmentedControlButton value='settings' icon={Settings}>
147
+ Settings
148
+ </SegmentedControlButton>
149
+ </SegmentedControl>
150
+ );
151
+ }
152
+ ```
153
+
154
+ <Box lx={{ flexDirection: 'column', gap: 's24' }}>
155
+ <CommonRulesDoAndDont />
156
+ </Box>
157
+
158
+ </Tab>
159
+ </CustomTabs>
@@ -0,0 +1,102 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
2
+ import React, { useState } from 'react';
3
+ import { Coins, Nft, TransferHorizontal, Settings } from '../../Symbols';
4
+ import { Box } from '../Utility';
5
+ import { SegmentedControl, SegmentedControlButton } from './SegmentedControl';
6
+
7
+ const meta = {
8
+ title: 'Navigation/SegmentedControl',
9
+ component: SegmentedControl,
10
+ subcomponents: {
11
+ SegmentedControlButton,
12
+ },
13
+ parameters: {
14
+ layout: 'centered',
15
+ backgrounds: { default: 'light' },
16
+ },
17
+ argTypes: {
18
+ onSelectedChange: {
19
+ action: 'change',
20
+ description: 'Callback when the selected value changes',
21
+ table: {
22
+ type: { summary: '(value: string) => void' },
23
+ },
24
+ },
25
+ accessibilityLabel: {
26
+ control: 'text',
27
+ description: 'Accessible label for the control',
28
+ table: {
29
+ type: { summary: 'string' },
30
+ },
31
+ },
32
+ selectedValue: {
33
+ control: 'text',
34
+ description:
35
+ 'The value of the currently selected segment (drives the sliding pill)',
36
+ table: {
37
+ type: { summary: 'string' },
38
+ },
39
+ },
40
+ children: {
41
+ control: false,
42
+ description: 'SegmentedControlButton elements',
43
+ table: {
44
+ type: { summary: 'ReactNode' },
45
+ },
46
+ },
47
+ },
48
+ } satisfies Meta<typeof SegmentedControl>;
49
+
50
+ export default meta;
51
+ type Story = StoryObj<typeof meta>;
52
+
53
+ export const Base: Story = {
54
+ args: {} as React.ComponentProps<typeof SegmentedControl>,
55
+ render: () => {
56
+ const [state, setState] = useState('send');
57
+
58
+ return (
59
+ <Box lx={{ width: 's256' }}>
60
+ <SegmentedControl
61
+ selectedValue={state}
62
+ onSelectedChange={setState}
63
+ accessibilityLabel='Transaction type'
64
+ >
65
+ <SegmentedControlButton value='send'>Send</SegmentedControlButton>
66
+ <SegmentedControlButton value='receive'>
67
+ Receive
68
+ </SegmentedControlButton>
69
+ <SegmentedControlButton value='buy'>Buy</SegmentedControlButton>
70
+ </SegmentedControl>
71
+ </Box>
72
+ );
73
+ },
74
+ };
75
+
76
+ export const WithIcons: Story = {
77
+ args: {} as React.ComponentProps<typeof SegmentedControl>,
78
+ render: () => {
79
+ const [state, setState] = useState('tokens');
80
+
81
+ return (
82
+ <SegmentedControl
83
+ selectedValue={state}
84
+ onSelectedChange={setState}
85
+ accessibilityLabel='Asset section'
86
+ >
87
+ <SegmentedControlButton value='tokens' icon={Coins}>
88
+ Tokens
89
+ </SegmentedControlButton>
90
+ <SegmentedControlButton value='nfts' icon={Nft}>
91
+ NFTs
92
+ </SegmentedControlButton>
93
+ <SegmentedControlButton value='activity' icon={TransferHorizontal}>
94
+ Activity
95
+ </SegmentedControlButton>
96
+ <SegmentedControlButton value='settings' icon={Settings}>
97
+ Settings
98
+ </SegmentedControlButton>
99
+ </SegmentedControl>
100
+ );
101
+ },
102
+ };
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect, jest } from '@jest/globals';
2
+ import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
3
+ import { render, fireEvent } from '@testing-library/react-native';
4
+ import React from 'react';
5
+ import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
6
+ import { SegmentedControl, SegmentedControlButton } from './SegmentedControl';
7
+
8
+ const TestWrapper = ({ children }: { children: React.ReactNode }) => (
9
+ <ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
10
+ {children}
11
+ </ThemeProvider>
12
+ );
13
+
14
+ describe('SegmentedControl', () => {
15
+ it('renders segments with labels', () => {
16
+ const { getByText } = render(
17
+ <TestWrapper>
18
+ <SegmentedControl
19
+ selectedValue='send'
20
+ onSelectedChange={() => {
21
+ /* empty */
22
+ }}
23
+ accessibilityLabel='Transaction type'
24
+ >
25
+ <SegmentedControlButton value='send'>Send</SegmentedControlButton>
26
+ <SegmentedControlButton value='receive'>
27
+ Receive
28
+ </SegmentedControlButton>
29
+ </SegmentedControl>
30
+ </TestWrapper>,
31
+ );
32
+ expect(getByText('Send')).toBeTruthy();
33
+ expect(getByText('Receive')).toBeTruthy();
34
+ });
35
+
36
+ it('calls onSelectedChange with segment value when a segment is pressed', () => {
37
+ const onSelectedChange = jest.fn();
38
+ const { getByText } = render(
39
+ <TestWrapper>
40
+ <SegmentedControl
41
+ selectedValue='send'
42
+ onSelectedChange={onSelectedChange}
43
+ accessibilityLabel='Transaction type'
44
+ >
45
+ <SegmentedControlButton value='send'>Send</SegmentedControlButton>
46
+ <SegmentedControlButton value='receive'>
47
+ Receive
48
+ </SegmentedControlButton>
49
+ </SegmentedControl>
50
+ </TestWrapper>,
51
+ );
52
+
53
+ fireEvent.press(getByText('Receive'));
54
+
55
+ expect(onSelectedChange).toHaveBeenCalledWith('receive');
56
+ });
57
+ });
@@ -0,0 +1,202 @@
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';
8
+ import { useStyleSheet } from '../../../styles';
9
+ import { durations, easingCurves } from '../../Animations/constants';
10
+ import { Box, Pressable, Text } from '../Utility';
11
+ import {
12
+ SegmentedControlContextProvider,
13
+ useSegmentedControlContext,
14
+ } from './SegmentedControlContext';
15
+ import type {
16
+ SegmentedControlButtonProps,
17
+ SegmentedControlProps,
18
+ } from './types';
19
+
20
+ const ICON_SIZE = 16;
21
+
22
+ export function SegmentedControlButton({
23
+ value,
24
+ children,
25
+ icon: Icon,
26
+ onPress,
27
+ ...props
28
+ }: SegmentedControlButtonProps) {
29
+ const styles = useButtonStyles();
30
+ const { selectedValue, onSelectedChange } = useSegmentedControlContext();
31
+
32
+ const selected = selectedValue === value;
33
+
34
+ function handlePress() {
35
+ onSelectedChange(value);
36
+ onPress?.();
37
+ }
38
+
39
+ return (
40
+ <Pressable
41
+ onPress={handlePress}
42
+ accessibilityState={{ selected }}
43
+ style={styles.button}
44
+ {...props}
45
+ >
46
+ <Box style={styles.content}>
47
+ {Icon && (
48
+ <Box style={styles.iconWrap}>
49
+ <Icon size={ICON_SIZE} />
50
+ </Box>
51
+ )}
52
+ <Text
53
+ typography={selected ? 'body2SemiBold' : 'body2'}
54
+ lx={{ color: 'base' }}
55
+ style={styles.label}
56
+ >
57
+ {children}
58
+ </Text>
59
+ </Box>
60
+ </Pressable>
61
+ );
62
+ }
63
+
64
+ SegmentedControlButton.displayName = 'SegmentedControlButton';
65
+
66
+ function useButtonStyles() {
67
+ return useStyleSheet(
68
+ (t) => ({
69
+ button: {
70
+ flex: 1,
71
+ flexDirection: 'row',
72
+ alignItems: 'center',
73
+ justifyContent: 'center',
74
+ paddingHorizontal: t.spacings.s16,
75
+ paddingVertical: t.spacings.s8,
76
+ borderRadius: t.borderRadius.full,
77
+ zIndex: 1,
78
+ },
79
+ content: {
80
+ flexDirection: 'row',
81
+ alignItems: 'center',
82
+ justifyContent: 'center',
83
+ gap: t.spacings.s8,
84
+ },
85
+ label: {
86
+ textAlign: 'center',
87
+ includeFontPadding: false,
88
+ },
89
+ iconWrap: {
90
+ flexDirection: 'row',
91
+ alignItems: 'center',
92
+ },
93
+ }),
94
+ [],
95
+ );
96
+ }
97
+
98
+ export function SegmentedControl({
99
+ selectedValue,
100
+ onSelectedChange,
101
+ accessibilityLabel,
102
+ children,
103
+ ...props
104
+ }: 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],
155
+ );
156
+
157
+ return (
158
+ <SegmentedControlContextProvider
159
+ value={{ selectedValue, onSelectedChange }}
160
+ >
161
+ <Box
162
+ accessibilityRole='radiogroup'
163
+ accessibilityLabel={accessibilityLabel}
164
+ onLayout={onLayout}
165
+ style={styles.container}
166
+ {...props}
167
+ >
168
+ {children}
169
+ <Animated.View
170
+ style={[styles.pill, animatedPillStyle]}
171
+ pointerEvents='none'
172
+ />
173
+ </Box>
174
+ </SegmentedControlContextProvider>
175
+ );
176
+ }
177
+
178
+ SegmentedControl.displayName = 'SegmentedControl';
179
+
180
+ function useRootStyles() {
181
+ return useStyleSheet(
182
+ (t) => ({
183
+ container: {
184
+ flexDirection: 'row',
185
+ alignItems: 'center',
186
+ position: 'relative',
187
+ width: '100%',
188
+ borderRadius: t.borderRadius.full,
189
+ backgroundColor: t.colors.bg.baseTransparent,
190
+ },
191
+ pill: {
192
+ position: 'absolute',
193
+ top: 0,
194
+ left: 0,
195
+ borderRadius: t.borderRadius.sm,
196
+ backgroundColor: t.colors.bg.muted,
197
+ zIndex: 0,
198
+ },
199
+ }),
200
+ [],
201
+ );
202
+ }
@@ -0,0 +1,17 @@
1
+ import { createSafeContext } from '@ledgerhq/lumen-utils-shared';
2
+
3
+ export type SegmentedControlContextValue = {
4
+ selectedValue: string;
5
+ onSelectedChange: (value: string) => void;
6
+ };
7
+
8
+ const [SegmentedControlContextProvider, _useSegmentedControlSafeContext] =
9
+ createSafeContext<SegmentedControlContextValue>('SegmentedControl');
10
+
11
+ export const useSegmentedControlContext = () =>
12
+ _useSegmentedControlSafeContext({
13
+ consumerName: 'SegmentedControlButton',
14
+ contextRequired: true,
15
+ });
16
+
17
+ export { SegmentedControlContextProvider };
@@ -0,0 +1,2 @@
1
+ export * from './SegmentedControl';
2
+ export type * from './types';
@@ -0,0 +1,46 @@
1
+ import { ComponentType, ReactNode } from 'react';
2
+ import { StyledPressableProps } from '../../../styles';
3
+ import { IconSize } from '../Icon';
4
+ import { BoxProps } from '../Utility';
5
+
6
+ export type SegmentedControlProps = {
7
+ /**
8
+ * The value of the currently selected segment (drives the sliding pill).
9
+ */
10
+ selectedValue: string;
11
+ /**
12
+ * Callback when the selected segment value changes.
13
+ */
14
+ onSelectedChange: (value: string) => void;
15
+ /**
16
+ * Accessible label for the control (e.g. "File view").
17
+ */
18
+ accessibilityLabel?: string;
19
+ /**
20
+ * Segment buttons (SegmentedControlButton). Can be wrapped (e.g. in Tooltip).
21
+ */
22
+ children: ReactNode;
23
+ } & Omit<BoxProps, 'children'>;
24
+
25
+ type IconComponent = ComponentType<{
26
+ size?: IconSize;
27
+ }>;
28
+
29
+ export type SegmentedControlButtonProps = {
30
+ /**
31
+ * Value for this segment (must be unique among siblings).
32
+ */
33
+ value: string;
34
+ /**
35
+ * Button label (e.g. "Preview", "Raw").
36
+ */
37
+ children: ReactNode;
38
+ /**
39
+ * Optional icon shown to the left of the label (from Symbols).
40
+ */
41
+ icon?: IconComponent;
42
+ /**
43
+ * Optional callback when the button is pressed (in addition to onSelectedChange on the parent).
44
+ */
45
+ onPress?: () => void;
46
+ } & Omit<StyledPressableProps, 'children'>;
@@ -287,6 +287,7 @@ const useStyles = () =>
287
287
  },
288
288
  pill: {
289
289
  position: 'absolute',
290
+ pointerEvents: 'none',
290
291
  top: PILL_INSET,
291
292
  left: PILL_INSET,
292
293
  borderRadius: t.borderRadius.full,
@@ -5,7 +5,6 @@ import { BoxProps } from '../Utility';
5
5
 
6
6
  type IconComponent = ComponentType<{
7
7
  size?: IconSize;
8
- className?: string;
9
8
  }>;
10
9
 
11
10
  export type TabBarItemProps = {
@@ -17,6 +17,7 @@ export * from './ListItem';
17
17
  export * from './NavBar';
18
18
  export * from './PageIndicator';
19
19
  export * from './SearchInput';
20
+ export * from './SegmentedControl';
20
21
  export * from './Select';
21
22
  export * from './Skeleton';
22
23
  export * from './Spinner';
@@ -15,7 +15,7 @@ describe('createStylesheetTheme', () => {
15
15
  });
16
16
 
17
17
  it('should flatten heading/body typography tokens', () => {
18
- jest.spyOn(RuntimeConstants, 'isAndroid', 'get').mockReturnValue(true);
18
+ jest.spyOn(RuntimeConstants, 'isIOS', 'get').mockReturnValue(true);
19
19
  const theme = ledgerLiveThemes.dark;
20
20
 
21
21
  const result = createStylesheetTheme(theme);
@@ -9,8 +9,9 @@ describe('resolveTypographies', () => {
9
9
  jest.restoreAllMocks();
10
10
  });
11
11
 
12
- it('should not modify fontFamily on android', () => {
13
- jest.spyOn(RuntimeConstants, 'isAndroid', 'get').mockReturnValue(true);
12
+ it('should not modify fontFamily on iOS', () => {
13
+ jest.spyOn(RuntimeConstants, 'isIOS', 'get').mockReturnValue(true);
14
+ jest.spyOn(RuntimeConstants, 'isBrowser', 'get').mockReturnValue(false);
14
15
  const theme = ledgerLiveThemes.dark;
15
16
 
16
17
  const result = createStylesheetTheme(theme);
@@ -24,8 +25,9 @@ describe('resolveTypographies', () => {
24
25
  );
25
26
  });
26
27
 
27
- it('should append weight suffix to fontFamily on non-android', () => {
28
- jest.spyOn(RuntimeConstants, 'isAndroid', 'get').mockReturnValue(false);
28
+ it('should append weight suffix to fontFamily on Android', () => {
29
+ jest.spyOn(RuntimeConstants, 'isIOS', 'get').mockReturnValue(false);
30
+ jest.spyOn(RuntimeConstants, 'isBrowser', 'get').mockReturnValue(false);
29
31
  const theme = ledgerLiveThemes.dark;
30
32
 
31
33
  const result = createStylesheetTheme(theme);
@@ -44,8 +46,9 @@ describe('resolveTypographies', () => {
44
46
  });
45
47
  });
46
48
 
47
- it('should not modify other typography properties on non-android', () => {
48
- jest.spyOn(RuntimeConstants, 'isAndroid', 'get').mockReturnValue(false);
49
+ it('should not modify other typography properties on Android', () => {
50
+ jest.spyOn(RuntimeConstants, 'isIOS', 'get').mockReturnValue(false);
51
+ jest.spyOn(RuntimeConstants, 'isBrowser', 'get').mockReturnValue(false);
49
52
  const theme = ledgerLiveThemes.dark;
50
53
 
51
54
  const result = createStylesheetTheme(theme);
@@ -14,7 +14,7 @@ export const FONT_WEIGHT_SUFFIX_MAP = {
14
14
  };
15
15
 
16
16
  export const resolveFontWeights = (typographies: LumenTypographyTokens) => {
17
- if (RuntimeConstants.isAndroid || RuntimeConstants.isBrowser) {
17
+ if (RuntimeConstants.isIOS || RuntimeConstants.isBrowser) {
18
18
  return typographies;
19
19
  }
20
20
 
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=Banner.figma.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Banner.figma.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/Banner/Banner.figma.tsx"],"names":[],"mappings":""}