From 5ec0e9de7abbe84bac026b5d3c1c48b84c115b8a Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Fri, 13 Feb 2026 18:10:19 +0100 Subject: [PATCH 1/3] margin.at card --- src/lib/cards/index.ts | 4 +- .../cards/social/MarginCard/MarginCard.svelte | 183 ++++++++++++++++++ .../MarginCard/MarginCardSettings.svelte | 63 ++++++ src/lib/cards/social/MarginCard/index.ts | 62 ++++++ 4 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 src/lib/cards/social/MarginCard/MarginCard.svelte create mode 100644 src/lib/cards/social/MarginCard/MarginCardSettings.svelte create mode 100644 src/lib/cards/social/MarginCard/index.ts diff --git a/src/lib/cards/index.ts b/src/lib/cards/index.ts index 5427783..e4bf070 100644 --- a/src/lib/cards/index.ts +++ b/src/lib/cards/index.ts @@ -47,6 +47,7 @@ import { LastFMTopTracksCardDefinition } from './media/LastFMCard/LastFMTopTrack import { LastFMTopAlbumsCardDefinition } from './media/LastFMCard/LastFMTopAlbumsCard'; import { LastFMProfileCardDefinition } from './media/LastFMCard/LastFMProfileCard'; import { PlyrFMCardDefinition } from './media/PlyrFMCard'; +import { MarginCardDefinition } from './social/MarginCard'; // import { Model3DCardDefinition } from './visual/Model3DCard'; export const AllCardDefinitions = [ @@ -98,7 +99,8 @@ export const AllCardDefinitions = [ LastFMTopTracksCardDefinition, LastFMTopAlbumsCardDefinition, LastFMProfileCardDefinition, - PlyrFMCardDefinition + PlyrFMCardDefinition, + MarginCardDefinition ] as const; export const CardDefinitionsByType = AllCardDefinitions.reduce( diff --git a/src/lib/cards/social/MarginCard/MarginCard.svelte b/src/lib/cards/social/MarginCard/MarginCard.svelte new file mode 100644 index 0000000..202385f --- /dev/null +++ b/src/lib/cards/social/MarginCard/MarginCard.svelte @@ -0,0 +1,183 @@ + + +
+ {#if filtered && filtered.length > 0} +
+ {#each filtered as entry (entry.uri)} + {@const source = + entry.type === 'bookmark' ? entry.value.source : entry.value.target?.source} + +
+ {#if entry.type === 'bookmark'} + + + + {:else if entry.type === 'annotation'} + + + + {:else} + + + + + {/if} + {entry.type} + + {new Date(entry.createdAt).toLocaleDateString()} + +
+ + {#if entry.type === 'bookmark' && entry.value.title} + + {truncate(entry.value.title as string, 80)} + + {/if} + + {#if entry.type === 'annotation' && entry.value.body?.value} + + {truncate(entry.value.body.value as string, 120)} + + {/if} + + {#if entry.type === 'highlight' && entry.value.target?.selector} + {@const selectors = Array.isArray(entry.value.target.selector) + ? entry.value.target.selector + : [entry.value.target.selector]} + {@const quote = selectors.find((s: any) => s.exact)?.exact} + {#if quote} + + {truncate(quote as string, 120)} + + {/if} + {/if} + + {#if source} + + {getDisplayUrl(source as string)} + + {/if} +
+ {/each} +
+ {:else if filtered} +
+ No margin entries yet. + {#if canEdit()} + + {/if} +
+ {:else} +
+ Loading... +
+ {/if} +
diff --git a/src/lib/cards/social/MarginCard/MarginCardSettings.svelte b/src/lib/cards/social/MarginCard/MarginCardSettings.svelte new file mode 100644 index 0000000..6ab4ae0 --- /dev/null +++ b/src/lib/cards/social/MarginCard/MarginCardSettings.svelte @@ -0,0 +1,63 @@ + + +
+
+ item.cardData.showBookmarks !== false, (val) => (item.cardData.showBookmarks = val) + } + id="show-bookmarks" + aria-labelledby="show-bookmarks-label" + variant="secondary" + /> + +
+ +
+ item.cardData.showAnnotations !== false, + (val) => (item.cardData.showAnnotations = val) + } + id="show-annotations" + aria-labelledby="show-annotations-label" + variant="secondary" + /> + +
+ +
+ item.cardData.showHighlights !== false, (val) => (item.cardData.showHighlights = val) + } + id="show-highlights" + aria-labelledby="show-highlights-label" + variant="secondary" + /> + +
+
diff --git a/src/lib/cards/social/MarginCard/index.ts b/src/lib/cards/social/MarginCard/index.ts new file mode 100644 index 0000000..41ecf4d --- /dev/null +++ b/src/lib/cards/social/MarginCard/index.ts @@ -0,0 +1,62 @@ +import type { CardDefinition } from '../../types'; +import { listRecords } from '$lib/atproto'; +import MarginCard from './MarginCard.svelte'; +import MarginCardSettings from './MarginCardSettings.svelte'; + +export type MarginEntry = { + type: 'bookmark' | 'annotation' | 'highlight'; + uri: string; + value: any; + createdAt: string; +}; + +export const MarginCardDefinition = { + type: 'margin', + contentComponent: MarginCard, + settingsComponent: MarginCardSettings, + createNew: (card) => { + card.w = 4; + card.mobileW = 8; + card.h = 4; + card.mobileH = 6; + }, + loadData: async (_items, { did }) => { + const [bookmarks, annotations, highlights] = await Promise.all([ + listRecords({ did, collection: 'at.margin.bookmark' }).catch(() => []), + listRecords({ did, collection: 'at.margin.annotation' }).catch(() => []), + listRecords({ did, collection: 'at.margin.highlight' }).catch(() => []) + ]); + + const entries: MarginEntry[] = [ + ...bookmarks.map((r: any) => ({ + type: 'bookmark' as const, + uri: r.uri, + value: r.value, + createdAt: r.value.createdAt + })), + ...annotations.map((r: any) => ({ + type: 'annotation' as const, + uri: r.uri, + value: r.value, + createdAt: r.value.createdAt + })), + ...highlights.map((r: any) => ({ + type: 'highlight' as const, + uri: r.uri, + value: r.value, + createdAt: r.value.createdAt + })) + ]; + + entries.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + + return entries; + }, + minH: 2, + canHaveLabel: true, + + keywords: ['margin', 'bookmarks', 'annotations', 'highlights', 'reading', 'web'], + groups: ['Social'], + name: 'Margin highlights, bookmarks, annotations', + icon: `` +} as CardDefinition & { type: 'margin' }; -- 2.51.2 From 303be86094ff02c9cb256c9013512c4d7aeefaa3 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Fri, 13 Feb 2026 19:03:36 +0100 Subject: [PATCH 2/3] add semble collection card --- src/lib/cards/index.ts | 4 +- .../cards/social/MarginCard/MarginCard.svelte | 4 +- .../CreateSembleCollectionCardModal.svelte | 43 ++++++ .../SembleCollectionCard.svelte | 123 ++++++++++++++++ .../social/SembleCollectionCard/index.ts | 133 ++++++++++++++++++ 5 files changed, 305 insertions(+), 2 deletions(-) create mode 100644 src/lib/cards/social/SembleCollectionCard/CreateSembleCollectionCardModal.svelte create mode 100644 src/lib/cards/social/SembleCollectionCard/SembleCollectionCard.svelte create mode 100644 src/lib/cards/social/SembleCollectionCard/index.ts diff --git a/src/lib/cards/index.ts b/src/lib/cards/index.ts index e4bf070..6b37d1e 100644 --- a/src/lib/cards/index.ts +++ b/src/lib/cards/index.ts @@ -48,6 +48,7 @@ import { LastFMTopAlbumsCardDefinition } from './media/LastFMCard/LastFMTopAlbum import { LastFMProfileCardDefinition } from './media/LastFMCard/LastFMProfileCard'; import { PlyrFMCardDefinition } from './media/PlyrFMCard'; import { MarginCardDefinition } from './social/MarginCard'; +import { SembleCollectionCardDefinition } from './social/SembleCollectionCard'; // import { Model3DCardDefinition } from './visual/Model3DCard'; export const AllCardDefinitions = [ @@ -100,7 +101,8 @@ export const AllCardDefinitions = [ LastFMTopAlbumsCardDefinition, LastFMProfileCardDefinition, PlyrFMCardDefinition, - MarginCardDefinition + MarginCardDefinition, + SembleCollectionCardDefinition ] as const; export const CardDefinitionsByType = AllCardDefinitions.reduce( diff --git a/src/lib/cards/social/MarginCard/MarginCard.svelte b/src/lib/cards/social/MarginCard/MarginCard.svelte index 202385f..96f70ff 100644 --- a/src/lib/cards/social/MarginCard/MarginCard.svelte +++ b/src/lib/cards/social/MarginCard/MarginCard.svelte @@ -137,7 +137,9 @@ {/if} {#if entry.type === 'annotation' && entry.value.body?.value} - + {truncate(entry.value.body.value as string, 120)} {/if} diff --git a/src/lib/cards/social/SembleCollectionCard/CreateSembleCollectionCardModal.svelte b/src/lib/cards/social/SembleCollectionCard/CreateSembleCollectionCardModal.svelte new file mode 100644 index 0000000..f8718df --- /dev/null +++ b/src/lib/cards/social/SembleCollectionCard/CreateSembleCollectionCardModal.svelte @@ -0,0 +1,43 @@ + + + + Enter a Semble collection URL + + + {#if errorMessage} + {errorMessage} + {/if} + +
+ + +
+
diff --git a/src/lib/cards/social/SembleCollectionCard/SembleCollectionCard.svelte b/src/lib/cards/social/SembleCollectionCard/SembleCollectionCard.svelte new file mode 100644 index 0000000..b9ce74e --- /dev/null +++ b/src/lib/cards/social/SembleCollectionCard/SembleCollectionCard.svelte @@ -0,0 +1,123 @@ + + +
+ {#if collectionData} +
+

+ {collectionData.name} +

+ {#if collectionData.description} +

+ {collectionData.description} +

+ {/if} +
+ + {#if collectionData.cards.length > 0} +
+ {#each collectionData.cards as card (card.uri)} + {#if card.type === 'URL' && card.url} + + {#if card.title} + + {truncate(card.title, 80)} + + {/if} + {#if card.description} + + {truncate(card.description, 120)} + + {/if} + + {getDisplayUrl(card.url)} + + + {:else if card.type === 'NOTE' && card.text} +
+ + {truncate(card.text, 200)} + +
+ {/if} + {/each} +
+ {:else} +
+ No cards in this collection yet. +
+ {/if} + {:else} +
+ Loading... +
+ {/if} +
diff --git a/src/lib/cards/social/SembleCollectionCard/index.ts b/src/lib/cards/social/SembleCollectionCard/index.ts new file mode 100644 index 0000000..78806d6 --- /dev/null +++ b/src/lib/cards/social/SembleCollectionCard/index.ts @@ -0,0 +1,133 @@ +import type { CardDefinition } from '../../types'; +import { listRecords, getRecord, resolveHandle } from '$lib/atproto'; +import SembleCollectionCard from './SembleCollectionCard.svelte'; +import CreateSembleCollectionCardModal from './CreateSembleCollectionCardModal.svelte'; + +export type SembleCard = { + uri: string; + type: 'URL' | 'NOTE'; + url?: string; + title?: string; + description?: string; + imageUrl?: string; + siteName?: string; + text?: string; + createdAt?: string; +}; + +export type SembleCollectionData = { + name: string; + description?: string; + cards: SembleCard[]; +}; + +function parseSembleUrl(url: string) { + const match = url.match(/^https?:\/\/semble\.so\/profile\/([^/]+)\/collections\/([a-z0-9]+)$/); + if (!match) return null; + return { handle: match[1], rkey: match[2] }; +} + +async function loadCollectionData( + handle: string, + collectionRkey: string +): Promise { + const did = await resolveHandle({ handle: handle as `${string}.${string}` }); + if (!did) return undefined; + + const collectionUri = `at://${did}/network.cosmik.collection/${collectionRkey}`; + + const [collection, allLinks, allCards] = await Promise.all([ + getRecord({ + did, + collection: 'network.cosmik.collection', + rkey: collectionRkey + }).catch(() => undefined), + listRecords({ did, collection: 'network.cosmik.collectionLink' }).catch(() => []), + listRecords({ did, collection: 'network.cosmik.card' }).catch(() => []) + ]); + + if (!collection) return undefined; + + const linkedCardUris = new Set( + allLinks + .filter((link: any) => link.value.collection?.uri === collectionUri) + .map((link: any) => link.value.card?.uri) + ); + + const cards: SembleCard[] = allCards + .filter((card: any) => linkedCardUris.has(card.uri)) + .map((card: any) => { + const v = card.value; + const content = v.content; + if (v.type === 'URL') { + return { + uri: card.uri, + type: 'URL' as const, + url: content?.url, + title: content?.metadata?.title, + description: content?.metadata?.description, + imageUrl: content?.metadata?.imageUrl, + siteName: content?.metadata?.siteName, + createdAt: v.createdAt + }; + } + return { + uri: card.uri, + type: 'NOTE' as const, + text: content?.text, + createdAt: v.createdAt + }; + }); + + return { + name: collection.value.name as string, + description: collection.value.description as string | undefined, + cards + }; +} + +export const SembleCollectionCardDefinition = { + type: 'sembleCollection', + contentComponent: SembleCollectionCard, + creationModalComponent: CreateSembleCollectionCardModal, + createNew: (card) => { + card.w = 4; + card.mobileW = 8; + card.h = 4; + card.mobileH = 6; + }, + loadData: async (items) => { + const results: Record = {}; + for (const item of items) { + const handle = item.cardData.handle; + const rkey = item.cardData.collectionRkey; + if (!handle || !rkey) continue; + try { + const data = await loadCollectionData(handle, rkey); + if (data) results[`${handle}/${rkey}`] = data; + } catch { + // skip failed fetches + } + } + return results; + }, + onUrlHandler: (url, item) => { + const parsed = parseSembleUrl(url); + if (!parsed) return null; + item.cardData.handle = parsed.handle; + item.cardData.collectionRkey = parsed.rkey; + item.cardData.href = url; + item.w = 4; + item.mobileW = 8; + item.h = 4; + item.mobileH = 6; + return item; + }, + urlHandlerPriority: 5, + minH: 2, + + keywords: ['semble', 'collection', 'bookmarks', 'links', 'cards', 'cosmik'], + groups: ['Social'], + name: 'Semble Collection', + icon: `` +} as CardDefinition & { type: 'sembleCollection' }; -- 2.51.2 From b23442197ffa9d42991be13d419baa3a78b60f5c Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:26:55 +0100 Subject: [PATCH 3/3] add icons --- src/lib/cards/social/BigSocialCard/index.ts | 56 ++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/src/lib/cards/social/BigSocialCard/index.ts b/src/lib/cards/social/BigSocialCard/index.ts index d41d055..1dd3de7 100644 --- a/src/lib/cards/social/BigSocialCard/index.ts +++ b/src/lib/cards/social/BigSocialCard/index.ts @@ -169,7 +169,11 @@ export const platformPatterns: Record = { mail: /(?:mailto:)/i, - npmx: /(?:npmx\.dev)/i + npmx: /(?:npmx\.dev)/i, + + roomy: /(?:roomy\.space)/i, + + blacksky: /(?:blacksky\.community)/i }; export const platformsData: Record = { @@ -302,6 +306,56 @@ export const platformsData: Record = { ` + }, + + blacksky: { + slug: 'blacksy', + path: '', + title: 'blacksy', + source: '', + svg: ` + + + + + + + + + + + + + + + + + + + + + +`, + hex: '080B0B' + }, + roomy: { + slug: 'roomy', + path: '', + title: 'roomy', + source: '', + hex: 'FFFFFF', + svg: ` + + + + + + + + + + +` } }; -- 2.51.2