@aureuma/svelta 0.1.1 → 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 (48) hide show
  1. package/README.md +22 -1
  2. package/package.json +18 -6
  3. package/packages/core/dist/appearance/AppearanceSwitcher.svelte +58 -0
  4. package/packages/core/dist/appearance/AppearanceSwitcher.svelte.d.ts +21 -0
  5. package/packages/core/dist/appearance/index.d.ts +3 -0
  6. package/packages/core/dist/appearance/index.js +3 -0
  7. package/packages/core/dist/appearance/palettes.d.ts +10 -0
  8. package/packages/core/dist/appearance/palettes.js +36 -0
  9. package/packages/core/dist/appearance/store.d.ts +20 -0
  10. package/packages/core/dist/appearance/store.js +87 -0
  11. package/packages/core/dist/components/blog/BackLink.svelte +7 -4
  12. package/packages/core/dist/components/blog/BlogCard.svelte +23 -10
  13. package/packages/core/dist/components/blog/BlogHeroCard.svelte +31 -15
  14. package/packages/core/dist/components/blog/Container.svelte +3 -2
  15. package/packages/core/dist/components/blog/Container.svelte.d.ts +1 -1
  16. package/packages/core/dist/components/blog/MorePosts.svelte +1 -1
  17. package/packages/core/dist/components/blog/ShareButtons.svelte +81 -47
  18. package/packages/core/dist/components/blog/ShareButtons.svelte.d.ts +2 -0
  19. package/packages/core/dist/components/blog/TagTabs.svelte +71 -18
  20. package/packages/core/dist/components/blog/TagTabs.svelte.d.ts +4 -2
  21. package/packages/core/dist/components/docs/DocsPager.svelte +28 -0
  22. package/packages/core/dist/{theme/ThemeSwitcher.svelte.d.ts → components/docs/DocsPager.svelte.d.ts} +6 -5
  23. package/packages/core/dist/components/docs/DocsSectionGrid.svelte +26 -0
  24. package/packages/core/dist/components/docs/DocsSectionGrid.svelte.d.ts +21 -0
  25. package/packages/core/dist/components/docs/DocsShell.svelte +19 -0
  26. package/packages/core/dist/components/docs/DocsShell.svelte.d.ts +31 -0
  27. package/packages/core/dist/components/docs/DocsSidebar.svelte +31 -0
  28. package/packages/core/dist/components/docs/DocsSidebar.svelte.d.ts +22 -0
  29. package/packages/core/dist/experience/index.d.ts +7 -0
  30. package/packages/core/dist/experience/index.js +43 -0
  31. package/packages/core/dist/index.d.ts +8 -1
  32. package/packages/core/dist/index.js +5 -0
  33. package/packages/core/dist/server/blog.d.ts +14 -1
  34. package/packages/core/dist/server/blog.js +144 -14
  35. package/packages/core/dist/server/docs.d.ts +44 -0
  36. package/packages/core/dist/server/docs.js +193 -0
  37. package/packages/core/dist/server/index.d.ts +1 -0
  38. package/packages/core/dist/server/index.js +1 -0
  39. package/packages/core/dist/types/blog.d.ts +14 -0
  40. package/packages/core/dist/types/docs.d.ts +28 -0
  41. package/packages/core/dist/types/docs.js +1 -0
  42. package/packages/core/dist/types/experience.d.ts +13 -0
  43. package/packages/core/dist/types/experience.js +1 -0
  44. package/packages/core/dist/theme/ThemeSwitcher.svelte +0 -34
  45. package/packages/core/dist/theme/index.d.ts +0 -2
  46. package/packages/core/dist/theme/index.js +0 -2
  47. package/packages/core/dist/theme/store.d.ts +0 -12
  48. package/packages/core/dist/theme/store.js +0 -50
package/README.md CHANGED
@@ -1,9 +1,17 @@
1
1
  # svelta
2
2
 
3
- A documentation-first 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 @@ A documentation-first 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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aureuma/svelta",
3
3
  "private": false,
4
- "version": "0.1.1",
4
+ "version": "0.2.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "packages/core/dist",
@@ -20,10 +20,14 @@
20
20
  "types": "./packages/core/dist/server/index.d.ts",
21
21
  "default": "./packages/core/dist/server/index.js"
22
22
  },
