From e9880b7249c183b61a92bd28943d27317b10746c Mon Sep 17 00:00:00 2001 From: chidr Date: Thu, 13 Aug 2026 17:26:58 +0530 Subject: [PATCH 1/2] ui: give the app a focus state, a brand selection, and honest reduced motion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four foundations the design system was missing. All of it lives in src/styles.css with no component changes, so it composes with everything currently in flight. FOCUS. Nine components clear the UA outline with `outline-none`, and nothing anywhere put a ring back — there is not one `focus-visible:` utility in the app. Measured in the running app, all eight text fields focused invisibly: the composer, sidebar search, the answer box on an approval card, both onboarding fields, and all three API key fields. A keyboard user filling in App Settings could not tell which key they were typing into. Buttons kept the browser default, which is legible but reads as an unstyled page inside a hand-tuned dark UI. The rules are deliberately unlayered. Tailwind ships utilities in @layer utilities and unlayered styles outrank every layer, so this beats `outline-none` with no !important and without editing the nine components — every one of which has an open PR against it. Verified: a mouse click still paints nothing, Tab paints a 2px accent ring. The generic ring sets no radius, because an outline already follows the element's own corners and 26 controls here are `rounded-full`. Text fields are the exception: most carry a radius already, but the composer input has none, so its ring came out a hard rectangle inside a pill. SELECTION. ::selection was never styled, so dragging over a bot's reply painted Chrome's default blue through a palette that was pixel-sampled off the real app. It now tints the accent. REDUCED MOTION. The mascot was the only thing honouring prefers-reduced-motion. Panels still flew in, cards still popped, and every hover transition still ran. Now the app stills: panel-in 0.24s and pop-in 0.2s both collapse to 0.01ms under reduce, and normal playback is untouched without it. Durations collapse rather than `animation: none`, because a spinner frozen mid-turn reads as a hung app. The scrollbar thumb also gets a hover state; it was inert. No palette values changed, no radii tokens, no type. Nothing here restyles what was already designed — it fills in what had no styling. --- src/styles.css | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/styles.css b/src/styles.css index 008587f66..80bde2416 100644 --- a/src/styles.css +++ b/src/styles.css @@ -19,6 +19,9 @@ --color-ink-secondary: #fcfcfc99; --color-accent: #1084fe; --color-accent-border: #459ffe; + /* the ring a keyboard user follows; sits on the accent so focus reads as + "the app is talking to you", not as a browser default */ + --color-focus: #459ffe; --color-bubble-user: #5a5a5a; --color-success: #38d591; --color-danger: #ff5667; @@ -45,16 +48,65 @@ body { overflow: hidden; } +/* ── focus ─────────────────────────────────────────────────────────────── + Nine components clear the UA outline with `outline-none` and nothing + put a ring back, so every text field in the app — composer, search, the + answer box on an approval card, both onboarding fields, all three API + key fields — focused invisibly. Buttons kept the browser default, which + is legible but looks like an unstyled page inside a hand-tuned dark UI. + + These rules are deliberately UNLAYERED. Tailwind ships its utilities in + @layer utilities, and unlayered styles beat every layer regardless of + specificity, so this lands on top of `outline-none` without !important + and without touching the nine components (all of which have open PRs + against them). Everything is :focus-visible, so a mouse click never + paints a ring — only keyboard and programmatic focus do. + + The ring deliberately sets no border-radius: an outline already follows + the element's own corners, and 26 controls in here are `rounded-full`, + which a fixed radius would square off. */ +:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +/* Text fields fill their container edge to edge — the composer and the + sidebar search sit flush inside a padded surface — so an outline drawn + outside them is clipped by the parent. Draw this one just inside the + edge instead, where it always survives. + + Most fields already carry a radius and the outline picks it up. The + composer input has none, so its ring came out a hard rectangle sitting + inside a pill; the radius here only ever softens that corner, and text + fields are never the `rounded-full` controls. */ +:is(input, textarea, select):focus-visible { + outline-offset: -2px; + border-radius: var(--radius-xl); +} + +::selection { + background: color-mix(in oklab, var(--color-accent) 45%, transparent); + color: var(--color-ink); +} + ::-webkit-scrollbar { width: 8px; + height: 8px; } ::-webkit-scrollbar-thumb { background: #3d3d3d; border-radius: 4px; } +/* the thumb was inert — it never acknowledged the pointer */ +::-webkit-scrollbar-thumb:hover { + background: var(--color-raised-hover); +} ::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-corner { + background: transparent; +} @keyframes panel-in { from { transform: translateX(28px); opacity: 0; } @@ -554,4 +606,22 @@ body { } .maus-eyes { transform: none; } + + /* The mascot was the only thing that honoured this. Panels still flew in + from the right, cards still popped, and every hover transition still + ran — the settings drawer and the computer panel are the big ones. A + user who asked the OS for less motion gets a still app now. + + Not `animation: none`: a spinner that never advances reads as a hung + app. Collapse the duration instead, so anything looping still ticks + over without travelling. */ + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-delay: 0ms !important; + transition-duration: 0.01ms !important; + transition-delay: 0ms !important; + scroll-behavior: auto !important; + } } From b35023b75b7a7776e015f532a86c1add0effa05d Mon Sep 17 00:00:00 2001 From: milind-soni Date: Tue, 18 Aug 2026 10:26:45 +0530 Subject: [PATCH 2/2] Address focus and reduced-motion review --- src/styles.css | 57 +++++++++++++++++--------------------------------- 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/src/styles.css b/src/styles.css index 9c7f10b27..82ffcdb7c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -48,40 +48,19 @@ body { overflow: hidden; } -/* ── focus ─────────────────────────────────────────────────────────────── - Nine components clear the UA outline with `outline-none` and nothing - put a ring back, so every text field in the app — composer, search, the - answer box on an approval card, both onboarding fields, all three API - key fields — focused invisibly. Buttons kept the browser default, which - is legible but looks like an unstyled page inside a hand-tuned dark UI. - - These rules are deliberately UNLAYERED. Tailwind ships its utilities in - @layer utilities, and unlayered styles beat every layer regardless of - specificity, so this lands on top of `outline-none` without !important - and without touching the nine components (all of which have open PRs - against them). Everything is :focus-visible, so a mouse click never - paints a ring — only keyboard and programmatic focus do. - - The ring deliberately sets no border-radius: an outline already follows - the element's own corners, and 26 controls in here are `rounded-full`, - which a fixed radius would square off. */ +/* Keep focus visible after components clear the browser outline. These + unlayered rules outrank Tailwind utilities without needing !important. */ :focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; } -/* Text fields fill their container edge to edge — the composer and the - sidebar search sit flush inside a padded surface — so an outline drawn - outside them is clipped by the parent. Draw this one just inside the - edge instead, where it always survives. - - Most fields already carry a radius and the outline picks it up. The - composer input has none, so its ring came out a hard rectangle sitting - inside a pill; the radius here only ever softens that corner, and text - fields are never the `rounded-full` controls. */ -:is(input, textarea, select):focus-visible { +/* Chromium intentionally matches :focus-visible on text controls after a + pointer click too. Make that behavior explicit: every focused editor has + a clear inset ring, while retaining each control's own border radius. */ +:is(input, textarea, select):focus { + outline: 2px solid var(--color-focus); outline-offset: -2px; - border-radius: var(--radius-xl); } ::selection { @@ -131,6 +110,10 @@ body { from { background-position: 200% 50%; } to { background-position: -200% 50%; } } +@keyframes reduced-loader-pulse { + 0%, 100% { opacity: 0.45; } + 50% { opacity: 1; } +} .thinking-shimmer { background: linear-gradient(90deg, var(--color-ink-secondary) 40%, var(--color-ink) 50%, var(--color-ink-secondary) 60%); background-size: 200% 100%; @@ -609,21 +592,19 @@ body { .maus-eyes { transform: none; } - /* The mascot was the only thing that honoured this. Panels still flew in - from the right, cards still popped, and every hover transition still - ran — the settings drawer and the computer panel are the big ones. A - user who asked the OS for less motion gets a still app now. - - Not `animation: none`: a spinner that never advances reads as a hung - app. Collapse the duration instead, so anything looping still ticks - over without travelling. */ + /* Stop spatial motion instead of accelerating infinite animations. Loading + icons keep a slow opacity pulse, so progress remains visible without + rotation, bounce, or flicker. */ *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-delay: 0ms !important; + animation: none !important; transition-duration: 0.01ms !important; transition-delay: 0ms !important; scroll-behavior: auto !important; } + + .animate-spin { + animation: reduced-loader-pulse 1.6s ease-in-out infinite !important; + } }