@ewanc26/ui 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.
- package/README.md +65 -0
- package/dist/components/layout/ThemeToggle.svelte +50 -0
- package/dist/components/layout/ThemeToggle.svelte.d.ts +4 -0
- package/dist/components/layout/ThemeToggle.svelte.d.ts.map +1 -0
- package/dist/components/layout/WolfToggle.svelte +19 -0
- package/dist/components/layout/WolfToggle.svelte.d.ts +4 -0
- package/dist/components/layout/WolfToggle.svelte.d.ts.map +1 -0
- package/dist/components/layout/index.d.ts +5 -0
- package/dist/components/layout/index.d.ts.map +1 -0
- package/dist/components/layout/index.js +4 -0
- package/dist/components/layout/main/DynamicLinks.svelte +63 -0
- package/dist/components/layout/main/DynamicLinks.svelte.d.ts +7 -0
- package/dist/components/layout/main/DynamicLinks.svelte.d.ts.map +1 -0
- package/dist/components/layout/main/ScrollToTop.svelte +36 -0
- package/dist/components/layout/main/ScrollToTop.svelte.d.ts +4 -0
- package/dist/components/layout/main/ScrollToTop.svelte.d.ts.map +1 -0
- package/dist/components/layout/main/card/BlueskyPostCard.svelte +261 -0
- package/dist/components/layout/main/card/BlueskyPostCard.svelte.d.ts +9 -0
- package/dist/components/layout/main/card/BlueskyPostCard.svelte.d.ts.map +1 -0
- package/dist/components/layout/main/card/KibunStatusCard.svelte +48 -0
- package/dist/components/layout/main/card/KibunStatusCard.svelte.d.ts +8 -0
- package/dist/components/layout/main/card/KibunStatusCard.svelte.d.ts.map +1 -0
- package/dist/components/layout/main/card/LinkCard.svelte +63 -0
- package/dist/components/layout/main/card/LinkCard.svelte.d.ts +17 -0
- package/dist/components/layout/main/card/LinkCard.svelte.d.ts.map +1 -0
- package/dist/components/layout/main/card/MusicStatusCard.svelte +101 -0
- package/dist/components/layout/main/card/MusicStatusCard.svelte.d.ts +8 -0
- package/dist/components/layout/main/card/MusicStatusCard.svelte.d.ts.map +1 -0
- package/dist/components/layout/main/card/PostCard.svelte +46 -0
- package/dist/components/layout/main/card/PostCard.svelte.d.ts +8 -0
- package/dist/components/layout/main/card/PostCard.svelte.d.ts.map +1 -0
- package/dist/components/layout/main/card/ProfileCard.svelte +70 -0
- package/dist/components/layout/main/card/ProfileCard.svelte.d.ts +8 -0
- package/dist/components/layout/main/card/ProfileCard.svelte.d.ts.map +1 -0
- package/dist/components/layout/main/card/TangledRepoCard.svelte +80 -0
- package/dist/components/layout/main/card/TangledRepoCard.svelte.d.ts +11 -0
- package/dist/components/layout/main/card/TangledRepoCard.svelte.d.ts.map +1 -0
- package/dist/components/layout/main/card/index.d.ts +8 -0
- package/dist/components/layout/main/card/index.d.ts.map +1 -0
- package/dist/components/layout/main/card/index.js +7 -0
- package/dist/components/layout/main/index.d.ts +4 -0
- package/dist/components/layout/main/index.d.ts.map +1 -0
- package/dist/components/layout/main/index.js +3 -0
- package/dist/components/seo/MetaTags.svelte +39 -0
- package/dist/components/seo/MetaTags.svelte.d.ts +9 -0
- package/dist/components/seo/MetaTags.svelte.d.ts.map +1 -0
- package/dist/components/seo/index.d.ts +2 -0
- package/dist/components/seo/index.d.ts.map +1 -0
- package/dist/components/seo/index.js +1 -0
- package/dist/components/ui/BlogPostCard.svelte +44 -0
- package/dist/components/ui/BlogPostCard.svelte.d.ts +9 -0
- package/dist/components/ui/BlogPostCard.svelte.d.ts.map +1 -0
- package/dist/components/ui/Card.svelte +144 -0
- package/dist/components/ui/Card.svelte.d.ts +27 -0
- package/dist/components/ui/Card.svelte.d.ts.map +1 -0
- package/dist/components/ui/DocumentCard.svelte +42 -0
- package/dist/components/ui/DocumentCard.svelte.d.ts +9 -0
- package/dist/components/ui/DocumentCard.svelte.d.ts.map +1 -0
- package/dist/components/ui/Dropdown.svelte +36 -0
- package/dist/components/ui/Dropdown.svelte.d.ts +15 -0
- package/dist/components/ui/Dropdown.svelte.d.ts.map +1 -0
- package/dist/components/ui/InternalCard.svelte +41 -0
- package/dist/components/ui/InternalCard.svelte.d.ts +14 -0
- package/dist/components/ui/InternalCard.svelte.d.ts.map +1 -0
- package/dist/components/ui/Pagination.svelte +74 -0
- package/dist/components/ui/Pagination.svelte.d.ts +11 -0
- package/dist/components/ui/Pagination.svelte.d.ts.map +1 -0
- package/dist/components/ui/PostsGroupedView.svelte +40 -0
- package/dist/components/ui/PostsGroupedView.svelte.d.ts +10 -0
- package/dist/components/ui/PostsGroupedView.svelte.d.ts.map +1 -0
- package/dist/components/ui/SearchBar.svelte +26 -0
- package/dist/components/ui/SearchBar.svelte.d.ts +9 -0
- package/dist/components/ui/SearchBar.svelte.d.ts.map +1 -0
- package/dist/components/ui/Tabs.svelte +25 -0
- package/dist/components/ui/Tabs.svelte.d.ts +13 -0
- package/dist/components/ui/Tabs.svelte.d.ts.map +1 -0
- package/dist/components/ui/index.d.ts +11 -0
- package/dist/components/ui/index.d.ts.map +1 -0
- package/dist/components/ui/index.js +10 -0
- package/dist/config/themes.config.d.ts +23 -0
- package/dist/config/themes.config.d.ts.map +1 -0
- package/dist/config/themes.config.js +116 -0
- package/dist/helper/badges.d.ts +9 -0
- package/dist/helper/badges.d.ts.map +1 -0
- package/dist/helper/badges.js +28 -0
- package/dist/helper/posts.d.ts +14 -0
- package/dist/helper/posts.d.ts.map +1 -0
- package/dist/helper/posts.js +47 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/stores/colorTheme.d.ts +12 -0
- package/dist/stores/colorTheme.d.ts.map +1 -0
- package/dist/stores/colorTheme.js +36 -0
- package/dist/stores/dropdownState.d.ts +2 -0
- package/dist/stores/dropdownState.d.ts.map +1 -0
- package/dist/stores/dropdownState.js +2 -0
- package/dist/stores/happyMac.d.ts +11 -0
- package/dist/stores/happyMac.d.ts.map +1 -0
- package/dist/stores/happyMac.js +19 -0
- package/dist/stores/index.d.ts +6 -0
- package/dist/stores/index.d.ts.map +1 -0
- package/dist/stores/index.js +4 -0
- package/dist/stores/wolfMode.d.ts +7 -0
- package/dist/stores/wolfMode.d.ts.map +1 -0
- package/dist/stores/wolfMode.js +130 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/formatNumber.d.ts +3 -0
- package/dist/utils/formatNumber.d.ts.map +1 -0
- package/dist/utils/formatNumber.js +26 -0
- package/dist/utils/locale.d.ts +4 -0
- package/dist/utils/locale.d.ts.map +1 -0
- package/dist/utils/locale.js +32 -0
- package/package.json +45 -0
- package/src/lib/components/layout/ThemeToggle.svelte +50 -0
- package/src/lib/components/layout/WolfToggle.svelte +19 -0
- package/src/lib/components/layout/index.ts +4 -0
- package/src/lib/components/layout/main/DynamicLinks.svelte +63 -0
- package/src/lib/components/layout/main/ScrollToTop.svelte +36 -0
- package/src/lib/components/layout/main/card/BlueskyPostCard.svelte +261 -0
- package/src/lib/components/layout/main/card/KibunStatusCard.svelte +48 -0
- package/src/lib/components/layout/main/card/LinkCard.svelte +63 -0
- package/src/lib/components/layout/main/card/MusicStatusCard.svelte +101 -0
- package/src/lib/components/layout/main/card/PostCard.svelte +46 -0
- package/src/lib/components/layout/main/card/ProfileCard.svelte +70 -0
- package/src/lib/components/layout/main/card/TangledRepoCard.svelte +80 -0
- package/src/lib/components/layout/main/card/index.ts +7 -0
- package/src/lib/components/layout/main/index.ts +3 -0
- package/src/lib/components/seo/MetaTags.svelte +39 -0
- package/src/lib/components/seo/index.ts +1 -0
- package/src/lib/components/ui/BlogPostCard.svelte +44 -0
- package/src/lib/components/ui/Card.svelte +144 -0
- package/src/lib/components/ui/DocumentCard.svelte +42 -0
- package/src/lib/components/ui/Dropdown.svelte +36 -0
- package/src/lib/components/ui/InternalCard.svelte +41 -0
- package/src/lib/components/ui/Pagination.svelte +74 -0
- package/src/lib/components/ui/PostsGroupedView.svelte +40 -0
- package/src/lib/components/ui/SearchBar.svelte +26 -0
- package/src/lib/components/ui/Tabs.svelte +25 -0
- package/src/lib/components/ui/index.ts +10 -0
- package/src/lib/config/themes.config.ts +130 -0
- package/src/lib/helper/badges.ts +44 -0
- package/src/lib/helper/posts.ts +63 -0
- package/src/lib/index.ts +32 -0
- package/src/lib/stores/colorTheme.ts +44 -0
- package/src/lib/stores/dropdownState.ts +3 -0
- package/src/lib/stores/happyMac.ts +28 -0
- package/src/lib/stores/index.ts +5 -0
- package/src/lib/stores/wolfMode.ts +127 -0
- package/src/lib/types/index.ts +19 -0
- package/src/lib/utils/formatNumber.ts +27 -0
- package/src/lib/utils/locale.ts +29 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Card from '../../../ui/Card.svelte';
|
|
3
|
+
import type { MusicStatusData } from '@ewanc26/atproto';
|
|
4
|
+
import { formatRelativeTime } from '../../../../utils/locale.js';
|
|
5
|
+
import { Music, Disc3, Users, Album, Clock, Radio } from '@lucide/svelte';
|
|
6
|
+
|
|
7
|
+
interface Props { musicStatus?: MusicStatusData | null; }
|
|
8
|
+
let { musicStatus = null }: Props = $props();
|
|
9
|
+
|
|
10
|
+
let artworkError = $state(false);
|
|
11
|
+
|
|
12
|
+
function formatArtists(artists: { artistName: string }[]): string {
|
|
13
|
+
if (!artists?.length) return 'Unknown Artist';
|
|
14
|
+
return artists.map((a) => a.artistName).join(', ');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatDuration(seconds?: number): string {
|
|
18
|
+
if (!seconds) return '';
|
|
19
|
+
const minutes = Math.floor(seconds / 60);
|
|
20
|
+
const remainingSeconds = seconds % 60;
|
|
21
|
+
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatServiceName(domain?: string): string {
|
|
25
|
+
if (!domain) return '';
|
|
26
|
+
return domain.replace('lastfm', 'Last.fm').replace('last.fm', 'Last.fm');
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<div class="mx-auto w-full max-w-2xl">
|
|
31
|
+
{#if !musicStatus}
|
|
32
|
+
<Card loading={true} variant="elevated" padding="md">
|
|
33
|
+
{#snippet skeleton()}
|
|
34
|
+
<div class="mb-3 flex items-start gap-4">
|
|
35
|
+
<div class="h-20 w-20 shrink-0 rounded-lg bg-canvas-300 dark:bg-canvas-700"></div>
|
|
36
|
+
<div class="flex-1">
|
|
37
|
+
<div class="mb-2 flex items-center gap-2">
|
|
38
|
+
<div class="h-4 w-4 rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
39
|
+
<div class="h-3 w-32 rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="mb-1 h-5 w-3/4 rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
42
|
+
<div class="mb-2 h-4 w-1/2 rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
43
|
+
<div class="h-3 w-40 rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
{/snippet}
|
|
47
|
+
</Card>
|
|
48
|
+
{:else}
|
|
49
|
+
{@const s = musicStatus}
|
|
50
|
+
<Card variant="elevated" padding="md">
|
|
51
|
+
{#snippet children()}
|
|
52
|
+
<div>
|
|
53
|
+
<div class="mb-4 flex items-center gap-2">
|
|
54
|
+
<Music class="h-4 w-4 text-primary-600 dark:text-primary-400" aria-hidden="true" />
|
|
55
|
+
<span class="text-xs font-semibold tracking-wide text-ink-800 uppercase dark:text-ink-100">
|
|
56
|
+
{s.$type === 'fm.teal.alpha.actor.status' ? 'Now Listening' : 'Last Played'}
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="flex items-start gap-3">
|
|
60
|
+
<div class="shrink-0">
|
|
61
|
+
{#if s.artworkUrl && !artworkError}
|
|
62
|
+
<img src={s.artworkUrl} alt="Album artwork for {s.releaseName || s.trackName}" class="h-20 w-20 rounded-lg object-cover shadow-md" loading="lazy" onerror={() => (artworkError = true)} />
|
|
63
|
+
{:else}
|
|
64
|
+
<div class="flex h-20 w-20 items-center justify-center rounded-lg bg-canvas-200 shadow-md dark:bg-canvas-700">
|
|
65
|
+
<Disc3 class="h-10 w-10 text-ink-500 dark:text-ink-400" aria-hidden="true" />
|
|
66
|
+
</div>
|
|
67
|
+
{/if}
|
|
68
|
+
</div>
|
|
69
|
+
<div class="min-w-0 flex-1">
|
|
70
|
+
<div class="mb-4">
|
|
71
|
+
<a href={s.originUrl || '#'} target="_blank" rel="noopener noreferrer" class="block max-w-full text-lg font-semibold wrap-break-word whitespace-normal text-primary-600 transition-colors hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300" class:pointer-events-none={!s.originUrl} class:cursor-default={!s.originUrl}>
|
|
72
|
+
{s.trackName}
|
|
73
|
+
</a>
|
|
74
|
+
<p class="mt-1 flex max-w-full items-start gap-1.5 text-base wrap-break-word whitespace-normal text-ink-800 dark:text-ink-100">
|
|
75
|
+
<Users class="mt-0.5 h-4 w-4 shrink-0 text-ink-600 dark:text-ink-300" />
|
|
76
|
+
{formatArtists(s.artists)}
|
|
77
|
+
</p>
|
|
78
|
+
{#if s.releaseName}
|
|
79
|
+
<p class="mt-1 flex max-w-full items-start gap-1.5 text-sm wrap-break-word whitespace-normal text-ink-700 dark:text-ink-200">
|
|
80
|
+
<Album class="mt-0.5 h-4 w-4 shrink-0 text-ink-500 dark:text-ink-400" />
|
|
81
|
+
<span>{s.releaseName}{#if s.duration}<span class="ml-1 inline-flex items-center gap-1 text-ink-600 dark:text-ink-300">· <Clock class="h-3 w-3" />{formatDuration(s.duration)}</span>{/if}</span>
|
|
82
|
+
</p>
|
|
83
|
+
{/if}
|
|
84
|
+
</div>
|
|
85
|
+
<div class="flex items-center gap-2 text-xs text-ink-700 dark:text-ink-200">
|
|
86
|
+
<time datetime={s.playedTime}>{formatRelativeTime(s.playedTime)}</time>
|
|
87
|
+
{#if s.musicServiceBaseDomain}
|
|
88
|
+
<span class="text-ink-600 dark:text-ink-300">·</span>
|
|
89
|
+
<a href="https://teal.fm" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-1 transition-colors hover:text-primary-600 dark:hover:text-primary-400" title="Powered by teal.fm">
|
|
90
|
+
<Radio class="h-3 w-3" />
|
|
91
|
+
{formatServiceName(s.musicServiceBaseDomain)} via {s.submissionClientAgent}
|
|
92
|
+
</a>
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
{/snippet}
|
|
99
|
+
</Card>
|
|
100
|
+
{/if}
|
|
101
|
+
</div>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Card from '../../../ui/Card.svelte';
|
|
3
|
+
import DocumentCard from '../../../ui/DocumentCard.svelte';
|
|
4
|
+
import type { StandardSiteDocument } from '@ewanc26/atproto';
|
|
5
|
+
|
|
6
|
+
interface Props { documents?: StandardSiteDocument[] | null; }
|
|
7
|
+
let { documents = null }: Props = $props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<div class="mx-auto w-full max-w-2xl">
|
|
11
|
+
{#if !documents}
|
|
12
|
+
<Card loading={true} variant="elevated" padding="md">
|
|
13
|
+
{#snippet skeleton()}
|
|
14
|
+
<div class="mb-4 h-6 w-32 rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
15
|
+
<div class="space-y-3">
|
|
16
|
+
{#each Array(3) as _}
|
|
17
|
+
<div class="rounded-lg bg-canvas-200 p-4 dark:bg-canvas-800">
|
|
18
|
+
<div class="mb-2 h-5 w-3/4 rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
19
|
+
<div class="mb-2 h-4 w-full rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
20
|
+
<div class="h-3 w-24 rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
21
|
+
</div>
|
|
22
|
+
{/each}
|
|
23
|
+
</div>
|
|
24
|
+
{/snippet}
|
|
25
|
+
</Card>
|
|
26
|
+
{:else if documents.length > 0}
|
|
27
|
+
<Card variant="elevated" padding="md">
|
|
28
|
+
{#snippet children()}
|
|
29
|
+
<h2 class="mb-4 text-2xl font-bold text-ink-900 dark:text-ink-50">Recent Posts</h2>
|
|
30
|
+
<div class="space-y-3">
|
|
31
|
+
{#each documents as document}
|
|
32
|
+
<DocumentCard {document} />
|
|
33
|
+
{/each}
|
|
34
|
+
</div>
|
|
35
|
+
{/snippet}
|
|
36
|
+
</Card>
|
|
37
|
+
{:else}
|
|
38
|
+
<Card variant="flat" padding="lg">
|
|
39
|
+
{#snippet children()}
|
|
40
|
+
<div class="text-center">
|
|
41
|
+
<p class="text-ink-700 dark:text-ink-300">No documents available. Start writing on <a href="https://standard.site/" class="text-primary-600 hover:underline dark:text-primary-400" target="_blank" rel="noopener noreferrer">Standard.site</a> to get started!</p>
|
|
42
|
+
</div>
|
|
43
|
+
{/snippet}
|
|
44
|
+
</Card>
|
|
45
|
+
{/if}
|
|
46
|
+
</div>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Card from '../../../ui/Card.svelte';
|
|
3
|
+
import type { ProfileData } from '@ewanc26/atproto';
|
|
4
|
+
import LinkCard from './LinkCard.svelte';
|
|
5
|
+
import { formatCompactNumber } from '../../../../utils/formatNumber.js';
|
|
6
|
+
|
|
7
|
+
interface Props { profile?: ProfileData | null; }
|
|
8
|
+
let { profile = null }: Props = $props();
|
|
9
|
+
|
|
10
|
+
let imageLoaded = $state(false);
|
|
11
|
+
let bannerLoaded = $state(false);
|
|
12
|
+
const locale = typeof navigator !== 'undefined' ? navigator.language || 'en-GB' : 'en-GB';
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div class="mx-auto w-full max-w-2xl">
|
|
16
|
+
{#if !profile}
|
|
17
|
+
<Card loading={true} variant="elevated" padding="none" class="overflow-hidden">
|
|
18
|
+
{#snippet skeleton()}
|
|
19
|
+
<div class="h-32 w-full rounded-t-xl bg-canvas-300 dark:bg-canvas-700"></div>
|
|
20
|
+
<div class="relative -mt-16 flex justify-center sm:ml-6 sm:justify-start">
|
|
21
|
+
<div class="h-32 w-32 rounded-full border-4 border-white bg-canvas-300 dark:border-canvas-900 dark:bg-canvas-700"></div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="space-y-2 p-6 pt-2 sm:pt-4">
|
|
24
|
+
<div class="h-6 w-1/2 rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
25
|
+
<div class="h-4 w-1/3 rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
26
|
+
<div class="h-4 rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
27
|
+
<div class="h-4 w-5/6 rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
28
|
+
</div>
|
|
29
|
+
{/snippet}
|
|
30
|
+
</Card>
|
|
31
|
+
{:else}
|
|
32
|
+
{@const p = profile}
|
|
33
|
+
<Card variant="elevated" padding="none" ariaLabel="Profile information">
|
|
34
|
+
{#snippet children()}
|
|
35
|
+
<div class="relative h-32 w-full overflow-hidden rounded-t-xl">
|
|
36
|
+
{#if p.banner}
|
|
37
|
+
<img src={p.banner} alt="" class="h-full w-full object-cover opacity-0 transition-opacity duration-300" class:opacity-100={bannerLoaded} onload={() => (bannerLoaded = true)} loading="lazy" role="presentation" />
|
|
38
|
+
{:else}
|
|
39
|
+
<div class="h-full w-full bg-linear-to-r from-primary-400 to-secondary-400" role="presentation"></div>
|
|
40
|
+
{/if}
|
|
41
|
+
</div>
|
|
42
|
+
<div class="relative -mt-16 flex justify-center sm:ml-6 sm:justify-start">
|
|
43
|
+
<div class="h-32 w-32 overflow-hidden rounded-full border-4 border-white bg-canvas-200 dark:border-canvas-900">
|
|
44
|
+
{#if p.avatar}
|
|
45
|
+
<img src={p.avatar} alt="{p.displayName || p.handle}'s profile picture" class="h-full w-full object-cover opacity-0 transition-opacity duration-300" class:opacity-100={imageLoaded} onload={() => (imageLoaded = true)} loading="lazy" />
|
|
46
|
+
{:else}
|
|
47
|
+
<div class="flex h-full w-full items-center justify-center bg-primary-200 text-3xl font-bold text-primary-800 dark:bg-primary-800 dark:text-primary-200" role="img" aria-label="{p.displayName || p.handle}'s avatar initials">
|
|
48
|
+
{(p.displayName || p.handle).charAt(0).toUpperCase()}
|
|
49
|
+
</div>
|
|
50
|
+
{/if}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="p-6">
|
|
54
|
+
<h2 class="text-2xl font-bold text-ink-900 dark:text-ink-50">{p.displayName || p.handle}</h2>
|
|
55
|
+
<p class="font-medium text-ink-700 dark:text-ink-200">@{p.handle}</p>
|
|
56
|
+
{#if p.pronouns}<p class="text-sm text-ink-600 italic dark:text-ink-300">{p.pronouns}</p>{/if}
|
|
57
|
+
{#if p.description}<p class="wrap-break-words mb-4 break-all whitespace-pre-wrap text-ink-700 dark:text-ink-200">{p.description}</p>{/if}
|
|
58
|
+
<div class="flex gap-6 text-sm font-medium" role="list" aria-label="Profile statistics">
|
|
59
|
+
<div class="flex items-center gap-1" role="listitem"><span class="font-bold text-ink-900 dark:text-ink-50">{formatCompactNumber(p.postsCount, locale)}</span><span class="text-ink-700 dark:text-ink-200">Posts</span></div>
|
|
60
|
+
<div class="flex items-center gap-1" role="listitem"><span class="font-bold text-ink-900 dark:text-ink-50">{formatCompactNumber(p.followersCount, locale)}</span><span class="text-ink-700 dark:text-ink-200">Followers</span></div>
|
|
61
|
+
<div class="flex items-center gap-1" role="listitem"><span class="font-bold text-ink-900 dark:text-ink-50">{formatCompactNumber(p.followsCount, locale)}</span><span class="text-ink-700 dark:text-ink-200">Following</span></div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="mt-4">
|
|
64
|
+
<LinkCard url="https://witchsky.app/profile/{p.did}" title="View on Bluesky" variant="button" />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
{/snippet}
|
|
68
|
+
</Card>
|
|
69
|
+
{/if}
|
|
70
|
+
</div>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ExternalLink, GitBranch, Server, User } from '@lucide/svelte';
|
|
3
|
+
import Card from '../../../ui/Card.svelte';
|
|
4
|
+
import InternalCard from '../../../ui/InternalCard.svelte';
|
|
5
|
+
import type { TangledReposData, ProfileData } from '@ewanc26/atproto';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
repos?: TangledReposData | null;
|
|
9
|
+
profile?: ProfileData | null;
|
|
10
|
+
/** Fallback DID if profile handle is unavailable */
|
|
11
|
+
did?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { repos = null, profile = null, did = '' }: Props = $props();
|
|
15
|
+
let handle = $derived(profile?.handle || null);
|
|
16
|
+
|
|
17
|
+
function buildRepoUrl(repoName: string): string {
|
|
18
|
+
const identifier = handle || did;
|
|
19
|
+
return `https://tangled.org/${identifier}/${repoName}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getKnotServerName(knot: string): string {
|
|
23
|
+
if (knot.startsWith('http')) {
|
|
24
|
+
try { return new URL(knot).hostname; } catch { return knot; }
|
|
25
|
+
}
|
|
26
|
+
return knot;
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<div class="mx-auto w-full max-w-2xl">
|
|
31
|
+
{#if !repos}
|
|
32
|
+
<Card loading={true} variant="elevated" padding="md">
|
|
33
|
+
{#snippet skeleton()}
|
|
34
|
+
<div class="mb-4 h-6 w-32 rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
35
|
+
<div class="space-y-3">
|
|
36
|
+
{#each Array(3) as _}
|
|
37
|
+
<div class="h-24 rounded-lg bg-canvas-300 dark:bg-canvas-700"></div>
|
|
38
|
+
{/each}
|
|
39
|
+
</div>
|
|
40
|
+
{/snippet}
|
|
41
|
+
</Card>
|
|
42
|
+
{:else if repos.repos.length > 0}
|
|
43
|
+
<Card variant="elevated" padding="md">
|
|
44
|
+
{#snippet children()}
|
|
45
|
+
<h2 class="mb-4 text-2xl font-bold text-ink-900 dark:text-ink-50">Tangled Repositories</h2>
|
|
46
|
+
<div class="space-y-3">
|
|
47
|
+
{#each repos.repos as repo}
|
|
48
|
+
<InternalCard href={buildRepoUrl(repo.name)}>
|
|
49
|
+
{#snippet children()}
|
|
50
|
+
<GitBranch class="h-5 w-5 shrink-0 text-primary-600 dark:text-primary-400" aria-hidden="true" />
|
|
51
|
+
<div class="min-w-0 flex-1 space-y-2">
|
|
52
|
+
<h3 class="overflow-wrap-anywhere font-semibold wrap-break-word text-ink-900 dark:text-ink-50">{repo.name}</h3>
|
|
53
|
+
<div class="flex flex-wrap items-center gap-3 text-xs text-ink-700 dark:text-ink-200">
|
|
54
|
+
<div class="flex min-w-0 items-center gap-1">
|
|
55
|
+
<Server class="h-3 w-3 shrink-0" aria-hidden="true" />
|
|
56
|
+
<span class="truncate">{getKnotServerName(repo.knot)}</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="flex min-w-0 items-center gap-1">
|
|
59
|
+
<User class="h-3 w-3 shrink-0" aria-hidden="true" />
|
|
60
|
+
<span class="truncate">{handle || did}</span>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<ExternalLink class="h-4 w-4 shrink-0 text-ink-700 transition-colors dark:text-ink-200" aria-hidden="true" />
|
|
65
|
+
{/snippet}
|
|
66
|
+
</InternalCard>
|
|
67
|
+
{/each}
|
|
68
|
+
</div>
|
|
69
|
+
{/snippet}
|
|
70
|
+
</Card>
|
|
71
|
+
{:else}
|
|
72
|
+
<Card variant="flat" padding="lg">
|
|
73
|
+
{#snippet children()}
|
|
74
|
+
<div class="text-center">
|
|
75
|
+
<p class="text-ink-700 dark:text-ink-300">No Tangled repositories found.</p>
|
|
76
|
+
</div>
|
|
77
|
+
{/snippet}
|
|
78
|
+
</Card>
|
|
79
|
+
{/if}
|
|
80
|
+
</div>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as LinkCard } from './LinkCard.svelte';
|
|
2
|
+
export { default as ProfileCard } from './ProfileCard.svelte';
|
|
3
|
+
export { default as PostCard } from './PostCard.svelte';
|
|
4
|
+
export { default as BlueskyPostCard } from './BlueskyPostCard.svelte';
|
|
5
|
+
export { default as TangledRepoCard } from './TangledRepoCard.svelte';
|
|
6
|
+
export { default as MusicStatusCard } from './MusicStatusCard.svelte';
|
|
7
|
+
export { default as KibunStatusCard } from './KibunStatusCard.svelte';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SiteMetadata } from '../../types/index.js';
|
|
3
|
+
|
|
4
|
+
interface Props { meta: SiteMetadata; siteMeta: SiteMetadata; }
|
|
5
|
+
let { meta, siteMeta }: Props = $props();
|
|
6
|
+
|
|
7
|
+
const finalMeta = $derived({
|
|
8
|
+
title: meta.title || siteMeta.title,
|
|
9
|
+
description: meta.description || siteMeta.description,
|
|
10
|
+
keywords: meta.keywords || siteMeta.keywords,
|
|
11
|
+
url: meta.url || siteMeta.url,
|
|
12
|
+
image: meta.image || siteMeta.image,
|
|
13
|
+
imageWidth: meta.imageWidth || siteMeta.imageWidth,
|
|
14
|
+
imageHeight: meta.imageHeight || siteMeta.imageHeight
|
|
15
|
+
});
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<svelte:head>
|
|
19
|
+
<title>{finalMeta.title}</title>
|
|
20
|
+
<meta name="description" content={finalMeta.description} />
|
|
21
|
+
<meta name="keywords" content={finalMeta.keywords} />
|
|
22
|
+
<meta property="og:type" content="website" />
|
|
23
|
+
<meta property="og:url" content={finalMeta.url} />
|
|
24
|
+
<meta property="og:title" content={finalMeta.title} />
|
|
25
|
+
<meta property="og:description" content={finalMeta.description} />
|
|
26
|
+
<meta property="og:site_name" content={siteMeta.title} />
|
|
27
|
+
<meta property="og:image" content={finalMeta.image} />
|
|
28
|
+
{#if finalMeta.imageWidth}
|
|
29
|
+
<meta property="og:image:width" content={finalMeta.imageWidth.toString()} />
|
|
30
|
+
{/if}
|
|
31
|
+
{#if finalMeta.imageHeight}
|
|
32
|
+
<meta property="og:image:height" content={finalMeta.imageHeight.toString()} />
|
|
33
|
+
{/if}
|
|
34
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
35
|
+
<meta name="twitter:url" content={finalMeta.url} />
|
|
36
|
+
<meta name="twitter:title" content={finalMeta.title} />
|
|
37
|
+
<meta name="twitter:description" content={finalMeta.description} />
|
|
38
|
+
<meta name="twitter:image" content={finalMeta.image} />
|
|
39
|
+
</svelte:head>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as MetaTags } from './MetaTags.svelte';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ExternalLink, Tag } from '@lucide/svelte';
|
|
3
|
+
import type { BlogPost } from '@ewanc26/atproto';
|
|
4
|
+
import InternalCard from './InternalCard.svelte';
|
|
5
|
+
import { getPostBadges, getBadgeClasses } from '../../helper/badges.js';
|
|
6
|
+
import { formatLocalizedDate } from '../../utils/locale.js';
|
|
7
|
+
|
|
8
|
+
interface Props { post: BlogPost; locale?: string; }
|
|
9
|
+
let { post, locale }: Props = $props();
|
|
10
|
+
const badges = $derived(getPostBadges(post));
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<InternalCard href={post.url}>
|
|
14
|
+
{#snippet children()}
|
|
15
|
+
{#if post.coverImage}
|
|
16
|
+
<div class="mb-3 overflow-hidden rounded-lg">
|
|
17
|
+
<img src={post.coverImage} alt={post.title} class="h-48 w-full object-cover transition-transform duration-300 hover:scale-105" />
|
|
18
|
+
</div>
|
|
19
|
+
{/if}
|
|
20
|
+
<div class="relative min-w-0 flex-1 space-y-2">
|
|
21
|
+
{#if badges.length > 0}
|
|
22
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
23
|
+
{#each badges as badge}<span class={getBadgeClasses(badge)}>{badge.text}</span>{/each}
|
|
24
|
+
</div>
|
|
25
|
+
{/if}
|
|
26
|
+
<h4 class="overflow-wrap-anywhere font-semibold wrap-break-word text-ink-900 dark:text-ink-50">{post.title}</h4>
|
|
27
|
+
{#if post.description}
|
|
28
|
+
<p class="overflow-wrap-anywhere line-clamp-2 text-sm wrap-break-word text-ink-700 dark:text-ink-200">{post.description}</p>
|
|
29
|
+
{/if}
|
|
30
|
+
<div class="pt-1">
|
|
31
|
+
<p class="text-xs font-medium text-ink-800 dark:text-ink-100">{formatLocalizedDate(post.createdAt, locale)}</p>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="flex shrink-0 flex-col items-end justify-between gap-2 self-stretch">
|
|
35
|
+
<ExternalLink class="h-4 w-4 text-ink-700 transition-colors dark:text-ink-200" aria-hidden="true" />
|
|
36
|
+
{#if post.tags && post.tags.length > 0}
|
|
37
|
+
<div class="flex items-center gap-1.5 rounded bg-ink-100 px-2 py-0.5 dark:bg-ink-800">
|
|
38
|
+
<Tag class="h-3 w-3 text-ink-700 dark:text-ink-200" aria-hidden="true" />
|
|
39
|
+
<span class="text-xs font-medium text-ink-800 dark:text-ink-100">{post.tags.length}</span>
|
|
40
|
+
</div>
|
|
41
|
+
{/if}
|
|
42
|
+
</div>
|
|
43
|
+
{/snippet}
|
|
44
|
+
</InternalCard>
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ExternalLink } from '@lucide/svelte';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
|
|
5
|
+
export interface Badge {
|
|
6
|
+
text: string;
|
|
7
|
+
color?: 'mint' | 'sage' | 'jade' | 'ink';
|
|
8
|
+
variant?: 'solid' | 'soft';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
variant?: 'default' | 'elevated' | 'flat' | 'button' | 'outline';
|
|
13
|
+
padding?: 'sm' | 'md' | 'lg' | 'none';
|
|
14
|
+
interactive?: boolean;
|
|
15
|
+
href?: string;
|
|
16
|
+
target?: string;
|
|
17
|
+
rel?: string;
|
|
18
|
+
showExternalIcon?: boolean;
|
|
19
|
+
badges?: Badge[];
|
|
20
|
+
loading?: boolean;
|
|
21
|
+
error?: boolean;
|
|
22
|
+
errorMessage?: string;
|
|
23
|
+
class?: string;
|
|
24
|
+
ariaLabel?: string;
|
|
25
|
+
children?: Snippet;
|
|
26
|
+
skeleton?: Snippet;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
variant = 'default',
|
|
31
|
+
padding = 'md',
|
|
32
|
+
interactive = false,
|
|
33
|
+
href,
|
|
34
|
+
target = '_blank',
|
|
35
|
+
rel = 'noopener noreferrer',
|
|
36
|
+
showExternalIcon = false,
|
|
37
|
+
badges = [],
|
|
38
|
+
loading = false,
|
|
39
|
+
error = false,
|
|
40
|
+
errorMessage = 'Something went wrong',
|
|
41
|
+
class: customClass = '',
|
|
42
|
+
ariaLabel,
|
|
43
|
+
children,
|
|
44
|
+
skeleton
|
|
45
|
+
}: Props = $props();
|
|
46
|
+
|
|
47
|
+
let isLink = $derived(!!href);
|
|
48
|
+
const baseClasses = 'rounded-xl transition-all duration-300';
|
|
49
|
+
const variantClasses = {
|
|
50
|
+
default: 'bg-canvas-100 dark:bg-canvas-900 shadow-md',
|
|
51
|
+
elevated: 'bg-canvas-100 dark:bg-canvas-900 shadow-lg hover:shadow-xl',
|
|
52
|
+
flat: 'bg-canvas-200 dark:bg-canvas-800',
|
|
53
|
+
button:
|
|
54
|
+
'bg-canvas-200 dark:bg-canvas-800 hover:bg-canvas-300 dark:hover:bg-canvas-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600',
|
|
55
|
+
outline:
|
|
56
|
+
'bg-transparent border-2 border-canvas-300 dark:border-canvas-700 hover:border-primary-400 dark:hover:border-primary-600'
|
|
57
|
+
};
|
|
58
|
+
const paddingClasses = { none: '', sm: 'p-4', md: 'p-6', lg: 'p-8' };
|
|
59
|
+
let interactiveClasses = $derived(interactive || isLink ? 'cursor-pointer' : '');
|
|
60
|
+
let cardClasses = $derived(
|
|
61
|
+
`${baseClasses} ${variantClasses[variant]} ${paddingClasses[padding]} ${interactiveClasses} ${customClass}`
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
function getBadgeClasses(badge: Badge): string {
|
|
65
|
+
const baseStyle =
|
|
66
|
+
badge.variant === 'soft'
|
|
67
|
+
? 'px-2 py-0.5 text-xs font-medium rounded'
|
|
68
|
+
: 'px-2 py-0.5 text-xs font-semibold uppercase rounded';
|
|
69
|
+
const colorClasses = {
|
|
70
|
+
mint:
|
|
71
|
+
badge.variant === 'soft'
|
|
72
|
+
? 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-200'
|
|
73
|
+
: 'bg-secondary-500 text-white dark:bg-secondary-600',
|
|
74
|
+
sage:
|
|
75
|
+
badge.variant === 'soft'
|
|
76
|
+
? 'bg-primary-100 text-primary-800 dark:bg-primary-900 dark:text-primary-200'
|
|
77
|
+
: 'bg-primary-500 text-white dark:bg-primary-600',
|
|
78
|
+
jade:
|
|
79
|
+
badge.variant === 'soft'
|
|
80
|
+
? 'bg-accent-100 text-accent-800 dark:bg-accent-900 dark:text-accent-200'
|
|
81
|
+
: 'bg-accent-500 text-white dark:bg-accent-600',
|
|
82
|
+
ink:
|
|
83
|
+
badge.variant === 'soft'
|
|
84
|
+
? 'bg-ink-100 text-ink-800 dark:bg-ink-800 dark:text-ink-100'
|
|
85
|
+
: 'bg-ink-700 text-white dark:bg-ink-300 dark:text-ink-900'
|
|
86
|
+
};
|
|
87
|
+
return `${baseStyle} ${colorClasses[badge.color || 'ink']}`;
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
{#if loading}
|
|
92
|
+
<div class="{cardClasses} animate-pulse" aria-label="Loading content" role="status">
|
|
93
|
+
{#if skeleton}
|
|
94
|
+
{@render skeleton()}
|
|
95
|
+
{:else}
|
|
96
|
+
<div class="space-y-3">
|
|
97
|
+
<div class="h-4 w-3/4 rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
98
|
+
<div class="h-4 w-full rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
99
|
+
<div class="h-4 w-5/6 rounded bg-canvas-300 dark:bg-canvas-700"></div>
|
|
100
|
+
</div>
|
|
101
|
+
{/if}
|
|
102
|
+
<span class="sr-only">Loading...</span>
|
|
103
|
+
</div>
|
|
104
|
+
{:else if error}
|
|
105
|
+
<div
|
|
106
|
+
class="{cardClasses} border-2 border-red-300 bg-red-50 dark:border-red-700 dark:bg-red-900/20"
|
|
107
|
+
role="alert"
|
|
108
|
+
aria-live="polite"
|
|
109
|
+
>
|
|
110
|
+
<p class="text-red-600 dark:text-red-400">{errorMessage}</p>
|
|
111
|
+
</div>
|
|
112
|
+
{:else if isLink}
|
|
113
|
+
<a {href} {target} {rel} class={cardClasses} aria-label={ariaLabel || `Link to ${href}`}>
|
|
114
|
+
{#if badges.length > 0}
|
|
115
|
+
<div class="mb-3 flex flex-wrap items-center gap-2">
|
|
116
|
+
{#each badges as badge}
|
|
117
|
+
<span class={getBadgeClasses(badge)}>{badge.text}</span>
|
|
118
|
+
{/each}
|
|
119
|
+
</div>
|
|
120
|
+
{/if}
|
|
121
|
+
<div class="flex items-start justify-between gap-3">
|
|
122
|
+
<div class="flex-1">
|
|
123
|
+
{#if children}{@render children()}{/if}
|
|
124
|
+
</div>
|
|
125
|
+
{#if showExternalIcon}
|
|
126
|
+
<ExternalLink
|
|
127
|
+
class="h-4 w-4 shrink-0 text-ink-700 transition-colors group-hover:text-primary-600 dark:text-ink-200 dark:group-hover:text-primary-400"
|
|
128
|
+
aria-hidden="true"
|
|
129
|
+
/>
|
|
130
|
+
{/if}
|
|
131
|
+
</div>
|
|
132
|
+
</a>
|
|
133
|
+
{:else}
|
|
134
|
+
<div class={cardClasses} role={ariaLabel ? 'region' : undefined} aria-label={ariaLabel}>
|
|
135
|
+
{#if badges.length > 0}
|
|
136
|
+
<div class="mb-3 flex flex-wrap items-center gap-2">
|
|
137
|
+
{#each badges as badge}
|
|
138
|
+
<span class={getBadgeClasses(badge)}>{badge.text}</span>
|
|
139
|
+
{/each}
|
|
140
|
+
</div>
|
|
141
|
+
{/if}
|
|
142
|
+
{#if children}{@render children()}{/if}
|
|
143
|
+
</div>
|
|
144
|
+
{/if}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ExternalLink, Tag } from '@lucide/svelte';
|
|
3
|
+
import type { StandardSiteDocument } from '@ewanc26/atproto';
|
|
4
|
+
import InternalCard from './InternalCard.svelte';
|
|
5
|
+
import { formatLocalizedDate } from '../../utils/locale.js';
|
|
6
|
+
|
|
7
|
+
interface Props { document: StandardSiteDocument; locale?: string; }
|
|
8
|
+
let { document, locale }: Props = $props();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<InternalCard href={document.url}>
|
|
12
|
+
{#snippet children()}
|
|
13
|
+
{#if document.coverImage}
|
|
14
|
+
<div class="mb-3 overflow-hidden rounded-lg">
|
|
15
|
+
<img src={document.coverImage} alt={document.title} class="h-48 w-full object-cover transition-transform duration-300 hover:scale-105" />
|
|
16
|
+
</div>
|
|
17
|
+
{/if}
|
|
18
|
+
<div class="relative min-w-0 flex-1 space-y-2">
|
|
19
|
+
{#if document.publicationName}
|
|
20
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
21
|
+
<span class="rounded bg-accent-500 px-2 py-0.5 text-xs font-semibold text-white uppercase dark:bg-accent-600">{document.publicationName}</span>
|
|
22
|
+
</div>
|
|
23
|
+
{/if}
|
|
24
|
+
<h4 class="overflow-wrap-anywhere font-semibold wrap-break-word text-ink-900 dark:text-ink-50">{document.title}</h4>
|
|
25
|
+
{#if document.description}
|
|
26
|
+
<p class="overflow-wrap-anywhere line-clamp-2 text-sm wrap-break-word text-ink-700 dark:text-ink-200">{document.description}</p>
|
|
27
|
+
{/if}
|
|
28
|
+
<div class="pt-1">
|
|
29
|
+
<p class="text-xs font-medium text-ink-800 dark:text-ink-100">{formatLocalizedDate(document.publishedAt, locale)}</p>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="flex shrink-0 flex-col items-end justify-between gap-2 self-stretch">
|
|
33
|
+
<ExternalLink class="h-4 w-4 text-ink-700 transition-colors dark:text-ink-200" aria-hidden="true" />
|
|
34
|
+
{#if document.tags && document.tags.length > 0}
|
|
35
|
+
<div class="flex items-center gap-1.5 rounded bg-ink-100 px-2 py-0.5 dark:bg-ink-800">
|
|
36
|
+
<Tag class="h-3 w-3 text-ink-700 dark:text-ink-200" aria-hidden="true" />
|
|
37
|
+
<span class="text-xs font-medium text-ink-800 dark:text-ink-100">{document.tags.length}</span>
|
|
38
|
+
</div>
|
|
39
|
+
{/if}
|
|
40
|
+
</div>
|
|
41
|
+
{/snippet}
|
|
42
|
+
</InternalCard>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ChevronDown } from '@lucide/svelte';
|
|
3
|
+
|
|
4
|
+
interface Option { value: string; label: string; }
|
|
5
|
+
interface Props {
|
|
6
|
+
options: Option[];
|
|
7
|
+
value: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
id?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { options, value = $bindable(), label, placeholder = 'Select...', id = 'dropdown' }: Props = $props();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div class="relative">
|
|
17
|
+
{#if label}
|
|
18
|
+
<label for={id} class="mb-2 block text-sm font-medium text-ink-700 dark:text-ink-200">{label}</label>
|
|
19
|
+
{/if}
|
|
20
|
+
<div class="relative">
|
|
21
|
+
<select
|
|
22
|
+
{id}
|
|
23
|
+
bind:value
|
|
24
|
+
class="w-full appearance-none rounded-lg border-2 border-canvas-300 bg-canvas-100 py-2 pr-10 pl-3 text-ink-900 transition-colors focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 focus:outline-none dark:border-canvas-700 dark:bg-canvas-900 dark:text-ink-50 dark:focus:border-primary-400"
|
|
25
|
+
aria-label={label || 'Select an option'}
|
|
26
|
+
>
|
|
27
|
+
<option value="" disabled>{placeholder}</option>
|
|
28
|
+
{#each options as option}
|
|
29
|
+
<option value={option.value}>{option.label}</option>
|
|
30
|
+
{/each}
|
|
31
|
+
</select>
|
|
32
|
+
<div class="pointer-events-none absolute top-1/2 right-3 -translate-y-1/2 text-ink-500 dark:text-ink-400" aria-hidden="true">
|
|
33
|
+
<ChevronDown class="h-5 w-5" />
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|