veslx 0.1.21 → 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 (73) hide show
  1. package/bin/lib/serve.ts +16 -2
  2. package/bin/lib/start.ts +12 -10
  3. package/bin/veslx.ts +1 -1
  4. package/dist/client/components/front-matter.js +2 -6
  5. package/dist/client/components/front-matter.js.map +1 -1
  6. package/dist/client/components/gallery/components/figure-caption.js +1 -1
  7. package/dist/client/components/gallery/components/figure-caption.js.map +1 -1
  8. package/dist/client/components/gallery/components/figure-header.js +1 -1
  9. package/dist/client/components/gallery/components/figure-header.js.map +1 -1
  10. package/dist/client/components/gallery/components/lightbox.js +1 -1
  11. package/dist/client/components/gallery/components/lightbox.js.map +1 -1
  12. package/dist/client/components/gallery/index.js +38 -8
  13. package/dist/client/components/gallery/index.js.map +1 -1
  14. package/dist/client/components/header.js +45 -21
  15. package/dist/client/components/header.js.map +1 -1
  16. package/dist/client/components/mdx-components.js +8 -0
  17. package/dist/client/components/mdx-components.js.map +1 -1
  18. package/dist/client/components/post-list.js +13 -11
  19. package/dist/client/components/post-list.js.map +1 -1
  20. package/dist/client/components/slides/figure-slide.js +14 -0
  21. package/dist/client/components/slides/figure-slide.js.map +1 -0
  22. package/dist/client/components/slides/hero-slide.js +21 -0
  23. package/dist/client/components/slides/hero-slide.js.map +1 -0
  24. package/dist/client/components/slides/slide-outline.js +28 -0
  25. package/dist/client/components/slides/slide-outline.js.map +1 -0
  26. package/dist/client/components/slides/text-slide.js +18 -0
  27. package/dist/client/components/slides/text-slide.js.map +1 -0
  28. package/dist/client/components/slides-renderer.js.map +1 -1
  29. package/dist/client/hooks/use-mdx-content.js +19 -6
  30. package/dist/client/hooks/use-mdx-content.js.map +1 -1
  31. package/dist/client/lib/content-classification.js +11 -2
  32. package/dist/client/lib/content-classification.js.map +1 -1
  33. package/dist/client/lib/frontmatter-context.js +17 -0
  34. package/dist/client/lib/frontmatter-context.js.map +1 -0
  35. package/dist/client/pages/content-router.js +4 -1
  36. package/dist/client/pages/content-router.js.map +1 -1
  37. package/dist/client/pages/home.js +2 -6
  38. package/dist/client/pages/home.js.map +1 -1
  39. package/dist/client/pages/post.js +2 -9
  40. package/dist/client/pages/post.js.map +1 -1
  41. package/dist/client/pages/slides.js +9 -12
  42. package/dist/client/pages/slides.js.map +1 -1
  43. package/dist/client/plugin/src/client.js +20 -2
  44. package/dist/client/plugin/src/client.js.map +1 -1
  45. package/index.html +13 -0
  46. package/package.json +1 -1
  47. package/plugin/src/client.tsx +28 -2
  48. package/plugin/src/plugin.ts +49 -4
  49. package/src/components/content-tabs.tsx +4 -4
  50. package/src/components/front-matter.tsx +3 -8
  51. package/src/components/gallery/components/figure-caption.tsx +1 -1
  52. package/src/components/gallery/components/figure-header.tsx +1 -1
  53. package/src/components/gallery/components/lightbox.tsx +1 -1
  54. package/src/components/gallery/index.tsx +68 -29
  55. package/src/components/header.tsx +44 -25
  56. package/src/components/mdx-components.tsx +12 -0
  57. package/src/components/post-list.tsx +14 -10
  58. package/src/components/slides/figure-slide.tsx +16 -0
  59. package/src/components/slides/hero-slide.tsx +34 -0
  60. package/src/components/slides/slide-outline.tsx +38 -0
  61. package/src/components/slides/text-slide.tsx +35 -0
  62. package/src/components/slides-renderer.tsx +1 -1
  63. package/src/hooks/use-mdx-content.ts +27 -6
  64. package/src/index.css +1 -2
  65. package/src/lib/content-classification.ts +13 -2
  66. package/src/lib/frontmatter-context.tsx +29 -0
  67. package/src/pages/content-router.tsx +7 -1
  68. package/src/pages/home.tsx +3 -3
  69. package/src/pages/post.tsx +6 -24
  70. package/src/pages/slides.tsx +14 -16
  71. package/vite.config.ts +4 -3
  72. package/dist/client/components/content-tabs.js +0 -50
  73. package/dist/client/components/content-tabs.js.map +0 -1
