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 +9 -1
- package/bin/lib/start.ts +3 -0
- package/dist/client/components/gallery/index.js +23 -2
- package/dist/client/components/gallery/index.js.map +1 -1
- package/dist/client/components/parameter-table.js +155 -22
- package/dist/client/components/parameter-table.js.map +1 -1
- package/dist/client/lib/parameter-utils.js +1 -3
- package/dist/client/lib/parameter-utils.js.map +1 -1
- package/package.json +1 -1
- package/plugin/src/plugin.ts +44 -1
- package/src/components/gallery/index.tsx +34 -2
- package/src/components/parameter-table.tsx +223 -12
- package/src/index.css +12 -12
- package/src/lib/parameter-utils.ts +1 -3
package/bin/lib/serve.ts
CHANGED
|
@@ -102,7 +102,15 @@ export default async function serve(dir?: string) {
|
|
|
102
102
|
],
|
|
103
103
|
})
|
|
104
104
|
|
|
105
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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: "
|
|
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__ */
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
"div",
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
|
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
package/plugin/src/plugin.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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="
|
|
323
|
-
<p className="text-[11px] font-mono text-muted-foreground">
|
|
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="
|
|
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
|
-
/*
|
|
50
|
-
--primary:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
/*
|
|
96
|
-
--primary:
|
|
97
|
-
--primary-foreground: 0 0%
|
|
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:
|
|
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:
|
|
117
|
-
--sidebar-primary-foreground: 0 0%
|
|
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:
|
|
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
|
-
|
|
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 {
|