Status: Active
Phase: 2 - UI Base
Date: 2024-01-XX
The UI Base (components/ui/) is a minimal design system foundation that provides reusable, generic UI primitives for all domains in the monorepo. These components are domain-agnostic, contain no business logic, and can be safely used across QRACK, Discart-me, and future applications.
Existing screens MUST NOT be refactored to use these components yet.
These components are:
File: components/ui/Button.tsx
A generic button component with consistent styling and behavior.
primary - Cyan background (default, matches QRACK theme)secondary - Zinc gray backgroundghost - Transparent with hover effectdanger - Red background for destructive actionssm - Small (px-3 py-1.5, text-sm)md - Medium (px-4 py-2, text-sm) - defaultlg - Large (px-6 py-3, text-base)interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
children: React.ReactNode;
}
import { Button } from '@/components/ui';
// Basic usage
<Button variant="primary" onClick={handleClick}>
Click me
</Button>
// With loading state
<Button variant="primary" loading={isLoading}>
Saving...
</Button>
// Disabled state
<Button variant="danger" disabled={!canDelete} onClick={handleDelete}>
Delete
</Button>
Link separately)File: components/ui/Input.tsx
A generic input component with proper accessibility and error handling.
htmlFor bindingaria-invalid, aria-required, etc.)interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
helperText?: string;
required?: boolean;
fullWidth?: boolean;
}
import { Input } from '@/components/ui';
// Basic input with label
<Input
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
/>
// With error handling
<Input
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
error={errors.password}
required
/>
// With helper text
<Input
label="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
helperText="Choose a unique username"
/>
File: components/ui/Loading.tsx
A generic loading indicator with spinner and skeleton variants.
spinner - Animated spinner (default)skeleton - Skeleton loading linessm - Smallmd - Medium (default)lg - Largeinterface LoadingProps {
variant?: 'spinner' | 'skeleton';
size?: 'sm' | 'md' | 'lg';
message?: string;
className?: string;
lines?: number; // Only for skeleton variant
}
import { Loading } from '@/components/ui';
// Spinner with message
<Loading variant="spinner" size="md" message="Loading data..." />
// Skeleton loader
<Loading variant="skeleton" lines={3} />
// Full page loading
<div className="flex items-center justify-center min-h-screen">
<Loading variant="spinner" size="lg" message="Please wait..." />
</div>
File: components/ui/Modal.tsx
A generic modal component with portal rendering and accessibility support.
document.body)interface ModalProps {
open: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
footer?: React.ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl';
closeOnEscape?: boolean;
closeOnBackdropClick?: boolean;
className?: string;
}
import { Modal, Button } from '@/components/ui';
// Basic modal
<Modal
open={isOpen}
onClose={() => setIsOpen(false)}
title="Confirm Action"
>
<p>Are you sure you want to proceed?</p>
</Modal>
// With footer actions
<Modal
open={isOpen}
onClose={() => setIsOpen(false)}
title="Delete Item"
footer={
<>
<Button variant="ghost" onClick={() => setIsOpen(false)}>
Cancel
</Button>
<Button variant="danger" onClick={handleDelete}>
Delete
</Button>
</>
}
>
<p>This action cannot be undone.</p>
</Modal>
// Large modal
<Modal
open={isOpen}
onClose={() => setIsOpen(false)}
title="Details"
size="xl"
>
{/* Large content */}
</Modal>
All components are designed to work across all domains without domain-specific logic or assumptions.
Components are pure UI primitives. All state and business logic should be handled by parent components or hooks.
All components follow accessibility best practices:
Components use Tailwind CSS and follow the existing dark theme patterns:
bg-zinc-900, bg-zinc-800)cyan-500)All components are fully typed with TypeScript and extend native HTML element props where applicable.
During Phase 3, existing screens can be gradually refactored to use these components:
When adding new UI primitives:
components/ui/index.tsimport { Input, Button } from '@/components/ui';
function MyForm() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
return (
<form onSubmit={handleSubmit}>
<Input
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
error={error}
required
/>
<Button type="submit" variant="primary">
Submit
</Button>
</form>
);
}
import { Loading } from '@/components/ui';
function DataDisplay() {
const { data, isLoading } = useData();
if (isLoading) {
return <Loading variant="spinner" message="Loading..." />;
}
return <div>{/* Render data */}</div>;
}
import { Modal, Button } from '@/components/ui';
function DeleteButton() {
const [showModal, setShowModal] = useState(false);
return (
<>
<Button variant="danger" onClick={() => setShowModal(true)}>
Delete
</Button>
<Modal
open={showModal}
onClose={() => setShowModal(false)}
title="Confirm Deletion"
footer={
<>
<Button variant="ghost" onClick={() => setShowModal(false)}>
Cancel
</Button>
<Button variant="danger" onClick={handleDelete}>
Delete
</Button>
</>
}
>
<p>Are you sure? This action cannot be undone.</p>
</Modal>
</>
);
}
If you’re unsure whether to use a UI component: