veslx 0.1.27 → 0.1.28
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/serve.ts +2 -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 +39 -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 +7 -20
- 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 +18 -8
- package/plugin/src/types.ts +17 -4
- 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 +58 -115
- package/src/components/welcome.tsx +2 -2
- package/src/hooks/use-mdx-content.ts +96 -7
- package/src/index.css +12 -0
- package/src/lib/content-classification.ts +0 -24
- package/src/pages/content-router.tsx +6 -17
- package/src/pages/home.tsx +8 -50
- 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,63 @@
|
|
|
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";
|
|
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";
|
|
14
12
|
|
|
15
|
-
// Helper to
|
|
16
|
-
function
|
|
17
|
-
const match = name.match(/^(\d+)-/);
|
|
18
|
-
return match ? parseInt(match[1], 10) : null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Helper to strip numeric prefix for display (e.g., "01-getting-started" → "Getting Started")
|
|
22
|
-
function stripNumericPrefix(name: string): string {
|
|
13
|
+
// Helper to format name for display (e.g., "01-getting-started" → "Getting Started")
|
|
14
|
+
function formatName(name: string): string {
|
|
23
15
|
return name
|
|
24
16
|
.replace(/^\d+-/, '')
|
|
25
17
|
.replace(/-/g, ' ')
|
|
26
18
|
.replace(/\b\w/g, c => c.toUpperCase());
|
|
27
19
|
}
|
|
28
20
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
// Helper to get link path from post
|
|
22
|
+
function getLinkPath(post: PostEntry): string {
|
|
23
|
+
if (post.file) {
|
|
24
|
+
// Standalone MDX file
|
|
25
|
+
return `/${post.file.path}`;
|
|
26
|
+
} else if (post.slides && !post.readme) {
|
|
27
|
+
// Folder with only slides
|
|
28
|
+
return `/${post.slides.path}`;
|
|
29
|
+
} else if (post.readme) {
|
|
30
|
+
// Folder with readme
|
|
31
|
+
return `/${post.readme.path}`;
|
|
32
|
+
} else {
|
|
33
|
+
// Fallback to folder path
|
|
34
|
+
return `/${post.path}`;
|
|
35
|
+
}
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
export
|
|
38
|
+
export function PostList() {
|
|
39
|
+
const { "*": path = "." } = useParams();
|
|
40
|
+
|
|
41
|
+
const { directory, loading, error } = useDirectory(path)
|
|
42
|
+
|
|
43
|
+
if (error) {
|
|
44
|
+
return <ErrorDisplay error={error} path={path} />;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (loading) {
|
|
48
|
+
return (
|
|
49
|
+
<Loading />
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!directory) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="py-24 text-center">
|
|
56
|
+
<p className="text-muted-foreground font-mono text-sm tracking-wide">no entries</p>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
35
61
|
let posts = directoryToPostEntries(directory);
|
|
36
62
|
|
|
37
63
|
if (posts.length === 0) {
|
|
@@ -45,9 +71,6 @@ export default function PostList({ directory, view = 'all' }: PostListProps) {
|
|
|
45
71
|
// Filter out hidden and draft posts
|
|
46
72
|
posts = filterVisiblePosts(posts);
|
|
47
73
|
|
|
48
|
-
// Apply view filter
|
|
49
|
-
posts = filterByView(posts, view);
|
|
50
|
-
|
|
51
74
|
if (posts.length === 0) {
|
|
52
75
|
return (
|
|
53
76
|
<div className="py-24 text-center">
|
|
@@ -56,108 +79,28 @@ export default function PostList({ directory, view = 'all' }: PostListProps) {
|
|
|
56
79
|
);
|
|
57
80
|
}
|
|
58
81
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
const frontmatter = getFrontmatter(post);
|
|
62
|
-
return frontmatter?.date ? new Date(frontmatter.date as string) : null;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// Smart sorting: numeric prefix → date → alphabetical
|
|
66
|
-
posts = posts.sort((a, b) => {
|
|
67
|
-
const aOrder = extractOrder(a.name);
|
|
68
|
-
const bOrder = extractOrder(b.name);
|
|
69
|
-
const aDate = getPostDate(a);
|
|
70
|
-
const bDate = getPostDate(b);
|
|
71
|
-
|
|
72
|
-
// Both have numeric prefix → sort by number
|
|
73
|
-
if (aOrder !== null && bOrder !== null) {
|
|
74
|
-
return aOrder - bOrder;
|
|
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
|
-
});
|
|
82
|
+
// Alphanumeric sorting by name
|
|
83
|
+
posts = posts.sort((a, b) => a.name.localeCompare(b.name));
|
|
93
84
|
|
|
94
85
|
return (
|
|
95
|
-
<div className="space-y-1">
|
|
86
|
+
<div className="space-y-1 not-prose">
|
|
96
87
|
{posts.map((post) => {
|
|
97
88
|
const frontmatter = getFrontmatter(post);
|
|
98
|
-
|
|
99
|
-
// Title: explicit frontmatter > stripped numeric prefix > raw name
|
|
100
|
-
const title = (frontmatter?.title as string) || stripNumericPrefix(post.name);
|
|
89
|
+
const title = (frontmatter?.title as string) || formatName(post.name);
|
|
101
90
|
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
|
-
|
|
91
|
+
const date = frontmatter?.date ? new Date(frontmatter.date as string) : undefined;
|
|
92
|
+
const linkPath = getLinkPath(post);
|
|
120
93
|
const isSlides = linkPath.endsWith('SLIDES.mdx') || linkPath.endsWith('.slides.mdx');
|
|
121
94
|
|
|
122
95
|
return (
|
|
123
|
-
<
|
|
96
|
+
<PostListItem
|
|
124
97
|
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>
|
|
98
|
+
title={title}
|
|
99
|
+
description={description}
|
|
100
|
+
date={date}
|
|
101
|
+
linkPath={linkPath}
|
|
102
|
+
isSlides={isSlides}
|
|
103
|
+
/>
|
|
161
104
|
);
|
|
162
105
|
})}
|
|
163
106
|
</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
|
@@ -292,6 +292,18 @@
|
|
|
292
292
|
animation: shimmer 1.5s ease-in-out infinite;
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
/* Scroll snap for slides - enabled by default, configurable via veslx.config */
|
|
296
|
+
.slides-scroll-snap {
|
|
297
|
+
scroll-snap-type: y mandatory;
|
|
298
|
+
overflow-y: auto;
|
|
299
|
+
height: 100vh;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.slides-scroll-snap .slide-section {
|
|
303
|
+
scroll-snap-align: start;
|
|
304
|
+
scroll-snap-stop: always;
|
|
305
|
+
}
|
|
306
|
+
|
|
295
307
|
/* Print styles - slides landscape, posts portrait */
|
|
296
308
|
@media print {
|
|
297
309
|
/* 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,56 +1,16 @@
|
|
|
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;
|
|
8
|
+
const config = veslxConfig.site;
|
|
33
9
|
|
|
34
|
-
const isRoot = path === "." || path === ""
|
|
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
|
-
|
|
41
|
-
if (error) {
|
|
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>
|
|
@@ -69,11 +29,9 @@ export function Home({ view }: HomeProps) {
|
|
|
69
29
|
)}
|
|
70
30
|
|
|
71
31
|
<div className="flex flex-col gap-2">
|
|
72
|
-
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
</div>
|
|
76
|
-
)}
|
|
32
|
+
<div className="animate-fade-in">
|
|
33
|
+
<PostList />
|
|
34
|
+
</div>
|
|
77
35
|
</div>
|
|
78
36
|
</main>
|
|
79
37
|
</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
|
+
}
|
package/src/pages/post.tsx
CHANGED
|
@@ -2,7 +2,6 @@ import { useParams } from "react-router-dom";
|
|
|
2
2
|
import { findSlides, isSimulationRunning, useDirectory } from "../../plugin/src/client";
|
|
3
3
|
import Loading from "@/components/loading";
|
|
4
4
|
import { FileEntry } from "plugin/src/lib";
|
|
5
|
-
import { RunningBar } from "@/components/running-bar";
|
|
6
5
|
import { Header } from "@/components/header";
|
|
7
6
|
import { useMDXContent } from "@/hooks/use-mdx-content";
|
|
8
7
|
import { mdxComponents } from "@/components/mdx-components";
|
|
@@ -41,7 +40,6 @@ export function Post() {
|
|
|
41
40
|
return (
|
|
42
41
|
<div className="flex min-h-screen flex-col bg-background noise-overlay">
|
|
43
42
|
<title>{frontmatter?.title}</title>
|
|
44
|
-
<RunningBar />
|
|
45
43
|
<Header />
|
|
46
44
|
<main className="flex-1 w-full overflow-x-clip">
|
|
47
45
|
{isRunning && (
|
|
@@ -56,7 +54,7 @@ export function Post() {
|
|
|
56
54
|
|
|
57
55
|
{Content && (
|
|
58
56
|
<FrontmatterProvider frontmatter={frontmatter}>
|
|
59
|
-
<article className="
|
|
57
|
+
<article className="mt-12 mb-64 mx-auto px-[var(--page-padding)] max-w-[var(--content-width)] animate-fade-in">
|
|
60
58
|
<Content components={mdxComponents} />
|
|
61
59
|
</article>
|
|
62
60
|
</FrontmatterProvider>
|
package/src/pages/slides.tsx
CHANGED
|
@@ -2,11 +2,12 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import { FULLSCREEN_DATA_ATTR } from "@/lib/constants";
|
|
3
3
|
import { useParams, useSearchParams } from "react-router-dom"
|
|
4
4
|
import Loading from "@/components/loading";
|
|
5
|
-
import { RunningBar } from "@/components/running-bar";
|
|
6
5
|
import { Header } from "@/components/header";
|
|
7
6
|
import { useMDXSlides } from "@/hooks/use-mdx-content";
|
|
8
7
|
import { slidesMdxComponents } from "@/components/slides-renderer";
|
|
9
8
|
import { FrontmatterProvider } from "@/lib/frontmatter-context";
|
|
9
|
+
import veslxConfig from "virtual:veslx-config";
|
|
10
|
+
import { cn } from "@/lib/utils";
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
export function SlidesPage() {
|
|
@@ -121,10 +122,11 @@ export function SlidesPage() {
|
|
|
121
122
|
);
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
const scrollSnap = veslxConfig.slides.scrollSnap;
|
|
126
|
+
|
|
124
127
|
return (
|
|
125
|
-
<main className="slides-container">
|
|
128
|
+
<main className={cn("slides-container", scrollSnap && "slides-scroll-snap")}>
|
|
126
129
|
<title>{frontmatter?.title}</title>
|
|
127
|
-
<RunningBar />
|
|
128
130
|
<Header
|
|
129
131
|
slideControls={{
|
|
130
132
|
current: currentSlide,
|