veslx 0.1.27 → 0.1.29

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 (50) hide show
  1. package/bin/lib/build.ts +2 -1
  2. package/bin/lib/export.ts +214 -0
  3. package/bin/lib/serve.ts +2 -1
  4. package/dist/client/components/front-matter.js +46 -1
  5. package/dist/client/components/front-matter.js.map +1 -1
  6. package/dist/client/components/header.js +2 -24
  7. package/dist/client/components/header.js.map +1 -1
  8. package/dist/client/components/mdx-components.js +2 -0
  9. package/dist/client/components/mdx-components.js.map +1 -1
  10. package/dist/client/components/post-list-item.js +43 -0
  11. package/dist/client/components/post-list-item.js.map +1 -0
  12. package/dist/client/components/post-list.js +54 -79
  13. package/dist/client/components/post-list.js.map +1 -1
  14. package/dist/client/hooks/use-mdx-content.js +64 -4
  15. package/dist/client/hooks/use-mdx-content.js.map +1 -1
  16. package/dist/client/lib/content-classification.js +1 -22
  17. package/dist/client/lib/content-classification.js.map +1 -1
  18. package/dist/client/pages/content-router.js +2 -10
  19. package/dist/client/pages/content-router.js.map +1 -1
  20. package/dist/client/pages/home.js +20 -23
  21. package/dist/client/pages/home.js.map +1 -1
  22. package/dist/client/pages/index-post.js +34 -0
  23. package/dist/client/pages/index-post.js.map +1 -0
  24. package/dist/client/pages/post.js +1 -3
  25. package/dist/client/pages/post.js.map +1 -1
  26. package/dist/client/pages/slides.js +4 -3
  27. package/dist/client/pages/slides.js.map +1 -1
  28. package/package.json +1 -1
  29. package/plugin/src/plugin.ts +127 -30
  30. package/plugin/src/types.ts +34 -4
  31. package/src/components/front-matter.tsx +60 -3
  32. package/src/components/header.tsx +2 -20
  33. package/src/components/mdx-components.tsx +3 -1
  34. package/src/components/post-list-item.tsx +54 -0
  35. package/src/components/post-list.tsx +74 -116
  36. package/src/components/welcome.tsx +2 -2
  37. package/src/hooks/use-mdx-content.ts +96 -7
  38. package/src/index.css +17 -0
  39. package/src/lib/content-classification.ts +0 -24
  40. package/src/pages/content-router.tsx +6 -17
  41. package/src/pages/home.tsx +26 -58
  42. package/src/pages/index-post.tsx +59 -0
  43. package/src/pages/post.tsx +1 -3
  44. package/src/pages/slides.tsx +5 -3
  45. package/src/vite-env.d.ts +11 -1
  46. package/vite.config.ts +4 -3
  47. package/dist/client/components/running-bar.js +0 -15
  48. package/dist/client/components/running-bar.js.map +0 -1
  49. package/src/components/content-tabs.tsx +0 -64
  50. package/src/components/running-bar.tsx +0 -21
@@ -2,7 +2,6 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useParams } from "react-router-dom";
3
3
  import { useDirectory, isSimulationRunning, findSlides } from "../plugin/src/client.js";
4
4
  import Loading from "../components/loading.js";
5
- import { RunningBar } from "../components/running-bar.js";
6
5
  import { Header } from "../components/header.js";
7
6
  import { useMDXContent } from "../hooks/use-mdx-content.js";
8
7
  import { mdxComponents } from "../components/mdx-components.js";
@@ -24,7 +23,6 @@ function Post() {
24
23
  }
25
24
  return /* @__PURE__ */ jsxs("div", { className: "flex min-h-screen flex-col bg-background noise-overlay", children: [
26
25
  /* @__PURE__ */ jsx("title", { children: frontmatter == null ? void 0 : frontmatter.title }),
27
- /* @__PURE__ */ jsx(RunningBar, {}),
28
26
  /* @__PURE__ */ jsx(Header, {}),
29
27
  /* @__PURE__ */ jsxs("main", { className: "flex-1 w-full overflow-x-clip", children: [
30
28
  isRunning && /* @__PURE__ */ jsx("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", children: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-3", children: [
@@ -32,7 +30,7 @@ function Post() {
32
30
  /* @__PURE__ */ jsx("span", { className: "uppercase tracking-widest", children: "simulation running" }),
33
31
  /* @__PURE__ */ jsx("span", { className: "text-primary-foreground/60", children: "Page will auto-refresh on completion" })
34
32
  ] }) }),
35
- Content && /* @__PURE__ */ jsx(FrontmatterProvider, { frontmatter, children: /* @__PURE__ */ jsx("article", { className: "my-12 mx-auto px-[var(--page-padding)] 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(--content-width)] animate-fade-in", children: /* @__PURE__ */ jsx(Content, { components: mdxComponents }) }) })
33
+ Content && /* @__PURE__ */ jsx(FrontmatterProvider, { frontmatter, children: /* @__PURE__ */ jsx("article", { className: "mt-12 mb-64 mx-auto px-[var(--page-padding)] max-w-[var(--content-width)] animate-fade-in", children: /* @__PURE__ */ jsx(Content, { components: mdxComponents }) }) })
36
34
  ] })
