fix: rewrite decoration grid to contain images within cells
CI / Build & Quality Checks (push) Successful in 11m39s
Trigger Desktop Build / trigger (push) Successful in 6s

The INSET overflow approach (position:absolute images extending beyond
52×52 buttons) was fundamentally broken: absolutely positioned children
don't contribute to flex row height, so rowGap controlled button-to-button
spacing but image pixels still painted into the gap, causing visual overlap
regardless of how large rowGap was set.

New approach: 72×72 circle cells, overflow:hidden, image fills the cell
via inset:0 with objectFit:contain. Gap of 16px is actual clear space
between cell edges — no math needed. Also bumped maxHeight 420→480.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 14:49:42 -04:00
parent d7d7b59866
commit a5fe358313
@@ -12,7 +12,7 @@ import {
import { invalidateDecorationCache } from '../../../hooks/useAvatarDecoration'; import { invalidateDecorationCache } from '../../../hooks/useAvatarDecoration';
const PROFILE_FIELD = 'io.lotus.avatar_decoration'; const PROFILE_FIELD = 'io.lotus.avatar_decoration';
const INSET = 8; const CELL_SIZE = 72;
function DecorationPreviewCell({ function DecorationPreviewCell({
slug, slug,
@@ -34,29 +34,19 @@ function DecorationPreviewCell({
onClick={() => onSelect(slug)} onClick={() => onSelect(slug)}
style={{ style={{
position: 'relative', position: 'relative',
width: 52, width: CELL_SIZE,
height: 52, height: CELL_SIZE,
flexShrink: 0, flexShrink: 0,
border: `2px solid ${selected ? 'var(--accent-cyan)' : 'transparent'}`, border: `2px solid ${selected ? 'var(--accent-cyan)' : 'transparent'}`,
borderRadius: '0.75rem', borderRadius: '50%',
background: 'var(--bg-surface-variant)', background: 'var(--bg-surface-variant)',
cursor: 'pointer', cursor: 'pointer',
padding: 0, padding: 0,
boxShadow: selected ? '0 0 0 1px var(--accent-cyan)' : 'none', boxShadow: selected ? '0 0 0 1px var(--accent-cyan)' : 'none',
overflow: 'visible', overflow: 'hidden',
outline: 'none', outline: 'none',
}} }}
> >
{/* Avatar placeholder tint */}
<div
style={{
position: 'absolute',
inset: 0,
borderRadius: '0.75rem',
background: 'var(--bg-surface-variant)',
overflow: 'hidden',
}}
/>
<img <img
src={`${DECORATION_CDN}/${slug}.png`} src={`${DECORATION_CDN}/${slug}.png`}
alt={name} alt={name}
@@ -64,10 +54,9 @@ function DecorationPreviewCell({
decoding="async" decoding="async"
style={{ style={{
position: 'absolute', position: 'absolute',
top: -INSET, inset: 0,
left: -INSET, width: '100%',
width: `calc(100% + ${INSET * 2}px)`, height: '100%',
height: `calc(100% + ${INSET * 2}px)`,
objectFit: 'contain', objectFit: 'contain',
pointerEvents: 'none', pointerEvents: 'none',
}} }}
@@ -149,12 +138,12 @@ export function ProfileDecoration() {
<div <div
style={{ style={{
position: 'relative', position: 'relative',
width: 52, width: CELL_SIZE,
height: 52, height: CELL_SIZE,
flexShrink: 0, flexShrink: 0,
borderRadius: '0.75rem', borderRadius: '50%',
background: 'var(--bg-surface-variant)', background: 'var(--bg-surface-variant)',
overflow: 'visible', overflow: 'hidden',
}} }}
> >
{selected && ( {selected && (
@@ -163,10 +152,9 @@ export function ProfileDecoration() {
alt="Selected decoration preview" alt="Selected decoration preview"
style={{ style={{
position: 'absolute', position: 'absolute',
top: -INSET, inset: 0,
left: -INSET, width: '100%',
width: `calc(100% + ${INSET * 2}px)`, height: '100%',
height: `calc(100% + ${INSET * 2}px)`,
objectFit: 'contain', objectFit: 'contain',
pointerEvents: 'none', pointerEvents: 'none',
}} }}
@@ -236,7 +224,7 @@ export function ProfileDecoration() {
direction="Column" direction="Column"
gap="300" gap="300"
style={{ style={{
maxHeight: 420, maxHeight: 480,
overflowY: 'auto', overflowY: 'auto',
paddingRight: 4, paddingRight: 4,
}} }}
@@ -250,12 +238,8 @@ export function ProfileDecoration() {
style={{ style={{
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexWrap: 'wrap',
columnGap: 28, gap: 16,
rowGap: 52, padding: 4,
paddingBottom: INSET,
paddingLeft: INSET,
paddingRight: INSET,
paddingTop: INSET,
}} }}
> >
{category.decorations.map((d) => ( {category.decorations.map((d) => (