veslx 0.1.5 → 0.1.15
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 +262 -55
- package/bin/lib/build.ts +65 -13
- package/bin/lib/import-config.ts +10 -9
- package/bin/lib/init.ts +21 -22
- package/bin/lib/serve.ts +66 -12
- package/bin/veslx.ts +2 -2
- package/dist/client/App.js +3 -9
- package/dist/client/App.js.map +1 -1
- package/dist/client/components/front-matter.js +11 -25
- package/dist/client/components/front-matter.js.map +1 -1
- package/dist/client/components/gallery/components/figure-caption.js +6 -4
- package/dist/client/components/gallery/components/figure-caption.js.map +1 -1
- package/dist/client/components/gallery/components/figure-header.js +3 -3
- package/dist/client/components/gallery/components/figure-header.js.map +1 -1
- package/dist/client/components/gallery/components/lightbox.js +13 -13
- package/dist/client/components/gallery/components/lightbox.js.map +1 -1
- package/dist/client/components/gallery/components/loading-image.js +11 -10
- package/dist/client/components/gallery/components/loading-image.js.map +1 -1
- package/dist/client/components/gallery/hooks/use-gallery-images.js +31 -7
- package/dist/client/components/gallery/hooks/use-gallery-images.js.map +1 -1
- package/dist/client/components/gallery/index.js +22 -15
- package/dist/client/components/gallery/index.js.map +1 -1
- package/dist/client/components/header.js +5 -3
- package/dist/client/components/header.js.map +1 -1
- package/dist/client/components/mdx-components.js +42 -8
- package/dist/client/components/mdx-components.js.map +1 -1
- package/dist/client/components/post-list.js +97 -90
- package/dist/client/components/post-list.js.map +1 -1
- package/dist/client/components/running-bar.js +1 -1
- package/dist/client/components/running-bar.js.map +1 -1
- package/dist/client/components/slide.js +18 -0
- package/dist/client/components/slide.js.map +1 -0
- package/dist/client/components/slides-renderer.js +7 -71
- package/dist/client/components/slides-renderer.js.map +1 -1
- package/dist/client/hooks/use-mdx-content.js +55 -9
- package/dist/client/hooks/use-mdx-content.js.map +1 -1
- package/dist/client/main.js +1 -0
- package/dist/client/main.js.map +1 -1
- package/dist/client/pages/content-router.js +19 -0
- package/dist/client/pages/content-router.js.map +1 -0
- package/dist/client/pages/home.js +11 -7
- package/dist/client/pages/home.js.map +1 -1
- package/dist/client/pages/post.js +8 -20
- package/dist/client/pages/post.js.map +1 -1
- package/dist/client/pages/slides.js +62 -86
- package/dist/client/pages/slides.js.map +1 -1
- package/dist/client/plugin/src/client.js +58 -96
- package/dist/client/plugin/src/client.js.map +1 -1
- package/dist/client/plugin/src/directory-tree.js +111 -0
- package/dist/client/plugin/src/directory-tree.js.map +1 -0
- package/index.html +1 -1
- package/package.json +34 -15
- package/plugin/src/client.tsx +64 -116
- package/plugin/src/directory-tree.ts +171 -0
- package/plugin/src/lib.ts +6 -249
- package/plugin/src/plugin.ts +93 -50
- package/plugin/src/remark-slides.ts +100 -0
- package/plugin/src/types.ts +22 -0
- package/src/App.tsx +3 -6
- package/src/components/front-matter.tsx +14 -29
- package/src/components/gallery/components/figure-caption.tsx +15 -7
- package/src/components/gallery/components/figure-header.tsx +3 -3
- package/src/components/gallery/components/lightbox.tsx +15 -13
- package/src/components/gallery/components/loading-image.tsx +15 -12
- package/src/components/gallery/hooks/use-gallery-images.ts +51 -10
- package/src/components/gallery/index.tsx +32 -26
- package/src/components/header.tsx +14 -9
- package/src/components/mdx-components.tsx +61 -8
- package/src/components/post-list.tsx +149 -115
- package/src/components/running-bar.tsx +1 -1
- package/src/components/slide.tsx +22 -5
- package/src/components/slides-renderer.tsx +7 -115
- package/src/components/welcome.tsx +11 -14
- package/src/hooks/use-mdx-content.ts +94 -9
- package/src/index.css +159 -0
- package/src/main.tsx +1 -0
- package/src/pages/content-router.tsx +27 -0
- package/src/pages/home.tsx +16 -2
- package/src/pages/post.tsx +10 -13
- package/src/pages/slides.tsx +75 -88
- package/src/vite-env.d.ts +7 -17
- package/vite.config.ts +25 -6
- package/dist/assets/README-NSyLDlyP.js +0 -7
- package/dist/assets/SLIDES-C12TOqNU.js +0 -10
- package/dist/assets/_virtual_content-modules-DK3Yb9K2.js +0 -2
- package/dist/assets/index-BUMwRZ7d.js +0 -468
- package/dist/assets/index-C8sJQuOZ.js +0 -1
- package/dist/assets/index-PspMxLnH.css +0 -1
- package/dist/index.html +0 -18
- package/dist/logo_dark.png +0 -0
- package/dist/logo_light.png +0 -0
- package/dist/raw/.veslx.json +0 -61
- package/dist/raw/README.md +0 -33
- package/dist/raw/test-post/Chart.tsx +0 -16
- package/dist/raw/test-post/README.mdx +0 -21
- package/dist/raw/test-slides/Counter.tsx +0 -25
- package/dist/raw/test-slides/SLIDES.mdx +0 -27
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { useTheme } from "next-themes";
|
|
3
|
+
import { useParams } from "react-router-dom";
|
|
3
4
|
import { useDirectory } from "../../../../plugin/src/client";
|
|
4
|
-
import { FileEntry } from "../../../../plugin/src/lib";
|
|
5
|
+
import { FileEntry, DirectoryEntry } from "../../../../plugin/src/lib";
|
|
5
6
|
import { minimatch } from "minimatch";
|
|
6
7
|
|
|
8
|
+
// Recursively collect all image files from a directory tree
|
|
9
|
+
function collectAllImages(entry: DirectoryEntry | FileEntry): FileEntry[] {
|
|
10
|
+
if (entry.type === "file") {
|
|
11
|
+
if (entry.name.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i)) {
|
|
12
|
+
return [entry];
|
|
13
|
+
}
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
// It's a directory - recurse into children
|
|
17
|
+
const images: FileEntry[] = [];
|
|
18
|
+
for (const child of entry.children || []) {
|
|
19
|
+
images.push(...collectAllImages(child));
|
|
20
|
+
}
|
|
21
|
+
return images;
|
|
22
|
+
}
|
|
23
|
+
|
|
7
24
|
function sortPathsNumerically(paths: string[]): void {
|
|
8
25
|
paths.sort((a, b) => {
|
|
9
26
|
const nums = (s: string) => (s.match(/\d+/g) || []).map(Number);
|
|
@@ -65,24 +82,48 @@ export function useGalleryImages({
|
|
|
65
82
|
page?: number;
|
|
66
83
|
}) {
|
|
67
84
|
const { resolvedTheme } = useTheme();
|
|
85
|
+
const { "*": routePath = "" } = useParams();
|
|
68
86
|
|
|
69
|
-
|
|
87
|
+
// Get the current post's directory from the route
|
|
88
|
+
// Route is like "04-components/README.mdx" -> "04-components"
|
|
89
|
+
// Or "gallery-examples" -> "gallery-examples"
|
|
90
|
+
const currentDir = routePath
|
|
91
|
+
.replace(/\/[^/]+\.mdx$/i, "") // Remove /filename.mdx
|
|
92
|
+
.replace(/\/$/, "") // Remove trailing slash
|
|
93
|
+
|| ".";
|
|
70
94
|
|
|
71
|
-
|
|
95
|
+
// Resolve the path relative to current directory
|
|
96
|
+
let resolvedPath = path;
|
|
97
|
+
if (path?.startsWith("./")) {
|
|
98
|
+
// Relative path like "./images" -> "gallery-examples/images"
|
|
99
|
+
const relativePart = path.slice(2);
|
|
100
|
+
resolvedPath = currentDir === "." ? relativePart : `${currentDir}/${relativePart}`;
|
|
101
|
+
} else if (path && !path.startsWith("/") && !path.includes("/")) {
|
|
102
|
+
// Simple name like "images" -> "gallery-examples/images"
|
|
103
|
+
resolvedPath = currentDir === "." ? path : `${currentDir}/${path}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// If only globs provided (no path), use root directory
|
|
107
|
+
const directoryPath = resolvedPath || ".";
|
|
108
|
+
const { directory } = useDirectory(directoryPath);
|
|
72
109
|
|
|
73
110
|
const paths = useMemo(() => {
|
|
74
111
|
if (!directory) return [];
|
|
75
112
|
|
|
76
|
-
|
|
77
|
-
return !!child.name.match(/\.(png|jpeg|gif|svg|webp)$/i) && child.type === "file";
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
let imagePaths = imageChildren.map(child => child.path);
|
|
113
|
+
let imagePaths: string[];
|
|
81
114
|
|
|
82
115
|
if (globs && globs.length > 0) {
|
|
83
|
-
|
|
84
|
-
|
|
116
|
+
// When globs provided, collect all images recursively and match against filename
|
|
117
|
+
const allImages = collectAllImages(directory);
|
|
118
|
+
imagePaths = allImages
|
|
119
|
+
.map(img => img.path)
|
|
120
|
+
.filter(p => globs.some(glob => minimatch(p, glob, { matchBase: true })));
|
|
121
|
+
} else {
|
|
122
|
+
// No globs - just get images from the specified directory
|
|
123
|
+
const imageChildren = directory.children.filter((child): child is FileEntry => {
|
|
124
|
+
return !!child.name.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i) && child.type === "file";
|
|
85
125
|
});
|
|
126
|
+
imagePaths = imageChildren.map(child => child.path);
|
|
86
127
|
}
|
|
87
128
|
|
|
88
129
|
sortPathsNumerically(imagePaths);
|
|
@@ -62,63 +62,69 @@ export default function Gallery({
|
|
|
62
62
|
|
|
63
63
|
if (isLoading) {
|
|
64
64
|
return (
|
|
65
|
-
<
|
|
66
|
-
<div className="grid grid-cols-3 gap-
|
|
65
|
+
<figure className="not-prose py-6 md:py-8">
|
|
66
|
+
<div className="grid grid-cols-3 gap-3 max-w-[var(--gallery-width)] mx-auto">
|
|
67
67
|
{[...Array(3)].map((_, i) => (
|
|
68
68
|
<div
|
|
69
69
|
key={i}
|
|
70
|
-
className="aspect-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
className="aspect-square rounded-sm bg-muted/20 relative overflow-hidden"
|
|
71
|
+
>
|
|
72
|
+
<div
|
|
73
|
+
className="absolute inset-0 bg-gradient-to-r from-transparent via-muted/30 to-transparent animate-shimmer"
|
|
74
|
+
style={{ animationDelay: `${i * 150}ms` }}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
73
77
|
))}
|
|
74
78
|
</div>
|
|
75
|
-
</
|
|
79
|
+
</figure>
|
|
76
80
|
);
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
if (isEmpty) {
|
|
80
84
|
return (
|
|
81
|
-
<
|
|
82
|
-
<div className="inline-flex items-center gap-
|
|
83
|
-
<Image className="h-
|
|
84
|
-
<span className="font-mono text-
|
|
85
|
+
<figure className="not-prose py-12 text-center">
|
|
86
|
+
<div className="inline-flex items-center gap-2.5 text-muted-foreground/40">
|
|
87
|
+
<Image className="h-3.5 w-3.5" strokeWidth={1.5} />
|
|
88
|
+
<span className="font-mono text-xs uppercase tracking-widest">No images</span>
|
|
85
89
|
</div>
|
|
86
|
-
</
|
|
90
|
+
</figure>
|
|
87
91
|
);
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
return (
|
|
91
95
|
<>
|
|
92
|
-
<
|
|
96
|
+
<figure className="not-prose relative py-6 md:py-8 -mx-[calc((var(--gallery-width)-var(--content-width))/2+var(--page-padding))]">
|
|
93
97
|
<FigureHeader title={title} subtitle={subtitle} />
|
|
94
98
|
|
|
95
|
-
<Carousel>
|
|
96
|
-
<CarouselContent>
|
|
99
|
+
<Carousel className="w-full">
|
|
100
|
+
<CarouselContent className={`-ml-2 md:-ml-3 ${images.length < 3 ? 'justify-center' : ''}`}>
|
|
97
101
|
{images.map((img, index) => (
|
|
98
|
-
<CarouselItem
|
|
99
|
-
key={index}
|
|
100
|
-
className=
|
|
101
|
-
// wrapperClassName="h-full"
|
|
102
|
+
<CarouselItem
|
|
103
|
+
key={index}
|
|
104
|
+
className={`pl-2 md:pl-3 md:basis-1/2 lg:basis-1/3 cursor-pointer group ${images.length < 3 ? 'flex-none' : ''}`}
|
|
102
105
|
onClick={() => lightbox.open(index)}
|
|
103
106
|
>
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
<div className="aspect-square overflow-hidden rounded-sm ring-1 ring-border/50 transition-all duration-300 group-hover:ring-border group-hover:shadow-lg bg-muted/10">
|
|
108
|
+
<LoadingImage
|
|
109
|
+
src={img.src}
|
|
110
|
+
alt={img.label}
|
|
111
|
+
className="object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
108
114
|
</CarouselItem>
|
|
109
115
|
))}
|
|
110
116
|
</CarouselContent>
|
|
111
117
|
|
|
112
118
|
{images.length > 3 && (
|
|
113
119
|
<>
|
|
114
|
-
<CarouselPrevious />
|
|
115
|
-
<CarouselNext />
|
|
120
|
+
<CarouselPrevious className="left-4 bg-background/80 backdrop-blur-sm border-border/50 hover:bg-background hover:border-border" />
|
|
121
|
+
<CarouselNext className="right-4 bg-background/80 backdrop-blur-sm border-border/50 hover:bg-background hover:border-border" />
|
|
116
122
|
</>
|
|
117
|
-
)}
|
|
123
|
+
)}
|
|
118
124
|
</Carousel>
|
|
119
125
|
|
|
120
126
|
<FigureCaption caption={caption} label={captionLabel} />
|
|
121
|
-
</
|
|
127
|
+
</figure>
|
|
122
128
|
|
|
123
129
|
{lightbox.isOpen && lightbox.selectedIndex !== null && (
|
|
124
130
|
<Lightbox
|
|
@@ -2,6 +2,7 @@ import { Link } 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
|
+
import siteConfig from "virtual:veslx-config";
|
|
5
6
|
|
|
6
7
|
interface HeaderProps {
|
|
7
8
|
slideControls?: {
|
|
@@ -13,6 +14,8 @@ interface HeaderProps {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function Header({ slideControls }: HeaderProps = {}) {
|
|
17
|
+
const config = siteConfig;
|
|
18
|
+
|
|
16
19
|
return (
|
|
17
20
|
<header className="print:hidden fixed top-0 left-0 right-0 z-40">
|
|
18
21
|
<div className="mx-auto w-full px-[var(--page-padding)] flex items-center gap-8 py-4">
|
|
@@ -21,7 +24,7 @@ export function Header({ slideControls }: HeaderProps = {}) {
|
|
|
21
24
|
to="/"
|
|
22
25
|
className="rounded-lg font-mono py-1.5 text-sm font-medium text-muted-foreground hover:underline"
|
|
23
26
|
>
|
|
24
|
-
|
|
27
|
+
{config.name}
|
|
25
28
|
</Link>
|
|
26
29
|
</nav>
|
|
27
30
|
|
|
@@ -52,14 +55,16 @@ export function Header({ slideControls }: HeaderProps = {}) {
|
|
|
52
55
|
|
|
53
56
|
{/* Navigation */}
|
|
54
57
|
<nav className="flex items-center gap-2">
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
{config.github && (
|
|
59
|
+
<Link
|
|
60
|
+
to={`https://github.com/${config.github}`}
|
|
61
|
+
target="_blank"
|
|
62
|
+
className="text-muted-foreground/70 hover:text-foreground transition-colors duration-300"
|
|
63
|
+
aria-label="GitHub"
|
|
64
|
+
>
|
|
65
|
+
<SiGithub className="h-4 w-4" />
|
|
66
|
+
</Link>
|
|
67
|
+
)}
|
|
63
68
|
<ModeToggle />
|
|
64
69
|
</nav>
|
|
65
70
|
</div>
|
|
@@ -1,7 +1,62 @@
|
|
|
1
|
-
|
|
1
|
+
import { Link, useLocation } from 'react-router-dom'
|
|
2
2
|
import Gallery from '@/components/gallery'
|
|
3
3
|
import { ParameterTable } from '@/components/parameter-table'
|
|
4
4
|
import { ParameterBadge } from '@/components/parameter-badge'
|
|
5
|
+
import { FrontMatter } from './front-matter'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Smart link component that uses React Router for internal links
|
|
9
|
+
* and regular anchor tags for external links.
|
|
10
|
+
*/
|
|
11
|
+
function SmartLink({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
|
|
12
|
+
const location = useLocation()
|
|
13
|
+
|
|
14
|
+
// External links: absolute URLs, mailto, tel, etc.
|
|
15
|
+
const isExternal = href?.startsWith('http') || href?.startsWith('mailto:') || href?.startsWith('tel:')
|
|
16
|
+
|
|
17
|
+
// Hash-only links stay as anchors for in-page navigation
|
|
18
|
+
const isHashOnly = href?.startsWith('#')
|
|
19
|
+
|
|
20
|
+
if (isExternal || isHashOnly || !href) {
|
|
21
|
+
return (
|
|
22
|
+
<a
|
|
23
|
+
href={href}
|
|
24
|
+
className="text-primary hover:underline underline-offset-2"
|
|
25
|
+
{...(isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
|
26
|
+
{...props}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</a>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Resolve relative paths (./foo.mdx, ../bar.mdx) against current location
|
|
34
|
+
let resolvedHref = href
|
|
35
|
+
if (href.startsWith('./') || href.startsWith('../')) {
|
|
36
|
+
// Get current directory from pathname
|
|
37
|
+
const currentPath = location.pathname
|
|
38
|
+
const currentDir = currentPath.replace(/\/[^/]+\.mdx$/, '') || currentPath.replace(/\/[^/]*$/, '') || '/'
|
|
39
|
+
|
|
40
|
+
// Simple relative path resolution
|
|
41
|
+
if (href.startsWith('./')) {
|
|
42
|
+
resolvedHref = `${currentDir}/${href.slice(2)}`.replace(/\/+/g, '/')
|
|
43
|
+
} else if (href.startsWith('../')) {
|
|
44
|
+
const parentDir = currentDir.replace(/\/[^/]+$/, '') || '/'
|
|
45
|
+
resolvedHref = `${parentDir}/${href.slice(3)}`.replace(/\/+/g, '/')
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Internal link - use React Router Link
|
|
50
|
+
return (
|
|
51
|
+
<Link
|
|
52
|
+
to={resolvedHref}
|
|
53
|
+
className="text-primary hover:underline underline-offset-2"
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</Link>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
5
60
|
|
|
6
61
|
function generateId(children: unknown): string {
|
|
7
62
|
return children
|
|
@@ -13,6 +68,9 @@ function generateId(children: unknown): string {
|
|
|
13
68
|
|
|
14
69
|
// Shared MDX components - lab notebook / coder aesthetic
|
|
15
70
|
export const mdxComponents = {
|
|
71
|
+
|
|
72
|
+
FrontMatter,
|
|
73
|
+
|
|
16
74
|
Gallery,
|
|
17
75
|
|
|
18
76
|
ParameterTable,
|
|
@@ -110,13 +168,8 @@ export const mdxComponents = {
|
|
|
110
168
|
<li className="mt-1.5" {...props} />
|
|
111
169
|
),
|
|
112
170
|
|
|
113
|
-
// Links
|
|
114
|
-
a:
|
|
115
|
-
<a
|
|
116
|
-
className="text-primary hover:underline underline-offset-2"
|
|
117
|
-
{...props}
|
|
118
|
-
/>
|
|
119
|
-
),
|
|
171
|
+
// Links - uses React Router for internal navigation
|
|
172
|
+
a: SmartLink,
|
|
120
173
|
|
|
121
174
|
// Tables
|
|
122
175
|
table: (props: React.TableHTMLAttributes<HTMLTableElement>) => (
|
|
@@ -1,14 +1,64 @@
|
|
|
1
1
|
import { Link } from "react-router-dom";
|
|
2
2
|
import { cn } from "@/lib/utils";
|
|
3
|
-
import { DirectoryEntry } from "../../plugin/src/lib";
|
|
4
|
-
import { findReadme, findSlides } from "../../plugin/src/client";
|
|
3
|
+
import { DirectoryEntry, FileEntry } from "../../plugin/src/lib";
|
|
4
|
+
import { findReadme, findSlides, findMdxFiles } from "../../plugin/src/client";
|
|
5
5
|
import { formatDate } from "@/lib/format-date";
|
|
6
6
|
import { ArrowRight } from "lucide-react";
|
|
7
7
|
|
|
8
|
+
type PostEntry = {
|
|
9
|
+
type: 'folder' | 'file';
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
readme: FileEntry | null;
|
|
13
|
+
slides: FileEntry | null;
|
|
14
|
+
file: FileEntry | null; // For standalone MDX files
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Helper to extract numeric prefix from filename (e.g., "01-intro" → 1)
|
|
18
|
+
function extractOrder(name: string): number | null {
|
|
19
|
+
const match = name.match(/^(\d+)-/);
|
|
20
|
+
return match ? parseInt(match[1], 10) : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Helper to strip numeric prefix for display (e.g., "01-getting-started" → "Getting Started")
|
|
24
|
+
function stripNumericPrefix(name: string): string {
|
|
25
|
+
return name
|
|
26
|
+
.replace(/^\d+-/, '')
|
|
27
|
+
.replace(/-/g, ' ')
|
|
28
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
29
|
+
}
|
|
30
|
+
|
|
8
31
|
export default function PostList({ directory }: { directory: DirectoryEntry }) {
|
|
9
32
|
const folders = directory.children.filter((c): c is DirectoryEntry => c.type === "directory");
|
|
33
|
+
const standaloneFiles = findMdxFiles(directory);
|
|
34
|
+
|
|
35
|
+
// Convert folders to post entries
|
|
36
|
+
const folderPosts: PostEntry[] = folders.map((folder) => {
|
|
37
|
+
const readme = findReadme(folder);
|
|
38
|
+
const slides = findSlides(folder);
|
|
39
|
+
return {
|
|
40
|
+
type: 'folder' as const,
|
|
41
|
+
name: folder.name,
|
|
42
|
+
path: folder.path,
|
|
43
|
+
readme,
|
|
44
|
+
slides,
|
|
45
|
+
file: null,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
10
48
|
|
|
11
|
-
|
|
49
|
+
// Convert standalone MDX files to post entries
|
|
50
|
+
const filePosts: PostEntry[] = standaloneFiles.map((file) => ({
|
|
51
|
+
type: 'file' as const,
|
|
52
|
+
name: file.name.replace(/\.mdx?$/, ''),
|
|
53
|
+
path: file.path,
|
|
54
|
+
readme: null,
|
|
55
|
+
slides: null,
|
|
56
|
+
file,
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
let posts: PostEntry[] = [...folderPosts, ...filePosts];
|
|
60
|
+
|
|
61
|
+
if (posts.length === 0) {
|
|
12
62
|
return (
|
|
13
63
|
<div className="py-24 text-center">
|
|
14
64
|
<p className="text-muted-foreground font-mono text-sm tracking-wide">no entries</p>
|
|
@@ -16,133 +66,117 @@ export default function PostList({ directory }: { directory: DirectoryEntry }) {
|
|
|
16
66
|
);
|
|
17
67
|
}
|
|
18
68
|
|
|
19
|
-
|
|
20
|
-
const readme = findReadme(folder);
|
|
21
|
-
const slides = findSlides(folder);
|
|
22
|
-
return {
|
|
23
|
-
...folder,
|
|
24
|
-
readme,
|
|
25
|
-
slides,
|
|
26
|
-
}
|
|
27
|
-
})
|
|
28
|
-
|
|
69
|
+
// Filter out hidden and draft posts
|
|
29
70
|
posts = posts.filter((post) => {
|
|
30
|
-
|
|
71
|
+
const frontmatter = post.readme?.frontmatter || post.file?.frontmatter;
|
|
72
|
+
return frontmatter?.visibility !== "hidden" && frontmatter?.draft !== true;
|
|
31
73
|
});
|
|
32
74
|
|
|
75
|
+
// Helper to get frontmatter from post
|
|
76
|
+
const getFrontmatter = (post: PostEntry) => {
|
|
77
|
+
return post.readme?.frontmatter || post.file?.frontmatter || post.slides?.frontmatter;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Helper to get date from post
|
|
81
|
+
const getPostDate = (post: PostEntry): Date | null => {
|
|
82
|
+
const frontmatter = getFrontmatter(post);
|
|
83
|
+
return frontmatter?.date ? new Date(frontmatter.date as string) : null;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Smart sorting: numeric prefix → date → alphabetical
|
|
33
87
|
posts = posts.sort((a, b) => {
|
|
34
|
-
|
|
35
|
-
|
|
88
|
+
const aOrder = extractOrder(a.name);
|
|
89
|
+
const bOrder = extractOrder(b.name);
|
|
90
|
+
const aDate = getPostDate(a);
|
|
91
|
+
const bDate = getPostDate(b);
|
|
36
92
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (!bDate && b.slides) {
|
|
41
|
-
bDate = b.slides.frontmatter?.date ? new Date(b.slides.frontmatter.date as string) : null;
|
|
93
|
+
// Both have numeric prefix → sort by number
|
|
94
|
+
if (aOrder !== null && bOrder !== null) {
|
|
95
|
+
return aOrder - bOrder;
|
|
42
96
|
}
|
|
97
|
+
// One has prefix, one doesn't → prefixed comes first
|
|
98
|
+
if (aOrder !== null) return -1;
|
|
99
|
+
if (bOrder !== null) return 1;
|
|
43
100
|
|
|
101
|
+
// Both have dates → sort by date (newest first)
|
|
44
102
|
if (aDate && bDate) {
|
|
45
103
|
return bDate.getTime() - aDate.getTime();
|
|
46
|
-
} else if (aDate) {
|
|
47
|
-
return -1;
|
|
48
|
-
} else if (bDate) {
|
|
49
|
-
return 1;
|
|
50
|
-
} else {
|
|
51
|
-
return a.name.localeCompare(b.name);
|
|
52
104
|
}
|
|
53
|
-
|
|
105
|
+
// One has date → dated comes first
|
|
106
|
+
if (aDate) return -1;
|
|
107
|
+
if (bDate) return 1;
|
|
54
108
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
date = post.slides.frontmatter?.date ? new Date(post.slides.frontmatter.date as string) : null;
|
|
60
|
-
}
|
|
61
|
-
const monthYear = date ? `${date.getFullYear()}-${date.getMonth() + 1}` : "unknown";
|
|
62
|
-
if (!postsGroupedByMonthAndYear[monthYear]) {
|
|
63
|
-
postsGroupedByMonthAndYear[monthYear] = [];
|
|
64
|
-
}
|
|
65
|
-
postsGroupedByMonthAndYear[monthYear].push(post);
|
|
109
|
+
// Neither → alphabetical by title
|
|
110
|
+
const aTitle = (getFrontmatter(a)?.title as string) || a.name;
|
|
111
|
+
const bTitle = (getFrontmatter(b)?.title as string) || b.name;
|
|
112
|
+
return aTitle.localeCompare(bTitle);
|
|
66
113
|
});
|
|
67
114
|
|
|
68
115
|
return (
|
|
69
|
-
<div className="space-y-
|
|
70
|
-
{
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<span>{title}</span>
|
|
129
|
-
<ArrowRight className="h-3 w-3 opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200 text-primary" />
|
|
130
|
-
</h3>
|
|
131
|
-
|
|
132
|
-
{description && (
|
|
133
|
-
<p className="text-sm text-muted-foreground line-clamp-1 mt-0.5">
|
|
134
|
-
{description}
|
|
135
|
-
</p>
|
|
136
|
-
)}
|
|
137
|
-
</div>
|
|
138
|
-
</article>
|
|
139
|
-
</Link>
|
|
140
|
-
);
|
|
141
|
-
})}
|
|
116
|
+
<div className="space-y-1">
|
|
117
|
+
{posts.map((post) => {
|
|
118
|
+
const frontmatter = getFrontmatter(post);
|
|
119
|
+
|
|
120
|
+
// Title: explicit frontmatter > stripped numeric prefix > raw name
|
|
121
|
+
const title = (frontmatter?.title as string) || stripNumericPrefix(post.name);
|
|
122
|
+
const description = frontmatter?.description as string | undefined;
|
|
123
|
+
const date = frontmatter?.date ? new Date(frontmatter.date as string) : null;
|
|
124
|
+
|
|
125
|
+
// Determine the link path
|
|
126
|
+
let linkPath: string;
|
|
127
|
+
if (post.file) {
|
|
128
|
+
// Standalone MDX file
|
|
129
|
+
linkPath = `/${post.file.path}`;
|
|
130
|
+
} else if (post.slides && !post.readme) {
|
|
131
|
+
// Folder with only slides
|
|
132
|
+
linkPath = `/${post.slides.path}`;
|
|
133
|
+
} else if (post.readme) {
|
|
134
|
+
// Folder with readme
|
|
135
|
+
linkPath = `/${post.readme.path}`;
|
|
136
|
+
} else {
|
|
137
|
+
// Fallback to folder path
|
|
138
|
+
linkPath = `/${post.path}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<Link
|
|
143
|
+
key={post.path}
|
|
144
|
+
to={linkPath}
|
|
145
|
+
className={cn(
|
|
146
|
+
"group block py-3 px-3 -mx-3 rounded-md",
|
|
147
|
+
"transition-colors duration-150",
|
|
148
|
+
)}
|
|
149
|
+
>
|
|
150
|
+
<article className="flex items-start gap-4">
|
|
151
|
+
{/* Date - left side, fixed width */}
|
|
152
|
+
<time
|
|
153
|
+
dateTime={date?.toISOString()}
|
|
154
|
+
className="font-mono text-xs text-muted-foreground tabular-nums w-20 flex-shrink-0 pt-0.5"
|
|
155
|
+
>
|
|
156
|
+
{date ? formatDate(date) : <span className="text-muted-foreground/30">—</span>}
|
|
157
|
+
</time>
|
|
158
|
+
|
|
159
|
+
{/* Main content */}
|
|
160
|
+
<div className="flex-1 min-w-0">
|
|
161
|
+
<h3 className={cn(
|
|
162
|
+
"text-sm font-medium text-foreground",
|
|
163
|
+
"group-hover:underline",
|
|
164
|
+
"flex items-center gap-2"
|
|
165
|
+
)}>
|
|
166
|
+
<span>{title}</span>
|
|
167
|
+
<ArrowRight className="h-3 w-3 opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200 text-primary" />
|
|
168
|
+
</h3>
|
|
169
|
+
|
|
170
|
+
{description && (
|
|
171
|
+
<p className="text-sm text-muted-foreground line-clamp-1 mt-0.5">
|
|
172
|
+
{description}
|
|
173
|
+
</p>
|
|
174
|
+
)}
|
|
142
175
|
</div>
|
|
143
|
-
</
|
|
144
|
-
|
|
145
|
-
|
|
176
|
+
</article>
|
|
177
|
+
</Link>
|
|
178
|
+
);
|
|
179
|
+
})}
|
|
146
180
|
</div>
|
|
147
181
|
);
|
|
148
182
|
}
|
|
@@ -8,7 +8,7 @@ export function RunningBar() {
|
|
|
8
8
|
<>
|
|
9
9
|
{isRunning && (
|
|
10
10
|
// this should stay red not another color
|
|
11
|
-
<div className="sticky top-0 z-50 px-[var(--page-padding)] py-2 bg-red-500 text-primary-foreground font-mono text-xs text-center tracking-wide">
|
|
11
|
+
<div className="running-bar sticky top-0 z-50 px-[var(--page-padding)] py-2 bg-red-500 text-primary-foreground font-mono text-xs text-center tracking-wide">
|
|
12
12
|
<span className="inline-flex items-center gap-3">
|
|
13
13
|
<span className="h-1.5 w-1.5 rounded-full bg-current animate-pulse" />
|
|
14
14
|
<span className="uppercase tracking-widest">simulation running</span>
|