improvement(desktop): help menu

This commit is contained in:
Siddharth Ganesan
2026-08-11 18:06:40 -07:00
committed by GitHub
parent 092311ea68
commit e7590c828c
7 changed files with 68 additions and 24 deletions
+2 -2
View File
@@ -90,9 +90,9 @@ describe('buildMenuTemplate', () => {
])
})
it('keeps Help limited to documentation and system status', () => {
it('keeps Help limited to documentation and Sim status', () => {
const help = submenu(buildMenuTemplate(makeDeps()), 'Help')
expect(help.map((item) => item.label)).toEqual(['Sim Documentation', 'System Status'])
expect(help.map((item) => item.label)).toEqual(['Sim Documentation', 'Sim Status'])
})
it('never exposes developer tools in the application menu', () => {
+1 -1
View File
@@ -211,7 +211,7 @@ export function buildMenuTemplate(deps: MenuDeps): MenuItemConstructorOptions[]
click: () => void openExternalSafe(DOCS_URL, deps.allowHttpLocalhost()),
},
{
label: 'System Status',
label: 'Sim Status',
click: () => void openExternalSafe(STATUS_URL, deps.allowHttpLocalhost()),
},
],
@@ -686,10 +686,13 @@ export function AddResourceDropdown({
filtered.length > 0 ? (
filtered.map(({ type, item }, index) => {
const config = getResourceConfig(type)
/* The search box keeps focus, so rows never take DOM focus and the menu's
own `focus:` highlight never fires — `activeIndex` is this list's
cursor, so it paints the hover surface rather than the selected one. */
return (
<DropdownMenuItem
key={`${type}:${item.id}`}
className={cn(index === activeIndex && 'bg-[var(--surface-active)]')}
className={cn(index === activeIndex && 'bg-[var(--surface-hover)]')}
onMouseEnter={() => setActiveIndex(index)}
onClick={() => select({ type, id: item.id, title: item.name })}
>
@@ -345,8 +345,9 @@ export const PlusMenuDropdown = React.memo(
handleSelect({ type, id: item.id, title: item.name })
}}
className={cn(
'relative flex w-full min-w-0 cursor-pointer select-none items-center gap-2 rounded-[5px] px-2 py-1.5 text-left text-[var(--text-body)] text-caption outline-none transition-colors [&>span]:min-w-0 [&>span]:truncate [&_svg]:pointer-events-none [&_svg]:size-[14px] [&_svg]:shrink-0 [&_svg]:text-[var(--text-icon)]',
isActive && 'bg-[var(--surface-active)]'
'relative flex w-full min-w-0 cursor-pointer select-none items-center gap-2 rounded-[5px] px-2 py-1.5 text-left text-[var(--text-body)] text-caption outline-none transition-colors duration-0 [&>span]:min-w-0 [&>span]:truncate [&_svg]:pointer-events-none [&_svg]:size-[14px] [&_svg]:shrink-0 [&_svg]:text-[var(--text-icon)]',
/* `activeIndex` is the cursor, not a selection — hover surface. */
isActive && 'bg-[var(--surface-hover)]'
)}
>
{config.renderDropdownItem({ item })}
@@ -210,8 +210,9 @@ export const SkillsMenuDropdown = React.memo(
onMouseEnter={() => setActiveIndex(index)}
onClick={() => handleSelect(target)}
className={cn(
'relative flex w-full min-w-0 cursor-pointer select-none items-center gap-2 rounded-[5px] px-2 py-1.5 text-left text-[var(--text-body)] text-caption outline-none transition-colors [&>span]:min-w-0 [&>span]:truncate [&_svg]:pointer-events-none [&_svg]:size-[14px] [&_svg]:shrink-0 [&_svg]:text-[var(--text-icon)]',
isActive && 'bg-[var(--surface-active)]'
'relative flex w-full min-w-0 cursor-pointer select-none items-center gap-2 rounded-[5px] px-2 py-1.5 text-left text-[var(--text-body)] text-caption outline-none transition-colors duration-0 [&>span]:min-w-0 [&>span]:truncate [&_svg]:pointer-events-none [&_svg]:size-[14px] [&_svg]:shrink-0 [&_svg]:text-[var(--text-icon)]',
/* `activeIndex` is the cursor, not a selection — hover surface. */
isActive && 'bg-[var(--surface-hover)]'
)}
>
{target.kind === 'skill' ? <AgentSkillsIcon /> : <McpIcon />}
@@ -81,11 +81,8 @@ export function CollapsedFileFolderItems({
{fileFlyoutEntries(nodes, rootFiles ?? []).map((entry) => {
if (entry.kind === 'file') {
return (
<DropdownMenuItem key={entry.id} asChild>
<Link
href={`/workspace/${workspaceId}/files/${entry.file.id}`}
className={cn(currentFileId === entry.file.id && 'bg-[var(--surface-active)]')}
>
<DropdownMenuItem key={entry.id} asChild active={currentFileId === entry.file.id}>
<Link href={`/workspace/${workspaceId}/files/${entry.file.id}`}>
{FILE_FLYOUT_ICON}
<span className='truncate'>{entry.name}</span>
</Link>
@@ -107,7 +104,7 @@ export function CollapsedFileFolderItems({
return (
<DropdownMenuSub key={folder.id}>
<DropdownMenuSubTrigger className='focus:bg-[var(--surface-hover)] data-[state=open]:bg-[var(--surface-hover)]'>
<DropdownMenuSubTrigger>
<Folder className='size-[14px]' />
<span className='truncate'>{folder.name}</span>
</DropdownMenuSubTrigger>
@@ -265,7 +262,7 @@ export function CollapsedChatFlyoutItem({
return (
<DropdownMenuItem
asChild
className={cn((isCurrentRoute || isMenuOpen) && 'bg-[var(--surface-active)]')}
active={isCurrentRoute || isMenuOpen}
action={
showActions ? (
<DropdownMenuItemAction
@@ -342,7 +339,7 @@ export function CollapsedWorkflowFlyoutItem({
return (
<DropdownMenuItem
asChild
className={cn((isCurrentRoute || actionsOpen) && 'bg-[var(--surface-active)]')}
active={isCurrentRoute || actionsOpen}
action={
hasActions ? (
<DropdownMenuSub
@@ -460,7 +457,7 @@ export function CollapsedFolderItems(props: CollapsedFolderItemsProps) {
return (
<DropdownMenuSub key={folder.id}>
<DropdownMenuSubTrigger className='focus:bg-[var(--surface-active)] data-[state=open]:bg-[var(--surface-active)]'>
<DropdownMenuSubTrigger>
<Folder className='size-[14px]' />
<span className='truncate'>{folder.name}</span>
</DropdownMenuSubTrigger>
@@ -40,6 +40,33 @@ const ANIMATION_CLASSES =
const MENU_ROW_HEIGHT_CLASS = 'h-[28px]'
const MENU_ROW_RADIUS_CLASS = 'rounded-lg'
/**
* Rows settle instantly, matching the sidebar (`[&_.group.cursor-pointer]:duration-0`
* on its `aside`). A menu is walked, not read: at the default 150ms the fill lags a
* cursor dragged down the list and two or three rows are mid-fade at once, which reads
* as smear rather than as one row following the pointer. `transition-colors` stays so a
* consumer can opt a row back into a duration.
*/
const MENU_ROW_TRANSITION_CLASS = 'transition-colors duration-0'
/**
* The two row surfaces, mirroring `chipHoverSurfaceClass` / `chipActiveSurfaceClass`
* — mutually exclusive, so a selected row holds its surface through hover instead of
* dimming to the hover fill under the cursor.
*
* Highlight is keyed off `focus:`, not `hover:`: Radix moves DOM focus to the row on
* pointer-move, so one selector covers both the pointer and the arrow-key cursor.
* Rows previously highlighted to `--surface-active` — the *selected* surface — so a
* hovered row looked selected and a menu appeared to have two selections at once. The
* `group-*` variants are inert outside the `action` layout, which is the only place a
* `group/dropdownitem` ancestor exists.
*/
const MENU_ROW_HIGHLIGHT_CLASS =
'focus:bg-[var(--surface-hover)] group-focus-within/dropdownitem:bg-[var(--surface-hover)] group-hover/dropdownitem:bg-[var(--surface-hover)]'
/** @see {@link MENU_ROW_HIGHLIGHT_CLASS} — the selected half of the same pair. */
const MENU_ROW_SELECTED_CLASS =
'bg-[var(--surface-active)] focus:bg-[var(--surface-active)] group-focus-within/dropdownitem:bg-[var(--surface-active)] group-hover/dropdownitem:bg-[var(--surface-active)]'
/**
* Rows are a fixed height, so a label that wraps overflows its row and paints
* over its neighbours instead of growing the row. Every row is therefore held
@@ -152,7 +179,10 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
`flex ${MENU_ROW_HEIGHT_CLASS} min-w-0 cursor-default select-none items-center gap-2 ${MENU_ROW_RADIUS_CLASS} px-2 text-[var(--text-body)] text-small outline-none transition-colors focus:bg-[var(--surface-active)] data-[state=open]:bg-[var(--surface-active)] ${MENU_ROW_SINGLE_LINE_CLASS} [&_svg]:pointer-events-none [&_svg]:size-[14px] [&_svg]:shrink-0 [&_svg]:text-[var(--text-icon)]`,
/* An open submenu keeps its trigger on the selected surface — including while
the pointer is on it, so walking into the submenu doesn't drop the trigger
back to the hover fill. */
`flex ${MENU_ROW_HEIGHT_CLASS} min-w-0 cursor-default select-none items-center gap-2 ${MENU_ROW_RADIUS_CLASS} px-2 text-[var(--text-body)] text-small outline-none ${MENU_ROW_TRANSITION_CLASS} ${MENU_ROW_HIGHLIGHT_CLASS} data-[state=open]:bg-[var(--surface-active)] data-[state=open]:focus:bg-[var(--surface-active)] ${MENU_ROW_SINGLE_LINE_CLASS} [&_svg]:pointer-events-none [&_svg]:size-[14px] [&_svg]:shrink-0 [&_svg]:text-[var(--text-icon)]`,
inset && 'pl-7',
className
)}
@@ -214,12 +244,22 @@ const DropdownMenuContent = React.forwardRef<
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DROPDOWN_MENU_ITEM_BASE_CLASSES = `relative flex ${MENU_ROW_HEIGHT_CLASS} min-w-0 cursor-pointer select-none items-center gap-2 ${MENU_ROW_RADIUS_CLASS} px-2 text-[var(--text-body)] text-small outline-none transition-colors focus:bg-[var(--surface-active)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50 ${MENU_ROW_SINGLE_LINE_CLASS} [&_svg]:pointer-events-none [&_svg]:size-[14px] [&_svg]:shrink-0 [&_svg]:text-[var(--text-icon)]`
const DROPDOWN_MENU_ITEM_BASE_CLASSES = `relative flex ${MENU_ROW_HEIGHT_CLASS} min-w-0 cursor-pointer select-none items-center gap-2 ${MENU_ROW_RADIUS_CLASS} px-2 text-[var(--text-body)] text-small outline-none ${MENU_ROW_TRANSITION_CLASS} data-[disabled]:pointer-events-none data-[disabled]:opacity-50 ${MENU_ROW_SINGLE_LINE_CLASS} [&_svg]:pointer-events-none [&_svg]:size-[14px] [&_svg]:shrink-0 [&_svg]:text-[var(--text-icon)]`
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
/**
* Renders the row as selected — the current route, the checked value, the row
* whose own menu is open. Selected is a state the row *holds*, so it keeps
* `--surface-active` through hover rather than dimming to the hover fill.
*
* Not for a pointer/keyboard cursor: that is the row highlight, which the row
* already paints on its own. A menu that marks its cursor row `active` puts two
* selections on screen.
*/
active?: boolean
/**
* Optional inline action rendered on the right edge of the item — e.g. a
* "more" icon button. Reveals on hover/focus of the row, and the row stays
@@ -227,8 +267,9 @@ const DropdownMenuItem = React.forwardRef<
*/
action?: React.ReactNode
}
>(({ className, inset, action, asChild, children, ...props }, ref) => {
>(({ className, inset, active, action, asChild, children, ...props }, ref) => {
const content = asChild ? children : withEllipsizedLabel(children)
const stateClasses = active ? MENU_ROW_SELECTED_CLASS : MENU_ROW_HIGHLIGHT_CLASS
if (action) {
return (
<div className='group/dropdownitem relative'>
@@ -236,7 +277,8 @@ const DropdownMenuItem = React.forwardRef<
ref={ref}
className={cn(
DROPDOWN_MENU_ITEM_BASE_CLASSES,
'pr-[28px] group-focus-within/dropdownitem:bg-[var(--surface-active)] group-hover/dropdownitem:bg-[var(--surface-active)]',
stateClasses,
'pr-[28px]',
inset && 'pl-7',
className
)}
@@ -254,7 +296,7 @@ const DropdownMenuItem = React.forwardRef<
return (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(DROPDOWN_MENU_ITEM_BASE_CLASSES, inset && 'pl-7', className)}
className={cn(DROPDOWN_MENU_ITEM_BASE_CLASSES, stateClasses, inset && 'pl-7', className)}
asChild={asChild}
{...props}
>
@@ -301,7 +343,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
`relative flex ${MENU_ROW_HEIGHT_CLASS} min-w-0 cursor-default select-none items-center ${MENU_ROW_RADIUS_CLASS} whitespace-nowrap pr-2 pl-7 text-[var(--text-body)] text-small outline-none transition-colors focus:bg-[var(--surface-active)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50`,
`relative flex ${MENU_ROW_HEIGHT_CLASS} min-w-0 cursor-default select-none items-center ${MENU_ROW_RADIUS_CLASS} whitespace-nowrap pr-2 pl-7 text-[var(--text-body)] text-small outline-none ${MENU_ROW_TRANSITION_CLASS} ${MENU_ROW_HIGHLIGHT_CLASS} data-[disabled]:pointer-events-none data-[disabled]:opacity-50`,
className
)}
checked={checked}
@@ -324,7 +366,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
`relative flex ${MENU_ROW_HEIGHT_CLASS} min-w-0 cursor-default select-none items-center ${MENU_ROW_RADIUS_CLASS} whitespace-nowrap pr-2 pl-7 text-[var(--text-body)] text-small outline-none transition-colors focus:bg-[var(--surface-active)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50`,
`relative flex ${MENU_ROW_HEIGHT_CLASS} min-w-0 cursor-default select-none items-center ${MENU_ROW_RADIUS_CLASS} whitespace-nowrap pr-2 pl-7 text-[var(--text-body)] text-small outline-none ${MENU_ROW_TRANSITION_CLASS} ${MENU_ROW_HIGHLIGHT_CLASS} data-[disabled]:pointer-events-none data-[disabled]:opacity-50`,
className
)}
{...props}