Owner profile header: GitHub-style composition — grouped action row, no dead space #403

Closed
opened 2026-09-12 18:30:40 +00:00 by crueber · 3 comments
Owner

What's requested

The owner-profile header (/:owner, web/src/pages/Repos.jsx) is already a composed two-column block after #395/#400 (identity left, avatar right) — this ticket closes the remaining gaps so it reads like GitHub's profile header and has zero dead space. Layout only; all gating, data flow, and cache behavior unchanged.

Evidence (tree f75a04b, web/src/pages/Repos.jsx)

  1. Orphaned CTA row above the header (L310-315). When the viewer has write access, New repository renders alone in a full-width mb-3 flex justify-end row. At any width that is a dead half-to-full-width band above the header — the exact "orphan row that knows nothing about the header" shape #395 removed for the avatar, now carried by the button. GitHub has no such band: profile actions sit inside the header block.
  2. Identity typography undersized (L318). The h1 is text-xl (20px). GitHub's profile name is the largest text on the page (~24px / text-2xl); here the page reads with no visual anchor before the muted handle.
  3. No section break under the header (L441). The "Repositories" heading follows the bio/edit block with only mt-6 whitespace — GitHub separates the profile header from the tab/repo content with a hairline divider; without it the header and the list run together.
  4. Handle/identity rhythm (L322-328). Handle, and the location · timezone line, are fine content-wise (verified: @owner renders only when a display name is set; location/timezone joined with ·), but the vertical rhythm is generic mt-1/mt-3 — GitHub tightens handle under the name (small muted, minimal gap) and gives bio a slightly larger gap than meta lines.

Proposed design

