From e00f0439da840baaa2e61e80c9a0f7801aedb08a Mon Sep 17 00:00:00 2001 From: Roland Date: Wed, 5 Aug 2026 19:15:57 -0600 Subject: [PATCH] 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) --- app/cypress/e2e/domains/domain-edit.cy.js | 2 +- app/cypress/e2e/domains/domains-list.cy.js | 2 +- app/scripts/generate-favicons.js | 20 +- app/src/App.css | 279 +++--------------- app/src/Tags.js | 2 +- app/src/Tags.test.js | 2 +- app/src/components/Archive.js | 6 +- app/src/components/Calculators.js | 2 +- app/src/components/Common.js | 8 +- app/src/components/Common.test.js | 8 +- app/src/components/DomainEditPage.test.js | 2 +- app/src/components/Download.js | 10 +- app/src/components/Files.js | 2 +- .../components/KeyboardShortcutsProvider.js | 2 +- app/src/components/Nav.js | 14 +- app/src/components/Search.js | 6 +- app/src/components/Theme.tsx | 17 +- app/src/components/ThemeSamplePage.js | 28 +- app/src/components/Vars.js | 17 +- app/src/components/VideoPlayer.js | 2 +- .../components/admin/ControllerPage.test.js | 2 +- .../components/admin/ExtensionPage.test.js | 2 +- app/src/components/admin/Settings.js | 4 +- .../collections/CollectionEditForm.js | 7 +- .../components/collections/CollectionTable.js | 2 +- app/src/components/inventory/FieldCell.js | 2 +- .../components/inventory/InventoryTable.js | 2 +- app/src/components/ui/Button.tsx | 31 +- app/src/components/ui/DataTable.tsx | 4 +- app/src/components/ui/Feedback.tsx | 14 +- app/src/components/ui/Icon.tsx | 26 +- app/src/components/ui/Inputs.tsx | 6 +- app/src/components/ui/Navigation.tsx | 6 +- app/src/components/ui/Overlays.tsx | 18 +- app/src/components/ui/SearchBox.tsx | 18 +- app/src/components/ui/Surfaces.tsx | 8 +- app/src/components/ui/index.ts | 2 +- app/src/components/ui/toast.test.js | 6 +- app/src/components/ui/toast.ts | 13 +- app/src/components/ui/ui.css | 71 +++-- app/src/components/ui/ui.test.js | 32 +- app/src/contexts/contexts.js | 2 +- app/src/hooks/customHooks.test.js | 4 +- app/src/hooks/useForm.js | 2 +- app/src/hooks/useForm.test.js | 2 +- app/src/no-semantic-ui.test.js | 81 +++++ app/src/semantic-baseline.json | 1 - app/src/semantic-baseline.test.js | 134 --------- app/src/setupTests.js | 2 +- app/src/test-fixtures.js | 2 +- app/src/test-fixtures.test.js | 4 +- app/src/themes/mantine.ts | 18 +- app/src/themes/names.ts | 6 +- app/src/themes/navColors.test.js | 6 +- app/src/themes/navColors.ts | 2 +- app/src/themes/tokens.css | 16 +- app/src/themes/ui-scale.test.js | 6 +- wrolpi/schema.py | 10 +- 58 files changed, 387 insertions(+), 618 deletions(-) create mode 100644 app/src/no-semantic-ui.test.js delete mode 100644 app/src/semantic-baseline.json delete mode 100644 app/src/semantic-baseline.test.js diff --git a/app/cypress/e2e/domains/domain-edit.cy.js b/app/cypress/e2e/domains/domain-edit.cy.js index ccd3c9b7..ac2512f8 100644 --- a/app/cypress/e2e/domains/domain-edit.cy.js +++ b/app/cypress/e2e/domains/domain-edit.cy.js @@ -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 diff --git a/app/cypress/e2e/domains/domains-list.cy.js b/app/cypress/e2e/domains/domains-list.cy.js index d2227e7a..7515ef3e 100644 --- a/app/cypress/e2e/domains/domains-list.cy.js +++ b/app/cypress/e2e/domains/domains-list.cy.js @@ -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. diff --git a/app/scripts/generate-favicons.js b/app/scripts/generate-favicons.js index 5db437d5..a72b262a 100644 --- a/app/scripts/generate-favicons.js +++ b/app/scripts/generate-favicons.js @@ -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/`); diff --git a/app/src/App.css b/app/src/App.css index 28ad04d2..8f417f52 100644 --- a/app/src/App.css +++ b/app/src/App.css @@ -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 { diff --git a/app/src/Tags.js b/app/src/Tags.js index d109e640..abcd0517 100644 --- a/app/src/Tags.js +++ b/app/src/Tags.js @@ -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. * diff --git a/app/src/Tags.test.js b/app/src/Tags.test.js index 24a7c588..70bcf613 100644 --- a/app/src/Tags.test.js +++ b/app/src/Tags.test.js @@ -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. diff --git a/app/src/components/Archive.js b/app/src/components/Archive.js index 73c4e25b..e7b1e2ba 100644 --- a/app/src/components/Archive.js +++ b/app/src/components/Archive.js @@ -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. */}
diff --git a/app/src/components/Calculators.js b/app/src/components/Calculators.js index 7254be0f..5c04945f 100644 --- a/app/src/components/Calculators.js +++ b/app/src/components/Calculators.js @@ -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'}, diff --git a/app/src/components/Common.js b/app/src/components/Common.js index 93bc8414..9e93e039 100644 --- a/app/src/components/Common.js +++ b/app/src/components/Common.js @@ -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 {text} } - // 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 }> {crumbs.map(getSection)} diff --git a/app/src/components/Common.test.js b/app/src/components/Common.test.js index b8bf7e38..ba373bbf 100644 --- a/app/src/components/Common.test.js +++ b/app/src/components/Common.test.js @@ -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', () => { ); - // 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 diff --git a/app/src/components/DomainEditPage.test.js b/app/src/components/DomainEditPage.test.js index 0a8ac9ae..e9fce0ca 100644 --- a/app/src/components/DomainEditPage.test.js +++ b/app/src/components/DomainEditPage.test.js @@ -95,7 +95,7 @@ describe('DomainEditPage', () => { render(); - // 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 diff --git a/app/src/components/Download.js b/app/src/components/Download.js index d854348b..db1f1478 100644 --- a/app/src/components/Download.js +++ b/app/src/components/Download.js @@ -37,11 +37,11 @@ import { } from "./Vars"; /* - * Download.js was built around Semantic UI's
family (Form.Input, + * Download.js was built around the old 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({
} -/** 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 } -/** Multiline URLs field (Semantic's `UrlsTextarea`). */ +/** Multiline URLs field: one URL per line. */ function UrlsField({name = 'urls', required, form}) { required = required !== undefined; diff --git a/app/src/components/Files.js b/app/src/components/Files.js index 09e8428b..7120bcdd 100644 --- a/app/src/components/Files.js +++ b/app/src/components/Files.js @@ -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
{link.text} {link.links.map(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 diff --git a/app/src/components/Search.js b/app/src/components/Search.js index 83e89f8f..c064b848 100644 --- a/app/src/components/Search.js +++ b/app/src/components/Search.js @@ -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 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. */ diff --git a/app/src/components/Theme.tsx b/app/src/components/Theme.tsx index 7e9b0aa8..56b91d0f 100644 --- a/app/src/components/Theme.tsx +++ b/app/src/components/Theme.tsx @@ -226,15 +226,12 @@ export function ThemeProvider({children, ...props}: ThemeProviderProps) { } /* - * 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 , 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 , 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. */ diff --git a/app/src/components/ThemeSamplePage.js b/app/src/components/ThemeSamplePage.js index 46a48592..66825e3a 100644 --- a/app/src/components/ThemeSamplePage.js +++ b/app/src/components/ThemeSamplePage.js @@ -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() { setConfirmOpen(true)}/> - +
@@ -389,7 +387,7 @@ export function ThemeSamplePage() { {/* * `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() { -
+
{/* The roles side by side, which is the only way to see the thing that @@ -765,7 +763,7 @@ export function ThemeSamplePage() {
- {semanticColorNames.map(color => )} + {paletteColorNames.map(color => )} @@ -938,8 +936,8 @@ export function ThemeSamplePage() {

- 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.

setModalOpen(false)} size='small'> Restore Backup: channels.yaml diff --git a/app/src/components/Vars.js b/app/src/components/Vars.js index 99071eb9..08890d96 100644 --- a/app/src/components/Vars.js +++ b/app/src/components/Vars.js @@ -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-.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 = [ diff --git a/app/src/components/VideoPlayer.js b/app/src/components/VideoPlayer.js index ca360d0b..86193cf2 100644 --- a/app/src/components/VideoPlayer.js +++ b/app/src/components/VideoPlayer.js @@ -129,7 +129,7 @@ const NoComments = ({video}) => { } } -// Semantic's / 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; diff --git a/app/src/components/admin/ControllerPage.test.js b/app/src/components/admin/ControllerPage.test.js index 62ddad71..e2ff6c83 100644 --- a/app/src/components/admin/ControllerPage.test.js +++ b/app/src/components/admin/ControllerPage.test.js @@ -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); diff --git a/app/src/components/admin/ExtensionPage.test.js b/app/src/components/admin/ExtensionPage.test.js index 93cf8111..0f3eb979 100644 --- a/app/src/components/admin/ExtensionPage.test.js +++ b/app/src/components/admin/ExtensionPage.test.js @@ -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 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: { diff --git a/app/src/components/admin/Settings.js b/app/src/components/admin/Settings.js index 8cfc8644..bceba925 100644 --- a/app/src/components/admin/Settings.js +++ b/app/src/components/admin/Settings.js @@ -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)} }); diff --git a/app/src/components/collections/CollectionEditForm.js b/app/src/components/collections/CollectionEditForm.js index 01474ab4..447dbb7d 100644 --- a/app/src/components/collections/CollectionEditForm.js +++ b/app/src/components/collections/CollectionEditForm.js @@ -46,12 +46,7 @@ export function CollectionEditForm({ } - {/* - 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. */}
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(); diff --git a/app/src/components/inventory/InventoryTable.js b/app/src/components/inventory/InventoryTable.js index 52abd1ad..f9e4480f 100644 --- a/app/src/components/inventory/InventoryTable.js +++ b/app/src/components/inventory/InventoryTable.js @@ -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 DOM node (unlike Semantic's Input, + // Our inputs forward their ref straight to the underlying DOM node (unlike the old Input, // which wrapped it behind `.inputRef`). const node = firstInputRef.current; if (node && node.focus) { diff --git a/app/src/components/ui/Button.tsx b/app/src/components/ui/Button.tsx index cd253f9e..0a97f22d 100644 --- a/app/src/components/ui/Button.tsx +++ b/app/src/components/ui/Button.tsx @@ -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 { 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; iconAfter?: string | React.ComponentType; onClick?: React.MouseEventHandler; @@ -41,14 +40,15 @@ const renderIcon = (icon?: string | React.ComponentType) => { } /* - * 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 = { +const sizeAliases: Record = { mini: 'xs', tiny: 'xs', small: 'sm', @@ -60,9 +60,9 @@ const semanticSizes: Record = { }; // 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 = (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(( {role, icon, iconAfter, className, children, ...props}, ref @@ -105,9 +105,8 @@ export const Button = forwardRef(( 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. diff --git a/app/src/components/ui/DataTable.tsx b/app/src/components/ui/DataTable.tsx index c305eb43..732edb75 100644 --- a/app/src/components/ui/DataTable.tsx +++ b/app/src/components/ui/DataTable.tsx @@ -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 { diff --git a/app/src/components/ui/Feedback.tsx b/app/src/components/ui/Feedback.tsx index ae021d7b..4266103d 100644 --- a/app/src/components/ui/Feedback.tsx +++ b/app/src/components/ui/Feedback.tsx @@ -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; /** 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; /** * 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 ``, 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)
} -/** 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
{Array.from({length: lines}, (_, index) => = {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 = { +const iconNameAliases: Record = { '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, '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; @@ -207,9 +209,9 @@ export interface IconProps extends Omit, '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 | undefined => { - const taberName = semanticNames[name.trim().toLowerCase()]; + const taberName = iconNameAliases[name.trim().toLowerCase()]; return taberName ? (Tabler[taberName] as React.ComponentType) : 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; } } diff --git a/app/src/components/ui/Inputs.tsx b/app/src/components/ui/Inputs.tsx index d0d40a85..524a62ca 100644 --- a/app/src/components/ui/Inputs.tsx +++ b/app/src/components/ui/Inputs.tsx @@ -49,7 +49,7 @@ export function Checkbox({style, ...props}: CheckboxProps) { export type ToggleProps = React.ComponentProps; -/** An on/off switch. Replaces Semantic's `Radio toggle`. */ +/** An on/off switch. */ export function Toggle({style, className, ...props}: ToggleProps) { return `. + * 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 diff --git a/app/src/components/ui/Navigation.tsx b/app/src/components/ui/Navigation.tsx index ef02c2f2..eced9fcb 100644 --- a/app/src/components/ui/Navigation.tsx +++ b/app/src/components/ui/Navigation.tsx @@ -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, diff --git a/app/src/components/ui/Overlays.tsx b/app/src/components/ui/Overlays.tsx index 24202cf3..2471eafe 100644 --- a/app/src/components/ui/Overlays.tsx +++ b/app/src/components/ui/Overlays.tsx @@ -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 = { mini: 'xs', tiny: 'sm', @@ -34,12 +33,12 @@ const modalSizes: Record = { }; export interface ModalProps extends Omit, '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, '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; -/** Replaces Semantic's Popup. */ +/** A hover/focus tooltip. */ export function Tooltip(props: TooltipProps) { return } diff --git a/app/src/components/ui/SearchBox.tsx b/app/src/components/ui/SearchBox.tsx index b53d3f9b..11c2aca3 100644 --- a/app/src/components/ui/SearchBox.tsx +++ b/app/src/components/ui/SearchBox.tsx @@ -6,9 +6,9 @@ import {Loader} from './Feedback'; /* * The search box, with optional grouped suggestions. * - * Replaces Semantic's ``, 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 `