feat: add chat and website updates

This commit is contained in:
Mohamed Boudra
2026-03-29 12:49:05 +07:00
parent 147482d6ed
commit 094864d779
26 changed files with 677 additions and 126 deletions
@@ -16,6 +16,7 @@ import {
import { Mic } from "lucide-react";
import { HeroMockup } from "~/components/hero-mockup";
import { ClaudeIcon } from "~/components/mockup";
import { SiteHeader } from "~/components/site-header";
import "~/styles.css";
interface LandingPageProps {
@@ -70,6 +71,12 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
<div className="space-y-3">
<p className="text-white/60 font-medium">Product</p>
<div className="space-y-2">
<a
href="/blog"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Blog
</a>
<a
href="/docs"
className="block text-white/40 hover:text-white/60 transition-colors"
@@ -186,68 +193,8 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
function Nav() {
return (
<nav className="flex flex-col sm:flex-row items-center sm:justify-between gap-4 mb-16">
<div className="flex items-center gap-3">
<a href="/" className="flex items-center gap-3">
<img src="/logo.svg" alt="Paseo" className="w-7 h-7" />
<span className="text-lg font-medium">Paseo</span>
</a>
</div>
<div className="flex items-center gap-4">
<a
href="/docs"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Docs
</a>
<a
href="/changelog"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Changelog
</a>
<a
href="/download"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Download
</a>
<a
href="https://discord.gg/jz8T2uahpH"
target="_blank"
rel="noopener noreferrer"
aria-label="Discord"
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
>
<svg
role="img"
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
</svg>
</a>
<a
href="https://github.com/getpaseo/paseo"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 0C5.37 0 0 5.484 0 12.252c0 5.418 3.438 10.013 8.205 11.637.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.738-4.042-1.61-4.042-1.61-.546-1.403-1.333-1.776-1.333-1.776-1.089-.756.084-.741.084-.741 1.205.087 1.838 1.262 1.838 1.262 1.07 1.87 2.809 1.33 3.495 1.017.108-.79.417-1.33.76-1.636-2.665-.31-5.467-1.35-5.467-6.005 0-1.327.465-2.413 1.235-3.262-.124-.31-.535-1.556.117-3.243 0 0 1.008-.33 3.3 1.248a11.2 11.2 0 0 1 3.003-.404c1.02.005 2.045.138 3.003.404 2.29-1.578 3.297-1.248 3.297-1.248.653 1.687.242 2.933.118 3.243.77.85 1.233 1.935 1.233 3.262 0 4.667-2.807 5.692-5.48 5.995.43.38.823 1.133.823 2.285 0 1.65-.015 2.98-.015 3.386 0 .315.218.694.825.576C20.565 22.26 24 17.667 24 12.252 24 5.484 18.627 0 12 0z" />
</svg>
</a>
</div>
<nav className="mb-16">
<SiteHeader />
</nav>
);
}
@@ -0,0 +1,73 @@
import "~/styles.css";
export function SiteHeader() {
return (
<header className="flex items-center justify-between gap-4">
<a href="/" className="flex items-center gap-3">
<img src="/logo.svg" alt="Paseo" className="w-6 h-6" />
<span className="text-lg font-medium">Paseo</span>
</a>
<div className="flex items-center gap-4">
<a
href="/blog"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Blog
</a>
<a
href="/docs"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Docs
</a>
<a
href="/changelog"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Changelog
</a>
<a
href="/download"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Download
</a>
<a
href="https://discord.gg/jz8T2uahpH"
target="_blank"
rel="noopener noreferrer"
aria-label="Discord"
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
>
<svg
role="img"
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
</svg>
</a>
<a
href="https://github.com/getpaseo/paseo"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 0C5.37 0 0 5.484 0 12.252c0 5.418 3.438 10.013 8.205 11.637.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.738-4.042-1.61-4.042-1.61-.546-1.403-1.333-1.776-1.333-1.776-1.089-.756.084-.741.084-.741 1.205.087 1.838 1.262 1.838 1.262 1.07 1.87 2.809 1.33 3.495 1.017.108-.79.417-1.33.76-1.636-2.665-.31-5.467-1.35-5.467-6.005 0-1.327.465-2.413 1.235-3.262-.124-.31-.535-1.556.117-3.243 0 0 1.008-.33 3.3 1.248a11.2 11.2 0 0 1 3.003-.404c1.02.005 2.045.138 3.003.404 2.29-1.578 3.297-1.248 3.297-1.248.653 1.687.242 2.933.118 3.243.77.85 1.233 1.935 1.233 3.262 0 4.667-2.807 5.692-5.48 5.995.43.38.823 1.133.823 2.285 0 1.65-.015 2.98-.015 3.386 0 .315.218.694.825.576C20.565 22.26 24 17.667 24 12.252 24 5.484 18.627 0 12 0z" />
</svg>
</a>
</div>
</header>
);
}
+82
View File
@@ -0,0 +1,82 @@
interface PostFrontmatter {
title: string;
description: string;
date: string;
draft: boolean;
}
export interface Post {
slug: string;
frontmatter: PostFrontmatter;
content: string;
}
function parseFrontmatter(raw: string): { data: Record<string, string>; content: string } {
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!match) return { data: {}, content: raw };
const data: Record<string, string> = {};
for (const line of match[1].split("\n")) {
const colonIdx = line.indexOf(":");
if (colonIdx === -1) continue;
const key = line.slice(0, colonIdx).trim();
const value = line
.slice(colonIdx + 1)
.trim()
.replace(/^["']|["']$/g, "");
data[key] = value;
}
return { data, content: match[2] };
}
const postModules = import.meta.glob("../posts/**/*.md", {
eager: true,
query: "?raw",
import: "default",
}) as Record<string, string>;
function loadPosts(includeDrafts: boolean): Post[] {
const posts: Post[] = [];
for (const [path, raw] of Object.entries(postModules)) {
const isDraft = path.includes("/drafts/");
if (isDraft && !includeDrafts) continue;
const { data, content } = parseFrontmatter(raw);
const fileName = path.split("/").pop()!.replace(".md", "");
const slug = isDraft ? `drafts/${fileName}` : fileName;
posts.push({
slug,
frontmatter: {
title: data.title ?? "",
description: data.description ?? "",
date: data.date ?? "",
draft: isDraft,
},
content,
});
}
posts.sort(
(a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime(),
);
return posts;
}
export function getPosts(includeDrafts = false): Post[] {
return loadPosts(includeDrafts);
}
export function getPost(slug: string): Post | undefined {
return loadPosts(true).find((p) => p.slug === slug);
}
export function formatDate(date: Date): string {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
return `${year}-${month}-${day}`;
}
+69
View File
@@ -16,8 +16,10 @@ import { Route as DocsRouteImport } from './routes/docs'
import { Route as CodexRouteImport } from './routes/codex'
import { Route as ClaudeCodeRouteImport } from './routes/claude-code'
import { Route as ChangelogRouteImport } from './routes/changelog'
import { Route as BlogRouteImport } from './routes/blog'
import { Route as IndexRouteImport } from './routes/index'
import { Route as DocsIndexRouteImport } from './routes/docs/index'
import { Route as BlogIndexRouteImport } from './routes/blog/index'
import { Route as DocsWorktreesRouteImport } from './routes/docs/worktrees'
import { Route as DocsVoiceRouteImport } from './routes/docs/voice'
import { Route as DocsUpdatesRouteImport } from './routes/docs/updates'
@@ -25,6 +27,7 @@ import { Route as DocsSecurityRouteImport } from './routes/docs/security'
import { Route as DocsConfigurationRouteImport } from './routes/docs/configuration'
import { Route as DocsCliRouteImport } from './routes/docs/cli'
import { Route as DocsBestPracticesRouteImport } from './routes/docs/best-practices'
import { Route as BlogSplatRouteImport } from './routes/blog/$'
const PrivacyRoute = PrivacyRouteImport.update({
id: '/privacy',
@@ -61,6 +64,11 @@ const ChangelogRoute = ChangelogRouteImport.update({
path: '/changelog',
getParentRoute: () => rootRouteImport,
} as any)
const BlogRoute = BlogRouteImport.update({
id: '/blog',
path: '/blog',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
@@ -71,6 +79,11 @@ const DocsIndexRoute = DocsIndexRouteImport.update({
path: '/',
getParentRoute: () => DocsRoute,
} as any)
const BlogIndexRoute = BlogIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => BlogRoute,
} as any)
const DocsWorktreesRoute = DocsWorktreesRouteImport.update({
id: '/worktrees',
path: '/worktrees',
@@ -106,9 +119,15 @@ const DocsBestPracticesRoute = DocsBestPracticesRouteImport.update({
path: '/best-practices',
getParentRoute: () => DocsRoute,
} as any)
const BlogSplatRoute = BlogSplatRouteImport.update({
id: '/$',
path: '/$',
getParentRoute: () => BlogRoute,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/blog': typeof BlogRouteWithChildren
'/changelog': typeof ChangelogRoute
'/claude-code': typeof ClaudeCodeRoute
'/codex': typeof CodexRoute
@@ -116,6 +135,7 @@ export interface FileRoutesByFullPath {
'/download': typeof DownloadRoute
'/opencode': typeof OpencodeRoute
'/privacy': typeof PrivacyRoute
'/blog/$': typeof BlogSplatRoute
'/docs/best-practices': typeof DocsBestPracticesRoute
'/docs/cli': typeof DocsCliRoute
'/docs/configuration': typeof DocsConfigurationRoute
@@ -123,6 +143,7 @@ export interface FileRoutesByFullPath {
'/docs/updates': typeof DocsUpdatesRoute
'/docs/voice': typeof DocsVoiceRoute
'/docs/worktrees': typeof DocsWorktreesRoute
'/blog/': typeof BlogIndexRoute
'/docs/': typeof DocsIndexRoute
}
export interface FileRoutesByTo {
@@ -133,6 +154,7 @@ export interface FileRoutesByTo {
'/download': typeof DownloadRoute
'/opencode': typeof OpencodeRoute
'/privacy': typeof PrivacyRoute
'/blog/$': typeof BlogSplatRoute
'/docs/best-practices': typeof DocsBestPracticesRoute
'/docs/cli': typeof DocsCliRoute
'/docs/configuration': typeof DocsConfigurationRoute
@@ -140,11 +162,13 @@ export interface FileRoutesByTo {
'/docs/updates': typeof DocsUpdatesRoute
'/docs/voice': typeof DocsVoiceRoute
'/docs/worktrees': typeof DocsWorktreesRoute
'/blog': typeof BlogIndexRoute
'/docs': typeof DocsIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/blog': typeof BlogRouteWithChildren
'/changelog': typeof ChangelogRoute
'/claude-code': typeof ClaudeCodeRoute
'/codex': typeof CodexRoute
@@ -152,6 +176,7 @@ export interface FileRoutesById {
'/download': typeof DownloadRoute
'/opencode': typeof OpencodeRoute
'/privacy': typeof PrivacyRoute
'/blog/$': typeof BlogSplatRoute
'/docs/best-practices': typeof DocsBestPracticesRoute
'/docs/cli': typeof DocsCliRoute
'/docs/configuration': typeof DocsConfigurationRoute
@@ -159,12 +184,14 @@ export interface FileRoutesById {
'/docs/updates': typeof DocsUpdatesRoute
'/docs/voice': typeof DocsVoiceRoute
'/docs/worktrees': typeof DocsWorktreesRoute
'/blog/': typeof BlogIndexRoute
'/docs/': typeof DocsIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/blog'
| '/changelog'
| '/claude-code'
| '/codex'
@@ -172,6 +199,7 @@ export interface FileRouteTypes {
| '/download'
| '/opencode'
| '/privacy'
| '/blog/$'
| '/docs/best-practices'
| '/docs/cli'
| '/docs/configuration'
@@ -179,6 +207,7 @@ export interface FileRouteTypes {
| '/docs/updates'
| '/docs/voice'
| '/docs/worktrees'
| '/blog/'
| '/docs/'
fileRoutesByTo: FileRoutesByTo
to:
@@ -189,6 +218,7 @@ export interface FileRouteTypes {
| '/download'
| '/opencode'
| '/privacy'
| '/blog/$'
| '/docs/best-practices'
| '/docs/cli'
| '/docs/configuration'
@@ -196,10 +226,12 @@ export interface FileRouteTypes {
| '/docs/updates'
| '/docs/voice'
| '/docs/worktrees'
| '/blog'
| '/docs'
id:
| '__root__'
| '/'
| '/blog'
| '/changelog'
| '/claude-code'
| '/codex'
@@ -207,6 +239,7 @@ export interface FileRouteTypes {
| '/download'
| '/opencode'
| '/privacy'
| '/blog/$'
| '/docs/best-practices'
| '/docs/cli'
| '/docs/configuration'
@@ -214,11 +247,13 @@ export interface FileRouteTypes {
| '/docs/updates'
| '/docs/voice'
| '/docs/worktrees'
| '/blog/'
| '/docs/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
BlogRoute: typeof BlogRouteWithChildren
ChangelogRoute: typeof ChangelogRoute
ClaudeCodeRoute: typeof ClaudeCodeRoute
CodexRoute: typeof CodexRoute
@@ -279,6 +314,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ChangelogRouteImport
parentRoute: typeof rootRouteImport
}
'/blog': {
id: '/blog'
path: '/blog'
fullPath: '/blog'
preLoaderRoute: typeof BlogRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
@@ -293,6 +335,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DocsIndexRouteImport
parentRoute: typeof DocsRoute
}
'/blog/': {
id: '/blog/'
path: '/'
fullPath: '/blog/'
preLoaderRoute: typeof BlogIndexRouteImport
parentRoute: typeof BlogRoute
}
'/docs/worktrees': {
id: '/docs/worktrees'
path: '/worktrees'
@@ -342,9 +391,28 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DocsBestPracticesRouteImport
parentRoute: typeof DocsRoute
}
'/blog/$': {
id: '/blog/$'
path: '/$'
fullPath: '/blog/$'
preLoaderRoute: typeof BlogSplatRouteImport
parentRoute: typeof BlogRoute
}
}
}
interface BlogRouteChildren {
BlogSplatRoute: typeof BlogSplatRoute
BlogIndexRoute: typeof BlogIndexRoute
}
const BlogRouteChildren: BlogRouteChildren = {
BlogSplatRoute: BlogSplatRoute,
BlogIndexRoute: BlogIndexRoute,
}
const BlogRouteWithChildren = BlogRoute._addFileChildren(BlogRouteChildren)
interface DocsRouteChildren {
DocsBestPracticesRoute: typeof DocsBestPracticesRoute
DocsCliRoute: typeof DocsCliRoute
@@ -371,6 +439,7 @@ const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren)
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
BlogRoute: BlogRouteWithChildren,
ChangelogRoute: ChangelogRoute,
ClaudeCodeRoute: ClaudeCodeRoute,
CodexRoute: CodexRoute,
+19
View File
@@ -0,0 +1,19 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { SiteHeader } from "~/components/site-header";
export const Route = createFileRoute("/blog")({
component: BlogLayout,
});
function BlogLayout() {
return (
<div className="min-h-screen bg-background">
<div className="max-w-prose mx-auto p-6 md:p-12">
<div className="mb-8">
<SiteHeader />
</div>
<Outlet />
</div>
</div>
);
}
+40
View File
@@ -0,0 +1,40 @@
import { createFileRoute } from "@tanstack/react-router";
import ReactMarkdown from "react-markdown";
import { getPost, formatDate } from "~/posts";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/blog/$")({
head: ({ params }) => {
const slug = params._splat ?? "";
const post = getPost(slug);
if (!post) return { meta: pageMeta("Not Found - Paseo", "Post not found.") };
return {
meta: pageMeta(`${post.frontmatter.title} - Paseo`, post.frontmatter.description),
};
},
component: BlogPost,
});
function BlogPost() {
const { _splat } = Route.useParams();
const slug = _splat ?? "";
const post = getPost(slug);
if (!post) {
return <p className="text-muted-foreground">Post not found.</p>;
}
return (
<article>
<div className="mb-8">
<h1 className="text-4xl font-bold mb-2">{post.frontmatter.title}</h1>
<span className="text-lg text-muted-foreground">
{formatDate(new Date(post.frontmatter.date))}
</span>
</div>
<div className="blog-prose">
<ReactMarkdown>{post.content}</ReactMarkdown>
</div>
</article>
);
}
@@ -0,0 +1,53 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { getPosts, formatDate } from "~/posts";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/blog/")({
validateSearch: (search: Record<string, unknown>) => ({
drafts: search.drafts !== undefined,
}),
head: () => ({
meta: pageMeta("Blog - Paseo", "Updates, thoughts, and announcements from the Paseo team."),
}),
component: BlogIndex,
});
function BlogIndex() {
const { drafts } = Route.useSearch();
const posts = getPosts(drafts);
return (
<div>
{drafts && (
<div className="mb-6 p-4 bg-primary/10 rounded border-l-4 border-primary">
<p className="text-sm text-foreground/80">Showing draft posts</p>
</div>
)}
<div className="space-y-2">
{posts.map(({ slug, frontmatter }) => (
<div
key={slug}
className="flex flex-col-reverse items-start md:flex-row md:items-center gap-x-4"
>
<span className="text-lg text-muted-foreground tabular-nums">
{formatDate(new Date(frontmatter.date))}
</span>
<Link
to="/blog/$"
params={{ _splat: slug }}
className="text-lg text-foreground hover:text-primary transition-colors"
>
{frontmatter.title}
{frontmatter.draft && (
<span className="ml-2 text-xs px-2 py-1 bg-primary/20 text-primary rounded">
DRAFT
</span>
)}
</Link>
</div>
))}
{posts.length === 0 && <p className="text-muted-foreground">No posts yet.</p>}
</div>
</div>
);
}
+5 -52
View File
@@ -1,8 +1,8 @@
import { Link, createFileRoute } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import ReactMarkdown from "react-markdown";
import changelogMarkdown from "../../../../CHANGELOG.md?raw";
import { pageMeta } from "~/meta";
import "~/styles.css";
import { SiteHeader } from "~/components/site-header";
export const Route = createFileRoute("/changelog")({
head: () => ({
@@ -18,56 +18,9 @@ function Changelog() {
return (
<div className="min-h-screen bg-background">
<div className="max-w-4xl mx-auto p-6 md:p-12">
<header className="flex items-center justify-between gap-4 mb-8">
<Link to="/" className="flex items-center gap-3">
<img src="/logo.svg" alt="Paseo" className="w-6 h-6" />
<span className="text-lg font-medium">Paseo</span>
</Link>
<div className="flex items-center gap-4">
<Link
to="/docs"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Docs
</Link>
<a
href="https://discord.gg/jz8T2uahpH"
target="_blank"
rel="noopener noreferrer"
aria-label="Discord"
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
>
<svg
role="img"
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
</svg>
</a>
<a
href="https://github.com/getpaseo/paseo"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 0C5.37 0 0 5.484 0 12.252c0 5.418 3.438 10.013 8.205 11.637.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.738-4.042-1.61-4.042-1.61-.546-1.403-1.333-1.776-1.333-1.776-1.089-.756.084-.741.084-.741 1.205.087 1.838 1.262 1.838 1.262 1.07 1.87 2.809 1.33 3.495 1.017.108-.79.417-1.33.76-1.636-2.665-.31-5.467-1.35-5.467-6.005 0-1.327.465-2.413 1.235-3.262-.124-.31-.535-1.556.117-3.243 0 0 1.008-.33 3.3 1.248a11.2 11.2 0 0 1 3.003-.404c1.02.005 2.045.138 3.003.404 2.29-1.578 3.297-1.248 3.297-1.248.653 1.687.242 2.933.118 3.243.77.85 1.233 1.935 1.233 3.262 0 4.667-2.807 5.692-5.48 5.995.43.38.823 1.133.823 2.285 0 1.65-.015 2.98-.015 3.386 0 .315.218.694.825.576C20.565 22.26 24 17.667 24 12.252 24 5.484 18.627 0 12 0z" />
</svg>
</a>
</div>
</header>
<div className="mb-8">
<SiteHeader />
</div>
<article className="changelog-markdown rounded-xl border border-border bg-card/40 p-6 md:p-8">
<ReactMarkdown>{changelogMarkdown}</ReactMarkdown>
</article>
+106
View File
@@ -80,6 +80,112 @@
color: rgba(250, 250, 250, 0.8);
}
/* Blog post prose styles — adapted from moboudra.com */
.blog-prose {
color: rgba(250, 250, 250, 0.82);
line-height: 1.8;
font-size: 1.125rem;
}
.blog-prose h1,
.blog-prose h2,
.blog-prose h3 {
color: var(--color-foreground);
font-weight: 600;
text-decoration: underline;
text-decoration-color: color-mix(in srgb, var(--color-primary) 50%, transparent);
text-decoration-thickness: 2px;
text-underline-offset: 4px;
}
.blog-prose h1,
.blog-prose h2 {
font-size: 1.5rem;
line-height: 1.3;
margin-top: 2.5rem;
margin-bottom: 0.75rem;
}
.blog-prose h3 {
font-size: 1.25rem;
line-height: 1.4;
margin-top: 2rem;
margin-bottom: 0.5rem;
}
.blog-prose p {
margin-top: 1rem;
margin-bottom: 1rem;
}
.blog-prose a {
color: var(--color-foreground);
text-decoration: underline;
text-underline-offset: 2px;
}
.blog-prose a:hover {
color: var(--color-primary);
}
.blog-prose ul,
.blog-prose ol {
margin-top: 1rem;
margin-bottom: 1rem;
padding-left: 1.5rem;
}
.blog-prose ul {
list-style: disc;
}
.blog-prose ol {
list-style: decimal;
}
.blog-prose li {
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
.blog-prose blockquote {
border-left: 3px solid var(--color-border);
padding-left: 1rem;
color: rgba(250, 250, 250, 0.6);
margin-top: 1rem;
margin-bottom: 1rem;
}
.blog-prose pre {
background-color: var(--color-card);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1rem;
overflow-x: auto;
margin-top: 1rem;
margin-bottom: 1rem;
}
.blog-prose :not(pre) > code {
background-color: var(--color-muted);
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.875em;
}
.blog-prose img {
max-width: 100%;
border-radius: 0.5rem;
margin-top: 1rem;
margin-bottom: 1rem;
}
.blog-prose hr {
border-color: var(--color-border);
margin-top: 2rem;
margin-bottom: 2rem;
}
@theme {
--color-background: #0a0a0a;
--color-foreground: #fafafa;
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />