mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 10:51:36 -04:00
Repos keeps local mirrors of git repositories, cloned over https and kept up to date on a schedule. - A Repo is a Collection (kind='repo') plus a `repository` row. The new `git` downloader clones it, then updates it (weekly by default, or Never: the clone is kept as it is and only updated on request). - Repos are saved in `repos/<tag>/<name>` (the Repos Directory, ignored by default) or a directory the user chooses. An existing clone can be imported where it is. repos.yaml is the source of truth. - A GitHub-like page shows each repo's README, files, and history, with a ZIP download. READMEs are searchable under the global search's Other tab. - git is hardened against hostile repos and configs: https only, pinned addresses with no private addresses (for the repo and its submodules), sanitized configs, no hooks or links, and only clones WROLPi made or imported are updated. - Tagging a Repo moves its clone as one directory. Repo changes wait for repos.yaml to be imported at startup. - The file mover groups unindexed files with no primary file as one FileGroup, and never replaces a file at the destination. A failed tag move restores the Collection's tag. A Collection's directory cannot leave the media directory. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
843 lines
26 KiB
CSS
843 lines
26 KiB
CSS
:root {
|
|
/* Shared with the toast offset below, so the two cannot drift apart. */
|
|
--navbar-height: 3.4em;
|
|
}
|
|
|
|
/* Nav bar: a hand-rolled flexbox bar. The user-chosen background and the foreground
|
|
measured against it are set inline (see themes/navColors); this stylesheet only
|
|
supplies structure and hover affordances. */
|
|
.wrolpi-navbar {
|
|
display: flex;
|
|
align-items: stretch;
|
|
/*
|
|
* Wraps rather than clipping. With this many items the bar overflows a 1000px
|
|
* viewport, and `nowrap` cut "Admin" off the right edge -- the one link a user needs
|
|
* to reach the settings that fix a broken WROLPi.
|
|
*/
|
|
flex-wrap: wrap;
|
|
min-height: var(--navbar-height);
|
|
padding: 0 0.25em;
|
|
}
|
|
|
|
/*
|
|
* Toasts start below the navigation bar instead of on top of it.
|
|
*
|
|
* They are fixed at `top: var(--mantine-spacing-md)` -- 16px -- while the bar occupies the
|
|
* first 54px of the page, so every toast landed over the bar's right-hand corner: the search
|
|
* glyph, the hamburger, Help and Admin. For as long as the toast was up those controls could
|
|
* not be clicked, and an error toast is exactly when a user reaches for Admin.
|
|
*
|
|
* It went unnoticed until the search glyph was given a proper hit area: at 18px it sat in the
|
|
* gap under the toast, and growing it to the height of the bar moved it into the overlap.
|
|
* The toast was always covering that corner; there was just nothing there to hit.
|
|
*
|
|
* `:where()` gives Mantine's own rule zero specificity, so one class is enough to win, but
|
|
* the attribute is matched too -- only the top-anchored positions need moving, and a future
|
|
* switch to `bottom-right` should not silently inherit a top offset.
|
|
*/
|
|
html .mantine-Notifications-root[data-position^='top'] {
|
|
top: calc(var(--navbar-height) + var(--mantine-spacing-md, 1rem));
|
|
}
|
|
|
|
.wrolpi-navbar-right {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-left: auto;
|
|
gap: 0.2em;
|
|
}
|
|
|
|
.wrolpi-navbar-link,
|
|
.wrolpi-navbar-link-button {
|
|
display: flex;
|
|
align-items: center;
|
|
/* Tightened from 1.1em: the bar has eleven links plus nine indicators. */
|
|
padding: 0 0.8em;
|
|
color: inherit;
|
|
text-decoration: none;
|
|
white-space: nowrap;
|
|
background: none;
|
|
border: none;
|
|
font: inherit;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.wrolpi-navbar-link:hover,
|
|
.wrolpi-navbar-link-button:hover {
|
|
/* A translucent black works on every navbar color, light or dark. */
|
|
background: rgba(0, 0, 0, 0.12);
|
|
}
|
|
|
|
/*
|
|
* The tab for the section the user is in.
|
|
*
|
|
* react-router marks it with `active` and `aria-current` on its own, and always did -- the
|
|
* mark reached the DOM and nothing here drew it, so every tab looked identical and clicking
|
|
* Videos appeared to leave the bar untouched.
|
|
*
|
|
* The underline is a pseudo-element in `currentColor`, not a border: `currentColor` follows
|
|
* the foreground the bar computed for whatever navbar color the user picked, so this works
|
|
* on all twelve of them (see `navColorNames`) and in all four themes without naming a color;
|
|
* and a pseudo-element adds no height, where a border would jog the whole row as the mark
|
|
* moved between tabs. The darker wash is the hover treatment deepened, so the two read as
|
|
* the same idea.
|
|
*/
|
|
.wrolpi-navbar-link.active {
|
|
position: relative;
|
|
background: rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.wrolpi-navbar-link.active::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
/* rem, not px, so the mark grows with the --ui-scale setting like the text above it. */
|
|
height: 0.1875rem;
|
|
background: currentColor;
|
|
}
|
|
|
|
/*
|
|
* The same tabs inside the overflow "More" menu and the mobile hamburger, which are the
|
|
* only place most of them appear on a phone. Those render in a portal, outside the bar,
|
|
* so they take theme tokens rather than the bar's foreground.
|
|
*/
|
|
.mantine-Menu-item.active {
|
|
background: var(--head);
|
|
font-weight: 600;
|
|
}
|
|
|
|
/*
|
|
* Everything in the bar takes the bar's foreground.
|
|
*
|
|
* `color: inherit` on the links alone was not enough, because two of the things in the
|
|
* corner do not inherit at all and both came out blue on every bar in every theme:
|
|
*
|
|
* - An anchor takes `a {color: var(--blue)}` from tokens.css, which outranks an inherited
|
|
* value however the bar sets it. The search glyph is an anchor, and so is every status
|
|
* icon that links to /admin -- those only looked right because they carry an inline
|
|
* color of their own, which still wins here and is meant to.
|
|
* - A Mantine ActionIcon paints `--ai-color`, its own themed blue. The hamburger and the
|
|
* theme toggle are ActionIcons. The variable is no use as a hook: Mantine writes it
|
|
* INLINE on the element, so a stylesheet declaring it loses every time (the same trap
|
|
* as `--group-justify` on Pagination). The painted property has to be set instead, and
|
|
* `html` lifts this over Mantine's own single-class rule.
|
|
*
|
|
* Measuring the foreground against the bar is worth nothing if the icons opt out of it.
|
|
*/
|
|
.wrolpi-navbar a,
|
|
html .wrolpi-navbar .mantine-ActionIcon-root {
|
|
color: inherit;
|
|
}
|
|
|
|
/* The bar's own hover, rather than a blue tint from Mantine's palette. */
|
|
html .wrolpi-navbar .mantine-ActionIcon-root:hover {
|
|
background: rgba(0, 0, 0, 0.12);
|
|
color: inherit;
|
|
}
|
|
|
|
/*
|
|
* One status indicator's slot. Laid out rather than nudged: see NavIconWrapper in Nav.js
|
|
* for the 0.8em top margin this replaces and why it looked correct on half the indicators.
|
|
*/
|
|
.wrolpi-navbar-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
/* A sliver only: the corner's gap and the slots' own padding already separate the
|
|
icons, and anything more has to fit a 320px phone times the number of slots. */
|
|
margin-left: 0.25em;
|
|
/* Full bar height, so the anchor inside has somewhere to stretch to. */
|
|
align-self: stretch;
|
|
}
|
|
|
|
/*
|
|
* An icon-only trigger in the corner gets a link's hit area, not its glyph's.
|
|
*
|
|
* The share glyph already computed `cursor: pointer` -- it is an `<a href>` -- but the
|
|
* target was the 24px box the icon occupies, so the pointer appeared only if you found it
|
|
* exactly, and there was no hover highlight to aim at. Stretched and padded, it behaves
|
|
* like Help and Admin beside it.
|
|
*
|
|
* `align-self` rather than `align-items: stretch` on the wrapper: the IconButtons in the
|
|
* corner have an explicit height, and a stretch container places an item it cannot stretch
|
|
* at the start -- which would push the hotspot and theme glyphs back off centre.
|
|
*/
|
|
.wrolpi-navbar-icon > a {
|
|
align-self: stretch;
|
|
padding: 0 0.5em;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.wrolpi-navbar-icon > a:hover {
|
|
background: rgba(0, 0, 0, 0.12);
|
|
}
|
|
|
|
/*
|
|
* An anchor in the corner hugs its glyph instead of a text line box.
|
|
*
|
|
* A line box reserves descender space for text that is not there, which leaves an icon
|
|
* sitting about 3px above the centre of the box being centred -- the share and search
|
|
* glyphs, but not the hotspot or theme picker, which are square ActionIcons that centre
|
|
* their own glyph. That difference is what made a single top margin look right on some
|
|
* indicators and wrong on others.
|
|
*/
|
|
.wrolpi-navbar-right a {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
/*
|
|
* A link in the corner fills the bar's height, as one outside it does.
|
|
*
|
|
* Help and Admin highlighted only a box around their text while Videos and Statistics
|
|
* highlighted the full height. Same class and same rules; the difference is the parent.
|
|
* The bar stretches its children, but the corner is a nested flex container that centres
|
|
* its own, so a link inside it was sized to its content. The hover background is painted
|
|
* on the link box, so this is the hover affordance itself and not just its outline.
|
|
*/
|
|
.wrolpi-navbar-right .wrolpi-navbar-link,
|
|
.wrolpi-navbar-right .wrolpi-navbar-link-button {
|
|
align-self: stretch;
|
|
}
|
|
|
|
::-ms-clear {
|
|
display: none;
|
|
}
|
|
|
|
pre.wrap-text {
|
|
white-space: pre-wrap; /* Since CSS 2.1 */
|
|
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
|
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
|
}
|
|
|
|
/*
|
|
* A video's length, pinned to the corner of its poster.
|
|
*
|
|
* Tokens, not a literal white-on-black. The poster under it is filtered by night mode -- it is a
|
|
* `.media` leaf -- but this chip is drawn beside the image rather than inside it, so it was never
|
|
* filtered: a white tile on a card whose picture had gone dim red, which is the brightest thing on
|
|
* the videos page in the theme meant to have nothing bright on it.
|
|
*
|
|
* Opaque, because it sits on an arbitrary poster and has to be legible over any of them.
|
|
*/
|
|
.duration-overlay {
|
|
position: absolute;
|
|
top: 0.25rem;
|
|
right: 0.25rem;
|
|
color: var(--text);
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
padding: 0px 0.25rem;
|
|
border-radius: 0.1875rem;
|
|
}
|
|
|
|
.no-link-underscore {
|
|
text-decoration: none !important;
|
|
}
|
|
|
|
/*
|
|
* A card's title and its author/channel/domain line are links, but they are the card's
|
|
* text -- they take the body color rather than the link color, so a grid of cards reads
|
|
* as titles and not as a wall of blue. This was a literal `black` until the themes
|
|
* arrived, which left every title at about 1.1:1 on the dark, night and amber panels.
|
|
*/
|
|
.card-link {
|
|
color: var(--text);
|
|
}
|
|
|
|
/*
|
|
* The hand-rolled `.toggle` / `.slider` switch is gone.
|
|
*
|
|
* Nothing referenced either class: `Toggle` is a Mantine Switch, and the only remaining
|
|
* matches for the words were the `classList.toggle` DOM method and the controller's own
|
|
* templates, which do not load this stylesheet.
|
|
*
|
|
* It is deleted rather than finished because it was already half-converted and broken: the
|
|
* track and knob became rem while the checked-state travel stayed `translateX(26px)`, so on a
|
|
* 54px track the thumb would have stopped ~4px short. Dead CSS that is also wrong is a trap
|
|
* for whoever revives it, and it also hardcoded #6435C9 and white, which no theme can reach.
|
|
*/
|
|
|
|
/* File preview Modal.Actions toolbar.
|
|
Single layout that works at every viewport — buttons keep their order at any width.
|
|
On narrow screens, the tags selector wraps to its own line while the button groups stay together. */
|
|
.preview-toolbar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 0.5em;
|
|
/* Fill the actions row: `.wrolpi-modal-actions` packs its children to the right, which
|
|
is where a confirming button belongs but strands this toolbar. At full width the
|
|
toolbar's own layout wins and everything reads from the left. */
|
|
flex: 1 1 100%;
|
|
}
|
|
|
|
.preview-toolbar-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25em;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.preview-toolbar-tags {
|
|
/* rem so the basis grows with the tags it holds. */
|
|
flex: 1 1 12.5rem;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* On a phone the tags selector always gets its own line rather than squeezing in beside
|
|
the buttons. 700px is the app's `tablet` breakpoint (`AppMedia` in contexts.js). */
|
|
@media (max-width: 699px) {
|
|
.preview-toolbar-tags {
|
|
flex-basis: 100%;
|
|
}
|
|
}
|
|
|
|
/* Caps preview content to fit inside the file-preview Modal.
|
|
300px reserves room for pathContent <pre>, Modal.Header, Modal.Actions row,
|
|
inner content (e.g. STL dimensions banner), and modal padding.
|
|
Use 100dvh so mobile browser chrome (URL bar) is accounted for. */
|
|
.preview-fit {
|
|
width: 100%;
|
|
height: calc(100dvh - 18.75rem);
|
|
max-height: calc(100dvh - 18.75rem);
|
|
min-height: 15rem;
|
|
overflow: hidden;
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* Fresnel (Media) wraps iframes in a div which would otherwise shrink to the
|
|
default iframe size; make it fill the preview so the iframe's 100% works. */
|
|
.preview-fit .fresnel-container {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* A markdown file preview, formatted or raw. Scrolls rather than fitting like .preview-fit, because
|
|
it is a document to read, not media to frame. Colors come from the theme tokens. */
|
|
.markdown-preview {
|
|
height: calc(100dvh - 21rem);
|
|
min-height: 15rem;
|
|
overflow: auto;
|
|
margin-top: 0.5em;
|
|
padding: 0 0.25em;
|
|
}
|
|
|
|
/* A repo's README is part of its page, not a modal; it grows with its content. */
|
|
.repo-readme .markdown-preview {
|
|
height: auto;
|
|
min-height: 0;
|
|
overflow: visible;
|
|
}
|
|
|
|
.markdown-raw {
|
|
margin: 0;
|
|
white-space: pre-wrap;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.markdown-body {
|
|
max-width: 50rem;
|
|
line-height: 1.6;
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
.markdown-body > :first-child {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.markdown-body h1, .markdown-body h2 {
|
|
padding-bottom: 0.25em;
|
|
border-bottom: 1px solid var(--table-line);
|
|
}
|
|
|
|
.markdown-body img {
|
|
max-width: 100%;
|
|
}
|
|
|
|
.markdown-body blockquote {
|
|
margin: 0 0 1em;
|
|
padding: 0 1em;
|
|
color: var(--muted);
|
|
border-left: 0.25em solid var(--border);
|
|
}
|
|
|
|
.markdown-body code {
|
|
padding: 0.1em 0.3em;
|
|
border-radius: 0.25em;
|
|
background-color: var(--head);
|
|
}
|
|
|
|
.markdown-body pre {
|
|
padding: 0.75em 1em;
|
|
overflow-x: auto;
|
|
border-radius: 0.25em;
|
|
background-color: var(--head);
|
|
}
|
|
|
|
.markdown-body pre code {
|
|
padding: 0;
|
|
background-color: transparent;
|
|
}
|
|
|
|
.markdown-body table {
|
|
display: block;
|
|
max-width: 100%;
|
|
overflow-x: auto;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.markdown-body th, .markdown-body td {
|
|
padding: 0.3em 0.75em;
|
|
border: 1px solid var(--table-line);
|
|
}
|
|
|
|
.markdown-body hr {
|
|
border: none;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
td.file-path {
|
|
cursor: pointer;
|
|
}
|
|
|
|
/*
|
|
* The row is an icon followed by a filename, as inline siblings. An inline SVG
|
|
* sits on the text baseline by default, so a 24px glyph next to 13px text hangs
|
|
* well above it and the name looks like it is resting on the icon's floor.
|
|
* `middle` centres the glyph against the text instead.
|
|
*
|
|
* The margin is the gap that was never there: the folder icon touched the first
|
|
* letter of its own name, and two icons (a folder and its ignored marker) touched
|
|
* each other.
|
|
*/
|
|
td.file-path > svg {
|
|
vertical-align: middle;
|
|
margin-right: 0.35em;
|
|
}
|
|
|
|
textarea.otp {
|
|
width: 100%;
|
|
color: #333;
|
|
}
|
|
|
|
.card-icon {
|
|
min-height: 10.1875rem;
|
|
text-align: center;
|
|
padding-top: 3.3125rem;
|
|
}
|
|
|
|
.clickable {
|
|
cursor: pointer;
|
|
}
|
|
|
|
/*
|
|
* A table that truncates to the width it has rather than growing past it: the columns divide
|
|
* the table's width, and `column-ellipsis` gives up what it cannot show. A column that must
|
|
* fit something exact states a width on its header cell; the rest is the ellipsis column's.
|
|
*
|
|
* `table.`, not the bare class: Mantine sets `table-layout` on its own `.mantine-Table-table`
|
|
* at the same specificity and later in the cascade, so the bare class loses -- silently, with
|
|
* every other property here still applying.
|
|
*/
|
|
table.table-ellipsis {
|
|
width: 100%;
|
|
table-layout: fixed;
|
|
}
|
|
|
|
.table-ellipsis td.column-ellipsis {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
/*
|
|
* Column widths for the downloads tables. Phone first, and the phone is 375px: below the
|
|
* breakpoint each column is cut to what it must hold -- its header's longest word, the label
|
|
* wrapping, or its buttons -- and the URL takes what is left, which is little. Above it the
|
|
* labels get their own line back and the URL gets the rest.
|
|
*
|
|
* 700px is the app's `tablet` breakpoint (`AppMedia` in contexts.js). `em` here is the
|
|
* table's own font (~15.4px), not the root's.
|
|
*/
|
|
/*
|
|
* A header too long for a phone carries both forms and shows one. The short word is what the
|
|
* mobile column widths below are sized for; the full words return at the breakpoint.
|
|
*/
|
|
.table-ellipsis th .label-long {
|
|
display: none;
|
|
}
|
|
|
|
@media (min-width: 700px) {
|
|
.table-ellipsis th .label-long {
|
|
display: inline;
|
|
}
|
|
|
|
.table-ellipsis th .label-short {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.table-ellipsis th.col-select {
|
|
/* 22px of checkbox plus 11px of cell padding either side. */
|
|
width: 3em;
|
|
}
|
|
|
|
.table-ellipsis th.col-frequency {
|
|
width: 4.2em;
|
|
}
|
|
|
|
.table-ellipsis th.col-completed {
|
|
width: 5.5em;
|
|
}
|
|
|
|
.table-ellipsis th.col-completed-plain {
|
|
width: 4.2em;
|
|
}
|
|
|
|
.table-ellipsis th.col-next {
|
|
width: 3.9em;
|
|
}
|
|
|
|
/* A `ButtonGroup` cannot wrap -- its buttons are joined -- so this holds all three of them
|
|
(delete, edit, retry on a failed download) at every width. */
|
|
.table-ellipsis th.col-control-group {
|
|
width: 9.5em;
|
|
}
|
|
|
|
/* A `.wrolpi-button-row` wraps, so on a phone this holds one button and the second drops
|
|
beneath it. It is sized by the word "Control" rather than by the button, which is wider. */
|
|
.table-ellipsis th.col-control-row {
|
|
width: 5.2em;
|
|
}
|
|
|
|
@media (min-width: 700px) {
|
|
.table-ellipsis th.col-frequency {
|
|
width: 12em;
|
|
}
|
|
|
|
.table-ellipsis th.col-completed {
|
|
width: 9.5em;
|
|
}
|
|
|
|
.table-ellipsis th.col-completed-plain {
|
|
width: 8.5em;
|
|
}
|
|
|
|
.table-ellipsis th.col-next {
|
|
width: 4em;
|
|
}
|
|
|
|
.table-ellipsis th.col-control-row {
|
|
width: 7.5em;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* A header wraps instead of running into the next column: `.wrolpi-th-sort` is nowrap, which a
|
|
* fixed column cannot widen for, so a long label printed over its neighbour.
|
|
*
|
|
* `min-width: 0` as well as the wrap -- the label is an anonymous flex item whose automatic
|
|
* minimum is its longest word, and without this it still refuses to go below that word. A
|
|
* column narrower than its longest word is a width to fix at the call site.
|
|
*/
|
|
.table-ellipsis th .wrolpi-th-sort {
|
|
display: flex;
|
|
white-space: normal;
|
|
min-width: 0;
|
|
text-align: left;
|
|
}
|
|
|
|
/*
|
|
* A header is clipped rather than painted over its neighbour. Wrapping cannot save a
|
|
* single-word label in the elastic column, which may be narrower than the word itself.
|
|
*/
|
|
.table-ellipsis th {
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* `ButtonGroup` is a nowrap flex row whose children shrink; `col-control-group` is sized for
|
|
the whole group, so the buttons in it must not. */
|
|
.table-ellipsis td .mantine-ButtonGroup-group > * {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.card-title-ellipsis {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.search-container {
|
|
display: flex;
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.search-input {
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.video-player {
|
|
display: block;
|
|
margin: 0 auto; /* Center the video horizontally */
|
|
}
|
|
|
|
.audio-poster-container {
|
|
position: relative;
|
|
display: block;
|
|
text-align: center;
|
|
}
|
|
|
|
.audio-poster-overlay {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: rgba(255, 255, 255, 0.85);
|
|
text-shadow: 0 0 20px rgba(0, 0, 0, 0.6);
|
|
pointer-events: none;
|
|
}
|
|
|
|
table.ratio-table td.equal-width {
|
|
width: 48%;
|
|
}
|
|
|
|
table.ratio-table td.min-width {
|
|
width: auto;
|
|
}
|
|
|
|
/*
|
|
* Sticky footer for the file browser.
|
|
*
|
|
* The surface is a token. It was two hardcoded greys with `.inverted` selecting the dark
|
|
* one, and once nothing set `.inverted` any more the footer stayed at near-white `#f9fafb`
|
|
* in dark, night and amber. In night mode a white bar pinned to the bottom of the screen
|
|
* costs the user the dark adaptation the whole theme exists to protect. A token has no
|
|
* such failure mode: there is one rule and each theme fills it in.
|
|
*/
|
|
.sticky-footer {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
padding: 0.5em 1em;
|
|
background-color: var(--panel);
|
|
border-top: 1px solid var(--border);
|
|
z-index: 100;
|
|
/*
|
|
* A row with gaps, like every other group of buttons in the app.
|
|
*
|
|
* This was a plain block, so the buttons -- inline-block elements with no whitespace
|
|
* between them, because JSX drops whitespace-only lines -- sat flush against each other
|
|
* in one unbroken slab. Everywhere else a group of buttons is a flex row with a gap;
|
|
* the file browser's footer is hand-rolled and never got one. It wraps because the bar
|
|
* carries eight controls and a phone cannot hold them.
|
|
*/
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 0.5em;
|
|
}
|
|
|
|
/*
|
|
* Drag selection visual feedback for FileBrowser: a blue tint over the rows being taken,
|
|
* red over the rows being dropped.
|
|
*
|
|
* Each had a second, stronger rule under `.inverted` for the dark themes. Nothing has set
|
|
* that class since ThemeProvider stopped supplying it, so only the light tint has been
|
|
* drawn for some time -- and 15% of a fixed blue over a dark row is close to invisible,
|
|
* which is the one moment the user needs to see what they have hold of.
|
|
*
|
|
* A token mixed with transparent instead: the hue follows the theme, and the strength can
|
|
* go up without pinning it to a literal that only suits one background.
|
|
*/
|
|
tr.drag-selecting td {
|
|
background-color: color-mix(in srgb, var(--blue) 22%, transparent) !important;
|
|
}
|
|
|
|
tr.drag-deselecting td {
|
|
background-color: color-mix(in srgb, var(--red) 22%, transparent) !important;
|
|
}
|
|
|
|
/* Prevent text selection during drag */
|
|
.file-browser-dragging {
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
-moz-user-select: none;
|
|
-ms-user-select: none;
|
|
}
|
|
|
|
/* Filter input and collapse-all button: right-aligned on desktop, full width on mobile (below). */
|
|
.file-browser-filter-container {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
/* The gap is the container's. The collapse-all button carried `margin-left: 0.5em` for it,
|
|
which is the same thing one element to the right of where it belongs. */
|
|
gap: 0.5em;
|
|
/* No `margin-bottom`: this is a top-level block of the Files page, and the page's stack
|
|
spaces it (see `.wrolpi-stack`). Its own 0.5em was added to that. */
|
|
}
|
|
|
|
/*
|
|
* Double the default input width on desktop.
|
|
*
|
|
* The selector was `.ui.input.file-browser-filter`, naming a wrapper the old input
|
|
* component rendered around the field. Nothing carries `ui input` any more, so this rule
|
|
* and the mobile one below it had been matching nothing at all: the filter was rendering
|
|
* at the browser's default width on a bar laid out to give it 34em.
|
|
*
|
|
* The class lands on ActionInput's wrapper, which is the field and its clear button
|
|
* together -- that pair is the control being sized, not the text box alone.
|
|
*
|
|
* Qualified by the container so it carries two classes. `.wrolpi-action-input` sets
|
|
* `width: 100%` in ui.css, which is one class like `.file-browser-filter` is, and ui.css
|
|
* is loaded after this file -- so a bare class selector here loses the tie and the field
|
|
* fills the row anyway. Specificity, not source order, is what should decide it.
|
|
*/
|
|
.file-browser-filter-container .file-browser-filter {
|
|
width: 34em;
|
|
max-width: 100%;
|
|
}
|
|
|
|
@media only screen and (max-width: 700px) {
|
|
.hide-on-mobile {
|
|
display: none !important;
|
|
}
|
|
|
|
.file-browser-filter-container {
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
/* Fill the row, but stay on one line with the collapse-all button. */
|
|
.file-browser-filter-container .file-browser-filter {
|
|
flex: 1 1 auto;
|
|
width: auto;
|
|
}
|
|
}
|
|
|
|
/* Inventory "PDF" export: the printable table is hidden on screen and becomes the only
|
|
visible content when printing, so a user can use the browser dialog to Save as PDF. */
|
|
.inventory-print {
|
|
display: none;
|
|
}
|
|
|
|
@media print {
|
|
body * {
|
|
visibility: hidden;
|
|
}
|
|
|
|
.inventory-print,
|
|
.inventory-print * {
|
|
visibility: visible;
|
|
}
|
|
|
|
.inventory-print {
|
|
display: block;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
color: #000;
|
|
}
|
|
|
|
.inventory-print h1 {
|
|
font-size: 18pt;
|
|
margin: 0 0 0.25em;
|
|
}
|
|
|
|
.inventory-print h2 {
|
|
font-size: 13pt;
|
|
margin: 1.25em 0 0.5em;
|
|
}
|
|
|
|
.inventory-print-meta {
|
|
font-size: 9pt;
|
|
color: #444;
|
|
margin: 0 0 0.75em;
|
|
}
|
|
|
|
.inventory-print table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 10pt;
|
|
}
|
|
|
|
.inventory-print th,
|
|
.inventory-print td {
|
|
border: 1px solid #999;
|
|
padding: 0.25rem 0.375rem;
|
|
text-align: left;
|
|
vertical-align: top;
|
|
}
|
|
|
|
/* Repeat the header row at the top of each printed page. */
|
|
.inventory-print thead {
|
|
display: table-header-group;
|
|
}
|
|
|
|
.inventory-print tr {
|
|
page-break-inside: avoid;
|
|
}
|
|
|
|
/* The supply-plan shopping list reuses the .inventory-print element styling (h1/table/etc.) but is a separate
|
|
printable block. By default (e.g. the Export tab's whole-inventory PDF) it is hidden; it becomes the ONLY
|
|
printed block when the supply plan toggles `body.printing-shopping` just before window.print(). */
|
|
.shopping-print,
|
|
.shopping-print * {
|
|
visibility: hidden;
|
|
}
|
|
.shopping-print {
|
|
display: none;
|
|
}
|
|
body.printing-shopping .inventory-print:not(.shopping-print),
|
|
body.printing-shopping .inventory-print:not(.shopping-print) * {
|
|
visibility: hidden;
|
|
}
|
|
body.printing-shopping .inventory-print:not(.shopping-print) {
|
|
display: none;
|
|
}
|
|
body.printing-shopping .shopping-print,
|
|
body.printing-shopping .shopping-print * {
|
|
visibility: visible;
|
|
}
|
|
body.printing-shopping .shopping-print {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
/* Map popups (POI details and pins) follow the active theme's tokens. */
|
|
/* Scoped under .maplibregl-popup to outrank maplibre-gl.css, which loads after this file. */
|
|
.maplibregl-popup .maplibregl-popup-content {
|
|
background: var(--panel);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
border-radius: 0.25rem;
|
|
}
|
|
.maplibregl-popup .maplibregl-popup-close-button {
|
|
color: var(--muted);
|
|
}
|
|
.maplibregl-popup .maplibregl-popup-close-button:hover {
|
|
background: var(--head);
|
|
color: var(--text);
|
|
}
|
|
.maplibregl-popup.maplibregl-popup-anchor-top .maplibregl-popup-tip,
|
|
.maplibregl-popup.maplibregl-popup-anchor-top-left .maplibregl-popup-tip,
|
|
.maplibregl-popup.maplibregl-popup-anchor-top-right .maplibregl-popup-tip {
|
|
border-bottom-color: var(--panel);
|
|
}
|
|
.maplibregl-popup.maplibregl-popup-anchor-bottom .maplibregl-popup-tip,
|
|
.maplibregl-popup.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip,
|
|
.maplibregl-popup.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip {
|
|
border-top-color: var(--panel);
|
|
}
|
|
.maplibregl-popup.maplibregl-popup-anchor-left .maplibregl-popup-tip {
|
|
border-right-color: var(--panel);
|
|
}
|
|
.maplibregl-popup.maplibregl-popup-anchor-right .maplibregl-popup-tip {
|
|
border-left-color: var(--panel);
|
|
}
|