veslx 0.1.22 → 0.1.23

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.
Files changed (46) hide show
  1. package/dist/client/components/gallery/components/figure-caption.js +1 -1
  2. package/dist/client/components/gallery/components/figure-caption.js.map +1 -1
  3. package/dist/client/components/gallery/components/figure-header.js +1 -1
  4. package/dist/client/components/gallery/components/figure-header.js.map +1 -1
  5. package/dist/client/components/gallery/components/lightbox.js +1 -1
  6. package/dist/client/components/gallery/components/lightbox.js.map +1 -1
  7. package/dist/client/components/gallery/index.js +37 -7
  8. package/dist/client/components/gallery/index.js.map +1 -1
  9. package/dist/client/components/header.js +45 -21
  10. package/dist/client/components/header.js.map +1 -1
  11. package/dist/client/components/mdx-components.js +8 -0
  12. package/dist/client/components/mdx-components.js.map +1 -1
  13. package/dist/client/components/post-list.js +13 -11
  14. package/dist/client/components/post-list.js.map +1 -1
  15. package/dist/client/components/slides/figure-slide.js +14 -0
  16. package/dist/client/components/slides/figure-slide.js.map +1 -0
  17. package/dist/client/components/slides/hero-slide.js +21 -0
  18. package/dist/client/components/slides/hero-slide.js.map +1 -0
  19. package/dist/client/components/slides/slide-outline.js +28 -0
  20. package/dist/client/components/slides/slide-outline.js.map +1 -0
  21. package/dist/client/components/slides/text-slide.js +18 -0
  22. package/dist/client/components/slides/text-slide.js.map +1 -0
  23. package/dist/client/components/slides-renderer.js.map +1 -1
  24. package/dist/client/pages/home.js +2 -6
  25. package/dist/client/pages/home.js.map +1 -1
  26. package/dist/client/pages/slides.js +7 -11
  27. package/dist/client/pages/slides.js.map +1 -1
  28. package/index.html +13 -0
  29. package/package.json +1 -1
  30. package/src/components/content-tabs.tsx +4 -4
  31. package/src/components/gallery/components/figure-caption.tsx +1 -1
  32. package/src/components/gallery/components/figure-header.tsx +1 -1
  33. package/src/components/gallery/components/lightbox.tsx +1 -1
  34. package/src/components/gallery/index.tsx +68 -29
  35. package/src/components/header.tsx +44 -25
  36. package/src/components/mdx-components.tsx +12 -0
  37. package/src/components/post-list.tsx +14 -10
  38. package/src/components/slides/figure-slide.tsx +16 -0
  39. package/src/components/slides/hero-slide.tsx +34 -0
  40. package/src/components/slides/slide-outline.tsx +38 -0
  41. package/src/components/slides/text-slide.tsx +35 -0
  42. package/src/components/slides-renderer.tsx +1 -1
  43. package/src/pages/home.tsx +3 -3
  44. package/src/pages/slides.tsx +7 -12
  45. package/dist/client/components/content-tabs.js +0 -50
  46. package/dist/client/components/content-tabs.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"slides-renderer.js","sources":["../../../src/components/slides-renderer.tsx"],"sourcesContent":["import { ReactNode } from 'react'\nimport { mdxComponents } from '@/components/mdx-components'\nimport { Slide } from '@/components/slide'\n\n/**\n * MDX components for slides - includes the Slide component\n */\nexport const slidesMdxComponents = {\n ...mdxComponents,\n Slide,\n}\n\n/**\n * Renders a single slide's content\n */\nexport function SlideContent({ children }: { children: ReactNode }) {\n return (\n <div className=\"slide-content prose dark:prose-invert prose-headings:tracking-tight prose-p:leading-relaxed max-w-xl\">\n {children}\n </div>\n )\n}\n"],"names":[],"mappings":";;;AAOO,MAAM,sBAAsB;AAAA,EACjC,GAAG;AAAA,EACH;AACF;"}
1
+ {"version":3,"file":"slides-renderer.js","sources":["../../../src/components/slides-renderer.tsx"],"sourcesContent":["import { ReactNode } from 'react'\nimport { mdxComponents } from '@/components/mdx-components'\nimport { Slide } from '@/components/slide'\n\n/**\n * MDX components for slides - includes the Slide component\n */\nexport const slidesMdxComponents = {\n ...mdxComponents,\n Slide,\n}\n\n/**\n * Renders a single slide's content\n */\nexport function SlideContent({ children }: { children: ReactNode }) {\n return (\n <div className=\"slide-content prose dark:prose-invert prose-headings:tracking-tight prose-p:leading-relaxed\">\n {children}\n </div>\n )\n}\n"],"names":[],"mappings":";;;AAOO,MAAM,sBAAsB;AAAA,EACjC,GAAG;AAAA,EACH;AACF;"}
@@ -5,7 +5,6 @@ import PostList from "../components/post-list.js";
5
5
  import { ErrorDisplay } from "../components/page-error.js";
6
6
  import { RunningBar } from "../components/running-bar.js";
7
7
  import { Header } from "../components/header.js";
8
- import { ContentTabs } from "../components/content-tabs.js";
9
8
  import { getViewCounts, filterVisiblePosts, directoryToPostEntries } from "../lib/content-classification.js";
10
9
  import siteConfig from "virtual:veslx-config";
11
10
  function Home({ view }) {
@@ -16,7 +15,7 @@ function Home({ view }) {
16
15
  const { directory, error } = useDirectory(directoryPath);
17
16
  const activeView = view ?? config.defaultView;
18
17
  const isRoot = path === "." || path === "" || isViewRoute;
19
- const counts = directory ? getViewCounts(filterVisiblePosts(directoryToPostEntries(directory))) : { posts: 0, docs: 0, all: 0 };
18
+ directory ? getViewCounts(filterVisiblePosts(directoryToPostEntries(directory))) : {};
20
19
  if (error) {
21
20
  return /* @__PURE__ */ jsx(ErrorDisplay, { error, path });
22
21
  }
@@ -30,10 +29,7 @@ function Home({ view }) {
30
29
  /* @__PURE__ */ jsx("h1", { className: "text-2xl md:text-3xl font-semibold tracking-tight text-foreground", children: config.name }),
31
30
  config.description && /* @__PURE__ */ jsx("p", { className: "mt-2 text-muted-foreground", children: config.description })
32
31
  ] }),
33
- /* @__PURE__ */ jsxs("div", { className: "", children: [
34
- isRoot && directory && /* @__PURE__ */ jsx("div", { className: "animate-fade-in", children: /* @__PURE__ */ jsx(ContentTabs, { value: activeView, counts }) }),
35
- directory && /* @__PURE__ */ jsx("div", { className: "animate-fade-in", children: /* @__PURE__ */ jsx(PostList, { directory, view: isRoot ? activeView : "all" }) })
36
- ] })
32
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: directory && /* @__PURE__ */ jsx("div", { className: "animate-fade-in", children: /* @__PURE__ */ jsx(PostList, { directory, view: isRoot ? activeView : "all" }) }) })
37
33
  ] })
