veslx 0.1.21 → 0.1.23

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 (73) hide show
  1. package/bin/lib/serve.ts +16 -2
  2. package/bin/lib/start.ts +12 -10
  3. package/bin/veslx.ts +1 -1
  4. package/dist/client/components/front-matter.js +2 -6
  5. package/dist/client/components/front-matter.js.map +1 -1
  6. package/dist/client/components/gallery/components/figure-caption.js +1 -1
  7. package/dist/client/components/gallery/components/figure-caption.js.map +1 -1
  8. package/dist/client/components/gallery/components/figure-header.js +1 -1
  9. package/dist/client/components/gallery/components/figure-header.js.map +1 -1
  10. package/dist/client/components/gallery/components/lightbox.js +1 -1
  11. package/dist/client/components/gallery/components/lightbox.js.map +1 -1
  12. package/dist/client/components/gallery/index.js +38 -8
  13. package/dist/client/components/gallery/index.js.map +1 -1
  14. package/dist/client/components/header.js +45 -21
  15. package/dist/client/components/header.js.map +1 -1
  16. package/dist/client/components/mdx-components.js +8 -0
  17. package/dist/client/components/mdx-components.js.map +1 -1
  18. package/dist/client/components/post-list.js +13 -11
  19. package/dist/client/components/post-list.js.map +1 -1
  20. package/dist/client/components/slides/figure-slide.js +14 -0
  21. package/dist/client/components/slides/figure-slide.js.map +1 -0
  22. package/dist/client/components/slides/hero-slide.js +21 -0
  23. package/dist/client/components/slides/hero-slide.js.map +1 -0
  24. package/dist/client/components/slides/slide-outline.js +28 -0
  25. package/dist/client/components/slides/slide-outline.js.map +1 -0
  26. package/dist/client/components/slides/text-slide.js +18 -0
  27. package/dist/client/components/slides/text-slide.js.map +1 -0
  28. package/dist/client/components/slides-renderer.js.map +1 -1
  29. package/dist/client/hooks/use-mdx-content.js +19 -6
  30. package/dist/client/hooks/use-mdx-content.js.map +1 -1
  31. package/dist/client/lib/content-classification.js +11 -2
  32. package/dist/client/lib/content-classification.js.map +1 -1
  33. package/dist/client/lib/frontmatter-context.js +17 -0
  34. package/dist/client/lib/frontmatter-context.js.map +1 -0
  35. package/dist/client/pages/content-router.js +4 -1
  36. package/dist/client/pages/content-router.js.map +1 -1
  37. package/dist/client/pages/home.js +2 -6
  38. package/dist/client/pages/home.js.map +1 -1
  39. package/dist/client/pages/post.js +2 -9
  40. package/dist/client/pages/post.js.map +1 -1
  41. package/dist/client/pages/slides.js +9 -12
  42. package/dist/client/pages/slides.js.map +1 -1
  43. package/dist/client/plugin/src/client.js +20 -2
  44. package/dist/client/plugin/src/client.js.map +1 -1
  45. package/index.html +13 -0
  46. package/package.json +1 -1
  47. package/plugin/src/client.tsx +28 -2
  48. package/plugin/src/plugin.ts +49 -4
  49. package/src/components/content-tabs.tsx +4 -4
  50. package/src/components/front-matter.tsx +3 -8
  51. package/src/components/gallery/components/figure-caption.tsx +1 -1
  52. package/src/components/gallery/components/figure-header.tsx +1 -1
  53. package/src/components/gallery/components/lightbox.tsx +1 -1
  54. package/src/components/gallery/index.tsx +68 -29
  55. package/src/components/header.tsx +44 -25
  56. package/src/components/mdx-components.tsx +12 -0
  57. package/src/components/post-list.tsx +14 -10
  58. package/src/components/slides/figure-slide.tsx +16 -0
  59. package/src/components/slides/hero-slide.tsx +34 -0
  60. package/src/components/slides/slide-outline.tsx +38 -0
  61. package/src/components/slides/text-slide.tsx +35 -0
  62. package/src/components/slides-renderer.tsx +1 -1
  63. package/src/hooks/use-mdx-content.ts +27 -6
  64. package/src/index.css +1 -2
  65. package/src/lib/content-classification.ts +13 -2
  66. package/src/lib/frontmatter-context.tsx +29 -0
  67. package/src/pages/content-router.tsx +7 -1
  68. package/src/pages/home.tsx +3 -3
  69. package/src/pages/post.tsx +6 -24
  70. package/src/pages/slides.tsx +14 -16
  71. package/vite.config.ts +4 -3
  72. package/dist/client/components/content-tabs.js +0 -50
  73. package/dist/client/components/content-tabs.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sources":["../../../../plugin/src/client.tsx"],"sourcesContent":["import { useState, useEffect, useMemo } from \"react\";\nimport { DirectoryEntry, FileEntry } from \"./lib\";\nimport { buildDirectoryTree, navigateToPath } from \"./directory-tree\";\n// @ts-ignore - virtual module\nimport { files, frontmatters } from \"virtual:content-modules\";\n\n/**\n * Find the main content file for a directory.\n * Supports (in order of preference):\n * - index.mdx / index.md (modern convention)\n * - README.mdx / README.md (traditional convention)\n */\nexport function findReadme(directory: DirectoryEntry): FileEntry | null {\n const indexFiles = [\n \"index.mdx\", \"index.md\",\n \"README.mdx\", \"Readme.mdx\", \"readme.mdx\",\n \"README.md\", \"Readme.md\", \"readme.md\",\n ];\n\n for (const filename of indexFiles) {\n const found = directory.children.find((child) =>\n child.type === \"file\" && child.name === filename\n ) as FileEntry | undefined;\n if (found) return found;\n }\n\n return null;\n}\n\n/**\n * Find all MDX files in a directory (excluding index/README and slides)\n */\nexport function findMdxFiles(directory: DirectoryEntry): FileEntry[] {\n const indexFiles = [\n \"index.mdx\", \"index.md\",\n \"README.mdx\", \"Readme.mdx\", \"readme.mdx\",\n \"README.md\", \"Readme.md\", \"readme.md\",\n ];\n const slideFiles = [\n \"SLIDES.mdx\", \"Slides.mdx\", \"slides.mdx\",\n \"SLIDES.md\", \"Slides.md\", \"slides.md\",\n ];\n const excludeFiles = [...indexFiles, ...slideFiles];\n\n return directory.children.filter((child): child is FileEntry =>\n child.type === \"file\" &&\n (child.name.endsWith('.mdx') || child.name.endsWith('.md')) &&\n !excludeFiles.includes(child.name) &&\n !child.name.endsWith('.slides.mdx') &&\n !child.name.endsWith('.slides.md')\n );\n}\n\nexport function findSlides(directory: DirectoryEntry): FileEntry | null {\n const readme = directory.children.find((child) =>\n child.type === \"file\" &&\n [\n \"SLIDES.md\", \"Slides.md\", \"slides.md\",\n \"SLIDES.mdx\", \"Slides.mdx\", \"slides.mdx\"\n ].includes(child.name)\n ) as FileEntry | undefined;\n\n return readme || null;\n}\n\n\nexport type DirectoryError =\n | { type: 'path_not_found'; message: string; status: 404 };\n\n// Build directory tree once from glob keys, with frontmatter metadata\nconst directoryTree = buildDirectoryTree(Object.keys(files), frontmatters as Record<string, FileEntry['frontmatter']>);\n\nexport function useDirectory(path: string = \".\") {\n const [error, setError] = useState<DirectoryError | null>(null);\n\n const result = useMemo(() => {\n try {\n const { directory, file } = navigateToPath(directoryTree, path);\n return { directory, file };\n } catch {\n setError({ type: 'path_not_found', message: `Path not found: ${path}`, status: 404 });\n return { directory: null, file: null };\n }\n }, [path]);\n\n return {\n directory: result.directory,\n file: result.file,\n loading: false, // No async loading needed\n error\n };\n}\n\nexport function useFileContent(path: string) {\n const [blob, setBlob] = useState<Blob | null>(null);\n const [content, setContent] = useState<string | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const controller = new AbortController();\n setLoading(true);\n setError(null);\n\n (async () => {\n try {\n const res = await fetch(`/raw/${path}`, {\n signal: controller.signal,\n });\n\n if (!res.ok) {\n throw new Error(`Failed to fetch: ${res.status} ${res.statusText}`);\n }\n\n const fetchedBlob = await res.blob();\n setBlob(fetchedBlob);\n\n // Try to read as text - some binary files may fail\n try {\n const text = await fetchedBlob.text();\n setContent(text);\n } catch {\n // Binary file - text content not available\n setContent(null);\n }\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n return; // Ignore abort errors\n }\n setError(err instanceof Error ? err.message : 'Unknown error');\n } finally {\n setLoading(false);\n }\n })();\n\n return () => controller.abort();\n }, [path]);\n\n return { blob, content, loading, error };\n}\n\nexport function isSimulationRunning() {\n const [running, setRunning] = useState<boolean>(false);\n\n useEffect(() => {\n let interval: ReturnType<typeof setInterval>;\n\n const fetchStatus = async () => {\n const response = await fetch(`/raw/.running`);\n\n // this is an elaborate workaround to stop devtools logging errors on 404s\n const text = await response.text()\n if (text === \"\") {\n setRunning(true);\n } else {\n setRunning(false);\n }\n };\n\n // Initial fetch\n fetchStatus();\n\n // Poll every second\n interval = setInterval(fetchStatus, 1000);\n\n return () => {\n clearInterval(interval);\n };\n }, []);\n\n return running;\n}"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sources":["../../../../plugin/src/client.tsx"],"sourcesContent":["import { useState, useEffect, useMemo } from \"react\";\nimport { DirectoryEntry, FileEntry } from \"./lib\";\nimport { buildDirectoryTree, navigateToPath } from \"./directory-tree\";\n// @ts-ignore - virtual module\nimport { files, frontmatters } from \"virtual:content-modules\";\n\n/**\n * Find the main content file for a directory.\n * Supports (in order of preference):\n * - index.mdx / index.md (modern convention)\n * - README.mdx / README.md (traditional convention)\n */\nexport function findReadme(directory: DirectoryEntry): FileEntry | null {\n const indexFiles = [\n \"index.mdx\", \"index.md\",\n \"README.mdx\", \"Readme.mdx\", \"readme.mdx\",\n \"README.md\", \"Readme.md\", \"readme.md\",\n ];\n\n for (const filename of indexFiles) {\n const found = directory.children.find((child) =>\n child.type === \"file\" && child.name === filename\n ) as FileEntry | undefined;\n if (found) return found;\n }\n\n return null;\n}\n\n/**\n * Find all MDX files in a directory (excluding index/README and slides)\n */\nexport function findMdxFiles(directory: DirectoryEntry): FileEntry[] {\n const indexFiles = [\n \"index.mdx\", \"index.md\",\n \"README.mdx\", \"Readme.mdx\", \"readme.mdx\",\n \"README.md\", \"Readme.md\", \"readme.md\",\n ];\n const slideFiles = [\n \"SLIDES.mdx\", \"Slides.mdx\", \"slides.mdx\",\n \"SLIDES.md\", \"Slides.md\", \"slides.md\",\n ];\n const excludeFiles = [...indexFiles, ...slideFiles];\n\n return directory.children.filter((child): child is FileEntry =>\n child.type === \"file\" &&\n (child.name.endsWith('.mdx') || child.name.endsWith('.md')) &&\n !excludeFiles.includes(child.name) &&\n !child.name.endsWith('.slides.mdx') &&\n !child.name.endsWith('.slides.md')\n );\n}\n\nexport function findSlides(directory: DirectoryEntry): FileEntry | null {\n // First check for standard SLIDES.mdx files\n const standardSlides = directory.children.find((child) =>\n child.type === \"file\" &&\n [\n \"SLIDES.md\", \"Slides.md\", \"slides.md\",\n \"SLIDES.mdx\", \"Slides.mdx\", \"slides.mdx\"\n ].includes(child.name)\n ) as FileEntry | undefined;\n\n if (standardSlides) return standardSlides;\n\n // Then check for *.slides.mdx files\n const dotSlides = directory.children.find((child) =>\n child.type === \"file\" &&\n (child.name.endsWith('.slides.mdx') || child.name.endsWith('.slides.md'))\n ) as FileEntry | undefined;\n\n return dotSlides || null;\n}\n\n/**\n * Find all standalone slides files in a directory (*.slides.mdx, *.slides.md)\n * These are slides files that aren't part of a folder (like getting-started.slides.mdx)\n */\nexport function findStandaloneSlides(directory: DirectoryEntry): FileEntry[] {\n const standardSlideFiles = [\n \"SLIDES.mdx\", \"Slides.mdx\", \"slides.mdx\",\n \"SLIDES.md\", \"Slides.md\", \"slides.md\",\n ];\n\n return directory.children.filter((child): child is FileEntry =>\n child.type === \"file\" &&\n (child.name.endsWith('.slides.mdx') || child.name.endsWith('.slides.md')) &&\n !standardSlideFiles.includes(child.name)\n );\n}\n\n\nexport type DirectoryError =\n | { type: 'path_not_found'; message: string; status: 404 };\n\n// Build directory tree once from glob keys, with frontmatter metadata\nconst directoryTree = buildDirectoryTree(Object.keys(files), frontmatters as Record<string, FileEntry['frontmatter']>);\n\nexport function useDirectory(path: string = \".\") {\n const [error, setError] = useState<DirectoryError | null>(null);\n\n const result = useMemo(() => {\n try {\n const { directory, file } = navigateToPath(directoryTree, path);\n return { directory, file };\n } catch {\n setError({ type: 'path_not_found', message: `Path not found: ${path}`, status: 404 });\n return { directory: null, file: null };\n }\n }, [path]);\n\n return {\n directory: result.directory,\n file: result.file,\n loading: false, // No async loading needed\n error\n };\n}\n\nexport function useFileContent(path: string) {\n const [blob, setBlob] = useState<Blob | null>(null);\n const [content, setContent] = useState<string | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const controller = new AbortController();\n setLoading(true);\n setError(null);\n\n (async () => {\n try {\n const res = await fetch(`/raw/${path}`, {\n signal: controller.signal,\n });\n\n if (!res.ok) {\n throw new Error(`Failed to fetch: ${res.status} ${res.statusText}`);\n }\n\n const fetchedBlob = await res.blob();\n setBlob(fetchedBlob);\n\n // Try to read as text - some binary files may fail\n try {\n const text = await fetchedBlob.text();\n setContent(text);\n } catch {\n // Binary file - text content not available\n setContent(null);\n }\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n return; // Ignore abort errors\n }\n setError(err instanceof Error ? err.message : 'Unknown error');\n } finally {\n setLoading(false);\n }\n })();\n\n return () => controller.abort();\n }, [path]);\n\n return { blob, content, loading, error };\n}\n\nexport function isSimulationRunning() {\n const [running, setRunning] = useState<boolean>(false);\n\n useEffect(() => {\n let interval: ReturnType<typeof setInterval>;\n\n const fetchStatus = async () => {\n const response = await fetch(`/raw/.running`);\n\n // this is an elaborate workaround to stop devtools logging errors on 404s\n const text = await response.text()\n if (text === \"\") {\n setRunning(true);\n } else {\n setRunning(false);\n }\n };\n\n // Initial fetch\n fetchStatus();\n\n // Poll every second\n interval = setInterval(fetchStatus, 1000);\n\n return () => {\n clearInterval(interval);\n };\n }, []);\n\n return running;\n}"],"names":[],"mappings":"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"}
