Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
9b6310b
feat: add avatar utility primitives
pawelgrimm May 26, 2026
ee84719
fix: preserve unicode avatar initials
pawelgrimm May 26, 2026
0aee3f2
fix: preserve avatar utility type checks
pawelgrimm May 26, 2026
0cdebab
fix: normalize avatar initials before comparison
pawelgrimm May 26, 2026
e9f0f0e
feat: replace avatar primitive API
pawelgrimm May 26, 2026
b815073
fix: retry avatar image after prop reset
pawelgrimm May 26, 2026
4d52372
docs: update avatar stories
pawelgrimm May 26, 2026
c69e709
docs: add avatar product wrappers
pawelgrimm May 26, 2026
2c93a0e
fix: preserve avatar initials and fallback examples
pawelgrimm May 26, 2026
8af1637
fix: cap avatar initials after uppercase expansion
pawelgrimm May 26, 2026
0b805e7
fix: address avatar review findings
pawelgrimm May 26, 2026
e2fcd1c
fix: type avatar story playground args
pawelgrimm May 26, 2026
0c14cc0
refactor: inline avatar accessibility props
pawelgrimm May 26, 2026
5ec2269
refactor: rely on native avatar srcset selection
pawelgrimm May 26, 2026
728fefb
fix: retry avatar srcset candidates after load failure
pawelgrimm May 26, 2026
438a865
refactor: simplify avatar initials generation
pawelgrimm May 26, 2026
65d84fd
docs: document avatar public api
pawelgrimm May 26, 2026
592e220
refactor: clarify avatar image identity key
pawelgrimm May 26, 2026
e887da4
refactor: Clean up and clarify
pawelgrimm May 26, 2026
97821be
refactor: Use Box props where possible, restructure CSS
pawelgrimm May 26, 2026
cb37567
feat: Implement new stories
pawelgrimm May 26, 2026
9a3f9c3
fix: Add inset border to avatars
pawelgrimm May 26, 2026
ab691c4
fix: handle empty avatar state
pawelgrimm May 26, 2026
9f61a07
refactor: Create improved avatar docs
pawelgrimm May 27, 2026
0b6b6c8
Update avatar meta color tokens
pawelgrimm May 27, 2026
8f6ff49
refactor: Rework meta color CSS custom props
pawelgrimm May 27, 2026
be6aeea
docs: add avatar migration guidance
pawelgrimm May 27, 2026
f18bcf8
fix: Add ref + passthrough props support
pawelgrimm May 27, 2026
e96ca5c
fix: Only set aria-hidden on container
pawelgrimm May 27, 2026
748df88
test: add avatar axe coverage
pawelgrimm May 27, 2026
1f07bcc
fix: normalize avatar fallback labels
pawelgrimm May 27, 2026
05b6b4e
refactor: apply avatar meta colors to initials
pawelgrimm May 27, 2026
12ccd13
refactor: simplify avatar initials splitting
pawelgrimm May 27, 2026
03e56ce
perf: skip avatar source filtering when unchanged
pawelgrimm May 27, 2026
ee36f49
refactor: reuse avatar size constants in stories
pawelgrimm May 27, 2026
5cde795
docs: render avatar migration table as markdown
pawelgrimm May 27, 2026
fa2fc97
feat: support polymorphic avatar root
pawelgrimm May 27, 2026
3e5e579
feat: add AvatarGroup
pawelgrimm May 27, 2026
1f1b1d5
fix: round avatar group overlap masks
pawelgrimm May 27, 2026
ed729e8
docs: add avatar group stories
pawelgrimm May 27, 2026
551ba19
fix: render rounded avatar group masks directly
pawelgrimm May 27, 2026
f61655f
fix: Apply rounded mask corectly
pawelgrimm May 27, 2026
f0feb0c
fix: render avatar group count as DOM element
pawelgrimm May 27, 2026
91f09c1
test: cover avatar group with axe
pawelgrimm May 28, 2026
fc112b9
docs: clarify avatar group count
pawelgrimm May 28, 2026
dc206a4
refactor: drop non-null assertions in avatar collection stories
pawelgrimm May 27, 2026
9cccd19
fix: hide avatar group count overlay from assistive tech
pawelgrimm May 28, 2026
908d4c7
refactor: suppress avatar group count overlay mask unconditionally
pawelgrimm May 28, 2026
f1b9ab6
refactor: simplify avatar group overflow count expression
pawelgrimm May 28, 2026
bed92d0
docs: clarify AvatarGroup count is a decorative overlay
pawelgrimm May 28, 2026
ac61d3e
refactor: split AvatarGroup into own files
pawelgrimm May 28, 2026
5507aa2
fix: remove avatar group count plus sign
pawelgrimm May 28, 2026
61515da
fix: clip avatar group count overlay
pawelgrimm May 28, 2026
af3c338
test: remove avatar group visual assertions
pawelgrimm May 28, 2026
27bd29f
refactor: move avatar group styles
pawelgrimm May 28, 2026
ee23214
refactor: move avatar group sizing to css
pawelgrimm May 28, 2026
f9b0edf
refactor: document avatar group css, rename mask + overlay tokens
pawelgrimm May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions src/avatar/__snapshots__/avatar.test.tsx.snap

