diff --git a/.gitignore b/.gitignore index a37e8ff..664fa07 100755 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ plans/ui-shots/ # Transport regression screenshots plans/transport-shots/ + +# Search button visual review screenshots +plans/search-button-shots/ diff --git a/.impeccable/config.json b/.impeccable/config.json index 570fb0e..8613286 100644 --- a/.impeccable/config.json +++ b/.impeccable/config.json @@ -21,6 +21,12 @@ ], "createdAt": "2026-10-05T18:33:43.625Z", "reason": "Framework7 swipeout reveal retains its existing width transition for the action area" + }, + { + "rule": "design-system-font", + "value": "plus jakarta sans", + "createdAt": "2026-10-11T09:31:13.372Z", + "reason": "Existing Bento Hub layout intentionally declares Plus Jakarta Sans in theme-bento-hub.css; the Search redesign preserves that established layout typography." } ] } diff --git a/frontend/styles.css b/frontend/styles.css index 1fc87a0..22d0f82 100755 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -23,6 +23,8 @@ --accent-bright: #ff6a52; --accent-deep: #d4321d; --accent-glow: rgba(255, 75, 50, 0.45); + --search-action-surface: #16161b; + --search-action-foreground: #f5f3f0; --display: "Bricolage Grotesque", Georgia, serif; --ui: "Hanken Grotesk", system-ui, sans-serif; @@ -386,23 +388,6 @@ body::after { background: var(--bg-1); box-shadow: 0 0 0 4px rgba(255, 75, 50, 0.12); } -.search-btn { - background: linear-gradient(145deg, var(--accent-bright), var(--accent-deep)); - color: #fff; - border: none; - padding: 0 24px; - border-radius: 11px; - font-family: var(--ui); - font-weight: 700; - font-size: 14px; - letter-spacing: 0.01em; - cursor: pointer; - box-shadow: 0 8px 22px -8px var(--accent-glow); - transition: transform 0.16s var(--ease), box-shadow 0.16s; -} -.search-btn:hover { transform: translateY(-1px); box-shadow: 0 12px 28px -8px var(--accent-glow); } -.search-btn:active { transform: translateY(0); } - .body { flex: 1; display: flex; overflow: hidden; } /* ===================== Player pane ===================== */ @@ -1796,6 +1781,8 @@ input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.25); } --text: #1b1a18; --text-2: #4a4843; --text-dim: #7c776e; + --search-action-surface: #efece6; + --search-action-foreground: #1b1a18; --shadow: 0 18px 50px -18px rgba(0, 0, 0, 0.25); } :root[data-theme="light"] body::after { opacity: 0.02; } @@ -1821,6 +1808,8 @@ input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.25); } --accent-bright: #ff8a6c; --accent-deep: #ff5a3c; --accent-glow: rgba(255, 90, 60, 0.85); + --search-action-surface: #0a0a0a; + --search-action-foreground: #ffffff; } :root[data-theme="contrast"] body::after { display: none; } :root[data-theme="contrast"] body::before { display: none; } @@ -3106,6 +3095,8 @@ html[data-layout="liturgical"] { --accent-bright: #a83028; --accent-deep: #6b1c14; --accent-glow: rgba(139, 38, 29, 0.35); + --search-action-surface: var(--bg-2); + --search-action-foreground: var(--text); --display: Georgia, 'Times New Roman', serif; --ui: Georgia, 'Times New Roman', serif; --shadow: 0 18px 50px -12px rgba(0, 0, 0, 0.2); @@ -4357,17 +4348,45 @@ html[data-layout="f7-swipe"] .media-shape-f7-swipe, html[data-layout="glass-stage"] .media-shape-glass-stage, html[data-layout="bento-hub"] .media-shape-bento-hub { display:inline; } -/* ===== Search button — one icon + label, restyled per layout and colour theme ===== */ -.search-btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; } +/* Search action: token-backed surfaces, one accessible focus treatment. */ +.search-btn { + display: inline-flex; flex: 0 0 auto; align-items: center; justify-content: center; gap: 7px; + min-width: 76px; min-height: 44px; padding: 0 14px; + border: 1px solid color-mix(in srgb, var(--search-action-foreground) 45%, var(--accent-deep)); border-radius: 9px; + background: var(--search-action-surface); color: var(--search-action-foreground); + font: 700 14px/1 var(--ui); letter-spacing: .01em; white-space: nowrap; + box-shadow: none; cursor: pointer; + transition: background-color .14s ease, border-color .14s ease, transform .12s ease; +} .search-btn::before { content: ""; width: 17px; height: 17px; flex: none; background: currentColor; - -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='m16 16 5 5'/%3E%3C/svg%3E") center / contain no-repeat; - mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='m16 16 5 5'/%3E%3C/svg%3E") center / contain no-repeat; + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='m16 16 5 5'/%3E%3C/svg%3E") center / contain no-repeat; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='m16 16 5 5'/%3E%3C/svg%3E") center / contain no-repeat; } -.search-btn { box-shadow: 0 8px 22px -8px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, .28); } -.search-btn:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 3px; } -:where(:root[data-theme="light"]) .search-btn { background: var(--accent-deep); box-shadow: 0 2px 8px -2px rgba(0, 0, 0, .35), inset 0 1px 0 rgba(255, 255, 255, .25); } -:where(:root[data-theme="light"]) .search-btn:hover { background: var(--accent); box-shadow: 0 4px 12px -3px rgba(0, 0, 0, .4); } -:where(:root[data-theme="contrast"]) .search-btn { background: #fff; color: #000; border: 2px solid #fff; box-shadow: none; } -:where(:root[data-theme="contrast"]) .search-btn:hover { background: var(--accent-bright); border-color: var(--accent-bright); box-shadow: none; } -:where(:root[data-theme="contrast"]) .search-btn:focus-visible { outline: 3px solid #fff; outline-offset: 3px; } +.search-btn:hover { background: var(--bg-3); border-color: var(--accent); transform: none; } +.search-btn:active { transform: scale(.98); } +.search-btn:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 2px; } +html[data-layout] .search-btn:disabled { opacity: 1; background: var(--bg-1); border: 1px dashed var(--search-action-foreground); color: var(--search-action-foreground); cursor: not-allowed; } +.search-btn:disabled:hover { transform: none; } +html[data-layout="classic"] .search-btn { border-radius: 7px; background: transparent; border-width: 1.5px; } +html[data-layout="f7-swipe"] .search-btn { min-width: 64px; padding-inline: 10px; border-color: transparent; border-bottom-color: color-mix(in srgb, var(--search-action-foreground) 45%, var(--accent-deep)); border-radius: 5px 5px 2px 2px; background: transparent; font-size: 16px; font-weight: 600; } +html[data-layout="f7-swipe"] .search-btn::before { display: none; } +html[data-layout="glass-stage"] .search-btn { border-radius: 999px; background: color-mix(in srgb, var(--search-action-surface) 88%, transparent); -webkit-backdrop-filter: blur(14px) saturate(1.35); backdrop-filter: blur(14px) saturate(1.35); box-shadow: inset 0 1px 0 color-mix(in srgb, var(--search-action-foreground) 12%, transparent); } +html[data-layout="bento-hub"] .search-btn { min-width: 44px; border-radius: 999px; border-color: color-mix(in srgb, var(--search-action-foreground) 45%, var(--accent-deep)); background: var(--search-action-surface); color: var(--search-action-foreground); font: 700 13px/1 var(--ui); } +html[data-layout="bento-hub"] .search-btn:hover { background: var(--bg-3); border-color: var(--accent); transform: none; } +html[data-layout="bento-hub"] .search-btn::before { display: none; } +html[data-layout="sanctuary-stage"] .search-btn { border-radius: 4px; border-width: 1px 1px 2px; letter-spacing: .055em; text-transform: uppercase; } +html[data-layout="liturgical"] .search-btn { border-color: color-mix(in srgb, var(--search-action-foreground) 70%, var(--accent-deep)); border-radius: 3px; border-width: 1px 1px 2px; font-family: var(--display); font-weight: 700; letter-spacing: .02em; } +html[data-layout="liturgical"] .search-btn:hover { background: var(--bg-2); border-color: var(--accent); color: var(--search-action-foreground); } +html[data-layout="living-hymnal"] .search-btn { border-radius: 10px; border-width: 1px 1px 1px 3px; } +html[data-layout="broadcaster"] .search-btn { border-radius: 3px; border-width: 0 0 2px; letter-spacing: .06em; text-transform: uppercase; } +html[data-layout="contemplative"] .search-btn { border-color: var(--line); border-bottom-color: color-mix(in srgb, var(--search-action-foreground) 45%, var(--accent-deep)); border-radius: 3px; background: transparent; font-family: var(--display); font-weight: 600; } +html[data-layout="contemplative"] .search-btn:hover { background: var(--bg-3); border-color: var(--accent); color: var(--search-action-foreground); } +html[data-layout] .search-btn:active { transform: scale(.98); } +@media (max-width: 430px) { + .topbar { min-width: 0; gap: 6px; } + .search-form { min-width: 0; gap: 5px; } + .search-btn { min-width: 64px; padding-inline: 9px; gap: 5px; } + .search-btn::before { width: 15px; height: 15px; } +} +@media (prefers-reduced-motion: reduce) { .search-btn { transition: none; } } diff --git a/frontend/theme-bento-hub.css b/frontend/theme-bento-hub.css index eae713e..910eefd 100644 --- a/frontend/theme-bento-hub.css +++ b/frontend/theme-bento-hub.css @@ -51,7 +51,7 @@ html[data-layout="bento-hub"] .sidebar-footer { border-top:1px solid var(--line) html[data-layout="bento-hub"] .topbar { background:var(--bg-1); border-bottom:1px solid var(--line); padding:12px 20px; gap:12px; backdrop-filter:none; } html[data-layout="bento-hub"] .search-form { max-width:640px; gap:8px; } html[data-layout="bento-hub"] #searchInput { border:1px solid var(--line); border-radius:999px; background:var(--bg-2); padding:12px 18px; font-size:14px; min-height:44px; box-shadow:none; } -html[data-layout="bento-hub"] .search-btn { border:0; border-radius:999px; min-height:44px; background:var(--accent); color:var(--on-accent); font:700 13px var(--ui); box-shadow:none; } +html[data-layout="bento-hub"] .search-btn { min-width:44px; border:1px solid color-mix(in srgb, var(--search-action-foreground) 45%, var(--accent-deep)); border-radius:999px; min-height:44px; background:var(--search-action-surface); color:var(--search-action-foreground); font:700 13px var(--ui); box-shadow:none; } html[data-layout="bento-hub"] :is(.sidebar-toggle,.icon-btn,.update-icon-btn) { min-width:44px; width:44px; min-height:44px; border-radius:10px; background:var(--bg-2); color:var(--text-2); } html[data-layout="bento-hub"] .bottom-nav { height:calc(64px + env(safe-area-inset-bottom)); background:var(--bg-glass); border-top:1px solid var(--line); backdrop-filter:blur(16px); -webkit-backdrop-filter:blur(16px); padding:0 4px env(safe-area-inset-bottom); } html[data-layout="bento-hub"] .bottom-nav-btn { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; padding:8px 0; min-height:64px; color:var(--text-2); } diff --git a/frontend/theme-classic.css b/frontend/theme-classic.css index 198ccc8..bd572d6 100644 --- a/frontend/theme-classic.css +++ b/frontend/theme-classic.css @@ -65,8 +65,6 @@ html[data-layout="classic"] .topbar { } html[data-layout="classic"] .search-form { width: 100%; min-width: 0; max-width: 860px; gap: 8px; } html[data-layout="classic"] #searchInput { min-width: 0; font-size: 16px; padding: 11px 14px; min-height: 46px; border-radius: 8px; background: var(--bg-1); box-shadow: none; } -html[data-layout="classic"] .search-btn { min-height: 46px; padding: 0 22px; border-radius: 8px; background: var(--bg-3); color: var(--text); box-shadow: none; transform: none; } -html[data-layout="classic"] .search-btn:hover { background: var(--accent-deep); color: #fff; } html[data-layout="classic"] .sidebar-toggle { min-width: 44px; min-height: 44px; border-radius: 8px; box-shadow: none; } html[data-layout="classic"] .player-pane { container-type: inline-size; @@ -236,10 +234,6 @@ html[data-layout="classic"] .card:has(.card-note-btn) .card-menu { grid-column: html[data-layout="classic"] .card:has(.card-note-btn) .card-del { grid-column: 4; grid-row: 2; } html[data-layout="classic"] .card:has(.card-note-btn) { column-gap: 8px; } -/* Search button: outlined rectangle that fills with the accent on hover. */ -html[data-layout="classic"] .search-btn { background: transparent; color: var(--accent); border: 1.5px solid var(--accent); box-shadow: none; } -html[data-layout="classic"] .search-btn:hover { background: var(--accent); color: #fff; transform: none; box-shadow: none; } -html[data-layout="classic"][data-theme="contrast"] .search-btn { background: #fff; color: #000; border-color: #fff; } -html[data-layout="classic"][data-theme="contrast"] .search-btn:hover { background: var(--accent-bright); border-color: var(--accent-bright); color: #000; } -html[data-layout="classic"][data-theme="light"] .search-btn { color: var(--accent-deep); border-color: var(--accent-deep); } -html[data-layout="classic"][data-theme="light"] .search-btn:hover { background: var(--accent-deep); color: #fff; } +/* Search action stays outlined, with its label on the theme surface. */ +html[data-layout="classic"] .search-btn { background: transparent; color: var(--search-action-foreground); border: 1.5px solid color-mix(in srgb, var(--search-action-foreground) 45%, var(--accent-deep)); box-shadow: none; } +html[data-layout="classic"] .search-btn:hover { background: var(--search-action-surface); border-color: var(--accent); color: var(--search-action-foreground); transform: none; box-shadow: none; } diff --git a/frontend/theme-f7.css b/frontend/theme-f7.css index e006b39..d37486f 100644 --- a/frontend/theme-f7.css +++ b/frontend/theme-f7.css @@ -206,7 +206,8 @@ html[data-layout="f7-swipe"] #searchInput { box-shadow: none; font: 400 17px/1.25 var(--ui, system-ui, sans-serif); } html[data-layout="f7-swipe"] .search-form::before { content: ''; position: absolute; left: 13px; top: 12px; z-index: 1; width: 18px; height: 18px; background: var(--text-dim); -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m16 16 5 5' stroke='black' stroke-width='2'/%3E%3C/svg%3E") center/contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m16 16 5 5' stroke='black' stroke-width='2'/%3E%3C/svg%3E") center/contain no-repeat; pointer-events: none; } -html[data-layout="f7-swipe"] .search-btn { min-height: 42px; padding: 0 8px; border: 0; background: transparent; color: var(--accent); font-size: 16px; font-weight: 500; box-shadow: none; } +html[data-layout="f7-swipe"] .search-btn { min-width: 64px; min-height: 44px; padding: 0 10px; border: 0; border-bottom: 2px solid color-mix(in srgb, var(--search-action-foreground) 45%, var(--accent-deep)); border-radius: 5px 5px 2px 2px; background: transparent; color: var(--search-action-foreground); font-size: 16px; font-weight: 600; box-shadow: none; } +html[data-layout="f7-swipe"] .search-btn:hover { border-bottom-color: var(--accent); background: var(--bg-2); color: var(--search-action-foreground); } html[data-layout="f7-swipe"] #updateIconBtn { width: 40px; height: 40px; } html[data-layout="f7-swipe"] .search-recent { top: calc(100% + 8px); border: 1px solid var(--f7-hairline); border-radius: 14px; background: var(--bg-1); box-shadow: 0 12px 36px rgb(0 0 0 / .22); overflow: hidden; } html[data-layout="f7-swipe"] .search-recent > * { border-radius: 0; border: 0; border-bottom: 1px solid var(--f7-hairline); background: transparent; min-height: 48px; } diff --git a/frontend/theme-glass.css b/frontend/theme-glass.css index 033a834..4a22d05 100644 --- a/frontend/theme-glass.css +++ b/frontend/theme-glass.css @@ -376,14 +376,11 @@ html[data-layout="glass-stage"] #controls .ch-badge { cursor: pointer; font-fami html[data-layout="glass-stage"] #controls .ch-badge:hover { background: rgba(255, 255, 255, 0.12); border-color: var(--accent); } html[data-layout="glass-stage"] #controls .ch-badge:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } -/* Search button: frosted pill, accent-tinted, with a soft glow. */ +/* Search action uses a frosted token surface; label contrast remains stable. */ html[data-layout="glass-stage"] .search-btn { - background: color-mix(in srgb, var(--accent) 30%, transparent); color: var(--text); - border: 1px solid color-mix(in srgb, var(--accent-bright) 55%, transparent); border-radius: 999px; + background: color-mix(in srgb, var(--search-action-surface) 88%, transparent); color: var(--search-action-foreground); + border: 1px solid color-mix(in srgb, var(--search-action-foreground) 45%, var(--accent-deep)); border-radius: 999px; -webkit-backdrop-filter: blur(14px) saturate(1.4); backdrop-filter: blur(14px) saturate(1.4); - box-shadow: 0 6px 22px -8px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, .3); + box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 12%, transparent); } -html[data-layout="glass-stage"] .search-btn:hover { background: color-mix(in srgb, var(--accent) 48%, transparent); transform: translateY(-1px); } -html[data-layout="glass-stage"][data-theme="contrast"] .search-btn { background: #fff; color: #000; border-color: #fff; } -html[data-layout="glass-stage"][data-theme="light"] .search-btn { background: color-mix(in srgb, var(--accent-deep) 88%, transparent); color: #fff; border-color: color-mix(in srgb, var(--accent-deep) 70%, #fff); } -html[data-layout="glass-stage"][data-theme="light"] .search-btn:hover { background: var(--accent-deep); } +html[data-layout="glass-stage"] .search-btn:hover { background: color-mix(in srgb, var(--search-action-surface) 94%, var(--accent)); border-color: var(--accent); color: var(--search-action-foreground); transform: none; } diff --git a/perf/README.md b/perf/README.md index a703240..c241885 100644 --- a/perf/README.md +++ b/perf/README.md @@ -1,5 +1,10 @@ # Performance Baseline Harness (`perf/`) +`node perf/search-button.mjs` checks Search-button contrast, states, target size, +and row overflow across all nine layouts, three colour themes, and four widths. +It saves the 390px and 1280px review screenshots under ignored +`plans/search-button-shots/`. + Automated performance baseline and regression harness for the **ytplayer PWA**. The harness drives the real Bun backend server (`server/server.js`) serving static Brotli-compressed assets, combined with Playwright browser automation on Chromium and WebKit. diff --git a/perf/search-button.mjs b/perf/search-button.mjs new file mode 100644 index 0000000..3cccaab --- /dev/null +++ b/perf/search-button.mjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import { createServer } from 'node:http'; +import { readFile, mkdir } from 'node:fs/promises'; +import { extname, resolve } from 'node:path'; +import { chromium } from 'playwright'; + +const root = resolve('frontend'); +const shots = resolve('plans/search-button-shots'); +await mkdir(shots, { recursive: true }); +const mime = { '.html':'text/html', '.js':'text/javascript', '.css':'text/css', '.json':'application/json', '.woff2':'font/woff2', '.png':'image/png', '.svg':'image/svg+xml' }; +const server = createServer(async (req,res) => { + const path = resolve(root, '.' + (new URL(req.url,'http://local').pathname === '/' ? '/index.html' : new URL(req.url,'http://local').pathname)); + if (!path.startsWith(root + '/')) { res.writeHead(403).end(); return; } + try { const body=await readFile(path); res.writeHead(200, {'Content-Type':mime[extname(path)] || 'application/octet-stream'}).end(body); } + catch { res.writeHead(404).end(); } +}); +await new Promise(r=>server.listen(0,'127.0.0.1',r)); +const origin=`http://127.0.0.1:${server.address().port}`; +const layouts=['classic','f7-swipe','glass-stage','bento-hub','sanctuary-stage','broadcaster','contemplative','liturgical','living-hymnal']; +const themes=['dark','light','contrast']; +const only=process.argv.includes('--only')?process.argv[process.argv.indexOf('--only')+1]:null; +const failures=[]; +const ratio=(a,b)=>{const l=c=>{c/=255;return c<=.04045?c/12.92:((c+.055)/1.055)**2.4;};const lum=x=>.2126*l(x[0])+.7152*l(x[1])+.0722*l(x[2]);const [hi,lo]=[lum(a),lum(b)].sort((x,y)=>y-x);return(hi+.05)/(lo+.05);}; +const fail=(ok,msg)=>{if(!ok)failures.push(msg);}; +const browser=await chromium.launch({headless:true}); +try { + const context=await browser.newContext({viewport:{width:390,height:844},deviceScaleFactor:2,serviceWorkers:'block'}); + await context.route('**/*',route=>route.request().url().startsWith(origin)?route.continue():route.abort()); + const page=await context.newPage(); + page.on('pageerror',e=>failures.push('page error: '+e.message)); + for(const layout of layouts) for(const theme of themes) { + if(only&&only!==`${layout}/${theme}`)continue; + await context.addInitScript(({layout,theme})=>localStorage.setItem('_ytpdata',JSON.stringify({playlists:[],history:[],queue:[],settings:{layout,theme}})),{layout,theme}); + await page.setViewportSize({width:390,height:844}); + await page.goto(origin,{waitUntil:'domcontentloaded'}); + await page.waitForFunction(({layout,theme})=>document.documentElement.dataset.layout===layout&&document.documentElement.dataset.theme===theme, {layout,theme},{timeout:10000}); + await page.waitForFunction(()=>document.querySelector('.search-btn')&&getComputedStyle(document.querySelector('.search-btn')).display!=='none'); + await page.evaluate(()=>document.fonts.ready); + for(const width of [320,390,768,1280]) { + await page.setViewportSize({width,height:844}); + await page.waitForTimeout(60); + const before=await page.locator('.search-btn').boundingBox(); + const data=await page.evaluate(()=>{ + const btn=document.querySelector('.search-btn'),form=document.querySelector('#searchForm'),bar=document.querySelector('.topbar'); + const s=getComputedStyle(btn),formS=getComputedStyle(form),barS=getComputedStyle(bar),rootS=getComputedStyle(document.documentElement); + const canvas=document.createElement('canvas'),ctx=canvas.getContext('2d',{willReadFrequently:true}); + const parseRGBA=c=>{ctx.clearRect(0,0,1,1);ctx.fillStyle='#000';ctx.fillStyle=c;ctx.fillRect(0,0,1,1);const p=ctx.getImageData(0,0,1,1).data;return{rgb:[p[0],p[1],p[2]],a:p[3]/255};}; + const bg1=parseRGBA(rootS.getPropertyValue('--bg-1')).rgb,bg2=parseRGBA(rootS.getPropertyValue('--bg-2')).rgb; + const composite=(fg,bg,a)=>fg.map((v,i)=>v*a+bg[i]*(1-a)); + const layers=[];for(let e=bar;e&&e!==document.documentElement;e=e.parentElement)layers.push(parseRGBA(getComputedStyle(e).backgroundColor)); + let barSurface=parseRGBA(rootS.backgroundColor).rgb;for(const layer of layers.reverse())barSurface=composite(layer.rgb,barSurface,layer.a); + const buttonBg=parseRGBA(s.backgroundColor),surface=composite(buttonBg.rgb,barSurface,buttonBg.a); + const border=parseRGBA(s.borderBottomColor).rgb; + return{color:s.color,bg:s.backgroundColor,border:s.borderBottomColor,borderWidth:s.borderBottomWidth,minHeight:s.minHeight,height:btn.getBoundingClientRect().height,width:btn.getBoundingClientRect().width, + formScroll:form.scrollWidth,formClient:form.clientWidth,barScroll:bar.scrollWidth,barClient:bar.clientWidth,right:btn.getBoundingClientRect().right, + fg:parseRGBA(s.color).rgb,surface,bg1,bg2,barSurface,borderRGB:border,icon:getComputedStyle(btn,'::before').display,bentoIcon:!!btn.querySelector('.bento-search-icon'),bentoIconDisplay:btn.querySelector('.bento-search-icon')&&getComputedStyle(btn.querySelector('.bento-search-icon')).display,font:s.fontFamily}; + }); + const key=`${layout}/${theme}/${width}`; + // Search labels are always rendered over the opaque/token-mixed bg-2 surface. + fail(ratio(data.fg,data.surface)>=4.5,`${key}: label contrast ${ratio(data.fg,data.surface).toFixed(2)}:1 ${JSON.stringify(data)}`); + fail(ratio(data.borderRGB,data.surface)>=3,`${key}: boundary/surface contrast ${ratio(data.borderRGB,data.surface).toFixed(2)}:1 ${JSON.stringify(data)}`); + fail(ratio(data.borderRGB,data.bg1)>=3,`${key}: boundary/topbar contrast ${ratio(data.borderRGB,data.bg1).toFixed(2)}:1 ${JSON.stringify(data)}`); + fail(data.height>=43.5,`${key}: target height ${data.height}`); + fail(data.formScroll<=data.formClient+1,`${key}: search row overflows ${data.formScroll}/${data.formClient}`); + fail(data.barScroll<=data.barClient+1,`${key}: topbar overflows ${data.barScroll}/${data.barClient}`); + fail(data.right<=width+1,`${key}: button clipped at viewport edge (${data.right})`); + if(layout==='bento-hub'&&width<=860) fail(data.bentoIcon&&data.bentoIconDisplay!=='none',`${key}: Bento injected icon missing/hidden: ${data.bentoIconDisplay}`); + const normalPaint=await page.locator('.search-btn').evaluate(e=>{const s=getComputedStyle(e);return s.backgroundColor+'|'+s.borderColor;}); + await page.locator('.search-btn').hover(); + const hoverPaint=await page.locator('.search-btn').evaluate(e=>{const s=getComputedStyle(e);return s.backgroundColor+'|'+s.borderColor;}); + fail(hoverPaint!==normalPaint,`${key}: hover state is not visible`); + await page.mouse.down(); + const activeTransform=await page.locator('.search-btn').evaluate(e=>({transform:getComputedStyle(e).transform,active:e.matches(':active')})); + await page.mouse.up(); + fail(activeTransform.transform!=='none'&&activeTransform.active,`${key}: active state is not visible ${JSON.stringify(activeTransform)}`); + const hovered=await page.locator('.search-btn').boundingBox(); + fail(Math.abs(hovered.width-before.width)<.1&&Math.abs(hovered.height-before.height)<.1,`${key}: hover caused layout shift`); + await page.keyboard.press('Tab'); + const outline=await page.locator('.search-btn').evaluate(e=>getComputedStyle(e).outlineStyle); + // Focus can land on earlier toolbar controls, so use programmatic focus-visible through keyboard modality. + await page.locator('.search-btn').focus(); + const focus=await page.locator('.search-btn').evaluate(e=>({style:getComputedStyle(e).outlineStyle,width:getComputedStyle(e).outlineWidth})); + fail(focus.style!=='none'&&parseFloat(focus.width)>=2,`${key}: missing focus-visible indicator (${focus.style}/${focus.width}; tab=${outline})`); + await page.locator('.search-btn').evaluate(e=>e.blur()); + await page.mouse.move(width-2,4); + if(width===390||width===1280)await page.screenshot({path:resolve(shots,`${layout}-${theme}-${width}.png`),fullPage:false}); + await page.locator('.search-btn').evaluate(e=>{e.disabled=true;}); + const disabled=await page.locator('.search-btn').evaluate(e=>({opacity:getComputedStyle(e).opacity,cursor:getComputedStyle(e).cursor,borderStyle:getComputedStyle(e).borderStyle})); + fail(disabled.cursor==='not-allowed'&&disabled.borderStyle==='dashed',`${key}: disabled feedback missing ${JSON.stringify(disabled)}`); + await page.locator('.search-btn').evaluate(e=>{e.disabled=false;}); + } + } + await context.close(); +} finally { await browser.close(); await new Promise(r=>server.close(r)); } +if(failures.length){console.error(failures.join('\n'));process.exitCode=1;}else console.log(`Search button checks passed: ${(only?1:layouts.length*themes.length)*4} layout/theme/width combinations; screenshots saved to ${shots}`);