# HuLibraries — Backoffice Reference
Este módulo es la implementación de referencia de HuLibraries en el **admin/backoffice**. Sirve como fuente de verdad para entender la arquitectura, patrones y convenciones a seguir en la implementación web.
---
## Estructura de directorios
```
HuLibraries/
├── Layout/
│ ├── index.tsx # Layout principal con Helmet + HuGoThemeProvider + Sidebar
│ ├── RootArticleLayout.tsx # Guard de :rootId, monta debajo de /:rootId
│ ├── ChildArticleLayout.tsx # Guard de :childId, monta debajo de /:rootId/child/:childId
│ └── articleActionRoutes.tsx # Subárbol de acciones (segmentation, report) reutilizado en root y child
├── Home/
│ └── index.tsx # Página de inicio (placeholder actual)
├── Article/
│ ├── index.tsx # Página de artículo
│ └── components/
│ ├── ArticleBreadcrumbs.tsx
│ ├── ArticleDetail/
│ │ ├── index.tsx
│ │ └── components/
│ │ ├── ArticleTitle.tsx
│ │ ├── ArticleBody.tsx
│ │ ├── ArticleAttachments.tsx
│ │ ├── ArticleChildren.tsx
│ │ ├── ArticleNewAlert.tsx
│ │ ├── ArticleNotSegmentedAlert.tsx
│ │ └── ArticleDetailSkeleton.tsx
│ └── ArticleHeader/
│ ├── index.tsx
│ ├── DeleteArticleModal.tsx
│ ├── DisabledArticleModal.tsx
│ ├── ActivateArticleModal.tsx
│ ├── NotificationsSettingsDrawer.tsx
│ └── DuplicateArticleDrawer/
│ ├── index.tsx
│ ├── DuplicateArticleForm.tsx
│ └── CancelDuplicateArticleModal.tsx
├── Segmentation/
│ ├── index.tsx
│ ├── types.ts
│ ├── constants.ts
│ ├── services.ts
│ ├── components/
│ │ ├── ArticleAudience.tsx
│ │ ├── ArticleAudienceForm.tsx
│ │ ├── ArticleAudienceFormSkeleton.tsx
│ │ └── ArticleAudienceFooter.tsx
│ └── hooks/
│ ├── useSaveArticleAudience.ts # artículo-específico
│ └── useGetAudienceItems.ts # artículo-específico
├── hooks/
│ ├── useArticleId.ts
│ ├── useArticleNavigation.ts
│ ├── useNavigateToCurrentArticle.ts
│ ├── useArticleBreadcrumb.ts
│ ├── useLibraryAccessGuard.ts
│ ├── useGetArticle.ts
│ ├── useGetArticleTree.ts
│ ├── useArticleInformation.ts
│ ├── useLibrariesTitle.ts
│ ├── useLibraryNotFoundHandler.ts
│ ├── useChangeArticleName.ts
│ ├── useUpdateArticleStatus.ts
│ ├── useUpdateArticleBody.ts
│ ├── useDeleteArticle.ts
│ ├── useDuplicateArticle.ts
│ ├── useCreateArticleAttachment.ts
│ ├── useCreateArticleAttachmentBulk.ts
│ ├── useDeleteArticleAttachment.ts
│ ├── useUpdateArticleAttachmentName.ts
│ ├── useUpdateArticleAttachmentsPositions.ts
│ ├── useHeaderArticleForm.ts
│ ├── useUpdateCoverPicture.ts
│ ├── useRemoveCoverPicture.ts
│ ├── useGetCollaborators.ts
│ ├── useGetEditors.ts
│ ├── useUpdateEditors.ts
│ ├── useSetArticleQueryData.ts
│ ├── useCoverPictureActions/
│ │ ├── index.tsx
│ │ └── components/
│ │ └── RemoveCoverPictureModal.tsx
│ ├── useEditPermissionsDrawer/
│ │ ├── index.tsx
│ │ └── components/
│ │ ├── EditPermissionsFormContent.tsx
│ │ └── CancelEditPermissionsModal.tsx
│ └── useSharePermissionsActions/
│ └── index.tsx
├── components/
│ └── ArticleInternalHeader.tsx
├── utils/
│ └── libraryErrorUtils.ts
├── types.ts
├── queries.ts
├── constants.ts
├── services.ts
├── utils.ts
└── routes.ts
```
---
## Reglas de estructura
1. **Organización por feature, no por tipo**: cada sub-feature (Article, Segmentation) tiene su propio directorio con sus propios `types.ts`, `services.ts`, `utils.ts`, `hooks/`, `components/`.
2. **Archivos raíz del módulo**: `types.ts`, `queries.ts`, `constants.ts`, `services.ts`, `utils.ts`, `routes.ts` — comparten funcionalidad entre sub-features.
3. **Hooks que renderizan UI** viven en subdirectorio propio con `index.tsx` + `components/` internos. Ej: `useCoverPictureActions/`, `useEditPermissionsDrawer/`.
4. **Componentes puramente presentacionales** van en `components/` sin subcarpeta, a menos que tengan sub-componentes propios.
5. **No crear archivos barrel** (`index.ts`) salvo en casos donde explícitamente se requiera re-exportar desde fuera del módulo.
6. **Utils: archivo único vs directorio**:
- Si hay un solo archivo de utils → `utils.ts` en la raíz del scope.
- Si hay más de uno → directorio `utils/` con archivos en `camelCase` sin sufijo `Utils` (ej: `utils/libraryError.ts`, no `utils/libraryErrorUtils.ts`).
---
## Reglas de componentes
### Estructura de un componente
```tsx
// 1. Imports React/externos
// 2. Imports del design system (@material-hu/...)
// 3. Imports internos del módulo (hooks, services, utils, types)
// 4. Definición de Props (interface)
// 5. Componente como arrow function con export nombrado o default
// 6. Export default al final
interface ArticleTitleProps {
article: Article;
}
const ArticleTitle = ({ article }: ArticleTitleProps) => {
// ...
};
export default ArticleTitle;
```
### Patrones de componentes
- **Inline save**: Para campos editables (título, body), usar `onBlur` para disparar la mutación. Envolver con `FormProvider` + `useForm`.
- **Lazy load de modales**: Los modales de confirmación se importan con `lazy()` dentro del componente que los usa para no inflar el bundle inicial.
- **Skeleton como estado de carga**: Cada sección con carga asíncrona tiene su propio componente `*Skeleton.tsx` (ej: `ArticleDetailSkeleton`, `ArticleAudienceFormSkeleton`).
- **Alertas contextuales**: Usar `ArticleNewAlert` / `ArticleNotSegmentedAlert` para guiar al usuario según el estado del artículo.
- **Renderizado condicional**: Usar flags booleanas explícitas (`showLoading`, `showDetail`, `showError`) en vez de ternarios anidados.
```tsx
const showLoading = isLoading;
const showError = !isLoading && isError;
const showContent = !isLoading && !isError && !!article;
return (
<>
{showLoading && }
{showError && }
{showContent && }
>
);
```
---
## Reglas de hooks
### Nomenclatura de hooks
Los hooks se clasifican en cuatro categorías con convenciones de nombre distintas:
| Categoría | Patrón | Ejemplos |
|-----------|--------|---------|
| **Query** (fetching de datos) | `useGet[Data]` | `useGetArticle`, `useGetAudienceCount`, `useGetEditors` |
| **Mutation** (acciones que modifican datos) | `use[Action][Data]` | `useDeleteArticle`, `useUpdateArticleStatus`, `useCreateArticleAttachment` |
| **Drawer** (hooks que encapsulan un drawer) | `use[Data]Drawer` | `useEditPermissionsDrawer`, `useDuplicateArticleDrawer` |
| **Otros** (utilidades, estado, lógica de UI) | `use[Something]` | `useArticleId`, `useArticleInformation`, `useLibrariesTitle` |
### Hooks de queries (fetching)
Si el hook depende de un parámetro opcional (ej: un ID de ruta), se debe validar dentro del `queryFn` y lanzar un error descriptivo. No usar el operador `!` para forzar el tipo.
```ts
// hooks/useGetArticleTree.ts
import { useQuery } from 'react-query';
import { librariesKeys } from '../queries';
import { getArticleTree } from '../services';
export const useGetArticleTree = (articleId: number | undefined) => {
return useQuery({
queryKey: librariesKeys.articleTree(articleId),
queryFn: () => {
if (!articleId) {
throw new Error('[useGetArticleTree] Article ID is required');
}
return getArticleTree(articleId);
},
select: response => response.data,
enabled: !!articleId,
});
};
```
- El `enabled: !!articleId` evita que se ejecute sin el ID.
- El guard dentro de `queryFn` protege el tipo en tiempo de ejecución y hace el error trazable.
- Usar `select: response => response.data` para exponer directamente el tipo de dominio.
- Pasar `options` como último parámetro para permitir overrides desde el consumidor.
### Hooks de mutations
```ts
// hooks/useDeleteArticle.ts
export const useDeleteArticle = () => {
const { enqueueSnackbar } = useHuSnackbar();
const queryClient = useQueryClient();
const navigate = useNavigate();
return useMutation({
mutationFn: (articleId: number) => deleteArticle(articleId),
onSuccess: () => {
enqueueSnackbar({ title: t('delete.success'), variant: 'success' });
queryClient.invalidateQueries(librariesKeys.all());
navigate(librariesRoutes.base());
},
onError: (err) => {
// Manejar errores específicos antes del fallback genérico
if (isArticleSpecificError(err)) {
enqueueSnackbar({ title: t('specific.error'), variant: 'error' });
return;
}
enqueueSnackbar({ title: t('delete.error'), variant: 'error' });
},
});
};
```
- Invalidar queries relacionadas en `onSuccess`.
- Manejar errores específicos (por código) antes del fallback genérico.
- Mostrar snackbar de éxito/error en el hook, no en el componente.
- Navegar en el hook cuando aplica (ej: después de delete).
### Hooks de drawers (con UI)
Para hooks que encapsulan un drawer, usar la convención de subdirectorio:
```
hooks/useEditPermissionsDrawer/
├── index.tsx ← exporta el hook (nombrado, no default)
└── components/
├── EditPermissionsFormContent.tsx
└── CancelEditPermissionsModal.tsx
```
El hook retorna un objeto con la UI rendereable y las funciones para mostrarla:
```tsx
export const useEditPermissionsDrawer = () => {
const { drawer, showDrawer } = useDrawer(EditPermissionsFormContent);
const { modal, showModal } = useLazyModal(CancelEditPermissionsModal);
return {
editPermissionsDrawer: drawer,
cancelPermissionsModal: modal,
showEditPermissionsDrawer: showDrawer,
};
};
```
### Hooks de utilidad del módulo
- `useArticleId()`: Extrae y parsea los route params. Devuelve `{ rootId, childId, articleId }` donde `articleId = childId ?? rootId` (id del artículo activo).
- `useArticleInformation(article)`: Calcula flags de permisos y estado (`canEdit`, `canDelete`, `isActive`, etc.).
- `useLibrariesTitle()`: Obtiene el título localizado del módulo con prefijo "HU".
- `useLibraryNotFoundHandler()`: Registra handler global de 404 (errores de la API) y navega al base route. Deduplica errores con ventana de 500ms.
- `useLibraryAccessGuard({ resetKey, isInvalid })`: Hook genérico usado por los layouts de ruta para validar URL params y redirigir al home si son inválidos. Ver sección **Routes & Navigation**.
- `useNavigateToCurrentArticle()`: Devuelve un callback que navega al artículo del contexto actual (útil para botones de "back" desde sub-pantallas como segmentation/report).
- `useArticleNavigation()`: Devuelve `navigateBack` con lógica de prioridad search → parent → home.
- `useSetArticleQueryData()`: Actualiza el cache de React Query de forma type-safe sin invalidar.
---
## Reglas de queries/mutations/services
### services.ts — Capa de API
```ts
// services.ts
export const getArticle = (articleId: number) =>
api.get(`/articles/${articleId}`);
export const updateArticleStatus = (payload: UpdateArticleStatusPayload) =>
api.patch('/articles/status', payload);
```
- Funciones puras que retornan la promesa del HTTP client.
- Sin lógica de negocio ni manejo de errores: eso va en los hooks.
- Tipar la respuesta con el genérico del cliente HTTP.
- Agrupar por entidad en el mismo archivo (artículo, adjuntos, etc.).
### queries.ts — React Query Key Factory
```ts
// queries.ts
export const librariesKeys = {
all: () => ['libraries'] as const,
article: (id?: number) => [...librariesKeys.all(), 'article', id] as const,
articleTree: (id?: number) => [...librariesKeys.all(), 'tree', id] as const,
collaborators: (id?: number, search?: string) =>
[...librariesKeys.all(), 'collaborators', id, search] as const,
editors: (id?: number) => [...librariesKeys.all(), 'editors', id] as const,
audience: {
all: (id?: number) => [...librariesKeys.all(), 'audience', id] as const,
count: (id?: number) => [...librariesKeys.audience.all(id), 'count'] as const,
},
};
```
- Usar `as const` para type safety.
- Estructura jerárquica: `all > entidad > id > sub-recurso`.
- La jerarquía permite invalidar en cascada: `librariesKeys.all()` invalida todo.
- Nunca hardcodear strings de query keys fuera de este archivo.
---
## Reglas de utils/constants
### utils — archivo único vs directorio
- **Un solo dominio de utils** → usar `utils.ts` directamente.
- **Múltiples dominios** → crear directorio `utils/` con archivos en `camelCase` sin sufijo `Utils`:
```
utils/
├── breadcrumb.ts # getBreadcrumbItems, etc.
├── editors.ts # splitEditors, formatEditorsPayload, etc.
└── libraryError.ts # isLibraryQuery, isNotFoundError, etc.
```
```ts
// utils.ts (caso de un solo archivo)
export const getBreadcrumbItems = (tree: ArticleTree[]): BreadcrumbLink[] => { ... };
export const splitEditors = (editors: ArticleCollaborator[]) => { ... };
export const formatEditorsPayload = (editors: ArticleCollaborator[]) => { ... };
export const getPillConfig = (status: ArticleStatus) => { ... };
```
```ts
// utils/libraryError.ts (caso de directorio — nombre sin sufijo "Utils")
export const isLibraryQuery = (queryKey: unknown[]) => { ... };
export const isLibraryMutation = (mutationKey: unknown[]) => { ... };
export const isNotFoundError = (error: unknown) => { ... };
export const isLibraryNotFoundError = (queryKey: unknown[], error: unknown) => { ... };
```
- Solo transformaciones puras de datos.
- Sin side effects, sin llamadas a hooks, sin imports de react.
- Testeable unitariamente.
### constants.ts
```ts
export const ARTICLE_TITLE_MAX_LENGTH = 240;
export const MAX_EDITORS_LENGTH = 150;
```
- Solo valores literales que se usan en múltiples lugares del módulo.
- No mezclar con tipos ni funciones.
---
## Reglas de types
### types.ts — Tipos del dominio
```ts
// Enums de estado
export enum ArticleStatus {
ENABLED = 'ENABLED',
DISABLED = 'DISABLED',
}
// Tipos de dominio (respuesta de API)
export type Article = {
id: number;
title: string;
body: string;
status: ArticleStatus;
attachments: ArticleAttachment[];
segmentation: ArticleSegmentation;
children: ArticleChild[];
// ...
};
// Tipos de payload para mutaciones
export type UpdateArticleStatusPayload = {
articleId: number;
status: ArticleStatus;
};
```
- Separar enums, tipos de dominio y tipos de payload.
- Los tipos de payload llevan sufijo `Payload` o `Request`.
- Los tipos de respuesta de API son los mismos que los de dominio (se mapean en `select`).
- Tipos de sub-feature van en `Segmentation/types.ts`, no en el raíz.
---
## Segmentation — Arquitectura
La segmentación es el sub-sistema más complejo. Documenta su propia arquitectura.
### Tipos de condición (`SegmentationConditionType`)
| Tipo | Descripción |
|------|-------------|
| `ALL` | Todos los usuarios |
| `USERS` | IDs específicos de usuario |
| `ITEMS` | IDs de items de segmentación |
| `EXPRESSION` | Condiciones anidadas con operadores AND/OR |
| `USERS_OR_ITEMS` | Mezcla de usuarios e items |
### Flujo de datos
```
API (AudienceQuery)
→ audienceQueryToCriterias() # API → form criteria [src/components/Audience/utils]
→ [FormState]
→ criteriasToSegmentationExpression() # form criteria → API payload [src/components/Audience/utils]
→ API
```
### Hooks de segmentación
Los hooks genéricos de audiencia viven en `src/components/Audience/hooks/` y son compartidos por otros módulos (ServiceManagement, RolesAndPermissions, Competencies). Solo permanecen aquí los hooks específicos del dominio de artículo.
**Hooks artículo-específicos** (`Segmentation/hooks/`):
| Hook | Propósito |
|------|-----------|
| `useGetAudienceItems` | Carga grupos e items de segmentación con mapas indexados por ID |
| `useSaveArticleAudience` | Mutación para persistir la expresión en el artículo |
**Hooks compartidos** (`src/components/Audience/hooks/`):
| Hook | Propósito |
|------|-----------|
| `useGetAudienceCount` | Cuenta usuarios que cumplen una expresión de segmentación |
| `useGetAudienceUsers` | Query infinita para búsqueda de usuarios paginada |
| `useSelectedUsers` | Fetch datos completos de usuarios seleccionados por ID |
| `useAudienceFormCount` | Count reactivo desde criterias del formulario de Audience |
| `useSegmentationDrawerCount` | Count para el drawer de segmentación |
| `useIndividualDrawerCount` | Count para el drawer de selección individual |
| `useSegmentationFieldItems` | Carga dinámica de campos (grupos) con filtro de exclusión |
| `useSegmentationValueItems` | Carga dinámica de valores con paginación infinita |
| `useSegmentationDrawerService` | Crea search service desde condiciones de segmentación |
| `useIndividualDrawerService` | Crea search service desde IDs de usuarios |
**Utils compartidas** (`src/components/Audience/utils/`):
| Función | Propósito |
|---------|-----------|
| `criteriasToSegmentationExpression` | Form criteria → `SegmentationExpressionPayload` |
| `criteriaToSegmentationExpression` | Un `CriteriaEntry` → `SegmentationExpressionCondition` |
| `audienceQueryToCriterias` | `AudienceQuery` (API) → form criteria |
| `audienceQueryToSegmentationExpression` | `AudienceQuery` (API) → `SegmentationExpressionPayload` |
| `getSegmentationDescription` | Texto legible para una entrada de segmentación |
| `getIndividualDescription` | Texto legible para una entrada individual (resuelve nombres) |
| `getCollaboratorsService` | Factory de search service vinculado a una expresión |
**Archivos compartidos en raíz** (`src/components/Audience/`):
| Archivo | Propósito |
|---------|-----------|
| `services.ts` | `countAudienceByExpression`, `searchAudienceByExpression` |
| `queryKeys.ts` | `audienceExpressionKeys` — query key factory para React Query |
| `types.ts` | Tipos del dominio: `AudienceQuery`, `SegmentationExpressionPayload`, etc. |
---
## Patrones de modales y drawers
### Modal de confirmación
```tsx
// En el componente que dispara el modal
const DeleteArticleModal = lazy(() => import('./DeleteArticleModal'));
const { isOpen, openModal, closeModal } = useModalState();
return (
<>
{isOpen && (
)}
>
);
```
### Drawer con formulario
```tsx
// Drawer con FormProvider interno
const DuplicateArticleDrawer = () => {
const form = useForm();
return (
{/* modal anidado para confirmar cierre */}
);
};
```
---
## Routes & Navigation
### URL shape
El módulo soporta dos niveles de artículo en la URL: **root** y **child** (un descendiente cualquiera del root). El `rootId` es siempre el primer segmento porque el sidebar fetchea por root.
```
/library → Home
/library/:rootId → Article (root)
/library/:rootId/segmentation → Segmentation (root)
/library/:rootId/report → Report (root)
/library/:rootId/child/:childId → Article (child)
/library/:rootId/child/:childId/segmentation → Segmentation (child)
/library/:rootId/child/:childId/report → Report (child)
```
### Estructura de rutas (anidada)
Las rutas se declaran en `src/routes.tsx`. La key del diseño: las acciones (`segmentation`, `report`) se declaran **una sola vez** como un array reutilizable (`articleActionRoutes`) y se montan tanto bajo el layout root como bajo el layout child.
```tsx
{
path: 'library',
element: , // sidebar + theme + search
children: [
{ index: true, element: },
{
path: ':rootId',
element: , // valida rootId, redirige si inválido
children: [
{ index: true, element: },
...articleActionRoutes, // segmentation, report
{
path: 'child/:childId',
element: , // valida que childId sea descendiente
children: [
{ index: true, element: },
...articleActionRoutes, // mismas acciones, contexto child
],
},
],
},
],
}
```
`articleActionRoutes` vive en [`Layout/articleActionRoutes.tsx`](Layout/articleActionRoutes.tsx) dentro del módulo (no en `src/routes.tsx`) para mantener ownership.
**Por qué anidado**: en una estructura plana habría que declarar cada acción dos veces (una bajo `:rootId/...` y otra bajo `:rootId/child/:childId/...`). La estructura anidada permite reutilizar el subárbol y agregar acciones nuevas en una sola línea.
### URL builders (`routes.ts`)
```ts
// routes.ts
export const librariesRoutes = {
base, // /library
article, // /library/:rootId | .../child/:childId
report: (rootId, childId?) => `${article(rootId, childId)}/report`,
segmentation: (rootId, childId?) => `${article(rootId, childId)}/segmentation`,
};
```
- Siempre usar estos helpers, nunca strings hardcodeados.
- `article(rootId, childId?)` agrega el segmento `/child/:childId` solo cuando `childId` está presente y difiere de `rootId`. Esto significa que el caller no tiene que normalizar `childId === rootId ? undefined : childId` en cada call site — el helper lo hace.
### Layouts como guards
Los dos layouts intermedios (`RootArticleLayout` y `ChildArticleLayout`) son guards que validan los params contra los datos cargados, y redirigen al home con un snackbar si la URL es inválida. Renderizan `` siempre, así los hijos manejan su propio loading sin bloquearse.
**[`RootArticleLayout`](Layout/RootArticleLayout.tsx)** — monta debajo de `:rootId`:
- Valida que `rootId` parsee a número.
- Valida que exista en `useGetRootArticles()`.
- Espera a que la query no esté `isLoading` ni `isFetching` (importante: tras una mutación que invalida `rootList`, no debe redirigir prematuramente).
**[`ChildArticleLayout`](Layout/ChildArticleLayout.tsx)** — monta debajo de `:rootId/child/:childId`:
- Valida que `childId` parsee a número.
- Valida que el árbol del childId tenga como root el `rootId` (vía `useGetArticleTree`).
Ambos delegan la mecánica del redirect (snackbar + dedup + `replace: true`) al hook compartido [`useLibraryAccessGuard`](hooks/useLibraryAccessGuard.ts).
### Hooks de navegación
| Hook | Qué hace |
|---|---|
| [`useArticleId`](hooks/useArticleId.ts) | Parsea `params.rootId` y `params.childId`. Devuelve `{ rootId, childId, articleId }`. `articleId = childId ?? rootId` es el id del artículo activo. |
| [`useNavigateToCurrentArticle`](hooks/useNavigateToCurrentArticle.ts) | Devuelve un callback que navega al artículo del contexto actual (`article(rootId, childId)`). Usado en `ArticleInternalHeader.handleBack` y `ArticleAudienceForm` (volver tras guardar/cancelar segmentation). |
| [`useArticleNavigation`](hooks/useArticleNavigation.ts) | Devuelve `navigateBack` con prioridad: (1) si `state.search` está seteado → `navigate(-1)` para restaurar resultados de búsqueda; (2) si hay `parentId` → ir al padre en el árbol; (3) fallback al home. |
| [`useArticleBreadcrumb`](hooks/useArticleBreadcrumb.ts) | Construye los items del breadcrumb a partir del árbol. Cada click navega vía `librariesRoutes.article(rootId, ancestorId)`. |
| [`useLibraryAccessGuard`](hooks/useLibraryAccessGuard.ts) | Hook genérico usado por los dos layouts. Recibe `{ resetKey, isInvalid }`. Cuando `isInvalid` es true, muestra snackbar y redirige al home. Dedupe interno por `resetKey`. |
### Estado de navegación tipado
Cuando se navega desde un resultado de búsqueda hacia un artículo, [`SearchItem`](Article/components/ArticleSearch/SearchItem.tsx) pasa el query actual como `state` para que después el back lo restaure. El contrato está tipado en `types.ts`:
```ts
export type LibraryNavigationState = {
search?: string;
};
```
- **Productor**: `SearchItem.handleSelect` hace `navigate(url, { state: { search } satisfies LibraryNavigationState })`.
- **Consumidor**: `useArticleNavigation.navigateBack` lee `location.state as LibraryNavigationState | null` y, si encuentra `search`, hace `navigate(-1)` para que el usuario regrese a los resultados de búsqueda con el query intacto.
### Reglas para agregar features
- **Nueva acción de artículo (e.g. `analytics`)**: agregar una entrada en `articleActionRoutes` y un builder en `librariesRoutes`. Funciona automáticamente para root y child.
- **Nueva URL del módulo que NO es acción de artículo**: declararla bajo el layout correspondiente en `src/routes.tsx`.
- **Cambiar la shape de una URL**: tocar solo `routes.ts` (los builders) y `src/routes.tsx` (los `path` declarados). Mantener ambos sincronizados — son las dos fuentes que React Router consume.
- **Nueva navegación desde un componente**: usar `librariesRoutes.*` para construir la URL; nunca strings hardcodeados.
---
## Convenciones de naming
| Concepto | Convención | Ejemplo |
|----------|-----------|---------|
| Componente | PascalCase | `ArticleTitle` |
| Hook query | `useGet[Data]` | `useGetArticle`, `useGetAudienceCount` |
| Hook mutation | `use[Action][Data]` | `useDeleteArticle`, `useUpdateArticleStatus` |
| Hook drawer | `use[Data]Drawer` | `useEditPermissionsDrawer` |
| Hook otros | `use[Something]` | `useArticleId`, `useArticleInformation` |
| Service function | camelCase | `getArticle`, `updateArticleStatus` |
| Query key factory | camelCase + `Keys` | `librariesKeys` |
| Enum | PascalCase | `ArticleStatus` |
| Enum value | UPPER_SNAKE_CASE | `ArticleStatus.ENABLED` |
| Tipo de payload | PascalCase + `Payload` | `UpdateArticleStatusPayload` |
| Constante | UPPER_SNAKE_CASE | `ARTICLE_TITLE_MAX_LENGTH` |
| Ruta helper | camelCase + `Routes` | `librariesRoutes` |
---
## Stack tecnológico del módulo
- **React** — componentes funcionales + hooks
- **react-router-dom** — navegación, `useParams`, `useNavigate`, `useMatch`
- **react-hook-form** — gestión de formularios con `FormProvider` + `useWatch`
- **@tanstack/react-query** — fetching, caching, invalidación
- **@material-hu/mui** — componentes UI (Stack, Typography, Box, etc.)
- **@material-hu/components** — design system propio (HuSnackbar, HuPills, etc.)
- **react-helmet-async** — gestión del `` del documento