diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css index 3fc1f06996d15..6bd413648fd9a 100644 --- a/apps/desktop/src/styles.css +++ b/apps/desktop/src/styles.css @@ -2344,6 +2344,10 @@ button[data-slot='aui_msg-reactions'] svg { --hud-band-inset: 0.5rem; /* Clear space above the composer for the exit chip. */ --hud-chip-strip: 1.625rem; + /* How long the exit chip stays after you stop pointing at the HUD. Long + enough to cross the gap between the bar and the chip without it + evaporating, short enough that it isn't furniture. */ + --hud-exit-hold: 600ms; } /* Chat surface carries nothing in HUD mode — the visual is the BAND below. */ @@ -2865,11 +2869,42 @@ button[data-slot='aui_msg-reactions'] svg { } /* The exit button. HUD mode has no titlebar, so this is the only visible way - back; it lives in the open space off the bar's outer edge, at the right. + back; it rides the bar's outer edge at the right. - A bare themed glyph rather than a chip: it sits over whatever the user is - really working in, and a card floating there is a stray fragment of app on - their screen. + It wears the BAR'S material, not the desktop's. Every shipped control that + floats over content the app does not own resolves this the same way — give + the control its own substrate and let the glyph read against that, never + against the backdrop: + + - Apple HIG, Materials: "Materials help visually separate foreground + elements, such as text and controls, from background elements." Controls + sit ON a material, never directly on content; even `clear` Liquid Glass + is specced with a 35%-opacity dimming layer behind it over bright + content. + https://developer.apple.com/design/human-interface-guidelines/materials + - Firefox picture-in-picture, the closest analogue we have (a small + always-on-top window over arbitrary content): the close/unpip buttons + are `background-color: rgba(255,255,255,.8)` with a `#000` glyph — an + opaque chip, not a bare icon. + toolkit/themes/shared/pictureinpicture/player.css + - Discord's overlay redesign: "This layer provided contrast against the + game making the UI easier to see." + https://discord.com/blog/redesigning-the-discord-overlay + + Anything that instead tries to derive contrast from the backdrop is a dead + end here. mix-blend-difference composites against the PAGE behind the + element, and behind a transparent Electron window that is nothing — the + desktop is composited by WindowServer after Chromium has finished drawing + the frame (the same reason backdrop-filter can't frost the HUD). A glyph + halo has the same flaw from the other side: it guesses one backdrop + luminance and is grime over everything else. + + So: the composer bar's exact tokens — same fill, same hairline, same bottom + shadow. The bar is the HUD's solved case for "our surface, over an unknown + desktop, in either appearance", and theme plus OS light/dark (mode 'system') + come free with it, because --dt-card and --ui-text-primary are the pair the + whole app is built on. The chip inverts with the appearance instead of + betting on one. Placed from --hud-bar-height rather than CSS anchor(). Lightning CSS drops a whole rule containing an `anchor()` on the vertical axis, so the flipped-edge @@ -2879,22 +2914,42 @@ button[data-slot='aui_msg-reactions'] svg { top: auto; right: 0.375rem; bottom: calc(var(--hud-bar-height, var(--composer-fallback-height)) + 0.375rem); - aspect-ratio: 1; + /* Square. `size="icon-titlebar"` sizes width and height from two DIFFERENT + vars (--titlebar-control-size / --titlebar-control-height, 20x22 today), + which is right in a titlebar row and reads as a dent on a lone floating + chip — so height comes off the width instead. */ + aspect-ratio: 1 / 1; height: auto; - background: transparent !important; - border: 0; + background: var(--dt-card) !important; + border: 1px solid var(--ui-stroke-secondary) !important; + border-radius: 0.5rem; + box-shadow: 0 2px 2px -1px rgb(0 0 0 / 0.12); color: var(--ui-text-primary) !important; - /* Always clickable, dim at rest. Gating the CLICKS on composer `:focus` made - the only visible way out of HUD mode depend on the thing most likely to be - broken when someone wants out: if focus never lands (#81893) you can't type - AND the chip silently swallows clicks, leaving a transparent always-on-top - rectangle over the desktop with no in-app way to dismiss it. + /* Gone until you reach for the HUD. A permanent chip is a fragment of Hermes + parked on top of whatever you are really working in; reaching for the bar + is the motion that means "I want the app", so that is what brings the way + out with it. Hover, not focus — the gate #81893 warned about made the + escape hatch depend on the caret landing in the composer, broken exactly + when you most want out, whereas pointing at the bar needs nothing to be + working. - Dim rather than invisible keeps the original intent — this shouldn't be a - loud chip floating over whatever you're actually working in — without - making the escape hatch conditional on the failure it exists for. */ - opacity: 0.45; - transition: opacity var(--hud-fade) var(--hud-ease-exit); + `visibility`, not opacity alone: an always-on-top window eats clicks + wherever the page hands the hit test something real, so a 0-opacity chip + with `pointer-events: auto` would be an invisible button in the corner + that drops you out of HUD mode when you click the app behind it. + `visibility: hidden` takes it out of `elementFromPoint` as well as off the + screen, and it transitions discretely, so it flips in step with the fade + rather than needing a second mechanism. */ + opacity: 0; + visibility: hidden; + /* Leaving holds first. The chip sits 0.375rem clear of the bar, so the + cursor crosses shell dead space on its way up from the bar to the chip and + both hover triggers are false for that moment — without the hold it would + fade out from under a hand that is on its way to press it. */ + transition: + opacity var(--hud-fade) var(--hud-ease-exit) var(--hud-exit-hold), + visibility 0s linear calc(var(--hud-exit-hold) + var(--hud-fade)), + background-color var(--hud-reveal) var(--hud-ease-enter); pointer-events: auto; } @@ -2905,18 +2960,36 @@ button[data-slot='aui_msg-reactions'] svg { top: 0; } -/* Hover and focus-visible brighten it too, so pointing at the chip confirms it - is a control before you commit the click. */ -[data-hud-shell]:has([data-slot='composer-rich-input']:focus) [data-hud-exit], +/* What counts as reaching for it: the bar, the transcript band, or the chip + itself. The band only answers `:hover` while it is on screen (faded out it + is `pointer-events: none`), so this can't reveal the chip over a HUD that + isn't there. The chip's own hover is in the set so arriving re-asserts the + reveal that the hold above was covering for. + + Hovering the shell at large is deliberately NOT a trigger — most of the + HUD's rectangle is empty window over someone else's app, and it is + `pointer-events: none` precisely so the cursor passing through means + nothing. */ +[data-hud-shell]:has([data-slot='composer-dock']:hover) [data-hud-exit], +[data-hud-shell]:has([data-slot='composer-bounds']:hover) [data-hud-exit], [data-hud-shell] [data-hud-exit]:hover, [data-hud-shell] [data-hud-exit]:focus-visible { - opacity: 0.9; - transition-duration: var(--hud-reveal); + opacity: 1; + visibility: visible; + transition-duration: var(--hud-reveal), 0s, var(--hud-reveal); transition-delay: 0s; } -[data-hud-shell]:has([data-slot='composer-rich-input']:focus) [data-hud-exit]:hover { - opacity: 1; +/* Hover and focus-visible: the app's own control-hover tint, so pointing at it + confirms it is a button with the same feedback every other icon button in + Hermes gives. Layered OVER the card rather than replacing it — + --ui-control-hover-background is a translucent color-mix meant to sit on an + opaque titlebar, and here the button IS the opaque surface, so assigning it + directly would make the chip see-through on hover. */ +[data-hud-shell] [data-hud-exit]:hover, +[data-hud-shell] [data-hud-exit]:focus-visible { + background: + linear-gradient(var(--ui-control-hover-background), var(--ui-control-hover-background)), var(--dt-card) !important; } /* The corner resize handle — a hot corner, not a button. The window is created