diff --git a/src/design-system/primitives/Avatar/Avatar.module.css b/src/design-system/primitives/Avatar/Avatar.module.css
new file mode 100644
index 0000000..8e10927
--- /dev/null
+++ b/src/design-system/primitives/Avatar/Avatar.module.css
@@ -0,0 +1,12 @@
+.avatar {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ font-weight: 600;
+ border: 1px solid;
+ flex-shrink: 0;
+}
+.sm { width: 24px; height: 24px; font-size: 9px; }
+.md { width: 28px; height: 28px; font-size: 11px; }
+.lg { width: 40px; height: 40px; font-size: 14px; }
diff --git a/src/design-system/primitives/Avatar/Avatar.tsx b/src/design-system/primitives/Avatar/Avatar.tsx
new file mode 100644
index 0000000..109ad8e
--- /dev/null
+++ b/src/design-system/primitives/Avatar/Avatar.tsx
@@ -0,0 +1,29 @@
+import styles from './Avatar.module.css'
+import { hashColor } from '../../utils/hashColor'
+import { useTheme } from '../../providers/ThemeProvider'
+
+interface AvatarProps {
+ name: string
+ size?: 'sm' | 'md' | 'lg'
+ className?: string
+}
+
+function getInitials(name: string): string {
+ const parts = name.split(/[\s-]+/).filter(Boolean)
+ if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase()
+ return name.slice(0, 2).toUpperCase()
+}
+
+export function Avatar({ name, size = 'md', className }: AvatarProps) {
+ const { theme } = useTheme()
+ const colors = hashColor(name, theme)
+ return (
+
+ {getInitials(name)}
+
+ )
+}
diff --git a/src/design-system/primitives/Badge/Badge.module.css b/src/design-system/primitives/Badge/Badge.module.css
new file mode 100644
index 0000000..baf9777
--- /dev/null
+++ b/src/design-system/primitives/Badge/Badge.module.css
@@ -0,0 +1,43 @@
+.badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 1px 8px;
+ border-radius: 10px;
+ font-family: var(--font-mono);
+ font-size: 10px;
+ font-weight: 600;
+ white-space: nowrap;
+ border: 1px solid transparent;
+ text-transform: uppercase;
+ letter-spacing: 0.3px;
+}
+
+.filled { /* default — inline styles from hashColor or semantic class */ }
+
+.outlined {
+ background: transparent !important;
+}
+
+.dashed {
+ background: transparent !important;
+ border-style: dashed;
+}
+
+.primary { background: var(--amber-bg); color: var(--amber-deep); border-color: var(--amber-light); }
+.success { background: var(--success-bg); color: var(--success); border-color: var(--success-border); }
+.warning { background: var(--warning-bg); color: var(--warning); border-color: var(--warning-border); }
+.error { background: var(--error-bg); color: var(--error); border-color: var(--error-border); }
+.running { background: var(--running-bg); color: var(--running); border-color: var(--running-border); }
+
+.remove {
+ background: none;
+ border: none;
+ color: inherit;
+ cursor: pointer;
+ font-size: 12px;
+ line-height: 1;
+ opacity: 0.5;
+ padding: 0;
+}
+.remove:hover { opacity: 1; }
diff --git a/src/design-system/primitives/Badge/Badge.test.tsx b/src/design-system/primitives/Badge/Badge.test.tsx
new file mode 100644
index 0000000..6c02041
--- /dev/null
+++ b/src/design-system/primitives/Badge/Badge.test.tsx
@@ -0,0 +1,43 @@
+import { describe, it, expect } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { ThemeProvider } from '../../providers/ThemeProvider'
+import { Badge } from './Badge'
+
+describe('Badge', () => {
+ it('renders label text', () => {
+ render(
+
+
+ ,
+ )
+ expect(screen.getByText('VIEWER')).toBeInTheDocument()
+ })
+
+ it('applies inline hash color when color is auto', () => {
+ const { container } = render(
+
+
+ ,
+ )
+ const badge = container.firstChild as HTMLElement
+ expect(badge.style.backgroundColor).toBeTruthy()
+ })
+
+ it('applies semantic color class when specified', () => {
+ const { container } = render(
+
+
+ ,
+ )
+ expect(container.firstChild).toHaveClass('error')
+ })
+
+ it('applies dashed variant class', () => {
+ const { container } = render(
+
+
+ ,
+ )
+ expect(container.firstChild).toHaveClass('dashed')
+ })
+})
diff --git a/src/design-system/primitives/Badge/Badge.tsx b/src/design-system/primitives/Badge/Badge.tsx
new file mode 100644
index 0000000..335b784
--- /dev/null
+++ b/src/design-system/primitives/Badge/Badge.tsx
@@ -0,0 +1,52 @@
+import styles from './Badge.module.css'
+import { hashColor } from '../../utils/hashColor'
+import { useTheme } from '../../providers/ThemeProvider'
+
+type BadgeColor = 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto'
+type BadgeVariant = 'filled' | 'outlined' | 'dashed'
+
+interface BadgeProps {
+ label: string
+ variant?: BadgeVariant
+ color?: BadgeColor
+ onRemove?: () => void
+ className?: string
+}
+
+export function Badge({
+ label,
+ variant = 'filled',
+ color = 'auto',
+ onRemove,
+ className,
+}: BadgeProps) {
+ const { theme } = useTheme()
+ const isAuto = color === 'auto'
+ const hashColors = isAuto ? hashColor(label, theme) : null
+
+ const inlineStyle = isAuto
+ ? {
+ backgroundColor: variant === 'filled' ? hashColors!.bg : 'transparent',
+ color: hashColors!.text,
+ borderColor: hashColors!.border,
+ }
+ : undefined
+
+ const classes = [
+ styles.badge,
+ styles[variant],
+ !isAuto ? styles[color] : '',
+ className ?? '',
+ ].filter(Boolean).join(' ')
+
+ return (
+
+ {label}
+ {onRemove && (
+
+ )}
+
+ )
+}
diff --git a/src/design-system/primitives/Tag/Tag.module.css b/src/design-system/primitives/Tag/Tag.module.css
new file mode 100644
index 0000000..9406933
--- /dev/null
+++ b/src/design-system/primitives/Tag/Tag.module.css
@@ -0,0 +1,28 @@
+.tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 3px 10px;
+ border: 1px solid;
+ border-radius: 20px;
+ font-size: 11px;
+ font-family: var(--font-mono);
+}
+
+.primary { background: var(--amber-bg); color: var(--amber-deep); border-color: var(--amber-light); }
+.success { background: var(--success-bg); color: var(--success); border-color: var(--success-border); }
+.warning { background: var(--warning-bg); color: var(--warning); border-color: var(--warning-border); }
+.error { background: var(--error-bg); color: var(--error); border-color: var(--error-border); }
+.running { background: var(--running-bg); color: var(--running); border-color: var(--running-border); }
+
+.remove {
+ cursor: pointer;
+ opacity: 0.5;
+ font-size: 13px;
+ line-height: 1;
+ background: none;
+ border: none;
+ color: inherit;
+ padding: 0;
+}
+.remove:hover { opacity: 1; }
diff --git a/src/design-system/primitives/Tag/Tag.tsx b/src/design-system/primitives/Tag/Tag.tsx
new file mode 100644
index 0000000..2b8bdcc
--- /dev/null
+++ b/src/design-system/primitives/Tag/Tag.tsx
@@ -0,0 +1,35 @@
+import styles from './Tag.module.css'
+import { hashColor } from '../../utils/hashColor'
+import { useTheme } from '../../providers/ThemeProvider'
+
+type TagColor = 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto'
+
+interface TagProps {
+ label: string
+ onRemove?: () => void
+ color?: TagColor
+ className?: string
+}
+
+export function Tag({ label, onRemove, color = 'auto', className }: TagProps) {
+ const { theme } = useTheme()
+ const isAuto = color === 'auto'
+ const hashColors = isAuto ? hashColor(label, theme) : null
+
+ const inlineStyle = isAuto
+ ? { backgroundColor: hashColors!.bg, color: hashColors!.text, borderColor: hashColors!.border }
+ : undefined
+
+ const classes = [styles.tag, !isAuto ? styles[color] : '', className ?? ''].filter(Boolean).join(' ')
+
+ return (
+
+ {label}
+ {onRemove && (
+
+ )}
+
+ )
+}