veslx 0.1.14 → 0.1.16
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 +27 -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 +31 -6
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Root, Content } from 'mdast'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Remark plugin that transforms MDX content into slides.
|
|
5
|
+
* Splits content at thematic breaks (---) and wraps each section
|
|
6
|
+
* in a <Slide index={n}> component.
|
|
7
|
+
*
|
|
8
|
+
* Also exports `slideCount` from the MDX module.
|
|
9
|
+
*
|
|
10
|
+
* Note: This plugin should run AFTER remark-frontmatter so that
|
|
11
|
+
* YAML frontmatter is already extracted and won't be confused with
|
|
12
|
+
* slide breaks.
|
|
13
|
+
*/
|
|
14
|
+
export function remarkSlides() {
|
|
15
|
+
return (tree: Root) => {
|
|
16
|
+
const slides: Content[][] = [[]]
|
|
17
|
+
let frontmatterNode: Content | null = null
|
|
18
|
+
|
|
19
|
+
// Split children by thematic breaks (skip yaml/toml frontmatter nodes)
|
|
20
|
+
for (const node of tree.children) {
|
|
21
|
+
if (node.type === 'thematicBreak') {
|
|
22
|
+
// Start a new slide
|
|
23
|
+
slides.push([])
|
|
24
|
+
} else if (node.type === 'yaml' || node.type === 'toml') {
|
|
25
|
+
// Keep frontmatter to add back later
|
|
26
|
+
frontmatterNode = node
|
|
27
|
+
} else {
|
|
28
|
+
// Add to current slide
|
|
29
|
+
slides[slides.length - 1].push(node)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Filter out empty slides
|
|
34
|
+
const nonEmptySlides = slides.filter(slide => slide.length > 0)
|
|
35
|
+
|
|
36
|
+
// Build new tree with Slide wrappers
|
|
37
|
+
const newChildren: Content[] = []
|
|
38
|
+
|
|
39
|
+
// Preserve frontmatter at the top (for remark-mdx-frontmatter to process)
|
|
40
|
+
if (frontmatterNode) {
|
|
41
|
+
newChildren.push(frontmatterNode)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Add slideCount export
|
|
45
|
+
newChildren.push({
|
|
46
|
+
type: 'mdxjsEsm',
|
|
47
|
+
value: `export const slideCount = ${nonEmptySlides.length};`,
|
|
48
|
+
data: {
|
|
49
|
+
estree: {
|
|
50
|
+
type: 'Program',
|
|
51
|
+
sourceType: 'module',
|
|
52
|
+
body: [{
|
|
53
|
+
type: 'ExportNamedDeclaration',
|
|
54
|
+
declaration: {
|
|
55
|
+
type: 'VariableDeclaration',
|
|
56
|
+
kind: 'const',
|
|
57
|
+
declarations: [{
|
|
58
|
+
type: 'VariableDeclarator',
|
|
59
|
+
id: { type: 'Identifier', name: 'slideCount' },
|
|
60
|
+
init: { type: 'Literal', value: nonEmptySlides.length }
|
|
61
|
+
}]
|
|
62
|
+
},
|
|
63
|
+
specifiers: [],
|
|
64
|
+
source: null
|
|
65
|
+
}]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} as any)
|
|
69
|
+
|
|
70
|
+
// Wrap each slide's content in a Slide component
|
|
71
|
+
nonEmptySlides.forEach((slideContent, index) => {
|
|
72
|
+
// Opening <Slide index={n}>
|
|
73
|
+
newChildren.push({
|
|
74
|
+
type: 'mdxJsxFlowElement',
|
|
75
|
+
name: 'Slide',
|
|
76
|
+
attributes: [{
|
|
77
|
+
type: 'mdxJsxAttribute',
|
|
78
|
+
name: 'index',
|
|
79
|
+
value: {
|
|
80
|
+
type: 'mdxJsxAttributeValueExpression',
|
|
81
|
+
value: String(index),
|
|
82
|
+
data: {
|
|
83
|
+
estree: {
|
|
84
|
+
type: 'Program',
|
|
85
|
+
sourceType: 'module',
|
|
86
|
+
body: [{
|
|
87
|
+
type: 'ExpressionStatement',
|
|
88
|
+
expression: { type: 'Literal', value: index }
|
|
89
|
+
}]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}],
|
|
94
|
+
children: slideContent
|
|
95
|
+
} as any)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
tree.children = newChildren
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface SiteConfig {
|
|
2
|
+
name?: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
github?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface VeslxConfig {
|
|
8
|
+
dir?: string;
|
|
9
|
+
site?: SiteConfig;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ResolvedSiteConfig {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
github: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_SITE_CONFIG: ResolvedSiteConfig = {
|
|
19
|
+
name: 'veslx',
|
|
20
|
+
description: '',
|
|
21
|
+
github: '',
|
|
22
|
+
};
|
package/src/App.tsx
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { BrowserRouter, Routes, Route } from "react-router-dom"
|
|
2
2
|
import { ThemeProvider } from "./components/theme-provider"
|
|
3
|
-
import {
|
|
4
|
-
import { Post } from "./pages/post"
|
|
5
|
-
import { SlidesPage } from "./pages/slides"
|
|
3
|
+
import { ContentRouter } from "./pages/content-router"
|
|
6
4
|
|
|
7
5
|
function App() {
|
|
8
6
|
return (
|
|
9
7
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
|
10
8
|
<BrowserRouter>
|
|
11
9
|
<Routes>
|
|
12
|
-
|
|
13
|
-
<Route path="
|
|
14
|
-
<Route path="/*" element={<Home />} />
|
|
10
|
+
{/* Single catch-all route - ContentRouter determines page type */}
|
|
11
|
+
<Route path="/*" element={<ContentRouter />} />
|
|
15
12
|
</Routes>
|
|
16
13
|
</BrowserRouter>
|
|
17
14
|
</ThemeProvider>
|
|
@@ -1,49 +1,34 @@
|
|
|
1
|
+
import { useMDXContent, useMDXSlides } from "@/hooks/use-mdx-content";
|
|
1
2
|
import { formatDate } from "@/lib/format-date"
|
|
2
|
-
import {
|
|
3
|
-
import { FileEntry } from "plugin/src/lib"
|
|
4
|
-
import { Link } from "react-router-dom"
|
|
3
|
+
import { useParams } from "react-router-dom"
|
|
5
4
|
|
|
6
|
-
export function FrontMatter({
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
title?: string
|
|
13
|
-
date?: string
|
|
14
|
-
description?: string
|
|
15
|
-
slides?: FileEntry | null
|
|
16
|
-
}){
|
|
5
|
+
export function FrontMatter(){
|
|
6
|
+
const { "path": path = "." } = useParams();
|
|
7
|
+
const { frontmatter: readmeFm } = useMDXContent(path);
|
|
8
|
+
const { frontmatter: slidesFm } = useMDXSlides(path);
|
|
9
|
+
|
|
10
|
+
let frontmatter = readmeFm || slidesFm;
|
|
17
11
|
|
|
18
12
|
return (
|
|
19
13
|
<div>
|
|
20
|
-
{title && (
|
|
14
|
+
{frontmatter?.title && (
|
|
21
15
|
<header className="not-prose flex flex-col gap-2 mb-8 pt-4">
|
|
22
16
|
<h1 className="text-2xl md:text-3xl font-semibold tracking-tight text-foreground mb-3">
|
|
23
|
-
{title}
|
|
17
|
+
{frontmatter?.title}
|
|
24
18
|
</h1>
|
|
25
19
|
|
|
26
20
|
{/* Meta line */}
|
|
27
21
|
<div className="flex flex-wrap items-center gap-3 text-muted-foreground">
|
|
28
|
-
{date && (
|
|
22
|
+
{frontmatter?.date && (
|
|
29
23
|
<time className="font-mono text-xs bg-muted px-2 py-0.5 rounded">
|
|
30
|
-
{formatDate(new Date(date as string))}
|
|
24
|
+
{formatDate(new Date(frontmatter.date as string))}
|
|
31
25
|
</time>
|
|
32
26
|
)}
|
|
33
|
-
{slides && (
|
|
34
|
-
<Link
|
|
35
|
-
to={`/${slides.path}`}
|
|
36
|
-
className="font-mono text-xs px-2 py-0.5 rounded flex items-center gap-1"
|
|
37
|
-
>
|
|
38
|
-
<Presentation className="h-3.5 w-3.5" />
|
|
39
|
-
<span>slides</span>
|
|
40
|
-
</Link>
|
|
41
|
-
)}
|
|
42
27
|
</div>
|
|
43
28
|
|
|
44
|
-
{description && (
|
|
29
|
+
{frontmatter?.description && (
|
|
45
30
|
<div className="flex flex-wrap text-sm items-center gap-3 text-muted-foreground">
|
|
46
|
-
{description}
|
|
31
|
+
{frontmatter?.description}
|
|
47
32
|
</div>
|
|
48
33
|
)}
|
|
49
34
|
</header>
|
|
@@ -4,12 +4,20 @@ export function FigureCaption({ caption, label }: { caption?: string; label?: st
|
|
|
4
4
|
if (!caption && !label) return null;
|
|
5
5
|
|
|
6
6
|
return (
|
|
7
|
-
<
|
|
8
|
-
<
|
|
9
|
-
{label &&
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
<figcaption className="px-[calc((var(--gallery-width)-var(--content-width))/2)] mt-4">
|
|
8
|
+
<p className="text-[13px] leading-[1.6] text-muted-foreground">
|
|
9
|
+
{label && (
|
|
10
|
+
<span className="font-semibold text-foreground tracking-tight">
|
|
11
|
+
{label}
|
|
12
|
+
{caption && <span className="font-normal mx-1.5">·</span>}
|
|
13
|
+
</span>
|
|
14
|
+
)}
|
|
15
|
+
{caption && (
|
|
16
|
+
<span className="text-muted-foreground/90">
|
|
17
|
+
{renderMathInText(caption)}
|
|
18
|
+
</span>
|
|
19
|
+
)}
|
|
20
|
+
</p>
|
|
21
|
+
</figcaption>
|
|
14
22
|
);
|
|
15
23
|
}
|
|
@@ -4,14 +4,14 @@ 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="px-[calc((var(--gallery-width)-var(--content-width))/2)] mb-4">
|
|
8
8
|
{title && (
|
|
9
|
-
<h3 className="text-
|
|
9
|
+
<h3 className="text-[15px] font-medium tracking-[-0.01em] text-foreground">
|
|
10
10
|
{renderMathInText(title)}
|
|
11
11
|
</h3>
|
|
12
12
|
)}
|
|
13
13
|
{subtitle && (
|
|
14
|
-
<p className="text-
|
|
14
|
+
<p className="text-[13px] text-muted-foreground/80 leading-relaxed mt-1">
|
|
15
15
|
{renderMathInText(subtitle)}
|
|
16
16
|
</p>
|
|
17
17
|
)}
|
|
@@ -31,25 +31,27 @@ export function Lightbox({
|
|
|
31
31
|
|
|
32
32
|
return createPortal(
|
|
33
33
|
<div
|
|
34
|
-
className="fixed inset-0 z-[9999] bg-background"
|
|
34
|
+
className="fixed inset-0 z-[9999] bg-background/98 backdrop-blur-md animate-fade-in-slow"
|
|
35
35
|
onClick={onClose}
|
|
36
36
|
{...{ [FULLSCREEN_DATA_ATTR]: "true" }}
|
|
37
37
|
style={{ top: 0, left: 0, right: 0, bottom: 0 }}
|
|
38
38
|
>
|
|
39
39
|
{/* Top bar */}
|
|
40
40
|
<div
|
|
41
|
-
className="fixed top-0 left-0 right-0 z-10 flex items-center justify-between px-
|
|
41
|
+
className="fixed top-0 left-0 right-0 z-10 flex items-center justify-between px-6 py-4"
|
|
42
42
|
onClick={(e) => e.stopPropagation()}
|
|
43
43
|
>
|
|
44
|
-
<div className="font-mono text-
|
|
45
|
-
{String(selectedIndex + 1).padStart(2, '0')}
|
|
44
|
+
<div className="font-mono text-[11px] text-muted-foreground/60 tabular-nums tracking-wider uppercase">
|
|
45
|
+
{String(selectedIndex + 1).padStart(2, '0')}
|
|
46
|
+
<span className="mx-1.5 text-muted-foreground/30">/</span>
|
|
47
|
+
{String(images.length).padStart(2, '0')}
|
|
46
48
|
</div>
|
|
47
49
|
<button
|
|
48
50
|
onClick={onClose}
|
|
49
|
-
className="p-2 text-muted-foreground hover:text-foreground transition-colors"
|
|
51
|
+
className="p-2 -m-2 text-muted-foreground/50 hover:text-foreground transition-colors duration-200"
|
|
50
52
|
aria-label="Close"
|
|
51
53
|
>
|
|
52
|
-
<X className="h-
|
|
54
|
+
<X className="h-4 w-4" strokeWidth={1.5} />
|
|
53
55
|
</button>
|
|
54
56
|
</div>
|
|
55
57
|
|
|
@@ -60,10 +62,10 @@ export function Lightbox({
|
|
|
60
62
|
e.stopPropagation();
|
|
61
63
|
onPrevious();
|
|
62
64
|
}}
|
|
63
|
-
className="fixed left-
|
|
65
|
+
className="fixed left-6 top-1/2 -translate-y-1/2 z-10 p-3 -m-3 text-muted-foreground/40 hover:text-foreground transition-colors duration-200"
|
|
64
66
|
aria-label="Previous image"
|
|
65
67
|
>
|
|
66
|
-
<ChevronLeft className="h-
|
|
68
|
+
<ChevronLeft className="h-6 w-6" strokeWidth={1.5} />
|
|
67
69
|
</button>
|
|
68
70
|
)}
|
|
69
71
|
|
|
@@ -74,10 +76,10 @@ export function Lightbox({
|
|
|
74
76
|
e.stopPropagation();
|
|
75
77
|
onNext();
|
|
76
78
|
}}
|
|
77
|
-
className="fixed right-
|
|
79
|
+
className="fixed right-6 top-1/2 -translate-y-1/2 z-10 p-3 -m-3 text-muted-foreground/40 hover:text-foreground transition-colors duration-200"
|
|
78
80
|
aria-label="Next image"
|
|
79
81
|
>
|
|
80
|
-
<ChevronRight className="h-
|
|
82
|
+
<ChevronRight className="h-6 w-6" strokeWidth={1.5} />
|
|
81
83
|
</button>
|
|
82
84
|
)}
|
|
83
85
|
|
|
@@ -86,17 +88,17 @@ export function Lightbox({
|
|
|
86
88
|
<img
|
|
87
89
|
src={current.src}
|
|
88
90
|
alt={current.label}
|
|
89
|
-
className="max-w-full max-h-full object-contain"
|
|
91
|
+
className="max-w-full max-h-full object-contain rounded-sm shadow-2xl"
|
|
90
92
|
onClick={(e) => e.stopPropagation()}
|
|
91
93
|
/>
|
|
92
94
|
</div>
|
|
93
95
|
|
|
94
96
|
{/* Caption */}
|
|
95
97
|
<div
|
|
96
|
-
className="fixed bottom-0 left-0 right-0 z-10
|
|
98
|
+
className="fixed bottom-0 left-0 right-0 z-10 px-6 py-5 text-center"
|
|
97
99
|
onClick={(e) => e.stopPropagation()}
|
|
98
100
|
>
|
|
99
|
-
<span className="font-mono text-
|
|
101
|
+
<span className="font-mono text-[11px] text-muted-foreground/50 tracking-wide">
|
|
100
102
|
{current.label}
|
|
101
103
|
</span>
|
|
102
104
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, ImgHTMLAttributes } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { ImageOff } from "lucide-react";
|
|
3
3
|
import { cn } from "@/lib/utils";
|
|
4
4
|
|
|
5
5
|
export function LoadingImage({
|
|
@@ -11,27 +11,30 @@ export function LoadingImage({
|
|
|
11
11
|
const [hasError, setHasError] = useState(false);
|
|
12
12
|
|
|
13
13
|
return (
|
|
14
|
-
<div className={cn("relative", wrapperClassName)}>
|
|
14
|
+
<div className={cn("relative overflow-hidden rounded-sm bg-muted/20", wrapperClassName)}>
|
|
15
15
|
{isLoading && !hasError && (
|
|
16
|
-
<div className="absolute inset-0
|
|
17
|
-
<div className="
|
|
16
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
17
|
+
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-muted/40 to-transparent animate-shimmer" />
|
|
18
18
|
</div>
|
|
19
19
|
)}
|
|
20
20
|
{hasError && (
|
|
21
|
-
<div className="absolute inset-0 bg-muted/
|
|
22
|
-
<div className="text-center">
|
|
23
|
-
<
|
|
24
|
-
<span className="text-
|
|
21
|
+
<div className="absolute inset-0 bg-muted/10 flex items-center justify-center backdrop-blur-sm">
|
|
22
|
+
<div className="text-center space-y-1">
|
|
23
|
+
<ImageOff className="h-4 w-4 text-muted-foreground/30 mx-auto" strokeWidth={1.5} />
|
|
24
|
+
<span className="text-[10px] text-muted-foreground/30 block font-mono uppercase tracking-wider">
|
|
25
|
+
unavailable
|
|
26
|
+
</span>
|
|
25
27
|
</div>
|
|
26
28
|
</div>
|
|
27
29
|
)}
|
|
28
30
|
<img
|
|
29
31
|
{...props}
|
|
30
32
|
className={cn(
|
|
31
|
-
|
|
32
|
-
"transition-
|
|
33
|
-
isLoading
|
|
34
|
-
hasError && "opacity-0"
|
|
33
|
+
"w-full h-full",
|
|
34
|
+
"transition-all duration-500 ease-out",
|
|
35
|
+
isLoading ? "opacity-0 scale-[1.02]" : "opacity-100 scale-100",
|
|
36
|
+
hasError && "opacity-0",
|
|
37
|
+
className
|
|
35
38
|
)}
|
|
36
39
|
onLoad={(e) => {
|
|
37
40
|
setIsLoading(false);
|
|
@@ -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>
|