From 51f9c8a9874a990d67a2d3f5181cd53433c73711 Mon Sep 17 00:00:00 2001 From: Roland Date: Thu, 30 Jul 2026 13:41:05 -0600 Subject: [PATCH] ui: the last Semantic UI file, and the stylesheet with it ControllerPage was the final call site; Theme.tsx then had nothing left to wrap, so it drops from 861 lines to 338 -- the provider that resolves the theme, stamps `data-theme` and `data-media-filter`, persists both and hands Mantine our tokens. Everything below that was Semantic wrappers. The dark-mode compatibility props went with them. `i`, `s`, `t` and `inverted` existed so unmigrated components could borrow Semantic's dark treatment, and they carried hardcoded greys -- #1B1C1D, #dddddd, #eeeeee -- which is exactly what the token themes replaced. Three call sites in Common.js were still injecting the #eeeeee one into links. Then `semantic-ui-offline/semantic.min.css` came out of App.js. That took more with it than expected, and looking at the running app is the only reason it was caught: Every link lost its colour and gained the browser's underline and visited purple. The dashboard's load averages and section headings were underlined and purple. Links now take `--blue`, which remaps per theme, and underline only on hover -- many of them wrap cards, posters and icons. `box-sizing: border-box` disappeared, which silently changes the size of every padded element in the app. Semantic's reset had been providing it. The navigation bar overflowed a 1000px viewport with `nowrap` and cut "Admin" off the right edge -- the one link a user needs to reach the settings that fix a broken WROLPi. Semantic's menu wrapped; ours does now too. The gzipped CSS bundle is 47.5 kB, and one call site was still leaning on Semantic's grid classes -- now a flex column, since all three of its callers pass library components. The ratchet stays rather than being deleted. At zero it asserts four things: no file imports Semantic UI, none calls its toast library, none leans on its stylesheet, and no file reaches back through Theme.tsx. It can go when the packages are uninstalled. Theme.test.js is deleted, but not before porting the two behaviours it protected: that Button forwards a ref to the real DOM element (a class-component ref once crashed the Control page, because portals call `node.contains`), and that the progress percentage stays readable on every bar colour. --- app/src/App.css | 13 +- app/src/App.js | 1 - app/src/components/Common.js | 8 +- app/src/components/Theme.test.js | 109 --- app/src/components/Theme.tsx | 658 +----------------- .../collections/CollectionEditForm.js | 65 +- app/src/components/ui/ui.css | 48 ++ app/src/components/ui/ui.test.js | 43 ++ app/src/hooks/useForm.js | 65 +- app/src/index.css | 4 +- app/src/semantic-baseline.json | 17 +- app/src/semantic-baseline.test.js | 9 +- app/src/themes/tokens.css | 49 ++ app/src/types/theme.ts | 8 - 14 files changed, 238 insertions(+), 859 deletions(-) delete mode 100644 app/src/components/Theme.test.js diff --git a/app/src/App.css b/app/src/App.css index 9c52d444..7dee8bee 100644 --- a/app/src/App.css +++ b/app/src/App.css @@ -4,7 +4,12 @@ .wrolpi-navbar { display: flex; align-items: stretch; - flex-wrap: nowrap; + /* + * Wraps rather than clipping. Semantic's menu wrapped, and with this many items the + * bar overflows a 1000px viewport -- `nowrap` cut "Admin" off the right edge, which + * is the one link a user needs to reach the settings that fix a broken WROLPi. + */ + flex-wrap: wrap; min-height: 3.4em; padding: 0 0.25em; } @@ -20,7 +25,8 @@ .wrolpi-navbar-link-button { display: flex; align-items: center; - padding: 0 1.1em; + /* Tightened from 1.1em: the bar has eleven links plus nine indicators. */ + padding: 0 0.8em; color: inherit; text-decoration: none; white-space: nowrap; @@ -32,7 +38,8 @@ .wrolpi-navbar-link:hover, .wrolpi-navbar-link-button:hover { - background: rgba(0, 0, 0, 0.08); + /* A translucent black works on every navbar colour, light or dark. */ + background: rgba(0, 0, 0, 0.12); } .channel-edit { diff --git a/app/src/App.js b/app/src/App.js index 68d69f8a..1e372a53 100644 --- a/app/src/App.js +++ b/app/src/App.js @@ -5,7 +5,6 @@ import {createBrowserRouter, createRoutesFromElements, Link, Outlet, Route, Rout import {VideosTabLayout, VideosPage, VideosSettingsPage, VideosStatistics, VideoWrapper} from "./components/Videos"; import AdminRoute from "./components/admin/AdminRoute"; import {Box, Header} from "./components/ui"; -import 'semantic-ui-offline/semantic.min.css'; import '@mantine/core/styles.css'; import '@mantine/notifications/styles.css'; // Theme tokens load last so they win over both component libraries. diff --git a/app/src/components/Common.js b/app/src/components/Common.js index 1d79990d..ed62c693 100644 --- a/app/src/components/Common.js +++ b/app/src/components/Common.js @@ -175,9 +175,6 @@ export function isoDatetimeToString(dt, time = false) { } export function CardLink({to, newTab = false, ...props}) { - const {t} = useContext(ThemeContext); - props = {...props, ...t}; - props = newTab === true ? {...props, target: '_blank', rel: 'noopener noreferrer'} : props; return {props.children} @@ -185,13 +182,11 @@ export function CardLink({to, newTab = false, ...props}) { } export function ExternalCardLink({to, children, ...props}) { - const {t} = useContext(ThemeContext); return {children} @@ -199,10 +194,9 @@ export function ExternalCardLink({to, children, ...props}) { } export function PreviewLink({file, children, className, ...props}) { - const {t} = useContext(ThemeContext); const {setPreviewFile} = React.useContext(FilePreviewContext); className = className ? `clickable ${className}` : `clickable `; - return setPreviewFile(file)} {...props} {...t}> + return setPreviewFile(file)} {...props}> {children} } diff --git a/app/src/components/Theme.test.js b/app/src/components/Theme.test.js deleted file mode 100644 index 8389e509..00000000 --- a/app/src/components/Theme.test.js +++ /dev/null @@ -1,109 +0,0 @@ -import React from 'react'; -import {Button, Modal, Progress} from './Theme'; -import {renderInDarkMode, renderInLightMode} from '../test-utils'; - -// All semantic-ui colors. -const allColors = ['red', 'orange', 'yellow', 'olive', 'green', 'teal', 'blue', 'violet', 'purple', 'pink', 'brown', 'grey']; - -// Light bar colors that always need dark percent text (light bars in any theme). -const lightBarColors = ['yellow', 'olive', 'teal']; -// Bar colors that need dark percent text only in dark mode (inverted makes bar lighter). -const darkModeLightBarColors = ['grey', 'blue']; - -describe('Button ref forwarding', () => { - // Regression: SButton is a class component, so forwarding a ref straight to it resolves to the class instance - // (no `.contains`). Semantic UI's Modal/Popup Portal calls `node.contains(...)` on the trigger's ref during - // handleDocumentClick, throwing "node.contains is not a function". The forwarded ref must be the DOM - - - - ); -}; - -// ---------------------------------------------------------------------------- -// Form Compound Component -// ---------------------------------------------------------------------------- - -type FormFieldComponent = React.FC; -type FormGroupComponent = React.FC; -type FormInputComponent = React.FC; - -interface FormComponent extends React.FC { - Field: FormFieldComponent; - Group: FormGroupComponent; - Input: FormInputComponent; -} - -const FormField: FormFieldComponent = (props) => { - const {i} = useContext(ThemeContext); - const mergedProps = invertedNull({...i, ...props}); - return -}; - -const FormGroup: FormGroupComponent = (props) => { - const {i} = useContext(ThemeContext); - const mergedProps = invertedNull({...i, ...props}); - return -}; - -const FormInput: FormInputComponent = (props) => { - const {i} = useContext(ThemeContext); - return -}; - -const FormBase: React.FC = (props) => { - const {i} = useContext(ThemeContext); - return -}; - -export const Form: FormComponent = Object.assign(FormBase, { - Field: FormField, - Group: FormGroup, - Input: FormInput, -}); - -// ---------------------------------------------------------------------------- -// Card Compound Component -// ---------------------------------------------------------------------------- - -interface CardIconProps extends HTMLAttributes { - onClick?: () => void; - children?: React.ReactNode; -} - -type CardIconComponent = React.FC; - -interface CardProps extends Omit { - color?: string; -} - -interface CardComponent extends React.FC { - Icon: CardIconComponent; -} - -const CardIcon: CardIconComponent = ({onClick, children, ...props}) => { - const {inverted} = useContext(ThemeContext); - const cardIcon =
{children}
; - if (onClick) { - return
- {cardIcon} -
- } else { - return cardIcon; - } -}; - -const CardBase: React.FC = ({color, ...props}) => { - const {inverted} = useContext(ThemeContext); - - const style: CSSProperties = props.style || {}; - const emphasisColor = ColorToSemanticHexColor(color); - if (emphasisColor) { - // Increase drop shadow to emphasize color. - const borderColor = inverted ? '#888' : '#ddd'; - style.boxShadow = `0 0 0 2px ${borderColor}, 0 5px 0 0 ${emphasisColor}, 0 0px 3px 0 #d4d4d5`; - } - return -}; - -export const Card: CardComponent = Object.assign(CardBase, { - Icon: CardIcon, -}); - -// ---------------------------------------------------------------------------- -// Tab Compound Component -// ---------------------------------------------------------------------------- - -type TabPaneComponent = React.FC; - -interface TabComponent extends React.FC { - Pane: TabPaneComponent; -} - -const TabPane: TabPaneComponent = (props) => { - const {i} = useContext(ThemeContext); - return -}; - -const TabBase: React.FC = (props) => { - const {i} = useContext(ThemeContext); - const mergedProps = invertedNull({...i, ...props}); - return -}; - -export const Tab: TabComponent = Object.assign(TabBase, { - Pane: TabPane, -}); - -// ---------------------------------------------------------------------------- -// Statistic Compound Component -// ---------------------------------------------------------------------------- - -type StatisticGroupComponent = React.FC; - -interface StatisticComponent extends React.FC { - Group: StatisticGroupComponent; -} - -const StatisticGroup: StatisticGroupComponent = (props) => { - const {i} = useContext(ThemeContext); - const style: CSSProperties = {...props.style, marginLeft: 0, marginRight: 0}; - return -}; - -const StatisticBase: React.FC = (props) => { - const {i, inverted} = useContext(ThemeContext); - const mergedProps = defaultGrey({...i, ...props}, !!inverted); - return -}; - -export const Statistic: StatisticComponent = Object.assign(StatisticBase, { - Group: StatisticGroup, -}); - -// ---------------------------------------------------------------------------- -// Breadcrumb Compound Component -// ---------------------------------------------------------------------------- - -interface BreadcrumbDividerProps { - icon?: string; -} - -type BreadcrumbDividerComponent = React.FC; - -interface BreadcrumbComponent extends React.FC { - Divider: BreadcrumbDividerComponent; -} - -const BreadcrumbDivider: BreadcrumbDividerComponent = ({icon, ...props}) => { - const {inverted} = useContext(ThemeContext); - - // TODO this only handles icons for now. - const className = `divider icon ${inverted} ${icon || ''}`; - return -}; - -const BreadcrumbBase: React.FC = (props) => { - const {t} = useContext(ThemeContext); - return -}; - -export const Breadcrumb: BreadcrumbComponent = Object.assign(BreadcrumbBase, { - Divider: BreadcrumbDivider, -}); - -// ---------------------------------------------------------------------------- -// Table Compound Component -// ---------------------------------------------------------------------------- - -type TableBodyComponent = React.FC; -type TableCellComponent = React.FC; -type TableFooterComponent = React.FC; -type TableHeaderComponent = React.FC; -type TableHeaderCellComponent = React.FC; -type TableRowComponent = React.FC; - -interface TableComponent extends React.FC { - Body: TableBodyComponent; - Cell: TableCellComponent; - Footer: TableFooterComponent; - Header: TableHeaderComponent; - HeaderCell: TableHeaderCellComponent; - Row: TableRowComponent; -} - -const TableBody: TableBodyComponent = (props) => { - return -}; - -const TableCell: TableCellComponent = (props) => { - return -}; - -const TableFooter: TableFooterComponent = (props) => { - return -}; - -const TableHeader: TableHeaderComponent = (props) => { - return -}; - -const TableHeaderCell: TableHeaderCellComponent = (props) => { - return -}; - -const TableRow: TableRowComponent = (props) => { - return -}; - -const TableBase: React.FC = (props) => { - const {i} = useContext(ThemeContext); - return -}; - -export const Table: TableComponent = Object.assign(TableBase, { - Body: TableBody, - Cell: TableCell, - Footer: TableFooter, - Header: TableHeader, - HeaderCell: TableHeaderCell, - Row: TableRow, -}); +/* + * The Semantic UI wrappers that used to live below this line are gone. + * + * They existed so call sites could get a themed Button, Segment, Modal and so on while + * the app still rendered Semantic components; every one of those call sites now imports + * from `src/components/ui` instead. The dark-mode compatibility props they consumed -- + * `i`, `s`, `t`, `inverted` -- went with them: those carried hardcoded greys, which is + * precisely what the token themes replaced. + * + * What remains is the part that was never about Semantic: the provider that resolves the + * chosen theme, stamps `data-theme` and `data-media-filter` on , persists both, and + * hands Mantine our tokens. + */ diff --git a/app/src/components/collections/CollectionEditForm.js b/app/src/components/collections/CollectionEditForm.js index f0354cee..f9c4a078 100644 --- a/app/src/components/collections/CollectionEditForm.js +++ b/app/src/components/collections/CollectionEditForm.js @@ -47,50 +47,39 @@ export function CollectionEditForm({
{/* - * `ui stackable grid` replicates Semantic's own Grid CSS classes (still loaded - * globally via semantic-ui-offline while other call sites remain unmigrated). - * Callers of this component (Channels.js, Archive.js, Playlists.js) still pass - * Semantic / children -- they have not migrated to the - * component library yet. This div keeps their layout without importing - * semantic-ui-react here. Replace with the real `Grid`/`Grid.Col` once every - * caller has migrated. - */} + Every caller now passes library components rather than Semantic Grid rows, so + this is a plain flex column instead of the `ui stackable grid` bridge it used + to be. That bridge was the last thing in the app depending on Semantic's + stylesheet. + */}
{children} - {appliedTagName &&
-
- -
-
} + {appliedTagName &&
} -
-
- {actionButtons} - {onCancel && } -
-
- -
+
+ {actionButtons} + {onCancel && } +
diff --git a/app/src/components/ui/ui.css b/app/src/components/ui/ui.css index fbd75c1c..464da8c8 100644 --- a/app/src/components/ui/ui.css +++ b/app/src/components/ui/ui.css @@ -342,6 +342,54 @@ html[data-theme="amber"] .wrolpi-searchbox-control:focus-within { border-top: 1px solid var(--border); } +/* A stack of form rows, replacing Semantic's `ui stackable grid` in edit forms. */ +.wrolpi-form-rows { + display: flex; + flex-direction: column; + gap: 14px; +} + +/* ------------------------------------------------------------- Form fields */ +/* + * `hooks/useForm.js` renders its own - - - +
+ +
{messageElm} } @@ -463,31 +463,36 @@ export function UrlsTextarea({name = 'urls', required, form}) { }; return ( - -