feat(frontend): bootstrap Vite + React 19 + TS strict toolchain (F0.1)

- Vite 8 / React 19 / TS 6 strict (noUncheckedIndexedAccess, no baseUrl deprecation)
- Tailwind 4 via @tailwindcss/vite (no PostCSS step)
- TanStack Query 5, react-router-dom 7, Recharts, clsx
- Vitest + Testing Library + jsdom for unit tests
- Playwright skeleton + first smoke spec (login redirect)
- ESLint flat config: typescript-eslint type-checked, react-hooks, react-refresh, prettier
- Prettier config (semi, single quotes, 100-col, lf)
- IBM Plex font @font-face declarations targeting /fonts/ (self-host, no CDN — OPSEC)
This commit is contained in:
ux-frontend
2026-05-21 20:30:23 +02:00
parent 2ead16114d
commit 80ca4641a3
19 changed files with 5348 additions and 0 deletions

View File

7
frontend/.prettierignore Normal file
View File

@@ -0,0 +1,7 @@
node_modules
dist
coverage
playwright-report
test-results
*.lock
package-lock.json

View File

@@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"arrowParens": "always",
"endOfLine": "lf"
}

64
frontend/README.md Normal file
View File

@@ -0,0 +1,64 @@
# Mimic frontend
React + TypeScript SPA for the Mimic BAS platform. Lives behind the existing
RT reverse proxy (Caddy + TLS + IP allowlist) — see D-007.
## Stack
- React 19, TypeScript strict
- Vite 8 (dev/build)
- Tailwind 4 (semantic tokens in `src/styles/theme.css`)
- TanStack Query 5 for server state
- react-router-dom v7 for routing
- Recharts for the report visualisations
- Vitest + Testing Library for unit tests
- Playwright for E2E (covered in sprint 1+)
## Scripts
```bash
npm install
npm run dev # vite dev server on :5173
npm run build # tsc -b && vite build
npm run typecheck # tsc -b --noEmit
npm run lint # eslint . (strict TS + react-hooks + prettier)
npm run format # prettier --write .
npm test # vitest run
npm run e2e # playwright test (needs npm run e2e:install once)
```
## Layout
```
src/
├── components/
│ ├── brand/ # Wordmark (provisional, awaits PR3 charter)
│ ├── shell/ # AppShell, Sidebar, StatusRail
│ └── ui/ # Panel, Pill, Button — instrument-grade primitives
├── mocks/ # sprint 0 fixtures (no backend yet)
├── router/ # RootLayout helper
├── screens/ # one folder per top-level route
├── session/ # mock auth context (D-003 v1 hook-in point)
├── styles/ # theme.css (tokens), fonts.css, globals.css
└── types/ # cross-cutting type aliases
```
## Design system status (provisional)
The token layer in `src/styles/theme.css` is structured to receive the RT
graphic charter (PR3) without touching component code. Component CSS reads
exclusively from semantic tokens (`--accent-rt`, `--status-detected`, …);
swapping the underlying primitive palette is the only change required when
PR3 lands.
## Auth status (sprint 0)
The session context reads/writes a mock user in `sessionStorage`. No real
auth yet. v1 will wire local username/password + bcrypt (D-003); v2 maps
Keycloak OIDC claims onto the same role enum.
## Fonts
IBM Plex (Sans / Sans Condensed / Mono), self-hosted under `public/fonts/`.
Files are not yet vendored — see `public/fonts/README.md`. Until then the
UI falls back to `ui-sans-serif` / `ui-monospace`.

View File

@@ -0,0 +1,7 @@
import { test, expect } from '@playwright/test';
test('home redirects to login', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveURL(/\/login$/);
await expect(page.getByRole('heading', { name: /mimic/i })).toBeVisible();
});

54
frontend/eslint.config.js Normal file
View File

@@ -0,0 +1,54 @@
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 prettier from 'eslint-config-prettier';
import { defineConfig, globalIgnores } from 'eslint/config';
export default defineConfig([
globalIgnores([
'dist',
'coverage',
'playwright-report',
'test-results',
'node_modules',
'e2e/**',
'playwright.config.ts',
'vitest.config.ts',
'vite.config.ts',
]),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommendedTypeChecked,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
prettier,
],
languageOptions: {
globals: globals.browser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'react-hooks/exhaustive-deps': 'error',
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
},
},
{
files: ['**/*.test.{ts,tsx}', 'src/test/**'],
rules: {
'@typescript-eslint/no-floating-promises': 'off',
},
},
]);

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
<title>Mimic — BAS</title>
</head>
<body class="bg-surface-0 text-fg-default antialiased">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4956
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
frontend/package.json Normal file
View File

@@ -0,0 +1,53 @@
{
"name": "mimic-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc -b --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"e2e": "playwright test",
"e2e:install": "playwright install --with-deps chromium"
},
"dependencies": {
"@tanstack/react-query": "^5.100.11",
"clsx": "^2.1.1",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-router-dom": "^7.15.1",
"recharts": "^3.8.1"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.60.0",
"@tailwindcss/vite": "^4.3.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^24.12.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/coverage-v8": "^4.1.7",
"eslint": "^10.3.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"jsdom": "^29.1.1",
"prettier": "^3.8.3",
"tailwindcss": "^4.3.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12",
"vitest": "^4.1.7"
}
}

View File

@@ -0,0 +1,26 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
timeout: 60_000,
},
});

View File

@@ -0,0 +1,14 @@
# IBM Plex — vendored fonts
Drop the following .woff2 files here before merging to `main`:
- `IBMPlexSans-{400,500,600,700}.woff2`
- `IBMPlexSansCondensed-{500,600,700}.woff2`
- `IBMPlexMono-{400,500,600}.woff2`
Source: <https://github.com/IBM/plex> (OFL-1.1).
Until they are vendored, the UI falls back to `ui-sans-serif` / `ui-monospace`
via the `font-display: swap` directive — visually different but functional.
Tracked as a sprint 0 chore (follow-up commit, not a blocker for the skeleton PR).

22
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,22 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RouterProvider } from 'react-router-dom';
import { router } from './router';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
gcTime: 5 * 60_000,
retry: 1,
refetchOnWindowFocus: false,
},
},
});
export function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}

15
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,15 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './styles/globals.css';
import { App } from './App';
const container = document.getElementById('root');
if (!container) {
throw new Error('Root element #root missing in index.html');
}
createRoot(container).render(
<StrictMode>
<App />
</StrictMode>,
);

View File

@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest';

View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"isolatedModules": true,
"useDefineForClassFields": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

7
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -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"]
}

17
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,17 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
import path from 'node:path';
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 5173,
strictPort: false,
},
});

25
frontend/vitest.config.ts Normal file
View File

@@ -0,0 +1,25 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
import path from 'node:path';
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
css: true,
include: ['src/**/*.{test,spec}.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
exclude: ['node_modules/', 'src/test/', '**/*.config.*', '**/*.d.ts', 'src/main.tsx', 'src/mocks/**'],
},
},
});