Something went wrong. Try again.
your personal website on atproto - mirror blento.app
Something went wrong. Try again.
7.0 kB · 218 lines
Svelte
at commit 2b9e4dfd
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219<script lang="ts"> import { AllCardDefinitions } from '$lib/cards'; import type { CardDefinition } from '$lib/cards/types'; import { Command, Dialog } from 'bits-ui'; import { isTyping } from '$lib/helpers/utils'; import { describeRepo, user } from '$lib/atproto';
let { open = $bindable(false), onselect, onlink, filter }: { open: boolean; onselect: (cardDef: CardDefinition) => void; onlink?: (url: string, cardDef: CardDefinition) => void; filter?: (cardDef: CardDefinition) => boolean; } = $props();
let collections = $state<string[]>([]); let fetchedForDid = $state<string | undefined>(undefined);
$effect(() => { if (open && user.did && fetchedForDid !== user.did) { const did = user.did; describeRepo({ did }).then((result) => { if (result?.collections) { collections = result.collections; } fetchedForDid = did; }); } });
let filteredCardDefs = $derived( AllCardDefinitions.filter( (d) => (!d.canAdd || d.canAdd({ collections })) && (!filter || filter(d)) ) );
let cardDefGroups = $derived([ 'Core', ...Array.from( new Set( filteredCardDefs .map((cardDef) => cardDef.groups) .flat() .filter((g) => g) ) ) .sort() .filter((g) => g !== 'Core') ]);
let searchValue = $state('');
let normalizedUrl = $derived.by(() => { if (!searchValue || searchValue.length < 8) return ''; try { const val = searchValue.trim(); const urlStr = val.startsWith('http') ? val : `https://${val}`; const url = new URL(urlStr); if (!url.hostname.includes('.')) return ''; return urlStr; } catch { return ''; } });
let urlMatchingCards = $derived.by(() => { if (!normalizedUrl) return []; return filteredCardDefs .filter((d) => d.onUrlHandler) .filter((d) => { try { const testItem = { cardData: {} }; return d.onUrlHandler!(normalizedUrl, testItem as any); } catch { return false; } }) .toSorted((a, b) => (b.urlHandlerPriority ?? 0) - (a.urlHandlerPriority ?? 0)); });
function selectUrl(cardDef: CardDefinition) { const url = normalizedUrl; open = false; searchValue = ''; onlink?.(url, cardDef); }
function commandFilter(value: string, search: string, keywords?: string[]): number { if (value.startsWith('url:')) return 1; const s = search.toLowerCase(); for (const t of [value, ...(keywords ?? [])]) { if (t.toLowerCase().includes(s)) return 1; } return 0; }
function handleKeydown(e: KeyboardEvent) { if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); open = !open; } if (e.key === '+' && !isTyping()) { e.preventDefault(); open = true; } }</script>
<svelte:document onkeydown={handleKeydown} />
<Dialog.Root bind:open> <Dialog.Portal> <Dialog.Overlay class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80" /> <Dialog.Content class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-36 left-[50%] z-50 w-full max-w-[94%] translate-x-[-50%] outline-hidden sm:max-w-lg md:w-full" > <Dialog.Title class="sr-only">Command Menu</Dialog.Title> <Dialog.Description class="sr-only"> This is the command menu. Use the arrow keys to navigate and press ⌘K to open the search bar. </Dialog.Description> <Command.Root filter={commandFilter} class="border-base-200 dark:border-base-800 mx-auto flex h-full w-full max-w-[90vw] flex-col overflow-hidden rounded-2xl border bg-white dark:bg-black" > <Command.Input class="focus-override placeholder:text-base-900/50 dark:placeholder:text-base-50/50 border-base-200 dark:border-base-800 bg-base-100 mx-1 mt-1 inline-flex truncate rounded-2xl rounded-tl-2xl px-4 text-sm transition-colors focus:ring-0 focus:outline-hidden dark:bg-black" placeholder="Search for a card or paste a link..." oninput={(e) => { searchValue = e.currentTarget.value; }} />
<Command.List class="focus:outline-accent-500/50 max-h-[50vh] overflow-x-hidden overflow-y-auto rounded-br-2xl rounded-bl-2xl bg-white px-2 pb-2 focus:border-0 dark:bg-black" > <Command.Viewport> <Command.Empty class="text-base-900 dark:text-base-100 flex w-full items-center justify-center pt-8 pb-6 text-sm" > No results found. </Command.Empty>
{#if urlMatchingCards.length > 0} <Command.Group> <Command.GroupHeading class="text-base-600 dark:text-base-400 px-3 pt-3 pb-2 text-xs" > Add from link </Command.GroupHeading> <Command.GroupItems> {#each urlMatchingCards as cardDef (cardDef.type)} <Command.Item value="url:{cardDef.type}" onSelect={() => { selectUrl(cardDef); }} class="rounded-button data-selected:bg-accent-500/10 flex h-10 cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm outline-hidden select-none" > {#if cardDef.icon} <div class="text-base-700 dark:text-base-300"> {@html cardDef.icon} </div> {/if} {cardDef.name} </Command.Item> {/each} </Command.GroupItems> </Command.Group> <Command.Separator class="bg-base-900/5 dark:bg-base-50/5 my-1 h-px w-full" /> {/if}
{#each cardDefGroups as group, index (group)} {#if group && filteredCardDefs.some((cardDef) => cardDef.groups?.includes(group))} <Command.Group> <Command.GroupHeading class="text-base-600 dark:text-base-400 px-3 pt-4 pb-2 text-xs" > {group} </Command.GroupHeading> <Command.GroupItems> {#each filteredCardDefs.filter( (cardDef) => cardDef.groups?.includes(group) ) as cardDef (cardDef.type)} <Command.Item onSelect={() => { open = false; searchValue = ''; onselect(cardDef); }} class="rounded-button data-selected:bg-accent-500/10 flex h-10 cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm outline-hidden select-none" keywords={[group, cardDef.type, ...(cardDef.keywords || [])]} > {#if cardDef.icon} <div class="text-base-700 dark:text-base-300"> {@html cardDef.icon} </div> {/if} {cardDef.name} </Command.Item> {/each} </Command.GroupItems> </Command.Group> {#if index < cardDefGroups.length - 1} <Command.Separator class="bg-base-900/5 dark:bg-base-50/5 my-1 h-px w-full" /> {/if} {/if} {/each} </Command.Viewport> </Command.List> </Command.Root> </Dialog.Content> </Dialog.Portal></Dialog.Root>