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:
@@ -0,0 +1,57 @@
|
||||
import React, { useId } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
helperText?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ label, error, helperText, required, className, id, ...props }, ref) => {
|
||||
const generatedId = useId();
|
||||
const inputId = id || generatedId;
|
||||
const errorId = `${inputId}-error`;
|
||||
const helperId = `${inputId}-helper`;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<label htmlFor={inputId} className="block text-sm font-medium text-secondary-700 mb-1">
|
||||
{label}
|
||||
{required && <span className="text-danger-500 ml-1" aria-label="required">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
id={inputId}
|
||||
className={clsx(
|
||||
'block w-full rounded-md border px-3 py-2 text-base min-h-touch',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500',
|
||||
'motion-safe:transition-colors',
|
||||
error
|
||||
? 'border-danger-500 text-danger-900 placeholder-danger-300'
|
||||
: 'border-secondary-300 text-secondary-900 placeholder-secondary-400',
|
||||
className
|
||||
)}
|
||||
aria-invalid={!!error}
|
||||
aria-describedby={clsx(error && errorId, helperText && helperId) || undefined}
|
||||
aria-required={required}
|
||||
{...props}
|
||||
/>
|
||||
{error && (
|
||||
<p id={errorId} className="mt-1 text-sm text-danger-600" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
{helperText && !error && (
|
||||
<p id={helperId} className="mt-1 text-sm text-secondary-500">
|
||||
{helperText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
Input.displayName = 'Input';
|
||||
Reference in New Issue
Block a user