style(AUT-1): visible primary-button hover state + visual diff (#303)

echo "(body edit needs verification — see above)"

## Screenshots
<!-- agent-screenshots:start -->

![desktop-before.png](https://github.com/user-attachments/assets/e2381ec6-a198-4a65-98e3-6658a05a6a00)

![desktop-after.png](https://github.com/user-attachments/assets/dc339d6d-d47c-4754-b853-5d6835f54a94)

![mobile-before.png](https://github.com/user-attachments/assets/7ca6a9b7-e77b-49bf-8b95-d478ca23ad8a)

![mobile-after.png](https://github.com/user-attachments/assets/c122b8d0-81af-4417-8e80-a5aec693b601)
<!-- agent-screenshots:end -->

---------

Co-authored-by: Jeppe <jeppe@copenhagentruckwash.io>
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Jeppe B
2026-08-15 20:52:13 +02:00
committed by GitHub
co-authored by Jeppe openhands
parent 0369664a96
commit 1e7298245d
7 changed files with 232 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env node
// AUT-1 smoke run — capture the before/after visual diff for the
// `primary-button hover state` change in `src/assets/main.css`.
//
// Renders tests/visual-previews/AUT-1/preview.html in a real Chromium
// against the project's bundled Bulma stylesheet, then takes two
// screenshots:
//
// desktop-before.png — Bulma defaults, button idle (no hover effect).
// desktop-after.png — With the AUT-1 CSS rules applied, button
// hovered (lift + brightness shift visible).
// mobile-before.png — Same as desktop-before, captured at the
// Pixel 5 viewport.
// mobile-after.png — Same as desktop-after, captured at the
// Pixel 5 viewport.
//
// Output is written to tests/visual-previews/AUT-1/. The script is
// idempotent: existing files are overwritten, not appended to.
//
// Invoke with: `node scripts/aut-1-capture-hover-preview.mjs`
// (Chromium must already be installed via `npm run test:e2e:install`.)
import { chromium } from "playwright";
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, "..");
const PREVIEW_HTML = path.join(
REPO_ROOT,
"tests/visual-previews/AUT-1/preview.html",
);
const OUTPUT_DIR = path.join(REPO_ROOT, "tests/visual-previews/AUT-1");
// The AUT-1 hover rules — kept in lockstep with the diff in
// src/assets/main.css. We inject these via a <style> tag on the
// "after" pass and leave them off for the "before" pass so the
// screenshots show the same Bulma theme but with vs without the new
// hover effect.
const AUT1_HOVER_CSS = `
.button.is-primary {
transition:
filter 0.15s ease,
transform 0.15s ease;
}
.button.is-primary:hover,
.button.is-primary.is-hovered {
filter: brightness(1.08);
transform: translateY(-1px);
}
.button.is-primary:active,
.button.is-primary.is-active {
filter: brightness(0.95);
transform: translateY(0);
}
.button.is-primary:focus-visible {
filter: brightness(1.04);
transform: translateY(-1px);
}
`;
async function captureVariant({ browser, viewport, label, withHoverCss, hover }) {
const context = await browser.newContext({
viewport,
deviceScaleFactor: 2,
});
const page = await context.newPage();
await page.goto(pathToFileUrl(PREVIEW_HTML));
if (withHoverCss) {
await page.addStyleTag({ content: AUT1_HOVER_CSS });
}
const target = page.locator(
hover
? '[data-testid="primary-button-hover"]'
: '[data-testid="primary-button-idle"]',
);
const stage = page.locator(
hover ? "#stage-hover" : "#stage-idle",
);
if (hover) {
await target.hover();
// Wait for the 0.15s transition to settle.
await page.waitForTimeout(220);
}
const filename = `${label}-${withHoverCss ? "after" : "before"}.png`;
const destination = path.join(OUTPUT_DIR, filename);
await stage.screenshot({ path: destination, type: "png" });
await context.close();
return destination;
}
function pathToFileUrl(filePath) {
// Playwright's `file://` URLs need absolute paths. On POSIX this is
// straightforward; on Windows this helper keeps the script cross-platform
// should it ever run there.
const absolute = path.resolve(filePath);
return absolute.startsWith("/") ? `file://${absolute}` : `file:///${absolute}`;
}
async function main() {
await fs.mkdir(OUTPUT_DIR, { recursive: true });
const browser = await chromium.launch({ headless: true });
try {
const viewports = [
{ label: "desktop", viewport: { width: 1280, height: 720 } },
{
label: "mobile",
viewport: { width: 393, height: 851 },
},
];
for (const { label, viewport } of viewports) {
for (const withHoverCss of [false, true]) {
for (const hover of [false, true]) {
// We only want idle on the "before" pass and hover on the
// "after" pass — skip the two redundant combinations.
const isIdleShot = !hover;
const isBeforeShot = !withHoverCss;
if (isIdleShot !== isBeforeShot) continue;
const file = await captureVariant({
browser,
viewport,
label,
withHoverCss,
hover,
});
// eslint-disable-next-line no-console
console.log("wrote", path.relative(REPO_ROOT, file));
}
}
}
} finally {
await browser.close();
}
}
main().catch((err) => {
// eslint-disable-next-line no-console
console.error("aut-1 capture failed:", err);
process.exitCode = 1;
});
+27
View File
@@ -144,3 +144,30 @@ body:not(.pleno-large-table-headers) .table thead th {
margin-top: 30px !important;
}
}
/* AUT-1: enhanced primary-button hover state.
Bulma's default only nudges background lightness; this adds a small
lift + brightness shift so the hover is unambiguous. Transitions keep
it smooth so it doesn't feel jarring on click-heavy screens. */
.button.is-primary {
transition:
filter 0.15s ease,
transform 0.15s ease;
}
.button.is-primary:hover,
.button.is-primary.is-hovered {
filter: brightness(1.08);
transform: translateY(-1px);
}
.button.is-primary:active,
.button.is-primary.is-active {
filter: brightness(0.95);
transform: translateY(0);
}
.button.is-primary:focus-visible {
filter: brightness(1.04);
transform: translateY(-1px);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+63
View File
@@ -0,0 +1,63 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>AUT-1 primary-button hover preview</title>
<!--
Bulma 1.x stylesheet from the project's node_modules so the preview
matches the real app's default theme. The :root override is the same
--bulma-primary-h override that lives in src/assets/main.css so the
primary colour renders as the brand teal-blue (#0787BB ≈ HSL 197deg).
-->
<link rel="stylesheet" href="../../../node_modules/bulma/css/bulma.min.css" />
<style>
:root {
--bulma-primary-h: 197deg;
--bulma-primary-s: 92%;
--bulma-primary-l: 38%;
}
body {
background: #f5f5f5;
margin: 0;
padding: 48px 64px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.stage {
display: flex;
align-items: center;
gap: 48px;
background: white;
padding: 40px 56px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(10, 10, 10, 0.12);
width: fit-content;
}
.label {
font-size: 12px;
color: #7a7a7a;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 12px;
}
.button.is-primary {
min-width: 168px;
font-weight: 600;
}
</style>
</head>
<body>
<div id="stage-idle" class="stage" data-state="idle">
<div class="label">Primary button — default</div>
<button class="button is-primary" data-testid="primary-button-idle">
Confirm booking
</button>
</div>
<div style="height: 32px"></div>
<div id="stage-hover" class="stage" data-state="hover">
<div class="label">Primary button — hover</div>
<button class="button is-primary" data-testid="primary-button-hover">
Confirm booking
</button>
</div>
</body>
</html>