# Patterns
Code snippets for common UI patterns in humand-create-app projects. These are ready-to-adapt references — read the relevant pattern and pass it to the implementation subagent.
All imports use `@material-hu/components/...`. Never import from `@mui/material` directly.
---
## Page layout with Title header
```tsx
import Stack from '@material-hu/mui/Stack';
import Button from '@material-hu/components/design-system/Buttons/Button';
import Title from '@material-hu/components/design-system/Title';
import { IconPlus } from '@material-hu/icons/tabler';
import { DashboardLayout } from '../../layouts/DashboardLayout';
export const MyPage = () => {
return (
}
>
Action label
{/* page content */}
);
};
```
---
## Empty state
Uses the **composed** StateCard (not the design-system one).
```tsx
import StateCard from '@material-hu/components/composed-components/StateCard';
import { IconInfoCircle } from '@material-hu/icons/tabler';
```
---
## Table with pagination
```tsx
import Table from '@material-hu/components/design-system/Table';
import TableContainer from '@material-hu/components/design-system/Table/components/TableContainer';
import TableHead from '@material-hu/components/design-system/Table/components/TableHead';
import TableBody from '@material-hu/components/design-system/Table/components/TableBody';
import TableRow from '@material-hu/components/design-system/Table/components/TableRow';
import TableCell from '@material-hu/components/design-system/Table/components/TableCell';
import Pagination from '@material-hu/components/design-system/Inputs/Pagination';
const [page, setPage] = useState(1);
const [limit, setLimit] = useState(10);
const totalPages = Math.ceil(items.length / limit);
const paginatedItems = items.slice((page - 1) * limit, page * limit);
Nombre
Estado
{/* more columns */}
{paginatedItems.map(item => (
{item.name}
{/* content */}
))}
{
setLimit(newLimit);
setPage(1);
}}
/>
```
---
## Status badges with Pills
```tsx
import Pills from '@material-hu/components/design-system/Pills';
type StatusType = 'active' | 'paused' | 'draft';
const STATUS_CONFIG: Record = {
active: { label: 'ACTIVO', type: 'success' },
paused: { label: 'PAUSADO', type: 'warning' },
draft: { label: 'BORRADOR', type: 'neutral' },
};
const { label, type } = STATUS_CONFIG[item.status];
```
---
## Menu via useMenuLayer
Always use the layer hook. Never use MenuList or Menu directly.
```tsx
import { useMenuLayer } from '@material-hu/components/layers/Menus';
import IconButton from '@material-hu/components/design-system/Buttons/IconButton';
import { IconDotsVertical, IconEdit, IconCopy, IconTrash } from '@material-hu/icons/tabler';
const { openMenu } = useMenuLayer();
const handleOpenMenu = (e: React.MouseEvent, item: MyItem) => {
openMenu({
anchorEl: e.currentTarget,
items: [
{
id: 'edit',
title: 'Editar',
icon: IconEdit,
onSelect: () => handleEdit(item),
},
{
id: 'duplicate',
title: 'Duplicar',
icon: IconCopy,
onSelect: () => handleDuplicate(item),
},
{
id: 'delete',
title: 'Eliminar',
icon: IconTrash,
onSelect: () => handleDelete(item),
},
],
});
};
handleOpenMenu(e, item)}
>
```
---
## Dialog confirmation via useDialogLayer
Always use the layer hook. Never use Dialog directly.
```tsx
import { useDialogLayer } from '@material-hu/components/layers/Dialogs';
const { openDialog, closeDialog } = useDialogLayer();
const handleDelete = (item: MyItem) => {
openDialog({
title: '¿Eliminar proceso?',
textBody: `Se eliminará "${item.name}" de forma permanente.`,
primaryButtonProps: {
children: 'Eliminar',
color: 'error',
onClick: () => {
// perform delete
closeDialog();
},
},
secondaryButtonProps: {
children: 'Cancelar',
onClick: () => closeDialog(),
},
});
};
```
---
## Drawer form via useDrawerLayer
Always use the layer hook. Never use Drawer directly.
```tsx
import { useDrawerLayer } from '@material-hu/components/layers/Drawers';
const { openDrawer, closeDrawer } = useDrawerLayer();
const handleOpenDrawer = () => {
openDrawer({
title: 'Editar paso',
size: 'medium',
children: ,
footerProps: {
primaryButtonProps: {
children: 'Guardar',
form: 'my-form-id',
type: 'submit',
},
secondaryButtonProps: {
children: 'Cancelar',
onClick: () => closeDrawer(),
},
},
});
};
```
---
## Search input
```tsx
import Search from '@material-hu/components/design-system/Inputs/Search';
const [search, setSearch] = useState('');
setSearch(e.target.value)}
placeholder="Buscar"
variant="classic"
/>
```
---
## Stepper
```tsx
import Stepper from '@material-hu/components/design-system/Stepper';
import Step from '@material-hu/components/design-system/Stepper/components/Step';
const [activeStep, setActiveStep] = useState(0);
const steps = ['Información básica', 'Contenido', 'Asignación', 'Revisión'];
{steps.map((label, index) => (
))}
```
---
## Form with React Hook Form + Zod
```tsx
import { useForm, FormProvider } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import FormInputClassic from '@material-hu/components/design-system/Inputs/Classic/form';
import FormTextArea from '@material-hu/components/design-system/Inputs/TextArea/form';
const schema = z.object({
name: z.string().min(1, 'Requerido').max(100),
description: z.string().max(256).optional(),
});
type FormValues = z.infer;
const methods = useForm({
resolver: zodResolver(schema),
defaultValues: { name: '', description: '' },
});
```
---
## Vercel API proxy route (external service)
Server-side proxy that keeps API keys out of the browser. Each operation is an explicit route — no open passthrough.
```typescript
// api//[...path].ts
import type { VercelRequest, VercelResponse } from '@vercel/node';
const ROUTES: Record Promise> = {
'GET /contacts': handleGetContacts,
'POST /contacts': handleCreateContact,
};
export default async function handler(req: VercelRequest, res: VercelResponse) {
const path = Array.isArray(req.query.path)
? req.query.path.join('/')
: req.query.path ?? '';
const routeKey = `${req.method} /${path}`;
const routeHandler = ROUTES[routeKey];
if (!routeHandler) {
return res.status(404).json({ error: 'Not found' });
}
try {
const data = await routeHandler(req);
return res.status(200).json(data);
} catch (error) {
console.error(`[api/] ${routeKey} failed:`, error);
return res.status(500).json({ error: 'Internal server error' });
}
}
async function handleGetContacts(_req: VercelRequest) {
const API_KEY = process.env.SERVICE_API_KEY;
if (!API_KEY) throw new Error('Missing API key');
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
try {
const response = await fetch('https://api.example.com/contacts', {
headers: { Authorization: `Bearer ${API_KEY}` },
signal: controller.signal,
});
if (!response.ok) throw new Error(`Upstream ${response.status}`);
const raw = await response.json();
return raw.results.map((c: any) => ({ id: c.id, name: c.name, email: c.email }));
} finally {
clearTimeout(timeout);
}
}
```
---
## react-query: query hook
Wraps a service call with `useQuery`. Always use single-object syntax.
```tsx
import { useQuery } from '@tanstack/react-query';
import { contactsService } from '../services/contacts';
// Query keys factory
export const contactsKeys = {
all: () => ['contacts'] as const,
lists: () => [...contactsKeys.all(), 'list'] as const,
list: (params?: unknown) => [...contactsKeys.lists(), params] as const,
};
export const useGetContacts = (params?: ContactFilters) => {
const { data, ...query } = useQuery({
queryKey: contactsKeys.list(params),
queryFn: () => contactsService.getContacts(params),
});
return { contacts: data ?? [], ...query };
};
```
---
## react-query: mutation hook
Wraps a service call with `useMutation`. Invalidates related queries on success.
```tsx
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { contactsService } from '../services/contacts';
export const useCreateContactMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: contactsService.createContact,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: contactsKeys.lists() });
},
});
};
```
---
## react-query: using hooks in a component
```tsx
import { useGetContacts, useCreateContactMutation } from '../../services/contacts.hooks';
export const ContactList = () => {
const { contacts, isLoading } = useGetContacts();
const createContact = useCreateContactMutation();
const handleCreate = (data: CreateContactInput) => {
createContact.mutate(data, {
onSuccess: () => {
// UI feedback: toast, close drawer, etc.
},
});
};
if (isLoading) return ;
return (
// render contacts...
);
};
```