aaf3298d61
Committing a model with Enter left focus on the pill (Radix restores it to the trigger), so the next thing typed went nowhere instead of into the message being written. And with the cursor parked over the list, rows re-flowing under it as the query narrowed hover-stole the selection mid-type — Enter then committed whatever the mouse happened to be over. Both surfaces adopt the shared primitives: the model menu and every cmdk list (palette, model dialog, session picker, searchable selects) go pointer-inert until the mouse moves, and the model menu plus the command palette hand typing focus back on close. The release defers a frame and yields when something editable already claimed focus, so a palette action that opens a dialog or navigates keeps its own focus. Replaces the model menu's focus/blur highlight gate — pointer intent is the real signal, so the keyboard highlight no longer flickers off when Radix moves DOM focus onto a hovered row.
125 lines
4.0 KiB
TypeScript
125 lines
4.0 KiB
TypeScript
import { Command as CommandPrimitive } from 'cmdk'
|
|
import * as React from 'react'
|
|
|
|
import { usePointerQuiet } from '@/components/ui/keyboard-first'
|
|
import { SearchIcon } from '@/lib/icons'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
|
|
return (
|
|
<CommandPrimitive
|
|
className={cn(
|
|
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
|
className
|
|
)}
|
|
data-slot="command"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
interface CommandInputProps extends React.ComponentProps<typeof CommandPrimitive.Input> {
|
|
/** Inline trailing slot, rendered on the right of the search row. */
|
|
right?: React.ReactNode
|
|
}
|
|
|
|
function CommandInput({ className, right, ...props }: CommandInputProps) {
|
|
return (
|
|
<div className="flex h-11 items-center gap-2 border-b border-border px-3" data-slot="command-input-wrapper">
|
|
<SearchIcon className="size-4 shrink-0 text-muted-foreground" />
|
|
<CommandPrimitive.Input
|
|
className={cn(
|
|
'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
|
|
className
|
|
)}
|
|
data-slot="command-input"
|
|
{...props}
|
|
/>
|
|
{right}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {
|
|
// cmdk selects on pointer-enter, so a list that opens under a parked cursor —
|
|
// or re-flows under one as the query narrows — hands the selection to
|
|
// whatever row slid beneath the mouse, and Enter commits THAT. Inert until
|
|
// the pointer actually moves (see usePointerQuiet).
|
|
const pointerQuiet = usePointerQuiet()
|
|
|
|
return (
|
|
<CommandPrimitive.List
|
|
className={cn('max-h-100 overflow-y-auto overflow-x-hidden', pointerQuiet && 'pointer-events-none', className)}
|
|
data-slot="command-list"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
|
return (
|
|
<CommandPrimitive.Empty
|
|
className="py-6 text-center text-sm text-muted-foreground"
|
|
data-slot="command-empty"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function CommandGroup({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
|
return (
|
|
<CommandPrimitive.Group
|
|
className={cn(
|
|
'overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:sticky **:[[cmdk-group-heading]]:top-0 **:[[cmdk-group-heading]]:z-10 **:[[cmdk-group-heading]]:bg-popover **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground',
|
|
className
|
|
)}
|
|
data-slot="command-group"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function CommandSeparator({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
|
return (
|
|
<CommandPrimitive.Separator
|
|
className={cn('-mx-1 h-px bg-border', className)}
|
|
data-slot="command-separator"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
|
return (
|
|
<CommandPrimitive.Item
|
|
className={cn(
|
|
'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50',
|
|
className
|
|
)}
|
|
data-slot="command-item"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function CommandShortcut({ className, ...props }: React.ComponentProps<'span'>) {
|
|
return (
|
|
<span
|
|
className={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)}
|
|
data-slot="command-shortcut"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
CommandSeparator,
|
|
CommandShortcut
|
|
}
|