Profile page: avatar is a floating orphan — the header did not reflow around it (#390 follow-up) #395

Closed
opened 2026-09-12 17:23:00 +00:00 by crueber · 3 comments
Owner

What's wrong

The #390 profile-page avatar landed in the right corner but as a floating island disconnected from the profile content — the rest of the page didn't reflow around it. Screenshot evidence:

  • The avatar (96px circle) sits alone at the top-right, with the "New repository" button directly beneath it — an orphaned column occupying the full right half of the header.
  • The profile identity block (username crueber, location/timezone, "Hi there!" bio) is left-aligned on the far left, with a large band of dead whitespace between the two columns.
  • "Edit profile" / "Regenerate avatar" / "Remove avatar" buttons stack under the bio on the left, while the avatar-related buttons (Regenerate/Remove) are semantically tied to the avatar sitting far away on the right — the controls for an object are visually separated from the object.
  • The header reads as two unrelated clusters rather than one composed identity block; the layout looks unfinished.

Root cause (code evidence)

web/src/pages/Repos.jsx:291-307 — #390 added the avatar as its own mb-3 flex justify-end row (:298) above the header row (:308, flex items-center justify-between with the h2 username), rather than integrating it into the header flow. The header itself only contains the username + New-repository button; location, bio, and the profile action buttons render in separate blocks below. Nothing anchors the avatar to the identity content.

Fix direction

Compose a real profile header: a two-column flex/grid row — identity content left (username as h1, location · timezone, bio), avatar right (the h-24 circle) — with the avatar's own actions (Regenerate / Remove) grouped under or beside the avatar, and the New-repository + Edit-profile actions either in a shared action row or placed with their related content:

  • Suggested shape (GitHub-like): left column = username, location · timezone, bio, then Edit profile; right column = large avatar with Regenerate/Remove directly beneath it. The New-repository CTA stays prominent (top-right above the avatar, or in an action row — implementer's call, note it).
  • Mobile (390px): the two columns stack — avatar above/right of the identity block, buttons wrapping — per the #273-#278 sweep conventions; verify no dead half-width band.
  • Keep all existing gating (Edit profile owner-only; Regenerate/Remove owner-only) and the cache invalidation behavior (#234/#376) — layout only.

Acceptance criteria

  • The profile header is a composed identity block: username, location/timezone, bio on the left; large avatar on the right; no orphaned floating avatar column and no dead whitespace band.
  • Regenerate/Remove avatar buttons sit with the avatar they act on (not separated across the page).
  • New-repository CTA remains prominent and does not collide with the avatar at desktop or 390px widths (stacks cleanly on narrow screens).
  • Edit-profile flow, avatar regenerate/remove behavior, and cache invalidation unchanged (layout-only change).
  • Light/dark themes verified.
## What's wrong The #390 profile-page avatar landed in the right *corner* but as a **floating island disconnected from the profile content** — the rest of the page didn't reflow around it. Screenshot evidence: - The avatar (96px circle) sits alone at the top-right, with the "New repository" button directly beneath it — an orphaned column occupying the full right half of the header. - The profile identity block (username `crueber`, location/timezone, "Hi there!" bio) is left-aligned on the far left, with a **large band of dead whitespace** between the two columns. - "Edit profile" / "Regenerate avatar" / "Remove avatar" buttons stack under the bio on the left, while the avatar-related buttons (Regenerate/Remove) are semantically tied to the avatar sitting far away on the right — the controls for an object are visually separated from the object. - The header reads as two unrelated clusters rather than one composed identity block; the layout looks unfinished. ## Root cause (code evidence) `web/src/pages/Repos.jsx:291-307` — #390 added the avatar as its own `mb-3 flex justify-end` row (`:298`) **above** the header row (`:308`, `flex items-center justify-between` with the h2 username), rather than integrating it into the header flow. The header itself only contains the username + New-repository button; location, bio, and the profile action buttons render in separate blocks below. Nothing anchors the avatar to the identity content. ## Fix direction Compose a real **profile header**: a two-column flex/grid row — identity content left (username as h1, location · timezone, bio), avatar right (the h-24 circle) — with the avatar's own actions (Regenerate / Remove) grouped **under or beside the avatar**, and the New-repository + Edit-profile actions either in a shared action row or placed with their related content: - Suggested shape (GitHub-like): left column = username, location · timezone, bio, then Edit profile; right column = large avatar with Regenerate/Remove directly beneath it. The New-repository CTA stays prominent (top-right above the avatar, or in an action row — implementer's call, note it). - Mobile (390px): the two columns stack — avatar above/right of the identity block, buttons wrapping — per the #273-#278 sweep conventions; verify no dead half-width band. - Keep all existing gating (Edit profile owner-only; Regenerate/Remove owner-only) and the cache invalidation behavior (#234/#376) — layout only. ## Acceptance criteria - [ ] The profile header is a composed identity block: username, location/timezone, bio on the left; large avatar on the right; no orphaned floating avatar column and no dead whitespace band. - [ ] Regenerate/Remove avatar buttons sit with the avatar they act on (not separated across the page). - [ ] New-repository CTA remains prominent and does not collide with the avatar at desktop or 390px widths (stacks cleanly on narrow screens). - [ ] Edit-profile flow, avatar regenerate/remove behavior, and cache invalidation unchanged (layout-only change). - [ ] Light/dark themes verified.
crueber added this to the v1 milestone 2026-09-12 17:23:00 +00:00
Author
Owner

Fixed by #400 (branch fix/issue-395): #400 — composed two-column profile header; CTA choice noted (top-right row above the grid); light+dark via shared layout/ring classes; headless tests + vite build green, browser proof open per workspace rules.

Fixed by #400 (branch fix/issue-395): https://git.packden.us/crueber/walhub/pulls/400 — composed two-column profile header; CTA choice noted (top-right row above the grid); light+dark via shared layout/ring classes; headless tests + vite build green, browser proof open per workspace rules.
Author
Owner

REVIEW PR #400 (fix/issue-395) — verified in scratch worktree /tmp/walhub-395 (pre-existing; left in place). Main worktree untouched (still clean apart from pre-existing untracked .opencode/).

ACCEPTANCE (all vs web/src/pages/Repos.jsx):
(1) Composed header OK — profile-header flex row :316 (identity left: h1 displayName :318, handle, location·timezone, bio via renderBody, Edit profile; avatar right in profile-avatar :354 with Regenerate/Remove beneath :366-373). New-repo CTA in its own top-right row above the grid :310-314 (implementer's call per issue), canWrite-gated as before.
(2) Orphan gone OK — the only 'mb-3 flex justify-end' left is :310 and it holds the CTA, not the avatar; avatar img exists only inside profile-avatar.
(3) Orgs: behavior-identical, NOT byte-untouched as D-WEB-9 claimed — redundant inner isOrg guards folded into the outer Show gate; '{isOrg() ? orgName() : displayName()}' -> '{orgName()}'. One real (correct, #359-aligned) behavior change: owner-profile location/bio/Edit blocks previously also rendered on org pages (old guards lacked !isOrg); now non-org-only so org pages read the org doc alone. Tests pin this. FIXED DOC: pushed d6175e9 rewording D-WEB-9 to 'behavior-identical … owner-profile fields now render non-org-only'.
(4) Gates unchanged OK — Edit = getProfile()?.can_edit (:336,:382); avatar actions = isSelf() (:364), Remove on userSrc; isSelf/regenerate/remove/refreshAvatar + invalidate user:/me (:250-281) byte-identical to main. #376 intact.
(5) 390px OK by reasoning (no browser per review instructions — noted explicitly): flex-col-reverse + identity-first DOM paints avatar on top, both columns full-width (flex-1 / shrink-0, flex-wrap buttons), no dead half-width band.
(6) h1 OK — single h1 on the page (:318, user only); org keeps h2 (:400, mutually exclusive Shows); ProfileForm has no headings; displayName() falls back to owner slug.
(7) No-avatar self keeps Regenerate OK — column gate 'userSrc() || isSelf()' (:353) preserves #376 opt-back-in; correct.
(8) Light+dark OK by reasoning (no browser): ring-zinc-300 dark:ring-zinc-600 kept on avatar; layout classes theme-independent.
(9) No backend change (diff = DEVIATIONS.md + Repos.jsx + 2 test files), no new deps (package manifests untouched), D-WEB-9 warranted (law 12, follows D-WEB-8 pattern; wording fixed by d6175e9).

TESTS: full 'node --test web/test/unit/*.test.js' = 786 pass / 2 fail; the 2 failures are both server-dependent smoke.test.js cases (they fetch :8080, currently answering 401 from a live instance I did not touch) — smoke.test.js is byte-identical main..branch so definitionally pre-existing/environmental (with no server they skip: 3 skipped). All 8 new profile-header.test.js + rewritten identity-nav #395 test pass (38/38 in the two files). vite build + esbuild SDK build green (chunk-size warning only, advisory).

MERGE RECOMMENDATION: ready to merge.

REVIEW PR #400 (fix/issue-395) — verified in scratch worktree /tmp/walhub-395 (pre-existing; left in place). Main worktree untouched (still clean apart from pre-existing untracked .opencode/). ACCEPTANCE (all vs web/src/pages/Repos.jsx): (1) Composed header OK — profile-header flex row :316 (identity left: h1 displayName :318, handle, location·timezone, bio via renderBody, Edit profile; avatar right in profile-avatar :354 with Regenerate/Remove beneath :366-373). New-repo CTA in its own top-right row above the grid :310-314 (implementer's call per issue), canWrite-gated as before. (2) Orphan gone OK — the only 'mb-3 flex justify-end' left is :310 and it holds the CTA, not the avatar; avatar img exists only inside profile-avatar. (3) Orgs: behavior-identical, NOT byte-untouched as D-WEB-9 claimed — redundant inner isOrg guards folded into the outer Show gate; '{isOrg() ? orgName() : displayName()}' -> '{orgName()}'. One real (correct, #359-aligned) behavior change: owner-profile location/bio/Edit blocks previously also rendered on org pages (old guards lacked !isOrg); now non-org-only so org pages read the org doc alone. Tests pin this. FIXED DOC: pushed d6175e9 rewording D-WEB-9 to 'behavior-identical … owner-profile fields now render non-org-only'. (4) Gates unchanged OK — Edit = getProfile()?.can_edit (:336,:382); avatar actions = isSelf() (:364), Remove on userSrc; isSelf/regenerate/remove/refreshAvatar + invalidate user:/me (:250-281) byte-identical to main. #376 intact. (5) 390px OK by reasoning (no browser per review instructions — noted explicitly): flex-col-reverse + identity-first DOM paints avatar on top, both columns full-width (flex-1 / shrink-0, flex-wrap buttons), no dead half-width band. (6) h1 OK — single h1 on the page (:318, user only); org keeps h2 (:400, mutually exclusive Shows); ProfileForm has no headings; displayName() falls back to owner slug. (7) No-avatar self keeps Regenerate OK — column gate 'userSrc() || isSelf()' (:353) preserves #376 opt-back-in; correct. (8) Light+dark OK by reasoning (no browser): ring-zinc-300 dark:ring-zinc-600 kept on avatar; layout classes theme-independent. (9) No backend change (diff = DEVIATIONS.md + Repos.jsx + 2 test files), no new deps (package manifests untouched), D-WEB-9 warranted (law 12, follows D-WEB-8 pattern; wording fixed by d6175e9). TESTS: full 'node --test web/test/unit/*.test.js' = 786 pass / 2 fail; the 2 failures are both server-dependent smoke.test.js cases (they fetch :8080, currently answering 401 from a live instance I did not touch) — smoke.test.js is byte-identical main..branch so definitionally pre-existing/environmental (with no server they skip: 3 skipped). All 8 new profile-header.test.js + rewritten identity-nav #395 test pass (38/38 in the two files). vite build + esbuild SDK build green (chunk-size warning only, advisory). MERGE RECOMMENDATION: ready to merge.
Author
Owner

Fixed by PR #400 (review clean + one D-WEB-9 wording fix by reviewer; header composed, org leak closed, gates intact), merged. Closing.

Fixed by PR #400 (review clean + one D-WEB-9 wording fix by reviewer; header composed, org leak closed, gates intact), merged. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#395
No description provided.