import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters" import type { Meta, StoryObj } from "@storybook/react" import type { ReactNode } from "react" import { useState } from "react" import { controlSizeArgType } from "../argTypes.storyHelpers.ts" import { Button } from "../Button/Button.tsx" import { StoryCell, StoryGrid, StorySection, } from "../board.storyHelpers.tsx" import { CommandPalette, type CommandPaletteProps, } from "./CommandPalette.tsx" import { CommandPalettePanel, type CommandPalettePanelProps, } from "./CommandPalettePanel.tsx" import { getWebSearchItem, LAUNCHER_ITEMS, LAUNCHER_PREFIXES, LAUNCHER_SUGGESTIONS, PanelFrame, } from "./commandPalette.storyHelpers.tsx" import type { CommandPaletteItem } from "./commandPaletteSections.ts" import { useCommandPalette } from "./useCommandPalette.ts" const meta = { title: "Components/Overlays/CommandPalette", component: CommandPalette, parameters: { layout: "padded" }, argTypes: { itemSize: controlSizeArgType }, args: { getFallbackItem: getWebSearchItem, isVisible: false, itemSize: "lg", items: LAUNCHER_ITEMS, label: "Search apps, bookmarks, and the web", maxResultsPerGroup: 6, onClose: () => {}, placeholder: "Search apps, bookmarks, or the web", prefixes: LAUNCHER_PREFIXES, suggestions: LAUNCHER_SUGGESTIONS, }, } satisfies Meta export default meta type Story = StoryObj /** * What an app writes: `useCommandPalette()` for the open state and the * global keys, a button for the pointer, and the palette. */ const LauncherDemo = ({ isVisible: _isVisible, onClose: _onClose, ...paletteProps }: CommandPaletteProps): ReactNode => { const palette = useCommandPalette() return (

Or press Ctrl K, Ctrl Space, or /, or start typing.

) } export const Playground: Story = { parameters: playgroundParameters, render: (paletteProps) => ( ), } const panelProps = ( paletteProps: CommandPaletteProps, ): CommandPalettePanelProps => { const { className: _className, isVisible: _isVisible, onClose: _onClose, ...rest } = paletteProps return rest } /** * The row's parts, and the three row sizes. Each panel is the * palette's contents laid out in place, so the boards can show every * case side by side; in an app it is always inside the dialog. */ export const AllVariants: Story = { render: (paletteProps) => (
{(["sm", "md", "lg"] as const).map((itemSize) => ( ))}
), } /** * Every state the list can be in. The first is the reason the * component exists: it lists something before anything is typed. */ export const AllStates: Story = { render: (paletteProps) => ( ), } /** * Opened over the page, before anything is typed. Like `Dialog`, the * palette is measured against the viewport, not a container, so this * story has no `ContainerBoard`: it is the window that varies. On a * wide window it hangs from the top at up to 45rem wide; in the Narrow * View it takes nearly the whole screen, and the key hints in the * footer give way to the prefix list. */ const OpenDemo = ({ defaultQuery, isVisible: _isVisible, onClose: _onClose, ...paletteProps }: CommandPaletteProps): ReactNode => { const [isVisible, setIsVisible] = useState(true) return (
setIsVisible(false)} />
) } export const Responsive: Story = { render: (paletteProps) => , } /** Open with `serv` typed: matches under their groups, then the fallback row. */ export const OpenFiltered: Story = { args: { defaultQuery: "serv" }, render: (paletteProps) => , } /** Open with `au lego` typed: one row, for the target that owns `au`. */ export const OpenPrefixCommand: Story = { args: { defaultQuery: "au lego" }, render: (paletteProps) => , } /** * The whole keyboard path. Ctrl K, Ctrl Space or / open it with an * empty field; any other key opens it with that key typed. Arrow * keys, Home and End move; Enter opens the row; Ctrl Enter opens a * link row in a new tab; Escape closes, and focus goes back to where * it was. A clear button appears when the app passes `clearIcon`. */ const ClearIcon = (): ReactNode => ( ) /** * Every row runs `onSelect` instead of navigating — the items, the * suggestions, the prefix rows and the fallback — so the story can * show what was chosen and never leaves the page. */ const InteractiveDemo = ( paletteProps: CommandPaletteProps, ): ReactNode => { const [chosen, setChosen] = useState("nothing yet") const toChoice = (item: CommandPaletteItem) => ({ ...item, href: undefined, onSelect: () => setChosen(item.title), }) const items = paletteProps.items.map(toChoice) const suggestions = (paletteProps.suggestions ?? []).map( (group) => ({ ...group, items: group.items.map(toChoice), }), ) const prefixes = (paletteProps.prefixes ?? []).map( ({ getHref: _getHref, ...prefix }) => ({ ...prefix, onSelect: (query: string) => setChosen(`${prefix.label}: ${query}`), }), ) const getFallbackItem = (query: string) => { const item = paletteProps.getFallbackItem?.(query) return item == null ? item : toChoice(item) } return (

Last chosen: {chosen}

) } /** * The whole keyboard path. Ctrl K, Ctrl Space or / open it with an * empty field; any other key opens it with that key typed. Arrow * keys, Home and End move; Enter opens the row; Ctrl Enter opens a * link row in a new tab; Escape closes, and focus goes back to where * it was. A clear button appears when the app passes `clearIcon`. */ export const Interactive: Story = { args: { clearIcon: }, render: (paletteProps) => ( ), }