From a65fbfd53203213fb0ae475b6397cb197067834f Mon Sep 17 00:00:00 2001 From: Antti Kettunen Date: Sun, 24 May 2026 15:43:41 +0300 Subject: [PATCH] refactor(webui): move badge into primitives - move the shared badge primitive and styling out of the form component module - keep primary-button badge styling working via a data-slot hook instead of a form-local class - update the import pages and primitive tests to consume the new home for Badge --- webui/src/components/form/form.module.css | 44 +------------------ webui/src/components/form/form.test.tsx | 4 -- webui/src/components/form/form.tsx | 16 ------- webui/src/components/form/index.ts | 1 - .../primitives/primitives.module.css | 42 ++++++++++++++++++ .../components/primitives/primitives.test.tsx | 17 ++++++- .../src/components/primitives/primitives.tsx | 24 ++++++++++ .../src/routes/import/-ui/auto-import-tab.tsx | 2 +- .../routes/import/-ui/singles-import-tab.tsx | 4 +- 9 files changed, 86 insertions(+), 68 deletions(-) diff --git a/webui/src/components/form/form.module.css b/webui/src/components/form/form.module.css index ae8998b0..31eff1a8 100644 --- a/webui/src/components/form/form.module.css +++ b/webui/src/components/form/form.module.css @@ -449,48 +449,6 @@ } } -.badge { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 2ch; - padding: 1px 7px; - border: 1px solid transparent; - border-radius: 999px; - font-size: 0.74rem; - font-weight: 700; - line-height: 1.2; - white-space: nowrap; - vertical-align: middle; - color: rgba(255, 255, 255, 0.45); - background: rgba(255, 255, 255, 0.05); - border-color: rgba(255, 255, 255, 0.08); - - &[data-tone='info'] { - color: rgb(var(--accent-light-rgb)); - background: rgba(var(--accent-rgb), 0.12); - border-color: rgba(var(--accent-light-rgb), 0.12); - } - - &[data-tone='success'] { - color: #4ade80; - background: rgba(74, 222, 128, 0.12); - border-color: rgba(74, 222, 128, 0.12); - } - - &[data-tone='warning'] { - color: #fbbf24; - background: rgba(251, 191, 36, 0.12); - border-color: rgba(251, 191, 36, 0.12); - } - - &[data-tone='danger'] { - color: #f87171; - background: rgba(248, 113, 113, 0.12); - border-color: rgba(248, 113, 113, 0.12); - } -} - .button { display: inline-flex; align-items: center; @@ -576,7 +534,7 @@ color: rgba(0, 0, 0, 0.55); } - &[data-variant='primary'] .badge { + &[data-variant='primary'] [data-slot='badge'] { color: #fff; background: rgba(0, 0, 0, 0.18); border-color: rgba(0, 0, 0, 0.22); diff --git a/webui/src/components/form/form.test.tsx b/webui/src/components/form/form.test.tsx index 21e6abb3..77b7677b 100644 --- a/webui/src/components/form/form.test.tsx +++ b/webui/src/components/form/form.test.tsx @@ -3,7 +3,6 @@ import { useState } from 'react'; import { describe, expect, it } from 'vitest'; import { - Badge, Button, Checkbox, FormActions, @@ -115,8 +114,6 @@ function FormDemo() { /> - 12 - @@ -170,7 +167,6 @@ describe('form primitives', () => { fireEvent.click(highPriority); expect(highPriority).toHaveAttribute('aria-pressed', 'true'); - expect(screen.getByText('12')).toHaveAttribute('data-tone', 'warning'); expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Save' })).toHaveAttribute('data-variant', 'primary'); }); diff --git a/webui/src/components/form/form.tsx b/webui/src/components/form/form.tsx index 06cf217d..9e894d43 100644 --- a/webui/src/components/form/form.tsx +++ b/webui/src/components/form/form.tsx @@ -283,22 +283,6 @@ export const OptionButton = forwardRef(fun ); }); -type BaseBadgeProps = ComponentPropsWithoutRef<'span'>; - -export type BadgeTone = 'neutral' | 'info' | 'success' | 'warning' | 'danger'; - -export type BadgeProps = Omit & { - className?: string; - tone?: BadgeTone; -}; - -export const Badge = forwardRef(function Badge( - { className, tone = 'neutral', ...props }, - ref, -) { - return ; -}); - type BaseButtonProps = ComponentPropsWithoutRef; export type ButtonProps = Omit & { diff --git a/webui/src/components/form/index.ts b/webui/src/components/form/index.ts index 0f6ffb0d..f9a268a6 100644 --- a/webui/src/components/form/index.ts +++ b/webui/src/components/form/index.ts @@ -1,6 +1,5 @@ export { Button, - Badge, FormActions, FormError, FormField, diff --git a/webui/src/components/primitives/primitives.module.css b/webui/src/components/primitives/primitives.module.css index 23914559..f585d5e8 100644 --- a/webui/src/components/primitives/primitives.module.css +++ b/webui/src/components/primitives/primitives.module.css @@ -43,3 +43,45 @@ background: rgba(255, 120, 120, 0.08); color: rgba(255, 214, 214, 0.94); } + +.badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2ch; + padding: 1px 7px; + border: 1px solid transparent; + border-radius: 999px; + font-size: 0.74rem; + font-weight: 700; + line-height: 1.2; + white-space: nowrap; + vertical-align: middle; + color: rgba(255, 255, 255, 0.45); + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.08); +} + +.badge[data-tone='info'] { + color: rgb(var(--accent-light-rgb)); + background: rgba(var(--accent-rgb), 0.12); + border-color: rgba(var(--accent-light-rgb), 0.12); +} + +.badge[data-tone='success'] { + color: #4ade80; + background: rgba(74, 222, 128, 0.12); + border-color: rgba(74, 222, 128, 0.12); +} + +.badge[data-tone='warning'] { + color: #fbbf24; + background: rgba(251, 191, 36, 0.12); + border-color: rgba(251, 191, 36, 0.12); +} + +.badge[data-tone='danger'] { + color: #f87171; + background: rgba(248, 113, 113, 0.12); + border-color: rgba(248, 113, 113, 0.12); +} diff --git a/webui/src/components/primitives/primitives.test.tsx b/webui/src/components/primitives/primitives.test.tsx index add08aeb..9f5d6e2c 100644 --- a/webui/src/components/primitives/primitives.test.tsx +++ b/webui/src/components/primitives/primitives.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; -import { Notice, Show } from './primitives'; +import { Badge, Notice, Show } from './primitives'; describe('Show', () => { it('renders children when the condition is true', () => { @@ -50,3 +50,18 @@ describe('Notice', () => { expect(screen.getByRole('note')).toHaveAttribute('data-tone', 'warning'); }); }); + +describe('Badge', () => { + it('renders with neutral styling by default', () => { + render(12); + + expect(screen.getByText('12')).toHaveAttribute('data-slot', 'badge'); + expect(screen.getByText('12')).toHaveAttribute('data-tone', 'neutral'); + }); + + it('supports tone overrides', () => { + render(12); + + expect(screen.getByText('12')).toHaveAttribute('data-tone', 'warning'); + }); +}); diff --git a/webui/src/components/primitives/primitives.tsx b/webui/src/components/primitives/primitives.tsx index 1fec52b9..8972632e 100644 --- a/webui/src/components/primitives/primitives.tsx +++ b/webui/src/components/primitives/primitives.tsx @@ -23,6 +23,30 @@ export function Show({ fallback = null, children, when }: ShowProps) { return <>{children}; } +type BaseBadgeProps = ComponentPropsWithoutRef<'span'>; + +export type BadgeTone = 'neutral' | 'info' | 'success' | 'warning' | 'danger'; + +export type BadgeProps = Omit & { + className?: string; + tone?: BadgeTone; +}; + +export const Badge = forwardRef(function Badge( + { className, tone = 'neutral', ...props }, + ref, +) { + return ( + + ); +}); + export type NoticeTone = 'neutral' | 'info' | 'success' | 'warning' | 'danger'; export type NoticeProps = Omit, 'className'> & { diff --git a/webui/src/routes/import/-ui/auto-import-tab.tsx b/webui/src/routes/import/-ui/auto-import-tab.tsx index c1576442..761a08df 100644 --- a/webui/src/routes/import/-ui/auto-import-tab.tsx +++ b/webui/src/routes/import/-ui/auto-import-tab.tsx @@ -3,7 +3,6 @@ import clsx from 'clsx'; import { useEffect, useState } from 'react'; import { - Badge, Button, OptionButton, OptionButtonGroup, @@ -11,6 +10,7 @@ import { Select, Switch, } from '@/components/form/form'; +import { Badge } from '@/components/primitives'; import type { ImportAutoFilter, diff --git a/webui/src/routes/import/-ui/singles-import-tab.tsx b/webui/src/routes/import/-ui/singles-import-tab.tsx index a636bdee..7aac6728 100644 --- a/webui/src/routes/import/-ui/singles-import-tab.tsx +++ b/webui/src/routes/import/-ui/singles-import-tab.tsx @@ -1,8 +1,8 @@ import clsx from 'clsx'; import { useEffect } from 'react'; -import { Badge, Button, Checkbox, TextInput } from '@/components/form/form'; -import { Notice } from '@/components/primitives'; +import { Button, Checkbox, TextInput } from '@/components/form/form'; +import { Badge, Notice } from '@/components/primitives'; import type { SingleSearchState } from '../-import.store'; import type { ImportTrackResult } from '../-import.types';