@ledgerhq/lumen-ui-rnative 0.1.33 → 0.1.35

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 (151) hide show
  1. package/dist/module/index.js +1 -0
  2. package/dist/module/index.js.map +1 -1
  3. package/dist/module/lib/Animations/Pulse/Pulse.js +17 -7
  4. package/dist/module/lib/Animations/Pulse/Pulse.js.map +1 -1
  5. package/dist/module/lib/Components/BaseTag/BaseTag.js +122 -0
  6. package/dist/module/lib/Components/BaseTag/BaseTag.js.map +1 -0
  7. package/dist/module/lib/Components/BaseTag/BaseTag.test.js +144 -0
  8. package/dist/module/lib/Components/BaseTag/BaseTag.test.js.map +1 -0
  9. package/dist/module/lib/Components/BaseTag/index.js +5 -0
  10. package/dist/module/lib/Components/BaseTag/index.js.map +1 -0
  11. package/dist/module/lib/Components/BaseTag/types.js +4 -0
  12. package/dist/module/lib/Components/BaseTag/types.js.map +1 -0
  13. package/dist/module/lib/Components/BottomSheet/BottomSheet.js +12 -7
  14. package/dist/module/lib/Components/BottomSheet/BottomSheet.js.map +1 -1
  15. package/dist/module/lib/Components/BottomSheet/BottomSheet.stories.js +220 -1
  16. package/dist/module/lib/Components/BottomSheet/BottomSheet.stories.js.map +1 -1
  17. package/dist/module/lib/Components/BottomSheet/BottomSheet.test.js +73 -0
  18. package/dist/module/lib/Components/BottomSheet/BottomSheet.test.js.map +1 -1
  19. package/dist/module/lib/Components/BottomSheet/BottomSheetHeader.js +1 -1
  20. package/dist/module/lib/Components/BottomSheet/BottomSheetHeader.js.map +1 -1
  21. package/dist/module/lib/Components/BottomSheet/CustomHandle.js +15 -2
  22. package/dist/module/lib/Components/BottomSheet/CustomHandle.js.map +1 -1
  23. package/dist/module/lib/Components/Card/Card.js.map +1 -1
  24. package/dist/module/lib/Components/ListItem/ListItem.js.map +1 -1
  25. package/dist/module/lib/Components/MediaImage/MediaImage.js +10 -2
  26. package/dist/module/lib/Components/MediaImage/MediaImage.js.map +1 -1
  27. package/dist/module/lib/Components/MediaTag/MediaTag.js +39 -0
  28. package/dist/module/lib/Components/MediaTag/MediaTag.js.map +1 -0
  29. package/dist/module/lib/Components/MediaTag/MediaTag.mdx +161 -0
  30. package/dist/module/lib/Components/MediaTag/MediaTag.stories.js +122 -0
  31. package/dist/module/lib/Components/MediaTag/MediaTag.stories.js.map +1 -0
  32. package/dist/module/lib/Components/MediaTag/MediaTag.test.js +30 -0
  33. package/dist/module/lib/Components/MediaTag/MediaTag.test.js.map +1 -0
  34. package/dist/module/lib/Components/MediaTag/index.js +5 -0
  35. package/dist/module/lib/Components/MediaTag/index.js.map +1 -0
  36. package/dist/module/lib/Components/MediaTag/types.js +4 -0
  37. package/dist/module/lib/Components/MediaTag/types.js.map +1 -0
  38. package/dist/module/lib/Components/OptionList/OptionList.js +45 -4
  39. package/dist/module/lib/Components/OptionList/OptionList.js.map +1 -1
  40. package/dist/module/lib/Components/OptionList/OptionList.mdx +19 -0
  41. package/dist/module/lib/Components/OptionList/OptionList.stories.js +254 -1
  42. package/dist/module/lib/Components/OptionList/OptionList.stories.js.map +1 -1
  43. package/dist/module/lib/Components/OptionList/OptionList.test.js +136 -1
  44. package/dist/module/lib/Components/OptionList/OptionList.test.js.map +1 -1
  45. package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.js +39 -13
  46. package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.js.map +1 -1
  47. package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.test.js +117 -2
  48. package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.test.js.map +1 -1
  49. package/dist/module/lib/Components/PageIndicator/PageIndicator.test.js.map +1 -1
  50. package/dist/module/lib/Components/Skeleton/Skeleton.js +10 -3
  51. package/dist/module/lib/Components/Skeleton/Skeleton.js.map +1 -1
  52. package/dist/module/lib/Components/TabBar/TabBar.js +7 -6
  53. package/dist/module/lib/Components/TabBar/TabBar.js.map +1 -1
  54. package/dist/module/lib/Components/Tag/Tag.js +10 -95
  55. package/dist/module/lib/Components/Tag/Tag.js.map +1 -1
  56. package/dist/module/lib/Components/Tag/Tag.mdx +1 -79
  57. package/dist/module/lib/Components/Tag/Tag.stories.js +8 -1
  58. package/dist/module/lib/Components/Tag/Tag.stories.js.map +1 -1
  59. package/dist/module/lib/Components/Tag/Tag.test.js +69 -0
  60. package/dist/module/lib/Components/Tag/Tag.test.js.map +1 -0
  61. package/dist/module/lib/Components/index.js +1 -0
  62. package/dist/module/lib/Components/index.js.map +1 -1
  63. package/dist/module/styles/lx/resolveStyle.js.map +1 -1
  64. package/dist/typescript/src/index.d.ts +1 -0
  65. package/dist/typescript/src/index.d.ts.map +1 -1
  66. package/dist/typescript/src/lib/Animations/Pulse/Pulse.d.ts +1 -1
  67. package/dist/typescript/src/lib/Animations/Pulse/Pulse.d.ts.map +1 -1
  68. package/dist/typescript/src/lib/Animations/Pulse/types.d.ts +2 -1
  69. package/dist/typescript/src/lib/Animations/Pulse/types.d.ts.map +1 -1
  70. package/dist/typescript/src/lib/Components/BaseTag/BaseTag.d.ts +3 -0
  71. package/dist/typescript/src/lib/Components/BaseTag/BaseTag.d.ts.map +1 -0
  72. package/dist/typescript/src/lib/Components/BaseTag/index.d.ts +3 -0
  73. package/dist/typescript/src/lib/Components/BaseTag/index.d.ts.map +1 -0
  74. package/dist/typescript/src/lib/Components/BaseTag/types.d.ts +10 -0
  75. package/dist/typescript/src/lib/Components/BaseTag/types.d.ts.map +1 -0
  76. package/dist/typescript/src/lib/Components/BottomSheet/BottomSheet.d.ts +1 -1
  77. package/dist/typescript/src/lib/Components/BottomSheet/BottomSheet.d.ts.map +1 -1
  78. package/dist/typescript/src/lib/Components/BottomSheet/CustomHandle.d.ts +5 -2
  79. package/dist/typescript/src/lib/Components/BottomSheet/CustomHandle.d.ts.map +1 -1
  80. package/dist/typescript/src/lib/Components/BottomSheet/types.d.ts +16 -3
  81. package/dist/typescript/src/lib/Components/BottomSheet/types.d.ts.map +1 -1
  82. package/dist/typescript/src/lib/Components/Card/Card.d.ts.map +1 -1
  83. package/dist/typescript/src/lib/Components/ListItem/ListItem.d.ts +3 -3
  84. package/dist/typescript/src/lib/Components/ListItem/ListItem.d.ts.map +1 -1
  85. package/dist/typescript/src/lib/Components/MediaImage/MediaImage.d.ts.map +1 -1
  86. package/dist/typescript/src/lib/Components/MediaTag/MediaTag.d.ts +26 -0
  87. package/dist/typescript/src/lib/Components/MediaTag/MediaTag.d.ts.map +1 -0
  88. package/dist/typescript/src/lib/Components/MediaTag/index.d.ts +3 -0
  89. package/dist/typescript/src/lib/Components/MediaTag/index.d.ts.map +1 -0
  90. package/dist/typescript/src/lib/Components/MediaTag/types.d.ts +10 -0
  91. package/dist/typescript/src/lib/Components/MediaTag/types.d.ts.map +1 -0
  92. package/dist/typescript/src/lib/Components/OptionList/OptionList.d.ts +3 -2
  93. package/dist/typescript/src/lib/Components/OptionList/OptionList.d.ts.map +1 -1
  94. package/dist/typescript/src/lib/Components/OptionList/types.d.ts +42 -5
  95. package/dist/typescript/src/lib/Components/OptionList/types.d.ts.map +1 -1
  96. package/dist/typescript/src/lib/Components/OptionList/useOptionList/useOptionListItems.d.ts +9 -1
  97. package/dist/typescript/src/lib/Components/OptionList/useOptionList/useOptionListItems.d.ts.map +1 -1
  98. package/dist/typescript/src/lib/Components/Skeleton/Skeleton.d.ts +1 -1
  99. package/dist/typescript/src/lib/Components/Skeleton/Skeleton.d.ts.map +1 -1
  100. package/dist/typescript/src/lib/Components/TabBar/TabBar.d.ts.map +1 -1
  101. package/dist/typescript/src/lib/Components/Tag/Tag.d.ts +1 -1
  102. package/dist/typescript/src/lib/Components/Tag/Tag.d.ts.map +1 -1
  103. package/dist/typescript/src/lib/Components/Tag/types.d.ts +1 -1
  104. package/dist/typescript/src/lib/Components/Tag/types.d.ts.map +1 -1
  105. package/dist/typescript/src/lib/Components/index.d.ts +1 -0
  106. package/dist/typescript/src/lib/Components/index.d.ts.map +1 -1
  107. package/dist/typescript/src/lib/types/index.d.ts +3 -3
  108. package/dist/typescript/src/lib/types/index.d.ts.map +1 -1
  109. package/dist/typescript/src/styles/lx/resolveStyle.d.ts +3 -3
  110. package/dist/typescript/src/styles/lx/resolveStyle.d.ts.map +1 -1
  111. package/package.json +1 -1
  112. package/src/index.ts +1 -0
  113. package/src/lib/Animations/Pulse/Pulse.tsx +34 -29
  114. package/src/lib/Animations/Pulse/types.ts +2 -1
  115. package/src/lib/Components/BaseTag/BaseTag.test.tsx +137 -0
  116. package/src/lib/Components/BaseTag/BaseTag.tsx +152 -0
  117. package/src/lib/Components/BaseTag/index.ts +2 -0
  118. package/src/lib/Components/BaseTag/types.ts +11 -0
  119. package/src/lib/Components/BottomSheet/BottomSheet.stories.tsx +174 -1
  120. package/src/lib/Components/BottomSheet/BottomSheet.test.tsx +59 -0
  121. package/src/lib/Components/BottomSheet/BottomSheet.tsx +19 -7
  122. package/src/lib/Components/BottomSheet/BottomSheetHeader.tsx +1 -1
  123. package/src/lib/Components/BottomSheet/CustomHandle.tsx +26 -5
  124. package/src/lib/Components/BottomSheet/types.ts +24 -3
  125. package/src/lib/Components/Card/Card.tsx +3 -3
  126. package/src/lib/Components/ListItem/ListItem.tsx +3 -3
  127. package/src/lib/Components/MediaImage/MediaImage.tsx +10 -2
  128. package/src/lib/Components/MediaTag/MediaTag.mdx +161 -0
  129. package/src/lib/Components/MediaTag/MediaTag.stories.tsx +112 -0
  130. package/src/lib/Components/MediaTag/MediaTag.test.tsx +27 -0
  131. package/src/lib/Components/MediaTag/MediaTag.tsx +36 -0
  132. package/src/lib/Components/MediaTag/index.ts +2 -0
  133. package/src/lib/Components/MediaTag/types.ts +10 -0
  134. package/src/lib/Components/OptionList/OptionList.mdx +19 -0
  135. package/src/lib/Components/OptionList/OptionList.stories.tsx +254 -0
  136. package/src/lib/Components/OptionList/OptionList.test.tsx +143 -0
  137. package/src/lib/Components/OptionList/OptionList.tsx +49 -3
  138. package/src/lib/Components/OptionList/types.ts +46 -5
  139. package/src/lib/Components/OptionList/useOptionList/useOptionListItems.test.ts +124 -2
  140. package/src/lib/Components/OptionList/useOptionList/useOptionListItems.ts +53 -10
  141. package/src/lib/Components/PageIndicator/PageIndicator.test.tsx +2 -1
  142. package/src/lib/Components/Skeleton/Skeleton.tsx +9 -5
  143. package/src/lib/Components/TabBar/TabBar.tsx +3 -2
  144. package/src/lib/Components/Tag/Tag.mdx +1 -79
  145. package/src/lib/Components/Tag/Tag.stories.tsx +3 -0
  146. package/src/lib/Components/Tag/Tag.test.tsx +51 -0
  147. package/src/lib/Components/Tag/Tag.tsx +12 -119
  148. package/src/lib/Components/Tag/types.ts +2 -1
  149. package/src/lib/Components/index.ts +1 -0
  150. package/src/lib/types/index.ts +3 -3
  151. package/src/styles/lx/resolveStyle.ts +4 -3
