@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
@@ -22,6 +22,9 @@
22
22
  },
23
23
  "link": {
24
24
  "opensInNewTabAriaLabel": "(新しいタブで開く)"
25
+ },
26
+ "stepper": {
27
+ "progressAriaLabel": "ステップ {{currentStep}}/{{totalSteps}}"
25
28
  }
26
29
  }
27
30
  }
@@ -22,6 +22,9 @@
22
22
  },
23
23
  "link": {
24
24
  "opensInNewTabAriaLabel": "(새 탭에서 열기)"
25
+ },
26
+ "stepper": {
27
+ "progressAriaLabel": "{{totalSteps}}단계 중 {{currentStep}}단계"
25
28
  }
26
29
  }
27
30
  }
@@ -22,6 +22,9 @@
22
22
  },
23
23
  "link": {
24
24
  "opensInNewTabAriaLabel": "(abre em uma nova aba)"
25
+ },
26
+ "stepper": {
27
+ "progressAriaLabel": "Passo {{currentStep}} de {{totalSteps}}"
25
28
  }
26
29
  }
27
30
  }
@@ -22,6 +22,9 @@
22
22
  },
23
23
  "link": {
24
24
  "opensInNewTabAriaLabel": "(открывается в новой вкладке)"
25
+ },
26
+ "stepper": {
27
+ "progressAriaLabel": "Шаг {{currentStep}} из {{totalSteps}}"
25
28
  }
26
29
  }
27
30
  }
@@ -22,6 +22,9 @@
22
22
  },
23
23
  "link": {
24
24
  "opensInNewTabAriaLabel": "(เปิดในแท็บใหม่)"
25
+ },
26
+ "stepper": {
27
+ "progressAriaLabel": "ขั้นตอนที่ {{currentStep}} จาก {{totalSteps}}"
25
28
  }
26
29
  }
27
30
  }
@@ -22,6 +22,9 @@
22
22
  },
23
23
  "link": {
24
24
  "opensInNewTabAriaLabel": "(yeni sekmede açılır)"
25
+ },
26
+ "stepper": {
27
+ "progressAriaLabel": "{{totalSteps}} adımdan {{currentStep}}. adım"
25
28
  }
26
29
  }
27
30
  }
@@ -22,6 +22,9 @@
22
22
  },
23
23
  "link": {
24
24
  "opensInNewTabAriaLabel": "(在新选项卡中打开)"
25
+ },
26
+ "stepper": {
27
+ "progressAriaLabel": "第 {{currentStep}} 步,共 {{totalSteps}} 步"
25
28
  }
26
29
  }
27
30
  }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { type SupportedLocale, Languages } from './i18n';
2
2
  export * from './lib/Components';
3
3
  export * from './lib/Patterns';
