Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/frontend-kit/ui/src/components/custom/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { CustomComponent } from "./custom-component";
// Generic tab-style toggle — use anywhere a set of mutually exclusive options needs switching.
export { SegmentedControl } from "./segmented-control";
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Generic segmented control (tab-style toggle) shared across all views.
// Renders a sliding indicator that animates to the active option.
// All options are rendered at equal width; pass icon for icon+label segments.
//
// Layout note: no horizontal padding on the container — the indicator is sized
// as a percentage of the container so that translateX(100%) moves it by exactly
// one segment width. Adding horizontal padding would break that alignment because
// translateX % is relative to the element's own width, not the parent.
import type { LucideIcon } from "lucide-react";
import { cn } from "../../lib/utils";

interface Option<T extends string> {
label: string;
value: T;
icon?: LucideIcon;
}

interface SegmentedControlProps<T extends string> {
options: Option<T>[];
value: T;
onChange: (value: T) => void;
className?: string;
}

const SegmentedControl = <T extends string>({
options,
value,
onChange,
className,
}: SegmentedControlProps<T>) => {
const activeIndex = options.findIndex((o) => o.value === value);
const segmentWidth = 100 / options.length;

return (
// Outer div provides the background and visual side padding.
// The indicator lives in the inner div (no horizontal padding) so that
// width/translateX percentages stay correct — they reference that inner width.
<div
role="group"
className={cn("bg-muted rounded-lg px-1 py-0.5", className)}
>
<div className="relative flex items-center">
{/* Sliding background indicator — vertical inset only */}
<div
aria-hidden
className="bg-background absolute top-0.5 bottom-0.5 rounded-md shadow-sm transition-transform duration-200 ease-in-out"
style={{
width: `${segmentWidth}%`,
transform: `translateX(${100 * activeIndex}%)`,
}}
/>

{options.map((option) => {
const active = option.value === value;
const Icon = option.icon;
return (
<button
key={option.value}
onClick={() => !active && onChange(option.value)}
aria-pressed={active}
style={{ width: `${segmentWidth}%` }}
className={cn(
"relative z-10 flex items-center justify-center gap-1.5 rounded-md px-3 py-1 text-sm font-medium transition-colors duration-150",
active
? "text-foreground cursor-default"
: "text-muted-foreground hover:text-foreground cursor-pointer",
)}
>
{Icon && <Icon className="size-3.5 shrink-0" />}
{option.label}
</button>
);
})}
</div>
</div>
);
};

