feat: StatusDot and Spinner primitives

This commit is contained in:
hsiegeln
2026-03-18 09:24:30 +01:00
parent c24c2829b2
commit 49789d6a15
5 changed files with 88 additions and 0 deletions

View File

@@ -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;
}

View File

@@ -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 (
<span
className={`${styles.spinner} ${className ?? ''}`}
style={{ width: px, height: px }}
role="status"
aria-label="Loading"
/>
)
}

View File

@@ -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; }

View File

@@ -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(<StatusDot variant="success" />)
expect(container.firstChild).toBeInTheDocument()
})
it('applies variant class', () => {
const { container } = render(<StatusDot variant="error" />)
expect(container.firstChild).toHaveClass('error')
})
it('applies pulse class for live variant by default', () => {
const { container } = render(<StatusDot variant="live" />)
expect(container.firstChild).toHaveClass('pulse')
})
it('disables pulse when pulse=false', () => {
const { container } = render(<StatusDot variant="live" pulse={false} />)
expect(container.firstChild).not.toHaveClass('pulse')
})
})

View File

@@ -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 <span className={classes} aria-hidden="true" />
}