From ae0711f4641747468f696764641dfb8cee90383a Mon Sep 17 00:00:00 2001 From: Antti Kettunen Date: Sat, 16 May 2026 21:01:07 +0300 Subject: [PATCH] refactor(webui): theme shared import controls - add a shared switch primitive for theme-aware toggle styling\n- keep import-page buttons leaning on shared variants instead of local color rules\n- simplify the singles and auto-import controls around the shared form layer --- webui/src/components/form/form.module.css | 56 +++++++++++++++++++ webui/src/components/form/form.test.tsx | 11 ++++ webui/src/components/form/form.tsx | 18 ++++++ .../src/routes/import/-ui/auto-import-tab.tsx | 17 +++--- .../routes/import/-ui/import-page.module.css | 37 +----------- .../routes/import/-ui/singles-import-tab.tsx | 3 +- 6 files changed, 96 insertions(+), 46 deletions(-) diff --git a/webui/src/components/form/form.module.css b/webui/src/components/form/form.module.css index 2da07111..1222e247 100644 --- a/webui/src/components/form/form.module.css +++ b/webui/src/components/form/form.module.css @@ -183,6 +183,62 @@ transform: translateY(-0.5px) scaleX(1.18); } +.switch { + position: relative; + display: inline-flex; + align-items: center; + width: 44px; + height: 24px; + flex-shrink: 0; + box-sizing: border-box; + margin: 0; + padding: 0 3px; + border: 1px solid rgba(255, 255, 255, 0.16); + border-radius: 999px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.04)), + rgba(255, 255, 255, 0.06); + cursor: pointer; + transition: + border-color 0.18s ease, + background 0.18s ease, + box-shadow 0.18s ease, + transform 0.18s ease; + color-scheme: dark; +} + +.switch[data-checked] { + border-color: rgba(var(--accent-light-rgb), 0.55); + background: + linear-gradient(180deg, rgba(var(--accent-light-rgb), 0.55), rgba(var(--accent-rgb), 0.8)), + rgba(var(--accent-rgb), 0.45); +} + +.switch[data-focused] { + outline: none; + box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.14); +} + +.switch[data-disabled] { + opacity: 0.55; + cursor: not-allowed; +} + +.switchThumb { + display: block; + width: 18px; + height: 18px; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); + transform: translateX(0); + transition: transform 0.18s ease; +} + +.switch[data-checked] .switchThumb { + transform: translateX(20px); +} + .rangeRoot { display: inline-flex; flex: 0 0 160px; diff --git a/webui/src/components/form/form.test.tsx b/webui/src/components/form/form.test.tsx index 21a2cfa3..cadef869 100644 --- a/webui/src/components/form/form.test.tsx +++ b/webui/src/components/form/form.test.tsx @@ -14,6 +14,7 @@ import { OptionCardGroup, RangeInput, Select, + Switch, TextArea, TextInput, } from './form'; @@ -25,6 +26,7 @@ function FormDemo() { const [priority, setPriority] = useState<'low' | 'normal' | 'high'>('normal'); const [status, setStatus] = useState('open'); const [archive, setArchive] = useState(false); + const [enabled, setEnabled] = useState(true); const [confidence, setConfidence] = useState(90); return ( @@ -98,6 +100,10 @@ function FormDemo() { + + + + { fireEvent.click(archiveCheckbox); expect(archiveCheckbox).toBeChecked(); + const enabledSwitch = screen.getByRole('switch', { name: 'Enabled' }); + expect(enabledSwitch).toBeChecked(); + fireEvent.click(enabledSwitch); + expect(enabledSwitch).not.toBeChecked(); + const confidenceSlider = screen.getByLabelText('Confidence', { selector: 'input' }); expect(confidenceSlider).toHaveValue('90'); fireEvent.change(confidenceSlider, { target: { value: '75' } }); diff --git a/webui/src/components/form/form.tsx b/webui/src/components/form/form.tsx index 6d6b04fa..ea6a2785 100644 --- a/webui/src/components/form/form.tsx +++ b/webui/src/components/form/form.tsx @@ -3,6 +3,7 @@ import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox'; import { Field } from '@base-ui/react/field'; import { Input as BaseInput } from '@base-ui/react/input'; import { Slider } from '@base-ui/react/slider'; +import { Switch as BaseSwitch } from '@base-ui/react/switch'; import { Toggle as BaseToggle } from '@base-ui/react/toggle'; import clsx from 'clsx'; import { @@ -106,6 +107,23 @@ export const Checkbox = forwardRef(function Checkbox ); }); +type BaseSwitchProps = ComponentPropsWithoutRef; + +export type SwitchProps = Omit & { + className?: string; +}; + +export const Switch = forwardRef(function Switch( + { className, ...props }, + ref, +) { + return ( + + + + ); +}); + export interface RangeInputProps { className?: string; disabled?: boolean; diff --git a/webui/src/routes/import/-ui/auto-import-tab.tsx b/webui/src/routes/import/-ui/auto-import-tab.tsx index 471c10f3..062173c8 100644 --- a/webui/src/routes/import/-ui/auto-import-tab.tsx +++ b/webui/src/routes/import/-ui/auto-import-tab.tsx @@ -1,7 +1,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; -import { Button, RangeInput, Select } from '@/components/form/form'; +import { Button, RangeInput, Select, Switch } from '@/components/form/form'; import type { ImportAutoFilter, @@ -155,17 +155,16 @@ export function AutoImportPanel({ <>
- + Auto-Import +
selected.has(getStagingFileKey(file))).length; const allSelected = files.length > 0 && selectedCount === files.length; + const processVariant = selectedCount > 0 ? 'primary' : 'default'; return ( <> @@ -181,7 +182,7 @@ export function SinglesImportPanel({