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.
- package/bin/lib/serve.ts +16 -2
- package/bin/lib/start.ts +12 -10
- package/bin/veslx.ts +1 -1
- package/dist/client/components/front-matter.js +2 -6
- package/dist/client/components/front-matter.js.map +1 -1
- package/dist/client/components/gallery/components/figure-caption.js +1 -1
- package/dist/client/components/gallery/components/figure-caption.js.map +1 -1
- package/dist/client/components/gallery/components/figure-header.js +1 -1
- package/dist/client/components/gallery/components/figure-header.js.map +1 -1
- package/dist/client/components/gallery/components/lightbox.js +1 -1
- package/dist/client/components/gallery/components/lightbox.js.map +1 -1
- package/dist/client/components/gallery/index.js +38 -8
- package/dist/client/components/gallery/index.js.map +1 -1
- package/dist/client/components/header.js +45 -21
- package/dist/client/components/header.js.map +1 -1
- package/dist/client/components/mdx-components.js +8 -0
- package/dist/client/components/mdx-components.js.map +1 -1
- package/dist/client/components/post-list.js +13 -11
- package/dist/client/components/post-list.js.map +1 -1
- package/dist/client/components/slides/figure-slide.js +14 -0
- package/dist/client/components/slides/figure-slide.js.map +1 -0
- package/dist/client/components/slides/hero-slide.js +21 -0
- package/dist/client/components/slides/hero-slide.js.map +1 -0
- package/dist/client/components/slides/slide-outline.js +28 -0
- package/dist/client/components/slides/slide-outline.js.map +1 -0
- package/dist/client/components/slides/text-slide.js +18 -0
- package/dist/client/components/slides/text-slide.js.map +1 -0
- package/dist/client/components/slides-renderer.js.map +1 -1
- package/dist/client/hooks/use-mdx-content.js +19 -6
- package/dist/client/hooks/use-mdx-content.js.map +1 -1
- package/dist/client/lib/content-classification.js +11 -2
- package/dist/client/lib/content-classification.js.map +1 -1
- package/dist/client/lib/frontmatter-context.js +17 -0
- package/dist/client/lib/frontmatter-context.js.map +1 -0
- package/dist/client/pages/content-router.js +4 -1
- package/dist/client/pages/content-router.js.map +1 -1
- package/dist/client/pages/home.js +2 -6
- package/dist/client/pages/home.js.map +1 -1
- package/dist/client/pages/post.js +2 -9
- package/dist/client/pages/post.js.map +1 -1
- package/dist/client/pages/slides.js +9 -12
- package/dist/client/pages/slides.js.map +1 -1
- package/dist/client/plugin/src/client.js +20 -2
- package/dist/client/plugin/src/client.js.map +1 -1
- package/index.html +13 -0
- package/package.json +1 -1
- package/plugin/src/client.tsx +28 -2
- package/plugin/src/plugin.ts +49 -4
- package/src/components/content-tabs.tsx +4 -4
- package/src/components/front-matter.tsx +3 -8
- package/src/components/gallery/components/figure-caption.tsx +1 -1
- package/src/components/gallery/components/figure-header.tsx +1 -1
- package/src/components/gallery/components/lightbox.tsx +1 -1
- package/src/components/gallery/index.tsx +68 -29
- package/src/components/header.tsx +44 -25
- package/src/components/mdx-components.tsx +12 -0
- package/src/components/post-list.tsx +14 -10
- package/src/components/slides/figure-slide.tsx +16 -0
- package/src/components/slides/hero-slide.tsx +34 -0
- package/src/components/slides/slide-outline.tsx +38 -0
- package/src/components/slides/text-slide.tsx +35 -0
- package/src/components/slides-renderer.tsx +1 -1
- package/src/hooks/use-mdx-content.ts +27 -6
- package/src/index.css +1 -2
- package/src/lib/content-classification.ts +13 -2
- package/src/lib/frontmatter-context.tsx +29 -0
- package/src/pages/content-router.tsx +7 -1
- package/src/pages/home.tsx +3 -3
- package/src/pages/post.tsx +6 -24
- package/src/pages/slides.tsx +14 -16
- package/vite.config.ts +4 -3
- package/dist/client/components/content-tabs.js +0 -50
- 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
|
|
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
package/plugin/src/client.tsx
CHANGED
|
@@ -52,7 +52,8 @@ export function findMdxFiles(directory: DirectoryEntry): FileEntry[] {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export function findSlides(directory: DirectoryEntry): FileEntry | null {
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
package/plugin/src/plugin.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
87
|
-
|
|
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: "
|
|
12
|
-
{ key: "docs", label: "
|
|
13
|
-
{ key: "all", label: "
|
|
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
|
|
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 {
|
|
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
|
|
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="
|
|
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="
|
|
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-
|
|
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=
|
|
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
|
-
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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-
|
|
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-
|
|
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,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
|
+
}
|