Skip to main content
Glama
command.stories.tsx20.4 kB
import type { Meta, StoryObj } from '@storybook/react'; import { expect, userEvent, within } from '@storybook/test'; import { Calculator, Calendar, CreditCard, FileText, Home, Mail, MessageSquare, Plus, Search, Settings, Smile, User, } from 'lucide-react'; import { useState } from 'react'; import { Command, CommandRoot } from './index'; /** * Command Component Stories * * The Command component is a composable command palette built on top of cmdk and Radix UI. * It provides a searchable interface for executing commands and displaying structured data. * * ## Key Features * - **Searchable**: Built-in search functionality with fuzzy matching * - **Keyboard Navigation**: Full arrow key and enter support * - **Composable**: Multiple sub-components for flexible layouts * - **Dialog Mode**: Can be used as a modal dialog * - **Grouping**: Organize commands into logical groups * - **Accessibility**: ARIA compliant with screen reader support * - **Shortcuts**: Display keyboard shortcuts for commands * * ## Sub-components * - `Command` (Root): Container for the entire command palette * - `Command.Dialog`: Modal dialog wrapper * - `Command.Input`: Search input with icon * - `Command.List`: Scrollable list container * - `Command.Group`: Grouped section with optional heading * - `Command.Item`: Individual command items * - `Command.Empty`: Placeholder when no results found * - `Command.Separator`: Visual separator between groups * - `Command.Shortcut`: Keyboard shortcut display * * ## When to Use * - Application command palettes (Cmd+K interfaces) * - Search interfaces with structured data * - Quick action menus * - Autocomplete dropdowns * - Modal command selection interfaces */ const meta = { title: 'Components/Command', component: CommandRoot, parameters: { docs: { description: { component: ` A powerful command palette component for creating searchable interfaces with keyboard navigation. Built with accessibility in mind and supporting both standalone and modal dialog modes. ### Usage Examples - Global command palette (triggered by Cmd+K) - Search-driven navigation menus - Quick action selection - Autocomplete interfaces - File/document selection `, }, }, layout: 'centered', backgrounds: { values: [ { name: 'light', value: '#ffffff' }, { name: 'dark', value: '#1a1a1a' }, { name: 'neutral', value: '#f8f9fa' }, ], }, a11y: { config: { rules: [ { id: 'color-contrast', enabled: true, }, { id: 'keyboard-navigation', enabled: true, }, { id: 'focus-order-semantics', enabled: true, }, ], }, }, }, tags: ['autodocs'], argTypes: { className: { description: 'Additional CSS classes for styling customization', control: 'text', }, }, } satisfies Meta<typeof CommandRoot>; export default meta; type Story = StoryObj<typeof meta>; /** * ## Basic Examples * * These stories demonstrate the core functionality and various configurations * of the Command component. */ /** * ### Default Command Palette * * A basic command palette with search input and grouped commands. * Demonstrates the standard layout with icons, shortcuts, and organization. */ export const Default: Story = { render: () => ( <div className="w-96 rounded-lg border shadow-lg"> <CommandRoot> <Command.Input placeholder="Type a command or search..." /> <Command.List> <Command.Empty>No results found.</Command.Empty> <Command.Group heading="Suggestions"> <Command.Item> <Calendar className="mr-2 h-4 w-4" /> <span>Calendar</span> </Command.Item> <Command.Item> <Smile className="mr-2 h-4 w-4" /> <span>Search Emoji</span> </Command.Item> <Command.Item> <Calculator className="mr-2 h-4 w-4" /> <span>Calculator</span> </Command.Item> </Command.Group> <Command.Separator /> <Command.Group heading="Settings"> <Command.Item> <User className="mr-2 h-4 w-4" /> <span>Profile</span> <Command.Shortcut>⌘P</Command.Shortcut> </Command.Item> <Command.Item> <CreditCard className="mr-2 h-4 w-4" /> <span>Billing</span> <Command.Shortcut>⌘B</Command.Shortcut> </Command.Item> <Command.Item> <Settings className="mr-2 h-4 w-4" /> <span>Settings</span> <Command.Shortcut>⌘S</Command.Shortcut> </Command.Item> </Command.Group> </Command.List> </CommandRoot> </div> ), play: async ({ canvasElement }) => { const canvas = within(canvasElement); const searchInput = canvas.getByPlaceholderText( 'Type a command or search...' ); // Verify input is focusable and accessible await expect(searchInput).toBeInTheDocument(); await expect(searchInput).not.toBeDisabled(); // Test search functionality await userEvent.click(searchInput); await userEvent.type(searchInput, 'cal'); // Verify filtered results appear const calendarItem = canvas.getByText('Calendar'); await expect(calendarItem).toBeVisible(); // Test keyboard navigation await userEvent.keyboard('{ArrowDown}'); await userEvent.keyboard('{Enter}'); }, }; /** * ### Dialog Mode * * Command palette displayed as a modal dialog, commonly triggered by * keyboard shortcuts like Cmd+K in applications. */ export const DialogMode: Story = { render: () => { const [open, setOpen] = useState(false); return ( <div> <button onClick={() => setOpen(true)} className="rounded border bg-blue-500 px-4 py-2 text-white" > Trigger Command Palette (Dialog) </button> <Command.Dialog open={open} onOpenChange={setOpen}> <Command.Input placeholder="Type a command or search..." /> <Command.List> <Command.Empty>No results found.</Command.Empty> <Command.Group heading="Quick Actions"> <Command.Item> <FileText className="mr-2 h-4 w-4" /> <span>New Document</span> <Command.Shortcut>⌘N</Command.Shortcut> </Command.Item> <Command.Item> <Plus className="mr-2 h-4 w-4" /> <span>Create Project</span> <Command.Shortcut>⌘Shift+N</Command.Shortcut> </Command.Item> </Command.Group> <Command.Separator /> <Command.Group heading="Navigation"> <Command.Item> <Home className="mr-2 h-4 w-4" /> <span>Go to Dashboard</span> <Command.Shortcut>⌘D</Command.Shortcut> </Command.Item> <Command.Item> <Mail className="mr-2 h-4 w-4" /> <span>Open Inbox</span> <Command.Shortcut>⌘I</Command.Shortcut> </Command.Item> <Command.Item> <MessageSquare className="mr-2 h-4 w-4" /> <span>Messages</span> <Command.Shortcut>⌘M</Command.Shortcut> </Command.Item> </Command.Group> </Command.List> </Command.Dialog> </div> ); }, play: async ({ canvasElement }) => { // Open the dialog const canvas = within(canvasElement); const triggerButton = canvas.getByText('Trigger Command Palette (Dialog)'); await userEvent.click(triggerButton); await expect(triggerButton).toBeInTheDocument(); // Dialog should be open by default in this story const searchInput = canvas.getByPlaceholderText( 'Type a command or search...' ); await expect(searchInput).toBeInTheDocument(); // Test search within dialog await userEvent.type(searchInput, 'new'); const newDocItem = canvas.getByText('New Document'); await expect(newDocItem).toBeVisible(); }, }; /** * ### Empty State * * Demonstrates the command palette when no commands match the search query. * Shows the customizable empty state message. */ export const EmptyState: Story = { render: () => ( <div className="w-96 rounded-lg border shadow-lg"> <CommandRoot> <Command.Input placeholder="Search for something that doesn't exist..." /> <Command.List> <Command.Empty> <div className="py-6 text-center"> <Search className="mx-auto mb-2 h-8 w-8 text-gray-400" /> <p className="text-gray-500 text-sm">No commands found.</p> <p className="mt-1 text-gray-400 text-xs"> Try searching for calendar, settings, or profile. </p> </div> </Command.Empty> <Command.Group heading="Available Commands"> <Command.Item> <Calendar className="mr-2 h-4 w-4" /> <span>Calendar</span> </Command.Item> <Command.Item> <Settings className="mr-2 h-4 w-4" /> <span>Settings</span> </Command.Item> <Command.Item> <User className="mr-2 h-4 w-4" /> <span>Profile</span> </Command.Item> </Command.Group> </Command.List> </CommandRoot> </div> ), play: async ({ canvasElement }) => { const canvas = within(canvasElement); const searchInput = canvas.getByPlaceholderText(/Search for something/); // Search for something that won't match await userEvent.type(searchInput, 'nonexistent'); // Verify empty state is shown const emptyMessage = canvas.getByText('No commands found.'); await expect(emptyMessage).toBeVisible(); // Clear search to show available commands await userEvent.clear(searchInput); const calendarItem = canvas.getByText('Calendar'); await expect(calendarItem).toBeVisible(); }, }; /** * ### Simple List * * A minimal command palette without groups or separators, * showing just a simple list of commands. */ export const SimpleList: Story = { render: () => ( <div className="w-80 rounded-lg border shadow-lg"> <CommandRoot> <Command.Input placeholder="Search commands..." /> <Command.List> <Command.Empty>No results found.</Command.Empty> <Command.Item> <Calendar className="mr-2 h-4 w-4" /> <span>Open Calendar</span> </Command.Item> <Command.Item> <Mail className="mr-2 h-4 w-4" /> <span>Check Email</span> </Command.Item> <Command.Item> <FileText className="mr-2 h-4 w-4" /> <span>New Document</span> </Command.Item> <Command.Item> <Settings className="mr-2 h-4 w-4" /> <span>Open Settings</span> </Command.Item> <Command.Item> <User className="mr-2 h-4 w-4" /> <span>View Profile</span> </Command.Item> </Command.List> </CommandRoot> </div> ), }; /** * ### With Keyboard Shortcuts * * Showcases commands with keyboard shortcut indicators, * commonly used in application command palettes. */ export const WithShortcuts: Story = { render: () => ( <div className="w-96 rounded-lg border shadow-lg"> <CommandRoot> <Command.Input placeholder="Type a command..." /> <Command.List> <Command.Empty>No results found.</Command.Empty> <Command.Group heading="File"> <Command.Item> <FileText className="mr-2 h-4 w-4" /> <span>New File</span> <Command.Shortcut>⌘N</Command.Shortcut> </Command.Item> <Command.Item> <span>Open File</span> <Command.Shortcut>⌘O</Command.Shortcut> </Command.Item> <Command.Item> <span>Save File</span> <Command.Shortcut>⌘S</Command.Shortcut> </Command.Item> </Command.Group> <Command.Separator /> <Command.Group heading="Edit"> <Command.Item> <span>Copy</span> <Command.Shortcut>⌘C</Command.Shortcut> </Command.Item> <Command.Item> <span>Paste</span> <Command.Shortcut>⌘V</Command.Shortcut> </Command.Item> <Command.Item> <span>Select All</span> <Command.Shortcut>⌘A</Command.Shortcut> </Command.Item> </Command.Group> <Command.Separator /> <Command.Group heading="View"> <Command.Item> <span>Toggle Sidebar</span> <Command.Shortcut>⌘B</Command.Shortcut> </Command.Item> <Command.Item> <span>Zoom In</span> <Command.Shortcut>⌘+</Command.Shortcut> </Command.Item> <Command.Item> <span>Zoom Out</span> <Command.Shortcut>⌘-</Command.Shortcut> </Command.Item> </Command.Group> </Command.List> </CommandRoot> </div> ), play: async ({ canvasElement }) => { const canvas = within(canvasElement); // Verify shortcuts are displayed const newFileShortcut = canvas.getByText('⌘N'); await expect(newFileShortcut).toBeVisible(); const copyShortcut = canvas.getByText('⌘C'); await expect(copyShortcut).toBeVisible(); // Test search filtering const searchInput = canvas.getByPlaceholderText('Type a command...'); await userEvent.type(searchInput, 'file'); // Should show file-related commands const newFileItem = canvas.getByText('New File'); await expect(newFileItem).toBeVisible(); }, }; /** * ### Accessibility Testing * * Story focused on testing keyboard navigation, screen reader support, * and other accessibility features of the command palette. */ export const AccessibilityTest: Story = { render: () => ( <div className="w-96 rounded-lg border shadow-lg"> <CommandRoot> <Command.Input placeholder="Search commands..." aria-label="Command palette search" /> <Command.List role="listbox" aria-label="Command options"> <Command.Empty>No commands match your search.</Command.Empty> <Command.Group heading="Actions" role="group" aria-labelledby="actions-heading" > <Command.Item role="option" aria-describedby="calendar-desc"> <Calendar className="mr-2 h-4 w-4" /> <span>Open Calendar</span> <Command.Shortcut>⌘K C</Command.Shortcut> </Command.Item> <Command.Item role="option" aria-describedby="mail-desc"> <Mail className="mr-2 h-4 w-4" /> <span>Check Mail</span> <Command.Shortcut>⌘K M</Command.Shortcut> </Command.Item> <Command.Item role="option" aria-describedby="settings-desc"> <Settings className="mr-2 h-4 w-4" /> <span>Settings</span> <Command.Shortcut>⌘,</Command.Shortcut> </Command.Item> </Command.Group> </Command.List> </CommandRoot> </div> ), play: async ({ canvasElement }) => { const canvas = within(canvasElement); const searchInput = canvas.getByLabelText('Command palette search'); // Test focus management await userEvent.click(searchInput); await expect(searchInput).toHaveFocus(); // Test keyboard navigation await userEvent.keyboard('{ArrowDown}'); // The first command item should be focused (though we can't easily test focus state in play functions) await userEvent.keyboard('{ArrowDown}'); await userEvent.keyboard('{ArrowUp}'); // Test escape key await userEvent.keyboard('{Escape}'); // Test search and selection await userEvent.type(searchInput, 'calendar'); await userEvent.keyboard('{ArrowDown}'); await userEvent.keyboard('{Enter}'); }, }; /** * ### Interactive Search Demo * * Demonstrates real-time search filtering with a larger dataset * to showcase the search capabilities. */ export const InteractiveSearch: Story = { render: () => { const items = [ { icon: Calendar, name: 'Open Calendar', category: 'Apps', shortcut: '⌘K C', }, { icon: Mail, name: 'Check Email', category: 'Apps', shortcut: '⌘K E' }, { icon: FileText, name: 'New Document', category: 'File', shortcut: '⌘N', }, { icon: Settings, name: 'Open Settings', category: 'System', shortcut: '⌘,', }, { icon: User, name: 'User Profile', category: 'Account', shortcut: '⌘K U', }, { icon: Calculator, name: 'Calculator', category: 'Tools', shortcut: '⌘K Calc', }, { icon: Home, name: 'Go Home', category: 'Navigation', shortcut: '⌘H' }, { icon: CreditCard, name: 'Billing', category: 'Account', shortcut: '⌘K B', }, { icon: MessageSquare, name: 'Messages', category: 'Communication', shortcut: '⌘K M', }, { icon: Plus, name: 'Create New', category: 'Actions', shortcut: '⌘Shift+N', }, ]; // Group items by category const groupedItems = items.reduce( (acc, item) => { if (!acc[item.category]) { acc[item.category] = []; } acc[item.category].push(item); return acc; }, {} as Record<string, typeof items> ); return ( <div className="w-96 rounded-lg border shadow-lg"> <CommandRoot> <Command.Input placeholder="Search from 10 commands..." /> <Command.List> <Command.Empty> <div className="py-6 text-center"> <Search className="mx-auto mb-2 h-6 w-6 text-gray-400" /> <p className="text-gray-500 text-sm">No matching commands</p> </div> </Command.Empty> {Object.entries(groupedItems).map(([category, categoryItems]) => ( <div key={category}> <Command.Group heading={category}> {categoryItems.map((item) => { const IconComponent = item.icon; return ( <Command.Item key={item.name} value={item.name}> <IconComponent className="mr-2 h-4 w-4" /> <span>{item.name}</span> <Command.Shortcut>{item.shortcut}</Command.Shortcut> </Command.Item> ); })} </Command.Group> <Command.Separator /> </div> ))} </Command.List> </CommandRoot> </div> ); }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); const searchInput = canvas.getByPlaceholderText( 'Search from 10 commands...' ); // Test progressive search refinement await userEvent.type(searchInput, 'ca'); // Should show Calendar and Calculator await expect(canvas.getByText('Open Calendar')).toBeVisible(); await expect(canvas.getByText('Calculator')).toBeVisible(); // Refine search await userEvent.type(searchInput, 'l'); // "cal" // Test clearing search await userEvent.clear(searchInput); // All items should be visible again await expect(canvas.getByText('Open Calendar')).toBeVisible(); await expect(canvas.getByText('Check Email')).toBeVisible(); await expect(canvas.getByText('User Profile')).toBeVisible(); }, };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server