create-lexy 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +7 -2
  2. package/assets/fonts/{OFL-NotoSans.txt → LICENSE-Geist.txt} +4 -5
  3. package/assets/fonts/geist-mono-variable-italic.woff2 +0 -0
  4. package/assets/fonts/geist-mono-variable.woff2 +0 -0
  5. package/assets/fonts/geist-sans-variable-italic.woff2 +0 -0
  6. package/assets/fonts/geist-sans-variable.woff2 +0 -0
  7. package/assets/r/accordion.json +3 -3
  8. package/assets/r/alert-dialog.json +3 -3
  9. package/assets/r/app-accordion.json +1 -1
  10. package/assets/r/app-dialog.json +3 -3
  11. package/assets/r/app-header-bar.json +2 -2
  12. package/assets/r/app-sidebar.json +2 -2
  13. package/assets/r/avatar.json +3 -3
  14. package/assets/r/badge.json +3 -3
  15. package/assets/r/brand-background.json +1 -1
  16. package/assets/r/breadcrumb.json +3 -3
  17. package/assets/r/button-group.json +3 -3
  18. package/assets/r/button.json +3 -3
  19. package/assets/r/calendar.json +2 -2
  20. package/assets/r/card.json +3 -3
  21. package/assets/r/chart.json +2 -2
  22. package/assets/r/checkbox.json +2 -2
  23. package/assets/r/combobox.json +3 -3
  24. package/assets/r/command.json +3 -3
  25. package/assets/r/confirmacion.json +1 -1
  26. package/assets/r/counter-badge.json +3 -3
  27. package/assets/r/crm-desk.json +1 -1
  28. package/assets/r/crm-detalle-caso.json +5 -5
  29. package/assets/r/date-picker.json +1 -1
  30. package/assets/r/dialog.json +3 -3
  31. package/assets/r/dropdown-menu.json +3 -3
  32. package/assets/r/empty.json +2 -2
  33. package/assets/r/feature-card.json +3 -3
  34. package/assets/r/form.json +3 -3
  35. package/assets/r/header-bar.json +2 -2
  36. package/assets/r/input.json +3 -3
  37. package/assets/r/intake-wizard.json +1 -1
  38. package/assets/r/label.json +3 -3
  39. package/assets/r/logo.json +2 -2
  40. package/assets/r/menubar.json +3 -3
  41. package/assets/r/navigation-menu.json +3 -3
  42. package/assets/r/pagination.json +2 -2
  43. package/assets/r/popover.json +3 -3
  44. package/assets/r/profile-card.json +3 -3
  45. package/assets/r/progress.json +2 -2
  46. package/assets/r/radio-group.json +2 -2
  47. package/assets/r/registry.json +47 -47
  48. package/assets/r/scroll-area.json +2 -2
  49. package/assets/r/searchbox.json +3 -3
  50. package/assets/r/select.json +3 -3
  51. package/assets/r/separator.json +2 -2
  52. package/assets/r/sheet.json +2 -2
  53. package/assets/r/sidebar.json +3 -3
  54. package/assets/r/skeleton.json +2 -2
  55. package/assets/r/slider.json +3 -3
  56. package/assets/r/snippet.json +3 -3
  57. package/assets/r/spinner.json +2 -2
  58. package/assets/r/status-dot.json +3 -3
  59. package/assets/r/switch.json +3 -3
  60. package/assets/r/table.json +3 -3
  61. package/assets/r/tabs.json +3 -3
  62. package/assets/r/tag.json +3 -3
  63. package/assets/r/textarea.json +3 -3
  64. package/assets/r/toaster.json +3 -3
  65. package/assets/r/tooltip.json +3 -3
  66. package/assets/r/tree.json +3 -3
  67. package/assets/registry-version +1 -1
  68. package/assets/theme/lexy-theme.css +562 -112
  69. package/dist/index.js +590 -276
  70. package/package.json +4 -2
  71. package/templates/.claude/skills/lexy-design/SKILL.md +189 -0
  72. package/templates/.claude/skills/lexy-dev/SKILL.md +168 -0
  73. package/templates/.claude/skills/lexy-mock-data/SKILL.md +50 -0
  74. package/templates/.github/copilot-instructions.md +50 -0
  75. package/templates/.mcp.json +8 -0
  76. package/templates/AGENTS.md +243 -0
  77. package/templates/CLAUDE.md +61 -0
  78. package/templates/ai/IMPLEMENTATION-PROTOCOL.md +148 -0
  79. package/templates/ai/PRODUCTION-CLEANUP.md +17 -0
  80. package/templates/ai/PROJECT-CONTEXT.md +65 -0
  81. package/templates/ai/README.md +19 -0
  82. package/templates/ai/TECHNICAL-USAGE.md +220 -0
  83. package/templates/ai/pautas/arquitectura-informacion-ux.md +243 -0
  84. package/templates/ai/pautas/buenas-practicas.md +236 -0
  85. package/templates/ai/pautas/calidad-industria.md +109 -0
  86. package/templates/ai/pautas/diseno-cliente.md +136 -0
  87. package/templates/ai/pautas/diseno-crm-lexy.md +109 -0
  88. package/templates/ai/pautas/patrones-de-codigo.md +234 -0
  89. package/templates/ai/pautas/recetas-layout.md +419 -0
  90. package/templates/ai/pautas/sistema-visual.md +197 -0
  91. package/templates/ai/pautas/ux-writing.md +214 -0
  92. package/templates/scripts/check-geometry.mjs +139 -0
  93. package/assets/fonts/noto-sans-latin.woff2 +0 -0
