feat: add react api and shadcn foundation

This commit is contained in:
vince 2026-06-19 23:35:46 +02:00
parent f6d6d39a7b
commit a71d66b1b8
21 changed files with 1524 additions and 36 deletions

View file

@ -1,5 +1,13 @@
import { useEffect, useState } from "react";
import type { DashboardRuntimeState } from "$lib/server/dashboard";
const loadingDashboardState: DashboardRuntimeState = {
state: "loading",
title: "Loading Dashboard",
subtitle: "Fetching active model",
message: "Waiting for the active dashboard document.",
};
export function AppStateView({
dashboard,
}: {
@ -19,14 +27,45 @@ export function AppStateView({
}
export default function App() {
return (
<AppStateView
dashboard={{
state: "loading",
title: "Loading Dashboard",
subtitle: "Fetching active model",
message: "Waiting for the active dashboard document.",
}}
/>
);
const [dashboard, setDashboard] =
useState<DashboardRuntimeState>(loadingDashboardState);
useEffect(() => {
let cancelled = false;
let refreshTimer: number | undefined;
async function loadDashboard() {
const response = await fetch("/api/dashboard");
const nextDashboard = (await response.json()) as DashboardRuntimeState;
if (cancelled) return;
setDashboard(nextDashboard);
if (refreshTimer) {
window.clearInterval(refreshTimer);
refreshTimer = undefined;
}
const refreshIntervalSeconds =
nextDashboard.state === "ready"
? nextDashboard.document.metadata.refreshIntervalSeconds
: undefined;
if (refreshIntervalSeconds) {
refreshTimer = window.setInterval(
() => void loadDashboard(),
refreshIntervalSeconds * 1000,
);
}
}
void loadDashboard();
return () => {
cancelled = true;
if (refreshTimer) window.clearInterval(refreshTimer);
};
}, []);
return <AppStateView dashboard={dashboard} />;
}

View file

@ -1 +1,145 @@
@import "tailwindcss";
@import "./lib/ui/tokens.css";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@fontsource-variable/geist";
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-heading: var(--font-sans);
--font-sans: 'Geist Variable', sans-serif;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
:root,
.dark {
--background: var(--ui-color-canvas);
--foreground: var(--ui-color-text);
--card: var(--ui-color-surface);
--card-foreground: var(--ui-color-text);
--popover: var(--ui-color-surface-raised);
--popover-foreground: var(--ui-color-text);
--primary: var(--ui-color-accent);
--primary-foreground: var(--ui-color-canvas);
--secondary: var(--ui-color-surface-raised);
--secondary-foreground: var(--ui-color-text);
--muted: var(--ui-color-surface-raised);
--muted-foreground: var(--ui-color-muted);
--accent: var(--ui-color-accent);
--accent-foreground: var(--ui-color-canvas);
--destructive: var(--ui-color-danger);
--border: rgba(244, 244, 244, 0.16);
--input: rgba(244, 244, 244, 0.18);
--ring: var(--ui-color-accent);
--chart-1: var(--ui-color-accent);
--chart-2: var(--ui-color-ok);
--chart-3: var(--ui-color-warning);
--chart-4: var(--ui-color-danger);
--chart-5: var(--ui-color-stale);
--radius: 0.5rem;
--sidebar: var(--ui-color-surface);
--sidebar-foreground: var(--ui-color-text);
--sidebar-primary: var(--ui-color-accent);
--sidebar-primary-foreground: var(--ui-color-canvas);
--sidebar-accent: var(--ui-color-surface-raised);
--sidebar-accent-foreground: var(--ui-color-text);
--sidebar-border: rgba(244, 244, 244, 0.16);
--sidebar-ring: var(--ui-color-accent);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}
html {
background: var(--ui-color-canvas);
}
body {
min-width: 320px;
min-height: 100vh;
margin: 0;
background:
linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
var(--ui-color-canvas);
background-size: 48px 48px, 48px 48px, auto;
color: var(--ui-color-text);
font-family: var(--ui-font-mono);
text-rendering: geometricPrecision;
}
button,
input,
textarea,
select {
font: inherit;
}
button:focus-visible,
a:focus-visible,
[tabindex]:focus-visible {
outline: 0;
box-shadow: var(--ui-focus-ring);
}
a {
color: inherit;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
}
}

View file

@ -0,0 +1,76 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "$lib/utils"
const alertVariants = cva(
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
className
)}
{...props}
/>
)
}
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-action"
className={cn("absolute top-2 right-2", className)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription, AlertAction }

View file

@ -0,0 +1,49 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "$lib/utils"
const badgeVariants = cva(
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
secondary:
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive:
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
outline:
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost:
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View file

@ -0,0 +1,67 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "$lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View file

@ -0,0 +1,103 @@
import * as React from "react"
import { cn } from "$lib/utils"
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-(--card-spacing)", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View file

@ -0,0 +1,29 @@
import * as React from "react"
import { Progress as ProgressPrimitive } from "radix-ui"
import { cn } from "$lib/utils"
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"relative flex h-1 w-full items-center overflow-x-hidden rounded-full bg-muted",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="size-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }

View file

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import { Separator as SeparatorPrimitive } from "radix-ui"
import { cn } from "$lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}
/>
)
}
export { Separator }

