fix(frontend): improve search modal (#4088)

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): improve search modal style

* feat(frontend): fix content search height

* feat(frontend): fix overflow

* feat(frontend): revert unrelated changes
This commit is contained in:
Faton Ramadani
2024-07-17 14:29:41 +02:00
committed by GitHub
parent 030b616999
commit cdc7190d89
6 changed files with 366 additions and 287 deletions
@@ -1,16 +1,25 @@
<script lang="ts">
import { AppService, FlowService, ResourceService, ScriptService } from '$lib/gen'
import { enterpriseLicense, workspaceStore } from '$lib/stores'
import { Boxes, Code2, Edit, LayoutDashboard, Loader2 } from 'lucide-svelte'
import {
ArrowDown,
Boxes,
Code2,
Edit,
ExternalLink,
LayoutDashboard,
Loader2
} from 'lucide-svelte'
import SearchItems from './SearchItems.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import FlowIcon from './home/FlowIcon.svelte'
import { Alert, Button } from './common'
import YAML from 'yaml'
import { twMerge } from 'tailwind-merge'
import ContentSearchInnerItem from './ContentSearchInnerItem.svelte'
export let search: string = ''
export let classNameInner = 'max-h-[80vh]'
export async function open(nsearch?: string) {
await Promise.all([loadScripts(), loadResources(), loadApps(), loadFlows()])
@@ -143,10 +152,10 @@
bind:filteredItems={filteredAppItems}
/>
<div class="px-2 py-2 overflow-auto">
<div class="flex gap-2 flex-wrap">
<div class="flex justify-start">
<ToggleButtonGroup bind:selected={searchKind} class="h-10">
<div class="flex flex-col gap-2">
<div class="flex gap-2 flex-wrap sticky top-0 left-0 right-0 bg-surface">
<div class="p-2">
<ToggleButtonGroup bind:selected={searchKind} class="h-10 ">
<ToggleButton small light value="all" label={'All' + counts.all} />
<ToggleButton small light value="scripts" icon={Code2} label={'Scripts' + counts.scripts} />
<ToggleButton
@@ -174,60 +183,37 @@
/>
</ToggleButtonGroup>
</div>
<div class="relative text-tertiary grow min-w-[100px]">
<slot name="input-slot" />
<button type="submit" class="absolute right-0 top-0 mt-3 mr-4">
<svg
class="h-4 w-4 fill-current"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 56.966 56.966"
style="enable-background:new 0 0 56.966 56.966;"
xml:space="preserve"
width="512px"
height="512px"
>
<path
d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23 s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92 c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17 s-17-7.626-17-17S14.61,6,23.984,6z"
/>
</svg>
</button>
</div>
<div class="px-2">
<div class="text-xs text-secondary">
Searching among <div class="inline-flex">
{#if scripts}{scripts?.length}{:else}
<Loader2 size={10} class="animate-spin " />
{/if}
</div>
scripts,
<div class="inline-flex">
{#if resources}{resources?.length}{:else}
<Loader2 size={10} class="animate-spin " />
{/if}
</div>
resources,
<div class="inline-flex">
{#if flows}{flows?.length}{:else}
<Loader2 size={10} class="animate-spin " />
{/if}
</div>
flows,
<div class="inline-flex">
{#if apps}{apps?.length}{:else}
<Loader2 size={10} class="animate-spin " />
{/if}
</div>
apps
</div>
</div>
<div class="mt-1">
<div class="text-xs text-secondary"
>Searching among <div class="inline-flex"
>{#if scripts}{scripts?.length}{:else}
<Loader2 size={10} class="animate-spin " />
{/if}</div
>
scripts,
<div class="inline-flex"
>{#if resources}{resources?.length}{:else}
<Loader2 size={10} class="animate-spin " />
{/if}</div
>
resources,
<div class="inline-flex"
>{#if flows}{flows?.length}{:else}
<Loader2 size={10} class="animate-spin " />
{/if}</div
>
flows,
<div class="inline-flex"
>{#if apps}{apps?.length}{:else}
<Loader2 size={10} class="animate-spin " />
{/if}</div
>
apps</div
>
</div>
<div class="mt-1 overflow-auto {classNameInner}">
<div class={twMerge('p-2')}>
{#if !$enterpriseLicense}
<div class="py-1" />
@@ -242,126 +228,171 @@
<div class="flex flex-col gap-4">
{#if (searchKind == 'all' || searchKind == 'scripts') && filteredScriptItems?.length > 0}
{#each filteredScriptItems.slice(0, showNbScripts) ?? [] as item}
<div>
<div class="text-sm font-semibold"
><a href="/scripts/get/{item.path}">Script: {item.path}</a></div
>
<div class="flex gap-2 justify-between">
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full max-w-2xl"
><code>{@html item.marked}</code></pre
<ContentSearchInnerItem
title={`Script: ${item.path}`}
href={`/scripts/get/${item.path}`}
>
<svelte:fragment slot="actions">
<Button
href={`/scripts/get/${item.path}`}
color="light"
size="xs"
startIcon={{ icon: ExternalLink }}
>
<div>
<div class="flex gap-2">
<Button
on:click|once={() => {
window.open(`/scripts/edit/${item.path}?no_draft=true`, '_blank')?.focus()
}}
color="light"
size="sm"
startIcon={{ icon: Edit }}
>
Edit
</Button>
</div>
</div>
</div>
</div>
Open
</Button>
<Button
href={`/scripts/edit/${item.path}?no_draft=true`}
target="_blank"
color="light"
size="xs"
startIcon={{ icon: Edit }}
>
Edit
</Button>
</svelte:fragment>
<pre class="text-xs border rounded-md p-2 overflow-auto max-h-40 w-full"
><code>{@html item.marked}</code>
</pre>
</ContentSearchInnerItem>
{/each}
{#if filteredScriptItems.length > showNbScripts}
<a
href="#"
class="text-center font-semibold cursor-pointer pb-40"
<Button
color="light"
size="xs"
on:click={() => {
showNbScripts += 30
}}
startIcon={{
icon: ArrowDown
}}
>
({showNbScripts} of {filteredScriptItems.length}) Show more scripts
</a>
Show more scripts ({showNbScripts} of {filteredScriptItems.length})
</Button>
{/if}
{/if}
{#if (searchKind == 'all' || searchKind == 'resources') && filteredResourceItems?.length > 0}
{#each filteredResourceItems.slice(0, showNbResources) ?? [] as item}
<div>
<div class="text-sm font-semibold">Resource: {item.path}</div>
<div class="flex gap-2 justify-between">
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full max-w-2xl"
><code>{@html item.marked}</code></pre
<ContentSearchInnerItem
title={`Resource: ${item.path}`}
href={`/resources#${item.path}`}
>
<svelte:fragment slot="actions">
<Button
href={`/resources#${item.path}`}
color="light"
target="_blank"
size="xs"
startIcon={{ icon: Edit }}
>
</div>
</div>
Edit
</Button>
</svelte:fragment>
<pre class="text-xs border rounded-md p-2 overflow-auto max-h-40 w-full"
><code>{@html item.marked}</code></pre
>
</ContentSearchInnerItem>
{/each}
{#if filteredResourceItems.length > showNbResources}
<a
href="#"
class="text-center font-semibold cursor-pointer pb-40"
<Button
color="light"
size="xs"
on:click={() => {
showNbResources += 30
}}
startIcon={{
icon: ArrowDown
}}
>
({showNbResources} of {filteredResourceItems.length}) Show more resources
</a>
Show more resources ({showNbResources} of {filteredResourceItems.length})
</Button>
{/if}
{/if}
{#if (searchKind == 'all' || searchKind == 'flows') && filteredFlowItems?.length > 0}
{#each filteredFlowItems.slice(0, showNbFlows) ?? [] as item}
<div>
<div class="text-sm font-semibold"
><a href="/flows/get/{item.path}">Flow: {item.path}</a></div
>
<div class="flex gap-2 justify-between">
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full max-w-2xl"
><code>{@html item.marked}</code></pre
<ContentSearchInnerItem title={`Flow: ${item.path}`} href={`/flows/get/${item.path}`}>
<svelte:fragment slot="actions">
<Button
href={`/flows/get/${item.path}`}
color="light"
size="xs"
startIcon={{ icon: ExternalLink }}
>
<div>
<div class="flex gap-2">
<Button
on:click|once={() => {
window.open(`/flows/edit/${item.path}?no_draft=true`, '_blank')?.focus()
}}
color="light"
size="sm"
startIcon={{ icon: Edit }}>Edit</Button
>
</div>
</div>
</div>
</div>
Open
</Button>
<Button
href={`/flows/edit/${item.path}?no_draft=true`}
color="light"
target="_blank"
size="xs"
startIcon={{ icon: Edit }}
>
Edit
</Button>
</svelte:fragment>
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full"
><code>{@html item.marked}</code></pre
>
</ContentSearchInnerItem>
{/each}
{#if filteredFlowItems.length > showNbFlows}
<a
href="#"
class="text-center font-semibold cursor-pointer pb-40"
<Button
color="light"
size="xs"
on:click={() => {
showNbScripts += 30
showNbFlows += 30
}}
startIcon={{
icon: ArrowDown
}}
>
({showNbFlows} of {filteredFlowItems.length}) Show more flows
</a>
Show more flows ({showNbFlows} of {filteredFlowItems.length})
</Button>
{/if}
{/if}
{#if (searchKind == 'all' || searchKind == 'apps') && filteredAppItems?.length > 0}
{#each filteredAppItems.slice(0, showNbApps) ?? [] as item}
<div>
<div class="text-sm font-semibold"
><a href="/apps/get/{item.path}">App: {item.path}</a></div
>
<div class="flex gap-2 justify-between">
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full max-w-2xl"
><code>{@html item.marked}</code></pre
<ContentSearchInnerItem title={`App: ${item.path}`} href={`/apps/get/${item.path}`}>
<svelte:fragment slot="actions">
<Button
href={`/apps/get/${item.path}`}
color="light"
size="xs"
startIcon={{ icon: ExternalLink }}
>
</div>
</div>
Open
</Button>
<Button
href={`/apps/edit/${item.path}?no_draft=true`}
color="light"
target="_blank"
size="xs"
startIcon={{ icon: Edit }}
>
Edit
</Button>
</svelte:fragment>
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full"
><code>{@html item.marked}</code></pre
>
</ContentSearchInnerItem>
{/each}
{#if filteredAppItems.length > showNbApps}
<a
href="#"
class="text-center font-semibold cursor-pointer pb-40"
<Button
color="light"
size="xs"
on:click={() => {
showNbApps += 30
}}
startIcon={{
icon: ArrowDown
}}
>
({showNbApps} of {filteredAppItems.length}) Show more apps
</a>
Show more apps ({showNbApps} of {filteredAppItems.length})
</Button>
{/if}
{/if}
</div>
@@ -0,0 +1,17 @@
<script lang="ts">
export let title: string
export let href: string
</script>
<div class="flex flex-col gap-2">
<div class="flex flex-row items-center justify-between">
<a class="text-sm text-primary truncate" {href}>
{title}
</a>
<div class="flex flex-row gap-2 items-center">
<slot name="actions" />
</div>
</div>
<slot />
</div>
+22 -1
View File
@@ -5,7 +5,8 @@
type Input,
type RunnableType,
type CreateInput,
type Job
type Job,
JobService
} from '$lib/gen/index.js'
import { userStore, workspaceStore } from '$lib/stores.js'
import { classNames, displayDate, displayDateOnly, sendUserToast } from '$lib/utils.js'
@@ -328,6 +329,26 @@
`w-full flex items-center justify-between gap-4 py-2 px-4 text-left border rounded-sm hover:bg-surface-hover transition-a`,
'border-orange-400'
)}
on:click={async () => {
if (!$workspaceStore) {
return
}
const args = await JobService.getJobArgs({
workspace: $workspaceStore,
id: i.id
})
selectedInput = {
id: i.id,
name: 'Running job: ' + i.id,
created_at: i.created_at ?? '',
created_by: i.created_by ?? '',
is_public: true
}
selectArgs(args)
}}
>
<div
class="w-full h-full items-center text-xs font-normal grid grid-cols-8 gap-4 min-w-0"
@@ -7,6 +7,7 @@
export let small = false
$: tooBig = code && code?.length > 1000000
function parseJson() {
try {
if (code == '') {
@@ -77,7 +77,7 @@
},
{
search_id: 'switchto:log-search',
label: 'Search windmill logs',
label: 'Search Windmill logs',
action: () => switchMode('logs'),
shortcutKey: LOGS_PREFIX,
icon: Search
@@ -332,6 +332,7 @@
onDestroy(() => {
window.removeEventListener('keydown', handleKeydown)
})
$: searchTerm, handleSearch()
function placeholderFromPrefix(text: string): string {
@@ -436,87 +437,101 @@
await handleSearch()
}
async function openModal() {
open = !open
async function focusTextInput() {
await tick()
textInput?.focus()
textInput?.select()
if (open) {
if (combinedItems == undefined) {
combinedItems = await fetchCombinedItems()
handleSearch()
}
selectedItem = selectItem(0)
textInput.focus()
textInput.select()
}
}
async function openModal() {
open = !open
focusTextInput()
}
let height: number | undefined = undefined
</script>
{#if open}
<Portal>
<div
class={twMerge(
`fixed top-0 bottom-0 left-0 right-0 transition-all duration-50`,
`fixed top-0 bottom-0 left-0 right-0 transition-all duration-50 flex items-center justify-center`,
' bg-black bg-opacity-40',
'z-[1100]'
)}
>
<div
class={'max-w-4xl lg:mx-auto mx-10 mt-40 bg-surface rounded-lg relative'}
class={'max-w-4xl w-full h-[80vh] bg-surface rounded-lg relative'}
use:clickOutside={false}
on:click_outside={() => {
open = false
}}
>
<div class="py-2 items-center">
<div class="px-4 flex flex-row gap-1 items-center pb-1 mb-2 border-b">
<Search />
<div class="relative inline-block w-full">
<input
id="quickSearchInput"
bind:this={textInput}
type="text"
class="quick-search-input"
bind:value={searchTerm}
/>
<label
for="quickSearchInput"
class="absolute top-1/2 left-2 transform -translate-y-1/2 pointer-events-none text-gray-400 transition-all duration-200 whitespace-pre"
>{placeholderFromPrefix(searchTerm)}</label
>
</div>
<div class="px-4 py-2 flex flex-row gap-1 items-center border-b" bind:clientHeight={height}>
<Search size="16" />
<div class="relative inline-block w-full">
<input
id="quickSearchInput"
bind:this={textInput}
type="text"
class="quick-search-input"
bind:value={searchTerm}
/>
<label
for="quickSearchInput"
class="absolute top-1/2 left-2 transform -translate-y-1/2 pointer-events-none text-gray-400 transition-all duration-200 whitespace-pre"
>{placeholderFromPrefix(searchTerm)}</label
>
</div>
<div class="px-4 overflow-scroll max-h-[30rem]">
{#if tab === 'default' || tab === 'switch-mode'}
{#each (itemMap[tab] ?? []).filter((e) => defaultMenuItems.includes(e)) as el}
<QuickMenuItem
on:select={el?.action}
on:hover={() => (selectedItem = el)}
id={el?.search_id}
hovered={el?.search_id === selectedItem?.search_id}
label={el?.label}
icon={el?.icon}
shortcutKey={el?.shortcutKey}
/>
{/each}
</div>
<div class="overflow-y-auto relative" style={`height: calc(100% - ${height + 1}px);`}>
{#if tab === 'default' || tab === 'switch-mode'}
{@const items = (itemMap[tab] ?? []).filter((e) => defaultMenuItems.includes(e))}
{#if items.length > 0}
<div class="p-2 border-b">
{#each items as el}
<QuickMenuItem
on:select={el?.action}
on:hover={() => (selectedItem = el)}
id={el?.search_id}
hovered={el?.search_id === selectedItem?.search_id}
label={el?.label}
icon={el?.icon}
shortcutKey={el?.shortcutKey}
/>
{/each}
</div>
{/if}
{#if tab === 'default'}
{/if}
{#if tab === 'default'}
<div class="p-2">
{#if (itemMap[tab] ?? []).filter((e) => (combinedItems ?? []).includes(e)).length > 0}
<div class="mt-2 pt-2 pb-1 px-1 text-xs text-sm font-bold border-t text-tertiary"
>Flows/Scripts/Apps</div
>
<div class="py-2 px-1 text-xs font-semibold text-tertiary">
Flows/Scripts/Apps
</div>
{#each (itemMap[tab] ?? []).filter((e) => (combinedItems ?? []).includes(e)) as el}
<QuickMenuItem
on:select={() => gotoWindmillItemPage(el)}
on:hover={() => (selectedItem = el)}
id={el?.search_id}
hovered={el?.path === selectedItem?.path}
label={(el.summary ? `${el.summary} - ` : '') +
el.path +
(el.starred ? ' ★' : '')}
icon={iconForWindmillItem(el.type)}
/>
{/each}
{/if}
{#each (itemMap[tab] ?? []).filter((e) => (combinedItems ?? []).includes(e)) as el}
<QuickMenuItem
on:select={() => gotoWindmillItemPage(el)}
on:hover={() => (selectedItem = el)}
id={el?.search_id}
hovered={el?.path === selectedItem?.path}
label={(el.summary ? `${el.summary} - ` : '') +
el.path +
(el.starred ? ' ★' : '')}
icon={iconForWindmillItem(el.type)}
/>
{/each}
{#if (itemMap[tab] ?? []).length === 0}
<div class="flex w-full justify-center items-center h-48">
<div class="text-tertiary text-center">
@@ -525,65 +540,55 @@
</div>
</div>
{/if}
{:else if tab === 'content'}
<ContentSearchInner
classNameInner="max-h-[20rem]"
search={removePrefix(searchTerm, '#')}
bind:this={contentSearch}
/>
{:else if tab === 'logs'}
<div class="p-2">
<Alert title="Service log search is coming soon" type="info">
Full text search on windmill's service logs is coming soon
</Alert>
</div>
{:else if tab === 'runs'}
<div class="flex h-96">
{#if loadingCompletedRuns}
<div class="flex w-full justify-center items-center h-48">
<div class="text-tertiary text-center">
<Loader2 size={34} class="animate-spin" />
</div>
</div>
{:else if tab === 'content'}
<ContentSearchInner search={removePrefix(searchTerm, '#')} bind:this={contentSearch} />
{:else if tab === 'logs'}
<div class="p-2">
<Alert title="Service log search is coming soon" type="info">
Full text search on windmill's service logs is coming soon
</Alert>
</div>
{:else if tab === 'runs'}
<div class="flex h-full p-2">
{#if loadingCompletedRuns}
<div class="flex w-full justify-center items-center h-48">
<div class="text-tertiary text-center">
<Loader2 size={34} class="animate-spin" />
</div>
{:else if itemMap['runs'] && itemMap['runs'].length > 0}
<div class="w-5/12 overflow-scroll">
{#each itemMap['runs'] ?? [] as r}
<QuickMenuItem
on:hover={() => {
selectedItem = r
selectedWorkspace = r?.document.workspace_id[0]
}}
on:select={() => {
open = false
goto(`/run/${r?.document.id[0]}`)
}}
id={r?.document.id[0]}
hovered={selectedItem && r?.document.id[0] === selectedItem?.document.id[0]}
icon={r?.icon}
>
<svelte:fragment slot="itemReplacement">
<button
class={twMerge(
`w-full flex items-center justify-between gap-1 py-2 px-2 text-left border rounded-sm transition-a`,
r?.document.id === selectedItem?.document?.id
? 'bg-surface-hover'
: ''
)}
on:click={() => {}}
>
<div
class="w-full h-full items-center text-xs font-normal grid grid-cols-10 gap-0 min-w-0"
>
<div class="col-span-1">
<div
class="rounded-full w-2 h-2 {r?.document.success[0]
? 'bg-green-400'
: 'bg-red-400'}"
/>
</div>
<div class="col-span-5">
{r?.document.script_path}
</div>
</div>
{:else if itemMap['runs'] && itemMap['runs'].length > 0}
<div class="w-5/12 overflow-y-auto border rounded-md divide-y">
{#each itemMap['runs'] ?? [] as r}
<QuickMenuItem
on:hover={() => {
selectedItem = r
selectedWorkspace = r?.document.workspace_id[0]
}}
on:select={() => {
open = false
goto(`/run/${r?.document.id[0]}`)
}}
id={r?.document.id[0]}
hovered={selectedItem && r?.document.id[0] === selectedItem?.document.id[0]}
icon={r?.icon}
containerClass="!rounded-none"
>
<svelte:fragment slot="itemReplacement">
<div
class={twMerge(
`w-full flex flex-row items-center gap-4 p-2 transition-all`,
r?.document.id === selectedItem?.document?.id ? 'bg-surface-hover' : ''
)}
>
<div
class="rounded-full w-2 h-2 {r?.document.success[0]
? 'bg-green-400'
: 'bg-red-400'}"
/>
<div class="flex flex-col gap-2">
<div class="text-xs"> {r?.document.script_path} </div>
<div class="flex flex-row gap-2">
<div
class="whitespace-nowrap col-span-2 !text-tertiary !text-2xs overflow-hidden text-ellipsis flex-shrink text-center"
>
@@ -595,43 +600,39 @@
<TimeAgo date={r?.document.created_at[0] ?? ''} />
</div>
</div>
</button>
</svelte:fragment>
</QuickMenuItem>
{/each}
</div>
{#if selectedItem === undefined}
select a result to preview
{:else}
<div class="w-7/12 overflow-y-scroll">
<JobPreview
id={selectedItem?.document?.id[0]}
workspace={selectedWorkspace}
/>
</div>
{/if}
</div>
</div>
</svelte:fragment>
</QuickMenuItem>
{/each}
</div>
{#if selectedItem === undefined}
select a result to preview
{:else}
<div class="flex w-full justify-center items-center h-96">
<div class="text-tertiary text-center">
<div class="text-2xl font-bold">No runs found</div>
<div class="text-sm">There were no completed runs that match your query</div>
<div class="text-sm"
>Note that new runs might take a while to become searchable (by default
~5min)</div
>
{#if !$enterpriseLicense}
<div class="py-6" />
<Alert title="This is an EE feature" type="warning">
Full-text search on jobs is only available on EE.
</Alert>
{/if}
</div>
<div class="w-7/12 overflow-y-scroll">
<JobPreview id={selectedItem?.document?.id[0]} workspace={selectedWorkspace} />
</div>
{/if}
</div>
{/if}
</div>
{:else}
<div class="flex w-full justify-center items-center">
<div class="text-tertiary text-center">
<div class="text-2xl font-bold">No runs found</div>
<div class="text-sm">There were no completed runs that match your query</div>
<div class="text-sm"
>Note that new runs might take a while to become searchable (by default ~5min)</div
>
{#if !$enterpriseLicense}
<div class="py-6" />
<Alert title="This is an EE feature" type="warning">
Full-text search on jobs is only available on EE.
</Alert>
{/if}
</div>
</div>
{/if}
</div>
{/if}
</div>
</div>
</div>
@@ -8,6 +8,7 @@
export let label: string = ''
export let icon: any = undefined
export let shortcutKey: string | undefined = undefined
export let containerClass: string | undefined = undefined
const dispatch = createEventDispatcher()
@@ -43,19 +44,26 @@
}
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
{id}
on:click|stopPropagation={runAction}
on:mouseenter={() => dispatch('hover')}
class={`rounded-md w-full ${hovered ? 'bg-surface-hover' : ''}`}
class={twMerge(
`rounded-md w-full transition-all cursor-pointer hover:bg-surface-hover ${
hovered ? 'bg-surface-hover' : ''
}`,
containerClass
)}
>
{#if $$slots.itemReplacement}
<slot name="itemReplacement" />
{:else}
<div class="flex flex-row gap-2 items-center px-1 py-0.5 rounded-md pr-6 font-light">
<div class="flex flex-row gap-2 items-center px-2 py-1.5 rounded-md pr-6 text-sm">
<div class="w-4">
{#if icon}
<svelte:component this={icon} size={14} />
<svelte:component this={icon} size={16} />
{:else if shortcutKey != undefined}
<div class="font-bold flex items-center justify-center w-full">
<span