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. */}