@ledgerhq/lumen-ui-rnative 0.1.11 → 0.1.13

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 (140) hide show
  1. package/dist/module/i18n/locales/de.json +3 -0
  2. package/dist/module/i18n/locales/en.json +3 -0
  3. package/dist/module/i18n/locales/es.json +3 -0
  4. package/dist/module/i18n/locales/fr.json +3 -0
  5. package/dist/module/i18n/locales/ja.json +3 -0
  6. package/dist/module/i18n/locales/ko.json +3 -0
  7. package/dist/module/i18n/locales/pt.json +3 -0
  8. package/dist/module/i18n/locales/ru.json +3 -0
  9. package/dist/module/i18n/locales/th.json +3 -0
  10. package/dist/module/i18n/locales/tr.json +3 -0
  11. package/dist/module/i18n/locales/zh.json +3 -0
  12. package/dist/module/lib/Animations/Pulse/Pulse.js +1 -1
  13. package/dist/module/lib/Animations/Spin/Spin.js +1 -1
  14. package/dist/module/lib/Components/AmountDisplay/AmountDisplay.js +21 -21
  15. package/dist/module/lib/Components/AmountDisplay/AmountDisplay.js.map +1 -1
  16. package/dist/module/lib/Components/AmountInput/AmountInput.js +3 -3
  17. package/dist/module/lib/Components/BaseInput/BaseInput.js +1 -1
  18. package/dist/module/lib/Components/BottomSheet/BottomSheet.stories.js.map +1 -1
  19. package/dist/module/lib/Components/Card/Card.js +39 -29
  20. package/dist/module/lib/Components/Card/Card.js.map +1 -1
  21. package/dist/module/lib/Components/InteractiveIcon/InteractiveIcon.js +22 -2
  22. package/dist/module/lib/Components/InteractiveIcon/InteractiveIcon.js.map +1 -1
  23. package/dist/module/lib/Components/Link/Link.mdx +1 -0
  24. package/dist/module/lib/Components/MediaBanner/MediaBanner.js +158 -0
  25. package/dist/module/lib/Components/MediaBanner/MediaBanner.js.map +1 -0
  26. package/dist/module/lib/Components/MediaBanner/MediaBanner.mdx +150 -0
  27. package/dist/module/lib/Components/MediaBanner/MediaBanner.stories.js +135 -0
  28. package/dist/module/lib/Components/MediaBanner/MediaBanner.stories.js.map +1 -0
  29. package/dist/module/lib/Components/MediaBanner/MediaBanner.test.js +83 -0
  30. package/dist/module/lib/Components/MediaBanner/MediaBanner.test.js.map +1 -0
  31. package/dist/module/lib/Components/MediaBanner/index.js +5 -0
  32. package/dist/module/lib/Components/MediaBanner/index.js.map +1 -0
  33. package/dist/module/lib/Components/MediaBanner/types.js +4 -0
  34. package/dist/module/lib/Components/MediaBanner/types.js.map +1 -0
  35. package/dist/module/lib/Components/MediaCard/MediaCard.js +183 -0
  36. package/dist/module/lib/Components/MediaCard/MediaCard.js.map +1 -0
  37. package/dist/module/lib/Components/MediaCard/MediaCard.mdx +111 -0
  38. package/dist/module/lib/Components/MediaCard/MediaCard.stories.js +199 -0
  39. package/dist/module/lib/Components/MediaCard/MediaCard.stories.js.map +1 -0
  40. package/dist/module/lib/Components/MediaCard/MediaCard.test.js +140 -0
  41. package/dist/module/lib/Components/MediaCard/MediaCard.test.js.map +1 -0
  42. package/dist/module/lib/Components/MediaCard/index.js +5 -0
  43. package/dist/module/lib/Components/MediaCard/index.js.map +1 -0
  44. package/dist/module/lib/Components/MediaCard/types.js +4 -0
  45. package/dist/module/lib/Components/MediaCard/types.js.map +1 -0
  46. package/dist/module/lib/Components/PageIndicator/PageIndicator.js +2 -2
  47. package/dist/module/lib/Components/SegmentedControl/usePillLayout.js +1 -1
  48. package/dist/module/lib/Components/Stepper/Stepper.js +1 -1
  49. package/dist/module/lib/Components/Switch/BaseSwitch.js +1 -1
  50. package/dist/module/lib/Components/TabBar/TabBar.js +4 -4
  51. package/dist/module/lib/Components/ThemeProvider/ThemeProvider.test.js +22 -20
  52. package/dist/module/lib/Components/ThemeProvider/ThemeProvider.test.js.map +1 -1
  53. package/dist/module/lib/Components/TriggerButton/TriggerButton.js +197 -0
  54. package/dist/module/lib/Components/TriggerButton/TriggerButton.js.map +1 -0
  55. package/dist/module/lib/Components/TriggerButton/TriggerButton.mdx +44 -0
  56. package/dist/module/lib/Components/TriggerButton/TriggerButton.stories.js +170 -0
  57. package/dist/module/lib/Components/TriggerButton/TriggerButton.stories.js.map +1 -0
  58. package/dist/module/lib/Components/TriggerButton/TriggerButton.test.js +146 -0
  59. package/dist/module/lib/Components/TriggerButton/TriggerButton.test.js.map +1 -0
  60. package/dist/module/lib/Components/TriggerButton/index.js +5 -0
  61. package/dist/module/lib/Components/TriggerButton/index.js.map +1 -0
  62. package/dist/module/lib/Components/TriggerButton/types.js +4 -0
  63. package/dist/module/lib/Components/TriggerButton/types.js.map +1 -0
  64. package/dist/module/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.stories.js.map +1 -1
  65. package/dist/module/lib/Components/index.js +3 -0
  66. package/dist/module/lib/Components/index.js.map +1 -1
  67. package/dist/module/lib/Symbols/Icons/NanoGen5.js +49 -0
  68. package/dist/module/lib/Symbols/Icons/NanoGen5.js.map +1 -0
  69. package/dist/module/lib/Symbols/index.js +1 -0
  70. package/dist/module/lib/Symbols/index.js.map +1 -1
  71. package/dist/typescript/src/lib/Components/Card/Card.d.ts.map +1 -1
  72. package/dist/typescript/src/lib/Components/InteractiveIcon/InteractiveIcon.d.ts +1 -1
  73. package/dist/typescript/src/lib/Components/InteractiveIcon/InteractiveIcon.d.ts.map +1 -1
  74. package/dist/typescript/src/lib/Components/InteractiveIcon/types.d.ts +8 -0
  75. package/dist/typescript/src/lib/Components/InteractiveIcon/types.d.ts.map +1 -1
  76. package/dist/typescript/src/lib/Components/MediaBanner/MediaBanner.d.ts +16 -0
  77. package/dist/typescript/src/lib/Components/MediaBanner/MediaBanner.d.ts.map +1 -0
  78. package/dist/typescript/src/lib/Components/MediaBanner/index.d.ts +3 -0
  79. package/dist/typescript/src/lib/Components/MediaBanner/index.d.ts.map +1 -0
  80. package/dist/typescript/src/lib/Components/MediaBanner/types.d.ts +42 -0
  81. package/dist/typescript/src/lib/Components/MediaBanner/types.d.ts.map +1 -0
  82. package/dist/typescript/src/lib/Components/MediaCard/MediaCard.d.ts +32 -0
  83. package/dist/typescript/src/lib/Components/MediaCard/MediaCard.d.ts.map +1 -0
  84. package/dist/typescript/src/lib/Components/MediaCard/index.d.ts +3 -0
  85. package/dist/typescript/src/lib/Components/MediaCard/index.d.ts.map +1 -0
  86. package/dist/typescript/src/lib/Components/MediaCard/types.d.ts +38 -0
  87. package/dist/typescript/src/lib/Components/MediaCard/types.d.ts.map +1 -0
  88. package/dist/typescript/src/lib/Components/TriggerButton/TriggerButton.d.ts +26 -0
  89. package/dist/typescript/src/lib/Components/TriggerButton/TriggerButton.d.ts.map +1 -0
  90. package/dist/typescript/src/lib/Components/TriggerButton/index.d.ts +3 -0
  91. package/dist/typescript/src/lib/Components/TriggerButton/index.d.ts.map +1 -0
  92. package/dist/typescript/src/lib/Components/TriggerButton/types.d.ts +38 -0
  93. package/dist/typescript/src/lib/Components/TriggerButton/types.d.ts.map +1 -0
  94. package/dist/typescript/src/lib/Components/index.d.ts +3 -0
  95. package/dist/typescript/src/lib/Components/index.d.ts.map +1 -1
  96. package/dist/typescript/src/lib/Symbols/Icons/NanoGen5.d.ts +35 -0
  97. package/dist/typescript/src/lib/Symbols/Icons/NanoGen5.d.ts.map +1 -0
  98. package/dist/typescript/src/lib/Symbols/index.d.ts +1 -0
  99. package/dist/typescript/src/lib/Symbols/index.d.ts.map +1 -1
  100. package/package.json +3 -3
  101. package/src/i18n/locales/de.json +3 -0
  102. package/src/i18n/locales/en.json +3 -0
  103. package/src/i18n/locales/es.json +3 -0
  104. package/src/i18n/locales/fr.json +3 -0
  105. package/src/i18n/locales/ja.json +3 -0
  106. package/src/i18n/locales/ko.json +3 -0
  107. package/src/i18n/locales/pt.json +3 -0
  108. package/src/i18n/locales/ru.json +3 -0
  109. package/src/i18n/locales/th.json +3 -0
  110. package/src/i18n/locales/tr.json +3 -0
  111. package/src/i18n/locales/zh.json +3 -0
  112. package/src/lib/Components/AmountDisplay/AmountDisplay.tsx +20 -20
  113. package/src/lib/Components/BottomSheet/BottomSheet.stories.tsx +9 -9
  114. package/src/lib/Components/Card/Card.tsx +38 -33
  115. package/src/lib/Components/InteractiveIcon/InteractiveIcon.tsx +26 -4
  116. package/src/lib/Components/InteractiveIcon/types.ts +8 -0
  117. package/src/lib/Components/Link/Link.mdx +1 -0
  118. package/src/lib/Components/MediaBanner/MediaBanner.mdx +150 -0
  119. package/src/lib/Components/MediaBanner/MediaBanner.stories.tsx +143 -0
  120. package/src/lib/Components/MediaBanner/MediaBanner.test.tsx +77 -0
  121. package/src/lib/Components/MediaBanner/MediaBanner.tsx +172 -0
  122. package/src/lib/Components/MediaBanner/index.ts +2 -0
  123. package/src/lib/Components/MediaBanner/types.ts +44 -0
  124. package/src/lib/Components/MediaCard/MediaCard.mdx +111 -0
  125. package/src/lib/Components/MediaCard/MediaCard.stories.tsx +190 -0
  126. package/src/lib/Components/MediaCard/MediaCard.test.tsx +125 -0
  127. package/src/lib/Components/MediaCard/MediaCard.tsx +203 -0
  128. package/src/lib/Components/MediaCard/index.ts +2 -0
  129. package/src/lib/Components/MediaCard/types.ts +39 -0
  130. package/src/lib/Components/ThemeProvider/ThemeProvider.test.tsx +16 -18
  131. package/src/lib/Components/TriggerButton/TriggerButton.mdx +44 -0
  132. package/src/lib/Components/TriggerButton/TriggerButton.stories.tsx +132 -0
  133. package/src/lib/Components/TriggerButton/TriggerButton.test.tsx +157 -0
  134. package/src/lib/Components/TriggerButton/TriggerButton.tsx +228 -0
  135. package/src/lib/Components/TriggerButton/index.ts +2 -0
  136. package/src/lib/Components/TriggerButton/types.ts +38 -0
  137. package/src/lib/Components/Utility/Gradient/RadialGradient/RadialGradient.stories.tsx +1 -1
  138. package/src/lib/Components/index.ts +3 -0
  139. package/src/lib/Symbols/Icons/NanoGen5.tsx +44 -0
  140. package/src/lib/Symbols/index.ts +1 -0
