Initial scaffold: React 19 + Vite + Tailwind + shadcn + Vitest
Scaffolds APESS 2026 workshop platform per PRD §4.1 with strict-TDD infrastructure and the 1300-LOC ceiling enforced via ESLint. - React 19.2 + Vite 8 + TypeScript 6 baseline - Tailwind v3 + shadcn/ui (new-york style) configured (components.json) - Path alias @/* -> src/* - Zustand session store mirroring the prototype's shared.js shape (team, device, phases, harness, stats, add, submission) with sessionStorage persistence - PhaseStrip component covering the five PRD-mandated phases, wired to react-router-dom v7 with active/done/pending data-state surfaces for snapshot-friendly testing - Vitest + React Testing Library + jsdom green: 4/4 tests pass - ESLint max-lines: 1300 enforced; largest file currently 106 LOC Next: shadcn primitive install, landing page from prototype index.html, workshop screens 1-5, /admin instructor dashboard, /judge review.
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# APESS 2026 — Workshop Platform
|
||||||
|
|
||||||
|
Production web app for the **15th Asia-Pacific-Euro Summer School on Smart Structures Technology**, FabLab Torino, July 27 2026.
|
||||||
|
|
||||||
|
Guides 60+ PhD students across 15 teams from kit unboxing → ZeroClaw firmware deployment → Agent Design Document (ADD) submission in a single 5.5-hour session.
|
||||||
|
|
||||||
|
- **Live URL** (planned): https://apess.redclaw.dev
|
||||||
|
- **API** (planned): https://api.apess.redclaw.dev
|
||||||
|
- **Host node**: `zeroclaw-gw-03` (Architect)
|
||||||
|
- **PRD**: `APESS-2026-Workshop-PRD-v1.0.docx` (separate, confidential)
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **React 19** + TypeScript + Vite 8
|
||||||
|
- **Tailwind CSS v3** + **shadcn/ui** (new-york style)
|
||||||
|
- **React Router v7** for client routing
|
||||||
|
- **Zustand** for cross-screen session state (sessionStorage-persisted)
|
||||||
|
- **Vitest** + React Testing Library + jsdom for strict TDD
|
||||||
|
- **Web Serial API** for live USB serial to Arduino UNO R4 WiFi
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **Strict TDD**: every feature lands as a failing test first, then the minimum impl to make it pass.
|
||||||
|
- **1300 LOC ceiling**: enforced by ESLint `max-lines`. Split files before they exceed.
|
||||||
|
- **Path alias**: `@/` → `src/`.
|
||||||
|
- **Co-located tests**: `Foo.tsx` next to `Foo.test.tsx`.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm dev # local dev server
|
||||||
|
pnpm test # vitest run (one-shot)
|
||||||
|
pnpm test:watch # vitest watch mode
|
||||||
|
pnpm build # type-check + production build
|
||||||
|
pnpm lint # eslint (includes 1300-LOC rule)
|
||||||
|
pnpm typecheck # tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Screens (per PRD §5.1)
|
||||||
|
|
||||||
|
| Route | Screen |
|
||||||
|
|---|---|
|
||||||
|
| `/` | Landing + programme |
|
||||||
|
| `/workshop` | Team registration + device connect |
|
||||||
|
| `/workshop/setup` | Environment setup (OS-aware) |
|
||||||
|
| `/workshop/module1` | Module 1 — Sense → Reason |
|
||||||
|
| `/workshop/module2` | Module 2 — Harness engineering |
|
||||||
|
| `/workshop/add` | ADD builder + submit |
|
||||||
|
| `/lecture` | Lecture page (5 movements) |
|
||||||
|
| `/admin` | Instructor dashboard (15-card grid, WS-driven) |
|
||||||
|
| `/judge` | Judge review + score + leaderboard |
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- Firmware: `clawverse/zeroclaw` (separate repo, planned)
|
||||||
|
- Backend API: `api/` subdir in this repo (TBD)
|
||||||
|
- Infrastructure: Valhalla vault `20 Infrastructure/20 Nodes/zeroclaw-gw-03.md`
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist', 'coverage', 'node_modules']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
globals: { ...globals.browser, ...globals.node },
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'max-lines': [
|
||||||
|
'error',
|
||||||
|
{ max: 1300, skipBlankLines: false, skipComments: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="description" content="APESS 2026 Workshop — on-device agentic systems for structural intelligence. FabLab Torino, July 27." />
|
||||||
|
<title>APESS 2026 · Workshop</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&family=Newsreader:wght@400;600;700&display=swap" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "apress",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
|
"typecheck": "tsc -b --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^1.17.0",
|
||||||
|
"react": "^19.2.6",
|
||||||
|
"react-dom": "^19.2.6",
|
||||||
|
"react-router-dom": "^7.17.0",
|
||||||
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"zustand": "^5.0.14"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
"@types/node": "^24.12.3",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
|
"@vitest/ui": "^4.1.8",
|
||||||
|
"autoprefixer": "^10.5.0",
|
||||||
|
"eslint": "^10.3.0",
|
||||||
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
|
"globals": "^17.6.0",
|
||||||
|
"jsdom": "^29.1.1",
|
||||||
|
"postcss": "^8.5.15",
|
||||||
|
"tailwindcss": "^3.4.19",
|
||||||
|
"typescript": "~6.0.2",
|
||||||
|
"typescript-eslint": "^8.59.2",
|
||||||
|
"vite": "^8.0.12",
|
||||||
|
"vitest": "^4.1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+3139
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
+66
@@ -0,0 +1,66 @@
|
|||||||
|
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'
|
||||||
|
import { PhaseStrip } from '@/components/PhaseStrip'
|
||||||
|
|
||||||
|
function Landing() {
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen flex flex-col">
|
||||||
|
<header className="px-8 py-6 border-b border-border flex items-center justify-between">
|
||||||
|
<div className="font-mono text-xs tracking-widest uppercase text-muted-foreground">
|
||||||
|
APESS <span className="text-primary font-bold">2026</span>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
to="/workshop"
|
||||||
|
className="font-mono text-xs px-4 py-2 rounded-md bg-primary text-primary-foreground hover:opacity-90"
|
||||||
|
>
|
||||||
|
Enter workshop →
|
||||||
|
</Link>
|
||||||
|
</header>
|
||||||
|
<section className="flex-1 flex flex-col items-center justify-center px-8 text-center gap-6 max-w-3xl mx-auto">
|
||||||
|
<p className="font-mono text-[11px] tracking-wider uppercase text-primary">
|
||||||
|
FabLab Torino · July 27, 2026
|
||||||
|
</p>
|
||||||
|
<h1 className="text-5xl font-bold tracking-tight leading-tight">
|
||||||
|
On-device agentic systems<br />
|
||||||
|
<span className="text-primary">for structural intelligence</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-base text-muted-foreground leading-relaxed max-w-xl">
|
||||||
|
Build a ZeroClaw agent that senses, reasons, and responds autonomously on an
|
||||||
|
Arduino UNO R4 WiFi. Five-layer Agent Design Document by 19:00 CEST.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<footer className="px-8 py-4 border-t border-border font-mono text-[10px] text-muted-foreground flex justify-between">
|
||||||
|
<span>RedClaw Systems</span>
|
||||||
|
<span>apess.redclaw.dev</span>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function WorkshopShell({ title, phase }: { title: string; phase: Parameters<typeof PhaseStrip>[0]['active'] }) {
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen">
|
||||||
|
<PhaseStrip active={phase} />
|
||||||
|
<section className="px-8 py-12">
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
|
||||||
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
|
Screen under construction. See PRD §5.1 for spec.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Landing />} />
|
||||||
|
<Route path="/workshop" element={<WorkshopShell title="Team registration" phase="reg" />} />
|
||||||
|
<Route path="/workshop/setup" element={<WorkshopShell title="Environment setup" phase="setup" />} />
|
||||||
|
<Route path="/workshop/module1" element={<WorkshopShell title="Module 1 · Sense → Reason" phase="m1" />} />
|
||||||
|
<Route path="/workshop/module2" element={<WorkshopShell title="Module 2 · Harness engineering" phase="m2" />} />
|
||||||
|
<Route path="/workshop/add" element={<WorkshopShell title="ADD builder & submit" phase="add" />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
|
import { PhaseStrip } from './PhaseStrip'
|
||||||
|
import { useSession } from '@/store/session'
|
||||||
|
|
||||||
|
function renderAt(path: string, active?: Parameters<typeof PhaseStrip>[0]['active']) {
|
||||||
|
return render(
|
||||||
|
<MemoryRouter initialEntries={[path]}>
|
||||||
|
<PhaseStrip active={active} />
|
||||||
|
</MemoryRouter>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PhaseStrip', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useSession.getState().reset()
|
||||||
|
sessionStorage.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders all five PRD-mandated phases in order', () => {
|
||||||
|
renderAt('/workshop')
|
||||||
|
const phases = screen.getAllByRole('link')
|
||||||
|
expect(phases).toHaveLength(5)
|
||||||
|
expect(phases.map((p) => p.textContent)).toEqual([
|
||||||
|
expect.stringContaining('Team reg'),
|
||||||
|
expect.stringContaining('Env setup'),
|
||||||
|
expect.stringContaining('Module 1'),
|
||||||
|
expect.stringContaining('Module 2'),
|
||||||
|
expect.stringContaining('ADD · submit'),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('marks the active phase with data-state="active"', () => {
|
||||||
|
renderAt('/workshop/module1', 'm1')
|
||||||
|
const m1 = screen.getByText('Module 1').closest('a')
|
||||||
|
expect(m1).toHaveAttribute('data-state', 'active')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('marks completed phases with data-state="done" from session store', () => {
|
||||||
|
useSession.getState().completePhase('reg')
|
||||||
|
useSession.getState().completePhase('setup')
|
||||||
|
renderAt('/workshop/module1', 'm1')
|
||||||
|
expect(screen.getByText('Team reg').closest('a')).toHaveAttribute('data-state', 'done')
|
||||||
|
expect(screen.getByText('Env setup').closest('a')).toHaveAttribute('data-state', 'done')
|
||||||
|
expect(screen.getByText('Module 1').closest('a')).toHaveAttribute('data-state', 'active')
|
||||||
|
expect(screen.getByText('Module 2').closest('a')).toHaveAttribute('data-state', 'pending')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('links each phase to its workshop sub-route', () => {
|
||||||
|
renderAt('/workshop')
|
||||||
|
expect(screen.getByText('Team reg').closest('a')).toHaveAttribute('href', '/workshop')
|
||||||
|
expect(screen.getByText('Env setup').closest('a')).toHaveAttribute('href', '/workshop/setup')
|
||||||
|
expect(screen.getByText('Module 1').closest('a')).toHaveAttribute('href', '/workshop/module1')
|
||||||
|
expect(screen.getByText('Module 2').closest('a')).toHaveAttribute('href', '/workshop/module2')
|
||||||
|
expect(screen.getByText('ADD · submit').closest('a')).toHaveAttribute('href', '/workshop/add')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { NavLink } from 'react-router-dom'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useSession, type PhaseKey } from '@/store/session'
|
||||||
|
|
||||||
|
interface PhaseMeta {
|
||||||
|
key: PhaseKey
|
||||||
|
name: string
|
||||||
|
to: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const PHASES: PhaseMeta[] = [
|
||||||
|
{ key: 'reg', name: 'Team reg', to: '/workshop' },
|
||||||
|
{ key: 'setup', name: 'Env setup', to: '/workshop/setup' },
|
||||||
|
{ key: 'm1', name: 'Module 1', to: '/workshop/module1' },
|
||||||
|
{ key: 'm2', name: 'Module 2', to: '/workshop/module2' },
|
||||||
|
{ key: 'add', name: 'ADD · submit', to: '/workshop/add' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export interface PhaseStripProps {
|
||||||
|
active?: PhaseKey
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PhaseStrip({ active }: PhaseStripProps) {
|
||||||
|
const phases = useSession((s) => s.phases)
|
||||||
|
return (
|
||||||
|
<nav aria-label="Workshop phases" data-testid="phase-strip" className="grid grid-cols-5 gap-2 px-8 py-4 border-b border-border">
|
||||||
|
{PHASES.map((p, i) => {
|
||||||
|
const done = phases[p.key]
|
||||||
|
const isActive = active === p.key
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
key={p.key}
|
||||||
|
to={p.to}
|
||||||
|
data-phase={p.key}
|
||||||
|
data-state={done ? 'done' : isActive ? 'active' : 'pending'}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col gap-1 px-3 py-2 rounded-md border text-left transition',
|
||||||
|
done && 'border-teal bg-teal/5 text-teal',
|
||||||
|
!done && isActive && 'border-primary bg-primary/5 text-primary',
|
||||||
|
!done && !isActive && 'border-border text-muted-foreground hover:border-primary/40',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="font-mono text-[10px] uppercase tracking-wider opacity-70">Phase {i + 1}</span>
|
||||||
|
<span className="text-sm font-semibold tracking-tight">{p.name}</span>
|
||||||
|
</NavLink>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222 47% 11%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222 47% 11%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222 47% 11%;
|
||||||
|
--primary: 211 100% 50%;
|
||||||
|
--primary-foreground: 0 0% 100%;
|
||||||
|
--secondary: 210 40% 96%;
|
||||||
|
--secondary-foreground: 222 47% 11%;
|
||||||
|
--muted: 210 40% 96%;
|
||||||
|
--muted-foreground: 215 16% 47%;
|
||||||
|
--accent: 211 100% 50%;
|
||||||
|
--accent-foreground: 0 0% 100%;
|
||||||
|
--destructive: 0 84% 60%;
|
||||||
|
--destructive-foreground: 0 0% 100%;
|
||||||
|
--border: 214 32% 91%;
|
||||||
|
--input: 214 32% 91%;
|
||||||
|
--ring: 211 100% 50%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222 47% 11%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222 47% 11%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222 47% 11%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary: 211 100% 60%;
|
||||||
|
--primary-foreground: 222 47% 11%;
|
||||||
|
--secondary: 217 32% 17%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217 32% 17%;
|
||||||
|
--muted-foreground: 215 20% 65%;
|
||||||
|
--accent: 217 32% 17%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--destructive: 0 62% 50%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217 32% 17%;
|
||||||
|
--input: 217 32% 17%;
|
||||||
|
--ring: 211 100% 60%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground antialiased;
|
||||||
|
font-feature-settings: 'rlig' 1, 'calt' 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from 'clsx'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import '@testing-library/jest-dom/vitest'
|
||||||
|
import { cleanup } from '@testing-library/react'
|
||||||
|
import { afterEach } from 'vitest'
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
import { persist, createJSONStorage } from 'zustand/middleware'
|
||||||
|
|
||||||
|
export type PhaseKey = 'reg' | 'setup' | 'm1' | 'm2' | 'add'
|
||||||
|
|
||||||
|
export const PHASE_ORDER: PhaseKey[] = ['reg', 'setup', 'm1', 'm2', 'add']
|
||||||
|
|
||||||
|
export type Provider = 'anthropic' | 'groq' | 'openai'
|
||||||
|
|
||||||
|
export interface Team {
|
||||||
|
name: string
|
||||||
|
members: string[]
|
||||||
|
kit: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Device {
|
||||||
|
connected: boolean
|
||||||
|
port: string | null
|
||||||
|
uptimeS: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Harness {
|
||||||
|
thresholdG: number
|
||||||
|
thresholdDb: number
|
||||||
|
callsPerMinute: number
|
||||||
|
provider: Provider
|
||||||
|
model: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionStats {
|
||||||
|
calls: number
|
||||||
|
nominal: number
|
||||||
|
anomalous: number
|
||||||
|
critical: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddLayers {
|
||||||
|
L1: unknown | null
|
||||||
|
L2: string
|
||||||
|
L3: string
|
||||||
|
L4: string
|
||||||
|
L5: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Submission {
|
||||||
|
code: string | null
|
||||||
|
submittedAt: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionState {
|
||||||
|
team: Team
|
||||||
|
device: Device
|
||||||
|
phases: Record<PhaseKey, boolean>
|
||||||
|
harness: Harness
|
||||||
|
stats: SessionStats
|
||||||
|
add: AddLayers
|
||||||
|
submission: Submission
|
||||||
|
setTeam: (patch: Partial<Team>) => void
|
||||||
|
setDevice: (patch: Partial<Device>) => void
|
||||||
|
completePhase: (phase: PhaseKey) => void
|
||||||
|
setHarness: (patch: Partial<Harness>) => void
|
||||||
|
recordEvent: (kind: 'nominal' | 'anomalous' | 'critical') => void
|
||||||
|
setAddLayer: <K extends keyof AddLayers>(key: K, value: AddLayers[K]) => void
|
||||||
|
setSubmission: (s: Submission) => void
|
||||||
|
reset: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const initial = {
|
||||||
|
team: { name: '', members: [] as string[], kit: 'KIT-01' },
|
||||||
|
device: { connected: false, port: null, uptimeS: 0 },
|
||||||
|
phases: { reg: false, setup: false, m1: false, m2: false, add: false } as Record<PhaseKey, boolean>,
|
||||||
|
harness: {
|
||||||
|
thresholdG: 0.8,
|
||||||
|
thresholdDb: 65,
|
||||||
|
callsPerMinute: 8,
|
||||||
|
provider: 'anthropic' as Provider,
|
||||||
|
model: 'claude-haiku-4-5',
|
||||||
|
},
|
||||||
|
stats: { calls: 0, nominal: 0, anomalous: 0, critical: 0 },
|
||||||
|
add: { L1: null, L2: '', L3: '', L4: '', L5: '' } as AddLayers,
|
||||||
|
submission: { code: null, submittedAt: null } as Submission,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSession = create<SessionState>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
...initial,
|
||||||
|
setTeam: (patch) => set((s) => ({ team: { ...s.team, ...patch } })),
|
||||||
|
setDevice: (patch) => set((s) => ({ device: { ...s.device, ...patch } })),
|
||||||
|
completePhase: (phase) =>
|
||||||
|
set((s) => ({ phases: { ...s.phases, [phase]: true } })),
|
||||||
|
setHarness: (patch) => set((s) => ({ harness: { ...s.harness, ...patch } })),
|
||||||
|
recordEvent: (kind) =>
|
||||||
|
set((s) => ({
|
||||||
|
stats: { ...s.stats, calls: s.stats.calls + 1, [kind]: s.stats[kind] + 1 },
|
||||||
|
})),
|
||||||
|
setAddLayer: (key, value) => set((s) => ({ add: { ...s.add, [key]: value } })),
|
||||||
|
setSubmission: (submission) => set({ submission }),
|
||||||
|
reset: () => set(initial),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'apess_state',
|
||||||
|
storage: createJSONStorage(() => sessionStorage),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
darkMode: ['class'],
|
||||||
|
content: ['./index.html', './src/**/*.{ts,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Newsreader', 'ui-serif', 'serif'],
|
||||||
|
mono: ['JetBrains Mono', 'ui-monospace', 'monospace'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))',
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))',
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))',
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
|
foreground: 'hsl(var(--popover-foreground))',
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: 'hsl(var(--card))',
|
||||||
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
|
},
|
||||||
|
teal: 'hsl(166 80% 36%)',
|
||||||
|
rose: 'hsl(347 77% 50%)',
|
||||||
|
amber: 'hsl(38 92% 50%)',
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "es2023",
|
||||||
|
"lib": ["ES2023", "DOM"],
|
||||||
|
"module": "esnext",
|
||||||
|
"types": ["vite/client", "vitest/globals", "@testing-library/jest-dom"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "es2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "esnext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: ['./src/setupTests.ts'],
|
||||||
|
css: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user