Empty-state section cards: action button collides with hint text — harden CSS and extract SectionCard #275

Closed
opened 2026-08-04 14:20:40 +02:00 by dominik.polakovics · 1 comment

Bug

At empty states, the action button in a section card's head row can sit on top of the hint text below it. Reported on repo settings → Imports: the "+ Add import" button covers the hint "Other lab repos this repo's instances may read as read-only snapshots, mounted at spawn." Reporter sees it persistently, on desktop and mobile, on a fresh build of main. Other sections built on the same pattern (e.g. Secrets) are presumed affected.

Diagnosis (from code inspection)

The overlap class is enabled by a fragile stack — no absolute positioning is involved:

  1. .hint-block (web/src/styles/forms.css) uses a negative top margin (margin: -0.35rem 0 0.9rem), pulling the hint up toward whatever precedes it. It hard-codes an assumption about the height of the element above.
  2. .card-head (web/src/styles/cards.css) does not reset the h2 margins, unlike the sibling pattern .section-head (web/src/styles/shell.css, which has h2 { margin: 0 }). The head row's height therefore depends on browser-default heading margins — font-metric/zoom dependent, and the headings load Source Serif 4 with font-display: swap, so the height even shifts during font load. When the row renders short, the button (36px, button.small) is the tallest item and the hint's negative margin pulls the text into it.
  3. The "title + action button + hint + empty state" card is hand-rolled in every section — there is no shared component, so nothing enforces the spacing contract. The empty state itself is <p class="empty"> repeated at ~16 call sites.

Note: the exact pixel mechanism was not reproduced in-session (no browser available); the fix below removes the entire enabling class rather than patching one geometry. The reporter verifies visually before merge.

Fix (agreed with reporter)

1. CSS hardening (fixes every page immediately):

  • .hint-block: drop the negative top margin (margin: 0 0 0.9rem).
  • .card-head: add h2 { margin: 0; overflow-wrap: anywhere; } to match .section-head h2. Since the heading's default margins previously provided the row's vertical breathing room, give .card-head an explicit margin-bottom (align with .section-head's margin-bottom: var(--space-3)) so the visual rhythm is preserved deliberately instead of accidentally.

2. Extract a shared SectionCard component (web/src/components/SectionCard.tsx):

  • Owns the whole contract: title, action slot (JSX for the head-row button), hint (optional), and body children (error banner / add-form / empty-vs-list markup stay with the caller).
  • All spacing between head, hint, and body lives ONLY in this component. The empty-state paragraph can stay <p class="empty"> inside children for now.

3. Migrate all settings sections to SectionCard:

  • web/src/routes/repo-settings/sections/: Imports, Secrets, Schedules, Agents, Autoland, Branches, Danger, General, Integrations, Runner.
  • web/src/routes/settings/sections/ where the pattern appears (e.g. Notifications).
  • Out of scope: non-settings routes (Tokens, Credentials, History, Repos, RepoIssues, RepoCRs, …) — they are covered by the CSS hardening now and migrate in the follow-up umbrella issue.

Acceptance criteria

  • No negative margins remain in the head/hint area; head-row height no longer depends on heading font metrics.
  • All listed settings sections render title/action/hint through SectionCard; component test asserts the structure (title, action slot, hint, children render in order).
  • Existing section tests still pass; npm run build, npm run test, npm run lint clean.
  • Reporter visually confirms on repo settings → Imports (empty state) that the button no longer touches/covers the hint, desktop and mobile widths.
## Bug At empty states, the action button in a section card's head row can sit on top of the hint text below it. Reported on **repo settings → Imports**: the "+ Add import" button covers the hint "Other lab repos this repo's instances may read as read-only snapshots, mounted at spawn." Reporter sees it persistently, on desktop and mobile, on a fresh build of main. Other sections built on the same pattern (e.g. Secrets) are presumed affected. ## Diagnosis (from code inspection) The overlap class is enabled by a fragile stack — no absolute positioning is involved: 1. `.hint-block` (`web/src/styles/forms.css`) uses a **negative top margin** (`margin: -0.35rem 0 0.9rem`), pulling the hint up toward whatever precedes it. It hard-codes an assumption about the height of the element above. 2. `.card-head` (`web/src/styles/cards.css`) does **not** reset the `h2` margins, unlike the sibling pattern `.section-head` (`web/src/styles/shell.css`, which has `h2 { margin: 0 }`). The head row's height therefore depends on browser-default heading margins — font-metric/zoom dependent, and the headings load `Source Serif 4` with `font-display: swap`, so the height even shifts during font load. When the row renders short, the button (36px, `button.small`) is the tallest item and the hint's negative margin pulls the text into it. 3. The "title + action button + hint + empty state" card is hand-rolled in every section — there is no shared component, so nothing enforces the spacing contract. The empty state itself is `<p class="empty">` repeated at ~16 call sites. Note: the exact pixel mechanism was not reproduced in-session (no browser available); the fix below removes the entire enabling class rather than patching one geometry. The reporter verifies visually before merge. ## Fix (agreed with reporter) **1. CSS hardening (fixes every page immediately):** - `.hint-block`: drop the negative top margin (`margin: 0 0 0.9rem`). - `.card-head`: add `h2 { margin: 0; overflow-wrap: anywhere; }` to match `.section-head h2`. Since the heading's default margins previously provided the row's vertical breathing room, give `.card-head` an explicit `margin-bottom` (align with `.section-head`'s `margin-bottom: var(--space-3)`) so the visual rhythm is preserved deliberately instead of accidentally. **2. Extract a shared `SectionCard` component** (`web/src/components/SectionCard.tsx`): - Owns the whole contract: `title`, `action` slot (JSX for the head-row button), `hint` (optional), and body children (error banner / add-form / empty-vs-list markup stay with the caller). - All spacing between head, hint, and body lives ONLY in this component. The empty-state paragraph can stay `<p class="empty">` inside children for now. **3. Migrate all settings sections to `SectionCard`:** - `web/src/routes/repo-settings/sections/`: Imports, Secrets, Schedules, Agents, Autoland, Branches, Danger, General, Integrations, Runner. - `web/src/routes/settings/sections/` where the pattern appears (e.g. Notifications). - Out of scope: non-settings routes (Tokens, Credentials, History, Repos, RepoIssues, RepoCRs, …) — they are covered by the CSS hardening now and migrate in the follow-up umbrella issue. ## Acceptance criteria - No negative margins remain in the head/hint area; head-row height no longer depends on heading font metrics. - All listed settings sections render title/action/hint through `SectionCard`; component test asserts the structure (title, action slot, hint, children render in order). - Existing section tests still pass; `npm run build`, `npm run test`, `npm run lint` clean. - Reporter visually confirms on repo settings → Imports (empty state) that the button no longer touches/covers the hint, desktop and mobile widths.
Author
Owner

Follow-up umbrella for the rest of the frontend (per-family component extraction, inventory-first): #276.

Follow-up umbrella for the rest of the frontend (per-family component extraction, inventory-first): #276.
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
Cloonar/coding-lab#275
No description provided.