@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,45 @@
1
+ <script lang="ts">
2
+ import BlockRenderer from './BlockRenderer.svelte';
3
+
4
+ interface LinearDocumentPage {
5
+ $type: 'pub.leaflet.pages.linearDocument';
6
+ id?: string;
7
+ blocks: Array<{
8
+ $type: 'pub.leaflet.pages.linearDocument#block';
9
+ block: any;
10
+ alignment?: string;
11
+ }>;
12
+ }
13
+
14
+ interface Props {
15
+ page: LinearDocumentPage;
16
+ did?: string;
17
+ pds?: string;
18
+ hasTheme?: boolean;
19
+ }
20
+
21
+ const { page, did = '', pds = '', hasTheme = false }: Props = $props();
22
+
23
+ function getAlignmentClass(alignment?: string): string {
24
+ switch (alignment) {
25
+ case '#textAlignLeft':
26
+ return 'text-left';
27
+ case '#textAlignCenter':
28
+ return 'text-center';
29
+ case '#textAlignRight':
30
+ return 'text-right';
31
+ case '#textAlignJustify':
32
+ return 'text-justify';
33
+ default:
34
+ return '';
35
+ }
36
+ }
37
+ </script>
38
+
39
+ <div class="space-y-6">
40
+ {#each page.blocks as blockWrapper}
41
+ <div class={getAlignmentClass(blockWrapper.alignment)}>
42
+ <BlockRenderer block={blockWrapper.block} {did} {pds} {hasTheme} />
43
+ </div>
44
+ {/each}
45
+ </div>
@@ -0,0 +1,18 @@
1
+ interface LinearDocumentPage {
2
+ $type: 'pub.leaflet.pages.linearDocument';
3
+ id?: string;
4
+ blocks: Array<{
5
+ $type: 'pub.leaflet.pages.linearDocument#block';
6
+ block: any;
7
+ alignment?: string;
8
+ }>;
9
+ }
10
+ interface Props {
11
+ page: LinearDocumentPage;
12
+ did?: string;
13
+ pds?: string;
14
+ hasTheme?: boolean;
15
+ }
16
+ declare const LinearDocumentRenderer: import("svelte").Component<Props, {}, "">;
17
+ type LinearDocumentRenderer = ReturnType<typeof LinearDocumentRenderer>;
18
+ export default LinearDocumentRenderer;
@@ -0,0 +1,62 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+
4
+ interface Props {
5
+ /** Raw markdown string — rendered client-side via unified pipeline */
6
+ markdown?: string;
7
+ /** Pre-rendered HTML from the server — bypasses client-side processing */
8
+ html?: string;
9
+ class?: string;
10
+ }
11
+
12
+ let { markdown, html: prerenderedHtml, class: className = '' }: Props = $props();
13
+
14
+ let renderedHtml = $state<string>(prerenderedHtml ?? '');
15
+ let loading = $state(!prerenderedHtml && !!markdown);
16
+
17
+ onMount(async () => {
18
+ if (prerenderedHtml || !markdown) return;
19
+
20
+ try {
21
+ const [
22
+ { unified },
23
+ { default: remarkParse },
24
+ { default: remarkGfm },
25
+ { default: remarkRehype },
26
+ { default: rehypeSlug },
27
+ { default: rehypeStringify }
28
+ ] = await Promise.all([
29
+ import('unified'),
30
+ import('remark-parse'),
31
+ import('remark-gfm'),
32
+ import('remark-rehype'),
33
+ import('rehype-slug'),
34
+ import('rehype-stringify')
35
+ ]);
36
+
37
+ const processor = unified()
38
+ .use(remarkParse)
39
+ .use(remarkGfm)
40
+ .use(remarkRehype)
41
+ .use(rehypeSlug)
42
+ .use(rehypeStringify);
43
+
44
+ renderedHtml = String(await processor.process(markdown));
45
+ } catch {
46
+ // Fallback: naive paragraph split
47
+ renderedHtml = markdown
48
+ .split(/\n{2,}/)
49
+ .filter(Boolean)
50
+ .map((p) => `<p>${p.replace(/\n/g, '<br>')}</p>`)
51
+ .join('\n');
52
+ } finally {
53
+ loading = false;
54
+ }
55
+ });
56
+ </script>
57
+
58
+ {#if renderedHtml}
59
+ <div class="prose prose-lg max-w-none {className}">{@html renderedHtml}</div>
60
+ {:else if loading}
61
+ <div class="prose prose-lg max-w-none {className} whitespace-pre-wrap opacity-70">{markdown}</div>
62
+ {/if}
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ /** Raw markdown string — rendered client-side via unified pipeline */
3
+ markdown?: string;
4
+ /** Pre-rendered HTML from the server — bypasses client-side processing */
5
+ html?: string;
6
+ class?: string;
7
+ }
8
+ declare const MarkdownRenderer: import("svelte").Component<Props, {}, "">;
9
+ type MarkdownRenderer = ReturnType<typeof MarkdownRenderer>;
10
+ export default MarkdownRenderer;
@@ -0,0 +1,272 @@
1
+ <script lang="ts">
2
+ import InlineMath from './InlineMath.svelte';
3
+ import { UnicodeString } from '@atproto/api';
4
+
5
+ interface Facet {
6
+ index: {
7
+ byteStart: number;
8
+ byteEnd: number;
9
+ };
10
+ features: Array<{
11
+ $type: string;
12
+ [key: string]: any;
13
+ }>;
14
+ }
15
+
16
+ interface Props {
17
+ plaintext: string;
18
+ facets?: Facet[];
19
+ hasTheme?: boolean;
20
+ }
21
+
22
+ const { plaintext, facets = [], hasTheme = false }: Props = $props();
23
+
24
+ interface RichTextSegment {
25
+ text: string;
26
+ facet?: Array<{ $type: string; [key: string]: any }>;
27
+ }
28
+
29
+ class RichText {
30
+ unicodeText: UnicodeString;
31
+ facets: Facet[];
32
+
33
+ constructor(props: { text: string; facets: Facet[] }) {
34
+ this.unicodeText = new UnicodeString(props.text || '');
35
+ this.facets = props.facets || [];
36
+ if (this.facets) {
37
+ this.facets = this.facets
38
+ .filter((facet) => facet.index.byteStart <= facet.index.byteEnd)
39
+ .sort((a, b) => a.index.byteStart - b.index.byteStart);
40
+ }
41
+ }
42
+
43
+ *segments(): Generator<RichTextSegment, void, void> {
44
+ const facets = this.facets || [];
45
+ if (!facets.length) {
46
+ yield { text: this.unicodeText.utf16 || '' };
47
+ return;
48
+ }
49
+
50
+ let textCursor = 0;
51
+ let facetCursor = 0;
52
+ do {
53
+ const currFacet = facets[facetCursor];
54
+ if (textCursor < currFacet.index.byteStart) {
55
+ const sliced = this.unicodeText.slice(textCursor, currFacet.index.byteStart);
56
+ yield {
57
+ text: sliced || ''
58
+ };
59
+ } else if (textCursor > currFacet.index.byteStart) {
60
+ facetCursor++;
61
+ continue;
62
+ }
63
+ if (currFacet.index.byteStart < currFacet.index.byteEnd) {
64
+ const subtext = this.unicodeText.slice(
65
+ currFacet.index.byteStart,
66
+ currFacet.index.byteEnd
67
+ );
68
+ const subtextStr = subtext || '';
69
+ if (!subtextStr.trim()) {
70
+ // don't emit empty string entities
71
+ yield { text: subtextStr };
72
+ } else {
73
+ yield { text: subtextStr, facet: currFacet.features };
74
+ }
75
+ }
76
+ textCursor = currFacet.index.byteEnd;
77
+ facetCursor++;
78
+ } while (facetCursor < facets.length);
79
+ if (textCursor < this.unicodeText.length) {
80
+ const sliced = this.unicodeText.slice(textCursor, this.unicodeText.length);
81
+ yield {
82
+ text: sliced || ''
83
+ };
84
+ }
85
+ }
86
+ }
87
+
88
+ interface ProcessedSegment {
89
+ parts: Array<{ text: string; isBr: boolean }>;
90
+ isBold: boolean;
91
+ isItalic: boolean;
92
+ isUnderline: boolean;
93
+ isStrikethrough: boolean;
94
+ isCode: boolean;
95
+ isHighlighted: boolean;
96
+ isMath: boolean;
97
+ isDidMention: boolean;
98
+ isAtMention: boolean;
99
+ link?: string;
100
+ id?: string;
101
+ did?: string;
102
+ atURI?: string;
103
+ }
104
+
105
+ function processSegments(): ProcessedSegment[] {
106
+ // Handle undefined or empty plaintext
107
+ const text = plaintext || '';
108
+ const richText = new RichText({ text, facets });
109
+ const result: ProcessedSegment[] = [];
110
+
111
+ for (const segment of richText.segments()) {
112
+ const id = segment.facet?.find((f) => f.$type === 'pub.leaflet.richtext.facet#id');
113
+ const link = segment.facet?.find((f) => f.$type === 'pub.leaflet.richtext.facet#link');
114
+ const isBold = segment.facet?.some((f) => f.$type === 'pub.leaflet.richtext.facet#bold');
115
+ const isCode = segment.facet?.some((f) => f.$type === 'pub.leaflet.richtext.facet#code');
116
+ const isStrikethrough = segment.facet?.some(
117
+ (f) => f.$type === 'pub.leaflet.richtext.facet#strikethrough'
118
+ );
119
+ const isDidMention = segment.facet?.find(
120
+ (f) => f.$type === 'pub.leaflet.richtext.facet#didMention'
121
+ );
122
+ const isAtMention = segment.facet?.find(
123
+ (f) => f.$type === 'pub.leaflet.richtext.facet#atMention'
124
+ );
125
+ const isUnderline = segment.facet?.some(
126
+ (f) => f.$type === 'pub.leaflet.richtext.facet#underline'
127
+ );
128
+ const isItalic = segment.facet?.some((f) => f.$type === 'pub.leaflet.richtext.facet#italic');
129
+ const isHighlighted = segment.facet?.some(
130
+ (f) => f.$type === 'pub.leaflet.richtext.facet#highlight'
131
+ );
132
+ const isMath = segment.facet?.some((f) => f.$type === 'pub.leaflet.richtext.facet#math');
133
+
134
+ // Split text by newlines and mark br elements - handle undefined segment.text
135
+ const segmentText = segment.text || '';
136
+ const textParts = segmentText.split('\n');
137
+ const parts = textParts.flatMap((part, i) =>
138
+ i < textParts.length - 1
139
+ ? [
140
+ { text: part, isBr: false },
141
+ { text: '', isBr: true }
142
+ ]
143
+ : [{ text: part, isBr: false }]
144
+ );
145
+
146
+ result.push({
147
+ parts,
148
+ isBold: isBold || false,
149
+ isItalic: isItalic || false,
150
+ isUnderline: isUnderline || false,
151
+ isStrikethrough: isStrikethrough || false,
152
+ isCode: isCode || false,
153
+ isHighlighted: isHighlighted || false,
154
+ isMath: isMath || false,
155
+ isDidMention: !!isDidMention,
156
+ isAtMention: !!isAtMention,
157
+ link: link?.uri,
158
+ id: id?.id,
159
+ did: isDidMention?.did,
160
+ atURI: isAtMention?.atURI
161
+ });
162
+ }
163
+
164
+ return result;
165
+ }
166
+
167
+ const segments = $derived(processSegments());
168
+ </script>
169
+
170
+ {#each segments as segment, i}
171
+ {#each segment.parts as part, j}
172
+ {#if part.isBr}
173
+ <br />
174
+ {:else if segment.isMath}
175
+ <InlineMath tex={part.text} {hasTheme} />
176
+ {:else}
177
+ {@const classes = [
178
+ segment.isCode ? 'inline-code' : '',
179
+ segment.id ? 'scroll-mt-12 scroll-mb-10' : '',
180
+ segment.isBold ? 'font-bold' : '',
181
+ segment.isItalic ? 'italic' : '',
182
+ segment.isUnderline ? 'underline' : '',
183
+ segment.isStrikethrough ? 'line-through decoration-tertiary' : '',
184
+ segment.isHighlighted ? 'highlight' : ''
185
+ ]
186
+ .filter(Boolean)
187
+ .join(' ')}
188
+
189
+ {#if segment.isCode}
190
+ <code class={classes} id={segment.id}>{part.text}</code>
191
+ {:else if segment.isDidMention}
192
+ <a
193
+ href={`https://leaflet.pub/p/${segment.did}`}
194
+ target="_blank"
195
+ rel="noopener noreferrer"
196
+ class="no-underline"
197
+ >
198
+ <span class="mention {classes}" class:themed={hasTheme}>{part.text}</span>
199
+ </a>
200
+ {:else if segment.isAtMention}
201
+ <a
202
+ href={segment.atURI}
203
+ target="_blank"
204
+ rel="noopener noreferrer"
205
+ class="hover:underline {classes}"
206
+ class:themed={hasTheme}
207
+ >
208
+ {part.text}
209
+ </a>
210
+ {:else if segment.link}
211
+ <a
212
+ href={segment.link.trim()}
213
+ class="hover:underline {classes}"
214
+ class:themed={hasTheme}
215
+ target="_blank"
216
+ rel="noopener noreferrer"
217
+ >
218
+ {part.text}
219
+ </a>
220
+ {:else}
221
+ <span class={classes} id={segment.id}>{part.text}</span>
222
+ {/if}
223
+ {/if}
224
+ {/each}
225
+ {/each}
226
+
227
+ <style>
228
+ .mention {
229
+ cursor: pointer;
230
+ color: rgb(0 0 225);
231
+ padding: 0 0.125rem;
232
+ border-radius: 0.25rem;
233
+ background-color: color-mix(in oklab, rgb(0 0 225), transparent 80%);
234
+ border: 1px solid transparent;
235
+ display: inline;
236
+ white-space: normal;
237
+ }
238
+
239
+ .mention.themed {
240
+ color: var(--theme-accent);
241
+ background-color: color-mix(in oklab, var(--theme-accent), transparent 80%);
242
+ }
243
+
244
+ a {
245
+ color: rgb(0 0 225);
246
+ }
247
+
248
+ a.themed {
249
+ color: var(--theme-accent);
250
+ }
251
+
252
+ .inline-code {
253
+ display: inline;
254
+ font-size: 1em;
255
+ background-color: color-mix(in oklab, currentColor, transparent 90%);
256
+ font-family: ui-monospace, monospace;
257
+ padding: 1px;
258
+ margin: -1px;
259
+ border-radius: 4px;
260
+ box-decoration-break: clone;
261
+ -webkit-box-decoration-break: clone;
262
+ }
263
+
264
+ .highlight {
265
+ padding: 1px;
266
+ margin: -1px;
267
+ border-radius: 4px;
268
+ box-decoration-break: clone;
269
+ -webkit-box-decoration-break: clone;
270
+ background-color: rgb(255, 177, 177);
271
+ }
272
+ </style>
@@ -0,0 +1,18 @@
1
+ interface Facet {
2
+ index: {
3
+ byteStart: number;
4
+ byteEnd: number;
5
+ };
6
+ features: Array<{
7
+ $type: string;
8
+ [key: string]: any;
9
+ }>;
10
+ }
11
+ interface Props {
12
+ plaintext: string;
13
+ facets?: Facet[];
14
+ hasTheme?: boolean;
15
+ }
16
+ declare const RichText: import("svelte").Component<Props, {}, "">;
17
+ type RichText = ReturnType<typeof RichText>;
18
+ export default RichText;
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import RichText from '../RichText.svelte';
3
+
4
+ interface Props {
5
+ block: {
6
+ plaintext: string;
7
+ facets?: any[];
8
+ };
9
+ hasTheme?: boolean;
10
+ }
11
+
12
+ const { block, hasTheme = false }: Props = $props();
13
+ </script>
14
+
15
+ <blockquote class="blockquote mt-1 mb-2" class:themed={hasTheme}>
16
+ <RichText plaintext={block.plaintext} facets={block.facets} {hasTheme} />
17
+ </blockquote>
18
+
19
+ <style>
20
+ .blockquote {
21
+ border-left: 2px solid rgb(107 114 128); /* Default gray color */
22
+ padding-left: 0.75rem;
23
+ margin-left: 0.5rem;
24
+ }
25
+
26
+ .blockquote.themed {
27
+ border-color: var(--theme-accent);
28
+ }
29
+ </style>
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ block: {
3
+ plaintext: string;
4
+ facets?: any[];
5
+ };
6
+ hasTheme?: boolean;
7
+ }
8
+ declare const BlockquoteBlock: import("svelte").Component<Props, {}, "">;
9
+ type BlockquoteBlock = ReturnType<typeof BlockquoteBlock>;
10
+ export default BlockquoteBlock;
@@ -0,0 +1,202 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+
4
+ interface Props {
5
+ block: {
6
+ postRef: {
7
+ uri: string;
8
+ cid: string;
9
+ };
10
+ };
11
+ hasTheme?: boolean;
12
+ postData?: any; // The full post data if already fetched
13
+ }
14
+
15
+ const { block, hasTheme = false, postData }: Props = $props();
16
+
17
+ let post = $state<any>(null);
18
+ let loading = $state(true);
19
+ let error = $state<string | null>(null);
20
+
21
+ // Extract post info from AT URI
22
+ function extractPostInfo(uri: string): { did: string; rkey: string } | null {
23
+ const match = uri.match(/^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/(.+)$/);
24
+ if (!match) return null;
25
+ return { did: match[1], rkey: match[2] };
26
+ }
27
+
28
+ const postInfo = $derived(extractPostInfo(block.postRef.uri));
29
+ const postUrl = $derived(
30
+ postInfo ? `https://bsky.app/profile/${postInfo.did}/post/${postInfo.rkey}` : null
31
+ );
32
+
33
+ onMount(async () => {
34
+ // Use postData if provided
35
+ if (postData) {
36
+ post = postData;
37
+ loading = false;
38
+ return;
39
+ }
40
+
41
+ // You would fetch the post data here from your API or Bluesky API
42
+ // For now, we'll just show a simple link
43
+ loading = false;
44
+ });
45
+ </script>
46
+
47
+ {#if loading}
48
+ <div
49
+ class="relative my-2 flex w-full flex-col gap-2 overflow-hidden rounded-md border bg-white p-3 text-sm dark:bg-gray-900"
50
+ style:border-color={hasTheme ? 'var(--theme-accent)' : undefined}
51
+ class:border-gray-200={!hasTheme}
52
+ class:dark:border-gray-700={!hasTheme}
53
+ >
54
+ <div class="animate-pulse">
55
+ <div class="mb-2 h-8 w-8 rounded-full bg-gray-200 dark:bg-gray-700"></div>
56
+ <div class="mb-2 h-4 w-3/4 rounded bg-gray-200 dark:bg-gray-700"></div>
57
+ <div class="h-4 w-1/2 rounded bg-gray-200 dark:bg-gray-700"></div>
58
+ </div>
59
+ </div>
60
+ {:else if error}
61
+ <div
62
+ class="relative my-2 flex w-full flex-col gap-2 overflow-hidden rounded-md border border-red-200 bg-red-50 p-3 text-sm dark:border-red-800 dark:bg-red-950"
63
+ >
64
+ <p class="text-red-600 dark:text-red-400">Failed to load Bluesky post: {error}</p>
65
+ </div>
66
+ {:else if post && post.author && post.record}
67
+ <div
68
+ class="relative my-2 flex w-full flex-col gap-2 overflow-hidden rounded-md border bg-white p-3 text-sm dark:bg-gray-900"
69
+ style:border-color={hasTheme ? 'var(--theme-accent)' : undefined}
70
+ class:border-gray-200={!hasTheme}
71
+ class:dark:border-gray-700={!hasTheme}
72
+ >
73
+ <div class="flex w-full items-center gap-2">
74
+ {#if post.author.avatar}
75
+ <img
76
+ src={post.author.avatar}
77
+ alt="{post.author.displayName}'s avatar"
78
+ class="h-8 w-8 shrink-0 rounded-full border border-gray-200 dark:border-gray-700"
79
+ />
80
+ {/if}
81
+ <div class="flex grow flex-col gap-0.5 leading-tight">
82
+ <div class="font-bold text-gray-900 dark:text-gray-100">
83
+ {post.author.displayName}
84
+ </div>
85
+ <a
86
+ href="https://bsky.app/profile/{post.author.handle}"
87
+ target="_blank"
88
+ rel="noopener noreferrer"
89
+ class="text-xs text-gray-600 hover:underline dark:text-gray-400"
90
+ >
91
+ @{post.author.handle}
92
+ </a>
93
+ </div>
94
+ </div>
95
+
96
+ <div class="flex flex-col gap-2">
97
+ {#if post.record.text}
98
+ <pre class="whitespace-pre-wrap text-gray-900 dark:text-gray-100">{post.record.text}</pre>
99
+ {/if}
100
+
101
+ {#if post.embed}
102
+ <div>
103
+ <!-- Embed rendering would go here -->
104
+ <div class="text-sm text-gray-600 italic dark:text-gray-400">[Embedded content]</div>
105
+ </div>
106
+ {/if}
107
+ </div>
108
+
109
+ <div class="flex w-full items-center justify-between gap-2 text-gray-600 dark:text-gray-400">
110
+ {#if post.record.createdAt}
111
+ <div class="text-xs">
112
+ {new Date(post.record.createdAt).toLocaleDateString('en-US', {
113
+ month: 'short',
114
+ day: 'numeric',
115
+ year: 'numeric',
116
+ hour: 'numeric',
117
+ minute: 'numeric',
118
+ hour12: true
119
+ })}
120
+ </div>
121
+ {/if}
122
+ <div class="flex items-center gap-2">
123
+ {#if post.replyCount != null && post.replyCount > 0}
124
+ <span class="flex items-center gap-1 text-xs">
125
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
126
+ <path
127
+ stroke-linecap="round"
128
+ stroke-linejoin="round"
129
+ stroke-width="2"
130
+ d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
131
+ />
132
+ </svg>
133
+ {post.replyCount}
134
+ </span>
135
+ {/if}
136
+ {#if post.quoteCount != null && post.quoteCount > 0}
137
+ <span class="flex items-center gap-1 text-xs">
138
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
139
+ <path
140
+ stroke-linecap="round"
141
+ stroke-linejoin="round"
142
+ stroke-width="2"
143
+ d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
144
+ />
145
+ </svg>
146
+ {post.quoteCount}
147
+ </span>
148
+ {/if}
149
+ <a
150
+ href={postUrl}
151
+ target="_blank"
152
+ rel="noopener noreferrer"
153
+ class="transition-opacity hover:opacity-70"
154
+ title="View on Bluesky"
155
+ >
156
+ <svg class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
157
+ <path
158
+ d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z"
159
+ />
160
+ </svg>
161
+ </a>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ {:else if postUrl}
166
+ <div
167
+ class="my-2 rounded-md border bg-white p-6 dark:bg-gray-900"
168
+ style:border-color={hasTheme ? 'var(--theme-accent)' : undefined}
169
+ class:border-gray-200={!hasTheme}
170
+ class:dark:border-gray-700={!hasTheme}
171
+ >
172
+ <div class="mb-3 flex items-center gap-2 text-sm font-medium">
173
+ <svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
174
+ <path
175
+ d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z"
176
+ />
177
+ </svg>
178
+ <span style:color={hasTheme ? 'var(--theme-accent)' : undefined}> Bluesky Post </span>
179
+ </div>
180
+ <a
181
+ href={postUrl}
182
+ target="_blank"
183
+ rel="noopener noreferrer"
184
+ class="inline-flex items-center gap-2 text-sm font-medium transition-all hover:gap-3"
185
+ style:color={hasTheme ? 'var(--theme-accent)' : undefined}
186
+ >
187
+ View on Bluesky
188
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
189
+ <path
190
+ stroke-linecap="round"
191
+ stroke-linejoin="round"
192
+ stroke-width="2"
193
+ d="M14 5l7 7m0 0l-7 7m7-7H3"
194
+ />
195
+ </svg>
196
+ </a>
197
+ </div>
198
+ {:else}
199
+ <div class="my-4 rounded-lg border border-yellow-500/20 bg-yellow-500/5 p-4">
200
+ <p class="text-sm text-yellow-600 dark:text-yellow-400">Invalid Bluesky post reference</p>
201
+ </div>
202
+ {/if}
@@ -0,0 +1,13 @@
1
+ interface Props {
2
+ block: {
3
+ postRef: {
4
+ uri: string;
5
+ cid: string;
6
+ };
7
+ };
8
+ hasTheme?: boolean;
9
+ postData?: any;
10
+ }
11
+ declare const BskyPostBlock: import("svelte").Component<Props, {}, "">;
12
+ type BskyPostBlock = ReturnType<typeof BskyPostBlock>;
13
+ export default BskyPostBlock;