@ledgerhq/lumen-ui-rnative 0.1.12 → 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 (29) hide show
  1. package/dist/module/lib/Components/MediaBanner/MediaBanner.js +158 -0
  2. package/dist/module/lib/Components/MediaBanner/MediaBanner.js.map +1 -0
  3. package/dist/module/lib/Components/MediaBanner/MediaBanner.mdx +150 -0
  4. package/dist/module/lib/Components/MediaBanner/MediaBanner.stories.js +135 -0
  5. package/dist/module/lib/Components/MediaBanner/MediaBanner.stories.js.map +1 -0
  6. package/dist/module/lib/Components/MediaBanner/MediaBanner.test.js +83 -0
  7. package/dist/module/lib/Components/MediaBanner/MediaBanner.test.js.map +1 -0
  8. package/dist/module/lib/Components/MediaBanner/index.js +5 -0
  9. package/dist/module/lib/Components/MediaBanner/index.js.map +1 -0
  10. package/dist/module/lib/Components/MediaBanner/types.js +4 -0
  11. package/dist/module/lib/Components/MediaBanner/types.js.map +1 -0
  12. package/dist/module/lib/Components/index.js +1 -0
  13. package/dist/module/lib/Components/index.js.map +1 -1
  14. package/dist/typescript/src/lib/Components/MediaBanner/MediaBanner.d.ts +16 -0
  15. package/dist/typescript/src/lib/Components/MediaBanner/MediaBanner.d.ts.map +1 -0
  16. package/dist/typescript/src/lib/Components/MediaBanner/index.d.ts +3 -0
  17. package/dist/typescript/src/lib/Components/MediaBanner/index.d.ts.map +1 -0
  18. package/dist/typescript/src/lib/Components/MediaBanner/types.d.ts +42 -0
  19. package/dist/typescript/src/lib/Components/MediaBanner/types.d.ts.map +1 -0
  20. package/dist/typescript/src/lib/Components/index.d.ts +1 -0
  21. package/dist/typescript/src/lib/Components/index.d.ts.map +1 -1
  22. package/package.json +1 -1
  23. package/src/lib/Components/MediaBanner/MediaBanner.mdx +150 -0
  24. package/src/lib/Components/MediaBanner/MediaBanner.stories.tsx +143 -0
  25. package/src/lib/Components/MediaBanner/MediaBanner.test.tsx +77 -0
  26. package/src/lib/Components/MediaBanner/MediaBanner.tsx +172 -0
  27. package/src/lib/Components/MediaBanner/index.ts +2 -0
  28. package/src/lib/Components/MediaBanner/types.ts +44 -0
  29. package/src/lib/Components/index.ts +1 -0
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { Image, StyleSheet } from 'react-native';
5
+ import { useCommonTranslation } from "../../../i18n/index.js";
6
+ import { useStyleSheet, useTheme } from "../../../styles/index.js";
7
+ import { Close } from "../../Symbols/index.js";
8
+ import { InteractiveIcon } from "../InteractiveIcon/index.js";
9
+ import { Box, LinearGradient, Pressable, Text } from "../Utility/index.js";
10
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
+ /**
12
+ * A promotional banner with a background image, title, description, and an optional close button.
13
+ *
14
+ * @see {@link https://ldls.vercel.app/?path=/docs/communication-mediabanner--docs Storybook}
15
+ */
16
+ export function MediaBanner({
17
+ lx,
18
+ style,
19
+ imageUrl,
20
+ onClose,
21
+ closeAccessibilityLabel,
22
+ children,
23
+ ...props
24
+ }) {
25
+ const {
26
+ t: translate
27
+ } = useCommonTranslation();
28
+ const {
29
+ theme: t
30
+ } = useTheme();
31
+ const [imageLoadError, setImageLoadError] = useState(false);
32
+ useEffect(() => {
33
+ setImageLoadError(false);
34
+ }, [imageUrl]);
35
+ const showImage = imageUrl && !imageLoadError;
36
+ const styles = useStyleSheet(t => ({
37
+ container: {
38
+ backgroundColor: t.colors.bg.surface,
39
+ borderRadius: t.borderRadius.md,
40
+ overflow: 'hidden',
41
+ flexDirection: 'row',
42
+ height: t.sizes.s72
43
+ },
44
+ contentWrapper: {
45
+ flex: 1,
46
+ overflow: 'hidden',
47
+ justifyContent: 'center',
48
+ paddingHorizontal: t.spacings.s12,
49
+ paddingVertical: t.spacings.s2
50
+ },
51
+ contentContainer: {
52
+ paddingVertical: t.spacings.s12,
53
+ gap: 4
54
+ },
55
+ closeButton: {
56
+ position: 'absolute',
57
+ top: 8.5,
58
+ right: 8.5
59
+ }
60
+ }), []);
61
+ return /*#__PURE__*/_jsxs(Pressable, {
62
+ lx: lx,
63
+ style: [styles.container, style],
64
+ ...props,
65
+ children: [/*#__PURE__*/_jsx(Box, {
66
+ style: styles.contentWrapper,
67
+ children: /*#__PURE__*/_jsx(Box, {
68
+ style: styles.contentContainer,
69
+ children: children
70
+ })
71
+ }), /*#__PURE__*/_jsxs(Box, {
72
+ style: {
73
+ width: 120
74
+ },
75
+ children: [showImage && /*#__PURE__*/_jsx(Image, {
76
+ source: {
77
+ uri: imageUrl
78
+ },
79
+ style: StyleSheet.absoluteFill,
80
+ resizeMode: "cover",
81
+ onError: () => setImageLoadError(true),
82
+ accessible: false
83
+ }), /*#__PURE__*/_jsx(LinearGradient, {
84
+ direction: "to-topright",
85
+ stops: [{
86
+ color: t.colors.bg.black,
87
+ opacity: 0,
88
+ offset: 0.67
89
+ }, {
90
+ color: t.colors.bg.black,
91
+ opacity: 0.8
92
+ }],
93
+ style: StyleSheet.absoluteFill,
94
+ accessible: false,
95
+ pointerEvents: "none"
96
+ })]
97
+ }), onClose && /*#__PURE__*/_jsx(Box, {
98
+ style: styles.closeButton,
99
+ children: /*#__PURE__*/_jsx(InteractiveIcon, {
100
+ testID: "media-banner-close-button",
101
+ iconType: "stroked",
102
+ appearance: "white",
103
+ onPress: onClose,
104
+ accessibilityLabel: closeAccessibilityLabel || translate('components.banner.closeAriaLabel'),
105
+ children: /*#__PURE__*/_jsx(Close, {
106
+ size: 16
107
+ })
108
+ })
109
+ })]
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
+ }) {
122
+ const styles = useStyleSheet(t => ({
123
+ title: StyleSheet.flatten([t.typographies.body2SemiBold, {
124
+ color: t.colors.text.base
125
+ }])
126
+ }), []);
127
+ return /*#__PURE__*/_jsx(Text, {
128
+ lx: lx,
129
+ style: [styles.title, style],
130
+ numberOfLines: 1,
131
+ ...props,
132
+ children: children
133
+ });
134
+ }
135
+
136
+ /**
137
+ * The description of the MediaBanner. Clamps at 2 lines.
138
+ */
139
+ export function MediaBannerDescription({
140
+ lx,
141
+ style,
142
+ children,
143
+ ...props
144
+ }) {
145
+ const styles = useStyleSheet(t => ({
146
+ description: StyleSheet.flatten([t.typographies.body3, {
147
+ color: t.colors.text.muted
148
+ }])
149
+ }), []);
150
+ return /*#__PURE__*/_jsx(Text, {
151
+ lx: lx,
152
+ style: [styles.description, style],
153
+ numberOfLines: 2,
154
+ ...props,
155
+ children: children
156
+ });
157
+ }
158
+ //# sourceMappingURL=MediaBanner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["useEffect","useState","Image","StyleSheet","useCommonTranslation","useStyleSheet","useTheme","Close","InteractiveIcon","Box","LinearGradient","Pressable","Text","jsx","_jsx","jsxs","_jsxs","MediaBanner","lx","style","imageUrl","onClose","closeAccessibilityLabel","children","props","t","translate","theme","imageLoadError","setImageLoadError","showImage","styles","container","backgroundColor","colors","bg","surface","borderRadius","md","overflow","flexDirection","height","sizes","s72","contentWrapper","flex","justifyContent","paddingHorizontal","spacings","s12","paddingVertical","s2","contentContainer","gap","closeButton","position","top","right","width","source","uri","absoluteFill","resizeMode","onError","accessible","direction","stops","color","black","opacity","offset","pointerEvents","testID","iconType","appearance","onPress","accessibilityLabel","size","MediaBannerTitle","title","flatten","typographies","body2SemiBold","text","base","numberOfLines","MediaBannerDescription","description","body3","muted"],"sourceRoot":"../../../../../src","sources":["lib/Components/MediaBanner/MediaBanner.tsx"],"mappings":"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","ignoreList":[]}
@@ -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,135 @@
1
+ "use strict";
2
+
3
+ import { useState } from 'react';
4
+ import { Button } from "../Button/index.js";
5
+ import { Box } from "../Utility/index.js";
6
+ import { MediaBanner, MediaBannerDescription, MediaBannerTitle } from "./MediaBanner.js";
7
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
+ const IMAGE_URL = 'https://images.unsplash.com/photo-1663741954108-d15d514529ef';
9
+ const meta = {
10
+ component: MediaBanner,
11
+ title: 'Communication/MediaBanner',
12
+ subcomponents: {
13
+ MediaBannerTitle,
14
+ MediaBannerDescription
15
+ },
16
+ parameters: {
17
+ docs: {
18
+ source: {
19
+ language: 'tsx',
20
+ format: true,
21
+ type: 'code'
22
+ }
23
+ }
24
+ },
25
+ argTypes: {
26
+ imageUrl: {
27
+ control: 'text',
28
+ description: 'URL of the background image'
29
+ },
30
+ onClose: {
31
+ control: 'select',
32
+ description: 'Close action callback',
33
+ options: ['With Close', 'None'],
34
+ mapping: {
35
+ 'With Close': () => {
36
+ console.log('Close clicked');
37
+ },
38
+ None: undefined
39
+ }
40
+ }
41
+ }
42
+ };
43
+ export default meta;
44
+ export const Base = {
45
+ args: {
46
+ imageUrl: IMAGE_URL
47
+ },
48
+ render: args => /*#__PURE__*/_jsx(Box, {
49
+ lx: {
50
+ width: 's400'
51
+ },
52
+ children: /*#__PURE__*/_jsxs(MediaBanner, {
53
+ ...args,
54
+ children: [/*#__PURE__*/_jsx(MediaBannerTitle, {
55
+ children: "Firmware Update"
56
+ }), /*#__PURE__*/_jsx(MediaBannerDescription, {
57
+ children: "Keep your Nano updated!"
58
+ })]
59
+ })
60
+ }),
61
+ parameters: {
62
+ docs: {
63
+ source: {
64
+ code: `
65
+ <MediaBanner imageUrl="https://images.unsplash.com/photo-1663741954108-d15d514529ef">
66
+ <MediaBannerTitle>Firmware Update</MediaBannerTitle>
67
+ <MediaBannerDescription>
68
+ Keep your Nano updated!
69
+ </MediaBannerDescription>
70
+ </MediaBanner>
71
+ `
72
+ }
73
+ }
74
+ }
75
+ };
76
+ export const Truncation = {
77
+ render: () => /*#__PURE__*/_jsx(Box, {
78
+ lx: {
79
+ width: 's400'
80
+ },
81
+ children: /*#__PURE__*/_jsxs(MediaBanner, {
82
+ imageUrl: IMAGE_URL,
83
+ onClose: () => console.log('close'),
84
+ children: [/*#__PURE__*/_jsx(MediaBannerTitle, {
85
+ children: "Earn Up to 12% APY With Staking Now And Much More Rewards Awaiting You"
86
+ }), /*#__PURE__*/_jsx(MediaBannerDescription, {
87
+ children: "Put your idle crypto to work. Start staking SOL, ETH, ATOM and more directly from Ledger Live. Maximize your returns with our secure staking solutions."
88
+ })]
89
+ })
90
+ })
91
+ };
92
+ export const WithBrokenImage = {
93
+ render: () => /*#__PURE__*/_jsx(Box, {
94
+ lx: {
95
+ width: 's400'
96
+ },
97
+ children: /*#__PURE__*/_jsxs(MediaBanner, {
98
+ imageUrl: "https://broken-url.invalid/image.jpg",
99
+ onClose: () => console.log('close'),
100
+ children: [/*#__PURE__*/_jsx(MediaBannerTitle, {
101
+ children: "Sorry!"
102
+ }), /*#__PURE__*/_jsx(MediaBannerDescription, {
103
+ children: "The image failed to load so the banner decided to gracefully hide it."
104
+ })]
105
+ })
106
+ })
107
+ };
108
+ export const WithClose = {
109
+ render: () => {
110
+ const [visible, setVisible] = useState(true);
111
+ if (!visible) {
112
+ return /*#__PURE__*/_jsx(Button, {
113
+ appearance: "transparent",
114
+ size: "sm",
115
+ onPress: () => setVisible(true),
116
+ children: "Show banner"
117
+ });
118
+ }
119
+ return /*#__PURE__*/_jsx(Box, {
120
+ lx: {
121
+ width: 's400'
122
+ },
123
+ children: /*#__PURE__*/_jsxs(MediaBanner, {
124
+ imageUrl: IMAGE_URL,
125
+ onClose: () => setVisible(false),
126
+ children: [/*#__PURE__*/_jsx(MediaBannerTitle, {
127
+ children: "Earn Up to 12% APY With Staking Now!"
128
+ }), /*#__PURE__*/_jsx(MediaBannerDescription, {
129
+ children: "Put your idle crypto to work. Start staking SOL, ETH, ATOM and more directly from Ledger Live"
130
+ })]
131
+ })
132
+ });
133
+ }
134
+ };
135
+ //# sourceMappingURL=MediaBanner.stories.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["useState","Button","Box","MediaBanner","MediaBannerDescription","MediaBannerTitle","jsx","_jsx","jsxs","_jsxs","IMAGE_URL","meta","component","title","subcomponents","parameters","docs","source","language","format","type","argTypes","imageUrl","control","description","onClose","options","mapping","With Close","console","log","None","undefined","Base","args","render","lx","width","children","code","Truncation","WithBrokenImage","WithClose","visible","setVisible","appearance","size","onPress"],"sourceRoot":"../../../../../src","sources":["lib/Components/MediaBanner/MediaBanner.stories.tsx"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+
3
+ import { describe, it, expect, jest } from '@jest/globals';
4
+ import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
5
+ import { fireEvent, render } from '@testing-library/react-native';
6
+ import { ThemeProvider } from "../ThemeProvider/ThemeProvider.js";
7
+ import { MediaBanner, MediaBannerTitle, MediaBannerDescription } from "./MediaBanner.js";
8
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
+ const TestWrapper = ({
10
+ children
11
+ }) => /*#__PURE__*/_jsx(ThemeProvider, {
12
+ themes: ledgerLiveThemes,
13
+ colorScheme: "dark",
14
+ locale: "en",
15
+ children: children
16
+ });
17
+ const IMAGE_URL = 'https://example.com/image.jpg';
18
+ describe('MediaBanner', () => {
19
+ it('should render title and description', () => {
20
+ const {
21
+ getByText
22
+ } = render(/*#__PURE__*/_jsx(TestWrapper, {
23
+ children: /*#__PURE__*/_jsxs(MediaBanner, {
24
+ imageUrl: IMAGE_URL,
25
+ children: [/*#__PURE__*/_jsx(MediaBannerTitle, {
26
+ children: "Banner Title"
27
+ }), /*#__PURE__*/_jsx(MediaBannerDescription, {
28
+ children: "Banner description"
29
+ })]
30
+ })
31
+ }));
32
+ getByText('Banner Title');
33
+ getByText('Banner description');
34
+ });
35
+ it('should call onClose when close button is pressed', () => {
36
+ const handleClose = jest.fn();
37
+ const {
38
+ getByTestId
39
+ } = render(/*#__PURE__*/_jsx(TestWrapper, {
40
+ children: /*#__PURE__*/_jsx(MediaBanner, {
41
+ imageUrl: IMAGE_URL,
42
+ onClose: handleClose,
43
+ children: /*#__PURE__*/_jsx(MediaBannerTitle, {
44
+ children: "Title"
45
+ })
46
+ })
47
+ }));
48
+ const closeButton = getByTestId('media-banner-close-button');
49
+ expect(closeButton).toBeTruthy();
50
+ fireEvent.press(closeButton);
51
+ expect(handleClose).toHaveBeenCalledTimes(1);
52
+ });
53
+ it('should apply surface background color', () => {
54
+ const {
55
+ getByTestId
56
+ } = render(/*#__PURE__*/_jsx(TestWrapper, {
57
+ children: /*#__PURE__*/_jsx(MediaBanner, {
58
+ testID: "media-banner",
59
+ imageUrl: IMAGE_URL,
60
+ children: /*#__PURE__*/_jsx(MediaBannerTitle, {
61
+ children: "Title"
62
+ })
63
+ })
64
+ }));
65
+ const banner = getByTestId('media-banner');
66
+ expect(banner.props.style.backgroundColor).toBe(ledgerLiveThemes.dark.colors.bg.surface);
67
+ });
68
+ it('should render with imageUrl prop', () => {
69
+ const {
70
+ getByTestId
71
+ } = render(/*#__PURE__*/_jsx(TestWrapper, {
72
+ children: /*#__PURE__*/_jsx(MediaBanner, {
73
+ testID: "media-banner",
74
+ imageUrl: IMAGE_URL,
75
+ children: /*#__PURE__*/_jsx(MediaBannerTitle, {
76
+ children: "Title"
77
+ })
78
+ })
79
+ }));
80
+ expect(getByTestId('media-banner')).toBeTruthy();
81
+ });
82
+ });
83
+ //# sourceMappingURL=MediaBanner.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["describe","it","expect","jest","ledgerLiveThemes","fireEvent","render","ThemeProvider","MediaBanner","MediaBannerTitle","MediaBannerDescription","jsx","_jsx","jsxs","_jsxs","TestWrapper","children","themes","colorScheme","locale","IMAGE_URL","getByText","imageUrl","handleClose","fn","getByTestId","onClose","closeButton","toBeTruthy","press","toHaveBeenCalledTimes","testID","banner","props","style","backgroundColor","toBe","dark","colors","bg","surface"],"sourceRoot":"../../../../../src","sources":["lib/Components/MediaBanner/MediaBanner.test.tsx"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ export * from "./MediaBanner.js";
4
+ export * from "./types.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../../../../src","sources":["lib/Components/MediaBanner/index.ts"],"mappings":";;AAAA,cAAc,kBAAe;AAC7B,cAAc,YAAS","ignoreList":[]}
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ export {};
4
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../../../../src","sources":["lib/Components/MediaBanner/types.ts"],"mappings":"","ignoreList":[]}
@@ -18,6 +18,7 @@ export * from "./IconButton/index.js";
18
18
  export * from "./InteractiveIcon/index.js";
