diff --git a/wrangler.jsonc b/wrangler.jsonc index ddf6559..0ba01c6 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -33,9 +33,9 @@ * https://developers.cloudflare.com/workers/configuration/secrets/ */ "vars": { - "PUBLIC_HANDLE": "blento.app", - "PUBLIC_IS_SELFHOSTED": "", - "PUBLIC_DOMAIN": "https://blento.app", + "PUBLIC_HANDLE": "polijn.org", + "PUBLIC_IS_SELFHOSTED": "true", + "PUBLIC_DOMAIN": "https://polijn.org", "PUBLIC_GIPHY_API_TOKEN": "ltXijv1bkNPrEgnpJ0tIdLWXjnAeE7bL" }, "kv_namespaces": [ -- 2.51.2 From ae4fe500694947fac19a7a11c7f5280594849077 Mon Sep 17 00:00:00 2001 From: polijn <45722770+polijn@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:26:04 +0100 Subject: [PATCH 02/21] Remove USER_DATA_CACHE from kv_namespaces Removed USER_DATA_CACHE from kv_namespaces. --- wrangler.jsonc | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/wrangler.jsonc b/wrangler.jsonc index 0ba01c6..4eb5086 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -38,12 +38,7 @@ "PUBLIC_DOMAIN": "https://polijn.org", "PUBLIC_GIPHY_API_TOKEN": "ltXijv1bkNPrEgnpJ0tIdLWXjnAeE7bL" }, - "kv_namespaces": [ - { - "binding": "USER_DATA_CACHE", - "id": "d6ff203259de48538d332b0a5df258a7" - } - ] + /** * Service Bindings (communicate between multiple Workers) * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings -- 2.51.2 From bb73ff934bda40e6d3c13e24d369036f52ea53b6 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:54:36 +0100 Subject: [PATCH 03/21] commit --- src/lib/cards/BaseCard/BaseCard.svelte | 12 +- src/lib/cards/BaseCard/BaseEditingCard.svelte | 45 +- src/lib/website/EditBar.svelte | 467 +++++++++++++----- src/lib/website/EditableWebsite.svelte | 215 +++++++- src/lib/website/context.ts | 3 + 5 files changed, 596 insertions(+), 146 deletions(-) diff --git a/src/lib/cards/BaseCard/BaseCard.svelte b/src/lib/cards/BaseCard/BaseCard.svelte index 6eb6b55..7585d22 100644 --- a/src/lib/cards/BaseCard/BaseCard.svelte +++ b/src/lib/cards/BaseCard/BaseCard.svelte @@ -5,6 +5,16 @@ import type { Snippet } from 'svelte'; import type { HTMLAttributes } from 'svelte/elements'; import { getColor } from '..'; + import { getIsCoarse } from '$lib/website/context'; + + function tryGetIsCoarse(): (() => boolean) | undefined { + try { + return getIsCoarse(); + } catch { + return undefined; + } + } + const isCoarse = tryGetIsCoarse(); const colors = { base: 'bg-base-200/50 dark:bg-base-950/50', @@ -39,7 +49,7 @@ id={item.id} data-flip-id={item.id} bind:this={ref} - draggable={isEditing && !locked} + draggable={isEditing && !locked && !isCoarse?.()} class={[ 'card group/card selection:bg-accent-600/50 focus-within:outline-accent-500 @container/card absolute isolate z-0 rounded-3xl outline-offset-2 transition-all duration-200 focus-within:outline-2', color ? (colors[color] ?? colors.accent) : colors.base, diff --git a/src/lib/cards/BaseCard/BaseEditingCard.svelte b/src/lib/cards/BaseCard/BaseEditingCard.svelte index 93d80de..55dac42 100644 --- a/src/lib/cards/BaseCard/BaseEditingCard.svelte +++ b/src/lib/cards/BaseCard/BaseEditingCard.svelte @@ -7,7 +7,13 @@ import { ColorSelect } from '@foxui/colors'; import { AllCardDefinitions, CardDefinitionsByType, getColor } from '..'; import { COLUMNS } from '$lib'; - import { getCanEdit, getIsMobile } from '$lib/website/context'; + import { + getCanEdit, + getIsCoarse, + getIsMobile, + getSelectedCardId, + getSelectCard + } from '$lib/website/context'; import PlainTextEditor from '$lib/components/PlainTextEditor.svelte'; import { fixAllCollisions, fixCollisions } from '$lib/helper'; @@ -53,6 +59,12 @@ let canEdit = getCanEdit(); let isMobile = getIsMobile(); + let isCoarse = getIsCoarse(); + + let selectedCardId = getSelectedCardId(); + let selectCard = getSelectCard(); + let isSelected = $derived(selectedCardId?.() === item.id); + let isDimmed = $derived(isCoarse?.() && selectedCardId?.() != null && !isSelected); let colorPopoverOpen = $state(false); @@ -173,13 +185,28 @@ {item} isEditing={true} bind:ref - showOutline={isResizing} + showOutline={isResizing || (isCoarse?.() && isSelected)} locked={item.cardData?.locked} - class="scale-100 opacity-100 starting:scale-0 starting:opacity-0" + class={[ + 'scale-100 starting:scale-0 starting:opacity-0', + isCoarse?.() && isSelected ? 'ring-accent-500 z-10 ring-2 ring-offset-2' : '', + isDimmed ? 'opacity-70' : 'opacity-100' + ]} {...rest} > {#if !item.cardData?.locked} -
+ +
{ + if (isCoarse?.()) { + e.stopPropagation(); + selectCard?.(item.id); + } + }} + >
{/if} {@render children?.()} @@ -187,7 +214,7 @@
1} diff --git a/src/lib/cards/PopfeedReviews/index.ts b/src/lib/cards/PopfeedReviews/index.ts index 6888e0f..eef90bc 100644 --- a/src/lib/cards/PopfeedReviews/index.ts +++ b/src/lib/cards/PopfeedReviews/index.ts @@ -18,5 +18,9 @@ export const PopfeedReviewsCardDefinition = { }, minH: 3, sidebarButtonText: 'Popfeed Reviews', - canHaveLabel: true + canHaveLabel: true, + + groups: ['Media'], + name: 'Movie and TV Reviews', + icon: `` } as CardDefinition & { type: 'recentPopfeedReviews' }; diff --git a/src/lib/cards/SectionCard/index.ts b/src/lib/cards/SectionCard/index.ts index f8a2235..c3128df 100644 --- a/src/lib/cards/SectionCard/index.ts +++ b/src/lib/cards/SectionCard/index.ts @@ -26,7 +26,22 @@ export const SectionCardDefinition = { defaultColor: 'transparent', maxH: 1, canResize: false, - settingsComponent: SectionCardSettings + settingsComponent: SectionCardSettings, + + name: 'Heading', + groups: ['Core'], + + icon: `` } as CardDefinition & { type: 'section' }; export const textAlignClasses: Record = { diff --git a/src/lib/cards/SpotifyCard/index.ts b/src/lib/cards/SpotifyCard/index.ts index 9db2828..1c09753 100644 --- a/src/lib/cards/SpotifyCard/index.ts +++ b/src/lib/cards/SpotifyCard/index.ts @@ -40,7 +40,10 @@ export const SpotifyCardDefinition = { name: 'Spotify Embed', canResize: true, minW: 4, - minH: 5 + minH: 5, + + groups: ['Media'], + icon: `` } as CardDefinition & { type: typeof cardType }; // Match Spotify album and playlist URLs diff --git a/src/lib/cards/StandardSiteDocumentListCard/StandardSiteDocumentListCard.svelte b/src/lib/cards/StandardSiteDocumentListCard/StandardSiteDocumentListCard.svelte index a1070d3..8cdcfe5 100644 --- a/src/lib/cards/StandardSiteDocumentListCard/StandardSiteDocumentListCard.svelte +++ b/src/lib/cards/StandardSiteDocumentListCard/StandardSiteDocumentListCard.svelte @@ -27,12 +27,38 @@
- {#each feed ?? [] as document (document.uri)} - - {/each} + {#if feed && feed.length > 0} + {#each feed as document (document.uri)} + + {/each} + {:else if feed} +
+ No blog posts found. + + Create some on Leaflet + or + Pckt + +
+ {:else} +
+ Loading blog posts... +
+ {/if}
diff --git a/src/lib/cards/StandardSiteDocumentListCard/index.ts b/src/lib/cards/StandardSiteDocumentListCard/index.ts index f0d7fc8..a18bac1 100644 --- a/src/lib/cards/StandardSiteDocumentListCard/index.ts +++ b/src/lib/cards/StandardSiteDocumentListCard/index.ts @@ -42,5 +42,10 @@ export const StandardSiteDocumentListCardDefinition = { return records; }, - sidebarButtonText: 'site.standard.document list' + sidebarButtonText: 'site.standard.document list', + + name: 'Blog Posts', + + groups: ['Content'], + icon: `` } as CardDefinition & { type: 'site.standard.document list' }; diff --git a/src/lib/cards/StatusphereCard/index.ts b/src/lib/cards/StatusphereCard/index.ts index 31aa6e1..5520ba1 100644 --- a/src/lib/cards/StatusphereCard/index.ts +++ b/src/lib/cards/StatusphereCard/index.ts @@ -47,7 +47,11 @@ export const StatusphereCardDefinition = { item.cardData.label = item.cardData.title; } }, - canHaveLabel: true + canHaveLabel: true, + + name: 'Emoji', + groups: ['Media'], + icon: `` } as CardDefinition & { type: 'statusphere' }; export function emojiToNotoAnimatedWebp(emoji: string | undefined): string | undefined { diff --git a/src/lib/cards/TealFMPlaysCard/TealFMPlaysCard.svelte b/src/lib/cards/TealFMPlaysCard/TealFMPlaysCard.svelte index 4066169..610a352 100644 --- a/src/lib/cards/TealFMPlaysCard/TealFMPlaysCard.svelte +++ b/src/lib/cards/TealFMPlaysCard/TealFMPlaysCard.svelte @@ -85,13 +85,27 @@ {/snippet}
- {#each feed ?? [] as play (play.uri)} - {#if play.value.originUrl} - + {#if feed && feed.length > 0} + {#each feed as play (play.uri)} + {#if play.value.originUrl} + + {@render musicItem(play)} + + {:else} {@render musicItem(play)} - - {:else} - {@render musicItem(play)} - {/if} - {/each} + {/if} + {/each} + {:else if feed} +
+ No recent plays found. +
+ {:else} +
+ Loading plays... +
+ {/if}
diff --git a/src/lib/cards/TealFMPlaysCard/index.ts b/src/lib/cards/TealFMPlaysCard/index.ts index c18a511..79955e0 100644 --- a/src/lib/cards/TealFMPlaysCard/index.ts +++ b/src/lib/cards/TealFMPlaysCard/index.ts @@ -22,5 +22,10 @@ export const TealFMPlaysCardDefinition = { }, minW: 4, sidebarButtonText: 'teal.fm Plays', - canHaveLabel: true + canHaveLabel: true, + + name: 'Teal.fm Plays', + + groups: ['Media'], + icon: `` } as CardDefinition & { type: 'recentTealFMPlays' }; diff --git a/src/lib/cards/TextCard/index.ts b/src/lib/cards/TextCard/index.ts index 4f8296a..ff7c4b9 100644 --- a/src/lib/cards/TextCard/index.ts +++ b/src/lib/cards/TextCard/index.ts @@ -14,7 +14,22 @@ export const TextCardDefinition = { }; }, - settingsComponent: TextCardSettings + settingsComponent: TextCardSettings, + + name: 'Text', + + groups: ['Core'], + + icon: `` } as CardDefinition & { type: 'text' }; export const textAlignClasses: Record = { diff --git a/src/lib/cards/TimerCard/index.ts b/src/lib/cards/TimerCard/index.ts index 7bbe0b2..0cdc7c0 100644 --- a/src/lib/cards/TimerCard/index.ts +++ b/src/lib/cards/TimerCard/index.ts @@ -17,7 +17,6 @@ export const TimerCardDefinition = { type: 'timer', contentComponent: TimerCard, settingsComponent: TimerCardSettings, - sidebarButtonText: 'Timer', createNew: (card) => { card.w = 4; @@ -31,7 +30,20 @@ export const TimerCardDefinition = { }, allowSetColor: true, - name: 'Timer Card', minW: 4, - canHaveLabel: true + canHaveLabel: true, + + migrate: (item) => { + const data = item.cardData as TimerCardData; + if (data.mode === 'event') { + item.cardType = 'countdown'; + item.cardData = { targetDate: data.targetDate }; + } else { + item.cardType = 'clock'; + item.cardData = { timezone: data.timezone }; + } + if (data.label) { + item.cardData.label = data.label; + } + } } as CardDefinition & { type: 'timer' }; diff --git a/src/lib/cards/VCardCard/index.ts b/src/lib/cards/VCardCard/index.ts index 6632304..fdd2065 100644 --- a/src/lib/cards/VCardCard/index.ts +++ b/src/lib/cards/VCardCard/index.ts @@ -122,5 +122,7 @@ export const VCardCardDefinition = { sidebarButtonText: 'vCard', allowSetColor: true, - name: 'vCard Card' + name: 'vCard Card', + groups: ['Social'], + icon: `` } as CardDefinition & { type: 'vcard' }; diff --git a/src/lib/cards/VideoCard/index.ts b/src/lib/cards/VideoCard/index.ts index c969ec1..2138edb 100644 --- a/src/lib/cards/VideoCard/index.ts +++ b/src/lib/cards/VideoCard/index.ts @@ -59,5 +59,7 @@ export const VideoCardDefinition = { }, settingsComponent: VideoCardSettings, - name: 'Video Card' + name: 'Video', + groups: ['Media'], + icon: `` } as CardDefinition & { type: 'video' }; diff --git a/src/lib/cards/YoutubeVideoCard/CreateYoutubeCardModal.svelte b/src/lib/cards/YoutubeVideoCard/CreateYoutubeCardModal.svelte new file mode 100644 index 0000000..c677f8e --- /dev/null +++ b/src/lib/cards/YoutubeVideoCard/CreateYoutubeCardModal.svelte @@ -0,0 +1,52 @@ + + + +
{ + const url = item.cardData.href?.trim(); + if (!url) return; + + const id = matcher(url); + if (!id) { + errorMessage = 'Please enter a valid YouTube URL'; + return; + } + + item.cardData.youtubeId = id; + item.cardData.poster = `https://i.ytimg.com/vi/${id}/hqdefault.jpg`; + item.cardData.showInline = true; + + item.w = 4; + item.mobileW = 8; + item.h = 3; + item.mobileH = 5; + + oncreate?.(); + }} + class="flex flex-col gap-2" + > + Enter a YouTube URL + + + {#if errorMessage} +

{errorMessage}

+ {/if} + +
+ + +
+
+
diff --git a/src/lib/cards/YoutubeVideoCard/index.ts b/src/lib/cards/YoutubeVideoCard/index.ts index d8579cb..5bd3f01 100644 --- a/src/lib/cards/YoutubeVideoCard/index.ts +++ b/src/lib/cards/YoutubeVideoCard/index.ts @@ -1,4 +1,5 @@ import type { CardDefinition } from '../types'; +import CreateYoutubeCardModal from './CreateYoutubeCardModal.svelte'; import YoutubeCard from './YoutubeCard.svelte'; import YoutubeCardSettings from './YoutubeCardSettings.svelte'; @@ -6,6 +7,7 @@ export const YoutubeCardDefinition = { type: 'youtubeVideo', contentComponent: YoutubeCard, settingsComponent: YoutubeCardSettings, + creationModalComponent: CreateYoutubeCardModal, createNew: (card) => { card.cardType = 'youtubeVideo'; card.cardData = {}; @@ -51,7 +53,16 @@ export const YoutubeCardDefinition = { return item; }, - name: 'Youtube Video' + name: 'Youtube Video', + + groups: ['Media'], + + icon: `` } as CardDefinition & { type: 'youtubeVideo' }; // Thanks to eleventy-plugin-youtube-embed diff --git a/src/lib/cards/index.ts b/src/lib/cards/index.ts index cdf8e14..a95ce54 100644 --- a/src/lib/cards/index.ts +++ b/src/lib/cards/index.ts @@ -30,6 +30,8 @@ import { EventCardDefinition } from './EventCard'; import { VCardCardDefinition } from './VCardCard'; import { DrawCardDefinition } from './DrawCard'; import { TimerCardDefinition } from './TimerCard'; +import { ClockCardDefinition } from './ClockCard'; +import { CountdownCardDefinition } from './CountdownCard'; import { SpotifyCardDefinition } from './SpotifyCard'; import { ButtonCardDefinition } from './ButtonCard'; import { GuestbookCardDefinition } from './GuestbookCard'; @@ -69,6 +71,8 @@ export const AllCardDefinitions = [ VCardCardDefinition, DrawCardDefinition, TimerCardDefinition, + ClockCardDefinition, + CountdownCardDefinition, SpotifyCardDefinition // Model3DCardDefinition ] as const; diff --git a/src/lib/cards/types.ts b/src/lib/cards/types.ts index 3143eec..0620740 100644 --- a/src/lib/cards/types.ts +++ b/src/lib/cards/types.ts @@ -73,4 +73,10 @@ export type CardDefinition = { canHaveLabel?: boolean; migrate?: (item: Item) => void; + + groups?: string[]; + + keywords?: string[]; + + icon?: string; }; diff --git a/src/lib/components/card-command/CardCommand.svelte b/src/lib/components/card-command/CardCommand.svelte new file mode 100644 index 0000000..71d94a7 --- /dev/null +++ b/src/lib/components/card-command/CardCommand.svelte @@ -0,0 +1,192 @@ + + + + + + + + + Command Menu + + This is the command menu. Use the arrow keys to navigate and press ⌘K to open the search + bar. + + + { + searchValue = e.currentTarget.value; + }} + /> + + + + + No results found. + + + {#if urlMatchingCards.length > 0} + + + Add from link + + + {#each urlMatchingCards as cardDef (cardDef.type)} + { + selectUrl(cardDef); + }} + class="rounded-button data-selected:bg-accent-500/10 flex h-10 cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm outline-hidden select-none" + > + {#if cardDef.icon} +
+ {@html cardDef.icon} +
+ {/if} + {cardDef.name} +
+ {/each} +
+
+ + {/if} + + {#each CardDefGroups as group, index (group)} + {#if group && AllCardDefinitions.some((cardDef) => cardDef.groups?.includes(group))} + + + {group} + + + {#each AllCardDefinitions.filter( (cardDef) => cardDef.groups?.includes(group) ) as cardDef (cardDef.type)} + { + open = false; + searchValue = ''; + onselect(cardDef); + }} + class="rounded-button data-selected:bg-accent-500/10 flex h-10 cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm outline-hidden select-none" + keywords={[group, cardDef.type, ...(cardDef.keywords || [])]} + > + {#if cardDef.icon} +
+ {@html cardDef.icon} +
+ {/if} + {cardDef.name} +
+ {/each} +
+
+ {#if index < CardDefGroups.length - 1} + + {/if} + {/if} + {/each} +
+
+
+
+
+
diff --git a/src/lib/website/EditBar.svelte b/src/lib/website/EditBar.svelte index e07681b..a862e3a 100644 --- a/src/lib/website/EditBar.svelte +++ b/src/lib/website/EditBar.svelte @@ -2,13 +2,10 @@ import { dev } from '$app/environment'; import { user } from '$lib/atproto'; import type { WebsiteData } from '$lib/types'; - import { Button, Input, Navbar, Popover, Toggle, toast } from '@foxui/core'; + import { Button, Navbar, Toggle, toast } from '@foxui/core'; let { data, - linkValue = $bindable(), - newCard, - addLink, showingMobileView = $bindable(), isSaving = $bindable(), @@ -16,13 +13,9 @@ save, - handleImageInputChange, - handleVideoInputChange + showCardCommand }: { data: WebsiteData; - linkValue: string; - newCard: (type: string) => void; - addLink: (url: string) => void; showingMobileView: boolean; @@ -31,15 +24,9 @@ save: () => Promise; - handleImageInputChange: (evt: Event) => void; - handleVideoInputChange: (evt: Event) => void; + showCardCommand: () => void; } = $props(); - let linkPopoverOpen = $state(false); - - let imageInputRef: HTMLInputElement | undefined = $state(); - let videoInputRef: HTMLInputElement | undefined = $state(); - function getShareUrl() { const base = typeof window !== 'undefined' ? window.location.origin : ''; const pagePath = @@ -54,24 +41,6 @@ } - - - - {#if dev || (user.isLoggedIn && user.profile?.did === data.did)}
- - - - - - {#snippet child({ props })} - - {/snippet} - { - if (event.code === 'Enter') { - addLink(linkValue); - event.preventDefault(); - } - }} - placeholder="Enter link" - /> - - - - - - {#if dev} - - {/if} - -
+ + + + { + showCardCommand = true; + }} /> -- 2.51.2 From 21079b5f69efdfc7671ab2e7b31b3556b41d6b54 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sat, 31 Jan 2026 21:04:13 +0100 Subject: [PATCH 12/21] fix wrangler --- wrangler.jsonc | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/wrangler.jsonc b/wrangler.jsonc index 0ab7433..c383635 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -33,11 +33,17 @@ * https://developers.cloudflare.com/workers/configuration/secrets/ */ "vars": { - "PUBLIC_HANDLE": "polijn.org", - "PUBLIC_IS_SELFHOSTED": "true", - "PUBLIC_DOMAIN": "https://polijn.org", + "PUBLIC_HANDLE": "blento.app", + "PUBLIC_IS_SELFHOSTED": "", + "PUBLIC_DOMAIN": "https://blento.app", "PUBLIC_GIPHY_API_TOKEN": "ltXijv1bkNPrEgnpJ0tIdLWXjnAeE7bL" - } + }, + "kv_namespaces": [ + { + "binding": "USER_DATA_CACHE", + "id": "d6ff203259de48538d332b0a5df258a7" + } + ] /** * Service Bindings (communicate between multiple Workers) -- 2.51.2 From 363e250a1ab65936a40eec5497d6fafa70f1e065 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sat, 31 Jan 2026 21:40:06 +0100 Subject: [PATCH 13/21] more layout stuff --- src/lib/cards/SectionCard/index.ts | 1 + src/lib/website/layout-mirror.ts | 65 ++++++++++++++++-------------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/lib/cards/SectionCard/index.ts b/src/lib/cards/SectionCard/index.ts index c3128df..2bb2097 100644 --- a/src/lib/cards/SectionCard/index.ts +++ b/src/lib/cards/SectionCard/index.ts @@ -24,6 +24,7 @@ export const SectionCardDefinition = { }, defaultColor: 'transparent', + minW: COLUMNS, maxH: 1, canResize: false, settingsComponent: SectionCardSettings, diff --git a/src/lib/website/layout-mirror.ts b/src/lib/website/layout-mirror.ts index 5a2c5c9..afd025b 100644 --- a/src/lib/website/layout-mirror.ts +++ b/src/lib/website/layout-mirror.ts @@ -1,6 +1,6 @@ import { COLUMNS } from '$lib'; import { CardDefinitionsByType } from '$lib/cards'; -import { clamp, fixAllCollisions } from '$lib/helper'; +import { clamp, findValidPosition, fixAllCollisions } from '$lib/helper'; import type { Item } from '$lib/types'; /** @@ -23,28 +23,17 @@ function snapEven(v: number): number { */ export function mirrorItemSize(item: Item, fromMobile: boolean): void { const def = CardDefinitionsByType[item.cardType]; - const minW = def?.minW ?? 2; - const maxW = def?.maxW ?? COLUMNS; - const minH = def?.minH ?? 2; - const maxH = def?.maxH ?? Infinity; if (fromMobile) { - const srcW = item.mobileW; - const srcH = item.mobileH; - // Full-width cards stay full-width - item.w = srcW >= COLUMNS ? COLUMNS : clamp(snapEven(srcW / 2), minW, maxW); - item.h = clamp(snapEven((srcH * item.w) / srcW), minH, maxH); + // Mobile → Desktop: halve both dimensions, then clamp to card def constraints + // (constraints are in desktop units) + item.w = clamp(snapEven(item.mobileW / 2), def?.minW ?? 2, def?.maxW ?? COLUMNS); + item.h = clamp(Math.round(item.mobileH / 2), def?.minH ?? 1, def?.maxH ?? Infinity); } else { - const srcW = item.w; - const srcH = item.h; - // Full-width cards stay full-width - if (srcW >= COLUMNS) { - item.mobileW = clamp(COLUMNS, minW, Math.min(maxW, COLUMNS)); - } else { - const scaleFactor = Math.min(2, COLUMNS / srcW); - item.mobileW = clamp(snapEven(srcW * scaleFactor), minW, Math.min(maxW, COLUMNS)); - } - item.mobileH = clamp(snapEven((srcH * item.mobileW) / srcW), minH, maxH); + // Desktop → Mobile: double both dimensions + // (don't apply card def constraints — they're in desktop units) + item.mobileW = Math.min(item.w * 2, COLUMNS); + item.mobileH = Math.max(item.h * 2, 2); } } @@ -54,20 +43,36 @@ export function mirrorItemSize(item: Item, fromMobile: boolean): void { * Mutates items in-place. */ export function mirrorLayout(items: Item[], fromMobile: boolean): void { + // Mirror sizes first for (const item of items) { mirrorItemSize(item, fromMobile); + } + + if (fromMobile) { + // Mobile → Desktop: reflow items to use the full grid width. + // Sort by mobile position so items are placed in reading order. + const sorted = items.toSorted( + (a, b) => a.mobileY - b.mobileY || a.mobileX - b.mobileX + ); - if (fromMobile) { - // Mobile → Desktop positions - item.x = clamp(Math.floor(item.mobileX / 2 / 2) * 2, 0, COLUMNS - item.w); - item.y = Math.max(0, Math.round(item.mobileY / 2)); - } else { - // Desktop → Mobile positions - item.mobileX = clamp(Math.floor((item.x * 2) / 2) * 2, 0, COLUMNS - item.mobileW); + // Place each item into the first available spot on the desktop grid + const placed: Item[] = []; + for (const item of sorted) { + item.x = 0; + item.y = 0; + findValidPosition(item, placed, false); + placed.push(item); + } + } else { + // Desktop → Mobile: proportional positions + for (const item of items) { + item.mobileX = clamp( + Math.floor((item.x * 2) / 2) * 2, + 0, + COLUMNS - item.mobileW + ); item.mobileY = Math.max(0, Math.round(item.y * 2)); } + fixAllCollisions(items, true); } - - // Resolve collisions on the target layout - fixAllCollisions(items, !fromMobile); } -- 2.51.2 From a3c68b05559fcff2719ff835fdddac8d91fe6f20 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sat, 31 Jan 2026 21:59:37 +0100 Subject: [PATCH 14/21] remove buttons --- src/lib/website/EditBar.svelte | 116 --------------------------------- 1 file changed, 116 deletions(-) diff --git a/src/lib/website/EditBar.svelte b/src/lib/website/EditBar.svelte index 4083af6..7ed8dfd 100644 --- a/src/lib/website/EditBar.svelte +++ b/src/lib/website/EditBar.svelte @@ -331,123 +331,7 @@
{:else} -
- - - - - - {#snippet child({ props })} - - {/snippet} - { - if (event.code === 'Enter') { - addLink(linkValue); - event.preventDefault(); - } - }} - placeholder="Enter link" - /> - - - - - diff --git a/src/lib/cards/VideoCard/index.ts b/src/lib/cards/VideoCard/index.ts deleted file mode 100644 index 2138edb..0000000 --- a/src/lib/cards/VideoCard/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { uploadBlob } from '$lib/atproto'; -import type { CardDefinition } from '../types'; -import VideoCard from './VideoCard.svelte'; -import VideoCardSettings from './VideoCardSettings.svelte'; - -async function getAspectRatio(videoBlob: Blob): Promise<{ width: number; height: number }> { - return new Promise((resolve, reject) => { - const video = document.createElement('video'); - video.preload = 'metadata'; - - video.onloadedmetadata = () => { - URL.revokeObjectURL(video.src); - resolve({ - width: video.videoWidth, - height: video.videoHeight - }); - }; - - video.onerror = () => { - URL.revokeObjectURL(video.src); - reject(new Error('Failed to load video metadata')); - }; - - video.src = URL.createObjectURL(videoBlob); - }); -} - -export const VideoCardDefinition = { - type: 'video', - contentComponent: VideoCard, - createNew: (card) => { - card.cardType = 'video'; - card.cardData = { - video: null, - href: '' - }; - }, - upload: async (item) => { - if (item.cardData.blob) { - const blob = item.cardData.blob; - const aspectRatio = await getAspectRatio(blob); - const uploadedBlob = await uploadBlob({ blob }); - - item.cardData.video = { - $type: 'app.bsky.embed.video', - video: uploadedBlob, - aspectRatio - }; - - delete item.cardData.blob; - } - - if (item.cardData.objectUrl) { - URL.revokeObjectURL(item.cardData.objectUrl); - delete item.cardData.objectUrl; - } - - return item; - }, - settingsComponent: VideoCardSettings, - - name: 'Video', - groups: ['Media'], - icon: `` -} as CardDefinition & { type: 'video' }; diff --git a/src/lib/cards/YoutubeVideoCard/index.ts b/src/lib/cards/YoutubeVideoCard/index.ts index 5bd3f01..b6dc9df 100644 --- a/src/lib/cards/YoutubeVideoCard/index.ts +++ b/src/lib/cards/YoutubeVideoCard/index.ts @@ -55,6 +55,7 @@ export const YoutubeCardDefinition = { }, name: 'Youtube Video', + keywords: ['video', 'yt', 'stream', 'watch'], groups: ['Media'], icon: ` void; }; -export type SidebarComponentProps = { - onclick: () => void; -}; - export type ContentComponentProps = { item: Item; isEditing?: boolean; @@ -33,9 +29,6 @@ export type CardDefinition = { upload?: (item: Item) => Promise; // optionally upload some other data needed for this card - // has to be set for a card to appear in the sidebar - sidebarButtonText?: string; - // if this component exists, a settings button with a popover will be shown containing this component settingsComponent?: Component; diff --git a/src/lib/website/EditableWebsite.svelte b/src/lib/website/EditableWebsite.svelte index 8672bd1..abfd441 100644 --- a/src/lib/website/EditableWebsite.svelte +++ b/src/lib/website/EditableWebsite.svelte @@ -256,7 +256,7 @@ } } - const sidebarItems = AllCardDefinitions.filter((cardDef) => cardDef.sidebarButtonText); + const sidebarItems = AllCardDefinitions.filter((cardDef) => cardDef.name); let debugPoint = $state({ x: 0, y: 0 }); -- 2.51.2 From cb11c84bae35c71812a88065f6168d78f7685418 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sat, 31 Jan 2026 23:16:00 +0100 Subject: [PATCH 17/21] commit --- src/lib/cards/FriendsCard/FriendsCard.svelte | 120 ++++++++++++++++++ .../FriendsCard/FriendsCardSettings.svelte | 104 +++++++++++++++ src/lib/cards/FriendsCard/index.ts | 44 +++++++ .../PhotoGalleryCard/PhotoGalleryCard.svelte | 9 +- src/lib/cards/index.ts | 4 +- src/lib/website/EditableWebsite.svelte | 1 - src/lib/website/layout-mirror.ts | 10 +- 7 files changed, 281 insertions(+), 11 deletions(-) create mode 100644 src/lib/cards/FriendsCard/FriendsCard.svelte create mode 100644 src/lib/cards/FriendsCard/FriendsCardSettings.svelte create mode 100644 src/lib/cards/FriendsCard/index.ts diff --git a/src/lib/cards/FriendsCard/FriendsCard.svelte b/src/lib/cards/FriendsCard/FriendsCard.svelte new file mode 100644 index 0000000..08dd76f --- /dev/null +++ b/src/lib/cards/FriendsCard/FriendsCard.svelte @@ -0,0 +1,120 @@ + + +
+ {#if dids.length === 0} + {#if canEdit()} + + Add friends in settings + + {/if} + {:else} +
+ {#each visibleProfiles as profile, i (profile.did)} + 0 && sizeClass === 'sm'} + class:-ml-5={i > 0 && sizeClass === 'md'} + class:-ml-6={i > 0 && sizeClass === 'lg'} + > + + + {/each} + {#if overflowCount > 0} +
+ + +{overflowCount} + +
+ {/if} +
+ {/if} +
diff --git a/src/lib/cards/FriendsCard/FriendsCardSettings.svelte b/src/lib/cards/FriendsCard/FriendsCardSettings.svelte new file mode 100644 index 0000000..6f70ca6 --- /dev/null +++ b/src/lib/cards/FriendsCard/FriendsCardSettings.svelte @@ -0,0 +1,104 @@ + + +
+ + + {#if dids.length > 0} +
+ {#each dids as did (did)} + {@const profile = getProfile(did)} +
+ + + {profile?.handle ?? did} + + +
+ {/each} +
+ {/if} +
diff --git a/src/lib/cards/FriendsCard/index.ts b/src/lib/cards/FriendsCard/index.ts new file mode 100644 index 0000000..ca13d22 --- /dev/null +++ b/src/lib/cards/FriendsCard/index.ts @@ -0,0 +1,44 @@ +import type { CardDefinition } from '../types'; +import type { Did } from '@atcute/lexicons'; +import { getBlentoOrBskyProfile } from '$lib/atproto/methods'; +import FriendsCard from './FriendsCard.svelte'; +import FriendsCardSettings from './FriendsCardSettings.svelte'; + +export type FriendsProfile = Awaited>; + +export const FriendsCardDefinition = { + type: 'friends', + contentComponent: FriendsCard, + settingsComponent: FriendsCardSettings, + createNew: (card) => { + card.w = 4; + card.h = 2; + card.mobileW = 8; + card.mobileH = 4; + card.cardData.friends = []; + }, + loadData: async (items) => { + const allDids = new Set(); + for (const item of items) { + for (const did of item.cardData.friends ?? []) { + allDids.add(did as Did); + } + } + if (allDids.size === 0) return []; + + const profiles = await Promise.all( + Array.from(allDids).map((did) => + getBlentoOrBskyProfile({ did }).catch(() => undefined) + ) + ); + return profiles.filter((p) => p && p.handle !== 'handle.invalid'); + }, + allowSetColor: true, + defaultColor: 'base', + minW: 2, + minH: 2, + name: 'Friends', + groups: ['Social'], + keywords: ['friends', 'avatars', 'people', 'community', 'blentos'], + icon: `` +} as CardDefinition & { type: 'friends' }; diff --git a/src/lib/cards/PhotoGalleryCard/PhotoGalleryCard.svelte b/src/lib/cards/PhotoGalleryCard/PhotoGalleryCard.svelte index b97ebcb..4619b05 100644 --- a/src/lib/cards/PhotoGalleryCard/PhotoGalleryCard.svelte +++ b/src/lib/cards/PhotoGalleryCard/PhotoGalleryCard.svelte @@ -49,7 +49,7 @@ }); let images = $derived( - feed + (feed ?.toSorted((a: PhotoItem, b: PhotoItem) => { return (a.value.position ?? 0) - (b.value.position ?? 0); }) @@ -63,6 +63,13 @@ position: i.value.position ?? 0 }; }) + .filter((i) => i.src !== undefined) || []) as { + src: string; + name: string; + width: number; + height: number; + position: number; + }[] ); let isMobile = getIsMobile(); diff --git a/src/lib/cards/index.ts b/src/lib/cards/index.ts index a95ce54..4efcddf 100644 --- a/src/lib/cards/index.ts +++ b/src/lib/cards/index.ts @@ -35,6 +35,7 @@ import { CountdownCardDefinition } from './CountdownCard'; import { SpotifyCardDefinition } from './SpotifyCard'; import { ButtonCardDefinition } from './ButtonCard'; import { GuestbookCardDefinition } from './GuestbookCard'; +import { FriendsCardDefinition } from './FriendsCard'; // import { Model3DCardDefinition } from './Model3DCard'; export const AllCardDefinitions = [ @@ -73,8 +74,9 @@ export const AllCardDefinitions = [ TimerCardDefinition, ClockCardDefinition, CountdownCardDefinition, - SpotifyCardDefinition + SpotifyCardDefinition, // Model3DCardDefinition + FriendsCardDefinition ] as const; export const CardDefinitionsByType = AllCardDefinitions.reduce( diff --git a/src/lib/website/EditableWebsite.svelte b/src/lib/website/EditableWebsite.svelte index d380a6f..3dfe6f8 100644 --- a/src/lib/website/EditableWebsite.svelte +++ b/src/lib/website/EditableWebsite.svelte @@ -931,7 +931,6 @@ >
-
{ diff --git a/src/lib/website/layout-mirror.ts b/src/lib/website/layout-mirror.ts index afd025b..9de9a8c 100644 --- a/src/lib/website/layout-mirror.ts +++ b/src/lib/website/layout-mirror.ts @@ -51,9 +51,7 @@ export function mirrorLayout(items: Item[], fromMobile: boolean): void { if (fromMobile) { // Mobile → Desktop: reflow items to use the full grid width. // Sort by mobile position so items are placed in reading order. - const sorted = items.toSorted( - (a, b) => a.mobileY - b.mobileY || a.mobileX - b.mobileX - ); + const sorted = items.toSorted((a, b) => a.mobileY - b.mobileY || a.mobileX - b.mobileX); // Place each item into the first available spot on the desktop grid const placed: Item[] = []; @@ -66,11 +64,7 @@ export function mirrorLayout(items: Item[], fromMobile: boolean): void { } else { // Desktop → Mobile: proportional positions for (const item of items) { - item.mobileX = clamp( - Math.floor((item.x * 2) / 2) * 2, - 0, - COLUMNS - item.mobileW - ); + item.mobileX = clamp(Math.floor((item.x * 2) / 2) * 2, 0, COLUMNS - item.mobileW); item.mobileY = Math.max(0, Math.round(item.y * 2)); } fixAllCollisions(items, true); -- 2.51.2 From 37aab1b876c1e561cfeede58e9a69066589e85c2 Mon Sep 17 00:00:00 2001 From: unbedenklich <106080544+unbedenklich@users.noreply.github.com> Date: Sat, 31 Jan 2026 23:30:15 +0100 Subject: [PATCH 18/21] copy pages and add allcards to page in dev mode --- src/lib/cards/index.ts | 2 +- src/lib/website/EditableWebsite.svelte | 293 ++++++++++++++++++++++++- 2 files changed, 291 insertions(+), 4 deletions(-) diff --git a/src/lib/cards/index.ts b/src/lib/cards/index.ts index 80b9d6a..fe98c81 100644 --- a/src/lib/cards/index.ts +++ b/src/lib/cards/index.ts @@ -49,7 +49,7 @@ export const AllCardDefinitions = [ LatestBlueskyPostCardDefinition, LivestreamCardDefitition, LivestreamEmbedCardDefitition, - EmbedCardDefinition, + // EmbedCardDefinition, MapCardDefinition, ATProtoCollectionsCardDefinition, SectionCardDefinition, diff --git a/src/lib/website/EditableWebsite.svelte b/src/lib/website/EditableWebsite.svelte index abfd441..5e3e9f5 100644 --- a/src/lib/website/EditableWebsite.svelte +++ b/src/lib/website/EditableWebsite.svelte @@ -7,6 +7,7 @@ compactItems, createEmptyCard, findValidPosition, + fixAllCollisions, fixCollisions, getHideProfileSection, getProfilePosition, @@ -35,7 +36,8 @@ import EditBar from './EditBar.svelte'; import SaveModal from './SaveModal.svelte'; import FloatingEditButton from './FloatingEditButton.svelte'; - import { user } from '$lib/atproto'; + import { user, resolveHandle, listRecords, getCDNImageBlobUrl } from '$lib/atproto'; + import * as TID from '@atcute/tid'; import { launchConfetti } from '@foxui/visual'; import Controls from './Controls.svelte'; import CardCommand from '$lib/components/card-command/CardCommand.svelte'; @@ -258,6 +260,279 @@ const sidebarItems = AllCardDefinitions.filter((cardDef) => cardDef.name); + function addAllCardTypes() { + const groupOrder = ['Core', 'Social', 'Media', 'Content', 'Visual', 'Utilities', 'Games']; + const grouped = new Map(); + + for (const def of AllCardDefinitions) { + if (!def.name) continue; + const group = def.groups?.[0] ?? 'Other'; + if (!grouped.has(group)) grouped.set(group, []); + grouped.get(group)!.push(def); + } + + // Sort groups by predefined order, unknowns at end + const sortedGroups = [...grouped.keys()].sort((a, b) => { + const ai = groupOrder.indexOf(a); + const bi = groupOrder.indexOf(b); + return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi); + }); + + // Sample data for cards that would otherwise render empty + const sampleData: Record> = { + text: { text: 'The quick brown fox jumps over the lazy dog. This is a sample text card.' }, + link: { + href: 'https://bsky.app', + title: 'Bluesky', + domain: 'bsky.app', + description: 'Social networking that gives you choice', + hasFetched: true + }, + image: { + image: 'https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=600', + alt: 'Mountain landscape' + }, + button: { text: 'Visit Bluesky', href: 'https://bsky.app' }, + bigsocial: { platform: 'bluesky', href: 'https://bsky.app', color: '0085ff' }, + blueskyPost: { + uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jt64kgkbbs2y', + href: 'https://bsky.app/profile/bsky.app/post/3jt64kgkbbs2y' + }, + blueskyProfile: { + handle: 'bsky.app', + displayName: 'Bluesky', + avatar: + 'https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreihagr2cmvl2jt4mgx3sppwe2it3fwolkrbtjrhcnwjk4pcnbaq53m@jpeg' + }, + blueskyMedia: {}, + latestPost: {}, + youtubeVideo: { + youtubeId: 'dQw4w9WgXcQ', + poster: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg', + href: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + showInline: true + }, + 'spotify-list-embed': { + spotifyType: 'album', + spotifyId: '4aawyAB9vmqN3uQ7FjRGTy', + href: 'https://open.spotify.com/album/4aawyAB9vmqN3uQ7FjRGTy' + }, + latestLivestream: {}, + livestreamEmbed: { + href: 'https://stream.place/', + embed: 'https://stream.place/embed/' + }, + mapLocation: { lat: 48.8584, lon: 2.2945, zoom: 13, name: 'Eiffel Tower, Paris' }, + gif: { url: 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.mp4', alt: 'Cat typing' }, + event: { + uri: 'at://did:plc:257wekqxg4hyapkq6k47igmp/community.lexicon.calendar.event/3mcsoqzy7gm2q' + }, + guestbook: { label: 'Guestbook' }, + githubProfile: { user: 'sveltejs', href: 'https://github.com/sveltejs' }, + photoGallery: { + galleryUri: 'at://did:plc:tas6hj2xjrqben5653v5kohk/social.grain.gallery/3mclhsljs6h2w' + }, + atprotocollections: {}, + publicationList: {}, + recentPopfeedReviews: {}, + recentTealFMPlays: {}, + statusphere: { emoji: '✨' }, + vcard: {}, + 'fluid-text': { text: 'Hello World' }, + draw: { strokesJson: '[]', viewBox: '', strokeWidth: 1, locked: true }, + clock: {}, + countdown: { targetDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() }, + timer: {}, + 'dino-game': {}, + tetris: {}, + updatedBlentos: {} + }; + + // Labels for cards that support canHaveLabel + const sampleLabels: Record = { + image: 'Mountain Landscape', + mapLocation: 'Eiffel Tower', + gif: 'Cat Typing', + bigsocial: 'Bluesky', + guestbook: 'Guestbook', + statusphere: 'My Status', + recentPopfeedReviews: 'My Reviews', + recentTealFMPlays: 'Recently Played', + clock: 'Local Time', + countdown: 'Launch Day', + timer: 'Timer', + 'dino-game': 'Dino Game', + tetris: 'Tetris', + blueskyMedia: 'Bluesky Media' + }; + + const newItems: Item[] = []; + let cursorY = 0; + let mobileCursorY = 0; + + for (const group of sortedGroups) { + const defs = grouped.get(group)!; + + // Add a section heading for the group + const heading = createEmptyCard(data.page); + heading.cardType = 'section'; + heading.cardData = { text: group, verticalAlign: 'bottom', textSize: 1 }; + heading.w = COLUMNS; + heading.h = 1; + heading.x = 0; + heading.y = cursorY; + heading.mobileW = COLUMNS; + heading.mobileH = 2; + heading.mobileX = 0; + heading.mobileY = mobileCursorY; + newItems.push(heading); + cursorY += 1; + mobileCursorY += 2; + + // Place cards in rows + let rowX = 0; + let rowMaxH = 0; + let mobileRowX = 0; + let mobileRowMaxH = 0; + + for (const def of defs) { + if (def.type === 'section' || def.type === 'embed') continue; + + const item = createEmptyCard(data.page); + item.cardType = def.type; + item.cardData = {}; + def.createNew?.(item); + + // Merge in sample data (without overwriting createNew defaults) + const extra = sampleData[def.type]; + if (extra) { + item.cardData = { ...item.cardData, ...extra }; + } + + // Set item-level color for cards that need it + if (def.type === 'button') { + item.color = 'transparent'; + } + + // Add label if card supports it + const label = sampleLabels[def.type]; + if (label && def.canHaveLabel) { + item.cardData.label = label; + } + + // Desktop layout + if (rowX + item.w > COLUMNS) { + cursorY += rowMaxH; + rowX = 0; + rowMaxH = 0; + } + item.x = rowX; + item.y = cursorY; + rowX += item.w; + rowMaxH = Math.max(rowMaxH, item.h); + + // Mobile layout + if (mobileRowX + item.mobileW > COLUMNS) { + mobileCursorY += mobileRowMaxH; + mobileRowX = 0; + mobileRowMaxH = 0; + } + item.mobileX = mobileRowX; + item.mobileY = mobileCursorY; + mobileRowX += item.mobileW; + mobileRowMaxH = Math.max(mobileRowMaxH, item.mobileH); + + newItems.push(item); + } + + // Move cursor past last row + cursorY += rowMaxH; + mobileCursorY += mobileRowMaxH; + } + + items = newItems; + onLayoutChanged(); + } + + let copyInput = $state(''); + let isCopying = $state(false); + + async function copyPageFrom() { + const input = copyInput.trim(); + if (!input) return; + + isCopying = true; + try { + // Parse "handle" or "handle/page" + const parts = input.split('/'); + const handle = parts[0]; + const pageName = parts[1] || 'self'; + + const did = await resolveHandle({ handle: handle as `${string}.${string}` }); + if (!did) throw new Error('Could not resolve handle'); + + const records = await listRecords({ did, collection: 'app.blento.card' }); + const targetPage = 'blento.' + pageName; + + const copiedCards: Item[] = records + .map((r) => ({ ...r.value }) as Item) + .filter((card) => { + // v0/v1 cards without page field belong to blento.self + if (!card.page) return targetPage === 'blento.self'; + return card.page === targetPage; + }) + .map((card) => { + // Apply v0→v1 migration (coords were halved in old format) + if (!card.version) { + card.x *= 2; + card.y *= 2; + card.h *= 2; + card.w *= 2; + card.mobileX *= 2; + card.mobileY *= 2; + card.mobileH *= 2; + card.mobileW *= 2; + card.version = 1; + } + + // Convert blob refs to CDN URLs using source DID + if (card.cardData) { + for (const key of Object.keys(card.cardData)) { + const val = card.cardData[key]; + if (val && typeof val === 'object' && val.$type === 'blob') { + const url = getCDNImageBlobUrl({ did, blob: val }); + if (url) card.cardData[key] = url; + } + } + } + + // Regenerate ID and assign to current page + card.id = TID.now(); + card.page = data.page; + return card; + }); + + if (copiedCards.length === 0) { + toast.error('No cards found on that page'); + return; + } + + fixAllCollisions(copiedCards); + fixAllCollisions(copiedCards, true); + compactItems(copiedCards); + compactItems(copiedCards, true); + + items = copiedCards; + onLayoutChanged(); + toast.success(`Copied ${copiedCards.length} cards from ${handle}`); + } catch (e) { + console.error('Failed to copy page:', e); + toast.error('Failed to copy page'); + } finally { + isCopying = false; + } + } + let debugPoint = $state({ x: 0, y: 0 }); function getGridPosition( @@ -1152,9 +1427,21 @@ {#if dev}
- editedOn: {editedOn} + editedOn: {editedOn} + + { + if (e.key === 'Enter') copyPageFrom(); + }} + /> +
{/if} -- 2.51.2 From 6e47836b9ada2dc678c702f93f50a9df5c7ea8c1 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sat, 31 Jan 2026 23:31:00 +0100 Subject: [PATCH 19/21] commit --- src/lib/cards/FriendsCard/FriendsCard.svelte | 54 ++++++++------------ 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/src/lib/cards/FriendsCard/FriendsCard.svelte b/src/lib/cards/FriendsCard/FriendsCard.svelte index 08dd76f..c1ff18e 100644 --- a/src/lib/cards/FriendsCard/FriendsCard.svelte +++ b/src/lib/cards/FriendsCard/FriendsCard.svelte @@ -80,41 +80,27 @@ {/if} {:else} -
- {#each visibleProfiles as profile, i (profile.did)} - 0 && sizeClass === 'sm'} - class:-ml-5={i > 0 && sizeClass === 'md'} - class:-ml-6={i > 0 && sizeClass === 'lg'} - > - - - {/each} - {#if overflowCount > 0} -
- +
+ {#each profiles as profile (profile.did)} + - +{overflowCount} - -
- {/if} + + + {/each} +
{/if}
-- 2.51.2 From 28a155dfb819b7811177da00796005c7eb37b068 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sat, 31 Jan 2026 23:34:21 +0100 Subject: [PATCH 20/21] format --- .claude/settings.local.json | 62 +++++++++---------- src/lib/cards/FriendsCard/FriendsCard.svelte | 5 +- .../FriendsCard/FriendsCardSettings.svelte | 16 +---- src/lib/cards/FriendsCard/index.ts | 4 +- src/lib/website/EditableWebsite.svelte | 3 +- 5 files changed, 38 insertions(+), 52 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8345922..71d43d5 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,33 +1,33 @@ { - "permissions": { - "allow": [ - "Bash(pnpm check:*)", - "mcp__ide__getDiagnostics", - "mcp__plugin_svelte_svelte__svelte-autofixer", - "mcp__plugin_svelte_svelte__list-sections", - "Bash(pkill:*)", - "Bash(timeout 8 pnpm dev:*)", - "Bash(git checkout:*)", - "Bash(npx svelte-kit:*)", - "Bash(ls:*)", - "Bash(pnpm format:*)", - "Bash(pnpm add:*)", - "WebSearch", - "WebFetch(domain:github.com)", - "WebFetch(domain:flipclockjs.com)", - "WebFetch(domain:codepen.io)", - "WebFetch(domain:flo-bit.dev)", - "Bash(pnpm install)", - "Bash(pnpm install:*)", - "Bash(pnpm config:*)", - "Bash(lsof:*)", - "Bash(pnpm dev)", - "Bash(pnpm exec svelte-kit:*)", - "Bash(pnpm build:*)", - "Bash(pnpm remove:*)", - "Bash(grep:*)", - "Bash(find:*)", - "Bash(npx prettier:*)" - ] - } + "permissions": { + "allow": [ + "Bash(pnpm check:*)", + "mcp__ide__getDiagnostics", + "mcp__plugin_svelte_svelte__svelte-autofixer", + "mcp__plugin_svelte_svelte__list-sections", + "Bash(pkill:*)", + "Bash(timeout 8 pnpm dev:*)", + "Bash(git checkout:*)", + "Bash(npx svelte-kit:*)", + "Bash(ls:*)", + "Bash(pnpm format:*)", + "Bash(pnpm add:*)", + "WebSearch", + "WebFetch(domain:github.com)", + "WebFetch(domain:flipclockjs.com)", + "WebFetch(domain:codepen.io)", + "WebFetch(domain:flo-bit.dev)", + "Bash(pnpm install)", + "Bash(pnpm install:*)", + "Bash(pnpm config:*)", + "Bash(lsof:*)", + "Bash(pnpm dev)", + "Bash(pnpm exec svelte-kit:*)", + "Bash(pnpm build:*)", + "Bash(pnpm remove:*)", + "Bash(grep:*)", + "Bash(find:*)", + "Bash(npx prettier:*)" + ] + } } diff --git a/src/lib/cards/FriendsCard/FriendsCard.svelte b/src/lib/cards/FriendsCard/FriendsCard.svelte index c1ff18e..c4ac918 100644 --- a/src/lib/cards/FriendsCard/FriendsCard.svelte +++ b/src/lib/cards/FriendsCard/FriendsCard.svelte @@ -83,10 +83,7 @@ {@const olX = sizeClass === 'sm' ? 12 : sizeClass === 'md' ? 20 : 24} {@const olY = sizeClass === 'sm' ? 8 : sizeClass === 'md' ? 12 : 16}
-
+
{#each profiles as profile (profile.did)} getBlentoOrBskyProfile({ did: did as Did }).catch(() => undefined)) ); - profiles = results.filter( - (p): p is FriendsProfile => !!p && p.handle !== 'handle.invalid' - ); + profiles = results.filter((p): p is FriendsProfile => !!p && p.handle !== 'handle.invalid'); } function addFriend(actor: AppBskyActorDefs.ProfileViewBasic) { @@ -68,11 +66,7 @@ {#each dids as did (did)} {@const profile = getProfile(did)}
- + {profile?.handle ?? did} @@ -90,11 +84,7 @@ stroke="currentColor" class="size-3.5" > - +
diff --git a/src/lib/cards/FriendsCard/index.ts b/src/lib/cards/FriendsCard/index.ts index ca13d22..0afabcf 100644 --- a/src/lib/cards/FriendsCard/index.ts +++ b/src/lib/cards/FriendsCard/index.ts @@ -27,9 +27,7 @@ export const FriendsCardDefinition = { if (allDids.size === 0) return []; const profiles = await Promise.all( - Array.from(allDids).map((did) => - getBlentoOrBskyProfile({ did }).catch(() => undefined) - ) + Array.from(allDids).map((did) => getBlentoOrBskyProfile({ did }).catch(() => undefined)) ); return profiles.filter((p) => p && p.handle !== 'handle.invalid'); }, diff --git a/src/lib/website/EditableWebsite.svelte b/src/lib/website/EditableWebsite.svelte index c83c809..023a862 100644 --- a/src/lib/website/EditableWebsite.svelte +++ b/src/lib/website/EditableWebsite.svelte @@ -42,6 +42,7 @@ import Controls from './Controls.svelte'; import CardCommand from '$lib/components/card-command/CardCommand.svelte'; import { shouldMirror, mirrorLayout } from './layout-mirror'; + import { SvelteMap } from 'svelte/reactivity'; let { data @@ -262,7 +263,7 @@ function addAllCardTypes() { const groupOrder = ['Core', 'Social', 'Media', 'Content', 'Visual', 'Utilities', 'Games']; - const grouped = new Map(); + const grouped = new SvelteMap(); for (const def of AllCardDefinitions) { if (!def.name) continue; -- 2.51.2 From dbcafa860f2c6584b80eb6479ae5bdd4e8333075 Mon Sep 17 00:00:00 2001 From: polijn Date: Sat, 31 Jan 2026 23:38:32 +0100 Subject: [PATCH 21/21] fall trough fix --- src/lib/helper.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/lib/helper.ts b/src/lib/helper.ts index e542f72..0a6b596 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -57,12 +57,36 @@ export function fixCollisions( const pushDownCascade = (target: Item, blocker: Item) => { // Keep x fixed always when pushing down const fixedX = mobile ? target.mobileX : target.x; + const prevY = mobile ? target.mobileY : target.y; // We need target to move just below `blocker` const desiredY = mobile ? blocker.mobileY + blocker.mobileH : blocker.y + blocker.h; if (!mobile && target.y < desiredY) target.y = desiredY; if (mobile && target.mobileY < desiredY) target.mobileY = desiredY; + const newY = mobile ? target.mobileY : target.y; + const targetH = mobile ? target.mobileH : target.h; + + // fall trough fix + if (newY > prevY) { + const prevBottom = prevY + targetH; + const newBottom = newY + targetH; + for (const it of items) { + if (it === target || it === movedItem || it === blocker) continue; + const itY = mobile ? it.mobileY : it.y; + const itH = mobile ? it.mobileH : it.h; + const itBottom = itY + itH; + if (itBottom <= prevBottom || itY >= newBottom) continue; + // horizontal overlap check + const hOverlap = mobile + ? target.mobileX < it.mobileX + it.mobileW && target.mobileX + target.mobileW > it.mobileX + : target.x < it.x + it.w && target.x + target.w > it.x; + if (hOverlap) { + pushDownCascade(it, target); + } + } + } + // Now resolve any collisions that creates by pushing those items down first // Repeat until target is clean. while (true) {