veslx 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/lib/build.ts CHANGED
@@ -4,6 +4,7 @@ import path from 'path'
4
4
  import fs from 'fs'
5
5
  import importConfig from "./import-config";
6
6
  import veslxPlugin from '../../plugin/src/plugin'
7
+ import { log } from './log'
7
8
 
8
9
  /**
9
10
  * Recursively copy a directory
@@ -67,8 +68,6 @@ async function getDefaultConfig(cwd: string) {
67
68
  export default async function buildApp(dir?: string) {
68
69
  const cwd = process.cwd()
69
70
 
70
- console.log(`Building veslx app in ${cwd}`);
71
-
72
71
  // Resolve content directory from CLI arg
73
72
  const contentDir = dir
74
73
  ? (path.isAbsolute(dir) ? dir : path.resolve(cwd, dir))
@@ -129,5 +128,5 @@ export default async function buildApp(dir?: string) {
129
128
  // Clean up temp build directory
130
129
  fs.rmSync(tempOutDir, { recursive: true })
131
130
 
132
- console.log(`\nBuild complete: ${finalOutDir}`)
131
+ log.success(`dist/`)
133
132
  }
package/bin/lib/log.ts ADDED
@@ -0,0 +1,18 @@
1
+ // Minimal CLI logger with subtle styling
2
+
3
+ const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
4
+ const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
5
+ const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
6
+ const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
7
+
8
+ export const log = {
9
+ info: (msg: string) => console.log(dim(` ${msg}`)),
10
+ success: (msg: string) => console.log(` ${green('✓')} ${msg}`),
11
+ error: (msg: string) => console.error(` ${red('✗')} ${msg}`),
12
+ url: (url: string) => console.log(` ${cyan(url)}`),
13
+ blank: () => console.log(),
14
+ };
15
+
16
+ export const banner = () => {
17
+ console.log(dim(` veslx`));
18
+ };
package/bin/lib/serve.ts CHANGED
@@ -3,6 +3,7 @@ import { execSync } from 'child_process'
3
3
  import importConfig from "./import-config";
4
4
  import veslxPlugin from '../../plugin/src/plugin'
5
5
  import path from 'path'
6
+ import { log } from './log'
6
7
 
7
8
  interface PackageJson {
8
9
  name?: string;
@@ -45,11 +46,9 @@ async function getDefaultConfig(cwd: string) {
45
46
  };
46
47
  }
47
48
 
48
- export default async function start(dir?: string) {
49
+ export default async function serve(dir?: string) {
49
50
  const cwd = process.cwd()
50
51
 
51
- console.log(`Starting veslx dev server in ${cwd}`);
52
-
53
52
  // Resolve content directory - CLI arg takes precedence
54
53
  const contentDir = dir
55
54
  ? (path.isAbsolute(dir) ? dir : path.resolve(cwd, dir))
@@ -88,6 +87,11 @@ export default async function start(dir?: string) {
88
87
  })
89
88
 
90
89
  await server.listen()
91
- server.printUrls()
92
- server.bindCLIShortcuts({ print: true })
90
+
91
+ const info = server.resolvedUrls
92
+ if (info?.local[0]) {
93
+ log.url(info.local[0])
94
+ }
95
+
96
+ server.bindCLIShortcuts({ print: false })
93
97
  }
package/bin/lib/start.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import importConfig from "./import-config"
2
2
  import pm2 from "pm2";
3
+ import { log } from './log'
3
4
 
4
5
  export default async function start() {
5
6
  const config = await importConfig(process.cwd());
6
7
 
7
8
  if (!config) {
8
- console.error("Configuration file 'veslx.config.ts' not found in the current directory.");
9
+ log.error("veslx.yaml not found");
9
10
  return
10
11
  }
11
12
 
@@ -14,7 +15,7 @@ export default async function start() {
14
15
 
15
16
  pm2.connect((err) => {
16
17
  if (err) {
17
- console.error('PM2 connect error:', err);
18
+ log.error('pm2 connection failed');
18
19
  return;
19
20
  }
20
21
 
@@ -26,15 +27,15 @@ export default async function start() {
26
27
  autorestart: true,
27
28
  watch: false,
28
29
  max_memory_restart: '200M'
29
- }, (err, apps) => {
30
- pm2.disconnect(); // Disconnects from PM2
30
+ }, (err) => {
31
+ pm2.disconnect();
31
32
 
32
33
  if (err) {
33
- console.error('Failed to start daemon:', err);
34
+ log.error('daemon failed to start');
34
35
  return;
35
36
  }
36
37
 
37
- console.log(`veslx daemon started in ${cwd}`);
38
+ log.success('daemon started');
38
39
  });
39
40
  })
40
41
  }
package/bin/lib/stop.ts CHANGED
@@ -1,24 +1,25 @@
1
1
  import pm2 from "pm2";
2
+ import { log } from './log'
2
3
 
3
- export default async function start() {
4
+ export default async function stop() {
4
5
  const cwd = process.cwd();
5
6
  const name = `veslx-${cwd.replace(/\//g, '-').replace(/^-/, '')}`.toLowerCase();
6
7
 
7
8
  pm2.connect((err) => {
8
9
  if (err) {
9
- console.error('PM2 connect error:', err);
10
+ log.error('pm2 connection failed');
10
11
  return;
11
12
  }
12
13
 
13
- pm2.stop(name, (err, apps) => {
14
- pm2.disconnect(); // Disconnects from PM2
14
+ pm2.stop(name, (err) => {
15
+ pm2.disconnect();
15
16
 
16
17
  if (err) {
17
- console.error('Failed to stop daemon:', err);
18
+ log.error('daemon failed to stop');
18
19
  return;
19
20
  }
20
21
 
21
- console.log(`veslx daemon stopped as "${name}" in ${cwd}`);
22
+ log.success('daemon stopped');
22
23
  });
23
24
  })
24
25
  }
package/bin/veslx.ts CHANGED
@@ -7,14 +7,11 @@ import serve from "./lib/serve";
7
7
  import start from "./lib/start";
8
8
  import stop from "./lib/stop";
9
9
  import build from "./lib/build";
10
+ import { banner } from "./lib/log";
10
11
 
11
12
  const cli = cac("veslx");
12
13
 
13
- console.log(`▗▖ ▗▖▗▄▄▄▖ ▗▄▄▖▗▖ ▗▖ ▗▖
14
- ▐▌ ▐▌▐▌ ▐▌ ▐▌ ▝▚▞▘
15
- ▐▌ ▐▌▐▛▀▀▘ ▝▀▚▖▐▌ ▐▌
16
- ▝▚▞▘ ▐▙▄▄▖▗▄▄▞▘▐▙▄▄▖▗▞▘▝▚▖
17
- `);
14
+ banner();
18
15
 
19
16
  cli
20
17
  .command("init", "Initialize a new veslx project")
@@ -0,0 +1,50 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { Link } from "react-router-dom";
3
+ import { cn } from "../lib/utils.js";
4
+ const views = [
5
+ { key: "posts", label: "posts", path: "/posts" },
6
+ { key: "docs", label: "docs", path: "/docs" },
7
+ { key: "all", label: "all", path: "/all" }
8
+ ];
9
+ function ContentTabs({ value, counts }) {
10
+ const hasOnlyPosts = counts.posts > 0 && counts.docs === 0;
11
+ const hasOnlyDocs = counts.docs > 0 && counts.posts === 0;
12
+ if (hasOnlyPosts || hasOnlyDocs) {
13
+ return null;
14
+ }
15
+ const isDisabled = (key) => {
16
+ if (key === "posts") return counts.posts === 0;
17
+ if (key === "docs") return counts.docs === 0;
18
+ return false;
19
+ };
20
+ return /* @__PURE__ */ jsx("nav", { className: "flex justify-end items-center gap-3 font-mono font-medium text-xs text-muted-foreground", children: views.map((view) => {
21
+ const disabled = isDisabled(view.key);
22
+ if (disabled) {
23
+ return /* @__PURE__ */ jsx(
24
+ "span",
25
+ {
26
+ className: "opacity-30 cursor-not-allowed",
27
+ children: view.label
28
+ },
29
+ view.key
30
+ );
31
+ }
32
+ return /* @__PURE__ */ jsx(
33
+ Link,
34
+ {
35
+ to: view.path,
36
+ className: cn(
37
+ "transition-colors duration-150",
38
+ "hover:text-foreground hover:underline hover:underline-offset-4 hover:decoration-primary/60",
39
+ value === view.key ? "text-foreground underline-offset-4 decoration-primary/60" : ""
40
+ ),
41
+ children: view.label
42
+ },
43
+ view.key
44
+ );
45
+ }) });
46
+ }
47
+ export {
48
+ ContentTabs
49
+ };
50
+ //# sourceMappingURL=content-tabs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-tabs.js","sources":["../../../src/components/content-tabs.tsx"],"sourcesContent":["import { Link } from \"react-router-dom\";\nimport { cn } from \"@/lib/utils\";\nimport type { ContentView } from \"@/lib/content-classification\";\n\ninterface ContentTabsProps {\n value: ContentView;\n counts: { posts: number; docs: number; all: number };\n}\n\nconst views: { key: ContentView; label: string; path: string }[] = [\n { key: \"posts\", label: \"posts\", path: \"/posts\" },\n { key: \"docs\", label: \"docs\", path: \"/docs\" },\n { key: \"all\", label: \"all\", path: \"/all\" },\n];\n\nexport function ContentTabs({ value, counts }: ContentTabsProps) {\n const hasOnlyPosts = counts.posts > 0 && counts.docs === 0;\n const hasOnlyDocs = counts.docs > 0 && counts.posts === 0;\n\n if (hasOnlyPosts || hasOnlyDocs) {\n return null;\n }\n\n const isDisabled = (key: ContentView) => {\n if (key === \"posts\") return counts.posts === 0;\n if (key === \"docs\") return counts.docs === 0;\n return false;\n };\n\n return (\n <nav className=\"flex justify-end items-center gap-3 font-mono font-medium text-xs text-muted-foreground\">\n {views.map((view) => {\n const disabled = isDisabled(view.key);\n\n if (disabled) {\n return (\n <span\n key={view.key}\n className=\"opacity-30 cursor-not-allowed\"\n >\n {view.label}\n </span>\n );\n }\n\n return (\n <Link\n key={view.key}\n to={view.path}\n className={cn(\n \"transition-colors duration-150\",\n \"hover:text-foreground hover:underline hover:underline-offset-4 hover:decoration-primary/60\",\n value === view.key\n ? \"text-foreground underline-offset-4 decoration-primary/60\"\n : \"\"\n )}\n >\n {view.label}\n </Link>\n );\n })}\n </nav>\n );\n}\n"],"names":[],"mappings":";;;AASA,MAAM,QAA6D;AAAA,EACjE,EAAE,KAAK,SAAS,OAAO,SAAS,MAAM,SAAA;AAAA,EACtC,EAAE,KAAK,QAAQ,OAAO,QAAQ,MAAM,QAAA;AAAA,EACpC,EAAE,KAAK,OAAO,OAAO,OAAO,MAAM,OAAA;AACpC;AAEO,SAAS,YAAY,EAAE,OAAO,UAA4B;AAC/D,QAAM,eAAe,OAAO,QAAQ,KAAK,OAAO,SAAS;AACzD,QAAM,cAAc,OAAO,OAAO,KAAK,OAAO,UAAU;AAExD,MAAI,gBAAgB,aAAa;AAC/B,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,CAAC,QAAqB;AACvC,QAAI,QAAQ,QAAS,QAAO,OAAO,UAAU;AAC7C,QAAI,QAAQ,OAAQ,QAAO,OAAO,SAAS;AAC3C,WAAO;AAAA,EACT;AAEA,6BACG,OAAA,EAAI,WAAU,2FACZ,UAAA,MAAM,IAAI,CAAC,SAAS;AACnB,UAAM,WAAW,WAAW,KAAK,GAAG;AAEpC,QAAI,UAAU;AACZ,aACE;AAAA,QAAC;AAAA,QAAA;AAAA,UAEC,WAAU;AAAA,UAET,UAAA,KAAK;AAAA,QAAA;AAAA,QAHD,KAAK;AAAA,MAAA;AAAA,IAMhB;AAEA,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QAEC,IAAI,KAAK;AAAA,QACT,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA,UAAU,KAAK,MACX,6DACA;AAAA,QAAA;AAAA,QAGL,UAAA,KAAK;AAAA,MAAA;AAAA,MAVD,KAAK;AAAA,IAAA;AAAA,EAahB,CAAC,EAAA,CACH;AAEJ;"}
@@ -1,9 +1,9 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { Link } from "react-router-dom";
3
3
  import { cn } from "../lib/utils.js";
