import React, {useContext, useState} from 'react'; import {ThemeContext} from '../contexts/contexts'; import { ActionInput, Button, Card, CardGroup, ColoredInput, Confirm, Header, Icon, IconButton, Label, Loader, Loading, IconStack, MediaFilterToggle, Message, MultiSelect, NumberInput, Modal, Pagination, Panel, PathInput, Placeholder, Progress, SearchBox, Select, Statistic, StatisticGroup, TabBar, tabClassName, Status, Table, TextInput, Textarea, Checkbox, ThemePicker, Toggle, Tooltip, clearToasts, toast, } from './ui'; import {semanticColorNames} from '../themes/mantine'; import {contrastingColor} from './Common'; import {navBarStyle, navColorNames, useNavColors} from '../themes/navColors'; import {IconMenu2} from '@tabler/icons-react'; /* * A gallery of every component in the library, in the current theme. * * Reachable at /theme-sample and linked from Settings, so a user can see a theme * before committing to it. * * EVERY new component belongs here. It is how we review the design and how we * catch a component that only looks wrong in one of the four themes — which has * already happened more than once. */ /* * A stand-in poster of exact intrinsic dimensions, so the poster caps can be seen against * the ratios that actually break them -- a real library has all five and the gallery has * no media of its own. */ const samplePoster = (width, height) => `data:image/svg+xml,${encodeURIComponent( `` + ``)}`; const Section = ({label, children}) =>

{label}

{children}
; const Row = ({children}) =>
{children}
; /* * One navigation bar, in one of the twelve colors a user can pick for it. * * The bar is the only surface in the app whose background the USER chooses, and every theme * resolves those twelve names to twelve of its own colors -- forty-eight backgrounds that * the same links and status icons have to stay legible on. They did not: the bar drew every * one of them in `--btn-text`, which is near-black in three of the four themes, and night's * `--brown` is #451212. That glyph was 1.33:1 against the bar behind it. * * These are the real `.wrolpi-navbar` chrome and the real `navBarStyle`, not a mock-up, so a * sample cannot show a bar the app does not draw. The measured ratio is printed beside each * one because ten of the forty-eight still sit under 4.5:1 and no foreground will lift them * -- a monochrome theme resolves all twelve names onto one ramp, and a mid-tone of a hue * cannot carry text of that same hue. Fixing those means changing the palette, which is a * decision to make while looking at them. */ const AA_CONTRAST = 4.5; export const NavBarSample = ({color}) => { const colors = useNavColors(color); const short = colors.ratio !== null && colors.ratio < AA_CONTRAST; return
{color} {colors.ratio === null ? '—' : `${colors.ratio.toFixed(2)}:1`} {short && below 4.5:1 — the palette has no better option}
}; // Numbers the stacked toasts so it is obvious which press produced which, and that a second // press adds a toast rather than replacing the first. Module scope, so it survives re-renders. let toastCounter = 0; export function ThemeSamplePage() { const {theme} = useContext(ThemeContext); const [confirmOpen, setConfirmOpen] = useState(false); const [wideConfirmOpen, setWideConfirmOpen] = useState(false); const [comments, setComments] = useState(true); const [hotspot, setHotspot] = useState(true); const [dismissed, setDismissed] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [realColors, setRealColors] = useState(false); const [search, setSearch] = useState(''); const [activeTab, setActiveTab] = useState('Videos'); const [page, setPage] = useState(3); return

Component gallery

Every component in the WROLPi interface library, rendered in the {theme} theme. Switch themes here or from the navigation bar.

The same picker the Settings page uses. The navigation bar has a compact version; both read the same list of themes.

Every color the navigation bar can be set to (Settings → navbar color), in the {theme} theme. The text and icons are not a fixed color: each bar is measured, and drawn in whichever of this theme's own colors reads best on it. The ratio under each bar is that measurement — anything under 4.5:1 is called out, and means the palette itself has no color that reads on that background.

{navColorNames.map(color => )}
{/* A real , so it goes through whatever filter the current theme applies — this is the only way to see the filter working without leaving the page. The unfiltered copy beside it opts out with `night-unfiltered`, so the two can be compared side by side. */} The WROLPi logo, filtered like any other media

Filtered

{/* Kept behind a click: an unfiltered image is a bright patch, and leaving one on screen in night mode would cost a user the dark adaptation they came here to protect. They can ask for it. */} {realColors ? The WROLPi logo, unfiltered : }

Real colors

Images, video, PDFs, embedded pages, and the map canvas cannot read theme tokens, so a monochrome theme remaps them with an SVG color matrix instead. Night filters to red unless you turn it off; amber tints to match only if you ask. Light and dark offer no filter, so both images look the same here.

Use the toggle below to see the difference. An element can opt out with night-unfiltered — that is what the right-hand copy does, which is why it is behind a click: an unfiltered image is a bright patch, and night mode should not spring one on you.

{/* The same control the theme picker renders, and the same state: flipping either one moves both. It renders nothing at all in light and dark, which is why it appears to be missing there. */}

