create-lexy 0.1.1 → 0.3.0
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/assets/r/accordion.json +25 -0
- package/assets/r/alert-dialog.json +28 -0
- package/assets/r/app-accordion.json +25 -0
- package/assets/r/app-dialog.json +26 -0
- package/assets/r/app-header-bar.json +29 -0
- package/assets/r/app-sidebar.json +28 -0
- package/assets/r/avatar.json +25 -0
- package/assets/r/badge.json +25 -0
- package/assets/r/breadcrumb.json +25 -0
- package/assets/r/button-group.json +28 -0
- package/assets/r/button.json +1 -0
- package/assets/r/calendar.json +28 -0
- package/assets/r/card.json +25 -0
- package/assets/r/chart.json +25 -0
- package/assets/r/checkbox.json +25 -0
- package/assets/r/combobox.json +27 -0
- package/assets/r/command.json +25 -0
- package/assets/r/confirmacion.json +27 -0
- package/assets/r/counter-badge.json +25 -0
- package/assets/r/crm-app-layout.json +29 -0
- package/assets/r/crm-desk.json +31 -0
- package/assets/r/crm-detalle-caso.json +28 -0
- package/assets/r/date-picker.json +30 -0
- package/assets/r/dialog.json +25 -0
- package/assets/r/dropdown-menu.json +25 -0
- package/assets/r/empty.json +25 -0
- package/assets/r/feature-card.json +23 -0
- package/assets/r/form.json +30 -0
- package/assets/r/header-bar.json +27 -0
- package/assets/r/input.json +23 -0
- package/assets/r/intake-wizard.json +28 -0
- package/assets/r/label.json +26 -0
- package/assets/r/login.json +28 -0
- package/assets/r/logo.json +131 -0
- package/assets/r/menubar.json +25 -0
- package/assets/r/navigation-menu.json +26 -0
- package/assets/r/pagination.json +28 -0
- package/assets/r/popover.json +25 -0
- package/assets/r/profile-card.json +26 -0
- package/assets/r/progress.json +25 -0
- package/assets/r/radio-group.json +25 -0
- package/assets/r/registry.json +1447 -1
- package/assets/r/scroll-area.json +25 -0
- package/assets/r/searchbox.json +23 -0
- package/assets/r/select.json +25 -0
- package/assets/r/separator.json +25 -0
- package/assets/r/sheet.json +26 -0
- package/assets/r/sidebar.json +34 -0
- package/assets/r/skeleton.json +25 -0
- package/assets/r/slider.json +25 -0
- package/assets/r/snippet.json +25 -0
- package/assets/r/spinner.json +26 -0
- package/assets/r/status-dot.json +25 -0
- package/assets/r/switch.json +25 -0
- package/assets/r/table.json +25 -0
- package/assets/r/tabs.json +25 -0
- package/assets/r/tag.json +25 -0
- package/assets/r/textarea.json +23 -0
- package/assets/r/toaster.json +25 -0
- package/assets/r/tooltip.json +25 -0
- package/assets/r/tree.json +23 -0
- package/assets/theme/lexy-theme.css +3 -0
- package/dist/index.js +37 -36
- package/package.json +1 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "accordion",
|
|
4
|
+
"type": "registry:ui",
|
|
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. El componente vive en tu proyecto: instálalo con `create-lexy add accordion` y edítalo con libertad.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@radix-ui/react-accordion"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "components/base/Accordion.tsx",
|
|
14
|
+
"type": "registry:ui",
|
|
15
|
+
"target": "components/base/Accordion.tsx",
|
|
16
|
+
"content": "import * as React from \"react\";\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\";\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 text-sm font-medium text-foreground transition-all [&[data-state=open]>svg]:rotate-180\",\n className,\n )}\n {...props}>\n {children}\n <svg\n className='h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200'\n viewBox='0 0 24 24'\n fill='none'\n stroke='currentColor'\n strokeWidth='2'\n strokeLinecap='round'\n strokeLinejoin='round'>\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 text-sm text-muted-foreground data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down'\n {...props}>\n <div className={cn(\"pb-4 pt-0\", className)}>{children}</div>\n </AccordionPrimitive.Content>\n));\nAccordionContent.displayName = AccordionPrimitive.Content.displayName;\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent };\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "components/base/Accordion.md",
|
|
20
|
+
"type": "registry:file",
|
|
21
|
+
"target": "components/base/Accordion.md",
|
|
22
|
+
"content": "# Accordion — guía de uso\n\nSecciones colapsables para mostrar detalle bajo demanda (Radix). Progressive disclosure: muestra primero los títulos y despliega el contenido al pedirlo. El componente vive en tu proyecto: instálalo con `create-lexy add accordion` y edítalo con libertad.\n\n## Import\n\n```tsx\nimport {\n Accordion, AccordionItem, AccordionTrigger, AccordionContent,\n} from \"@/components/base/Accordion\";\n```\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## 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## 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## 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
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "alert-dialog",
|
|
4
|
+
"type": "registry:ui",
|
|
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. El componente vive en tu proyecto: instálalo con `create-lexy add alert-dialog` y edítalo con libertad.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@radix-ui/react-alert-dialog",
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"button"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/base/AlertDialog.tsx",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/base/AlertDialog.tsx",
|
|
19
|
+
"content": "import * as React from \"react\";\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\";\nimport type { VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils/cn\";\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-4 border border-border bg-background p-6 shadow-lg duration-200 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-lg\",\n className,\n )}\n {...props}\n />\n </AlertDialogPortal>\n));\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;\n\nconst AlertDialogHeader = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col space-y-2 text-center sm:text-left\",\n className,\n )}\n {...props}\n />\n);\nAlertDialogHeader.displayName = \"AlertDialogHeader\";\n\nconst AlertDialogFooter = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n className,\n )}\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(\n \"text-lg font-semibold leading-none tracking-tight text-foreground\",\n className,\n )}\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(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n));\nAlertDialogDescription.displayName =\n AlertDialogPrimitive.Description.displayName;\n\nexport interface AlertDialogActionProps\n extends 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 AlertDialogPortal,\n AlertDialogOverlay,\n AlertDialogTrigger,\n AlertDialogContent,\n AlertDialogHeader,\n AlertDialogFooter,\n AlertDialogTitle,\n AlertDialogDescription,\n AlertDialogAction,\n AlertDialogCancel,\n};\n"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": "components/base/AlertDialog.md",
|
|
23
|
+
"type": "registry:file",
|
|
24
|
+
"target": "components/base/AlertDialog.md",
|
|
25
|
+
"content": "# AlertDialog — guía de uso\n\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. El componente vive en tu proyecto: instálalo con `create-lexy add alert-dialog` y edítalo con libertad.\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## 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## 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## 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## 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
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "app-accordion",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "AppAccordion",
|
|
6
|
+
"description": "Acordeón de aplicación **data-driven**. Describe los items como datos (`items`)",
|
|
7
|
+
"dependencies": [],
|
|
8
|
+
"registryDependencies": [
|
|
9
|
+
"accordion"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "components/base/AppAccordion.tsx",
|
|
14
|
+
"type": "registry:ui",
|
|
15
|
+
"target": "components/base/AppAccordion.tsx",
|
|
16
|
+
"content": "import * as React from \"react\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"./Accordion\";\nimport { cn } from \"@/lib/utils/cn\";\n\nexport interface AppAccordionItem {\n /** Identificador único del item. */\n value: string;\n /** Título visible del item. */\n title: string;\n /** Icono decorativo antes del título. */\n icon?: React.ReactNode;\n /** Contenido del item (puede ser texto o ReactNode). */\n content: React.ReactNode;\n /** Deshabilita el item. */\n disabled?: boolean;\n}\n\nexport interface AppAccordionProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"defaultValue\" | \"dir\"> {\n /** Items del acordeón. */\n items: AppAccordionItem[];\n /** Tipo de acordeón: siempre un item abierto o múltiples. */\n type?: \"single\" | \"multiple\";\n /** Valor del item abierto por defecto (en modo \"single\"). */\n defaultValue?: string;\n /** Valores abiertos por defecto (en modo \"multiple\"). */\n defaultValues?: string[];\n /** Callback cuando cambia el item abierto. */\n onValueChange?: (value: string) => void;\n /** Envuelve el acordeón en un contenedor con borde, fondo y bordes redondeados. */\n containered?: boolean;\n /** Clase adicional para cada item. */\n itemClassName?: string;\n /** Si es `true`, permite colapsar el item abierto haciendo click (modo \"single\"). */\n collapsible?: boolean;\n}\n\n/**\n * Acordeón de aplicación data-driven.\n *\n * Wrapper ergonómico sobre el `Accordion` compuesto: describe los items como\n * datos (`items`) y compone las primitivas internamente. Las props extra\n * (`aria-*`, `id`…) pasan al nodo raíz y el `ref` apunta a ese nodo. Si\n * necesitas variar la estructura (triggers custom, contenido anidado), usa\n * Accordion/AccordionItem/AccordionTrigger/AccordionContent directamente.\n */\nconst AppAccordion = React.forwardRef<HTMLDivElement, AppAccordionProps>(\n function AppAccordion(\n {\n items,\n type = \"single\",\n defaultValue,\n defaultValues,\n onValueChange,\n containered = false,\n className,\n itemClassName,\n collapsible = true,\n ...props\n },\n ref,\n ) {\n const rootClass = containered\n ? cn(\"rounded-lg border border-border bg-background overflow-hidden\", className)\n : className;\n\n const resolvedItemClass = containered\n ? cn(\"px-4 last:border-b-0\", itemClassName)\n : itemClassName;\n\n const renderItems = () =>\n items.map((item) => (\n <AccordionItem\n key={item.value}\n value={item.value}\n className={resolvedItemClass}\n disabled={item.disabled}\n >\n <AccordionTrigger>\n <span className=\"flex items-center gap-2 text-left\">\n {item.icon && (\n <span className=\"shrink-0\">{item.icon}</span>\n )}\n {item.title}\n </span>\n </AccordionTrigger>\n <AccordionContent>{item.content}</AccordionContent>\n </AccordionItem>\n ));\n\n if (type === \"single\") {\n return (\n <Accordion\n ref={ref}\n type=\"single\"\n collapsible={collapsible}\n defaultValue={defaultValue}\n onValueChange={(value) => onValueChange?.(value)}\n className={rootClass}\n {...props}\n >\n {renderItems()}\n </Accordion>\n );\n }\n\n return (\n <Accordion\n ref={ref}\n type=\"multiple\"\n defaultValue={defaultValues}\n className={rootClass}\n {...props}\n >\n {renderItems()}\n </Accordion>\n );\n },\n);\nAppAccordion.displayName = \"AppAccordion\";\n\nexport { AppAccordion };\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "components/base/AppAccordion.md",
|
|
20
|
+
"type": "registry:file",
|
|
21
|
+
"target": "components/base/AppAccordion.md",
|
|
22
|
+
"content": "# AppAccordion — guía de uso\n\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. El componente vive en tu proyecto: instálalo con `create-lexy add app-accordion` y edítalo con libertad.\n\n## Import\n\n```tsx\nimport { AppAccordion } from \"@/components/base/AppAccordion\";\n```\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## 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## 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## 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
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "app-dialog",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "AppDialog",
|
|
6
|
+
"description": "Diálogo de aplicación al estilo **Material 3**. Estructura canónica sin dividers: El componente vive en tu proyecto: instálalo con `create-lexy add app-dialog` y edítalo con libertad.",
|
|
7
|
+
"dependencies": [],
|
|
8
|
+
"registryDependencies": [
|
|
9
|
+
"button",
|
|
10
|
+
"dialog"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "components/base/AppDialog.tsx",
|
|
15
|
+
"type": "registry:ui",
|
|
16
|
+
"target": "components/base/AppDialog.tsx",
|
|
17
|
+
"content": "import * as React from \"react\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogTitle,\n DialogTrigger,\n} from \"./Dialog\";\nimport { Button } from \"./Button\";\nimport { cn } from \"@/lib/utils/cn\";\n\nexport interface AppDialogProps\n 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 (headline). */\n title: string;\n /** Descripción o texto de soporte (supporting text). Usa `text-sm` igual que los botones. */\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?:\n | \"default\"\n | \"destructive\"\n | \"outline\"\n | \"secondary\"\n | \"ghost\"\n | \"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. Headline (título)\n * 3. Supporting text (text-sm, igual que botones)\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. El supporting text siempre usa `text-sm` para igualar la\n * tipografía del action area.\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<\n React.ElementRef<typeof DialogContent>,\n 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(\n \"flex flex-col\",\n icon ? \"text-center\" : \"text-left\"\n )}>\n <DialogTitle\n className={cn(\n \"text-lg font-semibold leading-none tracking-tight text-foreground\",\n icon && \"text-center\"\n )}\n >\n {title}\n </DialogTitle>\n {description && (\n <DialogDescription\n className={cn(\n \"text-sm text-muted-foreground mt-2\",\n icon && \"text-center\"\n )}\n >\n {description}\n </DialogDescription>\n )}\n </div>\n\n {/* Content area */}\n {children && (\n <div className=\"py-4 overflow-y-auto max-h-[60vh]\">\n {children}\n </div>\n )}\n\n {/* Action area (sin dividers). Sin padding propio: el gap-4 del\n DialogContent ya separa, y la base queda en p-6 = 24px = tope. */}\n {!hideFooter && (\n <div className={cn(\n \"flex flex-col-reverse sm:flex-row gap-2\",\n alignmentClasses[actionsAlignment]\n )}>\n <Button\n variant=\"ghost\"\n onClick={() => handleOpenChange(false)}\n >\n {cancelLabel}\n </Button>\n <Button\n variant={confirmVariant}\n disabled={confirmDisabled}\n onClick={handleConfirm}\n >\n {confirmLabel}\n </Button>\n </div>\n )}\n </DialogContent>\n </Dialog>\n );\n});\nAppDialog.displayName = \"AppDialog\";\n\nexport { AppDialog };\n"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "components/base/AppDialog.md",
|
|
21
|
+
"type": "registry:file",
|
|
22
|
+
"target": "components/base/AppDialog.md",
|
|
23
|
+
"content": "# AppDialog — guía de uso\n\nDiálogo de aplicación al estilo **Material 3**. Estructura canónica sin dividers: El componente vive en tu proyecto: instálalo con `create-lexy add app-dialog` y edítalo con libertad.\n\n1. **Icono** (opcional, sin fondo, centrado)\n2. **Headline** (título, 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## Import\n\n```tsx\nimport { AppDialog } from \"@/components/base/AppDialog\";\n```\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## 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 (headline). |\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## 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## 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
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "app-header-bar",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "AppHeaderBar",
|
|
6
|
+
"description": "Barra de navegación superior **data-driven**. Describe la navegación como datos",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"class-variance-authority"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"button",
|
|
12
|
+
"header-bar",
|
|
13
|
+
"logo"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "components/base/AppHeaderBar.tsx",
|
|
18
|
+
"type": "registry:ui",
|
|
19
|
+
"target": "components/base/AppHeaderBar.tsx",
|
|
20
|
+
"content": "import * as React from \"react\";\nimport type { VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils/cn\";\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>,\n 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>(\n 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 text-sm font-medium rounded-md 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-4\" />}\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);\n\nAppHeaderBar.displayName = \"AppHeaderBar\";\n"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "components/base/AppHeaderBar.md",
|
|
24
|
+
"type": "registry:file",
|
|
25
|
+
"target": "components/base/AppHeaderBar.md",
|
|
26
|
+
"content": "# AppHeaderBar — guía de uso\n\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. El componente vive en tu proyecto: instálalo con `create-lexy add app-header-bar` y edítalo con libertad.\n\n## Import\n\n```tsx\nimport { AppHeaderBar } from \"@/components/base/AppHeaderBar\";\n```\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## 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## 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## 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
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "app-sidebar",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "AppSidebar",
|
|
6
|
+
"description": "Barra lateral de navegación **data-driven**. Describes la navegación como datos",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"lucide-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"avatar",
|
|
12
|
+
"sidebar"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/base/AppSidebar.tsx",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/base/AppSidebar.tsx",
|
|
19
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronRight } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils/cn\"\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\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"./Avatar\"\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\n 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\n type=\"button\"\n aria-expanded={open}\n onClick={() => setOpen((v) => !v)}\n >\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\">\n {user.email}\n </span>\n )}\n </div>\n </SidebarMenuButton>\n </SidebarMenuItem>\n </SidebarMenu>\n )\n}\n\n// ─── AppSidebar ───────────────────────────────────────────────────────────────\n\nexport const AppSidebar = React.forwardRef<HTMLDivElement, AppSidebarProps>(\n function AppSidebar(\n {\n logo,\n logoIcon,\n groups,\n user,\n footer,\n bordered = true,\n className,\n ...sidebarProps\n },\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)\n\nAppSidebar.displayName = \"AppSidebar\"\n"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": "components/base/AppSidebar.md",
|
|
23
|
+
"type": "registry:file",
|
|
24
|
+
"target": "components/base/AppSidebar.md",
|
|
25
|
+
"content": "# AppSidebar — guía de uso\n\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. El componente vive en tu proyecto: instálalo con `create-lexy add app-sidebar` y edítalo con libertad.\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## 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## 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## 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\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- 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"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "avatar",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Avatar",
|
|
6
|
+
"description": "Imagen de perfil con fallback. Para identificación visual. El componente vive en tu proyecto: instálalo con `create-lexy add avatar` y edítalo con libertad.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@radix-ui/react-avatar"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "components/base/Avatar.tsx",
|
|
14
|
+
"type": "registry:ui",
|
|
15
|
+
"target": "components/base/Avatar.tsx",
|
|
16
|
+
"content": "import * as React from \"react\";\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\";\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(\n \"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\",\n className,\n )}\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 text-sm font-medium text-muted-foreground\",\n className,\n )}\n {...props}\n />\n));\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;\n\nexport { Avatar, AvatarImage, AvatarFallback };\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "components/base/Avatar.md",
|
|
20
|
+
"type": "registry:file",
|
|
21
|
+
"target": "components/base/Avatar.md",
|
|
22
|
+
"content": "# Avatar — guía de uso\n\nImagen de perfil con fallback. Para identificación visual. El componente vive en tu proyecto: instálalo con `create-lexy add avatar` y edítalo con libertad.\n\n## Import\n\n```tsx\nimport { Avatar } from \"@/components/base/Avatar\";\n```\n\n## Uso básico\n\n```tsx\n<Avatar />\n```\n\n## Props\n\nConsulta la story en Storybook para ver las props disponibles.\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## 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
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "badge",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Badge",
|
|
6
|
+
"description": "Etiqueta de estado o categoría, **no interactiva** (`<span>`). Para marcar estados (\"Activo\", \"Pendiente\") o clasificar. El componente vive en tu proyecto: instálalo con `create-lexy add badge` y edítalo con libertad.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"class-variance-authority"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "components/base/Badge.tsx",
|
|
14
|
+
"type": "registry:ui",
|
|
15
|
+
"target": "components/base/Badge.tsx",
|
|
16
|
+
"content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nconst badgeVariants = cva(\n \"inline-flex h-5 items-center justify-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium leading-none whitespace-nowrap select-none\",\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 },\n defaultVariants: {\n variant: \"default\",\n },\n },\n);\n\nexport interface BadgeProps\n extends React.HTMLAttributes<HTMLSpanElement>,\n VariantProps<typeof badgeVariants> {}\n\nconst Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(\n ({ className, variant, ...props }, ref) => (\n <span\n ref={ref}\n className={cn(badgeVariants({ variant }), className)}\n {...props}\n />\n ),\n);\nBadge.displayName = \"Badge\";\n\nexport { Badge, badgeVariants };\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "components/base/Badge.md",
|
|
20
|
+
"type": "registry:file",
|
|
21
|
+
"target": "components/base/Badge.md",
|
|
22
|
+
"content": "# Badge — guía de uso\n\nEtiqueta de estado o categoría, **no interactiva** (`<span>`). Para marcar estados (\"Activo\", \"Pendiente\") o clasificar. El componente vive en tu proyecto: instálalo con `create-lexy add badge` y edítalo con libertad.\n\n## Import\n\n```tsx\nimport { Badge } from \"@/components/base/Badge\";\n```\n\n## Uso básico\n\n```tsx\n<Badge>Nuevo</Badge>\n<Badge variant=\"secondary\">Borrador</Badge>\n<Badge variant=\"destructive\">Vencido</Badge>\n<Badge variant=\"outline\">Archivado</Badge>\n```\n\n## Props\n\n| Prop | Tipo | Default | Descripción |\n|---|---|---|---|\n| `variant` | `\"default\" \\| \"secondary\" \\| \"destructive\" \\| \"outline\"` | `\"default\"` | Tono visual. |\n| ...resto | `span` props | — | `className`, `aria-*`, etc. |\n\n## Reglas\n\n- El badge siempre lleva texto: no comuniques estado solo por color (accesibilidad).\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## 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
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "breadcrumb",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Breadcrumb",
|
|
6
|
+
"description": "Componente Breadcrumb. El componente vive en tu proyecto: instálalo con `create-lexy add breadcrumb` y edítalo con libertad.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@radix-ui/react-slot"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "components/base/Breadcrumb.tsx",
|
|
14
|
+
"type": "registry:ui",
|
|
15
|
+
"target": "components/base/Breadcrumb.tsx",
|
|
16
|
+
"content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nconst Breadcrumb = React.forwardRef<\n HTMLElement,\n React.ComponentPropsWithoutRef<\"nav\">\n>((props, ref) => <nav ref={ref} aria-label='breadcrumb' {...props} />);\nBreadcrumb.displayName = \"Breadcrumb\";\n\nconst BreadcrumbList = React.forwardRef<\n HTMLOListElement,\n React.ComponentPropsWithoutRef<\"ol\">\n>(({ className, ...props }, ref) => (\n <ol\n ref={ref}\n className={cn(\n \"flex flex-wrap items-center gap-2 break-words text-sm text-muted-foreground\",\n className,\n )}\n {...props}\n />\n));\nBreadcrumbList.displayName = \"BreadcrumbList\";\n\nconst BreadcrumbItem = React.forwardRef<\n HTMLLIElement,\n React.ComponentPropsWithoutRef<\"li\">\n>(({ className, ...props }, ref) => (\n <li\n ref={ref}\n className={cn(\"inline-flex items-center gap-2\", className)}\n {...props}\n />\n));\nBreadcrumbItem.displayName = \"BreadcrumbItem\";\n\nconst BreadcrumbLink = React.forwardRef<\n HTMLAnchorElement,\n React.ComponentPropsWithoutRef<\"a\"> & {\n /** Renderiza el hijo como el enlace (patrón Slot): `<BreadcrumbLink asChild><Link …/></BreadcrumbLink>`. */\n asChild?: boolean;\n }\n>(({ asChild = false, className, ...props }, ref) => {\n const Comp = asChild ? Slot : \"a\";\n return (\n <Comp\n ref={ref}\n className={cn(\n \"rounded text-primary transition-colors hover:text-primary/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n className,\n )}\n {...props}\n />\n );\n});\nBreadcrumbLink.displayName = \"BreadcrumbLink\";\n\nconst BreadcrumbPage = React.forwardRef<\n HTMLSpanElement,\n React.ComponentPropsWithoutRef<\"span\">\n>(({ className, ...props }, ref) => (\n <span\n ref={ref}\n role='link'\n aria-disabled='true'\n aria-current='page'\n className={cn(\"font-medium text-foreground\", className)}\n {...props}\n />\n));\nBreadcrumbPage.displayName = \"BreadcrumbPage\";\n\nconst BreadcrumbSeparator = ({\n children,\n className,\n ...props\n}: React.ComponentProps<\"li\">) => (\n <li\n role='presentation'\n aria-hidden='true'\n className={cn(\"flex items-center text-muted-foreground [&>svg]:size-4\", className)}\n {...props}>\n {children ?? (\n <svg\n xmlns='http://www.w3.org/2000/svg'\n width='24'\n height='24'\n viewBox='0 0 24 24'\n fill='none'\n stroke='currentColor'\n strokeWidth='2'\n strokeLinecap='round'\n strokeLinejoin='round'>\n <path d='m9 18 6-6-6-6' />\n </svg>\n )}\n </li>\n);\nBreadcrumbSeparator.displayName = \"BreadcrumbSeparator\";\n\nconst BreadcrumbEllipsis = ({\n className,\n ...props\n}: React.ComponentProps<\"span\">) => (\n <span\n role='presentation'\n aria-hidden='true'\n className={cn(\"flex items-center text-muted-foreground\", className)}\n {...props}>\n <svg\n xmlns='http://www.w3.org/2000/svg'\n width='24'\n height='24'\n viewBox='0 0 24 24'\n fill='currentColor'\n className='size-4'>\n <circle cx='12' cy='12' r='1' />\n <circle cx='19' cy='12' r='1' />\n <circle cx='5' cy='12' r='1' />\n </svg>\n <span className='sr-only'>More</span>\n </span>\n);\nBreadcrumbEllipsis.displayName = \"BreadcrumbEllipsis\";\n\nexport {\n Breadcrumb,\n BreadcrumbList,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbPage,\n BreadcrumbSeparator,\n BreadcrumbEllipsis,\n};\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "components/base/Breadcrumb.md",
|
|
20
|
+
"type": "registry:file",
|
|
21
|
+
"target": "components/base/Breadcrumb.md",
|
|
22
|
+
"content": "# Breadcrumb — guía de uso\n\nComponente Breadcrumb. El componente vive en tu proyecto: instálalo con `create-lexy add breadcrumb` y edítalo con libertad.\n\n## Import\n\n```tsx\nimport { Breadcrumb } from \"@/components/base/Breadcrumb\";\n```\n\n## Uso básico\n\n```tsx\n<Breadcrumb />\n```\n\n## Props\n\nConsulta la story en Storybook para ver las props disponibles.\n\n## Reglas\n\n- Usa Breadcrumb según el propósito descrito.\n- No abuses de este componente en contexts donde no aplica.\n\n## Para IA\n\n1. Identifica el contexto de uso del componente.\n2. Importa Breadcrumb 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
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "button-group",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "ButtonGroup",
|
|
6
|
+
"description": "Agrupa botones relacionados en una sola unidad visual. Sirve para acciones hermanas, comandos de toolbar o split actions. No mantiene estado de selección por sí mismo. El componente vive en tu proyecto: instálalo con `create-lexy add button-group` y edítalo con libertad.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@radix-ui/react-slot",
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"separator"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/base/ButtonGroup.tsx",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/base/ButtonGroup.tsx",
|
|
19
|
+
"content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils/cn\";\nimport { Separator } from \"./Separator\";\n\nconst buttonGroupVariants = cva(\n \"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:relative [&>*]:hover:z-10 [&>*]:focus-visible:z-10 [&>input]:flex-1\",\n {\n variants: {\n orientation: {\n horizontal:\n \"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:last-child)]:rounded-r-none [&>*:not(:first-child)]:-ml-px [&>[data-slot=button-group-separator]]:!ml-0 [&>[data-slot=button-group-separator]+*]:!ml-0 [&>[data-slot=button-group-separator]+*]:border-l-0 [&>:not([data-slot=button-group-separator]):has(+[data-slot=button-group-separator])]:border-r-0\",\n vertical:\n \"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:last-child)]:rounded-b-none [&>*:not(:first-child)]:-mt-px [&>[data-slot=button-group-separator]]:!mt-0 [&>[data-slot=button-group-separator]+*]:!mt-0 [&>[data-slot=button-group-separator]+*]:border-t-0 [&>:not([data-slot=button-group-separator]):has(+[data-slot=button-group-separator])]:border-b-0\",\n },\n },\n defaultVariants: {\n orientation: \"horizontal\",\n },\n },\n);\n\nexport interface ButtonGroupProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof buttonGroupVariants> {}\n\nconst ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(\n ({ className, orientation = \"horizontal\", ...props }, ref) => (\n <div\n ref={ref}\n role=\"group\"\n data-slot=\"button-group\"\n data-orientation={orientation}\n className={cn(buttonGroupVariants({ orientation }), className)}\n {...props}\n />\n ),\n);\nButtonGroup.displayName = \"ButtonGroup\";\n\nexport interface ButtonGroupSeparatorProps\n extends React.ComponentPropsWithoutRef<typeof Separator> {}\n\nconst ButtonGroupSeparator = React.forwardRef<\n React.ElementRef<typeof Separator>,\n ButtonGroupSeparatorProps\n>(({ className, orientation = \"vertical\", ...props }, ref) => (\n <Separator\n ref={ref}\n decorative\n orientation={orientation}\n data-slot=\"button-group-separator\"\n className={cn(\n orientation === \"vertical\"\n ? \"mx-0 h-auto self-stretch\"\n : \"my-0 w-auto self-stretch\",\n className,\n )}\n {...props}\n />\n));\nButtonGroupSeparator.displayName = \"ButtonGroupSeparator\";\n\nexport interface ButtonGroupTextProps\n extends React.HTMLAttributes<HTMLDivElement> {\n asChild?: boolean;\n}\n\nconst ButtonGroupText = React.forwardRef<HTMLDivElement, ButtonGroupTextProps>(\n ({ className, asChild = false, ...props }, ref) => {\n const Comp = asChild ? Slot : \"div\";\n\n return (\n <Comp\n ref={ref}\n data-slot=\"button-group-text\"\n className={cn(\n \"flex h-10 items-center whitespace-nowrap rounded border border-border bg-background px-3 text-sm font-medium text-muted-foreground\",\n className,\n )}\n {...props}\n />\n );\n },\n);\nButtonGroupText.displayName = \"ButtonGroupText\";\n\nexport {\n ButtonGroup,\n ButtonGroupSeparator,\n ButtonGroupText,\n buttonGroupVariants,\n};\n"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": "components/base/ButtonGroup.md",
|
|
23
|
+
"type": "registry:file",
|
|
24
|
+
"target": "components/base/ButtonGroup.md",
|
|
25
|
+
"content": "# ButtonGroup — guía de uso\n\nAgrupa botones relacionados en una sola unidad visual. Sirve para acciones hermanas, comandos de toolbar o split actions. No mantiene estado de selección por sí mismo. El componente vive en tu proyecto: instálalo con `create-lexy add button-group` y edítalo con libertad.\n\n## Import\n\n```tsx\nimport {\n Button,\n ButtonGroup,\n ButtonGroupSeparator,\n ButtonGroupText,\n} from \"@/components/base/ButtonGroup\";\n```\n\n## Uso básico\n\n```tsx\n<ButtonGroup aria-label=\"Acciones del documento\">\n <Button variant=\"outline\">Editar</Button>\n <Button variant=\"outline\">Duplicar</Button>\n <Button variant=\"outline\">Archivar</Button>\n</ButtonGroup>\n```\n\n## Orientación vertical\n\n```tsx\n<ButtonGroup orientation=\"vertical\" aria-label=\"Acciones rápidas\">\n <Button variant=\"outline\">Asignar</Button>\n <Button variant=\"outline\">Comentar</Button>\n <Button variant=\"outline\">Cerrar</Button>\n</ButtonGroup>\n```\n\n## Separadores\n\nUsa `ButtonGroupSeparator` cuando el grupo mezcla acciones con pesos distintos o cuando las variantes sin borde necesitan separación explícita.\n\n```tsx\n<ButtonGroup aria-label=\"Exportar\">\n <Button variant=\"secondary\">Exportar PDF</Button>\n <ButtonGroupSeparator />\n <Button variant=\"secondary\" size=\"icon\" aria-label=\"Más formatos\">\n <ChevronDown />\n </Button>\n</ButtonGroup>\n```\n\n## Texto auxiliar\n\n`ButtonGroupText` permite agregar una etiqueta breve alineada con los controles.\n\n```tsx\n<ButtonGroup aria-label=\"Filtros de estado\">\n <ButtonGroupText>Estado</ButtonGroupText>\n <Button variant=\"outline\">Activo</Button>\n <Button variant=\"outline\">Pendiente</Button>\n</ButtonGroup>\n```\n\n## Props\n\n| Prop | Tipo | Default | Descripción |\n|---|---|---|---|\n| `orientation` | `\"horizontal\" \\| \"vertical\"` | `\"horizontal\"` | Dirección del grupo y forma de conectar bordes/radios. |\n| `aria-label` / `aria-labelledby` | `string` | — | Etiqueta accesible del grupo cuando no hay título visible. |\n| ...resto | `HTMLAttributes<HTMLDivElement>` | — | `className`, `id`, `data-*`, etc. |\n\n## Reglas\n\n- Usa `ButtonGroup` solo para acciones relacionadas por contexto inmediato.\n- Agrega `aria-label` o `aria-labelledby` si el grupo no tiene un título visible.\n- Mantén los botones con variantes compatibles; `outline` funciona bien para grupos conectados.\n- En botones solo-icono, cada `Button` debe tener su propio `aria-label`.\n- No uses el grupo para mostrar selección persistente: visualmente son acciones, no opciones activas.\n\n## Cuándo NO usar\n\n- **Elegir una opción activa** → `Tabs`, `RadioGroup` o `Select`.\n- **Activar/desactivar una preferencia** → `Switch` o `Checkbox`.\n- **Barra de navegación** → navegación real con `Link`/`a`, no botones agrupados.\n- **Acciones sin relación fuerte** → botones separados con espaciado normal.\n\n## Para IA\n\n1. Primero decide si son acciones hermanas o si realmente son opciones seleccionables.\n2. El componente vive en tu proyecto: impórtalo local (`@/components/base/ButtonGroup`) y edítalo con libertad si necesitas cambiar estructura, lógica o defaults.\n3. Usa `ButtonGroup` con `aria-label` claro.\n4. Usa `Button` dentro del grupo; no reconstruyas botones con `div` o clases manuales.\n5. Si hay icon-only buttons, agrega `aria-label` individual.\n6. Para selección persistente, cambia a `Tabs`/`RadioGroup` en vez de forzar `ButtonGroup`.\n"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
package/assets/r/button.json
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"path": "components/base/Button.tsx",
|
|
15
15
|
"type": "registry:ui",
|
|
16
|
+
"target": "components/base/Button.tsx",
|
|
16
17
|
"content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap select-none rounded text-sm font-medium transition-colors duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n {\n variants: {\n variant: {\n default:\n \"bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80\",\n destructive:\n \"bg-destructive text-destructive-foreground hover:bg-destructive/90 active:bg-destructive/80\",\n outline:\n \"border border-border bg-background text-foreground hover:border-primary hover:text-primary active:bg-accent active:text-accent-foreground\",\n secondary:\n \"bg-background text-secondary-foreground hover:bg-muted active:bg-accent active:text-accent-foreground\",\n ghost: \"text-secondary-foreground hover:bg-muted active:bg-muted\",\n link: \"text-primary underline-offset-4 hover:underline active:text-primary/80\",\n },\n size: {\n default: \"h-10 px-4 py-2\",\n sm: \"h-8 px-3\",\n lg: \"h-12 px-5\",\n icon: \"h-10 w-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n },\n);\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n /** Renderiza el hijo como el elemento raíz (patrón Slot de Radix): `<Button asChild><Link …/></Button>`. */\n asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n ({ className, variant, size, asChild = false, ...props }, ref) => {\n const Comp = asChild ? Slot : \"button\";\n return (\n <Comp\n className={cn(buttonVariants({ variant, size, className }))}\n ref={ref}\n {...props}\n />\n );\n },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
|
|
17
18
|
},
|
|
18
19
|
{
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "calendar",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Calendar",
|
|
6
|
+
"description": "Selector de fechas construido sobre [React DayPicker](https://daypicker.dev),",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"lucide-react",
|
|
9
|
+
"react-day-picker@^9.8.0"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"button"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/base/Calendar.tsx",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/base/Calendar.tsx",
|
|
19
|
+
"content": "import * as React from \"react\";\nimport {\n DayPicker,\n getDefaultClassNames,\n type DayButton,\n type Locale,\n} from \"react-day-picker\";\nimport {\n ChevronLeftIcon,\n ChevronRightIcon,\n ChevronDownIcon,\n} from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils/cn\";\nimport { Button, buttonVariants } from \"./Button\";\n\nfunction Calendar({\n className,\n classNames,\n showOutsideDays = true,\n captionLayout = \"label\",\n buttonVariant = \"ghost\",\n locale,\n formatters,\n components,\n ...props\n}: React.ComponentProps<typeof DayPicker> & {\n buttonVariant?: React.ComponentProps<typeof Button>[\"variant\"];\n}) {\n const defaultClassNames = getDefaultClassNames();\n\n return (\n <DayPicker\n showOutsideDays={showOutsideDays}\n className={cn(\n \"group/calendar bg-background p-3 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(8)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent\",\n String.raw`rtl:**:[.rdp-button\\_next>svg]:rotate-180`,\n String.raw`rtl:**:[.rdp-button\\_previous>svg]:rotate-180`,\n className,\n )}\n captionLayout={captionLayout}\n locale={locale}\n formatters={{\n formatMonthDropdown: (date) =>\n date.toLocaleString(locale?.code, { month: \"short\" }),\n ...formatters,\n }}\n classNames={{\n root: cn(\"w-fit\", defaultClassNames.root),\n months: cn(\n \"relative flex flex-col gap-4 md:flex-row\",\n defaultClassNames.months,\n ),\n month: cn(\"flex w-full flex-col gap-4\", defaultClassNames.month),\n nav: cn(\n \"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1\",\n defaultClassNames.nav,\n ),\n button_previous: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) p-0 select-none aria-disabled:opacity-50\",\n defaultClassNames.button_previous,\n ),\n button_next: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) p-0 select-none aria-disabled:opacity-50\",\n defaultClassNames.button_next,\n ),\n month_caption: cn(\n \"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)\",\n defaultClassNames.month_caption,\n ),\n dropdowns: cn(\n \"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium\",\n defaultClassNames.dropdowns,\n ),\n dropdown_root: cn(\n \"cn-calendar-dropdown-root relative rounded-(--cell-radius)\",\n defaultClassNames.dropdown_root,\n ),\n dropdown: cn(\n \"absolute inset-0 bg-popover opacity-0\",\n defaultClassNames.dropdown,\n ),\n caption_label: cn(\n \"font-medium select-none\",\n captionLayout === \"label\"\n ? \"cn-calendar-caption text-sm\"\n : \"cn-calendar-caption-label flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground\",\n defaultClassNames.caption_label,\n ),\n table: \"w-full border-collapse\",\n weekdays: cn(\"flex\", defaultClassNames.weekdays),\n weekday: cn(\n \"flex-1 rounded-(--cell-radius) text-xs font-normal text-muted-foreground select-none\",\n defaultClassNames.weekday,\n ),\n week: cn(\"mt-2 flex w-full\", defaultClassNames.week),\n week_number_header: cn(\n \"w-(--cell-size) select-none\",\n defaultClassNames.week_number_header,\n ),\n week_number: cn(\n \"text-xs text-muted-foreground select-none\",\n defaultClassNames.week_number,\n ),\n day: cn(\n \"group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)\",\n props.showWeekNumber\n ? \"[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)\"\n : \"[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)\",\n defaultClassNames.day,\n ),\n range_start: cn(\n \"relative isolate z-0 rounded-l-(--cell-radius) bg-background after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-background\",\n defaultClassNames.range_start,\n ),\n range_middle: cn(\n \"rounded-none bg-background\",\n defaultClassNames.range_middle,\n ),\n range_end: cn(\n \"relative isolate z-0 rounded-r-(--cell-radius) bg-background after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-background\",\n defaultClassNames.range_end,\n ),\n today: cn(\n \"rounded-(--cell-radius) bg-background text-foreground data-[selected=true]:rounded-none\",\n defaultClassNames.today,\n ),\n outside: cn(\n \"text-muted-foreground aria-selected:text-muted-foreground\",\n defaultClassNames.outside,\n ),\n disabled: cn(\n \"text-muted-foreground opacity-50\",\n defaultClassNames.disabled,\n ),\n hidden: cn(\"invisible\", defaultClassNames.hidden),\n ...classNames,\n }}\n components={{\n Root: ({ className, rootRef, ...props }) => {\n return (\n <div\n data-slot=\"calendar\"\n ref={rootRef}\n className={cn(className)}\n {...props}\n />\n );\n },\n Chevron: ({ className, orientation, ...props }) => {\n if (orientation === \"left\") {\n return (\n <ChevronLeftIcon\n className={cn(\"cn-rtl-flip size-4\", className)}\n {...props}\n />\n );\n }\n\n if (orientation === \"right\") {\n return (\n <ChevronRightIcon\n className={cn(\"cn-rtl-flip size-4\", className)}\n {...props}\n />\n );\n }\n\n return (\n <ChevronDownIcon className={cn(\"size-4\", className)} {...props} />\n );\n },\n DayButton: ({ ...props }) => (\n <CalendarDayButton locale={locale} {...props} />\n ),\n WeekNumber: ({ children, ...props }) => {\n return (\n <td {...props}>\n <div className=\"flex size-(--cell-size) items-center justify-center text-center\">\n {children}\n </div>\n </td>\n );\n },\n ...components,\n }}\n {...props}\n />\n );\n}\n\nfunction CalendarDayButton({\n className,\n day,\n modifiers,\n locale,\n ...props\n}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {\n const defaultClassNames = getDefaultClassNames();\n\n const ref = React.useRef<HTMLButtonElement>(null);\n React.useEffect(() => {\n if (modifiers.focused) ref.current?.focus();\n }, [modifiers.focused]);\n\n return (\n <Button\n ref={ref}\n variant=\"ghost\"\n size=\"icon\"\n data-day={day.date.toLocaleDateString(locale?.code)}\n data-selected-single={\n modifiers.selected &&\n !modifiers.range_start &&\n !modifiers.range_end &&\n !modifiers.range_middle\n }\n data-range-start={modifiers.range_start}\n data-range-end={modifiers.range_end}\n data-range-middle={modifiers.range_middle}\n className={cn(\n // Sin ring por data-focused: ese estado se marca al abrir aunque sea con\n // mouse. El foco de teclado lo cubre el focus-visible:ring del Button.\n \"relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal focus-visible:z-10 data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70\",\n defaultClassNames.day,\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { Calendar, CalendarDayButton };\n"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": "components/base/Calendar.md",
|
|
23
|
+
"type": "registry:file",
|
|
24
|
+
"target": "components/base/Calendar.md",
|
|
25
|
+
"content": "# Calendar — guía de uso\n\nSelector de fechas construido sobre [React DayPicker](https://daypicker.dev),\ncon los estilos y el `Button` de Lexy. Soporta selección simple, múltiple y de\nrango, navegación por mes/año y localización. El componente vive en tu proyecto: instálalo con `create-lexy add calendar` y edítalo con libertad.\n\n## Import\n\n```tsx\nimport { Calendar } from \"@/components/base/Calendar\";\nimport { es } from \"react-day-picker/locale\";\n```\n\n## Uso básico (fecha única)\n\n```tsx\nconst [date, setDate] = React.useState<Date | undefined>(new Date());\n\n<Calendar\n mode=\"single\"\n selected={date}\n onSelect={setDate}\n locale={es}\n className=\"rounded-lg border border-border\"\n/>;\n```\n\n## Rango\n\n```tsx\nconst [range, setRange] = React.useState<DateRange | undefined>();\n\n<Calendar\n mode=\"range\"\n selected={range}\n onSelect={setRange}\n numberOfMonths={2}\n locale={es}\n className=\"rounded-lg border border-border\"\n/>;\n```\n\n## Selección múltiple\n\n```tsx\n<Calendar mode=\"multiple\" selected={dates} onSelect={setDates} locale={es} />\n```\n\n## Navegación con dropdowns\n\n`captionLayout=\"dropdown\"` reemplaza el título por selectores de mes y año.\n\n```tsx\n<Calendar\n mode=\"single\"\n captionLayout=\"dropdown\"\n locale={es}\n className=\"rounded-lg border border-border\"\n/>\n```\n\n## Props (las más usadas)\n\n| Prop | Tipo | Descripción |\n|---|---|---|\n| `mode` | `\"single\" \\| \"multiple\" \\| \"range\"` | Tipo de selección. |\n| `selected` / `onSelect` | según `mode` | Valor controlado y callback. |\n| `captionLayout` | `\"label\" \\| \"dropdown\" \\| \"dropdown-months\" \\| \"dropdown-years\"` | Encabezado como texto o como selectores. |\n| `numberOfMonths` | `number` | Cantidad de meses visibles. |\n| `locale` | `Locale` | Localización (p. ej. `es` de `react-day-picker/locale`). |\n| `buttonVariant` | variante de `Button` | Variante de los botones de navegación. |\n| `disabled` | `Matcher \\| Matcher[]` | Días deshabilitados. |\n| ...resto | `DayPickerProps` | Todas las props de React DayPicker. |\n\n## Reglas\n\n- Es un componente controlado: maneja `selected`/`onSelect` con estado propio.\n- Pasa `locale` (p. ej. `es`) para meses y días en español.\n- Para elegir una fecha dentro de un formulario, combínalo con `Popover` + un `Button` que muestre la fecha (patrón date-picker).\n- No reconstruyas la grilla a mano; toda la estructura sale de React DayPicker.\n\n## Cuándo NO usar\n\n- **Una hora/duración** sin fecha → un `Input type=\"time\"` o un selector dedicado.\n- **Rango de fechas en texto** con muchos presets → considera un date-picker con presets sobre este Calendar.\n\n## Para IA\n\n1. Decide el `mode` (`single`, `multiple` o `range`) según el caso.\n2. Importa `Calendar` local (`@/components/base/Calendar`) y el locale (`es`) desde `react-day-picker/locale`.\n3. Mantén el estado de `selected` y pásalo con su `onSelect`.\n4. Para un date-picker de formulario, envuélvelo en `Popover` con un `Button` disparador.\n5. Usa `captionLayout=\"dropdown\"` cuando el usuario deba saltar muchos meses/años.\n"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "card",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Card",
|
|
6
|
+
"description": "Contenedor de contenido. Para agrupar información relacionada. El componente vive en tu proyecto: instálalo con `create-lexy add card` y edítalo con libertad.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"class-variance-authority"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "components/base/Card.tsx",
|
|
14
|
+
"type": "registry:ui",
|
|
15
|
+
"target": "components/base/Card.tsx",
|
|
16
|
+
"content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils/cn\";\n\nconst cardVariants = cva(\n \"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-lg bg-background py-(--card-spacing) text-sm text-card-foreground shadow-sm ring-1 ring-border has-[>img:first-child]:pt-0 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg\",\n {\n variants: {\n size: {\n default: \"[--card-spacing:--spacing(6)]\",\n sm: \"[--card-spacing:--spacing(4)]\",\n },\n },\n defaultVariants: { size: \"default\" },\n },\n);\n\nexport interface CardProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof cardVariants> {}\n\nconst Card = React.forwardRef<HTMLDivElement, CardProps>(\n ({ className, size, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"card\"\n data-size={size ?? \"default\"}\n className={cn(cardVariants({ size }), className)}\n {...props}\n />\n ),\n);\nCard.displayName = \"Card\";\n\nconst CardHeader = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"card-header\"\n className={cn(\n \"@container/card-header grid auto-rows-min items-start gap-1 rounded-t-lg px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)\",\n className,\n )}\n {...props}\n />\n));\nCardHeader.displayName = \"CardHeader\";\n\nconst CardTitle = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"card-title\"\n className={cn(\n \"text-base font-medium leading-normal text-foreground group-data-[size=sm]/card:text-sm\",\n className,\n )}\n {...props}\n />\n));\nCardTitle.displayName = \"CardTitle\";\n\nconst CardDescription = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"card-description\"\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n));\nCardDescription.displayName = \"CardDescription\";\n\nconst CardAction = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"card-action\"\n className={cn(\n \"col-start-2 row-span-2 row-start-1 self-start justify-self-end\",\n className,\n )}\n {...props}\n />\n));\nCardAction.displayName = \"CardAction\";\n\nconst CardContent = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"card-content\"\n className={cn(\"px-(--card-spacing)\", className)}\n {...props}\n />\n));\nCardContent.displayName = \"CardContent\";\n\nconst CardFooter = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-slot=\"card-footer\"\n className={cn(\n \"flex items-center rounded-b-lg px-(--card-spacing) [.border-t]:pt-(--card-spacing)\",\n className,\n )}\n {...props}\n />\n));\nCardFooter.displayName = \"CardFooter\";\n\nexport {\n Card,\n CardHeader,\n CardFooter,\n CardTitle,\n CardAction,\n CardDescription,\n CardContent,\n cardVariants,\n};\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "components/base/Card.md",
|
|
20
|
+
"type": "registry:file",
|
|
21
|
+
"target": "components/base/Card.md",
|
|
22
|
+
"content": "# Card — guía de uso\n\nContenedor de contenido. Para agrupar información relacionada. El componente vive en tu proyecto: instálalo con `create-lexy add card` y edítalo con libertad.\n\n## Import\n\n```tsx\nimport { Card } from \"@/components/base/Card\";\n```\n\n## Uso básico\n\n```tsx\n<Card />\n```\n\n## Props\n\nConsulta la story en Storybook para ver las props disponibles.\n\n## Reglas\n\n- Usa Card según el propósito descrito.\n- No abuses de este componente en contexts donde no aplica.\n\n## Para IA\n\n1. Identifica el contexto de uso del componente.\n2. Importa Card 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
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|