tangled
alpha
login
or
join now
flo-bit.dev
/
blento
your personal website on atproto - mirror
blento.app
20
fork
atom
overview
issues
pulls
pipelines
guestbook card
unbedenklich
1 week ago
f356d837
cda2c9ee
+466
-7
10 changed files
expand all
collapse all
unified
split
.claude
settings.local.json
src
lib
atproto
index.ts
methods.ts
settings.ts
cards
GuestbookCard
CreateGuestbookCardModal.svelte
GuestbookCard.svelte
index.ts
index.ts
components
bluesky-post
BlueskyPost.svelte
post
Post.svelte
+2
-1
.claude/settings.local.json
···
24
"Bash(pnpm dev)",
25
"Bash(pnpm exec svelte-kit:*)",
26
"Bash(pnpm build:*)",
27
-
"Bash(pnpm remove:*)"
0
28
]
29
}
30
}
···
24
"Bash(pnpm dev)",
25
"Bash(pnpm exec svelte-kit:*)",
26
"Bash(pnpm build:*)",
27
+
"Bash(pnpm remove:*)",
28
+
"Bash(grep:*)"
29
]
30
}
31
}
+3
-1
src/lib/atproto/index.ts
···
16
getBlobURL,
17
getCDNImageBlobUrl,
18
searchActorsTypeahead,
19
-
getAuthorFeed
0
0
20
} from './methods';
···
16
getBlobURL,
17
getCDNImageBlobUrl,
18
searchActorsTypeahead,
19
+
getAuthorFeed,
20
+
getPostThread,
21
+
createPost
22
} from './methods';
+69
src/lib/atproto/methods.ts
···
465
return profile.did;
466
}
467
}
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
···
465
return profile.did;
466
}
467
}
468
+
469
+
/**
470
+
* Fetches a post's thread including replies.
471
+
* @param uri - The AT URI of the post
472
+
* @param depth - How many levels of replies to fetch (default 1)
473
+
* @param client - The client to use (defaults to public Bluesky API)
474
+
* @returns The thread data or undefined on failure
475
+
*/
476
+
export async function getPostThread({
477
+
uri,
478
+
depth = 1,
479
+
client
480
+
}: {
481
+
uri: string;
482
+
depth?: number;
483
+
client?: Client;
484
+
}) {
485
+
client ??= new Client({
486
+
handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' })
487
+
});
488
+
489
+
const response = await client.get('app.bsky.feed.getPostThread', {
490
+
params: { uri: uri as ResourceUri, depth }
491
+
});
492
+
493
+
if (!response.ok) return;
494
+
495
+
return response.data.thread;
496
+
}
497
+
498
+
/**
499
+
* Creates a Bluesky post on the authenticated user's account.
500
+
* @param text - The post text
501
+
* @param facets - Optional rich text facets (links, mentions, etc.)
502
+
* @returns The response containing the post's URI and CID
503
+
* @throws If the user is not logged in
504
+
*/
505
+
export async function createPost({
506
+
text,
507
+
facets
508
+
}: {
509
+
text: string;
510
+
facets?: Array<{
511
+
index: { byteStart: number; byteEnd: number };
512
+
features: Array<{ $type: string; uri?: string; did?: string; tag?: string }>;
513
+
}>;
514
+
}) {
515
+
if (!user.client || !user.did) throw new Error('No client or did');
516
+
517
+
const record: Record<string, unknown> = {
518
+
$type: 'app.bsky.feed.post',
519
+
text,
520
+
createdAt: new Date().toISOString()
521
+
};
522
+
523
+
if (facets) {
524
+
record.facets = facets;
525
+
}
526
+
527
+
const response = await user.client.post('com.atproto.repo.createRecord', {
528
+
input: {
529
+
collection: 'app.bsky.feed.post',
530
+
repo: user.did,
531
+
record
532
+
}
533
+
});
534
+
535
+
return response;
536
+
}
+1
src/lib/atproto/settings.ts
···
20
'app.blento.settings',
21
'app.blento.comment',
22
'app.blento.guestbook.entry',
0
23
'site.standard.publication',
24
'site.standard.document',
25
'xyz.statusphere.status'
···
20
'app.blento.settings',
21
'app.blento.comment',
22
'app.blento.guestbook.entry',
23
+
'app.bsky.feed.post',
24
'site.standard.publication',
25
'site.standard.document',
26
'xyz.statusphere.status'
+166
src/lib/cards/GuestbookCard/CreateGuestbookCardModal.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
···
1
+
<script lang="ts">
2
+
import { Alert, Button, Input, Modal, Subheading } from '@foxui/core';
3
+
import type { CreationModalComponentProps } from '../types';
4
+
import { createPost } from '$lib/atproto/methods';
5
+
import { user } from '$lib/atproto/auth.svelte';
6
+
import { parseBlueskyPostUrl } from '../BlueskyPostCard/utils';
7
+
8
+
let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props();
9
+
10
+
let mode = $state<'create' | 'existing'>('create');
11
+
12
+
const profileUrl = `https://blento.app/${user.profile?.handle ?? ''}`;
13
+
let postText = $state(`Comment on this post to appear on my Blento! ${profileUrl}`);
14
+
let postUrl = $state('');
15
+
let isPosting = $state(false);
16
+
let errorMessage = $state('');
17
+
18
+
function buildFacets(text: string, url: string) {
19
+
const encoder = new TextEncoder();
20
+
const encoded = encoder.encode(text);
21
+
const urlBytes = encoder.encode(url);
22
+
23
+
let byteStart = -1;
24
+
for (let i = 0; i <= encoded.length - urlBytes.length; i++) {
25
+
let match = true;
26
+
for (let j = 0; j < urlBytes.length; j++) {
27
+
if (encoded[i + j] !== urlBytes[j]) {
28
+
match = false;
29
+
break;
30
+
}
31
+
}
32
+
if (match) {
33
+
byteStart = i;
34
+
break;
35
+
}
36
+
}
37
+
38
+
if (byteStart === -1) return undefined;
39
+
40
+
return [
41
+
{
42
+
index: { byteStart, byteEnd: byteStart + urlBytes.length },
43
+
features: [{ $type: 'app.bsky.richtext.facet#link', uri: url }]
44
+
}
45
+
];
46
+
}
47
+
48
+
async function handleCreateNew() {
49
+
if (!postText.trim()) {
50
+
errorMessage = 'Post text cannot be empty.';
51
+
return;
52
+
}
53
+
54
+
isPosting = true;
55
+
errorMessage = '';
56
+
57
+
try {
58
+
const facets = buildFacets(postText, profileUrl);
59
+
const response = await createPost({ text: postText, facets });
60
+
61
+
if (!response.ok) {
62
+
throw new Error('Failed to create post');
63
+
}
64
+
65
+
item.cardData.uri = response.data.uri;
66
+
67
+
const rkey = response.data.uri.split('/').pop();
68
+
item.cardData.href = `https://bsky.app/profile/${user.profile?.handle}/post/${rkey}`;
69
+
70
+
oncreate();
71
+
} catch (err) {
72
+
errorMessage =
73
+
err instanceof Error ? err.message : 'Failed to create post. Please try again.';
74
+
} finally {
75
+
isPosting = false;
76
+
}
77
+
}
78
+
79
+
function handleExisting() {
80
+
errorMessage = '';
81
+
const parsed = parseBlueskyPostUrl(postUrl.trim());
82
+
83
+
if (!parsed) {
84
+
errorMessage =
85
+
'Invalid URL. Please enter a valid Bluesky post URL (e.g., https://bsky.app/profile/handle/post/...)';
86
+
return;
87
+
}
88
+
89
+
item.cardData.uri = `at://${parsed.handle}/app.bsky.feed.post/${parsed.rkey}`;
90
+
item.cardData.href = postUrl.trim();
91
+
92
+
oncreate();
93
+
}
94
+
95
+
async function handleSubmit() {
96
+
if (mode === 'create') {
97
+
await handleCreateNew();
98
+
} else {
99
+
handleExisting();
100
+
}
101
+
}
102
+
</script>
103
+
104
+
<Modal open={true} closeButton={false}>
105
+
<form
106
+
onsubmit={(e) => {
107
+
e.preventDefault();
108
+
handleSubmit();
109
+
}}
110
+
class="flex flex-col gap-2"
111
+
>
112
+
<Subheading>Guestbook</Subheading>
113
+
114
+
<div class="flex gap-2">
115
+
<Button
116
+
size="sm"
117
+
variant="ghost"
118
+
class={mode === 'create' ? 'bg-base-200 dark:bg-base-700' : ''}
119
+
onclick={() => (mode = 'create')}
120
+
>
121
+
Create new post
122
+
</Button>
123
+
<Button
124
+
size="sm"
125
+
variant="ghost"
126
+
class={mode === 'existing' ? 'bg-base-200 dark:bg-base-700' : ''}
127
+
onclick={() => (mode = 'existing')}
128
+
>
129
+
Use existing post
130
+
</Button>
131
+
</div>
132
+
133
+
{#if mode === 'create'}
134
+
<p class="text-base-500 dark:text-base-400 text-sm">
135
+
This will create a post on your Bluesky account. Replies to that post will appear on your
136
+
guestbook card.
137
+
</p>
138
+
<textarea
139
+
bind:value={postText}
140
+
rows="4"
141
+
class="bg-base-100 dark:bg-base-800 border-base-300 dark:border-base-600 mt-2 w-full rounded-lg border p-3 text-sm focus:outline-none"
142
+
></textarea>
143
+
{:else}
144
+
<p class="text-base-500 dark:text-base-400 text-sm">
145
+
Paste a Bluesky post URL to use as your guestbook. Replies to that post will appear on your
146
+
card.
147
+
</p>
148
+
<Input bind:value={postUrl} placeholder="https://bsky.app/profile/handle/post/..." />
149
+
{/if}
150
+
151
+
{#if errorMessage}
152
+
<Alert type="error" title="Error"><span>{errorMessage}</span></Alert>
153
+
{/if}
154
+
155
+
<div class="mt-4 flex justify-end gap-2">
156
+
<Button onclick={oncancel} variant="ghost">Cancel</Button>
157
+
{#if mode === 'create'}
158
+
<Button type="submit" disabled={isPosting || !postText.trim()}>
159
+
{isPosting ? 'Posting...' : 'Post to Bluesky & Create'}
160
+
</Button>
161
+
{:else}
162
+
<Button type="submit" disabled={!postUrl.trim()}>Create</Button>
163
+
{/if}
164
+
</div>
165
+
</form>
166
+
</Modal>
+126
src/lib/cards/GuestbookCard/GuestbookCard.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
···
1
+
<script lang="ts">
2
+
import { onMount } from 'svelte';
3
+
import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context';
4
+
import { CardDefinitionsByType } from '..';
5
+
import type { ContentComponentProps } from '../types';
6
+
import { Button } from '@foxui/core';
7
+
import { BlueskyPost } from '$lib/components/bluesky-post';
8
+
import type { PostView } from '@atcute/bluesky/types/app/feed/defs';
9
+
10
+
let { item }: ContentComponentProps = $props();
11
+
12
+
const data = getAdditionalUserData();
13
+
const did = getDidContext();
14
+
const handle = getHandleContext();
15
+
16
+
type Reply = {
17
+
$type: string;
18
+
post: PostView;
19
+
};
20
+
21
+
let isLoaded = $state(false);
22
+
23
+
let cardUri = $derived(item.cardData.uri as string);
24
+
25
+
// svelte-ignore state_referenced_locally
26
+
let replies = $state<Reply[]>(
27
+
((data['guestbook'] as Record<string, Reply[]>)?.[item.cardData.uri as string] ?? []) as Reply[]
28
+
);
29
+
30
+
onMount(async () => {
31
+
if (!cardUri) {
32
+
isLoaded = true;
33
+
return;
34
+
}
35
+
36
+
try {
37
+
const loaded = await CardDefinitionsByType[item.cardType]?.loadData?.([item], {
38
+
did,
39
+
handle
40
+
});
41
+
const result = loaded as Record<string, Reply[]> | undefined;
42
+
const freshReplies = result?.[cardUri] ?? [];
43
+
44
+
if (freshReplies.length > 0) {
45
+
replies = freshReplies;
46
+
}
47
+
48
+
if (!data['guestbook']) {
49
+
data['guestbook'] = {};
50
+
}
51
+
(data['guestbook'] as Record<string, Reply[]>)[cardUri] = replies;
52
+
} catch (e) {
53
+
console.error('Failed to load guestbook replies', e);
54
+
}
55
+
56
+
isLoaded = true;
57
+
});
58
+
</script>
59
+
60
+
<div class="flex h-full flex-col overflow-hidden p-4">
61
+
{#if item.cardData.href}
62
+
<div class="mb-2 flex justify-end">
63
+
<a href={item.cardData.href} target="_blank" rel="noopener noreferrer">
64
+
<Button size="sm">Add a comment on Bluesky</Button>
65
+
</a>
66
+
</div>
67
+
{/if}
68
+
69
+
<div class="flex-1 overflow-y-auto">
70
+
{#if replies.length > 0}
71
+
<div class="replies">
72
+
{#each replies as reply (reply.post.uri)}
73
+
<div class="reply">
74
+
<BlueskyPost feedViewPost={reply.post} showAvatar compact showLogo={false} />
75
+
</div>
76
+
{/each}
77
+
</div>
78
+
{:else if isLoaded}
79
+
<div
80
+
class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center text-center text-sm"
81
+
>
82
+
No comments yet — share your Bluesky post to get started!
83
+
</div>
84
+
{:else}
85
+
<div
86
+
class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center text-center text-sm"
87
+
>
88
+
Loading comments...
89
+
</div>
90
+
{/if}
91
+
</div>
92
+
</div>
93
+
94
+
<style>
95
+
.reply {
96
+
padding-bottom: 1rem;
97
+
margin-bottom: 1rem;
98
+
border-bottom: 1px solid oklch(0.5 0 0 / 0.1);
99
+
}
100
+
101
+
.reply:last-child {
102
+
border-bottom: none;
103
+
margin-bottom: 0;
104
+
padding-bottom: 0;
105
+
}
106
+
107
+
.reply :global(img:not([class*='rounded-full'])) {
108
+
max-height: 10rem;
109
+
}
110
+
111
+
.reply :global(article) {
112
+
max-height: 10rem;
113
+
}
114
+
115
+
@container card (width >= 30rem) {
116
+
.replies {
117
+
columns: 2;
118
+
column-gap: 1.5rem;
119
+
column-rule: 1px solid oklch(0.5 0 0 / 0.15);
120
+
}
121
+
122
+
.reply {
123
+
break-inside: avoid;
124
+
}
125
+
}
126
+
</style>
+64
src/lib/cards/GuestbookCard/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
0
0
···
1
+
import { getPostThread } from '$lib/atproto/methods';
2
+
import type { CardDefinition } from '../types';
3
+
import GuestbookCard from './GuestbookCard.svelte';
4
+
import CreateGuestbookCardModal from './CreateGuestbookCardModal.svelte';
5
+
6
+
export const GuestbookCardDefinition = {
7
+
type: 'guestbook',
8
+
contentComponent: GuestbookCard,
9
+
creationModalComponent: CreateGuestbookCardModal,
10
+
sidebarButtonText: 'Guestbook',
11
+
createNew: (card) => {
12
+
card.w = 4;
13
+
card.h = 6;
14
+
card.mobileW = 8;
15
+
card.mobileH = 12;
16
+
card.cardData.label = 'Guestbook';
17
+
},
18
+
minW: 4,
19
+
minH: 4,
20
+
defaultColor: 'base',
21
+
canHaveLabel: true,
22
+
loadData: async (items) => {
23
+
const uris = items
24
+
.filter((item) => item.cardData?.uri)
25
+
.map((item) => item.cardData.uri as string);
26
+
27
+
if (uris.length === 0) return {};
28
+
29
+
const results: Record<string, unknown[]> = {};
30
+
31
+
await Promise.all(
32
+
uris.map(async (uri) => {
33
+
try {
34
+
const thread = await getPostThread({ uri, depth: 1 });
35
+
if (thread && '$type' in thread && thread.$type === 'app.bsky.feed.defs#threadViewPost') {
36
+
const typedThread = thread as { replies?: unknown[] };
37
+
results[uri] = (typedThread.replies ?? [])
38
+
.filter(
39
+
(r: unknown) =>
40
+
r != null &&
41
+
typeof r === 'object' &&
42
+
'$type' in r &&
43
+
(r as { $type: string }).$type === 'app.bsky.feed.defs#threadViewPost'
44
+
)
45
+
.sort((a: unknown, b: unknown) => {
46
+
const timeA = new Date(
47
+
((a as any).post?.record?.createdAt as string) ?? 0
48
+
).getTime();
49
+
const timeB = new Date(
50
+
((b as any).post?.record?.createdAt as string) ?? 0
51
+
).getTime();
52
+
return timeB - timeA;
53
+
});
54
+
}
55
+
} catch (e) {
56
+
console.error('Failed to load guestbook thread for', uri, e);
57
+
}
58
+
})
59
+
);
60
+
61
+
return results;
62
+
},
63
+
name: 'Guestbook'
64
+
} as CardDefinition & { type: 'guestbook' };
+2
src/lib/cards/index.ts
···
32
import { TimerCardDefinition } from './TimerCard';
33
import { SpotifyCardDefinition } from './SpotifyCard';
34
import { ButtonCardDefinition } from './ButtonCard';
0
35
// import { Model3DCardDefinition } from './Model3DCard';
36
37
export const AllCardDefinitions = [
0
38
ButtonCardDefinition,
39
ImageCardDefinition,
40
VideoCardDefinition,
···
32
import { TimerCardDefinition } from './TimerCard';
33
import { SpotifyCardDefinition } from './SpotifyCard';
34
import { ButtonCardDefinition } from './ButtonCard';
35
+
import { GuestbookCardDefinition } from './GuestbookCard';
36
// import { Model3DCardDefinition } from './Model3DCard';
37
38
export const AllCardDefinitions = [
39
+
GuestbookCardDefinition,
40
ButtonCardDefinition,
41
ImageCardDefinition,
42
VideoCardDefinition,
+11
-1
src/lib/components/bluesky-post/BlueskyPost.svelte
···
8
feedViewPost,
9
children,
10
showLogo = false,
0
0
11
...restProps
12
-
}: { feedViewPost?: PostView; children?: Snippet; showLogo?: boolean } = $props();
0
0
0
0
0
0
13
14
const postData = $derived(feedViewPost ? blueskyPostToPostData(feedViewPost) : undefined);
15
</script>
···
37
likeHref={postData?.href}
38
showBookmark={false}
39
logo={showLogo ? logo : undefined}
0
0
40
{...restProps}
41
>
42
{@render children?.()}
···
8
feedViewPost,
9
children,
10
showLogo = false,
11
+
showAvatar = false,
12
+
compact = false,
13
...restProps
14
+
}: {
15
+
feedViewPost?: PostView;
16
+
children?: Snippet;
17
+
showLogo?: boolean;
18
+
showAvatar?: boolean;
19
+
compact?: boolean;
20
+
} = $props();
21
22
const postData = $derived(feedViewPost ? blueskyPostToPostData(feedViewPost) : undefined);
23
</script>
···
45
likeHref={postData?.href}
46
showBookmark={false}
47
logo={showLogo ? logo : undefined}
48
+
{showAvatar}
49
+
{compact}
50
{...restProps}
51
>
52
{@render children?.()}
+22
-4
src/lib/components/post/Post.svelte
···
36
37
children,
38
39
-
logo
0
0
0
40
}: WithElementRef<WithChildren<HTMLAttributes<HTMLDivElement>>> & {
41
data: PostData;
42
class?: string;
···
61
customActions?: Snippet;
62
63
logo?: Snippet;
0
0
0
64
} = $props();
65
</script>
66
···
121
</div>
122
{/if}
123
<div class="flex gap-4">
0
0
0
0
0
0
0
0
0
124
<div class="w-full">
125
<div class="mb-1 flex items-start justify-between gap-2">
126
<div class="flex items-start gap-4">
···
161
{/if}
162
163
<div
164
-
class="text-base-600 dark:text-base-400 accent:text-accent-950 block text-sm no-underline"
0
0
0
165
>
166
<RelativeTime date={new Date(data.createdAt)} locale="en" />
167
</div>
···
173
</div>
174
175
<Prose
176
-
size="md"
177
class="accent:prose-a:text-accent-950 accent:text-base-900 accent:prose-p:text-base-900 accent:prose-a:underline"
178
>
179
{#if data.htmlContent}
···
185
186
<PostEmbed {data} />
187
188
-
{#if showReply || showRepost || showLike || showBookmark || customActions}
189
<div
190
class="text-base-500 dark:text-base-400 accent:text-base-900 mt-4 flex justify-between gap-2"
191
>
···
36
37
children,
38
39
+
logo,
40
+
41
+
showAvatar = false,
42
+
compact = false
43
}: WithElementRef<WithChildren<HTMLAttributes<HTMLDivElement>>> & {
44
data: PostData;
45
class?: string;
···
64
customActions?: Snippet;
65
66
logo?: Snippet;
67
+
68
+
showAvatar?: boolean;
69
+
compact?: boolean;
70
} = $props();
71
</script>
72
···
127
</div>
128
{/if}
129
<div class="flex gap-4">
130
+
{#if showAvatar && data.author.avatar}
131
+
<a href={data.author.href} class="flex-shrink-0">
132
+
<img
133
+
src={data.author.avatar}
134
+
alt=""
135
+
class={compact ? 'size-7 rounded-full object-cover' : 'size-10 rounded-full object-cover'}
136
+
/>
137
+
</a>
138
+
{/if}
139
<div class="w-full">
140
<div class="mb-1 flex items-start justify-between gap-2">
141
<div class="flex items-start gap-4">
···
176
{/if}
177
178
<div
179
+
class={cn(
180
+
'text-base-600 dark:text-base-400 accent:text-accent-950 block no-underline',
181
+
compact ? 'text-xs' : 'text-sm'
182
+
)}
183
>
184
<RelativeTime date={new Date(data.createdAt)} locale="en" />
185
</div>
···
191
</div>
192
193
<Prose
194
+
size={compact ? 'default' : 'md'}
195
class="accent:prose-a:text-accent-950 accent:text-base-900 accent:prose-p:text-base-900 accent:prose-a:underline"
196
>
197
{#if data.htmlContent}
···
203
204
<PostEmbed {data} />
205
206
+
{#if !compact && (showReply || showRepost || showLike || showBookmark || customActions)}
207
<div
208
class="text-base-500 dark:text-base-400 accent:text-base-900 mt-4 flex justify-between gap-2"
209
>