ui: the last of Semantic UI, in name and in dead CSS

The library has been uninstalled for some time, but the codebase still read as
though it were there: identifiers named after it, rules written against its
class names, and about 180 comments explaining a rule in terms of a thing that
is no longer installed.

Renamed, keeping the vocabulary the app actually uses: semanticUIColorMap ->
navColorHexMap, semanticColorNames -> paletteColorNames, semanticNames ->
iconNameAliases, semanticSizes -> sizeAliases, and SemanticUIColors ->
NavColors in wrolpi/schema.py, which is what validates nav_color on the way in.
ColorToSemanticHexColor had no callers at all and is gone.

The baseline ratchet has done its job -- the list reached zero, so the JSON is
deleted and the guard keeps only the checks with teeth: no package, no import,
no `className="ui "`, no toast library.  Its Theme.tsx check went with the
wrappers it was watching for.  It also asserts its own walk found files, so a
clean result cannot mean it scanned nothing.

Deleted from App.css: every rule requiring `.ui`, `.inverted`, `.segment` or
`.modal`, none of which any component has set since the stylesheet stopped
loading, plus the form skeletons, ui-alerts and progress-text rules that
nothing renders.  Two of them were doing harm rather than nothing: the drag
tints had a dark-theme variant under `.inverted` that stopped applying, leaving
15% of a fixed blue over a dark row, and now mix a token instead.

Two selectors naming markup that no longer exists were live bugs.  The file
browser's filter had `.ui.input.file-browser-filter`, so it rendered at the
browser default width on a bar laid out to give it 34em -- now qualified by its
container, because `.wrolpi-action-input` sets `width: 100%` at the same
specificity and is loaded later.  And SearchBox takes named props rather than a
rest spread, so the `id` DestinationForm hands it was dropped in silence and
the label it renders pointed at nothing; the download forms' Destination field
has had no associated label since the migration.

"Semantic roles" survives in four places, where it means what it says.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Roland 2026-08-05 19:15:57 -06:00
parent 72228ed67f
commit e00f0439da
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
58 changed files with 387 additions and 618 deletions

View file

