# sebs7n-ui 0.1.0 > Design system para React: Geist (el lenguaje visual de Vercel) sobre las primitivas de shadcn/ui base-nova (Base UI), empaquetado como una sola dependencia. Tailwind v4, Base UI, React 19, Next 15/16. Generado de https://ui.sebastianfermanelli.com. Cada sección es una página del sitio; la misma URL + ".md" devuelve solo esa sección. --- Fuente: https://ui.sebastianfermanelli.com/docs/instalacion # Instalación > Una dependencia, un `@import` y tres variables de marca. Una dependencia, un `@import` y tres variables de marca. No hay `tailwind.config.js` ni archivos copiados al repo de la app. ## Instalar ```bash pnpm add sebs7n-ui @base-ui/react next-themes sonner geist ``` ```bash npm install sebs7n-ui @base-ui/react next-themes sonner geist ``` > **Todavía no está en npm.** Hasta que se publique, el paquete se distribuye como tarball: `npm pack` en el repo y `pnpm add ./sebs7n-ui-0.1.0.tgz`, o `pnpm add github:sebafermanelli/sebs7n-ui#v0.1.0`. Todo lo de abajo vale igual en los dos casos. Las `peerDependencies` las instala la app, para que haya **una sola copia** de React y de Base UI: | Peer | Rango | |---|---| | `react` · `react-dom` | `^19.2.0` | | `@base-ui/react` | `^1.8.0` | | `next-themes` | `^0.4.6` | | `sonner` | `^2.0.7` | ## Compatibilidad | | Versión | |---|---| | React | 19.2+ (Server Components y `"use client"`) | | Next.js | 15 o 16, App Router (Turbopack o webpack) | | Tailwind CSS | **v4** — tokens por `@theme`, sin `tailwind.config.js` | | Base UI | `@base-ui/react` 1.8+ | | TypeScript | `moduleResolution: "bundler"` (o `node16` / `nodenext`) | | Módulos | Solo ESM | Los componentes no dependen de Next: funcionan en cualquier bundler que entienda `exports` y la directiva `"use client"`. Lo único específico de Next en los ejemplos es `next/link` y `next/navigation`. ## 1. CSS En `globals.css`, **en este orden**: ```css @import "tailwindcss"; @import "sebs7n-ui/theme.css"; /* Las clases de los componentes las genera el Tailwind de la app, en una sola hoja ordenada. La ruta es relativa a ESTE archivo: app/globals.css → "../node_modules/…"; src/app/globals.css → "../../node_modules/…". */ @source "../node_modules/sebs7n-ui/dist"; :root { --brand-base: oklch(0.573 0.214 258); --brand-base-dark: oklch(0.573 0.214 258); --brand-contrast-dark: #fff; } ``` > Existe también `@import "sebs7n-ui/styles.css"` (la hoja precompilada), pero con dos hojas de utilidades un `hidden lg:block` de la app pierde contra el `hidden` del paquete. **No combines `@source` con `@import "sebs7n-ui/styles.css"`**: es una cosa o la otra, y la recomendada es `@source`. ## 2. Layout raíz `GeistSans.variable` y `GeistMono.variable` en ``, el `ThemeProvider` de `next-themes` con `attribute="class"`, y `TooltipProvider` + `` de `sebs7n-ui`. ```tsx import { GeistMono } from "geist/font/mono" import { GeistSans } from "geist/font/sans" import { Toaster, TooltipProvider } from "sebs7n-ui" import { ThemeProvider } from "next-themes" import "./globals.css" export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` `suppressHydrationWarning` en `` no es opcional: `next-themes` escribe la clase `dark` antes de hidratar y sin eso React avisa en cada carga. Geist tiene que ser la **fuente variable** (rango `100 900`): la corrección óptica de los títulos usa pesos intermedios (450, 500, 550) que con una estática se redondean. ## 3. Usar ```tsx import { Button } from "sebs7n-ui/button" import { Card, CardContent, CardHeader, CardTitle } from "sebs7n-ui/card" export function Panel() { return ( Facturas ) } ``` ## Imports por componente Cada módulo tiene su entry point. **En páginas de marketing o landing, importá por subpath**; el barrel queda para dashboards, que igual usan casi todo. ```tsx import { Button } from "sebs7n-ui/button" import { Card, CardContent } from "sebs7n-ui/card" import { ThemeSwitcher } from "sebs7n-ui/theme-switcher" import { buttonVariants } from "sebs7n-ui/variants/button" // sin "use client" import { cn } from "sebs7n-ui/lib/utils" ``` | Subpath | Archivo | |---|---| | `sebs7n-ui/` | `src/components/.tsx` (kebab-case: `alert-dialog`, `app-shell`, `user-menu`, …) | | `sebs7n-ui/variants/` | `button`, `badge`, `card`, `link`, `menu`, `sidebar`, `input`, `toggle` | | `sebs7n-ui/lib/` | `utils` | Por qué: el barrel hace `export *` de ~30 módulos `"use client"`. Next no puede podar referencias cliente a través de ese barrel (tampoco con `optimizePackageImports`), así que una página con `Button` + `Card` + `ThemeSwitcher` se lleva también Sonner, Sidebar, Select, AlertDialog y el resto. Medido en Next 16.3 (Turbopack) con esa página: **297,5 KB → 234,8 KB** de JS cliente gzip (−21 %). No mezcles barrel y subpaths en la misma página: el barrel vuelve a traer todo. ## Verificar que quedó bien 1. `bg-blue-500` **no** tiene que compilar: la paleta de Tailwind está reseteada y solo existen los tokens del paquete. 2. En oscuro, un `Input` tiene que verse **más claro** que el fondo de la página (`#0a0a0a` sobre `#000`). Si son el mismo negro, falta la 2.0 o hay un parche viejo pisando `--sf-background-100`. 3. Tabulá: la primera parada de un `AppShell` es «Ir al contenido». --- Fuente: https://ui.sebastianfermanelli.com/docs/tokens # Tokens > Color, tipografía, radios y sombras. Los valores salen de `tokens/geist.json` y de `theme.css`. La paleta, los radios y las sombras por defecto de Tailwind están **reseteados**: `bg-blue-50` o `shadow-md` no compilan. Solo existen los tokens del paquete. Los valores viven en `tokens/geist.json` y se compilan a `src/styles/colors.css` con `npm run tokens`; un test falla si el CSS quedó desactualizado. Las tablas de esta página se generan de esos mismos archivos. ## Fondos: página, superficie y banda Son tres roles distintos y cada uno tiene su token. Elegir mal se nota sobre todo en oscuro, donde la página es negro puro: | Token | Rol | Claro | Oscuro | |---|---|---|---| | `bg-background` | La página. `body` (ya lo pone el paquete) y la raíz del `AppShell`. | #ffffff | #000000 | | `bg-background-100` | La superficie: lo que flota sobre la página. Input, Select, Textarea, popup de menú, Popover, Dialog, Sheet, Card, Alert, Toast, barra mobile del shell. | #ffffff | #0a0a0a | | `bg-background-200` | El fondo sutil / banda: Sidebar, `thead`/`tfoot` de Table, `Card variant="subtle"`, `EmptyState`. | #fafafa | #0a0a0a | **La regla:** si el elemento **es** la página, `bg-background`; si flota **sobre** ella, `bg-background-100`. Los valores de oscuro son los de vercel.com medidos con `getComputedStyle` (contact/sales, 2026-09-22): `--ds-background-100: hsla(0,0%,4%)` = `#0a0a0a` para las superficies y `--ds-background-200: hsla(0,0%,0%)` = `#000` para la página. `background-200` es siempre el tono que **no** es el de la página: en claro baja a `#fafafa`, en oscuro no puede bajar de `#000` y sube a `#0a0a0a`. Por eso en oscuro coincide con `background-100`: Geist tiene dos fondos por tema, no tres. ## Color Diez pasos por familia, de `100` (el más claro en tema claro) a `1000`. La convención de Geist: **100–400 son fondos, 500–700 son bordes y elementos, 800–1000 son texto**. El contraste de `gray-900` sobre cualquier fondo de la misma familia llega a AA. ### gray | Paso | Claro | Oscuro | |---|---|---| | `gray-100` | #f2f2f2 | #1a1a1a | | `gray-200` | #ebebeb | #1f1f1f | | `gray-300` | #e6e6e6 | #292929 | | `gray-400` | #eaeaea | #2e2e2e | | `gray-500` | #c9c9c9 | #454545 | | `gray-600` | #a8a8a8 | #878787 | | `gray-700` | #8f8f8f | #8f8f8f | | `gray-800` | #7d7d7d | #7d7d7d | | `gray-900` | #4d4d4d | #a0a0a0 | | `gray-1000` | #171717 | #ededed | ### gray-alpha | Paso | Claro | Oscuro | |---|---|---| | `gray-alpha-100` | #0000000d | #ffffff12 | | `gray-alpha-200` | #00000015 | #ffffff17 | | `gray-alpha-300` | #0000001a | #ffffff21 | | `gray-alpha-400` | #00000014 | #ffffff24 | | `gray-alpha-500` | #00000036 | #ffffff3d | | `gray-alpha-600` | #0000003d | #ffffff82 | | `gray-alpha-700` | #00000070 | #ffffff8a | | `gray-alpha-800` | #00000082 | #ffffff78 | | `gray-alpha-900` | #000000b3 | #ffffff9c | | `gray-alpha-1000` | #000000e8 | #ffffffeb | ### blue | Paso | Claro | Oscuro | |---|---|---| | `blue-100` | #f0f7ff | #06193a | | `blue-200` | #eaf4ff | #022248 | | `blue-300` | #e0efff | #002f62 | | `blue-400` | #cce7ff | #003771 | | `blue-500` | #97ccff | #004287 | | `blue-600` | #51aeff | #0090ff | | `blue-700` | #0070f7 | #0071f6 | | `blue-800` | #005edc | #005fd8 | | `blue-900` | #0064e2 | #50a8ff | | `blue-1000` | #002453 | #ebf6ff | ### red | Paso | Claro | Oscuro | |---|---|---| | `red-100` | #ffeef0 | #330a11 | | `red-200` | #ffe9ea | #440d13 | | `red-300` | #ffe4e5 | #5d0e17 | | `red-400` | #ffd8d7 | #6f101b | | `red-500` | #ffb5b6 | #88151f | | `red-600` | #ff6a6e | #f32e40 | | `red-700` | #fc0035 | #f13242 | | `red-800` | #e70022 | #e2162a | | `red-900` | #d60020 | #ff5e63 | | `red-1000` | #46000c | #ffeaed | ### amber | Paso | Claro | Oscuro | |---|---|---| | `amber-100` | #fff6e1 | #291800 | | `amber-200` | #fff4d4 | #331b00 | | `amber-300` | #fff1c8 | #4f2900 | | `amber-400` | #ffdd84 | #573200 | | `amber-500` | #ffc85e | #6c4100 | | `amber-600` | #ffaa00 | #e99c00 | | `amber-700` | #ffb200 | #ffb200 | | `amber-800` | #ff9900 | #ff9900 | | `amber-900` | #a64f00 | #ff9900 | | `amber-1000` | #541c00 | #fff3d9 | ### green | Paso | Claro | Oscuro | |---|---|---| | `green-100` | #ecfdec | #00250a | | `green-200` | #e5fce7 | #003110 | | `green-300` | #d3fad1 | #003814 | | `green-400` | #b9f5bc | #004616 | | `green-500` | #82eb8d | #00661d | | `green-600` | #4ce15e | #009431 | | `green-700` | #28a948 | #00ab3e | | `green-800` | #279141 | #009335 | | `green-900` | #107d32 | #00ca52 | | `green-1000` | #00370d | #daffe5 | ### teal | Paso | Claro | Oscuro | |---|---|---| | `teal-100` | #dffffb | #00211b | | `teal-200` | #ddfef6 | #002922 | | `teal-300` | #ccf9f1 | #003b33 | | `teal-400` | #b1f7ec | #003f35 | | `teal-500` | #52f0db | #005f53 | | `teal-600` | #00e2c4 | #009885 | | `teal-700` | #00a694 | #00a794 | | `teal-800` | #008d7d | #008d7d | | `teal-900` | #007a6e | #00c9b5 | | `teal-1000` | #003d34 | #cefff5 | ### purple | Paso | Claro | Oscuro | |---|---|---| | `purple-100` | #f9f0ff | #290c33 | | `purple-200` | #f9f1ff | #341142 | | `purple-300` | #f5e8ff | #47185e | | `purple-400` | #f1d9ff | #541a76 | | `purple-500` | #dda9ff | #642290 | | `purple-600` | #c77dff | #9440d5 | | `purple-700` | #9f00f4 | #9440d5 | | `purple-800` | #8400cd | #7d2bba | | `purple-900` | #7c00c9 | #c472fb | | `purple-1000` | #2e004d | #faedff | ### pink | Paso | Claro | Oscuro | |---|---|---| | `pink-100` | #ffeaf5 | #310d1e | | `pink-200` | #ffeaf2 | #420c25 | | `pink-300` | #ffe0eb | #571032 | | `pink-400` | #ffd5e1 | #5d0c34 | | `pink-500` | #fdb3cc | #76063f | | `pink-600` | #f97ea7 | #b90056 | | `pink-700` | #f22782 | #f12b82 | | `pink-800` | #e4106e | #e6006e | | `pink-900` | #c41562 | #ff518d | | `pink-1000` | #460523 | #ffeaf4 | `brand` no está en la tabla porque no tiene valores fijos: se deriva de las tres variables de la app. Ver [Theming](/docs/theming). ## Tipografía Las utilidades de Geist. `cn()` las entiende como tamaño de fuente, así que conviven con `text-gray-900`. | Utilidad | Tamaño | Interlineado | Peso | Tracking | |---|---|---|---|---| | `text-heading-72` | 72px | 72px | 400 | -4.32px | | `text-heading-64` | 64px | 64px | 400 | -3.84px | | `text-heading-56` | 56px | 56px | 450 | -3.36px | | `text-heading-48` | 48px | 56px | 450 | -2.88px | | `text-heading-40` | 40px | 48px | 450 | -2.4px | | `text-heading-32` | 32px | 40px | 500 | -1.28px | | `text-heading-24` | 24px | 32px | 500 | -0.96px | | `text-heading-20` | 20px | 26px | 550 | -0.4px | | `text-heading-16` | 16px | 24px | 600 | -0.32px | | `text-heading-14` | 14px | 20px | 600 | -0.28px | | `text-button-16` | 16px | 20px | 500 | 0 | | `text-button-14` | 14px | 20px | 500 | 0 | | `text-button-12` | 12px | 16px | 500 | 0 | | `text-label-20` | 20px | 32px | 400 | 0 | | `text-label-18` | 18px | 20px | 400 | 0 | | `text-label-16` | 16px | 20px | 400 | 0 | | `text-label-14` | 14px | 20px | 400 | 0 | | `text-label-13` | 13px | 16px | 400 | 0 | | `text-label-12` | 12px | 16px | 400 | 0 | | `text-label-14-mono` | 14px | 20px | 400 | 0 | | `text-label-13-mono` | 13px | 20px | 400 | 0 | | `text-label-12-mono` | 12px | 16px | 400 | 0 | | `text-copy-24` | 24px | 36px | 400 | 0 | | `text-copy-20` | 20px | 36px | 400 | 0 | | `text-copy-18` | 18px | 28px | 400 | 0 | | `text-copy-16` | 16px | 24px | 400 | 0 | | `text-copy-14` | 14px | 20px | 400 | 0 | | `text-copy-13` | 13px | 18px | 400 | 0 | | `text-copy-14-mono` | 14px | 20px | 400 | 0 | | `text-copy-13-mono` | 13px | 18px | 400 | 0 | **Los `heading` llevan el peso corregido ópticamente**, no 600 fijo. Un mismo peso no se ve igual a 14px que a 64px: cuanto más grande el cuerpo, más gruesos se leen los trazos, y 600 a 64px sale plomizo. Los números salen de medir vercel.com (computed style, 2026-09-22); los pasos que no aparecían ahí se interpolan en la misma curva. **No lo pises con `font-semibold`**: para eso está el paso de arriba de la escala. Y necesita Geist como fuente variable (rango `100 900`); con una estática los pesos intermedios se redondean y la corrección se pierde. Cuándo usar cada familia: | Familia | Para qué | |---|---| | `heading-*` | Títulos. Peso óptico, tracking negativo. | | `copy-*` | Párrafos y texto corrido. Interlineado holgado. | | `label-*` | Etiquetas, celdas, metadatos. Interlineado ajustado, peso 400. | | `button-*` | Texto dentro de controles. Peso 500. | | `*-mono` | Código, IDs, importes. Geist Mono. | ## Radios | Utilidad | Valor | |---|---| | `rounded-xs` | 4px | | `rounded-sm` | 6px | | `rounded-md` | 6px | | `rounded-lg` | 12px | | `rounded-xl` | 12px | | `rounded-2xl` | 16px | | `rounded-3xl` | 16px | | `rounded-4xl` | 9999px | `rounded-md` (6px) es el radio del sistema: botones, inputs, ítems de menú. `rounded-xl` (12px) es el de las tarjetas. `rounded-full` solo en `Badge`, en avatares y en `Button shape="pill"`. ## Sombras | Utilidad | Dónde | |---|---| | `shadow-tooltip` | Tooltip. | | `shadow-menu` | DropdownMenu, Select, Combobox, Popover. | | `shadow-modal` | Dialog, AlertDialog, Sheet. | Las tres terminan en un anillo de 1px que en oscuro reemplaza al borde. No hay sombras decorativas: una sombra en este sistema significa «esto flota», y solo flotan los tres tipos de superficie de arriba. ## Foco y movimiento | Utilidad | Dónde | |---|---| | `focus-visible:focus-ring` | Controles: botones, ítems de menú, tabs, toggles. Anillo de 2px en `brand-700` con hueco. | | `focus:focus-border` | Campos: Input, Textarea, Select, Combobox. Tiñe el borde y agrega un halo. | | `focus:focus-border-error` | Lo mismo, en rojo, cuando el campo tiene `aria-invalid`. | | `transition-control` | 150 ms, `ease`, solo color / fondo / borde / sombra / opacidad. | | `animate-skeleton` | El latido del `Skeleton`. | Todas las animaciones pasan por `motion-reduce`, además del reset global del paquete. --- Fuente: https://ui.sebastianfermanelli.com/docs/theming # Theming > Tres variables de marca, claro y oscuro, radio y densidad. Una app define **tres variables** y nada más. No hay que tocar ningún archivo del paquete ni recompilar nada. ## Color de marca ```css :root { --brand-base: oklch(0.55 0.16 35); /* acento en claro */ --brand-base-dark: oklch(0.55 0.16 35); /* acento en oscuro; por defecto, igual a la base */ --brand-contrast-dark: #fff; /* texto sobre el acento en oscuro */ } ``` De ahí el paquete deriva la escala `brand-100…1000` y `brand-contrast`, con `oklch(from …)`: se fija la luminosidad del paso equivalente de `blue` en Geist y se escala el croma. Por eso un acento naranja y uno azul dan escalas que «pesan» igual. **La regla es una sola: el texto sobre `brand-700` tiene que llegar a 4,5:1.** Si el acento es claro, `--brand-contrast-dark: #000`. Hay un test en el paquete que recalcula el ratio desde OKLCH y falla si una marca no da. `tokens/brands.json` trae cuatro marcas de ejemplo (`teal`, `terracotta`, `emerald`, `blue`) que usan el playground y los tests de contraste. **Son solo demos del sistema**: una app real no las usa ni edita ese archivo. ### Dónde aparece el acento | Token | Dónde | |---|---| | `brand-700` | `Button variant="accent"`, `Switch variant="accent"`, anillo de foco, borde de `Card selected`. | | `brand-800` | Hover del acento. | | `brand-900` | `Button variant="link"`, texto de `Badge color="brand"`, `linkVariants`. | | `brand-100…400` | Fondos y bordes suaves: `Badge variant="subtle" color="brand"`. | | `brand-contrast` | El texto **encima** del acento. | **Un solo acento por pantalla.** Si el CTA principal es `accent`, el switch de al lado no. ## Claro y oscuro Por clase (`.dark` en ``), vía `next-themes` con `attribute="class"`: ```tsx ``` Dos controles para cambiarlo: - **`ThemeSwitcher`** — barra segmentada de tres estados, para fuera de un menú: header público, página de ajustes. - **`ThemeMenuRadio`** — los mismos tres estados como `menuitemradio` dentro de un `DropdownMenu` propio. `UserMenu` ya lo trae. Sin `enableSystem` ninguno de los dos muestra la opción «Sistema». El paquete define `color-scheme` en `html` y `html.dark`, así que los scrollbars, los ` }>Cancelar }>Guardar ) } ``` ### Controlado, con carga `open` en el estado de la app: el diálogo se cierra cuando termina la operación. ```tsx import { Button } from "sebs7n-ui/button" import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "sebs7n-ui/dialog" import { useState } from "react" function Controlado() { const [abierto, setAbierto] = useState(false) const [guardando, setGuardando] = useState(false) return ( }>Emitir factura Emitir la factura 0014 Se envía a AFIP y después no se puede editar. }>Cancelar ) } ``` ## Props ### Dialog Hereda las props de `Dialog.Root`. Sin props propias: pasa todo al primitivo. ### DialogClose Hereda las props de `Dialog.Close`. Sin props propias: pasa todo al primitivo. ### DialogContent Hereda las props de `Dialog.Popup`. | Prop | Tipo | Por defecto | Descripción | |---|---|---|---| | `showCloseButton` | `boolean` | `true` | El botón X de la esquina. Si lo sacás, tiene que haber otra salida visible. | | `className` | `string` | — | — | ### DialogDescription Hereda las props de `Dialog.Description`. | Prop | Tipo | Por defecto | Descripción | |---|---|---|---| | `className` | `string` | — | — | ### DialogFooter Hereda las props de `
`. | Prop | Tipo | Por defecto | Descripción | |---|---|---|---| | `className` | `string` | — | — | ### DialogHeader Hereda las props de `
`. | Prop | Tipo | Por defecto | Descripción | |---|---|---|---| | `className` | `string` | — | — | ### DialogOverlay Hereda las props de `Dialog.Backdrop`. | Prop | Tipo | Por defecto | Descripción | |---|---|---|---| | `className` | `string` | — | — | ### DialogTitle Hereda las props de `Dialog.Title`. | Prop | Tipo | Por defecto | Descripción | |---|---|---|---| | `className` | `string` | — | — | ### DialogTrigger Hereda las props de `Dialog.Trigger`. Sin props propias: pasa todo al primitivo. ## Teclado | Tecla | Qué hace | |---|---| | Enter · Espacio | Abre desde el trigger. | | Tab · ⇧Tab | Recorre solo el contenido del diálogo: el foco queda atrapado adentro. | | Escape | Cierra y devuelve el foco al trigger. | | Click en el fondo | Cierra. | ## Accesibilidad - `DialogTitle` no es opcional: es el nombre accesible del diálogo. Si no querés mostrarlo, ocultalo visualmente pero dejalo en el DOM. - `DialogDescription` se asocia con `aria-describedby` y se lee después del título. - El foco entra al abrir y vuelve al trigger al cerrar, y el fondo queda inerte. - El botón X trae su propio nombre accesible; `showCloseButton={false}` obliga a dejar otra salida visible. ## Reglas de uso - **Los triggers usan `render={} description="Cuando emitas la primera, va a aparecer acá con su estado y su vencimiento." icon={} title="Todavía no emitiste ninguna factura" /> Limpiar filtros} description="Probá con otro rango de fechas o sacá el filtro de cliente." icon={} title="Ninguna factura coincide con el filtro" />
) } ``` ## Props ### EmptyState Hereda las props de `
`. | Prop | Tipo | Por defecto | Descripción | |---|---|---|---| | `title` * | `React.ReactNode` | — | — | | `action` | `React.ReactNode` | — | Botón o link para salir del vacío. Uno solo. | | `description` | `React.ReactNode` | — | — | | `icon` | `React.ReactNode` | — | — | | `titleAs` | `"h2" \| "h3" \| "h4" \| "h5" \| "h6"` | `"h2"` | Nivel del título. h2 por defecto (debajo del h1 del PageHeader). | | `className` | `string` | — | — | `*` obligatoria. ## Teclado | Tecla | Qué hace | |---|---| | Tab | Llega a la acción. | ## Accesibilidad - `titleAs` controla el nivel del heading: dentro de una página con `

