tangled
alpha
login
or
join now
flo-bit.dev
/
blento
21
fork
atom
your personal website on atproto - mirror
blento.app
21
fork
atom
overview
issues
pulls
pipelines
margin.at card
Florian
4 weeks ago
5ec0e9de
1bc6705d
+311
-1
4 changed files
expand all
collapse all
unified
split
src
lib
cards
index.ts
social
MarginCard
MarginCard.svelte
MarginCardSettings.svelte
index.ts
+3
-1
src/lib/cards/index.ts
···
47
import { LastFMTopAlbumsCardDefinition } from './media/LastFMCard/LastFMTopAlbumsCard';
48
import { LastFMProfileCardDefinition } from './media/LastFMCard/LastFMProfileCard';
49
import { PlyrFMCardDefinition } from './media/PlyrFMCard';
0
50
// import { Model3DCardDefinition } from './visual/Model3DCard';
51
52
export const AllCardDefinitions = [
···
98
LastFMTopTracksCardDefinition,
99
LastFMTopAlbumsCardDefinition,
100
LastFMProfileCardDefinition,
101
-
PlyrFMCardDefinition
0
102
] as const;
103
104
export const CardDefinitionsByType = AllCardDefinitions.reduce(
···
47
import { LastFMTopAlbumsCardDefinition } from './media/LastFMCard/LastFMTopAlbumsCard';
48
import { LastFMProfileCardDefinition } from './media/LastFMCard/LastFMProfileCard';
49
import { PlyrFMCardDefinition } from './media/PlyrFMCard';
50
+
import { MarginCardDefinition } from './social/MarginCard';
51
// import { Model3DCardDefinition } from './visual/Model3DCard';
52
53
export const AllCardDefinitions = [
···
99
LastFMTopTracksCardDefinition,
100
LastFMTopAlbumsCardDefinition,
101
LastFMProfileCardDefinition,
102
+
PlyrFMCardDefinition,
103
+
MarginCardDefinition
104
] as const;
105
106
export const CardDefinitionsByType = AllCardDefinitions.reduce(
+183
src/lib/cards/social/MarginCard/MarginCard.svelte
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
<script lang="ts">
2
+
import type { Item } from '$lib/types';
3
+
import { onMount } from 'svelte';
4
+
import {
5
+
getAdditionalUserData,
6
+
getCanEdit,
7
+
getDidContext,
8
+
getHandleContext
9
+
} from '$lib/website/context';
10
+
import { CardDefinitionsByType } from '../..';
11
+
import type { MarginEntry } from './index';
12
+
import { Button } from '@foxui/core';
13
+
14
+
let { item }: { item: Item } = $props();
15
+
16
+
const data = getAdditionalUserData();
17
+
// svelte-ignore state_referenced_locally
18
+
let entries = $state(data[item.cardType] as MarginEntry[] | undefined);
19
+
20
+
let did = getDidContext();
21
+
let handle = getHandleContext();
22
+
23
+
onMount(async () => {
24
+
if (!entries) {
25
+
entries = (await CardDefinitionsByType[item.cardType]?.loadData?.([], {
26
+
did,
27
+
handle
28
+
})) as MarginEntry[];
29
+
30
+
data[item.cardType] = entries;
31
+
}
32
+
});
33
+
34
+
let canEdit = getCanEdit();
35
+
36
+
let filtered = $derived(
37
+
entries?.filter((e) => {
38
+
if (e.type === 'bookmark' && item.cardData.showBookmarks === false) return false;
39
+
if (e.type === 'annotation' && item.cardData.showAnnotations === false) return false;
40
+
if (e.type === 'highlight' && item.cardData.showHighlights === false) return false;
41
+
return true;
42
+
})
43
+
);
44
+
45
+
function getMarginUrl(entry: MarginEntry) {
46
+
const rkey = entry.uri.split('/').pop();
47
+
return `https://margin.at/${handle}/${entry.type}/${rkey}`;
48
+
}
49
+
50
+
function getDisplayUrl(url: string) {
51
+
try {
52
+
const u = new URL(url);
53
+
return u.hostname + (u.pathname !== '/' ? u.pathname : '');
54
+
} catch {
55
+
return url;
56
+
}
57
+
}
58
+
59
+
function truncate(text: string, max: number) {
60
+
if (text.length <= max) return text;
61
+
return text.slice(0, max) + '…';
62
+
}
63
+
</script>
64
+
65
+
<div class={['flex h-full flex-col overflow-y-auto px-5 py-4', item.cardData.label ? 'pt-12' : '']}>
66
+
{#if filtered && filtered.length > 0}
67
+
<div class="flex flex-col gap-3">
68
+
{#each filtered as entry (entry.uri)}
69
+
{@const source =
70
+
entry.type === 'bookmark' ? entry.value.source : entry.value.target?.source}
71
+
<a
72
+
href={getMarginUrl(entry)}
73
+
target="_blank"
74
+
rel="noopener noreferrer"
75
+
class="bg-base-100 dark:bg-base-800 accent:bg-black/10 hover:bg-base-200 dark:hover:bg-base-700 accent:hover:bg-black/15 flex flex-col gap-1.5 rounded-xl px-5 py-3 transition-colors"
76
+
>
77
+
<div class="flex items-center gap-2 pb-1">
78
+
{#if entry.type === 'bookmark'}
79
+
<svg
80
+
xmlns="http://www.w3.org/2000/svg"
81
+
fill="none"
82
+
viewBox="0 0 24 24"
83
+
stroke-width="2"
84
+
stroke="currentColor"
85
+
class="text-accent-500 accent:text-black size-3.5 shrink-0"
86
+
>
87
+
<path
88
+
stroke-linecap="round"
89
+
stroke-linejoin="round"
90
+
d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z"
91
+
/>
92
+
</svg>
93
+
{:else if entry.type === 'annotation'}
94
+
<svg
95
+
xmlns="http://www.w3.org/2000/svg"
96
+
fill="none"
97
+
viewBox="0 0 24 24"
98
+
stroke-width="2"
99
+
stroke="currentColor"
100
+
class="text-accent-500 accent:text-black size-3.5 shrink-0"
101
+
>
102
+
<path
103
+
stroke-linecap="round"
104
+
stroke-linejoin="round"
105
+
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
106
+
/>
107
+
</svg>
108
+
{:else}
109
+
<svg
110
+
xmlns="http://www.w3.org/2000/svg"
111
+
viewBox="0 0 24 24"
112
+
fill="none"
113
+
stroke="currentColor"
114
+
stroke-width="2"
115
+
stroke-linecap="round"
116
+
stroke-linejoin="round"
117
+
class="text-accent-500 accent:text-black size-3.5 shrink-0"
118
+
>
119
+
<path d="m9 11-6 6v3h9l3-3" />
120
+
<path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" />
121
+
</svg>
122
+
{/if}
123
+
<span class="text-base-500 dark:text-base-400 accent:text-black/80 text-xs capitalize"
124
+
>{entry.type}</span
125
+
>
126
+
<span class="text-base-400 dark:text-base-500 accent:text-black/60 ml-auto text-xs">
127
+
{new Date(entry.createdAt).toLocaleDateString()}
128
+
</span>
129
+
</div>
130
+
131
+
{#if entry.type === 'bookmark' && entry.value.title}
132
+
<span
133
+
class="text-base-900 dark:text-base-100 accent:text-black text-sm leading-snug font-medium"
134
+
>
135
+
{truncate(entry.value.title as string, 80)}
136
+
</span>
137
+
{/if}
138
+
139
+
{#if entry.type === 'annotation' && entry.value.body?.value}
140
+
<span class="text-base-900 dark:text-base-100 accent:text-black text-sm leading-snug font-medium">
141
+
{truncate(entry.value.body.value as string, 120)}
142
+
</span>
143
+
{/if}
144
+
145
+
{#if entry.type === 'highlight' && entry.value.target?.selector}
146
+
{@const selectors = Array.isArray(entry.value.target.selector)
147
+
? entry.value.target.selector
148
+
: [entry.value.target.selector]}
149
+
{@const quote = selectors.find((s: any) => s.exact)?.exact}
150
+
{#if quote}
151
+
<span
152
+
class="text-base-700 dark:text-base-300 accent:text-black/80 border-accent-500 accent:border-black/60 border-l-2 pl-3 text-sm leading-snug italic"
153
+
>
154
+
{truncate(quote as string, 120)}
155
+
</span>
156
+
{/if}
157
+
{/if}
158
+
159
+
{#if source}
160
+
<span class="text-base-400 dark:text-base-500 accent:text-black/60 truncate text-xs">
161
+
{getDisplayUrl(source as string)}
162
+
</span>
163
+
{/if}
164
+
</a>
165
+
{/each}
166
+
</div>
167
+
{:else if filtered}
168
+
<div class="flex h-full w-full flex-col items-center justify-center gap-4 text-center text-sm">
169
+
No margin entries yet.
170
+
{#if canEdit()}
171
+
<Button href="https://margin.at" target="_blank" rel="noopener noreferrer">
172
+
Try Margin
173
+
</Button>
174
+
{/if}
175
+
</div>
176
+
{:else}
177
+
<div
178
+
class="text-base-500 dark:text-base-400 accent:text-black/60 flex h-full w-full items-center justify-center text-center text-sm"
179
+
>
180
+
Loading...
181
+
</div>
182
+
{/if}
183
+
</div>
+63
src/lib/cards/social/MarginCard/MarginCardSettings.svelte
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
<script lang="ts">
2
+
import type { SettingsComponentProps } from '../../types';
3
+
import { Checkbox, Label } from '@foxui/core';
4
+
5
+
let { item }: SettingsComponentProps = $props();
6
+
</script>
7
+
8
+
<div class="flex flex-col gap-3">
9
+
<div class="flex items-center space-x-2">
10
+
<Checkbox
11
+
bind:checked={
12
+
() => item.cardData.showBookmarks !== false, (val) => (item.cardData.showBookmarks = val)
13
+
}
14
+
id="show-bookmarks"
15
+
aria-labelledby="show-bookmarks-label"
16
+
variant="secondary"
17
+
/>
18
+
<Label
19
+
id="show-bookmarks-label"
20
+
for="show-bookmarks"
21
+
class="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
22
+
>
23
+
Bookmarks
24
+
</Label>
25
+
</div>
26
+
27
+
<div class="flex items-center space-x-2">
28
+
<Checkbox
29
+
bind:checked={
30
+
() => item.cardData.showAnnotations !== false,
31
+
(val) => (item.cardData.showAnnotations = val)
32
+
}
33
+
id="show-annotations"
34
+
aria-labelledby="show-annotations-label"
35
+
variant="secondary"
36
+
/>
37
+
<Label
38
+
id="show-annotations-label"
39
+
for="show-annotations"
40
+
class="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
41
+
>
42
+
Annotations
43
+
</Label>
44
+
</div>
45
+
46
+
<div class="flex items-center space-x-2">
47
+
<Checkbox
48
+
bind:checked={
49
+
() => item.cardData.showHighlights !== false, (val) => (item.cardData.showHighlights = val)
50
+
}
51
+
id="show-highlights"
52
+
aria-labelledby="show-highlights-label"
53
+
variant="secondary"
54
+
/>
55
+
<Label
56
+
id="show-highlights-label"
57
+
for="show-highlights"
58
+
class="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
59
+
>
60
+
Highlights
61
+
</Label>
62
+
</div>
63
+
</div>
+62
src/lib/cards/social/MarginCard/index.ts
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import type { CardDefinition } from '../../types';
2
+
import { listRecords } from '$lib/atproto';
3
+
import MarginCard from './MarginCard.svelte';
4
+
import MarginCardSettings from './MarginCardSettings.svelte';
5
+
6
+
export type MarginEntry = {
7
+
type: 'bookmark' | 'annotation' | 'highlight';
8
+
uri: string;
9
+
value: any;
10
+
createdAt: string;
11
+
};
12
+
13
+
export const MarginCardDefinition = {
14
+
type: 'margin',
15
+
contentComponent: MarginCard,
16
+
settingsComponent: MarginCardSettings,
17
+
createNew: (card) => {
18
+
card.w = 4;
19
+
card.mobileW = 8;
20
+
card.h = 4;
21
+
card.mobileH = 6;
22
+
},
23
+
loadData: async (_items, { did }) => {
24
+
const [bookmarks, annotations, highlights] = await Promise.all([
25
+
listRecords({ did, collection: 'at.margin.bookmark' }).catch(() => []),
26
+
listRecords({ did, collection: 'at.margin.annotation' }).catch(() => []),
27
+
listRecords({ did, collection: 'at.margin.highlight' }).catch(() => [])
28
+
]);
29
+
30
+
const entries: MarginEntry[] = [
31
+
...bookmarks.map((r: any) => ({
32
+
type: 'bookmark' as const,
33
+
uri: r.uri,
34
+
value: r.value,
35
+
createdAt: r.value.createdAt
36
+
})),
37
+
...annotations.map((r: any) => ({
38
+
type: 'annotation' as const,
39
+
uri: r.uri,
40
+
value: r.value,
41
+
createdAt: r.value.createdAt
42
+
})),
43
+
...highlights.map((r: any) => ({
44
+
type: 'highlight' as const,
45
+
uri: r.uri,
46
+
value: r.value,
47
+
createdAt: r.value.createdAt
48
+
}))
49
+
];
50
+
51
+
entries.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
52
+
53
+
return entries;
54
+
},
55
+
minH: 2,
56
+
canHaveLabel: true,
57
+
58
+
keywords: ['margin', 'bookmarks', 'annotations', 'highlights', 'reading', 'web'],
59
+
groups: ['Social'],
60
+
name: 'Margin highlights, bookmarks, annotations',
61
+
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.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z" /></svg>`
62
+
} as CardDefinition & { type: 'margin' };