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.
Files changed (46) hide show
  1. package/bin/lib/build.ts +2 -1
  2. package/bin/lib/serve.ts +2 -1
  3. package/dist/client/components/header.js +2 -24
  4. package/dist/client/components/header.js.map +1 -1
  5. package/dist/client/components/mdx-components.js +2 -0
  6. package/dist/client/components/mdx-components.js.map +1 -1
  7. package/dist/client/components/post-list-item.js +43 -0
  8. package/dist/client/components/post-list-item.js.map +1 -0
  9. package/dist/client/components/post-list.js +39 -79
  10. package/dist/client/components/post-list.js.map +1 -1
  11. package/dist/client/hooks/use-mdx-content.js +64 -4
  12. package/dist/client/hooks/use-mdx-content.js.map +1 -1
  13. package/dist/client/lib/content-classification.js +1 -22
  14. package/dist/client/lib/content-classification.js.map +1 -1
  15. package/dist/client/pages/content-router.js +2 -10
  16. package/dist/client/pages/content-router.js.map +1 -1
  17. package/dist/client/pages/home.js +7 -20
  18. package/dist/client/pages/home.js.map +1 -1
  19. package/dist/client/pages/index-post.js +34 -0
  20. package/dist/client/pages/index-post.js.map +1 -0
  21. package/dist/client/pages/post.js +1 -3
  22. package/dist/client/pages/post.js.map +1 -1
  23. package/dist/client/pages/slides.js +4 -3
  24. package/dist/client/pages/slides.js.map +1 -1
  25. package/package.json +1 -1
  26. package/plugin/src/plugin.ts +18 -8
  27. package/plugin/src/types.ts +17 -4
  28. package/src/components/header.tsx +2 -20
  29. package/src/components/mdx-components.tsx +3 -1
  30. package/src/components/post-list-item.tsx +54 -0
  31. package/src/components/post-list.tsx +58 -115
  32. package/src/components/welcome.tsx +2 -2
  33. package/src/hooks/use-mdx-content.ts +96 -7
  34. package/src/index.css +12 -0
  35. package/src/lib/content-classification.ts +0 -24
  36. package/src/pages/content-router.tsx +6 -17
  37. package/src/pages/home.tsx +8 -50
  38. package/src/pages/index-post.tsx +59 -0
  39. package/src/pages/post.tsx +1 -3
  40. package/src/pages/slides.tsx +5 -3
  41. package/src/vite-env.d.ts +11 -1
  42. package/vite.config.ts +4 -3
  43. package/dist/client/components/running-bar.js +0 -15
  44. package/dist/client/components/running-bar.js.map +0 -1
  45. package/src/components/content-tabs.tsx +0 -64
  46. package/src/components/running-bar.tsx +0 -21
@@ -1,37 +1,63 @@
1
- import { Link } from "react-router-dom";
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 extract numeric prefix from filename (e.g., "01-intro" → 1)
16
- function extractOrder(name: string): number | null {
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
- interface PostListProps {
30
- directory: DirectoryEntry;
31
- view?: ContentView;
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 default function PostList({ directory, view = 'all' }: PostListProps) {
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
- // Helper to get date from post
60
- const getPostDate = (post: PostEntry): Date | null => {
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) : null;
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
- <Link
96
+ <PostListItem
124
97
  key={post.path}
125
- to={linkPath}
126
- className={cn(
127
- "group block py-3 px-3 -mx-3 rounded-md",
128
- "transition-colors duration-150",
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>
@@ -1,7 +1,7 @@
1
- import siteConfig from "virtual:veslx-config";
1
+ import veslxConfig from "virtual:veslx-config";
2
2
 
3
3
  export function Welcome() {
4
- const config = siteConfig;
4
+ const config = veslxConfig.site;
5
5
 
6
6
  return (
7
7
  <div className="text-muted-foreground">
@@ -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
- // Otherwise show directory listing
45
- return <Home />
33
+ // For directories, try to render index.mdx/index.md, fallback to Home
34
+ return <IndexPost fallback={<Home />} />
46
35
  }
@@ -1,56 +1,16 @@
1
1
  import { useParams } from "react-router-dom"
2
- import { useDirectory } from "../../plugin/src/client";
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 { ContentTabs } from "@/components/content-tabs";
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
- interface HomeProps {
18
- view?: ContentView;
19
- }
20
-
21
- export function Home({ view }: HomeProps) {
6
+ export function Home() {
22
7
  const { "*": path = "." } = useParams();
23
- const config = siteConfig;
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 === "" || 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
-
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
- {directory && (
73
- <div className="animate-fade-in">
74
- <PostList directory={directory} view={isRoot ? activeView : 'all'} />
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
+ }
@@ -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="my-12 mx-auto px-[var(--page-padding)] 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(--content-width)] animate-fade-in">
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>
@@ -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,