Mostrando entradas con la etiqueta shadcn-ui. Mostrar todas las entradas
Mostrando entradas con la etiqueta shadcn-ui. Mostrar todas las entradas

sábado, 24 de enero de 2026

Shadcn/ui popup dialog

 ¿Tiene shadcn ui algún popup dialog que adentro pueda tener cualquier HTML y JSX?

Sí, shadcn/ui incluye un componente de Dialog que funciona como un popup modal y dentro de su contenido (DialogContent) podés renderizar cualquier HTML o JSX sin restricciones. Está construido sobre los primitives de Radix UI, lo que garantiza accesibilidad, manejo de foco y soporte para teclado.

🔹 Cómo funciona el Dialog en shadcn/ui

  • Componentes principales:

    • Dialog: contenedor raíz.

    • DialogTrigger: define el elemento que abre el popup.

    • DialogContent: el área del popup donde podés poner cualquier HTML/JSX.

    • DialogHeader, DialogTitle, DialogDescription: opcionales para estructurar el contenido.

    • DialogFooter: opcional para botones de acción.

    • DialogClose: para cerrar el popup.

  • Flexibilidad: Dentro de DialogContent podés incluir formularios, inputs, listas, imágenes, componentes personalizados, incluso otros hooks de React. No hay limitación: es un contenedor React normal.

🔹 Ejemplo práctico

tsx
import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogClose,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

export function SoapDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="outline">Abrir popup</Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-[425px]">
        <DialogHeader>
          <DialogTitle>Editar jabón</DialogTitle>
          <DialogDescription>
            Podés modificar cualquier dato del jabón aquí.
          </DialogDescription>
        </DialogHeader>
        {/* Aquí podés poner cualquier HTML/JSX */}
        <form className="space-y-4">
          <input type="text" placeholder="Nombre" className="border p-2 w-full" />
          <textarea placeholder="Descripción" className="border p-2 w-full" />
        </form>
        <DialogFooter>
          <Button type="submit">Guardar</Button>
          <DialogClose asChild>
            <Button variant="secondary">Cancelar</Button>
          </DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

👉 Dentro de DialogContent podés poner cualquier cosa: inputs, tablas, componentes React, incluso otro layout complejo.

🔹 Variantes disponibles

  • Alert Dialog (confirmaciones destructivas).

  • Scrollable Dialog (contenido largo con scroll).

  • Fullscreen Dialog (ocupa toda la pantalla).

  • Animaciones con Motion para transiciones suaves.

✨ Conclusión

  • Sí, shadcn/ui tiene un Dialog que actúa como popup modal.

  • Dentro de DialogContent podés renderizar cualquier HTML o JSX.

  • Es accesible, flexible y extensible, ideal para formularios, confirmaciones o contenido complejo.

¿Querés que te arme un ejemplo donde el Dialog contenga un componente React completo (por ejemplo, un formulario con validación y estado) para ver cómo se integra dentro del popup?

Sí.

Genial, Gabriel 🙌. Te armo un ejemplo completo donde el Dialog de shadcn/ui contiene un formulario React con validación y estado, para que veas cómo se integra dentro del popup:

🔹 Ejemplo: Formulario dentro de un Dialog

tsx
import * as React from "react"
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"

export function SoapDialogForm() {
  const [name, setName] = React.useState("")
  const [description, setDescription] = React.useState("")
  const [error, setError] = React.useState("")

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    if (!name.trim()) {
      setError("El nombre es obligatorio")
      return
    }
    // Aquí podrías enviar al back end
    console.log("Datos enviados:", { name, description })
    setError("")
  }

  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="outline">Editar jabón</Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-[425px]">
        <DialogHeader>
          <DialogTitle>Editar jabón</DialogTitle>
          <DialogDescription>
            Modificá los datos y guardalos en el back end.
          </DialogDescription>
        </DialogHeader>

        <form onSubmit={handleSubmit} className="space-y-4">
          <input
            type="text"
            placeholder="Nombre"
            value={name}
            onChange={(e) => setName(e.target.value)}
            className="border p-2 w-full"
          />
          <textarea
            placeholder="Descripción"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            className="border p-2 w-full"
          />
          {error && <p className="text-red-500 text-sm">{error}</p>}
          <DialogFooter>
            <Button type="submit">Guardar</Button>
            <DialogClose asChild>
              <Button variant="secondary">Cancelar</Button>
            </DialogClose>
          </DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
  )
}

