veslx 0.1.27 → 0.1.29
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 +2 -1
- package/bin/lib/export.ts +214 -0
- package/bin/lib/serve.ts +2 -1
- package/dist/client/components/front-matter.js +46 -1
- package/dist/client/components/front-matter.js.map +1 -1
- package/dist/client/components/header.js +2 -24
- package/dist/client/components/header.js.map +1 -1
- package/dist/client/components/mdx-components.js +2 -0
- package/dist/client/components/mdx-components.js.map +1 -1
- package/dist/client/components/post-list-item.js +43 -0
- package/dist/client/components/post-list-item.js.map +1 -0
- package/dist/client/components/post-list.js +54 -79
- package/dist/client/components/post-list.js.map +1 -1
- package/dist/client/hooks/use-mdx-content.js +64 -4
- package/dist/client/hooks/use-mdx-content.js.map +1 -1
- package/dist/client/lib/content-classification.js +1 -22
- package/dist/client/lib/content-classification.js.map +1 -1
- package/dist/client/pages/content-router.js +2 -10
- package/dist/client/pages/content-router.js.map +1 -1
- package/dist/client/pages/home.js +20 -23
- package/dist/client/pages/home.js.map +1 -1
- package/dist/client/pages/index-post.js +34 -0
- package/dist/client/pages/index-post.js.map +1 -0
- package/dist/client/pages/post.js +1 -3
- package/dist/client/pages/post.js.map +1 -1
- package/dist/client/pages/slides.js +4 -3
- package/dist/client/pages/slides.js.map +1 -1
- package/package.json +1 -1
- package/plugin/src/plugin.ts +127 -30
- package/plugin/src/types.ts +34 -4
- package/src/components/front-matter.tsx +60 -3
- package/src/components/header.tsx +2 -20
- package/src/components/mdx-components.tsx +3 -1
- package/src/components/post-list-item.tsx +54 -0
- package/src/components/post-list.tsx +74 -116
- package/src/components/welcome.tsx +2 -2
- package/src/hooks/use-mdx-content.ts +96 -7
- package/src/index.css +17 -0
- package/src/lib/content-classification.ts +0 -24
- package/src/pages/content-router.tsx +6 -17
- package/src/pages/home.tsx +26 -58
- package/src/pages/index-post.tsx +59 -0
- package/src/pages/post.tsx +1 -3
- package/src/pages/slides.tsx +5 -3
- package/src/vite-env.d.ts +11 -1
- package/vite.config.ts +4 -3
- package/dist/client/components/running-bar.js +0 -15
- package/dist/client/components/running-bar.js.map +0 -1
- package/src/components/content-tabs.tsx +0 -64
- package/src/components/running-bar.tsx +0 -21
|
@@ -1,37 +1,64 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { cn } from "@/lib/utils";
|
|
3
|
-
import type { DirectoryEntry } from "../../plugin/src/lib";
|
|
4
|
-
import { formatDate } from "@/lib/format-date";
|
|
5
|
-
import { ArrowRight, Presentation } from "lucide-react";
|
|
1
|
+
import { useParams } from "react-router-dom";
|
|
6
2
|
import {
|
|
7
|
-
type ContentView,
|
|
8
3
|
type PostEntry,
|
|
9
4
|
directoryToPostEntries,
|
|
10
5
|
filterVisiblePosts,
|
|
11
|
-
filterByView,
|
|
12
6
|
getFrontmatter,
|
|
13
7
|
} from "@/lib/content-classification";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
function stripNumericPrefix(name: string): string {
|
|
8
|
+
import { useDirectory } from "../../plugin/src/client";
|
|
9
|
+
import { ErrorDisplay } from "./page-error";
|
|
10
|
+
import Loading from "./loading";
|
|
11
|
+
import { PostListItem } from "./post-list-item";
|
|
12
|
+
import veslxConfig from "virtual:veslx-config";
|
|
13
|
+
|
|
14
|
+
// Helper to format name for display (e.g., "01-getting-started" → "Getting Started")
|
|
15
|
+
function formatName(name: string): string {
|
|
23
16
|
return name
|
|
24
17
|
.replace(/^\d+-/, '')
|
|
25
18
|
.replace(/-/g, ' ')
|
|
26
19
|
.replace(/\b\w/g, c => c.toUpperCase());
|
|
27
20
|
}
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
// Helper to get link path from post
|
|
23
|
+
function getLinkPath(post: PostEntry): string {
|
|
24
|
+
if (post.file) {
|
|
25
|
+
// Standalone MDX file
|
|
26
|
+
return `/${post.file.path}`;
|
|
27
|
+
} else if (post.slides && !post.readme) {
|
|
28
|
+
// Folder with only slides
|
|
29
|
+
return `/${post.slides.path}`;
|
|
30
|
+
} else if (post.readme) {
|
|
31
|
+
// Folder with readme
|
|
32
|
+
return `/${post.readme.path}`;
|
|
33
|
+
} else {
|
|
34
|
+
// Fallback to folder path
|
|
35
|
+
return `/${post.path}`;
|
|
36
|
+
}
|
|
32
37
|
}
|
|
33
38
|
|
|
34
|
-
export
|
|
39
|
+
export function PostList() {
|
|
40
|
+
const { "*": path = "." } = useParams();
|
|
41
|
+
|
|
42
|
+
const { directory, loading, error } = useDirectory(path)
|
|
43
|
+
|
|
44
|
+
if (error) {
|
|
45
|
+
return <ErrorDisplay error={error} path={path} />;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (loading) {
|
|
49
|
+
return (
|
|
50
|
+
<Loading />
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!directory) {
|
|
55
|
+
return (
|
|
56
|
+
<div className="py-24 text-center">
|
|
57
|
+
<p className="text-muted-foreground font-mono text-sm tracking-wide">no entries</p>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
let posts = directoryToPostEntries(directory);
|
|
36
63
|
|
|
37
64
|
if (posts.length === 0) {
|
|
@@ -45,9 +72,6 @@ export default function PostList({ directory, view = 'all' }: PostListProps) {
|
|
|
45
72
|
// Filter out hidden and draft posts
|
|
46
73
|
posts = filterVisiblePosts(posts);
|
|
47
74
|
|
|
48
|
-
// Apply view filter
|
|
49
|
-
posts = filterByView(posts, view);
|
|
50
|
-
|
|
51
75
|
if (posts.length === 0) {
|
|
52
76
|
return (
|
|
53
77
|
<div className="py-24 text-center">
|
|
@@ -56,108 +80,42 @@ export default function PostList({ directory, view = 'all' }: PostListProps) {
|
|
|
56
80
|
);
|
|
57
81
|
}
|
|
58
82
|
|
|
59
|
-
//
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
// One has prefix, one doesn't → prefixed comes first
|
|
77
|
-
if (aOrder !== null) return -1;
|
|
78
|
-
if (bOrder !== null) return 1;
|
|
79
|
-
|
|
80
|
-
// Both have dates → sort by date (newest first)
|
|
81
|
-
if (aDate && bDate) {
|
|
82
|
-
return bDate.getTime() - aDate.getTime();
|
|
83
|
-
}
|
|
84
|
-
// One has date → dated comes first
|
|
85
|
-
if (aDate) return -1;
|
|
86
|
-
if (bDate) return 1;
|
|
87
|
-
|
|
88
|
-
// Neither → alphabetical by title
|
|
89
|
-
const aTitle = (getFrontmatter(a)?.title as string) || a.name;
|
|
90
|
-
const bTitle = (getFrontmatter(b)?.title as string) || b.name;
|
|
91
|
-
return aTitle.localeCompare(bTitle);
|
|
92
|
-
});
|
|
83
|
+
// Sort based on config
|
|
84
|
+
const sortMode = veslxConfig.posts?.sort ?? 'alpha';
|
|
85
|
+
if (sortMode === 'date') {
|
|
86
|
+
// Sort by date descending (newest first), posts without dates go to the end
|
|
87
|
+
posts = posts.sort((a, b) => {
|
|
88
|
+
const dateA = getFrontmatter(a)?.date;
|
|
89
|
+
const dateB = getFrontmatter(b)?.date;
|
|
90
|
+
if (!dateA && !dateB) return a.name.localeCompare(b.name);
|
|
91
|
+
if (!dateA) return 1;
|
|
92
|
+
if (!dateB) return -1;
|
|
93
|
+
return new Date(dateB as string).getTime() - new Date(dateA as string).getTime();
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
// Alphanumeric sorting by name
|
|
97
|
+
posts = posts.sort((a, b) => a.name.localeCompare(b.name));
|
|
98
|
+
}
|
|
93
99
|
|
|
94
100
|
return (
|
|
95
|
-
<div className="space-y-1">
|
|
101
|
+
<div className="space-y-1 not-prose">
|
|
96
102
|
{posts.map((post) => {
|
|
97
103
|
const frontmatter = getFrontmatter(post);
|
|
98
|
-
|
|
99
|
-
// Title: explicit frontmatter > stripped numeric prefix > raw name
|
|
100
|
-
const title = (frontmatter?.title as string) || stripNumericPrefix(post.name);
|
|
104
|
+
const title = (frontmatter?.title as string) || formatName(post.name);
|
|
101
105
|
const description = frontmatter?.description as string | undefined;
|
|
102
|
-
const date = frontmatter?.date ? new Date(frontmatter.date as string) :
|
|
103
|
-
|
|
104
|
-
// Determine the link path
|
|
105
|
-
let linkPath: string;
|
|
106
|
-
if (post.file) {
|
|
107
|
-
// Standalone MDX file
|
|
108
|
-
linkPath = `/${post.file.path}`;
|
|
109
|
-
} else if (post.slides && !post.readme) {
|
|
110
|
-
// Folder with only slides
|
|
111
|
-
linkPath = `/${post.slides.path}`;
|
|
112
|
-
} else if (post.readme) {
|
|
113
|
-
// Folder with readme
|
|
114
|
-
linkPath = `/${post.readme.path}`;
|
|
115
|
-
} else {
|
|
116
|
-
// Fallback to folder path
|
|
117
|
-
linkPath = `/${post.path}`;
|
|
118
|
-
}
|
|
119
|
-
|
|
106
|
+
const date = frontmatter?.date ? new Date(frontmatter.date as string) : undefined;
|
|
107
|
+
const linkPath = getLinkPath(post);
|
|
120
108
|
const isSlides = linkPath.endsWith('SLIDES.mdx') || linkPath.endsWith('.slides.mdx');
|
|
121
109
|
|
|
122
110
|
return (
|
|
123
|
-
<
|
|
111
|
+
<PostListItem
|
|
124
112
|
key={post.path}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
<article className="flex items-center gap-4">
|
|
132
|
-
{/* Main content */}
|
|
133
|
-
<div className="flex-1 min-w-0">
|
|
134
|
-
<h3 className={cn(
|
|
135
|
-
"text-sm font-medium text-foreground",
|
|
136
|
-
"group-hover:underline",
|
|
137
|
-
"flex items-center gap-2"
|
|
138
|
-
)}>
|
|
139
|
-
<span>{title}</span>
|
|
140
|
-
<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" />
|
|
141
|
-
</h3>
|
|
142
|
-
|
|
143
|
-
{description && (
|
|
144
|
-
<p className="text-sm text-muted-foreground line-clamp-1 mt-0.5">
|
|
145
|
-
{description}
|
|
146
|
-
</p>
|
|
147
|
-
)}
|
|
148
|
-
</div>
|
|
149
|
-
|
|
150
|
-
{isSlides && (
|
|
151
|
-
<Presentation className="h-3 w-3 text-muted-foreground" />
|
|
152
|
-
)}
|
|
153
|
-
<time
|
|
154
|
-
dateTime={date?.toISOString()}
|
|
155
|
-
className="font-mono text-xs text-muted-foreground tabular-nums w-20 flex-shrink-0"
|
|
156
|
-
>
|
|
157
|
-
{date && formatDate(date)}
|
|
158
|
-
</time>
|
|
159
|
-
</article>
|
|
160
|
-
</Link>
|
|
113
|
+
title={title}
|
|
114
|
+
description={description}
|
|
115
|
+
date={date}
|
|
116
|
+
linkPath={linkPath}
|
|
117
|
+
isSlides={isSlides}
|
|
118
|
+
/>
|
|
161
119
|
);
|
|
162
120
|
})}
|
|
163
121
|
</div>
|
|
@@ -17,9 +17,9 @@ type ModuleLoader = () => Promise<MDXModule>
|
|
|
17
17
|
type ModuleMap = Record<string, ModuleLoader>
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Find MDX module by path. Supports:
|
|
21
|
-
* - Full path: "docs/intro.mdx" -> matches exactly
|
|
22
|
-
* - Folder path: "docs" -> matches "docs/index.mdx", "docs/README.mdx", or "docs.mdx"
|
|
20
|
+
* Find MDX/MD module by path. Supports:
|
|
21
|
+
* - Full path: "docs/intro.mdx" or "docs/intro.md" -> matches exactly
|
|
22
|
+
* - Folder path: "docs" -> matches "docs/index.mdx", "docs/README.mdx", or "docs.mdx" (and .md variants)
|
|
23
23
|
*/
|
|
24
24
|
function findMdxModule(modules: ModuleMap, path: string): ModuleLoader | null {
|
|
25
25
|
const keys = Object.keys(modules)
|
|
@@ -27,8 +27,8 @@ function findMdxModule(modules: ModuleMap, path: string): ModuleLoader | null {
|
|
|
27
27
|
// Normalize path - remove leading slash if present
|
|
28
28
|
const normalizedPath = path.replace(/^\//, '')
|
|
29
29
|
|
|
30
|
-
// If path already ends with .mdx, match exactly
|
|
31
|
-
if (normalizedPath.endsWith('.mdx')) {
|
|
30
|
+
// If path already ends with .mdx or .md, match exactly
|
|
31
|
+
if (normalizedPath.endsWith('.mdx') || normalizedPath.endsWith('.md')) {
|
|
32
32
|
// Try multiple matching strategies for different Vite glob formats
|
|
33
33
|
const matchingKey = keys.find(key => {
|
|
34
34
|
// Strategy 1: Key ends with /path (e.g., @content/docs/foo.mdx matches docs/foo.mdx)
|
|
@@ -50,10 +50,14 @@ function findMdxModule(modules: ModuleMap, path: string): ModuleLoader | null {
|
|
|
50
50
|
// 1. folder/index.mdx (modern convention)
|
|
51
51
|
// 2. folder/README.mdx (current convention)
|
|
52
52
|
// 3. folder.mdx (file alongside folders)
|
|
53
|
+
// Also try .md variants
|
|
53
54
|
const candidates = [
|
|
54
55
|
`${normalizedPath}/index.mdx`,
|
|
56
|
+
`${normalizedPath}/index.md`,
|
|
55
57
|
`${normalizedPath}/README.mdx`,
|
|
58
|
+
`${normalizedPath}/README.md`,
|
|
56
59
|
`${normalizedPath}.mdx`,
|
|
60
|
+
`${normalizedPath}.md`,
|
|
57
61
|
]
|
|
58
62
|
|
|
59
63
|
for (const candidate of candidates) {
|
|
@@ -126,8 +130,8 @@ function findSlidesModule(modules: ModuleMap, path: string): ModuleLoader | null
|
|
|
126
130
|
// Normalize path - remove leading slash if present
|
|
127
131
|
const normalizedPath = path.replace(/^\//, '')
|
|
128
132
|
|
|
129
|
-
// If path already ends with .mdx, match exactly
|
|
130
|
-
if (normalizedPath.endsWith('.mdx')) {
|
|
133
|
+
// If path already ends with .mdx or .md, match exactly
|
|
134
|
+
if (normalizedPath.endsWith('.mdx') || normalizedPath.endsWith('.md')) {
|
|
131
135
|
// Try multiple matching strategies for different Vite glob formats
|
|
132
136
|
const matchingKey = keys.find(key => {
|
|
133
137
|
// Strategy 1: Key ends with /path (e.g., @content/docs/foo.slides.mdx matches docs/foo.slides.mdx)
|
|
@@ -148,9 +152,12 @@ function findSlidesModule(modules: ModuleMap, path: string): ModuleLoader | null
|
|
|
148
152
|
// Otherwise, try folder conventions:
|
|
149
153
|
// 1. folder/SLIDES.mdx (current convention)
|
|
150
154
|
// 2. folder/index.slides.mdx (alternative)
|
|
155
|
+
// Also try .md variants
|
|
151
156
|
const candidates = [
|
|
152
157
|
`${normalizedPath}/SLIDES.mdx`,
|
|
158
|
+
`${normalizedPath}/SLIDES.md`,
|
|
153
159
|
`${normalizedPath}/index.slides.mdx`,
|
|
160
|
+
`${normalizedPath}/index.slides.md`,
|
|
154
161
|
]
|
|
155
162
|
|
|
156
163
|
for (const candidate of candidates) {
|
|
@@ -213,3 +220,85 @@ export function useMDXSlides(path: string) {
|
|
|
213
220
|
|
|
214
221
|
return { Content, frontmatter, slideCount, loading, error }
|
|
215
222
|
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Find only index.mdx or index.md in a directory (not README or other conventions)
|
|
226
|
+
*/
|
|
227
|
+
function findIndexModule(modules: ModuleMap, path: string): ModuleLoader | null {
|
|
228
|
+
const keys = Object.keys(modules)
|
|
229
|
+
|
|
230
|
+
// Normalize path - remove leading slash, handle root
|
|
231
|
+
const normalizedPath = path.replace(/^\//, '') || '.'
|
|
232
|
+
|
|
233
|
+
// Only look for index.mdx or index.md
|
|
234
|
+
const candidates = normalizedPath === '.'
|
|
235
|
+
? ['index.mdx', 'index.md']
|
|
236
|
+
: [`${normalizedPath}/index.mdx`, `${normalizedPath}/index.md`]
|
|
237
|
+
|
|
238
|
+
for (const candidate of candidates) {
|
|
239
|
+
const matchingKey = keys.find(key => {
|
|
240
|
+
if (key.endsWith(`/${candidate}`)) return true
|
|
241
|
+
if (key === `@content/${candidate}`) return true
|
|
242
|
+
if (key === candidate) return true
|
|
243
|
+
return false
|
|
244
|
+
})
|
|
245
|
+
if (matchingKey) {
|
|
246
|
+
return modules[matchingKey]
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return null
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Hook for loading index.mdx/index.md content only.
|
|
255
|
+
* Returns notFound: true if no index file exists (instead of throwing an error).
|
|
256
|
+
*/
|
|
257
|
+
export function useIndexContent(path: string) {
|
|
258
|
+
const [Content, setContent] = useState<MDXModule['default'] | null>(null)
|
|
259
|
+
const [frontmatter, setFrontmatter] = useState<MDXModule['frontmatter']>(undefined)
|
|
260
|
+
const [loading, setLoading] = useState(true)
|
|
261
|
+
const [notFound, setNotFound] = useState(false)
|
|
262
|
+
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
let cancelled = false
|
|
265
|
+
setLoading(true)
|
|
266
|
+
setNotFound(false)
|
|
267
|
+
|
|
268
|
+
import('virtual:content-modules')
|
|
269
|
+
.then(({ modules }) => {
|
|
270
|
+
const loader = findIndexModule(modules as ModuleMap, path)
|
|
271
|
+
|
|
272
|
+
if (!loader) {
|
|
273
|
+
// No index file - this is not an error, just means fallback to directory listing
|
|
274
|
+
if (!cancelled) {
|
|
275
|
+
setNotFound(true)
|
|
276
|
+
setLoading(false)
|
|
277
|
+
}
|
|
278
|
+
return null
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return loader()
|
|
282
|
+
})
|
|
283
|
+
.then((mod) => {
|
|
284
|
+
if (mod && !cancelled) {
|
|
285
|
+
setContent(() => mod.default)
|
|
286
|
+
setFrontmatter(mod.frontmatter)
|
|
287
|
+
setLoading(false)
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
.catch(() => {
|
|
291
|
+
// Treat load errors as not found
|
|
292
|
+
if (!cancelled) {
|
|
293
|
+
setNotFound(true)
|
|
294
|
+
setLoading(false)
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
return () => {
|
|
299
|
+
cancelled = true
|
|
300
|
+
}
|
|
301
|
+
}, [path])
|
|
302
|
+
|
|
303
|
+
return { Content, frontmatter, loading, notFound }
|
|
304
|
+
}
|
package/src/index.css
CHANGED
|
@@ -209,6 +209,11 @@
|
|
|
209
209
|
letter-spacing: 0;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/* Code inside pre blocks should inherit background from pre */
|
|
213
|
+
pre code {
|
|
214
|
+
background: transparent;
|
|
215
|
+
}
|
|
216
|
+
|
|
212
217
|
/* Scrollbar styling */
|
|
213
218
|
::-webkit-scrollbar {
|
|
214
219
|
width: 8px;
|
|
@@ -292,6 +297,18 @@
|
|
|
292
297
|
animation: shimmer 1.5s ease-in-out infinite;
|
|
293
298
|
}
|
|
294
299
|
|
|
300
|
+
/* Scroll snap for slides - enabled by default, configurable via veslx.config */
|
|
301
|
+
.slides-scroll-snap {
|
|
302
|
+
scroll-snap-type: y mandatory;
|
|
303
|
+
overflow-y: auto;
|
|
304
|
+
height: 100vh;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.slides-scroll-snap .slide-section {
|
|
308
|
+
scroll-snap-align: start;
|
|
309
|
+
scroll-snap-stop: always;
|
|
310
|
+
}
|
|
311
|
+
|
|
295
312
|
/* Print styles - slides landscape, posts portrait */
|
|
296
313
|
@media print {
|
|
297
314
|
/* Default page is portrait for posts */
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ContentView } from "../../plugin/src/types";
|
|
2
1
|
import type { DirectoryEntry, FileEntry } from "../../plugin/src/lib";
|
|
3
2
|
import { findReadme, findSlides, findMdxFiles, findStandaloneSlides } from "../../plugin/src/client";
|
|
4
3
|
|
|
@@ -15,27 +14,6 @@ export function getFrontmatter(post: PostEntry) {
|
|
|
15
14
|
return post.readme?.frontmatter || post.file?.frontmatter || post.slides?.frontmatter;
|
|
16
15
|
}
|
|
17
16
|
|
|
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
17
|
export function directoryToPostEntries(directory: DirectoryEntry): PostEntry[] {
|
|
40
18
|
const folders = directory.children.filter((c): c is DirectoryEntry => c.type === "directory");
|
|
41
19
|
const standaloneFiles = findMdxFiles(directory);
|
|
@@ -80,5 +58,3 @@ export function filterVisiblePosts(posts: PostEntry[]): PostEntry[] {
|
|
|
80
58
|
return frontmatter?.visibility !== "hidden" && frontmatter?.draft !== true;
|
|
81
59
|
});
|
|
82
60
|
}
|
|
83
|
-
|
|
84
|
-
export type { ContentView };
|
|
@@ -2,29 +2,18 @@ import { useParams } from "react-router-dom"
|
|
|
2
2
|
import { Home } from "./home"
|
|
3
3
|
import { Post } from "./post"
|
|
4
4
|
import { SlidesPage } from "./slides"
|
|
5
|
+
import { IndexPost } from "./index-post"
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Routes to the appropriate page based on the URL path:
|
|
8
|
-
* - /posts → Home with posts view
|
|
9
|
-
* - /docs → Home with docs view
|
|
10
9
|
* - *.slides.mdx or *SLIDES.mdx → SlidesPage
|
|
11
|
-
* - *.mdx → Post
|
|
10
|
+
* - *.mdx or *.md → Post
|
|
11
|
+
* - directory with index.mdx/index.md → IndexPost (renders index file)
|
|
12
12
|
* - everything else → Home (directory listing)
|
|
13
13
|
*/
|
|
14
14
|
export function ContentRouter() {
|
|
15
15
|
const { "*": path = "" } = useParams()
|
|
16
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
|
-
|
|
28
17
|
// Check if this is a slides file
|
|
29
18
|
const filename = path.split('/').pop()?.toLowerCase() || ''
|
|
30
19
|
const isSlides =
|
|
@@ -36,11 +25,11 @@ export function ContentRouter() {
|
|
|
36
25
|
return <SlidesPage />
|
|
37
26
|
}
|
|
38
27
|
|
|
39
|
-
// Check if this is any MDX file
|
|
28
|
+
// Check if this is any MDX/MD file
|
|
40
29
|
if (path.endsWith('.mdx') || path.endsWith('.md')) {
|
|
41
30
|
return <Post />
|
|
42
31
|
}
|
|
43
32
|
|
|
44
|
-
//
|
|
45
|
-
return <Home />
|
|
33
|
+
// For directories, try to render index.mdx/index.md, fallback to Home
|
|
34
|
+
return <IndexPost fallback={<Home />} />
|
|
46
35
|
}
|
package/src/pages/home.tsx
CHANGED
|
@@ -1,79 +1,47 @@
|
|
|
1
1
|
import { useParams } from "react-router-dom"
|
|
2
|
-
import {
|
|
3
|
-
import Loading from "@/components/loading";
|
|
4
|
-
import PostList from "@/components/post-list";
|
|
5
|
-
import { ErrorDisplay } from "@/components/page-error";
|
|
6
|
-
import { RunningBar } from "@/components/running-bar";
|
|
2
|
+
import { PostList } from "@/components/post-list";
|
|
7
3
|
import { Header } from "@/components/header";
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
type ContentView,
|
|
11
|
-
directoryToPostEntries,
|
|
12
|
-
filterVisiblePosts,
|
|
13
|
-
getViewCounts,
|
|
14
|
-
} from "@/lib/content-classification";
|
|
15
|
-
import siteConfig from "virtual:veslx-config";
|
|
4
|
+
import veslxConfig from "virtual:veslx-config";
|
|
16
5
|
|
|
17
|
-
|
|
18
|
-
view?: ContentView;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function Home({ view }: HomeProps) {
|
|
6
|
+
export function Home() {
|
|
22
7
|
const { "*": path = "." } = useParams();
|
|
23
|
-
const config =
|
|
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 };
|
|
8
|
+
const config = veslxConfig.site;
|
|
40
9
|
|
|
41
|
-
|
|
42
|
-
return <ErrorDisplay error={error} path={path} />;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (loading) {
|
|
46
|
-
return (
|
|
47
|
-
<Loading />
|
|
48
|
-
)
|
|
49
|
-
}
|
|
10
|
+
const isRoot = path === "." || path === "";
|
|
50
11
|
|
|
51
12
|
return (
|
|
52
13
|
<div className="flex min-h-screen flex-col bg-background noise-overlay">
|
|
53
|
-
<RunningBar />
|
|
54
14
|
<Header />
|
|
55
15
|
<main className="flex-1 mx-auto w-full max-w-[var(--content-width)] px-[var(--page-padding)]">
|
|
56
16
|
<title>{isRoot ? config.name : `${config.name} - ${path}`}</title>
|
|
57
17
|
<main className="flex flex-col gap-8 mb-32 mt-12">
|
|
58
18
|
{isRoot && (
|
|
59
|
-
<div className="animate-fade-in">
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
19
|
+
<div className="animate-fade-in flex items-start justify-between gap-4">
|
|
20
|
+
<div>
|
|
21
|
+
<h1 className="text-2xl md:text-3xl font-semibold tracking-tight text-foreground">
|
|
22
|
+
{config.name}
|
|
23
|
+
</h1>
|
|
24
|
+
{config.description && (
|
|
25
|
+
<p className="mt-2 text-muted-foreground">
|
|
26
|
+
{config.description}
|
|
27
|
+
</p>
|
|
28
|
+
)}
|
|
29
|
+
</div>
|
|
30
|
+
{config.llmsTxt && (
|
|
31
|
+
<a
|
|
32
|
+
href="/llms-full.txt"
|
|
33
|
+
className="font-mono text-xs text-muted-foreground/70 hover:text-foreground underline underline-offset-2 transition-colors shrink-0"
|
|
34
|
+
>
|
|
35
|
+
llms.txt
|
|
36
|
+
</a>
|
|
67
37
|
)}
|
|
68
38
|
</div>
|
|
69
39
|
)}
|
|
70
40
|
|
|
71
41
|
<div className="flex flex-col gap-2">
|
|
72
|
-
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
</div>
|
|
76
|
-
)}
|
|
42
|
+
<div className="animate-fade-in">
|
|
43
|
+
<PostList />
|
|
44
|
+
</div>
|
|
77
45
|
</div>
|
|
78
46
|
</main>
|
|
79
47
|
</main>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { useParams } from "react-router-dom";
|
|
3
|
+
import { isSimulationRunning } from "../../plugin/src/client";
|
|
4
|
+
import Loading from "@/components/loading";
|
|
5
|
+
import { Header } from "@/components/header";
|
|
6
|
+
import { useIndexContent } from "@/hooks/use-mdx-content";
|
|
7
|
+
import { mdxComponents } from "@/components/mdx-components";
|
|
8
|
+
import { FrontmatterProvider } from "@/lib/frontmatter-context";
|
|
9
|
+
|
|
10
|
+
interface IndexPostProps {
|
|
11
|
+
fallback: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Attempts to render an index.mdx or index.md file for a directory.
|
|
16
|
+
* Falls back to the provided component if no index file exists.
|
|
17
|
+
*/
|
|
18
|
+
export function IndexPost({ fallback }: IndexPostProps) {
|
|
19
|
+
const { "*": rawPath = "." } = useParams();
|
|
20
|
+
|
|
21
|
+
// Normalize path for index lookup
|
|
22
|
+
const dirPath = rawPath || ".";
|
|
23
|
+
|
|
24
|
+
const { Content, frontmatter, loading, notFound } = useIndexContent(dirPath);
|
|
25
|
+
const isRunning = isSimulationRunning();
|
|
26
|
+
|
|
27
|
+
if (loading) return <Loading />
|
|
28
|
+
|
|
29
|
+
// No index file found - render fallback (usually Home)
|
|
30
|
+
if (notFound) {
|
|
31
|
+
return <>{fallback}</>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex min-h-screen flex-col bg-background noise-overlay">
|
|
36
|
+
<title>{frontmatter?.title}</title>
|
|
37
|
+
<Header />
|
|
38
|
+
<main className="flex-1 w-full overflow-x-clip">
|
|
39
|
+
{isRunning && (
|
|
40
|
+
<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">
|
|
41
|
+
<span className="inline-flex items-center gap-3">
|
|
42
|
+
<span className="h-1.5 w-1.5 rounded-full bg-current animate-pulse" />
|
|
43
|
+
<span className="uppercase tracking-widest">simulation running</span>
|
|
44
|
+
<span className="text-primary-foreground/60">Page will auto-refresh on completion</span>
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
{Content && (
|
|
50
|
+
<FrontmatterProvider frontmatter={frontmatter}>
|
|
51
|
+
<article className="my-12 mx-auto px-[var(--page-padding)] max-w-[var(--content-width)] animate-fade-in">
|
|
52
|
+
<Content components={mdxComponents} />
|
|
53
|
+
</article>
|
|
54
|
+
</FrontmatterProvider>
|
|
55
|
+
)}
|
|
56
|
+
</main>
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|