Skip to content

Commit 637a046

Browse files
committed
improvement(search): redesign Cmd-K palette with curated empty state and drill-down browse
- Stop dumping the full ~1,500-item catalog (blocks, tools, triggers, 1,000+ tool operations, docs) into the palette on open; surface them only once the user types, mirroring the existing integrations group - Add a curated empty state: frecency-ranked Recents (new persisted store) and a Browse section that drills into block/trigger/integration categories - Scope drill-down shown as a removable pill in the search bar; Backspace on empty input or Escape pops the scope - Cap each result group to keep the DOM bounded on broad queries - Derive browse categories from block category + integrationType
1 parent 8c5da02 commit 637a046

7 files changed

Lines changed: 626 additions & 99 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import type { ComponentType } from 'react'
44
import { memo } from 'react'
55
import { Command } from 'cmdk'
6+
import { ChevronRight } from 'lucide-react'
67
import { File, Workflow } from '@/components/emcn/icons'
78
import { cn } from '@/lib/core/utils/cn'
89
import type { CommandItemProps } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils'
@@ -328,6 +329,43 @@ export const MemoizedPageItem = memo(
328329
prev.query === next.query
329330
)
330331

332+
export const MemoizedCategoryItem = memo(
333+
function CategoryItem({
334+
value,
335+
onSelect,
336+
icon: Icon,
337+
name,
338+
count,
339+
query,
340+
}: {
341+
value: string
342+
onSelect: () => void
343+
icon: ComponentType<{ className?: string }>
344+
name: string
345+
count: number
346+
query?: string
347+
}) {
348+
return (
349+
<Command.Item value={value} onSelect={onSelect} className={COMMAND_ITEM_CLASSNAME}>
350+
<Icon className='size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
351+
<span className='truncate text-[var(--text-body)]'>
352+
<HighlightedText text={name} query={query} />
353+
</span>
354+
<span className='ml-auto flex flex-shrink-0 items-center gap-1.5 text-[var(--text-subtle)] text-small'>
355+
{count}
356+
<ChevronRight className='size-[14px]' />
357+
</span>
358+
</Command.Item>
359+
)
360+
},
361+
(prev, next) =>
362+
prev.value === next.value &&
363+
prev.icon === next.icon &&
364+
prev.name === next.name &&
365+
prev.count === next.count &&
366+
prev.query === next.query
367+
)
368+
331369
export const MemoizedIconItem = memo(
332370
function IconItem({
333371
value,

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/search-groups.tsx

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,29 @@
33
import type { ComponentType } from 'react'
44
import { memo } from 'react'
55
import { Command } from 'cmdk'
6+
import {
7+
Activity,
8+
BarChart3,
9+
Blocks,
10+
FileText,
11+
GitBranch,
12+
LifeBuoy,
13+
type LucideIcon,
14+
Mail,
15+
Megaphone,
16+
MessageCircle,
17+
Search as SearchIcon,
18+
Shield,
19+
ShoppingCart,
20+
Sparkles,
21+
TrendingUp,
22+
Users,
23+
Zap,
24+
} from 'lucide-react'
625
import { Database, Table } from '@/components/emcn/icons'
726
import {
827
MemoizedActionItem,
28+
MemoizedCategoryItem,
929
MemoizedCommandItem,
1030
MemoizedFileItem,
1131
MemoizedIconItem,
@@ -26,10 +46,33 @@ import type {
2646
import { GROUP_HEADING_CLASSNAME } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils'
2747
import type {
2848
SearchBlockItem,
49+
SearchCategory,
2950
SearchDocItem,
3051
SearchToolOperationItem,
3152
} from '@/stores/modals/search/types'
3253

54+
/** Icon for each browsable category, keyed by {@link SearchCategory.id}. */
55+
const CATEGORY_ICONS: Record<string, LucideIcon> = {
56+
blocks: Blocks,
57+
triggers: Zap,
58+
ai: Sparkles,
59+
analytics: BarChart3,
60+
commerce: ShoppingCart,
61+
communication: MessageCircle,
62+
databases: Database,
63+
devops: GitBranch,
64+
documents: FileText,
65+
email: Mail,
66+
hr: Users,
67+
marketing: Megaphone,
68+
observability: Activity,
69+
productivity: Blocks,
70+
sales: TrendingUp,
71+
search: SearchIcon,
72+
security: Shield,
73+
support: LifeBuoy,
74+
}
75+
3376
export const ActionsGroup = memo(function ActionsGroup({
3477
items,
3578
onSelect,
@@ -57,18 +100,72 @@ export const ActionsGroup = memo(function ActionsGroup({
57100
)
58101
})
59102

103+
/** A recent selection resolved to a renderable row by the modal. */
104+
export interface RecentRenderItem {
105+
id: string
106+
label: string
107+
icon: ComponentType<{ className?: string }>
108+
bgColor: string
109+
onSelect: () => void
110+
}
111+
112+
export const RecentsGroup = memo(function RecentsGroup({ items }: { items: RecentRenderItem[] }) {
113+
if (items.length === 0) return null
114+
return (
115+
<Command.Group heading='Recent' className={GROUP_HEADING_CLASSNAME}>
116+
{items.map((item) => (
117+
<MemoizedCommandItem
118+
key={item.id}
119+
value={`${item.label} recent-${item.id}`}
120+
onSelect={item.onSelect}
121+
icon={item.icon}
122+
bgColor={item.bgColor}
123+
showColoredIcon
124+
label={item.label}
125+
/>
126+
))}
127+
</Command.Group>
128+
)
129+
})
130+
131+
export const BrowseGroup = memo(function BrowseGroup({
132+
items,
133+
onSelect,
134+
}: {
135+
items: SearchCategory[]
136+
onSelect: (category: SearchCategory) => void
137+
}) {
138+
if (items.length === 0) return null
139+
return (
140+
<Command.Group heading='Browse' className={GROUP_HEADING_CLASSNAME}>
141+
{items.map((category) => (
142+
<MemoizedCategoryItem
143+
key={category.id}
144+
value={`${category.label} category-${category.id}`}
145+
onSelect={() => onSelect(category)}
146+
icon={CATEGORY_ICONS[category.id] ?? Blocks}
147+
name={category.label}
148+
count={category.count}
149+
/>
150+
))}
151+
</Command.Group>
152+
)
153+
})
154+
60155
export const BlocksGroup = memo(function BlocksGroup({
61156
items,
62157
onSelect,
63158
query,
159+
heading = 'Blocks',
64160
}: {
65161
items: SearchBlockItem[]
66162
onSelect: (block: SearchBlockItem) => void
67163
query?: string
164+
heading?: string
68165
}) {
69166
if (items.length === 0) return null
70167
return (
71-
<Command.Group heading='Blocks' className={GROUP_HEADING_CLASSNAME}>
168+
<Command.Group heading={heading} className={GROUP_HEADING_CLASSNAME}>
72169
{items.map((block) => (
73170
<MemoizedCommandItem
74171
key={block.id}
@@ -89,14 +186,16 @@ export const ToolsGroup = memo(function ToolsGroup({
89186
items,
90187
onSelect,
91188
query,
189+
heading = 'Tools',
92190
}: {
93191
items: SearchBlockItem[]
94192
onSelect: (tool: SearchBlockItem) => void
95193
query?: string
194+
heading?: string
96195
}) {
97196
if (items.length === 0) return null
98197
return (
99-
<Command.Group heading='Tools' className={GROUP_HEADING_CLASSNAME}>
198+
<Command.Group heading={heading} className={GROUP_HEADING_CLASSNAME}>
100199
{items.map((tool) => (
101200
<MemoizedCommandItem
102201
key={tool.id}
@@ -117,14 +216,16 @@ export const TriggersGroup = memo(function TriggersGroup({
117216
items,
118217
onSelect,
119218
query,
219+
heading = 'Triggers',
120220
}: {
121221
items: SearchBlockItem[]
122222
onSelect: (trigger: SearchBlockItem) => void
123223
query?: string
224+
heading?: string
124225
}) {
125226
if (items.length === 0) return null
126227
return (
127-
<Command.Group heading='Triggers' className={GROUP_HEADING_CLASSNAME}>
228+
<Command.Group heading={heading} className={GROUP_HEADING_CLASSNAME}>
128229
{items.map((trigger) => (
129230
<MemoizedCommandItem
130231
key={trigger.id}

0 commit comments

Comments
 (0)