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
+}