Skip to content

Commit e7590c8

Browse files
authored
improvement(desktop): help menu
1 parent 092311e commit e7590c8

7 files changed

Lines changed: 68 additions & 24 deletions

File tree

apps/desktop/src/main/menu.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,9 @@ describe('buildMenuTemplate', () => {
9090
])
9191
})
9292

93-
it('keeps Help limited to documentation and system status', () => {
93+
it('keeps Help limited to documentation and Sim status', () => {
9494
const help = submenu(buildMenuTemplate(makeDeps()), 'Help')
95-
expect(help.map((item) => item.label)).toEqual(['Sim Documentation', 'System Status'])
95+
expect(help.map((item) => item.label)).toEqual(['Sim Documentation', 'Sim Status'])
9696
})
9797

9898
it('never exposes developer tools in the application menu', () => {

apps/desktop/src/main/menu.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ export function buildMenuTemplate(deps: MenuDeps): MenuItemConstructorOptions[]
211211
click: () => void openExternalSafe(DOCS_URL, deps.allowHttpLocalhost()),
212212
},
213213
{
214-
label: 'System Status',
214+
label: 'Sim Status',
215215
click: () => void openExternalSafe(STATUS_URL, deps.allowHttpLocalhost()),
216216
},
217217
],

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,10 +686,13 @@ export function AddResourceDropdown({
686686
filtered.length > 0 ? (
687687
filtered.map(({ type, item }, index) => {
688688
const config = getResourceConfig(type)
689+
/* The search box keeps focus, so rows never take DOM focus and the menu's
690+
own `focus:` highlight never fires — `activeIndex` is this list's
691+
cursor, so it paints the hover surface rather than the selected one. */
689692
return (
690693
<DropdownMenuItem
691694
key={`${type}:${item.id}`}
692-
className={cn(index === activeIndex && 'bg-[var(--surface-active)]')}
695+
className={cn(index === activeIndex && 'bg-[var(--surface-hover)]')}
693696
onMouseEnter={() => setActiveIndex(index)}
694697
onClick={() => select({ type, id: item.id, title: item.name })}
695698
>

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,9 @@ export const PlusMenuDropdown = React.memo(
345345
handleSelect({ type, id: item.id, title: item.name })
346346
}}
347347
className={cn(
348-
'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)]',
349-
isActive && 'bg-[var(--surface-active)]'
348+
'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)]',
349+
/* `activeIndex` is the cursor, not a selection — hover surface. */
350+
isActive && 'bg-[var(--surface-hover)]'
350351
)}
351352
>
352353
{config.renderDropdownItem({ item })}

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/skills-menu-dropdown/skills-menu-dropdown.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,9 @@ export const SkillsMenuDropdown = React.memo(
210210
onMouseEnter={() => setActiveIndex(index)}
211211
onClick={() => handleSelect(target)}
212212
className={cn(
213-
'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)]',
214-
isActive && 'bg-[var(--surface-active)]'
213+
'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)]',
214+
/* `activeIndex` is the cursor, not a selection — hover surface. */
215+
isActive && 'bg-[var(--surface-hover)]'
215216
)}
216217
>
217218
{target.kind === 'skill' ? <AgentSkillsIcon /> : <McpIcon />}

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,8 @@ export function CollapsedFileFolderItems({
8181
{fileFlyoutEntries(nodes, rootFiles ?? []).map((entry) => {
8282
if (entry.kind === 'file') {
8383
return (
84-
<DropdownMenuItem key={entry.id} asChild>
85-
<Link
86-
href={`/workspace/${workspaceId}/files/${entry.file.id}`}
87-
className={cn(currentFileId === entry.file.id && 'bg-[var(--surface-active)]')}
88-
>
84+
<DropdownMenuItem key={entry.id} asChild active={currentFileId === entry.file.id}>
85+
<Link href={`/workspace/${workspaceId}/files/${entry.file.id}`}>
8986
{FILE_FLYOUT_ICON}
9087
<span className='truncate'>{entry.name}</span>
9188
</Link>
@@ -107,7 +104,7 @@ export function CollapsedFileFolderItems({
107104

108105
return (
109106
<DropdownMenuSub key={folder.id}>
110-
<DropdownMenuSubTrigger className='focus:bg-[var(--surface-hover)] data-[state=open]:bg-[var(--surface-hover)]'>
107+
<DropdownMenuSubTrigger>
111108
<Folder className='size-[14px]' />
112109
<span className='truncate'>{folder.name}</span>
113110
</DropdownMenuSubTrigger>
@@ -265,7 +262,7 @@ export function CollapsedChatFlyoutItem({
265262
return (
266263
<DropdownMenuItem
267264
asChild
268-
className={cn((isCurrentRoute || isMenuOpen) && 'bg-[var(--surface-active)]')}
265+
active={isCurrentRoute || isMenuOpen}
269266
action={
270267
showActions ? (
271268
<DropdownMenuItemAction
@@ -342,7 +339,7 @@ export function CollapsedWorkflowFlyoutItem({
342339
return (
343340
<DropdownMenuItem
344341
asChild
345-
className={cn((isCurrentRoute || actionsOpen) && 'bg-[var(--surface-active)]')}
342+
active={isCurrentRoute || actionsOpen}
346343
action={
347344
hasActions ? (
348345
<DropdownMenuSub
@@ -460,7 +457,7 @@ export function CollapsedFolderItems(props: CollapsedFolderItemsProps) {
460457

461458
return (
462459
<DropdownMenuSub key={folder.id}>
463-
<DropdownMenuSubTrigger className='focus:bg-[var(--surface-active)] data-[state=open]:bg-[var(--surface-active)]'>
460+
<DropdownMenuSubTrigger>
464461
<Folder className='size-[14px]' />
465462
<span className='truncate'>{folder.name}</span>
466463
</DropdownMenuSubTrigger>

packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,33 @@ const ANIMATION_CLASSES =
4040
const MENU_ROW_HEIGHT_CLASS = 'h-[28px]'
4141
const MENU_ROW_RADIUS_CLASS = 'rounded-lg'
4242

43+
/**
44+
* Rows settle instantly, matching the sidebar (`[&_.group.cursor-pointer]:duration-0`
45+
* on its `aside`). A menu is walked, not read: at the default 150ms the fill lags a
46+
* cursor dragged down the list and two or three rows are mid-fade at once, which reads
47+
* as smear rather than as one row following the pointer. `transition-colors` stays so a
48+
* consumer can opt a row back into a duration.
49+
*/
50+
const MENU_ROW_TRANSITION_CLASS = 'transition-colors duration-0'
51+
52+
/**
53+
* The two row surfaces, mirroring `chipHoverSurfaceClass` / `chipActiveSurfaceClass`
54+
* — mutually exclusive, so a selected row holds its surface through hover instead of
55+
* dimming to the hover fill under the cursor.
56+
*
57+
* Highlight is keyed off `focus:`, not `hover:`: Radix moves DOM focus to the row on
58+
* pointer-move, so one selector covers both the pointer and the arrow-key cursor.
59+
* Rows previously highlighted to `--surface-active` — the *selected* surface — so a
60+
* hovered row looked selected and a menu appeared to have two selections at once. The
61+
* `group-*` variants are inert outside the `action` layout, which is the only place a
62+
* `group/dropdownitem` ancestor exists.
63+
*/
64+
const MENU_ROW_HIGHLIGHT_CLASS =
65+
'focus:bg-[var(--surface-hover)] group-focus-within/dropdownitem:bg-[var(--surface-hover)] group-hover/dropdownitem:bg-[var(--surface-hover)]'
66+
/** @see {@link MENU_ROW_HIGHLIGHT_CLASS} — the selected half of the same pair. */
67+
const MENU_ROW_SELECTED_CLASS =
68+
'bg-[var(--surface-active)] focus:bg-[var(--surface-active)] group-focus-within/dropdownitem:bg-[var(--surface-active)] group-hover/dropdownitem:bg-[var(--surface-active)]'
69+
4370
/**
4471
* Rows are a fixed height, so a label that wraps overflows its row and paints
4572
* over its neighbours instead of growing the row. Every row is therefore held
@@ -152,7 +179,10 @@ const DropdownMenuSubTrigger = React.forwardRef<
152179
<DropdownMenuPrimitive.SubTrigger
153180
ref={ref}
154181
className={cn(
155-
`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)]`,
182+
/* An open submenu keeps its trigger on the selected surface — including while
183+
the pointer is on it, so walking into the submenu doesn't drop the trigger
184+
back to the hover fill. */
185+
`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)]`,
156186
inset && 'pl-7',
157187
className
158188
)}
@@ -214,29 +244,41 @@ const DropdownMenuContent = React.forwardRef<
214244
))
215245
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
216246

217-
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)]`
247+
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)]`
218248

219249
const DropdownMenuItem = React.forwardRef<
220250
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
221251
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
222252
inset?: boolean
253+
/**
254+
* Renders the row as selected — the current route, the checked value, the row
255+
* whose own menu is open. Selected is a state the row *holds*, so it keeps
256+
* `--surface-active` through hover rather than dimming to the hover fill.
257+
*
258+
* Not for a pointer/keyboard cursor: that is the row highlight, which the row
259+
* already paints on its own. A menu that marks its cursor row `active` puts two
260+
* selections on screen.
261+
*/
262+
active?: boolean
223263
/**
224264
* Optional inline action rendered on the right edge of the item — e.g. a
225265
* "more" icon button. Reveals on hover/focus of the row, and the row stays
226266
* highlighted while the cursor is over the action.
227267
*/
228268
action?: React.ReactNode
229269
}
230-
>(({ className, inset, action, asChild, children, ...props }, ref) => {
270+
>(({ className, inset, active, action, asChild, children, ...props }, ref) => {
231271
const content = asChild ? children : withEllipsizedLabel(children)
272+
const stateClasses = active ? MENU_ROW_SELECTED_CLASS : MENU_ROW_HIGHLIGHT_CLASS
232273
if (action) {
233274
return (
234275
<div className='group/dropdownitem relative'>
235276
<DropdownMenuPrimitive.Item
236277
ref={ref}
237278
className={cn(
238279
DROPDOWN_MENU_ITEM_BASE_CLASSES,
239-
'pr-[28px] group-focus-within/dropdownitem:bg-[var(--surface-active)] group-hover/dropdownitem:bg-[var(--surface-active)]',
280+
stateClasses,
281+
'pr-[28px]',
240282
inset && 'pl-7',
241283
className
242284
)}
@@ -254,7 +296,7 @@ const DropdownMenuItem = React.forwardRef<
254296
return (
255297
<DropdownMenuPrimitive.Item
256298
ref={ref}
257-
className={cn(DROPDOWN_MENU_ITEM_BASE_CLASSES, inset && 'pl-7', className)}
299+
className={cn(DROPDOWN_MENU_ITEM_BASE_CLASSES, stateClasses, inset && 'pl-7', className)}
258300
asChild={asChild}
259301
{...props}
260302
>
@@ -301,7 +343,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
301343
<DropdownMenuPrimitive.CheckboxItem
302344
ref={ref}
303345
className={cn(
304-
`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`,
346+
`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`,
305347
className
306348
)}
307349
checked={checked}
@@ -324,7 +366,7 @@ const DropdownMenuRadioItem = React.forwardRef<
324366
<DropdownMenuPrimitive.RadioItem
325367
ref={ref}
326368
className={cn(
327-
`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`,
369+
`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`,
328370
className
329371
)}
330372
{...props}

0 commit comments

Comments
 (0)