@aureuma/svelta 0.0.1

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 (115) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +14 -0
  3. package/.changeset/publish-blogkit.md +5 -0
  4. package/.github/workflows/release.yml +65 -0
  5. package/LICENSE +22 -0
  6. package/README.md +35 -0
  7. package/docs/mintlify-blog-study.md +697 -0
  8. package/package.json +59 -0
  9. package/packages/blogkit/CHANGELOG.md +6 -0
  10. package/packages/blogkit/LICENSE +22 -0
  11. package/packages/blogkit/README.md +93 -0
  12. package/packages/blogkit/dist/components/blog/Avatar.svelte +15 -0
  13. package/packages/blogkit/dist/components/blog/Avatar.svelte.d.ts +22 -0
  14. package/packages/blogkit/dist/components/blog/BackLink.svelte +23 -0
  15. package/packages/blogkit/dist/components/blog/BackLink.svelte.d.ts +21 -0
  16. package/packages/blogkit/dist/components/blog/BlogCard.svelte +37 -0
  17. package/packages/blogkit/dist/components/blog/BlogCard.svelte.d.ts +22 -0
  18. package/packages/blogkit/dist/components/blog/BlogHeroCard.svelte +36 -0
  19. package/packages/blogkit/dist/components/blog/BlogHeroCard.svelte.d.ts +21 -0
  20. package/packages/blogkit/dist/components/blog/Container.svelte +8 -0
  21. package/packages/blogkit/dist/components/blog/Container.svelte.d.ts +29 -0
  22. package/packages/blogkit/dist/components/blog/ImageLightbox.svelte +58 -0
  23. package/packages/blogkit/dist/components/blog/ImageLightbox.svelte.d.ts +24 -0
  24. package/packages/blogkit/dist/components/blog/MorePosts.svelte +15 -0
  25. package/packages/blogkit/dist/components/blog/MorePosts.svelte.d.ts +21 -0
  26. package/packages/blogkit/dist/components/blog/ShareButtons.svelte +113 -0
  27. package/packages/blogkit/dist/components/blog/ShareButtons.svelte.d.ts +23 -0
  28. package/packages/blogkit/dist/components/blog/SummaryCard.svelte +11 -0
  29. package/packages/blogkit/dist/components/blog/SummaryCard.svelte.d.ts +20 -0
  30. package/packages/blogkit/dist/components/blog/TagTabs.svelte +32 -0
  31. package/packages/blogkit/dist/components/blog/TagTabs.svelte.d.ts +23 -0
  32. package/packages/blogkit/dist/index.d.ts +11 -0
  33. package/packages/blogkit/dist/index.js +11 -0
  34. package/packages/blogkit/dist/server/blog.d.ts +39 -0
  35. package/packages/blogkit/dist/server/blog.js +222 -0
  36. package/packages/blogkit/dist/server/index.d.ts +1 -0
  37. package/packages/blogkit/dist/server/index.js +1 -0
  38. package/packages/blogkit/dist/theme/ThemeSwitcher.svelte +34 -0
  39. package/packages/blogkit/dist/theme/ThemeSwitcher.svelte.d.ts +21 -0
  40. package/packages/blogkit/dist/theme/index.d.ts +2 -0
  41. package/packages/blogkit/dist/theme/index.js +2 -0
  42. package/packages/blogkit/dist/theme/store.d.ts +12 -0
  43. package/packages/blogkit/dist/theme/store.js +50 -0
  44. package/packages/blogkit/dist/types/blog.d.ts +31 -0
  45. package/packages/blogkit/dist/types/blog.js +1 -0
  46. package/packages/blogkit/package.json +66 -0
  47. package/packages/blogkit/src/lib/components/blog/Avatar.svelte +15 -0
  48. package/packages/blogkit/src/lib/components/blog/BackLink.svelte +23 -0
  49. package/packages/blogkit/src/lib/components/blog/BlogCard.svelte +37 -0
  50. package/packages/blogkit/src/lib/components/blog/BlogHeroCard.svelte +36 -0
  51. package/packages/blogkit/src/lib/components/blog/Container.svelte +8 -0
  52. package/packages/blogkit/src/lib/components/blog/ImageLightbox.svelte +58 -0
  53. package/packages/blogkit/src/lib/components/blog/MorePosts.svelte +15 -0
  54. package/packages/blogkit/src/lib/components/blog/ShareButtons.svelte +113 -0
  55. package/packages/blogkit/src/lib/components/blog/SummaryCard.svelte +11 -0
  56. package/packages/blogkit/src/lib/components/blog/TagTabs.svelte +32 -0
  57. package/packages/blogkit/src/lib/index.ts +15 -0
  58. package/packages/blogkit/src/lib/server/blog.ts +264 -0
  59. package/packages/blogkit/src/lib/server/index.ts +2 -0
  60. package/packages/blogkit/src/lib/theme/ThemeSwitcher.svelte +34 -0
  61. package/packages/blogkit/src/lib/theme/index.ts +3 -0
  62. package/packages/blogkit/src/lib/theme/store.ts +64 -0
  63. package/packages/blogkit/src/lib/types/blog.ts +36 -0
  64. package/packages/blogkit/svelte.config.js +8 -0
  65. package/packages/blogkit/tsconfig.json +5 -0
  66. package/playwright.config.ts +24 -0
  67. package/postcss.config.cjs +6 -0
  68. package/src/app.css +146 -0
  69. package/src/app.d.ts +13 -0
  70. package/src/app.html +26 -0
  71. package/src/content/blog/ai-summary-cards-with-frontmatter.md +32 -0
  72. package/src/content/blog/announcing-svelta-blog.md +19 -0
  73. package/src/content/blog/best-practices-ship-with-checklists.md +26 -0
  74. package/src/content/blog/building-a-mintlify-inspired-blog.md +49 -0
  75. package/src/content/blog/design-tokens-that-scale.md +47 -0
  76. package/src/content/blog/for-founders-why-speed-matters.md +23 -0
  77. package/src/content/blog/infinite-scroll-with-intersection-observer.md +37 -0
  78. package/src/content/blog/markdown-kitchen-sink.md +101 -0
  79. package/src/content/blog/markdown-pipeline-mdsvex-shiki.md +39 -0
  80. package/src/content/blog/rss-feeds-that-actually-work.md +25 -0
  81. package/src/content/blog/tag-tabs-and-mobile-fade-masks.md +25 -0
  82. package/src/lib/assets/favicon.svg +1 -0
  83. package/src/lib/components/site/SiteFooter.svelte +24 -0
  84. package/src/lib/components/site/SiteHeader.svelte +36 -0
  85. package/src/lib/content/authors.ts +28 -0
  86. package/src/lib/index.ts +1 -0
  87. package/src/lib/server/blog.ts +22 -0
  88. package/src/lib/server/rss.ts +58 -0
  89. package/src/lib/server/seo.ts +31 -0
  90. package/src/lib/stores/theme.ts +10 -0
  91. package/src/lib/types/blog.ts +1 -0
  92. package/src/routes/+layout.svelte +31 -0
  93. package/src/routes/+page.svelte +44 -0
  94. package/src/routes/blog/+page.server.ts +28 -0
  95. package/src/routes/blog/+page.svelte +122 -0
  96. package/src/routes/blog/[slug]/+page.server.ts +39 -0
  97. package/src/routes/blog/[slug]/+page.svelte +118 -0
  98. package/src/routes/blog/posts.json/+server.ts +32 -0
  99. package/src/routes/feed.xml/+server.ts +21 -0
  100. package/static/blog/authors/alex.svg +13 -0
  101. package/static/blog/authors/maria.svg +13 -0
  102. package/static/blog/authors/shawn.svg +13 -0
  103. package/static/blog/covers/ai-summary.svg +38 -0
  104. package/static/blog/covers/design-tokens.svg +37 -0
  105. package/static/blog/covers/infinite-scroll.svg +38 -0
  106. package/static/blog/covers/kitchen-sink.svg +36 -0
  107. package/static/blog/covers/markdown-pipeline.svg +41 -0
  108. package/static/blog/covers/mintlify-style.svg +35 -0
  109. package/static/blog/covers/rss.svg +34 -0
  110. package/static/robots.txt +3 -0
  111. package/svelte.config.js +70 -0
  112. package/tailwind.config.cjs +133 -0
  113. package/tests/blog.spec.ts +63 -0
  114. package/tsconfig.json +21 -0
  115. package/vite.config.ts +14 -0
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@aureuma/svelta",
3
+ "private": false,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "workspaces": [
7
+ "packages/*"
8
+ ],
9
+ "scripts": {
10
+ "dev": "vite dev",
11
+ "build": "vite build",
12
+ "preview": "vite preview",
13
+ "prepare": "svelte-kit sync || echo ''",
14
+ "postinstall": "npm -w @aureuma/blogkit run build",
15
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
16
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
17
+ "test:e2e": "playwright test",
18
+ "test": "npm run check && npm run test:e2e",
19
+ "changeset": "changeset",
20
+ "version-packages": "changeset version",
21
+ "release": "changeset publish"
22
+ },
23
+ "devDependencies": {
24
+ "@changesets/changelog-github": "^0.5.2",
25
+ "@changesets/cli": "^2.29.8",
26
+ "@playwright/test": "^1.58.2",
27
+ "@shikijs/rehype": "^3.22.0",
28
+ "@sveltejs/adapter-auto": "^7.0.0",
29
+ "@sveltejs/kit": "^2.50.2",
30
+ "@sveltejs/package": "^2.5.7",
31
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
32
+ "@tailwindcss/postcss": "^4.1.18",
33
+ "@tailwindcss/typography": "^0.5.19",
34
+ "@types/node": "^25.2.2",
35
+ "autoprefixer": "^10.4.24",
36
+ "mdsvex": "^0.12.6",
37
+ "postcss": "^8.5.6",
38
+ "rehype-autolink-headings": "^7.1.0",
39
+ "rehype-slug": "^6.0.0",
40
+ "remark-gfm": "^4.0.1",
41
+ "shiki": "^3.22.0",
42
+ "svelte": "^5.49.2",
43
+ "svelte-check": "^4.3.6",
44
+ "svelte2tsx": "^0.7.47",
45
+ "tailwindcss": "^4.1.18",
46
+ "typescript": "^5.9.3",
47
+ "vite": "^7.3.1"
48
+ },
49
+ "dependencies": {
50
+ "@fontsource/geist-mono": "^5.2.7",
51
+ "@fontsource/inter": "^5.2.8",
52
+ "gray-matter": "^4.0.3",
53
+ "reading-time": "^1.5.0",
54
+ "zod": "^4.3.6"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ }
59
+ }
@@ -0,0 +1,6 @@
1
+ # @aureuma/blogkit Changelog
2
+
3
+ All notable changes to this package will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aureuma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,93 @@
1
+ # @aureuma/blogkit
2
+
3
+ A Mintlify-inspired blog UI + content pipeline helpers for SvelteKit + mdsvex.
4
+
5
+ This repo keeps `svelta` as a demo app, and exposes the reusable parts as a local workspace package under `packages/blogkit`.
6
+
7
+ ## What you get
8
+
9
+ - Blog UI components: hero card, post cards, tag tabs, share buttons, summary card, more-posts grid, image lightbox.
10
+ - Server helpers: `createBlog()` to load Markdown posts (frontmatter, excerpt, reading-time, categories, hero).
11
+ - Theme helper: `createThemeController()` and a simple theme switcher component.
12
+
13
+ ### Compatibility
14
+
15
+ - Component source is compatible with Svelte 4 and Svelte 5 (no runes required).
16
+ - If you already have an existing frontmatter schema, use `mapFrontmatter` to adapt it to BlogKit's expected fields.
17
+
18
+ ## Use In Another Repo
19
+
20
+ ### Install from npm
21
+
22
+ ```sh
23
+ npm i @aureuma/blogkit
24
+ ```
25
+
26
+ ### Option A: mono-repo / workspaces (recommended)
27
+
28
+ 1. Add this repo as a submodule (or subtree) inside your project:
29
+
30
+ ```sh
31
+ git submodule add git@github.com:Aureuma/svelta.git packages/svelta
32
+ ```
33
+
34
+ 2. Point your workspace config at the package:
35
+
36
+ ```json
37
+ {
38
+ "workspaces": ["packages/*", "packages/svelta/packages/*"]
39
+ }
40
+ ```
41
+
42
+ 3. Import and use:
43
+
44
+ ```ts
45
+ import { BlogCard } from '@aureuma/blogkit';
46
+ import { createBlog } from '@aureuma/blogkit/server';
47
+ ```
48
+
49
+ ### Option B: publish to npm
50
+
51
+ 1. Set `"private": false` in `packages/blogkit/package.json`.
52
+ 2. From `packages/blogkit`, run `npm publish`.
53
+
54
+ ## Integration Steps (SvelteKit)
55
+
56
+ 1. mdsvex: configure Markdown compilation (`.md`) and Shiki/anchors (see this repo's `svelte.config.js`).
57
+ 2. Content:
58
+ - posts: `src/content/blog/*.md` with YAML frontmatter
59
+ - assets: `static/blog/covers/*` and `static/blog/authors/*`
60
+ 3. Blog loader glue:
61
+
62
+ ```ts
63
+ // src/lib/server/blog.ts
64
+ import { createBlog } from '@aureuma/blogkit/server';
65
+ import { getAuthor } from '$lib/content/authors';
66
+ import type { BlogPostFull } from '@aureuma/blogkit';
67
+
68
+ type CompiledModule = { default: BlogPostFull['component'] };
69
+
70
+ const compiledModules = import.meta.glob('/src/content/blog/*.md') as Record<
71
+ string,
72
+ () => Promise<CompiledModule>
73
+ >;
74
+ const rawModules = import.meta.glob('/src/content/blog/*.md', {
75
+ query: '?raw',
76
+ import: 'default'
77
+ }) as Record<string, () => Promise<string>>;
78
+
79
+ export const blog = createBlog({ compiledModules, rawModules, getAuthor });
80
+ export const { getAllPosts, getPostBySlug, getCategories, pickHero } = blog;
81
+ ```
82
+
83
+ 4. Tailwind: ensure your `content` globs include the package so class names are not purged.
84
+ - If installed from npm: include `./node_modules/@aureuma/blogkit/dist/**/*`.
85
+
86
+ 5. Routes: copy the blog routes from this repo:
87
+ - `src/routes/blog/*`
88
+ - `src/routes/feed.xml/*`
89
+
90
+ ## Notes
91
+
92
+ - `createBlog()` builds the index from raw Markdown only (fast), and imports the compiled mdsvex component only for individual post pages.
93
+ - This package is intended for SvelteKit. UI uses Tailwind utility classes and expects your app to provide the CSS variables/tokens.
@@ -0,0 +1,15 @@
1
+ <script lang="ts">
2
+ export let src: string;
3
+ export let alt: string;
4
+ export let size: number = 48;
5
+ </script>
6
+
7
+ <img
8
+ src={src}
9
+ alt={alt}
10
+ width={size}
11
+ height={size}
12
+ class="shrink-0 rounded-full border border-border-soft/10 bg-background-soft object-cover"
13
+ style="width: {size}px; height: {size}px;"
14
+ loading="lazy"
15
+ />
@@ -0,0 +1,22 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: Props & {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const Avatar: $$__sveltets_2_IsomorphicComponent<{
15
+ src: string;
16
+ alt: string;
17
+ size?: number;
18
+ }, {
19
+ [evt: string]: CustomEvent<any>;
20
+ }, {}, {}, string>;
21
+ type Avatar = InstanceType<typeof Avatar>;
22
+ export default Avatar;
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ export let href: string = '/blog';
3
+ export let label: string = 'Back to blog';
4
+ </script>
5
+
6
+ <a
7
+ href={href}
8
+ class="inline-flex items-center gap-2 text-xs font-mono uppercase tracking-[0.6px] text-text-sub transition hover:text-text-main"
9
+ >
10
+ <svg
11
+ class="size-4"
12
+ viewBox="0 0 24 24"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ stroke-width="2"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ aria-hidden="true"
19
+ >
20
+ <path d="M15 18l-6-6 6-6" />
21
+ </svg>
22
+ <span>{label}</span>
23
+ </a>
@@ -0,0 +1,21 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: Props & {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const BackLink: $$__sveltets_2_IsomorphicComponent<{
15
+ href?: string;
16
+ label?: string;
17
+ }, {
18
+ [evt: string]: CustomEvent<any>;
19
+ }, {}, {}, string>;
20
+ type BackLink = InstanceType<typeof BackLink>;
21
+ export default BackLink;
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ import type { BlogPost } from '../../types/blog';
3
+ import Avatar from './Avatar.svelte';
4
+
5
+ export let post: BlogPost;
6
+ export let variant: 'default' | 'suggestion' = 'default';
7
+ $: thumbHeight = variant === 'suggestion' ? 'h-[190px]' : 'h-[280px]';
8
+ </script>
9
+
10
+ <a href={`/blog/${post.slug}`} class="group block" data-testid="blog-card">
11
+ <div class="relative overflow-hidden rounded-2xl border border-border-soft/10 bg-background-soft {thumbHeight}">
12
+ <img
13
+ src={post.cover}
14
+ alt={post.title}
15
+ class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.02]"
16
+ loading="lazy"
17
+ />
18
+ </div>
19
+
20
+ <div class="mt-4">
21
+ <p class="text-xs font-mono uppercase tracking-[0.6px] text-brand">{post.category.label}</p>
22
+ <h3
23
+ class="mt-1 text-xl font-medium leading-[30px] tracking-tight underline-offset-[6px] decoration-border-soft/30 group-hover:underline"
24
+ >
25
+ {post.title}
26
+ </h3>
27
+ <p class="mt-2 text-sm leading-6 text-text-sub">{post.excerpt}</p>
28
+
29
+ <div class="mt-4 flex items-center gap-3">
30
+ <Avatar src={post.author.avatar} alt={post.author.name} size={24} />
31
+ <div class="leading-tight">
32
+ <div class="text-sm font-medium tracking-tight text-text-main">{post.author.name}</div>
33
+ <div class="text-xs text-text-muted">{post.author.title}</div>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </a>
@@ -0,0 +1,22 @@
1
+ import type { BlogPost } from '../../types/blog';
2
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
+ $$bindings?: Bindings;
5
+ } & Exports;
6
+ (internal: unknown, props: Props & {
7
+ $$events?: Events;
8
+ $$slots?: Slots;
9
+ }): Exports & {
10
+ $set?: any;
11
+ $on?: any;
12
+ };
13
+ z_$$bindings?: Bindings;
14
+ }
15
+ declare const BlogCard: $$__sveltets_2_IsomorphicComponent<{
16
+ post: BlogPost;
17
+ variant?: "default" | "suggestion";
18
+ }, {
19
+ [evt: string]: CustomEvent<any>;
20
+ }, {}, {}, string>;
21
+ type BlogCard = InstanceType<typeof BlogCard>;
22
+ export default BlogCard;
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import type { BlogPost } from '../../types/blog';
3
+
4
+ export let post: BlogPost;
5
+ </script>
6
+
7
+ <a
8
+ href={`/blog/${post.slug}`}
9
+ data-testid="blog-hero"
10
+ class="group relative my-12 block h-[480px] overflow-hidden rounded-3xl shadow-drop-md"
11
+ >
12
+ <img
13
+ src={post.cover}
14
+ alt={post.title}
15
+ class="absolute inset-0 h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.02]"
16
+ loading="eager"
17
+ />
18
+
19
+ <div class="absolute inset-0 bg-gradient-to-t from-black/65 via-black/25 to-black/0"></div>
20
+
21
+ <div class="relative flex h-full flex-col justify-end p-8">
22
+ <p class="text-xs font-mono uppercase tracking-[0.6px] text-brand">{post.category.label}</p>
23
+ <h2
24
+ class="mt-2 max-w-3xl text-3xl font-semibold leading-tight tracking-[-0.8px] text-white md:text-4xl"
25
+ >
26
+ {post.title}
27
+ </h2>
28
+ <p class="mt-3 max-w-2xl text-base leading-6 text-white/80">{post.excerpt}</p>
29
+
30
+ <div class="mt-6 flex items-center gap-2 text-xs font-mono uppercase tracking-[0.6px] text-white/70">
31
+ <span>{post.dateShort}</span>
32
+ <span class="text-white/35" aria-hidden="true">•</span>
33
+ <span>{post.readingTimeShort}</span>
34
+ </div>
35
+ </div>
36
+ </a>
@@ -0,0 +1,21 @@
1
+ import type { BlogPost } from '../../types/blog';
2
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
+ $$bindings?: Bindings;
5
+ } & Exports;
6
+ (internal: unknown, props: Props & {
7
+ $$events?: Events;
8
+ $$slots?: Slots;
9
+ }): Exports & {
10
+ $set?: any;
11
+ $on?: any;
12
+ };
13
+ z_$$bindings?: Bindings;
14
+ }
15
+ declare const BlogHeroCard: $$__sveltets_2_IsomorphicComponent<{
16
+ post: BlogPost;
17
+ }, {
18
+ [evt: string]: CustomEvent<any>;
19
+ }, {}, {}, string>;
20
+ type BlogHeroCard = InstanceType<typeof BlogHeroCard>;
21
+ export default BlogHeroCard;
@@ -0,0 +1,8 @@
1
+ <script lang="ts">
2
+ export let size: '4xl' | '5xl' = '5xl';
3
+ $: max = size === '4xl' ? 'max-w-4xl' : 'max-w-5xl';
4
+ </script>
5
+
6
+ <div class="mx-auto w-full {max} px-6">
7
+ <slot />
8
+ </div>
@@ -0,0 +1,29 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: Props & {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ type $$__sveltets_2_PropsWithChildren<Props, Slots> = Props & (Slots extends {
15
+ default: any;
16
+ } ? Props extends Record<string, never> ? any : {
17
+ children?: any;
18
+ } : {});
19
+ declare const Container: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_PropsWithChildren<{
20
+ size?: "4xl" | "5xl";
21
+ }, {
22
+ default: {};
23
+ }>, {
24
+ [evt: string]: CustomEvent<any>;
25
+ }, {
26
+ default: {};
27
+ }, {}, string>;
28
+ type Container = InstanceType<typeof Container>;
29
+ export default Container;
@@ -0,0 +1,58 @@
1
+ <script lang="ts">
2
+ import { BROWSER } from 'esm-env';
3
+ import { onDestroy, onMount, tick } from 'svelte';
4
+
5
+ export let image: { src: string; alt: string } | null;
6
+ export let onClose: () => void;
7
+
8
+ let dialogEl: HTMLDivElement | null = null;
9
+
10
+ $: if (BROWSER) {
11
+ // Prevent background scroll while open.
12
+ document.body.style.overflow = image ? 'hidden' : '';
13
+ }
14
+
15
+ $: if (BROWSER && image) {
16
+ // Ensure the dialog is focusable for Escape-to-close and accessibility.
17
+ void (async () => {
18
+ await tick();
19
+ dialogEl?.focus();
20
+ })();
21
+ }
22
+
23
+ onMount(() => {
24
+ if (!BROWSER) return;
25
+ const onKeyDown = (e: KeyboardEvent) => {
26
+ if (e.key === 'Escape') onClose();
27
+ };
28
+ window.addEventListener('keydown', onKeyDown);
29
+ return () => window.removeEventListener('keydown', onKeyDown);
30
+ });
31
+
32
+ onDestroy(() => {
33
+ if (!BROWSER) return;
34
+ document.body.style.overflow = '';
35
+ });
36
+ </script>
37
+
38
+ {#if image}
39
+ <div
40
+ bind:this={dialogEl}
41
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/75 p-6"
42
+ role="dialog"
43
+ aria-modal="true"
44
+ tabindex="-1"
45
+ >
46
+ <button
47
+ type="button"
48
+ class="absolute inset-0 cursor-zoom-out"
49
+ on:click={onClose}
50
+ aria-label="Close image"
51
+ ></button>
52
+ <img
53
+ src={image.src}
54
+ alt={image.alt}
55
+ class="relative max-h-[90vh] max-w-[92vw] rounded-2xl border border-white/10 bg-black/10 object-contain"
56
+ />
57
+ </div>
58
+ {/if}
@@ -0,0 +1,24 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: Props & {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const ImageLightbox: $$__sveltets_2_IsomorphicComponent<{
15
+ image: {
16
+ src: string;
17
+ alt: string;
18
+ } | null;
19
+ onClose: () => void;
20
+ }, {
21
+ [evt: string]: CustomEvent<any>;
22
+ }, {}, {}, string>;
23
+ type ImageLightbox = InstanceType<typeof ImageLightbox>;
24
+ export default ImageLightbox;
@@ -0,0 +1,15 @@
1
+ <script lang="ts">
2
+ import type { BlogPost } from '../../types/blog';
3
+ import BlogCard from './BlogCard.svelte';
4
+
5
+ export let posts: BlogPost[];
6
+ </script>
7
+
8
+ <section class="mt-16" data-testid="blog-more-posts">
9
+ <h2 class="text-lg font-medium tracking-tight text-text-main">More blog posts to read</h2>
10
+ <div class="mt-8 grid grid-cols-1 gap-x-5 gap-y-12 md:grid-cols-2">
11
+ {#each posts as post (post.slug)}
12
+ <BlogCard post={post} variant="suggestion" />
13
+ {/each}
14
+ </div>
15
+ </section>
@@ -0,0 +1,21 @@
1
+ import type { BlogPost } from '../../types/blog';
2
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
+ $$bindings?: Bindings;
5
+ } & Exports;
6
+ (internal: unknown, props: Props & {
7
+ $$events?: Events;
8
+ $$slots?: Slots;
9
+ }): Exports & {
10
+ $set?: any;
11
+ $on?: any;
12
+ };
13
+ z_$$bindings?: Bindings;
14
+ }
15
+ declare const MorePosts: $$__sveltets_2_IsomorphicComponent<{
16
+ posts: BlogPost[];
17
+ }, {
18
+ [evt: string]: CustomEvent<any>;
19
+ }, {}, {}, string>;
20
+ type MorePosts = InstanceType<typeof MorePosts>;
21
+ export default MorePosts;
@@ -0,0 +1,113 @@
1
+ <script lang="ts">
2
+ import { BROWSER } from 'esm-env';
3
+
4
+ export let title: string;
5
+ export let url: string;
6
+ export let label: string = 'Share this article';
7
+ export let testId: string | undefined = undefined;
8
+
9
+ let copied = false;
10
+
11
+ function openShare(href: string) {
12
+ if (!BROWSER) return;
13
+ window.open(href, '_blank', 'noopener,noreferrer');
14
+ }
15
+
16
+ async function copyLink() {
17
+ if (!BROWSER) return;
18
+ try {
19
+ await navigator.clipboard.writeText(url);
20
+ copied = true;
21
+ window.setTimeout(() => (copied = false), 1200);
22
+ } catch {
23
+ // Fallback for older browsers.
24
+ const ta = document.createElement('textarea');
25
+ ta.value = url;
26
+ ta.style.position = 'fixed';
27
+ ta.style.opacity = '0';
28
+ document.body.appendChild(ta);
29
+ ta.select();
30
+ document.execCommand('copy');
31
+ document.body.removeChild(ta);
32
+ copied = true;
33
+ window.setTimeout(() => (copied = false), 1200);
34
+ }
35
+ }
36
+
37
+ $: encodedUrl = encodeURIComponent(url);
38
+ $: encodedTitle = encodeURIComponent(title);
39
+ </script>
40
+
41
+ <div data-testid={testId}>
42
+ <p class="text-xs font-mono uppercase tracking-[0.6px] text-text-muted">{label}</p>
43
+ <div class="mt-3 flex flex-wrap gap-2">
44
+ <button
45
+ type="button"
46
+ class="inline-flex size-8 items-center justify-center rounded-full border border-border-soft/10 bg-background-soft text-text-sub transition hover:bg-background-main/60 hover:text-text-main"
47
+ on:click={() =>
48
+ openShare(`https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`)}
49
+ aria-label="Share on X"
50
+ >
51
+ <svg viewBox="0 0 24 24" class="size-4" aria-hidden="true">
52
+ <path
53
+ fill="currentColor"
54
+ d="M18.9 2H22l-6.8 7.8L23 22h-6.8l-5.3-6.9L4.9 22H2l7.4-8.6L1.5 2h7l4.8 6.2L18.9 2Zm-1.2 18h1.7L7.8 3.9H6.1L17.7 20Z"
55
+ />
56
+ </svg>
57
+ </button>
58
+
59
+ <button
60
+ type="button"
61
+ class="inline-flex size-8 items-center justify-center rounded-full border border-border-soft/10 bg-background-soft text-text-sub transition hover:bg-background-main/60 hover:text-text-main"
62
+ on:click={() =>
63
+ openShare(`https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`)}
64
+ aria-label="Share on LinkedIn"
65
+ >
66
+ <svg viewBox="0 0 24 24" class="size-4" aria-hidden="true">
67
+ <path
68
+ fill="currentColor"
69
+ d="M4.98 3.5C4.98 4.88 3.87 6 2.5 6S0 4.88 0 3.5 1.12 1 2.5 1s2.48 1.12 2.48 2.5ZM0.5 23.5h4V7.98h-4V23.5ZM8 7.98h3.84v2.12h.05c.53-1 1.83-2.12 3.77-2.12 4.03 0 4.78 2.65 4.78 6.1v9.42h-4v-8.36c0-1.99-.03-4.55-2.77-4.55-2.77 0-3.2 2.16-3.2 4.4v8.5H8V7.98Z"
70
+ />
71
+ </svg>
72
+ </button>
73
+
74
+ <button
75
+ type="button"
76
+ class="inline-flex size-8 items-center justify-center rounded-full border border-border-soft/10 bg-background-soft text-text-sub transition hover:bg-background-main/60 hover:text-text-main"
77
+ on:click={() => openShare(`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`)}
78
+ aria-label="Share on Facebook"
79
+ >
80
+ <svg viewBox="0 0 24 24" class="size-4" aria-hidden="true">
81
+ <path
82
+ fill="currentColor"
83
+ d="M13.5 22v-8h2.7l.4-3H13.5V9.1c0-.9.3-1.6 1.7-1.6h1.6V4.8c-.3 0-1.4-.1-2.7-.1-2.7 0-4.5 1.6-4.5 4.6V11H7v3h2.6v8h3.9Z"
84
+ />
85
+ </svg>
86
+ </button>
87
+
88
+ <button
89
+ type="button"
90
+ class="inline-flex size-8 items-center justify-center rounded-full border border-border-soft/10 bg-background-soft text-text-sub transition hover:bg-background-main/60 hover:text-text-main"
91
+ on:click={copyLink}
92
+ aria-label="Copy link"
93
+ >
94
+ <svg
95
+ viewBox="0 0 24 24"
96
+ class="size-4"
97
+ fill="none"
98
+ stroke="currentColor"
99
+ stroke-width="2"
100
+ stroke-linecap="round"
101
+ stroke-linejoin="round"
102
+ aria-hidden="true"
103
+ >
104
+ <path d="M10 13a5 5 0 0 1 0-7l1-1a5 5 0 0 1 7 7l-1 1" />
105
+ <path d="M14 11a5 5 0 0 1 0 7l-1 1a5 5 0 0 1-7-7l1-1" />
106
+ </svg>
107
+ </button>
108
+ </div>
109
+
110
+ {#if copied}
111
+ <p class="mt-2 text-xs font-mono uppercase tracking-[0.6px] text-brand">Copied</p>
112
+ {/if}
113
+ </div>