This file was deleted.

272 changes: 272 additions & 0 deletions src/avatar/avatar-group.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
/*
* AvatarGroup overlaps each child avatar over the previous one (negative
* margin-left). Because that partly covers the previous avatar, every
* non-first child uses a mask-image to cut a transparent notch where the
* previous sibling sits — letting it show through cleanly. Per-size custom
* properties (set by .avatarGroupSize-N) tune the overlap, mask thickness,
* and rounded-corner radius.
*/

.avatarGroup {
--reactist-avatar-group-count-overlay-background: rgba(0, 0, 0, 0.6);

/* Radius of the corner-cutter gradient (rounded shape): the previous
avatar's own corner radius, padded by the mask gap. */
--reactist-avatar-group-rounded-mask-radius: calc(
var(--reactist-avatar-group-rounded-radius) + var(--reactist-avatar-group-mask-thickness)
);

/* Horizontal slice on the left that holds the rounded corner cut-outs;
everything to its right is shown unmasked. */
--reactist-avatar-group-rounded-mask-width: calc(
var(--reactist-avatar-group-overlap) + var(--reactist-avatar-group-mask-thickness)
);

/* x-coordinate of the rounded corner-cutter's center. Sits one corner
radius to the left of where the overlap ends, so the curve lines up
with the previous avatar's rounded corner. */
--reactist-avatar-group-rounded-mask-corner-x: calc(
var(--reactist-avatar-group-overlap) - var(--reactist-avatar-group-rounded-radius)
);

/* x-coordinate of the previous avatar's center in this avatar's local
coordinate space. Negative: the previous sibling lies to the left.
Used to place the circle mask's notch. */
--reactist-avatar-group-previous-center-x: calc(
(var(--reactist-avatar-group-size) / 2) - var(--reactist-avatar-group-size) +
var(--reactist-avatar-group-overlap)
);

position: relative;
}

/*
* Per-size knobs. AvatarGroup applies exactly one of these classes based on
* its `size` prop, so one of them always wins.
*
* --reactist-avatar-group-size avatar width/height (must match the children's `size`)
* --reactist-avatar-group-overlap how far each avatar slides over the previous one
* --reactist-avatar-group-mask-thickness padding around the cut-out so the previous avatar's outline doesn't bleed through
* --reactist-avatar-group-rounded-radius corner radius for shape="rounded"; mirrors
* ROUNDED_AVATAR_RADIUS_BY_SIZE in src/avatar/utils.ts
*/

.avatarGroupSize-80 {
--reactist-avatar-group-size: 80px;
--reactist-avatar-group-overlap: 8px;
--reactist-avatar-group-mask-thickness: 3px;
--reactist-avatar-group-rounded-radius: 10px;
}

.avatarGroupSize-72 {
--reactist-avatar-group-size: 72px;
--reactist-avatar-group-overlap: 8px;
--reactist-avatar-group-mask-thickness: 3px;
--reactist-avatar-group-rounded-radius: 10px;
}

.avatarGroupSize-62 {
--reactist-avatar-group-size: 62px;
--reactist-avatar-group-overlap: 8px;
--reactist-avatar-group-mask-thickness: 3px;
--reactist-avatar-group-rounded-radius: 8.5px;
}

.avatarGroupSize-50 {
--reactist-avatar-group-size: 50px;
--reactist-avatar-group-overlap: 4px;
--reactist-avatar-group-mask-thickness: 3px;
--reactist-avatar-group-rounded-radius: 7px;
}

.avatarGroupSize-40 {
--reactist-avatar-group-size: 40px;
--reactist-avatar-group-overlap: 4px;
--reactist-avatar-group-mask-thickness: 3px;
--reactist-avatar-group-rounded-radius: 5.5px;
}

.avatarGroupSize-36 {
--reactist-avatar-group-size: 36px;
--reactist-avatar-group-overlap: 4px;
--reactist-avatar-group-mask-thickness: 2.5px;
--reactist-avatar-group-rounded-radius: 5px;
}

.avatarGroupSize-30 {
--reactist-avatar-group-size: 30px;
--reactist-avatar-group-overlap: 2px;
--reactist-avatar-group-mask-thickness: 2.5px;
--reactist-avatar-group-rounded-radius: 5px;
}

.avatarGroupSize-28 {
--reactist-avatar-group-size: 28px;
--reactist-avatar-group-overlap: 2px;
--reactist-avatar-group-mask-thickness: 2px;
--reactist-avatar-group-rounded-radius: 5px;
}

.avatarGroupSize-24 {
--reactist-avatar-group-size: 24px;
--reactist-avatar-group-overlap: 2px;
--reactist-avatar-group-mask-thickness: 2px;
--reactist-avatar-group-rounded-radius: 3.2px;
}

.avatarGroupSize-20 {
--reactist-avatar-group-size: 20px;
--reactist-avatar-group-overlap: 2px;
--reactist-avatar-group-mask-thickness: 2px;
--reactist-avatar-group-rounded-radius: 3px;
}

