@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.
Files changed (188) hide show
  1. package/lib/commonjs/benefit-list/index.js +119 -0
  2. package/lib/commonjs/benefit-list/index.js.map +1 -0
  3. package/lib/commonjs/benefit-list/types.js +6 -0
  4. package/lib/commonjs/benefit-list/types.js.map +1 -0
  5. package/lib/commonjs/connection-dots/ConnectionDots.js +112 -0
  6. package/lib/commonjs/connection-dots/ConnectionDots.js.map +1 -0
  7. package/lib/commonjs/connection-dots/ConnectionDots.web.js +116 -0
  8. package/lib/commonjs/connection-dots/ConnectionDots.web.js.map +1 -0
  9. package/lib/commonjs/connection-dots/ConnectionDotsBase.js +55 -0
  10. package/lib/commonjs/connection-dots/ConnectionDotsBase.js.map +1 -0
  11. package/lib/commonjs/connection-dots/index.js +13 -0
  12. package/lib/commonjs/connection-dots/index.js.map +1 -0
  13. package/lib/commonjs/connection-dots/index.web.js +13 -0
  14. package/lib/commonjs/connection-dots/index.web.js.map +1 -0
  15. package/lib/commonjs/connection-dots/types.js +6 -0
  16. package/lib/commonjs/connection-dots/types.js.map +1 -0
  17. package/lib/commonjs/content-panel/index.js +64 -0
  18. package/lib/commonjs/content-panel/index.js.map +1 -0
  19. package/lib/commonjs/content-panel/index.web.js +99 -0
  20. package/lib/commonjs/content-panel/index.web.js.map +1 -0
  21. package/lib/commonjs/design-tokens/color-roles.js +112 -0
  22. package/lib/commonjs/design-tokens/color-roles.js.map +1 -0
  23. package/lib/commonjs/design-tokens/index.js +89 -0
  24. package/lib/commonjs/design-tokens/index.js.map +1 -0
  25. package/lib/commonjs/design-tokens/scales.js +168 -0
  26. package/lib/commonjs/design-tokens/scales.js.map +1 -0
  27. package/lib/commonjs/design-tokens/shadows.js +48 -0
  28. package/lib/commonjs/design-tokens/shadows.js.map +1 -0
  29. package/lib/commonjs/design-tokens/shadows.native.js +53 -0
  30. package/lib/commonjs/design-tokens/shadows.native.js.map +1 -0
  31. package/lib/commonjs/design-tokens/tailwind-preset.js +139 -0
  32. package/lib/commonjs/design-tokens/tailwind-preset.js.map +1 -0
  33. package/lib/commonjs/design-tokens/theme-css.js +98 -0
  34. package/lib/commonjs/design-tokens/theme-css.js.map +1 -0
  35. package/lib/commonjs/index.js +208 -93
  36. package/lib/commonjs/index.js.map +1 -1
  37. package/lib/commonjs/index.web.js +223 -108
  38. package/lib/commonjs/index.web.js.map +1 -1
  39. package/lib/commonjs/text-field/index.js +156 -0
  40. package/lib/commonjs/text-field/index.js.map +1 -1
  41. package/lib/module/benefit-list/index.js +114 -0
  42. package/lib/module/benefit-list/index.js.map +1 -0
  43. package/lib/module/benefit-list/types.js +4 -0
  44. package/lib/module/benefit-list/types.js.map +1 -0
  45. package/lib/module/connection-dots/ConnectionDots.js +107 -0
  46. package/lib/module/connection-dots/ConnectionDots.js.map +1 -0
  47. package/lib/module/connection-dots/ConnectionDots.web.js +111 -0
  48. package/lib/module/connection-dots/ConnectionDots.web.js.map +1 -0
  49. package/lib/module/connection-dots/ConnectionDotsBase.js +50 -0
  50. package/lib/module/connection-dots/ConnectionDotsBase.js.map +1 -0
  51. package/lib/module/connection-dots/index.js +4 -0
  52. package/lib/module/connection-dots/index.js.map +1 -0
  53. package/lib/module/connection-dots/index.web.js +4 -0
  54. package/lib/module/connection-dots/index.web.js.map +1 -0
  55. package/lib/module/connection-dots/types.js +4 -0
  56. package/lib/module/connection-dots/types.js.map +1 -0
  57. package/lib/module/content-panel/index.js +59 -0
  58. package/lib/module/content-panel/index.js.map +1 -0
  59. package/lib/module/content-panel/index.web.js +94 -0
  60. package/lib/module/content-panel/index.web.js.map +1 -0
  61. package/lib/module/design-tokens/color-roles.js +108 -0
  62. package/lib/module/design-tokens/color-roles.js.map +1 -0
  63. package/lib/module/design-tokens/index.js +27 -0
  64. package/lib/module/design-tokens/index.js.map +1 -0
  65. package/lib/module/design-tokens/scales.js +164 -0
  66. package/lib/module/design-tokens/scales.js.map +1 -0
  67. package/lib/module/design-tokens/shadows.js +43 -0
  68. package/lib/module/design-tokens/shadows.js.map +1 -0
  69. package/lib/module/design-tokens/shadows.native.js +48 -0
  70. package/lib/module/design-tokens/shadows.native.js.map +1 -0
  71. package/lib/module/design-tokens/tailwind-preset.js +136 -0
  72. package/lib/module/design-tokens/tailwind-preset.js.map +1 -0
  73. package/lib/module/design-tokens/theme-css.js +94 -0
  74. package/lib/module/design-tokens/theme-css.js.map +1 -0
  75. package/lib/module/index.js +6 -1
  76. package/lib/module/index.js.map +1 -1
  77. package/lib/module/index.web.js +6 -1
  78. package/lib/module/index.web.js.map +1 -1
  79. package/lib/module/text-field/index.js +158 -2
  80. package/lib/module/text-field/index.js.map +1 -1
  81. package/lib/typescript/commonjs/benefit-list/index.d.ts +6 -0
  82. package/lib/typescript/commonjs/benefit-list/index.d.ts.map +1 -0
  83. package/lib/typescript/commonjs/benefit-list/types.d.ts +35 -0
  84. package/lib/typescript/commonjs/benefit-list/types.d.ts.map +1 -0
  85. package/lib/typescript/commonjs/connection-dots/ConnectionDots.d.ts +4 -0
  86. package/lib/typescript/commonjs/connection-dots/ConnectionDots.d.ts.map +1 -0
  87. package/lib/typescript/commonjs/connection-dots/ConnectionDots.web.d.ts +14 -0
  88. package/lib/typescript/commonjs/connection-dots/ConnectionDots.web.d.ts.map +1 -0
  89. package/lib/typescript/commonjs/connection-dots/ConnectionDotsBase.d.ts +18 -0
  90. package/lib/typescript/commonjs/connection-dots/ConnectionDotsBase.d.ts.map +1 -0
  91. package/lib/typescript/commonjs/connection-dots/index.d.ts +3 -0
  92. package/lib/typescript/commonjs/connection-dots/index.d.ts.map +1 -0
  93. package/lib/typescript/commonjs/connection-dots/index.web.d.ts +3 -0
  94. package/lib/typescript/commonjs/connection-dots/index.web.d.ts.map +1 -0
  95. package/lib/typescript/commonjs/connection-dots/types.d.ts +41 -0
  96. package/lib/typescript/commonjs/connection-dots/types.d.ts.map +1 -0
  97. package/lib/typescript/commonjs/content-panel/index.d.ts +54 -0
  98. package/lib/typescript/commonjs/content-panel/index.d.ts.map +1 -0
  99. package/lib/typescript/commonjs/content-panel/index.web.d.ts +59 -0
  100. package/lib/typescript/commonjs/content-panel/index.web.d.ts.map +1 -0
  101. package/lib/typescript/commonjs/design-tokens/color-roles.d.ts +102 -0
  102. package/lib/typescript/commonjs/design-tokens/color-roles.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/design-tokens/index.d.ts +28 -0
  104. package/lib/typescript/commonjs/design-tokens/index.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/design-tokens/scales.d.ts +152 -0
  106. package/lib/typescript/commonjs/design-tokens/scales.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/design-tokens/shadows.d.ts +34 -0
  108. package/lib/typescript/commonjs/design-tokens/shadows.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/design-tokens/shadows.native.d.ts +19 -0
  110. package/lib/typescript/commonjs/design-tokens/shadows.native.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/design-tokens/tailwind-preset.d.ts +59 -0
  112. package/lib/typescript/commonjs/design-tokens/tailwind-preset.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/design-tokens/theme-css.d.ts +39 -0
  114. package/lib/typescript/commonjs/design-tokens/theme-css.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/index.d.ts +6 -0
  116. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/index.web.d.ts +6 -0
  118. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/text-field/index.d.ts +14 -1
  120. package/lib/typescript/commonjs/text-field/index.d.ts.map +1 -1
  121. package/lib/typescript/module/benefit-list/index.d.ts +6 -0
  122. package/lib/typescript/module/benefit-list/index.d.ts.map +1 -0
  123. package/lib/typescript/module/benefit-list/types.d.ts +35 -0
  124. package/lib/typescript/module/benefit-list/types.d.ts.map +1 -0
  125. package/lib/typescript/module/connection-dots/ConnectionDots.d.ts +4 -0
  126. package/lib/typescript/module/connection-dots/ConnectionDots.d.ts.map +1 -0
  127. package/lib/typescript/module/connection-dots/ConnectionDots.web.d.ts +14 -0
  128. package/lib/typescript/module/connection-dots/ConnectionDots.web.d.ts.map +1 -0
  129. package/lib/typescript/module/connection-dots/ConnectionDotsBase.d.ts +18 -0
  130. package/lib/typescript/module/connection-dots/ConnectionDotsBase.d.ts.map +1 -0
  131. package/lib/typescript/module/connection-dots/index.d.ts +3 -0
  132. package/lib/typescript/module/connection-dots/index.d.ts.map +1 -0
  133. package/lib/typescript/module/connection-dots/index.web.d.ts +3 -0
  134. package/lib/typescript/module/connection-dots/index.web.d.ts.map +1 -0
  135. package/lib/typescript/module/connection-dots/types.d.ts +41 -0
  136. package/lib/typescript/module/connection-dots/types.d.ts.map +1 -0
  137. package/lib/typescript/module/content-panel/index.d.ts +54 -0
  138. package/lib/typescript/module/content-panel/index.d.ts.map +1 -0
  139. package/lib/typescript/module/content-panel/index.web.d.ts +59 -0
  140. package/lib/typescript/module/content-panel/index.web.d.ts.map +1 -0
  141. package/lib/typescript/module/design-tokens/color-roles.d.ts +102 -0
  142. package/lib/typescript/module/design-tokens/color-roles.d.ts.map +1 -0
  143. package/lib/typescript/module/design-tokens/index.d.ts +28 -0
  144. package/lib/typescript/module/design-tokens/index.d.ts.map +1 -0
  145. package/lib/typescript/module/design-tokens/scales.d.ts +152 -0
  146. package/lib/typescript/module/design-tokens/scales.d.ts.map +1 -0
  147. package/lib/typescript/module/design-tokens/shadows.d.ts +34 -0
  148. package/lib/typescript/module/design-tokens/shadows.d.ts.map +1 -0
  149. package/lib/typescript/module/design-tokens/shadows.native.d.ts +19 -0
  150. package/lib/typescript/module/design-tokens/shadows.native.d.ts.map +1 -0
  151. package/lib/typescript/module/design-tokens/tailwind-preset.d.ts +59 -0
  152. package/lib/typescript/module/design-tokens/tailwind-preset.d.ts.map +1 -0
  153. package/lib/typescript/module/design-tokens/theme-css.d.ts +39 -0
  154. package/lib/typescript/module/design-tokens/theme-css.d.ts.map +1 -0
  155. package/lib/typescript/module/index.d.ts +6 -0
  156. package/lib/typescript/module/index.d.ts.map +1 -1
  157. package/lib/typescript/module/index.web.d.ts +6 -0
  158. package/lib/typescript/module/index.web.d.ts.map +1 -1
  159. package/lib/typescript/module/text-field/index.d.ts +14 -1
  160. package/lib/typescript/module/text-field/index.d.ts.map +1 -1
  161. package/package.json +66 -1
  162. package/src/__tests__/BenefitList.test.tsx +108 -0
  163. package/src/__tests__/ConnectionDots.test.tsx +86 -0
  164. package/src/__tests__/TextFieldFloatingLabel.test.tsx +93 -0
  165. package/src/__tests__/design-tokens.test.ts +169 -0
  166. package/src/benefit-list/BenefitList.stories.tsx +50 -0
  167. package/src/benefit-list/index.tsx +133 -0
  168. package/src/benefit-list/types.ts +36 -0
  169. package/src/connection-dots/ConnectionDots.stories.tsx +54 -0
  170. package/src/connection-dots/ConnectionDots.tsx +139 -0
  171. package/src/connection-dots/ConnectionDots.web.tsx +122 -0
  172. package/src/connection-dots/ConnectionDotsBase.tsx +62 -0
  173. package/src/connection-dots/index.ts +2 -0
  174. package/src/connection-dots/index.web.ts +2 -0
  175. package/src/connection-dots/types.ts +41 -0
  176. package/src/content-panel/index.tsx +87 -0
  177. package/src/content-panel/index.web.tsx +134 -0
  178. package/src/design-tokens/color-roles.ts +110 -0
  179. package/src/design-tokens/index.ts +45 -0
  180. package/src/design-tokens/scales.ts +134 -0
  181. package/src/design-tokens/shadows.native.ts +44 -0
  182. package/src/design-tokens/shadows.ts +42 -0
  183. package/src/design-tokens/tailwind-preset.ts +139 -0
  184. package/src/design-tokens/theme-css.ts +99 -0
  185. package/src/index.ts +33 -0
  186. package/src/index.web.ts +33 -0
  187. package/src/text-field/TextField.stories.tsx +46 -0
  188. 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,2 @@
1
+ export { ConnectionDots } from './ConnectionDots';
2
+ export type { ConnectionDotsProps } from './types';
@@ -0,0 +1,2 @@
1
+ export { ConnectionDots } from './ConnectionDots.web';
2
+ export type { ConnectionDotsProps } from './types';
@@ -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';