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.
Files changed (83) hide show
  1. package/README.md +3 -0
  2. package/bin/lib/import-config.ts +13 -0
  3. package/bin/lib/init.ts +31 -0
  4. package/bin/lib/serve.ts +35 -0
  5. package/bin/lib/start.ts +40 -0
  6. package/bin/lib/stop.ts +24 -0
  7. package/bin/vesl.ts +41 -0
  8. package/components.json +20 -0
  9. package/eslint.config.js +23 -0
  10. package/index.html +17 -0
  11. package/package.json +89 -0
  12. package/plugin/README.md +21 -0
  13. package/plugin/package.json +26 -0
  14. package/plugin/src/cli.ts +30 -0
  15. package/plugin/src/client.tsx +224 -0
  16. package/plugin/src/lib.ts +268 -0
  17. package/plugin/src/plugin.ts +109 -0
  18. package/postcss.config.js +5 -0
  19. package/public/logo_dark.png +0 -0
  20. package/public/logo_light.png +0 -0
  21. package/src/App.tsx +21 -0
  22. package/src/components/front-matter.tsx +53 -0
  23. package/src/components/gallery/components/figure-caption.tsx +15 -0
  24. package/src/components/gallery/components/figure-header.tsx +20 -0
  25. package/src/components/gallery/components/lightbox.tsx +106 -0
  26. package/src/components/gallery/components/loading-image.tsx +48 -0
  27. package/src/components/gallery/hooks/use-gallery-images.ts +103 -0
  28. package/src/components/gallery/hooks/use-lightbox.ts +40 -0
  29. package/src/components/gallery/index.tsx +134 -0
  30. package/src/components/gallery/lib/render-math-in-text.tsx +47 -0
  31. package/src/components/header.tsx +68 -0
  32. package/src/components/index.ts +5 -0
  33. package/src/components/loading.tsx +16 -0
  34. package/src/components/mdx-components.tsx +163 -0
  35. package/src/components/mode-toggle.tsx +44 -0
  36. package/src/components/page-error.tsx +59 -0
  37. package/src/components/parameter-badge.tsx +78 -0
  38. package/src/components/parameter-table.tsx +420 -0
  39. package/src/components/post-list.tsx +148 -0
  40. package/src/components/running-bar.tsx +21 -0
  41. package/src/components/runtime-mdx.tsx +82 -0
  42. package/src/components/slide.tsx +11 -0
  43. package/src/components/theme-provider.tsx +6 -0
  44. package/src/components/ui/badge.tsx +36 -0
  45. package/src/components/ui/breadcrumb.tsx +115 -0
  46. package/src/components/ui/button.tsx +56 -0
  47. package/src/components/ui/card.tsx +79 -0
  48. package/src/components/ui/carousel.tsx +260 -0
  49. package/src/components/ui/dropdown-menu.tsx +198 -0
  50. package/src/components/ui/input.tsx +22 -0
  51. package/src/components/ui/kbd.tsx +22 -0
  52. package/src/components/ui/select.tsx +158 -0
  53. package/src/components/ui/separator.tsx +29 -0
  54. package/src/components/ui/shadcn-io/code-block/index.tsx +620 -0
  55. package/src/components/ui/shadcn-io/code-block/server.tsx +63 -0
  56. package/src/components/ui/sheet.tsx +140 -0
  57. package/src/components/ui/sidebar.tsx +771 -0
  58. package/src/components/ui/skeleton.tsx +15 -0
  59. package/src/components/ui/spinner.tsx +16 -0
  60. package/src/components/ui/tooltip.tsx +28 -0
  61. package/src/components/welcome.tsx +21 -0
  62. package/src/hooks/use-key-bindings.ts +72 -0
  63. package/src/hooks/use-mobile.tsx +19 -0
  64. package/src/index.css +279 -0
  65. package/src/lib/constants.ts +10 -0
  66. package/src/lib/format-date.tsx +6 -0
  67. package/src/lib/format-file-size.ts +10 -0
  68. package/src/lib/parameter-utils.ts +134 -0
  69. package/src/lib/utils.ts +6 -0
  70. package/src/main.tsx +10 -0
  71. package/src/pages/home.tsx +39 -0
  72. package/src/pages/post.tsx +65 -0
  73. package/src/pages/slides.tsx +173 -0
  74. package/tailwind.config.js +136 -0
  75. package/test-content/.vesl.json +49 -0
  76. package/test-content/README.md +33 -0
  77. package/test-content/test-post/README.mdx +7 -0
  78. package/test-content/test-slides/SLIDES.mdx +8 -0
  79. package/tsconfig.app.json +32 -0
  80. package/tsconfig.json +15 -0
  81. package/tsconfig.node.json +25 -0
  82. package/vesl.config.ts +4 -0
  83. package/vite.config.ts +54 -0
