@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
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { ImageSourcePropType, StyleProp, ViewStyle, ImageStyle } from 'react-native';
|
|
3
3
|
export type AvatarShape = 'circle' | 'squircle';
|
|
4
|
+
/** Gradient sweep direction for a multi-color {@link AvatarRingConfig}. */
|
|
5
|
+
export type AvatarRingGradientDirection = 'diagonal' | 'horizontal' | 'vertical';
|
|
6
|
+
export interface AvatarRingConfig {
|
|
7
|
+
/** Solid ring: one color. Gradient ring: 2+ colors. */
|
|
8
|
+
colors: string | string[];
|
|
9
|
+
/** Ring stroke width in px. Default: size > 16 ? 2 : 1. */
|
|
10
|
+
width?: number;
|
|
11
|
+
/** Gap between avatar edge and ring. Default 0 (ring overlays the edge). */
|
|
12
|
+
gap?: number;
|
|
13
|
+
/** Gradient sweep direction for multi-color rings. Default 'diagonal'. */
|
|
14
|
+
gradientDirection?: AvatarRingGradientDirection;
|
|
15
|
+
}
|
|
4
16
|
export interface AvatarProps {
|
|
5
17
|
/**
|
|
6
18
|
* Flexible image source — accepts a URL string, an ImageSourcePropType
|
|
@@ -66,6 +78,24 @@ export interface AvatarProps {
|
|
|
66
78
|
* live treatments.
|
|
67
79
|
*/
|
|
68
80
|
liveColor?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Decorative ring drawn around the avatar. Pass a single color string for a
|
|
83
|
+
* solid ring, or an array of 2+ colors for a gradient ring (e.g. an
|
|
84
|
+
* Instagram-stories ring). The gradient form requires `react-native-svg`
|
|
85
|
+
* (falls back to a solid ring of the first color if it is not installed).
|
|
86
|
+
*
|
|
87
|
+
* When `gap` is 0 (default) the ring overlays the avatar edge and the
|
|
88
|
+
* component's footprint is unchanged. When `gap` > 0 the ring is drawn OUTSIDE
|
|
89
|
+
* the avatar and the rendered footprint grows to `size + 2*(width + gap)` with
|
|
90
|
+
* the avatar centered inside (matching how Instagram-stories rings sit outside
|
|
91
|
+
* the avatar).
|
|
92
|
+
*
|
|
93
|
+
* `live` is sugar over this same primitive: when set (and no explicit `ring`
|
|
94
|
+
* is passed) it renders a solid theme-`negative` ring plus the "LIVE" badge.
|
|
95
|
+
* An explicit `ring` always wins for geometry and colors; the badge stays
|
|
96
|
+
* controlled by `live`/`hideLiveBadge`.
|
|
97
|
+
*/
|
|
98
|
+
ring?: AvatarRingConfig;
|
|
69
99
|
testID?: string;
|
|
70
100
|
}
|
|
71
101
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/avatar/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAI1F,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEhD,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI,CAAC;IAC7C,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8FAA8F;IAC9F,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,wFAAwF;IACxF,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,sBAAsB;IACtB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,sCAAsC;IACtC,UAAU,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACnC,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oFAAoF;IACpF,eAAe,CAAC,EAAE,SAAS,CAAC;IAC5B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,8EAA8E;IAC9E,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/avatar/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAI1F,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEhD,2EAA2E;AAC3E,MAAM,MAAM,2BAA2B,GAAG,UAAU,GAAG,YAAY,GAAG,UAAU,CAAC;AAEjF,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,2BAA2B,CAAC;CACjD;AAED,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI,CAAC;IAC7C,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8FAA8F;IAC9F,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,wFAAwF;IACxF,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,sBAAsB;IACtB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,sCAAsC;IACtC,UAAU,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACnC,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oFAAoF;IACpF,eAAe,CAAC,EAAE,SAAS,CAAC;IAC5B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,8EAA8E;IAC9E,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;;;;;;OAgBG;IACH,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Avatar.d.ts","sourceRoot":"","sources":["../../../../src/avatar/Avatar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA0C,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"Avatar.d.ts","sourceRoot":"","sources":["../../../../src/avatar/Avatar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA0C,MAAM,OAAO,CAAC;AAY/D,OAAO,KAAK,EAAE,WAAW,EAAoB,MAAM,SAAS,CAAC;AA4X7D,eAAO,MAAM,MAAM,yCAAwB,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AvatarRingGradientDirection, AvatarShape } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Size of the box the ring occupies. With `gap === 0` the ring overlays the
|
|
5
|
+
* avatar edge and the box equals the avatar size (footprint unchanged). With
|
|
6
|
+
* `gap > 0` the ring sits OUTSIDE the avatar, so the box grows and the avatar is
|
|
7
|
+
* centered inside it. Exported so `Avatar` reserves the same footprint with a
|
|
8
|
+
* single source for the geometry formula.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getRingOuterSize(size: number, width: number, gap: number): number;
|
|
11
|
+
interface AvatarRingProps {
|
|
12
|
+
/** Avatar diameter in px (the inner content size, before any gap). */
|
|
13
|
+
size: number;
|
|
14
|
+
/** Matches the avatar shape. */
|
|
15
|
+
shape: AvatarShape;
|
|
16
|
+
/** Solid ring: one color. Gradient ring: 2+ colors. */
|
|
17
|
+
colors: string | string[];
|
|
18
|
+
/** Ring stroke width in px. */
|
|
19
|
+
width: number;
|
|
20
|
+
/** Gap between the avatar edge and the ring (0 = overlay the edge). */
|
|
21
|
+
gap: number;
|
|
22
|
+
/** Gradient sweep direction for multi-color rings. */
|
|
23
|
+
gradientDirection: AvatarRingGradientDirection;
|
|
24
|
+
}
|
|
25
|
+
export declare const AvatarRing: React.NamedExoticComponent<AvatarRingProps>;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=AvatarRing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AvatarRing.d.ts","sourceRoot":"","sources":["../../../../src/avatar/AvatarRing.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwB,MAAM,OAAO,CAAC;AAM7C,OAAO,KAAK,EAAE,2BAA2B,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAOxE;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjF;AAED,UAAU,eAAe;IACvB,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,KAAK,EAAE,WAAW,CAAC;IACnB,uDAAuD;IACvD,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,iBAAiB,EAAE,2BAA2B,CAAC;CAChD;AAmID,eAAO,MAAM,UAAU,6CAA4B,CAAC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { Avatar } from './Avatar';
|
|
2
2
|
export { AvatarPlaceholderProvider } from './placeholder-context';
|
|
3
3
|
export type { AvatarPlaceholderConfig } from './placeholder-context';
|
|
4
|
-
export type { AvatarProps, AvatarShape } from './types';
|
|
4
|
+
export type { AvatarProps, AvatarShape, AvatarRingConfig, AvatarRingGradientDirection, } from './types';
|
|
5
5
|
/** Default avatar placeholder image — use as `fallbackSource` on Avatar or in AvatarPlaceholderProvider */
|
|
6
6
|
export declare const defaultAvatarSource: {
|
|
7
7
|
uri: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/avatar/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAClE,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AACrE,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/avatar/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAClE,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AACrE,YAAY,EACV,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,2BAA2B,GAC5B,MAAM,SAAS,CAAC;AAMjB,2GAA2G;AAC3G,eAAO,MAAM,mBAAmB;;CAAgB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
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 declare const SQUIRCLE_PATH: string;
|
|
8
|
+
//# sourceMappingURL=squircle-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"squircle-path.d.ts","sourceRoot":"","sources":["../../../../src/avatar/squircle-path.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,aAAa,QAIyM,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazily loads `react-native-svg`, returning `null` when it is not installed
|
|
3
|
+
* (e.g. a web bundle that does not ship the optional peer). Shared by the
|
|
4
|
+
* squircle avatar image clip in `Avatar.tsx` and the `AvatarRing` so there is a
|
|
5
|
+
* single loader rather than one per call site.
|
|
6
|
+
*/
|
|
7
|
+
export declare const getSvgModule: () => typeof import("react-native-svg") | null;
|
|
8
|
+
//# sourceMappingURL=svg-module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"svg-module.d.ts","sourceRoot":"","sources":["../../../../src/avatar/svg-module.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,eAAO,MAAM,YAAY,gDAAiD,CAAC"}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { ImageSourcePropType, StyleProp, ViewStyle, ImageStyle } from 'react-native';
|
|
3
3
|
export type AvatarShape = 'circle' | 'squircle';
|
|
4
|
+
/** Gradient sweep direction for a multi-color {@link AvatarRingConfig}. */
|
|
5
|
+
export type AvatarRingGradientDirection = 'diagonal' | 'horizontal' | 'vertical';
|
|
6
|
+
export interface AvatarRingConfig {
|
|
7
|
+
/** Solid ring: one color. Gradient ring: 2+ colors. */
|
|
8
|
+
colors: string | string[];
|
|
9
|
+
/** Ring stroke width in px. Default: size > 16 ? 2 : 1. */
|
|
10
|
+
width?: number;
|
|
11
|
+
/** Gap between avatar edge and ring. Default 0 (ring overlays the edge). */
|
|
12
|
+
gap?: number;
|
|
13
|
+
/** Gradient sweep direction for multi-color rings. Default 'diagonal'. */
|
|
14
|
+
gradientDirection?: AvatarRingGradientDirection;
|
|
15
|
+
}
|
|
4
16
|
export interface AvatarProps {
|
|
5
17
|
/**
|
|
6
18
|
* Flexible image source — accepts a URL string, an ImageSourcePropType
|
|
@@ -66,6 +78,24 @@ export interface AvatarProps {
|
|
|
66
78
|
* live treatments.
|
|
67
79
|
*/
|
|
68
80
|
liveColor?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Decorative ring drawn around the avatar. Pass a single color string for a
|
|
83
|
+
* solid ring, or an array of 2+ colors for a gradient ring (e.g. an
|
|
84
|
+
* Instagram-stories ring). The gradient form requires `react-native-svg`
|
|
85
|
+
* (falls back to a solid ring of the first color if it is not installed).
|
|
86
|
+
*
|
|
87
|
+
* When `gap` is 0 (default) the ring overlays the avatar edge and the
|
|
88
|
+
* component's footprint is unchanged. When `gap` > 0 the ring is drawn OUTSIDE
|
|
89
|
+
* the avatar and the rendered footprint grows to `size + 2*(width + gap)` with
|
|
90
|
+
* the avatar centered inside (matching how Instagram-stories rings sit outside
|
|
91
|
+
* the avatar).
|
|
92
|
+
*
|
|
93
|
+
* `live` is sugar over this same primitive: when set (and no explicit `ring`
|
|
94
|
+
* is passed) it renders a solid theme-`negative` ring plus the "LIVE" badge.
|
|
95
|
+
* An explicit `ring` always wins for geometry and colors; the badge stays
|
|
96
|
+
* controlled by `live`/`hideLiveBadge`.
|
|
97
|
+
*/
|
|
98
|
+
ring?: AvatarRingConfig;
|
|
69
99
|
testID?: string;
|
|
70
100
|
}
|
|
71
101
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/avatar/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAI1F,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEhD,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI,CAAC;IAC7C,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8FAA8F;IAC9F,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,wFAAwF;IACxF,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,sBAAsB;IACtB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,sCAAsC;IACtC,UAAU,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACnC,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oFAAoF;IACpF,eAAe,CAAC,EAAE,SAAS,CAAC;IAC5B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,8EAA8E;IAC9E,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/avatar/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAI1F,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEhD,2EAA2E;AAC3E,MAAM,MAAM,2BAA2B,GAAG,UAAU,GAAG,YAAY,GAAG,UAAU,CAAC;AAEjF,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,2BAA2B,CAAC;CACjD;AAED,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI,CAAC;IAC7C,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8FAA8F;IAC9F,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,wFAAwF;IACxF,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,sBAAsB;IACtB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,sCAAsC;IACtC,UAAU,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACnC,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oFAAoF;IACpF,eAAe,CAAC,EAAE,SAAS,CAAC;IAC5B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,8EAA8E;IAC9E,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;;;;;;OAgBG;IACH,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/bloom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -895,6 +895,7 @@
|
|
|
895
895
|
"typescript": "tsc --noEmit",
|
|
896
896
|
"pretypescript": "node scripts/generate-font-data.mjs",
|
|
897
897
|
"clean": "rm -rf lib",
|
|
898
|
+
"prepack": "bun run clean && bunx bob build",
|
|
898
899
|
"prepare": "bob build || true",
|
|
899
900
|
"release": "rm -rf lib && bun run build && release-it",
|
|
900
901
|
"storybook": "storybook dev -p 6006 --no-open",
|
|
@@ -19,6 +19,7 @@ const meta: Meta<typeof Avatar> = {
|
|
|
19
19
|
verified: { control: 'boolean' },
|
|
20
20
|
live: { control: 'boolean' },
|
|
21
21
|
hideLiveBadge: { control: 'boolean' },
|
|
22
|
+
ring: { control: 'object' },
|
|
22
23
|
},
|
|
23
24
|
};
|
|
24
25
|
|
|
@@ -29,6 +30,13 @@ type Story = StoryObj<typeof Avatar>;
|
|
|
29
30
|
const SAMPLE_URI =
|
|
30
31
|
'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200&h=200&fit=crop';
|
|
31
32
|
|
|
33
|
+
// Sample Instagram-stories-style gradient, defined locally in the story only —
|
|
34
|
+
// Bloom ships no such palette (it stays brand-neutral). Consumers supply their
|
|
35
|
+
// own ring colors via the `ring` prop.
|
|
36
|
+
const STORY_GRADIENT = ['#F58529', '#DD2A7B', '#8134AF', '#515BD4'];
|
|
37
|
+
// Muted gray used by IG for an already-seen story.
|
|
38
|
+
const SEEN_GRAY = '#C7C7CC';
|
|
39
|
+
|
|
32
40
|
export const Basic: Story = {
|
|
33
41
|
args: { size: 64, name: 'Nate Isern' },
|
|
34
42
|
};
|
|
@@ -118,3 +126,90 @@ export const LiveGallery: Story = {
|
|
|
118
126
|
),
|
|
119
127
|
name: 'Live (gallery)',
|
|
120
128
|
};
|
|
129
|
+
|
|
130
|
+
export const GradientRing: Story = {
|
|
131
|
+
args: {
|
|
132
|
+
size: 72,
|
|
133
|
+
uri: SAMPLE_URI,
|
|
134
|
+
ring: { colors: STORY_GRADIENT, width: 3, gap: 3 },
|
|
135
|
+
},
|
|
136
|
+
name: 'Gradient ring (stories)',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const SeenVsUnseen: Story = {
|
|
140
|
+
render: () => (
|
|
141
|
+
<View style={{ flexDirection: 'row', gap: 20, alignItems: 'center' }}>
|
|
142
|
+
{/* Unseen → gradient ring. */}
|
|
143
|
+
<Avatar
|
|
144
|
+
size={72}
|
|
145
|
+
uri={SAMPLE_URI}
|
|
146
|
+
ring={{ colors: STORY_GRADIENT, width: 3, gap: 3 }}
|
|
147
|
+
/>
|
|
148
|
+
{/* Seen → muted solid gray ring. */}
|
|
149
|
+
<Avatar
|
|
150
|
+
size={72}
|
|
151
|
+
uri={SAMPLE_URI}
|
|
152
|
+
ring={{ colors: SEEN_GRAY, width: 3, gap: 3 }}
|
|
153
|
+
/>
|
|
154
|
+
</View>
|
|
155
|
+
),
|
|
156
|
+
name: 'Seen vs unseen',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const RingWithGap: Story = {
|
|
160
|
+
render: () => (
|
|
161
|
+
<View style={{ flexDirection: 'row', gap: 24, alignItems: 'center' }}>
|
|
162
|
+
{/* gap 0 → overlays the edge, footprint unchanged. */}
|
|
163
|
+
<Avatar size={72} uri={SAMPLE_URI} ring={{ colors: STORY_GRADIENT, width: 3, gap: 0 }} />
|
|
164
|
+
{/* gap 4 → sits outside the avatar, footprint grows. */}
|
|
165
|
+
<Avatar size={72} uri={SAMPLE_URI} ring={{ colors: STORY_GRADIENT, width: 3, gap: 4 }} />
|
|
166
|
+
{/* gap 8 → wider clearance. */}
|
|
167
|
+
<Avatar size={72} uri={SAMPLE_URI} ring={{ colors: STORY_GRADIENT, width: 4, gap: 8 }} />
|
|
168
|
+
</View>
|
|
169
|
+
),
|
|
170
|
+
name: 'Ring with gap',
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const RingSquircle: Story = {
|
|
174
|
+
args: {
|
|
175
|
+
size: 72,
|
|
176
|
+
uri: SAMPLE_URI,
|
|
177
|
+
shape: 'squircle',
|
|
178
|
+
ring: { colors: STORY_GRADIENT, width: 3, gap: 3 },
|
|
179
|
+
},
|
|
180
|
+
name: 'Gradient ring (squircle)',
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const SolidRing: Story = {
|
|
184
|
+
render: () => (
|
|
185
|
+
<View style={{ flexDirection: 'row', gap: 20, alignItems: 'center' }}>
|
|
186
|
+
<Avatar size={72} uri={SAMPLE_URI} ring={{ colors: '#1A73E8', width: 3 }} />
|
|
187
|
+
<Avatar size={72} uri={SAMPLE_URI} shape="squircle" ring={{ colors: '#1A73E8', width: 3 }} />
|
|
188
|
+
<Avatar size={72} uri={SAMPLE_URI} ring={{ colors: '#1A73E8', width: 3, gap: 4 }} />
|
|
189
|
+
</View>
|
|
190
|
+
),
|
|
191
|
+
name: 'Solid ring',
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const GradientDirections: Story = {
|
|
195
|
+
render: () => (
|
|
196
|
+
<View style={{ flexDirection: 'row', gap: 20, alignItems: 'center' }}>
|
|
197
|
+
<Avatar
|
|
198
|
+
size={72}
|
|
199
|
+
uri={SAMPLE_URI}
|
|
200
|
+
ring={{ colors: STORY_GRADIENT, width: 3, gap: 3, gradientDirection: 'diagonal' }}
|
|
201
|
+
/>
|
|
202
|
+
<Avatar
|
|
203
|
+
size={72}
|
|
204
|
+
uri={SAMPLE_URI}
|
|
205
|
+
ring={{ colors: STORY_GRADIENT, width: 3, gap: 3, gradientDirection: 'horizontal' }}
|
|
206
|
+
/>
|
|
207
|
+
<Avatar
|
|
208
|
+
size={72}
|
|
209
|
+
uri={SAMPLE_URI}
|
|
210
|
+
ring={{ colors: STORY_GRADIENT, width: 3, gap: 3, gradientDirection: 'vertical' }}
|
|
211
|
+
/>
|
|
212
|
+
</View>
|
|
213
|
+
),
|
|
214
|
+
name: 'Gradient directions',
|
|
215
|
+
};
|
package/src/avatar/Avatar.tsx
CHANGED
|
@@ -5,10 +5,12 @@ import type { TextStyle } from 'react-native';
|
|
|
5
5
|
import { useTheme } from '../theme/use-theme';
|
|
6
6
|
import { useImageResolver } from '../image-resolver/context';
|
|
7
7
|
import { Z_INDEX } from '../styles/z-index';
|
|
8
|
-
import { lazyRequire } from '../utils/lazy-require';
|
|
9
8
|
import { useAvatarPlaceholder } from './placeholder-context';
|
|
10
9
|
import { LiveBadge } from './LiveBadge';
|
|
11
|
-
import
|
|
10
|
+
import { AvatarRing, getRingOuterSize } from './AvatarRing';
|
|
11
|
+
import { getSvgModule } from './svg-module';
|
|
12
|
+
import { SQUIRCLE_PATH } from './squircle-path';
|
|
13
|
+
import type { AvatarProps, AvatarRingConfig } from './types';
|
|
12
14
|
|
|
13
15
|
// Google Contacts-inspired palette used to pick a deterministic background
|
|
14
16
|
// color for name-based placeholder avatars.
|
|
@@ -40,32 +42,8 @@ function getNameColor(name: string): string {
|
|
|
40
42
|
// consumers compiling Bloom's source files directly.
|
|
41
43
|
import DEFAULT_AVATAR_IMAGE from './default-avatar';
|
|
42
44
|
|
|
43
|
-
// Squircle clip path normalized to 0–1 coordinate space (viewBox="0 0 1 1").
|
|
44
|
-
const SQUIRCLE_PATH =
|
|
45
|
-
'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' +
|
|
46
|
-
' 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' +
|
|
47
|
-
' 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' +
|
|
48
|
-
' 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';
|
|
49
|
-
|
|
50
45
|
let clipIdCounter = 0;
|
|
51
46
|
|
|
52
|
-
// Lazy-loaded SVG components for squircle shape.
|
|
53
|
-
// Returns null if react-native-svg is not installed.
|
|
54
|
-
type SvgModuleType = typeof import('react-native-svg');
|
|
55
|
-
const getSvgModule = lazyRequire<SvgModuleType>('react-native-svg');
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Live-ring drawing config passed down to shape renderers.
|
|
59
|
-
* - `color`: resolved ring color (theme `negative` or `liveColor` override).
|
|
60
|
-
* - `strokeWidth`: stroke width in the squircle's 0–1 path coordinate space.
|
|
61
|
-
* - `borderWidth`: ring border width in pixels for the circle fallback.
|
|
62
|
-
*/
|
|
63
|
-
interface LiveRingConfig {
|
|
64
|
-
color: string;
|
|
65
|
-
strokeWidth: number;
|
|
66
|
-
borderWidth: number;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
47
|
function SquircleImage({
|
|
70
48
|
uri,
|
|
71
49
|
fallbackSource,
|
|
@@ -74,7 +52,6 @@ function SquircleImage({
|
|
|
74
52
|
placeholderIcon,
|
|
75
53
|
name,
|
|
76
54
|
onError,
|
|
77
|
-
liveRing,
|
|
78
55
|
}: {
|
|
79
56
|
uri?: string;
|
|
80
57
|
fallbackSource?: AvatarProps['fallbackSource'];
|
|
@@ -83,34 +60,23 @@ function SquircleImage({
|
|
|
83
60
|
placeholderIcon?: React.ReactNode;
|
|
84
61
|
name?: string;
|
|
85
62
|
onError: () => void;
|
|
86
|
-
liveRing?: LiveRingConfig;
|
|
87
63
|
}) {
|
|
64
|
+
const clipId = useMemo(() => `bloom-sqc${clipIdCounter++}`, []);
|
|
88
65
|
const svg = getSvgModule();
|
|
89
66
|
if (!svg) {
|
|
90
|
-
// Fallback to circle if react-native-svg is not installed. The
|
|
91
|
-
//
|
|
67
|
+
// Fallback to a circle if react-native-svg is not installed. The ring (if
|
|
68
|
+
// any) is drawn separately by AvatarRing, which also degrades to a circle.
|
|
92
69
|
return (
|
|
93
|
-
|
|
94
|
-
<CircleFallback size={size} fallbackColor={fallbackColor} icon={placeholderIcon} name={name} />
|
|
95
|
-
{liveRing && (
|
|
96
|
-
<CircleLiveRing radius={size / 2} color={liveRing.color} borderWidth={liveRing.borderWidth} />
|
|
97
|
-
)}
|
|
98
|
-
</>
|
|
70
|
+
<CircleFallback size={size} fallbackColor={fallbackColor} icon={placeholderIcon} name={name} />
|
|
99
71
|
);
|
|
100
72
|
}
|
|
101
73
|
|
|
102
74
|
const { default: Svg, Defs, ClipPath, Path, Image: SvgImage } = svg;
|
|
103
|
-
const clipId = useMemo(() => `bloom-sqc${clipIdCounter++}`, []);
|
|
104
75
|
|
|
105
76
|
const href = uri ? { uri } : fallbackSource;
|
|
106
77
|
if (!href) {
|
|
107
78
|
return (
|
|
108
|
-
|
|
109
|
-
<CircleFallback size={size} fallbackColor={fallbackColor} icon={placeholderIcon} name={name} />
|
|
110
|
-
{liveRing && (
|
|
111
|
-
<CircleLiveRing radius={size / 2} color={liveRing.color} borderWidth={liveRing.borderWidth} />
|
|
112
|
-
)}
|
|
113
|
-
</>
|
|
79
|
+
<CircleFallback size={size} fallbackColor={fallbackColor} icon={placeholderIcon} name={name} />
|
|
114
80
|
);
|
|
115
81
|
}
|
|
116
82
|
|
|
@@ -137,18 +103,6 @@ function SquircleImage({
|
|
|
137
103
|
preserveAspectRatio="xMidYMid slice"
|
|
138
104
|
clipPath={`url(#${clipId})`}
|
|
139
105
|
/>
|
|
140
|
-
{/* Live ring: the squircle outline stroked in the ring color. The
|
|
141
|
-
path lives in 0–1 space, so the stroke width is expressed in the
|
|
142
|
-
same units; the outer half is clipped by the SVG viewport, leaving
|
|
143
|
-
a ~borderWidth-px inset ring on the squircle edge. */}
|
|
144
|
-
{liveRing && (
|
|
145
|
-
<Path
|
|
146
|
-
d={SQUIRCLE_PATH}
|
|
147
|
-
fill="none"
|
|
148
|
-
stroke={liveRing.color}
|
|
149
|
-
strokeWidth={liveRing.strokeWidth}
|
|
150
|
-
/>
|
|
151
|
-
)}
|
|
152
106
|
</Svg>
|
|
153
107
|
</>
|
|
154
108
|
);
|
|
@@ -208,37 +162,6 @@ function CircleFallback({
|
|
|
208
162
|
);
|
|
209
163
|
}
|
|
210
164
|
|
|
211
|
-
/**
|
|
212
|
-
* Solid live ring for circular avatars — an absolutely-positioned inset-0
|
|
213
|
-
* overlay whose border overlays the avatar edge (drawn on top of the image,
|
|
214
|
-
* never a border on the image itself). Also used as the fallback ring for a
|
|
215
|
-
* squircle avatar when react-native-svg is unavailable.
|
|
216
|
-
*/
|
|
217
|
-
function CircleLiveRing({
|
|
218
|
-
radius,
|
|
219
|
-
color,
|
|
220
|
-
borderWidth,
|
|
221
|
-
}: {
|
|
222
|
-
radius: number;
|
|
223
|
-
color: string;
|
|
224
|
-
borderWidth: number;
|
|
225
|
-
}) {
|
|
226
|
-
return (
|
|
227
|
-
<View
|
|
228
|
-
style={[
|
|
229
|
-
StyleSheet.absoluteFill,
|
|
230
|
-
{
|
|
231
|
-
borderRadius: radius,
|
|
232
|
-
borderColor: color,
|
|
233
|
-
borderWidth,
|
|
234
|
-
zIndex: Z_INDEX.raised,
|
|
235
|
-
pointerEvents: 'none',
|
|
236
|
-
},
|
|
237
|
-
]}
|
|
238
|
-
/>
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
165
|
const AvatarComponent: React.FC<AvatarProps> = ({
|
|
243
166
|
source,
|
|
244
167
|
uri,
|
|
@@ -258,6 +181,7 @@ const AvatarComponent: React.FC<AvatarProps> = ({
|
|
|
258
181
|
hideLiveBadge = false,
|
|
259
182
|
liveLabel = 'LIVE',
|
|
260
183
|
liveColor,
|
|
184
|
+
ring,
|
|
261
185
|
testID,
|
|
262
186
|
}) => {
|
|
263
187
|
const [errored, setErrored] = useState(false);
|
|
@@ -323,25 +247,40 @@ const AvatarComponent: React.FC<AvatarProps> = ({
|
|
|
323
247
|
return fallbackSource;
|
|
324
248
|
}, [effectiveUri, resolvedImageSource, fallbackSource]);
|
|
325
249
|
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
?
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
250
|
+
// Resolve a SINGLE ring config shared by `live` and the public `ring` prop.
|
|
251
|
+
// An explicit `ring` wins for geometry and colors; otherwise `live` derives a
|
|
252
|
+
// solid theme-`negative` ring (modeled on Bluesky's live indicator). The
|
|
253
|
+
// "LIVE" badge stays independently controlled by `live` / `hideLiveBadge` and
|
|
254
|
+
// uses the theme `negative` color regardless of the ring's colors.
|
|
255
|
+
const liveBadgeColor = liveColor ?? theme.colors.negative;
|
|
256
|
+
const resolvedRing: AvatarRingConfig | undefined = ring
|
|
257
|
+
? ring
|
|
258
|
+
: live
|
|
259
|
+
? { colors: liveBadgeColor, width: size > 16 ? 2 : 1, gap: 0 }
|
|
260
|
+
: undefined;
|
|
261
|
+
const ringWidth = resolvedRing?.width ?? (size > 16 ? 2 : 1);
|
|
262
|
+
const ringGap = resolvedRing?.gap ?? 0;
|
|
263
|
+
// When gap > 0 the ring sits outside the avatar and the footprint grows so the
|
|
264
|
+
// avatar can be centered inside; gap 0 overlays the edge (footprint unchanged).
|
|
265
|
+
const outer = resolvedRing ? getRingOuterSize(size, ringWidth, ringGap) : size;
|
|
341
266
|
const showLiveBadge = live && size > 16 && !hideLiveBadge;
|
|
342
267
|
|
|
343
|
-
const
|
|
344
|
-
<
|
|
268
|
+
const ringElement = resolvedRing ? (
|
|
269
|
+
<AvatarRing
|
|
270
|
+
size={size}
|
|
271
|
+
shape={shape}
|
|
272
|
+
colors={resolvedRing.colors}
|
|
273
|
+
width={ringWidth}
|
|
274
|
+
gap={ringGap}
|
|
275
|
+
gradientDirection={resolvedRing.gradientDirection ?? 'diagonal'}
|
|
276
|
+
/>
|
|
277
|
+
) : null;
|
|
278
|
+
|
|
279
|
+
// The avatar box holds the image and the badges (which hug the avatar). When
|
|
280
|
+
// gap > 0 this box is centered inside the larger container; when gap is 0 it
|
|
281
|
+
// fills the container exactly.
|
|
282
|
+
const avatarBox = (
|
|
283
|
+
<View style={[styles.avatarBox, { width: size, height: size }]}>
|
|
345
284
|
{shape === 'squircle' ? (
|
|
346
285
|
<SquircleImage
|
|
347
286
|
uri={effectiveUri}
|
|
@@ -351,7 +290,6 @@ const AvatarComponent: React.FC<AvatarProps> = ({
|
|
|
351
290
|
placeholderIcon={resolvedPlaceholderIcon}
|
|
352
291
|
name={name}
|
|
353
292
|
onError={() => setErrored(true)}
|
|
354
|
-
liveRing={liveRing}
|
|
355
293
|
/>
|
|
356
294
|
) : (
|
|
357
295
|
<View style={[styles.imageContainer, { width: size, height: size, borderRadius: radius }]}>
|
|
@@ -373,11 +311,9 @@ const AvatarComponent: React.FC<AvatarProps> = ({
|
|
|
373
311
|
</View>
|
|
374
312
|
)}
|
|
375
313
|
|
|
376
|
-
{/*
|
|
377
|
-
|
|
378
|
-
{
|
|
379
|
-
<CircleLiveRing radius={radius} color={liveRingColor} borderWidth={liveBorderWidth} />
|
|
380
|
-
)}
|
|
314
|
+
{/* Overlay ring (gap 0) overlays the avatar edge, above the image but
|
|
315
|
+
below the badges. */}
|
|
316
|
+
{ringGap === 0 ? ringElement : null}
|
|
381
317
|
|
|
382
318
|
{verified && verifiedIcon && (
|
|
383
319
|
<View
|
|
@@ -397,13 +333,22 @@ const AvatarComponent: React.FC<AvatarProps> = ({
|
|
|
397
333
|
<LiveBadge
|
|
398
334
|
variant={size > 32 ? 'small' : 'tiny'}
|
|
399
335
|
label={liveLabel}
|
|
400
|
-
backgroundColor={
|
|
336
|
+
backgroundColor={liveBadgeColor}
|
|
401
337
|
textColor={theme.colors.negativeForeground}
|
|
402
338
|
/>
|
|
403
339
|
)}
|
|
404
340
|
</View>
|
|
405
341
|
);
|
|
406
342
|
|
|
343
|
+
const content = (
|
|
344
|
+
<View style={[styles.container, { width: outer, height: outer }, style]} testID={testID}>
|
|
345
|
+
{/* Outside ring (gap > 0) sits on the reserved perimeter, behind the
|
|
346
|
+
avatar box so the avatar and its badges paint on top. */}
|
|
347
|
+
{ringGap > 0 ? ringElement : null}
|
|
348
|
+
{avatarBox}
|
|
349
|
+
</View>
|
|
350
|
+
);
|
|
351
|
+
|
|
407
352
|
if (onPress) {
|
|
408
353
|
return <TouchableOpacity onPress={onPress}>{content}</TouchableOpacity>;
|
|
409
354
|
}
|
|
@@ -418,6 +363,12 @@ const styles = StyleSheet.create({
|
|
|
418
363
|
justifyContent: 'center',
|
|
419
364
|
alignItems: 'center',
|
|
420
365
|
},
|
|
366
|
+
avatarBox: {
|
|
367
|
+
position: 'relative',
|
|
368
|
+
overflow: 'visible',
|
|
369
|
+
justifyContent: 'center',
|
|
370
|
+
alignItems: 'center',
|
|
371
|
+
},
|
|
421
372
|
imageContainer: {
|
|
422
373
|
overflow: 'hidden',
|
|
423
374
|
justifyContent: 'center',
|