veslx 0.1.63 → 0.1.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/lib/serve.ts CHANGED
@@ -3,6 +3,7 @@ import importConfig from "./import-config.js";
3
3
  import veslxPlugin from '../../plugin/src/plugin.js'
4
4
  import path from 'path'
5
5
  import fs from 'fs'
6
+ import net from 'net'
6
7
  import { fileURLToPath } from 'url'
7
8
  import { log } from './log.js'
8
9
 
@@ -49,6 +50,11 @@ async function listenWithFallback(server: Awaited<ReturnType<typeof createServer
49
50
 
50
51
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
51
52
  const port = startPort + attempt;
53
+ if (await isPortOccupiedOnLoopback(port)) {
54
+ log.info(`busy :${port} → :${port + 1}`);
55
+ continue;
56
+ }
57
+
52
58
  try {
53
59
  await server.listen(port);
54
60
  const address = server.httpServer?.address();
@@ -67,6 +73,46 @@ async function listenWithFallback(server: Awaited<ReturnType<typeof createServer
67
73
  process.exit(1);
68
74
  }
69
75
 
76
+ async function isPortOccupiedOnLoopback(port: number): Promise<boolean> {
77
+ const hosts = ['127.0.0.1', '::1'];
78
+
79
+ for (const host of hosts) {
80
+ const canListen = await canListenOnHost(port, host);
81
+ if (!canListen) {
82
+ return true;
83
+ }
84
+ }
85
+
86
+ return false;
87
+ }
88
+
89
+ async function canListenOnHost(port: number, host: string): Promise<boolean> {
90
+ return await new Promise((resolve) => {
91
+ const probe = net.createServer();
92
+
93
+ probe.once('error', (err: NodeJS.ErrnoException) => {
94
+ // IPv6 may be unavailable on some systems; skip that host.
95
+ if (err.code === 'EAFNOSUPPORT' || err.code === 'EADDRNOTAVAIL') {
96
+ resolve(true);
97
+ return;
98
+ }
99
+
100
+ if (err.code === 'EADDRINUSE') {
101
+ resolve(false);
102
+ return;
103
+ }
104
+
105
+ resolve(true);
106
+ });
107
+
108
+ probe.once('listening', () => {
109
+ probe.close(() => resolve(true));
110
+ });
111
+
112
+ probe.listen(port, host);
113
+ });
114
+ }
115
+
70
116
  function resolveVeslxRoot() {
71
117
  const candidates = [
72
118
  new URL('../..', import.meta.url),
@@ -3,6 +3,7 @@ import importConfig from "./import-config.js";
3
3
  import veslxPlugin from '../../plugin/src/plugin.js';
4
4
  import path from 'path';
5
5
  import fs from 'fs';
6
+ import net from 'net';
6
7
  import { fileURLToPath } from 'url';
7
8
  import { log } from './log.js';
8
9
  async function readPackageJson(cwd) {
@@ -42,6 +43,10 @@ async function listenWithFallback(server) {
42
43
  const maxAttempts = 50;
43
44
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
44
45
  const port = startPort + attempt;
46
+ if (await isPortOccupiedOnLoopback(port)) {
47
+ log.info(`busy :${port} → :${port + 1}`);
48
+ continue;
49
+ }
45
50
  try {
46
51
  await server.listen(port);
47
52
  const address = server.httpServer?.address();
@@ -59,6 +64,37 @@ async function listenWithFallback(server) {
59
64
  log.error(`no available ports from ${startPort} to ${startPort + maxAttempts - 1}`);
60
65
  process.exit(1);
61
66
  }
67
+ async function isPortOccupiedOnLoopback(port) {
68
+ const hosts = ['127.0.0.1', '::1'];
69
+ for (const host of hosts) {
70
+ const canListen = await canListenOnHost(port, host);
71
+ if (!canListen) {
72
+ return true;
73
+ }
74
+ }
75
+ return false;
76
+ }
77
+ async function canListenOnHost(port, host) {
78
+ return await new Promise((resolve) => {
79
+ const probe = net.createServer();
80
+ probe.once('error', (err) => {
81
+ // IPv6 may be unavailable on some systems; skip that host.
82
+ if (err.code === 'EAFNOSUPPORT' || err.code === 'EADDRNOTAVAIL') {
83
+ resolve(true);
84
+ return;
85
+ }
86
+ if (err.code === 'EADDRINUSE') {
87
+ resolve(false);
88
+ return;
89
+ }
90
+ resolve(true);
91
+ });
92
+ probe.once('listening', () => {
93
+ probe.close(() => resolve(true));
94
+ });
95
+ probe.listen(port, host);
96
+ });
97
+ }
62
98
  function resolveVeslxRoot() {
63
99
  const candidates = [
64
100
  new URL('../..', import.meta.url),
@@ -83,7 +83,7 @@ function Gallery({
83
83
  return /* @__PURE__ */ jsx("figure", { className: "not-prose py-6 md:py-8", style: galleryStyle, children: /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-3 max-w-[var(--gallery-width)] mx-auto", children: [...Array(3)].map((_, i) => /* @__PURE__ */ jsx(
84
84
  "div",
85
85
  {
86
- className: "aspect-square rounded-sm bg-muted/20 relative overflow-hidden",
86
+ className: "h-40 rounded-sm bg-muted/20 relative overflow-hidden",
87
87
  children: /* @__PURE__ */ jsx(
88
88
  "div",
89
89
  {
@@ -113,14 +113,14 @@ function Gallery({
113
113
  "div",
114
114
  {
115
115
  title: img.label,
116
- className: `aspect-square overflow-hidden rounded-sm bg-muted/10 cursor-pointer group ${className || ""}`,
116
+ className: `overflow-hidden rounded-sm bg-muted/10 cursor-pointer group ${className || ""}`,
117
117
  onClick: () => lightbox.open(index),
118
118
  children: /* @__PURE__ */ jsx(
119
119
  LoadingImage,
120
120
  {
121
121
  src: img.src,
122
122
  alt: img.label,
123
- className: "object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
123
+ className: "w-full h-auto object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
124
124
  }
125
125
  )
126
126
  },
@@ -149,29 +149,13 @@ function Gallery({
149
149
  const offset = rowsToRender.slice(0, rowIndex).reduce((acc, row) => acc + row.length, 0);
150
150
  const rowSubtitle = subtitles == null ? void 0 : subtitles[rowIndex];
151
151
  const rowWrapperClass = "max-w-[var(--gallery-width)] w-full mx-auto";
152
- const placeholders = Math.max(0, maxRowColumns - rowImages.length);
153
- const rowCells = [
154
- ...rowImages.map((img, index) => ({ img, index })),
155
- ...Array.from({ length: placeholders }, () => ({ img: null, index: -1 }))
156
- ];
157
152
  return /* @__PURE__ */ jsxs("div", { children: [
158
153
  /* @__PURE__ */ jsx("div", { className: rowWrapperClass, children: /* @__PURE__ */ jsx(
159
154
  "div",
160
155
  {
161
156
  className: "grid gap-3",
162
157
  style: { gridTemplateColumns: `repeat(${Math.max(1, maxRowColumns)}, minmax(0, 1fr))` },
163
- children: rowCells.map((cell, index) => {
164
- if (!cell.img) {
165
- return /* @__PURE__ */ jsx(
166
- "div",
167
- {
168
- className: "aspect-square rounded-sm opacity-0 pointer-events-none"
169
- },
170
- `empty-${rowIndex}-${index}`
171
- );
172
- }
173
- return imageElement(offset + cell.index, cell.img, "w-full");
174
- })
158
+ children: rowImages.map((img, index) => imageElement(offset + index, img, "w-full"))
175
159
  }
176
160
  ) }),
177
161
  rowSubtitle && /* @__PURE__ */ jsx("div", { className: "max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]", children: /* @__PURE__ */ jsx(FigureHeader, { subtitle: rowSubtitle }) })
@@ -180,18 +164,18 @@ function Gallery({
180
164
  /* @__PURE__ */ jsx(FigureHeader, { title, subtitle }),
181
165
  imageElement(0, images[0]),
182
166
  /* @__PURE__ */ jsx(FigureCaption, { caption, label: captionLabel })
183
- ] }) : isCompact ? /* @__PURE__ */ jsx("div", { className: "flex gap-3", children: images.map((img, index) => imageElement(index, img, "flex-1")) }) : /* @__PURE__ */ jsx("div", { ref: scrollRef, className: "gallery-scroll-row flex gap-3 overflow-x-auto overscroll-x-contain pb-4", children: images.map((img, index) => /* @__PURE__ */ jsx(
167
+ ] }) : isCompact ? /* @__PURE__ */ jsx("div", { className: "flex items-start gap-3", children: images.map((img, index) => imageElement(index, img, "flex-1")) }) : /* @__PURE__ */ jsx("div", { ref: scrollRef, className: "gallery-scroll-row flex items-start gap-3 overflow-x-auto overscroll-x-contain pb-4", children: images.map((img, index) => /* @__PURE__ */ jsx(
184
168
  "div",
185
169
  {
186
170
  title: img.label,
187
- className: "flex-none w-[calc(var(--gallery-width)*0.3)] min-w-[250px] aspect-square overflow-hidden rounded-sm bg-muted/10 cursor-pointer group",
171
+ className: "flex-none w-[calc(var(--gallery-width)*0.3)] min-w-[250px] overflow-hidden rounded-sm bg-muted/10 cursor-pointer group",
188
172
  onClick: () => lightbox.open(index),
189
173
  children: /* @__PURE__ */ jsx(
190
174
  LoadingImage,
191
175
  {
192
176
  src: img.src,
193
177
  alt: img.label,
194
- className: "object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
178
+ className: "w-full h-auto object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
195
179
  }
196
180
  )
197
181
  },
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../src/components/gallery/index.tsx"],"sourcesContent":["import { useMemo, useRef, useEffect, type CSSProperties } from \"react\";\nimport { Image } from \"lucide-react\";\nimport { Lightbox, LightboxImage } from \"@/components/gallery/components/lightbox\";\nimport { useGalleryImages } from \"./hooks/use-gallery-images\";\nimport { useLightbox } from \"./hooks/use-lightbox\";\nimport { LoadingImage } from \"./components/loading-image\";\nimport { FigureHeader } from \"./components/figure-header\";\nimport { FigureCaption } from \"./components/figure-caption\";\n\n/**\n * Hook to prevent horizontal scroll from triggering browser back/forward gestures.\n * Captures wheel events and prevents default when at scroll boundaries.\n */\nfunction usePreventSwipeNavigation(ref: React.RefObject<HTMLElement | null>) {\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n\n const handleWheel = (e: WheelEvent) => {\n // Only handle horizontal scrolling\n if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;\n\n const { scrollLeft, scrollWidth, clientWidth } = el;\n const atLeftEdge = scrollLeft <= 0;\n const atRightEdge = scrollLeft + clientWidth >= scrollWidth - 1;\n\n // Prevent default if trying to scroll past boundaries\n if ((atLeftEdge && e.deltaX < 0) || (atRightEdge && e.deltaX > 0)) {\n e.preventDefault();\n }\n };\n\n el.addEventListener('wheel', handleWheel, { passive: false });\n return () => el.removeEventListener('wheel', handleWheel);\n }, [ref]);\n}\n\nfunction getImageLabel(path: string): string {\n const cleanPath = path.split(/[?#]/)[0];\n const filename = cleanPath.split('/').pop() || cleanPath;\n return filename\n .replace(/\\.(png|jpg|jpeg|gif|svg|webp)$/i, '')\n .replace(/[_-]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction isAbsoluteUrl(path: string): boolean {\n return /^https?:\\/\\//i.test(path) || path.startsWith('//');\n}\n\nfunction joinUrl(baseUrl: string, path: string): string {\n const trimmedBase = baseUrl.replace(/\\/+$/, '');\n const trimmedPath = path.replace(/^\\/+/, '');\n return `${trimmedBase}/${trimmedPath}`;\n}\n\nfunction getImageUrl(path: string, baseUrl?: string): string {\n if (isAbsoluteUrl(path)) return path;\n if (baseUrl) return joinUrl(baseUrl, path);\n return `/raw/${path}`;\n}\n\nexport default function Gallery({\n path,\n globs = null,\n baseUrl,\n caption,\n captionLabel,\n title,\n subtitle,\n subtitles,\n size = \"lg\",\n limit = null,\n page = 0,\n children,\n childAlign = \"right\",\n}: {\n path?: string;\n globs?: string[] | string[][] | null;\n baseUrl?: string;\n caption?: string;\n captionLabel?: string;\n title?: string;\n subtitle?: string;\n subtitles?: string[];\n size?: \"sm\" | \"md\" | \"lg\";\n limit?: number | null;\n page?: number;\n children?: React.ReactNode;\n childAlign?: \"left\" | \"right\";\n}) {\n const { paths, rows, isLoading, isEmpty } = useGalleryImages({\n path,\n globs,\n limit,\n page: page,\n });\n\n const lightbox = useLightbox(paths.length);\n const scrollRef = useRef<HTMLDivElement>(null);\n usePreventSwipeNavigation(scrollRef);\n\n const galleryWidthMap: Record<\"sm\" | \"md\" | \"lg\", string> = {\n sm: \"min(80vw, 18rem)\",\n md: \"min(85vw, 30rem)\",\n lg: \"min(90vw, 48rem)\",\n };\n const galleryStyle = { \"--gallery-width\": galleryWidthMap[size] } as CSSProperties;\n\n const rowsToRender = rows.length > 0 ? rows : [paths];\n const flatPaths = rowsToRender.flat();\n const maxRowColumns = rowsToRender.reduce((max, row) => Math.max(max, row.length), 0);\n\n const images: LightboxImage[] = useMemo(\n () => flatPaths.map(p => ({ src: getImageUrl(p, baseUrl), label: getImageLabel(p) })),\n [flatPaths, baseUrl]\n );\n\n if (isLoading) {\n return (\n <figure className=\"not-prose py-6 md:py-8\" style={galleryStyle}>\n <div className=\"grid grid-cols-3 gap-3 max-w-[var(--gallery-width)] mx-auto\">\n {[...Array(3)].map((_, i) => (\n <div\n key={i}\n className=\"aspect-square rounded-sm bg-muted/20 relative overflow-hidden\"\n >\n <div\n className=\"absolute inset-0 bg-gradient-to-r from-transparent via-muted/30 to-transparent animate-shimmer\"\n style={{ animationDelay: `${i * 150}ms` }}\n />\n </div>\n ))}\n </div>\n </figure>\n );\n }\n\n if (isEmpty) {\n return (\n <figure className=\"not-prose py-12 text-center\" style={galleryStyle}>\n <div className=\"inline-flex items-center gap-2.5 text-muted-foreground/40\">\n <Image className=\"h-3.5 w-3.5\" strokeWidth={1.5} />\n <span className=\"font-mono text-xs uppercase tracking-widest\">No images</span>\n </div>\n </figure>\n );\n }\n\n const isSingle = images.length === 1;\n const isTwo = images.length === 2;\n const isCompact = images.length <= 3;\n const isGroupedRows = rowsToRender.length > 1;\n const isScrollRow = images.length > 3;\n const isSingleWithChildren = images.length === 1 && children;\n // 2+ images should break out of the content width\n const shouldBreakOut = images.length >= 2;\n const breakoutClass = shouldBreakOut\n ? isGroupedRows\n ? \"\"\n : isScrollRow\n ? \"gallery-breakout w-screen\"\n : \"gallery-breakout w-[var(--gallery-width)] max-w-none\"\n : \"\";\n\n const imageElement = (index: number, img: LightboxImage, className?: string) => (\n <div\n key={index}\n title={img.label}\n className={`aspect-square overflow-hidden rounded-sm bg-muted/10 cursor-pointer group ${className || ''}`}\n onClick={() => lightbox.open(index)}\n >\n <LoadingImage\n src={img.src}\n alt={img.label}\n className=\"object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]\"\n />\n </div>\n );\n\n return (\n <>\n <figure\n className={`not-prose relative py-6 md:py-8 ${breakoutClass}`}\n style={galleryStyle}\n >\n {!isSingleWithChildren && !isSingle && (\n <div className=\"max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]\">\n <FigureHeader title={title} subtitle={subtitle} />\n </div>\n )}\n\n {isSingleWithChildren ? (\n <div className={`grid items-start gap-12 md:gap-16 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] ${childAlign === 'left' ? '' : 'md:[&>div:first-child]:order-2'}`}>\n <div className=\"min-w-0 self-center 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\">\n {children}\n </div>\n <div className=\"min-w-0\">\n <FigureHeader title={title} subtitle={subtitle} />\n {imageElement(0, images[0])}\n <FigureCaption caption={caption} label={captionLabel} />\n </div>\n </div>\n ) : isGroupedRows ? (\n <div className=\"space-y-6\">\n {rowsToRender.map((rowPaths, rowIndex) => {\n const rowImages = rowPaths.map((p) => ({\n src: getImageUrl(p, baseUrl),\n label: getImageLabel(p),\n }));\n const offset = rowsToRender.slice(0, rowIndex).reduce((acc, row) => acc + row.length, 0);\n const rowSubtitle = subtitles?.[rowIndex];\n const rowWrapperClass = \"max-w-[var(--gallery-width)] w-full mx-auto\";\n const placeholders = Math.max(0, maxRowColumns - rowImages.length);\n const rowCells = [\n ...rowImages.map((img, index) => ({ img, index })),\n ...Array.from({ length: placeholders }, () => ({ img: null, index: -1 })),\n ];\n\n return (\n <div key={`${rowIndex}-${rowPaths.join(\"|\")}`}>\n <div className={rowWrapperClass}>\n <div\n className=\"grid gap-3\"\n style={{ gridTemplateColumns: `repeat(${Math.max(1, maxRowColumns)}, minmax(0, 1fr))` }}\n >\n {rowCells.map((cell, index) => {\n if (!cell.img) {\n return (\n <div\n key={`empty-${rowIndex}-${index}`}\n className=\"aspect-square rounded-sm opacity-0 pointer-events-none\"\n />\n );\n }\n return imageElement(offset + cell.index, cell.img, 'w-full');\n })}\n </div>\n </div>\n\n {rowSubtitle && (\n <div className=\"max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]\">\n <FigureHeader subtitle={rowSubtitle} />\n </div>\n )}\n </div>\n );\n })}\n </div>\n ) : isSingle ? (\n <div className=\"max-w-[var(--gallery-width)] w-full mx-auto\">\n <FigureHeader title={title} subtitle={subtitle} />\n {imageElement(0, images[0])}\n <FigureCaption caption={caption} label={captionLabel} />\n </div>\n ) : isCompact ? (\n <div className=\"flex gap-3\">\n {images.map((img, index) => imageElement(index, img, 'flex-1'))}\n </div>\n ) : (\n <div ref={scrollRef} className=\"gallery-scroll-row flex gap-3 overflow-x-auto overscroll-x-contain pb-4\">\n {images.map((img, index) => (\n <div\n key={index}\n title={img.label}\n className=\"flex-none w-[calc(var(--gallery-width)*0.3)] min-w-[250px] aspect-square overflow-hidden rounded-sm bg-muted/10 cursor-pointer group\"\n onClick={() => lightbox.open(index)}\n >\n <LoadingImage\n src={img.src}\n alt={img.label}\n className=\"object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]\"\n />\n </div>\n ))}\n </div>\n )}\n\n {!isSingleWithChildren && !isSingle && (\n <div className=\"max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]\">\n <FigureCaption caption={caption} label={captionLabel} />\n </div>\n )}\n </figure>\n\n {lightbox.isOpen && lightbox.selectedIndex !== null && (\n <Lightbox\n images={images}\n selectedIndex={lightbox.selectedIndex}\n onClose={lightbox.close}\n onPrevious={lightbox.goToPrevious}\n onNext={lightbox.goToNext}\n />\n )}\n </>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../../../src/components/gallery/index.tsx"],"sourcesContent":["import { useMemo, useRef, useEffect, type CSSProperties } from \"react\";\nimport { Image } from \"lucide-react\";\nimport { Lightbox, LightboxImage } from \"@/components/gallery/components/lightbox\";\nimport { useGalleryImages } from \"./hooks/use-gallery-images\";\nimport { useLightbox } from \"./hooks/use-lightbox\";\nimport { LoadingImage } from \"./components/loading-image\";\nimport { FigureHeader } from \"./components/figure-header\";\nimport { FigureCaption } from \"./components/figure-caption\";\n\n/**\n * Hook to prevent horizontal scroll from triggering browser back/forward gestures.\n * Captures wheel events and prevents default when at scroll boundaries.\n */\nfunction usePreventSwipeNavigation(ref: React.RefObject<HTMLElement | null>) {\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n\n const handleWheel = (e: WheelEvent) => {\n // Only handle horizontal scrolling\n if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;\n\n const { scrollLeft, scrollWidth, clientWidth } = el;\n const atLeftEdge = scrollLeft <= 0;\n const atRightEdge = scrollLeft + clientWidth >= scrollWidth - 1;\n\n // Prevent default if trying to scroll past boundaries\n if ((atLeftEdge && e.deltaX < 0) || (atRightEdge && e.deltaX > 0)) {\n e.preventDefault();\n }\n };\n\n el.addEventListener('wheel', handleWheel, { passive: false });\n return () => el.removeEventListener('wheel', handleWheel);\n }, [ref]);\n}\n\nfunction getImageLabel(path: string): string {\n const cleanPath = path.split(/[?#]/)[0];\n const filename = cleanPath.split('/').pop() || cleanPath;\n return filename\n .replace(/\\.(png|jpg|jpeg|gif|svg|webp)$/i, '')\n .replace(/[_-]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction isAbsoluteUrl(path: string): boolean {\n return /^https?:\\/\\//i.test(path) || path.startsWith('//');\n}\n\nfunction joinUrl(baseUrl: string, path: string): string {\n const trimmedBase = baseUrl.replace(/\\/+$/, '');\n const trimmedPath = path.replace(/^\\/+/, '');\n return `${trimmedBase}/${trimmedPath}`;\n}\n\nfunction getImageUrl(path: string, baseUrl?: string): string {\n if (isAbsoluteUrl(path)) return path;\n if (baseUrl) return joinUrl(baseUrl, path);\n return `/raw/${path}`;\n}\n\nexport default function Gallery({\n path,\n globs = null,\n baseUrl,\n caption,\n captionLabel,\n title,\n subtitle,\n subtitles,\n size = \"lg\",\n limit = null,\n page = 0,\n children,\n childAlign = \"right\",\n}: {\n path?: string;\n globs?: string[] | string[][] | null;\n baseUrl?: string;\n caption?: string;\n captionLabel?: string;\n title?: string;\n subtitle?: string;\n subtitles?: string[];\n size?: \"sm\" | \"md\" | \"lg\";\n limit?: number | null;\n page?: number;\n children?: React.ReactNode;\n childAlign?: \"left\" | \"right\";\n}) {\n const { paths, rows, isLoading, isEmpty } = useGalleryImages({\n path,\n globs,\n limit,\n page: page,\n });\n\n const lightbox = useLightbox(paths.length);\n const scrollRef = useRef<HTMLDivElement>(null);\n usePreventSwipeNavigation(scrollRef);\n\n const galleryWidthMap: Record<\"sm\" | \"md\" | \"lg\", string> = {\n sm: \"min(80vw, 18rem)\",\n md: \"min(85vw, 30rem)\",\n lg: \"min(90vw, 48rem)\",\n };\n const galleryStyle = { \"--gallery-width\": galleryWidthMap[size] } as CSSProperties;\n\n const rowsToRender = rows.length > 0 ? rows : [paths];\n const flatPaths = rowsToRender.flat();\n const maxRowColumns = rowsToRender.reduce((max, row) => Math.max(max, row.length), 0);\n\n const images: LightboxImage[] = useMemo(\n () => flatPaths.map(p => ({ src: getImageUrl(p, baseUrl), label: getImageLabel(p) })),\n [flatPaths, baseUrl]\n );\n\n if (isLoading) {\n return (\n <figure className=\"not-prose py-6 md:py-8\" style={galleryStyle}>\n <div className=\"grid grid-cols-3 gap-3 max-w-[var(--gallery-width)] mx-auto\">\n {[...Array(3)].map((_, i) => (\n <div\n key={i}\n className=\"h-40 rounded-sm bg-muted/20 relative overflow-hidden\"\n >\n <div\n className=\"absolute inset-0 bg-gradient-to-r from-transparent via-muted/30 to-transparent animate-shimmer\"\n style={{ animationDelay: `${i * 150}ms` }}\n />\n </div>\n ))}\n </div>\n </figure>\n );\n }\n\n if (isEmpty) {\n return (\n <figure className=\"not-prose py-12 text-center\" style={galleryStyle}>\n <div className=\"inline-flex items-center gap-2.5 text-muted-foreground/40\">\n <Image className=\"h-3.5 w-3.5\" strokeWidth={1.5} />\n <span className=\"font-mono text-xs uppercase tracking-widest\">No images</span>\n </div>\n </figure>\n );\n }\n\n const isSingle = images.length === 1;\n const isTwo = images.length === 2;\n const isCompact = images.length <= 3;\n const isGroupedRows = rowsToRender.length > 1;\n const isScrollRow = images.length > 3;\n const isSingleWithChildren = images.length === 1 && children;\n // 2+ images should break out of the content width\n const shouldBreakOut = images.length >= 2;\n const breakoutClass = shouldBreakOut\n ? isGroupedRows\n ? \"\"\n : isScrollRow\n ? \"gallery-breakout w-screen\"\n : \"gallery-breakout w-[var(--gallery-width)] max-w-none\"\n : \"\";\n\n const imageElement = (index: number, img: LightboxImage, className?: string) => (\n <div\n key={index}\n title={img.label}\n className={`overflow-hidden rounded-sm bg-muted/10 cursor-pointer group ${className || ''}`}\n onClick={() => lightbox.open(index)}\n >\n <LoadingImage\n src={img.src}\n alt={img.label}\n className=\"w-full h-auto object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]\"\n />\n </div>\n );\n\n return (\n <>\n <figure\n className={`not-prose relative py-6 md:py-8 ${breakoutClass}`}\n style={galleryStyle}\n >\n {!isSingleWithChildren && !isSingle && (\n <div className=\"max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]\">\n <FigureHeader title={title} subtitle={subtitle} />\n </div>\n )}\n\n {isSingleWithChildren ? (\n <div className={`grid items-start gap-12 md:gap-16 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] ${childAlign === 'left' ? '' : 'md:[&>div:first-child]:order-2'}`}>\n <div className=\"min-w-0 self-center 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\">\n {children}\n </div>\n <div className=\"min-w-0\">\n <FigureHeader title={title} subtitle={subtitle} />\n {imageElement(0, images[0])}\n <FigureCaption caption={caption} label={captionLabel} />\n </div>\n </div>\n ) : isGroupedRows ? (\n <div className=\"space-y-6\">\n {rowsToRender.map((rowPaths, rowIndex) => {\n const rowImages = rowPaths.map((p) => ({\n src: getImageUrl(p, baseUrl),\n label: getImageLabel(p),\n }));\n const offset = rowsToRender.slice(0, rowIndex).reduce((acc, row) => acc + row.length, 0);\n const rowSubtitle = subtitles?.[rowIndex];\n const rowWrapperClass = \"max-w-[var(--gallery-width)] w-full mx-auto\";\n\n return (\n <div key={`${rowIndex}-${rowPaths.join(\"|\")}`}>\n <div className={rowWrapperClass}>\n <div\n className=\"grid gap-3\"\n style={{ gridTemplateColumns: `repeat(${Math.max(1, maxRowColumns)}, minmax(0, 1fr))` }}\n >\n {rowImages.map((img, index) => imageElement(offset + index, img, 'w-full'))}\n </div>\n </div>\n\n {rowSubtitle && (\n <div className=\"max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]\">\n <FigureHeader subtitle={rowSubtitle} />\n </div>\n )}\n </div>\n );\n })}\n </div>\n ) : isSingle ? (\n <div className=\"max-w-[var(--gallery-width)] w-full mx-auto\">\n <FigureHeader title={title} subtitle={subtitle} />\n {imageElement(0, images[0])}\n <FigureCaption caption={caption} label={captionLabel} />\n </div>\n ) : isCompact ? (\n <div className=\"flex items-start gap-3\">\n {images.map((img, index) => imageElement(index, img, 'flex-1'))}\n </div>\n ) : (\n <div ref={scrollRef} className=\"gallery-scroll-row flex items-start gap-3 overflow-x-auto overscroll-x-contain pb-4\">\n {images.map((img, index) => (\n <div\n key={index}\n title={img.label}\n className=\"flex-none w-[calc(var(--gallery-width)*0.3)] min-w-[250px] overflow-hidden rounded-sm bg-muted/10 cursor-pointer group\"\n onClick={() => lightbox.open(index)}\n >\n <LoadingImage\n src={img.src}\n alt={img.label}\n className=\"w-full h-auto object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]\"\n />\n </div>\n ))}\n </div>\n )}\n\n {!isSingleWithChildren && !isSingle && (\n <div className=\"max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]\">\n <FigureCaption caption={caption} label={captionLabel} />\n </div>\n )}\n </figure>\n\n {lightbox.isOpen && lightbox.selectedIndex !== null && (\n <Lightbox\n images={images}\n selectedIndex={lightbox.selectedIndex}\n onClose={lightbox.close}\n onPrevious={lightbox.goToPrevious}\n onNext={lightbox.goToNext}\n />\n )}\n </>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -51,7 +51,14 @@ function sortPathsNumerically(paths) {
51
51
  return a.localeCompare(b);
52
52
  });
53
53
  }
54
- function SingleParameterTable({ path, pairs, label, withMargin = true, compact = false }) {
54
+ function SingleParameterTable({
55
+ path,
56
+ pairs,
57
+ label,
58
+ withMargin = true,
59
+ scrollable = true,
60
+ compact = false
61
+ }) {
55
62
  const { content, loading, error } = useFileContent(path);
56
63
  if (!pairs || pairs.length === 0) {
57
64
  return /* @__PURE__ */ jsx("div", { className: cn("p-3 rounded border border-border/50 bg-card/30", withMargin && "my-6"), children: /* @__PURE__ */ jsx("p", { className: "text-[11px] font-mono text-muted-foreground", children: "pairs is required" }) });
@@ -89,7 +96,10 @@ function SingleParameterTable({ path, pairs, label, withMargin = true, compact =
89
96
  ] }) });
90
97
  }
91
98
  const renderPairsTable = () => {
92
- return /* @__PURE__ */ jsx("div", { className: "not-prose my-6 overflow-x-auto border border-border rounded-md", children: /* @__PURE__ */ jsxs("table", { className: cn(
99
+ return /* @__PURE__ */ jsx("div", { className: cn(
100
+ "border border-border rounded-md",
101
+ scrollable ? "overflow-x-auto" : "overflow-x-hidden"
102
+ ), children: /* @__PURE__ */ jsxs("table", { className: cn(
93
103
  "w-full border-collapse",
94
104
  compact ? "text-[11px]" : "text-sm"
95
105
  ), children: [
@@ -187,14 +197,15 @@ function ParameterTable({ path, pairs, compact = false }) {
187
197
  const scrollRef = useRef(null);
188
198
  usePreventSwipeNavigation(scrollRef);
189
199
  const count = matchingPaths.length;
190
- const breakoutClass = count >= 4 ? "w-[96vw] ml-[calc(-48vw+50%)]" : count >= 2 ? "w-[75vw] ml-[calc(-37.5vw+50%)]" : "";
191
- return /* @__PURE__ */ jsx("div", { className: `my-6 ${breakoutClass}`, children: /* @__PURE__ */ jsx("div", { ref: scrollRef, className: "flex gap-4 overflow-x-auto overscroll-x-contain pb-2", children: matchingPaths.map((filePath) => /* @__PURE__ */ jsx("div", { className: "flex-none w-[280px]", children: /* @__PURE__ */ jsx(
200
+ const breakoutClass = count >= 4 ? "relative left-1/2 w-[96vw] -translate-x-1/2" : count >= 2 ? "relative left-1/2 w-[75vw] -translate-x-1/2" : "";
201
+ return /* @__PURE__ */ jsx("div", { className: `my-6 ${breakoutClass} overflow-x-hidden`, children: /* @__PURE__ */ jsx("div", { ref: scrollRef, className: "flex gap-4 overflow-x-auto overscroll-x-contain pb-2", children: matchingPaths.map((filePath) => /* @__PURE__ */ jsx("div", { className: "flex-none w-[280px]", children: /* @__PURE__ */ jsx(
192
202
  SingleParameterTable,
193
203
  {
194
204
  path: filePath,
195
205
  pairs,
196
206
  label: filePath.split("/").pop() || filePath,
197
207
  withMargin: false,
208
+ scrollable: false,
198
209
  compact
199
210
  }
200
211
  ) }, filePath)) }) });
@@ -1 +1 @@
1
- {"version":3,"file":"parameter-table.js","sources":["../../../src/components/parameter-table.tsx"],"sourcesContent":["import { useFileContent, useDirectory } from \"../../plugin/src/client\";\nimport { useMemo, useRef, useEffect } from \"react\";\nimport { useParams } from \"react-router-dom\";\nimport { cn } from \"@/lib/utils\";\nimport { minimatch } from \"minimatch\";\nimport {\n type ParameterValue,\n extractPath,\n getValueType,\n formatValue,\n parseConfigFile,\n} from \"@/lib/parameter-utils\";\nimport { FileEntry, DirectoryEntry } from \"../../plugin/src/lib\";\n\n/**\n * Hook to prevent horizontal scroll from triggering browser back/forward gestures.\n */\nfunction usePreventSwipeNavigation(ref: React.RefObject<HTMLElement | null>) {\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n\n const handleWheel = (e: WheelEvent) => {\n if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;\n\n const { scrollLeft, scrollWidth, clientWidth } = el;\n const atLeftEdge = scrollLeft <= 0;\n const atRightEdge = scrollLeft + clientWidth >= scrollWidth - 1;\n\n if ((atLeftEdge && e.deltaX < 0) || (atRightEdge && e.deltaX > 0)) {\n e.preventDefault();\n }\n };\n\n el.addEventListener('wheel', handleWheel, { passive: false });\n return () => el.removeEventListener('wheel', handleWheel);\n }, [ref]);\n}\n\n// Check if a path contains glob patterns\nfunction isGlobPattern(path: string): boolean {\n return path.includes('*') || path.includes('?') || path.includes('[');\n}\n\n// Recursively collect all config files from a directory tree\nfunction collectAllConfigFiles(entry: DirectoryEntry | FileEntry): FileEntry[] {\n if (entry.type === \"file\") {\n if (entry.name.match(/\\.(yaml|yml|json)$/i)) {\n return [entry];\n }\n return [];\n }\n const files: FileEntry[] = [];\n for (const child of entry.children || []) {\n files.push(...collectAllConfigFiles(child));\n }\n return files;\n}\n\n// Sort paths numerically\nfunction sortPathsNumerically(paths: string[]): void {\n paths.sort((a, b) => {\n const nums = (s: string) => (s.match(/\\d+/g) || []).map(Number);\n const na = nums(a);\n const nb = nums(b);\n const len = Math.max(na.length, nb.length);\n for (let i = 0; i < len; i++) {\n const diff = (na[i] ?? 0) - (nb[i] ?? 0);\n if (diff !== 0) return diff;\n }\n return a.localeCompare(b);\n });\n}\n\ninterface SingleParameterTableProps {\n /** Path to the YAML or JSON file */\n path: string;\n /**\n * Required array of key/label pairs to render as a markdown-style table.\n * Each key is a jq-like path (e.g., \".base.dt\").\n */\n pairs: Array<{ key: string; label?: string }>;\n /** Optional label to show above the table */\n label?: string;\n /** Whether to include vertical margin (default true) */\n withMargin?: boolean;\n /** Compact mode for dense rendering */\n compact?: boolean;\n}\n\ninterface ParameterTableProps {\n /** Path to the YAML or JSON file, supports glob patterns like \"*.yaml\" */\n path: string;\n /**\n * Required array of key/label pairs to render Complete table.\n */\n pairs: Array<{ key: string; label?: string }>;\n /** Compact mode for dense rendering */\n compact?: boolean;\n}\n\nfunction SingleParameterTable({ path, pairs, label, withMargin = true, compact = false }: SingleParameterTableProps) {\n const { content, loading, error } = useFileContent(path);\n\n if (!pairs || pairs.length === 0) {\n return (\n <div className={cn(\"p-3 rounded border border-border/50 bg-card/30\", withMargin && \"my-6\")}>\n <p className=\"text-[11px] font-mono text-muted-foreground\">\n pairs is required\n </p>\n </div>\n );\n }\n\n const { parsed, parseError } = useMemo(() => {\n if (!content) return { parsed: null, parseError: 'no content' };\n\n const data = parseConfigFile(content, path);\n if (!data) {\n // Check why parsing failed\n if (!path.match(/\\.(yaml|yml|json)$/i)) {\n return { parsed: null, parseError: `unsupported file type` };\n }\n // Check if content looks like HTML (404 page)\n if (content.trim().startsWith('<!') || content.trim().startsWith('<html')) {\n return { parsed: null, parseError: `file not found` };\n }\n return { parsed: null, parseError: `invalid ${path.split('.').pop()} syntax` };\n }\n\n return { parsed: data, parseError: null };\n }, [content, path]);\n\n if (loading) {\n return (\n <div className=\"my-6 p-4 rounded border border-border/50 bg-card/30\">\n <div className=\"flex items-center gap-2 text-muted-foreground/60\">\n <div className=\"w-3 h-3 border border-current border-t-transparent rounded-full animate-spin\" />\n <span className=\"text-[11px] font-mono\">loading parameters...</span>\n </div>\n </div>\n );\n }\n\n if (error) {\n return (\n <div className=\"my-6 p-3 rounded border border-destructive/30 bg-destructive/5\">\n <p className=\"text-[11px] font-mono text-destructive\">{error}</p>\n </div>\n );\n }\n\n if (!parsed) {\n return (\n <div className={cn(\"p-3 rounded border border-border/50 bg-card/30\", withMargin && \"my-6\")}>\n <p className=\"text-[11px] font-mono text-muted-foreground\">\n {label && <span className=\"text-foreground/60\">{label}: </span>}\n {parseError || 'unable to parse'}\n </p>\n </div>\n );\n }\n\n const renderPairsTable = () => {\n return (\n <div className=\"not-prose my-6 overflow-x-auto border border-border rounded-md\">\n <table className={cn(\n \"w-full border-collapse\",\n compact ? \"text-[11px]\" : \"text-sm\"\n )}>\n <thead className=\"bg-muted/50\">\n <tr className=\"border-b border-border last:border-b-0\">\n <th className={cn(\n \"text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\",\n compact ? \"px-3 py-2\" : \"px-4 py-3\"\n )}>\n Parameter\n </th>\n <th className={cn(\n \"text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\",\n compact ? \"px-3 py-2\" : \"px-4 py-3\"\n )}>\n Value\n </th>\n </tr>\n </thead>\n <tbody>\n {pairs.map(({ key, label: rowLabel }) => {\n const value = extractPath(parsed, key);\n const type = value === undefined ? \"missing\" : getValueType(value);\n const displayValue = value === undefined\n ? \"—\"\n : type === \"string\"\n ? `\"${formatValue(value)}\"`\n : formatValue(value);\n\n return (\n <tr key={key} className=\"border-b border-border last:border-b-0\">\n <td className={cn(\n \"align-top\",\n compact ? \"px-3 py-2\" : \"px-4 py-3\"\n )}>{rowLabel || key}</td>\n <td\n className={cn(\n \"align-top\",\n compact ? \"px-3 py-2\" : \"px-4 py-3\",\n type === \"missing\" && \"text-muted-foreground\"\n )}\n >\n {displayValue}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n );\n };\n\n return (\n <div className={cn(\"not-prose\", withMargin && \"my-6\")}>\n {label && (\n <div className=\"text-[11px] font-mono text-muted-foreground mb-1.5 truncate\" title={label}>\n {label}\n </div>\n )}\n {renderPairsTable()}\n </div>\n );\n}\n\n/**\n * ParameterTable component that displays YAML/JSON config files.\n * Supports glob patterns in the path prop to show multiple files.\n */\nexport function ParameterTable({ path, pairs, compact = false }: ParameterTableProps) {\n const { \"*\": routePath = \"\" } = useParams();\n\n if (!pairs || pairs.length === 0) {\n return (\n <div className=\"my-6 p-3 rounded border border-border/50 bg-card/30\">\n <p className=\"text-[11px] font-mono text-muted-foreground\">pairs is required</p>\n </div>\n );\n }\n\n // Get current directory from route\n const currentDir = routePath\n .replace(/\\/?[^/]+\\.mdx$/i, \"\")\n .replace(/\\/$/, \"\")\n || \".\";\n\n // Resolve relative paths\n let resolvedPath = path;\n if (path?.startsWith(\"./\")) {\n const relativePart = path.slice(2);\n resolvedPath = currentDir === \".\" ? relativePart : `${currentDir}/${relativePart}`;\n } else if (path && !path.startsWith(\"/\") && !path.includes(\"/\") && !isGlobPattern(path)) {\n resolvedPath = currentDir === \".\" ? path : `${currentDir}/${path}`;\n }\n\n // Check if this is a glob pattern\n const hasGlob = isGlobPattern(resolvedPath);\n\n // For glob patterns, get the base directory (directory containing the glob pattern)\n const baseDir = useMemo(() => {\n if (!hasGlob) return null;\n // Get everything before the first glob character\n const beforeGlob = resolvedPath.split(/[*?\\[]/, 1)[0];\n // Extract directory portion (everything up to the last slash)\n const lastSlash = beforeGlob.lastIndexOf('/');\n if (lastSlash === -1) return \".\";\n return beforeGlob.slice(0, lastSlash) || \".\";\n }, [hasGlob, resolvedPath]);\n\n const { directory } = useDirectory(baseDir || \".\");\n\n // Find matching files for glob patterns\n const matchingPaths = useMemo(() => {\n if (!hasGlob || !directory) return [];\n\n const allFiles = collectAllConfigFiles(directory);\n const paths = allFiles\n .map(f => f.path)\n .filter(p => minimatch(p, resolvedPath, { matchBase: true }));\n\n sortPathsNumerically(paths);\n\n return paths;\n }, [hasGlob, directory, resolvedPath, path, baseDir]);\n\n // If not a glob pattern, just render the single table\n if (!hasGlob) {\n return <SingleParameterTable path={resolvedPath} pairs={pairs} compact={compact} />;\n }\n\n // Loading state for glob patterns\n if (!directory) {\n return (\n <div className=\"my-6 p-4 rounded border border-border/50 bg-card/30\">\n <div className=\"flex items-center gap-2 text-muted-foreground/60\">\n <div className=\"w-3 h-3 border border-current border-t-transparent rounded-full animate-spin\" />\n <span className=\"text-[11px] font-mono\">loading parameters...</span>\n </div>\n </div>\n );\n }\n\n // No matches\n if (matchingPaths.length === 0) {\n return (\n <div className=\"my-6 p-3 rounded border border-border/50 bg-card/30\">\n <p className=\"text-[11px] font-mono text-muted-foreground\">\n no files matching: {resolvedPath}\n <br />\n <span className=\"text-muted-foreground/50\">(base dir: {baseDir}, original: {path})</span>\n </p>\n </div>\n );\n }\n\n const scrollRef = useRef<HTMLDivElement>(null);\n usePreventSwipeNavigation(scrollRef);\n\n // Breakout width based on count\n const count = matchingPaths.length;\n const breakoutClass = count >= 4\n ? 'w-[96vw] ml-[calc(-48vw+50%)]'\n : count >= 2\n ? 'w-[75vw] ml-[calc(-37.5vw+50%)]'\n : '';\n\n return (\n <div className={`my-6 ${breakoutClass}`}>\n <div ref={scrollRef} className=\"flex gap-4 overflow-x-auto overscroll-x-contain pb-2\">\n {matchingPaths.map((filePath) => (\n <div key={filePath} className=\"flex-none w-[280px]\">\n <SingleParameterTable\n path={filePath}\n pairs={pairs}\n label={filePath.split('/').pop() || filePath}\n withMargin={false}\n compact={compact}\n />\n </div>\n ))}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;AAiBA,SAAS,0BAA0B,KAA0C;AAC3E,YAAU,MAAM;AACd,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AAET,UAAM,cAAc,CAAC,MAAkB;AACrC,UAAI,KAAK,IAAI,EAAE,MAAM,KAAK,KAAK,IAAI,EAAE,MAAM,EAAG;AAE9C,YAAM,EAAE,YAAY,aAAa,YAAA,IAAgB;AACjD,YAAM,aAAa,cAAc;AACjC,YAAM,cAAc,aAAa,eAAe,cAAc;AAE9D,UAAK,cAAc,EAAE,SAAS,KAAO,eAAe,EAAE,SAAS,GAAI;AACjE,UAAE,eAAA;AAAA,MACJ;AAAA,IACF;AAEA,OAAG,iBAAiB,SAAS,aAAa,EAAE,SAAS,OAAO;AAC5D,WAAO,MAAM,GAAG,oBAAoB,SAAS,WAAW;AAAA,EAC1D,GAAG,CAAC,GAAG,CAAC;AACV;AAGA,SAAS,cAAc,MAAuB;AAC5C,SAAO,KAAK,SAAS,GAAG,KAAK,KAAK,SAAS,GAAG,KAAK,KAAK,SAAS,GAAG;AACtE;AAGA,SAAS,sBAAsB,OAAgD;AAC7E,MAAI,MAAM,SAAS,QAAQ;AACzB,QAAI,MAAM,KAAK,MAAM,qBAAqB,GAAG;AAC3C,aAAO,CAAC,KAAK;AAAA,IACf;AACA,WAAO,CAAA;AAAA,EACT;AACA,QAAM,QAAqB,CAAA;AAC3B,aAAW,SAAS,MAAM,YAAY,CAAA,GAAI;AACxC,UAAM,KAAK,GAAG,sBAAsB,KAAK,CAAC;AAAA,EAC5C;AACA,SAAO;AACT;AAGA,SAAS,qBAAqB,OAAuB;AACnD,QAAM,KAAK,CAAC,GAAG,MAAM;AACnB,UAAM,OAAO,CAAC,OAAe,EAAE,MAAM,MAAM,KAAK,CAAA,GAAI,IAAI,MAAM;AAC9D,UAAM,KAAK,KAAK,CAAC;AACjB,UAAM,KAAK,KAAK,CAAC;AACjB,UAAM,MAAM,KAAK,IAAI,GAAG,QAAQ,GAAG,MAAM;AACzC,aAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,YAAM,QAAQ,GAAG,CAAC,KAAK,MAAM,GAAG,CAAC,KAAK;AACtC,UAAI,SAAS,EAAG,QAAO;AAAA,IACzB;AACA,WAAO,EAAE,cAAc,CAAC;AAAA,EAC1B,CAAC;AACH;AA6BA,SAAS,qBAAqB,EAAE,MAAM,OAAO,OAAO,aAAa,MAAM,UAAU,SAAoC;AACnH,QAAM,EAAE,SAAS,SAAS,MAAA,IAAU,eAAe,IAAI;AAEvD,MAAI,CAAC,SAAS,MAAM,WAAW,GAAG;AAChC,WACE,oBAAC,OAAA,EAAI,WAAW,GAAG,kDAAkD,cAAc,MAAM,GACvF,UAAA,oBAAC,KAAA,EAAE,WAAU,+CAA8C,+BAE3D,GACF;AAAA,EAEJ;AAEA,QAAM,EAAE,QAAQ,WAAA,IAAe,QAAQ,MAAM;AAC3C,QAAI,CAAC,QAAS,QAAO,EAAE,QAAQ,MAAM,YAAY,aAAA;AAEjD,UAAM,OAAO,gBAAgB,SAAS,IAAI;AAC1C,QAAI,CAAC,MAAM;AAET,UAAI,CAAC,KAAK,MAAM,qBAAqB,GAAG;AACtC,eAAO,EAAE,QAAQ,MAAM,YAAY,wBAAA;AAAA,MACrC;AAEA,UAAI,QAAQ,OAAO,WAAW,IAAI,KAAK,QAAQ,KAAA,EAAO,WAAW,OAAO,GAAG;AACzE,eAAO,EAAE,QAAQ,MAAM,YAAY,iBAAA;AAAA,MACrC;AACA,aAAO,EAAE,QAAQ,MAAM,YAAY,WAAW,KAAK,MAAM,GAAG,EAAE,IAAA,CAAK,UAAA;AAAA,IACrE;AAEA,WAAO,EAAE,QAAQ,MAAM,YAAY,KAAA;AAAA,EACrC,GAAG,CAAC,SAAS,IAAI,CAAC;AAElB,MAAI,SAAS;AACX,+BACG,OAAA,EAAI,WAAU,uDACb,UAAA,qBAAC,OAAA,EAAI,WAAU,oDACb,UAAA;AAAA,MAAA,oBAAC,OAAA,EAAI,WAAU,+EAAA,CAA+E;AAAA,MAC9F,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,wBAAA,CAAqB;AAAA,IAAA,EAAA,CAC/D,EAAA,CACF;AAAA,EAEJ;AAEA,MAAI,OAAO;AACT,WACE,oBAAC,SAAI,WAAU,kEACb,8BAAC,KAAA,EAAE,WAAU,0CAA0C,UAAA,MAAA,CAAM,EAAA,CAC/D;AAAA,EAEJ;AAEA,MAAI,CAAC,QAAQ;AACX,WACE,oBAAC,OAAA,EAAI,WAAW,GAAG,kDAAkD,cAAc,MAAM,GACvF,UAAA,qBAAC,KAAA,EAAE,WAAU,+CACV,UAAA;AAAA,MAAA,SAAS,qBAAC,QAAA,EAAK,WAAU,sBAAsB,UAAA;AAAA,QAAA;AAAA,QAAM;AAAA,MAAA,GAAE;AAAA,MACvD,cAAc;AAAA,IAAA,EAAA,CACjB,EAAA,CACF;AAAA,EAEJ;AAEA,QAAM,mBAAmB,MAAM;AAC7B,+BACG,OAAA,EAAI,WAAU,kEACb,UAAA,qBAAC,WAAM,WAAW;AAAA,MAChB;AAAA,MACA,UAAU,gBAAgB;AAAA,IAAA,GAE1B,UAAA;AAAA,MAAA,oBAAC,WAAM,WAAU,eACf,UAAA,qBAAC,MAAA,EAAG,WAAU,0CACZ,UAAA;AAAA,QAAA,oBAAC,QAAG,WAAW;AAAA,UACb;AAAA,UACA,UAAU,cAAc;AAAA,QAAA,GACvB,UAAA,aAEH;AAAA,QACA,oBAAC,QAAG,WAAW;AAAA,UACb;AAAA,UACA,UAAU,cAAc;AAAA,QAAA,GACvB,UAAA,QAAA,CAEH;AAAA,MAAA,EAAA,CACF,EAAA,CACF;AAAA,MACA,oBAAC,WACE,UAAA,MAAM,IAAI,CAAC,EAAE,KAAK,OAAO,eAAe;AACvC,cAAM,QAAQ,YAAY,QAAQ,GAAG;AACrC,cAAM,OAAO,UAAU,SAAY,YAAY,aAAa,KAAK;AACjE,cAAM,eAAe,UAAU,SAC3B,MACA,SAAS,WACP,IAAI,YAAY,KAAK,CAAC,MACtB,YAAY,KAAK;AAEvB,eACE,qBAAC,MAAA,EAAa,WAAU,0CACtB,UAAA;AAAA,UAAA,oBAAC,QAAG,WAAW;AAAA,YACb;AAAA,YACA,UAAU,cAAc;AAAA,UAAA,GACtB,sBAAY,KAAI;AAAA,UACpB;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,UAAU,cAAc;AAAA,gBACxB,SAAS,aAAa;AAAA,cAAA;AAAA,cAGvB,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,QACH,EAAA,GAbO,GAcT;AAAA,MAEJ,CAAC,EAAA,CACH;AAAA,IAAA,EAAA,CACF,EAAA,CACF;AAAA,EAEJ;AAEA,8BACG,OAAA,EAAI,WAAW,GAAG,aAAa,cAAc,MAAM,GACjD,UAAA;AAAA,IAAA,6BACE,OAAA,EAAI,WAAU,+DAA8D,OAAO,OACjF,UAAA,OACH;AAAA,IAED,iBAAA;AAAA,EAAiB,GACpB;AAEJ;AAMO,SAAS,eAAe,EAAE,MAAM,OAAO,UAAU,SAA8B;AACpF,QAAM,EAAE,KAAK,YAAY,GAAA,IAAO,UAAA;AAEhC,MAAI,CAAC,SAAS,MAAM,WAAW,GAAG;AAChC,WACE,oBAAC,SAAI,WAAU,uDACb,8BAAC,KAAA,EAAE,WAAU,+CAA8C,UAAA,oBAAA,CAAiB,EAAA,CAC9E;AAAA,EAEJ;AAGA,QAAM,aAAa,UAChB,QAAQ,mBAAmB,EAAE,EAC7B,QAAQ,OAAO,EAAE,KACf;AAGL,MAAI,eAAe;AACnB,MAAI,6BAAM,WAAW,OAAO;AAC1B,UAAM,eAAe,KAAK,MAAM,CAAC;AACjC,mBAAe,eAAe,MAAM,eAAe,GAAG,UAAU,IAAI,YAAY;AAAA,EAClF,WAAW,QAAQ,CAAC,KAAK,WAAW,GAAG,KAAK,CAAC,KAAK,SAAS,GAAG,KAAK,CAAC,cAAc,IAAI,GAAG;AACvF,mBAAe,eAAe,MAAM,OAAO,GAAG,UAAU,IAAI,IAAI;AAAA,EAClE;AAGA,QAAM,UAAU,cAAc,YAAY;AAG1C,QAAM,UAAU,QAAQ,MAAM;AAC5B,QAAI,CAAC,QAAS,QAAO;AAErB,UAAM,aAAa,aAAa,MAAM,UAAU,CAAC,EAAE,CAAC;AAEpD,UAAM,YAAY,WAAW,YAAY,GAAG;AAC5C,QAAI,cAAc,GAAI,QAAO;AAC7B,WAAO,WAAW,MAAM,GAAG,SAAS,KAAK;AAAA,EAC3C,GAAG,CAAC,SAAS,YAAY,CAAC;AAE1B,QAAM,EAAE,UAAA,IAAc,aAAa,WAAW,GAAG;AAGjD,QAAM,gBAAgB,QAAQ,MAAM;AAClC,QAAI,CAAC,WAAW,CAAC,kBAAkB,CAAA;AAEnC,UAAM,WAAW,sBAAsB,SAAS;AAChD,UAAM,QAAQ,SACX,IAAI,CAAA,MAAK,EAAE,IAAI,EACf,OAAO,CAAA,MAAK,UAAU,GAAG,cAAc,EAAE,WAAW,KAAA,CAAM,CAAC;AAE9D,yBAAqB,KAAK;AAE1B,WAAO;AAAA,EACT,GAAG,CAAC,SAAS,WAAW,cAAc,MAAM,OAAO,CAAC;AAGpD,MAAI,CAAC,SAAS;AACZ,WAAO,oBAAC,sBAAA,EAAqB,MAAM,cAAc,OAAc,SAAkB;AAAA,EACnF;AAGA,MAAI,CAAC,WAAW;AACd,+BACG,OAAA,EAAI,WAAU,uDACb,UAAA,qBAAC,OAAA,EAAI,WAAU,oDACb,UAAA;AAAA,MAAA,oBAAC,OAAA,EAAI,WAAU,+EAAA,CAA+E;AAAA,MAC9F,oBAAC,QAAA,EAAK,WAAU,yBAAwB,UAAA,wBAAA,CAAqB;AAAA,IAAA,EAAA,CAC/D,EAAA,CACF;AAAA,EAEJ;AAGA,MAAI,cAAc,WAAW,GAAG;AAC9B,+BACG,OAAA,EAAI,WAAU,uDACb,UAAA,qBAAC,KAAA,EAAE,WAAU,+CAA8C,UAAA;AAAA,MAAA;AAAA,MACrC;AAAA,0BACnB,MAAA,EAAG;AAAA,MACJ,qBAAC,QAAA,EAAK,WAAU,4BAA2B,UAAA;AAAA,QAAA;AAAA,QAAY;AAAA,QAAQ;AAAA,QAAa;AAAA,QAAK;AAAA,MAAA,EAAA,CAAC;AAAA,IAAA,EAAA,CACpF,EAAA,CACF;AAAA,EAEJ;AAEA,QAAM,YAAY,OAAuB,IAAI;AAC7C,4BAA0B,SAAS;AAGnC,QAAM,QAAQ,cAAc;AAC5B,QAAM,gBAAgB,SAAS,IAC3B,kCACA,SAAS,IACP,oCACA;AAEN,6BACG,OAAA,EAAI,WAAW,QAAQ,aAAa,IACnC,8BAAC,OAAA,EAAI,KAAK,WAAW,WAAU,wDAC5B,wBAAc,IAAI,CAAC,aAClB,oBAAC,OAAA,EAAmB,WAAU,uBAC5B,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAM;AAAA,MACN;AAAA,MACA,OAAO,SAAS,MAAM,GAAG,EAAE,SAAS;AAAA,MACpC,YAAY;AAAA,MACZ;AAAA,IAAA;AAAA,EAAA,EACF,GAPQ,QAQV,CACD,EAAA,CACH,GACF;AAEJ;"}
1
+ {"version":3,"file":"parameter-table.js","sources":["../../../src/components/parameter-table.tsx"],"sourcesContent":["import { useFileContent, useDirectory } from \"../../plugin/src/client\";\nimport { useMemo, useRef, useEffect } from \"react\";\nimport { useParams } from \"react-router-dom\";\nimport { cn } from \"@/lib/utils\";\nimport { minimatch } from \"minimatch\";\nimport {\n type ParameterValue,\n extractPath,\n getValueType,\n formatValue,\n parseConfigFile,\n} from \"@/lib/parameter-utils\";\nimport { FileEntry, DirectoryEntry } from \"../../plugin/src/lib\";\n\n/**\n * Hook to prevent horizontal scroll from triggering browser back/forward gestures.\n */\nfunction usePreventSwipeNavigation(ref: React.RefObject<HTMLElement | null>) {\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n\n const handleWheel = (e: WheelEvent) => {\n if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;\n\n const { scrollLeft, scrollWidth, clientWidth } = el;\n const atLeftEdge = scrollLeft <= 0;\n const atRightEdge = scrollLeft + clientWidth >= scrollWidth - 1;\n\n if ((atLeftEdge && e.deltaX < 0) || (atRightEdge && e.deltaX > 0)) {\n e.preventDefault();\n }\n };\n\n el.addEventListener('wheel', handleWheel, { passive: false });\n return () => el.removeEventListener('wheel', handleWheel);\n }, [ref]);\n}\n\n// Check if a path contains glob patterns\nfunction isGlobPattern(path: string): boolean {\n return path.includes('*') || path.includes('?') || path.includes('[');\n}\n\n// Recursively collect all config files from a directory tree\nfunction collectAllConfigFiles(entry: DirectoryEntry | FileEntry): FileEntry[] {\n if (entry.type === \"file\") {\n if (entry.name.match(/\\.(yaml|yml|json)$/i)) {\n return [entry];\n }\n return [];\n }\n const files: FileEntry[] = [];\n for (const child of entry.children || []) {\n files.push(...collectAllConfigFiles(child));\n }\n return files;\n}\n\n// Sort paths numerically\nfunction sortPathsNumerically(paths: string[]): void {\n paths.sort((a, b) => {\n const nums = (s: string) => (s.match(/\\d+/g) || []).map(Number);\n const na = nums(a);\n const nb = nums(b);\n const len = Math.max(na.length, nb.length);\n for (let i = 0; i < len; i++) {\n const diff = (na[i] ?? 0) - (nb[i] ?? 0);\n if (diff !== 0) return diff;\n }\n return a.localeCompare(b);\n });\n}\n\ninterface SingleParameterTableProps {\n /** Path to the YAML or JSON file */\n path: string;\n /**\n * Required array of key/label pairs to render as a markdown-style table.\n * Each key is a jq-like path (e.g., \".base.dt\").\n */\n pairs: Array<{ key: string; label?: string }>;\n /** Optional label to show above the table */\n label?: string;\n /** Whether to include vertical margin (default true) */\n withMargin?: boolean;\n /**\n * Whether this table should manage its own horizontal scrolling.\n * Set false when the parent already provides a single shared scrollbar.\n */\n scrollable?: boolean;\n /** Compact mode for dense rendering */\n compact?: boolean;\n}\n\ninterface ParameterTableProps {\n /** Path to the YAML or JSON file, supports glob patterns like \"*.yaml\" */\n path: string;\n /**\n * Required array of key/label pairs to render Complete table.\n */\n pairs: Array<{ key: string; label?: string }>;\n /** Compact mode for dense rendering */\n compact?: boolean;\n}\n\nfunction SingleParameterTable({\n path,\n pairs,\n label,\n withMargin = true,\n scrollable = true,\n compact = false,\n}: SingleParameterTableProps) {\n const { content, loading, error } = useFileContent(path);\n\n if (!pairs || pairs.length === 0) {\n return (\n <div className={cn(\"p-3 rounded border border-border/50 bg-card/30\", withMargin && \"my-6\")}>\n <p className=\"text-[11px] font-mono text-muted-foreground\">\n pairs is required\n </p>\n </div>\n );\n }\n\n const { parsed, parseError } = useMemo(() => {\n if (!content) return { parsed: null, parseError: 'no content' };\n\n const data = parseConfigFile(content, path);\n if (!data) {\n // Check why parsing failed\n if (!path.match(/\\.(yaml|yml|json)$/i)) {\n return { parsed: null, parseError: `unsupported file type` };\n }\n // Check if content looks like HTML (404 page)\n if (content.trim().startsWith('<!') || content.trim().startsWith('<html')) {\n return { parsed: null, parseError: `file not found` };\n }\n return { parsed: null, parseError: `invalid ${path.split('.').pop()} syntax` };\n }\n\n return { parsed: data, parseError: null };\n }, [content, path]);\n\n if (loading) {\n return (\n <div className=\"my-6 p-4 rounded border border-border/50 bg-card/30\">\n <div className=\"flex items-center gap-2 text-muted-foreground/60\">\n <div className=\"w-3 h-3 border border-current border-t-transparent rounded-full animate-spin\" />\n <span className=\"text-[11px] font-mono\">loading parameters...</span>\n </div>\n </div>\n );\n }\n\n if (error) {\n return (\n <div className=\"my-6 p-3 rounded border border-destructive/30 bg-destructive/5\">\n <p className=\"text-[11px] font-mono text-destructive\">{error}</p>\n </div>\n );\n }\n\n if (!parsed) {\n return (\n <div className={cn(\"p-3 rounded border border-border/50 bg-card/30\", withMargin && \"my-6\")}>\n <p className=\"text-[11px] font-mono text-muted-foreground\">\n {label && <span className=\"text-foreground/60\">{label}: </span>}\n {parseError || 'unable to parse'}\n </p>\n </div>\n );\n }\n\n const renderPairsTable = () => {\n return (\n <div className={cn(\n \"border border-border rounded-md\",\n scrollable ? \"overflow-x-auto\" : \"overflow-x-hidden\"\n )}>\n <table className={cn(\n \"w-full border-collapse\",\n compact ? \"text-[11px]\" : \"text-sm\"\n )}>\n <thead className=\"bg-muted/50\">\n <tr className=\"border-b border-border last:border-b-0\">\n <th className={cn(\n \"text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\",\n compact ? \"px-3 py-2\" : \"px-4 py-3\"\n )}>\n Parameter\n </th>\n <th className={cn(\n \"text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\",\n compact ? \"px-3 py-2\" : \"px-4 py-3\"\n )}>\n Value\n </th>\n </tr>\n </thead>\n <tbody>\n {pairs.map(({ key, label: rowLabel }) => {\n const value = extractPath(parsed, key);\n const type = value === undefined ? \"missing\" : getValueType(value);\n const displayValue = value === undefined\n ? \"—\"\n : type === \"string\"\n ? `\"${formatValue(value)}\"`\n : formatValue(value);\n\n return (\n <tr key={key} className=\"border-b border-border last:border-b-0\">\n <td className={cn(\n \"align-top\",\n compact ? \"px-3 py-2\" : \"px-4 py-3\"\n )}>{rowLabel || key}</td>\n <td\n className={cn(\n \"align-top\",\n compact ? \"px-3 py-2\" : \"px-4 py-3\",\n type === \"missing\" && \"text-muted-foreground\"\n )}\n >\n {displayValue}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n );\n };\n\n return (\n <div className={cn(\"not-prose\", withMargin && \"my-6\")}>\n {label && (\n <div className=\"text-[11px] font-mono text-muted-foreground mb-1.5 truncate\" title={label}>\n {label}\n </div>\n )}\n {renderPairsTable()}\n </div>\n );\n}\n\n/**\n * ParameterTable component that displays YAML/JSON config files.\n * Supports glob patterns in the path prop to show multiple files.\n */\nexport function ParameterTable({ path, pairs, compact = false }: ParameterTableProps) {\n const { \"*\": routePath = \"\" } = useParams();\n\n if (!pairs || pairs.length === 0) {\n return (\n <div className=\"my-6 p-3 rounded border border-border/50 bg-card/30\">\n <p className=\"text-[11px] font-mono text-muted-foreground\">pairs is required</p>\n </div>\n );\n }\n\n // Get current directory from route\n const currentDir = routePath\n .replace(/\\/?[^/]+\\.mdx$/i, \"\")\n .replace(/\\/$/, \"\")\n || \".\";\n\n // Resolve relative paths\n let resolvedPath = path;\n if (path?.startsWith(\"./\")) {\n const relativePart = path.slice(2);\n resolvedPath = currentDir === \".\" ? relativePart : `${currentDir}/${relativePart}`;\n } else if (path && !path.startsWith(\"/\") && !path.includes(\"/\") && !isGlobPattern(path)) {\n resolvedPath = currentDir === \".\" ? path : `${currentDir}/${path}`;\n }\n\n // Check if this is a glob pattern\n const hasGlob = isGlobPattern(resolvedPath);\n\n // For glob patterns, get the base directory (directory containing the glob pattern)\n const baseDir = useMemo(() => {\n if (!hasGlob) return null;\n // Get everything before the first glob character\n const beforeGlob = resolvedPath.split(/[*?\\[]/, 1)[0];\n // Extract directory portion (everything up to the last slash)\n const lastSlash = beforeGlob.lastIndexOf('/');\n if (lastSlash === -1) return \".\";\n return beforeGlob.slice(0, lastSlash) || \".\";\n }, [hasGlob, resolvedPath]);\n\n const { directory } = useDirectory(baseDir || \".\");\n\n // Find matching files for glob patterns\n const matchingPaths = useMemo(() => {\n if (!hasGlob || !directory) return [];\n\n const allFiles = collectAllConfigFiles(directory);\n const paths = allFiles\n .map(f => f.path)\n .filter(p => minimatch(p, resolvedPath, { matchBase: true }));\n\n sortPathsNumerically(paths);\n\n return paths;\n }, [hasGlob, directory, resolvedPath, path, baseDir]);\n\n // If not a glob pattern, just render the single table\n if (!hasGlob) {\n return <SingleParameterTable path={resolvedPath} pairs={pairs} compact={compact} />;\n }\n\n // Loading state for glob patterns\n if (!directory) {\n return (\n <div className=\"my-6 p-4 rounded border border-border/50 bg-card/30\">\n <div className=\"flex items-center gap-2 text-muted-foreground/60\">\n <div className=\"w-3 h-3 border border-current border-t-transparent rounded-full animate-spin\" />\n <span className=\"text-[11px] font-mono\">loading parameters...</span>\n </div>\n </div>\n );\n }\n\n // No matches\n if (matchingPaths.length === 0) {\n return (\n <div className=\"my-6 p-3 rounded border border-border/50 bg-card/30\">\n <p className=\"text-[11px] font-mono text-muted-foreground\">\n no files matching: {resolvedPath}\n <br />\n <span className=\"text-muted-foreground/50\">(base dir: {baseDir}, original: {path})</span>\n </p>\n </div>\n );\n }\n\n const scrollRef = useRef<HTMLDivElement>(null);\n usePreventSwipeNavigation(scrollRef);\n\n // Breakout width based on count\n const count = matchingPaths.length;\n // Use translate centering (instead of negative margins) to avoid creating a tiny\n // page-level horizontal scrollbar while still \"breaking out\" of prose width.\n const breakoutClass = count >= 4\n ? 'relative left-1/2 w-[96vw] -translate-x-1/2'\n : count >= 2\n ? 'relative left-1/2 w-[75vw] -translate-x-1/2'\n : '';\n\n return (\n <div className={`my-6 ${breakoutClass} overflow-x-hidden`}>\n <div ref={scrollRef} className=\"flex gap-4 overflow-x-auto overscroll-x-contain pb-2\">\n {matchingPaths.map((filePath) => (\n <div key={filePath} className=\"flex-none w-[280px]\">\n <SingleParameterTable\n path={filePath}\n pairs={pairs}\n label={filePath.split('/').pop() || filePath}\n withMargin={false}\n scrollable={false}\n compact={compact}\n />\n </div>\n ))}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -9,13 +9,18 @@ import veslxConfig from "virtual:veslx-config";
9
9
  function formatName(name) {
10
10
  return name.replace(/^\d+-/, "").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
11
11
  }
12
+ function filePathToRoutePath(filePath) {
13
+ const normalized = filePath.replace(/^\/+/, "");
14
+ const stripped = normalized.replace(/(?:^|\/)(?:index|readme)\.mdx?$/i, "");
15
+ return stripped ? `/${stripped}` : "/";
16
+ }
12
17
  function getLinkPath(post) {
13
18
  if (post.file) {
14
- return `/${post.file.path}`;
19
+ return filePathToRoutePath(post.file.path);
15
20
  } else if (post.slides && !post.readme) {
16
21
  return `/${post.slides.path}`;
17
22
  } else if (post.readme) {
18
- return `/${post.readme.path}`;
23
+ return filePathToRoutePath(post.readme.path);
19
24
  } else {
20
25
  return `/${post.path}`;
21
26
  }
@@ -1 +1 @@
1
- {"version":3,"file":"post-list.js","sources":["../../../src/components/post-list.tsx"],"sourcesContent":["import { useParams } from \"react-router-dom\";\nimport { minimatch } from \"minimatch\";\nimport {\n type PostEntry,\n directoryToPostEntries,\n filterVisiblePosts,\n getFrontmatter,\n} from \"@/lib/content-classification\";\nimport { useDirectory } from \"../../plugin/src/client\";\nimport { ErrorDisplay } from \"./page-error\";\nimport Loading from \"./loading\";\nimport { PostListItem } from \"./post-list-item\";\nimport veslxConfig from \"virtual:veslx-config\";\n\ninterface PostListProps {\n /** Glob patterns to filter posts by name (e.g., [\"01-*\", \"getting-*\"]) */\n globs?: string[] | null;\n}\n\n// Helper to format name for display (e.g., \"01-getting-started\" → \"Getting Started\")\nfunction formatName(name: string): string {\n return name\n .replace(/^\\d+-/, '')\n .replace(/-/g, ' ')\n .replace(/\\b\\w/g, c => c.toUpperCase());\n}\n\n// Helper to get link path from post\nfunction getLinkPath(post: PostEntry): string {\n if (post.file) {\n // Standalone MDX file\n return `/${post.file.path}`;\n } else if (post.slides && !post.readme) {\n // Folder with only slides\n return `/${post.slides.path}`;\n } else if (post.readme) {\n // Folder with readme\n return `/${post.readme.path}`;\n } else {\n // Fallback to folder path\n return `/${post.path}`;\n }\n}\n\nfunction isRouterPath(href: string): boolean {\n return href.startsWith(\"/\") && !href.startsWith(\"//\");\n}\n\nexport function PostList({ globs = null }: PostListProps) {\n const { \"*\": path = \".\" } = useParams();\n\n const { directory, loading, error } = useDirectory(path)\n\n if (error) {\n return <ErrorDisplay error={error} path={path} />;\n }\n\n if (loading) {\n return (\n <Loading />\n )\n }\n\n if (!directory) {\n return (\n <div className=\"py-24 text-center\">\n <p className=\"text-muted-foreground font-mono text-sm tracking-wide\">no entries</p>\n </div>\n );\n }\n\n let posts = directoryToPostEntries(directory);\n\n if (posts.length === 0) {\n return (\n <div className=\"py-24 text-center\">\n <p className=\"text-muted-foreground font-mono text-sm tracking-wide\">no entries</p>\n </div>\n );\n }\n\n // Filter out hidden and draft posts\n posts = filterVisiblePosts(posts);\n\n // Filter by glob patterns if provided\n if (globs && globs.length > 0) {\n posts = posts.filter(post =>\n globs.some(pattern => minimatch(post.name, pattern, { matchBase: true }))\n );\n }\n\n if (posts.length === 0) {\n return (\n <div className=\"py-24 text-center\">\n <p className=\"text-muted-foreground font-mono text-sm tracking-wide\">no entries</p>\n </div>\n );\n }\n\n // Sort based on config\n const sortMode = veslxConfig.posts?.sort ?? 'alpha';\n if (sortMode === 'date' || sortMode === 'date-asc') {\n const ascending = sortMode === 'date-asc';\n // Sort by date, posts without dates go to the top\n posts = posts.sort((a, b) => {\n const dateA = getFrontmatter(a)?.date;\n const dateB = getFrontmatter(b)?.date;\n if (!dateA && !dateB) return a.name.localeCompare(b.name);\n if (!dateA) return -1;\n if (!dateB) return 1;\n const diff = new Date(dateA as string).getTime() - new Date(dateB as string).getTime();\n return ascending ? diff : -diff;\n });\n } else {\n // Alphanumeric sorting by name\n posts = posts.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n return (\n <div className=\"space-y-1 not-prose\">\n {posts.map((post) => {\n const frontmatter = getFrontmatter(post);\n const title = (frontmatter?.title as string) || formatName(post.name);\n const description = frontmatter?.description as string | undefined;\n const date = frontmatter?.date ? new Date(frontmatter.date as string) : undefined;\n const internalLinkPath = getLinkPath(post);\n const normalizedLink = internalLinkPath.toLowerCase();\n const isSlides =\n normalizedLink.endsWith('/slides.mdx') ||\n normalizedLink.endsWith('/slides.md') ||\n normalizedLink.endsWith('.slides.mdx') ||\n normalizedLink.endsWith('.slides.md');\n\n const frontmatterLink =\n typeof frontmatter?.link === \"string\" ? frontmatter.link.trim() : \"\";\n const href = frontmatterLink || internalLinkPath;\n const external = !isRouterPath(href);\n\n return (\n <PostListItem\n key={post.path}\n title={title}\n description={description}\n date={date}\n href={href}\n external={external}\n isSlides={isSlides}\n />\n );\n })}\n </div>\n );\n}\n"],"names":["_a"],"mappings":"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"}
1
+ {"version":3,"file":"post-list.js","sources":["../../../src/components/post-list.tsx"],"sourcesContent":["import { useParams } from \"react-router-dom\";\nimport { minimatch } from \"minimatch\";\nimport {\n type PostEntry,\n directoryToPostEntries,\n filterVisiblePosts,\n getFrontmatter,\n} from \"@/lib/content-classification\";\nimport { useDirectory } from \"../../plugin/src/client\";\nimport { ErrorDisplay } from \"./page-error\";\nimport Loading from \"./loading\";\nimport { PostListItem } from \"./post-list-item\";\nimport veslxConfig from \"virtual:veslx-config\";\n\ninterface PostListProps {\n /** Glob patterns to filter posts by name (e.g., [\"01-*\", \"getting-*\"]) */\n globs?: string[] | null;\n}\n\n// Helper to format name for display (e.g., \"01-getting-started\" → \"Getting Started\")\nfunction formatName(name: string): string {\n return name\n .replace(/^\\d+-/, '')\n .replace(/-/g, ' ')\n .replace(/\\b\\w/g, c => c.toUpperCase());\n}\n\nfunction filePathToRoutePath(filePath: string): string {\n // Routes without an extension are treated as directories; ContentRouter will\n // render index/readme files automatically via IndexPost.\n const normalized = filePath.replace(/^\\/+/, '');\n const stripped = normalized.replace(/(?:^|\\/)(?:index|readme)\\.mdx?$/i, '');\n return stripped ? `/${stripped}` : '/';\n}\n\n// Helper to get link path from post\nfunction getLinkPath(post: PostEntry): string {\n if (post.file) {\n // Standalone MDX file\n return filePathToRoutePath(post.file.path);\n } else if (post.slides && !post.readme) {\n // Folder with only slides\n return `/${post.slides.path}`;\n } else if (post.readme) {\n // Folder with readme\n return filePathToRoutePath(post.readme.path);\n } else {\n // Fallback to folder path\n return `/${post.path}`;\n }\n}\n\nfunction isRouterPath(href: string): boolean {\n return href.startsWith(\"/\") && !href.startsWith(\"//\");\n}\n\nexport function PostList({ globs = null }: PostListProps) {\n const { \"*\": path = \".\" } = useParams();\n\n const { directory, loading, error } = useDirectory(path)\n\n if (error) {\n return <ErrorDisplay error={error} path={path} />;\n }\n\n if (loading) {\n return (\n <Loading />\n )\n }\n\n if (!directory) {\n return (\n <div className=\"py-24 text-center\">\n <p className=\"text-muted-foreground font-mono text-sm tracking-wide\">no entries</p>\n </div>\n );\n }\n\n let posts = directoryToPostEntries(directory);\n\n if (posts.length === 0) {\n return (\n <div className=\"py-24 text-center\">\n <p className=\"text-muted-foreground font-mono text-sm tracking-wide\">no entries</p>\n </div>\n );\n }\n\n // Filter out hidden and draft posts\n posts = filterVisiblePosts(posts);\n\n // Filter by glob patterns if provided\n if (globs && globs.length > 0) {\n posts = posts.filter(post =>\n globs.some(pattern => minimatch(post.name, pattern, { matchBase: true }))\n );\n }\n\n if (posts.length === 0) {\n return (\n <div className=\"py-24 text-center\">\n <p className=\"text-muted-foreground font-mono text-sm tracking-wide\">no entries</p>\n </div>\n );\n }\n\n // Sort based on config\n const sortMode = veslxConfig.posts?.sort ?? 'alpha';\n if (sortMode === 'date' || sortMode === 'date-asc') {\n const ascending = sortMode === 'date-asc';\n // Sort by date, posts without dates go to the top\n posts = posts.sort((a, b) => {\n const dateA = getFrontmatter(a)?.date;\n const dateB = getFrontmatter(b)?.date;\n if (!dateA && !dateB) return a.name.localeCompare(b.name);\n if (!dateA) return -1;\n if (!dateB) return 1;\n const diff = new Date(dateA as string).getTime() - new Date(dateB as string).getTime();\n return ascending ? diff : -diff;\n });\n } else {\n // Alphanumeric sorting by name\n posts = posts.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n return (\n <div className=\"space-y-1 not-prose\">\n {posts.map((post) => {\n const frontmatter = getFrontmatter(post);\n const title = (frontmatter?.title as string) || formatName(post.name);\n const description = frontmatter?.description as string | undefined;\n const date = frontmatter?.date ? new Date(frontmatter.date as string) : undefined;\n const internalLinkPath = getLinkPath(post);\n const normalizedLink = internalLinkPath.toLowerCase();\n const isSlides =\n normalizedLink.endsWith('/slides.mdx') ||\n normalizedLink.endsWith('/slides.md') ||\n normalizedLink.endsWith('.slides.mdx') ||\n normalizedLink.endsWith('.slides.md');\n\n const frontmatterLink =\n typeof frontmatter?.link === \"string\" ? frontmatter.link.trim() : \"\";\n const href = frontmatterLink || internalLinkPath;\n const external = !isRouterPath(href);\n\n return (\n <PostListItem\n key={post.path}\n title={title}\n description={description}\n date={date}\n href={href}\n external={external}\n isSlides={isSlides}\n />\n );\n })}\n </div>\n );\n}\n"],"names":["_a"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veslx",
3
- "version": "0.1.63",
3
+ "version": "0.1.65",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -124,7 +124,7 @@ export default function Gallery({
124
124
  {[...Array(3)].map((_, i) => (
125
125
  <div
126
126
  key={i}
127
- className="aspect-square rounded-sm bg-muted/20 relative overflow-hidden"
127
+ className="h-40 rounded-sm bg-muted/20 relative overflow-hidden"
128
128
  >
129
129
  <div
130
130
  className="absolute inset-0 bg-gradient-to-r from-transparent via-muted/30 to-transparent animate-shimmer"
@@ -168,13 +168,13 @@ export default function Gallery({
168
168
  <div
169
169
  key={index}
170
170
  title={img.label}
171
- className={`aspect-square overflow-hidden rounded-sm bg-muted/10 cursor-pointer group ${className || ''}`}
171
+ className={`overflow-hidden rounded-sm bg-muted/10 cursor-pointer group ${className || ''}`}
172
172
  onClick={() => lightbox.open(index)}
173
173
  >
174
174
  <LoadingImage
175
175
  src={img.src}
176
176
  alt={img.label}
177
- className="object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
177
+ className="w-full h-auto object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
178
178
  />
179
179
  </div>
180
180
  );
@@ -212,11 +212,6 @@ export default function Gallery({
212
212
  const offset = rowsToRender.slice(0, rowIndex).reduce((acc, row) => acc + row.length, 0);
213
213
  const rowSubtitle = subtitles?.[rowIndex];
214
214
  const rowWrapperClass = "max-w-[var(--gallery-width)] w-full mx-auto";
215
- const placeholders = Math.max(0, maxRowColumns - rowImages.length);
216
- const rowCells = [
217
- ...rowImages.map((img, index) => ({ img, index })),
218
- ...Array.from({ length: placeholders }, () => ({ img: null, index: -1 })),
219
- ];
220
215
 
221
216
  return (
222
217
  <div key={`${rowIndex}-${rowPaths.join("|")}`}>
@@ -225,17 +220,7 @@ export default function Gallery({
225
220
  className="grid gap-3"
226
221
  style={{ gridTemplateColumns: `repeat(${Math.max(1, maxRowColumns)}, minmax(0, 1fr))` }}
227
222
  >
228
- {rowCells.map((cell, index) => {
229
- if (!cell.img) {
230
- return (
231
- <div
232
- key={`empty-${rowIndex}-${index}`}
233
- className="aspect-square rounded-sm opacity-0 pointer-events-none"
234
- />
235
- );
236
- }
237
- return imageElement(offset + cell.index, cell.img, 'w-full');
238
- })}
223
+ {rowImages.map((img, index) => imageElement(offset + index, img, 'w-full'))}
239
224
  </div>
240
225
  </div>
241
226
 
@@ -255,22 +240,22 @@ export default function Gallery({
255
240
  <FigureCaption caption={caption} label={captionLabel} />
256
241
  </div>
257
242
  ) : isCompact ? (
258
- <div className="flex gap-3">
243
+ <div className="flex items-start gap-3">
259
244
  {images.map((img, index) => imageElement(index, img, 'flex-1'))}
260
245
  </div>
261
246
  ) : (
262
- <div ref={scrollRef} className="gallery-scroll-row flex gap-3 overflow-x-auto overscroll-x-contain pb-4">
247
+ <div ref={scrollRef} className="gallery-scroll-row flex items-start gap-3 overflow-x-auto overscroll-x-contain pb-4">
263
248
  {images.map((img, index) => (
264
249
  <div
265
250
  key={index}
266
251
  title={img.label}
267
- className="flex-none w-[calc(var(--gallery-width)*0.3)] min-w-[250px] aspect-square overflow-hidden rounded-sm bg-muted/10 cursor-pointer group"
252
+ className="flex-none w-[calc(var(--gallery-width)*0.3)] min-w-[250px] overflow-hidden rounded-sm bg-muted/10 cursor-pointer group"
268
253
  onClick={() => lightbox.open(index)}
269
254
  >
270
255
  <LoadingImage
271
256
  src={img.src}
272
257
  alt={img.label}
273
- className="object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
258
+ className="w-full h-auto object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
274
259
  />
275
260
  </div>
276
261
  ))}
@@ -84,6 +84,11 @@ interface SingleParameterTableProps {
84
84
  label?: string;
85
85
  /** Whether to include vertical margin (default true) */
86
86
  withMargin?: boolean;
87
+ /**
88
+ * Whether this table should manage its own horizontal scrolling.
89
+ * Set false when the parent already provides a single shared scrollbar.
90
+ */
91
+ scrollable?: boolean;
87
92
  /** Compact mode for dense rendering */
88
93
  compact?: boolean;
89
94
  }
@@ -99,7 +104,14 @@ interface ParameterTableProps {
99
104
  compact?: boolean;
100
105
  }
101
106
 
102
- function SingleParameterTable({ path, pairs, label, withMargin = true, compact = false }: SingleParameterTableProps) {
107
+ function SingleParameterTable({
108
+ path,
109
+ pairs,
110
+ label,
111
+ withMargin = true,
112
+ scrollable = true,
113
+ compact = false,
114
+ }: SingleParameterTableProps) {
103
115
  const { content, loading, error } = useFileContent(path);
104
116
 
105
117
  if (!pairs || pairs.length === 0) {
@@ -163,7 +175,10 @@ function SingleParameterTable({ path, pairs, label, withMargin = true, compact =
163
175
 
164
176
  const renderPairsTable = () => {
165
177
  return (
166
- <div className="not-prose my-6 overflow-x-auto border border-border rounded-md">
178
+ <div className={cn(
179
+ "border border-border rounded-md",
180
+ scrollable ? "overflow-x-auto" : "overflow-x-hidden"
181
+ )}>
167
182
  <table className={cn(
168
183
  "w-full border-collapse",
169
184
  compact ? "text-[11px]" : "text-sm"
@@ -325,14 +340,16 @@ export function ParameterTable({ path, pairs, compact = false }: ParameterTableP
325
340
 
326
341
  // Breakout width based on count
327
342
  const count = matchingPaths.length;
343
+ // Use translate centering (instead of negative margins) to avoid creating a tiny
344
+ // page-level horizontal scrollbar while still "breaking out" of prose width.
328
345
  const breakoutClass = count >= 4
329
- ? 'w-[96vw] ml-[calc(-48vw+50%)]'
346
+ ? 'relative left-1/2 w-[96vw] -translate-x-1/2'
330
347
  : count >= 2
331
- ? 'w-[75vw] ml-[calc(-37.5vw+50%)]'
348
+ ? 'relative left-1/2 w-[75vw] -translate-x-1/2'
332
349
  : '';
333
350
 
334
351
  return (
335
- <div className={`my-6 ${breakoutClass}`}>
352
+ <div className={`my-6 ${breakoutClass} overflow-x-hidden`}>
336
353
  <div ref={scrollRef} className="flex gap-4 overflow-x-auto overscroll-x-contain pb-2">
337
354
  {matchingPaths.map((filePath) => (
338
355
  <div key={filePath} className="flex-none w-[280px]">
@@ -341,6 +358,7 @@ export function ParameterTable({ path, pairs, compact = false }: ParameterTableP
341
358
  pairs={pairs}
342
359
  label={filePath.split('/').pop() || filePath}
343
360
  withMargin={false}
361
+ scrollable={false}
344
362
  compact={compact}
345
363
  />
346
364
  </div>
@@ -25,17 +25,25 @@ function formatName(name: string): string {
25
25
  .replace(/\b\w/g, c => c.toUpperCase());
26
26
  }
27
27
 
28
+ function filePathToRoutePath(filePath: string): string {
29
+ // Routes without an extension are treated as directories; ContentRouter will
30
+ // render index/readme files automatically via IndexPost.
31
+ const normalized = filePath.replace(/^\/+/, '');
32
+ const stripped = normalized.replace(/(?:^|\/)(?:index|readme)\.mdx?$/i, '');
33
+ return stripped ? `/${stripped}` : '/';
34
+ }
35
+
28
36
  // Helper to get link path from post
29
37
  function getLinkPath(post: PostEntry): string {
30
38
  if (post.file) {
31
39
  // Standalone MDX file
32
- return `/${post.file.path}`;
40
+ return filePathToRoutePath(post.file.path);
33
41
  } else if (post.slides && !post.readme) {
34
42
  // Folder with only slides
35
43
  return `/${post.slides.path}`;
36
44
  } else if (post.readme) {
37
45
  // Folder with readme
38
- return `/${post.readme.path}`;
46
+ return filePathToRoutePath(post.readme.path);
39
47
  } else {
40
48
  // Fallback to folder path
41
49
  return `/${post.path}`;