diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-chrome/command-chrome.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-chrome/command-chrome.test.tsx index f08c6adda6d..68b290bff6d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-chrome/command-chrome.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-chrome/command-chrome.test.tsx @@ -57,9 +57,12 @@ describe('CommandFadedList', () => { }) const list = container.querySelector('[cmdk-list]') + const input = container.querySelector('[cmdk-input]') const search = container.querySelector('[cmdk-input]')?.parentElement expect(list?.className).toContain('transparent_36px,black_58px,black_calc(100%_-_13px)') expect(list?.className).not.toContain('scrollbar-track') + expect(input?.className).toContain('-ml-1') + expect(input?.className).toContain('indent-1') expect(search?.className).toContain('var(--bg)') }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-chrome/command-chrome.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-chrome/command-chrome.tsx index a71d83db3eb..bedee9fff8e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-chrome/command-chrome.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-chrome/command-chrome.tsx @@ -55,7 +55,12 @@ const LIST_FADE_CLASSNAME = { '[-webkit-mask-image:linear-gradient(to_bottom,transparent_0px,transparent_36px,black_58px,black_calc(100%_-_13px),transparent_100%)] [mask-image:linear-gradient(to_bottom,transparent_0px,transparent_36px,black_58px,black_calc(100%_-_13px),transparent_100%)]', } as const -/** Borderless search field layered over a fading command-result list. */ +/** + * Borderless search field layered over a fading command-result list. + * + * The matching indent and negative margin give leading glyphs room inside + * Chrome's input clip edge without moving the text out of alignment. + */ export const CommandSearch = forwardRef( function CommandSearch( { surface, cycleResultsOnTab = false, endAdornment, onKeyDown, ...props }, @@ -85,7 +90,7 @@ export const CommandSearch = forwardRef( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.test.tsx index fcdd9e3c14b..4024fa40810 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.test.tsx @@ -252,6 +252,45 @@ describe('SearchModal', () => { expect(rows[2]).toContain('Onboarding') }) + it('puts an exact-name block before its page and contents on the workflow editor', async () => { + const Icon = () => null + const original = { ...mockSearchState.data } + mockSearchState.data = { + ...mockSearchState.data, + blocks: [{ id: 'logs', name: 'Logs', icon: Icon, bgColor: '#111', type: 'logs' }], + } + const logs = [ + { + id: 'log-1', + name: 'Billing sync', + href: '/workspace/workspace-1/logs?executionId=e1', + date: 'Aug 8, 1:00 PM', + }, + { + id: 'log-2', + name: 'Onboarding', + href: '/workspace/workspace-1/logs?executionId=e2', + date: 'Aug 8, 2:00 PM', + }, + ] + try { + await act(async () => { + root.render() + }) + + await enterSearchQuery('Logs') + const rows = Array.from(document.querySelectorAll('[cmdk-item]')) + expect(rows.slice(0, 4).map((row) => row.textContent ?? '')).toEqual([ + 'Logs', + 'Logs⇧⌘L', + 'Billing syncAug 8, 1:00 PM', + 'OnboardingAug 8, 2:00 PM', + ]) + } finally { + mockSearchState.data = original + } + }) + it('puts Create workflow first for the module-name query, then the workflows', async () => { const workflows = [ { id: 'workflow-a', name: 'Alpha', href: '/workspace/workspace-1/w/workflow-a' }, @@ -278,6 +317,24 @@ describe('SearchModal', () => { expect(rows[2]).toContain('Beta') }) + it('puts New chat first for the module-name query, then the chats', async () => { + const chats = [ + { id: 'chat-a', name: 'Alpha', href: '/workspace/workspace-1/home?chatId=chat-a' }, + { id: 'chat-b', name: 'Beta', href: '/workspace/workspace-1/home?chatId=chat-b' }, + ] + await act(async () => { + root.render() + }) + + await enterSearchQuery('chats') + const rows = Array.from(document.querySelectorAll('[cmdk-item]')).map( + (el) => el.textContent ?? '' + ) + expect(rows[0]).toContain('New chat') + expect(rows[1]).toContain('Alpha') + expect(rows[2]).toContain('Beta') + }) + it('shows an empty state when search has no results', async () => { await act(async () => { root.render() diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx index 0073598ccab..58a79fad7e6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx @@ -99,6 +99,17 @@ import { useSearchModalStore } from '@/stores/modals/search/store' import type { SearchBlockItem, SearchToolOperationItem } from '@/stores/modals/search/types' const logger = createLogger('SearchModal') + +/** + * Half of the dialog's effective width (`min(500px, 100% - 32px)`), used to + * clamp the centered `left` position. The dialog centers over the content + * area — offset right by the sidebar (and panel on the canvas) — so on narrow + * viewports the unclamped position pushes it past the right edge, clipping + * the input adornment and the empty state. Clamping keeps a 16px gutter on + * both sides; when the viewport is narrower than the dialog plus gutters, + * both clamp bounds collapse to `50%` and the dialog re-centers. + */ +const PALETTE_HALF_WIDTH = 'min(250px, 50% - 16px)' /** * Global row budget for the browse (empty-query) list, applied cumulatively in * section order. Individual sections are never capped in browse — the budget @@ -377,7 +388,8 @@ function SearchModalContent({ list.push({ id: 'new-chat', name: 'New chat', - keywords: 'chat message ask sim assistant home', + keywords: 'chat chats message ask sim assistant home', + exactQueries: ['chats'], icon: Home, context: 'global', run: () => routerRef.current.push(`/workspace/${workspaceId}/home`), @@ -1065,7 +1077,11 @@ function SearchModalContent({ availableBlocks, (item) => item.name, (item) => item.searchValue - ).map(({ item, score }) => ({ section: 'blocks', item, score })), + ).map(({ item, score }) => ({ + section: 'blocks', + item, + score: item.name.toLowerCase() === query.toLowerCase() ? PAGE_MATCH_TIER : score, + })), triggers: rank( 'triggers', displayTriggers, @@ -1301,14 +1317,15 @@ function SearchModalContent({ aria-hidden={!visuallyOpen} aria-label='Search' className={cn( - '-translate-x-1/2 fixed top-[15%] z-[var(--z-modal)] w-[500px] rounded-xl border border-[var(--border-muted)] bg-[var(--surface-4)] p-[3px] shadow-[var(--shadow-overlay)] dark:bg-[var(--surface-5)]', + '-translate-x-1/2 fixed top-[15%] z-[var(--z-modal)] w-[min(500px,calc(100%-32px))] rounded-xl border border-[var(--border-muted)] bg-[var(--surface-4)] p-[3px] shadow-[var(--shadow-overlay)] dark:bg-[var(--surface-5)]', visuallyOpen ? 'visible opacity-100' : 'invisible opacity-0' )} style={{ - left: + left: `clamp(calc(16px + ${PALETTE_HALF_WIDTH}), ${ pageContext === 'workflow' ? 'calc(50% + (var(--sidebar-width) - var(--panel-width)) / 2)' - : 'calc(var(--sidebar-width) / 2 + 50%)', + : 'calc(var(--sidebar-width) / 2 + 50%)' + }, calc(100% - 16px - ${PALETTE_HALF_WIDTH}))`, }} >
@@ -1319,11 +1336,17 @@ function SearchModalContent({ value={askMode ? askSimLabel : undefined} >
+ {/* 85dvh - 26px = viewport minus the 15% top offset, 10px of + dialog chrome, and a 16px bottom gutter. The cap keeps the + scroll box fully on-screen: cmdk aligns the selected row to + the box's bottom edge, so a box past the fold parks the + selection below the viewport and held arrow keys judder + rows against an edge the user cannot see. */}