refactor: migrate views to MainLayout

This commit is contained in:
ByteAtATime 2025-07-06 10:08:04 -07:00
parent 3a11a73133
commit 8c395f0a60
No known key found for this signature in database
4 changed files with 465 additions and 427 deletions

View file

@ -15,6 +15,7 @@
import BaseList from './BaseList.svelte';
import KeyboardShortcut from './KeyboardShortcut.svelte';
import HeaderInput from './HeaderInput.svelte';
import MainLayout from './layout/MainLayout.svelte';
type Props = {
onBack: () => void;
@ -225,150 +226,158 @@
<svelte:window onkeydown={handleKeydown} />
<main class="bg-background text-foreground flex h-screen flex-col">
<header class="flex h-15 shrink-0 items-center">
<Button variant="ghost" size="icon" onclick={onBack}>
<ArrowLeft class="size-5" />
</Button>
<HeaderInput placeholder="Type to filter entries..." bind:value={searchText} autofocus />
<Select.Root bind:value={filter} type="single">
<Select.Trigger class="w-32">
{filter === 'all' ? 'All Types' : filter.charAt(0).toUpperCase() + filter.slice(1) + 's'}
</Select.Trigger>
<Select.Content>
<Select.Item value="all">All Types</Select.Item>
<Select.Item value="text">Text</Select.Item>
<Select.Item value="image">Images</Select.Item>
<Select.Item value="link">Links</Select.Item>
<Select.Item value="color">Colors</Select.Item>
</Select.Content>
</Select.Root>
</header>
<div class="grid grow grid-cols-[minmax(0,_1.5fr)_minmax(0,_2.5fr)] overflow-y-hidden">
<div class="flex-grow overflow-y-auto border-r" bind:this={listContainerEl}>
<BaseList
items={displayedItems}
bind:selectedIndex
onenter={(item) => handleCopy(item.data as ClipboardItem)}
isItemSelectable={(item) => item.itemType === 'item'}
>
{#snippet itemSnippet({ item, isSelected, onclick: itemOnClick })}
{#if item.itemType === 'header'}
<h3 class="text-muted-foreground px-4 pt-2.5 pb-1 text-xs font-semibold uppercase">
{item.data as string}
</h3>
{:else if item.itemType === 'item'}
{@const clipboardItem = item.data as ClipboardItem}
<button class="w-full" onclick={itemOnClick}>
<ListItemBase
icon={iconMap.get(clipboardItem.contentType) ?? 'question-mark-circle-16'}
title={clipboardItem.preview ?? clipboardItem.contentValue ?? ''}
{isSelected}
/>
</button>
{/if}
{/snippet}
</BaseList>
{#if isFetching && allItems.length > 0}
<div class="text-muted-foreground flex h-10 items-center justify-center">
<Loader2 class="size-4 animate-spin" />
</div>
{/if}
</div>
<div class="flex flex-col overflow-y-hidden">
{#if selectedItem}
<div class="relative flex-grow overflow-y-auto p-4">
{#if isContentLoading}
<div
class="bg-background/50 absolute inset-0 z-10 flex items-center justify-center backdrop-blur-sm"
>
<Loader2 class="text-muted-foreground size-6 animate-spin" />
</div>
{/if}
{#if selectedItemContent}
{#if selectedItem.contentType === 'color'}
<div class="flex flex-col items-center justify-center gap-4 py-8">
<div
class="size-24 rounded-full border"
style:background-color={selectedItemContent}
></div>
<p class="font-mono text-lg">{selectedItemContent}</p>
</div>
{:else if selectedItem.contentType === 'image'}
<img
src={convertFileSrc(selectedItemContent)}
alt="Clipboard content"
class="mx-auto max-h-full max-w-full rounded-lg object-contain"
/>
{:else if virtualizedLines.length > 0}
<div class="h-full font-mono text-sm">
<VList data={virtualizedLines}>
{#snippet children(item)}
<div class="whitespace-pre">{item}</div>
{/snippet}
</VList>
</div>
{:else if selectedItem.contentType === 'text'}
<div class="rounded bg-black/10 p-4 font-mono text-sm whitespace-pre-wrap">
{selectedItemContent}
<MainLayout>
{#snippet header()}
<header class="flex h-15 shrink-0 items-center">
<Button variant="ghost" size="icon" onclick={onBack}>
<ArrowLeft class="size-5" />
</Button>
<HeaderInput placeholder="Type to filter entries..." bind:value={searchText} autofocus />
<Select.Root bind:value={filter} type="single">
<Select.Trigger class="w-32">
{filter === 'all' ? 'All Types' : filter.charAt(0).toUpperCase() + filter.slice(1) + 's'}
</Select.Trigger>
<Select.Content>
<Select.Item value="all">All Types</Select.Item>
<Select.Item value="text">Text</Select.Item>
<Select.Item value="image">Images</Select.Item>
<Select.Item value="link">Links</Select.Item>
<Select.Item value="color">Colors</Select.Item>
</Select.Content>
</Select.Root>
</header>
{/snippet}
{#snippet content()}
<div class="grid grow grid-cols-[minmax(0,_1.5fr)_minmax(0,_2.5fr)] overflow-y-hidden">
<div class="flex-grow overflow-y-auto border-r" bind:this={listContainerEl}>
<BaseList
items={displayedItems}
bind:selectedIndex
onenter={(item) => handleCopy(item.data as ClipboardItem)}
isItemSelectable={(item) => item.itemType === 'item'}
>
{#snippet itemSnippet({ item, isSelected, onclick: itemOnClick })}
{#if item.itemType === 'header'}
<h3 class="text-muted-foreground px-4 pt-2.5 pb-1 text-xs font-semibold uppercase">
{item.data as string}
</h3>
{:else if item.itemType === 'item'}
{@const clipboardItem = item.data as ClipboardItem}
<button class="w-full" onclick={itemOnClick}>
<ListItemBase
icon={iconMap.get(clipboardItem.contentType) ?? 'question-mark-circle-16'}
title={clipboardItem.preview ?? clipboardItem.contentValue ?? ''}
{isSelected}
/>
</button>
{/if}
{/snippet}
</BaseList>
{#if isFetching && allItems.length > 0}
<div class="text-muted-foreground flex h-10 items-center justify-center">
<Loader2 class="size-4 animate-spin" />
</div>
{/if}
</div>
<div class="flex flex-col overflow-y-hidden">
{#if selectedItem}
<div class="relative flex-grow overflow-y-auto p-4">
{#if isContentLoading}
<div
class="bg-background/50 absolute inset-0 z-10 flex items-center justify-center backdrop-blur-sm"
>
<Loader2 class="text-muted-foreground size-6 animate-spin" />
</div>
{/if}
{/if}
</div>
<div class="border-t p-4">
<h3 class="text-muted-foreground mb-2 text-xs font-semibold uppercase">Information</h3>
<div class="flex flex-col gap-3 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">Application</span>
<span>{selectedItem.sourceAppName ?? 'Unknown'}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Content type</span>
<span class="capitalize">{selectedItem.contentType}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Times copied</span>
<span>{selectedItem.timesCopied}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Last copied</span>
<span>{formatDateTime(selectedItem.lastCopiedAt)}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">First copied</span>
<span>{formatDateTime(selectedItem.firstCopiedAt)}</span>
{#if selectedItemContent}
{#if selectedItem.contentType === 'color'}
<div class="flex flex-col items-center justify-center gap-4 py-8">
<div
class="size-24 rounded-full border"
style:background-color={selectedItemContent}
></div>
<p class="font-mono text-lg">{selectedItemContent}</p>
</div>
{:else if selectedItem.contentType === 'image'}
<img
src={convertFileSrc(selectedItemContent)}
alt="Clipboard content"
class="mx-auto max-h-full max-w-full rounded-lg object-contain"
/>
{:else if virtualizedLines.length > 0}
<div class="h-full font-mono text-sm">
<VList data={virtualizedLines}>
{#snippet children(item)}
<div class="whitespace-pre">{item}</div>
{/snippet}
</VList>
</div>
{:else if selectedItem.contentType === 'text'}
<div class="rounded bg-black/10 p-4 font-mono text-sm whitespace-pre-wrap">
{selectedItemContent}
</div>
{/if}
{/if}
</div>
<div class="border-t p-4">
<h3 class="text-muted-foreground mb-2 text-xs font-semibold uppercase">Information</h3>
<div class="flex flex-col gap-3 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">Application</span>
<span>{selectedItem.sourceAppName ?? 'Unknown'}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Content type</span>
<span class="capitalize">{selectedItem.contentType}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Times copied</span>
<span>{selectedItem.timesCopied}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Last copied</span>
<span>{formatDateTime(selectedItem.lastCopiedAt)}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">First copied</span>
<span>{formatDateTime(selectedItem.firstCopiedAt)}</span>
</div>
</div>
</div>
</div>
<ActionBar>
{#snippet primaryAction({ props })}
<Button {...props} onclick={() => handleCopy(selectedItem)}>
Copy to Clipboard <Kbd></Kbd>
</Button>
{/snippet}
{#snippet actions()}
<ActionMenu>
<DropdownMenu.Item onclick={() => handlePin(selectedItem)}>
<Pin class="mr-2 size-4" />
<span>{selectedItem.isPinned ? 'Unpin' : 'Pin'}</span>
<DropdownMenu.Shortcut>
<KeyboardShortcut shortcut={{ key: 'P', modifiers: ['cmd', 'shift'] }} />
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => handleDelete(selectedItem)}>
<Trash class="mr-2 size-4" />
<span>Delete</span>
<DropdownMenu.Shortcut>
<KeyboardShortcut shortcut={{ key: 'x', modifiers: ['ctrl'] }} />
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
</ActionMenu>
{/snippet}
</ActionBar>
{/if}
{/if}
</div>
</div>
</div>
</main>
{/snippet}
{#snippet footer()}
{#if selectedItem}
<ActionBar>
{#snippet primaryAction({ props })}
<Button {...props} onclick={() => handleCopy(selectedItem)}>
Copy to Clipboard <Kbd></Kbd>
</Button>
{/snippet}
{#snippet actions()}
<ActionMenu>
<DropdownMenu.Item onclick={() => handlePin(selectedItem)}>
<Pin class="mr-2 size-4" />
<span>{selectedItem.isPinned ? 'Unpin' : 'Pin'}</span>
<DropdownMenu.Shortcut>
<KeyboardShortcut shortcut={{ key: 'P', modifiers: ['cmd', 'shift'] }} />
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => handleDelete(selectedItem)}>
<Trash class="mr-2 size-4" />
<span>Delete</span>
<DropdownMenu.Shortcut>
<KeyboardShortcut shortcut={{ key: 'x', modifiers: ['ctrl'] }} />
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
</ActionMenu>
{/snippet}
</ActionBar>
{/if}
{/snippet}
</MainLayout>

View file

@ -14,6 +14,7 @@
import KeyboardShortcut from './KeyboardShortcut.svelte';
import { focusManager } from '$lib/focus.svelte';
import HeaderInput from './HeaderInput.svelte';
import MainLayout from './layout/MainLayout.svelte';
type Props = {
onBack: () => void;
@ -132,104 +133,112 @@
<svelte:window onkeydown={handleKeydown} />
<main class="bg-background text-foreground flex h-screen flex-col">
<header class="mb-2 flex h-15 shrink-0 items-center border-b">
<Button variant="ghost" size="icon" onclick={onBack}>
<ArrowLeft class="size-5" />
</Button>
<HeaderInput
placeholder="Search for files and folders..."
bind:value={searchText}
bind:ref={searchInputEl}
autofocus
/>
</header>
<div class="grid grow grid-cols-[minmax(0,_1.5fr)_minmax(0,_2.5fr)] overflow-y-hidden">
<div class="flex-grow overflow-y-auto border-r">
{#if isFetching && searchResults.length === 0}
<div class="text-muted-foreground flex h-full items-center justify-center">
<Loader2 class="size-6 animate-spin" />
</div>
{/if}
<BaseList
items={searchResults.map((item) => ({ ...item, id: item.path }))}
bind:selectedIndex
onenter={(item) => handleOpen(item)}
>
{#snippet itemSnippet({ item, isSelected, onclick })}
<button class="w-full text-left" {onclick}>
<ListItemBase
icon={item.fileType === 'directory' ? 'folder-16' : 'blank-document-16'}
title={item.name}
subtitle={item.parentPath}
{isSelected}
/>
</button>
<MainLayout>
{#snippet header()}
<header class="mb-2 flex h-15 shrink-0 items-center border-b">
<Button variant="ghost" size="icon" onclick={onBack}>
<ArrowLeft class="size-5" />
</Button>
<HeaderInput
placeholder="Search for files and folders..."
bind:value={searchText}
bind:ref={searchInputEl}
autofocus
/>
</header>
{/snippet}
{#snippet content()}
<div class="grid grow grid-cols-[minmax(0,_1.5fr)_minmax(0,_2.5fr)] overflow-y-hidden">
<div class="flex-grow overflow-y-auto border-r">
{#if isFetching && searchResults.length === 0}
<div class="text-muted-foreground flex h-full items-center justify-center">
<Loader2 class="size-6 animate-spin" />
</div>
{/if}
<BaseList
items={searchResults.map((item) => ({ ...item, id: item.path }))}
bind:selectedIndex
onenter={(item) => handleOpen(item)}
>
{#snippet itemSnippet({ item, isSelected, onclick })}
<button class="w-full text-left" {onclick}>
<ListItemBase
icon={item.fileType === 'directory' ? 'folder-16' : 'blank-document-16'}
title={item.name}
subtitle={item.parentPath}
{isSelected}
/>
</button>
{/snippet}
</BaseList>
</div>
<div class="flex flex-col overflow-y-hidden">
{#if selectedItem}
<div class="flex h-full flex-col items-center justify-center p-4">
<div class="mb-4">
{#if selectedItem.fileType === 'directory'}
<Folder class="size-24 text-gray-500" />
{:else}
<File class="size-24 text-gray-500" />
{/if}
</div>
<p class="text-xl font-semibold">{selectedItem.name}</p>
<p class="text-muted-foreground text-sm">{selectedItem.path}</p>
</div>
<div class="border-t p-4">
<h3 class="text-muted-foreground mb-2 text-xs font-semibold uppercase">Information</h3>
<div class="flex flex-col gap-3 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">Type</span>
<span class="capitalize">{selectedItem.fileType}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Last Modified</span>
<span>{formatDateTime(selectedItem.lastModified)}</span>
</div>
</div>
</div>
{/if}
</div>
</div>
{/snippet}
{#snippet footer()}
{#if selectedItem}
<ActionBar>
{#snippet primaryAction({ props })}
<Button {...props} onclick={() => handleOpen(selectedItem)}>
Open <KeyboardShortcut shortcut={{ key: 'enter', modifiers: [] }} />
</Button>
{/snippet}
</BaseList>
</div>
<div class="flex flex-col overflow-y-hidden">
{#if selectedItem}
<div class="flex h-full flex-col items-center justify-center p-4">
<div class="mb-4">
{#if selectedItem.fileType === 'directory'}
<Folder class="size-24 text-gray-500" />
{:else}
<File class="size-24 text-gray-500" />
{/if}
</div>
<p class="text-xl font-semibold">{selectedItem.name}</p>
<p class="text-muted-foreground text-sm">{selectedItem.path}</p>
</div>
<div class="border-t p-4">
<h3 class="text-muted-foreground mb-2 text-xs font-semibold uppercase">Information</h3>
<div class="flex flex-col gap-3 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">Type</span>
<span class="capitalize">{selectedItem.fileType}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Last Modified</span>
<span>{formatDateTime(selectedItem.lastModified)}</span>
</div>
</div>
</div>
<ActionBar>
{#snippet primaryAction({ props })}
<Button {...props} onclick={() => handleOpen(selectedItem)}>
Open <KeyboardShortcut shortcut={{ key: 'enter', modifiers: [] }} />
</Button>
{/snippet}
{#snippet actions()}
<ActionMenu>
<DropdownMenu.Item onclick={() => handleShow(selectedItem)}>
<Eye class="mr-2 size-4" />
<span>Show in File Manager</span>
<DropdownMenu.Shortcut>
<KeyboardShortcut shortcut={{ key: 'Enter', modifiers: ['cmd'] }} />
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => handleCopyPath(selectedItem)}>
<Copy class="mr-2 size-4" />
<span>Copy Path</span>
<DropdownMenu.Shortcut>
<KeyboardShortcut shortcut={{ key: 'c', modifiers: ['ctrl'] }} />
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-red-500" onclick={() => handleDelete(selectedItem)}>
<Trash class="mr-2 size-4" />
<span>Move to Trash</span>
<DropdownMenu.Shortcut>
<KeyboardShortcut shortcut={{ key: 'x', modifiers: ['ctrl'] }} />
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
</ActionMenu>
{/snippet}
</ActionBar>
{/if}
</div>
</div>
</main>
{#snippet actions()}
<ActionMenu>
<DropdownMenu.Item onclick={() => handleShow(selectedItem)}>
<Eye class="mr-2 size-4" />
<span>Show in File Manager</span>
<DropdownMenu.Shortcut>
<KeyboardShortcut shortcut={{ key: 'Enter', modifiers: ['cmd'] }} />
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => handleCopyPath(selectedItem)}>
<Copy class="mr-2 size-4" />
<span>Copy Path</span>
<DropdownMenu.Shortcut>
<KeyboardShortcut shortcut={{ key: 'c', modifiers: ['ctrl'] }} />
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-red-500" onclick={() => handleDelete(selectedItem)}>
<Trash class="mr-2 size-4" />
<span>Move to Trash</span>
<DropdownMenu.Shortcut>
<KeyboardShortcut shortcut={{ key: 'x', modifiers: ['ctrl'] }} />
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
</ActionMenu>
{/snippet}
</ActionBar>
{/if}
{/snippet}
</MainLayout>

View file

@ -11,6 +11,7 @@
import BaseList from './BaseList.svelte';
import KeyboardShortcut from './KeyboardShortcut.svelte';
import HeaderInput from './HeaderInput.svelte';
import MainLayout from './layout/MainLayout.svelte';
type Props = {
onBack: () => void;
@ -160,93 +161,101 @@
<svelte:window onkeydown={handleKeydown} />
<main class="bg-background text-foreground flex h-screen flex-col">
<header class="mb-2 flex h-15 shrink-0 items-center border-b">
<Button variant="ghost" size="icon" onclick={onBack}>
<ArrowLeft class="size-5" />
</Button>
<HeaderInput placeholder="Search snippets..." bind:value={searchText} autofocus />
</header>
<div class="grid grow grid-cols-[minmax(0,_1.5fr)_minmax(0,_2.5fr)] overflow-y-hidden">
<div class="flex-grow overflow-y-auto border-r">
{#if isFetching && snippets.length === 0}
<div class="text-muted-foreground flex h-full items-center justify-center">
<Loader2 class="size-6 animate-spin" />
</div>
{:else}
<BaseList
items={displayedItems}
bind:selectedIndex
onenter={(item) => handlePaste(item.data as Snippet)}
isItemSelectable={(item) => item.itemType === 'item'}
>
{#snippet itemSnippet({ item, isSelected, onclick: itemOnClick })}
{#if item.itemType === 'header'}
<h3 class="text-muted-foreground px-4 pt-2.5 pb-1 text-xs font-semibold uppercase">
{item.data as string}
</h3>
{:else if item.itemType === 'item'}
{@const snippetItem = item.data as Snippet}
<button class="w-full text-left" onclick={itemOnClick}>
<ListItemBase
icon="snippets-16"
title={snippetItem.name}
subtitle={snippetItem.keyword}
{isSelected}
/>
</button>
{/if}
{/snippet}
</BaseList>
{/if}
</div>
<div class="flex flex-col overflow-y-hidden">
{#if selectedItem}
<div class="relative flex-grow overflow-y-auto p-4">
<div class="font-mono text-sm whitespace-pre-wrap">{selectedItem.content}</div>
</div>
<MainLayout>
{#snippet header()}
<header class="mb-2 flex h-15 shrink-0 items-center border-b">
<Button variant="ghost" size="icon" onclick={onBack}>
<ArrowLeft class="size-5" />
</Button>
<HeaderInput placeholder="Search snippets..." bind:value={searchText} autofocus />
</header>
{/snippet}
{#snippet content()}
<div class="grid grow grid-cols-[minmax(0,_1.5fr)_minmax(0,_2.5fr)] overflow-y-hidden">
<div class="flex-grow overflow-y-auto border-r">
{#if isFetching && snippets.length === 0}
<div class="text-muted-foreground flex h-full items-center justify-center">
<Loader2 class="size-6 animate-spin" />
</div>
{:else}
<BaseList
items={displayedItems}
bind:selectedIndex
onenter={(item) => handlePaste(item.data as Snippet)}
isItemSelectable={(item) => item.itemType === 'item'}
>
{#snippet itemSnippet({ item, isSelected, onclick: itemOnClick })}
{#if item.itemType === 'header'}
<h3 class="text-muted-foreground px-4 pt-2.5 pb-1 text-xs font-semibold uppercase">
{item.data as string}
</h3>
{:else if item.itemType === 'item'}
{@const snippetItem = item.data as Snippet}
<button class="w-full text-left" onclick={itemOnClick}>
<ListItemBase
icon="snippets-16"
title={snippetItem.name}
subtitle={snippetItem.keyword}
{isSelected}
/>
</button>
{/if}
{/snippet}
</BaseList>
{/if}
</div>
<div class="flex flex-col overflow-y-hidden">
{#if selectedItem}
<div class="relative flex-grow overflow-y-auto p-4">
<div class="font-mono text-sm whitespace-pre-wrap">{selectedItem.content}</div>
</div>
<div class="border-t p-4">
<h3 class="text-muted-foreground mb-2 text-xs font-semibold uppercase">Information</h3>
<div class="flex flex-col gap-3 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">Name</span>
<span>{selectedItem.name}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Content type</span>
<span class="capitalize">Text</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Times used</span>
<span>{selectedItem.timesUsed}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Last used</span>
<span>{formatDateTime(selectedItem.lastUsedAt)}</span>
<div class="border-t p-4">
<h3 class="text-muted-foreground mb-2 text-xs font-semibold uppercase">Information</h3>
<div class="flex flex-col gap-3 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">Name</span>
<span>{selectedItem.name}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Content type</span>
<span class="capitalize">Text</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Times used</span>
<span>{selectedItem.timesUsed}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Last used</span>
<span>{formatDateTime(selectedItem.lastUsedAt)}</span>
</div>
</div>
</div>
</div>
<ActionBar>
{#snippet primaryAction({ props })}
<Button {...props} onclick={() => handlePaste(selectedItem)}>
Paste <Kbd></Kbd>
</Button>
{/snippet}
{#snippet actions()}
<ActionMenu>
<DropdownMenu.Item onclick={() => handleDelete(selectedItem)}>
<Trash class="mr-2 size-4" />
<span>Delete</span>
<DropdownMenu.Shortcut>
<KeyboardShortcut shortcut={{ key: 'x', modifiers: ['ctrl'] }} />
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
</ActionMenu>
{/snippet}
</ActionBar>
{/if}
{/if}
</div>
</div>
</div>
</main>
{/snippet}
{#snippet footer()}
{#if selectedItem}
<ActionBar>
{#snippet primaryAction({ props })}
<Button {...props} onclick={() => handlePaste(selectedItem)}>
Paste <Kbd></Kbd>
</Button>
{/snippet}
{#snippet actions()}
<ActionMenu>
<DropdownMenu.Item onclick={() => handleDelete(selectedItem)}>
<Trash class="mr-2 size-4" />
<span>Delete</span>
<DropdownMenu.Shortcut>
<KeyboardShortcut shortcut={{ key: 'x', modifiers: ['ctrl'] }} />
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
</ActionMenu>
{/snippet}
</ActionBar>
{/if}
{/snippet}
</MainLayout>

View file

@ -14,6 +14,7 @@
import { focusManager } from '$lib/focus.svelte';
import HeaderInput from '../HeaderInput.svelte';
import { Input } from '$lib/components/ui/input';
import MainLayout from '../layout/MainLayout.svelte';
type Props = {
plugins: PluginInfo[];
@ -133,110 +134,120 @@
}
</script>
<main class="bg-background text-foreground flex h-screen flex-col">
<header class="flex h-15 shrink-0 items-center border-b">
<div class="relative flex w-full items-center">
<HeaderInput
placeholder={selectedQuicklinkForArgument
? selectedQuicklinkForArgument.name
: 'Search for apps and commands...'}
bind:value={searchText}
bind:ref={searchInputEl}
onkeydown={handleKeyDown}
autofocus
/>
<MainLayout>
{#snippet header()}
<header class="flex h-15 shrink-0 items-center border-b">
<div class="relative flex w-full items-center">
<HeaderInput
placeholder={selectedQuicklinkForArgument
? selectedQuicklinkForArgument.name
: 'Search for apps and commands...'}
bind:value={searchText}
bind:ref={searchInputEl}
onkeydown={handleKeyDown}
autofocus
/>
{#if selectedQuicklinkForArgument}
<div class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center pl-4">
<span class="text-lg whitespace-pre text-transparent"
>{searchText || selectedQuicklinkForArgument.name}</span
{#if selectedQuicklinkForArgument}
<div
class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center pl-4"
>
<span class="w-2"></span>
<div class="pointer-events-auto">
<div class="inline-grid items-center">
<span
class="invisible col-start-1 row-start-1 px-3 text-base whitespace-pre md:text-sm"
aria-hidden="true"
>
{quicklinkArgument || 'Query'}
</span>
<span class="text-lg whitespace-pre text-transparent"
>{searchText || selectedQuicklinkForArgument.name}</span
>
<span class="w-2"></span>
<div class="pointer-events-auto">
<div class="inline-grid items-center">
<span
class="invisible col-start-1 row-start-1 px-3 text-base whitespace-pre md:text-sm"
aria-hidden="true"
>
{quicklinkArgument || 'Query'}
</span>
<Input
class="border-border col-start-1 row-start-1 h-7 w-full"
placeholder="Query"
bind:value={quicklinkArgument}
bind:ref={argumentInputEl}
onkeydown={handleArgumentKeydown}
/>
<Input
class="border-border col-start-1 row-start-1 h-7 w-full"
placeholder="Query"
bind:value={quicklinkArgument}
bind:ref={argumentInputEl}
onkeydown={handleArgumentKeydown}
/>
</div>
</div>
</div>
</div>
{/if}
</div>
</header>
<div class="grow overflow-y-auto">
<BaseList
items={displayItems.map((item) => ({ ...item, itemType: 'item' }))}
onenter={actions.handleEnter}
bind:selectedIndex
bind:listElement
>
{#snippet itemSnippet({ item, isSelected, onclick })}
{#if item.type === 'calculator'}
<Calculator
searchText={item.data.value}
mathResult={item.data.result}
mathResultType={item.data.resultType}
{isSelected}
onSelect={onclick}
/>
{:else if item.type === 'plugin'}
{@const assetsPath = path.dirname(item.data.pluginPath) + '/assets'}
<ListItemBase
title={item.data.title}
subtitle={item.data.pluginTitle}
icon={item.data.icon || 'app-window-16'}
{assetsPath}
{isSelected}
{onclick}
>
{#snippet accessories()}
<span class="text-muted-foreground ml-auto text-xs whitespace-nowrap"> Command </span>
{/snippet}
</ListItemBase>
{:else if item.type === 'app'}
<ListItemBase
title={item.data.name}
subtitle={item.data.comment}
icon={item.data.icon_path ?? 'app-window-16'}
{isSelected}
{onclick}
>
{#snippet accessories()}
<span class="text-muted-foreground ml-auto text-xs whitespace-nowrap">
Application
</span>
{/snippet}
</ListItemBase>
{:else if item.type === 'quicklink'}
<ListItemBase
title={item.data.name}
subtitle={item.data.link.replace(/\{argument\}/g, '...')}
icon={item.data.icon ?? 'link-16'}
{isSelected}
{onclick}
>
{#snippet accessories()}
<span class="text-muted-foreground ml-auto text-xs whitespace-nowrap">
Quicklink
</span>
{/snippet}
</ListItemBase>
{/if}
{/snippet}
</BaseList>
</div>
</div>
</header>
{/snippet}
<CommandPaletteActionBar {selectedItem} {actions} {setSearchText} />
</main>
{#snippet content()}
<div class="grow overflow-y-auto">
<BaseList
items={displayItems.map((item) => ({ ...item, itemType: 'item' }))}
onenter={actions.handleEnter}
bind:selectedIndex
bind:listElement
>
{#snippet itemSnippet({ item, isSelected, onclick })}
{#if item.type === 'calculator'}
<Calculator
searchText={item.data.value}
mathResult={item.data.result}
mathResultType={item.data.resultType}
{isSelected}
onSelect={onclick}
/>
{:else if item.type === 'plugin'}
{@const assetsPath = path.dirname(item.data.pluginPath) + '/assets'}
<ListItemBase
title={item.data.title}
subtitle={item.data.pluginTitle}
icon={item.data.icon || 'app-window-16'}
{assetsPath}
{isSelected}
{onclick}
>
{#snippet accessories()}
<span class="text-muted-foreground ml-auto text-xs whitespace-nowrap">
Command
</span>
{/snippet}
</ListItemBase>
{:else if item.type === 'app'}
<ListItemBase
title={item.data.name}
subtitle={item.data.comment}
icon={item.data.icon_path ?? 'app-window-16'}
{isSelected}
{onclick}
>
{#snippet accessories()}
<span class="text-muted-foreground ml-auto text-xs whitespace-nowrap">
Application
</span>
{/snippet}
</ListItemBase>
{:else if item.type === 'quicklink'}
<ListItemBase
title={item.data.name}
subtitle={item.data.link.replace(/\{argument\}/g, '...')}
icon={item.data.icon ?? 'link-16'}
{isSelected}
{onclick}
>
{#snippet accessories()}
<span class="text-muted-foreground ml-auto text-xs whitespace-nowrap">
Quicklink
</span>
{/snippet}
</ListItemBase>
{/if}
{/snippet}
</BaseList>
</div>
{/snippet}
{#snippet footer()}
<CommandPaletteActionBar {selectedItem} {actions} {setSearchText} />
{/snippet}
</MainLayout>