your personal website on atproto - mirror blento.app

Merge pull request #4 from flo-bit/icons

Icons

authored by Florian and committed by GitHub 9f6ca592 8925c6bc

+474 -42
+1
package.json
··· 68 68 "link-preview-js": "^4.0.0", 69 69 "marked": "^15.0.11", 70 70 "plyr": "^3.8.4", 71 + "simple-icons": "^16.5.0", 71 72 "svelte-sonner": "^1.0.7", 72 73 "tailwind-merge": "^3.4.0", 73 74 "tailwind-variants": "^3.2.2",
+9
pnpm-lock.yaml
··· 89 89 plyr: 90 90 specifier: ^3.8.4 91 91 version: 3.8.4 92 + simple-icons: 93 + specifier: ^16.5.0 94 + version: 16.5.0 92 95 svelte-sonner: 93 96 specifier: ^1.0.7 94 97 version: 1.0.7(svelte@5.45.8) ··· 2668 2671 side-channel@1.1.0: 2669 2672 resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==, tarball: https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz} 2670 2673 engines: {node: '>= 0.4'} 2674 + 2675 + simple-icons@16.5.0: 2676 + resolution: {integrity: sha512-72nn0oHADKx6Hknu7q6M0vfL8LiCUMKABOHane2+4xdqaFBSHfNNBjuZioihiqVQMz7IvVle4NKAM0IlXvl/9A==, tarball: https://registry.npmjs.org/simple-icons/-/simple-icons-16.5.0.tgz} 2677 + engines: {node: '>=0.12.18'} 2671 2678 2672 2679 simple-swizzle@0.2.4: 2673 2680 resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==, tarball: https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz} ··· 5438 5445 side-channel-list: 1.0.0 5439 5446 side-channel-map: 1.0.1 5440 5447 side-channel-weakmap: 1.0.2 5448 + 5449 + simple-icons@16.5.0: {} 5441 5450 5442 5451 simple-swizzle@0.2.4: 5443 5452 dependencies:
+2 -2
src/app.html
··· 1 1 <!doctype html> 2 - <html lang="en" class="stone"> 2 + <html lang="en" class="neutral"> 3 3 <head> 4 4 <meta charset="utf-8" /> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1" /> ··· 11 11 data-website-id="c55efa23-9abe-4a7e-b8fd-81b9fa7e8052" 12 12 ></script> 13 13 </head> 14 - <body data-sveltekit-preload-data="hover" class="bg-base-200/50 dark:bg-base-950"> 14 + <body data-sveltekit-preload-data="hover" class="bg-base-50 dark:bg-base-900"> 15 15 <div style="display: contents">%sveltekit.body%</div> 16 16 </body> 17 17 </html>
+56 -3
src/lib/EditableWebsite.svelte
··· 28 28 import { setDidContext, setHandleContext } from './website/context'; 29 29 import BaseEditingCard from './cards/BaseCard/BaseEditingCard.svelte'; 30 30 import Settings from './Settings.svelte'; 31 + import ImageDropper from './components/ImageDropper.svelte'; 31 32 32 33 let { 33 34 handle, ··· 234 235 } 235 236 </script> 236 237 238 + <svelte:body 239 + onpaste={(event) => { 240 + const target = event.target; 241 + 242 + const active = document.activeElement; 243 + const isEditable = 244 + active instanceof HTMLInputElement || 245 + active instanceof HTMLTextAreaElement || 246 + active?.isContentEditable; 247 + 248 + if (isEditable) { 249 + // Let normal paste happen 250 + return; 251 + } 252 + 253 + const text = event.clipboardData?.getData('text/plain'); 254 + 255 + if (!text) return; 256 + 257 + try { 258 + const url = new URL(text); 259 + 260 + let item: Item = { 261 + id: TID.nextStr(), 262 + x: 0, 263 + y: 0, 264 + w: 2, 265 + h: 2, 266 + mobileH: 4, 267 + mobileW: 4, 268 + mobileX: 0, 269 + mobileY: 0, 270 + cardType: '', 271 + cardData: {} 272 + }; 273 + 274 + newItem.item = item; 275 + 276 + for (const cardDef of AllCardDefinitions) { 277 + if (cardDef.onUrlHandler?.(text, item)) { 278 + item.cardType = cardDef.type; 279 + saveNewItem(); 280 + } 281 + } 282 + 283 + newItem = {}; 284 + } catch (e) { 285 + return; 286 + } 287 + }} 288 + /> 289 + 290 + <!-- <ImageDropper processImageFile={(file: File) => {}} /> --> 291 + 237 292 {#if !dev} 238 293 <div 239 294 class="bg-base-200 dark:bg-base-800 fixed inset-0 z-50 inline-flex h-full w-full items-center justify-center p-4 text-center lg:hidden" ··· 270 325 > 271 326 <Profile {handle} {did} {data} /> 272 327 273 - <div 274 - class="mx-auto max-w-2xl @5xl/wrapper:grid @5xl/wrapper:max-w-7xl @5xl/wrapper:grid-cols-4" 275 - > 328 + <div class="mx-auto max-w-lg @5xl/wrapper:grid @5xl/wrapper:max-w-7xl @5xl/wrapper:grid-cols-4"> 276 329 <div></div> 277 330 <!-- svelte-ignore a11y_no_static_element_interactions --> 278 331 <div
+11 -7
src/lib/Profile.svelte
··· 22 22 </script> 23 23 24 24 <Head 25 - favicon={'https://cdn.bsky.app/img/avatar/plain/' + did + '/' + profileData?.avatar.ref.$link} 25 + favicon={profileData?.avatar?.ref?.$link ? 'https://cdn.bsky.app/img/avatar/plain/' + did + '/' + profileData.avatar.ref.$link : null} 26 26 title={profileData?.displayName || handle} 27 27 image={'/' + handle + '/og.png'} 28 28 /> 29 29 30 30 <!-- lg:fixed lg:h-screen lg:w-1/4 lg:max-w-none lg:px-12 lg:pt-24 xl:w-1/3 --> 31 31 <div 32 - class="mx-auto flex max-w-2xl 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" 32 + 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" 33 33 > 34 34 <div class="flex flex-col gap-4 pt-16 pb-8 @5xl/wrapper:h-screen @5xl/wrapper:pt-24"> 35 - <img 36 - class="rounded-fulll size-32 rounded-full @5xl/wrapper:size-44" 37 - src={'https://cdn.bsky.app/img/avatar/plain/' + did + '/' + profileData?.avatar.ref.$link} 38 - alt="" 39 - /> 35 + {#if profileData?.avatar?.ref?.$link} 36 + <img 37 + class="size-32 rounded-full @5xl/wrapper:size-44 border border-base-400 dark:border-base-800" 38 + src={'https://cdn.bsky.app/img/avatar/plain/' + did + '/' + profileData.avatar.ref.$link} 39 + alt="" 40 + /> 41 + {:else} 42 + <div class="bg-base-300 dark:bg-base-700 size-32 rounded-full @5xl/wrapper:size-44"></div> 43 + {/if} 40 44 <div class="text-4xl font-bold wrap-anywhere"> 41 45 {profileData?.displayName || handle} 42 46 </div>
+1 -1
src/lib/Website.svelte
··· 39 39 <div class="@container/wrapper relative w-full"> 40 40 <Profile {handle} {did} {data} showEditButton={true} /> 41 41 42 - <div class="mx-auto max-w-2xl lg:grid lg:max-w-none lg:grid-cols-4"> 42 + <div class="mx-auto max-w-lg lg:grid lg:max-w-none lg:grid-cols-4"> 43 43 <div></div> 44 44 <div 45 45 bind:this={container}
+3 -1
src/lib/cards/BaseCard/BaseCard.svelte
··· 7 7 import { getColor } from '..'; 8 8 9 9 const colors = { 10 - base: 'bg-base-50 dark:bg-base-900', 10 + base: 'bg-base-200/50 dark:bg-base-950/50', 11 11 accent: 12 12 'bg-accent-400 dark:bg-accent-500 accent', 13 13 transparent: '' ··· 27 27 isEditing = false, 28 28 controls, 29 29 showOutline, 30 + class: className, 30 31 ...rest 31 32 }: BaseCardProps = $props(); 32 33 ··· 43 44 color ? (colors[color] ?? colors.accent) : colors.base, 44 45 color !== 'accent' && item.color !== 'base' && item.color !== 'transparent' ? color : '', 45 46 showOutline ? 'outline-2' : '', 47 + className 46 48 ]} 47 49 style={` 48 50 --mx: ${item.mobileX};
+6 -7
src/lib/cards/BaseCard/BaseEditingCard.svelte
··· 106 106 let newW = resizeStartW + gridDeltaW; 107 107 let newH = resizeStartH + gridDeltaH; 108 108 109 - console.log(item.mobileW, newW); 110 109 if (isMobile()) { 111 110 newW = Math.round(newW / 4) * 4; 112 111 } else { 113 112 newW = Math.round(newW / 2) * 2; 114 113 } 115 - console.log(item.mobileW, newW); 114 + let mult = isMobile() ? 2 : 1; 116 115 117 116 // Clamp to min/max 118 - newW = Math.max(minW, Math.min(maxW, newW)); 119 - newH = Math.max(minH, Math.min(maxH, newH)); 117 + newW = Math.max(minW * mult, Math.min(maxW, newW)); 118 + newH = Math.max(minH * mult, Math.min(maxH, newH)); 120 119 121 120 // Only call onsetsize if size changed 122 121 const currentW = isMobile() ? (item.mobileW ?? item.w) : item.w; ··· 137 136 if (!cardDef) return false; 138 137 139 138 if (isMobile()) { 140 - w *= 2; 141 - h *= 2; 139 + 140 + return w >= minW && w*2 <= maxW && h >= minH && h*2 <= maxH; 142 141 } 143 142 144 143 return w >= minW && w <= maxW && h >= minH && h <= maxH; ··· 155 154 let settingsPopoverOpen = $state(false); 156 155 </script> 157 156 158 - <BaseCard {item} {...rest} isEditing={true} bind:ref showOutline={isResizing}> 157 + <BaseCard {item} isEditing={true} bind:ref showOutline={isResizing} class="starting:scale-0 scale-100 starting:opacity-0 opacity-100" {...rest} > 159 158 {@render children?.()} 160 159 161 160 {#snippet controls()}
+24
src/lib/cards/BigSocialCard/BigSocialCard.svelte
··· 1 + <script lang="ts"> 2 + import { platformsData } from '.'; 3 + import type { ContentComponentProps } from '../types'; 4 + 5 + let { item }: ContentComponentProps = $props(); 6 + 7 + const platform = $derived(item.cardData.platform as string); 8 + 9 + $inspect(platformsData[platform].svg) 10 + </script> 11 + 12 + <a 13 + href={item.cardData.href} 14 + target="_blank" 15 + rel="noopener noreferrer" 16 + class="flex h-full w-full items-center justify-center p-10" 17 + style={ 18 + `background-color: #${item.cardData.color}` 19 + } 20 + > 21 + <div class="flex aspect-square max-h-full max-w-full items-center justify-center [&_svg]:size-full [&_svg]:max-w-60 [&_svg]:fill-white"> 22 + {@html platformsData[platform].svg} 23 + </div> 24 + </a>
+56
src/lib/cards/BigSocialCard/CreateBigSocialCardModal.svelte
··· 1 + <script lang="ts"> 2 + import { Alert, Button, Input, Modal, Subheading } from '@foxui/core'; 3 + import type { CreationModalComponentProps } from '../types'; 4 + import { detectPlatform, platformPatterns, platformsData } from '.'; 5 + 6 + let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props(); 7 + 8 + let errorMessage = $state(''); 9 + 10 + function handleCreate() { 11 + errorMessage = ''; 12 + 13 + try { 14 + new URL(item.cardData.href); 15 + } catch { 16 + errorMessage = 'Please enter a valid URL'; 17 + return; 18 + } 19 + 20 + const platform = detectPlatform(item.cardData.href); 21 + if (!platform) { 22 + errorMessage = 'Could not detect social media platform from URL'; 23 + return; 24 + } 25 + 26 + item.cardData.platform = platform; 27 + item.cardData.color = platformsData[platform].hex; 28 + 29 + oncreate(); 30 + } 31 + </script> 32 + 33 + <Modal open={true} closeButton={false}> 34 + <Subheading>Enter a social media link</Subheading> 35 + <Input 36 + bind:value={item.cardData.href} 37 + placeholder="https://instagram.com/username" 38 + onkeydown={(e) => { 39 + if (e.key === 'Enter') handleCreate(); 40 + }} 41 + /> 42 + 43 + <p class="text-base-500 mt-2 text-sm"> 44 + Supported: Instagram, Facebook, X/Twitter, YouTube, TikTok, LinkedIn, Bluesky, Threads, 45 + Snapchat, Pinterest, Twitch, Discord, GitHub, Spotify, Reddit, WhatsApp, Telegram, Mastodon 46 + </p> 47 + 48 + {#if errorMessage} 49 + <Alert type="error" title="Error"><span>{errorMessage}</span></Alert> 50 + {/if} 51 + 52 + <div class="mt-4 flex justify-end gap-2"> 53 + <Button onclick={oncancel} variant="ghost">Cancel</Button> 54 + <Button onclick={handleCreate}>Create</Button> 55 + </div> 56 + </Modal>
+21
src/lib/cards/BigSocialCard/SidebarItemBigSocialCard.svelte
··· 1 + <script lang="ts"> 2 + import { Button } from '@foxui/core'; 3 + 4 + let { onclick }: { onclick: () => void } = $props(); 5 + </script> 6 + 7 + <Button {onclick} variant="ghost" class="w-full justify-start"> 8 + <svg 9 + xmlns="http://www.w3.org/2000/svg" 10 + viewBox="0 0 24 24" 11 + fill="currentColor" 12 + class="text-accent-600 dark:text-accent-400" 13 + > 14 + <path 15 + fill-rule="evenodd" 16 + d="M4.848 2.771A49.144 49.144 0 0 1 12 2.25c2.43 0 4.817.178 7.152.52 1.978.292 3.348 2.024 3.348 3.97v6.02c0 1.946-1.37 3.678-3.348 3.97a48.901 48.901 0 0 1-3.476.383.39.39 0 0 0-.297.17l-2.755 4.133a.75.75 0 0 1-1.248 0l-2.755-4.133a.39.39 0 0 0-.297-.17 48.9 48.9 0 0 1-3.476-.384c-1.978-.29-3.348-2.024-3.348-3.97V6.741c0-1.946 1.37-3.68 3.348-3.97ZM6.75 8.25a.75.75 0 0 1 .75-.75h9a.75.75 0 0 1 0 1.5h-9a.75.75 0 0 1-.75-.75Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H7.5Z" 17 + clip-rule="evenodd" 18 + /> 19 + </svg> 20 + Big Social Icon</Button 21 + >
+109
src/lib/cards/BigSocialCard/index.ts
··· 1 + import type { CardDefinition } from '../types'; 2 + import BigSocialCard from './BigSocialCard.svelte'; 3 + import CreateBigSocialCardModal from './CreateBigSocialCardModal.svelte'; 4 + import SidebarItemBigSocialCard from './SidebarItemBigSocialCard.svelte'; 5 + 6 + export const BigSocialCardDefinition = { 7 + type: 'bigsocial', 8 + contentComponent: BigSocialCard, 9 + creationModalComponent: CreateBigSocialCardModal, 10 + sidebarComponent: SidebarItemBigSocialCard, 11 + createNew: (card) => { 12 + card.cardType = 'bigsocial'; 13 + card.cardData = { 14 + href: '', 15 + platform: '' 16 + }; 17 + card.w = 2; 18 + card.h = 2; 19 + card.mobileW = 4; 20 + card.mobileH = 4; 21 + }, 22 + allowSetColor: false, 23 + defaultColor: 'transparent', 24 + minW: 2, 25 + minH: 2, 26 + onUrlHandler: (url, item) => { 27 + const platform = detectPlatform(url); 28 + if (!platform) return null; 29 + 30 + item.cardData.platform = platform; 31 + item.cardData.color = platformsData[platform].hex; 32 + item.cardData.href = url; 33 + 34 + return item; 35 + } 36 + } as CardDefinition & { type: 'bigsocial' }; 37 + 38 + export const platformPatterns: Record<string, RegExp> = { 39 + instagram: /(?:instagram\.com|instagr\.am)/i, 40 + facebook: /(?:facebook\.com|fb\.com|fb\.me)/i, 41 + twitter: /(?:twitter\.com)/i, 42 + x: /(?:x\.com)/i, 43 + youtube: /(?:youtube\.com|youtu\.be)/i, 44 + tiktok: /(?:tiktok\.com)/i, 45 + linkedin: /(?:linkedin\.com)/i, 46 + bluesky: /(?:bsky\.app|bsky\.social)/i, 47 + threads: /(?:threads\.net)/i, 48 + snapchat: /(?:snapchat\.com)/i, 49 + pinterest: /(?:pinterest\.com|pin\.it)/i, 50 + twitch: /(?:twitch\.tv)/i, 51 + discord: /(?:discord\.gg|discord\.com)/i, 52 + github: /(?:github\.com)/i, 53 + spotify: /(?:spotify\.com|open\.spotify\.com)/i, 54 + reddit: /(?:reddit\.com)/i, 55 + whatsapp: /(?:whatsapp\.com|wa\.me)/i, 56 + telegram: /(?:t\.me|telegram\.org)/i, 57 + mastodon: /(?:mastodon\.social|mastodon\.online|mstdn\.social)/i 58 + }; 59 + 60 + import { 61 + siInstagram, 62 + siFacebook, 63 + siX, 64 + siYoutube, 65 + siTiktok, 66 + siBluesky, 67 + siThreads, 68 + siSnapchat, 69 + siPinterest, 70 + siTwitch, 71 + siDiscord, 72 + siGithub, 73 + siSpotify, 74 + siReddit, 75 + siWhatsapp, 76 + siTelegram, 77 + siMastodon, 78 + type SimpleIcon 79 + } from 'simple-icons'; 80 + 81 + export const platformsData: Record<string, SimpleIcon> = { 82 + instagram: siInstagram, 83 + facebook: siFacebook, 84 + twitter: siX, 85 + x: siX, 86 + youtube: siYoutube, 87 + tiktok: siTiktok, 88 + bluesky: siBluesky, 89 + threads: siThreads, 90 + snapchat: siSnapchat, 91 + pinterest: siPinterest, 92 + twitch: siTwitch, 93 + discord: siDiscord, 94 + github: siGithub, 95 + spotify: siSpotify, 96 + reddit: siReddit, 97 + whatsapp: siWhatsapp, 98 + telegram: siTelegram, 99 + mastodon: siMastodon 100 + }; 101 + 102 + export function detectPlatform(url: string): string | null { 103 + for (const [platform, pattern] of Object.entries(platformPatterns)) { 104 + if (pattern.test(url)) { 105 + return platform; 106 + } 107 + } 108 + return null; 109 + }
+2 -1
src/lib/cards/LinkCard/EditingLinkCard.svelte
··· 1 1 <script lang="ts"> 2 + import { browser } from '$app/environment'; 2 3 import { getIsMobile } from '$lib/helper'; 3 4 import type { ContentComponentProps } from '../types'; 4 5 import PlainTextEditor from '../utils/PlainTextEditor.svelte'; ··· 57 58 </div> 58 59 </div> 59 60 60 - {#if ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) && item.cardData.image} 61 + {#if browser && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) && item.cardData.image} 61 62 <img class=" mb-2 max-h-32 w-full rounded-xl object-cover" src={item.cardData.image} alt="" /> 62 63 {/if} 63 64 </div>
+3 -2
src/lib/cards/LinkCard/LinkCard.svelte
··· 1 1 <script lang="ts"> 2 + import { browser } from '$app/environment'; 2 3 import { getIsMobile } from '$lib/helper'; 3 4 import type { ContentComponentProps } from '../types'; 4 5 ··· 56 57 </div> 57 58 </div> 58 59 59 - {#if ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) && item.cardData.image} 60 - <img class=" mb-2 max-h-32 w-full rounded-xl object-cover" src={item.cardData.image} alt="" /> 60 + {#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="" /> 61 62 {/if} 62 63 {#if item.cardData.href} 63 64 <a
+1 -1
src/lib/cards/SpecialCards/UpdatedBlentos/UpdatedBlentosCard.svelte
··· 22 22 target="_blank" 23 23 > 24 24 <img src={profile.avatar} class="aspect-square size-28 rounded-full" alt="" /> 25 - <div class="line-clamp-1 text-lg font-bold">{profile.displayName || profile.handle}</div> 25 + <div class="line-clamp-1 text-md font-bold text-center">{profile.displayName || profile.handle}</div> 26 26 </a> 27 27 {/each} 28 28 </div>
+18 -6
src/lib/cards/TextCard/EditingTextCard.svelte
··· 1 1 <script lang="ts"> 2 2 import type { Item } from '$lib/types'; 3 - import { textAlignClasses, verticalAlignClasses } from '.'; 3 + import type { Editor } from '@tiptap/core'; 4 + import { textAlignClasses, textSizeClasses, verticalAlignClasses } from '.'; 4 5 import type { ContentComponentProps } from '../types'; 5 6 import MarkdownTextEditor from '../utils/MarkdownTextEditor.svelte'; 7 + import { cn } from '@foxui/core'; 6 8 7 9 let { item = $bindable<Item>() }: ContentComponentProps = $props(); 10 + 11 + let editor: Editor | null = $state(null); 12 + 13 + $inspect(textSizeClasses[item.cardData.textSize as number]); 8 14 </script> 9 15 16 + <!-- svelte-ignore a11y_no_static_element_interactions --> 17 + <!-- svelte-ignore a11y_click_events_have_key_events --> 10 18 <div 11 - class={[ 12 - 'prose dark:prose-invert prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 hover:bg-base-500/20 prose-p:first:mt-0 prose-p:last:mb-0 h-full overflow-y-scroll rounded-md p-2 inline-flex w-full max-w-none', 19 + class={cn( 20 + 'prose dark:prose-invert prose-neutral prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 hover:bg-base-700/5 accent:hover:bg-accent-300/20 prose-p:first:mt-0 prose-p:last:mb-0 inline-flex h-full w-full text-lg max-w-none overflow-y-scroll rounded-md p-2 transition-colors duration-150 cursor-text', 13 21 textAlignClasses[item.cardData.textAlign as string], 14 - verticalAlignClasses[item.cardData.verticalAlign as string] 15 - ]} 22 + verticalAlignClasses[item.cardData.verticalAlign as string], 23 + textSizeClasses[(item.cardData.textSize ?? 0) as number] 24 + )} 25 + onclick={() => { 26 + editor?.commands.focus('end'); 27 + }} 16 28 > 17 - <MarkdownTextEditor bind:item /> 29 + <MarkdownTextEditor bind:item bind:editor /> 18 30 </div>
+4 -3
src/lib/cards/TextCard/TextCard.svelte
··· 1 1 <script lang="ts"> 2 2 import { marked } from 'marked'; 3 3 import type { ContentComponentProps } from '../types'; 4 - import { textAlignClasses, verticalAlignClasses } from '.'; 4 + import { textAlignClasses, textSizeClasses, verticalAlignClasses } from '.'; 5 5 6 6 let { item }: ContentComponentProps = $props(); 7 7 ··· 12 12 13 13 <div 14 14 class={[ 15 - 'prose dark:prose-invert prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 prose-p:first:mt-0 prose-p:last:mb-0 inline-flex h-full w-full overflow-y-scroll rounded-md p-3 max-w-none', 15 + 'prose dark:prose-invert prose-neutral prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 prose-p:first:mt-0 prose-p:last:mb-0 prose-headings:first:mt-0 prose-headings:last:mb-0 inline-flex h-full min-h-full w-full max-w-none overflow-y-scroll rounded-md p-3 text-lg', 16 16 textAlignClasses?.[item.cardData.textAlign as string], 17 - verticalAlignClasses[item.cardData.verticalAlign as string] 17 + verticalAlignClasses[item.cardData.verticalAlign as string], 18 + textSizeClasses[(item.cardData.textSize ?? 0) as number] 18 19 ]} 19 20 > 20 21 <span>{@html marked.parse(item.cardData.text ?? '', { renderer })}</span>
+50 -1
src/lib/cards/TextCard/TextCardSettings.svelte
··· 1 1 <script lang="ts"> 2 2 import type { Item } from '$lib/types'; 3 3 import type { ContentComponentProps } from '../types'; 4 - import { ToggleGroup, ToggleGroupItem } from '@foxui/core'; 4 + import { ToggleGroup, ToggleGroupItem, Button } from '@foxui/core'; 5 5 6 6 let { item = $bindable<Item>() }: ContentComponentProps = $props(); 7 7 ··· 119 119 ></ToggleGroupItem 120 120 > 121 121 </ToggleGroup> 122 + 123 + <div> 124 + <Button 125 + variant="ghost" 126 + onclick={() => { 127 + item.cardData.textSize = Math.max((item.cardData.textSize ?? 0) - 1, 0); 128 + }} 129 + disabled={(item.cardData.textSize ?? 0) < 1} 130 + > 131 + <svg 132 + xmlns="http://www.w3.org/2000/svg" 133 + width="24" 134 + height="24" 135 + viewBox="0 0 24 24" 136 + fill="none" 137 + stroke="currentColor" 138 + stroke-width="2" 139 + stroke-linecap="round" 140 + stroke-linejoin="round" 141 + class="lucide lucide-aarrow-down-icon lucide-a-arrow-down" 142 + ><path d="m14 12 4 4 4-4" /><path d="M18 16V7" /><path 143 + d="m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16" 144 + /><path d="M3.304 13h6.392" /></svg 145 + > 146 + </Button> 147 + <Button 148 + variant="ghost" 149 + onclick={() => { 150 + item.cardData.textSize = Math.min((item.cardData.textSize ?? 0) + 1, 5); 151 + }} 152 + disabled={(item.cardData.textSize ?? 0) > 4} 153 + > 154 + <svg 155 + xmlns="http://www.w3.org/2000/svg" 156 + width="24" 157 + height="24" 158 + viewBox="0 0 24 24" 159 + fill="none" 160 + stroke="currentColor" 161 + stroke-width="2" 162 + stroke-linecap="round" 163 + stroke-linejoin="round" 164 + class="lucide lucide-aarrow-up-icon lucide-a-arrow-up" 165 + ><path d="m14 11 4-4 4 4" /><path d="M18 16V7" /><path 166 + d="m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16" 167 + /><path d="M3.304 13h6.392" /></svg 168 + > 169 + </Button> 170 + </div> 122 171 </div>
+10 -1
src/lib/cards/TextCard/index.ts
··· 24 24 }; 25 25 26 26 export const verticalAlignClasses: Record<string, string> = { 27 - top: 'items-start', 27 + top: 'items-stretch', 28 28 center: 'items-center-safe', 29 29 bottom: 'items-end-safe' 30 30 }; 31 31 32 + export const textSizeClasses = [ 33 + 'text-lg', 34 + 'text-xl', 35 + 'text-2xl', 36 + 'text-3xl', 37 + 'text-4xl', 38 + 'text-5xl' 39 + ]; 40 +
+2
src/lib/cards/index.ts
··· 1 1 import type { Item } from '$lib/types'; 2 2 import { ATProtoCollectionsCardDefinition } from './ATProtoCollectionsCard'; 3 + import { BigSocialCardDefinition } from './BigSocialCard'; 3 4 import { BlueskyMediaCardDefinition } from './BlueskyMediaCard'; 4 5 import { BlueskyPostCardDefinition } from './BlueskyPostCard'; 5 6 import { EmbedCardDefinition } from './EmbedCard'; ··· 17 18 ImageCardDefinition, 18 19 TextCardDefinition, 19 20 LinkCardDefinition, 21 + BigSocialCardDefinition, 20 22 UpdatedBlentosCardDefitition, 21 23 YoutubeCardDefinition, 22 24 BlueskyPostCardDefinition,
+2
src/lib/cards/types.ts
··· 60 60 maxH?: number; 61 61 62 62 canResize?: boolean; 63 + 64 + onUrlHandler?: (url: string, item: Item) => Item | null; 63 65 };
+17 -6
src/lib/cards/utils/MarkdownTextEditor.svelte
··· 10 10 import TurndownService from 'turndown'; 11 11 import { RichTextLink } from './extensions/RichTextLink'; 12 12 import type { Item } from '$lib/types'; 13 + import { textAlignClasses, verticalAlignClasses } from '../TextCard'; 13 14 14 15 let element: HTMLElement | undefined = $state(); 15 - let editor: Editor | null = $state(null); 16 16 17 17 let loaded = $state(false); 18 18 19 19 let { 20 + editor = $bindable(), 20 21 item = $bindable(), 21 22 placeholder = '', 22 23 defaultContent = '' 23 24 }: { 25 + editor: Editor | null; 24 26 item: Item; 25 27 placeholder?: string; 26 28 defaultContent?: string; ··· 50 52 51 53 // parse to json 52 54 json = generateJSON(html, [ 53 - StarterKit.configure(), 55 + StarterKit.configure({ 56 + heading: false, 57 + bulletList: false, 58 + codeBlock: false 59 + }), 54 60 Image.configure(), 55 61 RichTextLink.configure({ 56 62 openOnClick: false ··· 61 67 } 62 68 63 69 let extensions: Extensions = [ 64 - StarterKit.configure(), 70 + StarterKit.configure({ 71 + heading: false, 72 + bulletList: false, 73 + codeBlock: false, 74 + dropcursor: false 75 + }), 65 76 Image.configure(), 66 77 Link.configure({ 67 78 openOnClick: false ··· 92 103 93 104 editorProps: { 94 105 attributes: { 95 - class: 'outline-none' 106 + class: 'outline-none w-full' 96 107 }, 97 - handleDOMEvents: { drop: () => true } 108 + handleDOMEvents: { drop: () => false } 98 109 } 99 110 }); 100 111 ··· 108 119 }); 109 120 </script> 110 121 111 - <div bind:this={element}></div> 122 + <div class="w-full" bind:this={element}></div> 112 123 113 124 <style> 114 125 :global(.tiptap p.is-editor-empty:first-child::before) {
+66
src/lib/components/ImageDropper.svelte
··· 1 + <script lang="ts"> 2 + import { Portal } from 'bits-ui'; 3 + 4 + let isDragOver = $state(false); 5 + 6 + let { 7 + processImageFile 8 + }: { 9 + processImageFile: (file: File) => Promise<void>; 10 + } = $props(); 11 + 12 + function handleDragOver(event: DragEvent) { 13 + event.preventDefault(); 14 + event.stopPropagation(); 15 + 16 + const dt = event.dataTransfer; 17 + if (!dt) return; 18 + 19 + let imageCount = 0; 20 + if (dt.items) { 21 + for (let i = 0; i < dt.items.length; i++) { 22 + const item = dt.items[i]; 23 + if (item && item.kind === 'file' && item.type.startsWith('image/')) { 24 + imageCount++; 25 + } 26 + } 27 + } else if (dt.files) { 28 + for (let i = 0; i < dt.files.length; i++) { 29 + const file = dt.files[i]; 30 + if (file?.type.startsWith('image/')) { 31 + imageCount++; 32 + } 33 + } 34 + } 35 + 36 + isDragOver = imageCount > 0; 37 + } 38 + function handleDragLeave(event: DragEvent) { 39 + event.preventDefault(); 40 + event.stopPropagation(); 41 + isDragOver = false; 42 + } 43 + async function handleDrop(event: DragEvent) { 44 + event.preventDefault(); 45 + event.stopPropagation(); 46 + isDragOver = false; 47 + if (!event.dataTransfer?.files?.length) return; 48 + for (const file of event.dataTransfer.files) { 49 + if (file?.type.startsWith('image/')) { 50 + await processImageFile(file); 51 + } 52 + } 53 + } 54 + </script> 55 + 56 + <svelte:window ondragover={handleDragOver} ondragleave={handleDragLeave} ondrop={handleDrop} /> 57 + 58 + {#if isDragOver} 59 + <Portal> 60 + <div 61 + class="bg-base-100/80 dark:bg-base-900/80 text-primary dark:text-base-100 pointer-events-none absolute inset-0 z-[1000] flex items-center justify-center text-4xl font-bold backdrop-blur-md" 62 + > 63 + Drop file to add it to your message 64 + </div> 65 + </Portal> 66 + {/if}