@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.
Files changed (154) hide show
  1. package/README.md +65 -0
  2. package/dist/components/layout/ThemeToggle.svelte +50 -0
  3. package/dist/components/layout/ThemeToggle.svelte.d.ts +4 -0
  4. package/dist/components/layout/ThemeToggle.svelte.d.ts.map +1 -0
  5. package/dist/components/layout/WolfToggle.svelte +19 -0
  6. package/dist/components/layout/WolfToggle.svelte.d.ts +4 -0
  7. package/dist/components/layout/WolfToggle.svelte.d.ts.map +1 -0
  8. package/dist/components/layout/index.d.ts +5 -0
  9. package/dist/components/layout/index.d.ts.map +1 -0
  10. package/dist/components/layout/index.js +4 -0
  11. package/dist/components/layout/main/DynamicLinks.svelte +63 -0
  12. package/dist/components/layout/main/DynamicLinks.svelte.d.ts +7 -0
  13. package/dist/components/layout/main/DynamicLinks.svelte.d.ts.map +1 -0
  14. package/dist/components/layout/main/ScrollToTop.svelte +36 -0
  15. package/dist/components/layout/main/ScrollToTop.svelte.d.ts +4 -0
  16. package/dist/components/layout/main/ScrollToTop.svelte.d.ts.map +1 -0
  17. package/dist/components/layout/main/card/BlueskyPostCard.svelte +261 -0
  18. package/dist/components/layout/main/card/BlueskyPostCard.svelte.d.ts +9 -0
  19. package/dist/components/layout/main/card/BlueskyPostCard.svelte.d.ts.map +1 -0
  20. package/dist/components/layout/main/card/KibunStatusCard.svelte +48 -0
  21. package/dist/components/layout/main/card/KibunStatusCard.svelte.d.ts +8 -0
  22. package/dist/components/layout/main/card/KibunStatusCard.svelte.d.ts.map +1 -0
  23. package/dist/components/layout/main/card/LinkCard.svelte +63 -0
  24. package/dist/components/layout/main/card/LinkCard.svelte.d.ts +17 -0
  25. package/dist/components/layout/main/card/LinkCard.svelte.d.ts.map +1 -0
  26. package/dist/components/layout/main/card/MusicStatusCard.svelte +101 -0
  27. package/dist/components/layout/main/card/MusicStatusCard.svelte.d.ts +8 -0
  28. package/dist/components/layout/main/card/MusicStatusCard.svelte.d.ts.map +1 -0
  29. package/dist/components/layout/main/card/PostCard.svelte +46 -0
  30. package/dist/components/layout/main/card/PostCard.svelte.d.ts +8 -0
  31. package/dist/components/layout/main/card/PostCard.svelte.d.ts.map +1 -0
  32. package/dist/components/layout/main/card/ProfileCard.svelte +70 -0
  33. package/dist/components/layout/main/card/ProfileCard.svelte.d.ts +8 -0
  34. package/dist/components/layout/main/card/ProfileCard.svelte.d.ts.map +1 -0
  35. package/dist/components/layout/main/card/TangledRepoCard.svelte +80 -0
  36. package/dist/components/layout/main/card/TangledRepoCard.svelte.d.ts +11 -0
  37. package/dist/components/layout/main/card/TangledRepoCard.svelte.d.ts.map +1 -0
  38. package/dist/components/layout/main/card/index.d.ts +8 -0
  39. package/dist/components/layout/main/card/index.d.ts.map +1 -0
  40. package/dist/components/layout/main/card/index.js +7 -0
  41. package/dist/components/layout/main/index.d.ts +4 -0
  42. package/dist/components/layout/main/index.d.ts.map +1 -0
  43. package/dist/components/layout/main/index.js +3 -0
  44. package/dist/components/seo/MetaTags.svelte +39 -0
  45. package/dist/components/seo/MetaTags.svelte.d.ts +9 -0
  46. package/dist/components/seo/MetaTags.svelte.d.ts.map +1 -0
  47. package/dist/components/seo/index.d.ts +2 -0
  48. package/dist/components/seo/index.d.ts.map +1 -0
  49. package/dist/components/seo/index.js +1 -0
  50. package/dist/components/ui/BlogPostCard.svelte +44 -0
  51. package/dist/components/ui/BlogPostCard.svelte.d.ts +9 -0
  52. package/dist/components/ui/BlogPostCard.svelte.d.ts.map +1 -0
  53. package/dist/components/ui/Card.svelte +144 -0
  54. package/dist/components/ui/Card.svelte.d.ts +27 -0
  55. package/dist/components/ui/Card.svelte.d.ts.map +1 -0
  56. package/dist/components/ui/DocumentCard.svelte +42 -0
  57. package/dist/components/ui/DocumentCard.svelte.d.ts +9 -0
  58. package/dist/components/ui/DocumentCard.svelte.d.ts.map +1 -0
  59. package/dist/components/ui/Dropdown.svelte +36 -0
  60. package/dist/components/ui/Dropdown.svelte.d.ts +15 -0
  61. package/dist/components/ui/Dropdown.svelte.d.ts.map +1 -0
  62. package/dist/components/ui/InternalCard.svelte +41 -0
  63. package/dist/components/ui/InternalCard.svelte.d.ts +14 -0
  64. package/dist/components/ui/InternalCard.svelte.d.ts.map +1 -0
  65. package/dist/components/ui/Pagination.svelte +74 -0
  66. package/dist/components/ui/Pagination.svelte.d.ts +11 -0
  67. package/dist/components/ui/Pagination.svelte.d.ts.map +1 -0
  68. package/dist/components/ui/PostsGroupedView.svelte +40 -0
  69. package/dist/components/ui/PostsGroupedView.svelte.d.ts +10 -0
  70. package/dist/components/ui/PostsGroupedView.svelte.d.ts.map +1 -0
  71. package/dist/components/ui/SearchBar.svelte +26 -0
  72. package/dist/components/ui/SearchBar.svelte.d.ts +9 -0
  73. package/dist/components/ui/SearchBar.svelte.d.ts.map +1 -0
  74. package/dist/components/ui/Tabs.svelte +25 -0
  75. package/dist/components/ui/Tabs.svelte.d.ts +13 -0
  76. package/dist/components/ui/Tabs.svelte.d.ts.map +1 -0
  77. package/dist/components/ui/index.d.ts +11 -0
  78. package/dist/components/ui/index.d.ts.map +1 -0
  79. package/dist/components/ui/index.js +10 -0
  80. package/dist/config/themes.config.d.ts +23 -0
  81. package/dist/config/themes.config.d.ts.map +1 -0
  82. package/dist/config/themes.config.js +116 -0
  83. package/dist/helper/badges.d.ts +9 -0
  84. package/dist/helper/badges.d.ts.map +1 -0
  85. package/dist/helper/badges.js +28 -0
  86. package/dist/helper/posts.d.ts +14 -0
  87. package/dist/helper/posts.d.ts.map +1 -0
  88. package/dist/helper/posts.js +47 -0
  89. package/dist/index.d.ts +16 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +18 -0
  92. package/dist/stores/colorTheme.d.ts +12 -0
  93. package/dist/stores/colorTheme.d.ts.map +1 -0
  94. package/dist/stores/colorTheme.js +36 -0
  95. package/dist/stores/dropdownState.d.ts +2 -0
  96. package/dist/stores/dropdownState.d.ts.map +1 -0
  97. package/dist/stores/dropdownState.js +2 -0
  98. package/dist/stores/happyMac.d.ts +11 -0
  99. package/dist/stores/happyMac.d.ts.map +1 -0
  100. package/dist/stores/happyMac.js +19 -0
  101. package/dist/stores/index.d.ts +6 -0
  102. package/dist/stores/index.d.ts.map +1 -0
  103. package/dist/stores/index.js +4 -0
  104. package/dist/stores/wolfMode.d.ts +7 -0
  105. package/dist/stores/wolfMode.d.ts.map +1 -0
  106. package/dist/stores/wolfMode.js +130 -0
  107. package/dist/types/index.d.ts +19 -0
  108. package/dist/types/index.d.ts.map +1 -0
  109. package/dist/types/index.js +1 -0
  110. package/dist/utils/formatNumber.d.ts +3 -0
  111. package/dist/utils/formatNumber.d.ts.map +1 -0
  112. package/dist/utils/formatNumber.js +26 -0
  113. package/dist/utils/locale.d.ts +4 -0
  114. package/dist/utils/locale.d.ts.map +1 -0
  115. package/dist/utils/locale.js +32 -0
  116. package/package.json +45 -0
  117. package/src/lib/components/layout/ThemeToggle.svelte +50 -0
  118. package/src/lib/components/layout/WolfToggle.svelte +19 -0
  119. package/src/lib/components/layout/index.ts +4 -0
  120. package/src/lib/components/layout/main/DynamicLinks.svelte +63 -0
  121. package/src/lib/components/layout/main/ScrollToTop.svelte +36 -0
  122. package/src/lib/components/layout/main/card/BlueskyPostCard.svelte +261 -0
  123. package/src/lib/components/layout/main/card/KibunStatusCard.svelte +48 -0
  124. package/src/lib/components/layout/main/card/LinkCard.svelte +63 -0
  125. package/src/lib/components/layout/main/card/MusicStatusCard.svelte +101 -0
  126. package/src/lib/components/layout/main/card/PostCard.svelte +46 -0
  127. package/src/lib/components/layout/main/card/ProfileCard.svelte +70 -0
  128. package/src/lib/components/layout/main/card/TangledRepoCard.svelte +80 -0
  129. package/src/lib/components/layout/main/card/index.ts +7 -0
  130. package/src/lib/components/layout/main/index.ts +3 -0
  131. package/src/lib/components/seo/MetaTags.svelte +39 -0
  132. package/src/lib/components/seo/index.ts +1 -0
  133. package/src/lib/components/ui/BlogPostCard.svelte +44 -0
  134. package/src/lib/components/ui/Card.svelte +144 -0
  135. package/src/lib/components/ui/DocumentCard.svelte +42 -0
  136. package/src/lib/components/ui/Dropdown.svelte +36 -0
  137. package/src/lib/components/ui/InternalCard.svelte +41 -0
  138. package/src/lib/components/ui/Pagination.svelte +74 -0
  139. package/src/lib/components/ui/PostsGroupedView.svelte +40 -0
  140. package/src/lib/components/ui/SearchBar.svelte +26 -0
  141. package/src/lib/components/ui/Tabs.svelte +25 -0
  142. package/src/lib/components/ui/index.ts +10 -0
  143. package/src/lib/config/themes.config.ts +130 -0
  144. package/src/lib/helper/badges.ts +44 -0
  145. package/src/lib/helper/posts.ts +63 -0
  146. package/src/lib/index.ts +32 -0
  147. package/src/lib/stores/colorTheme.ts +44 -0
  148. package/src/lib/stores/dropdownState.ts +3 -0
  149. package/src/lib/stores/happyMac.ts +28 -0
  150. package/src/lib/stores/index.ts +5 -0
  151. package/src/lib/stores/wolfMode.ts +127 -0
  152. package/src/lib/types/index.ts +19 -0
  153. package/src/lib/utils/formatNumber.ts +27 -0
  154. package/src/lib/utils/locale.ts +29 -0
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # @ewanc26/ui
2
+
3
+ Svelte UI component library extracted from [ewancroft.uk](https://ewancroft.uk). Provides layout and card components, UI primitives, SEO helpers, Svelte stores, post utilities, and a multi-theme configuration system.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @ewanc26/ui
9
+ ```
10
+
11
+ Peer dependencies: `svelte >= 5`, `@sveltejs/kit >= 2`, `tailwindcss >= 4`. Optional: `@ewanc26/atproto` (for AT Protocol card components).
12
+
13
+ ## What's Exported
14
+
15
+ ### Components
16
+
17
+ | Group | Components |
18
+ |-------|------------|
19
+ | Layout toggles | `ThemeToggle`, `WolfToggle` |
20
+ | Layout main | `DynamicLinks`, `ScrollToTop` |
21
+ | Cards | `ProfileCard`, `PostCard`, `BlueskyPostCard`, `LinkCard`, `MusicStatusCard`, `KibunStatusCard`, `TangledRepoCard` |
22
+ | UI primitives | `Card`, `InternalCard`, `Dropdown`, `Pagination`, `SearchBar`, `Tabs`, `PostsGroupedView`, `DocumentCard`, `BlogPostCard` |
23
+ | SEO | `MetaTags` |
24
+
25
+ ### Stores
26
+
27
+ | Store | Type | Description |
28
+ |-------|------|-------------|
29
+ | `wolfMode` | `Writable<boolean>` | Wolf mode text transformation |
30
+ | `colorTheme` | `Writable<ColorTheme>` | Active colour theme |
31
+ | `colorThemeDropdownOpen` | `Writable<boolean>` | Theme picker open state |
32
+ | `happyMacStore` | `Writable<boolean>` | Happy Mac easter egg |
33
+
34
+ ### Theme Configuration
35
+
36
+ 12 themes across four categories (neutral, warm, cool, vibrant) using OKLCH colour values. Default: `slate`.
37
+
38
+ ```typescript
39
+ import { THEMES, DEFAULT_THEME, getTheme, getThemesByCategory, CATEGORY_LABELS } from '@ewanc26/ui';
40
+ ```
41
+
42
+ ### Helpers
43
+
44
+ ```typescript
45
+ import { getPostBadges, getBadgeClasses } from '@ewanc26/ui';
46
+ import { filterPosts, groupPostsByDate, getSortedMonths, getSortedYears, getAllTags } from '@ewanc26/ui';
47
+ ```
48
+
49
+ ### Types
50
+
51
+ ```typescript
52
+ import type { SiteMetadata, NavItem, ColorTheme, ThemeDefinition, PostBadge, MonthData, GroupedPosts } from '@ewanc26/ui';
53
+ ```
54
+
55
+ ## Build
56
+
57
+ ```bash
58
+ pnpm build # svelte-package
59
+ pnpm dev # svelte-package --watch
60
+ pnpm check # svelte-check
61
+ ```
62
+
63
+ ## Licence
64
+
65
+ See the root [LICENSE](../../LICENSE).
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { Sun, Moon } from '@lucide/svelte';
4
+
5
+ let isDark = $state(false);
6
+ let mounted = $state(false);
7
+
8
+ onMount(() => {
9
+ const stored = localStorage.getItem('theme');
10
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
11
+ isDark = stored === 'light' || (!stored && !prefersDark);
12
+ updateTheme();
13
+ mounted = true;
14
+
15
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
16
+ const handleChange = (e: MediaQueryListEvent) => {
17
+ if (!localStorage.getItem('theme')) { isDark = e.matches; updateTheme(); }
18
+ };
19
+ mediaQuery.addEventListener('change', handleChange);
20
+ return () => mediaQuery.removeEventListener('change', handleChange);
21
+ });
22
+
23
+ function updateTheme() {
24
+ const html = document.documentElement;
25
+ if (isDark) { html.classList.remove('dark'); html.style.colorScheme = 'light'; }
26
+ else { html.classList.add('dark'); html.style.colorScheme = 'dark'; }
27
+ }
28
+
29
+ function toggleTheme() {
30
+ isDark = !isDark;
31
+ localStorage.setItem('theme', isDark ? 'light' : 'dark');
32
+ updateTheme();
33
+ }
34
+ </script>
35
+
36
+ <button
37
+ onclick={toggleTheme}
38
+ class="relative flex h-10 w-10 items-center justify-center rounded-lg bg-canvas-200 text-ink-900 transition-all hover:bg-canvas-300 dark:bg-canvas-800 dark:text-ink-50 dark:hover:bg-canvas-700"
39
+ aria-label={isDark ? 'Switch to dark mode' : 'Switch to light mode'}
40
+ type="button"
41
+ >
42
+ {#if mounted}
43
+ <div class="relative h-5 w-5">
44
+ <Moon class="absolute inset-0 h-5 w-5 transition-all duration-300 {isDark ? 'scale-100 rotate-0 opacity-100' : 'scale-0 rotate-90 opacity-0'}" aria-hidden="true" />
45
+ <Sun class="absolute inset-0 h-5 w-5 transition-all duration-300 {isDark ? 'scale-0 -rotate-90 opacity-0' : 'scale-100 rotate-0 opacity-100'}" aria-hidden="true" />
46
+ </div>
47
+ {:else}
48
+ <div class="h-5 w-5 animate-pulse rounded bg-canvas-300 dark:bg-canvas-700"></div>
49
+ {/if}
50
+ </button>
@@ -0,0 +1,4 @@
1
+ declare const ThemeToggle: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ThemeToggle = ReturnType<typeof ThemeToggle>;
3
+ export default ThemeToggle;
4
+ //# sourceMappingURL=ThemeToggle.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeToggle.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/layout/ThemeToggle.svelte.ts"],"names":[],"mappings":"AAsDA,QAAA,MAAM,WAAW,2DAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import { wolfMode } from '../../stores/wolfMode.js';
3
+
4
+ let isWolfMode = $state(false);
5
+ $effect(() => {
6
+ const unsubscribe = wolfMode.subscribe((value) => { isWolfMode = value; });
7
+ return unsubscribe;
8
+ });
9
+ </script>
10
+
11
+ <button
12
+ onclick={() => wolfMode.toggle()}
13
+ class="relative flex h-10 w-10 items-center justify-center rounded-lg bg-canvas-200 text-ink-900 transition-all hover:bg-canvas-300 dark:bg-canvas-800 dark:text-ink-50 dark:hover:bg-canvas-700"
14
+ aria-label={isWolfMode ? 'Disable wolf mode' : 'Enable wolf mode'}
15
+ type="button"
16
+ title={isWolfMode ? 'Return to normal text' : 'Transform to wolf speak - awoo!'}
17
+ >
18
+ <span class="text-2xl transition-transform duration-300 {isWolfMode ? 'scale-125' : 'scale-100'}" aria-hidden="true">🐺</span>
19
+ </button>
@@ -0,0 +1,4 @@
1
+ declare const WolfToggle: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type WolfToggle = ReturnType<typeof WolfToggle>;
3
+ export default WolfToggle;
4
+ //# sourceMappingURL=WolfToggle.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WolfToggle.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/layout/WolfToggle.svelte.ts"],"names":[],"mappings":"AAqBA,QAAA,MAAM,UAAU,2DAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { default as ThemeToggle } from './ThemeToggle.svelte';
2
+ export { default as WolfToggle } from './WolfToggle.svelte';
3
+ export { DynamicLinks, ScrollToTop, TangledRepos } from './main/index.js';
4
+ export { LinkCard, ProfileCard, PostCard, BlueskyPostCard, TangledRepoCard, MusicStatusCard, KibunStatusCard } from './main/card/index.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/layout/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { default as ThemeToggle } from './ThemeToggle.svelte';
2
+ export { default as WolfToggle } from './WolfToggle.svelte';
3
+ export { DynamicLinks, ScrollToTop, TangledRepos } from './main/index.js';
4
+ export { LinkCard, ProfileCard, PostCard, BlueskyPostCard, TangledRepoCard, MusicStatusCard, KibunStatusCard } from './main/card/index.js';
@@ -0,0 +1,63 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import Card from '../../ui/Card.svelte';
4
+ import LinkCard from './card/LinkCard.svelte';
5
+ import { fetchLinks, type LinkData } from '@ewanc26/atproto';
6
+
7
+ interface Props { did: string; }
8
+ let { did }: Props = $props();
9
+
10
+ let links = $state<LinkData | null>(null);
11
+ let loading = $state(true);
12
+ let error = $state<string | null>(null);
13
+
14
+ onMount(async () => {
15
+ try {
16
+ links = await fetchLinks(did);
17
+ } catch (err) {
18
+ error = err instanceof Error ? err.message : 'Failed to load links';
19
+ } finally {
20
+ loading = false;
21
+ }
22
+ });
23
+ </script>
24
+
25
+ <div class="mx-auto w-full max-w-2xl">
26
+ {#if loading}
27
+ <Card loading={true} variant="elevated" padding="md">
28
+ {#snippet skeleton()}
29
+ <div class="mb-4 h-6 w-20 rounded bg-canvas-300 dark:bg-canvas-700"></div>
30
+ <div class="grid gap-3 sm:grid-cols-2">
31
+ {#each Array(4) as _}
32
+ <div class="rounded-lg bg-canvas-200 p-4 dark:bg-canvas-800">
33
+ <div class="h-5 w-3/4 rounded bg-canvas-300 dark:bg-canvas-700 mb-2"></div>
34
+ <div class="h-4 w-1/2 rounded bg-canvas-300 dark:bg-canvas-700"></div>
35
+ </div>
36
+ {/each}
37
+ </div>
38
+ {/snippet}
39
+ </Card>
40
+ {:else if error}
41
+ <Card error={true} errorMessage={error} />
42
+ {:else if links && links.cards.length > 0}
43
+ {@const safeLinks = links}
44
+ <Card variant="elevated" padding="md">
45
+ {#snippet children()}
46
+ <h2 class="mb-4 text-2xl font-bold text-ink-900 dark:text-ink-50">Links</h2>
47
+ <div class="grid gap-3 sm:grid-cols-2">
48
+ {#each safeLinks.cards as link}
49
+ <LinkCard url={link.url} title={link.text} emoji={link.emoji} />
50
+ {/each}
51
+ </div>
52
+ {/snippet}
53
+ </Card>
54
+ {:else}
55
+ <Card variant="flat" padding="lg">
56
+ {#snippet children()}
57
+ <div class="text-center">
58
+ <p class="text-ink-700 dark:text-ink-300">No links available. Create a <code class="rounded bg-canvas-200 px-1 dark:bg-canvas-800">blue.linkat.board</code> record at <a href="https://linkat.blue/" class="text-primary-600 hover:underline dark:text-primary-400" target="_blank" rel="noopener noreferrer">https://linkat.blue/</a></p>
59
+ </div>
60
+ {/snippet}
61
+ </Card>
62
+ {/if}
63
+ </div>
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ did: string;
3
+ }
4
+ declare const DynamicLinks: import("svelte").Component<Props, {}, "">;
5
+ type DynamicLinks = ReturnType<typeof DynamicLinks>;
6
+ export default DynamicLinks;
7
+ //# sourceMappingURL=DynamicLinks.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DynamicLinks.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/layout/main/DynamicLinks.svelte.ts"],"names":[],"mappings":"AASC,UAAU,KAAK;IAAG,GAAG,EAAE,MAAM,CAAC;CAAE;AAkEjC,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { ChevronUp } from '@lucide/svelte';
4
+
5
+ let scrollY = $state(0);
6
+ let isVisible = $derived(scrollY > 300);
7
+
8
+ function scrollToTop() {
9
+ window.scrollTo({ top: 0, behavior: 'smooth' });
10
+ }
11
+
12
+ onMount(() => {
13
+ const updateScrollY = () => (scrollY = window.scrollY);
14
+ window.addEventListener('scroll', updateScrollY, { passive: true });
15
+ return () => window.removeEventListener('scroll', updateScrollY);
16
+ });
17
+ </script>
18
+
19
+ <svelte:window bind:scrollY />
20
+
21
+ <div
22
+ class="fixed bottom-8 left-8 z-50 transition-opacity duration-300 motion-reduce:transition-none sm:bottom-6 sm:left-6"
23
+ class:opacity-100={isVisible}
24
+ class:opacity-0={!isVisible}
25
+ >
26
+ <button
27
+ onclick={scrollToTop}
28
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); scrollToTop(); } }}
29
+ aria-label="Scroll to top"
30
+ title="Scroll to top"
31
+ type="button"
32
+ class="flex h-12 w-12 items-center justify-center rounded-full border border-primary-200 bg-canvas-100 text-ink-900 shadow-lg transition-all duration-300 ease-out hover:-translate-y-0.5 hover:bg-primary-500 hover:text-ink-50 hover:shadow-xl focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:outline-none motion-reduce:transition-none motion-reduce:hover:translate-y-0 sm:h-11 sm:w-11 dark:border-primary-800 dark:bg-canvas-900 dark:text-ink-50 dark:hover:bg-primary-600"
33
+ >
34
+ <ChevronUp width="20" height="20" aria-hidden="true" />
35
+ </button>
36
+ </div>
@@ -0,0 +1,4 @@
1
+ declare const ScrollToTop: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ScrollToTop = ReturnType<typeof ScrollToTop>;
3
+ export default ScrollToTop;
4
+ //# sourceMappingURL=ScrollToTop.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScrollToTop.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/layout/main/ScrollToTop.svelte.ts"],"names":[],"mappings":"AAkCA,QAAA,MAAM,WAAW,2DAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,261 @@
1
+ <script lang="ts">
2
+ import Card from '../../../ui/Card.svelte';
3
+ import { fetchLatestBlueskyPost, type BlueskyPost } from '@ewanc26/atproto';
4
+ import { formatRelativeTime } from '../../../../utils/locale.js';
5
+ import { formatCompactNumber } from '../../../../utils/formatNumber.js';
6
+ import { Heart, Repeat2, MessageCircle, ExternalLink, X } from '@lucide/svelte';
7
+ import Hls from 'hls.js';
8
+
9
+ interface Props {
10
+ did: string;
11
+ post?: BlueskyPost | null;
12
+ }
13
+
14
+ let { did, post: initialPost = null }: Props = $props();
15
+
16
+ let post = $state<BlueskyPost | null>(null);
17
+ let loading = $state(false);
18
+ let error = $state<string | null>(null);
19
+ let lightboxImage = $state<{ url: string; alt: string } | null>(null);
20
+ let videoElements = new Map<string, { element: HTMLVideoElement; hls: Hls | null }>();
21
+
22
+ const locale = typeof navigator !== 'undefined' ? navigator.language || 'en-GB' : 'en-GB';
23
+ const POLL_INTERVAL = 2 * 60 * 1000;
24
+
25
+ async function loadPost() {
26
+ try {
27
+ const newPost = await fetchLatestBlueskyPost(did);
28
+ if (newPost && (!post || newPost.uri !== post.uri)) post = newPost;
29
+ } catch (err) {
30
+ error = err instanceof Error ? err.message : 'Failed to load latest post';
31
+ } finally {
32
+ loading = false;
33
+ }
34
+ }
35
+
36
+ $effect(() => {
37
+ if (initialPost && !post) post = initialPost;
38
+ if (!post) { loading = true; loadPost(); }
39
+ const pollInterval = setInterval(() => loadPost(), POLL_INTERVAL);
40
+ return () => {
41
+ clearInterval(pollInterval);
42
+ videoElements.forEach(({ hls }) => { if (hls) hls.destroy(); });
43
+ videoElements.clear();
44
+ };
45
+ });
46
+
47
+ function getPostUrl(uri: string): string {
48
+ const parts = uri.split('/');
49
+ return `https://witchsky.app/profile/${parts[2]}/post/${parts[4]}`;
50
+ }
51
+
52
+ function getProfileUrl(handle: string): string {
53
+ return `https://witchsky.app/profile/${handle}`;
54
+ }
55
+
56
+ function openLightbox(url: string, alt: string) {
57
+ lightboxImage = { url, alt };
58
+ document.body.style.overflow = 'hidden';
59
+ }
60
+
61
+ function closeLightbox() {
62
+ lightboxImage = null;
63
+ document.body.style.overflow = '';
64
+ }
65
+
66
+ function escapeHtml(text: string): string {
67
+ const div = document.createElement('div');
68
+ div.textContent = text;
69
+ return div.innerHTML;
70
+ }
71
+
72
+ function renderRichText(text: string, facets?: any[]): string {
73
+ if (!facets?.length) return escapeHtml(text);
74
+ const sortedFacets = [...facets].sort((a, b) => a.index.byteStart - b.index.byteStart);
75
+ const encoder = new TextEncoder();
76
+ const decoder = new TextDecoder();
77
+ const bytes = encoder.encode(text);
78
+ let result = '';
79
+ let lastByteIndex = 0;
80
+ for (const facet of sortedFacets) {
81
+ const { byteStart, byteEnd } = facet.index;
82
+ if (lastByteIndex < byteStart) result += escapeHtml(decoder.decode(bytes.slice(lastByteIndex, byteStart)));
83
+ const facetText = decoder.decode(bytes.slice(byteStart, byteEnd));
84
+ const feature = facet.features?.[0];
85
+ if (feature) {
86
+ if (feature.$type === 'app.bsky.richtext.facet#link') {
87
+ result += `<a href="${escapeHtml(feature.uri)}" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 underline">${escapeHtml(facetText)}</a>`;
88
+ } else if (feature.$type === 'app.bsky.richtext.facet#mention') {
89
+ result += `<a href="https://witchsky.app/profile/${escapeHtml(feature.did)}" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 font-medium">${escapeHtml(facetText)}</a>`;
90
+ } else if (feature.$type === 'app.bsky.richtext.facet#tag') {
91
+ result += `<a href="https://witchsky.app/hashtag/${escapeHtml(feature.tag)}" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 font-medium">${escapeHtml(facetText)}</a>`;
92
+ } else { result += escapeHtml(facetText); }
93
+ } else { result += escapeHtml(facetText); }
94
+ lastByteIndex = byteEnd;
95
+ }
96
+ if (lastByteIndex < bytes.length) result += escapeHtml(new TextDecoder().decode(bytes.slice(lastByteIndex)));
97
+ return result;
98
+ }
99
+
100
+ function setupVideo(videoElement: HTMLVideoElement, videoUrl: string) {
101
+ if (!videoElement || !videoUrl) return;
102
+ const existing = videoElements.get(videoUrl);
103
+ if (existing?.hls) existing.hls.destroy();
104
+ let hls: Hls | null = null;
105
+ if (videoUrl.includes('.m3u8')) {
106
+ if (Hls.isSupported()) {
107
+ hls = new Hls({ enableSoftwareAES: true, maxBufferLength: 30, maxMaxBufferLength: 600 });
108
+ hls.loadSource(videoUrl);
109
+ hls.attachMedia(videoElement);
110
+ videoElements.set(videoUrl, { element: videoElement, hls });
111
+ } else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
112
+ videoElement.src = videoUrl;
113
+ videoElements.set(videoUrl, { element: videoElement, hls: null });
114
+ }
115
+ } else {
116
+ videoElement.src = videoUrl;
117
+ videoElements.set(videoUrl, { element: videoElement, hls: null });
118
+ }
119
+ return { destroy() { if (hls) hls.destroy(); videoElements.delete(videoUrl); } };
120
+ }
121
+ </script>
122
+
123
+ {#snippet postContent(postData: BlueskyPost, depth: number = 0, isReplyParent: boolean = false)}
124
+ <div>
125
+ <div class="relative flex gap-3">
126
+ <a href={getProfileUrl(postData.author.handle)} target="_blank" rel="noopener noreferrer" class="shrink-0 transition-opacity hover:opacity-80">
127
+ {#if postData.author.avatar}
128
+ <img src={postData.author.avatar} alt={postData.author.displayName || postData.author.handle} class="h-{isReplyParent ? '8' : '10'} w-{isReplyParent ? '8' : '10'} rounded-full object-cover sm:h-{isReplyParent ? '10' : '12'} sm:w-{isReplyParent ? '10' : '12'}" loading="lazy" />
129
+ {:else}
130
+ <div class="flex h-10 w-10 items-center justify-center rounded-full bg-primary-200 dark:bg-primary-800">
131
+ <span class="text-base font-semibold text-primary-700 dark:text-primary-300">{(postData.author.displayName || postData.author.handle).charAt(0).toUpperCase()}</span>
132
+ </div>
133
+ {/if}
134
+ </a>
135
+ <div class="min-w-0 flex-1">
136
+ <a href={getProfileUrl(postData.author.handle)} target="_blank" rel="noopener noreferrer" class="mb-2 inline-block transition-opacity hover:opacity-80">
137
+ <div class="flex flex-col">
138
+ <span class="text-base leading-tight font-semibold text-ink-900 dark:text-ink-50">{postData.author.displayName || postData.author.handle}</span>
139
+ <span class="text-xs leading-tight text-ink-600 dark:text-ink-400">@{postData.author.handle}</span>
140
+ </div>
141
+ </a>
142
+ <div class="overflow-wrap-anywhere mb-3 text-base leading-relaxed wrap-break-word whitespace-pre-wrap text-ink-900 dark:text-ink-50">
143
+ {@html renderRichText(postData.text, postData.facets)}
144
+ </div>
145
+ {#if postData.hasVideo && postData.videoUrl}
146
+ <div class="mb-3 max-w-full overflow-hidden rounded-xl border border-canvas-300 bg-black dark:border-canvas-700">
147
+ <video use:setupVideo={postData.videoUrl} controls class="w-full max-w-full" preload="metadata" poster={postData.videoThumbnail} playsinline>
148
+ <track kind="captions" />
149
+ </video>
150
+ </div>
151
+ {/if}
152
+ {#if postData.hasImages && postData.imageUrls?.length}
153
+ <div class="mb-3 grid max-w-full gap-1 {postData.imageUrls.length === 1 ? 'grid-cols-1' : postData.imageUrls.length === 2 ? 'grid-cols-2' : postData.imageUrls.length === 3 ? 'grid-cols-3' : 'grid-cols-2'}">
154
+ {#each postData.imageUrls as imageUrl, index}
155
+ <button type="button" onclick={() => openLightbox(imageUrl, postData.imageAlts?.[index] || `Post attachment ${index + 1}`)} class="h-auto w-full max-w-full overflow-hidden rounded-lg border border-canvas-300 transition-opacity hover:opacity-90 dark:border-canvas-700">
156
+ <img src={imageUrl} alt={postData.imageAlts?.[index] || `Post attachment ${index + 1}`} class="h-auto w-full max-w-full object-cover {postData.imageUrls!.length > 1 ? 'aspect-video' : 'max-h-96'}" loading="lazy" />
157
+ </button>
158
+ {/each}
159
+ </div>
160
+ {/if}
161
+ {#if postData.externalLink}
162
+ <a href={postData.externalLink.uri} target="_blank" rel="noopener noreferrer" class="mb-3 flex max-w-full flex-col overflow-hidden rounded-xl border border-canvas-300 bg-canvas-200 transition-colors hover:bg-canvas-300 dark:border-canvas-700 dark:bg-canvas-800 dark:hover:bg-canvas-700">
163
+ {#if postData.externalLink.thumb}<img src={postData.externalLink.thumb} alt={postData.externalLink.title} class="h-48 w-full max-w-full object-cover" loading="lazy" />{/if}
164
+ <div class="p-3">
165
+ <h3 class="overflow-wrap-anywhere mb-1 line-clamp-2 text-sm font-semibold wrap-break-word text-ink-900 dark:text-ink-50">{postData.externalLink.title}</h3>
166
+ {#if postData.externalLink.description}<p class="overflow-wrap-anywhere mb-2 line-clamp-2 text-xs wrap-break-word text-ink-700 dark:text-ink-300">{postData.externalLink.description}</p>{/if}
167
+ <p class="overflow-wrap-anywhere text-xs wrap-break-word text-ink-600 dark:text-ink-400">{new URL(postData.externalLink.uri).hostname}</p>
168
+ </div>
169
+ </a>
170
+ {/if}
171
+ {#if postData.quotedPost && depth < 3}
172
+ <div class="mb-3 rounded-xl border border-canvas-300 bg-canvas-200 p-3 dark:border-canvas-700 dark:bg-canvas-800">
173
+ {@render postContent(postData.quotedPost, depth + 1, depth === 0)}
174
+ </div>
175
+ {/if}
176
+ {#if !isReplyParent}
177
+ <div class="flex flex-wrap items-center gap-3 pt-1 text-xs sm:gap-6 sm:text-sm">
178
+ {#if postData.replyCount !== undefined}<div class="flex items-center gap-1 text-ink-600 dark:text-ink-400"><MessageCircle class="h-3.5 w-3.5" aria-hidden="true" /><span class="font-medium">{formatCompactNumber(postData.replyCount, locale)}</span></div>{/if}
179
+ {#if postData.repostCount !== undefined}<div class="flex items-center gap-1 text-ink-600 dark:text-ink-400"><Repeat2 class="h-3.5 w-3.5" aria-hidden="true" /><span class="font-medium">{formatCompactNumber(postData.repostCount, locale)}</span></div>{/if}
180
+ {#if postData.likeCount !== undefined}<div class="flex items-center gap-1 text-ink-600 dark:text-ink-400"><Heart class="h-3.5 w-3.5" aria-hidden="true" /><span class="font-medium">{formatCompactNumber(postData.likeCount, locale)}</span></div>{/if}
181
+ <time datetime={postData.createdAt} class="ml-auto text-xs font-medium text-ink-700 dark:text-ink-300">{formatRelativeTime(postData.createdAt)}</time>
182
+ </div>
183
+ {/if}
184
+ </div>
185
+ </div>
186
+ </div>
187
+ {/snippet}
188
+
189
+ <div class="mx-auto w-full max-w-2xl">
190
+ {#if loading}
191
+ <Card loading={true} variant="elevated" padding="md">
192
+ {#snippet skeleton()}
193
+ <div class="mb-3 flex items-center gap-3">
194
+ <div class="h-12 w-12 rounded-full bg-canvas-300 dark:bg-canvas-700"></div>
195
+ <div class="flex-1 space-y-2">
196
+ <div class="h-4 w-32 rounded bg-canvas-300 dark:bg-canvas-700"></div>
197
+ <div class="h-3 w-24 rounded bg-canvas-300 dark:bg-canvas-700"></div>
198
+ </div>
199
+ </div>
200
+ <div class="mb-3 space-y-2">
201
+ <div class="h-4 w-full rounded bg-canvas-300 dark:bg-canvas-700"></div>
202
+ <div class="h-4 w-5/6 rounded bg-canvas-300 dark:bg-canvas-700"></div>
203
+ </div>
204
+ {/snippet}
205
+ </Card>
206
+ {:else if error}
207
+ <Card error={true} errorMessage={error} />
208
+ {:else if post}
209
+ <article class="rounded-xl bg-canvas-100 p-6 shadow-lg transition-all duration-300 hover:shadow-xl dark:bg-canvas-900">
210
+ <div class="mb-4 flex items-start justify-between gap-2 sm:items-center">
211
+ <div class="flex min-w-0 flex-col gap-1 sm:flex-row sm:items-center sm:gap-2">
212
+ <span class="text-xs font-semibold tracking-wide whitespace-nowrap text-ink-700 uppercase dark:text-ink-300">Latest Bluesky Post</span>
213
+ {#if post.isRepost && post.repostAuthor}
214
+ <div class="flex items-center gap-1.5 text-xs text-ink-600 dark:text-ink-400">
215
+ <Repeat2 class="h-3 w-3 shrink-0" aria-hidden="true" />
216
+ <a href={getProfileUrl(post.repostAuthor.handle)} target="_blank" rel="noopener noreferrer" class="truncate font-medium text-primary-600 transition-colors hover:text-primary-700 dark:text-primary-400">
217
+ {post.repostAuthor.displayName || post.repostAuthor.handle}
218
+ </a>
219
+ <span class="whitespace-nowrap">reposted</span>
220
+ </div>
221
+ {:else if post.replyParent}
222
+ <div class="flex items-center gap-1.5 text-xs text-ink-600 dark:text-ink-400">
223
+ <MessageCircle class="h-3 w-3 shrink-0" aria-hidden="true" />
224
+ <span class="whitespace-nowrap">Replying to</span>
225
+ <a href={getProfileUrl(post.replyParent.author.handle)} target="_blank" rel="noopener noreferrer" class="truncate font-medium text-primary-600 dark:text-primary-400">@{post.replyParent.author.handle}</a>
226
+ </div>
227
+ {/if}
228
+ </div>
229
+ <a href={getPostUrl(post.uri)} target="_blank" rel="noopener noreferrer" class="shrink-0 text-ink-600 transition-colors hover:text-primary-600 dark:text-ink-400 dark:hover:text-primary-400" aria-label="View post on Bluesky">
230
+ <ExternalLink class="h-4 w-4" aria-hidden="true" />
231
+ </a>
232
+ </div>
233
+ {#if post.replyParent}
234
+ <div class="mb-4 rounded-xl border border-canvas-300 bg-canvas-200 p-3 dark:border-canvas-700 dark:bg-canvas-800">
235
+ {@render postContent(post.replyParent, 0, true)}
236
+ </div>
237
+ {/if}
238
+ {@render postContent(post, 0, false)}
239
+ </article>
240
+ {:else}
241
+ <Card variant="flat" padding="lg">
242
+ {#snippet children()}
243
+ <div class="text-center"><p class="text-ink-700 dark:text-ink-300">No posts found</p></div>
244
+ {/snippet}
245
+ </Card>
246
+ {/if}
247
+ </div>
248
+
249
+ {#if lightboxImage}
250
+ <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/90 p-4" onclick={closeLightbox} onkeydown={(e) => e.key === 'Escape' && closeLightbox()} role="button" tabindex="0" aria-label="Close image lightbox">
251
+ <button type="button" onclick={closeLightbox} class="absolute top-4 right-4 z-10 rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70" aria-label="Close">
252
+ <X class="h-6 w-6" />
253
+ </button>
254
+ <div class="relative flex max-h-[90vh] w-full max-w-[90vw] flex-col items-center">
255
+ <img src={lightboxImage.url} alt={lightboxImage.alt} class="max-h-[80vh] w-full object-contain" loading="lazy" />
256
+ {#if lightboxImage.alt}
257
+ <div class="mt-4 w-full rounded-lg bg-black/70 px-4 py-2 text-center text-sm text-white">{lightboxImage.alt}</div>
258
+ {/if}
259
+ </div>
260
+ </div>
261
+ {/if}
@@ -0,0 +1,9 @@
1
+ import { type BlueskyPost } from '@ewanc26/atproto';
2
+ interface Props {
3
+ did: string;
4
+ post?: BlueskyPost | null;
5
+ }
6
+ declare const BlueskyPostCard: import("svelte").Component<Props, {}, "">;
7
+ type BlueskyPostCard = ReturnType<typeof BlueskyPostCard>;
8
+ export default BlueskyPostCard;
9
+ //# sourceMappingURL=BlueskyPostCard.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BlueskyPostCard.svelte.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/layout/main/card/BlueskyPostCard.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAA0B,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAO3E,UAAU,KAAK;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CAC1B;AAqQF,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
@@ -0,0 +1,48 @@
1
+ <script lang="ts">
2
+ import Card from '../../../ui/Card.svelte';
3
+ import type { KibunStatusData } from '@ewanc26/atproto';
4
+ import { formatRelativeTime } from '../../../../utils/locale.js';
5
+ import { Heart } from '@lucide/svelte';
6
+
7
+ interface Props { kibunStatus?: KibunStatusData | null; }
8
+ let { kibunStatus = null }: Props = $props();
9
+ </script>
10
+
11
+ <div class="mx-auto w-full max-w-2xl">
12
+ {#if !kibunStatus}
13
+ <Card loading={true} variant="elevated" padding="md">
14
+ {#snippet skeleton()}
15
+ <div class="mb-3">
16
+ <div class="mb-2 flex items-center gap-2">
17
+ <div class="h-4 w-4 rounded bg-canvas-300 dark:bg-canvas-700"></div>
18
+ <div class="h-3 w-24 rounded bg-canvas-300 dark:bg-canvas-700"></div>
19
+ </div>
20
+ <div class="mb-2 flex items-center gap-3">
21
+ <div class="h-12 w-12 rounded bg-canvas-300 dark:bg-canvas-700"></div>
22
+ <div class="h-6 w-48 rounded bg-canvas-300 dark:bg-canvas-700"></div>
23
+ </div>
24
+ <div class="h-3 w-32 rounded bg-canvas-300 dark:bg-canvas-700"></div>
25
+ </div>
26
+ {/snippet}
27
+ </Card>
28
+ {:else}
29
+ {@const s = kibunStatus}
30
+ <Card variant="elevated" padding="md">
31
+ {#snippet children()}
32
+ <div>
33
+ <div class="mb-4 flex items-center gap-2">
34
+ <Heart class="h-4 w-4 text-primary-600 dark:text-primary-400" aria-hidden="true" />
35
+ <span class="text-xs font-semibold tracking-wide text-ink-800 uppercase dark:text-ink-100">Current Mood</span>
36
+ </div>
37
+ <div class="mb-4 flex items-center gap-3">
38
+ <div class="flex h-12 w-12 items-center justify-center rounded-lg bg-canvas-100 text-3xl dark:bg-canvas-800">{s.emoji}</div>
39
+ <p class="flex-1 text-lg font-medium wrap-break-word whitespace-normal text-ink-900 dark:text-ink-50">{s.text}</p>
40
+ </div>
41
+ <div class="text-xs text-ink-700 dark:text-ink-200">
42
+ <time datetime={s.createdAt}>{formatRelativeTime(s.createdAt)}</time>
43
+ </div>
44
+ </div>
45
+ {/snippet}
46
+ </Card>
47
+ {/if}
48
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { KibunStatusData } from '@ewanc26/atproto';
2
+ interface Props {
3
+ kibunStatus?: KibunStatusData | null;
4
+ }
5
+ declare const KibunStatusCard: import("svelte").Component<Props, {}, "">;
6
+ type KibunStatusCard = ReturnType<typeof KibunStatusCard>;
7
+ export default KibunStatusCard;
8
+ //# sourceMappingURL=KibunStatusCard.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"KibunStatusCard.svelte.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/layout/main/card/KibunStatusCard.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAKvD,UAAU,KAAK;IAAG,WAAW,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CAAE;AAmD1D,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}