38
34
  ] })
39
35
  ] });
@@ -1 +1 @@
1
- {"version":3,"file":"home.js","sources":["../../../src/pages/home.tsx"],"sourcesContent":["import { useParams } from \"react-router-dom\"\nimport { useDirectory } from \"../../plugin/src/client\";\nimport Loading from \"@/components/loading\";\nimport PostList from \"@/components/post-list\";\nimport { ErrorDisplay } from \"@/components/page-error\";\nimport { RunningBar } from \"@/components/running-bar\";\nimport { Header } from \"@/components/header\";\nimport { ContentTabs } from \"@/components/content-tabs\";\nimport {\n type ContentView,\n directoryToPostEntries,\n filterVisiblePosts,\n getViewCounts,\n} from \"@/lib/content-classification\";\nimport siteConfig from \"virtual:veslx-config\";\n\ninterface HomeProps {\n view?: ContentView;\n}\n\nexport function Home({ view }: HomeProps) {\n const { \"*\": path = \".\" } = useParams();\n const config = siteConfig;\n\n // Normalize path - \"posts\", \"docs\", and \"all\" are view routes, not directories\n const isViewRoute = path === \"posts\" || path === \"docs\" || path === \"all\";\n const directoryPath = isViewRoute ? \".\" : path;\n\n const { directory, loading, error } = useDirectory(directoryPath)\n\n // Use prop view, fallback to config default\n const activeView = view ?? config.defaultView;\n\n const isRoot = path === \".\" || path === \"\" || isViewRoute;\n\n // Calculate counts for tabs (only meaningful on root)\n const counts = directory\n ? getViewCounts(filterVisiblePosts(directoryToPostEntries(directory)))\n : { posts: 0, docs: 0, all: 0 };\n\n if (error) {\n return <ErrorDisplay error={error} path={path} />;\n }\n\n if (loading) {\n return (\n <Loading />\n )\n }\n\n return (\n <div className=\"flex min-h-screen flex-col bg-background noise-overlay\">\n <RunningBar />\n <Header />\n <main className=\"flex-1 mx-auto w-full max-w-[var(--content-width)] px-[var(--page-padding)]\">\n <title>{isRoot ? config.name : `${config.name} - ${path}`}</title>\n <main className=\"flex flex-col gap-8 mb-32 mt-32\">\n {isRoot && (\n <div className=\"animate-fade-in\">\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-foreground\">\n {config.name}\n </h1>\n {config.description && (\n <p className=\"mt-2 text-muted-foreground\">\n {config.description}\n </p>\n )}\n </div>\n )}\n\n <div className=\"\">\n {isRoot && directory && (\n <div className=\"animate-fade-in\">\n <ContentTabs value={activeView} counts={counts} />\n </div>\n )}\n {directory && (\n <div className=\"animate-fade-in\">\n <PostList directory={directory} view={isRoot ? activeView : 'all'} />\n </div>\n )}\n </div>\n </main>\n </main>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;AAoBO,SAAS,KAAK,EAAE,QAAmB;AACxC,QAAM,EAAE,KAAK,OAAO,IAAA,IAAQ,UAAA;AAC5B,QAAM,SAAS;AAGf,QAAM,cAAc,SAAS,WAAW,SAAS,UAAU,SAAS;AACpE,QAAM,gBAAgB,cAAc,MAAM;AAE1C,QAAM,EAAE,WAAoB,UAAU,aAAa,aAAa;AAGhE,QAAM,aAAa,QAAQ,OAAO;AAElC,QAAM,SAAS,SAAS,OAAO,SAAS,MAAM;AAG9C,QAAM,SAAS,YACX,cAAc,mBAAmB,uBAAuB,SAAS,CAAC,CAAC,IACnE,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,EAAA;AAE9B,MAAI,OAAO;AACT,WAAO,oBAAC,cAAA,EAAa,OAAc,KAAA,CAAY;AAAA,EACjD;AAQA,SACE,qBAAC,OAAA,EAAI,WAAU,0DACb,UAAA;AAAA,IAAA,oBAAC,YAAA,EAAW;AAAA,wBACX,QAAA,EAAO;AAAA,IACR,qBAAC,QAAA,EAAK,WAAU,+EACd,UAAA;AAAA,MAAA,oBAAC,SAAA,EAAO,mBAAS,OAAO,OAAO,GAAG,OAAO,IAAI,MAAM,IAAI,GAAA,CAAG;AAAA,MAC1D,qBAAC,QAAA,EAAK,WAAU,mCACb,UAAA;AAAA,QAAA,UACC,qBAAC,OAAA,EAAI,WAAU,mBACb,UAAA;AAAA,UAAA,oBAAC,MAAA,EAAG,WAAU,qEACX,UAAA,OAAO,MACV;AAAA,UACC,OAAO,eACN,oBAAC,OAAE,WAAU,8BACV,iBAAO,YAAA,CACV;AAAA,QAAA,GAEJ;AAAA,QAGF,qBAAC,OAAA,EAAI,WAAU,IACZ,UAAA;AAAA,UAAA,UAAU,aACT,oBAAC,OAAA,EAAI,WAAU,mBACb,8BAAC,aAAA,EAAY,OAAO,YAAY,OAAA,CAAgB,EAAA,CAClD;AAAA,UAED,aACC,oBAAC,OAAA,EAAI,WAAU,mBACb,UAAA,oBAAC,UAAA,EAAS,WAAsB,MAAM,SAAS,aAAa,MAAA,CAAO,EAAA,CACrE;AAAA,QAAA,EAAA,CAEJ;AAAA,MAAA,EAAA,CACF;AAAA,IAAA,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
1
+ {"version":3,"file":"home.js","sources":["../../../src/pages/home.tsx"],"sourcesContent":["import { useParams } from \"react-router-dom\"\nimport { useDirectory } from \"../../plugin/src/client\";\nimport Loading from \"@/components/loading\";\nimport PostList from \"@/components/post-list\";\nimport { ErrorDisplay } from \"@/components/page-error\";\nimport { RunningBar } from \"@/components/running-bar\";\nimport { Header } from \"@/components/header\";\nimport { ContentTabs } from \"@/components/content-tabs\";\nimport {\n type ContentView,\n directoryToPostEntries,\n filterVisiblePosts,\n getViewCounts,\n} from \"@/lib/content-classification\";\nimport siteConfig from \"virtual:veslx-config\";\n\ninterface HomeProps {\n view?: ContentView;\n}\n\nexport function Home({ view }: HomeProps) {\n const { \"*\": path = \".\" } = useParams();\n const config = siteConfig;\n\n // Normalize path - \"posts\", \"docs\", and \"all\" are view routes, not directories\n const isViewRoute = path === \"posts\" || path === \"docs\" || path === \"all\";\n const directoryPath = isViewRoute ? \".\" : path;\n\n const { directory, loading, error } = useDirectory(directoryPath)\n\n // Use prop view, fallback to config default\n const activeView = view ?? config.defaultView;\n\n const isRoot = path === \".\" || path === \"\" || isViewRoute;\n\n // Calculate counts for tabs (only meaningful on root)\n const counts = directory\n ? getViewCounts(filterVisiblePosts(directoryToPostEntries(directory)))\n : { posts: 0, docs: 0, all: 0 };\n\n if (error) {\n return <ErrorDisplay error={error} path={path} />;\n }\n\n if (loading) {\n return (\n <Loading />\n )\n }\n\n return (\n <div className=\"flex min-h-screen flex-col bg-background noise-overlay\">\n <RunningBar />\n <Header />\n <main className=\"flex-1 mx-auto w-full max-w-[var(--content-width)] px-[var(--page-padding)]\">\n <title>{isRoot ? config.name : `${config.name} - ${path}`}</title>\n <main className=\"flex flex-col gap-8 mb-32 mt-32\">\n {isRoot && (\n <div className=\"animate-fade-in\">\n <h1 className=\"text-2xl md:text-3xl font-semibold tracking-tight text-foreground\">\n {config.name}\n </h1>\n {config.description && (\n <p className=\"mt-2 text-muted-foreground\">\n {config.description}\n </p>\n )}\n </div>\n )}\n\n <div className=\"flex flex-col gap-2\">\n {/* {isRoot && directory && (\n <div className=\"animate-fade-in\">\n <ContentTabs value={activeView} counts={counts} />\n </div>\n )} */}\n {directory && (\n <div className=\"animate-fade-in\">\n <PostList directory={directory} view={isRoot ? activeView : 'all'} />\n </div>\n )}\n </div>\n </main>\n </main>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;AAoBO,SAAS,KAAK,EAAE,QAAmB;AACxC,QAAM,EAAE,KAAK,OAAO,IAAA,IAAQ,UAAA;AAC5B,QAAM,SAAS;AAGf,QAAM,cAAc,SAAS,WAAW,SAAS,UAAU,SAAS;AACpE,QAAM,gBAAgB,cAAc,MAAM;AAE1C,QAAM,EAAE,WAAoB,UAAU,aAAa,aAAa;AAGhE,QAAM,aAAa,QAAQ,OAAO;AAElC,QAAM,SAAS,SAAS,OAAO,SAAS,MAAM;AAG/B,cACX,cAAc,mBAAmB,uBAAuB,SAAS,CAAC,CAAC,IACnE,CAA4B;AAEhC,MAAI,OAAO;AACT,WAAO,oBAAC,cAAA,EAAa,OAAc,KAAA,CAAY;AAAA,EACjD;AAQA,SACE,qBAAC,OAAA,EAAI,WAAU,0DACb,UAAA;AAAA,IAAA,oBAAC,YAAA,EAAW;AAAA,wBACX,QAAA,EAAO;AAAA,IACR,qBAAC,QAAA,EAAK,WAAU,+EACd,UAAA;AAAA,MAAA,oBAAC,SAAA,EAAO,mBAAS,OAAO,OAAO,GAAG,OAAO,IAAI,MAAM,IAAI,GAAA,CAAG;AAAA,MAC1D,qBAAC,QAAA,EAAK,WAAU,mCACb,UAAA;AAAA,QAAA,UACC,qBAAC,OAAA,EAAI,WAAU,mBACb,UAAA;AAAA,UAAA,oBAAC,MAAA,EAAG,WAAU,qEACX,UAAA,OAAO,MACV;AAAA,UACC,OAAO,eACN,oBAAC,OAAE,WAAU,8BACV,iBAAO,YAAA,CACV;AAAA,QAAA,GAEJ;AAAA,4BAGD,OAAA,EAAI,WAAU,uBAMZ,UAAA,iCACE,OAAA,EAAI,WAAU,mBACb,UAAA,oBAAC,YAAS,WAAsB,MAAM,SAAS,aAAa,OAAO,GACrE,EAAA,CAEJ;AAAA,MAAA,EAAA,CACF;AAAA,IAAA,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
@@ -13,14 +13,14 @@ function SlidesPage() {
13
13
  const [searchParams, setSearchParams] = useSearchParams();
14
14
  const mdxPath = rawPath;
15
15
  const { Content, frontmatter, slideCount, loading, error } = useMDXSlides(mdxPath);
16
- const totalSlides = (slideCount || 0) + 1;
16
+ const totalSlides = slideCount || 0;
17
17
  const [currentSlide, setCurrentSlide] = useState(0);
18
18
  const titleSlideRef = useRef(null);
19
19
  const contentRef = useRef(null);
20
20
  useEffect(() => {
21
21
  const slideParam = parseInt(searchParams.get("slide") || "0", 10);
22
22
  if (slideParam > 0 && contentRef.current) {
23
- const slideEl = contentRef.current.querySelector(`[data-slide-index="${slideParam - 1}"]`);
23
+ const slideEl = contentRef.current.querySelector(`[data-slide-index="${slideParam}"]`);
24
24
  if (slideEl) {
25
25
  slideEl.scrollIntoView({ behavior: "auto" });
26
26
  }
@@ -33,7 +33,7 @@ function SlidesPage() {
33
33
  if (entry.isIntersecting) {
34
34
  const index = entry.target.getAttribute("data-slide-index");
35
35
  if (index !== null) {
36
- const slideNum = index === "title" ? 0 : parseInt(index, 10) + 1;
36
+ const slideNum = index === "title" ? 0 : parseInt(index, 10);
37
37
  setCurrentSlide(slideNum);
38
38
  setSearchParams(slideNum > 0 ? { slide: String(slideNum) } : {}, { replace: true });
39
39
  }
@@ -53,19 +53,15 @@ function SlidesPage() {
53
53
  }, [Content, setSearchParams]);
54
54
  const goToPrevious = useCallback(() => {
55
55
  const prev = Math.max(0, currentSlide - 1);
56
- if (prev === 0 && titleSlideRef.current) {
57
- titleSlideRef.current.scrollIntoView({ behavior: "smooth" });
58
- } else if (contentRef.current) {
59
- const slideEl = contentRef.current.querySelector(`[data-slide-index="${prev - 1}"]`);
56
+ if (contentRef.current) {
57
+ const slideEl = contentRef.current.querySelector(`[data-slide-index="${prev}"]`);
60
58
  slideEl == null ? void 0 : slideEl.scrollIntoView({ behavior: "smooth" });
61
59
  }
62
60
  }, [currentSlide]);
63
61
  const goToNext = useCallback(() => {
64
62
  const next = Math.min(totalSlides - 1, currentSlide + 1);
65
- if (next === 0 && titleSlideRef.current) {
66
- titleSlideRef.current.scrollIntoView({ behavior: "smooth" });
67
- } else if (contentRef.current) {
68
- const slideEl = contentRef.current.querySelector(`[data-slide-index="${next - 1}"]`);
63
+ if (contentRef.current) {
64
+ const slideEl = contentRef.current.querySelector(`[data-slide-index="${next}"]`);
69
65
  slideEl == null ? void 0 : slideEl.scrollIntoView({ behavior: "smooth" });
70
66
  }
71
67
  }, [currentSlide, totalSlides]);
@@ -1 +1 @@
1
- {"version":3,"file":"slides.js","sources":["../../../src/pages/slides.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { FULLSCREEN_DATA_ATTR } from \"@/lib/constants\";\nimport { useParams, useSearchParams } from \"react-router-dom\"\nimport Loading from \"@/components/loading\";\nimport { RunningBar } from \"@/components/running-bar\";\nimport { Header } from \"@/components/header\";\nimport { useMDXSlides } from \"@/hooks/use-mdx-content\";\nimport { slidesMdxComponents } from \"@/components/slides-renderer\";\nimport { FrontmatterProvider } from \"@/lib/frontmatter-context\";\n\n\nexport function SlidesPage() {\n const { \"*\": rawPath = \".\" } = useParams();\n const [searchParams, setSearchParams] = useSearchParams();\n\n // The path includes the .mdx extension from the route\n const mdxPath = rawPath;\n\n // Load the compiled MDX module (now includes slideCount export)\n const { Content, frontmatter, slideCount, loading, error } = useMDXSlides(mdxPath);\n\n // Total slides = 1 (title) + content slides\n const totalSlides = (slideCount || 0) + 1;\n\n const [currentSlide, setCurrentSlide] = useState(0);\n const titleSlideRef = useRef<HTMLDivElement>(null);\n const contentRef = useRef<HTMLDivElement>(null);\n\n // Scroll to slide on initial load if query param is set\n useEffect(() => {\n const slideParam = parseInt(searchParams.get(\"slide\") || \"0\", 10);\n if (slideParam > 0 && contentRef.current) {\n const slideEl = contentRef.current.querySelector(`[data-slide-index=\"${slideParam - 1}\"]`);\n if (slideEl) {\n slideEl.scrollIntoView({ behavior: \"auto\" });\n }\n }\n }, [searchParams, Content]);\n\n // Track current slide based on scroll position\n useEffect(() => {\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n const index = entry.target.getAttribute(\"data-slide-index\");\n if (index !== null) {\n const slideNum = index === \"title\" ? 0 : parseInt(index, 10) + 1;\n setCurrentSlide(slideNum);\n setSearchParams(slideNum > 0 ? { slide: String(slideNum) } : {}, { replace: true });\n }\n }\n }\n },\n { threshold: 0.5 }\n );\n\n // Observe title slide\n if (titleSlideRef.current) {\n observer.observe(titleSlideRef.current);\n }\n\n // Observe content slides\n if (contentRef.current) {\n const slides = contentRef.current.querySelectorAll(\"[data-slide-index]\");\n slides.forEach((slide) => observer.observe(slide));\n }\n\n return () => observer.disconnect();\n }, [Content, setSearchParams]);\n\n // Keyboard/scroll navigation helpers\n const goToPrevious = useCallback(() => {\n const prev = Math.max(0, currentSlide - 1);\n if (prev === 0 && titleSlideRef.current) {\n titleSlideRef.current.scrollIntoView({ behavior: \"smooth\" });\n } else if (contentRef.current) {\n const slideEl = contentRef.current.querySelector(`[data-slide-index=\"${prev - 1}\"]`);\n slideEl?.scrollIntoView({ behavior: \"smooth\" });\n }\n }, [currentSlide]);\n\n const goToNext = useCallback(() => {\n const next = Math.min(totalSlides - 1, currentSlide + 1);\n if (next === 0 && titleSlideRef.current) {\n titleSlideRef.current.scrollIntoView({ behavior: \"smooth\" });\n } else if (contentRef.current) {\n const slideEl = contentRef.current.querySelector(`[data-slide-index=\"${next - 1}\"]`);\n slideEl?.scrollIntoView({ behavior: \"smooth\" });\n }\n }, [currentSlide, totalSlides]);\n\n // Keyboard navigation\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"ArrowUp\" || e.key === \"ArrowLeft\" || e.key === \"k\") {\n e.preventDefault();\n goToPrevious();\n } else if (e.key === \"ArrowDown\" || e.key === \"ArrowRight\" || e.key === \"j\") {\n e.preventDefault();\n goToNext();\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [goToPrevious, goToNext]);\n\n if (loading) {\n return <Loading />\n }\n\n if (error) {\n return (\n <main className=\"min-h-screen bg-background container mx-auto max-w-4xl py-12\">\n <p className=\"text-center text-red-600\">{error.message}</p>\n </main>\n )\n }\n\n if (!Content) {\n return (\n <div className=\"flex items-center justify-center p-12 text-muted-foreground font-mono text-sm\">\n no slides found — use \"---\" to separate slides\n </div>\n );\n }\n\n return (\n <main className=\"slides-container\">\n <title>{frontmatter?.title}</title>\n <RunningBar />\n <Header\n slideControls={{\n current: currentSlide,\n total: totalSlides,\n onPrevious: goToPrevious,\n onNext: goToNext,\n }}\n />\n <FrontmatterProvider frontmatter={frontmatter}>\n <div {...{[FULLSCREEN_DATA_ATTR]: \"true\"}}>\n <div ref={contentRef}>\n <Content components={slidesMdxComponents} />\n </div>\n </div>\n </FrontmatterProvider>\n </main>\n )\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"slides.js","sources":["../../../src/pages/slides.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { FULLSCREEN_DATA_ATTR } from \"@/lib/constants\";\nimport { useParams, useSearchParams } from \"react-router-dom\"\nimport Loading from \"@/components/loading\";\nimport { RunningBar } from \"@/components/running-bar\";\nimport { Header } from \"@/components/header\";\nimport { useMDXSlides } from \"@/hooks/use-mdx-content\";\nimport { slidesMdxComponents } from \"@/components/slides-renderer\";\nimport { FrontmatterProvider } from \"@/lib/frontmatter-context\";\n\n\nexport function SlidesPage() {\n const { \"*\": rawPath = \".\" } = useParams();\n const [searchParams, setSearchParams] = useSearchParams();\n\n // The path includes the .mdx extension from the route\n const mdxPath = rawPath;\n\n // Load the compiled MDX module (now includes slideCount export)\n const { Content, frontmatter, slideCount, loading, error } = useMDXSlides(mdxPath);\n\n const totalSlides = slideCount || 0;\n\n const [currentSlide, setCurrentSlide] = useState(0);\n const titleSlideRef = useRef<HTMLDivElement>(null);\n const contentRef = useRef<HTMLDivElement>(null);\n\n // Scroll to slide on initial load if query param is set\n useEffect(() => {\n const slideParam = parseInt(searchParams.get(\"slide\") || \"0\", 10);\n if (slideParam > 0 && contentRef.current) {\n const slideEl = contentRef.current.querySelector(`[data-slide-index=\"${slideParam}\"]`);\n if (slideEl) {\n slideEl.scrollIntoView({ behavior: \"auto\" });\n }\n }\n }, [searchParams, Content]);\n\n // Track current slide based on scroll position\n useEffect(() => {\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n const index = entry.target.getAttribute(\"data-slide-index\");\n if (index !== null) {\n const slideNum = index === \"title\" ? 0 : parseInt(index, 10);\n setCurrentSlide(slideNum);\n setSearchParams(slideNum > 0 ? { slide: String(slideNum) } : {}, { replace: true });\n }\n }\n }\n },\n { threshold: 0.5 }\n );\n\n // Observe title slide\n if (titleSlideRef.current) {\n observer.observe(titleSlideRef.current);\n }\n\n // Observe content slides\n if (contentRef.current) {\n const slides = contentRef.current.querySelectorAll(\"[data-slide-index]\");\n slides.forEach((slide) => observer.observe(slide));\n }\n\n return () => observer.disconnect();\n }, [Content, setSearchParams]);\n\n // Keyboard/scroll navigation helpers\n const goToPrevious = useCallback(() => {\n const prev = Math.max(0, currentSlide - 1);\n if (contentRef.current) {\n const slideEl = contentRef.current.querySelector(`[data-slide-index=\"${prev}\"]`);\n slideEl?.scrollIntoView({ behavior: \"smooth\" });\n }\n }, [currentSlide]);\n\n const goToNext = useCallback(() => {\n const next = Math.min(totalSlides - 1, currentSlide + 1);\n if (contentRef.current) {\n const slideEl = contentRef.current.querySelector(`[data-slide-index=\"${next}\"]`);\n slideEl?.scrollIntoView({ behavior: \"smooth\" });\n }\n }, [currentSlide, totalSlides]);\n\n // Keyboard navigation\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"ArrowUp\" || e.key === \"ArrowLeft\" || e.key === \"k\") {\n e.preventDefault();\n goToPrevious();\n } else if (e.key === \"ArrowDown\" || e.key === \"ArrowRight\" || e.key === \"j\") {\n e.preventDefault();\n goToNext();\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [goToPrevious, goToNext]);\n\n if (loading) {\n return <Loading />\n }\n\n if (error) {\n return (\n <main className=\"min-h-screen bg-background container mx-auto max-w-4xl py-12\">\n <p className=\"text-center text-red-600\">{error.message}</p>\n </main>\n )\n }\n\n if (!Content) {\n return (\n <div className=\"flex items-center justify-center p-12 text-muted-foreground font-mono text-sm\">\n no slides found — use \"---\" to separate slides\n </div>\n );\n }\n\n return (\n <main className=\"slides-container\">\n <title>{frontmatter?.title}</title>\n <RunningBar />\n <Header\n slideControls={{\n current: currentSlide,\n total: totalSlides,\n onPrevious: goToPrevious,\n onNext: goToNext,\n }}\n />\n <FrontmatterProvider frontmatter={frontmatter}>\n <div {...{[FULLSCREEN_DATA_ATTR]: \"true\"}}>\n <div ref={contentRef}>\n <Content components={slidesMdxComponents} />\n </div>\n </div>\n </FrontmatterProvider>\n </main>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;AAWO,SAAS,aAAa;AAC3B,QAAM,EAAE,KAAK,UAAU,IAAA,IAAQ,UAAA;AAC/B,QAAM,CAAC,cAAc,eAAe,IAAI,gBAAA;AAGxC,QAAM,UAAU;AAGhB,QAAM,EAAE,SAAS,aAAa,YAAY,SAAS,MAAA,IAAU,aAAa,OAAO;AAEjF,QAAM,cAAc,cAAc;AAElC,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,CAAC;AAClD,QAAM,gBAAgB,OAAuB,IAAI;AACjD,QAAM,aAAa,OAAuB,IAAI;AAG9C,YAAU,MAAM;AACd,UAAM,aAAa,SAAS,aAAa,IAAI,OAAO,KAAK,KAAK,EAAE;AAChE,QAAI,aAAa,KAAK,WAAW,SAAS;AACxC,YAAM,UAAU,WAAW,QAAQ,cAAc,sBAAsB,UAAU,IAAI;AACrF,UAAI,SAAS;AACX,gBAAQ,eAAe,EAAE,UAAU,OAAA,CAAQ;AAAA,MAC7C;AAAA,IACF;AAAA,EACF,GAAG,CAAC,cAAc,OAAO,CAAC;AAG1B,YAAU,MAAM;AACd,UAAM,WAAW,IAAI;AAAA,MACnB,CAAC,YAAY;AACX,mBAAW,SAAS,SAAS;AAC3B,cAAI,MAAM,gBAAgB;AACxB,kBAAM,QAAQ,MAAM,OAAO,aAAa,kBAAkB;AAC1D,gBAAI,UAAU,MAAM;AAClB,oBAAM,WAAW,UAAU,UAAU,IAAI,SAAS,OAAO,EAAE;AAC3D,8BAAgB,QAAQ;AACxB,8BAAgB,WAAW,IAAI,EAAE,OAAO,OAAO,QAAQ,EAAA,IAAM,CAAA,GAAI,EAAE,SAAS,MAAM;AAAA,YACpF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,MACA,EAAE,WAAW,IAAA;AAAA,IAAI;AAInB,QAAI,cAAc,SAAS;AACzB,eAAS,QAAQ,cAAc,OAAO;AAAA,IACxC;AAGA,QAAI,WAAW,SAAS;AACtB,YAAM,SAAS,WAAW,QAAQ,iBAAiB,oBAAoB;AACvE,aAAO,QAAQ,CAAC,UAAU,SAAS,QAAQ,KAAK,CAAC;AAAA,IACnD;AAEA,WAAO,MAAM,SAAS,WAAA;AAAA,EACxB,GAAG,CAAC,SAAS,eAAe,CAAC;AAG7B,QAAM,eAAe,YAAY,MAAM;AACrC,UAAM,OAAO,KAAK,IAAI,GAAG,eAAe,CAAC;AACzC,QAAI,WAAW,SAAS;AACtB,YAAM,UAAU,WAAW,QAAQ,cAAc,sBAAsB,IAAI,IAAI;AAC/E,yCAAS,eAAe,EAAE,UAAU,SAAA;AAAA,IACtC;AAAA,EACF,GAAG,CAAC,YAAY,CAAC;AAEjB,QAAM,WAAW,YAAY,MAAM;AACjC,UAAM,OAAO,KAAK,IAAI,cAAc,GAAG,eAAe,CAAC;AACvD,QAAI,WAAW,SAAS;AACtB,YAAM,UAAU,WAAW,QAAQ,cAAc,sBAAsB,IAAI,IAAI;AAC/E,yCAAS,eAAe,EAAE,UAAU,SAAA;AAAA,IACtC;AAAA,EACF,GAAG,CAAC,cAAc,WAAW,CAAC;AAG9B,YAAU,MAAM;AACd,UAAM,gBAAgB,CAAC,MAAqB;AAC1C,UAAI,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,EAAE,QAAQ,KAAK;AACjE,UAAE,eAAA;AACF,qBAAA;AAAA,MACF,WAAW,EAAE,QAAQ,eAAe,EAAE,QAAQ,gBAAgB,EAAE,QAAQ,KAAK;AAC3E,UAAE,eAAA;AACF,iBAAA;AAAA,MACF;AAAA,IACF;AAEA,WAAO,iBAAiB,WAAW,aAAa;AAChD,WAAO,MAAM,OAAO,oBAAoB,WAAW,aAAa;AAAA,EAClE,GAAG,CAAC,cAAc,QAAQ,CAAC;AAE3B,MAAI,SAAS;AACX,+BAAQ,SAAA,EAAQ;AAAA,EAClB;AAEA,MAAI,OAAO;AACT,WACE,oBAAC,QAAA,EAAK,WAAU,gEACd,UAAA,oBAAC,OAAE,WAAU,4BAA4B,UAAA,MAAM,QAAA,CAAQ,GACzD;AAAA,EAEJ;AAEA,MAAI,CAAC,SAAS;AACZ,WACE,oBAAC,OAAA,EAAI,WAAU,iFAAgF,UAAA,kDAE/F;AAAA,EAEJ;AAEA,SACE,qBAAC,QAAA,EAAK,WAAU,oBACd,UAAA;AAAA,IAAA,oBAAC,SAAA,EAAO,qDAAa,MAAA,CAAM;AAAA,wBAC1B,YAAA,EAAW;AAAA,IACZ;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,eAAe;AAAA,UACb,SAAS;AAAA,UACT,OAAO;AAAA,UACP,YAAY;AAAA,UACZ,QAAQ;AAAA,QAAA;AAAA,MACV;AAAA,IAAA;AAAA,IAEF,oBAAC,uBAAoB,aACnB,UAAA,oBAAC,SAAK,GAAG,EAAC,CAAC,oBAAoB,GAAG,OAAA,GAChC,UAAA,oBAAC,OAAA,EAAI,KAAK,YACR,UAAA,oBAAC,WAAQ,YAAY,oBAAA,CAAqB,EAAA,CAC5C,EAAA,CACF,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
package/index.html CHANGED
@@ -5,6 +5,19 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/logo_dark.png" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>veslx</title>
8
+ <!-- Prevent FOUC: apply theme before render -->
9
+ <script>
10
+ (function() {
11
+ var theme = localStorage.getItem('theme');
12
+ var isDark = theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches);
13
+ if (isDark) document.documentElement.classList.add('dark');
14
+ document.documentElement.style.colorScheme = isDark ? 'dark' : 'light';
15
+ })();
16
+ </script>
17
+ <style>
18
+ html { background: hsl(0 0% 100%); }
19
+ html.dark { background: hsl(0 0% 7%); }
20
+ </style>
8
21
  <!-- Google Fonts: DM Sans + DM Mono -->
9
22
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
23
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veslx",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,9 +8,9 @@ interface ContentTabsProps {
8
8
  }
9
9
 
10
10
  const views: { key: ContentView; label: string; path: string }[] = [
11
- { key: "posts", label: "posts", path: "/posts" },
12
- { key: "docs", label: "docs", path: "/docs" },
13
- { key: "all", label: "all", path: "/all" },
11
+ { key: "posts", label: "Posts", path: "/posts" },
12
+ { key: "docs", label: "Docs", path: "/docs" },
13
+ // { key: "all", label: "All", path: "/all" },
14
14
  ];
15
15
 
16
16
  export function ContentTabs({ value, counts }: ContentTabsProps) {
@@ -28,7 +28,7 @@ export function ContentTabs({ value, counts }: ContentTabsProps) {
28
28
  };
29
29
 
30
30
  return (
31
- <nav className="flex justify-end items-center gap-3 font-mono font-medium text-xs text-muted-foreground">
31
+ <nav className="flex items-center gap-3 font-mono font-medium text-xs text-muted-foreground">
32
32
  {views.map((view) => {
33
33
  const disabled = isDisabled(view.key);
34
34
 
@@ -4,7 +4,7 @@ export function FigureCaption({ caption, label }: { caption?: string; label?: st
4
4
  if (!caption && !label) return null;
5
5
 
6
6
  return (
7
- <figcaption className="px-[calc((var(--gallery-width)-var(--content-width))/2)] mt-4">
7
+ <figcaption className="mt-4">
8
8
  <p className="text-[13px] leading-[1.6] text-muted-foreground">
9
9
  {label && (
10
10
  <span className="font-semibold text-foreground tracking-tight">
@@ -4,7 +4,7 @@ export function FigureHeader({ title, subtitle }: { title?: string; subtitle?: s
4
4
  if (!title && !subtitle) return null;
5
5
 
6
6
  return (
7
- <div className="px-[calc((var(--gallery-width)-var(--content-width))/2)] mb-4">
7
+ <div className="mb-4">
8
8
  {title && (
9
9
  <h3 className="text-[15px] font-medium tracking-[-0.01em] text-foreground">
10
10
  {renderMathInText(title)}
@@ -31,7 +31,7 @@ export function Lightbox({
31
31
 
32
32
  return createPortal(
33
33
  <div
34
- className="fixed inset-0 z-[9999] bg-background/98 backdrop-blur-md animate-fade-in-slow"
34
+ className="fixed inset-0 z-[9999] bg-background/98 backdrop-blur-md animate-[fade-in_150ms_ease-out]"
35
35
  onClick={onClose}
36
36
  {...{ [FULLSCREEN_DATA_ATTR]: "true" }}
37
37
  style={{ top: 0, left: 0, right: 0, bottom: 0 }}
@@ -29,13 +29,15 @@ function getImageUrl(path: string): string {
29
29
 
30
30
  export default function Gallery({
31
31
  path,
32
- globs = null,
32
+ globs = null,
33
33
  caption,
34
34
  captionLabel,
35
35
  title,
36
36
  subtitle,
37
37
  limit = null,
38
38
  page = 0,
39
+ children,
40
+ childAlign = "right",
39
41
  }: {
40
42
  path?: string;
41
43
  globs?: string[] | null;
@@ -45,6 +47,8 @@ export default function Gallery({
45
47
  subtitle?: string;
46
48
  limit?: number | null;
47
49
  page?: number;
50
+ children?: React.ReactNode;
51
+ childAlign?: "left" | "right";
48
52
  }) {
49
53
  const { paths, isLoading, isEmpty } = useGalleryImages({
50
54
  path,
@@ -91,39 +95,74 @@ export default function Gallery({
91
95
  );
92
96
  }
93
97
 
98
+ const isCompact = images.length <= 3;
99
+ const isSingleWithChildren = images.length === 1 && children;
100
+
101
+ const imageElement = (index: number, img: LightboxImage, className?: string) => (
102
+ <div
103
+ key={index}
104
+ className={`aspect-square overflow-hidden rounded-sm bg-muted/10 cursor-pointer group ${className || ''}`}
105
+ onClick={() => lightbox.open(index)}
106
+ >
107
+ <LoadingImage
108
+ src={img.src}
109
+ alt={img.label}
110
+ className="object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
111
+ />
112
+ </div>
113
+ );
114
+
94
115
  return (
95
116
  <>
96
- <figure className="not-prose relative py-6 md:py-8 -mx-[calc((var(--gallery-width)-var(--content-width))/2+var(--page-padding))]">
97
- <FigureHeader title={title} subtitle={subtitle} />
117
+ <figure className={`not-prose relative py-6 md:py-8 ${isCompact ? '' : '-mx-[calc((var(--gallery-width)-var(--content-width))/2+var(--page-padding))] px-[calc((var(--gallery-width)-var(--content-width))/2)]'}`}>
118
+ {!isSingleWithChildren && <FigureHeader title={title} subtitle={subtitle} />}
98
119
 
99
- <Carousel className="w-full">
100
- <CarouselContent className={`-ml-2 md:-ml-3 ${images.length < 3 ? 'justify-center' : ''}`}>
101
- {images.map((img, index) => (
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' : ''}`}
105
- onClick={() => lightbox.open(index)}
106
- >
107
- <div className="aspect-square overflow-hidden rounded-sm 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>
114
- </CarouselItem>
115
- ))}
116
- </CarouselContent>
120
+ {isSingleWithChildren ? (
121
+ <div className={`flex gap-6 ${childAlign === 'left' ? '' : 'flex-row-reverse'}`}>
122
+ <div className="flex-1 text-sm leading-relaxed text-foreground/90 space-y-3 [&>ul]:space-y-1.5 [&>ul]:list-disc [&>ul]:pl-5 [&>ol]:space-y-1.5 [&>ol]:list-decimal [&>ol]:pl-5 flex flex-col">
123
+ {(title || subtitle) && <div className="invisible"><FigureHeader title={title} subtitle={subtitle} /></div>}
124
+ <div>{children}</div>
125
+ </div>
126
+ <div className="w-3/5 flex-shrink-0">
127
+ <FigureHeader title={title} subtitle={subtitle} />
128
+ {imageElement(0, images[0])}
129
+ <FigureCaption caption={caption} label={captionLabel} />
130
+ </div>
131
+ </div>
132
+ ) : isCompact ? (
133
+ <div className="flex gap-3">
134
+ {images.map((img, index) => imageElement(index, img, 'flex-1'))}
135
+ </div>
136
+ ) : (
137
+ <Carousel className="w-full">
138
+ <CarouselContent className="-ml-2 md:-ml-3">
139
+ {images.map((img, index) => (
140
+ <CarouselItem
141
+ key={index}
142
+ className="pl-2 md:pl-3 md:basis-1/2 lg:basis-1/3 cursor-pointer group"
143
+ onClick={() => lightbox.open(index)}
144
+ >
145
+ <div className="aspect-square overflow-hidden rounded-sm bg-muted/10">
146
+ <LoadingImage
147
+ src={img.src}
148
+ alt={img.label}
149
+ className="object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
150
+ />
151
+ </div>
152
+ </CarouselItem>
153
+ ))}
154
+ </CarouselContent>
117
155
 
118
- {images.length > 3 && (
119
- <>
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" />
122
- </>
123
- )}
124
- </Carousel>
156
+ {images.length > 3 && (
157
+ <>
158
+ <CarouselPrevious className="left-4 bg-background/80 backdrop-blur-sm border-border/50 hover:bg-background hover:border-border" />
159
+ <CarouselNext className="right-4 bg-background/80 backdrop-blur-sm border-border/50 hover:bg-background hover:border-border" />
160
+ </>
161
+ )}
162
+ </Carousel>
163
+ )}
125
164
 
126
- <FigureCaption caption={caption} label={captionLabel} />
165
+ {!isSingleWithChildren && <FigureCaption caption={caption} label={captionLabel} />}
127
166
  </figure>
128
167
 
129
168
  {lightbox.isOpen && lightbox.selectedIndex !== null && (
@@ -1,8 +1,9 @@
1
- import { Link } from "react-router-dom";
1
+ import { Link, useParams } 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
5
  import siteConfig from "virtual:veslx-config";
6
+ import { cn } from "@/lib/utils";
6
7
 
7
8
  interface HeaderProps {
8
9
  slideControls?: {
@@ -16,6 +17,8 @@ interface HeaderProps {
16
17
  export function Header({ slideControls }: HeaderProps = {}) {
17
18
  const config = siteConfig;
18
19
 
20
+ const { "*": path } = useParams()
21
+
19
22
  return (
20
23
  <header className="print:hidden fixed top-0 left-0 right-0 z-40">
21
24
  <div className="mx-auto w-full px-[var(--page-padding)] flex items-center gap-8 py-4">
@@ -30,31 +33,47 @@ export function Header({ slideControls }: HeaderProps = {}) {
30
33
 
31
34
  <div className="flex-1" />
32
35
 
33
- {/* Slide navigation controls */}
34
- {slideControls && (
35
- <nav className="flex items-center gap-1">
36
- <button
37
- onClick={slideControls.onPrevious}
38
- className="p-1.5 text-muted-foreground/70 hover:text-foreground transition-colors duration-200"
39
- title="Previous slide (↑)"
40
- >
41
- <ChevronUp className="h-4 w-4" />
42
- </button>
43
- <span className="font-mono text-xs text-muted-foreground/70 tabular-nums min-w-[3ch] text-center">
44
- {slideControls.current + 1}/{slideControls.total}
45
- </span>
46
- <button
47
- onClick={slideControls.onNext}
48
- className="p-1.5 text-muted-foreground/70 hover:text-foreground transition-colors duration-200"
49
- title="Next slide (↓)"
50
- >
51
- <ChevronDown className="h-4 w-4" />
52
- </button>
53
- </nav>
54
- )}
55
-
56
36
  {/* Navigation */}
57
- <nav className="flex items-center gap-2">
37
+ <nav className="flex items-center gap-4">
38
+ {slideControls && (
39
+ <>
40
+ <button
41
+ onClick={slideControls.onPrevious}
42
+ className="p-1.5 text-muted-foreground/70 hover:text-foreground transition-colors duration-200"
43
+ title="Previous slide (↑)"
44
+ >
45
+ <ChevronUp className="h-4 w-4" />
46
+ </button>
47
+ <span className="font-mono text-xs text-muted-foreground/70 tabular-nums min-w-[3ch] text-center">
48
+ {slideControls.current + 1}/{slideControls.total}
49
+ </span>
50
+ <button
51
+ onClick={slideControls.onNext}
52
+ className="p-1.5 text-muted-foreground/70 hover:text-foreground transition-colors duration-200"
53
+ title="Next slide (↓)"
54
+ >
55
+ <ChevronDown className="h-4 w-4" />
56
+ </button>
57
+ </>
58
+ )}
59
+ <Link
60
+ to={`/posts`}
61
+ className={cn(
62
+ "font-medium text-muted-foreground/70 hover:text-foreground transition-colors duration-300",
63
+ path?.startsWith("posts") && "font-semibold text-foreground",
64
+ )}
65
+ >
66
+ Posts
67
+ </Link>
68
+ <Link
69
+ to={`/docs`}
70
+ className={cn(
71
+ "font-medium text-muted-foreground/70 hover:text-foreground transition-colors duration-300",
72
+ path?.startsWith("docs") && "font-semibold text-foreground",
73
+ )}
74
+ >
75
+ Docs
76
+ </Link>
58
77
  {config.github && (
59
78
  <Link
60
79
  to={`https://github.com/${config.github}`}
@@ -3,6 +3,10 @@ import Gallery from '@/components/gallery'
3
3
  import { ParameterTable } from '@/components/parameter-table'
4
4
  import { ParameterBadge } from '@/components/parameter-badge'
5
5
  import { FrontMatter } from './front-matter'
6
+ import { HeroSlide } from './slides/hero-slide'
7
+ import { FigureSlide } from './slides/figure-slide'
8
+ import { TextSlide } from './slides/text-slide'
9
+ import { SlideOutline } from './slides/slide-outline'
6
10
 
7
11
  /**
8
12
  * Smart link component that uses React Router for internal links
@@ -77,6 +81,14 @@ export const mdxComponents = {
77
81
 
78
82
  ParameterBadge,
79
83
 
84
+ HeroSlide,
85
+
86
+ FigureSlide,
87
+
88
+ TextSlide,
89
+
90
+ SlideOutline,
91
+
80
92
  // Headings - clean sans-serif
81
93
  h1: (props: React.HTMLAttributes<HTMLHeadingElement>) => {
82
94
  const id = generateId(props.children)
@@ -2,7 +2,7 @@ import { Link } from "react-router-dom";
2
2
  import { cn } from "@/lib/utils";
3
3
  import type { DirectoryEntry } from "../../plugin/src/lib";
4
4
  import { formatDate } from "@/lib/format-date";
5
- import { ArrowRight } from "lucide-react";
5
+ import { ArrowRight, Presentation } from "lucide-react";
6
6
  import {
7
7
  type ContentView,
8
8
  type PostEntry,
@@ -117,6 +117,8 @@ export default function PostList({ directory, view = 'all' }: PostListProps) {
117
117
  linkPath = `/${post.path}`;
118
118
  }
119
119
 
120
+ const isSlides = linkPath.endsWith('SLIDES.mdx');
121
+
120
122
  return (
121
123
  <Link
122
124
  key={post.path}
@@ -126,15 +128,7 @@ export default function PostList({ directory, view = 'all' }: PostListProps) {
126
128
  "transition-colors duration-150",
127
129
  )}
128
130
  >
129
- <article className="flex items-start gap-4">
130
- {/* Date - left side, fixed width */}
131
- <time
132
- dateTime={date?.toISOString()}
133
- className="font-mono text-xs text-muted-foreground tabular-nums w-20 flex-shrink-0 pt-0.5"
134
- >
135
- {date ? formatDate(date) : <span className="text-muted-foreground/30">—</span>}
136
- </time>
137
-
131
+ <article className="flex items-center gap-4">
138
132
  {/* Main content */}
139
133
  <div className="flex-1 min-w-0">
140
134
  <h3 className={cn(
@@ -152,6 +146,16 @@ export default function PostList({ directory, view = 'all' }: PostListProps) {
152
146
  </p>
153
147
  )}
154
148
  </div>
149
+
150
+ {isSlides && (
151
+ <Presentation className="h-3 w-3 text-muted-foreground" />
152
+ )}
153
+ <time
154
+ dateTime={date?.toISOString()}
155
+ className="font-mono text-xs text-muted-foreground tabular-nums w-20 flex-shrink-0"
156
+ >
157
+ {date && formatDate(date)}
158
+ </time>
155
159
  </article>
156
160
  </Link>
157
161
  );
@@ -0,0 +1,16 @@
1
+
2
+
3
+ export function FigureSlide({
4
+ title,
5
+ src,
6
+ }: {
7
+ title: string;
8
+ src: string;
9
+ }) {
10
+ return (
11
+ <div className="figure-slide">
12
+ <h2>{title}</h2>
13
+ <img src={src} alt={title} />
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,34 @@
1
+
2
+ export function HeroSlide({
3
+ title,
4
+ subtitle,
5
+ author,
6
+ date,
7
+ }: {
8
+ title: string;
9
+ subtitle?: string;
10
+ author?: string;
11
+ date?: string;
12
+ }) {
13
+ return (
14
+ <div>
15
+ <h1 className="text-[clamp(2.5rem,6vw,5rem)] font-semibold leading-[1.1] tracking-[-0.02em] text-foreground text-balance">
16
+ {title}
17
+ </h1>
18
+
19
+ {subtitle && (
20
+ <p className="text-[clamp(1rem,2vw,1.5rem)] text-muted-foreground max-w-[50ch] leading-relaxed">
21
+ {subtitle}
22
+ </p>
23
+ )}
24
+
25
+ {(author || date) && (
26
+ <div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted-foreground mt-4">
27
+ {author && <span>{author}</span>}
28
+ {author && date && <span className="text-border">·</span>}
29
+ {date && <span>{date}</span>}
30
+ </div>
31
+ )}
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,38 @@
1
+
2
+
3
+ export function SlideOutline({
4
+ children,
5
+ className,
6
+ size="md"
7
+ }: {
8
+ children?: React.ReactNode;
9
+ className?: string;
10
+ size?: "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "full";
11
+ }) {
12
+
13
+ const wClasses: Record<string, string> = {
14
+ sm: "max-w-lg",
15
+ md: "max-w-2xl",
16
+ lg: "max-w-5xl",
17
+ xl: "max-w-7xl",
18
+ full: "max-w-full",
19
+ };
20
+
21
+ const wClassName = `${wClasses[size]} ${className ?? ""}`;
22
+
23
+ const hClasses: Record<string, string> = {
24
+ sm: "min-h-[300px]",
25
+ md: "min-h-[400px]",
26
+ lg: "min-h-[500px]",
27
+ xl: "min-h-[600px]",
28
+ full: "min-h-[600px]",
29
+ };
30
+
31
+ const hClassName = `${hClasses[size]} ${className ?? ""}`;
32
+
33
+ return (
34
+ <div className={`border rounded relative left-1/2 -translate-x-1/2 w-screen ${wClassName} ${hClassName} ${className}`}>
35
+ {children}
36
+ </div>
37
+ );
38
+ }