@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.
Files changed (71) hide show
  1. package/dist/package.json +2 -2
  2. package/dist/src/lib/Components/BaseInput/BaseInput.d.ts +1 -0
  3. package/dist/src/lib/Components/BaseInput/BaseInput.d.ts.map +1 -1
  4. package/dist/src/lib/Components/BaseInput/BaseInput.js +6 -2
  5. package/dist/src/lib/Components/BaseInput/types.d.ts +5 -1
  6. package/dist/src/lib/Components/BaseInput/types.d.ts.map +1 -1
  7. package/dist/src/lib/Components/SearchInput/SearchInput.d.ts +3 -1
  8. package/dist/src/lib/Components/SearchInput/SearchInput.d.ts.map +1 -1
  9. package/dist/src/lib/Components/SearchInput/SearchInput.js +12 -2
  10. package/dist/src/lib/Components/SearchInput/SearchInput.stories.d.ts.map +1 -1
  11. package/dist/src/lib/Components/SearchInput/SearchInput.stories.js +3 -0
  12. package/dist/src/lib/Components/SearchInput/types.d.ts +7 -1
  13. package/dist/src/lib/Components/SearchInput/types.d.ts.map +1 -1
  14. package/dist/src/lib/Components/Tile/Tile.d.ts.map +1 -1
  15. package/dist/src/lib/Components/Tile/Tile.js +1 -0
  16. package/dist/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.d.ts +32 -0
  17. package/dist/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.d.ts.map +1 -0
  18. package/dist/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.js +75 -0
  19. package/dist/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.stories.d.ts +11 -0
  20. package/dist/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.stories.d.ts.map +1 -0
  21. package/dist/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.stories.js +107 -0
  22. package/dist/src/lib/Components/Utility/Gradient/LinearGradient/index.d.ts +3 -0
  23. package/dist/src/lib/Components/Utility/Gradient/LinearGradient/index.d.ts.map +1 -0
  24. package/dist/src/lib/Components/Utility/Gradient/LinearGradient/index.js +2 -0
  25. package/dist/src/lib/Components/Utility/Gradient/LinearGradient/types.d.ts +32 -0
  26. package/dist/src/lib/Components/Utility/Gradient/LinearGradient/types.d.ts.map +1 -0
  27. package/dist/src/lib/Components/Utility/Gradient/LinearGradient/types.js +1 -0
  28. package/dist/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.d.ts +31 -0
  29. package/dist/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.d.ts.map +1 -0
  30. package/dist/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.js +52 -0
  31. package/dist/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.stories.d.ts +10 -0
  32. package/dist/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.stories.d.ts.map +1 -0
  33. package/dist/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.stories.js +101 -0
  34. package/dist/src/lib/Components/Utility/Gradient/RadialGradient/index.d.ts +3 -0
  35. package/dist/src/lib/Components/Utility/Gradient/RadialGradient/index.d.ts.map +1 -0
  36. package/dist/src/lib/Components/Utility/Gradient/RadialGradient/index.js +2 -0
  37. package/dist/src/lib/Components/Utility/Gradient/RadialGradient/types.d.ts +32 -0
  38. package/dist/src/lib/Components/Utility/Gradient/RadialGradient/types.d.ts.map +1 -0
  39. package/dist/src/lib/Components/Utility/Gradient/RadialGradient/types.js +1 -0
  40. package/dist/src/lib/Components/Utility/Gradient/gradient.types.d.ts +21 -0
  41. package/dist/src/lib/Components/Utility/Gradient/gradient.types.d.ts.map +1 -0
  42. package/dist/src/lib/Components/Utility/Gradient/gradient.types.js +1 -0
  43. package/dist/src/lib/Components/Utility/Gradient/utils/resolveGradientColor.d.ts +21 -0
  44. package/dist/src/lib/Components/Utility/Gradient/utils/resolveGradientColor.d.ts.map +1 -0
  45. package/dist/src/lib/Components/Utility/Gradient/utils/resolveGradientColor.js +38 -0
  46. package/dist/src/lib/Components/Utility/index.d.ts +2 -0
  47. package/dist/src/lib/Components/Utility/index.d.ts.map +1 -1
  48. package/dist/src/lib/Components/Utility/index.js +2 -0
  49. package/package.json +2 -2
  50. package/src/lib/Components/BaseInput/BaseInput.tsx +7 -2
  51. package/src/lib/Components/BaseInput/types.ts +5 -1
  52. package/src/lib/Components/SearchInput/SearchInput.stories.tsx +3 -0
  53. package/src/lib/Components/SearchInput/SearchInput.tsx +32 -8
  54. package/src/lib/Components/SearchInput/types.ts +7 -1
  55. package/src/lib/Components/Tile/Tile.tsx +1 -0
  56. package/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.mdx +142 -0
  57. package/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.stories.tsx +173 -0
  58. package/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.test.tsx +69 -0
  59. package/src/lib/Components/Utility/Gradient/LinearGradient/LinearGradient.tsx +127 -0
  60. package/src/lib/Components/Utility/Gradient/LinearGradient/index.ts +2 -0
  61. package/src/lib/Components/Utility/Gradient/LinearGradient/types.ts +42 -0
  62. package/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.mdx +109 -0
  63. package/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.stories.tsx +148 -0
  64. package/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.test.tsx +69 -0
  65. package/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.tsx +102 -0
  66. package/src/lib/Components/Utility/Gradient/RadialGradient/index.ts +2 -0
  67. package/src/lib/Components/Utility/Gradient/RadialGradient/types.ts +32 -0
  68. package/src/lib/Components/Utility/Gradient/gradient.types.ts +22 -0
  69. package/src/lib/Components/Utility/Gradient/utils/resolveGradientColor.test.ts +144 -0
  70. package/src/lib/Components/Utility/Gradient/utils/resolveGradientColor.ts +59 -0
  71. 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,2 @@
1
+ export * from './LinearGradient';
2
+ export * from './types';
@@ -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
+ };