4
+ export * from './lib/Animations';
@@ -0,0 +1,86 @@
1
+ import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks';
2
+ import * as PulseStories from './Pulse.stories';
3
+ import { Pulse } from './Pulse';
4
+ import {
5
+ CustomTabs,
6
+ Tab,
7
+ } from '../../../../.storybook/components';
8
+ import CommonRulesDoAndDont from '../../../../.storybook/components/DoVsDont/CommonRulesDoAndDont.mdx';
9
+
10
+ <Meta title='Animations/Pulse' of={PulseStories} />
11
+
12
+ # ⚡ Pulse
13
+
14
+ <CustomTabs>
15
+ <Tab label="Overview">
16
+
17
+ ## Introduction
18
+
19
+ The Pulse animation creates a smooth opacity fade in/out effect, commonly used for loading states and skeleton screens. It provides visual feedback that content is being processed or loaded.
20
+
21
+ ## Anatomy
22
+
23
+ <Canvas of={PulseStories.Base} />
24
+
25
+ - **Children**: The content to animate with opacity changes
26
+ - **Duration**: Complete cycle duration (fade out + fade in)
27
+ - **Animate**: Controls whether the animation is active
28
+
29
+ ## Properties
30
+
31
+ ### Overview
32
+
33
+ <Canvas of={PulseStories.Base} />
34
+ <Controls of={PulseStories.Base} />
35
+
36
+ ### Duration
37
+
38
+ The duration prop controls how long a complete pulse cycle takes (fade out to fade in). Shorter durations create faster, more noticeable pulses, while longer durations provide subtle, gentle feedback.
39
+
40
+ <Canvas of={PulseStories.DurationShowcase} />
41
+
42
+ ### Practical Usage
43
+
44
+ The Pulse animation is commonly used with components like AmountDisplay to indicate that data is being fetched or updated.
45
+
46
+ <Canvas of={PulseStories.WithAmountDisplay} />
47
+
48
+ ## Accessibility
49
+
50
+ - Does not rely on animation alone to convey information
51
+ - Maintains readable opacity range (0.35 to 1.0)
52
+
53
+ </Tab>
54
+ <Tab label="Implementation">
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ npm install @ledgerhq/lumen-ui-rnative
60
+ ```
61
+
62
+ > **Note**: `@ledgerhq/lumen-design-core` and other peer dependencies (`react`, `react-native`, etc.) are required. See our Setup Guide for complete installation.
63
+
64
+ ## Basic Usage
65
+
66
+ ```tsx
67
+ import { Pulse } from '@ledgerhq/lumen-ui-rnative';
68
+ import { View } from 'react-native';
69
+
70
+ function LoadingCard() {
71
+ return (
72
+ <Pulse animate={true}>
73
+ <View style={{ width: 200, height: 100, backgroundColor: '#E0E0E0' }} />
74
+ </Pulse>
75
+ );
76
+ }
77
+ ```
78
+
79
+ ## Performance Considerations
80
+
81
+ - The Pulse component uses `useNativeDriver` for optimal performance on native platforms
82
+ - Animation cleanup is handled automatically on unmount
83
+ - Consider using different duration values for multiple skeleton elements to create a more natural loading pattern
84
+
85
+ </Tab>
86
+ </CustomTabs>
@@ -0,0 +1,90 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
2
+ import { View } from 'react-native';
3
+ import { AmountDisplay } from '../../Components/AmountDisplay';
4
+ import { type FormattedValue } from '../../Components/AmountDisplay/types';
5
+ import { Box, Text } from '../../Components/Utility';
6
+ import { Pulse } from './Pulse';
7
+
8
+ const usdFormatter = (value: number): FormattedValue => {
9
+ const [integerPart, decimalPart] = value.toFixed(2).split(/\.|,/);
10
+
11
+ return {
12
+ integerPart,
13
+ decimalPart,
14
+ currencyText: '$',
15
+ decimalSeparator: '.',
16
+ currencyPosition: 'start',
17
+ };
18
+ };
19
+
20
+ const meta: Meta<typeof Pulse> = {
21
+ title: 'Animations/Pulse',
22
+ component: Pulse,
23
+ parameters: {
24
+ layout: 'centered',
25
+ },
26
+ } satisfies Meta<typeof Pulse>;
27
+
28
+ export default meta;
29
+ type Story = StoryObj<typeof meta>;
30
+
31
+ export const Base: Story = {
32
+ args: {
33
+ duration: 2000,
34
+ animate: true,
35
+ children: (
36
+ <Box lx={{ width: 's48', height: 's48', backgroundColor: 'accent' }} />
37
+ ),
38
+ },
39
+ };
40
+
41
+ export const DurationShowcase: Story = {
42
+ render: () => (
43
+ <View style={{ flexDirection: 'row', gap: 24, alignItems: 'center' }}>
44
+ <View style={{ alignItems: 'center', gap: 8 }}>
45
+ <Pulse duration={1000} animate={true}>
46
+ <Box
47
+ lx={{ width: 's48', height: 's48', backgroundColor: 'accent' }}
48
+ />
49
+ </Pulse>
50
+ <Text typography='body4' lx={{ color: 'muted' }}>
51
+ 1000ms
52
+ </Text>
53
+ </View>
54
+ <View style={{ alignItems: 'center', gap: 8 }}>
55
+ <Pulse duration={2000} animate={true}>
56
+ <Box
57
+ lx={{ width: 's48', height: 's48', backgroundColor: 'accent' }}
58
+ />
59
+ </Pulse>
60
+ <Text typography='body4' lx={{ color: 'muted' }}>
61
+ 2000ms
62
+ </Text>
63
+ </View>
64
+ <View style={{ alignItems: 'center', gap: 8 }}>
65
+ <Pulse duration={3000} animate={true}>
66
+ <Box
67
+ lx={{ width: 's48', height: 's48', backgroundColor: 'accent' }}
68
+ />
69
+ </Pulse>
70
+ <Text typography='body4' lx={{ color: 'muted' }}>
71
+ 3000ms
72
+ </Text>
73
+ </View>
74
+ </View>
75
+ ),
76
+ };
77
+
78
+ export const WithAmountDisplay: Story = {
79
+ render: () => {
80
+ return (
81
+ <View style={{ flexDirection: 'column', alignItems: 'center', gap: 16 }}>
82
+ <AmountDisplay
83
+ formatter={usdFormatter}
84
+ value={1234.56}
85
+ loading={true}
86
+ />
87
+ </View>
88
+ );
89
+ },
90
+ };
@@ -0,0 +1,55 @@
1
+ import { useRef, useEffect, memo } from 'react';
2
+ import { Animated, Easing } from 'react-native';
3
+ import { RuntimeConstants } from '../../utils';
4
+ import { PulseProps } from './types';
5
+
6
+ export const Pulse = memo(
7
+ ({ children, duration = 2000, animate }: PulseProps) => {
8
+ const pulseAnim = useRef(new Animated.Value(1)).current;
9
+ const animationRef = useRef<Animated.CompositeAnimation | null>(null);
10
+
11
+ useEffect(() => {
12
+ if (animate) {
13
+ const animation = Animated.loop(
14
+ Animated.sequence([
15
+ Animated.timing(pulseAnim, {
16
+ toValue: 0,
17
+ duration: duration / 2,
18
+ easing: Easing.linear,
19
+ useNativeDriver: RuntimeConstants.isNative,
20
+ }),
21
+ Animated.timing(pulseAnim, {
22
+ toValue: 1,
23
+ duration: duration / 2,
24
+ easing: Easing.linear,
25
+ useNativeDriver: RuntimeConstants.isNative,
26
+ }),
27
+ ]),
28
+ );
29
+
30
+ animationRef.current = animation;
31
+ animation.start();
32
+ } else {
33
+ animationRef.current?.stop();
34
+ Animated.timing(pulseAnim, {
35
+ toValue: 1,
36
+ duration: 200,
37
+ easing: Easing.out(Easing.ease),
38
+ useNativeDriver: RuntimeConstants.isNative,
39
+ }).start();
40
+ }
41
+
42
+ return () => {
43
+ animationRef.current?.stop();
44
+ };
45
+ }, [pulseAnim, duration, animate]);
46
+
47
+ const pulse = pulseAnim.interpolate({
48
+ inputRange: [0, 1],
49
+ outputRange: [0.35, 1],
50
+ });
51
+
52
+ return <Animated.View style={{ opacity: pulse }}>{children}</Animated.View>;
53
+ },
54
+ );
55
+ Pulse.displayName = 'Pulse';
@@ -0,0 +1,2 @@
1
+ export * from './Pulse';
2
+ export * from './types';
@@ -0,0 +1,18 @@
1
+ import { ReactNode } from 'react';
2
+ import { Duration } from '../types';
3
+
4
+ export type PulseProps = {
5
+ /**
6
+ * The content to animate
7
+ */
8
+ children: ReactNode;
9
+ /**
10
+ * Duration of one complete pulse in milliseconds
11
+ * @default 2000
12
+ */
13
+ duration?: Duration;
14
+ /**
15
+ * Whether the pulse animation should play
16
+ */
17
+ animate?: boolean;
18
+ };
@@ -0,0 +1,85 @@
1
+ import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks';
2
+ import * as SpinStories from './Spin.stories';
3
+ import { Spin } from './Spin';
4
+ import {
5
+ CustomTabs,
6
+ Tab,
7
+ } from '../../../../.storybook/components';
8
+ import CommonRulesDoAndDont from '../../../../.storybook/components/DoVsDont/CommonRulesDoAndDont.mdx';
9
+
10
+ <Meta title='Animations/Spin' of={SpinStories} />
11
+
12
+ # 🔄 Spin
13
+
14
+ <CustomTabs>
15
+ <Tab label="Overview">
16
+
17
+ ## Introduction
18
+
19
+ The Spin animation creates a continuous rotation effect, commonly used for loading indicators and progress spinners. It provides visual feedback that a process is ongoing or content is being loaded.
20
+
21
+ ## Anatomy
22
+
23
+ <Canvas of={SpinStories.Base} />
24
+
25
+ - **Children**: The content to rotate continuously
26
+ - **Duration**: Time for one complete 360° rotation
27
+
28
+ ## Properties
29
+
30
+ ### Overview
31
+
32
+ <Canvas of={SpinStories.Base} />
33
+ <Controls of={SpinStories.Base} />
34
+
35
+ ### Duration
36
+
37
+ The duration prop controls how long one complete rotation takes. Shorter durations create faster spins, while longer durations provide slower, more subtle rotation.
38
+
39
+ <Canvas of={SpinStories.DurationShowcase} />
40
+
41
+ ### Practical Usage
42
+
43
+ The Spin animation is commonly used with the Spinner component to create loading indicators throughout the application.
44
+
45
+ <Canvas of={SpinStories.WithSpinner} />
46
+
47
+ ## Accessibility
48
+
49
+ - Does not rely on animation alone to convey information
50
+ - Should be paired with appropriate loading labels for screen readers
51
+
52
+ </Tab>
53
+ <Tab label="Implementation">
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ npm install @ledgerhq/lumen-ui-rnative
59
+ ```
60
+
61
+ > **Note**: `@ledgerhq/lumen-design-core` and other peer dependencies (`react`, `react-native`, etc.) are required. See our Setup Guide for complete installation.
62
+
63
+ ## Basic Usage
64
+
65
+ ```tsx
66
+ import { Spin } from '@ledgerhq/lumen-ui-rnative';
67
+ import { View } from 'react-native';
68
+
69
+ function LoadingIcon() {
70
+ return (
71
+ <Spin>
72
+ <View style={{ width: 40, height: 40, backgroundColor: '#007AFF' }} />
73
+ </Spin>
74
+ );
75
+ }
76
+ ```
77
+
78
+ ## Performance Considerations
79
+
80
+ - The Spin component uses `useNativeDriver` for optimal performance on native platforms
81
+ - Rotation animation runs continuously until component unmounts
82
+ - Animation cleanup is handled automatically on unmount
83
+
84
+ </Tab>
85
+ </CustomTabs>
@@ -0,0 +1,72 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
2
+ import { View } from 'react-native';
3
+ import { Spinner } from '../../Components/Spinner';
4
+ import { Box, Text } from '../../Components/Utility';
5
+ import { Spin } from './Spin';
6
+
7
+ const meta: Meta<typeof Spin> = {
8
+ title: 'Animations/Spin',
9
+ component: Spin,
10
+ parameters: {
11
+ layout: 'centered',
12
+ },
13
+ } satisfies Meta<typeof Spin>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const Base: Story = {
19
+ args: {
20
+ duration: 1000,
21
+ children: (
22
+ <Box lx={{ width: 's48', height: 's48', backgroundColor: 'accent' }} />
23
+ ),
24
+ },
25
+ };
26
+
27
+ export const DurationShowcase: Story = {
28
+ render: () => (
29
+ <View style={{ flexDirection: 'row', gap: 24, alignItems: 'center' }}>
30
+ <View style={{ alignItems: 'center', gap: 8 }}>
31
+ <Spin duration={500}>
32
+ <Box
33
+ lx={{ width: 's48', height: 's48', backgroundColor: 'accent' }}
34
+ />
35
+ </Spin>
36
+ <Text typography='body4' lx={{ color: 'muted' }}>
37
+ 500ms
38
+ </Text>
39
+ </View>
40
+ <View style={{ alignItems: 'center', gap: 8 }}>
41
+ <Spin duration={1000}>
42
+ <Box
43
+ lx={{ width: 's48', height: 's48', backgroundColor: 'accent' }}
44
+ />
45
+ </Spin>
46
+ <Text typography='body4' lx={{ color: 'muted' }}>
47
+ 1000ms
48
+ </Text>
49
+ </View>
50
+ <View style={{ alignItems: 'center', gap: 8 }}>
51
+ <Spin duration={2000}>
52
+ <Box
53
+ lx={{ width: 's48', height: 's48', backgroundColor: 'accent' }}
54
+ />
55
+ </Spin>
56
+ <Text typography='body4' lx={{ color: 'muted' }}>
57
+ 2000ms
58
+ </Text>
59
+ </View>
60
+ </View>
61
+ ),
62
+ };
63
+
64
+ export const WithSpinner: Story = {
65
+ render: () => {
66
+ return (
67
+ <View style={{ flexDirection: 'column', alignItems: 'center', gap: 16 }}>
68
+ <Spinner size={40} />
69
+ </View>
70
+ );
71
+ },
72
+ };
@@ -0,0 +1,34 @@
1
+ import { memo, useEffect, useRef } from 'react';
2
+ import { Animated, Easing } from 'react-native';
3
+ import { RuntimeConstants } from '../../utils';
4
+ import { SpinProps } from './types';
5
+
6
+ export const Spin = memo(({ children, duration = 1000 }: SpinProps) => {
7
+ const spinValue = useRef(new Animated.Value(0)).current;
8
+
9
+ useEffect(() => {
10
+ const animation = Animated.loop(
11
+ Animated.timing(spinValue, {
12
+ toValue: 1,
13
+ duration: duration,
14
+ easing: Easing.linear,
15
+ useNativeDriver: RuntimeConstants.isNative,
16
+ }),
17
+ );
18
+ animation.start();
19
+
20
+ return () => animation.stop();
21
+ }, [spinValue, duration]);
22
+
23
+ const spin = spinValue.interpolate({
24
+ inputRange: [0, 1],
25
+ outputRange: ['0deg', '360deg'],
26
+ });
27
+
28
+ return (
29
+ <Animated.View style={{ transform: [{ rotate: spin }] }}>
30
+ {children}
31
+ </Animated.View>
32
+ );
33
+ });
34
+ Spin.displayName = 'Spin';
@@ -0,0 +1,2 @@
1
+ export * from './Spin';
2
+ export * from './types';
@@ -0,0 +1,14 @@
1
+ import { ReactNode } from 'react';
2
+ import { Duration } from '../types';
3
+
4
+ export type SpinProps = {
5
+ /**
6
+ * The content to animate
7
+ */
8
+ children: ReactNode;
9
+ /**
10
+ * Duration of one complete rotation in milliseconds
11
+ * @default 1000
12
+ */
13
+ duration?: Duration;
14
+ };
@@ -0,0 +1,3 @@
1
+ export * from './Spin';
2
+ export * from './Pulse';
3
+ export * from './types';
@@ -0,0 +1,11 @@
1
+ export type Duration =
2
+ | 75
3
+ | 100
4
+ | 150
5
+ | 200
6
+ | 300
7
+ | 500
8
+ | 700
9
+ | 1000
10
+ | 2000
11
+ | 3000;
@@ -45,6 +45,12 @@ The `hidden` prop allows you to toggle amount visibility for privacy-sensitive a
45
45
 
