diff --git a/apps/desktop/src/main/menu.test.ts b/apps/desktop/src/main/menu.test.ts index 3f982bc35d5..714ed9f0d4c 100644 --- a/apps/desktop/src/main/menu.test.ts +++ b/apps/desktop/src/main/menu.test.ts @@ -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', () => { diff --git a/apps/desktop/src/main/menu.ts b/apps/desktop/src/main/menu.ts index 615fa1354db..26121ed077b 100644 --- a/apps/desktop/src/main/menu.ts +++ b/apps/desktop/src/main/menu.ts @@ -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()), }, ], diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx index d3f76ca1ae1..6f8653d1611 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx @@ -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 ( setActiveIndex(index)} onClick={() => select({ type, id: item.id, title: item.name })} > diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx index 1855d99eced..a16be8f1c04 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx @@ -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 })} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/skills-menu-dropdown/skills-menu-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/skills-menu-dropdown/skills-menu-dropdown.tsx index f2569647696..c350b1d6be9 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/skills-menu-dropdown/skills-menu-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/skills-menu-dropdown/skills-menu-dropdown.tsx @@ -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' ? : } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx index 29bc6910540..814535edf1b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx @@ -81,11 +81,8 @@ export function CollapsedFileFolderItems({ {fileFlyoutEntries(nodes, rootFiles ?? []).map((entry) => { if (entry.kind === 'file') { return ( - - + + {FILE_FLYOUT_ICON} {entry.name} @@ -107,7 +104,7 @@ export function CollapsedFileFolderItems({ return ( - + {folder.name} @@ -265,7 +262,7 @@ export function CollapsedChatFlyoutItem({ return ( - + {folder.name} diff --git a/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx b/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx index a9af709e481..ee7555508d6 100644 --- a/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx +++ b/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx @@ -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< , React.ComponentPropsWithoutRef & { 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 (
@@ -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 ( @@ -301,7 +343,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<