37
35
  ] });
38
36
  }
@@ -1 +1 @@
1
- {"version":3,"file":"post.js","sources":["../../../src/pages/post.tsx"],"sourcesContent":["import { useParams } from \"react-router-dom\";\nimport { findSlides, isSimulationRunning, useDirectory } from \"../../plugin/src/client\";\nimport Loading from \"@/components/loading\";\nimport { FileEntry } from \"plugin/src/lib\";\nimport { RunningBar } from \"@/components/running-bar\";\nimport { Header } from \"@/components/header\";\nimport { useMDXContent } from \"@/hooks/use-mdx-content\";\nimport { mdxComponents } from \"@/components/mdx-components\";\nimport { FrontmatterProvider } from \"@/lib/frontmatter-context\";\n\nexport function Post() {\n const { \"*\": rawPath = \".\" } = useParams();\n\n // The path includes the .mdx extension from the route\n const mdxPath = rawPath;\n\n // Extract directory path for finding sibling files (slides, etc.)\n const dirPath = mdxPath.replace(/\\/[^/]+\\.mdx$/, '') || '.';\n\n const { directory, loading: dirLoading } = useDirectory(dirPath)\n const { Content, frontmatter, loading: mdxLoading, error } = useMDXContent(mdxPath);\n const isRunning = isSimulationRunning();\n\n let slides: FileEntry | null = null;\n if (directory) {\n slides = findSlides(directory);\n }\n\n const loading = dirLoading || mdxLoading;\n\n if (loading) return <Loading />\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 return (\n <div className=\"flex min-h-screen flex-col bg-background noise-overlay\">\n <title>{frontmatter?.title}</title>\n <RunningBar />\n <Header />\n <main className=\"flex-1 w-full overflow-x-clip\">\n {isRunning && (\n <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\">\n <span className=\"inline-flex items-center gap-3\">\n <span className=\"h-1.5 w-1.5 rounded-full bg-current animate-pulse\" />\n <span className=\"uppercase tracking-widest\">simulation running</span>\n <span className=\"text-primary-foreground/60\">Page will auto-refresh on completion</span>\n </span>\n </div>\n )}\n\n {Content && (\n <FrontmatterProvider frontmatter={frontmatter}>\n <article className=\"my-12 mx-auto px-[var(--page-padding)] 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(--content-width)] animate-fade-in\">\n <Content components={mdxComponents} />\n </article>\n </FrontmatterProvider>\n )}\n </main>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;AAUO,SAAS,OAAO;AACrB,QAAM,EAAE,KAAK,UAAU,IAAA,IAAQ,UAAA;AAG/B,QAAM,UAAU;AAGhB,QAAM,UAAU,QAAQ,QAAQ,iBAAiB,EAAE,KAAK;AAExD,QAAM,EAAE,UAA+B,IAAI,aAAa,OAAO;AAC/D,QAAM,EAAE,SAAS,aAAa,SAAS,YAAY,MAAA,IAAU,cAAc,OAAO;AAClF,QAAM,YAAY,oBAAA;AAGlB,MAAI,WAAW;AACJ,eAAW,SAAS;AAAA,EAC/B;AAEA,QAAM,UAAwB;AAE9B,MAAI,QAAS,QAAO,oBAAC,SAAA,CAAA,CAAQ;AAE7B,MAAI,OAAO;AACT,WACE,oBAAC,QAAA,EAAK,WAAU,gEACd,UAAA,oBAAC,OAAE,WAAU,4BAA4B,UAAA,MAAM,QAAA,CAAQ,GACzD;AAAA,EAEJ;AAEA,SACE,qBAAC,OAAA,EAAI,WAAU,0DACb,UAAA;AAAA,IAAA,oBAAC,SAAA,EAAO,qDAAa,MAAA,CAAM;AAAA,wBAC1B,YAAA,EAAW;AAAA,wBACX,QAAA,EAAO;AAAA,IACR,qBAAC,QAAA,EAAK,WAAU,iCACb,UAAA;AAAA,MAAA,iCACE,OAAA,EAAI,WAAU,kIACb,UAAA,qBAAC,QAAA,EAAK,WAAU,kCACd,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAK,WAAU,oDAAA,CAAoD;AAAA,QACpE,oBAAC,QAAA,EAAK,WAAU,6BAA4B,UAAA,sBAAkB;AAAA,QAC9D,oBAAC,QAAA,EAAK,WAAU,8BAA6B,UAAA,uCAAA,CAAoC;AAAA,MAAA,EAAA,CACnF,EAAA,CACF;AAAA,MAGD,WACC,oBAAC,qBAAA,EAAoB,aACnB,UAAA,oBAAC,WAAA,EAAQ,WAAU,uOACjB,UAAA,oBAAC,SAAA,EAAQ,YAAY,cAAA,CAAe,GACtC,EAAA,CACF;AAAA,IAAA,EAAA,CAEJ;AAAA,EAAA,GACF;AAEJ;"}
1
+ {"version":3,"file":"post.js","sources":["../../../src/pages/post.tsx"],"sourcesContent":["import { useParams } from \"react-router-dom\";\nimport { findSlides, isSimulationRunning, useDirectory } from \"../../plugin/src/client\";\nimport Loading from \"@/components/loading\";\nimport { FileEntry } from \"plugin/src/lib\";\nimport { Header } from \"@/components/header\";\nimport { useMDXContent } from \"@/hooks/use-mdx-content\";\nimport { mdxComponents } from \"@/components/mdx-components\";\nimport { FrontmatterProvider } from \"@/lib/frontmatter-context\";\n\nexport function Post() {\n const { \"*\": rawPath = \".\" } = useParams();\n\n // The path includes the .mdx extension from the route\n const mdxPath = rawPath;\n\n // Extract directory path for finding sibling files (slides, etc.)\n const dirPath = mdxPath.replace(/\\/[^/]+\\.mdx$/, '') || '.';\n\n const { directory, loading: dirLoading } = useDirectory(dirPath)\n const { Content, frontmatter, loading: mdxLoading, error } = useMDXContent(mdxPath);\n const isRunning = isSimulationRunning();\n\n let slides: FileEntry | null = null;\n if (directory) {\n slides = findSlides(directory);\n }\n\n const loading = dirLoading || mdxLoading;\n\n if (loading) return <Loading />\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 return (\n <div className=\"flex min-h-screen flex-col bg-background noise-overlay\">\n <title>{frontmatter?.title}</title>\n <Header />\n <main className=\"flex-1 w-full overflow-x-clip\">\n {isRunning && (\n <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\">\n <span className=\"inline-flex items-center gap-3\">\n <span className=\"h-1.5 w-1.5 rounded-full bg-current animate-pulse\" />\n <span className=\"uppercase tracking-widest\">simulation running</span>\n <span className=\"text-primary-foreground/60\">Page will auto-refresh on completion</span>\n </span>\n </div>\n )}\n\n {Content && (\n <FrontmatterProvider frontmatter={frontmatter}>\n <article className=\"mt-12 mb-64 mx-auto px-[var(--page-padding)] max-w-[var(--content-width)] animate-fade-in\">\n <Content components={mdxComponents} />\n </article>\n </FrontmatterProvider>\n )}\n </main>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;;AASO,SAAS,OAAO;AACrB,QAAM,EAAE,KAAK,UAAU,IAAA,IAAQ,UAAA;AAG/B,QAAM,UAAU;AAGhB,QAAM,UAAU,QAAQ,QAAQ,iBAAiB,EAAE,KAAK;AAExD,QAAM,EAAE,UAA+B,IAAI,aAAa,OAAO;AAC/D,QAAM,EAAE,SAAS,aAAa,SAAS,YAAY,MAAA,IAAU,cAAc,OAAO;AAClF,QAAM,YAAY,oBAAA;AAGlB,MAAI,WAAW;AACJ,eAAW,SAAS;AAAA,EAC/B;AAEA,QAAM,UAAwB;AAE9B,MAAI,QAAS,QAAO,oBAAC,SAAA,CAAA,CAAQ;AAE7B,MAAI,OAAO;AACT,WACE,oBAAC,QAAA,EAAK,WAAU,gEACd,UAAA,oBAAC,OAAE,WAAU,4BAA4B,UAAA,MAAM,QAAA,CAAQ,GACzD;AAAA,EAEJ;AAEA,SACE,qBAAC,OAAA,EAAI,WAAU,0DACb,UAAA;AAAA,IAAA,oBAAC,SAAA,EAAO,qDAAa,MAAA,CAAM;AAAA,wBAC1B,QAAA,EAAO;AAAA,IACR,qBAAC,QAAA,EAAK,WAAU,iCACb,UAAA;AAAA,MAAA,iCACE,OAAA,EAAI,WAAU,kIACb,UAAA,qBAAC,QAAA,EAAK,WAAU,kCACd,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAK,WAAU,oDAAA,CAAoD;AAAA,QACpE,oBAAC,QAAA,EAAK,WAAU,6BAA4B,UAAA,sBAAkB;AAAA,QAC9D,oBAAC,QAAA,EAAK,WAAU,8BAA6B,UAAA,uCAAA,CAAoC;AAAA,MAAA,EAAA,CACnF,EAAA,CACF;AAAA,MAGD,WACC,oBAAC,qBAAA,EAAoB,aACnB,UAAA,oBAAC,WAAA,EAAQ,WAAU,6FACjB,UAAA,oBAAC,SAAA,EAAQ,YAAY,cAAA,CAAe,GACtC,EAAA,CACF;AAAA,IAAA,EAAA,CAEJ;AAAA,EAAA,GACF;AAEJ;"}
@@ -3,11 +3,12 @@ import { useState, useRef, useEffect, useCallback } from "react";
3
3
  import { FULLSCREEN_DATA_ATTR } from "../lib/constants.js";
4
4
  import { useParams, useSearchParams } from "react-router-dom";
5
5
  import Loading from "../components/loading.js";
6
- import { RunningBar } from "../components/running-bar.js";
7
6
  import { Header } from "../components/header.js";
8
7
  import { useMDXSlides } from "../hooks/use-mdx-content.js";
9
8
  import { slidesMdxComponents } from "../components/slides-renderer.js";
10
9
  import { FrontmatterProvider } from "../lib/frontmatter-context.js";
10
+ import veslxConfig from "virtual:veslx-config";
11
+ import { cn } from "../lib/utils.js";
11
12
  function SlidesPage() {
12
13
  const { "*": rawPath = "." } = useParams();
13
14
  const [searchParams, setSearchParams] = useSearchParams();
@@ -87,9 +88,9 @@ function SlidesPage() {
87
88
  if (!Content) {
88
89
  return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center p-12 text-muted-foreground font-mono text-sm", children: 'no slides found — use "---" to separate slides' });
89
90
  }
90
- return /* @__PURE__ */ jsxs("main", { className: "slides-container", children: [
91
+ const scrollSnap = veslxConfig.slides.scrollSnap;
92
+ return /* @__PURE__ */ jsxs("main", { className: cn("slides-container", scrollSnap && "slides-scroll-snap"), children: [
91
93
  /* @__PURE__ */ jsx("title", { children: frontmatter == null ? void 0 : frontmatter.title }),
92
- /* @__PURE__ */ jsx(RunningBar, {}),
93
94
  /* @__PURE__ */ jsx(
94
95
  Header,
95
96
  {
@@ -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 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 (up/down only - left/right reserved for horizontal scrolling)\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"ArrowUp\") {\n e.preventDefault();\n goToPrevious();\n } else if (e.key === \"ArrowDown\") {\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 { Header } from \"@/components/header\";\nimport { useMDXSlides } from \"@/hooks/use-mdx-content\";\nimport { slidesMdxComponents } from \"@/components/slides-renderer\";\nimport { FrontmatterProvider } from \"@/lib/frontmatter-context\";\nimport veslxConfig from \"virtual:veslx-config\";\nimport { cn } from \"@/lib/utils\";\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 (up/down only - left/right reserved for horizontal scrolling)\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"ArrowUp\") {\n e.preventDefault();\n goToPrevious();\n } else if (e.key === \"ArrowDown\") {\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 const scrollSnap = veslxConfig.slides.scrollSnap;\n\n return (\n <main className={cn(\"slides-container\", scrollSnap && \"slides-scroll-snap\")}>\n <title>{frontmatter?.title}</title>\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veslx",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -3,7 +3,7 @@ import path from 'path'
3
3
  import fs from 'fs'
4
4
  import yaml from 'js-yaml'
5
5
  import type { IncomingMessage, ServerResponse } from 'http'
6
- import { type VeslxConfig, type ResolvedSiteConfig, DEFAULT_SITE_CONFIG } from './types'
6
+ import { type VeslxConfig, type ResolvedSiteConfig, type ResolvedSlidesConfig, type ResolvedPostsConfig, type ResolvedConfig, DEFAULT_SITE_CONFIG, DEFAULT_SLIDES_CONFIG, DEFAULT_POSTS_CONFIG } from './types'
7
7
  import matter from 'gray-matter'
8
8
 
9
9
  /**
@@ -89,12 +89,22 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig,
89
89
  const dir = contentDir
90
90
  const configPath = options?.configPath
91
91
 
92
- // Mutable site config that can be updated on hot reload
92
+ // Mutable config that can be updated on hot reload
93
93
  let siteConfig: ResolvedSiteConfig = {
94
94
  ...DEFAULT_SITE_CONFIG,
95
95
  ...config?.site,
96
96
  }
97
97
 
98
+ let slidesConfig: ResolvedSlidesConfig = {
99
+ ...DEFAULT_SLIDES_CONFIG,
100
+ ...config?.slides,
101
+ }
102
+
103
+ let postsConfig: ResolvedPostsConfig = {
104
+ ...DEFAULT_POSTS_CONFIG,
105
+ ...config?.posts,
106
+ }
107
+
98
108
  // Helper to reload config from file
99
109
  function reloadConfig(): boolean {
100
110
  if (!configPath || !fs.existsSync(configPath)) return false
@@ -105,6 +115,14 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig,
105
115
  ...DEFAULT_SITE_CONFIG,
106
116
  ...parsed?.site,
107
117
  }
118
+ slidesConfig = {
119
+ ...DEFAULT_SLIDES_CONFIG,
120
+ ...parsed?.slides,
121
+ }
122
+ postsConfig = {
123
+ ...DEFAULT_POSTS_CONFIG,
124
+ ...parsed?.posts,
125
+ }
108
126
  return true
109
127
  } catch (e) {
110
128
  console.error('[veslx] Failed to reload config:', e)
@@ -117,8 +135,8 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig,
117
135
 
118
136
  urlToDir.set('/raw', dir)
119
137
 
120
- // Generate llms.txt content dynamically
121
- function generateLlmsTxt(): string {
138
+ // Get sorted entries for llms.txt generation
139
+ function getLlmsEntries() {
122
140
  const frontmatters = extractFrontmatters(dir)
123
141
  const entries: { path: string; title?: string; description?: string; date?: string; isSlides: boolean }[] = []
124
142
 
@@ -134,50 +152,100 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig,
134
152
  })
135
153
  }
136
154
 
137
- entries.sort((a, b) => {
138
- if (a.date && b.date) return b.date.localeCompare(a.date)
139
- if (a.date) return -1
140
- if (b.date) return 1
141
- return a.path.localeCompare(b.path)
142
- })
155
+ // Sort alphanumerically by path (0-foo before 1-bar before 10-baz)
156
+ entries.sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true }))
157
+
158
+ return entries
159
+ }
160
+
161
+ // Generate llms.txt index (links to articles)
162
+ function generateLlmsTxt(): string {
163
+ const entries = getLlmsEntries()
143
164
 
144
165
  const lines: string[] = [`# ${siteConfig.name}`]
145
166
  if (siteConfig.description) {
146
- lines.push(siteConfig.description)
167
+ lines.push(`> ${siteConfig.description}`)
147
168
  }
148
169
  lines.push('')
149
170
 
150
171
  // Links section
151
172
  if (siteConfig.homepage) {
152
- lines.push(`Homepage: ${siteConfig.homepage}`)
173
+ lines.push(`- Homepage: ${siteConfig.homepage}`)
153
174
  }
154
175
  if (siteConfig.github) {
155
- lines.push(`GitHub: https://github.com/${siteConfig.github}`)
176
+ lines.push(`- GitHub: https://github.com/${siteConfig.github}`)
156
177
  }
157
- lines.push('Install: bun install -g veslx')
158
178
  lines.push('')
159
179
 
160
- lines.push('Content is served as raw MDX files at /raw/{path}.')
180
+ lines.push('## Documentation')
161
181
  lines.push('')
162
182
 
163
183
  for (const entry of entries) {
164
- const type = entry.isSlides ? '[slides]' : entry.date ? '[post]' : '[doc]'
165
184
  const title = entry.title || entry.path.replace(/\.mdx?$/, '').split('/').pop()
166
- const desc = entry.description ? ` - ${entry.description}` : ''
167
- lines.push(`${type} ${title}: /raw/${entry.path}${desc}`)
185
+ const desc = entry.description ? `: ${entry.description}` : ''
186
+ lines.push(`- [${title}](/raw/${entry.path})${desc}`)
187
+ }
188
+ lines.push('')
189
+ return lines.join('\n')
190
+ }
191
+
192
+ // Generate llms-full.txt with all article content inline
193
+ function generateLlmsFullTxt(): string {
194
+ const entries = getLlmsEntries()
195
+
196
+ const lines: string[] = [`# ${siteConfig.name}`]
197
+ if (siteConfig.description) {
198
+ lines.push(`> ${siteConfig.description}`)
168
199
  }
169
200
  lines.push('')
201
+
202
+ for (const entry of entries) {
203
+ const filePath = path.join(dir, entry.path)
204
+ if (!fs.existsSync(filePath)) continue
205
+
206
+ try {
207
+ const content = fs.readFileSync(filePath, 'utf-8')
208
+ // Remove frontmatter and JSX components
209
+ const contentWithoutFrontmatter = content
210
+ .replace(/^---[\s\S]*?---\n*/, '')
211
+ .replace(/<[A-Z][a-zA-Z]*\s*\/>/g, '') // Self-closing JSX like <FrontMatter />
212
+ .replace(/<[A-Z][a-zA-Z]*[^>]*>[\s\S]*?<\/[A-Z][a-zA-Z]*>/g, '') // JSX with children
213
+ .replace(/\n{3,}/g, '\n\n') // Collapse multiple newlines
214
+
215
+ const title = entry.title || entry.path.replace(/\.mdx?$/, '').split('/').pop()
216
+
217
+ lines.push('---')
218
+ lines.push('')
219
+ lines.push(`## ${title}`)
220
+ if (entry.description) {
221
+ lines.push(`> ${entry.description}`)
222
+ }
223
+ lines.push('')
224
+ lines.push(contentWithoutFrontmatter.trim())
225
+ lines.push('')
226
+ } catch {
227
+ // Skip files that can't be read
228
+ }
229
+ }
230
+
170
231
  return lines.join('\n')
171
232
  }
172
233
 
173
234
  const middleware: Connect.NextHandleFunction = (req: IncomingMessage, res: ServerResponse, next: Connect.NextFunction) => {
174
- // Serve llms.txt dynamically
175
- if (req.url === '/llms.txt') {
235
+ // Serve llms.txt dynamically (only if enabled)
236
+ if (req.url === '/llms.txt' && siteConfig.llmsTxt) {
176
237
  res.setHeader('Content-Type', 'text/plain')
177
238
  res.end(generateLlmsTxt())
178
239
  return
179
240
  }
180
241
 
242
+ // Serve llms-full.txt with all content inline (only if enabled)
243
+ if (req.url === '/llms-full.txt' && siteConfig.llmsTxt) {
244
+ res.setHeader('Content-Type', 'text/plain')
245
+ res.end(generateLlmsFullTxt())
246
+ return
247
+ }
248
+
181
249
  // Check if URL matches any registered content directory
182
250
  for (const [urlBase, contentDir] of urlToDir.entries()) {
183
251
  if (req.url?.startsWith(urlBase + '/')) {
@@ -215,6 +283,28 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig,
215
283
 
216
284
  return {
217
285
  name: 'content',
286
+ enforce: 'pre',
287
+
288
+ // Inject @source directive for Tailwind to scan content directory for classes
289
+ transform(code, id) {
290
+ // Only process CSS files containing the tailwindcss import
291
+ if (!id.endsWith('.css')) return null
292
+ if (!code.includes('@import "tailwindcss"')) return null
293
+
294
+ // Calculate relative path from CSS file to content directory
295
+ const cssDir = path.dirname(id)
296
+ let relativeContentDir = path.relative(cssDir, dir)
297
+ relativeContentDir = relativeContentDir.replace(/\\/g, '/') // Windows compatibility
298
+
299
+ // Inject @source directive after the tailwindcss import
300
+ const sourceDirective = `@source "${relativeContentDir}";`
301
+ const modified = code.replace(
302
+ /(@import\s+["']tailwindcss["'];?)/,
303
+ `$1\n${sourceDirective}`
304
+ )
305
+
306
+ return { code: modified, map: null }
307
+ },
218
308
 
219
309
  // Inject @content alias and fs.allow into Vite config
220
310
  config() {
@@ -252,12 +342,12 @@ export default function contentPlugin(contentDir: string, config?: VeslxConfig,
252
342
 
253
343
  // Generate virtual module with import.meta.glob for MDX files
254
344
  return `
255
- export const posts = import.meta.glob('@content/**/*.mdx', {
345
+ export const posts = import.meta.glob(['@content/**/*.mdx', '@content/**/*.md'], {
256
346
  import: 'default',
257
347
  query: { skipSlides: true }
258
348
  });
259
- export const allMdx = import.meta.glob('@content/**/*.mdx');
260
- export const slides = import.meta.glob(['@content/**/SLIDES.mdx', '@content/**/*.slides.mdx']);
349
+ export const allMdx = import.meta.glob(['@content/**/*.mdx', '@content/**/*.md']);
350
+ export const slides = import.meta.glob(['@content/**/SLIDES.mdx', '@content/**/SLIDES.md', '@content/**/*.slides.mdx', '@content/**/*.slides.md']);
261
351
 
262
352
  // All files for directory tree building (web-compatible files only)
263
353
  export const files = import.meta.glob([
@@ -280,12 +370,13 @@ export const files = import.meta.glob([
280
370
  export const frontmatters = ${JSON.stringify(frontmatterData)};
281
371
 
282
372
  // Legacy aliases for backwards compatibility
283
- export const modules = import.meta.glob('@content/**/*.mdx');
373
+ export const modules = import.meta.glob(['@content/**/*.mdx', '@content/**/*.md']);
284
374
  `
285
375
  }
286
376
  if (id === RESOLVED_VIRTUAL_CONFIG_ID) {
287
- // Generate virtual module with site config
288
- return `export default ${JSON.stringify(siteConfig)};`
377
+ // Generate virtual module with full config
378
+ const fullConfig: ResolvedConfig = { site: siteConfig, slides: slidesConfig, posts: postsConfig }
379
+ return `export default ${JSON.stringify(fullConfig)};`
289
380
  }
290
381
  },
291
382
 
@@ -330,10 +421,16 @@ export const modules = import.meta.glob('@content/**/*.mdx');
330
421
  copyDirSync(dir, destDir)
331
422
  console.log(`Content copied successfully`)
332
423
 
333
- // Generate llms.txt for CLI tools and LLMs
334
- const llmsTxtPath = path.join(outDir, 'llms.txt')
335
- fs.writeFileSync(llmsTxtPath, generateLlmsTxt())
336
- console.log(`Generated llms.txt`)
424
+ // Generate llms.txt files if enabled
425
+ if (siteConfig.llmsTxt) {
426
+ const llmsTxtPath = path.join(outDir, 'llms.txt')
427
+ fs.writeFileSync(llmsTxtPath, generateLlmsTxt())
428
+ console.log(`Generated llms.txt`)
429
+
430
+ const llmsFullTxtPath = path.join(outDir, 'llms-full.txt')
431
+ fs.writeFileSync(llmsFullTxtPath, generateLlmsFullTxt())
432
+ console.log(`Generated llms-full.txt`)
433
+ }
337
434
  } else {
338
435
  console.warn(`Content directory not found: ${dir}`)
339
436
  }
@@ -1,16 +1,32 @@
1
- export type ContentView = 'posts' | 'docs' | 'all';
1
+ export interface SlidesConfig {
2
+ scrollSnap?: boolean;
3
+ }
4
+
5
+ export interface PostsConfig {
6
+ sort?: 'date' | 'alpha';
7
+ }
2
8
 
3
9
  export interface SiteConfig {
4
10
  name?: string;
5
11
  description?: string;
6
12
  github?: string;
7
13
  homepage?: string;
8
- defaultView?: ContentView;
14
+ llmsTxt?: boolean;
9
15
  }
10
16
 
11
17
  export interface VeslxConfig {
12
18
  dir?: string;
13
19
  site?: SiteConfig;
20
+ slides?: SlidesConfig;
21
+ posts?: PostsConfig;
22
+ }
23
+
24
+ export interface ResolvedSlidesConfig {
25
+ scrollSnap: boolean;
26
+ }
27
+
28
+ export interface ResolvedPostsConfig {
29
+ sort: 'date' | 'alpha';
14
30
  }
15
31
 
16
32
  export interface ResolvedSiteConfig {
@@ -18,7 +34,13 @@ export interface ResolvedSiteConfig {
18
34
  description: string;
19
35
  github: string;
20
36
  homepage: string;
21
- defaultView: ContentView;
37
+ llmsTxt: boolean;
38
+ }
39
+
40
+ export interface ResolvedConfig {
41
+ site: ResolvedSiteConfig;
42
+ slides: ResolvedSlidesConfig;
43
+ posts: ResolvedPostsConfig;
22
44
  }
23
45
 
24
46
  export const DEFAULT_SITE_CONFIG: ResolvedSiteConfig = {
@@ -26,5 +48,13 @@ export const DEFAULT_SITE_CONFIG: ResolvedSiteConfig = {
26
48
  description: '',
27
49
  github: '',
28
50
  homepage: '',
29
- defaultView: 'all',
51
+ llmsTxt: false,
52
+ };
53
+
54
+ export const DEFAULT_SLIDES_CONFIG: ResolvedSlidesConfig = {
55
+ scrollSnap: true,
56
+ };
57
+
58
+ export const DEFAULT_POSTS_CONFIG: ResolvedPostsConfig = {
59
+ sort: 'alpha',
30
60
  };
@@ -1,8 +1,56 @@
1
+ import { useLocation } from "react-router-dom";
1
2
  import { useFrontmatter } from "@/lib/frontmatter-context";
2
- import { formatDate } from "@/lib/format-date"
3
+ import { formatDate } from "@/lib/format-date";
4
+ import veslxConfig from "virtual:veslx-config";
3
5
 
4
- export function FrontMatter(){
6
+ /**
7
+ * Convert MDX content to llms.txt format.
8
+ */
9
+ function convertToLlmsTxt(
10
+ rawMdx: string,
11
+ frontmatter?: { title?: string; description?: string }
12
+ ): string {
13
+ const contentWithoutFrontmatter = rawMdx.replace(/^---[\s\S]*?---\n*/, '')
14
+
15
+ const parts: string[] = []
16
+
17
+ const title = frontmatter?.title || 'Untitled'
18
+ parts.push(`# ${title}`)
19
+
20
+ if (frontmatter?.description) {
21
+ parts.push('')
22
+ parts.push(`> ${frontmatter.description}`)
23
+ }
24
+
25
+ if (contentWithoutFrontmatter.trim()) {
26
+ parts.push('')
27
+ parts.push(contentWithoutFrontmatter.trim())
28
+ }
29
+
30
+ return parts.join('\n')
31
+ }
32
+
33
+ export function FrontMatter() {
5
34
  const frontmatter = useFrontmatter();
35
+ const location = useLocation();
36
+ const config = veslxConfig.site;
37
+
38
+ const rawUrl = `/raw${location.pathname.replace(/^\//, '/')}`;
39
+
40
+ const handleLlmsTxt = async (e: React.MouseEvent) => {
41
+ e.preventDefault();
42
+ try {
43
+ const res = await fetch(rawUrl);
44
+ if (!res.ok) throw new Error('Failed to fetch');
45
+ const rawMdx = await res.text();
46
+ const llmsTxt = convertToLlmsTxt(rawMdx, frontmatter);
47
+ const blob = new Blob([llmsTxt], { type: 'text/plain' });
48
+ const url = URL.createObjectURL(blob);
49
+ window.location.href = url;
50
+ } catch {
51
+ console.error('Failed to load llms.txt');
52
+ }
53
+ };
6
54
 
7
55
  return (
8
56
  <div>
@@ -19,6 +67,15 @@ export function FrontMatter(){
19
67
  {formatDate(new Date(frontmatter.date as string))}
20
68
  </time>
21
69
  )}
70
+ {config.llmsTxt && (
71
+ <a
72
+ href="#"
73
+ onClick={handleLlmsTxt}
74
+ className="font-mono text-xs text-muted-foreground/70 hover:text-foreground underline underline-offset-2 transition-colors"
75
+ >
76
+ llms.txt
77
+ </a>
78
+ )}
22
79
  </div>
23
80
 
24
81
  {frontmatter?.description && (
@@ -29,5 +86,5 @@ export function FrontMatter(){
29
86
  </header>
30
87
  )}
31
88
  </div>
32
- )
89
+ );
33
90
  }
@@ -2,7 +2,7 @@ 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
- import siteConfig from "virtual:veslx-config";
5
+ import veslxConfig from "virtual:veslx-config";
6
6
  import { cn } from "@/lib/utils";
7
7
 
8
8
  interface HeaderProps {
@@ -15,7 +15,7 @@ interface HeaderProps {
15
15
  }
16
16
 
17
17
  export function Header({ slideControls }: HeaderProps = {}) {
18
- const config = siteConfig;
18
+ const config = veslxConfig.site;
19
19
 
20
20
  const { "*": path } = useParams()
21
21
 
@@ -62,24 +62,6 @@ export function Header({ slideControls }: HeaderProps = {}) {
62
62
  </button>
63
63
  </>
64
64
  )}
65
- <Link
66
- to={`/posts`}
67
- className={cn(
68
- "font-medium text-muted-foreground/70 hover:text-foreground transition-colors duration-300",
69
- path?.startsWith("posts") && "font-semibold text-foreground",
70
- )}
71
- >
72
- Posts
73
- </Link>
74
- <Link
75
- to={`/docs`}
76
- className={cn(
77
- "font-medium text-muted-foreground/70 hover:text-foreground transition-colors duration-300",
78
- path?.startsWith("docs") && "font-semibold text-foreground",
79
- )}
80
- >
81
- Docs
82
- </Link>
83
65
  {config.github && (
84
66
  <Link
85
67
  to={`https://github.com/${config.github}`}
@@ -7,7 +7,7 @@ import { HeroSlide } from './slides/hero-slide'
7
7
  import { FigureSlide } from './slides/figure-slide'
8
8
  import { TextSlide } from './slides/text-slide'
9
9
  import { SlideOutline } from './slides/slide-outline'
10
-
10
+ import { PostList } from '@/components/post-list'
11
11
  /**
12
12
  * Smart link component that uses React Router for internal links
13
13
  * and regular anchor tags for external links.
@@ -89,6 +89,8 @@ export const mdxComponents = {
89
89
 
90
90
  SlideOutline,
91
91
 
92
+ PostList,
93
+
92
94
  // Headings - clean sans-serif
93
95
  h1: (props: React.HTMLAttributes<HTMLHeadingElement>) => {
94
96
  const id = generateId(props.children)
@@ -0,0 +1,54 @@
1
+ import { Link } from "react-router-dom";
2
+ import { cn } from "@/lib/utils";
3
+ import { formatDate } from "@/lib/format-date";
4
+ import { ArrowRight, Presentation } from "lucide-react";
5
+
6
+ interface PostListItemProps {
7
+ title: string;
8
+ description?: string;
9
+ date?: Date;
10
+ linkPath: string;
11
+ isSlides?: boolean;
12
+ }
13
+
14
+ export function PostListItem({ title, description, date, linkPath, isSlides }: PostListItemProps) {
15
+ return (
16
+ <Link
17
+ to={linkPath}
18
+ className={cn(
19
+ "group block py-3 px-3 -mx-3 rounded-md",
20
+ "transition-colors duration-150",
21
+ )}
22
+ >
23
+ <article className="flex items-center gap-4">
24
+ {/* Main content */}
25
+ <div className="flex-1 min-w-0">
26
+ <div className={cn(
27
+ "text-sm font-medium text-foreground",
28
+ "group-hover:underline",
29
+ "flex items-center gap-2"
30
+ )}>
31
+ <span>{title}</span>
32
+ <ArrowRight className="h-3 w-3 opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200 text-primary" />
33
+ </div>
34
+
35
+ {description && (
36
+ <div className="text-sm text-muted-foreground line-clamp-1 mt-0.5">
37
+ {description}
38
+ </div>
39
+ )}
40
+ </div>
41
+
42
+ {isSlides && (
43
+ <Presentation className="h-3 w-3 text-muted-foreground" />
44
+ )}
45
+ <time
46
+ dateTime={date?.toISOString()}
47
+ className="font-mono text-xs text-muted-foreground tabular-nums w-20 flex-shrink-0"
48
+ >
49
+ {date && formatDate(date)}
50
+ </time>
51
+ </article>
52
+ </Link>
53
+ );
54
+ }