4
- import { findMdxFiles, findReadme, findSlides } from "../plugin/src/client.js";
5
4
  import { formatDate } from "../lib/format-date.js";
6
5
  import { ArrowRight } from "lucide-react";
6
+ import { directoryToPostEntries, filterVisiblePosts, filterByView, getFrontmatter } from "../lib/content-classification.js";
7
7
  function extractOrder(name) {
8
8
  const match = name.match(/^(\d+)-/);
9
9
  return match ? parseInt(match[1], 10) : null;
@@ -11,42 +11,16 @@ function extractOrder(name) {
11
11
  function stripNumericPrefix(name) {
12
12
  return name.replace(/^\d+-/, "").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
13
13
  }
14
- function PostList({ directory }) {
15
- const folders = directory.children.filter((c) => c.type === "directory");
16
- const standaloneFiles = findMdxFiles(directory);
17
- const folderPosts = folders.map((folder) => {
18
- const readme = findReadme(folder);
19
- const slides = findSlides(folder);
20
- return {
21
- type: "folder",
22
- name: folder.name,
23
- path: folder.path,
24
- readme,
25
- slides,
26
- file: null
27
- };
28
- });
29
- const filePosts = standaloneFiles.map((file) => ({
30
- type: "file",
31
- name: file.name.replace(/\.mdx?$/, ""),
32
- path: file.path,
33
- readme: null,
34
- slides: null,
35
- file
36
- }));
37
- let posts = [...folderPosts, ...filePosts];
14
+ function PostList({ directory, view = "all" }) {
15
+ let posts = directoryToPostEntries(directory);
16
+ if (posts.length === 0) {
17
+ return /* @__PURE__ */ jsx("div", { className: "py-24 text-center", children: /* @__PURE__ */ jsx("p", { className: "text-muted-foreground font-mono text-sm tracking-wide", children: "no entries" }) });
18
+ }
19
+ posts = filterVisiblePosts(posts);
20
+ posts = filterByView(posts, view);
38
21
  if (posts.length === 0) {
39
22
  return /* @__PURE__ */ jsx("div", { className: "py-24 text-center", children: /* @__PURE__ */ jsx("p", { className: "text-muted-foreground font-mono text-sm tracking-wide", children: "no entries" }) });
40
23
  }
41
- posts = posts.filter((post) => {
42
- var _a, _b;
43
- const frontmatter = ((_a = post.readme) == null ? void 0 : _a.frontmatter) || ((_b = post.file) == null ? void 0 : _b.frontmatter);
44
- return (frontmatter == null ? void 0 : frontmatter.visibility) !== "hidden" && (frontmatter == null ? void 0 : frontmatter.draft) !== true;
45
- });
46
- const getFrontmatter = (post) => {
47
- var _a, _b, _c;
48
- return ((_a = post.readme) == null ? void 0 : _a.frontmatter) || ((_b = post.file) == null ? void 0 : _b.frontmatter) || ((_c = post.slides) == null ? void 0 : _c.frontmatter);
49
- };
50
24
  const getPostDate = (post) => {
51
25
  const frontmatter = getFrontmatter(post);
52
26
  return (frontmatter == null ? void 0 : frontmatter.date) ? new Date(frontmatter.date) : null;
@@ -1 +1 @@
1
- {"version":3,"file":"post-list.js","sources":["../../../src/components/post-list.tsx"],"sourcesContent":["import { Link } from \"react-router-dom\";\nimport { cn } from \"@/lib/utils\";\nimport { DirectoryEntry, FileEntry } from \"../../plugin/src/lib\";\nimport { findReadme, findSlides, findMdxFiles } from \"../../plugin/src/client\";\nimport { formatDate } from \"@/lib/format-date\";\nimport { ArrowRight } from \"lucide-react\";\n\ntype PostEntry = {\n type: 'folder' | 'file';\n name: string;\n path: string;\n readme: FileEntry | null;\n slides: FileEntry | null;\n file: FileEntry | null; // For standalone MDX files\n};\n\n// Helper to extract numeric prefix from filename (e.g., \"01-intro\" → 1)\nfunction extractOrder(name: string): number | null {\n const match = name.match(/^(\\d+)-/);\n return match ? parseInt(match[1], 10) : null;\n}\n\n// Helper to strip numeric prefix for display (e.g., \"01-getting-started\" → \"Getting Started\")\nfunction stripNumericPrefix(name: string): string {\n return name\n .replace(/^\\d+-/, '')\n .replace(/-/g, ' ')\n .replace(/\\b\\w/g, c => c.toUpperCase());\n}\n\nexport default function PostList({ directory }: { directory: DirectoryEntry }) {\n const folders = directory.children.filter((c): c is DirectoryEntry => c.type === \"directory\");\n const standaloneFiles = findMdxFiles(directory);\n\n // Convert folders to post entries\n const folderPosts: PostEntry[] = folders.map((folder) => {\n const readme = findReadme(folder);\n const slides = findSlides(folder);\n return {\n type: 'folder' as const,\n name: folder.name,\n path: folder.path,\n readme,\n slides,\n file: null,\n };\n });\n\n // Convert standalone MDX files to post entries\n const filePosts: PostEntry[] = standaloneFiles.map((file) => ({\n type: 'file' as const,\n name: file.name.replace(/\\.mdx?$/, ''),\n path: file.path,\n readme: null,\n slides: null,\n file,\n }));\n\n let posts: PostEntry[] = [...folderPosts, ...filePosts];\n\n if (posts.length === 0) {\n return (\n <div className=\"py-24 text-center\">\n <p className=\"text-muted-foreground font-mono text-sm tracking-wide\">no entries</p>\n </div>\n );\n }\n\n // Filter out hidden and draft posts\n posts = posts.filter((post) => {\n const frontmatter = post.readme?.frontmatter || post.file?.frontmatter;\n return frontmatter?.visibility !== \"hidden\" && frontmatter?.draft !== true;\n });\n\n // Helper to get frontmatter from post\n const getFrontmatter = (post: PostEntry) => {\n return post.readme?.frontmatter || post.file?.frontmatter || post.slides?.frontmatter;\n };\n\n // Helper to get date from post\n const getPostDate = (post: PostEntry): Date | null => {\n const frontmatter = getFrontmatter(post);\n return frontmatter?.date ? new Date(frontmatter.date as string) : null;\n };\n\n // Smart sorting: numeric prefix → date → alphabetical\n posts = posts.sort((a, b) => {\n const aOrder = extractOrder(a.name);\n const bOrder = extractOrder(b.name);\n const aDate = getPostDate(a);\n const bDate = getPostDate(b);\n\n // Both have numeric prefix → sort by number\n if (aOrder !== null && bOrder !== null) {\n return aOrder - bOrder;\n }\n // One has prefix, one doesn't → prefixed comes first\n if (aOrder !== null) return -1;\n if (bOrder !== null) return 1;\n\n // Both have dates → sort by date (newest first)\n if (aDate && bDate) {\n return bDate.getTime() - aDate.getTime();\n }\n // One has date → dated comes first\n if (aDate) return -1;\n if (bDate) return 1;\n\n // Neither → alphabetical by title\n const aTitle = (getFrontmatter(a)?.title as string) || a.name;\n const bTitle = (getFrontmatter(b)?.title as string) || b.name;\n return aTitle.localeCompare(bTitle);\n });\n\n return (\n <div className=\"space-y-1\">\n {posts.map((post) => {\n const frontmatter = getFrontmatter(post);\n\n // Title: explicit frontmatter > stripped numeric prefix > raw name\n const title = (frontmatter?.title as string) || stripNumericPrefix(post.name);\n const description = frontmatter?.description as string | undefined;\n const date = frontmatter?.date ? new Date(frontmatter.date as string) : null;\n\n // Determine the link path\n let linkPath: string;\n if (post.file) {\n // Standalone MDX file\n linkPath = `/${post.file.path}`;\n } else if (post.slides && !post.readme) {\n // Folder with only slides\n linkPath = `/${post.slides.path}`;\n } else if (post.readme) {\n // Folder with readme\n linkPath = `/${post.readme.path}`;\n } else {\n // Fallback to folder path\n linkPath = `/${post.path}`;\n }\n\n return (\n <Link\n key={post.path}\n to={linkPath}\n className={cn(\n \"group block py-3 px-3 -mx-3 rounded-md\",\n \"transition-colors duration-150\",\n )}\n >\n <article className=\"flex items-start gap-4\">\n {/* Date - left side, fixed width */}\n <time\n dateTime={date?.toISOString()}\n className=\"font-mono text-xs text-muted-foreground tabular-nums w-20 flex-shrink-0 pt-0.5\"\n >\n {date ? formatDate(date) : <span className=\"text-muted-foreground/30\">—</span>}\n </time>\n\n {/* Main content */}\n <div className=\"flex-1 min-w-0\">\n <h3 className={cn(\n \"text-sm font-medium text-foreground\",\n \"group-hover:underline\",\n \"flex items-center gap-2\"\n )}>\n <span>{title}</span>\n <ArrowRight className=\"h-3 w-3 opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200 text-primary\" />\n </h3>\n\n {description && (\n <p className=\"text-sm text-muted-foreground line-clamp-1 mt-0.5\">\n {description}\n </p>\n )}\n </div>\n </article>\n </Link>\n );\n })}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"post-list.js","sources":["../../../src/components/post-list.tsx"],"sourcesContent":["import { Link } from \"react-router-dom\";\nimport { cn } from \"@/lib/utils\";\nimport type { DirectoryEntry } from \"../../plugin/src/lib\";\nimport { formatDate } from \"@/lib/format-date\";\nimport { ArrowRight } from \"lucide-react\";\nimport {\n type ContentView,\n type PostEntry,\n directoryToPostEntries,\n filterVisiblePosts,\n filterByView,\n getFrontmatter,\n} from \"@/lib/content-classification\";\n\n// Helper to extract numeric prefix from filename (e.g., \"01-intro\" → 1)\nfunction extractOrder(name: string): number | null {\n const match = name.match(/^(\\d+)-/);\n return match ? parseInt(match[1], 10) : null;\n}\n\n// Helper to strip numeric prefix for display (e.g., \"01-getting-started\" → \"Getting Started\")\nfunction stripNumericPrefix(name: string): string {\n return name\n .replace(/^\\d+-/, '')\n .replace(/-/g, ' ')\n .replace(/\\b\\w/g, c => c.toUpperCase());\n}\n\ninterface PostListProps {\n directory: DirectoryEntry;\n view?: ContentView;\n}\n\nexport default function PostList({ directory, view = 'all' }: PostListProps) {\n let posts = directoryToPostEntries(directory);\n\n if (posts.length === 0) {\n return (\n <div className=\"py-24 text-center\">\n <p className=\"text-muted-foreground font-mono text-sm tracking-wide\">no entries</p>\n </div>\n );\n }\n\n // Filter out hidden and draft posts\n posts = filterVisiblePosts(posts);\n\n // Apply view filter\n posts = filterByView(posts, view);\n\n if (posts.length === 0) {\n return (\n <div className=\"py-24 text-center\">\n <p className=\"text-muted-foreground font-mono text-sm tracking-wide\">no entries</p>\n </div>\n );\n }\n\n // Helper to get date from post\n const getPostDate = (post: PostEntry): Date | null => {\n const frontmatter = getFrontmatter(post);\n return frontmatter?.date ? new Date(frontmatter.date as string) : null;\n };\n\n // Smart sorting: numeric prefix → date → alphabetical\n posts = posts.sort((a, b) => {\n const aOrder = extractOrder(a.name);\n const bOrder = extractOrder(b.name);\n const aDate = getPostDate(a);\n const bDate = getPostDate(b);\n\n // Both have numeric prefix → sort by number\n if (aOrder !== null && bOrder !== null) {\n return aOrder - bOrder;\n }\n // One has prefix, one doesn't → prefixed comes first\n if (aOrder !== null) return -1;\n if (bOrder !== null) return 1;\n\n // Both have dates → sort by date (newest first)\n if (aDate && bDate) {\n return bDate.getTime() - aDate.getTime();\n }\n // One has date → dated comes first\n if (aDate) return -1;\n if (bDate) return 1;\n\n // Neither → alphabetical by title\n const aTitle = (getFrontmatter(a)?.title as string) || a.name;\n const bTitle = (getFrontmatter(b)?.title as string) || b.name;\n return aTitle.localeCompare(bTitle);\n });\n\n return (\n <div className=\"space-y-1\">\n {posts.map((post) => {\n const frontmatter = getFrontmatter(post);\n\n // Title: explicit frontmatter > stripped numeric prefix > raw name\n const title = (frontmatter?.title as string) || stripNumericPrefix(post.name);\n const description = frontmatter?.description as string | undefined;\n const date = frontmatter?.date ? new Date(frontmatter.date as string) : null;\n\n // Determine the link path\n let linkPath: string;\n if (post.file) {\n // Standalone MDX file\n linkPath = `/${post.file.path}`;\n } else if (post.slides && !post.readme) {\n // Folder with only slides\n linkPath = `/${post.slides.path}`;\n } else if (post.readme) {\n // Folder with readme\n linkPath = `/${post.readme.path}`;\n } else {\n // Fallback to folder path\n linkPath = `/${post.path}`;\n }\n\n return (\n <Link\n key={post.path}\n to={linkPath}\n className={cn(\n \"group block py-3 px-3 -mx-3 rounded-md\",\n \"transition-colors duration-150\",\n )}\n >\n <article className=\"flex items-start gap-4\">\n {/* Date - left side, fixed width */}\n <time\n dateTime={date?.toISOString()}\n className=\"font-mono text-xs text-muted-foreground tabular-nums w-20 flex-shrink-0 pt-0.5\"\n >\n {date ? formatDate(date) : <span className=\"text-muted-foreground/30\">—</span>}\n </time>\n\n {/* Main content */}\n <div className=\"flex-1 min-w-0\">\n <h3 className={cn(\n \"text-sm font-medium text-foreground\",\n \"group-hover:underline\",\n \"flex items-center gap-2\"\n )}>\n <span>{title}</span>\n <ArrowRight className=\"h-3 w-3 opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200 text-primary\" />\n </h3>\n\n {description && (\n <p className=\"text-sm text-muted-foreground line-clamp-1 mt-0.5\">\n {description}\n </p>\n )}\n </div>\n </article>\n </Link>\n );\n })}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ import { findMdxFiles, findSlides, findReadme } from "../plugin/src/client.js";
2
+ function getFrontmatter(post) {
3
+ var _a, _b, _c;
4
+ return ((_a = post.readme) == null ? void 0 : _a.frontmatter) || ((_b = post.file) == null ? void 0 : _b.frontmatter) || ((_c = post.slides) == null ? void 0 : _c.frontmatter);
5
+ }
6
+ function hasDate(post) {
7
+ const frontmatter = getFrontmatter(post);
8
+ return (frontmatter == null ? void 0 : frontmatter.date) !== void 0 && frontmatter.date !== null && frontmatter.date !== "";
9
+ }
10
+ function filterByView(posts, view) {
11
+ if (view === "all") return posts;
12
+ if (view === "posts") return posts.filter(hasDate);
13
+ if (view === "docs") return posts.filter((post) => !hasDate(post));
14
+ return posts;
15
+ }
16
+ function getViewCounts(posts) {
17
+ const dated = posts.filter(hasDate).length;
18
+ return {
19
+ posts: dated,
20
+ docs: posts.length - dated,
21
+ all: posts.length
22
+ };
23
+ }
24
+ function directoryToPostEntries(directory) {
25
+ const folders = directory.children.filter((c) => c.type === "directory");
26
+ const standaloneFiles = findMdxFiles(directory);
27
+ const folderPosts = folders.map((folder) => ({
28
+ type: "folder",
29
+ name: folder.name,
30
+ path: folder.path,
31
+ readme: findReadme(folder),
32
+ slides: findSlides(folder),
33
+ file: null
34
+ })).filter((post) => post.readme || post.slides);
35
+ const filePosts = standaloneFiles.map((file) => ({
36
+ type: "file",
37
+ name: file.name.replace(/\.mdx?$/, ""),
38
+ path: file.path,
39
+ readme: null,
40
+ slides: null,
41
+ file
42
+ }));
43
+ return [...folderPosts, ...filePosts];
44
+ }
45
+ function filterVisiblePosts(posts) {
46
+ return posts.filter((post) => {
47
+ const frontmatter = getFrontmatter(post);
48
+ return (frontmatter == null ? void 0 : frontmatter.visibility) !== "hidden" && (frontmatter == null ? void 0 : frontmatter.draft) !== true;
49
+ });
50
+ }
51
+ export {
52
+ directoryToPostEntries,
53
+ filterByView,
54
+ filterVisiblePosts,
55
+ getFrontmatter,
56
+ getViewCounts,
57
+ hasDate
58
+ };
59
+ //# sourceMappingURL=content-classification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-classification.js","sources":["../../../src/lib/content-classification.ts"],"sourcesContent":["import type { ContentView } from \"../../plugin/src/types\";\nimport type { DirectoryEntry, FileEntry } from \"../../plugin/src/lib\";\nimport { findReadme, findSlides, findMdxFiles } from \"../../plugin/src/client\";\n\nexport type PostEntry = {\n type: 'folder' | 'file';\n name: string;\n path: string;\n readme: FileEntry | null;\n slides: FileEntry | null;\n file: FileEntry | null;\n};\n\nexport function getFrontmatter(post: PostEntry) {\n return post.readme?.frontmatter || post.file?.frontmatter || post.slides?.frontmatter;\n}\n\nexport function hasDate(post: PostEntry): boolean {\n const frontmatter = getFrontmatter(post);\n return frontmatter?.date !== undefined && frontmatter.date !== null && frontmatter.date !== '';\n}\n\nexport function filterByView(posts: PostEntry[], view: ContentView): PostEntry[] {\n if (view === 'all') return posts;\n if (view === 'posts') return posts.filter(hasDate);\n if (view === 'docs') return posts.filter(post => !hasDate(post));\n return posts;\n}\n\nexport function getViewCounts(posts: PostEntry[]): { posts: number; docs: number; all: number } {\n const dated = posts.filter(hasDate).length;\n return {\n posts: dated,\n docs: posts.length - dated,\n all: posts.length,\n };\n}\n\nexport function directoryToPostEntries(directory: DirectoryEntry): PostEntry[] {\n const folders = directory.children.filter((c): c is DirectoryEntry => c.type === \"directory\");\n const standaloneFiles = findMdxFiles(directory);\n\n const folderPosts: PostEntry[] = folders\n .map((folder) => ({\n type: 'folder' as const,\n name: folder.name,\n path: folder.path,\n readme: findReadme(folder),\n slides: findSlides(folder),\n file: null,\n }))\n .filter((post) => post.readme || post.slides); // Only include folders with content\n\n const filePosts: PostEntry[] = standaloneFiles.map((file) => ({\n type: 'file' as const,\n name: file.name.replace(/\\.mdx?$/, ''),\n path: file.path,\n readme: null,\n slides: null,\n file,\n }));\n\n return [...folderPosts, ...filePosts];\n}\n\nexport function filterVisiblePosts(posts: PostEntry[]): PostEntry[] {\n return posts.filter((post) => {\n const frontmatter = getFrontmatter(post);\n return frontmatter?.visibility !== \"hidden\" && frontmatter?.draft !== true;\n });\n}\n\nexport type { ContentView };\n"],"names":[],"mappings":";AAaO,SAAS,eAAe,MAAiB;;AAC9C,WAAO,UAAK,WAAL,mBAAa,kBAAe,UAAK,SAAL,mBAAW,kBAAe,UAAK,WAAL,mBAAa;AAC5E;AAEO,SAAS,QAAQ,MAA0B;AAChD,QAAM,cAAc,eAAe,IAAI;AACvC,UAAO,2CAAa,UAAS,UAAa,YAAY,SAAS,QAAQ,YAAY,SAAS;AAC9F;AAEO,SAAS,aAAa,OAAoB,MAAgC;AAC/E,MAAI,SAAS,MAAO,QAAO;AAC3B,MAAI,SAAS,QAAS,QAAO,MAAM,OAAO,OAAO;AACjD,MAAI,SAAS,OAAQ,QAAO,MAAM,OAAO,CAAA,SAAQ,CAAC,QAAQ,IAAI,CAAC;AAC/D,SAAO;AACT;AAEO,SAAS,cAAc,OAAkE;AAC9F,QAAM,QAAQ,MAAM,OAAO,OAAO,EAAE;AACpC,SAAO;AAAA,IACL,OAAO;AAAA,IACP,MAAM,MAAM,SAAS;AAAA,IACrB,KAAK,MAAM;AAAA,EAAA;AAEf;AAEO,SAAS,uBAAuB,WAAwC;AAC7E,QAAM,UAAU,UAAU,SAAS,OAAO,CAAC,MAA2B,EAAE,SAAS,WAAW;AAC5F,QAAM,kBAAkB,aAAa,SAAS;AAE9C,QAAM,cAA2B,QAC9B,IAAI,CAAC,YAAY;AAAA,IAChB,MAAM;AAAA,IACN,MAAM,OAAO;AAAA,IACb,MAAM,OAAO;AAAA,IACb,QAAQ,WAAW,MAAM;AAAA,IACzB,QAAQ,WAAW,MAAM;AAAA,IACzB,MAAM;AAAA,EAAA,EACN,EACD,OAAO,CAAC,SAAS,KAAK,UAAU,KAAK,MAAM;AAE9C,QAAM,YAAyB,gBAAgB,IAAI,CAAC,UAAU;AAAA,IAC5D,MAAM;AAAA,IACN,MAAM,KAAK,KAAK,QAAQ,WAAW,EAAE;AAAA,IACrC,MAAM,KAAK;AAAA,IACX,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR;AAAA,EAAA,EACA;AAEF,SAAO,CAAC,GAAG,aAAa,GAAG,SAAS;AACtC;AAEO,SAAS,mBAAmB,OAAiC;AAClE,SAAO,MAAM,OAAO,CAAC,SAAS;AAC5B,UAAM,cAAc,eAAe,IAAI;AACvC,YAAO,2CAAa,gBAAe,aAAY,2CAAa,WAAU;AAAA,EACxE,CAAC;AACH;"}
@@ -5,6 +5,15 @@ import { Post } from "./post.js";
5
5
  import { SlidesPage } from "./slides.js";
6
6
  function ContentRouter() {
7
7
  const { "*": path = "" } = useParams();
8
+ if (path === "posts") {
9
+ return /* @__PURE__ */ jsx(Home, { view: "posts" });
10
+ }
11
+ if (path === "docs") {
12
+ return /* @__PURE__ */ jsx(Home, { view: "docs" });
13
+ }
14
+ if (path === "all") {
15
+ return /* @__PURE__ */ jsx(Home, { view: "all" });
16
+ }
8
17
  if (path.endsWith(".slides.mdx") || path.endsWith("SLIDES.mdx")) {
9
18
  return /* @__PURE__ */ jsx(SlidesPage, {});
10
19
  }
@@ -1 +1 @@
1
- {"version":3,"file":"content-router.js","sources":["../../../src/pages/content-router.tsx"],"sourcesContent":["import { useParams } from \"react-router-dom\"\nimport { Home } from \"./home\"\nimport { Post } from \"./post\"\nimport { SlidesPage } from \"./slides\"\n\n/**\n * Routes to the appropriate page based on the URL path:\n * - *.slides.mdx or *SLIDES.mdx → SlidesPage\n * - *.mdx → Post\n * - everything else → Home (directory listing)\n */\nexport function ContentRouter() {\n const { \"*\": path = \"\" } = useParams()\n\n // Check if this is a slides file\n if (path.endsWith('.slides.mdx') || path.endsWith('SLIDES.mdx')) {\n return <SlidesPage />\n }\n\n // Check if this is any MDX file\n if (path.endsWith('.mdx') || path.endsWith('.md')) {\n return <Post />\n }\n\n // Otherwise show directory listing\n return <Home />\n}\n"],"names":[],"mappings":";;;;;AAWO,SAAS,gBAAgB;AAC9B,QAAM,EAAE,KAAK,OAAO,GAAA,IAAO,UAAA;AAG3B,MAAI,KAAK,SAAS,aAAa,KAAK,KAAK,SAAS,YAAY,GAAG;AAC/D,+BAAQ,YAAA,EAAW;AAAA,EACrB;AAGA,MAAI,KAAK,SAAS,MAAM,KAAK,KAAK,SAAS,KAAK,GAAG;AACjD,+BAAQ,MAAA,EAAK;AAAA,EACf;AAGA,6BAAQ,MAAA,EAAK;AACf;"}
1
+ {"version":3,"file":"content-router.js","sources":["../../../src/pages/content-router.tsx"],"sourcesContent":["import { useParams } from \"react-router-dom\"\nimport { Home } from \"./home\"\nimport { Post } from \"./post\"\nimport { SlidesPage } from \"./slides\"\n\n/**\n * Routes to the appropriate page based on the URL path:\n * - /posts → Home with posts view\n * - /docs → Home with docs view\n * - *.slides.mdx or *SLIDES.mdx → SlidesPage\n * - *.mdx → Post\n * - everything else → Home (directory listing)\n */\nexport function ContentRouter() {\n const { \"*\": path = \"\" } = useParams()\n\n // Check for content view routes\n if (path === 'posts') {\n return <Home view=\"posts\" />\n }\n if (path === 'docs') {\n return <Home view=\"docs\" />\n }\n if (path === 'all') {\n return <Home view=\"all\" />\n }\n\n // Check if this is a slides file\n if (path.endsWith('.slides.mdx') || path.endsWith('SLIDES.mdx')) {\n return <SlidesPage />\n }\n\n // Check if this is any MDX file\n if (path.endsWith('.mdx') || path.endsWith('.md')) {\n return <Post />\n }\n\n // Otherwise show directory listing\n return <Home />\n}\n"],"names":[],"mappings":";;;;;AAaO,SAAS,gBAAgB;AAC9B,QAAM,EAAE,KAAK,OAAO,GAAA,IAAO,UAAA;AAG3B,MAAI,SAAS,SAAS;AACpB,WAAO,oBAAC,MAAA,EAAK,MAAK,QAAA,CAAQ;AAAA,EAC5B;AACA,MAAI,SAAS,QAAQ;AACnB,WAAO,oBAAC,MAAA,EAAK,MAAK,OAAA,CAAO;AAAA,EAC3B;AACA,MAAI,SAAS,OAAO;AAClB,WAAO,oBAAC,MAAA,EAAK,MAAK,MAAA,CAAM;AAAA,EAC1B;AAGA,MAAI,KAAK,SAAS,aAAa,KAAK,KAAK,SAAS,YAAY,GAAG;AAC/D,+BAAQ,YAAA,EAAW;AAAA,EACrB;AAGA,MAAI,KAAK,SAAS,MAAM,KAAK,KAAK,SAAS,KAAK,GAAG;AACjD,+BAAQ,MAAA,EAAK;AAAA,EACf;AAGA,6BAAQ,MAAA,EAAK;AACf;"}
@@ -5,11 +5,18 @@ import PostList from "../components/post-list.js";
5
5
  import { ErrorDisplay } from "../components/page-error.js";
6
6
  import { RunningBar } from "../components/running-bar.js";
7
7
  import { Header } from "../components/header.js";
8
+ import { ContentTabs } from "../components/content-tabs.js";
9
+ import { getViewCounts, filterVisiblePosts, directoryToPostEntries } from "../lib/content-classification.js";
8
10
  import siteConfig from "virtual:veslx-config";
9
- function Home() {
11
+ function Home({ view }) {
10
12
  const { "*": path = "." } = useParams();
11
- const { directory, error } = useDirectory(path);
12
13
  const config = siteConfig;
14
+ const isViewRoute = path === "posts" || path === "docs" || path === "all";
15
+ const directoryPath = isViewRoute ? "." : path;
16
+ const { directory, error } = useDirectory(directoryPath);
17
+ const activeView = view ?? config.defaultView;
18
+ const isRoot = path === "." || path === "" || isViewRoute;
19
+ const counts = directory ? getViewCounts(filterVisiblePosts(directoryToPostEntries(directory))) : { posts: 0, docs: 0, all: 0 };
13
20
  if (error) {
14
21
  return /* @__PURE__ */ jsx(ErrorDisplay, { error, path });
15
22
  }
@@ -17,13 +24,16 @@ function Home() {
17
24
  /* @__PURE__ */ jsx(RunningBar, {}),
18
25
  /* @__PURE__ */ jsx(Header, {}),
19
26
  /* @__PURE__ */ jsxs("main", { className: "flex-1 mx-auto w-full max-w-[var(--content-width)] px-[var(--page-padding)]", children: [
20
- /* @__PURE__ */ jsx("title", { children: path === "." || path === "" ? config.name : `${config.name} - ${path}` }),
27
+ /* @__PURE__ */ jsx("title", { children: isRoot ? config.name : `${config.name} - ${path}` }),
21
28
  /* @__PURE__ */ jsxs("main", { className: "flex flex-col gap-8 mb-32 mt-32", children: [
22
- (path === "." || path === "") && /* @__PURE__ */ jsxs("div", { className: "animate-fade-in", children: [
29
+ isRoot && /* @__PURE__ */ jsxs("div", { className: "animate-fade-in", children: [
23
30
  /* @__PURE__ */ jsx("h1", { className: "text-2xl md:text-3xl font-semibold tracking-tight text-foreground", children: config.name }),
24
31
  config.description && /* @__PURE__ */ jsx("p", { className: "mt-2 text-muted-foreground", children: config.description })
25
32
  ] }),
26
- directory && /* @__PURE__ */ jsx("div", { className: "animate-fade-in", children: /* @__PURE__ */ jsx(PostList, { directory }) })
33
+ /* @__PURE__ */ jsxs("div", { className: "", children: [
34
+ isRoot && directory && /* @__PURE__ */ jsx("div", { className: "animate-fade-in", children: /* @__PURE__ */ jsx(ContentTabs, { value: activeView, counts }) }),
35
+ directory && /* @__PURE__ */ jsx("div", { className: "animate-fade-in", children: /* @__PURE__ */ jsx(PostList, { directory, view: isRoot ? activeView : "all" }) })
36
+ ] })
27
37
  ] })
28
38
  ] })
29
39
  ] });
@@ -1 +1 @@
1
- {"version":3,"file":"home.js","sources":["../../../src/pages/home.tsx"],"sourcesContent":["import { useParams } from \"react-router-dom\"\nimport { useDirectory } from \"../../plugin/src/client\";\nimport Loading from \"@/components/loading\";\nimport PostList from \"@/components/post-list\";\nimport { ErrorDisplay } from \"@/components/page-error\";\nimport { RunningBar } from \"@/components/running-bar\";\nimport { Header } from \"@/components/header\";\nimport siteConfig from \"virtual:veslx-config\";\n\nexport function Home() {\n const { \"*\": path = \".\" } = useParams();\n const { directory, loading, error } = useDirectory(path)\n const config = siteConfig;\n\n if (error) {\n return <ErrorDisplay error={error} path={path} />;\n }\n\n if (loading) {\n return (\n <Loading />\n )\n }\n\n return (\n <div className=\"flex min-h-screen flex-col bg-background noise-overlay\">\n <RunningBar />\n <Header />\n <main className=\"flex-1 mx-auto w-full max-w-[var(--content-width)] px-[var(--page-padding)]\">\n <title>{(path === \".\" || path === \"\") ? config.name : `${config.name} - ${path}`}</title>\n <main className=\"flex flex-col gap-8 mb-32 mt-32\">\n {(path === \".\" || path === \"\") && (\n <div className=\"animate-fade-in\">\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-foreground\">\n {config.name}\n </h1>\n {config.description && (\n <p className=\"mt-2 text-muted-foreground\">\n {config.description}\n </p>\n )}\n </div>\n )}\n {directory && (\n <div className=\"animate-fade-in\">\n <PostList directory={directory}/>\n </div>\n )}\n </main>\n </main>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;;AASO,SAAS,OAAO;AACrB,QAAM,EAAE,KAAK,OAAO,IAAA,IAAQ,UAAA;AAC5B,QAAM,EAAE,WAAoB,UAAU,aAAa,IAAI;AACvD,QAAM,SAAS;AAEf,MAAI,OAAO;AACT,WAAO,oBAAC,cAAA,EAAa,OAAc,KAAA,CAAY;AAAA,EACjD;AAQA,SACE,qBAAC,OAAA,EAAI,WAAU,0DACb,UAAA;AAAA,IAAA,oBAAC,YAAA,EAAW;AAAA,wBACX,QAAA,EAAO;AAAA,IACR,qBAAC,QAAA,EAAK,WAAU,+EACd,UAAA;AAAA,MAAA,oBAAC,SAAA,EAAQ,UAAA,SAAS,OAAO,SAAS,KAAM,OAAO,OAAO,GAAG,OAAO,IAAI,MAAM,IAAI,IAAG;AAAA,MACjF,qBAAC,QAAA,EAAK,WAAU,mCACZ,UAAA;AAAA,SAAA,SAAS,OAAO,SAAS,OACzB,qBAAC,OAAA,EAAI,WAAU,mBACb,UAAA;AAAA,UAAA,oBAAC,MAAA,EAAG,WAAU,qEACX,UAAA,OAAO,MACV;AAAA,UACC,OAAO,eACN,oBAAC,OAAE,WAAU,8BACV,iBAAO,YAAA,CACV;AAAA,QAAA,GAEJ;AAAA,QAED,iCACE,OAAA,EAAI,WAAU,mBACb,UAAA,oBAAC,UAAA,EAAS,WAAqB,EAAA,CACjC;AAAA,MAAA,EAAA,CAEJ;AAAA,IAAA,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
1
+ {"version":3,"file":"home.js","sources":["../../../src/pages/home.tsx"],"sourcesContent":["import { useParams } from \"react-router-dom\"\nimport { useDirectory } from \"../../plugin/src/client\";\nimport Loading from \"@/components/loading\";\nimport PostList from \"@/components/post-list\";\nimport { ErrorDisplay } from \"@/components/page-error\";\nimport { RunningBar } from \"@/components/running-bar\";\nimport { Header } from \"@/components/header\";\nimport { ContentTabs } from \"@/components/content-tabs\";\nimport {\n type ContentView,\n directoryToPostEntries,\n filterVisiblePosts,\n getViewCounts,\n} from \"@/lib/content-classification\";\nimport siteConfig from \"virtual:veslx-config\";\n\ninterface HomeProps {\n view?: ContentView;\n}\n\nexport function Home({ view }: HomeProps) {\n const { \"*\": path = \".\" } = useParams();\n const config = siteConfig;\n\n // Normalize path - \"posts\", \"docs\", and \"all\" are view routes, not directories\n const isViewRoute = path === \"posts\" || path === \"docs\" || path === \"all\";\n const directoryPath = isViewRoute ? \".\" : path;\n\n const { directory, loading, error } = useDirectory(directoryPath)\n\n // Use prop view, fallback to config default\n const activeView = view ?? config.defaultView;\n\n const isRoot = path === \".\" || path === \"\" || isViewRoute;\n\n // Calculate counts for tabs (only meaningful on root)\n const counts = directory\n ? getViewCounts(filterVisiblePosts(directoryToPostEntries(directory)))\n : { posts: 0, docs: 0, all: 0 };\n\n if (error) {\n return <ErrorDisplay error={error} path={path} />;\n }\n\n if (loading) {\n return (\n <Loading />\n )\n }\n\n return (\n <div className=\"flex min-h-screen flex-col bg-background noise-overlay\">\n <RunningBar />\n <Header />\n <main className=\"flex-1 mx-auto w-full max-w-[var(--content-width)] px-[var(--page-padding)]\">\n <title>{isRoot ? config.name : `${config.name} - ${path}`}</title>\n <main className=\"flex flex-col gap-8 mb-32 mt-32\">\n {isRoot && (\n <div className=\"animate-fade-in\">\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-foreground\">\n {config.name}\n </h1>\n {config.description && (\n <p className=\"mt-2 text-muted-foreground\">\n {config.description}\n </p>\n )}\n </div>\n )}\n\n <div className=\"\">\n {isRoot && directory && (\n <div className=\"animate-fade-in\">\n <ContentTabs value={activeView} counts={counts} />\n </div>\n )}\n {directory && (\n <div className=\"animate-fade-in\">\n <PostList directory={directory} view={isRoot ? activeView : 'all'} />\n </div>\n )}\n </div>\n </main>\n </main>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;AAoBO,SAAS,KAAK,EAAE,QAAmB;AACxC,QAAM,EAAE,KAAK,OAAO,IAAA,IAAQ,UAAA;AAC5B,QAAM,SAAS;AAGf,QAAM,cAAc,SAAS,WAAW,SAAS,UAAU,SAAS;AACpE,QAAM,gBAAgB,cAAc,MAAM;AAE1C,QAAM,EAAE,WAAoB,UAAU,aAAa,aAAa;AAGhE,QAAM,aAAa,QAAQ,OAAO;AAElC,QAAM,SAAS,SAAS,OAAO,SAAS,MAAM;AAG9C,QAAM,SAAS,YACX,cAAc,mBAAmB,uBAAuB,SAAS,CAAC,CAAC,IACnE,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,EAAA;AAE9B,MAAI,OAAO;AACT,WAAO,oBAAC,cAAA,EAAa,OAAc,KAAA,CAAY;AAAA,EACjD;AAQA,SACE,qBAAC,OAAA,EAAI,WAAU,0DACb,UAAA;AAAA,IAAA,oBAAC,YAAA,EAAW;AAAA,wBACX,QAAA,EAAO;AAAA,IACR,qBAAC,QAAA,EAAK,WAAU,+EACd,UAAA;AAAA,MAAA,oBAAC,SAAA,EAAO,mBAAS,OAAO,OAAO,GAAG,OAAO,IAAI,MAAM,IAAI,GAAA,CAAG;AAAA,MAC1D,qBAAC,QAAA,EAAK,WAAU,mCACb,UAAA;AAAA,QAAA,UACC,qBAAC,OAAA,EAAI,WAAU,mBACb,UAAA;AAAA,UAAA,oBAAC,MAAA,EAAG,WAAU,qEACX,UAAA,OAAO,MACV;AAAA,UACC,OAAO,eACN,oBAAC,OAAE,WAAU,8BACV,iBAAO,YAAA,CACV;AAAA,QAAA,GAEJ;AAAA,QAGF,qBAAC,OAAA,EAAI,WAAU,IACZ,UAAA;AAAA,UAAA,UAAU,aACT,oBAAC,OAAA,EAAI,WAAU,mBACb,8BAAC,aAAA,EAAY,OAAO,YAAY,OAAA,CAAgB,EAAA,CAClD;AAAA,UAED,aACC,oBAAC,OAAA,EAAI,WAAU,mBACb,UAAA,oBAAC,UAAA,EAAS,WAAsB,MAAM,SAAS,aAAa,MAAA,CAAO,EAAA,CACrE;AAAA,QAAA,EAAA,CAEJ;AAAA,MAAA,EAAA,CACF;AAAA,IAAA,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
@@ -6,6 +6,7 @@ import { RunningBar } from "../components/running-bar.js";
6
6
  import { Header } from "../components/header.js";
7
7
  import { useMDXContent } from "../hooks/use-mdx-content.js";
8
8
  import { mdxComponents } from "../components/mdx-components.js";
9
+ import { formatDate } from "../lib/format-date.js";
9
10
  function Post() {
10
11
  const { "*": rawPath = "." } = useParams();
11
12
  const mdxPath = rawPath;
@@ -31,7 +32,14 @@ function Post() {
31
32
  /* @__PURE__ */ jsx("span", { className: "uppercase tracking-widest", children: "simulation running" }),
32
33
  /* @__PURE__ */ jsx("span", { className: "text-primary-foreground/60", children: "Page will auto-refresh on completion" })
33
34
  ] }) }),
34
- Content && /* @__PURE__ */ jsx("article", { className: "my-24 prose dark:prose-invert prose-headings:tracking-tight prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline max-w-[var(--prose-width)] animate-fade-in", children: /* @__PURE__ */ jsx(Content, { components: mdxComponents }) })
35
+ Content && /* @__PURE__ */ jsxs("article", { className: "my-24 prose dark:prose-invert prose-headings:tracking-tight prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline max-w-[var(--prose-width)] animate-fade-in", children: [
36
+ (frontmatter == null ? void 0 : frontmatter.title) && /* @__PURE__ */ jsxs("header", { className: "not-prose flex flex-col gap-2 mb-8 pt-4", children: [
37
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl md:text-3xl font-semibold tracking-tight text-foreground mb-3", children: frontmatter.title }),
38
+ (frontmatter == null ? void 0 : frontmatter.date) && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-3 text-muted-foreground", children: /* @__PURE__ */ jsx("time", { className: "font-mono text-xs bg-muted px-2 py-0.5 rounded", children: formatDate(new Date(frontmatter.date)) }) }),
39
+ (frontmatter == null ? void 0 : frontmatter.description) && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap text-sm items-center gap-3 text-muted-foreground", children: frontmatter.description })
40
+ ] }),
41
+ /* @__PURE__ */ jsx(Content, { components: mdxComponents })
42
+ ] })
35
43
  ] })
36
44
  ] });
37
45
  }
@@ -1 +1 @@
1
- {"version":3,"file":"post.js","sources":["../../../src/pages/post.tsx"],"sourcesContent":["import { useParams } from \"react-router-dom\";\nimport { findSlides, isSimulationRunning, useDirectory } from \"../../plugin/src/client\";\nimport Loading from \"@/components/loading\";\nimport { FileEntry } from \"plugin/src/lib\";\nimport { RunningBar } from \"@/components/running-bar\";\nimport { Header } from \"@/components/header\";\nimport { useMDXContent } from \"@/hooks/use-mdx-content\";\nimport { mdxComponents } from \"@/components/mdx-components\";\n\nexport function Post() {\n const { \"*\": rawPath = \".\" } = useParams();\n\n // The path includes the .mdx extension from the route\n const mdxPath = rawPath;\n\n // Extract directory path for finding sibling files (slides, etc.)\n const dirPath = mdxPath.replace(/\\/[^/]+\\.mdx$/, '') || '.';\n\n const { directory, loading: dirLoading } = useDirectory(dirPath)\n const { Content, frontmatter, loading: mdxLoading, error } = useMDXContent(mdxPath);\n const isRunning = isSimulationRunning();\n\n let slides: FileEntry | null = null;\n if (directory) {\n slides = findSlides(directory);\n }\n\n const loading = dirLoading || mdxLoading;\n\n if (loading) return <Loading />\n\n if (error) {\n return (\n <main className=\"min-h-screen bg-background container mx-auto max-w-4xl py-12\">\n <p className=\"text-center text-red-600\">{error.message}</p>\n </main>\n )\n }\n\n return (\n <div className=\"flex min-h-screen flex-col bg-background noise-overlay\">\n <title>{frontmatter?.title}</title>\n <RunningBar />\n <Header />\n <main className=\"flex-1 mx-auto w-full max-w-[var(--content-width)] px-[var(--page-padding)]\">\n {isRunning && (\n <div className=\"sticky top-0 z-50 px-[var(--page-padding)] py-2 bg-red-500 text-primary-foreground font-mono text-xs text-center tracking-wide\">\n <span className=\"inline-flex items-center gap-3\">\n <span className=\"h-1.5 w-1.5 rounded-full bg-current animate-pulse\" />\n <span className=\"uppercase tracking-widest\">simulation running</span>\n <span className=\"text-primary-foreground/60\">Page will auto-refresh on completion</span>\n </span>\n </div>\n )}\n\n {Content && (\n <article className=\"my-24 prose dark:prose-invert prose-headings:tracking-tight prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline max-w-[var(--prose-width)] animate-fade-in\">\n <Content components={mdxComponents} />\n </article>\n )}\n </main>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;;AASO,SAAS,OAAO;AACrB,QAAM,EAAE,KAAK,UAAU,IAAA,IAAQ,UAAA;AAG/B,QAAM,UAAU;AAGhB,QAAM,UAAU,QAAQ,QAAQ,iBAAiB,EAAE,KAAK;AAExD,QAAM,EAAE,UAA+B,IAAI,aAAa,OAAO;AAC/D,QAAM,EAAE,SAAS,aAAa,SAAS,YAAY,MAAA,IAAU,cAAc,OAAO;AAClF,QAAM,YAAY,oBAAA;AAGlB,MAAI,WAAW;AACJ,eAAW,SAAS;AAAA,EAC/B;AAEA,QAAM,UAAwB;AAE9B,MAAI,QAAS,QAAO,oBAAC,SAAA,CAAA,CAAQ;AAE7B,MAAI,OAAO;AACT,WACE,oBAAC,QAAA,EAAK,WAAU,gEACd,UAAA,oBAAC,OAAE,WAAU,4BAA4B,UAAA,MAAM,QAAA,CAAQ,GACzD;AAAA,EAEJ;AAEA,SACE,qBAAC,OAAA,EAAI,WAAU,0DACb,UAAA;AAAA,IAAA,oBAAC,SAAA,EAAO,qDAAa,MAAA,CAAM;AAAA,wBAC1B,YAAA,EAAW;AAAA,wBACX,QAAA,EAAO;AAAA,IACR,qBAAC,QAAA,EAAK,WAAU,+EACb,UAAA;AAAA,MAAA,iCACE,OAAA,EAAI,WAAU,kIACb,UAAA,qBAAC,QAAA,EAAK,WAAU,kCACd,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAK,WAAU,oDAAA,CAAoD;AAAA,QACpE,oBAAC,QAAA,EAAK,WAAU,6BAA4B,UAAA,sBAAkB;AAAA,QAC9D,oBAAC,QAAA,EAAK,WAAU,8BAA6B,UAAA,uCAAA,CAAoC;AAAA,MAAA,EAAA,CACnF,EAAA,CACF;AAAA,MAGD,+BACE,WAAA,EAAQ,WAAU,oMACjB,UAAA,oBAAC,SAAA,EAAQ,YAAY,cAAA,CAAe,EAAA,CACtC;AAAA,IAAA,EAAA,CAEJ;AAAA,EAAA,GACF;AAEJ;"}
1
+ {"version":3,"file":"post.js","sources":["../../../src/pages/post.tsx"],"sourcesContent":["import { useParams } from \"react-router-dom\";\nimport { findSlides, isSimulationRunning, useDirectory } from \"../../plugin/src/client\";\nimport Loading from \"@/components/loading\";\nimport { FileEntry } from \"plugin/src/lib\";\nimport { RunningBar } from \"@/components/running-bar\";\nimport { Header } from \"@/components/header\";\nimport { useMDXContent } from \"@/hooks/use-mdx-content\";\nimport { mdxComponents } from \"@/components/mdx-components\";\nimport { formatDate } from \"@/lib/format-date\";\n\nexport function Post() {\n const { \"*\": rawPath = \".\" } = useParams();\n\n // The path includes the .mdx extension from the route\n const mdxPath = rawPath;\n\n // Extract directory path for finding sibling files (slides, etc.)\n const dirPath = mdxPath.replace(/\\/[^/]+\\.mdx$/, '') || '.';\n\n const { directory, loading: dirLoading } = useDirectory(dirPath)\n const { Content, frontmatter, loading: mdxLoading, error } = useMDXContent(mdxPath);\n const isRunning = isSimulationRunning();\n\n let slides: FileEntry | null = null;\n if (directory) {\n slides = findSlides(directory);\n }\n\n const loading = dirLoading || mdxLoading;\n\n if (loading) return <Loading />\n\n if (error) {\n return (\n <main className=\"min-h-screen bg-background container mx-auto max-w-4xl py-12\">\n <p className=\"text-center text-red-600\">{error.message}</p>\n </main>\n )\n }\n\n return (\n <div className=\"flex min-h-screen flex-col bg-background noise-overlay\">\n <title>{frontmatter?.title}</title>\n <RunningBar />\n <Header />\n <main className=\"flex-1 mx-auto w-full max-w-[var(--content-width)] px-[var(--page-padding)]\">\n {isRunning && (\n <div className=\"sticky top-0 z-50 px-[var(--page-padding)] py-2 bg-red-500 text-primary-foreground font-mono text-xs text-center tracking-wide\">\n <span className=\"inline-flex items-center gap-3\">\n <span className=\"h-1.5 w-1.5 rounded-full bg-current animate-pulse\" />\n <span className=\"uppercase tracking-widest\">simulation running</span>\n <span className=\"text-primary-foreground/60\">Page will auto-refresh on completion</span>\n </span>\n </div>\n )}\n\n {Content && (\n <article className=\"my-24 prose dark:prose-invert prose-headings:tracking-tight prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline max-w-[var(--prose-width)] animate-fade-in\">\n {/* Render frontmatter header */}\n {frontmatter?.title && (\n <header className=\"not-prose flex flex-col gap-2 mb-8 pt-4\">\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-foreground mb-3\">\n {frontmatter.title}\n </h1>\n {frontmatter?.date && (\n <div className=\"flex flex-wrap items-center gap-3 text-muted-foreground\">\n <time className=\"font-mono text-xs bg-muted px-2 py-0.5 rounded\">\n {formatDate(new Date(frontmatter.date as string))}\n </time>\n </div>\n )}\n {frontmatter?.description && (\n <div className=\"flex flex-wrap text-sm items-center gap-3 text-muted-foreground\">\n {frontmatter.description}\n </div>\n )}\n </header>\n )}\n <Content components={mdxComponents} />\n </article>\n )}\n </main>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;AAUO,SAAS,OAAO;AACrB,QAAM,EAAE,KAAK,UAAU,IAAA,IAAQ,UAAA;AAG/B,QAAM,UAAU;AAGhB,QAAM,UAAU,QAAQ,QAAQ,iBAAiB,EAAE,KAAK;AAExD,QAAM,EAAE,UAA+B,IAAI,aAAa,OAAO;AAC/D,QAAM,EAAE,SAAS,aAAa,SAAS,YAAY,MAAA,IAAU,cAAc,OAAO;AAClF,QAAM,YAAY,oBAAA;AAGlB,MAAI,WAAW;AACJ,eAAW,SAAS;AAAA,EAC/B;AAEA,QAAM,UAAwB;AAE9B,MAAI,QAAS,QAAO,oBAAC,SAAA,CAAA,CAAQ;AAE7B,MAAI,OAAO;AACT,WACE,oBAAC,QAAA,EAAK,WAAU,gEACd,UAAA,oBAAC,OAAE,WAAU,4BAA4B,UAAA,MAAM,QAAA,CAAQ,GACzD;AAAA,EAEJ;AAEA,SACE,qBAAC,OAAA,EAAI,WAAU,0DACb,UAAA;AAAA,IAAA,oBAAC,SAAA,EAAO,qDAAa,MAAA,CAAM;AAAA,wBAC1B,YAAA,EAAW;AAAA,wBACX,QAAA,EAAO;AAAA,IACR,qBAAC,QAAA,EAAK,WAAU,+EACb,UAAA;AAAA,MAAA,iCACE,OAAA,EAAI,WAAU,kIACb,UAAA,qBAAC,QAAA,EAAK,WAAU,kCACd,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAK,WAAU,oDAAA,CAAoD;AAAA,QACpE,oBAAC,QAAA,EAAK,WAAU,6BAA4B,UAAA,sBAAkB;AAAA,QAC9D,oBAAC,QAAA,EAAK,WAAU,8BAA6B,UAAA,uCAAA,CAAoC;AAAA,MAAA,EAAA,CACnF,EAAA,CACF;AAAA,MAGD,WACC,qBAAC,WAAA,EAAQ,WAAU,oMAEhB,UAAA;AAAA,SAAA,2CAAa,UACZ,qBAAC,UAAA,EAAO,WAAU,2CAChB,UAAA;AAAA,UAAA,oBAAC,MAAA,EAAG,WAAU,0EACX,UAAA,YAAY,OACf;AAAA,WACC,2CAAa,SACZ,oBAAC,OAAA,EAAI,WAAU,2DACb,UAAA,oBAAC,QAAA,EAAK,WAAU,kDACb,qBAAW,IAAI,KAAK,YAAY,IAAc,CAAC,GAClD,GACF;AAAA,WAED,2CAAa,gBACZ,oBAAC,SAAI,WAAU,mEACZ,sBAAY,YAAA,CACf;AAAA,QAAA,GAEJ;AAAA,QAEF,oBAAC,SAAA,EAAQ,YAAY,cAAA,CAAe;AAAA,MAAA,EAAA,CACtC;AAAA,IAAA,EAAA,CAEJ;AAAA,EAAA,GACF;AAEJ;"}
@@ -65,7 +65,14 @@ function buildDirectoryTree(globKeys, frontmatters) {
65
65
  (c) => c.type === "file" && c.name === filename
66
66
  );
67
67
  if (exists) continue;
68
- const frontmatter = frontmatters == null ? void 0 : frontmatters[key];
68
+ let frontmatter = frontmatters == null ? void 0 : frontmatters[key];
69
+ if (!frontmatter) {
70
+ frontmatter = frontmatters == null ? void 0 : frontmatters[`@content/${relativePath}`];
71
+ }
72
+ if (!frontmatter) {
73
+ const keyWithoutSlash = key.startsWith("/") ? key.slice(1) : key;
74
+ frontmatter = frontmatters == null ? void 0 : frontmatters[keyWithoutSlash];
75
+ }
69
76
  const fileEntry = {
70
77
  type: "file",
71
78
  name: filename,
@@ -1 +1 @@
1
- {"version":3,"file":"directory-tree.js","sources":["../../../../plugin/src/directory-tree.ts"],"sourcesContent":["import type { DirectoryEntry, FileEntry } from './lib';\n\n/**\n * Find the common directory prefix among all paths.\n * E.g., [\"/docs/a.mdx\", \"/docs/b/c.mdx\"] -> \"/docs\"\n * Only considers directory segments, not filenames.\n */\nfunction findCommonPrefix(paths: string[]): string {\n if (paths.length === 0) return '';\n\n // Extract directory parts only (exclude filename from each path)\n const dirPaths = paths.map(p => {\n const parts = p.split('/').filter(Boolean);\n // Remove the last part (filename)\n return parts.slice(0, -1);\n });\n\n if (dirPaths.length === 0 || dirPaths[0].length === 0) return '';\n\n // Find how many directory segments are common to all paths\n const firstDirParts = dirPaths[0];\n let commonLength = 0;\n\n for (let i = 0; i < firstDirParts.length; i++) {\n const segment = firstDirParts[i];\n const allMatch = dirPaths.every(parts => parts[i] === segment);\n if (allMatch) {\n commonLength = i + 1;\n } else {\n break;\n }\n }\n\n if (commonLength > 0) {\n return '/' + firstDirParts.slice(0, commonLength).join('/');\n }\n return '';\n}\n\n/**\n * Build a directory tree from glob keys.\n * Keys are paths like \"/docs/file.mdx\" (Vite-resolved from @content alias)\n * We auto-detect and strip the common prefix (content directory).\n * @param globKeys - Array of file paths from import.meta.glob\n * @param frontmatters - Optional map of paths to frontmatter objects\n */\nexport function buildDirectoryTree(\n globKeys: string[],\n frontmatters?: Record<string, FileEntry['frontmatter']>\n): DirectoryEntry {\n const root: DirectoryEntry = {\n type: 'directory',\n name: '.',\n path: '.',\n children: []\n };\n\n if (globKeys.length === 0) return root;\n\n // Auto-detect the content directory prefix\n // Vite resolves @content to the actual path, so keys look like \"/docs/file.mdx\"\n const commonPrefix = findCommonPrefix(globKeys);\n\n for (const key of globKeys) {\n // Strip the common prefix to get path relative to content root\n let relativePath = key;\n if (commonPrefix && key.startsWith(commonPrefix)) {\n relativePath = key.slice(commonPrefix.length);\n }\n // Remove leading slash if present\n if (relativePath.startsWith('/')) {\n relativePath = relativePath.slice(1);\n }\n\n // Skip hidden files and directories\n if (relativePath.split('/').some(part => part.startsWith('.'))) {\n continue;\n }\n\n const parts = relativePath.split('/').filter(Boolean);\n if (parts.length === 0) continue;\n\n let current = root;\n\n // Navigate/create directories for all but the last part\n for (let i = 0; i < parts.length - 1; i++) {\n const dirName = parts[i];\n let dir = current.children.find(\n c => c.type === 'directory' && c.name === dirName\n ) as DirectoryEntry | undefined;\n\n if (!dir) {\n dir = {\n type: 'directory',\n name: dirName,\n path: parts.slice(0, i + 1).join('/'),\n children: []\n };\n current.children.push(dir);\n }\n current = dir;\n }\n\n // Add file entry (last part)\n const filename = parts[parts.length - 1];\n\n // Don't add duplicates\n const exists = current.children.some(\n c => c.type === 'file' && c.name === filename\n );\n if (exists) continue;\n\n // Look up frontmatter using the original key (before prefix stripping)\n const frontmatter = frontmatters?.[key];\n\n const fileEntry: FileEntry = {\n type: 'file',\n name: filename,\n path: relativePath,\n size: 0, // Size not available from glob keys\n frontmatter\n };\n current.children.push(fileEntry);\n }\n\n return root;\n}\n\n/**\n * Navigate to a path within the directory tree.\n * Returns the directory and optionally a file if the path points to one.\n */\nexport function navigateToPath(\n root: DirectoryEntry,\n path: string\n): { directory: DirectoryEntry; file: FileEntry | null } {\n const parts = path === '.' || path === '' ? [] : path.split('/').filter(Boolean);\n\n let currentDir = root;\n let file: FileEntry | null = null;\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n const isLastPart = i === parts.length - 1;\n\n // Check if this part matches a file (only on last part)\n if (isLastPart) {\n const matchedFile = currentDir.children.find(\n child => child.type === 'file' && child.name === part\n ) as FileEntry | undefined;\n\n if (matchedFile) {\n file = matchedFile;\n break;\n }\n }\n\n // Otherwise, look for a directory\n const nextDir = currentDir.children.find(\n child => child.type === 'directory' && child.name === part\n ) as DirectoryEntry | undefined;\n\n if (!nextDir) {\n throw new Error(`Path not found: ${path}`);\n }\n\n currentDir = nextDir;\n }\n\n return { directory: currentDir, file };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"directory-tree.js","sources":["../../../../plugin/src/directory-tree.ts"],"sourcesContent":["import type { DirectoryEntry, FileEntry } from './lib';\n\n/**\n * Find the common directory prefix among all paths.\n * E.g., [\"/docs/a.mdx\", \"/docs/b/c.mdx\"] -> \"/docs\"\n * Only considers directory segments, not filenames.\n */\nfunction findCommonPrefix(paths: string[]): string {\n if (paths.length === 0) return '';\n\n // Extract directory parts only (exclude filename from each path)\n const dirPaths = paths.map(p => {\n const parts = p.split('/').filter(Boolean);\n // Remove the last part (filename)\n return parts.slice(0, -1);\n });\n\n if (dirPaths.length === 0 || dirPaths[0].length === 0) return '';\n\n // Find how many directory segments are common to all paths\n const firstDirParts = dirPaths[0];\n let commonLength = 0;\n\n for (let i = 0; i < firstDirParts.length; i++) {\n const segment = firstDirParts[i];\n const allMatch = dirPaths.every(parts => parts[i] === segment);\n if (allMatch) {\n commonLength = i + 1;\n } else {\n break;\n }\n }\n\n if (commonLength > 0) {\n return '/' + firstDirParts.slice(0, commonLength).join('/');\n }\n return '';\n}\n\n/**\n * Build a directory tree from glob keys.\n * Keys are paths like \"/docs/file.mdx\" (Vite-resolved from @content alias)\n * We auto-detect and strip the common prefix (content directory).\n * @param globKeys - Array of file paths from import.meta.glob\n * @param frontmatters - Optional map of paths to frontmatter objects\n */\nexport function buildDirectoryTree(\n globKeys: string[],\n frontmatters?: Record<string, FileEntry['frontmatter']>\n): DirectoryEntry {\n const root: DirectoryEntry = {\n type: 'directory',\n name: '.',\n path: '.',\n children: []\n };\n\n if (globKeys.length === 0) return root;\n\n // Auto-detect the content directory prefix\n // Vite resolves @content to the actual path, so keys look like \"/docs/file.mdx\"\n const commonPrefix = findCommonPrefix(globKeys);\n\n for (const key of globKeys) {\n // Strip the common prefix to get path relative to content root\n let relativePath = key;\n if (commonPrefix && key.startsWith(commonPrefix)) {\n relativePath = key.slice(commonPrefix.length);\n }\n // Remove leading slash if present\n if (relativePath.startsWith('/')) {\n relativePath = relativePath.slice(1);\n }\n\n // Skip hidden files and directories\n if (relativePath.split('/').some(part => part.startsWith('.'))) {\n continue;\n }\n\n const parts = relativePath.split('/').filter(Boolean);\n if (parts.length === 0) continue;\n\n let current = root;\n\n // Navigate/create directories for all but the last part\n for (let i = 0; i < parts.length - 1; i++) {\n const dirName = parts[i];\n let dir = current.children.find(\n c => c.type === 'directory' && c.name === dirName\n ) as DirectoryEntry | undefined;\n\n if (!dir) {\n dir = {\n type: 'directory',\n name: dirName,\n path: parts.slice(0, i + 1).join('/'),\n children: []\n };\n current.children.push(dir);\n }\n current = dir;\n }\n\n // Add file entry (last part)\n const filename = parts[parts.length - 1];\n\n // Don't add duplicates\n const exists = current.children.some(\n c => c.type === 'file' && c.name === filename\n );\n if (exists) continue;\n\n // Look up frontmatter - try multiple key formats since Vite resolves paths differently\n // Plugin stores keys as \"@content/path\", but glob keys may be resolved paths\n let frontmatter = frontmatters?.[key];\n if (!frontmatter) {\n // Try with @content prefix using the relative path\n frontmatter = frontmatters?.[`@content/${relativePath}`];\n }\n if (!frontmatter) {\n // Try without leading slash\n const keyWithoutSlash = key.startsWith('/') ? key.slice(1) : key;\n frontmatter = frontmatters?.[keyWithoutSlash];\n }\n\n const fileEntry: FileEntry = {\n type: 'file',\n name: filename,\n path: relativePath,\n size: 0, // Size not available from glob keys\n frontmatter\n };\n current.children.push(fileEntry);\n }\n\n return root;\n}\n\n/**\n * Navigate to a path within the directory tree.\n * Returns the directory and optionally a file if the path points to one.\n */\nexport function navigateToPath(\n root: DirectoryEntry,\n path: string\n): { directory: DirectoryEntry; file: FileEntry | null } {\n const parts = path === '.' || path === '' ? [] : path.split('/').filter(Boolean);\n\n let currentDir = root;\n let file: FileEntry | null = null;\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n const isLastPart = i === parts.length - 1;\n\n // Check if this part matches a file (only on last part)\n if (isLastPart) {\n const matchedFile = currentDir.children.find(\n child => child.type === 'file' && child.name === part\n ) as FileEntry | undefined;\n\n if (matchedFile) {\n file = matchedFile;\n break;\n }\n }\n\n // Otherwise, look for a directory\n const nextDir = currentDir.children.find(\n child => child.type === 'directory' && child.name === part\n ) as DirectoryEntry | undefined;\n\n if (!nextDir) {\n throw new Error(`Path not found: ${path}`);\n }\n\n currentDir = nextDir;\n }\n\n return { directory: currentDir, file };\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veslx",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,6 +42,7 @@
42
42
  "@radix-ui/react-separator": "^1.1.8",
43
43
  "@radix-ui/react-slider": "^1.3.6",
44
44
  "@radix-ui/react-slot": "^1.2.4",
45
+ "@radix-ui/react-tabs": "^1.1.2",
45
46
  "@radix-ui/react-tooltip": "^1.2.8",
46
47
  "@radix-ui/react-use-controllable-state": "^1.2.2",
47
48
  "@tailwindcss/postcss": "^4.1.17",
@@ -109,5 +110,8 @@
109
110
  "typescript-eslint": "^8.46.3",
110
111
  "vite-plugin-dts": "^4.5.4"
111
112
  },
112
- "overrides": {}
113
+ "overrides": {
114
+ "react": "^19.2.0",
115
+ "react-dom": "^19.2.0"
116
+ }
113
117
  }
@@ -110,8 +110,18 @@ export function buildDirectoryTree(
110
110
  );
111
111
  if (exists) continue;
112
112
 
113
- // Look up frontmatter using the original key (before prefix stripping)
114
- const frontmatter = frontmatters?.[key];
113
+ // Look up frontmatter - try multiple key formats since Vite resolves paths differently
114
+ // Plugin stores keys as "@content/path", but glob keys may be resolved paths
115
+ let frontmatter = frontmatters?.[key];
116
+ if (!frontmatter) {
117
+ // Try with @content prefix using the relative path
118
+ frontmatter = frontmatters?.[`@content/${relativePath}`];
119
+ }
120
+ if (!frontmatter) {
121
+ // Try without leading slash
122
+ const keyWithoutSlash = key.startsWith('/') ? key.slice(1) : key;
123
+ frontmatter = frontmatters?.[keyWithoutSlash];
124
+ }
115
125
 
116
126
  const fileEntry: FileEntry = {
117
127
  type: 'file',
@@ -1,7 +1,10 @@
1
+ export type ContentView = 'posts' | 'docs' | 'all';
2
+
1
3
  export interface SiteConfig {
2
4
  name?: string;
3
5
  description?: string;
4
6
  github?: string;
7
+ defaultView?: ContentView;
5
8
  }
6
9
 
7
10
  export interface VeslxConfig {
@@ -13,10 +16,12 @@ export interface ResolvedSiteConfig {
13
16
  name: string;
14
17
  description: string;
15
18
  github: string;
19
+ defaultView: ContentView;
16
20
  }
17
21
 
18
22
  export const DEFAULT_SITE_CONFIG: ResolvedSiteConfig = {
19
23
  name: 'veslx',
20
24
  description: '',
21
25
  github: '',
26
+ defaultView: 'all',
22
27
  };
@@ -0,0 +1,64 @@
1
+ import { Link } from "react-router-dom";
2
+ import { cn } from "@/lib/utils";
3
+ import type { ContentView } from "@/lib/content-classification";
4
+
5
+ interface ContentTabsProps {
6
+ value: ContentView;
7
+ counts: { posts: number; docs: number; all: number };
8
+ }
9
+
10
+ const views: { key: ContentView; label: string; path: string }[] = [
11
+ { key: "posts", label: "posts", path: "/posts" },
12
+ { key: "docs", label: "docs", path: "/docs" },
13
+ { key: "all", label: "all", path: "/all" },
14
+ ];
15
+
16
+ export function ContentTabs({ value, counts }: ContentTabsProps) {
17
+ const hasOnlyPosts = counts.posts > 0 && counts.docs === 0;
18
+ const hasOnlyDocs = counts.docs > 0 && counts.posts === 0;
19
+
20
+ if (hasOnlyPosts || hasOnlyDocs) {
21
+ return null;
22
+ }
23
+
24
+ const isDisabled = (key: ContentView) => {
25
+ if (key === "posts") return counts.posts === 0;
26
+ if (key === "docs") return counts.docs === 0;
27
+ return false;
28
+ };
29
+
30
+ return (
31
+ <nav className="flex justify-end items-center gap-3 font-mono font-medium text-xs text-muted-foreground">
32
+ {views.map((view) => {
33
+ const disabled = isDisabled(view.key);
34
+
35
+ if (disabled) {
36
+ return (
37
+ <span
38
+ key={view.key}
39
+ className="opacity-30 cursor-not-allowed"
40
+ >
41
+ {view.label}
42
+ </span>
43
+ );
44
+ }
45
+
46
+ return (
47
+ <Link
48
+ key={view.key}
49
+ to={view.path}
50
+ className={cn(
51
+ "transition-colors duration-150",
52
+ "hover:text-foreground hover:underline hover:underline-offset-4 hover:decoration-primary/60",
53
+ value === view.key
54
+ ? "text-foreground underline-offset-4 decoration-primary/60"
55
+ : ""
56
+ )}
57
+ >
58
+ {view.label}
59
+ </Link>
60
+ );
61
+ })}
62
+ </nav>
63
+ );
64
+ }
@@ -1,18 +1,16 @@
1
1
  import { Link } from "react-router-dom";
2
2
  import { cn } from "@/lib/utils";
3
- import { DirectoryEntry, FileEntry } from "../../plugin/src/lib";
4
- import { findReadme, findSlides, findMdxFiles } from "../../plugin/src/client";
3
+ import type { DirectoryEntry } from "../../plugin/src/lib";
5
4
  import { formatDate } from "@/lib/format-date";
6
5
  import { ArrowRight } from "lucide-react";
7
-
8
- type PostEntry = {
9
- type: 'folder' | 'file';
10
- name: string;
11
- path: string;
12
- readme: FileEntry | null;
13
- slides: FileEntry | null;
14
- file: FileEntry | null; // For standalone MDX files
15
- };
6
+ import {
7
+ type ContentView,
8
+ type PostEntry,
9
+ directoryToPostEntries,
10
+ filterVisiblePosts,
11
+ filterByView,
12
+ getFrontmatter,
13
+ } from "@/lib/content-classification";
16
14
 
17
15
  // Helper to extract numeric prefix from filename (e.g., "01-intro" → 1)
18
16
  function extractOrder(name: string): number | null {
@@ -28,35 +26,13 @@ function stripNumericPrefix(name: string): string {
28
26
  .replace(/\b\w/g, c => c.toUpperCase());
29
27
  }
30
28
 
31
- export default function PostList({ directory }: { directory: DirectoryEntry }) {
32
- const folders = directory.children.filter((c): c is DirectoryEntry => c.type === "directory");
33
- const standaloneFiles = findMdxFiles(directory);
34
-
35
- // Convert folders to post entries
36
- const folderPosts: PostEntry[] = folders.map((folder) => {
37
- const readme = findReadme(folder);
38
- const slides = findSlides(folder);
39
- return {
40
- type: 'folder' as const,
41
- name: folder.name,
42
- path: folder.path,
43
- readme,
44
- slides,
45
- file: null,
46
- };
47
- });
48
-
49
- // Convert standalone MDX files to post entries
50
- const filePosts: PostEntry[] = standaloneFiles.map((file) => ({
51
- type: 'file' as const,
52
- name: file.name.replace(/\.mdx?$/, ''),
53
- path: file.path,
54
- readme: null,
55
- slides: null,
56
- file,
57
- }));
29
+ interface PostListProps {
30
+ directory: DirectoryEntry;
31
+ view?: ContentView;
32
+ }
58
33
 
59
- let posts: PostEntry[] = [...folderPosts, ...filePosts];
34
+ export default function PostList({ directory, view = 'all' }: PostListProps) {
35
+ let posts = directoryToPostEntries(directory);
60
36
 
61
37
  if (posts.length === 0) {
62
38
  return (
@@ -67,15 +43,18 @@ export default function PostList({ directory }: { directory: DirectoryEntry }) {
67
43
  }
68
44
 
69
45
  // Filter out hidden and draft posts
70
- posts = posts.filter((post) => {
71
- const frontmatter = post.readme?.frontmatter || post.file?.frontmatter;
72
- return frontmatter?.visibility !== "hidden" && frontmatter?.draft !== true;
73
- });
46
+ posts = filterVisiblePosts(posts);
74
47
 
75
- // Helper to get frontmatter from post
76
- const getFrontmatter = (post: PostEntry) => {
77
- return post.readme?.frontmatter || post.file?.frontmatter || post.slides?.frontmatter;
78
- };
48
+ // Apply view filter
49
+ posts = filterByView(posts, view);
50
+
51
+ if (posts.length === 0) {
52
+ return (
53
+ <div className="py-24 text-center">
54
+ <p className="text-muted-foreground font-mono text-sm tracking-wide">no entries</p>
55
+ </div>
56
+ );
57
+ }
79
58
 
80
59
  // Helper to get date from post
81
60
  const getPostDate = (post: PostEntry): Date | null => {
@@ -0,0 +1,53 @@
1
+ import * as React from "react"
2
+ import * as TabsPrimitive from "@radix-ui/react-tabs"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const Tabs = TabsPrimitive.Root
7
+
8
+ const TabsList = React.forwardRef<
9
+ React.ElementRef<typeof TabsPrimitive.List>,
10
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
11
+ >(({ className, ...props }, ref) => (
12
+ <TabsPrimitive.List
13
+ ref={ref}
14
+ className={cn(
15
+ "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ ))
21
+ TabsList.displayName = TabsPrimitive.List.displayName
22
+
23
+ const TabsTrigger = React.forwardRef<
24
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
25
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
26
+ >(({ className, ...props }, ref) => (
27
+ <TabsPrimitive.Trigger
28
+ ref={ref}
29
+ className={cn(
30
+ "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
31
+ className
32
+ )}
33
+ {...props}
34
+ />
35
+ ))
36
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
37
+
38
+ const TabsContent = React.forwardRef<
39
+ React.ElementRef<typeof TabsPrimitive.Content>,
40
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
41
+ >(({ className, ...props }, ref) => (
42
+ <TabsPrimitive.Content
43
+ ref={ref}
44
+ className={cn(
45
+ "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ ))
51
+ TabsContent.displayName = TabsPrimitive.Content.displayName
52
+
53
+ export { Tabs, TabsList, TabsTrigger, TabsContent }
@@ -0,0 +1,73 @@
1
+ import type { ContentView } from "../../plugin/src/types";
2
+ import type { DirectoryEntry, FileEntry } from "../../plugin/src/lib";
3
+ import { findReadme, findSlides, findMdxFiles } from "../../plugin/src/client";
4
+
5
+ export type PostEntry = {
6
+ type: 'folder' | 'file';
7
+ name: string;
8
+ path: string;
9
+ readme: FileEntry | null;
10
+ slides: FileEntry | null;
11
+ file: FileEntry | null;
12
+ };
13
+
14
+ export function getFrontmatter(post: PostEntry) {
15
+ return post.readme?.frontmatter || post.file?.frontmatter || post.slides?.frontmatter;
16
+ }
17
+
18
+ export function hasDate(post: PostEntry): boolean {
19
+ const frontmatter = getFrontmatter(post);
20
+ return frontmatter?.date !== undefined && frontmatter.date !== null && frontmatter.date !== '';
21
+ }
22
+
23
+ export function filterByView(posts: PostEntry[], view: ContentView): PostEntry[] {
24
+ if (view === 'all') return posts;
25
+ if (view === 'posts') return posts.filter(hasDate);
26
+ if (view === 'docs') return posts.filter(post => !hasDate(post));
27
+ return posts;
28
+ }
29
+
30
+ export function getViewCounts(posts: PostEntry[]): { posts: number; docs: number; all: number } {
31
+ const dated = posts.filter(hasDate).length;
32
+ return {
33
+ posts: dated,
34
+ docs: posts.length - dated,
35
+ all: posts.length,
36
+ };
37
+ }
38
+
39
+ export function directoryToPostEntries(directory: DirectoryEntry): PostEntry[] {
40
+ const folders = directory.children.filter((c): c is DirectoryEntry => c.type === "directory");
41
+ const standaloneFiles = findMdxFiles(directory);
42
+
43
+ const folderPosts: PostEntry[] = folders
44
+ .map((folder) => ({
45
+ type: 'folder' as const,
46
+ name: folder.name,
47
+ path: folder.path,
48
+ readme: findReadme(folder),
49
+ slides: findSlides(folder),
50
+ file: null,
51
+ }))
52
+ .filter((post) => post.readme || post.slides); // Only include folders with content
53
+
54
+ const filePosts: PostEntry[] = standaloneFiles.map((file) => ({
55
+ type: 'file' as const,
56
+ name: file.name.replace(/\.mdx?$/, ''),
57
+ path: file.path,
58
+ readme: null,
59
+ slides: null,
60
+ file,
61
+ }));
62
+
63
+ return [...folderPosts, ...filePosts];
64
+ }
65
+
66
+ export function filterVisiblePosts(posts: PostEntry[]): PostEntry[] {
67
+ return posts.filter((post) => {
68
+ const frontmatter = getFrontmatter(post);
69
+ return frontmatter?.visibility !== "hidden" && frontmatter?.draft !== true;
70
+ });
71
+ }
72
+
73
+ export type { ContentView };
@@ -5,6 +5,8 @@ import { SlidesPage } from "./slides"
5
5
 
6
6
  /**
7
7
  * Routes to the appropriate page based on the URL path:
8
+ * - /posts → Home with posts view
9
+ * - /docs → Home with docs view
8
10
  * - *.slides.mdx or *SLIDES.mdx → SlidesPage
9
11
  * - *.mdx → Post
10
12
  * - everything else → Home (directory listing)
@@ -12,6 +14,17 @@ import { SlidesPage } from "./slides"
12
14
  export function ContentRouter() {
13
15
  const { "*": path = "" } = useParams()
14
16
 
17
+ // Check for content view routes
18
+ if (path === 'posts') {
19
+ return <Home view="posts" />
20
+ }
21
+ if (path === 'docs') {
22
+ return <Home view="docs" />
23
+ }
24
+ if (path === 'all') {
25
+ return <Home view="all" />
26
+ }
27
+
15
28
  // Check if this is a slides file
16
29
  if (path.endsWith('.slides.mdx') || path.endsWith('SLIDES.mdx')) {
17
30
  return <SlidesPage />
@@ -5,13 +5,39 @@ import PostList from "@/components/post-list";
5
5
  import { ErrorDisplay } from "@/components/page-error";
6
6
  import { RunningBar } from "@/components/running-bar";
7
7
  import { Header } from "@/components/header";
8
+ import { ContentTabs } from "@/components/content-tabs";
9
+ import {
10
+ type ContentView,
11
+ directoryToPostEntries,
12
+ filterVisiblePosts,
13
+ getViewCounts,
14
+ } from "@/lib/content-classification";
8
15
  import siteConfig from "virtual:veslx-config";
9
16
 
10
- export function Home() {
17
+ interface HomeProps {
18
+ view?: ContentView;
19
+ }
20
+
21
+ export function Home({ view }: HomeProps) {
11
22
  const { "*": path = "." } = useParams();
12
- const { directory, loading, error } = useDirectory(path)
13
23
  const config = siteConfig;
14
24
 
25
+ // Normalize path - "posts", "docs", and "all" are view routes, not directories
26
+ const isViewRoute = path === "posts" || path === "docs" || path === "all";
27
+ const directoryPath = isViewRoute ? "." : path;
28
+
29
+ const { directory, loading, error } = useDirectory(directoryPath)
30
+
31
+ // Use prop view, fallback to config default
32
+ const activeView = view ?? config.defaultView;
33
+
34
+ const isRoot = path === "." || path === "" || isViewRoute;
35
+
36
+ // Calculate counts for tabs (only meaningful on root)
37
+ const counts = directory
38
+ ? getViewCounts(filterVisiblePosts(directoryToPostEntries(directory)))
39
+ : { posts: 0, docs: 0, all: 0 };
40
+
15
41
  if (error) {
16
42
  return <ErrorDisplay error={error} path={path} />;
17
43
  }
@@ -27,9 +53,9 @@ export function Home() {
27
53
  <RunningBar />
28
54
  <Header />
29
55
  <main className="flex-1 mx-auto w-full max-w-[var(--content-width)] px-[var(--page-padding)]">
30
- <title>{(path === "." || path === "") ? config.name : `${config.name} - ${path}`}</title>
56
+ <title>{isRoot ? config.name : `${config.name} - ${path}`}</title>
31
57
  <main className="flex flex-col gap-8 mb-32 mt-32">
32
- {(path === "." || path === "") && (
58
+ {isRoot && (
33
59
  <div className="animate-fade-in">
34
60
  <h1 className="text-2xl md:text-3xl font-semibold tracking-tight text-foreground">
35
61
  {config.name}
@@ -41,11 +67,19 @@ export function Home() {
41
67
  )}
42
68
  </div>
43
69
  )}
44
- {directory && (
45
- <div className="animate-fade-in">
46
- <PostList directory={directory}/>
47
- </div>
48
- )}
70
+
71
+ <div className="">
72
+ {isRoot && directory && (
73
+ <div className="animate-fade-in">
74
+ <ContentTabs value={activeView} counts={counts} />
75
+ </div>
76
+ )}
77
+ {directory && (
78
+ <div className="animate-fade-in">
79
+ <PostList directory={directory} view={isRoot ? activeView : 'all'} />
80
+ </div>
81
+ )}
82
+ </div>
49
83
  </main>
50
84
  </main>
51
85
  </div>
@@ -6,6 +6,7 @@ import { RunningBar } from "@/components/running-bar";
6
6
  import { Header } from "@/components/header";
7
7
  import { useMDXContent } from "@/hooks/use-mdx-content";
8
8
  import { mdxComponents } from "@/components/mdx-components";
9
+ import { formatDate } from "@/lib/format-date";
9
10
 
10
11
  export function Post() {
11
12
  const { "*": rawPath = "." } = useParams();
@@ -55,6 +56,26 @@ export function Post() {
55
56
 
56
57
  {Content && (
57
58
  <article className="my-24 prose dark:prose-invert prose-headings:tracking-tight prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline max-w-[var(--prose-width)] animate-fade-in">
59
+ {/* Render frontmatter header */}
60
+ {frontmatter?.title && (
61
+ <header className="not-prose flex flex-col gap-2 mb-8 pt-4">
62
+ <h1 className="text-2xl md:text-3xl font-semibold tracking-tight text-foreground mb-3">
63
+ {frontmatter.title}
64
+ </h1>
65
+ {frontmatter?.date && (
66
+ <div className="flex flex-wrap items-center gap-3 text-muted-foreground">
67
+ <time className="font-mono text-xs bg-muted px-2 py-0.5 rounded">
68
+ {formatDate(new Date(frontmatter.date as string))}
69
+ </time>
70
+ </div>
71
+ )}
72
+ {frontmatter?.description && (
73
+ <div className="flex flex-wrap text-sm items-center gap-3 text-muted-foreground">
74
+ {frontmatter.description}
75
+ </div>
76
+ )}
77
+ </header>
78
+ )}
58
79
  <Content components={mdxComponents} />
59
80
  </article>
60
81
  )}
package/src/vite-env.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  /// <reference types="vite/client" />
2
2
 
3
3
  declare module 'virtual:veslx-config' {
4
+ type ContentView = 'posts' | 'docs' | 'all';
5
+
4
6
  interface SiteConfig {
5
7
  name: string;
6
8
  description: string;
7
9
  github: string;
10
+ defaultView: ContentView;
8
11
  }
9
12
  const config: SiteConfig;
10
13
  export default config;