@@ -0,0 +1,137 @@
1
+ import { describe, it, expect, jest } from '@jest/globals';
2
+ import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
3
+ import { render, screen } from '@testing-library/react-native';
4
+ import type { ViewStyle } from 'react-native';
5
+ import { Text } from 'react-native';
6
+ import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
7
+ import { BaseTag } from './BaseTag';
8
+ import type { BaseTagProps } from './types';
9
+
10
+ const { colors } = ledgerLiveThemes.dark;
11
+
12
+ const renderWithProvider = (component: React.ReactElement) =>
13
+ render(
14
+ <ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
15
+ {component}
16
+ </ThemeProvider>,
17
+ );
18
+
19
+ const baseProps = {
20
+ consumerName: 'BaseTagTest',
21
+ variant: 'tag',
22
+ label: 'Label',
23
+ testID: 'base-tag',
24
+ } satisfies BaseTagProps;
25
+
26
+ describe('BaseTag Component', () => {
27
+ describe('Rendering', () => {
28
+ it('should render label text', () => {
29
+ renderWithProvider(<BaseTag {...baseProps} label='Bitcoin' />);
30
+ expect(screen.getByText('Bitcoin')).toBeTruthy();
31
+ });
32
+
33
+ it('should render testID on root element', () => {
34
+ renderWithProvider(<BaseTag {...baseProps} />);
35
+ expect(screen.getByTestId('base-tag')).toBeTruthy();
36
+ });
37
+
38
+ it('should not render any icon when renderIcon is not provided', () => {
39
+ renderWithProvider(<BaseTag {...baseProps} />);
40
+ expect(screen.queryByTestId('test-icon')).toBeNull();
41
+ });
42
+
43
+ it('should call renderIcon with the computed icon style', () => {
44
+ const renderIcon = jest.fn(() => (
45
+ <Text testID='test-icon'>icon</Text>
46
+ )) as BaseTagProps['renderIcon'];
47
+ renderWithProvider(<BaseTag {...baseProps} renderIcon={renderIcon} />);
48
+ expect(screen.getByTestId('test-icon')).toBeTruthy();
49
+ expect(renderIcon).toHaveBeenCalledWith(
50
+ expect.objectContaining({ color: expect.any(String) }),
51
+ );
52
+ });
53
+ });
54
+
55
+ describe('Appearances', () => {
56
+ const cases: [NonNullable<BaseTagProps['appearance']>, string][] = [
57
+ ['base', colors.bg.mutedTransparent],
58
+ ['gray', colors.bg.mutedTransparent],
59
+ ['accent', colors.bg.accent],
60
+ ['accent-subtle', colors.bg.activeSubtle],
61
+ ['success', colors.bg.success],
62
+ ['error', colors.bg.error],
63
+ ['warning', colors.bg.warning],
64
+ ['white', colors.bg.white],
65
+ ];
66
+
67
+ it.each(cases)(
68
+ 'should render with %s background color',
69
+ (appearance, expected) => {
70
+ renderWithProvider(<BaseTag {...baseProps} appearance={appearance} />);
71
+ expect(screen.getByTestId('base-tag').props.style.backgroundColor).toBe(
72
+ expected,
73
+ );
74
+ },
75
+ );
76
+ });
77
+
78
+ describe('Sizes', () => {
79
+ it.each(['sm', 'md'] as const)('should render with %s size', (size) => {
80
+ renderWithProvider(<BaseTag {...baseProps} size={size} />);
81
+ expect(screen.getByTestId('base-tag')).toBeTruthy();
82
+ });
83
+
84
+ it('should use smaller padding for sm than md (tag variant)', () => {
85
+ const { rerender } = renderWithProvider(
86
+ <BaseTag {...baseProps} size='md' />,
87
+ );
88
+ const mdPadding =
89
+ screen.getByTestId('base-tag').props.style.paddingHorizontal;
90
+
91
+ rerender(
92
+ <ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
93
+ <BaseTag {...baseProps} size='sm' />
94
+ </ThemeProvider>,
95
+ );
96
+ const smPadding =
97
+ screen.getByTestId('base-tag').props.style.paddingHorizontal;
98
+
99
+ expect(smPadding).toBeLessThan(mdPadding);
100
+ });
101
+ });
102
+
103
+ describe('Variants', () => {
104
+ it('should use paddingHorizontal for tag variant', () => {
105
+ renderWithProvider(<BaseTag {...baseProps} variant='tag' />);
106
+ const style = screen.getByTestId('base-tag').props.style;
107
+ expect(style.paddingHorizontal).toBeDefined();
108
+ expect(style.paddingLeft).toBeUndefined();
109
+ expect(style.paddingRight).toBeUndefined();
110
+ });
111
+
112
+ it('should use asymmetric paddingLeft / paddingRight for media variant', () => {
113
+ renderWithProvider(<BaseTag {...baseProps} variant='media' />);
114
+ const style = screen.getByTestId('base-tag').props.style;
115
+ expect(style.paddingLeft).toBeDefined();
116
+ expect(style.paddingRight).toBeDefined();
117
+ expect(style.paddingHorizontal).toBeUndefined();
118
+ });
119
+ });
120
+
121
+ describe('Disabled', () => {
122
+ it('should apply disabled background when disabled', () => {
123
+ renderWithProvider(<BaseTag {...baseProps} disabled />);
124
+ expect(screen.getByTestId('base-tag').props.style.backgroundColor).toBe(
125
+ colors.bg.disabled,
126
+ );
127
+ });
128
+ });
129
+
130
+ describe('Styling', () => {
131
+ it('should merge custom style with computed root style', () => {
132
+ const custom: ViewStyle = { marginTop: 16 };
133
+ renderWithProvider(<BaseTag {...baseProps} style={custom} />);
134
+ expect(screen.getByTestId('base-tag').props.style.marginTop).toBe(16);
135
+ });
136
+ });
137
+ });
@@ -0,0 +1,152 @@
1
+ import { useDisabledContext } from '@ledgerhq/lumen-utils-shared';
2
+ import { StyleSheet, Text } from 'react-native';
3
+ import { useStyleSheet } from '../../../styles';
4
+ import type { TagProps } from '../Tag/types';
5
+ import { Box } from '../Utility';
6
+ import type { BaseTagProps, BaseTagVariant } from './types';
7
+
8
+ type Appearance = NonNullable<TagProps['appearance']>;
9
+ type Size = NonNullable<TagProps['size']>;
10
+
11
+ const useBaseTagStyles = ({
12
+ appearance,
13
+ size,
14
+ disabled,
15
+ variant,
16
+ }: {
17
+ appearance: Appearance;
18
+ size: Size;
19
+ disabled: boolean;
20
+ variant: BaseTagVariant;
21
+ }) => {
22
+ return useStyleSheet(
23
+ (t) => {
24
+ const bgColors: Record<Appearance, string> = {
25
+ base: t.colors.bg.mutedTransparent,
26
+ gray: t.colors.bg.mutedTransparent,
27
+ accent: t.colors.bg.accent,
28
+ 'accent-subtle': t.colors.bg.activeSubtle,
29
+ success: t.colors.bg.success,
30
+ error: t.colors.bg.error,
31
+ warning: t.colors.bg.warning,
32
+ white: t.colors.bg.white,
33
+ };
34
+
35
+ const textColors: Record<Appearance, string> = {
36
+ base: t.colors.text.base,
37
+ gray: t.colors.text.muted,
38
+ accent: t.colors.text.onAccent,
39
+ 'accent-subtle': t.colors.text.active,
40
+ success: t.colors.text.success,
41
+ error: t.colors.text.error,
42
+ warning: t.colors.text.warning,
43
+ white: t.colors.text.black,
44
+ };
45
+
46
+ const tagPadding = {
47
+ md: {
48
+ paddingHorizontal: t.spacings.s8,
49
+ paddingVertical: t.spacings.s4,
50
+ },
51
+ sm: {
52
+ paddingHorizontal: t.spacings.s4,
53
+ paddingVertical: t.spacings.s2,
54
+ },
55
+ } satisfies Record<Size, object>;
56
+
57
+ const mediaPadding = {
58
+ md: {
59
+ paddingLeft: t.spacings.s4,
60
+ paddingRight: t.spacings.s8,
61
+ paddingVertical: t.spacings.s4,
62
+ },
63
+ sm: {
64
+ paddingLeft: t.spacings.s4,
65
+ paddingRight: t.spacings.s4,
66
+ paddingVertical: t.spacings.s2,
67
+ },
68
+ } satisfies Record<Size, object>;
69
+
70
+ const padding =
71
+ variant === 'media' ? mediaPadding[size] : tagPadding[size];
72
+
73
+ const textTypography =
74
+ size === 'md' ? t.typographies.body3 : t.typographies.body4;
75
+
76
+ return {
77
+ root: StyleSheet.flatten([
78
+ {
79
+ flexDirection: 'row',
80
+ alignItems: 'center',
81
+ justifyContent: 'center',
82
+ gap: t.spacings.s4,
83
+ borderRadius: t.borderRadius.xs,
84
+ backgroundColor: bgColors[appearance],
85
+ ...padding,
86
+ },
87
+ disabled && {
88
+ backgroundColor: t.colors.bg.disabled,
89
+ },
90
+ ]),
91
+ text: StyleSheet.flatten([
92
+ textTypography,
93
+ {
94
+ color: textColors[appearance],
95
+ },
96
+ disabled && {
97
+ color: t.colors.text.disabled,
98
+ },
99
+ ]),
100
+ icon: StyleSheet.flatten([
101
+ {
102
+ flexShrink: 0,
103
+ color: textColors[appearance],
104
+ },
105
+ disabled && {
106
+ color: t.colors.text.disabled,
107
+ },
108
+ ]),
109
+ };
110
+ },
111
+ [appearance, size, disabled, variant],
112
+ );
113
+ };
114
+
115
+ export const BaseTag = ({
116
+ appearance = 'accent',
117
+ size = 'md',
118
+ variant,
119
+ consumerName,
120
+ label,
121
+ renderIcon,
122
+ disabled: disabledProp = false,
123
+ lx = {},
124
+ style,
125
+ ref,
126
+ ...props
127
+ }: BaseTagProps) => {
128
+ const disabled = useDisabledContext({
129
+ consumerName,
130
+ mergeWith: { disabled: disabledProp },
131
+ });
132
+ const styles = useBaseTagStyles({
133
+ appearance,
134
+ size,
135
+ disabled: !!disabled,
136
+ variant,
137
+ });
138
+
139
+ return (
140
+ <Box
141
+ ref={ref}
142
+ lx={lx}
143
+ style={StyleSheet.flatten([styles.root, style])}
144
+ {...props}
145
+ >
146
+ {renderIcon?.(styles.icon)}
147
+ <Text style={styles.text} numberOfLines={1}>
148
+ {label}
149
+ </Text>
150
+ </Box>
151
+ );
152
+ };
@@ -0,0 +1,2 @@
1
+ export * from './BaseTag';
2
+ export * from './types';
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { StyleProp, TextStyle } from 'react-native';
3
+ import type { TagProps } from '../Tag/types';
4
+
5
+ export type BaseTagVariant = 'tag' | 'media';
6
+
7
+ export type BaseTagProps = Omit<TagProps, 'icon'> & {
8
+ variant: BaseTagVariant;
9
+ consumerName: string;
10
+ renderIcon?: (style: StyleProp<TextStyle>) => ReactNode;
11
+ };
@@ -1,8 +1,10 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import type { FC } from 'react';
2
3
  import { useState } from 'react';