23
- "./theme": {
24
- "types": "./packages/core/dist/theme/index.d.ts",
25
- "svelte": "./packages/core/dist/theme/index.js",
26
- "default": "./packages/core/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
33
  "types": "./packages/core/dist/index.d.ts",
@@ -39,6 +43,7 @@
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
48
  "postinstall": "npm -w @aureuma/svelta-core run build",
44
49
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@@ -53,6 +58,8 @@
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",
@@ -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
+ }
@@ -5,19 +5,22 @@
5
5
 
6
6
  <a
7
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"
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
9
  >
10
10
  <svg
11
- class="size-4"
11
+ class="size-4 transition-colors"
12
12
  viewBox="0 0 24 24"
13
13
  fill="none"
14
14
  stroke="currentColor"
15
- stroke-width="2"
15
+ stroke-width="1.7"
16
16
  stroke-linecap="round"
17
17
  stroke-linejoin="round"
18
18
  aria-hidden="true"
19
19
  >
20
- <path d="M15 18l-6-6 6-6" />
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
+ />
21
24
  </svg>
22
25
  <span>{label}</span>
23
26
  </a>
@@ -4,34 +4,47 @@
4
4
 
5
5
  export let post: BlogPost;
6
6
  export let variant: 'default' | 'suggestion' = 'default';
7
- $: thumbHeight = variant === 'suggestion' ? 'h-[190px]' : 'h-[280px]';
7
+ $: thumbHeight = variant === 'suggestion' ? 'h-[170px]' : 'h-[236px]';
8
+ $: primaryTag = post.tags[0] ?? post.category.label;
9
+ $: showAuthor = variant === 'default';
8
10
  </script>
9
11
 
10
12
  <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}">
13
+ <div class="relative overflow-hidden rounded-2xl {thumbHeight}">
12
14
  <img
13
15
  src={post.cover}
14
16
  alt={post.title}
15
17
  class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.02]"
16
18
  loading="lazy"
17
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>
18
24
  </div>
19
25
 
20
26
  <div class="mt-4">
21
- <p class="text-xs font-mono uppercase tracking-[0.6px] text-brand">{post.category.label}</p>
27
+ <p class="text-[10px] font-mono uppercase tracking-[0.7px] text-brand">{primaryTag}</p>
22
28
  <h3
23
- class="mt-1 text-xl font-medium leading-[30px] tracking-tight underline-offset-[6px] decoration-border-soft/30 group-hover:underline"
29
+ class="mt-1 text-[19px] font-medium leading-7 tracking-tight underline-offset-[6px] decoration-border-soft/30 group-hover:underline"
24
30
  >
25
31
  {post.title}
26
32
  </h3>
27
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>
28
39
 
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>
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>
34
47
  </div>
35
- </div>
48
+ {/if}
36
49
  </div>
37
50
  </a>
@@ -1,36 +1,52 @@
1
1
  <script lang="ts">
2
2
  import type { BlogPost } from '../../types/blog';
3
+ import Avatar from './Avatar.svelte';
3
4
 
4
5
  export let post: BlogPost;
6
+ $: primaryTag = post.tags[0] ?? post.category.label;
5
7
  </script>
6
8
 
7
9
  <a
8
10
  href={`/blog/${post.slug}`}
9
11
  data-testid="blog-hero"
10
- class="group relative my-12 block h-[480px] overflow-hidden rounded-3xl shadow-drop-md"
12
+ class="group relative my-10 block overflow-hidden rounded-[22px]"
11
13
  >
12
14
  <img
13
15
  src={post.cover}
14
16
  alt={post.title}
15
- class="absolute inset-0 h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.02]"
17
+ class="absolute inset-0 h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.03]"
16
18
  loading="eager"
17
19
  />
18
20
 
19
- <div class="absolute inset-0 bg-gradient-to-t from-black/65 via-black/25 to-black/0"></div>
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>
20
24
 
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>
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>
29
34
 
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>
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>
34
50
  </div>
35
51
  </div>
36
52
  </a>
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
- export let size: '4xl' | '5xl' = '5xl';
3
- $: max = size === '4xl' ? 'max-w-4xl' : 'max-w-5xl';
2
+ export let size: '4xl' | '5xl' | '6xl' = '5xl';
3
+ $: max =
4
+ size === '4xl' ? 'max-w-4xl' : size === '6xl' ? 'max-w-6xl' : 'max-w-5xl';
4
5
  </script>
5
6
 
6
7
  <div class="mx-auto w-full {max} px-6">
@@ -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" />