@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.
Files changed (34) hide show
  1. package/lib/commonjs/avatar-group/AvatarGroup.js +3 -2
  2. package/lib/commonjs/avatar-group/AvatarGroup.js.map +1 -1
  3. package/lib/commonjs/avatar-group/AvatarGroup.web.js +5 -5
  4. package/lib/commonjs/avatar-group/AvatarGroupBase.js +217 -48
  5. package/lib/commonjs/avatar-group/AvatarGroupBase.js.map +1 -1
  6. package/lib/commonjs/avatar-group/cluster-layout.js +217 -0
  7. package/lib/commonjs/avatar-group/cluster-layout.js.map +1 -0
  8. package/lib/module/avatar-group/AvatarGroup.js +3 -2
  9. package/lib/module/avatar-group/AvatarGroup.js.map +1 -1
  10. package/lib/module/avatar-group/AvatarGroup.web.js +5 -5
  11. package/lib/module/avatar-group/AvatarGroupBase.js +217 -48
  12. package/lib/module/avatar-group/AvatarGroupBase.js.map +1 -1
  13. package/lib/module/avatar-group/cluster-layout.js +213 -0
  14. package/lib/module/avatar-group/cluster-layout.js.map +1 -0
  15. package/lib/typescript/commonjs/avatar-group/AvatarGroup.d.ts.map +1 -1
  16. package/lib/typescript/commonjs/avatar-group/AvatarGroupBase.d.ts.map +1 -1
  17. package/lib/typescript/commonjs/avatar-group/cluster-layout.d.ts +48 -0
  18. package/lib/typescript/commonjs/avatar-group/cluster-layout.d.ts.map +1 -0
  19. package/lib/typescript/commonjs/avatar-group/types.d.ts +23 -5
  20. package/lib/typescript/commonjs/avatar-group/types.d.ts.map +1 -1
  21. package/lib/typescript/module/avatar-group/AvatarGroup.d.ts.map +1 -1
  22. package/lib/typescript/module/avatar-group/AvatarGroupBase.d.ts.map +1 -1
  23. package/lib/typescript/module/avatar-group/cluster-layout.d.ts +48 -0
  24. package/lib/typescript/module/avatar-group/cluster-layout.d.ts.map +1 -0
  25. package/lib/typescript/module/avatar-group/types.d.ts +23 -5
  26. package/lib/typescript/module/avatar-group/types.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/__tests__/AvatarGroupCluster.test.tsx +123 -0
  29. package/src/avatar-group/AvatarGroup.stories.tsx +68 -0
  30. package/src/avatar-group/AvatarGroup.tsx +3 -2
  31. package/src/avatar-group/AvatarGroup.web.tsx +5 -5
  32. package/src/avatar-group/AvatarGroupBase.tsx +298 -89
  33. package/src/avatar-group/cluster-layout.ts +204 -0
  34. 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":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAUA;AACA;AACA,MAAMA,YAAY,GAAGC,IAAI,CAACC,EAAE,IAAI,CAAC,GAAGD,IAAI,CAACE,IAAI,CAAC,CAAC,CAAC,CAAC;AACjD;AACA;AACA,MAAMC,UAAU,GAAG,GAAG;AACtB,MAAMC,kBAAkB,GAAG,EAAE;AAC7B;AACA;AACA;AACA,MAAMC,SAAS,GAAG,IAAI;AACtB;AACA;AACA,MAAMC,mBAAmB,GAAG,CAAC;AAC7B;AACA;AACA,MAAMC,UAAU,GAAG,IAAI;AACvB;AACA;AACA,MAAMC,YAAY,GAAG,GAAG;AACxB;AACA;AACA;AACA,MAAMC,cAAc,GAAG,CAAC;AACxB,MAAMC,aAAa,GAAG,IAAI;AAC1B,MAAMC,aAAa,GAAG,GAAG;AACzB,MAAMC,OAAO,GAAG,IAAI;;AAEpB;AACA,SAASC,cAAcA,CAACC,KAAa,EAAEC,KAAa,EAAU;EAC5D,IAAID,KAAK,KAAK,CAAC,EAAE,OAAOL,cAAc;EACtC,IAAIM,KAAK,IAAI,CAAC,EAAE,OAAOL,aAAa;EACpC,MAAMM,CAAC,GAAG,CAACF,KAAK,GAAG,CAAC,KAAKC,KAAK,GAAG,CAAC,CAAC;EACnC,OAAOL,aAAa,GAAG,CAACC,aAAa,GAAGD,aAAa,IAAIM,CAAC;AAC5D;;AAEA;AACA;AACA;AACA;AACA;AACA,SAASC,QAAQA,CAACC,EAAY,EAAEC,EAAY,EAAEC,KAAe,EAAEL,KAAa,EAAQ;EAClF,KAAK,IAAIM,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGN,KAAK,EAAEM,CAAC,EAAE,EAAE;IAC9B,KAAK,IAAIC,CAAC,GAAGD,CAAC,GAAG,CAAC,EAAEC,CAAC,GAAGP,KAAK,EAAEO,CAAC,EAAE,EAAE;MAClC,IAAIC,EAAE,GAAG,CAACL,EAAE,CAACI,CAAC,CAAC,IAAI,CAAC,KAAKJ,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC,CAAC;MACpC,IAAIG,EAAE,GAAG,CAACL,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC,KAAKH,EAAE,CAACE,CAAC,CAAC,IAAI,CAAC,CAAC;MACpC,IAAII,IAAI,GAAGzB,IAAI,CAAC0B,KAAK,CAACH,EAAE,EAAEC,EAAE,CAAC;MAC7B,MAAMG,OAAO,GAAG,CAACP,KAAK,CAACC,CAAC,CAAC,IAAI,CAAC,KAAKD,KAAK,CAACE,CAAC,CAAC,IAAI,CAAC,CAAC,GAAGf,UAAU;MAC9D,IAAIkB,IAAI,IAAIE,OAAO,EAAE;MACrB,IAAIF,IAAI,GAAGb,OAAO,EAAE;QAClB;QACA;QACA,MAAMgB,CAAC,GAAG,CAACP,CAAC,GAAG,CAAC,IAAItB,YAAY,GAAGuB,CAAC;QACpCC,EAAE,GAAGvB,IAAI,CAAC6B,GAAG,CAACD,CAAC,CAAC;QAChBJ,EAAE,GAAGxB,IAAI,CAAC8B,GAAG,CAACF,CAAC,CAAC;QAChBH,IAAI,GAAG,CAAC;MACV;MACA,MAAMM,OAAO,GAAG,CAACJ,OAAO,GAAGF,IAAI,IAAInB,mBAAmB;MACtD,MAAM0B,EAAE,GAAGT,EAAE,GAAGE,IAAI;MACpB,MAAMQ,EAAE,GAAGT,EAAE,GAAGC,IAAI;MACpB,IAAIJ,CAAC,KAAK,CAAC,EAAE;QACX;QACAH,EAAE,CAACI,CAAC,CAAC,GAAG,CAACJ,EAAE,CAACI,CAAC,CAAC,IAAI,CAAC,IAAIU,EAAE,GAAGD,OAAO;QACnCZ,EAAE,CAACG,CAAC,CAAC,GAAG,CAACH,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC,IAAIW,EAAE,GAAGF,OAAO;MACrC,CAAC,MAAM;QACLb,EAAE,CAACG,CAAC,CAAC,GAAG,CAACH,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC,IAAKW,EAAE,GAAGD,OAAO,GAAI,CAAC;QACzCZ,EAAE,CAACE,CAAC,CAAC,GAAG,CAACF,EAAE,CAACE,CAAC,CAAC,IAAI,CAAC,IAAKY,EAAE,GAAGF,OAAO,GAAI,CAAC;QACzCb,EAAE,CAACI,CAAC,CAAC,GAAG,CAACJ,EAAE,CAACI,CAAC,CAAC,IAAI,CAAC,IAAKU,EAAE,GAAGD,OAAO,GAAI,CAAC;QACzCZ,EAAE,CAACG,CAAC,CAAC,GAAG,CAACH,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC,IAAKW,EAAE,GAAGF,OAAO,GAAI,CAAC;MAC3C;IACF;EACF;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,SAASG,WAAWA,CAACnB,KAAa,EAAmB;EACnD,MAAMK,KAAK,GAAG,IAAIe,KAAK,CAASpB,KAAK,CAAC;EACtC,MAAMG,EAAE,GAAG,IAAIiB,KAAK,CAASpB,KAAK,CAAC;EACnC,MAAMI,EAAE,GAAG,IAAIgB,KAAK,CAASpB,KAAK,CAAC;EAEnC,KAAK,IAAIM,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGN,KAAK,EAAEM,CAAC,EAAE,EAAE;IAC9BD,KAAK,CAACC,CAAC,CAAC,GAAGR,cAAc,CAACQ,CAAC,EAAEN,KAAK,CAAC;IACnC,IAAIM,CAAC,KAAK,CAAC,EAAE;MACXH,EAAE,CAACG,CAAC,CAAC,GAAG,CAAC;MACTF,EAAE,CAACE,CAAC,CAAC,GAAG,CAAC;IACX,CAAC,MAAM;MACL;MACA,MAAMe,KAAK,GAAG5B,YAAY,GAAGR,IAAI,CAACE,IAAI,CAACmB,CAAC,CAAC;MACzC,MAAMgB,KAAK,GAAGhB,CAAC,GAAGtB,YAAY;MAC9BmB,EAAE,CAACG,CAAC,CAAC,GAAGe,KAAK,GAAGpC,IAAI,CAAC6B,GAAG,CAACQ,KAAK,CAAC;MAC/BlB,EAAE,CAACE,CAAC,CAAC,GAAGe,KAAK,GAAGpC,IAAI,CAAC8B,GAAG,CAACO,KAAK,CAAC;IACjC;EACF;EAEA,KAAK,IAAIC,IAAI,GAAG,CAAC,EAAEA,IAAI,GAAGnC,UAAU,EAAEmC,IAAI,EAAE,EAAE;IAC5C;IACA,KAAK,IAAIjB,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGN,KAAK,EAAEM,CAAC,EAAE,EAAE;MAC9BH,EAAE,CAACG,CAAC,CAAC,GAAG,CAACH,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAGhB,SAAS,CAAC;MACtCc,EAAE,CAACE,CAAC,CAAC,GAAG,CAACF,EAAE,CAACE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAGhB,SAAS,CAAC;IACxC;IACA;IACAY,QAAQ,CAACC,EAAE,EAAEC,EAAE,EAAEC,KAAK,EAAEL,KAAK,CAAC;IAC9BG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;IACTC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;EACX;EACA;EACA,KAAK,IAAImB,IAAI,GAAG,CAAC,EAAEA,IAAI,GAAGlC,kBAAkB,EAAEkC,IAAI,EAAE,EAAE;IACpDrB,QAAQ,CAACC,EAAE,EAAEC,EAAE,EAAEC,KAAK,EAAEL,KAAK,CAAC;IAC9BG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;IACTC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;EACX;;EAEA;EACA;EACA;EACA,IAAIoB,KAAK,GAAG,CAAC;EACb,KAAK,IAAIlB,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGN,KAAK,EAAEM,CAAC,EAAE,EAAE;IAC9B,MAAMmB,KAAK,GAAGxC,IAAI,CAAC0B,KAAK,CAACR,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC,EAAEF,EAAE,CAACE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAID,KAAK,CAACC,CAAC,CAAC,IAAI,CAAC,CAAC;IAClE,IAAImB,KAAK,GAAGD,KAAK,EAAEA,KAAK,GAAGC,KAAK;EAClC;EACA,MAAMC,KAAK,GAAGF,KAAK,GAAG3B,OAAO,GAAG,GAAG,GAAG2B,KAAK,GAAG,GAAG;EAEjD,MAAMG,OAAO,GAAG,IAAIP,KAAK,CAAgBpB,KAAK,CAAC;EAC/C,KAAK,IAAIM,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGN,KAAK,EAAEM,CAAC,EAAE,EAAE;IAC9BqB,OAAO,CAACrB,CAAC,CAAC,GAAG;MACXsB,EAAE,EAAE,GAAG,GAAG,CAACzB,EAAE,CAACG,CAAC,CAAC,IAAI,CAAC,IAAIoB,KAAK;MAC9BG,EAAE,EAAE,GAAG,GAAG,CAACzB,EAAE,CAACE,CAAC,CAAC,IAAI,CAAC,IAAIoB,KAAK;MAC9BI,CAAC,EAAE,CAAC,IAAIzB,KAAK,CAACC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAGoB;IAC3B,CAAC;EACH;EACA,OAAOC,OAAO;AAChB;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASI,oBAAoBA,CAAC/B,KAAa,EAAmB;EACnE,IAAIA,KAAK,IAAI,CAAC,EAAE,OAAO,EAAE;EACzB,IAAIA,KAAK,KAAK,CAAC,EAAE,OAAO,CAAC;IAAE4B,EAAE,EAAE,GAAG;IAAEC,EAAE,EAAE,GAAG;IAAEC,CAAC,EAAE;EAAE,CAAC,CAAC;EACpD,IAAI9B,KAAK,KAAK,CAAC,EAAE;IACf,OAAO,CACL;MAAE4B,EAAE,EAAE,IAAI;MAAEC,EAAE,EAAE,IAAI;MAAEC,CAAC,EAAE;IAAK,CAAC,EAC/B;MAAEF,EAAE,EAAE,IAAI;MAAEC,EAAE,EAAE,IAAI;MAAEC,CAAC,EAAE;IAAI,CAAC,CAC/B;EACH;EACA,IAAI9B,KAAK,KAAK,CAAC,EAAE;IACf,OAAO,CACL;MAAE4B,EAAE,EAAE,GAAG;MAAEC,EAAE,EAAE,IAAI;MAAEC,CAAC,EAAE;IAAK,CAAC,EAC9B;MAAEF,EAAE,EAAE,KAAK;MAAEC,EAAE,EAAE,IAAI;MAAEC,CAAC,EAAE;IAAI,CAAC,EAC/B;MAAEF,EAAE,EAAE,KAAK;MAAEC,EAAE,EAAE,IAAI;MAAEC,CAAC,EAAE;IAAI,CAAC,CAChC;EACH;EACA,OAAOX,WAAW,CAACnB,KAAK,CAAC;AAC3B","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;AAchD,eAAO,MAAM,WAAW,8CAA6B,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,EAEL,MAAM,cAAc,CAAC;AAKtB,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAiBjE,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;AAuPD,eAAO,MAAM,eAAe,kDAAiC,CAAC"}
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 ring, a
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
- /** Maximum number of avatars to render before collapsing into the overflow chip. */
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;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IACzB,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,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
+ {"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;AAchD,eAAO,MAAM,WAAW,8CAA6B,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,EAEL,MAAM,cAAc,CAAC;AAKtB,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAiBjE,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;AAuPD,eAAO,MAAM,eAAe,kDAAiC,CAAC"}
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 ring, a
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
- /** Maximum number of avatars to render before collapsing into the overflow chip. */
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;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IACzB,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,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
+ {"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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.34.3",
3
+ "version": "0.35.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",
@@ -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 + &quot;+N&quot;</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` — an overlapping, ringed stack of {@link Avatar}s with a
8
- * trailing `+N` overflow chip.
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 stack rendering to native via
32
- * {@link AvatarGroupBase}, plus an optional hover card: when `hoverCard` is
33
- * enabled, hovering an avatar reveals a {@link UserHoverCard} positioned beneath
34
- * it. The card receives the per-item `renderItemAction` slot so the app can drop
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;