@ledgerhq/lumen-ui-rnative 0.0.70 → 0.0.71
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/.storybook/preview.tsx +4 -0
- package/dist/package.json +4 -4
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/lib/Animations/Pulse/Pulse.d.ts +3 -0
- package/dist/src/lib/Animations/Pulse/Pulse.d.ts.map +1 -0
- package/dist/src/lib/Animations/Pulse/Pulse.js +46 -0
- package/dist/src/lib/Animations/Pulse/Pulse.stories.d.ts +9 -0
- package/dist/src/lib/Animations/Pulse/Pulse.stories.d.ts.map +1 -0
- package/dist/src/lib/Animations/Pulse/Pulse.stories.js +38 -0
- package/dist/src/lib/Animations/Pulse/index.d.ts +3 -0
- package/dist/src/lib/Animations/Pulse/index.d.ts.map +1 -0
- package/dist/src/lib/Animations/Pulse/index.js +2 -0
- package/dist/src/lib/Animations/Pulse/types.d.ts +18 -0
- package/dist/src/lib/Animations/Pulse/types.d.ts.map +1 -0
- package/dist/src/lib/Animations/Pulse/types.js +1 -0
- package/dist/src/lib/Animations/Spin/Spin.d.ts +3 -0
- package/dist/src/lib/Animations/Spin/Spin.d.ts.map +1 -0
- package/dist/src/lib/Animations/Spin/Spin.js +23 -0
- package/dist/src/lib/Animations/Spin/Spin.stories.d.ts +9 -0
- package/dist/src/lib/Animations/Spin/Spin.stories.d.ts.map +1 -0
- package/dist/src/lib/Animations/Spin/Spin.stories.js +27 -0
- package/dist/src/lib/Animations/Spin/index.d.ts +3 -0
- package/dist/src/lib/Animations/Spin/index.d.ts.map +1 -0
- package/dist/src/lib/Animations/Spin/index.js +2 -0
- package/dist/src/lib/Animations/Spin/types.d.ts +14 -0
- package/dist/src/lib/Animations/Spin/types.d.ts.map +1 -0
- package/dist/src/lib/Animations/Spin/types.js +1 -0
- package/dist/src/lib/Animations/index.d.ts +4 -0
- package/dist/src/lib/Animations/index.d.ts.map +1 -0
- package/dist/src/lib/Animations/index.js +3 -0
- package/dist/src/lib/Animations/types.d.ts +2 -0
- package/dist/src/lib/Animations/types.d.ts.map +1 -0
- package/dist/src/lib/Animations/types.js +1 -0
- package/dist/src/lib/Components/AmountDisplay/AmountDisplay.d.ts +5 -7
- package/dist/src/lib/Components/AmountDisplay/AmountDisplay.d.ts.map +1 -1
- package/dist/src/lib/Components/AmountDisplay/AmountDisplay.js +7 -6
- package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.d.ts +1 -0
- package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.d.ts.map +1 -1
- package/dist/src/lib/Components/AmountDisplay/AmountDisplay.stories.js +5 -0
- package/dist/src/lib/Components/AmountDisplay/types.d.ts +7 -1
- package/dist/src/lib/Components/AmountDisplay/types.d.ts.map +1 -1
- package/dist/src/lib/Components/Spinner/Spinner.d.ts.map +1 -1
- package/dist/src/lib/Components/Spinner/Spinner.js +2 -23
- package/dist/src/lib/Components/TabBar/TabBar.d.ts +1 -0
- package/dist/src/lib/Components/TabBar/TabBar.d.ts.map +1 -1
- package/dist/src/lib/Components/TabBar/TabBar.js +2 -1
- package/dist/src/lib/Components/TabBar/index.d.ts +1 -1
- package/dist/src/lib/Components/TabBar/index.d.ts.map +1 -1
- package/dist/src/lib/Components/TabBar/index.js +1 -1
- package/dist/src/lib/Components/TileButton/TileButton.d.ts +4 -3
- package/dist/src/lib/Components/TileButton/TileButton.d.ts.map +1 -1
- package/dist/src/lib/Components/TileButton/TileButton.js +3 -4
- package/dist/src/styles/types/factories.types.d.ts +1 -1
- package/dist/src/styles/types/factories.types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +1 -0
- package/src/lib/Animations/Pulse/Pulse.mdx +86 -0
- package/src/lib/Animations/Pulse/Pulse.stories.tsx +90 -0
- package/src/lib/Animations/Pulse/Pulse.tsx +55 -0
- package/src/lib/Animations/Pulse/index.ts +2 -0
- package/src/lib/Animations/Pulse/types.ts +18 -0
- package/src/lib/Animations/Spin/Spin.mdx +85 -0
- package/src/lib/Animations/Spin/Spin.stories.tsx +72 -0
- package/src/lib/Animations/Spin/Spin.tsx +34 -0
- package/src/lib/Animations/Spin/index.ts +2 -0
- package/src/lib/Animations/Spin/types.ts +14 -0
- package/src/lib/Animations/index.ts +3 -0
- package/src/lib/Animations/types.ts +11 -0
- package/src/lib/Components/AmountDisplay/AmountDisplay.mdx +6 -0
- package/src/lib/Components/AmountDisplay/AmountDisplay.stories.tsx +12 -0
- package/src/lib/Components/AmountDisplay/AmountDisplay.test.tsx +13 -0
- package/src/lib/Components/AmountDisplay/AmountDisplay.tsx +39 -35
- package/src/lib/Components/AmountDisplay/types.ts +7 -1
- package/src/lib/Components/Spinner/Spinner.tsx +3 -35
- package/src/lib/Components/TabBar/TabBar.tsx +2 -1
- package/src/lib/Components/TabBar/index.ts +1 -1
- package/src/lib/Components/TileButton/TileButton.tsx +35 -44
- package/src/styles/types/factories.types.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ledgerhq/lumen-ui-rnative",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.71",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"./styles": "./src/styles/index.ts"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@ledgerhq/lumen-utils-shared": "0.0.
|
|
30
|
+
"@ledgerhq/lumen-utils-shared": "0.0.19",
|
|
31
31
|
"i18next": "^23.7.0",
|
|
32
32
|
"react-i18next": "^14.0.0"
|
|
33
33
|
},
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@types/react": "^19.0.0",
|
|
39
39
|
"@gorhom/bottom-sheet": "^5.0.0",
|
|
40
|
-
"@ledgerhq/lumen-design-core": "0.0.
|
|
41
|
-
"react": "19.0.0",
|
|
40
|
+
"@ledgerhq/lumen-design-core": "0.0.52",
|
|
41
|
+
"react": "^19.0.0",
|
|
42
42
|
"react-native": "~0.79.7",
|
|
43
43
|
"react-native-reanimated": "^3.0.0",
|
|
44
44
|
"react-native-safe-area-context": "^4.0.0 || ^5.0.0",
|
package/src/index.ts
CHANGED
|
@@ -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,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,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
|
+
};
|
|
@@ -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
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { View, Text, StyleSheet } from 'react-native';
|
|
1
|
+
import { View, Text } from 'react-native';
|
|
3
2
|
import { useStyleSheet } from '../../../styles';
|
|
4
|
-
import {
|
|
3
|
+
import { Pulse } from '../../Animations/Pulse';
|
|
4
|
+
import { Box } from '../Utility';
|
|
5
5
|
import { AmountDisplayProps } from './types';
|
|
6
6
|
|
|
7
7
|
const useStyles = () => {
|
|
@@ -71,39 +71,43 @@ const useStyles = () => {
|
|
|
71
71
|
* <AmountDisplay value={1234.56} formatter={usdFormatter} hidden={true} />
|
|
72
72
|
* ```
|
|
73
73
|
*/
|
|
74
|
-
export const AmountDisplay =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
export const AmountDisplay = ({
|
|
75
|
+
value,
|
|
76
|
+
formatter,
|
|
77
|
+
hidden = false,
|
|
78
|
+
loading = false,
|
|
79
|
+
...props
|
|
80
|
+
}: AmountDisplayProps) => {
|
|
81
|
+
const styles = useStyles();
|
|
82
|
+
const parts = formatter(value);
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
style={
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
return (
|
|
85
|
+
<Box {...props}>
|
|
86
|
+
<Pulse animate={loading}>
|
|
87
|
+
<View style={styles.container}>
|
|
88
|
+
{(parts.currencyPosition === undefined ||
|
|
89
|
+
parts.currencyPosition === 'start') && (
|
|
90
|
+
<Text style={[styles.currencyStartText, styles.spacingStart]}>
|
|
91
|
+
{parts.currencyText}
|
|
92
|
+
</Text>
|
|
93
|
+
)}
|
|
94
|
+
<Text style={styles.integerText}>
|
|
95
|
+
{hidden ? '••••' : parts.integerPart}
|
|
89
96
|
</Text>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
{
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
);
|
|
106
|
-
},
|
|
107
|
-
);
|
|
97
|
+
{parts.decimalPart && !hidden && (
|
|
98
|
+
<Text style={styles.decimalText}>
|
|
99
|
+
{(parts.decimalSeparator || '.') + parts.decimalPart}
|
|
100
|
+
</Text>
|
|
101
|
+
)}
|
|
102
|
+
{parts.currencyPosition === 'end' && (
|
|
103
|
+
<Text style={[styles.currencyEndText, styles.spacingEnd]}>
|
|
104
|
+
{parts.currencyText}
|
|
105
|
+
</Text>
|
|
106
|
+
)}
|
|
107
|
+
</View>
|
|
108
|
+
</Pulse>
|
|
109
|
+
</Box>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
108
112
|
|
|
109
113
|
AmountDisplay.displayName = 'AmountDisplay';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ViewProps } from 'react-native';
|
|
2
|
+
import { StyledViewProps } from '../../../styles';
|
|
2
3
|
|
|
3
4
|
export type FormattedValue = {
|
|
4
5
|
/**
|
|
@@ -45,4 +46,9 @@ export type AmountDisplayProps = ViewProps & {
|
|
|
45
46
|
* @default false
|
|
46
47
|
*/
|
|
47
48
|
hidden?: boolean;
|
|
48
|
-
|
|
49
|
+
/**
|
|
50
|
+
* When true, applies a pulse animation to indicate the amount is being fetched or updated.
|
|
51
|
+
* @default false
|
|
52
|
+
*/
|
|
53
|
+
loading?: boolean;
|
|
54
|
+
} & Omit<StyledViewProps, 'children'>;
|