your personal website on atproto - mirror blento.app

v0

jycouet 42e3b83a 890d1bf9

+369 -27
+1
package.json
··· 73 "mapbox-gl": "^3.18.1", 74 "marked": "^17.0.1", 75 "plyr": "^3.8.4", 76 "simple-icons": "^16.6.0", 77 "svelte-sonner": "^1.0.7", 78 "tailwind-merge": "^3.4.0",
··· 73 "mapbox-gl": "^3.18.1", 74 "marked": "^17.0.1", 75 "plyr": "^3.8.4", 76 + "qr-code-styling": "^1.8.6", 77 "simple-icons": "^16.6.0", 78 "svelte-sonner": "^1.0.7", 79 "tailwind-merge": "^3.4.0",
+16
pnpm-lock.yaml
··· 110 plyr: 111 specifier: ^3.8.4 112 version: 3.8.4 113 simple-icons: 114 specifier: ^16.6.0 115 version: 16.6.0 ··· 2484 punycode@2.3.1: 2485 resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 2486 engines: {node: '>=6'} 2487 2488 quickselect@3.0.0: 2489 resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} ··· 5047 punycode.js@2.3.1: {} 5048 5049 punycode@2.3.1: {} 5050 5051 quickselect@3.0.0: {} 5052
··· 110 plyr: 111 specifier: ^3.8.4 112 version: 3.8.4 113 + qr-code-styling: 114 + specifier: ^1.8.6 115 + version: 1.9.2 116 simple-icons: 117 specifier: ^16.6.0 118 version: 16.6.0 ··· 2487 punycode@2.3.1: 2488 resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 2489 engines: {node: '>=6'} 2490 + 2491 + qr-code-styling@1.9.2: 2492 + resolution: {integrity: sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==} 2493 + engines: {node: '>=18.18.0'} 2494 + 2495 + qrcode-generator@1.5.2: 2496 + resolution: {integrity: sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==} 2497 2498 quickselect@3.0.0: 2499 resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} ··· 5057 punycode.js@2.3.1: {} 5058 5059 punycode@2.3.1: {} 5060 + 5061 + qr-code-styling@1.9.2: 5062 + dependencies: 5063 + qrcode-generator: 1.5.2 5064 + 5065 + qrcode-generator@1.5.2: {} 5066 5067 quickselect@3.0.0: {} 5068
+4 -4
src/lib/cards/BaseCard/BaseEditingCard.svelte
··· 57 58 const cardDef = $derived(CardDefinitionsByType[item.cardType]); 59 60 - const minW = $derived(cardDef.minW ?? (isMobile() ? 2 : 2)); 61 - const minH = $derived(cardDef.minH ?? (isMobile() ? 2 : 2)); 62 63 - const maxW = $derived(cardDef.maxW ?? COLUMNS); 64 - const maxH = $derived(cardDef.maxH ?? (isMobile() ? 12 : 6)); 65 66 // Resize handle state 67 let isResizing = $state(false);
··· 57 58 const cardDef = $derived(CardDefinitionsByType[item.cardType]); 59 60 + const minW = $derived(cardDef?.minW ?? (isMobile() ? 2 : 2)); 61 + const minH = $derived(cardDef?.minH ?? (isMobile() ? 2 : 2)); 62 63 + const maxW = $derived(cardDef?.maxW ?? COLUMNS); 64 + const maxH = $derived(cardDef?.maxH ?? (isMobile() ? 12 : 6)); 65 66 // Resize handle state 67 let isResizing = $state(false);
+16 -3
src/lib/cards/BigSocialCard/BigSocialCard.svelte
··· 1 <script lang="ts"> 2 import { platformsData } from '.'; 3 import type { ContentComponentProps } from '../types'; 4 5 let { item, isEditing }: ContentComponentProps = $props(); 6 7 const platform = $derived(item.cardData.platform as string); 8 </script> 9 10 <div ··· 14 <div 15 class="flex aspect-square max-h-full max-w-full items-center justify-center [&_svg]:size-full [&_svg]:max-w-60 [&_svg]:fill-white" 16 > 17 - {@html platformsData[platform].svg} 18 </div> 19 </div> 20 21 {#if !isEditing} 22 - <a href={item.cardData.href} target="_blank" rel="noopener noreferrer"> 23 <div class="absolute inset-0 z-50"></div> 24 - <span class="sr-only">open {platformsData[platform].title}</span> 25 </a> 26 {/if}
··· 1 <script lang="ts"> 2 import { platformsData } from '.'; 3 import type { ContentComponentProps } from '../types'; 4 + import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 5 6 let { item, isEditing }: ContentComponentProps = $props(); 7 8 const platform = $derived(item.cardData.platform as string); 9 + const platformData = $derived(platformsData[platform]); 10 </script> 11 12 <div ··· 16 <div 17 class="flex aspect-square max-h-full max-w-full items-center justify-center [&_svg]:size-full [&_svg]:max-w-60 [&_svg]:fill-white" 18 > 19 + {@html platformData?.svg} 20 </div> 21 </div> 22 23 {#if !isEditing} 24 + <a 25 + href={item.cardData.href} 26 + target="_blank" 27 + rel="noopener noreferrer" 28 + use:qrOverlay={{ 29 + context: { 30 + title: platformData?.title, 31 + icon: platformData?.svg, 32 + iconColor: platformData?.hex 33 + } 34 + }} 35 + > 36 <div class="absolute inset-0 z-50"></div> 37 + <span class="sr-only">open {platformData?.title}</span> 38 </a> 39 {/if}
+13 -3
src/lib/cards/BlueskyProfileCard/BlueskyProfileCard.svelte
··· 1 <script lang="ts"> 2 - import type { Item } from '$lib/types'; 3 4 - let { item }: { item: Item } = $props(); 5 </script> 6 7 <a 8 target="_blank" 9 - href="/{item.cardData.handle}" 10 class="flex h-full w-full flex-col items-center justify-center gap-2 rounded-xl p-2 transition-colors duration-150" 11 > 12 <img 13 src={item.cardData.avatar}
··· 1 <script lang="ts"> 2 + import type { ContentComponentProps } from '../types'; 3 + import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 4 5 + let { item, isEditing }: ContentComponentProps = $props(); 6 + 7 + const profileUrl = $derived(`https://bsky.app/profile/${item.cardData.handle}`); 8 </script> 9 10 <a 11 target="_blank" 12 + href={profileUrl} 13 class="flex h-full w-full flex-col items-center justify-center gap-2 rounded-xl p-2 transition-colors duration-150" 14 + use:qrOverlay={{ 15 + disabled: isEditing, 16 + context: { 17 + title: item.cardData.displayName || item.cardData.handle, 18 + avatar: item.cardData.avatar 19 + } 20 + }} 21 > 22 <img 23 src={item.cardData.avatar}
+14 -4
src/lib/cards/GitHubProfileCard/GitHubProfileCard.svelte
··· 7 import GithubContributionsGraph from './GithubContributionsGraph.svelte'; 8 import { Button } from '@foxui/core'; 9 import { browser } from '$app/environment'; 10 11 - let { item }: ContentComponentProps = $props(); 12 13 const data = getAdditionalUserData(); 14 ··· 75 </div> 76 </div> 77 78 - {#if item.cardData.href} 79 <a 80 - href={item.cardData.href} 81 class="absolute inset-0 h-full w-full" 82 target="_blank" 83 rel="noopener noreferrer" 84 > 85 - <span class="sr-only"> Show on github </span> 86 </a> 87 {/if}
··· 7 import GithubContributionsGraph from './GithubContributionsGraph.svelte'; 8 import { Button } from '@foxui/core'; 9 import { browser } from '$app/environment'; 10 + import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 11 + 12 + let { item, isEditing }: ContentComponentProps = $props(); 13 14 + const githubUrl = $derived(`https://github.com/${item.cardData.user}`); 15 16 const data = getAdditionalUserData(); 17 ··· 78 </div> 79 </div> 80 81 + {#if (item.cardData.href || item.cardData.user) && !isEditing} 82 <a 83 + href={item.cardData.href || githubUrl} 84 class="absolute inset-0 h-full w-full" 85 target="_blank" 86 rel="noopener noreferrer" 87 + use:qrOverlay={{ 88 + context: { 89 + title: item.cardData.user, 90 + icon: siGithub.svg, 91 + iconColor: siGithub.hex 92 + } 93 + }} 94 > 95 + <span class="sr-only">Show on github</span> 96 </a> 97 {/if}
+2
src/lib/cards/ImageCard/ImageCard.svelte
··· 2 import { getDidContext } from '$lib/website/context'; 3 import { getImageBlobUrl } from '$lib/atproto'; 4 import type { ContentComponentProps } from '../types'; 5 6 let { item = $bindable(), isEditing }: ContentComponentProps = $props(); 7 ··· 33 class="absolute inset-0 z-50 h-full w-full" 34 target="_blank" 35 rel="noopener noreferrer" 36 > 37 <span class="sr-only"> 38 {item.cardData.hrefText ?? 'Learn more'}
··· 2 import { getDidContext } from '$lib/website/context'; 3 import { getImageBlobUrl } from '$lib/atproto'; 4 import type { ContentComponentProps } from '../types'; 5 + import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 6 7 let { item = $bindable(), isEditing }: ContentComponentProps = $props(); 8 ··· 34 class="absolute inset-0 z-50 h-full w-full" 35 target="_blank" 36 rel="noopener noreferrer" 37 + use:qrOverlay={{ context: { title: item.cardData.hrefText ?? 'Learn more' } }} 38 > 39 <span class="sr-only"> 40 {item.cardData.hrefText ?? 'Learn more'}
+9 -2
src/lib/cards/LinkCard/LinkCard.svelte
··· 2 import { browser } from '$app/environment'; 3 import { getIsMobile } from '$lib/website/context'; 4 import type { ContentComponentProps } from '../types'; 5 6 - let { item }: ContentComponentProps = $props(); 7 8 let isMobile = getIsMobile(); 9 ··· 62 alt="" 63 /> 64 {/if} 65 - {#if item.cardData.href} 66 <a 67 href={item.cardData.href} 68 class="absolute inset-0 h-full w-full" 69 target="_blank" 70 rel="noopener noreferrer" 71 > 72 <span class="sr-only"> 73 {item.cardData.hrefText ?? 'Learn more'}
··· 2 import { browser } from '$app/environment'; 3 import { getIsMobile } from '$lib/website/context'; 4 import type { ContentComponentProps } from '../types'; 5 + import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 6 7 + let { item, isEditing }: ContentComponentProps = $props(); 8 9 let isMobile = getIsMobile(); 10 ··· 63 alt="" 64 /> 65 {/if} 66 + {#if item.cardData.href && !isEditing} 67 <a 68 href={item.cardData.href} 69 class="absolute inset-0 h-full w-full" 70 target="_blank" 71 rel="noopener noreferrer" 72 + use:qrOverlay={{ 73 + context: { 74 + title: item.cardData.title, 75 + favicon: item.cardData.favicon 76 + } 77 + }} 78 > 79 <span class="sr-only"> 80 {item.cardData.hrefText ?? 'Learn more'}
+8 -2
src/lib/cards/MapCard/MapCard.svelte
··· 1 <script lang="ts"> 2 import type { ContentComponentProps } from '../types'; 3 import Map from './Map.svelte'; 4 5 let { item = $bindable(), isEditing }: ContentComponentProps = $props(); 6 </script> 7 8 <Map bind:item /> ··· 11 <a 12 target="_blank" 13 rel="noopener noreferrer" 14 - href={'http://maps.google.com/maps?q=' + 15 - encodeURIComponent(item.cardData.lat + ',' + item.cardData.lon)} 16 > 17 <div class="absolute inset-0 z-100"></div> 18 <span class="sr-only">open map</span>
··· 1 <script lang="ts"> 2 import type { ContentComponentProps } from '../types'; 3 import Map from './Map.svelte'; 4 + import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 5 6 let { item = $bindable(), isEditing }: ContentComponentProps = $props(); 7 + 8 + const mapsUrl = $derived( 9 + 'https://maps.google.com/maps?q=' + 10 + encodeURIComponent(item.cardData.lat + ',' + item.cardData.lon) 11 + ); 12 </script> 13 14 <Map bind:item /> ··· 17 <a 18 target="_blank" 19 rel="noopener noreferrer" 20 + href={mapsUrl} 21 + use:qrOverlay={{ context: { title: 'Google Maps' } }} 22 > 23 <div class="absolute inset-0 z-100"></div> 24 <span class="sr-only">open map</span>
+52
src/lib/components/qr/QRCodeDisplay.svelte
···
··· 1 + <script lang="ts"> 2 + import { browser } from '$app/environment'; 3 + 4 + let { url, size = 280, logo }: { url: string; size?: number; logo?: string } = $props(); 5 + 6 + let container: HTMLDivElement | undefined = $state(); 7 + 8 + $effect(() => { 9 + if (!browser || !container) return; 10 + 11 + const render = async () => { 12 + const QRCodeStylingModule = await import('qr-code-styling'); 13 + const QRCodeStyling = QRCodeStylingModule.default; 14 + 15 + container!.innerHTML = ''; 16 + 17 + const options: ConstructorParameters<typeof QRCodeStyling>[0] = { 18 + width: size, 19 + height: size, 20 + data: url, 21 + dotsOptions: { 22 + color: '#000', 23 + type: 'rounded' 24 + }, 25 + backgroundOptions: { 26 + color: '#fff' 27 + }, 28 + cornersSquareOptions: { 29 + type: 'extra-rounded' 30 + }, 31 + cornersDotOptions: { 32 + type: 'dot' 33 + } 34 + }; 35 + 36 + if (logo) { 37 + options.image = logo; 38 + options.imageOptions = { 39 + crossOrigin: 'anonymous', 40 + margin: 4 41 + }; 42 + } 43 + 44 + const qrCode = new QRCodeStyling(options); 45 + qrCode.append(container!); 46 + }; 47 + 48 + render(); 49 + }); 50 + </script> 51 + 52 + <div bind:this={container} class="flex items-center justify-center"></div>
+84
src/lib/components/qr/QRCodeModal.svelte
···
··· 1 + <script lang="ts"> 2 + import { Modal, Button, toast } from '@foxui/core'; 3 + import QRCodeDisplay from './QRCodeDisplay.svelte'; 4 + 5 + export type QRContext = { 6 + title?: string; 7 + icon?: string; 8 + iconColor?: string; 9 + favicon?: string; 10 + avatar?: string; 11 + }; 12 + 13 + let { 14 + open = $bindable(false), 15 + href, 16 + context = {} 17 + }: { 18 + open: boolean; 19 + href: string; 20 + context?: QRContext; 21 + } = $props(); 22 + 23 + async function copyUrl() { 24 + try { 25 + await navigator.clipboard.writeText(href); 26 + toast.success('URL copied!'); 27 + } catch { 28 + toast.error('Failed to copy'); 29 + } 30 + } 31 + 32 + const logoUrl = $derived(context.avatar || context.favicon); 33 + </script> 34 + 35 + <Modal bind:open closeButton={true} class="max-w-sm"> 36 + <div class="flex flex-col items-center gap-4 p-2"> 37 + {#if context.icon} 38 + <div 39 + class="flex size-14 items-center justify-center rounded-2xl [&_svg]:size-8 [&_svg]:fill-white" 40 + style:background-color={context.iconColor ? `#${context.iconColor}` : '#000'} 41 + > 42 + {@html context.icon} 43 + </div> 44 + {:else if context.avatar} 45 + <img src={context.avatar} alt="" class="size-14 rounded-full object-cover" /> 46 + {:else if context.favicon} 47 + <img src={context.favicon} alt="" class="size-10 rounded-lg object-cover" /> 48 + {/if} 49 + 50 + {#if context.title} 51 + <div class="text-base-900 dark:text-base-100 text-lg font-semibold"> 52 + {context.title} 53 + </div> 54 + {/if} 55 + 56 + <div class="overflow-hidden rounded-2xl"> 57 + <QRCodeDisplay url={href} size={280} logo={logoUrl} /> 58 + </div> 59 + 60 + <div class="flex w-full items-center gap-2"> 61 + <div 62 + class="bg-base-100 dark:bg-base-800 text-base-600 dark:text-base-400 flex-1 truncate rounded-lg px-3 py-2 text-sm" 63 + > 64 + {href} 65 + </div> 66 + <Button onclick={copyUrl} variant="ghost" size="sm"> 67 + <svg 68 + xmlns="http://www.w3.org/2000/svg" 69 + fill="none" 70 + viewBox="0 0 24 24" 71 + stroke-width="1.5" 72 + stroke="currentColor" 73 + class="size-4" 74 + > 75 + <path 76 + stroke-linecap="round" 77 + stroke-linejoin="round" 78 + d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9.75a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184" 79 + /> 80 + </svg> 81 + </Button> 82 + </div> 83 + </div> 84 + </Modal>
+25
src/lib/components/qr/QRModalProvider.svelte
···
··· 1 + <script lang="ts"> 2 + import { onMount, onDestroy } from 'svelte'; 3 + import QRCodeModal, { type QRContext } from './QRCodeModal.svelte'; 4 + import { registerQRModal, unregisterQRModal } from './qrOverlay.svelte'; 5 + 6 + let open = $state(false); 7 + let href = $state(''); 8 + let context = $state<QRContext>({}); 9 + 10 + function showModal(newHref: string, newContext: QRContext) { 11 + href = newHref; 12 + context = newContext; 13 + open = true; 14 + } 15 + 16 + onMount(() => { 17 + registerQRModal(showModal); 18 + }); 19 + 20 + onDestroy(() => { 21 + unregisterQRModal(); 22 + }); 23 + </script> 24 + 25 + <QRCodeModal bind:open {href} {context} />
+72
src/lib/components/qr/qrOverlay.svelte.ts
···
··· 1 + import type { QRContext } from './QRCodeModal.svelte'; 2 + 3 + // Global state for QR modal 4 + let openModal: ((href: string, context: QRContext) => void) | null = null; 5 + 6 + export function registerQRModal(fn: (href: string, context: QRContext) => void) { 7 + openModal = fn; 8 + } 9 + 10 + export function unregisterQRModal() { 11 + openModal = null; 12 + } 13 + 14 + export function qrOverlay( 15 + node: HTMLAnchorElement, 16 + params: { context?: QRContext; disabled?: boolean } = {} 17 + ) { 18 + const LONG_PRESS_DURATION = 500; 19 + let longPressTimer: ReturnType<typeof setTimeout> | null = null; 20 + let isLongPress = false; 21 + 22 + function startLongPress() { 23 + if (params.disabled) return; 24 + isLongPress = false; 25 + longPressTimer = setTimeout(() => { 26 + isLongPress = true; 27 + openModal?.(node.href, params.context ?? {}); 28 + }, LONG_PRESS_DURATION); 29 + } 30 + 31 + function cancelLongPress() { 32 + if (longPressTimer) { 33 + clearTimeout(longPressTimer); 34 + longPressTimer = null; 35 + } 36 + } 37 + 38 + function handleClick(e: MouseEvent) { 39 + if (isLongPress) { 40 + e.preventDefault(); 41 + isLongPress = false; 42 + } 43 + } 44 + 45 + function handleContextMenu(e: MouseEvent) { 46 + if (params.disabled) return; 47 + e.preventDefault(); 48 + openModal?.(node.href, params.context ?? {}); 49 + } 50 + 51 + node.addEventListener('pointerdown', startLongPress); 52 + node.addEventListener('pointerup', cancelLongPress); 53 + node.addEventListener('pointercancel', cancelLongPress); 54 + node.addEventListener('pointerleave', cancelLongPress); 55 + node.addEventListener('click', handleClick); 56 + node.addEventListener('contextmenu', handleContextMenu); 57 + 58 + return { 59 + update(newParams: { context?: QRContext; disabled?: boolean }) { 60 + params = newParams; 61 + }, 62 + destroy() { 63 + node.removeEventListener('pointerdown', startLongPress); 64 + node.removeEventListener('pointerup', cancelLongPress); 65 + node.removeEventListener('pointercancel', cancelLongPress); 66 + node.removeEventListener('pointerleave', cancelLongPress); 67 + node.removeEventListener('click', handleClick); 68 + node.removeEventListener('contextmenu', handleContextMenu); 69 + cancelLongPress(); 70 + } 71 + }; 72 + }
+51 -9
src/lib/website/Profile.svelte
··· 8 import { getDescription, getName } from '$lib/helper'; 9 import { page } from '$app/state'; 10 import type { ActorIdentifier } from '@atcute/lexicons'; 11 12 let { 13 data, ··· 20 const renderer = new marked.Renderer(); 21 renderer.link = ({ href, title, text }) => 22 `<a target="_blank" href="${href}" title="${title}">${text}</a>`; 23 </script> 24 25 <!-- lg:fixed lg:h-screen lg:w-1/4 lg:max-w-none lg:px-12 lg:pt-24 xl:w-1/3 --> ··· 27 class="mx-auto flex max-w-lg flex-col justify-between px-8 @5xl/wrapper:fixed @5xl/wrapper:h-screen @5xl/wrapper:w-1/4 @5xl/wrapper:max-w-none @5xl/wrapper:px-12" 28 > 29 <div class="flex flex-col gap-4 pt-16 pb-8 @5xl/wrapper:h-screen @5xl/wrapper:pt-24"> 30 - {#if data.profile.avatar} 31 - <img 32 - class="border-base-400 dark:border-base-800 size-32 rounded-full border @5xl/wrapper:size-44" 33 - src={data.profile.avatar} 34 - alt="" 35 - /> 36 - {:else} 37 - <div class="bg-base-300 dark:bg-base-700 size-32 rounded-full @5xl/wrapper:size-44"></div> 38 - {/if} 39 40 <div class="text-4xl font-bold wrap-anywhere"> 41 {getName(data)}
··· 8 import { getDescription, getName } from '$lib/helper'; 9 import { page } from '$app/state'; 10 import type { ActorIdentifier } from '@atcute/lexicons'; 11 + import QRCodeModal from '$lib/components/qr/QRCodeModal.svelte'; 12 13 let { 14 data, ··· 21 const renderer = new marked.Renderer(); 22 renderer.link = ({ href, title, text }) => 23 `<a target="_blank" href="${href}" title="${title}">${text}</a>`; 24 + 25 + let qrOpen = $state(false); 26 + let longPressTimer: ReturnType<typeof setTimeout> | null = null; 27 + 28 + const profileUrl = $derived(`${page.url}/${data.handle}`); 29 + 30 + function startLongPress() { 31 + longPressTimer = setTimeout(() => { 32 + qrOpen = true; 33 + }, 500); 34 + } 35 + 36 + function cancelLongPress() { 37 + if (longPressTimer) { 38 + clearTimeout(longPressTimer); 39 + longPressTimer = null; 40 + } 41 + } 42 + 43 + function handleContextMenu(e: MouseEvent) { 44 + e.preventDefault(); 45 + qrOpen = true; 46 + } 47 </script> 48 49 <!-- lg:fixed lg:h-screen lg:w-1/4 lg:max-w-none lg:px-12 lg:pt-24 xl:w-1/3 --> ··· 51 class="mx-auto flex max-w-lg flex-col justify-between px-8 @5xl/wrapper:fixed @5xl/wrapper:h-screen @5xl/wrapper:w-1/4 @5xl/wrapper:max-w-none @5xl/wrapper:px-12" 52 > 53 <div class="flex flex-col gap-4 pt-16 pb-8 @5xl/wrapper:h-screen @5xl/wrapper:pt-24"> 54 + <!-- svelte-ignore a11y_no_static_element_interactions --> 55 + <div 56 + class="w-fit cursor-pointer" 57 + onpointerdown={startLongPress} 58 + onpointerup={cancelLongPress} 59 + onpointercancel={cancelLongPress} 60 + onpointerleave={cancelLongPress} 61 + oncontextmenu={handleContextMenu} 62 + > 63 + {#if data.profile.avatar} 64 + <img 65 + class="border-base-400 dark:border-base-800 size-32 rounded-full border @5xl/wrapper:size-44" 66 + src={data.profile.avatar} 67 + alt="" 68 + /> 69 + {:else} 70 + <div class="bg-base-300 dark:bg-base-700 size-32 rounded-full @5xl/wrapper:size-44"></div> 71 + {/if} 72 + </div> 73 + <QRCodeModal 74 + bind:open={qrOpen} 75 + href={profileUrl} 76 + context={{ 77 + title: getName(data), 78 + avatar: data.profile.avatar 79 + }} 80 + /> 81 82 <div class="text-4xl font-bold wrap-anywhere"> 83 {getName(data)}
+2
src/lib/website/Website.svelte
··· 9 import Context from './Context.svelte'; 10 import Head from './Head.svelte'; 11 import type { Did, Handle } from '@atcute/lexicons'; 12 13 let { data }: { data: WebsiteData } = $props(); 14 ··· 38 /> 39 40 <Context {data}> 41 <div class="@container/wrapper relative w-full"> 42 {#if !getHideProfileSection(data)} 43 <Profile {data} showEditButton={true} />
··· 9 import Context from './Context.svelte'; 10 import Head from './Head.svelte'; 11 import type { Did, Handle } from '@atcute/lexicons'; 12 + import QRModalProvider from '$lib/components/qr/QRModalProvider.svelte'; 13 14 let { data }: { data: WebsiteData } = $props(); 15 ··· 39 /> 40 41 <Context {data}> 42 + <QRModalProvider /> 43 <div class="@container/wrapper relative w-full"> 44 {#if !getHideProfileSection(data)} 45 <Profile {data} showEditButton={true} />