export { SegmentedControl };
2 changes: 2 additions & 0 deletions frontend/frontend-kit/ui/src/icons/arrows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ export {
ChevronLeft,
ChevronRight,
ChevronUp,
ChevronsLeftRight,
ChevronsUpDown,
ExternalLink,
Play,
RefreshCw,
TrendingDown,
Expand Down
2 changes: 1 addition & 1 deletion frontend/frontend-kit/ui/src/icons/development.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { Terminal } from "lucide-react";
export { Activity, BookOpen, Cpu, Database, GitCommit, Layers, Network, Search, Server, Terminal, Timer } from "lucide-react";
1 change: 0 additions & 1 deletion frontend/frontend-kit/ui/src/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export * from "./gaming";
export * from "./layout";
export * from "./mail";
export * from "./math";
export * from "./medical";
export * from "./notifications";
export * from "./photography";
export * from "./science";
Expand Down
2 changes: 1 addition & 1 deletion frontend/frontend-kit/ui/src/icons/math.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { Plus, X } from "lucide-react";
export { Plus } from "lucide-react";
1 change: 0 additions & 1 deletion frontend/frontend-kit/ui/src/icons/medical.ts

This file was deleted.

1 change: 1 addition & 0 deletions frontend/frontend-kit/ui/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./utils";
export * from "./typeUtils";
48 changes: 48 additions & 0 deletions frontend/frontend-kit/ui/src/lib/typeUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Color classes for ADJ measurement type badges — shared across all views.
export const typeBadgeClasses = {
float:
"bg-sky-500/20 text-sky-400 border-sky-500/40 dark:bg-sky-500/15 dark:text-sky-300",
integer:
"bg-emerald-500/20 text-emerald-500 border-emerald-500/40 dark:bg-emerald-500/15 dark:text-emerald-400",
uint:
"bg-orange-500/20 text-orange-500 border-orange-500/40 dark:bg-orange-500/15 dark:text-orange-400",
enum: "bg-violet-500/20 text-violet-500 border-violet-500/40 dark:bg-violet-500/15 dark:text-violet-400",
boolean:
"bg-amber-500/20 text-amber-500 border-amber-500/40 dark:bg-amber-500/15 dark:text-amber-400",
unknown:
"bg-slate-500/20 text-slate-400 border-slate-500/40 dark:bg-slate-500/15 dark:text-slate-300",
};

export const getTypeBadgeClass = (type: string): string => {
switch (type.toLowerCase()) {
case "float":
case "float32":
case "float64":
return typeBadgeClasses.float;

case "integer":
case "int":
case "int8":
case "int16":
case "int32":
case "int64":
return typeBadgeClasses.integer;

case "uint8":
case "uint16":
case "uint32":
case "uint64":
return typeBadgeClasses.uint;

case "string":
case "enum":
return typeBadgeClasses.enum;

case "boolean":
case "bool":
return typeBadgeClasses.boolean;

default:
return typeBadgeClasses.unknown;
}
};
82 changes: 82 additions & 0 deletions frontend/logging-view/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```bash
pnpm dev # Dev server on port 9003
pnpm build # tsc -b && vite build
pnpm lint # ESLint
pnpm preview # Preview production build
```

Run from the monorepo root targeting this workspace:

```bash
pnpm dev --filter logging-view
pnpm add <package> --filter logging-view
```

> **pnpm only** — the `preinstall` hook enforces this via `only-allow`.

## Architecture

`logging-view` is one of several frontend workspaces in the monorepo (`frontend/`). It shares infrastructure with `competition-view` and `testing-view` but is a standalone Vite+React app running on its own port.

**Current status: scaffold, not yet wired up.** The store slices, types, and layout shell were carried over from `competition-view`, but `App.tsx` doesn't yet subscribe to any topics or define routes, and some sidebar components (`ConnectionStatusGroup`, `KeyboardShortcutsHelp`) import from `../hooks/useConnections` and `../hooks/useKeyboardShortcuts`, which don't exist in this workspace yet — check `competition-view/src` for the reference implementation before assuming a missing import is a bug.

### Workspace dependencies

| Package | Alias | What it provides |
|---|---|---|
| `frontend-kit/ui` | `@workspace/ui` | shadcn/Radix UI components, custom hooks (`useWebSocket`, `useTopic`), Lucide icons |
| `frontend-kit/core` | `@workspace/core` | WebSocket utilities, `socketService`, shared business logic |

Import components from the shared packages:

```tsx
import { Button, Sidebar } from "@workspace/ui/components";
import { Plus } from "@workspace/ui/icons";
import { socketService } from "@workspace/core";
```

### State management

Single Zustand store composed of slices (`src/store/`). Only `isDarkMode` is persisted (localStorage key `competition-view-storage`). All other state is ephemeral.

| Slice | Purpose |
|---|---|
| `appSlice` | Dark mode toggle |
| `connectionsSlice` | WebSocket connection statuses (`Record<string, Connection>`) |
| `messagesSlice` | Incoming log messages, capped at 500 entries (newest first) |
| `telemetrySlice` | Flat map of latest measurement values, flattened from `TelemetryData` packets |
| `catalogSlice` | Commands catalog fetched from backend (`GET /backend/orderStructures`) |

### WebSocket integration

Use hooks from `@workspace/core`/`@workspace/ui`:

```tsx
import { useTopic, useWebSocket } from "@workspace/ui/hooks";

const { isConnected } = useWebSocket();

useTopic<TelemetryData>("podData/update", (data) => {
updateTelemetry(data);
});

socketService.post("order/send", payload);
```

Relevant topics: `podData/update`, `connection/update`, `message/update`.

### Layout structure

`AppLayout` (sidebar + header shell) wraps all page content. The sidebar is collapsible-to-icon and has dark mode toggle in the footer. Pages/views go inside the `children` slot.

### Adding icons

1. Find the icon on lucide.dev — note its **first category**.
2. Add the export to the matching category file in `frontend-kit/ui/src/icons/`.
3. Re-export from `index.ts` if the category file is new.
1 change: 1 addition & 0 deletions frontend/logging-view/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Here should be installed competition view using Vite
3 changes: 3 additions & 0 deletions frontend/logging-view/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { config } from "@workspace/eslint-config/vite";

export default config;
15 changes: 15 additions & 0 deletions frontend/logging-view/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hyperloop UPV logging view</title>
</head>

<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

</html>
39 changes: 39 additions & 0 deletions frontend/logging-view/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "logging-view",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "vite",
"dev:main": "pnpm dev",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.18",
"@types/plotly.js": "^3.0.10",
"@vitejs/plugin-react-swc": "^4.2.3",
"@workspace/core": "workspace:*",
"@workspace/ui": "workspace:*",
"lucide-react": "^0.563.0",
"plotly.js-dist": "^3.7.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router": "^7.13.0",
"tailwindcss": "^4.1.18",
"uplot": "^1.6.32",
"zustand": "^5.0.11"
},
"devDependencies": {
"@types/react": "^19.2.11",
"@types/react-dom": "^19.2.3",
"@workspace/eslint-config": "workspace:^",
"@workspace/typescript-config": "workspace:*",
"eslint": "^9.39.2",
"globals": "^17.3.0",
"typescript": "^5.9.3",
"vite": "^7.3.1"
}
}
21 changes: 21 additions & 0 deletions frontend/logging-view/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Root component. Wraps all pages in the shared AppLayout (sidebar + header).
// The active view mode (Normal / Simple) is encoded in the URL so each mode
// can have a completely independent UI. Navigating to "/" redirects to "/normal".
import { Navigate, Route, Routes } from "react-router";
import AppLayout from "./layout/AppLayout";
import NormalPage from "./pages/NormalPage";
import SimplePage from "./pages/SimplePage";

const App = () => {
return (
<AppLayout>
<Routes>
<Route index element={<Navigate to="/normal" replace />} />
<Route path="/normal" element={<NormalPage />} />
<Route path="/simple" element={<SimplePage />} />
</Routes>
</AppLayout>
);
};

export default App;
13 changes: 13 additions & 0 deletions frontend/logging-view/src/assets/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions frontend/logging-view/src/base/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# `base` folder

Includes previous concepts from which the logging-view is based

- `plot_gui_web`: Mutti's *vibecoded* app with Claude at H11
Loading