sveltekit-embeds 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -0
  3. package/dist/components/apple-music.svelte +39 -0
  4. package/dist/components/apple-music.svelte.d.ts +11 -0
  5. package/dist/components/apple-podcasts.svelte +39 -0
  6. package/dist/components/apple-podcasts.svelte.d.ts +11 -0
  7. package/dist/components/bluesky.svelte +75 -0
  8. package/dist/components/bluesky.svelte.d.ts +10 -0
  9. package/dist/components/dailymotion.svelte +70 -0
  10. package/dist/components/dailymotion.svelte.d.ts +12 -0
  11. package/dist/components/deezer.svelte +40 -0
  12. package/dist/components/deezer.svelte.d.ts +11 -0
  13. package/dist/components/general-observer.svelte +46 -0
  14. package/dist/components/general-observer.svelte.d.ts +9 -0
  15. package/dist/components/instagram.svelte +37 -0
  16. package/dist/components/instagram.svelte.d.ts +10 -0
  17. package/dist/components/linked-in.svelte +37 -0
  18. package/dist/components/linked-in.svelte.d.ts +10 -0
  19. package/dist/components/mastodon.svelte +78 -0
  20. package/dist/components/mastodon.svelte.d.ts +10 -0
  21. package/dist/components/pinterest.svelte +86 -0
  22. package/dist/components/pinterest.svelte.d.ts +7 -0
  23. package/dist/components/reddit.svelte +38 -0
  24. package/dist/components/reddit.svelte.d.ts +11 -0
  25. package/dist/components/sound-cloud.svelte +48 -0
  26. package/dist/components/sound-cloud.svelte.d.ts +13 -0
  27. package/dist/components/spotify.svelte +40 -0
  28. package/dist/components/spotify.svelte.d.ts +11 -0
  29. package/dist/components/threads.svelte +88 -0
  30. package/dist/components/threads.svelte.d.ts +7 -0
  31. package/dist/components/tidal.svelte +37 -0
  32. package/dist/components/tidal.svelte.d.ts +10 -0
  33. package/dist/components/tik-tok.svelte +74 -0
  34. package/dist/components/tik-tok.svelte.d.ts +13 -0
  35. package/dist/components/twitch.svelte +71 -0
  36. package/dist/components/twitch.svelte.d.ts +12 -0
  37. package/dist/components/x-embed.svelte +94 -0
  38. package/dist/components/x-embed.svelte.d.ts +8 -0
  39. package/dist/components/you-tube.svelte +85 -0
  40. package/dist/components/you-tube.svelte.d.ts +14 -0
  41. package/dist/index.d.ts +20 -0
  42. package/dist/index.js +20 -0
  43. package/dist/utils/apple-music.d.ts +11 -0
  44. package/dist/utils/apple-music.js +59 -0
  45. package/dist/utils/apple-podcasts.d.ts +11 -0
  46. package/dist/utils/apple-podcasts.js +56 -0
  47. package/dist/utils/bluesky.d.ts +8 -0
  48. package/dist/utils/bluesky.js +35 -0
  49. package/dist/utils/dailymotion.d.ts +2 -0
  50. package/dist/utils/dailymotion.js +21 -0
  51. package/dist/utils/deezer.d.ts +10 -0
  52. package/dist/utils/deezer.js +56 -0
  53. package/dist/utils/index.d.ts +19 -0
  54. package/dist/utils/index.js +25 -0
  55. package/dist/utils/instagram.d.ts +2 -0
  56. package/dist/utils/instagram.js +21 -0
  57. package/dist/utils/linkedin.d.ts +7 -0
  58. package/dist/utils/linkedin.js +38 -0
  59. package/dist/utils/mastodon.d.ts +12 -0
  60. package/dist/utils/mastodon.js +36 -0
  61. package/dist/utils/pinterest.d.ts +2 -0
  62. package/dist/utils/pinterest.js +21 -0
  63. package/dist/utils/reddit.d.ts +2 -0
  64. package/dist/utils/reddit.js +45 -0
  65. package/dist/utils/soundcloud.d.ts +11 -0
  66. package/dist/utils/soundcloud.js +26 -0
  67. package/dist/utils/spotify.d.ts +10 -0
  68. package/dist/utils/spotify.js +71 -0
  69. package/dist/utils/threads.d.ts +9 -0
  70. package/dist/utils/threads.js +49 -0
  71. package/dist/utils/tidal.d.ts +10 -0
  72. package/dist/utils/tidal.js +53 -0
  73. package/dist/utils/tiktok.d.ts +2 -0
  74. package/dist/utils/tiktok.js +18 -0
  75. package/dist/utils/twitch.d.ts +9 -0
  76. package/dist/utils/twitch.js +37 -0
  77. package/dist/utils/twitter.d.ts +2 -0
  78. package/dist/utils/twitter.js +35 -0
  79. package/dist/utils/youtube.d.ts +3 -0
  80. package/dist/utils/youtube.js +40 -0
  81. package/package.json +79 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sander Ginn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # sveltekit-embeds
