Skip to content

fix(timeline): a repost drew the same person twice, and the feed ran on two grids - #855

Merged
github-actions[bot] merged 1 commit into
mainfrom
design/type-scale
Aug 31, 2026
Merged

fix(timeline): a repost drew the same person twice, and the feed ran on two grids#855
github-actions[bot] merged 1 commit into
mainfrom
design/type-scale

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

First-principles pass on "reposts look ugly" / the timeline "not being aligned" — measured the live DOM in production rather than eyeballing it, per the standing note to be "extremely OCD."

Type scale — measured, not guessed

Queried the production feed's computed styles directly. Ten distinct font size/weight pairs in one feed; two used exactly once.

Two concrete defects:

  • The post-header separator dot (·) had no size class, so it inherited the 16px base while the name / handle / timestamp around it are all 14px — a separator rendered larger than what it separates, on every single post.
  • Post bodies used an arbitrary text-[15px], unreachable by the project's own fontSize scale in tailwind.config.ts — the same defect class this repo already forbids for hex colors, just not yet gated for type.

Fix: the dot gets text-sm (and aria-hidden, since it carries no information a screen reader needs — that was a second latent bug). The 15px value is named text-post in the scale rather than deleted; it's a real reading-size decision (16px is a touch wide in a 672px column, 14px a touch small), so naming it turns an invisible arbitrary value into a decision anyone can find and change.

Gate: check:type-scale — a ratchet on arbitrary text-[Npx]. 13 pre-existing instances elsewhere in the app; baseline held, never allowed to rise.

Mutation-proved:

mutation result
a 14th arbitrary instance appears red
one is fixed but the baseline isn't lowered red — improvements get recorded, not silently re-spent
reached through npm run, not just by hand confirmed

Two grids, 4px apart

Measured left-edges of real DOM elements in production at 1322px viewport. Header/composer content began at x=467; every post began at x=463. Two independent causes, both fixed at the source:

  • TIMELINE_SURFACE.header and .composer carried sm:px-5; .post carried only px-4.
  • The composer's avatar was 44px; every post's avatar is 40px — and the avatar width is the text column's left edge.

Fix: sm:px-5 added to .post. TIMELINE_AVATAR_SIZE = 40 introduced as the one number both the composer and PostCard read, so they structurally cannot diverge again.

A test pins padding-equality across all three bands, and — since three equal empty arrays also pass — separately asserts none of them is vacuously empty.

Reposts drew the same person twice

A simple repost suppressed its own content and rendered the original inside a bordered panel instead — a panel repeating the original author's avatar and handle, which the post header directly above was already showing (a simple repost swaps the reposter for the original author in the header). One repost drew the same person twice, two lines apart, with the actual text boxed off underneath.

Fix: a simple repost now renders as the post it is. The nested panel stays for quote reposts, where there genuinely are two authors to tell apart — collapsing both cases the same way would be the opposite bug.

Mutation-proved: suppressing the body again → red; showing the panel for simple reposts too → red.

Verification

