refactor(client): fold the responsive layout into Tailwind variants and intrinsic flex
All seven hand-written @container blocks move out of the sheets: the head-band stack, the stats grid's wide/narrow columns, the donut row's gap fold and stack, and the events/file rows' narrow-card wraps now ride @min/@max-[…]/lc-card utilities on the elements (named lc-card / lc-head-row containers). Reviewing that migration against the shared column idiom folded the head band's 720px container query away entirely — the rows were already flex-wrap rows, so the head cards take the same `flex-1 min-w-[min(360px,100%)]` floor as every other column and wrap intrinsically. The count grid became repeat(auto-fit, minmax(108px,1fr)) — 108px is the floor where the longest English label fits — replacing the 700px/320px breakpoints and filling the card at every width instead of jumping 3 to 6 columns. What stays hand-written: the 240px donut stack and 380px row wraps (the ring/legend and the 92px/46px reservations are measured-content facts, and Tailwind's preset container scale is rem-based where this plugin is px-anchored), plus the lc-card container definition whose .lc-root scope deliberately keeps the /context modal's cards container-less. Verified in the browser across 1280/900/736/640/390 in both locales and in the /context modal: same folds at the same widths, no overflow, no horizontal scrollbars. The visible delta is the count grid filling intermediate widths with 4/5 columns.
This commit is contained in:
1 parent
17183882be
commit
ff10cff71c
13 files changed
+60
-110
No files matched your search
@@ -509,8 +509,8 @@ export function makeContextView(
|
||||
{/* The head band splits into two rows: the session's shape beside the
|
||||
plugin card, then the two donut cards together. The sidebar panel
|
||||
drops the first row (context stats / plugin info pay off only on
|
||||
the full-width tab) and keeps the donut row, whose container query
|
||||
stacks the pair in the narrow pane. */}
|
||||
the full-width tab); the rows' own flex-wrap stacks the pair in a
|
||||
narrow pane at the shared 360px card floor. */}
|
||||
{inSidebar ? null : (
|
||||
<div className="lc-cols lc-head">
|
||||
<StatsContext counts={counts} humanInputs={data.humanInputs} toolCalls={data.toolCalls} usage={usage}
|
||||
@@ -525,18 +525,19 @@ export function makeContextView(
|
||||
|
||||
{/* One arrangement for every host: composition over trend in the left
|
||||
column, the browser beside them and stretched to the pair's height.
|
||||
The sidebar panel's column simply folds to one at the shared 360px
|
||||
floor instead of splitting the three cards across two ragged rows. */}
|
||||
All columns share the 360px floor (`min-w-[min(360px,100%)]`): the
|
||||
rows wrap at it, and a sub-360px pane narrows the column instead
|
||||
of overflowing. */}
|
||||
<div className="lc-cols lc-cols-main">
|
||||
<div className="lc-col">{compositionCard}{trendCard}</div>
|
||||
<div className="lc-col flex-1 min-w-[min(360px,100%)]">{compositionCard}{trendCard}</div>
|
||||
{/* `lc-col-browser` stretches the browser card to the left column's
|
||||
height; the /context modal, which draws its own stack, must stay
|
||||
content-sized. */}
|
||||
<div className="lc-col lc-col-browser">{browserCard}</div>
|
||||
<div className="lc-col lc-col-browser flex-1 min-w-[min(360px,100%)]">{browserCard}</div>
|
||||
</div>
|
||||
|
||||
<div className="lc-cols">
|
||||
<div className="lc-card lc-col">
|
||||
<div className="lc-card lc-col flex-1 min-w-[min(360px,100%)]">
|
||||
<div className="lc-card-title">
|
||||
<span className="lc-card-title-text">{t('events.title')}</span>
|
||||
<div className="lc-kinds">
|
||||
|
||||
@@ -80,7 +80,7 @@ export function makeDonut(kit: ViewKit): (props: DonutProps) => ReactElement {
|
||||
&& arcs.some(a => a.key === props.hoverKey)
|
||||
return (
|
||||
<div
|
||||
className={'lc-donut' + (hovering ? ' lc-donut-dim' : '')}
|
||||
className={'lc-donut @max-[240px]/lc-card:mx-auto' + (hovering ? ' lc-donut-dim' : '')}
|
||||
style={{ width: size, height: size }}
|
||||
onMouseLeave={() => { if (props.onHoverKey !== undefined) props.onHoverKey(null) }}
|
||||
>
|
||||
|
||||
@@ -119,11 +119,15 @@ export function makeEventList(kit: ViewKit): (props: EventListProps) => ReactEle
|
||||
: EVENT_ICONS[ev.kind] || '•'
|
||||
// Key on the durable seq alone: the list renders newest-first, so a fresh event lands at index 0
|
||||
// and an index-bearing key would shift EVERY existing row's key — a full-list remount on every push.
|
||||
// Narrow cards (phone panes): the single-line row would crush the label and push the meta
|
||||
// tail into a scrollbar, so the row wraps — the label's near-full-width basis keeps line 1
|
||||
// to icon + kind + label, and the meta tail folds onto line 2. The 92px basis reservation
|
||||
// covers icon + gap + the widest kind chip (both locales) + gap.
|
||||
return (
|
||||
<div key={ev.seq} className="lc-event">
|
||||
<div key={ev.seq} className="lc-event @max-[380px]/lc-card:flex-wrap">
|
||||
<span className={'lc-event-icon lc-event-' + ev.kind}>{glyph}</span>
|
||||
<span className={'lc-kind lc-kind-' + ev.kind}>{t('kind.' + ev.kind)}</span>
|
||||
<span className="lc-event-label">{label}</span>
|
||||
<span className="lc-event-label flex-1 @max-[380px]/lc-card:basis-[calc(100%-92px)]">{label}</span>
|
||||
{at !== null ? <span className="lc-event-at">{at}</span> : null}
|
||||
{ev.tokens ? (
|
||||
<span className={'lc-event-tokens' + (ev.kind === 'inject' ? ' lc-up' : ' lc-down')}>
|
||||
|
||||
@@ -156,7 +156,8 @@ export function makeFileCard(kit: ViewKit, settings: ContextSettings): Component
|
||||
) : (
|
||||
<div>
|
||||
<div className="lc-fa-ctl">
|
||||
<div className="lc-gran">
|
||||
{/* The five purpose chips (labels + counts) overflow a ~300px card in English — flow the group to two lines. */}
|
||||
<div className="lc-gran @max-[380px]/lc-card:flex-wrap">
|
||||
{chips.map(c => (
|
||||
<button
|
||||
key={c.key}
|
||||
@@ -225,7 +226,7 @@ export function makeFileCard(kit: ViewKit, settings: ContextSettings): Component
|
||||
<div key={e.path} className={'lc-fa-item' + (open ? ' lc-fa-item-on' : '')}>
|
||||
<button
|
||||
type="button"
|
||||
className="lc-fa-row hover:bg-(--dsw-alias-interactive-bg-hover)"
|
||||
className="lc-fa-row hover:bg-(--dsw-alias-interactive-bg-hover) @max-[380px]/lc-card:flex-wrap"
|
||||
title={e.path}
|
||||
onClick={() => { setOpenPath(open ? null : e.path) }}
|
||||
>
|
||||
@@ -239,7 +240,10 @@ export function makeFileCard(kit: ViewKit, settings: ContextSettings): Component
|
||||
)
|
||||
: glyph.glyph}
|
||||
</span>
|
||||
<span className="lc-fa-path">
|
||||
{/* Narrow cards: wrap instead of crushing — the path's near-full-width basis keeps
|
||||
line 1 to chevron + icon + path, badges/delta/time fold onto line 2. The 46px
|
||||
reservation is chevron (12) + gaps (2×7) + form icon (20). */}
|
||||
<span className="lc-fa-path flex-1 @max-[380px]/lc-card:basis-[calc(100%-46px)]">
|
||||
{dir !== '' ? <em>{dir}</em> : null}
|
||||
{openable
|
||||
? (
|
||||
|
||||
@@ -34,7 +34,7 @@ export function makePluginInfo(kit: ViewKit): () => ReactElement {
|
||||
const nameValue: ReactNode[] = [nameText]
|
||||
if (update) nameValue.push(<span key="update" className="lc-pi-update">{'↑ v' + update}</span>)
|
||||
return (
|
||||
<div className="lc-card">
|
||||
<div className="lc-card flex-1 min-w-[min(360px,100%)]">
|
||||
<div className="lc-card-title">
|
||||
<span className="lc-card-title-text">{t('plugin.title')}</span>
|
||||
{/* The tagline doubles as the repo link: hover underlines it, a click opens GitHub. */}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function makeSliceList(kit: ViewKit): (props: {
|
||||
onHoverKey?: (key: string | null) => void
|
||||
}): ReactElement {
|
||||
return (
|
||||
<div className="lc-sl" onMouseLeave={() => { if (props.onHoverKey !== undefined) props.onHoverKey(null) }}>
|
||||
<div className="lc-sl flex-auto @max-[240px]/lc-card:basis-full" onMouseLeave={() => { if (props.onHoverKey !== undefined) props.onHoverKey(null) }}>
|
||||
{props.rows.map(r => (
|
||||
<div
|
||||
key={r.key}
|
||||
|
||||
@@ -171,11 +171,14 @@ export function makeStatsContext(kit: ViewKit): (props: {
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="lc-card lc-col-stats">
|
||||
<div className="lc-card lc-col-stats flex-1 min-w-[min(360px,100%)]">
|
||||
<div className="lc-card-title">
|
||||
<span className="lc-card-title-text">{t('stats.title')}</span>
|
||||
</div>
|
||||
<div className="lc-stats">
|
||||
{/* The count grid: auto-fit keeps every cell ≥108px (the floor where the longest
|
||||
English label still fits), so cells fill the card — 3 across at the default
|
||||
half-card, 6 across on a wide card, 2 on a phone-width one. */}
|
||||
<div className="lc-stats grid grid-cols-[repeat(auto-fit,minmax(108px,1fr))] gap-1.5">
|
||||
{cell(t('stats.turns'), props.counts.turns)}
|
||||
{cell(t('stats.steps'), props.counts.steps)}
|
||||
{cell(t('stats.humanInputs'), props.humanInputs ?? 0, t('stats.humanInputsTip'))}
|
||||
|
||||
@@ -148,14 +148,18 @@ export function makeStatsTiming(kit: ViewKit, Donut: (props: DonutProps) => Reac
|
||||
].filter(row => !row.dim)
|
||||
}
|
||||
return (
|
||||
<div className="lc-card lc-col-stats lc-col-donut">
|
||||
<div className="lc-card lc-col-stats lc-col-donut flex-1 min-w-[min(360px,100%)]">
|
||||
<div className="lc-card-title">
|
||||
<span className="lc-card-title-text">{t('timing.title')}</span>
|
||||
</div>
|
||||
{rows.length === 0
|
||||
? <div className="lc-empty">{t('timing.empty')}</div>
|
||||
: (
|
||||
<div className="lc-donut-row">
|
||||
<div
|
||||
className="lc-donut-row flex items-center justify-start gap-3 min-w-0 @max-[320px]/lc-card:gap-2 @max-[240px]/lc-card:flex-wrap"
|
||||
>
|
||||
{/* donut + legend row: the gap folds at a 320px card, below 240px the row wraps
|
||||
and the ring centers over the full-width legend (keyed to the lc-card container). */}
|
||||
<Donut
|
||||
segments={segments}
|
||||
size={96}
|
||||
|
||||
@@ -58,11 +58,13 @@ export function makeStatsTokens(kit: ViewKit, Donut: (props: DonutProps) => Reac
|
||||
count: p.key === 'output' ? `${fmt(p.value)} · ${t('tokens.outputNote')}` : '≈' + fmt(p.value),
|
||||
}))
|
||||
return (
|
||||
<div className="lc-card lc-col-stats lc-col-donut">
|
||||
<div className="lc-card lc-col-stats lc-col-donut flex-1 min-w-[min(360px,100%)]">
|
||||
<div className="lc-card-title">
|
||||
<span className="lc-card-title-text">{t('tokens.title')}</span>
|
||||
</div>
|
||||
<div className="lc-donut-row">
|
||||
{/* donut + legend row: the gap folds at a 320px card, below 240px the row wraps
|
||||
and the ring centers over the full-width legend (all keyed to the lc-card container). */}
|
||||
<div className="lc-donut-row flex items-center justify-start gap-3 min-w-0 @max-[320px]/lc-card:gap-2 @max-[240px]/lc-card:flex-wrap">
|
||||
<Donut
|
||||
segments={shown}
|
||||
size={96}
|
||||
|
||||
+12
-23
@@ -89,12 +89,6 @@
|
||||
The row owns the 14px vertical rhythm (lc-cols gap); cards drop their own bottom margin (incl. the last in a column) so every section boundary measures exactly one 14px whichever way the flex wraps.
|
||||
*/
|
||||
.lc-cols { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 14px; }
|
||||
/* Columns fold at 360px: the cards that pair up — the main row's composition/trend
|
||||
against the browser, and the events card against file activity — read cramped at a
|
||||
quarter-share of a narrow pane, so the main row needs 2 x 360px + the 7px gap to stay
|
||||
side by side. min() caps the floor at the row's own width: on a sub-360px pane the
|
||||
column narrows instead of overflowing. */
|
||||
.lc-col { flex: 1; min-width: min(360px, 100%); }
|
||||
.lc-cols > .lc-card { margin-bottom: 0; }
|
||||
.lc-col > .lc-card:last-child { margin-bottom: 0; }
|
||||
.lc-col-browser { display: flex; flex-direction: column; }
|
||||
@@ -102,24 +96,19 @@
|
||||
/*
|
||||
Head band: TWO rows of two EQUAL-width cards — the session's shape beside
|
||||
the plugin card (context / plugin), then the two donut cards together
|
||||
(token / timing). `flex-basis: 0` + equal grow halves each row exactly, and
|
||||
`min-width: 0` lets the split ignore content. Each row is its own
|
||||
`lc-head-row` query container, so the split folds instead of crushing the
|
||||
cards (issue #48): below 720px of row width (a half under ~350px — where the
|
||||
donut cards' ring+legend single-line and the context card's 3-column cell
|
||||
grid both start to cramp) the pair stacks single-column. Cards under that
|
||||
end of the band sit under their own lc-card reflows (stats.css: the ring
|
||||
stacks over its legend, the cell grid folds), so the two-card line is the
|
||||
trade this threshold buys. Engines without container queries keep the wide
|
||||
two-across layout. The right Sidebar's panel renders only the donut row (it
|
||||
drops the context stats / plugin info row — see contextView.tsx); the same
|
||||
rules then stack the pair in the narrow pane.
|
||||
(token / timing). The rows are `.lc-cols` flex-wrap rows and the cards ride
|
||||
the shared column idiom (`flex-1 min-w-[min(360px,100%)]` in the
|
||||
components): equal `flex: 1` halves each row exactly and the min-width
|
||||
floor is the fold — below 2 x 360px + gap of row width (a half under
|
||||
~360px, where the donut cards' ring+legend single-line and the count grid
|
||||
both start to cramp, issue #48) the pair wraps onto full-width lines. Cards
|
||||
under that end of the band sit under their own lc-card reflows (stats.css:
|
||||
the ring stacks over its legend, the cell grid folds), so the two-card
|
||||
line is the trade this floor buys. Engines without container queries keep
|
||||
the wide two-across layout. The right Sidebar's panel renders only the
|
||||
donut row (it drops the context stats / plugin info row — see
|
||||
contextView.tsx); the same wrap then stacks the pair in the narrow pane.
|
||||
*/
|
||||
.lc-head { container: lc-head-row / inline-size; }
|
||||
.lc-head > .lc-card { flex: 1 1 0; min-width: 0; }
|
||||
@container lc-head-row (max-width: 720px) {
|
||||
.lc-head > .lc-card { flex-basis: 100%; }
|
||||
}
|
||||
/* The context card's cell grid stretches to the plugin card's height, so the row reads as one level band. */
|
||||
.lc-head > .lc-card:has(.lc-stats) { display: flex; flex-direction: column; }
|
||||
.lc-head .lc-stats { flex: 1; align-content: stretch; }
|
||||
|
||||
@@ -18,15 +18,3 @@
|
||||
.lc-event-tokens { color: var(--dsw-alias-state-success-primary); font-weight: 600; white-space: nowrap; }
|
||||
.lc-event-tokens.lc-up { color: var(--dsw-alias-state-warn-primary); }
|
||||
.lc-event-time { color: var(--dsw-alias-label-secondary); font-size: 12px; }
|
||||
/*
|
||||
Narrow cards (phone panes): the single-line row crushes the label to a
|
||||
couple of characters and the no-shrink meta tail (at/tokens/time) overflows
|
||||
into a horizontal scrollbar. Wrap instead: the label's near-full-width
|
||||
basis keeps line 1 to icon + kind + label, and the meta tail folds onto
|
||||
line 2 intact. The 92px reservation covers icon + gap + the widest kind
|
||||
chip (both locales) + gap.
|
||||
*/
|
||||
@container lc-card (max-width: 380px) {
|
||||
.lc-event { flex-wrap: wrap; }
|
||||
.lc-event-label { flex-basis: calc(100% - 92px); }
|
||||
}
|
||||
@@ -22,22 +22,7 @@
|
||||
.lc-fa-lang { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 13px; padding: 0 2px; border-radius: 4px; font-size: 8px; font-weight: 700; line-height: 1; }
|
||||
/* The chevron sits close to the icon: only half the row gap stays visible. */
|
||||
.lc-fa-row .lc-br-chev { margin-right: -2px; }
|
||||
.lc-fa-path { flex: 1; min-width: 0; word-break: break-all; }
|
||||
/*
|
||||
Narrow cards (phone panes): the nowrap row's break-all path otherwise
|
||||
collapses into a one-character-wide vertical strip beside the chips. Wrap
|
||||
instead: the path's near-full-width basis keeps line 1 to chevron + icon +
|
||||
path (free to wrap to two text lines), and the badges/delta/time fold onto
|
||||
line 2 intact. The 46px reservation is chevron (12) + gaps (2×7) + form
|
||||
icon (20). Must sit AFTER the `.lc-fa-path` base rule — equal specificity,
|
||||
so only source order lets the query's basis beat the `flex: 1` shorthand.
|
||||
*/
|
||||
@container lc-card (max-width: 380px) {
|
||||
.lc-fa-row { flex-wrap: wrap; }
|
||||
.lc-fa-path { flex-basis: calc(100% - 46px); }
|
||||
/* The five purpose chips (labels + counts) overflow a ~300px card in English — let the segmented group flow to two lines. */
|
||||
.lc-fa-ctl .lc-gran { flex-wrap: wrap; }
|
||||
}
|
||||
.lc-fa-path { min-width: 0; word-break: break-all; }
|
||||
.lc-fa-file { cursor: pointer; }
|
||||
.lc-fa-path em { font-style: normal; color: var(--dsw-alias-label-secondary); }
|
||||
/* Per-purpose count badges: pill chips, one fixed tint per purpose (read = brand, write = success, search = warn). */
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
* 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; }
|
||||
@@ -36,11 +32,13 @@
|
||||
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; }
|
||||
/* 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 donut+legend row over their full height (`flex: 1` below), so
|
||||
the head row's extra space sits around the chart instead of pooling under it; the row's own flex layout
|
||||
and its 320px/240px folds ride utilities on the elements (statsTokens.tsx / statsTiming.tsx /
|
||||
sliceList.tsx / donut.tsx). */
|
||||
.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); }
|
||||
@@ -99,37 +97,9 @@
|
||||
*/
|
||||
.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%; }
|
||||
}
|
||||
/* The stats grid's cell count is intrinsic (`repeat(auto-fit, minmax(108px,1fr))` on the element, see
|
||||
statsContext.tsx); the donut row's 320px gap fold and 240px stack still ride
|
||||
`@max-[…]/lc-card` utilities — statsTokens.tsx / statsTiming.tsx / sliceList.tsx / donut.tsx. */
|
||||
/* Reduced motion (the harness's own idiom): the ring shows its final arcs immediately. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.lc-donut-seg { animation: none; }
|
||||
|
||||
Reference in new issue
Block a user