The chat's model picker shows name, context window (CTX 131K) and an eye for vision, on shared column tracks; the capability tags are gone from it. Settings -> Models gives the name its own row and wraps the tags beneath. The picker's tick now follows an in-place choice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
644 lines
22 KiB
CSS
644 lines
22 KiB
CSS
/* Settings and administration screens. */
|
|
|
|
/* --- Page scaffolding ------------------------------------------------------
|
|
One scroll container per screen, and this is the rule that decides which.
|
|
|
|
`.admin-scroll` is it on every admin page. `.tabs__body` is it only where the
|
|
tabs are a *bounded* flex child -- `.main > .tabs` on the settings page --
|
|
which is why the selector below says so rather than naming the class alone.
|
|
|
|
It used to name the class alone, and the result was two scrollers stacked on
|
|
/admin/prompts, where `.tabs` sits inside `.admin-scroll > .admin-page`, a
|
|
plain block. `flex: 1` and `min-height: 0` mean nothing there, so
|
|
`.tabs__body` had `height: auto` and never scrolled while still declaring
|
|
`overflow-y: auto` -- and everything written to reset "the scroller" reset
|
|
that one, silently, while the reader was lost in the other. Under
|
|
`.admin-scroll` the body is now an ordinary block and the page scrolls as one.
|
|
*/
|
|
/* What makes one of these scroll is `.scroll-region` in app.css, which both of
|
|
these selectors are listed in. Named there so the four declarations exist
|
|
once; named *here* is the reasoning above, which is about which element is
|
|
the scroller on which screen rather than about how a scroller behaves. */
|
|
|
|
.page,
|
|
.admin-page {
|
|
/* The same measure as the transcript, and the same token: a settings page and
|
|
a conversation are both prose, and having them differ by a rounding is the
|
|
kind of thing nobody reports and everybody notices. */
|
|
max-width: var(--thread-max-width);
|
|
margin: 0 auto;
|
|
padding: var(--sp-6) var(--sp-5) var(--sp-12);
|
|
}
|
|
|
|
/* A directory chosen rather than typed. The path is the only part allowed to
|
|
shrink, and it truncates -- an absolute path on somebody else's machine is
|
|
long enough to push the Clear button off the card otherwise. */
|
|
.dir-row { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
|
|
.dir-row > .btn { min-width: 0; }
|
|
.dir-row__path {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-xs);
|
|
}
|
|
|
|
.page-header { margin-bottom: var(--sp-6); }
|
|
.admin-lede,
|
|
.page-header__lede {
|
|
color: var(--ink-muted);
|
|
font-size: var(--text-sm);
|
|
line-height: var(--leading-relaxed);
|
|
margin: 0 0 var(--sp-6);
|
|
max-width: 44rem;
|
|
}
|
|
|
|
.admin-section-title,
|
|
.section-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
font-size: var(--text-lg);
|
|
margin: var(--sp-8) 0 var(--sp-4);
|
|
}
|
|
|
|
/* --- Tabs ------------------------------------------------------------------
|
|
Radio inputs plus sibling selectors: no JavaScript, and the browser keeps
|
|
the chosen tab across a re-render.
|
|
*/
|
|
.tabs { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|
|
|
|
.tabs__bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-1);
|
|
padding: 0 var(--sp-5);
|
|
border-bottom: var(--border-w) solid var(--border);
|
|
background: var(--bg);
|
|
flex: none;
|
|
overflow-x: auto;
|
|
scrollbar-width: none;
|
|
/* Sticky where the *page* is the scroller, which is the admin layout. The
|
|
Tools panel is longer than a screen, so without this the bar scrolls away
|
|
and changing tab means scrolling back up to find it. Harmless on the
|
|
settings page, where `.tabs__body` scrolls underneath a bar that never
|
|
moves anyway.
|
|
|
|
It needs the opaque `background` above it already has, or the panel would
|
|
show through. `z-index` because a panel's own cards establish stacking
|
|
contexts and would otherwise paint over it. */
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: var(--z-raised);
|
|
}
|
|
|
|
.tabs__tab {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
height: var(--control-h-lg);
|
|
padding: 0 var(--sp-4);
|
|
border-bottom: var(--border-w-thick) solid transparent;
|
|
color: var(--ink-muted);
|
|
font-size: var(--text-sm);
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
transition: color var(--transition-fast), border-color var(--transition-fast);
|
|
}
|
|
.tabs__tab:hover { color: var(--ink); }
|
|
/* The library's tabs are links between pages rather than radios, so the active
|
|
one is marked server-side. Same bar, same look. */
|
|
.tabs__tab.is-active { color: var(--ink); border-bottom-color: var(--accent); }
|
|
a.tabs__tab { text-decoration: none; }
|
|
|
|
.tabs__panel { display: none; }
|
|
|
|
/* The active tab's label. Each radio is immediately followed by its own label,
|
|
so this needs to know nothing about how many tabs there are or what they are
|
|
called. */
|
|
.tabs__bar input:checked + .tabs__tab {
|
|
color: var(--ink);
|
|
border-bottom-color: var(--accent);
|
|
}
|
|
|
|
/*
|
|
The active panel, matched by position.
|
|
|
|
CSS cannot compare a radio's id with a panel's data-tab, so this used to name
|
|
every tab twice -- and a tab added without also adding its two rules here
|
|
rendered a label that selected nothing. That is not a failure anyone spots in
|
|
review; it looks like a blank page.
|
|
|
|
Position is derivable, so it is used instead: the Nth radio shows the Nth
|
|
panel. Both lists are rendered in the same order, and a conditional tab drops
|
|
out of both at once, so they cannot drift apart. :nth-of-type counts by
|
|
element name, which is why the panels are <section> and the alerts above them
|
|
are <div> -- the alerts are not counted.
|
|
|
|
Enumerated to eight, comfortably more than exist. A ninth tab needs one line.
|
|
*/
|
|
.tabs__bar:has(input:nth-of-type(1):checked) ~ .tabs__body .tabs__panel:nth-of-type(1),
|
|
.tabs__bar:has(input:nth-of-type(2):checked) ~ .tabs__body .tabs__panel:nth-of-type(2),
|
|
.tabs__bar:has(input:nth-of-type(3):checked) ~ .tabs__body .tabs__panel:nth-of-type(3),
|
|
.tabs__bar:has(input:nth-of-type(4):checked) ~ .tabs__body .tabs__panel:nth-of-type(4),
|
|
.tabs__bar:has(input:nth-of-type(5):checked) ~ .tabs__body .tabs__panel:nth-of-type(5),
|
|
.tabs__bar:has(input:nth-of-type(6):checked) ~ .tabs__body .tabs__panel:nth-of-type(6),
|
|
.tabs__bar:has(input:nth-of-type(7):checked) ~ .tabs__body .tabs__panel:nth-of-type(7),
|
|
.tabs__bar:has(input:nth-of-type(8):checked) ~ .tabs__body .tabs__panel:nth-of-type(8) {
|
|
display: block;
|
|
}
|
|
.tabs__tab:has(:focus-visible) { outline: var(--outline-w) solid var(--accent); outline-offset: -2px; }
|
|
|
|
/*
|
|
The bar scrolls sideways when the tabs do not fit, and said nothing about it.
|
|
|
|
`scrollbar-width: none` is right -- a scrollbar under a row of tabs is ugly
|
|
and, on a touch device, invisible anyway -- but with nothing in its place the
|
|
overflow is undetectable. On a 390px phone the six tabs on /settings overflow
|
|
by about 190px, and the two that fall off the end are Memory and Security,
|
|
with Appearance only just reachable. Appearance is where both the Install and
|
|
the Notifications buttons live, so the effect was an install prompt nobody
|
|
could find on the device it exists for.
|
|
|
|
A fade at the edge that is only painted when there is something behind it:
|
|
`scroll-driven` would be nicer and is not universal, so this is two gradients
|
|
pinned to the scrollport with `background-attachment: local`, which is the old
|
|
trick and works everywhere -- the `local` layers scroll with the content and
|
|
cover the `scroll` ones exactly when there is nothing more to see.
|
|
*/
|
|
.tabs__bar {
|
|
background-image:
|
|
linear-gradient(to right, var(--bg) 40%, transparent),
|
|
linear-gradient(to left, var(--bg) 40%, transparent),
|
|
linear-gradient(to right, var(--scrim), transparent 1.5rem),
|
|
linear-gradient(to left, var(--scrim), transparent 1.5rem);
|
|
background-position: left center, right center, left center, right center;
|
|
background-repeat: no-repeat;
|
|
background-size: 1.5rem 100%;
|
|
background-attachment: local, local, scroll, scroll;
|
|
/* A tab is a destination, so a flick should land on one rather than between
|
|
two. */
|
|
scroll-snap-type: x proximity;
|
|
}
|
|
.tabs__tab { scroll-snap-align: start; }
|
|
|
|
/*
|
|
A form's action row, and the space after the form it closes.
|
|
|
|
Not a plain .btn-row: a settings form is a stack of cards, and Save has to
|
|
read as belonging to the cards above it rather than to whatever comes next.
|
|
The gap that matters is the one *after* the form -- a card following it (the
|
|
"Try it" panel on the search page, say) otherwise sits flush against Save and
|
|
looks like another field of the same form. That margin therefore belongs to
|
|
the form, not to the action row: the action row is always its form's last
|
|
child, so a bottom margin there would have nothing to push away from.
|
|
*/
|
|
.form-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
flex-wrap: wrap;
|
|
margin-top: var(--sp-5);
|
|
}
|
|
|
|
.admin-page > form { margin-bottom: var(--sp-8); }
|
|
.admin-page > form:last-child { margin-bottom: 0; }
|
|
|
|
/* --- Cards ----------------------------------------------------------------- */
|
|
.card {
|
|
background: var(--surface);
|
|
border: var(--border-w) solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--sp-5);
|
|
margin-bottom: var(--sp-4);
|
|
}
|
|
.card:last-child { margin-bottom: 0; }
|
|
.card__title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
font-size: var(--text-md);
|
|
margin-bottom: var(--sp-2);
|
|
}
|
|
.card__lede {
|
|
color: var(--ink-muted);
|
|
font-size: var(--text-sm);
|
|
margin-bottom: var(--sp-4);
|
|
line-height: var(--leading-relaxed);
|
|
}
|
|
.card__footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: var(--sp-3);
|
|
margin-top: var(--sp-5);
|
|
padding-top: var(--sp-4);
|
|
border-top: var(--border-w) solid var(--border);
|
|
flex-wrap: wrap;
|
|
}
|
|
.card__header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: var(--sp-3);
|
|
margin-bottom: var(--sp-4);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
/*
|
|
Two or three short fields on one line.
|
|
|
|
Numbers with their own units — width and height, steps and cfg and denoise —
|
|
read as a set and take a fraction of the width each, so stacking them makes a
|
|
card that is mostly whitespace and a form that is mostly scrolling. Auto-fit
|
|
rather than a fixed count: the same markup gives three columns on a wide
|
|
screen and one on a narrow one with no breakpoint, which matters because
|
|
`chat.css` has a test refusing media queries and this file should not drift
|
|
from that habit without a reason.
|
|
*/
|
|
.field-row {
|
|
display: grid;
|
|
/* Both halves of the pair -- see `.grid--2` in app.css. */
|
|
min-width: 0;
|
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr));
|
|
gap: var(--sp-3);
|
|
}
|
|
.field-row > .field { margin-bottom: var(--sp-4); }
|
|
|
|
/* Legacy aliases so existing admin templates keep their spacing. */
|
|
.form-grid { display: block; }
|
|
.connection__head { display: flex; align-items: center; justify-content: space-between;
|
|
gap: var(--sp-3); margin-bottom: var(--sp-4); flex-wrap: wrap; }
|
|
.connection__footer { display: flex; align-items: center; justify-content: space-between;
|
|
gap: var(--sp-3); margin-top: var(--sp-5); padding-top: var(--sp-4);
|
|
border-top: var(--border-w) solid var(--border); flex-wrap: wrap; }
|
|
.field--actions { margin-top: var(--sp-5); margin-bottom: 0; }
|
|
|
|
/* --- Definition lists ------------------------------------------------------ */
|
|
.detail-list {
|
|
display: grid;
|
|
grid-template-columns: minmax(6rem, auto) 1fr;
|
|
gap: var(--sp-2) var(--sp-4);
|
|
margin: 0;
|
|
font-size: var(--text-sm);
|
|
}
|
|
.detail-list dt { color: var(--ink-muted); font-weight: 500; }
|
|
.detail-list dd { margin: 0; }
|
|
|
|
/* --- Status ---------------------------------------------------------------- */
|
|
.status-dot {
|
|
width: 0.55rem;
|
|
height: 0.55rem;
|
|
border-radius: var(--radius-full);
|
|
flex: none;
|
|
background: var(--ink-faint);
|
|
}
|
|
.status-dot.is-ok { background: var(--success); }
|
|
.status-dot.is-bad { background: var(--danger); }
|
|
.status-dot.is-off { background: var(--ink-faint); }
|
|
|
|
/* --- Filter bar ------------------------------------------------------------ */
|
|
.filter-bar {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-3);
|
|
margin-bottom: var(--sp-4);
|
|
}
|
|
|
|
.filter-tabs {
|
|
display: flex;
|
|
gap: var(--sp-1);
|
|
flex-wrap: wrap;
|
|
border-bottom: var(--border-w) solid var(--border);
|
|
}
|
|
.filter-tab {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
height: var(--control-h);
|
|
padding: 0 var(--sp-3);
|
|
border-bottom: var(--border-w-thick) solid transparent;
|
|
color: var(--ink-muted);
|
|
font-size: var(--text-sm);
|
|
font-weight: 500;
|
|
text-decoration: none;
|
|
white-space: nowrap;
|
|
}
|
|
.filter-tab:hover { color: var(--ink); }
|
|
.filter-tab.is-active { color: var(--ink); border-bottom-color: var(--accent); }
|
|
.filter-tab__count {
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-faint);
|
|
background: var(--surface-active);
|
|
border-radius: var(--radius-full);
|
|
padding: 0 0.4rem;
|
|
min-width: 1.4rem;
|
|
text-align: center;
|
|
}
|
|
.filter-tab.is-active .filter-tab__count { background: var(--accent-soft); color: var(--accent); }
|
|
|
|
.filter-form { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
|
|
.filter-form .input { width: auto; flex: 1; min-width: 12rem; }
|
|
.filter-form .select { width: auto; min-width: 10rem; }
|
|
|
|
/* --- Model list ------------------------------------------------------------
|
|
Compact rows only. Editing is a page of its own -- a connection can advertise
|
|
a hundred models, and a list that renders a form for each is unusable.
|
|
*/
|
|
.bulk-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
flex-wrap: wrap;
|
|
padding: var(--sp-2) var(--sp-3);
|
|
border: var(--border-w) solid var(--border);
|
|
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
|
border-bottom: 0;
|
|
background: var(--bg-sunken);
|
|
}
|
|
.bulk-bar__label { font-size: var(--text-sm); color: var(--ink-muted); }
|
|
|
|
.model-rows {
|
|
border: var(--border-w) solid var(--border);
|
|
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
|
|
overflow: hidden;
|
|
background: var(--surface);
|
|
}
|
|
.model-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-3);
|
|
padding: var(--sp-2) var(--sp-3);
|
|
border-bottom: var(--border-w) solid var(--border);
|
|
}
|
|
.model-row:last-child { border-bottom: 0; }
|
|
.model-row:hover { background: var(--surface-hover); }
|
|
.model-row.is-off { opacity: 0.55; }
|
|
.model-row__check { accent-color: var(--accent); width: 1rem; height: 1rem; flex: none; }
|
|
.model-row__pos {
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-faint);
|
|
min-width: 1.75rem;
|
|
text-align: right;
|
|
flex: none;
|
|
}
|
|
.model-row__avatar { flex: none; width: 1.75rem; height: 1.75rem; }
|
|
.model-row__main { flex: 1; min-width: 0; }
|
|
.model-row__title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
flex-wrap: wrap;
|
|
}
|
|
.model-row__name {
|
|
color: var(--ink);
|
|
font-weight: 500;
|
|
font-size: var(--text-sm);
|
|
text-decoration: none;
|
|
}
|
|
.model-row__name:hover { color: var(--accent); text-decoration: underline; }
|
|
.model-row__id {
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-faint);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
display: block;
|
|
}
|
|
.model-row__actions { display: flex; align-items: center; gap: var(--sp-1); flex: none; }
|
|
|
|
.list-footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: var(--sp-3);
|
|
margin-top: var(--sp-3);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
/* --- Model detail ---------------------------------------------------------- */
|
|
.crumbs {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
margin-bottom: var(--sp-4);
|
|
font-size: var(--text-sm);
|
|
}
|
|
.crumbs > a:first-child {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--sp-1);
|
|
color: var(--ink-muted);
|
|
text-decoration: none;
|
|
}
|
|
.crumbs > a:first-child:hover { color: var(--ink); }
|
|
.crumbs__back { transform: rotate(180deg); }
|
|
.model-detail__avatar { width: 3rem; height: 3rem; flex: none; }
|
|
|
|
.model-list { list-style: none; margin: 0; padding: 0; }
|
|
.model-list__item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: var(--sp-3);
|
|
padding: var(--sp-3) 0;
|
|
border-bottom: var(--border-w) solid var(--border);
|
|
}
|
|
.model-list__item:first-child { padding-top: 0; }
|
|
.model-list__item:last-child { border-bottom: 0; padding-bottom: 0; }
|
|
|
|
/* The models card in /settings. Every row shares the list's column tracks, so
|
|
the context sizes and the eyes line up; the description and the capability
|
|
tags take the rest of the row underneath, never the space beside the name. */
|
|
.model-list--models {
|
|
display: grid;
|
|
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
|
column-gap: var(--sp-3);
|
|
}
|
|
.model-list--models .model-list__item {
|
|
grid-column: 1 / -1;
|
|
display: grid;
|
|
grid-template-columns: subgrid;
|
|
align-items: center;
|
|
row-gap: var(--sp-1);
|
|
}
|
|
/* Wraps rather than truncates: there is room below, and a name cut to
|
|
"Gemma 4 E…" on a phone is a different model's name. */
|
|
.model-list__name {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: var(--sp-1) var(--sp-2);
|
|
min-width: 0;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.model-list__more { grid-column: 2 / -1; min-width: 0; }
|
|
.model-list__tags { display: flex; flex-wrap: wrap; gap: var(--sp-1); }
|
|
.model-list__tags:empty { display: none; }
|
|
|
|
/* --- Permission grids ------------------------------------------------------ */
|
|
.checkbox-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--sp-2) var(--sp-4);
|
|
}
|
|
.checkbox-row .checkbox { flex: 0 0 auto; }
|
|
.checkbox.is-muted { opacity: 0.6; }
|
|
|
|
.perm-row {
|
|
align-items: flex-start;
|
|
padding: var(--sp-3) 0;
|
|
border-bottom: var(--border-w) solid var(--border);
|
|
}
|
|
.perm-row:last-child { border-bottom: 0; }
|
|
.perm-row input { margin-top: 0.15rem; }
|
|
.perm-row__desc {
|
|
display: block;
|
|
font-size: var(--text-xs);
|
|
color: var(--ink-faint);
|
|
font-weight: 400;
|
|
line-height: var(--leading-normal);
|
|
margin-top: 0.1rem;
|
|
}
|
|
|
|
.perm-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
|
|
.perm-list__item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
font-size: var(--text-sm);
|
|
}
|
|
.perm-list__state {
|
|
display: grid;
|
|
place-items: center;
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
border-radius: var(--radius-full);
|
|
background: var(--surface-active);
|
|
color: var(--ink-faint);
|
|
flex: none;
|
|
}
|
|
.perm-list__state.is-on { background: var(--success-soft); color: var(--success); }
|
|
|
|
/* --- Reference lists -------------------------------------------------------
|
|
The variable legend on the prompts page, and the read-only tool list beneath
|
|
it. A grid rather than a table because both have to collapse to a stack on a
|
|
narrow screen, which a table cannot do without abandoning its header.
|
|
*/
|
|
.ref-list { display: flex; flex-direction: column; }
|
|
|
|
.ref-row {
|
|
display: grid;
|
|
grid-template-columns: minmax(7rem, 11rem) 1fr minmax(0, 12rem);
|
|
gap: var(--sp-3);
|
|
align-items: baseline;
|
|
padding: var(--sp-2) 0;
|
|
border-top: var(--border-w) solid var(--border);
|
|
font-size: var(--text-sm);
|
|
line-height: var(--leading-normal);
|
|
}
|
|
.ref-row:first-child { border-top: 0; padding-top: 0; }
|
|
.ref-row > code { overflow-wrap: anywhere; }
|
|
.ref-row__value {
|
|
color: var(--ink-faint);
|
|
font-size: var(--text-xs);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
@media (max-width: 44rem) {
|
|
.ref-row { grid-template-columns: 1fr; gap: var(--sp-1); }
|
|
}
|
|
|
|
/* The assembled prompt. Wraps rather than scrolls sideways -- it is prose, and
|
|
a horizontal scrollbar on prose is unreadable. */
|
|
.prompt-preview {
|
|
margin: 0 0 var(--sp-3);
|
|
padding: var(--sp-3);
|
|
max-height: 28rem;
|
|
overflow-y: auto;
|
|
white-space: pre-wrap;
|
|
overflow-wrap: anywhere;
|
|
background: var(--code-bg);
|
|
border: var(--border-w) solid var(--code-border);
|
|
border-radius: var(--radius);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-xs);
|
|
line-height: var(--leading-normal);
|
|
color: var(--ink-muted);
|
|
}
|
|
.prompt-preview code { font-family: inherit; background: none; border: 0; padding: 0; }
|
|
|
|
/* --- Misc ------------------------------------------------------------------ */
|
|
.input--file {
|
|
height: auto;
|
|
padding: 0.35rem;
|
|
font-size: var(--text-xs);
|
|
background: var(--surface-raised);
|
|
}
|
|
|
|
.nav-item.is-disabled { opacity: 0.4; cursor: default; }
|
|
.nav-item.is-disabled:hover { background: none; color: var(--ink-muted); }
|
|
|
|
.field__hint code,
|
|
.card__lede code {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.92em;
|
|
padding: 0.05em 0.3em;
|
|
border-radius: var(--radius-sm);
|
|
background: var(--code-bg);
|
|
border: var(--border-w) solid var(--code-border);
|
|
}
|
|
|
|
/* --- The permission modes, explained on the agents page ------------------- */
|
|
.mode-list { margin: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
|
|
/* Whatever follows the list inside a card needs air -- on /admin/updates that is
|
|
the "Check the remote" button, and it sat flush against the last row as
|
|
though it were one. Keyed on *not being last* rather than on the sibling's
|
|
class, because three different things follow it there depending on the
|
|
host's state (the button, the version-mismatch alert, the not-a-checkout
|
|
hint) and enumerating them is how the fourth one gets missed. */
|
|
.mode-list:not(:last-child) { margin-bottom: var(--sp-5); }
|
|
.mode-list__row { display: flex; gap: var(--sp-3); align-items: baseline; }
|
|
.mode-list__row dt { flex: 0 0 5rem; color: var(--ink); }
|
|
.mode-list__row dd { margin: 0; color: var(--ink-muted); font-size: var(--text-sm); }
|
|
|
|
/* --- The schedule setup form ------------------------------------------------ */
|
|
/*
|
|
Which "when" fieldset is showing follows the radio, with no JavaScript. `:has()`
|
|
is what makes that possible, and it is the same move the "Something else" box
|
|
on an approval card makes: a control and the thing it reveals cannot fall out
|
|
of step if the stylesheet is the only thing relating them.
|
|
|
|
Hidden with `display: none` on the container rather than `[hidden]`, which
|
|
would need `!important` here -- see the note in app.css about `.btn` being
|
|
`inline-flex`. Fields inside a hidden fieldset are still submitted, which is
|
|
correct: `_rule_from_form` reads only the keys the chosen repeat mode uses.
|
|
*/
|
|
.schedule-repeat {
|
|
border: var(--border-w) solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
padding: var(--sp-4);
|
|
margin-bottom: var(--sp-4);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-3);
|
|
}
|
|
.schedule-repeat > legend { padding: 0 var(--sp-2); }
|
|
.schedule-repeat [data-repeat] { display: none; }
|
|
.schedule-repeat:has(input[value="every"]:checked) [data-repeat="every"] { display: block; }
|
|
.schedule-repeat:has(input[value="calendar"]:checked) [data-repeat="calendar"] { display: block; }
|
|
|
|
/*
|
|
A row of buttons above a list. `.btn-row` carries no bottom margin -- it is
|
|
used inside forms where `.field` provides the rhythm -- so "Add a workflow"
|
|
sat flush against the first row of the list it adds to, and the two read as
|
|
one control. Stated as an adjacency rather than a margin on the button row,
|
|
because the row is right to have none everywhere else it appears.
|
|
*/
|
|
.btn-row + .model-rows,
|
|
.btn-row + .model-list { margin-top: var(--sp-4); }
|