@oxyhq/bloom 0.19.0 → 0.20.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/button/Button.web.js +1 -1
- 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/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/button/Button.web.js +1 -1
- 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/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/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/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 +50 -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/button/Button.web.tsx +1 -1
- 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/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,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
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
|
+
|
|
5
|
+
import { ConnectionDots } from './index';
|
|
6
|
+
import { IconCircle } from '../icon-circle';
|
|
7
|
+
import { Person_Stroke2_Corner0_Rounded as PersonIcon } from '../icons/Person';
|
|
8
|
+
import { Lock_Stroke2_Corner0_Rounded as LockIcon } from '../icons/Lock';
|
|
9
|
+
|
|
10
|
+
const meta: Meta<typeof ConnectionDots> = {
|
|
11
|
+
title: 'Components/ConnectionDots',
|
|
12
|
+
component: ConnectionDots,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
|
|
17
|
+
type Story = StoryObj<typeof ConnectionDots>;
|
|
18
|
+
|
|
19
|
+
export const Basic: Story = {
|
|
20
|
+
render: () => (
|
|
21
|
+
<View style={{ padding: 24 }}>
|
|
22
|
+
<ConnectionDots
|
|
23
|
+
left={<IconCircle icon={PersonIcon} size="lg" />}
|
|
24
|
+
right={<IconCircle icon={LockIcon} size="lg" />}
|
|
25
|
+
accessibilityLabel="Connecting your account"
|
|
26
|
+
/>
|
|
27
|
+
</View>
|
|
28
|
+
),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const ManyDots: Story = {
|
|
32
|
+
render: () => (
|
|
33
|
+
<View style={{ padding: 24 }}>
|
|
34
|
+
<ConnectionDots
|
|
35
|
+
left={<IconCircle icon={PersonIcon} size="lg" />}
|
|
36
|
+
right={<IconCircle icon={LockIcon} size="lg" />}
|
|
37
|
+
dotCount={10}
|
|
38
|
+
dotSize={5}
|
|
39
|
+
/>
|
|
40
|
+
</View>
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const ReducedMotion: Story = {
|
|
45
|
+
render: () => (
|
|
46
|
+
<View style={{ padding: 24 }}>
|
|
47
|
+
<ConnectionDots
|
|
48
|
+
left={<IconCircle icon={PersonIcon} size="lg" />}
|
|
49
|
+
right={<IconCircle icon={LockIcon} size="lg" />}
|
|
50
|
+
reducedMotion
|
|
51
|
+
/>
|
|
52
|
+
</View>
|
|
53
|
+
),
|
|
54
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React, { memo, useEffect } from 'react';
|
|
2
|
+
import Animated, {
|
|
3
|
+
Easing,
|
|
4
|
+
interpolate,
|
|
5
|
+
type SharedValue,
|
|
6
|
+
useAnimatedStyle,
|
|
7
|
+
useDerivedValue,
|
|
8
|
+
useReducedMotion,
|
|
9
|
+
useSharedValue,
|
|
10
|
+
withRepeat,
|
|
11
|
+
withTiming,
|
|
12
|
+
} from 'react-native-reanimated';
|
|
13
|
+
|
|
14
|
+
import { useTheme } from '../theme/use-theme';
|
|
15
|
+
import { ConnectionDotsBase } from './ConnectionDotsBase';
|
|
16
|
+
import type { ConnectionDotsProps } from './types';
|
|
17
|
+
|
|
18
|
+
const DEFAULT_DOT_COUNT = 6;
|
|
19
|
+
const DEFAULT_DOT_SIZE = 6;
|
|
20
|
+
const WAVE_DURATION = 1400;
|
|
21
|
+
/** Opacity floor of a dot at the trough of the wave. */
|
|
22
|
+
const DOT_DIM = 0.25;
|
|
23
|
+
|
|
24
|
+
interface ShimmerDotProps {
|
|
25
|
+
/** 0..1 phase offset of this dot within the wave. */
|
|
26
|
+
phase: number;
|
|
27
|
+
color: string;
|
|
28
|
+
size: number;
|
|
29
|
+
/** Shared 0..1 driver advanced once per loop for the whole row. */
|
|
30
|
+
driver: SharedValue<number>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One dot. Its opacity is the shared driver phase-shifted by `phase`, mapped
|
|
35
|
+
* through a triangle wave so the bright crest sweeps left→right across the row
|
|
36
|
+
* (the "ellipsis shimmer").
|
|
37
|
+
*/
|
|
38
|
+
const ShimmerDot: React.FC<ShimmerDotProps> = ({ phase, color, size, driver }) => {
|
|
39
|
+
const opacity = useDerivedValue(() => {
|
|
40
|
+
// wrap into 0..1 then fold into a 0→1→0 triangle so the crest is a moving band
|
|
41
|
+
const t = (driver.value + phase) % 1;
|
|
42
|
+
const wave = 1 - Math.abs(t * 2 - 1);
|
|
43
|
+
return interpolate(wave, [0, 1], [DOT_DIM, 1]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value }));
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Animated.View
|
|
50
|
+
style={[
|
|
51
|
+
{
|
|
52
|
+
width: size,
|
|
53
|
+
height: size,
|
|
54
|
+
borderRadius: size / 2,
|
|
55
|
+
backgroundColor: color,
|
|
56
|
+
},
|
|
57
|
+
animatedStyle,
|
|
58
|
+
]}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Native `ConnectionDots` — a consent/linking header rendering
|
|
65
|
+
* `left · (animated ellipsis dots) · right`. The dots run a looping opacity wave
|
|
66
|
+
* in the brand color (`fill-brand` → `theme.colors.primary`). Reduced-motion
|
|
67
|
+
* (Reanimated's `useReducedMotion`, or the `reducedMotion` prop) renders the
|
|
68
|
+
* dots static at full opacity.
|
|
69
|
+
*/
|
|
70
|
+
const ConnectionDotsComponent: React.FC<ConnectionDotsProps> = ({
|
|
71
|
+
left,
|
|
72
|
+
right,
|
|
73
|
+
dotCount = DEFAULT_DOT_COUNT,
|
|
74
|
+
dotSize = DEFAULT_DOT_SIZE,
|
|
75
|
+
accessibilityLabel,
|
|
76
|
+
className,
|
|
77
|
+
style,
|
|
78
|
+
reducedMotion,
|
|
79
|
+
}) => {
|
|
80
|
+
const theme = useTheme();
|
|
81
|
+
const systemReduceMotion = useReducedMotion();
|
|
82
|
+
const reduce = reducedMotion ?? systemReduceMotion;
|
|
83
|
+
const color = theme.colors.primary;
|
|
84
|
+
|
|
85
|
+
const driver = useSharedValue(0);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (reduce) {
|
|
89
|
+
driver.value = 0;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
driver.value = withRepeat(
|
|
93
|
+
withTiming(1, { duration: WAVE_DURATION, easing: Easing.linear }),
|
|
94
|
+
-1,
|
|
95
|
+
false,
|
|
96
|
+
);
|
|
97
|
+
// driver is a stable shared-value ref; withRepeat/withTiming/Easing are
|
|
98
|
+
// module-level constants from a static import.
|
|
99
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
100
|
+
}, [reduce]);
|
|
101
|
+
|
|
102
|
+
const count = Math.max(1, Math.floor(dotCount));
|
|
103
|
+
|
|
104
|
+
const dots = reduce
|
|
105
|
+
? Array.from({ length: count }, (_, i) => (
|
|
106
|
+
<Animated.View
|
|
107
|
+
key={i}
|
|
108
|
+
style={{
|
|
109
|
+
width: dotSize,
|
|
110
|
+
height: dotSize,
|
|
111
|
+
borderRadius: dotSize / 2,
|
|
112
|
+
backgroundColor: color,
|
|
113
|
+
}}
|
|
114
|
+
/>
|
|
115
|
+
))
|
|
116
|
+
: Array.from({ length: count }, (_, i) => (
|
|
117
|
+
<ShimmerDot
|
|
118
|
+
key={i}
|
|
119
|
+
phase={i / count}
|
|
120
|
+
color={color}
|
|
121
|
+
size={dotSize}
|
|
122
|
+
driver={driver}
|
|
123
|
+
/>
|
|
124
|
+
));
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<ConnectionDotsBase
|
|
128
|
+
left={left}
|
|
129
|
+
right={right}
|
|
130
|
+
dots={dots}
|
|
131
|
+
accessibilityLabel={accessibilityLabel}
|
|
132
|
+
className={className}
|
|
133
|
+
style={style}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const ConnectionDots = memo(ConnectionDotsComponent);
|
|
139
|
+
ConnectionDots.displayName = 'ConnectionDots';
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React, { memo, useEffect, useState } from 'react';
|
|
2
|
+
import { View, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { useTheme } from '../theme/use-theme';
|
|
5
|
+
import { ConnectionDotsBase } from './ConnectionDotsBase';
|
|
6
|
+
import type { ConnectionDotsProps } from './types';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_DOT_COUNT = 6;
|
|
9
|
+
const DEFAULT_DOT_SIZE = 6;
|
|
10
|
+
const WAVE_DURATION_MS = 1400;
|
|
11
|
+
|
|
12
|
+
const KEYFRAMES_ID = 'bloom-connection-dots-keyframes';
|
|
13
|
+
const WAVE_ANIMATION_NAME = 'bloomConnectionDotsWave';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* CSS keyframes powering the ellipsis shimmer. Mirrors the native Reanimated
|
|
17
|
+
* triangle wave: each dot's opacity rises to 1 and falls back to a dim floor,
|
|
18
|
+
* with a per-dot `animation-delay` sweeping the bright crest left→right.
|
|
19
|
+
*
|
|
20
|
+
* A `prefers-reduced-motion: reduce` media query pins every dot to full opacity,
|
|
21
|
+
* matching the native `useReducedMotion` branch — defence-in-depth alongside the
|
|
22
|
+
* JS `matchMedia` gate below (which also stops emitting the `animation` prop).
|
|
23
|
+
*/
|
|
24
|
+
export const BLOOM_CONNECTION_DOTS_CSS = `
|
|
25
|
+
@keyframes ${WAVE_ANIMATION_NAME} { 0%, 100% { opacity: 0.25; } 50% { opacity: 1; } }
|
|
26
|
+
@media (prefers-reduced-motion: reduce) {
|
|
27
|
+
[data-bloom-connection-dot] { animation: none !important; opacity: 1 !important; }
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
function useKeyframes(): void {
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (typeof document === 'undefined') return;
|
|
34
|
+
if (document.getElementById(KEYFRAMES_ID)) return;
|
|
35
|
+
const style = document.createElement('style');
|
|
36
|
+
style.id = KEYFRAMES_ID;
|
|
37
|
+
style.textContent = BLOOM_CONNECTION_DOTS_CSS;
|
|
38
|
+
document.head.appendChild(style);
|
|
39
|
+
}, []);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Read the OS `prefers-reduced-motion` preference reactively. */
|
|
43
|
+
function usePrefersReducedMotion(): boolean {
|
|
44
|
+
const [reduced, setReduced] = useState(false);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (typeof window === 'undefined' || !window.matchMedia) return;
|
|
48
|
+
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
49
|
+
setReduced(mq.matches);
|
|
50
|
+
const onChange = (e: MediaQueryListEvent) => setReduced(e.matches);
|
|
51
|
+
mq.addEventListener('change', onChange);
|
|
52
|
+
return () => mq.removeEventListener('change', onChange);
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
return reduced;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Web fork of `./ConnectionDots`. The native variant runs the opacity wave with
|
|
60
|
+
* `react-native-reanimated`, which cannot ship to a web bundle. This fork drives
|
|
61
|
+
* the same motion with a CSS `@keyframes` animation (injected once into
|
|
62
|
+
* `<head>`) and a per-dot `animation-delay`. Brand color comes from the
|
|
63
|
+
* `fill-brand` role (`theme.colors.primary`). Reduced-motion (the `reducedMotion`
|
|
64
|
+
* prop, the JS `matchMedia` gate, or the CSS media query) renders static dots.
|
|
65
|
+
*/
|
|
66
|
+
const ConnectionDotsComponent: React.FC<ConnectionDotsProps> = ({
|
|
67
|
+
left,
|
|
68
|
+
right,
|
|
69
|
+
dotCount = DEFAULT_DOT_COUNT,
|
|
70
|
+
dotSize = DEFAULT_DOT_SIZE,
|
|
71
|
+
accessibilityLabel,
|
|
72
|
+
className,
|
|
73
|
+
style,
|
|
74
|
+
reducedMotion,
|
|
75
|
+
}) => {
|
|
76
|
+
const theme = useTheme();
|
|
77
|
+
useKeyframes();
|
|
78
|
+
const systemReduceMotion = usePrefersReducedMotion();
|
|
79
|
+
const reduce = reducedMotion ?? systemReduceMotion;
|
|
80
|
+
const color = theme.colors.primary;
|
|
81
|
+
|
|
82
|
+
const count = Math.max(1, Math.floor(dotCount));
|
|
83
|
+
|
|
84
|
+
const dots = Array.from({ length: count }, (_, i) => {
|
|
85
|
+
const base: ViewStyle = {
|
|
86
|
+
width: dotSize,
|
|
87
|
+
height: dotSize,
|
|
88
|
+
borderRadius: dotSize / 2,
|
|
89
|
+
backgroundColor: color,
|
|
90
|
+
};
|
|
91
|
+
const animated: ViewStyle = reduce
|
|
92
|
+
? { opacity: 1 }
|
|
93
|
+
: ({
|
|
94
|
+
// `animation` / `animationDelay` are real CSS props on web;
|
|
95
|
+
// react-native-web passes them through to the DOM node.
|
|
96
|
+
animation: `${WAVE_ANIMATION_NAME} ${WAVE_DURATION_MS}ms linear infinite`,
|
|
97
|
+
animationDelay: `${(i / count) * WAVE_DURATION_MS}ms`,
|
|
98
|
+
} as ViewStyle);
|
|
99
|
+
return (
|
|
100
|
+
<View
|
|
101
|
+
key={i}
|
|
102
|
+
// marker attribute lets the reduced-motion media query target dots
|
|
103
|
+
{...({ 'data-bloom-connection-dot': '' } as Record<string, string>)}
|
|
104
|
+
style={[base, animated]}
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<ConnectionDotsBase
|
|
111
|
+
left={left}
|
|
112
|
+
right={right}
|
|
113
|
+
dots={dots}
|
|
114
|
+
accessibilityLabel={accessibilityLabel}
|
|
115
|
+
className={className}
|
|
116
|
+
style={style}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const ConnectionDots = memo(ConnectionDotsComponent);
|
|
122
|
+
ConnectionDots.displayName = 'ConnectionDots';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { atoms as a, tokens } from '../styles';
|
|
5
|
+
import type { ConnectionDotsProps } from './types';
|
|
6
|
+
|
|
7
|
+
export interface ConnectionDotsBaseProps
|
|
8
|
+
extends Pick<
|
|
9
|
+
ConnectionDotsProps,
|
|
10
|
+
'left' | 'right' | 'accessibilityLabel' | 'className' | 'style'
|
|
11
|
+
> {
|
|
12
|
+
/** The platform-specific animated dots row (built by the native/web fork). */
|
|
13
|
+
dots: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Shared layout + accessibility chrome for {@link ConnectionDots}:
|
|
18
|
+
*
|
|
19
|
+
* leftSlot · (animated ellipsis dots) · rightSlot
|
|
20
|
+
*
|
|
21
|
+
* The dots themselves are supplied by the platform fork (`ConnectionDots.tsx`
|
|
22
|
+
* native / `ConnectionDots.web.tsx` web) because the shimmer animation impl
|
|
23
|
+
* differs (Reanimated vs CSS). This base owns the row layout, the slot wrappers,
|
|
24
|
+
* and the single accessibility label that stands in for the decorative dots.
|
|
25
|
+
*/
|
|
26
|
+
export function ConnectionDotsBase({
|
|
27
|
+
left,
|
|
28
|
+
right,
|
|
29
|
+
dots,
|
|
30
|
+
accessibilityLabel = 'Connecting',
|
|
31
|
+
className,
|
|
32
|
+
style,
|
|
33
|
+
}: ConnectionDotsBaseProps) {
|
|
34
|
+
const rowStyle: StyleProp<ViewStyle> = [
|
|
35
|
+
a.flex_row,
|
|
36
|
+
a.align_center,
|
|
37
|
+
a.justify_center,
|
|
38
|
+
{ gap: tokens.space.md },
|
|
39
|
+
style,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<View
|
|
44
|
+
{...(className ? ({ className } as Record<string, string>) : {})}
|
|
45
|
+
style={rowStyle}>
|
|
46
|
+
<View style={[a.align_center, a.justify_center]}>{left}</View>
|
|
47
|
+
|
|
48
|
+
{/* The dots are decorative; the whole cluster carries ONE a11y label
|
|
49
|
+
(e.g. "Connecting") so screen readers announce the link relationship
|
|
50
|
+
without reading each dot. The left/right slots keep their own a11y. */}
|
|
51
|
+
<View
|
|
52
|
+
accessibilityRole="image"
|
|
53
|
+
accessibilityLabel={accessibilityLabel}
|
|
54
|
+
aria-label={accessibilityLabel}
|
|
55
|
+
style={[a.flex_row, a.align_center, { gap: tokens.space.xs }]}>
|
|
56
|
+
{dots}
|
|
57
|
+
</View>
|
|
58
|
+
|
|
59
|
+
<View style={[a.align_center, a.justify_center]}>{right}</View>
|
|
60
|
+
</View>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface ConnectionDotsProps {
|
|
5
|
+
/**
|
|
6
|
+
* Left-hand content — typically a logo or avatar of the first party in a
|
|
7
|
+
* link/consent flow. Rendered as-is.
|
|
8
|
+
*/
|
|
9
|
+
left: ReactNode;
|
|
10
|
+
/**
|
|
11
|
+
* Right-hand content — typically a logo or avatar of the second party.
|
|
12
|
+
* Rendered as-is.
|
|
13
|
+
*/
|
|
14
|
+
right: ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* Number of ellipsis dots animating between the two slots.
|
|
17
|
+
* @default 6
|
|
18
|
+
*/
|
|
19
|
+
dotCount?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Diameter of each dot in px.
|
|
22
|
+
* @default 6
|
|
23
|
+
*/
|
|
24
|
+
dotSize?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Accessibility label for the whole connection band. Read by screen readers in
|
|
27
|
+
* place of the (decorative) dots.
|
|
28
|
+
* @default 'Connecting'
|
|
29
|
+
*/
|
|
30
|
+
accessibilityLabel?: string;
|
|
31
|
+
/** NativeWind className passthrough on the outer row. */
|
|
32
|
+
className?: string;
|
|
33
|
+
/** Style passthrough on the outer row. */
|
|
34
|
+
style?: StyleProp<ViewStyle>;
|
|
35
|
+
/**
|
|
36
|
+
* Force-disable the shimmer regardless of the OS reduced-motion setting.
|
|
37
|
+
* (When omitted, the component reads the platform reduced-motion preference
|
|
38
|
+
* and renders the dots statically when it is on.)
|
|
39
|
+
*/
|
|
40
|
+
reducedMotion?: boolean;
|
|
41
|
+
}
|