create-lexy 0.6.2 → 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.
- package/README.md +7 -2
- package/assets/fonts/{OFL-NotoSans.txt → LICENSE-Geist.txt} +4 -5
- package/assets/fonts/geist-mono-variable-italic.woff2 +0 -0
- package/assets/fonts/geist-mono-variable.woff2 +0 -0
- package/assets/fonts/geist-sans-variable-italic.woff2 +0 -0
- package/assets/fonts/geist-sans-variable.woff2 +0 -0
- package/assets/r/accordion.json +3 -3
- package/assets/r/alert-dialog.json +3 -3
- package/assets/r/app-accordion.json +1 -1
- package/assets/r/app-dialog.json +3 -3
- package/assets/r/app-header-bar.json +2 -2
- package/assets/r/app-sidebar.json +2 -2
- package/assets/r/avatar.json +3 -3
- package/assets/r/badge.json +3 -3
- package/assets/r/brand-background.json +1 -1
- package/assets/r/breadcrumb.json +3 -3
- package/assets/r/button-group.json +3 -3
- package/assets/r/button.json +3 -3
- package/assets/r/calendar.json +2 -2
- package/assets/r/card.json +3 -3
- package/assets/r/chart.json +2 -2
- package/assets/r/checkbox.json +2 -2
- package/assets/r/combobox.json +3 -3
- package/assets/r/command.json +3 -3
- package/assets/r/confirmacion.json +1 -1
- package/assets/r/counter-badge.json +3 -3
- package/assets/r/crm-desk.json +1 -1
- package/assets/r/crm-detalle-caso.json +5 -5
- package/assets/r/date-picker.json +1 -1
- package/assets/r/dialog.json +3 -3
- package/assets/r/dropdown-menu.json +3 -3
- package/assets/r/empty.json +2 -2
- package/assets/r/feature-card.json +3 -3
- package/assets/r/form.json +3 -3
- package/assets/r/header-bar.json +2 -2
- package/assets/r/input.json +3 -3
- package/assets/r/intake-wizard.json +1 -1
- package/assets/r/label.json +3 -3
- package/assets/r/logo.json +2 -2
- package/assets/r/menubar.json +3 -3
- package/assets/r/navigation-menu.json +3 -3
- package/assets/r/pagination.json +2 -2
- package/assets/r/popover.json +3 -3
- package/assets/r/profile-card.json +3 -3
- package/assets/r/progress.json +2 -2
- package/assets/r/radio-group.json +2 -2
- package/assets/r/registry.json +47 -47
- package/assets/r/scroll-area.json +2 -2
- package/assets/r/searchbox.json +3 -3
- package/assets/r/select.json +3 -3
- package/assets/r/separator.json +2 -2
- package/assets/r/sheet.json +2 -2
- package/assets/r/sidebar.json +3 -3
- package/assets/r/skeleton.json +2 -2
- package/assets/r/slider.json +3 -3
- package/assets/r/snippet.json +3 -3
- package/assets/r/spinner.json +2 -2
- package/assets/r/status-dot.json +3 -3
- package/assets/r/switch.json +3 -3
- package/assets/r/table.json +3 -3
- package/assets/r/tabs.json +3 -3
- package/assets/r/tag.json +3 -3
- package/assets/r/textarea.json +3 -3
- package/assets/r/toaster.json +3 -3
- package/assets/r/tooltip.json +3 -3
- package/assets/r/tree.json +3 -3
- package/assets/registry-version +1 -1
- package/assets/theme/lexy-theme.css +562 -112
- package/dist/index.js +97 -91
- package/package.json +4 -2
- package/templates/.claude/skills/lexy-design/SKILL.md +189 -0
- package/templates/.claude/skills/lexy-dev/SKILL.md +168 -0
- package/templates/.claude/skills/lexy-mock-data/SKILL.md +50 -0
- package/templates/.github/copilot-instructions.md +50 -0
- package/templates/.mcp.json +8 -0
- package/templates/AGENTS.md +243 -0
- package/templates/CLAUDE.md +61 -0
- package/templates/ai/IMPLEMENTATION-PROTOCOL.md +148 -0
- package/templates/ai/PRODUCTION-CLEANUP.md +17 -0
- package/templates/ai/PROJECT-CONTEXT.md +65 -0
- package/templates/ai/README.md +19 -0
- package/templates/ai/TECHNICAL-USAGE.md +220 -0
- package/templates/ai/pautas/arquitectura-informacion-ux.md +243 -0
- package/templates/ai/pautas/buenas-practicas.md +236 -0
- package/templates/ai/pautas/calidad-industria.md +109 -0
- package/templates/ai/pautas/diseno-cliente.md +136 -0
- package/templates/ai/pautas/diseno-crm-lexy.md +109 -0
- package/templates/ai/pautas/patrones-de-codigo.md +234 -0
- package/templates/ai/pautas/recetas-layout.md +419 -0
- package/templates/ai/pautas/sistema-visual.md +197 -0
- package/templates/ai/pautas/ux-writing.md +214 -0
- package/templates/scripts/check-geometry.mjs +139 -0
- 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.
|