MediaFilterToggle, the control the theme picker ends with. It is per theme — turning it off for night leaves amber's alone — and it renders nothing for a theme that offers no filter, so light and dark show only this caption.

Page title (h1)
Section (h2)
With an icon and a divider (h3)
Subsection (h4)
Smallest (h5)
{/* The `after` slot. A help button beside the text rather than under it -- which is where every one of them sat until headings gained an `after`, because a sibling placed after the heading's block wrapper wraps. */}
}> With a help icon beside it

The level picks the size, so the type scale stays a scale: call sites choose a heading level for the document outline, never a font size.

{/* * Seven, deliberately: enough to wrap on a narrow window, which is where the row * hairlines are worth looking at. The group rules both axes from --border, so the * grid holds together however the tracks fall. */} {/* Status colors a reading that carries meaning. Check these in night and amber, where there is no hue to spend and the value has to read some other way. */} {/* The group above ends in a label, and a heading has no margin of its own, so without this the two run together. */}
Colored readings, as Status draws them
setConfirmOpen(true)}/>
{['xs', 'sm', 'md', 'lg', 'xl'].map(size =>
)}
A row of actions
{/* `.wrolpi-button-row` is how a page lists the actions on a thing -- the archive page's View/Read/Update/Delete, the edit pages' action row. It wraps, so the last button drops to a second line at this width instead of overflowing, and the row gap keeps the two lines apart. */}

In night mode Delete drops its fill for a dashed red outline; dashed means destructive or failed, and nothing else uses it.

Buttons in a row are spaced by the row, never by margins of their own. Semantic's Button brought its own margin and nothing replaced it, so pages that listed buttons as bare siblings had them sharing edges — and the ones that answered with margin-top got a button sitting lower than its neighbours instead, because vertical-align: middle is measured on the margin box.

An icon button is exactly as tall as a labelled one at every size, so a group of them lines up. Mantine's own scale for icon buttons is unrelated to its scale for buttons — sm is 22px against a button's 36px — and the two components start from different defaults.

{/* * `toast` has thirteen call sites and no test, and it has already failed once * app-wide: App.js dropped Semantic's toast container while seven pages were * still importing the old helper, and every notification silently vanished. * A toast is also the one component you cannot see by loading a page -- it has * to be provoked -- which is why it needs buttons here rather than a sample. */} {/* Click this repeatedly: each press is a separate toast, and the container holds five at once. */} {/* Events.js sends three of these, each opening a URL. Click the toast body, or Tab to it and press Enter; the dismiss button does not follow. */}

Top right, five at a time, five seconds each unless the caller says otherwise. Check each type in all four themes: night has no second hue to spend, so info, success, warning and error cannot be told apart by color there.

{/* The roles side by side, which is the only way to see the thing that matters: in night and amber these are five brightnesses of one hue, and they have to stay tellable apart with no hue to help. */} {/* 120px so all five usually sit on one row; they still wrap on a phone, which is why the caption says "in order" and not "left to right". */}
{[ ['neutral', 'pending, disabled, unknown'], ['info', 'in progress, informational'], ['success', 'complete, healthy'], ['warning', 'needs attention'], ['danger', 'failed, destructive'], ].map(([role, meaning]) =>
{role}
{meaning}
)}

Components ask for a meaning — danger — not a hue. Light and dark spend a color on each. Night and amber have only one hue, so a role is a step on a brightness ramp instead; that is why an error and an info toast used to be the same pixel there. Severity climbs in the order shown for those two themes, and both warning and{' '} danger are brighter than ordinary text — with no hue to carry "look at this", brighter is the only thing left.

complete downloading pending failed

Four states drawn from four roles. Failure also carries weight, because brightness is the first thing lost on a dim screen and it is all night has.

{/* The label sits on BOTH halves of the bar, so every one of these is really a contrast test: the text has to read over the fill and over the track, in all four themes. That is why light brightens the fill rather than using the raw token, and why night and amber drop it to --muted instead of a hue. */}
Its own label instead of the percentage
No label at all, and unknown size
{/* Indeterminate: a band sliding inside the bar, for work whose size is not known yet. It must stay within the track -- it used to slide out past both edges, because nothing clipped it. */}
Carrying a meaning

A bar is 22px so its label has room; at 16px the text filled the bar edge to edge. Check the mid-range ones — 37% and 62% are where the label crosses the boundary between fill and track, which is the case that has to work.

Indexing the media directory. This can take several minutes. Channel settings were written to config/channels.yaml. Downloads will resume tomorrow at 00:00. HTTP 403 from the remote server after 3 attempts. Retry, or check the URL. setDismissed(true)}> {dismissed ? 'Dismissed — it would be gone in the real interface.' : 'A nudge the user can clear. Only offer dismiss when clearing it is harmless.'}
{}}>URL {}}>Downloader Progress Status Size Actions youtube.com/watch?v=hT6Rkfahyg video complete 312 MiB youtube.com/watch?v=Zk91nq2rrE video downloading 198 MiB example.org/prepping/water-storage.html archive failed — wikipedia_en_all_maxi_2026-06.zim zim pending 104 GiB