your personal website on atproto - mirror blento.app

improve link adding

Florian 604ac7af 0faab9c0

+361 -205
+16 -5
src/lib/cards/BaseCard/BaseEditingCard.svelte
··· 136 136 if (!cardDef) return false; 137 137 138 138 if (isMobile()) { 139 - 140 - return w >= minW && w*2 <= maxW && h >= minH && h*2 <= maxH; 139 + return w >= minW && w * 2 <= maxW && h >= minH && h * 2 <= maxH; 141 140 } 142 141 143 142 return w >= minW && w <= maxW && h >= minH && h <= maxH; ··· 154 153 let settingsPopoverOpen = $state(false); 155 154 </script> 156 155 157 - <BaseCard {item} isEditing={true} bind:ref showOutline={isResizing} class="starting:scale-0 scale-100 starting:opacity-0 opacity-100" {...rest} > 156 + <BaseCard 157 + {item} 158 + isEditing={true} 159 + bind:ref 160 + showOutline={isResizing} 161 + class="scale-100 opacity-100 starting:scale-0 starting:opacity-0" 162 + {...rest} 163 + > 158 164 {@render children?.()} 159 165 160 166 {#snippet controls()} ··· 283 289 {/if} 284 290 285 291 {#if cardDef.settingsComponent} 286 - <Popover bind:open={settingsPopoverOpen}> 292 + <Popover bind:open={settingsPopoverOpen} class="bg-base-50 dark:bg-base-900"> 287 293 {#snippet child({ props })} 288 294 <button {...props} class="hover:bg-accent-500/10 cursor-pointer rounded-xl p-2"> 289 295 <svg ··· 307 313 </svg> 308 314 </button> 309 315 {/snippet} 310 - <cardDef.settingsComponent bind:item /> 316 + <cardDef.settingsComponent 317 + bind:item 318 + onclose={() => { 319 + settingsPopoverOpen = false; 320 + }} 321 + /> 311 322 </Popover> 312 323 {/if} 313 324 </div>
+2 -1
src/lib/cards/BigSocialCard/index.ts
··· 32 32 item.cardData.href = url; 33 33 34 34 return item; 35 - } 35 + }, 36 + urlHandlerPriority: 1 36 37 } as CardDefinition & { type: 'bigsocial' }; 37 38 38 39 import {
-59
src/lib/cards/LinkCard/CreateLinkCardModal.svelte
··· 1 - <script lang="ts"> 2 - import { Alert, Button, Input, Modal, Subheading } from '@foxui/core'; 3 - import type { CreationModalComponentProps } from '../types'; 4 - 5 - let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props(); 6 - 7 - let isFetchingMetadata = $state(false); 8 - 9 - let errorMessage = $state(''); 10 - 11 - async function fetchMetadata() { 12 - errorMessage = ''; 13 - try { 14 - item.cardData.domain = new URL(item.cardData.href).hostname; 15 - } catch (error) { 16 - errorMessage = 'Invalid URL!'; 17 - return false; 18 - } 19 - isFetchingMetadata = true; 20 - 21 - try { 22 - const response = await fetch('/api/links?link=' + encodeURIComponent(item.cardData.href)); 23 - if (response.ok) { 24 - const data = await response.json(); 25 - item.cardData.description = data.description || ''; 26 - item.cardData.title = data.title || ''; 27 - item.cardData.image = data.images?.[0] || ''; 28 - item.cardData.favicon = data.favicons?.[0] || undefined; 29 - } else { 30 - throw new Error(); 31 - } 32 - } catch (error) { 33 - errorMessage = "Couldn't fetch metadata for this link!"; 34 - return false; 35 - } finally { 36 - isFetchingMetadata = false; 37 - } 38 - return true; 39 - } 40 - </script> 41 - 42 - <Modal open={true} closeButton={false}> 43 - <Subheading>Enter a link</Subheading> 44 - <Input bind:value={item.cardData.href} /> 45 - 46 - {#if errorMessage} 47 - <Alert type="error" title="Failed to create link card"><span>{errorMessage}</span></Alert> 48 - {/if} 49 - 50 - <div class="mt-4 flex justify-end gap-2"> 51 - <Button onclick={oncancel} variant="ghost">Cancel</Button> 52 - <Button 53 - disabled={isFetchingMetadata} 54 - onclick={async () => { 55 - if (await fetchMetadata()) oncreate(); 56 - }}>{isFetchingMetadata ? 'Creating...' : 'Create'}</Button 57 - > 58 - </div> 59 - </Modal>
+99 -38
src/lib/cards/LinkCard/EditingLinkCard.svelte
··· 3 3 import { getIsMobile } from '$lib/website/context'; 4 4 import type { ContentComponentProps } from '../types'; 5 5 import PlainTextEditor from '../utils/PlainTextEditor.svelte'; 6 + import { onMount } from 'svelte'; 6 7 7 8 let { item = $bindable() }: ContentComponentProps = $props(); 8 9 9 10 let isMobile = getIsMobile(); 10 11 11 12 let faviconHasError = $state(false); 13 + let isFetchingMetadata = $state(false); 14 + 15 + let hasFetched = $derived(item.cardData.hasFetched !== false); 16 + 17 + async function fetchMetadata() { 18 + let domain: string; 19 + try { 20 + domain = new URL(item.cardData.href).hostname; 21 + } catch (error) { 22 + return; 23 + } 24 + item.cardData.domain = domain; 25 + faviconHasError = false; 26 + 27 + try { 28 + const response = await fetch('/api/links?link=' + encodeURIComponent(item.cardData.href)); 29 + if (!response.ok) { 30 + throw new Error(); 31 + } 32 + const data = await response.json(); 33 + item.cardData.description = data.description || ''; 34 + item.cardData.title = data.title || ''; 35 + item.cardData.image = data.images?.[0] || ''; 36 + item.cardData.favicon = data.favicons?.[0] || undefined; 37 + } catch (error) { 38 + return; 39 + } 40 + } 41 + 42 + $effect(() => { 43 + if (hasFetched !== false || isFetchingMetadata) { 44 + return; 45 + } 46 + 47 + isFetchingMetadata = true; 48 + 49 + fetchMetadata().then(() => { 50 + item.cardData.hasFetched = true; 51 + isFetchingMetadata = false; 52 + }); 53 + }); 12 54 </script> 13 55 14 - <div class="flex h-full flex-col justify-between p-4"> 15 - <div> 16 - {#if item.cardData.favicon} 17 - <div 18 - class="bg-base-100 border-base-300 dark:border-base-800 dark:bg-base-900 mb-2 inline-flex size-8 items-center justify-center rounded-xl border shadow-sm" 19 - > 20 - {#if !faviconHasError} 21 - <img 22 - class="size-6 rounded-lg object-cover" 23 - onerror={() => (faviconHasError = true)} 24 - src={item.cardData.favicon} 25 - alt="" 56 + <div class="relative flex h-full flex-col justify-between p-4"> 57 + <div 58 + class={[ 59 + 'accent:bg-accent-500/50 absolute inset-0 z-20 bg-white/50 dark:bg-black/50', 60 + !hasFetched ? 'animate-pulse' : 'hidden' 61 + ]} 62 + ></div> 63 + 64 + <div class={isFetchingMetadata ? 'pointer-events-none' : ''}> 65 + <div 66 + class="bg-base-100 border-base-300 accent:bg-accent-100/50 accent:border-accent-200 dark:border-base-800 dark:bg-base-900 mb-2 inline-flex size-8 items-center justify-center rounded-xl border" 67 + > 68 + {#if hasFetched && item.cardData.favicon && !faviconHasError} 69 + <img 70 + class="size-6 rounded-lg object-cover" 71 + onerror={() => (faviconHasError = true)} 72 + src={item.cardData.favicon} 73 + alt="" 74 + /> 75 + {:else} 76 + <svg 77 + xmlns="http://www.w3.org/2000/svg" 78 + fill="none" 79 + viewBox="0 0 24 24" 80 + stroke-width="1.5" 81 + stroke="currentColor" 82 + class="size-4" 83 + > 84 + <path 85 + stroke-linecap="round" 86 + stroke-linejoin="round" 87 + d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 1 1.242 7.244" 26 88 /> 27 - {:else} 28 - <svg 29 - xmlns="http://www.w3.org/2000/svg" 30 - fill="none" 31 - viewBox="0 0 24 24" 32 - stroke-width="1.5" 33 - stroke="currentColor" 34 - class="size-4" 35 - > 36 - <path 37 - stroke-linecap="round" 38 - stroke-linejoin="round" 39 - d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" 40 - /> 41 - </svg> 42 - {/if} 43 - </div> 44 - {/if} 89 + </svg> 90 + {/if} 91 + </div> 45 92 46 93 <div 47 - class="hover:bg-base-200/70 dark:hover:bg-base-800/70 accent:hover:bg-accent-400 -m-1 rounded-md p-1 transition-colors duration-200" 94 + class={[ 95 + '-m-1 rounded-md p-1 transition-colors duration-200', 96 + hasFetched 97 + ? 'hover:bg-base-200/70 dark:hover:bg-base-800/70 accent:hover:bg-accent-200/30' 98 + : '' 99 + ]} 48 100 > 49 - <PlainTextEditor 50 - class="text-base-900 dark:text-base-50 text-lg font-bold" 51 - key="title" 52 - bind:item 53 - /> 101 + {#if hasFetched} 102 + <PlainTextEditor 103 + class="text-base-900 dark:text-base-50 line-clamp-2 text-lg font-bold" 104 + key="title" 105 + bind:item 106 + placeholder="Title here" 107 + /> 108 + {:else} 109 + <span class={'text-base-900 dark:text-base-50 line-clamp-2 text-lg font-bold'}> 110 + Loading data... 111 + </span> 112 + {/if} 54 113 </div> 55 114 <!-- <div class="text-base-800 dark:text-base-100 mt-2 text-xs">{item.cardData.description}</div> --> 56 - <div class="text-accent-600 accent:text-accent-950 font-semibold dark:text-accent-400 mt-2 text-xs"> 115 + <div 116 + class="text-accent-600 accent:text-accent-950 dark:text-accent-400 mt-2 text-xs font-semibold" 117 + > 57 118 {item.cardData.domain} 58 119 </div> 59 120 </div> 60 121 61 - {#if browser && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) && item.cardData.image} 122 + {#if hasFetched && browser && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) && item.cardData.image} 62 123 <img class=" mb-2 max-h-32 w-full rounded-xl object-cover" src={item.cardData.image} alt="" /> 63 124 {/if} 64 125 </div>
+31 -29
src/lib/cards/LinkCard/LinkCard.svelte
··· 12 12 13 13 <div class="flex h-full flex-col justify-between p-4"> 14 14 <div> 15 - {#if item.cardData.favicon} 16 - <div 17 - class="bg-base-100 border-base-300 dark:border-base-800 dark:bg-base-900 mb-2 inline-flex size-8 items-center justify-center rounded-xl border shadow-sm" 18 - > 19 - {#if !faviconHasError} 20 - <img 21 - class="size-6 rounded-lg object-cover" 22 - onerror={() => (faviconHasError = true)} 23 - src={item.cardData.favicon} 24 - alt="" 15 + <div 16 + class="bg-base-100 border-base-300 accent:bg-accent-100/50 accent:border-accent-200 dark:border-base-800 dark:bg-base-900 mb-2 inline-flex size-8 items-center justify-center rounded-xl border" 17 + > 18 + {#if item.cardData.favicon && !faviconHasError} 19 + <img 20 + class="size-6 rounded-lg object-cover" 21 + onerror={() => (faviconHasError = true)} 22 + src={item.cardData.favicon} 23 + alt="" 24 + /> 25 + {:else} 26 + <svg 27 + xmlns="http://www.w3.org/2000/svg" 28 + fill="none" 29 + viewBox="0 0 24 24" 30 + stroke-width="1.5" 31 + stroke="currentColor" 32 + class="size-4" 33 + > 34 + <path 35 + stroke-linecap="round" 36 + stroke-linejoin="round" 37 + d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" 25 38 /> 26 - {:else} 27 - <svg 28 - xmlns="http://www.w3.org/2000/svg" 29 - fill="none" 30 - viewBox="0 0 24 24" 31 - stroke-width="1.5" 32 - stroke="currentColor" 33 - class="size-4" 34 - > 35 - <path 36 - stroke-linecap="round" 37 - stroke-linejoin="round" 38 - d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" 39 - /> 40 - </svg> 41 - {/if} 42 - </div> 43 - {/if} 39 + </svg> 40 + {/if} 41 + </div> 44 42 <div 45 43 class={[ 46 44 'text-base-900 dark:text-base-50 text-lg font-bold', ··· 58 56 </div> 59 57 60 58 {#if browser && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) && item.cardData.image} 61 - <img class="mb-2 max-h-32 w-full starting:opacity-0 opacity-100 transition-opacity duration-100 rounded-xl object-cover" src={item.cardData.image} alt="" /> 59 + <img 60 + class="mb-2 max-h-32 w-full rounded-xl object-cover opacity-100 transition-opacity duration-100 starting:opacity-0" 61 + src={item.cardData.image} 62 + alt="" 63 + /> 62 64 {/if} 63 65 {#if item.cardData.href} 64 66 <a
+50
src/lib/cards/LinkCard/LinkCardSettings.svelte
··· 1 + <script lang="ts"> 2 + import { validateLink } from '$lib/helper'; 3 + import type { Item } from '$lib/types'; 4 + import { Button, Input, toast } from '@foxui/core'; 5 + 6 + let { item, onclose }: { item: Item; onclose: () => void } = $props(); 7 + 8 + let linkValue = $derived(item.cardData.href.replace('https://', '').replace('http://', '')); 9 + 10 + function updateLink() { 11 + if (!linkValue.trim()) return; 12 + 13 + let link = validateLink(linkValue); 14 + if (!link) { 15 + toast.error('Invalid link'); 16 + return; 17 + } 18 + 19 + item.cardData.href = link; 20 + item.cardData.domain = new URL(link).hostname; 21 + item.cardData.hasFetched = false; 22 + 23 + onclose?.(); 24 + } 25 + </script> 26 + 27 + <Input 28 + spellcheck={false} 29 + type="url" 30 + bind:value={linkValue} 31 + onkeydown={(event) => { 32 + if (event.code === 'Enter') { 33 + updateLink(); 34 + event.preventDefault(); 35 + } 36 + }} 37 + placeholder="Enter link" 38 + /> 39 + <Button onclick={updateLink} size="icon" 40 + ><svg 41 + xmlns="http://www.w3.org/2000/svg" 42 + fill="none" 43 + viewBox="0 0 24 24" 44 + stroke-width="1.5" 45 + stroke="currentColor" 46 + class="size-6" 47 + > 48 + <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /> 49 + </svg> 50 + </Button>
+9 -5
src/lib/cards/LinkCard/index.ts
··· 1 1 import type { CardDefinition } from '../types'; 2 - import CreateLinkCardModal from './CreateLinkCardModal.svelte'; 3 2 import EditingLinkCard from './EditingLinkCard.svelte'; 4 3 import LinkCard from './LinkCard.svelte'; 4 + import LinkCardSettings from './LinkCardSettings.svelte'; 5 5 6 6 export const LinkCardDefinition = { 7 7 type: 'link', ··· 9 9 editingContentComponent: EditingLinkCard, 10 10 createNew: (card) => { 11 11 card.cardType = 'link'; 12 - card.cardData = { 13 - href: 'https://' 14 - }; 15 12 }, 16 - creationModalComponent: CreateLinkCardModal 13 + settingsComponent: LinkCardSettings, 14 + onUrlHandler: (url, item) => { 15 + item.cardData.href = url; 16 + item.cardData.domain = new URL(url).hostname; 17 + item.cardData.hasFetched = false; 18 + return item; 19 + }, 20 + urlHandlerPriority: 0 17 21 } as CardDefinition & { type: 'link' };
+6 -6
src/lib/cards/TextCard/TextCardSettings.svelte
··· 27 27 viewBox="0 0 24 24" 28 28 fill="none" 29 29 stroke="currentColor" 30 - stroke-width="1.5" 30 + stroke-width="2" 31 31 stroke-linecap="round" 32 32 stroke-linejoin="round"><path d="M21 5H3" /><path d="M15 12H3" /><path d="M17 19H3" /></svg 33 33 ></ToggleGroupItem ··· 38 38 viewBox="0 0 24 24" 39 39 fill="none" 40 40 stroke="currentColor" 41 - stroke-width="1.5" 41 + stroke-width="2" 42 42 stroke-linecap="round" 43 43 stroke-linejoin="round"><path d="M21 5H3" /><path d="M17 12H7" /><path d="M19 19H5" /></svg 44 44 ></ToggleGroupItem ··· 49 49 viewBox="0 0 24 24" 50 50 fill="none" 51 51 stroke="currentColor" 52 - stroke-width="1.5" 52 + stroke-width="2" 53 53 stroke-linecap="round" 54 54 stroke-linejoin="round"><path d="M21 5H3" /><path d="M21 12H9" /><path d="M21 19H7" /></svg 55 55 ></ToggleGroupItem ··· 74 74 viewBox="0 0 24 24" 75 75 fill="none" 76 76 stroke="currentColor" 77 - stroke-width="1.5" 77 + stroke-width="2" 78 78 stroke-linecap="round" 79 79 stroke-linejoin="round" 80 80 ><rect width="6" height="16" x="4" y="6" rx="2" /><rect ··· 92 92 viewBox="0 0 24 24" 93 93 fill="none" 94 94 stroke="currentColor" 95 - stroke-width="1.5" 95 + stroke-width="2" 96 96 stroke-linecap="round" 97 97 stroke-linejoin="round" 98 98 ><rect width="10" height="6" x="7" y="9" rx="2" /><path d="M22 20H2" /><path ··· 106 106 viewBox="0 0 24 24" 107 107 fill="none" 108 108 stroke="currentColor" 109 - stroke-width="1.5" 109 + stroke-width="2" 110 110 stroke-linecap="round" 111 111 stroke-linejoin="round" 112 112 ><rect width="14" height="6" x="5" y="12" rx="2" /><rect
+4 -4
src/lib/cards/types.ts
··· 7 7 oncancel: () => void; 8 8 }; 9 9 10 - export type SettingsModalComponentProps = { 10 + export type SettingsComponentProps = { 11 11 item: Item; 12 - onsave: (item: Item) => void; 13 - oncancel: () => void; 12 + onclose: () => void; 14 13 }; 15 14 16 15 export type SidebarComponentProps = { ··· 37 36 sidebarButtonText?: string; 38 37 39 38 // if this component exists, a settings button with a popover will be shown containing this component 40 - settingsComponent?: Component<ContentComponentProps>; 39 + settingsComponent?: Component<SettingsComponentProps>; 41 40 42 41 // optionally load some extra data 43 42 loadData?: ( ··· 62 61 canResize?: boolean; 63 62 64 63 onUrlHandler?: (url: string, item: Item) => Item | null; 64 + urlHandlerPriority?: number; 65 65 };
+1
src/lib/cards/utils/PlainTextEditor.svelte
··· 76 76 :global(.tiptap p.is-editor-empty:first-child::before) { 77 77 color: var(--color-base-800); 78 78 content: attr(data-placeholder); 79 + opacity: 50%; 79 80 float: left; 80 81 height: 0; 81 82 pointer-events: none;
+25
src/lib/helper.ts
··· 299 299 300 300 return isEditable; 301 301 } 302 + 303 + export function validateLink( 304 + link: string | undefined, 305 + tryAdding: boolean = true 306 + ): string | undefined { 307 + if (!link) return; 308 + try { 309 + new URL(link); 310 + 311 + return link; 312 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 313 + } catch (e) { 314 + if (!tryAdding) return; 315 + 316 + try { 317 + link = 'https://' + link; 318 + new URL(link); 319 + 320 + return link; 321 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 322 + } catch (e) { 323 + return; 324 + } 325 + } 326 + }
+118 -58
src/lib/website/EditableWebsite.svelte
··· 1 1 <script lang="ts"> 2 2 import { client, login } from '$lib/oauth/auth.svelte.js'; 3 3 4 - import { Navbar, Button, toast, Toaster, Toggle, Sidebar } from '@foxui/core'; 4 + import { 5 + Navbar, 6 + Button, 7 + toast, 8 + Toaster, 9 + Toggle, 10 + Sidebar, 11 + Popover, 12 + Input, 13 + Label 14 + } from '@foxui/core'; 5 15 import { BlueskyLogin } from '@foxui/social'; 6 16 7 17 import { COLUMNS, margin, mobileMargin } from '$lib'; ··· 14 24 getHideProfile, 15 25 getName, 16 26 isTyping, 17 - setPositionOfNewItem 27 + setPositionOfNewItem, 28 + validateLink 18 29 } from '../helper'; 19 30 import Profile from './Profile.svelte'; 20 31 import type { Item, WebsiteData } from '../types'; ··· 76 87 77 88 let maxHeight = $derived(items.reduce((max, item) => Math.max(max, getY(item) + getH(item)), 0)); 78 89 79 - function newCard(type: string = 'link') { 90 + function newCard(type: string = 'link', cardData?: any) { 80 91 // close sidebar if open 81 92 const popover = document.getElementById('mobile-menu'); 82 93 if (popover) { ··· 94 105 mobileX: 0, 95 106 mobileY: 0, 96 107 cardType: type, 97 - cardData: {}, 108 + cardData: cardData ?? {}, 98 109 version: 2, 99 110 page: data.page 100 111 }; ··· 232 243 233 244 let debugPoint = $state({ x: 0, y: 0 }); 234 245 246 + let linkPopoverOpen = $state(false); 247 + 235 248 function getDragXY( 236 249 e: DragEvent & { 237 250 currentTarget: EventTarget & HTMLDivElement; ··· 264 277 } 265 278 return { x: gridX, y: gridY }; 266 279 } 267 - </script> 268 280 269 - <svelte:body 270 - onpaste={(event) => { 271 - if (isTyping()) return; 281 + let linkValue = $state(''); 272 282 273 - const text = event.clipboardData?.getData('text/plain'); 274 - 275 - if (!text) return; 283 + function addLink(url: string) { 284 + let link = validateLink(url); 285 + if (!link) { 286 + toast.error('invalid link'); 287 + return; 288 + } 276 289 277 - try { 278 - const url = new URL(text); 290 + let item: Item = { 291 + id: TID.nextStr(), 292 + x: 0, 293 + y: 0, 294 + w: 2, 295 + h: 2, 296 + mobileH: 4, 297 + mobileW: 4, 298 + mobileX: 0, 299 + mobileY: 0, 300 + cardType: '', 301 + cardData: {} 302 + }; 279 303 280 - let item: Item = { 281 - id: TID.nextStr(), 282 - x: 0, 283 - y: 0, 284 - w: 2, 285 - h: 2, 286 - mobileH: 4, 287 - mobileW: 4, 288 - mobileX: 0, 289 - mobileY: 0, 290 - cardType: '', 291 - cardData: {} 292 - }; 304 + newItem.item = item; 293 305 294 - newItem.item = item; 306 + console.log(AllCardDefinitions.toSorted( 307 + (a, b) => (b.urlHandlerPriority ?? 0) - (a.urlHandlerPriority ?? 0) 308 + )); 295 309 296 - for (const cardDef of AllCardDefinitions) { 297 - if (cardDef.onUrlHandler?.(text, item)) { 298 - item.cardType = cardDef.type; 299 - saveNewItem(); 300 - return; 301 - } 310 + for (const cardDef of AllCardDefinitions.toSorted( 311 + (a, b) => (b.urlHandlerPriority ?? 0) - (a.urlHandlerPriority ?? 0) 312 + )) { 313 + if (cardDef.onUrlHandler?.(link, item)) { 314 + item.cardType = cardDef.type; 315 + saveNewItem(); 316 + break; 302 317 } 318 + } 303 319 304 - newItem = {}; 305 - } catch (e) { 306 - return; 320 + newItem = {}; 321 + 322 + if(linkValue === url) { 323 + linkValue = ''; 324 + linkPopoverOpen = false; 307 325 } 326 + } 327 + </script> 328 + 329 + <svelte:body 330 + onpaste={(event) => { 331 + if (isTyping()) return; 332 + 333 + const text = event.clipboardData?.getData('text/plain'); 334 + const link = validateLink(text, false); 335 + if (!link) return; 336 + 337 + addLink(link); 308 338 }} 309 339 /> 310 340 ··· 514 544 /></svg 515 545 > 516 546 </Button> 517 - <Button 518 - size="iconLg" 519 - variant="ghost" 520 - class="backdrop-blur-none" 521 - onclick={() => { 522 - newCard('link'); 523 - }} 524 - > 525 - <svg 526 - xmlns="http://www.w3.org/2000/svg" 527 - fill="none" 528 - viewBox="-2 -2 28 28" 529 - stroke-width="1.5" 530 - stroke="currentColor" 531 - > 532 - <path 533 - stroke-linecap="round" 534 - stroke-linejoin="round" 535 - d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" 536 - /> 537 - </svg> 538 - </Button> 547 + 548 + <Popover sideOffset={16} bind:open={linkPopoverOpen} class="bg-base-100 dark:bg-base-900"> 549 + {#snippet child({ props })} 550 + <Button 551 + size="iconLg" 552 + variant="ghost" 553 + class="backdrop-blur-none" 554 + onclick={() => { 555 + newCard('link'); 556 + }} 557 + {...props} 558 + > 559 + <svg 560 + xmlns="http://www.w3.org/2000/svg" 561 + fill="none" 562 + viewBox="-2 -2 28 28" 563 + stroke-width="1.5" 564 + stroke="currentColor" 565 + > 566 + <path 567 + stroke-linecap="round" 568 + stroke-linejoin="round" 569 + d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" 570 + /> 571 + </svg> 572 + </Button> 573 + {/snippet} 574 + <Input 575 + spellcheck={false} 576 + type="url" 577 + bind:value={linkValue} 578 + onkeydown={(event) => { 579 + if (event.code === 'Enter') { 580 + addLink(linkValue); 581 + event.preventDefault(); 582 + } 583 + }} 584 + placeholder="Enter link" 585 + /> 586 + <Button onclick={() => addLink(linkValue)} size="icon" 587 + ><svg 588 + xmlns="http://www.w3.org/2000/svg" 589 + fill="none" 590 + viewBox="0 0 24 24" 591 + stroke-width="1.5" 592 + stroke="currentColor" 593 + class="size-6" 594 + > 595 + <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /> 596 + </svg> 597 + </Button> 598 + </Popover> 539 599 540 600 <Button 541 601 size="iconLg"