@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.
- package/README.md +22 -1
- package/package.json +18 -6
- package/packages/core/dist/appearance/AppearanceSwitcher.svelte +58 -0
- package/packages/core/dist/appearance/AppearanceSwitcher.svelte.d.ts +21 -0
- package/packages/core/dist/appearance/index.d.ts +3 -0
- package/packages/core/dist/appearance/index.js +3 -0
- package/packages/core/dist/appearance/palettes.d.ts +10 -0
- package/packages/core/dist/appearance/palettes.js +36 -0
- package/packages/core/dist/appearance/store.d.ts +20 -0
- package/packages/core/dist/appearance/store.js +87 -0
- package/packages/core/dist/components/blog/BackLink.svelte +7 -4
- package/packages/core/dist/components/blog/BlogCard.svelte +23 -10
- package/packages/core/dist/components/blog/BlogHeroCard.svelte +31 -15
- package/packages/core/dist/components/blog/Container.svelte +3 -2
- package/packages/core/dist/components/blog/Container.svelte.d.ts +1 -1
- package/packages/core/dist/components/blog/MorePosts.svelte +1 -1
- package/packages/core/dist/components/blog/ShareButtons.svelte +81 -47
- package/packages/core/dist/components/blog/ShareButtons.svelte.d.ts +2 -0
- package/packages/core/dist/components/blog/TagTabs.svelte +71 -18
- package/packages/core/dist/components/blog/TagTabs.svelte.d.ts +4 -2
- package/packages/core/dist/components/docs/DocsPager.svelte +28 -0
- package/packages/core/dist/{theme/ThemeSwitcher.svelte.d.ts → components/docs/DocsPager.svelte.d.ts} +6 -5
- package/packages/core/dist/components/docs/DocsSectionGrid.svelte +26 -0
- package/packages/core/dist/components/docs/DocsSectionGrid.svelte.d.ts +21 -0
- package/packages/core/dist/components/docs/DocsShell.svelte +19 -0
- package/packages/core/dist/components/docs/DocsShell.svelte.d.ts +31 -0
- package/packages/core/dist/components/docs/DocsSidebar.svelte +31 -0
- package/packages/core/dist/components/docs/DocsSidebar.svelte.d.ts +22 -0
- package/packages/core/dist/experience/index.d.ts +7 -0
- package/packages/core/dist/experience/index.js +43 -0
- package/packages/core/dist/index.d.ts +8 -1
- package/packages/core/dist/index.js +5 -0
- package/packages/core/dist/server/blog.d.ts +14 -1
- package/packages/core/dist/server/blog.js +144 -14
- package/packages/core/dist/server/docs.d.ts +44 -0
- package/packages/core/dist/server/docs.js +193 -0
- package/packages/core/dist/server/index.d.ts +1 -0
- package/packages/core/dist/server/index.js +1 -0
- package/packages/core/dist/types/blog.d.ts +14 -0
- package/packages/core/dist/types/docs.d.ts +28 -0
- package/packages/core/dist/types/docs.js +1 -0
- package/packages/core/dist/types/experience.d.ts +13 -0
- package/packages/core/dist/types/experience.js +1 -0
- package/packages/core/dist/theme/ThemeSwitcher.svelte +0 -34
- package/packages/core/dist/theme/index.d.ts +0 -2
- package/packages/core/dist/theme/index.js +0 -2
- package/packages/core/dist/theme/store.d.ts +0 -12
- package/packages/core/dist/theme/store.js +0 -50
package/README.md
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
# svelta
|
|
2
2
|
|
|
3
|
-
A
|
|
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.
|
|
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
|
-
"./
|
|
24
|
-
"types": "./packages/core/dist/
|
|
25
|
-
"svelte": "./packages/core/dist/
|
|
26
|
-
"default": "./packages/core/dist/
|
|
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,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-
|
|
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="
|
|
15
|
+
stroke-width="1.7"
|
|
16
16
|
stroke-linecap="round"
|
|
17
17
|
stroke-linejoin="round"
|
|
18
18
|
aria-hidden="true"
|
|
19
19
|
>
|
|
20
|
-
<path
|
|
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-[
|
|
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
|
|
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-
|
|
27
|
+
<p class="text-[10px] font-mono uppercase tracking-[0.7px] text-brand">{primaryTag}</p>
|
|
22
28
|
<h3
|
|
23
|
-
class="mt-1 text-
|
|
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
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
<div class="
|
|
33
|
-
|
|
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
|
-
|
|
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
|
+
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.
|
|
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
|
|
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
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 =
|
|
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
|
|
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" />
|