@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,177 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createEventDispatcher } from 'svelte';
|
|
3
|
+
|
|
4
|
+
export type ToastType = 'success' | 'error' | 'info' | 'warning';
|
|
5
|
+
|
|
6
|
+
export interface ToastMessage {
|
|
7
|
+
id: string;
|
|
8
|
+
message: string;
|
|
9
|
+
type: ToastType;
|
|
10
|
+
duration?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
toasts: ToastMessage[];
|
|
15
|
+
position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
|
|
16
|
+
hasTheme?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { toasts, position = 'bottom-right', hasTheme = false }: Props = $props();
|
|
20
|
+
|
|
21
|
+
const dispatch = createEventDispatcher<{ dismiss: string }>();
|
|
22
|
+
|
|
23
|
+
function dismiss(id: string) {
|
|
24
|
+
dispatch('dismiss', id);
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<div class="toast-container {position}" class:themed={hasTheme}>
|
|
29
|
+
{#each toasts as toast}
|
|
30
|
+
<div
|
|
31
|
+
class="toast {toast.type}"
|
|
32
|
+
class:themed={hasTheme}
|
|
33
|
+
role="alert"
|
|
34
|
+
aria-live="polite"
|
|
35
|
+
>
|
|
36
|
+
<div class="toast-icon">
|
|
37
|
+
{#if toast.type === 'success'}
|
|
38
|
+
<svg viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
|
39
|
+
<path
|
|
40
|
+
fill-rule="evenodd"
|
|
41
|
+
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
|
42
|
+
clip-rule="evenodd"
|
|
43
|
+
/>
|
|
44
|
+
</svg>
|
|
45
|
+
{:else if toast.type === 'error'}
|
|
46
|
+
<svg viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
|
47
|
+
<path
|
|
48
|
+
fill-rule="evenodd"
|
|
49
|
+
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
|
|
50
|
+
clip-rule="evenodd"
|
|
51
|
+
/>
|
|
52
|
+
</svg>
|
|
53
|
+
{:else if toast.type === 'warning'}
|
|
54
|
+
<svg viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
|
55
|
+
<path
|
|
56
|
+
fill-rule="evenodd"
|
|
57
|
+
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.63-1.516 2.63H3.72c-1.347 0-2.189-1.463-1.516-2.63l6.28-10.875zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
|
|
58
|
+
clip-rule="evenodd"
|
|
59
|
+
/>
|
|
60
|
+
</svg>
|
|
61
|
+
{:else}
|
|
62
|
+
<svg viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
|
63
|
+
<path
|
|
64
|
+
fill-rule="evenodd"
|
|
65
|
+
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
|
|
66
|
+
clip-rule="evenodd"
|
|
67
|
+
/>
|
|
68
|
+
</svg>
|
|
69
|
+
{/if}
|
|
70
|
+
</div>
|
|
71
|
+
<span class="toast-message">{toast.message}</span>
|
|
72
|
+
<button class="toast-dismiss" onclick={() => dismiss(toast.id)} aria-label="Dismiss">
|
|
73
|
+
<svg viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
|
74
|
+
<path
|
|
75
|
+
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
76
|
+
/>
|
|
77
|
+
</svg>
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
{/each}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<style>
|
|
84
|
+
.toast-container {
|
|
85
|
+
position: fixed;
|
|
86
|
+
z-index: 9999;
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
gap: 0.5rem;
|
|
90
|
+
max-width: 24rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.top-right {
|
|
94
|
+
top: 1rem;
|
|
95
|
+
right: 1rem;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.top-left {
|
|
99
|
+
top: 1rem;
|
|
100
|
+
left: 1rem;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.bottom-right {
|
|
104
|
+
bottom: 1rem;
|
|
105
|
+
right: 1rem;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.bottom-left {
|
|
109
|
+
bottom: 1rem;
|
|
110
|
+
left: 1rem;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.toast {
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
gap: 0.75rem;
|
|
117
|
+
padding: 0.75rem 1rem;
|
|
118
|
+
border-radius: 0.5rem;
|
|
119
|
+
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
120
|
+
animation: slide-in 0.2s ease-out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@keyframes slide-in {
|
|
124
|
+
from {
|
|
125
|
+
transform: translateX(100%);
|
|
126
|
+
opacity: 0;
|
|
127
|
+
}
|
|
128
|
+
to {
|
|
129
|
+
transform: translateX(0);
|
|
130
|
+
opacity: 1;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.toast.success {
|
|
135
|
+
background-color: rgb(220 252 231);
|
|
136
|
+
color: rgb(22 101 52);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.toast.error {
|
|
140
|
+
background-color: rgb(254 226 226);
|
|
141
|
+
color: rgb(185 28 28);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.toast.warning {
|
|
145
|
+
background-color: rgb(254 249 195);
|
|
146
|
+
color: rgb(161 98 7);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.toast.info {
|
|
150
|
+
background-color: rgb(239 246 255);
|
|
151
|
+
color: rgb(30 64 175);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.toast-icon {
|
|
155
|
+
flex-shrink: 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.toast-message {
|
|
159
|
+
flex: 1;
|
|
160
|
+
font-size: 0.875rem;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.toast-dismiss {
|
|
164
|
+
flex-shrink: 0;
|
|
165
|
+
padding: 0.25rem;
|
|
166
|
+
border-radius: 0.25rem;
|
|
167
|
+
background: transparent;
|
|
168
|
+
border: none;
|
|
169
|
+
cursor: pointer;
|
|
170
|
+
opacity: 0.6;
|
|
171
|
+
transition: opacity 0.15s;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.toast-dismiss:hover {
|
|
175
|
+
opacity: 1;
|
|
176
|
+
}
|
|
177
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type ToastType = 'success' | 'error' | 'info' | 'warning';
|
|
2
|
+
export interface ToastMessage {
|
|
3
|
+
id: string;
|
|
4
|
+
message: string;
|
|
5
|
+
type: ToastType;
|
|
6
|
+
duration?: number;
|
|
7
|
+
}
|
|
8
|
+
interface Props {
|
|
9
|
+
toasts: ToastMessage[];
|
|
10
|
+
position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
|
|
11
|
+
hasTheme?: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
14
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
15
|
+
$$bindings?: Bindings;
|
|
16
|
+
} & Exports;
|
|
17
|
+
(internal: unknown, props: Props & {
|
|
18
|
+
$$events?: Events;
|
|
19
|
+
$$slots?: Slots;
|
|
20
|
+
}): Exports & {
|
|
21
|
+
$set?: any;
|
|
22
|
+
$on?: any;
|
|
23
|
+
};
|
|
24
|
+
z_$$bindings?: Bindings;
|
|
25
|
+
}
|
|
26
|
+
declare const Toast: $$__sveltets_2_IsomorphicComponent<Props, {
|
|
27
|
+
dismiss: CustomEvent<string>;
|
|
28
|
+
} & {
|
|
29
|
+
[evt: string]: CustomEvent<any>;
|
|
30
|
+
}, {}, {}, "">;
|
|
31
|
+
type Toast = InstanceType<typeof Toast>;
|
|
32
|
+
export default Toast;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Text to display as watermark */
|
|
4
|
+
text?: string;
|
|
5
|
+
/** Publication name */
|
|
6
|
+
publicationName?: string;
|
|
7
|
+
/** Publication URL */
|
|
8
|
+
publicationUrl?: string;
|
|
9
|
+
/** Position of watermark */
|
|
10
|
+
position?: 'bottom-right' | 'bottom-left' | 'bottom-center';
|
|
11
|
+
/** Opacity (0-1) */
|
|
12
|
+
opacity?: number;
|
|
13
|
+
/** Has theme applied */
|
|
14
|
+
hasTheme?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
text,
|
|
19
|
+
publicationName,
|
|
20
|
+
publicationUrl,
|
|
21
|
+
position = 'bottom-right',
|
|
22
|
+
opacity = 0.6,
|
|
23
|
+
hasTheme = false
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
// Display text - prefer explicit text, then publication name
|
|
27
|
+
const displayText = $derived(text || publicationName || 'Leaflet');
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<div
|
|
31
|
+
class="watermark {position}"
|
|
32
|
+
class:themed={hasTheme}
|
|
33
|
+
style:opacity={opacity}
|
|
34
|
+
>
|
|
35
|
+
{#if publicationUrl && !text}
|
|
36
|
+
<a
|
|
37
|
+
href={publicationUrl}
|
|
38
|
+
target="_blank"
|
|
39
|
+
rel="noopener noreferrer"
|
|
40
|
+
class="watermark-link"
|
|
41
|
+
>
|
|
42
|
+
{displayText}
|
|
43
|
+
</a>
|
|
44
|
+
{:else}
|
|
45
|
+
<span class="watermark-text">{displayText}</span>
|
|
46
|
+
{/if}
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<style>
|
|
50
|
+
.watermark {
|
|
51
|
+
position: fixed;
|
|
52
|
+
padding: 0.5rem 1rem;
|
|
53
|
+
font-size: 0.75rem;
|
|
54
|
+
font-weight: 500;
|
|
55
|
+
letter-spacing: 0.05em;
|
|
56
|
+
text-transform: uppercase;
|
|
57
|
+
pointer-events: none;
|
|
58
|
+
z-index: 100;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.bottom-right {
|
|
62
|
+
bottom: 1rem;
|
|
63
|
+
right: 1rem;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.bottom-left {
|
|
67
|
+
bottom: 1rem;
|
|
68
|
+
left: 1rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.bottom-center {
|
|
72
|
+
bottom: 1rem;
|
|
73
|
+
left: 50%;
|
|
74
|
+
transform: translateX(-50%);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.watermark-text {
|
|
78
|
+
color: rgb(107 114 128);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.watermark-link {
|
|
82
|
+
color: rgb(107 114 128);
|
|
83
|
+
text-decoration: none;
|
|
84
|
+
pointer-events: auto;
|
|
85
|
+
transition: color 0.15s;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.watermark-link:hover {
|
|
89
|
+
color: rgb(0 0 225);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.watermark.themed .watermark-text,
|
|
93
|
+
.watermark.themed .watermark-link {
|
|
94
|
+
color: var(--theme-foreground);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.watermark.themed .watermark-link:hover {
|
|
98
|
+
color: var(--theme-accent);
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Text to display as watermark */
|
|
3
|
+
text?: string;
|
|
4
|
+
/** Publication name */
|
|
5
|
+
publicationName?: string;
|
|
6
|
+
/** Publication URL */
|
|
7
|
+
publicationUrl?: string;
|
|
8
|
+
/** Position of watermark */
|
|
9
|
+
position?: 'bottom-right' | 'bottom-left' | 'bottom-center';
|
|
10
|
+
/** Opacity (0-1) */
|
|
11
|
+
opacity?: number;
|
|
12
|
+
/** Has theme applied */
|
|
13
|
+
hasTheme?: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare const Watermark: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type Watermark = ReturnType<typeof Watermark>;
|
|
17
|
+
export default Watermark;
|
|
@@ -9,9 +9,14 @@
|
|
|
9
9
|
children: Snippet;
|
|
10
10
|
class?: string;
|
|
11
11
|
href?: string;
|
|
12
|
+
/**
|
|
13
|
+
* If true, renders only the themed wrapper without default styles.
|
|
14
|
+
* Useful for custom layouts — pass your own classes via `class`.
|
|
15
|
+
*/
|
|
16
|
+
headless?: boolean;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
let { theme, children, class: className = '', href }: Props = $props();
|
|
19
|
+
let { theme, children, class: className = '', href, headless = false }: Props = $props();
|
|
15
20
|
|
|
16
21
|
const themeVars = $derived(theme ? getThemeVars(theme) : {});
|
|
17
22
|
const hasTheme = $derived(!!theme);
|
|
@@ -30,20 +35,19 @@
|
|
|
30
35
|
}
|
|
31
36
|
return base;
|
|
32
37
|
});
|
|
38
|
+
|
|
39
|
+
// Default styles for non-headless mode
|
|
40
|
+
const defaultStyles = $derived(
|
|
41
|
+
headless
|
|
42
|
+
? ''
|
|
43
|
+
: 'rounded-lg border p-6 transition-all bg-canvas-50 dark:bg-canvas-950 border-canvas-200 dark:border-canvas-800 hover:border-primary-300 dark:hover:border-primary-700 focus-within:border-primary-300 dark:focus-within:border-primary-700'
|
|
44
|
+
);
|
|
33
45
|
</script>
|
|
34
46
|
|
|
35
47
|
{#if href}
|
|
36
48
|
<a {href} class="group block">
|
|
37
49
|
<article
|
|
38
|
-
class="
|
|
39
|
-
class:bg-canvas-50={!hasTheme}
|
|
40
|
-
class:dark:bg-canvas-950={!hasTheme}
|
|
41
|
-
class:border-canvas-200={!hasTheme}
|
|
42
|
-
class:dark:border-canvas-800={!hasTheme}
|
|
43
|
-
class:hover:border-primary-300={!hasTheme}
|
|
44
|
-
class:dark:hover:border-primary-700={!hasTheme}
|
|
45
|
-
class:focus-within:border-primary-300={!hasTheme}
|
|
46
|
-
class:dark:focus-within:border-primary-700={!hasTheme}
|
|
50
|
+
class="{defaultStyles} {className}"
|
|
47
51
|
style:background-color={hasTheme ? 'var(--theme-background)' : undefined}
|
|
48
52
|
style={allStyles()}
|
|
49
53
|
>
|
|
@@ -52,11 +56,7 @@
|
|
|
52
56
|
</a>
|
|
53
57
|
{:else}
|
|
54
58
|
<article
|
|
55
|
-
class="
|
|
56
|
-
class:bg-canvas-50={!hasTheme}
|
|
57
|
-
class:dark:bg-canvas-950={!hasTheme}
|
|
58
|
-
class:border-canvas-200={!hasTheme}
|
|
59
|
-
class:dark:border-canvas-800={!hasTheme}
|
|
59
|
+
class="{defaultStyles} {className}"
|
|
60
60
|
style:background-color={hasTheme ? 'var(--theme-background)' : undefined}
|
|
61
61
|
style={allStyles()}
|
|
62
62
|
>
|
|
@@ -5,6 +5,11 @@ interface Props {
|
|
|
5
5
|
children: Snippet;
|
|
6
6
|
class?: string;
|
|
7
7
|
href?: string;
|
|
8
|
+
/**
|
|
9
|
+
* If true, renders only the themed wrapper without default styles.
|
|
10
|
+
* Useful for custom layouts — pass your own classes via `class`.
|
|
11
|
+
*/
|
|
12
|
+
headless?: boolean;
|
|
8
13
|
}
|
|
9
14
|
declare const ThemedCard: import("svelte").Component<Props, {}, "">;
|
|
10
15
|
type ThemedCard = ReturnType<typeof ThemedCard>;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import CodeBlock from './blocks/CodeBlock.svelte';
|
|
7
7
|
import MathBlock from './blocks/MathBlock.svelte';
|
|
8
8
|
import UnorderedListBlock from './blocks/UnorderedListBlock.svelte';
|
|
9
|
+
import OrderedListBlock from './blocks/OrderedListBlock.svelte';
|
|
9
10
|
import HorizontalRuleBlock from './blocks/HorizontalRuleBlock.svelte';
|
|
10
11
|
import IframeBlock from './blocks/IframeBlock.svelte';
|
|
11
12
|
import WebsiteBlock from './blocks/WebsiteBlock.svelte';
|
|
@@ -38,6 +39,8 @@
|
|
|
38
39
|
<MathBlock {block} {hasTheme} />
|
|
39
40
|
{:else if block.$type === 'pub.leaflet.blocks.unorderedList'}
|
|
40
41
|
<UnorderedListBlock {block} {hasTheme} />
|
|
42
|
+
{:else if block.$type === 'pub.leaflet.blocks.orderedList'}
|
|
43
|
+
<OrderedListBlock {block} {hasTheme} {did} {pds} />
|
|
41
44
|
{:else if block.$type === 'pub.leaflet.blocks.horizontalRule'}
|
|
42
45
|
<HorizontalRuleBlock {hasTheme} />
|
|
43
46
|
{:else if block.$type === 'pub.leaflet.blocks.iframe'}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { setContext, getContext } from 'svelte';
|
|
2
3
|
import type { Document } from '../../types.js';
|
|
3
4
|
import LeafletContentRenderer from './LeafletContentRenderer.svelte';
|
|
4
5
|
import MarkdownRenderer from './MarkdownRenderer.svelte';
|
|
6
|
+
import Footnotes from '../Footnotes.svelte';
|
|
5
7
|
import { mixThemeColor } from '../../utils/theme-helpers.js';
|
|
6
8
|
|
|
9
|
+
interface FootnoteData {
|
|
10
|
+
footnoteId: string;
|
|
11
|
+
contentPlaintext: string;
|
|
12
|
+
contentFacets?: any[];
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
interface Props {
|
|
8
16
|
document: Document;
|
|
9
17
|
/** DID of the record author — needed to resolve blob URLs */
|
|
@@ -20,6 +28,34 @@
|
|
|
20
28
|
|
|
21
29
|
const { document, did = '', pds = '', hasTheme = false, prerenderedHtml }: Props = $props();
|
|
22
30
|
|
|
31
|
+
// Footnote collection state
|
|
32
|
+
let footnoteCounter = $state(0);
|
|
33
|
+
let footnotes = $state<Array<FootnoteData & { number: number }>>([]);
|
|
34
|
+
|
|
35
|
+
// Reset footnotes when document changes
|
|
36
|
+
$effect(() => {
|
|
37
|
+
footnoteCounter = 0;
|
|
38
|
+
footnotes = [];
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Set up footnote context for child components
|
|
42
|
+
function registerFootnote(footnote: FootnoteData): number {
|
|
43
|
+
// Check if footnote already registered
|
|
44
|
+
const existing = footnotes.find((f) => f.footnoteId === footnote.footnoteId);
|
|
45
|
+
if (existing) {
|
|
46
|
+
return existing.number;
|
|
47
|
+
}
|
|
48
|
+
footnoteCounter++;
|
|
49
|
+
const newFootnote = { ...footnote, number: footnoteCounter };
|
|
50
|
+
footnotes = [...footnotes, newFootnote];
|
|
51
|
+
return footnoteCounter;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setContext('footnotes', {
|
|
55
|
+
registerFootnote,
|
|
56
|
+
getFootnotes: () => footnotes
|
|
57
|
+
});
|
|
58
|
+
|
|
23
59
|
// Content priority:
|
|
24
60
|
// 1. pub.leaflet.content — rich Leaflet block tree
|
|
25
61
|
// 2. textContent — stored as (possibly markdown) text; render via MarkdownRenderer
|
|
@@ -60,9 +96,13 @@
|
|
|
60
96
|
document.content,
|
|
61
97
|
null,
|
|
62
98
|
2
|
|
63
|
-
)}</pre
|
|
99
|
+
)}</pre
|
|
100
|
+
>
|
|
64
101
|
</div>
|
|
65
102
|
{:else}
|
|
66
103
|
<p class="italic opacity-50">No content available</p>
|
|
67
104
|
{/if}
|
|
105
|
+
|
|
106
|
+
<!-- Footnotes section -->
|
|
107
|
+
<Footnotes {footnotes} {hasTheme} />
|
|
68
108
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import InlineMath from './InlineMath.svelte';
|
|
3
3
|
import { UnicodeString } from '@atproto/api';
|
|
4
|
+
import { getContext, setContext } from 'svelte';
|
|
4
5
|
|
|
5
6
|
interface Facet {
|
|
6
7
|
index: {
|
|
@@ -13,13 +14,32 @@
|
|
|
13
14
|
}>;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
interface FootnoteData {
|
|
18
|
+
footnoteId: string;
|
|
19
|
+
contentPlaintext: string;
|
|
20
|
+
contentFacets?: Facet[];
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
interface Props {
|
|
17
24
|
plaintext: string;
|
|
18
25
|
facets?: Facet[];
|
|
19
26
|
hasTheme?: boolean;
|
|
27
|
+
/** Footnote index in the document (for numbering) */
|
|
28
|
+
footnoteIndex?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { plaintext, facets = [], hasTheme = false, footnoteIndex = 0 }: Props = $props();
|
|
32
|
+
|
|
33
|
+
// Footnote context - allows parent components to collect footnotes
|
|
34
|
+
interface FootnoteContext {
|
|
35
|
+
registerFootnote: (footnote: FootnoteData) => number;
|
|
36
|
+
getFootnotes: () => Array<FootnoteData & { number: number }>;
|
|
20
37
|
}
|
|
21
38
|
|
|
22
|
-
const
|
|
39
|
+
const footnoteContext = getContext<FootnoteContext | undefined>('footnotes');
|
|
40
|
+
|
|
41
|
+
// Track footnotes found in this text
|
|
42
|
+
const foundFootnotes: Map<string, number> = new Map();
|
|
23
43
|
|
|
24
44
|
interface RichTextSegment {
|
|
25
45
|
text: string;
|
|
@@ -96,10 +116,13 @@
|
|
|
96
116
|
isMath: boolean;
|
|
97
117
|
isDidMention: boolean;
|
|
98
118
|
isAtMention: boolean;
|
|
119
|
+
isFootnote: boolean;
|
|
99
120
|
link?: string;
|
|
100
121
|
id?: string;
|
|
101
122
|
did?: string;
|
|
102
123
|
atURI?: string;
|
|
124
|
+
footnote?: FootnoteData;
|
|
125
|
+
footnoteNumber?: number;
|
|
103
126
|
}
|
|
104
127
|
|
|
105
128
|
function processSegments(): ProcessedSegment[] {
|
|
@@ -131,6 +154,26 @@
|
|
|
131
154
|
);
|
|
132
155
|
const isMath = segment.facet?.some((f) => f.$type === 'pub.leaflet.richtext.facet#math');
|
|
133
156
|
|
|
157
|
+
// Handle footnote
|
|
158
|
+
const footnoteFeature = segment.facet?.find(
|
|
159
|
+
(f) => f.$type === 'pub.leaflet.richtext.facet#footnote'
|
|
160
|
+
) as FootnoteFeature | undefined;
|
|
161
|
+
|
|
162
|
+
let footnoteNumber: number | undefined;
|
|
163
|
+
if (footnoteFeature && footnoteContext) {
|
|
164
|
+
if (!foundFootnotes.has(footnoteFeature.footnoteId)) {
|
|
165
|
+
const num = footnoteContext.registerFootnote({
|
|
166
|
+
footnoteId: footnoteFeature.footnoteId,
|
|
167
|
+
contentPlaintext: footnoteFeature.contentPlaintext,
|
|
168
|
+
contentFacets: footnoteFeature.contentFacets
|
|
169
|
+
});
|
|
170
|
+
foundFootnotes.set(footnoteFeature.footnoteId, num);
|
|
171
|
+
footnoteNumber = num;
|
|
172
|
+
} else {
|
|
173
|
+
footnoteNumber = foundFootnotes.get(footnoteFeature.footnoteId);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
134
177
|
// Split text by newlines and mark br elements - handle undefined segment.text
|
|
135
178
|
const segmentText = segment.text || '';
|
|
136
179
|
const textParts = segmentText.split('\n');
|
|
@@ -154,10 +197,19 @@
|
|
|
154
197
|
isMath: isMath || false,
|
|
155
198
|
isDidMention: !!isDidMention,
|
|
156
199
|
isAtMention: !!isAtMention,
|
|
200
|
+
isFootnote: !!footnoteFeature,
|
|
157
201
|
link: link?.uri,
|
|
158
202
|
id: id?.id,
|
|
159
203
|
did: isDidMention?.did,
|
|
160
|
-
atURI: isAtMention?.atURI
|
|
204
|
+
atURI: isAtMention?.atURI,
|
|
205
|
+
footnote: footnoteFeature
|
|
206
|
+
? {
|
|
207
|
+
footnoteId: footnoteFeature.footnoteId,
|
|
208
|
+
contentPlaintext: footnoteFeature.contentPlaintext,
|
|
209
|
+
contentFacets: footnoteFeature.contentFacets
|
|
210
|
+
}
|
|
211
|
+
: undefined,
|
|
212
|
+
footnoteNumber
|
|
161
213
|
});
|
|
162
214
|
}
|
|
163
215
|
|
|
@@ -165,12 +217,30 @@
|
|
|
165
217
|
}
|
|
166
218
|
|
|
167
219
|
const segments = $derived(processSegments());
|
|
220
|
+
|
|
221
|
+
// Footnote feature type
|
|
222
|
+
interface FootnoteFeature {
|
|
223
|
+
$type: 'pub.leaflet.richtext.facet#footnote';
|
|
224
|
+
footnoteId: string;
|
|
225
|
+
contentPlaintext: string;
|
|
226
|
+
contentFacets?: Facet[];
|
|
227
|
+
}
|
|
168
228
|
</script>
|
|
169
229
|
|
|
170
230
|
{#each segments as segment, i}
|
|
171
231
|
{#each segment.parts as part, j}
|
|
172
232
|
{#if part.isBr}
|
|
173
233
|
<br />
|
|
234
|
+
{:else if segment.isFootnote}
|
|
235
|
+
<!-- Footnote reference -->
|
|
236
|
+
<a
|
|
237
|
+
href="#{segment.footnote?.footnoteId}"
|
|
238
|
+
class="footnote-ref"
|
|
239
|
+
class:themed={hasTheme}
|
|
240
|
+
id="{segment.footnote?.footnoteId}-ref"
|
|
241
|
+
>
|
|
242
|
+
<sup>{segment.footnoteNumber ?? '*'}</sup>
|
|
243
|
+
</a>
|
|
174
244
|
{:else if segment.isMath}
|
|
175
245
|
<InlineMath tex={part.text} {hasTheme} />
|
|
176
246
|
{:else}
|
|
@@ -269,4 +339,19 @@
|
|
|
269
339
|
-webkit-box-decoration-break: clone;
|
|
270
340
|
background-color: rgb(255, 177, 177);
|
|
271
341
|
}
|
|
342
|
+
|
|
343
|
+
.footnote-ref {
|
|
344
|
+
text-decoration: none;
|
|
345
|
+
color: rgb(0 0 225);
|
|
346
|
+
font-size: 0.75em;
|
|
347
|
+
padding: 0 0.1em;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.footnote-ref.themed {
|
|
351
|
+
color: var(--theme-accent);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.footnote-ref:hover {
|
|
355
|
+
text-decoration: underline;
|
|
356
|
+
}
|
|
272
357
|
</style>
|
|
@@ -12,6 +12,8 @@ interface Props {
|
|
|
12
12
|
plaintext: string;
|
|
13
13
|
facets?: Facet[];
|
|
14
14
|
hasTheme?: boolean;
|
|
15
|
+
/** Footnote index in the document (for numbering) */
|
|
16
|
+
footnoteIndex?: number;
|
|
15
17
|
}
|
|
16
18
|
declare const RichText: import("svelte").Component<Props, {}, "">;
|
|
17
19
|
type RichText = ReturnType<typeof RichText>;
|