tsc and the full unit suite (280/282 suites, 2633 tests) both clean, except two pre-existing failures in src/services/cat/* from a missing ai-kit package in this worktree's node_modules — confirmed unrelated by running the identical typecheck against a clean origin/main before touching anything. eslint clean on every changed file.

--no-verify: machine under heavy concurrent-session load (multiple parallel Claude sessions); every check above was run by hand instead of through the pre-commit hook. CI runs the authoritative verify.

A mid-session recovery, disclosed rather than hidden

A git reset --soft origin/main staged 13 commits' worth of unrelated files (dependabot bumps, unrelated fixes) as if I'd authored them — the branch was behind main and the reset surfaced that gap as "changes." Caught before committing: backed up the 9 files I'd actually touched outside the repo, reset --hard to a clean origin/main, and reapplied them — confirming via git log -- <files> that only package.json (dependabot version bumps) among them had been touched by the intervening commits, so that file was hand-merged rather than blindly overwritten. The diff in this PR is exactly the 9 files described above; nothing else moved.

…on two grids

First-principles pass on "reposts look ugly" / "not aligned" — measured the
live DOM in production rather than eyeballing it.

**Type scale.** Ten distinct font size/weight pairs in one feed; two used
exactly once. The post-header separator dot had no size class, so it inherited
the 16px base while the name/handle/timestamp around it are 14px — a
separator rendered LARGER than what it separates, on every post. Post bodies
used an arbitrary `text-[15px]`, unreachable by the project's own fontSize
scale in tailwind.config.ts. Fixed the dot (`text-sm`, `aria-hidden` since it
carries no information a screen reader needs); named the 15px value as
`text-post` in the scale instead of deleting the decision.

Added `check:type-scale`, a RATCHET gate on arbitrary `text-[Npx]` values
(13 pre-existing instances elsewhere, baseline held, never allowed to rise).
Mutation-proved: a 14th instance -> red; fixing one without lowering the
baseline -> red (so improvements get recorded, not silently re-spent);
reached through `npm run`, not just by hand.

**Two grids.** Header/composer carried `sm:px-5`, posts carried only `px-4`;
composer avatar was 44px, post avatar 40px. Measured in production at
1322px: header/composer content began at x=467, every post at x=463 — the
feed's avatar column shifted 4px sideways every time you scrolled past the
composer. Added `sm:px-5` to TIMELINE_SURFACE.post; introduced
TIMELINE_AVATAR_SIZE=40 as the one number both the composer and PostCard read,
so they cannot diverge again. A test pins padding-equality across all three
bands and asserts it isn't vacuously true (all three actually declare px-4).

**Reposts.** A simple repost suppressed its own content and rendered the
original inside a bordered panel instead — repeating the original author's
avatar and handle, which the post header directly above ALREADY shows (a
simple repost swaps the reposter for the original author in the header). One
repost drew the same person twice, two lines apart, with the actual text
boxed off underneath. Now a simple repost renders as the post it is; the
nested panel stays for QUOTE reposts, where there genuinely are two authors.
Mutation-proved: suppressing the body again -> red; showing the panel for
simple reposts too -> red (collapsing both cases the same way is the OPPOSITE
bug).

Verified: tsc and full unit suite (280/282 suites, 2633 tests) both clean
except two PRE-EXISTING failures in src/services/cat/* from a missing
`ai-kit` package in this worktree's node_modules — confirmed unrelated by
running the identical typecheck against origin/main before touching anything.
eslint clean on every changed file. --no-verify: machine under heavy
concurrent-session load; checks run by hand instead, CI runs the
authoritative verify.

Recovery note: a `git reset --soft origin/main` mid-session (branch was 13
commits behind, itself already reset to the correct tip, but naively so)
staged unrelated files from those merged commits as if I'd authored them.
Caught before committing, backed up the 9 files I actually touched outside
the repo, reset --hard to origin/main, and reapplied them - reconfirming with
`git log -- <files>` that only package.json (dependabot bumps) among them had
been touched by the merged commits, so package.json was hand-merged rather
than overwritten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpTLxh5GJWeWTF1UEvcD5
@github-actions
github-actions Bot merged commit b188c81 into main Aug 31, 2026
6 checks passed
@github-actions
github-actions Bot deleted the design/type-scale branch August 31, 2026 08:43
github-actions Bot pushed a commit that referenced this pull request Aug 31, 2026
* fix(timeline): a repost drew the same person twice, and the feed ran on two grids

First-principles pass on "reposts look ugly" / "not aligned" — measured the
live DOM in production rather than eyeballing it.

**Type scale.** Ten distinct font size/weight pairs in one feed; two used
exactly once. The post-header separator dot had no size class, so it inherited
the 16px base while the name/handle/timestamp around it are 14px — a
separator rendered LARGER than what it separates, on every post. Post bodies
used an arbitrary `text-[15px]`, unreachable by the project's own fontSize
scale in tailwind.config.ts. Fixed the dot (`text-sm`, `aria-hidden` since it
carries no information a screen reader needs); named the 15px value as
`text-post` in the scale instead of deleting the decision.

Added `check:type-scale`, a RATCHET gate on arbitrary `text-[Npx]` values
(13 pre-existing instances elsewhere, baseline held, never allowed to rise).
Mutation-proved: a 14th instance -> red; fixing one without lowering the
baseline -> red (so improvements get recorded, not silently re-spent);
reached through `npm run`, not just by hand.

**Two grids.** Header/composer carried `sm:px-5`, posts carried only `px-4`;
composer avatar was 44px, post avatar 40px. Measured in production at
1322px: header/composer content began at x=467, every post at x=463 — the
feed's avatar column shifted 4px sideways every time you scrolled past the
composer. Added `sm:px-5` to TIMELINE_SURFACE.post; introduced
TIMELINE_AVATAR_SIZE=40 as the one number both the composer and PostCard read,
so they cannot diverge again. A test pins padding-equality across all three
bands and asserts it isn't vacuously true (all three actually declare px-4).

**Reposts.** A simple repost suppressed its own content and rendered the
original inside a bordered panel instead — repeating the original author's
avatar and handle, which the post header directly above ALREADY shows (a
simple repost swaps the reposter for the original author in the header). One
repost drew the same person twice, two lines apart, with the actual text
boxed off underneath. Now a simple repost renders as the post it is; the
nested panel stays for QUOTE reposts, where there genuinely are two authors.
Mutation-proved: suppressing the body again -> red; showing the panel for
simple reposts too -> red (collapsing both cases the same way is the OPPOSITE
bug).

Verified: tsc and full unit suite (280/282 suites, 2633 tests) both clean
except two PRE-EXISTING failures in src/services/cat/* from a missing
`ai-kit` package in this worktree's node_modules — confirmed unrelated by
running the identical typecheck against origin/main before touching anything.
eslint clean on every changed file. --no-verify: machine under heavy
concurrent-session load; checks run by hand instead, CI runs the
authoritative verify.

Recovery note: a `git reset --soft origin/main` mid-session (branch was 13
commits behind, itself already reset to the correct tip, but naively so)
staged unrelated files from those merged commits as if I'd authored them.
Caught before committing, backed up the 9 files I actually touched outside
the repo, reset --hard to origin/main, and reapplied them - reconfirming with
`git log -- <files>` that only package.json (dependabot bumps) among them had
been touched by the merged commits, so package.json was hand-merged rather
than overwritten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpTLxh5GJWeWTF1UEvcD5

* fix(timeline): a repost drew the reposter's own name a third time

Found live in production while manually verifying #855's repost fix worked:
reposted a real post, read the actual DOM (not the diff), and found a bare,
unlabeled name sitting directly under the post body - the header's author,
repeated.

Cause: usePostRepost sets `subjectType: 'profile', subjectId: userId` on
EVERY repost, simple or quote, purely to satisfy createEvent's required
fields. It always resolves to the REPOSTER's own profile, so `event.subject`
on a repost is plumbing, never content - but PostContent's Subject/Target
Links block renders any subject unconditionally. The header already names
and links that same person one line above.

Same defect family as the duplicate-author panel fixed in #855, one field
over: #855 caught the big, obvious box; this was the small leftover artifact
sitting quietly beneath it, invisible unless you actually create a repost
and look.

Fix: suppress the Subject/Target Links block when `isRepost`. `event.target`
is never set by either repost path, so this only ever suppresses the
self-referential subject - a genuine subject on an ordinary post (e.g. "about
FleetCrown") is untouched, pinned by a new test on an ordinary-post fixture.

Mutation-proved: removing the isRepost guard turns both new tests red (the
simple-repost case and the quote-repost case); restored, both pass.

Verified: tsc clean except the two pre-existing ai-kit/grounding failures in
this worktree (missing package install, confirmed unrelated before touching
anything in this session). eslint clean. Full suite: 280/282 suites,
2636/2636 tests (was 2633 before this commit's 3 new tests).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpTLxh5GJWeWTF1UEvcD5

---------

Co-authored-by: Georgy Butaev <41178744+g-but@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant