blob: 4926f31dad529cd68f58cd13e13bbfd4e96323fc [file]
/* Settings row primitives — the single style home for the primitives
rendered by settings/settings-rows.tsx (.settingsRows / .settingsRow)
and the form-row family that shares the same grouped-card rhythm
(.settingsFormRow / .settingsField / .settingsFormGrid).
PR-SETTINGS-ROWS-CONVERGENCE-0: these rules were previously scattered
across bot.css (base layout), connection.css (hover + hint),
provider-editor.css (label/value typography), and nav-sidebar.css
(form rows + width variants), which let the two row kinds drift:
.settingsFormRow titles rendered 15px while .settingsRow titles on
the SAME page (通用: 默认模型 vs 隐身模式) rendered 13px, with
different padding. One file, one typography contract:
- row title (strong): --font-size-heading / medium
- field label (span): --font-size-ui / medium
- hint (small): --font-size-base, muted
- value (.settingsRow span): --font-size-ui, secondary */
/* PR-SETTINGS-GROUPED-CARD-0 (WAWQAQ msg `1abecd66`): grouped card —
outer card + hairline dividers, no per-row chrome. The ONLY card
primitive on settings pages (see .settingsStructuredPage note in
nav-sidebar.css). */
.settingsRows {
display: grid;
gap: 0;
border: var(--border-width-hairline) solid oklch(from var(--foreground) l c h / 0.08);
overflow: hidden;
/* #1362: the card is the query container for the row-stacking rules at the
bottom of this file. Rows respond to the width the card actually has —
the settings content column is narrower than the window by the nav
sidebar, so viewport @media blocks systematically fire too late/early
(the #1361 540px and #1364 900px lessons). Same mechanism as the
permission dialog's inline-size container in permission-dialog.css. */
container: settings-rows / inline-size;
}
/* Label/value row: title + hint stack on the left, value or a single
control on the right column.
THE LABEL IS THE CONTENT; the right column is the accessory. So the
right track is sized to what it actually holds and the label takes
everything else — `fit-content(45%)`, not a proportional share.
This used to read `minmax(150px, 0.36fr) minmax(0, 1fr)`, which is
backwards: it handed the accessory 1fr and rationed the content to
0.36fr. Nothing in this codebase ever fills that 1fr — every right
column is a switch, a ~94px time field, a capped select, or (28 rows
across 5 pages, via SettingRow) a short read-only string. Measured on
the 数据 and 开放网关 pages at a ~1076px row: the value used ~64px of
a ~791px track while the label was pinned to ~285px and wrapped its
hint to four lines — mid-word in Chinese, which has no spaces to break
at — with ~730px sitting blank between them.
#1524 already made this trade for switches and the compact bucket, but
it enumerated control types instead of stating the rule, so read-only
values — the most common right column in the app — kept the backwards
default. Stating it as the default is what makes it hold for the next
row someone adds.
`fit-content(45%)` rather than `auto` because the cap is load-bearing:
the workspace path on the 数据 page is a ~690px unbroken string, and an
`auto` track sizes to max-content, which would starve the label exactly
the way the old default did — just in the other direction. 45% matches
the share the select bucket below already caps at. A value longer than
the cap wraps, which is the documented intent for mono paths (see the
`[data-mono]` rule further down).
Controls that genuinely fill their track opt out: see the select rule
below, whose `w-full` trigger collapses under an intrinsic track. */
.settingsRow {
display: grid;
grid-template-columns: minmax(0, 1fr) fit-content(45%);
align-items: center;
gap: var(--space-4);
/* Same padding as .settingsFormRow below so the two row kinds read
as one rhythm when stacked on a page (was 16/20 vs 20/24). */
padding: var(--space-5) var(--space-6);
border: 0;
border-bottom: var(--border-width-hairline) solid oklch(from var(--foreground) l c h / 0.06);
border-radius: 0;
background: transparent;
transition: background var(--duration-base) var(--ease-out-strong);
}
/* Detail audit + alignment-governance round: bare Switch controls sat at
the value column's START (x≈mid-page) while inputs/selects hug the right
rail. The original fix tag-qualified the selector with a button prefix, but
Base UI renders the Switch root as a SPAN — the selector matched nothing
and rotted silently. Tag-AGNOSTIC by rule: role is the contract, the
rendered tag never is. */
.settingsRow > [role="switch"] {
justify-self: end;
}
/* #1524's switch and `[data-control-width="compact"]` track splits lived
here. Both said `minmax(0, 1fr) auto` — which is what the default above
now says, only stated as the rule rather than as two exceptions to it.
A 32px switch and a ~94px time field both sit well under the 45% cap,
so they size identically and the rules were pure duplication. The
`compact` attribute stays: it still carries the control-width and
stacking rules further down.
Selects can't take the intrinsic track above: their trigger is `w-full`
under a 320px cap, so an `auto` track collapses it to the width of
whatever option happens to be selected (measured: 320px → 118px).
They get an explicit cap instead, and it has to be BOTH absolute and
proportional. A flat `minmax(0, 320px)` holds 320px all the way down
and eats the label instead — at a 468px card (just above the 460px
stacking breakpoint) that leaves the label 82px. Capping by share as
well keeps the split sane at every width: measured 353/302 at a 720px
card, 214/188 at 468px. */
.settingsRow[data-control-width="select"] {
grid-template-columns: minmax(0, 1fr) minmax(0, min(320px, 45%));
}
.settingsRow:last-child {
border-bottom: 0;
}
.settingsRow:hover {
background: var(--state-hover-bg);
}
.settingsRow > div {
min-width: 0;
}
/* PR-SETTINGS-ROW-SPACIOUS-0 (2026-06-23, WAWQAQ msg `f7e9d166`):
reference rows are MUCH more spacious than maka's. WAWQAQ's screenshot
of QoderWork's preferences card showed rows ~80–90px tall with
noticeable internal padding. Bumped: min-height 56→72, padding
16/20 → 20/24, gap inside row 16→20, label 14→15px, hint 12→13. */
.settingsFormRow {
min-height: 72px;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-5);
padding: var(--space-5) var(--space-6);
border: 0;
border-bottom: var(--border-width-hairline) solid oklch(from var(--foreground) l c h / 0.06);
border-radius: 0;
background: transparent;
transition: background var(--duration-base) var(--ease-out-strong);
}
/* #1362: the label stack is a flex item; without min-width:0 its intrinsic
min-content (the widest unbreakable run) sets the row's minimum and the
card clips at narrow widths instead of letting the text wrap. */
.settingsFormRow > div:first-child {
min-width: 0;
}
/* #1363 review: a row whose control side is more than a bare switch (e.g.
Memory's status Chip + switch) groups them into one cluster, so the
stacked layout keeps them on one line together. The internal gap matches
the row gap, so the wide layout is pixel-identical to the former loose
siblings. */
.settingsFormRowControlCluster {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-5);
/* No min-width:0 here — the cluster's floor is its widest nowrap chip;
shrinking below that bleeds the chip out of the box. The LABEL side
owns the squeeze (it has min-width:0 and wraps). */
}
/* Detail audit round 3: vertical variant for full-width controls (the
personalization tone textarea) living inside the shared card rows. */
.settingsFormRow[data-orient="vertical"] {
flex-direction: column;
align-items: stretch;
gap: var(--space-3);
}
.settingsFormRow[data-orient="vertical"] .settingsActionRow {
margin: 0;
}
.settingsFormRow:last-child {
border-bottom: 0;
}
.settingsFormRow:hover {
background: var(--state-hover-bg);
}
/* === typography ========================================================= */
/* Row titles — one tier for BOTH row kinds. No `white-space: nowrap` here:
the label column has a fixed 150px minimum, so a nowrap title longer than
the column bled into the value column instead of wrapping (#1362; the
nowrap never affected titles that fit). */
.settingsRow strong,
.settingsFormRow strong {
display: block;
color: var(--foreground);
font-size: var(--font-size-heading);
font-weight: var(--font-weight-medium);
line-height: var(--leading-snug);
/* Last resort for unbreakable runs (product names like "Claude", env-var
tokens) on a squeezed label column: break inside the word rather than
bleed past the card. No effect while the title fits. */
overflow-wrap: anywhere;
}
/* Field labels — one tier BELOW row titles so the proxy grid's 代理协议 /
服务器地址 / 端口 labels don't compete with the 代理服务器 row title
(they used to share the 15px heading tier). */
.settingsField span,
.settingsFormGrid label span {
display: block;
color: var(--foreground);
font-size: var(--font-size-ui);
font-weight: var(--font-weight-medium);
line-height: var(--leading-snug);
}
/* Hints — the most-read body text on the page; body tier (13). */
.settingsRow small,
.settingsFormRow small,
.settingsField small {
display: block;
margin-top: var(--space-1-5);
color: var(--muted-foreground);
font-size: var(--font-size-base);
line-height: var(--leading-normal);
/* Hints carry paths and URLs — same last-resort break as the titles. */
overflow-wrap: anywhere;
}
.settingsRow > span {
/* PR-SETTINGS-MOTION-TYPO-TOKENS-0 (round 15/15): `tabular-nums` so
numeric values (version strings, model IDs, counts) align cleanly
when stacked vertically. baseline-ui + /typeset both require this
for any numeric data. */
min-width: 0;
color: var(--foreground-secondary);
font-size: var(--font-size-ui);
font-variant-numeric: tabular-nums;
overflow-wrap: anywhere;
text-align: right;
}
/* Long machine values (workspace path): right-aligned proportional text
wraps into a ragged multi-line block. Mono + left-aligned + caption
size reads as the file path it is. */
.settingsRow > span[data-mono="true"] {
font-family: var(--font-mono);
font-size: var(--font-size-caption);
text-align: left;
line-height: var(--leading-normal);
word-break: break-all;
}
.settingsFieldWarning {
display: block;
margin-top: var(--space-1);
color: var(--warning-text, var(--info-text));
}
/* === control-width variants ============================================ */
/* PR daily-review-bundled (@kenji audit finding #5): semantic width
bucket on the row, not a class selector keyed off the input
className. Different settings rows ask for different control
widths (time-pickers want compact, free-text selects want a 320px
cap); keying off the row makes the contract local and obvious. */
.settingsRow[data-control-width="compact"] > input,
.settingsRow[data-control-width="compact"] > .settingsTimeInput {
/* `width: auto` over the Input primitive's `w-full`: a 140px flat cap
stretched the box to 140px while `08:00` + the picker glyph only ink
~80px, leaving a ~60px dead strip inside the field's right edge. The
field now hugs its content and stays pinned to the same right rail as
the switch rows above it; 140px remains the ceiling for the wider
compact controls that share this bucket. */
width: auto;
max-width: 140px;
justify-self: end;
}
.settingsRow[data-control-width="select"] > .settingsSelectTrigger {
justify-self: end;
}
/* === fields and grids =================================================== */
.settingsFormGrid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-3);
}
/* #1362: non-row children of a rows card — the proxy sub-form's grids,
bypass field, info alert, and action row — sat flush against the card
edge while every sibling row is inset 24px. One horizontal inset for
everything inside a card. */
.settingsRows > .settingsFormGrid,
.settingsRows > .settingsField,
.settingsRows > .settingsActionRow,
.settingsRows > [data-slot="alert"] {
margin: var(--space-4) var(--space-6);
}
/* The Alert primitive carries `w-full`; with horizontal margins that is
wider than the card. The stretched grid-item width (auto) is the right
one here. */
.settingsRows > [data-slot="alert"] {
width: auto;
}
.settingsRows > .settingsFormGrid + .settingsFormRow {
border-top: var(--border-width-hairline) solid oklch(from var(--foreground) l c h / 0.06);
}
.settingsFormGridProxy {
grid-template-columns: minmax(150px, 0.8fr) minmax(0, 1.8fr) minmax(84px, 0.5fr);
}
.settingsField,
.settingsFormGrid label {
display: grid;
gap: var(--space-1);
}
/* PR-S2 (2026-06-23): reference-style horizontal Settings row — label
stack on the left (title + description), control aligned right. Use
`<div className="settingsField" data-orient="horizontal">` on rows
whose control is a single inline element (Input, Switch, Segmented,
Select). Leave the default vertical stack for full-width controls
(Textarea, multi-line forms). The 1px hairline divider between rows
pulls the look closer to reference's flat divider list and away from
the previous "each row in its own implicit card" feel. */
.settingsField[data-orient="horizontal"] {
grid-template-columns: minmax(0, 1fr) auto;
grid-template-areas:
"label control"
"hint control";
column-gap: var(--space-4);
row-gap: var(--space-0-5);
align-items: center;
padding: var(--space-3) 0;
border-bottom: var(--border-width-hairline) solid var(--border);
}
.settingsField[data-orient="horizontal"] > span {
grid-area: label;
align-self: end;
}
.settingsField[data-orient="horizontal"] > small {
grid-area: hint;
margin-top: 0;
align-self: start;
}
.settingsField[data-orient="horizontal"] > :not(span):not(small) {
grid-area: control;
justify-self: end;
align-self: center;
min-width: 0;
}
/* Allow inputs to take a comfortable cap width on horizontal rows so
the description column has space to breathe.
PR-SETTINGS-INPUT-POLISH-0: cap from 280 → 320 since rows are now
roomier (24px L/R padding). */
.settingsField[data-orient="horizontal"] > input,
.settingsField[data-orient="horizontal"] > .settingsSelectTrigger {
max-width: 320px;
}
/* The horizontal row above the first one shouldn't carry a top border;
below the last one the section footer takes over. The default
border-bottom is fine for the inter-row hairlines. */
.settingsField[data-orient="horizontal"] + .settingsField[data-orient="horizontal"] {
border-top: 0;
}
/* === focus and motion =================================================== */
/* PR-SETTINGS-A11Y-POLISH-0 (round 11/15) — keyboard-only focus ring
on settings rows + inputs. Reference's settings cards use a 2px
ring on focus-visible; maka had no visible affordance. Use the
accent color with 2px outline + 2px offset so it doesn't blend
with the row's hairline divider. */
.settingsFormRow:focus-visible,
.settingsRow:focus-visible {
outline: var(--focus-ring-width) solid var(--focus-ring);
outline-offset: var(--focus-ring-offset);
background: oklch(from var(--focus-ring) l c h / 0.04);
}
.settingsFormGrid select:focus-visible {
outline: none;
border-color: var(--focus-ring);
box-shadow: 0 0 0 var(--focus-ring-width) oklch(from var(--focus-ring) l c h / 0.18);
}
/* PR-SETTINGS-A11Y-POLISH-0 — respect prefers-reduced-motion. The
interaction still fires, just instantly.
Justified: a11y reduced-motion override; the only `!important` in this
file, tracked in renderer-important-audit-contract.test.ts. */
@media (prefers-reduced-motion: reduce) {
.settingsFormRow,
.settingsRow,
.settingsField input,
.settingsField textarea {
transition: none !important;
}
}
/* === narrow-card row stacking (#1362) =================================== */
/* THE row-wrapping decision for every form-row page (#1303 tracking):
below 460px of CARD width — not viewport width — label/control rows
stack vertically. 460px is the app's one container breakpoint (shared
with permission-dialog.css) and sits just above the widest one-line
row minimum (150px label floor + gap + a ~180px intrinsic-width input
+ 48px row padding ≈ 394px), with margin for the select triggers'
320px cap to stay usable. Switch rows are the exception: a switch is
~40px wide and always fits beside its label — stacking it reads as a
detached orphan, so those rows keep two columns and let the label wrap. */
@container settings-rows (max-width: 460px) {
.settingsRow {
grid-template-columns: minmax(0, 1fr);
row-gap: var(--space-2-5);
}
/* Repeat the control-width variants so the narrow track has enough
specificity to beat their wide declarations above. */
.settingsRow[data-control-width="compact"],
.settingsRow[data-control-width="select"] {
grid-template-columns: minmax(0, 1fr);
}
/* #1363 review: the exception applies only when the switch is the row's
ONLY control (label + switch, nothing else). A three-part row — e.g.
Memory's label + status Chip + switch — held horizontal would crush
the label to a char-per-line sliver, so it stacks like any other row.
`:not(input)` spares Base UI Switch's hidden sibling checkbox, which
is a real third child on every switch row. */
.settingsRow:has(> [role="switch"]):not(:has(> :nth-child(3):not(input))) {
grid-template-columns: minmax(0, 1fr) auto;
}
/* #1524 gave the compact and select buckets their own full-width track
splits at `.settingsRow[data-control-width="…"]` — specificity 0,2,0.
A container query adds NO specificity to the rules inside it, so those
two outrank the plain `.settingsRow` stacking rule above (0,1,0) and
those rows kept two columns all the way down. Restate them here at
matching specificity so they stack like every other row.
The `justify-self: start` rules just below are the tell that stacking
was always the intent for these buckets: start-aligning a control only
makes sense once it sits under its label rather than beside it. */
.settingsRow[data-control-width="compact"],
.settingsRow[data-control-width="select"] {
grid-template-columns: minmax(0, 1fr);
}
/* Values and controls sit under their label now; right-alignment against
the card edge would detach them from it. */
.settingsRow > span {
text-align: left;
}
.settingsRow[data-control-width="compact"] > input,
.settingsRow[data-control-width="compact"] > .settingsTimeInput,
.settingsRow[data-control-width="select"] > .settingsSelectTrigger {
justify-self: start;
}
.settingsFormRow {
flex-direction: column;
align-items: stretch;
gap: var(--space-2-5);
}
.settingsFormRow:has(> [role="switch"]):not(:has(> :nth-child(3):not(input))) {
flex-direction: row;
align-items: center;
gap: var(--space-5);
}
.settingsFormGrid,
.settingsFormGridProxy {
grid-template-columns: minmax(0, 1fr);
}
/* The Button primitive is `shrink-0` with a fixed height; on a squeezed
card an action button wider than the row would bleed past the card
edge. Let it shrink and let the label wrap, keeping the primitive's
32px as a floor. */
.settingsActionRow > [data-slot="button"] {
flex-shrink: 1;
height: auto;
min-height: 2rem;
}
}