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,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,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
|
+
}
|
package/src/lib/utils.ts
ADDED
package/src/main.tsx
ADDED
|
@@ -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
|
+
}
|