View file

@ -0,0 +1,13 @@
import { cn } from "$lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

6
src/lib/utils.ts Normal file
View file

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

84
src/server/index.ts Normal file
View file

@ -0,0 +1,84 @@
import { extname, normalize } from "node:path";
import { handleAgentDashboardRoute } from "./routes/agent-dashboard";
import { handleDashboardRoute } from "./routes/dashboard";
const host = process.env.HOST || "0.0.0.0";
const port = Number(process.env.PORT || 3000);
const distRoot = `${process.cwd()}/dist`;
const contentTypes = new Map([
[".css", "text/css; charset=utf-8"],
[".html", "text/html; charset=utf-8"],
[".js", "text/javascript; charset=utf-8"],
[".json", "application/json; charset=utf-8"],
[".svg", "image/svg+xml"],
[".wasm", "application/wasm"],
]);
export async function handleRequest(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/api/dashboard") {
if (request.method !== "GET") return methodNotAllowed(["GET"]);
return handleDashboardRoute();
}
if (url.pathname === "/api/agent/dashboard") {
if (request.method !== "POST") return methodNotAllowed(["POST"]);
return handleAgentDashboardRoute(request);
}
if (url.pathname.startsWith("/api/")) {
return Response.json({ ok: false, message: "Not found" }, { status: 404 });
}
return serveStaticAsset(url.pathname);
}
async function serveStaticAsset(pathname: string): Promise<Response> {
const safePath = normalize(pathname).replace(/^(\.\.(\/|\\|$))+/, "");
const assetPath = safePath === "/" || safePath === "." ? "/index.html" : safePath;
const file = Bun.file(`${distRoot}${assetPath}`);
if (await file.exists()) {
return new Response(file, {
headers: contentTypeHeaders(assetPath),
});
}
const fallback = Bun.file(`${distRoot}/index.html`);
if (await fallback.exists()) {
return new Response(fallback, {
headers: contentTypeHeaders(".html"),
});
}
return new Response("Build output not found", { status: 404 });
}
function methodNotAllowed(allowedMethods: string[]): Response {
return Response.json(
{ ok: false, message: "Method not allowed" },
{
status: 405,
headers: {
Allow: allowedMethods.join(", "),
},
},
);
}
function contentTypeHeaders(pathname: string): HeadersInit {
const contentType = contentTypes.get(extname(pathname));
return contentType ? { "Content-Type": contentType } : {};
}
if (import.meta.main) {
Bun.serve({
hostname: host,
port,
fetch: handleRequest,
});
console.info(`Dimension Lab website listening on http://${host}:${port}`);
}

View file

@ -0,0 +1,12 @@
import { describe, expect, test } from "vitest";
import { handleAgentDashboardRoute } from "./agent-dashboard";
describe("agent dashboard API route", () => {
test("delegates unauthorized requests to the existing agent handler", async () => {
const response = await handleAgentDashboardRoute(
new Request("http://localhost/api/agent/dashboard", { method: "POST" }),
);
expect(response.status).toBe(401);
});
});

View file

@ -0,0 +1,5 @@
import { handleAgentDashboardRequest } from "$lib/server/agent-config";
export function handleAgentDashboardRoute(request: Request): Promise<Response> {
return handleAgentDashboardRequest(request);
}

View file

@ -0,0 +1,19 @@
import { describe, expect, test } from "vitest";
import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab";
import { loadDashboardResponse } from "./dashboard";
describe("dashboard API route", () => {
test("returns ready dashboard runtime state from the existing model loader", async () => {
const response = await loadDashboardResponse({
disableLiveDatasources: true,
refreshSeedDocument: true,
seedIfEmpty: true,
});
expect(response.state).toBe("ready");
if (response.state !== "ready") throw new Error("expected ready dashboard");
expect(response.document.metadata.title).toBe(
dimensionLabDashboardFixture.metadata.title,
);
});
});

View file

@ -0,0 +1,41 @@
import {
loadDashboardRuntime,
type DashboardRuntimeOptions,
type DashboardRuntimeState,
} from "$lib/server/dashboard";
import { resolveDashboardDatasources } from "$lib/server/datasources";
export interface LoadDashboardResponseOptions
extends Pick<
DashboardRuntimeOptions,
"refreshSeedDocument" | "seedIfEmpty" | "seedDocument"
> {
disableLiveDatasources?: boolean;
}
export async function loadDashboardResponse(
options: LoadDashboardResponseOptions = {},
): Promise<DashboardRuntimeState> {
const dashboard = loadDashboardRuntime(undefined, {
refreshSeedDocument: options.refreshSeedDocument ?? true,
seedIfEmpty: options.seedIfEmpty ?? true,
seedDocument: options.seedDocument,
});
if (dashboard.state !== "ready") {
return dashboard;
}
if (options.disableLiveDatasources || process.env.DISABLE_LIVE_DATASOURCES === "1") {
return dashboard;
}
return {
...dashboard,
document: await resolveDashboardDatasources(dashboard.document),
};
}
export async function handleDashboardRoute(): Promise<Response> {
return Response.json(await loadDashboardResponse());
}