@@ -0,0 +1,15 @@
1
+ import { cn } from "@/lib/utils"
2
+
3
+ function Skeleton({
4
+ className,
5
+ ...props
6
+ }: React.HTMLAttributes<HTMLDivElement>) {
7
+ return (
8
+ <div
9
+ className={cn("animate-pulse rounded-md bg-muted", className)}
10
+ {...props}
11
+ />
12
+ )
13
+ }
14
+
15
+ export { Skeleton }
@@ -0,0 +1,16 @@
1
+ import { Loader2Icon } from "lucide-react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
6
+ return (
7
+ <Loader2Icon
8
+ role="status"
9
+ aria-label="Loading"
10
+ className={cn("size-4 animate-spin", className)}
11
+ {...props}
12
+ />
13
+ )
14
+ }
15
+
16
+ export { Spinner }
@@ -0,0 +1,28 @@
1
+ import * as React from "react"
2
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const TooltipProvider = TooltipPrimitive.Provider
7
+
8
+ const Tooltip = TooltipPrimitive.Root
9
+
10
+ const TooltipTrigger = TooltipPrimitive.Trigger
11
+
12
+ const TooltipContent = React.forwardRef<
13
+ React.ElementRef<typeof TooltipPrimitive.Content>,
14
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
15
+ >(({ className, sideOffset = 4, ...props }, ref) => (
16
+ <TooltipPrimitive.Content
17
+ ref={ref}
18
+ sideOffset={sideOffset}
19
+ className={cn(
20
+ "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ ))
26
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName
27
+
28
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,21 @@
1
+
2
+ export function Welcome() {
3
+ return (
4
+ <div className="text-muted-foreground">
5
+ <pre
6
+ className="not-prose text-xs md:text-sm rounded"
7
+ >
8
+ {/* Rubifont on figlet */}
9
+ {`
10
+ ▗▄▄▖▗▄▄▄▖▗▖ ▗▖ ▗▄▄▖▗▖ ▗▄▖ ▗▄▄▖
11
+ ▐▌ ▐▌ █ ▐▛▚▖▐▌▐▌ ▐▌ ▐▌ ▐▌▐▌ ▐▌
12
+ ▐▛▀▘ █ ▐▌ ▝▜▌▐▌▝▜▌▐▌ ▐▛▀▜▌▐▛▀▚▖
13
+ ▐▌ ▗▄█▄▖▐▌ ▐▌▝▚▄▞▘▐▙▄▄▖▐▌ ▐▌▐▙▄▞▘
14
+ `}
15
+ </pre>
16
+ <div className="text-xs mt-2 font-mono">
17
+ PingLab is a repository for running experiments on PING Spiking Neural Networks.
18
+ </div>
19
+ </div>
20
+ )
21
+ }
@@ -0,0 +1,72 @@
1
+ import { useEffect } from "react";
2
+
3
+ type KeyModifiers = {
4
+ meta?: boolean;
5
+ ctrl?: boolean;
6
+ alt?: boolean;
7
+ shift?: boolean;
8
+ };
9
+
10
+ type KeyBinding = {
11
+ key: string | string[];
12
+ action: () => void;
13
+ modifiers?: KeyModifiers;
14
+ preventDefault?: boolean;
15
+ };
16
+
17
+ type UseKeyBindingsOptions = {
18
+ enabled?: boolean | (() => boolean);
19
+ ignoreModifiers?: boolean;
20
+ };
21
+
22
+ export function useKeyBindings(
23
+ bindings: KeyBinding[],
24
+ options: UseKeyBindingsOptions = {}
25
+ ) {
26
+ const { enabled = true, ignoreModifiers = true } = options;
27
+
28
+ useEffect(() => {
29
+ const handleKeyDown = (e: KeyboardEvent) => {
30
+ const isEnabled = typeof enabled === "function" ? enabled() : enabled;
31
+ if (!isEnabled) return;
32
+
33
+ // By default, ignore events with modifier keys unless explicitly specified
34
+ if (ignoreModifiers && (e.metaKey || e.ctrlKey || e.altKey)) {
35
+ // Check if any binding explicitly wants this modifier combination
36
+ const hasModifierBinding = bindings.some((b) => {
37
+ const mods = b.modifiers || {};
38
+ return mods.meta || mods.ctrl || mods.alt;
39
+ });
40
+ if (!hasModifierBinding) return;
41
+ }
42
+
43
+ for (const binding of bindings) {
44
+ const keys = Array.isArray(binding.key) ? binding.key : [binding.key];
45
+ const mods = binding.modifiers || {};
46
+
47
+ // Check if key matches
48
+ if (!keys.includes(e.key)) continue;
49
+
50
+ // Check modifiers if specified
51
+ if (mods.meta !== undefined && mods.meta !== e.metaKey) continue;
52
+ if (mods.ctrl !== undefined && mods.ctrl !== e.ctrlKey) continue;
53
+ if (mods.alt !== undefined && mods.alt !== e.altKey) continue;
54
+ if (mods.shift !== undefined && mods.shift !== e.shiftKey) continue;
55
+
56
+ // If ignoreModifiers is true and no modifiers specified, skip if any modifier is pressed
57
+ if (ignoreModifiers && !binding.modifiers && (e.metaKey || e.ctrlKey || e.altKey)) {
58
+ continue;
59
+ }
60
+
61
+ if (binding.preventDefault !== false) {
62
+ e.preventDefault();
63
+ }
64
+ binding.action();
65
+ return;
66
+ }
67
+ };
68
+
69
+ window.addEventListener("keydown", handleKeyDown);
70
+ return () => window.removeEventListener("keydown", handleKeyDown);
71
+ }, [bindings, enabled, ignoreModifiers]);
72
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener("change", onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener("change", onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
package/src/index.css ADDED
@@ -0,0 +1,279 @@
1
+
2
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:ital,wght@0,300;0,400;0,500;0,600;1,400&display=swap');
3
+
4
+ @import "tailwindcss";
5
+
6
+ @theme {
7
+ --color-background: hsl(var(--background));
8
+ --color-foreground: hsl(var(--foreground));
9
+ --color-card: hsl(var(--card));
10
+ --color-card-foreground: hsl(var(--card-foreground));
11
+ --color-popover: hsl(var(--popover));
12
+ --color-popover-foreground: hsl(var(--popover-foreground));
13
+ --color-primary: hsl(var(--primary));
14
+ --color-primary-foreground: hsl(var(--primary-foreground));
15
+ --color-secondary: hsl(var(--secondary));
16
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
17
+ --color-muted: hsl(var(--muted));
18
+ --color-muted-foreground: hsl(var(--muted-foreground));
19
+ --color-accent: hsl(var(--accent));
20
+ --color-accent-foreground: hsl(var(--accent-foreground));
21
+ --color-destructive: hsl(var(--destructive));
22
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
23
+ --color-border: hsl(var(--border));
24
+ --color-input: hsl(var(--input));
25
+ --color-ring: hsl(var(--ring));
26
+ --color-sidebar-background: hsl(var(--sidebar-background));
27
+ --color-sidebar-foreground: hsl(var(--sidebar-foreground));
28
+ --color-sidebar-primary: hsl(var(--sidebar-primary));
29
+ --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
30
+ --color-sidebar-accent: hsl(var(--sidebar-accent));
31
+ --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
32
+ --color-sidebar-border: hsl(var(--sidebar-border));
33
+ --color-sidebar-ring: hsl(var(--sidebar-ring));
34
+ --radius-sm: calc(var(--radius) - 4px);
35
+ --radius-md: calc(var(--radius) - 2px);
36
+ --radius-lg: var(--radius);
37
+ --radius-xl: calc(var(--radius) + 4px);
38
+ }
39
+
40
+ :root {
41
+ /* Clean light theme - lab notebook feel */
42
+ --background: 0 0% 100%;
43
+ --foreground: 240 10% 10%;
44
+ --card: 0 0% 99%;
45
+ --card-foreground: 240 10% 10%;
46
+ --popover: 0 0% 100%;
47
+ --popover-foreground: 240 10% 10%;
48
+
49
+ /* Cyan/teal accent - technical, precise */
50
+ --primary: 190 80% 42%;
51
+ --primary-foreground: 0 0% 100%;
52
+
53
+ --secondary: 220 14% 96%;
54
+ --secondary-foreground: 240 10% 20%;
55
+ --muted: 220 14% 95%;
56
+ --muted-foreground: 220 10% 45%;
57
+ --accent: 220 14% 96%;
58
+ --accent-foreground: 240 10% 10%;
59
+
60
+ --destructive: 0 84% 60%;
61
+ --destructive-foreground: 0 0% 100%;
62
+
63
+ --border: 220 13% 90%;
64
+ --input: 220 13% 90%;
65
+ --ring: 190 80% 42%;
66
+ --radius: 0.375rem;
67
+
68
+ /* Sidebar */
69
+ --sidebar-background: 220 14% 98%;
70
+ --sidebar-foreground: 220 10% 40%;
71
+ --sidebar-primary: 190 80% 42%;
72
+ --sidebar-primary-foreground: 0 0% 100%;
73
+ --sidebar-accent: 220 14% 95%;
74
+ --sidebar-accent-foreground: 240 10% 10%;
75
+ --sidebar-border: 220 13% 91%;
76
+ --sidebar-ring: 190 80% 42%;
77
+
78
+ /* Layout widths - generous for galleries */
79
+ --content-width: 44rem;
80
+ --content-width-wide: 72rem;
81
+ --gallery-width: 90vw;
82
+ --prose-width: 60ch;
83
+ --page-padding: 2rem;
84
+ }
85
+
86
+ .dark {
87
+ /* Dark theme - zinc neutrals, no blue tint */
88
+ --background: 0 0% 7%;
89
+ --foreground: 0 0% 93%;
90
+ --card: 0 0% 10%;
91
+ --card-foreground: 0 0% 93%;
92
+ --popover: 0 0% 10%;
93
+ --popover-foreground: 0 0% 93%;
94
+
95
+ /* Cyan accent - crisp against zinc */
96
+ --primary: 175 70% 45%;
97
+ --primary-foreground: 0 0% 98%;
98
+
99
+ --secondary: 0 0% 14%;
100
+ --secondary-foreground: 0 0% 80%;
101
+ --muted: 0 0% 16%;
102
+ --muted-foreground: 0 0% 50%;
103
+ --accent: 0 0% 14%;
104
+ --accent-foreground: 0 0% 93%;
105
+
106
+ --destructive: 0 65% 55%;
107
+ --destructive-foreground: 0 0% 98%;
108
+
109
+ --border: 0 0% 22%;
110
+ --input: 0 0% 22%;
111
+ --ring: 175 70% 45%;
112
+
113
+ /* Sidebar - zinc */
114
+ --sidebar-background: 0 0% 10%;
115
+ --sidebar-foreground: 0 0% 65%;
116
+ --sidebar-primary: 175 70% 45%;
117
+ --sidebar-primary-foreground: 0 0% 98%;
118
+ --sidebar-accent: 0 0% 13%;
119
+ --sidebar-accent-foreground: 0 0% 93%;
120
+ --sidebar-border: 0 0% 19%;
121
+ --sidebar-ring: 175 70% 45%;
122
+ }
123
+
124
+ @layer base {
125
+ * {
126
+ border-color: hsl(var(--border));
127
+ }
128
+
129
+ html {
130
+ scroll-behavior: smooth;
131
+ }
132
+
133
+ body {
134
+ background-color: hsl(var(--background));
135
+ color: hsl(var(--foreground));
136
+ -webkit-font-smoothing: antialiased;
137
+ -moz-osx-font-smoothing: grayscale;
138
+ font-feature-settings: "cv11", "ss01", "liga", "calt";
139
+ line-height: 1.7;
140
+ letter-spacing: -0.01em;
141
+ }
142
+
143
+ /* Headings - clean sans-serif */
144
+ h1, h2, h3, h4, h5, h6 {
145
+ font-family: ui-sans-serif, system-ui, sans-serif;
146
+ font-weight: 600;
147
+ letter-spacing: -0.02em;
148
+ line-height: 1.25;
149
+ }
150
+
151
+ h1 {
152
+ font-weight: 700;
153
+ letter-spacing: -0.025em;
154
+ }
155
+
156
+ :focus-visible {
157
+ outline: none;
158
+ box-shadow: 0 0 0 1px hsl(var(--ring)), 0 0 0 2px hsl(var(--background));
159
+ }
160
+
161
+ ::selection {
162
+ background-color: hsl(var(--primary) / 0.15);
163
+ color: hsl(var(--foreground));
164
+ }
165
+
166
+ /* Prose styling */
167
+ .prose {
168
+ line-height: 1.8;
169
+ letter-spacing: 0.005em;
170
+ max-width: var(--prose-width);
171
+ }
172
+
173
+ .prose p {
174
+ margin-bottom: 1.5em;
175
+ }
176
+
177
+ .prose a {
178
+ color: hsl(var(--primary));
179
+ text-decoration: underline;
180
+ text-underline-offset: 2px;
181
+ text-decoration-color: hsl(var(--primary) / 0.4);
182
+ transition: text-decoration-color 0.15s;
183
+ }
184
+
185
+ .prose a:hover {
186
+ text-decoration-color: hsl(var(--primary));
187
+ }
188
+
189
+ /* Keyboard hint styling */
190
+ kbd {
191
+ font-family: ui-monospace, monospace;
192
+ font-size: 0.75em;
193
+ padding: 0.125rem 0.375rem;
194
+ border-radius: 0.25rem;
195
+ border: 1px solid hsl(var(--border));
196
+ background-color: hsl(var(--muted) / 0.5);
197
+ }
198
+
199
+ /* Code blocks - terminal aesthetic */
200
+ pre {
201
+ font-family: ui-monospace, monospace;
202
+ font-size: 0.875rem;
203
+ letter-spacing: 0;
204
+ }
205
+
206
+ code {
207
+ font-family: ui-monospace, monospace;
208
+ font-size: 0.875em;
209
+ letter-spacing: 0;
210
+ }
211
+
212
+ /* Scrollbar styling */
213
+ ::-webkit-scrollbar {
214
+ width: 8px;
215
+ height: 8px;
216
+ }
217
+
218
+ ::-webkit-scrollbar-track {
219
+ background: transparent;
220
+ }
221
+
222
+ ::-webkit-scrollbar-thumb {
223
+ background-color: hsl(var(--border));
224
+ border-radius: 9999px;
225
+ }
226
+
227
+ ::-webkit-scrollbar-thumb:hover {
228
+ background-color: hsl(var(--muted-foreground) / 0.3);
229
+ }
230
+
231
+ /* Custom cursor for interactive elements */
232
+ button, a, [role="button"] {
233
+ cursor: pointer;
234
+ }
235
+ }
236
+
237
+ /* Animation utilities */
238
+ @keyframes fade-in {
239
+ from {
240
+ opacity: 0;
241
+ transform: translateY(8px);
242
+ }
243
+ to {
244
+ opacity: 1;
245
+ transform: translateY(0);
246
+ }
247
+ }
248
+
249
+ @keyframes fade-in-slow {
250
+ from {
251
+ opacity: 0;
252
+ }
253
+ to {
254
+ opacity: 1;
255
+ }
256
+ }
257
+
258
+ @keyframes slide-up {
259
+ from {
260
+ opacity: 0;
261
+ transform: translateY(20px);
262
+ }
263
+ to {
264
+ opacity: 1;
265
+ transform: translateY(0);
266
+ }
267
+ }
268
+
269
+ .animate-fade-in {
270
+ animation: fade-in 0.4s ease-out forwards;
271
+ }
272
+
273
+ .animate-fade-in-slow {
274
+ animation: fade-in-slow 0.6s ease-out forwards;
275
+ }
276
+
277
+ .animate-slide-up {
278
+ animation: slide-up 0.5s ease-out forwards;
279
+ }
@@ -0,0 +1,10 @@
1
+ // Data attributes for fullscreen components
2
+ export const FULLSCREEN_DATA_ATTR = "data-fullscreen-active";
3
+
4
+ /**
5
+ * Check if any fullscreen component (Gallery or Slides) is currently active.
6
+ * Components using fullscreen should set the data-fullscreen-active attribute.
7
+ */
8
+ export function isFullscreenActive(): boolean {
9
+ return document.querySelector(`[${FULLSCREEN_DATA_ATTR}="true"]`) !== null;
10
+ }
@@ -0,0 +1,6 @@
1
+
2
+ import { format } from 'date-fns'
3
+
4
+ export function formatDate(date: string | number | Date): string {
5
+ return format(new Date(date), "dd MMM, yy")
6
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Format a file size in bytes to a human-readable string.
3
+ */
4
+ export function formatFileSize(bytes: number): string {
5
+ if (bytes === 0) return "0 B";
6
+ const k = 1024;
7
+ const sizes = ["B", "KB", "MB", "GB"];
8
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
9
+ return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
10
+ }
@@ -0,0 +1,134 @@
1
+ import { load } from "js-yaml";
2
+
3
+ export type ParameterValue = string | number | boolean | null | ParameterValue[] | { [key: string]: ParameterValue };
4
+
5
+ /**
6
+ * Extract a value from nested data using a jq-like path.
7
+ * Supports:
8
+ * - .foo.bar → nested keys
9
+ * - .foo[0] → array index
10
+ * - .foo[] → all array elements (returns array)
11
+ */
12
+ export function extractPath(data: ParameterValue, path: string): ParameterValue | undefined {
13
+ if (!path || path === ".") return data;
14
+
15
+ const cleanPath = path.startsWith(".") ? path.slice(1) : path;
16
+ if (!cleanPath) return data;
17
+
18
+ const segments: Array<{ type: "key" | "index" | "all"; value: string | number }> = [];
19
+ let current = "";
20
+ let i = 0;
21
+
22
+ while (i < cleanPath.length) {
23
+ const char = cleanPath[i];
24
+
25
+ if (char === ".") {
26
+ if (current) {
27
+ segments.push({ type: "key", value: current });
28
+ current = "";
29
+ }
30
+ i++;
31
+ } else if (char === "[") {
32
+ if (current) {
33
+ segments.push({ type: "key", value: current });
34
+ current = "";
35
+ }
36
+ const closeIdx = cleanPath.indexOf("]", i);
37
+ if (closeIdx === -1) return undefined;
38
+ const inner = cleanPath.slice(i + 1, closeIdx);
39
+ if (inner === "") {
40
+ segments.push({ type: "all", value: 0 });
41
+ } else {
42
+ const idx = parseInt(inner, 10);
43
+ if (isNaN(idx)) return undefined;
44
+ segments.push({ type: "index", value: idx });
45
+ }
46
+ i = closeIdx + 1;
47
+ } else {
48
+ current += char;
49
+ i++;
50
+ }
51
+ }
52
+ if (current) {
53
+ segments.push({ type: "key", value: current });
54
+ }
55
+
56
+ let result: ParameterValue | undefined = data;
57
+
58
+ for (const seg of segments) {
59
+ if (result === null || result === undefined) return undefined;
60
+
61
+ if (seg.type === "key") {
62
+ if (typeof result !== "object" || Array.isArray(result)) return undefined;
63
+ result = (result as Record<string, ParameterValue>)[seg.value as string];
64
+ } else if (seg.type === "index") {
65
+ if (!Array.isArray(result)) return undefined;
66
+ result = result[seg.value as number];
67
+ } else if (seg.type === "all") {
68
+ if (!Array.isArray(result)) return undefined;
69
+ // Return the array itself for further processing
70
+ }
71
+ }
72
+
73
+ return result;
74
+ }
75
+
76
+ export function getValueType(value: ParameterValue): "string" | "number" | "boolean" | "null" | "array" | "object" {
77
+ if (value === null) return "null";
78
+ if (Array.isArray(value)) return "array";
79
+ if (typeof value === "object") return "object";
80
+ if (typeof value === "boolean") return "boolean";
81
+ if (typeof value === "number") return "number";
82
+ return "string";
83
+ }
84
+
85
+ export function formatNumber(value: number): string {
86
+ if (Math.abs(value) < 0.0001 && value !== 0) return value.toExponential(1);
87
+ if (Math.abs(value) >= 10000) return value.toExponential(1);
88
+ if (Number.isInteger(value)) return value.toString();
89
+ const str = value.toString();
90
+ if (str.length > 6) return value.toPrecision(4);
91
+ return str;
92
+ }
93
+
94
+ export function formatValue(value: ParameterValue): string {
95
+ if (value === null) return "null";
96
+ if (typeof value === "boolean") return value ? "true" : "false";
97
+ if (typeof value === "number") return formatNumber(value);
98
+ if (Array.isArray(value)) return `[${value.length}]`;
99
+ if (typeof value === "object") return "{...}";
100
+ return String(value);
101
+ }
102
+
103
+ /**
104
+ * Parse YAML or JSON content based on file extension.
105
+ */
106
+ export function parseConfigFile(content: string, path: string): Record<string, ParameterValue> | null {
107
+ if (path.endsWith(".yaml") || path.endsWith(".yml")) {
108
+ try {
109
+ return load(content) as Record<string, ParameterValue>;
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ if (path.endsWith(".json")) {
116
+ try {
117
+ return JSON.parse(content) as Record<string, ParameterValue>;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ return null;
124
+ }
125
+
126
+ /**
127
+ * Derive a label from a jq-like keyPath.
128
+ * E.g., ".base.N_E" → "N_E"
129
+ */
130
+ export function deriveLabelFromPath(keyPath: string): string {
131
+ const cleanPath = keyPath.startsWith(".") ? keyPath.slice(1) : keyPath;
132
+ const parts = cleanPath.split(".");
133
+ return parts[parts.length - 1].replace(/\[\d+\]/g, "");
134
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
@@ -0,0 +1,39 @@
1
+ import { useParams } from "react-router-dom"
2
+ import { useDirectory } from "../../plugin/src/client";
3
+ import Loading from "@/components/loading";
4
+ import PostList from "@/components/post-list";
5
+ import { ErrorDisplay } from "@/components/page-error";
6
+ import { RunningBar } from "@/components/running-bar";
7
+ import { Header } from "@/components/header";
8
+
9
+ export function Home() {
10
+ const { "*": path = "." } = useParams();
11
+ const { directory, loading, error } = useDirectory(path)
12
+
13
+ if (error) {
14
+ return <ErrorDisplay error={error} path={path} />;
15
+ }
16
+
17
+ if (loading) {
18
+ return (
19
+ <Loading />
20
+ )
21
+ }
22
+
23
+ return (
24
+ <div className="flex min-h-screen flex-col bg-background noise-overlay">
25
+ <RunningBar />
26
+ <Header />
27
+ <main className="flex-1 mx-auto w-full max-w-[var(--content-width)] px-[var(--page-padding)]">
28
+ <title>{`Pinglab ${path}`}</title>
29
+ <main className="flex flex-col gap-6 mb-32 mt-32">
30
+ {directory && (
31
+ <div className="animate-fade-in">
32
+ <PostList directory={directory}/>
33
+ </div>
34
+ )}
35
+ </main>
36
+ </main>
37
+ </div>
38
+ )
39
+ }