desktop-new: glass is one material, applied whole

A glass surface is four things at once — a translucent fill, a blur behind it, a
directional rim, and sometimes an outer lift — and they were four separate
things a consumer assembled by hand. Every consumer did, differently. The four
chrome surfaces meant to match disagreed about whether to carry an outer shadow,
and four inner panels set a translucent fill with no blur at all.

`glass-primary` and `glass-secondary` are Tailwind `@utility` blocks carrying the
whole material, plus `-interactive` variants that add the hover. Named by
stacking depth rather than by component: an earlier cut named them chrome /
panel / float and it was wrong twice over, because `chrome` and `float` both
landed on glass-3 — 66% opaque in dark, which is why chrome did not read as
glass — and naming a material after a component invites a second material the
first time a component wants a different depth.

`box-shadow` is composed inside each material because it has to be: a rim and a
lift both set it, so as separate utilities the second silently overwrites the
first. That is a structural argument for bundling, not a preference. The fills
are deliberately not registered as colour utilities, so a bare `bg-glass-primary`
class cannot exist.

Removing a redundant fill from the four workspace panels fixed something no
token held. They each set the same `glass-2` as the container they exactly cover
— measured 610x551 inside 612x553, the two pixels being its border — and two
identical translucent layers composite: 0.52 over 0.52 is 0.77, between glass-4
and glass-5. Panels meant to be the most translucent surface in the system read
as nearly solid.

That in turn exposed a bug the density had been masking since the dock landed:
dockview's theme sets `--dv-group-view-background-color` to a literal `white`
and `DockWorkspace` passes `themeLight` in both modes, so an opaque white sheet
sat behind the glass. Reset here, because every other dockview surface the
variable feeds was already overridden to transparent.

Signed-off-by: Morgan Martin <morganmartin@squareup.com>
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
morgmart
2026-09-07 15:59:07 -07:00
parent ec918bb75f
commit 698b73d943
4 changed files with 158 additions and 57 deletions

View File