`, que sea `h2`. - Es una zona hundida (`bg-background-200`), sin borde ni sombra. - Sin `"use client"`: sirve en un Server Component. ## Reglas de uso - **Distinguí los tres vacíos**: todavía no hay nada (con acción), el filtro no encontró nada (con «limpiar filtros») y hubo un error (con «reintentar»). No son el mismo texto. - El título dice qué falta, no «Sin resultados». - Una sola acción. ## Relacionados [alert](/docs/components/alert.md) · [card](/docs/components/card.md) · [table](/docs/components/table.md) --- Fuente: https://ui.sebastianfermanelli.com/docs/components/input # Input > El campo de texto. Tres alturas y el mismo cuerpo que Select, Combobox y Textarea. ```tsx import { Input } from "sebs7n-ui/input" ``` ## Ejemplos ### Con etiqueta ```tsx import { Input } from "sebs7n-ui/input" import { Label } from "sebs7n-ui/label" import { useId } from "react" function ConEtiqueta() { const id = useId() return (
) } ``` ### Tamaños ```tsx import { Input } from "sebs7n-ui/input" function Tamanos() { return (
) } ``` ### Error `aria-invalid` es todo: el borde rojo y el anillo salen de ahí. ```tsx import { Input } from "sebs7n-ui/input" import { Label } from "sebs7n-ui/label" import { useId, useState } from "react" function Error() { const id = useId() const [valor, setValor] = useState("acme@") const invalido = !/^[^@\s]+@[^@\s.]+\.[^@\s]+$/.test(valor) return (
setValor(event.target.value)} type="email" value={valor} /> {invalido && (

Escribí un email válido, con dominio.

)}
) } ``` ### Deshabilitado y solo lectura ```tsx import { Input } from "sebs7n-ui/input" function Estados() { return (
) } ``` ## Props ### Input Hereda las props de `Input`. | Prop | Tipo | Por defecto | Descripción | |---|---|---|---| | `size` | `"sm" \| "md" \| "lg"` | `"md"` | `sm` 32px · `md` 40px · `lg` 48px, con el texto un paso más grande. | | `className` | `string` | — | — | ## Teclado | Tecla | Qué hace | |---|---| | Tab | Entra y sale del campo. | | Escape | No hace nada por defecto: si limpia el campo, lo implementa la app. | ## Accesibilidad - **`aria-invalid` es todo lo que hace falta para el error**: el borde rojo y el anillo salen de ahí, no de una clase aparte. - Siempre con `Label` asociado por `htmlFor`/`id`. Un `placeholder` no es una etiqueta: desaparece al escribir. - El mensaje de error va en un `

