diff --git a/dev.py b/dev.py index a087b7bc..95ca24bd 100755 --- a/dev.py +++ b/dev.py @@ -318,7 +318,7 @@ def watch_and_run_backend() -> None: def main() -> int: if not (ROOT_DIR / 'webui' / 'node_modules').is_dir(): print('webui/node_modules is missing.') - print('Run: cd webui && npm install') + print('Run: cd webui && npm ci') return 1 vite_proc = start_vite() diff --git a/webui/package-lock.json b/webui/package-lock.json index 59e87641..3d5aaf67 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -6,9 +6,11 @@ "": { "name": "soulsync-webui", "dependencies": { + "@base-ui/react": "^1.4.1", "@tanstack/react-form": "^1.29.1", "@tanstack/react-query": "^5.100.5", "@tanstack/react-router": "^1.168.24", + "clsx": "^2.1.1", "ky": "^2.0.2", "react": "^19.2.5", "react-dom": "^19.2.5" @@ -338,7 +340,6 @@ "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -392,6 +393,66 @@ "node": ">=6.9.0" } }, + "node_modules/@base-ui/react": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@base-ui/react/-/react-1.4.1.tgz", + "integrity": "sha512-Ab5/LIhcmL8BQcsBUYiOfkSDRdLpvgUBzMK30cu684JPcLclYlztharvCZyNNgzJtbAiREzI9q0pI5erHCMgCw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@base-ui/utils": "0.2.8", + "@floating-ui/react-dom": "^2.1.8", + "@floating-ui/utils": "^0.2.11", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@date-fns/tz": "^1.2.0", + "@types/react": "^17 || ^18 || ^19", + "date-fns": "^4.0.0", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@date-fns/tz": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "date-fns": { + "optional": true + } + } + }, + "node_modules/@base-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-jvOi+c+ftGlGotNcKnzPVg2IhCaDTB6/6R3JeqdjdXktuAJi3wKH9T7+svuaKh1mmfVU11UWzUZVH74JDfi/wQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@floating-ui/utils": "^0.2.11", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "@types/react": "^17 || ^18 || ^19", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -563,6 +624,44 @@ } } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, "node_modules/@inquirer/ansi": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz", @@ -2001,7 +2100,7 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2631,6 +2730,15 @@ "node": ">=12" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2703,7 +2811,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/data-urls": { @@ -3952,6 +4060,12 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/rettime": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.8.tgz", diff --git a/webui/package.json b/webui/package.json index 460486d8..ff82b9fe 100644 --- a/webui/package.json +++ b/webui/package.json @@ -12,9 +12,11 @@ "test:e2e": "playwright test" }, "dependencies": { + "@base-ui/react": "^1.4.1", "@tanstack/react-form": "^1.29.1", "@tanstack/react-query": "^5.100.5", "@tanstack/react-router": "^1.168.24", + "clsx": "^2.1.1", "ky": "^2.0.2", "react": "^19.2.5", "react-dom": "^19.2.5" diff --git a/webui/src/app/api-client.test.ts b/webui/src/app/api-client.test.ts index 5e2dbd1c..6e60ee2e 100644 --- a/webui/src/app/api-client.test.ts +++ b/webui/src/app/api-client.test.ts @@ -1,8 +1,7 @@ -import { describe, expect, it, vi } from 'vite-plus/test'; +import type { ResponsePromise } from 'ky'; import { HTTPError } from 'ky'; - -import type { ResponsePromise } from 'ky'; +import { describe, expect, it, vi } from 'vite-plus/test'; import { readJson } from './api-client'; @@ -46,9 +45,6 @@ describe('readJson', () => { const result = readJson(promise); await expect(result).rejects.toBe(error); - await expect(result).rejects.toHaveProperty( - 'message', - error.message, - ); + await expect(result).rejects.toHaveProperty('message', error.message); }); }); diff --git a/webui/src/components/form/form.tsx b/webui/src/components/form/form.tsx index 56e78726..15938435 100644 --- a/webui/src/components/form/form.tsx +++ b/webui/src/components/form/form.tsx @@ -1,7 +1,12 @@ +import { Button as BaseButton } from '@base-ui/react/button'; +import { Field } from '@base-ui/react/field'; +import { Input as BaseInput } from '@base-ui/react/input'; +import { Toggle as BaseToggle } from '@base-ui/react/toggle'; +import clsx from 'clsx'; import { forwardRef, + type ComponentPropsWithoutRef, type ButtonHTMLAttributes, - type InputHTMLAttributes, type SelectHTMLAttributes, type ReactNode, type TextareaHTMLAttributes, @@ -9,10 +14,6 @@ import { import styles from './form.module.css'; -function mergeClassNames(...classNames: Array) { - return classNames.filter(Boolean).join(' '); -} - export interface FormFieldProps { children: ReactNode; className?: string; @@ -31,30 +32,36 @@ export function FormField({ label, }: FormFieldProps) { return ( -
+
{htmlFor ? ( ) : ( -
{label}
+ {label} )} - {helperText ?
{helperText}
: null} + {helperText ? ( + {helperText} + ) : null}
{children}
{error ? : null} -
+ ); } -export type TextInputProps = InputHTMLAttributes; +type BaseInputProps = ComponentPropsWithoutRef; + +export type TextInputProps = Omit & { + className?: string; +}; export const TextInput = forwardRef(function TextInput( { className, ...props }, ref, ) { - return ; + return ; }); export type TextAreaProps = TextareaHTMLAttributes; @@ -63,7 +70,7 @@ export const TextArea = forwardRef(function { className, ...props }, ref, ) { - return