From 7b68f6358d4168f62ccc4e0f4cf55886f0f77536 Mon Sep 17 00:00:00 2001 From: Egor Kraev Date: Wed, 21 Oct 2026 13:58:41 +0200 Subject: [PATCH] Colour the Hubs button like the selected tab; frame the deletion dialog in red The Hubs button gets the selected tab's blue background, border and white text, hovered or not. The open Hubs menu goes back to its thin grey border. The worktree-deletion confirmation is framed 3px in a new --danger red (the failed-mark red), not blue. --- src/web/main/styles.css | 11 ++++++----- test/e2e/contract.ts | 6 +++--- test/e2e/fixture.ts | 4 ---- test/e2e/hubs.spec.ts | 25 ++++++++++++++++++++----- test/e2e/sidebar.spec.ts | 10 +++++----- 5 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/web/main/styles.css b/src/web/main/styles.css index c555e6e..4cab22b 100644 --- a/src/web/main/styles.css +++ b/src/web/main/styles.css @@ -10,6 +10,7 @@ --accent: #4c8dff; --accent-soft: #233b61; --star: #f5c451; + --danger: #f14c4c; font-family: Inter, ui-sans-serif, @@ -85,8 +86,10 @@ input:focus-visible, .repo-tab.active, .term-tab.active, +.hubs-menu, .repo-tab.active:hover, -.term-tab.active:hover { +.term-tab.active:hover, +.hubs-menu:hover:not(:disabled) { background: var(--accent); border-color: var(--accent); color: #fff; @@ -512,10 +515,8 @@ input:focus-visible, box-shadow: 0 12px 36px rgb(0 0 0 / 55%); } -/* Framed thick in the selected tab's blue. */ -.hubs-list, .confirm-dialog.delete-worktree { - border: 3px solid var(--accent); + border: 3px solid var(--danger); } /* Undo the user agent's centring and colours of an open dialog. */ @@ -670,7 +671,7 @@ dialog.add-repo-dialog { } .mark-failed { - background: #f14c4c; + background: var(--danger); } .mark-exited { diff --git a/test/e2e/contract.ts b/test/e2e/contract.ts index ece1e19..a5e4163 100644 --- a/test/e2e/contract.ts +++ b/test/e2e/contract.ts @@ -34,7 +34,7 @@ export const TID = { hostLogin: 'host-login', /** Button inside a host banner: "Restart daemon" (outdated local), "Reinstall & restart" (outdated remote) or "Install" (down remote); opens the confirmation dialog, then sends `restartDaemon` or `reinstallDaemon`. */ hostAction: 'host-action', - /** In-page confirmation of a host action or a repo removal; nothing is sent before it is confirmed; text says every running terminal on the host will be killed when it lists none; for a worktree deletion only, framed 3px in the selected repo tab's background colour. */ + /** In-page confirmation of a host action or a repo removal; nothing is sent before it is confirmed; text says every running terminal on the host will be killed when it lists none; for a worktree deletion only, framed 3px in red. */ confirmDialog: 'confirm-dialog', /** One running terminal the page last saw on the daemon instance the host reports; text holds its worktree label and preset. */ confirmTarget: 'confirm-target', @@ -130,9 +130,9 @@ export const TID = { notice: 'notice', /** Terminal container, `data-host` and `data-term`; holds the `.xterm` element; hidden ones are not visible. */ terminal: 'terminal', - /** Header button "Hubs", present only while there is a remote host; opens the list of `hubsEntry`. */ + /** Header button "Hubs", present only while there is a remote host; opens the list of `hubsEntry`; coloured like the selected repo tab, hovered or not. */ hubsMenu: 'hubs-menu', - /** The open Hubs menu holding the `hubsEntry` items; framed 3px in the selected repo tab's background colour. */ + /** The open Hubs menu holding the `hubsEntry` items, with a thin border. */ hubsList: 'hubs-list', /** One per hub in the open Hubs menu, in order: text = "This machine" or the host name; `aria-current="true"` on the page's own hub. */ hubsEntry: 'hubs-entry', diff --git a/test/e2e/fixture.ts b/test/e2e/fixture.ts index 4934a63..0c3033b 100644 --- a/test/e2e/fixture.ts +++ b/test/e2e/fixture.ts @@ -108,10 +108,6 @@ export const borderOf = (locator: Locator): Promise => }); }); -/** The computed background colour of the selected repo tab. */ -export const selectedTabColour = (page: Page): Promise => - page.locator(`${byTestId(TID.repoTab)}[aria-selected="true"]`).evaluate((el) => getComputedStyle(el).backgroundColor); - /** Confirms the open confirmation dialog. */ export const confirmAction = async (page: Page): Promise => { const dialog = page.locator(byTestId(TID.confirmDialog)); diff --git a/test/e2e/hubs.spec.ts b/test/e2e/hubs.spec.ts index 870635d..12ef1b3 100644 --- a/test/e2e/hubs.spec.ts +++ b/test/e2e/hubs.spec.ts @@ -1,4 +1,4 @@ -import type { Page } from '@playwright/test'; +import type { Locator, Page } from '@playwright/test'; import { join } from 'node:path'; import { makeRepo } from '../support/daemon-host.js'; import { freePort, type HubHost } from '../support/hub-host.js'; @@ -12,7 +12,6 @@ import { nodeRemote, openUi, runUi, - selectedTabColour, test, writeHubConfig, type HostConfig, @@ -82,12 +81,28 @@ test.describe('Hubs menu', () => { await expect(page.locator(byTestId(TID.hubsMenu))).toBeFocused(); }); - test("is framed 3px in the selected tab's colour", async ({ hub, page, ssh }) => { + test('the open menu has a thin border', async ({ hub, page, ssh }) => { ssh.failHost('box', RESOLVE_ERROR); await home(hub, page, [{ name: 'box', ssh: 'box' }]); await page.locator(byTestId(TID.hubsMenu)).click(); - const blue = await selectedTabColour(page); - expect(await borderOf(page.locator(byTestId(TID.hubsList)))).toEqual(Array(4).fill(`3px ${blue}`)); + expect((await borderOf(page.locator(byTestId(TID.hubsList)))).every((side) => side.startsWith('1px '))).toBe(true); + }); + + test('the Hubs button is coloured like the selected tab, hovered or not', async ({ hub, page, ssh }) => { + ssh.failHost('box', RESOLVE_ERROR); + await home(hub, page, [{ name: 'box', ssh: 'box' }]); + const button = page.locator(byTestId(TID.hubsMenu)); + const selected = page.locator(`${byTestId(TID.repoTab)}[aria-selected="true"]`); + const colours = (locator: Locator): Promise => + locator.evaluate((el) => { + const style = getComputedStyle(el); + return [style.backgroundColor, style.borderTopColor, style.color]; + }); + await page.mouse.move(0, 0); + const tab = await colours(selected); + await expect.poll(() => colours(button)).toEqual(tab); + await button.hover(); + await expect.poll(() => colours(button)).toEqual(tab); }); test('is absent without remote hosts', async ({ hub, page }) => { diff --git a/test/e2e/sidebar.spec.ts b/test/e2e/sidebar.spec.ts index e226cd5..4c47953 100644 --- a/test/e2e/sidebar.spec.ts +++ b/test/e2e/sidebar.spec.ts @@ -17,7 +17,6 @@ import { openPage, openUi, ready, - selectedTabColour, selectRepo, selectWorktree, test, @@ -26,6 +25,8 @@ import { watcher, } from './fixture.js'; +/** `--danger`, as computed. */ +const DANGER = 'rgb(241, 76, 76)'; const label = (path: string): string => `${worktreeEntry(path)} ${byTestId(TID.worktreeLabel)}`; const checkbox = (path: string): string => `${worktreeEntry(path)} input[type="checkbox"]`; const boxOf = async (locator: Locator): Promise<{ x: number; y: number; width: number; height: number }> => { @@ -471,7 +472,7 @@ test.describe('worktree status and deletion', () => { expect(git(repo, 'rev-parse', 'feat')).toMatch(/^[0-9a-f]{40}$/); }); - test("the deletion dialog is framed 3px in the selected tab's colour; a repo removal's is not", async ({ hub, page }) => { + test("the deletion dialog is framed 3px in red; a repo removal's is not", async ({ hub, page }) => { const { repo, worktrees } = makeRepoWith(hub, 'app', ['feat']); const feat = worktrees[0] ?? ''; withOrigin(hub, repo); @@ -482,13 +483,12 @@ test.describe('worktree status and deletion', () => { await page.locator(worktreeEntry(feat)).click({ button: 'right' }); await page.locator(byTestId(TID.deleteWorktree)).click(); await expect(dialog).toBeVisible(); - const blue = await selectedTabColour(page); - expect(await borderOf(dialog)).toEqual(Array(4).fill(`3px ${blue}`)); + expect(await borderOf(dialog)).toEqual(Array(4).fill(`3px ${DANGER}`)); await dialog.locator(byTestId(TID.confirmCancel)).click(); await page.locator(repoTab(repo)).locator(byTestId(TID.removeRepo)).click(); await expect(dialog).toBeVisible(); - expect((await borderOf(dialog)).every((side) => side.startsWith('1px ') && !side.endsWith(blue))).toBe(true); + expect((await borderOf(dialog)).every((side) => side.startsWith('1px ') && !side.endsWith(DANGER))).toBe(true); }); test('deleting a detached worktree with commits says they become unreachable and names its head', async ({ hub, page }) => {