mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
CI's cypress-e2e job went red on the interface scale: `directory-search.cy.js` failed `expected '<div.wrolpi-searchbox-result-title>' to be 'visible'`. The suggestion list is capped at `min(60vh, 30rem)`. The rem ceiling scales with the interface but a fraction of the VIEWPORT does not, so when the type grew 10% the rows grew and the cap did not: the full set of grouped suggestions now needs about 420px, and 60vh of a 660px-tall window -- Cypress's default -- is 396px. The last group was clipped again, which is the exact defect the comment on that rule already describes. 70vh is 462px there, and the 30rem ceiling still bounds it on a tall window, so the list cannot cover the page either way. Measured rather than guessed: the spec fails at 660px of viewport height and passes from 700px up, so the shortfall was about 24px. Verified: all 50 e2e specs pass locally, 248 ui-layout component tests, 1141 jest tests.
1698 lines
57 KiB
CSS
1698 lines
57 KiB
CSS
/*
|
|
* Styles for the token-driven component library.
|
|
*
|
|
* Rules here encode design decisions Mantine's variables cannot express on their
|
|
* own — chiefly the two that change *form* rather than color between themes:
|
|
* the dashed danger treatment and status-by-brightness. See ui-design.md.
|
|
*
|
|
* Nothing in this file hardcodes a color; every value comes from a token.
|
|
*/
|
|
|
|
/* ------------------------------------------------------------------ Icons */
|
|
|
|
.wrolpi-icon-spin {
|
|
animation: wrolpi-icon-spin 0.9s linear infinite;
|
|
}
|
|
|
|
@keyframes wrolpi-icon-spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.wrolpi-icon-spin {
|
|
animation: none;
|
|
}
|
|
}
|
|
|
|
.wrolpi-icon-stack {
|
|
position: relative;
|
|
display: inline-flex;
|
|
line-height: 0;
|
|
}
|
|
|
|
/*
|
|
* The corner glyph sits on the main one, so it carries the surface color behind
|
|
* it — without that the two sets of strokes cross and neither reads.
|
|
*
|
|
* Which surface that is depends on where the stack was placed, and only the call
|
|
* site knows: the file browser puts one inside a filled blue button, where a disc
|
|
* of the page background is a hole punched in the button. `--icon-stack-bg`
|
|
* defaults to the page, and a call site anywhere else sets it.
|
|
*/
|
|
.wrolpi-icon-stack-corner {
|
|
position: absolute;
|
|
right: -3px;
|
|
bottom: -3px;
|
|
display: flex;
|
|
background: var(--icon-stack-bg, var(--bg));
|
|
border-radius: 50%;
|
|
padding: 1px;
|
|
}
|
|
|
|
/*
|
|
* A stack inside a button is the one case the call site should not have to describe,
|
|
* because the surface changes underneath it: Mantine paints a filled button from
|
|
* `--button-bg`, and the disc has to follow it into whatever state the button is in.
|
|
* The file browser's New Folder button is disabled in WROL mode, so a disc pinned to the
|
|
* enabled fill would be a blue chip on a grey button -- the original hole, inverted.
|
|
*
|
|
* Only `filled` gets the first rule: an outline or subtle button sets `--button-bg` to
|
|
* `transparent`, and there the surface behind the button is still the right answer.
|
|
* `:not([data-variant])` is that same case -- Mantine omits the attribute entirely when a
|
|
* call site names a color but no variant, which is how every filled button here is
|
|
* written, and keying on the attribute alone matched none of them. `--button-bg` itself
|
|
* is absent when a button names neither, hence the fallback chain.
|
|
*/
|
|
.mantine-Button-root:not([data-variant]) .wrolpi-icon-stack-corner,
|
|
.mantine-Button-root[data-variant="filled"] .wrolpi-icon-stack-corner {
|
|
background: var(--button-bg, var(--icon-stack-bg, var(--bg)));
|
|
}
|
|
|
|
/*
|
|
* There is deliberately no `:disabled` override here any more. There used to be one pinning
|
|
* the disc to `--mantine-color-disabled`, because Mantine repainted a disabled button grey
|
|
* and a disc left on `--blue` became a blue chip on a grey surface. Disabled buttons now
|
|
* keep their own fill (see the disabled-button rule further down), so `--button-bg` is
|
|
* correct in both states and the rule above covers them.
|
|
*/
|
|
|
|
/* ---------------------------------------------------------------- Buttons */
|
|
/*
|
|
* Dashed borders mean "destructive or failed", and nothing else may use them.
|
|
* In night mode a destructive button drops its fill entirely: 1.5px keeps the
|
|
* dashes legible at low brightness, where a 1px hairline dash disappears.
|
|
*/
|
|
|
|
/*
|
|
* In light, dark, and amber the button stays filled, which `color="red"` already
|
|
* does. Night is the exception, and it cannot be expressed by overriding
|
|
* Mantine's `--button-*` variables: Mantine writes those as inline styles on the
|
|
* element, which no stylesheet rule can outrank. Setting the real properties
|
|
* works instead — this selector (0,2,0 plus an element) outranks the Mantine
|
|
* class rule that reads the variables.
|
|
*/
|
|
html[data-theme="night"] .wrolpi-button-danger {
|
|
background: transparent;
|
|
color: var(--danger);
|
|
border: 1.5px dashed var(--danger);
|
|
}
|
|
|
|
html[data-theme="night"] .wrolpi-button-danger:hover:not(:disabled) {
|
|
background: var(--panel);
|
|
}
|
|
|
|
/* The icon inside inherits currentColor, so it follows without another rule. */
|
|
|
|
/*
|
|
* An icon button is the same height as a labelled one, so a group of them lines up.
|
|
*
|
|
* Mantine gives ActionIcon a size scale unrelated to Button's -- `sm` is 22px against a
|
|
* button's 36px -- which left the Details/Open pair on every archive card ragged, and any
|
|
* other toolbar that mixes the two. It already ships the aligned scale as
|
|
* `--ai-size-input-*`, for an ActionIcon standing beside an Input; those five values are
|
|
* identical to `--button-height-*`. Pointing one at the other fixes every size at once,
|
|
* including ones no call site uses yet.
|
|
*
|
|
* The `html` prefix is load-bearing. Mantine declares this scale on ActionIcon's own
|
|
* hashed class, which has the same specificity as `.mantine-ActionIcon-root` and would win
|
|
* on source order.
|
|
*/
|
|
html .mantine-ActionIcon-root {
|
|
--ai-size-xs: var(--ai-size-input-xs);
|
|
--ai-size-sm: var(--ai-size-input-sm);
|
|
--ai-size-md: var(--ai-size-input-md);
|
|
--ai-size-lg: var(--ai-size-input-lg);
|
|
--ai-size-xl: var(--ai-size-input-xl);
|
|
}
|
|
|
|
/* ----------------------------------------------------------- Theme picker */
|
|
|
|
.wrolpi-theme-picker {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(14.375rem, 1fr));
|
|
gap: 0.625rem;
|
|
}
|
|
|
|
.wrolpi-theme-option {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
align-items: flex-start;
|
|
text-align: left;
|
|
padding: 0.625rem 0.75rem;
|
|
background: var(--panel);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
font: inherit;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.wrolpi-theme-option:hover {
|
|
background: var(--head);
|
|
}
|
|
|
|
/* The selected theme is marked by a heavier border as well as color, so the
|
|
selection survives in monochrome themes. */
|
|
.wrolpi-theme-option-active {
|
|
border: 2px solid var(--blue);
|
|
padding: 0.5625rem 0.6875rem;
|
|
}
|
|
|
|
html[data-theme="night"] .wrolpi-theme-option-active,
|
|
html[data-theme="amber"] .wrolpi-theme-option-active {
|
|
border-color: var(--text);
|
|
}
|
|
|
|
.wrolpi-theme-option:focus-visible {
|
|
outline: 2px solid var(--blue);
|
|
outline-offset: 1px;
|
|
}
|
|
|
|
.wrolpi-theme-option-name {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.4375rem;
|
|
font-weight: 600;
|
|
font-size: 0.8125rem;
|
|
}
|
|
|
|
.wrolpi-theme-option-description {
|
|
font-size: 0.75rem;
|
|
color: var(--muted);
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* Separated from the theme grid: it configures the chosen theme, it is not a choice. */
|
|
.wrolpi-media-filter-setting {
|
|
margin-top: 0.875rem;
|
|
padding-top: 0.75rem;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
/* ------------------------------------------------------------ Pagination */
|
|
|
|
/*
|
|
* The pager is centred here rather than by whoever renders it. Three call sites wrapped it
|
|
* in `<center>` or `text-align: center` and it still sat hard against the left edge.
|
|
*
|
|
* Mantine builds a Pagination as a BLOCK root -- full width, so those wrappers had nothing
|
|
* to centre -- holding a flex Group that carries the controls and lays them out at
|
|
* `flex-start`. `justify-content` on the root therefore does nothing at all; it has to
|
|
* reach the Group.
|
|
*
|
|
* Setting `--group-justify` on the root does not work either: Group defaults that prop and
|
|
* writes the variable INLINE on its own element, where an inherited value cannot reach it.
|
|
* The property itself is fair game, though -- Mantine only ever reads the variable from a
|
|
* hashed class -- so this rule at (0,2,0) is what actually lands.
|
|
*
|
|
* The Group already wraps by default (`--group-wrap` falls back to `wrap`), which matters
|
|
* once the row is centred: centred overflow spills off BOTH edges, and the first page would
|
|
* be unreachable rather than merely off-screen to the right.
|
|
*/
|
|
.wrolpi-pagination .mantine-Group-root {
|
|
justify-content: center;
|
|
}
|
|
|
|
.wrolpi-pagination button {
|
|
background: var(--panel);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.wrolpi-pagination button[data-active] {
|
|
background: var(--blue);
|
|
color: var(--btn-text);
|
|
border-color: var(--blue);
|
|
}
|
|
|
|
/* Night and amber have no second hue to spend, so the current page is marked by a
|
|
heavier border and the panel surface instead of a filled block of color. */
|
|
html[data-theme="night"] .wrolpi-pagination button[data-active],
|
|
html[data-theme="amber"] .wrolpi-pagination button[data-active] {
|
|
background: var(--panel);
|
|
color: var(--text);
|
|
border: 2px solid var(--text);
|
|
}
|
|
|
|
.wrolpi-pagination button:disabled {
|
|
color: var(--muted);
|
|
background: var(--bg);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- Tab bar */
|
|
|
|
.wrolpi-tabs {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: space-between;
|
|
gap: 0.75rem;
|
|
border-bottom: 1px solid var(--border);
|
|
margin-bottom: 0.875rem;
|
|
}
|
|
|
|
.wrolpi-tabs-links {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.wrolpi-tabs-right {
|
|
padding-bottom: 0.375rem;
|
|
}
|
|
|
|
.wrolpi-tab {
|
|
display: inline-block;
|
|
padding: 0.5rem 0.875rem;
|
|
color: var(--muted);
|
|
text-decoration: none;
|
|
font-size: 0.8125rem;
|
|
/*
|
|
* A tab is a NavLink in the app's nav bars but a <button> on Inventory and in the
|
|
* gallery, and a button carries the UA's own `buttonface` background -- rgb(239,239,239).
|
|
* Only the active tab set a background, so every inactive one was a near-white block in
|
|
* dark, night and amber. The bar's own surface has to show through instead.
|
|
*/
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
/* Sits on the bar's own rule, so the active tab can erase it and read as
|
|
connected to the panel below. */
|
|
margin-bottom: -1px;
|
|
}
|
|
|
|
.wrolpi-tab:hover {
|
|
color: var(--text);
|
|
}
|
|
|
|
.wrolpi-tab-active {
|
|
color: var(--text);
|
|
background: var(--panel);
|
|
border-color: var(--border);
|
|
border-bottom-color: var(--panel);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.wrolpi-tab:focus-visible {
|
|
outline: 2px solid var(--blue);
|
|
outline-offset: -2px;
|
|
}
|
|
|
|
/* ------------------------------------------------------------- Search box */
|
|
|
|
.wrolpi-searchbox {
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
|
|
.wrolpi-searchbox-control {
|
|
display: flex;
|
|
align-items: center;
|
|
/*
|
|
* One inset for the padding, the gap between children, and the negative margin that
|
|
* welds the clear button to the edge. They were three separate 8px literals that had to
|
|
* stay in lockstep: changing the padding alone would have pushed the clear button past
|
|
* the border, and changing the gap alone would have re-inset it.
|
|
*/
|
|
--searchbox-inset: 0.5rem;
|
|
gap: var(--searchbox-inset);
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
padding: 0 var(--searchbox-inset);
|
|
}
|
|
|
|
.wrolpi-searchbox-control:focus-within {
|
|
border-color: var(--blue);
|
|
}
|
|
|
|
html[data-theme="night"] .wrolpi-searchbox-control:focus-within,
|
|
html[data-theme="amber"] .wrolpi-searchbox-control:focus-within {
|
|
border-color: var(--text);
|
|
}
|
|
|
|
.wrolpi-searchbox-icon {
|
|
display: flex;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.wrolpi-searchbox-input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding: 0.5rem 0;
|
|
background: none;
|
|
border: none;
|
|
outline: none;
|
|
color: var(--text);
|
|
font: inherit;
|
|
font-size: 0.8125rem;
|
|
}
|
|
|
|
.wrolpi-searchbox-input::placeholder {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.wrolpi-searchbox-loading,
|
|
.wrolpi-searchbox-clear {
|
|
flex: none;
|
|
}
|
|
|
|
/*
|
|
* The clear button is attached to the field, not floating inside it.
|
|
*
|
|
* The control is a bordered box with `padding: 0 8px`, so the button sat 9px short of the
|
|
* right edge and 2px short of the full height -- reading as a glyph dropped inside the input
|
|
* rather than as a control of its own. Semantic rendered this as an action button welded to
|
|
* the field: measured on the QA Pi, zero gap to the input, zero to the outer edge, and the
|
|
* full 44px height.
|
|
*
|
|
* A negative right margin rather than removing the control's padding: the padding is what
|
|
* holds the search icon and the text off the border, and only this one child should escape
|
|
* it. The gap to its LEFT is deliberately left alone -- cancelling that too welded the
|
|
* loading spinner to the divider whenever a field was both `loading` and `clearable`, which
|
|
* SearchResultsInput does, and the spinner is not part of this control.
|
|
*
|
|
* `html` because Mantine sets ActionIcon's height from `--ai-size` on its own single class;
|
|
* at equal specificity the winner is whichever stylesheet loaded last, which is not a thing
|
|
* to rely on. `min-height` has to go with it: stretch cannot shrink a box below its own
|
|
* minimum, so leaving that at `--ai-size` would mean this rule only half-owned the height it
|
|
* claims to set -- true today only because the field happens to be the taller of the two.
|
|
*
|
|
* The corner radius needs no rule: the theme zeroes every `--mantine-radius-*`, so there are
|
|
* no rounded corners to leave panel-colored wedges against the square field.
|
|
*/
|
|
html .wrolpi-searchbox-clear {
|
|
align-self: stretch;
|
|
height: auto;
|
|
min-height: unset;
|
|
margin-right: calc(-1 * var(--searchbox-inset));
|
|
border-left: 1px solid var(--border);
|
|
}
|
|
|
|
/*
|
|
* The suggestion list overlays the page rather than pushing it down — a search
|
|
* that reflows the results underneath it is unusable while typing.
|
|
*/
|
|
.wrolpi-searchbox-results {
|
|
position: absolute;
|
|
z-index: 20;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
/*
|
|
* Tall enough for a full set of grouped suggestions -- directories, channels and
|
|
* domains together run past 320px, and the last group was being clipped -- but
|
|
* still capped so a long list cannot cover the page.
|
|
*
|
|
* 70vh, raised from 60. The rem ceiling scales with the interface but a fraction of
|
|
* the VIEWPORT does not, so when the type grew 10% the rows grew with it and the cap
|
|
* did not: the full set now needs about 420px, and 60vh of a 660px-tall window is
|
|
* 396px, so the last group was clipped again -- the exact defect this comment already
|
|
* describes. 70vh is 462px there, and the 30rem ceiling still bounds it on a tall
|
|
* window, so it cannot cover the page either way.
|
|
*
|
|
* `directory-search.cy.js` is the regression test: it fails at 660px of viewport
|
|
* height and passes from 700px up, which is how the shortfall was measured.
|
|
*/
|
|
max-height: min(70vh, 30rem);
|
|
overflow-y: auto;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-top: none;
|
|
}
|
|
|
|
/*
|
|
* Inside the global search modal the suggestions ARE the panel's content, so they sit in
|
|
* flow instead of floating over the page.
|
|
*
|
|
* Overlaying is right everywhere else -- a list that reflows the page while you type is
|
|
* unusable -- but in a modal it fails twice. An absolutely positioned list contributes no
|
|
* height, so the panel sized itself to the search box alone (114px), and the panel's own
|
|
* `overflow: auto` then clipped the list at that edge: the modal showed an input and the
|
|
* first line of the first group. In flow, the panel grows to fit and its own max-height
|
|
* does the scrolling.
|
|
*/
|
|
.wrolpi-search-modal .wrolpi-searchbox-results {
|
|
position: static;
|
|
max-height: min(50vh, 26.25rem);
|
|
}
|
|
|
|
.wrolpi-searchbox-group-name {
|
|
padding: 0.375rem 0.625rem;
|
|
font-size: 0.6875rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
background: var(--head);
|
|
}
|
|
|
|
.wrolpi-searchbox-result {
|
|
padding: 0.4375rem 0.625rem;
|
|
cursor: pointer;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.wrolpi-searchbox-result-active {
|
|
background: var(--head);
|
|
}
|
|
|
|
.wrolpi-searchbox-result-title {
|
|
font-size: 0.8125rem;
|
|
color: var(--text);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.wrolpi-searchbox-result-description {
|
|
font-size: 0.6875rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.wrolpi-searchbox-empty {
|
|
padding: 0.625rem;
|
|
font-size: 0.75rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- Modals */
|
|
/*
|
|
* Actions sit below the body, separated by a hairline rather than a raised bar.
|
|
* Right-aligned, so the confirming action lands where the eye finishes reading.
|
|
*/
|
|
|
|
.wrolpi-modal-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
flex-wrap: wrap;
|
|
gap: 0.625rem;
|
|
/* Bleed back out through the modal body's own padding so the rule spans the
|
|
full width; -1 * whatever Mantine padded the body with. */
|
|
margin: 1.125rem calc(var(--mantine-spacing-md) * -1) calc(var(--mantine-spacing-md) * -1);
|
|
padding: 0.75rem var(--mantine-spacing-md);
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
/* A confirmation's buttons: under the question, no separating rule. See Confirm. */
|
|
.wrolpi-confirm-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 0.625rem;
|
|
margin-top: 1.125rem;
|
|
}
|
|
|
|
/* A stack of form rows, replacing Semantic's `ui stackable grid` in edit forms. */
|
|
.wrolpi-form-rows {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.875rem;
|
|
}
|
|
|
|
/* ------------------------------------------------------------- Form fields */
|
|
/*
|
|
* A Pill is what MultiSelect renders for each chosen option, and it is the one place
|
|
* Mantine does not honour the radius theme. MultiSelect leaves `radius` undefined, so
|
|
* Pill emits no `--pill-radius` at all and its stylesheet falls back to
|
|
* `border-radius: var(--pill-radius, 1000rem)` -- a literal in Mantine's own CSS, which
|
|
* zeroing `--mantine-radius-xl` never touches. Defining the variable is what stops the
|
|
* fallback from ever applying. Everything else in the library has hard corners.
|
|
*/
|
|
.mantine-Pill-root {
|
|
--pill-radius: 0;
|
|
border-radius: 0;
|
|
}
|
|
|
|
/*
|
|
* `hooks/useForm.js` renders its own <label> and native <input> — it always did, with
|
|
* Semantic's Form.Input only wrapping them for field styling. These rules supply that
|
|
* styling from tokens instead, so a native input matches the library's own controls.
|
|
*/
|
|
|
|
.wrolpi-form-field input {
|
|
width: 100%;
|
|
padding: 0.4375rem 0.5625rem;
|
|
background: var(--panel);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
font: inherit;
|
|
font-size: 0.8125rem;
|
|
}
|
|
|
|
.wrolpi-form-field input::placeholder {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.wrolpi-form-field input:focus {
|
|
outline: none;
|
|
border-color: var(--blue);
|
|
}
|
|
|
|
html[data-theme="night"] .wrolpi-form-field input:focus,
|
|
html[data-theme="amber"] .wrolpi-form-field input:focus {
|
|
border-color: var(--text);
|
|
}
|
|
|
|
/* An invalid field is dashed, matching every other failure state in the interface. */
|
|
.wrolpi-form-field[data-error] input {
|
|
border: 1.5px dashed var(--danger);
|
|
}
|
|
|
|
.wrolpi-form-field input:disabled {
|
|
color: var(--muted);
|
|
background: var(--bg);
|
|
}
|
|
|
|
/* --------------------------------------------------------------- Path input */
|
|
/*
|
|
* Prefix and input are siblings in a flex row sharing one outline, so the prefix cannot
|
|
* print over the value -- the failure that Mantine's absolutely-positioned `leftSection`
|
|
* allowed when its reserved width was wrong. The prefix takes its natural width, so any
|
|
* media directory fits without a number being maintained anywhere.
|
|
*/
|
|
|
|
.wrolpi-path-input-label {
|
|
display: block;
|
|
font-weight: 600;
|
|
font-size: 0.8125rem;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.wrolpi-path-input-control {
|
|
display: flex;
|
|
align-items: stretch;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.wrolpi-path-input-control:focus-within {
|
|
border-color: var(--blue);
|
|
}
|
|
|
|
html[data-theme="night"] .wrolpi-path-input-control:focus-within,
|
|
html[data-theme="amber"] .wrolpi-path-input-control:focus-within {
|
|
border-color: var(--text);
|
|
}
|
|
|
|
.wrolpi-path-input-control[data-error] {
|
|
border: 1.5px dashed var(--danger);
|
|
}
|
|
|
|
.wrolpi-path-input-control[data-disabled] {
|
|
background: var(--bg);
|
|
}
|
|
|
|
/*
|
|
* Muted and on the header surface: it reads as context, not as something to edit.
|
|
*
|
|
* It shrinks and ellipsises rather than holding its full width. The prefix is whatever the
|
|
* API reports as the media directory -- `/media/wrolpi` on a Pi, but a development box or an
|
|
* unusual Docker mount can report a much longer path -- and at `flex: none` a long one took
|
|
* the whole row and left the input 18px wide, a field nobody could type into. Capping it at
|
|
* half the control keeps the editable part editable; the full path stays available through
|
|
* `title` and is read out in full by the accessible description either way.
|
|
*/
|
|
.wrolpi-path-input-prefix {
|
|
display: flex;
|
|
align-items: center;
|
|
flex: 0 1 auto;
|
|
min-width: 3ch;
|
|
max-width: 50%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
padding: 0 0.375rem 0 0.5625rem;
|
|
background: var(--head);
|
|
border-right: 1px solid var(--border);
|
|
color: var(--muted);
|
|
font-size: 0.75rem;
|
|
white-space: nowrap;
|
|
user-select: none;
|
|
}
|
|
|
|
.wrolpi-path-input-control input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding: 0.4375rem 0.5625rem;
|
|
background: none;
|
|
border: none;
|
|
outline: none;
|
|
color: var(--text);
|
|
font: inherit;
|
|
font-size: 0.8125rem;
|
|
}
|
|
|
|
.wrolpi-path-input-control input::placeholder {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.wrolpi-path-input-control input:disabled {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.wrolpi-path-input-description {
|
|
font-size: 0.75rem;
|
|
color: var(--muted);
|
|
margin-top: 0.1875rem;
|
|
}
|
|
|
|
.wrolpi-path-input-error {
|
|
font-size: 0.75rem;
|
|
color: var(--danger);
|
|
margin-top: 0.1875rem;
|
|
}
|
|
|
|
/* ---------------------------------------------------------- Action inputs */
|
|
/*
|
|
* Input and button share one outline: the input gives up its right border and
|
|
* the button's left border becomes the seam between them.
|
|
*/
|
|
|
|
.wrolpi-action-input {
|
|
display: flex;
|
|
align-items: stretch;
|
|
width: 100%;
|
|
}
|
|
|
|
.wrolpi-action-input > .mantine-TextInput-root {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.wrolpi-action-input .mantine-TextInput-input {
|
|
border-right-width: 0;
|
|
}
|
|
|
|
.wrolpi-action-input > :not(.mantine-TextInput-root) {
|
|
flex: none;
|
|
}
|
|
|
|
/* A colored label beside an input, unlike ActionInput's shared outline: they stay two
|
|
controls, so the gap between them is real and scales with the type. */
|
|
.wrolpi-colored-input {
|
|
display: flex;
|
|
align-items: stretch;
|
|
gap: 0.375rem;
|
|
}
|
|
|
|
/* ------------------------------------------------- Loading & placeholders */
|
|
/*
|
|
* A centered loader with a caption, and the skeleton stack that stands in for text.
|
|
* Both were inline px in Feedback.tsx, which put them outside the interface scale.
|
|
*/
|
|
.wrolpi-loading {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.625rem;
|
|
}
|
|
|
|
.wrolpi-loading-caption {
|
|
font-size: 0.8125rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.wrolpi-placeholder {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
/* --------------------------------------------------------------- Switches */
|
|
/*
|
|
* Mantine's unchecked track is a gray from its own palette, which would put a
|
|
* non-red pixel on screen in night mode. Scoped to `:not(:checked)` so the
|
|
* checked track keeps coming from the `color` prop.
|
|
*/
|
|
/*
|
|
* Semantic's inverted stylesheet colors *every* descendant of `.ui.inverted.segment`
|
|
* (rgba(255,255,255,.9), and #dddddd on labels). Our components are mounted inside
|
|
* unmigrated pages that still use those wrappers, so an inherited color puts white
|
|
* pixels on screen in night mode. Restating from tokens is the fix; the rule can go
|
|
* when the last Semantic wrapper does.
|
|
*/
|
|
.wrolpi-switch .mantine-Switch-label {
|
|
color: var(--text);
|
|
}
|
|
|
|
.wrolpi-switch .mantine-Switch-description {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.wrolpi-switch input:disabled + * .mantine-Switch-label,
|
|
.wrolpi-switch input:disabled ~ * .mantine-Switch-label {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.wrolpi-switch input:not(:checked) + .mantine-Switch-track {
|
|
--switch-bg: var(--border);
|
|
}
|
|
|
|
/*
|
|
* The disabled thumb has the same problem: Mantine sets it from its gray palette,
|
|
* on the thumb element itself, which is a near-white pixel in night mode.
|
|
*/
|
|
.wrolpi-switch input:disabled + * > .mantine-Switch-thumb,
|
|
.wrolpi-switch input[data-disabled] + * > .mantine-Switch-thumb {
|
|
--switch-thumb-bg-disabled: var(--muted);
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- Labels */
|
|
|
|
/*
|
|
* Sized to Semantic's label (12px text, ~7px of vertical padding), which these replaced.
|
|
* The first pass came out at 11.5px with 3px of padding and read as noticeably smaller than
|
|
* what users had before -- a tag is a hit target as well as a word.
|
|
*/
|
|
.wrolpi-label {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
padding: 0.3125rem 0.625rem;
|
|
font-size: 0.78125rem;
|
|
font-weight: 500;
|
|
line-height: 1.3;
|
|
background: var(--label-color);
|
|
/*
|
|
* `--label-text` is set per-tag by Tags.js, which calculates black or light text against
|
|
* the user's chosen fill. The fallback covers every label whose fill is a theme token.
|
|
* It is a variable rather than an inline `color` so that a theme which discards the fill
|
|
* can also discard the text color calculated for it.
|
|
*/
|
|
color: var(--label-text, var(--btn-text));
|
|
border: 1px solid transparent;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------- Tags */
|
|
/*
|
|
* A tag is a label with a physical shape: a pointed left edge and an eyelet, like a tag
|
|
* tied to a garment. The point is a square rotated 45 degrees behind the left edge -- its
|
|
* right half sits under the body in the same color and merges, leaving the left half
|
|
* showing as the point. At 20px a side its diagonal is 28px, which is the body's height,
|
|
* so the two edges of the point meet the body exactly at its corners.
|
|
*
|
|
* The eyelet is drawn in `currentColor`, so it reads as a hole punched through the tag on
|
|
* any surface, filled or outlined, without needing to know what is behind it.
|
|
*/
|
|
|
|
.wrolpi-tag {
|
|
position: relative;
|
|
/*
|
|
* Room for the point, which is drawn outside the body: half its diagonal (14.1px) plus
|
|
* the 1px it is nudged left, rounded up.
|
|
*/
|
|
margin-left: 1rem;
|
|
/* Tags wrap, and a wrapped row was sitting on the row above it. */
|
|
margin-bottom: 0.3125rem;
|
|
/*
|
|
* The point is centred on the left edge, so its inner half covers the body's first 10px
|
|
* -- and a positioned pseudo-element paints above the parent's text, not behind it, so
|
|
* without this the first letter is hidden under the point. Semantic's tag label carried
|
|
* the same asymmetry (`padding-left: 1.5em`) for the same reason.
|
|
*/
|
|
padding-left: 0.8125rem;
|
|
}
|
|
|
|
.wrolpi-tag::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 50%;
|
|
right: 100%;
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
/*
|
|
* Nudged 1px left of where `right: 100%` puts it. That property positions against the
|
|
* containing block's *padding* box, and the body carries a 1px border, so a square centred
|
|
* there sits a pixel inside the visible left edge -- and the point read as set slightly
|
|
* into the tag rather than growing out of it. This is exactly that border: 2px opened a
|
|
* visible gap on the inner side, because the point's edges then meet the body's left edge
|
|
* short of its corners and leave the difference showing.
|
|
*/
|
|
transform: translate(calc(50% - 1px), -50%) rotate(45deg);
|
|
background: var(--label-color);
|
|
}
|
|
|
|
/*
|
|
* The eyelet sits in the point, where a real tag's hole is -- inside the body it reads as a
|
|
* bullet in front of the word. It is outside the element's own box, over the ::before, and
|
|
* paints on top of it by being the later pseudo-element.
|
|
*/
|
|
.wrolpi-tag::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 50%;
|
|
left: -7px;
|
|
width: 0.3125rem;
|
|
height: 0.3125rem;
|
|
margin-top: -2.5px;
|
|
border-radius: 50%;
|
|
background: currentColor;
|
|
/* A hole, not a dot: it should read as absence rather than as a bullet. */
|
|
opacity: 0.45;
|
|
}
|
|
|
|
/*
|
|
* White and black labels would vanish into the surface they sit on. They set the text
|
|
* color through `--label-text` rather than `color` for the same reason the per-tag color
|
|
* does: setting `color` here outranked the base rule, so amber -- which replaces every fill
|
|
* with its own and has no `color` rule of its own -- was left painting `--white` on `--text`,
|
|
* light orange on orange.
|
|
*/
|
|
.wrolpi-label-white {
|
|
--label-text: var(--black);
|
|
border-color: var(--border);
|
|
}
|
|
|
|
.wrolpi-label-black {
|
|
--label-text: var(--white);
|
|
border-color: var(--border);
|
|
}
|
|
|
|
/*
|
|
* Night mode: a filled label is a bright patch, and a wall of tags would be a
|
|
* wall of light. Every label becomes an outline instead.
|
|
*/
|
|
html[data-theme="night"] .wrolpi-label {
|
|
background: transparent;
|
|
color: var(--label-color);
|
|
border-color: var(--label-color);
|
|
}
|
|
|
|
/*
|
|
* Night draws the tag as an outline, so the point has to be two strokes rather than a solid
|
|
* diamond, and the body's own left border has to go -- otherwise a line closes across the
|
|
* base of the point and it reads as a chip with a triangle stuck to it.
|
|
*/
|
|
html[data-theme="night"] .wrolpi-tag {
|
|
border-left-color: transparent;
|
|
}
|
|
|
|
html[data-theme="night"] .wrolpi-tag::before {
|
|
background: transparent;
|
|
border-left: 1px solid var(--label-color);
|
|
border-bottom: 1px solid var(--label-color);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- Headers */
|
|
|
|
.wrolpi-header {
|
|
margin: 0 0 0.625rem;
|
|
}
|
|
|
|
/*
|
|
* A heading opens a section, so it needs more air above it than below — it belongs to
|
|
* what follows, not to what came before. With no top margin at all a heading sat flush
|
|
* against the panel above it, which read as part of that panel rather than as the start
|
|
* of the next thing (visible on /more/statistics, where "Tags" touched the panel above).
|
|
*
|
|
* `* +` rather than a blanket margin: a heading that opens a Panel or a page is already
|
|
* where it should be, and pushing it down would just dent the top of its own container.
|
|
*/
|
|
* + .wrolpi-header {
|
|
margin-top: 1.375rem;
|
|
}
|
|
|
|
/* The heading and its trailing control share one row. */
|
|
.wrolpi-header-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
/*
|
|
* Trailing content keeps the heading's row but not its size: a help button rendered at
|
|
* heading scale is a 26px glyph next to a 17px word.
|
|
*
|
|
* `flex-shrink: 0` states the intent -- a control is not the thing that should give way,
|
|
* a long title should wrap instead. Declared, but NOT covered by a test: with the row's
|
|
* only other item being a heading, the heading's automatic minimum size absorbs the
|
|
* pressure and no width could be found where removing this changes what is painted. It
|
|
* is here for the day the slot holds something beside a heading that can shrink, not
|
|
* because a case exists today.
|
|
*/
|
|
.wrolpi-header-after {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
font-size: 0.8125rem;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.wrolpi-header-dividing {
|
|
border-bottom: 1px solid var(--border);
|
|
padding-bottom: 0.375rem;
|
|
}
|
|
|
|
.wrolpi-header-text {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin: 0;
|
|
/*
|
|
* Stated explicitly because semantic.min.css sets `h1,h2,h3,h4,h5 { font-family:
|
|
* Lato }` and loads after Mantine's element rule, so headings would ignore the
|
|
* theme's body face — visible in amber, where the body is mono and headings were
|
|
* not. A class outranks the element selector. Can go once Semantic is gone.
|
|
*/
|
|
font-family: var(--font-body);
|
|
font-weight: 600;
|
|
line-height: 1.25;
|
|
color: var(--text);
|
|
text-wrap: balance;
|
|
}
|
|
|
|
/* The scale lives here so no call site invents a size of its own. */
|
|
.wrolpi-header-h1 {
|
|
font-size: 1.625rem;
|
|
}
|
|
|
|
.wrolpi-header-h2 {
|
|
font-size: 1.3125rem;
|
|
}
|
|
|
|
.wrolpi-header-h3 {
|
|
font-size: 1.0625rem;
|
|
}
|
|
|
|
.wrolpi-header-h4 {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.wrolpi-header-h5 {
|
|
font-size: 0.8125rem;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
.wrolpi-header-sub {
|
|
font-size: 0.75rem;
|
|
color: var(--muted);
|
|
margin-top: 0.1875rem;
|
|
}
|
|
|
|
/*
|
|
* Night and amber are single-hue by definition, and a call site may legitimately want
|
|
* many distinguishable colors — the Flasher's chip-family badges set `--label-color`
|
|
* inline from a twelve-color palette that no four tokens can express.
|
|
*
|
|
* In those themes the palette is overridden back to the theme's own text color, so a
|
|
* label cannot put a green or blue pixel on a night-adapted screen. `!important` is
|
|
* required because the palette arrives as an inline custom property, which a normal
|
|
* rule cannot outrank. The labels stop being distinguishable by color, which is the
|
|
* correct trade: the theme's whole purpose is that no other hue reaches the eye.
|
|
*/
|
|
html[data-theme="night"] .wrolpi-label,
|
|
html[data-theme="amber"] .wrolpi-label {
|
|
--label-color: var(--text) !important;
|
|
/*
|
|
* The per-tag text color goes with the fill it was calculated against. Tags.js works
|
|
* out black-or-light from the user's chosen hex, and once that hex is discarded the
|
|
* answer is not just useless but wrong: amber was painting `#dddddd` on `#ff9500`, about
|
|
* 1.9:1 and a neutral grey in a theme whose whole point is that nothing else is. Night,
|
|
* which is an outline over a near-black page, was painting that black text on nothing.
|
|
*
|
|
* There is no contrast to calculate here, which is why this is a token and not a second
|
|
* version of `contrastingColor`: the fill is the theme's own, so the answer is fixed.
|
|
*/
|
|
--label-text: var(--btn-text) !important;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- Toasts */
|
|
/*
|
|
* Notifications take their colors from our tokens rather than from Mantine's color scheme.
|
|
*
|
|
* Mantine paints a dark-scheme notification title with `--mantine-color-white`, and the
|
|
* bridge aliases that to `--btn-text` -- deliberately, because it is the text drawn on a
|
|
* filled button, which is near-black in dark, night and amber. So the title came out at
|
|
* rgb(28, 31, 33) on a rgb(38, 41, 43) surface: about 1.1:1, present and invisible. Fixing
|
|
* the alias is not an option; it is correct for the buttons it exists for.
|
|
*
|
|
* The `html` prefix is only there to outrank Mantine's single-class rules. One set of values
|
|
* covers every theme, because the tokens are what differ.
|
|
*/
|
|
|
|
html .mantine-Notification-root {
|
|
background-color: var(--panel);
|
|
border-color: var(--border);
|
|
}
|
|
|
|
html .mantine-Notification-title {
|
|
color: var(--text);
|
|
}
|
|
|
|
html .mantine-Notification-description {
|
|
color: var(--muted);
|
|
}
|
|
|
|
/*
|
|
* The description is the message itself, so it has to be read, and `--muted` is too dim to
|
|
* read on a panel in the two monochrome themes -- 2.0:1 in night, 3.1:1 in amber, against
|
|
* 5.1:1 and 5.7:1 in dark and light. Those two drop the muting and let the title's weight
|
|
* carry the hierarchy instead, which is the same brightness-over-hue trade the rest of night
|
|
* makes.
|
|
*/
|
|
html[data-theme="night"] .mantine-Notification-description,
|
|
html[data-theme="amber"] .mantine-Notification-description {
|
|
color: var(--text);
|
|
}
|
|
|
|
html .mantine-Notification-closeButton {
|
|
color: var(--muted);
|
|
}
|
|
|
|
html .mantine-Notification-closeButton:hover {
|
|
background-color: var(--head);
|
|
color: var(--text);
|
|
}
|
|
|
|
/* --------------------------------------------------------------- Messages */
|
|
|
|
.wrolpi-message {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0.625rem;
|
|
border: 1px solid var(--border);
|
|
border-left: 3px solid var(--message-color, var(--border));
|
|
background: var(--panel);
|
|
padding: 0.625rem 0.875rem;
|
|
font-size: 0.8125rem;
|
|
}
|
|
|
|
.wrolpi-message-content {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* The icon repeats what the border color already says, so it takes that color. */
|
|
.wrolpi-message-icon {
|
|
color: var(--message-color, var(--text));
|
|
display: flex;
|
|
padding-top: 1px;
|
|
}
|
|
|
|
.wrolpi-message-dismiss {
|
|
background: none;
|
|
border: none;
|
|
padding: 0;
|
|
color: var(--muted);
|
|
cursor: pointer;
|
|
display: flex;
|
|
/* Widen the tap target past the glyph without moving it. */
|
|
margin: -6px -6px -6px 0;
|
|
padding: 0.375rem;
|
|
}
|
|
|
|
.wrolpi-message-dismiss:hover {
|
|
color: var(--text);
|
|
}
|
|
|
|
.wrolpi-message-dismiss:focus-visible {
|
|
outline: 2px solid var(--blue);
|
|
outline-offset: -2px;
|
|
}
|
|
|
|
.wrolpi-message-title {
|
|
font-weight: 600;
|
|
margin-bottom: 0.125rem;
|
|
}
|
|
|
|
.wrolpi-message-body {
|
|
color: var(--muted);
|
|
}
|
|
|
|
/* Errors take the danger treatment in every theme, so "dashed" stays one rule. */
|
|
/*
|
|
* The dashed frame tracks the role the message was given, exactly as its accent and
|
|
* icon already do. Naming a hue here meant the frame and the accent could disagree --
|
|
* invisible in night, where they are nearly the same pixel, but still wrong.
|
|
*/
|
|
.wrolpi-message-error {
|
|
border: 1.5px dashed var(--message-color);
|
|
}
|
|
|
|
/* --------------------------------------------------------------- Progress */
|
|
|
|
/*
|
|
* Tall enough for the label inside it. At 16px a 14px line box left half a pixel of air
|
|
* above and below the text, which on the Status page is 44 bars of cramped 11px bold --
|
|
* legible only if you already knew what it said.
|
|
*
|
|
* `overflow: hidden` is what keeps the indeterminate band inside the bar. Without it the
|
|
* band slides from `margin-left: -35%` to `100%` in full view, so the animation ran from
|
|
* outside the left edge to outside the right one, painting over whatever sat beside it.
|
|
*/
|
|
.wrolpi-progress {
|
|
position: relative;
|
|
height: 1.375rem;
|
|
min-width: 7.5rem;
|
|
overflow: hidden;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
/*
|
|
* Bars stacked directly on top of each other need a gap; bars that a container already
|
|
* spaces do not. On the Status page the CPU and RAM bars are bare siblings in a Panel and
|
|
* were touching, while the drive-bandwidth bars each sit alone in a Grid column and get
|
|
* their spacing from the Grid.
|
|
*
|
|
* `* +` distinguishes those two cases by itself: it fires only when a bar actually follows
|
|
* something, so an only-child bar in a column is untouched.
|
|
*/
|
|
* + .wrolpi-progress {
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.wrolpi-progress-fill {
|
|
height: 100%;
|
|
background: var(--progress-color, var(--blue));
|
|
transition: width 0.2s linear;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.wrolpi-progress-fill {
|
|
transition: none;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Indeterminate: a sliding band, for work whose size is not known yet. Semantic called
|
|
* this `indicating`; without it an upload that has not reported its size sits at 0% and
|
|
* reads as stalled.
|
|
*/
|
|
.wrolpi-progress-indeterminate .wrolpi-progress-fill {
|
|
width: 35%;
|
|
animation: wrolpi-progress-slide 1.4s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes wrolpi-progress-slide {
|
|
from {
|
|
margin-left: -35%;
|
|
}
|
|
to {
|
|
margin-left: 100%;
|
|
}
|
|
}
|
|
|
|
/* Motion is the only signal here, so when it is suppressed the bar fills instead. */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.wrolpi-progress-indeterminate .wrolpi-progress-fill {
|
|
animation: none;
|
|
width: 100%;
|
|
opacity: 0.5;
|
|
}
|
|
}
|
|
|
|
.wrolpi-progress-text {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
/* 12px rather than 11: these labels carry units and device names, not just a number. */
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
color: var(--text);
|
|
font-variant-numeric: tabular-nums;
|
|
/*
|
|
* A long label is a real case, not a hypothetical: uploads label with a filename
|
|
* (Upload.js, FileBrowser.js) and a failed refresh labels with the error string.
|
|
* Without `nowrap` it wraps to a second line inside a 22px bar and the clip bisects
|
|
* it; with a hard clip and no ellipsis, `Complete: very-long-archive-name.tar.gz`
|
|
* ends mid-glyph and reads as broken rather than truncated.
|
|
*/
|
|
padding: 0 0.375rem;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
/*
|
|
* The percent text sits on both the filled and empty halves of the bar, so the
|
|
* fill has to stay light enough in light mode for dark text to read over it.
|
|
*
|
|
* MEASURED, and only light actually clears it. The label against the fill is
|
|
* 5.49:1 in light (the brightness filter below is what buys that) but 2.35 in
|
|
* dark, 2.48 in night and 2.74 in amber; against the empty track it is 5.2-15:1
|
|
* everywhere. Reaching AA over the fill needs the label drawn twice, clipped to
|
|
* the fill boundary, so each half gets its own color -- and in night not even
|
|
* that would do it, since `--text` is only ~4:1 against pure black and the fill
|
|
* is lighter than black by definition. Left as it is, deliberately, rather than
|
|
* asserted at a threshold chosen to pass.
|
|
*/
|
|
html[data-theme="light"] .wrolpi-progress {
|
|
background: var(--panel);
|
|
}
|
|
|
|
html[data-theme="light"] .wrolpi-progress-fill {
|
|
filter: brightness(1.75) saturate(0.55);
|
|
}
|
|
|
|
/* Night: a mostly-full bar must not become a large bright surface. */
|
|
html[data-theme="night"] .wrolpi-progress-fill,
|
|
html[data-theme="amber"] .wrolpi-progress-fill {
|
|
background: var(--muted);
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- Panels */
|
|
|
|
.wrolpi-panel {
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
padding: 0.875rem 1rem;
|
|
}
|
|
|
|
/* Danger zones are dashed in every theme; it doubles as a colorblind-safe cue. */
|
|
.wrolpi-panel-danger {
|
|
border: 1.5px dashed var(--danger);
|
|
}
|
|
|
|
/* ----------------------------------------------------------- Card posters */
|
|
/*
|
|
* The icon shown when a file has no poster, and the corner mark for a tagged
|
|
* file. Both live here rather than in Common.js so the card surface and the
|
|
* poster area agree in every theme.
|
|
*/
|
|
|
|
/*
|
|
* A poster is bounded by the card and keeps whatever ratio the source image has. It used
|
|
* to carry a fixed 290x163 box, which is wider than a card in a default grid (~233px at
|
|
* desktop widths), so `overflow: hidden` on the card cropped the sides off and the poster
|
|
* read as the wrong aspect ratio.
|
|
*
|
|
* Both caps are needed. Bounding the width alone lets a square thumbnail stand as tall as
|
|
* the card is wide, and a portrait book cover taller still, so the media band dwarfs the
|
|
* title it belongs to. `width` and `height` stay `auto` so the pair behaves as "contain":
|
|
* whichever cap bites first, the other axis comes down with it and the ratio survives.
|
|
*/
|
|
:root {
|
|
--card-poster-max-height: 12.5rem;
|
|
/* How far a disabled control fades. Semantic used 0.45 and it reads correctly against
|
|
every theme's panel; the tests read this rather than repeating the number. */
|
|
--disabled-opacity: 0.45;
|
|
}
|
|
|
|
/*
|
|
* The band is the full width of the card and centres the poster in it. The frame inside
|
|
* shrink-wraps the painted image, and is what corner chrome -- the tagged mark, a video's
|
|
* duration -- is positioned against. Pinning that chrome to the BAND puts it in the
|
|
* letterbox space beside a height-capped poster: 39px adrift on a book cover, 60px on a
|
|
* vertical video.
|
|
*
|
|
* The frame's own width falls out of the caps below. Its intrinsic contribution is the
|
|
* image's natural width already reduced by `max-height`, so a portrait cover contributes
|
|
* 155px and gets 155px; a 16:9 poster contributes 1280px and is flex-shrunk to the band,
|
|
* at which point `max-width: 100%` resolves against a width that is finally definite.
|
|
*/
|
|
.wrolpi-card-poster {
|
|
display: flex;
|
|
justify-content: center;
|
|
/*
|
|
* The band is the query container for the cap below, so the cap can be a fraction of the
|
|
* card's own width. Containment is safe here: the band's width comes from the card, never
|
|
* from the image inside it, and it has no fixed-position descendants.
|
|
*/
|
|
container-type: inline-size;
|
|
}
|
|
|
|
/*
|
|
* The card's text block.
|
|
*
|
|
* These lived as inline px in Card's JSX, which put them outside the interface scale --
|
|
* `fontSize: 13` is 13px whatever the root font-size is, so a card's title would have
|
|
* stayed put while the prose around it grew. Rules now, for the same reason every other
|
|
* size is one: a single place to retune, and ui-scale.test.js can see them.
|
|
*/
|
|
.wrolpi-card-body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
flex: 1;
|
|
padding: 0.625rem 0.75rem 0.75rem;
|
|
}
|
|
|
|
.wrolpi-card-title {
|
|
font-size: 0.8125rem;
|
|
font-weight: 600;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.wrolpi-card-meta {
|
|
font-size: 0.75rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
/* The foot: `auto` pushes the actions down so a row of cards lines its buttons up
|
|
however tall the titles above them got. ui-layout.cy.js measures that. */
|
|
.wrolpi-card-actions {
|
|
margin-top: auto;
|
|
padding-top: 0.5rem;
|
|
}
|
|
|
|
/* A grid of cards. The track width is the caller's `minWidth`, converted to rem in
|
|
CardGroup so a column scales with the type -- which is what returns a phone to one
|
|
card per row, as the pre-migration build had it. */
|
|
.wrolpi-card-group {
|
|
display: grid;
|
|
gap: 0.875rem;
|
|
}
|
|
|
|
.wrolpi-card-poster-frame {
|
|
position: relative;
|
|
display: block;
|
|
min-width: 0;
|
|
}
|
|
|
|
.wrolpi-card-poster img {
|
|
display: block;
|
|
width: auto;
|
|
height: auto;
|
|
max-width: 100%;
|
|
/*
|
|
* The cap grows with the card, so a landscape poster can reach the card's edges.
|
|
*
|
|
* A fixed height stops scaling: held to 220px, a 16:9 thumbnail is 391px wide, so any card
|
|
* wider than that centred it with empty panel on both sides. That is every video card at
|
|
* one column -- a phone, or any window under about 456px of content -- which is where the
|
|
* whole library is browsed on a phone. The cases below all used a 233px card, so it went
|
|
* unseen for the entire migration.
|
|
*
|
|
* `56.25cqw` is nine sixteenths of the band: exactly the height of a 16:9 image that fills
|
|
* the width, and the same number states the rule for every other ratio -- a poster may be
|
|
* as tall as a 16:9 slice of this card and no taller. So a book cover still reads as a
|
|
* band rather than a column, and now grows with the card instead of stopping at 220px.
|
|
*
|
|
* `max()`, not `min()`: on a narrow card the fraction is the smaller value and the floor
|
|
* has to win, or the cap would tighten as cards got smaller and crop a portrait cover.
|
|
*/
|
|
max-height: max(var(--card-poster-max-height), 56.25cqw);
|
|
}
|
|
|
|
.wrolpi-card-icon {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 7.5rem;
|
|
background: var(--head);
|
|
}
|
|
|
|
.wrolpi-card-tag {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
display: flex;
|
|
padding: 0.1875rem 0.25rem;
|
|
background: var(--green);
|
|
color: var(--btn-text);
|
|
}
|
|
|
|
/* Night and amber have no green to spend; the mark reads as an outline instead. */
|
|
html[data-theme="night"] .wrolpi-card-tag,
|
|
html[data-theme="amber"] .wrolpi-card-tag {
|
|
background: var(--panel);
|
|
color: var(--text);
|
|
border-right: 1px solid var(--border);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
/* ------------------------------------------------------------- Statistics */
|
|
/*
|
|
* A statistic carries its own spacing rather than relying on a parent to supply
|
|
* it, so that one dropped on a page by itself still sits clear of its
|
|
* neighbours. Semantic's `Statistic.Group` only spaced elements carrying
|
|
* Semantic's own classes, so without this the labels ran together into one
|
|
* unreadable line.
|
|
*/
|
|
|
|
.wrolpi-statistic {
|
|
display: inline-block;
|
|
vertical-align: top;
|
|
margin: 0 1.625rem 0.625rem 0;
|
|
}
|
|
|
|
/*
|
|
* The group is spacing and nothing else: no frame, no rules between cells, and
|
|
* no surface of its own, so the statistics sit on whatever they are dropped onto
|
|
* -- the page in one place, a Panel in another -- and pick up its color.
|
|
*
|
|
* That also settles a question this went round twice on. Ruling the cells needs
|
|
* a technique that survives wrapping, because `auto-fit` decides the number of
|
|
* rows at paint time: a `border-right` per cell rules columns but not rows, and
|
|
* a border-colored background behind a 1px gap rules both but paints every
|
|
* track no cell landed in. With no rules at all, neither problem exists.
|
|
*
|
|
* The gap is wider across than down. Two statistics side by side have nothing
|
|
* between them but air, so they need enough of it not to read as one column of
|
|
* text; stacked ones are already separated by the label's line.
|
|
*/
|
|
.wrolpi-statistic-group {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(9.375rem, 1fr));
|
|
gap: 1.125rem 1.875rem;
|
|
}
|
|
|
|
/*
|
|
* With the frame gone there is nothing holding the group off whatever precedes it, and on
|
|
* most pages that is a Header -- the first row of values sat directly against the heading.
|
|
*
|
|
* Only when something does precede it, though. Apps opens each of its Panels with a group,
|
|
* and a blanket `margin-top` there pushed the values down off the panel's own top padding
|
|
* while leaving the bottom where it was, so the whole panel read as lopsided.
|
|
*/
|
|
* + .wrolpi-statistic-group {
|
|
margin-top: 1.125rem;
|
|
}
|
|
|
|
/*
|
|
* A statistic that renders nothing -- Status omits the fan reading on a device
|
|
* with no fan connector -- must not hold a track open. The group cannot know in
|
|
* advance, since it would have to render the child to find out, so the cell is
|
|
* emitted either way and this takes the empty ones back out. It is also why the
|
|
* cells carry no inline style: there is nothing here for it to outrank.
|
|
*/
|
|
.wrolpi-statistic-cell:empty {
|
|
display: none;
|
|
}
|
|
|
|
/* Inside our own group the grid does the spacing, so the intrinsic margin goes. */
|
|
.wrolpi-statistic-cell .wrolpi-statistic {
|
|
display: block;
|
|
margin: 0;
|
|
}
|
|
|
|
.wrolpi-statistic-value {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
font-variant-numeric: tabular-nums;
|
|
line-height: 1.15;
|
|
}
|
|
|
|
.wrolpi-statistic-label {
|
|
font-size: 0.6875rem;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
margin-top: 0.125rem;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- Status */
|
|
/*
|
|
* One rule for four states in four themes. The color is the role the component
|
|
* chose, and the role already knows what it looks like here -- which is what
|
|
* replaced the three `html[data-theme="night"]` overrides that used to live below.
|
|
*/
|
|
.wrolpi-status {
|
|
font-size: 0.78125rem;
|
|
font-weight: 500;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.3125rem;
|
|
color: var(--status-color);
|
|
}
|
|
|
|
/*
|
|
* Failure is not left to color alone. Night and amber distinguish the roles by
|
|
* brightness, and brightness is the first thing lost on a dim screen or to a user
|
|
* who cannot separate these hues, so the loudest state also carries weight.
|
|
*/
|
|
.wrolpi-status-failed {
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- Tables */
|
|
|
|
/*
|
|
* The header is a surface, not just bold text.
|
|
*
|
|
* Mantine gives `thead` no background at all, so the header showed whatever was
|
|
* behind the table -- which is exactly what the unstriped body rows show. Bold
|
|
* type was the only thing separating the two, and against a striped body the
|
|
* header read as one more stripe. `--head` is the token for a recessed surface
|
|
* and is distinct from both the panel and the stripe in all four themes.
|
|
*/
|
|
.mantine-Table-thead .mantine-Table-th {
|
|
background: var(--head);
|
|
}
|
|
|
|
/*
|
|
* A full-strength rule under the header, because that is the one division in a
|
|
* table that carries meaning: everything above it names, everything below it is
|
|
* data. The rules between body rows are `--table-line` and much fainter -- see
|
|
* the note there. Applied to the last header row so a grouped header does not
|
|
* get a hard rule through the middle of itself.
|
|
*/
|
|
.mantine-Table-thead .mantine-Table-tr:last-child .mantine-Table-th {
|
|
border-bottom: 2px solid var(--border);
|
|
}
|
|
|
|
/*
|
|
* Row rules are quieter than any other border in the interface. They only have
|
|
* to let the eye track across a row; at `--border` strength, with a rule under
|
|
* every row, a long table turns into a stack of boxes.
|
|
*
|
|
* `html` for specificity, not decoration: Mantine declares this variable on its
|
|
* own hashed class, which ties with `.mantine-Table-table` and wins on source
|
|
* order because its stylesheet is imported after this one.
|
|
*/
|
|
html .mantine-Table-table {
|
|
--table-border-color: var(--table-line);
|
|
}
|
|
|
|
/*
|
|
* The outline around the whole table is not a row rule and keeps full strength;
|
|
* Mantine draws both from the same variable, so it has to be restated.
|
|
*/
|
|
html .mantine-Table-table[data-with-table-border] {
|
|
border-color: var(--border);
|
|
}
|
|
|
|
/* A sortable header: a real button, so the column is reachable by keyboard. */
|
|
.wrolpi-th-sort {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.3125rem;
|
|
background: none;
|
|
border: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
font: inherit;
|
|
color: inherit;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* The unsorted columns show a faint arrow: the affordance is visible, but it does
|
|
not compete with the column actually doing the sorting. */
|
|
.wrolpi-th-sort-idle {
|
|
opacity: 0.35;
|
|
}
|
|
|
|
.wrolpi-th-sort:hover .wrolpi-th-sort-idle {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.wrolpi-th-sort:focus-visible {
|
|
outline: 2px solid var(--blue);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
html[data-theme="night"] .wrolpi-th-sort:focus-visible,
|
|
html[data-theme="amber"] .wrolpi-th-sort:focus-visible {
|
|
outline-color: var(--text);
|
|
}
|
|
|
|
/* A failed row carries the danger border, matching failed messages. */
|
|
.wrolpi-row-failed > td {
|
|
border-top: 1.5px dashed var(--danger);
|
|
border-bottom: 1.5px dashed var(--danger);
|
|
}
|
|
|
|
.wrolpi-row-failed > td:first-child {
|
|
border-left: 1.5px dashed var(--danger);
|
|
}
|
|
|
|
.wrolpi-row-failed > td:last-child {
|
|
border-right: 1.5px dashed var(--danger);
|
|
}
|
|
|
|
/* Wide tables scroll inside their own container; the page never scrolls sideways. */
|
|
.wrolpi-table-scroll {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
/*
|
|
* A disabled button keeps its own color.
|
|
*
|
|
* Mantine repaints one flat grey -- background, text and border all replaced with
|
|
* `--mantine-color-disabled`. Semantic kept the color and dropped the whole control to
|
|
* `opacity: 0.45`, and that is the better signal: it says "not available" without also
|
|
* saying "no longer the delete button".
|
|
*
|
|
* The file browser is where the difference shows. Its footer is eight buttons, six of them
|
|
* disabled until something is selected, so the toolbar a user meets on arriving at /files was
|
|
* an undifferentiated grey row -- Delete, Rename, Move, Ignore and Tag all the same pixel.
|
|
* Color is how those are told apart at a glance.
|
|
*
|
|
* Mantine leaves the variables alone and overrides only the painted properties, so the color
|
|
* the call site asked for is still in `--button-bg` and can simply be reinstated. `--button-bd`
|
|
* is a whole border shorthand, not a color, so it is set with `border`.
|
|
*
|
|
* `:not([data-loading])` mirrors Mantine, and is not decoration. Mantine renders a loading
|
|
* button as `disabled={disabled || loading}`, so every busy control in the app is `:disabled`
|
|
* -- every APIButton mid-request, the refresh button, the flasher's connect. Its own grey
|
|
* repaint is careful to exclude them; without the same exclusion here, "working..." fades to
|
|
* 0.45 and reads as unavailable rather than busy.
|
|
*
|
|
* On specificity: Mantine's disabled paint is
|
|
* `.m_hash:where(:disabled:not([data-loading]), ...)`, and `:where()` contributes nothing, so
|
|
* it scores (0,1,0) -- a single class. The `html` prefix is not needed to beat THAT. It is
|
|
* needed to stay level with the app's other `html ...` overrides, and the night danger rule
|
|
* below shows why that matters: at equal specificity the later rule wins, which is how this
|
|
* one silently repainted night's dashed danger buttons until it was told not to.
|
|
*/
|
|
html .mantine-Button-root:disabled:not([data-loading]),
|
|
html .mantine-Button-root[data-disabled]:not([data-loading]) {
|
|
background: var(--button-bg);
|
|
color: var(--button-color);
|
|
border: var(--button-bd);
|
|
opacity: var(--disabled-opacity);
|
|
}
|
|
|
|
html .mantine-ActionIcon-root:disabled:not([data-loading]),
|
|
html .mantine-ActionIcon-root[data-disabled]:not([data-loading]) {
|
|
background: var(--ai-bg);
|
|
color: var(--ai-color);
|
|
border: var(--ai-bd);
|
|
opacity: var(--disabled-opacity);
|
|
}
|
|
|
|
/*
|
|
* Night keeps its dashed danger treatment when disabled.
|
|
*
|
|
* Night has no second hue to spend, so danger is encoded by a dashed border on a transparent
|
|
* fill rather than by a red fill. The general rule above repaints from `--button-bg`, which
|
|
* is the filled red Mantine put in the variable, and at (0,2,1) it tied with the night rule
|
|
* and won on source order -- turning the file browser's disabled Delete into a solid red chip
|
|
* in the one theme where a solid red chip is not what danger looks like.
|
|
*
|
|
* (0,3,1), so it does not depend on where it sits in the file. No `background`/`color`
|
|
* shorthand games: it restates the night treatment, and the fade comes from the rule above.
|
|
*/
|
|
html[data-theme="night"] .wrolpi-button-danger:disabled:not([data-loading]),
|
|
html[data-theme="night"] .wrolpi-button-danger[data-disabled]:not([data-loading]) {
|
|
background: transparent;
|
|
color: var(--danger);
|
|
border: 1.5px dashed var(--danger);
|
|
}
|
|
|
|
/*
|
|
* A button whose only content is an icon is square, like the ActionIcon it sits beside.
|
|
*
|
|
* A Button is as wide as its content plus its horizontal padding; an ActionIcon is square by
|
|
* construction. So a row mixing the two at one nominal size came out at different widths --
|
|
* the map's pin actions are edit (Button), add-to-playlist (IconButton) and delete (Button),
|
|
* and measured 46, 30 and 46 wide. Their heights already agree, because ActionIcon's scale
|
|
* was remapped onto Button's; this is the other axis of the same alignment.
|
|
*
|
|
* `--button-height` is Mantine's own per-size height, so the two stay in step through every
|
|
* size without a table here to keep current.
|
|
*/
|
|
html .mantine-Button-root[data-icon-only] {
|
|
width: var(--button-height);
|
|
padding: 0;
|
|
}
|