fix(settings): two cards on the profile page were touching

Reported straight after a sweep that had measured that page and found nothing,
which is the interesting part: they ARE both proper cards, and every check the
sweep makes passed. Both insets agree with each other, both agree with the
headings, nothing is out by a pixel. They are adjacent siblings with no
space-y, so they render as one card with a hairline through it and two headings
inside it — and every one of those checks is about where content starts
HORIZONTALLY.

The property was never being measured, so no amount of the existing guard would
ever have found it.

Check D measures it now: two cards stacked on each other may not touch. Compared
against the smallest real gap on the page rather than a fixed figure, so a
section that deliberately packs its cards tighter than space-y-4 is not forced
apart — zero is the only thing refused, and zero is two borders sharing an edge.
Side-by-side pairs are skipped, since the appearance section has one.

Proven both ways, and it swept every other section: profile was the only one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul LütznerandClaude Opus 5 committed 2026-08-27 18:33:31 +02:00
1 parent 1ef1704828
commit b6743fc35e
2 files changed
+59

No files matched your search

+8
View File
@@ -8,6 +8,13 @@ find.
is one of the few cards that takes the chrome's body inset. Until now it was
not a card at all — a bare heading and a form, which on a narrow pane sat
flush against the pane border like security's heading did. #}
{# Two cards, so they need the gap two cards take. Without it the pair
render as adjacent siblings with no margin: one continuous panel with
a hairline through it, which reads as one card carrying two headings
rather than two subjects. space-y-4 is what the multi-card sections in
index.html.twig use. #}
<div class="space-y-4">
{% embed 'settings/_card.html.twig' with {
heading: 'settings.profile.heading'|trans,
subheading: 'settings.profile.subheading'|trans,
@@ -115,3 +122,4 @@ other. This lives beside the avatar, which is what it is: a picture of you.
</div>
{% endblock %}
{% endembed %}
</div>
+51
View File
@@ -200,6 +200,15 @@ const TOLERANCE = {
betweenCards: 1,
/** A bare heading against a card, which is one border-width further in. */
aroundCards: 2,
/**
* The smallest gap that still reads as two cards.
*
* Not a design figure — the sections use space-y-4 (16px) — but the line
* below which a pair stops being two things. Two borders sharing an edge is
* 0; anything with real air in it is fine.
*/
cardGap: 4,
};
/**
@@ -506,6 +515,48 @@ async function measure(page: Page, section: string): Promise<Measurement> {
}
}
/**
* Check D — two cards do not touch.
*
* The other three checks are all about where content starts
* HORIZONTALLY, and a pair of cards rendered with no gap satisfies
* every one of them: both insets agree, both agree with the
* headings, nothing is misaligned by a pixel. They simply read as
* one card with a rule through it and two headings in it.
*
* That is how Settings -> Profile shipped: two `{% embed %}` cards
* as adjacent siblings with no `space-y`, reported by the owner
* immediately after a sweep that had measured the page and found
* nothing — because the property was never being measured.
*
* Compared against the SMALLEST real gap on the page rather than a
* fixed figure, so a section that deliberately packs its cards
* tighter than space-y-4 is not forced apart; zero is the only
* thing this refuses, and zero is two borders sharing an edge.
*/
for (let i = 1; i < cardList.length; i++) {
const above = cardList[i - 1].getBoundingClientRect();
const below = cardList[i].getBoundingClientRect();
// Only cards actually stacked on each other. A split pair side
// by side — the appearance section has one — is not this.
if (below.top < above.bottom - 1) {
continue;
}
const gap = below.top - above.bottom;
if (gap < tolerance.cardGap) {
offences.push(
`${section}: two cards are touching — the one ending at ` +
`y=${round(above.bottom)} and the one starting at y=${round(below.top)}, ` +
`${round(gap)}px apart. Cards stacked without a gap render as one card ` +
`with a rule through it and two headings inside it. The multi-card ` +
`sections wrap their cards in space-y-4.`,
);
}
}
return { offences, cards: cardList.length };
},
{ section, wrapper: WRAPPER, tolerance: TOLERANCE },