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}
+
+ {: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}
+
+ {: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