npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@darksnow-ui/menus

v0.1.4

Published

Accessible and customizable menu components for React built on Radix UI

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.

npm version TypeScript License: MIT

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/menus

Dependê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 item
    • Arrow Up/Down: Navegar entre items
    • Arrow Right: Abrir submenu
    • Arrow Left: Fechar submenu
    • Escape: Fechar menu
    • Tab: 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 children do 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 content do 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 build

Licença

MIT © Darksnow UI


Feito com ❤️ usando Radix UI e Tailwind CSS