One composed header, first element of the page (no sibling rows above it):

  • Container: keep the profile-header flex flex-col-reverse gap-4 sm:flex-row sm:items-start sm:justify-between from #395, add pb-6 border-b border-zinc-200 dark:border-zinc-700 so the Repositories section (L441) starts on a clean rule.
  • Left column (identity, min-w-0 flex-1):
    • h1 bumped to text-2xl font-semibold.
    • @{owner} handle directly under the name, text-sm muted, tight gap (mt-0.5).
    • Location · timezone line stays text-sm muted (mt-1).
    • Bio markdown-body mt-3 unchanged (shared render pipeline, do not touch).
    • Action row under the bio (replaces the orphan CTA row): a single mt-3 flex flex-wrap gap-2 row containing New repository (btn primary) — when canWrite() — and Edit profile (btn) — when profile().can_edit — grouped together, GitHub-style, acting on the page the header names. Delete the standalone mb-3 flex justify-end block (L309-315).
  • Right column (avatar, shrink-0): unchanged from #395/#376 — h-24 w-24 rounded-full with ring, Regenerate/Remove grouped beneath, gated as today (self-only; the client never decides). Top-aligned with the h1 (sm:items-start already does this).
  • Mobile (390px, #273-#278 conventions): keep flex-col-reverse stacking (avatar block on top, identity below, both full-width, buttons wrapping). The grouped action row wraps naturally; verify no root overflow at 390px.

Architecture notes

  • Pure client layout — no API/wire changes, no cached-payload fields touched (profile doc, user:{owner}, repos:{owner} keys all unchanged), so no ETag/cache-class concern.
  • All gating stays server-decided: can_edit for Edit profile, require_write for New repository, self-or-admin for avatar actions (Repos.jsx L236-278). Layout must not change any Show condition.
  • The org landing branch on the same page (L398-440, #359) still uses the older inline-36px-avatar h2 + loose paragraphs shape; it should adopt this same composition eventually, but that is out of scope here — its own ticket if wanted.
  • Live-render verification was not possible while filing: hub.packden.us currently 401s every page/API for anonymous requests (auth-gated, no anonymous_read), so the evidence above is from the tree, not a screenshot. The implementer should eyeball both widths locally.

Acceptance criteria

  • No full-width row exists above the profile header; New repository renders inside the header's identity column action row (grouped with Edit profile when both gates pass)
  • Profile h1 renders at text-2xl; handle sits tight under it
  • Header closes with a bottom divider before the Repositories section
  • Identity left / avatar right composition from #395 preserved; avatar column untouched functionally (gates, invalidation, note rendering)
  • 390px: stacked layout with no horizontal page overflow (scrollWidth === clientWidth)
  • All Show gates byte-identical to pre-change (canWrite, can_edit, isSelf, userSrc)
  • No changes to any fetch, cache key, or server interaction
## What's requested The owner-profile header (`/:owner`, `web/src/pages/Repos.jsx`) is already a composed two-column block after #395/#400 (identity left, avatar right) — this ticket closes the remaining gaps so it reads like GitHub's profile header and has zero dead space. Layout only; all gating, data flow, and cache behavior unchanged. ## Evidence (tree f75a04b, web/src/pages/Repos.jsx) 1. **Orphaned CTA row above the header (L310-315).** When the viewer has write access, `New repository` renders alone in a full-width `mb-3 flex justify-end` row. At any width that is a dead half-to-full-width band above the header — the exact "orphan row that knows nothing about the header" shape #395 removed for the avatar, now carried by the button. GitHub has no such band: profile actions sit *inside* the header block. 2. **Identity typography undersized (L318).** The h1 is `text-xl` (20px). GitHub's profile name is the largest text on the page (~24px / `text-2xl`); here the page reads with no visual anchor before the muted handle. 3. **No section break under the header (L441).** The "Repositories" heading follows the bio/edit block with only `mt-6` whitespace — GitHub separates the profile header from the tab/repo content with a hairline divider; without it the header and the list run together. 4. **Handle/identity rhythm (L322-328).** Handle, and the location · timezone line, are fine content-wise (verified: `@owner` renders only when a display name is set; location/timezone joined with `·`), but the vertical rhythm is generic `mt-1`/`mt-3` — GitHub tightens handle under the name (small muted, minimal gap) and gives bio a slightly larger gap than meta lines. ## Proposed design One composed header, first element of the page (no sibling rows above it): - **Container:** keep the `profile-header flex flex-col-reverse gap-4 sm:flex-row sm:items-start sm:justify-between` from #395, add `pb-6 border-b border-zinc-200 dark:border-zinc-700` so the Repositories section (L441) starts on a clean rule. - **Left column (identity, `min-w-0 flex-1`):** - `h1` bumped to `text-2xl font-semibold`. - `@{owner}` handle directly under the name, `text-sm muted`, tight gap (`mt-0.5`). - Location · timezone line stays `text-sm muted` (`mt-1`). - Bio `markdown-body mt-3` unchanged (shared render pipeline, do not touch). - **Action row under the bio (replaces the orphan CTA row):** a single `mt-3 flex flex-wrap gap-2` row containing `New repository` (`btn primary`) — when `canWrite()` — and `Edit profile` (`btn`) — when `profile().can_edit` — grouped together, GitHub-style, acting on the page the header names. Delete the standalone `mb-3 flex justify-end` block (L309-315). - **Right column (avatar, `shrink-0`):** unchanged from #395/#376 — `h-24 w-24 rounded-full` with ring, Regenerate/Remove grouped beneath, gated as today (self-only; the client never decides). Top-aligned with the h1 (`sm:items-start` already does this). - **Mobile (390px, #273-#278 conventions):** keep `flex-col-reverse` stacking (avatar block on top, identity below, both full-width, buttons wrapping). The grouped action row wraps naturally; verify no root overflow at 390px. ## Architecture notes - Pure client layout — no API/wire changes, no cached-payload fields touched (profile doc, `user:{owner}`, `repos:{owner}` keys all unchanged), so no ETag/cache-class concern. - All gating stays server-decided: `can_edit` for Edit profile, `require_write` for New repository, self-or-admin for avatar actions (Repos.jsx L236-278). Layout must not change any `Show` condition. - The org landing branch on the same page (L398-440, #359) still uses the older inline-36px-avatar h2 + loose paragraphs shape; it should adopt this same composition eventually, but that is out of scope here — its own ticket if wanted. - Live-render verification was not possible while filing: hub.packden.us currently 401s every page/API for anonymous requests (auth-gated, no anonymous_read), so the evidence above is from the tree, not a screenshot. The implementer should eyeball both widths locally. ## Acceptance criteria - [ ] No full-width row exists above the profile header; `New repository` renders inside the header's identity column action row (grouped with `Edit profile` when both gates pass) - [ ] Profile h1 renders at `text-2xl`; handle sits tight under it - [ ] Header closes with a bottom divider before the Repositories section - [ ] Identity left / avatar right composition from #395 preserved; avatar column untouched functionally (gates, invalidation, note rendering) - [ ] 390px: stacked layout with no horizontal page overflow (`scrollWidth === clientWidth`) - [ ] All `Show` gates byte-identical to pre-change (canWrite, can_edit, isSelf, userSrc) - [ ] No changes to any fetch, cache key, or server interaction
crueber added this to the v1 milestone 2026-09-12 18:31:09 +00:00
Author
Owner

Fixed by #407 (branch fix/issue-403): grouped action row under the bio, h1 text-2xl + tight handle, bottom divider before Repositories. Gates byte-identical, zero fetch/cache changes; 46/46 header tests green, vite build green.

Fixed by #407 (branch fix/issue-403): grouped action row under the bio, h1 text-2xl + tight handle, bottom divider before Repositories. Gates byte-identical, zero fetch/cache changes; 46/46 header tests green, vite build green.
Author
Owner

REVIEW PR #407 (fix/issue-403, commit d47159a) — verified in scratch worktree /tmp/pr407, main worktree untouched (clean before/after; only pre-existing untracked .opencode/).

All 7 acceptance criteria PASS, verified against the tree (no browser per review instructions — node tests + reasoning only):

  1. No row above header — PASS. web/src/pages/Repos.jsx: orphan mb-3 flex justify-end CTA block deleted; pre-header region (Show !isOrg → profile-header) contains no justify-end/mb-3/New-repository (checked programmatically).
  2. Action row grouped — PASS. Repos.jsx ~L326: one mt-3 flex flex-wrap gap-2 div under the bio holding New repository (own canWrite gate) + Edit profile (own can_edit && !editing gate). Both-visible → both buttons, either → single button, both-hidden → empty div (12px mt-3 only; inherent to the byte-identical-gate constraint, not a finding). Order pinned: New leads, Edit follows.
  3. h1 text-2xl, handle tight — PASS. <h1 class=text-2xl font-semibold>, handle <p class=muted mt-0.5 text-sm>; location·timezone stays mt-1, bio markdown-body mt-3.
  4. Divider — PASS. profile-header tag carries pb-6 border-b border-zinc-200 dark:border-zinc-700; Repositories h3 keeps mt-6 below the rule.
  5. Avatar column untouched — PASS. profile-avatar region diff main→PR is byte-identical (verified by script): userSrc()||isSelf() gate, Regenerate/Remove grouping, #376 invalidate(user:${owner()}) all intact.
  6. Org branch untouched — PASS. isOrg() region byte-identical (#359: OrgAvatar size 36, own CTA, Manage affordance; no profile-header/border-b leak).
  7. Show gates byte-identical — PASS. Sorted multiset of all identical main→PR; only canWrite() moved position (the change itself). No gate widened/narrowed.
  8. Zero fetch/cache/server — PASS. Diff touches only Repos.jsx + DEVIATIONS.md + 3 test files; no .go, no SDK, no package.json; cache keys (repos:/profile:/org:/user:) pinned by new test.
  9. 390px — PASS by reasoning (no browser): flex-col-reverse stack, min-w-0 flex-1, shrink-0 avatar, action row flex-wrap; no new unbreakable content vs pre-change.
  10. D-WEB-10 — warranted, docs accurate: layout-only, zero new deps (runtime deps still exactly solid-js + @solidjs/router + marked@18.0.11 + dompurify@3.4.15), follows D-WEB-8/9 pattern, Sources L1/L12 cited.

TESTS: targeted 46/46 green (profile-header-403: 8 new + profile-header + identity-nav); full suite 803 pass / 2 fail — the 2 failures are smoke.test.js (need live server), confirmed identical on main; vite build green (2.14s). No fixes needed, nothing pushed.

RECOMMENDATION: ready to merge.

REVIEW PR #407 (fix/issue-403, commit d47159a) — verified in scratch worktree /tmp/pr407, main worktree untouched (clean before/after; only pre-existing untracked .opencode/). All 7 acceptance criteria PASS, verified against the tree (no browser per review instructions — node tests + reasoning only): 1. No row above header — PASS. web/src/pages/Repos.jsx: orphan `mb-3 flex justify-end` CTA block deleted; pre-header region (Show !isOrg → profile-header) contains no justify-end/mb-3/New-repository (checked programmatically). 2. Action row grouped — PASS. Repos.jsx ~L326: one `mt-3 flex flex-wrap gap-2` div under the bio holding New repository (own canWrite gate) + Edit profile (own can_edit && !editing gate). Both-visible → both buttons, either → single button, both-hidden → empty div (12px mt-3 only; inherent to the byte-identical-gate constraint, not a finding). Order pinned: New leads, Edit follows. 3. h1 text-2xl, handle tight — PASS. `<h1 class=text-2xl font-semibold>`, handle `<p class=muted mt-0.5 text-sm>`; location·timezone stays mt-1, bio markdown-body mt-3. 4. Divider — PASS. profile-header tag carries `pb-6 border-b border-zinc-200 dark:border-zinc-700`; Repositories h3 keeps mt-6 below the rule. 5. Avatar column untouched — PASS. profile-avatar region diff main→PR is byte-identical (verified by script): userSrc()||isSelf() gate, Regenerate/Remove grouping, #376 invalidate(`user:${owner()}`) all intact. 6. Org branch untouched — PASS. isOrg() region byte-identical (#359: OrgAvatar size 36, own CTA, Manage affordance; no profile-header/border-b leak). 7. Show gates byte-identical — PASS. Sorted multiset of all <Show when> identical main→PR; only canWrite() moved position (the change itself). No gate widened/narrowed. 8. Zero fetch/cache/server — PASS. Diff touches only Repos.jsx + DEVIATIONS.md + 3 test files; no .go, no SDK, no package.json; cache keys (repos:/profile:/org:/user:) pinned by new test. 9. 390px — PASS by reasoning (no browser): flex-col-reverse stack, min-w-0 flex-1, shrink-0 avatar, action row flex-wrap; no new unbreakable content vs pre-change. 10. D-WEB-10 — warranted, docs accurate: layout-only, zero new deps (runtime deps still exactly solid-js + @solidjs/router + marked@18.0.11 + dompurify@3.4.15), follows D-WEB-8/9 pattern, Sources L1/L12 cited. TESTS: targeted 46/46 green (profile-header-403: 8 new + profile-header + identity-nav); full suite 803 pass / 2 fail — the 2 failures are smoke.test.js (need live server), confirmed identical on main; vite build green (2.14s). No fixes needed, nothing pushed. RECOMMENDATION: ready to merge.
Author
Owner

Fixed by PR #407 (review clean — all 7 criteria pass, gates byte-identical, zero server changes), merged. Closing.

Fixed by PR #407 (review clean — all 7 criteria pass, gates byte-identical, zero server changes), 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#403
No description provided.