mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
283 occurrences across 23 files, all of them mine and all of them in the theme/UI work: comments, test names, and a handful of local identifiers (`colours`, `roleColours`, `awaitColoured`) plus one test fixture tag named 'Uncoloured'. Nothing was required to keep the other spelling. Checked before replacing: no CSS custom property or class name contains it, no library prop or API key does, and every string holding it is a test name or our own copy. The renamed identifiers were checked for collisions with an existing `color`/`colors` in the same file first -- there were none. Verified as a pure rename: 262 lines changed, and normalising the spelling on every removed line reproduces the added line exactly, for all 262. So the diff contains no other edit hiding in it. 1134 jest / 64 suites, 271 cypress component tests with the same 12 pre-existing Semantic-era failures in Tags/Channels/Download/Inventory, clean npm run build.
1584 lines
52 KiB
CSS
1584 lines
52 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(230px, 1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.wrolpi-theme-option {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
align-items: flex-start;
|
|
text-align: left;
|
|
padding: 10px 12px;
|
|
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: 9px 11px;
|
|
}
|
|
|
|
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: 7px;
|
|
font-weight: 600;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.wrolpi-theme-option-description {
|
|
font-size: 12px;
|
|
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: 14px;
|
|
padding-top: 12px;
|
|
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: 12px;
|
|
border-bottom: 1px solid var(--border);
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.wrolpi-tabs-links {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.wrolpi-tabs-right {
|
|
padding-bottom: 6px;
|
|
}
|
|
|
|
.wrolpi-tab {
|
|
display: inline-block;
|
|
padding: 8px 14px;
|
|
color: var(--muted);
|
|
text-decoration: none;
|
|
font-size: 13px;
|
|
/*
|
|
* 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: 8px;
|
|
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: 8px 0;
|
|
background: none;
|
|
border: none;
|
|
outline: none;
|
|
color: var(--text);
|
|
font: inherit;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.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.
|
|
*/
|
|
max-height: min(60vh, 480px);
|
|
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, 420px);
|
|
}
|
|
|
|
.wrolpi-searchbox-group-name {
|
|
padding: 6px 10px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
background: var(--head);
|
|
}
|
|
|
|
.wrolpi-searchbox-result {
|
|
padding: 7px 10px;
|
|
cursor: pointer;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.wrolpi-searchbox-result-active {
|
|
background: var(--head);
|
|
}
|
|
|
|
.wrolpi-searchbox-result-title {
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.wrolpi-searchbox-result-description {
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.wrolpi-searchbox-empty {
|
|
padding: 10px;
|
|
font-size: 12px;
|
|
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: 10px;
|
|
/* 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: 18px calc(var(--mantine-spacing-md) * -1) calc(var(--mantine-spacing-md) * -1);
|
|
padding: 12px var(--mantine-spacing-md);
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
/* A stack of form rows, replacing Semantic's `ui stackable grid` in edit forms. */
|
|
.wrolpi-form-rows {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
}
|
|
|
|
/* ------------------------------------------------------------- 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: 7px 9px;
|
|
background: var(--panel);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
font: inherit;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.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: 13px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.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 6px 0 9px;
|
|
background: var(--head);
|
|
border-right: 1px solid var(--border);
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
user-select: none;
|
|
}
|
|
|
|
.wrolpi-path-input-control input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding: 7px 9px;
|
|
background: none;
|
|
border: none;
|
|
outline: none;
|
|
color: var(--text);
|
|
font: inherit;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.wrolpi-path-input-control input::placeholder {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.wrolpi-path-input-control input:disabled {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.wrolpi-path-input-description {
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
margin-top: 3px;
|
|
}
|
|
|
|
.wrolpi-path-input-error {
|
|
font-size: 12px;
|
|
color: var(--danger);
|
|
margin-top: 3px;
|
|
}
|
|
|
|
/* ---------------------------------------------------------- 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;
|
|
}
|
|
|
|
/* --------------------------------------------------------------- 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: 4px;
|
|
padding: 5px 10px;
|
|
font-size: 12.5px;
|
|
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: 16px;
|
|
/* Tags wrap, and a wrapped row was sitting on the row above it. */
|
|
margin-bottom: 5px;
|
|
/*
|
|
* 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: 13px;
|
|
}
|
|
|
|
.wrolpi-tag::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 50%;
|
|
right: 100%;
|
|
width: 20px;
|
|
height: 20px;
|
|
/*
|
|
* 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: 5px;
|
|
height: 5px;
|
|
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 10px;
|
|
}
|
|
|
|
/*
|
|
* 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: 22px;
|
|
}
|
|
|
|
/* The heading and its trailing control share one row. */
|
|
.wrolpi-header-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
/*
|
|
* 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: 13px;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.wrolpi-header-dividing {
|
|
border-bottom: 1px solid var(--border);
|
|
padding-bottom: 6px;
|
|
}
|
|
|
|
.wrolpi-header-text {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
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: 26px;
|
|
}
|
|
|
|
.wrolpi-header-h2 {
|
|
font-size: 21px;
|
|
}
|
|
|
|
.wrolpi-header-h3 {
|
|
font-size: 17px;
|
|
}
|
|
|
|
.wrolpi-header-h4 {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.wrolpi-header-h5 {
|
|
font-size: 13px;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
.wrolpi-header-sub {
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
margin-top: 3px;
|
|
}
|
|
|
|
/*
|
|
* 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: 10px;
|
|
border: 1px solid var(--border);
|
|
border-left: 3px solid var(--message-color, var(--border));
|
|
background: var(--panel);
|
|
padding: 10px 14px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.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: 6px;
|
|
}
|
|
|
|
.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: 2px;
|
|
}
|
|
|
|
.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: 22px;
|
|
min-width: 120px;
|
|
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: 8px;
|
|
}
|
|
|
|
.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: 12px;
|
|
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 6px;
|
|
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: 14px 16px;
|
|
}
|
|
|
|
/* 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: 200px;
|
|
/* 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;
|
|
}
|
|
|
|
.wrolpi-card-poster-frame {
|
|
position: relative;
|
|
display: block;
|
|
min-width: 0;
|
|
}
|
|
|
|
.wrolpi-card-poster img {
|
|
display: block;
|
|
width: auto;
|
|
height: auto;
|
|
max-width: 100%;
|
|
max-height: var(--card-poster-max-height);
|
|
}
|
|
|
|
.wrolpi-card-icon {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 120px;
|
|
background: var(--head);
|
|
}
|
|
|
|
.wrolpi-card-tag {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
display: flex;
|
|
padding: 3px 4px;
|
|
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 26px 10px 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(150px, 1fr));
|
|
gap: 18px 30px;
|
|
}
|
|
|
|
/*
|
|
* 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: 18px;
|
|
}
|
|
|
|
/*
|
|
* 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: 24px;
|
|
font-weight: 600;
|
|
font-variant-numeric: tabular-nums;
|
|
line-height: 1.15;
|
|
}
|
|
|
|
.wrolpi-statistic-label {
|
|
font-size: 11px;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- 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: 12.5px;
|
|
font-weight: 500;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
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: 5px;
|
|
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;
|
|
}
|