@aureuma/svelta 0.1.0 → 0.2.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 (71) hide show
  1. package/README.md +22 -2
  2. package/package.json +31 -19
  3. package/packages/{blogkit → core}/CHANGELOG.md +1 -1
  4. package/packages/core/README.md +11 -0
  5. package/packages/core/dist/appearance/AppearanceSwitcher.svelte +58 -0
  6. package/packages/core/dist/appearance/AppearanceSwitcher.svelte.d.ts +21 -0
  7. package/packages/core/dist/appearance/index.d.ts +3 -0
  8. package/packages/core/dist/appearance/index.js +3 -0
  9. package/packages/core/dist/appearance/palettes.d.ts +10 -0
  10. package/packages/core/dist/appearance/palettes.js +36 -0
  11. package/packages/core/dist/appearance/store.d.ts +20 -0
  12. package/packages/core/dist/appearance/store.js +87 -0
  13. package/packages/core/dist/components/blog/BackLink.svelte +26 -0
  14. package/packages/core/dist/components/blog/BlogCard.svelte +50 -0
  15. package/packages/core/dist/components/blog/BlogHeroCard.svelte +52 -0
  16. package/packages/core/dist/components/blog/Container.svelte +9 -0
  17. package/packages/{blogkit → core}/dist/components/blog/Container.svelte.d.ts +1 -1
  18. package/packages/{blogkit → core}/dist/components/blog/MorePosts.svelte +1 -1
  19. package/packages/core/dist/components/blog/ShareButtons.svelte +147 -0
  20. package/packages/{blogkit → core}/dist/components/blog/ShareButtons.svelte.d.ts +2 -0
  21. package/packages/core/dist/components/blog/TagTabs.svelte +85 -0
  22. package/packages/{blogkit → core}/dist/components/blog/TagTabs.svelte.d.ts +4 -2
  23. package/packages/core/dist/components/docs/DocsPager.svelte +28 -0
  24. package/packages/core/dist/components/docs/DocsPager.svelte.d.ts +22 -0
  25. package/packages/core/dist/components/docs/DocsSectionGrid.svelte +26 -0
  26. package/packages/core/dist/components/docs/DocsSectionGrid.svelte.d.ts +21 -0
  27. package/packages/core/dist/components/docs/DocsShell.svelte +19 -0
  28. package/packages/core/dist/components/docs/DocsShell.svelte.d.ts +31 -0
  29. package/packages/core/dist/components/docs/DocsSidebar.svelte +31 -0
  30. package/packages/core/dist/components/docs/DocsSidebar.svelte.d.ts +22 -0
  31. package/packages/core/dist/experience/index.d.ts +7 -0
  32. package/packages/core/dist/experience/index.js +43 -0
  33. package/packages/core/dist/index.d.ts +18 -0
  34. package/packages/{blogkit → core}/dist/index.js +5 -0
  35. package/packages/{blogkit → core}/dist/server/blog.d.ts +14 -1
  36. package/packages/{blogkit → core}/dist/server/blog.js +144 -14
  37. package/packages/core/dist/server/docs.d.ts +44 -0
  38. package/packages/core/dist/server/docs.js +193 -0
  39. package/packages/{blogkit → core}/dist/server/index.d.ts +1 -0
  40. package/packages/{blogkit → core}/dist/server/index.js +1 -0
  41. package/packages/{blogkit → core}/dist/types/blog.d.ts +14 -0
  42. package/packages/core/dist/types/docs.d.ts +28 -0
  43. package/packages/core/dist/types/docs.js +1 -0
  44. package/packages/core/dist/types/experience.d.ts +13 -0
  45. package/packages/core/dist/types/experience.js +1 -0
  46. package/packages/blogkit/README.md +0 -93
  47. package/packages/blogkit/dist/components/blog/BackLink.svelte +0 -23
  48. package/packages/blogkit/dist/components/blog/BlogCard.svelte +0 -37
  49. package/packages/blogkit/dist/components/blog/BlogHeroCard.svelte +0 -36
  50. package/packages/blogkit/dist/components/blog/Container.svelte +0 -8
  51. package/packages/blogkit/dist/components/blog/ShareButtons.svelte +0 -113
  52. package/packages/blogkit/dist/components/blog/TagTabs.svelte +0 -32
  53. package/packages/blogkit/dist/index.d.ts +0 -11
  54. package/packages/blogkit/dist/theme/ThemeSwitcher.svelte +0 -34
  55. package/packages/blogkit/dist/theme/ThemeSwitcher.svelte.d.ts +0 -21
  56. package/packages/blogkit/dist/theme/index.d.ts +0 -2
  57. package/packages/blogkit/dist/theme/index.js +0 -2
  58. package/packages/blogkit/dist/theme/store.d.ts +0 -12
  59. package/packages/blogkit/dist/theme/store.js +0 -50
  60. /package/packages/{blogkit → core}/LICENSE +0 -0
  61. /package/packages/{blogkit → core}/dist/components/blog/Avatar.svelte +0 -0
  62. /package/packages/{blogkit → core}/dist/components/blog/Avatar.svelte.d.ts +0 -0
  63. /package/packages/{blogkit → core}/dist/components/blog/BackLink.svelte.d.ts +0 -0
  64. /package/packages/{blogkit → core}/dist/components/blog/BlogCard.svelte.d.ts +0 -0
  65. /package/packages/{blogkit → core}/dist/components/blog/BlogHeroCard.svelte.d.ts +0 -0
  66. /package/packages/{blogkit → core}/dist/components/blog/ImageLightbox.svelte +0 -0
  67. /package/packages/{blogkit → core}/dist/components/blog/ImageLightbox.svelte.d.ts +0 -0
  68. /package/packages/{blogkit → core}/dist/components/blog/MorePosts.svelte.d.ts +0 -0
  69. /package/packages/{blogkit → core}/dist/components/blog/SummaryCard.svelte +0 -0
  70. /package/packages/{blogkit → core}/dist/components/blog/SummaryCard.svelte.d.ts +0 -0
  71. /package/packages/{blogkit → core}/dist/types/blog.js +0 -0
