New repo page: Owner/Name row misaligned — matched heights, aligned labels, narrower Owner column (also on Import) #497

Closed
opened 2026-09-13 21:33:43 +00:00 by crueber · 3 comments
Owner

What's requested

On the New repository page, the Owner select and Name input sit side-by-side in a 50/50 sm:grid-cols-2 grid, and the pair is visibly misaligned: the two controls don't read as matched-height siblings, the Name column carries a fixed-height error slot the Owner column lacks (making the columns unequal), and the Owner column is far wider than a bounded dropdown needs — crowding the Name field users actually type into.

Prescribe a matched pair:

  1. Matched heights — Owner select and Name input must render the same control height and share a common baseline across the row. Native <select> vs <input> render a few px apart in some browsers even with the same .input class; normalize explicitly (e.g. a shared height utility on both, or appearance-none styling on the select) rather than relying on the class alone.
  2. Aligned labels — the "Owner" and "Name" labels must sit on the same line at the same size/weight, and any error/help text under Name must not drag the Owner column's height or shift the row. Keep Name's reserved error slot (min-h-[2rem], aria-live) but make it not affect the sibling column's geometry (e.g. error text outside the two-column grid row, or items-start on the grid so the controls stay top-aligned).
  3. Narrower Owner column — the Owner dropdown holds a bounded list of names (self + member orgs, from allowedOwners); it doesn't need 50% of the row. Use an asymmetric split (e.g. sm:grid-cols-[minmax(0,1fr)_minmax(0,2fr)] or similar), implementer's call on exact ratio, with minmax(0, …) so long owner names truncate instead of blowing the column.

Evidence (static read of current tree, commit b3544e5)

  • web/src/pages/New.jsx:147-186 — the row:
    • <div class="grid grid-cols-1 gap-3 sm:grid-cols-2"> wraps both fields: equal 50/50 columns.
    • Owner label (New.jsx:148-168): <label class="grid gap-1"> with a <span> label + <select class="input font-mono"> (with a disabled fallback select while owners load). No sub-text.
    • Name label (New.jsx:169-186): same shape plus <p id="new-name-error" class="min-h-[2rem] …"> inside the label — a 2rem-tall block that exists only in the Name column, so the two grid columns have unequal intrinsic heights and the row's baseline math differs per column.
  • web/src/pages/Import.jsx:279-319 — the identical pair is copy-pasted on Import (import-owner select + import-name input, same sm:grid-cols-2, same in-label min-h-[2rem] error paragraph). Whatever the fix is, it must land on both or the two pages will diverge.
  • web/src/pages/OrgNew.jsx:80-124 — New organization uses stacked full-width grid gap-1 labels (no two-column row), so it's not affected by the 50/50 issue, but if a shared Owner/Name field component is extracted it should be the one place this idiom lives going forward.
  • web/src/ui.css:120-123 — .input is w-full px-2.5 py-1.5 text-sm; it styles both controls but does nothing to pin select/input rendered heights equal (native select chrome differs per browser), which is the height-match gap.

