@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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { Document, Publication, AtProtoRecord } from '../types.js';
|
|
3
3
|
import { extractRkey } from '../index.js';
|
|
4
4
|
import { ThemedCard, ThemedText, DateDisplay, TagList } from './index.js';
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
document: AtProtoRecord<Document>;
|
|
@@ -9,6 +10,45 @@
|
|
|
9
10
|
class?: string;
|
|
10
11
|
showCover?: boolean;
|
|
11
12
|
href?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Custom render snippet for the entire card content.
|
|
15
|
+
* Receives { document, publication, href }.
|
|
16
|
+
* If provided, default rendering is replaced entirely.
|
|
17
|
+
*/
|
|
18
|
+
layout?: Snippet<
|
|
19
|
+
[
|
|
20
|
+
{
|
|
21
|
+
document: AtProtoRecord<Document>;
|
|
22
|
+
publication?: AtProtoRecord<Publication>;
|
|
23
|
+
href: string;
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
>;
|
|
27
|
+
/**
|
|
28
|
+
* Custom render snippet for the cover image.
|
|
29
|
+
* Receives { src, alt }.
|
|
30
|
+
*/
|
|
31
|
+
cover?: Snippet<[{ src: string; alt: string }]>;
|
|
32
|
+
/**
|
|
33
|
+
* Custom render snippet for the title.
|
|
34
|
+
* Receives { title, href }.
|
|
35
|
+
*/
|
|
36
|
+
title?: Snippet<[{ title: string; href: string }]>;
|
|
37
|
+
/**
|
|
38
|
+
* Custom render snippet for the description.
|
|
39
|
+
* Receives { description }.
|
|
40
|
+
*/
|
|
41
|
+
description?: Snippet<[{ description: string }]>;
|
|
42
|
+
/**
|
|
43
|
+
* Custom render snippet for the metadata section.
|
|
44
|
+
* Receives { publishedAt, updatedAt }.
|
|
45
|
+
*/
|
|
46
|
+
metadata?: Snippet<[{ publishedAt: string; updatedAt?: string }]>;
|
|
47
|
+
/**
|
|
48
|
+
* Custom render snippet for tags.
|
|
49
|
+
* Receives { tags }.
|
|
50
|
+
*/
|
|
51
|
+
tags?: Snippet<[{ tags: string[] }]>;
|
|
12
52
|
}
|
|
13
53
|
|
|
14
54
|
const {
|
|
@@ -16,7 +56,13 @@
|
|
|
16
56
|
publication,
|
|
17
57
|
class: className = '',
|
|
18
58
|
showCover = true,
|
|
19
|
-
href: customHref
|
|
59
|
+
href: customHref,
|
|
60
|
+
layout,
|
|
61
|
+
cover,
|
|
62
|
+
title,
|
|
63
|
+
description,
|
|
64
|
+
metadata,
|
|
65
|
+
tags
|
|
20
66
|
}: Props = $props();
|
|
21
67
|
|
|
22
68
|
const value = $derived(document.value);
|
|
@@ -32,64 +78,88 @@
|
|
|
32
78
|
{href}
|
|
33
79
|
class="flex gap-6 duration-200 focus-within:shadow-md hover:shadow-md {className}"
|
|
34
80
|
>
|
|
35
|
-
{#if
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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>
|
|
81
|
+
{#if layout}
|
|
82
|
+
{@render layout({ document, publication, href })}
|
|
83
|
+
{:else}
|
|
84
|
+
{#if showCover && value.coverImage}
|
|
85
|
+
{#if cover}
|
|
86
|
+
{@render cover({ src: value.coverImage, alt: `${value.title} cover` })}
|
|
87
|
+
{:else}
|
|
88
|
+
<img
|
|
89
|
+
src={value.coverImage}
|
|
90
|
+
alt="{value.title} cover"
|
|
91
|
+
class="h-48 w-32 shrink-0 rounded-lg object-cover shadow-sm"
|
|
92
|
+
/>
|
|
93
|
+
{/if}
|
|
61
94
|
{/if}
|
|
62
95
|
|
|
63
|
-
<div class="
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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}
|
|
96
|
+
<div class="flex-1">
|
|
97
|
+
{#if title}
|
|
98
|
+
{@render title({ title: value.title, href })}
|
|
99
|
+
{:else}
|
|
100
|
+
<ThemedText
|
|
101
|
+
{hasTheme}
|
|
102
|
+
element="h3"
|
|
103
|
+
class="group-hover:text-primary-600 dark:group-hover:text-primary-400 mb-2 text-2xl font-bold transition-colors"
|
|
77
104
|
>
|
|
78
|
-
|
|
79
|
-
|
|
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>
|
|
105
|
+
{value.title}
|
|
106
|
+
</ThemedText>
|
|
88
107
|
{/if}
|
|
89
|
-
</div>
|
|
90
108
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
{#if value.description}
|
|
110
|
+
{#if description}
|
|
111
|
+
{@render description({ description: value.description })}
|
|
112
|
+
{:else}
|
|
113
|
+
<ThemedText
|
|
114
|
+
{hasTheme}
|
|
115
|
+
opacity={70}
|
|
116
|
+
element="p"
|
|
117
|
+
class="mb-4 line-clamp-3 text-sm leading-relaxed"
|
|
118
|
+
>
|
|
119
|
+
{value.description}
|
|
120
|
+
</ThemedText>
|
|
121
|
+
{/if}
|
|
122
|
+
{/if}
|
|
123
|
+
|
|
124
|
+
{#if metadata}
|
|
125
|
+
{@render metadata({ publishedAt: value.publishedAt, updatedAt: value.updatedAt })}
|
|
126
|
+
{:else}
|
|
127
|
+
<div class="mb-4 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm">
|
|
128
|
+
<DateDisplay
|
|
129
|
+
date={value.publishedAt}
|
|
130
|
+
class="font-medium"
|
|
131
|
+
style={hasTheme
|
|
132
|
+
? `color: ${hasTheme ? 'color-mix(in srgb, var(--theme-foreground) 80%, transparent)' : ''}`
|
|
133
|
+
: ''}
|
|
134
|
+
/>
|
|
135
|
+
{#if value.updatedAt}
|
|
136
|
+
<span
|
|
137
|
+
class="flex items-center gap-1"
|
|
138
|
+
style:color={hasTheme
|
|
139
|
+
? 'color-mix(in srgb, var(--theme-foreground) 60%, transparent)'
|
|
140
|
+
: undefined}
|
|
141
|
+
>
|
|
142
|
+
<span
|
|
143
|
+
class="h-1 w-1 rounded-full"
|
|
144
|
+
class:bg-ink-400={!hasTheme}
|
|
145
|
+
class:dark:bg-ink-600={!hasTheme}
|
|
146
|
+
style:background-color={hasTheme
|
|
147
|
+
? 'color-mix(in srgb, var(--theme-foreground) 40%, transparent)'
|
|
148
|
+
: undefined}
|
|
149
|
+
></span>
|
|
150
|
+
Updated <DateDisplay date={value.updatedAt} />
|
|
151
|
+
</span>
|
|
152
|
+
{/if}
|
|
153
|
+
</div>
|
|
154
|
+
{/if}
|
|
155
|
+
|
|
156
|
+
{#if value.tags && value.tags.length > 0}
|
|
157
|
+
{#if tags}
|
|
158
|
+
{@render tags({ tags: value.tags })}
|
|
159
|
+
{:else}
|
|
160
|
+
<TagList tags={value.tags} {hasTheme} />
|
|
161
|
+
{/if}
|
|
162
|
+
{/if}
|
|
163
|
+
</div>
|
|
164
|
+
{/if}
|
|
95
165
|
</ThemedCard>
|
|
@@ -1,10 +1,61 @@
|
|
|
1
1
|
import type { Document, Publication, AtProtoRecord } from '../types.js';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
2
3
|
interface Props {
|
|
3
4
|
document: AtProtoRecord<Document>;
|
|
4
5
|
publication?: AtProtoRecord<Publication>;
|
|
5
6
|
class?: string;
|
|
6
7
|
showCover?: boolean;
|
|
7
8
|
href?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Custom render snippet for the entire card content.
|
|
11
|
+
* Receives { document, publication, href }.
|
|
12
|
+
* If provided, default rendering is replaced entirely.
|
|
13
|
+
*/
|
|
14
|
+
layout?: Snippet<[
|
|
15
|
+
{
|
|
16
|
+
document: AtProtoRecord<Document>;
|
|
17
|
+
publication?: AtProtoRecord<Publication>;
|
|
18
|
+
href: string;
|
|
19
|
+
}
|
|
20
|
+
]>;
|
|
21
|
+
/**
|
|
22
|
+
* Custom render snippet for the cover image.
|
|
23
|
+
* Receives { src, alt }.
|
|
24
|
+
*/
|
|
25
|
+
cover?: Snippet<[{
|
|
26
|
+
src: string;
|
|
27
|
+
alt: string;
|
|
28
|
+
}]>;
|
|
29
|
+
/**
|
|
30
|
+
* Custom render snippet for the title.
|
|
31
|
+
* Receives { title, href }.
|
|
32
|
+
*/
|
|
33
|
+
title?: Snippet<[{
|
|
34
|
+
title: string;
|
|
35
|
+
href: string;
|
|
36
|
+
}]>;
|
|
37
|
+
/**
|
|
38
|
+
* Custom render snippet for the description.
|
|
39
|
+
* Receives { description }.
|
|
40
|
+
*/
|
|
41
|
+
description?: Snippet<[{
|
|
42
|
+
description: string;
|
|
43
|
+
}]>;
|
|
44
|
+
/**
|
|
45
|
+
* Custom render snippet for the metadata section.
|
|
46
|
+
* Receives { publishedAt, updatedAt }.
|
|
47
|
+
*/
|
|
48
|
+
metadata?: Snippet<[{
|
|
49
|
+
publishedAt: string;
|
|
50
|
+
updatedAt?: string;
|
|
51
|
+
}]>;
|
|
52
|
+
/**
|
|
53
|
+
* Custom render snippet for tags.
|
|
54
|
+
* Receives { tags }.
|
|
55
|
+
*/
|
|
56
|
+
tags?: Snippet<[{
|
|
57
|
+
tags: string[];
|
|
58
|
+
}]>;
|
|
8
59
|
}
|
|
9
60
|
declare const DocumentCard: import("svelte").Component<Props, {}, "">;
|
|
10
61
|
type DocumentCard = ReturnType<typeof DocumentCard>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import RichText from './document/RichText.svelte';
|
|
3
|
+
|
|
4
|
+
export interface Footnote {
|
|
5
|
+
footnoteId: string;
|
|
6
|
+
contentPlaintext: string;
|
|
7
|
+
contentFacets?: any[];
|
|
8
|
+
number: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
footnotes: Footnote[];
|
|
13
|
+
hasTheme?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { footnotes, hasTheme = false }: Props = $props();
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
{#if footnotes.length > 0}
|
|
20
|
+
<section class="footnotes mt-12 pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
21
|
+
<h2 class="text-lg font-semibold mb-4">Footnotes</h2>
|
|
22
|
+
<ol class="footnotes-list space-y-3">
|
|
23
|
+
{#each footnotes as footnote}
|
|
24
|
+
<li id={footnote.footnoteId} class="footnote-item flex gap-3">
|
|
25
|
+
<span class="footnote-number shrink-0 w-6 text-right text-sm text-gray-500 dark:text-gray-400">
|
|
26
|
+
{footnote.number}.
|
|
27
|
+
</span>
|
|
28
|
+
<div class="flex-1 text-sm">
|
|
29
|
+
<RichText plaintext={footnote.contentPlaintext} facets={footnote.contentFacets} {hasTheme} />
|
|
30
|
+
<a
|
|
31
|
+
href="#{footnote.footnoteId}-ref"
|
|
32
|
+
class="footnote-backref ml-1 text-xs"
|
|
33
|
+
class:themed={hasTheme}
|
|
34
|
+
aria-label="Back to reference"
|
|
35
|
+
>
|
|
36
|
+
↩
|
|
37
|
+
</a>
|
|
38
|
+
</div>
|
|
39
|
+
</li>
|
|
40
|
+
{/each}
|
|
41
|
+
</ol>
|
|
42
|
+
</section>
|
|
43
|
+
{/if}
|
|
44
|
+
|
|
45
|
+
<style>
|
|
46
|
+
.footnotes-list {
|
|
47
|
+
list-style: none;
|
|
48
|
+
padding-left: 0;
|
|
49
|
+
counter-reset: footnote;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.footnote-item {
|
|
53
|
+
scroll-margin-top: 2rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.footnote-number {
|
|
57
|
+
color: rgb(107 114 128);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.footnote-backref {
|
|
61
|
+
color: rgb(0 0 225);
|
|
62
|
+
text-decoration: none;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.footnote-backref.themed {
|
|
66
|
+
color: var(--theme-accent);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.footnote-backref:hover {
|
|
70
|
+
text-decoration: underline;
|
|
71
|
+
}
|
|
72
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface Footnote {
|
|
2
|
+
footnoteId: string;
|
|
3
|
+
contentPlaintext: string;
|
|
4
|
+
contentFacets?: any[];
|
|
5
|
+
number: number;
|
|
6
|
+
}
|
|
7
|
+
interface Props {
|
|
8
|
+
footnotes: Footnote[];
|
|
9
|
+
hasTheme?: boolean;
|
|
10
|
+
}
|
|
11
|
+
declare const Footnotes: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type Footnotes = ReturnType<typeof Footnotes>;
|
|
13
|
+
export default Footnotes;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
/** AT-URI of the document */
|
|
6
|
+
documentUri: string;
|
|
7
|
+
/** Initial recommend count */
|
|
8
|
+
count?: number;
|
|
9
|
+
/** Whether current user has recommended */
|
|
10
|
+
recommended?: boolean;
|
|
11
|
+
/** Has theme applied */
|
|
12
|
+
hasTheme?: boolean;
|
|
13
|
+
/** On recommend callback */
|
|
14
|
+
onRecommend?: () => Promise<void>;
|
|
15
|
+
/** On unrecommend callback */
|
|
16
|
+
onUnrecommend?: () => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
documentUri,
|
|
21
|
+
count = 0,
|
|
22
|
+
recommended = false,
|
|
23
|
+
hasTheme = false,
|
|
24
|
+
onRecommend,
|
|
25
|
+
onUnrecommend
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
let isRecommended = $state(recommended);
|
|
29
|
+
let recommendCount = $state(count);
|
|
30
|
+
let isLoading = $state(false);
|
|
31
|
+
|
|
32
|
+
async function toggleRecommend() {
|
|
33
|
+
if (isLoading) return;
|
|
34
|
+
isLoading = true;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
if (isRecommended) {
|
|
38
|
+
if (onUnrecommend) {
|
|
39
|
+
await onUnrecommend();
|
|
40
|
+
}
|
|
41
|
+
isRecommended = false;
|
|
42
|
+
recommendCount = Math.max(0, recommendCount - 1);
|
|
43
|
+
} else {
|
|
44
|
+
if (onRecommend) {
|
|
45
|
+
await onRecommend();
|
|
46
|
+
}
|
|
47
|
+
isRecommended = true;
|
|
48
|
+
recommendCount += 1;
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('Failed to toggle recommend:', error);
|
|
52
|
+
} finally {
|
|
53
|
+
isLoading = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<button
|
|
59
|
+
class="recommend-button"
|
|
60
|
+
class:recommended={isRecommended}
|
|
61
|
+
class:themed={hasTheme}
|
|
62
|
+
class:loading={isLoading}
|
|
63
|
+
onclick={toggleRecommend}
|
|
64
|
+
disabled={isLoading}
|
|
65
|
+
aria-label={isRecommended ? 'Remove recommendation' : 'Recommend'}
|
|
66
|
+
aria-pressed={isRecommended}
|
|
67
|
+
>
|
|
68
|
+
<span class="icon">
|
|
69
|
+
{#if isRecommended}
|
|
70
|
+
<svg viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
|
71
|
+
<path
|
|
72
|
+
d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001a.752.752 0 01-.704 0l-.003-.001z"
|
|
73
|
+
/>
|
|
74
|
+
</svg>
|
|
75
|
+
{:else}
|
|
76
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-5 h-5">
|
|
77
|
+
<path
|
|
78
|
+
stroke-linecap="round"
|
|
79
|
+
stroke-linejoin="round"
|
|
80
|
+
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"
|
|
81
|
+
/>
|
|
82
|
+
</svg>
|
|
83
|
+
{/if}
|
|
84
|
+
</span>
|
|
85
|
+
{#if recommendCount > 0}
|
|
86
|
+
<span class="count">{recommendCount}</span>
|
|
87
|
+
{/if}
|
|
88
|
+
</button>
|
|
89
|
+
|
|
90
|
+
<style>
|
|
91
|
+
.recommend-button {
|
|
92
|
+
display: inline-flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: 0.25rem;
|
|
95
|
+
padding: 0.5rem 0.75rem;
|
|
96
|
+
border-radius: 9999px;
|
|
97
|
+
border: 1px solid rgb(229 231 235);
|
|
98
|
+
background-color: transparent;
|
|
99
|
+
color: rgb(107 114 128);
|
|
100
|
+
font-size: 0.875rem;
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
transition: all 0.15s;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.recommend-button:hover:not(:disabled) {
|
|
106
|
+
background-color: rgb(249 250 251);
|
|
107
|
+
border-color: rgb(209 213 219);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.recommend-button.recommended {
|
|
111
|
+
color: rgb(239 68 68);
|
|
112
|
+
border-color: rgb(254 202 202);
|
|
113
|
+
background-color: rgb(254 242 242);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.recommend-button.recommended:hover:not(:disabled) {
|
|
117
|
+
background-color: rgb(254 226 226);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.recommend-button.themed {
|
|
121
|
+
border-color: color-mix(in srgb, var(--theme-foreground) 20%, transparent);
|
|
122
|
+
color: var(--theme-foreground);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.recommend-button.themed:hover:not(:disabled) {
|
|
126
|
+
background-color: color-mix(in srgb, var(--theme-foreground) 5%, transparent);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.recommend-button.themed.recommended {
|
|
130
|
+
color: var(--theme-accent);
|
|
131
|
+
border-color: var(--theme-accent);
|
|
132
|
+
background-color: color-mix(in srgb, var(--theme-accent) 10%, transparent);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.recommend-button.themed.recommended:hover:not(:disabled) {
|
|
136
|
+
background-color: color-mix(in srgb, var(--theme-accent) 20%, transparent);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.recommend-button:disabled {
|
|
140
|
+
opacity: 0.6;
|
|
141
|
+
cursor: not-allowed;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.icon {
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.count {
|
|
151
|
+
font-weight: 500;
|
|
152
|
+
}
|
|
153
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** AT-URI of the document */
|
|
3
|
+
documentUri: string;
|
|
4
|
+
/** Initial recommend count */
|
|
5
|
+
count?: number;
|
|
6
|
+
/** Whether current user has recommended */
|
|
7
|
+
recommended?: boolean;
|
|
8
|
+
/** Has theme applied */
|
|
9
|
+
hasTheme?: boolean;
|
|
10
|
+
/** On recommend callback */
|
|
11
|
+
onRecommend?: () => Promise<void>;
|
|
12
|
+
/** On unrecommend callback */
|
|
13
|
+
onUnrecommend?: () => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
declare const RecommendButton: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type RecommendButton = ReturnType<typeof RecommendButton>;
|
|
17
|
+
export default RecommendButton;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import type { BasicTheme, ExtendedTheme, BackgroundImage } from '../types.js';
|
|
4
|
+
import {
|
|
5
|
+
anyThemeToCssVars,
|
|
6
|
+
getBackgroundImageStyles,
|
|
7
|
+
getFontStyles,
|
|
8
|
+
getPageWidthStyles
|
|
9
|
+
} from '../utils/theme-helpers.js';
|
|
10
|
+
import { getGoogleFontsUrl } from '../utils/theme.js';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
theme?: BasicTheme | ExtendedTheme;
|
|
14
|
+
/** Child content */
|
|
15
|
+
children: import('svelte').Snippet;
|
|
16
|
+
/** Whether to apply the theme */
|
|
17
|
+
applyTheme?: boolean;
|
|
18
|
+
/** CSS class for the container */
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { theme, children, applyTheme = true, class: className = '' }: Props = $props();
|
|
23
|
+
|
|
24
|
+
// Get the Google Fonts URL if custom fonts are specified
|
|
25
|
+
const googleFontsUrl = $derived(
|
|
26
|
+
theme && 'headingFont' in theme
|
|
27
|
+
? getGoogleFontsUrl({
|
|
28
|
+
headingFont: (theme as ExtendedTheme).headingFont,
|
|
29
|
+
bodyFont: (theme as ExtendedTheme).bodyFont
|
|
30
|
+
})
|
|
31
|
+
: null
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Background image styles
|
|
35
|
+
const bgImageStyles = $derived(
|
|
36
|
+
theme && 'backgroundImage' in theme
|
|
37
|
+
? getBackgroundImageStyles((theme as ExtendedTheme).backgroundImage)
|
|
38
|
+
: {}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Page width styles
|
|
42
|
+
const pageWidthStyles = $derived(
|
|
43
|
+
theme && 'pageWidth' in theme
|
|
44
|
+
? getPageWidthStyles(theme as ExtendedTheme)
|
|
45
|
+
: {}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Font styles
|
|
49
|
+
const fontStyles = $derived(
|
|
50
|
+
theme && 'bodyFont' in theme
|
|
51
|
+
? getFontStyles(theme as ExtendedTheme)
|
|
52
|
+
: {}
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Theme CSS variables
|
|
56
|
+
const themeVars = $derived(applyTheme ? anyThemeToCssVars(theme) : {});
|
|
57
|
+
|
|
58
|
+
// Load Google Fonts on mount
|
|
59
|
+
onMount(() => {
|
|
60
|
+
if (googleFontsUrl) {
|
|
61
|
+
// Check if font link already exists
|
|
62
|
+
const existingLink = document.querySelector(`link[href="${googleFontsUrl}"]`);
|
|
63
|
+
if (!existingLink) {
|
|
64
|
+
const link = document.createElement('link');
|
|
65
|
+
link.rel = 'stylesheet';
|
|
66
|
+
link.href = googleFontsUrl;
|
|
67
|
+
document.head.appendChild(link);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<div
|
|
74
|
+
class="theme-provider {className}"
|
|
75
|
+
style:bg-image={bgImageStyles.backgroundImage ? 'url' : undefined}
|
|
76
|
+
style={Object.entries({ ...themeVars, ...fontStyles, ...pageWidthStyles })
|
|
77
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
78
|
+
.join('; ')}
|
|
79
|
+
>
|
|
80
|
+
{#if googleFontsUrl}
|
|
81
|
+
<!-- Google Fonts will be loaded via onMount -->
|
|
82
|
+
{/if}
|
|
83
|
+
|
|
84
|
+
{@render children()}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<style>
|
|
88
|
+
.theme-provider {
|
|
89
|
+
min-height: 100%;
|
|
90
|
+
width: 100%;
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BasicTheme, ExtendedTheme } from '../types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
theme?: BasicTheme | ExtendedTheme;
|
|
4
|
+
/** Child content */
|
|
5
|
+
children: import('svelte').Snippet;
|
|
6
|
+
/** Whether to apply the theme */
|
|
7
|
+
applyTheme?: boolean;
|
|
8
|
+
/** CSS class for the container */
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
declare const ThemeProvider: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type ThemeProvider = ReturnType<typeof ThemeProvider>;
|
|
13
|
+
export default ThemeProvider;
|