@ewanc26/svelte-standard-site 0.2.2 → 0.2.4
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/dist/components/ActionBar.svelte +85 -0
- package/dist/components/ActionBar.svelte.d.ts +13 -0
- package/dist/components/Avatar.svelte +104 -0
- package/dist/components/Avatar.svelte.d.ts +19 -0
- package/dist/components/Comment.svelte +172 -0
- package/dist/components/Comment.svelte.d.ts +22 -0
- package/dist/components/CommentsSection.svelte +89 -0
- package/dist/components/DocumentCard.svelte +126 -56
- package/dist/components/DocumentCard.svelte.d.ts +51 -0
- package/dist/components/Footnotes.svelte +72 -0
- package/dist/components/Footnotes.svelte.d.ts +13 -0
- package/dist/components/RecommendButton.svelte +153 -0
- package/dist/components/RecommendButton.svelte.d.ts +17 -0
- package/dist/components/ThemeProvider.svelte +92 -0
- package/dist/components/ThemeProvider.svelte.d.ts +13 -0
- package/dist/components/Toast.svelte +177 -0
- package/dist/components/Toast.svelte.d.ts +32 -0
- package/dist/components/Watermark.svelte +100 -0
- package/dist/components/Watermark.svelte.d.ts +17 -0
- package/dist/components/common/ThemedCard.svelte +15 -15
- package/dist/components/common/ThemedCard.svelte.d.ts +5 -0
- package/dist/components/document/BlockRenderer.svelte +3 -0
- package/dist/components/document/DocumentRenderer.svelte +41 -1
- package/dist/components/document/RichText.svelte +87 -2
- package/dist/components/document/RichText.svelte.d.ts +2 -0
- package/dist/components/document/blocks/OrderedListBlock.svelte +152 -0
- package/dist/components/document/blocks/UnorderedListBlock.svelte +1 -1
- package/dist/components/index.d.ts +28 -0
- package/dist/components/index.js +30 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +6 -4
- package/dist/publisher.d.ts +73 -0
- package/dist/publisher.js +185 -0
- package/dist/schemas.d.ts +1162 -2
- package/dist/schemas.js +316 -0
- package/dist/types.d.ts +393 -2
- package/dist/types.js +1 -1
- package/dist/utils/native-comments.d.ts +68 -0
- package/dist/utils/native-comments.js +149 -0
- package/dist/utils/theme-helpers.d.ts +41 -1
- package/dist/utils/theme-helpers.js +98 -1
- package/dist/utils/theme.d.ts +48 -1
- package/dist/utils/theme.js +158 -0
- package/package.json +62 -65
- package/src/lib/components/ActionBar.svelte +85 -0
- package/src/lib/components/Avatar.svelte +104 -0
- package/src/lib/components/Comment.svelte +172 -0
- package/src/lib/components/CommentsSection.svelte +89 -0
- package/src/lib/components/DocumentCard.svelte +126 -56
- package/src/lib/components/Footnotes.svelte +72 -0
- package/src/lib/components/RecommendButton.svelte +153 -0
- package/src/lib/components/ThemeProvider.svelte +92 -0
- package/src/lib/components/Toast.svelte +177 -0
- package/src/lib/components/Watermark.svelte +100 -0
- package/src/lib/components/common/ThemedCard.svelte +15 -15
- package/src/lib/components/document/BlockRenderer.svelte +3 -0
- package/src/lib/components/document/DocumentRenderer.svelte +41 -1
- package/src/lib/components/document/RichText.svelte +87 -2
- package/src/lib/components/document/blocks/OrderedListBlock.svelte +152 -0
- package/src/lib/components/document/blocks/UnorderedListBlock.svelte +1 -1
- package/src/lib/components/index.ts +32 -0
- package/src/lib/index.ts +119 -5
- package/src/lib/publisher.ts +251 -0
- package/src/lib/schemas.ts +411 -0
- package/src/lib/types.ts +506 -2
- package/src/lib/utils/native-comments.ts +197 -0
- package/src/lib/utils/theme-helpers.ts +136 -3
- package/src/lib/utils/theme.ts +189 -1
- package/dist/components/document/blocks/UnorderedListBlock.svelte.d.ts +0 -9
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Snippet for action items */
|
|
4
|
+
children: import('svelte').Snippet;
|
|
5
|
+
/** Position of the action bar */
|
|
6
|
+
position?: 'left' | 'bottom' | 'right';
|
|
7
|
+
/** Has theme applied */
|
|
8
|
+
hasTheme?: boolean;
|
|
9
|
+
/** Sticky positioning */
|
|
10
|
+
sticky?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { children, position = 'left', hasTheme = false, sticky = false }: Props = $props();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div
|
|
17
|
+
class="action-bar {position}"
|
|
18
|
+
class:themed={hasTheme}
|
|
19
|
+
class:sticky
|
|
20
|
+
>
|
|
21
|
+
{@render children()}
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<style>
|
|
25
|
+
.action-bar {
|
|
26
|
+
display: flex;
|
|
27
|
+
gap: 0.5rem;
|
|
28
|
+
padding: 0.5rem;
|
|
29
|
+
border-radius: 0.5rem;
|
|
30
|
+
background-color: rgb(255 255 255);
|
|
31
|
+
box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.action-bar.themed {
|
|
35
|
+
background-color: var(--theme-page-background, var(--theme-background));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.action-bar.left {
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
align-items: center;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.action-bar.bottom {
|
|
44
|
+
flex-direction: row;
|
|
45
|
+
align-items: center;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.action-bar.right {
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
align-items: center;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.action-bar.sticky {
|
|
54
|
+
position: sticky;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.action-bar.left.sticky {
|
|
58
|
+
top: 1rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.action-bar.bottom.sticky {
|
|
62
|
+
bottom: 1rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.action-bar :global(button) {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
justify-content: center;
|
|
69
|
+
width: 2.5rem;
|
|
70
|
+
height: 2.5rem;
|
|
71
|
+
border-radius: 9999px;
|
|
72
|
+
background: transparent;
|
|
73
|
+
border: none;
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
transition: background-color 0.15s;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.action-bar :global(button:hover) {
|
|
79
|
+
background-color: rgb(243 244 246);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.action-bar.themed :global(button:hover) {
|
|
83
|
+
background-color: color-mix(in srgb, var(--theme-foreground) 10%, transparent);
|
|
84
|
+
}
|
|
85
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Snippet for action items */
|
|
3
|
+
children: import('svelte').Snippet;
|
|
4
|
+
/** Position of the action bar */
|
|
5
|
+
position?: 'left' | 'bottom' | 'right';
|
|
6
|
+
/** Has theme applied */
|
|
7
|
+
hasTheme?: boolean;
|
|
8
|
+
/** Sticky positioning */
|
|
9
|
+
sticky?: boolean;
|
|
10
|
+
}
|
|
11
|
+
declare const ActionBar: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type ActionBar = ReturnType<typeof ActionBar>;
|
|
13
|
+
export default ActionBar;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
/** User's DID */
|
|
4
|
+
did: string;
|
|
5
|
+
/** User's handle (optional, for display) */
|
|
6
|
+
handle?: string;
|
|
7
|
+
/** User's display name */
|
|
8
|
+
displayName?: string;
|
|
9
|
+
/** Avatar image URL */
|
|
10
|
+
src?: string;
|
|
11
|
+
/** Size in pixels */
|
|
12
|
+
size?: number;
|
|
13
|
+
/** Whether to link to profile */
|
|
14
|
+
link?: boolean;
|
|
15
|
+
/** Has theme applied */
|
|
16
|
+
hasTheme?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
did,
|
|
21
|
+
handle,
|
|
22
|
+
displayName,
|
|
23
|
+
src,
|
|
24
|
+
size = 40,
|
|
25
|
+
link = true,
|
|
26
|
+
hasTheme = false
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
// Generate initials from display name or handle
|
|
30
|
+
const initials = $derived(
|
|
31
|
+
(displayName || handle || did)
|
|
32
|
+
.split(/[.\s_@]+/)
|
|
33
|
+
.slice(0, 2)
|
|
34
|
+
.map((s) => s.charAt(0).toUpperCase())
|
|
35
|
+
.join('')
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Profile URL
|
|
39
|
+
const profileUrl = $derived(`https://leaflet.pub/p/${did}`);
|
|
40
|
+
|
|
41
|
+
// Size classes
|
|
42
|
+
const sizeStyle = $derived(`width: ${size}px; height: ${size}px; font-size: ${size * 0.4}px;`);
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
{#if link}
|
|
46
|
+
<a
|
|
47
|
+
href={profileUrl}
|
|
48
|
+
target="_blank"
|
|
49
|
+
rel="noopener noreferrer"
|
|
50
|
+
class="avatar-link"
|
|
51
|
+
title={displayName || handle || did}
|
|
52
|
+
>
|
|
53
|
+
{#if src}
|
|
54
|
+
<img
|
|
55
|
+
src={src}
|
|
56
|
+
alt={displayName || handle || 'Avatar'}
|
|
57
|
+
class="avatar rounded-full object-cover"
|
|
58
|
+
style={sizeStyle}
|
|
59
|
+
loading="lazy"
|
|
60
|
+
/>
|
|
61
|
+
{:else}
|
|
62
|
+
<div
|
|
63
|
+
class="avatar-placeholder rounded-full flex items-center justify-center font-medium"
|
|
64
|
+
class:themed={hasTheme}
|
|
65
|
+
style={sizeStyle}
|
|
66
|
+
>
|
|
67
|
+
{initials}
|
|
68
|
+
</div>
|
|
69
|
+
{/if}
|
|
70
|
+
</a>
|
|
71
|
+
{:else if src}
|
|
72
|
+
<img
|
|
73
|
+
src={src}
|
|
74
|
+
alt={displayName || handle || 'Avatar'}
|
|
75
|
+
class="avatar rounded-full object-cover"
|
|
76
|
+
style={sizeStyle}
|
|
77
|
+
loading="lazy"
|
|
78
|
+
/>
|
|
79
|
+
{:else}
|
|
80
|
+
<div
|
|
81
|
+
class="avatar-placeholder rounded-full flex items-center justify-center font-medium"
|
|
82
|
+
class:themed={hasTheme}
|
|
83
|
+
style={sizeStyle}
|
|
84
|
+
>
|
|
85
|
+
{initials}
|
|
86
|
+
</div>
|
|
87
|
+
{/if}
|
|
88
|
+
|
|
89
|
+
<style>
|
|
90
|
+
.avatar-link {
|
|
91
|
+
display: inline-block;
|
|
92
|
+
text-decoration: none;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.avatar-placeholder {
|
|
96
|
+
background-color: rgb(229 231 235);
|
|
97
|
+
color: rgb(107 114 128);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.avatar-placeholder.themed {
|
|
101
|
+
background-color: color-mix(in srgb, var(--theme-accent) 20%, transparent);
|
|
102
|
+
color: var(--theme-accent);
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** User's DID */
|
|
3
|
+
did: string;
|
|
4
|
+
/** User's handle (optional, for display) */
|
|
5
|
+
handle?: string;
|
|
6
|
+
/** User's display name */
|
|
7
|
+
displayName?: string;
|
|
8
|
+
/** Avatar image URL */
|
|
9
|
+
src?: string;
|
|
10
|
+
/** Size in pixels */
|
|
11
|
+
size?: number;
|
|
12
|
+
/** Whether to link to profile */
|
|
13
|
+
link?: boolean;
|
|
14
|
+
/** Has theme applied */
|
|
15
|
+
hasTheme?: boolean;
|
|
16
|
+
}
|
|
17
|
+
declare const Avatar: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type Avatar = ReturnType<typeof Avatar>;
|
|
19
|
+
export default Avatar;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import RichText from './document/RichText.svelte';
|
|
3
|
+
import Comment from './Comment.svelte';
|
|
4
|
+
import type { CommentRecord, LinearDocumentQuote } from '../types.js';
|
|
5
|
+
|
|
6
|
+
interface CommentProps extends CommentRecord {
|
|
7
|
+
uri: string;
|
|
8
|
+
cid: string;
|
|
9
|
+
author: {
|
|
10
|
+
did: string;
|
|
11
|
+
handle?: string;
|
|
12
|
+
displayName?: string;
|
|
13
|
+
avatar?: string;
|
|
14
|
+
};
|
|
15
|
+
replies?: CommentProps[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
comment: CommentProps;
|
|
20
|
+
hasTheme?: boolean;
|
|
21
|
+
/** Whether to show the attachment/quote */
|
|
22
|
+
showQuote?: boolean;
|
|
23
|
+
/** Depth in the thread (for styling) */
|
|
24
|
+
depth?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { comment, hasTheme = false, showQuote = true, depth = 0 }: Props = $props();
|
|
28
|
+
|
|
29
|
+
// Format date
|
|
30
|
+
const formattedDate = $derived(
|
|
31
|
+
new Date(comment.createdAt).toLocaleDateString(undefined, {
|
|
32
|
+
year: 'numeric',
|
|
33
|
+
month: 'short',
|
|
34
|
+
day: 'numeric'
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Author display name
|
|
39
|
+
const authorName = $derived(
|
|
40
|
+
comment.author.displayName || comment.author.handle || comment.author.did
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Author profile URL
|
|
44
|
+
const authorUrl = $derived(`https://leaflet.pub/p/${comment.author.did}`);
|
|
45
|
+
|
|
46
|
+
// Quote text extraction
|
|
47
|
+
const quoteText = $derived(
|
|
48
|
+
comment.attachment?.quote ? extractQuotePreview(comment.attachment) : null
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
function extractQuotePreview(quote: LinearDocumentQuote): string {
|
|
52
|
+
if (quote.quote) {
|
|
53
|
+
return `Quote from block ${quote.quote.start.block.join('.')}`;
|
|
54
|
+
}
|
|
55
|
+
return 'Document quote';
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<article
|
|
60
|
+
class="comment"
|
|
61
|
+
class:themed={hasTheme}
|
|
62
|
+
class:reply={depth > 0}
|
|
63
|
+
style:padding-left={depth > 0 ? '1rem' : undefined}
|
|
64
|
+
>
|
|
65
|
+
<header class="comment-header flex items-center gap-3 mb-2">
|
|
66
|
+
{#if comment.author.avatar}
|
|
67
|
+
<img
|
|
68
|
+
src={comment.author.avatar}
|
|
69
|
+
alt={authorName}
|
|
70
|
+
class="w-8 h-8 rounded-full"
|
|
71
|
+
/>
|
|
72
|
+
{:else}
|
|
73
|
+
<div
|
|
74
|
+
class="avatar-placeholder w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium"
|
|
75
|
+
class:themed={hasTheme}
|
|
76
|
+
>
|
|
77
|
+
{authorName.charAt(0).toUpperCase()}
|
|
78
|
+
</div>
|
|
79
|
+
{/if}
|
|
80
|
+
|
|
81
|
+
<div class="flex-1 min-w-0">
|
|
82
|
+
<a
|
|
83
|
+
href={authorUrl}
|
|
84
|
+
target="_blank"
|
|
85
|
+
rel="noopener noreferrer"
|
|
86
|
+
class="author-name font-medium hover:underline"
|
|
87
|
+
class:themed={hasTheme}
|
|
88
|
+
>
|
|
89
|
+
{authorName}
|
|
90
|
+
</a>
|
|
91
|
+
{#if comment.author.handle}
|
|
92
|
+
<span class="author-handle text-sm opacity-60">@{comment.author.handle}</span>
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<time datetime={comment.createdAt} class="text-sm opacity-60">
|
|
97
|
+
{formattedDate}
|
|
98
|
+
</time>
|
|
99
|
+
</header>
|
|
100
|
+
|
|
101
|
+
<div class="comment-body">
|
|
102
|
+
{#if showQuote && comment.attachment}
|
|
103
|
+
<blockquote class="quote-attachment mb-2 px-3 py-2 border-l-2 text-sm opacity-80">
|
|
104
|
+
{quoteText}
|
|
105
|
+
</blockquote>
|
|
106
|
+
{/if}
|
|
107
|
+
|
|
108
|
+
<div class="comment-text">
|
|
109
|
+
<RichText plaintext={comment.plaintext} facets={comment.facets} {hasTheme} />
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{#if comment.replies && comment.replies.length > 0}
|
|
114
|
+
<div class="replies mt-4 space-y-4 border-l-2 pl-4">
|
|
115
|
+
{#each comment.replies as reply}
|
|
116
|
+
<Comment comment={reply} {hasTheme} {showQuote} depth={depth + 1} />
|
|
117
|
+
{/each}
|
|
118
|
+
</div>
|
|
119
|
+
{/if}
|
|
120
|
+
</article>
|
|
121
|
+
|
|
122
|
+
<style>
|
|
123
|
+
.comment {
|
|
124
|
+
padding: 1rem;
|
|
125
|
+
border-radius: 0.5rem;
|
|
126
|
+
background-color: rgb(249 250 251);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.comment.themed {
|
|
130
|
+
background-color: color-mix(in srgb, var(--theme-foreground) 5%, transparent);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.comment.reply {
|
|
134
|
+
background-color: transparent;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.avatar-placeholder {
|
|
138
|
+
background-color: rgb(229 231 235);
|
|
139
|
+
color: rgb(107 114 128);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.avatar-placeholder.themed {
|
|
143
|
+
background-color: color-mix(in srgb, var(--theme-accent) 20%, transparent);
|
|
144
|
+
color: var(--theme-accent);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.author-name {
|
|
148
|
+
color: rgb(0 0 0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.author-name.themed {
|
|
152
|
+
color: var(--theme-foreground);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.quote-attachment {
|
|
156
|
+
border-color: rgb(209 213 219);
|
|
157
|
+
background-color: rgb(255 255 255);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.comment:global(.themed) .quote-attachment {
|
|
161
|
+
border-color: color-mix(in srgb, var(--theme-foreground) 20%, transparent);
|
|
162
|
+
background-color: color-mix(in srgb, var(--theme-background) 50%, transparent);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.replies {
|
|
166
|
+
border-color: rgb(229 231 235);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.replies:global(.themed) {
|
|
170
|
+
border-color: color-mix(in srgb, var(--theme-foreground) 10%, transparent);
|
|
171
|
+
}
|
|
172
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Comment from './Comment.svelte';
|
|
2
|
+
import type { CommentRecord } from '../types.js';
|
|
3
|
+
declare const Comment: import("svelte").Component<{
|
|
4
|
+
comment: CommentRecord & {
|
|
5
|
+
uri: string;
|
|
6
|
+
cid: string;
|
|
7
|
+
author: {
|
|
8
|
+
did: string;
|
|
9
|
+
handle?: string;
|
|
10
|
+
displayName?: string;
|
|
11
|
+
avatar?: string;
|
|
12
|
+
};
|
|
13
|
+
replies?: (CommentRecord & /*elided*/ any)[];
|
|
14
|
+
};
|
|
15
|
+
hasTheme?: boolean;
|
|
16
|
+
/** Whether to show the attachment/quote */
|
|
17
|
+
showQuote?: boolean;
|
|
18
|
+
/** Depth in the thread (for styling) */
|
|
19
|
+
depth?: number;
|
|
20
|
+
}, {}, "">;
|
|
21
|
+
type Comment = ReturnType<typeof Comment>;
|
|
22
|
+
export default Comment;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Comment from './Comment.svelte';
|
|
3
|
+
import { organizeCommentsIntoThreads } from '../utils/native-comments.js';
|
|
4
|
+
|
|
5
|
+
interface CommentProps {
|
|
6
|
+
uri: string;
|
|
7
|
+
cid: string;
|
|
8
|
+
author: {
|
|
9
|
+
did: string;
|
|
10
|
+
handle?: string;
|
|
11
|
+
displayName?: string;
|
|
12
|
+
avatar?: string;
|
|
13
|
+
};
|
|
14
|
+
replies?: CommentProps[];
|
|
15
|
+
$type: 'pub.leaflet.comment';
|
|
16
|
+
subject: string;
|
|
17
|
+
plaintext: string;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
facets?: any[];
|
|
20
|
+
attachment?: any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
/** AT-URI of the document */
|
|
25
|
+
subject: string;
|
|
26
|
+
/** Raw comments from API */
|
|
27
|
+
comments?: Array<any>;
|
|
28
|
+
/** Pre-organized comment threads */
|
|
29
|
+
threads?: CommentProps[];
|
|
30
|
+
hasTheme?: boolean;
|
|
31
|
+
/** Show quote attachments */
|
|
32
|
+
showQuotes?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { subject, comments = [], threads: propThreads, hasTheme = false, showQuotes = true }: Props = $props();
|
|
36
|
+
|
|
37
|
+
// Organize comments into threads if not provided
|
|
38
|
+
const commentThreads = $derived(
|
|
39
|
+
propThreads ?? organizeCommentsIntoThreads(comments)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Total comment count
|
|
43
|
+
const totalComments = $derived(
|
|
44
|
+
commentThreads.reduce((sum, thread) => sum + countThread(thread), 0)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
function countThread(thread: any): number {
|
|
48
|
+
let count = 1;
|
|
49
|
+
if (thread.replies) {
|
|
50
|
+
for (const reply of thread.replies) {
|
|
51
|
+
count += countThread(reply);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return count;
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<section class="comments-section" class:themed={hasTheme}>
|
|
59
|
+
<header class="comments-header mb-6">
|
|
60
|
+
<h2 class="text-xl font-semibold">
|
|
61
|
+
Comments
|
|
62
|
+
{#if totalComments > 0}
|
|
63
|
+
<span class="text-sm font-normal opacity-60">({totalComments})</span>
|
|
64
|
+
{/if}
|
|
65
|
+
</h2>
|
|
66
|
+
</header>
|
|
67
|
+
|
|
68
|
+
{#if commentThreads.length === 0}
|
|
69
|
+
<p class="text-sm opacity-60">No comments yet.</p>
|
|
70
|
+
{:else}
|
|
71
|
+
<div class="comments-list space-y-4">
|
|
72
|
+
{#each commentThreads as thread}
|
|
73
|
+
<Comment comment={thread} {hasTheme} showQuote={showQuotes} />
|
|
74
|
+
{/each}
|
|
75
|
+
</div>
|
|
76
|
+
{/if}
|
|
77
|
+
</section>
|
|
78
|
+
|
|
79
|
+
<style>
|
|
80
|
+
.comments-section {
|
|
81
|
+
margin-top: 2rem;
|
|
82
|
+
padding-top: 2rem;
|
|
83
|
+
border-top: 1px solid rgb(229 231 235);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.comments-section.themed {
|
|
87
|
+
border-top-color: color-mix(in srgb, var(--theme-foreground) 10%, transparent);
|
|
88
|
+
}
|
|
89
|
+
</style>
|