Architecture notes

  • Owner options come from allowedOwners in web/src/lib/orgs.js (self + member orgs, server-admitted set per #346); the list is bounded and short — hence the narrower column ask.
  • The Owner/Name pair markup is duplicated verbatim between New.jsx and Import.jsx. Extracting a small shared component (or at minimum applying the identical classes to both) is preferred so the layout can't drift again; headless validation helpers (validateRepoName) already live in web/src/lib/ and are unaffected.
  • OrgNew.jsx has no Owner field — "uniform across New/Import/OrgNew" resolves to: New and Import get the fixed paired row; OrgNew just shouldn't regress if a shared component is introduced.

Acceptance criteria

  • On /new, Owner select and Name input are the same rendered height and top-aligned; their labels sit on one shared baseline.
  • The Name error slot does not change the Owner column's height or position; validation errors still render in place with aria-live intact.
  • Owner column is narrower than Name column at sm: and up (asymmetric split); long owner names truncate/ellipsis rather than stretching the column; mobile single-column stack still works.
  • /import shows the same corrected layout (identical classes or shared component).
  • Owner select keyboard/a11y behavior unchanged (aria-label, disabled loading fallback intact on both pages).
## What's requested On the **New repository** page, the **Owner** select and **Name** input sit side-by-side in a 50/50 `sm:grid-cols-2` grid, and the pair is visibly misaligned: the two controls don't read as matched-height siblings, the Name column carries a fixed-height error slot the Owner column lacks (making the columns unequal), and the Owner column is far wider than a bounded dropdown needs — crowding the Name field users actually type into. Prescribe a matched pair: 1. **Matched heights** — Owner select and Name input must render the same control height and share a common baseline across the row. Native `<select>` vs `<input>` render a few px apart in some browsers even with the same `.input` class; normalize explicitly (e.g. a shared height utility on both, or `appearance-none` styling on the select) rather than relying on the class alone. 2. **Aligned labels** — the "Owner" and "Name" labels must sit on the same line at the same size/weight, and any error/help text under Name must not drag the Owner column's height or shift the row. Keep Name's reserved error slot (`min-h-[2rem]`, aria-live) but make it not affect the sibling column's geometry (e.g. error text outside the two-column grid row, or `items-start` on the grid so the controls stay top-aligned). 3. **Narrower Owner column** — the Owner dropdown holds a bounded list of names (self + member orgs, from `allowedOwners`); it doesn't need 50% of the row. Use an asymmetric split (e.g. `sm:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]` or similar), implementer's call on exact ratio, with `minmax(0, …)` so long owner names truncate instead of blowing the column. ## Evidence (static read of current tree, commit b3544e5) - `web/src/pages/New.jsx:147-186` — the row: - `<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">` wraps both fields: equal 50/50 columns. - Owner label (`New.jsx:148-168`): `<label class="grid gap-1">` with a `<span>` label + `<select class="input font-mono">` (with a disabled fallback select while owners load). No sub-text. - Name label (`New.jsx:169-186`): same shape plus `<p id="new-name-error" class="min-h-[2rem] …">` **inside** the label — a 2rem-tall block that exists only in the Name column, so the two grid columns have unequal intrinsic heights and the row's baseline math differs per column. - `web/src/pages/Import.jsx:279-319` — the identical pair is copy-pasted on Import (`import-owner` select + `import-name` input, same `sm:grid-cols-2`, same in-label `min-h-[2rem]` error paragraph). Whatever the fix is, it must land on both or the two pages will diverge. - `web/src/pages/OrgNew.jsx:80-124` — New organization uses stacked full-width `grid gap-1` labels (no two-column row), so it's not affected by the 50/50 issue, but if a shared Owner/Name field component is extracted it should be the one place this idiom lives going forward. - `web/src/ui.css:120-123` — `.input` is `w-full px-2.5 py-1.5 text-sm`; it styles both controls but does nothing to pin select/input rendered heights equal (native select chrome differs per browser), which is the height-match gap. ## Architecture notes - Owner options come from `allowedOwners` in `web/src/lib/orgs.js` (self + member orgs, server-admitted set per #346); the list is bounded and short — hence the narrower column ask. - The Owner/Name pair markup is duplicated verbatim between `New.jsx` and `Import.jsx`. Extracting a small shared component (or at minimum applying the identical classes to both) is preferred so the layout can't drift again; headless validation helpers (`validateRepoName`) already live in `web/src/lib/` and are unaffected. - `OrgNew.jsx` has no Owner field — "uniform across New/Import/OrgNew" resolves to: New and Import get the fixed paired row; OrgNew just shouldn't regress if a shared component is introduced. ## Acceptance criteria - [ ] On `/new`, Owner select and Name input are the same rendered height and top-aligned; their labels sit on one shared baseline. - [ ] The Name error slot does not change the Owner column's height or position; validation errors still render in place with `aria-live` intact. - [ ] Owner column is narrower than Name column at `sm:` and up (asymmetric split); long owner names truncate/ellipsis rather than stretching the column; mobile single-column stack still works. - [ ] `/import` shows the same corrected layout (identical classes or shared component). - [ ] Owner select keyboard/a11y behavior unchanged (`aria-label`, disabled loading fallback intact on both pages).
crueber added this to the v1 milestone 2026-09-13 21:34:11 +00:00
Author
Owner

Fixed by PR #500 (#500) — shared OwnerNameRow component on /new + /import: matched h-9 heights, items-start + below-grid error slot (aria-live intact), asymmetric sm 1fr/2fr with truncate. Tests 1097/1095/2 (2 pre-existing smoke fails), vite green. Browser proof open.

Fixed by PR #500 (https://git.packden.us/crueber/walhub/pulls/500) — shared OwnerNameRow component on /new + /import: matched h-9 heights, items-start + below-grid error slot (aria-live intact), asymmetric sm 1fr/2fr with truncate. Tests 1097/1095/2 (2 pre-existing smoke fails), vite green. Browser proof open.
Author
Owner

REVIEW of PR #500 (fix/issue-497, commit c4ef9b7) — verified in scratch worktree /tmp/pr500 (since removed). No browser drive (node tests + source/CSS reasoning only — noted explicitly per instructions).

ACCEPTANCE CRITERIA (all 5 met):

  1. Matched heights/top-aligned labels — OwnerNameRow.jsx: both selects (loading fallback + populated) and the Name input carry explicit h-9 (OwnerNameRow.jsx:52,60,70); grid has items-start (OwnerNameRow.jsx:46). h-9 pins border-box height on both controls so native select-vs-input chrome cannot differ; labels are identical text-sm font-medium spans, Owner first. The issue allowed 'shared height utility OR appearance-none' — shared-h-9 satisfies it; keeping native chrome (no appearance-none, pinned by test) preserves the dropdown arrow/keyboard with no custom chevron. Sound.
  2. Error slot shift-free, a11y intact — slot moved OUT of the grid to a full-width

    below it (OwnerNameRow.jsx:84) AND items-start on the grid (belt and braces, both options the issue suggested). min-h-[2rem] reserve + aria-live=polite kept; input aria-describedby/aria-invalid wiring intact (OwnerNameRow.jsx:77-78); slot unconditionally rendered (no Show gate — pinned). aria-describedby always resolves (id always mounted). No sibling-shift path remains.

  3. Asymmetric split — sm:grid-cols-[minmax(0,1fr)_minmax(0,2fr)] (Owner 1/3, Name 2/3), min-w-0 on both cells, truncate on select, grid-cols-1 mobile stack. Compiled CSS confirmed: .sm:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]{grid-template-columns:minmax(0,1fr) minmax(0,2fr)} + .h-9 + items-start all present in dist/assets/*.css after vite build.
  4. Shared component, no drift — New.jsx (prefix=new) + Import.jsx (prefix=import) render with identical props; old copy-pasted markup deleted from both (no in-page error slot, no in-page two-col row — pinned); id namespace (new-owner/new-name/new-name-error, import-*) byte-identical to the old hardcoded ids. Submit handlers, validation imports (validateRepoName/validateRepoChars/allowedOwners), fetches, navigation untouched. New.jsx solid-js import correctly drops now-unused For (no remaining <For; Show still used 4x, retained); Import.jsx still uses For elsewhere (mirror presets/bars/log/heads), import untouched.
  5. Owner a11y unchanged — aria-label=Owner on both selects, null-loading disabled fallback with current-owner option, options from admitted set via For, Name aria-label kept. OrgNew.jsx untouched (no diff; test pins no OwnerNameRow/owner wiring/grid-cols-2).

PINS FAITHFUL: #479 collapse test retargeted to shared row (collapsing base + asymmetric sm: split pinned, no bare grid-cols-2); #486 aria/no-shift tests retargeted to below-grid slot (reserved/live/wired + items-start). No backend change (no non-web/non-docs files in diff), no new deps (no package.json/lock changes), docs accurate (12_web_ui.md entry matches verified numbers).

TESTS: full node --test web/test/unit/*.test.js in scratch worktree (node_modules symlinked from main): 1097 total / 1095 pass / 2 fail — the 2 failures are the live-server smoke subtests (smoke.test.js, need a server on :8080); verified IDENTICAL 2 failures on pristine main worktree, so environmental/pre-existing, zero PR-caused. Targeted owner-name-row-497 + create-forms-479 + repo-name-486: 24/24 pass. vite build green.

NO FIXES PUSHED — nothing to fix; branch left untouched.

MERGE RECOMMENDATION: ready to merge.

REVIEW of PR #500 (fix/issue-497, commit c4ef9b7) — verified in scratch worktree /tmp/pr500 (since removed). No browser drive (node tests + source/CSS reasoning only — noted explicitly per instructions). ACCEPTANCE CRITERIA (all 5 met): 1. Matched heights/top-aligned labels — OwnerNameRow.jsx: both selects (loading fallback + populated) and the Name input carry explicit h-9 (OwnerNameRow.jsx:52,60,70); grid has items-start (OwnerNameRow.jsx:46). h-9 pins border-box height on both controls so native select-vs-input chrome cannot differ; labels are identical text-sm font-medium spans, Owner first. The issue allowed 'shared height utility OR appearance-none' — shared-h-9 satisfies it; keeping native chrome (no appearance-none, pinned by test) preserves the dropdown arrow/keyboard with no custom chevron. Sound. 2. Error slot shift-free, a11y intact — slot moved OUT of the grid to a full-width <p> below it (OwnerNameRow.jsx:84) AND items-start on the grid (belt and braces, both options the issue suggested). min-h-[2rem] reserve + aria-live=polite kept; input aria-describedby/aria-invalid wiring intact (OwnerNameRow.jsx:77-78); slot unconditionally rendered (no Show gate — pinned). aria-describedby always resolves (id always mounted). No sibling-shift path remains. 3. Asymmetric split — sm:grid-cols-[minmax(0,1fr)_minmax(0,2fr)] (Owner 1/3, Name 2/3), min-w-0 on both cells, truncate on select, grid-cols-1 mobile stack. Compiled CSS confirmed: .sm\:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]{grid-template-columns:minmax(0,1fr) minmax(0,2fr)} + .h-9 + items-start all present in dist/assets/*.css after vite build. 4. Shared component, no drift — New.jsx (prefix=new) + Import.jsx (prefix=import) render <OwnerNameRow> with identical props; old copy-pasted markup deleted from both (no in-page error slot, no in-page two-col row — pinned); id namespace (new-owner/new-name/new-name-error, import-*) byte-identical to the old hardcoded ids. Submit handlers, validation imports (validateRepoName/validateRepoChars/allowedOwners), fetches, navigation untouched. New.jsx solid-js import correctly drops now-unused For (no remaining <For; Show still used 4x, retained); Import.jsx still uses For elsewhere (mirror presets/bars/log/heads), import untouched. 5. Owner a11y unchanged — aria-label=Owner on both selects, null-loading disabled fallback with current-owner option, options from admitted set via For, Name aria-label kept. OrgNew.jsx untouched (no diff; test pins no OwnerNameRow/owner wiring/grid-cols-2). PINS FAITHFUL: #479 collapse test retargeted to shared row (collapsing base + asymmetric sm: split pinned, no bare grid-cols-2); #486 aria/no-shift tests retargeted to below-grid slot (reserved/live/wired + items-start). No backend change (no non-web/non-docs files in diff), no new deps (no package.json/lock changes), docs accurate (12_web_ui.md entry matches verified numbers). TESTS: full node --test web/test/unit/*.test.js in scratch worktree (node_modules symlinked from main): 1097 total / 1095 pass / 2 fail — the 2 failures are the live-server smoke subtests (smoke.test.js, need a server on :8080); verified IDENTICAL 2 failures on pristine main worktree, so environmental/pre-existing, zero PR-caused. Targeted owner-name-row-497 + create-forms-479 + repo-name-486: 24/24 pass. vite build green. NO FIXES PUSHED — nothing to fix; branch left untouched. MERGE RECOMMENDATION: ready to merge.
Author
Owner

Fixed by PR #500 (review clean — all 5 criteria pass, h-9 + below-grid slot + asymmetric split verified in compiled CSS), merged. Closing.

Fixed by PR #500 (review clean — all 5 criteria pass, h-9 + below-grid slot + asymmetric split verified in compiled CSS), 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#497
No description provided.