700b7a71ad
- 11 new feature pages (CompaniesList/Detail/Form, ContactsList/Detail/Form, SettingsProfile/Roles/Users, AuditLog, GlobalSearchResults) - 3 page updates (Dashboard with StatCard+ActivityFeed, Settings with tree nav+Outlet, TopBar with SearchDropdown) - 13 new routes in routes/index.tsx - i18n updates (de.json + en.json) with companies/contacts/settings/audit/search keys - 12 new test files + 2 existing test fixes (TopBar, AppShell) - 7 shared components (DataGrid, Tabs, SearchDropdown, CsvImportDialog, StatCard, ActivityFeed, UnsavedChangesGuard) - 16 new API hooks in hooks.ts - Verification: 141 tests pass, build succeeds, tsc --noEmit clean
63 lines
2.0 KiB
TypeScript
63 lines
2.0 KiB
TypeScript
import React, { useState } from 'react';
|
|
import clsx from 'clsx';
|
|
|
|
export interface TabItem {
|
|
key: string;
|
|
label: string;
|
|
content: React.ReactNode;
|
|
badge?: number;
|
|
}
|
|
|
|
export interface TabsProps {
|
|
tabs: TabItem[];
|
|
defaultKey?: string;
|
|
className?: string;
|
|
}
|
|
|
|
export function Tabs({ tabs, defaultKey, className }: TabsProps) {
|
|
const [activeKey, setActiveKey] = useState(defaultKey || tabs[0]?.key || '');
|
|
const activeTab = tabs.find((t) => t.key === activeKey);
|
|
|
|
return (
|
|
<div className={clsx('w-full', className)}>
|
|
<div className="border-b border-secondary-200" role="tablist">
|
|
<div className="flex gap-1 px-6 overflow-x-auto">
|
|
{tabs.map((tab) => (
|
|
<button
|
|
key={tab.key}
|
|
role="tab"
|
|
aria-selected={activeKey === tab.key}
|
|
aria-controls={`panel-${tab.key}`}
|
|
id={`tab-${tab.key}`}
|
|
tabIndex={activeKey === tab.key ? 0 : -1}
|
|
onClick={() => setActiveKey(tab.key)}
|
|
className={clsx(
|
|
'px-4 py-3 text-sm font-medium border-b-2 min-h-touch whitespace-nowrap',
|
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 rounded-t-md',
|
|
activeKey === tab.key
|
|
? 'border-primary-600 text-primary-700'
|
|
: 'border-transparent text-secondary-600 hover:text-secondary-900 hover:border-secondary-300'
|
|
)}
|
|
>
|
|
{tab.label}
|
|
{tab.badge !== undefined && tab.badge > 0 && (
|
|
<span className="ml-2 inline-flex items-center justify-center px-2 py-0.5 rounded-full text-xs bg-primary-100 text-primary-700">
|
|
{tab.badge}
|
|
</span>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div
|
|
id={`panel-${activeKey}`}
|
|
role="tabpanel"
|
|
aria-labelledby={`tab-${activeKey}`}
|
|
className="px-6 py-4"
|
|
>
|
|
{activeTab?.content}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|