cmux NEXT glass: the glass lints fail on opaque and near-opaque overlay surfaces
Leo: "I don't like the non-glassness". The standing rule: Glass, never opaque.
What and why
Two overlay surfaces went opaque again without any check noticing: the sidebar's Updated card (#22670 made it near-opaque on purpose: a 94% fill layered under its tint) and the agent chat's Summary popover (the solid popup surface, no blur, no glass class). Both slipped past the existing glass lints. Those lints only checked fills they could prove 100% opaque, and on the web only on rules that already blur or are named glass. This PR extends the two lints we already have instead of adding a third. Popovers, cards, menus, toasts and sheets now fail CI when they paint an opaque or near-opaque fill outside an allowlist, on both native and web.
The failure message states the rule: "Glass, never opaque" (Leo). Legibility comes from blur, tint and contrast inside the glass. Reduce Transparency is the only opaque fallback. An allowlisted surface needs a written reason.
Native, scripts/cmux-next/check-overlay-glass.py (same heuristic, line-based style as the #24228 rules):
surface-window: a toast, sheet, dialog, card, banner, bubble or notice window class (NSPanel/NSWindow) whose file builds no shared glass (Glass.makeOverlayPanel,Glass.makePanel,OverlaySurfaceView,CardSurfaceView,GlassPanelView). If the glass is built in another file, the window is listed inSURFACE_GLASS_FROM, and the check confirms that file still builds it. An entry is stale when its window is gone.dense-fill: in a file that builds a floating surface's glass, abackgroundColor/fillColor/.background(...)at 90% or more (withAlphaComponent,withAlpha,opacity). This is the native version of what #22670 did.card-material:CardMaterial(...), the mostly opaque (90% or more) card fill, used outside its own files.TEAMMATE_SURFACES: Lawrence's R48 surfaces are never flagged (see below).
Web, webviews/scripts/check-glass-never-opaque.ts: findOpaqueOverlays checks every rule whose subject's class ends in an overlay role word (popover, popup, menu, dropdown, toast, sheet, dialog, card, tooltip, hovercard, picker, palette, bubble; a part such as .ui-menu-item is not a surface), plus every glass rule. Each such rule fails when its fill covers 90% or more outside Reduce Transparency. Layered backgrounds are composited, and color-mix shares and host tokens are resolved (--cmux-card-tint counts as 90%). Gallery stages (*.gallery.css, gallery/) and the unshipped prototypes/ are skipped. Exceptions go in OVERLAY_ALLOWED with a reason, or in OVERLAY_PENDING for surfaces that are solid today. A pending entry fails as stale once its surface is glass, so the entry gets removed along with the fix. The test lane runs it through test/glass-never-opaque.test.ts, as it already runs the other glass checks.
What the lints now report (terminal output, not UI)
Native fixture: a toast window with no glass, and a 94% veil on a glass card:
--- old check-overlay-glass.py
check-overlay-glass: ok
--- new check-overlay-glass.py
Demo/DemoToast.swift:3: [dense-fill] func paint() { veil.layer?.backgroundColor = Palette.textPrimary.withAlphaComponent(0.94).cgColor }
Demo/DemoToastPanel.swift:1: [surface-window] final class DemoToastPanel: NSPanel {}
Web fixture: the Summary popover's solid fill and #22670's layered 94% Updated card:
--- old findOpaqueGlass: []
--- new findOpaqueOverlays:
fixture.css:1: .acpmux-summary-card[data-placement=popover] { background: var(--cmux-popup-surface) }
fixture.css:2: .wn-card { background: linear-gradient(red, red), color-mix(in srgb, #fff 94%, transparent) }
On base, both lints pass with the lists below (check-overlay-glass: ok, Glass fills checked: none opaque.).
Red/green: every new expect_fail case in scripts/cmux-next/tests/check-overlay-glass.test.sh fails when run against the old checker (I ran the new cases against the base script: each one reported passed, expected [rule]). The new web tests assert that findOpaqueGlass returns nothing for the same fixtures that findOpaqueOverlays now fails.
No capture: lint-only change with no UI. Every changed file is a lint script or its test (scripts/cmux-next/check-overlay-glass.py and its test, webviews/scripts/check-glass-never-opaque.ts, webviews/test/glass-never-opaque.test.ts). No stylesheet, component or Swift view changes, so no surface renders differently and there is no screenshot to take. Visual review: not applicable (no pixel changes); the terminal output above is the whole change.
Allowlist (every surface this PR exempts, and why)
| Surface | Lint | Kind | Why |
|---|---|---|---|
CmuxNextDesignTokens/Windows/ThemeTokens+Surface.swift (R48: surfaceBackground, cardFill, legibilityScrim, capsuleScrim) | native | teammate | Lawrence R48: the window's one surface and the cards on it are a tint over the window. They are his call and are kept unchanged. |
CmuxNextSettingsWindow/Views/SettingsStyle.swift (Settings group card, ThemeTokens.cardFill) | native | teammate | Lawrence R48, as above |
CmuxNextDesign/OverlaySurface.swift card-material | native | allowed | The opt-in card fill's own code path. Only makeOverlayPanel(cardFill: true) reaches it, and card-fill already flags each caller. |
CmuxNextDesign/Windows/WebTheme.swift card-material | native | allowed | Hands pages --cmux-card-tint; the web check counts that token as near-opaque on an overlay. |
CmuxNextLayout/Views/DockBackdropView.swift dense-fill | native | allowed | A dock column's backdrop: the person's docks background over the window color (R55). It is a pane, not an overlay. |
CmuxNextServer/Views/AddServerCardPanel.swift (SURFACE_GLASS_FROM) | native | glass elsewhere | Its content view, ServerHostView, is the server surfaces' OverlaySurfaceView. |
optchat-inspector/styles.css .card | web | allowed | The developer inspector's in-page report cards: they sit on their own page and float over nothing. |
pages/chief-views/styles.css .cv-card | web | allowed | A Chief view's in-page content card that does not float. The Chief area belongs to its owners, so this PR leaves it alone. |
acpmux/conversation/conversation.css .acpmux-image-viewer | web | allowed | The full-pane image viewer: a blurred lightbox (94% over a 24px blur) that sets the image apart from the chat. It is not a floating card. |
acpmux/summary/summary.css .acpmux-chat-gallery | web | allowed | The chat's full-pane image gallery, the same lightbox |
acpmux/summary/summary.css .acpmux-summary-card and [data-placement=popover] | web | pending | The Summary popover's regression. Its glass fix is in flight; the entries fail as stale once it lands. |
pages/shared/pageBase.css .page-menu | web | pending | The shared page menu paints the solid popup surface |
pages/cloud/styles.css .cloud-create-sheet | web | pending | Cloud's create sheet paints the elevated surface |
pages/home-channels/ledger.css .lg-sheet, .lg-palette, .lg-form | web | pending | The ledger's side sheet, palette dialog and form dialog are solid |
pages/markdown/styles.css .cmux-comment-card | web | pending | The Markdown viewer's comment card paints the elevated surface |
pages/markdown/styles.css .md-link-bubble | web | pending | The link editor popover paints --cmux-md-popover-background (the solid popup surface) over its glass |
ui/ui.css .ui-sheet | web | pending | The shared sheet paints --ui-surface |
ui/findPill.css .find-pill | web | pending | The find pill paints the card material (--cmux-card-tint, at least 90%) |
The pending surfaces are the backlog this lint found on base. Each one fails CI as stale when it turns glass, so the fix PR removes its entry.
Risk
The new rules only add checks; no product code changes. They are heuristics in the existing style, so a false failure on plausible new code is the main risk. Each such case has an escape: an inline glass-opaque-allow: <reason> or an allowlist entry with a reason. The pending entries tie this PR to the Summary popover fix. Whichever of the two lands second removes the stale entries.
Agent session: agent session
š¤ Generated with Claude Code