46
46
  <Canvas of={AmountDisplayStories.WithHideButton} />
47
47
 
48
+ ### Loading State
49
+
50
+ The `loading` prop displays a pulse animation around the amount to indicate that data is being fetched or updated. When set to `true`, the component applies a subtle pulsing effect while maintaining the displayed value. This is useful for showing users that the amount is being refreshed in the background.
51
+
52
+ <Canvas of={AmountDisplayStories.Loading} />
53
+
48
54
  ## Accessibility
49
55
 
50
56
  - Uses semantic components for proper screen reader support
@@ -126,3 +126,15 @@ export const WithHideButton: Story = {
126
126
  );
127
127
  },
128
128
  };
129
+
130
+ export const Loading: Story = {
131
+ render: (props) => {
132
+ return (
133
+ <AmountDisplay
134
+ formatter={props.formatter}
135
+ value={1234.56}
136
+ loading={true}
137
+ />
138
+ );
139
+ },
140
+ };
@@ -178,4 +178,17 @@ describe('AmountDisplay', () => {
178
178
  expect(screen.getByText('••••')).toBeTruthy();
179
179
  expect(screen.queryByText('.56')).toBeNull();
180
180
  });
181
+
182
+ it('renders correctly when loading is true', () => {
183
+ const formatter = createFormatter();
184
+ render(
185
+ <TestWrapper>
186
+ <AmountDisplay value={1234.56} formatter={formatter} loading={true} />
187
+ </TestWrapper>,
188
+ );
189
+
190
+ expect(screen.getByText('USD')).toBeTruthy();
191
+ expect(screen.getByText('1234')).toBeTruthy();
192
+ expect(screen.getByText('.56')).toBeTruthy();
193
+ });
181
194
  });