3
4
  import { Button } from '../Button';
4
5
  import { SearchInput } from '../SearchInput';
5
- import { Box, Text } from '../Utility';
6
+ import { Spot } from '../Spot';
7
+ import { Box, RadialGradient, Text } from '../Utility';
6
8
  import { BottomSheet } from './BottomSheet';
7
9
  import { BottomSheetHeader } from './BottomSheetHeader';
8
10
  import {
@@ -11,6 +13,7 @@ import {
11
13
  BottomSheetView,
12
14
  BottomSheetVirtualizedList,
13
15
  } from './Scrollables';
16
+ import type { BottomSheetBackgroundProps } from './types';
14
17
  import { useBottomSheetRef } from './useBottomSheetRef';
15
18
 
16
19
  const meta = {
@@ -654,3 +657,173 @@ export const VirtualizedList: Story = {
654
657
  );
655
658
  },
656
659
  };
660
+
661
+ type StrongBackgroundColor = 'errorStrong' | 'successStrong' | 'mutedStrong';
662
+
663
+ const createGradientBackground =
664
+ (color: StrongBackgroundColor): FC<BottomSheetBackgroundProps> =>
665
+ ({ style }) => (
666
+ <Box
667
+ style={style}
668
+ lx={{ backgroundColor: 'canvasSheet', overflow: 'hidden' }}
669
+ >
670
+ <RadialGradient
671
+ center={{ x: 0.5, y: 0 }}
672
+ stops={[
673
+ { color, offset: 0, opacity: 0.3 },
674
+ { color, offset: 1, opacity: 0 },
675
+ ]}
676
+ lx={{
677
+ position: 'absolute',
678
+ top: 's0',
679
+ left: 's0',
680
+ right: 's0',
681
+ height: 's320',
682
+ }}
683
+ />
684
+ </Box>
685
+ );
686
+
687
+ const ErrorBackground = createGradientBackground('errorStrong');
688
+ const SuccessBackground = createGradientBackground('successStrong');
689
+ const MutedBackground = createGradientBackground('mutedStrong');
690
+
691
+ export const InfoStateVariants: Story = {
692
+ args: {
693
+ snapPoints: 'full',
694
+ hideCloseButton: false,
695
+ onBack: undefined,
696
+ onClose: undefined,
697
+ enableHandlePanningGesture: true,
698
+ enablePanDownToClose: true,
699
+ enableBlurKeyboardOnGesture: true,
700
+ enableDynamicSizing: false,
701
+ detached: false,
702
+ backdropPressBehavior: 'close',
703
+ hideHandle: true,
704
+ },
705
+ render: (args) => {
706
+ const errorBottomSheetRef = useBottomSheetRef();
707
+ const successBottomSheetRef = useBottomSheetRef();
708
+ const mutedBottomSheetRef = useBottomSheetRef();
709
+
710
+ return (
711
+ <Box
712
+ lx={{
713
+ height: 's320',
714
+ width: 'full',
715
+ alignItems: 'center',
716
+ justifyContent: 'center',
717
+ paddingTop: 's32',
718
+ gap: 's12',
719
+ }}
720
+ >
721
+ <Button
722
+ size='sm'
723
+ onPress={() => errorBottomSheetRef.current?.present()}
724
+ >
725
+ Error
726
+ </Button>
727
+ <Button
728
+ size='sm'
729
+ onPress={() => successBottomSheetRef.current?.present()}
730
+ >
731
+ Success
732
+ </Button>
733
+ <Button
734
+ size='sm'
735
+ onPress={() => mutedBottomSheetRef.current?.present()}
736
+ >
737
+ Muted
738
+ </Button>
739
+
740
+ <BottomSheet
741
+ {...args}
742
+ ref={errorBottomSheetRef}
743
+ backgroundComponent={ErrorBackground}
744
+ >
745
+ <BottomSheetView>
746
+ <BottomSheetHeader density='compact' />
747
+ <Box lx={{ alignItems: 'center', gap: 's24' }}>
748
+ <Spot appearance='error' size={72} />
749
+ <Box lx={{ alignItems: 'center', gap: 's12' }}>
750
+ <Text typography='heading4SemiBold' lx={{ color: 'base' }}>
751
+ Title
752
+ </Text>
753
+ <Text
754
+ typography='body2'
755
+ lx={{ color: 'muted', textAlign: 'center' }}
756
+ >
757
+ Description
758
+ </Text>
759
+ </Box>
760
+ </Box>
761
+ <Box lx={{ marginTop: 's24' }}>
762
+ <Button appearance='base' size='lg' isFull>
763
+ Label
764
+ </Button>
765
+ </Box>
766
+ </BottomSheetView>
767
+ </BottomSheet>
768
+
769
+ <BottomSheet
770
+ {...args}
771
+ ref={successBottomSheetRef}
772
+ backgroundComponent={SuccessBackground}
773
+ >
774
+ <BottomSheetView>
775
+ <BottomSheetHeader density='compact' />
776
+ <Box lx={{ alignItems: 'center', gap: 's24' }}>
777
+ <Spot appearance='check' size={72} />
778
+ <Box lx={{ alignItems: 'center', gap: 's12' }}>
779
+ <Text typography='heading4SemiBold' lx={{ color: 'base' }}>
780
+ Title
781
+ </Text>
782
+ <Text
783
+ typography='body2'
784
+ lx={{ color: 'muted', textAlign: 'center' }}
785
+ >
786
+ Description
787
+ </Text>
788
+ </Box>
789
+ </Box>
790
+ <Box lx={{ marginTop: 's24' }}>
791
+ <Button appearance='base' size='lg' isFull>
792
+ Label
793
+ </Button>
794
+ </Box>
795
+ </BottomSheetView>
796
+ </BottomSheet>
797
+
798
+ <BottomSheet
799
+ {...args}
800
+ ref={mutedBottomSheetRef}
801
+ backgroundComponent={MutedBackground}
802
+ >
803
+ <BottomSheetView>
804
+ <BottomSheetHeader density='compact' />
805
+ <Box lx={{ alignItems: 'center', gap: 's24' }}>
806
+ <Spot appearance='info' size={72} />
807
+ <Box lx={{ alignItems: 'center', gap: 's12' }}>
808
+ <Text typography='heading4SemiBold' lx={{ color: 'base' }}>
809
+ Title
810
+ </Text>
811
+ <Text
812
+ typography='body2'
813
+ lx={{ color: 'muted', textAlign: 'center' }}
814
+ >
815
+ Description
816
+ </Text>
817
+ </Box>
818
+ </Box>
819
+ <Box lx={{ marginTop: 's24' }}>
820
+ <Button appearance='base' size='lg' isFull>
821
+ Label
822
+ </Button>
823
+ </Box>
824
+ </BottomSheetView>
825
+ </BottomSheet>
826
+ </Box>
827
+ );
828
+ },
829
+ };
@@ -184,6 +184,65 @@ describe('BottomSheet', () => {
184
184
  expect(element.props['data-detached']).toBe('true');
185
185
  });
