From e9fc8c24e88b9edf195e866a524937f865032a88 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:42:03 +0100 Subject: [PATCH] feat: Breadcrumb and Tabs composites Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Breadcrumb/Breadcrumb.module.css | 35 +++++++++++++ .../composites/Breadcrumb/Breadcrumb.tsx | 37 +++++++++++++ .../composites/Tabs/Tabs.module.css | 52 +++++++++++++++++++ .../composites/Tabs/Tabs.test.tsx | 31 +++++++++++ src/design-system/composites/Tabs/Tabs.tsx | 36 +++++++++++++ 5 files changed, 191 insertions(+) create mode 100644 src/design-system/composites/Breadcrumb/Breadcrumb.module.css create mode 100644 src/design-system/composites/Breadcrumb/Breadcrumb.tsx create mode 100644 src/design-system/composites/Tabs/Tabs.module.css create mode 100644 src/design-system/composites/Tabs/Tabs.test.tsx create mode 100644 src/design-system/composites/Tabs/Tabs.tsx diff --git a/src/design-system/composites/Breadcrumb/Breadcrumb.module.css b/src/design-system/composites/Breadcrumb/Breadcrumb.module.css new file mode 100644 index 0000000..015836c --- /dev/null +++ b/src/design-system/composites/Breadcrumb/Breadcrumb.module.css @@ -0,0 +1,35 @@ +.list { + display: flex; + align-items: center; + gap: 6px; + list-style: none; + font-size: 13px; + color: var(--text-muted); + flex-wrap: wrap; +} + +.item { + display: flex; + align-items: center; + gap: 6px; +} + +.sep { + color: var(--text-faint); + font-size: 11px; +} + +.link { + color: var(--text-muted); + text-decoration: none; + transition: color 0.12s; +} + +.link:hover { + color: var(--text-secondary); +} + +.active { + color: var(--text-primary); + font-weight: 600; +} diff --git a/src/design-system/composites/Breadcrumb/Breadcrumb.tsx b/src/design-system/composites/Breadcrumb/Breadcrumb.tsx new file mode 100644 index 0000000..8f62ed0 --- /dev/null +++ b/src/design-system/composites/Breadcrumb/Breadcrumb.tsx @@ -0,0 +1,37 @@ +import styles from './Breadcrumb.module.css' + +interface BreadcrumbItem { + label: string + href?: string +} + +interface BreadcrumbProps { + items: BreadcrumbItem[] + className?: string +} + +export function Breadcrumb({ items, className }: BreadcrumbProps) { + return ( + + ) +} diff --git a/src/design-system/composites/Tabs/Tabs.module.css b/src/design-system/composites/Tabs/Tabs.module.css new file mode 100644 index 0000000..5859525 --- /dev/null +++ b/src/design-system/composites/Tabs/Tabs.module.css @@ -0,0 +1,52 @@ +.bar { + display: flex; + align-items: center; + border-bottom: 1px solid var(--border-subtle); + gap: 0; +} + +.tab { + display: flex; + align-items: center; + gap: 6px; + padding: 9px 16px; + font-size: 13px; + font-weight: 500; + font-family: var(--font-body); + color: var(--text-muted); + background: none; + border: none; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + cursor: pointer; + transition: color 0.15s, border-color 0.15s; + white-space: nowrap; +} + +.tab:hover { + color: var(--text-secondary); +} + +.tab.active { + color: var(--amber); + border-bottom-color: var(--amber); +} + +.label { + /* no special styles needed */ +} + +.count { + font-family: var(--font-mono); + font-size: 10px; + background: var(--bg-inset); + color: var(--text-muted); + padding: 1px 5px; + border-radius: 10px; + line-height: 1.4; +} + +.tab.active .count { + background: var(--amber-bg); + color: var(--amber); +} diff --git a/src/design-system/composites/Tabs/Tabs.test.tsx b/src/design-system/composites/Tabs/Tabs.test.tsx new file mode 100644 index 0000000..4c36b2f --- /dev/null +++ b/src/design-system/composites/Tabs/Tabs.test.tsx @@ -0,0 +1,31 @@ +import { describe, it, expect, vi } from 'vitest' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { Tabs } from './Tabs' + +describe('Tabs', () => { + const tabs = [ + { label: 'All', count: 14, value: 'all' }, + { label: 'Executions', count: 8, value: 'executions' }, + { label: 'Routes', count: 3, value: 'routes' }, + ] + + it('renders all tab labels', () => { + render( {}} />) + expect(screen.getByText('All')).toBeInTheDocument() + expect(screen.getByText('Executions')).toBeInTheDocument() + }) + + it('shows count badges', () => { + render( {}} />) + expect(screen.getByText('14')).toBeInTheDocument() + }) + + it('calls onChange with tab value', async () => { + const onChange = vi.fn() + const user = userEvent.setup() + render() + await user.click(screen.getByText('Routes')) + expect(onChange).toHaveBeenCalledWith('routes') + }) +}) diff --git a/src/design-system/composites/Tabs/Tabs.tsx b/src/design-system/composites/Tabs/Tabs.tsx new file mode 100644 index 0000000..284e8b7 --- /dev/null +++ b/src/design-system/composites/Tabs/Tabs.tsx @@ -0,0 +1,36 @@ +import styles from './Tabs.module.css' + +interface TabItem { + label: string + count?: number + value: string +} + +interface TabsProps { + tabs: TabItem[] + active: string + onChange: (value: string) => void + className?: string +} + +export function Tabs({ tabs, active, onChange, className }: TabsProps) { + return ( +
+ {tabs.map((tab) => ( + + ))} +
+ ) +}