veslx 0.1.34 → 0.1.36

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
@@ -102,7 +102,15 @@ export default async function serve(dir?: string) {
102
102
  ],
103
103
  })
104
104
 
105
- await server.listen()
105
+ try {
106
+ await server.listen()
107
+ } catch (err: any) {
108
+ if (err?.code === 'EADDRINUSE') {
109
+ log.error(`port already in use`)
110
+ process.exit(1)
111
+ }
112
+ throw err
113
+ }
106
114
 
107
115
  const info = server.resolvedUrls
108
116
  if (info?.local[0]) {
package/bin/lib/start.ts CHANGED
@@ -18,6 +18,7 @@ export default async function start(dir?: string) {
18
18
  pm2.connect((err) => {
19
19
  if (err) {
20
20
  log.error('pm2 connection failed');
21
+ process.exit(1);
21
22
  return;
22
23
  }
23
24
 
@@ -34,10 +35,12 @@ export default async function start(dir?: string) {
34
35
 
35
36
  if (err) {
36
37
  log.error('daemon failed to start');
38
+ process.exit(1);
37
39
  return;
38
40
  }
39
41
 
40
42
  log.success('daemon started');
43
+ process.exit(0);
41
44
  });
42
45
  })
43
46
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
- import { useMemo } from "react";
2
+ import { useRef, useMemo, useEffect } from "react";
3
3
  import { Image } from "lucide-react";
4
4
  import { Lightbox } from "./components/lightbox.js";
5
5
  import { useGalleryImages } from "./hooks/use-gallery-images.js";
@@ -7,6 +7,23 @@ import { useLightbox } from "./hooks/use-lightbox.js";
7
7
  import { LoadingImage } from "./components/loading-image.js";
8
8
  import { FigureHeader } from "./components/figure-header.js";
9
9
  import { FigureCaption } from "./components/figure-caption.js";
10
+ function usePreventSwipeNavigation(ref) {
11
+ useEffect(() => {
12
+ const el = ref.current;
13
+ if (!el) return;
14
+ const handleWheel = (e) => {
15
+ if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;
16
+ const { scrollLeft, scrollWidth, clientWidth } = el;
17
+ const atLeftEdge = scrollLeft <= 0;
18
+ const atRightEdge = scrollLeft + clientWidth >= scrollWidth - 1;
19
+ if (atLeftEdge && e.deltaX < 0 || atRightEdge && e.deltaX > 0) {
20
+ e.preventDefault();
21
+ }
22
+ };
23
+ el.addEventListener("wheel", handleWheel, { passive: false });
24
+ return () => el.removeEventListener("wheel", handleWheel);
25
+ }, [ref]);
26
+ }
10
27
  function getImageLabel(path) {
11
28
  const filename = path.split("/").pop() || path;
12
29
  return filename.replace(/\.(png|jpg|jpeg|gif|svg|webp)$/i, "").replace(/[_-]/g, " ").replace(/\s+/g, " ").trim();
@@ -33,6 +50,8 @@ function Gallery({
33
50
  page
34
51
  });
35
52
  const lightbox = useLightbox(paths.length);
53
+ const scrollRef = useRef(null);
54
+ usePreventSwipeNavigation(scrollRef);
36
55
  const images = useMemo(
37
56
  () => paths.map((p) => ({ src: getImageUrl(p), label: getImageLabel(p) })),
38
57
  [paths]
@@ -67,6 +86,7 @@ function Gallery({
67
86
  const imageElement = (index, img, className) => /* @__PURE__ */ jsx(
68
87
  "div",
69
88
  {
89
+ title: img.label,
70
90
  className: `aspect-square overflow-hidden rounded-sm bg-muted/10 cursor-pointer group ${className || ""}`,
71
91
  onClick: () => lightbox.open(index),
72
92
  children: /* @__PURE__ */ jsx(
@@ -97,9 +117,10 @@ function Gallery({
97
117
  /* @__PURE__ */ jsx(FigureHeader, { title, subtitle }),
98
118
  imageElement(0, images[0]),
99
119
  /* @__PURE__ */ jsx(FigureCaption, { caption, label: captionLabel })
100
- ] }) : isCompact ? /* @__PURE__ */ jsx("div", { className: "flex gap-3", children: images.map((img, index) => imageElement(index, img, "flex-1")) }) : /* @__PURE__ */ jsx("div", { className: "flex gap-3 overflow-x-auto pb-4", children: images.map((img, index) => /* @__PURE__ */ jsx(
120
+ ] }) : isCompact ? /* @__PURE__ */ jsx("div", { className: "flex gap-3", children: images.map((img, index) => imageElement(index, img, "flex-1")) }) : /* @__PURE__ */ jsx("div", { ref: scrollRef, className: "flex gap-3 overflow-x-auto overscroll-x-contain pb-4", children: images.map((img, index) => /* @__PURE__ */ jsx(
101
121
  "div",
102
122
  {
123
+ title: img.label,
103
124
  className: "flex-none w-[30%] min-w-[250px] aspect-square overflow-hidden rounded-sm bg-muted/10 cursor-pointer group",
104
125
  onClick: () => lightbox.open(index),
105
126
  children: /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../src/components/gallery/index.tsx"],"sourcesContent":["import { useMemo } 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\nfunction getImageLabel(path: string): string {\n const filename = path.split('/').pop() || path;\n return filename\n .replace(/\\.(png|jpg|jpeg|gif|svg|webp)$/i, '')\n .replace(/[_-]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction getImageUrl(path: string): string {\n return `/raw/${path}`;\n}\n\nexport default function Gallery({\n path,\n globs = null,\n caption,\n captionLabel,\n title,\n subtitle,\n limit = null,\n page = 0,\n children,\n childAlign = \"right\",\n}: {\n path?: string;\n globs?: string[] | null;\n caption?: string;\n captionLabel?: string;\n title?: string;\n subtitle?: string;\n limit?: number | null;\n page?: number;\n children?: React.ReactNode;\n childAlign?: \"left\" | \"right\";\n}) {\n const { paths, isLoading, isEmpty } = useGalleryImages({\n path,\n globs,\n limit,\n page: page,\n });\n\n const lightbox = useLightbox(paths.length);\n\n const images: LightboxImage[] = useMemo(() =>\n paths.map(p => ({ src: getImageUrl(p), label: getImageLabel(p) })),\n [paths]\n );\n\n if (isLoading) {\n return (\n <figure className=\"not-prose py-6 md:py-8\">\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\">\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 isSingleWithChildren = images.length === 1 && children;\n // 2-3 images should break out of the content width\n const shouldBreakOut = images.length >= 2;\n\n const imageElement = (index: number, img: LightboxImage, className?: string) => (\n <div\n key={index}\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 className={`not-prose relative py-6 md:py-8 ${shouldBreakOut ? (isTwo ? 'w-[75vw] ml-[calc(-37.5vw+50%)]' : isCompact ? 'w-[96vw] ml-[calc(-48vw+50%)]' : 'w-[var(--gallery-width)] ml-[calc(-45vw+50%)]') : ''}`}>\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={`flex gap-6 ${childAlign === 'left' ? '' : 'flex-row-reverse'}`}>\n <div className=\"flex-1 text-sm leading-relaxed text-foreground/90 space-y-3 [&>ul]:space-y-1.5 [&>ul]:list-disc [&>ul]:pl-5 [&>ol]:space-y-1.5 [&>ol]:list-decimal [&>ol]:pl-5 flex flex-col\">\n {(title || subtitle) && <div className=\"invisible\"><FigureHeader title={title} subtitle={subtitle} /></div>}\n <div>{children}</div>\n </div>\n <div className=\"w-3/5 flex-shrink-0\">\n <FigureHeader title={title} subtitle={subtitle} />\n {imageElement(0, images[0])}\n <FigureCaption caption={caption} label={captionLabel} />\n </div>\n </div>\n ) : isSingle ? (\n <div className=\"max-w-[70%] 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 className=\"flex gap-3 overflow-x-auto pb-4\">\n {images.map((img, index) => (\n <div\n key={index}\n className=\"flex-none w-[30%] 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 } 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 filename = path.split('/').pop() || path;\n return filename\n .replace(/\\.(png|jpg|jpeg|gif|svg|webp)$/i, '')\n .replace(/[_-]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction getImageUrl(path: string): string {\n return `/raw/${path}`;\n}\n\nexport default function Gallery({\n path,\n globs = null,\n caption,\n captionLabel,\n title,\n subtitle,\n limit = null,\n page = 0,\n children,\n childAlign = \"right\",\n}: {\n path?: string;\n globs?: string[] | null;\n caption?: string;\n captionLabel?: string;\n title?: string;\n subtitle?: string;\n limit?: number | null;\n page?: number;\n children?: React.ReactNode;\n childAlign?: \"left\" | \"right\";\n}) {\n const { paths, 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 images: LightboxImage[] = useMemo(() =>\n paths.map(p => ({ src: getImageUrl(p), label: getImageLabel(p) })),\n [paths]\n );\n\n if (isLoading) {\n return (\n <figure className=\"not-prose py-6 md:py-8\">\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\">\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 isSingleWithChildren = images.length === 1 && children;\n // 2-3 images should break out of the content width\n const shouldBreakOut = images.length >= 2;\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 className={`not-prose relative py-6 md:py-8 ${shouldBreakOut ? (isTwo ? 'w-[75vw] ml-[calc(-37.5vw+50%)]' : isCompact ? 'w-[96vw] ml-[calc(-48vw+50%)]' : 'w-[var(--gallery-width)] ml-[calc(-45vw+50%)]') : ''}`}>\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={`flex gap-6 ${childAlign === 'left' ? '' : 'flex-row-reverse'}`}>\n <div className=\"flex-1 text-sm leading-relaxed text-foreground/90 space-y-3 [&>ul]:space-y-1.5 [&>ul]:list-disc [&>ul]:pl-5 [&>ol]:space-y-1.5 [&>ol]:list-decimal [&>ol]:pl-5 flex flex-col\">\n {(title || subtitle) && <div className=\"invisible\"><FigureHeader title={title} subtitle={subtitle} /></div>}\n <div>{children}</div>\n </div>\n <div className=\"w-3/5 flex-shrink-0\">\n <FigureHeader title={title} subtitle={subtitle} />\n {imageElement(0, images[0])}\n <FigureCaption caption={caption} label={captionLabel} />\n </div>\n </div>\n ) : isSingle ? (\n <div className=\"max-w-[70%] 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=\"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-[30%] 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,8 +1,56 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useFileContent } from "../plugin/src/client.js";
3
- import { useMemo, useState } from "react";
2
+ import { useDirectory, useFileContent } from "../plugin/src/client.js";
3
+ import { useMemo, useRef, useEffect, useState } from "react";
4
+ import { useParams } from "react-router-dom";
4
5
  import { cn } from "../lib/utils.js";
6
+ import { minimatch } from "minimatch";
5
7
  import { parseConfigFile, extractPath, getValueType, formatValue } from "../lib/parameter-utils.js";
8
+ function usePreventSwipeNavigation(ref) {
9
+ useEffect(() => {
10
+ const el = ref.current;
11
+ if (!el) return;
12
+ const handleWheel = (e) => {
13
+ if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;
14
+ const { scrollLeft, scrollWidth, clientWidth } = el;
15
+ const atLeftEdge = scrollLeft <= 0;
16
+ const atRightEdge = scrollLeft + clientWidth >= scrollWidth - 1;
17
+ if (atLeftEdge && e.deltaX < 0 || atRightEdge && e.deltaX > 0) {
18
+ e.preventDefault();
19
+ }
20
+ };
21
+ el.addEventListener("wheel", handleWheel, { passive: false });
22
+ return () => el.removeEventListener("wheel", handleWheel);
23
+ }, [ref]);
24
+ }
25
+ function isGlobPattern(path) {
26
+ return path.includes("*") || path.includes("?") || path.includes("[");
27
+ }
28
+ function collectAllConfigFiles(entry) {
29
+ if (entry.type === "file") {
30
+ if (entry.name.match(/\.(yaml|yml|json)$/i)) {
31
+ return [entry];
32
+ }
33
+ return [];
34
+ }
35
+ const files = [];
36
+ for (const child of entry.children || []) {
37
+ files.push(...collectAllConfigFiles(child));
38
+ }
39
+ return files;
40
+ }
41
+ function sortPathsNumerically(paths) {
42
+ paths.sort((a, b) => {
43
+ const nums = (s) => (s.match(/\d+/g) || []).map(Number);
44
+ const na = nums(a);
45
+ const nb = nums(b);
46
+ const len = Math.max(na.length, nb.length);
47
+ for (let i = 0; i < len; i++) {
48
+ const diff = (na[i] ?? 0) - (nb[i] ?? 0);
49
+ if (diff !== 0) return diff;
50
+ }
51
+ return a.localeCompare(b);
52
+ });
53
+ }
6
54
  function filterData(data, keys) {
7
55
  const result = {};
8
56
  for (const keyPath of keys) {
@@ -200,16 +248,28 @@ function splitIntoColumns(entries, numColumns) {
200
248
  }
201
249
  return columns;
202
250
  }
203
- function ParameterTable({ path, keys }) {
251
+ function SingleParameterTable({ path, keys, label, withMargin = true }) {
204
252
  const { content, loading, error } = useFileContent(path);
205
- const parsed = useMemo(() => {
206
- if (!content) return null;
253
+ const { parsed, parseError } = useMemo(() => {
254
+ if (!content) return { parsed: null, parseError: "no content" };
207
255
  const data = parseConfigFile(content, path);
208
- if (!data) return null;
256
+ if (!data) {
257
+ if (!path.match(/\.(yaml|yml|json)$/i)) {
258
+ return { parsed: null, parseError: `unsupported file type` };
259
+ }
260
+ if (content.trim().startsWith("<!") || content.trim().startsWith("<html")) {
261
+ return { parsed: null, parseError: `file not found` };
262
+ }
263
+ return { parsed: null, parseError: `invalid ${path.split(".").pop()} syntax` };
264
+ }
209
265
  if (keys && keys.length > 0) {
210
- return filterData(data, keys);
266
+ const filtered = filterData(data, keys);
267
+ if (Object.keys(filtered).length === 0) {
268
+ return { parsed: null, parseError: `keys not found: ${keys.join(", ")}` };
269
+ }
270
+ return { parsed: filtered, parseError: null };
211
271
  }
212
- return data;
272
+ return { parsed: data, parseError: null };
213
273
  }, [content, path, keys]);
214
274
  if (loading) {
215
275
  return /* @__PURE__ */ jsx("div", { className: "my-6 p-4 rounded border border-border/50 bg-card/30", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-muted-foreground/60", children: [
@@ -221,7 +281,13 @@ function ParameterTable({ path, keys }) {
221
281
  return /* @__PURE__ */ jsx("div", { className: "my-6 p-3 rounded border border-destructive/30 bg-destructive/5", children: /* @__PURE__ */ jsx("p", { className: "text-[11px] font-mono text-destructive", children: error }) });
222
282
  }
223
283
  if (!parsed) {
224
- return /* @__PURE__ */ jsx("div", { className: "my-6 p-3 rounded border border-border/50 bg-card/30", children: /* @__PURE__ */ jsx("p", { className: "text-[11px] font-mono text-muted-foreground", children: "unable to parse config" }) });
284
+ return /* @__PURE__ */ jsx("div", { className: cn("p-3 rounded border border-border/50 bg-card/30", withMargin && "my-6"), children: /* @__PURE__ */ jsxs("p", { className: "text-[11px] font-mono text-muted-foreground", children: [
285
+ label && /* @__PURE__ */ jsxs("span", { className: "text-foreground/60", children: [
286
+ label,
287
+ ": "
288
+ ] }),
289
+ parseError || "unable to parse"
290
+ ] }) });
225
291
  }
226
292
  const entries = Object.entries(parsed);
227
293
  const topLeaves = entries.filter(([, v]) => {
@@ -281,19 +347,86 @@ function ParameterTable({ path, keys }) {
281
347
  key
282
348
  );
283
349
  };
284
- return /* @__PURE__ */ jsx("div", { className: "my-6 not-prose", children: /* @__PURE__ */ jsxs("div", { className: "rounded border border-border/60 bg-card/20 p-3 overflow-hidden", children: [
285
- topLeaves.length > 0 && /* @__PURE__ */ jsx("div", { className: cn(topNested.length > 0 && "mb-4 pb-3 border-b border-border/30"), children: /* @__PURE__ */ jsx(ParameterGrid, { entries: topLeaves }) }),
286
- useColumns ? /* @__PURE__ */ jsx(
287
- "div",
288
- {
289
- className: "grid gap-6",
290
- style: { gridTemplateColumns: `repeat(${columns.length}, 1fr)` },
291
- children: columns.map((columnEntries, colIndex) => /* @__PURE__ */ jsx("div", { className: cn(
292
- colIndex > 0 && "border-l border-border/30 pl-6"
293
- ), children: columnEntries.map(renderNestedEntry) }, colIndex))
294
- }
295
- ) : topNested.map(renderNestedEntry)
296
- ] }) });
350
+ return /* @__PURE__ */ jsxs("div", { className: cn("not-prose", withMargin && "my-6"), children: [
351
+ label && /* @__PURE__ */ jsx("div", { className: "text-[11px] font-mono text-muted-foreground mb-1.5 truncate", title: label, children: label }),
352
+ /* @__PURE__ */ jsxs("div", { className: "rounded border border-border/60 bg-card/20 p-3 overflow-hidden", children: [
353
+ topLeaves.length > 0 && /* @__PURE__ */ jsx("div", { className: cn(topNested.length > 0 && "mb-4 pb-3 border-b border-border/30"), children: /* @__PURE__ */ jsx(ParameterGrid, { entries: topLeaves }) }),
354
+ useColumns ? /* @__PURE__ */ jsx(
355
+ "div",
356
+ {
357
+ className: "grid gap-6",
358
+ style: { gridTemplateColumns: `repeat(${columns.length}, 1fr)` },
359
+ children: columns.map((columnEntries, colIndex) => /* @__PURE__ */ jsx("div", { className: cn(
360
+ colIndex > 0 && "border-l border-border/30 pl-6"
361
+ ), children: columnEntries.map(renderNestedEntry) }, colIndex))
362
+ }
363
+ ) : topNested.map(renderNestedEntry)
364
+ ] })
365
+ ] });
366
+ }
367
+ function ParameterTable({ path, keys }) {
368
+ const { "*": routePath = "" } = useParams();
369
+ const currentDir = routePath.replace(/\/?[^/]+\.mdx$/i, "").replace(/\/$/, "") || ".";
370
+ let resolvedPath = path;
371
+ if (path == null ? void 0 : path.startsWith("./")) {
372
+ const relativePart = path.slice(2);
373
+ resolvedPath = currentDir === "." ? relativePart : `${currentDir}/${relativePart}`;
374
+ } else if (path && !path.startsWith("/") && !path.includes("/") && !isGlobPattern(path)) {
375
+ resolvedPath = currentDir === "." ? path : `${currentDir}/${path}`;
376
+ }
377
+ const hasGlob = isGlobPattern(resolvedPath);
378
+ const baseDir = hasGlob ? resolvedPath.split(/[*?\[]/, 1)[0].replace(/\/$/, "") || "." : null;
379
+ const { directory } = useDirectory(baseDir || ".");
380
+ const matchingPaths = useMemo(() => {
381
+ if (!hasGlob || !directory) return [];
382
+ const allFiles = collectAllConfigFiles(directory);
383
+ const paths = allFiles.map((f) => f.path).filter((p) => minimatch(p, resolvedPath, { matchBase: true }));
384
+ sortPathsNumerically(paths);
385
+ console.log("[ParameterTable]", {
386
+ original: path,
387
+ resolved: resolvedPath,
388
+ baseDir,
389
+ allConfigFiles: allFiles.map((f) => f.path),
390
+ matched: paths
391
+ });
392
+ return paths;
393
+ }, [hasGlob, directory, resolvedPath, path, baseDir]);
394
+ if (!hasGlob) {
395
+ return /* @__PURE__ */ jsx(SingleParameterTable, { path: resolvedPath, keys });
396
+ }
397
+ if (!directory) {
398
+ return /* @__PURE__ */ jsx("div", { className: "my-6 p-4 rounded border border-border/50 bg-card/30", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-muted-foreground/60", children: [
399
+ /* @__PURE__ */ jsx("div", { className: "w-3 h-3 border border-current border-t-transparent rounded-full animate-spin" }),
400
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-mono", children: "loading parameters..." })
401
+ ] }) });
402
+ }
403
+ if (matchingPaths.length === 0) {
404
+ return /* @__PURE__ */ jsx("div", { className: "my-6 p-3 rounded border border-border/50 bg-card/30", children: /* @__PURE__ */ jsxs("p", { className: "text-[11px] font-mono text-muted-foreground", children: [
405
+ "no files matching: ",
406
+ resolvedPath,
407
+ /* @__PURE__ */ jsx("br", {}),
408
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground/50", children: [
409
+ "(base dir: ",
410
+ baseDir,
411
+ ", original: ",
412
+ path,
413
+ ")"
414
+ ] })
415
+ ] }) });
416
+ }
417
+ const count = matchingPaths.length;
418
+ const breakoutClass = count >= 3 ? "w-[90vw] ml-[calc(-45vw+50%)]" : count === 2 ? "w-[75vw] ml-[calc(-37.5vw+50%)]" : "";
419
+ const scrollRef = useRef(null);
420
+ usePreventSwipeNavigation(scrollRef);
421
+ return /* @__PURE__ */ jsx("div", { ref: scrollRef, className: `my-6 flex gap-4 overflow-x-auto overscroll-x-contain pb-2 ${breakoutClass}`, children: matchingPaths.map((filePath) => /* @__PURE__ */ jsx("div", { className: "flex-none min-w-[300px] max-w-[400px]", children: /* @__PURE__ */ jsx(
422
+ SingleParameterTable,
423
+ {
424
+ path: filePath,
425
+ keys,
426
+ label: filePath.split("/").pop() || filePath,
427
+ withMargin: false
428
+ }
429
+ ) }, filePath)) });
297
430
  }
298
431
  export {
299
432
  ParameterTable
@@ -1 +1 @@
1
- {"version":3,"file":"parameter-table.js","sources":["../../../src/components/parameter-table.tsx"],"sourcesContent":["import { useFileContent } from \"../../plugin/src/client\";\nimport { useMemo, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n type ParameterValue,\n extractPath,\n getValueType,\n formatValue,\n parseConfigFile,\n} from \"@/lib/parameter-utils\";\n\n/**\n * Build a filtered data object from an array of jq-like paths.\n * Each path extracts data and places it in the result under the final key name.\n */\nfunction filterData(\n data: Record<string, ParameterValue>,\n keys: string[]\n): Record<string, ParameterValue> {\n const result: Record<string, ParameterValue> = {};\n\n for (const keyPath of keys) {\n const extracted = extractPath(data, keyPath);\n if (extracted === undefined) continue;\n\n const cleanPath = keyPath.startsWith(\".\") ? keyPath.slice(1) : keyPath;\n\n // For simple paths like .base.N_E, use \"N_E\" as key\n // For paths with [], preserve more context\n let keyName: string;\n if (cleanPath.includes(\"[\")) {\n keyName = cleanPath.replace(/\\[\\]/g, \"\").replace(/\\[(\\d+)\\]/g, \"_$1\");\n } else {\n const parts = cleanPath.split(\".\");\n keyName = parts[parts.length - 1];\n }\n\n result[keyName] = extracted;\n }\n\n return result;\n}\n\n// Renders a flat section of key-value pairs in a dense grid\nfunction ParameterGrid({ entries }: { entries: [string, ParameterValue][] }) {\n if (entries.length === 0) return null;\n\n return (\n <div className=\"grid grid-cols-[repeat(auto-fill,minmax(180px,1fr))] gap-x-6 gap-y-px\">\n {entries.map(([key, value]) => {\n const type = getValueType(value);\n return (\n <div\n key={key}\n className=\"flex items-baseline justify-between gap-2 py-1 group hover:bg-muted/30 -mx-1.5 px-1.5 rounded-sm transition-colors\"\n >\n <span className=\"text-[11px] text-muted-foreground font-mono truncate\">\n {key}\n </span>\n <span\n className={cn(\n \"text-[11px] font-mono tabular-nums font-medium shrink-0\",\n type === \"number\" && \"text-foreground\",\n type === \"string\" && \"text-amber-600 dark:text-amber-500\",\n type === \"boolean\" && \"text-cyan-600 dark:text-cyan-500\",\n type === \"null\" && \"text-muted-foreground/50\"\n )}\n >\n {type === \"string\" ? `\"${formatValue(value)}\"` : formatValue(value)}\n </span>\n </div>\n );\n })}\n </div>\n );\n}\n\n// Renders a nested section with its own header\nfunction ParameterSection({\n name,\n data,\n depth = 0\n}: {\n name: string;\n data: Record<string, ParameterValue>;\n depth?: number;\n}) {\n const [isCollapsed, setIsCollapsed] = useState(false);\n\n const entries = Object.entries(data);\n const leafEntries = entries.filter(([, v]) => {\n const t = getValueType(v);\n return t !== \"object\" && t !== \"array\";\n });\n const nestedEntries = entries.filter(([, v]) => {\n const t = getValueType(v);\n return t === \"object\" || t === \"array\";\n });\n\n return (\n <div className={cn(depth === 0 && \"mb-4 last:mb-0\")}>\n <button\n onClick={() => setIsCollapsed(!isCollapsed)}\n className={cn(\n \"flex items-center gap-2 w-full text-left group mb-1.5\",\n depth === 0 && \"pb-1 border-b border-border/50\"\n )}\n >\n <span className={cn(\n \"text-[10px] text-muted-foreground/60 transition-transform duration-150 select-none\",\n isCollapsed && \"-rotate-90\"\n )}>\n {isCollapsed ? \"+\" : \"-\"}\n </span>\n\n <span className={cn(\n \"font-mono text-[11px] uppercase tracking-widest\",\n depth === 0\n ? \"text-foreground/80 font-semibold\"\n : \"text-muted-foreground/70\"\n )}>\n {name.replace(/_/g, \" \")}\n </span>\n\n <span className=\"text-[9px] font-mono text-muted-foreground/40 ml-auto\">\n {entries.length}\n </span>\n </button>\n\n {!isCollapsed && (\n <div className={cn(\n depth > 0 && \"pl-3 ml-1 border-l border-border/40\"\n )}>\n {leafEntries.length > 0 && (\n <div className={cn(nestedEntries.length > 0 && \"mb-3\")}>\n <ParameterGrid entries={leafEntries} />\n </div>\n )}\n\n {nestedEntries.map(([key, value]) => {\n const type = getValueType(value);\n if (type === \"array\") {\n const arr = value as ParameterValue[];\n return (\n <div key={key} className=\"mb-2 last:mb-0\">\n <div className=\"text-[10px] font-mono text-muted-foreground/60 uppercase tracking-wider mb-1\">\n {key} [{arr.length}]\n </div>\n <div className=\"pl-3 ml-1 border-l border-border/40\">\n {arr.map((item, i) => {\n const itemType = getValueType(item);\n if (itemType === \"object\") {\n return (\n <ParameterSection\n key={i}\n name={`${i}`}\n data={item as Record<string, ParameterValue>}\n depth={depth + 1}\n />\n );\n }\n return (\n <div key={i} className=\"text-[11px] font-mono text-foreground py-0.5\">\n [{i}] {formatValue(item)}\n </div>\n );\n })}\n </div>\n </div>\n );\n }\n return (\n <ParameterSection\n key={key}\n name={key}\n data={value as Record<string, ParameterValue>}\n depth={depth + 1}\n />\n );\n })}\n </div>\n )}\n </div>\n );\n}\n\ninterface ParameterTableProps {\n /** Path to the YAML or JSON file */\n path: string;\n /**\n * Optional array of jq-like paths to filter which parameters to show.\n * Examples:\n * - [\".base.N_E\", \".base.N_I\"] → show only N_E and N_I from base\n * - [\".base\"] → show entire base section\n * - [\".default_inputs\", \".base.dt\"] → show default_inputs section and dt from base\n */\n keys?: string[];\n}\n\n/**\n * Estimate the height contribution of a data structure.\n */\nfunction estimateHeight(data: Record<string, ParameterValue>, depth = 0): number {\n const entries = Object.entries(data);\n let height = 0;\n\n for (const [, value] of entries) {\n const type = getValueType(value);\n if (type === \"object\") {\n height += 28 + estimateHeight(value as Record<string, ParameterValue>, depth + 1);\n } else if (type === \"array\") {\n const arr = value as ParameterValue[];\n height += 28;\n for (const item of arr) {\n if (getValueType(item) === \"object\") {\n height += 24 + estimateHeight(item as Record<string, ParameterValue>, depth + 1);\n } else {\n height += 24;\n }\n }\n } else {\n height += 24;\n }\n }\n\n return height;\n}\n\n/**\n * Split entries into balanced columns based on estimated height.\n */\nfunction splitIntoColumns<T extends [string, ParameterValue]>(\n entries: T[],\n numColumns: number\n): T[][] {\n if (numColumns <= 1) return [entries];\n\n const entryHeights = entries.map(([, value]) => {\n const type = getValueType(value);\n if (type === \"object\") {\n return 28 + estimateHeight(value as Record<string, ParameterValue>);\n } else if (type === \"array\") {\n const arr = value as ParameterValue[];\n let h = 28;\n for (const item of arr) {\n if (getValueType(item) === \"object\") {\n h += 24 + estimateHeight(item as Record<string, ParameterValue>);\n } else {\n h += 24;\n }\n }\n return h;\n }\n return 24;\n });\n\n const totalHeight = entryHeights.reduce((a, b) => a + b, 0);\n const targetPerColumn = totalHeight / numColumns;\n\n const columns: T[][] = [];\n let currentColumn: T[] = [];\n let currentHeight = 0;\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n const entryHeight = entryHeights[i];\n\n if (currentHeight >= targetPerColumn && columns.length < numColumns - 1 && currentColumn.length > 0) {\n columns.push(currentColumn);\n currentColumn = [];\n currentHeight = 0;\n }\n\n currentColumn.push(entry);\n currentHeight += entryHeight;\n }\n\n if (currentColumn.length > 0) {\n columns.push(currentColumn);\n }\n\n return columns;\n}\n\nexport function ParameterTable({ path, keys }: ParameterTableProps) {\n const { content, loading, error } = useFileContent(path);\n\n const parsed = useMemo(() => {\n if (!content) return null;\n\n const data = parseConfigFile(content, path);\n if (!data) return null;\n\n if (keys && keys.length > 0) {\n return filterData(data, keys);\n }\n\n return data;\n }, [content, path, keys]);\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=\"my-6 p-3 rounded border border-border/50 bg-card/30\">\n <p className=\"text-[11px] font-mono text-muted-foreground\">unable to parse config</p>\n </div>\n );\n }\n\n const entries = Object.entries(parsed);\n\n const topLeaves = entries.filter(([, v]) => {\n const t = getValueType(v);\n return t !== \"object\" && t !== \"array\";\n });\n const topNested = entries.filter(([, v]) => {\n const t = getValueType(v);\n return t === \"object\" || t === \"array\";\n });\n\n const estHeight = estimateHeight(parsed);\n const HEIGHT_THRESHOLD = 500;\n const numColumns = estHeight > HEIGHT_THRESHOLD ? Math.min(Math.ceil(estHeight / HEIGHT_THRESHOLD), 3) : 1;\n const useColumns = numColumns > 1 && topNested.length > 1;\n\n const columns = useColumns\n ? splitIntoColumns(topNested as [string, ParameterValue][], numColumns)\n : [topNested];\n\n const filename = path.split(\"/\").pop() || path;\n\n const renderNestedEntry = ([key, value]: [string, ParameterValue]) => {\n const type = getValueType(value);\n if (type === \"array\") {\n const arr = value as ParameterValue[];\n return (\n <div key={key} className=\"mb-4 last:mb-0\">\n <div className=\"text-[11px] font-mono text-foreground/80 uppercase tracking-widest font-semibold mb-1.5 pb-1 border-b border-border/50\">\n {key.replace(/_/g, \" \")} [{arr.length}]\n </div>\n <div className=\"pl-3 ml-1 border-l border-border/40\">\n {arr.map((item, i) => {\n const itemType = getValueType(item);\n if (itemType === \"object\") {\n return (\n <ParameterSection\n key={i}\n name={`${i}`}\n data={item as Record<string, ParameterValue>}\n depth={1}\n />\n );\n }\n return (\n <div key={i} className=\"text-[11px] font-mono text-foreground py-0.5\">\n [{i}] {formatValue(item)}\n </div>\n );\n })}\n </div>\n </div>\n );\n }\n return (\n <ParameterSection\n key={key}\n name={key}\n data={value as Record<string, ParameterValue>}\n depth={0}\n />\n );\n };\n\n return (\n <div className=\"my-6 not-prose\">\n <div className=\"rounded border border-border/60 bg-card/20 p-3 overflow-hidden\">\n {topLeaves.length > 0 && (\n <div className={cn(topNested.length > 0 && \"mb-4 pb-3 border-b border-border/30\")}>\n <ParameterGrid entries={topLeaves} />\n </div>\n )}\n\n {useColumns ? (\n <div\n className=\"grid gap-6\"\n style={{ gridTemplateColumns: `repeat(${columns.length}, 1fr)` }}\n >\n {columns.map((columnEntries, colIndex) => (\n <div key={colIndex} className={cn(\n colIndex > 0 && \"border-l border-border/30 pl-6\"\n )}>\n {columnEntries.map(renderNestedEntry)}\n </div>\n ))}\n </div>\n ) : (\n topNested.map(renderNestedEntry)\n )}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"parameter-table.js","sources":["../../../src/components/parameter-table.tsx"],"sourcesContent":["import { useFileContent, useDirectory } from \"../../plugin/src/client\";\nimport { useMemo, useState, 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\n/**\n * Build a filtered data object from an array of jq-like paths.\n * Each path extracts data and places it in the result under the final key name.\n */\nfunction filterData(\n data: Record<string, ParameterValue>,\n keys: string[]\n): Record<string, ParameterValue> {\n const result: Record<string, ParameterValue> = {};\n\n for (const keyPath of keys) {\n const extracted = extractPath(data, keyPath);\n if (extracted === undefined) continue;\n\n const cleanPath = keyPath.startsWith(\".\") ? keyPath.slice(1) : keyPath;\n\n // For simple paths like .base.N_E, use \"N_E\" as key\n // For paths with [], preserve more context\n let keyName: string;\n if (cleanPath.includes(\"[\")) {\n keyName = cleanPath.replace(/\\[\\]/g, \"\").replace(/\\[(\\d+)\\]/g, \"_$1\");\n } else {\n const parts = cleanPath.split(\".\");\n keyName = parts[parts.length - 1];\n }\n\n result[keyName] = extracted;\n }\n\n return result;\n}\n\n// Renders a flat section of key-value pairs in a dense grid\nfunction ParameterGrid({ entries }: { entries: [string, ParameterValue][] }) {\n if (entries.length === 0) return null;\n\n return (\n <div className=\"grid grid-cols-[repeat(auto-fill,minmax(180px,1fr))] gap-x-6 gap-y-px\">\n {entries.map(([key, value]) => {\n const type = getValueType(value);\n return (\n <div\n key={key}\n className=\"flex items-baseline justify-between gap-2 py-1 group hover:bg-muted/30 -mx-1.5 px-1.5 rounded-sm transition-colors\"\n >\n <span className=\"text-[11px] text-muted-foreground font-mono truncate\">\n {key}\n </span>\n <span\n className={cn(\n \"text-[11px] font-mono tabular-nums font-medium shrink-0\",\n type === \"number\" && \"text-foreground\",\n type === \"string\" && \"text-amber-600 dark:text-amber-500\",\n type === \"boolean\" && \"text-cyan-600 dark:text-cyan-500\",\n type === \"null\" && \"text-muted-foreground/50\"\n )}\n >\n {type === \"string\" ? `\"${formatValue(value)}\"` : formatValue(value)}\n </span>\n </div>\n );\n })}\n </div>\n );\n}\n\n// Renders a nested section with its own header\nfunction ParameterSection({\n name,\n data,\n depth = 0\n}: {\n name: string;\n data: Record<string, ParameterValue>;\n depth?: number;\n}) {\n const [isCollapsed, setIsCollapsed] = useState(false);\n\n const entries = Object.entries(data);\n const leafEntries = entries.filter(([, v]) => {\n const t = getValueType(v);\n return t !== \"object\" && t !== \"array\";\n });\n const nestedEntries = entries.filter(([, v]) => {\n const t = getValueType(v);\n return t === \"object\" || t === \"array\";\n });\n\n return (\n <div className={cn(depth === 0 && \"mb-4 last:mb-0\")}>\n <button\n onClick={() => setIsCollapsed(!isCollapsed)}\n className={cn(\n \"flex items-center gap-2 w-full text-left group mb-1.5\",\n depth === 0 && \"pb-1 border-b border-border/50\"\n )}\n >\n <span className={cn(\n \"text-[10px] text-muted-foreground/60 transition-transform duration-150 select-none\",\n isCollapsed && \"-rotate-90\"\n )}>\n {isCollapsed ? \"+\" : \"-\"}\n </span>\n\n <span className={cn(\n \"font-mono text-[11px] uppercase tracking-widest\",\n depth === 0\n ? \"text-foreground/80 font-semibold\"\n : \"text-muted-foreground/70\"\n )}>\n {name.replace(/_/g, \" \")}\n </span>\n\n <span className=\"text-[9px] font-mono text-muted-foreground/40 ml-auto\">\n {entries.length}\n </span>\n </button>\n\n {!isCollapsed && (\n <div className={cn(\n depth > 0 && \"pl-3 ml-1 border-l border-border/40\"\n )}>\n {leafEntries.length > 0 && (\n <div className={cn(nestedEntries.length > 0 && \"mb-3\")}>\n <ParameterGrid entries={leafEntries} />\n </div>\n )}\n\n {nestedEntries.map(([key, value]) => {\n const type = getValueType(value);\n if (type === \"array\") {\n const arr = value as ParameterValue[];\n return (\n <div key={key} className=\"mb-2 last:mb-0\">\n <div className=\"text-[10px] font-mono text-muted-foreground/60 uppercase tracking-wider mb-1\">\n {key} [{arr.length}]\n </div>\n <div className=\"pl-3 ml-1 border-l border-border/40\">\n {arr.map((item, i) => {\n const itemType = getValueType(item);\n if (itemType === \"object\") {\n return (\n <ParameterSection\n key={i}\n name={`${i}`}\n data={item as Record<string, ParameterValue>}\n depth={depth + 1}\n />\n );\n }\n return (\n <div key={i} className=\"text-[11px] font-mono text-foreground py-0.5\">\n [{i}] {formatValue(item)}\n </div>\n );\n })}\n </div>\n </div>\n );\n }\n return (\n <ParameterSection\n key={key}\n name={key}\n data={value as Record<string, ParameterValue>}\n depth={depth + 1}\n />\n );\n })}\n </div>\n )}\n </div>\n );\n}\n\ninterface SingleParameterTableProps {\n /** Path to the YAML or JSON file */\n path: string;\n /**\n * Optional array of jq-like paths to filter which parameters to show.\n * Examples:\n * - [\".base.N_E\", \".base.N_I\"] → show only N_E and N_I from base\n * - [\".base\"] → show entire base section\n * - [\".default_inputs\", \".base.dt\"] → show default_inputs section and dt from base\n */\n keys?: 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\ninterface ParameterTableProps {\n /** Path to the YAML or JSON file, supports glob patterns like \"*.yaml\" */\n path: string;\n /**\n * Optional array of jq-like paths to filter which parameters to show.\n */\n keys?: string[];\n}\n\n/**\n * Estimate the height contribution of a data structure.\n */\nfunction estimateHeight(data: Record<string, ParameterValue>, depth = 0): number {\n const entries = Object.entries(data);\n let height = 0;\n\n for (const [, value] of entries) {\n const type = getValueType(value);\n if (type === \"object\") {\n height += 28 + estimateHeight(value as Record<string, ParameterValue>, depth + 1);\n } else if (type === \"array\") {\n const arr = value as ParameterValue[];\n height += 28;\n for (const item of arr) {\n if (getValueType(item) === \"object\") {\n height += 24 + estimateHeight(item as Record<string, ParameterValue>, depth + 1);\n } else {\n height += 24;\n }\n }\n } else {\n height += 24;\n }\n }\n\n return height;\n}\n\n/**\n * Split entries into balanced columns based on estimated height.\n */\nfunction splitIntoColumns<T extends [string, ParameterValue]>(\n entries: T[],\n numColumns: number\n): T[][] {\n if (numColumns <= 1) return [entries];\n\n const entryHeights = entries.map(([, value]) => {\n const type = getValueType(value);\n if (type === \"object\") {\n return 28 + estimateHeight(value as Record<string, ParameterValue>);\n } else if (type === \"array\") {\n const arr = value as ParameterValue[];\n let h = 28;\n for (const item of arr) {\n if (getValueType(item) === \"object\") {\n h += 24 + estimateHeight(item as Record<string, ParameterValue>);\n } else {\n h += 24;\n }\n }\n return h;\n }\n return 24;\n });\n\n const totalHeight = entryHeights.reduce((a, b) => a + b, 0);\n const targetPerColumn = totalHeight / numColumns;\n\n const columns: T[][] = [];\n let currentColumn: T[] = [];\n let currentHeight = 0;\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n const entryHeight = entryHeights[i];\n\n if (currentHeight >= targetPerColumn && columns.length < numColumns - 1 && currentColumn.length > 0) {\n columns.push(currentColumn);\n currentColumn = [];\n currentHeight = 0;\n }\n\n currentColumn.push(entry);\n currentHeight += entryHeight;\n }\n\n if (currentColumn.length > 0) {\n columns.push(currentColumn);\n }\n\n return columns;\n}\n\nfunction SingleParameterTable({ path, keys, label, withMargin = true }: SingleParameterTableProps) {\n const { content, loading, error } = useFileContent(path);\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 if (keys && keys.length > 0) {\n const filtered = filterData(data, keys);\n if (Object.keys(filtered).length === 0) {\n return { parsed: null, parseError: `keys not found: ${keys.join(', ')}` };\n }\n return { parsed: filtered, parseError: null };\n }\n\n return { parsed: data, parseError: null };\n }, [content, path, keys]);\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 entries = Object.entries(parsed);\n\n const topLeaves = entries.filter(([, v]) => {\n const t = getValueType(v);\n return t !== \"object\" && t !== \"array\";\n });\n const topNested = entries.filter(([, v]) => {\n const t = getValueType(v);\n return t === \"object\" || t === \"array\";\n });\n\n const estHeight = estimateHeight(parsed);\n const HEIGHT_THRESHOLD = 500;\n const numColumns = estHeight > HEIGHT_THRESHOLD ? Math.min(Math.ceil(estHeight / HEIGHT_THRESHOLD), 3) : 1;\n const useColumns = numColumns > 1 && topNested.length > 1;\n\n const columns = useColumns\n ? splitIntoColumns(topNested as [string, ParameterValue][], numColumns)\n : [topNested];\n\n const filename = path.split(\"/\").pop() || path;\n\n const renderNestedEntry = ([key, value]: [string, ParameterValue]) => {\n const type = getValueType(value);\n if (type === \"array\") {\n const arr = value as ParameterValue[];\n return (\n <div key={key} className=\"mb-4 last:mb-0\">\n <div className=\"text-[11px] font-mono text-foreground/80 uppercase tracking-widest font-semibold mb-1.5 pb-1 border-b border-border/50\">\n {key.replace(/_/g, \" \")} [{arr.length}]\n </div>\n <div className=\"pl-3 ml-1 border-l border-border/40\">\n {arr.map((item, i) => {\n const itemType = getValueType(item);\n if (itemType === \"object\") {\n return (\n <ParameterSection\n key={i}\n name={`${i}`}\n data={item as Record<string, ParameterValue>}\n depth={1}\n />\n );\n }\n return (\n <div key={i} className=\"text-[11px] font-mono text-foreground py-0.5\">\n [{i}] {formatValue(item)}\n </div>\n );\n })}\n </div>\n </div>\n );\n }\n return (\n <ParameterSection\n key={key}\n name={key}\n data={value as Record<string, ParameterValue>}\n depth={0}\n />\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 <div className=\"rounded border border-border/60 bg-card/20 p-3 overflow-hidden\">\n {topLeaves.length > 0 && (\n <div className={cn(topNested.length > 0 && \"mb-4 pb-3 border-b border-border/30\")}>\n <ParameterGrid entries={topLeaves} />\n </div>\n )}\n\n {useColumns ? (\n <div\n className=\"grid gap-6\"\n style={{ gridTemplateColumns: `repeat(${columns.length}, 1fr)` }}\n >\n {columns.map((columnEntries, colIndex) => (\n <div key={colIndex} className={cn(\n colIndex > 0 && \"border-l border-border/30 pl-6\"\n )}>\n {columnEntries.map(renderNestedEntry)}\n </div>\n ))}\n </div>\n ) : (\n topNested.map(renderNestedEntry)\n )}\n </div>\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, keys }: ParameterTableProps) {\n const { \"*\": routePath = \"\" } = useParams();\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 (everything before the first glob character)\n const baseDir = hasGlob\n ? resolvedPath.split(/[*?\\[]/, 1)[0].replace(/\\/$/, \"\") || \".\"\n : null;\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 // Debug logging\n console.log('[ParameterTable]', {\n original: path,\n resolved: resolvedPath,\n baseDir,\n allConfigFiles: allFiles.map(f => f.path),\n matched: paths\n });\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} keys={keys} />;\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 // Render a table for each matching file horizontally\n // Break out of content width when there are multiple tables\n const count = matchingPaths.length;\n const breakoutClass = count >= 3\n ? 'w-[90vw] ml-[calc(-45vw+50%)]'\n : count === 2\n ? 'w-[75vw] ml-[calc(-37.5vw+50%)]'\n : '';\n\n const scrollRef = useRef<HTMLDivElement>(null);\n usePreventSwipeNavigation(scrollRef);\n\n return (\n <div ref={scrollRef} className={`my-6 flex gap-4 overflow-x-auto overscroll-x-contain pb-2 ${breakoutClass}`}>\n {matchingPaths.map((filePath) => (\n <div key={filePath} className=\"flex-none min-w-[300px] max-w-[400px]\">\n <SingleParameterTable\n path={filePath}\n keys={keys}\n label={filePath.split('/').pop() || filePath}\n withMargin={false}\n />\n </div>\n ))}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -65,9 +65,7 @@ function formatNumber(value) {
65
65
  if (Math.abs(value) < 1e-4 && value !== 0) return value.toExponential(1);
66
66
  if (Math.abs(value) >= 1e4) return value.toExponential(1);
67
67
  if (Number.isInteger(value)) return value.toString();
68
- const str = value.toString();
69
- if (str.length > 6) return value.toPrecision(4);
70
- return str;
68
+ return value.toFixed(1);
71
69
  }
72
70
  function formatValue(value) {
73
71
  if (value === null) return "null";
@@ -1 +1 @@
1
- {"version":3,"file":"parameter-utils.js","sources":["../../../src/lib/parameter-utils.ts"],"sourcesContent":["import { load } from \"js-yaml\";\n\nexport type ParameterValue = string | number | boolean | null | ParameterValue[] | { [key: string]: ParameterValue };\n\n/**\n * Extract a value from nested data using a jq-like path.\n * Supports:\n * - .foo.bar → nested keys\n * - .foo[0] → array index\n * - .foo[] → all array elements (returns array)\n */\nexport function extractPath(data: ParameterValue, path: string): ParameterValue | undefined {\n if (!path || path === \".\") return data;\n\n const cleanPath = path.startsWith(\".\") ? path.slice(1) : path;\n if (!cleanPath) return data;\n\n const segments: Array<{ type: \"key\" | \"index\" | \"all\"; value: string | number }> = [];\n let current = \"\";\n let i = 0;\n\n while (i < cleanPath.length) {\n const char = cleanPath[i];\n\n if (char === \".\") {\n if (current) {\n segments.push({ type: \"key\", value: current });\n current = \"\";\n }\n i++;\n } else if (char === \"[\") {\n if (current) {\n segments.push({ type: \"key\", value: current });\n current = \"\";\n }\n const closeIdx = cleanPath.indexOf(\"]\", i);\n if (closeIdx === -1) return undefined;\n const inner = cleanPath.slice(i + 1, closeIdx);\n if (inner === \"\") {\n segments.push({ type: \"all\", value: 0 });\n } else {\n const idx = parseInt(inner, 10);\n if (isNaN(idx)) return undefined;\n segments.push({ type: \"index\", value: idx });\n }\n i = closeIdx + 1;\n } else {\n current += char;\n i++;\n }\n }\n if (current) {\n segments.push({ type: \"key\", value: current });\n }\n\n let result: ParameterValue | undefined = data;\n\n for (const seg of segments) {\n if (result === null || result === undefined) return undefined;\n\n if (seg.type === \"key\") {\n if (typeof result !== \"object\" || Array.isArray(result)) return undefined;\n result = (result as Record<string, ParameterValue>)[seg.value as string];\n } else if (seg.type === \"index\") {\n if (!Array.isArray(result)) return undefined;\n result = result[seg.value as number];\n } else if (seg.type === \"all\") {\n if (!Array.isArray(result)) return undefined;\n // Return the array itself for further processing\n }\n }\n\n return result;\n}\n\nexport function getValueType(value: ParameterValue): \"string\" | \"number\" | \"boolean\" | \"null\" | \"array\" | \"object\" {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"array\";\n if (typeof value === \"object\") return \"object\";\n if (typeof value === \"boolean\") return \"boolean\";\n if (typeof value === \"number\") return \"number\";\n return \"string\";\n}\n\nexport function formatNumber(value: number): string {\n if (Math.abs(value) < 0.0001 && value !== 0) return value.toExponential(1);\n if (Math.abs(value) >= 10000) return value.toExponential(1);\n if (Number.isInteger(value)) return value.toString();\n const str = value.toString();\n if (str.length > 6) return value.toPrecision(4);\n return str;\n}\n\nexport function formatValue(value: ParameterValue): string {\n if (value === null) return \"null\";\n if (typeof value === \"boolean\") return value ? \"true\" : \"false\";\n if (typeof value === \"number\") return formatNumber(value);\n if (Array.isArray(value)) return `[${value.length}]`;\n if (typeof value === \"object\") return \"{...}\";\n return String(value);\n}\n\n/**\n * Parse YAML or JSON content based on file extension.\n */\nexport function parseConfigFile(content: string, path: string): Record<string, ParameterValue> | null {\n if (path.endsWith(\".yaml\") || path.endsWith(\".yml\")) {\n try {\n return load(content) as Record<string, ParameterValue>;\n } catch {\n return null;\n }\n }\n\n if (path.endsWith(\".json\")) {\n try {\n return JSON.parse(content) as Record<string, ParameterValue>;\n } catch {\n return null;\n }\n }\n\n return null;\n}\n\n/**\n * Derive a label from a jq-like keyPath.\n * E.g., \".base.N_E\" → \"N_E\"\n */\nexport function deriveLabelFromPath(keyPath: string): string {\n const cleanPath = keyPath.startsWith(\".\") ? keyPath.slice(1) : keyPath;\n const parts = cleanPath.split(\".\");\n return parts[parts.length - 1].replace(/\\[\\d+\\]/g, \"\");\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"parameter-utils.js","sources":["../../../src/lib/parameter-utils.ts"],"sourcesContent":["import { load } from \"js-yaml\";\n\nexport type ParameterValue = string | number | boolean | null | ParameterValue[] | { [key: string]: ParameterValue };\n\n/**\n * Extract a value from nested data using a jq-like path.\n * Supports:\n * - .foo.bar → nested keys\n * - .foo[0] → array index\n * - .foo[] → all array elements (returns array)\n */\nexport function extractPath(data: ParameterValue, path: string): ParameterValue | undefined {\n if (!path || path === \".\") return data;\n\n const cleanPath = path.startsWith(\".\") ? path.slice(1) : path;\n if (!cleanPath) return data;\n\n const segments: Array<{ type: \"key\" | \"index\" | \"all\"; value: string | number }> = [];\n let current = \"\";\n let i = 0;\n\n while (i < cleanPath.length) {\n const char = cleanPath[i];\n\n if (char === \".\") {\n if (current) {\n segments.push({ type: \"key\", value: current });\n current = \"\";\n }\n i++;\n } else if (char === \"[\") {\n if (current) {\n segments.push({ type: \"key\", value: current });\n current = \"\";\n }\n const closeIdx = cleanPath.indexOf(\"]\", i);\n if (closeIdx === -1) return undefined;\n const inner = cleanPath.slice(i + 1, closeIdx);\n if (inner === \"\") {\n segments.push({ type: \"all\", value: 0 });\n } else {\n const idx = parseInt(inner, 10);\n if (isNaN(idx)) return undefined;\n segments.push({ type: \"index\", value: idx });\n }\n i = closeIdx + 1;\n } else {\n current += char;\n i++;\n }\n }\n if (current) {\n segments.push({ type: \"key\", value: current });\n }\n\n let result: ParameterValue | undefined = data;\n\n for (const seg of segments) {\n if (result === null || result === undefined) return undefined;\n\n if (seg.type === \"key\") {\n if (typeof result !== \"object\" || Array.isArray(result)) return undefined;\n result = (result as Record<string, ParameterValue>)[seg.value as string];\n } else if (seg.type === \"index\") {\n if (!Array.isArray(result)) return undefined;\n result = result[seg.value as number];\n } else if (seg.type === \"all\") {\n if (!Array.isArray(result)) return undefined;\n // Return the array itself for further processing\n }\n }\n\n return result;\n}\n\nexport function getValueType(value: ParameterValue): \"string\" | \"number\" | \"boolean\" | \"null\" | \"array\" | \"object\" {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"array\";\n if (typeof value === \"object\") return \"object\";\n if (typeof value === \"boolean\") return \"boolean\";\n if (typeof value === \"number\") return \"number\";\n return \"string\";\n}\n\nexport function formatNumber(value: number): string {\n if (Math.abs(value) < 0.0001 && value !== 0) return value.toExponential(1);\n if (Math.abs(value) >= 10000) return value.toExponential(1);\n if (Number.isInteger(value)) return value.toString();\n return value.toFixed(1);\n}\n\nexport function formatValue(value: ParameterValue): string {\n if (value === null) return \"null\";\n if (typeof value === \"boolean\") return value ? \"true\" : \"false\";\n if (typeof value === \"number\") return formatNumber(value);\n if (Array.isArray(value)) return `[${value.length}]`;\n if (typeof value === \"object\") return \"{...}\";\n return String(value);\n}\n\n/**\n * Parse YAML or JSON content based on file extension.\n */\nexport function parseConfigFile(content: string, path: string): Record<string, ParameterValue> | null {\n if (path.endsWith(\".yaml\") || path.endsWith(\".yml\")) {\n try {\n return load(content) as Record<string, ParameterValue>;\n } catch {\n return null;\n }\n }\n\n if (path.endsWith(\".json\")) {\n try {\n return JSON.parse(content) as Record<string, ParameterValue>;\n } catch {\n return null;\n }\n }\n\n return null;\n}\n\n/**\n * Derive a label from a jq-like keyPath.\n * E.g., \".base.N_E\" → \"N_E\"\n */\nexport function deriveLabelFromPath(keyPath: string): string {\n const cleanPath = keyPath.startsWith(\".\") ? keyPath.slice(1) : keyPath;\n const parts = cleanPath.split(\".\");\n return parts[parts.length - 1].replace(/\\[\\d+\\]/g, \"\");\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veslx",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -392,7 +392,7 @@ export const posts = import.meta.glob(['@content/**/*.mdx', '@content/**/*.md'],
392
392
  export const allMdx = import.meta.glob(['@content/**/*.mdx', '@content/**/*.md']);
393
393
  export const slides = import.meta.glob(['@content/**/SLIDES.mdx', '@content/**/SLIDES.md', '@content/**/*.slides.mdx', '@content/**/*.slides.md']);
394
394
 
395
- // All files for directory tree building (web-compatible files only)
395
+ // All files for directory tree building
396
396
  export const files = import.meta.glob([
397
397
  '@content/**/*.mdx',
398
398
  '@content/**/*.md',
@@ -407,6 +407,9 @@ export const files = import.meta.glob([
407
407
  '@content/**/*.svg',
408
408
  '@content/**/*.webp',
409
409
  '@content/**/*.css',
410
+ '@content/**/*.yaml',
411
+ '@content/**/*.yml',
412
+ '@content/**/*.json',
410
413
  ], { eager: false });
411
414
 
412
415
  // Frontmatter extracted at build time (no MDX execution required)
@@ -431,6 +434,36 @@ export const modules = import.meta.glob(['@content/**/*.mdx', '@content/**/*.md'
431
434
  if (configPath && fs.existsSync(configPath)) {
432
435
  server.watcher.add(configPath)
433
436
  }
437
+
438
+ // Watch content directory for all file changes (add, delete, change)
439
+ server.watcher.add(dir)
440
+
441
+ // File extensions that should trigger a full reload
442
+ const watchedExtensions = ['.mdx', '.md', '.yaml', '.yml', '.json', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.tsx', '.ts', '.jsx', '.js', '.css']
443
+
444
+ const handleContentChange = (filePath: string, event: 'add' | 'unlink' | 'change') => {
445
+ // Check if the file is in the content directory
446
+ if (!filePath.startsWith(dir)) return
447
+
448
+ // Check if it's a watched file type
449
+ const ext = path.extname(filePath).toLowerCase()
450
+ if (!watchedExtensions.includes(ext)) return
451
+
452
+ console.log(`[veslx] Content ${event}: ${path.relative(dir, filePath)}`)
453
+
454
+ // Invalidate the virtual content module so frontmatters are re-extracted
455
+ const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_MODULE_ID)
456
+ if (mod) {
457
+ server.moduleGraph.invalidateModule(mod)
458
+ }
459
+
460
+ // Full reload to pick up new/deleted files
461
+ server.ws.send({ type: 'full-reload' })
462
+ }
463
+
464
+ server.watcher.on('add', (filePath) => handleContentChange(filePath, 'add'))
465
+ server.watcher.on('unlink', (filePath) => handleContentChange(filePath, 'unlink'))
466
+ server.watcher.on('change', (filePath) => handleContentChange(filePath, 'change'))
434
467
  },
435
468
 
436
469
  handleHotUpdate({ file, server }) {
@@ -448,6 +481,16 @@ export const modules = import.meta.glob(['@content/**/*.mdx', '@content/**/*.md'
448
481
  return [] // Prevent default HMR handling
449
482
  }
450
483
  }
484
+
485
+ // Check if the changed file is in the content directory
486
+ // Return empty array to prevent default HMR - we handle it in configureServer
487
+ if (file.startsWith(dir)) {
488
+ const watchedExtensions = ['.mdx', '.md', '.yaml', '.yml', '.json', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.tsx', '.ts', '.jsx', '.js', '.css']
489
+ const ext = path.extname(file).toLowerCase()
490
+ if (watchedExtensions.includes(ext)) {
491
+ return [] // Prevent default HMR, we already handle this via watcher events
492
+ }
493
+ }
451
494
  },
452
495
  configurePreviewServer(server) {
453
496
  // Add middleware for preview server too
@@ -1,4 +1,4 @@
1
- import { useMemo } from "react";
1
+ import { useMemo, useRef, useEffect } from "react";
2
2
  import { Image } from "lucide-react";
3
3
  import { Lightbox, LightboxImage } from "@/components/gallery/components/lightbox";
4
4
  import { useGalleryImages } from "./hooks/use-gallery-images";
@@ -7,6 +7,34 @@ import { LoadingImage } from "./components/loading-image";
7
7
  import { FigureHeader } from "./components/figure-header";
8
8
  import { FigureCaption } from "./components/figure-caption";
9
9
 
10
+ /**
11
+ * Hook to prevent horizontal scroll from triggering browser back/forward gestures.
12
+ * Captures wheel events and prevents default when at scroll boundaries.
13
+ */
14
+ function usePreventSwipeNavigation(ref: React.RefObject<HTMLElement | null>) {
15
+ useEffect(() => {
16
+ const el = ref.current;
17
+ if (!el) return;
18
+
19
+ const handleWheel = (e: WheelEvent) => {
20
+ // Only handle horizontal scrolling
21
+ if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;
22
+
23
+ const { scrollLeft, scrollWidth, clientWidth } = el;
24
+ const atLeftEdge = scrollLeft <= 0;
25
+ const atRightEdge = scrollLeft + clientWidth >= scrollWidth - 1;
26
+
27
+ // Prevent default if trying to scroll past boundaries
28
+ if ((atLeftEdge && e.deltaX < 0) || (atRightEdge && e.deltaX > 0)) {
29
+ e.preventDefault();
30
+ }
31
+ };
32
+
33
+ el.addEventListener('wheel', handleWheel, { passive: false });
34
+ return () => el.removeEventListener('wheel', handleWheel);
35
+ }, [ref]);
36
+ }
37
+
10
38
  function getImageLabel(path: string): string {
11
39
  const filename = path.split('/').pop() || path;
12
40
  return filename
@@ -51,6 +79,8 @@ export default function Gallery({
51
79
  });
52
80
 
53
81
  const lightbox = useLightbox(paths.length);
82
+ const scrollRef = useRef<HTMLDivElement>(null);
83
+ usePreventSwipeNavigation(scrollRef);
54
84
 
55
85
  const images: LightboxImage[] = useMemo(() =>
56
86
  paths.map(p => ({ src: getImageUrl(p), label: getImageLabel(p) })),
@@ -98,6 +128,7 @@ export default function Gallery({
98
128
  const imageElement = (index: number, img: LightboxImage, className?: string) => (
99
129
  <div
100
130
  key={index}
131
+ title={img.label}
101
132
  className={`aspect-square overflow-hidden rounded-sm bg-muted/10 cursor-pointer group ${className || ''}`}
102
133
  onClick={() => lightbox.open(index)}
103
134
  >
@@ -141,10 +172,11 @@ export default function Gallery({
141
172
  {images.map((img, index) => imageElement(index, img, 'flex-1'))}
142
173
  </div>
143
174
  ) : (
144
- <div className="flex gap-3 overflow-x-auto pb-4">
175
+ <div ref={scrollRef} className="flex gap-3 overflow-x-auto overscroll-x-contain pb-4">
145
176
  {images.map((img, index) => (
146
177
  <div
147
178
  key={index}
179
+ title={img.label}
148
180
  className="flex-none w-[30%] min-w-[250px] aspect-square overflow-hidden rounded-sm bg-muted/10 cursor-pointer group"
149
181
  onClick={() => lightbox.open(index)}
150
182
  >
@@ -1,6 +1,8 @@
1
- import { useFileContent } from "../../plugin/src/client";
2
- import { useMemo, useState } from "react";
1
+ import { useFileContent, useDirectory } from "../../plugin/src/client";
2
+ import { useMemo, useState, useRef, useEffect } from "react";
3
+ import { useParams } from "react-router-dom";
3
4
  import { cn } from "@/lib/utils";
5
+ import { minimatch } from "minimatch";
4
6
  import {
5
7
  type ParameterValue,
6
8
  extractPath,
@@ -8,6 +10,67 @@ import {
8
10
  formatValue,
9
11
  parseConfigFile,
10
12
  } from "@/lib/parameter-utils";
13
+ import { FileEntry, DirectoryEntry } from "../../plugin/src/lib";
14
+
15
+ /**
16
+ * Hook to prevent horizontal scroll from triggering browser back/forward gestures.
17
+ */
18
+ function usePreventSwipeNavigation(ref: React.RefObject<HTMLElement | null>) {
19
+ useEffect(() => {
20
+ const el = ref.current;
21
+ if (!el) return;
22
+
23
+ const handleWheel = (e: WheelEvent) => {
24
+ if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;
25
+
26
+ const { scrollLeft, scrollWidth, clientWidth } = el;
27
+ const atLeftEdge = scrollLeft <= 0;
28
+ const atRightEdge = scrollLeft + clientWidth >= scrollWidth - 1;
29
+
30
+ if ((atLeftEdge && e.deltaX < 0) || (atRightEdge && e.deltaX > 0)) {
31
+ e.preventDefault();
32
+ }
33
+ };
34
+
35
+ el.addEventListener('wheel', handleWheel, { passive: false });
36
+ return () => el.removeEventListener('wheel', handleWheel);
37
+ }, [ref]);
38
+ }
39
+
40
+ // Check if a path contains glob patterns
41
+ function isGlobPattern(path: string): boolean {
42
+ return path.includes('*') || path.includes('?') || path.includes('[');
43
+ }
44
+
45
+ // Recursively collect all config files from a directory tree
46
+ function collectAllConfigFiles(entry: DirectoryEntry | FileEntry): FileEntry[] {
47
+ if (entry.type === "file") {
48
+ if (entry.name.match(/\.(yaml|yml|json)$/i)) {
49
+ return [entry];
50
+ }
51
+ return [];
52
+ }
53
+ const files: FileEntry[] = [];
54
+ for (const child of entry.children || []) {
55
+ files.push(...collectAllConfigFiles(child));
56
+ }
57
+ return files;
58
+ }
59
+
60
+ // Sort paths numerically
61
+ function sortPathsNumerically(paths: string[]): void {
62
+ paths.sort((a, b) => {
63
+ const nums = (s: string) => (s.match(/\d+/g) || []).map(Number);
64
+ const na = nums(a);
65
+ const nb = nums(b);
66
+ const len = Math.max(na.length, nb.length);
67
+ for (let i = 0; i < len; i++) {
68
+ const diff = (na[i] ?? 0) - (nb[i] ?? 0);
69
+ if (diff !== 0) return diff;
70
+ }
71
+ return a.localeCompare(b);
72
+ });
73
+ }
11
74
 
12
75
  /**
13
76
  * Build a filtered data object from an array of jq-like paths.
@@ -184,7 +247,7 @@ function ParameterSection({
184
247
  );
185
248
  }
186
249
 
187
- interface ParameterTableProps {
250
+ interface SingleParameterTableProps {
188
251
  /** Path to the YAML or JSON file */
189
252
  path: string;
190
253
  /**
@@ -195,6 +258,19 @@ interface ParameterTableProps {
195
258
  * - [".default_inputs", ".base.dt"] → show default_inputs section and dt from base
196
259
  */
197
260
  keys?: string[];
261
+ /** Optional label to show above the table */
262
+ label?: string;
263
+ /** Whether to include vertical margin (default true) */
264
+ withMargin?: boolean;
265
+ }
266
+
267
+ interface ParameterTableProps {
268
+ /** Path to the YAML or JSON file, supports glob patterns like "*.yaml" */
269
+ path: string;
270
+ /**
271
+ * Optional array of jq-like paths to filter which parameters to show.
272
+ */
273
+ keys?: string[];
198
274
  }
199
275
 
200
276
  /**
@@ -282,20 +358,34 @@ function splitIntoColumns<T extends [string, ParameterValue]>(
282
358
  return columns;
283
359
  }
284
360
 
285
- export function ParameterTable({ path, keys }: ParameterTableProps) {
361
+ function SingleParameterTable({ path, keys, label, withMargin = true }: SingleParameterTableProps) {
286
362
  const { content, loading, error } = useFileContent(path);
287
363
 
288
- const parsed = useMemo(() => {
289
- if (!content) return null;
364
+ const { parsed, parseError } = useMemo(() => {
365
+ if (!content) return { parsed: null, parseError: 'no content' };
290
366
 
291
367
  const data = parseConfigFile(content, path);
292
- if (!data) return null;
368
+ if (!data) {
369
+ // Check why parsing failed
370
+ if (!path.match(/\.(yaml|yml|json)$/i)) {
371
+ return { parsed: null, parseError: `unsupported file type` };
372
+ }
373
+ // Check if content looks like HTML (404 page)
374
+ if (content.trim().startsWith('<!') || content.trim().startsWith('<html')) {
375
+ return { parsed: null, parseError: `file not found` };
376
+ }
377
+ return { parsed: null, parseError: `invalid ${path.split('.').pop()} syntax` };
378
+ }
293
379
 
294
380
  if (keys && keys.length > 0) {
295
- return filterData(data, keys);
381
+ const filtered = filterData(data, keys);
382
+ if (Object.keys(filtered).length === 0) {
383
+ return { parsed: null, parseError: `keys not found: ${keys.join(', ')}` };
384
+ }
385
+ return { parsed: filtered, parseError: null };
296
386
  }
297
387
 
298
- return data;
388
+ return { parsed: data, parseError: null };
299
389
  }, [content, path, keys]);
300
390
 
301
391
  if (loading) {
@@ -319,8 +409,11 @@ export function ParameterTable({ path, keys }: ParameterTableProps) {
319
409
 
320
410
  if (!parsed) {
321
411
  return (
322
- <div className="my-6 p-3 rounded border border-border/50 bg-card/30">
323
- <p className="text-[11px] font-mono text-muted-foreground">unable to parse config</p>
412
+ <div className={cn("p-3 rounded border border-border/50 bg-card/30", withMargin && "my-6")}>
413
+ <p className="text-[11px] font-mono text-muted-foreground">
414
+ {label && <span className="text-foreground/60">{label}: </span>}
415
+ {parseError || 'unable to parse'}
416
+ </p>
324
417
  </div>
325
418
  );
326
419
  }
@@ -390,7 +483,12 @@ export function ParameterTable({ path, keys }: ParameterTableProps) {
390
483
  };
391
484
 
392
485
  return (
393
- <div className="my-6 not-prose">
486
+ <div className={cn("not-prose", withMargin && "my-6")}>
487
+ {label && (
488
+ <div className="text-[11px] font-mono text-muted-foreground mb-1.5 truncate" title={label}>
489
+ {label}
490
+ </div>
491
+ )}
394
492
  <div className="rounded border border-border/60 bg-card/20 p-3 overflow-hidden">
395
493
  {topLeaves.length > 0 && (
396
494
  <div className={cn(topNested.length > 0 && "mb-4 pb-3 border-b border-border/30")}>
@@ -418,3 +516,116 @@ export function ParameterTable({ path, keys }: ParameterTableProps) {
418
516
  </div>
419
517
  );
420
518
  }
519
+
520
+ /**
521
+ * ParameterTable component that displays YAML/JSON config files.
522
+ * Supports glob patterns in the path prop to show multiple files.
523
+ */
524
+ export function ParameterTable({ path, keys }: ParameterTableProps) {
525
+ const { "*": routePath = "" } = useParams();
526
+
527
+ // Get current directory from route
528
+ const currentDir = routePath
529
+ .replace(/\/?[^/]+\.mdx$/i, "")
530
+ .replace(/\/$/, "")
531
+ || ".";
532
+
533
+ // Resolve relative paths
534
+ let resolvedPath = path;
535
+ if (path?.startsWith("./")) {
536
+ const relativePart = path.slice(2);
537
+ resolvedPath = currentDir === "." ? relativePart : `${currentDir}/${relativePart}`;
538
+ } else if (path && !path.startsWith("/") && !path.includes("/") && !isGlobPattern(path)) {
539
+ resolvedPath = currentDir === "." ? path : `${currentDir}/${path}`;
540
+ }
541
+
542
+ // Check if this is a glob pattern
543
+ const hasGlob = isGlobPattern(resolvedPath);
544
+
545
+ // For glob patterns, get the base directory (everything before the first glob character)
546
+ const baseDir = hasGlob
547
+ ? resolvedPath.split(/[*?\[]/, 1)[0].replace(/\/$/, "") || "."
548
+ : null;
549
+
550
+ const { directory } = useDirectory(baseDir || ".");
551
+
552
+ // Find matching files for glob patterns
553
+ const matchingPaths = useMemo(() => {
554
+ if (!hasGlob || !directory) return [];
555
+
556
+ const allFiles = collectAllConfigFiles(directory);
557
+ const paths = allFiles
558
+ .map(f => f.path)
559
+ .filter(p => minimatch(p, resolvedPath, { matchBase: true }));
560
+
561
+ sortPathsNumerically(paths);
562
+
563
+ // Debug logging
564
+ console.log('[ParameterTable]', {
565
+ original: path,
566
+ resolved: resolvedPath,
567
+ baseDir,
568
+ allConfigFiles: allFiles.map(f => f.path),
569
+ matched: paths
570
+ });
571
+
572
+ return paths;
573
+ }, [hasGlob, directory, resolvedPath, path, baseDir]);
574
+
575
+ // If not a glob pattern, just render the single table
576
+ if (!hasGlob) {
577
+ return <SingleParameterTable path={resolvedPath} keys={keys} />;
578
+ }
579
+
580
+ // Loading state for glob patterns
581
+ if (!directory) {
582
+ return (
583
+ <div className="my-6 p-4 rounded border border-border/50 bg-card/30">
584
+ <div className="flex items-center gap-2 text-muted-foreground/60">
585
+ <div className="w-3 h-3 border border-current border-t-transparent rounded-full animate-spin" />
586
+ <span className="text-[11px] font-mono">loading parameters...</span>
587
+ </div>
588
+ </div>
589
+ );
590
+ }
591
+
592
+ // No matches
593
+ if (matchingPaths.length === 0) {
594
+ return (
595
+ <div className="my-6 p-3 rounded border border-border/50 bg-card/30">
596
+ <p className="text-[11px] font-mono text-muted-foreground">
597
+ no files matching: {resolvedPath}
598
+ <br />
599
+ <span className="text-muted-foreground/50">(base dir: {baseDir}, original: {path})</span>
600
+ </p>
601
+ </div>
602
+ );
603
+ }
604
+
605
+ // Render a table for each matching file horizontally
606
+ // Break out of content width when there are multiple tables
607
+ const count = matchingPaths.length;
608
+ const breakoutClass = count >= 3
609
+ ? 'w-[90vw] ml-[calc(-45vw+50%)]'
610
+ : count === 2
611
+ ? 'w-[75vw] ml-[calc(-37.5vw+50%)]'
612
+ : '';
613
+
614
+ const scrollRef = useRef<HTMLDivElement>(null);
615
+ usePreventSwipeNavigation(scrollRef);
616
+
617
+ return (
618
+ <div ref={scrollRef} className={`my-6 flex gap-4 overflow-x-auto overscroll-x-contain pb-2 ${breakoutClass}`}>
619
+ {matchingPaths.map((filePath) => (
620
+ <div key={filePath} className="flex-none min-w-[300px] max-w-[400px]">
621
+ <SingleParameterTable
622
+ path={filePath}
623
+ keys={keys}
624
+ label={filePath.split('/').pop() || filePath}
625
+ withMargin={false}
626
+ />
627
+ </div>
628
+ ))}
629
+ </div>
630
+ );
631
+ }
package/src/index.css CHANGED
@@ -46,8 +46,8 @@
46
46
  --popover: 0 0% 100%;
47
47
  --popover-foreground: 240 10% 10%;
48
48
 
49
- /* Cyan/teal accent - technical, precise */
50
- --primary: 190 80% 42%;
49
+ /* Zinc accent - clean, neutral */
50
+ --primary: 240 5% 34%;
51
51
  --primary-foreground: 0 0% 100%;
52
52
 
53
53
  --secondary: 220 14% 96%;
@@ -62,18 +62,18 @@
62
62
 
63
63
  --border: 220 13% 90%;
64
64
  --input: 220 13% 90%;
65
- --ring: 190 80% 42%;
65
+ --ring: 240 5% 34%;
66
66
  --radius: 0.375rem;
67
67
 
68
68
  /* Sidebar */
69
69
  --sidebar-background: 220 14% 98%;
70
70
  --sidebar-foreground: 220 10% 40%;
71
- --sidebar-primary: 190 80% 42%;
71
+ --sidebar-primary: 240 5% 34%;
72
72
  --sidebar-primary-foreground: 0 0% 100%;
73
73
  --sidebar-accent: 220 14% 95%;
74
74
  --sidebar-accent-foreground: 240 10% 10%;
75
75
  --sidebar-border: 220 13% 91%;
76
- --sidebar-ring: 190 80% 42%;
76
+ --sidebar-ring: 240 5% 34%;
77
77
 
78
78
  /* Layout widths - generous for galleries */
79
79
  --content-width: 44rem;
@@ -92,9 +92,9 @@
92
92
  --popover: 0 0% 10%;
93
93
  --popover-foreground: 0 0% 93%;
94
94
 
95
- /* Cyan accent - crisp against zinc */
96
- --primary: 175 70% 45%;
97
- --primary-foreground: 0 0% 98%;
95
+ /* Zinc accent - clean, neutral */
96
+ --primary: 240 5% 65%;
97
+ --primary-foreground: 0 0% 7%;
98
98
 
99
99
  --secondary: 0 0% 14%;
100
100
  --secondary-foreground: 0 0% 80%;
@@ -108,17 +108,17 @@
108
108
 
109
109
  --border: 0 0% 22%;
110
110
  --input: 0 0% 22%;
111
- --ring: 175 70% 45%;
111
+ --ring: 240 5% 65%;
112
112
 
113
113
  /* Sidebar - zinc */
114
114
  --sidebar-background: 0 0% 10%;
115
115
  --sidebar-foreground: 0 0% 65%;
116
- --sidebar-primary: 175 70% 45%;
117
- --sidebar-primary-foreground: 0 0% 98%;
116
+ --sidebar-primary: 240 5% 65%;
117
+ --sidebar-primary-foreground: 0 0% 7%;
118
118
  --sidebar-accent: 0 0% 13%;
119
119
  --sidebar-accent-foreground: 0 0% 93%;
120
120
  --sidebar-border: 0 0% 19%;
121
- --sidebar-ring: 175 70% 45%;
121
+ --sidebar-ring: 240 5% 65%;
122
122
  }
123
123
 
124
124
  @layer base {
@@ -86,9 +86,7 @@ export function formatNumber(value: number): string {
86
86
  if (Math.abs(value) < 0.0001 && value !== 0) return value.toExponential(1);
87
87
  if (Math.abs(value) >= 10000) return value.toExponential(1);
88
88
  if (Number.isInteger(value)) return value.toString();
89
- const str = value.toString();
90
- if (str.length > 6) return value.toPrecision(4);
91
- return str;
89
+ return value.toFixed(1);
92
90
  }
93
91
 
94
92
  export function formatValue(value: ParameterValue): string {