@oxyhq/bloom 0.34.3 → 0.35.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-group/AvatarGroup.js +3 -2
- package/lib/commonjs/avatar-group/AvatarGroup.js.map +1 -1
- package/lib/commonjs/avatar-group/AvatarGroup.web.js +5 -5
- package/lib/commonjs/avatar-group/AvatarGroupBase.js +217 -48
- package/lib/commonjs/avatar-group/AvatarGroupBase.js.map +1 -1
- package/lib/commonjs/avatar-group/cluster-layout.js +217 -0
- package/lib/commonjs/avatar-group/cluster-layout.js.map +1 -0
- package/lib/module/avatar-group/AvatarGroup.js +3 -2
- package/lib/module/avatar-group/AvatarGroup.js.map +1 -1
- package/lib/module/avatar-group/AvatarGroup.web.js +5 -5
- package/lib/module/avatar-group/AvatarGroupBase.js +217 -48
- package/lib/module/avatar-group/AvatarGroupBase.js.map +1 -1
- package/lib/module/avatar-group/cluster-layout.js +213 -0
- package/lib/module/avatar-group/cluster-layout.js.map +1 -0
- package/lib/typescript/commonjs/avatar-group/AvatarGroup.d.ts.map +1 -1
- package/lib/typescript/commonjs/avatar-group/AvatarGroupBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/avatar-group/cluster-layout.d.ts +48 -0
- package/lib/typescript/commonjs/avatar-group/cluster-layout.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar-group/types.d.ts +23 -5
- package/lib/typescript/commonjs/avatar-group/types.d.ts.map +1 -1
- package/lib/typescript/module/avatar-group/AvatarGroup.d.ts.map +1 -1
- package/lib/typescript/module/avatar-group/AvatarGroupBase.d.ts.map +1 -1
- package/lib/typescript/module/avatar-group/cluster-layout.d.ts +48 -0
- package/lib/typescript/module/avatar-group/cluster-layout.d.ts.map +1 -0
- package/lib/typescript/module/avatar-group/types.d.ts +23 -5
- package/lib/typescript/module/avatar-group/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/AvatarGroupCluster.test.tsx +123 -0
- package/src/avatar-group/AvatarGroup.stories.tsx +68 -0
- package/src/avatar-group/AvatarGroup.tsx +3 -2
- package/src/avatar-group/AvatarGroup.web.tsx +5 -5
- package/src/avatar-group/AvatarGroupBase.tsx +298 -89
- package/src/avatar-group/cluster-layout.ts +204 -0
- package/src/avatar-group/types.ts +23 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["GOLDEN_ANGLE","Math","PI","sqrt","ITERATIONS","CLEANUP_ITERATIONS","CENTERING","SEPARATION_STRENGTH","LAYOUT_GAP","SEED_SPACING","PRIMARY_RADIUS","SECONDARY_MAX","SECONDARY_MIN","EPSILON","relativeRadius","index","count","t","separate","xs","ys","radii","i","j","dx","dy","dist","hypot","minDist","a","cos","sin","overlap","nx","ny","packCluster","Array","seedR","angle","iter","bound","reach","scale","bubbles","cx","cy","d","computeClusterLayout"],"sourceRoot":"../../../src","sources":["avatar-group/cluster-layout.ts"],"mappings":"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","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AvatarGroup.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/AvatarGroup.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAe,MAAM,OAAO,CAAC;AAGpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"AvatarGroup.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/AvatarGroup.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAe,MAAM,OAAO,CAAC;AAGpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAehD,eAAO,MAAM,WAAW,8CAA6B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AvatarGroupBase.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/AvatarGroupBase.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwB,MAAM,OAAO,CAAC;AAC7C,OAAO,EAIL,IAAI,
|
|
1
|
+
{"version":3,"file":"AvatarGroupBase.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/AvatarGroupBase.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwB,MAAM,OAAO,CAAC;AAC7C,OAAO,EAIL,IAAI,EAIL,MAAM,cAAc,CAAC;AAMtB,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AA0BjE,6EAA6E;AAC7E,MAAM,WAAW,4BAA4B;IAC3C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5D,oEAAoE;IACpE,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;CAC9D;AAED,UAAU,oBAAqB,SAAQ,gBAAgB;IACrD,aAAa,CAAC,EAAE,4BAA4B,CAAC;CAC9C;AA4bD,eAAO,MAAM,eAAe,kDAAiC,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic organic circle-packing for the {@link AvatarGroup} `cluster`
|
|
3
|
+
* layout — the iMessage-style "magnetic bubble cluster" where several avatars of
|
|
4
|
+
* varying sizes nestle together inside a round bounding box (a large primary in
|
|
5
|
+
* the middle/front, smaller members packed around it with a uniform gap).
|
|
6
|
+
*
|
|
7
|
+
* For 4+ members the layout is produced by a small, FULLY DETERMINISTIC
|
|
8
|
+
* force-directed relaxation (no `Math.random`): the primary is pinned at the
|
|
9
|
+
* centre and the remaining members are seeded on a golden-angle spiral, then a
|
|
10
|
+
* fixed number of relaxation passes (a) pull every non-primary circle toward the
|
|
11
|
+
* centre and (b) push any two circles apart until they clear a uniform gap. A
|
|
12
|
+
* fixed iteration count + deterministic seed means the same `count` always
|
|
13
|
+
* yields byte-identical positions, so native and web render the cluster
|
|
14
|
+
* identically with no `onLayout`/DOM measurement. The very small counts (1, 2,
|
|
15
|
+
* 3) — where a relaxation degenerates into a line or a lone pair — use explicit
|
|
16
|
+
* iMessage-style arrangements instead.
|
|
17
|
+
*
|
|
18
|
+
* Output is resolution-independent: each bubble is expressed as a fraction of
|
|
19
|
+
* the group's bounding box (`cx`/`cy` centre, `d` diameter, all 0..1), so the
|
|
20
|
+
* consumer just multiplies by the pixel `size`. Every bubble is guaranteed to
|
|
21
|
+
* sit fully inside the `[0, 1]` box (the packed result is scaled + translated to
|
|
22
|
+
* fit), so the cluster drops in exactly where a single round Avatar would.
|
|
23
|
+
*/
|
|
24
|
+
/** A single packed bubble, expressed as fractions of the bounding box (0..1). */
|
|
25
|
+
export interface ClusterBubble {
|
|
26
|
+
/** Centre X as a fraction of the box width. */
|
|
27
|
+
cx: number;
|
|
28
|
+
/** Centre Y as a fraction of the box height. */
|
|
29
|
+
cy: number;
|
|
30
|
+
/** Diameter as a fraction of the box size. */
|
|
31
|
+
d: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Deterministic cluster layout for `count` bubbles, as box-fraction bubbles
|
|
35
|
+
* ordered primary-first. `count` includes the `+N` overflow bubble when present
|
|
36
|
+
* (it is simply the last, smallest member of the pack).
|
|
37
|
+
*
|
|
38
|
+
* - `<= 0` → empty.
|
|
39
|
+
* - `1` → a single bubble filling the box.
|
|
40
|
+
* - `2` → the iMessage "one in front, one behind" pair: a larger primary set
|
|
41
|
+
* low-left with a smaller member tucked behind it to the upper-right.
|
|
42
|
+
* - `3` → an iMessage-style triangle: the larger primary along the bottom with
|
|
43
|
+
* two smaller members above it.
|
|
44
|
+
* - `4+` → the deterministic force-directed pack (primary centred, the rest
|
|
45
|
+
* packed magnetically around it, denser as the count grows).
|
|
46
|
+
*/
|
|
47
|
+
export declare function computeClusterLayout(count: number): ClusterBubble[];
|
|
48
|
+
//# sourceMappingURL=cluster-layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cluster-layout.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/cluster-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,EAAE,EAAE,MAAM,CAAC;IACX,8CAA8C;IAC9C,CAAC,EAAE,MAAM,CAAC;CACX;AA4ID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAiBnE"}
|
|
@@ -28,14 +28,27 @@ export interface AvatarGroupProps {
|
|
|
28
28
|
items: AvatarGroupItem[];
|
|
29
29
|
/**
|
|
30
30
|
* How the avatars are arranged.
|
|
31
|
-
* - `'stack'` (default): overlapping facepile with a thin separator
|
|
32
|
-
* trailing `+N` overflow chip, and the first item on top.
|
|
31
|
+
* - `'stack'` (default): overlapping horizontal facepile with a thin separator
|
|
32
|
+
* ring, a trailing `+N` overflow chip, and the first item on top.
|
|
33
33
|
* - `'row'`: avatars placed adjacent with a positive `spacing` gap and NO
|
|
34
34
|
* separator ring — an "adjacent row" of icons (e.g. a top-tokens strip).
|
|
35
35
|
* Still collapses into the `+N` overflow chip at `max`.
|
|
36
|
+
* - `'cluster'`: a compact 2D "magnetic bubble cluster" (iMessage-style) —
|
|
37
|
+
* avatars of VARYING sizes deterministically packed into a round bounding
|
|
38
|
+
* box with a uniform gap: the first item (primary) is the largest and sits
|
|
39
|
+
* centred/in front, the rest nestle around it. Scales from 2 (a "front +
|
|
40
|
+
* behind" pair) up through a dense pack (~20), collapsing into a trailing
|
|
41
|
+
* `+N` bubble past `max`. Unlike the horizontal layouts, `size` is the
|
|
42
|
+
* overall box diameter (see `size`) and `max` defaults to 20 (see `max`), so
|
|
43
|
+
* it drops in where a single round Avatar would.
|
|
44
|
+
*/
|
|
45
|
+
layout?: 'stack' | 'row' | 'cluster';
|
|
46
|
+
/**
|
|
47
|
+
* Diameter in pixels. For `'stack'`/`'row'` this is the diameter of EACH
|
|
48
|
+
* avatar. For `'cluster'` this is the diameter of the whole bounding box (the
|
|
49
|
+
* packed bubbles are sized as fractions of it), so the cluster occupies the
|
|
50
|
+
* same footprint as a single `size`-px Avatar.
|
|
36
51
|
*/
|
|
37
|
-
layout?: 'stack' | 'row';
|
|
38
|
-
/** Diameter of each avatar in pixels. */
|
|
39
52
|
size?: number;
|
|
40
53
|
/**
|
|
41
54
|
* Rendition variant forwarded to each {@link Avatar}'s `variant` prop, which
|
|
@@ -44,7 +57,12 @@ export interface AvatarGroupProps {
|
|
|
44
57
|
* `'thumb'`; pass `undefined` to request full-size renditions.
|
|
45
58
|
*/
|
|
46
59
|
variant?: string;
|
|
47
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Maximum number of avatars to render before collapsing into the overflow
|
|
62
|
+
* chip. Defaults to 5 for `'stack'`/`'row'`, and to 20 for `'cluster'` (which
|
|
63
|
+
* packs densely). In `'cluster'` the cap is inclusive of the `+N` bubble: past
|
|
64
|
+
* the cap the last slot becomes the overflow bubble.
|
|
65
|
+
*/
|
|
48
66
|
max?: number;
|
|
49
67
|
/**
|
|
50
68
|
* Real total count of members. When provided and larger than the number of
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,SAAS,CAAC;IACrC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,KAAK,SAAS,CAAC;IACvE,gCAAgC;IAChC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AvatarGroup.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/AvatarGroup.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAe,MAAM,OAAO,CAAC;AAGpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"AvatarGroup.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/AvatarGroup.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAe,MAAM,OAAO,CAAC;AAGpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAehD,eAAO,MAAM,WAAW,8CAA6B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AvatarGroupBase.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/AvatarGroupBase.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwB,MAAM,OAAO,CAAC;AAC7C,OAAO,EAIL,IAAI,
|
|
1
|
+
{"version":3,"file":"AvatarGroupBase.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/AvatarGroupBase.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwB,MAAM,OAAO,CAAC;AAC7C,OAAO,EAIL,IAAI,EAIL,MAAM,cAAc,CAAC;AAMtB,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AA0BjE,6EAA6E;AAC7E,MAAM,WAAW,4BAA4B;IAC3C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5D,oEAAoE;IACpE,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;CAC9D;AAED,UAAU,oBAAqB,SAAQ,gBAAgB;IACrD,aAAa,CAAC,EAAE,4BAA4B,CAAC;CAC9C;AA4bD,eAAO,MAAM,eAAe,kDAAiC,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic organic circle-packing for the {@link AvatarGroup} `cluster`
|
|
3
|
+
* layout — the iMessage-style "magnetic bubble cluster" where several avatars of
|
|
4
|
+
* varying sizes nestle together inside a round bounding box (a large primary in
|
|
5
|
+
* the middle/front, smaller members packed around it with a uniform gap).
|
|
6
|
+
*
|
|
7
|
+
* For 4+ members the layout is produced by a small, FULLY DETERMINISTIC
|
|
8
|
+
* force-directed relaxation (no `Math.random`): the primary is pinned at the
|
|
9
|
+
* centre and the remaining members are seeded on a golden-angle spiral, then a
|
|
10
|
+
* fixed number of relaxation passes (a) pull every non-primary circle toward the
|
|
11
|
+
* centre and (b) push any two circles apart until they clear a uniform gap. A
|
|
12
|
+
* fixed iteration count + deterministic seed means the same `count` always
|
|
13
|
+
* yields byte-identical positions, so native and web render the cluster
|
|
14
|
+
* identically with no `onLayout`/DOM measurement. The very small counts (1, 2,
|
|
15
|
+
* 3) — where a relaxation degenerates into a line or a lone pair — use explicit
|
|
16
|
+
* iMessage-style arrangements instead.
|
|
17
|
+
*
|
|
18
|
+
* Output is resolution-independent: each bubble is expressed as a fraction of
|
|
19
|
+
* the group's bounding box (`cx`/`cy` centre, `d` diameter, all 0..1), so the
|
|
20
|
+
* consumer just multiplies by the pixel `size`. Every bubble is guaranteed to
|
|
21
|
+
* sit fully inside the `[0, 1]` box (the packed result is scaled + translated to
|
|
22
|
+
* fit), so the cluster drops in exactly where a single round Avatar would.
|
|
23
|
+
*/
|
|
24
|
+
/** A single packed bubble, expressed as fractions of the bounding box (0..1). */
|
|
25
|
+
export interface ClusterBubble {
|
|
26
|
+
/** Centre X as a fraction of the box width. */
|
|
27
|
+
cx: number;
|
|
28
|
+
/** Centre Y as a fraction of the box height. */
|
|
29
|
+
cy: number;
|
|
30
|
+
/** Diameter as a fraction of the box size. */
|
|
31
|
+
d: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Deterministic cluster layout for `count` bubbles, as box-fraction bubbles
|
|
35
|
+
* ordered primary-first. `count` includes the `+N` overflow bubble when present
|
|
36
|
+
* (it is simply the last, smallest member of the pack).
|
|
37
|
+
*
|
|
38
|
+
* - `<= 0` → empty.
|
|
39
|
+
* - `1` → a single bubble filling the box.
|
|
40
|
+
* - `2` → the iMessage "one in front, one behind" pair: a larger primary set
|
|
41
|
+
* low-left with a smaller member tucked behind it to the upper-right.
|
|
42
|
+
* - `3` → an iMessage-style triangle: the larger primary along the bottom with
|
|
43
|
+
* two smaller members above it.
|
|
44
|
+
* - `4+` → the deterministic force-directed pack (primary centred, the rest
|
|
45
|
+
* packed magnetically around it, denser as the count grows).
|
|
46
|
+
*/
|
|
47
|
+
export declare function computeClusterLayout(count: number): ClusterBubble[];
|
|
48
|
+
//# sourceMappingURL=cluster-layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cluster-layout.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/cluster-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,EAAE,EAAE,MAAM,CAAC;IACX,8CAA8C;IAC9C,CAAC,EAAE,MAAM,CAAC;CACX;AA4ID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAiBnE"}
|
|
@@ -28,14 +28,27 @@ export interface AvatarGroupProps {
|
|
|
28
28
|
items: AvatarGroupItem[];
|
|
29
29
|
/**
|
|
30
30
|
* How the avatars are arranged.
|
|
31
|
-
* - `'stack'` (default): overlapping facepile with a thin separator
|
|
32
|
-
* trailing `+N` overflow chip, and the first item on top.
|
|
31
|
+
* - `'stack'` (default): overlapping horizontal facepile with a thin separator
|
|
32
|
+
* ring, a trailing `+N` overflow chip, and the first item on top.
|
|
33
33
|
* - `'row'`: avatars placed adjacent with a positive `spacing` gap and NO
|
|
34
34
|
* separator ring — an "adjacent row" of icons (e.g. a top-tokens strip).
|
|
35
35
|
* Still collapses into the `+N` overflow chip at `max`.
|
|
36
|
+
* - `'cluster'`: a compact 2D "magnetic bubble cluster" (iMessage-style) —
|
|
37
|
+
* avatars of VARYING sizes deterministically packed into a round bounding
|
|
38
|
+
* box with a uniform gap: the first item (primary) is the largest and sits
|
|
39
|
+
* centred/in front, the rest nestle around it. Scales from 2 (a "front +
|
|
40
|
+
* behind" pair) up through a dense pack (~20), collapsing into a trailing
|
|
41
|
+
* `+N` bubble past `max`. Unlike the horizontal layouts, `size` is the
|
|
42
|
+
* overall box diameter (see `size`) and `max` defaults to 20 (see `max`), so
|
|
43
|
+
* it drops in where a single round Avatar would.
|
|
44
|
+
*/
|
|
45
|
+
layout?: 'stack' | 'row' | 'cluster';
|
|
46
|
+
/**
|
|
47
|
+
* Diameter in pixels. For `'stack'`/`'row'` this is the diameter of EACH
|
|
48
|
+
* avatar. For `'cluster'` this is the diameter of the whole bounding box (the
|
|
49
|
+
* packed bubbles are sized as fractions of it), so the cluster occupies the
|
|
50
|
+
* same footprint as a single `size`-px Avatar.
|
|
36
51
|
*/
|
|
37
|
-
layout?: 'stack' | 'row';
|
|
38
|
-
/** Diameter of each avatar in pixels. */
|
|
39
52
|
size?: number;
|
|
40
53
|
/**
|
|
41
54
|
* Rendition variant forwarded to each {@link Avatar}'s `variant` prop, which
|
|
@@ -44,7 +57,12 @@ export interface AvatarGroupProps {
|
|
|
44
57
|
* `'thumb'`; pass `undefined` to request full-size renditions.
|
|
45
58
|
*/
|
|
46
59
|
variant?: string;
|
|
47
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Maximum number of avatars to render before collapsing into the overflow
|
|
62
|
+
* chip. Defaults to 5 for `'stack'`/`'row'`, and to 20 for `'cluster'` (which
|
|
63
|
+
* packs densely). In `'cluster'` the cap is inclusive of the `+N` bubble: past
|
|
64
|
+
* the cap the last slot becomes the overflow bubble.
|
|
65
|
+
*/
|
|
48
66
|
max?: number;
|
|
49
67
|
/**
|
|
50
68
|
* Real total count of members. When provided and larger than the number of
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/avatar-group/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,SAAS,CAAC;IACrC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,KAAK,SAAS,CAAC;IACvE,gCAAgC;IAChC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react-native';
|
|
3
|
+
|
|
4
|
+
import { BloomThemeProvider } from '../theme/BloomThemeProvider';
|
|
5
|
+
import { AvatarGroup } from '../avatar-group';
|
|
6
|
+
import type { AvatarGroupItem } from '../avatar-group';
|
|
7
|
+
import {
|
|
8
|
+
computeClusterLayout,
|
|
9
|
+
type ClusterBubble,
|
|
10
|
+
} from '../avatar-group/cluster-layout';
|
|
11
|
+
|
|
12
|
+
/** Returns bubble at `index` or throws — keeps the tests `!`/`as`-free under
|
|
13
|
+
* `noUncheckedIndexedAccess`. */
|
|
14
|
+
function bubbleAt(bubbles: readonly ClusterBubble[], index: number): ClusterBubble {
|
|
15
|
+
const bubble = bubbles[index];
|
|
16
|
+
if (!bubble) throw new Error(`expected a bubble at index ${index}`);
|
|
17
|
+
return bubble;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Nearest-neighbour edge gap for every bubble (negative = overlap). */
|
|
21
|
+
function minEdgeGap(bubbles: readonly ClusterBubble[]): number {
|
|
22
|
+
let worst = Infinity;
|
|
23
|
+
for (const a of bubbles) {
|
|
24
|
+
for (const b of bubbles) {
|
|
25
|
+
if (a === b) continue;
|
|
26
|
+
const centerDist = Math.hypot(a.cx - b.cx, a.cy - b.cy);
|
|
27
|
+
const gap = centerDist - (a.d / 2 + b.d / 2);
|
|
28
|
+
if (gap < worst) worst = gap;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return worst;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('computeClusterLayout', () => {
|
|
35
|
+
it('returns an empty layout for non-positive counts', () => {
|
|
36
|
+
expect(computeClusterLayout(0)).toEqual([]);
|
|
37
|
+
expect(computeClusterLayout(-3)).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns exactly `count` bubbles', () => {
|
|
41
|
+
for (let count = 1; count <= 20; count++) {
|
|
42
|
+
expect(computeClusterLayout(count)).toHaveLength(count);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('is fully deterministic (no Math.random)', () => {
|
|
47
|
+
for (const count of [2, 3, 4, 8, 20]) {
|
|
48
|
+
expect(computeClusterLayout(count)).toEqual(computeClusterLayout(count));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('keeps every bubble inside the unit box with no NaN', () => {
|
|
53
|
+
for (let count = 1; count <= 20; count++) {
|
|
54
|
+
const bubbles = computeClusterLayout(count);
|
|
55
|
+
for (const bubble of bubbles) {
|
|
56
|
+
expect(Number.isNaN(bubble.cx)).toBe(false);
|
|
57
|
+
expect(Number.isNaN(bubble.cy)).toBe(false);
|
|
58
|
+
expect(Number.isNaN(bubble.d)).toBe(false);
|
|
59
|
+
expect(bubble.cx - bubble.d / 2).toBeGreaterThanOrEqual(-1e-6);
|
|
60
|
+
expect(bubble.cx + bubble.d / 2).toBeLessThanOrEqual(1 + 1e-6);
|
|
61
|
+
expect(bubble.cy - bubble.d / 2).toBeGreaterThanOrEqual(-1e-6);
|
|
62
|
+
expect(bubble.cy + bubble.d / 2).toBeLessThanOrEqual(1 + 1e-6);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('makes the primary (index 0) the largest bubble', () => {
|
|
68
|
+
for (let count = 2; count <= 20; count++) {
|
|
69
|
+
const bubbles = computeClusterLayout(count);
|
|
70
|
+
const primary = bubbleAt(bubbles, 0);
|
|
71
|
+
for (const bubble of bubbles) {
|
|
72
|
+
expect(primary.d).toBeGreaterThanOrEqual(bubble.d - 1e-9);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('centres the primary for the force-packed counts (4+)', () => {
|
|
78
|
+
for (const count of [4, 5, 8, 12, 20]) {
|
|
79
|
+
const primary = bubbleAt(computeClusterLayout(count), 0);
|
|
80
|
+
expect(primary.cx).toBeCloseTo(0.5, 5);
|
|
81
|
+
expect(primary.cy).toBeCloseTo(0.5, 5);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('keeps a uniform, non-overlapping gap for 3+ members', () => {
|
|
86
|
+
// n=2 is the intentional "front + behind" overlap; 3+ pack with a gap.
|
|
87
|
+
for (let count = 3; count <= 20; count++) {
|
|
88
|
+
expect(minEdgeGap(computeClusterLayout(count))).toBeGreaterThan(-1e-3);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const CLUSTER_ITEMS: AvatarGroupItem[] = Array.from({ length: 25 }, (_, i) => ({
|
|
94
|
+
id: `c${i}`,
|
|
95
|
+
displayName: `Member ${i}`,
|
|
96
|
+
username: `member${i}`,
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
function renderCluster(ui: React.ReactElement) {
|
|
100
|
+
return render(<BloomThemeProvider mode="light" colorPreset="teal">{ui}</BloomThemeProvider>);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
describe('AvatarGroup cluster layout', () => {
|
|
104
|
+
it('renders a "+N" overflow bubble past the cap', () => {
|
|
105
|
+
// 25 members, cap 20 → 19 avatars + a "+6" bubble.
|
|
106
|
+
const { getByText } = renderCluster(
|
|
107
|
+
<AvatarGroup layout="cluster" items={CLUSTER_ITEMS} size={140} max={20} showInitials />,
|
|
108
|
+
);
|
|
109
|
+
expect(getByText('+6')).toBeTruthy();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('shows no overflow bubble when members fit under the cap', () => {
|
|
113
|
+
const { queryByText } = renderCluster(
|
|
114
|
+
<AvatarGroup
|
|
115
|
+
layout="cluster"
|
|
116
|
+
items={CLUSTER_ITEMS.slice(0, 4)}
|
|
117
|
+
size={64}
|
|
118
|
+
showInitials
|
|
119
|
+
/>,
|
|
120
|
+
);
|
|
121
|
+
expect(queryByText(/^\+/)).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -147,3 +147,71 @@ export const RowLayout: Story = {
|
|
|
147
147
|
),
|
|
148
148
|
name: 'Row layout',
|
|
149
149
|
};
|
|
150
|
+
|
|
151
|
+
// A larger pool of initials-only members to show the cluster densely packed.
|
|
152
|
+
const MANY: AvatarGroupItem[] = Array.from({ length: 25 }, (_, i) => ({
|
|
153
|
+
id: `c${i}`,
|
|
154
|
+
displayName: String.fromCharCode(65 + (i % 26)),
|
|
155
|
+
username: `user${i}`,
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Cluster layout: a compact iMessage-style "magnetic bubble cluster" of
|
|
160
|
+
* varying-size avatars packed into a round box (`size` = box diameter). The
|
|
161
|
+
* primary is largest and centred/in front; the rest nestle around it with a
|
|
162
|
+
* uniform gap. `showInitials` renders a colored letter for avatar-less members
|
|
163
|
+
* so the packing reads clearly. Shown at counts 3, 5, 8, 12, and 20.
|
|
164
|
+
*/
|
|
165
|
+
export const ClusterLayout: Story = {
|
|
166
|
+
render: () => (
|
|
167
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 24, alignItems: 'center' }}>
|
|
168
|
+
{[3, 5, 8, 12, 20].map((count) => (
|
|
169
|
+
<View key={count} style={{ alignItems: 'center', gap: 8 }}>
|
|
170
|
+
<AvatarGroup
|
|
171
|
+
layout="cluster"
|
|
172
|
+
items={MANY.slice(0, count)}
|
|
173
|
+
size={120}
|
|
174
|
+
showInitials
|
|
175
|
+
/>
|
|
176
|
+
<Text>{count}</Text>
|
|
177
|
+
</View>
|
|
178
|
+
))}
|
|
179
|
+
</View>
|
|
180
|
+
),
|
|
181
|
+
name: 'Cluster layout',
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Cluster with photos at the common collaborative-post sizes (2 = "front +
|
|
186
|
+
* behind", then 3 and 4).
|
|
187
|
+
*/
|
|
188
|
+
export const ClusterWithPhotos: Story = {
|
|
189
|
+
render: () => (
|
|
190
|
+
<View style={{ flexDirection: 'row', gap: 24, alignItems: 'center' }}>
|
|
191
|
+
{[2, 3, 4].map((count) => (
|
|
192
|
+
<AvatarGroup
|
|
193
|
+
key={count}
|
|
194
|
+
layout="cluster"
|
|
195
|
+
items={WITH_PHOTOS.slice(0, count)}
|
|
196
|
+
size={64}
|
|
197
|
+
showInitials
|
|
198
|
+
/>
|
|
199
|
+
))}
|
|
200
|
+
</View>
|
|
201
|
+
),
|
|
202
|
+
name: 'Cluster with photos',
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Cluster overflow: 25 members with `max={20}` shows 19 avatars + a trailing
|
|
207
|
+
* "+N" bubble as the last (smallest) cluster member.
|
|
208
|
+
*/
|
|
209
|
+
export const ClusterOverflow: Story = {
|
|
210
|
+
render: () => (
|
|
211
|
+
<View style={{ flexDirection: 'row', gap: 24, alignItems: 'center' }}>
|
|
212
|
+
<AvatarGroup layout="cluster" items={MANY} size={140} max={20} showInitials />
|
|
213
|
+
<Text>25 members, max 20 → cap + "+N"</Text>
|
|
214
|
+
</View>
|
|
215
|
+
),
|
|
216
|
+
name: 'Cluster overflow',
|
|
217
|
+
};
|
|
@@ -4,8 +4,9 @@ import { AvatarGroupBase } from './AvatarGroupBase';
|
|
|
4
4
|
import type { AvatarGroupProps } from './types';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Native `AvatarGroup` —
|
|
8
|
-
*
|
|
7
|
+
* Native `AvatarGroup` — {@link Avatar}s arranged as a horizontal facepile
|
|
8
|
+
* (`stack`), an adjacent `row`, or a 2D magnetic `cluster`, with a trailing
|
|
9
|
+
* `+N` overflow chip. See `layout` in {@link AvatarGroupProps}.
|
|
9
10
|
*
|
|
10
11
|
* Hover behavior is web-only; on native the `hoverCard` / `renderItemAction`
|
|
11
12
|
* props are accepted for API parity but intentionally do nothing (no hover on
|
|
@@ -28,11 +28,11 @@ function getItemName(item: AvatarGroupItem): string {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Web `AvatarGroup`. Identical
|
|
32
|
-
* {@link AvatarGroupBase}, plus an optional hover card: when
|
|
33
|
-
* enabled, hovering an avatar reveals a {@link UserHoverCard}
|
|
34
|
-
* it. The card receives the per-item `renderItemAction` slot
|
|
35
|
-
* its SDK FollowButton inside.
|
|
31
|
+
* Web `AvatarGroup`. Identical layout rendering to native (stack / row /
|
|
32
|
+
* cluster) via {@link AvatarGroupBase}, plus an optional hover card: when
|
|
33
|
+
* `hoverCard` is enabled, hovering an avatar reveals a {@link UserHoverCard}
|
|
34
|
+
* positioned beneath it. The card receives the per-item `renderItemAction` slot
|
|
35
|
+
* so the app can drop its SDK FollowButton inside.
|
|
36
36
|
*/
|
|
37
37
|
const AvatarGroupWebComponent: React.FC<AvatarGroupProps> = (props) => {
|
|
38
38
|
const { hoverCard, renderItemAction, onPressItem } = props;
|