@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.
Files changed (69) hide show
  1. package/dist/components/ActionBar.svelte +85 -0
  2. package/dist/components/ActionBar.svelte.d.ts +13 -0
  3. package/dist/components/Avatar.svelte +104 -0
  4. package/dist/components/Avatar.svelte.d.ts +19 -0
  5. package/dist/components/Comment.svelte +172 -0
  6. package/dist/components/Comment.svelte.d.ts +22 -0
  7. package/dist/components/CommentsSection.svelte +89 -0
  8. package/dist/components/DocumentCard.svelte +126 -56
  9. package/dist/components/DocumentCard.svelte.d.ts +51 -0
  10. package/dist/components/Footnotes.svelte +72 -0
  11. package/dist/components/Footnotes.svelte.d.ts +13 -0
  12. package/dist/components/RecommendButton.svelte +153 -0
  13. package/dist/components/RecommendButton.svelte.d.ts +17 -0
  14. package/dist/components/ThemeProvider.svelte +92 -0
  15. package/dist/components/ThemeProvider.svelte.d.ts +13 -0
  16. package/dist/components/Toast.svelte +177 -0
  17. package/dist/components/Toast.svelte.d.ts +32 -0
  18. package/dist/components/Watermark.svelte +100 -0
  19. package/dist/components/Watermark.svelte.d.ts +17 -0
  20. package/dist/components/common/ThemedCard.svelte +15 -15
  21. package/dist/components/common/ThemedCard.svelte.d.ts +5 -0
  22. package/dist/components/document/BlockRenderer.svelte +3 -0
  23. package/dist/components/document/DocumentRenderer.svelte +41 -1
  24. package/dist/components/document/RichText.svelte +87 -2
  25. package/dist/components/document/RichText.svelte.d.ts +2 -0
  26. package/dist/components/document/blocks/OrderedListBlock.svelte +152 -0
  27. package/dist/components/document/blocks/UnorderedListBlock.svelte +1 -1
  28. package/dist/components/index.d.ts +28 -0
  29. package/dist/components/index.js +30 -0
  30. package/dist/index.d.ts +5 -4
  31. package/dist/index.js +6 -4
  32. package/dist/publisher.d.ts +73 -0
  33. package/dist/publisher.js +185 -0
  34. package/dist/schemas.d.ts +1162 -2
  35. package/dist/schemas.js +316 -0
  36. package/dist/types.d.ts +393 -2
  37. package/dist/types.js +1 -1
  38. package/dist/utils/native-comments.d.ts +68 -0
  39. package/dist/utils/native-comments.js +149 -0
  40. package/dist/utils/theme-helpers.d.ts +41 -1
  41. package/dist/utils/theme-helpers.js +98 -1
  42. package/dist/utils/theme.d.ts +48 -1
  43. package/dist/utils/theme.js +158 -0
  44. package/package.json +62 -65
  45. package/src/lib/components/ActionBar.svelte +85 -0
  46. package/src/lib/components/Avatar.svelte +104 -0
  47. package/src/lib/components/Comment.svelte +172 -0
  48. package/src/lib/components/CommentsSection.svelte +89 -0
  49. package/src/lib/components/DocumentCard.svelte +126 -56
  50. package/src/lib/components/Footnotes.svelte +72 -0
  51. package/src/lib/components/RecommendButton.svelte +153 -0
  52. package/src/lib/components/ThemeProvider.svelte +92 -0
  53. package/src/lib/components/Toast.svelte +177 -0
  54. package/src/lib/components/Watermark.svelte +100 -0
  55. package/src/lib/components/common/ThemedCard.svelte +15 -15
  56. package/src/lib/components/document/BlockRenderer.svelte +3 -0
  57. package/src/lib/components/document/DocumentRenderer.svelte +41 -1
  58. package/src/lib/components/document/RichText.svelte +87 -2
  59. package/src/lib/components/document/blocks/OrderedListBlock.svelte +152 -0
  60. package/src/lib/components/document/blocks/UnorderedListBlock.svelte +1 -1
  61. package/src/lib/components/index.ts +32 -0
  62. package/src/lib/index.ts +119 -5
  63. package/src/lib/publisher.ts +251 -0
  64. package/src/lib/schemas.ts +411 -0
  65. package/src/lib/types.ts +506 -2
  66. package/src/lib/utils/native-comments.ts +197 -0
  67. package/src/lib/utils/theme-helpers.ts +136 -3
  68. package/src/lib/utils/theme.ts +189 -1
  69. 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 showCover && value.coverImage}
36
- <img
37
- src={value.coverImage}
38
- alt="{value.title} cover"
39
- class="h-48 w-32 shrink-0 rounded-lg object-cover shadow-sm"
40
- />
41
- {/if}
42
-
43
- <div class="flex-1">
44
- <ThemedText
45
- {hasTheme}
46
- element="h3"
47
- class="group-hover:text-primary-600 dark:group-hover:text-primary-400 mb-2 text-2xl font-bold transition-colors"
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="mb-4 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm">
64
- <DateDisplay
65
- date={value.publishedAt}
66
- class="font-medium"
67
- style={hasTheme
68
- ? `color: ${hasTheme ? 'color-mix(in srgb, var(--theme-foreground) 80%, transparent)' : ''}`
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
- <span
79
- class="h-1 w-1 rounded-full"
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
- {#if value.tags && value.tags.length > 0}
92
- <TagList tags={value.tags} {hasTheme} />
93
- {/if}
94
- </div>
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;