@oxyhq/bloom 0.26.1 → 0.27.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/avatar/Avatar.js +67 -96
- package/lib/commonjs/avatar/Avatar.js.map +1 -1
- package/lib/commonjs/avatar/AvatarRing.js +183 -0
- package/lib/commonjs/avatar/AvatarRing.js.map +1 -0
- package/lib/commonjs/avatar/index.js.map +1 -1
- package/lib/commonjs/avatar/squircle-path.js +14 -0
- package/lib/commonjs/avatar/squircle-path.js.map +1 -0
- package/lib/commonjs/avatar/svg-module.js +15 -0
- package/lib/commonjs/avatar/svg-module.js.map +1 -0
- package/lib/module/avatar/Avatar.js +66 -96
- package/lib/module/avatar/Avatar.js.map +1 -1
- package/lib/module/avatar/AvatarRing.js +177 -0
- package/lib/module/avatar/AvatarRing.js.map +1 -0
- package/lib/module/avatar/index.js.map +1 -1
- package/lib/module/avatar/squircle-path.js +10 -0
- package/lib/module/avatar/squircle-path.js.map +1 -0
- package/lib/module/avatar/svg-module.js +11 -0
- package/lib/module/avatar/svg-module.js.map +1 -0
- package/lib/typescript/commonjs/avatar/Avatar.d.ts.map +1 -1
- package/lib/typescript/commonjs/avatar/AvatarRing.d.ts +27 -0
- package/lib/typescript/commonjs/avatar/AvatarRing.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar/index.d.ts +1 -1
- package/lib/typescript/commonjs/avatar/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/avatar/squircle-path.d.ts +8 -0
- package/lib/typescript/commonjs/avatar/squircle-path.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar/svg-module.d.ts +8 -0
- package/lib/typescript/commonjs/avatar/svg-module.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar/types.d.ts +30 -0
- package/lib/typescript/commonjs/avatar/types.d.ts.map +1 -1
- package/lib/typescript/module/avatar/Avatar.d.ts.map +1 -1
- package/lib/typescript/module/avatar/AvatarRing.d.ts +27 -0
- package/lib/typescript/module/avatar/AvatarRing.d.ts.map +1 -0
- package/lib/typescript/module/avatar/index.d.ts +1 -1
- package/lib/typescript/module/avatar/index.d.ts.map +1 -1
- package/lib/typescript/module/avatar/squircle-path.d.ts +8 -0
- package/lib/typescript/module/avatar/squircle-path.d.ts.map +1 -0
- package/lib/typescript/module/avatar/svg-module.d.ts +8 -0
- package/lib/typescript/module/avatar/svg-module.d.ts.map +1 -0
- package/lib/typescript/module/avatar/types.d.ts +30 -0
- package/lib/typescript/module/avatar/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/avatar/Avatar.stories.tsx +95 -0
- package/src/avatar/Avatar.tsx +61 -110
- package/src/avatar/AvatarRing.tsx +170 -0
- package/src/avatar/index.ts +6 -1
- package/src/avatar/squircle-path.ts +11 -0
- package/src/avatar/svg-module.ts +11 -0
- package/src/avatar/types.ts +32 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React, { memo, useMemo } from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { Z_INDEX } from '../styles/z-index';
|
|
5
|
+
import { getSvgModule } from './svg-module';
|
|
6
|
+
import { SQUIRCLE_PATH } from './squircle-path';
|
|
7
|
+
import type { AvatarRingGradientDirection, AvatarShape } from './types';
|
|
8
|
+
|
|
9
|
+
// Module counter for unique gradient element ids — mirrors the `clipIdCounter`
|
|
10
|
+
// pattern in `Avatar.tsx`. Deterministic (no `Math.random()`) so ids are stable
|
|
11
|
+
// per mounted instance.
|
|
12
|
+
let ringGradientIdCounter = 0;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Size of the box the ring occupies. With `gap === 0` the ring overlays the
|
|
16
|
+
* avatar edge and the box equals the avatar size (footprint unchanged). With
|
|
17
|
+
* `gap > 0` the ring sits OUTSIDE the avatar, so the box grows and the avatar is
|
|
18
|
+
* centered inside it. Exported so `Avatar` reserves the same footprint with a
|
|
19
|
+
* single source for the geometry formula.
|
|
20
|
+
*/
|
|
21
|
+
export function getRingOuterSize(size: number, width: number, gap: number): number {
|
|
22
|
+
return gap > 0 ? size + 2 * (width + gap) : size;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface AvatarRingProps {
|
|
26
|
+
/** Avatar diameter in px (the inner content size, before any gap). */
|
|
27
|
+
size: number;
|
|
28
|
+
/** Matches the avatar shape. */
|
|
29
|
+
shape: AvatarShape;
|
|
30
|
+
/** Solid ring: one color. Gradient ring: 2+ colors. */
|
|
31
|
+
colors: string | string[];
|
|
32
|
+
/** Ring stroke width in px. */
|
|
33
|
+
width: number;
|
|
34
|
+
/** Gap between the avatar edge and the ring (0 = overlay the edge). */
|
|
35
|
+
gap: number;
|
|
36
|
+
/** Gradient sweep direction for multi-color rings. */
|
|
37
|
+
gradientDirection: AvatarRingGradientDirection;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function gradientCoords(direction: AvatarRingGradientDirection) {
|
|
41
|
+
switch (direction) {
|
|
42
|
+
case 'horizontal':
|
|
43
|
+
return { x1: '0', y1: '0', x2: '1', y2: '0' };
|
|
44
|
+
case 'vertical':
|
|
45
|
+
return { x1: '0', y1: '0', x2: '0', y2: '1' };
|
|
46
|
+
case 'diagonal':
|
|
47
|
+
default:
|
|
48
|
+
return { x1: '0', y1: '0', x2: '1', y2: '1' };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Single ring primitive shared by the `live` indicator and the public `ring`
|
|
54
|
+
* prop. Renders either a solid or a gradient ring, for both `circle` and
|
|
55
|
+
* `squircle` avatars.
|
|
56
|
+
*
|
|
57
|
+
* - Solid circle → a plain bordered `<View>` (no `react-native-svg` needed).
|
|
58
|
+
* - Solid squircle → an SVG `<Path>` stroking the squircle outline.
|
|
59
|
+
* - Gradient (either shape) → an SVG `<LinearGradient>` stroke; degrades to a
|
|
60
|
+
* solid ring of the first color when `react-native-svg` is unavailable (a
|
|
61
|
+
* squircle then degrades to a circle border, matching the avatar's own no-SVG
|
|
62
|
+
* circle fallback).
|
|
63
|
+
*
|
|
64
|
+
* Internal to the avatar family — not exported from the package.
|
|
65
|
+
*/
|
|
66
|
+
const AvatarRingComponent: React.FC<AvatarRingProps> = ({
|
|
67
|
+
size,
|
|
68
|
+
shape,
|
|
69
|
+
colors,
|
|
70
|
+
width,
|
|
71
|
+
gap,
|
|
72
|
+
gradientDirection,
|
|
73
|
+
}) => {
|
|
74
|
+
const gradientId = useMemo(() => `bloom-ring-grad${ringGradientIdCounter++}`, []);
|
|
75
|
+
|
|
76
|
+
const colorList = Array.isArray(colors) ? colors : [colors];
|
|
77
|
+
const wantsGradient = colorList.length >= 2;
|
|
78
|
+
const solidColor = colorList[0] ?? '#000000';
|
|
79
|
+
|
|
80
|
+
const outer = getRingOuterSize(size, width, gap);
|
|
81
|
+
// The ring's visible band is always the outermost `width` px inside the outer
|
|
82
|
+
// box; when it degrades to a circle border it is a full circle → radius
|
|
83
|
+
// `outer / 2`.
|
|
84
|
+
const ringRadius = outer / 2;
|
|
85
|
+
|
|
86
|
+
// `react-native-svg` is required for gradients (both shapes) and for the
|
|
87
|
+
// squircle outline. A solid circle ring is a plain border and never needs it.
|
|
88
|
+
const needsSvg = wantsGradient || shape === 'squircle';
|
|
89
|
+
const svg = needsSvg ? getSvgModule() : null;
|
|
90
|
+
|
|
91
|
+
// No SVG available (or a solid circle, which never needs SVG): draw a plain
|
|
92
|
+
// bordered View. A gradient degrades to its first color; a squircle degrades
|
|
93
|
+
// to a circle border.
|
|
94
|
+
if (!svg) {
|
|
95
|
+
return (
|
|
96
|
+
<View
|
|
97
|
+
pointerEvents="none"
|
|
98
|
+
style={{
|
|
99
|
+
position: 'absolute',
|
|
100
|
+
top: 0,
|
|
101
|
+
left: 0,
|
|
102
|
+
width: outer,
|
|
103
|
+
height: outer,
|
|
104
|
+
borderRadius: ringRadius,
|
|
105
|
+
borderColor: solidColor,
|
|
106
|
+
borderWidth: width,
|
|
107
|
+
zIndex: gap > 0 ? Z_INDEX.base : Z_INDEX.raised,
|
|
108
|
+
}}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { default: Svg, Defs, LinearGradient, Stop, Circle, Path } = svg;
|
|
114
|
+
const stroke = wantsGradient ? `url(#${gradientId})` : solidColor;
|
|
115
|
+
const coords = gradientCoords(gradientDirection);
|
|
116
|
+
const gradientDefs = wantsGradient ? (
|
|
117
|
+
<Defs>
|
|
118
|
+
<LinearGradient id={gradientId} x1={coords.x1} y1={coords.y1} x2={coords.x2} y2={coords.y2}>
|
|
119
|
+
{colorList.map((color, index) => (
|
|
120
|
+
<Stop
|
|
121
|
+
key={`${gradientId}-${index}`}
|
|
122
|
+
offset={colorList.length === 1 ? 0 : index / (colorList.length - 1)}
|
|
123
|
+
stopColor={color}
|
|
124
|
+
/>
|
|
125
|
+
))}
|
|
126
|
+
</LinearGradient>
|
|
127
|
+
</Defs>
|
|
128
|
+
) : null;
|
|
129
|
+
|
|
130
|
+
// The overlay fills its parent; the SVG within is sized to the ring box so a
|
|
131
|
+
// `gap === 0` ring overlays the avatar edge exactly and a `gap > 0` ring sits
|
|
132
|
+
// on the reserved outer perimeter.
|
|
133
|
+
const overlayStyle = [
|
|
134
|
+
StyleSheet.absoluteFill,
|
|
135
|
+
{ zIndex: gap > 0 ? Z_INDEX.base : Z_INDEX.raised },
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
if (shape === 'squircle') {
|
|
139
|
+
// The path lives in 0–1 space, so the stroke width is expressed in the same
|
|
140
|
+
// units; center-stroking it and letting the viewport clip the outer half
|
|
141
|
+
// yields a ~`width`-px band on the inner edge of the outer box.
|
|
142
|
+
return (
|
|
143
|
+
<View pointerEvents="none" style={overlayStyle}>
|
|
144
|
+
<Svg width={outer} height={outer} viewBox="0 0 1 1">
|
|
145
|
+
{gradientDefs}
|
|
146
|
+
<Path d={SQUIRCLE_PATH} fill="none" stroke={stroke} strokeWidth={(width / outer) * 2} />
|
|
147
|
+
</Svg>
|
|
148
|
+
</View>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<View pointerEvents="none" style={overlayStyle}>
|
|
154
|
+
<Svg width={outer} height={outer} viewBox={`0 0 ${outer} ${outer}`}>
|
|
155
|
+
{gradientDefs}
|
|
156
|
+
<Circle
|
|
157
|
+
cx={outer / 2}
|
|
158
|
+
cy={outer / 2}
|
|
159
|
+
r={(outer - width) / 2}
|
|
160
|
+
fill="none"
|
|
161
|
+
stroke={stroke}
|
|
162
|
+
strokeWidth={width}
|
|
163
|
+
/>
|
|
164
|
+
</Svg>
|
|
165
|
+
</View>
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const AvatarRing = memo(AvatarRingComponent);
|
|
170
|
+
AvatarRing.displayName = 'AvatarRing';
|
package/src/avatar/index.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export { Avatar } from './Avatar';
|
|
2
2
|
export { AvatarPlaceholderProvider } from './placeholder-context';
|
|
3
3
|
export type { AvatarPlaceholderConfig } from './placeholder-context';
|
|
4
|
-
export type {
|
|
4
|
+
export type {
|
|
5
|
+
AvatarProps,
|
|
6
|
+
AvatarShape,
|
|
7
|
+
AvatarRingConfig,
|
|
8
|
+
AvatarRingGradientDirection,
|
|
9
|
+
} from './types';
|
|
5
10
|
|
|
6
11
|
// Inlined as a base64 data URI so consumers compiling Bloom's source directly
|
|
7
12
|
// do not need ambient `*.jpg` module declarations or a `.jpg` asset loader.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Squircle clip/outline path normalized to a 0–1 coordinate space
|
|
3
|
+
* (viewBox="0 0 1 1"). Shared by the squircle avatar image clip in `Avatar.tsx`
|
|
4
|
+
* and the squircle outline in `AvatarRing.tsx` so the geometry has a single
|
|
5
|
+
* source of truth.
|
|
6
|
+
*/
|
|
7
|
+
export const SQUIRCLE_PATH =
|
|
8
|
+
'M0 0.5 L0.00122 0.31674 L0.00489 0.25123 L0.01103 0.20331 L0.01969 0.16478 L0.03097 0.13257 L0.04495 0.10518 L0.0618 0.08177 L0.08177 0.0618 L0.10518 0.04495 L0.13257 0.03097 L0.16478 0.01969 L0.20331 0.01103 L0.25123 0.00489 L0.31674 0.00122 L0.5 0' +
|
|
9
|
+
' L0.68895 0.0014 L0.7564 0.00561 L0.80559 0.01267 L0.84499 0.02264 L0.87771 0.03564 L0.9053 0.05181 L0.92862 0.07138 L0.94819 0.0947 L0.96436 0.12228 L0.97736 0.15501 L0.98733 0.19441 L0.99439 0.2436 L0.9986 0.31105 L1 0.5' +
|
|
10
|
+
' L0.9986 0.68895 L0.99439 0.7564 L0.98733 0.80559 L0.97736 0.84499 L0.96436 0.87771 L0.94819 0.9053 L0.92862 0.92862 L0.9053 0.94819 L0.87771 0.96436 L0.84499 0.97736 L0.80559 0.98733 L0.7564 0.99439 L0.68895 0.9986 L0.5 1' +
|
|
11
|
+
' L0.31105 0.9986 L0.2436 0.99439 L0.19441 0.98733 L0.15501 0.97736 L0.12228 0.96436 L0.0947 0.94819 L0.07138 0.92862 L0.05181 0.9053 L0.03564 0.87771 L0.02264 0.84499 L0.01267 0.80559 L0.00561 0.7564 L0.0014 0.68895 L0 0.5Z';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { lazyRequire } from '../utils/lazy-require';
|
|
2
|
+
|
|
3
|
+
type SvgModuleType = typeof import('react-native-svg');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Lazily loads `react-native-svg`, returning `null` when it is not installed
|
|
7
|
+
* (e.g. a web bundle that does not ship the optional peer). Shared by the
|
|
8
|
+
* squircle avatar image clip in `Avatar.tsx` and the `AvatarRing` so there is a
|
|
9
|
+
* single loader rather than one per call site.
|
|
10
|
+
*/
|
|
11
|
+
export const getSvgModule = lazyRequire<SvgModuleType>('react-native-svg');
|
package/src/avatar/types.ts
CHANGED
|
@@ -5,6 +5,20 @@ import type { ImageResolver } from '../image-resolver/context';
|
|
|
5
5
|
|
|
6
6
|
export type AvatarShape = 'circle' | 'squircle';
|
|
7
7
|
|
|
8
|
+
/** Gradient sweep direction for a multi-color {@link AvatarRingConfig}. */
|
|
9
|
+
export type AvatarRingGradientDirection = 'diagonal' | 'horizontal' | 'vertical';
|
|
10
|
+
|
|
11
|
+
export interface AvatarRingConfig {
|
|
12
|
+
/** Solid ring: one color. Gradient ring: 2+ colors. */
|
|
13
|
+
colors: string | string[];
|
|
14
|
+
/** Ring stroke width in px. Default: size > 16 ? 2 : 1. */
|
|
15
|
+
width?: number;
|
|
16
|
+
/** Gap between avatar edge and ring. Default 0 (ring overlays the edge). */
|
|
17
|
+
gap?: number;
|
|
18
|
+
/** Gradient sweep direction for multi-color rings. Default 'diagonal'. */
|
|
19
|
+
gradientDirection?: AvatarRingGradientDirection;
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
export interface AvatarProps {
|
|
9
23
|
/**
|
|
10
24
|
* Flexible image source — accepts a URL string, an ImageSourcePropType
|
|
@@ -70,5 +84,23 @@ export interface AvatarProps {
|
|
|
70
84
|
* live treatments.
|
|
71
85
|
*/
|
|
72
86
|
liveColor?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Decorative ring drawn around the avatar. Pass a single color string for a
|
|
89
|
+
* solid ring, or an array of 2+ colors for a gradient ring (e.g. an
|
|
90
|
+
* Instagram-stories ring). The gradient form requires `react-native-svg`
|
|
91
|
+
* (falls back to a solid ring of the first color if it is not installed).
|
|
92
|
+
*
|
|
93
|
+
* When `gap` is 0 (default) the ring overlays the avatar edge and the
|
|
94
|
+
* component's footprint is unchanged. When `gap` > 0 the ring is drawn OUTSIDE
|
|
95
|
+
* the avatar and the rendered footprint grows to `size + 2*(width + gap)` with
|
|
96
|
+
* the avatar centered inside (matching how Instagram-stories rings sit outside
|
|
97
|
+
* the avatar).
|
|
98
|
+
*
|
|
99
|
+
* `live` is sugar over this same primitive: when set (and no explicit `ring`
|
|
100
|
+
* is passed) it renders a solid theme-`negative` ring plus the "LIVE" badge.
|
|
101
|
+
* An explicit `ring` always wins for geometry and colors; the badge stays
|
|
102
|
+
* controlled by `live`/`hideLiveBadge`.
|
|
103
|
+
*/
|
|
104
|
+
ring?: AvatarRingConfig;
|
|
73
105
|
testID?: string;
|
|
74
106
|
}
|