@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.
Files changed (160) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +108 -0
  3. package/dist/__tests__/content.test.d.ts +4 -0
  4. package/dist/__tests__/content.test.js +128 -0
  5. package/dist/client.d.ts +71 -0
  6. package/dist/client.js +307 -0
  7. package/dist/components/Comments.svelte +277 -0
  8. package/dist/components/Comments.svelte.d.ts +17 -0
  9. package/dist/components/DocumentCard.svelte +95 -0
  10. package/dist/components/DocumentCard.svelte.d.ts +11 -0
  11. package/dist/components/PublicationCard.svelte +54 -0
  12. package/dist/components/PublicationCard.svelte.d.ts +9 -0
  13. package/dist/components/StandardSiteLayout.svelte +102 -0
  14. package/dist/components/StandardSiteLayout.svelte.d.ts +18 -0
  15. package/dist/components/ThemeToggle.svelte +55 -0
  16. package/dist/components/ThemeToggle.svelte.d.ts +6 -0
  17. package/dist/components/common/DateDisplay.svelte +38 -0
  18. package/dist/components/common/DateDisplay.svelte.d.ts +11 -0
  19. package/dist/components/common/TagList.svelte +31 -0
  20. package/dist/components/common/TagList.svelte.d.ts +8 -0
  21. package/dist/components/common/ThemedCard.svelte +65 -0
  22. package/dist/components/common/ThemedCard.svelte.d.ts +11 -0
  23. package/dist/components/common/ThemedContainer.svelte +55 -0
  24. package/dist/components/common/ThemedContainer.svelte.d.ts +11 -0
  25. package/dist/components/common/ThemedText.svelte +75 -0
  26. package/dist/components/common/ThemedText.svelte.d.ts +11 -0
  27. package/dist/components/document/BlockRenderer.svelte +67 -0
  28. package/dist/components/document/BlockRenderer.svelte.d.ts +9 -0
  29. package/dist/components/document/CanvasRenderer.svelte +41 -0
  30. package/dist/components/document/CanvasRenderer.svelte.d.ts +22 -0
  31. package/dist/components/document/DocumentRenderer.svelte +68 -0
  32. package/dist/components/document/DocumentRenderer.svelte.d.ts +17 -0
  33. package/dist/components/document/InlineMath.svelte +41 -0
  34. package/dist/components/document/InlineMath.svelte.d.ts +7 -0
  35. package/dist/components/document/LeafletContentRenderer.svelte +64 -0
  36. package/dist/components/document/LeafletContentRenderer.svelte.d.ts +36 -0
  37. package/dist/components/document/LinearDocumentRenderer.svelte +45 -0
  38. package/dist/components/document/LinearDocumentRenderer.svelte.d.ts +18 -0
  39. package/dist/components/document/MarkdownRenderer.svelte +62 -0
  40. package/dist/components/document/MarkdownRenderer.svelte.d.ts +10 -0
  41. package/dist/components/document/RichText.svelte +272 -0
  42. package/dist/components/document/RichText.svelte.d.ts +18 -0
  43. package/dist/components/document/blocks/BlockquoteBlock.svelte +29 -0
  44. package/dist/components/document/blocks/BlockquoteBlock.svelte.d.ts +10 -0
  45. package/dist/components/document/blocks/BskyPostBlock.svelte +202 -0
  46. package/dist/components/document/blocks/BskyPostBlock.svelte.d.ts +13 -0
  47. package/dist/components/document/blocks/ButtonBlock.svelte +24 -0
  48. package/dist/components/document/blocks/ButtonBlock.svelte.d.ts +10 -0
  49. package/dist/components/document/blocks/CodeBlock.svelte +68 -0
  50. package/dist/components/document/blocks/CodeBlock.svelte.d.ts +12 -0
  51. package/dist/components/document/blocks/HeaderBlock.svelte +56 -0
  52. package/dist/components/document/blocks/HeaderBlock.svelte.d.ts +11 -0
  53. package/dist/components/document/blocks/HorizontalRuleBlock.svelte +14 -0
  54. package/dist/components/document/blocks/HorizontalRuleBlock.svelte.d.ts +6 -0
  55. package/dist/components/document/blocks/IframeBlock.svelte +32 -0
  56. package/dist/components/document/blocks/IframeBlock.svelte.d.ts +10 -0
  57. package/dist/components/document/blocks/ImageBlock.svelte +55 -0
  58. package/dist/components/document/blocks/ImageBlock.svelte.d.ts +25 -0
  59. package/dist/components/document/blocks/MathBlock.svelte +34 -0
  60. package/dist/components/document/blocks/MathBlock.svelte.d.ts +10 -0
  61. package/dist/components/document/blocks/PageBlock.svelte +66 -0
  62. package/dist/components/document/blocks/PageBlock.svelte.d.ts +10 -0
  63. package/dist/components/document/blocks/PollBlock.svelte +122 -0
  64. package/dist/components/document/blocks/PollBlock.svelte.d.ts +27 -0
  65. package/dist/components/document/blocks/TextBlock.svelte +26 -0
  66. package/dist/components/document/blocks/TextBlock.svelte.d.ts +11 -0
  67. package/dist/components/document/blocks/UnorderedListBlock.svelte +71 -0
  68. package/dist/components/document/blocks/UnorderedListBlock.svelte.d.ts +9 -0
  69. package/dist/components/document/blocks/WebsiteBlock.svelte +81 -0
  70. package/dist/components/document/blocks/WebsiteBlock.svelte.d.ts +21 -0
  71. package/dist/components/index.d.ts +11 -0
  72. package/dist/components/index.js +13 -0
  73. package/dist/config/env.d.ts +11 -0
  74. package/dist/config/env.js +26 -0
  75. package/dist/index.d.ts +20 -0
  76. package/dist/index.js +23 -0
  77. package/dist/publisher.d.ts +193 -0
  78. package/dist/publisher.js +349 -0
  79. package/dist/schemas.d.ts +626 -0
  80. package/dist/schemas.js +113 -0
  81. package/dist/stores/index.d.ts +1 -0
  82. package/dist/stores/index.js +1 -0
  83. package/dist/stores/theme.d.ts +11 -0
  84. package/dist/stores/theme.js +67 -0
  85. package/dist/styles/base.css +188 -0
  86. package/dist/styles/themes.css +5 -0
  87. package/dist/types.d.ts +106 -0
  88. package/dist/types.js +4 -0
  89. package/dist/utils/agents.d.ts +35 -0
  90. package/dist/utils/agents.js +96 -0
  91. package/dist/utils/at-uri.d.ts +50 -0
  92. package/dist/utils/at-uri.js +71 -0
  93. package/dist/utils/cache.d.ts +14 -0
  94. package/dist/utils/cache.js +33 -0
  95. package/dist/utils/comments.d.ts +61 -0
  96. package/dist/utils/comments.js +159 -0
  97. package/dist/utils/content.d.ts +94 -0
  98. package/dist/utils/content.js +178 -0
  99. package/dist/utils/document.d.ts +23 -0
  100. package/dist/utils/document.js +33 -0
  101. package/dist/utils/theme-helpers.d.ts +34 -0
  102. package/dist/utils/theme-helpers.js +63 -0
  103. package/dist/utils/theme.d.ts +18 -0
  104. package/dist/utils/theme.js +24 -0
  105. package/dist/utils/verification.d.ts +129 -0
  106. package/dist/utils/verification.js +157 -0
  107. package/package.json +139 -0
  108. package/src/lib/__tests__/content.test.ts +155 -0
  109. package/src/lib/client.ts +368 -0
  110. package/src/lib/components/Comments.svelte +277 -0
  111. package/src/lib/components/DocumentCard.svelte +95 -0
  112. package/src/lib/components/PublicationCard.svelte +54 -0
  113. package/src/lib/components/StandardSiteLayout.svelte +102 -0
  114. package/src/lib/components/ThemeToggle.svelte +55 -0
  115. package/src/lib/components/common/DateDisplay.svelte +38 -0
  116. package/src/lib/components/common/TagList.svelte +31 -0
  117. package/src/lib/components/common/ThemedCard.svelte +65 -0
  118. package/src/lib/components/common/ThemedContainer.svelte +55 -0
  119. package/src/lib/components/common/ThemedText.svelte +75 -0
  120. package/src/lib/components/document/BlockRenderer.svelte +67 -0
  121. package/src/lib/components/document/CanvasRenderer.svelte +41 -0
  122. package/src/lib/components/document/DocumentRenderer.svelte +68 -0
  123. package/src/lib/components/document/InlineMath.svelte +41 -0
  124. package/src/lib/components/document/LeafletContentRenderer.svelte +64 -0
  125. package/src/lib/components/document/LinearDocumentRenderer.svelte +45 -0
  126. package/src/lib/components/document/MarkdownRenderer.svelte +62 -0
  127. package/src/lib/components/document/RichText.svelte +272 -0
  128. package/src/lib/components/document/blocks/BlockquoteBlock.svelte +29 -0
  129. package/src/lib/components/document/blocks/BskyPostBlock.svelte +202 -0
  130. package/src/lib/components/document/blocks/ButtonBlock.svelte +24 -0
  131. package/src/lib/components/document/blocks/CodeBlock.svelte +68 -0
  132. package/src/lib/components/document/blocks/HeaderBlock.svelte +56 -0
  133. package/src/lib/components/document/blocks/HorizontalRuleBlock.svelte +14 -0
  134. package/src/lib/components/document/blocks/IframeBlock.svelte +32 -0
  135. package/src/lib/components/document/blocks/ImageBlock.svelte +55 -0
  136. package/src/lib/components/document/blocks/MathBlock.svelte +34 -0
  137. package/src/lib/components/document/blocks/PageBlock.svelte +66 -0
  138. package/src/lib/components/document/blocks/PollBlock.svelte +122 -0
  139. package/src/lib/components/document/blocks/TextBlock.svelte +26 -0
  140. package/src/lib/components/document/blocks/UnorderedListBlock.svelte +71 -0
  141. package/src/lib/components/document/blocks/WebsiteBlock.svelte +81 -0
  142. package/src/lib/components/index.ts +15 -0
  143. package/src/lib/config/env.ts +31 -0
  144. package/src/lib/index.ts +104 -0
  145. package/src/lib/publisher.ts +489 -0
  146. package/src/lib/schemas.ts +137 -0
  147. package/src/lib/stores/index.ts +1 -0
  148. package/src/lib/stores/theme.ts +80 -0
  149. package/src/lib/styles/base.css +188 -0
  150. package/src/lib/styles/themes.css +5 -0
  151. package/src/lib/types.ts +116 -0
  152. package/src/lib/utils/agents.ts +124 -0
  153. package/src/lib/utils/at-uri.ts +89 -0
  154. package/src/lib/utils/cache.ts +46 -0
  155. package/src/lib/utils/comments.ts +217 -0
  156. package/src/lib/utils/content.ts +234 -0
  157. package/src/lib/utils/document.ts +41 -0
  158. package/src/lib/utils/theme-helpers.ts +87 -0
  159. package/src/lib/utils/theme.ts +33 -0
  160. 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
+ &copy; {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,6 @@
1
+ interface Props {
2
+ class?: string;
3
+ }
4
+ declare const ThemeToggle: import("svelte").Component<Props, {}, "">;
5
+ type ThemeToggle = ReturnType<typeof ThemeToggle>;
6
+ export default ThemeToggle;
@@ -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>