package/index.html CHANGED
@@ -5,6 +5,19 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/logo_dark.png" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>veslx</title>
8
+ <!-- Prevent FOUC: apply theme before render -->
9
+ <script>
10
+ (function() {
11
+ var theme = localStorage.getItem('theme');
12
+ var isDark = theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches);
13
+ if (isDark) document.documentElement.classList.add('dark');
14
+ document.documentElement.style.colorScheme = isDark ? 'dark' : 'light';
15
+ })();
16
+ </script>
17
+ <style>
18
+ html { background: hsl(0 0% 100%); }
19
+ html.dark { background: hsl(0 0% 7%); }
20
+ </style>
8
21
  <!-- Google Fonts: DM Sans + DM Mono -->
9
22
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
23
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veslx",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -52,7 +52,8 @@ export function findMdxFiles(directory: DirectoryEntry): FileEntry[] {
52
52
  }
53
53
 
54
54
  export function findSlides(directory: DirectoryEntry): FileEntry | null {
55
- const readme = directory.children.find((child) =>
55
+ // First check for standard SLIDES.mdx files
56
+ const standardSlides = directory.children.find((child) =>
56
57
  child.type === "file" &&
57
58
  [
58
59
  "SLIDES.md", "Slides.md", "slides.md",
@@ -60,7 +61,32 @@ export function findSlides(directory: DirectoryEntry): FileEntry | null {
60
61
  ].includes(child.name)
61
62
  ) as FileEntry | undefined;
62
63
 
63
- return readme || null;
64
+ if (standardSlides) return standardSlides;
65
+
66
+ // Then check for *.slides.mdx files
67
+ const dotSlides = directory.children.find((child) =>
68
+ child.type === "file" &&
69
+ (child.name.endsWith('.slides.mdx') || child.name.endsWith('.slides.md'))
70
+ ) as FileEntry | undefined;
71
+
72
+ return dotSlides || null;
73
+ }
74
+
75
+ /**
76
+ * Find all standalone slides files in a directory (*.slides.mdx, *.slides.md)
77
+ * These are slides files that aren't part of a folder (like getting-started.slides.mdx)
78
+ */
79
+ export function findStandaloneSlides(directory: DirectoryEntry): FileEntry[] {
80
+ const standardSlideFiles = [
81
+ "SLIDES.mdx", "Slides.mdx", "slides.mdx",
82
+ "SLIDES.md", "Slides.md", "slides.md",
83
+ ];
84
+
85
+ return directory.children.filter((child): child is FileEntry =>
86
+ child.type === "file" &&
87
+ (child.name.endsWith('.slides.mdx') || child.name.endsWith('.slides.md')) &&
88
+ !standardSlideFiles.includes(child.name)
89
+ );
64
90
  }
65
91
 
66
92
 
@@ -1,6 +1,7 @@
1
- import { type Plugin, type Connect } from 'vite'
1
+ import { type Plugin, type Connect, type ViteDevServer } from 'vite'
2
2
  import path from 'path'
3
3
  import fs from 'fs'
4
+ import yaml from 'js-yaml'
4
5
  import type { IncomingMessage, ServerResponse } from 'http'
5
6
  import { type VeslxConfig, type ResolvedSiteConfig, DEFAULT_SITE_CONFIG } from './types'
6
7
  import matter from 'gray-matter'
@@ -71,7 +72,11 @@ function copyDirSync(src: string, dest: string) {
71
72
  }
72
73
  }
73
74
 
74
- export default function contentPlugin(contentDir: string, config?: VeslxConfig): Plugin {
75
+ interface PluginOptions {
76
+ configPath?: string
77
+ }
78
+
79
+ export default function contentPlugin(contentDir: string, config?: VeslxConfig, options?: PluginOptions): Plugin {
75
80
 
76
81
  if (!contentDir) {
77
82
  throw new Error('Content directory must be specified.')
@@ -82,13 +87,31 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig):
82
87
  }
83
88
 
84
89
  const dir = contentDir
90
+ const configPath = options?.configPath
85
91
 
86
- // Resolve site config with defaults
87
- const siteConfig: ResolvedSiteConfig = {
92
+ // Mutable site config that can be updated on hot reload
93
+ let siteConfig: ResolvedSiteConfig = {
88
94
  ...DEFAULT_SITE_CONFIG,
89
95
  ...config?.site,
90
96
  }
91
97
 
98
+ // Helper to reload config from file
99
+ function reloadConfig(): boolean {
100
+ if (!configPath || !fs.existsSync(configPath)) return false
101
+ try {
102
+ const content = fs.readFileSync(configPath, 'utf-8')
103
+ const parsed = yaml.load(content) as VeslxConfig
104
+ siteConfig = {
105
+ ...DEFAULT_SITE_CONFIG,
106
+ ...parsed?.site,
107
+ }
108
+ return true
109
+ } catch (e) {
110
+ console.error('[veslx] Failed to reload config:', e)
111
+ return false
112
+ }
113
+ }
114
+
92
115
  // Server middleware for serving content files
93
116
  const urlToDir = new Map<string, string>()
94
117
 
@@ -209,6 +232,28 @@ export const modules = import.meta.glob('@content/**/*.mdx');
209
232
  configureServer(server) {
210
233
  // Add middleware for serving content files
211
234
  server.middlewares.use(middleware)
235
+
236
+ // Watch config file for hot reload
237
+ if (configPath && fs.existsSync(configPath)) {
238
+ server.watcher.add(configPath)
239
+ }
240
+ },
241
+
242
+ handleHotUpdate({ file, server }) {
243
+ // Check if the changed file is our config
244
+ if (configPath && file === configPath) {
245
+ console.log('[veslx] Config changed, reloading...')
246
+ if (reloadConfig()) {
247
+ // Invalidate the virtual config module
248
+ const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_CONFIG_ID)
249
+ if (mod) {
250
+ server.moduleGraph.invalidateModule(mod)
251
+ }
252
+ // Full reload since config affects the entire app
253
+ server.ws.send({ type: 'full-reload' })
254
+ return [] // Prevent default HMR handling
255
+ }
256
+ }
212
257
  },
213
258
  configurePreviewServer(server) {
214
259
  // Add middleware for preview server too
@@ -8,9 +8,9 @@ interface ContentTabsProps {
8
8
  }
9
9
 
10
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" },
11
+ { key: "posts", label: "Posts", path: "/posts" },
12
+ { key: "docs", label: "Docs", path: "/docs" },
13
+ // { key: "all", label: "All", path: "/all" },
14
14
  ];
15
15
 
16
16
  export function ContentTabs({ value, counts }: ContentTabsProps) {
@@ -28,7 +28,7 @@ export function ContentTabs({ value, counts }: ContentTabsProps) {
28
28
  };
29
29
 
30
30
  return (
31
- <nav className="flex justify-end items-center gap-3 font-mono font-medium text-xs text-muted-foreground">
31
+ <nav className="flex items-center gap-3 font-mono font-medium text-xs text-muted-foreground">
32
32
  {views.map((view) => {
33
33
  const disabled = isDisabled(view.key);
34
34
 
@@ -1,13 +1,8 @@
1
- import { useMDXContent, useMDXSlides } from "@/hooks/use-mdx-content";
1
+ import { useFrontmatter } from "@/lib/frontmatter-context";
2
2
  import { formatDate } from "@/lib/format-date"
3
- import { useParams } from "react-router-dom"
4
3
 
5
4
  export function FrontMatter(){
6
- const { "path": path = "." } = useParams();
7
- const { frontmatter: readmeFm } = useMDXContent(path);
8
- const { frontmatter: slidesFm } = useMDXSlides(path);
9
-
10
- let frontmatter = readmeFm || slidesFm;
5
+ const frontmatter = useFrontmatter();
11
6
 
12
7
  return (
13
8
  <div>
@@ -35,4 +30,4 @@ export function FrontMatter(){
35
30
  )}
36
31
  </div>
37
32
  )
38
- }
33
+ }
@@ -4,7 +4,7 @@ export function FigureCaption({ caption, label }: { caption?: string; label?: st
4
4
  if (!caption && !label) return null;
5
5
 
6
6
  return (
7
- <figcaption className="px-[calc((var(--gallery-width)-var(--content-width))/2)] mt-4">
7
+ <figcaption className="mt-4">
8
8
  <p className="text-[13px] leading-[1.6] text-muted-foreground">
9
9
  {label && (
10
10
  <span className="font-semibold text-foreground tracking-tight">
@@ -4,7 +4,7 @@ export function FigureHeader({ title, subtitle }: { title?: string; subtitle?: s
4
4
  if (!title && !subtitle) return null;
5
5
 
6
6
  return (
7
- <div className="px-[calc((var(--gallery-width)-var(--content-width))/2)] mb-4">
7
+ <div className="mb-4">
8
8
  {title && (
9
9
  <h3 className="text-[15px] font-medium tracking-[-0.01em] text-foreground">
10
10
  {renderMathInText(title)}
@@ -31,7 +31,7 @@ export function Lightbox({
31
31
 
32
32
  return createPortal(
33
33
  <div
34
- className="fixed inset-0 z-[9999] bg-background/98 backdrop-blur-md animate-fade-in-slow"
34
+ className="fixed inset-0 z-[9999] bg-background/98 backdrop-blur-md animate-[fade-in_150ms_ease-out]"
35
35
  onClick={onClose}
36
36
  {...{ [FULLSCREEN_DATA_ATTR]: "true" }}
37
37
  style={{ top: 0, left: 0, right: 0, bottom: 0 }}
@@ -29,13 +29,15 @@ function getImageUrl(path: string): string {
29
29
 
30
30
  export default function Gallery({
31
31
  path,
32
- globs = null,
32
+ globs = null,
33
33
  caption,
34
34
  captionLabel,
35
35
  title,
36
36
  subtitle,
37
37
  limit = null,
38
38
  page = 0,
39
+ children,
40
+ childAlign = "right",
39
41
  }: {
40
42
  path?: string;
41
43
  globs?: string[] | null;
@@ -45,6 +47,8 @@ export default function Gallery({
45
47
  subtitle?: string;
46
48
  limit?: number | null;
47
49
  page?: number;
50
+ children?: React.ReactNode;
51
+ childAlign?: "left" | "right";
48
52
  }) {
49
53
  const { paths, isLoading, isEmpty } = useGalleryImages({
50
54
  path,
@@ -91,39 +95,74 @@ export default function Gallery({
91
95
  );
92
96
  }
93
97
 
98
+ const isCompact = images.length <= 3;
99
+ const isSingleWithChildren = images.length === 1 && children;
100
+
101
+ const imageElement = (index: number, img: LightboxImage, className?: string) => (
102
+ <div
103
+ key={index}
104
+ className={`aspect-square overflow-hidden rounded-sm bg-muted/10 cursor-pointer group ${className || ''}`}
105
+ onClick={() => lightbox.open(index)}
106
+ >
107
+ <LoadingImage
108
+ src={img.src}
109
+ alt={img.label}
110
+ className="object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
111
+ />
112
+ </div>
113
+ );
114
+
94
115
  return (
95
116
  <>
96
- <figure className="not-prose relative py-6 md:py-8 -mx-[calc((var(--gallery-width)-var(--content-width))/2+var(--page-padding))]">
97
- <FigureHeader title={title} subtitle={subtitle} />
117
+ <figure className={`not-prose relative py-6 md:py-8 ${isCompact ? '' : '-mx-[calc((var(--gallery-width)-var(--content-width))/2+var(--page-padding))] px-[calc((var(--gallery-width)-var(--content-width))/2)]'}`}>
118
+ {!isSingleWithChildren && <FigureHeader title={title} subtitle={subtitle} />}
98
119
 
99
- <Carousel className="w-full">
100
- <CarouselContent className={`-ml-2 md:-ml-3 ${images.length < 3 ? 'justify-center' : ''}`}>
101
- {images.map((img, index) => (
102
- <CarouselItem
103
- key={index}
104
- className={`pl-2 md:pl-3 md:basis-1/2 lg:basis-1/3 cursor-pointer group ${images.length < 3 ? 'flex-none' : ''}`}
105
- onClick={() => lightbox.open(index)}
106
- >
107
- <div className="aspect-square overflow-hidden rounded-sm ring-1 ring-border/50 transition-all duration-300 group-hover:ring-border group-hover:shadow-lg bg-muted/10">
108
- <LoadingImage
109
- src={img.src}
110
- alt={img.label}
111
- className="object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
112
- />
113
- </div>
114
- </CarouselItem>
115
- ))}
116
- </CarouselContent>
120
+ {isSingleWithChildren ? (
121
+ <div className={`flex gap-6 ${childAlign === 'left' ? '' : 'flex-row-reverse'}`}>
122
+ <div className="flex-1 text-sm leading-relaxed text-foreground/90 space-y-3 [&>ul]:space-y-1.5 [&>ul]:list-disc [&>ul]:pl-5 [&>ol]:space-y-1.5 [&>ol]:list-decimal [&>ol]:pl-5 flex flex-col">
123
+ {(title || subtitle) && <div className="invisible"><FigureHeader title={title} subtitle={subtitle} /></div>}
124
+ <div>{children}</div>
125
+ </div>
126
+ <div className="w-3/5 flex-shrink-0">
127
+ <FigureHeader title={title} subtitle={subtitle} />
128
+ {imageElement(0, images[0])}
129
+ <FigureCaption caption={caption} label={captionLabel} />
130
+ </div>
131
+ </div>
132
+ ) : isCompact ? (
133
+ <div className="flex gap-3">
134
+ {images.map((img, index) => imageElement(index, img, 'flex-1'))}
135
+ </div>
136
+ ) : (
137
+ <Carousel className="w-full">
138
+ <CarouselContent className="-ml-2 md:-ml-3">
139
+ {images.map((img, index) => (
140
+ <CarouselItem
141
+ key={index}
142
+ className="pl-2 md:pl-3 md:basis-1/2 lg:basis-1/3 cursor-pointer group"
143
+ onClick={() => lightbox.open(index)}
144
+ >
145
+ <div className="aspect-square overflow-hidden rounded-sm bg-muted/10">
146
+ <LoadingImage
147
+ src={img.src}
148
+ alt={img.label}
149
+ className="object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
150
+ />
151
+ </div>
152
+ </CarouselItem>
153
+ ))}
154
+ </CarouselContent>
117
155
 
118
- {images.length > 3 && (
119
- <>
120
- <CarouselPrevious className="left-4 bg-background/80 backdrop-blur-sm border-border/50 hover:bg-background hover:border-border" />
121
- <CarouselNext className="right-4 bg-background/80 backdrop-blur-sm border-border/50 hover:bg-background hover:border-border" />
122
- </>
123
- )}
124
- </Carousel>
156
+ {images.length > 3 && (
157
+ <>
158
+ <CarouselPrevious className="left-4 bg-background/80 backdrop-blur-sm border-border/50 hover:bg-background hover:border-border" />
159
+ <CarouselNext className="right-4 bg-background/80 backdrop-blur-sm border-border/50 hover:bg-background hover:border-border" />
160
+ </>
161
+ )}
162
+ </Carousel>
163
+ )}
125
164
 
126
- <FigureCaption caption={caption} label={captionLabel} />
165
+ {!isSingleWithChildren && <FigureCaption caption={caption} label={captionLabel} />}
127
166
  </figure>
128
167
 
129
168
  {lightbox.isOpen && lightbox.selectedIndex !== null && (
@@ -1,8 +1,9 @@
1
- import { Link } from "react-router-dom";
1
+ import { Link, useParams } from "react-router-dom";
2
2
  import { ModeToggle } from "./mode-toggle";
3
3
  import { SiGithub } from "@icons-pack/react-simple-icons";
4
4
  import { ChevronUp, ChevronDown } from "lucide-react";
5
5
  import siteConfig from "virtual:veslx-config";
6
+ import { cn } from "@/lib/utils";
6
7
 
7
8
  interface HeaderProps {
8
9
  slideControls?: {
@@ -16,6 +17,8 @@ interface HeaderProps {
16
17
  export function Header({ slideControls }: HeaderProps = {}) {
17
18
  const config = siteConfig;
18
19
 
20
+ const { "*": path } = useParams()
21
+
19
22
  return (
20
23
  <header className="print:hidden fixed top-0 left-0 right-0 z-40">
21
24
  <div className="mx-auto w-full px-[var(--page-padding)] flex items-center gap-8 py-4">
@@ -30,31 +33,47 @@ export function Header({ slideControls }: HeaderProps = {}) {
30
33
 
31
34
  <div className="flex-1" />
32
35
 
33
- {/* Slide navigation controls */}
34
- {slideControls && (
35
- <nav className="flex items-center gap-1">
36
- <button
37
- onClick={slideControls.onPrevious}
38
- className="p-1.5 text-muted-foreground/70 hover:text-foreground transition-colors duration-200"
39
- title="Previous slide (↑)"
40
- >
41
- <ChevronUp className="h-4 w-4" />
42
- </button>
43
- <span className="font-mono text-xs text-muted-foreground/70 tabular-nums min-w-[3ch] text-center">
44
- {slideControls.current + 1}/{slideControls.total}
45
- </span>
46
- <button
47
- onClick={slideControls.onNext}
48
- className="p-1.5 text-muted-foreground/70 hover:text-foreground transition-colors duration-200"
49
- title="Next slide (↓)"
50
- >
51
- <ChevronDown className="h-4 w-4" />
52
- </button>
53
- </nav>
54
- )}
55
-
56
36
  {/* Navigation */}
57
- <nav className="flex items-center gap-2">
37
+ <nav className="flex items-center gap-4">
38
+ {slideControls && (
39
+ <>
40
+ <button
41
+ onClick={slideControls.onPrevious}
42
+ className="p-1.5 text-muted-foreground/70 hover:text-foreground transition-colors duration-200"
43
+ title="Previous slide (↑)"
44
+ >
45
+ <ChevronUp className="h-4 w-4" />
46
+ </button>
47
+ <span className="font-mono text-xs text-muted-foreground/70 tabular-nums min-w-[3ch] text-center">
48
+ {slideControls.current + 1}/{slideControls.total}
49
+ </span>
50
+ <button
51
+ onClick={slideControls.onNext}
52
+ className="p-1.5 text-muted-foreground/70 hover:text-foreground transition-colors duration-200"
53
+ title="Next slide (↓)"
54
+ >
55
+ <ChevronDown className="h-4 w-4" />
56
+ </button>
57
+ </>
58
+ )}
59
+ <Link
60
+ to={`/posts`}
61
+ className={cn(
62
+ "font-medium text-muted-foreground/70 hover:text-foreground transition-colors duration-300",
63
+ path?.startsWith("posts") && "font-semibold text-foreground",
64
+ )}
65
+ >
66
+ Posts
67
+ </Link>
68
+ <Link
69
+ to={`/docs`}
70
+ className={cn(
71
+ "font-medium text-muted-foreground/70 hover:text-foreground transition-colors duration-300",
72
+ path?.startsWith("docs") && "font-semibold text-foreground",
73
+ )}
74
+ >
75
+ Docs
76
+ </Link>
58
77
  {config.github && (
59
78
  <Link
60
79
  to={`https://github.com/${config.github}`}
@@ -3,6 +3,10 @@ import Gallery from '@/components/gallery'
3
3
  import { ParameterTable } from '@/components/parameter-table'
4
4
  import { ParameterBadge } from '@/components/parameter-badge'
5
5
  import { FrontMatter } from './front-matter'
6
+ import { HeroSlide } from './slides/hero-slide'
7
+ import { FigureSlide } from './slides/figure-slide'
8
+ import { TextSlide } from './slides/text-slide'
9
+ import { SlideOutline } from './slides/slide-outline'
6
10
 
7
11
  /**
8
12
  * Smart link component that uses React Router for internal links
@@ -77,6 +81,14 @@ export const mdxComponents = {
77
81
 
78
82
  ParameterBadge,
79
83
 
84
+ HeroSlide,
85
+
86
+ FigureSlide,
87
+
88
+ TextSlide,
89
+
90
+ SlideOutline,
91
+
80
92
  // Headings - clean sans-serif
81
93
  h1: (props: React.HTMLAttributes<HTMLHeadingElement>) => {
82
94
  const id = generateId(props.children)
@@ -2,7 +2,7 @@ import { Link } from "react-router-dom";
2
2
  import { cn } from "@/lib/utils";
3
3
  import type { DirectoryEntry } from "../../plugin/src/lib";
4
4
  import { formatDate } from "@/lib/format-date";
5
- import { ArrowRight } from "lucide-react";
5
+ import { ArrowRight, Presentation } from "lucide-react";
6
6
  import {
7
7
  type ContentView,
8
8
  type PostEntry,
@@ -117,6 +117,8 @@ export default function PostList({ directory, view = 'all' }: PostListProps) {
117
117
  linkPath = `/${post.path}`;
118
118
  }
119
119
 
120
+ const isSlides = linkPath.endsWith('SLIDES.mdx');
121
+
120
122
  return (
121
123
  <Link
122
124
  key={post.path}
@@ -126,15 +128,7 @@ export default function PostList({ directory, view = 'all' }: PostListProps) {
126
128
  "transition-colors duration-150",
127
129
  )}
128
130
  >
129
- <article className="flex items-start gap-4">
130
- {/* Date - left side, fixed width */}
131
- <time
132
- dateTime={date?.toISOString()}
133
- className="font-mono text-xs text-muted-foreground tabular-nums w-20 flex-shrink-0 pt-0.5"
134
- >
135
- {date ? formatDate(date) : <span className="text-muted-foreground/30">—</span>}
136
- </time>
137
-
131
+ <article className="flex items-center gap-4">
138
132
  {/* Main content */}
139
133
  <div className="flex-1 min-w-0">
140
134
  <h3 className={cn(
@@ -152,6 +146,16 @@ export default function PostList({ directory, view = 'all' }: PostListProps) {
152
146
  </p>
153
147
  )}
154
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>
155
159
  </article>
156
160
  </Link>
157
161
  );
@@ -0,0 +1,16 @@
1
+
2
+
3
+ export function FigureSlide({
4
+ title,
5
+ src,
6
+ }: {
7
+ title: string;
8
+ src: string;
9
+ }) {
10
+ return (
11
+ <div className="figure-slide">
12
+ <h2>{title}</h2>
13
+ <img src={src} alt={title} />
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,34 @@
1
+
2
+ export function HeroSlide({
3
+ title,
4
+ subtitle,
5
+ author,
6
+ date,
7
+ }: {
8
+ title: string;
9
+ subtitle?: string;
10
+ author?: string;
11
+ date?: string;
12
+ }) {
13
+ return (
14
+ <div>
15
+ <h1 className="text-[clamp(2.5rem,6vw,5rem)] font-semibold leading-[1.1] tracking-[-0.02em] text-foreground text-balance">
16
+ {title}
17
+ </h1>
18
+
19
+ {subtitle && (
20
+ <p className="text-[clamp(1rem,2vw,1.5rem)] text-muted-foreground max-w-[50ch] leading-relaxed">
21
+ {subtitle}
22
+ </p>
23
+ )}
24
+
25
+ {(author || date) && (
26
+ <div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted-foreground mt-4">
27
+ {author && <span>{author}</span>}
28
+ {author && date && <span className="text-border">·</span>}
29
+ {date && <span>{date}</span>}
30
+ </div>
31
+ )}
32
+ </div>
33
+ );
34
+ }