wrolpi/app/src/components/ui/Button.tsx

176 lines
7.5 KiB
TypeScript
Raw Normal View History

ui: token-driven component library on Mantine Adds src/components/ui: buttons, messages, labels, progress, status, tables, form controls, panels, cards, statistics, modals, tooltips, tabs, and icons. Everything resolves its colors through theme tokens, so no component branches on the current theme, and Semantic UI keeps rendering the app meanwhile. Mantine is bridged rather than restyled: its own CSS variables are pointed at our tokens. Four details were not obvious, and each caused a visible defect in night mode before being fixed: - Structural colors must go in the resolver's light/dark blocks. Mantine emits its values under [data-mantine-color-scheme], which outranks the :root block that `variables` becomes, so its grays were winning. - The dark and gray palettes are mapped shade by shade to tokens. Mantine reads them directly for table borders, zebra stripes and input chrome; they can be neither flattened nor left alone. - Mantine writes --button-* as inline styles, which no stylesheet can outrank, so the dashed danger treatment sets real properties instead. - Switch colors are written onto inner elements, so they need scoped CSS rather than a variable on the root. Adds a --knob token for control handles, because Mantine's default thumb follows theme.white, which is near-black in the dark-based themes. An audit of every painted pixel in the /theme-sample gallery now finds no non-red channel in night mode. Icons come from @tabler/icons-react with the 102 Semantic names the app uses mapped to components; they stroke with currentColor, so they follow the theme without any rule of their own. Toasts keep the react-semantic-toasts-2 signature so those call sites migrate by changing an import. TypeScript 5 is required: Mantine's declarations use `const` type parameters, which 4.9 cannot parse (Mantine 7 is the same, so downgrading is no escape). That needs two npm overrides to keep clean installs working, both verified against a Docker build: react-scripts declares peerOptional typescript ^3||^4, and fork-ts-checker 6.5.2 assigns typescript.performance.mark, which TS 5 made getter-only - crashing the dev server's type checker on every rebuild. semantic-baseline.test.js ratchets the removal: it fails if a file not already listed imports Semantic UI. An ESLint rule would have printed 75 warnings per rebuild, and CI unsets CI= for its build step, so it would guard nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:22:03 -06:00
import React, {forwardRef} from 'react';
import {ActionIcon, ActionIconProps, Button as MButton, ButtonProps as MButtonProps} from '@mantine/core';
import {Icon} from './Icon';
/*
* Buttons.
*
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>
2026-08-05 19:15:57 -06:00
* 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.
ui: token-driven component library on Mantine Adds src/components/ui: buttons, messages, labels, progress, status, tables, form controls, panels, cards, statistics, modals, tooltips, tabs, and icons. Everything resolves its colors through theme tokens, so no component branches on the current theme, and Semantic UI keeps rendering the app meanwhile. Mantine is bridged rather than restyled: its own CSS variables are pointed at our tokens. Four details were not obvious, and each caused a visible defect in night mode before being fixed: - Structural colors must go in the resolver's light/dark blocks. Mantine emits its values under [data-mantine-color-scheme], which outranks the :root block that `variables` becomes, so its grays were winning. - The dark and gray palettes are mapped shade by shade to tokens. Mantine reads them directly for table borders, zebra stripes and input chrome; they can be neither flattened nor left alone. - Mantine writes --button-* as inline styles, which no stylesheet can outrank, so the dashed danger treatment sets real properties instead. - Switch colors are written onto inner elements, so they need scoped CSS rather than a variable on the root. Adds a --knob token for control handles, because Mantine's default thumb follows theme.white, which is near-black in the dark-based themes. An audit of every painted pixel in the /theme-sample gallery now finds no non-red channel in night mode. Icons come from @tabler/icons-react with the 102 Semantic names the app uses mapped to components; they stroke with currentColor, so they follow the theme without any rule of their own. Toasts keep the react-semantic-toasts-2 signature so those call sites migrate by changing an import. TypeScript 5 is required: Mantine's declarations use `const` type parameters, which 4.9 cannot parse (Mantine 7 is the same, so downgrading is no escape). That needs two npm overrides to keep clean installs working, both verified against a Docker build: react-scripts declares peerOptional typescript ^3||^4, and fork-ts-checker 6.5.2 assigns typescript.performance.mark, which TS 5 made getter-only - crashing the dev server's type checker on every rebuild. semantic-baseline.test.js ratchets the removal: it fails if a file not already listed imports Semantic UI. An ESLint rule would have printed 75 warnings per rebuild, and CI unsets CI= for its build step, so it would guard nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:22:03 -06:00
*/
export type ButtonRole = 'primary' | 'save' | 'retry' | 'danger' | 'cancel';
const roleProps: Record<ButtonRole, {color?: string; variant: string; className?: string}> = {
primary: {color: 'blue', variant: 'filled'},
save: {color: 'green', variant: 'filled'},
retry: {color: 'yellow', variant: 'filled'},
// Danger resolves differently per theme (filled in light/dark, dashed outline in
// night). The branching lives in ui.css, never in a call site.
danger: {color: 'red', variant: 'filled', className: 'wrolpi-button-danger'},
cancel: {variant: 'default'},
};
export interface ButtonProps extends Omit<MButtonProps, 'leftSection' | 'rightSection'> {
role?: ButtonRole;
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>
2026-08-05 19:15:57 -06:00
/** An icon name (see Icon.tsx) or a Tabler component, rendered before the label. */
ui: token-driven component library on Mantine Adds src/components/ui: buttons, messages, labels, progress, status, tables, form controls, panels, cards, statistics, modals, tooltips, tabs, and icons. Everything resolves its colors through theme tokens, so no component branches on the current theme, and Semantic UI keeps rendering the app meanwhile. Mantine is bridged rather than restyled: its own CSS variables are pointed at our tokens. Four details were not obvious, and each caused a visible defect in night mode before being fixed: - Structural colors must go in the resolver's light/dark blocks. Mantine emits its values under [data-mantine-color-scheme], which outranks the :root block that `variables` becomes, so its grays were winning. - The dark and gray palettes are mapped shade by shade to tokens. Mantine reads them directly for table borders, zebra stripes and input chrome; they can be neither flattened nor left alone. - Mantine writes --button-* as inline styles, which no stylesheet can outrank, so the dashed danger treatment sets real properties instead. - Switch colors are written onto inner elements, so they need scoped CSS rather than a variable on the root. Adds a --knob token for control handles, because Mantine's default thumb follows theme.white, which is near-black in the dark-based themes. An audit of every painted pixel in the /theme-sample gallery now finds no non-red channel in night mode. Icons come from @tabler/icons-react with the 102 Semantic names the app uses mapped to components; they stroke with currentColor, so they follow the theme without any rule of their own. Toasts keep the react-semantic-toasts-2 signature so those call sites migrate by changing an import. TypeScript 5 is required: Mantine's declarations use `const` type parameters, which 4.9 cannot parse (Mantine 7 is the same, so downgrading is no escape). That needs two npm overrides to keep clean installs working, both verified against a Docker build: react-scripts declares peerOptional typescript ^3||^4, and fork-ts-checker 6.5.2 assigns typescript.performance.mark, which TS 5 made getter-only - crashing the dev server's type checker on every rebuild. semantic-baseline.test.js ratchets the removal: it fails if a file not already listed imports Semantic UI. An ESLint rule would have printed 75 warnings per rebuild, and CI unsets CI= for its build step, so it would guard nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:22:03 -06:00
icon?: string | React.ComponentType<any>;
iconAfter?: string | React.ComponentType<any>;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
type?: 'button' | 'submit' | 'reset';
href?: string;
target?: string;
ui: three silent library defects the agents' reports surfaced Each of these was reported by an agent working around it rather than patched in one file, which is why all three are fixed once, for every call site. Button accepted an `href` and dropped it. Mantine renders an anchor only when it also gets `component='a'`; Semantic spelled that `as='a'`. So a migrated call site that kept just the href produced something that looks like a download link and navigates nowhere -- silently, with no warning. Button and IconButton now infer the anchor from the href. The media filter ran twice on nested media. A `.media` wrapper filters its subtree as one unit -- the audio player needs that, because its control glyphs are SVG and the element-type rule does not reach them -- but the poster image inside also matched that rule. Two passes of a luminance-to-red matrix is not one: the second takes 0.2126 of an already-red pixel, so the poster came out markedly darker than everything around it. Leaf media inside a filtered wrapper is now left alone. Progress had no indeterminate state. Semantic's `indicating` animated a stripe when the size was unknown; without it an upload that has not reported yet sits at 0% and reads as stalled -- and `aria-valuenow="0"` tells a screen reader the same wrong thing. It now animates, reports no value, and falls back to a static fill under prefers-reduced-motion. Also: night and amber now override an inline `--label-color`. A call site may legitimately need many distinguishable colours -- the Flasher's chip badges use a twelve-colour palette no four tokens can express -- but on a night-adapted screen a green or blue chip badge defeats the entire mode. The labels lose their distinctness there, which is the right trade.
2026-07-30 13:15:03 -06:00
component?: any;
ui: token-driven component library on Mantine Adds src/components/ui: buttons, messages, labels, progress, status, tables, form controls, panels, cards, statistics, modals, tooltips, tabs, and icons. Everything resolves its colors through theme tokens, so no component branches on the current theme, and Semantic UI keeps rendering the app meanwhile. Mantine is bridged rather than restyled: its own CSS variables are pointed at our tokens. Four details were not obvious, and each caused a visible defect in night mode before being fixed: - Structural colors must go in the resolver's light/dark blocks. Mantine emits its values under [data-mantine-color-scheme], which outranks the :root block that `variables` becomes, so its grays were winning. - The dark and gray palettes are mapped shade by shade to tokens. Mantine reads them directly for table borders, zebra stripes and input chrome; they can be neither flattened nor left alone. - Mantine writes --button-* as inline styles, which no stylesheet can outrank, so the dashed danger treatment sets real properties instead. - Switch colors are written onto inner elements, so they need scoped CSS rather than a variable on the root. Adds a --knob token for control handles, because Mantine's default thumb follows theme.white, which is near-black in the dark-based themes. An audit of every painted pixel in the /theme-sample gallery now finds no non-red channel in night mode. Icons come from @tabler/icons-react with the 102 Semantic names the app uses mapped to components; they stroke with currentColor, so they follow the theme without any rule of their own. Toasts keep the react-semantic-toasts-2 signature so those call sites migrate by changing an import. TypeScript 5 is required: Mantine's declarations use `const` type parameters, which 4.9 cannot parse (Mantine 7 is the same, so downgrading is no escape). That needs two npm overrides to keep clean installs working, both verified against a Docker build: react-scripts declares peerOptional typescript ^3||^4, and fork-ts-checker 6.5.2 assigns typescript.performance.mark, which TS 5 made getter-only - crashing the dev server's type checker on every rebuild. semantic-baseline.test.js ratchets the removal: it fails if a file not already listed imports Semantic UI. An ESLint rule would have printed 75 warnings per rebuild, and CI unsets CI= for its build step, so it would guard nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:22:03 -06:00
}
const renderIcon = (icon?: string | React.ComponentType<any>) => {
if (!icon) return undefined;
return typeof icon === 'string' ? <Icon name={icon}/> : <Icon component={icon}/>;
}
ui: Common.js and thirteen more files off Semantic Phase 2, part 3. Baseline 63 -> 50. Common.js is the one that mattered: 51 files import it, so its Semantic usage was propping up the rest of the migration. Everything in it now comes from the library -- Paginator, the two search inputs, DirectorySearch, TabLinks, the toggles, the messages, APIButton, Breadcrumbs, SimpleAccordion, IframeViewer, CardPoster, FileIcon, LoadStatistic, the theme menu, and the hotspot icons. Also migrated: Placeholder, Apps, ArchivePreview, CbzViewer, AddToPlaylist, TaggedDeleteConfirmModal, the collection edit/tag forms, and four inventory components. New in the library, all in the gallery and tested: SearchBox Semantic's `<Search category>` with its grouped results shape kept, because four call sites already build it. A real combobox rather than a div that listens for clicks. Pagination Semantic's prop names; onPageChange gets the page number. TabBar the strip only -- routing stays with the caller. IconStack Semantic's IconGroup, named once for assistive technology. Table.HeaderCell sorted/onSort -- a real button carrying aria-sort. Button/IconButton translate Semantic's size names. That last one is defensive: unmigrated callers still pass size='tiny', and Mantine drops a size it does not recognise without a word, so the button quietly renders at the wrong size. Mapping here keeps the mixed state honest. Three real bugs found and fixed on the way, none of them cosmetic: * `fileSuffixIconName` returned fourteen icon names the map did not have, so opening a folder with a PDF, a Word document or an archive in it logged an error and drew a fallback glyph. All fourteen mapped, with a test that walks the mapper's own return values, and another asserting every mapped name resolves to a Tabler component that exists. * `Statistic` relied on Semantic's `Statistic.Group` for its spacing, so on the dashboard the load averages ran together into "1 MIN. LOAD5 MIN. LOAD15 MIN. LOAD". It carries its own spacing now. * Semantic's inverted stylesheet colors every descendant of an inverted segment, so Toggle labels inherited #dddddd -- white pixels in night mode, on the Settings page, which is exactly where someone adjusts their WROLPi in the dark. Restated from tokens. The baseline ratchet gained a third assertion: a migrated file may still lean on Semantic's CSS classes without importing the library, which the import check cannot see and which would collapse silently when Phase 4 deletes the stylesheet. There is one such bridge today and it is now declared.
2026-07-30 10:00:48 -06:00
/*
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>
2026-08-05 19:15:57 -06:00
* WROLPi's size names, mapped onto Mantine's scale.
ui: Common.js and thirteen more files off Semantic Phase 2, part 3. Baseline 63 -> 50. Common.js is the one that mattered: 51 files import it, so its Semantic usage was propping up the rest of the migration. Everything in it now comes from the library -- Paginator, the two search inputs, DirectorySearch, TabLinks, the toggles, the messages, APIButton, Breadcrumbs, SimpleAccordion, IframeViewer, CardPoster, FileIcon, LoadStatistic, the theme menu, and the hotspot icons. Also migrated: Placeholder, Apps, ArchivePreview, CbzViewer, AddToPlaylist, TaggedDeleteConfirmModal, the collection edit/tag forms, and four inventory components. New in the library, all in the gallery and tested: SearchBox Semantic's `<Search category>` with its grouped results shape kept, because four call sites already build it. A real combobox rather than a div that listens for clicks. Pagination Semantic's prop names; onPageChange gets the page number. TabBar the strip only -- routing stays with the caller. IconStack Semantic's IconGroup, named once for assistive technology. Table.HeaderCell sorted/onSort -- a real button carrying aria-sort. Button/IconButton translate Semantic's size names. That last one is defensive: unmigrated callers still pass size='tiny', and Mantine drops a size it does not recognise without a word, so the button quietly renders at the wrong size. Mapping here keeps the mixed state honest. Three real bugs found and fixed on the way, none of them cosmetic: * `fileSuffixIconName` returned fourteen icon names the map did not have, so opening a folder with a PDF, a Word document or an archive in it logged an error and drew a fallback glyph. All fourteen mapped, with a test that walks the mapper's own return values, and another asserting every mapped name resolves to a Tabler component that exists. * `Statistic` relied on Semantic's `Statistic.Group` for its spacing, so on the dashboard the load averages ran together into "1 MIN. LOAD5 MIN. LOAD15 MIN. LOAD". It carries its own spacing now. * Semantic's inverted stylesheet colors every descendant of an inverted segment, so Toggle labels inherited #dddddd -- white pixels in night mode, on the Settings page, which is exactly where someone adjusts their WROLPi in the dark. Restated from tokens. The baseline ratchet gained a third assertion: a migrated file may still lean on Semantic's CSS classes without importing the library, which the import check cannot see and which would collapse silently when Phase 4 deletes the stylesheet. There is one such bridge today and it is now declared.
2026-07-30 10:00:48 -06:00
*
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>
2026-08-05 19:15:57 -06:00
* 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.
ui: Common.js and thirteen more files off Semantic Phase 2, part 3. Baseline 63 -> 50. Common.js is the one that mattered: 51 files import it, so its Semantic usage was propping up the rest of the migration. Everything in it now comes from the library -- Paginator, the two search inputs, DirectorySearch, TabLinks, the toggles, the messages, APIButton, Breadcrumbs, SimpleAccordion, IframeViewer, CardPoster, FileIcon, LoadStatistic, the theme menu, and the hotspot icons. Also migrated: Placeholder, Apps, ArchivePreview, CbzViewer, AddToPlaylist, TaggedDeleteConfirmModal, the collection edit/tag forms, and four inventory components. New in the library, all in the gallery and tested: SearchBox Semantic's `<Search category>` with its grouped results shape kept, because four call sites already build it. A real combobox rather than a div that listens for clicks. Pagination Semantic's prop names; onPageChange gets the page number. TabBar the strip only -- routing stays with the caller. IconStack Semantic's IconGroup, named once for assistive technology. Table.HeaderCell sorted/onSort -- a real button carrying aria-sort. Button/IconButton translate Semantic's size names. That last one is defensive: unmigrated callers still pass size='tiny', and Mantine drops a size it does not recognise without a word, so the button quietly renders at the wrong size. Mapping here keeps the mixed state honest. Three real bugs found and fixed on the way, none of them cosmetic: * `fileSuffixIconName` returned fourteen icon names the map did not have, so opening a folder with a PDF, a Word document or an archive in it logged an error and drew a fallback glyph. All fourteen mapped, with a test that walks the mapper's own return values, and another asserting every mapped name resolves to a Tabler component that exists. * `Statistic` relied on Semantic's `Statistic.Group` for its spacing, so on the dashboard the load averages ran together into "1 MIN. LOAD5 MIN. LOAD15 MIN. LOAD". It carries its own spacing now. * Semantic's inverted stylesheet colors every descendant of an inverted segment, so Toggle labels inherited #dddddd -- white pixels in night mode, on the Settings page, which is exactly where someone adjusts their WROLPi in the dark. Restated from tokens. The baseline ratchet gained a third assertion: a migrated file may still lean on Semantic's CSS classes without importing the library, which the import check cannot see and which would collapse silently when Phase 4 deletes the stylesheet. There is one such bridge today and it is now declared.
2026-07-30 10:00:48 -06:00
*/
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>
2026-08-05 19:15:57 -06:00
const sizeAliases: Record<string, string> = {
ui: Common.js and thirteen more files off Semantic Phase 2, part 3. Baseline 63 -> 50. Common.js is the one that mattered: 51 files import it, so its Semantic usage was propping up the rest of the migration. Everything in it now comes from the library -- Paginator, the two search inputs, DirectorySearch, TabLinks, the toggles, the messages, APIButton, Breadcrumbs, SimpleAccordion, IframeViewer, CardPoster, FileIcon, LoadStatistic, the theme menu, and the hotspot icons. Also migrated: Placeholder, Apps, ArchivePreview, CbzViewer, AddToPlaylist, TaggedDeleteConfirmModal, the collection edit/tag forms, and four inventory components. New in the library, all in the gallery and tested: SearchBox Semantic's `<Search category>` with its grouped results shape kept, because four call sites already build it. A real combobox rather than a div that listens for clicks. Pagination Semantic's prop names; onPageChange gets the page number. TabBar the strip only -- routing stays with the caller. IconStack Semantic's IconGroup, named once for assistive technology. Table.HeaderCell sorted/onSort -- a real button carrying aria-sort. Button/IconButton translate Semantic's size names. That last one is defensive: unmigrated callers still pass size='tiny', and Mantine drops a size it does not recognise without a word, so the button quietly renders at the wrong size. Mapping here keeps the mixed state honest. Three real bugs found and fixed on the way, none of them cosmetic: * `fileSuffixIconName` returned fourteen icon names the map did not have, so opening a folder with a PDF, a Word document or an archive in it logged an error and drew a fallback glyph. All fourteen mapped, with a test that walks the mapper's own return values, and another asserting every mapped name resolves to a Tabler component that exists. * `Statistic` relied on Semantic's `Statistic.Group` for its spacing, so on the dashboard the load averages ran together into "1 MIN. LOAD5 MIN. LOAD15 MIN. LOAD". It carries its own spacing now. * Semantic's inverted stylesheet colors every descendant of an inverted segment, so Toggle labels inherited #dddddd -- white pixels in night mode, on the Settings page, which is exactly where someone adjusts their WROLPi in the dark. Restated from tokens. The baseline ratchet gained a third assertion: a migrated file may still lean on Semantic's CSS classes without importing the library, which the import check cannot see and which would collapse silently when Phase 4 deletes the stylesheet. There is one such bridge today and it is now declared.
2026-07-30 10:00:48 -06:00
mini: 'xs',
tiny: 'xs',
small: 'sm',
medium: 'md',
large: 'lg',
big: 'lg',
huge: 'xl',
massive: 'xl',
};
// Generic so it passes each component's own size type straight through; only a
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>
2026-08-05 19:15:57 -06:00
// string in the table above is rewritten.
ui: Common.js and thirteen more files off Semantic Phase 2, part 3. Baseline 63 -> 50. Common.js is the one that mattered: 51 files import it, so its Semantic usage was propping up the rest of the migration. Everything in it now comes from the library -- Paginator, the two search inputs, DirectorySearch, TabLinks, the toggles, the messages, APIButton, Breadcrumbs, SimpleAccordion, IframeViewer, CardPoster, FileIcon, LoadStatistic, the theme menu, and the hotspot icons. Also migrated: Placeholder, Apps, ArchivePreview, CbzViewer, AddToPlaylist, TaggedDeleteConfirmModal, the collection edit/tag forms, and four inventory components. New in the library, all in the gallery and tested: SearchBox Semantic's `<Search category>` with its grouped results shape kept, because four call sites already build it. A real combobox rather than a div that listens for clicks. Pagination Semantic's prop names; onPageChange gets the page number. TabBar the strip only -- routing stays with the caller. IconStack Semantic's IconGroup, named once for assistive technology. Table.HeaderCell sorted/onSort -- a real button carrying aria-sort. Button/IconButton translate Semantic's size names. That last one is defensive: unmigrated callers still pass size='tiny', and Mantine drops a size it does not recognise without a word, so the button quietly renders at the wrong size. Mapping here keeps the mixed state honest. Three real bugs found and fixed on the way, none of them cosmetic: * `fileSuffixIconName` returned fourteen icon names the map did not have, so opening a folder with a PDF, a Word document or an archive in it logged an error and drew a fallback glyph. All fourteen mapped, with a test that walks the mapper's own return values, and another asserting every mapped name resolves to a Tabler component that exists. * `Statistic` relied on Semantic's `Statistic.Group` for its spacing, so on the dashboard the load averages ran together into "1 MIN. LOAD5 MIN. LOAD15 MIN. LOAD". It carries its own spacing now. * Semantic's inverted stylesheet colors every descendant of an inverted segment, so Toggle labels inherited #dddddd -- white pixels in night mode, on the Settings page, which is exactly where someone adjusts their WROLPi in the dark. Restated from tokens. The baseline ratchet gained a third assertion: a migrated file may still lean on Semantic's CSS classes without importing the library, which the import check cannot see and which would collapse silently when Phase 4 deletes the stylesheet. There is one such bridge today and it is now declared.
2026-07-30 10:00:48 -06:00
export const resolveSize = <T, >(size: T): T =>
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>
2026-08-05 19:15:57 -06:00
typeof size === 'string' ? ((sizeAliases[size] ?? size) as T) : size;
ui: Common.js and thirteen more files off Semantic Phase 2, part 3. Baseline 63 -> 50. Common.js is the one that mattered: 51 files import it, so its Semantic usage was propping up the rest of the migration. Everything in it now comes from the library -- Paginator, the two search inputs, DirectorySearch, TabLinks, the toggles, the messages, APIButton, Breadcrumbs, SimpleAccordion, IframeViewer, CardPoster, FileIcon, LoadStatistic, the theme menu, and the hotspot icons. Also migrated: Placeholder, Apps, ArchivePreview, CbzViewer, AddToPlaylist, TaggedDeleteConfirmModal, the collection edit/tag forms, and four inventory components. New in the library, all in the gallery and tested: SearchBox Semantic's `<Search category>` with its grouped results shape kept, because four call sites already build it. A real combobox rather than a div that listens for clicks. Pagination Semantic's prop names; onPageChange gets the page number. TabBar the strip only -- routing stays with the caller. IconStack Semantic's IconGroup, named once for assistive technology. Table.HeaderCell sorted/onSort -- a real button carrying aria-sort. Button/IconButton translate Semantic's size names. That last one is defensive: unmigrated callers still pass size='tiny', and Mantine drops a size it does not recognise without a word, so the button quietly renders at the wrong size. Mapping here keeps the mixed state honest. Three real bugs found and fixed on the way, none of them cosmetic: * `fileSuffixIconName` returned fourteen icon names the map did not have, so opening a folder with a PDF, a Word document or an archive in it logged an error and drew a fallback glyph. All fourteen mapped, with a test that walks the mapper's own return values, and another asserting every mapped name resolves to a Tabler component that exists. * `Statistic` relied on Semantic's `Statistic.Group` for its spacing, so on the dashboard the load averages ran together into "1 MIN. LOAD5 MIN. LOAD15 MIN. LOAD". It carries its own spacing now. * Semantic's inverted stylesheet colors every descendant of an inverted segment, so Toggle labels inherited #dddddd -- white pixels in night mode, on the Settings page, which is exactly where someone adjusts their WROLPi in the dark. Restated from tokens. The baseline ratchet gained a third assertion: a migrated file may still lean on Semantic's CSS classes without importing the library, which the import check cannot see and which would collapse silently when Phase 4 deletes the stylesheet. There is one such bridge today and it is now declared.
2026-07-30 10:00:48 -06:00
ui: token-driven component library on Mantine Adds src/components/ui: buttons, messages, labels, progress, status, tables, form controls, panels, cards, statistics, modals, tooltips, tabs, and icons. Everything resolves its colors through theme tokens, so no component branches on the current theme, and Semantic UI keeps rendering the app meanwhile. Mantine is bridged rather than restyled: its own CSS variables are pointed at our tokens. Four details were not obvious, and each caused a visible defect in night mode before being fixed: - Structural colors must go in the resolver's light/dark blocks. Mantine emits its values under [data-mantine-color-scheme], which outranks the :root block that `variables` becomes, so its grays were winning. - The dark and gray palettes are mapped shade by shade to tokens. Mantine reads them directly for table borders, zebra stripes and input chrome; they can be neither flattened nor left alone. - Mantine writes --button-* as inline styles, which no stylesheet can outrank, so the dashed danger treatment sets real properties instead. - Switch colors are written onto inner elements, so they need scoped CSS rather than a variable on the root. Adds a --knob token for control handles, because Mantine's default thumb follows theme.white, which is near-black in the dark-based themes. An audit of every painted pixel in the /theme-sample gallery now finds no non-red channel in night mode. Icons come from @tabler/icons-react with the 102 Semantic names the app uses mapped to components; they stroke with currentColor, so they follow the theme without any rule of their own. Toasts keep the react-semantic-toasts-2 signature so those call sites migrate by changing an import. TypeScript 5 is required: Mantine's declarations use `const` type parameters, which 4.9 cannot parse (Mantine 7 is the same, so downgrading is no escape). That needs two npm overrides to keep clean installs working, both verified against a Docker build: react-scripts declares peerOptional typescript ^3||^4, and fork-ts-checker 6.5.2 assigns typescript.performance.mark, which TS 5 made getter-only - crashing the dev server's type checker on every rebuild. semantic-baseline.test.js ratchets the removal: it fails if a file not already listed imports Semantic UI. An ESLint rule would have printed 75 warnings per rebuild, and CI unsets CI= for its build step, so it would guard nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:22:03 -06:00
export const Button = forwardRef<HTMLButtonElement, ButtonProps>((
ui: three button-layout defects, each with a different cause Reported together as "button groups", but they turned out to be three unrelated things. The file browser's footer buttons touched while the Channel Edit page's had gaps. Channel Edit lays its buttons out with Mantine's `Group`, which is a flex row with a gap; the footer is hand-rolled and was a plain block, so its inline-block buttons sat flush -- JSX drops whitespace-only lines, so there was not even a space character between them. The footer is now the flex row every other button group already is, wrapping because it carries eight controls. The Settings config table's Import and Save buttons had off-centre icons while Restore in the same row looked right. Restore is an IconButton -- an ActionIcon, which centres a lone glyph by construction. Import and Save are icon-only Buttons, and an icon on a Button goes into Mantine's `leftSection`, which carries a `margin-inline-end` to hold it off the label. With no label that margin is pure offset: measured at 8px left of centre. An icon with no label is now rendered as the button's content instead, so it centres. `Children.toArray` decides that, because it drops nulls and the file browser passes `{label('Delete')}` -- null whenever the bar is too narrow for words, which is exactly where the offset showed most. The search field's clear button sat inside the field rather than attached to it: the control is a bordered box with `padding: 0 8px`, leaving the button 9px short of the edge and 2px short of the height. Semantic welded it to the field -- measured on the QA Pi at zero gap and full height -- so it now stretches and negative margins cancel the padding and the flex gap. Negative margins rather than dropping the control's padding, because that padding is what holds the search icon and the text off the border; only this one child should escape it. The `html` prefix on that rule is load-bearing: Mantine sets ActionIcon's height from `--ai-size` on its own single class, and at equal specificity the winner is whichever stylesheet loaded last. Fourteen new cases. One correction worth recording: the height assertion first compared against the control's border box and failed on correct behaviour -- the button fills the CONTENT box, and the 1px above and below it is the field's own border, which it should sit inside rather than paint over. Each fix planted separately; the icon change reddens three, the footer one, the clear button one. Every case is paired with an inverse -- a labelled button must keep its icon on the left, and the field must still be padded around its other children. 1128 jest / 63 suites, 226 in ui-layout.cy.js, clean npm run build. Confirmed live: footer gaps 8px on one row, config glyphs at 0.0px from centre, clear button 1px from the field edge at the field's full inner height.
2026-08-02 17:13:28 -06:00
{role, icon, iconAfter, className, children, ...props}, ref
ui: token-driven component library on Mantine Adds src/components/ui: buttons, messages, labels, progress, status, tables, form controls, panels, cards, statistics, modals, tooltips, tabs, and icons. Everything resolves its colors through theme tokens, so no component branches on the current theme, and Semantic UI keeps rendering the app meanwhile. Mantine is bridged rather than restyled: its own CSS variables are pointed at our tokens. Four details were not obvious, and each caused a visible defect in night mode before being fixed: - Structural colors must go in the resolver's light/dark blocks. Mantine emits its values under [data-mantine-color-scheme], which outranks the :root block that `variables` becomes, so its grays were winning. - The dark and gray palettes are mapped shade by shade to tokens. Mantine reads them directly for table borders, zebra stripes and input chrome; they can be neither flattened nor left alone. - Mantine writes --button-* as inline styles, which no stylesheet can outrank, so the dashed danger treatment sets real properties instead. - Switch colors are written onto inner elements, so they need scoped CSS rather than a variable on the root. Adds a --knob token for control handles, because Mantine's default thumb follows theme.white, which is near-black in the dark-based themes. An audit of every painted pixel in the /theme-sample gallery now finds no non-red channel in night mode. Icons come from @tabler/icons-react with the 102 Semantic names the app uses mapped to components; they stroke with currentColor, so they follow the theme without any rule of their own. Toasts keep the react-semantic-toasts-2 signature so those call sites migrate by changing an import. TypeScript 5 is required: Mantine's declarations use `const` type parameters, which 4.9 cannot parse (Mantine 7 is the same, so downgrading is no escape). That needs two npm overrides to keep clean installs working, both verified against a Docker build: react-scripts declares peerOptional typescript ^3||^4, and fork-ts-checker 6.5.2 assigns typescript.performance.mark, which TS 5 made getter-only - crashing the dev server's type checker on every rebuild. semantic-baseline.test.js ratchets the removal: it fails if a file not already listed imports Semantic UI. An ESLint rule would have printed 75 warnings per rebuild, and CI unsets CI= for its build step, so it would guard nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:22:03 -06:00
) => {
const fromRole = role ? roleProps[role] : undefined;
ui: three button-layout defects, each with a different cause Reported together as "button groups", but they turned out to be three unrelated things. The file browser's footer buttons touched while the Channel Edit page's had gaps. Channel Edit lays its buttons out with Mantine's `Group`, which is a flex row with a gap; the footer is hand-rolled and was a plain block, so its inline-block buttons sat flush -- JSX drops whitespace-only lines, so there was not even a space character between them. The footer is now the flex row every other button group already is, wrapping because it carries eight controls. The Settings config table's Import and Save buttons had off-centre icons while Restore in the same row looked right. Restore is an IconButton -- an ActionIcon, which centres a lone glyph by construction. Import and Save are icon-only Buttons, and an icon on a Button goes into Mantine's `leftSection`, which carries a `margin-inline-end` to hold it off the label. With no label that margin is pure offset: measured at 8px left of centre. An icon with no label is now rendered as the button's content instead, so it centres. `Children.toArray` decides that, because it drops nulls and the file browser passes `{label('Delete')}` -- null whenever the bar is too narrow for words, which is exactly where the offset showed most. The search field's clear button sat inside the field rather than attached to it: the control is a bordered box with `padding: 0 8px`, leaving the button 9px short of the edge and 2px short of the height. Semantic welded it to the field -- measured on the QA Pi at zero gap and full height -- so it now stretches and negative margins cancel the padding and the flex gap. Negative margins rather than dropping the control's padding, because that padding is what holds the search icon and the text off the border; only this one child should escape it. The `html` prefix on that rule is load-bearing: Mantine sets ActionIcon's height from `--ai-size` on its own single class, and at equal specificity the winner is whichever stylesheet loaded last. Fourteen new cases. One correction worth recording: the height assertion first compared against the control's border box and failed on correct behaviour -- the button fills the CONTENT box, and the 1px above and below it is the field's own border, which it should sit inside rather than paint over. Each fix planted separately; the icon change reddens three, the footer one, the clear button one. Every case is paired with an inverse -- a labelled button must keep its icon on the left, and the field must still be padded around its other children. 1128 jest / 63 suites, 226 in ui-layout.cy.js, clean npm run build. Confirmed live: footer gaps 8px on one row, config glyphs at 0.0px from centre, clear button 1px from the field edge at the field's full inner height.
2026-08-02 17:13:28 -06:00
/*
* An icon with no label is the button's CONTENT, not a leading icon.
*
* Mantine gives `leftSection` a `margin-inline-end` to hold it off the label. With no
* label that margin is pure offset: the glyph sits about 8px left of the button's centre,
* which is what the import and save buttons on the Settings config table looked like next
* to a correctly centred Restore -- Restore is an IconButton, which centres a lone glyph
* by construction.
*
* `React.Children.toArray` drops null, undefined and booleans, which matters because the
* file browser's footer passes `{label('Delete')}` and that is null whenever the bar is
* too narrow for words. Those buttons are icon-only at exactly the widths where the
* offset is most visible.
*/
review: one inset, and two tests that could not fail Acting on the review of #625. No bugs; four suggestions and three nits, of which two were already handled and one needed no code. The weld used three separate 8px literals -- the control's padding, its flex gap, and the clear button's negative margins -- which had to stay in lockstep or the button would drift past the border or back inside it. They are one `--searchbox-inset` now. Cancelling the gap on the clear button's LEFT was a real side effect: when a field is both `loading` and `clearable`, which SearchResultsInput allows, the DOM order is input, spinner, clear, and the spinner ended up welded to the divider. Only the right-hand padding is cancelled now; the 8px before the button stays, and a case covers the pair. `min-height` was still coming from `--ai-size`, so the rule only half-owned the height it claimed to set -- true today by 0.15px, the field being 36.15 against a 36 minimum. Unset now. Two nits needed no code and are recorded as findings rather than changes. The clear button's corners are already square: the theme zeroes every `--mantine-radius-*`, so there are no wedges to show against the field, and a case now depends on that explicitly. And a label-less button with BOTH `icon` and `iconAfter` would stay mis-centred, but no call site writes one, so the limit is documented instead of designed around. Blank children now count as no label. `toArray` drops null and booleans but keeps `''` and whitespace, which would have left the glyph off-centre; no call site does it today and the predicate should not depend on that. Two corrections to my own work, both caught by planting: The fragment case. I wrote that `toArray` flattens fragments away to nothing and tested `<></>` as unlabelled. It does not -- it returns the fragment as one child, so `<></>` is indistinguishable from real content. The test now records what actually happens, with `<>Delete</>` as the shape that occurs in practice. The min-height assertion compared `--ai-size` -- which reads back as `calc(2.25rem * var(--mantine-scale))` -- against a computed `36px`. Two strings that can never match, so it passed with the rule removed. It compares resolved lengths now, paired with an ordinary IconButton that DOES carry the minimum, so its absence is attributable to this rule rather than to a Mantine default. An earlier attempt fabricated a shorter field by overriding the input's font size; that was deleted, because a test that invents a layout the app never renders is not evidence about this app. 1128 jest / 63 suites, 231 in ui-layout.cy.js, clean npm run build. Confirmed live: inset 8px, clear button 1px from the field edge, 8px from the input, at the field's full inner height with no minimum of its own.
2026-08-02 17:28:46 -06:00
/*
* Blank text is not a label. `toArray` drops null, undefined and booleans, but keeps an
* empty or whitespace-only string, which would leave the icon in `leftSection` with its
* margin and the glyph off-centre again. No call site does that today; the predicate
* should not depend on that staying true.
*
* Two limits, both deliberate. "Icon-only" means exactly one of `icon`/`iconAfter`: a
* label-less button with both would keep both section margins and stay mis-centred, and
* there are no such call sites to design for. And any ELEMENT child counts as a label,
* including an empty fragment -- `toArray` does not look inside one, so `<></>` is
* indistinguishable from real content without recursing into fragment props.
*/
const labelled = React.Children.toArray(children)
.some(child => typeof child !== 'string' || child.trim().length > 0);
ui: three button-layout defects, each with a different cause Reported together as "button groups", but they turned out to be three unrelated things. The file browser's footer buttons touched while the Channel Edit page's had gaps. Channel Edit lays its buttons out with Mantine's `Group`, which is a flex row with a gap; the footer is hand-rolled and was a plain block, so its inline-block buttons sat flush -- JSX drops whitespace-only lines, so there was not even a space character between them. The footer is now the flex row every other button group already is, wrapping because it carries eight controls. The Settings config table's Import and Save buttons had off-centre icons while Restore in the same row looked right. Restore is an IconButton -- an ActionIcon, which centres a lone glyph by construction. Import and Save are icon-only Buttons, and an icon on a Button goes into Mantine's `leftSection`, which carries a `margin-inline-end` to hold it off the label. With no label that margin is pure offset: measured at 8px left of centre. An icon with no label is now rendered as the button's content instead, so it centres. `Children.toArray` decides that, because it drops nulls and the file browser passes `{label('Delete')}` -- null whenever the bar is too narrow for words, which is exactly where the offset showed most. The search field's clear button sat inside the field rather than attached to it: the control is a bordered box with `padding: 0 8px`, leaving the button 9px short of the edge and 2px short of the height. Semantic welded it to the field -- measured on the QA Pi at zero gap and full height -- so it now stretches and negative margins cancel the padding and the flex gap. Negative margins rather than dropping the control's padding, because that padding is what holds the search icon and the text off the border; only this one child should escape it. The `html` prefix on that rule is load-bearing: Mantine sets ActionIcon's height from `--ai-size` on its own single class, and at equal specificity the winner is whichever stylesheet loaded last. Fourteen new cases. One correction worth recording: the height assertion first compared against the control's border box and failed on correct behaviour -- the button fills the CONTENT box, and the 1px above and below it is the field's own border, which it should sit inside rather than paint over. Each fix planted separately; the icon change reddens three, the footer one, the clear button one. Every case is paired with an inverse -- a labelled button must keep its icon on the left, and the field must still be padded around its other children. 1128 jest / 63 suites, 226 in ui-layout.cy.js, clean npm run build. Confirmed live: footer gaps 8px on one row, config glyphs at 0.0px from centre, clear button 1px from the field edge at the field's full inner height.
2026-08-02 17:13:28 -06:00
const soleIcon = !labelled && (icon || iconAfter) && !(icon && iconAfter)
? renderIcon(icon || iconAfter)
: undefined;
ui: token-driven component library on Mantine Adds src/components/ui: buttons, messages, labels, progress, status, tables, form controls, panels, cards, statistics, modals, tooltips, tabs, and icons. Everything resolves its colors through theme tokens, so no component branches on the current theme, and Semantic UI keeps rendering the app meanwhile. Mantine is bridged rather than restyled: its own CSS variables are pointed at our tokens. Four details were not obvious, and each caused a visible defect in night mode before being fixed: - Structural colors must go in the resolver's light/dark blocks. Mantine emits its values under [data-mantine-color-scheme], which outranks the :root block that `variables` becomes, so its grays were winning. - The dark and gray palettes are mapped shade by shade to tokens. Mantine reads them directly for table borders, zebra stripes and input chrome; they can be neither flattened nor left alone. - Mantine writes --button-* as inline styles, which no stylesheet can outrank, so the dashed danger treatment sets real properties instead. - Switch colors are written onto inner elements, so they need scoped CSS rather than a variable on the root. Adds a --knob token for control handles, because Mantine's default thumb follows theme.white, which is near-black in the dark-based themes. An audit of every painted pixel in the /theme-sample gallery now finds no non-red channel in night mode. Icons come from @tabler/icons-react with the 102 Semantic names the app uses mapped to components; they stroke with currentColor, so they follow the theme without any rule of their own. Toasts keep the react-semantic-toasts-2 signature so those call sites migrate by changing an import. TypeScript 5 is required: Mantine's declarations use `const` type parameters, which 4.9 cannot parse (Mantine 7 is the same, so downgrading is no escape). That needs two npm overrides to keep clean installs working, both verified against a Docker build: react-scripts declares peerOptional typescript ^3||^4, and fork-ts-checker 6.5.2 assigns typescript.performance.mark, which TS 5 made getter-only - crashing the dev server's type checker on every rebuild. semantic-baseline.test.js ratchets the removal: it fails if a file not already listed imports Semantic UI. An ESLint rule would have printed 75 warnings per rebuild, and CI unsets CI= for its build step, so it would guard nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:22:03 -06:00
return <MButton
ref={ref}
ui: three silent library defects the agents' reports surfaced Each of these was reported by an agent working around it rather than patched in one file, which is why all three are fixed once, for every call site. Button accepted an `href` and dropped it. Mantine renders an anchor only when it also gets `component='a'`; Semantic spelled that `as='a'`. So a migrated call site that kept just the href produced something that looks like a download link and navigates nowhere -- silently, with no warning. Button and IconButton now infer the anchor from the href. The media filter ran twice on nested media. A `.media` wrapper filters its subtree as one unit -- the audio player needs that, because its control glyphs are SVG and the element-type rule does not reach them -- but the poster image inside also matched that rule. Two passes of a luminance-to-red matrix is not one: the second takes 0.2126 of an already-red pixel, so the poster came out markedly darker than everything around it. Leaf media inside a filtered wrapper is now left alone. Progress had no indeterminate state. Semantic's `indicating` animated a stripe when the size was unknown; without it an upload that has not reported yet sits at 0% and reads as stalled -- and `aria-valuenow="0"` tells a screen reader the same wrong thing. It now animates, reports no value, and falls back to a static fill under prefers-reduced-motion. Also: night and amber now override an inline `--label-color`. A call site may legitimately need many distinguishable colours -- the Flasher's chip badges use a twelve-colour palette no four tokens can express -- but on a night-adapted screen a green or blue chip badge defeats the entire mode. The labels lose their distinctness there, which is the right trade.
2026-07-30 13:15:03 -06:00
/*
* An `href` makes this an anchor. Mantine needs `component='a'` to render one, and
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>
2026-08-05 19:15:57 -06:00
* 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.
ui: three silent library defects the agents' reports surfaced Each of these was reported by an agent working around it rather than patched in one file, which is why all three are fixed once, for every call site. Button accepted an `href` and dropped it. Mantine renders an anchor only when it also gets `component='a'`; Semantic spelled that `as='a'`. So a migrated call site that kept just the href produced something that looks like a download link and navigates nowhere -- silently, with no warning. Button and IconButton now infer the anchor from the href. The media filter ran twice on nested media. A `.media` wrapper filters its subtree as one unit -- the audio player needs that, because its control glyphs are SVG and the element-type rule does not reach them -- but the poster image inside also matched that rule. Two passes of a luminance-to-red matrix is not one: the second takes 0.2126 of an already-red pixel, so the poster came out markedly darker than everything around it. Leaf media inside a filtered wrapper is now left alone. Progress had no indeterminate state. Semantic's `indicating` animated a stripe when the size was unknown; without it an upload that has not reported yet sits at 0% and reads as stalled -- and `aria-valuenow="0"` tells a screen reader the same wrong thing. It now animates, reports no value, and falls back to a static fill under prefers-reduced-motion. Also: night and amber now override an inline `--label-color`. A call site may legitimately need many distinguishable colours -- the Flasher's chip badges use a twelve-colour palette no four tokens can express -- but on a night-adapted screen a green or blue chip badge defeats the entire mode. The labels lose their distinctness there, which is the right trade.
2026-07-30 13:15:03 -06:00
*/
component={props.component ?? (props.href ? 'a' : undefined)}
ui: token-driven component library on Mantine Adds src/components/ui: buttons, messages, labels, progress, status, tables, form controls, panels, cards, statistics, modals, tooltips, tabs, and icons. Everything resolves its colors through theme tokens, so no component branches on the current theme, and Semantic UI keeps rendering the app meanwhile. Mantine is bridged rather than restyled: its own CSS variables are pointed at our tokens. Four details were not obvious, and each caused a visible defect in night mode before being fixed: - Structural colors must go in the resolver's light/dark blocks. Mantine emits its values under [data-mantine-color-scheme], which outranks the :root block that `variables` becomes, so its grays were winning. - The dark and gray palettes are mapped shade by shade to tokens. Mantine reads them directly for table borders, zebra stripes and input chrome; they can be neither flattened nor left alone. - Mantine writes --button-* as inline styles, which no stylesheet can outrank, so the dashed danger treatment sets real properties instead. - Switch colors are written onto inner elements, so they need scoped CSS rather than a variable on the root. Adds a --knob token for control handles, because Mantine's default thumb follows theme.white, which is near-black in the dark-based themes. An audit of every painted pixel in the /theme-sample gallery now finds no non-red channel in night mode. Icons come from @tabler/icons-react with the 102 Semantic names the app uses mapped to components; they stroke with currentColor, so they follow the theme without any rule of their own. Toasts keep the react-semantic-toasts-2 signature so those call sites migrate by changing an import. TypeScript 5 is required: Mantine's declarations use `const` type parameters, which 4.9 cannot parse (Mantine 7 is the same, so downgrading is no escape). That needs two npm overrides to keep clean installs working, both verified against a Docker build: react-scripts declares peerOptional typescript ^3||^4, and fork-ts-checker 6.5.2 assigns typescript.performance.mark, which TS 5 made getter-only - crashing the dev server's type checker on every rebuild. semantic-baseline.test.js ratchets the removal: it fails if a file not already listed imports Semantic UI. An ESLint rule would have printed 75 warnings per rebuild, and CI unsets CI= for its build step, so it would guard nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:22:03 -06:00
// An explicit color/variant still wins, so a call site can deviate when it must.
color={props.color ?? fromRole?.color}
variant={props.variant ?? fromRole?.variant}
className={[fromRole?.className, className].filter(Boolean).join(' ') || undefined}
ui: three button-layout defects, each with a different cause Reported together as "button groups", but they turned out to be three unrelated things. The file browser's footer buttons touched while the Channel Edit page's had gaps. Channel Edit lays its buttons out with Mantine's `Group`, which is a flex row with a gap; the footer is hand-rolled and was a plain block, so its inline-block buttons sat flush -- JSX drops whitespace-only lines, so there was not even a space character between them. The footer is now the flex row every other button group already is, wrapping because it carries eight controls. The Settings config table's Import and Save buttons had off-centre icons while Restore in the same row looked right. Restore is an IconButton -- an ActionIcon, which centres a lone glyph by construction. Import and Save are icon-only Buttons, and an icon on a Button goes into Mantine's `leftSection`, which carries a `margin-inline-end` to hold it off the label. With no label that margin is pure offset: measured at 8px left of centre. An icon with no label is now rendered as the button's content instead, so it centres. `Children.toArray` decides that, because it drops nulls and the file browser passes `{label('Delete')}` -- null whenever the bar is too narrow for words, which is exactly where the offset showed most. The search field's clear button sat inside the field rather than attached to it: the control is a bordered box with `padding: 0 8px`, leaving the button 9px short of the edge and 2px short of the height. Semantic welded it to the field -- measured on the QA Pi at zero gap and full height -- so it now stretches and negative margins cancel the padding and the flex gap. Negative margins rather than dropping the control's padding, because that padding is what holds the search icon and the text off the border; only this one child should escape it. The `html` prefix on that rule is load-bearing: Mantine sets ActionIcon's height from `--ai-size` on its own single class, and at equal specificity the winner is whichever stylesheet loaded last. Fourteen new cases. One correction worth recording: the height assertion first compared against the control's border box and failed on correct behaviour -- the button fills the CONTENT box, and the 1px above and below it is the field's own border, which it should sit inside rather than paint over. Each fix planted separately; the icon change reddens three, the footer one, the clear button one. Every case is paired with an inverse -- a labelled button must keep its icon on the left, and the field must still be padded around its other children. 1128 jest / 63 suites, 226 in ui-layout.cy.js, clean npm run build. Confirmed live: footer gaps 8px on one row, config glyphs at 0.0px from centre, clear button 1px from the field edge at the field's full inner height.
2026-08-02 17:13:28 -06:00
leftSection={soleIcon ? undefined : renderIcon(icon)}
rightSection={soleIcon ? undefined : renderIcon(iconAfter)}
ui: the map pin buttons were three sizes for three reasons Reported as "all different sizes", and the pin row's three actions measured 46x30, 30x30 and 54x36. Three causes. APIButton dropped `size`. `useAPIButton` declared `size = 'medium'` as a parameter and then never put it into the props it hands to Button, so every APIButton in the app rendered at Mantine's default regardless of what its call site asked for -- seventeen call sites pass one, from `xs` on the map pins to `big` and `huge` on Settings. The delete pin was 36px tall beside two 30px siblings because of it. Passing the parameter default through would have fixed the bug by breaking everything else -- every APIButton naming no size would have grown from `sm` to `md` -- so the default is undefined now and Button's own default stands. Eight buttons on three other pages do grow as a result, which is their call sites finally being honoured: VideoPlayer's `large`, one of Channels' `big`, and Settings' two `big` and two `huge`. An icon-only Button was not square. A Button is as wide as its content plus its horizontal padding; an ActionIcon is square by construction; and the pin row mixes the two, because AddToPlaylistButton renders an IconButton when it has no label. Their heights already agreed -- ActionIcon's scale was remapped onto Button's some commits ago -- and this is the other axis of the same alignment, keyed off Mantine's own `--button-height` so the two stay in step through every size. They were also misaligned and touching, which is a third thing: bare inline-block siblings align on text BASELINES, and a Button's baseline is not an ActionIcon's, so the middle control sat 4.3px above the other two. A Group, as every other row of buttons in the app uses. Written test-first at Roland's request. Seven cypress cases went red on the sizes -- widths 46 against 30, 54 against 36, 62 against 42, 70 against 50, and "xs is shorter than lg: expected 36 to be below 32" for the dropped size -- with both inverses green throughout: a labelled button stays as wide as its label, and an APIButton naming no size keeps the height it had. The alignment claim is measured in ui-layout.cy.js, including the premise that bare siblings really are misaligned, since three controls of identical height not lining up is not believable otherwise. The call sites are held by a source guard, because MapPins fetches from the API and builds its rows through SortableTable. That guard earned itself twice over. Written for the pin rows, it found the map-files subscribe cell putting a preview button beside a subscribe button with the same defect. Then a first attempt at a general "any cell with two controls" rule had to be abandoned: a cell whose buttons are the arms of a ternary renders only one and needs no Group, and excluding ternaries to allow for that also excluded the subscribe cell, whose `role={isSubscribed ? ... : ...}` says nothing about how many controls it holds. Named cells with a stated scope beat a general rule that quietly stops covering things. Two slips of mine in that file, both caught by running it: `expect(value, message)` is chai and throws in jest, and `onSave(pin.id, ...)` was not unique to the cell it named -- the edit row's TextInput calls it from onKeyDown in a different cell, so the check passed against the wrong one. 1132 jest / 64 suites, 243 in ui-layout.cy.js, clean npm run build. Confirmed live: three 30x30 controls, vertical spread 0, 10px gaps.
2026-08-02 17:44:25 -06:00
/*
* Marks the icon-only case for ui.css, which makes such a button square so it matches
* an IconButton of the same size. A Button is as wide as its content plus padding and
* an ActionIcon is square, so a row mixing the two -- the map's pin actions are edit
* (Button), add-to-playlist (IconButton) and delete (Button) -- came out 46, 30 and 46
* wide at the same nominal size.
*/
data-icon-only={soleIcon ? 'true' : undefined}
ui: token-driven component library on Mantine Adds src/components/ui: buttons, messages, labels, progress, status, tables, form controls, panels, cards, statistics, modals, tooltips, tabs, and icons. Everything resolves its colors through theme tokens, so no component branches on the current theme, and Semantic UI keeps rendering the app meanwhile. Mantine is bridged rather than restyled: its own CSS variables are pointed at our tokens. Four details were not obvious, and each caused a visible defect in night mode before being fixed: - Structural colors must go in the resolver's light/dark blocks. Mantine emits its values under [data-mantine-color-scheme], which outranks the :root block that `variables` becomes, so its grays were winning. - The dark and gray palettes are mapped shade by shade to tokens. Mantine reads them directly for table borders, zebra stripes and input chrome; they can be neither flattened nor left alone. - Mantine writes --button-* as inline styles, which no stylesheet can outrank, so the dashed danger treatment sets real properties instead. - Switch colors are written onto inner elements, so they need scoped CSS rather than a variable on the root. Adds a --knob token for control handles, because Mantine's default thumb follows theme.white, which is near-black in the dark-based themes. An audit of every painted pixel in the /theme-sample gallery now finds no non-red channel in night mode. Icons come from @tabler/icons-react with the 102 Semantic names the app uses mapped to components; they stroke with currentColor, so they follow the theme without any rule of their own. Toasts keep the react-semantic-toasts-2 signature so those call sites migrate by changing an import. TypeScript 5 is required: Mantine's declarations use `const` type parameters, which 4.9 cannot parse (Mantine 7 is the same, so downgrading is no escape). That needs two npm overrides to keep clean installs working, both verified against a Docker build: react-scripts declares peerOptional typescript ^3||^4, and fork-ts-checker 6.5.2 assigns typescript.performance.mark, which TS 5 made getter-only - crashing the dev server's type checker on every rebuild. semantic-baseline.test.js ratchets the removal: it fails if a file not already listed imports Semantic UI. An ESLint rule would have printed 75 warnings per rebuild, and CI unsets CI= for its build step, so it would guard nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:22:03 -06:00
{...props}
ui: Common.js and thirteen more files off Semantic Phase 2, part 3. Baseline 63 -> 50. Common.js is the one that mattered: 51 files import it, so its Semantic usage was propping up the rest of the migration. Everything in it now comes from the library -- Paginator, the two search inputs, DirectorySearch, TabLinks, the toggles, the messages, APIButton, Breadcrumbs, SimpleAccordion, IframeViewer, CardPoster, FileIcon, LoadStatistic, the theme menu, and the hotspot icons. Also migrated: Placeholder, Apps, ArchivePreview, CbzViewer, AddToPlaylist, TaggedDeleteConfirmModal, the collection edit/tag forms, and four inventory components. New in the library, all in the gallery and tested: SearchBox Semantic's `<Search category>` with its grouped results shape kept, because four call sites already build it. A real combobox rather than a div that listens for clicks. Pagination Semantic's prop names; onPageChange gets the page number. TabBar the strip only -- routing stays with the caller. IconStack Semantic's IconGroup, named once for assistive technology. Table.HeaderCell sorted/onSort -- a real button carrying aria-sort. Button/IconButton translate Semantic's size names. That last one is defensive: unmigrated callers still pass size='tiny', and Mantine drops a size it does not recognise without a word, so the button quietly renders at the wrong size. Mapping here keeps the mixed state honest. Three real bugs found and fixed on the way, none of them cosmetic: * `fileSuffixIconName` returned fourteen icon names the map did not have, so opening a folder with a PDF, a Word document or an archive in it logged an error and drew a fallback glyph. All fourteen mapped, with a test that walks the mapper's own return values, and another asserting every mapped name resolves to a Tabler component that exists. * `Statistic` relied on Semantic's `Statistic.Group` for its spacing, so on the dashboard the load averages ran together into "1 MIN. LOAD5 MIN. LOAD15 MIN. LOAD". It carries its own spacing now. * Semantic's inverted stylesheet colors every descendant of an inverted segment, so Toggle labels inherited #dddddd -- white pixels in night mode, on the Settings page, which is exactly where someone adjusts their WROLPi in the dark. Restated from tokens. The baseline ratchet gained a third assertion: a migrated file may still lean on Semantic's CSS classes without importing the library, which the import check cannot see and which would collapse silently when Phase 4 deletes the stylesheet. There is one such bridge today and it is now declared.
2026-07-30 10:00:48 -06:00
size={resolveSize(props.size)}
ui: three button-layout defects, each with a different cause Reported together as "button groups", but they turned out to be three unrelated things. The file browser's footer buttons touched while the Channel Edit page's had gaps. Channel Edit lays its buttons out with Mantine's `Group`, which is a flex row with a gap; the footer is hand-rolled and was a plain block, so its inline-block buttons sat flush -- JSX drops whitespace-only lines, so there was not even a space character between them. The footer is now the flex row every other button group already is, wrapping because it carries eight controls. The Settings config table's Import and Save buttons had off-centre icons while Restore in the same row looked right. Restore is an IconButton -- an ActionIcon, which centres a lone glyph by construction. Import and Save are icon-only Buttons, and an icon on a Button goes into Mantine's `leftSection`, which carries a `margin-inline-end` to hold it off the label. With no label that margin is pure offset: measured at 8px left of centre. An icon with no label is now rendered as the button's content instead, so it centres. `Children.toArray` decides that, because it drops nulls and the file browser passes `{label('Delete')}` -- null whenever the bar is too narrow for words, which is exactly where the offset showed most. The search field's clear button sat inside the field rather than attached to it: the control is a bordered box with `padding: 0 8px`, leaving the button 9px short of the edge and 2px short of the height. Semantic welded it to the field -- measured on the QA Pi at zero gap and full height -- so it now stretches and negative margins cancel the padding and the flex gap. Negative margins rather than dropping the control's padding, because that padding is what holds the search icon and the text off the border; only this one child should escape it. The `html` prefix on that rule is load-bearing: Mantine sets ActionIcon's height from `--ai-size` on its own single class, and at equal specificity the winner is whichever stylesheet loaded last. Fourteen new cases. One correction worth recording: the height assertion first compared against the control's border box and failed on correct behaviour -- the button fills the CONTENT box, and the 1px above and below it is the field's own border, which it should sit inside rather than paint over. Each fix planted separately; the icon change reddens three, the footer one, the clear button one. Every case is paired with an inverse -- a labelled button must keep its icon on the left, and the field must still be padded around its other children. 1128 jest / 63 suites, 226 in ui-layout.cy.js, clean npm run build. Confirmed live: footer gaps 8px on one row, config glyphs at 0.0px from centre, clear button 1px from the field edge at the field's full inner height.
2026-08-02 17:13:28 -06:00
>
{soleIcon ?? children}
</MButton>
ui: token-driven component library on Mantine Adds src/components/ui: buttons, messages, labels, progress, status, tables, form controls, panels, cards, statistics, modals, tooltips, tabs, and icons. Everything resolves its colors through theme tokens, so no component branches on the current theme, and Semantic UI keeps rendering the app meanwhile. Mantine is bridged rather than restyled: its own CSS variables are pointed at our tokens. Four details were not obvious, and each caused a visible defect in night mode before being fixed: - Structural colors must go in the resolver's light/dark blocks. Mantine emits its values under [data-mantine-color-scheme], which outranks the :root block that `variables` becomes, so its grays were winning. - The dark and gray palettes are mapped shade by shade to tokens. Mantine reads them directly for table borders, zebra stripes and input chrome; they can be neither flattened nor left alone. - Mantine writes --button-* as inline styles, which no stylesheet can outrank, so the dashed danger treatment sets real properties instead. - Switch colors are written onto inner elements, so they need scoped CSS rather than a variable on the root. Adds a --knob token for control handles, because Mantine's default thumb follows theme.white, which is near-black in the dark-based themes. An audit of every painted pixel in the /theme-sample gallery now finds no non-red channel in night mode. Icons come from @tabler/icons-react with the 102 Semantic names the app uses mapped to components; they stroke with currentColor, so they follow the theme without any rule of their own. Toasts keep the react-semantic-toasts-2 signature so those call sites migrate by changing an import. TypeScript 5 is required: Mantine's declarations use `const` type parameters, which 4.9 cannot parse (Mantine 7 is the same, so downgrading is no escape). That needs two npm overrides to keep clean installs working, both verified against a Docker build: react-scripts declares peerOptional typescript ^3||^4, and fork-ts-checker 6.5.2 assigns typescript.performance.mark, which TS 5 made getter-only - crashing the dev server's type checker on every rebuild. semantic-baseline.test.js ratchets the removal: it fails if a file not already listed imports Semantic UI. An ESLint rule would have printed 75 warnings per rebuild, and CI unsets CI= for its build step, so it would guard nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:22:03 -06:00
});
Button.displayName = 'Button';
export interface IconButtonProps extends Omit<ActionIconProps, 'children'> {
role?: ButtonRole;
icon: string | React.ComponentType<any>;
/** Required: an icon-only button has no visible name. */
label: string;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
type?: 'button' | 'submit' | 'reset';
ui: three silent library defects the agents' reports surfaced Each of these was reported by an agent working around it rather than patched in one file, which is why all three are fixed once, for every call site. Button accepted an `href` and dropped it. Mantine renders an anchor only when it also gets `component='a'`; Semantic spelled that `as='a'`. So a migrated call site that kept just the href produced something that looks like a download link and navigates nowhere -- silently, with no warning. Button and IconButton now infer the anchor from the href. The media filter ran twice on nested media. A `.media` wrapper filters its subtree as one unit -- the audio player needs that, because its control glyphs are SVG and the element-type rule does not reach them -- but the poster image inside also matched that rule. Two passes of a luminance-to-red matrix is not one: the second takes 0.2126 of an already-red pixel, so the poster came out markedly darker than everything around it. Leaf media inside a filtered wrapper is now left alone. Progress had no indeterminate state. Semantic's `indicating` animated a stripe when the size was unknown; without it an upload that has not reported yet sits at 0% and reads as stalled -- and `aria-valuenow="0"` tells a screen reader the same wrong thing. It now animates, reports no value, and falls back to a static fill under prefers-reduced-motion. Also: night and amber now override an inline `--label-color`. A call site may legitimately need many distinguishable colours -- the Flasher's chip badges use a twelve-colour palette no four tokens can express -- but on a night-adapted screen a green or blue chip badge defeats the entire mode. The labels lose their distinctness there, which is the right trade.
2026-07-30 13:15:03 -06:00
href?: string;
target?: string;
component?: any;
ui: token-driven component library on Mantine Adds src/components/ui: buttons, messages, labels, progress, status, tables, form controls, panels, cards, statistics, modals, tooltips, tabs, and icons. Everything resolves its colors through theme tokens, so no component branches on the current theme, and Semantic UI keeps rendering the app meanwhile. Mantine is bridged rather than restyled: its own CSS variables are pointed at our tokens. Four details were not obvious, and each caused a visible defect in night mode before being fixed: - Structural colors must go in the resolver's light/dark blocks. Mantine emits its values under [data-mantine-color-scheme], which outranks the :root block that `variables` becomes, so its grays were winning. - The dark and gray palettes are mapped shade by shade to tokens. Mantine reads them directly for table borders, zebra stripes and input chrome; they can be neither flattened nor left alone. - Mantine writes --button-* as inline styles, which no stylesheet can outrank, so the dashed danger treatment sets real properties instead. - Switch colors are written onto inner elements, so they need scoped CSS rather than a variable on the root. Adds a --knob token for control handles, because Mantine's default thumb follows theme.white, which is near-black in the dark-based themes. An audit of every painted pixel in the /theme-sample gallery now finds no non-red channel in night mode. Icons come from @tabler/icons-react with the 102 Semantic names the app uses mapped to components; they stroke with currentColor, so they follow the theme without any rule of their own. Toasts keep the react-semantic-toasts-2 signature so those call sites migrate by changing an import. TypeScript 5 is required: Mantine's declarations use `const` type parameters, which 4.9 cannot parse (Mantine 7 is the same, so downgrading is no escape). That needs two npm overrides to keep clean installs working, both verified against a Docker build: react-scripts declares peerOptional typescript ^3||^4, and fork-ts-checker 6.5.2 assigns typescript.performance.mark, which TS 5 made getter-only - crashing the dev server's type checker on every rebuild. semantic-baseline.test.js ratchets the removal: it fails if a file not already listed imports Semantic UI. An ESLint rule would have printed 75 warnings per rebuild, and CI unsets CI= for its build step, so it would guard nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:22:03 -06:00
}
/**
* A square, icon-only button, exactly as tall as a labelled `Button` beside it.
*
* The height needs BOTH halves of the fix. ui.css aligns ActionIcon's size scale with
* Button's, and this default aligns the starting point: Mantine defaults ActionIcon to `md`
* where it defaults Button to `sm`, so leaving it alone would trade a 28-vs-36 mismatch for
* a 42-vs-36 one.
*/
ui: token-driven component library on Mantine Adds src/components/ui: buttons, messages, labels, progress, status, tables, form controls, panels, cards, statistics, modals, tooltips, tabs, and icons. Everything resolves its colors through theme tokens, so no component branches on the current theme, and Semantic UI keeps rendering the app meanwhile. Mantine is bridged rather than restyled: its own CSS variables are pointed at our tokens. Four details were not obvious, and each caused a visible defect in night mode before being fixed: - Structural colors must go in the resolver's light/dark blocks. Mantine emits its values under [data-mantine-color-scheme], which outranks the :root block that `variables` becomes, so its grays were winning. - The dark and gray palettes are mapped shade by shade to tokens. Mantine reads them directly for table borders, zebra stripes and input chrome; they can be neither flattened nor left alone. - Mantine writes --button-* as inline styles, which no stylesheet can outrank, so the dashed danger treatment sets real properties instead. - Switch colors are written onto inner elements, so they need scoped CSS rather than a variable on the root. Adds a --knob token for control handles, because Mantine's default thumb follows theme.white, which is near-black in the dark-based themes. An audit of every painted pixel in the /theme-sample gallery now finds no non-red channel in night mode. Icons come from @tabler/icons-react with the 102 Semantic names the app uses mapped to components; they stroke with currentColor, so they follow the theme without any rule of their own. Toasts keep the react-semantic-toasts-2 signature so those call sites migrate by changing an import. TypeScript 5 is required: Mantine's declarations use `const` type parameters, which 4.9 cannot parse (Mantine 7 is the same, so downgrading is no escape). That needs two npm overrides to keep clean installs working, both verified against a Docker build: react-scripts declares peerOptional typescript ^3||^4, and fork-ts-checker 6.5.2 assigns typescript.performance.mark, which TS 5 made getter-only - crashing the dev server's type checker on every rebuild. semantic-baseline.test.js ratchets the removal: it fails if a file not already listed imports Semantic UI. An ESLint rule would have printed 75 warnings per rebuild, and CI unsets CI= for its build step, so it would guard nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:22:03 -06:00
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((
{role, icon, label, className, size = 'sm', ...props}, ref
ui: token-driven component library on Mantine Adds src/components/ui: buttons, messages, labels, progress, status, tables, form controls, panels, cards, statistics, modals, tooltips, tabs, and icons. Everything resolves its colors through theme tokens, so no component branches on the current theme, and Semantic UI keeps rendering the app meanwhile. Mantine is bridged rather than restyled: its own CSS variables are pointed at our tokens. Four details were not obvious, and each caused a visible defect in night mode before being fixed: - Structural colors must go in the resolver's light/dark blocks. Mantine emits its values under [data-mantine-color-scheme], which outranks the :root block that `variables` becomes, so its grays were winning. - The dark and gray palettes are mapped shade by shade to tokens. Mantine reads them directly for table borders, zebra stripes and input chrome; they can be neither flattened nor left alone. - Mantine writes --button-* as inline styles, which no stylesheet can outrank, so the dashed danger treatment sets real properties instead. - Switch colors are written onto inner elements, so they need scoped CSS rather than a variable on the root. Adds a --knob token for control handles, because Mantine's default thumb follows theme.white, which is near-black in the dark-based themes. An audit of every painted pixel in the /theme-sample gallery now finds no non-red channel in night mode. Icons come from @tabler/icons-react with the 102 Semantic names the app uses mapped to components; they stroke with currentColor, so they follow the theme without any rule of their own. Toasts keep the react-semantic-toasts-2 signature so those call sites migrate by changing an import. TypeScript 5 is required: Mantine's declarations use `const` type parameters, which 4.9 cannot parse (Mantine 7 is the same, so downgrading is no escape). That needs two npm overrides to keep clean installs working, both verified against a Docker build: react-scripts declares peerOptional typescript ^3||^4, and fork-ts-checker 6.5.2 assigns typescript.performance.mark, which TS 5 made getter-only - crashing the dev server's type checker on every rebuild. semantic-baseline.test.js ratchets the removal: it fails if a file not already listed imports Semantic UI. An ESLint rule would have printed 75 warnings per rebuild, and CI unsets CI= for its build step, so it would guard nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:22:03 -06:00
) => {
const fromRole = role ? roleProps[role] : undefined;
return <ActionIcon
ref={ref}
ui: three silent library defects the agents' reports surfaced Each of these was reported by an agent working around it rather than patched in one file, which is why all three are fixed once, for every call site. Button accepted an `href` and dropped it. Mantine renders an anchor only when it also gets `component='a'`; Semantic spelled that `as='a'`. So a migrated call site that kept just the href produced something that looks like a download link and navigates nowhere -- silently, with no warning. Button and IconButton now infer the anchor from the href. The media filter ran twice on nested media. A `.media` wrapper filters its subtree as one unit -- the audio player needs that, because its control glyphs are SVG and the element-type rule does not reach them -- but the poster image inside also matched that rule. Two passes of a luminance-to-red matrix is not one: the second takes 0.2126 of an already-red pixel, so the poster came out markedly darker than everything around it. Leaf media inside a filtered wrapper is now left alone. Progress had no indeterminate state. Semantic's `indicating` animated a stripe when the size was unknown; without it an upload that has not reported yet sits at 0% and reads as stalled -- and `aria-valuenow="0"` tells a screen reader the same wrong thing. It now animates, reports no value, and falls back to a static fill under prefers-reduced-motion. Also: night and amber now override an inline `--label-color`. A call site may legitimately need many distinguishable colours -- the Flasher's chip badges use a twelve-colour palette no four tokens can express -- but on a night-adapted screen a green or blue chip badge defeats the entire mode. The labels lose their distinctness there, which is the right trade.
2026-07-30 13:15:03 -06:00
// See Button: an href without `component='a'` renders a non-navigating button.
component={props.component ?? (props.href ? 'a' : undefined)}
ui: token-driven component library on Mantine Adds src/components/ui: buttons, messages, labels, progress, status, tables, form controls, panels, cards, statistics, modals, tooltips, tabs, and icons. Everything resolves its colors through theme tokens, so no component branches on the current theme, and Semantic UI keeps rendering the app meanwhile. Mantine is bridged rather than restyled: its own CSS variables are pointed at our tokens. Four details were not obvious, and each caused a visible defect in night mode before being fixed: - Structural colors must go in the resolver's light/dark blocks. Mantine emits its values under [data-mantine-color-scheme], which outranks the :root block that `variables` becomes, so its grays were winning. - The dark and gray palettes are mapped shade by shade to tokens. Mantine reads them directly for table borders, zebra stripes and input chrome; they can be neither flattened nor left alone. - Mantine writes --button-* as inline styles, which no stylesheet can outrank, so the dashed danger treatment sets real properties instead. - Switch colors are written onto inner elements, so they need scoped CSS rather than a variable on the root. Adds a --knob token for control handles, because Mantine's default thumb follows theme.white, which is near-black in the dark-based themes. An audit of every painted pixel in the /theme-sample gallery now finds no non-red channel in night mode. Icons come from @tabler/icons-react with the 102 Semantic names the app uses mapped to components; they stroke with currentColor, so they follow the theme without any rule of their own. Toasts keep the react-semantic-toasts-2 signature so those call sites migrate by changing an import. TypeScript 5 is required: Mantine's declarations use `const` type parameters, which 4.9 cannot parse (Mantine 7 is the same, so downgrading is no escape). That needs two npm overrides to keep clean installs working, both verified against a Docker build: react-scripts declares peerOptional typescript ^3||^4, and fork-ts-checker 6.5.2 assigns typescript.performance.mark, which TS 5 made getter-only - crashing the dev server's type checker on every rebuild. semantic-baseline.test.js ratchets the removal: it fails if a file not already listed imports Semantic UI. An ESLint rule would have printed 75 warnings per rebuild, and CI unsets CI= for its build step, so it would guard nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:22:03 -06:00
aria-label={label}
title={label}
color={props.color ?? fromRole?.color}
variant={props.variant ?? fromRole?.variant ?? 'default'}
className={[fromRole?.className, className].filter(Boolean).join(' ') || undefined}
{...props}
size={resolveSize(size)}
ui: token-driven component library on Mantine Adds src/components/ui: buttons, messages, labels, progress, status, tables, form controls, panels, cards, statistics, modals, tooltips, tabs, and icons. Everything resolves its colors through theme tokens, so no component branches on the current theme, and Semantic UI keeps rendering the app meanwhile. Mantine is bridged rather than restyled: its own CSS variables are pointed at our tokens. Four details were not obvious, and each caused a visible defect in night mode before being fixed: - Structural colors must go in the resolver's light/dark blocks. Mantine emits its values under [data-mantine-color-scheme], which outranks the :root block that `variables` becomes, so its grays were winning. - The dark and gray palettes are mapped shade by shade to tokens. Mantine reads them directly for table borders, zebra stripes and input chrome; they can be neither flattened nor left alone. - Mantine writes --button-* as inline styles, which no stylesheet can outrank, so the dashed danger treatment sets real properties instead. - Switch colors are written onto inner elements, so they need scoped CSS rather than a variable on the root. Adds a --knob token for control handles, because Mantine's default thumb follows theme.white, which is near-black in the dark-based themes. An audit of every painted pixel in the /theme-sample gallery now finds no non-red channel in night mode. Icons come from @tabler/icons-react with the 102 Semantic names the app uses mapped to components; they stroke with currentColor, so they follow the theme without any rule of their own. Toasts keep the react-semantic-toasts-2 signature so those call sites migrate by changing an import. TypeScript 5 is required: Mantine's declarations use `const` type parameters, which 4.9 cannot parse (Mantine 7 is the same, so downgrading is no escape). That needs two npm overrides to keep clean installs working, both verified against a Docker build: react-scripts declares peerOptional typescript ^3||^4, and fork-ts-checker 6.5.2 assigns typescript.performance.mark, which TS 5 made getter-only - crashing the dev server's type checker on every rebuild. semantic-baseline.test.js ratchets the removal: it fails if a file not already listed imports Semantic UI. An ESLint rule would have printed 75 warnings per rebuild, and CI unsets CI= for its build step, so it would guard nothing there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:22:03 -06:00
>
{renderIcon(icon)}
</ActionIcon>
});
IconButton.displayName = 'IconButton';
export {ButtonGroup} from '@mantine/core';