@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,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
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native variant of `ContentPanel` — the framed app-content surface.
|
|
3
|
+
*
|
|
4
|
+
* `ContentPanel` is the rounded, bordered panel that holds an app's routed
|
|
5
|
+
* content. On WEB it adds two sticky overlays (a gutter "bleed-mask" box-shadow
|
|
6
|
+
* ring and a continuous rounded border frame) so feed content that bleeds into
|
|
7
|
+
* the rounded corners / lateral gutters is masked while a single seamless border
|
|
8
|
+
* is drawn around the panel — see `index.web.tsx`.
|
|
9
|
+
*
|
|
10
|
+
* On NATIVE none of that applies: there is no document scroll, no sticky
|
|
11
|
+
* positioning, and no bleed to mask, so the panel is simply a rounded, bordered
|
|
12
|
+
* surface wrapping its content. The `framed` and `showStickyFrame` props are
|
|
13
|
+
* accepted for cross-platform API parity but are intentionally no-ops here
|
|
14
|
+
* (mirroring the no-op native fork of `../scroll`).
|
|
15
|
+
*
|
|
16
|
+
* Web bundlers select `./index.web` via the `"browser"` export condition in
|
|
17
|
+
* `package.json`; native bundlers fall through to this file.
|
|
18
|
+
*
|
|
19
|
+
* Styling is NativeWind-className-first; the literal class strings below must
|
|
20
|
+
* stay literal so a consumer's Tailwind content-scan over `lib/**` (and the
|
|
21
|
+
* native `src/**`) picks them up.
|
|
22
|
+
*/
|
|
23
|
+
import React, { memo } from 'react';
|
|
24
|
+
import { View, type StyleProp, type ViewStyle } from 'react-native';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Width (in px) of the WEB sticky gutter-mask box-shadow ring. Exported for
|
|
28
|
+
* cross-platform API parity (it has no effect on native).
|
|
29
|
+
*/
|
|
30
|
+
export const GUTTER_MASK_SPREAD = 40;
|
|
31
|
+
/** Top sticky inset (px) of the framed panel chrome. */
|
|
32
|
+
export const PANEL_TOP_INSET = 8;
|
|
33
|
+
/** Bottom sticky inset (px) of the framed panel chrome. */
|
|
34
|
+
export const PANEL_BOTTOM_INSET = 8;
|
|
35
|
+
|
|
36
|
+
export interface ContentPanelProps {
|
|
37
|
+
children: React.ReactNode;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the panel is framed (wide screens) or full-bleed (narrow screens).
|
|
40
|
+
* No-op on native — the surface is always a rounded, bordered View.
|
|
41
|
+
*/
|
|
42
|
+
framed: boolean;
|
|
43
|
+
/** Override the surface background utility (defaults to `bg-card`). */
|
|
44
|
+
surfaceClassName?: string;
|
|
45
|
+
surfaceStyle?: StyleProp<ViewStyle>;
|
|
46
|
+
/** Extra utilities for the inner content wrapper. */
|
|
47
|
+
contentClassName?: string;
|
|
48
|
+
contentStyle?: StyleProp<ViewStyle>;
|
|
49
|
+
/**
|
|
50
|
+
* Whether to render the WEB sticky border frame. No-op on native (the border
|
|
51
|
+
* is always part of the surface).
|
|
52
|
+
*/
|
|
53
|
+
showStickyFrame?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
57
|
+
children,
|
|
58
|
+
surfaceClassName,
|
|
59
|
+
surfaceStyle,
|
|
60
|
+
contentClassName,
|
|
61
|
+
contentStyle,
|
|
62
|
+
}) => {
|
|
63
|
+
const surfaceClass = [
|
|
64
|
+
'flex-1',
|
|
65
|
+
'rounded-radius-28',
|
|
66
|
+
'overflow-hidden border border-border',
|
|
67
|
+
surfaceClassName ?? 'bg-card',
|
|
68
|
+
].join(' ');
|
|
69
|
+
const contentClass = ['flex-1', contentClassName].filter(Boolean).join(' ');
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<View
|
|
73
|
+
{...({ className: surfaceClass } as Record<string, string>)}
|
|
74
|
+
style={surfaceStyle}
|
|
75
|
+
>
|
|
76
|
+
<View
|
|
77
|
+
{...({ className: contentClass } as Record<string, string>)}
|
|
78
|
+
style={contentStyle}
|
|
79
|
+
>
|
|
80
|
+
{children}
|
|
81
|
+
</View>
|
|
82
|
+
</View>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const ContentPanel = memo(ContentPanelComponent);
|
|
87
|
+
ContentPanel.displayName = 'ContentPanel';
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web variant of `ContentPanel` — the framed app-content surface.
|
|
3
|
+
*
|
|
4
|
+
* On the web, app content scrolls the DOCUMENT, and a multi-column shell keeps
|
|
5
|
+
* the routed content in a rounded, bordered center panel. Two problems follow
|
|
6
|
+
* from letting feed content bleed to the rounded panel edge, both solved here
|
|
7
|
+
* with sticky, full-viewport-height overlays inside the panel surface:
|
|
8
|
+
*
|
|
9
|
+
* 1. BLEED-MASK overlay (`z-30`): a `box-shadow` ring in the GUTTER color
|
|
10
|
+
* (Bloom `background` token) drawn OVER content that bleeds into the thin
|
|
11
|
+
* lateral gutter / rounded corners. `clip-path: inset(-12px)` keeps that
|
|
12
|
+
* ring off the side columns. It sits below opaque chrome (headers/banners),
|
|
13
|
+
* so it only masks the content's bleed, never the chrome. No border.
|
|
14
|
+
* (`clip-path` MUST be the arbitrary class — RN-web drops it from `style`.)
|
|
15
|
+
*
|
|
16
|
+
* 2. BORDER-FRAME overlay (`z-[120]`): a single 1px rounded `border-border`
|
|
17
|
+
* outline with a transparent interior, ABOVE all content. Being one element
|
|
18
|
+
* above everything, it draws ONE continuous rounded border around all four
|
|
19
|
+
* sides — no seams, no double lines, no per-chrome borders.
|
|
20
|
+
*
|
|
21
|
+
* Both overlays are `position: sticky; top: 8px` with a negative
|
|
22
|
+
* `margin-bottom` so they occupy zero layout height (they overlay the scrolled
|
|
23
|
+
* content rather than displacing it). They render BEFORE the content wrapper so
|
|
24
|
+
* z-index — not DOM order — decides layering.
|
|
25
|
+
*
|
|
26
|
+
* When `framed` is false (narrow screens) the panel is full-bleed: a single
|
|
27
|
+
* flat surface, no rounding, no overlays.
|
|
28
|
+
*
|
|
29
|
+
* Native bundlers use `./index.tsx` (a plain rounded surface); web bundlers
|
|
30
|
+
* select this file via the `"browser"` export condition in `package.json`.
|
|
31
|
+
*
|
|
32
|
+
* Styling is NativeWind-className-first; the literal class strings below MUST
|
|
33
|
+
* stay literal so a consumer's Tailwind content-scan over `lib/**` picks them up
|
|
34
|
+
* (no dynamic concatenation of the arbitrary `web:[…]` / `rounded-radius-28`
|
|
35
|
+
* parts).
|
|
36
|
+
*/
|
|
37
|
+
import React, { memo } from 'react';
|
|
38
|
+
import { View, type StyleProp, type ViewStyle } from 'react-native';
|
|
39
|
+
|
|
40
|
+
import { useTheme } from '../theme/use-theme';
|
|
41
|
+
|
|
42
|
+
/** Width (in px) of the sticky gutter-mask box-shadow ring. */
|
|
43
|
+
export const GUTTER_MASK_SPREAD = 40;
|
|
44
|
+
/** Top sticky inset (px) the framed panel chrome pins to. */
|
|
45
|
+
export const PANEL_TOP_INSET = 8;
|
|
46
|
+
/** Bottom sticky inset (px) of the framed panel chrome. */
|
|
47
|
+
export const PANEL_BOTTOM_INSET = 8;
|
|
48
|
+
|
|
49
|
+
export interface ContentPanelProps {
|
|
50
|
+
children: React.ReactNode;
|
|
51
|
+
/** Framed (wide screens) vs full-bleed (narrow screens). */
|
|
52
|
+
framed: boolean;
|
|
53
|
+
/** Override the surface background utility (defaults to `bg-card`). */
|
|
54
|
+
surfaceClassName?: string;
|
|
55
|
+
surfaceStyle?: StyleProp<ViewStyle>;
|
|
56
|
+
/** Extra utilities for the inner content wrapper. */
|
|
57
|
+
contentClassName?: string;
|
|
58
|
+
contentStyle?: StyleProp<ViewStyle>;
|
|
59
|
+
/** Render the sticky border frame (defaults to true when framed). */
|
|
60
|
+
showStickyFrame?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const ContentPanelComponent: React.FC<ContentPanelProps> = ({
|
|
64
|
+
children,
|
|
65
|
+
framed,
|
|
66
|
+
surfaceClassName,
|
|
67
|
+
surfaceStyle,
|
|
68
|
+
contentClassName,
|
|
69
|
+
contentStyle,
|
|
70
|
+
showStickyFrame,
|
|
71
|
+
}) => {
|
|
72
|
+
const { colors } = useTheme();
|
|
73
|
+
|
|
74
|
+
if (!framed) {
|
|
75
|
+
// Full-bleed: a single flat surface. No rounding, no sticky overlays.
|
|
76
|
+
const fullBleedClass = [
|
|
77
|
+
'flex-1',
|
|
78
|
+
surfaceClassName ?? 'bg-card',
|
|
79
|
+
contentClassName,
|
|
80
|
+
]
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
.join(' ');
|
|
83
|
+
return (
|
|
84
|
+
<View
|
|
85
|
+
{...({ className: fullBleedClass } as Record<string, string>)}
|
|
86
|
+
style={[surfaceStyle, contentStyle]}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
</View>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const surfaceClass = ['flex-1', 'rounded-radius-28', surfaceClassName ?? 'bg-card'].join(' ');
|
|
94
|
+
const contentClass = ['flex-1', 'rounded-radius-28 web:overflow-x-clip', contentClassName]
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.join(' ');
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<View
|
|
100
|
+
{...({ className: surfaceClass } as Record<string, string>)}
|
|
101
|
+
style={surfaceStyle}
|
|
102
|
+
>
|
|
103
|
+
{/* (1) Bleed-mask overlay — gutter box-shadow ring, below chrome. */}
|
|
104
|
+
<View
|
|
105
|
+
pointerEvents="none"
|
|
106
|
+
{...({
|
|
107
|
+
className:
|
|
108
|
+
'web:sticky web:top-2 z-30 h-[calc(100dvh-16px)] w-full rounded-radius-28 web:[margin-bottom:calc(-100dvh+16px)] web:[clip-path:inset(-12px)]',
|
|
109
|
+
} as Record<string, string>)}
|
|
110
|
+
style={{ boxShadow: `0 0 0 ${GUTTER_MASK_SPREAD}px ${colors.background}` }}
|
|
111
|
+
/>
|
|
112
|
+
{/* (2) Border-frame overlay — one continuous rounded border, above all. */}
|
|
113
|
+
{showStickyFrame !== false && (
|
|
114
|
+
<View
|
|
115
|
+
pointerEvents="none"
|
|
116
|
+
{...({
|
|
117
|
+
className:
|
|
118
|
+
'web:sticky web:top-2 z-[120] h-[calc(100dvh-16px)] w-full rounded-radius-28 border border-border web:[margin-bottom:calc(-100dvh+16px)]',
|
|
119
|
+
} as Record<string, string>)}
|
|
120
|
+
/>
|
|
121
|
+
)}
|
|
122
|
+
{/* Content wrapper — clipped to the rounded panel shape on web. */}
|
|
123
|
+
<View
|
|
124
|
+
{...({ className: contentClass } as Record<string, string>)}
|
|
125
|
+
style={contentStyle}
|
|
126
|
+
>
|
|
127
|
+
{children}
|
|
128
|
+
</View>
|
|
129
|
+
</View>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const ContentPanel = memo(ContentPanelComponent);
|
|
134
|
+
ContentPanel.displayName = 'ContentPanel';
|