@oxyhq/bloom 0.19.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/benefit-list/index.js +119 -0
- package/lib/commonjs/benefit-list/index.js.map +1 -0
- package/lib/commonjs/benefit-list/types.js +6 -0
- package/lib/commonjs/benefit-list/types.js.map +1 -0
- package/lib/commonjs/connection-dots/ConnectionDots.js +112 -0
- package/lib/commonjs/connection-dots/ConnectionDots.js.map +1 -0
- package/lib/commonjs/connection-dots/ConnectionDots.web.js +116 -0
- package/lib/commonjs/connection-dots/ConnectionDots.web.js.map +1 -0
- package/lib/commonjs/connection-dots/ConnectionDotsBase.js +55 -0
- package/lib/commonjs/connection-dots/ConnectionDotsBase.js.map +1 -0
- package/lib/commonjs/connection-dots/index.js +13 -0
- package/lib/commonjs/connection-dots/index.js.map +1 -0
- package/lib/commonjs/connection-dots/index.web.js +13 -0
- package/lib/commonjs/connection-dots/index.web.js.map +1 -0
- package/lib/commonjs/connection-dots/types.js +6 -0
- package/lib/commonjs/connection-dots/types.js.map +1 -0
- package/lib/commonjs/content-panel/index.js +64 -0
- package/lib/commonjs/content-panel/index.js.map +1 -0
- package/lib/commonjs/content-panel/index.web.js +99 -0
- package/lib/commonjs/content-panel/index.web.js.map +1 -0
- package/lib/commonjs/design-tokens/color-roles.js +112 -0
- package/lib/commonjs/design-tokens/color-roles.js.map +1 -0
- package/lib/commonjs/design-tokens/index.js +89 -0
- package/lib/commonjs/design-tokens/index.js.map +1 -0
- package/lib/commonjs/design-tokens/scales.js +168 -0
- package/lib/commonjs/design-tokens/scales.js.map +1 -0
- package/lib/commonjs/design-tokens/shadows.js +48 -0
- package/lib/commonjs/design-tokens/shadows.js.map +1 -0
- package/lib/commonjs/design-tokens/shadows.native.js +53 -0
- package/lib/commonjs/design-tokens/shadows.native.js.map +1 -0
- package/lib/commonjs/design-tokens/tailwind-preset.js +139 -0
- package/lib/commonjs/design-tokens/tailwind-preset.js.map +1 -0
- package/lib/commonjs/design-tokens/theme-css.js +98 -0
- package/lib/commonjs/design-tokens/theme-css.js.map +1 -0
- package/lib/commonjs/index.js +208 -93
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +223 -108
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/text-field/index.js +156 -0
- package/lib/commonjs/text-field/index.js.map +1 -1
- package/lib/module/benefit-list/index.js +114 -0
- package/lib/module/benefit-list/index.js.map +1 -0
- package/lib/module/benefit-list/types.js +4 -0
- package/lib/module/benefit-list/types.js.map +1 -0
- package/lib/module/connection-dots/ConnectionDots.js +107 -0
- package/lib/module/connection-dots/ConnectionDots.js.map +1 -0
- package/lib/module/connection-dots/ConnectionDots.web.js +111 -0
- package/lib/module/connection-dots/ConnectionDots.web.js.map +1 -0
- package/lib/module/connection-dots/ConnectionDotsBase.js +50 -0
- package/lib/module/connection-dots/ConnectionDotsBase.js.map +1 -0
- package/lib/module/connection-dots/index.js +4 -0
- package/lib/module/connection-dots/index.js.map +1 -0
- package/lib/module/connection-dots/index.web.js +4 -0
- package/lib/module/connection-dots/index.web.js.map +1 -0
- package/lib/module/connection-dots/types.js +4 -0
- package/lib/module/connection-dots/types.js.map +1 -0
- package/lib/module/content-panel/index.js +59 -0
- package/lib/module/content-panel/index.js.map +1 -0
- package/lib/module/content-panel/index.web.js +94 -0
- package/lib/module/content-panel/index.web.js.map +1 -0
- package/lib/module/design-tokens/color-roles.js +108 -0
- package/lib/module/design-tokens/color-roles.js.map +1 -0
- package/lib/module/design-tokens/index.js +27 -0
- package/lib/module/design-tokens/index.js.map +1 -0
- package/lib/module/design-tokens/scales.js +164 -0
- package/lib/module/design-tokens/scales.js.map +1 -0
- package/lib/module/design-tokens/shadows.js +43 -0
- package/lib/module/design-tokens/shadows.js.map +1 -0
- package/lib/module/design-tokens/shadows.native.js +48 -0
- package/lib/module/design-tokens/shadows.native.js.map +1 -0
- package/lib/module/design-tokens/tailwind-preset.js +136 -0
- package/lib/module/design-tokens/tailwind-preset.js.map +1 -0
- package/lib/module/design-tokens/theme-css.js +94 -0
- package/lib/module/design-tokens/theme-css.js.map +1 -0
- package/lib/module/index.js +6 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +6 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/text-field/index.js +158 -2
- package/lib/module/text-field/index.js.map +1 -1
- package/lib/typescript/commonjs/benefit-list/index.d.ts +6 -0
- package/lib/typescript/commonjs/benefit-list/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/benefit-list/types.d.ts +35 -0
- package/lib/typescript/commonjs/benefit-list/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/connection-dots/ConnectionDots.d.ts +4 -0
- package/lib/typescript/commonjs/connection-dots/ConnectionDots.d.ts.map +1 -0
- package/lib/typescript/commonjs/connection-dots/ConnectionDots.web.d.ts +14 -0
- package/lib/typescript/commonjs/connection-dots/ConnectionDots.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/connection-dots/ConnectionDotsBase.d.ts +18 -0
- package/lib/typescript/commonjs/connection-dots/ConnectionDotsBase.d.ts.map +1 -0
- package/lib/typescript/commonjs/connection-dots/index.d.ts +3 -0
- package/lib/typescript/commonjs/connection-dots/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/connection-dots/index.web.d.ts +3 -0
- package/lib/typescript/commonjs/connection-dots/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/connection-dots/types.d.ts +41 -0
- package/lib/typescript/commonjs/connection-dots/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/content-panel/index.d.ts +54 -0
- package/lib/typescript/commonjs/content-panel/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/content-panel/index.web.d.ts +59 -0
- package/lib/typescript/commonjs/content-panel/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/design-tokens/color-roles.d.ts +102 -0
- package/lib/typescript/commonjs/design-tokens/color-roles.d.ts.map +1 -0
- package/lib/typescript/commonjs/design-tokens/index.d.ts +28 -0
- package/lib/typescript/commonjs/design-tokens/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/design-tokens/scales.d.ts +152 -0
- package/lib/typescript/commonjs/design-tokens/scales.d.ts.map +1 -0
- package/lib/typescript/commonjs/design-tokens/shadows.d.ts +34 -0
- package/lib/typescript/commonjs/design-tokens/shadows.d.ts.map +1 -0
- package/lib/typescript/commonjs/design-tokens/shadows.native.d.ts +19 -0
- package/lib/typescript/commonjs/design-tokens/shadows.native.d.ts.map +1 -0
- package/lib/typescript/commonjs/design-tokens/tailwind-preset.d.ts +59 -0
- package/lib/typescript/commonjs/design-tokens/tailwind-preset.d.ts.map +1 -0
- package/lib/typescript/commonjs/design-tokens/theme-css.d.ts +39 -0
- package/lib/typescript/commonjs/design-tokens/theme-css.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +6 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +6 -0
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/text-field/index.d.ts +14 -1
- package/lib/typescript/commonjs/text-field/index.d.ts.map +1 -1
- package/lib/typescript/module/benefit-list/index.d.ts +6 -0
- package/lib/typescript/module/benefit-list/index.d.ts.map +1 -0
- package/lib/typescript/module/benefit-list/types.d.ts +35 -0
- package/lib/typescript/module/benefit-list/types.d.ts.map +1 -0
- package/lib/typescript/module/connection-dots/ConnectionDots.d.ts +4 -0
- package/lib/typescript/module/connection-dots/ConnectionDots.d.ts.map +1 -0
- package/lib/typescript/module/connection-dots/ConnectionDots.web.d.ts +14 -0
- package/lib/typescript/module/connection-dots/ConnectionDots.web.d.ts.map +1 -0
- package/lib/typescript/module/connection-dots/ConnectionDotsBase.d.ts +18 -0
- package/lib/typescript/module/connection-dots/ConnectionDotsBase.d.ts.map +1 -0
- package/lib/typescript/module/connection-dots/index.d.ts +3 -0
- package/lib/typescript/module/connection-dots/index.d.ts.map +1 -0
- package/lib/typescript/module/connection-dots/index.web.d.ts +3 -0
- package/lib/typescript/module/connection-dots/index.web.d.ts.map +1 -0
- package/lib/typescript/module/connection-dots/types.d.ts +41 -0
- package/lib/typescript/module/connection-dots/types.d.ts.map +1 -0
- package/lib/typescript/module/content-panel/index.d.ts +54 -0
- package/lib/typescript/module/content-panel/index.d.ts.map +1 -0
- package/lib/typescript/module/content-panel/index.web.d.ts +59 -0
- package/lib/typescript/module/content-panel/index.web.d.ts.map +1 -0
- package/lib/typescript/module/design-tokens/color-roles.d.ts +102 -0
- package/lib/typescript/module/design-tokens/color-roles.d.ts.map +1 -0
- package/lib/typescript/module/design-tokens/index.d.ts +28 -0
- package/lib/typescript/module/design-tokens/index.d.ts.map +1 -0
- package/lib/typescript/module/design-tokens/scales.d.ts +152 -0
- package/lib/typescript/module/design-tokens/scales.d.ts.map +1 -0
- package/lib/typescript/module/design-tokens/shadows.d.ts +34 -0
- package/lib/typescript/module/design-tokens/shadows.d.ts.map +1 -0
- package/lib/typescript/module/design-tokens/shadows.native.d.ts +19 -0
- package/lib/typescript/module/design-tokens/shadows.native.d.ts.map +1 -0
- package/lib/typescript/module/design-tokens/tailwind-preset.d.ts +59 -0
- package/lib/typescript/module/design-tokens/tailwind-preset.d.ts.map +1 -0
- package/lib/typescript/module/design-tokens/theme-css.d.ts +39 -0
- package/lib/typescript/module/design-tokens/theme-css.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +6 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +6 -0
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/text-field/index.d.ts +14 -1
- package/lib/typescript/module/text-field/index.d.ts.map +1 -1
- package/package.json +66 -1
- package/src/__tests__/BenefitList.test.tsx +108 -0
- package/src/__tests__/ConnectionDots.test.tsx +86 -0
- package/src/__tests__/TextFieldFloatingLabel.test.tsx +93 -0
- package/src/__tests__/design-tokens.test.ts +169 -0
- package/src/benefit-list/BenefitList.stories.tsx +50 -0
- package/src/benefit-list/index.tsx +133 -0
- package/src/benefit-list/types.ts +36 -0
- package/src/connection-dots/ConnectionDots.stories.tsx +54 -0
- package/src/connection-dots/ConnectionDots.tsx +139 -0
- package/src/connection-dots/ConnectionDots.web.tsx +122 -0
- package/src/connection-dots/ConnectionDotsBase.tsx +62 -0
- package/src/connection-dots/index.ts +2 -0
- package/src/connection-dots/index.web.ts +2 -0
- package/src/connection-dots/types.ts +41 -0
- package/src/content-panel/index.tsx +87 -0
- package/src/content-panel/index.web.tsx +134 -0
- package/src/design-tokens/color-roles.ts +110 -0
- package/src/design-tokens/index.ts +45 -0
- package/src/design-tokens/scales.ts +134 -0
- package/src/design-tokens/shadows.native.ts +44 -0
- package/src/design-tokens/shadows.ts +42 -0
- package/src/design-tokens/tailwind-preset.ts +139 -0
- package/src/design-tokens/theme-css.ts +99 -0
- package/src/index.ts +33 -0
- package/src/index.web.ts +33 -0
- package/src/text-field/TextField.stories.tsx +46 -0
- package/src/text-field/index.tsx +212 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, View } from 'react-native';
|
|
3
|
+
import { render } from '@testing-library/react-native';
|
|
4
|
+
|
|
5
|
+
import { BloomThemeProvider } from '../theme/BloomThemeProvider';
|
|
6
|
+
import { BenefitList, BenefitRow } from '../benefit-list';
|
|
7
|
+
|
|
8
|
+
function renderWithTheme(ui: React.ReactElement) {
|
|
9
|
+
return render(
|
|
10
|
+
<BloomThemeProvider mode="light" colorPreset="teal">
|
|
11
|
+
{ui}
|
|
12
|
+
</BloomThemeProvider>,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('BenefitRow', () => {
|
|
17
|
+
it('renders the label as caption text', () => {
|
|
18
|
+
const { getByText } = renderWithTheme(
|
|
19
|
+
<BenefitRow icon={<View />} label="Your data stays private" />,
|
|
20
|
+
);
|
|
21
|
+
expect(getByText('Your data stays private')).toBeTruthy();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders children when provided instead of label', () => {
|
|
25
|
+
const { getByText, queryByText } = renderWithTheme(
|
|
26
|
+
<BenefitRow icon={<View />} label="ignored">
|
|
27
|
+
Encrypted end to end
|
|
28
|
+
</BenefitRow>,
|
|
29
|
+
);
|
|
30
|
+
expect(getByText('Encrypted end to end')).toBeTruthy();
|
|
31
|
+
expect(queryByText('ignored')).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('renders the (decorative, a11y-hidden) icon slot', () => {
|
|
35
|
+
const { getByText } = renderWithTheme(
|
|
36
|
+
<BenefitRow icon={<Text>ICON</Text>} label="Row" />,
|
|
37
|
+
);
|
|
38
|
+
// The icon square is aria-hidden (the caption carries the meaning), so the
|
|
39
|
+
// icon node is only reachable with hidden elements included.
|
|
40
|
+
expect(getByText('ICON', { includeHiddenElements: true })).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('derives an accessibility label from string content', () => {
|
|
44
|
+
const { getByLabelText } = renderWithTheme(
|
|
45
|
+
<BenefitRow icon={<View />} label="Revoke anytime" />,
|
|
46
|
+
);
|
|
47
|
+
const node = getByLabelText('Revoke anytime');
|
|
48
|
+
expect(node.props.accessibilityRole).toBe('text');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('uses an explicit accessibilityLabel over the text content', () => {
|
|
52
|
+
const { getByLabelText } = renderWithTheme(
|
|
53
|
+
<BenefitRow
|
|
54
|
+
icon={<View />}
|
|
55
|
+
label="Revoke anytime"
|
|
56
|
+
accessibilityLabel="Custom label"
|
|
57
|
+
/>,
|
|
58
|
+
);
|
|
59
|
+
expect(getByLabelText('Custom label')).toBeTruthy();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('BenefitList', () => {
|
|
64
|
+
it('renders stacked rows', () => {
|
|
65
|
+
const { getByText } = renderWithTheme(
|
|
66
|
+
<BenefitList accessibilityLabel="Benefits">
|
|
67
|
+
<BenefitRow icon={<View />} label="First benefit" />
|
|
68
|
+
<BenefitRow icon={<View />} label="Second benefit" />
|
|
69
|
+
<BenefitRow icon={<View />} label="Third benefit" />
|
|
70
|
+
</BenefitList>,
|
|
71
|
+
);
|
|
72
|
+
expect(getByText('First benefit')).toBeTruthy();
|
|
73
|
+
expect(getByText('Second benefit')).toBeTruthy();
|
|
74
|
+
expect(getByText('Third benefit')).toBeTruthy();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('applies the card accessibility label', () => {
|
|
78
|
+
const { getByLabelText } = renderWithTheme(
|
|
79
|
+
<BenefitList accessibilityLabel="What you're sharing">
|
|
80
|
+
<BenefitRow icon={<View />} label="Profile photo" />
|
|
81
|
+
</BenefitList>,
|
|
82
|
+
);
|
|
83
|
+
expect(getByLabelText("What you're sharing")).toBeTruthy();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('renders a bordered, rounded card surface using the token roles', () => {
|
|
87
|
+
const { getByLabelText } = renderWithTheme(
|
|
88
|
+
<BenefitList accessibilityLabel="card">
|
|
89
|
+
<BenefitRow icon={<View />} label="Row" />
|
|
90
|
+
</BenefitList>,
|
|
91
|
+
);
|
|
92
|
+
const card = getByLabelText('card');
|
|
93
|
+
// The card style is a nested style array; flatten it fully to assert the
|
|
94
|
+
// radius-20 / border-hairline (0.5px) / border-image token values landed.
|
|
95
|
+
const flat: Record<string, unknown> = {};
|
|
96
|
+
const visit = (s: unknown): void => {
|
|
97
|
+
if (Array.isArray(s)) {
|
|
98
|
+
s.forEach(visit);
|
|
99
|
+
} else if (s && typeof s === 'object') {
|
|
100
|
+
Object.assign(flat, s);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
visit(card.props.style);
|
|
104
|
+
expect(flat.borderRadius).toBe(20);
|
|
105
|
+
expect(flat.borderWidth).toBe(0.5);
|
|
106
|
+
expect(flat.borderColor).toBeTruthy();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, View } from 'react-native';
|
|
3
|
+
import { render } from '@testing-library/react-native';
|
|
4
|
+
|
|
5
|
+
import { BloomThemeProvider } from '../theme/BloomThemeProvider';
|
|
6
|
+
import { ConnectionDots } from '../connection-dots';
|
|
7
|
+
|
|
8
|
+
function renderWithTheme(ui: React.ReactElement) {
|
|
9
|
+
return render(
|
|
10
|
+
<BloomThemeProvider mode="light" colorPreset="teal">
|
|
11
|
+
{ui}
|
|
12
|
+
</BloomThemeProvider>,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Count animated dot nodes in the rendered tree. On the native fork each dot is
|
|
18
|
+
* an `Animated.View` (the mock emits a host node of that type); the left/right
|
|
19
|
+
* slots use plain `View`s, so the count equals the number of dots.
|
|
20
|
+
*/
|
|
21
|
+
function countAnimatedDots(json: unknown): number {
|
|
22
|
+
return (JSON.stringify(json).match(/Animated\.View/g) ?? []).length;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('ConnectionDots', () => {
|
|
26
|
+
it('renders the left and right slots', () => {
|
|
27
|
+
const { getByText } = renderWithTheme(
|
|
28
|
+
<ConnectionDots left={<Text>LEFT</Text>} right={<Text>RIGHT</Text>} />,
|
|
29
|
+
);
|
|
30
|
+
expect(getByText('LEFT')).toBeTruthy();
|
|
31
|
+
expect(getByText('RIGHT')).toBeTruthy();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('exposes a single accessibility label for the (decorative) dots', () => {
|
|
35
|
+
const { getByLabelText } = renderWithTheme(
|
|
36
|
+
<ConnectionDots
|
|
37
|
+
left={<View />}
|
|
38
|
+
right={<View />}
|
|
39
|
+
accessibilityLabel="Linking your account"
|
|
40
|
+
/>,
|
|
41
|
+
);
|
|
42
|
+
const node = getByLabelText('Linking your account');
|
|
43
|
+
expect(node.props.accessibilityRole).toBe('image');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('defaults the accessibility label to "Connecting"', () => {
|
|
47
|
+
const { getByLabelText } = renderWithTheme(
|
|
48
|
+
<ConnectionDots left={<View />} right={<View />} />,
|
|
49
|
+
);
|
|
50
|
+
expect(getByLabelText('Connecting')).toBeTruthy();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('renders dotCount dots', () => {
|
|
54
|
+
const { toJSON } = renderWithTheme(
|
|
55
|
+
<ConnectionDots left={<View />} right={<View />} dotCount={8} />,
|
|
56
|
+
);
|
|
57
|
+
expect(countAnimatedDots(toJSON())).toBe(8);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('renders the default of 6 dots when dotCount is omitted', () => {
|
|
61
|
+
const { toJSON } = renderWithTheme(
|
|
62
|
+
<ConnectionDots left={<View />} right={<View />} />,
|
|
63
|
+
);
|
|
64
|
+
expect(countAnimatedDots(toJSON())).toBe(6);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('renders dots under reduced motion without crashing', () => {
|
|
68
|
+
const { toJSON, getByLabelText } = renderWithTheme(
|
|
69
|
+
<ConnectionDots
|
|
70
|
+
left={<View />}
|
|
71
|
+
right={<View />}
|
|
72
|
+
dotCount={5}
|
|
73
|
+
reducedMotion
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
expect(getByLabelText('Connecting')).toBeTruthy();
|
|
77
|
+
expect(countAnimatedDots(toJSON())).toBe(5);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('clamps a zero/negative dotCount to at least one dot', () => {
|
|
81
|
+
const { toJSON } = renderWithTheme(
|
|
82
|
+
<ConnectionDots left={<View />} right={<View />} dotCount={0} />,
|
|
83
|
+
);
|
|
84
|
+
expect(countAnimatedDots(toJSON())).toBe(1);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react-native';
|
|
3
|
+
|
|
4
|
+
import { BloomThemeProvider } from '../theme/BloomThemeProvider';
|
|
5
|
+
import { TextField, TextFieldInput } from '../text-field';
|
|
6
|
+
|
|
7
|
+
function renderWithTheme(ui: React.ReactElement) {
|
|
8
|
+
return render(
|
|
9
|
+
<BloomThemeProvider mode="light" colorPreset="teal">
|
|
10
|
+
{ui}
|
|
11
|
+
</BloomThemeProvider>,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('TextFieldInput floatingLabel', () => {
|
|
16
|
+
it('renders the floating label text inside the field', () => {
|
|
17
|
+
const { toJSON } = renderWithTheme(
|
|
18
|
+
<TextField>
|
|
19
|
+
<TextFieldInput floatingLabel label="Email" value="" onChangeText={() => {}} />
|
|
20
|
+
</TextField>,
|
|
21
|
+
);
|
|
22
|
+
// The visual label renders in an animated text node; assert it lands in the
|
|
23
|
+
// tree (it's aria-hidden because the input carries the accessibilityLabel).
|
|
24
|
+
expect(JSON.stringify(toJSON())).toContain('Email');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('drives accessibilityLabel from the label', () => {
|
|
28
|
+
const { getByLabelText } = renderWithTheme(
|
|
29
|
+
<TextField>
|
|
30
|
+
<TextFieldInput floatingLabel label="Email" value="" onChangeText={() => {}} />
|
|
31
|
+
</TextField>,
|
|
32
|
+
);
|
|
33
|
+
expect(getByLabelText('Email')).toBeTruthy();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('suppresses the native placeholder (the floating label replaces it)', () => {
|
|
37
|
+
const { getByLabelText } = renderWithTheme(
|
|
38
|
+
<TextField>
|
|
39
|
+
<TextFieldInput floatingLabel label="Email" value="" onChangeText={() => {}} />
|
|
40
|
+
</TextField>,
|
|
41
|
+
);
|
|
42
|
+
const input = getByLabelText('Email');
|
|
43
|
+
expect(input.props.placeholder).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('self-wraps in a TextField when rendered without a root', () => {
|
|
47
|
+
const { getByLabelText, toJSON } = renderWithTheme(
|
|
48
|
+
<TextFieldInput floatingLabel label="Standalone" value="" onChangeText={() => {}} />,
|
|
49
|
+
);
|
|
50
|
+
expect(getByLabelText('Standalone')).toBeTruthy();
|
|
51
|
+
expect(JSON.stringify(toJSON())).toContain('Standalone');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('reflects a present value on the input', () => {
|
|
55
|
+
const { getByLabelText } = renderWithTheme(
|
|
56
|
+
<TextField>
|
|
57
|
+
<TextFieldInput
|
|
58
|
+
floatingLabel
|
|
59
|
+
label="Email"
|
|
60
|
+
value="nate@oxy.so"
|
|
61
|
+
onChangeText={() => {}}
|
|
62
|
+
/>
|
|
63
|
+
</TextField>,
|
|
64
|
+
);
|
|
65
|
+
expect(getByLabelText('Email').props.value).toBe('nate@oxy.so');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('TextFieldInput default (non-floating) is unchanged', () => {
|
|
70
|
+
it('falls back to the label as the placeholder when none is given', () => {
|
|
71
|
+
const { getByLabelText } = renderWithTheme(
|
|
72
|
+
<TextField>
|
|
73
|
+
<TextFieldInput label="Username" value="" onChangeText={() => {}} />
|
|
74
|
+
</TextField>,
|
|
75
|
+
);
|
|
76
|
+
// Default variant keeps the existing behavior: placeholder defaults to label.
|
|
77
|
+
expect(getByLabelText('Username').props.placeholder).toBe('Username');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('honors an explicit placeholder', () => {
|
|
81
|
+
const { getByLabelText } = renderWithTheme(
|
|
82
|
+
<TextField>
|
|
83
|
+
<TextFieldInput
|
|
84
|
+
label="Username"
|
|
85
|
+
placeholder="oxylander"
|
|
86
|
+
value=""
|
|
87
|
+
onChangeText={() => {}}
|
|
88
|
+
/>
|
|
89
|
+
</TextField>,
|
|
90
|
+
);
|
|
91
|
+
expect(getByLabelText('Username').props.placeholder).toBe('oxylander');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bloomTailwindPreset,
|
|
3
|
+
bloomThemeCss,
|
|
4
|
+
bloomThemeBlock,
|
|
5
|
+
FILL_ROLES,
|
|
6
|
+
TEXT_ROLES,
|
|
7
|
+
BORDER_ROLES,
|
|
8
|
+
SPACING,
|
|
9
|
+
RADIUS,
|
|
10
|
+
BORDER_WIDTH,
|
|
11
|
+
TYPOGRAPHY,
|
|
12
|
+
FONT_FAMILY_VARS,
|
|
13
|
+
SHADOW_BOX,
|
|
14
|
+
bloomShadowStyle,
|
|
15
|
+
} from '../design-tokens';
|
|
16
|
+
import { CANONICAL_TOKENS } from '../theme/token-registry';
|
|
17
|
+
|
|
18
|
+
describe('design-tokens color roles', () => {
|
|
19
|
+
const allRoles = { ...FILL_ROLES, ...TEXT_ROLES, ...BORDER_ROLES };
|
|
20
|
+
|
|
21
|
+
it('every color role is a var(--canonical) reference (no hardcoded color)', () => {
|
|
22
|
+
for (const [role, value] of Object.entries(allRoles)) {
|
|
23
|
+
expect(value).toMatch(/^var\(--[a-z-]+\)$/);
|
|
24
|
+
// Never wrap a base token in hsl() — bloom base tokens are full rgb colors.
|
|
25
|
+
expect(value).not.toContain('hsl(');
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('every color role aliases an existing canonical theme token', () => {
|
|
30
|
+
for (const value of Object.values(allRoles)) {
|
|
31
|
+
const token = value.replace(/^var\(--/, '').replace(/\)$/, '');
|
|
32
|
+
expect(CANONICAL_TOKENS).toContain(token as never);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('design-tokens numeric scales', () => {
|
|
38
|
+
it('exposes the spec spacing scale + screen-margin', () => {
|
|
39
|
+
expect(SPACING).toMatchObject({
|
|
40
|
+
'space-2': 2,
|
|
41
|
+
'space-4': 4,
|
|
42
|
+
'space-8': 8,
|
|
43
|
+
'space-12': 12,
|
|
44
|
+
'space-16': 16,
|
|
45
|
+
'space-20': 20,
|
|
46
|
+
'space-24': 24,
|
|
47
|
+
'space-32': 32,
|
|
48
|
+
'screen-margin': 20,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('exposes the spec radius scale', () => {
|
|
53
|
+
expect(RADIUS).toMatchObject({
|
|
54
|
+
'radius-8': 8,
|
|
55
|
+
'radius-12': 12,
|
|
56
|
+
'radius-20': 20,
|
|
57
|
+
'radius-28': 28,
|
|
58
|
+
'radius-max': 9999,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('exposes the 0.5px hairline border width', () => {
|
|
63
|
+
expect(BORDER_WIDTH.hairline).toBe(0.5);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('design-tokens typography', () => {
|
|
68
|
+
it('covers every spec type role', () => {
|
|
69
|
+
const names = Object.keys(TYPOGRAPHY).sort();
|
|
70
|
+
expect(names).toEqual(
|
|
71
|
+
[
|
|
72
|
+
'body',
|
|
73
|
+
'bodySmall',
|
|
74
|
+
'bodyTitleSmall',
|
|
75
|
+
'buttonLarge',
|
|
76
|
+
'caption',
|
|
77
|
+
'headerBold',
|
|
78
|
+
'sectionTitle',
|
|
79
|
+
'subtitle',
|
|
80
|
+
].sort(),
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('each role has a valid size/lineHeight/weight/family', () => {
|
|
85
|
+
for (const role of Object.values(TYPOGRAPHY)) {
|
|
86
|
+
expect(role.size).toBeGreaterThan(0);
|
|
87
|
+
expect(role.lineHeight).toBeGreaterThanOrEqual(role.size);
|
|
88
|
+
expect(['400', '500', '600', '700']).toContain(role.weight);
|
|
89
|
+
expect(FONT_FAMILY_VARS[role.family]).toBeDefined();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('design-tokens Tailwind preset', () => {
|
|
95
|
+
const ext = bloomTailwindPreset.theme.extend;
|
|
96
|
+
|
|
97
|
+
it('only extends theme (additive, no top-level theme override)', () => {
|
|
98
|
+
expect(Object.keys(bloomTailwindPreset)).toEqual(['theme']);
|
|
99
|
+
expect(Object.keys(bloomTailwindPreset.theme)).toEqual(['extend']);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('color roles reach background/text/border buckets', () => {
|
|
103
|
+
expect(ext.backgroundColor['fill']).toBe('var(--card)');
|
|
104
|
+
expect(ext.textColor['text-tertiary']).toBe('var(--muted-foreground)');
|
|
105
|
+
expect(ext.borderColor['border-input-active']).toBe('var(--ring)');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('spacing/radius are emitted in px', () => {
|
|
109
|
+
expect(ext.spacing['space-8']).toBe('8px');
|
|
110
|
+
expect(ext.spacing['screen-margin']).toBe('20px');
|
|
111
|
+
expect(ext.borderRadius['radius-20']).toBe('20px');
|
|
112
|
+
expect(ext.borderWidth['hairline']).toBe('0.5px');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('fontSize entries are [size, { lineHeight }] tuples', () => {
|
|
116
|
+
expect(ext.fontSize['body']).toEqual(['15px', { lineHeight: '22px' }]);
|
|
117
|
+
expect(ext.fontSize['headerBold']).toEqual(['28px', { lineHeight: '34px' }]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('fontFamily entries are [family, { fontWeight }] tuples', () => {
|
|
121
|
+
expect(ext.fontFamily['body']).toEqual([
|
|
122
|
+
'var(--bloom-font-sans)',
|
|
123
|
+
{ fontWeight: '400' },
|
|
124
|
+
]);
|
|
125
|
+
expect(ext.fontFamily['headerBold']).toEqual([
|
|
126
|
+
'var(--bloom-font-display)',
|
|
127
|
+
{ fontWeight: '700' },
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('registers shadow-s and shadow-m box shadows', () => {
|
|
132
|
+
expect(ext.boxShadow['s']).toBe(SHADOW_BOX.s);
|
|
133
|
+
expect(ext.boxShadow['m']).toBe(SHADOW_BOX.m);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('is frozen so consumer config merges cannot mutate it', () => {
|
|
137
|
+
expect(Object.isFrozen(bloomTailwindPreset)).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('design-tokens v4 @theme css', () => {
|
|
142
|
+
const css = bloomThemeCss();
|
|
143
|
+
|
|
144
|
+
it('emits --color-<role> aliases as direct var(--x) (never hsl-wrapped)', () => {
|
|
145
|
+
expect(css).toContain('--color-fill: var(--card);');
|
|
146
|
+
expect(css).toContain('--color-text-tertiary: var(--muted-foreground);');
|
|
147
|
+
expect(css).not.toContain('hsl(var(');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('emits spacing, radius, typography and shadow vars', () => {
|
|
151
|
+
expect(css).toContain('--spacing-space-8: 8px;');
|
|
152
|
+
expect(css).toContain('--radius-radius-20: 20px;');
|
|
153
|
+
expect(css).toContain('--text-body: 15px;');
|
|
154
|
+
expect(css).toContain('--text-body--line-height: 22px;');
|
|
155
|
+
expect(css).toContain('--font-body: var(--bloom-font-sans);');
|
|
156
|
+
expect(css).toContain('--shadow-s:');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('wraps the body in a @theme block', () => {
|
|
160
|
+
expect(bloomThemeBlock()).toMatch(/^@theme \{[\s\S]*\}$/);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('design-tokens shadow style (web default)', () => {
|
|
165
|
+
it('returns a boxShadow style on the web/default fork', () => {
|
|
166
|
+
const style = bloomShadowStyle('m');
|
|
167
|
+
expect(style).toHaveProperty('boxShadow', SHADOW_BOX.m);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
|
+
|
|
5
|
+
import { BenefitList, BenefitRow } from './index';
|
|
6
|
+
import { Person_Stroke2_Corner0_Rounded as PersonIcon } from '../icons/Person';
|
|
7
|
+
import { Lock_Stroke2_Corner0_Rounded as LockIcon } from '../icons/Lock';
|
|
8
|
+
import { Shield_Stroke2_Corner0_Rounded as ShieldIcon } from '../icons/Shield';
|
|
9
|
+
|
|
10
|
+
const meta: Meta<typeof BenefitList> = {
|
|
11
|
+
title: 'Components/BenefitList',
|
|
12
|
+
component: BenefitList,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
|
|
17
|
+
type Story = StoryObj<typeof BenefitList>;
|
|
18
|
+
|
|
19
|
+
export const Consent: Story = {
|
|
20
|
+
render: () => (
|
|
21
|
+
<View style={{ width: 360, padding: 24 }}>
|
|
22
|
+
<BenefitList accessibilityLabel="What you're sharing">
|
|
23
|
+
<BenefitRow
|
|
24
|
+
icon={<PersonIcon size="sm" />}
|
|
25
|
+
label="Share your name and profile photo"
|
|
26
|
+
/>
|
|
27
|
+
<BenefitRow
|
|
28
|
+
icon={<LockIcon size="sm" />}
|
|
29
|
+
label="Your password is never shared with this app"
|
|
30
|
+
/>
|
|
31
|
+
<BenefitRow
|
|
32
|
+
icon={<ShieldIcon size="sm" />}
|
|
33
|
+
label="You can revoke access at any time from settings"
|
|
34
|
+
/>
|
|
35
|
+
</BenefitList>
|
|
36
|
+
</View>
|
|
37
|
+
),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const SingleRow: Story = {
|
|
41
|
+
render: () => (
|
|
42
|
+
<View style={{ width: 360, padding: 24 }}>
|
|
43
|
+
<BenefitList>
|
|
44
|
+
<BenefitRow icon={<ShieldIcon size="sm" />}>
|
|
45
|
+
End-to-end encrypted by default
|
|
46
|
+
</BenefitRow>
|
|
47
|
+
</BenefitList>
|
|
48
|
+
</View>
|
|
49
|
+
),
|
|
50
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React, { memo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
type StyleProp,
|
|
5
|
+
type TextStyle,
|
|
6
|
+
type ViewStyle,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
|
|
9
|
+
import { useTheme } from '../theme/use-theme';
|
|
10
|
+
import { atoms as a } from '../styles';
|
|
11
|
+
import { bloomShadowStyle } from '../design-tokens/shadows';
|
|
12
|
+
import { TYPOGRAPHY, RADIUS, SPACING, BORDER_WIDTH } from '../design-tokens/scales';
|
|
13
|
+
import { Text } from '../typography';
|
|
14
|
+
import type { BenefitRowProps, BenefitListProps } from './types';
|
|
15
|
+
|
|
16
|
+
/** Side of the rounded `fill-secondary` icon square. */
|
|
17
|
+
const ICON_SQUARE_SIZE = 36;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `BenefitRow` — an icon in a rounded `fill-secondary` square paired with a
|
|
21
|
+
* `text-secondary` caption (the `caption` type role). The building block of
|
|
22
|
+
* {@link BenefitList} (the "icon + caption row" of the consent reference), but
|
|
23
|
+
* usable standalone.
|
|
24
|
+
*
|
|
25
|
+
* Token roles → runtime theme colors:
|
|
26
|
+
* - icon square bg → `fill-secondary` (`--muted`) → `theme.colors.backgroundTertiary`
|
|
27
|
+
* - caption color → `text-secondary` (`--muted-foreground`) → `theme.colors.textSecondary`
|
|
28
|
+
* - caption type → `caption` role (size 11 / line-height 14)
|
|
29
|
+
*/
|
|
30
|
+
const BenefitRowComponent: React.FC<BenefitRowProps> = ({
|
|
31
|
+
icon,
|
|
32
|
+
label,
|
|
33
|
+
children,
|
|
34
|
+
accessibilityLabel,
|
|
35
|
+
className,
|
|
36
|
+
style,
|
|
37
|
+
textStyle,
|
|
38
|
+
}) => {
|
|
39
|
+
const theme = useTheme();
|
|
40
|
+
const content = children ?? label;
|
|
41
|
+
const a11y =
|
|
42
|
+
accessibilityLabel ?? (typeof content === 'string' ? content : undefined);
|
|
43
|
+
|
|
44
|
+
const rowStyle: StyleProp<ViewStyle> = [
|
|
45
|
+
a.flex_row,
|
|
46
|
+
a.align_center,
|
|
47
|
+
{ gap: SPACING['space-12'] },
|
|
48
|
+
style,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const squareStyle: ViewStyle = {
|
|
52
|
+
width: ICON_SQUARE_SIZE,
|
|
53
|
+
height: ICON_SQUARE_SIZE,
|
|
54
|
+
borderRadius: RADIUS['radius-12'],
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
justifyContent: 'center',
|
|
57
|
+
backgroundColor: theme.colors.backgroundTertiary,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const captionStyle: StyleProp<TextStyle> = [
|
|
61
|
+
{
|
|
62
|
+
fontSize: TYPOGRAPHY.caption.size,
|
|
63
|
+
lineHeight: TYPOGRAPHY.caption.lineHeight,
|
|
64
|
+
fontWeight: TYPOGRAPHY.caption.weight,
|
|
65
|
+
color: theme.colors.textSecondary,
|
|
66
|
+
},
|
|
67
|
+
a.flex_1,
|
|
68
|
+
textStyle,
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<View
|
|
73
|
+
{...(className ? ({ className } as Record<string, string>) : {})}
|
|
74
|
+
style={rowStyle}
|
|
75
|
+
accessibilityLabel={a11y}
|
|
76
|
+
{...(a11y ? { accessibilityRole: 'text' as const } : {})}>
|
|
77
|
+
<View style={squareStyle} accessibilityElementsHidden aria-hidden>
|
|
78
|
+
{icon}
|
|
79
|
+
</View>
|
|
80
|
+
<Text style={captionStyle}>{content}</Text>
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const BenefitRow = memo(BenefitRowComponent);
|
|
86
|
+
BenefitRow.displayName = 'BenefitRow';
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* `BenefitList` — a bordered card that stacks {@link BenefitRow}s with consistent
|
|
90
|
+
* spacing. The "3 icon + caption rows in a card" from the consent reference.
|
|
91
|
+
*
|
|
92
|
+
* Token roles → runtime values:
|
|
93
|
+
* - card bg → `fill` role (`--card`) → `theme.colors.card`
|
|
94
|
+
* - hairline → `border-image` role (`--border`) → `theme.colors.border` at
|
|
95
|
+
* the `border-hairline` (0.5px) width
|
|
96
|
+
* - radius → `radius-20`
|
|
97
|
+
* - elevation → `shadow-s` (platform-correct via `bloomShadowStyle`)
|
|
98
|
+
*/
|
|
99
|
+
const BenefitListComponent: React.FC<BenefitListProps> = ({
|
|
100
|
+
children,
|
|
101
|
+
accessibilityLabel,
|
|
102
|
+
className,
|
|
103
|
+
style,
|
|
104
|
+
}) => {
|
|
105
|
+
const theme = useTheme();
|
|
106
|
+
|
|
107
|
+
const cardStyle: StyleProp<ViewStyle> = [
|
|
108
|
+
{
|
|
109
|
+
backgroundColor: theme.colors.card,
|
|
110
|
+
borderRadius: RADIUS['radius-20'],
|
|
111
|
+
borderWidth: BORDER_WIDTH.hairline,
|
|
112
|
+
borderColor: theme.colors.border,
|
|
113
|
+
padding: SPACING['space-16'],
|
|
114
|
+
gap: SPACING['space-16'],
|
|
115
|
+
},
|
|
116
|
+
bloomShadowStyle('s'),
|
|
117
|
+
style,
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<View
|
|
122
|
+
{...(className ? ({ className } as Record<string, string>) : {})}
|
|
123
|
+
style={cardStyle}
|
|
124
|
+
accessibilityLabel={accessibilityLabel}>
|
|
125
|
+
{children}
|
|
126
|
+
</View>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const BenefitList = memo(BenefitListComponent);
|
|
131
|
+
BenefitList.displayName = 'BenefitList';
|
|
132
|
+
|
|
133
|
+
export type { BenefitRowProps, BenefitListProps } from './types';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface BenefitRowProps {
|
|
5
|
+
/**
|
|
6
|
+
* Leading icon — rendered inside a rounded `fill-secondary` square. Pass a
|
|
7
|
+
* Bloom icon element (e.g. `<Icons.Lock size="sm" />`) or any node.
|
|
8
|
+
*/
|
|
9
|
+
icon: ReactNode;
|
|
10
|
+
/**
|
|
11
|
+
* Caption text. Provide either `label` or `children` (string or nodes).
|
|
12
|
+
* Rendered as `text-secondary` at the `caption` type role.
|
|
13
|
+
*/
|
|
14
|
+
label?: string;
|
|
15
|
+
/** Caption content as children (alternative to `label`). */
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
/** Accessibility label for the row (defaults to the text content). */
|
|
18
|
+
accessibilityLabel?: string;
|
|
19
|
+
/** NativeWind className passthrough on the row container. */
|
|
20
|
+
className?: string;
|
|
21
|
+
/** Style passthrough on the row container. */
|
|
22
|
+
style?: StyleProp<ViewStyle>;
|
|
23
|
+
/** Style passthrough on the caption text. */
|
|
24
|
+
textStyle?: StyleProp<TextStyle>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BenefitListProps {
|
|
28
|
+
/** `BenefitRow`s (or any nodes) stacked with consistent spacing. */
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
/** Accessibility label for the card. */
|
|
31
|
+
accessibilityLabel?: string;
|
|
32
|
+
/** NativeWind className passthrough on the card container. */
|
|
33
|
+
className?: string;
|
|
34
|
+
/** Style passthrough on the card container. */
|
|
35
|
+
style?: StyleProp<ViewStyle>;
|
|
36
|
+
}
|