@@ -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
+ }
@@ -0,0 +1,35 @@
1
+
2
+ export function TextSlide({
3
+ title,
4
+ subtitle,
5
+ children,
6
+ }: {
7
+ title?: string;
8
+ subtitle?: string;
9
+ children?: React.ReactNode;
10
+ }) {
11
+ return (
12
+ <div>
13
+ {(title || subtitle) && (
14
+ <header className="flex flex-col gap-2">
15
+ {title && (
16
+ <h2 className="text-[clamp(1.75rem,4vw,3rem)] font-semibold leading-tight tracking-[-0.02em] text-foreground">
17
+ {title}
18
+ </h2>
19
+ )}
20
+ {subtitle && (
21
+ <p className="text-[clamp(0.95rem,1.5vw,1.25rem)] text-muted-foreground">
22
+ {subtitle}
23
+ </p>
24
+ )}
25
+ </header>
26
+ )}
27
+
28
+ {children && (
29
+ <div className="text-[clamp(1rem,1.8vw,1.35rem)] leading-[1.6] text-foreground/90 space-y-4 [&>ul]:space-y-2 [&>ul]:list-disc [&>ul]:pl-6 [&>ol]:space-y-2 [&>ol]:list-decimal [&>ol]:pl-6">
30
+ {children}
31
+ </div>
32
+ )}
33
+ </div>
34
+ );
35
+ }
@@ -15,7 +15,7 @@ export const slidesMdxComponents = {
15
15
  */
16
16
  export function SlideContent({ children }: { children: ReactNode }) {
17
17
  return (
18
- <div className="slide-content prose dark:prose-invert prose-headings:tracking-tight prose-p:leading-relaxed max-w-xl">
18
+ <div className="slide-content prose dark:prose-invert prose-headings:tracking-tight prose-p:leading-relaxed">
19
19
  {children}
20
20
  </div>
21
21
  )
@@ -123,9 +123,25 @@ export function useMDXContent(path: string) {
123
123
  function findSlidesModule(modules: ModuleMap, path: string): ModuleLoader | null {
124
124
  const keys = Object.keys(modules)
125
125
 
126
+ // Normalize path - remove leading slash if present
127
+ const normalizedPath = path.replace(/^\//, '')
128
+
126
129
  // If path already ends with .mdx, match exactly
127
- if (path.endsWith('.mdx')) {
128
- const matchingKey = keys.find(key => key.endsWith(`/${path}`))
130
+ if (normalizedPath.endsWith('.mdx')) {
131
+ // Try multiple matching strategies for different Vite glob formats
132
+ const matchingKey = keys.find(key => {
133
+ // Strategy 1: Key ends with /path (e.g., @content/docs/foo.slides.mdx matches docs/foo.slides.mdx)
134
+ if (key.endsWith(`/${normalizedPath}`)) return true
135
+ // Strategy 2: Key equals @content/path (alias form)
136
+ if (key === `@content/${normalizedPath}`) return true
137
+ // Strategy 3: Key equals /@content/path (with leading slash)
138
+ if (key === `/@content/${normalizedPath}`) return true
139
+ // Strategy 4: Key equals path directly
140
+ if (key === normalizedPath) return true
141
+ // Strategy 5: Key equals /path (with leading slash)
142
+ if (key === `/${normalizedPath}`) return true
143
+ return false
144
+ })
129
145
  return matchingKey ? modules[matchingKey] : null
130
146
  }
131
147
 
@@ -133,12 +149,17 @@ function findSlidesModule(modules: ModuleMap, path: string): ModuleLoader | null
133
149
  // 1. folder/SLIDES.mdx (current convention)
134
150
  // 2. folder/index.slides.mdx (alternative)
135
151
  const candidates = [
136
- `/${path}/SLIDES.mdx`,
137
- `/${path}/index.slides.mdx`,
152
+ `${normalizedPath}/SLIDES.mdx`,
153
+ `${normalizedPath}/index.slides.mdx`,
138
154
  ]
139
155
 
140
- for (const suffix of candidates) {
141
- const matchingKey = keys.find(key => key.endsWith(suffix))
156
+ for (const candidate of candidates) {
157
+ const matchingKey = keys.find(key => {
158
+ if (key.endsWith(`/${candidate}`)) return true
159
+ if (key === `@content/${candidate}`) return true
160
+ if (key === candidate) return true
161
+ return false
162
+ })
142
163
  if (matchingKey) {
143
164
  return modules[matchingKey]
144
165
  }
package/src/index.css CHANGED
@@ -325,8 +325,7 @@
325
325
  .slides-container > .running-bar,
326
326
  .slides-container hr,
327
327
  .slides-container > title,
328
- header.print\\:hidden,
329
- [class*="print:hidden"] {
328
+ body > header {
330
329
  display: none !important;
331
330
  height: 0 !important;
332
331
  width: 0 !important;
@@ -1,6 +1,6 @@
1
1
  import type { ContentView } from "../../plugin/src/types";
2
2
  import type { DirectoryEntry, FileEntry } from "../../plugin/src/lib";
3
- import { findReadme, findSlides, findMdxFiles } from "../../plugin/src/client";
3
+ import { findReadme, findSlides, findMdxFiles, findStandaloneSlides } from "../../plugin/src/client";
4
4
 
5
5
  export type PostEntry = {
6
6
  type: 'folder' | 'file';
@@ -39,6 +39,7 @@ export function getViewCounts(posts: PostEntry[]): { posts: number; docs: number
39
39
  export function directoryToPostEntries(directory: DirectoryEntry): PostEntry[] {
40
40
  const folders = directory.children.filter((c): c is DirectoryEntry => c.type === "directory");
41
41
  const standaloneFiles = findMdxFiles(directory);
42
+ const standaloneSlidesFiles = findStandaloneSlides(directory);
42
43
 
43
44
  const folderPosts: PostEntry[] = folders
44
45
  .map((folder) => ({
@@ -60,7 +61,17 @@ export function directoryToPostEntries(directory: DirectoryEntry): PostEntry[] {
60
61
  file,
61
62
  }));
62
63
 
63
- return [...folderPosts, ...filePosts];
64
+ // Standalone slides files (e.g., getting-started.slides.mdx)
65
+ const slidesPosts: PostEntry[] = standaloneSlidesFiles.map((file) => ({
66
+ type: 'file' as const,
67
+ name: file.name.replace(/\.slides\.mdx?$/, ''),
68
+ path: file.path,
69
+ readme: null,
70
+ slides: file,
71
+ file: null,
72
+ }));
73
+
74
+ return [...folderPosts, ...filePosts, ...slidesPosts];
64
75
  }
65
76
 
66
77
  export function filterVisiblePosts(posts: PostEntry[]): PostEntry[] {
@@ -0,0 +1,29 @@
1
+ import { createContext, useContext, ReactNode } from 'react';
2
+
3
+ export interface Frontmatter {
4
+ title?: string;
5
+ description?: string;
6
+ date?: string;
7
+ visibility?: string;
8
+ draft?: boolean;
9
+ }
10
+
11
+ const FrontmatterContext = createContext<Frontmatter | undefined>(undefined);
12
+
13
+ export function FrontmatterProvider({
14
+ frontmatter,
15
+ children
16
+ }: {
17
+ frontmatter: Frontmatter | undefined;
18
+ children: ReactNode
19
+ }) {
20
+ return (
21
+ <FrontmatterContext.Provider value={frontmatter}>
22
+ {children}
23
+ </FrontmatterContext.Provider>
24
+ );
25
+ }
26
+
27
+ export function useFrontmatter() {
28
+ return useContext(FrontmatterContext);
29
+ }
@@ -26,7 +26,13 @@ export function ContentRouter() {
26
26
  }
27
27
 
28
28
  // Check if this is a slides file
29
- if (path.endsWith('.slides.mdx') || path.endsWith('SLIDES.mdx')) {
29
+ const filename = path.split('/').pop()?.toLowerCase() || ''
30
+ const isSlides =
31
+ path.endsWith('.slides.mdx') ||
32
+ path.endsWith('.slides.md') ||
33
+ filename === 'slides.mdx' ||
34
+ filename === 'slides.md'
35
+ if (isSlides) {
30
36
  return <SlidesPage />
31
37
  }
32
38
 
@@ -68,12 +68,12 @@ export function Home({ view }: HomeProps) {
68
68
  </div>
69
69
  )}
70
70
 
71
- <div className="">
72
- {isRoot && directory && (
71
+ <div className="flex flex-col gap-2">
72
+ {/* {isRoot && directory && (
73
73
  <div className="animate-fade-in">
74
74
  <ContentTabs value={activeView} counts={counts} />
75
75
  </div>
76
- )}
76
+ )} */}
77
77
  {directory && (
78
78
  <div className="animate-fade-in">
79
79
  <PostList directory={directory} view={isRoot ? activeView : 'all'} />
@@ -6,7 +6,7 @@ import { RunningBar } from "@/components/running-bar";
6
6
  import { Header } from "@/components/header";
7
7
  import { useMDXContent } from "@/hooks/use-mdx-content";
8
8
  import { mdxComponents } from "@/components/mdx-components";
9
- import { formatDate } from "@/lib/format-date";
9
+ import { FrontmatterProvider } from "@/lib/frontmatter-context";
10
10
 
11
11
  export function Post() {
12
12
  const { "*": rawPath = "." } = useParams();
@@ -55,29 +55,11 @@ export function Post() {
55
55
  )}
56
56
 
57
57
  {Content && (
58
- <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">
59
- {/* Render frontmatter header */}
60
- {frontmatter?.title && (
61
- <header className="not-prose flex flex-col gap-2 mb-8 pt-4">
62
- <h1 className="text-2xl md:text-3xl font-semibold tracking-tight text-foreground mb-3">
63
- {frontmatter.title}
64
- </h1>
65
- {frontmatter?.date && (
66
- <div className="flex flex-wrap items-center gap-3 text-muted-foreground">
67
- <time className="font-mono text-xs bg-muted px-2 py-0.5 rounded">
68
- {formatDate(new Date(frontmatter.date as string))}
69
- </time>
70
- </div>
71
- )}
72
- {frontmatter?.description && (
73
- <div className="flex flex-wrap text-sm items-center gap-3 text-muted-foreground">
74
- {frontmatter.description}
75
- </div>
76
- )}
77
- </header>
78
- )}
79
- <Content components={mdxComponents} />
80
- </article>
58
+ <FrontmatterProvider frontmatter={frontmatter}>
59
+ <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">
60
+ <Content components={mdxComponents} />
61
+ </article>
62
+ </FrontmatterProvider>
81
63
  )}
82
64
  </main>
83
65
  </div>
@@ -6,6 +6,7 @@ import { RunningBar } from "@/components/running-bar";
6
6
  import { Header } from "@/components/header";
7
7
  import { useMDXSlides } from "@/hooks/use-mdx-content";
8
8
  import { slidesMdxComponents } from "@/components/slides-renderer";
9
+ import { FrontmatterProvider } from "@/lib/frontmatter-context";
9
10
 
10
11
 
11
12
  export function SlidesPage() {
@@ -18,8 +19,7 @@ export function SlidesPage() {
18
19
  // Load the compiled MDX module (now includes slideCount export)
19
20
  const { Content, frontmatter, slideCount, loading, error } = useMDXSlides(mdxPath);
20
21
 
21
- // Total slides = 1 (title) + content slides
22
- const totalSlides = (slideCount || 0) + 1;
22
+ const totalSlides = slideCount || 0;
23
23
 
24
24
  const [currentSlide, setCurrentSlide] = useState(0);
25
25
  const titleSlideRef = useRef<HTMLDivElement>(null);
@@ -29,7 +29,7 @@ export function SlidesPage() {
29
29
  useEffect(() => {
30
30
  const slideParam = parseInt(searchParams.get("slide") || "0", 10);
31
31
  if (slideParam > 0 && contentRef.current) {
32
- const slideEl = contentRef.current.querySelector(`[data-slide-index="${slideParam - 1}"]`);
32
+ const slideEl = contentRef.current.querySelector(`[data-slide-index="${slideParam}"]`);
33
33
  if (slideEl) {
34
34
  slideEl.scrollIntoView({ behavior: "auto" });
35
35
  }
@@ -44,7 +44,7 @@ export function SlidesPage() {
44
44
  if (entry.isIntersecting) {
45
45
  const index = entry.target.getAttribute("data-slide-index");
46
46
  if (index !== null) {
47
- const slideNum = index === "title" ? 0 : parseInt(index, 10) + 1;
47
+ const slideNum = index === "title" ? 0 : parseInt(index, 10);
48
48
  setCurrentSlide(slideNum);
49
49
  setSearchParams(slideNum > 0 ? { slide: String(slideNum) } : {}, { replace: true });
50
50
  }
@@ -71,20 +71,16 @@ export function SlidesPage() {
71
71
  // Keyboard/scroll navigation helpers
72
72
  const goToPrevious = useCallback(() => {
73
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}"]`);
74
+ if (contentRef.current) {
75
+ const slideEl = contentRef.current.querySelector(`[data-slide-index="${prev}"]`);
78
76
  slideEl?.scrollIntoView({ behavior: "smooth" });
79
77
  }
80
78
  }, [currentSlide]);
81
79
 
82
80
  const goToNext = useCallback(() => {
83
81
  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}"]`);
82
+ if (contentRef.current) {
83
+ const slideEl = contentRef.current.querySelector(`[data-slide-index="${next}"]`);
88
84
  slideEl?.scrollIntoView({ behavior: "smooth" });
89
85
  }
90
86
  }, [currentSlide, totalSlides]);
@@ -137,11 +133,13 @@ export function SlidesPage() {
137
133
  onNext: goToNext,
138
134
  }}
139
135
  />
140
- <div {...{[FULLSCREEN_DATA_ATTR]: "true"}}>
141
- <div ref={contentRef}>
142
- <Content components={slidesMdxComponents} />
136
+ <FrontmatterProvider frontmatter={frontmatter}>
137
+ <div {...{[FULLSCREEN_DATA_ATTR]: "true"}}>
138
+ <div ref={contentRef}>
139
+ <Content components={slidesMdxComponents} />
140
+ </div>
143
141
  </div>
144
- </div>
142
+ </FrontmatterProvider>
145
143
  </main>
146
144
  )
147
145
  }
package/vite.config.ts CHANGED
@@ -80,10 +80,11 @@ export default defineConfig(({ command }) => {
80
80
  reactResolverPlugin(),
81
81
  tailwindcss(),
82
82
  // MDX for slides - splits at --- into <Slide> components
83
+ // Matches: SLIDES.mdx, slides.mdx, *.slides.mdx
83
84
  {
84
85
  enforce: 'pre',
85
86
  ...mdx({
86
- include: /SLIDES\.mdx$/,
87
+ include: /SLIDES\.mdx$|slides\.mdx$/i,
87
88
  remarkPlugins: [
88
89
  ...commonRemarkPlugins,
89
90
  remarkSlides, // Transform --- into <Slide> wrappers
@@ -92,11 +93,11 @@ export default defineConfig(({ command }) => {
92
93
  providerImportSource: '@mdx-js/react',
93
94
  }),
94
95
  },
95
- // MDX for regular posts
96
+ // MDX for regular posts (excludes all slides files)
96
97
  {
97
98
  enforce: 'pre',
98
99
  ...mdx({
99
- exclude: /SLIDES\.mdx$/,
100
+ exclude: /SLIDES\.mdx$|slides\.mdx$/i,
100
101
  remarkPlugins: commonRemarkPlugins,
101
102
  rehypePlugins: [rehypeKatex],
102
103
  providerImportSource: '@mdx-js/react',
@@ -1,50 +0,0 @@
1
- import { jsx } from "react/jsx-runtime";
2
- import { Link } from "react-router-dom";
3
- import { cn } from "../lib/utils.js";
4
- const views = [
5
- { key: "posts", label: "posts", path: "/posts" },
6
- { key: "docs", label: "docs", path: "/docs" },
7
- { key: "all", label: "all", path: "/all" }
8
- ];
9
- function ContentTabs({ value, counts }) {
10
- const hasOnlyPosts = counts.posts > 0 && counts.docs === 0;
11
- const hasOnlyDocs = counts.docs > 0 && counts.posts === 0;
12
- if (hasOnlyPosts || hasOnlyDocs) {
13
- return null;
14
- }
15
- const isDisabled = (key) => {
16
- if (key === "posts") return counts.posts === 0;
17
- if (key === "docs") return counts.docs === 0;
18
- return false;
19
- };
20
- return /* @__PURE__ */ jsx("nav", { className: "flex justify-end items-center gap-3 font-mono font-medium text-xs text-muted-foreground", children: views.map((view) => {
21
- const disabled = isDisabled(view.key);
22
- if (disabled) {
23
- return /* @__PURE__ */ jsx(
24
- "span",
25
- {
26
- className: "opacity-30 cursor-not-allowed",
27
- children: view.label
28
- },
29
- view.key
30
- );
31
- }
32
- return /* @__PURE__ */ jsx(
33
- Link,
34
- {
35
- to: view.path,
36
- className: cn(
37
- "transition-colors duration-150",
38
- "hover:text-foreground hover:underline hover:underline-offset-4 hover:decoration-primary/60",
39
- value === view.key ? "text-foreground underline-offset-4 decoration-primary/60" : ""
40
- ),
41
- children: view.label
42
- },
43
- view.key
44
- );
45
- }) });
46
- }
47
- export {
48
- ContentTabs
49
- };
50
- //# sourceMappingURL=content-tabs.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"content-tabs.js","sources":["../../../src/components/content-tabs.tsx"],"sourcesContent":["import { Link } from \"react-router-dom\";\nimport { cn } from \"@/lib/utils\";\nimport type { ContentView } from \"@/lib/content-classification\";\n\ninterface ContentTabsProps {\n value: ContentView;\n counts: { posts: number; docs: number; all: number };\n}\n\nconst views: { key: ContentView; label: string; path: string }[] = [\n { key: \"posts\", label: \"posts\", path: \"/posts\" },\n { key: \"docs\", label: \"docs\", path: \"/docs\" },\n { key: \"all\", label: \"all\", path: \"/all\" },\n];\n\nexport function ContentTabs({ value, counts }: ContentTabsProps) {\n const hasOnlyPosts = counts.posts > 0 && counts.docs === 0;\n const hasOnlyDocs = counts.docs > 0 && counts.posts === 0;\n\n if (hasOnlyPosts || hasOnlyDocs) {\n return null;\n }\n\n const isDisabled = (key: ContentView) => {\n if (key === \"posts\") return counts.posts === 0;\n if (key === \"docs\") return counts.docs === 0;\n return false;\n };\n\n return (\n <nav className=\"flex justify-end items-center gap-3 font-mono font-medium text-xs text-muted-foreground\">\n {views.map((view) => {\n const disabled = isDisabled(view.key);\n\n if (disabled) {\n return (\n <span\n key={view.key}\n className=\"opacity-30 cursor-not-allowed\"\n >\n {view.label}\n </span>\n );\n }\n\n return (\n <Link\n key={view.key}\n to={view.path}\n className={cn(\n \"transition-colors duration-150\",\n \"hover:text-foreground hover:underline hover:underline-offset-4 hover:decoration-primary/60\",\n value === view.key\n ? \"text-foreground underline-offset-4 decoration-primary/60\"\n : \"\"\n )}\n >\n {view.label}\n </Link>\n );\n })}\n </nav>\n );\n}\n"],"names":[],"mappings":";;;AASA,MAAM,QAA6D;AAAA,EACjE,EAAE,KAAK,SAAS,OAAO,SAAS,MAAM,SAAA;AAAA,EACtC,EAAE,KAAK,QAAQ,OAAO,QAAQ,MAAM,QAAA;AAAA,EACpC,EAAE,KAAK,OAAO,OAAO,OAAO,MAAM,OAAA;AACpC;AAEO,SAAS,YAAY,EAAE,OAAO,UAA4B;AAC/D,QAAM,eAAe,OAAO,QAAQ,KAAK,OAAO,SAAS;AACzD,QAAM,cAAc,OAAO,OAAO,KAAK,OAAO,UAAU;AAExD,MAAI,gBAAgB,aAAa;AAC/B,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,CAAC,QAAqB;AACvC,QAAI,QAAQ,QAAS,QAAO,OAAO,UAAU;AAC7C,QAAI,QAAQ,OAAQ,QAAO,OAAO,SAAS;AAC3C,WAAO;AAAA,EACT;AAEA,6BACG,OAAA,EAAI,WAAU,2FACZ,UAAA,MAAM,IAAI,CAAC,SAAS;AACnB,UAAM,WAAW,WAAW,KAAK,GAAG;AAEpC,QAAI,UAAU;AACZ,aACE;AAAA,QAAC;AAAA,QAAA;AAAA,UAEC,WAAU;AAAA,UAET,UAAA,KAAK;AAAA,QAAA;AAAA,QAHD,KAAK;AAAA,MAAA;AAAA,IAMhB;AAEA,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QAEC,IAAI,KAAK;AAAA,QACT,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA,UAAU,KAAK,MACX,6DACA;AAAA,QAAA;AAAA,QAGL,UAAA,KAAK;AAAA,MAAA;AAAA,MAVD,KAAK;AAAA,IAAA;AAAA,EAahB,CAAC,EAAA,CACH;AAEJ;"}