package/README.md CHANGED
@@ -1,9 +1,17 @@
1
1
  # svelta
2
2
 
3
- Mintlify-inspired blogging system built with SvelteKit (Svelte 5) + Markdown.
3
+ A markdown publishing system for two first-class experiences in SvelteKit (Svelte 5): docs and blog.
4
+
5
+ ## Naming model
6
+
7
+ - `experience`: content mode (`docs` or `blog`)
8
+ - `appearance`: UI mode (`system`, `light`, `dark`)
4
9
 
5
10
  ## Routes
6
11
 
12
+ - `/` landing page with the selected initial experience
13
+ - `/docs` docs index (section cards + guided entry)
14
+ - `/docs/[slug]` docs page (sidebar + previous/next navigation)
7
15
  - `/blog` blog index (hero + category pills + infinite scroll)
8
16
  - `/blog/[slug]` blog post page (sticky author/share rail on desktop, folds into header on mobile)
9
17
  - `/feed.xml` RSS 2.0 feed
@@ -11,15 +19,28 @@ Mintlify-inspired blogging system built with SvelteKit (Svelte 5) + Markdown.
11
19
  ## Content
12
20
 
13
21
  Markdown posts live in `src/content/blog/*.md` (YAML frontmatter required).
22
+ Markdown docs pages live in `src/content/docs/*.md` (YAML frontmatter required).
14
23
 
15
24
  Static assets (covers/avatars) live in `static/blog/*`.
16
25
 
26
+ ## Initial experience
27
+
28
+ Set `PUBLIC_SVELTA_EXPERIENCE=docs` or `PUBLIC_SVELTA_EXPERIENCE=blog`.
29
+
17
30
  ## Development
18
31
 