@ -102,7 +102,7 @@ describe('Domain Editing Workflow', () => {
// Set directory - DirectorySearch component uses input inside field
cy.contains('label', 'Directory').parent().find('input').clear().type('archive/example.com');
// Set description - Semantic UI TextArea
// Set description
cy.contains('label', 'Description').parent().find('textarea').clear().type('Updated example domain');
// Mock tags endpoint

View file

@ -90,7 +90,7 @@ describe('Domains List Page', () => {
it('Edit control is a link to that domain\'s edit page', () => {
/*
* This used to assert Semantic's `ui button secondary` classes. Framework class
* This used to assert the old `ui button secondary` classes. Framework class
* names say nothing a user can perceive and they broke the moment the button
* changed libraries, so it now checks what actually matters: the control is a real
* link, and it points at the edit route rather than relying on a click handler.

View file

@ -5,7 +5,7 @@
* Usage: node scripts/generate-favicons.js
*
* This script reads the base icon.svg and generates a colored version
* for each color in semanticUIColorMap (from src/components/Vars.js).
* for each color in navColorHexMap (from src/components/Vars.js).
* The favicons are saved to public/favicon-{colorname}.svg
*/
@ -17,14 +17,14 @@ const publicDir = path.join(__dirname, '..', 'public');
const varsPath = path.join(srcDir, 'Vars.js');
const iconPath = path.join(publicDir, 'icon.svg');
// Parse semanticUIColorMap from Vars.js (single source of truth)
// Parse navColorHexMap from Vars.js (single source of truth)
function parseColorMapFromVars() {
const varsContent = fs.readFileSync(varsPath, 'utf8');
// Extract the semanticUIColorMap object using regex
const match = varsContent.match(/export const semanticUIColorMap\s*=\s*\{([^}]+)\}/);
// Extract the navColorHexMap object using regex
const match = varsContent.match(/export const navColorHexMap\s*=\s*\{([^}]+)\}/);
if (!match) {
throw new Error('Could not find semanticUIColorMap in Vars.js');
throw new Error('Could not find navColorHexMap in Vars.js');
}
const colorMap = {};
@ -35,15 +35,15 @@ function parseColorMapFromVars() {
}
if (Object.keys(colorMap).length === 0) {
throw new Error('No colors found in semanticUIColorMap');
throw new Error('No colors found in navColorHexMap');
}
return colorMap;
}
// Parse colors from Vars.js
const semanticUIColorMap = parseColorMapFromVars();
console.log(`Found ${Object.keys(semanticUIColorMap).length} colors in Vars.js\n`);
const navColorHexMap = parseColorMapFromVars();
console.log(`Found ${Object.keys(navColorHexMap).length} colors in Vars.js\n`);
// Colors that conflict with white stroke - use black stroke instead
// Must match Nav.js conflictingColors
@ -55,7 +55,7 @@ const iconSvg = fs.readFileSync(iconPath, 'utf8');
console.log('Generating colored favicons from icon.svg...\n');
// Generate a favicon for each color
for (const [colorName, hexColor] of Object.entries(semanticUIColorMap)) {
for (const [colorName, hexColor] of Object.entries(navColorHexMap)) {
// Replace the dark fill color (#1b1c1d) with the nav color
let coloredSvg = iconSvg.replace(/fill:#1b1c1d/g, `fill:${hexColor}`);
@ -72,4 +72,4 @@ for (const [colorName, hexColor] of Object.entries(semanticUIColorMap)) {
console.log(` Created favicon-${colorName}.svg (${hexColor}${strokeInfo})`);
}
console.log(`\nGenerated ${Object.keys(semanticUIColorMap).length} favicon files in public/`);
console.log(`\nGenerated ${Object.keys(navColorHexMap).length} favicon files in public/`);

View file

@ -3,16 +3,16 @@
--navbar-height: 3.4em;
}
/* Nav bar: a hand-rolled flexbox bar (was a Semantic Menu). The user-chosen background
and the foreground measured against it are set inline (see themes/navColors); this
stylesheet only supplies structure and hover affordances. */
/* 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. Semantic's menu wrapped, and with this many items the
* bar overflows a 1000px viewport -- `nowrap` cut "Admin" off the right edge, which
* is the one link a user needs to reach the settings that fix a broken WROLPi.
* 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);
@ -158,26 +158,10 @@ html .wrolpi-navbar .mantine-ActionIcon-root:hover {
align-self: stretch;
}
.channel-edit {
border-radius: 0.1875rem;
padding: 0.75em;
}
.channel-edit:hover {
background-color: #7abaff;
cursor: pointer;
}
::-ms-clear {
display: none;
}
.form-control-clear {
z-index: 10;
pointer-events: auto;
cursor: pointer;
}
pre.wrap-text {
white-space: pre-wrap; /* Since CSS 2.1 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
@ -219,14 +203,6 @@ pre.wrap-text {
color: var(--text);
}
.center-me {
margin: 0 auto;
}
div.inverted {
background-color: #1B1C1D;
}
/*
* The hand-rolled `.toggle` / `.slider` switch is gone.
*
@ -240,17 +216,6 @@ div.inverted {
* for whoever revives it, and it also hardcoded #6435C9 and white, which no theme can reach.
*/
.inverted-progress-text.ui.indicating.progress .bar > .progress,
.inverted-progress-text.ui.progress > .label {
color: white !important;
}
/* Light bar colors (yellow, olive) need dark text on the bar — must override inverted-progress-text. */
.light-bar-progress-text.ui.indicating.progress .bar > .progress,
.light-bar-progress-text.ui.progress .bar > .progress {
color: rgba(0, 0, 0, 0.85) !important;
}
/* 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. */
@ -316,18 +281,6 @@ td.file-path > svg {
margin-right: 0.35em;
}
/* Necessary until react-semantic-toasts is upgraded to Node 18 */
.ui-alerts {
position: fixed;
z-index: 2060;
padding: 1.5rem;
}
.ui-alerts.top-right {
top: 1.25rem;
right: 1.25rem;
}
textarea.otp {
width: 100%;
color: #333;
@ -339,69 +292,10 @@ textarea.otp {
padding-top: 3.3125rem;
}
.card-icon.inverted {
background-color: #222222;
}
.modal .inverted {
border-color: #888888;
}
.modal .header.inverted {
color: #DDDDDD !important;
background-color: #171616 !important;
}
.modal .content.inverted {
color: #DDDDDD !important;
border-color: #888888 !important;
background-color: #171616 !important;
}
.modal .actions.inverted {
color: #DDDDDD !important;
border-color: #888888 !important;
background-color: #171616 !important;
}
.modal.inverted i.close {
color: #DDDDDD !important;
}
/* The accordion has no background of its own; it inherits whatever container it sits in (Modal
content #171616, Segment #1B1C1D, the page body, etc.). This matches how the light theme already
renders accordions (transparent), and avoids a darker band when the parent isn't #171616.
Direct children only, so nested components that use the `content`/`title` class names (e.g. a
Message's Message.Content) are not affected. */
.ui.inverted.accordion,
.ui.inverted.accordion > .title,
.ui.inverted.accordion > .content {
background-color: transparent !important;
}
/* Semantic UI paints a definition table's empty corner cell white (background on the inverted
variant, white box-shadows on the celled/plain variants) to fake a "cut-out" corner on a white
page. Our dark theme has a dark page, so the corner must be transparent with no shadow. */
.ui.inverted.definition.table thead:not(.full-width) th:first-child,
.ui.inverted.definition.table tfoot:not(.full-width) th:first-child,
.ui.inverted.celled.definition.table thead:not(.full-width) th:first-child,
.ui.inverted.celled.definition.table tfoot:not(.full-width) th:first-child {
background: transparent !important;
box-shadow: none !important;
}
.clickable {
cursor: pointer;
}
.segment.inverted {
border: 1px solid #ffffff22 !important;
}
.inverted label {
color: #DDDDDD !important;
}
.table-ellipsis td.column-ellipsis {
white-space: nowrap;
overflow: hidden;
@ -431,14 +325,6 @@ textarea.otp {
margin: 0.625rem;
}
.icon.divider {
color: rgb(0, 0, 0, 0.4) !important;
}
.divider.inverted {
color: rgb(255, 255, 255, 0.6) !important;
}
.video-player {
display: block;
margin: 0 auto; /* Center the video horizontally */
@ -468,80 +354,14 @@ table.ratio-table td.min-width {
width: auto;
}
/* CollectionEditForm styles */
.required-indicator {
color: red;
}
/* Form loading skeleton */
.form-skeleton-field {
margin-bottom: 1.5em;
}
.form-skeleton-label {
height: 0.875rem;
width: 5rem;
margin-bottom: 0.5rem;
background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: 0.1875rem;
}
.form-skeleton-input {
height: 2.375rem;
width: 100%;
background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: 0.25rem;
}
.form-skeleton-textarea {
height: 5rem;
width: 100%;
background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: 0.25rem;
}
.form-skeleton-button {
height: 2.625rem;
width: 6.25rem;
background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: 0.25rem;
}
@keyframes skeleton-shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* Dark mode skeleton */
.inverted .form-skeleton-label,
.inverted .form-skeleton-input,
.inverted .form-skeleton-textarea,
.inverted .form-skeleton-button {
background: linear-gradient(90deg, #333 25%, #444 50%, #333 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
}
/*
* Sticky footer for the file browser.
*
* These were two hardcoded Semantic greys with `.inverted` selecting the dark one, but
* nothing has set `.inverted` since ThemeProvider stopped supplying it -- so 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.
* Tokens have no such failure mode: there is one rule and each theme fills it in.
* 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;
@ -557,9 +377,9 @@ table.ratio-table td.min-width {
*
* 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 buttons are laid out with Mantine's `Group`,
* which 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.
* 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;
@ -567,22 +387,24 @@ table.ratio-table td.min-width {
gap: 0.5em;
}
/* Drag selection visual feedback for FileBrowser */
/*
* 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: rgba(33, 133, 208, 0.15) !important;
background-color: color-mix(in srgb, var(--blue) 22%, transparent) !important;
}
.inverted tr.drag-selecting td {
background-color: rgba(33, 133, 208, 0.25) !important;
}
/* Drag deselection visual feedback (red tint) */
tr.drag-deselecting td {
background-color: rgba(219, 40, 40, 0.15) !important;
}
.inverted tr.drag-deselecting td {
background-color: rgba(219, 40, 40, 0.25) !important;
background-color: color-mix(in srgb, var(--red) 22%, transparent) !important;
}
/* Prevent text selection during drag */
@ -604,8 +426,23 @@ tr.drag-deselecting td {
spaces it (see `.wrolpi-stack`). Its own 0.5em was added to that. */
}
/* Double the default input width on desktop. */
.ui.input.file-browser-filter {
/*
* 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%;
}
@ -620,36 +457,12 @@ tr.drag-deselecting td {
}
/* Fill the row, but stay on one line with the collapse-all button. */
.ui.input.file-browser-filter {
.file-browser-filter-container .file-browser-filter {
flex: 1 1 auto;
width: auto;
}
}
/* Ensure step icons are always visible, even when disabled */
.ui.steps .step.disabled > .icon,
.ui.steps .step.disabled > i.icon,
.ui.steps .disabled.step > .icon,
.ui.steps .disabled.step > i.icon {
opacity: 0.45 !important;
display: inline-block !important;
visibility: visible !important;
width: auto !important;
height: auto !important;
color: rgba(0, 0, 0, 0.87) !important;
}
/* Ensure the icon glyph (::before) is visible for disabled steps */
.ui.steps .step.disabled > .icon:before,
.ui.steps .step.disabled > i.icon:before,
.ui.steps .disabled.step > .icon:before,
.ui.steps .disabled.step > i.icon:before {
opacity: 0.45 !important;
visibility: visible !important;
color: rgba(0, 0, 0, 0.87) !important;
}
/* 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 {

View file

@ -144,7 +144,7 @@ export function useTags() {
/*
* The link carries no margin of its own. It had `0.3em` either side, from when these were
* Semantic labels that had none -- but a tag already reserves a `margin-left` in ui.css for
* labels that had none -- but a tag already reserves a `margin-left` in ui.css for
* the point drawn outside its body, so that was 0.3em twice on top of the group's gap and
* the point's own room. Two tags sat 34.7px apart, of which the point needs 18.7px.
*

View file

@ -150,7 +150,7 @@ describe('a row of tags', () => {
it('adds no margin of its own around a linked tag', async () => {
/*
* The link wrapper carried `0.3em` either side, from when these were Semantic labels
* The link wrapper carried `0.3em` either side, from when these were framework labels
* that had no margin of their own. It is 0.3em twice, plus the group's gap, plus the
* structural margin -- so two tags sat 34.7px apart where the point needs 18.7px, and
* a wall of tags on the dashboard read as loose.

View file

@ -709,10 +709,8 @@ export function DomainEditPage() {
appliedTagName={domain?.tag_name}
>
{/*
* CollectionEditForm still wraps its children in a `ui stackable grid` div (Semantic's
* grid CSS, not a component) because it is shared with unmigrated callers (Channels.js,
* Playlists.js). Plain "row"/"column" divs are that CSS's own class names, not a
* semantic-ui-react import.
* CollectionEditForm lays its children out as a grid, and "row"/"column" are that
* grid's own class names -- plain divs, not components.
*/}
<div className="row">
<div className="column">

View file

@ -18,7 +18,7 @@ import {VinDecoderCalculator} from "./calculators/VinDecoderCalculator";
// `group` matching one of these names; any calculator whose group is missing/unknown falls
// into a trailing "Other" section so a new calculator can never silently disappear.
//
// `color` is a Semantic UI named color (colorblind-friendly palette) used to tint the group's
// `color` is one of the named palette colors (see `paletteColorNames`) used to tint the group's
// buttons. Violet is intentionally avoided — it is the default navbar color and would blend in.
export const CALCULATOR_GROUPS = [
{name: 'Preparedness & Storage', color: 'red'},

View file

@ -492,7 +492,7 @@ export function SearchResultsInput({
const localHandleResultSelect = (result) => {
if (handleResultSelect) {
// Semantic handed its callers `{result}`, and they destructure it -- passing the
// Callers destructure `{result}` from this -- passing the
// bare result silently broke navigation from the search suggestions. The shape
// stays until the call sites are ready to change together.
handleResultSelect({result});
@ -1626,7 +1626,7 @@ export const TAG_TEXT_LIGHT = '#dddddd';
* Dark or light text, whichever a user's chosen tag color actually reads better against.
*
* Measures both instead of comparing a brightness figure to a threshold. The threshold
* version put light text on Semantic's blue (`#2185d0`) at 2.9:1 when dark text on the same
* version put light text on the palette blue (`#2185d0`) at 2.9:1 when dark text on the same
* fill gives 5.3:1 -- it had picked the worse of the only two options. Every mid-tone blue,
* teal and purple a user might choose sat in that band.
*/
@ -1823,7 +1823,7 @@ export function useAPIButton(
}
}
// `color` was a Semantic color name; a role carries the meaning instead, and a
// `color` was a bare color name; a role carries the meaning instead, and a
// caller that really wants a color can still pass one through `props`.
const buttonArgs = {
onClick: localOnClick, disabled, loading, type, size,
@ -2118,7 +2118,7 @@ export function Breadcrumbs({crumbs, size = undefined}) {
return <span key={index}>{text}</span>
}
// The separator is a chevron rather than Semantic's slash, matching the icon set.
// The separator is a chevron rather than a slash, matching the icon set.
return <UIBreadcrumbs separator={<Icon name='chevron right' size={14}/>}>
{crumbs.map(getSection)}
</UIBreadcrumbs>

View file

@ -110,7 +110,7 @@ describe('DirectorySearch', () => {
it('renders the dropdown without crashing when directoryName is null', async () => {
// An unset form field (e.g. the new Channel page) passes a null value. With
// isDir=false a "New Directory" result is built from directoryName; if that title is
// null, Semantic UI's Search crashes reading null.length when the menu opens.
// null, the old Search crashed reading null.length when the menu opened.
setMockHookState({
directories: [],
channelDirectories: [],
@ -135,7 +135,7 @@ describe('DirectorySearch', () => {
<DirectorySearch onSelect={mockOnSelect} value="" required/>
);
// Unlike Semantic UI's Search, the SearchBox forwards `required` straight
// Unlike the old Search, the SearchBox forwards `required` straight
// onto the input.
const input = screen.getByPlaceholderText(/search directory names/i);
expect(input).toBeRequired();
@ -495,7 +495,7 @@ describe('contrastingColor', () => {
*/
it('picks whichever of the two options actually reads better', () => {
// Semantic's blue. The old threshold check chose light text here, at 2.9:1, when dark
// The palette blue. The old threshold check chose light text here, at 2.9:1, when dark
// text on the same fill gives 5.3:1 -- the worse of the only two available answers.
// Every mid-tone blue, teal and purple a user might choose sat in that band.
const blue = '#2185d0';
@ -781,7 +781,7 @@ describe('PageContainer', () => {
describe('a row of buttons', () => {
/*
* Semantic's Button brought `margin: 0 .25em .25em 0` with it, so a page could list buttons as
* Buttons used to bring `margin: 0 .25em .25em 0` with them, so a page could list buttons as
* bare siblings and they arrived both spaced and, when the row wrapped, separated. Nothing
* replaced that margin. The archive page's six actions shared edges, and two of them carried
* `marginTop: 0.5em` -- put there to keep a wrapped row from touching vertically, which instead

View file

@ -95,7 +95,7 @@ describe('DomainEditPage', () => {
render(<DomainEditPage/>);
// Should show Semantic UI Loader with text
// Should show the Loading component with text
expect(screen.getByText(/loading domain/i)).toBeInTheDocument();
// Form should not be visible during initial load

View file

@ -37,11 +37,11 @@ import {
} from "./Vars";
/*
* Download.js was built around Semantic UI's <Form> family (Form.Input,
* Download.js was built around the old <Form> family (Form.Input,
* FormDropdown, TextArea) via `hooks/useForm.js`'s JSX helpers (InputForm,
* NumberInputForm, ToggleForm, UrlInput, UrlsTextarea). `hooks/useForm.js` is
* off-limits for this migration and its JSX helpers still render through
* `./Theme` (Semantic UI). Rather than use those helpers, the field
* `./Theme`. Rather than use those helpers, the field
* components below call `useForm`'s plain data getters (getInputProps,
* getSelectionProps, getCustomProps) directly and render our own markup with
* the token-driven `./ui` library. See the migration report for what this
@ -110,7 +110,7 @@ function NumberField({
</div>
}
/** A single-select field. `options` are Semantic-shaped ({key, text, value}); Mantine's Select
/** A single-select field. `options` are {key, text, value}; Mantine's Select
* requires string data values, so values are stringified for display and mapped back on change. */
function SelectField({form, name, path, label, options, required = false, placeholder, disabled = false, afterChange}) {
const [inputProps] = form.getSelectionProps({name, path, required, afterChange});
@ -1547,7 +1547,7 @@ export function EditScrapeFilesDownloadForm({download, onDelete, onCancel, onSuc
/>
}
/** URL field for a single URL (Semantic's `UrlInput`). */
/** URL field for a single URL. */
function UrlField({form, required = true, name = 'url', path = 'url', disabled = false}) {
const validator = (i) => validURL(i) ? null : 'Invalid URL';
return <TextField
@ -1562,7 +1562,7 @@ function UrlField({form, required = true, name = 'url', path = 'url', disabled =
/>
}
/** Multiline URLs field (Semantic's `UrlsTextarea`). */
/** Multiline URLs field: one URL per line. */
function UrlsField({name = 'urls', required, form}) {
required = required !== undefined;

View file

@ -984,7 +984,7 @@ const REFRESH_PHASES = [
{key: 5, icon: 'check circle', title: 'Cleanup', description: 'Finalizing'},
];
// Replaces Semantic's Step.Group/Step: a row of phase markers, brightness/weight (via the active
// A row of phase markers, brightness/weight (via the active
// border) carrying the current stage rather than a bespoke component from the shared library.
function RefreshStepItem({phase, isActive, isCompleted, isDisabled, description}) {
return <div style={{

View file

@ -69,7 +69,7 @@ function SearchModal({open, onClose}) {
*
* `autoFocus` on the input below is what actually does it: the modal mounts its
* content on a later tick, so this effect runs while `inputRef.current` is still
* null. Semantic's modal mounted synchronously, which is why focusing here used
* null. The modal this replaced mounted synchronously, which is why focusing here used
* to be enough. The ref call is kept for the case where the modal is already
* mounted and merely re-opened.
*/

View file

@ -17,14 +17,14 @@ import {
} from "../hooks/customHooks";
import {useReorganizationStatus} from "../contexts/FileWorkerStatusContext";
import {SearchIconButton} from "./Search";
import {HELP_VIEWER_URI, NAME, semanticUIColorMap} from "./Vars";
import {HELP_VIEWER_URI, NAME, navColorHexMap} from "./Vars";
import {useOverflowNav} from "../hooks/useOverflowNav";
import {defaultNavColor, navBarStyle, useNavColors} from "../themes/navColors";
import _ from "lodash";
function updateFavicon(colorName) {
// Fall back to violet if invalid color
const safeColor = colorName in semanticUIColorMap ? colorName : 'violet';
const safeColor = colorName in navColorHexMap ? colorName : 'violet';
const faviconPath = `/favicon-${safeColor}.svg`;
// Update all favicon links
@ -40,7 +40,7 @@ function updateFavicon(colorName) {
if (icon16) icon16.href = faviconPath;
// Update theme-color meta tag for iOS/Android status bar
const hexColor = semanticUIColorMap[safeColor];
const hexColor = navColorHexMap[safeColor];
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
if (themeColorMeta) themeColorMeta.content = hexColor;
}
@ -88,8 +88,8 @@ function DropdownMenuItem({link}) {
// "More" overflow menu.
if (link.links) {
// Mantine's Menu does not nest submenus without extra plumbing; a labelled,
// indented group stands in for Semantic's nested Dropdown. No current data
// uses this path (no allLinks entry has its own `links`).
// indented group stands in for one. No current data uses this path (no
// allLinks entry has its own `links`).
return <React.Fragment>
<Menu.Label>{link.text}</Menu.Label>
{link.links.map(l => <DropdownMenuItem key={l.key} link={l}/>)}
@ -158,8 +158,8 @@ function MobileMenu({links}) {
/*
* One status indicator's slot in the right-hand corner.
*
* This used to carry `marginTop: 0.8em`, a nudge left over from Semantic's menu. The corner
* centres its children, and a top margin is part of the margin box being centred, so the
* This used to carry `marginTop: 0.8em`, a nudge inherited from the bar this replaced. The
* corner centres its children, and a top margin is part of the margin box being centred, so the
* nudge pushed every indicator down by half of it -- about 6px.
*
* That read as an error on some and not others, which is why it survived: the bare-icon

View file

@ -247,7 +247,7 @@ export function useSearchSuggestions(defaultSearchStr, defaultTagNames, anyTag)
const noResults = [{title: 'No results'}];
const normalizeSuggestionsResults = (newSuggestions) => {
// Convert the suggestions from the Backend to what the Semantic <Search> expects.
// Convert the suggestions from the Backend to what SearchBox expects.
const lowerSearchStr = searchStr ? searchStr.toLowerCase() : '';
let results = {};
@ -516,8 +516,8 @@ export function SearchIconButton() {
/*
* A single button which opens the search modal via keyboard shortcuts context.
*
* `wrolpi-navbar-link`, not Semantic's leftover `item`, which carried no rules at all
* once Semantic was removed: no pointer cursor, no hover highlight, and a hit area of
* `wrolpi-navbar-link`, not the leftover `item` class, which carries no rules at all
* now that the stylesheet defining it is gone: no pointer cursor, no hover highlight, and a hit area of
* whatever the 18px glyph occupied rather than the height of the bar. It is the same
* kind of thing as Help and Admin and now has the same affordance.
*/

View file

@ -226,15 +226,12 @@ export function ThemeProvider({children, ...props}: ThemeProviderProps) {
</ThemeContext.Provider>
}
/*
* The Semantic UI wrappers that used to live below this line are gone.
* This module is the theme provider and nothing else: it resolves the chosen theme, stamps
* `data-theme` and `data-media-filter` on <html>, persists both, and hands Mantine our
* tokens.
*
* They existed so call sites could get a themed Button, Segment, Modal and so on while
* the app still rendered Semantic components; every one of those call sites now imports
* from `src/components/ui` instead. The dark-mode compatibility props they consumed --
* `i`, `s`, `t`, `inverted` -- went with them: those carried hardcoded greys, which is
* precisely what the token themes replaced.
*
* What remains is the part that was never about Semantic: the provider that resolves the
* chosen theme, stamps `data-theme` and `data-media-filter` on <html>, persists both, and
* hands Mantine our tokens.
* It used to re-export a set of themed component wrappers as well, along with the
* dark-mode compatibility props they consumed (`i`, `s`, `t`, `inverted`). Those carried
* hardcoded greys, which is precisely what the token themes replaced; components come from
* `src/components/ui` instead.
*/

View file

@ -41,7 +41,7 @@ import {
clearToasts,
toast,
} from './ui';
import {semanticColorNames} from '../themes/mantine';
import {paletteColorNames} from '../themes/mantine';
import {contrastingColor} from './Common';
import {navBarStyle, navColorNames, useNavColors} from '../themes/navColors';
import {IconMenu2} from '@tabler/icons-react';
@ -315,7 +315,7 @@ export function ThemeSamplePage() {
<IconButton icon='search' label='Search'/>
<IconButton icon='trash' role='danger' label='Delete'
onClick={() => setConfirmOpen(true)}/>
<Tooltip label='Tooltips replace Semantic popups'>
<Tooltip label='A tooltip, on hover or focus'>
<Button role='cancel'>Hover me</Button>
</Tooltip>
<Button role='cancel' onClick={() => toast({
@ -351,11 +351,10 @@ export function ThemeSamplePage() {
destructive or failed, and nothing else uses it.
</p>
<p style={{fontSize: '0.75rem', color: 'var(--muted)', marginBottom: 0}}>
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
<code>margin-top</code> got a button sitting lower than its neighbours instead,
because <code>vertical-align: middle</code> is measured on the margin box.
Buttons in a row are spaced by the row, never by margins of their own. A button
that carries its own <code>margin-top</code> sits lower than its neighbours,
because <code>vertical-align: middle</code> is measured on the margin box —
which is why the spacing belongs to the row and not to the buttons in it.
</p>
<p style={{fontSize: '0.75rem', color: 'var(--muted)', marginBottom: 0}}>
An icon button is exactly as tall as a labelled one at every size, so a
@ -368,9 +367,8 @@ export function ThemeSamplePage() {
<Section label='Two choices of equal weight'>
<Panel>
{/* Semantic's `Segment placeholder` with a vertical Divider, which is what the
dashboard's Download/Upload panel is. The halves are a grid so they stay equal
whatever the labels say, and the rule falls at the panel's true middle. */}
{/* The dashboard's Download/Upload panel. The halves are a grid so they stay
equal whatever the labels say, and the rule falls at the panel's true middle. */}
<div className='wrolpi-or-split'>
<div><Button role='primary' icon='download'>Download</Button></div>
<div><Button role='save' icon='upload'>Upload</Button></div>
@ -389,7 +387,7 @@ export function ThemeSamplePage() {
<Panel>
{/*
* `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
* app-wide: App.js dropped the old 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.
@ -464,7 +462,7 @@ export function ThemeSamplePage() {
</Panel>
</Section>
<Section label='Semantic roles'>
<Section label='Roles'>
<Panel>
{/*
The roles side by side, which is the only way to see the thing that
@ -765,7 +763,7 @@ export function ThemeSamplePage() {
<Section label='Labels'>
<Panel>
<Row>
{semanticColorNames.map(color => <Label key={color} color={color}>{color}</Label>)}
{paletteColorNames.map(color => <Label key={color} color={color}>{color}</Label>)}
<Label color='black'>black</Label>
<Label color='white'>white</Label>
<Label color='blue' icon='tag'>tagged</Label>
@ -938,8 +936,8 @@ export function ThemeSamplePage() {
<Button role='cancel' icon='eye' onClick={() => setModalOpen(true)}>Open a modal</Button>
</Row>
<p style={{fontSize: '0.75rem', color: 'var(--muted)', marginBottom: 0, marginTop: 10}}>
Semantic's compound shape is kept — Modal.Header, Modal.Content,
Modal.Actions — so the 34 call sites written against it migrate by import.
A modal is compound — Modal.Header, Modal.Content, Modal.Actions — so the
markup at a call site reads the way the dialog is laid out.
</p>
<Modal open={modalOpen} onClose={() => setModalOpen(false)} size='small'>
<Modal.Header>Restore Backup: channels.yaml</Modal.Header>

View file

@ -77,7 +77,18 @@ export const allFrequencyOptions = {
[days180Option.value]: days180Option,
};
export const semanticUIColorMap = {
/*
* The named colors a user can pick for the navigation bar, and the fixed hex each one is.
*
* Fixed, and deliberately not tokens: these are the colors of the favicon files in
* `public/favicon-<color>.svg`, which `scripts/generate-favicons.js` writes from this very
* object, and of the `theme-color` meta tag the mobile browser paints its own chrome with.
* Neither can read a CSS variable, so both need a literal that cannot drift from the bar.
*
* The palette is a colorblind-friendly one and is shared with `themes/navColors.ts`, which
* measures a legible foreground against each.
*/
export const navColorHexMap = {
red: '#db2828',
orange: '#f2711c',
yellow: '#fbbd08',
@ -92,10 +103,6 @@ export const semanticUIColorMap = {
grey: '#767676',
}
export const ColorToSemanticHexColor = (color) => {
return semanticUIColorMap[color] || null;
}
export const validUrlRegex = /^(http|https):\/\/[^ "]+$/;
export const downloadOrderOptions = [

View file

@ -129,7 +129,7 @@ const NoComments = ({video}) => {
}
}
// Semantic's <Comment>/<CommentGroup> have no equivalent in the token library -- this is a nested
// The token library has no comment-thread component -- this is a nested
// comment thread, not a reusable widget, so it is built here from plain elements and tokens.
const VideoComment = ({comment, children}) => {
const {is_favorited, like_count, author, timestamp, text, author_is_uploader} = comment;

View file

@ -214,7 +214,7 @@ describe('ControllerPage', () => {
});
expect(screen.getByText('Hotspot Protocol')).toBeInTheDocument();
// The saved protocol is selected; WPA3 is offered because the device supports it.
// Semantic UI renders the selected value as both the dropdown text and an option.
// The select renders its chosen value as both the field's text and an option.
await waitFor(() => {
expect(screen.getAllByText('WPA2 (most compatible)').length).toBeGreaterThan(0);
expect(screen.getAllByText('WPA3').length).toBeGreaterThan(0);

View file

@ -56,7 +56,7 @@ describe('ExtensionPage', () => {
it('shows the not-yet-installed warning when no binaries are available', async () => {
// This test exists specifically to render the <Message warning> block,
// which catches missing imports from the Theme/Semantic-UI module.
// which catches missing imports from the component library.
setUserAgent(CHROME_UA);
global.fetch = mockFetch({
files: {

View file

@ -14,7 +14,7 @@ import {
} from "../Common";
import {useConfigs, useDockerized} from "../../hooks/customHooks";
import {ConfigsTable} from "./Configs";
import {semanticUIColorMap} from "../Vars";
import {navColorHexMap} from "../Vars";
import {Link} from "react-router";
import {
Button,
@ -506,7 +506,7 @@ export function SettingsPage() {
{value: tz, label: tz}
));
const navColorOptions = Object.keys(semanticUIColorMap).map(i => {
const navColorOptions = Object.keys(navColorHexMap).map(i => {
return {value: i, label: i.charAt(0).toUpperCase() + i.slice(1)}
});

View file

@ -46,12 +46,7 @@ export function CollectionEditForm({
</Message>}
<form onSubmit={handleSubmit} autoComplete="off">
{/*
Every caller now passes library components rather than Semantic Grid rows, so
this is a plain flex column instead of the `ui stackable grid` bridge it used
to be. That bridge was the last thing in the app depending on Semantic's
stylesheet.
*/}
{/* Every caller passes library components, so this is a plain flex column. */}
<div
className='wrolpi-form-rows'
style={form.loading ? {opacity: 0.6, pointerEvents: 'none'} : undefined}

View file

@ -46,7 +46,7 @@ function getCollectionSearchLink(collection, routes, primaryKey) {
}
/**
* Convert a Semantic-style relative column width (1-16) into a CSS percentage.
* Convert a relative column width (1-16, sixteenths) into a CSS percentage.
* @param {number} width - Relative width, 1-16
* @returns {Object|undefined} A style object, or undefined when no width is configured
*/

View file

@ -32,7 +32,7 @@ export function FieldCell({field, value, unitValue, onChange, onUnitChange, onEn
return;
}
// Our inputs forward their ref straight to the underlying <input> DOM node, so focus + select work
// directly on it (unlike Semantic's Input, which wrapped the node behind `.inputRef`).
// directly on it (the old Input wrapped the node behind `.inputRef`).
const node = localRef.current;
if (node && node.focus) {
node.focus();

View file

@ -128,7 +128,7 @@ export function InventoryTable({slug, fields, items, locations, catalog, search,
const visibleItems = useMemo(() => filterItems(displayItems, fields, search), [displayItems, fields, search]);
const focusFirst = () => {
// Our inputs forward their ref straight to the underlying <input> DOM node (unlike Semantic's Input,
// Our inputs forward their ref straight to the underlying <input> DOM node (unlike the old Input,
// which wrapped it behind `.inputRef`).
const node = firstInputRef.current;
if (node && node.focus) {

View file

@ -5,10 +5,9 @@ import {Icon} from './Icon';
/*
* Buttons.
*
* Roles carry meaning (Semantic UI heritage): blue = primary/download,
* green = save/confirm, red = destructive, amber = retry/warning, neutral
* outline = cancel. Prefer `role` over naming a color, so a theme that remaps
* a color keeps the meaning.
* Roles carry meaning: blue = primary/download, green = save/confirm,
* red = destructive, amber = retry/warning, neutral outline = cancel. Prefer
* `role` over naming a color, so a theme that remaps a color keeps the meaning.
*/
export type ButtonRole = 'primary' | 'save' | 'retry' | 'danger' | 'cancel';
@ -25,7 +24,7 @@ const roleProps: Record<ButtonRole, {color?: string; variant: string; className?
export interface ButtonProps extends Omit<MButtonProps, 'leftSection' | 'rightSection'> {
role?: ButtonRole;
/** Semantic icon name or a Tabler component, rendered before the label. */
/** An icon name (see Icon.tsx) or a Tabler component, rendered before the label. */
icon?: string | React.ComponentType<any>;
iconAfter?: string | React.ComponentType<any>;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
@ -41,14 +40,15 @@ const renderIcon = (icon?: string | React.ComponentType<any>) => {
}
/*
* Semantic's size names, mapped onto Mantine's scale.
* WROLPi's size names, mapped onto Mantine's scale.
*
* Unmigrated call sites still pass `size='tiny'`, and Mantine silently ignores a
* size it does not know — the button renders at the default size and nobody sees
* a warning. Translating here keeps those call sites looking right until they
* migrate, and costs nothing once they have.
* Call sites across the app say `size='tiny'` or `size='huge'`, and Mantine silently
* ignores a size it does not know — the button renders at the default size, with no
* warning and nothing on screen to say a size was asked for at all. One table here
* is cheaper and safer than rewriting the size on several hundred call sites, and it
* keeps the vocabulary the rest of the app already reads in.
*/
const semanticSizes: Record<string, string> = {
const sizeAliases: Record<string, string> = {
mini: 'xs',
tiny: 'xs',
small: 'sm',
@ -60,9 +60,9 @@ const semanticSizes: Record<string, string> = {
};
// Generic so it passes each component's own size type straight through; only a
// string that Semantic knew about is rewritten.
// string in the table above is rewritten.
export const resolveSize = <T, >(size: T): T =>
typeof size === 'string' ? ((semanticSizes[size] ?? size) as T) : size;
typeof size === 'string' ? ((sizeAliases[size] ?? size) as T) : size;
export const Button = forwardRef<HTMLButtonElement, ButtonProps>((
{role, icon, iconAfter, className, children, ...props}, ref
@ -105,9 +105,8 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>((
ref={ref}
/*
* An `href` makes this an anchor. Mantine needs `component='a'` to render one, and
* without it the href is accepted and silently dropped — the control looks like a
* link and navigates nowhere. Semantic used `as='a'` for this, so every migrated
* call site that carried `href` alone was quietly broken.
* without it the href is accepted and silently dropped, so a call site carrying
* `href` alone renders a control that looks like a link and navigates nowhere.
*/
component={props.component ?? (props.href ? 'a' : undefined)}
// An explicit color/variant still wins, so a call site can deviate when it must.

View file

@ -15,8 +15,8 @@ import {Icon} from './Icon';
* Wide tables scroll in their own container so the page body never scrolls
* sideways.
*
* The compound names mirror Semantic's (Header/Body/Row/Cell/HeaderCell/Footer)
* so migrating a call site is a rename, not a rewrite.
* Compound: Table.Header / Body / Row / Cell / HeaderCell / Footer, so the markup at
* a call site reads the way the rendered table is structured.
*/
export interface TableProps extends MTableProps {

View file

@ -2,7 +2,7 @@ import React from 'react';
import {Loader as MLoader, Skeleton} from '@mantine/core';
import {Icon} from './Icon';
import {pxToRem} from './scale';
import {RoleName, SemanticColorName} from '../../themes/mantine';
import {RoleName, PaletteColorName} from '../../themes/mantine';
/*
* Feedback: messages, labels, progress, status text, loaders.
@ -33,7 +33,7 @@ export interface MessageProps {
kind?: MessageKind;
title?: React.ReactNode;
children?: React.ReactNode;
/** Semantic icon name or a Tabler component, shown to the left of the text. */
/** An icon name (see Icon.tsx) or a Tabler component, shown to the left of the text. */
icon?: string | React.ComponentType<any>;
/** Renders a dismiss button. Omit for messages the user cannot clear. */
onDismiss?: () => void;
@ -69,7 +69,7 @@ export function Message({kind = 'info', title, children, icon, onDismiss, classN
// -------------------------------------------------------------------- Labels
export interface LabelProps {
color?: SemanticColorName | RoleName | 'black' | 'white';
color?: PaletteColorName | RoleName | 'black' | 'white';
icon?: string | React.ComponentType<any>;
/**
* Draw it as a physical tag — pointed left edge and an eyelet — rather than a plain
@ -108,7 +108,7 @@ export interface ProgressProps {
indeterminate?: boolean;
/** Show the percentage inside the bar. */
showPercent?: boolean;
color?: SemanticColorName | RoleName;
color?: PaletteColorName | RoleName;
/** Replaces the percentage with arbitrary text (e.g. "2.1 GB / 5.3 GB"). */
label?: React.ReactNode;
className?: string;
@ -208,8 +208,8 @@ export interface LoadingProps {
/**
* A centered loader with a caption, for a region that has nothing to show yet.
* Replaces Semantic's `<Loader active inline='centered'>`, which 19 call sites
* used with their own wrapper div.
* A centred spinner with optional text under it. The wrapper is the component's,
* so the 19 call sites that show one do not each roll their own.
*/
export function Loading({children, size = 'sm', padding = '2em'}: LoadingProps) {
// `padding` is the caller's, so it stays inline; the rest is in ui.css so it scales.
@ -219,7 +219,7 @@ export function Loading({children, size = 'sm', padding = '2em'}: LoadingProps)
</div>
}
/** Placeholder for content that has not arrived. Replaces Semantic's Placeholder. */
/** Placeholder for content that has not arrived: a few skeleton lines. */
export function Placeholder({lines = 3, height = 12}: {lines?: number; height?: number}) {
return <div className='wrolpi-placeholder'>
{Array.from({length: lines}, (_, index) => <Skeleton

View file

@ -4,8 +4,9 @@ import * as Tabler from '@tabler/icons-react';
/*
* Icons.
*
* Semantic UI shipped an icon font; we use Tabler's bundled SVG components, which
* are tree-shaken at build time and need nothing from the network.
* Tabler's bundled SVG components, which are tree-shaken at build time and need
* nothing from the network -- an icon font would have to be fetched, and WROLPi
* has to draw its interface with no connection at all.
*
* Icons never name a color. They stroke with `currentColor`, inheriting from the
* button or text that wraps them, so status color and every theme come for free.
@ -17,13 +18,14 @@ export type IconSize = 'small' | 'medium' | 'large';
const sizes: Record<IconSize, number> = {small: 16, medium: 20, large: 24};
/**
* Semantic icon name -> Tabler component name.
* WROLPi's icon names -> Tabler component names.
*
* Covers every name the app renders today so call sites can migrate without also
* choosing new icons. New code should import the Tabler component directly and
* pass it as `component`; this map exists for the migration, not forever.
* The app names an icon by what it is for -- `icon='book'`, `icon='arrow left'` --
* rather than by which component draws it, so hundreds of call sites are not coupled
* to the icon set. Swapping Tabler for something else is then this table and nothing
* else. New code may pass a Tabler component directly as `component` instead.
*/
const semanticNames: Record<string, keyof typeof Tabler> = {
const iconNameAliases: Record<string, keyof typeof Tabler> = {
'add': 'IconPlus',
'apple': 'IconBrandApple',
'archive': 'IconArchive',
@ -178,7 +180,7 @@ export interface IconStackProps {
/**
* Two icons composed into one symbol — a wifi glyph with a question mark on it,
* say. Replaces Semantic's IconGroup.
* say.
*
* The corner glyph gets a background matching the surface so it stays legible
* over the icon beneath it; see `--icon-stack-bg` in ui.css.
@ -196,7 +198,7 @@ export function IconStack({children, corner, label, style, className}: IconStack
}
export interface IconProps extends Omit<React.SVGProps<SVGSVGElement>, 'name' | 'ref'> {
/** A Semantic UI icon name, for migrated call sites. */
/** One of the icon names in `iconNameAliases`. */
name?: string;
/** A Tabler component, for new code. */
component?: React.ComponentType<any>;
@ -207,9 +209,9 @@ export interface IconProps extends Omit<React.SVGProps<SVGSVGElement>, 'name' |
label?: string;
}
/** Resolve a Semantic icon name to its Tabler component, or undefined if unmapped. */
/** Resolve an icon name to its Tabler component, or undefined if unmapped. */
export const resolveIconName = (name: string): React.ComponentType<any> | undefined => {
const taberName = semanticNames[name.trim().toLowerCase()];
const taberName = iconNameAliases[name.trim().toLowerCase()];
return taberName ? (Tabler[taberName] as React.ComponentType<any>) : undefined;
}
@ -219,7 +221,7 @@ export function Icon({name, component, size = 'small', loading, label, ...props}
Component = resolveIconName(name);
if (!Component) {
// Loud on purpose: a silently missing icon leaves a hole in the interface.
console.error(`No icon for Semantic name "${name}"; pass a Tabler component instead.`);
console.error(`No icon named "${name}"; add it to iconNameAliases, or pass a Tabler component.`);
Component = Tabler.IconHelpCircle;
}
}

View file

@ -49,7 +49,7 @@ export function Checkbox({style, ...props}: CheckboxProps) {
export type ToggleProps = React.ComponentProps<typeof MSwitch>;
/** An on/off switch. Replaces Semantic's `Radio toggle`. */
/** An on/off switch. */
export function Toggle({style, className, ...props}: ToggleProps) {
return <MSwitch
className={['wrolpi-switch', className].filter(Boolean).join(' ')}
@ -146,8 +146,8 @@ export interface ActionInputProps extends React.ComponentProps<typeof MTextInput
}
/**
* A text input with a button joined to its right edge. Replaces Semantic's
* `<Input action={...}/>`.
* A text input with a button joined to its right edge -- a search field and its
* clear control, a path field and its browse control.
*
* The two share one outline rather than sitting side by side, so the pair reads
* as a single control — the input's own right border is dropped and the button

View file

@ -25,10 +25,8 @@ export interface PaginationProps {
}
/**
* Page controls. Replaces Semantic's Pagination, keeping its prop names
* (`activePage`, `totalPages`, `onPageChange`) so call sites migrate as a
* rename — but `onPageChange` receives the page number directly rather than
* Semantic's `(event, data)` pair.
* Page controls: `activePage`, `totalPages`, `onPageChange`. `onPageChange` receives
* the page number directly.
*/
export function Pagination({
activePage,

View file

@ -9,10 +9,9 @@ import {Button} from './Button';
/*
* Modals.
*
* Semantic's compound shape (`Modal.Header` / `Modal.Content` / `Modal.Actions`)
* is kept because 34 call sites are written against it. The subcomponents are
* markers: Modal reads them out of its children and places each in the right
* slot, so the children stay in source order at the call site.
* Compound: `Modal.Header` / `Modal.Content` / `Modal.Actions`. The subcomponents are
* markers -- Modal reads them out of its children and places each in the right slot, so
* the children stay in source order at the call site.
*/
const ModalHeader = ({children}: {children?: React.ReactNode}) => <>{children}</>;
@ -24,7 +23,7 @@ ModalContent.displayName = 'Modal.Content';
const ModalActions = ({children}: {children?: React.ReactNode}) => <>{children}</>;
ModalActions.displayName = 'Modal.Actions';
// Semantic's size names, mapped onto Mantine's scale.
// WROLPi's size names, mapped onto Mantine's scale. See `sizeAliases` in Button.tsx.
const modalSizes: Record<string, string> = {
mini: 'xs',
tiny: 'sm',
@ -34,12 +33,12 @@ const modalSizes: Record<string, string> = {
};
export interface ModalProps extends Omit<React.ComponentProps<typeof MModal>, 'opened' | 'size'> {
/** Semantic's name for `opened`. Either works. */
/** An alias for `opened`. Either works. */
open?: boolean;
opened?: boolean;
/** A title given directly, instead of a `Modal.Header` child. */
title?: React.ReactNode;
/** Semantic size name (mini/tiny/small/large/fullscreen) or a Mantine size. */
/** A size name (mini/tiny/small/large/fullscreen) or a Mantine size. */
size?: string | number;
/** Accepted and ignored: the close button is always shown. */
closeIcon?: boolean;
@ -52,8 +51,7 @@ export interface ModalProps extends Omit<React.ComponentProps<typeof MModal>, 'o
* modal is open -- there is no check for whether it is the one the user is looking at. Two
* modals open meant two listeners, so a single Escape closed BOTH: opening the search modal
* over the dashboard's download modal and pressing Escape once left you on the dashboard.
* Semantic handled this; the migration lost it.
*
* Mantine's own answer is `Modal.Stack` with a `stackId` per modal, which is the only thing
* that gates `closeOnEscape`. It is not used here for two reasons: it requires every call
* site to be rewritten to hoist its modal into a stack, and it sets `__hidden` on everything
@ -246,7 +244,7 @@ export function Confirm({
export type TooltipProps = React.ComponentProps<typeof MTooltip>;
/** Replaces Semantic's Popup. */
/** A hover/focus tooltip. */
export function Tooltip(props: TooltipProps) {
return <MTooltip withArrow={false} openDelay={200} {...props}/>
}

View file

@ -6,9 +6,9 @@ import {Loader} from './Feedback';
/*
* The search box, with optional grouped suggestions.
*
* Replaces Semantic's `<Search category>`, and deliberately keeps its `results`
* shape — `{groupKey: {name, results: [{title, description}]}}` — because four
* call sites already build that structure from the API.
* Grouped search suggestions. `results` is
* `{groupKey: {name, results: [{title, description}]}}`, which is the shape four call
* sites already build from the API.
*
* Suggestions are a combobox: the input keeps focus and owns the keyboard, while
* `aria-activedescendant` points at the highlighted option. Arrow keys move,
@ -64,6 +64,16 @@ export interface SearchBoxProps {
className?: string;
autoFocus?: boolean;
name?: string;
/**
* Set on the input, not the wrapper, so a caller's own `<label htmlFor>` reaches it.
*
* SearchBox takes named props rather than spreading a rest object, so an `id` handed
* to it used to be dropped in silence. `DestinationForm` passes one and renders a
* label pointing at it: the download forms' Destination field has therefore had a
* label associated with nothing at all, which a pointer user never notices and a
* screen reader user gets nothing from.
*/
id?: string;
}
/** Flatten the groups into the order they are displayed, for keyboard movement. */
@ -90,6 +100,7 @@ export function SearchBox({
className,
autoFocus,
name,
id,
}: SearchBoxProps) {
const [open, setOpen] = useState(false);
const [highlighted, setHighlighted] = useState(-1);
@ -179,6 +190,7 @@ export function SearchBox({
<input
ref={inputRef}
type='text'
id={id}
name={name}
className='wrolpi-searchbox-input'
placeholder={placeholder}

View file

@ -16,7 +16,7 @@ export type HeaderLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5';
export interface HeaderProps extends Omit<React.HTMLAttributes<HTMLHeadingElement>, 'color'> {
/** Heading level. Also the visual size — pick it for the outline, not the look. */
as?: HeaderLevel;
/** Semantic icon name or a Tabler component, shown before the text. */
/** An icon name (see Icon.tsx) or a Tabler component, shown before the text. */
icon?: string | React.ComponentType<any>;
/** Secondary line below, in muted text. */
subheader?: React.ReactNode;
@ -42,8 +42,8 @@ export interface HeaderProps extends Omit<React.HTMLAttributes<HTMLHeadingElemen
}
/**
* A section heading. Named and shaped like Semantic's Header (`as`, `icon`,
* `subheader`, `dividing`) so the app's 47 call sites migrate by import.
* A section heading: `as` picks the level, with optional `icon`, `subheader` and a
* `dividing` rule underneath.
*
* Sizes come from ui.css, keyed on the level, rather than each call site
* choosing — that is what keeps the type scale a scale.
@ -96,7 +96,7 @@ export interface PanelProps extends React.HTMLAttributes<HTMLDivElement> {
danger?: boolean;
}
/** A bordered surface. Replaces Semantic's Segment. */
/** A bordered surface: the panel most of the app's content sits on. */
export function Panel({danger, className, children, ...props}: PanelProps) {
return <div
className={['wrolpi-panel', danger ? 'wrolpi-panel-danger' : '', className]

View file

@ -1,7 +1,7 @@
/*
* The WROLPi component library: token-driven components built on Mantine.
*
* Import from here, not from `@mantine/core` or `semantic-ui-react`:
* Import from here, not from `@mantine/core` directly:
* import {Button, Table, Message} from '../components/ui';
*
* Everything resolves its colors through theme tokens, so no component branches

View file

@ -11,7 +11,7 @@ import {clearToasts, toast} from './toast';
* Tests for toasts.
*
* `toast` has thirteen call sites and had no test at all, having already failed once in the
* way that is hardest to notice: App.js dropped Semantic's toast container while seven pages
* way that is hardest to notice: App.js dropped the old toast container while seven pages
* were still importing the old helper, so every notification in the app silently went
* nowhere. Nothing threw, and nothing looked wrong until somebody expected a message.
*
@ -175,7 +175,7 @@ describe('toast', () => {
describe('a toast that can be clicked', () => {
/*
* Semantic's toast took an `onClick` in its options and made the whole toast activate it.
* The old toast took an `onClick` in its options and made the whole toast activate it.
* `Events.js` still passes one -- it did before the migration and was never changed -- for
* three events that each open a URL: a shared page pushed from the browser extension, a
* finished archive upload, and a generated screenshot. The migrated helper destructured
@ -314,7 +314,7 @@ describe('how long a toast stays up', () => {
it('stays until dismissed when the caller asks for time 0', async () => {
/*
* Semantic treated `time: 0` as "stay up". Mantine wants `autoClose: false`: its
* Callers spell "stay up" as `time: 0`. Mantine wants `autoClose: false`: its
* `getAutoClose` returns any number as given, and the container then calls
* `setTimeout(hide, 0)` -- so passing the 0 straight through dismisses the toast on the
* next tick. Call sites use 0 for errors the user must actually see, so getting this

View file

@ -5,9 +5,8 @@ import {RoleName} from '../../themes/mantine';
/*
* Toasts.
*
* Keeps the react-semantic-toasts-2 call signature so the ~19 call sites migrate
* by changing their import, not their code. The container is mounted by
* ThemeProvider.
* One call signature for the ~19 places that raise a toast. The container is mounted
* by ThemeProvider, so a caller only says what happened.
*/
/* `error` is the original spelling and stays; `danger` is the token's name for it. */
@ -20,9 +19,9 @@ export interface ToastOptions {
/** Milliseconds before auto-dismiss. 0 keeps it until dismissed. */
time?: number;
/**
* Makes the whole toast activate this — Semantic's behaviour, which `Events.js` still
* relies on for the three events that carry a URL: a page shared from the browser
* extension, a finished archive upload, and a generated screenshot.
* Makes the whole toast activate this. `Events.js` relies on it for the three events
* that carry a URL: a page shared from the browser extension, a finished archive
* upload, and a generated screenshot.
*/
onClick?: () => void;
}
@ -74,7 +73,7 @@ export function toast({type = 'info', title, description, time = 5000, onClick}:
title,
message: description,
color: toastRoles[type],
// Semantic treated 0 as "stay up"; Mantine wants false for that. Its `getAutoClose`
// Callers spell "stay up" as `time: 0`; Mantine wants `false`. Its `getAutoClose`
// returns any number as given and the container then calls `setTimeout(hide, 0)`, so
// passing the 0 through dismisses the toast on the next tick.
autoClose: time === 0 ? false : time,

View file

@ -365,9 +365,8 @@ html[data-theme="amber"] .wrolpi-searchbox-control:focus-within {
*
* 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.
* rather than as a control of its own. It should read as an action button welded to the
* field: zero gap to the input, zero to the outer edge, and the field's full 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
@ -503,7 +502,7 @@ html .wrolpi-searchbox-clear {
margin-top: 1.125rem;
}
/* A stack of form rows, replacing Semantic's `ui stackable grid` in edit forms. */
/* A stack of form rows: the layout of every edit form. */
.wrolpi-form-rows {
display: flex;
flex-direction: column;
@ -525,9 +524,9 @@ html .wrolpi-searchbox-clear {
}
/*
* `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.
* `hooks/useForm.js` renders its own <label> and native <input> rather than a library
* control. These rules dress a native input from the tokens, so one of those fields
* matches the library's own controls beside it.
*/
.wrolpi-form-field input {
@ -725,11 +724,10 @@ html[data-theme="amber"] .wrolpi-path-input-control:focus-within {
* 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.
* A switch's label and description are body text, stated from the tokens rather than
* inherited. Mantine draws the description from its own gray palette, which is a
* non-red pixel in night mode; and an inherited color is only ever as good as whatever
* the switch happens to be mounted inside.
*/
.wrolpi-switch .mantine-Switch-label {
color: var(--text);
@ -760,9 +758,9 @@ html[data-theme="amber"] .wrolpi-path-input-control:focus-within {
/* ----------------------------------------------------------------- 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.
* 12px text with ~7px of vertical padding. 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, and a phone is where it is hardest to hit.
*/
.wrolpi-label {
display: inline-flex;
@ -818,8 +816,8 @@ html[data-theme="amber"] .wrolpi-path-input-control:focus-within {
/*
* The point is centred on the left edge, so its inner half covers the body's first 10.5px
* -- 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.
* without this the first letter is hidden under the point. The asymmetry is the point's
* width, not a nudge.
*/
padding-left: 0.8125rem;
}
@ -970,10 +968,10 @@ html[data-theme="night"] .wrolpi-tag::before {
gap: 0.5rem;
margin: 0;
/*
* Stated explicitly because semantic.min.css sets `h1,h2,h3,h4,h5 { font-family:
* Lato }` and loads after Mantine's element rule, so headings would ignore the
* theme's body face — visible in amber, where the body is mono and headings were
* not. A class outranks the element selector. Can go once Semantic is gone.
* The face is stated here rather than left to inheritance or to Mantine's heading
* config, so a heading cannot quietly diverge from the theme's body face. Amber is
* where that shows: its body is mono, and a heading that misses the token renders in
* a proportional face beside it.
*/
font-family: var(--font-body);
font-weight: 600;
@ -1203,9 +1201,8 @@ html .mantine-Notification-closeButton:hover {
}
/*
* 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.
* Indeterminate: a sliding band, for work whose size is not known yet. 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%;
@ -1327,10 +1324,10 @@ html[data-theme="amber"] .wrolpi-progress-fill {
/*
* The same idea along the other axis: a row of buttons, spaced by the row.
*
* Semantic's Button carried `margin: 0 .25em .25em 0`, so a page could list buttons as bare
* Buttons used to carry `margin: 0 .25em .25em 0` of their own, so a page could list them as bare
* siblings and they arrived both spaced and, when the row ran out of width, separated from the line
* below. Nothing replaced that margin -- the archive page's six actions shared edges, and the
* downloads table's two icon buttons did too.
* below. Once that margin went nothing replaced it -- the archive page's six actions shared edges,
* and the downloads table's two icon buttons did too.
*
* Two of the archive page's six had `marginTop: 0.5em` instead, which is where the "Update sits
* lower than the rest" came from: a Button is `display: inline-flex; vertical-align: middle`, and
@ -1380,10 +1377,9 @@ html[data-theme="amber"] .wrolpi-progress-fill {
/* ------------------------------------------------------------- Or divider */
/*
* Two choices of equal weight, split down the middle -- Semantic's `Segment placeholder` with a
* vertical `Divider`, which the dashboard's Download/Upload panel was built as.
* Two choices of equal weight, split down the middle: the dashboard's Download/Upload panel.
*
* The migrated version was a `Group` with a divider between two buttons, so the halves were sized
* An earlier version was a flex row with a divider between two buttons, so the halves were sized
* to their buttons rather than to the panel: the pair huddled in the centre with the rule wherever
* they left it, instead of two equal fields with the rule at the true middle.
*
@ -1501,8 +1497,8 @@ html[data-theme="amber"] .wrolpi-progress-fill {
*/
:root {
--card-poster-max-height: 12.5rem;
/* How far a disabled control fades. Semantic used 0.45 and it reads correctly against
every theme's panel; the tests read this rather than repeating the number. */
/* How far a disabled control fades. 0.45 reads correctly against every theme's panel;
the tests read this rather than repeating the number. */
--disabled-opacity: 0.45;
}
@ -1641,9 +1637,8 @@ html[data-theme="amber"] .wrolpi-card-tag {
/*
* 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.
* neighbours. Without it the labels run together into one unreadable line
* wherever a statistic is used outside a group.
*/
.wrolpi-statistic {
@ -1846,9 +1841,9 @@ html[data-theme="amber"] .wrolpi-th-sort:focus-visible {
* 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".
* `--mantine-color-disabled`. Keeping the color and dropping the whole control to
* `opacity: 0.45` 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

View file

@ -6,7 +6,7 @@ import userEvent from '@testing-library/user-event';
import {MantineProvider} from '@mantine/core';
import {IconMoodSmile} from '@tabler/icons-react';
import {ThemeContext} from '../../contexts/contexts';
import {cssVariablesResolver, mantineTheme, semanticColorNames} from '../../themes/mantine';
import {cssVariablesResolver, mantineTheme, paletteColorNames} from '../../themes/mantine';
import {themeChoices} from '../../themes/names';
import {
ActionInput,
@ -49,7 +49,7 @@ const renderUI = (ui) => render(
);
describe('Icon', () => {
it('renders a Semantic name as an SVG', () => {
it('renders a named icon as an SVG', () => {
const {container} = renderUI(<Icon name='trash'/>);
expect(container.querySelector('svg')).toBeInTheDocument();
@ -61,7 +61,7 @@ describe('Icon', () => {
expect(container.querySelector('svg')).toBeInTheDocument();
});
it('maps every Semantic icon name the app uses', () => {
it('maps every icon name the app uses', () => {
// A name that falls through renders a fallback glyph, leaving a hole in the UI.
const names = ['trash', 'warning sign', 'download', 'refresh', 'plus', 'edit', 'wifi',
'search', 'lock', 'check', 'upload', 'play', 'folder', 'filter', 'file text',
@ -212,7 +212,7 @@ describe('Button', () => {
expect(screen.getByRole('button', {name: 'Save'})).toBeInTheDocument();
});
it('translates Semantic size names, which Mantine would otherwise ignore', () => {
it('translates our size names, which Mantine would otherwise ignore', () => {
// Unmigrated call sites still pass size='tiny'. Mantine drops a size it does not
// recognise without warning, so the button quietly renders at the default size.
renderUI(<><Button size='tiny'>Old</Button><Button size='sm'>New</Button></>);
@ -225,7 +225,7 @@ describe('Button', () => {
it('renders an anchor when given an href, without being told twice', () => {
/*
* Mantine drops `href` unless it also gets `component='a'`, so a button carrying
* only an href looked like a link and navigated nowhere. Semantic spelled this
* only an href looked like a link and navigated nowhere. The old library spelled this
* `as='a'`, so every migrated call site that kept just the href was silently
* broken — a download button that downloads nothing.
*/
@ -242,7 +242,7 @@ describe('Button', () => {
it('forwards a ref to the underlying DOM element', () => {
/*
* Ported from the old Theme.test.js, which caught a real crash: Semantic's Button
* Ported from the old Theme.test.js, which caught a real crash: the old Button
* was a class component, so a forwarded ref resolved to the class instance, and
* anything that called `node.contains(...)` on the trigger -- a Modal or Popup
* portal checking whether a click landed inside -- threw "contains is not a
@ -621,7 +621,7 @@ describe('Progress', () => {
it('keeps its percent text readable on every bar color', () => {
/*
* Ported from Theme.test.js, which asserted Semantic's `inverted-progress-text`
* Ported from Theme.test.js, which asserted the old `inverted-progress-text`
* class across all fourteen colors and both modes. That class is gone; the
* mechanism now is that the text always takes `--text` while light mode lightens
* the fill beneath it, because the text sits across both the filled and unfilled
@ -649,7 +649,7 @@ describe('Progress', () => {
});
it('reports no value when the size is unknown', () => {
// Semantic called this `indicating`. An upload that has not reported its size
// An upload that has not reported its size
// would otherwise sit at 0% and read as stalled, and `aria-valuenow=0` would tell
// a screen reader the same wrong thing.
renderUI(<Progress indeterminate label='Uploading…'/>);
@ -676,12 +676,12 @@ describe('Status', () => {
});
describe('Label', () => {
it('supports every Semantic color', () => {
it('supports every palette color', () => {
const {container} = renderUI(<>
{semanticColorNames.map(color => <Label key={color} color={color}>{color}</Label>)}
{paletteColorNames.map(color => <Label key={color} color={color}>{color}</Label>)}
</>);
expect(container.querySelectorAll('.wrolpi-label')).toHaveLength(semanticColorNames.length);
expect(container.querySelectorAll('.wrolpi-label')).toHaveLength(paletteColorNames.length);
});
});
@ -802,7 +802,7 @@ describe('nothing paints a label\'s text with an inline color', () => {
});
describe('Table', () => {
it('renders a Semantic-shaped compound table', () => {
it('renders a compound table', () => {
renderUI(<Table>
<Table.Header>
<Table.Row><Table.HeaderCell>URL</Table.HeaderCell></Table.Row>
@ -882,7 +882,7 @@ describe('Table', () => {
describe('Card', () => {
it('draws a mimetype accent from a token, not a hex', () => {
// File cards carry the mimetype's color on their top edge so a grid of results is
// scannable by kind. Semantic did this with `<Card color='violet'>`; two migrated
// scannable by kind. The old card did this with `<Card color='violet'>`; two migrated
// files had dropped the accent because our Card had no equivalent.
const {container} = renderUI(<Card title='Water Storage.pdf' color='red'/>);
@ -1520,7 +1520,7 @@ describe('modal sizes', () => {
/*
* The size table is the single thing that halved every modal in the migration.
*
* Semantic's names were measured on the pre-migration build: mini 360, tiny 540, small
* The names were measured on the build users had before: mini 360, tiny 540, small
* 720, large 1080, fullscreen 95%. They are mapped onto Mantine's much smaller scale, so
* a modal that said `small` went from 720px to 440px and one that said nothing at all
* went from 900px to 440px. That is deliberate now -- the sizes were audited call site
@ -1543,7 +1543,7 @@ describe('modal sizes', () => {
});
});
it('maps every Semantic name onto that scale', () => {
it('maps every size name onto that scale', () => {
// The mapping the call sites are written against. `fullscreen` is the one that did
// not shrink, and the one several audited modals were moved TO.
const source = fs.readFileSync(path.join(__dirname, 'Overlays.tsx'), 'utf8');
@ -1595,7 +1595,7 @@ describe('modal sizes', () => {
/*
* Two different hazards, and neither announces itself.
*
* A misspelled SEMANTIC name is not in the table, so it passes through untranslated,
* A misspelled name is not in the table, so it passes through untranslated,
* Mantine does not recognise it either, and the modal silently renders at the default
* 440px. A raw MANTINE name -- `sm`, `md` -- is recognised and honoured: it works,
* so nothing ever complains, and the call site quietly sits outside the vocabulary

View file

@ -25,7 +25,7 @@ export const useIsTouchDevice = () => {
/** @type {React.Context<import('../types/theme').ThemeContextValue>} */
/*
* The default is the declared shape of this context, and test-fixtures.test.js holds the
* fixtures to it key for key. It used to carry `i`, `s`, `t` and `inverted` -- Semantic
* fixtures to it key for key. It used to carry `i`, `s`, `t` and `inverted` -- the old
* compatibility props holding hardcoded greys -- long after ThemeProvider stopped supplying
* them, so the context advertised four properties that were always undefined in a real tree.
* FileBrowser was still reading one of them.

View file

@ -264,13 +264,13 @@ describe('useDriveHealth', () => {
describe('Paginator rendering', () => {
test('renders exactly totalPages numbered page links (no phantom trailing link)', () => {
// 48 results at 24 per page -> 2 pages. If totalPages is 3, semantic-ui
// 48 results at 24 per page -> 2 pages. If totalPages is 3, the pagination
// will render an extra "3" link that fetches an empty page.
render(
<Paginator activePage={1} totalPages={2} onPageChange={() => {}}/>
);
// Semantic UI renders page numbers as <a> elements with the page text.
// Page numbers render as <a> elements carrying the page text.
// Tablet + mobile variants both render here because Media is mocked to
// pass children through, so each numeric page appears at least once.
expect(screen.queryAllByText('1').length).toBeGreaterThan(0);

View file

@ -341,7 +341,7 @@ export function InputForm({
inputProps.disabled = disabled || inputProps.disabled;
/*
* Callers build these with Semantic's shape -- {content, header, positive|negative} --
* Callers build these as {content, header, positive|negative} --
* and the pages that build them are migrating separately, so both spellings are
* accepted until they have all moved to {kind, title}.
*/

View file

@ -774,7 +774,7 @@ describe('useForm', () => {
const [selectionProps] = result.current.getSelectionProps({name: 'selected'});
act(() => {
// Semantic UI dropdown passes (event, {value})
// A dropdown passes (event, {value})
selectionProps.onChange({}, {value: 'option2'});
});

View file

@ -0,0 +1,81 @@
import fs from 'fs';
import path from 'path';
/*
* Semantic UI is gone, and this is what keeps it gone.
*
* It began as a ratchet: a JSON baseline listing the 75 files that still imported the
* library, which was only ever allowed to shrink. The list reached zero and the file has
* been deleted with it; what is left is the part that still has teeth.
*
* An import of a missing module fails loudly on its own, so that is not really what this
* guards. It guards the quiet ways the library comes back: reinstalling a package to
* "just use a Semantic component for this one thing", which drags a 300kB stylesheet into
* the bundle with it, or writing `className="ui stackable grid"` in the belief that those
* rules are still loaded, which they have not been since Phase 4 -- markup like that does
* not error, it simply has no effect, and the layout quietly comes out wrong.
*
* It is a test rather than an ESLint rule deliberately: `no-restricted-imports` would
* either print warnings on every dev rebuild or fail the build outright, and CI unsets CI=
* for its build step, so warnings there guard nothing.
*
* New UI code imports from `src/components/ui`.
*/
const SRC = __dirname;
const REPO = path.join(SRC, '..');
/** Every source file under src/, excluding tests. */
const sourceFiles = (dir = SRC) => fs.readdirSync(dir, {withFileTypes: true}).flatMap(entry => {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) return sourceFiles(full);
if (!/\.(js|jsx|ts|tsx)$/.test(entry.name)) return [];
if (/\.(test|cy)\.(js|jsx|ts|tsx)$/.test(entry.name)) return [];
return [path.relative(REPO, full)];
});
const filesMatching = (pattern) => sourceFiles()
.filter(file => pattern.test(fs.readFileSync(path.join(REPO, file), 'utf8')))
.sort();
describe('Semantic UI stays removed', () => {
it('scans a source tree, so a clean result means something', () => {
// Every assertion below is "this list is empty". Without this one they would all
// pass just as well if the walk found no files at all.
expect(sourceFiles().length).toBeGreaterThan(100);
});
it('has no file importing Semantic UI', () => {
expect(filesMatching(/from ['"](semantic-ui-react|semantic-ui-react\/)/)).toEqual([]);
});
it('has nobody left calling the Semantic toast library', () => {
/*
* A toast raised through react-semantic-toasts-2 needs `<SemanticToastContainer/>`
* mounted, and that came out of App.js when App.js migrated. So this import fails
* in the way that is hardest to notice: no error, no warning, just a notification
* the user is never shown. Import `toast` from `src/components/ui` instead; it
* takes the same options.
*/
expect(filesMatching(/from ['"]react-semantic-toasts-2/)).toEqual([]);
});
it('has no file leaning on the Semantic stylesheet', () => {
/*
* `className="ui stackable grid"` and friends. semantic.min.css is not loaded, so
* a `ui ...` class name now styles nothing at all -- which is why this cannot be
* left to the import check: nothing about it fails, the element simply loses the
* layout its author expected.
*/
expect(filesMatching(/className=["']ui /)).toEqual([]);
});
it('has no Semantic package installed', () => {
const pkg = JSON.parse(fs.readFileSync(path.join(REPO, 'package.json'), 'utf8'));
const named = [...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.devDependencies || {})]
.filter(name => /semantic/i.test(name));
expect(named).toEqual([]);
});
});

View file

@ -1 +0,0 @@
[]

View file

@ -1,134 +0,0 @@
import fs from 'fs';
import path from 'path';
import baseline from './semantic-baseline.json';
/*
* Semantic UI is gone, and this keeps it gone.
*
* It began as a ratchet: semantic-baseline.json listed the 75 files still importing
* Semantic UI, the list only shrank, and adding a file to it failed the build. The list
* reached zero, the three packages are uninstalled, and the baseline is now an empty
* array kept only so a reintroduction has to delete a file rather than edit one.
*
* The assertions below are cheap and each one caught something real during the migration,
* so they stay: an import would now fail to resolve, but a `className="ui button"` or a
* reach back through Theme.tsx would not, and neither would reinstalling a package.
*
* It is a test rather than an ESLint rule deliberately: `no-restricted-imports` would
* either print dozens of warnings on every dev rebuild or fail the build outright, and CI
* unsets CI= for its build step, so warnings there guard nothing.
*/
const SRC = path.join(__dirname);
const LIBRARIES = /from ['"](semantic-ui-react|semantic-ui-react\/|react-semantic-toasts-2)/;
/** Every source file under src/, excluding tests. */
const sourceFiles = (dir = SRC) => fs.readdirSync(dir, {withFileTypes: true}).flatMap(entry => {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) return sourceFiles(full);
if (!/\.(js|jsx|ts|tsx)$/.test(entry.name)) return [];
if (/\.(test|cy)\.(js|jsx|ts|tsx)$/.test(entry.name)) return [];
return [path.relative(path.join(SRC, '..'), full)];
});
const importers = () => sourceFiles()
.filter(file => LIBRARIES.test(fs.readFileSync(path.join(SRC, '..', file), 'utf8')))
.sort();
describe('Semantic UI removal', () => {
it('has no new files importing Semantic UI', () => {
const added = importers().filter(file => !baseline.includes(file));
// New code must import from src/components/ui instead. See ui-migration-plan.md.
expect(added).toEqual([]);
});
it('keeps the baseline free of files that no longer import Semantic UI', () => {
const current = importers();
const stale = baseline.filter(file => !current.includes(file));
// Migrating a file should also delete its line here, so the count means something.
expect(stale).toEqual([]);
});
it('has nobody left calling the Semantic toast library', () => {
/*
* This one is a ratchet at zero, not a shrinking list.
*
* The Semantic toasts only appear if `<SemanticToastContainer/>` is mounted, and
* that container came out of App.js when App.js migrated. Any file that goes back
* to importing `toast` from react-semantic-toasts-2 would therefore raise
* notifications that silently never appear — no error, no warning, just a delete
* or a failure the user is never told about. Import `toast` from
* `src/components/ui` instead; it takes the same options.
*/
const TOASTS = /from ['"]react-semantic-toasts-2/;
const importers = sourceFiles()
.filter(file => TOASTS.test(fs.readFileSync(path.join(SRC, '..', file), 'utf8')));
expect(importers).toEqual([]);
});
it('declares every migrated file that still leans on Semantic CSS classes', () => {
/*
* A file can stop importing semantic-ui-react and still depend on its stylesheet
* by writing `className="ui stackable grid"`. The import ratchet above cannot see
* that, so when Phase 4 deletes semantic.min.css the layout would silently
* collapse. Anything on this list is a deliberate bridge that must be revisited
* then; anything NOT on it is an accident.
*/
// Empty, and it should stay that way: Semantic's stylesheet is no longer loaded,
// so a `ui ...` class name now styles nothing at all.
const allowed = [];
const bridges = sourceFiles()
.filter(file => !baseline.includes(file))
.filter(file => /className=["']ui /.test(fs.readFileSync(path.join(SRC, '..', file), 'utf8')));
expect(bridges.sort()).toEqual(allowed.sort());
});
it('lets no migrated file keep rendering Semantic through Theme.tsx', () => {
/*
* The import check above only sees `semantic-ui-react`. `Theme.tsx` re-exports
* Semantic-backed wrappers under friendly names, so a file could drop its direct
* imports, leave the baseline, and still put Semantic components on screen —
* which is exactly how a Semantic modal survived in the search shortcut long
* after that file looked migrated.
*
* Theme.tsx also re-exports theme *names* (`themeChoices`, `darkTheme`, ...),
* which are data and carry no markup; those come from `themes/names` instead, so
* any import from Theme in a migrated file is a component import and a mistake.
*/
const fromTheme = /from ['"](\.{1,2}\/)+Theme['"]/;
const leaks = sourceFiles()
.filter(file => !baseline.includes(file))
.filter(file => !file.endsWith('components/Theme.tsx'))
.filter(file => fromTheme.test(fs.readFileSync(path.join(SRC, '..', file), 'utf8')));
expect(leaks).toEqual([]);
});
it('has no Semantic package installed', () => {
/*
* The three packages are uninstalled. An import of a missing module fails loudly,
* so this guards the quieter mistake: someone reinstalling one -- to "just use a
* Semantic component for this one thing" -- which would drag the whole library and
* its 300kB stylesheet back into the bundle.
*/
const pkg = JSON.parse(
fs.readFileSync(path.join(SRC, '..', 'package.json'), 'utf8'));
const named = [...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.devDependencies || {})]
.filter(name => /semantic/i.test(name));
expect(named).toEqual([]);
});
it('keeps the baseline empty', () => {
// The migration is finished; the file stays as an empty list so that putting a
// name back requires a deliberate edit.
expect(baseline).toEqual([]);
expect(importers()).toEqual([]);
});
});

View file

@ -18,7 +18,7 @@ jest.mock('react-stl-viewer', () => ({
StlViewer: () => null,
}));
// Mock window.matchMedia (used by Semantic UI Media components)
// Mock window.matchMedia (used by the responsive Media components)
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({

View file

@ -11,7 +11,7 @@
* The second is drift, which is the worse of the two. A fixture that no longer matches the
* shape it stands in for makes a test *confidently* wrong: it passes while the component
* would break in the browser. The theme context fixture carried `i`, `s` and `t` with
* hardcoded Semantic greys for months after ThemeProvider stopped supplying them, and one
* hardcoded greys for months after ThemeProvider stopped supplying them, and one
* component was still reading a prop that no longer existed. test-fixtures.test.js compares
* these against the real contexts so that class of drift fails a test rather than surviving.
*

View file

@ -16,7 +16,7 @@ import {
* and the fixture is the last place anyone looks.
*
* That is not hypothetical here. The theme fixture supplied `i`, `s` and `t` holding
* hardcoded Semantic greys for months after ThemeProvider stopped supplying them, and
* hardcoded greys for months after ThemeProvider stopped supplying them, and
* FileBrowser was still reading `inverted` off the context and pasting it into a className --
* so the tests agreed with the fixture, the fixture agreed with nothing, and the file
* browser's footer stayed light in all three dark themes.
@ -128,7 +128,7 @@ describe('specs still mocking a context module', () => {
});
});
describe('the Semantic theme compatibility props are gone for good', () => {
describe('the theme compatibility props are gone for good', () => {
it('no context, fixture, or component carries i/s/t/inverted from the theme', () => {
/*
* These four props are why this whole file exists. They were removed from

View file

@ -29,16 +29,22 @@ export const roleNames = ['neutral', 'info', 'success', 'warning', 'danger'] as
export type RoleName = typeof roleNames[number];
/** The Semantic UI color names WROLPi uses. Each maps to a token of the same name. */
export const semanticColorNames = [
/**
* The named colors WROLPi uses, each mapping to a token of the same name.
*
* A hue, unlike a role: this is the palette a user picks a nav bar or a calculator group
* from, where the point is telling one thing from another rather than saying how serious
* it is. The same twelve as `navColorHexMap` in components/Vars.js.
*/
export const paletteColorNames = [
'red', 'orange', 'yellow', 'olive', 'green', 'teal', 'blue', 'violet', 'purple', 'pink',
'brown', 'grey',
] as const;
export type SemanticColorName = typeof semanticColorNames[number];
export type PaletteColorName = typeof paletteColorNames[number];
const semanticColors = Object.fromEntries(
[...semanticColorNames, ...roleNames].map(name => [name, tokenColor(name)])
const paletteColors = Object.fromEntries(
[...paletteColorNames, ...roleNames].map(name => [name, tokenColor(name)])
);
export const mantineTheme = createTheme({
@ -58,7 +64,7 @@ export const mantineTheme = createTheme({
// Mantine cannot compute contrast from a `var()`, so it must not try.
autoContrast: false,
colors: {
...semanticColors,
...paletteColors,
/*
* Mantine reads `dark` and `gray` shades directly for surfaces, borders, and
* secondary text — table borders, zebra stripes, input chrome, disabled states.

View file

@ -4,7 +4,7 @@ import {SavedThemeName, ThemeMediaFilter, ThemeName} from '../types/theme';
* Theme names and the choices offered by the pickers.
*
* These live here rather than in components/Theme.tsx so the component library can
* use them without importing that module, which still pulls in Semantic UI.
* use them without importing that module, which pulls in the whole ThemeProvider.
*/
export const darkTheme = 'dark';
@ -18,7 +18,7 @@ export const themeSessionKey = 'color-scheme';
/** Every theme a user can apply, in the order the pickers offer them. */
export const themeNames: ThemeName[] = [lightTheme, darkTheme, nightTheme, amberTheme];
/** Themes built on a dark background. Semantic components are `inverted` in these. */
/** Themes built on a dark background. */
export const darkThemes: ThemeName[] = [darkTheme, nightTheme, amberTheme];
/** Themes a user must choose deliberately; `prefers-color-scheme` never picks them. */
@ -146,7 +146,7 @@ export const resolveMediaFilter = (
export interface ThemeChoice {
value: SavedThemeName;
text: string;
/** Semantic icon name; resolved to a Tabler component by the Icon shim. */
/** An icon name; resolved to a Tabler component by Icon. */
icon: string;
description: string;
}

View file

@ -3,7 +3,7 @@ import path from 'path';
import {contrastRatio, isMeasurable} from './contrast';
import {defaultNavColor, navColorNames, navColorsFrom} from './navColors';
import {themeNames} from './names';
import {semanticUIColorMap} from '../components/Vars';
import {navColorHexMap} from '../components/Vars';
/*
* The navbar foreground, measured against the palette that actually ships.
@ -53,12 +53,12 @@ describe('the navbar palette', () => {
it('offers exactly the colors the favicon has an icon for', () => {
/*
* `semanticUIColorMap` supplies the fixed hexes for `/favicon-<color>.svg` and the
* `navColorHexMap` supplies the fixed hexes for `/favicon-<color>.svg` and the
* `theme-color` meta tag -- a file on disk and OS chrome, neither of which can take a
* token. A color offered here with no icon there falls back to violet, so the bar
* and the browser tab would disagree.
*/
expect([...navColorNames].sort()).toEqual(Object.keys(semanticUIColorMap).sort());
expect([...navColorNames].sort()).toEqual(Object.keys(navColorHexMap).sort());
});
it('defaults to a color it offers', () => {

View file

@ -53,7 +53,7 @@ import {bestForeground, contrastRatio, isMeasurable} from './contrast';
/**
* The colors offered for the bar, in picker order.
*
* The same keys as `semanticUIColorMap` in components/Vars.js, which maps them to the fixed
* The same keys as `navColorHexMap` in components/Vars.js, which maps them to the fixed
* hexes used for the favicon and the mobile status bar (those are files and OS chrome, so
* they cannot take a token). navColors.test.js holds the two lists together.
*/

View file

@ -236,10 +236,10 @@ html[data-theme="amber"] {
/*
* Base element styling.
*
* semantic.min.css used to supply all of this, and removing it took the lot with it --
* links lost their color and gained the browser's underline and visited purple, and
* A framework stylesheet used to supply all of this, and removing it took the lot with
* it -- links lost their color and gained the browser's underline and visited purple, and
* every padded element silently changed size when `box-sizing: border-box` went away.
* None of it belonged to Semantic; it belongs here, expressed in tokens.
* None of it belonged to a framework; it belongs here, expressed in tokens.
*/
/* ------------------------------------------------------------------------- */
@ -248,15 +248,15 @@ html[data-theme="amber"] {
/*
* How large the interface is, overall.
*
* Measured against the pre-migration Semantic build at 1280px, the drift was not one
* number: its headings were h1 28 / h2 24 / h3 18 / h4 15 / h5 14 against our
* Measured against the build users had before this library, at 1280px, the drift was not
* one number: its headings were h1 28 / h2 24 / h3 18 / h4 15 / h5 14 against our
* 26 / 21 / 17 / 14 / 13, and its video card a fixed 290px with an 18px title against
* our 238px with a 13px one -- while our BODY text and chrome went the other way,
* a 16px base against Semantic's 14 and 42px buttons against 36. No per-component
* nudge reproduces what a user comparing the two sees, which is the whole page.
* a 16px base against its 14 and 42px buttons against 36. No per-component nudge
* reproduces what a user comparing the two sees, which is the whole page.
*
* Ten percent is not a fudge factor: our heading scale times 1.1 is
* 28.6 / 23.1 / 18.7 / 15.4 / 14.3, which is Semantic's scale to within a pixel at
* 28.6 / 23.1 / 18.7 / 15.4 / 14.3, which is the old scale to within a pixel at
* every step.
*
* The lever is the root font-size, and everything that should grow with it is written

View file

@ -4,15 +4,15 @@ import path from 'path';
/*
* The size of the interface.
*
* Measured against the pre-migration Semantic build on the QA Pi at 1280px, the drift was not
* Measured on the QA Pi at 1280px against the build users had before, the drift was not
* one number: its headings were h1 28 / h2 24 / h3 18 / h4 15 / h5 14 against our 26 / 21 / 17
* / 14 / 13, and its video card a fixed 290px with an 18px title against our 238px with a 13px
* one -- while our BODY text and chrome went the other way, 16px base against Semantic's 14 and
* one -- while our BODY text and chrome went the other way, 16px base against its 14 and
* 42px buttons against 36. So no per-component nudge reproduces what a user comparing the two
* actually sees, which is the whole page at once.
*
* Ten percent is not a fudge factor. Our heading scale multiplied by 1.1 is 28.6 / 23.1 / 18.7
* / 15.4 / 14.3 -- Semantic's numbers, to within a pixel at every step.
* / 15.4 / 14.3 -- the old numbers, to within a pixel at every step.
*
* The lever is the root font-size, and everything that should grow with it is written in `rem`.
* That is Mantine's own mechanism: its 644 size declarations are `calc(Xrem * var(--mantine-

View file

@ -57,7 +57,13 @@ class SettingsResponse:
zims_destination: str = 'zims'
class SemanticUIColors(enum.StrEnum):
class NavColors(enum.StrEnum):
"""The colors a user may choose for the navigation bar.
The same twelve as `navColorHexMap` in app/src/components/Vars.js, which holds the hex
each one is drawn as, and as `navColorNames` in app/src/themes/navColors.ts. A color
not on this list has no favicon file to go with it.
"""
red = enum.auto()
orange = enum.auto()
yellow = enum.auto()
@ -107,7 +113,7 @@ class SettingsRequest:
def __post_init__(self):
self.nav_color = self.nav_color.lower() if self.nav_color else None
if self.nav_color and self.nav_color not in SemanticUIColors.__members__:
if self.nav_color and self.nav_color not in NavColors.__members__:
raise ValidationError('Nav color is invalid')