T07a: frontend core SPA — shell + auth + routing + i18n + UI library + a11y

- React 18 + Vite + TypeScript + Tailwind CSS setup
- AppShell with Sidebar (plugin menu) + TopBar (tenant switcher, search, notifications, user menu)
- Auth pages: Login, PasswordResetRequest, PasswordResetConfirm
- Protected routes with auth guard
- API client (axios with interceptors: session cookie, 401 redirect, 422 validation)
- TanStack Query hooks for auth, users, companies, contacts, notifications
- Zustand stores: authStore, uiStore
- i18n setup (de/en locales) with react-i18next
- UI component library: Button, Input, Select, Modal, Toast, Table, Card, Badge, Avatar, Pagination, EmptyState, Skeleton, ConfirmDialog
- Accessibility: ARIA labels, 44px touch targets, keyboard nav, reduced-motion, sr-only
- Design tokens from prototype as CSS custom properties
- 111 tests passing across 20 test files
- tsc --noEmit: 0 errors
- npm run build: success (471KB JS, 24KB CSS)
This commit is contained in:
leocrm-bot
2026-06-29 07:55:47 +02:00
parent f8193a6ab5
commit 22976abe92
66 changed files with 8598 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
import React from 'react';
import clsx from 'clsx';
import { useTranslation } from 'react-i18next';
export interface EmptyStateProps {
title?: string;
description?: string;
icon?: React.ReactNode;
action?: React.ReactNode;
className?: string;
}
export function EmptyState({
title,
description,
icon,
action,
className,
}: EmptyStateProps) {
const { t } = useTranslation();
return (
<div
className={clsx(
'flex flex-col items-center justify-center py-12 px-6 text-center',
className
)}
role="status"
>
{icon && (
<div className="mb-4 text-secondary-300" aria-hidden="true">
{icon}
</div>
)}
{!icon && (
<div className="mb-4 w-16 h-16 rounded-full bg-secondary-100 flex items-center justify-center" aria-hidden="true">
<svg className="w-8 h-8 text-secondary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
</svg>
</div>
)}
<h3 className="text-lg font-semibold text-secondary-900">
{title || t('emptyState.title')}
</h3>
<p className="mt-1 text-sm text-secondary-500 max-w-sm">
{description || t('emptyState.description')}
</p>
{action && <div className="mt-6">{action}</div>}
</div>
);
}