veslx 0.0.1
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/README.md +3 -0
- package/bin/lib/import-config.ts +13 -0
- package/bin/lib/init.ts +31 -0
- package/bin/lib/serve.ts +35 -0
- package/bin/lib/start.ts +40 -0
- package/bin/lib/stop.ts +24 -0
- package/bin/vesl.ts +41 -0
- package/components.json +20 -0
- package/eslint.config.js +23 -0
- package/index.html +17 -0
- package/package.json +89 -0
- package/plugin/README.md +21 -0
- package/plugin/package.json +26 -0
- package/plugin/src/cli.ts +30 -0
- package/plugin/src/client.tsx +224 -0
- package/plugin/src/lib.ts +268 -0
- package/plugin/src/plugin.ts +109 -0
- package/postcss.config.js +5 -0
- package/public/logo_dark.png +0 -0
- package/public/logo_light.png +0 -0
- package/src/App.tsx +21 -0
- package/src/components/front-matter.tsx +53 -0
- package/src/components/gallery/components/figure-caption.tsx +15 -0
- package/src/components/gallery/components/figure-header.tsx +20 -0
- package/src/components/gallery/components/lightbox.tsx +106 -0
- package/src/components/gallery/components/loading-image.tsx +48 -0
- package/src/components/gallery/hooks/use-gallery-images.ts +103 -0
- package/src/components/gallery/hooks/use-lightbox.ts +40 -0
- package/src/components/gallery/index.tsx +134 -0
- package/src/components/gallery/lib/render-math-in-text.tsx +47 -0
- package/src/components/header.tsx +68 -0
- package/src/components/index.ts +5 -0
- package/src/components/loading.tsx +16 -0
- package/src/components/mdx-components.tsx +163 -0
- package/src/components/mode-toggle.tsx +44 -0
- package/src/components/page-error.tsx +59 -0
- package/src/components/parameter-badge.tsx +78 -0
- package/src/components/parameter-table.tsx +420 -0
- package/src/components/post-list.tsx +148 -0
- package/src/components/running-bar.tsx +21 -0
- package/src/components/runtime-mdx.tsx +82 -0
- package/src/components/slide.tsx +11 -0
- package/src/components/theme-provider.tsx +6 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/breadcrumb.tsx +115 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +79 -0
- package/src/components/ui/carousel.tsx +260 -0
- package/src/components/ui/dropdown-menu.tsx +198 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/kbd.tsx +22 -0
- package/src/components/ui/select.tsx +158 -0
- package/src/components/ui/separator.tsx +29 -0
- package/src/components/ui/shadcn-io/code-block/index.tsx +620 -0
- package/src/components/ui/shadcn-io/code-block/server.tsx +63 -0
- package/src/components/ui/sheet.tsx +140 -0
- package/src/components/ui/sidebar.tsx +771 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/tooltip.tsx +28 -0
- package/src/components/welcome.tsx +21 -0
- package/src/hooks/use-key-bindings.ts +72 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/index.css +279 -0
- package/src/lib/constants.ts +10 -0
- package/src/lib/format-date.tsx +6 -0
- package/src/lib/format-file-size.ts +10 -0
- package/src/lib/parameter-utils.ts +134 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/src/pages/home.tsx +39 -0
- package/src/pages/post.tsx +65 -0
- package/src/pages/slides.tsx +173 -0
- package/tailwind.config.js +136 -0
- package/test-content/.vesl.json +49 -0
- package/test-content/README.md +33 -0
- package/test-content/test-post/README.mdx +7 -0
- package/test-content/test-slides/SLIDES.mdx +8 -0
- package/tsconfig.app.json +32 -0
- package/tsconfig.json +15 -0
- package/tsconfig.node.json +25 -0
- package/vesl.config.ts +4 -0
- package/vite.config.ts +54 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useTheme } from "next-themes";
|
|
3
|
+
import { useDirectory } from "../../../../plugin/src/client";
|
|
4
|
+
import { FileEntry } from "../../../../plugin/src/lib";
|
|
5
|
+
import { minimatch } from "minimatch";
|
|
6
|
+
|
|
7
|
+
function sortPathsNumerically(paths: string[]): void {
|
|
8
|
+
paths.sort((a, b) => {
|
|
9
|
+
const nums = (s: string) => (s.match(/\d+/g) || []).map(Number);
|
|
10
|
+
const na = nums(a);
|
|
11
|
+
const nb = nums(b);
|
|
12
|
+
const len = Math.max(na.length, nb.length);
|
|
13
|
+
for (let i = 0; i < len; i++) {
|
|
14
|
+
const diff = (na[i] ?? 0) - (nb[i] ?? 0);
|
|
15
|
+
if (diff !== 0) return diff;
|
|
16
|
+
}
|
|
17
|
+
return a.localeCompare(b);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function filterPathsByTheme(paths: string[], theme: string | undefined): string[] {
|
|
22
|
+
const pathGroups = new Map<string, { light?: string; dark?: string; original?: string }>();
|
|
23
|
+
|
|
24
|
+
paths.forEach(path => {
|
|
25
|
+
if (path.endsWith('_light.png')) {
|
|
26
|
+
const baseName = path.replace('_light.png', '');
|
|
27
|
+
const group = pathGroups.get(baseName) || {};
|
|
28
|
+
group.light = path;
|
|
29
|
+
pathGroups.set(baseName, group);
|
|
30
|
+
} else if (path.endsWith('_dark.png')) {
|
|
31
|
+
const baseName = path.replace('_dark.png', '');
|
|
32
|
+
const group = pathGroups.get(baseName) || {};
|
|
33
|
+
group.dark = path;
|
|
34
|
+
pathGroups.set(baseName, group);
|
|
35
|
+
} else {
|
|
36
|
+
pathGroups.set(path, { original: path });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const filtered: string[] = [];
|
|
41
|
+
pathGroups.forEach((group, baseName) => {
|
|
42
|
+
if (group.original) {
|
|
43
|
+
filtered.push(group.original);
|
|
44
|
+
} else {
|
|
45
|
+
const isDark = theme === 'dark';
|
|
46
|
+
const preferredPath = isDark ? group.dark : group.light;
|
|
47
|
+
const fallbackPath = isDark ? group.light : group.dark;
|
|
48
|
+
filtered.push(preferredPath || fallbackPath || baseName);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return filtered;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
export function useGalleryImages({
|
|
57
|
+
path,
|
|
58
|
+
globs = null,
|
|
59
|
+
limit,
|
|
60
|
+
page = 0,
|
|
61
|
+
}: {
|
|
62
|
+
path?: string;
|
|
63
|
+
globs?: string[] | null;
|
|
64
|
+
limit?: number | null;
|
|
65
|
+
page?: number;
|
|
66
|
+
}) {
|
|
67
|
+
const { resolvedTheme } = useTheme();
|
|
68
|
+
|
|
69
|
+
let resolvedPath = path;
|
|
70
|
+
|
|
71
|
+
const { directory } = useDirectory(resolvedPath);
|
|
72
|
+
|
|
73
|
+
const paths = useMemo(() => {
|
|
74
|
+
if (!directory) return [];
|
|
75
|
+
|
|
76
|
+
const imageChildren = directory.children.filter((child): child is FileEntry => {
|
|
77
|
+
return !!child.name.match(/\.(png|jpeg|gif|svg|webp)$/i) && child.type === "file";
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
let imagePaths = imageChildren.map(child => child.path);
|
|
81
|
+
|
|
82
|
+
if (globs && globs.length > 0) {
|
|
83
|
+
imagePaths = imagePaths.filter(p => {
|
|
84
|
+
return globs.some(glob => minimatch(p.split('/').pop() || '', glob));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
sortPathsNumerically(imagePaths);
|
|
89
|
+
let filtered = filterPathsByTheme(imagePaths, resolvedTheme);
|
|
90
|
+
|
|
91
|
+
if (limit) {
|
|
92
|
+
filtered = filtered.slice(page * limit, (page + 1) * limit);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return filtered;
|
|
96
|
+
}, [directory, globs, resolvedTheme, limit, page]);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
paths,
|
|
100
|
+
isLoading: !directory,
|
|
101
|
+
isEmpty: directory !== undefined && paths.length === 0,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useKeyBindings } from "@/hooks/use-key-bindings";
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
|
+
|
|
4
|
+
export function useLightbox(totalImages: number) {
|
|
5
|
+
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
|
6
|
+
|
|
7
|
+
const open = useCallback((index: number) => {
|
|
8
|
+
setSelectedIndex(index);
|
|
9
|
+
}, []);
|
|
10
|
+
|
|
11
|
+
const close = useCallback(() => {
|
|
12
|
+
setSelectedIndex(null);
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
15
|
+
const goToPrevious = useCallback(() => {
|
|
16
|
+
setSelectedIndex(prev => prev !== null && prev > 0 ? prev - 1 : prev);
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
const goToNext = useCallback(() => {
|
|
20
|
+
setSelectedIndex(prev => prev !== null && prev < totalImages - 1 ? prev + 1 : prev);
|
|
21
|
+
}, [totalImages]);
|
|
22
|
+
|
|
23
|
+
useKeyBindings(
|
|
24
|
+
[
|
|
25
|
+
{ key: "Escape", action: close },
|
|
26
|
+
{ key: "ArrowLeft", action: goToPrevious },
|
|
27
|
+
{ key: "ArrowRight", action: goToNext },
|
|
28
|
+
],
|
|
29
|
+
{ enabled: () => selectedIndex !== null }
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
selectedIndex,
|
|
34
|
+
open,
|
|
35
|
+
close,
|
|
36
|
+
goToPrevious,
|
|
37
|
+
goToNext,
|
|
38
|
+
isOpen: selectedIndex !== null,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { Image } from "lucide-react";
|
|
3
|
+
import { Lightbox, LightboxImage } from "@/components/gallery/components/lightbox";
|
|
4
|
+
import { useGalleryImages } from "./hooks/use-gallery-images";
|
|
5
|
+
import { useLightbox } from "./hooks/use-lightbox";
|
|
6
|
+
import { LoadingImage } from "./components/loading-image";
|
|
7
|
+
import { FigureHeader } from "./components/figure-header";
|
|
8
|
+
import { FigureCaption } from "./components/figure-caption";
|
|
9
|
+
import {
|
|
10
|
+
Carousel,
|
|
11
|
+
CarouselContent,
|
|
12
|
+
CarouselItem,
|
|
13
|
+
CarouselNext,
|
|
14
|
+
CarouselPrevious,
|
|
15
|
+
} from "@/components/ui/carousel"
|
|
16
|
+
|
|
17
|
+
function getImageLabel(path: string): string {
|
|
18
|
+
const filename = path.split('/').pop() || path;
|
|
19
|
+
return filename
|
|
20
|
+
.replace(/\.(png|jpg|jpeg|gif|svg|webp)$/i, '')
|
|
21
|
+
.replace(/[_-]/g, ' ')
|
|
22
|
+
.replace(/\s+/g, ' ')
|
|
23
|
+
.trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getImageUrl(path: string): string {
|
|
27
|
+
return `/raw/${path}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function Gallery({
|
|
31
|
+
path,
|
|
32
|
+
globs = null,
|
|
33
|
+
caption,
|
|
34
|
+
captionLabel,
|
|
35
|
+
title,
|
|
36
|
+
subtitle,
|
|
37
|
+
limit = null,
|
|
38
|
+
page = 0,
|
|
39
|
+
}: {
|
|
40
|
+
path?: string;
|
|
41
|
+
globs?: string[] | null;
|
|
42
|
+
caption?: string;
|
|
43
|
+
captionLabel?: string;
|
|
44
|
+
title?: string;
|
|
45
|
+
subtitle?: string;
|
|
46
|
+
limit?: number | null;
|
|
47
|
+
page?: number;
|
|
48
|
+
}) {
|
|
49
|
+
const { paths, isLoading, isEmpty } = useGalleryImages({
|
|
50
|
+
path,
|
|
51
|
+
globs,
|
|
52
|
+
limit,
|
|
53
|
+
page: page,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const lightbox = useLightbox(paths.length);
|
|
57
|
+
|
|
58
|
+
const images: LightboxImage[] = useMemo(() =>
|
|
59
|
+
paths.map(p => ({ src: getImageUrl(p), label: getImageLabel(p) })),
|
|
60
|
+
[paths]
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (isLoading) {
|
|
64
|
+
return (
|
|
65
|
+
<div className="not-prose py-4 md:py-6">
|
|
66
|
+
<div className="grid grid-cols-3 gap-2 sm:gap-4 max-w-[var(--gallery-width)] mx-auto">
|
|
67
|
+
{[...Array(3)].map((_, i) => (
|
|
68
|
+
<div
|
|
69
|
+
key={i}
|
|
70
|
+
className="aspect-[4/3] bg-muted/30 animate-pulse"
|
|
71
|
+
style={{ animationDelay: `${i * 100}ms` }}
|
|
72
|
+
/>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isEmpty) {
|
|
80
|
+
return (
|
|
81
|
+
<div className="not-prose py-16 text-center">
|
|
82
|
+
<div className="inline-flex items-center gap-3 text-muted-foreground/60">
|
|
83
|
+
<Image className="h-4 w-4" />
|
|
84
|
+
<span className="font-mono text-sm tracking-wide">no image(s) found</span>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<>
|
|
92
|
+
<div className="rounded-lg not-prose flex flex-col gap-0 relative p-4 -mx-[calc((var(--gallery-width)-var(--content-width))/2+var(--page-padding))]">
|
|
93
|
+
<FigureHeader title={title} subtitle={subtitle} />
|
|
94
|
+
|
|
95
|
+
<Carousel>
|
|
96
|
+
<CarouselContent>
|
|
97
|
+
{images.map((img, index) => (
|
|
98
|
+
<CarouselItem
|
|
99
|
+
key={index}
|
|
100
|
+
className="mx-auto md:basis-1/2 lg:basis-1/3 cursor-pointer transition-transform duration-700 ease-out-expo hover:scale-[1.02]"
|
|
101
|
+
// wrapperClassName="h-full"
|
|
102
|
+
onClick={() => lightbox.open(index)}
|
|
103
|
+
>
|
|
104
|
+
<LoadingImage
|
|
105
|
+
src={img.src}
|
|
106
|
+
alt={img.label}
|
|
107
|
+
/>
|
|
108
|
+
</CarouselItem>
|
|
109
|
+
))}
|
|
110
|
+
</CarouselContent>
|
|
111
|
+
|
|
112
|
+
{images.length > 3 && (
|
|
113
|
+
<>
|
|
114
|
+
<CarouselPrevious />
|
|
115
|
+
<CarouselNext />
|
|
116
|
+
</>
|
|
117
|
+
)}
|
|
118
|
+
</Carousel>
|
|
119
|
+
|
|
120
|
+
<FigureCaption caption={caption} label={captionLabel} />
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{lightbox.isOpen && lightbox.selectedIndex !== null && (
|
|
124
|
+
<Lightbox
|
|
125
|
+
images={images}
|
|
126
|
+
selectedIndex={lightbox.selectedIndex}
|
|
127
|
+
onClose={lightbox.close}
|
|
128
|
+
onPrevious={lightbox.goToPrevious}
|
|
129
|
+
onNext={lightbox.goToNext}
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
</>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import katex from "katex";
|
|
3
|
+
|
|
4
|
+
export function renderMathInText(text: string): ReactNode {
|
|
5
|
+
// Match $...$ for inline math and $$...$$ for display math
|
|
6
|
+
const parts: ReactNode[] = [];
|
|
7
|
+
let lastIndex = 0;
|
|
8
|
+
// Match display math ($$...$$) first, then inline math ($...$)
|
|
9
|
+
const regex = /\$\$([^$]+)\$\$|\$([^$]+)\$/g;
|
|
10
|
+
let match;
|
|
11
|
+
let key = 0;
|
|
12
|
+
|
|
13
|
+
while ((match = regex.exec(text)) !== null) {
|
|
14
|
+
// Add text before this match
|
|
15
|
+
if (match.index > lastIndex) {
|
|
16
|
+
parts.push(text.slice(lastIndex, match.index));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isDisplay = match[1] !== undefined;
|
|
20
|
+
const mathContent = match[1] || match[2];
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const html = katex.renderToString(mathContent, {
|
|
24
|
+
displayMode: isDisplay,
|
|
25
|
+
throwOnError: false,
|
|
26
|
+
});
|
|
27
|
+
parts.push(
|
|
28
|
+
<span
|
|
29
|
+
key={key++}
|
|
30
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
} catch {
|
|
34
|
+
// If KaTeX fails, just show the original text
|
|
35
|
+
parts.push(match[0]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
lastIndex = match.index + match[0].length;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Add remaining text
|
|
42
|
+
if (lastIndex < text.length) {
|
|
43
|
+
parts.push(text.slice(lastIndex));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return parts.length === 1 && typeof parts[0] === 'string' ? parts[0] : <>{parts}</>;
|
|
47
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Link } from "react-router-dom";
|
|
2
|
+
import { ModeToggle } from "./mode-toggle";
|
|
3
|
+
import { SiGithub } from "@icons-pack/react-simple-icons";
|
|
4
|
+
import { ChevronUp, ChevronDown } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
interface HeaderProps {
|
|
7
|
+
slideControls?: {
|
|
8
|
+
current: number;
|
|
9
|
+
total: number;
|
|
10
|
+
onPrevious: () => void;
|
|
11
|
+
onNext: () => void;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Header({ slideControls }: HeaderProps = {}) {
|
|
16
|
+
return (
|
|
17
|
+
<header className="print:hidden fixed top-0 left-0 right-0 z-40">
|
|
18
|
+
<div className="mx-auto w-full px-[var(--page-padding)] flex items-center gap-8 py-4">
|
|
19
|
+
<nav className="flex items-center gap-1">
|
|
20
|
+
<Link
|
|
21
|
+
to="/"
|
|
22
|
+
className="rounded-lg font-mono py-1.5 text-sm font-medium text-muted-foreground hover:underline"
|
|
23
|
+
>
|
|
24
|
+
pl
|
|
25
|
+
</Link>
|
|
26
|
+
</nav>
|
|
27
|
+
|
|
28
|
+
<div className="flex-1" />
|
|
29
|
+
|
|
30
|
+
{/* Slide navigation controls */}
|
|
31
|
+
{slideControls && (
|
|
32
|
+
<nav className="flex items-center gap-1">
|
|
33
|
+
<button
|
|
34
|
+
onClick={slideControls.onPrevious}
|
|
35
|
+
className="p-1.5 text-muted-foreground/70 hover:text-foreground transition-colors duration-200"
|
|
36
|
+
title="Previous slide (↑)"
|
|
37
|
+
>
|
|
38
|
+
<ChevronUp className="h-4 w-4" />
|
|
39
|
+
</button>
|
|
40
|
+
<span className="font-mono text-xs text-muted-foreground/70 tabular-nums min-w-[3ch] text-center">
|
|
41
|
+
{slideControls.current + 1}/{slideControls.total}
|
|
42
|
+
</span>
|
|
43
|
+
<button
|
|
44
|
+
onClick={slideControls.onNext}
|
|
45
|
+
className="p-1.5 text-muted-foreground/70 hover:text-foreground transition-colors duration-200"
|
|
46
|
+
title="Next slide (↓)"
|
|
47
|
+
>
|
|
48
|
+
<ChevronDown className="h-4 w-4" />
|
|
49
|
+
</button>
|
|
50
|
+
</nav>
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
{/* Navigation */}
|
|
54
|
+
<nav className="flex items-center gap-2">
|
|
55
|
+
<Link
|
|
56
|
+
to="https://github.com/eoinmurray/pinglab"
|
|
57
|
+
target="_blank"
|
|
58
|
+
className="text-muted-foreground/70 hover:text-foreground transition-colors duration-300"
|
|
59
|
+
aria-label="GitHub"
|
|
60
|
+
>
|
|
61
|
+
<SiGithub className="h-4 w-4" />
|
|
62
|
+
</Link>
|
|
63
|
+
<ModeToggle />
|
|
64
|
+
</nav>
|
|
65
|
+
</div>
|
|
66
|
+
</header>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export default function Loading() {
|
|
2
|
+
return (
|
|
3
|
+
<main className="min-h-screen flex items-center justify-center bg-background">
|
|
4
|
+
<div className="flex flex-col items-center gap-6 animate-fade-in">
|
|
5
|
+
<div className="flex items-center gap-3">
|
|
6
|
+
<div className="w-1.5 h-1.5 bg-primary/60 rounded-full animate-pulse" />
|
|
7
|
+
<div className="w-1.5 h-1.5 bg-primary/40 rounded-full animate-pulse" style={{ animationDelay: '150ms' }} />
|
|
8
|
+
<div className="w-1.5 h-1.5 bg-primary/20 rounded-full animate-pulse" style={{ animationDelay: '300ms' }} />
|
|
9
|
+
</div>
|
|
10
|
+
<p className="font-mono text-xs text-muted-foreground/50 tracking-widest uppercase">
|
|
11
|
+
loading
|
|
12
|
+
</p>
|
|
13
|
+
</div>
|
|
14
|
+
</main>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
|
|
2
|
+
import Gallery from '@/components/gallery'
|
|
3
|
+
import { ParameterTable } from '@/components/parameter-table'
|
|
4
|
+
import { ParameterBadge } from '@/components/parameter-badge'
|
|
5
|
+
|
|
6
|
+
function generateId(children: unknown): string {
|
|
7
|
+
return children
|
|
8
|
+
?.toString()
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
11
|
+
.replace(/\s+/g, '-') ?? ''
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Shared MDX components - lab notebook / coder aesthetic
|
|
15
|
+
export const mdxComponents = {
|
|
16
|
+
Gallery,
|
|
17
|
+
|
|
18
|
+
ParameterTable,
|
|
19
|
+
|
|
20
|
+
ParameterBadge,
|
|
21
|
+
|
|
22
|
+
// Headings - clean sans-serif
|
|
23
|
+
h1: (props: React.HTMLAttributes<HTMLHeadingElement>) => {
|
|
24
|
+
const id = generateId(props.children)
|
|
25
|
+
return (
|
|
26
|
+
<h1
|
|
27
|
+
id={id}
|
|
28
|
+
className="text-2xl font-semibold tracking-tight mt-12 mb-4 first:mt-0"
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
)
|
|
32
|
+
},
|
|
33
|
+
h2: (props: React.HTMLAttributes<HTMLHeadingElement>) => {
|
|
34
|
+
const id = generateId(props.children)
|
|
35
|
+
return (
|
|
36
|
+
<h2
|
|
37
|
+
id={id}
|
|
38
|
+
className="text-xl font-semibold tracking-tight mt-10 mb-3 pb-2 border-b border-border"
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
},
|
|
43
|
+
h3: (props: React.HTMLAttributes<HTMLHeadingElement>) => {
|
|
44
|
+
const id = generateId(props.children)
|
|
45
|
+
return (
|
|
46
|
+
<h3
|
|
47
|
+
id={id}
|
|
48
|
+
className="text-lg font-medium tracking-tight mt-8 mb-2"
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
},
|
|
53
|
+
h4: (props: React.HTMLAttributes<HTMLHeadingElement>) => {
|
|
54
|
+
const id = generateId(props.children)
|
|
55
|
+
return (
|
|
56
|
+
<h4
|
|
57
|
+
id={id}
|
|
58
|
+
className="text-base font-medium mt-6 mb-2"
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
},
|
|
63
|
+
h5: (props: React.HTMLAttributes<HTMLHeadingElement>) => {
|
|
64
|
+
const id = generateId(props.children)
|
|
65
|
+
return (
|
|
66
|
+
<h5
|
|
67
|
+
id={id}
|
|
68
|
+
className="text-sm font-medium mt-4 mb-1"
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
)
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Code blocks - IDE/terminal style
|
|
75
|
+
pre: (props: React.HTMLAttributes<HTMLPreElement>) => (
|
|
76
|
+
<pre
|
|
77
|
+
className="not-prose w-full overflow-x-auto p-4 text-sm bg-muted/50 border border-border rounded-md font-mono my-6"
|
|
78
|
+
{...props}
|
|
79
|
+
/>
|
|
80
|
+
),
|
|
81
|
+
code: (props: React.HTMLAttributes<HTMLElement> & { className?: string }) => {
|
|
82
|
+
const isInline = !props.className?.includes('language-')
|
|
83
|
+
if (isInline) {
|
|
84
|
+
return (
|
|
85
|
+
<code
|
|
86
|
+
className="font-mono text-[0.85em] bg-muted px-1.5 py-0.5 rounded text-primary"
|
|
87
|
+
{...props}
|
|
88
|
+
/>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
return <code {...props} />
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// Blockquote
|
|
95
|
+
blockquote: (props: React.HTMLAttributes<HTMLQuoteElement>) => (
|
|
96
|
+
<blockquote
|
|
97
|
+
className="border-l-2 border-primary pl-4 my-6 text-muted-foreground"
|
|
98
|
+
{...props}
|
|
99
|
+
/>
|
|
100
|
+
),
|
|
101
|
+
|
|
102
|
+
// Lists
|
|
103
|
+
ul: (props: React.HTMLAttributes<HTMLUListElement>) => (
|
|
104
|
+
<ul className="my-4 ml-6 list-disc marker:text-muted-foreground" {...props} />
|
|
105
|
+
),
|
|
106
|
+
ol: (props: React.HTMLAttributes<HTMLOListElement>) => (
|
|
107
|
+
<ol className="my-4 ml-6 list-decimal marker:text-muted-foreground" {...props} />
|
|
108
|
+
),
|
|
109
|
+
li: (props: React.HTMLAttributes<HTMLLIElement>) => (
|
|
110
|
+
<li className="mt-1.5" {...props} />
|
|
111
|
+
),
|
|
112
|
+
|
|
113
|
+
// Links
|
|
114
|
+
a: (props: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
|
|
115
|
+
<a
|
|
116
|
+
className="text-primary hover:underline underline-offset-2"
|
|
117
|
+
{...props}
|
|
118
|
+
/>
|
|
119
|
+
),
|
|
120
|
+
|
|
121
|
+
// Tables
|
|
122
|
+
table: (props: React.TableHTMLAttributes<HTMLTableElement>) => (
|
|
123
|
+
<div className="not-prose my-6 overflow-x-auto border border-border rounded-md">
|
|
124
|
+
<table className="w-full text-sm border-collapse" {...props} />
|
|
125
|
+
</div>
|
|
126
|
+
),
|
|
127
|
+
thead: (props: React.HTMLAttributes<HTMLTableSectionElement>) => (
|
|
128
|
+
<thead className="bg-muted/50" {...props} />
|
|
129
|
+
),
|
|
130
|
+
tbody: (props: React.HTMLAttributes<HTMLTableSectionElement>) => (
|
|
131
|
+
<tbody {...props} />
|
|
132
|
+
),
|
|
133
|
+
tr: (props: React.HTMLAttributes<HTMLTableRowElement>) => (
|
|
134
|
+
<tr className="border-b border-border last:border-b-0" {...props} />
|
|
135
|
+
),
|
|
136
|
+
th: (props: React.ThHTMLAttributes<HTMLTableCellElement>) => (
|
|
137
|
+
<th
|
|
138
|
+
className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider"
|
|
139
|
+
{...props}
|
|
140
|
+
/>
|
|
141
|
+
),
|
|
142
|
+
td: (props: React.TdHTMLAttributes<HTMLTableCellElement>) => (
|
|
143
|
+
<td className="px-4 py-3 align-top" {...props} />
|
|
144
|
+
),
|
|
145
|
+
|
|
146
|
+
// Horizontal rule
|
|
147
|
+
hr: (props: React.HTMLAttributes<HTMLHRElement>) => (
|
|
148
|
+
<hr className="my-8 border-t border-border" {...props} />
|
|
149
|
+
),
|
|
150
|
+
|
|
151
|
+
// Paragraph
|
|
152
|
+
p: (props: React.HTMLAttributes<HTMLParagraphElement>) => (
|
|
153
|
+
<p className="leading-relaxed mb-4 last:mb-0" {...props} />
|
|
154
|
+
),
|
|
155
|
+
|
|
156
|
+
// Strong/emphasis
|
|
157
|
+
strong: (props: React.HTMLAttributes<HTMLElement>) => (
|
|
158
|
+
<strong className="font-semibold" {...props} />
|
|
159
|
+
),
|
|
160
|
+
em: (props: React.HTMLAttributes<HTMLElement>) => (
|
|
161
|
+
<em className="italic" {...props} />
|
|
162
|
+
),
|
|
163
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Moon, Sun, Monitor } from "lucide-react"
|
|
2
|
+
import { useTheme } from "next-themes"
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
DropdownMenuContent,
|
|
6
|
+
DropdownMenuRadioGroup,
|
|
7
|
+
DropdownMenuRadioItem,
|
|
8
|
+
DropdownMenuTrigger,
|
|
9
|
+
} from "@/components/ui/dropdown-menu"
|
|
10
|
+
|
|
11
|
+
export function ModeToggle() {
|
|
12
|
+
const { theme, setTheme } = useTheme()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<DropdownMenu>
|
|
16
|
+
<DropdownMenuTrigger asChild>
|
|
17
|
+
<button
|
|
18
|
+
className="relative p-2 text-muted-foreground/70 hover:text-foreground transition-colors duration-300"
|
|
19
|
+
aria-label="Toggle theme"
|
|
20
|
+
>
|
|
21
|
+
<Sun className={`h-4 w-4 transition-all duration-300 ${theme === "light" ? "scale-100 rotate-0" : "scale-0 -rotate-90 absolute top-2 left-2"}`} />
|
|
22
|
+
<Moon className={`h-4 w-4 transition-all duration-300 ${theme === "dark" ? "scale-100 rotate-0" : "scale-0 rotate-90 absolute top-2 left-2"}`} />
|
|
23
|
+
<Monitor className={`h-4 w-4 transition-all duration-300 ${theme === "system" || !theme ? "scale-100" : "scale-0 absolute top-2 left-2"}`} />
|
|
24
|
+
</button>
|
|
25
|
+
</DropdownMenuTrigger>
|
|
26
|
+
<DropdownMenuContent align="end">
|
|
27
|
+
<DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
|
|
28
|
+
<DropdownMenuRadioItem value="light">
|
|
29
|
+
<Sun className="mr-2 h-4 w-4" />
|
|
30
|
+
Light
|
|
31
|
+
</DropdownMenuRadioItem>
|
|
32
|
+
<DropdownMenuRadioItem value="dark">
|
|
33
|
+
<Moon className="mr-2 h-4 w-4" />
|
|
34
|
+
Dark
|
|
35
|
+
</DropdownMenuRadioItem>
|
|
36
|
+
<DropdownMenuRadioItem value="system">
|
|
37
|
+
<Monitor className="mr-2 h-4 w-4" />
|
|
38
|
+
System
|
|
39
|
+
</DropdownMenuRadioItem>
|
|
40
|
+
</DropdownMenuRadioGroup>
|
|
41
|
+
</DropdownMenuContent>
|
|
42
|
+
</DropdownMenu>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
|
|
2
|
+
import { DirectoryError } from "../../plugin/src/client";
|
|
3
|
+
|
|
4
|
+
export function ErrorDisplay({ error, path }: { error: DirectoryError; path: string }) {
|
|
5
|
+
const containerClass = "min-h-screen bg-background container mx-auto max-w-[var(--content-width)] py-24 px-[var(--page-padding)]";
|
|
6
|
+
|
|
7
|
+
switch (error.type) {
|
|
8
|
+
case 'config_not_found':
|
|
9
|
+
return (
|
|
10
|
+
<main className={containerClass}>
|
|
11
|
+
<div className="text-center space-y-4">
|
|
12
|
+
<h1 className="text-2xl font-semibold tracking-tight">Setup Required</h1>
|
|
13
|
+
<p className="text-muted-foreground">
|
|
14
|
+
Could not find <code className="font-mono text-sm bg-muted px-2 py-1">.veslx.json</code>
|
|
15
|
+
</p>
|
|
16
|
+
<p className="text-muted-foreground/70 text-sm">
|
|
17
|
+
Run the veslx build script to generate the directory index.
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
</main>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
case 'path_not_found':
|
|
24
|
+
return (
|
|
25
|
+
<main className={containerClass}>
|
|
26
|
+
<div className="text-center space-y-4">
|
|
27
|
+
<h1 className="font-mono text-6xl tracking-tighter text-muted-foreground/30">404</h1>
|
|
28
|
+
<p className="text-lg text-foreground">Page not found</p>
|
|
29
|
+
<p className="text-muted-foreground text-sm">
|
|
30
|
+
<code className="font-mono bg-muted px-2 py-1">{path}</code>
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
</main>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
case 'parse_error':
|
|
37
|
+
return (
|
|
38
|
+
<main className={containerClass}>
|
|
39
|
+
<div className="text-center space-y-4">
|
|
40
|
+
<h1 className="text-2xl font-semibold text-destructive">Configuration Error</h1>
|
|
41
|
+
<p className="text-muted-foreground">
|
|
42
|
+
Failed to parse <code className="font-mono text-sm bg-muted px-2 py-1">.veslx.json</code>
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
</main>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
case 'fetch_error':
|
|
49
|
+
default:
|
|
50
|
+
return (
|
|
51
|
+
<main className={containerClass}>
|
|
52
|
+
<div className="text-center space-y-4">
|
|
53
|
+
<h1 className="text-2xl font-semibold text-destructive">Error</h1>
|
|
54
|
+
<p className="text-muted-foreground">{error.message}</p>
|
|
55
|
+
</div>
|
|
56
|
+
</main>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|