T07b: frontend feature pages — companies + contacts + settings + audit + dashboard + search
- 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
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user