@ledgerhq/lumen-ui-rnative 0.0.70 → 0.0.72

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 (142) hide show
  1. package/.storybook/preview.tsx +4 -0
  2. package/dist/package.json +4 -4
  3. package/dist/src/i18n/locales/de.json +3 -0
  4. package/dist/src/i18n/locales/en.json +3 -0
  5. package/dist/src/i18n/locales/es.json +3 -0
  6. package/dist/src/i18n/locales/fr.json +3 -0
  7. package/dist/src/i18n/locales/ja.json +3 -0
  8. package/dist/src/i18n/locales/ko.json +3 -0
  9. package/dist/src/i18n/locales/pt.json +3 -0
  10. package/dist/src/i18n/locales/ru.json +3 -0
  11. package/dist/src/i18n/locales/th.json +3 -0
  12. package/dist/src/i18n/locales/tr.json +3 -0
  13. package/dist/src/i18n/locales/zh.json +3 -0
  14. package/dist/src/index.d.ts +1 -0
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/index.js +1 -0
  17. package/dist/src/lib/Animations/Pulse/Pulse.d.ts +3 -0
  18. package/dist/src/lib/Animations/Pulse/Pulse.d.ts.map +1 -0
  19. package/dist/src/lib/Animations/Pulse/Pulse.js +46 -0
  20. package/dist/src/lib/Animations/Pulse/Pulse.stories.d.ts +9 -0
  21. package/dist/src/lib/Animations/Pulse/Pulse.stories.d.ts.map +1 -0
  22. package/dist/src/lib/Animations/Pulse/Pulse.stories.js +38 -0
  23. package/dist/src/lib/Animations/Pulse/index.d.ts +3 -0
  24. package/dist/src/lib/Animations/Pulse/index.d.ts.map +1 -0
  25. package/dist/src/lib/Animations/Pulse/index.js +2 -0
  26. package/dist/src/lib/Animations/Pulse/types.d.ts +18 -0
  27. package/dist/src/lib/Animations/Pulse/types.d.ts.map +1 -0
  28. package/dist/src/lib/Animations/Pulse/types.js +1 -0
  29. package/dist/src/lib/Animations/Spin/Spin.d.ts +3 -0
  30. package/dist/src/lib/Animations/Spin/Spin.d.ts.map +1 -0
  31. package/dist/src/lib/Animations/Spin/Spin.js +23 -0
  32. package/dist/src/lib/Animations/Spin/Spin.stories.d.ts +9 -0
  33. package/dist/src/lib/Animations/Spin/Spin.stories.d.ts.map +1 -0
  34. package/dist/src/lib/Animations/Spin/Spin.stories.js +27 -0
  35. package/dist/src/lib/Animations/Spin/index.d.ts +3 -0
  36. package/dist/src/lib/Animations/Spin/index.d.ts.map +1 -0
  37. package/dist/src/lib/Animations/Spin/index.js +2 -0
  38. package/dist/src/lib/Animations/Spin/types.d.ts +14 -0
  39. package/dist/src/lib/Animations/Spin/types.d.ts.map +1 -0
  40. package/dist/src/lib/Animations/Spin/types.js +1 -0
  41. package/dist/src/lib/Animations/index.d.ts +4 -0
  42. package/dist/src/lib/Animations/index.d.ts.map +1 -0
  43. package/dist/src/lib/Animations/index.js +3 -0
  44. package/dist/src/lib/Animations/types.d.ts +2 -0
  45. package/dist/src/lib/Animations/types.d.ts.map +1 -0
  46. package/dist/src/lib/Animations/types.js +1 -0
  47. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.d.ts +5 -7
  48. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.d.ts.map +1 -1
  49. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.js +7 -6
  50. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.d.ts +1 -0
  51. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.d.ts.map +1 -1
  52. package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.js +5 -0
  53. package/dist/src/lib/Components/AmountDisplay/types.d.ts +7 -1
  54. package/dist/src/lib/Components/AmountDisplay/types.d.ts.map +1 -1
  55. package/dist/src/lib/Components/Skeleton/Skeleton.d.ts +21 -0
  56. package/dist/src/lib/Components/Skeleton/Skeleton.d.ts.map +1 -0
  57. package/dist/src/lib/Components/Skeleton/Skeleton.js +81 -0
  58. package/dist/src/lib/Components/Skeleton/Skeleton.stories.d.ts +11 -0
  59. package/dist/src/lib/Components/Skeleton/Skeleton.stories.d.ts.map +1 -0
  60. package/dist/src/lib/Components/Skeleton/Skeleton.stories.js +49 -0
  61. package/dist/src/lib/Components/Skeleton/index.d.ts +3 -0
  62. package/dist/src/lib/Components/Skeleton/index.d.ts.map +1 -0
  63. package/dist/src/lib/Components/Skeleton/index.js +2 -0
  64. package/dist/src/lib/Components/Skeleton/types.d.ts +10 -0
  65. package/dist/src/lib/Components/Skeleton/types.d.ts.map +1 -0
  66. package/dist/src/lib/Components/Skeleton/types.js +1 -0
  67. package/dist/src/lib/Components/Spinner/Spinner.d.ts.map +1 -1
  68. package/dist/src/lib/Components/Spinner/Spinner.js +2 -23
  69. package/dist/src/lib/Components/Stepper/Stepper.d.ts +16 -0
  70. package/dist/src/lib/Components/Stepper/Stepper.d.ts.map +1 -0
  71. package/dist/src/lib/Components/Stepper/Stepper.js +74 -0
  72. package/dist/src/lib/Components/Stepper/Stepper.stories.d.ts +9 -0
  73. package/dist/src/lib/Components/Stepper/Stepper.stories.d.ts.map +1 -0
  74. package/dist/src/lib/Components/Stepper/Stepper.stories.js +35 -0
  75. package/dist/src/lib/Components/Stepper/index.d.ts +3 -0
  76. package/dist/src/lib/Components/Stepper/index.d.ts.map +1 -0
  77. package/dist/src/lib/Components/Stepper/index.js +2 -0
  78. package/dist/src/lib/Components/Stepper/types.d.ts +21 -0
  79. package/dist/src/lib/Components/Stepper/types.d.ts.map +1 -0
  80. package/dist/src/lib/Components/Stepper/types.js +1 -0
  81. package/dist/src/lib/Components/TabBar/TabBar.d.ts +1 -0
  82. package/dist/src/lib/Components/TabBar/TabBar.d.ts.map +1 -1
  83. package/dist/src/lib/Components/TabBar/TabBar.js +2 -1
  84. package/dist/src/lib/Components/TabBar/index.d.ts +1 -1
  85. package/dist/src/lib/Components/TabBar/index.d.ts.map +1 -1
  86. package/dist/src/lib/Components/TabBar/index.js +1 -1
  87. package/dist/src/lib/Components/TileButton/TileButton.d.ts +4 -3
  88. package/dist/src/lib/Components/TileButton/TileButton.d.ts.map +1 -1
  89. package/dist/src/lib/Components/TileButton/TileButton.js +3 -4
  90. package/dist/src/lib/Components/index.d.ts +3 -0
  91. package/dist/src/lib/Components/index.d.ts.map +1 -1
  92. package/dist/src/lib/Components/index.js +3 -0
  93. package/dist/src/styles/types/factories.types.d.ts +1 -1
  94. package/dist/src/styles/types/factories.types.d.ts.map +1 -1
  95. package/package.json +4 -4
  96. package/src/i18n/locales/de.json +3 -0
  97. package/src/i18n/locales/en.json +3 -0
  98. package/src/i18n/locales/es.json +3 -0
  99. package/src/i18n/locales/fr.json +3 -0
  100. package/src/i18n/locales/ja.json +3 -0
  101. package/src/i18n/locales/ko.json +3 -0
  102. package/src/i18n/locales/pt.json +3 -0
  103. package/src/i18n/locales/ru.json +3 -0
  104. package/src/i18n/locales/th.json +3 -0
  105. package/src/i18n/locales/tr.json +3 -0
  106. package/src/i18n/locales/zh.json +3 -0
  107. package/src/index.ts +1 -0
  108. package/src/lib/Animations/Pulse/Pulse.mdx +86 -0
  109. package/src/lib/Animations/Pulse/Pulse.stories.tsx +90 -0
  110. package/src/lib/Animations/Pulse/Pulse.tsx +55 -0
  111. package/src/lib/Animations/Pulse/index.ts +2 -0
  112. package/src/lib/Animations/Pulse/types.ts +18 -0
  113. package/src/lib/Animations/Spin/Spin.mdx +85 -0
  114. package/src/lib/Animations/Spin/Spin.stories.tsx +72 -0
  115. package/src/lib/Animations/Spin/Spin.tsx +34 -0
  116. package/src/lib/Animations/Spin/index.ts +2 -0
  117. package/src/lib/Animations/Spin/types.ts +14 -0
  118. package/src/lib/Animations/index.ts +3 -0
  119. package/src/lib/Animations/types.ts +11 -0
  120. package/src/lib/Components/AmountDisplay/AmountDisplay.mdx +6 -0
  121. package/src/lib/Components/AmountDisplay/AmountDisplay.stories.tsx +12 -0
  122. package/src/lib/Components/AmountDisplay/AmountDisplay.test.tsx +13 -0
  123. package/src/lib/Components/AmountDisplay/AmountDisplay.tsx +39 -35
  124. package/src/lib/Components/AmountDisplay/types.ts +7 -1
  125. package/src/lib/Components/Skeleton/Skeleton.mdx +200 -0
  126. package/src/lib/Components/Skeleton/Skeleton.stories.tsx +89 -0
  127. package/src/lib/Components/Skeleton/Skeleton.test.tsx +54 -0
  128. package/src/lib/Components/Skeleton/Skeleton.tsx +137 -0
  129. package/src/lib/Components/Skeleton/index.ts +2 -0
  130. package/src/lib/Components/Skeleton/types.ts +10 -0
  131. package/src/lib/Components/Spinner/Spinner.tsx +3 -35
  132. package/src/lib/Components/Stepper/Stepper.mdx +217 -0
  133. package/src/lib/Components/Stepper/Stepper.stories.tsx +62 -0
  134. package/src/lib/Components/Stepper/Stepper.test.tsx +132 -0
  135. package/src/lib/Components/Stepper/Stepper.tsx +159 -0
  136. package/src/lib/Components/Stepper/index.ts +2 -0
  137. package/src/lib/Components/Stepper/types.ts +21 -0
  138. package/src/lib/Components/TabBar/TabBar.tsx +2 -1
  139. package/src/lib/Components/TabBar/index.ts +1 -1
  140. package/src/lib/Components/TileButton/TileButton.tsx +35 -44
  141. package/src/lib/Components/index.ts +3 -0
  142. package/src/styles/types/factories.types.ts +1 -1
