@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,41 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
href?: string;
|
|
6
|
+
target?: string;
|
|
7
|
+
rel?: string;
|
|
8
|
+
onclick?: () => void;
|
|
9
|
+
class?: string;
|
|
10
|
+
ariaLabel?: string;
|
|
11
|
+
children?: Snippet;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
href,
|
|
16
|
+
target = '_blank',
|
|
17
|
+
rel = 'noopener noreferrer',
|
|
18
|
+
onclick,
|
|
19
|
+
class: customClass = '',
|
|
20
|
+
ariaLabel,
|
|
21
|
+
children
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
|
|
24
|
+
const baseClasses =
|
|
25
|
+
'flex items-start gap-3 rounded-lg bg-canvas-200 p-4 transition-colors hover:bg-canvas-300 dark:bg-canvas-800 dark:hover:bg-canvas-700 self-start';
|
|
26
|
+
let combinedClasses = $derived(`${baseClasses} ${customClass}`);
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
{#if href}
|
|
30
|
+
<a {href} {target} {rel} class={combinedClasses} aria-label={ariaLabel}>
|
|
31
|
+
{#if children}{@render children()}{/if}
|
|
32
|
+
</a>
|
|
33
|
+
{:else if onclick}
|
|
34
|
+
<button type="button" {onclick} class={combinedClasses} aria-label={ariaLabel}>
|
|
35
|
+
{#if children}{@render children()}{/if}
|
|
36
|
+
</button>
|
|
37
|
+
{:else}
|
|
38
|
+
<div class={combinedClasses} role={ariaLabel ? 'region' : undefined} aria-label={ariaLabel}>
|
|
39
|
+
{#if children}{@render children()}{/if}
|
|
40
|
+
</div>
|
|
41
|
+
{/if}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
currentPage: number;
|
|
6
|
+
totalPages: number;
|
|
7
|
+
totalItems: number;
|
|
8
|
+
itemsPerPage: number;
|
|
9
|
+
onPageChange: (page: number) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { currentPage, totalPages, totalItems, itemsPerPage, onPageChange }: Props = $props();
|
|
13
|
+
|
|
14
|
+
function getPageNumbers(current: number, total: number): (number | string)[] {
|
|
15
|
+
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
|
|
16
|
+
const pages: (number | string)[] = [1];
|
|
17
|
+
if (current > 3) pages.push('...');
|
|
18
|
+
const start = Math.max(2, current - 1);
|
|
19
|
+
const end = Math.min(total - 1, current + 1);
|
|
20
|
+
for (let i = start; i <= end; i++) pages.push(i);
|
|
21
|
+
if (current < total - 2) pages.push('...');
|
|
22
|
+
if (total > 1) pages.push(total);
|
|
23
|
+
return pages;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const pageNumbers = $derived(getPageNumbers(currentPage, totalPages));
|
|
27
|
+
const startItem = $derived((currentPage - 1) * itemsPerPage + 1);
|
|
28
|
+
const endItem = $derived(Math.min(currentPage * itemsPerPage, totalItems));
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
{#if totalPages > 1}
|
|
32
|
+
<nav class="mt-12" aria-label="Pagination navigation">
|
|
33
|
+
<div class="flex items-center justify-center gap-2" role="navigation">
|
|
34
|
+
<button
|
|
35
|
+
onclick={() => currentPage > 1 && onPageChange(currentPage - 1)}
|
|
36
|
+
disabled={currentPage === 1}
|
|
37
|
+
class="flex h-10 w-10 items-center justify-center rounded-lg border-2 border-canvas-300 bg-canvas-100 text-ink-700 transition-colors hover:bg-canvas-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-canvas-700 dark:bg-canvas-900 dark:text-ink-200 dark:hover:bg-canvas-800"
|
|
38
|
+
aria-label="Go to previous page"
|
|
39
|
+
>
|
|
40
|
+
<ChevronLeft class="h-5 w-5" aria-hidden="true" />
|
|
41
|
+
</button>
|
|
42
|
+
|
|
43
|
+
{#each pageNumbers as page}
|
|
44
|
+
{#if page === '...'}
|
|
45
|
+
<span class="px-2 text-ink-500 dark:text-ink-400" aria-hidden="true">...</span>
|
|
46
|
+
{:else}
|
|
47
|
+
<button
|
|
48
|
+
onclick={() => onPageChange(page as number)}
|
|
49
|
+
class="flex h-10 min-w-[2.5rem] items-center justify-center rounded-lg border-2 px-3 font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 {currentPage === page
|
|
50
|
+
? 'border-primary-500 bg-primary-500 text-white dark:border-primary-400 dark:bg-primary-400'
|
|
51
|
+
: 'border-canvas-300 bg-canvas-100 text-ink-700 hover:bg-canvas-200 dark:border-canvas-700 dark:bg-canvas-900 dark:text-ink-200 dark:hover:bg-canvas-800'}"
|
|
52
|
+
aria-label="Go to page {page}"
|
|
53
|
+
aria-current={currentPage === page ? 'page' : undefined}
|
|
54
|
+
>
|
|
55
|
+
{page}
|
|
56
|
+
</button>
|
|
57
|
+
{/if}
|
|
58
|
+
{/each}
|
|
59
|
+
|
|
60
|
+
<button
|
|
61
|
+
onclick={() => currentPage < totalPages && onPageChange(currentPage + 1)}
|
|
62
|
+
disabled={currentPage === totalPages}
|
|
63
|
+
class="flex h-10 w-10 items-center justify-center rounded-lg border-2 border-canvas-300 bg-canvas-100 text-ink-700 transition-colors hover:bg-canvas-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-canvas-700 dark:bg-canvas-900 dark:text-ink-200 dark:hover:bg-canvas-800"
|
|
64
|
+
aria-label="Go to next page"
|
|
65
|
+
>
|
|
66
|
+
<ChevronRight class="h-5 w-5" aria-hidden="true" />
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
<p class="mt-4 text-center text-sm text-ink-600 dark:text-ink-300" role="status" aria-live="polite" aria-atomic="true">
|
|
70
|
+
Page {currentPage} of {totalPages} · Showing {startItem}–{endItem} of {totalItems}
|
|
71
|
+
{totalItems === 1 ? 'item' : 'items'}
|
|
72
|
+
</p>
|
|
73
|
+
</nav>
|
|
74
|
+
{/if}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { BlogPost } from '@ewanc26/atproto';
|
|
3
|
+
import BlogPostCard from './BlogPostCard.svelte';
|
|
4
|
+
import { getUserLocale } from '../../utils/locale.js';
|
|
5
|
+
import { groupPostsByDate, getSortedMonths, getSortedYears } from '../../helper/posts.js';
|
|
6
|
+
|
|
7
|
+
interface Props { posts: BlogPost[]; locale?: string; filterYear?: string | null; }
|
|
8
|
+
let { posts, locale, filterYear }: Props = $props();
|
|
9
|
+
let userLocale = $derived(locale || getUserLocale());
|
|
10
|
+
const groupedPosts = $derived(groupPostsByDate(posts, userLocale));
|
|
11
|
+
const sortedYears = $derived(
|
|
12
|
+
filterYear && filterYear !== 'all'
|
|
13
|
+
? [parseInt(filterYear)].filter((year) => groupedPosts.has(year))
|
|
14
|
+
: getSortedYears(groupedPosts)
|
|
15
|
+
);
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<div class="space-y-12">
|
|
19
|
+
{#each sortedYears as year}
|
|
20
|
+
{@const yearGroup = groupedPosts.get(year)}
|
|
21
|
+
{#if yearGroup}
|
|
22
|
+
{@const sortedMonths = getSortedMonths(yearGroup)}
|
|
23
|
+
<section>
|
|
24
|
+
<h2 class="mb-6 text-3xl font-bold text-ink-900 dark:text-ink-50">{year}</h2>
|
|
25
|
+
<div class="space-y-8">
|
|
26
|
+
{#each sortedMonths as [_, monthData]}
|
|
27
|
+
<div>
|
|
28
|
+
<h3 class="mb-4 text-xl font-semibold text-ink-800 dark:text-ink-100">{monthData.monthName}</h3>
|
|
29
|
+
<div class="space-y-3">
|
|
30
|
+
{#each monthData.posts as post}
|
|
31
|
+
<BlogPostCard {post} locale={userLocale} />
|
|
32
|
+
{/each}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
{/each}
|
|
36
|
+
</div>
|
|
37
|
+
</section>
|
|
38
|
+
{/if}
|
|
39
|
+
{/each}
|
|
40
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Search } from '@lucide/svelte';
|
|
3
|
+
interface Props { value: string; placeholder?: string; resultCount?: number; }
|
|
4
|
+
let { value = $bindable(), placeholder = 'Search...', resultCount }: Props = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<div role="search">
|
|
8
|
+
<label for="search-input" class="sr-only">Search</label>
|
|
9
|
+
<div class="relative">
|
|
10
|
+
<Search class="absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 text-ink-500 dark:text-ink-400" aria-hidden="true" />
|
|
11
|
+
<input
|
|
12
|
+
id="search-input"
|
|
13
|
+
type="search"
|
|
14
|
+
{placeholder}
|
|
15
|
+
bind:value
|
|
16
|
+
class="w-full rounded-lg border-2 border-canvas-300 bg-canvas-100 py-3 pr-4 pl-11 text-ink-900 placeholder-ink-500 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:placeholder-ink-400 dark:focus:border-primary-400"
|
|
17
|
+
aria-label="Search"
|
|
18
|
+
autocomplete="off"
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
{#if value && resultCount !== undefined}
|
|
22
|
+
<p class="mt-2 text-sm text-ink-600 dark:text-ink-300" role="status" aria-live="polite">
|
|
23
|
+
Found {resultCount} {resultCount === 1 ? 'result' : 'results'}
|
|
24
|
+
</p>
|
|
25
|
+
{/if}
|
|
26
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Tab { id: string; label: string; }
|
|
3
|
+
interface Props { tabs: Tab[]; activeTab: string; onTabChange: (tabId: string) => void; }
|
|
4
|
+
let { tabs, activeTab, onTabChange }: Props = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<div class="mb-8" role="tablist" aria-label="Content tabs">
|
|
8
|
+
<div class="flex flex-wrap gap-2">
|
|
9
|
+
{#each tabs as tab}
|
|
10
|
+
<button
|
|
11
|
+
onclick={() => onTabChange(tab.id)}
|
|
12
|
+
role="tab"
|
|
13
|
+
aria-selected={activeTab === tab.id}
|
|
14
|
+
aria-controls="{tab.id}-panel"
|
|
15
|
+
id="{tab.id}-tab"
|
|
16
|
+
tabindex={activeTab === tab.id ? 0 : -1}
|
|
17
|
+
class="rounded-full px-4 py-2 text-sm font-medium transition-all focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 {activeTab === tab.id
|
|
18
|
+
? 'bg-primary-500 text-white shadow-md dark:bg-primary-400'
|
|
19
|
+
: 'bg-canvas-200 text-ink-700 hover:bg-canvas-300 dark:bg-canvas-800 dark:text-ink-200 dark:hover:bg-canvas-700'}"
|
|
20
|
+
>
|
|
21
|
+
{tab.label}
|
|
22
|
+
</button>
|
|
23
|
+
{/each}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { default as Card } from './Card.svelte';
|
|
2
|
+
export { default as InternalCard } from './InternalCard.svelte';
|
|
3
|
+
export { default as Dropdown } from './Dropdown.svelte';
|
|
4
|
+
export { default as Pagination } from './Pagination.svelte';
|
|
5
|
+
export { default as SearchBar } from './SearchBar.svelte';
|
|
6
|
+
export { default as Tabs } from './Tabs.svelte';
|
|
7
|
+
export { default as PostsGroupedView } from './PostsGroupedView.svelte';
|
|
8
|
+
export { default as DocumentCard } from './DocumentCard.svelte';
|
|
9
|
+
/** @deprecated Use DocumentCard instead */
|
|
10
|
+
export { default as BlogPostCard } from './BlogPostCard.svelte';
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central theme configuration
|
|
3
|
+
* Add new themes here and they'll automatically appear in the dropdown and type system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ThemeDefinition {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
description: string;
|
|
10
|
+
color: string;
|
|
11
|
+
category: 'neutral' | 'warm' | 'cool' | 'vibrant';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const THEMES: readonly ThemeDefinition[] = [
|
|
15
|
+
// Neutral themes
|
|
16
|
+
{
|
|
17
|
+
value: 'sage',
|
|
18
|
+
label: 'Sage',
|
|
19
|
+
description: 'Calm green-blue',
|
|
20
|
+
color: 'oklch(77.77% 0.182 127.42)',
|
|
21
|
+
category: 'neutral'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
value: 'monochrome',
|
|
25
|
+
label: 'Monochrome',
|
|
26
|
+
description: 'Pure greyscale',
|
|
27
|
+
color: 'oklch(78% 0 0)',
|
|
28
|
+
category: 'neutral'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
value: 'slate',
|
|
32
|
+
label: 'Slate',
|
|
33
|
+
description: 'Blue-grey',
|
|
34
|
+
color: 'oklch(78.5% 0.095 230)',
|
|
35
|
+
category: 'neutral'
|
|
36
|
+
},
|
|
37
|
+
// Warm themes
|
|
38
|
+
{
|
|
39
|
+
value: 'ruby',
|
|
40
|
+
label: 'Ruby',
|
|
41
|
+
description: 'Bold red',
|
|
42
|
+
color: 'oklch(81.5% 0.228 10)',
|
|
43
|
+
category: 'warm'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
value: 'coral',
|
|
47
|
+
label: 'Coral',
|
|
48
|
+
description: 'Orange-pink',
|
|
49
|
+
color: 'oklch(81.8% 0.212 20)',
|
|
50
|
+
category: 'warm'
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
value: 'sunset',
|
|
54
|
+
label: 'Sunset',
|
|
55
|
+
description: 'Warm orange',
|
|
56
|
+
color: 'oklch(80.5% 0.208 45)',
|
|
57
|
+
category: 'warm'
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
value: 'amber',
|
|
61
|
+
label: 'Amber',
|
|
62
|
+
description: 'Bright yellow',
|
|
63
|
+
color: 'oklch(82.8% 0.195 85)',
|
|
64
|
+
category: 'warm'
|
|
65
|
+
},
|
|
66
|
+
// Cool themes
|
|
67
|
+
{
|
|
68
|
+
value: 'forest',
|
|
69
|
+
label: 'Forest',
|
|
70
|
+
description: 'Natural green',
|
|
71
|
+
color: 'oklch(79.5% 0.195 145)',
|
|
72
|
+
category: 'cool'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
value: 'teal',
|
|
76
|
+
label: 'Teal',
|
|
77
|
+
description: 'Blue-green',
|
|
78
|
+
color: 'oklch(79% 0.205 195)',
|
|
79
|
+
category: 'cool'
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
value: 'ocean',
|
|
83
|
+
label: 'Ocean',
|
|
84
|
+
description: 'Deep blue',
|
|
85
|
+
color: 'oklch(78.2% 0.188 240)',
|
|
86
|
+
category: 'cool'
|
|
87
|
+
},
|
|
88
|
+
// Vibrant themes
|
|
89
|
+
{
|
|
90
|
+
value: 'lavender',
|
|
91
|
+
label: 'Lavender',
|
|
92
|
+
description: 'Soft purple',
|
|
93
|
+
color: 'oklch(82% 0.215 295)',
|
|
94
|
+
category: 'vibrant'
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
value: 'rose',
|
|
98
|
+
label: 'Rose',
|
|
99
|
+
description: 'Pink-red',
|
|
100
|
+
color: 'oklch(83.5% 0.230 350)',
|
|
101
|
+
category: 'vibrant'
|
|
102
|
+
}
|
|
103
|
+
] as const;
|
|
104
|
+
|
|
105
|
+
export type ColorTheme = (typeof THEMES)[number]['value'];
|
|
106
|
+
export const DEFAULT_THEME: ColorTheme = 'slate';
|
|
107
|
+
|
|
108
|
+
export const CATEGORY_LABELS = {
|
|
109
|
+
neutral: 'Neutral',
|
|
110
|
+
warm: 'Warm',
|
|
111
|
+
cool: 'Cool',
|
|
112
|
+
vibrant: 'Vibrant'
|
|
113
|
+
} as const;
|
|
114
|
+
|
|
115
|
+
export const getThemesByCategory = () => {
|
|
116
|
+
const grouped: Record<ThemeDefinition['category'], ThemeDefinition[]> = {
|
|
117
|
+
neutral: [],
|
|
118
|
+
warm: [],
|
|
119
|
+
cool: [],
|
|
120
|
+
vibrant: []
|
|
121
|
+
};
|
|
122
|
+
THEMES.forEach((theme) => {
|
|
123
|
+
grouped[theme.category].push(theme);
|
|
124
|
+
});
|
|
125
|
+
return grouped;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const getTheme = (value: string): ThemeDefinition | undefined => {
|
|
129
|
+
return THEMES.find((theme) => theme.value === value);
|
|
130
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { BlogPost } from '@ewanc26/atproto';
|
|
2
|
+
|
|
3
|
+
export interface PostBadge {
|
|
4
|
+
text: string;
|
|
5
|
+
color: 'mint' | 'sage' | 'jade' | 'ink';
|
|
6
|
+
variant: 'soft' | 'solid';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getPostBadges(post: BlogPost): PostBadge[] {
|
|
10
|
+
const badges: PostBadge[] = [];
|
|
11
|
+
badges.push({ text: 'Standard.site', color: 'jade', variant: 'solid' });
|
|
12
|
+
if (post.publicationName) {
|
|
13
|
+
badges.push({ text: post.publicationName, color: 'jade', variant: 'soft' });
|
|
14
|
+
}
|
|
15
|
+
return badges;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getBadgeClasses(badge: PostBadge): string {
|
|
19
|
+
const baseStyle =
|
|
20
|
+
badge.variant === 'soft'
|
|
21
|
+
? 'px-2 py-0.5 text-xs font-medium rounded'
|
|
22
|
+
: 'px-2 py-0.5 text-xs font-semibold uppercase rounded';
|
|
23
|
+
|
|
24
|
+
const colorClasses = {
|
|
25
|
+
mint:
|
|
26
|
+
badge.variant === 'soft'
|
|
27
|
+
? 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-200'
|
|
28
|
+
: 'bg-secondary-500 text-white dark:bg-secondary-600',
|
|
29
|
+
sage:
|
|
30
|
+
badge.variant === 'soft'
|
|
31
|
+
? 'bg-primary-100 text-primary-800 dark:bg-primary-900 dark:text-primary-200'
|
|
32
|
+
: 'bg-primary-500 text-white dark:bg-primary-600',
|
|
33
|
+
jade:
|
|
34
|
+
badge.variant === 'soft'
|
|
35
|
+
? 'bg-accent-100 text-accent-800 dark:bg-accent-900 dark:text-accent-200'
|
|
36
|
+
: 'bg-accent-500 text-white dark:bg-accent-600',
|
|
37
|
+
ink:
|
|
38
|
+
badge.variant === 'soft'
|
|
39
|
+
? 'bg-ink-100 text-ink-800 dark:bg-ink-800 dark:text-ink-100'
|
|
40
|
+
: 'bg-ink-700 text-white dark:bg-ink-300 dark:text-ink-900'
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return `${baseStyle} ${colorClasses[badge.color]}`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { BlogPost } from '@ewanc26/atproto';
|
|
2
|
+
import { getUserLocale } from '../utils/locale.js';
|
|
3
|
+
|
|
4
|
+
export interface MonthData {
|
|
5
|
+
monthName: string;
|
|
6
|
+
posts: BlogPost[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type GroupedPosts = Map<number, Map<number, MonthData>>;
|
|
10
|
+
|
|
11
|
+
export function filterPosts(posts: BlogPost[], query: string): BlogPost[] {
|
|
12
|
+
if (!query.trim()) return posts;
|
|
13
|
+
const lowerQuery = query.toLowerCase();
|
|
14
|
+
return posts.filter((post) => {
|
|
15
|
+
const titleMatch = post.title.toLowerCase().includes(lowerQuery);
|
|
16
|
+
const descMatch = post.description?.toLowerCase().includes(lowerQuery);
|
|
17
|
+
const platformMatch = post.platform.toLowerCase().includes(lowerQuery);
|
|
18
|
+
const pubMatch = post.publicationName?.toLowerCase().includes(lowerQuery);
|
|
19
|
+
const tagsMatch = post.tags?.some((tag: string) => tag.toLowerCase().includes(lowerQuery));
|
|
20
|
+
return titleMatch || descMatch || platformMatch || pubMatch || tagsMatch;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function groupPostsByDate(posts: BlogPost[], locale?: string): GroupedPosts {
|
|
25
|
+
const userLocale = locale || getUserLocale();
|
|
26
|
+
const grouped: GroupedPosts = new Map();
|
|
27
|
+
|
|
28
|
+
posts.forEach((post) => {
|
|
29
|
+
const date = new Date(post.createdAt);
|
|
30
|
+
const year = date.getFullYear();
|
|
31
|
+
const month = date.getMonth();
|
|
32
|
+
const monthName = date.toLocaleString(userLocale, { month: 'long' });
|
|
33
|
+
|
|
34
|
+
if (!grouped.has(year)) grouped.set(year, new Map());
|
|
35
|
+
const yearGroup = grouped.get(year)!;
|
|
36
|
+
if (!yearGroup.has(month)) yearGroup.set(month, { monthName, posts: [] });
|
|
37
|
+
yearGroup.get(month)!.posts.push(post);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
grouped.forEach((yearGroup) => {
|
|
41
|
+
yearGroup.forEach((monthData) => {
|
|
42
|
+
monthData.posts.sort(
|
|
43
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return grouped;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getSortedMonths(yearGroup: Map<number, MonthData>): [number, MonthData][] {
|
|
52
|
+
return Array.from(yearGroup.entries()).sort((a, b) => b[0] - a[0]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getSortedYears(groupedPosts: GroupedPosts): number[] {
|
|
56
|
+
return Array.from(groupedPosts.keys()).sort((a, b) => b - a);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getAllTags(items: Array<{ tags?: string[] }>): string[] {
|
|
60
|
+
const tagsSet = new Set<string>();
|
|
61
|
+
items.forEach((item) => item.tags?.forEach((tag) => tagsSet.add(tag.toLowerCase())));
|
|
62
|
+
return Array.from(tagsSet).sort();
|
|
63
|
+
}
|
package/src/lib/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// ─── Stores ───────────────────────────────────────────────────────────────────
|
|
2
|
+
export { wolfMode, colorThemeDropdownOpen, happyMacStore, colorTheme } from './stores/index.js';
|
|
3
|
+
export type { ColorTheme } from './stores/index.js';
|
|
4
|
+
|
|
5
|
+
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
6
|
+
export { THEMES, DEFAULT_THEME, CATEGORY_LABELS, getThemesByCategory, getTheme } from './config/themes.config.js';
|
|
7
|
+
export type { ThemeDefinition } from './config/themes.config.js';
|
|
8
|
+
|
|
9
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
10
|
+
export type { SiteMetadata, NavItem } from './types/index.js';
|
|
11
|
+
|
|
12
|
+
// ─── Helper ───────────────────────────────────────────────────────────────────
|
|
13
|
+
export { filterPosts, groupPostsByDate, getSortedMonths, getSortedYears, getAllTags } from './helper/posts.js';
|
|
14
|
+
export type { MonthData, GroupedPosts } from './helper/posts.js';
|
|
15
|
+
export { getPostBadges, getBadgeClasses } from './helper/badges.js';
|
|
16
|
+
export type { PostBadge } from './helper/badges.js';
|
|
17
|
+
|
|
18
|
+
// ─── Layout toggles ───────────────────────────────────────────────────────────
|
|
19
|
+
export { default as ThemeToggle } from './components/layout/ThemeToggle.svelte';
|
|
20
|
+
export { default as WolfToggle } from './components/layout/WolfToggle.svelte';
|
|
21
|
+
|
|
22
|
+
// ─── Layout main ──────────────────────────────────────────────────────────────
|
|
23
|
+
export { DynamicLinks, ScrollToTop, TangledRepos } from './components/layout/main/index.js';
|
|
24
|
+
|
|
25
|
+
// ─── Cards ────────────────────────────────────────────────────────────────────
|
|
26
|
+
export { LinkCard, ProfileCard, PostCard, BlueskyPostCard, TangledRepoCard, MusicStatusCard, KibunStatusCard } from './components/layout/main/card/index.js';
|
|
27
|
+
|
|
28
|
+
// ─── SEO ──────────────────────────────────────────────────────────────────────
|
|
29
|
+
export { MetaTags } from './components/seo/index.js';
|
|
30
|
+
|
|
31
|
+
// ─── UI primitives ────────────────────────────────────────────────────────────
|
|
32
|
+
export { Card, InternalCard, Dropdown, Pagination, SearchBar, Tabs, PostsGroupedView, DocumentCard, BlogPostCard } from './components/ui/index.js';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
import { DEFAULT_THEME, type ColorTheme } from '../config/themes.config.js';
|
|
3
|
+
|
|
4
|
+
const browser = typeof window !== 'undefined';
|
|
5
|
+
|
|
6
|
+
interface ColorThemeState {
|
|
7
|
+
current: ColorTheme;
|
|
8
|
+
mounted: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const STORAGE_KEY = 'color-theme';
|
|
12
|
+
|
|
13
|
+
function createColorThemeStore() {
|
|
14
|
+
const { subscribe, set, update } = writable<ColorThemeState>({
|
|
15
|
+
current: DEFAULT_THEME,
|
|
16
|
+
mounted: false
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
subscribe,
|
|
21
|
+
init: () => {
|
|
22
|
+
if (!browser) return;
|
|
23
|
+
const stored = localStorage.getItem(STORAGE_KEY) as ColorTheme | null;
|
|
24
|
+
const theme = stored || DEFAULT_THEME;
|
|
25
|
+
update((state) => ({ ...state, current: theme, mounted: true }));
|
|
26
|
+
const currentTheme = document.documentElement.getAttribute('data-color-theme');
|
|
27
|
+
if (currentTheme !== theme) applyTheme(theme);
|
|
28
|
+
},
|
|
29
|
+
setTheme: (theme: ColorTheme) => {
|
|
30
|
+
if (!browser) return;
|
|
31
|
+
localStorage.setItem(STORAGE_KEY, theme);
|
|
32
|
+
update((state) => ({ ...state, current: theme }));
|
|
33
|
+
applyTheme(theme);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function applyTheme(theme: ColorTheme) {
|
|
39
|
+
if (!browser) return;
|
|
40
|
+
document.documentElement.setAttribute('data-color-theme', theme);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const colorTheme = createColorThemeStore();
|
|
44
|
+
export type { ColorTheme };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
|
|
3
|
+
interface HappyMacState {
|
|
4
|
+
clickCount: number;
|
|
5
|
+
isTriggered: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createHappyMacStore() {
|
|
9
|
+
const { subscribe, set, update } = writable<HappyMacState>({
|
|
10
|
+
clickCount: 0,
|
|
11
|
+
isTriggered: false
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
subscribe,
|
|
16
|
+
incrementClick: () =>
|
|
17
|
+
update((state) => {
|
|
18
|
+
const newCount = state.clickCount + 1;
|
|
19
|
+
if (newCount === 24) {
|
|
20
|
+
return { clickCount: newCount, isTriggered: true };
|
|
21
|
+
}
|
|
22
|
+
return { ...state, clickCount: newCount };
|
|
23
|
+
}),
|
|
24
|
+
reset: () => set({ clickCount: 0, isTriggered: false })
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const happyMacStore = createHappyMacStore();
|