@ewanc26/svelte-standard-site 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +108 -0
- package/dist/__tests__/content.test.d.ts +4 -0
- package/dist/__tests__/content.test.js +128 -0
- package/dist/client.d.ts +71 -0
- package/dist/client.js +307 -0
- package/dist/components/Comments.svelte +277 -0
- package/dist/components/Comments.svelte.d.ts +17 -0
- package/dist/components/DocumentCard.svelte +95 -0
- package/dist/components/DocumentCard.svelte.d.ts +11 -0
- package/dist/components/PublicationCard.svelte +54 -0
- package/dist/components/PublicationCard.svelte.d.ts +9 -0
- package/dist/components/StandardSiteLayout.svelte +102 -0
- package/dist/components/StandardSiteLayout.svelte.d.ts +18 -0
- package/dist/components/ThemeToggle.svelte +55 -0
- package/dist/components/ThemeToggle.svelte.d.ts +6 -0
- package/dist/components/common/DateDisplay.svelte +38 -0
- package/dist/components/common/DateDisplay.svelte.d.ts +11 -0
- package/dist/components/common/TagList.svelte +31 -0
- package/dist/components/common/TagList.svelte.d.ts +8 -0
- package/dist/components/common/ThemedCard.svelte +65 -0
- package/dist/components/common/ThemedCard.svelte.d.ts +11 -0
- package/dist/components/common/ThemedContainer.svelte +55 -0
- package/dist/components/common/ThemedContainer.svelte.d.ts +11 -0
- package/dist/components/common/ThemedText.svelte +75 -0
- package/dist/components/common/ThemedText.svelte.d.ts +11 -0
- package/dist/components/document/BlockRenderer.svelte +67 -0
- package/dist/components/document/BlockRenderer.svelte.d.ts +9 -0
- package/dist/components/document/CanvasRenderer.svelte +41 -0
- package/dist/components/document/CanvasRenderer.svelte.d.ts +22 -0
- package/dist/components/document/DocumentRenderer.svelte +68 -0
- package/dist/components/document/DocumentRenderer.svelte.d.ts +17 -0
- package/dist/components/document/InlineMath.svelte +41 -0
- package/dist/components/document/InlineMath.svelte.d.ts +7 -0
- package/dist/components/document/LeafletContentRenderer.svelte +64 -0
- package/dist/components/document/LeafletContentRenderer.svelte.d.ts +36 -0
- package/dist/components/document/LinearDocumentRenderer.svelte +45 -0
- package/dist/components/document/LinearDocumentRenderer.svelte.d.ts +18 -0
- package/dist/components/document/MarkdownRenderer.svelte +62 -0
- package/dist/components/document/MarkdownRenderer.svelte.d.ts +10 -0
- package/dist/components/document/RichText.svelte +272 -0
- package/dist/components/document/RichText.svelte.d.ts +18 -0
- package/dist/components/document/blocks/BlockquoteBlock.svelte +29 -0
- package/dist/components/document/blocks/BlockquoteBlock.svelte.d.ts +10 -0
- package/dist/components/document/blocks/BskyPostBlock.svelte +202 -0
- package/dist/components/document/blocks/BskyPostBlock.svelte.d.ts +13 -0
- package/dist/components/document/blocks/ButtonBlock.svelte +24 -0
- package/dist/components/document/blocks/ButtonBlock.svelte.d.ts +10 -0
- package/dist/components/document/blocks/CodeBlock.svelte +68 -0
- package/dist/components/document/blocks/CodeBlock.svelte.d.ts +12 -0
- package/dist/components/document/blocks/HeaderBlock.svelte +56 -0
- package/dist/components/document/blocks/HeaderBlock.svelte.d.ts +11 -0
- package/dist/components/document/blocks/HorizontalRuleBlock.svelte +14 -0
- package/dist/components/document/blocks/HorizontalRuleBlock.svelte.d.ts +6 -0
- package/dist/components/document/blocks/IframeBlock.svelte +32 -0
- package/dist/components/document/blocks/IframeBlock.svelte.d.ts +10 -0
- package/dist/components/document/blocks/ImageBlock.svelte +55 -0
- package/dist/components/document/blocks/ImageBlock.svelte.d.ts +25 -0
- package/dist/components/document/blocks/MathBlock.svelte +34 -0
- package/dist/components/document/blocks/MathBlock.svelte.d.ts +10 -0
- package/dist/components/document/blocks/PageBlock.svelte +66 -0
- package/dist/components/document/blocks/PageBlock.svelte.d.ts +10 -0
- package/dist/components/document/blocks/PollBlock.svelte +122 -0
- package/dist/components/document/blocks/PollBlock.svelte.d.ts +27 -0
- package/dist/components/document/blocks/TextBlock.svelte +26 -0
- package/dist/components/document/blocks/TextBlock.svelte.d.ts +11 -0
- package/dist/components/document/blocks/UnorderedListBlock.svelte +71 -0
- package/dist/components/document/blocks/UnorderedListBlock.svelte.d.ts +9 -0
- package/dist/components/document/blocks/WebsiteBlock.svelte +81 -0
- package/dist/components/document/blocks/WebsiteBlock.svelte.d.ts +21 -0
- package/dist/components/index.d.ts +11 -0
- package/dist/components/index.js +13 -0
- package/dist/config/env.d.ts +11 -0
- package/dist/config/env.js +26 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +23 -0
- package/dist/publisher.d.ts +193 -0
- package/dist/publisher.js +349 -0
- package/dist/schemas.d.ts +626 -0
- package/dist/schemas.js +113 -0
- package/dist/stores/index.d.ts +1 -0
- package/dist/stores/index.js +1 -0
- package/dist/stores/theme.d.ts +11 -0
- package/dist/stores/theme.js +67 -0
- package/dist/styles/base.css +188 -0
- package/dist/styles/themes.css +5 -0
- package/dist/types.d.ts +106 -0
- package/dist/types.js +4 -0
- package/dist/utils/agents.d.ts +35 -0
- package/dist/utils/agents.js +96 -0
- package/dist/utils/at-uri.d.ts +50 -0
- package/dist/utils/at-uri.js +71 -0
- package/dist/utils/cache.d.ts +14 -0
- package/dist/utils/cache.js +33 -0
- package/dist/utils/comments.d.ts +61 -0
- package/dist/utils/comments.js +159 -0
- package/dist/utils/content.d.ts +94 -0
- package/dist/utils/content.js +178 -0
- package/dist/utils/document.d.ts +23 -0
- package/dist/utils/document.js +33 -0
- package/dist/utils/theme-helpers.d.ts +34 -0
- package/dist/utils/theme-helpers.js +63 -0
- package/dist/utils/theme.d.ts +18 -0
- package/dist/utils/theme.js +24 -0
- package/dist/utils/verification.d.ts +129 -0
- package/dist/utils/verification.js +157 -0
- package/package.json +139 -0
- package/src/lib/__tests__/content.test.ts +155 -0
- package/src/lib/client.ts +368 -0
- package/src/lib/components/Comments.svelte +277 -0
- package/src/lib/components/DocumentCard.svelte +95 -0
- package/src/lib/components/PublicationCard.svelte +54 -0
- package/src/lib/components/StandardSiteLayout.svelte +102 -0
- package/src/lib/components/ThemeToggle.svelte +55 -0
- package/src/lib/components/common/DateDisplay.svelte +38 -0
- package/src/lib/components/common/TagList.svelte +31 -0
- package/src/lib/components/common/ThemedCard.svelte +65 -0
- package/src/lib/components/common/ThemedContainer.svelte +55 -0
- package/src/lib/components/common/ThemedText.svelte +75 -0
- package/src/lib/components/document/BlockRenderer.svelte +67 -0
- package/src/lib/components/document/CanvasRenderer.svelte +41 -0
- package/src/lib/components/document/DocumentRenderer.svelte +68 -0
- package/src/lib/components/document/InlineMath.svelte +41 -0
- package/src/lib/components/document/LeafletContentRenderer.svelte +64 -0
- package/src/lib/components/document/LinearDocumentRenderer.svelte +45 -0
- package/src/lib/components/document/MarkdownRenderer.svelte +62 -0
- package/src/lib/components/document/RichText.svelte +272 -0
- package/src/lib/components/document/blocks/BlockquoteBlock.svelte +29 -0
- package/src/lib/components/document/blocks/BskyPostBlock.svelte +202 -0
- package/src/lib/components/document/blocks/ButtonBlock.svelte +24 -0
- package/src/lib/components/document/blocks/CodeBlock.svelte +68 -0
- package/src/lib/components/document/blocks/HeaderBlock.svelte +56 -0
- package/src/lib/components/document/blocks/HorizontalRuleBlock.svelte +14 -0
- package/src/lib/components/document/blocks/IframeBlock.svelte +32 -0
- package/src/lib/components/document/blocks/ImageBlock.svelte +55 -0
- package/src/lib/components/document/blocks/MathBlock.svelte +34 -0
- package/src/lib/components/document/blocks/PageBlock.svelte +66 -0
- package/src/lib/components/document/blocks/PollBlock.svelte +122 -0
- package/src/lib/components/document/blocks/TextBlock.svelte +26 -0
- package/src/lib/components/document/blocks/UnorderedListBlock.svelte +71 -0
- package/src/lib/components/document/blocks/WebsiteBlock.svelte +81 -0
- package/src/lib/components/index.ts +15 -0
- package/src/lib/config/env.ts +31 -0
- package/src/lib/index.ts +104 -0
- package/src/lib/publisher.ts +489 -0
- package/src/lib/schemas.ts +137 -0
- package/src/lib/stores/index.ts +1 -0
- package/src/lib/stores/theme.ts +80 -0
- package/src/lib/styles/base.css +188 -0
- package/src/lib/styles/themes.css +5 -0
- package/src/lib/types.ts +116 -0
- package/src/lib/utils/agents.ts +124 -0
- package/src/lib/utils/at-uri.ts +89 -0
- package/src/lib/utils/cache.ts +46 -0
- package/src/lib/utils/comments.ts +217 -0
- package/src/lib/utils/content.ts +234 -0
- package/src/lib/utils/document.ts +41 -0
- package/src/lib/utils/theme-helpers.ts +87 -0
- package/src/lib/utils/theme.ts +33 -0
- package/src/lib/utils/verification.ts +180 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Comments component for displaying Bluesky replies
|
|
4
|
+
*
|
|
5
|
+
* Fetches and displays threaded replies from Bluesky as comments on your blog.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <Comments
|
|
10
|
+
* bskyPostUri="at://did:plc:xxx/app.bsky.feed.post/abc123"
|
|
11
|
+
* canonicalUrl="https://yourblog.com/posts/my-post"
|
|
12
|
+
* maxDepth={3}
|
|
13
|
+
* />
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { onMount } from 'svelte';
|
|
18
|
+
import { fetchComments, formatRelativeTime, type Comment } from '../utils/comments.js';
|
|
19
|
+
import { MessageSquare, ExternalLink, Heart } from '@lucide/svelte';
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
/** AT-URI of the Bluesky announcement post */
|
|
23
|
+
bskyPostUri: string;
|
|
24
|
+
/** Canonical URL of your blog post */
|
|
25
|
+
canonicalUrl: string;
|
|
26
|
+
/** Maximum nesting depth for replies (default: 3) */
|
|
27
|
+
maxDepth?: number;
|
|
28
|
+
/** Section title (default: "Comments") */
|
|
29
|
+
title?: string;
|
|
30
|
+
/** Show "Reply on Bluesky" link (default: true) */
|
|
31
|
+
showReplyLink?: boolean;
|
|
32
|
+
/** Additional CSS classes */
|
|
33
|
+
class?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let {
|
|
37
|
+
bskyPostUri,
|
|
38
|
+
canonicalUrl,
|
|
39
|
+
maxDepth = 3,
|
|
40
|
+
title = 'Comments',
|
|
41
|
+
showReplyLink = true,
|
|
42
|
+
class: className = ''
|
|
43
|
+
}: Props = $props();
|
|
44
|
+
|
|
45
|
+
let comments = $state<Comment[]>([]);
|
|
46
|
+
let loading = $state(true);
|
|
47
|
+
let error = $state<string | null>(null);
|
|
48
|
+
|
|
49
|
+
// Convert AT-URI to HTTPS URL for the reply link
|
|
50
|
+
const bskyUrl = $derived.by(() => {
|
|
51
|
+
const match = bskyPostUri.match(/^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/(.+)$/);
|
|
52
|
+
if (!match) return null;
|
|
53
|
+
const [, did, rkey] = match;
|
|
54
|
+
// We'll need to resolve the DID to a handle, but for now use DID
|
|
55
|
+
return `https://bsky.app/profile/${did}/post/${rkey}`;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
onMount(async () => {
|
|
59
|
+
try {
|
|
60
|
+
comments = await fetchComments({
|
|
61
|
+
bskyPostUri,
|
|
62
|
+
canonicalUrl,
|
|
63
|
+
maxDepth
|
|
64
|
+
});
|
|
65
|
+
} catch (err) {
|
|
66
|
+
error = err instanceof Error ? err.message : 'Failed to load comments';
|
|
67
|
+
console.error('Failed to fetch comments:', err);
|
|
68
|
+
} finally {
|
|
69
|
+
loading = false;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function CommentItem(comment: Comment) {
|
|
74
|
+
const authorUrl = `https://bsky.app/profile/${comment.author.handle}`;
|
|
75
|
+
const postUrl = $derived.by(() => {
|
|
76
|
+
const match = comment.uri.match(/^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/(.+)$/);
|
|
77
|
+
if (!match) return null;
|
|
78
|
+
return `https://bsky.app/profile/${comment.author.handle}/post/${match[2]}`;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
comment,
|
|
83
|
+
authorUrl,
|
|
84
|
+
postUrl
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<section class="comments-section {className}">
|
|
90
|
+
<header class="mb-6">
|
|
91
|
+
<h2 class="text-ink-900 dark:text-ink-50 flex items-center gap-2 text-2xl font-bold">
|
|
92
|
+
<MessageSquare size={24} />
|
|
93
|
+
{title}
|
|
94
|
+
</h2>
|
|
95
|
+
{#if showReplyLink && bskyUrl}
|
|
96
|
+
<a
|
|
97
|
+
href={bskyUrl}
|
|
98
|
+
target="_blank"
|
|
99
|
+
rel="noopener noreferrer"
|
|
100
|
+
class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 mt-2 inline-flex items-center gap-1 text-sm transition-colors"
|
|
101
|
+
>
|
|
102
|
+
Reply on Bluesky
|
|
103
|
+
<ExternalLink size={14} />
|
|
104
|
+
</a>
|
|
105
|
+
{/if}
|
|
106
|
+
</header>
|
|
107
|
+
|
|
108
|
+
{#if loading}
|
|
109
|
+
<div class="text-ink-600 dark:text-ink-400 py-8 text-center">
|
|
110
|
+
<div class="inline-block h-8 w-8 animate-spin rounded-full border-4 border-primary-600 border-t-transparent"></div>
|
|
111
|
+
<p class="mt-2">Loading comments...</p>
|
|
112
|
+
</div>
|
|
113
|
+
{:else if error}
|
|
114
|
+
<div class="border-canvas-200 bg-canvas-100 dark:border-canvas-700 dark:bg-canvas-800 rounded-lg border p-4">
|
|
115
|
+
<p class="text-ink-900 dark:text-ink-50 font-medium">Failed to load comments</p>
|
|
116
|
+
<p class="text-ink-600 dark:text-ink-400 mt-1 text-sm">{error}</p>
|
|
117
|
+
</div>
|
|
118
|
+
{:else if comments.length === 0}
|
|
119
|
+
<div class="text-ink-600 dark:text-ink-400 py-8 text-center">
|
|
120
|
+
<MessageSquare size={48} class="mx-auto mb-2 opacity-50" />
|
|
121
|
+
<p>No comments yet. Be the first to comment!</p>
|
|
122
|
+
{#if showReplyLink && bskyUrl}
|
|
123
|
+
<a
|
|
124
|
+
href={bskyUrl}
|
|
125
|
+
target="_blank"
|
|
126
|
+
rel="noopener noreferrer"
|
|
127
|
+
class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 mt-2 inline-flex items-center gap-1 transition-colors"
|
|
128
|
+
>
|
|
129
|
+
Start the conversation on Bluesky
|
|
130
|
+
<ExternalLink size={14} />
|
|
131
|
+
</a>
|
|
132
|
+
{/if}
|
|
133
|
+
</div>
|
|
134
|
+
{:else}
|
|
135
|
+
<div class="space-y-4">
|
|
136
|
+
{#each comments as comment}
|
|
137
|
+
{@const item = CommentItem(comment)}
|
|
138
|
+
<article
|
|
139
|
+
class="border-canvas-200 bg-canvas-50 dark:border-canvas-700 dark:bg-canvas-900 rounded-lg border p-4"
|
|
140
|
+
style="margin-left: {comment.depth * 1.5}rem"
|
|
141
|
+
>
|
|
142
|
+
<header class="mb-2 flex items-start justify-between">
|
|
143
|
+
<div class="flex items-center gap-3">
|
|
144
|
+
{#if comment.author.avatar}
|
|
145
|
+
<img
|
|
146
|
+
src={comment.author.avatar}
|
|
147
|
+
alt={comment.author.displayName || comment.author.handle}
|
|
148
|
+
class="h-10 w-10 rounded-full"
|
|
149
|
+
/>
|
|
150
|
+
{:else}
|
|
151
|
+
<div class="bg-primary-200 dark:bg-primary-800 flex h-10 w-10 items-center justify-center rounded-full">
|
|
152
|
+
<span class="text-primary-900 dark:text-primary-100 text-sm font-bold">
|
|
153
|
+
{(comment.author.displayName || comment.author.handle)[0].toUpperCase()}
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
156
|
+
{/if}
|
|
157
|
+
|
|
158
|
+
<div>
|
|
159
|
+
<a
|
|
160
|
+
href={item.authorUrl}
|
|
161
|
+
target="_blank"
|
|
162
|
+
rel="noopener noreferrer"
|
|
163
|
+
class="text-ink-900 dark:text-ink-50 hover:text-primary-600 dark:hover:text-primary-400 font-medium transition-colors"
|
|
164
|
+
>
|
|
165
|
+
{comment.author.displayName || comment.author.handle}
|
|
166
|
+
</a>
|
|
167
|
+
<p class="text-ink-600 dark:text-ink-400 text-sm">@{comment.author.handle}</p>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<time class="text-ink-600 dark:text-ink-400 text-sm" datetime={comment.createdAt}>
|
|
172
|
+
{formatRelativeTime(comment.createdAt)}
|
|
173
|
+
</time>
|
|
174
|
+
</header>
|
|
175
|
+
|
|
176
|
+
<p class="text-ink-900 dark:text-ink-50 mb-3 whitespace-pre-wrap">{comment.text}</p>
|
|
177
|
+
|
|
178
|
+
<footer class="text-ink-600 dark:text-ink-400 flex items-center gap-4 text-sm">
|
|
179
|
+
{#if comment.likeCount > 0}
|
|
180
|
+
<span class="flex items-center gap-1">
|
|
181
|
+
<Heart size={14} />
|
|
182
|
+
{comment.likeCount}
|
|
183
|
+
</span>
|
|
184
|
+
{/if}
|
|
185
|
+
|
|
186
|
+
{#if item.postUrl}
|
|
187
|
+
<a
|
|
188
|
+
href={item.postUrl}
|
|
189
|
+
target="_blank"
|
|
190
|
+
rel="noopener noreferrer"
|
|
191
|
+
class="hover:text-primary-600 dark:hover:text-primary-400 flex items-center gap-1 transition-colors"
|
|
192
|
+
>
|
|
193
|
+
View on Bluesky
|
|
194
|
+
<ExternalLink size={12} />
|
|
195
|
+
</a>
|
|
196
|
+
{/if}
|
|
197
|
+
</footer>
|
|
198
|
+
|
|
199
|
+
{#if comment.replies && comment.replies.length > 0}
|
|
200
|
+
<div class="mt-4 space-y-4">
|
|
201
|
+
{#each comment.replies as reply}
|
|
202
|
+
{@const replyItem = CommentItem(reply)}
|
|
203
|
+
<article
|
|
204
|
+
class="border-canvas-200 bg-canvas-100 dark:border-canvas-600 dark:bg-canvas-800 rounded-lg border p-4"
|
|
205
|
+
>
|
|
206
|
+
<header class="mb-2 flex items-start justify-between">
|
|
207
|
+
<div class="flex items-center gap-3">
|
|
208
|
+
{#if reply.author.avatar}
|
|
209
|
+
<img
|
|
210
|
+
src={reply.author.avatar}
|
|
211
|
+
alt={reply.author.displayName || reply.author.handle}
|
|
212
|
+
class="h-8 w-8 rounded-full"
|
|
213
|
+
/>
|
|
214
|
+
{:else}
|
|
215
|
+
<div class="bg-primary-200 dark:bg-primary-800 flex h-8 w-8 items-center justify-center rounded-full">
|
|
216
|
+
<span class="text-primary-900 dark:text-primary-100 text-xs font-bold">
|
|
217
|
+
{(reply.author.displayName || reply.author.handle)[0].toUpperCase()}
|
|
218
|
+
</span>
|
|
219
|
+
</div>
|
|
220
|
+
{/if}
|
|
221
|
+
|
|
222
|
+
<div>
|
|
223
|
+
<a
|
|
224
|
+
href={replyItem.authorUrl}
|
|
225
|
+
target="_blank"
|
|
226
|
+
rel="noopener noreferrer"
|
|
227
|
+
class="text-ink-900 dark:text-ink-50 hover:text-primary-600 dark:hover:text-primary-400 text-sm font-medium transition-colors"
|
|
228
|
+
>
|
|
229
|
+
{reply.author.displayName || reply.author.handle}
|
|
230
|
+
</a>
|
|
231
|
+
<p class="text-ink-600 dark:text-ink-400 text-xs">@{reply.author.handle}</p>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<time class="text-ink-600 dark:text-ink-400 text-xs" datetime={reply.createdAt}>
|
|
236
|
+
{formatRelativeTime(reply.createdAt)}
|
|
237
|
+
</time>
|
|
238
|
+
</header>
|
|
239
|
+
|
|
240
|
+
<p class="text-ink-900 dark:text-ink-50 mb-2 whitespace-pre-wrap text-sm">{reply.text}</p>
|
|
241
|
+
|
|
242
|
+
<footer class="text-ink-600 dark:text-ink-400 flex items-center gap-4 text-xs">
|
|
243
|
+
{#if reply.likeCount > 0}
|
|
244
|
+
<span class="flex items-center gap-1">
|
|
245
|
+
<Heart size={12} />
|
|
246
|
+
{reply.likeCount}
|
|
247
|
+
</span>
|
|
248
|
+
{/if}
|
|
249
|
+
|
|
250
|
+
{#if replyItem.postUrl}
|
|
251
|
+
<a
|
|
252
|
+
href={replyItem.postUrl}
|
|
253
|
+
target="_blank"
|
|
254
|
+
rel="noopener noreferrer"
|
|
255
|
+
class="hover:text-primary-600 dark:hover:text-primary-400 flex items-center gap-1 transition-colors"
|
|
256
|
+
>
|
|
257
|
+
View on Bluesky
|
|
258
|
+
<ExternalLink size={10} />
|
|
259
|
+
</a>
|
|
260
|
+
{/if}
|
|
261
|
+
</footer>
|
|
262
|
+
</article>
|
|
263
|
+
{/each}
|
|
264
|
+
</div>
|
|
265
|
+
{/if}
|
|
266
|
+
</article>
|
|
267
|
+
{/each}
|
|
268
|
+
</div>
|
|
269
|
+
{/if}
|
|
270
|
+
</section>
|
|
271
|
+
|
|
272
|
+
<style>
|
|
273
|
+
.comments-section {
|
|
274
|
+
margin-top: 3rem;
|
|
275
|
+
margin-bottom: 3rem;
|
|
276
|
+
}
|
|
277
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** AT-URI of the Bluesky announcement post */
|
|
3
|
+
bskyPostUri: string;
|
|
4
|
+
/** Canonical URL of your blog post */
|
|
5
|
+
canonicalUrl: string;
|
|
6
|
+
/** Maximum nesting depth for replies (default: 3) */
|
|
7
|
+
maxDepth?: number;
|
|
8
|
+
/** Section title (default: "Comments") */
|
|
9
|
+
title?: string;
|
|
10
|
+
/** Show "Reply on Bluesky" link (default: true) */
|
|
11
|
+
showReplyLink?: boolean;
|
|
12
|
+
/** Additional CSS classes */
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
declare const Comments: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type Comments = ReturnType<typeof Comments>;
|
|
17
|
+
export default Comments;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Document, Publication, AtProtoRecord } from '../types.js';
|
|
3
|
+
import { extractRkey } from '../index.js';
|
|
4
|
+
import { ThemedCard, ThemedText, DateDisplay, TagList } from './index.js';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
document: AtProtoRecord<Document>;
|
|
8
|
+
publication?: AtProtoRecord<Publication>;
|
|
9
|
+
class?: string;
|
|
10
|
+
showCover?: boolean;
|
|
11
|
+
href?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
document,
|
|
16
|
+
publication,
|
|
17
|
+
class: className = '',
|
|
18
|
+
showCover = true,
|
|
19
|
+
href: customHref
|
|
20
|
+
}: Props = $props();
|
|
21
|
+
|
|
22
|
+
const value = $derived(document.value);
|
|
23
|
+
const hasTheme = $derived(!!publication?.value.basicTheme);
|
|
24
|
+
|
|
25
|
+
const pubRkey = $derived(extractRkey(value.site));
|
|
26
|
+
const docRkey = $derived(extractRkey(document.uri));
|
|
27
|
+
const href = $derived(customHref || `/${pubRkey}/${docRkey}`);
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<ThemedCard
|
|
31
|
+
theme={publication?.value.basicTheme}
|
|
32
|
+
{href}
|
|
33
|
+
class="flex gap-6 duration-200 focus-within:shadow-md hover:shadow-md {className}"
|
|
34
|
+
>
|
|
35
|
+
{#if showCover && value.coverImage}
|
|
36
|
+
<img
|
|
37
|
+
src={value.coverImage}
|
|
38
|
+
alt="{value.title} cover"
|
|
39
|
+
class="h-48 w-32 shrink-0 rounded-lg object-cover shadow-sm"
|
|
40
|
+
/>
|
|
41
|
+
{/if}
|
|
42
|
+
|
|
43
|
+
<div class="flex-1">
|
|
44
|
+
<ThemedText
|
|
45
|
+
{hasTheme}
|
|
46
|
+
element="h3"
|
|
47
|
+
class="group-hover:text-primary-600 dark:group-hover:text-primary-400 mb-2 text-2xl font-bold transition-colors"
|
|
48
|
+
>
|
|
49
|
+
{value.title}
|
|
50
|
+
</ThemedText>
|
|
51
|
+
|
|
52
|
+
{#if value.description}
|
|
53
|
+
<ThemedText
|
|
54
|
+
{hasTheme}
|
|
55
|
+
opacity={70}
|
|
56
|
+
element="p"
|
|
57
|
+
class="mb-4 line-clamp-3 text-sm leading-relaxed"
|
|
58
|
+
>
|
|
59
|
+
{value.description}
|
|
60
|
+
</ThemedText>
|
|
61
|
+
{/if}
|
|
62
|
+
|
|
63
|
+
<div class="mb-4 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm">
|
|
64
|
+
<DateDisplay
|
|
65
|
+
date={value.publishedAt}
|
|
66
|
+
class="font-medium"
|
|
67
|
+
style={hasTheme
|
|
68
|
+
? `color: ${hasTheme ? 'color-mix(in srgb, var(--theme-foreground) 80%, transparent)' : ''}`
|
|
69
|
+
: ''}
|
|
70
|
+
/>
|
|
71
|
+
{#if value.updatedAt}
|
|
72
|
+
<span
|
|
73
|
+
class="flex items-center gap-1"
|
|
74
|
+
style:color={hasTheme
|
|
75
|
+
? 'color-mix(in srgb, var(--theme-foreground) 60%, transparent)'
|
|
76
|
+
: undefined}
|
|
77
|
+
>
|
|
78
|
+
<span
|
|
79
|
+
class="h-1 w-1 rounded-full"
|
|
80
|
+
class:bg-ink-400={!hasTheme}
|
|
81
|
+
class:dark:bg-ink-600={!hasTheme}
|
|
82
|
+
style:background-color={hasTheme
|
|
83
|
+
? 'color-mix(in srgb, var(--theme-foreground) 40%, transparent)'
|
|
84
|
+
: undefined}
|
|
85
|
+
></span>
|
|
86
|
+
Updated <DateDisplay date={value.updatedAt} />
|
|
87
|
+
</span>
|
|
88
|
+
{/if}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{#if value.tags && value.tags.length > 0}
|
|
92
|
+
<TagList tags={value.tags} {hasTheme} />
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
</ThemedCard>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Document, Publication, AtProtoRecord } from '../types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
document: AtProtoRecord<Document>;
|
|
4
|
+
publication?: AtProtoRecord<Publication>;
|
|
5
|
+
class?: string;
|
|
6
|
+
showCover?: boolean;
|
|
7
|
+
href?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const DocumentCard: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type DocumentCard = ReturnType<typeof DocumentCard>;
|
|
11
|
+
export default DocumentCard;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Publication, AtProtoRecord } from '../types.js';
|
|
3
|
+
import { ThemedCard, ThemedText } from './index.js';
|
|
4
|
+
import { ExternalLink } from '@lucide/svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
publication: AtProtoRecord<Publication>;
|
|
8
|
+
class?: string;
|
|
9
|
+
showExternalIcon?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { publication, class: className = '', showExternalIcon = true }: Props = $props();
|
|
13
|
+
|
|
14
|
+
const value = $derived(publication.value);
|
|
15
|
+
const hasTheme = $derived(!!value.basicTheme);
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<ThemedCard theme={value.basicTheme} class="focus-within:shadow-lg hover:shadow-lg {className}">
|
|
19
|
+
<div class="mb-4 flex gap-4">
|
|
20
|
+
{#if value.icon}
|
|
21
|
+
<img
|
|
22
|
+
src={value.icon}
|
|
23
|
+
alt="{value.name} icon"
|
|
24
|
+
class="size-16 shrink-0 rounded-lg object-cover"
|
|
25
|
+
/>
|
|
26
|
+
{/if}
|
|
27
|
+
<div class="min-w-0 flex-1">
|
|
28
|
+
<ThemedText {hasTheme} element="h3" class="mb-2 text-xl font-semibold">
|
|
29
|
+
{value.name}
|
|
30
|
+
</ThemedText>
|
|
31
|
+
{#if value.description}
|
|
32
|
+
<ThemedText {hasTheme} opacity={70} element="p" class="text-sm">
|
|
33
|
+
{value.description}
|
|
34
|
+
</ThemedText>
|
|
35
|
+
{/if}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<a
|
|
39
|
+
href={value.url}
|
|
40
|
+
target="_blank"
|
|
41
|
+
rel="noopener noreferrer"
|
|
42
|
+
class="inline-flex items-center gap-2 font-medium transition-opacity hover:opacity-80 focus-visible:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
43
|
+
class:text-primary-600={!hasTheme}
|
|
44
|
+
class:dark:text-primary-400={!hasTheme}
|
|
45
|
+
class:focus-visible:outline-primary-600={!hasTheme}
|
|
46
|
+
style:color={hasTheme ? 'var(--theme-accent)' : undefined}
|
|
47
|
+
style:outline-color={hasTheme ? 'var(--theme-accent)' : undefined}
|
|
48
|
+
>
|
|
49
|
+
Visit Site
|
|
50
|
+
{#if showExternalIcon}
|
|
51
|
+
<ExternalLink class="size-4" aria-hidden="true" />
|
|
52
|
+
{/if}
|
|
53
|
+
</a>
|
|
54
|
+
</ThemedCard>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Publication, AtProtoRecord } from '../types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
publication: AtProtoRecord<Publication>;
|
|
4
|
+
class?: string;
|
|
5
|
+
showExternalIcon?: boolean;
|
|
6
|
+
}
|
|
7
|
+
declare const PublicationCard: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type PublicationCard = ReturnType<typeof PublicationCard>;
|
|
9
|
+
export default PublicationCard;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import ThemeToggle from './ThemeToggle.svelte';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Site title */
|
|
7
|
+
title?: string;
|
|
8
|
+
/** Header slot for custom header content */
|
|
9
|
+
header?: Snippet;
|
|
10
|
+
/** Footer slot for custom footer content */
|
|
11
|
+
footer?: Snippet;
|
|
12
|
+
/** Main content */
|
|
13
|
+
children: Snippet;
|
|
14
|
+
/** Additional CSS classes for the main container */
|
|
15
|
+
class?: string;
|
|
16
|
+
/** Show theme toggle in default header */
|
|
17
|
+
showThemeToggle?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
title = 'My Site',
|
|
22
|
+
header,
|
|
23
|
+
footer,
|
|
24
|
+
children,
|
|
25
|
+
class: className = '',
|
|
26
|
+
showThemeToggle = true
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<svelte:head>
|
|
31
|
+
<script>
|
|
32
|
+
// Prevent flash of unstyled content (FOUC) by applying theme before page renders
|
|
33
|
+
(function () {
|
|
34
|
+
const stored = localStorage.getItem('theme');
|
|
35
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
36
|
+
const isDark = stored === 'dark' || (!stored && prefersDark);
|
|
37
|
+
const htmlElement = document.documentElement;
|
|
38
|
+
|
|
39
|
+
if (isDark) {
|
|
40
|
+
htmlElement.classList.add('dark');
|
|
41
|
+
htmlElement.style.colorScheme = 'dark';
|
|
42
|
+
} else {
|
|
43
|
+
htmlElement.classList.remove('dark');
|
|
44
|
+
htmlElement.style.colorScheme = 'light';
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
</script>
|
|
48
|
+
</svelte:head>
|
|
49
|
+
|
|
50
|
+
<div
|
|
51
|
+
class="bg-canvas-50 text-ink-900 dark:bg-canvas-950 dark:text-ink-50 flex min-h-screen flex-col overflow-x-hidden"
|
|
52
|
+
>
|
|
53
|
+
{#if header}
|
|
54
|
+
{@render header()}
|
|
55
|
+
{:else}
|
|
56
|
+
<header
|
|
57
|
+
class="border-canvas-200 bg-canvas-50/90 dark:border-canvas-800 dark:bg-canvas-950/90 sticky top-0 z-50 w-full border-b backdrop-blur-md"
|
|
58
|
+
>
|
|
59
|
+
<nav
|
|
60
|
+
class="container mx-auto flex items-center justify-between px-4 py-4"
|
|
61
|
+
aria-label="Main navigation"
|
|
62
|
+
>
|
|
63
|
+
<a
|
|
64
|
+
href="/"
|
|
65
|
+
class="text-ink-900 hover:text-primary-600 focus-visible:text-primary-600 focus-visible:outline-primary-600 dark:text-ink-50 dark:hover:text-primary-400 dark:focus-visible:text-primary-400 text-xl font-bold transition-colors focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
66
|
+
>
|
|
67
|
+
{title}
|
|
68
|
+
</a>
|
|
69
|
+
|
|
70
|
+
{#if showThemeToggle}
|
|
71
|
+
<ThemeToggle />
|
|
72
|
+
{/if}
|
|
73
|
+
</nav>
|
|
74
|
+
</header>
|
|
75
|
+
{/if}
|
|
76
|
+
|
|
77
|
+
<main id="main-content" class="container mx-auto grow px-4 py-8 {className}" tabindex="-1">
|
|
78
|
+
{@render children()}
|
|
79
|
+
</main>
|
|
80
|
+
|
|
81
|
+
{#if footer}
|
|
82
|
+
{@render footer()}
|
|
83
|
+
{:else}
|
|
84
|
+
<footer
|
|
85
|
+
class="border-canvas-200 bg-canvas-50 dark:border-canvas-800 dark:bg-canvas-950 mt-auto w-full border-t py-6"
|
|
86
|
+
>
|
|
87
|
+
<div class="text-ink-700 dark:text-ink-200 container mx-auto px-4 text-center text-sm">
|
|
88
|
+
<p>
|
|
89
|
+
© {new Date().getFullYear()}
|
|
90
|
+
{title}. Powered by svelte-standard-site and the AT Protocol. Created by
|
|
91
|
+
<a
|
|
92
|
+
href="https://ewancroft.uk"
|
|
93
|
+
target="_blank"
|
|
94
|
+
rel="noopener noreferrer"
|
|
95
|
+
class="text-primary-600 hover:text-primary-700 focus-visible:outline-primary-600 dark:text-primary-400 dark:hover:text-primary-500 font-semibold transition focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
96
|
+
>Ewan Croft</a
|
|
97
|
+
>.
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
</footer>
|
|
101
|
+
{/if}
|
|
102
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Site title */
|
|
4
|
+
title?: string;
|
|
5
|
+
/** Header slot for custom header content */
|
|
6
|
+
header?: Snippet;
|
|
7
|
+
/** Footer slot for custom footer content */
|
|
8
|
+
footer?: Snippet;
|
|
9
|
+
/** Main content */
|
|
10
|
+
children: Snippet;
|
|
11
|
+
/** Additional CSS classes for the main container */
|
|
12
|
+
class?: string;
|
|
13
|
+
/** Show theme toggle in default header */
|
|
14
|
+
showThemeToggle?: boolean;
|
|
15
|
+
}
|
|
16
|
+
declare const StandardSiteLayout: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type StandardSiteLayout = ReturnType<typeof StandardSiteLayout>;
|
|
18
|
+
export default StandardSiteLayout;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { Sun, Moon } from '@lucide/svelte';
|
|
4
|
+
import { themeStore } from '../stores/index.js';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { class: className = '' }: Props = $props();
|
|
11
|
+
|
|
12
|
+
let isDark = $state(false);
|
|
13
|
+
let mounted = $state(false);
|
|
14
|
+
|
|
15
|
+
onMount(() => {
|
|
16
|
+
themeStore.init();
|
|
17
|
+
|
|
18
|
+
const unsubscribe = themeStore.subscribe((state) => {
|
|
19
|
+
isDark = state.isDark;
|
|
20
|
+
mounted = state.mounted;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return unsubscribe;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function toggle() {
|
|
27
|
+
themeStore.toggle();
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<button
|
|
32
|
+
onclick={toggle}
|
|
33
|
+
class="bg-canvas-200 text-ink-900 hover:bg-canvas-300 focus-visible:outline-primary-600 dark:bg-canvas-800 dark:text-ink-50 dark:hover:bg-canvas-700 relative flex h-10 w-10 items-center justify-center rounded-lg transition-all focus-visible:outline-2 focus-visible:outline-offset-2 {className}"
|
|
34
|
+
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
35
|
+
type="button"
|
|
36
|
+
>
|
|
37
|
+
{#if mounted}
|
|
38
|
+
<div class="relative h-5 w-5">
|
|
39
|
+
<Sun
|
|
40
|
+
class="absolute inset-0 h-5 w-5 transition-all duration-300 {isDark
|
|
41
|
+
? 'scale-0 -rotate-90 opacity-0'
|
|
42
|
+
: 'scale-100 rotate-0 opacity-100'}"
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
/>
|
|
45
|
+
<Moon
|
|
46
|
+
class="absolute inset-0 h-5 w-5 transition-all duration-300 {isDark
|
|
47
|
+
? 'scale-100 rotate-0 opacity-100'
|
|
48
|
+
: 'scale-0 rotate-90 opacity-0'}"
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
{:else}
|
|
53
|
+
<div class="bg-canvas-300 dark:bg-canvas-700 h-5 w-5 animate-pulse rounded"></div>
|
|
54
|
+
{/if}
|
|
55
|
+
</button>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
date: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
class?: string;
|
|
6
|
+
showIcon?: boolean;
|
|
7
|
+
style?: string;
|
|
8
|
+
locale?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { date, label, class: className = '', showIcon = false, style, locale }: Props = $props();
|
|
12
|
+
|
|
13
|
+
function formatDate(dateString: string): string {
|
|
14
|
+
// Use browser's locale if not specified
|
|
15
|
+
const userLocale = locale || navigator.language || 'en-US';
|
|
16
|
+
|
|
17
|
+
return new Date(dateString).toLocaleDateString(userLocale, {
|
|
18
|
+
year: 'numeric',
|
|
19
|
+
month: 'long',
|
|
20
|
+
day: 'numeric'
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<time datetime={date} class={className} {style}>
|
|
26
|
+
{#if showIcon}
|
|
27
|
+
<svg class="inline size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
28
|
+
<path
|
|
29
|
+
stroke-linecap="round"
|
|
30
|
+
stroke-linejoin="round"
|
|
31
|
+
stroke-width="2"
|
|
32
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
33
|
+
/>
|
|
34
|
+
</svg>
|
|
35
|
+
{/if}
|
|
36
|
+
{#if label}{label}{/if}
|
|
37
|
+
{formatDate(date)}
|
|
38
|
+
</time>
|