Menu
Un menú desplegable en elevation 2 cuyo encabezado interno y elementos comparten la curva del panel.
Instalación
$ pnpm dlx shadcn@latest add @ssntl/menuUso
import { Menu, MenuContent, MenuHeader, MenuItem, MenuSeparator, MenuTrigger } from "@/components/ui/menu"
<Menu>
<MenuTrigger asChild><Button variant="secondary">Acciones del producto</Button></MenuTrigger>
<MenuContent>
<MenuHeader>Taza de cerámica</MenuHeader>
<MenuItem><Pencil /> Editar</MenuItem>
<MenuSeparator />
<MenuItem destructive><Trash2 /> Eliminar</MenuItem>
</MenuContent>
</Menu>Reglas
Lo que este componente espera de ti, tal como se instala en components/ui/menu.md junto al código. El archivo completo agrega cuándo usarlo, teclado, los fundamentos de UI que aplica y cada regla compartida (en inglés).
## Reglas de Menu
- **Menú + encabezado interno.** El panel es `radius-md` (22) con 8px de padding, así el encabezado y los elementos toman 14 (`radius-sm`) y se anidan en su curva.
- **Capa de menús.** Elevation-2, entrando con `pop-in` desde el lado del disparador.
- Radix maneja flechas, búsqueda por letra, Escape y el regreso del foco. Los elementos miden 44px.
- Los elementos destructivos van en texto rojo, al final, tras un separador; confírmalos en un Dialog antes de ejecutarlos.
- Muestra solo atajos que funcionen.Código
"use client"
import * as React from "react"
import { DropdownMenu as MenuPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
// A dropdown menu (Radix): keyboard, typeahead, focus return and collision
// handling come from the primitive. Elevation 2: it floats above the page.
// Nested corners: the panel is radius-md (22) with 8px of padding, so the
// inset header and every item take 22 − 8 = 14 (radius-sm) and share its
// curve.
const Menu = MenuPrimitive.Root
const MenuTrigger = MenuPrimitive.Trigger
const MenuGroup = MenuPrimitive.Group
function MenuContent({
className,
sideOffset = 8,
align = "start",
...props
}: React.ComponentProps<typeof MenuPrimitive.Content>) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Content
data-slot="menu-content"
sideOffset={sideOffset}
align={align}
className={cn(
"z-50 grid max-h-(--radix-dropdown-menu-content-available-height) min-w-56 origin-(--radix-dropdown-menu-content-transform-origin) gap-0.5 overflow-y-auto overscroll-contain rounded-outer-md bg-popover p-concentric-2 text-popover-foreground elevation-2",
"data-[state=closed]:pop-out data-[state=open]:pop-in",
className
)}
{...props}
/>
</MenuPrimitive.Portal>
)
}
/** A muted card at the top: who or what the menu acts on. */
function MenuHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="menu-header"
className={cn(
"mb-1 grid gap-0.5 rounded-inner bg-muted px-3 py-2.5",
className
)}
{...props}
/>
)
}
function MenuItem({
className,
destructive = false,
...props
}: React.ComponentProps<typeof MenuPrimitive.Item> & {
/** Red text for Delete or Remove; confirm before it runs. */
destructive?: boolean
}) {
return (
<MenuPrimitive.Item
data-slot="menu-item"
className={cn(
"flex min-h-11 cursor-pointer items-center gap-3 rounded-inner px-3 text-label font-normal outline-none select-none",
"data-disabled:pointer-events-none data-disabled:opacity-40 data-highlighted:bg-secondary",
"[&_svg]:size-5 [&_svg]:shrink-0 [&_svg]:opacity-80",
destructive && "text-destructive",
className
)}
{...props}
/>
)
}
function MenuSeparator({
className,
...props
}: React.ComponentProps<typeof MenuPrimitive.Separator>) {
return (
<MenuPrimitive.Separator
data-slot="menu-separator"
className={cn("mx-3 my-1 h-px bg-border", className)}
{...props}
/>
)
}
/** A keyboard shortcut at the end of an item. */
function MenuShortcut({ className, ...props }: React.ComponentProps<"kbd">) {
return (
<kbd
data-slot="menu-shortcut"
className={cn(
"ms-auto font-sans text-caption text-muted-foreground",
className
)}
{...props}
/>
)
}
export {
Menu,
MenuContent,
MenuGroup,
MenuHeader,
MenuItem,
MenuSeparator,
MenuShortcut,
MenuTrigger,
}