diff --git a/gui/src/styles.css b/gui/src/styles.css index dadf818cad..e00c1acc2b 100644 --- a/gui/src/styles.css +++ b/gui/src/styles.css @@ -328,9 +328,11 @@ input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); } /* GitHub row: the label keeps the full-width link affordance, the two circular satellites (star, update) sit at the trailing edge without shrinking the label's - hover target. The update orb only exists when an update is available, so the row + hover target. That edge is the sidebar's 10px content inset, not the rail wall, so + the orbs stack under the lang chevron and the proxy orbs instead of hanging 10px + further out than both. The update orb only exists when an update is available, so the row is one or two circles wide — never a placeholder. */ -.sidebar-github-row { display: flex; align-items: center; gap: 4px; min-width: 0; } +.sidebar-github-row { display: flex; align-items: center; gap: 4px; min-width: 0; padding-right: 10px; } .sidebar-github-link { flex: 1 1 auto; min-width: 0; } .sidebar-github-actions { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; } .sidebar-orb { @@ -388,8 +390,12 @@ input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); } row with a quiet label. The stop control used to be a full-width button; pairing it with restart as icons keeps the foot from growing a fifth stacked row and puts the destructive action next to the recovery action it is most often confused with. */ -.sidebar-action-row { display: flex; align-items: center; gap: 4px; min-width: 0; padding: 8px 10px; } -.sidebar-action-label { flex: 1 1 auto; min-width: 0; color: var(--muted); font-size: var(--text-control); } +.sidebar-action-row { display: flex; align-items: center; gap: 4px; min-width: 0; padding-right: 10px; } +/* Left padding matches .sidebar-link, plus the 16px icon + 9px gap gutter the + iconless label has to clear to sit in the same text column as its neighbours. + Owning the block padding here (rather than on the row) keeps the row the same + height as the other three, which the 28px orbs would otherwise inflate. */ +.sidebar-action-label { flex: 1 1 auto; min-width: 0; padding: 8px 10px 8px calc(10px + 16px + 9px); color: var(--muted); font-size: var(--text-control); } .sidebar-action-orbs { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; } .sidebar-orb--danger { color: var(--red); } .sidebar-orb--danger:hover:not(:disabled) { background: var(--red-soft); color: var(--red); border-color: var(--red); } diff --git a/gui/tests/sidebar-rows.test.ts b/gui/tests/sidebar-rows.test.ts index b711c5421f..c53243c42c 100644 --- a/gui/tests/sidebar-rows.test.ts +++ b/gui/tests/sidebar-rows.test.ts @@ -50,6 +50,49 @@ test("the orphaned sidebar switch styles are gone", async () => { expect(css).not.toContain(".nav-entry-claude .switch"); }); +test("the foot's four rows share one text column and one trailing inset", async () => { + /* + * The foot stacks lang, theme, proxy and GitHub two pixels apart, so any row that + * measures itself differently is visible as a step in the stack. All four shipped + * out of line at once: the proxy label sat 25px left of its neighbours because it + * has no icon to clear, its row was 8.5px taller because it padded around 28px orbs + * the others do not have, and the GitHub orbs hung 10px further out because that row + * was the only one with no trailing inset. + */ + const css = await Bun.file(new URL("../src/styles.css", import.meta.url)).text(); + const rule = (selector: string) => { + const at = css.indexOf(`${selector} {`); + expect(at).toBeGreaterThan(-1); + return css.slice(at, css.indexOf("}", at)); + }; + + // The column every label sits in, owned by the rows that carry an icon. + for (const selector of [".lang-toggle", ".theme-toggle", ".sidebar-link"]) { + expect(rule(selector)).toContain("padding: 8px 10px"); + expect(rule(selector)).toContain("gap: 9px"); + } + + // The proxy label has no icon, so it clears that gutter itself. Holding the block + // padding on the label rather than the row is what keeps the row's height tied to + // its text, like its neighbours, instead of to the taller orbs beside it. + expect(rule(".sidebar-action-label")).toContain("padding: 8px 10px 8px calc(10px + 16px + 9px)"); + + /* + * Reject block padding on the row in every spelling, not just the shorthand it + * shipped with: `padding: 8px 0`, or a lone `padding-top`, would hand the 28px orbs + * back control of the row height and still slip past a check for the exact original + * string. `padding-right` survives both patterns — "padding" is followed by "-", + * never by a colon. + */ + const proxyRow = rule(".sidebar-action-row"); + expect(proxyRow).not.toMatch(/padding\s*:/); + expect(proxyRow).not.toMatch(/padding-(top|bottom|block)/); + + // Trailing controls stop on the same inset as the lang chevron above them. + expect(proxyRow).toContain("padding-right: 10px"); + expect(rule(".sidebar-github-row")).toContain("padding-right: 10px"); +}); + test("Claude Code is still reachable, just not as a duplicate row", async () => { // Removing the shortcut must not remove the destination. const routing = await Bun.file(new URL("../src/app-routing.ts", import.meta.url)).text();