veslx 0.1.35 → 0.1.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib/build.ts +0 -12
- package/bin/lib/export.ts +0 -12
- package/bin/lib/serve.ts +9 -14
- package/dist/client/components/footer.js +21 -0
- package/dist/client/components/footer.js.map +1 -0
- package/dist/client/components/gallery/index.js +21 -2
- package/dist/client/components/gallery/index.js.map +1 -1
- package/dist/client/components/mdx-components.js +2 -0
- package/dist/client/components/mdx-components.js.map +1 -1
- package/dist/client/components/parameter-table.js +163 -23
- package/dist/client/components/parameter-table.js.map +1 -1
- package/dist/client/components/post-list.js +7 -1
- package/dist/client/components/post-list.js.map +1 -1
- package/dist/client/hooks/use-mdx-content.js +29 -3
- package/dist/client/hooks/use-mdx-content.js.map +1 -1
- package/dist/client/lib/parameter-utils.js +1 -1
- package/dist/client/lib/parameter-utils.js.map +1 -1
- package/dist/client/package.json.js +9 -0
- package/dist/client/package.json.js.map +1 -0
- package/dist/client/pages/home.js +3 -1
- package/dist/client/pages/home.js.map +1 -1
- package/dist/client/pages/index-post.js +3 -1
- package/dist/client/pages/index-post.js.map +1 -1
- package/dist/client/pages/post.js +3 -1
- package/dist/client/pages/post.js.map +1 -1
- package/package.json +1 -1
- package/plugin/src/plugin.ts +70 -39
- package/src/components/footer.tsx +18 -0
- package/src/components/gallery/index.tsx +32 -2
- package/src/components/mdx-components.tsx +3 -0
- package/src/components/parameter-table.tsx +233 -14
- package/src/components/post-list.tsx +14 -1
- package/src/hooks/use-mdx-content.ts +54 -14
- package/src/lib/parameter-utils.ts +1 -1
- package/src/pages/home.tsx +2 -0
- package/src/pages/index-post.tsx +5 -2
- package/src/pages/post.tsx +3 -1
package/plugin/src/plugin.ts
CHANGED
|
@@ -310,9 +310,6 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig,
|
|
|
310
310
|
next()
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
// Virtual module ID for the modified CSS
|
|
314
|
-
const VIRTUAL_CSS_MODULE = '\0veslx:index.css'
|
|
315
|
-
|
|
316
313
|
return {
|
|
317
314
|
name: 'content',
|
|
318
315
|
enforce: 'pre',
|
|
@@ -336,17 +333,8 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig,
|
|
|
336
333
|
}
|
|
337
334
|
},
|
|
338
335
|
|
|
339
|
-
// Intercept
|
|
340
|
-
resolveId(id
|
|
341
|
-
// Intercept index.css imported from main.tsx and redirect to our virtual module
|
|
342
|
-
// This allows us to inject @source directive for Tailwind to scan user content
|
|
343
|
-
if (id === './index.css' && importer?.endsWith('/src/main.tsx')) {
|
|
344
|
-
return VIRTUAL_CSS_MODULE
|
|
345
|
-
}
|
|
346
|
-
// Also catch the resolved path
|
|
347
|
-
if (id.endsWith('/src/index.css') && !id.startsWith('\0')) {
|
|
348
|
-
return VIRTUAL_CSS_MODULE
|
|
349
|
-
}
|
|
336
|
+
// Intercept virtual module imports
|
|
337
|
+
resolveId(id) {
|
|
350
338
|
// Virtual modules for content
|
|
351
339
|
if (id === VIRTUAL_MODULE_ID) {
|
|
352
340
|
return RESOLVED_VIRTUAL_MODULE_ID
|
|
@@ -356,28 +344,25 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig,
|
|
|
356
344
|
}
|
|
357
345
|
},
|
|
358
346
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (id
|
|
363
|
-
// Read the original CSS
|
|
364
|
-
const veslxRoot = path.dirname(path.dirname(__dirname))
|
|
365
|
-
const cssPath = path.join(veslxRoot, 'src/index.css')
|
|
366
|
-
const cssContent = fs.readFileSync(cssPath, 'utf-8')
|
|
367
|
-
|
|
347
|
+
// Transform CSS to inject @source directive for Tailwind
|
|
348
|
+
// This enables Tailwind v4 to scan the user's content directory for classes
|
|
349
|
+
transform(code, id) {
|
|
350
|
+
if (id.endsWith('/src/index.css') && code.includes("@import 'tailwindcss'")) {
|
|
368
351
|
// Use absolute path for @source directive
|
|
369
352
|
const absoluteContentDir = dir.replace(/\\/g, '/')
|
|
353
|
+
const sourceDirective = `@source "${absoluteContentDir}";`
|
|
370
354
|
|
|
371
355
|
// Inject @source directive after the tailwindcss import
|
|
372
|
-
const
|
|
373
|
-
const modified = cssContent.replace(
|
|
356
|
+
const modified = code.replace(
|
|
374
357
|
/(@import\s+["']tailwindcss["'];?)/,
|
|
375
358
|
`$1\n${sourceDirective}`
|
|
376
359
|
)
|
|
377
360
|
|
|
378
|
-
return modified
|
|
361
|
+
return { code: modified, map: null }
|
|
379
362
|
}
|
|
363
|
+
},
|
|
380
364
|
|
|
365
|
+
load(id) {
|
|
381
366
|
// Virtual module for content
|
|
382
367
|
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
|
|
383
368
|
// Extract frontmatter from MDX files at build time (avoids MDX hook issues)
|
|
@@ -385,15 +370,32 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig,
|
|
|
385
370
|
|
|
386
371
|
// Generate virtual module with import.meta.glob for MDX files
|
|
387
372
|
return `
|
|
388
|
-
export const posts = import.meta.glob(['@content/**/*.mdx', '@content/**/*.md'], {
|
|
373
|
+
export const posts = import.meta.glob(['@content/*.mdx', '@content/*.md', '@content/**/*.mdx', '@content/**/*.md'], {
|
|
389
374
|
import: 'default',
|
|
390
375
|
query: { skipSlides: true }
|
|
391
376
|
});
|
|
392
|
-
export const allMdx = import.meta.glob(['@content/**/*.mdx', '@content/**/*.md']);
|
|
393
|
-
export const slides = import.meta.glob(['@content/**/SLIDES.mdx', '@content/**/SLIDES.md', '@content/**/*.slides.mdx', '@content/**/*.slides.md']);
|
|
377
|
+
export const allMdx = import.meta.glob(['@content/*.mdx', '@content/*.md', '@content/**/*.mdx', '@content/**/*.md']);
|
|
378
|
+
export const slides = import.meta.glob(['@content/SLIDES.mdx', '@content/SLIDES.md', '@content/*.slides.mdx', '@content/*.slides.md', '@content/**/SLIDES.mdx', '@content/**/SLIDES.md', '@content/**/*.slides.mdx', '@content/**/*.slides.md']);
|
|
394
379
|
|
|
395
|
-
// All files for directory tree building (
|
|
380
|
+
// All files for directory tree building (using ?url to avoid parsing non-JS files)
|
|
381
|
+
// Exclude veslx.yaml config files from bundling
|
|
396
382
|
export const files = import.meta.glob([
|
|
383
|
+
'@content/*.mdx',
|
|
384
|
+
'@content/*.md',
|
|
385
|
+
'@content/*.tsx',
|
|
386
|
+
'@content/*.ts',
|
|
387
|
+
'@content/*.jsx',
|
|
388
|
+
'@content/*.js',
|
|
389
|
+
'@content/*.png',
|
|
390
|
+
'@content/*.jpg',
|
|
391
|
+
'@content/*.jpeg',
|
|
392
|
+
'@content/*.gif',
|
|
393
|
+
'@content/*.svg',
|
|
394
|
+
'@content/*.webp',
|
|
395
|
+
'@content/*.css',
|
|
396
|
+
'@content/*.yaml',
|
|
397
|
+
'@content/*.yml',
|
|
398
|
+
'@content/*.json',
|
|
397
399
|
'@content/**/*.mdx',
|
|
398
400
|
'@content/**/*.md',
|
|
399
401
|
'@content/**/*.tsx',
|
|
@@ -407,13 +409,18 @@ export const files = import.meta.glob([
|
|
|
407
409
|
'@content/**/*.svg',
|
|
408
410
|
'@content/**/*.webp',
|
|
409
411
|
'@content/**/*.css',
|
|
410
|
-
|
|
412
|
+
'@content/**/*.yaml',
|
|
413
|
+
'@content/**/*.yml',
|
|
414
|
+
'@content/**/*.json',
|
|
415
|
+
'!@content/veslx.yaml',
|
|
416
|
+
'!@content/**/veslx.yaml',
|
|
417
|
+
], { eager: false, query: '?url', import: 'default' });
|
|
411
418
|
|
|
412
419
|
// Frontmatter extracted at build time (no MDX execution required)
|
|
413
420
|
export const frontmatters = ${JSON.stringify(frontmatterData)};
|
|
414
421
|
|
|
415
422
|
// Legacy aliases for backwards compatibility
|
|
416
|
-
export const modules = import.meta.glob(['@content/**/*.mdx', '@content/**/*.md']);
|
|
423
|
+
export const modules = import.meta.glob(['@content/*.mdx', '@content/*.md', '@content/**/*.mdx', '@content/**/*.md']);
|
|
417
424
|
`
|
|
418
425
|
}
|
|
419
426
|
if (id === RESOLVED_VIRTUAL_CONFIG_ID) {
|
|
@@ -438,15 +445,21 @@ export const modules = import.meta.glob(['@content/**/*.mdx', '@content/**/*.md'
|
|
|
438
445
|
// File extensions that should trigger a full reload
|
|
439
446
|
const watchedExtensions = ['.mdx', '.md', '.yaml', '.yml', '.json', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.tsx', '.ts', '.jsx', '.js', '.css']
|
|
440
447
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
448
|
+
// Debounce reload to avoid rapid-fire refreshes when multiple files change
|
|
449
|
+
let reloadTimeout: ReturnType<typeof setTimeout> | null = null
|
|
450
|
+
const pendingChanges: Array<{ filePath: string; event: 'add' | 'unlink' | 'change' }> = []
|
|
451
|
+
const DEBOUNCE_MS = 1000
|
|
444
452
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (!watchedExtensions.includes(ext)) return
|
|
453
|
+
const flushReload = () => {
|
|
454
|
+
if (pendingChanges.length === 0) return
|
|
448
455
|
|
|
449
|
-
|
|
456
|
+
// Log all pending changes
|
|
457
|
+
for (const { filePath, event } of pendingChanges) {
|
|
458
|
+
console.log(`[veslx] Content ${event}: ${path.relative(dir, filePath)}`)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Clear pending changes
|
|
462
|
+
pendingChanges.length = 0
|
|
450
463
|
|
|
451
464
|
// Invalidate the virtual content module so frontmatters are re-extracted
|
|
452
465
|
const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_MODULE_ID)
|
|
@@ -458,6 +471,24 @@ export const modules = import.meta.glob(['@content/**/*.mdx', '@content/**/*.md'
|
|
|
458
471
|
server.ws.send({ type: 'full-reload' })
|
|
459
472
|
}
|
|
460
473
|
|
|
474
|
+
const handleContentChange = (filePath: string, event: 'add' | 'unlink' | 'change') => {
|
|
475
|
+
// Check if the file is in the content directory
|
|
476
|
+
if (!filePath.startsWith(dir)) return
|
|
477
|
+
|
|
478
|
+
// Check if it's a watched file type
|
|
479
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
480
|
+
if (!watchedExtensions.includes(ext)) return
|
|
481
|
+
|
|
482
|
+
// Queue this change
|
|
483
|
+
pendingChanges.push({ filePath, event })
|
|
484
|
+
|
|
485
|
+
// Debounce: clear existing timeout and set a new one
|
|
486
|
+
if (reloadTimeout) {
|
|
487
|
+
clearTimeout(reloadTimeout)
|
|
488
|
+
}
|
|
489
|
+
reloadTimeout = setTimeout(flushReload, DEBOUNCE_MS)
|
|
490
|
+
}
|
|
491
|
+
|
|
461
492
|
server.watcher.on('add', (filePath) => handleContentChange(filePath, 'add'))
|
|
462
493
|
server.watcher.on('unlink', (filePath) => handleContentChange(filePath, 'unlink'))
|
|
463
494
|
server.watcher.on('change', (filePath) => handleContentChange(filePath, 'change'))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import packageJson from "../../package.json";
|
|
2
|
+
|
|
3
|
+
export function Footer() {
|
|
4
|
+
return (
|
|
5
|
+
<footer className="py-4 mx-auto w-full max-w-[var(--content-width)] px-[var(--page-padding)]">
|
|
6
|
+
<div className="flex justify-end">
|
|
7
|
+
<a
|
|
8
|
+
href="https://github.com/eoinmurray/veslx"
|
|
9
|
+
target="_blank"
|
|
10
|
+
rel="noopener noreferrer"
|
|
11
|
+
className="font-mono text-xs text-muted-foreground/50 hover:text-muted-foreground transition-colors"
|
|
12
|
+
>
|
|
13
|
+
veslx v{packageJson.version}
|
|
14
|
+
</a>
|
|
15
|
+
</div>
|
|
16
|
+
</footer>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
1
|
+
import { useMemo, useRef, useEffect } from "react";
|
|
2
2
|
import { Image } from "lucide-react";
|
|
3
3
|
import { Lightbox, LightboxImage } from "@/components/gallery/components/lightbox";
|
|
4
4
|
import { useGalleryImages } from "./hooks/use-gallery-images";
|
|
@@ -7,6 +7,34 @@ import { LoadingImage } from "./components/loading-image";
|
|
|
7
7
|
import { FigureHeader } from "./components/figure-header";
|
|
8
8
|
import { FigureCaption } from "./components/figure-caption";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Hook to prevent horizontal scroll from triggering browser back/forward gestures.
|
|
12
|
+
* Captures wheel events and prevents default when at scroll boundaries.
|
|
13
|
+
*/
|
|
14
|
+
function usePreventSwipeNavigation(ref: React.RefObject<HTMLElement | null>) {
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const el = ref.current;
|
|
17
|
+
if (!el) return;
|
|
18
|
+
|
|
19
|
+
const handleWheel = (e: WheelEvent) => {
|
|
20
|
+
// Only handle horizontal scrolling
|
|
21
|
+
if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;
|
|
22
|
+
|
|
23
|
+
const { scrollLeft, scrollWidth, clientWidth } = el;
|
|
24
|
+
const atLeftEdge = scrollLeft <= 0;
|
|
25
|
+
const atRightEdge = scrollLeft + clientWidth >= scrollWidth - 1;
|
|
26
|
+
|
|
27
|
+
// Prevent default if trying to scroll past boundaries
|
|
28
|
+
if ((atLeftEdge && e.deltaX < 0) || (atRightEdge && e.deltaX > 0)) {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
el.addEventListener('wheel', handleWheel, { passive: false });
|
|
34
|
+
return () => el.removeEventListener('wheel', handleWheel);
|
|
35
|
+
}, [ref]);
|
|
36
|
+
}
|
|
37
|
+
|
|
10
38
|
function getImageLabel(path: string): string {
|
|
11
39
|
const filename = path.split('/').pop() || path;
|
|
12
40
|
return filename
|
|
@@ -51,6 +79,8 @@ export default function Gallery({
|
|
|
51
79
|
});
|
|
52
80
|
|
|
53
81
|
const lightbox = useLightbox(paths.length);
|
|
82
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
83
|
+
usePreventSwipeNavigation(scrollRef);
|
|
54
84
|
|
|
55
85
|
const images: LightboxImage[] = useMemo(() =>
|
|
56
86
|
paths.map(p => ({ src: getImageUrl(p), label: getImageLabel(p) })),
|
|
@@ -142,7 +172,7 @@ export default function Gallery({
|
|
|
142
172
|
{images.map((img, index) => imageElement(index, img, 'flex-1'))}
|
|
143
173
|
</div>
|
|
144
174
|
) : (
|
|
145
|
-
<div className="flex gap-3 overflow-x-auto overscroll-x-contain pb-4">
|
|
175
|
+
<div ref={scrollRef} className="flex gap-3 overflow-x-auto overscroll-x-contain pb-4">
|
|
146
176
|
{images.map((img, index) => (
|
|
147
177
|
<div
|
|
148
178
|
key={index}
|
|
@@ -8,6 +8,7 @@ import { FigureSlide } from './slides/figure-slide'
|
|
|
8
8
|
import { TextSlide } from './slides/text-slide'
|
|
9
9
|
import { SlideOutline } from './slides/slide-outline'
|
|
10
10
|
import { PostList } from '@/components/post-list'
|
|
11
|
+
import { PostListItem } from '@/components/post-list-item'
|
|
11
12
|
/**
|
|
12
13
|
* Smart link component that uses React Router for internal links
|
|
13
14
|
* and regular anchor tags for external links.
|
|
@@ -91,6 +92,8 @@ export const mdxComponents = {
|
|
|
91
92
|
|
|
92
93
|
PostList,
|
|
93
94
|
|
|
95
|
+
PostListItem,
|
|
96
|
+
|
|
94
97
|
// Headings - clean sans-serif
|
|
95
98
|
h1: (props: React.HTMLAttributes<HTMLHeadingElement>) => {
|
|
96
99
|
const id = generateId(props.children)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { useFileContent } from "../../plugin/src/client";
|
|
2
|
-
import { useMemo, useState } from "react";
|
|
1
|
+
import { useFileContent, useDirectory } from "../../plugin/src/client";
|
|
2
|
+
import { useMemo, useState, useRef, useEffect } from "react";
|
|
3
|
+
import { useParams } from "react-router-dom";
|
|
3
4
|
import { cn } from "@/lib/utils";
|
|
5
|
+
import { minimatch } from "minimatch";
|
|
4
6
|
import {
|
|
5
7
|
type ParameterValue,
|
|
6
8
|
extractPath,
|
|
@@ -8,6 +10,67 @@ import {
|
|
|
8
10
|
formatValue,
|
|
9
11
|
parseConfigFile,
|
|
10
12
|
} from "@/lib/parameter-utils";
|
|
13
|
+
import { FileEntry, DirectoryEntry } from "../../plugin/src/lib";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hook to prevent horizontal scroll from triggering browser back/forward gestures.
|
|
17
|
+
*/
|
|
18
|
+
function usePreventSwipeNavigation(ref: React.RefObject<HTMLElement | null>) {
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const el = ref.current;
|
|
21
|
+
if (!el) return;
|
|
22
|
+
|
|
23
|
+
const handleWheel = (e: WheelEvent) => {
|
|
24
|
+
if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;
|
|
25
|
+
|
|
26
|
+
const { scrollLeft, scrollWidth, clientWidth } = el;
|
|
27
|
+
const atLeftEdge = scrollLeft <= 0;
|
|
28
|
+
const atRightEdge = scrollLeft + clientWidth >= scrollWidth - 1;
|
|
29
|
+
|
|
30
|
+
if ((atLeftEdge && e.deltaX < 0) || (atRightEdge && e.deltaX > 0)) {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
el.addEventListener('wheel', handleWheel, { passive: false });
|
|
36
|
+
return () => el.removeEventListener('wheel', handleWheel);
|
|
37
|
+
}, [ref]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if a path contains glob patterns
|
|
41
|
+
function isGlobPattern(path: string): boolean {
|
|
42
|
+
return path.includes('*') || path.includes('?') || path.includes('[');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Recursively collect all config files from a directory tree
|
|
46
|
+
function collectAllConfigFiles(entry: DirectoryEntry | FileEntry): FileEntry[] {
|
|
47
|
+
if (entry.type === "file") {
|
|
48
|
+
if (entry.name.match(/\.(yaml|yml|json)$/i)) {
|
|
49
|
+
return [entry];
|
|
50
|
+
}
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const files: FileEntry[] = [];
|
|
54
|
+
for (const child of entry.children || []) {
|
|
55
|
+
files.push(...collectAllConfigFiles(child));
|
|
56
|
+
}
|
|
57
|
+
return files;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Sort paths numerically
|
|
61
|
+
function sortPathsNumerically(paths: string[]): void {
|
|
62
|
+
paths.sort((a, b) => {
|
|
63
|
+
const nums = (s: string) => (s.match(/\d+/g) || []).map(Number);
|
|
64
|
+
const na = nums(a);
|
|
65
|
+
const nb = nums(b);
|
|
66
|
+
const len = Math.max(na.length, nb.length);
|
|
67
|
+
for (let i = 0; i < len; i++) {
|
|
68
|
+
const diff = (na[i] ?? 0) - (nb[i] ?? 0);
|
|
69
|
+
if (diff !== 0) return diff;
|
|
70
|
+
}
|
|
71
|
+
return a.localeCompare(b);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
11
74
|
|
|
12
75
|
/**
|
|
13
76
|
* Build a filtered data object from an array of jq-like paths.
|
|
@@ -59,12 +122,13 @@ function ParameterGrid({ entries }: { entries: [string, ParameterValue][] }) {
|
|
|
59
122
|
</span>
|
|
60
123
|
<span
|
|
61
124
|
className={cn(
|
|
62
|
-
"text-[11px] font-mono tabular-nums font-medium
|
|
125
|
+
"text-[11px] font-mono tabular-nums font-medium truncate max-w-[120px]",
|
|
63
126
|
type === "number" && "text-foreground",
|
|
64
127
|
type === "string" && "text-amber-600 dark:text-amber-500",
|
|
65
128
|
type === "boolean" && "text-cyan-600 dark:text-cyan-500",
|
|
66
129
|
type === "null" && "text-muted-foreground/50"
|
|
67
130
|
)}
|
|
131
|
+
title={type === "string" ? `"${formatValue(value)}"` : formatValue(value)}
|
|
68
132
|
>
|
|
69
133
|
{type === "string" ? `"${formatValue(value)}"` : formatValue(value)}
|
|
70
134
|
</span>
|
|
@@ -184,7 +248,7 @@ function ParameterSection({
|
|
|
184
248
|
);
|
|
185
249
|
}
|
|
186
250
|
|
|
187
|
-
interface
|
|
251
|
+
interface SingleParameterTableProps {
|
|
188
252
|
/** Path to the YAML or JSON file */
|
|
189
253
|
path: string;
|
|
190
254
|
/**
|
|
@@ -195,6 +259,19 @@ interface ParameterTableProps {
|
|
|
195
259
|
* - [".default_inputs", ".base.dt"] → show default_inputs section and dt from base
|
|
196
260
|
*/
|
|
197
261
|
keys?: string[];
|
|
262
|
+
/** Optional label to show above the table */
|
|
263
|
+
label?: string;
|
|
264
|
+
/** Whether to include vertical margin (default true) */
|
|
265
|
+
withMargin?: boolean;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
interface ParameterTableProps {
|
|
269
|
+
/** Path to the YAML or JSON file, supports glob patterns like "*.yaml" */
|
|
270
|
+
path: string;
|
|
271
|
+
/**
|
|
272
|
+
* Optional array of jq-like paths to filter which parameters to show.
|
|
273
|
+
*/
|
|
274
|
+
keys?: string[];
|
|
198
275
|
}
|
|
199
276
|
|
|
200
277
|
/**
|
|
@@ -282,20 +359,34 @@ function splitIntoColumns<T extends [string, ParameterValue]>(
|
|
|
282
359
|
return columns;
|
|
283
360
|
}
|
|
284
361
|
|
|
285
|
-
|
|
362
|
+
function SingleParameterTable({ path, keys, label, withMargin = true }: SingleParameterTableProps) {
|
|
286
363
|
const { content, loading, error } = useFileContent(path);
|
|
287
364
|
|
|
288
|
-
const parsed = useMemo(() => {
|
|
289
|
-
if (!content) return null;
|
|
365
|
+
const { parsed, parseError } = useMemo(() => {
|
|
366
|
+
if (!content) return { parsed: null, parseError: 'no content' };
|
|
290
367
|
|
|
291
368
|
const data = parseConfigFile(content, path);
|
|
292
|
-
if (!data)
|
|
369
|
+
if (!data) {
|
|
370
|
+
// Check why parsing failed
|
|
371
|
+
if (!path.match(/\.(yaml|yml|json)$/i)) {
|
|
372
|
+
return { parsed: null, parseError: `unsupported file type` };
|
|
373
|
+
}
|
|
374
|
+
// Check if content looks like HTML (404 page)
|
|
375
|
+
if (content.trim().startsWith('<!') || content.trim().startsWith('<html')) {
|
|
376
|
+
return { parsed: null, parseError: `file not found` };
|
|
377
|
+
}
|
|
378
|
+
return { parsed: null, parseError: `invalid ${path.split('.').pop()} syntax` };
|
|
379
|
+
}
|
|
293
380
|
|
|
294
381
|
if (keys && keys.length > 0) {
|
|
295
|
-
|
|
382
|
+
const filtered = filterData(data, keys);
|
|
383
|
+
if (Object.keys(filtered).length === 0) {
|
|
384
|
+
return { parsed: null, parseError: `keys not found: ${keys.join(', ')}` };
|
|
385
|
+
}
|
|
386
|
+
return { parsed: filtered, parseError: null };
|
|
296
387
|
}
|
|
297
388
|
|
|
298
|
-
return data;
|
|
389
|
+
return { parsed: data, parseError: null };
|
|
299
390
|
}, [content, path, keys]);
|
|
300
391
|
|
|
301
392
|
if (loading) {
|
|
@@ -319,8 +410,11 @@ export function ParameterTable({ path, keys }: ParameterTableProps) {
|
|
|
319
410
|
|
|
320
411
|
if (!parsed) {
|
|
321
412
|
return (
|
|
322
|
-
<div className="
|
|
323
|
-
<p className="text-[11px] font-mono text-muted-foreground">
|
|
413
|
+
<div className={cn("p-3 rounded border border-border/50 bg-card/30", withMargin && "my-6")}>
|
|
414
|
+
<p className="text-[11px] font-mono text-muted-foreground">
|
|
415
|
+
{label && <span className="text-foreground/60">{label}: </span>}
|
|
416
|
+
{parseError || 'unable to parse'}
|
|
417
|
+
</p>
|
|
324
418
|
</div>
|
|
325
419
|
);
|
|
326
420
|
}
|
|
@@ -390,8 +484,13 @@ export function ParameterTable({ path, keys }: ParameterTableProps) {
|
|
|
390
484
|
};
|
|
391
485
|
|
|
392
486
|
return (
|
|
393
|
-
<div className="
|
|
394
|
-
|
|
487
|
+
<div className={cn("not-prose", withMargin && "my-6")}>
|
|
488
|
+
{label && (
|
|
489
|
+
<div className="text-[11px] font-mono text-muted-foreground mb-1.5 truncate" title={label}>
|
|
490
|
+
{label}
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
<div className="rounded border border-border/60 bg-card/20 p-3 overflow-hidden max-w-full">
|
|
395
494
|
{topLeaves.length > 0 && (
|
|
396
495
|
<div className={cn(topNested.length > 0 && "mb-4 pb-3 border-b border-border/30")}>
|
|
397
496
|
<ParameterGrid entries={topLeaves} />
|
|
@@ -418,3 +517,123 @@ export function ParameterTable({ path, keys }: ParameterTableProps) {
|
|
|
418
517
|
</div>
|
|
419
518
|
);
|
|
420
519
|
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* ParameterTable component that displays YAML/JSON config files.
|
|
523
|
+
* Supports glob patterns in the path prop to show multiple files.
|
|
524
|
+
*/
|
|
525
|
+
export function ParameterTable({ path, keys }: ParameterTableProps) {
|
|
526
|
+
const { "*": routePath = "" } = useParams();
|
|
527
|
+
|
|
528
|
+
// Get current directory from route
|
|
529
|
+
const currentDir = routePath
|
|
530
|
+
.replace(/\/?[^/]+\.mdx$/i, "")
|
|
531
|
+
.replace(/\/$/, "")
|
|
532
|
+
|| ".";
|
|
533
|
+
|
|
534
|
+
// Resolve relative paths
|
|
535
|
+
let resolvedPath = path;
|
|
536
|
+
if (path?.startsWith("./")) {
|
|
537
|
+
const relativePart = path.slice(2);
|
|
538
|
+
resolvedPath = currentDir === "." ? relativePart : `${currentDir}/${relativePart}`;
|
|
539
|
+
} else if (path && !path.startsWith("/") && !path.includes("/") && !isGlobPattern(path)) {
|
|
540
|
+
resolvedPath = currentDir === "." ? path : `${currentDir}/${path}`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Check if this is a glob pattern
|
|
544
|
+
const hasGlob = isGlobPattern(resolvedPath);
|
|
545
|
+
|
|
546
|
+
// For glob patterns, get the base directory (directory containing the glob pattern)
|
|
547
|
+
const baseDir = useMemo(() => {
|
|
548
|
+
if (!hasGlob) return null;
|
|
549
|
+
// Get everything before the first glob character
|
|
550
|
+
const beforeGlob = resolvedPath.split(/[*?\[]/, 1)[0];
|
|
551
|
+
// Extract directory portion (everything up to the last slash)
|
|
552
|
+
const lastSlash = beforeGlob.lastIndexOf('/');
|
|
553
|
+
if (lastSlash === -1) return ".";
|
|
554
|
+
return beforeGlob.slice(0, lastSlash) || ".";
|
|
555
|
+
}, [hasGlob, resolvedPath]);
|
|
556
|
+
|
|
557
|
+
const { directory } = useDirectory(baseDir || ".");
|
|
558
|
+
|
|
559
|
+
// Find matching files for glob patterns
|
|
560
|
+
const matchingPaths = useMemo(() => {
|
|
561
|
+
if (!hasGlob || !directory) return [];
|
|
562
|
+
|
|
563
|
+
const allFiles = collectAllConfigFiles(directory);
|
|
564
|
+
const paths = allFiles
|
|
565
|
+
.map(f => f.path)
|
|
566
|
+
.filter(p => minimatch(p, resolvedPath, { matchBase: true }));
|
|
567
|
+
|
|
568
|
+
sortPathsNumerically(paths);
|
|
569
|
+
|
|
570
|
+
// Debug logging
|
|
571
|
+
console.log('[ParameterTable]', {
|
|
572
|
+
original: path,
|
|
573
|
+
resolved: resolvedPath,
|
|
574
|
+
baseDir,
|
|
575
|
+
allConfigFiles: allFiles.map(f => f.path),
|
|
576
|
+
matched: paths
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
return paths;
|
|
580
|
+
}, [hasGlob, directory, resolvedPath, path, baseDir]);
|
|
581
|
+
|
|
582
|
+
// If not a glob pattern, just render the single table
|
|
583
|
+
if (!hasGlob) {
|
|
584
|
+
return <SingleParameterTable path={resolvedPath} keys={keys} />;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Loading state for glob patterns
|
|
588
|
+
if (!directory) {
|
|
589
|
+
return (
|
|
590
|
+
<div className="my-6 p-4 rounded border border-border/50 bg-card/30">
|
|
591
|
+
<div className="flex items-center gap-2 text-muted-foreground/60">
|
|
592
|
+
<div className="w-3 h-3 border border-current border-t-transparent rounded-full animate-spin" />
|
|
593
|
+
<span className="text-[11px] font-mono">loading parameters...</span>
|
|
594
|
+
</div>
|
|
595
|
+
</div>
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// No matches
|
|
600
|
+
if (matchingPaths.length === 0) {
|
|
601
|
+
return (
|
|
602
|
+
<div className="my-6 p-3 rounded border border-border/50 bg-card/30">
|
|
603
|
+
<p className="text-[11px] font-mono text-muted-foreground">
|
|
604
|
+
no files matching: {resolvedPath}
|
|
605
|
+
<br />
|
|
606
|
+
<span className="text-muted-foreground/50">(base dir: {baseDir}, original: {path})</span>
|
|
607
|
+
</p>
|
|
608
|
+
</div>
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
613
|
+
usePreventSwipeNavigation(scrollRef);
|
|
614
|
+
|
|
615
|
+
// Breakout width based on count
|
|
616
|
+
const count = matchingPaths.length;
|
|
617
|
+
const breakoutClass = count >= 4
|
|
618
|
+
? 'w-[96vw] ml-[calc(-48vw+50%)]'
|
|
619
|
+
: count >= 2
|
|
620
|
+
? 'w-[75vw] ml-[calc(-37.5vw+50%)]'
|
|
621
|
+
: '';
|
|
622
|
+
|
|
623
|
+
return (
|
|
624
|
+
<div className={`my-6 ${breakoutClass}`}>
|
|
625
|
+
<div ref={scrollRef} className="flex gap-4 overflow-x-auto overscroll-x-contain pb-2">
|
|
626
|
+
{matchingPaths.map((filePath) => (
|
|
627
|
+
<div key={filePath} className="flex-none w-[280px]">
|
|
628
|
+
<SingleParameterTable
|
|
629
|
+
path={filePath}
|
|
630
|
+
keys={keys}
|
|
631
|
+
label={filePath.split('/').pop() || filePath}
|
|
632
|
+
withMargin={false}
|
|
633
|
+
/>
|
|
634
|
+
</div>
|
|
635
|
+
))}
|
|
636
|
+
</div>
|
|
637
|
+
</div>
|
|
638
|
+
);
|
|
639
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useParams } from "react-router-dom";
|
|
2
|
+
import { minimatch } from "minimatch";
|
|
2
3
|
import {
|
|
3
4
|
type PostEntry,
|
|
4
5
|
directoryToPostEntries,
|
|
@@ -11,6 +12,11 @@ import Loading from "./loading";
|
|
|
11
12
|
import { PostListItem } from "./post-list-item";
|
|
12
13
|
import veslxConfig from "virtual:veslx-config";
|
|
13
14
|
|
|
15
|
+
interface PostListProps {
|
|
16
|
+
/** Glob patterns to filter posts by name (e.g., ["01-*", "getting-*"]) */
|
|
17
|
+
globs?: string[] | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
// Helper to format name for display (e.g., "01-getting-started" → "Getting Started")
|
|
15
21
|
function formatName(name: string): string {
|
|
16
22
|
return name
|
|
@@ -36,7 +42,7 @@ function getLinkPath(post: PostEntry): string {
|
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
44
|
|
|
39
|
-
export function PostList() {
|
|
45
|
+
export function PostList({ globs = null }: PostListProps) {
|
|
40
46
|
const { "*": path = "." } = useParams();
|
|
41
47
|
|
|
42
48
|
const { directory, loading, error } = useDirectory(path)
|
|
@@ -72,6 +78,13 @@ export function PostList() {
|
|
|
72
78
|
// Filter out hidden and draft posts
|
|
73
79
|
posts = filterVisiblePosts(posts);
|
|
74
80
|
|
|
81
|
+
// Filter by glob patterns if provided
|
|
82
|
+
if (globs && globs.length > 0) {
|
|
83
|
+
posts = posts.filter(post =>
|
|
84
|
+
globs.some(pattern => minimatch(post.name, pattern, { matchBase: true }))
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
if (posts.length === 0) {
|
|
76
89
|
return (
|
|
77
90
|
<div className="py-24 text-center">
|