mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-10 06:48:04 -05:00
Updated POSTS to make main 24-12-08
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@skeletonlabs/skeleton": "^2.8.0",
|
||||
"@skeletonlabs/tw-plugin": "^0.3.1",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
@@ -22,13 +23,18 @@
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/node": "^20.10.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
"lucide-svelte": "^0.309.0",
|
||||
"mdsvex": "^0.11.0",
|
||||
"mdsvex": "^0.11.2",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-load-config": "^5.0.2",
|
||||
"postcss-load-config": "^6.0.1",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"shiki": "^1.24.3",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"svelte-inview": "^4.0.4",
|
||||
"svelte-markdown": "^0.4.1",
|
||||
"svelte-reveal": "^1.1.0",
|
||||
"svelte-youtube-embed": "^0.3.3",
|
||||
"svelte-youtube-lite": "^0.6.2",
|
||||
@@ -45,12 +51,13 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"highlight.js": "^11.10.0",
|
||||
"marked": "^15.0.1",
|
||||
"nanoid": "4.0.2",
|
||||
"rehype": "^13.0.2",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"rehype-unwrap-images": "^1.0.0",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"vfile-message": "^4.0.2",
|
||||
"yaml": "^2.6.1",
|
||||
"youtube-transcript": "^1.2.1"
|
||||
},
|
||||
"pnpm": {
|
||||
|
||||
822
web/pnpm-lock.yaml
generated
822
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
<script lang='ts'>
|
||||
import { getToastStore } from '@skeletonlabs/skeleton';
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import Input from '$components/ui/input/input.svelte';
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import { Toast } from '@skeletonlabs/skeleton';
|
||||
|
||||
let url = '';
|
||||
@@ -133,4 +133,4 @@
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
26
web/src/lib/components/contact/Contact.svelte
Normal file
26
web/src/lib/components/contact/Contact.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Contact } from 'lucide-svelte';
|
||||
</script>
|
||||
|
||||
<div class="form-control w-full m-auto p-4 rounded-lg bg-gradient-to-br variant-gradient-success-warning shadow-lg text-current" title="contact form">
|
||||
<h2 class="font-bold pl-2">We'd love to hear from you</h2>
|
||||
<p class="font-bold pl-2">Email</p>
|
||||
<div class="input-group input-group-divider grid-cols-[1fr_auto]">
|
||||
<input type="text" placeholder="Enter an email address where you can be reached..." />
|
||||
</div>
|
||||
<p class="font-bold pl-2">Website</p>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
|
||||
<div class="input-group-shim">https://</div>
|
||||
<input type="text" placeholder="www.example.com" />
|
||||
</div>
|
||||
<p class="font-bold pl-2">Contact Information</p>
|
||||
<div class="input-group input-group-divider grid-cols-[1fr_auto]">
|
||||
<input type="text" placeholder="Enter a other contact information here..." />
|
||||
</div>
|
||||
<label class="label">
|
||||
<span class="font-bold pl-2">Message</span>
|
||||
<textarea class="textarea" rows="4" placeholder="Enter your message ..." />
|
||||
</label>
|
||||
<a href="/" title=""><button class="button variant-filled-secondary rounded-lg p-2"><Contact /></button></a>
|
||||
|
||||
</div>
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
<!-- Note: This can be deleted. It is now in components/ui/connections/Connections.svelte -->
|
||||
|
||||
<!-- <script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { ParticleSystem } from './ParticleSystem';
|
||||
import { createParticleGradient } from '$lib/utils/canvas';
|
||||
@@ -136,4 +138,4 @@
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
</style> -->
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Sun, Moon, Menu, X, Github } from 'lucide-svelte';
|
||||
import { Avatar } from '@skeletonlabs/skeleton';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { theme, toggleTheme } from '$lib/store/theme';
|
||||
import { theme, cycleTheme, initTheme } from '$lib/store/theme';
|
||||
// import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
|
||||
$: currentPath = $page.url.pathname;
|
||||
$: isDarkMode = $theme === 'dark';
|
||||
$: isDarkMode = $theme === 'my-custom-theme';
|
||||
|
||||
const navItems = [
|
||||
{ href: '/', label: 'Home' },
|
||||
@@ -30,10 +30,9 @@
|
||||
{ href: '/about', label: 'About' },
|
||||
];
|
||||
|
||||
onMount(() => {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
theme.setTheme(prefersDark ? 'dark' : 'light');
|
||||
});
|
||||
onMount(() => {
|
||||
initTheme();
|
||||
});
|
||||
</script>
|
||||
|
||||
<header class="fixed top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
@@ -75,9 +74,9 @@
|
||||
<Github class="h-4 w-4" />
|
||||
<span class="sr-only">GitHub</span>
|
||||
</button>
|
||||
|
||||
|
||||
<button name="toggle-theme"
|
||||
on:click={toggleTheme}
|
||||
on:click={cycleTheme}
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-full border bg-background text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
@@ -121,4 +120,4 @@
|
||||
</nav>
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
</header>
|
||||
|
||||
@@ -1,110 +1,112 @@
|
||||
import type { Particle } from '$lib/types/interfaces/particle';
|
||||
import { generateGradientColor } from '$lib/utils/colors';
|
||||
// Note: This can be deleted. It is now in components/ui/connections/ParticleSystem.ts
|
||||
|
||||
export class ParticleSystem {
|
||||
private particles: Particle[] = [];
|
||||
private width: number;
|
||||
private height: number;
|
||||
private mouseX: number = 0;
|
||||
private mouseY: number = 0;
|
||||
private targetMouseX: number = 0;
|
||||
private targetMouseY: number = 0;
|
||||
|
||||
constructor(
|
||||
private readonly count: number,
|
||||
private readonly baseSize: number,
|
||||
private readonly speed: number,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.mouseX = width / 2;
|
||||
this.mouseY = height / 2;
|
||||
this.targetMouseX = this.mouseX;
|
||||
this.targetMouseY = this.mouseY;
|
||||
this.initParticles();
|
||||
}
|
||||
|
||||
private initParticles(): void {
|
||||
this.particles = [];
|
||||
for (let i = 0; i < this.count; i++) {
|
||||
// Distribute particles across the entire width
|
||||
const x = Math.random() * this.width;
|
||||
// Distribute particles vertically around the middle with some variation
|
||||
const yOffset = (Math.random() - 0.5) * 100;
|
||||
|
||||
this.particles.push({
|
||||
x,
|
||||
y: this.height / 2 + yOffset,
|
||||
baseY: this.height / 2 + yOffset,
|
||||
speed: (Math.random() - 0.5) * this.speed * 0.5, // Reduced base speed
|
||||
angle: Math.random() * Math.PI * 2,
|
||||
size: this.baseSize * (0.8 + Math.random() * 0.4),
|
||||
color: generateGradientColor(this.height / 2 + yOffset, this.height),
|
||||
velocityX: (Math.random() - 0.5) * this.speed // Reduced initial velocity
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public updateDimensions(width: number, height: number): void {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.mouseX = width / 2;
|
||||
this.mouseY = height / 2;
|
||||
this.targetMouseX = this.mouseX;
|
||||
this.targetMouseY = this.mouseY;
|
||||
this.initParticles();
|
||||
}
|
||||
|
||||
public updateMousePosition(x: number, y: number): void {
|
||||
this.targetMouseX = x;
|
||||
this.targetMouseY = y;
|
||||
}
|
||||
|
||||
public update(): void {
|
||||
// Smooth mouse movement
|
||||
this.mouseX += (this.targetMouseX - this.mouseX) * 0.05; // Slower mouse tracking
|
||||
this.mouseY += (this.targetMouseY - this.mouseY) * 0.05;
|
||||
|
||||
this.particles.forEach(particle => {
|
||||
// Update horizontal position with constant motion
|
||||
particle.x += particle.velocityX;
|
||||
|
||||
// Wave motion
|
||||
particle.angle += particle.speed;
|
||||
const waveAmplitude = 30 * (this.mouseY / this.height); // Reduced amplitude
|
||||
const frequencyFactor = (this.mouseX / this.width);
|
||||
|
||||
// Calculate vertical position with wave effect
|
||||
particle.y = particle.baseY +
|
||||
Math.sin(particle.angle * frequencyFactor + particle.x * 0.01) * // Slower wave
|
||||
waveAmplitude;
|
||||
|
||||
// Update particle color based on position
|
||||
particle.color = generateGradientColor(particle.y, this.height);
|
||||
|
||||
// Screen wrapping with position preservation
|
||||
if (particle.x < 0) {
|
||||
particle.x = this.width;
|
||||
particle.baseY = this.height / 2 + (Math.random() - 0.5) * 100;
|
||||
}
|
||||
if (particle.x > this.width) {
|
||||
particle.x = 0;
|
||||
particle.baseY = this.height / 2 + (Math.random() - 0.5) * 100;
|
||||
}
|
||||
|
||||
// Very subtle velocity adjustment to maintain spread
|
||||
if (Math.abs(particle.velocityX) < 0.1) {
|
||||
particle.velocityX += (Math.random() - 0.5) * 0.02;
|
||||
}
|
||||
|
||||
// Gentle velocity dampening
|
||||
particle.velocityX *= 0.99;
|
||||
});
|
||||
}
|
||||
|
||||
public getParticles(): Particle[] {
|
||||
return this.particles;
|
||||
}
|
||||
}
|
||||
//import type { Particle } from '$lib/types/interfaces/particle';
|
||||
//import { generateGradientColor } from '$lib/utils/colors';
|
||||
//
|
||||
//export class ParticleSystem {
|
||||
// private particles: Particle[] = [];
|
||||
// private width: number;
|
||||
// private height: number;
|
||||
// private mouseX: number = 0;
|
||||
// private mouseY: number = 0;
|
||||
// private targetMouseX: number = 0;
|
||||
// private targetMouseY: number = 0;
|
||||
//
|
||||
// constructor(
|
||||
// private readonly count: number,
|
||||
// private readonly baseSize: number,
|
||||
// private readonly speed: number,
|
||||
// width: number,
|
||||
// height: number
|
||||
// ) {
|
||||
// this.width = width;
|
||||
// this.height = height;
|
||||
// this.mouseX = width / 2;
|
||||
// this.mouseY = height / 2;
|
||||
// this.targetMouseX = this.mouseX;
|
||||
// this.targetMouseY = this.mouseY;
|
||||
// this.initParticles();
|
||||
// }
|
||||
//
|
||||
// private initParticles(): void {
|
||||
// this.particles = [];
|
||||
// for (let i = 0; i < this.count; i++) {
|
||||
// // Distribute particles across the entire width
|
||||
// const x = Math.random() * this.width;
|
||||
// // Distribute particles vertically around the middle with some variation
|
||||
// const yOffset = (Math.random() - 0.5) * 100;
|
||||
//
|
||||
// this.particles.push({
|
||||
// x,
|
||||
// y: this.height / 2 + yOffset,
|
||||
// baseY: this.height / 2 + yOffset,
|
||||
// speed: (Math.random() - 0.5) * this.speed * 0.5, // Reduced base speed
|
||||
// angle: Math.random() * Math.PI * 2,
|
||||
// size: this.baseSize * (0.8 + Math.random() * 0.4),
|
||||
// color: generateGradientColor(this.height / 2 + yOffset, this.height),
|
||||
// velocityX: (Math.random() - 0.5) * this.speed // Reduced initial velocity
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public updateDimensions(width: number, height: number): void {
|
||||
// this.width = width;
|
||||
// this.height = height;
|
||||
// this.mouseX = width / 2;
|
||||
// this.mouseY = height / 2;
|
||||
// this.targetMouseX = this.mouseX;
|
||||
// this.targetMouseY = this.mouseY;
|
||||
// this.initParticles();
|
||||
// }
|
||||
//
|
||||
// public updateMousePosition(x: number, y: number): void {
|
||||
// this.targetMouseX = x;
|
||||
// this.targetMouseY = y;
|
||||
// }
|
||||
//
|
||||
// public update(): void {
|
||||
// // Smooth mouse movement
|
||||
// this.mouseX += (this.targetMouseX - this.mouseX) * 0.05; // Slower mouse tracking
|
||||
// this.mouseY += (this.targetMouseY - this.mouseY) * 0.05;
|
||||
//
|
||||
// this.particles.forEach(particle => {
|
||||
// // Update horizontal position with constant motion
|
||||
// particle.x += particle.velocityX;
|
||||
//
|
||||
// // Wave motion
|
||||
// particle.angle += particle.speed;
|
||||
// const waveAmplitude = 30 * (this.mouseY / this.height); // Reduced amplitude
|
||||
// const frequencyFactor = (this.mouseX / this.width);
|
||||
//
|
||||
// // Calculate vertical position with wave effect
|
||||
// particle.y = particle.baseY +
|
||||
// Math.sin(particle.angle * frequencyFactor + particle.x * 0.01) * // Slower wave
|
||||
// waveAmplitude;
|
||||
//
|
||||
// // Update particle color based on position
|
||||
// particle.color = generateGradientColor(particle.y, this.height);
|
||||
//
|
||||
// // Screen wrapping with position preservation
|
||||
// if (particle.x < 0) {
|
||||
// particle.x = this.width;
|
||||
// particle.baseY = this.height / 2 + (Math.random() - 0.5) * 100;
|
||||
// }
|
||||
// if (particle.x > this.width) {
|
||||
// particle.x = 0;
|
||||
// particle.baseY = this.height / 2 + (Math.random() - 0.5) * 100;
|
||||
// }
|
||||
//
|
||||
// // Very subtle velocity adjustment to maintain spread
|
||||
// if (Math.abs(particle.velocityX) < 0.1) {
|
||||
// particle.velocityX += (Math.random() - 0.5) * 0.02;
|
||||
// }
|
||||
//
|
||||
// // Gentle velocity dampening
|
||||
// particle.velocityX *= 0.99;
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// public getParticles(): Particle[] {
|
||||
// return this.particles;
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
<!-- This file can be deleted. It is now in ui/components/terminal/Terminal.svelte -->
|
||||
|
||||
<!-- <script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
// import { fade } from 'svelte/transition';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -101,8 +103,9 @@
|
||||
<div class="pt-2 pb-8 px-4">
|
||||
<div class="container mx-auto max-w-4xl">
|
||||
<div class="terminal-window backdrop-blur-sm">
|
||||
<!-- Terminal header -->
|
||||
<div class="terminal-header flex items-center gap-2 px-4 py-2 border-b border-gray-700/50">
|
||||
<!-- Terminal header
|
||||
Comment Continued -->
|
||||
<!-- <div class="terminal-header flex items-center gap-2 px-4 py-2 border-b border-gray-700/50">
|
||||
<div class="flex gap-2">
|
||||
<div class="w-3 h-3 rounded-full bg-red-500/80"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-yellow-500/80"></div>
|
||||
@@ -114,14 +117,15 @@
|
||||
<div class="p-6">
|
||||
<div class="mb-4 whitespace-pre-wrap terminal-text leading-relaxed">{terminalContent}</div>
|
||||
|
||||
<!-- Command input -->
|
||||
{#if mounted}
|
||||
<!-- Command input
|
||||
Comment Continued -->
|
||||
<!-- {#if mounted}
|
||||
<div class="flex items-center command-input">
|
||||
<span class="mr-2 terminal-prompt font-bold">$</span>
|
||||
<!-- {#if showCursor}
|
||||
<span class="animate-blink terminal-text">▋</span>
|
||||
{/if} -->
|
||||
<input
|
||||
<!-- <input
|
||||
type="text"
|
||||
bind:value={currentCommand}
|
||||
on:keydown={handleKeydown}
|
||||
@@ -177,4 +181,4 @@
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-gray-600/50 rounded-full hover:bg-gray-500/50 transition-colors;
|
||||
}
|
||||
</style>
|
||||
</style> -->
|
||||
|
||||
52
web/src/lib/components/posts/PostCard.svelte
Normal file
52
web/src/lib/components/posts/PostCard.svelte
Normal file
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { formatDistance } from 'date-fns';
|
||||
import type { Post } from '$lib/interfaces/post-interface';
|
||||
import PostMeta from './PostMeta.svelte';
|
||||
import Card from '$lib/components/ui/cards/card.svelte';
|
||||
import { cn } from '$lib/utils/utils';
|
||||
|
||||
export let post: Post;
|
||||
export let className: string = '';
|
||||
|
||||
function parseDate(dateStr: string): Date {
|
||||
// Handle both ISO strings and YYYY-MM-DD formats
|
||||
return new Date(dateStr);
|
||||
}
|
||||
</script>
|
||||
|
||||
<article class="card card-hover group relative rounded-lg border p-6 hover:bg-primary-500/50 {className}">
|
||||
<a
|
||||
href="/posts/{post.slug}"
|
||||
class="absolute inset-0"
|
||||
data-sveltekit-preload-data="off"
|
||||
>
|
||||
<span class="sr-only">View {post.metadata?.title}</span>
|
||||
</a>
|
||||
<div class="flex flex-col justify-between space-y-4">
|
||||
<div class="space-y-2">
|
||||
<img src={post.metadata?.images?.[0]} alt="" class="rounded-lg" />
|
||||
<h2 class="text-xl font-semibold tracking-tight">{post.metadata?.title}</h2>
|
||||
<p class="text-muted-foreground">{post.metadata?.description}</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4 text-sm text-muted-foreground">
|
||||
<time datetime={post.metadata?.date}>
|
||||
{#if post.metadata?.date}
|
||||
{formatDistance(parseDate(post.metadata.date), new Date(), { addSuffix: false })}
|
||||
{/if}
|
||||
</time>
|
||||
{#if post.metadata?.tags?.length > 0}
|
||||
<span class="text-xs">•</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each post.metadata?.tags as tag}
|
||||
<a
|
||||
href="/tags/{tag}"
|
||||
class="inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-semibold transition-colors hover:bg-secondary"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
37
web/src/lib/components/posts/PostContent.svelte
Normal file
37
web/src/lib/components/posts/PostContent.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import PostMeta from './PostMeta.svelte';
|
||||
import type { Post } from '$lib/interfaces/post-interface'
|
||||
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
||||
import SideNav from '$lib/components/ui/side-nav/SideNav.svelte';
|
||||
|
||||
export let post: Post;
|
||||
</script>
|
||||
|
||||
<article class="py-6">
|
||||
{#if !post?.content || !post?.metadata}
|
||||
<div class="flex min-h-[400px] items-center justify-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Spinner class="h-6 w-6" />
|
||||
<span class="text-sm text-muted-foreground">Loading post...</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-4 pl-8 ml-8">
|
||||
<h1 class="inline-block text-4xl font-bold inherit-colors lg:text-5xl">{post.metadata.title}</h1>
|
||||
<PostMeta data={post.metadata} />
|
||||
</div>
|
||||
<div class="items-center py-8 mx-auto grid-cols-[80%_20%] grid gap-8 max-w-7xl relative prose prose-slate dark:prose-invert">
|
||||
{#if typeof post.content === 'function'}
|
||||
<SideNav />
|
||||
<svelte:component this={post.content} />
|
||||
{:else if typeof post.content === 'string'}
|
||||
{post.content}
|
||||
{:else}
|
||||
<div class="flex gap-2">
|
||||
<Spinner class="h-8 w-8" />
|
||||
<span class="text-sm text-muted-foreground">Loading content...</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
54
web/src/lib/components/posts/PostLayout.svelte
Normal file
54
web/src/lib/components/posts/PostLayout.svelte
Normal file
@@ -0,0 +1,54 @@
|
||||
<script>
|
||||
export let aliases
|
||||
export let date
|
||||
export let tags
|
||||
export let title
|
||||
export let description
|
||||
export let author
|
||||
export let updated
|
||||
//export let content
|
||||
</script>
|
||||
|
||||
<article class="prose prose-slate dark:prose-invert max-w-5xl flex-1">
|
||||
<h1 class="inline-block text-4xl font-bold inherit-colors lg:text-5xl">{aliases}</h1>
|
||||
<slot />
|
||||
</article>
|
||||
|
||||
<style lang="postcss">
|
||||
:global(textarea) {
|
||||
@apply textarea;
|
||||
}
|
||||
|
||||
:global(h1) {
|
||||
@apply h1;
|
||||
}
|
||||
|
||||
:global(h2) {
|
||||
@apply h2;
|
||||
}
|
||||
|
||||
:global(ul) {
|
||||
@layer ul;
|
||||
}
|
||||
|
||||
:global(li) {
|
||||
@layer li;
|
||||
}
|
||||
|
||||
:global(a) {
|
||||
@layer a;
|
||||
}
|
||||
|
||||
:global(img) {
|
||||
@layer img;
|
||||
}
|
||||
|
||||
:global(blockquote) {
|
||||
@layer blockquote;
|
||||
}
|
||||
|
||||
:global(code) {
|
||||
@layer code;
|
||||
}
|
||||
|
||||
</style>
|
||||
34
web/src/lib/components/posts/PostMeta.svelte
Normal file
34
web/src/lib/components/posts/PostMeta.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { formatDistance } from 'date-fns';
|
||||
import type { PostMetadata } from '$lib/interfaces/post-interface';
|
||||
|
||||
export let data: PostMetadata;
|
||||
export let showUpdated = true;
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2 text-sm text-muted-foreground">
|
||||
<div class="flex items-center gap-2">
|
||||
<span><b>Published on: </b>{data.date}</span>
|
||||
{#if showUpdated && data.updated !== data.date}
|
||||
<span>· Updated on {data.updated}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if data.author}
|
||||
<div class="text-xs font-bold">By {data.author}</div>
|
||||
{/if}
|
||||
{#if data.description}
|
||||
<p class="text-base font-bold">{data.description}</p>
|
||||
{/if}
|
||||
{#if data.tags && data.tags.length > 0}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each data.tags as tag}
|
||||
<a
|
||||
href="/tags/{tag}"
|
||||
class="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors hover:bg-muted/50"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
139
web/src/lib/components/ui/connections/Connections.svelte
Normal file
139
web/src/lib/components/ui/connections/Connections.svelte
Normal file
@@ -0,0 +1,139 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { ParticleSystem } from './ParticleSystem';
|
||||
import { createParticleGradient } from '$lib/components/ui/connections/canvas';
|
||||
|
||||
export let particleCount = 100;
|
||||
export let particleSize = 3;
|
||||
export let particleSpeed = 0.5;
|
||||
export let connectionDistance = 100;
|
||||
|
||||
let canvas: HTMLCanvasElement;
|
||||
let ctx: CanvasRenderingContext2D;
|
||||
let animationFrame: number;
|
||||
let particleSystem: ParticleSystem;
|
||||
let isMouseOver = false;
|
||||
let browser = false;
|
||||
|
||||
function handleMouseMove(event: MouseEvent) {
|
||||
if (!isMouseOver) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
particleSystem?.updateMousePosition(x, y);
|
||||
}
|
||||
|
||||
function handleMouseEnter() {
|
||||
isMouseOver = true;
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
isMouseOver = false;
|
||||
const centerX = canvas.width / 2;
|
||||
const centerY = canvas.height / 2;
|
||||
particleSystem?.updateMousePosition(centerX, centerY);
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
if (!browser) return;
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
particleSystem?.updateDimensions(canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
function drawConnections() {
|
||||
const particles = particleSystem.getParticles();
|
||||
ctx.lineWidth = 0.5;
|
||||
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
const dx = particles[i].x - particles[j].x;
|
||||
const dy = particles[i].y - particles[j].y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < connectionDistance) {
|
||||
const alpha = 1 - (distance / connectionDistance);
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.15})`; // Slightly reduced opacity
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(particles[i].x, particles[i].y);
|
||||
ctx.lineTo(particles[j].x, particles[j].y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawParticles() {
|
||||
const particles = particleSystem.getParticles();
|
||||
|
||||
particles.forEach(particle => {
|
||||
const gradient = createParticleGradient(
|
||||
ctx,
|
||||
particle.x,
|
||||
particle.y,
|
||||
particle.size,
|
||||
particle.color
|
||||
);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
function animate() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
particleSystem.update();
|
||||
drawConnections();
|
||||
drawParticles();
|
||||
|
||||
animationFrame = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
browser = true;
|
||||
if (!browser) return;
|
||||
|
||||
ctx = canvas.getContext('2d')!;
|
||||
handleResize();
|
||||
|
||||
particleSystem = new ParticleSystem(
|
||||
particleCount,
|
||||
particleSize,
|
||||
particleSpeed,
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
animationFrame = requestAnimationFrame(animate);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (!browser) return;
|
||||
window.removeEventListener('resize', handleResize);
|
||||
cancelAnimationFrame(animationFrame);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<canvas
|
||||
bind:this={canvas}
|
||||
on:mousemove={handleMouseMove}
|
||||
on:mouseenter={handleMouseEnter}
|
||||
on:mouseleave={handleMouseLeave}
|
||||
class="particle-wave"
|
||||
/>
|
||||
|
||||
<style>
|
||||
.particle-wave {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
110
web/src/lib/components/ui/connections/ParticleSystem.ts
Normal file
110
web/src/lib/components/ui/connections/ParticleSystem.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { Particle } from './particle';
|
||||
import { generateGradientColor } from './colors';
|
||||
|
||||
export class ParticleSystem {
|
||||
private particles: Particle[] = [];
|
||||
private width: number;
|
||||
private height: number;
|
||||
private mouseX: number = 0;
|
||||
private mouseY: number = 0;
|
||||
private targetMouseX: number = 0;
|
||||
private targetMouseY: number = 0;
|
||||
|
||||
constructor(
|
||||
private readonly count: number,
|
||||
private readonly baseSize: number,
|
||||
private readonly speed: number,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.mouseX = width / 2;
|
||||
this.mouseY = height / 2;
|
||||
this.targetMouseX = this.mouseX;
|
||||
this.targetMouseY = this.mouseY;
|
||||
this.initParticles();
|
||||
}
|
||||
|
||||
private initParticles(): void {
|
||||
this.particles = [];
|
||||
for (let i = 0; i < this.count; i++) {
|
||||
// Distribute particles across the entire width
|
||||
const x = Math.random() * this.width;
|
||||
// Distribute particles vertically around the middle with some variation
|
||||
const yOffset = (Math.random() - 0.5) * 100;
|
||||
|
||||
this.particles.push({
|
||||
x,
|
||||
y: this.height / 2 + yOffset,
|
||||
baseY: this.height / 2 + yOffset,
|
||||
speed: (Math.random() - 0.5) * this.speed * 0.5, // Reduced base speed
|
||||
angle: Math.random() * Math.PI * 2,
|
||||
size: this.baseSize * (0.8 + Math.random() * 0.4),
|
||||
color: generateGradientColor(this.height / 2 + yOffset, this.height),
|
||||
velocityX: (Math.random() - 0.5) * this.speed // Reduced initial velocity
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public updateDimensions(width: number, height: number): void {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.mouseX = width / 2;
|
||||
this.mouseY = height / 2;
|
||||
this.targetMouseX = this.mouseX;
|
||||
this.targetMouseY = this.mouseY;
|
||||
this.initParticles();
|
||||
}
|
||||
|
||||
public updateMousePosition(x: number, y: number): void {
|
||||
this.targetMouseX = x;
|
||||
this.targetMouseY = y;
|
||||
}
|
||||
|
||||
public update(): void {
|
||||
// Smooth mouse movement
|
||||
this.mouseX += (this.targetMouseX - this.mouseX) * 0.05; // Slower mouse tracking
|
||||
this.mouseY += (this.targetMouseY - this.mouseY) * 0.05;
|
||||
|
||||
this.particles.forEach(particle => {
|
||||
// Update horizontal position with constant motion
|
||||
particle.x += particle.velocityX;
|
||||
|
||||
// Wave motion
|
||||
particle.angle += particle.speed;
|
||||
const waveAmplitude = 30 * (this.mouseY / this.height); // Reduced amplitude
|
||||
const frequencyFactor = (this.mouseX / this.width);
|
||||
|
||||
// Calculate vertical position with wave effect
|
||||
particle.y = particle.baseY +
|
||||
Math.sin(particle.angle * frequencyFactor + particle.x * 0.01) * // Slower wave
|
||||
waveAmplitude;
|
||||
|
||||
// Update particle color based on position
|
||||
particle.color = generateGradientColor(particle.y, this.height);
|
||||
|
||||
// Screen wrapping with position preservation
|
||||
if (particle.x < 0) {
|
||||
particle.x = this.width;
|
||||
particle.baseY = this.height / 2 + (Math.random() - 0.5) * 100;
|
||||
}
|
||||
if (particle.x > this.width) {
|
||||
particle.x = 0;
|
||||
particle.baseY = this.height / 2 + (Math.random() - 0.5) * 100;
|
||||
}
|
||||
|
||||
// Very subtle velocity adjustment to maintain spread
|
||||
if (Math.abs(particle.velocityX) < 0.1) {
|
||||
particle.velocityX += (Math.random() - 0.5) * 0.02;
|
||||
}
|
||||
|
||||
// Gentle velocity dampening
|
||||
particle.velocityX *= 0.99;
|
||||
});
|
||||
}
|
||||
|
||||
public getParticles(): Particle[] {
|
||||
return this.particles;
|
||||
}
|
||||
}
|
||||
12
web/src/lib/components/ui/connections/canvas.ts
Normal file
12
web/src/lib/components/ui/connections/canvas.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export function createParticleGradient(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
size: number,
|
||||
color: string
|
||||
): CanvasGradient {
|
||||
const gradient = ctx.createRadialGradient(x, y, 0, x, y, size);
|
||||
gradient.addColorStop(0, color);
|
||||
gradient.addColorStop(1, 'transparent');
|
||||
return gradient;
|
||||
}
|
||||
4
web/src/lib/components/ui/connections/colors.ts
Normal file
4
web/src/lib/components/ui/connections/colors.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export function generateGradientColor(y: number, height: number): string {
|
||||
const hue = (y / height) * 60 + 200; // Blue to purple range
|
||||
return `hsla(${hue}, 70%, 60%, 0.8)`;
|
||||
}
|
||||
10
web/src/lib/components/ui/connections/particle.ts
Normal file
10
web/src/lib/components/ui/connections/particle.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
baseY: number;
|
||||
speed: number;
|
||||
angle: number;
|
||||
size: number;
|
||||
color: string;
|
||||
velocityX: number;
|
||||
}
|
||||
44
web/src/lib/components/ui/side-nav/SideNav.svelte
Normal file
44
web/src/lib/components/ui/side-nav/SideNav.svelte
Normal file
@@ -0,0 +1,44 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let toc = [];
|
||||
|
||||
onMount(() => {
|
||||
// Get all headings from the content
|
||||
const article = document.querySelector('article');
|
||||
if (article) {
|
||||
const headings = article.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
toc = Array.from(headings).map(heading => ({
|
||||
id: heading.id,
|
||||
text: heading.textContent,
|
||||
level: parseInt(heading.tagName.charAt(1))
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
function scrollToSection(id) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="hidden lg:block w-64 fixed top-24 right-[max(0px,calc(50%-45rem))] max-h-[calc(80vh-5rem)] overflow-y-auto">
|
||||
<div class="p-4 bg-card text-card-foreground">
|
||||
<h4 class="font-semibold mb-4">On this page</h4>
|
||||
<ul class="space-y-2">
|
||||
{#each toc.filter(item => item.text !== 'On this page') as item}
|
||||
<li style="margin-left: {(item.level - 1) * 1}rem">
|
||||
<a
|
||||
href="#{item.id}"
|
||||
class="text-xs hover:text-primary transition-colors"
|
||||
on:click|preventDefault={() => scrollToSection(item.id)}
|
||||
>
|
||||
{item.text}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
180
web/src/lib/components/ui/terminal/Terminal.svelte
Normal file
180
web/src/lib/components/ui/terminal/Terminal.svelte
Normal file
@@ -0,0 +1,180 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
// import { fade } from 'svelte/transition';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let mounted = false;
|
||||
let currentCommand = '';
|
||||
let commandHistory: string[] = [];
|
||||
let showCursor = true;
|
||||
|
||||
let terminalContent = '';
|
||||
let typing = false;
|
||||
|
||||
const pages = {
|
||||
home: 'Welcome to Fabric\n\nType `help` to see available commands.',
|
||||
about: 'About Fabric',
|
||||
chat: 'Enter `chat` to start a chat session.',
|
||||
posts: 'Enter `posts` to view blog posts.',
|
||||
tags: 'Enter `tags` to view tags.',
|
||||
contact: 'Enter `contact` to view contact info.',
|
||||
help: `Available commands:
|
||||
- help: Show this help message
|
||||
- about: Navigate to About page
|
||||
- chat: Start a chat session
|
||||
- posts: View all blog posts
|
||||
- tags: Browse content by tags
|
||||
- contact: Get in touch
|
||||
- clear: Clear the terminal
|
||||
- ls: List available pages`,
|
||||
};
|
||||
|
||||
// Simulate typing effect
|
||||
async function typeContent(content: string) {
|
||||
typing = true;
|
||||
terminalContent = '';
|
||||
for (const char of content) {
|
||||
terminalContent += char;
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
}
|
||||
typing = false;
|
||||
}
|
||||
|
||||
function handleCommand(cmd: string) {
|
||||
commandHistory = [...commandHistory, cmd];
|
||||
|
||||
switch (cmd) {
|
||||
case 'clear':
|
||||
terminalContent = '';
|
||||
break;
|
||||
case 'help':
|
||||
typeContent(pages.help);
|
||||
break;
|
||||
case 'about':
|
||||
goto('/about');
|
||||
break;
|
||||
case 'chat':
|
||||
goto('/chat');
|
||||
break;
|
||||
case 'posts':
|
||||
goto('/posts');
|
||||
break;
|
||||
case 'tags':
|
||||
goto('/tags');
|
||||
break;
|
||||
case 'contact':
|
||||
goto('/contact');
|
||||
break;
|
||||
case 'ls':
|
||||
typeContent(Object.keys(pages).join('\n'));
|
||||
break;
|
||||
default:
|
||||
const page = cmd.slice(3);
|
||||
if (pages[page]) {
|
||||
typeContent(pages[page]);
|
||||
} else {
|
||||
typeContent(`Error: Page '${page}' not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (typing) return;
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
handleCommand(currentCommand.trim());
|
||||
currentCommand = '';
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
setInterval(() => {
|
||||
showCursor = !showCursor;
|
||||
}, 500);
|
||||
|
||||
// Initial content
|
||||
typeContent(pages.home);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="pt-2 pb-8 px-4">
|
||||
<div class="container mx-auto max-w-4xl">
|
||||
<div class="terminal-window backdrop-blur-sm">
|
||||
<!-- Terminal header -->
|
||||
<div class="terminal-header flex items-center gap-2 px-4 py-2 border-b border-gray-700/50">
|
||||
<div class="flex gap-2">
|
||||
<div class="w-3 h-3 rounded-full bg-red-500/80"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-yellow-500/80"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-green-500/80"></div>
|
||||
</div>
|
||||
<span class="text-sm text-gray-400 ml-2">me@localhost</span>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="mb-4 whitespace-pre-wrap terminal-text leading-relaxed">{terminalContent}</div>
|
||||
|
||||
<!-- Command input -->
|
||||
{#if mounted}
|
||||
<div class="flex items-center command-input">
|
||||
<span class="mr-2 terminal-prompt font-bold">$</span>
|
||||
<!-- {#if showCursor}
|
||||
<span class="animate-blink terminal-text">▋</span>
|
||||
{/if} -->
|
||||
<input
|
||||
type="text"
|
||||
bind:value={currentCommand}
|
||||
on:keydown={handleKeydown}
|
||||
class="flex-1 bg-transparent border-none outline-none terminal-text caret-primary-500"
|
||||
placeholder="Type a command..."
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.terminal-window {
|
||||
@apply rounded-lg border border-gray-700/50 bg-gray-900/95 shadow-2xl;
|
||||
box-shadow: 0 0 60px -15px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.terminal-text {
|
||||
@apply font-mono text-green-400/90;
|
||||
}
|
||||
|
||||
.terminal-prompt {
|
||||
@apply text-blue-400/90;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
@apply text-gray-600;
|
||||
}
|
||||
|
||||
/* .animate-blink {
|
||||
animation: blink 1s step-end infinite;
|
||||
flex-col: 1;
|
||||
|
||||
}*/
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
@apply w-2;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-gray-800/50 rounded-full;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-gray-600/50 rounded-full hover:bg-gray-500/50 transition-colors;
|
||||
}
|
||||
</style>
|
||||
21
web/src/lib/content/posts/Contact.md
Normal file
21
web/src/lib/content/posts/Contact.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: "Contact"
|
||||
description: "Default Contact Page"
|
||||
date: 2024-11-24
|
||||
---
|
||||
|
||||
# THIS CAN BE DELETED. IT IS NOT IN USE RIGHT NOW.
|
||||
|
||||
## It is only here to show tables and importing components into markdown files.
|
||||
|
||||
| Contacts | Name | Email |
|
||||
| ------------- | -------------- | -------------- |
|
||||
| ID | Item1 | Item1 |
|
||||
|
||||
<script>
|
||||
import Contact from '$lib/components/contact/Contact.svelte';
|
||||
</script>
|
||||
|
||||
> **This is a placeholder contact page. No logic is implemented here.**
|
||||
|
||||
<Contact />
|
||||
@@ -1,8 +1,12 @@
|
||||
---
|
||||
title: Extract Wisdom
|
||||
date: 2024-01-01
|
||||
description:
|
||||
tags: [patterns, fabric]
|
||||
description: Something here
|
||||
updated:
|
||||
aliases: Extract Wisdom
|
||||
tags:
|
||||
- patterns
|
||||
- fabric
|
||||
---
|
||||
|
||||
# IDENTITY and PURPOSE
|
||||
@@ -21,7 +25,7 @@ Take a step back and think step-by-step about how to achieve the best possible r
|
||||
|
||||
- Extract 15 to 30 of the most surprising, insightful, and/or interesting quotes from the input into a section called QUOTES:. Use the exact quote text from the input.
|
||||
|
||||
- Extract 15 to 30 of the most practical and useful personal habits of the speakers, or mentioned by the speakers, in the content into a section called HABITS. Examples include but aren't limited to: sleep schedule, reading habits, things they always do, things they always avoid, productivity tips, diet, exercise, etc.
|
||||
- Extract 15 to 30 of the most practical and useful personal habits of the speakers, or mentioned by the speakers, in the content into a section called HABITS. Examples include but aren't limited to sleep schedule, reading habits, things they always do, things they always avoid, productivity tips, diet, exercise, etc.
|
||||
|
||||
- Extract 15 to 30 of the most surprising, insightful, and/or interesting valid facts about the greater world that were mentioned in the content into a section called FACTS:.
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
---
|
||||
title: Getting Started with SvelteKit
|
||||
aliases: [SvelteKit for Beginners]
|
||||
date: 2024-11-01
|
||||
description: How to get started with SvelteKit
|
||||
aliases: SvelteKit for Beginners
|
||||
date: '2024-11-01'
|
||||
updated:
|
||||
tags:
|
||||
- getting-started
|
||||
- sveltekit
|
||||
---
|
||||
|
||||
SvelteKit is a framework for building web applications of all sizes, with a beautiful development experience and flexible filesystem-based routing.
|
||||
@@ -13,7 +18,7 @@ SvelteKit is a framework for building web applications of all sizes, with a beau
|
||||
- Server-side rendering
|
||||
- Hot module replacement
|
||||
|
||||
```bash
|
||||
```shell
|
||||
npx sv create my-app
|
||||
cd my-app
|
||||
npm install
|
||||
@@ -21,13 +26,13 @@ npm install
|
||||
|
||||
**Install SkeletonUI**
|
||||
|
||||
```bash
|
||||
```
|
||||
npm i -D @skeletonlabs/skeleton@next @skeletonlabs/skeleton-svelte@next
|
||||
```
|
||||
|
||||
**Configure Tailwind CSS**
|
||||
|
||||
```tailwind.config
|
||||
``` ts
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
import { skeleton, contentPath } from '@skeletonlabs/skeleton/plugin';
|
||||
@@ -56,4 +61,4 @@ export default {
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Read more at https://svelte.dev, https://next.skeleton.dev/docs/get-started/installation/sveltekit, and https://www.skeleton.dev/docs/introduction
|
||||
Read more at https://svelte.dev, https://next.skeleton.dev/docs/get-started/installation/sveltekit, and https://www.skeleton.dev/docs/introduction
|
||||
|
||||
734
web/src/lib/content/posts/mdsvex-docs.md
Normal file
734
web/src/lib/content/posts/mdsvex-docs.md
Normal file
@@ -0,0 +1,734 @@
|
||||
---
|
||||
title: mdsvex - svelte in markdown
|
||||
description: mdsvex is a markdown preprocessor for svelte
|
||||
date: 2024-12-18
|
||||
tags: ["svelte", "markdown", "mdsvex"]
|
||||
---
|
||||
|
||||
|
||||
"They said it was free..."
|
||||
|
||||
URL Source: https://mdsvex.pngwn.io/docs
|
||||
|
||||
mdsvex is a markdown preprocessor for [Svelte](https://svelte.dev/) components. Basically [MDX](https://mdxjs.com/) for Svelte.
|
||||
|
||||
This preprocessor allows you to use Svelte components in your markdown, or markdown in your Svelte components.
|
||||
|
||||
mdsvex supports all Svelte syntax and _almost_ all markdown syntax. See [limitations](https://mdsvex.pngwn.io/docs/#limitations) for more information.
|
||||
|
||||
You can do this:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Chart } from "../components/Chart.svelte";
|
||||
</script>
|
||||
|
||||
# Here’s a chart
|
||||
|
||||
The chart is rendered inside our MDsveX document.
|
||||
|
||||
<Chart />
|
||||
```
|
||||
|
||||
It uses [unified](https://unifiedjs.com/), [remark](https://github.com/remarkjs) and [rehype](https://github.com/rehypejs/rehype) and you can use any [remark plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins) or [rehype plugins](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins) to enhance your experience.
|
||||
|
||||
[Try it](https://mdsvex.pngwn.io/playground)
|
||||
|
||||
Install it
|
||||
----------
|
||||
|
||||
Install it as a dev-dependency.
|
||||
|
||||
With `npm`:
|
||||
|
||||
```bash
|
||||
npm i --save-dev mdsvex
|
||||
```
|
||||
|
||||
With `yarn`:
|
||||
|
||||
```bash
|
||||
yarn add --dev mdsvex
|
||||
```
|
||||
|
||||
Use it
|
||||
------
|
||||
|
||||
There are two named exports from `mdsvex` that can be used to transform mdsvex documents, `mdsvex` and `compile`. `mdsvex` is a Svelte preprocessor and is the preferred way to use this library. The `compile` function is useful when you wish to compile mdsvex documents to Svelte components directly, without hooking into the Svelte compiler.
|
||||
|
||||
### `mdsvex`
|
||||
|
||||
The `mdsvex` preprocessor function is a named import from the `mdsvex` module. Add it as a preprocessor to your rollup or webpack config, and tell the Svelte plugin or loader to also handle `.svx` files.
|
||||
|
||||
With rollup and `rollup-plugin-svelte`:
|
||||
|
||||
```ts
|
||||
import { mdsvex } from "mdsvex";
|
||||
|
||||
export default {
|
||||
...boring_config_stuff,
|
||||
plugins: [
|
||||
svelte({
|
||||
// these are the defaults. If you want to add more extensions, see https://mdsvex.pngwn.io/docs#extensions
|
||||
extensions: [".svelte", ".svx"],
|
||||
preprocess: mdsvex()
|
||||
})
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
With webpack and `svelte-loader`:
|
||||
|
||||
```ts
|
||||
const { mdsvex } = require('mdsvex')
|
||||
|
||||
// add ".svx" to the extensions array
|
||||
const extensions = ['.mjs', '.js', '.json', '.svelte', '.html', '.svx'];
|
||||
|
||||
module.exports = {
|
||||
...boring_config_stuff,
|
||||
resolve: { alias, extensions, mainFields },
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
// tell svelte-loader to handle svx files as well
|
||||
test: /.(svelte|html|svx)$/,
|
||||
use: {
|
||||
loader: 'svelte-loader',
|
||||
options: {
|
||||
...svelte_options,
|
||||
preprocess: mdsvex()
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
If you want to use mdsvex without a bundler because you are your own person, then you can use `svelte.preprocess` directly:
|
||||
|
||||
```ts
|
||||
const svelte = require('svelte/compiler');
|
||||
const { mdsvex } = require('mdsvex');
|
||||
|
||||
// This will give you a valid svelte component
|
||||
const preprocessed = await svelte.preprocess(
|
||||
source,
|
||||
mdsvex(mdsvex_opts)
|
||||
);
|
||||
|
||||
// Now you can compile it if you wish
|
||||
const compiled = svelte.compile(
|
||||
preprocessed,
|
||||
compiler_options
|
||||
);
|
||||
```
|
||||
|
||||
> If you don’t like the `.svx` file extension, fear not, it is easily customised.
|
||||
|
||||
### `compile`
|
||||
|
||||
This option performs a very similar task to the preprocessor but it can be used directly, without needing to hook into the Svelte compiler, either directly or via a bundler. The compile option will transform valid mdsvex code into valid svelte code, but it will perform no further actions such as resolving imports.
|
||||
|
||||
It supports all of the same options as the preprocessor although the function signature is slightly different. The first argument should be the mdsvex source code you wish to compile, the second argument is an object of options.
|
||||
|
||||
```svelte
|
||||
import { compile } from 'mdsvex';
|
||||
|
||||
const transformed_code = await compile(`
|
||||
<script>
|
||||
import Chart from './Chart.svelte';
|
||||
</script>
|
||||
|
||||
# Hello friends
|
||||
|
||||
<Chart />
|
||||
`,
|
||||
mdsvexOptions
|
||||
);
|
||||
```
|
||||
|
||||
In addition to the standard mdsvex options, the options object can also take an optional `filename` property which will be passed to mdsvex. There is no significant advantage to doing this but this provided filename may be used for error reporting in the future. The extension you give to this filename must match one of the extensions provided in the options (defaults to `['.svx']`).
|
||||
|
||||
Options
|
||||
-------
|
||||
|
||||
The preprocessor function accepts an object of options, that allow you to customise your experience. The options are global to all parsed files.
|
||||
|
||||
```ts
|
||||
interface MdsvexOptions {
|
||||
extensions: string[];
|
||||
smartypants: boolean | smartypantsOptions;
|
||||
layout: string | { [name: string]: string };
|
||||
remarkPlugins: Array<plugin> | Array<[plugin, plugin_options]>;
|
||||
rehypePlugins: Array<plugin> | Array<[plugin, plugin_options]>;
|
||||
highlight: { highlighter: Function, alias: { [alias]: lang } };
|
||||
frontmatter: { parse: Function; marker: string };
|
||||
}
|
||||
```
|
||||
|
||||
### `extensions`
|
||||
|
||||
```ts
|
||||
extensions: string[] = [".svx"];
|
||||
```
|
||||
|
||||
The `extensions` option allows you to set custom file extensions for files written in mdsvex; the default value is `['.svx']`. Whatever value you choose here must be passed to the `extensions` field of `rollup-plugin-svelte` or `svelte-loader`. If you do not change the default, you must still pass the extension name to the plugin or loader config.
|
||||
|
||||
```ts
|
||||
export default {
|
||||
...config,
|
||||
plugins: [
|
||||
svelte({
|
||||
extensions: [".svelte", ".custom"],
|
||||
preprocess: mdsvex({
|
||||
extensions: [".custom"]
|
||||
})
|
||||
})
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
To import markdown files as components, add `.md` to both the Svelte compiler and `mdsvex` extensions:
|
||||
|
||||
```js
|
||||
// svelte.config.js
|
||||
import { mdsvex } from 'mdsvex'
|
||||
|
||||
export default {
|
||||
extensions: ['.svelte', '.svx', '.md'],
|
||||
preprocess: mdsvex({ extensions: ['.svx', '.md'] }),
|
||||
}
|
||||
```
|
||||
|
||||
If you use TypeScript, you should also declare an ambient module:
|
||||
|
||||
```ts
|
||||
declare module '*.md' {
|
||||
import type { SvelteComponent } from 'svelte'
|
||||
|
||||
export default class Comp extends SvelteComponent{}
|
||||
|
||||
export const metadata: Record<string, unknown>
|
||||
}
|
||||
```
|
||||
|
||||
Then you can do:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import Readme from '../readme.md'
|
||||
</script>
|
||||
|
||||
<Readme />
|
||||
```
|
||||
|
||||
|
||||
### `smartypants`
|
||||
|
||||
```ts
|
||||
smartypants: boolean | {
|
||||
quotes: boolean = true;
|
||||
ellipses: boolean = true;
|
||||
backticks: boolean | 'all' = true;
|
||||
dashes: boolean | 'oldschool' | 'inverted' = true;
|
||||
} = true;
|
||||
```
|
||||
|
||||
The `smartypants` option transforms ASCII punctuation into fancy typographic punctuation HTML entities.
|
||||
|
||||
It turns stuff like:
|
||||
|
||||
```
|
||||
"They said it was free..."
|
||||
```
|
||||
|
||||
into:
|
||||
|
||||
> “They said it was free…”
|
||||
|
||||
Notice the beautiful punctuation. It does other nice things.
|
||||
|
||||
`smartypants` can be either a `boolean` (pass `false` to disable it) or an options object (defaults to `true`). The possible options are as follows.
|
||||
|
||||
```
|
||||
quotes: boolean = true;
|
||||
```
|
||||
|
||||
Converts straight double and single quotes to smart double or single quotes.
|
||||
|
||||
* `"words"` **becomes**: “words”
|
||||
* `'words'` **becomes** ‘words’
|
||||
|
||||
```
|
||||
ellipses: boolean = true;
|
||||
```
|
||||
|
||||
Converts triple-dot characters (with or without spaces) into a single Unicode ellipsis character.
|
||||
|
||||
* `words...` **becomes** words…
|
||||
|
||||
```
|
||||
backticks: boolean | 'all' = true;
|
||||
```
|
||||
|
||||
When `true`, converts double back-ticks into an opening double quote, and double straight single quotes into a closing double quote.
|
||||
|
||||
* ` ``words''` **becomes** “words”
|
||||
|
||||
When `'all'` it also converts single back-ticks into a single opening quote, and a single straight quote into a closing single, smart quote.
|
||||
|
||||
Note: Quotes can not be `true` when backticks is `'all'`;
|
||||
|
||||
```
|
||||
dashes: boolean | 'oldschool' | 'inverted' = true;
|
||||
```
|
||||
|
||||
When `true`, converts two dashes into an em-dash character.
|
||||
|
||||
* `--` **becomes** —
|
||||
|
||||
When `'oldschool'`, converts two dashes into an en-dash, and three dashes into an em-dash.
|
||||
|
||||
* `--` **becomes** –
|
||||
* `---` **becomes** —
|
||||
|
||||
When `'inverted'`, converts two dashes into an em-dash, and three dashes into an en-dash.
|
||||
|
||||
* `--` **becomes** —
|
||||
* `---` **becomes** –
|
||||
|
||||
### `layout`
|
||||
|
||||
```
|
||||
layout: string | Array<string | RegExp, string>;
|
||||
```
|
||||
|
||||
The `layout` option allows you to provide a custom layout component that will wrap your mdsvex file like so:
|
||||
|
||||
```
|
||||
<Layout>
|
||||
<MdsvexDocument />
|
||||
<Layout>
|
||||
```
|
||||
|
||||
> Layout components receive all frontmatter values as props, which should provide a great deal of flexibility when designing your layouts.
|
||||
|
||||
You can provide a `string`, which should be the path to your layout component. An absolute path is preferred but mdsvex tries to resolve relative paths based upon the current working directory.
|
||||
|
||||
```
|
||||
import { join } from "path";
|
||||
|
||||
const path_to_layout = join(__dirname, "./src/Layout.svelte");
|
||||
|
||||
mdsvex({
|
||||
layout: path_to_layout
|
||||
});
|
||||
```
|
||||
|
||||
In some cases you may want different layouts for different types of document, to address this you may pass an object of named layouts instead. Each key should be a name for your layout, the value should be a path as described above. A fallback layout, or default, can be passed using `_` (underscore) as a key name.
|
||||
|
||||
```
|
||||
mdsvex({
|
||||
layout: {
|
||||
blog: "./path/to/blog/layout.svelte",
|
||||
article: "./path/to/article/layout.svelte",
|
||||
_: "./path/to/fallback/layout.svelte"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
```
|
||||
remarkPlugins: Array<plugin> | Array<[plugin, plugin_options]>;
|
||||
rehypePlugins: Array<plugin> | Array<[plugin, plugin_options]>;
|
||||
```
|
||||
|
||||
mdsvex has a simple pipeline. Your source file is first parsed into a Markdown AST (MDAST), this is where remark plugins would run. Then it is converted into an HTML AST (HAST), this is where rehype plugins would be run. After this it is converted (stringified) into a valid Svelte component ready to be compiled.
|
||||
|
||||
[remark](https://github.com/remarkjs) and [rehype](https://github.com/rehypejs/rehype) have a vibrant plugin ecosystem and mdsvex allows you to pass any [remark plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins) or [rehype plugins](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins) as options, which will run on the remark and rehype ASTs at the correct point in the pipeline.
|
||||
|
||||
These options take an array. If you do not wish to pass any options to a plugin then you can simply pass an array of plugins like so:
|
||||
|
||||
```
|
||||
import containers from "remark-containers";
|
||||
import github from "remark-github";
|
||||
|
||||
mdsvex({
|
||||
remarkPlugins: [containers, github]
|
||||
});
|
||||
```
|
||||
|
||||
If you _do_ wish to pass options to your plugins then those array items should be an array of `[plugin, options]`, like so:
|
||||
|
||||
```
|
||||
import containers from "remark-containers";
|
||||
import github from "remark-github";
|
||||
|
||||
mdsvex({
|
||||
remarkPlugins: [
|
||||
[containers, container_opts],
|
||||
[github, github_opts]
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
You can mix and match as needed, only providing an array when options are needed:
|
||||
|
||||
```
|
||||
import containers from "remark-containers";
|
||||
import github from "remark-github";
|
||||
|
||||
mdsvex({
|
||||
remarkPlugins: [
|
||||
[containers, container_opts],
|
||||
github,
|
||||
another_plugin,
|
||||
[yet_another_plugin, more_options]
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
While these examples use `remarkPlugins`, the `rehypePlugins` option works in exactly the same way. You are free to use one or both of these options as you wish.
|
||||
|
||||
Remark plugins work on the Markdown AST (MDAST) produced by remark, rehype plugins work on the HTML AST (HAST) produced by rehype and it is possible to write your own custom plugins if the existing ones do not satisfy your needs!
|
||||
|
||||
### `highlight`
|
||||
|
||||
```
|
||||
highlight: {
|
||||
highlighter: (code: string, lang: string) => string | Promise<string>
|
||||
alias: { [lang : string]: string }
|
||||
};
|
||||
```
|
||||
|
||||
Without any configuration, mdsvex will automatically highlight the syntax of over 100 languages using [PrismJS](https://prismjs.com/), you simply need to add the language name to the fenced code block and import the CSS file for a Prism theme of your choosing. See [here for available options](https://github.com/PrismJS/prism-themes). Languages are loaded on-demand and cached for later use, this feature does not unnecessarily load all languages for highlighting purposes.
|
||||
|
||||
Custom aliases for language names can be defined via the `alias` property of the highlight option. This property takes an object of key-value pairs: the key should be the alias you wish to define, the value should be the language you wish to assign it to.
|
||||
|
||||
```
|
||||
mdsvex({
|
||||
highlight: {
|
||||
alias: { yavascript: "javascript" }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
If you wish to handle syntax-highlighting yourself, you can provide a custom highlight function via the `highlighter` property. The function will receive two arguments, the `code` to be highlighted and the `lang` defined in the fenced code-block, both are strings. You can use this information to highlight as you wish. The function should return a string of highlighted code.
|
||||
|
||||
You can disable syntax highlighting by passing a function that does nothing:
|
||||
|
||||
```
|
||||
function highlighter(code, lang) {
|
||||
return `<pre><code>${code}</code></pre>`;
|
||||
}
|
||||
|
||||
mdsvex({
|
||||
highlight: {
|
||||
highlighter
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### `frontmatter`
|
||||
|
||||
```
|
||||
frontmatter: { parse: Function, marker: string };
|
||||
```
|
||||
|
||||
By default mdsvex supports yaml frontmatter, this is defined by enclosing the YAML in three hyphens (`---`). If you want to use a custom language or marker for frontmatter then you can use the `frontmatter` option.
|
||||
|
||||
`frontmatter` should be an object that can contain a `marker` and a `parse` property.
|
||||
|
||||
```
|
||||
marker: string = '-';
|
||||
```
|
||||
|
||||
The marker option defines the fence for your frontmatter. This defaults to `-` which corresponds to the standard triple-hyphen syntax (`---`) that you would normally use to define frontmatter. You can pass in a custom string to change this behaviour:
|
||||
|
||||
```
|
||||
mdsvex({
|
||||
frontmatter: {
|
||||
marker: "+"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Now you can use `+++` to mark frontmatter. Setting _only_ the marker will keep the default frontmatter parser which only supports YAML.
|
||||
|
||||
```
|
||||
parse: (frontmatter, message) => Object | undefined
|
||||
```
|
||||
|
||||
The `parse` property accepts a function which allows you to provide a custom parser for frontmatter. This is useful if you want to use a different language in your frontmatter.
|
||||
|
||||
The parse function gets the raw frontmatter as the first argument and a `messages` array as the second.
|
||||
|
||||
If parsing is successful, the function should return the parsed frontmatter (as an object of key-value pairs), if there is a problem the function should return `undefined` or `false` . Any parsing errors or warnings should be pushed into the `messages` array which will be printed to the console when mdsvex has finished parsing. If you would prefer to throw an error, you are free to do so but it will interrupt the parsing process.
|
||||
|
||||
In the following example, we will modify the frontmatter handling so we can write our frontmatter in TOML with a triple-`+` fence.
|
||||
|
||||
```
|
||||
mdsvex({
|
||||
marker: "+",
|
||||
parse(frontmatter, messages) {
|
||||
try {
|
||||
return toml.parse(frontmatter);
|
||||
} catch (e) {
|
||||
messages.push(
|
||||
"Parsing error on line " +
|
||||
e.line +
|
||||
", column " +
|
||||
e.column +
|
||||
": " +
|
||||
e.message
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Now we will be able to write TOML frontmatter:
|
||||
|
||||
```
|
||||
+++
|
||||
title = "TOML Example"
|
||||
|
||||
[owner]
|
||||
name = "some name"
|
||||
dob = 1879-05-27T07:32:00-08:00
|
||||
+++
|
||||
```
|
||||
|
||||
Layouts
|
||||
-------
|
||||
|
||||
Layouts are one of the more powerful features available in mdsvex and allow for a great deal of flexibility. At their simplest a layout is just a component that wraps an mdsvex document. Providing a string as the layout option will enable this behaviour:
|
||||
|
||||
```
|
||||
mdsvex({
|
||||
layout: "./path/to/layout.svelte"
|
||||
});
|
||||
```
|
||||
|
||||
Layouts receive all values defined in frontmatter as props:
|
||||
|
||||
```
|
||||
<Layout {...props} >
|
||||
<!-- mdsvex content here -->
|
||||
</Layout>
|
||||
```
|
||||
|
||||
You can then use these values in your layout however you wish, a typical use might be to define some fancy formatting for headings, authors, and dates. Although you could do all kinds of wonderful things. You just need to make sure you provide a default `slot` so the mdsvex content can be passed into your layout and rendered.
|
||||
|
||||
```
|
||||
<script>
|
||||
export let title;
|
||||
export let author;
|
||||
export let date;
|
||||
</script>
|
||||
|
||||
<h1>{ title }</h1>
|
||||
<p class="date">on: { date }</p>
|
||||
<p class="date">by: { author }</p>
|
||||
<slot>
|
||||
<!-- the mdsvex content will be slotted in here -->
|
||||
</slot>
|
||||
```
|
||||
|
||||
### Named Layouts
|
||||
|
||||
In some cases you may want different layouts for different types of document. To address this you can pass an object of named layouts instead. Each key should be a name for your layout, the value should be the path to that layout file. A fallback layout, or default, can be passed using `_` (underscore) as a key name.
|
||||
|
||||
```
|
||||
mdsvex({
|
||||
layout: {
|
||||
blog: "./path/to/blog/layout.svelte",
|
||||
article: "./path/to/article/layout.svelte",
|
||||
_: "./path/to/fallback/layout.svelte"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
If you pass an object of named layouts, you can decide which layout to use on a file-by-file basis by declaring it in the frontmatter. For example, if you wanted to force a document to be wrapped with the `blog` layout you would do the following:
|
||||
|
||||
```
|
||||
---
|
||||
layout: blog
|
||||
---
|
||||
```
|
||||
|
||||
If you are using named layouts and do not have a layout field in the frontmatter then mdsvex will try to pick the correct one based on the folder a file is stored in. Take the following folder structure:
|
||||
|
||||
```
|
||||
.
|
||||
├── blog
|
||||
│ └── my-blog-post.svx
|
||||
└── article
|
||||
└── my-article.svx
|
||||
```
|
||||
|
||||
If there is a layout named `blog` and `article` then documents in the `blog` folder will use the `blog` layout, articles in the `articles` folder will use the `article` layout. mdsvex will try to check both singular and pluralised names, as you may have named a folder `events` but the matching layout could be named `event`, however, having the same folder and layout name will make this process more reliable. The current working directory is removed from the path when checking for matches but nested folders can still cause problems if there are conflicts. Shallow folder structures and unique folder and layout names will prevent these kinds of collisions.
|
||||
|
||||
If there is no matching layout then the fallback layout (`_`) will be applied, if there is no fallback then no layout will be applied.
|
||||
|
||||
### disabling layouts
|
||||
|
||||
If you are using layouts but wish to disable them for a specific component, then you can set the `layout` field to `false` to prevent the application of a layout.
|
||||
|
||||
```
|
||||
---
|
||||
layout: false
|
||||
---
|
||||
```
|
||||
|
||||
### Custom Components
|
||||
|
||||
Layouts also allow you to provide custom components to any mdsvex file they are applied to. Custom components replace the elements that markdown would normally generate.
|
||||
|
||||
```
|
||||
# Title
|
||||
|
||||
Some text
|
||||
|
||||
- a
|
||||
- short
|
||||
- list
|
||||
```
|
||||
|
||||
Would normally compile to:
|
||||
|
||||
```
|
||||
<h1>Title</h1>
|
||||
<p>Some text</p>
|
||||
<ul>
|
||||
<li>a</li>
|
||||
<li>short</li>
|
||||
<li>list</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Custom components allow you to replace these elements with components. You can define components by exporting named exports from the `context="module"` script of your Layout file:
|
||||
|
||||
```
|
||||
<script context="module">
|
||||
import { h1, p, li } from './components.js';
|
||||
export { h1, p, li };
|
||||
</script>
|
||||
```
|
||||
|
||||
The named exports must be named after the actual element you want to replace (`p`, `blockquote`, etc.), the value must be the component you wish to replace them with. This makes certain named exports ‘protected’ API, make sure you don’t use html names as export names for other values. Named exports whose names do not correspond to an HTML element will be ignored, so feel free to continue using them for other purposes as well. As these are named exports it is possible for the bundler to treeshake unused custom components, even if they are exported.
|
||||
|
||||
The above custom components would generate:
|
||||
|
||||
```
|
||||
<script>
|
||||
import * as Components from './Layout.svelte';
|
||||
</script>
|
||||
|
||||
<Components.h1>Title</Components.h1>
|
||||
<Components.p>Some text</Components.p>
|
||||
<ul>
|
||||
<Components.li>a</Components.li>
|
||||
<Components.li>short</Components.li>
|
||||
<Components.li>list</Components.li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Notice that the `ul` is left intact: elements are replaced _after_ the markdown is parsed to HTML. This allows greater flexibility, for example, when using custom components to customise lists, tables or other markdown that compiles to a combination of different HTML elements.
|
||||
|
||||
You may also receive attributes of the normal HTML component. For example, to render a custom `<img>` tag you could do:
|
||||
|
||||
```
|
||||
<script>
|
||||
export let src;
|
||||
</script>
|
||||
|
||||
<img src={src} />
|
||||
```
|
||||
|
||||
Frontmatter
|
||||
-----------
|
||||
|
||||
YAML frontmatter is a common convention in blog posts and mdsvex supports it out of the box. If you want to use a custom language or marker for frontmatter than you can use the [`frontmatter`](https://mdsvex.pngwn.io/docs#frontmatter) option to modify the default behaviour.
|
||||
|
||||
Mdsvex integrates well with frontmatter providing additional flexibility when authoring documents.
|
||||
|
||||
All variables defined in frontmatter are available directly in the component, exactly as you wrote them:
|
||||
|
||||
```
|
||||
---
|
||||
title: My lovely article
|
||||
author: Dr. Fabuloso the Fabulous
|
||||
---
|
||||
|
||||
# {title} by {author}
|
||||
|
||||
Some amazing content.
|
||||
```
|
||||
|
||||
Additionally, all of these variables are exported as a single object named `metadata` from the `context="module"` script, so they can easily be imported in javascript:
|
||||
|
||||
```
|
||||
<script context="module">
|
||||
export let metadata = {
|
||||
title: "My lovely article",
|
||||
author: "Dr. Fabuloso the Fabulous"
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
Due to how `context="module"` scripts work, this metadata can be imported like this:
|
||||
|
||||
```
|
||||
import { metadata } from "./some-mdsvex-file.svx";
|
||||
```
|
||||
|
||||
Frontmatter also interacts with layouts, you can find more details in the [Layout section](https://mdsvex.pngwn.io/docs#layouts).
|
||||
|
||||
Integrations
|
||||
------------
|
||||
|
||||
### With shiki
|
||||
|
||||
You can use shiki for highlighting rather than prism by leveraging the `highlighter` option:
|
||||
|
||||
```ts
|
||||
import { mdsvex, escapeSvelte } from 'mdsvex';
|
||||
import { createHighlighter } from 'shiki';
|
||||
|
||||
const theme = 'github-dark';
|
||||
const highlighter = await createHighlighter({
|
||||
themes: [theme],
|
||||
langs: ['javascript', 'typescript']
|
||||
});
|
||||
|
||||
/** @type {import('mdsvex').MdsvexOptions} */
|
||||
const mdsvexOptions = {
|
||||
highlight: {
|
||||
highlighter: async (code, lang = 'text') => {
|
||||
const html = escapeSvelte(highlighter.codeToHtml(code, { lang, theme }));
|
||||
return `{@html \`${html}\` }`;
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
### Indentation
|
||||
|
||||
In markdown you can begin a code block by indenting 4 spaces. This doesn’t work in mdsvex as indentation is common with XML-based languages. Indenting 4 spaces will do nothing.
|
||||
|
||||
In general you have a lot more flexibility when it comes to indenting code in mdsvex than you do in markdown because of the above change, however, you need to be very careful when indenting fenced code blocks. By which I mean, don’t do it.
|
||||
|
||||
The following code block will break in a way that is both very bad and quite unexpected:
|
||||
|
||||
```js
|
||||
```js
|
||||
console.log('Hello, World!')
|
||||
```
|
||||
```
|
||||
|
||||
The solution is to not do this. When working with fenced code blocks, do not indent them. This isn’t an issue that can really be worked around, even if the parser did make assumptions about what you meant. Because code blocks are designed to respect whitespace, any fix would simply result in a different but equally frustrating failure. Don’t indent code blocks.
|
||||
@@ -1,7 +1,9 @@
|
||||
---
|
||||
title: Obsidian
|
||||
description: Create and manage your notes with Obsidian!
|
||||
aliases: Obsidian
|
||||
date: 2024-11-16
|
||||
updated: 2024-12-08
|
||||
tags: [type/note,obsidian]
|
||||
---
|
||||
<div align="center">
|
||||
@@ -24,7 +26,7 @@ Obsidian is a powerful knowledge base that works on top of a local folder of pla
|
||||
|
||||
### Example Note Structure
|
||||
|
||||
```markdown
|
||||
```md
|
||||
# Project Planning
|
||||
|
||||
## Goals
|
||||
|
||||
@@ -8,12 +8,13 @@ tags:
|
||||
- skeletonui
|
||||
- CSS
|
||||
date: 2023-01-17
|
||||
updated: 2024-12-08
|
||||
---
|
||||
SkeletonUI is a comprehensive UI toolkit that integrates seamlessly with SvelteKit and Tailwind CSS, enabling developers to build adaptive and accessible web interfaces efficiently.
|
||||
|
||||
SkeletonUI offers a comprehensive suite of components to enhance your Svelte applications. Below is a categorized list of these components, presented in Svelte syntax:
|
||||
|
||||
```svelte
|
||||
```
|
||||
<!-- Layout Components -->
|
||||
<AppShell />
|
||||
<AppBar />
|
||||
@@ -78,21 +79,21 @@ To set up SkeletonUI in a new SvelteKit project, follow these steps:
|
||||
|
||||
- **Create a new SvelteKit project**:
|
||||
|
||||
```bash
|
||||
```
|
||||
npx sv create my-skeleton-app
|
||||
cd my-skeleton-app
|
||||
npm install
|
||||
```
|
||||
- **Install SkeletonUI packages**:
|
||||
|
||||
```bash
|
||||
```
|
||||
npm install -D @skeletonlabs/skeleton@next @skeletonlabs/skeleton-svelte@next
|
||||
```
|
||||
- **Configure Tailwind CSS**:
|
||||
|
||||
In your `tailwind.config.js` file, add the following:
|
||||
|
||||
```javascript
|
||||
```
|
||||
import { skeleton, contentPath } from '@skeletonlabs/skeleton/plugin';
|
||||
import * as themes from '@skeletonlabs/skeleton/themes';
|
||||
|
||||
@@ -115,7 +116,7 @@ export default {
|
||||
|
||||
In your `src/app.html`, set the `data-theme` attribute on the `<body>` tag:
|
||||
|
||||
```html
|
||||
```
|
||||
<body data-theme="cerberus">
|
||||
<!-- Your content -->
|
||||
</body>
|
||||
@@ -129,7 +130,7 @@ SkeletonUI offers a variety of pre-designed components to accelerate your develo
|
||||
|
||||
- **Button**:
|
||||
|
||||
```svelte
|
||||
```
|
||||
<script>
|
||||
import { Button } from '@skeletonlabs/skeleton-svelte';
|
||||
</script>
|
||||
@@ -138,7 +139,7 @@ SkeletonUI offers a variety of pre-designed components to accelerate your develo
|
||||
```
|
||||
- **Card**:
|
||||
|
||||
```svelte
|
||||
```
|
||||
<script>
|
||||
import { Card } from '@skeletonlabs/skeleton-svelte';
|
||||
</script>
|
||||
@@ -150,7 +151,7 @@ SkeletonUI offers a variety of pre-designed components to accelerate your develo
|
||||
```
|
||||
- **Form Input**:
|
||||
|
||||
```svelte
|
||||
```
|
||||
<script>
|
||||
import { Input } from '@skeletonlabs/skeleton-svelte';
|
||||
let inputValue = '';
|
||||
@@ -161,18 +162,19 @@ SkeletonUI offers a variety of pre-designed components to accelerate your develo
|
||||
|
||||
For a comprehensive list of components and their usage, consult the SkeletonUI components documentation.
|
||||
|
||||
**3\. Theming**
|
||||
**3. Theming**
|
||||
|
||||
SkeletonUI's theming system allows for extensive customization:
|
||||
|
||||
- **Applying a Theme**:
|
||||
|
||||
Set the desired theme in your `tailwind.config.js` and `app.html` as shown in the installation steps above.
|
||||
|
||||
- **Switching Themes Dynamically**:
|
||||
|
||||
To enable dynamic theme switching, you can modify the `data-theme` attribute programmatically:
|
||||
|
||||
```svelte
|
||||
```
|
||||
<script>
|
||||
function switchTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
@@ -181,13 +183,14 @@ To enable dynamic theme switching, you can modify the `data-theme` attribute pro
|
||||
|
||||
<button on:click={() => switchTheme('rose')}>Switch to Rose Theme</button>
|
||||
```
|
||||
|
||||
- **Customizing Themes**:
|
||||
|
||||
You can create custom themes by defining your own color palettes and settings in the `tailwind.config.js` file.
|
||||
|
||||
For more information on theming, refer to the SkeletonUI theming guide.
|
||||
|
||||
**4\. Utilities**
|
||||
**4. Utilities**
|
||||
|
||||
SkeletonUI provides several utility functions and actions to enhance your SvelteKit application:
|
||||
|
||||
@@ -195,7 +198,7 @@ SkeletonUI provides several utility functions and actions to enhance your Svelte
|
||||
|
||||
Automatically generate a table of contents based on page headings:
|
||||
|
||||
```svelte
|
||||
```
|
||||
<script>
|
||||
import { TableOfContents, tocCrawler } from '@skeletonlabs/skeleton-svelte';
|
||||
</script>
|
||||
@@ -209,7 +212,7 @@ Automatically generate a table of contents based on page headings:
|
||||
|
||||
Utilize built-in transitions for smooth animations:
|
||||
|
||||
```svelte
|
||||
```
|
||||
<script>
|
||||
import { fade } from '@skeletonlabs/skeleton-svelte';
|
||||
let visible = true;
|
||||
|
||||
@@ -17,7 +17,7 @@ Here’s how you can handle streaming JSON and convert it into Markdown:
|
||||
Fetch the Streaming JSON: Use SvelteKit's load function to fetch the API data. If the API streams JSON, ensure you parse it incrementally using the ReadableStream interface in JavaScript.
|
||||
Parse and Extract Markdown: Once you receive the JSON chunks, extract the Markdown strings from the relevant fields. For example:
|
||||
|
||||
```javascript
|
||||
```
|
||||
const response = await fetch('https://api.example.com/stream');
|
||||
const reader = response.body.getReader();
|
||||
let markdownContent = '';
|
||||
@@ -31,9 +31,9 @@ while (true) {
|
||||
|
||||
Render Markdown with mdsvex or svelte-markdown:
|
||||
|
||||
Using mdsvex: Compile the Markdown string into HTML at runtime. Mdsvex provides a compile function for this purpose12.
|
||||
Using mdsvex: Compile the Markdown string into HTML at runtime. Mdsvex provides a compile function for this purpose.
|
||||
|
||||
```javascript
|
||||
```
|
||||
import { compile } from 'mdsvex';
|
||||
|
||||
const { code } = await compile(markdownContent);
|
||||
@@ -42,12 +42,12 @@ const { code } = await compile(markdownContent);
|
||||
You can then inject this compiled HTML into your Svelte component.
|
||||
Using svelte-markdown: This library directly renders Markdown strings as Svelte components, making it ideal for runtime rendering3. Install it with:
|
||||
|
||||
```text
|
||||
```
|
||||
npm install svelte-markdown
|
||||
```
|
||||
|
||||
Then use it in your component:
|
||||
```text
|
||||
```
|
||||
|
||||
import Markdown from 'svelte-markdown';
|
||||
let markdownContent = '# Example Heading\nThis is some text.';
|
||||
@@ -73,7 +73,7 @@ Install mdsvex and its dependencies:
|
||||
text
|
||||
npm install mdsvex
|
||||
Configure mdsvex in your svelte.config.js file:
|
||||
```javascript
|
||||
```
|
||||
import { mdsvex } from 'mdsvex';
|
||||
|
||||
const config = {
|
||||
@@ -92,7 +92,7 @@ It supports frontmatter, allowing you to include metadata at the top of your Mar
|
||||
You can use Svelte components directly within your Markdown content, enabling interactive elements2.
|
||||
Code highlighting is built-in, making it easy to display formatted code snippets2.
|
||||
For dynamic content, such as Markdown stored in a database or fetched from an API, you can use mdsvex to render Markdown strings at runtime:
|
||||
```javascript
|
||||
```
|
||||
import { compile } from 'mdsvex';
|
||||
|
||||
const markdownString = '# Hello, World!';
|
||||
@@ -106,7 +106,7 @@ By leveraging mdsvex, SvelteKit developers can create rich, interactive content
|
||||
## More Markdown Integration Examples
|
||||
Dynamic Markdown Rendering: For scenarios where Markdown content is dynamically fetched from an external API, you can use the marked library to parse the Markdown into HTML directly within a Svelte component. This approach is simple and effective for runtime rendering:
|
||||
|
||||
```text
|
||||
```
|
||||
|
||||
import { marked } from 'marked';
|
||||
let markdownContent = '';
|
||||
@@ -130,7 +130,7 @@ This method ensures that even dynamically loaded Markdown content is rendered ef
|
||||
|
||||
Markdown with Frontmatter: Mdsvex supports frontmatter, which allows you to embed metadata in your Markdown files. This is particularly useful for blogs or documentation sites. For example:
|
||||
|
||||
```text
|
||||
```
|
||||
---
|
||||
title: "My Blog Post"
|
||||
date: "2024-01-01"
|
||||
@@ -144,14 +144,14 @@ This is a post about integrating Markdown with SvelteKit.
|
||||
You can access this metadata in your Svelte components, enabling features like dynamic page titles or tag-based filtering34.
|
||||
|
||||
Interactive Charts in Markdown: Combine the power of Markdown with Svelte's interactivity by embedding components like charts. For instance, using Mdsvex, you can include a chart directly in your Markdown file:
|
||||
```text
|
||||
```
|
||||
# Sales Data
|
||||
```
|
||||
|
||||
Here, Chart is a Svelte component that renders a chart using libraries like Chart.js or D3.js. This approach makes it easy to create visually rich content while keeping the simplicity of Markdown56.
|
||||
Custom Styling for Markdown Content: To apply consistent styles to your rendered Markdown, wrap it in a container with scoped CSS. For example:
|
||||
|
||||
```text
|
||||
```
|
||||
|
||||
|
||||
.markdown-content h1 {
|
||||
@@ -165,8 +165,7 @@ Custom Styling for Markdown Content: To apply consistent styles to your rendered
|
||||
This ensures your Markdown content adheres to your application's design system without affecting other parts of the UI12.
|
||||
Pagination for Large Markdown Files: If you're dealing with extensive Markdown content, split it into smaller sections and implement pagination. For example, store each section in an array and render only the current page:
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
let currentPage = 0;
|
||||
const markdownPages = [
|
||||
'# Page 1\nThis is the first page.',
|
||||
@@ -192,7 +191,7 @@ Building on the previous examples, let's explore some more advanced techniques f
|
||||
|
||||
Syntax Highlighting: Enhance code blocks in your Markdown content with syntax highlighting using libraries like Prism.js. Here's how you can set it up with mdsvex:
|
||||
|
||||
```javascript
|
||||
```
|
||||
import { mdsvex } from 'mdsvex';
|
||||
import prism from 'prismjs';
|
||||
|
||||
@@ -213,9 +212,7 @@ const config = {
|
||||
This configuration will automatically apply syntax highlighting to code blocks in your Markdown files1.
|
||||
Custom Components for Markdown Elements: Create custom Svelte components to replace standard Markdown elements. For instance, you can create a custom Image component for enhanced image handling:
|
||||
|
||||
```text
|
||||
|
||||
|
||||
```
|
||||
export let src;
|
||||
export let alt;
|
||||
```
|
||||
@@ -223,7 +220,7 @@ Custom Components for Markdown Elements: Create custom Svelte components to repl
|
||||
|
||||
Then, configure mdsvex to use this component:
|
||||
|
||||
```javascript
|
||||
```
|
||||
import Image from './Image.svelte';
|
||||
|
||||
const config = {
|
||||
@@ -246,7 +243,7 @@ const config = {
|
||||
This setup allows you to add lazy loading, responsive images, or other custom behaviors to all images in your Markdown content2.
|
||||
Table of Contents Generation: Automatically generate a table of contents for your Markdown files using remark plugins:
|
||||
|
||||
```javascript
|
||||
```
|
||||
import { mdsvex } from 'mdsvex';
|
||||
import remarkToc from 'remark-toc';
|
||||
|
||||
@@ -265,7 +262,7 @@ const config = {
|
||||
This configuration will automatically generate a table of contents based on the headings in your Markdown files3.
|
||||
Live Markdown Editor: Create an interactive Markdown editor with real-time preview:
|
||||
|
||||
```text
|
||||
```
|
||||
|
||||
import { marked } from 'marked';
|
||||
let markdownInput = '# Live Markdown Editor\n\nStart typing...';
|
||||
|
||||
149
web/src/lib/content/posts/svelte_link_options.md
Normal file
149
web/src/lib/content/posts/svelte_link_options.md
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
title: Svelte
|
||||
description: sveltekit stuff.
|
||||
aliases: this is just a temp file
|
||||
date: 2024-12-18
|
||||
images:
|
||||
tags:
|
||||
- tag1
|
||||
- tag2
|
||||
---
|
||||
|
||||
|
||||
Can I import images from `static` to be used in Metadata? People have thought about this and it's better to keep images for posts in the Obsidian Vault. Check out Bramses opinionated vault. [Bramses-Highly-Opinioinated-Vault](https://github.com/bramses/bramses-highly-opinionated-vault-2023)
|
||||
|
||||
URL Source: https://svelte.dev/docs/kit/link-options
|
||||
|
||||
Markdown Content:
|
||||
In SvelteKit, `<a>` elements (rather than framework-specific `<Link>` components) are used to navigate between the routes of your app. If the user clicks on a link whose `href` is ‘owned’ by the app (as opposed to, say, a link to an external site) then SvelteKit will navigate to the new page by importing its code and then calling any `load` functions it needs to fetch data.
|
||||
|
||||
You can customise the behaviour of links with `data-sveltekit-*` attributes. These can be applied to the `<a>` itself, or to a parent element.
|
||||
|
||||
These options also apply to `<form>` elements with [`method="GET"`](https://svelte.dev/docs/kit/form-actions#GET-vs-POST).
|
||||
|
||||
[data-sveltekit-preload-data](https://svelte.dev/docs/kit/link-options#data-sveltekit-preload-data)
|
||||
---------------------------------------------------------------------------------------------------
|
||||
|
||||
Before the browser registers that the user has clicked on a link, we can detect that they’ve hovered the mouse over it (on desktop) or that a `touchstart` or `mousedown` event was triggered. In both cases, we can make an educated guess that a `click` event is coming.
|
||||
|
||||
SvelteKit can use this information to get a head start on importing the code and fetching the page’s data, which can give us an extra couple of hundred milliseconds — the difference between a user interface that feels laggy and one that feels snappy.
|
||||
|
||||
We can control this behaviour with the `data-sveltekit-preload-data` attribute, which can have one of two values:
|
||||
|
||||
* `"hover"` means that preloading will start if the mouse comes to a rest over a link. On mobile, preloading begins on `touchstart`
|
||||
* `"tap"` means that preloading will start as soon as a `touchstart` or `mousedown` event is registered
|
||||
|
||||
The default project template has a `data-sveltekit-preload-data="hover"` attribute applied to the `<body>` element in `src/app.html`, meaning that every link is preloaded on hover by default:
|
||||
|
||||
```svelte
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
```
|
||||
|
||||
Sometimes, calling `load` when the user hovers over a link might be undesirable, either because it’s likely to result in false positives (a click needn’t follow a hover) or because data is updating very quickly and a delay could mean staleness.
|
||||
|
||||
In these cases, you can specify the `"tap"` value, which causes SvelteKit to call `load` only when the user taps or clicks on a link:
|
||||
|
||||
```svelte
|
||||
<a data-sveltekit-preload-data="tap" href="/stonks">
|
||||
Get current stonk values
|
||||
</a>
|
||||
```
|
||||
|
||||
> You can also programmatically invoke `preloadData` from `$app/navigation`.
|
||||
|
||||
Data will never be preloaded if the user has chosen reduced data usage, meaning [`navigator.connection.saveData`](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/saveData) is `true`.
|
||||
|
||||
[data-sveltekit-preload-code](https://svelte.dev/docs/kit/link-options#data-sveltekit-preload-code)
|
||||
|
||||
---------------------------------------------------------------------------------------------------
|
||||
|
||||
Even in cases where you don’t want to preload _data_ for a link, it can be beneficial to preload the _code_. The `data-sveltekit-preload-code` attribute works similarly to `data-sveltekit-preload-data`, except that it can take one of four values, in decreasing ‘eagerness’:
|
||||
|
||||
* `"eager"` means that links will be preloaded straight away
|
||||
* `"viewport"` means that links will be preloaded once they enter the viewport
|
||||
* `"hover"` - as above, except that only code is preloaded
|
||||
* `"tap"` - as above, except that only code is preloaded
|
||||
|
||||
Note that `viewport` and `eager` only apply to links that are present in the DOM immediately following navigation — if a link is added later (in an `{#if ...}` block, for example) it will not be preloaded until triggered by `hover` or `tap`. This is to avoid performance pitfalls resulting from aggressively observing the DOM for changes.
|
||||
|
||||
> Since preloading code is a prerequisite for preloading data, this attribute will only have an effect if it specifies a more eager value than any `data-sveltekit-preload-data` attribute that is present.
|
||||
|
||||
As with `data-sveltekit-preload-data`, this attribute will be ignored if the user has chosen reduced data usage.
|
||||
|
||||
data-sveltekit-reload[etst](https://svelte.dev/docs/kit/link-options#data-sveltekit-reload)
|
||||
|
||||
---------------------------------------------------------------------------------------------------
|
||||
|
||||
Occasionally, we need to tell SvelteKit not to handle a link, but allow the browser to handle it. Adding a `data-sveltekit-reload` attribute to a link...
|
||||
|
||||
```svelte
|
||||
<a data-sveltekit-reload href="/path">Path</a>
|
||||
```
|
||||
|
||||
...will cause a full-page navigation when the link is clicked.
|
||||
|
||||
Links with a `rel="external"` attribute will receive the same treatment. In addition, they will be ignored during [prerendering](https://svelte.dev/docs/kit/page-options#prerender).
|
||||
|
||||
[data-sveltekit-replacestate](https://svelte.dev/docs/kit/link-options#data-sveltekit-replacestate)
|
||||
---------------------------------------------------------------------------------------------------
|
||||
|
||||
Sometimes you don’t want navigation to create a new entry in the browser’s session history. Adding a `data-sveltekit-replacestate` attribute to a link...
|
||||
|
||||
```svelte
|
||||
<a data-sveltekit-replacestate href="/path">Path</a>
|
||||
```
|
||||
|
||||
...will replace the current `history` entry rather than creating a new one with `pushState` when the link is clicked.
|
||||
|
||||
data-sveltekit-keepfocus[sveltekit-keepfocus](https://svelte.dev/docs/kit/link-options#data-sveltekit-keepfocus)
|
||||
---------------------------------------------------------------------------------------------
|
||||
|
||||
Sometimes you don’t want [focus to be reset](https://svelte.dev/docs/kit/accessibility#Focus-management) after navigation. For example, maybe you have a search form that submits as the user is typing, and you want to keep focus on the text input. Adding a `data-sveltekit-keepfocus` attribute to it...
|
||||
|
||||
```svelte
|
||||
<form data-sveltekit-keepfocus>
|
||||
<input type="text" name="query">
|
||||
</form>
|
||||
```
|
||||
|
||||
...will cause the currently focused element to retain focus after navigation. In general, avoid using this attribute on links, since the focused element would be the `<a>` tag (and not a previously focused element) and screen reader and other assistive technology users often expect focus to be moved after a navigation. You should also only use this attribute on elements that still exist after navigation. If the element no longer exists, the user’s focus will be lost, making for a confusing experience for assistive technology users.
|
||||
|
||||
When navigating to internal links, SvelteKit mirrors the browser’s default navigation behaviour: it will change the scroll position to 0,0 so that the user is at the very top left of the page (unless the link includes a `#hash`, in which case it will scroll to the element with a matching ID).
|
||||
|
||||
In certain cases, you may wish to disable this behaviour. Adding a `data-sveltekit-noscroll` attribute to a link...
|
||||
|
||||
```svelte
|
||||
<a href="path" data-sveltekit-noscroll ally="Path">Path</a>
|
||||
```
|
||||
|
||||
...will prevent scrolling after the link is clicked.
|
||||
|
||||
[Disabling options](https://svelte.dev/docs/kit/link-options#Disabling-options)
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
To disable any of these options inside an element where they have been enabled, use the `"false"` value:
|
||||
|
||||
```svelte
|
||||
<div data-sveltekit-preload-data>
|
||||
<!-- these links will be preloaded -->
|
||||
<a href="/a">a</a>
|
||||
<a href="/b">b</a>
|
||||
<a href="/c">c</a>
|
||||
|
||||
<div data-sveltekit-preload-data="false">
|
||||
<!-- these links will NOT be preloaded -->
|
||||
<a href="/d">d</a>
|
||||
<a href="/e">e</a>
|
||||
<a href="/f">f</a>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
To apply an attribute to an element conditionally, do this:
|
||||
|
||||
```svelte
|
||||
<div data-sveltekit-preload-data={condition ? 'hover' : false}>
|
||||
```
|
||||
|
||||
35
web/src/lib/content/posts/tutorial.md
Normal file
35
web/src/lib/content/posts/tutorial.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Tutorial Template
|
||||
aliases: Tutorial
|
||||
description: Get started writing a tutorial with a template
|
||||
date: 2024-12-21
|
||||
updated: 2024-12-21
|
||||
tags:
|
||||
- template
|
||||
- tutorial
|
||||
---
|
||||
|
||||
# How to [Achieve a Specific Goal or Task]
|
||||
|
||||
## Introduction
|
||||
In this tutorial, we will explore how to [briefly describe the goal or task]. By the end of this guide, you'll have the skills and knowledge needed to [explain what the reader will learn or accomplish].
|
||||
|
||||
## Step 1: [Describe the first step]
|
||||
[Provide detailed instructions on the first step. Explain why this step is important and any tips or tricks that can help.]
|
||||
|
||||
## Step 2: [Describe the second step]
|
||||
[Outline the second step in the process. Include any necessary details and emphasize its significance in achieving the overall goal.]
|
||||
|
||||
## Step 3: [Describe the third step]
|
||||
[Continue to detail each step necessary to complete the task. Make sure to provide clear instructions and helpful insights.]
|
||||
|
||||
## Step 4: [Describe the fourth step]
|
||||
[As with previous steps, be thorough in your explanation and guidance.]
|
||||
|
||||
## Conclusion
|
||||
Congratulations! You've now learned how to [recap the specific goal or task]. Remember that practice makes perfect, so don't hesitate to try this out yourself. Share your results or ask questions in the comments below!
|
||||
|
||||
|
||||
|
||||
**Replace the placeholder text in brackets with your specific content for each section of the tutorial!**
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
title: Using Markdown in Svelte
|
||||
description: Learn how to use your markdown documents in Svelte Applications!
|
||||
date: 2023-12-22
|
||||
updated:
|
||||
aliases: Using Markdonw in Svelte, Svelte in Markdown
|
||||
tags: [markdown, svelte, web-dev, docs, learn]
|
||||
---
|
||||
[Mdsvex](https://mdsvex.pngwn.io/docs#install-it)
|
||||
|
||||
Here are some examples illustrating how to use Mdsvex in a Svelte application:
|
||||
|
||||
|
||||
**Example 1**: Basic Markdown with Svelte Component
|
||||
Create a file named example.svx:
|
||||
|
||||
markdown
|
||||
```markdown
|
||||
---
|
||||
title: "Interactive Markdown Example"
|
||||
@@ -29,14 +30,14 @@ This is an example of combining Markdown with a Svelte component:
|
||||
```
|
||||
|
||||
In this example:
|
||||
|
||||
- The frontmatter (--- sections) defines variables like title.
|
||||
- A Svelte component Counter is imported and used inside the Markdown.
|
||||
|
||||
|
||||
**Example 2**: Custom Layouts with Mdsvex
|
||||
Assuming you have a layout component at src/lib/layouts/BlogLayout.svelte:
|
||||
|
||||
svelte
|
||||
```text
|
||||
```svelte
|
||||
<!-- BlogLayout.svelte -->
|
||||
<script>
|
||||
export let title;
|
||||
@@ -49,7 +50,7 @@ svelte
|
||||
```
|
||||
|
||||
Now, to use this layout in your Markdown:
|
||||
**markdown**
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "My Favorite Layout"
|
||||
@@ -62,7 +63,7 @@ This Markdown file will be wrapped by the `BlogLayout`.
|
||||
```
|
||||
|
||||
**Example 3:** Using Frontmatter Variables in Markdown
|
||||
**markdown**
|
||||
|
||||
```markdown
|
||||
---
|
||||
author: "John Doe"
|
||||
@@ -77,7 +78,7 @@ Here's some markdown content. You can reference frontmatter values directly in t
|
||||
```
|
||||
|
||||
**Example 4**: Interactive Elements in Markdown
|
||||
markdown
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "Interactive Chart"
|
||||
@@ -97,39 +98,44 @@ Below is an interactive chart:
|
||||
## Setting Up Mdsvex
|
||||
|
||||
To make these work, you need to configure your SvelteKit project:
|
||||
|
||||
1. Install Mdsvex:
|
||||
|
||||
```bash
|
||||
npm install -D mdsvex
|
||||
```
|
||||
|
||||
|
||||
2. Configure SvelteKit:
|
||||
In your svelte.config.js:
|
||||
```javascript
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
import { mdsvex } from 'mdsvex';
|
||||
|
||||
/** @type {import('mdsvex').MdsvexOptions} */
|
||||
const mdsvexOptions = {
|
||||
extensions: ['.svx'],
|
||||
};
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
extensions: ['.svelte', '.svx'],
|
||||
preprocess: [
|
||||
vitePreprocess(),
|
||||
mdsvex(mdsvexOptions),
|
||||
],
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
|
||||
In your svelte.config.js:
|
||||
|
||||
```javascript
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
import { mdsvex } from 'mdsvex';
|
||||
|
||||
/** @type {import('mdsvex').MdsvexOptions} */
|
||||
const mdsvexOptions = {
|
||||
extensions: ['.svx'],
|
||||
};
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
extensions: ['.svelte', '.svx'],
|
||||
preprocess: [
|
||||
vitePreprocess(),
|
||||
mdsvex(mdsvexOptions),
|
||||
],
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
3. Create a Route for Markdown Files:
|
||||
Place your .svx files in the src/routes directory or subdirectories, and SvelteKit will automatically handle them as routes.
|
||||
|
||||
|
||||
Place your .svx files in the src/routes directory or subdirectories, and SvelteKit will automatically handle them as routes.
|
||||
|
||||
These examples show how you can integrate Mdsvex into your Svelte application, combining the simplicity of Markdown with the interactivity of Svelte components. Remember, any Svelte component you want to use within Markdown must be exported from a .svelte file and imported in your .svx file.
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
---
|
||||
title: Welcome to Your Blog
|
||||
aliases: [Your First Post]
|
||||
description: First post on your new SvelteKit blog
|
||||
date: 2024-01-17
|
||||
tags: [welcome, blog, create, explore]
|
||||
tags:
|
||||
- welcome
|
||||
- blog
|
||||
- create
|
||||
- explore
|
||||
updated: 2024-01-17
|
||||
author: Your Name Here
|
||||
aliases:
|
||||
- Welcome!
|
||||
---
|
||||
<script>
|
||||
import textarea from '$lib/components/ui/textarea/textarea.svelte'
|
||||
</script>
|
||||
|
||||
**Improve the styling. Butt first, the functionality**
|
||||
|
||||
<textarea class="text-primary-800" placeholder="Get started creating notes now!" />
|
||||
|
||||
This is the first post of your new blog, powered by [SvelteKit](/posts/getting-started), [Obsidian](/obsidian), and [Fabric](/about). I'm excited to share this project with you, and I hope you find it useful for your own writing and experiences.
|
||||
|
||||
This part of the application is edited in <a href="http://localhost:5173/posts/obsidian" name="Obsidian">Obsidian</a>.
|
||||
@@ -16,4 +31,7 @@ This part of the application is edited in <a href="http://localhost:5173/posts/o
|
||||
- How to use Fabric and Obsidian to write and publish
|
||||
- More ways to use Obsidian and Fabric together!
|
||||
|
||||
Stay tuned for more content!
|
||||
Stay tuned for more content!
|
||||
|
||||
|
||||
|
||||
|
||||
13
web/src/lib/interfaces/post-interface.ts
Normal file
13
web/src/lib/interfaces/post-interface.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { SvelteComponent } from 'svelte';
|
||||
import type { Frontmatter } from '$lib/types/markdown';
|
||||
|
||||
export type PostMetadata = Frontmatter;
|
||||
|
||||
export interface Post {
|
||||
/** URL-friendly identifier for the post */
|
||||
slug: string;
|
||||
/** Post metadata from frontmatter */
|
||||
metadata: PostMetadata;
|
||||
/** Compiled Svelte component or HTML string */
|
||||
content: string | typeof SvelteComponent;
|
||||
}
|
||||
@@ -1,39 +1,41 @@
|
||||
import { compile } from 'mdsvex';
|
||||
// Note: This file can be deleted
|
||||
|
||||
export interface Post {
|
||||
slug: string;
|
||||
title: string;
|
||||
date: string;
|
||||
content?: any;
|
||||
}
|
||||
|
||||
const modules = import.meta.glob('../content/posts/*.md' + '../../routes/**/*.md', { eager: true });
|
||||
|
||||
export const posts: Post[] = Object.entries(modules).map(([path, module]: [string, any]) => {
|
||||
const slug = path.split('/').pop()?.replace('.md', '') || '';
|
||||
return {
|
||||
slug,
|
||||
title: module.metadata?.title || slug,
|
||||
aliases: module.metadata?.aliases || [],
|
||||
date: module.metadata?.date || new Date().toISOString().split('T')[0],
|
||||
description: module.metadata?.description || '',
|
||||
tags: module.metadata?.tags || [],
|
||||
updated: module.metadata?.updated || new Date().toISOString(),
|
||||
author: module.metadata?.author || '',
|
||||
lead: module.metadata?.lead || '',
|
||||
reference: module.metadata?.reference || '',
|
||||
content: module.default
|
||||
};
|
||||
});
|
||||
|
||||
export async function getPost(slug: string) {
|
||||
const post = posts.find(p => p.slug === slug);
|
||||
if (!post) return null;
|
||||
|
||||
if (typeof post.content === 'string') {
|
||||
const compiled = await compile(post.content);
|
||||
post.content = compiled?.code || post.content;
|
||||
}
|
||||
|
||||
return post;
|
||||
}
|
||||
//import { compile } from 'mdsvex';
|
||||
//
|
||||
//export interface Post {
|
||||
// slug: string;
|
||||
// title: string;
|
||||
// date: string;
|
||||
// content?: any;
|
||||
//}
|
||||
//
|
||||
//const modules = import.meta.glob('../content/posts/*.md' + '../../routes/**/*.md', { eager: true });
|
||||
//
|
||||
//export const posts: Post[] = Object.entries(modules).map(([path, module]: [string, any]) => {
|
||||
// const slug = path.split('/').pop()?.replace('.md', '') || '';
|
||||
// return {
|
||||
// slug,
|
||||
// title: module.metadata?.title || slug,
|
||||
// aliases: module.metadata?.aliases || [],
|
||||
// date: module.metadata?.date || new Date().toISOString().split('T')[0],
|
||||
// description: module.metadata?.description || '',
|
||||
// tags: module.metadata?.tags || [],
|
||||
// updated: module.metadata?.updated || new Date().toISOString(),
|
||||
// author: module.metadata?.author || '',
|
||||
// lead: module.metadata?.lead || '',
|
||||
// reference: module.metadata?.reference || '',
|
||||
// content: module.default
|
||||
// };
|
||||
//});
|
||||
//
|
||||
//export async function getPost(slug: string) {
|
||||
// const post = posts.find(p => p.slug === slug);
|
||||
// if (!post) return null;
|
||||
//
|
||||
// if (typeof post.content === 'string') {
|
||||
// const compiled = await compile(post.content);
|
||||
// post.content = compiled?.code || post.content;
|
||||
// }
|
||||
//
|
||||
// return post;
|
||||
//}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
<!-- This can be deleted if you don't want to use it! -->
|
||||
|
||||
<!-- <script lang="ts">
|
||||
import { formatDistance } from 'date-fns';
|
||||
import TagList from '$components/ui/tag-list/TagList.svelte';
|
||||
|
||||
@@ -27,7 +29,7 @@
|
||||
{#if date}
|
||||
<div class="flex items-center space-x-4 text-sm text-muted-foreground">
|
||||
<time datetime={date}>{formatDistance(new Date(date), new Date(), { addSuffix: true })}</time>
|
||||
|
||||
|
||||
{#if tags?.length}
|
||||
<span class="text-xs">•</span>
|
||||
<TagList {tags} className="flex-1" />
|
||||
@@ -47,4 +49,4 @@
|
||||
<div class="mt-8">
|
||||
<slot />
|
||||
</div>
|
||||
</article>
|
||||
</article> -->
|
||||
|
||||
@@ -1,25 +1,55 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const themes = [
|
||||
'my-custom-theme',
|
||||
'skeleton',
|
||||
'modern',
|
||||
'crimson',
|
||||
'gold-nouveau',
|
||||
'hamlindigo',
|
||||
'vintage',
|
||||
'seafoam',
|
||||
'sahara',
|
||||
'rocket'
|
||||
];
|
||||
|
||||
type ThemeType = typeof themes[number];
|
||||
|
||||
function createThemeStore() {
|
||||
const { subscribe, set, update } = writable<'light' | 'dark'>('dark');
|
||||
const { subscribe, set, update } = writable<ThemeType>('skeleton');
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
toggleTheme: () => update(theme => {
|
||||
const newTheme = theme === 'light' ? 'dark' : 'light';
|
||||
cycleTheme: () => update(currentTheme => {
|
||||
const currentIndex = themes.indexOf(currentTheme);
|
||||
const nextIndex = (currentIndex + 1) % themes.length;
|
||||
const newTheme = themes[nextIndex];
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.classList.toggle('dark', newTheme === 'dark');
|
||||
document.body.setAttribute('data-theme', newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
}
|
||||
return newTheme;
|
||||
}),
|
||||
setTheme: (theme: 'light' | 'dark') => {
|
||||
setTheme: (theme: ThemeType) => {
|
||||
set(theme);
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.classList.toggle('dark', theme === 'dark');
|
||||
document.body.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
},
|
||||
initTheme: () => {
|
||||
if (typeof document !== 'undefined') {
|
||||
const savedTheme = localStorage.getItem('theme') as ThemeType;
|
||||
if (savedTheme && themes.includes(savedTheme)) {
|
||||
set(savedTheme);
|
||||
document.body.setAttribute('data-theme', savedTheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const theme = createThemeStore();
|
||||
export const toggleTheme = theme.toggleTheme;
|
||||
export const cycleTheme = theme.cycleTheme;
|
||||
export const initTheme = theme.initTheme;
|
||||
|
||||
29
web/src/lib/types/markdown.ts
Normal file
29
web/src/lib/types/markdown.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
// Declare the interface first
|
||||
export interface Frontmatter {
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
tags: string[];
|
||||
updated?: string;
|
||||
author?: string;
|
||||
layout?: string;
|
||||
aliases?: string;
|
||||
images?: string[];
|
||||
}
|
||||
|
||||
export interface MdsvexCompileData {
|
||||
fm: Frontmatter;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// Then declare the module for .md files
|
||||
//declare module '*.md' {
|
||||
// import type { SvelteComponent } from 'svelte';
|
||||
// import type { PostMetadata } from '$lib/interfaces/post-interface';
|
||||
// export const metadata: PostMetadata;
|
||||
// export const frontmatter: Frontmatter;
|
||||
// const component: SvelteComponent;
|
||||
// export default class MarkdownComponent extends SvelteComponent {}
|
||||
// export default component;
|
||||
//}
|
||||
@@ -1,7 +1,9 @@
|
||||
<script>
|
||||
import '../app.postcss';
|
||||
import { AppShell } from '@skeletonlabs/skeleton';
|
||||
import ToastContainer from '$lib/components/ui/toast/ToastContainer.svelte';
|
||||
import { Toast } from '@skeletonlabs/skeleton';
|
||||
//import { getToastStore } from '@skeletonlabs/skeleton';
|
||||
import ToastContainer from '$lib/components/ui/toast/ToastContainer.svelte';
|
||||
import Footer from '$lib/components/home/Footer.svelte';
|
||||
import Header from '$lib/components/home/Header.svelte';
|
||||
import { initializeStores } from '@skeletonlabs/skeleton';
|
||||
@@ -17,6 +19,7 @@
|
||||
|
||||
onMount(() => {
|
||||
toastStore.trigger({
|
||||
type: 'info',
|
||||
message: "👋 Welcome to the site! Tell people about yourself and what you do.",
|
||||
background: 'variant-filled-primary',
|
||||
timeout: 3333,
|
||||
@@ -25,6 +28,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Toast />
|
||||
<ToastContainer />
|
||||
|
||||
{#key $page.url.pathname}
|
||||
@@ -38,7 +42,7 @@
|
||||
<div
|
||||
in:fly={{ duration: 500, delay: 100, y: 100 }}
|
||||
>
|
||||
<main class="mx-auto p-4">
|
||||
<main class="m-auto p-4">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Terminal from '../lib/components/home/Terminal.svelte';
|
||||
import Fabric from '../lib/components/home/Fabric.svelte';
|
||||
import Terminal from '$lib/components/ui/terminal/Terminal.svelte';
|
||||
import Connections from '$lib/components/ui/connections/Connections.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</script>
|
||||
|
||||
<div class="absolute inset-0 -z-10 overflow-hidden h-96">
|
||||
<Fabric particleCount={100} particleSize={3} particleSpeed={0.1} connectionDistance={100}/>
|
||||
<Connections particleCount={100} particleSize={3} particleSpeed={0.1} connectionDistance={100}/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-between items-center">
|
||||
|
||||
@@ -1,56 +1,12 @@
|
||||
<script>
|
||||
import SideNav from '$lib/components/ui/side-nav/SideNav.svelte';
|
||||
import Content from './README.md';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let toc = [];
|
||||
|
||||
onMount(() => {
|
||||
// Get all headings from the content
|
||||
const article = document.querySelector('article');
|
||||
if (article) {
|
||||
const headings = article.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
toc = Array.from(headings).map(heading => ({
|
||||
id: heading.id,
|
||||
text: heading.textContent,
|
||||
level: parseInt(heading.tagName.charAt(1))
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
function scrollToSection(id) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if Content}
|
||||
<div class="items-center mx-auto grid-cols-[80%_20%] grid gap-8 max-w-7xl relative">
|
||||
<article class="prose max-w-3xl flex-1">
|
||||
<div class="space-y-4">
|
||||
<svelte:component this={Content} />
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<nav class="hidden lg:block w-64 fixed top-24 right-[max(0px,calc(50%-45rem))] max-h-[calc(80vh-5rem)] overflow-y-auto">
|
||||
<div class="p-4 bg-card text-card-foreground">
|
||||
<h4 class="font-semibold mb-4">On this page</h4>
|
||||
<ul class="space-y-2">
|
||||
{#each toc as item}
|
||||
<li style="margin-left: {(item.level - 1) * 1}rem">
|
||||
<a
|
||||
href="#{item.id}"
|
||||
class="text-xs hover:text-primary transition-colors"
|
||||
on:click|preventDefault={() => scrollToSection(item.id)}
|
||||
>
|
||||
{item.text}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="items-center mx-auto pt-8 grid-cols-[80%_20%] grid gap-8 max-w-7xl relative">
|
||||
<svelte:component this={Content} />
|
||||
<SideNav />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="container py-12">
|
||||
@@ -60,11 +16,5 @@
|
||||
<p class="mt-2 text-sm text-muted-foreground">Check back later for new content.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
<!-- <style>
|
||||
:global(h1, h2, h3, h4, h5, h6) {
|
||||
scroll-margin-top: 5rem;
|
||||
}
|
||||
</style> -->
|
||||
@@ -1,20 +1,16 @@
|
||||
---
|
||||
title: README
|
||||
description: fabric is an open-source framework for augmenting humans using AI. It provides a modular framework for solving specific problems using a crowdsourced set of AI prompts that can be used anywhere.
|
||||
aliases: Fabric/Docs
|
||||
date: 2024-1-12
|
||||
updated: 2024-11-22
|
||||
---
|
||||
The UI for Fabric can be found [here](/chat).
|
||||
|
||||
<div align="center">
|
||||
<img src="/fabric-logo.gif" alt="fabriclogo" width="200" height="200"/>
|
||||
|
||||
# `fabric`
|
||||
|
||||

|
||||

|
||||
|
||||
[MIT Open Source License](https://opensource.org/licenses/MIT)
|
||||
|
||||
</div>
|
||||
<div align="center">
|
||||
<p class="align center">
|
||||
@@ -34,6 +30,12 @@ The UI for Fabric can be found [here](/chat).
|
||||
|
||||
</div>
|
||||
|
||||
<div class="justify-left flex gap-2">
|
||||
<img src="https://img.shields.io/github/languages/top/danielmiessler/fabric" alt="Github top language">
|
||||
<img src="https://img.shields.io/github/last-commit/danielmiessler/fabric" alt="GitHub last commit">
|
||||
<img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License">
|
||||
</div>
|
||||
|
||||
## Navigation
|
||||
|
||||
- [`fabric`](#fabric)
|
||||
@@ -448,7 +450,8 @@ The [examples](#examples) use the macOS program `pbpaste` to paste content from
|
||||
|
||||
On Windows, you can use the PowerShell command `Get-Clipboard` from a PowerShell command prompt. If you like, you can also alias it to `pbpaste`. If you are using classic PowerShell, edit the file `~\Documents\WindowsPowerShell\.profile.ps1`, or if you are using PowerShell Core, edit `~\Documents\PowerShell\.profile.ps1` and add the alias,
|
||||
|
||||
```powershell
|
||||
*powershell*
|
||||
```
|
||||
Set-Alias pbpaste Get-Clipboard
|
||||
```
|
||||
|
||||
@@ -493,4 +496,4 @@ alias pbpaste='xclip -selection clipboard -o'
|
||||
|
||||

|
||||
|
||||
</a>
|
||||
</a>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script>
|
||||
import Content from './Contact.md'
|
||||
import Content from '$lib/components/contact/Contact.svelte'
|
||||
|
||||
</script>
|
||||
|
||||
{#if Content}
|
||||
<article class="container max-w-3xl">
|
||||
<div class="space-y-4">
|
||||
<div class="container max-w-3xl">
|
||||
<div class="space-y-4 mx-auto py-8">
|
||||
<svelte:component this={Content} />
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
<div class="container py-12">
|
||||
@@ -19,4 +20,3 @@
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
title: "Contact"
|
||||
description: "Default Contact Page"
|
||||
date: 2024-11-24
|
||||
---
|
||||
<script lang="ts">
|
||||
import { Contact } from 'lucide-svelte';
|
||||
</script>
|
||||
|
||||
> **This is a placeholder contact page. No logic is implemented here.**
|
||||
|
||||
<div class="form-control w-full m-auto p-4 rounded-lg bg-gradient-to-br variant-gradient-success-warning shadow-lg text-current" title="contact form">
|
||||
<h2 class="font-bold pl-2">We'd love to hear from you</h2>
|
||||
<p class="font-bold pl-2">Email</p>
|
||||
<div class="input-group input-group-divider grid-cols-[1fr_auto]">
|
||||
<input type="text" placeholder="Enter an email address where you can be reached..." />
|
||||
</div>
|
||||
<p class="font-bold pl-2">Website</p>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
|
||||
<div class="input-group-shim">https://</div>
|
||||
<input type="text" placeholder="www.example.com" />
|
||||
</div>
|
||||
<p class="font-bold pl-2">Contact Information</p>
|
||||
<div class="input-group input-group-divider grid-cols-[1fr_auto]">
|
||||
<input type="text" placeholder="Enter a other contact information here..." />
|
||||
</div>
|
||||
<label class="label">
|
||||
<span class="font-bold pl-2">Message</span>
|
||||
<textarea class="textarea" rows="4" placeholder="Enter your message ..." />
|
||||
</label>
|
||||
<a href="/" title=""><button class="button variant-filled-secondary rounded-lg p-2"><Contact /></button></a>
|
||||
|
||||
</div>
|
||||
@@ -1,68 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { formatDistance } from 'date-fns';
|
||||
import type { PageData } from './$types';
|
||||
//import Search from './Search.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import Card from '$lib/components/ui/cards/card.svelte';
|
||||
import { Youtube } from 'svelte-youtube-lite';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { elasticOut, quintOut } from 'svelte/easing';
|
||||
import { Youtube } from 'svelte-youtube-lite';
|
||||
import PostCard from '$lib/components/posts/PostCard.svelte';
|
||||
import { InputChip } from '@skeletonlabs/skeleton';
|
||||
import Connections from '$lib/components/ui/connections/Connections.svelte';
|
||||
|
||||
let cards = false;
|
||||
let searchQuery = '';
|
||||
let searchQuery = '';
|
||||
let selectedTags: string[] = [];
|
||||
let allTags: string[] = [];
|
||||
|
||||
export let data: PageData;
|
||||
$: posts = data.posts;
|
||||
$: posts = data.posts || [];
|
||||
|
||||
// Extract all unique tags from posts
|
||||
$: {
|
||||
const tagSet = new Set<string>();
|
||||
posts.forEach(post => {
|
||||
post.meta.tags.forEach(tag => tagSet.add(tag));
|
||||
posts?.forEach(post => {
|
||||
post.metadata?.tags?.forEach(tag => tagSet.add(tag));
|
||||
});
|
||||
allTags = Array.from(tagSet);
|
||||
}
|
||||
|
||||
// Filter posts based on selected tags
|
||||
$: filteredPosts = posts.filter(post => {
|
||||
$: filteredPosts = posts?.filter(post => {
|
||||
if (selectedTags.length === 0) return true;
|
||||
return selectedTags.every(tag =>
|
||||
post.meta.tags.some(postTag => postTag.toLowerCase() === tag.toLowerCase())
|
||||
post.metadata?.tags?.some(postTag => postTag.toLowerCase() === tag.toLowerCase())
|
||||
);
|
||||
}) || [];
|
||||
|
||||
// Filter posts based on search query
|
||||
$: searchResults = filteredPosts.filter(post => {
|
||||
if (!searchQuery) return true;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return (
|
||||
post.metadata?.title?.toLowerCase().includes(query) ||
|
||||
post.metadata?.description?.toLowerCase().includes(query) ||
|
||||
post.metadata?.tags?.some(tag => tag.toLowerCase().includes(query))
|
||||
);
|
||||
});
|
||||
|
||||
function validateTag(value: string): boolean {
|
||||
return allTags.some(tag => tag.toLowerCase() === value.toLowerCase());
|
||||
}
|
||||
|
||||
let visible: boolean = true;
|
||||
</script>
|
||||
|
||||
<div class="container py-12">
|
||||
<!-- <Search /> -->
|
||||
|
||||
<div class="absolute inset-0 -z-10 overflow-hidden h-96">
|
||||
<Connections particleCount={100} particleSize={3} particleSpeed={0.1} connectionDistance={100}/>
|
||||
</div>
|
||||
|
||||
<div class="py-12">
|
||||
<h1 class="mb-4 text-3xl font-bold">Blog Posts</h1>
|
||||
<p class="text-sm mb-4 font-small">This blog is maintained in an Obsidian Vault</p>
|
||||
|
||||
<div >
|
||||
<div class="container mx-auto ml-auto grid grid-cols-1 md:grid-cols-2 gap-4 justify-end">
|
||||
<div class="container mx-auto justify-left">
|
||||
<img src="https://img.shields.io/github/languages/top/danielmiessler/fabric" alt="Github top language">
|
||||
<img src="https://img.shields.io/github/last-commit/danielmiessler/fabric" alt="GitHub last commit">
|
||||
<img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License">
|
||||
<br>
|
||||
<hr class="!border-t-4" />
|
||||
<br>
|
||||
<h4 class="h4"><b>Leverage Proven Patterns</b></h4>
|
||||
<br>
|
||||
<Youtube id="UbDyjIIGaxQ" title="Network Chuck Explains fabric" />
|
||||
<div class="mx-auto ml-auto grid grid-cols-1 md:grid-cols-2 gap-4 justify-end">
|
||||
<div class="container mx-auto justify-left">
|
||||
<img src="https://img.shields.io/github/languages/top/danielmiessler/fabric" alt="Github top language">
|
||||
<img src="https://img.shields.io/github/last-commit/danielmiessler/fabric" alt="GitHub last commit">
|
||||
<img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License">
|
||||
<br>
|
||||
<hr class="!border-t-4" />
|
||||
<br>
|
||||
<h4 class="h4"><b>Leverage Proven Patterns</b></h4>
|
||||
<br>
|
||||
<Youtube id="UbDyjIIGaxQ" title="Network Chuck Explains fabric" />
|
||||
<p>Post your favorite videos.</p>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="h4"><b>Share Your Most Important Toughts and Ideas</b></h4>
|
||||
<br>
|
||||
<Card
|
||||
</div>
|
||||
<div class="container mx-auto ml-auto grid grid-cols-1 md:grid-cols-2 gap-4 m-4">
|
||||
|
||||
<div>
|
||||
<h4 class=""><b>Find your interests, build your knowledge</b></h4>
|
||||
</div>
|
||||
<div class="m-auto md:col-start-1">
|
||||
<h4><b>share it with people</b></h4>
|
||||
</div>
|
||||
<div class="md:col-start-2">
|
||||
<br>
|
||||
<Card
|
||||
header="Let Your Voice Be Heard"
|
||||
imageUrl="/brain.png"
|
||||
imageAlt="Blog post header image"
|
||||
@@ -72,10 +92,9 @@
|
||||
authorAvatarUrl=""
|
||||
link="/"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto ml-auto grid grid-cols-1 md:grid-cols-2 gap-4 mt-8">
|
||||
</div>
|
||||
</div>
|
||||
<div class="container mx-auto ml-auto grid grid-cols-1 md:grid-cols-2 gap-4 mt-8">
|
||||
<Card
|
||||
header="Curate Your Content"
|
||||
imageUrl="/electric.png"
|
||||
@@ -90,6 +109,47 @@
|
||||
<blockquote class="blockquote">There are countless use cases for AI. What will you use if for?</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container mx-auto ml-auto grid grid-cols-1 md:grid-cols-2 gap-4 justify-end mt-8 pb-8">
|
||||
<div class="container mx-auto md:col-start-2 justify-left">
|
||||
<hr class="!border-t-4" />
|
||||
<br>
|
||||
<h4 class="h4">Showcase your interests. Tell people what you've been working on. Create your community.</h4>
|
||||
</div>
|
||||
<div class="md:col-start-1">
|
||||
<Card
|
||||
header="Explore the Possibilities"
|
||||
imageUrl=""
|
||||
imageAlt="Blog post header image"
|
||||
title="Enter a new title here"
|
||||
content="What will you share?"
|
||||
authorName="Your Name Here"
|
||||
authorAvatarUrl=""
|
||||
link="/"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="container mx-auto p-12 m-24 justify-right">
|
||||
<blockquote class="blockquote">There are countless use cases for AI. What will you use if for?</blockquote>
|
||||
</div>
|
||||
|
||||
<div class="rounded-tl-container-token m-auto grid grid-cols-1 gap-4 mt-8">
|
||||
<div class="mx-auto">something here</div>
|
||||
<!-- <Card
|
||||
header="Curate Your Content"
|
||||
imageUrl="/electric.png"
|
||||
imageAlt="Blog post header image"
|
||||
title="Enter a new title here"
|
||||
content="What will you share"
|
||||
authorName="Your Name Here"
|
||||
authorAvatarUrl=""
|
||||
link="/"
|
||||
/> -->
|
||||
<div class="container mx-auto justify-right">
|
||||
<blockquote class="blockquote">There are countless use cases for AI. What will you use if for?</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container mx-auto ml-auto grid grid-cols-1 md:grid-cols-2 gap-4 justify-end mt-8 pb-8">
|
||||
<div class="container mx-auto justify-left">
|
||||
<hr class="!border-t-4" />
|
||||
@@ -109,114 +169,18 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Tag search and filter section -->
|
||||
<div class="mb-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<InputChip
|
||||
bind:value={selectedTags}
|
||||
name="tags"
|
||||
placeholder="Search and press Enter to add tags..."
|
||||
validation={validateTag}
|
||||
allowDuplicates={false}
|
||||
class="input"
|
||||
/>
|
||||
<div class="tags-container overflow-x-auto pb-2">
|
||||
<div class="flex gap-2">
|
||||
{#each allTags.filter(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())) as tag}
|
||||
<button
|
||||
class="tag-button px-3 py-1 rounded-full text-sm font-medium transition-colors
|
||||
{selectedTags.includes(tag.toLowerCase())
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-secondary hover:bg-secondary/80'}"
|
||||
on:click={() => {
|
||||
const tagLower = tag.toLowerCase();
|
||||
if (!selectedTags.includes(tagLower)) {
|
||||
selectedTags = [...selectedTags, tagLower];
|
||||
}
|
||||
searchQuery = '';
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container py-12">
|
||||
<div class="my-4">
|
||||
<InputChip
|
||||
name="tags"
|
||||
placeholder="Filter by tags..."
|
||||
validation={validateTag}
|
||||
bind:value={selectedTags}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if filteredPosts.length === 0}
|
||||
{#if !visible}
|
||||
<aside class="alert variant-ghost">
|
||||
<div>(icon)</div>
|
||||
<slot:fragment href="./+error.svelte" />
|
||||
<div class="alert-actions">(buttons)</div>
|
||||
</aside>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each filteredPosts as post}
|
||||
<article class="card card-hover group relative rounded-lg border p-6 hover:bg-muted/50">
|
||||
<a href="/posts/{post.slug}" class="absolute inset-0">
|
||||
<span class="sr-only">View {post.meta.title}</span>
|
||||
</a>
|
||||
<div class="flex flex-col justify-between space-y-4">
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-xl font-semibold tracking-tight">{post.meta.title}</h2>
|
||||
<p class="text-muted-foreground">{post.meta.description}</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4 text-sm text-muted-foreground">
|
||||
<time datetime={post.meta.date}>
|
||||
{formatDistance(new Date(post.meta.date), new Date(), { addSuffix: true })}
|
||||
</time>
|
||||
{#if post.meta.tags.length > 0}
|
||||
<span class="text-xs">•</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each post.meta.tags as tag}
|
||||
<a
|
||||
href="/tags/{tag}"
|
||||
class="inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-semibold transition-colors hover:bg-secondary"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
{/each}
|
||||
<!-- <Paginator records={posts} limit={6} buttonClass="btn" /> -->
|
||||
{#each searchResults as post}
|
||||
<PostCard {post} /> <!-- TODO: Add images to post metadata -->
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
.tags-container {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-primary) transparent;
|
||||
}
|
||||
|
||||
.tags-container::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.tags-container::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tags-container::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-primary);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.tag-button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,41 +1,29 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
import type { Frontmatter } from '$lib/types/markdown';
|
||||
|
||||
// This is duplicated at components/ui/tagSearch/tags.ts
|
||||
// Consider removing this duplication
|
||||
|
||||
const posts = import.meta.glob<{ metadata: Frontmatter }>('/src/lib/content/posts/*.{md,svx}', { eager: true });
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
try {
|
||||
const postFiles = import.meta.glob('/src/lib/content/posts/*.{md,svx}', { eager: true });
|
||||
|
||||
if (Object.keys(postFiles).length === 0) {
|
||||
return {
|
||||
posts: []
|
||||
};
|
||||
}
|
||||
try {
|
||||
const allPosts = Object.entries(posts).map(([path, post]) => ({
|
||||
slug: path.split('/').pop()?.replace(/\.(md|svx)$/, '') ?? '',
|
||||
metadata: post.metadata,
|
||||
/* date: post.metadata.date,
|
||||
updated: post.metadata.updated || post.metadata.date */
|
||||
//}
|
||||
}));
|
||||
|
||||
const posts = Object.entries(postFiles).map(([path, post]: [string, any]) => {
|
||||
const slug = path.split('/').pop()?.replace(/\.(md|svx)$/, '');
|
||||
return {
|
||||
slug,
|
||||
meta: {
|
||||
title: post.metadata?.title || 'Untitled',
|
||||
date: post.metadata?.date || new Date().toISOString(),
|
||||
created: post.metadata?.created || new Date().toISOString(),
|
||||
description: post.metadata?.description || '',
|
||||
tags: post.metadata?.tags || [],
|
||||
aliases: post.metadata?.aliases || [],
|
||||
lead: post.metadata?.lead || '',
|
||||
updated: post.metadata?.updated || new Date().toISOString(),
|
||||
author: post.metadata?.author || 'Your Name Here',
|
||||
}
|
||||
};
|
||||
});
|
||||
// Sort posts by date, newest first
|
||||
allPosts.sort((a, b) =>
|
||||
new Date(b.metadata.date).getTime() - new Date(a.metadata.date).getTime()
|
||||
);
|
||||
|
||||
posts.sort((a, b) => new Date(b.meta.date).getTime() - new Date(a.meta.date).getTime());
|
||||
|
||||
return {
|
||||
posts
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Failed to load posts:', e);
|
||||
throw error(500, 'Failed to load posts');
|
||||
}
|
||||
};
|
||||
return { posts: allPosts };
|
||||
} catch (e) {
|
||||
console.error('Failed to load posts:', e);
|
||||
throw Error();
|
||||
}
|
||||
};
|
||||
|
||||
264
web/src/routes/posts/Search.svelte
Normal file
264
web/src/routes/posts/Search.svelte
Normal file
@@ -0,0 +1,264 @@
|
||||
<script lang="ts">
|
||||
import { formatDistance } from 'date-fns';
|
||||
import type { PageData } from './$types';
|
||||
import Card from '$lib/components/ui/cards/card.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { elasticOut, quintOut } from 'svelte/easing';
|
||||
import { InputChip } from '@skeletonlabs/skeleton';
|
||||
|
||||
let cards = false;
|
||||
let searchQuery = '';
|
||||
let selectedTags: string[] = [];
|
||||
let allTags: string[] = [];
|
||||
|
||||
export let data: PageData;
|
||||
$: posts = data.posts;
|
||||
|
||||
// Extract all unique tags from Posts
|
||||
$: {
|
||||
const tagSet = new Set<string>();
|
||||
posts.forEach(post => {
|
||||
post.meta.tags.forEach(tag => tagSet.add(tag));
|
||||
});
|
||||
allTags = Array.from(tagSet);
|
||||
}
|
||||
|
||||
// Filter posts based on selected tags-container
|
||||
$: filteredPosts = posts.filter(post => {
|
||||
if (selectedTags.length === 0) return true;
|
||||
return selectedTags.every(tag =>
|
||||
post.meta.tags.some(postTag => postTag.toLowerCase() === tag.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
function validateTag(value: string): boolean {
|
||||
return allTags.some(tag => tag.toLowerCase() === value.toLowerCase());
|
||||
}
|
||||
|
||||
let visible: boolean = true;
|
||||
</script>
|
||||
|
||||
<!-- This file can be deleted, It think it has better search functionality but it needs work to ...work
|
||||
Could this be the new component for the search bar?
|
||||
|
||||
<script lang="ts">
|
||||
import { formatDistance } from 'date-fns';
|
||||
import type { PageData } from './$types';
|
||||
import Card from '$lib/components/ui/cards/card.svelte';
|
||||
import { Youtube } from 'svelte-youtube-lite';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { elasticOut, quintOut } from 'svelte/easing';
|
||||
import { InputChip } from '@skeletonlabs/skeleton';
|
||||
|
||||
let cards = false;
|
||||
let searchQuery = '';
|
||||
let selectedTags: string[] = [];
|
||||
let allTags: string[] = [];
|
||||
|
||||
export let data: PageData;
|
||||
$: posts = data.posts;
|
||||
|
||||
// Extract all unique tags from posts
|
||||
$: {
|
||||
const tagSet = new Set<string>();
|
||||
posts.forEach(post => {
|
||||
post.meta.tags.forEach(tag => tagSet.add(tag));
|
||||
});
|
||||
allTags = Array.from(tagSet);
|
||||
}
|
||||
|
||||
// Filter posts based on selected tags
|
||||
$: filteredPosts = posts.filter(post => {
|
||||
if (selectedTags.length === 0) return true;
|
||||
return selectedTags.every(tag =>
|
||||
post.meta.tags.some(postTag => postTag.toLowerCase() === tag.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
function validateTag(value: string): boolean {
|
||||
return allTags.some(tag => tag.toLowerCase() === value.toLowerCase());
|
||||
}
|
||||
|
||||
let visible: boolean = true;
|
||||
</script>
|
||||
|
||||
<div class="container py-12">
|
||||
<h1 class="mb-4 text-3xl font-bold">Blog Posts</h1>
|
||||
<p class="text-sm mb-4 font-small">This blog is maintained in an Obsidian Vault</p>
|
||||
|
||||
<div >
|
||||
<div class="container mx-auto ml-auto grid grid-cols-1 md:grid-cols-2 gap-4 justify-end">
|
||||
<div class="container mx-auto justify-left">
|
||||
<img src="https://img.shields.io/github/languages/top/danielmiessler/fabric" alt="Github top language">
|
||||
<img src="https://img.shields.io/github/last-commit/danielmiessler/fabric" alt="GitHub last commit">
|
||||
<img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License">
|
||||
<br>
|
||||
<hr class="!border-t-4" />
|
||||
<br>
|
||||
<h4 class="h4"><b>Leverage Proven Patterns</b></h4>
|
||||
<br>
|
||||
<Youtube id="UbDyjIIGaxQ" title="Network Chuck Explains fabric" />
|
||||
<p>Post your favorite videos.</p>
|
||||
<br>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="h4"><b>Share Your Most Important Toughts and Ideas</b></h4>
|
||||
<br>
|
||||
<Card
|
||||
header="Let Your Voice Be Heard"
|
||||
imageUrl="/brain.png"
|
||||
imageAlt="Blog post header image"
|
||||
title="Blogging, Podcasting, Videos, and More."
|
||||
content="What will you create?"
|
||||
authorName="Your Name Here"
|
||||
authorAvatarUrl=""
|
||||
link="/"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto ml-auto grid grid-cols-1 md:grid-cols-2 gap-4 mt-8">
|
||||
<Card
|
||||
header="Curate Your Content"
|
||||
imageUrl="/electric.png"
|
||||
imageAlt="Blog post header image"
|
||||
title="Enter a new title here"
|
||||
content="What will you share"
|
||||
authorName="Your Name Here"
|
||||
authorAvatarUrl=""
|
||||
link="/"
|
||||
/>
|
||||
<div class="container mx-auto justify-right">
|
||||
<blockquote class="blockquote">There are countless use cases for AI. What will you use if for?</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container mx-auto ml-auto grid grid-cols-1 md:grid-cols-2 gap-4 justify-end mt-8 pb-8">
|
||||
<div class="container mx-auto justify-left">
|
||||
<hr class="!border-t-4" />
|
||||
<br>
|
||||
<h4 class="h4">Showcase your interests. Tell people what you've been working on. Create your community.</h4>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
header="Explore the Possibilities"
|
||||
imageUrl=""
|
||||
imageAlt="Blog post header image"
|
||||
title="Enter a new title here"
|
||||
content="What will you share?"
|
||||
authorName="Your Name Here"
|
||||
authorAvatarUrl=""
|
||||
link="/"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<!-- Tag search and filter section -->
|
||||
<div class="mb-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<InputChip
|
||||
bind:value={selectedTags}
|
||||
name="tags"
|
||||
placeholder="Search and press Enter to add tags..."
|
||||
validation={validateTag}
|
||||
allowDuplicates={false}
|
||||
class="input"
|
||||
/>
|
||||
<div class="tags-container overflow-x-auto pb-2">
|
||||
<div class="flex gap-2">
|
||||
{#each allTags.filter(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())) as tag}
|
||||
<button
|
||||
class="tag-button px-3 py-1 rounded-full text-sm font-medium transition-colors
|
||||
{selectedTags.includes(tag.toLowerCase())
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-secondary hover:bg-secondary/80'}"
|
||||
on:click={() => {
|
||||
const tagLower = tag.toLowerCase();
|
||||
if (!selectedTags.includes(tagLower)) {
|
||||
selectedTags = [...selectedTags, tagLower];
|
||||
}
|
||||
searchQuery = '';
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if filteredPosts.length === 0}
|
||||
{#if !visible}
|
||||
<aside class="alert variant-ghost">
|
||||
<div>(icon)</div>
|
||||
<slot:fragment href="./+error.svelte" />
|
||||
<div class="alert-actions">(buttons)</div>
|
||||
</aside>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each filteredPosts as post}
|
||||
<article class="card card-hover group relative rounded-lg border p-6 hover:bg-muted/50">
|
||||
<a href="/posts/{post.slug}" class="absolute inset-0">
|
||||
<span class="sr-only">View {post.meta.title}</span>
|
||||
</a>
|
||||
<div class="flex flex-col justify-between space-y-4">
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-xl font-semibold tracking-tight">{post.meta.title}</h2>
|
||||
<p class="text-muted-foreground">{post.meta.description}</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4 text-sm text-muted-foreground">
|
||||
<time datetime={post.meta.date}>
|
||||
{formatDistance(new Date(post.meta.date), new Date(), { addSuffix: true })}
|
||||
</time>
|
||||
{#if post.meta.tags.length > 0}
|
||||
<span class="text-xs">•</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each post.meta.tags as tag}
|
||||
<a
|
||||
href="/tags/{tag}"
|
||||
class="inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-semibold transition-colors hover:bg-secondary"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
{/each}
|
||||
<!-- <Paginator records={posts} limit={6} buttonClass="btn" /> -->
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
.tags-container {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-primary) transparent;
|
||||
}
|
||||
|
||||
.tags-container::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.tags-container::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tags-container::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-primary);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.tag-button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,38 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
||||
import PostContent from '$lib/components/posts/PostContent.svelte';
|
||||
import PostMeta from '$lib/components/posts/PostMeta.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
$: ({ content: Content, meta } = data);
|
||||
console.log('Page data:', data);
|
||||
</script>
|
||||
|
||||
<article class="container max-w-3xl py-6 lg:py-2">
|
||||
{#await Content}
|
||||
<div class="flex min-h-[400px] items-center justify-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Spinner class="h-6 w-6" />
|
||||
<span class="text-sm text-muted-foreground">Loading post...</span>
|
||||
</div>
|
||||
</div>
|
||||
{:then Content}
|
||||
<div class="space-y-4">
|
||||
<h1 class="inline-block text-4xl font-bold lg:text-5xl">{meta.title}</h1>
|
||||
|
||||
</div>
|
||||
<div class="prose prose-slate dark:prose-invert">
|
||||
<svelte:component this={Content} />
|
||||
</div>
|
||||
{:catch error}
|
||||
<div class="flex min-h-[400px] flex-col items-center justify-center text-center">
|
||||
<p class="text-lg font-medium">Failed to load post</p>
|
||||
<p class="mt-2 text-sm text-muted-foreground">{error.message}</p>
|
||||
<a
|
||||
href="/posts"
|
||||
class="mt-4 inline-flex items-center justify-center rounded-md bg-primary px-8 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
Back to Posts
|
||||
</a>
|
||||
</div>
|
||||
{/await}
|
||||
</article>
|
||||
<PostContent post={{
|
||||
content: data.content,
|
||||
metadata: data.metadata,
|
||||
slug: data.slug
|
||||
}} />
|
||||
|
||||
@@ -1,26 +1,46 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
import type { Frontmatter } from '$lib/types/markdown';
|
||||
|
||||
const posts = import.meta.glob<{ metadata: Frontmatter, default: unknown }>('/src/lib/content/posts/*.{md,svx}', { eager: true });
|
||||
// Refractor to use the Frontmatter type from $lib/types/markdown
|
||||
//const posts = import.meta.glob<{
|
||||
// metadata: {
|
||||
// title: string;
|
||||
// description: string;
|
||||
// date: string;
|
||||
// tags: string[];
|
||||
// updated?: string;
|
||||
// author?: string;
|
||||
// aliases?: string[];
|
||||
// };
|
||||
// default: unknown;
|
||||
//}>('/src/lib/content/posts/*.{md,svx}', { eager: true });
|
||||
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
try {
|
||||
const post = await import(`$lib/content/posts/${params.slug}.md`);
|
||||
|
||||
return {
|
||||
content: post.default,
|
||||
meta: {
|
||||
title: post.metadata.title,
|
||||
aliases: post.metadata.aliases || [],
|
||||
date: post.metadata.date,
|
||||
description: post.metadata.description,
|
||||
tags: post.metadata.tags || [],
|
||||
updated: post.metadata.updated || new Date().toISOString(),
|
||||
author: post.metadata.author || '',
|
||||
lead: post.metadata.lead || '',
|
||||
reference: post.metadata.reference || '',
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(`Failed to load post ${params.slug}:`, e);
|
||||
throw error(404, `Could not find post ${params.slug}`);
|
||||
}
|
||||
};
|
||||
const post = Object.entries(posts).find(([path]) =>
|
||||
path.endsWith(`${params.slug}.md`) || path.endsWith(`${params.slug}.svx`)
|
||||
);
|
||||
|
||||
if (!post) {
|
||||
throw error(404, `Post ${params.slug} not found`);
|
||||
}
|
||||
|
||||
function formatDateOnly(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
return {
|
||||
content: post[1].default,
|
||||
metadata: {
|
||||
...post[1].metadata,
|
||||
// Only keep the date portion YYYY-MM-DD
|
||||
date: formatDateOnly(post[1].metadata.date),
|
||||
updated: post[1].metadata.updated
|
||||
? formatDateOnly(post[1].metadata.updated)
|
||||
: formatDateOnly(post[1].metadata.date)
|
||||
},
|
||||
slug: params.slug
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,55 +5,81 @@ import rehypeSlug from 'rehype-slug';
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
||||
import rehypeExternalLinks from 'rehype-external-links';
|
||||
import rehypeUnwrapImages from 'rehype-unwrap-images';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { escapeSvelte } from 'mdsvex';
|
||||
//import { fileURLToPath } from 'url';
|
||||
//import { dirname, join } from 'path';
|
||||
import { getSingletonHighlighter } from 'shiki'
|
||||
import dracula from 'shiki/themes/dracula.mjs'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
//const __filename = fileURLToPath(import.meta.url);
|
||||
//const __dirname = dirname(__filename);
|
||||
|
||||
// Initialize Shiki highlighter
|
||||
const initializeHighlighter = async () => {
|
||||
try {
|
||||
return await getSingletonHighlighter({
|
||||
themes: ['dracula'],
|
||||
langs: ['javascript', 'typescript', 'svelte', 'markdown', 'bash', 'go', 'text', 'python', 'rust', 'c', 'c++', 'shell', 'ruby', 'json', 'html', 'css', 'java', 'sql', 'toml', 'yaml']
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Shiki highlighter:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
let shikiHighlighterPromise = initializeHighlighter();
|
||||
|
||||
/** @type {import('mdsvex').MdsvexOptions} */
|
||||
const mdsvexOptions = {
|
||||
extensions: ['.md', '.svx'],
|
||||
smartypants: {
|
||||
quotes: true,
|
||||
ellipses: true,
|
||||
backticks: true,
|
||||
dashes: true,
|
||||
},
|
||||
layout: {
|
||||
_: join(__dirname, './src/lib/layouts/post.svelte')
|
||||
_: './src/lib/components/posts/PostLayout.svelte',
|
||||
},
|
||||
highlight: {
|
||||
theme: {
|
||||
dark: 'github-dark',
|
||||
light: 'github-light'
|
||||
highlighter: async (code, lang) => {
|
||||
try {
|
||||
const highlighter = await shikiHighlighterPromise;
|
||||
if (!highlighter) {
|
||||
console.warn('Shiki highlighter not available, falling back to plain text');
|
||||
return `<pre><code>${code}</code></pre>`;
|
||||
}
|
||||
const html = escapeSvelte(highlighter.codeToHtml(code, { lang, theme: dracula }));
|
||||
return `{@html \`${html}\`}`;
|
||||
} catch (error) {
|
||||
console.error('Failed to highlight code:', error);
|
||||
return `<pre><code>${code}</code></pre>`;
|
||||
}
|
||||
}
|
||||
},
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
rehypeUnwrapImages,
|
||||
[rehypeAutolinkHeadings, {
|
||||
behavior: 'wrap'
|
||||
}],
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
rehypeUnwrapImages,
|
||||
[rehypeAutolinkHeadings, {behavior: 'wrap'}],
|
||||
[rehypeExternalLinks, {
|
||||
target: '_blank',
|
||||
rel: ['noopener', 'noreferrer']
|
||||
rel: ['nofollow', 'noopener', 'noreferrer']
|
||||
}]
|
||||
]
|
||||
],
|
||||
};
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
alias: {
|
||||
$components: join(__dirname, 'src/lib/components'),
|
||||
$lib: join(__dirname, 'src/lib'),
|
||||
$styles: join(__dirname, 'src/styles'),
|
||||
$stores: join(__dirname, 'src/lib/stores'),
|
||||
$content: join(__dirname, 'src/content'),
|
||||
$utils: join(__dirname, 'src/lib/utils')
|
||||
}
|
||||
},
|
||||
extensions: ['.svelte', '.md', '.svx'],
|
||||
preprocess: [
|
||||
vitePreprocess(),
|
||||
mdsvex(mdsvexOptions)
|
||||
]
|
||||
};
|
||||
|
||||
export default config;
|
||||
extensions: ['.svelte', '.md', '.svx'],
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
},
|
||||
preprocess: [
|
||||
vitePreprocess({
|
||||
script: true,
|
||||
}),
|
||||
mdsvex(mdsvexOptions)
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -6,12 +6,12 @@ import { skeleton } from '@skeletonlabs/tw-plugin';
|
||||
import { myCustomTheme } from './my-custom-theme.ts'
|
||||
|
||||
export default {
|
||||
darkMode: 'class',
|
||||
content: [
|
||||
'./src/**/*.{html,js,svelte,svx,md,ts}',
|
||||
join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts,svx,md}')
|
||||
],
|
||||
theme: {
|
||||
darkMode: 'class',
|
||||
content: [
|
||||
'./src/**/*.{html,js,svelte,svx,md,ts}',
|
||||
join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts,svx,md}')
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
@@ -19,7 +19,7 @@ export default {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
@@ -86,25 +86,57 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
forms,
|
||||
typography,
|
||||
skeleton({
|
||||
themes: {
|
||||
preset: [
|
||||
{
|
||||
name: 'skeleton',
|
||||
enhancements: true
|
||||
},
|
||||
{
|
||||
name: 'modern',
|
||||
enhancements: true
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
forms,
|
||||
typography,
|
||||
skeleton({
|
||||
themes: {
|
||||
preset: [
|
||||
{
|
||||
name: 'skeleton',
|
||||
enhancements: true
|
||||
},
|
||||
{
|
||||
name: 'modern',
|
||||
enhancements: true
|
||||
},
|
||||
{
|
||||
name: 'crimson',
|
||||
enhancements: true
|
||||
},
|
||||
{
|
||||
name: 'hamlindigo',
|
||||
enhancements: true
|
||||
},
|
||||
{
|
||||
name: 'gold-nouveau',
|
||||
enhancements: true
|
||||
},
|
||||
{
|
||||
name: 'seafoam',
|
||||
enhancements: true
|
||||
},
|
||||
{
|
||||
name: 'rocket',
|
||||
enhancements: true
|
||||
},
|
||||
{
|
||||
name: 'sahara',
|
||||
enhancements: true
|
||||
},
|
||||
{
|
||||
name: 'wintry',
|
||||
enhancements: true
|
||||
},
|
||||
{
|
||||
name: 'vintage',
|
||||
enhancements: true
|
||||
},
|
||||
],
|
||||
custom: [
|
||||
myCustomTheme
|
||||
]
|
||||
}
|
||||
})
|
||||
]
|
||||
} satisfies Config;
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
import { purgeCss } from 'vite-plugin-tailwind-purgecss';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import { mdsvex } from 'mdsvex';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit(), mdsvex(), purgeCss()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
usePolling: true,
|
||||
interval: 100,
|
||||
ignored: ['**/node_modules/**', '**/dist/**', '**/.git/**']
|
||||
}
|
||||
},
|
||||
optimizeDeps: {}
|
||||
});
|
||||
plugins: [sveltekit(), purgeCss()],
|
||||
define: {
|
||||
'process.env': {
|
||||
NODE_ENV: JSON.stringify(process.env.NODE_ENV)
|
||||
},
|
||||
'process.platform': JSON.stringify(process.platform),
|
||||
'process.cwd': JSON.stringify('/'),
|
||||
'process.browser': true,
|
||||
'process': {
|
||||
cwd: () => ('/')
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
process: 'process/browser'
|
||||
}
|
||||
},
|
||||
server: {
|
||||
fs: {
|
||||
allow: ['..'] // allows importing from the parent directory
|
||||
},
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
usePolling: true,
|
||||
interval: 100,
|
||||
ignored: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/.svelte-kit/**']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user