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 }) => {