Files
dsh-context/src/client/styles/stats.css
T
Bowen Liang 17183882be refactor(client): move pure pseudo-state styling to Tailwind state variants
The 20 hover/focus rules that are leaf styles (no state class re-asserts
them) become hover:/focus:/group-hover: variants on the elements' own
class lists, and their sheet rules are deleted: row and button hover
tints, retry/close/gate brightness, input focus borders, underline
hints, the three styled-tip reveals (named group/tip), the plugin-info
row underlines (group/pi), and the agent halo's hover/focus wash
(group/agent — the React hover state keeps driving the inspector;
lc-agent-hover stays as a test anchor).

The hover/lit(-on) cascade pyramids stay hand-written in the sheets —
the gran/rich-seg/rich-copy/fa-chip toggles and the settings card's
open state need ordered overrides over their own hover rules, several
of those state classes double as test anchors and DOM query hooks, and
moving them would mean a chip factory for zero visual change.
Transitions, animations and all harness --dsw-* tokens stay in sheets.

Behavior notes: variant utilities carry (0,2,0) specificity, so they
keep beating the sheet base rules exactly as the old :hover rules did;
Tailwind media-guards hover behind (hover: hover), so touch devices no
longer get sticky hover styling (dsh web is desktop-first).
2026-09-13 09:41:30 +08:00