2
+
3
+ Svelte 5 embed component library for media and social platforms.
4
+
5
+ ## What you get
6
+
7
+ - Lazy-loaded embeds via `GeneralObserver` (IntersectionObserver-based)
8
+ - URL parsing and embed URL helpers in `src/lib/utils`
9
+ - Iframe embeds and script-injection embeds in `src/lib/components`
10
+ - TypeScript + Svelte package output (`dist/`) via `svelte-package`
11
+
12
+ ## Exported components
13
+
14
+ - `GeneralObserver`
15
+ - `YouTube`, `TikTok`, `Twitch`, `Dailymotion`
16
+ - `Spotify`, `AppleMusic`, `ApplePodcasts`, `SoundCloud`, `Deezer`, `Tidal`
17
+ - `XEmbed`, `Instagram`, `Threads`, `Bluesky`, `Mastodon`, `LinkedIn`, `Reddit`, `Pinterest`
18
+
19
+ Utilities are also re-exported from `src/lib/utils`.
20
+
21
+ ## Usage
22
+
23
+ ```svelte
24
+ <script lang="ts">
25
+ import { YouTube, Spotify, XEmbed } from 'sveltekit-embeds';
26
+ </script>
27
+
28
+ <YouTube url="https://www.youtube.com/watch?v=dQw4w9WgXcQ" />
29
+ <Spotify url="https://open.spotify.com/track/4uLU6hMCjMI75M1A2tKUQC" />
30
+ <XEmbed url="https://x.com/jack/status/20" />
31
+ ```
32
+
33
+ Most components support:
34
+
35
+ - `url` (required)
36
+ - `disable_observer` (optional, bypass lazy loading)
37
+ - sizing/style props (varies per component, see component files)
38
+
39
+ ## Local development
40
+
41
+ From the repository root:
42
+
43
+ ```bash
44
+ pnpm install
45
+ pnpm --filter sveltekit-embeds dev
46
+ ```
47
+
48
+ Run static checks:
49
+
50
+ ```bash
51
+ pnpm --filter sveltekit-embeds check
52
+ ```
53
+
54
+ Run tests (server utils + browser component tests):
55
+
56
+ ```bash
57
+ pnpm --filter sveltekit-embeds test:unit
58
+ ```
59
+
60
+ Build package output and run publint:
61
+
62
+ ```bash
63
+ pnpm --filter sveltekit-embeds prepack
64
+ ```
65
+
66
+ ## Publish to npm
67
+
68
+ The package is configured for public npm publish (`publishConfig.access=public`).
69
+
70
+ From repository root:
71
+
72
+ ```bash
73
+ npm login
74
+ pnpm --filter sveltekit-embeds test:unit
75
+ pnpm --filter sveltekit-embeds check
76
+ pnpm --filter sveltekit-embeds pack:dry-run
77
+ pnpm --filter sveltekit-embeds exec npm version patch
78
+ pnpm --filter sveltekit-embeds publish:npm
79
+ ```
80
+
81
+ Use `npm version minor` or `npm version major` when needed.
82
+
83
+ ## Testing notes
84
+
85
+ - Browser component tests use `vitest-browser-svelte` + Playwright Chromium.
86
+ - First run may download browser binaries:
87
+
88
+ ```bash
89
+ pnpm --filter sveltekit-embeds exec playwright install chromium
90
+ ```
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ import GeneralObserver from './general-observer.svelte';
3
+ import { getAppleMusicEmbedUrl, parseAppleMusicUrl } from '../utils/apple-music.js';
4
+
5
+ interface Props {
6
+ url: string;
7
+ theme?: 'light' | 'dark';
8
+ width?: string;
9
+ height?: string;
10
+ disable_observer?: boolean;
11
+ iframe_styles?: string;
12
+ }
13
+
14
+ let {
15
+ url,
16
+ theme = 'light',
17
+ width = '100%',
18
+ height = '450',
19
+ disable_observer = false,
20
+ iframe_styles
21
+ }: Props = $props();
22
+
23
+ const parsed = $derived(parseAppleMusicUrl(url));
24
+ const embedUrl = $derived(getAppleMusicEmbedUrl(url, theme));
25
+ </script>
26
+
27
+ <GeneralObserver {disable_observer}>
28
+ {#if embedUrl && parsed.isValid}
29
+ <iframe
30
+ src={embedUrl}
31
+ {width}
32
+ {height}
33
+ title={`apple-music-${parsed.type}-${parsed.id}`}
34
+ allow="autoplay *; encrypted-media *; fullscreen *; clipboard-write"
35
+ loading="lazy"
36
+ style={iframe_styles}
37
+ ></iframe>
38
+ {/if}
39
+ </GeneralObserver>
@@ -0,0 +1,11 @@
1
+ interface Props {
2
+ url: string;
3
+ theme?: 'light' | 'dark';
4
+ width?: string;
5
+ height?: string;
6
+ disable_observer?: boolean;
7
+ iframe_styles?: string;
8
+ }
9
+ declare const AppleMusic: import("svelte").Component<Props, {}, "">;
10
+ type AppleMusic = ReturnType<typeof AppleMusic>;
11
+ export default AppleMusic;
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ import GeneralObserver from './general-observer.svelte';
3
+ import { getApplePodcastsEmbedUrl, parseApplePodcastsUrl } from '../utils/apple-podcasts.js';
4
+
5
+ interface Props {
6
+ url: string;
7
+ theme?: 'light' | 'dark';
8
+ width?: string;
9
+ height?: string;
10
+ disable_observer?: boolean;
11
+ iframe_styles?: string;
12
+ }
13
+
14
+ let {
15
+ url,
16
+ theme = 'light',
17
+ width = '100%',
18
+ height = '175',
19
+ disable_observer = false,
20
+ iframe_styles
21
+ }: Props = $props();
22
+
23
+ const parsed = $derived(parseApplePodcastsUrl(url));
24
+ const embedUrl = $derived(getApplePodcastsEmbedUrl(url, theme));
25
+ </script>
26
+
27
+ <GeneralObserver {disable_observer}>
28
+ {#if embedUrl && parsed.isValid}
29
+ <iframe
30
+ src={embedUrl}
31
+ {width}
32
+ {height}
33
+ title={`apple-podcasts-${parsed.type}-${parsed.id}`}
34
+ allow="autoplay *; encrypted-media *; fullscreen *"
35
+ loading="lazy"
36
+ style={iframe_styles}
37
+ ></iframe>
38
+ {/if}
39
+ </GeneralObserver>
@@ -0,0 +1,11 @@
1
+ interface Props {
2
+ url: string;
3
+ theme?: 'light' | 'dark';
4
+ width?: string;
5
+ height?: string;
6
+ disable_observer?: boolean;
7
+ iframe_styles?: string;
8
+ }
9
+ declare const ApplePodcasts: import("svelte").Component<Props, {}, "">;
10
+ type ApplePodcasts = ReturnType<typeof ApplePodcasts>;
11
+ export default ApplePodcasts;
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ import GeneralObserver from './general-observer.svelte';
3
+ import { getBlueskyEmbedUrl, parseBlueskyUrl } from '../utils/bluesky.js';
4
+
5
+ interface Props {
6
+ url: string;
7
+ width?: string;
8
+ height?: string;
9
+ disable_observer?: boolean;
10
+ iframe_styles?: string;
11
+ }
12
+
13
+ let {
14
+ url,
15
+ width = '100%',
16
+ height = '500',
17
+ disable_observer = false,
18
+ iframe_styles
19
+ }: Props = $props();
20
+
21
+ const parsed = $derived(parseBlueskyUrl(url));
22
+ const embedUrl = $derived(getBlueskyEmbedUrl(url));
23
+ let iframeElement = $state<HTMLIFrameElement | null>(null);
24
+ let wrapperHeight = $state('500px');
25
+
26
+ $effect(() => {
27
+ wrapperHeight = height;
28
+ });
29
+
30
+ $effect(() => {
31
+ if (!embedUrl) {
32
+ return;
33
+ }
34
+
35
+ const onMessage = (event: MessageEvent) => {
36
+ if (event.origin !== 'https://embed.bsky.app') {
37
+ return;
38
+ }
39
+
40
+ if (event.source !== iframeElement?.contentWindow) {
41
+ return;
42
+ }
43
+
44
+ if (typeof event.data !== 'object' || event.data === null) {
45
+ return;
46
+ }
47
+
48
+ const payload = event.data as { height?: unknown };
49
+ if (typeof payload.height !== 'number' || Number.isNaN(payload.height)) {
50
+ return;
51
+ }
52
+
53
+ wrapperHeight = `${Math.max(payload.height, 100)}px`;
54
+ };
55
+
56
+ window.addEventListener('message', onMessage);
57
+ return () => window.removeEventListener('message', onMessage);
58
+ });
59
+ </script>
60
+
61
+ <GeneralObserver {disable_observer}>
62
+ {#if embedUrl && parsed.isValid}
63
+ <div style={`height: ${wrapperHeight}; width: ${width};`}>
64
+ <iframe
65
+ bind:this={iframeElement}
66
+ src={embedUrl}
67
+ width={width}
68
+ height={wrapperHeight}
69
+ title={`bluesky-${parsed.handle}-${parsed.postId}`}
70
+ loading="lazy"
71
+ style={iframe_styles}
72
+ ></iframe>
73
+ </div>
74
+ {/if}
75
+ </GeneralObserver>
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ url: string;
3
+ width?: string;
4
+ height?: string;
5
+ disable_observer?: boolean;
6
+ iframe_styles?: string;
7
+ }
8
+ declare const Bluesky: import("svelte").Component<Props, {}, "">;
9
+ type Bluesky = ReturnType<typeof Bluesky>;
10
+ export default Bluesky;
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ import GeneralObserver from './general-observer.svelte';
3
+ import { getPadding } from '../utils/index.js';
4
+ import { extractDailymotionId, getDailymotionEmbedUrl } from '../utils/dailymotion.js';
5
+
6
+ interface Props {
7
+ url: string;
8
+ aspectRatio?: string;
9
+ autoplay?: boolean;
10
+ mute?: boolean;
11
+ controls?: boolean;
12
+ disable_observer?: boolean;
13
+ iframe_styles?: string;
14
+ }
15
+
16
+ let {
17
+ url,
18
+ aspectRatio = '16:9',
19
+ autoplay = false,
20
+ mute = false,
21
+ controls = true,
22
+ disable_observer = false,
23
+ iframe_styles
24
+ }: Props = $props();
25
+
26
+ const videoId = $derived(extractDailymotionId(url));
27
+ const baseEmbedUrl = $derived(getDailymotionEmbedUrl(url));
28
+ const paddingStyle = $derived(getPadding(aspectRatio));
29
+ const embedUrl = $derived.by(() => {
30
+ if (!baseEmbedUrl) {
31
+ return null;
32
+ }
33
+
34
+ const query = new URL(baseEmbedUrl);
35
+ query.searchParams.set('autoplay', autoplay ? '1' : '0');
36
+ query.searchParams.set('mute', mute ? '1' : '0');
37
+ query.searchParams.set('controls', controls ? '1' : '0');
38
+ return query.toString();
39
+ });
40
+ </script>
41
+
42
+ <GeneralObserver {disable_observer}>
43
+ {#if embedUrl}
44
+ <div class="wrapper" style={paddingStyle}>
45
+ <iframe
46
+ src={embedUrl}
47
+ title={`dailymotion-${videoId}`}
48
+ allow="autoplay; fullscreen; picture-in-picture"
49
+ allowfullscreen
50
+ loading="lazy"
51
+ style={iframe_styles}
52
+ ></iframe>
53
+ </div>
54
+ {/if}
55
+ </GeneralObserver>
56
+
57
+ <style>
58
+ .wrapper {
59
+ position: relative;
60
+ width: 100%;
61
+ }
62
+
63
+ iframe {
64
+ position: absolute;
65
+ inset: 0;
66
+ width: 100%;
67
+ height: 100%;
68
+ border: 0;
69
+ }
70
+ </style>
@@ -0,0 +1,12 @@
1
+ interface Props {
2
+ url: string;
3
+ aspectRatio?: string;
4
+ autoplay?: boolean;
5
+ mute?: boolean;
6
+ controls?: boolean;
7
+ disable_observer?: boolean;
8
+ iframe_styles?: string;
9
+ }
10
+ declare const Dailymotion: import("svelte").Component<Props, {}, "">;
11
+ type Dailymotion = ReturnType<typeof Dailymotion>;
12
+ export default Dailymotion;
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ import GeneralObserver from './general-observer.svelte';
3
+ import { getDeezerEmbedUrl, parseDeezerUrl } from '../utils/deezer.js';
4
+
5
+ interface Props {
6
+ url: string;
7
+ theme?: 'light' | 'dark';
8
+ width?: string;
9
+ height?: string;
10
+ disable_observer?: boolean;
11
+ iframe_styles?: string;
12
+ }
13
+
14
+ let {
15
+ url,
16
+ theme = 'light',
17
+ width = '100%',
18
+ height,
19
+ disable_observer = false,
20
+ iframe_styles
21
+ }: Props = $props();
22
+
23
+ const parsed = $derived(parseDeezerUrl(url));
24
+ const embedUrl = $derived(getDeezerEmbedUrl(url, theme));
25
+ const resolvedHeight = $derived(height ?? (parsed.type === 'track' ? '150' : '380'));
26
+ </script>
27
+
28
+ <GeneralObserver {disable_observer}>
29
+ {#if embedUrl}
30
+ <iframe
31
+ src={embedUrl}
32
+ {width}
33
+ height={resolvedHeight}
34
+ title={`deezer-${parsed.type}-${parsed.id}`}
35
+ allow="encrypted-media; clipboard-write"
36
+ loading="lazy"
37
+ style={iframe_styles}
38
+ ></iframe>
39
+ {/if}
40
+ </GeneralObserver>
@@ -0,0 +1,11 @@
1
+ interface Props {
2
+ url: string;
3
+ theme?: 'light' | 'dark';
4
+ width?: string;
5
+ height?: string;
6
+ disable_observer?: boolean;
7
+ iframe_styles?: string;
8
+ }
9
+ declare const Deezer: import("svelte").Component<Props, {}, "">;
10
+ type Deezer = ReturnType<typeof Deezer>;
11
+ export default Deezer;
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ interface Props {
5
+ threshold?: number;
6
+ disable_observer?: boolean;
7
+ children: Snippet;
8
+ }
9
+
10
+ let { threshold = 0.5, disable_observer = false, children }: Props = $props();
11
+ let loaded = $state(false);
12
+ let root = $state<HTMLElement | null>(null);
13
+
14
+ $effect(() => {
15
+ if (disable_observer || !root || loaded) {
16
+ return;
17
+ }
18
+
19
+ if (typeof IntersectionObserver === 'undefined') {
20
+ loaded = true;
21
+ return;
22
+ }
23
+
24
+ const observer = new IntersectionObserver(
25
+ ([entry]) => {
26
+ if (!entry?.isIntersecting) {
27
+ return;
28
+ }
29
+
30
+ loaded = true;
31
+ observer.disconnect();
32
+ },
33
+ { threshold }
34
+ );
35
+
36
+ observer.observe(root);
37
+
38
+ return () => observer.disconnect();
39
+ });
40
+ </script>
41
+
42
+ <div bind:this={root} data-testid="general-observer">
43
+ {#if disable_observer || loaded}
44
+ {@render children()}
45
+ {/if}
46
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ threshold?: number;
4
+ disable_observer?: boolean;
5
+ children: Snippet;
6
+ }
7
+ declare const GeneralObserver: import("svelte").Component<Props, {}, "">;
8
+ type GeneralObserver = ReturnType<typeof GeneralObserver>;
9
+ export default GeneralObserver;
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ import GeneralObserver from './general-observer.svelte';
3
+ import { extractInstagramId, getInstagramEmbedUrl } from '../utils/instagram.js';
4
+
5
+ interface Props {
6
+ url: string;
7
+ width?: string;
8
+ height?: string;
9
+ disable_observer?: boolean;
10
+ iframe_styles?: string;
11
+ }
12
+
13
+ let {
14
+ url,
15
+ width = '100%',
16
+ height = '560',
17
+ disable_observer = false,
18
+ iframe_styles
19
+ }: Props = $props();
20
+
21
+ const postId = $derived(extractInstagramId(url));
22
+ const embedUrl = $derived(getInstagramEmbedUrl(url));
23
+ </script>
24
+
25
+ <GeneralObserver {disable_observer}>
26
+ {#if embedUrl}
27
+ <iframe
28
+ src={embedUrl}
29
+ {width}
30
+ {height}
31
+ title={`instagram-${postId}`}
32
+ allow="autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"
33
+ loading="lazy"
34
+ style={iframe_styles}
35
+ ></iframe>
36
+ {/if}
37
+ </GeneralObserver>
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ url: string;
3
+ width?: string;
4
+ height?: string;
5
+ disable_observer?: boolean;
6
+ iframe_styles?: string;
7
+ }
8
+ declare const Instagram: import("svelte").Component<Props, {}, "">;
9
+ type Instagram = ReturnType<typeof Instagram>;
10
+ export default Instagram;
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ import GeneralObserver from './general-observer.svelte';
3
+ import { extractLinkedInPostId, getLinkedInEmbedUrl } from '../utils/linkedin.js';
4
+
5
+ interface Props {
6
+ url: string;
7
+ width?: string;
8
+ height?: string;
9
+ disable_observer?: boolean;
10
+ iframe_styles?: string;
11
+ }
12
+
13
+ let {
14
+ url,
15
+ width = '100%',
16
+ height = '399',
17
+ disable_observer = false,
18
+ iframe_styles
19
+ }: Props = $props();
20
+
21
+ const postId = $derived(extractLinkedInPostId(url));
22
+ const embedUrl = $derived(getLinkedInEmbedUrl(url));
23
+ </script>
24
+
25
+ <GeneralObserver {disable_observer}>
26
+ {#if embedUrl}
27
+ <iframe
28
+ src={embedUrl}
29
+ {width}
30
+ {height}
31
+ title={`linkedin-${postId}`}
32
+ allowfullscreen
33
+ loading="lazy"
34
+ style={iframe_styles}
35
+ ></iframe>
36
+ {/if}
37
+ </GeneralObserver>
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ url: string;
3
+ width?: string;
4
+ height?: string;
5
+ disable_observer?: boolean;
6
+ iframe_styles?: string;
7
+ }
8
+ declare const LinkedIn: import("svelte").Component<Props, {}, "">;
9
+ type LinkedIn = ReturnType<typeof LinkedIn>;
10
+ export default LinkedIn;
@@ -0,0 +1,78 @@
1
+ <script lang="ts">
2
+ import GeneralObserver from './general-observer.svelte';
3
+ import { getMastodonEmbedInfo } from '../utils/mastodon.js';
4
+
5
+ interface Props {
6
+ url: string;
7
+ width?: string;
8
+ height?: string;
9
+ disable_observer?: boolean;
10
+ iframe_styles?: string;
11
+ }
12
+
13
+ let {
14
+ url,
15
+ width = '100%',
16
+ height = '400',
17
+ disable_observer = false,
18
+ iframe_styles
19
+ }: Props = $props();
20
+
21
+ const embedInfo = $derived(getMastodonEmbedInfo(url));
22
+
23
+ const ensureMastodonScript = async (scriptUrl: string) => {
24
+ if (typeof window === 'undefined') {
25
+ return;
26
+ }
27
+
28
+ const existingScript = document.querySelector<HTMLScriptElement>(`script[src="${scriptUrl}"]`);
29
+ if (existingScript) {
30
+ if (existingScript.dataset.loaded === 'true') {
31
+ return;
32
+ }
33
+
34
+ await new Promise<void>((resolve, reject) => {
35
+ existingScript.addEventListener('load', () => resolve(), { once: true });
36
+ existingScript.addEventListener('error', () => reject(new Error('Failed to load Mastodon script')), {
37
+ once: true
38
+ });
39
+ });
40
+ return;
41
+ }
42
+
43
+ await new Promise<void>((resolve, reject) => {
44
+ const script = document.createElement('script');
45
+ script.src = scriptUrl;
46
+ script.async = true;
47
+ script.dataset.sveltekitEmbedsScript = 'mastodon';
48
+ script.onload = () => {
49
+ script.dataset.loaded = 'true';
50
+ resolve();
51
+ };
52
+ script.onerror = () => reject(new Error('Failed to load Mastodon script'));
53
+ document.head.appendChild(script);
54
+ });
55
+ };
56
+
57
+ $effect(() => {
58
+ if (!embedInfo) {
59
+ return;
60
+ }
61
+
62
+ void ensureMastodonScript(embedInfo.scriptUrl);
63
+ });
64
+ </script>
65
+
66
+ <GeneralObserver {disable_observer}>
67
+ {#if embedInfo}
68
+ <iframe
69
+ src={embedInfo.embedUrl}
70
+ {width}
71
+ {height}
72
+ title="mastodon-embed"
73
+ allow="autoplay; encrypted-media; picture-in-picture"
74
+ loading="lazy"
75
+ style={iframe_styles}
76
+ ></iframe>
77
+ {/if}
78
+ </GeneralObserver>
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ url: string;
3
+ width?: string;
4
+ height?: string;
5
+ disable_observer?: boolean;
6
+ iframe_styles?: string;
7
+ }
8
+ declare const Mastodon: import("svelte").Component<Props, {}, "">;
9
+ type Mastodon = ReturnType<typeof Mastodon>;
10
+ export default Mastodon;