@@ -35,8 +35,8 @@
display: grid;
place-items: center;
border-radius: 50%;
background: var(--bg-accent-tint);
color: var(--text-accent);
background: var(--purple-3);
color: var(--purple-12);
}
.agent-avatar {
@@ -69,13 +69,16 @@
}
.agent-presence[data-running] {
background: var(--bg-success);
background: var(--green-9);
}
/* No background, for the same reason as the panels in product.css:
this fills `.dv-content-container` (measured 610x718 inside 612x720, the two
pixels being its border) and that container is already primary glass. Its own
identical fill was compositing to 0.77 alpha rather than 0.52. */
.agent-main-panel {
height: 100%;
overflow-y: auto;
background: var(--bg-panel-glass);
}
.agent-profile-header {
@@ -85,7 +88,7 @@
align-items: center;
gap: 1rem;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--border-tertiary);
border-bottom: 1px solid var(--border-primary);
}
.agent-profile-title {
@@ -132,7 +135,7 @@
.field-stack textarea,
.field-stack select {
width: 100%;
border: 1px solid var(--border-secondary);
border: 1px solid var(--border-primary);
border-radius: 0.75rem;
padding: 0.75rem;
background: var(--bg-panel);

View File

@@ -18,7 +18,7 @@
border: 0;
border-radius: var(--radius-chip);
padding-inline: var(--space-chip-inset);
background: var(--bg-accent-tint);
background: var(--purple-3);
/* Primary text, not accent text. The tint is doing the colour work, so the
label stays the same weight as the sentence it sits in — a chip marks a
reference, it does not shout. */
@@ -40,11 +40,11 @@
}
button.inline-chip:hover {
background: var(--bg-accent-tint-hover);
background: var(--purple-4);
}
.inline-chip:focus-visible {
outline: 2px solid var(--ring-focus);
outline: 2px solid var(--purple-8);
outline-offset: 2px;
}

View File

@@ -17,23 +17,111 @@
}
/*
* The glass rim. Two inset shadows rather than a gradient border, because CSS
* borders accept only solid colours: border-image takes a gradient but ignores
* border-radius, and the two-background background-clip trick needs an opaque
* inner fill so it bleeds across a translucent surface. See DESIGN.md.
* ---------------------------------------------------------------------------
* Glass materials — PROPOSED, for review on /design/glass.
*
* A glass surface is four things at once: a translucent fill, a blur behind it,
* a directional rim, and (sometimes) an outer lift. Those used to be separate,
* so a consumer could take the fill and forget the rest — and every consumer
* did, differently. The four chrome surfaces meant to match each assembled their
* own material and disagreed about the outer shadow; four inner panels set a
* translucent fill with no blur *and* no need for one, since they exactly cover
* a surface that was already glass, so their fill composited 0.52 over 0.52 to
* 0.77 — a value no token holds and nobody chose. These bundle the material so
* it arrives whole or not at all.
*
* NAMED BY STACKING DEPTH, not by the component that uses it. An earlier cut
* named them chrome / panel / float and it was wrong twice over: `chrome` and
* `float` both landed on glass-3 (66% opaque in dark — two thirds solid, which
* is why chrome did not read as glass at all), and naming a material after a
* component invites a second material the first time a component wants a
* different depth. Depth is the axis that actually varies.
*
* The rule is DESIGN.md § Surface and depth: **elevation reads as less
* translucency, not as a lighter colour.** So the higher it sits, the more
* opaque it gets, and the shadow separates it from what it covers.
*
* `box-shadow` is composed here once because it has to be: `rim-glass` and
* `elevate-xs` both set `box-shadow`, so used together the second silently
* overwrites the first. A rim and a lift cannot compose as separate utilities.
*/
@utility rim-glass {
/* Sits directly on the backdrop: chrome, nav, a region in glass. The most
translucent, because the backdrop is the thing worth seeing through to. */
@utility glass-primary {
background: var(--bg-glass-primary);
backdrop-filter: blur(var(--blur-md));
box-shadow:
inset 0 1px 0 var(--rim-lit),
inset 0 -1px 0 var(--rim-shade);
}
@utility blur-chrome {
backdrop-filter: blur(var(--blur-md));
/* Sits over something already glass: popovers, menus, a modal over a panel.
Less translucent so it separates, deeper blur so what it covers is not
legible through it, and a real shadow because it is genuinely above. */
@utility glass-secondary {
background: var(--bg-glass-secondary);
backdrop-filter: blur(var(--blur-lg));
box-shadow:
var(--shadow-sm),
inset 0 1px 0 var(--rim-lit),
inset 0 -1px 0 var(--rim-shade);
}
@utility blur-panel {
/*
* Interactive glass: the material plus its hover, for a glass surface you can
* click. Separate utilities rather than a `:hover` inside the two above,
* because a glass *region* is not interactive and should not react to a cursor
* crossing it.
*
* A glass hover moves one step up the ramp and never changes blur —
* re-blurring a large surface every frame is expensive enough to feel. Opacity
* here is a step on a named ramp, not a fade applied to a colour.
*/
@utility glass-primary-interactive {
background: var(--bg-glass-primary);
backdrop-filter: blur(var(--blur-md));
box-shadow:
inset 0 1px 0 var(--rim-lit),
inset 0 -1px 0 var(--rim-shade);
&:hover {
background: var(--bg-glass-primary-hover);
}
}
@utility glass-secondary-interactive {
background: var(--bg-glass-secondary);
backdrop-filter: blur(var(--blur-lg));
box-shadow:
var(--shadow-sm),
inset 0 1px 0 var(--rim-lit),
inset 0 -1px 0 var(--rim-shade);
&:hover {
background: var(--bg-glass-secondary-hover);
}
}
/*
* `rim-glass`, `blur-panel`, `elevate-xs`, and `elevate-sm` were here and are
* gone: every one had zero readers once the glass materials absorbed them.
*
* `rim-glass` and `elevate-xs` are worth remembering as a pair, because they
* could not be composed. Both set `box-shadow`, so an element using the rim and
* a lift together silently kept whichever Tailwind emitted second — a rim and an
* elevation cannot be separate utilities. That is not a naming problem; it is why
* the materials compose `box-shadow` once, internally.
*
* The elevation *tokens* are alive and read directly as `var(--shadow-xs)` in
* five stylesheets. A one-declaration utility wrapping a token earns nothing.
*
* `blur-chrome` survives on two readers, both documentation: the /design pages
* need a blur without a fill so a swatch can show a ramp step against the real
* backdrop. Product code should reach for a material instead.
*/
@utility blur-chrome {
backdrop-filter: blur(var(--blur-md));
}
@utility texture-dots {
@@ -44,14 +132,6 @@
background-size: var(--texture-dots-size) var(--texture-dots-size);
}
@utility elevate-xs {
box-shadow: var(--shadow-xs);
}
@utility elevate-sm {
box-shadow: var(--shadow-sm);
}
/* Composition-input spike only. Remove with the spike route. */
.spike-editor {
outline: none;

View File

@@ -47,32 +47,30 @@
.destination-tabs button,
.topbar-icon-button {
@apply glass-primary-interactive;
height: 1.75rem;
border: 0;
border-radius: 0.5625rem;
background: var(--bg-chrome-glass);
color: var(--text-secondary);
font: inherit;
cursor: pointer;
backdrop-filter: blur(var(--blur-md));
box-shadow:
inset 0 1px 0 var(--rim-lit),
inset 0 -1px 0 var(--rim-shade);
}
.destination-tabs button {
padding: 0 0.75rem;
}
/* The material carries the hover fill; this only carries the text colour,
which is not part of a surface material. */
.destination-tabs button:hover,
.topbar-icon-button:hover {
background: var(--bg-chrome-glass-hover);
color: var(--text-primary);
}
.destination-tabs button[data-selected] {
background: var(--bg-inverse);
color: var(--text-on-inverse);
background: var(--neutral-11);
color: var(--neutral-1);
box-shadow: var(--shadow-sm);
}
@@ -101,12 +99,24 @@
.buzz-dockview {
--dv-background-color: transparent;
--dv-tabs-and-actions-container-background-color: transparent;
/* Dockview's own theme sets this to a literal `white`, and `DockWorkspace`
passes `themeLight` in both colour schemes — so in dark mode the vendor
painted an opaque white sheet *behind* our glass, and the 52% dark fill
over it composited to flat grey. The panels' redundant second
fill had been hiding it: two stacked fills reached 0.77 alpha, opaque
enough that the white underneath barely showed. Removing the redundancy
revealed a bug that had been there since the dock landed.
Reset here rather than swapping the JS theme, because every dockview
surface this variable feeds is already overridden to transparent below —
the variable was the one leak. */
--dv-group-view-background-color: transparent;
--dv-tab-active-background-color: var(--bg-chrome-selected);
--dv-tab-active-color: var(--text-primary);
--dv-tab-inactive-color: var(--text-secondary);
--dv-separator-border: transparent;
--dv-drag-over-background-color: var(--bg-accent-tint);
--dv-drag-over-border-color: var(--border-accent);
--dv-drag-over-background-color: var(--purple-3);
--dv-drag-over-border-color: var(--purple-8);
height: 100%;
}
@@ -139,37 +149,45 @@
box-shadow: var(--shadow-xs);
}
/* Dockview arranges panels; Panel owns the surface treatment. */
.buzz-dockview .dv-content-container {
overflow: hidden;
border: 1px solid var(--border-tertiary);
border-radius: 1.5rem;
background: var(--bg-panel-glass);
box-shadow:
var(--shadow-sm),
inset 0 1px 0 var(--rim-lit),
inset 0 -1px 0 var(--rim-shade);
backdrop-filter: blur(var(--blur-md));
}
.buzz-dockview .dv-sash {
width: var(--space-2);
background: transparent;
cursor: col-resize;
}
.buzz-dockview .dv-sash:hover {
background: var(--border-accent);
background: var(--border-primary);
}
.buzz-dockview:has(.dv-tab.dv-tab-dragging) {
user-select: none;
}
/* No background. These fill `.dv-content-container` edge to edge — measured at
610x551 inside 612x553, the two pixels being its border — and that container
is already primary glass.
They each set the same `glass-2` fill with no blur of their own, so the two
identical translucent layers composited: 0.52 over 0.52 is **0.77**, landing
between glass-4 and glass-5. The panels read as nearly solid, which is why
the backdrop barely showed through a surface that was supposed to be the
most translucent one in the system. Nothing declared 0.77 and no token holds
it — it was the arithmetic of a redundant fill.
A region that exactly covers a glass surface does not need its own. If one
of these later needs to sit apart from its container, it takes a whole
material rather than a bare fill. */
.workspace-navigator,
.session-view,
.new-session-view,
.empty-workspace-panel {
height: 100%;
min-width: 0;
background: var(--bg-panel-glass);
}
.workspace-navigator,
@@ -186,7 +204,7 @@
min-height: 4rem;
align-items: center;
padding: 0 1.25rem;
border-bottom: 1px solid var(--border-tertiary);
border-bottom: 1px solid var(--border-primary);
}
.navigator-heading {
@@ -262,7 +280,7 @@
.navigator-row:hover,
.session-child-row:hover,
.navigator-row-action:hover {
background: var(--bg-hover);
background: var(--neutral-4);
color: var(--text-primary);
}
@@ -307,8 +325,8 @@
.session-thread-line {
width: 0.625rem;
height: 0.875rem;
border-bottom: 1px solid var(--border-secondary);
border-left: 1px solid var(--border-secondary);
border-bottom: 1px solid var(--border-primary);
border-left: 1px solid var(--border-primary);
border-radius: 0 0 0 0.375rem;
}
@@ -368,8 +386,8 @@
}
.message-avatar[data-agent] {
background: var(--bg-accent-tint);
color: var(--text-accent);
background: var(--purple-3);
color: var(--purple-12);
}
.message-meta {
@@ -399,7 +417,7 @@
align-items: end;
gap: 0.75rem;
margin: auto;
border: 1px solid var(--border-secondary);
border: 1px solid var(--border-primary);
border-radius: 1rem;
padding: 0.625rem;
background: var(--bg-float);
@@ -433,8 +451,8 @@
height: 2.25rem;
place-items: center;
border-radius: 0.75rem;
background: var(--bg-inverse);
color: var(--text-on-inverse);
background: var(--neutral-11);
color: var(--neutral-1);
}
.quiet-button,
@@ -450,8 +468,8 @@
}
.primary-button {
background: var(--bg-inverse);
color: var(--text-on-inverse);
background: var(--neutral-11);
color: var(--neutral-1);
}
.new-session-center {
@@ -476,7 +494,7 @@
button:focus-visible,
textarea:focus-visible {
outline: 2px solid var(--border-accent);
outline: 2px solid var(--purple-8);
outline-offset: 2px;
}