137 lines
10 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* The head band's stat cards — the context/plugin row and the token/timing
* donut row: the context card's count grid with the cost cell's styled tip,
* the composition overview's big number, and the cards' shared anatomy
* — section heading, donut, slice list, plugin-info rows.
*/
/* The context card's six count cells: the session's shape, the cache-hit
rate, and the cost — 3×2 by default (the cells stay wide enough for the
longest English label), folding to a single row on wide cards. */
.lc-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
.lc-stat { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; padding: 6px 6px; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/* The 5-cell stats grid shares a half-width head-row card with a sibling, so its cells are moderate: tighter padding plus a corner-pinned '?' (out of the label's line) keep the longest labels on one line. */
.lc-stats .lc-stat { padding: 6px 3px; }
.lc-stats .lc-stat-q { position: absolute; top: 3px; right: 4px; margin: 0; }
.lc-stat-label { color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lc-stat-value { color: var(--dsw-alias-label-primary); font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; }
.lc-stat-sub { color: var(--dsw-alias-label-secondary); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/*
Stats cell tooltip (cost cell): styled DOM bubble because native `title` can't hold the multi-row price breakdown; a '?' signals the affordance and hovering anywhere on the cell reveals the bubble below (the cell is the tip's position frame). Chrome rides the shared `.lc-tip`; the reveal and the badge's brighten ride `group-hover/tip` variants (`group/tip` on the cell).
*/
.lc-stat-tipped { position: relative; }
.lc-stat-q { display: inline-block; margin-left: 4px; width: 11px; height: 11px; line-height: 11px; border-radius: 50%; text-align: center; font-size: 9px; font-weight: 700; font-style: normal; color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l2); cursor: help; transition: color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out), border-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-stat-tip { top: calc(100% + 6px); left: 0; max-width: 280px; line-height: 1.5; font-weight: 400; }
.lc-stat-tip-prices { display: block; margin-top: 5px; border-top: 1px dashed var(--dsw-alias-border-l1); padding-top: 5px; }
.lc-stat-tip-head { display: block; font-weight: 600; }
.lc-stat-tip-row { display: block; margin-top: 2px; }
.lc-stat-tip-model { color: var(--dsw-alias-label-primary); }
.lc-card-sub { font-weight: 400; color: var(--dsw-alias-label-secondary); font-size: 12px; margin-left: auto; }
.lc-overview-num { display: flex; align-items: baseline; gap: 6px; margin-bottom: 8px; }
.lc-overview-num > b { font-size: 20px; }
.lc-overview-num span { color: var(--dsw-alias-label-secondary); }
.lc-overview-pct { margin-left: auto; font-size: 11px; }
.lc-overview-pct b { font-size: 20px; color: var(--dsw-alias-label-primary); margin-right: 4px; }
/*
---- Stats cards' shared anatomy: a small section heading, the donut+list
row, and the token card's cost line (the '.lc-tip' bubble pattern). ----
*/
.lc-stat-head { font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-secondary); margin: 12px 0 8px; }
/* The ring hugs the left edge; the legend fills the rest of the card with its quantity and share columns right-aligned. The donut cards stretch the group over their full height, so the head row's extra space sits around the chart instead of pooling under it. */
.lc-donut-row { display: flex; align-items: center; justify-content: flex-start; gap: 12px; min-width: 0; }
.lc-col-donut { display: flex; flex-direction: column; }
.lc-col-donut .lc-donut-row { flex: 1; }
.lc-donut-row .lc-sl { flex: 1 1 auto; }
.lc-donut { position: relative; flex: none; }
.lc-donut svg { display: block; }
.lc-donut-track { stroke: color-mix(in srgb, var(--color-neutral-500) 18%, transparent); }
/* The legend row ↔ segment hover link: hovering either side dims the ring's
other slices to 0.35 (the stacked bars' idiom) while the active one stays
solid; the center figures stay at rest. */
.lc-donut-seg { transition: opacity var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
/*
Entrance sweep: each slice grows its dasharc from 0 100 up to the attribute value (the 100% keyframe is
omitted on purpose — CSS takes the element's own stroke-dasharray, so no per-slice value is duplicated
here), staggered by the slice index so the ring builds clockwise from 12 o'clock; `backwards` keeps a
slice hidden until its turn. Runs once on mount — hover-driven re-renders reuse the nodes and never
restart it.
*/
.lc-donut-seg { animation: lc-donut-in 0.45s cubic-bezier(0.4, 0, 0.2, 1) backwards; animation-delay: calc(var(--lc-i, 0) * 50ms); }
@keyframes lc-donut-in { from { stroke-dasharray: 0 100; } }
.lc-donut-dim .lc-donut-seg { opacity: 0.35; }
.lc-donut-dim .lc-donut-seg-on { opacity: 1; }
.lc-donut-center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px; pointer-events: none; text-align: center; padding: 0 7px; box-sizing: border-box; }
.lc-donut-center b { line-height: 1.15; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.lc-donut-center span { color: var(--dsw-alias-label-secondary); white-space: nowrap; }
/*
The slice rows — the donut's legend with numbers, no bars, two tiers per
row: the primary line pairs the color dot + name with the bold share
closing a fixed right column, and the secondary line drops the absolute
quantity (plus a qualifier like the call count) under the name in muted
small print. The name takes the whole primary line and only ellipsizes as
a last resort; a zero-figure row dims — the ring already carries the share.
The rows flow into as many ≥160px columns as the card offers (the
composition legend's own auto-fill idiom): one in a narrow pane, two on a
classic 1280px viewport where the row halves are ~456px, more on ultrawide
— so the classic size reads in the same paired order as the composition
card below.
*/
.lc-sl { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 4px 20px; min-width: 0; overflow: hidden; }
/* The pill tint spreads 6px past the text column (negative margins) so the
hovered row reads as one lit surface without shifting the columns. */
.lc-sl-row { display: flex; flex-direction: column; gap: 1px; font-size: 12px; min-width: 0; border-radius: 6px; padding: 1px 6px; margin: 0 -6px; transition: background-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-sl-row-on { background: var(--dsw-alias-interactive-bg-hover); }
.lc-sl-row-dim { opacity: 0.55; }
.lc-sl-main { display: flex; align-items: center; gap: 6px; min-width: 0; }
.lc-sl-dot { flex: none; width: 8px; height: 8px; border-radius: 2px; }
.lc-sl-label { flex: 1 1 auto; min-width: 0; line-height: 1.25; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lc-sl-pct { flex: none; min-width: 34px; text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; }
.lc-sl-sub { margin-left: 14px; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 1.3; font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lc-pi-grid { display: grid; grid-template-columns: 1fr; gap: 8px; }
/* The settings-entry row is a button wearing the link-row chrome — reset the button chrome the shared row style doesn't cover. */
.lc-pi-row-btn { appearance: none; border: 0; background: none; font: inherit; color: inherit; text-align: left; padding: 0; cursor: pointer; width: 100%; }
.lc-pi-row { display: flex; flex-direction: row; justify-content: space-between; align-items: baseline; gap: 12px; min-width: 0; text-decoration: none; color: inherit; }
.lc-pi-hint { text-decoration: none; }
.lc-pi-update { color: var(--dsw-alias-state-warn-primary); font-size: 11px; margin-left: 6px; white-space: nowrap; }
.lc-pi-label { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/*
min-width: 0 lets the value shrink inside the flex row and truncate with
an ellipsis; without it a narrow card pushes the value over the label.
*/
.lc-pi-value { min-width: 0; color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: right; }
.lc-pi-row-btn .lc-pi-value { font-weight: 400; }
/*
Wide head cards: on roomy panes the context card's six cells fit ONE line
(~110px each at the 700px mark). Degrades silently on engines without
container queries.
*/
@container lc-card (min-width: 700px) {
.lc-stats { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
/*
Narrow head cards: the side-by-side ring + legend reads single-line only
above ~320px of card content width. The two-tier rows already absorb the
squeeze — the name gives ground before the fixed share column — so the
compact block only tightens the ring gap. The context card's 6-cell grid
folds to 2 columns instead: three ~65px cells would ellipsize the longer
English labels (Tool Calls). Below 240px even the ring stops
fitting, so the card stacks: the ring centers over the full-width legend
rows. Cards resolve these queries against themselves (the shared `lc-card`
container in base.css) at every split of the head band — two-across or
stacked. Engines without container queries drop both blocks and keep
the wide layout. The 240px block must stay AFTER the 320px one — at that
width both match and the stack rules win by order.
*/
@container lc-card (max-width: 320px) {
.lc-donut-row { gap: 8px; }
.lc-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@container lc-card (max-width: 240px) {
.lc-donut-row { flex-wrap: wrap; }
.lc-donut { margin: 0 auto; }
.lc-donut-row .lc-sl { flex-basis: 100%; }
}
/* Reduced motion (the harness's own idiom): the ring shows its final arcs immediately. */
@media (prefers-reduced-motion: reduce) {
.lc-donut-seg { animation: none; }
}