veslx 0.1.14 → 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 +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 +25 -6
|
@@ -8,12 +8,69 @@ interface MDXModule {
|
|
|
8
8
|
description?: string
|
|
9
9
|
date?: string
|
|
10
10
|
visibility?: string
|
|
11
|
+
draft?: boolean
|
|
11
12
|
}
|
|
13
|
+
slideCount?: number // Exported by remark-slides plugin for SLIDES.mdx files
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
type ModuleLoader = () => Promise<MDXModule>
|
|
15
17
|
type ModuleMap = Record<string, ModuleLoader>
|
|
16
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Find MDX module by path. Supports:
|
|
21
|
+
* - Full path: "docs/intro.mdx" -> matches exactly
|
|
22
|
+
* - Folder path: "docs" -> matches "docs/index.mdx", "docs/README.mdx", or "docs.mdx"
|
|
23
|
+
*/
|
|
24
|
+
function findMdxModule(modules: ModuleMap, path: string): ModuleLoader | null {
|
|
25
|
+
const keys = Object.keys(modules)
|
|
26
|
+
|
|
27
|
+
// Normalize path - remove leading slash if present
|
|
28
|
+
const normalizedPath = path.replace(/^\//, '')
|
|
29
|
+
|
|
30
|
+
// If path already ends with .mdx, match exactly
|
|
31
|
+
if (normalizedPath.endsWith('.mdx')) {
|
|
32
|
+
// Try multiple matching strategies for different Vite glob formats
|
|
33
|
+
const matchingKey = keys.find(key => {
|
|
34
|
+
// Strategy 1: Key ends with /path (e.g., @content/docs/foo.mdx matches docs/foo.mdx)
|
|
35
|
+
if (key.endsWith(`/${normalizedPath}`)) return true
|
|
36
|
+
// Strategy 2: Key equals @content/path (alias form)
|
|
37
|
+
if (key === `@content/${normalizedPath}`) return true
|
|
38
|
+
// Strategy 3: Key equals /@content/path (with leading slash)
|
|
39
|
+
if (key === `/@content/${normalizedPath}`) return true
|
|
40
|
+
// Strategy 4: Key equals path directly
|
|
41
|
+
if (key === normalizedPath) return true
|
|
42
|
+
// Strategy 5: Key equals /path (with leading slash)
|
|
43
|
+
if (key === `/${normalizedPath}`) return true
|
|
44
|
+
return false
|
|
45
|
+
})
|
|
46
|
+
return matchingKey ? modules[matchingKey] : null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Otherwise, try folder conventions in order of preference:
|
|
50
|
+
// 1. folder/index.mdx (modern convention)
|
|
51
|
+
// 2. folder/README.mdx (current convention)
|
|
52
|
+
// 3. folder.mdx (file alongside folders)
|
|
53
|
+
const candidates = [
|
|
54
|
+
`${normalizedPath}/index.mdx`,
|
|
55
|
+
`${normalizedPath}/README.mdx`,
|
|
56
|
+
`${normalizedPath}.mdx`,
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
for (const candidate of candidates) {
|
|
60
|
+
const matchingKey = keys.find(key => {
|
|
61
|
+
if (key.endsWith(`/${candidate}`)) return true
|
|
62
|
+
if (key === `@content/${candidate}`) return true
|
|
63
|
+
if (key === candidate) return true
|
|
64
|
+
return false
|
|
65
|
+
})
|
|
66
|
+
if (matchingKey) {
|
|
67
|
+
return modules[matchingKey]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
17
74
|
export function useMDXContent(path: string) {
|
|
18
75
|
const [Content, setContent] = useState<MDXModule['default'] | null>(null)
|
|
19
76
|
const [frontmatter, setFrontmatter] = useState<MDXModule['frontmatter']>(undefined)
|
|
@@ -28,10 +85,7 @@ export function useMDXContent(path: string) {
|
|
|
28
85
|
// Dynamic import to avoid pre-bundling issues
|
|
29
86
|
import('virtual:content-modules')
|
|
30
87
|
.then(({ modules }) => {
|
|
31
|
-
const
|
|
32
|
-
key.endsWith(`/${path}/README.mdx`)
|
|
33
|
-
)
|
|
34
|
-
const loader = matchingKey ? (modules as ModuleMap)[matchingKey] : null
|
|
88
|
+
const loader = findMdxModule(modules as ModuleMap, path)
|
|
35
89
|
|
|
36
90
|
if (!loader) {
|
|
37
91
|
throw new Error(`MDX module not found for path: ${path}`)
|
|
@@ -61,9 +115,42 @@ export function useMDXContent(path: string) {
|
|
|
61
115
|
return { Content, frontmatter, loading, error }
|
|
62
116
|
}
|
|
63
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Find slides module by path. Supports:
|
|
120
|
+
* - Full path: "docs/intro.slides.mdx" or "docs/SLIDES.mdx" -> matches exactly
|
|
121
|
+
* - Folder path: "docs" -> matches "docs/SLIDES.mdx" or "docs/index.slides.mdx"
|
|
122
|
+
*/
|
|
123
|
+
function findSlidesModule(modules: ModuleMap, path: string): ModuleLoader | null {
|
|
124
|
+
const keys = Object.keys(modules)
|
|
125
|
+
|
|
126
|
+
// If path already ends with .mdx, match exactly
|
|
127
|
+
if (path.endsWith('.mdx')) {
|
|
128
|
+
const matchingKey = keys.find(key => key.endsWith(`/${path}`))
|
|
129
|
+
return matchingKey ? modules[matchingKey] : null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Otherwise, try folder conventions:
|
|
133
|
+
// 1. folder/SLIDES.mdx (current convention)
|
|
134
|
+
// 2. folder/index.slides.mdx (alternative)
|
|
135
|
+
const candidates = [
|
|
136
|
+
`/${path}/SLIDES.mdx`,
|
|
137
|
+
`/${path}/index.slides.mdx`,
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
for (const suffix of candidates) {
|
|
141
|
+
const matchingKey = keys.find(key => key.endsWith(suffix))
|
|
142
|
+
if (matchingKey) {
|
|
143
|
+
return modules[matchingKey]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
|
|
64
150
|
export function useMDXSlides(path: string) {
|
|
65
151
|
const [Content, setContent] = useState<MDXModule['default'] | null>(null)
|
|
66
152
|
const [frontmatter, setFrontmatter] = useState<MDXModule['frontmatter']>(undefined)
|
|
153
|
+
const [slideCount, setSlideCount] = useState<number | undefined>(undefined)
|
|
67
154
|
const [loading, setLoading] = useState(true)
|
|
68
155
|
const [error, setError] = useState<Error | null>(null)
|
|
69
156
|
|
|
@@ -75,10 +162,7 @@ export function useMDXSlides(path: string) {
|
|
|
75
162
|
// Dynamic import to avoid pre-bundling issues
|
|
76
163
|
import('virtual:content-modules')
|
|
77
164
|
.then(({ slides }) => {
|
|
78
|
-
const
|
|
79
|
-
key.endsWith(`/${path}/SLIDES.mdx`)
|
|
80
|
-
)
|
|
81
|
-
const loader = matchingKey ? (slides as ModuleMap)[matchingKey] : null
|
|
165
|
+
const loader = findSlidesModule(slides as ModuleMap, path)
|
|
82
166
|
|
|
83
167
|
if (!loader) {
|
|
84
168
|
throw new Error(`Slides module not found for path: ${path}`)
|
|
@@ -90,6 +174,7 @@ export function useMDXSlides(path: string) {
|
|
|
90
174
|
if (!cancelled) {
|
|
91
175
|
setContent(() => mod.default)
|
|
92
176
|
setFrontmatter(mod.frontmatter)
|
|
177
|
+
setSlideCount(mod.slideCount)
|
|
93
178
|
setLoading(false)
|
|
94
179
|
}
|
|
95
180
|
})
|
|
@@ -105,5 +190,5 @@ export function useMDXSlides(path: string) {
|
|
|
105
190
|
}
|
|
106
191
|
}, [path])
|
|
107
192
|
|
|
108
|
-
return { Content, frontmatter, loading, error }
|
|
193
|
+
return { Content, frontmatter, slideCount, loading, error }
|
|
109
194
|
}
|
package/src/index.css
CHANGED
|
@@ -277,3 +277,162 @@
|
|
|
277
277
|
.animate-slide-up {
|
|
278
278
|
animation: slide-up 0.5s ease-out forwards;
|
|
279
279
|
}
|
|
280
|
+
|
|
281
|
+
/* Shimmer loading animation for skeletons */
|
|
282
|
+
@keyframes shimmer {
|
|
283
|
+
0% {
|
|
284
|
+
transform: translateX(-100%);
|
|
285
|
+
}
|
|
286
|
+
100% {
|
|
287
|
+
transform: translateX(100%);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.animate-shimmer {
|
|
292
|
+
animation: shimmer 1.5s ease-in-out infinite;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* Print styles - slides landscape, posts portrait */
|
|
296
|
+
@media print {
|
|
297
|
+
/* Default page is portrait for posts */
|
|
298
|
+
@page {
|
|
299
|
+
size: portrait;
|
|
300
|
+
margin: 1in;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* Named page for slides - landscape */
|
|
304
|
+
@page slides {
|
|
305
|
+
size: landscape;
|
|
306
|
+
margin: 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* Slides container uses the slides page */
|
|
310
|
+
.slides-container {
|
|
311
|
+
page: slides;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* Reset everything */
|
|
315
|
+
html, body {
|
|
316
|
+
height: auto !important;
|
|
317
|
+
overflow: visible !important;
|
|
318
|
+
background: white !important;
|
|
319
|
+
margin: 0 !important;
|
|
320
|
+
padding: 0 !important;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* Hide ALL UI elements aggressively */
|
|
324
|
+
.slides-container > header,
|
|
325
|
+
.slides-container > .running-bar,
|
|
326
|
+
.slides-container hr,
|
|
327
|
+
.slides-container > title,
|
|
328
|
+
header.print\\:hidden,
|
|
329
|
+
[class*="print:hidden"] {
|
|
330
|
+
display: none !important;
|
|
331
|
+
height: 0 !important;
|
|
332
|
+
width: 0 !important;
|
|
333
|
+
overflow: hidden !important;
|
|
334
|
+
position: absolute !important;
|
|
335
|
+
left: -9999px !important;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* Main container - no space, no pages */
|
|
339
|
+
.slides-container {
|
|
340
|
+
display: block !important;
|
|
341
|
+
position: static !important;
|
|
342
|
+
margin: 0 !important;
|
|
343
|
+
padding: 0 !important;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* All wrapper divs - collapse completely */
|
|
347
|
+
.slides-container > div:not(.slide-section),
|
|
348
|
+
.slides-container > div > div:not(.slide-section) {
|
|
349
|
+
display: contents !important;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* Each slide is one page */
|
|
353
|
+
.slide-section {
|
|
354
|
+
display: flex !important;
|
|
355
|
+
align-items: center !important;
|
|
356
|
+
justify-content: center !important;
|
|
357
|
+
width: 100vw !important;
|
|
358
|
+
height: 100vh !important;
|
|
359
|
+
min-height: 100vh !important;
|
|
360
|
+
max-height: 100vh !important;
|
|
361
|
+
padding: 5vh 8vw !important;
|
|
362
|
+
margin: 0 !important;
|
|
363
|
+
page-break-after: always !important;
|
|
364
|
+
break-after: page !important;
|
|
365
|
+
page-break-inside: avoid !important;
|
|
366
|
+
break-inside: avoid !important;
|
|
367
|
+
box-sizing: border-box !important;
|
|
368
|
+
overflow: hidden !important;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/* Last slide shouldn't add extra page */
|
|
372
|
+
.slide-section:last-of-type {
|
|
373
|
+
page-break-after: auto !important;
|
|
374
|
+
break-after: auto !important;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* Slide content fills available space */
|
|
378
|
+
.slide-content {
|
|
379
|
+
max-width: 90% !important;
|
|
380
|
+
max-height: 90vh !important;
|
|
381
|
+
width: auto !important;
|
|
382
|
+
margin: 0 auto !important;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/* Scale images appropriately for print */
|
|
386
|
+
.slide-content img {
|
|
387
|
+
max-height: 60vh !important;
|
|
388
|
+
width: auto !important;
|
|
389
|
+
height: auto !important;
|
|
390
|
+
object-fit: contain !important;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* Gallery adjustments for print */
|
|
394
|
+
.slide-content figure {
|
|
395
|
+
margin: 0 !important;
|
|
396
|
+
padding: 1rem 0 !important;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/* Ensure text is black for print */
|
|
400
|
+
.slide-content,
|
|
401
|
+
.slide-content * {
|
|
402
|
+
color: black !important;
|
|
403
|
+
-webkit-print-color-adjust: exact !important;
|
|
404
|
+
print-color-adjust: exact !important;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* Hide carousel navigation in print */
|
|
408
|
+
.slide-content button[class*="Carousel"] {
|
|
409
|
+
display: none !important;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* === Post print styles === */
|
|
413
|
+
|
|
414
|
+
/* Hide carousel navigation buttons in posts */
|
|
415
|
+
figure button {
|
|
416
|
+
display: none !important;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/* Make carousel show all items in grid for print */
|
|
420
|
+
[data-slot="carousel-content"] {
|
|
421
|
+
display: grid !important;
|
|
422
|
+
grid-template-columns: repeat(3, 1fr) !important;
|
|
423
|
+
gap: 0.5rem !important;
|
|
424
|
+
transform: none !important;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
[data-slot="carousel-item"] {
|
|
428
|
+
flex: none !important;
|
|
429
|
+
width: 100% !important;
|
|
430
|
+
padding: 0 !important;
|
|
431
|
+
margin: 0 !important;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/* Ensure gallery images print well */
|
|
435
|
+
figure img {
|
|
436
|
+
break-inside: avoid !important;
|
|
437
|
+
}
|
|
438
|
+
}
|
package/src/main.tsx
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useParams } from "react-router-dom"
|
|
2
|
+
import { Home } from "./home"
|
|
3
|
+
import { Post } from "./post"
|
|
4
|
+
import { SlidesPage } from "./slides"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Routes to the appropriate page based on the URL path:
|
|
8
|
+
* - *.slides.mdx or *SLIDES.mdx → SlidesPage
|
|
9
|
+
* - *.mdx → Post
|
|
10
|
+
* - everything else → Home (directory listing)
|
|
11
|
+
*/
|
|
12
|
+
export function ContentRouter() {
|
|
13
|
+
const { "*": path = "" } = useParams()
|
|
14
|
+
|
|
15
|
+
// Check if this is a slides file
|
|
16
|
+
if (path.endsWith('.slides.mdx') || path.endsWith('SLIDES.mdx')) {
|
|
17
|
+
return <SlidesPage />
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check if this is any MDX file
|
|
21
|
+
if (path.endsWith('.mdx') || path.endsWith('.md')) {
|
|
22
|
+
return <Post />
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Otherwise show directory listing
|
|
26
|
+
return <Home />
|
|
27
|
+
}
|
package/src/pages/home.tsx
CHANGED
|
@@ -5,10 +5,12 @@ import PostList from "@/components/post-list";
|
|
|
5
5
|
import { ErrorDisplay } from "@/components/page-error";
|
|
6
6
|
import { RunningBar } from "@/components/running-bar";
|
|
7
7
|
import { Header } from "@/components/header";
|
|
8
|
+
import siteConfig from "virtual:veslx-config";
|
|
8
9
|
|
|
9
10
|
export function Home() {
|
|
10
11
|
const { "*": path = "." } = useParams();
|
|
11
12
|
const { directory, loading, error } = useDirectory(path)
|
|
13
|
+
const config = siteConfig;
|
|
12
14
|
|
|
13
15
|
if (error) {
|
|
14
16
|
return <ErrorDisplay error={error} path={path} />;
|
|
@@ -25,8 +27,20 @@ export function Home() {
|
|
|
25
27
|
<RunningBar />
|
|
26
28
|
<Header />
|
|
27
29
|
<main className="flex-1 mx-auto w-full max-w-[var(--content-width)] px-[var(--page-padding)]">
|
|
28
|
-
<title>{
|
|
29
|
-
<main className="flex flex-col gap-
|
|
30
|
+
<title>{(path === "." || path === "") ? config.name : `${config.name} - ${path}`}</title>
|
|
31
|
+
<main className="flex flex-col gap-8 mb-32 mt-32">
|
|
32
|
+
{(path === "." || path === "") && (
|
|
33
|
+
<div className="animate-fade-in">
|
|
34
|
+
<h1 className="text-2xl md:text-3xl font-semibold tracking-tight text-foreground">
|
|
35
|
+
{config.name}
|
|
36
|
+
</h1>
|
|
37
|
+
{config.description && (
|
|
38
|
+
<p className="mt-2 text-muted-foreground">
|
|
39
|
+
{config.description}
|
|
40
|
+
</p>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
30
44
|
{directory && (
|
|
31
45
|
<div className="animate-fade-in">
|
|
32
46
|
<PostList directory={directory}/>
|
package/src/pages/post.tsx
CHANGED
|
@@ -2,18 +2,22 @@ import { useParams } from "react-router-dom";
|
|
|
2
2
|
import { findSlides, isSimulationRunning, useDirectory } from "../../plugin/src/client";
|
|
3
3
|
import Loading from "@/components/loading";
|
|
4
4
|
import { FileEntry } from "plugin/src/lib";
|
|
5
|
-
import { FrontMatter } from "@/components/front-matter";
|
|
6
5
|
import { RunningBar } from "@/components/running-bar";
|
|
7
6
|
import { Header } from "@/components/header";
|
|
8
7
|
import { useMDXContent } from "@/hooks/use-mdx-content";
|
|
9
8
|
import { mdxComponents } from "@/components/mdx-components";
|
|
10
9
|
|
|
11
|
-
|
|
12
10
|
export function Post() {
|
|
13
|
-
const { "
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
11
|
+
const { "*": rawPath = "." } = useParams();
|
|
12
|
+
|
|
13
|
+
// The path includes the .mdx extension from the route
|
|
14
|
+
const mdxPath = rawPath;
|
|
15
|
+
|
|
16
|
+
// Extract directory path for finding sibling files (slides, etc.)
|
|
17
|
+
const dirPath = mdxPath.replace(/\/[^/]+\.mdx$/, '') || '.';
|
|
18
|
+
|
|
19
|
+
const { directory, loading: dirLoading } = useDirectory(dirPath)
|
|
20
|
+
const { Content, frontmatter, loading: mdxLoading, error } = useMDXContent(mdxPath);
|
|
17
21
|
const isRunning = isSimulationRunning();
|
|
18
22
|
|
|
19
23
|
let slides: FileEntry | null = null;
|
|
@@ -39,7 +43,6 @@ export function Post() {
|
|
|
39
43
|
<RunningBar />
|
|
40
44
|
<Header />
|
|
41
45
|
<main className="flex-1 mx-auto w-full max-w-[var(--content-width)] px-[var(--page-padding)]">
|
|
42
|
-
|
|
43
46
|
{isRunning && (
|
|
44
47
|
<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">
|
|
45
48
|
<span className="inline-flex items-center gap-3">
|
|
@@ -52,12 +55,6 @@ export function Post() {
|
|
|
52
55
|
|
|
53
56
|
{Content && (
|
|
54
57
|
<article className="my-24 prose dark:prose-invert prose-headings:tracking-tight prose-p:leading-relaxed prose-a:text-primary prose-a:no-underline hover:prose-a:underline max-w-[var(--prose-width)] animate-fade-in">
|
|
55
|
-
<FrontMatter
|
|
56
|
-
title={frontmatter?.title}
|
|
57
|
-
date={frontmatter?.date}
|
|
58
|
-
description={frontmatter?.description}
|
|
59
|
-
slides={slides}
|
|
60
|
-
/>
|
|
61
58
|
<Content components={mdxComponents} />
|
|
62
59
|
</article>
|
|
63
60
|
)}
|
package/src/pages/slides.tsx
CHANGED
|
@@ -2,60 +2,100 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import { FULLSCREEN_DATA_ATTR } from "@/lib/constants";
|
|
3
3
|
import { useParams, useSearchParams } from "react-router-dom"
|
|
4
4
|
import Loading from "@/components/loading";
|
|
5
|
-
import { FrontMatter } from "@/components/front-matter";
|
|
6
5
|
import { RunningBar } from "@/components/running-bar";
|
|
7
6
|
import { Header } from "@/components/header";
|
|
8
7
|
import { useMDXSlides } from "@/hooks/use-mdx-content";
|
|
9
|
-
import {
|
|
8
|
+
import { slidesMdxComponents } from "@/components/slides-renderer";
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
export function SlidesPage() {
|
|
13
|
-
const { "
|
|
12
|
+
const { "*": rawPath = "." } = useParams();
|
|
14
13
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
15
14
|
|
|
16
|
-
//
|
|
17
|
-
const
|
|
15
|
+
// The path includes the .mdx extension from the route
|
|
16
|
+
const mdxPath = rawPath;
|
|
18
17
|
|
|
19
|
-
//
|
|
20
|
-
const {
|
|
18
|
+
// Load the compiled MDX module (now includes slideCount export)
|
|
19
|
+
const { Content, frontmatter, slideCount, loading, error } = useMDXSlides(mdxPath);
|
|
21
20
|
|
|
22
21
|
// Total slides = 1 (title) + content slides
|
|
23
|
-
const totalSlides =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
22
|
+
const totalSlides = (slideCount || 0) + 1;
|
|
23
|
+
|
|
24
|
+
const [currentSlide, setCurrentSlide] = useState(0);
|
|
25
|
+
const titleSlideRef = useRef<HTMLDivElement>(null);
|
|
26
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
27
|
+
|
|
28
|
+
// Scroll to slide on initial load if query param is set
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const slideParam = parseInt(searchParams.get("slide") || "0", 10);
|
|
31
|
+
if (slideParam > 0 && contentRef.current) {
|
|
32
|
+
const slideEl = contentRef.current.querySelector(`[data-slide-index="${slideParam - 1}"]`);
|
|
33
|
+
if (slideEl) {
|
|
34
|
+
slideEl.scrollIntoView({ behavior: "auto" });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}, [searchParams, Content]);
|
|
38
|
+
|
|
39
|
+
// Track current slide based on scroll position
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const observer = new IntersectionObserver(
|
|
42
|
+
(entries) => {
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (entry.isIntersecting) {
|
|
45
|
+
const index = entry.target.getAttribute("data-slide-index");
|
|
46
|
+
if (index !== null) {
|
|
47
|
+
const slideNum = index === "title" ? 0 : parseInt(index, 10) + 1;
|
|
48
|
+
setCurrentSlide(slideNum);
|
|
49
|
+
setSearchParams(slideNum > 0 ? { slide: String(slideNum) } : {}, { replace: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{ threshold: 0.5 }
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Observe title slide
|
|
58
|
+
if (titleSlideRef.current) {
|
|
59
|
+
observer.observe(titleSlideRef.current);
|
|
41
60
|
}
|
|
42
|
-
}, [totalSlides]);
|
|
43
61
|
|
|
62
|
+
// Observe content slides
|
|
63
|
+
if (contentRef.current) {
|
|
64
|
+
const slides = contentRef.current.querySelectorAll("[data-slide-index]");
|
|
65
|
+
slides.forEach((slide) => observer.observe(slide));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return () => observer.disconnect();
|
|
69
|
+
}, [Content, setSearchParams]);
|
|
70
|
+
|
|
71
|
+
// Keyboard/scroll navigation helpers
|
|
44
72
|
const goToPrevious = useCallback(() => {
|
|
45
|
-
|
|
46
|
-
|
|
73
|
+
const prev = Math.max(0, currentSlide - 1);
|
|
74
|
+
if (prev === 0 && titleSlideRef.current) {
|
|
75
|
+
titleSlideRef.current.scrollIntoView({ behavior: "smooth" });
|
|
76
|
+
} else if (contentRef.current) {
|
|
77
|
+
const slideEl = contentRef.current.querySelector(`[data-slide-index="${prev - 1}"]`);
|
|
78
|
+
slideEl?.scrollIntoView({ behavior: "smooth" });
|
|
79
|
+
}
|
|
80
|
+
}, [currentSlide]);
|
|
47
81
|
|
|
48
82
|
const goToNext = useCallback(() => {
|
|
49
|
-
|
|
50
|
-
|
|
83
|
+
const next = Math.min(totalSlides - 1, currentSlide + 1);
|
|
84
|
+
if (next === 0 && titleSlideRef.current) {
|
|
85
|
+
titleSlideRef.current.scrollIntoView({ behavior: "smooth" });
|
|
86
|
+
} else if (contentRef.current) {
|
|
87
|
+
const slideEl = contentRef.current.querySelector(`[data-slide-index="${next - 1}"]`);
|
|
88
|
+
slideEl?.scrollIntoView({ behavior: "smooth" });
|
|
89
|
+
}
|
|
90
|
+
}, [currentSlide, totalSlides]);
|
|
51
91
|
|
|
52
92
|
// Keyboard navigation
|
|
53
93
|
useEffect(() => {
|
|
54
94
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
55
|
-
if (e.key === "ArrowUp" || e.key === "k") {
|
|
95
|
+
if (e.key === "ArrowUp" || e.key === "ArrowLeft" || e.key === "k") {
|
|
56
96
|
e.preventDefault();
|
|
57
97
|
goToPrevious();
|
|
58
|
-
} else if (e.key === "ArrowDown" || e.key === "j") {
|
|
98
|
+
} else if (e.key === "ArrowDown" || e.key === "ArrowRight" || e.key === "j") {
|
|
59
99
|
e.preventDefault();
|
|
60
100
|
goToNext();
|
|
61
101
|
}
|
|
@@ -65,37 +105,6 @@ export function SlidesPage() {
|
|
|
65
105
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
66
106
|
}, [goToPrevious, goToNext]);
|
|
67
107
|
|
|
68
|
-
// Update query param on scroll (delayed to avoid interference on load)
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
let observer: IntersectionObserver | null = null;
|
|
71
|
-
|
|
72
|
-
const timeoutId = setTimeout(() => {
|
|
73
|
-
observer = new IntersectionObserver(
|
|
74
|
-
(entries) => {
|
|
75
|
-
for (const entry of entries) {
|
|
76
|
-
if (entry.isIntersecting) {
|
|
77
|
-
const index = slideRefs.current.findIndex((ref) => ref === entry.target);
|
|
78
|
-
if (index !== -1) {
|
|
79
|
-
setCurrentSlide(index);
|
|
80
|
-
setSearchParams(index > 0 ? { slide: String(index) } : {}, { replace: true });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
{ threshold: 0.5 }
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
slideRefs.current.forEach((ref) => {
|
|
89
|
-
if (ref) observer!.observe(ref);
|
|
90
|
-
});
|
|
91
|
-
}, 100);
|
|
92
|
-
|
|
93
|
-
return () => {
|
|
94
|
-
clearTimeout(timeoutId);
|
|
95
|
-
observer?.disconnect();
|
|
96
|
-
};
|
|
97
|
-
}, [slides.length, setSearchParams]);
|
|
98
|
-
|
|
99
108
|
if (loading) {
|
|
100
109
|
return <Loading />
|
|
101
110
|
}
|
|
@@ -108,7 +117,7 @@ export function SlidesPage() {
|
|
|
108
117
|
)
|
|
109
118
|
}
|
|
110
119
|
|
|
111
|
-
if (
|
|
120
|
+
if (!Content) {
|
|
112
121
|
return (
|
|
113
122
|
<div className="flex items-center justify-center p-12 text-muted-foreground font-mono text-sm">
|
|
114
123
|
no slides found — use "---" to separate slides
|
|
@@ -129,31 +138,9 @@ export function SlidesPage() {
|
|
|
129
138
|
}}
|
|
130
139
|
/>
|
|
131
140
|
<div {...{[FULLSCREEN_DATA_ATTR]: "true"}}>
|
|
132
|
-
{
|
|
133
|
-
|
|
134
|
-
ref={(el) => { slideRefs.current[0] = el; }}
|
|
135
|
-
className="slide-page max-w-xl min-h-[50vh] sm:min-h-[70vh] md:min-h-screen flex items-center justify-center py-8 sm:py-12 md:py-16 px-4 mx-auto"
|
|
136
|
-
>
|
|
137
|
-
<FrontMatter
|
|
138
|
-
title={frontmatter?.title}
|
|
139
|
-
date={frontmatter?.date}
|
|
140
|
-
description={frontmatter?.description}
|
|
141
|
-
/>
|
|
141
|
+
<div ref={contentRef}>
|
|
142
|
+
<Content components={slidesMdxComponents} />
|
|
142
143
|
</div>
|
|
143
|
-
<hr className="print:hidden" />
|
|
144
|
-
|
|
145
|
-
{/* Content slides */}
|
|
146
|
-
{slides.map((slideContent, index) => (
|
|
147
|
-
<div key={index}>
|
|
148
|
-
<div
|
|
149
|
-
ref={(el) => { slideRefs.current[index + 1] = el; }}
|
|
150
|
-
className="slide-page min-h-[50vh] sm:min-h-[70vh] md:min-h-screen flex items-center justify-center py-8 sm:py-12 md:py-16 px-4 mx-auto"
|
|
151
|
-
>
|
|
152
|
-
<SlideContent>{slideContent}</SlideContent>
|
|
153
|
-
</div>
|
|
154
|
-
{index < slides.length - 1 && <hr className="print:hidden" />}
|
|
155
|
-
</div>
|
|
156
|
-
))}
|
|
157
144
|
</div>
|
|
158
145
|
</main>
|
|
159
146
|
)
|
package/src/vite-env.d.ts
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
2
|
|
|
3
|
-
declare module 'virtual:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
frontmatter?: {
|
|
9
|
-
title?: string
|
|
10
|
-
description?: string
|
|
11
|
-
date?: string
|
|
12
|
-
visibility?: string
|
|
13
|
-
}
|
|
3
|
+
declare module 'virtual:veslx-config' {
|
|
4
|
+
interface SiteConfig {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
github: string;
|
|
14
8
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export const modules: Record<string, ModuleLoader>
|
|
19
|
-
export const slides: Record<string, ModuleLoader>
|
|
20
|
-
export const index: Record<string, { default: unknown }>
|
|
9
|
+
const config: SiteConfig;
|
|
10
|
+
export default config;
|
|
21
11
|
}
|