diff --git a/CHANGELOG.md b/CHANGELOG.md index d461102..8837bcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ This file records what changes **in the product** – process and session state ## [Unreleased] ### Added +- Visual design for the pilot UI, implemented from the Claude Design prototype "RequestFlow A": warm + neutral palette with one blue accent, IBM Plex Sans/Mono (self-hosted via `@fontsource`, no + third-party requests), a header with company, name and role, a start page with open work, and a + two-column review page – fields, positions and documents on the left, the selected value with its + source and the decision in a sticky panel on the right. Status is always labelled (⚠ for values that + need attention); colours are design tokens (WCAG AA). Login and upload errors are announced as alerts; + after an upload the message links to the new request; invitation links can be copied. - Observability: structured JSON logs (pino) with IDs and codes only, correlated by the request id from web through worker to the AI service; `/api/health` also shows whether the AI service is reachable and how many jobs are waiting. diff --git a/package.json b/package.json index 7f1282e..25eb039 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "dependencies": { "@aws-sdk/client-s3": "3.1138.0", "@better-auth/drizzle-adapter": "1.7.5", + "@fontsource/ibm-plex-mono": "5.3.0", + "@fontsource/ibm-plex-sans": "5.3.0", "better-auth": "1.7.5", "drizzle-orm": "0.45.3", "next": "16.3.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d92ecd4..cf50eae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,12 @@ importers: '@better-auth/drizzle-adapter': specifier: 1.7.5 version: 1.7.5(@better-auth/core@1.7.5(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.3(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.0)) + '@fontsource/ibm-plex-mono': + specifier: 5.3.0 + version: 5.3.0 + '@fontsource/ibm-plex-sans': + specifier: 5.3.0 + version: 5.3.0 better-auth: specifier: 1.7.5 version: 1.7.5(drizzle-kit@0.31.11)(drizzle-orm@0.45.3(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.0))(next@16.3.6(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@24.13.6)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(vitest@5.0.1(@types/node@24.13.6)(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(tsx@4.23.15))) @@ -813,6 +819,12 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fontsource/ibm-plex-mono@5.3.0': + resolution: {integrity: sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==} + + '@fontsource/ibm-plex-sans@5.3.0': + resolution: {integrity: sha512-CbE4CbbEEZJX860XyUiRpsksXIQR8Rp2XDva2VO53NJox9tVNtusrysd2x5YkUEY3ErQ66W1IiiQL8/wihhw5w==} + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -3980,6 +3992,10 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@fontsource/ibm-plex-mono@5.3.0': {} + + '@fontsource/ibm-plex-sans@5.3.0': {} + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 diff --git a/src/app/_components/app-header.tsx b/src/app/_components/app-header.tsx new file mode 100644 index 0000000..8361a87 --- /dev/null +++ b/src/app/_components/app-header.tsx @@ -0,0 +1,61 @@ +import Link from "next/link"; +import { unstable_rethrow } from "next/navigation"; +import { getRuntime, requestActor } from "@/app/_server/runtime"; +import { getCompany, type Actor } from "@/features/identity"; +import { logEvent } from "@/features/observability"; +import { SignOutButton } from "./auth-form"; +import { NavLink } from "./nav-link"; + +export const ROLE_LABEL = { admin: "Administration", clerk: "Sachbearbeitung" } as const; + +async function signedIn(): Promise<{ actor: Actor; companyName: string | undefined } | null> { + try { + const actor = await requestActor(); + if (!actor) return null; + const company = await getCompany(getRuntime().database.db, actor.companyId); + return { actor, companyName: company?.name }; + } catch (error) { + unstable_rethrow(error); // Next.js control flow (dynamic rendering, redirects) is not a failure. + // The header must never take a page down (e.g. database unreachable): render no header. + logEvent("error", "app_header.failed", {}, { code: error instanceof Error ? error.name : "unknown" }); + return null; + } +} + +/** The brand mark + name – also used above the sign-in cards. */ +export function Brand({ className }: { className?: string }) { + return ( + +