@ledgerhq/lumen-ui-rnative 0.0.52 → 0.0.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +2 -2
- package/dist/src/lib/Components/BaseInput/BaseInput.d.ts +1 -0
- package/dist/src/lib/Components/BaseInput/BaseInput.d.ts.map +1 -1
- package/dist/src/lib/Components/BaseInput/BaseInput.js +6 -2
- package/dist/src/lib/Components/BaseInput/types.d.ts +5 -1
- package/dist/src/lib/Components/BaseInput/types.d.ts.map +1 -1
- package/dist/src/lib/Components/SearchInput/SearchInput.d.ts +3 -1
- package/dist/src/lib/Components/SearchInput/SearchInput.d.ts.map +1 -1
- package/dist/src/lib/Components/SearchInput/SearchInput.js +12 -2
- package/dist/src/lib/Components/SearchInput/SearchInput.stories.d.ts.map +1 -1
- package/dist/src/lib/Components/SearchInput/SearchInput.stories.js +3 -0
- package/dist/src/lib/Components/SearchInput/types.d.ts +7 -1
- package/dist/src/lib/Components/SearchInput/types.d.ts.map +1 -1
- package/dist/src/lib/Components/Tile/Tile.d.ts.map +1 -1
- package/dist/src/lib/Components/Tile/Tile.js +1 -0
- package/dist/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.d.ts +32 -0
- package/dist/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.d.ts.map +1 -0
- package/dist/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.js +75 -0
- package/dist/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.stories.d.ts +11 -0
- package/dist/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.stories.d.ts.map +1 -0
- package/dist/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.stories.js +107 -0
- package/dist/src/lib/Components/Utility/Gradient/LinearGradient/index.d.ts +3 -0
- package/dist/src/lib/Components/Utility/Gradient/LinearGradient/index.d.ts.map +1 -0
- package/dist/src/lib/Components/Utility/Gradient/LinearGradient/index.js +2 -0
- package/dist/src/lib/Components/Utility/Gradient/LinearGradient/types.d.ts +32 -0
- package/dist/src/lib/Components/Utility/Gradient/LinearGradient/types.d.ts.map +1 -0
- package/dist/src/lib/Components/Utility/Gradient/LinearGradient/types.js +1 -0
- package/dist/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.d.ts +31 -0
- package/dist/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.d.ts.map +1 -0
- package/dist/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.js +52 -0
- package/dist/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.stories.d.ts +10 -0
- package/dist/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.stories.d.ts.map +1 -0
- package/dist/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.stories.js +101 -0
- package/dist/src/lib/Components/Utility/Gradient/RadialGradient/index.d.ts +3 -0
- package/dist/src/lib/Components/Utility/Gradient/RadialGradient/index.d.ts.map +1 -0
- package/dist/src/lib/Components/Utility/Gradient/RadialGradient/index.js +2 -0
- package/dist/src/lib/Components/Utility/Gradient/RadialGradient/types.d.ts +32 -0
- package/dist/src/lib/Components/Utility/Gradient/RadialGradient/types.d.ts.map +1 -0
- package/dist/src/lib/Components/Utility/Gradient/RadialGradient/types.js +1 -0
- package/dist/src/lib/Components/Utility/Gradient/gradient.types.d.ts +21 -0
- package/dist/src/lib/Components/Utility/Gradient/gradient.types.d.ts.map +1 -0
- package/dist/src/lib/Components/Utility/Gradient/gradient.types.js +1 -0
- package/dist/src/lib/Components/Utility/Gradient/utils/resolveGradientColor.d.ts +21 -0
- package/dist/src/lib/Components/Utility/Gradient/utils/resolveGradientColor.d.ts.map +1 -0
- package/dist/src/lib/Components/Utility/Gradient/utils/resolveGradientColor.js +38 -0
- package/dist/src/lib/Components/Utility/index.d.ts +2 -0
- package/dist/src/lib/Components/Utility/index.d.ts.map +1 -1
- package/dist/src/lib/Components/Utility/index.js +2 -0
- package/package.json +2 -2
- package/src/lib/Components/BaseInput/BaseInput.tsx +7 -2
- package/src/lib/Components/BaseInput/types.ts +5 -1
- package/src/lib/Components/SearchInput/SearchInput.stories.tsx +3 -0
- package/src/lib/Components/SearchInput/SearchInput.tsx +32 -8
- package/src/lib/Components/SearchInput/types.ts +7 -1
- package/src/lib/Components/Tile/Tile.tsx +1 -0
- package/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.mdx +142 -0
- package/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.stories.tsx +173 -0
- package/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.test.tsx +69 -0
- package/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.tsx +127 -0
- package/src/lib/Components/Utility/Gradient/LinearGradient/index.ts +2 -0
- package/src/lib/Components/Utility/Gradient/LinearGradient/types.ts +42 -0
- package/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.mdx +109 -0
- package/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.stories.tsx +148 -0
- package/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.test.tsx +69 -0
- package/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.tsx +102 -0
- package/src/lib/Components/Utility/Gradient/RadialGradient/index.ts +2 -0
- package/src/lib/Components/Utility/Gradient/RadialGradient/types.ts +32 -0
- package/src/lib/Components/Utility/Gradient/gradient.types.ts +22 -0
- package/src/lib/Components/Utility/Gradient/utils/resolveGradientColor.test.ts +144 -0
- package/src/lib/Components/Utility/Gradient/utils/resolveGradientColor.ts +59 -0
- package/src/lib/Components/Utility/index.ts +2 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
|
+
import { useTheme } from 'src/styles';
|
|
3
|
+
import { Box } from '../../Box';
|
|
4
|
+
import { Text } from '../../Text';
|
|
5
|
+
import { LinearGradient } from './LinearGradient';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof LinearGradient> = {
|
|
8
|
+
component: LinearGradient,
|
|
9
|
+
title: 'Utility/LinearGradient',
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'centered',
|
|
12
|
+
backgrounds: { default: 'light' },
|
|
13
|
+
docs: {
|
|
14
|
+
source: {
|
|
15
|
+
language: 'tsx',
|
|
16
|
+
format: true,
|
|
17
|
+
type: 'code',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof LinearGradient>;
|
|
25
|
+
|
|
26
|
+
export const Base: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
direction: 'to-bottom',
|
|
29
|
+
stops: [{ color: 'accent' }, { color: 'active', opacity: 0 }],
|
|
30
|
+
lx: {
|
|
31
|
+
width: 's192',
|
|
32
|
+
height: 's56',
|
|
33
|
+
borderRadius: 'lg',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const DirectionShowcase: Story = {
|
|
39
|
+
render: () => {
|
|
40
|
+
const orientations = [
|
|
41
|
+
'to-bottom',
|
|
42
|
+
'to-right',
|
|
43
|
+
'to-top',
|
|
44
|
+
'to-left',
|
|
45
|
+
'to-bottomright',
|
|
46
|
+
'to-bottomleft',
|
|
47
|
+
'to-topright',
|
|
48
|
+
'to-topleft',
|
|
49
|
+
0,
|
|
50
|
+
90,
|
|
51
|
+
180,
|
|
52
|
+
270,
|
|
53
|
+
] as const;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Box lx={{ flexDirection: 'row', flexWrap: 'wrap', gap: 's20' }}>
|
|
57
|
+
{orientations.map((orientation) => {
|
|
58
|
+
return (
|
|
59
|
+
<Box key={orientation} lx={{ gap: 's4' }}>
|
|
60
|
+
<LinearGradient
|
|
61
|
+
direction={orientation}
|
|
62
|
+
stops={[{ color: 'accent' }, { color: 'active', opacity: 0 }]}
|
|
63
|
+
lx={{ height: 's40', width: 's176', borderRadius: 'md' }}
|
|
64
|
+
/>
|
|
65
|
+
<Text typography='body4' lx={{ color: 'base' }}>
|
|
66
|
+
{orientation}
|
|
67
|
+
</Text>
|
|
68
|
+
</Box>
|
|
69
|
+
);
|
|
70
|
+
})}
|
|
71
|
+
</Box>
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const WithChildren: Story = {
|
|
77
|
+
args: {
|
|
78
|
+
direction: 'to-bottomright',
|
|
79
|
+
stops: [{ color: 'accent' }, { color: 'active', opacity: 0, offset: 0.75 }],
|
|
80
|
+
lx: {
|
|
81
|
+
padding: 's24',
|
|
82
|
+
borderRadius: 'lg',
|
|
83
|
+
width: 's288',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
render: (args) => (
|
|
87
|
+
<LinearGradient {...args}>
|
|
88
|
+
<Text typography='heading2SemiBold' lx={{ color: 'base' }}>
|
|
89
|
+
With Children
|
|
90
|
+
</Text>
|
|
91
|
+
<Text typography='body2' lx={{ color: 'base', marginTop: 's8' }}>
|
|
92
|
+
Adapt height based on content.
|
|
93
|
+
</Text>
|
|
94
|
+
<Text typography='body3' lx={{ color: 'base', marginTop: 's8' }}>
|
|
95
|
+
Lorem ipsum dolor sit amet consectetur adipisicing elit consectetur
|
|
96
|
+
adipisicing elit adipisicing elit. Mas adename labin anet.
|
|
97
|
+
</Text>
|
|
98
|
+
</LinearGradient>
|
|
99
|
+
),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const WithMultipleStops: Story = {
|
|
103
|
+
args: {
|
|
104
|
+
direction: 'to-right',
|
|
105
|
+
stops: [
|
|
106
|
+
{ color: 'accent', offset: 0, opacity: 1 },
|
|
107
|
+
{ color: 'warning', offset: 0.5, opacity: 1 },
|
|
108
|
+
{ color: 'errorStrong', offset: 1, opacity: 1 },
|
|
109
|
+
],
|
|
110
|
+
lx: {
|
|
111
|
+
borderRadius: 'md',
|
|
112
|
+
height: 's56',
|
|
113
|
+
width: 's288',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
render: (args) => {
|
|
117
|
+
const { theme } = useTheme();
|
|
118
|
+
console.log({ theme });
|
|
119
|
+
return (
|
|
120
|
+
<Box lx={{ gap: 's12' }}>
|
|
121
|
+
<LinearGradient {...args} />
|
|
122
|
+
<Box lx={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
|
123
|
+
<Text>accent</Text>
|
|
124
|
+
<Text>warning</Text>
|
|
125
|
+
<Text>errorStrong</Text>
|
|
126
|
+
</Box>
|
|
127
|
+
</Box>
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const CryptoGradients: Story = {
|
|
133
|
+
args: {
|
|
134
|
+
direction: 'to-bottomright',
|
|
135
|
+
lx: {
|
|
136
|
+
borderRadius: 'md',
|
|
137
|
+
height: 's56',
|
|
138
|
+
width: 's288',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
render: (args) => {
|
|
142
|
+
const { theme } = useTheme();
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Box
|
|
146
|
+
lx={{
|
|
147
|
+
gap: 's20',
|
|
148
|
+
flexDirection: 'row',
|
|
149
|
+
flexWrap: 'wrap',
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{Object.entries(theme.colors.gradients.crypto).map(
|
|
153
|
+
([key, gradient]) => (
|
|
154
|
+
<Box key={key} lx={{ gap: 's4' }}>
|
|
155
|
+
<LinearGradient
|
|
156
|
+
{...args}
|
|
157
|
+
lx={{
|
|
158
|
+
height: 's40',
|
|
159
|
+
width: 's176',
|
|
160
|
+
borderRadius: 'md',
|
|
161
|
+
}}
|
|
162
|
+
stops={gradient}
|
|
163
|
+
></LinearGradient>
|
|
164
|
+
<Text typography='body4' lx={{ color: 'base' }}>
|
|
165
|
+
{key}
|
|
166
|
+
</Text>
|
|
167
|
+
</Box>
|
|
168
|
+
),
|
|
169
|
+
)}
|
|
170
|
+
</Box>
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
|
|
3
|
+
import { render, screen } from '@testing-library/react-native';
|
|
4
|
+
import React, { createRef } from 'react';
|
|
5
|
+
import { Text, View } from 'react-native';
|
|
6
|
+
|
|
7
|
+
import { ThemeProvider } from '../../../ThemeProvider/ThemeProvider';
|
|
8
|
+
import { LinearGradient } from './LinearGradient';
|
|
9
|
+
|
|
10
|
+
const renderWithProvider = (component: React.ReactElement) => {
|
|
11
|
+
return render(
|
|
12
|
+
<ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
|
|
13
|
+
{component}
|
|
14
|
+
</ThemeProvider>,
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('LinearGradient', () => {
|
|
19
|
+
it('should have correct display name', () => {
|
|
20
|
+
expect(LinearGradient.displayName).toBe('LinearGradient');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should render children', () => {
|
|
24
|
+
renderWithProvider(
|
|
25
|
+
<LinearGradient stops={[{ color: '#FF6B6B' }, { color: '#4ECDC4' }]}>
|
|
26
|
+
<Text>Child Content</Text>
|
|
27
|
+
</LinearGradient>,
|
|
28
|
+
);
|
|
29
|
+
expect(screen.getByText('Child Content')).toBeTruthy();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should forward ref', () => {
|
|
33
|
+
const ref = createRef<View>();
|
|
34
|
+
renderWithProvider(
|
|
35
|
+
<LinearGradient
|
|
36
|
+
ref={ref}
|
|
37
|
+
stops={[{ color: '#FF6B6B' }, { color: '#4ECDC4' }]}
|
|
38
|
+
/>,
|
|
39
|
+
);
|
|
40
|
+
expect(ref.current).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should resolve lx style tokens', () => {
|
|
44
|
+
renderWithProvider(
|
|
45
|
+
<LinearGradient
|
|
46
|
+
testID='linear-gradient'
|
|
47
|
+
stops={[{ color: '#FF6B6B' }, { color: '#4ECDC4' }]}
|
|
48
|
+
lx={{ padding: 's16', marginTop: 's8' }}
|
|
49
|
+
/>,
|
|
50
|
+
);
|
|
51
|
+
const gradient = screen.getByTestId('linear-gradient');
|
|
52
|
+
expect(gradient.props.style.padding).toBe(16);
|
|
53
|
+
expect(gradient.props.style.marginTop).toBe(8);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should pass accessibility props', () => {
|
|
57
|
+
renderWithProvider(
|
|
58
|
+
<LinearGradient
|
|
59
|
+
testID='linear-gradient'
|
|
60
|
+
stops={[{ color: '#FF6B6B' }, { color: '#4ECDC4' }]}
|
|
61
|
+
accessibilityLabel='Decorative gradient'
|
|
62
|
+
accessible={false}
|
|
63
|
+
/>,
|
|
64
|
+
);
|
|
65
|
+
const gradient = screen.getByTestId('linear-gradient');
|
|
66
|
+
expect(gradient.props.accessibilityLabel).toBe('Decorative gradient');
|
|
67
|
+
expect(gradient.props.accessible).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { forwardRef, memo, useId, useMemo } from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import type { View } from 'react-native';
|
|
4
|
+
import Svg, {
|
|
5
|
+
Defs,
|
|
6
|
+
LinearGradient as SvgLinearGradient,
|
|
7
|
+
Rect,
|
|
8
|
+
Stop,
|
|
9
|
+
} from 'react-native-svg';
|
|
10
|
+
import { useTheme } from '../../../../../styles';
|
|
11
|
+
import { Box } from '../../Box';
|
|
12
|
+
import { processGradientStops } from '../utils/resolveGradientColor';
|
|
13
|
+
import type {
|
|
14
|
+
GradientCoordinates,
|
|
15
|
+
LinearGradientDirection,
|
|
16
|
+
LinearGradientProps,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
const DIRECTION_MAP: Record<LinearGradientDirection, GradientCoordinates> = {
|
|
20
|
+
'to-bottom': { x1: '0%', y1: '0%', x2: '0%', y2: '100%' },
|
|
21
|
+
'to-top': { x1: '0%', y1: '100%', x2: '0%', y2: '0%' },
|
|
22
|
+
'to-right': { x1: '0%', y1: '0%', x2: '100%', y2: '0%' },
|
|
23
|
+
'to-left': { x1: '100%', y1: '0%', x2: '0%', y2: '0%' },
|
|
24
|
+
'to-bottomright': { x1: '0%', y1: '0%', x2: '100%', y2: '100%' },
|
|
25
|
+
'to-bottomleft': { x1: '100%', y1: '0%', x2: '0%', y2: '100%' },
|
|
26
|
+
'to-topright': { x1: '0%', y1: '100%', x2: '100%', y2: '0%' },
|
|
27
|
+
'to-topleft': { x1: '100%', y1: '100%', x2: '0%', y2: '0%' },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const angleToCoordinates = (angle: number): GradientCoordinates => {
|
|
31
|
+
const normalizedAngle = ((angle % 360) + 360) % 360;
|
|
32
|
+
|
|
33
|
+
// Convert to radians (CSS: 0° is up, going clockwise)
|
|
34
|
+
// SVG: we need to map this to x1,y1 -> x2,y2
|
|
35
|
+
const radians = ((normalizedAngle - 90) * Math.PI) / 180;
|
|
36
|
+
const x2 = Math.cos(radians) * 0.5 + 0.5;
|
|
37
|
+
const y2 = Math.sin(radians) * 0.5 + 0.5;
|
|
38
|
+
const x1 = 1 - x2;
|
|
39
|
+
const y1 = 1 - y2;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
x1: `${Math.round(x1 * 100)}%`,
|
|
43
|
+
y1: `${Math.round(y1 * 100)}%`,
|
|
44
|
+
x2: `${Math.round(x2 * 100)}%`,
|
|
45
|
+
y2: `${Math.round(y2 * 100)}%`,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const styles = StyleSheet.create({
|
|
50
|
+
gradient: {
|
|
51
|
+
...StyleSheet.absoluteFillObject,
|
|
52
|
+
width: '100%',
|
|
53
|
+
height: '100%',
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* LinearGradient - A container component that renders a linear gradient background.
|
|
59
|
+
*
|
|
60
|
+
* Uses react-native-svg to render gradients that work consistently across platforms.
|
|
61
|
+
* Extends Box, so it supports all lx style props for layout and sizing.
|
|
62
|
+
*
|
|
63
|
+
* @see {@link https://ldls.vercel.app/?path=/docs/utility-lineargradient--docs Storybook}
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```tsx
|
|
67
|
+
* import { LinearGradient } from '@ledgerhq/lumen-ui-rnative';
|
|
68
|
+
*
|
|
69
|
+
* // Basic usage with direction preset
|
|
70
|
+
* <LinearGradient
|
|
71
|
+
* direction="to-bottomright"
|
|
72
|
+
* stops={[
|
|
73
|
+
* { color: '#FF6B6B', offset: 0 },
|
|
74
|
+
* { color: '#6BCB77', offset: 1 },
|
|
75
|
+
* ]}
|
|
76
|
+
* lx={{ height: 's200', borderRadius: 'lg' }}
|
|
77
|
+
* />
|
|
78
|
+
*/
|
|
79
|
+
export const LinearGradient = memo(
|
|
80
|
+
forwardRef<View, LinearGradientProps>(
|
|
81
|
+
({ direction = 'to-bottom', stops, children, lx = {}, ...props }, ref) => {
|
|
82
|
+
const gradientId = useId();
|
|
83
|
+
const { theme } = useTheme();
|
|
84
|
+
|
|
85
|
+
const coordinates = useMemo(() => {
|
|
86
|
+
const isCustomAngle = typeof direction === 'number';
|
|
87
|
+
return isCustomAngle
|
|
88
|
+
? angleToCoordinates(direction)
|
|
89
|
+
: DIRECTION_MAP[direction];
|
|
90
|
+
}, [direction]);
|
|
91
|
+
|
|
92
|
+
const processedStops = useMemo(
|
|
93
|
+
() => processGradientStops(stops, theme.colors.bg),
|
|
94
|
+
[stops, theme.colors.bg],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Box ref={ref} lx={{ overflow: 'hidden', ...lx }} {...props}>
|
|
99
|
+
<Svg style={styles.gradient} preserveAspectRatio='none'>
|
|
100
|
+
<Defs>
|
|
101
|
+
<SvgLinearGradient id={gradientId} {...coordinates}>
|
|
102
|
+
{processedStops.map((stop, index) => (
|
|
103
|
+
<Stop
|
|
104
|
+
key={index}
|
|
105
|
+
offset={`${stop.offset * 100}%`}
|
|
106
|
+
stopColor={stop.color}
|
|
107
|
+
stopOpacity={stop.opacity}
|
|
108
|
+
/>
|
|
109
|
+
))}
|
|
110
|
+
</SvgLinearGradient>
|
|
111
|
+
</Defs>
|
|
112
|
+
<Rect
|
|
113
|
+
x='0'
|
|
114
|
+
y='0'
|
|
115
|
+
width='100%'
|
|
116
|
+
height='100%'
|
|
117
|
+
fill={`url(#${gradientId})`}
|
|
118
|
+
/>
|
|
119
|
+
</Svg>
|
|
120
|
+
{children}
|
|
121
|
+
</Box>
|
|
122
|
+
);
|
|
123
|
+
},
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
LinearGradient.displayName = 'LinearGradient';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { StyledViewProps } from '../../../../../styles';
|
|
3
|
+
import { GradientStop } from '../gradient.types';
|
|
4
|
+
|
|
5
|
+
export type GradientCoordinates = {
|
|
6
|
+
x1: string;
|
|
7
|
+
y1: string;
|
|
8
|
+
x2: string;
|
|
9
|
+
y2: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type LinearGradientDirection =
|
|
13
|
+
| 'to-bottom'
|
|
14
|
+
| 'to-top'
|
|
15
|
+
| 'to-left'
|
|
16
|
+
| 'to-right'
|
|
17
|
+
| 'to-bottomright'
|
|
18
|
+
| 'to-bottomleft'
|
|
19
|
+
| 'to-topright'
|
|
20
|
+
| 'to-topleft';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Props for the LinearGradient component.
|
|
24
|
+
*/
|
|
25
|
+
export type LinearGradientProps = {
|
|
26
|
+
/**
|
|
27
|
+
* Gradient color stops
|
|
28
|
+
*/
|
|
29
|
+
stops: GradientStop[];
|
|
30
|
+
/**
|
|
31
|
+
* Optional children to render on top of the gradient
|
|
32
|
+
*/
|
|
33
|
+
children?: ReactNode;
|
|
34
|
+
/**
|
|
35
|
+
* Direction preset for the gradient.
|
|
36
|
+
* @default 'to-bottom'
|
|
37
|
+
*
|
|
38
|
+
* Can be a direction preset or a custom angle in degrees.
|
|
39
|
+
* 0° points to the right, 90° points down.
|
|
40
|
+
*/
|
|
41
|
+
direction?: LinearGradientDirection | number;
|
|
42
|
+
} & StyledViewProps;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import * as RadialGradientStories from './RadialGradient.stories';
|
|
3
|
+
import { CustomTabs, Tab } from '../../../../../../.storybook/components';
|
|
4
|
+
|
|
5
|
+
<Meta title='Utility/RadialGradient' of={RadialGradientStories} />
|
|
6
|
+
|
|
7
|
+
# RadialGradient
|
|
8
|
+
|
|
9
|
+
<CustomTabs>
|
|
10
|
+
<Tab label="Overview ">
|
|
11
|
+
|
|
12
|
+
## Introduction
|
|
13
|
+
|
|
14
|
+
RadialGradient renders a radial gradient background using `react-native-svg`.
|
|
15
|
+
It extends Box, supporting all `lx` style props for layout and sizing.
|
|
16
|
+
|
|
17
|
+
## Anatomy
|
|
18
|
+
|
|
19
|
+
<Canvas of={RadialGradientStories.Base} />
|
|
20
|
+
<Controls of={RadialGradientStories.Base} />
|
|
21
|
+
|
|
22
|
+
## Properties
|
|
23
|
+
|
|
24
|
+
### Center Position
|
|
25
|
+
|
|
26
|
+
Control where the gradient originates using the `center` prop:
|
|
27
|
+
|
|
28
|
+
<Canvas of={RadialGradientStories.CenterPositionShowcase} />
|
|
29
|
+
|
|
30
|
+
### With Children
|
|
31
|
+
|
|
32
|
+
Content renders on top of the gradient:
|
|
33
|
+
|
|
34
|
+
<Canvas of={RadialGradientStories.WithChildren} />
|
|
35
|
+
|
|
36
|
+
### Multiple Stops
|
|
37
|
+
|
|
38
|
+
The `offset` prop controls where each color appears from center to edge (0 = center, 1 = edge).
|
|
39
|
+
When omitted, stops are distributed evenly.
|
|
40
|
+
For example, 3 stops without offsets will be placed at 0, 0.5, and 1.
|
|
41
|
+
|
|
42
|
+
<Canvas of={RadialGradientStories.WithMultipleStops} />
|
|
43
|
+
|
|
44
|
+
</Tab>
|
|
45
|
+
|
|
46
|
+
<Tab label="Implementation ">
|
|
47
|
+
|
|
48
|
+
## Setup
|
|
49
|
+
|
|
50
|
+
Install and set up the library with our [Setup Guide →](?path=/docs/getting-started-setup--docs).
|
|
51
|
+
|
|
52
|
+
## Basic Usage
|
|
53
|
+
|
|
54
|
+
By default, the gradient radiates from center outward. Use design tokens for colors to ensure theme consistency.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { RadialGradient } from '@ledgerhq/lumen-ui-rnative';
|
|
58
|
+
|
|
59
|
+
<RadialGradient
|
|
60
|
+
stops={[{ color: 'accent' }, { color: 'active', opacity: 0 }]}
|
|
61
|
+
lx={{ height: 's192', width: 's192', borderRadius: 'lg' }}
|
|
62
|
+
/>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Custom Center
|
|
66
|
+
|
|
67
|
+
The `center` prop uses normalized coordinates (0-1). `{ x: 0, y: 0 }` is top-left, `{ x: 1, y: 1 }` is bottom-right.
|
|
68
|
+
Useful for spotlight or vignette effects.
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
<RadialGradient
|
|
72
|
+
center={{ x: 0, y: 0 }}
|
|
73
|
+
stops={[{ color: 'accent' }, { color: 'active' }]}
|
|
74
|
+
lx={{ height: 's80', width: 's80' }}
|
|
75
|
+
/>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### With Children
|
|
79
|
+
|
|
80
|
+
The gradient acts as a background. Children are rendered on top and define the component's height when no explicit size is set.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
<RadialGradient
|
|
84
|
+
center={{ x: 0.5, y: 0.3 }}
|
|
85
|
+
stops={[{ color: 'accent' }, { color: 'active', opacity: 0 }]}
|
|
86
|
+
lx={{ padding: 's24', borderRadius: 'lg' }}
|
|
87
|
+
>
|
|
88
|
+
<Text lx={{ color: 'base' }}>Content on gradient</Text>
|
|
89
|
+
</RadialGradient>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Stops API
|
|
93
|
+
|
|
94
|
+
Each stop defines a color at a distance from center. Use `opacity: 0` for fade-out effects.
|
|
95
|
+
Colors support both design tokens (`'accent'`) and raw values (`'#FF6B6B'`).
|
|
96
|
+
|
|
97
|
+
**Default behavior when `offset` is omitted:** stops are evenly distributed.
|
|
98
|
+
With 2 stops → `[0, 1]`. With 3 stops → `[0, 0.5, 1]`. With 4 stops → `[0, 0.33, 0.66, 1]`.
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
type GradientStop = {
|
|
102
|
+
color: BackgroundColorToken | string; // Token or raw color
|
|
103
|
+
offset?: number; // 0-1, auto-spread if omitted
|
|
104
|
+
opacity?: number; // 0-1, defaults to 1
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
</Tab>
|
|
109
|
+
</CustomTabs>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
|
+
import { Box } from '../../Box';
|
|
3
|
+
import { Text } from '../../Text';
|
|
4
|
+
import { RadialGradient } from './RadialGradient';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof RadialGradient> = {
|
|
7
|
+
component: RadialGradient,
|
|
8
|
+
title: 'Utility/RadialGradient',
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'centered',
|
|
11
|
+
backgrounds: { default: 'light' },
|
|
12
|
+
docs: {
|
|
13
|
+
source: {
|
|
14
|
+
language: 'tsx',
|
|
15
|
+
format: true,
|
|
16
|
+
type: 'code',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof RadialGradient>;
|
|
24
|
+
|
|
25
|
+
export const Base: Story = {
|
|
26
|
+
args: {
|
|
27
|
+
stops: [{ color: 'accent' }, { color: 'active', opacity: 0 }],
|
|
28
|
+
lx: {
|
|
29
|
+
height: 's192',
|
|
30
|
+
width: 's192',
|
|
31
|
+
borderRadius: 'lg',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const CenterPositionShowcase: Story = {
|
|
37
|
+
render: () => {
|
|
38
|
+
const positions = [
|
|
39
|
+
{
|
|
40
|
+
title: 'Aspect ratio 1:1',
|
|
41
|
+
isSquare: true,
|
|
42
|
+
items: [
|
|
43
|
+
{
|
|
44
|
+
center: { x: 0.5, y: 0.5 },
|
|
45
|
+
label: '{x: 0.5, y: 0.5} (default)',
|
|
46
|
+
},
|
|
47
|
+
{ center: { x: 0.2, y: 0.2 }, label: '{x: 0.2, y: 0.2}' },
|
|
48
|
+
{ center: { x: 0.8, y: 0.8 }, label: '{x: 0.8, y: 0.8}' },
|
|
49
|
+
{ center: { x: 0.2, y: 0.8 }, label: '{x: 0.2, y: 0.8}' },
|
|
50
|
+
{ center: { x: 0.8, y: 0.2 }, label: '{x: 0.8, y: 0.2}' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
title: 'Aspect ratio 1:2',
|
|
55
|
+
isSquare: false,
|
|
56
|
+
items: [
|
|
57
|
+
{
|
|
58
|
+
center: { x: 0.5, y: 0.5 },
|
|
59
|
+
label: '{x: 0.5, y: 0.5} (default)',
|
|
60
|
+
},
|
|
61
|
+
{ center: { x: 0.2, y: 0.2 }, label: '{x: 0.2, y: 0.2}' },
|
|
62
|
+
{ center: { x: 0.8, y: 0.8 }, label: '{x: 0.8, y: 0.8}' },
|
|
63
|
+
{ center: { x: 0.2, y: 0.8 }, label: '{x: 0.2, y: 0.8}' },
|
|
64
|
+
{ center: { x: 0.8, y: 0.2 }, label: '{x: 0.8, y: 0.2}' },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
] as const;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Box lx={{ flexDirection: 'column', gap: 's40' }}>
|
|
71
|
+
{positions.map(({ title, isSquare, items }) => (
|
|
72
|
+
<Box key={title}>
|
|
73
|
+
<Text
|
|
74
|
+
typography='heading5SemiBold'
|
|
75
|
+
lx={{ color: 'base', marginBottom: 's16' }}
|
|
76
|
+
>
|
|
77
|
+
{title}
|
|
78
|
+
</Text>
|
|
79
|
+
<Box lx={{ flexDirection: 'row', flexWrap: 'wrap', gap: 's24' }}>
|
|
80
|
+
{items.map(({ center, label }) => (
|
|
81
|
+
<Box key={label} lx={{ gap: 's8', alignItems: 'center' }}>
|
|
82
|
+
<RadialGradient
|
|
83
|
+
center={center}
|
|
84
|
+
stops={[
|
|
85
|
+
{ color: 'accent' },
|
|
86
|
+
{ color: 'active', opacity: 0 },
|
|
87
|
+
]}
|
|
88
|
+
lx={{
|
|
89
|
+
height: 's72',
|
|
90
|
+
width: isSquare ? 's72' : 's144',
|
|
91
|
+
borderRadius: 'md',
|
|
92
|
+
borderColor: 'active',
|
|
93
|
+
borderWidth: 's1',
|
|
94
|
+
}}
|
|
95
|
+
/>
|
|
96
|
+
<Text typography='body4' lx={{ color: 'base' }}>
|
|
97
|
+
{label}
|
|
98
|
+
</Text>
|
|
99
|
+
</Box>
|
|
100
|
+
))}
|
|
101
|
+
</Box>
|
|
102
|
+
</Box>
|
|
103
|
+
))}
|
|
104
|
+
</Box>
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const WithChildren: Story = {
|
|
110
|
+
args: {
|
|
111
|
+
stops: [{ color: 'accent' }, { color: 'active', opacity: 0 }],
|
|
112
|
+
lx: {
|
|
113
|
+
padding: 's24',
|
|
114
|
+
borderRadius: 'lg',
|
|
115
|
+
width: 's288',
|
|
116
|
+
borderColor: 'active',
|
|
117
|
+
borderWidth: 's1',
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
render: (args) => (
|
|
121
|
+
<RadialGradient {...args}>
|
|
122
|
+
<Text typography='heading2SemiBold' lx={{ color: 'base' }}>
|
|
123
|
+
With Children
|
|
124
|
+
</Text>
|
|
125
|
+
<Text typography='body2' lx={{ color: 'base', marginTop: 's8' }}>
|
|
126
|
+
Adapt height based on content.
|
|
127
|
+
</Text>
|
|
128
|
+
<Text typography='body3' lx={{ color: 'base', marginTop: 's8' }}>
|
|
129
|
+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos
|
|
130
|
+
</Text>
|
|
131
|
+
</RadialGradient>
|
|
132
|
+
),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const WithMultipleStops: Story = {
|
|
136
|
+
args: {
|
|
137
|
+
stops: [
|
|
138
|
+
{ color: 'warning', offset: 0, opacity: 1 },
|
|
139
|
+
{ color: 'accent', offset: 0.5, opacity: 1 },
|
|
140
|
+
{ color: 'error', offset: 1, opacity: 1 },
|
|
141
|
+
],
|
|
142
|
+
lx: {
|
|
143
|
+
height: 's80',
|
|
144
|
+
width: 's80',
|
|
145
|
+
borderRadius: 'full',
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
};
|