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.
- 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 +590 -276
- 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
package/README.md
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
# create-lexy
|
|
2
2
|
|
|
3
|
-
CLI del **Lexy Design System**. Crea proyectos React + Vite + TS con el theme Lexy y trae componentes a demanda desde el registry (`@lexydesign/registry`, modelo shadcn): el código vive en tu proyecto, lo ves antes de instalarlo y lo editas con libertad.
|
|
3
|
+
CLI del **Lexy Design System**. Crea proyectos React + Vite + TS con el theme Lexy y trae componentes a demanda desde el registry (`@lexydesign/registry`, modelo shadcn): el código vive en tu proyecto, lo ves antes de instalarlo y lo editas con libertad. También instala la capa de IA (pautas, skills y entry points para agentes) — no hace falta ningún paquete aparte para eso.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
+
npx create-lexy # sin flags: menú interactivo (detecta si ya hay .lexy en la carpeta)
|
|
7
|
+
npx create-lexy kickoff # deja un .md para que un agente entreviste y arranque el proyecto por ti
|
|
6
8
|
npx create-lexy create mi-app # proyecto vacío con theme + .lexy
|
|
9
|
+
npx create-lexy install-ai # pautas, skills y entry points para agentes (AGENTS.md, CLAUDE.md, .claude/skills/...)
|
|
7
10
|
npx create-lexy view button # código + doc antes de instalar
|
|
8
11
|
npx create-lexy add button # lo instala local y editable, con sus deps
|
|
9
12
|
npx create-lexy diff button # copia local vs registry vigente
|
|
10
13
|
npx create-lexy doctor # drift de todo lo instalado (exit 1 para CI)
|
|
11
14
|
```
|
|
12
15
|
|
|
13
|
-
Todos los comandos funcionan con TUI interactiva para humanos y con flags para agentes/CI (`create mi-app -t feature -w crm`, `add button --overwrite`, `doctor --strict`).
|
|
16
|
+
Todos los comandos funcionan con TUI interactiva para humanos y con flags para agentes/CI (`create mi-app -t feature -w crm`, `add button --overwrite`, `doctor --strict`, `install-ai --force`). El menú interactivo (`npx create-lexy` sin argumentos) es solo una capa de comodidad sobre esos mismos comandos — un agente nunca necesita pasar por ahí, siempre puede ir directo con flags.
|
|
17
|
+
|
|
18
|
+
La única dependencia real para correr `npx create-lexy` es **Node.js** (`npx` viene con npm, que viene con Node). El CLI usa **pnpm** para instalar dependencias del proyecto; si no lo detecta, lo instala él mismo (o te pregunta primero, en terminal interactiva).
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
Copyright
|
|
1
|
+
Copyright (c) 2023 Vercel, in collaboration with basement.studio
|
|
2
2
|
|
|
3
3
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
4
|
This license is copied below, and is also available with a FAQ at:
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
7
6
|
|
|
8
7
|
-----------------------------------------------------------
|
|
9
8
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
@@ -18,7 +17,7 @@ with others.
|
|
|
18
17
|
|
|
19
18
|
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
19
|
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
-
fonts, including any derivative works, can be bundled, embedded,
|
|
20
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
21
|
redistributed and/or sold with any software provided that any reserved
|
|
23
22
|
names are not used by derivative works. The fonts and derivatives,
|
|
24
23
|
however, cannot be released under any other type of license. The
|
|
@@ -44,7 +43,7 @@ new environment.
|
|
|
44
43
|
"Author" refers to any designer, engineer, programmer, technical
|
|
45
44
|
writer or other person who contributed to the Font Software.
|
|
46
45
|
|
|
47
|
-
PERMISSION
|
|
46
|
+
PERMISSION AND CONDITIONS
|
|
48
47
|
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
48
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
49
|
redistribute, and sell modified and unmodified copies of the Font
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/assets/r/accordion.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "accordion",
|
|
4
4
|
"type": "registry:ui",
|
|
5
5
|
"title": "Accordion",
|
|
6
|
-
"description": "Secciones colapsables para mostrar detalle bajo demanda (Radix). Progressive disclosure: muestra primero los títulos y despliega el contenido al pedirlo.
|
|
6
|
+
"description": "Secciones colapsables para mostrar detalle bajo demanda (Radix). Progressive disclosure: muestra primero los títulos y despliega el contenido al pedirlo.",
|
|
7
7
|
"dependencies": [
|
|
8
8
|
"@radix-ui/react-accordion"
|
|
9
9
|
],
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
"path": "components/base/Accordion.tsx",
|
|
14
14
|
"type": "registry:ui",
|
|
15
15
|
"target": "components/base/Accordion.tsx",
|
|
16
|
-
"content": "import * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nconst Accordion = AccordionPrimitive.Root;\n\nconst AccordionItem = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>\n>(({ className, ...props }, ref) => (\n <AccordionPrimitive.Item\n ref={ref}\n className={cn(\"border-b border-border\", className)}\n {...props}\n />\n));\nAccordionItem.displayName = \"AccordionItem\";\n\nconst AccordionTrigger = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n <AccordionPrimitive.Header className=\"flex w-full\">\n <AccordionPrimitive.Trigger\n ref={ref}\n className={cn(\n \"flex flex-1 cursor-pointer items-center justify-between py-4 text-left
|
|
16
|
+
"content": "import * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nconst Accordion = AccordionPrimitive.Root;\n\nconst AccordionItem = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>\n>(({ className, ...props }, ref) => (\n <AccordionPrimitive.Item\n ref={ref}\n className={cn(\"border-b border-border\", className)}\n {...props}\n />\n));\nAccordionItem.displayName = \"AccordionItem\";\n\nconst AccordionTrigger = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n <AccordionPrimitive.Header className=\"flex w-full\">\n <AccordionPrimitive.Trigger\n ref={ref}\n className={cn(\n \"flex flex-1 cursor-pointer items-center justify-between py-4 text-left type-action-label text-foreground transition-all [&[data-state=open]>svg]:rotate-180\",\n className,\n )}\n {...props}\n >\n {children}\n <svg\n className=\"size-(--icon-size-inline) shrink-0 text-muted-foreground transition-transform duration-[var(--duration-normal)] ease-[var(--ease-standard)] motion-reduce:transition-none\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n));\nAccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;\n\nconst AccordionContent = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n <AccordionPrimitive.Content\n ref={ref}\n className=\"overflow-hidden type-supporting text-muted-foreground data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down\"\n {...props}\n >\n <div className={cn(\"pb-4 pt-0\", className)}>{children}</div>\n </AccordionPrimitive.Content>\n));\nAccordionContent.displayName = AccordionPrimitive.Content.displayName;\n\nexport { Accordion, AccordionContent, AccordionItem, AccordionTrigger };\n"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"path": "components/base/Accordion.md",
|
|
20
20
|
"type": "registry:file",
|
|
21
21
|
"target": "components/base/Accordion.md",
|
|
22
|
-
"content": "# Accordion
|
|
22
|
+
"content": "# Accordion\n\nSecciones colapsables para mostrar detalle bajo demanda (Radix). Progressive disclosure: muestra primero los títulos y despliega el contenido al pedirlo.\n\n## Cuándo usarlo\n\nPara revelar detalle bajo demanda cuando los títulos bastan para orientar: FAQs, secciones de ayuda, metadatos de un caso. Progressive disclosure: la persona ve primero el mapa (los títulos) y abre solo lo que le interesa, sin abandonar la página.\n\n## Composición\n\nCuatro piezas: `Accordion` (contenedor que gobierna el estado), un `AccordionItem` por sección, y dentro de cada una `AccordionTrigger` (el título clickeable con chevron) y `AccordionContent` (el detalle que se pliega). Para el caso data-driven usa `AppAccordion`, que arma esta composición desde un arreglo de items.\n\n## Uso básico\n\n```tsx\n<Accordion type=\"single\" collapsible>\n <AccordionItem value=\"envio\">\n <AccordionTrigger>¿Cuánto tarda el envío?</AccordionTrigger>\n <AccordionContent>Entre 3 y 5 días hábiles.</AccordionContent>\n </AccordionItem>\n <AccordionItem value=\"devolucion\">\n <AccordionTrigger>¿Puedo devolver un producto?</AccordionTrigger>\n <AccordionContent>Sí, dentro de 30 días.</AccordionContent>\n </AccordionItem>\n</Accordion>\n```\n\n## Varias secciones abiertas\n\n```tsx\n<Accordion type=\"multiple\" defaultValue={[\"envio\"]}>\n …\n</Accordion>\n```\n\n## Reglas\n\n- Cada `AccordionItem` necesita `value` único.\n- El `AccordionTrigger` lleva un título escaneable (sentence case), no una frase larga.\n- No escondas información crítica (costos, errores, próximos pasos) dentro de secciones cerradas.\n- Úsalo para detalle secundario u opcional; el contenido principal va visible.\n\n## Cuándo NO usar\n\n- **Categorías pares que se alternan** → `Tabs`.\n- **Una sola sección colapsable puntual** → basta un toggle simple; el accordion es para varias.\n- **Navegación lateral con jerarquía** → `AppSidebar` (grupos colapsables).\n- **Acción puntual en una capa** → `Dialog`/`Sheet`.\n\n## Import\n\n```tsx\nimport {\n Accordion, AccordionItem, AccordionTrigger, AccordionContent,\n} from \"@/components/base/Accordion\";\n```\n\n## Props (esenciales)\n\n| Parte | Prop | Descripción |\n|---|---|---|\n| `Accordion` | `type` | `\"single\"` (una abierta) o `\"multiple\"` (varias). **Requerido.** |\n| `Accordion` | `collapsible` | En `single`, permite cerrar la abierta. |\n| `Accordion` | `value` / `defaultValue` | Sección(es) abierta(s). |\n| `AccordionItem` | `value` | **Requerido.** Identificador único. |\n\n## Para IA\n\n1. Úsalo para FAQ o detalle opcional que conviene plegar.\n2. Elige `type=\"single\" collapsible` para una a la vez, `multiple` si conviene comparar.\n3. Da `value` único a cada ítem y títulos escaneables al trigger.\n4. Nunca ocultes información bloqueante o de riesgo dentro del accordion.\n"
|
|
23
23
|
}
|
|
24
24
|
]
|
|
25
25
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "alert-dialog",
|
|
4
4
|
"type": "registry:ui",
|
|
5
5
|
"title": "AlertDialog",
|
|
6
|
-
"description": "Confirmación modal que **interrumpe** (rol `alertdialog`): la persona debe elegir entre cancelar o confirmar — no hay X, ni cierre con click afuera. Para destruir, descartar cambios o salir de un flujo con costo.
|
|
6
|
+
"description": "Confirmación modal que **interrumpe** (rol `alertdialog`): la persona debe elegir entre cancelar o confirmar — no hay X, ni cierre con click afuera. Para destruir, descartar cambios o salir de un flujo con costo.",
|
|
7
7
|
"dependencies": [
|
|
8
8
|
"@radix-ui/react-alert-dialog",
|
|
9
9
|
"class-variance-authority"
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
"path": "components/base/AlertDialog.tsx",
|
|
17
17
|
"type": "registry:ui",
|
|
18
18
|
"target": "components/base/AlertDialog.tsx",
|
|
19
|
-
"content": "import * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\";\nimport type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nimport { buttonVariants } from \"./Button\";\n\n// Confirmación modal que interrumpe (rol alertdialog): sin X, no se cierra\n// con click en el overlay ni Escape implícito — la persona debe elegir.\n// Para diálogos de trabajo (formularios, contenido) usa Dialog/AppDialog.\n\nconst AlertDialog = AlertDialogPrimitive.Root;\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger;\nconst AlertDialogPortal = AlertDialogPrimitive.Portal;\n\nconst AlertDialogOverlay = React.forwardRef<\n React.ElementRef<typeof AlertDialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Overlay\n ref={ref}\n className={cn(\n \"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n className,\n )}\n {...props}\n />\n));\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;\n\nconst AlertDialogContent = React.forwardRef<\n React.ElementRef<typeof AlertDialogPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ className, ...props }, ref) => (\n <AlertDialogPortal>\n <AlertDialogOverlay />\n <AlertDialogPrimitive.Content\n ref={ref}\n className={cn(\n \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-
|
|
19
|
+
"content": "import * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\";\nimport type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nimport { buttonVariants } from \"./Button\";\n\n// Confirmación modal que interrumpe (rol alertdialog): sin X, no se cierra\n// con click en el overlay ni Escape implícito — la persona debe elegir.\n// Para diálogos de trabajo (formularios, contenido) usa Dialog/AppDialog.\n\nconst AlertDialog = AlertDialogPrimitive.Root;\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger;\nconst AlertDialogPortal = AlertDialogPrimitive.Portal;\n\nconst AlertDialogOverlay = React.forwardRef<\n React.ElementRef<typeof AlertDialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Overlay\n ref={ref}\n className={cn(\n \"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n className,\n )}\n {...props}\n />\n));\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;\n\nconst AlertDialogContent = React.forwardRef<\n React.ElementRef<typeof AlertDialogPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ className, ...props }, ref) => (\n <AlertDialogPortal>\n <AlertDialogOverlay />\n <AlertDialogPrimitive.Content\n ref={ref}\n className={cn(\n \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-grouped border border-border bg-background p-6 shadow-overlay duration-[var(--duration-overlay)] ease-[var(--ease-standard)] motion-reduce:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-xl\",\n className,\n )}\n {...props}\n />\n </AlertDialogPortal>\n));\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;\n\nconst AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n <div className={cn(\"flex flex-col gap-related text-center sm:text-left\", className)} {...props} />\n);\nAlertDialogHeader.displayName = \"AlertDialogHeader\";\n\nconst AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\"flex flex-col-reverse gap-related sm:flex-row sm:justify-end\", className)}\n {...props}\n />\n);\nAlertDialogFooter.displayName = \"AlertDialogFooter\";\n\nconst AlertDialogTitle = React.forwardRef<\n React.ElementRef<typeof AlertDialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Title\n ref={ref}\n className={cn(\"type-dialog-title text-foreground\", className)}\n {...props}\n />\n));\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;\n\nconst AlertDialogDescription = React.forwardRef<\n React.ElementRef<typeof AlertDialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Description\n ref={ref}\n className={cn(\"type-supporting text-muted-foreground\", className)}\n {...props}\n />\n));\nAlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;\n\nexport interface AlertDialogActionProps\n extends\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>,\n VariantProps<typeof buttonVariants> {}\n\n/** Acción que confirma. Para acciones irreversibles usa `variant=\"destructive\"`. */\nconst AlertDialogAction = React.forwardRef<\n React.ElementRef<typeof AlertDialogPrimitive.Action>,\n AlertDialogActionProps\n>(({ className, variant, size, ...props }, ref) => (\n <AlertDialogPrimitive.Action\n ref={ref}\n className={cn(buttonVariants({ variant, size }), className)}\n {...props}\n />\n));\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;\n\nconst AlertDialogCancel = React.forwardRef<\n React.ElementRef<typeof AlertDialogPrimitive.Cancel>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Cancel\n ref={ref}\n className={cn(buttonVariants({ variant: \"ghost\" }), className)}\n {...props}\n />\n));\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;\n\nexport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogOverlay,\n AlertDialogPortal,\n AlertDialogTitle,\n AlertDialogTrigger,\n};\n"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"path": "components/base/AlertDialog.md",
|
|
23
23
|
"type": "registry:file",
|
|
24
24
|
"target": "components/base/AlertDialog.md",
|
|
25
|
-
"content": "# AlertDialog
|
|
25
|
+
"content": "# AlertDialog\nConfirmación modal que **interrumpe** (rol `alertdialog`): la persona debe elegir entre cancelar o confirmar — no hay X, ni cierre con click afuera. Para destruir, descartar cambios o salir de un flujo con costo.\n\n## Cuándo usarlo\n\nPara confirmaciones que interrumpen de verdad: destruir, descartar cambios, salir de un flujo con costo. No hay X ni cierre con click afuera — la persona debe decidir. Si la acción es reversible o informativa, `AppDialog` basta.\n\n## Composición\n\nComposición Radix: `AlertDialogTrigger` abre `AlertDialogContent`, con `AlertDialogHeader` (`AlertDialogTitle`, `AlertDialogDescription`) y `AlertDialogFooter` con `AlertDialogCancel` y `AlertDialogAction`.\n\n## Uso básico (acción destructiva)\n\n```tsx\n<AlertDialog>\n <AlertDialogTrigger asChild>\n <Button variant=\"destructive\">Eliminar contrato</Button>\n </AlertDialogTrigger>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>¿Eliminar este contrato?</AlertDialogTitle>\n <AlertDialogDescription>\n Se eliminará de forma permanente, junto con sus documentos adjuntos.\n Esta acción no se puede deshacer.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Cancelar</AlertDialogCancel>\n <AlertDialogAction variant=\"destructive\" onClick={eliminar}>\n Eliminar contrato\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n</AlertDialog>\n```\n\n## Confirmación no destructiva\n\n```tsx\n<AlertDialogAction onClick={confirmar}>Enviar ahora</AlertDialogAction>\n```\n\n`AlertDialogAction` acepta las variantes de `Button` (`variant`/`size`); por defecto es la acción primaria. `AlertDialogCancel` siempre es `ghost`.\n\n## AlertDialog vs Dialog\n\n- **AlertDialog**: decisión bloqueante con consecuencias (eliminar, descartar, salir sin guardar). Escape/click afuera **no** cierran; siempre dos salidas explícitas.\n- **Dialog / AppDialog**: trabajo dentro de un modal (formularios, detalle, selección). Se puede cerrar con Escape o click en el overlay sin perder nada importante.\n- Si el \"confirmar\" requiere ingresar datos (motivo, contraseña), ya no es un AlertDialog: usa `Dialog` con formulario.\n\n## Reglas\n\n- El título es la pregunta concreta (*\"¿Eliminar este contrato?\"*), la descripción explica la consecuencia (*\"…no se puede deshacer\"*).\n- La acción repite el verbo del título (*\"Eliminar contrato\"*), nunca \"Aceptar\"/\"OK\".\n- Acción irreversible → `variant=\"destructive\"`.\n- No abuses: reserva la interrupción para decisiones con costo real; el éxito posterior se comunica con `Toaster`.\n\n## Import\n\n```tsx\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger,\n} from \"@/components/base/AlertDialog\";\n```\n\n## Props\n\n| Pieza | Qué es |\n|---|---|\n| `AlertDialog` / `AlertDialogTrigger` | Primitivas de Radix (`asChild` disponible en el trigger). |\n| `AlertDialogContent` | Panel modal, mismo estilo que `DialogContent`. `className`, `...props`, `ref`. |\n| `AlertDialogHeader` / `AlertDialogFooter` | Layout de título/descripcion y acciones. |\n| `AlertDialogTitle` / `AlertDialogDescription` | Texto accesible del diálogo (obligatorios). |\n| `AlertDialogAction` | Botón que confirma y cierra. `variant`/`size` de `buttonVariants`. |\n| `AlertDialogCancel` | Botón que cancela y cierra (ghost). |\n\n## Para IA\n\n1. Úsalo solo para confirmar acciones con consecuencias; el resto va en `Dialog`/`AppDialog`.\n2. Título = pregunta con el verbo de la acción; descripción = consecuencia concreta; botón = mismo verbo.\n3. `AlertDialogTrigger asChild` sobre un `Button` existente.\n4. Tras confirmar, comunica el resultado (toast de éxito o error).\n"
|
|
26
26
|
}
|
|
27
27
|
]
|
|
28
28
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"path": "components/base/AppAccordion.md",
|
|
20
20
|
"type": "registry:file",
|
|
21
21
|
"target": "components/base/AppAccordion.md",
|
|
22
|
-
"content": "# AppAccordion
|
|
22
|
+
"content": "# AppAccordion\nAcordeón de aplicación **data-driven**. Describe los items como datos (`items`)\ny el componente compone internamente las primitivas de `Accordion`. **Esta es la\nvía a usar para construir acordeones.** No compongas a mano `AccordionItem`,\n`AccordionTrigger` y `AccordionContent` si `AppAccordion` cubre el caso.\n\n## Cuándo usarlo\n\nPara acordeones descritos como datos: FAQs, filtros colapsables, paneles de ayuda. Describes los items (título, contenido, icono) y el componente arma la composición — ideal para contenido que viene de un catálogo o CMS.\n\n## Composición\n\nUna sola pieza data-driven (`AppAccordion`) que arma por debajo la composición de `Accordion`: recibe `items` (título, contenido, icono opcional), modos single/multiple y variante containered.\n\n## Uso básico\n\n```tsx\n<AppAccordion\n items={[\n {\n value: \"item-1\",\n title: \"¿Qué es Lexy?\",\n content: \"Lexy es un sistema de diseño para construir interfaces consistentes.\",\n },\n {\n value: \"item-2\",\n title: \"¿Cómo instalar?\",\n content: \"Usa create-lexy para inicializar un proyecto.\",\n },\n ]}\n/>\n```\n\n## Uso múltiple\n\n```tsx\n<AppAccordion\n type=\"multiple\"\n items={[\n {\n value: \"filters\",\n title: \"Filtros\",\n content: <FilterForm />,\n },\n {\n value: \"sort\",\n title: \"Ordenamiento\",\n content: <SortOptions />,\n },\n ]}\n/>\n```\n\n## Con iconos\n\n```tsx\nimport { CircleHelp, Settings } from \"lucide-react\";\n\n<AppAccordion\n items={[\n {\n value: \"help\",\n title: \"Ayuda\",\n icon: <CircleHelp className=\"h-4 w-4\" />,\n content: \"Respuestas a preguntas frecuentes.\",\n },\n {\n value: \"settings\",\n title: \"Configuración\",\n icon: <Settings className=\"h-4 w-4\" />,\n content: \"Ajusta las preferencias de tu cuenta.\",\n },\n ]}\n/>\n```\n\n## Containered\n\n```tsx\n<AppAccordion\n containered\n items={[\n { value: \"faq-1\", title: \"¿Cómo creo una cuenta?\", content: \"Haz click en 'Crear cuenta'.\" },\n { value: \"faq-2\", title: \"¿Puedo cancelar?\", content: \"Sí, desde Configuración.\" },\n ]}\n/>\n```\n\n## Receta compuesta\n\n`AppAccordion` es un wrapper ergonómico sobre el `Accordion` compuesto: las props extra (`aria-*`, `id`…) pasan al nodo raíz y el `ref` apunta a ese nodo. Si necesitas variar la estructura (triggers custom, contenido anidado, estilos por item), usa las piezas directamente:\n\n```tsx\nimport { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from \"@/components/base/Accordion\";\n```\n\n## Reglas\n\n- Define los items como datos y pásalos en `items`.\n- Usa `type=\"multiple\"` si necesitas que varios items estén abiertos simultáneamente.\n- Usa `value` como identificador único (no como índice).\n- `content` puede ser texto simple o componentes complejos (formularios, etc.).\n- Usa `disabled` para items no disponibles.\n- Para casos a medida que no encajen en `items`, usa las primitivas de `Accordion` directamente.\n\n## Import\n\n```tsx\nimport { AppAccordion } from \"@/components/base/AppAccordion\";\n```\n\n## Props\n\n### `AppAccordion`\n\n| Prop | Tipo | Default | Descripción |\n|---|---|---|---|\n| `items` | `AppAccordionItem[]` | — | **Requerido.** Items del acordeón. |\n| `type` | `\"single\" \\| \"multiple\"` | `\"single\"` | Tipo de acordeón. |\n| `defaultValue` | `string?` | — | Valor abierto por defecto (modo \"single\"). |\n| `defaultValues` | `string[]?` | — | Valores abiertos por defecto (modo \"multiple\"). |\n| `onValueChange` | `(value) => void` | — | Callback al cambiar item abierto. |\n| `containered` | `boolean` | `false` | Envuelve en contenedor con borde, fondo y bordes redondeados. |\n| `collapsible` | `boolean` | `true` | Permite colapsar el item abierto (modo \"single\"). |\n| `className` | `string?` | — | Clase adicional para el contenedor. |\n| `itemClassName` | `string?` | — | Clase adicional para cada item. |\n\n### `AppAccordionItem`\n\n| Campo | Tipo | Descripción |\n|---|---|---|\n| `value` | `string` | Identificador único del item. |\n| `title` | `string` | Título visible del item. |\n| `icon` | `ReactNode?` | Icono decorativo antes del título. |\n| `content` | `ReactNode` | Contenido del item. |\n| `disabled` | `boolean?` | Deshabilita el item. |\n\n## Para IA\n\n1. Identifica si el acordeón es FAQ, filtros, configuración o navegación.\n2. Si es FAQ/configuración → usa `AppAccordion`.\n3. Crea `AppAccordionItem[]` con `value`, `title` y `content`.\n4. Usa `icon` para agregar un icono decorativo antes del título de cada item.\n5. Usa `containered` para envolver el acordeón en un contenedor visual (borde, fondo, bordes redondeados).\n6. Usa `type=\"multiple\"` si el usuario necesita comparar contenido entre items.\n7. Usa `defaultValue` para abrir el primer item por defecto (útil para FAQs).\n8. Si el contenido es un formulario, pasa el componente como `content`.\n9. Solo usa `Accordion` primitivo si necesitas animaciones custom o layouts atípicos.\n"
|
|
23
23
|
}
|
|
24
24
|
]
|
|
25
25
|
}
|
package/assets/r/app-dialog.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "app-dialog",
|
|
4
4
|
"type": "registry:ui",
|
|
5
5
|
"title": "AppDialog",
|
|
6
|
-
"description": "Diálogo de aplicación al estilo **Material 3**. Estructura canónica sin dividers:
|
|
6
|
+
"description": "Diálogo de aplicación al estilo **Material 3**. Estructura canónica sin dividers:",
|
|
7
7
|
"dependencies": [],
|
|
8
8
|
"registryDependencies": [
|
|
9
9
|
"@lexy/button",
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
"path": "components/base/AppDialog.tsx",
|
|
15
15
|
"type": "registry:ui",
|
|
16
16
|
"target": "components/base/AppDialog.tsx",
|
|
17
|
-
"content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nimport { Button } from \"./Button\";\nimport { Dialog, DialogContent, DialogDescription, DialogTitle, DialogTrigger } from \"./Dialog\";\n\nexport interface AppDialogProps extends Omit<\n React.ComponentPropsWithoutRef<typeof DialogContent>,\n \"title\" | \"children\"\n> {\n /** Elemento que dispara el diálogo (botón, enlace, etc.). */\n trigger: React.ReactNode;\n /** Icono opcional arriba del título (sin fondo, centrado). */\n icon?: React.ReactNode;\n /** Título del diálogo
|
|
17
|
+
"content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nimport { Button } from \"./Button\";\nimport { Dialog, DialogContent, DialogDescription, DialogTitle, DialogTrigger } from \"./Dialog\";\n\nexport interface AppDialogProps extends Omit<\n React.ComponentPropsWithoutRef<typeof DialogContent>,\n \"title\" | \"children\"\n> {\n /** Elemento que dispara el diálogo (botón, enlace, etc.). */\n trigger: React.ReactNode;\n /** Icono opcional arriba del título (sin fondo, centrado). */\n icon?: React.ReactNode;\n /** Título del diálogo. Usa la escala compacta 22/28. */\n title: string;\n /** Descripción o texto de soporte. Consume el rol `type-supporting`. */\n description?: string;\n /** Contenido principal del diálogo (formularios, texto, etc.). */\n children?: React.ReactNode;\n /** Texto del botón de acción principal (derecha). */\n confirmLabel?: string;\n /** Texto del botón de cancelar (izquierda). */\n cancelLabel?: string;\n /** Variante del botón de confirmar. */\n confirmVariant?: \"default\" | \"destructive\" | \"outline\" | \"secondary\" | \"ghost\" | \"link\";\n /** Deshabilita el botón de confirmar. */\n confirmDisabled?: boolean;\n /** Callback al confirmar. Si devuelve `false`, el diálogo no se cierra. */\n onConfirm?: () => boolean | void | Promise<boolean | void>;\n /** Callback al cancelar o cerrar. */\n onCancel?: () => void;\n /** Alineación de los botones de acción. */\n actionsAlignment?: \"right\" | \"center\" | \"left\" | \"space-between\";\n /** Controla el estado abierto/cerrado (modo controlado). */\n open?: boolean;\n /** Callback cuando cambia el estado abierto. */\n onOpenChange?: (open: boolean) => void;\n /** Clase adicional para el contenido del diálogo. */\n className?: string;\n /** Si es `true`, oculta el footer con los botones de acción. */\n hideFooter?: boolean;\n}\n\n/**\n * Diálogo de aplicación al estilo Material 3.\n *\n * Estructura canónica (sin dividers):\n * 1. Icono opcional (sin fondo, centrado)\n * 2. Título compacto (`type-subsection-title`, 22/28)\n * 3. Supporting text (`type-supporting`)\n * 4. Content area (scrollable si es necesario)\n * 5. Action area (botones, sin divider, alineación configurable)\n *\n * Si hay `icon`, el headline se centra. Si no hay icono, el headline se alinea\n * a la izquierda. Supporting text y acciones usan roles distintos aunque\n * compartan 14/20: regular para lectura y medium para intención de acción.\n *\n * Wrapper ergonómico sobre el Dialog compuesto: las props extra (`aria-*`,\n * `onEscapeKeyDown`…) pasan al DialogContent y el `ref` apunta a ese nodo.\n * Para estructuras distintas, usa Dialog/DialogContent/DialogFooter directo.\n */\nconst AppDialog = React.forwardRef<React.ElementRef<typeof DialogContent>, AppDialogProps>(\n function AppDialog(\n {\n trigger,\n icon,\n title,\n description,\n children,\n confirmLabel = \"Confirmar\",\n cancelLabel = \"Cancelar\",\n confirmVariant = \"default\",\n confirmDisabled,\n onConfirm,\n onCancel,\n actionsAlignment = \"right\",\n open,\n onOpenChange,\n className,\n hideFooter,\n ...props\n },\n ref,\n ) {\n const [internalOpen, setInternalOpen] = React.useState(false);\n const isControlled = open !== undefined;\n const isOpen = isControlled ? open : internalOpen;\n\n const handleOpenChange = (next: boolean) => {\n if (!next) {\n onCancel?.();\n }\n if (!isControlled) {\n setInternalOpen(next);\n }\n onOpenChange?.(next);\n };\n\n const handleConfirm = async () => {\n const result = onConfirm?.();\n const shouldClose = result instanceof Promise ? await result : result;\n if (shouldClose !== false) {\n handleOpenChange(false);\n }\n };\n\n const alignmentClasses = {\n right: \"justify-end\",\n center: \"justify-center\",\n left: \"justify-start\",\n \"space-between\": \"justify-between\",\n };\n\n return (\n <Dialog open={isOpen} onOpenChange={handleOpenChange}>\n <DialogTrigger asChild>{trigger}</DialogTrigger>\n <DialogContent ref={ref} className={cn(\"sm:max-w-md\", className)} {...props}>\n {/* Icono opcional (sin fondo, centrado) */}\n {icon && (\n <div className=\"flex justify-center\">\n <div className=\"text-foreground\">{icon}</div>\n </div>\n )}\n\n {/* Headline + Supporting text */}\n <div className={cn(\"flex flex-col gap-related\", icon ? \"text-center\" : \"text-left\")}>\n <DialogTitle\n scale=\"compact\"\n className={cn(\"text-foreground\", icon && \"text-center\")}\n >\n {title}\n </DialogTitle>\n {description && (\n <DialogDescription\n className={cn(\"text-muted-foreground\", icon && \"text-center\")}\n >\n {description}\n </DialogDescription>\n )}\n </div>\n\n {/* Content area */}\n {children && <div className=\"max-h-[60vh] overflow-y-auto\">{children}</div>}\n\n {/* Action area (sin dividers). `gap-related` une las acciones y el\n `gap-grouped` de DialogContent separa las regiones del overlay. */}\n {!hideFooter && (\n <div\n className={cn(\n \"flex flex-col-reverse gap-related sm:flex-row\",\n alignmentClasses[actionsAlignment],\n )}\n >\n <Button variant=\"ghost\" onClick={() => handleOpenChange(false)}>\n {cancelLabel}\n </Button>\n <Button variant={confirmVariant} disabled={confirmDisabled} onClick={handleConfirm}>\n {confirmLabel}\n </Button>\n </div>\n )}\n </DialogContent>\n </Dialog>\n );\n },\n);\nAppDialog.displayName = \"AppDialog\";\n\nexport { AppDialog };\n"
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
"path": "components/base/AppDialog.md",
|
|
21
21
|
"type": "registry:file",
|
|
22
22
|
"target": "components/base/AppDialog.md",
|
|
23
|
-
"content": "# AppDialog
|
|
23
|
+
"content": "# AppDialog\nDiálogo de aplicación al estilo **Material 3**. Estructura canónica sin dividers:\n\n1. **Icono** (opcional, sin fondo, centrado)\n2. **Título compacto** (22/28, centrado si hay icono)\n3. **Supporting text** (text-sm, igual que botones)\n4. **Content area** (scrollable si es necesario)\n5. **Action area** (botones, sin divider, alineación configurable)\n\n## Cuándo usarlo\n\nPara diálogos de aplicación con la estructura ya resuelta: icono, título, descripción y acciones al estilo Material 3. La vía por defecto para modales — pides solo los datos y evitas inconsistencias de composición.\n\n## Composición\n\nUna sola pieza data-driven (`AppDialog`) sobre `Dialog`: recibe `title`, `description`, icono y acciones; el layout canónico (sin dividers) ya viene armado.\n\n## Uso básico\n\n```tsx\n<AppDialog\n trigger={<Button variant=\"outline\">Eliminar cuenta</Button>}\n title=\"¿Eliminar cuenta?\"\n description=\"Esta acción no se puede deshacer.\"\n confirmLabel=\"Eliminar\"\n confirmVariant=\"destructive\"\n onConfirm={() => {\n deleteAccount();\n }}\n/>\n```\n\n## Con icono\n\n```tsx\nimport { AlertCircle } from \"lucide-react\";\n\n<AppDialog\n trigger={<Button variant=\"outline\">Eliminar</Button>}\n icon={<AlertCircle className=\"size-6 text-destructive\" />}\n title=\"¿Eliminar cuenta?\"\n description=\"Esta acción no se puede deshacer.\"\n confirmLabel=\"Eliminar\"\n confirmVariant=\"destructive\"\n onConfirm={() => deleteAccount()}\n/>\n```\n\n## Uso controlado\n\n```tsx\nconst [open, setOpen] = useState(false);\n\n<AppDialog\n open={open}\n onOpenChange={setOpen}\n trigger={<Button>Invitar miembro</Button>}\n title=\"Invitar miembro\"\n description=\"Envía una invitación por email.\"\n confirmLabel=\"Invitar\"\n onConfirm={() => {\n sendInvite();\n return true; // cierra el diálogo\n }}\n>\n <Input placeholder=\"Email\" />\n</AppDialog>\n```\n\n## Alineación de acciones\n\n```tsx\n// Centrado para decisiones binarias\n<AppDialog\n trigger={<Button>Confirmar</Button>}\n title=\"¿Estás seguro?\"\n actionsAlignment=\"center\"\n confirmLabel=\"Sí\"\n cancelLabel=\"No\"\n/>\n\n// Space-between para guardar/descartar\n<AppDialog\n trigger={<Button>Salir</Button>}\n title=\"Cambios sin guardar\"\n actionsAlignment=\"space-between\"\n confirmLabel=\"Guardar\"\n cancelLabel=\"Descartar\"\n/>\n```\n\n## Receta compuesta\n\n`AppDialog` es un wrapper ergonómico sobre el `Dialog` compuesto: las props extra (`aria-*`, `onEscapeKeyDown`, `onInteractOutside`…) pasan al `DialogContent` y el `ref` apunta a ese nodo. Si la estructura canónica no te alcanza (varios bloques de acciones, layout propio, sin título), usa las piezas directamente:\n\n```tsx\nimport { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from \"@/components/base/Dialog\";\n```\n\n## Reglas\n\n- **Sí** usa `icon` para alertas o confirmaciones importantes (icono plano, sin fondo).\n- **Sí** usa `actionsAlignment=\"center\"` para decisiones binarias (sí/no).\n- **Sí** usa `actionsAlignment=\"space-between\"` para guardar/descartar.\n- **No** uses fondo coloreado en el icono; Material 3 usa icono plano.\n- **No** bajes a `Dialog` primitivo si `AppDialog` cubre el caso.\n- El supporting text usa `text-sm` para igualar la tipografía del action area.\n- No hay dividers entre contenido y acciones (siguiendo Material 3).\n- Si hay `icon`, el headline se centra. Si no hay icono, se alinea a la izquierda.\n\n## Import\n\n```tsx\nimport { AppDialog } from \"@/components/base/AppDialog\";\n```\n\n## Props\n\n| Prop | Tipo | Default | Descripción |\n|---|---|---|---|\n| `trigger` | `ReactNode` | — | Elemento que abre el diálogo. |\n| `icon` | `ReactNode?` | — | Icono arriba del título (sin fondo). |\n| `title` | `string` | — | **Requerido.** Título compacto (22/28). |\n| `description` | `string?` | — | Texto de soporte (text-sm). |\n| `children` | `ReactNode?` | — | Contenido principal entre descripción y acciones. |\n| `confirmLabel` | `string` | `\"Confirmar\"` | Texto del botón de acción. |\n| `cancelLabel` | `string` | `\"Cancelar\"` | Texto del botón de cancelar. |\n| `confirmVariant` | `Button variant` | `\"default\"` | Variante del botón de confirmar. |\n| `confirmDisabled` | `boolean` | `false` | Deshabilita el botón de confirmar. |\n| `onConfirm` | `() => boolean \\| void \\| Promise<...>` | — | Si devuelve `false`, no cierra. |\n| `onCancel` | `() => void` | — | Se llama al cancelar o cerrar. |\n| `actionsAlignment` | `\"right\" \\| \"center\" \\| \"left\" \\| \"space-between\"` | `\"right\"` | Alineación de botones. |\n| `open` | `boolean?` | — | Modo controlado. |\n| `onOpenChange` | `(boolean) => void` | — | Callback de cambio de estado. |\n| `hideFooter` | `boolean` | `false` | Oculta el action area. |\n\n## Para IA\n\n1. Identifica el patrón: ¿es confirmación, formulario modal o alerta informativa?\n2. Si es confirmación/formulario → usa `AppDialog`.\n3. Si es una alerta importante → añade `icon` (sin fondo, icono plano).\n4. Define `title` y `description` (supporting text).\n5. Si hay formulario, pasa inputs como `children` y maneja `onConfirm` para validar.\n6. Elige `actionsAlignment` según el contexto:\n - `right` para acciones estándar\n - `center` para decisiones binarias (sí/no)\n - `space-between` para guardar/descartar\n7. Usa `confirmVariant=\"destructive\"` para acciones irreversibles.\n8. Usa `open` + `onOpenChange` si necesitas controlar el estado desde fuera.\n9. Solo usa `Dialog` primitivo si necesitas un layout sin footer o sin título.\n"
|
|
24
24
|
}
|
|
25
25
|
]
|
|
26
26
|
}
|
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
"path": "components/base/AppHeaderBar.tsx",
|
|
18
18
|
"type": "registry:ui",
|
|
19
19
|
"target": "components/base/AppHeaderBar.tsx",
|
|
20
|
-
"content": "import type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nimport { Button } from \"./Button\";\nimport { HeaderBar, headerVariants } from \"./HeaderBar\";\nimport { Logo } from \"./Logo\";\n\nexport interface AppHeaderBarItem {\n label: string;\n href?: string;\n active?: boolean;\n disabled?: boolean;\n onClick?: () => void;\n}\n\nexport interface AppHeaderBarAction {\n label: string;\n icon?: React.ComponentType<{ className?: string }>;\n onClick?: () => void;\n variant?: \"default\" | \"ghost\" | \"outline\";\n disabled?: boolean;\n}\n\nexport interface AppHeaderBarProps\n extends React.HTMLAttributes<HTMLElement>, VariantProps<typeof headerVariants> {\n /** Qué mostrar como marca. `false` oculta el área de marca. ReactNode para custom. */\n brand?: React.ReactNode | false;\n /** Items de navegación principales (centro/izquierda). */\n items?: AppHeaderBarItem[];\n /** Acciones alineadas a la derecha (botones, avatar, menús). */\n actions?: React.ReactNode;\n /** Acciones como datos (alternativa a `actions` ReactNode). */\n actionItems?: AppHeaderBarAction[];\n /** Callback cuando cambia el item activo. */\n onItemClick?: (item: AppHeaderBarItem) => void;\n}\n\n/**\n * Barra de navegación superior data-driven.\n *\n * Wrapper ergonómico sobre `HeaderBar` + `Button`: describe la navegación como\n * datos (`items`, `actionItems`) y compone las piezas en el orden correcto.\n * Para estructuras distintas (mega-menús, buscador central), usa `HeaderBar`\n * directamente con `brand`/`actions` como ReactNode.\n */\nexport const AppHeaderBar = React.forwardRef<HTMLElement, AppHeaderBarProps>(function AppHeaderBar(\n {\n surface = \"default\",\n bordered,\n padding,\n sticky,\n brand,\n items,\n actions,\n actionItems,\n onItemClick,\n className,\n ...props\n },\n ref,\n) {\n const logoSurface = surface === \"navy\" ? \"dark\" : \"light\";\n\n const brandArea = (\n <>\n {brand !== false && (brand ?? <Logo surface={logoSurface} />)}\n {items && items.length > 0 && (\n <nav className=\"hidden md:flex items-center gap-1\">\n {items.map((item, index) => (\n <a\n key={index}\n href={item.href}\n onClick={(e) => {\n if (item.onClick || onItemClick) {\n e.preventDefault();\n item.onClick?.();\n onItemClick?.(item);\n }\n }}\n className={cn(\n \"px-3 py-2
|
|
20
|
+
"content": "import type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nimport { Button } from \"./Button\";\nimport { HeaderBar, headerVariants } from \"./HeaderBar\";\nimport { Logo } from \"./Logo\";\n\nexport interface AppHeaderBarItem {\n label: string;\n href?: string;\n active?: boolean;\n disabled?: boolean;\n onClick?: () => void;\n}\n\nexport interface AppHeaderBarAction {\n label: string;\n icon?: React.ComponentType<{ className?: string }>;\n onClick?: () => void;\n variant?: \"default\" | \"ghost\" | \"outline\";\n disabled?: boolean;\n}\n\nexport interface AppHeaderBarProps\n extends React.HTMLAttributes<HTMLElement>, VariantProps<typeof headerVariants> {\n /** Qué mostrar como marca. `false` oculta el área de marca. ReactNode para custom. */\n brand?: React.ReactNode | false;\n /** Items de navegación principales (centro/izquierda). */\n items?: AppHeaderBarItem[];\n /** Acciones alineadas a la derecha (botones, avatar, menús). */\n actions?: React.ReactNode;\n /** Acciones como datos (alternativa a `actions` ReactNode). */\n actionItems?: AppHeaderBarAction[];\n /** Callback cuando cambia el item activo. */\n onItemClick?: (item: AppHeaderBarItem) => void;\n}\n\n/**\n * Barra de navegación superior data-driven.\n *\n * Wrapper ergonómico sobre `HeaderBar` + `Button`: describe la navegación como\n * datos (`items`, `actionItems`) y compone las piezas en el orden correcto.\n * Para estructuras distintas (mega-menús, buscador central), usa `HeaderBar`\n * directamente con `brand`/`actions` como ReactNode.\n */\nexport const AppHeaderBar = React.forwardRef<HTMLElement, AppHeaderBarProps>(function AppHeaderBar(\n {\n surface = \"default\",\n bordered,\n padding,\n sticky,\n brand,\n items,\n actions,\n actionItems,\n onItemClick,\n className,\n ...props\n },\n ref,\n) {\n const logoSurface = surface === \"navy\" ? \"dark\" : \"light\";\n\n const brandArea = (\n <>\n {brand !== false && (brand ?? <Logo surface={logoSurface} />)}\n {items && items.length > 0 && (\n <nav className=\"hidden md:flex items-center gap-1\">\n {items.map((item, index) => (\n <a\n key={index}\n href={item.href}\n onClick={(e) => {\n if (item.onClick || onItemClick) {\n e.preventDefault();\n item.onClick?.();\n onItemClick?.(item);\n }\n }}\n className={cn(\n \"rounded-nav-item px-3 py-2 type-action-label transition-colors\",\n item.active\n ? surface === \"navy\"\n ? \"bg-white/15 text-white\"\n : \"bg-accent text-accent-foreground\"\n : surface === \"navy\"\n ? \"text-white/70 hover:text-white hover:bg-white/10\"\n : \"text-muted-foreground hover:text-foreground hover:bg-secondary\",\n item.disabled && \"opacity-50 pointer-events-none\",\n )}\n >\n {item.label}\n </a>\n ))}\n </nav>\n )}\n </>\n );\n\n const actionArea =\n actionItems || actions ? (\n <>\n {actionItems?.map((action, index) => {\n const Icon = action.icon;\n return (\n <Button\n key={index}\n variant={action.variant ?? \"default\"}\n onClick={action.onClick}\n disabled={action.disabled}\n >\n {Icon && <Icon className=\"size-(--icon-size-inline)\" />}\n {action.label}\n </Button>\n );\n })}\n {actions}\n </>\n ) : undefined;\n\n return (\n <HeaderBar\n ref={ref}\n surface={surface}\n bordered={bordered}\n padding={padding}\n sticky={sticky}\n brand={brandArea}\n actions={actionArea}\n className={className}\n {...props}\n />\n );\n});\n\nAppHeaderBar.displayName = \"AppHeaderBar\";\n"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"path": "components/base/AppHeaderBar.md",
|
|
24
24
|
"type": "registry:file",
|
|
25
25
|
"target": "components/base/AppHeaderBar.md",
|
|
26
|
-
"content": "# AppHeaderBar
|
|
26
|
+
"content": "# AppHeaderBar\nBarra de navegación superior **data-driven**. Describe la navegación como datos\n(`items`, `actionItems`) y el componente compone internamente las primitivas de\n`HeaderBar`. **Esta es la vía a usar para construir header bars.** No compongas a\nmano `HeaderBar` directamente si `AppHeaderBar` cubre el caso.\n\n## Cuándo usarlo\n\nPara la barra superior descrita como datos: marca, navegación y acciones sin componer piezas a mano. La vía rápida para el header de una app generada.\n\n## Composición\n\nUna sola pieza data-driven (`AppHeaderBar`) que compone `HeaderBar` por debajo: marca (logo o imagen), items de navegación y acciones descritos por props.\n\n## Uso básico\n\n```tsx\n<AppHeaderBar\n items={[\n { label: \"Dashboard\", href: \"/\", active: true },\n { label: \"Proyectos\", href: \"/proyectos\" },\n { label: \"Configuración\", href: \"/config\", disabled: true },\n ]}\n actionItems={[\n { label: \"Nuevo\", icon: Plus, onClick: () => openModal() },\n ]}\n/>\n```\n\n## Uso con marca personalizada\n\n```tsx\n<AppHeaderBar\n brand={<Logo layout=\"horizontal\" />}\n items={[\n { label: \"Inicio\", href: \"/\" },\n { label: \"Productos\", href: \"/productos\" },\n ]}\n actions={<UserMenu />}\n/>\n```\n\n## Sin marca\n\n```tsx\n<AppHeaderBar\n brand={false}\n items={[\n { label: \"Inicio\", href: \"/\" },\n ]}\n/>\n```\n\n## Marca como imagen\n\n```tsx\n<AppHeaderBar\n brand={<img src=\"/logo.png\" alt=\"Mi Marca\" className=\"h-8 w-auto\" />}\n items={[\n { label: \"Inicio\", href: \"/\" },\n ]}\n/>\n```\n\n## Receta compuesta\n\n`AppHeaderBar` es un wrapper ergonómico sobre `HeaderBar` + `Button`: describe la navegación como datos y compone las piezas en el orden correcto; las props extra pasan al `<header>` y el `ref` apunta a ese nodo. Para estructuras distintas (mega-menús, buscador central), usa `HeaderBar` directamente con `brand`/`actions` como ReactNode.\n\n## Reglas\n\n- Define la navegación como datos y pásala en `items`.\n- Usa `actionItems` para botones de acción a la derecha.\n- Usa `actions` (ReactNode) solo si necesitas componentes complejos (menus, dropdowns).\n- Marca la ruta actual con `active`.\n- Usa `disabled` para opciones bloqueadas en vez de ocultarlas.\n- `surface=\"navy\"` usa el color de marca `#0b013c`.\n- Para casos a medida que no encajen en `items`/`actionItems`, usa `HeaderBar` directamente.\n\n## Import\n\n```tsx\nimport { AppHeaderBar } from \"@/components/base/AppHeaderBar\";\n```\n\n## Props\n\n### `AppHeaderBar`\n\n| Prop | Tipo | Default | Descripción |\n|---|---|---|---|\n| `brand` | `ReactNode` | — | Marca del producto (por defecto, `Logo`). |\n| `items` | `AppHeaderBarItem[]` | — | Items de navegación principales. |\n| `actions` | `ReactNode` | — | Acciones como ReactNode (a la derecha). |\n| `actionItems` | `AppHeaderBarAction[]` | — | Acciones como datos (alternativa a `actions`). |\n| `onItemClick` | `(item) => void` | — | Callback al hacer click en un item. |\n| `surface` | `\"default\" \\| \"transparent\" \\| \"navy\"` | `\"default\"` | Superficie de fondo. |\n| `bordered` | `boolean` | `true` | Borde inferior. |\n| `padding` | `\"page\" \\| \"none\"` | `\"page\"` | Padding horizontal. |\n| `sticky` | `boolean` | `false` | Fija en la parte superior. |\n\n### `AppHeaderBarItem`\n\n| Campo | Tipo | Descripción |\n|---|---|---|\n| `label` | `string` | Texto visible. |\n| `href` | `string?` | URL de navegación. |\n| `active` | `boolean?` | Marca el item activo. |\n| `disabled` | `boolean?` | No navegable, atenuado. |\n| `onClick` | `() => void` | Callback al hacer click. |\n\n### `AppHeaderBarAction`\n\n| Campo | Tipo | Descripción |\n|---|---|---|\n| `label` | `string` | Texto del botón. |\n| `icon` | `LucideIcon?` | Icono a la izquierda. |\n| `onClick` | `() => void` | Callback al hacer click. |\n| `variant` | `\"default\" \\| \"ghost\" \\| \"outline\"` | Variante del botón. |\n| `disabled` | `boolean?` | Deshabilita el botón. |\n\n## Para IA\n\n1. Identifica la navegación principal de la página.\n2. Crea `AppHeaderBarItem[]` con los items de navegación.\n3. Define `active` en el item de la ruta actual.\n4. Crea `AppHeaderBarAction[]` para botones de acción (nuevo, crear, etc.).\n5. Usa `onItemClick` si necesitas navegación client-side (React Router, etc.).\n6. Si necesitas menus complejos o dropdowns, usa `actions` (ReactNode) en vez de `actionItems`.\n7. Solo usa `HeaderBar` primitivo si necesitas un layout que no encaje en `items`/`actionItems`.\n"
|
|
27
27
|
}
|
|
28
28
|
]
|
|
29
29
|
}
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
"path": "components/base/AppSidebar.tsx",
|
|
17
17
|
"type": "registry:ui",
|
|
18
18
|
"target": "components/base/AppSidebar.tsx",
|
|
19
|
-
"content": "\"use client\";\n\nimport { ChevronRight } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"./Avatar\";\nimport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarMenu,\n SidebarMenuBadge,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarMenuSub,\n SidebarMenuSubButton,\n SidebarMenuSubItem,\n SidebarTrigger,\n} from \"./Sidebar\";\n\n// AppSidebar — envoltorio *data-driven* sobre las primitivas de `Sidebar`.\n//\n// En lugar de componer 20+ subcomponentes a mano, describes la navegación como\n// datos (`groups`) y el componente arma la composición correcta, replicando la\n// estética canónica de `Sidebar` (paddings px-8, grupos colapsables con chevron,\n// sub-ítems indentados con `SidebarMenuSub` a pl-9, pie de usuario con borde).\n//\n// Para casos a medida, las primitivas de `./Sidebar` siguen disponibles.\n//\n// Se usa dentro de un <SidebarProvider> (junto a <SidebarInset> para el\n// contenido). Ver la story para el layout completo.\n\ntype IconType = React.ComponentType<{ className?: string }>;\n\nexport interface AppSidebarItem {\n /** Texto visible del ítem. */\n title: string;\n /** Si se define, el ítem se renderiza como enlace `<a href>`. */\n url?: string;\n /** Icono (p. ej. de lucide-react). Se renderiza a la izquierda. */\n icon?: IconType;\n /** Marca el ítem como activo (página actual). */\n isActive?: boolean;\n /** Deshabilita el ítem (no navegable). */\n disabled?: boolean;\n /** Contador/etiqueta a la derecha (p. ej. nº de notificaciones). */\n badge?: string | number;\n /** Subítems anidados → convierte el ítem en colapsable. */\n items?: AppSidebarItem[];\n /** Si el ítem tiene subítems, si arranca expandido. */\n defaultOpen?: boolean;\n}\n\nexport interface AppSidebarGroup {\n /** Encabezado del grupo. Si se omite, los ítems van sin título. */\n label?: string;\n /** Si el grupo se puede colapsar con un chevron en su encabezado. */\n collapsible?: boolean;\n /** Si es colapsable, si arranca expandido (default: true). */\n defaultOpen?: boolean;\n items: AppSidebarItem[];\n}\n\nexport interface AppSidebarUser {\n name: string;\n email?: string;\n avatar?: string;\n}\n\nexport interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {\n /** Marca en la cabecera cuando está expandida (normalmente el `Logo`). */\n logo?: React.ReactNode;\n /** Marca compacta para el modo colapsado a iconos (p. ej. el isotipo). */\n logoIcon?: React.ReactNode;\n /** Estructura de navegación. */\n groups: AppSidebarGroup[];\n /** Usuario en el pie. Ignorado si se pasa `footer`. */\n user?: AppSidebarUser;\n /** Pie a medida; sobrescribe el render de `user`. */\n footer?: React.ReactNode;\n /**\n * Borde lateral divisorio. Independiente del modo de colapso.\n * Default: `true`.\n */\n bordered?: boolean;\n}\n\n// ─── Ítem de primer nivel (botón de menú con icono) ───────────────────────────\n\nfunction LeafItem({ item }: { item: AppSidebarItem }) {\n const Icon = item.icon;\n const inner = (\n <>\n {Icon && <Icon className=\"size-4 shrink-0\" />}\n <span className=\"truncate\">{item.title}</span>\n </>\n );\n\n return (\n <SidebarMenuItem>\n <SidebarMenuButton\n asChild={!!item.url && !item.disabled}\n isActive={item.isActive}\n tooltip={item.title}\n aria-disabled={item.disabled || undefined}\n aria-current={item.isActive ? \"page\" : undefined}\n className={cn(\n \"h-8 rounded-md px-2 text-sm\",\n item.disabled && \"pointer-events-none opacity-50\",\n )}\n >\n {item.url && !item.disabled ? <a href={item.url}>{inner}</a> : inner}\n </SidebarMenuButton>\n {item.badge != null && <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>}\n </SidebarMenuItem>\n );\n}\n\n// ─── Sub-ítem indentado (compartido por grupos colapsables y branch items) ────\n\nfunction SubButton({ item }: { item: AppSidebarItem }) {\n const Icon = item.icon;\n const inner = (\n <>\n {Icon && <Icon className=\"size-4 shrink-0\" />}\n <span className=\"truncate\">{item.title}</span>\n {item.badge != null && (\n <span className=\"ml-auto text-xs font-medium tabular-nums text-muted-foreground\">\n {item.badge}\n </span>\n )}\n </>\n );\n\n return (\n <SidebarMenuSubItem>\n <SidebarMenuSubButton\n asChild={!!item.url && !item.disabled}\n isActive={item.isActive}\n aria-disabled={item.disabled || undefined}\n aria-current={item.isActive ? \"page\" : undefined}\n className={cn(\n \"h-8 rounded-md py-1.5 pl-9 pr-2 text-sm\",\n item.disabled && \"cursor-not-allowed text-muted-foreground\",\n )}\n >\n {item.url && !item.disabled ? <a href={item.url}>{inner}</a> : inner}\n </SidebarMenuSubButton>\n </SidebarMenuSubItem>\n );\n}\n\n// ─── Ítem de primer nivel con subítems (colapsable, chevron a la derecha) ─────\n\nfunction BranchItem({ item }: { item: AppSidebarItem }) {\n const [open, setOpen] = React.useState(item.defaultOpen ?? false);\n const Icon = item.icon;\n\n return (\n <SidebarMenuItem>\n <SidebarMenuButton\n onClick={() => setOpen((v) => !v)}\n aria-expanded={open}\n isActive={item.isActive}\n tooltip={item.title}\n className=\"h-8 rounded-md px-2 text-sm\"\n >\n {Icon && <Icon className=\"size-4 shrink-0\" />}\n <span className=\"truncate\">{item.title}</span>\n <ChevronRight\n className={cn(\n \"ml-auto size-4 shrink-0 text-muted-foreground transition-transform duration-200\",\n open && \"rotate-90\",\n )}\n />\n </SidebarMenuButton>\n\n {open && (\n <SidebarMenuSub className=\"mx-0 mt-1 gap-1 border-l-0 px-0 py-0\">\n {item.items!.map((sub) => (\n <SubButton key={sub.title} item={sub} />\n ))}\n </SidebarMenuSub>\n )}\n </SidebarMenuItem>\n );\n}\n\n// ─── Grupo ────────────────────────────────────────────────────────────────────\n\nfunction Group({ group }: { group: AppSidebarGroup }) {\n const [open, setOpen] = React.useState(group.defaultOpen ?? true);\n\n // Grupo colapsable: encabezado bold con chevron a la izquierda + sub-ítems\n // indentados (mismo patrón que las stories de Sidebar).\n if (group.collapsible && group.label) {\n return (\n <SidebarGroup className=\"mb-1 p-0\">\n <SidebarGroupLabel\n asChild\n className=\"h-8 px-2 text-sm font-semibold text-sidebar-foreground hover:bg-secondary hover:text-sidebar-foreground\"\n >\n <button type=\"button\" aria-expanded={open} onClick={() => setOpen((v) => !v)}>\n <ChevronRight\n className={cn(\n \"mr-3 size-4 shrink-0 text-muted-foreground transition-transform duration-200\",\n open && \"rotate-90\",\n )}\n />\n <span className=\"truncate\">{group.label}</span>\n </button>\n </SidebarGroupLabel>\n\n {open && (\n <SidebarGroupContent>\n <SidebarMenuSub className=\"mx-0 mt-1 gap-1 border-l-0 px-0 py-0\">\n {group.items.map((item) => (\n <SubButton key={item.title} item={item} />\n ))}\n </SidebarMenuSub>\n </SidebarGroupContent>\n )}\n </SidebarGroup>\n );\n }\n\n // Grupo plano: etiqueta estática opcional + ítems de primer nivel.\n return (\n <SidebarGroup className=\"mb-1 p-0\">\n {group.label && (\n <SidebarGroupLabel className=\"px-2 text-xs font-medium text-muted-foreground\">\n {group.label}\n </SidebarGroupLabel>\n )}\n <SidebarGroupContent>\n <SidebarMenu>\n {group.items.map((item) =>\n item.items && item.items.length > 0 ? (\n <BranchItem key={item.title} item={item} />\n ) : (\n <LeafItem key={item.title} item={item} />\n ),\n )}\n </SidebarMenu>\n </SidebarGroupContent>\n </SidebarGroup>\n );\n}\n\n// ─── Pie de usuario ───────────────────────────────────────────────────────────\n\nfunction UserFooter({ user }: { user: AppSidebarUser }) {\n const initials = user.name\n .split(\" \")\n .map((p) => p[0])\n .slice(0, 2)\n .join(\"\")\n .toUpperCase();\n\n return (\n <SidebarMenu>\n <SidebarMenuItem>\n <SidebarMenuButton\n tooltip={user.name}\n className=\"h-auto gap-3 rounded-md px-2 py-1.5 group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-0\"\n >\n <Avatar className=\"size-8 shrink-0\">\n {user.avatar && <AvatarImage src={user.avatar} alt={user.name} />}\n <AvatarFallback>{initials}</AvatarFallback>\n </Avatar>\n <div className=\"flex min-w-0 flex-col text-left leading-tight group-data-[collapsible=icon]:hidden\">\n <span className=\"truncate text-sm font-medium\">{user.name}</span>\n {user.email && (\n <span className=\"truncate text-xs text-muted-foreground\">{user.email}</span>\n )}\n </div>\n </SidebarMenuButton>\n </SidebarMenuItem>\n </SidebarMenu>\n );\n}\n\n// ─── AppSidebar ───────────────────────────────────────────────────────────────\n\nexport const AppSidebar = React.forwardRef<HTMLDivElement, AppSidebarProps>(function AppSidebar(\n { logo, logoIcon, groups, user, footer, bordered = true, className, ...sidebarProps },\n ref,\n) {\n const collapsible = sidebarProps.collapsible ?? \"offcanvas\";\n const canToggle = collapsible !== \"none\";\n\n // Borde desacoplado del modo de colapso: lo decide `bordered`, no `collapsible`.\n // El Sidebar trae el borde por defecto (variant sidebar) vía una clase con\n // modificador de `side`, así que para quitarlo hace falta `!important`.\n const borderClass = bordered ? \"\" : \"!border-l-0 !border-r-0\";\n\n return (\n <Sidebar ref={ref} className={cn(borderClass, className)} {...sidebarProps}>\n {(logo != null || logoIcon != null) && (\n <SidebarHeader className=\"px-0 py-6\">\n {/* Las dos marcas se superponen y hacen cross-fade. El isotipo se ancla\n a los primeros 64px (la franja de iconos) para aparecer ya en su\n posición final, sin desplazarse con la animación de ancho. */}\n <div className=\"relative h-8\">\n {/* Expandida: logo + trigger (sale con fade) */}\n <div className=\"absolute inset-0 flex items-center gap-3 pl-8 pr-2 transition-opacity duration-200 group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:opacity-0\">\n <div className=\"flex min-w-0 flex-1 items-center\">{logo}</div>\n {canToggle && (\n <SidebarTrigger\n aria-label=\"Colapsar barra lateral\"\n className=\"size-8 shrink-0 bg-transparent shadow-none\"\n />\n )}\n </div>\n {/* Colapsada a iconos: isotipo en posición final (entra con fade) */}\n {logoIcon != null && (\n <div className=\"pointer-events-none absolute inset-y-0 left-0 flex w-(--sidebar-width-icon) items-center justify-center opacity-0 transition-opacity duration-200 group-data-[collapsible=icon]:opacity-100\">\n {logoIcon}\n </div>\n )}\n </div>\n </SidebarHeader>\n )}\n\n <SidebarContent className=\"px-8 pb-6 group-data-[collapsible=icon]:px-4\">\n {groups.map((group, i) => (\n <Group key={group.label ?? `group-${i}`} group={group} />\n ))}\n </SidebarContent>\n\n {(footer != null || user != null) && (\n <SidebarFooter className=\"border-t border-sidebar-border px-8 py-3 group-data-[collapsible=icon]:px-4\">\n {footer != null ? footer : user != null ? <UserFooter user={user} /> : null}\n </SidebarFooter>\n )}\n </Sidebar>\n );\n});\n\nAppSidebar.displayName = \"AppSidebar\";\n"
|
|
19
|
+
"content": "\"use client\";\n\nimport { ChevronRight } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"./Avatar\";\nimport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarMenu,\n SidebarMenuBadge,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarMenuSub,\n SidebarMenuSubButton,\n SidebarMenuSubItem,\n SidebarTrigger,\n} from \"./Sidebar\";\n\n// AppSidebar — envoltorio *data-driven* sobre las primitivas de `Sidebar`.\n//\n// En lugar de componer 20+ subcomponentes a mano, describes la navegación como\n// datos (`groups`) y el componente arma la composición correcta, replicando la\n// estética canónica de `Sidebar` (paddings px-8, grupos colapsables con chevron,\n// sub-ítems indentados con `SidebarMenuSub` a pl-9, pie de usuario con borde).\n//\n// Para casos a medida, las primitivas de `./Sidebar` siguen disponibles.\n//\n// Se usa dentro de un <SidebarProvider> (junto a <SidebarInset> para el\n// contenido). Ver la story para el layout completo.\n\ntype IconType = React.ComponentType<{ className?: string }>;\n\nexport interface AppSidebarItem {\n /** Texto visible del ítem. */\n title: string;\n /** Si se define, el ítem se renderiza como enlace `<a href>`. */\n url?: string;\n /** Icono (p. ej. de lucide-react). Se renderiza a la izquierda. */\n icon?: IconType;\n /** Marca el ítem como activo (página actual). */\n isActive?: boolean;\n /** Deshabilita el ítem (no navegable). */\n disabled?: boolean;\n /** Contador/etiqueta a la derecha (p. ej. nº de notificaciones). */\n badge?: string | number;\n /** Subítems anidados → convierte el ítem en colapsable. */\n items?: AppSidebarItem[];\n /** Si el ítem tiene subítems, si arranca expandido. */\n defaultOpen?: boolean;\n}\n\nexport interface AppSidebarGroup {\n /** Encabezado del grupo. Si se omite, los ítems van sin título. */\n label?: string;\n /** Si el grupo se puede colapsar con un chevron en su encabezado. */\n collapsible?: boolean;\n /** Si es colapsable, si arranca expandido (default: true). */\n defaultOpen?: boolean;\n items: AppSidebarItem[];\n}\n\nexport interface AppSidebarUser {\n name: string;\n email?: string;\n avatar?: string;\n}\n\nexport interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {\n /** Marca en la cabecera cuando está expandida (normalmente el `Logo`). */\n logo?: React.ReactNode;\n /** Marca compacta para el modo colapsado a iconos (p. ej. el isotipo). */\n logoIcon?: React.ReactNode;\n /** Estructura de navegación. */\n groups: AppSidebarGroup[];\n /** Usuario en el pie. Ignorado si se pasa `footer`. */\n user?: AppSidebarUser;\n /** Pie a medida; sobrescribe el render de `user`. */\n footer?: React.ReactNode;\n /**\n * Borde lateral divisorio. Independiente del modo de colapso.\n * Default: `true`.\n */\n bordered?: boolean;\n}\n\n// ─── Ítem de primer nivel (botón de menú con icono) ───────────────────────────\n\nfunction LeafItem({ item }: { item: AppSidebarItem }) {\n const Icon = item.icon;\n const inner = (\n <>\n {Icon && <Icon className=\"size-(--icon-size-inline) shrink-0\" />}\n <span className=\"truncate\">{item.title}</span>\n </>\n );\n\n return (\n // El rail solo muestra iconos: un ítem sin icono desaparece al colapsar y\n // los que sí tienen se apilan arriba sin huecos.\n <SidebarMenuItem className={cn(!Icon && \"group-data-[collapsible=icon]:hidden\")}>\n <SidebarMenuButton\n asChild={!!item.url && !item.disabled}\n isActive={item.isActive}\n tooltip={item.title}\n aria-disabled={item.disabled || undefined}\n aria-current={item.isActive ? \"page\" : undefined}\n className={cn(\n \"h-8 rounded-nav-item px-2 type-supporting\",\n item.disabled && \"pointer-events-none opacity-50\",\n )}\n >\n {item.url && !item.disabled ? <a href={item.url}>{inner}</a> : inner}\n </SidebarMenuButton>\n {item.badge != null && <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>}\n </SidebarMenuItem>\n );\n}\n\n// ─── Sub-ítem indentado (compartido por grupos colapsables y branch items) ────\n\nfunction SubButton({ item }: { item: AppSidebarItem }) {\n if (item.items && item.items.length > 0) {\n return <SubBranchItem item={item} />;\n }\n\n const Icon = item.icon;\n const inner = (\n <>\n {Icon && <Icon className=\"size-(--icon-size-inline) shrink-0\" />}\n <span className=\"truncate\">{item.title}</span>\n {item.badge != null && (\n <span className=\"ml-auto type-meta font-medium tabular-nums text-muted-foreground\">\n {item.badge}\n </span>\n )}\n </>\n );\n\n return (\n <SidebarMenuSubItem>\n <SidebarMenuSubButton\n asChild={!!item.url && !item.disabled}\n isActive={item.isActive}\n aria-disabled={item.disabled || undefined}\n aria-current={item.isActive ? \"page\" : undefined}\n className={cn(\n \"h-8 rounded-nav-item py-1.5 pl-9 pr-2 type-supporting\",\n item.disabled && \"cursor-not-allowed text-muted-foreground\",\n )}\n >\n {item.url && !item.disabled ? <a href={item.url}>{inner}</a> : inner}\n </SidebarMenuSubButton>\n </SidebarMenuSubItem>\n );\n}\n\n// ─── Sub-ítem desplegable (permite jerarquías de navegación profundas) ────────\n\nfunction SubBranchItem({ item }: { item: AppSidebarItem }) {\n const [open, setOpen] = React.useState(item.defaultOpen ?? false);\n const Icon = item.icon;\n\n return (\n <SidebarMenuSubItem>\n <SidebarMenuSubButton\n isActive={item.isActive}\n aria-expanded={open}\n onClick={() => setOpen((value) => !value)}\n className=\"h-8 rounded-nav-item py-1.5 pl-9 pr-2 type-supporting\"\n >\n {Icon && <Icon className=\"size-(--icon-size-inline) shrink-0\" />}\n <span className=\"truncate\">{item.title}</span>\n <ChevronRight\n className={cn(\n \"ml-auto size-(--icon-size-inline) shrink-0 text-muted-foreground transition-transform duration-[var(--duration-normal)] ease-[var(--ease-standard)] motion-reduce:transition-none\",\n open && \"rotate-90\",\n )}\n />\n </SidebarMenuSubButton>\n\n {open && (\n <SidebarMenuSub className=\"ml-3 mr-0 mt-1 gap-1 border-l-0 px-0 py-0\">\n {item.items!.map((sub) => (\n <SubButton key={sub.title} item={sub} />\n ))}\n </SidebarMenuSub>\n )}\n </SidebarMenuSubItem>\n );\n}\n\n// ─── Ítem de primer nivel con subítems (colapsable, chevron a la derecha) ─────\n\nfunction BranchItem({ item }: { item: AppSidebarItem }) {\n const [open, setOpen] = React.useState(item.defaultOpen ?? false);\n const Icon = item.icon;\n\n return (\n <SidebarMenuItem className={cn(!Icon && \"group-data-[collapsible=icon]:hidden\")}>\n <SidebarMenuButton\n onClick={() => setOpen((v) => !v)}\n aria-expanded={open}\n isActive={item.isActive}\n tooltip={item.title}\n className=\"h-8 rounded-nav-item px-2 type-supporting\"\n >\n {Icon && <Icon className=\"size-(--icon-size-inline) shrink-0\" />}\n <span className=\"truncate\">{item.title}</span>\n <ChevronRight\n className={cn(\n \"ml-auto size-(--icon-size-inline) shrink-0 text-muted-foreground transition-transform duration-[var(--duration-normal)] ease-[var(--ease-standard)] motion-reduce:transition-none\",\n open && \"rotate-90\",\n )}\n />\n </SidebarMenuButton>\n\n {open && (\n <SidebarMenuSub className=\"mx-0 mt-1 gap-1 border-l-0 px-0 py-0\">\n {item.items!.map((sub) => (\n <SubButton key={sub.title} item={sub} />\n ))}\n </SidebarMenuSub>\n )}\n </SidebarMenuItem>\n );\n}\n\n// ─── Grupo ────────────────────────────────────────────────────────────────────\n\nfunction Group({ group }: { group: AppSidebarGroup }) {\n const [open, setOpen] = React.useState(group.defaultOpen ?? true);\n\n // Grupo colapsable: encabezado bold con chevron a la izquierda + sub-ítems\n // indentados (mismo patrón que las stories de Sidebar).\n if (group.collapsible && group.label) {\n return (\n <SidebarGroup className=\"mb-1 p-0\">\n <SidebarGroupLabel\n asChild\n className=\"h-8 rounded-nav-item px-2 type-supporting font-semibold text-sidebar-foreground hover:bg-sidebar-hover hover:text-sidebar-foreground\"\n >\n <button type=\"button\" aria-expanded={open} onClick={() => setOpen((v) => !v)}>\n <ChevronRight\n className={cn(\n \"mr-3 size-(--icon-size-inline) shrink-0 text-muted-foreground transition-transform duration-[var(--duration-normal)] ease-[var(--ease-standard)] motion-reduce:transition-none\",\n open && \"rotate-90\",\n )}\n />\n <span className=\"truncate\">{group.label}</span>\n </button>\n </SidebarGroupLabel>\n\n {open && (\n <SidebarGroupContent>\n <SidebarMenuSub className=\"mx-0 mt-1 gap-1 border-l-0 px-0 py-0\">\n {group.items.map((item) => (\n <SubButton key={item.title} item={item} />\n ))}\n </SidebarMenuSub>\n </SidebarGroupContent>\n )}\n </SidebarGroup>\n );\n }\n\n // Grupo plano: etiqueta estática opcional + ítems de primer nivel.\n return (\n <SidebarGroup className=\"mb-1 p-0\">\n {group.label && (\n <SidebarGroupLabel className=\"px-2 type-meta font-medium text-muted-foreground\">\n {group.label}\n </SidebarGroupLabel>\n )}\n <SidebarGroupContent>\n <SidebarMenu>\n {group.items.map((item) =>\n item.items && item.items.length > 0 ? (\n <BranchItem key={item.title} item={item} />\n ) : (\n <LeafItem key={item.title} item={item} />\n ),\n )}\n </SidebarMenu>\n </SidebarGroupContent>\n </SidebarGroup>\n );\n}\n\n// ─── Pie de usuario ───────────────────────────────────────────────────────────\n\nfunction UserFooter({ user }: { user: AppSidebarUser }) {\n const initials = user.name\n .split(\" \")\n .map((p) => p[0])\n .slice(0, 2)\n .join(\"\")\n .toUpperCase();\n\n return (\n <SidebarMenu>\n <SidebarMenuItem>\n <SidebarMenuButton\n tooltip={user.name}\n className=\"h-auto gap-3 rounded-nav-item px-2 py-1.5 group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-0\"\n >\n <Avatar className=\"size-8 shrink-0\">\n {user.avatar && <AvatarImage src={user.avatar} alt={user.name} />}\n <AvatarFallback>{initials}</AvatarFallback>\n </Avatar>\n <div className=\"flex min-w-0 flex-col text-left leading-tight group-data-[collapsible=icon]:hidden\">\n <span className=\"truncate type-supporting font-medium\">{user.name}</span>\n {user.email && (\n <span className=\"truncate type-meta text-muted-foreground\">{user.email}</span>\n )}\n </div>\n </SidebarMenuButton>\n </SidebarMenuItem>\n </SidebarMenu>\n );\n}\n\n// ─── AppSidebar ───────────────────────────────────────────────────────────────\n\nexport const AppSidebar = React.forwardRef<HTMLDivElement, AppSidebarProps>(function AppSidebar(\n { logo, logoIcon, groups, user, footer, bordered = true, className, ...sidebarProps },\n ref,\n) {\n const collapsible = sidebarProps.collapsible ?? \"offcanvas\";\n const canToggle = collapsible !== \"none\";\n\n // Borde desacoplado del modo de colapso: lo decide `bordered`, no `collapsible`.\n // El Sidebar trae el borde por defecto (variant sidebar) vía una clase con\n // modificador de `side`, así que para quitarlo hace falta `!important`.\n const borderClass = bordered ? \"\" : \"!border-l-0 !border-r-0\";\n\n return (\n <Sidebar ref={ref} className={cn(borderClass, className)} {...sidebarProps}>\n {(logo != null || logoIcon != null) && (\n <SidebarHeader className=\"px-0 py-6\">\n {/* Las dos marcas se superponen y hacen cross-fade. El isotipo se ancla\n a los primeros 64px (la franja de iconos) para aparecer ya en su\n posición final, sin desplazarse con la animación de ancho. */}\n <div className=\"relative h-8\">\n {/* Expandida: logo + trigger (sale con fade) */}\n <div className=\"absolute inset-0 flex items-center gap-3 pl-8 pr-2 transition-opacity duration-[var(--duration-normal)] ease-[var(--ease-standard)] motion-reduce:transition-none group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:opacity-0\">\n <div className=\"flex min-w-0 flex-1 items-center\">{logo}</div>\n {canToggle && (\n <SidebarTrigger\n aria-label=\"Colapsar barra lateral\"\n className=\"size-8 shrink-0 bg-transparent shadow-none\"\n />\n )}\n </div>\n {/* Colapsada a iconos: el isotipo conserva su altura dentro del logo\n (h-6, mismo viewBox alto) y se centra en el eje del rail (64px).\n Al pasar el cursor (o con foco) el isotipo se desvanece y aparece\n el MISMO SidebarTrigger que colapsa la sidebar expandida: idéntico\n estilo ghost, icono e hover. */}\n {logoIcon != null && (\n <div className=\"group/rail-logo pointer-events-none absolute inset-y-0 left-0 w-(--sidebar-width-icon) opacity-0 transition-opacity duration-[var(--duration-normal)] ease-[var(--ease-standard)] motion-reduce:transition-none group-data-[collapsible=icon]:pointer-events-auto group-data-[collapsible=icon]:opacity-100\">\n <span className=\"pointer-events-none absolute inset-0 flex items-center justify-center transition-opacity duration-[var(--duration-fast)] group-hover/rail-logo:opacity-0 group-focus-within/rail-logo:opacity-0\">\n {logoIcon}\n </span>\n {canToggle && (\n <div className=\"absolute inset-0 flex items-center justify-center opacity-0 transition-opacity duration-[var(--duration-fast)] group-hover/rail-logo:opacity-100 group-focus-within/rail-logo:opacity-100\">\n <SidebarTrigger\n aria-label=\"Expandir barra lateral\"\n className=\"size-8 shrink-0 bg-transparent shadow-none\"\n />\n </div>\n )}\n </div>\n )}\n </div>\n </SidebarHeader>\n )}\n\n <SidebarContent className=\"px-8 pb-6 group-data-[collapsible=icon]:px-4\">\n {groups.map((group, i) => (\n <Group key={group.label ?? `group-${i}`} group={group} />\n ))}\n </SidebarContent>\n\n {(footer != null || user != null) && (\n <SidebarFooter className=\"border-t border-sidebar-border px-8 py-3 group-data-[collapsible=icon]:px-4\">\n {footer != null ? footer : user != null ? <UserFooter user={user} /> : null}\n </SidebarFooter>\n )}\n </Sidebar>\n );\n});\n\nAppSidebar.displayName = \"AppSidebar\";\n"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"path": "components/base/AppSidebar.md",
|
|
23
23
|
"type": "registry:file",
|
|
24
24
|
"target": "components/base/AppSidebar.md",
|
|
25
|
-
"content": "# AppSidebar
|
|
25
|
+
"content": "# AppSidebar\nBarra lateral de navegación **data-driven**. Describes la navegación como datos\n(`groups`, `user`, `logo`) y el componente compone internamente las primitivas\nde `Sidebar`. **Esta es la vía a usar para construir sidebars.** No compongas a\nmano las primitivas de `Sidebar` (`SidebarGroup`, `SidebarMenuButton`, etc.) ni\ninsertes piezas sueltas (`SidebarSeparator`…): `AppSidebar` ya arma la\ncomposición correcta.\n\n## Cuándo usarlo\n\nPara la navegación lateral de una app interna descrita como datos: grupos, items, iconos, badges y estado activo sin componer primitivas a mano. Es la vía por defecto del layout CRM; colapsable y responsive (Sheet en móvil).\n\n## Composición\n\nUna sola pieza data-driven (`AppSidebar`) que arma por debajo las primitivas de `Sidebar`: recibe grupos e items (título, url, icono, badge, subitems) y resuelve colapso, estado activo y variante móvil. El área de trabajo va en `SidebarInset`.\n\n## Layout canónico\n\n`AppSidebar` vive dentro de `SidebarProvider`, junto a `SidebarInset` para el\ncontenido. El botón de colapso lo aporta `AppSidebar` **dentro** de su cabecera\n(expandida); cuando está colapsada, renderiza un `SidebarTrigger` **fuera** (en\nel contenido) para reabrirla. Solo aparece si `collapsible !== \"none\"`.\n\n```tsx\nimport { useState } from \"react\";\n\nexport function AppLayout() {\n const [open, setOpen] = useState(true);\n\n return (\n <SidebarProvider open={open} onOpenChange={setOpen}>\n <AppSidebar\n collapsible=\"offcanvas\"\n logo={<Logo layout=\"horizontal\" />}\n logoIcon={<Logo layout=\"isotipe\" className=\"size-7\" />}\n groups={groups}\n user={{ name: \"Ada Lovelace\", email: \"ada@lexy.cl\", avatar: \"/avatar.jpg\" }}\n />\n <SidebarInset>\n {!open && (\n <div className=\"p-4\">\n <SidebarTrigger /> {/* reabrir cuando está colapsada */}\n </div>\n )}\n <main>{/* contenido */}</main>\n </SidebarInset>\n </SidebarProvider>\n );\n}\n```\n\n## Anchos — cuándo usar cada uno\n\nEl ancho lo controla `--sidebar-width`. El **default es Estándar (16rem / 256px)**:\nes el punto resonante del sistema (4× el rail de 64px y 4:1 de contenido a\n1280px) y el estándar de la industria. Cubre labels de navegación con contador\nsin truncar. Para cambiarlo, pásalo por `style` en `SidebarProvider`:\n\n```tsx\n<SidebarProvider style={{ \"--sidebar-width\": \"17.5rem\" } as React.CSSProperties}>\n```\n\n| Preset | Valor | Cuándo usarlo |\n|---|---|---|\n| **Compacta** | `14rem` (224px) | Apps operacionales densas; priorizas área de contenido. Justo para labels ≤20 car. |\n| **Estándar** ← default | `16rem` (256px) | Navegación con iconos y contadores. La elección por defecto para casi todo. |\n| **Cómoda** | `17.5rem` (280px) | Labels largos, o un subtítulo/rol bajo cada ítem. |\n| **Workspace** | `20rem` (320px) | La barra es un panel de trabajo: switcher de organización, búsqueda embebida o árbol de navegación. En `lg` (1024px) ocupa ~31% — audita que lo justifique el contenido. |\n\nRegla: elige por lo que **es** la barra, no por estética. Navegación → Estándar;\npanel-workspace → Workspace. Los intermedios resuelven casos de contenido puntual.\n\n## Modos de colapso — `collapsible`\n\n| Modo | Comportamiento | Cuándo usarlo |\n|---|---|---|\n| `offcanvas` (default) | Se desliza fuera de pantalla (ancho 0). | Cuando al colapsar quieres recuperar **todo** el ancho para el contenido. |\n| `icon` | Colapsa al **rail de iconos** (64px): solo iconos + tooltips. | Navegación persistente accesible con un clic. **Requiere `icon` en todos los ítems** y pasar `logoIcon` (el isotipo). Ítems sin icono desaparecen del rail. |\n| `none` | No colapsa. | Barras fijas o layouts donde el colapso no aporta. |\n\nEn `icon`, el rail expande de vuelta al pasar el cursor (o con foco) sobre el\nisotipo, que se transforma en el mismo botón que colapsa la barra expandida.\n\n## Variantes — `variant`\n\n- `sidebar` (default): pegada al borde, con divisoria. El caso normal.\n- `floating`: tarjeta flotante con radio y sombra, separada del borde.\n- `inset`: la barra y el contenido quedan \"incrustados\" con margen; el contenido\n se vuelve una superficie elevada. Para layouts con más aire.\n\n## Modelo de datos\n\n```tsx\nconst groups: AppSidebarGroup[] = [\n {\n // grupo plano (sin título): ítems de primer nivel con icono\n items: [\n { title: \"Dashboard\", url: \"/\", icon: LayoutDashboard, isActive: true },\n { title: \"Proyectos\", url: \"/proyectos\", icon: FolderKanban, badge: 4 },\n {\n title: \"Equipo\",\n icon: Users,\n items: [ // branch → se expande (chevron)\n { title: \"Miembros\", url: \"/equipo/miembros\" },\n { title: \"Roles\", url: \"/equipo/roles\" },\n ],\n },\n ],\n },\n {\n label: \"Análisis\",\n collapsible: true, // grupo colapsable → ítems indentados (un nivel)\n defaultOpen: true,\n items: [\n { title: \"Reportes\", url: \"/reportes\", icon: BarChart3 },\n { title: \"Ventas\", url: \"/ventas\", isActive: true },\n { title: \"Tráfico\", url: \"/trafico\", badge: 12 },\n ],\n },\n];\n```\n\n## Receta compuesta\n\n`AppSidebar` es un wrapper ergonómico sobre las primitivas compuestas de `Sidebar`: describe la navegación como datos (`groups`) y compone la estructura canónica; las props extra pasan al `Sidebar` raíz y el `ref` apunta a ese nodo. Para layouts a medida (secciones custom, búsqueda embebida), usa las primitivas directamente:\n\n```tsx\nimport { Sidebar, SidebarContent, SidebarGroup, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider } from \"@/components/base/Sidebar\";\n```\n\n## Reglas\n\n- La superficie permanece blanca; hover y selección recuperan la marca Lexy con lavanda e índigo mediante `sidebar-hover`, `sidebar-accent` y `sidebar-accent-foreground`.\n- El ítem activo colorea texto e icono. No añadas color manualmente a cada entrada.\n- Asigna `icon` a **todos** los ítems de primer nivel: en `collapsible=\"icon\"`\n son lo único visible (la etiqueta aparece como tooltip).\n- No anides más de un nivel de subítems.\n- En un grupo colapsable los ítems van planos (un nivel); el anidamiento\n `item.items` aplica a grupos planos.\n- `bordered` controla el borde con independencia del modo de colapso.\n- Para casos a medida que no encajen en `groups`, usa las primitivas de\n `Sidebar` directamente; no mezcles ambos enfoques en la misma barra.\n\n## Import\n\n```tsx\nimport { AppSidebar } from \"@/components/base/AppSidebar\";\nimport {\n SidebarProvider,\n SidebarInset,\n SidebarTrigger,\n} from \"@/components/base/Sidebar\";\n```\n\n## Props\n\n### `AppSidebar`\n\n| Prop | Tipo | Default | Descripción |\n|---|---|---|---|\n| `groups` | `AppSidebarGroup[]` | — | **Requerido.** Estructura de navegación. |\n| `logo` | `ReactNode` | — | Marca en la cabecera (expandida). |\n| `logoIcon` | `ReactNode` | — | Marca compacta para el modo iconos (isotipo). |\n| `user` | `{ name, email?, avatar? }` | — | Pie de usuario. Omitir para ocultarlo. |\n| `footer` | `ReactNode` | — | Pie a medida; sobrescribe `user`. |\n| `collapsible` | `\"offcanvas\" \\| \"icon\" \\| \"none\"` | `\"offcanvas\"` | Comportamiento al colapsar. |\n| `side` | `\"left\" \\| \"right\"` | `\"left\"` | Lado de la pantalla. |\n| `variant` | `\"sidebar\" \\| \"floating\" \\| \"inset\"` | `\"sidebar\"` | Estilo del contenedor. |\n| `bordered` | `boolean` | `true` | Borde lateral divisorio. Independiente del colapso. |\n\n### `AppSidebarGroup`\n\n| Campo | Tipo | Descripción |\n|---|---|---|\n| `label` | `string?` | Encabezado del grupo. Si se omite, los ítems van sin título. |\n| `collapsible` | `boolean?` | Encabezado con chevron que despliega/colapsa. |\n| `defaultOpen` | `boolean?` | Estado inicial si es colapsable (default `true`). |\n| `items` | `AppSidebarItem[]` | Ítems del grupo. |\n\n### `AppSidebarItem`\n\n| Campo | Tipo | Descripción |\n|---|---|---|\n| `title` | `string` | Texto visible. |\n| `url` | `string?` | Si se define, se renderiza como `<a href>`. |\n| `icon` | `LucideIcon?` | Icono a la izquierda. |\n| `isActive` | `boolean?` | Marca la página actual. |\n| `disabled` | `boolean?` | No navegable, atenuado. |\n| `badge` | `string \\| number?` | Contador/etiqueta a la derecha. |\n| `items` | `AppSidebarItem[]?` | Subítems (solo en grupo plano → ítem expandible). |\n| `defaultOpen` | `boolean?` | Estado inicial si tiene subítems. |\n"
|
|
26
26
|
}
|
|
27
27
|
]
|
|
28
28
|
}
|
package/assets/r/avatar.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "avatar",
|
|
4
4
|
"type": "registry:ui",
|
|
5
5
|
"title": "Avatar",
|
|
6
|
-
"description": "Imagen de perfil con fallback. Para identificación visual.
|
|
6
|
+
"description": "Imagen de perfil con fallback. Para identificación visual.",
|
|
7
7
|
"dependencies": [
|
|
8
8
|
"@radix-ui/react-avatar"
|
|
9
9
|
],
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
"path": "components/base/Avatar.tsx",
|
|
14
14
|
"type": "registry:ui",
|
|
15
15
|
"target": "components/base/Avatar.tsx",
|
|
16
|
-
"content": "import * as AvatarPrimitive from \"@radix-ui/react-avatar\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nconst Avatar = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <AvatarPrimitive.Root\n ref={ref}\n className={cn(\"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\", className)}\n {...props}\n />\n));\nAvatar.displayName = AvatarPrimitive.Root.displayName;\n\nconst AvatarImage = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Image>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n <AvatarPrimitive.Image\n ref={ref}\n className={cn(\"aspect-square h-full w-full object-cover\", className)}\n {...props}\n />\n));\nAvatarImage.displayName = AvatarPrimitive.Image.displayName;\n\nconst AvatarFallback = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Fallback>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n <AvatarPrimitive.Fallback\n ref={ref}\n className={cn(\n \"flex h-full w-full items-center justify-center rounded-full bg-secondary
|
|
16
|
+
"content": "import * as AvatarPrimitive from \"@radix-ui/react-avatar\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nconst Avatar = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <AvatarPrimitive.Root\n ref={ref}\n className={cn(\"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\", className)}\n {...props}\n />\n));\nAvatar.displayName = AvatarPrimitive.Root.displayName;\n\nconst AvatarImage = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Image>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n <AvatarPrimitive.Image\n ref={ref}\n className={cn(\"aspect-square h-full w-full object-cover\", className)}\n {...props}\n />\n));\nAvatarImage.displayName = AvatarPrimitive.Image.displayName;\n\nconst AvatarFallback = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Fallback>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n <AvatarPrimitive.Fallback\n ref={ref}\n className={cn(\n \"flex h-full w-full items-center justify-center rounded-full bg-secondary type-supporting font-medium text-muted-foreground\",\n className,\n )}\n {...props}\n />\n));\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;\n\nexport { Avatar, AvatarFallback, AvatarImage };\n"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"path": "components/base/Avatar.md",
|
|
20
20
|
"type": "registry:file",
|
|
21
21
|
"target": "components/base/Avatar.md",
|
|
22
|
-
"content": "# Avatar
|
|
22
|
+
"content": "# Avatar\nImagen de perfil con fallback. Para identificación visual.\n\n## Cuándo usarlo\n\nPara identificar visualmente a una persona u organización: en headers, filas, comentarios, asignaciones. Con fallback de iniciales siempre — la foto es un privilegio, no una garantía.\n\n## Composición\n\n`Avatar` con `AvatarImage` (la foto) y `AvatarFallback` (iniciales sobre fondo de marca), que aparece si la imagen falla o no existe.\n\n## Uso básico\n\n```tsx\n<Avatar />\n```\n\n## Reglas\n\n- Usa Avatar según el propósito descrito.\n- No abuses de este componente en contexts donde no aplica.\n\n## Import\n\n```tsx\nimport { Avatar } from \"@/components/base/Avatar\";\n```\n\n## Props\n\nConsulta la story en Storybook para ver las props disponibles.\n\n## Para IA\n\n1. Identifica el contexto de uso del componente.\n2. Importa Avatar desde el path correcto.\n3. Configura las props según la necesidad.\n4. Verifica que el componente se integre correctamente en el layout.\n5. Solo usa variantes documentadas.\n"
|
|
23
23
|
}
|
|
24
24
|
]
|
|
25
25
|
}
|
package/assets/r/badge.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "badge",
|
|
4
4
|
"type": "registry:ui",
|
|
5
5
|
"title": "Badge",
|
|
6
|
-
"description": "Etiqueta de estado o categoría, **no interactiva** (`<span>`). Para marcar estados (\"Activo\", \"Pendiente\") o clasificar.
|
|
6
|
+
"description": "Etiqueta de estado o categoría, **no interactiva** (`<span>`). Para marcar estados (\"Activo\", \"Pendiente\") o clasificar.",
|
|
7
7
|
"dependencies": [
|
|
8
8
|
"class-variance-authority"
|
|
9
9
|
],
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
"path": "components/base/Badge.tsx",
|
|
14
14
|
"type": "registry:ui",
|
|
15
15
|
"target": "components/base/Badge.tsx",
|
|
16
|
-
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nconst badgeVariants = cva(\n \"inline-flex
|
|
16
|
+
"content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nconst badgeVariants = cva(\n \"inline-flex items-center justify-center gap-1 rounded-full font-medium whitespace-nowrap select-none [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground\",\n secondary: \"bg-secondary text-secondary-foreground\",\n destructive: \"bg-destructive text-destructive-foreground\",\n outline: \"border border-border bg-background text-foreground\",\n },\n size: {\n sm: \"h-5 px-2 type-meta [&_svg]:size-3\",\n md: \"h-6 px-2.5 type-supporting [&_svg]:size-3.5\",\n lg: \"h-7 px-3 type-body [&_svg]:size-4\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"sm\",\n },\n },\n);\n\nexport interface BadgeProps\n extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {}\n\nconst Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(\n ({ className, variant, size, ...props }, ref) => (\n <span ref={ref} className={cn(badgeVariants({ variant, size }), className)} {...props} />\n ),\n);\nBadge.displayName = \"Badge\";\n\nexport { Badge, badgeVariants };\n"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"path": "components/base/Badge.md",
|
|
20
20
|
"type": "registry:file",
|
|
21
21
|
"target": "components/base/Badge.md",
|
|
22
|
-
"content": "# Badge
|
|
22
|
+
"content": "# Badge\nEtiqueta de estado o categoría, **no interactiva** (`<span>`). Para marcar estados (\"Activo\", \"Pendiente\") o clasificar.\n\n## Cuándo usarlo\n\nPara marcar el estado o la clasificación de algo sin acción asociada: “Activo”, “Pendiente”, “Beta”. Es información pura — si al tocarlo pasa algo, es un botón o un filtro, no un badge.\n\n## Composición\n\nUna sola pieza no interactiva (`Badge`, un `<span>`) con variantes de intención.\nLa presentación `default` es contained y admite un icono antes del texto.\nPara conteos numéricos usa `CounterBadge`; para taxonomía, `Tag`.\n\n## Uso básico\n\n```tsx\nimport { Archive, CircleAlert, CircleCheck } from \"lucide-react\";\n\n<Badge>\n <CircleCheck aria-hidden=\"true\" />\n Activo\n</Badge>\n<Badge size=\"md\" variant=\"destructive\">\n <CircleAlert aria-hidden=\"true\" />\n Vencido\n</Badge>\n<Badge variant=\"outline\">\n <Archive aria-hidden=\"true\" />\n Archivado\n</Badge>\n```\n\n## Reglas\n\n- El badge siempre lleva texto: no comuniques estado solo por color (accesibilidad).\n- El icono es opcional y decorativo; usa `aria-hidden=\"true\"` cuando el texto ya expresa el estado.\n- El icono escala automáticamente a 12, 14 o 16 px según `size`.\n- Es informativo, no clicable. Si necesitas que haga algo, usa `Button` o `DropdownMenu`.\n- Mantén el texto corto (1–2 palabras).\n- Para un punto de estado mínimo junto a texto usa `StatusDot`; para contadores numéricos, `CounterBadge`.\n\n## Cuándo NO usar\n\n- **Conteo numérico** (mensajes, ítems) → `CounterBadge`.\n- **Indicador de estado tipo punto** junto a una fila → `StatusDot`.\n- **Etiqueta removible / chip de filtro** → `Tag` (tiene `removable`).\n- **Algo clicable** → `Button`.\n\n## Import\n\n```tsx\nimport { Badge } from \"@/components/base/Badge\";\n```\n\n## Props\n\n| Prop | Tipo | Default | Descripción |\n|---|---|---|---|\n| `variant` | `\"default\" \\| \"secondary\" \\| \"destructive\" \\| \"outline\"` | `\"default\"` | Tono visual. |\n| `size` | `\"sm\" \\| \"md\" \\| \"lg\"` | `\"sm\"` | Escala altura y tipografía: 12/16, 14/20 o 16/24. |\n| ...resto | `span` props | — | `className`, `aria-*`, etc. |\n\n## Para IA\n\n1. ¿Es una etiqueta de estado/categoría estática y corta? Entonces `Badge`.\n2. Elige la variante por intención: `destructive` para estados negativos, `secondary`/`outline` para neutros.\n3. Si es número, punto de estado o chip removible, usa `CounterBadge`/`StatusDot`/`Tag`.\n4. No lo uses como botón.\n"
|
|
23
23
|
}
|
|
24
24
|
]
|
|
25
25
|
}
|