59739cc7fd
The second audit pass. Four things, and the first two were reported. The Prompts page put a screen of variables and a screen of preview above the editor, so the tabs began two screens down and switching one had to drag the whole page to be any use -- and on a short tab it could not drag far enough, leaving the panel stranded above a screenful of nothing. Editor first, reference after, bar sticky. Custom themes were three fixed slots: fifty-seven empty colour boxes on a fresh instance and no way to make a fourth theme. One block per theme plus a blank one, colours behind a disclosure. Both measured rather than argued about -- rendered through TestClient and driven under headless Chromium, where the tab bar moved 385->642px before and does not move now, and the themes page went from 5495px to 2820px. Asking where generated images go found the other two. Deleting a chat cascades to the attachment rows and leaves every file on disk; the helper written for exactly that was called from one place, and it was not the delete button, a schedule's chat, a helper's chat or deleting an account. Underneath it, `claim` bound message_id and never chat_id, so anything picked before a chat existed kept an empty chat_id forever -- which six readers filter on, so those files were also unnamed in the prompt, unopenable in the canvas, and invisible to the one caller the cleanup had. And folders nest now. The route has handled parent_id since folders existed, with a cycle guard and a depth cap the move path never applied; the sidebar has always drawn a tree. Nothing could ask for one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
574 lines
19 KiB
CSS
574 lines
19 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.
|
|
*/
|
|
.admin-scroll,
|
|
.main > .tabs > .tabs__body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--border-strong) transparent;
|
|
}
|
|
|
|
.page,
|
|
.admin-page {
|
|
max-width: 48rem;
|
|
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: 1px 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: 1;
|
|
}
|
|
|
|
.tabs__tab {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
height: var(--control-h-lg);
|
|
padding: 0 var(--sp-4);
|
|
border-bottom: 2px 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: 2px solid var(--accent); outline-offset: -2px; }
|
|
|
|
/*
|
|
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: 1px 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: 1px 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;
|
|
grid-template-columns: repeat(auto-fit, minmax(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: 1px 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: 1px 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: 2px 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: 1px 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: 1px 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: 1px 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: 1px solid var(--border);
|
|
}
|
|
.model-list__item:first-child { padding-top: 0; }
|
|
.model-list__item:last-child { border-bottom: 0; padding-bottom: 0; }
|
|
|
|
/* --- 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: 1px 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: 1px 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: 1px 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: 1px 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); }
|
|
.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: 1px 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); }
|