From 49789d6a15a3d4157f64e69cda3e6e3cc452563f Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:24:30 +0100 Subject: [PATCH] feat: StatusDot and Spinner primitives --- .../primitives/Spinner/Spinner.module.css | 7 ++++++ .../primitives/Spinner/Spinner.tsx | 20 +++++++++++++++ .../primitives/StatusDot/StatusDot.module.css | 15 +++++++++++ .../primitives/StatusDot/StatusDot.test.tsx | 25 +++++++++++++++++++ .../primitives/StatusDot/StatusDot.tsx | 21 ++++++++++++++++ 5 files changed, 88 insertions(+) create mode 100644 src/design-system/primitives/Spinner/Spinner.module.css create mode 100644 src/design-system/primitives/Spinner/Spinner.tsx create mode 100644 src/design-system/primitives/StatusDot/StatusDot.module.css create mode 100644 src/design-system/primitives/StatusDot/StatusDot.test.tsx create mode 100644 src/design-system/primitives/StatusDot/StatusDot.tsx diff --git a/src/design-system/primitives/Spinner/Spinner.module.css b/src/design-system/primitives/Spinner/Spinner.module.css new file mode 100644 index 0000000..4f4a01b --- /dev/null +++ b/src/design-system/primitives/Spinner/Spinner.module.css @@ -0,0 +1,7 @@ +.spinner { + display: inline-block; + border: 2px solid var(--border); + border-top-color: var(--amber); + border-radius: 50%; + animation: spin 0.6s linear infinite; +} diff --git a/src/design-system/primitives/Spinner/Spinner.tsx b/src/design-system/primitives/Spinner/Spinner.tsx new file mode 100644 index 0000000..f4e9699 --- /dev/null +++ b/src/design-system/primitives/Spinner/Spinner.tsx @@ -0,0 +1,20 @@ +import styles from './Spinner.module.css' + +interface SpinnerProps { + size?: 'sm' | 'md' | 'lg' + className?: string +} + +const sizes = { sm: 16, md: 24, lg: 32 } + +export function Spinner({ size = 'md', className }: SpinnerProps) { + const px = sizes[size] + return ( + + ) +} diff --git a/src/design-system/primitives/StatusDot/StatusDot.module.css b/src/design-system/primitives/StatusDot/StatusDot.module.css new file mode 100644 index 0000000..a016960 --- /dev/null +++ b/src/design-system/primitives/StatusDot/StatusDot.module.css @@ -0,0 +1,15 @@ +.dot { + display: inline-block; + width: 7px; + height: 7px; + border-radius: 50%; + flex-shrink: 0; +} + +.live, .success { background: var(--success); } +.stale, .warning { background: var(--warning); } +.dead { background: var(--text-muted); } +.error { background: var(--error); } +.running { background: var(--running); } + +.pulse { animation: pulse 2s ease-in-out infinite; } diff --git a/src/design-system/primitives/StatusDot/StatusDot.test.tsx b/src/design-system/primitives/StatusDot/StatusDot.test.tsx new file mode 100644 index 0000000..f4a89d0 --- /dev/null +++ b/src/design-system/primitives/StatusDot/StatusDot.test.tsx @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest' +import { render } from '@testing-library/react' +import { StatusDot } from './StatusDot' + +describe('StatusDot', () => { + it('renders a dot element', () => { + const { container } = render() + expect(container.firstChild).toBeInTheDocument() + }) + + it('applies variant class', () => { + const { container } = render() + expect(container.firstChild).toHaveClass('error') + }) + + it('applies pulse class for live variant by default', () => { + const { container } = render() + expect(container.firstChild).toHaveClass('pulse') + }) + + it('disables pulse when pulse=false', () => { + const { container } = render() + expect(container.firstChild).not.toHaveClass('pulse') + }) +}) diff --git a/src/design-system/primitives/StatusDot/StatusDot.tsx b/src/design-system/primitives/StatusDot/StatusDot.tsx new file mode 100644 index 0000000..153c571 --- /dev/null +++ b/src/design-system/primitives/StatusDot/StatusDot.tsx @@ -0,0 +1,21 @@ +import styles from './StatusDot.module.css' + +type StatusDotVariant = 'live' | 'stale' | 'dead' | 'success' | 'warning' | 'error' | 'running' + +interface StatusDotProps { + variant: StatusDotVariant + pulse?: boolean + className?: string +} + +export function StatusDot({ variant, pulse, className }: StatusDotProps) { + const showPulse = pulse ?? variant === 'live' + const classes = [ + styles.dot, + styles[variant], + showPulse ? styles.pulse : '', + className ?? '', + ].filter(Boolean).join(' ') + + return