@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,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;
|