.avatarGroupSize-18 {
--reactist-avatar-group-size: 18px;
--reactist-avatar-group-overlap: 2px;
--reactist-avatar-group-mask-thickness: 1.5px;
--reactist-avatar-group-rounded-radius: 3px;
}

.avatarGroupSize-16 {
--reactist-avatar-group-size: 16px;
--reactist-avatar-group-overlap: 2px;
--reactist-avatar-group-mask-thickness: 1.25px;
--reactist-avatar-group-rounded-radius: 2px;
}

.avatarGroupSize-12 {
--reactist-avatar-group-size: 12px;
--reactist-avatar-group-overlap: 1px;
--reactist-avatar-group-mask-thickness: 1px;
--reactist-avatar-group-rounded-radius: 1.6px;
}

.avatarGroup > * {
flex-shrink: 0;
}

.avatarGroup > * + * {
margin-left: calc(-1 * var(--reactist-avatar-group-overlap));
}

/*
* Circle mask: one radial gradient centered on the previous avatar's center
* carves a transparent disc slightly larger than the avatar's outline
* (radius = size/2 + mask-thickness). The current avatar therefore shows
* nothing where the previous circle sits, letting it show through cleanly.
*/
.avatarGroupShape-circle > * + * {
-webkit-mask-image: radial-gradient(
circle
calc(
(var(--reactist-avatar-group-size) / 2) +
var(--reactist-avatar-group-mask-thickness)
)
at var(--reactist-avatar-group-previous-center-x) 50%,
transparent 99%,
#000 100%
);
mask-image: radial-gradient(
circle
calc(
(var(--reactist-avatar-group-size) / 2) +
var(--reactist-avatar-group-mask-thickness)
)
at var(--reactist-avatar-group-previous-center-x) 50%,
transparent 99%,
#000 100%
);
}

/*
* Rounded mask: built from three alpha-mask layers, listed top-to-bottom:
*
* 1. linear-gradient(#000), positioned `right top` at
* (100% - mask-width) × 100% — reveals the whole right portion of the
* avatar unmasked.
*
* 2. radial-gradient at the top-left, sized mask-width × rounded-radius —
* carves a concave arc matching the previous avatar's top-right
* rounded corner.
*
* 3. Same radial-gradient pinned to the bottom-left — same trick for the
* previous avatar's bottom-right corner.
*
* The left-middle strip (between the two corner cut-outs) is intentionally
* uncovered by any layer, so it stays transparent — that's where the body of
* the previous avatar shows through.
*/
.avatarGroupShape-rounded > * + * {
-webkit-mask-image:
linear-gradient(#000 0 0),
radial-gradient(
circle var(--reactist-avatar-group-rounded-mask-radius) at
var(--reactist-avatar-group-rounded-mask-corner-x)
var(--reactist-avatar-group-rounded-radius),
transparent 99%,
#000 100%
),
radial-gradient(
circle var(--reactist-avatar-group-rounded-mask-radius) at
var(--reactist-avatar-group-rounded-mask-corner-x) 0,
transparent 99%,
#000 100%
);
mask-image:
linear-gradient(#000 0 0),
radial-gradient(
circle var(--reactist-avatar-group-rounded-mask-radius) at
var(--reactist-avatar-group-rounded-mask-corner-x)
var(--reactist-avatar-group-rounded-radius),
transparent 99%,
#000 100%
),
radial-gradient(
circle var(--reactist-avatar-group-rounded-mask-radius) at
var(--reactist-avatar-group-rounded-mask-corner-x) 0,
transparent 99%,
#000 100%
);
-webkit-mask-position:
right top,
left top,
left bottom;
mask-position:
right top,
left top,
left bottom;
-webkit-mask-size:
calc(100% - var(--reactist-avatar-group-rounded-mask-width)) 100%,
var(--reactist-avatar-group-rounded-mask-width) var(--reactist-avatar-group-rounded-radius),
var(--reactist-avatar-group-rounded-mask-width) var(--reactist-avatar-group-rounded-radius);
mask-size:
calc(100% - var(--reactist-avatar-group-rounded-mask-width)) 100%,
var(--reactist-avatar-group-rounded-mask-width) var(--reactist-avatar-group-rounded-radius),
var(--reactist-avatar-group-rounded-mask-width) var(--reactist-avatar-group-rounded-radius);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
}

.avatarGroupCount {
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
width: var(--reactist-avatar-group-size);
height: var(--reactist-avatar-group-size);
border-radius: 50%;
background: var(--reactist-avatar-group-count-overlay-background);
color: var(--reactist-avatar-initials-color, var(--reactist-actionable-primary-idle-tint));
font-size: calc(var(--reactist-avatar-group-size) / 2);
font-weight: var(--reactist-font-weight-medium);
line-height: 1;
pointer-events: none;
user-select: none;
}

.avatarGroupShape-rounded > .avatarGroupCount {
border-radius: var(--reactist-avatar-group-rounded-radius);
}
Loading
Loading