your personal website on atproto - mirror blento.app

added tags. deleted videocard. deleted sidebarbutton.

+73 -300
+31 -29
.claude/settings.local.json
··· 1 1 { 2 - "permissions": { 3 - "allow": [ 4 - "Bash(pnpm check:*)", 5 - "mcp__ide__getDiagnostics", 6 - "mcp__plugin_svelte_svelte__svelte-autofixer", 7 - "mcp__plugin_svelte_svelte__list-sections", 8 - "Bash(pkill:*)", 9 - "Bash(timeout 8 pnpm dev:*)", 10 - "Bash(git checkout:*)", 11 - "Bash(npx svelte-kit:*)", 12 - "Bash(ls:*)", 13 - "Bash(pnpm format:*)", 14 - "Bash(pnpm add:*)", 15 - "WebSearch", 16 - "WebFetch(domain:github.com)", 17 - "WebFetch(domain:flipclockjs.com)", 18 - "WebFetch(domain:codepen.io)", 19 - "WebFetch(domain:flo-bit.dev)", 20 - "Bash(pnpm install)", 21 - "Bash(pnpm install:*)", 22 - "Bash(pnpm config:*)", 23 - "Bash(lsof:*)", 24 - "Bash(pnpm dev)", 25 - "Bash(pnpm exec svelte-kit:*)", 26 - "Bash(pnpm build:*)", 27 - "Bash(pnpm remove:*)", 28 - "Bash(grep:*)" 29 - ] 30 - } 2 + "permissions": { 3 + "allow": [ 4 + "Bash(pnpm check:*)", 5 + "mcp__ide__getDiagnostics", 6 + "mcp__plugin_svelte_svelte__svelte-autofixer", 7 + "mcp__plugin_svelte_svelte__list-sections", 8 + "Bash(pkill:*)", 9 + "Bash(timeout 8 pnpm dev:*)", 10 + "Bash(git checkout:*)", 11 + "Bash(npx svelte-kit:*)", 12 + "Bash(ls:*)", 13 + "Bash(pnpm format:*)", 14 + "Bash(pnpm add:*)", 15 + "WebSearch", 16 + "WebFetch(domain:github.com)", 17 + "WebFetch(domain:flipclockjs.com)", 18 + "WebFetch(domain:codepen.io)", 19 + "WebFetch(domain:flo-bit.dev)", 20 + "Bash(pnpm install)", 21 + "Bash(pnpm install:*)", 22 + "Bash(pnpm config:*)", 23 + "Bash(lsof:*)", 24 + "Bash(pnpm dev)", 25 + "Bash(pnpm exec svelte-kit:*)", 26 + "Bash(pnpm build:*)", 27 + "Bash(pnpm remove:*)", 28 + "Bash(grep:*)", 29 + "Bash(find:*)", 30 + "Bash(npx prettier:*)" 31 + ] 32 + } 31 33 }
+1 -3
src/lib/cards/ATProtoCollectionsCard/index.ts
··· 19 19 item.w = 4; 20 20 item.mobileW = 8; 21 21 }, 22 - sidebarButtonText: 'Atmosphere Collections', 23 - 24 22 name: 'ATProto Collections', 25 23 24 + keywords: ['bluesky', 'records', 'pds', 'data'], 26 25 groups: ['Social'], 27 26 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" /></svg>` 28 - 29 27 } as CardDefinition & { type: 'atprotocollections' };
+10 -1
src/lib/cards/BigSocialCard/index.ts
··· 53 53 urlHandlerPriority: 1, 54 54 canHaveLabel: true, 55 55 56 + keywords: [ 57 + 'twitter', 58 + 'instagram', 59 + 'tiktok', 60 + 'youtube', 61 + 'github', 62 + 'discord', 63 + 'linkedin', 64 + 'mastodon' 65 + ], 56 66 groups: ['Social'], 57 67 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z" /></svg>` 58 - 59 68 } as CardDefinition & { type: 'bigsocial' }; 60 69 61 70 import {
+1 -2
src/lib/cards/BlueskyMediaCard/index.ts
··· 7 7 contentComponent: BlueskyMediaCard, 8 8 createNew: () => {}, 9 9 creationModalComponent: CreateBlueskyMediaCardModal, 10 - sidebarButtonText: 'Bluesky Media', 11 10 canHaveLabel: true, 12 11 12 + keywords: ['bsky', 'atproto', 'media', 'feed'], 13 13 groups: ['Media'], 14 14 15 15 name: 'Video/Image from Bluesky', 16 16 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 0 1-1.125-1.125M3.375 19.5h1.5C5.496 19.5 6 18.996 6 18.375m-3.75 0V5.625m0 12.75v-1.5c0-.621.504-1.125 1.125-1.125m18.375 2.625V5.625m0 12.75c0 .621-.504 1.125-1.125 1.125m1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125m0 3.75h-1.5A1.125 1.125 0 0 1 18 18.375M20.625 4.5H3.375m17.25 0c.621 0 1.125.504 1.125 1.125M20.625 4.5h-1.5C18.504 4.5 18 5.004 18 5.625m3.75 0v1.5c0 .621-.504 1.125-1.125 1.125M3.375 4.5c-.621 0-1.125.504-1.125 1.125M3.375 4.5h1.5C5.496 4.5 6 5.004 6 5.625m-3.75 0v1.5c0 .621.504 1.125 1.125 1.125m0 0h1.5m-1.5 0c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125m1.5-3.75C5.496 8.25 6 7.746 6 7.125v-1.5M4.875 8.25C5.496 8.25 6 8.754 6 9.375v1.5m0-5.25v5.25m0-5.25C6 5.004 6.504 4.5 7.125 4.5h9.75c.621 0 1.125.504 1.125 1.125m1.125 2.625h1.5m-1.5 0A1.125 1.125 0 0 1 18 7.125v-1.5m1.125 2.625c-.621 0-1.125.504-1.125 1.125v1.5m2.625-2.625c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125M18 5.625v5.25M7.125 12h9.75m-9.75 0A1.125 1.125 0 0 1 6 10.875M7.125 12C6.504 12 6 12.504 6 13.125m0-2.25C6 11.496 5.496 12 4.875 12M18 10.875c0 .621-.504 1.125-1.125 1.125M18 10.875c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125m-12 5.25v-5.25m0 5.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125m-12 0v-1.5c0-.621-.504-1.125-1.125-1.125M18 18.375v-5.25m0 5.25v-1.5c0-.621.504-1.125 1.125-1.125M18 13.125v1.5c0 .621.504 1.125 1.125 1.125M18 13.125c0-.621.504-1.125 1.125-1.125M6 13.125v1.5c0 .621-.504 1.125-1.125 1.125M6 13.125C6 12.504 5.496 12 4.875 12m-1.5 0h1.5m-1.5 0c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125M19.125 12h1.5m0 0c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h1.5m14.25 0h1.5" /></svg>` 17 - 18 17 } as CardDefinition & { type: 'blueskyMedia' };
+1 -2
src/lib/cards/BlueskyPostCard/index.ts
··· 9 9 type: 'blueskyPost', 10 10 contentComponent: BlueskyPostCard, 11 11 creationModalComponent: CreateBlueskyPostCardModal, 12 - sidebarButtonText: 'Bluesky Post', 13 12 createNew: (card) => { 14 13 card.cardType = 'blueskyPost'; 15 14 card.w = 4; ··· 65 64 minW: 4, 66 65 name: 'Bluesky Post', 67 66 67 + keywords: ['skeet', 'bsky', 'atproto', 'post'], 68 68 groups: ['Social'], 69 69 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z" /></svg>` 70 - 71 70 } as CardDefinition & { type: 'blueskyPost' };
+1
src/lib/cards/BlueskyProfileCard/index.ts
··· 4 4 export const BlueskyProfileCardDefinition = { 5 5 type: 'blueskyProfile', 6 6 contentComponent: BlueskyProfileCard, 7 + keywords: ['bsky', 'atproto', 'account', 'user'], 7 8 createNew: () => {} 8 9 } as CardDefinition & { type: 'blueskyProfile' };
+1 -3
src/lib/cards/ButtonCard/index.ts
··· 8 8 contentComponent: ButtonCard, 9 9 editingContentComponent: EditingButtonCard, 10 10 settingsComponent: ButtonCardSettings, 11 - sidebarButtonText: 'Button', 12 - 13 11 createNew: (card) => { 14 12 card.cardData = { 15 13 text: 'Click me' ··· 29 27 maxW: 8, 30 28 maxH: 4, 31 29 30 + keywords: ['cta', 'action', 'click', 'link'], 32 31 groups: ['Utilities'], 33 32 name: 'Button', 34 33 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M15.042 21.672 13.684 16.6m0 0-2.51 2.225.569-9.47 5.227 7.917-3.286-.672ZM12 2.25V4.5m5.834.166-1.591 1.591M20.25 10.5H18M7.757 14.743l-1.59 1.59M6 10.5H3.75m4.007-4.243-1.59-1.59" /></svg>` 35 - 36 34 };
+1 -2
src/lib/cards/DrawCard/index.ts
··· 7 7 name: 'Drawing', 8 8 contentComponent: DrawCard, 9 9 editingContentComponent: EditingDrawCard, 10 - sidebarButtonText: 'Draw', 11 10 defaultColor: 'base', 12 11 allowSetColor: true, 13 12 minW: 2, ··· 25 24 }; 26 25 }, 27 26 27 + keywords: ['paint', 'sketch', 'doodle', 'canvas', 'art'], 28 28 groups: ['Visual'], 29 29 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" /></svg>` 30 - 31 30 } as CardDefinition & { type: 'draw' };
+1 -1
src/lib/cards/EmbedCard/index.ts
··· 20 20 // return item; 21 21 // }, 22 22 name: 'Embed', 23 + keywords: ['iframe', 'widget', 'html', 'website'], 23 24 groups: ['Media'], 24 25 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5" /></svg>` 25 - 26 26 } as CardDefinition & { type: 'embed' };
+1 -3
src/lib/cards/EventCard/index.ts
··· 42 42 type: 'event', 43 43 contentComponent: EventCard, 44 44 creationModalComponent: CreateEventCardModal, 45 - sidebarButtonText: 'Event', 46 - 47 45 createNew: (card) => { 48 46 card.w = 4; 49 47 card.h = 4; ··· 114 112 115 113 name: 'Event', 116 114 115 + keywords: ['calendar', 'meetup', 'schedule', 'date', 'rsvp'], 117 116 groups: ['Social'], 118 117 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" /></svg>` 119 - 120 118 } as CardDefinition & { type: 'event' };
+1 -2
src/lib/cards/FluidTextCard/index.ts
··· 20 20 }, 21 21 creationModalComponent: CreateFluidTextCardModal, 22 22 settingsComponent: FluidTextCardSettings, 23 - sidebarButtonText: 'Fluid Text', 24 23 defaultColor: 'transparent', 25 24 allowSetColor: true, 26 25 minW: 2, 27 26 27 + keywords: ['animated', 'big text', 'headline', 'display'], 28 28 groups: ['Visual'], 29 29 name: 'Fluid Text', 30 30 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12" /></svg>` 31 - 32 31 } as CardDefinition & { type: 'fluid-text' };
+1 -2
src/lib/cards/GIFCard/index.ts
··· 21 21 card.mobileH = 4; 22 22 }, 23 23 settingsComponent: GifCardSettings, 24 - sidebarButtonText: 'GIF', 25 24 defaultColor: 'transparent', 26 25 allowSetColor: false, 27 26 minW: 1, ··· 47 46 urlHandlerPriority: 5, 48 47 name: 'GIF', 49 48 49 + keywords: ['animation', 'giphy', 'meme', 'tenor'], 50 50 groups: ['Media'], 51 51 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M12.75 8.25v7.5m-6-3.75h3v3.75m-3-7.5h3M6 20.25h12A2.25 2.25 0 0 0 20.25 18V6A2.25 2.25 0 0 0 18 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25Z" /></svg>` 52 - 53 52 } as CardDefinition & { type: 'gif' };
+1 -2
src/lib/cards/GameCards/DinoGameCard/index.ts
··· 5 5 export const DinoGameCardDefinition = { 6 6 type: 'dino-game', 7 7 contentComponent: DinoGameCard as unknown as Component<ContentComponentProps>, 8 - sidebarButtonText: 'Dino Game', 9 8 allowSetColor: true, 10 9 createNew: (card) => { 11 10 card.w = 4; ··· 16 15 }, 17 16 canHaveLabel: true, 18 17 18 + keywords: ['chrome', 'dinosaur', 'runner', 'fun'], 19 19 groups: ['Games'], 20 20 name: 'Dino Game', 21 21 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M14.25 6.087c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.036-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959v0a.64.64 0 0 1-.657.643 48.491 48.491 0 0 1-4.163-.3c-1.228-.158-2.33.895-2.33 2.134v0c0 1.26 1.09 2.22 2.34 2.14a48.089 48.089 0 0 1 3.27-.108c.43 0 .78.348.78.78v0c0 .22-.09.422-.234.577a8.398 8.398 0 0 0-2.07 4.238c-.19 1.14.513 2.163 1.578 2.428a2.07 2.07 0 0 0 2.478-1.41c.203-.636.37-1.294.524-1.947.128-.537.612-.898 1.16-.84 1.378.15 2.782.18 4.17.076 1.156-.087 2.03-1.09 1.883-2.24a8.52 8.52 0 0 0-1.568-3.7A2.01 2.01 0 0 1 18 8.053v0c0-1.064.82-1.98 1.88-2.08A48.678 48.678 0 0 0 24 5.328v0" /></svg>` 22 - 23 22 } as CardDefinition & { type: 'dino-game' };
+1 -2
src/lib/cards/GameCards/TetrisCard/index.ts
··· 8 8 export const TetrisCardDefinition = { 9 9 type: 'tetris', 10 10 contentComponent: TetrisCard as unknown as Component<ContentComponentProps>, 11 - sidebarButtonText: 'Tetris', 12 11 allowSetColor: true, 13 12 defaultColor: 'accent', 14 13 createNew: (card) => { ··· 21 20 maxH: 10, 22 21 canHaveLabel: true, 23 22 23 + keywords: ['blocks', 'puzzle', 'game', 'fun'], 24 24 groups: ['Games'], 25 25 26 26 name: 'Tetris', 27 27 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M14 4h-4v4H6v4h4v4h4v-4h4V8h-4V4Z" /></svg>` 28 - 29 28 } as CardDefinition & { type: 'tetris' };
+1 -1
src/lib/cards/GitHubProfileCard/index.ts
··· 54 54 }, 55 55 name: 'Github Profile', 56 56 57 + keywords: ['developer', 'code', 'repos', 'contributions'], 57 58 groups: ['Social'], 58 59 icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" /></svg>` 59 - 60 60 } as CardDefinition & { type: 'githubProfile' }; 61 61 62 62 function getGitHubUsername(url: string | undefined): string | undefined {
+1 -2
src/lib/cards/GuestbookCard/index.ts
··· 7 7 type: 'guestbook', 8 8 contentComponent: GuestbookCard, 9 9 creationModalComponent: CreateGuestbookCardModal, 10 - sidebarButtonText: 'Guestbook', 11 10 createNew: (card) => { 12 11 card.w = 4; 13 12 card.h = 6; ··· 61 60 return results; 62 61 }, 63 62 name: 'Guestbook', 63 + keywords: ['comments', 'visitors', 'message', 'sign'], 64 64 groups: ['Social'], 65 65 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25" /></svg>` 66 - 67 66 } as CardDefinition & { type: 'guestbook' };
+1
src/lib/cards/ImageCard/index.ts
··· 46 46 47 47 canHaveLabel: true, 48 48 49 + keywords: ['photo', 'picture', 'upload', 'png', 'jpg'], 49 50 groups: ['Core'], 50 51 51 52 icon: `<svg
+1 -2
src/lib/cards/LatestBlueskyPostCard/index.ts
··· 12 12 card.h = 4; 13 13 card.mobileH = 8; 14 14 }, 15 - sidebarButtonText: 'Latest Bluesky Post', 16 15 loadData: async (items, { did }) => { 17 16 const authorFeed = await getAuthorFeed({ did, filter: 'posts_no_replies', limit: 2 }); 18 17 ··· 22 21 23 22 name: 'Latest Bluesky Post', 24 23 24 + keywords: ['bsky', 'atproto', 'recent', 'feed'], 25 25 groups: ['Social'], 26 26 icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4"><path d="M6.335 3.836a47.2 47.2 0 0 1 5.354 4.94c.088.093.165.18.232.26a18 18 0 0 1 .232-.26 47.2 47.2 0 0 1 5.355-4.94C18.882 2.687 21.46 1.37 22.553 2.483c.986 1.003.616 4.264.305 5.857-.567 2.902-2.018 4.274-3.703 4.542 2.348.386 4.678 1.96 3.13 5.602-1.97 4.636-7.065 1.763-9.795-.418a3 3 0 0 1-.18-.15 3 3 0 0 1-.18.15c-2.73 2.18-7.825 5.054-9.795.418-1.548-3.643.782-5.216 3.13-5.602C3.98 12.631 2.529 11.26 1.962 8.357c-.311-1.593-.681-4.854.305-5.857C3.361 1.37 5.94 2.687 6.335 3.836Z" /></svg>` 27 - 28 27 } as CardDefinition & { type: 'latestPost' };
+1
src/lib/cards/LinkCard/index.ts
··· 41 41 }, 42 42 urlHandlerPriority: 0, 43 43 44 + keywords: ['url', 'website', 'href', 'webpage'], 44 45 groups: ['Core'], 45 46 46 47 icon: `<svg
+1 -2
src/lib/cards/LivestreamCard/index.ts
··· 6 6 export const LivestreamCardDefitition = { 7 7 type: 'latestLivestream', 8 8 contentComponent: LivestreamCard, 9 - sidebarButtonText: 'stream.place info', 10 9 createNew: (card) => { 11 10 card.w = 4; 12 11 card.h = 4; ··· 82 81 urlHandlerPriority: 5, 83 82 84 83 name: 'Latest Livestream (stream.place)', 84 + keywords: ['stream', 'live', 'broadcast', 'video'], 85 85 groups: ['Media'], 86 86 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" /></svg>` 87 - 88 87 } as CardDefinition & { type: 'latestLivestream' }; 89 88 90 89 export const LivestreamEmbedCardDefitition = {
+1 -1
src/lib/cards/MapCard/index.ts
··· 6 6 export const MapCardDefinition = { 7 7 type: 'mapLocation', 8 8 contentComponent: MapCard, 9 - sidebarButtonText: 'Map', 10 9 createNew: (item) => { 11 10 item.w = 4; 12 11 item.h = 4; ··· 19 18 canHaveLabel: true, 20 19 settingsComponent: MapCardSettings, 21 20 21 + keywords: ['location', 'place', 'address', 'geo'], 22 22 groups: ['Core'], 23 23 24 24 name: 'Map',
-2
src/lib/cards/Model3DCard/index.ts
··· 7 7 type: 'model3d', 8 8 contentComponent: Model3DCard, 9 9 creationModalComponent: CreateModel3DCardModal, 10 - sidebarButtonText: '3D Model', 11 - 12 10 createNew: (card) => { 13 11 card.w = 4; 14 12 card.h = 4;
+1 -1
src/lib/cards/PhotoGalleryCard/index.ts
··· 68 68 69 69 return itemsData; 70 70 }, 71 + keywords: ['album', 'photos', 'slideshow', 'images', 'carousel'], 71 72 minW: 4 72 - //sidebarButtonText: 'Photo Gallery' 73 73 } as CardDefinition & { type: 'photoGallery' };
+1 -2
src/lib/cards/PopfeedReviews/index.ts
··· 17 17 return data; 18 18 }, 19 19 minH: 3, 20 - sidebarButtonText: 'Popfeed Reviews', 21 20 canHaveLabel: true, 22 21 22 + keywords: ['movies', 'tv', 'film', 'reviews', 'ratings', 'popfeed'], 23 23 groups: ['Media'], 24 24 name: 'Movie and TV Reviews', 25 25 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" /></svg>` 26 - 27 26 } as CardDefinition & { type: 'recentPopfeedReviews' };
+1
src/lib/cards/SectionCard/index.ts
··· 30 30 settingsComponent: SectionCardSettings, 31 31 32 32 name: 'Heading', 33 + keywords: ['title', 'section', 'header', 'divider'], 33 34 groups: ['Core'], 34 35 35 36 icon: `<svg
+1
src/lib/cards/SpecialCards/UpdatedBlentos/index.ts
··· 8 8 export const UpdatedBlentosCardDefitition = { 9 9 type: 'updatedBlentos', 10 10 contentComponent: UpdatedBlentosCard, 11 + keywords: ['feed', 'updates', 'recent', 'activity'], 11 12 loadData: async (items, { cache }) => { 12 13 try { 13 14 const response = await fetch(
+1 -3
src/lib/cards/SpotifyCard/index.ts
··· 8 8 type: cardType, 9 9 contentComponent: SpotifyCard, 10 10 creationModalComponent: CreateSpotifyCardModal, 11 - sidebarButtonText: 'Spotify Embed', 12 - 13 11 createNew: (item) => { 14 12 item.cardType = cardType; 15 13 item.cardData = {}; ··· 42 40 minW: 4, 43 41 minH: 5, 44 42 43 + keywords: ['music', 'song', 'playlist', 'album', 'podcast'], 45 44 groups: ['Media'], 46 45 icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4"><path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z" /></svg>` 47 - 48 46 } as CardDefinition & { type: typeof cardType }; 49 47 50 48 // Match Spotify album and playlist URLs
+1 -3
src/lib/cards/StandardSiteDocumentListCard/index.ts
··· 42 42 return records; 43 43 }, 44 44 45 - sidebarButtonText: 'site.standard.document list', 46 - 47 45 name: 'Blog Posts', 48 46 47 + keywords: ['articles', 'writing', 'blog', 'posts', 'frontpage'], 49 48 groups: ['Content'], 50 49 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /></svg>` 51 - 52 50 } as CardDefinition & { type: 'site.standard.document list' };
+1 -3
src/lib/cards/StatusphereCard/index.ts
··· 23 23 24 24 return data[0]; 25 25 }, 26 - sidebarButtonText: 'Statusphere', 27 - 28 26 upload: async (item) => { 29 27 if (item.cardData.hasUpdate) { 30 28 await putRecord({ ··· 50 48 canHaveLabel: true, 51 49 52 50 name: 'Emoji', 51 + keywords: ['status', 'mood', 'reaction', 'statusphere'], 53 52 groups: ['Media'], 54 53 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M15.182 15.182a4.5 4.5 0 0 1-6.364 0M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Z" /></svg>` 55 - 56 54 } as CardDefinition & { type: 'statusphere' }; 57 55 58 56 export function emojiToNotoAnimatedWebp(emoji: string | undefined): string | undefined {
+1 -2
src/lib/cards/TealFMPlaysCard/index.ts
··· 21 21 return data; 22 22 }, 23 23 minW: 4, 24 - sidebarButtonText: 'teal.fm Plays', 25 24 canHaveLabel: true, 26 25 26 + keywords: ['music', 'scrobble', 'listening', 'songs'], 27 27 name: 'Teal.fm Plays', 28 28 29 29 groups: ['Media'], 30 30 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="m9 9 10.5-3m0 6.553v3.75a2.25 2.25 0 0 1-1.632 2.163l-1.32.377a1.803 1.803 0 1 1-.99-3.467l2.31-.66a2.25 2.25 0 0 0 1.632-2.163Zm0 0V2.25L9 5.25v10.303m0 0v3.75a2.25 2.25 0 0 1-1.632 2.163l-1.32.377a1.803 1.803 0 0 1-.99-3.467l2.31-.66A2.25 2.25 0 0 0 9 15.553Z" /></svg>` 31 - 32 31 } as CardDefinition & { type: 'recentTealFMPlays' };
+1
src/lib/cards/TextCard/index.ts
··· 18 18 19 19 name: 'Text', 20 20 21 + keywords: ['paragraph', 'note', 'write', 'content', 'description', 'bio'], 21 22 groups: ['Core'], 22 23 23 24 icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-4"
+1 -1
src/lib/cards/TimerCard/index.ts
··· 29 29 } as TimerCardData; 30 30 }, 31 31 32 + keywords: ['stopwatch', 'clock', 'time'], 32 33 allowSetColor: true, 33 34 minW: 4, 34 35 canHaveLabel: true, ··· 46 47 item.cardData.label = data.label; 47 48 } 48 49 } 49 - 50 50 } as CardDefinition & { type: 'timer' };
+1 -2
src/lib/cards/VCardCard/index.ts
··· 120 120 card.cardData.displayName = displayName; 121 121 }, 122 122 123 - sidebarButtonText: 'vCard', 124 123 allowSetColor: true, 125 124 name: 'vCard Card', 125 + keywords: ['contact', 'phone', 'email', 'address', 'business card'], 126 126 groups: ['Social'], 127 127 icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Zm6-10.125a1.875 1.875 0 1 1-3.75 0 1.875 1.875 0 0 1 3.75 0Zm1.294 6.336a6.721 6.721 0 0 1-3.17.789 6.721 6.721 0 0 1-3.168-.789 3.376 3.376 0 0 1 6.338 0Z" /></svg>` 128 - 129 128 } as CardDefinition & { type: 'vcard' };
-90
src/lib/cards/VideoCard/VideoCard.svelte
··· 1 - <script lang="ts"> 2 - import { getDidContext } from '$lib/website/context'; 3 - import { getBlobURL } from '$lib/atproto'; 4 - import { onMount } from 'svelte'; 5 - import type { ContentComponentProps } from '../types'; 6 - 7 - let { item = $bindable() }: ContentComponentProps = $props(); 8 - 9 - const did = getDidContext(); 10 - 11 - let element: HTMLVideoElement | undefined = $state(); 12 - 13 - onMount(async () => { 14 - const el = element; 15 - if (!el) return; 16 - 17 - el.muted = true; 18 - 19 - // If we already have an objectUrl (preview before upload), use it directly 20 - if (item.cardData.objectUrl) { 21 - el.src = item.cardData.objectUrl; 22 - el.play().catch((e) => { 23 - console.error('Video play error:', e); 24 - }); 25 - return; 26 - } 27 - 28 - // Fetch the video blob from the PDS 29 - if (item.cardData.video?.video && typeof item.cardData.video.video === 'object') { 30 - try { 31 - const blobUrl = await getBlobURL({ did, blob: item.cardData.video.video }); 32 - const res = await fetch(blobUrl); 33 - if (!res.ok) throw new Error(res.statusText); 34 - const blob = await res.blob(); 35 - const url = URL.createObjectURL(blob); 36 - el.src = url; 37 - el.play().catch((e) => { 38 - console.error('Video play error:', e); 39 - }); 40 - } catch (e) { 41 - console.error('Failed to load video:', e); 42 - } 43 - } 44 - }); 45 - </script> 46 - 47 - {#key item.cardData.video || item.cardData.objectUrl} 48 - <video 49 - bind:this={element} 50 - muted 51 - loop 52 - autoplay 53 - playsinline 54 - class={[ 55 - 'absolute inset-0 h-full w-full object-cover opacity-100 transition-transform duration-300 ease-in-out', 56 - item.cardData.href ? 'group-hover:scale-102' : '' 57 - ]} 58 - ></video> 59 - {/key} 60 - {#if item.cardData.href} 61 - <a 62 - href={item.cardData.href} 63 - class="absolute inset-0 h-full w-full" 64 - target="_blank" 65 - rel="noopener noreferrer" 66 - > 67 - <span class="sr-only"> 68 - {item.cardData.hrefText ?? 'Learn more'} 69 - </span> 70 - 71 - <div 72 - class="bg-base-800/30 border-base-900/30 absolute top-2 right-2 rounded-full border p-1 text-white opacity-50 backdrop-blur-lg group-focus-within:opacity-100 group-hover:opacity-100" 73 - > 74 - <svg 75 - xmlns="http://www.w3.org/2000/svg" 76 - fill="none" 77 - viewBox="0 0 24 24" 78 - stroke-width="2.5" 79 - stroke="currentColor" 80 - class="size-4" 81 - > 82 - <path 83 - stroke-linecap="round" 84 - stroke-linejoin="round" 85 - d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" 86 - /> 87 - </svg> 88 - </div> 89 - </a> 90 - {/if}
-54
src/lib/cards/VideoCard/VideoCardSettings.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( 9 - item.cardData.href?.replace('https://', '').replace('http://', '') ?? '' 10 - ); 11 - 12 - function updateLink() { 13 - if (!linkValue.trim()) { 14 - item.cardData.href = ''; 15 - item.cardData.domain = ''; 16 - } 17 - 18 - let link = validateLink(linkValue); 19 - if (!link) { 20 - toast.error('Invalid link'); 21 - return; 22 - } 23 - 24 - item.cardData.href = link; 25 - item.cardData.domain = new URL(link).hostname; 26 - 27 - onclose?.(); 28 - } 29 - </script> 30 - 31 - <Input 32 - spellcheck={false} 33 - type="url" 34 - bind:value={linkValue} 35 - onkeydown={(event) => { 36 - if (event.code === 'Enter') { 37 - updateLink(); 38 - event.preventDefault(); 39 - } 40 - }} 41 - placeholder="Enter link" 42 - /> 43 - <Button onclick={updateLink} size="icon" 44 - ><svg 45 - xmlns="http://www.w3.org/2000/svg" 46 - fill="none" 47 - viewBox="0 0 24 24" 48 - stroke-width="1.5" 49 - stroke="currentColor" 50 - class="size-6" 51 - > 52 - <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /> 53 - </svg> 54 - </Button>
-65
src/lib/cards/VideoCard/index.ts
··· 1 - import { uploadBlob } from '$lib/atproto'; 2 - import type { CardDefinition } from '../types'; 3 - import VideoCard from './VideoCard.svelte'; 4 - import VideoCardSettings from './VideoCardSettings.svelte'; 5 - 6 - async function getAspectRatio(videoBlob: Blob): Promise<{ width: number; height: number }> { 7 - return new Promise((resolve, reject) => { 8 - const video = document.createElement('video'); 9 - video.preload = 'metadata'; 10 - 11 - video.onloadedmetadata = () => { 12 - URL.revokeObjectURL(video.src); 13 - resolve({ 14 - width: video.videoWidth, 15 - height: video.videoHeight 16 - }); 17 - }; 18 - 19 - video.onerror = () => { 20 - URL.revokeObjectURL(video.src); 21 - reject(new Error('Failed to load video metadata')); 22 - }; 23 - 24 - video.src = URL.createObjectURL(videoBlob); 25 - }); 26 - } 27 - 28 - export const VideoCardDefinition = { 29 - type: 'video', 30 - contentComponent: VideoCard, 31 - createNew: (card) => { 32 - card.cardType = 'video'; 33 - card.cardData = { 34 - video: null, 35 - href: '' 36 - }; 37 - }, 38 - upload: async (item) => { 39 - if (item.cardData.blob) { 40 - const blob = item.cardData.blob; 41 - const aspectRatio = await getAspectRatio(blob); 42 - const uploadedBlob = await uploadBlob({ blob }); 43 - 44 - item.cardData.video = { 45 - $type: 'app.bsky.embed.video', 46 - video: uploadedBlob, 47 - aspectRatio 48 - }; 49 - 50 - delete item.cardData.blob; 51 - } 52 - 53 - if (item.cardData.objectUrl) { 54 - URL.revokeObjectURL(item.cardData.objectUrl); 55 - delete item.cardData.objectUrl; 56 - } 57 - 58 - return item; 59 - }, 60 - settingsComponent: VideoCardSettings, 61 - 62 - name: 'Video', 63 - groups: ['Media'], 64 - icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" /></svg>` 65 - } as CardDefinition & { type: 'video' };
+1
src/lib/cards/YoutubeVideoCard/index.ts
··· 55 55 }, 56 56 name: 'Youtube Video', 57 57 58 + keywords: ['video', 'yt', 'stream', 'watch'], 58 59 groups: ['Media'], 59 60 60 61 icon: `<svg xmlns="http://www.w3.org/2000/svg" class="h-3" viewBox="0 0 256 180"
-2
src/lib/cards/index.ts
··· 15 15 import { UpdatedBlentosCardDefitition } from './SpecialCards/UpdatedBlentos'; 16 16 import { TextCardDefinition } from './TextCard'; 17 17 import type { CardDefinition } from './types'; 18 - import { VideoCardDefinition } from './VideoCard'; 19 18 import { YoutubeCardDefinition } from './YoutubeVideoCard'; 20 19 import { BlueskyProfileCardDefinition } from './BlueskyProfileCard'; 21 20 import { GithubProfileCardDefitition } from './GitHubProfileCard'; ··· 41 40 GuestbookCardDefinition, 42 41 ButtonCardDefinition, 43 42 ImageCardDefinition, 44 - VideoCardDefinition, 45 43 TextCardDefinition, 46 44 LinkCardDefinition, 47 45 BigSocialCardDefinition,
-7
src/lib/cards/types.ts
··· 13 13 onclose: () => void; 14 14 }; 15 15 16 - export type SidebarComponentProps = { 17 - onclick: () => void; 18 - }; 19 - 20 16 export type ContentComponentProps = { 21 17 item: Item; 22 18 isEditing?: boolean; ··· 32 28 creationModalComponent?: Component<CreationModalComponentProps>; 33 29 34 30 upload?: (item: Item) => Promise<Item>; // optionally upload some other data needed for this card 35 - 36 - // has to be set for a card to appear in the sidebar 37 - sidebarButtonText?: string; 38 31 39 32 // if this component exists, a settings button with a popover will be shown containing this component 40 33 settingsComponent?: Component<SettingsComponentProps>;
+1 -1
src/lib/website/EditableWebsite.svelte
··· 256 256 } 257 257 } 258 258 259 - const sidebarItems = AllCardDefinitions.filter((cardDef) => cardDef.sidebarButtonText); 259 + const sidebarItems = AllCardDefinitions.filter((cardDef) => cardDef.name); 260 260 261 261 let debugPoint = $state({ x: 0, y: 0 }); 262 262