19
19
  export * from "./Link/index.js";
20
20
  export * from "./ListItem/index.js";
21
+ export * from "./MediaBanner/index.js";
21
22
  export * from "./MediaCard/index.js";
22
23
  export * from "./NavBar/index.js";
23
24
  export * from "./PageIndicator/index.js";
@@ -1 +1 @@
1
- {"version":3,"names":[],"sourceRoot":"../../../../src","sources":["lib/Components/index.ts"],"mappings":";;AAAA,cAAc,yBAAgB;AAC9B,cAAc,0BAAiB;AAC/B,cAAc,wBAAe;AAC7B,cAAc,mBAAU;AACxB,cAAc,mBAAU;AACxB,cAAc,wBAAe;AAC7B,cAAc,mBAAU;AACxB,cAAc,0BAAiB;AAC/B,cAAc,iBAAQ;AACtB,cAAc,uBAAc;AAC5B,cAAc,0BAAiB;AAC/B,cAAc,qBAAY;AAC1B,cAAc,oBAAW;AACzB,cAAc,iBAAQ;AACtB,cAAc,uBAAc;AAC5B,cAAc,4BAAmB;AACjC,cAAc,iBAAQ;AACtB,cAAc,qBAAY;AAC1B,cAAc,sBAAa;AAC3B,cAAc,mBAAU;AACxB,cAAc,0BAAiB;AAC/B,cAAc,wBAAe;AAC7B,cAAc,6BAAoB;AAClC,cAAc,mBAAU;AACxB,cAAc,qBAAY;AAC1B,cAAc,oBAAW;AACzB,cAAc,iBAAQ;AACtB,cAAc,oBAAW;AACzB,cAAc,sBAAa;AAC3B,cAAc,mBAAU;AACxB,cAAc,mBAAU;AACxB,cAAc,gBAAO;AACrB,cAAc,oBAAW;AACzB,cAAc,sBAAa;AAC3B,cAAc,0BAAiB;AAC/B,cAAc,iBAAQ;AACtB,cAAc,uBAAc;AAC5B,cAAc,oBAAW","ignoreList":[]}
1
+ {"version":3,"names":[],"sourceRoot":"../../../../src","sources":["lib/Components/index.ts"],"mappings":";;AAAA,cAAc,yBAAgB;AAC9B,cAAc,0BAAiB;AAC/B,cAAc,wBAAe;AAC7B,cAAc,mBAAU;AACxB,cAAc,mBAAU;AACxB,cAAc,wBAAe;AAC7B,cAAc,mBAAU;AACxB,cAAc,0BAAiB;AAC/B,cAAc,iBAAQ;AACtB,cAAc,uBAAc;AAC5B,cAAc,0BAAiB;AAC/B,cAAc,qBAAY;AAC1B,cAAc,oBAAW;AACzB,cAAc,iBAAQ;AACtB,cAAc,uBAAc;AAC5B,cAAc,4BAAmB;AACjC,cAAc,iBAAQ;AACtB,cAAc,qBAAY;AAC1B,cAAc,wBAAe;AAC7B,cAAc,sBAAa;AAC3B,cAAc,mBAAU;AACxB,cAAc,0BAAiB;AAC/B,cAAc,wBAAe;AAC7B,cAAc,6BAAoB;AAClC,cAAc,mBAAU;AACxB,cAAc,qBAAY;AAC1B,cAAc,oBAAW;AACzB,cAAc,iBAAQ;AACtB,cAAc,oBAAW;AACzB,cAAc,sBAAa;AAC3B,cAAc,mBAAU;AACxB,cAAc,mBAAU;AACxB,cAAc,gBAAO;AACrB,cAAc,oBAAW;AACzB,cAAc,sBAAa;AAC3B,cAAc,0BAAiB;AAC/B,cAAc,iBAAQ;AACtB,cAAc,uBAAc;AAC5B,cAAc,oBAAW","ignoreList":[]}
@@ -0,0 +1,16 @@
1
+ import { MediaBannerDescriptionProps, MediaBannerProps, MediaBannerTitleProps } from './types';
2
+ /**
3
+ * A promotional banner with a background image, title, description, and an optional close button.
4
+ *
5
+ * @see {@link https://ldls.vercel.app/?path=/docs/communication-mediabanner--docs Storybook}
6
+ */
7
+ export declare function MediaBanner({ lx, style, imageUrl, onClose, closeAccessibilityLabel, children, ...props }: MediaBannerProps): import("react/jsx-runtime").JSX.Element;
8
+ /**
9
+ * The title of the MediaBanner. Clamps at 1 line.
10
+ */
11
+ export declare function MediaBannerTitle({ lx, style, children, ...props }: MediaBannerTitleProps): import("react/jsx-runtime").JSX.Element;
12
+ /**
13
+ * The description of the MediaBanner. Clamps at 2 lines.
14
+ */
15
+ export declare function MediaBannerDescription({ lx, style, children, ...props }: MediaBannerDescriptionProps): import("react/jsx-runtime").JSX.Element;
16
+ //# sourceMappingURL=MediaBanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MediaBanner.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/MediaBanner/MediaBanner.tsx"],"names":[],"mappings":"AAOA,OAAO,EACL,2BAA2B,EAC3B,gBAAgB,EAChB,qBAAqB,EACtB,MAAM,SAAS,CAAC;AAEjB;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAC1B,EAAE,EACF,KAAK,EACL,QAAQ,EACR,OAAO,EACP,uBAAuB,EACvB,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,gBAAgB,2CAoFlB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,EAAE,EACF,KAAK,EACL,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,qBAAqB,2CAkBvB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,EAAE,EACF,KAAK,EACL,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,2BAA2B,2CAuB7B"}
@@ -0,0 +1,3 @@
1
+ export * from './MediaBanner';
2
+ export * from './types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/MediaBanner/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC"}
@@ -0,0 +1,42 @@
1
+ import { ReactNode } from 'react';
2
+ import { StyledPressableProps, StyledTextProps } from '../../../styles';
3
+ /**
4
+ * Props for the MediaBanner root component.
5
+ */
6
+ export type MediaBannerProps = {
7
+ /**
8
+ * URL of the background image displayed on the right side.
9
+ */
10
+ imageUrl: string;
11
+ /**
12
+ * Optional close action callback.
13
+ */
14
+ onClose?: () => void;
15
+ /**
16
+ * Optional accessibility label for the close button.
17
+ */
18
+ closeAccessibilityLabel?: string;
19
+ /**
20
+ * The banner content (MediaBannerTitle, MediaBannerDescription).
21
+ */
22
+ children: ReactNode;
23
+ } & Omit<StyledPressableProps, 'children'>;
24
+ /**
25
+ * Props for the MediaBannerTitle component.
26
+ */
27
+ export type MediaBannerTitleProps = {
28
+ /**
29
+ * The title text content.
30
+ */
31
+ children: ReactNode;
32
+ } & Omit<StyledTextProps, 'children'>;
33
+ /**
34
+ * Props for the MediaBannerDescription component.
35
+ */
36
+ export type MediaBannerDescriptionProps = {
37
+ /**
38
+ * The description text content.
39
+ */
40
+ children: ReactNode;
41
+ } & Omit<StyledTextProps, 'children'>;
42
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/MediaBanner/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAExE;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;OAEG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB,GAAG,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC;AAE3C;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB,GAAG,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;AAEtC;;GAEG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB,GAAG,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC"}
@@ -16,6 +16,7 @@ export * from './IconButton';
16
16
  export * from './InteractiveIcon';
17
17
  export * from './Link';
18
18
  export * from './ListItem';
19
+ export * from './MediaBanner';
19
20
  export * from './MediaCard';
20
21
  export * from './NavBar';
21
22
  export * from './PageIndicator';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,OAAO,CAAC;AACtB,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,OAAO,CAAC;AACtB,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ledgerhq/lumen-ui-rnative",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "license": "Apache-2.0",
5
5
  "keywords": [
6
6
  "react-native",
@@ -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'>;
@@ -16,6 +16,7 @@ export * from './IconButton';
16
16
  export * from './InteractiveIcon';
17
17
  export * from './Link';
18
18
  export * from './ListItem';
19
+ export * from './MediaBanner';
19
20
  export * from './MediaCard';
20
21
  export * from './NavBar';
21
22
  export * from './PageIndicator';