@@ -0,0 +1,217 @@
1
+ import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks';
2
+ import { CustomTabs, Tab } from '../../../../.storybook/components';
3
+ import * as StepperStories from './Stepper.stories';
4
+ import { DoVsDontRow, DoBlockItem, DontBlockItem } from '../../../../.storybook/components/DoVsDont';
5
+ import CommonRulesDoAndDont from '../../../../.storybook/components/DoVsDont/CommonRulesDoAndDont.mdx';
6
+ import { Box } from '../Utility/Box';
7
+ import { Text } from '../Utility/Text';
8
+
9
+ <Meta title='Communication/Stepper/Docs' of={StepperStories} />
10
+
11
+ # 🔄 Stepper
12
+
13
+ <CustomTabs>
14
+ <Tab label="Overview">
15
+
16
+ The Stepper component provides visual feedback for multi-step processes and workflows. It displays progress as a circular indicator showing the current step out of total steps, helping users understand where they are in a sequence.
17
+
18
+ ## Figma Library
19
+
20
+ All stepper designs are created and managed in our components library. This ensures consistency across all platforms and use cases.
21
+
22
+ - 🔗 **[Figma](https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7/2.-Components-Library?node-id=11977-94&m=dev)**
23
+
24
+ ## Interactive Example
25
+
26
+ <Canvas of={StepperStories.Base} />
27
+ <Controls of={StepperStories.Base} />
28
+
29
+ ## 🎯 Anatomy
30
+
31
+ The Stepper consists of:
32
+ - **Track Circle**: A muted circular background showing the full progress path
33
+ - **Progress Arc**: A colored arc that fills clockwise from the top, representing completed progress
34
+ - **Label**: Centered text showing "current/total" steps (e.g., "2/4")
35
+
36
+ The progress arc starts at the top (12 o'clock position) and fills clockwise. When at 0% progress, a small dot appears at the top instead of an empty arc.
37
+
38
+ ---
39
+
40
+ ## 🏷️ Labels
41
+
42
+ **Default label**: The default label is "currentStep/totalSteps" (e.g., "2/4").
43
+
44
+
45
+ ### Step Ranges
46
+ - **`totalSteps`** must be between **2** and **9**
47
+ - **`currentStep`** must be between **0** and **`totalSteps`**
48
+ - Use `currentStep={0}` to show the initial state (minimal dot before starting)
49
+
50
+ > ⚠️ Using more than **9** total steps or exceeding the valid range may cause the UI to break and text to be truncated inside the circular indicator.
51
+
52
+ > ⚠️ Values outside valid ranges are automatically clamped: **`currentStep`** is kept between **0** and **`totalSteps`**.
53
+
54
+ ## 🎨 Disabled State
55
+
56
+ The Stepper supports a disabled state that changes the progress arc to a muted appearance:
57
+
58
+ <Canvas of={StepperStories.DisabledShowcase} />
59
+
60
+
61
+ ## Custom Labels
62
+
63
+ By default, the stepper displays "currentStep/totalSteps" (e.g., "2/4"). You can provide a custom label for special cases:
64
+
65
+ <Canvas of={StepperStories.WithCustomLabel} />
66
+
67
+ > ⚠️ **Note**: The label and step counts are automatically truncated if they overflow the circular indicator. Keep custom labels short to ensure readability within the 48px circle.
68
+
69
+ ---
70
+
71
+ ## Usage Guidelines
72
+
73
+ ### Best Practices
74
+
75
+ - Keep step counts reasonable (typically 3-7 steps)
76
+ - Use descriptive labels when the numeric format isn't clear
77
+ - Combine with textual step indicators for better context
78
+ - Use disabled for paused or inactive steppers
79
+
80
+ </Tab>
81
+ <Tab label="Implementation">
82
+
83
+ ## Installation
84
+
85
+ Install and set up the library with our [Setup Guide →](?path=/docs/getting-started-setup--docs).
86
+
87
+ ### Basic Usage
88
+
89
+ Import and use the Stepper component:
90
+
91
+ ```tsx
92
+ import React, { useState } from 'react';
93
+ import { View } from 'react-native';
94
+ import { Stepper } from '@ledgerhq/lumen-ui-rnative';
95
+
96
+ function OnboardingFlow() {
97
+ const [currentStep, setCurrentStep] = useState(0);
98
+ const totalSteps = 4;
99
+
100
+ return (
101
+ <View>
102
+ <Stepper currentStep={currentStep} totalSteps={totalSteps} />
103
+ {/* Your step content */}
104
+ </View>
105
+ );
106
+ }
107
+ ```
108
+
109
+ ### With Disabled State
110
+
111
+ Control the visual state based on step completion:
112
+
113
+ ```tsx
114
+ import React from 'react';
115
+ import { Box, Stepper } from '@ledgerhq/lumen-ui-rnative';
116
+
117
+ function MultiStepForm() {
118
+ return (
119
+ <Box lx={{ flexDirection: 'row', gap: 's16' }}>
120
+ <Stepper currentStep={1} totalSteps={3} />
121
+ <Stepper currentStep={2} totalSteps={3} />
122
+ <Stepper currentStep={0} totalSteps={3} disabled />
123
+ </Box>
124
+ );
125
+ }
126
+ ```
127
+
128
+ ### Custom Labels
129
+
130
+ Provide custom labels for localized or descriptive text:
131
+
132
+ ```tsx
133
+ import React from 'react';
134
+ import { Stepper } from '@ledgerhq/lumen-ui-rnative';
135
+
136
+ function LocalizedStepper() {
137
+ const currentStep = 3;
138
+ const totalSteps = 5;
139
+
140
+ return (
141
+ <Stepper
142
+ currentStep={currentStep}
143
+ totalSteps={totalSteps}
144
+ label={`Étape ${currentStep} sur ${totalSteps}`}
145
+ />
146
+ );
147
+ }
148
+ ```
149
+
150
+ ## Do's and Don'ts
151
+
152
+ <Box lx={{ gap: 's24' }}>
153
+ <DoVsDontRow>
154
+ <DoBlockItem
155
+ title='Provide meaningful step counts'
156
+ description='Use realistic total steps that match your flow'
157
+ >
158
+
159
+ {/* prettier-ignore */}
160
+ ```tsx
161
+ <Stepper currentStep={3} totalSteps={5} />
162
+ ```
163
+
164
+ </DoBlockItem>
165
+ <DontBlockItem
166
+ title="Don't use for single-step processes"
167
+ description='Steppers are for multi-step workflows only'
168
+ >
169
+
170
+ {/* prettier-ignore */}
171
+ ```tsx
172
+ <Stepper currentStep={1} totalSteps={1} />
173
+ ```
174
+
175
+ </DontBlockItem>
176
+
177
+ </DoVsDontRow>
178
+
179
+ <DoVsDontRow>
180
+ <DoBlockItem
181
+ title='Use disabled for paused steppers'
182
+ description='Reserve disabled for steppers that are not actively progressing'
183
+ >
184
+
185
+ {/* prettier-ignore */}
186
+ ```tsx
187
+ <Stepper
188
+ currentStep={2}
189
+ totalSteps={4}
190
+ disabled
191
+ />
192
+ ```
193
+
194
+ </DoBlockItem>
195
+ <DontBlockItem
196
+ title="Don't disable active steps"
197
+ description='Active steps in progress should not be disabled'
198
+ >
199
+
200
+ {/* prettier-ignore */}
201
+ ```tsx
202
+ <Stepper
203
+ currentStep={2}
204
+ totalSteps={4}
205
+ disabled // Wrong for active step
206
+ />
207
+ ```
208
+
209
+ </DontBlockItem>
210
+
211
+ </DoVsDontRow>
212
+
213
+ <CommonRulesDoAndDont />
214
+ </Box>
215
+
216
+ </Tab>
217
+ </CustomTabs>
@@ -0,0 +1,62 @@
1
+ import { Meta, StoryObj } from '@storybook/react-native-web-vite';
2
+ import { Box } from '../Utility/Box';
3
+ import { Text } from '../Utility/Text';
4
+ import { Stepper } from './Stepper';
5
+
6
+ const meta: Meta<typeof Stepper> = {
7
+ title: 'Communication/Stepper',
8
+ component: Stepper,
9
+ parameters: {
10
+ actions: { disable: true },
11
+ },
12
+ argTypes: {
13
+ currentStep: { control: 'number' },
14
+ totalSteps: { control: 'number' },
15
+ disabled: { control: 'boolean' },
16
+ },
17
+ };
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof Stepper>;
21
+
22
+ export const Base: Story = {
23
+ args: {
24
+ currentStep: 2,
25
+ totalSteps: 4,
26
+ },
27
+ render: (args) => <Stepper {...args} />,
28
+ };
29
+
30
+ export const DisabledShowcase: Story = {
31
+ render: () => (
32
+ <Box lx={{ gap: 's32', alignItems: 'center' }}>
33
+ <Box lx={{ alignItems: 'center', gap: 's8' }}>
34
+ <Text typography='body3' lx={{ color: 'muted' }}>
35
+ Default (2/4)
36
+ </Text>
37
+ <Stepper currentStep={2} totalSteps={4} />
38
+ </Box>
39
+ <Box lx={{ alignItems: 'center', gap: 's8' }}>
40
+ <Text typography='body3' lx={{ color: 'muted' }}>
41
+ Disabled (2/4)
42
+ </Text>
43
+ <Stepper currentStep={2} totalSteps={4} disabled />
44
+ </Box>
45
+ <Box lx={{ alignItems: 'center', gap: 's8' }}>
46
+ <Text typography='body3' lx={{ color: 'muted' }}>
47
+ Unstarted (0/9)
48
+ </Text>
49
+ <Stepper currentStep={0} totalSteps={9} />
50
+ </Box>
51
+ </Box>
52
+ ),
53
+ };
54
+
55
+ export const WithCustomLabel: Story = {
56
+ args: {
57
+ currentStep: 5,
58
+ totalSteps: 5,
59
+ label: '🎉',
60
+ },
61
+ render: (args) => <Stepper {...args} />,
62
+ };
@@ -0,0 +1,132 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
3
+ import { render } from '@testing-library/react-native';
4
+ import React from 'react';
5
+ import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
6
+ import { Stepper } from './Stepper';
7
+
8
+ const TestWrapper = ({ children }: { children: React.ReactNode }) => (
9
+ <ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
10
+ {children}
11
+ </ThemeProvider>
12
+ );
13
+
14
+ describe('Stepper Component', () => {
15
+ it('should render with i18n accessibility label', async () => {
16
+ const { findByLabelText } = render(
17
+ <TestWrapper>
18
+ <Stepper currentStep={2} totalSteps={4} />
19
+ </TestWrapper>,
20
+ );
21
+ const stepper = await findByLabelText('Step 2 of 4');
22
+ expect(stepper).toBeTruthy();
23
+ expect(stepper.props.accessibilityRole).toBe('progressbar');
24
+ expect(stepper.props.accessibilityValue).toEqual(
25
+ expect.objectContaining({
26
+ now: 2,
27
+ min: 1,
28
+ max: 4,
29
+ text: '2/4',
30
+ }),
31
+ );
32
+ });
33
+
34
+ it('should render with custom label', () => {
35
+ const { getByLabelText } = render(
36
+ <TestWrapper>
37
+ <Stepper currentStep={3} totalSteps={5} label='Step 3 of 5' />
38
+ </TestWrapper>,
39
+ );
40
+ const stepper = getByLabelText('Step 3 of 5');
41
+ expect(stepper).toBeTruthy();
42
+ });
43
+
44
+ it('should render active border color by default', () => {
45
+ const { getByTestId } = render(
46
+ <TestWrapper>
47
+ <Stepper currentStep={1} totalSteps={4} testID='stepper' />
48
+ </TestWrapper>,
49
+ );
50
+ const stepper = getByTestId('stepper');
51
+ expect(stepper).toBeTruthy();
52
+ });
53
+
54
+ it('should render disabled style when disabled', () => {
55
+ const { getByTestId } = render(
56
+ <TestWrapper>
57
+ <Stepper currentStep={2} totalSteps={4} disabled testID='stepper' />
58
+ </TestWrapper>,
59
+ );
60
+ const stepper = getByTestId('stepper');
61
+ expect(stepper).toBeTruthy();
62
+ });
63
+
64
+ it('should clamp currentStep to totalSteps in display', () => {
65
+ const { getByText } = render(
66
+ <TestWrapper>
67
+ <Stepper currentStep={10} totalSteps={4} />
68
+ </TestWrapper>,
69
+ );
70
+ expect(getByText('4')).toBeTruthy();
71
+ expect(getByText('/4')).toBeTruthy();
72
+ });
73
+
74
+ it('should handle zero totalSteps gracefully', async () => {
75
+ const { findByLabelText } = render(
76
+ <TestWrapper>
77
+ <Stepper currentStep={1} totalSteps={0} />
78
+ </TestWrapper>,
79
+ );
80
+ const stepper = await findByLabelText('Step 0 of 0');
81
+ expect(stepper).toBeTruthy();
82
+ });
83
+
84
+ it('should handle currentStep <= 0 (minimal dot)', async () => {
85
+ const { findByLabelText } = render(
86
+ <TestWrapper>
87
+ <Stepper currentStep={0} totalSteps={4} />
88
+ </TestWrapper>,
89
+ );
90
+ const stepper = await findByLabelText('Step 0 of 4');
91
+ expect(stepper).toBeTruthy();
92
+ });
93
+
94
+ it('should clamp negative currentStep to 0', async () => {
95
+ const { findByLabelText, getByText } = render(
96
+ <TestWrapper>
97
+ <Stepper currentStep={-3} totalSteps={4} />
98
+ </TestWrapper>,
99
+ );
100
+ const stepper = await findByLabelText('Step 0 of 4');
101
+ expect(stepper).toBeTruthy();
102
+ expect(stepper.props.accessibilityValue).toEqual(
103
+ expect.objectContaining({
104
+ now: -3,
105
+ min: 1,
106
+ max: 4,
107
+ text: '0/4',
108
+ }),
109
+ );
110
+ expect(getByText('0')).toBeTruthy();
111
+ expect(getByText('/4')).toBeTruthy();
112
+ });
113
+
114
+ it('should apply custom lx props', () => {
115
+ const { getByTestId } = render(
116
+ <TestWrapper>
117
+ <Stepper
118
+ currentStep={2}
119
+ totalSteps={4}
120
+ testID='stepper'
121
+ lx={{ marginTop: 's8' }}
122
+ />
123
+ </TestWrapper>,
124
+ );
125
+ const stepper = getByTestId('stepper');
126
+ const style = stepper.props.style;
127
+ const flatStyle = Array.isArray(style)
128
+ ? Object.assign({}, ...style)
129
+ : style;
130
+ expect(flatStyle.marginTop).toBe(8);
131
+ });
132
+ });
@@ -0,0 +1,159 @@
1
+ import { getStepperCalculations } from '@ledgerhq/lumen-utils-shared';
2
+ import { useEffect, useRef } from 'react';
3
+ import { Animated, Easing } from 'react-native';
4
+ import Svg, { Circle } from 'react-native-svg';
5
+ import { useCommonTranslation } from '../../../i18n';
6
+ import { useTheme } from '../../../styles';
7
+ import { Box } from '../Utility/Box';
8
+ import { Text } from '../Utility/Text';
9
+ import { StepperProps } from './types';
10
+
11
+ const SIZE = 48;
12
+ const STROKE_WIDTH = 4;
13
+
14
+ const AnimatedCircle = Animated.createAnimatedComponent(Circle);
15
+
16
+ /**
17
+ * A circular stepper component showing progress as current step out of total steps.
18
+ * Renders a track arc with a progress arc and a center label.
19
+ *
20
+ * @see [Figma – Stepper](https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7/2.-Components-Library?node-id=11977-94&m=dev)
21
+ *
22
+ * @example
23
+ * <Stepper currentStep={1} totalSteps={4} />
24
+ * <Stepper currentStep={0} totalSteps={9} disabled /> // Shows minimal dot, disabled style
25
+ */
26
+ export const Stepper = ({
27
+ lx = {},
28
+ currentStep,
29
+ totalSteps,
30
+ disabled = false,
31
+ label,
32
+ ref,
33
+ ...props
34
+ }: StepperProps) => {
35
+ const { t } = useCommonTranslation();
36
+ const { theme } = useTheme();
37
+
38
+ const {
39
+ displayLabel,
40
+ r,
41
+ cx,
42
+ cy,
43
+ trackDashArray,
44
+ progressDashArray,
45
+ progressDashOffset,
46
+ } = getStepperCalculations({
47
+ currentStep,
48
+ totalSteps,
49
+ size: SIZE,
50
+ label,
51
+ strokeWidth: STROKE_WIDTH,
52
+ });
53
+
54
+ const animatedOffset = useRef(new Animated.Value(progressDashOffset)).current;
55
+
56
+ useEffect(() => {
57
+ const animation = Animated.timing(animatedOffset, {
58
+ toValue: progressDashOffset,
59
+ duration: 300,
60
+ easing: Easing.inOut(Easing.ease),
61
+ useNativeDriver: false,
62
+ });
63
+ animation.start();
64
+ return () => animation.stop();
65
+ }, [progressDashOffset, animatedOffset]);
66
+
67
+ return (
68
+ <Box
69
+ ref={ref}
70
+ accessibilityRole='progressbar'
71
+ accessibilityValue={{
72
+ now: currentStep,
73
+ min: 1,
74
+ max: totalSteps,
75
+ text: displayLabel,
76
+ }}
77
+ accessibilityLabel={
78
+ label ??
79
+ t('components.stepper.progressAriaLabel', {
80
+ currentStep: Math.min(Math.max(currentStep, 0), totalSteps),
81
+ totalSteps,
82
+ })
83
+ }
84
+ lx={{
85
+ width: 's48',
86
+ height: 's48',
87
+ flexShrink: 0,
88
+ alignItems: 'center',
89
+ justifyContent: 'center',
90
+ borderRadius: 'full',
91
+ ...lx,
92
+ }}
93
+ {...props}
94
+ >
95
+ <Svg
96
+ width={SIZE}
97
+ height={SIZE}
98
+ viewBox={`0 0 ${SIZE} ${SIZE}`}
99
+ style={{ transform: [{ rotate: '135deg' }] }}
100
+ >
101
+ <Circle
102
+ cx={cx}
103
+ cy={cy}
104
+ r={r}
105
+ fill='none'
106
+ stroke={theme.colors.border.mutedSubtle}
107
+ strokeLinecap='round'
108
+ strokeWidth={STROKE_WIDTH}
109
+ strokeDasharray={trackDashArray}
110
+ strokeDashoffset={0}
111
+ />
112
+ <AnimatedCircle
113
+ cx={cx}
114
+ cy={cy}
115
+ r={r}
116
+ fill='none'
117
+ stroke={
118
+ disabled
119
+ ? theme.colors.border.mutedSubtleHover
120
+ : theme.colors.border.active
121
+ }
122
+ strokeLinecap='round'
123
+ strokeWidth={STROKE_WIDTH}
124
+ strokeDasharray={progressDashArray}
125
+ strokeDashoffset={animatedOffset}
126
+ />
127
+ </Svg>
128
+ <Box
129
+ lx={{
130
+ position: 'absolute',
131
+ alignItems: 'center',
132
+ justifyContent: 'center',
133
+ flexDirection: 'row',
134
+ top: 's0',
135
+ left: 's0',
136
+ right: 's0',
137
+ bottom: 's0',
138
+ }}
139
+ >
140
+ {label ? (
141
+ <Text typography='body2SemiBold' lx={{ color: 'base' }}>
142
+ {label}
143
+ </Text>
144
+ ) : (
145
+ <>
146
+ <Text typography='body1SemiBold' lx={{ color: 'base' }}>
147
+ {Math.min(Math.max(currentStep, 0), totalSteps)}
148
+ </Text>
149
+ <Text typography='body2SemiBold' lx={{ color: 'muted' }}>
150
+ /{totalSteps}
151
+ </Text>
152
+ </>
153
+ )}
154
+ </Box>
155
+ </Box>
156
+ );
157
+ };
158
+
159
+ Stepper.displayName = 'Stepper';
@@ -0,0 +1,2 @@
1
+ export * from './Stepper';
2
+ export * from './types';
@@ -0,0 +1,21 @@
1
+ import { StyledViewProps } from '../../../styles';
2
+
3
+ export type StepperProps = {
4
+ /**
5
+ * Current step number (1-based). Use 0 or negative to show minimal dot (before starting).
6
+ */
7
+ currentStep: number;
8
+ /**
9
+ * Total number of steps.
10
+ */
11
+ totalSteps: number;
12
+ /**
13
+ * Whether the stepper is disabled. Changes the progress arc to a muted style.
14
+ * @default false
15
+ */
16
+ disabled?: boolean;
17
+ /**
18
+ * Optional custom label. Defaults to "{currentStep}/{totalSteps}".
19
+ */
20
+ label?: string;
21
+ } & Omit<StyledViewProps, 'children'>;
@@ -15,6 +15,7 @@ import { Box, Pressable } from '../Utility';
15
15
  import { TabBarContextProvider, useTabBarContext } from './TabBarContext';
16
16
  import { TabBarItemProps, TabBarProps } from './types';
17
17
 
18
+ export const TAB_BAR_HEIGHT = 56;
18
19
  const PILL_INSET = 4;
19
20
 
20
21
  /**
@@ -239,7 +240,7 @@ const useStyles = () =>
239
240
  useStyleSheet(
240
241
  (t) => ({
241
242
  container: {
242
- height: t.sizes.s56,
243
+ height: TAB_BAR_HEIGHT,
243
244
  flexDirection: 'row',
244
245
  justifyContent: 'center',
245
246
  padding: t.spacings.s4,
@@ -1,2 +1,2 @@
1
- export { TabBar, TabBarItem } from './TabBar';
1
+ export { TabBar, TabBarItem, TAB_BAR_HEIGHT } from './TabBar';
2
2
  export type { TabBarProps, TabBarItemProps } from './types';