feat(ui): force every user and room avatar to render as a circle via globalStyle override on the folds Avatar wrapper

This commit is contained in:
heaven 2026-05-03 14:48:27 +03:00
parent ed1544dd5e
commit 8f49124043
6 changed files with 34 additions and 11 deletions

View file

@ -1,4 +1,4 @@
import { style } from '@vanilla-extract/css'; import { globalStyle, style } from '@vanilla-extract/css';
import { color } from 'folds'; import { color } from 'folds';
export const RoomAvatar = style({ export const RoomAvatar = style({
@ -12,3 +12,10 @@ export const RoomAvatar = style({
}, },
}, },
}); });
// See `UserAvatar.css.ts` for the rationale — same one-liner override forces
// every RoomAvatar (rooms, spaces, DMs, bridged puppets) into a circle without
// touching callsites.
globalStyle(`*:has(> .${RoomAvatar})`, {
borderRadius: '50% !important',
});

View file

@ -11,6 +11,9 @@ type RoomAvatarProps = {
alt?: string; alt?: string;
renderFallback: () => ReactNode; renderFallback: () => ReactNode;
}; };
// Always renders as a circle: a globalStyle in `RoomAvatar.css.ts` forces the
// parent folds `<Avatar>` to `border-radius: 50%`, so the `radii` prop on the
// outer `<Avatar>` is intentionally inert at every callsite.
export function RoomAvatar({ roomId, src, alt, renderFallback }: RoomAvatarProps) { export function RoomAvatar({ roomId, src, alt, renderFallback }: RoomAvatarProps) {
const [error, setError] = useState(false); const [error, setError] = useState(false);

View file

@ -1,4 +1,4 @@
import { style } from '@vanilla-extract/css'; import { globalStyle, style } from '@vanilla-extract/css';
import { color } from 'folds'; import { color } from 'folds';
export const UserAvatar = style({ export const UserAvatar = style({
@ -12,3 +12,14 @@ export const UserAvatar = style({
}, },
}, },
}); });
// Force every UserAvatar to render as a circle. Folds `<Avatar>` controls the
// outer shape via its `radii` variant; the inner `AvatarImage`/`AvatarFallback`
// inherit `border-radius` from it. By overriding the parent's radius we round
// every user avatar without touching the ~30 callsites or losing the radii
// system for non-avatar uses of folds `<Avatar>` (icon tabs, emoji-pack tiles).
// `!important` is necessary because folds' recipe rule has the same specificity
// and source-order would otherwise be a coin flip.
globalStyle(`*:has(> .${UserAvatar})`, {
borderRadius: '50% !important',
});

View file

@ -11,6 +11,9 @@ type UserAvatarProps = {
alt?: string; alt?: string;
renderFallback: () => ReactNode; renderFallback: () => ReactNode;
}; };
// Always renders as a circle: a globalStyle in `UserAvatar.css.ts` forces the
// parent folds `<Avatar>` to `border-radius: 50%`, so the `radii` prop on the
// outer `<Avatar>` is intentionally inert at every callsite.
export function UserAvatar({ className, userId, src, alt, renderFallback }: UserAvatarProps) { export function UserAvatar({ className, userId, src, alt, renderFallback }: UserAvatarProps) {
const [error, setError] = useState(false); const [error, setError] = useState(false);

View file

@ -53,7 +53,7 @@ export function BotCard({ preset, selected }: BotCardProps) {
padding: `${toRem(6)} 0`, padding: `${toRem(6)} 0`,
}} }}
> >
<Avatar size="300" radii="400" style={{ background: AVATAR_BG, color: '#0c0c0e' }}> <Avatar size="300" radii="Pill" style={{ background: AVATAR_BG, color: '#0c0c0e' }}>
{avatarUrl ? ( {avatarUrl ? (
<AvatarImage src={avatarUrl} alt={preset.name} /> <AvatarImage src={avatarUrl} alt={preset.name} />
) : ( ) : (

View file

@ -97,18 +97,18 @@ export const HeroBack = style([
}, },
]); ]);
// 56×56 square avatar with 14px radius, fleet violet (DAWN.fleet) bg. // 56×56 circular avatar, fleet violet (DAWN.fleet) bg. Fleet color is
// Fleet color is hardcoded here because it's the canonical bot accent in // hardcoded here because it's the canonical bot accent in the mockup and we
// the mockup and we don't want it varying with Folds palette swaps. The // don't want it varying with Folds palette swaps. The violet disk shows when
// violet square shows when the bot's Matrix profile has no `avatar_url` // the bot's Matrix profile has no `avatar_url` (fallback to the initial
// (fallback to the initial letter); when it does, the inner <img> covers // letter); when it does, the inner <img> covers the violet — `overflow:
// the violet — `overflow: hidden` keeps it inside the rounded corners. // hidden` keeps it inside the round mask.
export const HeroAvatar = style([ export const HeroAvatar = style([
DefaultReset, DefaultReset,
{ {
width: toRem(56), width: toRem(56),
height: toRem(56), height: toRem(56),
borderRadius: toRem(14), borderRadius: '50%',
backgroundColor: '#9580ff', backgroundColor: '#9580ff',
color: '#0c0c0e', color: '#0c0c0e',
fontSize: toRem(24), fontSize: toRem(24),
@ -123,7 +123,6 @@ export const HeroAvatar = style([
'(max-width: 600px)': { '(max-width: 600px)': {
width: toRem(36), width: toRem(36),
height: toRem(36), height: toRem(36),
borderRadius: toRem(8),
fontSize: toRem(16), fontSize: toRem(16),
}, },
}, },