186
186
 
187
+ it('uses default background style when no backgroundComponent is provided', () => {
188
+ const { BottomSheet } = require('./BottomSheet');
189
+ const { getByTestId } = renderWithTheme(
190
+ <BottomSheet testID='bottom-sheet'>
191
+ <Text>Content</Text>
192
+ </BottomSheet>,
193
+ );
194
+
195
+ const element = getByTestId('bottom-sheet');
196
+ expect(element.props['data-has-background-component']).toBe('false');
197
+ expect(element.props['data-has-background-style']).toBe('true');
198
+ });
199
+
200
+ it('renders the default handle when hideHandle is not set', () => {
201
+ const { BottomSheet } = require('./BottomSheet');
202
+ const { getByTestId, queryByTestId } = renderWithTheme(
203
+ <BottomSheet testID='bottom-sheet'>
204
+ <Text>Content</Text>
205
+ </BottomSheet>,
206
+ );
207
+
208
+ expect(getByTestId('bottom-sheet-handle')).toBeTruthy();
209
+ expect(queryByTestId('bottom-sheet-handle-hidden')).toBeNull();
210
+ });
211
+
212
+ it('renders the hidden handle placeholder when hideHandle is true', () => {
213
+ const { BottomSheet } = require('./BottomSheet');
214
+ const { getByTestId, queryByTestId } = renderWithTheme(
215
+ <BottomSheet hideHandle testID='bottom-sheet'>
216
+ <Text>Content</Text>
217
+ </BottomSheet>,
218
+ );
219
+
220
+ expect(getByTestId('bottom-sheet-handle-hidden')).toBeTruthy();
221
+ expect(queryByTestId('bottom-sheet-handle')).toBeNull();
222
+ });
223
+
224
+ it('forwards backgroundComponent and skips default background style', () => {
225
+ const { BottomSheet } = require('./BottomSheet');
226
+ const CustomBackground = () => (
227
+ <View testID='custom-bg'>
228
+ <Text>BG</Text>
229
+ </View>
230
+ );
231
+ const { getByTestId } = renderWithTheme(
232
+ <BottomSheet
233
+ backgroundComponent={CustomBackground}
234
+ testID='bottom-sheet'
235
+ >
236
+ <Text>Content</Text>
237
+ </BottomSheet>,
238
+ );
239
+
240
+ const element = getByTestId('bottom-sheet');
241
+ expect(element.props['data-has-background-component']).toBe('true');
242
+ expect(element.props['data-has-background-style']).toBe('false');
243
+ expect(getByTestId('custom-bg')).toBeTruthy();
244
+ });
245
+
187
246
  it('respects enableHandlePanningGesture prop', () => {
188
247
  const { BottomSheet } = require('./BottomSheet');
189
248
  const { getByTestId } = renderWithTheme(
@@ -6,7 +6,7 @@ import { StyleSheet } from 'react-native';
6
6
  import { useStyleSheet } from '../../../styles';
7
7
  import { RuntimeConstants } from '../../utils';
8
8
  import { CustomBackdrop } from './CustomBackdrop';
9
- import { CustomHandle } from './CustomHandle';
9
+ import { CustomHandle, HiddenHandle } from './CustomHandle';
10
10
  import type { BottomSheetProps } from './types';
11
11
 
12
12
  const OFFSET_TOP = 25;
@@ -25,7 +25,13 @@ const MAX_DYNAMIC_CONTENT_SIZE = {
25
25
  fullWithOffset: FULL_WITH_OFFSET,
26
26
  };
27
27
 
28
- const useStyles = ({ shadow }: { shadow: boolean }) => {
28
+ const useStyles = ({
29
+ shadow,
30
+ hasCustomBackground,
31
+ }: {
32
+ shadow: boolean;
33
+ hasCustomBackground: boolean;
34
+ }) => {
29
35
  return useStyleSheet(
30
36
  (t) => ({
31
37
  root: StyleSheet.flatten([
@@ -35,7 +41,7 @@ const useStyles = ({ shadow }: { shadow: boolean }) => {
35
41
  flex: 1,
36
42
  borderTopLeftRadius: t.borderRadius.xl,
37
43
  borderTopRightRadius: t.borderRadius.xl,
38
- backgroundColor: t.colors.bg.canvasSheet,
44
+ overflow: 'hidden',
39
45
  },
40
46
  shadow && {
41
47
  boxShadow: t.shadows.lg,
@@ -46,7 +52,7 @@ const useStyles = ({ shadow }: { shadow: boolean }) => {
46
52
  backgroundColor: t.colors.bg.canvasSheet,
47
53
  },
48
54
  }),
49
- [shadow],
55
+ [shadow, hasCustomBackground],
50
56
  );
51
57
  };
52
58
 
@@ -74,6 +80,8 @@ export const BottomSheet = ({
74
80
  onBackdropPress,
75
81
  onChange,
76
82
  snapPoints = 'fullWithOffset',
83
+ backgroundComponent,
84
+ hideHandle = false,
77
85
  ref,
78
86
  ...props
79
87
  }: BottomSheetProps) => {
@@ -82,7 +90,10 @@ export const BottomSheet = ({
82
90
  const mergedRefs = useMergedRef<GorhomBottomSheetModal>(ref, innerRef);
83
91
  const [isOpen, setIsOpen] = useState(false);
84
92
 
85
- const styles = useStyles({ shadow: hideBackdrop && isOpen });
93
+ const styles = useStyles({
94
+ shadow: hideBackdrop && isOpen,
95
+ hasCustomBackground: Boolean(backgroundComponent),
96
+ });
86
97
 
87
98
  /**
88
99
  * Match the snap points to the preset or the custom snap points array
@@ -163,7 +174,8 @@ export const BottomSheet = ({
163
174
  {...props}
164
175
  ref={mergedRefs}
165
176
  style={styles.root}
166
- backgroundStyle={styles.background}
177
+ backgroundStyle={backgroundComponent ? undefined : styles.background}
178
+ backgroundComponent={backgroundComponent}
167
179
  onChange={handleChange}
168
180
  onAnimate={handleAnimate}
169
181
  /**
@@ -188,7 +200,7 @@ export const BottomSheet = ({
188
200
  /**
189
201
  * Components
190
202
  */
191
- handleComponent={CustomHandle}
203
+ handleComponent={hideHandle ? HiddenHandle : CustomHandle}
192
204
  backdropComponent={hideBackdrop ? undefined : renderBackdrop}
193
205
  >
194
206
  <BottomSheetProvider value={{ onBack, hideCloseButton }}>
@@ -28,7 +28,7 @@ const useStyles = ({
28
28
  {
29
29
  position: 'relative',
30
30
  zIndex: Z_INDEX_DIALOG_CONTENT,
31
- backgroundColor: t.colors.bg.canvasSheet,
31
+ backgroundColor: 'transparent',
32
32
  paddingBottom: t.spacings.s12,
33
33
  },
34
34
  spacing && {