@@ -0,0 +1,150 @@
1
+ import { Meta, Story, Canvas, Controls } from '@storybook/addon-docs/blocks';
2
+ import * as MediaBannerStories from './MediaBanner.stories';
3
+ import {
4
+ MediaBanner,
5
+ MediaBannerTitle,
6
+ MediaBannerDescription,
7
+ } from './MediaBanner';
8
+ import { Box } from '../Utility';
9
+ import { CustomTabs, Tab } from '../../../../.storybook/components';
10
+ import { DoVsDontRow, DoBlockItem, DontBlockItem } from '../../../../.storybook/components/DoVsDont';
11
+ import CommonRulesDoAndDont from '../../../../.storybook/components/DoVsDont/CommonRulesDoAndDont.mdx';
12
+
13
+ <Meta title='Components/MediaBanner' of={MediaBannerStories} />
14
+
15
+ # 🖼️ MediaBanner
16
+
17
+ <CustomTabs>
18
+ <Tab label="Overview">
19
+
20
+ ## Introduction
21
+
22
+ MediaBanner is a promotional banner component that displays a background image alongside a title and description. It is designed for marketing or informational content and supports an optional close action for dismissibility.
23
+
24
+ > View in [Figma](https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7/2.-Components-Library?node-id=11235-5982&m=dev).
25
+
26
+ ## Anatomy
27
+
28
+ <Canvas of={MediaBannerStories.Base} />
29
+
30
+ - **MediaBanner**: Root pressable container with surface background, rounded corners, and a fixed height
31
+ - **MediaBannerTitle**: The main label of the banner (clamps at 1 line)
32
+ - **MediaBannerDescription**: Additional context below the title (clamps at 2 lines)
33
+ - **Image**: Background image displayed on the right side with a gradient overlay
34
+ - **Close button**: Optional InteractiveIcon rendered in the top-right corner via the `onClose` prop
35
+
36
+ ## Properties
37
+
38
+ ### Overview
39
+
40
+ <Canvas of={MediaBannerStories.Base} />
41
+ <Controls of={MediaBannerStories.Base} />
42
+
43
+ ### Truncation
44
+
45
+ Title clamps at 1 line and description clamps at 2 lines when content overflows.
46
+
47
+ <Canvas of={MediaBannerStories.Truncation} />
48
+
49
+ ### Broken Image
50
+
51
+ When the `imageUrl` fails to load, the broken image is hidden while the gradient overlay and image space are preserved. The banner remains functional and visually consistent.
52
+
53
+ <Canvas of={MediaBannerStories.WithBrokenImage} />
54
+
55
+ ### Dismissible
56
+
57
+ When `onClose` is provided, the close button allows the user to dismiss the banner.
58
+
59
+ <Canvas of={MediaBannerStories.WithClose} />
60
+
61
+ ## Accessibility
62
+
63
+ - The close button uses `InteractiveIcon` with proper press states
64
+ - The root element is a `Pressable` for interactive use cases
65
+ - Title and description use semantic `Text` elements
66
+
67
+ </Tab>
68
+ <Tab label="Implementation">
69
+
70
+ ## Setup
71
+
72
+ Install and set up the library with our [Setup Guide →](?path=/docs/getting-started-setup--docs).
73
+
74
+ ## Basic Usage
75
+
76
+ MediaBanner uses a composite component pattern with `MediaBannerTitle` and `MediaBannerDescription`:
77
+
78
+ ```tsx
79
+ import {
80
+ MediaBanner,
81
+ MediaBannerTitle,
82
+ MediaBannerDescription,
83
+ } from '@ledgerhq/lumen-ui-rnative';
84
+
85
+ function MyComponent() {
86
+ return (
87
+ <MediaBanner imageUrl='https://example.com/promo.jpg'>
88
+ <MediaBannerTitle>Firmware Update</MediaBannerTitle>
89
+ <MediaBannerDescription>
90
+ Keep your Nano updated!
91
+ </MediaBannerDescription>
92
+ </MediaBanner>
93
+ );
94
+ }
95
+ ```
96
+
97
+ ### With Close Button
98
+
99
+ Add an `onClose` callback to make the banner dismissible:
100
+
101
+ ```tsx
102
+ import {
103
+ MediaBanner,
104
+ MediaBannerTitle,
105
+ MediaBannerDescription,
106
+ } from '@ledgerhq/lumen-ui-rnative';
107
+ import { useState } from 'react';
108
+
109
+ function DismissibleBanner() {
110
+ const [isVisible, setIsVisible] = useState(true);
111
+
112
+ if (!isVisible) return null;
113
+
114
+ return (
115
+ <MediaBanner
116
+ imageUrl='https://example.com/promo.jpg'
117
+ onClose={() => setIsVisible(false)}
118
+ >
119
+ <MediaBannerTitle>
120
+ Earn Up to 12% APY With Staking Now!
121
+ </MediaBannerTitle>
122
+ <MediaBannerDescription>
123
+ Put your idle crypto to work. Start staking SOL, ETH, ATOM and more
124
+ directly from Ledger Live
125
+ </MediaBannerDescription>
126
+ </MediaBanner>
127
+ );
128
+ }
129
+ ```
130
+
131
+ ### Layout Adjustments with lx
132
+
133
+ Use the `lx` prop for layout adjustments like margins or positioning:
134
+
135
+ ```tsx
136
+ <MediaBanner
137
+ imageUrl='https://example.com/promo.jpg'
138
+ lx={{ marginTop: 's16', marginBottom: 's8' }}
139
+ >
140
+ <MediaBannerTitle>With Margin</MediaBannerTitle>
141
+ <MediaBannerDescription>
142
+ This banner has layout adjustments via lx.
143
+ </MediaBannerDescription>
144
+ </MediaBanner>
145
+ ```
146
+
147
+ <CommonRulesDoAndDont />
148
+
149
+ </Tab>
150
+ </CustomTabs>
@@ -0,0 +1,143 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
2
+ import { useState } from 'react';
3
+ import { Button } from '../Button';
4
+ import { Box } from '../Utility';
5
+ import {
6
+ MediaBanner,
7
+ MediaBannerDescription,
8
+ MediaBannerTitle,
9
+ } from './MediaBanner';
10
+
11
+ const IMAGE_URL =
12
+ 'https://images.unsplash.com/photo-1663741954108-d15d514529ef';
13
+
14
+ const meta: Meta<typeof MediaBanner> = {
15
+ component: MediaBanner,
16
+ title: 'Communication/MediaBanner',
17
+ subcomponents: {
18
+ MediaBannerTitle,
19
+ MediaBannerDescription,
20
+ },
21
+ parameters: {
22
+ docs: {
23
+ source: {
24
+ language: 'tsx',
25
+ format: true,
26
+ type: 'code',
27
+ },
28
+ },
29
+ },
30
+ argTypes: {
31
+ imageUrl: {
32
+ control: 'text',
33
+ description: 'URL of the background image',
34
+ },
35
+ onClose: {
36
+ control: 'select',
37
+ description: 'Close action callback',
38
+ options: ['With Close', 'None'],
39
+ mapping: {
40
+ 'With Close': () => {
41
+ console.log('Close clicked');
42
+ },
43
+ None: undefined,
44
+ },
45
+ },
46
+ },
47
+ };
48
+
49
+ export default meta;
50
+ type Story = StoryObj<typeof MediaBanner>;
51
+
52
+ export const Base: Story = {
53
+ args: {
54
+ imageUrl: IMAGE_URL,
55
+ },
56
+ render: (args) => (
57
+ <Box lx={{ width: 's400' }}>
58
+ <MediaBanner {...args}>
59
+ <MediaBannerTitle>Firmware Update</MediaBannerTitle>
60
+ <MediaBannerDescription>Keep your Nano updated!</MediaBannerDescription>
61
+ </MediaBanner>
62
+ </Box>
63
+ ),
64
+ parameters: {
65
+ docs: {
66
+ source: {
67
+ code: `
68
+ <MediaBanner imageUrl="https://images.unsplash.com/photo-1663741954108-d15d514529ef">
69
+ <MediaBannerTitle>Firmware Update</MediaBannerTitle>
70
+ <MediaBannerDescription>
71
+ Keep your Nano updated!
72
+ </MediaBannerDescription>
73
+ </MediaBanner>
74
+ `,
75
+ },
76
+ },
77
+ },
78
+ };
79
+
80
+ export const Truncation: Story = {
81
+ render: () => (
82
+ <Box lx={{ width: 's400' }}>
83
+ <MediaBanner imageUrl={IMAGE_URL} onClose={() => console.log('close')}>
84
+ <MediaBannerTitle>
85
+ Earn Up to 12% APY With Staking Now And Much More Rewards Awaiting You
86
+ </MediaBannerTitle>
87
+ <MediaBannerDescription>
88
+ Put your idle crypto to work. Start staking SOL, ETH, ATOM and more
89
+ directly from Ledger Live. Maximize your returns with our secure
90
+ staking solutions.
91
+ </MediaBannerDescription>
92
+ </MediaBanner>
93
+ </Box>
94
+ ),
95
+ };
96
+
97
+ export const WithBrokenImage: Story = {
98
+ render: () => (
99
+ <Box lx={{ width: 's400' }}>
100
+ <MediaBanner
101
+ imageUrl='https://broken-url.invalid/image.jpg'
102
+ onClose={() => console.log('close')}
103
+ >
104
+ <MediaBannerTitle>Sorry!</MediaBannerTitle>
105
+ <MediaBannerDescription>
106
+ The image failed to load so the banner decided to gracefully hide it.
107
+ </MediaBannerDescription>
108
+ </MediaBanner>
109
+ </Box>
110
+ ),
111
+ };
112
+
113
+ export const WithClose: Story = {
114
+ render: () => {
115
+ const [visible, setVisible] = useState(true);
116
+
117
+ if (!visible) {
118
+ return (
119
+ <Button
120
+ appearance='transparent'
121
+ size='sm'
122
+ onPress={() => setVisible(true)}
123
+ >
124
+ Show banner
125
+ </Button>
126
+ );
127
+ }
128
+
129
+ return (
130
+ <Box lx={{ width: 's400' }}>
131
+ <MediaBanner imageUrl={IMAGE_URL} onClose={() => setVisible(false)}>
132
+ <MediaBannerTitle>
133
+ Earn Up to 12% APY With Staking Now!
134
+ </MediaBannerTitle>
135
+ <MediaBannerDescription>
136
+ Put your idle crypto to work. Start staking SOL, ETH, ATOM and more
137
+ directly from Ledger Live
138
+ </MediaBannerDescription>
139
+ </MediaBanner>
140
+ </Box>
141
+ );
142
+ },
143
+ };
@@ -0,0 +1,77 @@
1
+ import { describe, it, expect, jest } from '@jest/globals';
2
+ import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
3
+ import { fireEvent, render } from '@testing-library/react-native';
4
+ import { type ReactNode } from 'react';
5
+ import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
6
+ import {
7
+ MediaBanner,
8
+ MediaBannerTitle,
9
+ MediaBannerDescription,
10
+ } from './MediaBanner';
11
+
12
+ const TestWrapper = ({ children }: { children: ReactNode }) => (
13
+ <ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
14
+ {children}
15
+ </ThemeProvider>
16
+ );
17
+
18
+ const IMAGE_URL = 'https://example.com/image.jpg';
19
+
20
+ describe('MediaBanner', () => {
21
+ it('should render title and description', () => {
22
+ const { getByText } = render(
23
+ <TestWrapper>
24
+ <MediaBanner imageUrl={IMAGE_URL}>
25
+ <MediaBannerTitle>Banner Title</MediaBannerTitle>
26
+ <MediaBannerDescription>Banner description</MediaBannerDescription>
27
+ </MediaBanner>
28
+ </TestWrapper>,
29
+ );
30
+
31
+ getByText('Banner Title');
32
+ getByText('Banner description');
33
+ });
34
+
35
+ it('should call onClose when close button is pressed', () => {
36
+ const handleClose = jest.fn();
37
+ const { getByTestId } = render(
38
+ <TestWrapper>
39
+ <MediaBanner imageUrl={IMAGE_URL} onClose={handleClose}>
40
+ <MediaBannerTitle>Title</MediaBannerTitle>
41
+ </MediaBanner>
42
+ </TestWrapper>,
43
+ );
44
+
45
+ const closeButton = getByTestId('media-banner-close-button');
46
+ expect(closeButton).toBeTruthy();
47
+ fireEvent.press(closeButton);
48
+ expect(handleClose).toHaveBeenCalledTimes(1);
49
+ });
50
+
51
+ it('should apply surface background color', () => {
52
+ const { getByTestId } = render(
53
+ <TestWrapper>
54
+ <MediaBanner testID='media-banner' imageUrl={IMAGE_URL}>
55
+ <MediaBannerTitle>Title</MediaBannerTitle>
56
+ </MediaBanner>
57
+ </TestWrapper>,
58
+ );
59
+
60
+ const banner = getByTestId('media-banner');
61
+ expect(banner.props.style.backgroundColor).toBe(
62
+ ledgerLiveThemes.dark.colors.bg.surface,
63
+ );
64
+ });
65
+
66
+ it('should render with imageUrl prop', () => {
67
+ const { getByTestId } = render(
68
+ <TestWrapper>
69
+ <MediaBanner testID='media-banner' imageUrl={IMAGE_URL}>
70
+ <MediaBannerTitle>Title</MediaBannerTitle>
71
+ </MediaBanner>
72
+ </TestWrapper>,
73
+ );
74
+
75
+ expect(getByTestId('media-banner')).toBeTruthy();
76
+ });
77
+ });
@@ -0,0 +1,172 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Image, StyleSheet } from 'react-native';
3
+ import { useCommonTranslation } from '../../../i18n';
4
+ import { useStyleSheet, useTheme } from '../../../styles';
5
+ import { Close } from '../../Symbols';
6
+ import { InteractiveIcon } from '../InteractiveIcon';
7
+ import { Box, LinearGradient, Pressable, Text } from '../Utility';
8
+ import {
9
+ MediaBannerDescriptionProps,
10
+ MediaBannerProps,
11
+ MediaBannerTitleProps,
12
+ } from './types';
13
+
14
+ /**
15
+ * A promotional banner with a background image, title, description, and an optional close button.
16
+ *
17
+ * @see {@link https://ldls.vercel.app/?path=/docs/communication-mediabanner--docs Storybook}
18
+ */
19
+ export function MediaBanner({
20
+ lx,
21
+ style,
22
+ imageUrl,
23
+ onClose,
24
+ closeAccessibilityLabel,
25
+ children,
26
+ ...props
27
+ }: MediaBannerProps) {
28
+ const { t: translate } = useCommonTranslation();
29
+ const { theme: t } = useTheme();
30
+ const [imageLoadError, setImageLoadError] = useState(false);
31
+
32
+ useEffect(() => {
33
+ setImageLoadError(false);
34
+ }, [imageUrl]);
35
+
36
+ const showImage = imageUrl && !imageLoadError;
37
+
38
+ const styles = useStyleSheet(
39
+ (t) => ({
40
+ container: {
41
+ backgroundColor: t.colors.bg.surface,
42
+ borderRadius: t.borderRadius.md,
43
+ overflow: 'hidden',
44
+ flexDirection: 'row',
45
+ height: t.sizes.s72,
46
+ },
47
+ contentWrapper: {
48
+ flex: 1,
49
+ overflow: 'hidden',
50
+ justifyContent: 'center',
51
+ paddingHorizontal: t.spacings.s12,
52
+ paddingVertical: t.spacings.s2,
53
+ },
54
+ contentContainer: {
55
+ paddingVertical: t.spacings.s12,
56
+ gap: 4,
57
+ },
58
+ closeButton: {
59
+ position: 'absolute',
60
+ top: 8.5,
61
+ right: 8.5,
62
+ },
63
+ }),
64
+ [],
65
+ );
66
+
67
+ return (
68
+ <Pressable lx={lx} style={[styles.container, style]} {...props}>
69
+ <Box style={styles.contentWrapper}>
70
+ <Box style={styles.contentContainer}>{children}</Box>
71
+ </Box>
72
+ <Box style={{ width: 120 }}>
73
+ {showImage && (
74
+ <Image
75
+ source={{ uri: imageUrl }}
76
+ style={StyleSheet.absoluteFill}
77
+ resizeMode='cover'
78
+ onError={() => setImageLoadError(true)}
79
+ accessible={false}
80
+ />
81
+ )}
82
+ <LinearGradient
83
+ direction='to-topright'
84
+ stops={[
85
+ { color: t.colors.bg.black, opacity: 0, offset: 0.67 },
86
+ { color: t.colors.bg.black, opacity: 0.8 },
87
+ ]}
88
+ style={StyleSheet.absoluteFill}
89
+ accessible={false}
90
+ pointerEvents='none'
91
+ />
92
+ </Box>
93
+ {onClose && (
94
+ <Box style={styles.closeButton}>
95
+ <InteractiveIcon
96
+ testID='media-banner-close-button'
97
+ iconType='stroked'
98
+ appearance='white'
99
+ onPress={onClose}
100
+ accessibilityLabel={
101
+ closeAccessibilityLabel ||
102
+ translate('components.banner.closeAriaLabel')
103
+ }
104
+ >
105
+ <Close size={16} />
106
+ </InteractiveIcon>
107
+ </Box>
108
+ )}
109
+ </Pressable>
110
+ );
111
+ }
112
+
113
+ /**
114
+ * The title of the MediaBanner. Clamps at 1 line.
115
+ */
116
+ export function MediaBannerTitle({
117
+ lx,
118
+ style,
119
+ children,
120
+ ...props
121
+ }: MediaBannerTitleProps) {
122
+ const styles = useStyleSheet(
123
+ (t) => ({
124
+ title: StyleSheet.flatten([
125
+ t.typographies.body2SemiBold,
126
+ {
127
+ color: t.colors.text.base,
128
+ },
129
+ ]),
130
+ }),
131
+ [],
132
+ );
133
+
134
+ return (
135
+ <Text lx={lx} style={[styles.title, style]} numberOfLines={1} {...props}>
136
+ {children}
137
+ </Text>
138
+ );
139
+ }
140
+
141
+ /**
142
+ * The description of the MediaBanner. Clamps at 2 lines.
143
+ */
144
+ export function MediaBannerDescription({
145
+ lx,
146
+ style,
147
+ children,
148
+ ...props
149
+ }: MediaBannerDescriptionProps) {
150
+ const styles = useStyleSheet(
151
+ (t) => ({
152
+ description: StyleSheet.flatten([
153
+ t.typographies.body3,
154
+ {
155
+ color: t.colors.text.muted,
156
+ },
157
+ ]),
158
+ }),
159
+ [],
160
+ );
161
+
162
+ return (
163
+ <Text
164
+ lx={lx}
165
+ style={[styles.description, style]}
166
+ numberOfLines={2}
167
+ {...props}
168
+ >
169
+ {children}
170
+ </Text>
171
+ );
172
+ }
@@ -0,0 +1,2 @@
1
+ export * from './MediaBanner';
2
+ export * from './types';
@@ -0,0 +1,44 @@
1
+ import { ReactNode } from 'react';
2
+ import { StyledPressableProps, StyledTextProps } from '../../../styles';
3
+
4
+ /**
5
+ * Props for the MediaBanner root component.
6
+ */
7
+ export type MediaBannerProps = {
8
+ /**
9
+ * URL of the background image displayed on the right side.
10
+ */
11
+ imageUrl: string;
12
+ /**
13
+ * Optional close action callback.
14
+ */
15
+ onClose?: () => void;
16
+ /**
17
+ * Optional accessibility label for the close button.
18
+ */
19
+ closeAccessibilityLabel?: string;
20
+ /**
21
+ * The banner content (MediaBannerTitle, MediaBannerDescription).
22
+ */
23
+ children: ReactNode;
24
+ } & Omit<StyledPressableProps, 'children'>;
25
+
26
+ /**
27
+ * Props for the MediaBannerTitle component.
28
+ */
29
+ export type MediaBannerTitleProps = {
30
+ /**
31
+ * The title text content.
32
+ */
33
+ children: ReactNode;
34
+ } & Omit<StyledTextProps, 'children'>;
35
+
36
+ /**
37
+ * Props for the MediaBannerDescription component.
38
+ */
39
+ export type MediaBannerDescriptionProps = {
40
+ /**
41
+ * The description text content.
42
+ */
43
+ children: ReactNode;
44
+ } & Omit<StyledTextProps, 'children'>;
@@ -0,0 +1,111 @@
1
+ import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks';
2
+ import * as MediaCardStories from './MediaCard.stories';
3
+ import { CustomTabs, Tab } from '../../../../.storybook/components';
4
+ import CommonRulesDoAndDont from '../../../../.storybook/components/DoVsDont/CommonRulesDoAndDont.mdx';
5
+
6
+ <Meta title='Communication/MediaCard' of={MediaCardStories} />
7
+
8
+ # MediaCard
9
+
10
+ <CustomTabs>
11
+ <Tab label="Overview">
12
+
13
+ ## Introduction
14
+
15
+ MediaCard is a promotional card component that displays a full-bleed background image with gradient overlays for text readability. It uses a simplified compound pattern with `MediaCardTitle` for the text and free-form children for leading content (e.g. tags, icons).
16
+
17
+ > View in [Figma](https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7?node-id=15160-2853).
18
+
19
+ ## Anatomy
20
+
21
+ <Canvas of={MediaCardStories.Base} />
22
+
23
+ - **MediaCard**: Root pressable container with background image and gradient overlays
24
+ - **MediaCardTitle**: Styled title text (clamps at 3 lines)
25
+ - **Leading content**: Any element rendered before `MediaCardTitle` (tags, badges, icons) — no wrapper needed
26
+ - **Close button**: Rendered via the `onClose` prop (positioned absolute top-right) — only visible when `onClose` is provided
27
+
28
+ ## Properties
29
+
30
+ <Canvas of={MediaCardStories.Base} />
31
+ <Controls of={MediaCardStories.Base} />
32
+
33
+ ### Layout
34
+
35
+ The card fills its parent width by default.
36
+
37
+ <Canvas of={MediaCardStories.LayoutShowcase} />
38
+
39
+ ### Compositions
40
+
41
+ Leading content is optional — just place any element before `MediaCardTitle` inside `MediaCard`.
42
+
43
+ <Canvas of={MediaCardStories.CompositionShowcase} />
44
+
45
+ ## Accessibility
46
+
47
+ - The root element uses `accessibilityRole='button'` when `onPress` is provided
48
+ - The close button includes an `accessibilityLabel` (auto-translated via i18n) — only rendered when `onClose` is provided
49
+ - The background image is not accessible (`accessible={false}`) since it is decorative
50
+ - Components forward refs and spread props for accessibility support
51
+
52
+ </Tab>
53
+ <Tab label="Implementation">
54
+
55
+ ## Setup
56
+
57
+ Install and set up the library with our [Setup Guide →](?path=/docs/getting-started-setup--docs).
58
+
59
+ ## Basic Usage
60
+
61
+ Both `onPress` and `onClose` are optional. When `onClose` is omitted, the close button is hidden. When `onPress` is omitted, the card is non-interactive.
62
+
63
+ ```tsx
64
+ import { MediaCard, MediaCardTitle, Tag } from '@ledgerhq/lumen-ui-rnative';
65
+
66
+ function MyComponent() {
67
+ return (
68
+ <MediaCard
69
+ imageUrl='https://example.com/promo.jpg'
70
+ onPress={() => console.log('pressed')}
71
+ onClose={() => console.log('closed')}
72
+ >
73
+ <Tag label='New' size='md' />
74
+ <MediaCardTitle>Card title</MediaCardTitle>
75
+ </MediaCard>
76
+ );
77
+ }
78
+ ```
79
+
80
+ ### Without Close Button
81
+
82
+ Omit `onClose` to hide the close button — useful for non-dismissible promotions:
83
+
84
+ ```tsx
85
+ <MediaCard
86
+ imageUrl='https://example.com/promo.jpg'
87
+ onPress={() => console.log('pressed')}
88
+ >
89
+ <Tag label='New' size='md' />
90
+ <MediaCardTitle>Card title</MediaCardTitle>
91
+ </MediaCard>
92
+ ```
93
+
94
+ ### Layout Adjustments with lx
95
+
96
+ Use the `lx` prop for layout adjustments like margins or positioning:
97
+
98
+ ```tsx
99
+ <MediaCard
100
+ imageUrl='https://example.com/promo.jpg'
101
+ onPress={() => console.log('pressed')}
102
+ lx={{ marginTop: 's16', marginBottom: 's8' }}
103
+ >
104
+ <MediaCardTitle>With margin</MediaCardTitle>
105
+ </MediaCard>
106
+ ```
107
+
108
+ <CommonRulesDoAndDont />
109
+
110
+ </Tab>
111
+ </CustomTabs>