19
32
  ```sh
20
33
  npm run dev
21
34
  ```
22
35
 
36
+ ## Internal Hosting (Pre-deploy)
37
+
38
+ ```sh
39
+ npm run host:internal
40
+ ```
41
+
42
+ This serves the built site on `0.0.0.0:4173` for internal network validation.
43
+
23
44
  ## Typecheck
24
45
 
25
46
  ```sh
@@ -32,4 +53,3 @@ npm run check
32
53
  npm run build
33
54
  npm run preview
34
55
  ```
35
-
package/package.json CHANGED
@@ -1,34 +1,38 @@
1
1
  {
2
2
  "name": "@aureuma/svelta",
3
3
  "private": false,
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "type": "module",
6
6
  "files": [
7
- "packages/blogkit/dist",
8
- "packages/blogkit/README.md",
9
- "packages/blogkit/CHANGELOG.md",
7
+ "packages/core/dist",
8
+ "packages/core/README.md",
9
+ "packages/core/CHANGELOG.md",
10
10
  "LICENSE",
11
11
  "README.md"
12
12
  ],
13
13
  "exports": {
14
14
  ".": {
15
- "types": "./packages/blogkit/dist/index.d.ts",
16
- "svelte": "./packages/blogkit/dist/index.js",
17
- "default": "./packages/blogkit/dist/index.js"
15
+ "types": "./packages/core/dist/index.d.ts",
16
+ "svelte": "./packages/core/dist/index.js",
17
+ "default": "./packages/core/dist/index.js"
18
18
  },
19
19
  "./server": {
20
- "types": "./packages/blogkit/dist/server/index.d.ts",
21
- "default": "./packages/blogkit/dist/server/index.js"
20
+ "types": "./packages/core/dist/server/index.d.ts",
21
+ "default": "./packages/core/dist/server/index.js"
22
22
  },
23
- "./theme": {
24
- "types": "./packages/blogkit/dist/theme/index.d.ts",
25
- "svelte": "./packages/blogkit/dist/theme/index.js",
26
- "default": "./packages/blogkit/dist/theme/index.js"
23
+ "./appearance": {
24
+ "types": "./packages/core/dist/appearance/index.d.ts",
25
+ "svelte": "./packages/core/dist/appearance/index.js",
26
+ "default": "./packages/core/dist/appearance/index.js"
27
+ },
28
+ "./experience": {
29
+ "types": "./packages/core/dist/experience/index.d.ts",
30
+ "default": "./packages/core/dist/experience/index.js"
27
31
  }
28
32
  },
29
- "types": "./packages/blogkit/dist/index.d.ts",
30
- "main": "./packages/blogkit/dist/index.js",
31
- "svelte": "./packages/blogkit/dist/index.js",
33
+ "types": "./packages/core/dist/index.d.ts",
34
+ "main": "./packages/core/dist/index.js",
35
+ "svelte": "./packages/core/dist/index.js",
32
36
  "peerDependencies": {
33
37
  "svelte": "^4.0.0 || ^5.0.0"
34
38
  },
@@ -39,8 +43,9 @@
39
43
  "dev": "vite dev",
40
44
  "build": "vite build",
41
45
  "preview": "vite preview",
46
+ "host:internal": "npm run build && npm run preview -- --host 0.0.0.0 --port 4173",
42
47
  "prepare": "svelte-kit sync || echo ''",
43
- "postinstall": "npm -w @aureuma/blogkit run build",
48
+ "postinstall": "npm -w @aureuma/svelta-core run build",
44
49
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
45
50
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
46
51
  "test:e2e": "playwright test",
@@ -48,11 +53,13 @@
48
53
  "changeset": "changeset",
49
54
  "version-packages": "changeset version",
50
55
  "release": "changeset publish",
51
- "prepack": "npm -w @aureuma/blogkit run build"
56
+ "prepack": "npm -w @aureuma/svelta-core run build"
52
57
  },
53
58
  "devDependencies": {
54
59
  "@changesets/changelog-github": "^0.5.2",
55
60
  "@changesets/cli": "^2.29.8",
61
+ "@internationalized/date": "^3.11.0",
62
+ "@lucide/svelte": "^0.561.0",
56
63
  "@playwright/test": "^1.58.2",
57
64
  "@shikijs/rehype": "^3.22.0",
58
65
  "@sveltejs/adapter-auto": "^7.0.0",
@@ -63,6 +70,8 @@
63
70
  "@tailwindcss/typography": "^0.5.19",
64
71
  "@types/node": "^25.2.2",
65
72
  "autoprefixer": "^10.4.24",
73
+ "bits-ui": "^2.16.2",
74
+ "clsx": "^2.1.1",
66
75
  "mdsvex": "^0.12.6",
67
76
  "postcss": "^8.5.6",
68
77
  "rehype-autolink-headings": "^7.1.0",
@@ -72,14 +81,17 @@
72
81
  "svelte": "^5.49.2",
73
82
  "svelte-check": "^4.3.6",
74
83
  "svelte2tsx": "^0.7.47",
84
+ "tailwind-merge": "^3.5.0",
85
+ "tailwind-variants": "^3.2.2",
75
86
  "tailwindcss": "^4.1.18",
87
+ "tw-animate-css": "^1.4.0",
76
88
  "typescript": "^5.9.3",
77
89
  "vite": "^7.3.1"
78
90
  },
79
91
  "dependencies": {
80
- "esm-env": "^1.2.2",
81
92
  "@fontsource/geist-mono": "^5.2.7",
82
93
  "@fontsource/inter": "^5.2.8",
94
+ "esm-env": "^1.2.2",
83
95
  "gray-matter": "^4.0.3",
84
96
  "marked": "^12.0.2",
85
97
  "reading-time": "^1.5.0",
@@ -1,4 +1,4 @@
1
- # @aureuma/blogkit Changelog
1
+ # svelta Core Changelog
2
2
 
3
3
  All notable changes to this package will be documented in this file.
4
4
 
@@ -0,0 +1,11 @@
1
+ # svelta Core (Internal)
2
+
3
+ This internal workspace package contains the reusable library source and build output consumed by `@aureuma/svelta`.
4
+
5
+ Public package consumers should install:
6
+
7
+ ```sh
8
+ npm i @aureuma/svelta
9
+ ```
10
+
11
+ The source of truth for public usage/docs is the repository root.
@@ -0,0 +1,58 @@
1
+ <script lang="ts">
2
+ import type { AppearanceController, AppearanceMode } from './store';
3
+
4
+ export let controller: AppearanceController;
5
+
6
+ const options: { id: AppearanceMode; label: string }[] = [
7
+ { id: 'system', label: 'System' },
8
+ { id: 'light', label: 'Light' },
9
+ { id: 'dark', label: 'Dark' }
10
+ ];
11
+
12
+ // Store auto-subscriptions only work on identifiers, so alias it.
13
+ const appearanceMode = controller.appearanceMode;
14
+ const appearancePalette = controller.appearancePalette;
15
+ $: currentMode = $appearanceMode;
16
+ $: currentPalette = $appearancePalette;
17
+ </script>
18
+
19
+ <div class="flex flex-col gap-3">
20
+ <div class="flex items-center gap-2">
21
+ <span class="text-xs font-mono uppercase tracking-[0.6px] text-text-muted">Appearance</span>
22
+ <div class="inline-flex rounded-full border border-border-soft/10 bg-background-soft p-1">
23
+ {#each options as opt (opt.id)}
24
+ <button
25
+ type="button"
26
+ class="rounded-full px-3 py-1 text-xs font-mono uppercase tracking-[0.6px] transition
27
+ hover:bg-background-main/60
28
+ {(currentMode === opt.id && 'bg-background-main shadow-sm') || 'text-text-sub'}"
29
+ onclick={() => controller.setAppearanceMode(opt.id)}
30
+ aria-pressed={currentMode === opt.id}
31
+ >
32
+ {opt.label}
33
+ </button>
34
+ {/each}
35
+ </div>
36
+ </div>
37
+
38
+ <div class="flex items-center gap-2">
39
+ <span class="text-xs font-mono uppercase tracking-[0.6px] text-text-muted">Palette</span>
40
+ <div class="flex flex-wrap gap-1.5">
41
+ {#each controller.palettes as palette (palette.id)}
42
+ <button
43
+ type="button"
44
+ class="inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-[11px] font-mono uppercase tracking-[0.6px] transition
45
+ {currentPalette === palette.id
46
+ ? 'border-border-soft/20 bg-background-main text-text-main'
47
+ : 'border-border-soft/10 bg-background-soft text-text-sub hover:bg-background-main/60'}"
48
+ onclick={() => controller.setAppearancePalette(palette.id)}
49
+ aria-pressed={currentPalette === palette.id}
50
+ aria-label={`Use ${palette.label} palette`}
51
+ >
52
+ <span>{palette.element}</span>
53
+ <span>{palette.label}</span>
54
+ </button>
55
+ {/each}
56
+ </div>
57
+ </div>
58
+ </div>
@@ -0,0 +1,21 @@
1
+ import type { AppearanceController } from './store';
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 AppearanceSwitcher: $$__sveltets_2_IsomorphicComponent<{
16
+ controller: AppearanceController;
17
+ }, {
18
+ [evt: string]: CustomEvent<any>;
19
+ }, {}, {}, string>;
20
+ type AppearanceSwitcher = InstanceType<typeof AppearanceSwitcher>;
21
+ export default AppearanceSwitcher;
@@ -0,0 +1,3 @@
1
+ export { createAppearanceController, type AppearanceController, type AppearanceMode } from './store';
2
+ export { default as AppearanceSwitcher } from './AppearanceSwitcher.svelte';
3
+ export { APPEARANCE_PALETTES, DEFAULT_APPEARANCE_PALETTE, isAppearancePalette, type AppearancePalette, type AppearancePaletteId } from './palettes';
@@ -0,0 +1,3 @@
1
+ export { createAppearanceController } from './store';
2
+ export { default as AppearanceSwitcher } from './AppearanceSwitcher.svelte';
3
+ export { APPEARANCE_PALETTES, DEFAULT_APPEARANCE_PALETTE, isAppearancePalette } from './palettes';
@@ -0,0 +1,10 @@
1
+ export type AppearancePaletteId = 'argon' | 'copper' | 'cobalt' | 'selenium' | 'neon';
2
+ export type AppearancePalette = {
3
+ id: AppearancePaletteId;
4
+ label: string;
5
+ element: string;
6
+ note: string;
7
+ };
8
+ export declare const APPEARANCE_PALETTES: readonly AppearancePalette[];
9
+ export declare const DEFAULT_APPEARANCE_PALETTE: AppearancePaletteId;
10
+ export declare function isAppearancePalette(value: string | null | undefined): value is AppearancePaletteId;
@@ -0,0 +1,36 @@
1
+ export const APPEARANCE_PALETTES = [
2
+ {
3
+ id: 'argon',
4
+ label: 'Argon',
5
+ element: 'Ar',
6
+ note: 'Neutral blue-gray with calm emerald accents'
7
+ },
8
+ {
9
+ id: 'copper',
10
+ label: 'Copper',
11
+ element: 'Cu',
12
+ note: 'Warm metal palette with amber highlights'
13
+ },
14
+ {
15
+ id: 'cobalt',
16
+ label: 'Cobalt',
17
+ element: 'Co',
18
+ note: 'Deep technical blues with cool contrast'
19
+ },
20
+ {
21
+ id: 'selenium',
22
+ label: 'Selenium',
23
+ element: 'Se',
24
+ note: 'Editorial crimson spectrum with dark ink'
25
+ },
26
+ {
27
+ id: 'neon',
28
+ label: 'Neon',
29
+ element: 'Ne',
30
+ note: 'High-energy lime/cyan futuristic signal'
31
+ }
32
+ ];
33
+ export const DEFAULT_APPEARANCE_PALETTE = 'argon';
34
+ export function isAppearancePalette(value) {
35
+ return APPEARANCE_PALETTES.some((palette) => palette.id === value);
36
+ }
@@ -0,0 +1,20 @@
1
+ import { type Writable } from 'svelte/store';
2
+ import { type AppearancePalette, type AppearancePaletteId } from './palettes';
3
+ export type AppearanceMode = 'system' | 'light' | 'dark';
4
+ export type AppearanceController = {
5
+ storageKey: string;
6
+ paletteStorageKey: string;
7
+ palettes: readonly AppearancePalette[];
8
+ appearanceMode: Writable<AppearanceMode>;
9
+ appearancePalette: Writable<AppearancePaletteId>;
10
+ initAppearance: () => void | (() => void);
11
+ setAppearanceMode: (mode: AppearanceMode) => void;
12
+ setAppearancePalette: (palette: AppearancePaletteId) => void;
13
+ };
14
+ export declare function createAppearanceController(opts?: {
15
+ storageKey?: string;
16
+ paletteStorageKey?: string;
17
+ defaultMode?: AppearanceMode;
18
+ defaultPalette?: AppearancePaletteId;
19
+ palettes?: readonly AppearancePalette[];
20
+ }): AppearanceController;
@@ -0,0 +1,87 @@
1
+ import { BROWSER } from 'esm-env';
2
+ import { writable } from 'svelte/store';
3
+ import { APPEARANCE_PALETTES, DEFAULT_APPEARANCE_PALETTE, isAppearancePalette } from './palettes';
4
+ function resolve(mode) {
5
+ if (mode === 'light' || mode === 'dark')
6
+ return mode;
7
+ const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ?? false;
8
+ return prefersDark ? 'dark' : 'light';
9
+ }
10
+ function apply(mode) {
11
+ const resolved = resolve(mode);
12
+ document.documentElement.classList.remove('light', 'dark');
13
+ document.documentElement.classList.add(resolved);
14
+ document.documentElement.dataset.appearance = mode;
15
+ }
16
+ function applyPalette(palette) {
17
+ document.documentElement.dataset.palette = palette;
18
+ }
19
+ export function createAppearanceController(opts) {
20
+ const storageKey = opts?.storageKey ?? 'svelta-appearance';
21
+ const paletteStorageKey = opts?.paletteStorageKey ?? 'svelta-appearance-palette';
22
+ const defaultMode = opts?.defaultMode ?? 'system';
23
+ const palettes = opts?.palettes ?? APPEARANCE_PALETTES;
24
+ const defaultPalette = opts?.defaultPalette ?? DEFAULT_APPEARANCE_PALETTE;
25
+ const appearanceMode = writable(defaultMode);
26
+ const appearancePalette = writable(defaultPalette);
27
+ function readStored() {
28
+ const v = localStorage.getItem(storageKey);
29
+ if (v === 'light' || v === 'dark' || v === 'system')
30
+ return v;
31
+ return defaultMode;
32
+ }
33
+ function readStoredPalette() {
34
+ const stored = localStorage.getItem(paletteStorageKey);
35
+ if (isAppearancePalette(stored) && palettes.some((palette) => palette.id === stored))
36
+ return stored;
37
+ if (palettes.some((palette) => palette.id === defaultPalette))
38
+ return defaultPalette;
39
+ return palettes[0]?.id ?? DEFAULT_APPEARANCE_PALETTE;
40
+ }
41
+ function initAppearance() {
42
+ if (!BROWSER)
43
+ return;
44
+ const mode = readStored();
45
+ const palette = readStoredPalette();
46
+ appearanceMode.set(mode);
47
+ appearancePalette.set(palette);
48
+ apply(mode);
49
+ applyPalette(palette);
50
+ const mq = window.matchMedia?.('(prefers-color-scheme: dark)');
51
+ const onChange = () => {
52
+ let current = mode;
53
+ const unsub = appearanceMode.subscribe((v) => (current = v));
54
+ unsub();
55
+ if (current === 'system')
56
+ apply('system');
57
+ };
58
+ mq?.addEventListener?.('change', onChange);
59
+ return () => mq?.removeEventListener?.('change', onChange);
60
+ }
61
+ function setAppearanceMode(mode) {
62
+ appearanceMode.set(mode);
63
+ if (!BROWSER)
64
+ return;
65
+ localStorage.setItem(storageKey, mode);
66
+ apply(mode);
67
+ }
68
+ function setAppearancePalette(palette) {
69
+ if (!palettes.some((item) => item.id === palette))
70
+ return;
71
+ appearancePalette.set(palette);
72
+ if (!BROWSER)
73
+ return;
74
+ localStorage.setItem(paletteStorageKey, palette);
75
+ applyPalette(palette);
76
+ }
77
+ return {
78
+ storageKey,
79
+ paletteStorageKey,
80
+ palettes,
81
+ appearanceMode,
82
+ appearancePalette,
83
+ initAppearance,
84
+ setAppearanceMode,
85
+ setAppearancePalette
86
+ };
87
+ }
@@ -0,0 +1,26 @@
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="group inline-flex items-center gap-2 text-xs font-mono uppercase tracking-[0.6px] text-text-muted transition-colors hover:text-text-main"
9
+ >
10
+ <svg
11
+ class="size-4 transition-colors"
12
+ viewBox="0 0 24 24"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ stroke-width="1.7"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ aria-hidden="true"
19
+ >
20
+ <path
21
+ class="fill-transparent transition-[fill,stroke] duration-200 group-hover:fill-current"
22
+ d="M20 11H7.83l4.58-4.59L11 5l-7 7 7 7 1.41-1.41L7.83 13H20v-2Z"
23
+ />
24
+ </svg>
25
+ <span>{label}</span>
26
+ </a>
@@ -0,0 +1,50 @@
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-[170px]' : 'h-[236px]';
8
+ $: primaryTag = post.tags[0] ?? post.category.label;
9
+ $: showAuthor = variant === 'default';
10
+ </script>
11
+
12
+ <a href={`/blog/${post.slug}`} class="group block" data-testid="blog-card">
13
+ <div class="relative overflow-hidden rounded-2xl {thumbHeight}">
14
+ <img
15
+ src={post.cover}
16
+ alt={post.title}
17
+ class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.02]"
18
+ loading="lazy"
19
+ />
20
+
21
+ <div
22
+ class="pointer-events-none absolute inset-x-0 bottom-0 h-[68%] bg-gradient-to-t from-black/58 via-black/22 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"
23
+ ></div>
24
+ </div>
25
+
26
+ <div class="mt-4">
27
+ <p class="text-[10px] font-mono uppercase tracking-[0.7px] text-brand">{primaryTag}</p>
28
+ <h3
29
+ class="mt-1 text-[19px] font-medium leading-7 tracking-tight underline-offset-[6px] decoration-border-soft/30 group-hover:underline"
30
+ >
31
+ {post.title}
32
+ </h3>
33
+ <p class="mt-2 text-sm leading-6 text-text-sub">{post.excerpt}</p>
34
+ <p class="mt-3 text-[11px] font-mono uppercase tracking-[0.65px] text-text-muted">
35
+ {post.dateShort}
36
+ <span class="mx-2 text-text-muted/50" aria-hidden="true">|</span>
37
+ {post.readingTimeShort}
38
+ </p>
39
+
40
+ {#if showAuthor}
41
+ <div class="mt-4 flex items-center gap-3">
42
+ <Avatar src={post.author.avatar} alt={post.author.name} size={24} />
43
+ <div class="leading-tight">
44
+ <div class="text-sm font-medium tracking-tight text-text-main">{post.author.name}</div>
45
+ <div class="text-xs text-text-muted">{post.author.title}</div>
46
+ </div>
47
+ </div>
48
+ {/if}
49
+ </div>
50
+ </a>
@@ -0,0 +1,52 @@
1
+ <script lang="ts">
2
+ import type { BlogPost } from '../../types/blog';
3
+ import Avatar from './Avatar.svelte';
4
+
5
+ export let post: BlogPost;
6
+ $: primaryTag = post.tags[0] ?? post.category.label;
7
+ </script>
8
+
9
+ <a
10
+ href={`/blog/${post.slug}`}
11
+ data-testid="blog-hero"
12
+ class="group relative my-10 block overflow-hidden rounded-[22px]"
13
+ >
14
+ <img
15
+ src={post.cover}
16
+ alt={post.title}
17
+ class="absolute inset-0 h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.03]"
18
+ loading="eager"
19
+ />
20
+
21
+ <div
22
+ class="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/72 via-black/28 to-black/0 transition-opacity duration-300 group-hover:opacity-95"
23
+ ></div>
24
+
25
+ <div class="relative aspect-[16/9] min-h-[320px] w-full md:min-h-[420px]">
26
+ <div class="relative flex h-full flex-col justify-end p-6 md:p-8">
27
+ <p class="text-xs font-mono uppercase tracking-[0.6px] text-brand">{primaryTag}</p>
28
+ <h2
29
+ class="mt-2 max-w-3xl text-3xl font-semibold leading-tight tracking-[-0.8px] text-white md:text-4xl"
30
+ >
31
+ {post.title}
32
+ </h2>
33
+ <p class="mt-3 max-w-2xl text-sm leading-6 text-white/82 md:text-base">{post.excerpt}</p>
34
+
35
+ <div
36
+ class="mt-6 flex items-center gap-2 text-xs font-mono uppercase tracking-[0.6px] text-white/70"
37
+ >
38
+ <span>{post.dateShort}</span>
39
+ <span class="text-white/35" aria-hidden="true">•</span>
40
+ <span>{post.readingTimeShort}</span>
41
+ </div>
42
+
43
+ <div class="mt-4 flex items-center gap-3">
44
+ <Avatar src={post.author.avatar} alt={post.author.name} size={26} />
45
+ <div class="leading-tight">
46
+ <div class="text-sm font-medium tracking-tight text-white">{post.author.name}</div>
47
+ <div class="text-xs text-white/70">{post.author.title}</div>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </a>
@@ -0,0 +1,9 @@
1
+ <script lang="ts">
2
+ export let size: '4xl' | '5xl' | '6xl' = '5xl';
3
+ $: max =
4
+ size === '4xl' ? 'max-w-4xl' : size === '6xl' ? 'max-w-6xl' : 'max-w-5xl';
5
+ </script>
6
+
7
+ <div class="mx-auto w-full {max} px-6">
8
+ <slot />
9
+ </div>
@@ -17,7 +17,7 @@ type $$__sveltets_2_PropsWithChildren<Props, Slots> = Props & (Slots extends {
17
17
  children?: any;
18
18
  } : {});
19
19
  declare const Container: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_PropsWithChildren<{
20
- size?: "4xl" | "5xl";
20
+ size?: "4xl" | "5xl" | "6xl";
21
21
  }, {
22
22
  default: {};
23
23
  }>, {
@@ -6,7 +6,7 @@
6
6
  </script>
7
7
 
8
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>
9
+ <h2 class="text-lg font-medium tracking-tight text-text-main">More posts to read</h2>
10
10
  <div class="mt-8 grid grid-cols-1 gap-x-5 gap-y-12 md:grid-cols-2">
11
11
  {#each posts as post (post.slug)}
12
12
  <BlogCard post={post} variant="suggestion" />