veslx 0.0.1
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/README.md +3 -0
- package/bin/lib/import-config.ts +13 -0
- package/bin/lib/init.ts +31 -0
- package/bin/lib/serve.ts +35 -0
- package/bin/lib/start.ts +40 -0
- package/bin/lib/stop.ts +24 -0
- package/bin/vesl.ts +41 -0
- package/components.json +20 -0
- package/eslint.config.js +23 -0
- package/index.html +17 -0
- package/package.json +89 -0
- package/plugin/README.md +21 -0
- package/plugin/package.json +26 -0
- package/plugin/src/cli.ts +30 -0
- package/plugin/src/client.tsx +224 -0
- package/plugin/src/lib.ts +268 -0
- package/plugin/src/plugin.ts +109 -0
- package/postcss.config.js +5 -0
- package/public/logo_dark.png +0 -0
- package/public/logo_light.png +0 -0
- package/src/App.tsx +21 -0
- package/src/components/front-matter.tsx +53 -0
- package/src/components/gallery/components/figure-caption.tsx +15 -0
- package/src/components/gallery/components/figure-header.tsx +20 -0
- package/src/components/gallery/components/lightbox.tsx +106 -0
- package/src/components/gallery/components/loading-image.tsx +48 -0
- package/src/components/gallery/hooks/use-gallery-images.ts +103 -0
- package/src/components/gallery/hooks/use-lightbox.ts +40 -0
- package/src/components/gallery/index.tsx +134 -0
- package/src/components/gallery/lib/render-math-in-text.tsx +47 -0
- package/src/components/header.tsx +68 -0
- package/src/components/index.ts +5 -0
- package/src/components/loading.tsx +16 -0
- package/src/components/mdx-components.tsx +163 -0
- package/src/components/mode-toggle.tsx +44 -0
- package/src/components/page-error.tsx +59 -0
- package/src/components/parameter-badge.tsx +78 -0
- package/src/components/parameter-table.tsx +420 -0
- package/src/components/post-list.tsx +148 -0
- package/src/components/running-bar.tsx +21 -0
- package/src/components/runtime-mdx.tsx +82 -0
- package/src/components/slide.tsx +11 -0
- package/src/components/theme-provider.tsx +6 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/breadcrumb.tsx +115 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +79 -0
- package/src/components/ui/carousel.tsx +260 -0
- package/src/components/ui/dropdown-menu.tsx +198 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/kbd.tsx +22 -0
- package/src/components/ui/select.tsx +158 -0
- package/src/components/ui/separator.tsx +29 -0
- package/src/components/ui/shadcn-io/code-block/index.tsx +620 -0
- package/src/components/ui/shadcn-io/code-block/server.tsx +63 -0
- package/src/components/ui/sheet.tsx +140 -0
- package/src/components/ui/sidebar.tsx +771 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/tooltip.tsx +28 -0
- package/src/components/welcome.tsx +21 -0
- package/src/hooks/use-key-bindings.ts +72 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/index.css +279 -0
- package/src/lib/constants.ts +10 -0
- package/src/lib/format-date.tsx +6 -0
- package/src/lib/format-file-size.ts +10 -0
- package/src/lib/parameter-utils.ts +134 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/src/pages/home.tsx +39 -0
- package/src/pages/post.tsx +65 -0
- package/src/pages/slides.tsx +173 -0
- package/tailwind.config.js +136 -0
- package/test-content/.vesl.json +49 -0
- package/test-content/README.md +33 -0
- package/test-content/test-post/README.mdx +7 -0
- package/test-content/test-slides/SLIDES.mdx +8 -0
- package/tsconfig.app.json +32 -0
- package/tsconfig.json +15 -0
- package/tsconfig.node.json +25 -0
- package/vesl.config.ts +4 -0
- package/vite.config.ts +54 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useFileContent } from "../../plugin/src/client";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import {
|
|
5
|
+
type ParameterValue,
|
|
6
|
+
extractPath,
|
|
7
|
+
getValueType,
|
|
8
|
+
formatValue,
|
|
9
|
+
parseConfigFile,
|
|
10
|
+
deriveLabelFromPath,
|
|
11
|
+
} from "@/lib/parameter-utils";
|
|
12
|
+
|
|
13
|
+
interface ParameterBadgeProps {
|
|
14
|
+
/** Path to the YAML or JSON file */
|
|
15
|
+
path: string;
|
|
16
|
+
/** jq-like path to the value (e.g., ".base.N_E") */
|
|
17
|
+
keyPath: string;
|
|
18
|
+
/** Optional label override (defaults to last segment of keyPath) */
|
|
19
|
+
label?: string;
|
|
20
|
+
/** Optional unit suffix (e.g., "ms", "Hz") */
|
|
21
|
+
unit?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ParameterBadge({ path, keyPath, label, unit }: ParameterBadgeProps) {
|
|
25
|
+
const { content, loading, error } = useFileContent(path);
|
|
26
|
+
|
|
27
|
+
const { value, displayLabel } = useMemo(() => {
|
|
28
|
+
if (!content) return { value: undefined, displayLabel: "" };
|
|
29
|
+
|
|
30
|
+
const data = parseConfigFile(content, path);
|
|
31
|
+
if (!data) return { value: undefined, displayLabel: "" };
|
|
32
|
+
|
|
33
|
+
const extracted = extractPath(data, keyPath);
|
|
34
|
+
const derivedLabel = label || deriveLabelFromPath(keyPath);
|
|
35
|
+
|
|
36
|
+
return { value: extracted, displayLabel: derivedLabel };
|
|
37
|
+
}, [content, path, keyPath, label]);
|
|
38
|
+
|
|
39
|
+
if (loading) {
|
|
40
|
+
return (
|
|
41
|
+
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-muted/50 border border-border/50">
|
|
42
|
+
<span className="w-2 h-2 border border-muted-foreground/40 border-t-transparent rounded-full animate-spin" />
|
|
43
|
+
</span>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (error || value === undefined) {
|
|
48
|
+
return (
|
|
49
|
+
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-destructive/10 border border-destructive/30">
|
|
50
|
+
<span className="text-[10px] font-mono text-destructive">—</span>
|
|
51
|
+
</span>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const type = getValueType(value);
|
|
56
|
+
const formattedValue = formatValue(value);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md font-mono text-[11px]">
|
|
60
|
+
<span className="text-muted-foreground">{displayLabel}</span>
|
|
61
|
+
<span className="text-muted-foreground/40">=</span>
|
|
62
|
+
<span
|
|
63
|
+
className={cn(
|
|
64
|
+
"font-medium tabular-nums",
|
|
65
|
+
type === "number" && "text-foreground",
|
|
66
|
+
type === "string" && "text-amber-600 dark:text-amber-500",
|
|
67
|
+
type === "boolean" && "text-cyan-600 dark:text-cyan-500",
|
|
68
|
+
type === "null" && "text-muted-foreground/50",
|
|
69
|
+
type === "array" && "text-purple-600 dark:text-purple-400",
|
|
70
|
+
type === "object" && "text-purple-600 dark:text-purple-400"
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
{type === "string" ? `"${formattedValue}"` : formattedValue}
|
|
74
|
+
</span>
|
|
75
|
+
{unit && <span className="text-muted-foreground/60">{unit}</span>}
|
|
76
|
+
</span>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { useFileContent } from "../../plugin/src/client";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import {
|
|
5
|
+
type ParameterValue,
|
|
6
|
+
extractPath,
|
|
7
|
+
getValueType,
|
|
8
|
+
formatValue,
|
|
9
|
+
parseConfigFile,
|
|
10
|
+
} from "@/lib/parameter-utils";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build a filtered data object from an array of jq-like paths.
|
|
14
|
+
* Each path extracts data and places it in the result under the final key name.
|
|
15
|
+
*/
|
|
16
|
+
function filterData(
|
|
17
|
+
data: Record<string, ParameterValue>,
|
|
18
|
+
keys: string[]
|
|
19
|
+
): Record<string, ParameterValue> {
|
|
20
|
+
const result: Record<string, ParameterValue> = {};
|
|
21
|
+
|
|
22
|
+
for (const keyPath of keys) {
|
|
23
|
+
const extracted = extractPath(data, keyPath);
|
|
24
|
+
if (extracted === undefined) continue;
|
|
25
|
+
|
|
26
|
+
const cleanPath = keyPath.startsWith(".") ? keyPath.slice(1) : keyPath;
|
|
27
|
+
|
|
28
|
+
// For simple paths like .base.N_E, use "N_E" as key
|
|
29
|
+
// For paths with [], preserve more context
|
|
30
|
+
let keyName: string;
|
|
31
|
+
if (cleanPath.includes("[")) {
|
|
32
|
+
keyName = cleanPath.replace(/\[\]/g, "").replace(/\[(\d+)\]/g, "_$1");
|
|
33
|
+
} else {
|
|
34
|
+
const parts = cleanPath.split(".");
|
|
35
|
+
keyName = parts[parts.length - 1];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
result[keyName] = extracted;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Renders a flat section of key-value pairs in a dense grid
|
|
45
|
+
function ParameterGrid({ entries }: { entries: [string, ParameterValue][] }) {
|
|
46
|
+
if (entries.length === 0) return null;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="grid grid-cols-[repeat(auto-fill,minmax(180px,1fr))] gap-x-6 gap-y-px">
|
|
50
|
+
{entries.map(([key, value]) => {
|
|
51
|
+
const type = getValueType(value);
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
key={key}
|
|
55
|
+
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"
|
|
56
|
+
>
|
|
57
|
+
<span className="text-[11px] text-muted-foreground font-mono truncate">
|
|
58
|
+
{key}
|
|
59
|
+
</span>
|
|
60
|
+
<span
|
|
61
|
+
className={cn(
|
|
62
|
+
"text-[11px] font-mono tabular-nums font-medium shrink-0",
|
|
63
|
+
type === "number" && "text-foreground",
|
|
64
|
+
type === "string" && "text-amber-600 dark:text-amber-500",
|
|
65
|
+
type === "boolean" && "text-cyan-600 dark:text-cyan-500",
|
|
66
|
+
type === "null" && "text-muted-foreground/50"
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
{type === "string" ? `"${formatValue(value)}"` : formatValue(value)}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
})}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Renders a nested section with its own header
|
|
79
|
+
function ParameterSection({
|
|
80
|
+
name,
|
|
81
|
+
data,
|
|
82
|
+
depth = 0
|
|
83
|
+
}: {
|
|
84
|
+
name: string;
|
|
85
|
+
data: Record<string, ParameterValue>;
|
|
86
|
+
depth?: number;
|
|
87
|
+
}) {
|
|
88
|
+
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
89
|
+
|
|
90
|
+
const entries = Object.entries(data);
|
|
91
|
+
const leafEntries = entries.filter(([, v]) => {
|
|
92
|
+
const t = getValueType(v);
|
|
93
|
+
return t !== "object" && t !== "array";
|
|
94
|
+
});
|
|
95
|
+
const nestedEntries = entries.filter(([, v]) => {
|
|
96
|
+
const t = getValueType(v);
|
|
97
|
+
return t === "object" || t === "array";
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div className={cn(depth === 0 && "mb-4 last:mb-0")}>
|
|
102
|
+
<button
|
|
103
|
+
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
104
|
+
className={cn(
|
|
105
|
+
"flex items-center gap-2 w-full text-left group mb-1.5",
|
|
106
|
+
depth === 0 && "pb-1 border-b border-border/50"
|
|
107
|
+
)}
|
|
108
|
+
>
|
|
109
|
+
<span className={cn(
|
|
110
|
+
"text-[10px] text-muted-foreground/60 transition-transform duration-150 select-none",
|
|
111
|
+
isCollapsed && "-rotate-90"
|
|
112
|
+
)}>
|
|
113
|
+
{isCollapsed ? "+" : "-"}
|
|
114
|
+
</span>
|
|
115
|
+
|
|
116
|
+
<span className={cn(
|
|
117
|
+
"font-mono text-[11px] uppercase tracking-widest",
|
|
118
|
+
depth === 0
|
|
119
|
+
? "text-foreground/80 font-semibold"
|
|
120
|
+
: "text-muted-foreground/70"
|
|
121
|
+
)}>
|
|
122
|
+
{name.replace(/_/g, " ")}
|
|
123
|
+
</span>
|
|
124
|
+
|
|
125
|
+
<span className="text-[9px] font-mono text-muted-foreground/40 ml-auto">
|
|
126
|
+
{entries.length}
|
|
127
|
+
</span>
|
|
128
|
+
</button>
|
|
129
|
+
|
|
130
|
+
{!isCollapsed && (
|
|
131
|
+
<div className={cn(
|
|
132
|
+
depth > 0 && "pl-3 ml-1 border-l border-border/40"
|
|
133
|
+
)}>
|
|
134
|
+
{leafEntries.length > 0 && (
|
|
135
|
+
<div className={cn(nestedEntries.length > 0 && "mb-3")}>
|
|
136
|
+
<ParameterGrid entries={leafEntries} />
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{nestedEntries.map(([key, value]) => {
|
|
141
|
+
const type = getValueType(value);
|
|
142
|
+
if (type === "array") {
|
|
143
|
+
const arr = value as ParameterValue[];
|
|
144
|
+
return (
|
|
145
|
+
<div key={key} className="mb-2 last:mb-0">
|
|
146
|
+
<div className="text-[10px] font-mono text-muted-foreground/60 uppercase tracking-wider mb-1">
|
|
147
|
+
{key} [{arr.length}]
|
|
148
|
+
</div>
|
|
149
|
+
<div className="pl-3 ml-1 border-l border-border/40">
|
|
150
|
+
{arr.map((item, i) => {
|
|
151
|
+
const itemType = getValueType(item);
|
|
152
|
+
if (itemType === "object") {
|
|
153
|
+
return (
|
|
154
|
+
<ParameterSection
|
|
155
|
+
key={i}
|
|
156
|
+
name={`${i}`}
|
|
157
|
+
data={item as Record<string, ParameterValue>}
|
|
158
|
+
depth={depth + 1}
|
|
159
|
+
/>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return (
|
|
163
|
+
<div key={i} className="text-[11px] font-mono text-foreground py-0.5">
|
|
164
|
+
[{i}] {formatValue(item)}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return (
|
|
173
|
+
<ParameterSection
|
|
174
|
+
key={key}
|
|
175
|
+
name={key}
|
|
176
|
+
data={value as Record<string, ParameterValue>}
|
|
177
|
+
depth={depth + 1}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
})}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
interface ParameterTableProps {
|
|
188
|
+
/** Path to the YAML or JSON file */
|
|
189
|
+
path: string;
|
|
190
|
+
/**
|
|
191
|
+
* Optional array of jq-like paths to filter which parameters to show.
|
|
192
|
+
* Examples:
|
|
193
|
+
* - [".base.N_E", ".base.N_I"] → show only N_E and N_I from base
|
|
194
|
+
* - [".base"] → show entire base section
|
|
195
|
+
* - [".default_inputs", ".base.dt"] → show default_inputs section and dt from base
|
|
196
|
+
*/
|
|
197
|
+
keys?: string[];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Estimate the height contribution of a data structure.
|
|
202
|
+
*/
|
|
203
|
+
function estimateHeight(data: Record<string, ParameterValue>, depth = 0): number {
|
|
204
|
+
const entries = Object.entries(data);
|
|
205
|
+
let height = 0;
|
|
206
|
+
|
|
207
|
+
for (const [, value] of entries) {
|
|
208
|
+
const type = getValueType(value);
|
|
209
|
+
if (type === "object") {
|
|
210
|
+
height += 28 + estimateHeight(value as Record<string, ParameterValue>, depth + 1);
|
|
211
|
+
} else if (type === "array") {
|
|
212
|
+
const arr = value as ParameterValue[];
|
|
213
|
+
height += 28;
|
|
214
|
+
for (const item of arr) {
|
|
215
|
+
if (getValueType(item) === "object") {
|
|
216
|
+
height += 24 + estimateHeight(item as Record<string, ParameterValue>, depth + 1);
|
|
217
|
+
} else {
|
|
218
|
+
height += 24;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
height += 24;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return height;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Split entries into balanced columns based on estimated height.
|
|
231
|
+
*/
|
|
232
|
+
function splitIntoColumns<T extends [string, ParameterValue]>(
|
|
233
|
+
entries: T[],
|
|
234
|
+
numColumns: number
|
|
235
|
+
): T[][] {
|
|
236
|
+
if (numColumns <= 1) return [entries];
|
|
237
|
+
|
|
238
|
+
const entryHeights = entries.map(([, value]) => {
|
|
239
|
+
const type = getValueType(value);
|
|
240
|
+
if (type === "object") {
|
|
241
|
+
return 28 + estimateHeight(value as Record<string, ParameterValue>);
|
|
242
|
+
} else if (type === "array") {
|
|
243
|
+
const arr = value as ParameterValue[];
|
|
244
|
+
let h = 28;
|
|
245
|
+
for (const item of arr) {
|
|
246
|
+
if (getValueType(item) === "object") {
|
|
247
|
+
h += 24 + estimateHeight(item as Record<string, ParameterValue>);
|
|
248
|
+
} else {
|
|
249
|
+
h += 24;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return h;
|
|
253
|
+
}
|
|
254
|
+
return 24;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const totalHeight = entryHeights.reduce((a, b) => a + b, 0);
|
|
258
|
+
const targetPerColumn = totalHeight / numColumns;
|
|
259
|
+
|
|
260
|
+
const columns: T[][] = [];
|
|
261
|
+
let currentColumn: T[] = [];
|
|
262
|
+
let currentHeight = 0;
|
|
263
|
+
|
|
264
|
+
for (let i = 0; i < entries.length; i++) {
|
|
265
|
+
const entry = entries[i];
|
|
266
|
+
const entryHeight = entryHeights[i];
|
|
267
|
+
|
|
268
|
+
if (currentHeight >= targetPerColumn && columns.length < numColumns - 1 && currentColumn.length > 0) {
|
|
269
|
+
columns.push(currentColumn);
|
|
270
|
+
currentColumn = [];
|
|
271
|
+
currentHeight = 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
currentColumn.push(entry);
|
|
275
|
+
currentHeight += entryHeight;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (currentColumn.length > 0) {
|
|
279
|
+
columns.push(currentColumn);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return columns;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function ParameterTable({ path, keys }: ParameterTableProps) {
|
|
286
|
+
const { content, loading, error } = useFileContent(path);
|
|
287
|
+
|
|
288
|
+
const parsed = useMemo(() => {
|
|
289
|
+
if (!content) return null;
|
|
290
|
+
|
|
291
|
+
const data = parseConfigFile(content, path);
|
|
292
|
+
if (!data) return null;
|
|
293
|
+
|
|
294
|
+
if (keys && keys.length > 0) {
|
|
295
|
+
return filterData(data, keys);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return data;
|
|
299
|
+
}, [content, path, keys]);
|
|
300
|
+
|
|
301
|
+
if (loading) {
|
|
302
|
+
return (
|
|
303
|
+
<div className="my-6 p-4 rounded border border-border/50 bg-card/30">
|
|
304
|
+
<div className="flex items-center gap-2 text-muted-foreground/60">
|
|
305
|
+
<div className="w-3 h-3 border border-current border-t-transparent rounded-full animate-spin" />
|
|
306
|
+
<span className="text-[11px] font-mono">loading parameters...</span>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (error) {
|
|
313
|
+
return (
|
|
314
|
+
<div className="my-6 p-3 rounded border border-destructive/30 bg-destructive/5">
|
|
315
|
+
<p className="text-[11px] font-mono text-destructive">{error}</p>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!parsed) {
|
|
321
|
+
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>
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const entries = Object.entries(parsed);
|
|
329
|
+
|
|
330
|
+
const topLeaves = entries.filter(([, v]) => {
|
|
331
|
+
const t = getValueType(v);
|
|
332
|
+
return t !== "object" && t !== "array";
|
|
333
|
+
});
|
|
334
|
+
const topNested = entries.filter(([, v]) => {
|
|
335
|
+
const t = getValueType(v);
|
|
336
|
+
return t === "object" || t === "array";
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const estHeight = estimateHeight(parsed);
|
|
340
|
+
const HEIGHT_THRESHOLD = 500;
|
|
341
|
+
const numColumns = estHeight > HEIGHT_THRESHOLD ? Math.min(Math.ceil(estHeight / HEIGHT_THRESHOLD), 3) : 1;
|
|
342
|
+
const useColumns = numColumns > 1 && topNested.length > 1;
|
|
343
|
+
|
|
344
|
+
const columns = useColumns
|
|
345
|
+
? splitIntoColumns(topNested as [string, ParameterValue][], numColumns)
|
|
346
|
+
: [topNested];
|
|
347
|
+
|
|
348
|
+
const filename = path.split("/").pop() || path;
|
|
349
|
+
|
|
350
|
+
const renderNestedEntry = ([key, value]: [string, ParameterValue]) => {
|
|
351
|
+
const type = getValueType(value);
|
|
352
|
+
if (type === "array") {
|
|
353
|
+
const arr = value as ParameterValue[];
|
|
354
|
+
return (
|
|
355
|
+
<div key={key} className="mb-4 last:mb-0">
|
|
356
|
+
<div className="text-[11px] font-mono text-foreground/80 uppercase tracking-widest font-semibold mb-1.5 pb-1 border-b border-border/50">
|
|
357
|
+
{key.replace(/_/g, " ")} [{arr.length}]
|
|
358
|
+
</div>
|
|
359
|
+
<div className="pl-3 ml-1 border-l border-border/40">
|
|
360
|
+
{arr.map((item, i) => {
|
|
361
|
+
const itemType = getValueType(item);
|
|
362
|
+
if (itemType === "object") {
|
|
363
|
+
return (
|
|
364
|
+
<ParameterSection
|
|
365
|
+
key={i}
|
|
366
|
+
name={`${i}`}
|
|
367
|
+
data={item as Record<string, ParameterValue>}
|
|
368
|
+
depth={1}
|
|
369
|
+
/>
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return (
|
|
373
|
+
<div key={i} className="text-[11px] font-mono text-foreground py-0.5">
|
|
374
|
+
[{i}] {formatValue(item)}
|
|
375
|
+
</div>
|
|
376
|
+
);
|
|
377
|
+
})}
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return (
|
|
383
|
+
<ParameterSection
|
|
384
|
+
key={key}
|
|
385
|
+
name={key}
|
|
386
|
+
data={value as Record<string, ParameterValue>}
|
|
387
|
+
depth={0}
|
|
388
|
+
/>
|
|
389
|
+
);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
<div className="my-6 not-prose">
|
|
394
|
+
<div className="rounded border border-border/60 bg-card/20 p-3 overflow-hidden">
|
|
395
|
+
{topLeaves.length > 0 && (
|
|
396
|
+
<div className={cn(topNested.length > 0 && "mb-4 pb-3 border-b border-border/30")}>
|
|
397
|
+
<ParameterGrid entries={topLeaves} />
|
|
398
|
+
</div>
|
|
399
|
+
)}
|
|
400
|
+
|
|
401
|
+
{useColumns ? (
|
|
402
|
+
<div
|
|
403
|
+
className="grid gap-6"
|
|
404
|
+
style={{ gridTemplateColumns: `repeat(${columns.length}, 1fr)` }}
|
|
405
|
+
>
|
|
406
|
+
{columns.map((columnEntries, colIndex) => (
|
|
407
|
+
<div key={colIndex} className={cn(
|
|
408
|
+
colIndex > 0 && "border-l border-border/30 pl-6"
|
|
409
|
+
)}>
|
|
410
|
+
{columnEntries.map(renderNestedEntry)}
|
|
411
|
+
</div>
|
|
412
|
+
))}
|
|
413
|
+
</div>
|
|
414
|
+
) : (
|
|
415
|
+
topNested.map(renderNestedEntry)
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Link } from "react-router-dom";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
import { DirectoryEntry } from "../../plugin/src/lib";
|
|
4
|
+
import { findReadme, findSlides } from "../../plugin/src/client";
|
|
5
|
+
import { formatDate } from "@/lib/format-date";
|
|
6
|
+
import { ArrowRight } from "lucide-react";
|
|
7
|
+
|
|
8
|
+
export default function PostList({ directory }: { directory: DirectoryEntry }) {
|
|
9
|
+
const folders = directory.children.filter((c): c is DirectoryEntry => c.type === "directory");
|
|
10
|
+
|
|
11
|
+
if (folders.length === 0) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="py-24 text-center">
|
|
14
|
+
<p className="text-muted-foreground font-mono text-sm tracking-wide">no entries</p>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let posts = folders.map((folder) => {
|
|
20
|
+
const readme = findReadme(folder);
|
|
21
|
+
const slides = findSlides(folder);
|
|
22
|
+
return {
|
|
23
|
+
...folder,
|
|
24
|
+
readme,
|
|
25
|
+
slides,
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
posts = posts.filter((post) => {
|
|
30
|
+
return post.readme?.frontmatter?.visibility !== "hidden";
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
posts = posts.sort((a, b) => {
|
|
34
|
+
let aDate = a.readme?.frontmatter?.date ? new Date(a.readme.frontmatter.date as string) : null;
|
|
35
|
+
let bDate = b.readme?.frontmatter?.date ? new Date(b.readme.frontmatter.date as string) : null;
|
|
36
|
+
|
|
37
|
+
if (!aDate && a.slides) {
|
|
38
|
+
aDate = a.slides.frontmatter?.date ? new Date(a.slides.frontmatter.date as string) : null;
|
|
39
|
+
}
|
|
40
|
+
if (!bDate && b.slides) {
|
|
41
|
+
bDate = b.slides.frontmatter?.date ? new Date(b.slides.frontmatter.date as string) : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (aDate && bDate) {
|
|
45
|
+
return bDate.getTime() - aDate.getTime();
|
|
46
|
+
} else if (aDate) {
|
|
47
|
+
return -1;
|
|
48
|
+
} else if (bDate) {
|
|
49
|
+
return 1;
|
|
50
|
+
} else {
|
|
51
|
+
return a.name.localeCompare(b.name);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const postsGroupedByMonthAndYear: { [key: string]: typeof posts } = {};
|
|
56
|
+
posts.forEach((post) => {
|
|
57
|
+
let date = post.readme?.frontmatter?.date ? new Date(post.readme.frontmatter.date as string) : null;
|
|
58
|
+
if (!date && post.slides) {
|
|
59
|
+
date = post.slides.frontmatter?.date ? new Date(post.slides.frontmatter.date as string) : null;
|
|
60
|
+
}
|
|
61
|
+
const monthYear = date ? `${date.getFullYear()}-${date.getMonth() + 1}` : "unknown";
|
|
62
|
+
if (!postsGroupedByMonthAndYear[monthYear]) {
|
|
63
|
+
postsGroupedByMonthAndYear[monthYear] = [];
|
|
64
|
+
}
|
|
65
|
+
postsGroupedByMonthAndYear[monthYear].push(post);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="space-y-8">
|
|
70
|
+
{Object.entries(postsGroupedByMonthAndYear)
|
|
71
|
+
.sort(([a], [b]) => {
|
|
72
|
+
if (a === "unknown") return 1;
|
|
73
|
+
if (b === "unknown") return -1;
|
|
74
|
+
return b.localeCompare(a);
|
|
75
|
+
})
|
|
76
|
+
.map(([monthYear, monthPosts]) => {
|
|
77
|
+
const [year, month] = monthYear.split("-");
|
|
78
|
+
const displayDate = monthYear === "unknown"
|
|
79
|
+
? "Unknown Date"
|
|
80
|
+
: new Date(parseInt(year), parseInt(month) - 1).toLocaleDateString("en-US", {
|
|
81
|
+
year: "numeric",
|
|
82
|
+
month: "long"
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div key={monthYear}>
|
|
87
|
+
<h2 className="text-xs font-mono uppercase tracking-wider text-muted-foreground mb-3">
|
|
88
|
+
{displayDate}
|
|
89
|
+
</h2>
|
|
90
|
+
<div className="space-y-1">
|
|
91
|
+
{monthPosts.map((post) => {
|
|
92
|
+
let frontmatter = post.readme?.frontmatter;
|
|
93
|
+
|
|
94
|
+
if (!post.readme && post.slides) {
|
|
95
|
+
frontmatter = post.slides.frontmatter;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const title = (frontmatter?.title as string) || post.name;
|
|
99
|
+
const description = frontmatter?.description as string | undefined;
|
|
100
|
+
const date = frontmatter?.date ? new Date(frontmatter.date as string) : null;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Link
|
|
104
|
+
key={post.path}
|
|
105
|
+
to={(post.slides && !post.readme) ? `/${post.slides.path}` : `/${post.readme.path}`}
|
|
106
|
+
className={cn(
|
|
107
|
+
"group block py-3 px-3 -mx-3 rounded-md",
|
|
108
|
+
"transition-colors duration-150",
|
|
109
|
+
// "hover:bg-accent"
|
|
110
|
+
)}
|
|
111
|
+
>
|
|
112
|
+
<article className="flex items-start gap-4">
|
|
113
|
+
{/* Date - left side, fixed width */}
|
|
114
|
+
<time
|
|
115
|
+
dateTime={date?.toISOString()}
|
|
116
|
+
className="font-mono text-xs text-muted-foreground tabular-nums w-20 flex-shrink-0 pt-0.5"
|
|
117
|
+
>
|
|
118
|
+
{date ? formatDate(date) : <span className="text-muted-foreground/30">—</span>}
|
|
119
|
+
</time>
|
|
120
|
+
|
|
121
|
+
{/* Main content */}
|
|
122
|
+
<div className="flex-1 min-w-0">
|
|
123
|
+
<h3 className={cn(
|
|
124
|
+
"text-sm font-medium text-foreground",
|
|
125
|
+
"group-hover:underline",
|
|
126
|
+
"flex items-center gap-2"
|
|
127
|
+
)}>
|
|
128
|
+
<span>{title}</span>
|
|
129
|
+
<ArrowRight className="h-3 w-3 opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200 text-primary" />
|
|
130
|
+
</h3>
|
|
131
|
+
|
|
132
|
+
{description && (
|
|
133
|
+
<p className="text-sm text-muted-foreground line-clamp-1 mt-0.5">
|
|
134
|
+
{description}
|
|
135
|
+
</p>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</article>
|
|
139
|
+
</Link>
|
|
140
|
+
);
|
|
141
|
+
})}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
})}
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { isSimulationRunning } from "../../plugin/src/client";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export function RunningBar() {
|
|
5
|
+
const isRunning = isSimulationRunning();
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
{isRunning && (
|
|
10
|
+
// this should stay red not another color
|
|
11
|
+
<div className="sticky top-0 z-50 px-[var(--page-padding)] py-2 bg-red-500 text-primary-foreground font-mono text-xs text-center tracking-wide">
|
|
12
|
+
<span className="inline-flex items-center gap-3">
|
|
13
|
+
<span className="h-1.5 w-1.5 rounded-full bg-current animate-pulse" />
|
|
14
|
+
<span className="uppercase tracking-widest">simulation running</span>
|
|
15
|
+
<span className="text-primary-foreground/60">Page will auto-refresh on completion</span>
|
|
16
|
+
</span>
|
|
17
|
+
</div>
|
|
18
|
+
)}
|
|
19
|
+
</>
|
|
20
|
+
)
|
|
21
|
+
}
|