@@ -0,0 +1,234 @@
1
+ # Patrones de código de componentes
2
+
3
+ Patrones que mantienen legible y mantenible el código React de un proyecto Lexy: composición antes que props-monolito, estado de formulario consolidado, constantes tipadas fuera del JSX y convenciones de assets y fuentes. Nacen de la auditoría de desarrollo del bootstrap (junio 2026).
4
+
5
+ Esta pauta responde *cómo se escribe el código*; para *cuándo usar qué componente*
6
+ usa [buenas-practicas.md](buenas-practicas.md), y para *valores* (espaciado, color,
7
+ densidad) usa [sistema-visual.md](sistema-visual.md).
8
+
9
+ ---
10
+
11
+ ## 1. Composición antes que props-monolito (compound components)
12
+
13
+ Un componente que recibe decenas de props para dibujar sus partes internas
14
+ ensucia el JSX y lo vuelve ilegible. La regla Lexy: **las partes visibles se
15
+ componen como hijos; las props configuran comportamiento, no contenido**.
16
+
17
+ - Los componentes del registry ya son compuestos: úsalos así. `Card` se arma con
18
+ `CardHeader`/`CardTitle`/`CardContent`/`CardFooter`; `Dialog` con
19
+ `DialogTrigger`/`DialogContent`; `Sidebar` con `SidebarProvider`/`SidebarInset`/
20
+ `SidebarTrigger`; `Tabs` con `TabsList`/`TabsTrigger`/`TabsContent`.
21
+ - Cuando crees un componente propio con más de una zona visible (encabezado,
22
+ cuerpo, acciones), expón subcomponentes en lugar de props tipo `tituloX`,
23
+ `mostrarY` o `renderZ`.
24
+
25
+ Anti-patrón:
26
+
27
+ ```tsx
28
+ <CasoResumen
29
+ titulo="Caso 2024-113"
30
+ estado="activo"
31
+ mostrarAcciones
32
+ textoAccionPrimaria="Archivar"
33
+ onAccionPrimaria={onArchivar}
34
+ piePersonalizado={renderPie()}
35
+ />
36
+ ```
37
+
38
+ Correcto:
39
+
40
+ ```tsx
41
+ <CasoResumen densidad="compacta">
42
+ <CasoResumen.Header>
43
+ <CasoResumen.Titulo>Caso 2024-113</CasoResumen.Titulo>
44
+ <Badge variant="outline">Activo</Badge>
45
+ </CasoResumen.Header>
46
+ <CasoResumen.Body>…</CasoResumen.Body>
47
+ <CasoResumen.Footer>
48
+ <Button variant="outline" onClick={onArchivar}>
49
+ Archivar
50
+ </Button>
51
+ </CasoResumen.Footer>
52
+ </CasoResumen>
53
+ ```
54
+
55
+ ### El mecanismo: contexto compartido + dot notation
56
+
57
+ El patrón de referencia (la `Table` del CLI Lexy de desarrollo, con variantes
58
+ `normal`/`basic`/`striped` y `stickyHeader`) tiene tres piezas:
59
+
60
+ 1. **Un Context propio del componente** guarda las decisiones que comparten todas
61
+ sus partes (variante, columnas, densidad); el padre lo provee una sola vez.
62
+ 2. **Los subcomponentes se cuelgan del padre** (`Table.Header = function Header`)
63
+ y leen ese contexto con `useContext`, en vez de recibir las mismas props
64
+ repetidas en cada nivel.
65
+ 3. **El consumidor solo compone** (`<Table.Header>`, `<Table.Row>`,
66
+ `<Table.Cell>`); el estilo por variante se resuelve adentro del componente,
67
+ no en el JSX de la pantalla.
68
+
69
+ ```tsx
70
+ import { createContext, useContext, type ReactNode } from "react";
71
+
72
+ type Densidad = "normal" | "compacta";
73
+
74
+ const CasoResumenContext = createContext<{ densidad: Densidad }>({
75
+ densidad: "normal",
76
+ });
77
+
78
+ export function CasoResumen({
79
+ children,
80
+ densidad = "normal",
81
+ }: {
82
+ children: ReactNode;
83
+ densidad?: Densidad;
84
+ }) {
85
+ const ctx = { densidad };
86
+ return (
87
+ <CasoResumenContext.Provider value={ctx}>
88
+ <section className="rounded-md border border-border bg-card">
89
+ {children}
90
+ </section>
91
+ </CasoResumenContext.Provider>
92
+ );
93
+ }
94
+
95
+ CasoResumen.Header = function Header({ children }: { children: ReactNode }) {
96
+ const { densidad } = useContext(CasoResumenContext);
97
+ return (
98
+ <header className={densidad === "compacta" ? "px-4 py-2" : "px-6 py-4"}>
99
+ {children}
100
+ </header>
101
+ );
102
+ };
103
+ ```
104
+
105
+ - La variante o densidad se pasa **una sola vez** al padre y todas las partes se
106
+ ajustan solas: nada de `variantHeader`, `variantRow` ni props espejo.
107
+ - Los estilos internos usan **tokens semánticos** (`bg-card`, `border-border`,
108
+ `text-muted-foreground`), nunca colores crudos.
109
+ - Las excepciones data-driven del sistema (`AppSidebar`, `AppHeaderBar`,
110
+ `AppDialog`, `AppAccordion`) existen para casos estándar de alto nivel; no
111
+ repliques ese patrón en componentes nuevos sin necesidad real.
112
+
113
+ ## 2. Estado consolidado: nada de `useState` en cascada
114
+
115
+ Varios `useState` que pertenecen al mismo rol (los campos de un formulario, los
116
+ filtros de una tabla) fragmentan el estado, multiplican re-renders y hacen
117
+ ilegible el componente.
118
+
119
+ **Regla: tres o más `useState` del mismo dominio → un solo objeto de estado o un
120
+ custom hook.**
121
+
122
+ Anti-patrón:
123
+
124
+ ```tsx
125
+ const [nombre, setNombre] = useState("");
126
+ const [rut, setRut] = useState("");
127
+ const [email, setEmail] = useState("");
128
+ const [telefono, setTelefono] = useState("");
129
+ const [comuna, setComuna] = useState("");
130
+ ```
131
+
132
+ Correcto:
133
+
134
+ ```tsx
135
+ type FormState = {
136
+ nombre: string;
137
+ rut: string;
138
+ email: string;
139
+ telefono: string;
140
+ comuna: string;
141
+ };
142
+
143
+ const FORM_INICIAL: FormState = {
144
+ nombre: "",
145
+ rut: "",
146
+ email: "",
147
+ telefono: "",
148
+ comuna: "",
149
+ };
150
+
151
+ const [form, setForm] = useState<FormState>(FORM_INICIAL);
152
+
153
+ const setCampo = <K extends keyof FormState>(campo: K, valor: FormState[K]) =>
154
+ setForm((prev) => ({ ...prev, [campo]: valor }));
155
+ ```
156
+
157
+ - Si la lógica se repite en más de una pantalla, extráela a un custom hook
158
+ (`useFormulario`, `useFiltros`) en la ruta de hooks del proyecto.
159
+ - El estado local y el de URL siguen siendo lo primero (ver
160
+ [buenas-practicas.md](buenas-practicas.md)); esta regla es sobre la *forma* del
161
+ estado, no sobre dónde vive.
162
+
163
+ ## 3. Constantes tipadas fuera del JSX
164
+
165
+ Las listas de opciones, catálogos y textos estructurales no se declaran inline en
166
+ el JSX ni se re-tipean en cada uso. Viven en un `constants.ts` junto al feature
167
+ (o en `src/shared/constants.ts` si son transversales), con un tipo estandarizado:
168
+
169
+ ```ts
170
+ // constants.ts
171
+ import type { Decision } from "./types";
172
+
173
+ export type Opcion = {
174
+ value: Decision;
175
+ label: string;
176
+ descripcion?: string;
177
+ };
178
+
179
+ export const OPCIONES: Opcion[] = [
180
+ { value: "aprobar", label: "Aprobar", descripcion: "Continúa el flujo" },
181
+ { value: "rechazar", label: "Rechazar", descripcion: "Cierra el caso" },
182
+ ];
183
+ ```
184
+
185
+ El componente solo consume:
186
+
187
+ ```tsx
188
+ <RadioGroup>
189
+ {OPCIONES.map((opcion) => (
190
+ <RadioGroupItem key={opcion.value} value={opcion.value}>
191
+ {opcion.label}
192
+ </RadioGroupItem>
193
+ ))}
194
+ </RadioGroup>
195
+ ```
196
+
197
+ - `value` sale de un tipo de dominio (`Decision`), nunca `string` suelto; `label`
198
+ y `descripcion` son presentación.
199
+ - Si el dato participa de la experiencia (visible, editable, calculado o
200
+ filtrable), su tipo nace del **contrato de datos del prototipo**, no de una
201
+ constante ad hoc.
202
+
203
+ ## 4. Assets: dónde viven y cómo se importan
204
+
205
+ - **Arquitectura layer**: assets compartidos en `src/assets/`.
206
+ - **Arquitectura feature**: los assets de un feature viven junto al feature; los
207
+ transversales, en `src/shared/assets/`.
208
+ - **Siempre imports estáticos** (`import logo from "./assets/logo.svg"`), nunca
209
+ rutas string armadas en runtime: el import estático permite al bundler
210
+ optimizar, versionar y eliminar lo no usado (tree-shaking).
211
+ - Lo que no pasa por el bundler (favicon, fuentes) vive en `public/`.
212
+
213
+ ## 5. Fuentes autoalojadas, no CDN
214
+
215
+ Decisión de sistema: **Geist Sans y Geist Mono se sirven desde `public/fonts/`**,
216
+ no desde Google Fonts ni otro CDN.
217
+
218
+ Razones: sin dependencia de terceros ni fuga de IPs de usuarios hacia Google
219
+ (privacidad), latencia estable y funcionamiento sin conexión o en intranet. El scaffold ya deja las fuentes y su
220
+ licencia en `public/fonts/`; no agregues `<link>` a fonts.googleapis.com.
221
+
222
+ ## 6. Bundle de producción
223
+
224
+ El `vite.config.ts` del scaffold ya trae la política de build; no la elimines:
225
+
226
+ - `minify: "terser"` con `drop_console`/`drop_debugger`: nada de `console.*` ni
227
+ `debugger` en producción.
228
+ - **Code splitting por grupos de vendor** (`rolldownOptions.output.codeSplitting`):
229
+ separa `node_modules` en bundles temáticos (react-core, router, query, state,
230
+ utils, ui, radix, icons, vendor) con `minSize` 20000, para carga diferida y
231
+ mejor caché entre deploys.
232
+
233
+ Si agregas una dependencia pesada nueva, evalúa darle un grupo propio en la misma
234
+ configuración antes de dejarla caer al grupo `vendor` genérico.
@@ -0,0 +1,419 @@
1
+ # Recetas de layout Lexy
2
+
3
+ Composiciones canónicas listas para adaptar. Cada receta indica para qué **mundo**
4
+ es (cliente o CRM), qué componentes del registry usa y el patrón. Úsalas como punto
5
+ de partida; no como plantilla rígida que se pega sin pensar la tarea real.
6
+
7
+ ## Antes de copiar
8
+
9
+ - **Las recetas 1–5 y 7 existen como blocks instalables**: `npx create-lexy add
10
+ intake-wizard | confirmacion | login | crm-desk | crm-detalle-caso | crm-app-layout`
11
+ instala la vista en tu proyecto con todos sus componentes resueltos — parte de ahí
12
+ y edítala con libertad en vez de copiar el código de esta pauta.
13
+ - **Si copias a mano, instala los componentes primero.** Cada componente de la receta
14
+ debe estar en tu proyecto: `npx create-lexy add button input label …` (resuelve
15
+ dependencias internas solo).
16
+ - **Imports locales.** Los ejemplos usan el deletreo de la arquitectura *layer*
17
+ (`@/components/base/X`); en *feature* es `@/shared/components/base/X`. El patrón exacto
18
+ de tu proyecto está en `componentImportPattern` de `ai/lexy-ai-manifest.json` — síguelo.
19
+ - Aplica densidad y tokens según [sistema-visual.md](sistema-visual.md) y las reglas de [buenas-practicas.md](buenas-practicas.md).
20
+
21
+ ---
22
+
23
+ ## Shell de página y degradación móvil
24
+
25
+ Toda vista se envuelve en el mismo shell: ancho máximo, centrada, márgenes
26
+ laterales que respiran según dispositivo. Los tokens ya existen — háblalos:
27
+
28
+ ```tsx
29
+ <div className="mx-auto max-w-container-max px-margin-mobile md:px-margin-desktop">
30
+ {/* contenido de la vista */}
31
+ </div>
32
+ ```
33
+
34
+ El contrato responsive es **móvil = <768px** (`--breakpoint-md` en el theme;
35
+ en JS, `useIsMobile` del registry). Ninguna receta se entrega «solo
36
+ desktop»: cada una define qué colapsa (pauta buenas-practicas.md §5).
37
+
38
+ | Receta | Degradación móvil |
39
+ | --- | --- |
40
+ | 1. Intake / wizard | Móvil primero. Bajo `sm` las acciones apilan full-width, CTA primero. |
41
+ | 2. Confirmación | CTA full-width bajo `sm`. |
42
+ | 3. Login | Móvil primero (columna `max-w-sm`), sin cambios. |
43
+ | 4. Desk con tabla | La tabla densa pasa a **lista** (`md:hidden` / `hidden md:block`); la toolbar apila y la búsqueda es full-width. |
44
+ | 5. Detalle de caso | Las columnas apilan bajo `lg` (`flex-col lg:flex-row`): contexto arriba, trabajo debajo. |
45
+ | 7. Layout de aplicación | El sidebar se vuelve `Sheet` (lo hace el componente) y un header solo-móvil expone `SidebarTrigger`. |
46
+
47
+ Al degradar: no escondas acciones frecuentes (esconde detalle secundario),
48
+ target táctil ≥44px (`size="default"` en touch), y revisa en Storybook con
49
+ el viewport «Móvil (375px)» antes de entregar. `pnpm lint:responsive`
50
+ fiscaliza que ningún block quede desktop-only.
51
+
52
+ ## 1. Cliente — Intake / ficha web (paso de wizard)
53
+
54
+ **Mundo:** cliente. **Patrón:** un paso enfocado, columna angosta, calma, CTA al final.
55
+ **Componentes:** `Progress`, `Input`, `Label`, `Button`, `Textarea`.
56
+
57
+ ```tsx
58
+ import { Button } from "@/components/base/Button";
59
+ import { Input } from "@/components/base/Input";
60
+ import { Label } from "@/components/base/Label";
61
+ import { Progress } from "@/components/base/Progress";
62
+
63
+ export function IntakeDatosPersonales() {
64
+ return (
65
+ <main className='mx-auto w-full max-w-xl px-4 py-10'>
66
+ <div className='mb-8'>
67
+ <p className='mb-2 type-supporting text-muted-foreground'>Paso 2 de 4</p>
68
+ <Progress value={50} aria-label='Avance del formulario' />
69
+ </div>
70
+
71
+ <header className='mb-6'>
72
+ <h1 className='type-page-title text-foreground'>Cuéntanos tus datos</h1>
73
+ <p className='mt-2 type-body text-muted-foreground'>
74
+ Usaremos esta información para preparar la primera revisión de tu caso.
75
+ </p>
76
+ </header>
77
+
78
+ <form className='space-y-5'>
79
+ <fieldset className='space-y-5 border-0 p-0'>
80
+ <legend className='sr-only'>Datos personales</legend>
81
+
82
+ <div className='space-y-2'>
83
+ <Label htmlFor='nombre'>Nombre completo</Label>
84
+ <Input id='nombre' name='nombre' placeholder='Escribe tu nombre' />
85
+ </div>
86
+
87
+ <div className='space-y-2'>
88
+ <Label htmlFor='correo'>Correo</Label>
89
+ <Input id='correo' name='correo' type='email' placeholder='tu@correo.cl' />
90
+ <p className='type-meta text-muted-foreground'>
91
+ Te escribiremos solo para avanzar con tu caso.
92
+ </p>
93
+ </div>
94
+ </fieldset>
95
+
96
+ <div className='flex items-center justify-between pt-2'>
97
+ <Button type='button' variant='ghost'>Volver</Button>
98
+ <Button type='submit'>Guardar y continuar</Button>
99
+ </div>
100
+ </form>
101
+ </main>
102
+ );
103
+ }
104
+ ```
105
+
106
+ **Evita:** hero comercial, eyebrow decorativo, una card por campo, mostrar los 4 pasos
107
+ en una sola pantalla.
108
+
109
+ ---
110
+
111
+ ## 2. Cliente — Confirmación / éxito
112
+
113
+ **Mundo:** cliente. **Patrón:** celebra breve, di qué pasa ahora y dónde seguir.
114
+ **Componentes:** `Button` (e icono de lucide).
115
+
116
+ ```tsx
117
+ import { CheckCircle2 } from "lucide-react";
118
+ import { Button } from "@/components/base/Button";
119
+
120
+ export function EnvioConfirmado() {
121
+ return (
122
+ <main className='mx-auto flex min-h-[60vh] w-full max-w-md flex-col items-center justify-center px-4 text-center'>
123
+ <span className='mb-4 flex size-12 items-center justify-center rounded-full bg-success/10'>
124
+ <CheckCircle2 className='size-6 text-success' aria-hidden />
125
+ </span>
126
+ <h1 className='type-page-title text-foreground'>
127
+ ¡Enviamos tus documentos!
128
+ </h1>
129
+ <p className='mt-2 type-body text-muted-foreground'>
130
+ El equipo legal los está revisando. Podrás seguir su estado en la pestaña
131
+ «Enviados» y te avisaremos si falta algo.
132
+ </p>
133
+ <Button className='mt-6'>Volver al inicio</Button>
134
+ </main>
135
+ );
136
+ }
137
+ ```
138
+
139
+ ---
140
+
141
+ ## 3. Cliente — Acceso (login)
142
+
143
+ **Mundo:** cliente. **Patrón:** una sola tarea, foco único, ayuda contextual breve.
144
+ **Componentes:** `Card`, `Input`, `Label`, `Button`, `Logo`.
145
+
146
+ ```tsx
147
+ import { Button } from "@/components/base/Button";
148
+ import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/base/Card";
149
+ import { Input } from "@/components/base/Input";
150
+ import { Label } from "@/components/base/Label";
151
+
152
+ export function Login() {
153
+ return (
154
+ <main className='flex min-h-screen items-center justify-center bg-background px-4'>
155
+ <Card className='w-full max-w-sm'>
156
+ <CardHeader>
157
+ <CardTitle>Ingresa a Lexy</CardTitle>
158
+ <CardDescription>Te acompañamos con tu caso desde aquí.</CardDescription>
159
+ </CardHeader>
160
+ <CardContent>
161
+ <form className='space-y-4'>
162
+ <div className='space-y-2'>
163
+ <Label htmlFor='email'>Correo</Label>
164
+ <Input id='email' type='email' placeholder='tu@correo.cl' />
165
+ </div>
166
+ <div className='space-y-2'>
167
+ <Label htmlFor='pass'>Contraseña</Label>
168
+ <Input id='pass' type='password' placeholder='Ingresa tu contraseña' />
169
+ </div>
170
+ <Button type='submit' className='w-full'>Entrar</Button>
171
+ </form>
172
+ </CardContent>
173
+ </Card>
174
+ </main>
175
+ );
176
+ }
177
+ ```
178
+
179
+ ---
180
+
181
+ ## 4. CRM — Desk con tabla y acciones
182
+
183
+ **Mundo:** CRM. **Patrón:** ancho completo, toolbar con búsqueda y acción primaria,
184
+ tabla densa y escaneable con estado por fila, paginación al pie cuando hay más
185
+ registros de los que caben. **Componentes:** `Searchbox` (o `Input`), `Button`,
186
+ `Table`, `StatusDot`, `Tag`, `Pagination`.
187
+
188
+ ```tsx
189
+ import { Plus } from "lucide-react";
190
+ import { Button } from "@/components/base/Button";
191
+ import { Input } from "@/components/base/Input";
192
+ import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/base/Pagination";
193
+ import { StatusDot } from "@/components/base/StatusDot";
194
+ import { Table } from "@/components/base/Table";
195
+
196
+ export function CasosDesk() {
197
+ return (
198
+ <main className='w-full p-4'>
199
+ <header className='mb-4 flex items-center justify-between gap-4'>
200
+ <div>
201
+ <h1 className='type-subsection-title text-foreground'>Casos</h1>
202
+ <p className='type-supporting text-muted-foreground'>24 activos · 3 vencen hoy</p>
203
+ </div>
204
+ <div className='flex items-center gap-2'>
205
+ <Input className='h-9 w-64' placeholder='Buscar por cliente o RUT' aria-label='Buscar casos' />
206
+ <Button size='sm'>
207
+ <Plus /> Nuevo caso
208
+ </Button>
209
+ </div>
210
+ </header>
211
+
212
+ <Table columns='2fr 1.5fr 1fr 1fr' stickyHeader>
213
+ <Table.Header>
214
+ <Table.Cell>Cliente</Table.Cell>
215
+ <Table.Cell>Materia</Table.Cell>
216
+ <Table.Cell>Estado</Table.Cell>
217
+ <Table.Cell>Plazo</Table.Cell>
218
+ </Table.Header>
219
+ <Table.Content>
220
+ <Table.Row onClick={() => {}}>
221
+ <Table.Cell className='type-item-title text-foreground'>María Pérez</Table.Cell>
222
+ <Table.Cell>Despido</Table.Cell>
223
+ <Table.Cell><StatusDot tone='warning'>En revisión</StatusDot></Table.Cell>
224
+ <Table.Cell className='type-data text-warning-strong'>Vence hoy</Table.Cell>
225
+ </Table.Row>
226
+ <Table.Row onClick={() => {}}>
227
+ <Table.Cell className='type-item-title text-foreground'>Juan Soto</Table.Cell>
228
+ <Table.Cell>Deuda</Table.Cell>
229
+ <Table.Cell><StatusDot tone='success'>Al día</StatusDot></Table.Cell>
230
+ <Table.Cell className='type-data text-muted-foreground'>12 días</Table.Cell>
231
+ </Table.Row>
232
+ </Table.Content>
233
+ </Table>
234
+
235
+ <footer className='mt-4 flex items-center justify-between'>
236
+ <p className='type-supporting text-muted-foreground'>Mostrando 1–20 de 64 casos</p>
237
+ <Pagination className='mx-0 w-auto justify-end'>
238
+ <PaginationContent>
239
+ <PaginationItem><PaginationPrevious href='#' /></PaginationItem>
240
+ <PaginationItem><PaginationLink href='#' isActive>1</PaginationLink></PaginationItem>
241
+ <PaginationItem><PaginationLink href='#'>2</PaginationLink></PaginationItem>
242
+ <PaginationItem><PaginationLink href='#'>3</PaginationLink></PaginationItem>
243
+ <PaginationItem><PaginationNext href='#' /></PaginationItem>
244
+ </PaginationContent>
245
+ </Pagination>
246
+ </footer>
247
+ </main>
248
+ );
249
+ }
250
+ ```
251
+
252
+ **Evita:** envolver cada fila en una card, espaciado de cliente, color como único
253
+ signo de estado (por eso `StatusDot` lleva texto), y listas de cientos de filas
254
+ sin `Pagination`.
255
+
256
+ ---
257
+
258
+ ## 5. CRM — Detalle de caso (master-detail)
259
+
260
+ **Mundo:** CRM. **Patrón:** contexto a la izquierda, trabajo al centro, acciones a la
261
+ mano. La información que se consulta junta vive junta. **Componentes:** `Tabs`,
262
+ `Button`, `Separator`, `Tag`, `Avatar`.
263
+
264
+ ```tsx
265
+ import { Button } from "@/components/base/Button";
266
+ import { Separator } from "@/components/base/Separator";
267
+ import { Tag } from "@/components/base/Tag";
268
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/base/Tabs";
269
+
270
+ export function CasoDetalle() {
271
+ return (
272
+ <main className='flex w-full gap-4 p-4'>
273
+ <aside className='w-72 shrink-0 space-y-4'>
274
+ <div>
275
+ <h1 className='type-item-title text-foreground'>María Pérez</h1>
276
+ <p className='type-supporting text-muted-foreground'>Caso #1042 · Despido</p>
277
+ </div>
278
+ <Tag tone='warning'>En revisión</Tag>
279
+ <Separator />
280
+ <dl className='space-y-2 type-supporting'>
281
+ <div className='flex justify-between'>
282
+ <dt className='text-muted-foreground'>Abogado</dt>
283
+ <dd className='text-foreground'>C. Rivera</dd>
284
+ </div>
285
+ <div className='flex justify-between'>
286
+ <dt className='text-muted-foreground'>Plazo</dt>
287
+ <dd className='text-warning'>Vence hoy</dd>
288
+ </div>
289
+ </dl>
290
+ </aside>
291
+
292
+ <section className='min-w-0 flex-1'>
293
+ <div className='mb-3 flex items-center justify-between'>
294
+ <h2 className='type-section-title text-foreground'>Gestiones</h2>
295
+ <Button size='sm'>Nueva gestión</Button>
296
+ </div>
297
+ <Tabs defaultValue='actividad'>
298
+ <TabsList>
299
+ <TabsTrigger value='actividad'>Actividad</TabsTrigger>
300
+ <TabsTrigger value='documentos'>Documentos</TabsTrigger>
301
+ <TabsTrigger value='notas'>Notas</TabsTrigger>
302
+ </TabsList>
303
+ <TabsContent value='actividad'>{/* timeline */}</TabsContent>
304
+ <TabsContent value='documentos'>{/* lista */}</TabsContent>
305
+ <TabsContent value='notas'>{/* notas */}</TabsContent>
306
+ </Tabs>
307
+ </section>
308
+ </main>
309
+ );
310
+ }
311
+ ```
312
+
313
+ ---
314
+
315
+ ## 6. Estados de datos: loading, empty, error
316
+
317
+ **Mundo:** ambos (obligatorio en toda vista con datos). **Patrón:** nunca dejes una
318
+ vista en blanco mientras carga ni un vacío sin salida. **Componentes:** `Skeleton`
319
+ (layout conocido) o `Spinner` (espera puntual), `Empty`, `Button`.
320
+
321
+ ```tsx
322
+ import { FolderOpen } from "lucide-react";
323
+ import { Button } from "@/components/base/Button";
324
+ import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/base/Empty";
325
+ import { Skeleton } from "@/components/base/Skeleton";
326
+
327
+ function ListaLoading() {
328
+ // Skeleton con la forma del contenido real. Usa Spinner solo para esperas
329
+ // puntuales sin layout (p. ej. dentro de un botón al enviar).
330
+ return (
331
+ <div className='space-y-3' aria-busy='true' aria-live='polite'>
332
+ {Array.from({ length: 5 }).map((_, i) => (
333
+ <Skeleton key={i} className='h-12 w-full' />
334
+ ))}
335
+ </div>
336
+ );
337
+ }
338
+
339
+ function ListaVacia() {
340
+ return (
341
+ <Empty>
342
+ <EmptyHeader>
343
+ <EmptyMedia variant='icon'>
344
+ <FolderOpen className='size-6' aria-hidden />
345
+ </EmptyMedia>
346
+ <EmptyTitle>Aún no hay casos</EmptyTitle>
347
+ <EmptyDescription>Cuando crees un caso, aparecerá aquí.</EmptyDescription>
348
+ </EmptyHeader>
349
+ <EmptyContent>
350
+ <Button size='sm'>Crear primer caso</Button>
351
+ </EmptyContent>
352
+ </Empty>
353
+ );
354
+ }
355
+
356
+ function ListaError({ onRetry }: { onRetry: () => void }) {
357
+ return (
358
+ <Empty>
359
+ <EmptyHeader>
360
+ <EmptyTitle>No pudimos cargar los casos</EmptyTitle>
361
+ <EmptyDescription>Revisa tu conexión e inténtalo de nuevo.</EmptyDescription>
362
+ </EmptyHeader>
363
+ <EmptyContent>
364
+ <Button size='sm' variant='outline' onClick={onRetry}>
365
+ Reintentar
366
+ </Button>
367
+ </EmptyContent>
368
+ </Empty>
369
+ );
370
+ }
371
+ ```
372
+
373
+ **Regla:** cada vista que trae datos resuelve los cuatro estados —loading, vacío,
374
+ error y contenido—. El éxito puntual (guardado, envío) se comunica con `Toast`.
375
+
376
+ ---
377
+
378
+ ## 7. CRM — Layout de aplicación (sidebar + contenido)
379
+
380
+ **Mundo:** CRM. **Patrón:** estructura persistente de la app interna: navegación
381
+ lateral colapsable data-driven y área de trabajo. Las vistas de las recetas 4 y 5
382
+ viven dentro de `SidebarInset`. **Componentes:** `SidebarProvider`, `AppSidebar`,
383
+ `SidebarInset`, `Logo`.
384
+
385
+ ```tsx
386
+ import { FolderKanban, Inbox, Settings } from "lucide-react";
387
+ import { AppSidebar } from "@/components/base/AppSidebar";
388
+ import { Logo } from "@/components/base/Logo";
389
+ import { SidebarInset, SidebarProvider } from "@/components/base/Sidebar";
390
+
391
+ export function AppLayout({ children }: { children: React.ReactNode }) {
392
+ return (
393
+ <SidebarProvider>
394
+ <AppSidebar
395
+ logo={<Logo layout='horizontal' />}
396
+ groups={[
397
+ {
398
+ label: "Trabajo",
399
+ items: [
400
+ { title: "Casos", icon: FolderKanban, url: "/casos", isActive: true },
401
+ { title: "Bandeja", icon: Inbox, url: "/bandeja" },
402
+ ],
403
+ },
404
+ {
405
+ label: "Sistema",
406
+ items: [{ title: "Configuración", icon: Settings, url: "/config" }],
407
+ },
408
+ ]}
409
+ />
410
+ <SidebarInset>{children}</SidebarInset>
411
+ </SidebarProvider>
412
+ );
413
+ }
414
+ ```
415
+
416
+ **Reglas:** la navegación va data-driven con `AppSidebar` (lee `AppSidebar.md`
417
+ antes de usarla; no compongas a mano las primitivas de `Sidebar`). **Evita:**
418
+ usar este layout en pantallas de cliente sin panel lateral (ahí va
419
+ `AppHeaderBar` + secciones) y duplicar el logo dentro del contenido.