🔹 Qué pasa aquí

  • El Dialog abre un popup modal.

  • Dentro de DialogContent hay un formulario React con estado (useState).

  • Se valida que el nombre no esté vacío antes de enviar.

  • Podés reemplazar el console.log por una llamada al back end (fetch, axios, o React Query).

  • El DialogClose permite cerrar el popup desde un botón.

✨ Con esto ves que el Dialog de shadcn/ui es totalmente flexible: podés meter cualquier HTML/JSX, manejar estado local, validaciones y lógica de negocio sin limitaciones.

¿Querés que te muestre cómo integrar este formulario con React Query para que al guardar se actualice automáticamente la lista de jabones en tu UI?

Sí.

Perfecto, Gabriel 🙌. Vamos a integrar el formulario dentro del Dialog de shadcn/ui con React Query, de modo que al guardar se actualice automáticamente la lista de jabones en tu UI.

🔹 Repositorio de datos

ts
// repositories/soapRepository.ts
export async function getSoaps() {
  const res = await fetch("/api/soaps");
  if (!res.ok) throw new Error("Error al obtener jabones");
  return res.json();
}

export async function updateSoap(id: string, data: any) {
  const res = await fetch(`/api/soaps/${id}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });
  if (!res.ok) throw new Error("Error al actualizar jabón");
  return res.json();
}

🔹 Hook con React Query

ts
// hooks/useSoaps.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { getSoaps, updateSoap } from "@repositories/soapRepository";

export function useSoaps() {
  return useQuery({
    queryKey: ["soaps"],
    queryFn: getSoaps,
  });
}

export function useUpdateSoap() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: any }) =>
      updateSoap(id, data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["soaps"] }); // ← refresca la lista
    },
  });
}

🔹 Formulario dentro del Dialog

tsx
import * as React from "react";
import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogClose,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useUpdateSoap } from "@hooks/useSoaps";

export function SoapDialogForm({ soap }: { soap: any }) {
  const [name, setName] = React.useState(soap.nombre);
  const [description, setDescription] = React.useState(soap.descripcion);
  const mutation = useUpdateSoap();

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    mutation.mutate({ id: soap.id, data: { nombre: name, descripcion: description } });
  }

  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="outline">Editar {soap.nombre}</Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-[425px]">
        <DialogHeader>
          <DialogTitle>Editar jabón</DialogTitle>
          <DialogDescription>
            Modificá los datos y guardalos en el back end.
          </DialogDescription>
        </DialogHeader>

        <form onSubmit={handleSubmit} className="space-y-4">
          <input
            type="text"
            placeholder="Nombre"
            value={name}
            onChange={(e) => setName(e.target.value)}
            className="border p-2 w-full"
          />
          <textarea
            placeholder="Descripción"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            className="border p-2 w-full"
          />
          <DialogFooter>
            <Button type="submit">Guardar</Button>
            <DialogClose asChild>
              <Button variant="secondary">Cancelar</Button>
            </DialogClose>
          </DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
  );
}

🔹 Lista de jabones que se refresca automáticamente

tsx
import { useSoaps } from "@hooks/useSoaps";
import { SoapDialogForm } from "@components/SoapDialogForm";

export default function SoapList() {
  const { data: soaps, isLoading } = useSoaps();

  if (isLoading) return <p>Cargando jabones...</p>;

  return (
    <div className="space-y-4">
      {soaps.map((soap: any) => (
        <div key={soap.id} className="border p-4">
          <h2>{soap.nombre}</h2>
          <p>{soap.descripcion}</p>
          <SoapDialogForm soap={soap} />
        </div>
      ))}
    </div>
  );
}

✨ Flujo completo

  1. SoapList carga la lista de jabones desde el back end.

  2. Cada jabón tiene un botón que abre un Dialog con el formulario.

  3. Al guardar, se dispara la mutación con React Query.

  4. onSuccess invalida la query → se refresca la lista automáticamente.

  5. La UI siempre queda sincronizada con el back end.

¿Querés que te prepare también un diagrama visual del flujo mostrando cómo el Dialog dispara la mutación y cómo React Query refresca la lista de jabones?

Ayer
¿Se pueden combinar las variantes scrollable dialog y Fullscreen dialog?

Muy buena pregunta, Gabriel 👌. En shadcn/ui (que se apoya en los Dialog primitives de Radix UI), las variantes como Scrollable y Fullscreen no son “tipos distintos de componente”, sino combinaciones de estilos y props aplicadas sobre el mismo DialogContent.

🔹 Cómo funcionan las variantes

  • Scrollable Dialog: se logra aplicando clases utilitarias (ej. max-h-[80vh] overflow-y-auto) al DialogContent para que el contenido tenga scroll interno.

  • Fullscreen Dialog: se logra aplicando clases que expanden el DialogContent a ocupar toda la pantalla (ej. w-screen h-screen max-w-none).

🔹 Combinación posible

Sí, podés combinarlas. Por ejemplo, un DialogContent que ocupe toda la pantalla y además tenga scroll interno:

tsx
<DialogContent className="w-screen h-screen max-w-none overflow-y-auto">
  <DialogHeader>
    <DialogTitle>Jabón en pantalla completa</DialogTitle>
    <DialogDescription>
      Este contenido ocupa toda la pantalla y se puede scrollear.
    </DialogDescription>
  </DialogHeader>
  <div className="space-y-4 p-6">
    {/* Aquí podés poner cualquier JSX largo */}
    <p>Sección 1...</p>
    <p>Sección 2...</p>
    <p>Sección 3...</p>
    {/* etc */}
  </div>
</DialogContent>

👉 Esto combina ambas variantes:

  • Fullscreen: ocupa toda la pantalla.

  • Scrollable: si el contenido es más largo que la altura, aparece scroll interno.

✨ Conclusión

  • Las variantes en shadcn/ui son patrones de estilo, no componentes separados.

  • Podés mezclarlas libremente aplicando las clases adecuadas.

  • Un DialogContent fullscreen puede ser scrollable sin problemas.

¿Querés que te arme un ejemplo extendido con contenido muy largo (ej. lista de productos) para que veas cómo se comporta el scroll dentro de un fullscreen dialog?

jueves, 17 de julio de 2025

Estructura de la Carpeta Resources en Laravel, Inertia, React y shadcn/ui

 ¿De qué consta lo que va dentro de la carpeta resources que corresponde a la parte visual de una aplicación con Laravel, Inertia, React y shadcn/ui? ¿Qué partes lo componen, cuál es su estructura?


En una aplicación Laravel que utiliza Inertia.js con React como frontend y shadcn/ui para los componentes, la carpeta resources es el corazón de tu parte visual (frontend). Aquí es donde resides todo el código JavaScript, CSS y otros activos que se compilan para el navegador.

La estructura típica y sus componentes principales son los siguientes:

resources

La carpeta resources contiene los activos sin compilar de tu aplicación, como archivos Blade, archivos de idioma y tus activos de frontend (JavaScript, CSS, imágenes, etc.).

  • resources/css:

    • app.css: Este es el archivo principal de tu CSS. En una configuración con Tailwind CSS, aquí es donde importarías las directivas de Tailwind (@tailwind base;, @tailwind components;, @tailwind utilities;) y cualquier CSS personalizado global.

      /* resources/css/app.css */
      @tailwind base;
      @tailwind components;
      @tailwind utilities;
      
      /* Tu CSS personalizado aquí (si lo necesitas) */
      
  • resources/js: Este es el directorio más importante para tu aplicación React/Inertia.

    • app.js: El punto de entrada principal de tu aplicación JavaScript. Aquí es donde se inicializa Inertia y se monta tu aplicación React. También es donde se importan los layouts y componentes de nivel superior.

      // resources/js/app.js
      import './bootstrap'; // Para Axios, etc.
      import '../css/app.css'; // Importa tu CSS de Tailwind
      
      import { createInertiaApp } from '@inertiajs/react';
      import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
      import { createRoot } from 'react-dom/client';
      
      createInertiaApp({
          // Resuelve los componentes de página React
          resolve: (name) => resolvePageComponent(`./Pages/${name}.jsx`, import.meta.glob('./Pages/**/*.jsx')),
          setup({ el, App, props }) {
              const root = createRoot(el);
              root.render(<App {...props} />);
          },
          progress: {
              color: '#4B5563', // Color de la barra de progreso de Inertia
          },
      });
      
    • bootstrap.js: Configuración inicial de JavaScript, como la configuración de Axios para realizar solicitudes HTTP.

    • Layouts:

      • AuthenticatedLayout.jsx: Un componente React que define el diseño general (header, sidebar, footer) para las páginas que requieren autenticación. Las páginas de Inertia suelen envolver su contenido en uno de estos layouts.

      • GuestLayout.jsx: Similar al anterior, pero para páginas públicas (login, registro, etc.).

    • Pages: Este directorio contiene tus componentes de página principales de React. Cada archivo aquí (o en sus subdirectorios) corresponde a una "página" que Inertia puede cargar.

      • Dashboard.jsx: Un ejemplo de componente de página.

      • Profile/Edit.jsx: Un componente de página anidado.

      • Auth/Login.jsx: Componentes de autenticación.

      • Admin/Users/Index.jsx: Ejemplo de una sección de administración.

    • Components: Este directorio contiene componentes React reutilizables que no son páginas completas. Aquí es donde brillan los componentes de shadcn/ui y tus propios componentes basados en ellos.

      • ui: Esta subcarpeta es crucial para shadcn/ui. Contiene los componentes que has "shadcn-izado" (copiado y adaptado a tu proyecto). Por ejemplo:

        • button.jsx (el componente Button de shadcn)

        • card.jsx (el componente Card de shadcn)

        • input.jsx (el componente Input de shadcn)

        • dialog.jsx (el componente Dialog de shadcn)

        • table.jsx (el componente Table de shadcn)

        • ...y muchos más, cada uno con sus estilos de Tailwind y lógica React.

      • ApplicationLogo.jsx: Tu propio componente para el logo de la aplicación.

      • Dropdown.jsx: Un componente de menú desplegable.

      • NavLink.jsx: Componentes para enlaces de navegación.

      • TextInput.jsx: Un wrapper para el input de shadcn/ui con manejo de errores de Inertia.

      • ThemeToggle.jsx: Un componente para cambiar entre tema claro/oscuro.

  • resources/views:

    • app.blade.php: Este es el archivo Blade principal que sirve como la "plantilla" base para tu aplicación Inertia. Contiene el div con el id="app" donde se montará tu aplicación React. También incluye los scripts de Vite para la compilación de activos.

      <!-- resources/views/app.blade.php -->
      <!DOCTYPE html>
      <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
          <head>
              <meta charset="utf-8">
              <meta name="viewport" content="width=device-width, initial-scale=1">
              <title inertia>{{ config('app.name', 'Laravel') }}</title>
              <!-- Scripts de Vite para cargar tu JS y CSS -->
              @viteReactRefresh
              @vite(['resources/js/app.js', 'resources/css/app.css'])
              @inertiaHead
          </head>
          <body class="font-sans antialiased">
              <div id="app" class="min-h-screen bg-gray-100 dark:bg-gray-900">
                  <!-- Aquí es donde Inertia montará tu aplicación React -->
              </div>
          </body>
      </html>
      
    • Otros archivos Blade (si los hay): Si tienes algunas vistas que no son manejadas por Inertia (ej. un email de bienvenida, una página de error estática), irían aquí.

  • resources/lang: Contiene los archivos de idioma de tu aplicación.

Flujo General:

  1. Cuando Laravel recibe una solicitud para una página Inertia, renderiza resources/views/app.blade.php.

  2. Este app.blade.php carga resources/js/app.js (y resources/css/app.css) a través de Vite.

  3. resources/js/app.js inicializa Inertia y React.

  4. Inertia determina qué "página" de resources/js/Pages debe renderizarse según la ruta de Laravel.

  5. Los componentes de shadcn/ui y tus propios componentes reutilizables (de resources/js/Components) se utilizan dentro de tus componentes de página y layouts para construir la interfaz de usuario.

  6. Tailwind CSS, importado en resources/css/app.css, proporciona todas las utilidades de estilo que usas directamente en tus componentes React.

Esta estructura promueve una clara separación de responsabilidades y facilita el desarrollo de aplicaciones modernas con Laravel, Inertia y React.

miércoles, 16 de julio de 2025

Estética para una aplicación web odontológica

 Diseñar una aplicación con una estética profesional y acorde a un consultorio odontológico, utilizando shadcn/ui y Tailwind CSS, es una gran manera de asegurar una interfaz limpia y funcional.


Notas Adicionales para una Apariencia Profesional:

  • Fuentes: Considera usar una fuente sans-serif limpia como "Inter", "Roboto", "Open Sans" o "Lato". Puedes importarlas desde Google Fonts o Bunny Fonts como ya vimos.

  • Espaciado Consistente: Utiliza la escala de espaciado de Tailwind (p-x, m-y, gap-z) de manera consistente en toda tu aplicación.

  • Iconografía: Integra iconos de librerías como lucide-react (que shadcn/ui utiliza) o Font Awesome para añadir claridad visual a los botones y elementos de navegación.

  • Estado de Focus: Asegúrate de que los estados de focus para los elementos interactivos (botones, inputs) sean claros y accesibles. Tailwind proporciona clases para esto (focus:outline-none, focus:ring).

  • Transiciones: Las transiciones suaves (transition-colors, duration-150) en los estados hover y focus añaden un toque pulido.

Esta tabla te proporciona una base sólida para estilizar tu aplicación de consultorio odontológico con un aspecto profesional y moderno, aprovechando la flexibilidad de Tailwind CSS y la estructura de shadcn/ui.


Lista de clases para la estética de la aplicación de consultorio odontológico.


¡Bienvenido a tu Consultorio!

Gestión profesional para tu práctica odontológica.

divflex w-full items-center justify-center opacity-100 transition-opacity duration-750 lg:grow starting:opacity-0flex w-full items-center justify-center opacity-100 transition-opacity duration-750 lg:grow starting:opacity-0Fondo de la Aplicacióndivbodybg-[#FDFDFC] text-[#1b1b18] font-sansdark:bg-[#0a0a0a] dark:text-[#EDEDEC] font-sansInterBarra de Navegación (Superior)navdivflexflex items-center justify-between p-4 bg-white shadow-sm border-b border-gray-200 rounded-b-lgdark:bg-[#1A202C] dark:border-gray-700 dark:shadow-mdLogo del Consultorioimgspanh-10 w-auto object-containh-10 w-auto object-containBotón Primario (Acción Principal)Buttoninline-flex items-center justify-center rounded-md px-6 py-3 text-base font-medium bg-[#00A896] text-white shadow-sm hover:bg-[#008F7E] focus:outline-none focus:ring-2 focus:ring-[#00A896] focus:ring-offset-2 transition-colors duration-200dark:bg-[#007366] dark:hover:bg-[#005C4F] dark:focus:ring-[#007366]Botón Secundario (Acción Menor)Buttoninline-flex items-center justify-center rounded-md px-5 py-2.5 text-base font-medium bg-gray-100 text-gray-800 border border-gray-300 shadow-sm hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors duration-200dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600 dark:focus:ring-gray-500Tarjeta de ContenidoCarddivbg-white rounded-lg shadow-md overflow-hidden border border-gray-200 p-6dark:bg-[#1A202C] dark:border-gray-700 dark:shadow-lgTítulo de Sección (dentro de tarjeta)h2text-2xl font-semibold text-gray-900 mb-4dark:text-whiteCampo de Formulario (Input)Inputflex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-[#00A896] focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:placeholder:text-gray-500 dark:focus:ring-[#00A896]Etiqueta de Formulario (Label)labelblock text-sm font-medium text-gray-700 mb-1dark:text-gray-300Mensaje de Error de Formulariopspantext-sm text-red-600 mt-1dark:text-red-400Tabla de DatosTablew-full text-sm text-left text-gray-500 border-collapsedark:text-gray-400Encabezado de Tabla (<th>)TableHeaderthpx-6 py-3 bg-gray-50 text-xs font-medium text-gray-700 uppercase tracking-wider border-b border-gray-200dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600Celda de Tabla (<td>)TableCelltdpx-6 py-4 whitespace-nowrap text-gray-800 border-b border-gray-200dark:text-gray-200 dark:border-gray-700Enlace de Navegación (Texto)Linkatext-[#007BFF] hover:underline transition-colors duration-150dark:text-[#66B3FF] dark:hover:underlinePaginación (Contenedor)divPaginationflex items-center justify-between mt-6 p-4 bg-white rounded-lg shadow-sm border border-gray-200dark:bg-[#1A202C] dark:border-gray-700 dark:shadow-mdBotón de PaginaciónButtoninline-flex items-center px-3 py-1.5 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600Switch/ToggleSwitchdata-[state=checked]:bg-[#00A896] data-[state=unchecked]:bg-gray-200dark:data-[state=checked]:bg-[#007366] dark:data-[state=unchecked]:bg-gray-600Badge/Etiqueta (Estado)Badgeinline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-blue-100 text-blue-800dark:bg-blue-800 dark:text-blue-100Separador (Línea)hrdivmy-6 border-t border-gray-200dark:border-gray-700Mensaje de Éxito (Toast/Banner)divp-4 rounded-md bg-green-50 text-green-800 border border-green-200dark:bg-green-900 dark:text-green-200 dark:border-green-700Mensaje de Error (Toast/Banner)divp-4 rounded-md bg-red-50 text-red-800 border border-red-200dark:bg-red-900 dark:text-red-200 dark:border-red-700Modal/Dialogo (Fondo Oscuro)DialogOverlayfixed inset-0 z-50 bg-black/80fixed inset-0 z-50 bg-black/80Modal/Dialogo (Contenido)DialogContentfixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-white 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 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lgdark:bg-[#1A202C] dark:border-gray-700

lunes, 23 de junio de 2025

Comparación Material UI y shadcn-ui

 


¡Excelente! La elección entre librerías de componentes UI en React es crucial, y Material UI (MUI) y shadcn/ui representan dos filosofías de diseño y desarrollo bastante diferentes.

Aquí tienes una comparación entre ambas para ayudarte a decidir:


Comparación: Material UI (MUI) vs. shadcn/ui

CaracterísticaMaterial UI (MUI)shadcn/ui
Concepto PrincipalUn sistema de componentes UI preconstruidos y estilizados que implementa Material Design de Google. Una librería de UI completa "listo para usar".Colección de componentes React reusables que puedes copiar y pegar directamente en tu proyecto. No es una "librería" en el sentido tradicional (no instalas un npm i shadcn-ui). Es más un recurso de código.
Filosofía / EnfoqueOpinionada (Material Design). Ofrece una estética y un comportamiento coherentes por defecto, diseñados para ser consistentes en diferentes plataformas.Estilos con Tailwind CSS. No opinionada en el diseño (más allá de usar Tailwind). Te da la libertad de estilos completa al tener el código fuente de los componentes.
Instalación / UsoInstalación tradicional con npm install @mui/material @emotion/react @emotion/styled. Importas componentes directamente: import { Button } from '@mui/material';No hay npm install shadcn-ui. Usas la CLI de shadcn/ui para añadir componentes específicos que se copian a tu carpeta de componentes (/components/ui/) como código.
Personalización de EstilosAlto. Potente sistema de theming (paletas, tipografía, espaciado). sx prop, styled() utilidades. Componentes diseñados para ser consistentes y luego personalizables.Extremadamente Alto. Al tener el código fuente en tu proyecto, puedes modificar directamente cualquier línea de CSS (Tailwind) o añadir lógica. Control total a nivel de clase de utilidad.
Tamaño del Bundle / DependenciasGeneralmente más grande. Importar componentes trae consigo las dependencias de Material UI y Emotion (o Styled Components).Más pequeño y modular. Solo incluyes el código de los componentes que realmente usas. Depende de Radix UI (sin estilos) y Tailwind CSS.
Modo Oscuro (Dark Mode)Integrado y fácil de configurar a través del theming.Integrado y bien soportado con la configuración de Tailwind CSS para modo oscuro. Fácil de implementar.
Accesibilidad (ARIA)Muy Alta. Prioridad clave. Sigue estrictamente las guías WCAG y ARIA, con componentes probados y accesibles por defecto.Muy Alta. Los componentes se construyen sobre Radix UI, que es famoso por sus primitivas accesibles y sin estilo. Esto garantiza una base de accesibilidad sólida.
Cantidad y Variedad de ComponentesMuy Alta. Una de las librerías más completas, con componentes básicos y muchos avanzados (Data Grid, Date Pickers, Charts) y módulos de pago.Media-Alta. Cubre la mayoría de los componentes UI básicos y muchos avanzados (dialogs, data tables, carousels). La lista crece constantemente, pero no es tan vasta en "componentes de negocios" listos como MUI.
Curva de AprendizajeMedia. Necesitas entender el sistema de theming de Material Design, la sx prop, y la estructura de los componentes de MUI.Media. Necesitas estar cómodo con Tailwind CSS y con la estructura de archivos que genera shadcn/ui. El concepto de "copiar y pegar" puede ser nuevo.
Mantenimiento / ActualizacionesAutomático al actualizar el paquete npm.Manual. Cuando hay actualizaciones de shadcn/ui, debes ejecutar npx shadcn-ui@latest add <componente> de nuevo, y esto puede sobrescribir tus personalizaciones (aunque te avisa).
Casos de Uso IdealProyectos que buscan una identidad visual fuerte y consistente (Material Design), desarrollo rápido con componentes listos, y un ecosistema maduro y bien documentado.Proyectos que buscan flexibilidad de diseño total, un bundle más pequeño, y están cómodos con Tailwind CSS. Ideal para construir tu propio "sistema de diseño" personalizado sobre una base sólida.

¿Cuál elegir?

La elección entre MUI y shadcn/ui realmente depende de tus prioridades y de la naturaleza de tu proyecto:

  • Elige Material UI (MUI) si:

    • Te gusta la estética y los principios de Material Design.
    • Quieres una librería de componentes "listos para usar" que te permitan construir rápidamente.
    • Prefieres menos configuración de estilos manual y más un enfoque de "themado" global.
    • Necesitas una gran variedad de componentes complejos y de nicho ya construidos.
    • No te importa un tamaño de bundle ligeramente mayor por la comodidad que ofrece.
  • Elige shadcn/ui si:

    • Buscas una libertad de diseño total y quieres que tus componentes se vean exactamente como tú quieras, sin seguir una guía de diseño predefinida.
    • Ya usas y te encanta Tailwind CSS para tus estilos.
    • Te preocupa el tamaño del bundle final de tu aplicación y quieres minimizar el JavaScript del lado del cliente.
    • Estás dispuesto a mantener el código de tus componentes directamente en tu proyecto y prefieres la granularidad de ese control.
    • Valoras las primitivas accesibles de Radix UI y quieres construir sobre ellas.

Ambas son excelentes opciones, pero satisfacen necesidades diferentes. MUI te da un coche de carreras ya montado y pintado de fábrica; shadcn/ui te da todas las piezas y herramientas de la más alta calidad para que armes y pintes tu coche de carreras exactamente como lo desees.