` referenciado con `aria-describedby`, para que se lea al enfocar. - `focus:focus-border` en vez del anillo: en un campo el borde teñido molesta menos y se ve igual. ## Reglas de uso - **El tamaño se elige una vez por formulario**, no por campo. `md` (40px) es el de una app; `lg` para un formulario de una sola pregunta. - `type` importa más que el estilo: `email`, `tel`, `url` y `numeric` cambian el teclado del celular. - Un campo obligatorio se marca en el `Label` (`required`), no con un asterisco pegado al placeholder. - Para un buscador con sugerencias no uses `Input` a mano: `Combobox` o `Autocomplete`. ## Relacionados [label](/docs/components/label.md) · [textarea](/docs/components/textarea.md) · [select](/docs/components/select.md) · [combobox](/docs/components/combobox.md) --- Fuente: https://ui.sebastianfermanelli.com/docs/components/kbd # Kbd > Una tecla o un atajo en línea, en Geist Mono. ```tsx import { Kbd } from "sebs7n-ui/kbd" ``` Sin `"use client"`: sirve en un Server Component. ## Ejemplos ### Atajos en línea ```tsx import { Kbd } from "sebs7n-ui/kbd" function Basico() { return (

Abrí la paleta con K y cerrá cualquier panel con Esc.

) } ``` ## Props ### Kbd Hereda las props de ``. | Prop | Tipo | Por defecto | Descripción | |---|---|---|---| | `className` | `string` | — | — | ## Teclado | Tecla | Qué hace | |---|---| | — | Es texto. | ## Accesibilidad - Emite ``, que es lo que corresponde. No registra ningún atajo: solo lo muestra. ## Reglas de uso - Para anunciar el atajo, `aria-keyshortcuts` va en el control que lo dispara, no en el `Kbd`. - Símbolos de Mac (`⌘`, `⌥`, `⇧`) o nombres (`Ctrl`, `Alt`): elegí uno y mantenelo en toda la app. ## Relacionados [sidebar](/docs/components/sidebar.md) · [dropdown-menu](/docs/components/dropdown-menu.md) --- Fuente: https://ui.sebastianfermanelli.com/docs/components/label # Label > La etiqueta de un campo, con la marca de obligatorio incluida. ```tsx import { Label } from "sebs7n-ui/label" ``` Sin `"use client"`: sirve en un Server Component. ## Ejemplos ### Opcional y obligatorio ```tsx import { Input } from "sebs7n-ui/input" import { Label } from "sebs7n-ui/label" import { useId } from "react" function Basico() { const cuit = useId() const alias = useId() return (
) } ``` ## Props ### Label Hereda las props de `