The archive page's six actions shared edges, and Update sat lower than the
rest. Both come from the same thing: Semantic's Button carried
`margin: 0 .25em .25em 0`, so a page could list buttons as bare siblings and
they arrived spaced -- and separated from the line below when the row wrapped.
Nothing replaced that margin.
Two of those six answered with `marginTop: 0.5em`, which is the wrong axis: a
Button is `display: inline-flex; vertical-align: middle`, and `vertical-align`
is measured on the MARGIN box, so half of any top margin shows up as the
visible button dropping. It was covering the wrapped-row case in the only axis
a margin can, and paying for it in the other one.
`.wrolpi-button-row` is a flex row with a gap, so both axes are covered at once
and no button needs a margin of its own -- the same rule as `.wrolpi-stack`,
along the other axis. Applied to the five rows in the app that had no gap: the
archive page, the shared collection edit form (which three pages hand action
buttons to, and where nine buttons plus Cancel carried `margin-top: 1em` each
while Save carried none), the doc page's two responsive branches, and the
downloads table's edit/restart pair.
`.action-button-spacing` goes with them; it was that same margin, on Cancel.
Guarded four ways. ui-layout.cy.js measures that the row really spaces what it
holds, wraps rather than overflows, centres its buttons against a taller item
beside them, and counts a confirmed APIButton as one item rather than two --
that one returns a fragment holding a `<Confirm/>`, which would open a hole in
the row if it did not portal. Common.test.js scans for the two ways this comes
back: a button in a row stamping a vertical margin, and a row of buttons that is
not a row. Neither throws, which is how the app got here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ControllerPage was the final call site; Theme.tsx then had nothing left to wrap,
so it drops from 861 lines to 338 -- the provider that resolves the theme, stamps
`data-theme` and `data-media-filter`, persists both and hands Mantine our tokens.
Everything below that was Semantic wrappers.
The dark-mode compatibility props went with them. `i`, `s`, `t` and `inverted`
existed so unmigrated components could borrow Semantic's dark treatment, and
they carried hardcoded greys -- #1B1C1D, #dddddd, #eeeeee -- which is exactly
what the token themes replaced. Three call sites in Common.js were still
injecting the #eeeeee one into links.
Then `semantic-ui-offline/semantic.min.css` came out of App.js. That took more
with it than expected, and looking at the running app is the only reason it was
caught:
Every link lost its colour and gained the browser's underline and visited
purple. The dashboard's load averages and section headings were underlined
and purple. Links now take `--blue`, which remaps per theme, and underline
only on hover -- many of them wrap cards, posters and icons.
`box-sizing: border-box` disappeared, which silently changes the size of every
padded element in the app. Semantic's reset had been providing it.
The navigation bar overflowed a 1000px viewport with `nowrap` and cut "Admin"
off the right edge -- the one link a user needs to reach the settings that fix
a broken WROLPi. Semantic's menu wrapped; ours does now too.
The gzipped CSS bundle is 47.5 kB, and one call site was still leaning on
Semantic's grid classes -- now a flex column, since all three of its callers
pass library components.
The ratchet stays rather than being deleted. At zero it asserts four things: no
file imports Semantic UI, none calls its toast library, none leans on its
stylesheet, and no file reaches back through Theme.tsx. It can go when the
packages are uninstalled.
Theme.test.js is deleted, but not before porting the two behaviours it
protected: that Button forwards a ref to the real DOM element (a class-component
ref once crashed the Control page, because portals call `node.contains`), and
that the progress percentage stays readable on every bar colour.
Phase 2, part 3. Baseline 63 -> 50.
Common.js is the one that mattered: 51 files import it, so its Semantic usage
was propping up the rest of the migration. Everything in it now comes from the
library -- Paginator, the two search inputs, DirectorySearch, TabLinks, the
toggles, the messages, APIButton, Breadcrumbs, SimpleAccordion, IframeViewer,
CardPoster, FileIcon, LoadStatistic, the theme menu, and the hotspot icons.
Also migrated: Placeholder, Apps, ArchivePreview, CbzViewer, AddToPlaylist,
TaggedDeleteConfirmModal, the collection edit/tag forms, and four inventory
components.
New in the library, all in the gallery and tested:
SearchBox Semantic's `<Search category>` with its grouped results shape
kept, because four call sites already build it. A real
combobox rather than a div that listens for clicks.
Pagination Semantic's prop names; onPageChange gets the page number.
TabBar the strip only -- routing stays with the caller.
IconStack Semantic's IconGroup, named once for assistive technology.
Table.HeaderCell sorted/onSort -- a real button carrying aria-sort.
Button/IconButton translate Semantic's size names.
That last one is defensive: unmigrated callers still pass size='tiny', and
Mantine drops a size it does not recognise without a word, so the button
quietly renders at the wrong size. Mapping here keeps the mixed state honest.
Three real bugs found and fixed on the way, none of them cosmetic:
* `fileSuffixIconName` returned fourteen icon names the map did not have, so
opening a folder with a PDF, a Word document or an archive in it logged an
error and drew a fallback glyph. All fourteen mapped, with a test that
walks the mapper's own return values, and another asserting every mapped
name resolves to a Tabler component that exists.
* `Statistic` relied on Semantic's `Statistic.Group` for its spacing, so on
the dashboard the load averages ran together into "1 MIN. LOAD5 MIN.
LOAD15 MIN. LOAD". It carries its own spacing now.
* Semantic's inverted stylesheet colors every descendant of an inverted
segment, so Toggle labels inherited #dddddd -- white pixels in night mode,
on the Settings page, which is exactly where someone adjusts their WROLPi
in the dark. Restated from tokens.
The baseline ratchet gained a third assertion: a migrated file may still lean
on Semantic's CSS classes without importing the library, which the import check
cannot see and which would collapse silently when Phase 4 deletes the
stylesheet. There is one such bridge today and it is now declared.
Replacing Channels and Domains with Collection.
Moving downloading relationships from Channel to Collection.
Created domains.yaml config to manage tagged domain collections.
Adding turn-key upgrade solution in the UI.