@darksnow-ui/menus
v0.1.4
Published
Accessible and customizable menu components for React built on Radix UI
Maintainers
Readme
@darksnow-ui/menus
Sistema de menus robusto e acessível para React, construído sobre Radix UI. Oferece três componentes distintos (DropdownMenu, ContextMenu, FloatingMenu) com lógica compartilhada e configuração simples via JSON.
Por que usar?
- 🚀 Simplicidade: Configure menus complexos apenas com arrays de objetos
- ♿ Acessibilidade total: Construído sobre Radix UI com suporte completo a teclado e screen readers
- 🎨 Design consistente: Visual unificado em todos os tipos de menu
- 📦 Zero configuração: Funciona imediatamente, sem setup complexo
- 🔧 Totalmente tipado: TypeScript nativo com generics para contexto customizado
- ⚡ Performance: Renderização otimizada com React 18+
Instalação
# npm
npm install @darksnow-ui/menus
# yarn
yarn add @darksnow-ui/menus
# pnpm
pnpm add @darksnow-ui/menusDependências Peer
{
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}Início Rápido
import { DropdownMenu } from "@darksnow-ui/menus";
const menuItems = [
{ type: "item", label: "New File", icon: "📄", shortcut: "Ctrl+N" },
{ type: "item", label: "Open", icon: "📁", shortcut: "Ctrl+O" },
{ type: "separator" },
{ type: "item", label: "Save", icon: "💾", shortcut: "Ctrl+S" },
];
function App() {
return (
<DropdownMenu items={menuItems}>
<button>File Menu</button>
</DropdownMenu>
);
}Componentes
1. DropdownMenu
Menu ativado por clique, ideal para ações de botões e toolbars.
import { DropdownMenu } from "@darksnow-ui/menus";
<DropdownMenu
items={menuItems}
align="start" // start | center | end
sideOffset={5}
>
<button>Click me</button>
</DropdownMenu>2. ContextMenu
Menu de contexto ativado por clique direito.
import { ContextMenu } from "@darksnow-ui/menus";
<ContextMenu items={menuItems}>
<div className="p-20 border-2 border-dashed">
Right click anywhere here
</div>
</ContextMenu>3. FloatingMenu
Menu com posicionamento manual, útil para casos customizados.
import { FloatingMenu } from "@darksnow-ui/menus";
<FloatingMenu
items={menuItems}
open={isOpen}
onOpenChange={setIsOpen}
x={mousePosition.x}
y={mousePosition.y}
/>Tipos de Items
1. Item (Ação)
{
type: "item",
label: "Save File",
icon: "💾", // String, emoji ou React component
shortcut: "Ctrl+S", // Opcional
disabled: false, // Opcional
className: "text-blue-500", // Opcional - classes customizadas
onClick: (context) => {
console.log("Saving...");
}
}2. Checkbox
{
type: "checkbox",
label: "Word Wrap",
checked: wordWrapEnabled,
icon: "📝", // Opcional
disabled: false, // Opcional
onClick: (context) => {
setWordWrapEnabled(!wordWrapEnabled);
}
}3. Radio
// Grupo de opções mutuamente exclusivas
[
{ type: "radio", label: "Small", checked: size === "sm", onClick: () => setSize("sm") },
{ type: "radio", label: "Medium", checked: size === "md", onClick: () => setSize("md") },
{ type: "radio", label: "Large", checked: size === "lg", onClick: () => setSize("lg") }
]4. Submenu
{
type: "submenu",
label: "Recent Files",
icon: "📁",
disabled: false, // Opcional
items: [
{ type: "item", label: "project.tsx", onClick: () => openFile("project.tsx") },
{ type: "item", label: "README.md", onClick: () => openFile("README.md") },
{ type: "separator" },
{ type: "item", label: "Clear Recent", className: "text-red-500" }
]
}5. Separator
{ type: "separator" }6. Label (Seção)
{
type: "label",
label: "Text Options",
inset: true // Adiciona padding à esquerda
}Context (Dados Customizados)
Passe dados contextuais que serão disponibilizados em todos os handlers:
interface FileContext {
fileId: string;
fileName: string;
permissions: string[];
}
const menuItems: MenuItemConfig<FileContext>[] = [
{
type: "item",
label: "Delete",
icon: "🗑️",
// Desabilita baseado no contexto
disabled: (ctx) => !ctx.permissions.includes("delete"),
onClick: (context) => {
deleteFile(context.fileId);
}
}
];
<ContextMenu<FileContext>
items={menuItems}
context={{
fileId: "123",
fileName: "document.txt",
permissions: ["read", "write", "delete"]
}}
>
<FileItem />
</ContextMenu>Exemplos Completos
Menu de Editor de Código
const editorMenu: MenuItemConfig[] = [
// Seção de navegação
{ type: "label", label: "Navigation" },
{ type: "item", label: "Go to Definition", shortcut: "F12", icon: "🔍" },
{ type: "item", label: "Find References", shortcut: "Shift+F12", icon: "📑" },
{ type: "separator" },
// Seção de edição
{ type: "label", label: "Edit" },
{ type: "item", label: "Rename Symbol", shortcut: "F2", icon: "✏️" },
{
type: "submenu",
label: "Refactor",
icon: "🔧",
items: [
{ type: "item", label: "Extract Method", shortcut: "Ctrl+Alt+M" },
{ type: "item", label: "Extract Variable", shortcut: "Ctrl+Alt+V" },
{ type: "item", label: "Inline", shortcut: "Ctrl+Alt+N" }
]
},
{ type: "separator" },
// Opções de visualização
{ type: "checkbox", label: "Show Minimap", checked: true },
{ type: "checkbox", label: "Word Wrap", checked: false },
{ type: "separator" },
// Tema
{ type: "label", label: "Theme" },
{ type: "radio", label: "Light", checked: false },
{ type: "radio", label: "Dark", checked: true },
{ type: "radio", label: "High Contrast", checked: false }
];Menu Dinâmico de Arquivos
function FileExplorer({ files }) {
const getFileMenu = (file: File): MenuItemConfig[] => {
const items: MenuItemConfig[] = [
{
type: "item",
label: "Open",
icon: "📄",
onClick: () => openFile(file.id)
},
{
type: "item",
label: "Open in New Tab",
icon: "🗂️",
onClick: () => openInNewTab(file.id)
}
];
// Adiciona opções específicas para pastas
if (file.type === "folder") {
items.push(
{ type: "separator" },
{
type: "submenu",
label: "New",
icon: "➕",
items: [
{ type: "item", label: "File", icon: "📄", onClick: () => createFile(file.id) },
{ type: "item", label: "Folder", icon: "📁", onClick: () => createFolder(file.id) }
]
}
);
}
// Adiciona opções perigosas no final
items.push(
{ type: "separator" },
{
type: "item",
label: "Delete",
icon: "🗑️",
shortcut: "Delete",
className: "text-red-500 hover:bg-red-50",
disabled: file.protected,
onClick: () => confirmAndDelete(file.id)
}
);
return items;
};
return (
<div>
{files.map(file => (
<ContextMenu
key={file.id}
items={getFileMenu(file)}
context={{ file }}
>
<FileRow file={file} />
</ContextMenu>
))}
</div>
);
}Menu de Configurações
const settingsMenu: MenuItemConfig[] = [
{ type: "label", label: "Appearance", inset: true },
{
type: "submenu",
label: "Font Size",
items: [
{ type: "radio", label: "Small (12px)", checked: false },
{ type: "radio", label: "Medium (14px)", checked: true },
{ type: "radio", label: "Large (16px)", checked: false },
{ type: "radio", label: "Extra Large (18px)", checked: false }
]
},
{ type: "checkbox", label: "Show Line Numbers", checked: true },
{ type: "checkbox", label: "Show Indent Guides", checked: true },
{ type: "separator" },
{ type: "label", label: "Editor", inset: true },
{ type: "checkbox", label: "Auto Save", checked: false },
{ type: "checkbox", label: "Format on Save", checked: true },
{ type: "checkbox", label: "Bracket Pair Colorization", checked: true },
{ type: "separator" },
{ type: "label", label: "Advanced", inset: true },
{ type: "item", label: "Configure Shortcuts...", icon: "⌨️" },
{ type: "item", label: "Export Settings", icon: "📤" },
{ type: "item", label: "Reset to Defaults", icon: "🔄", className: "text-orange-500" }
];Estilização
Classes CSS Customizadas
// Item individual
{
type: "item",
label: "Delete",
className: "text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
}
// Menu completo
<DropdownMenu
items={items}
className="bg-slate-900 border-slate-700 min-w-[300px]"
>
<button>Dark Theme Menu</button>
</DropdownMenu>Integração com Tailwind CSS
O componente usa classes do Tailwind por padrão. Para customizar cores e estilos:
/* Sobrescrever variáveis CSS */
:root {
--menu-bg: theme('colors.white');
--menu-border: theme('colors.gray.200');
--menu-text: theme('colors.gray.900');
--menu-hover: theme('colors.gray.100');
}
.dark {
--menu-bg: theme('colors.gray.900');
--menu-border: theme('colors.gray.700');
--menu-text: theme('colors.gray.100');
--menu-hover: theme('colors.gray.800');
}Acessibilidade
O componente segue todas as práticas de acessibilidade do Radix UI:
- ✅ Navegação completa por teclado
- ✅ Suporte para screen readers
- ✅ ARIA labels apropriados
- ✅ Gerenciamento de foco
- ✅ Teclas de atalho padrão:
Enter/Space: Selecionar itemArrow Up/Down: Navegar entre itemsArrow Right: Abrir submenuArrow Left: Fechar submenuEscape: Fechar menuTab: Mover para próximo item
API Reference
Props Comuns
| Prop | Tipo | Padrão | Descrição |
|------|------|--------|-----------|
| items | MenuItemConfig<T>[] | [] | Array de configuração dos items |
| context | T | {} | Dados contextuais para handlers |
| className | string | "" | Classes CSS adicionais |
| onOpenChange | (open: boolean) => void | - | Callback de mudança de estado |
Props do DropdownMenu
| Prop | Tipo | Padrão | Descrição |
|------|------|--------|-----------|
| children | ReactNode | - | Elemento trigger (obrigatório) |
| align | "start" \| "center" \| "end" | "start" | Alinhamento do menu |
| sideOffset | number | 5 | Distância do trigger em pixels |
Props do ContextMenu
| Prop | Tipo | Padrão | Descrição |
|------|------|--------|-----------|
| children | ReactNode | - | Área de clique direito (obrigatório) |
Props do FloatingMenu
| Prop | Tipo | Padrão | Descrição |
|------|------|--------|-----------|
| open | boolean | false | Estado de abertura |
| x | number | 0 | Posição horizontal |
| y | number | 0 | Posição vertical |
Troubleshooting
Menu não abre
- Verifique se o
childrendo DropdownMenu/ContextMenu é um elemento válido - Certifique-se de que não há
event.preventDefault()nos handlers
Submenu não funciona
- Submenus requerem hover ou navegação por teclado
- Em dispositivos touch, use FloatingMenu com lógica customizada
Estilos não aplicados
- Verifique se o Tailwind CSS está configurado no projeto
- Adicione as classes do componente ao
contentdo Tailwind
Contribuindo
Contribuições são bem-vindas! Por favor, abra uma issue primeiro para discutir mudanças maiores.
# Clone o repositório
git clone https://github.com/darksnow-ui/menus.git
# Instale as dependências
pnpm install
# Execute os testes
pnpm test
# Build
pnpm buildLicença
MIT © Darksnow UI
Feito com ❤️ usando Radix UI e Tailwind CSS
