Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
158 changes: 158 additions & 0 deletions gui/DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# gui/DESIGN.md — de ontwerptaal van het dashboard

> De levende ontwerp- en smaakgids voor het opencodex-dashboard (`gui/`).
> Dit is de ChefGroep-taal (v2 "Devin-richting"): een stil, warm, mat instrument.
> Bron van waarheid voor de *taal*: [`OnlineChefGroep/design-system`](https://github.com/OnlineChefGroep/design-system)
> (`tokens.css`, `DESIGN.md`, `motion-spec.md`). Dit bestand legt vast hoe die
> taal in dít dashboard leeft, en — belangrijker — **hoe je 'm uitbreidt zonder
> 'm te breken**.

Alles hier is gebouwd op tokens in `src/styles.css`. Verzin nooit losse
px-waarden of kleuren in een component; gebruik een token. Zo blijft de hele
app in één keer te herstemmen.

---

## 1. De drie pijlers

1. **Stil oppervlak.** Warm off-white, haarlijnen, plat. Geen glow, geen
gradients, geen glasmorfisme, geen geneste schaduw.
2. **Levende activiteit.** Werk toon je als rust of een golfje, nooit als
ronddraaiende spinner.
3. **Begrijpelijk.** Eén accent, één type-ladder, één set radii, één easing.
Hiërarchie komt uit grootte/gewicht/kleur — niet uit decoratie.

---

## 2. Kleur

Eén accent: blauw (`--accent-blue`). Alles wat "klik mij / hier ben je / dit is
aan" zegt is blauw: links, focus-ring, actieve nav, geselecteerde tab, toggles,
selectie. De **primaire knop** blijft juist monochroom (tekst↔achtergrond
omgekeerd) — dat is de shadcn-conventie, geen tweede accent.

| Rol | Token |
|---|---|
| Achtergrond / rail / kaart | `--bg` · `--rail` · `--surface` · `--raised` |
| Lijnen | `--border` (sterk) · `--border-soft` (hairline) |
| Tekst | `--text` · `--muted` · `--faint` |
| Primaire actie | `--accent` (+ `--accent-ink`) |
| Het accent | `--accent-blue` · `--accent-blue-ink` · `--accent-soft` (ring/tint) |
| Semantiek | `--green` (git/PR/toestemming) · `--amber` (wacht-op-jou) · `--red` (destructief) |

Regels: groen/amber/rood zijn **gereserveerd**, nooit decoratie. Neutraal is
warm, nooit koudgrijs. Dark mode is basalt-warm, geen zuiver zwart. Elke token
is `light-dark(licht, donker)` — schrijf beide kanten, altijd.

---

## 3. Typografie

- **Archivo** (`--font-ui`) voor alles; **JetBrains Mono** (`--font-code`)
uitsluitend voor machinedata (timers, model-id's, paden, diffs, tellers).
- Eén type-ladder — gebruik de tokens, nooit losse px:

| Token | px | Gebruik |
|---|---|---|
| `--text-micro` | 10.5 | meta, tellers, caps-labels |
| `--text-caption` | 11.5 | labels, captions |
| `--text-label` | 12.5 | secundair / beschrijvingen |
| `--text-control` | 13.5 | **UI-standaard** (body van de app) |
| `--text-body` | 14 | leestekst |
| `--text-subtitle` | 16 | kleine titels |
| `--text-section` | 18 | sectiekoppen |
| `--text-title` | 22 | paginatitels |
| `--text-display` | 28 | hero-getallen |

- Koppen: gewicht 500, `letter-spacing: var(--tracking-tight)` (−0.02em),
`text-wrap: balance`. Leading via `--leading-*` (tight 1.2 / ui 1.45 /
body 1.55 / relaxed 1.65).
- **Getallen lijnen uit**: alles wat een getal is krijgt `.num` of
`font-variant-numeric: tabular-nums` (stat-waarden, quota, tellers, timers).
- Utilities: `.num` (tabulaire cijfers), `.caps` (uppercase microlabel),
`.prose` (68ch leesmaat). Componeer hiermee; verzin geen nieuwe.

---

## 4. Motion

Bewegen is transform + opacity, nooit `width/height/top/left`. Eén easing
(`--ease-out`), duren `--motion-fast/normal/slow` (140/280/420ms). Alles settle-t
vroeg, niets bounct, niets loopt oneindig. De vaste set:

- **Intent-reveal:** één rustige rise per navigatie (`.main-inner > *`), niet
per kaart.
- **Press-physics:** `scale(0.97–0.98)` op knoppen, nav-rijen, tabs, chips,
segments. Nooit op inputs, tekst of panelen.
- **Modal:** scrim vervaagt in, kaart rijst en settle-t.
- **Ripple i.p.v. spinner:** `.spin` is een kalm blauw golfje.

`prefers-reduced-motion` zet **alles** uit met nul informatieverlies (globale
guard in `styles.css`). Nieuwe animatie = tokenduur + één keyframe in het
Motion-blok. Meer niet.

---

## 5. Skins (`data-style`)

Dezelfde taal draagt meerdere complete skins:

- `devin` (default) — warm, zacht, ronder (`:root`).
- `strak` — koeler grijs-blauw, scherpere radii.

Zetten: `?style=strak` of een opgeslagen keuze (`localStorage` `ocx-style`),
toegepast vóór eerste paint in `main.tsx`. Een nieuwe skin = één blok
token-overrides in `styles.css` (light **én** dark). De taal (§1–§4) blijft
onder elke skin gelden.

---

## 5b. Taste-regels (overgenomen uit `design-system/taste/`)

Bindend voor nieuw werk. Twee observaties minimum per regel (zie de bron).

- **Kleur:** neutraal warm tinten, **één accent max**; licht is eersteklas
standaard. Geen paarse gradients, AI-glow, acid-on-black. Het accent is voor
links/focus/toggles/status — **niet** voor nav-selectie (die is een kalme
`--raised`).
- **Type:** Archivo/General Sans-humanist voor interface; mono **strikt** voor
data. Nooit Inter/Geist/Space Grotesk of mono voor labels/prose.
- **Motion:** vroeg settelen, lage amplitude/frequentie; **één** signatuur-
systeem (de Stroom/ripple), geen verspreide micro-animaties. Geen bounce,
elastic of oneindige ambient motion.
- **Dichtheid:** compact, informatiedicht (dichtheid 5–7). `h28`/`r6` voor
secundaire controls, `r10` voor kaarten. Geen marketing-witruimte in product.
- **Stem:** warm, direct, menselijk Nederlands op Joep-vlakken. Geen em-dashes,
buzzwords, lifecycle-jargon of verzonnen metrics.
- **Structuur:** haarlijnen + ruimte voor scheiding. **Geen** kaart-in-kaart,
bento-velden of geneste elevation. Tweebaans sidebar met vaste glyph-baan.
- **Iconen:** echte SVG-lijniconen (Lucide/shadcn, ~1.75px stroke, 15–16px
grid). Nooit emoji als icoon — nergens.
- **Metafoor:** water/stroom voor systeemstatus; instrument-framing
("gezandstraald instrument"). Geen keuken/bon/brigade of corporate-dashboard.

## 6. Bans (hard)

- Geen spinners/loaders (ripple vervangt ze).
- Geen emoji als icoon of in copy. Iconen zijn SVG-lijniconen (Lucide-stijl).
- Geen em-dashes, buzzwords of verzonnen metrics in copy.
- Geen gradients, glow, glasmorfisme, bento-kaartjes.
- Geen kaart-in-kaart, geen geneste elevation.
- Geen oneindige ambient motion.

---

## 7. Zo breid je uit

- **Nieuwe kleur/rol:** token toevoegen in `:root` mét `light-dark()`, en in
élke skin (`[data-style="strak"]`). Nooit een losse hex in een component.
- **Nieuwe component:** hergebruik `.btn`/`.badge`/`.input`/`.switch`/`.seg`,
radii- en type-tokens. Haarlijn-border, rustige hover, press-physics als het
klikbaar is.
- **Nieuwe tekst:** géén hardgecodeerde UI-strings in `src/pages`/
`src/components` (zie `gui/AGENTS.md`). Zet de string in **alle** locale-
bestanden (`src/i18n/*.ts`) en render met `useT()`. Draai `bun run lint:i18n`.
- **Nieuwe motion:** tokenduur + keyframe in het Motion-blok; transform/opacity
only; check `prefers-reduced-motion`.
- **Controleren:** `bun run typecheck`, `bun --bun run lint:gui`,
`bun run lint:i18n`, `bun run privacy:scan`. Zie `AGENTS.md`.
6 changes: 6 additions & 0 deletions gui/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"preview": "vite preview"
},
"dependencies": {
"@fontsource-variable/archivo": "^5.3.0",
"@fontsource-variable/jetbrains-mono": "^5.3.0",
"@tanstack/react-virtual": "^3.14.5",
"react": "^19.2.7",
"react-dom": "^19.2.7"
Expand Down
3 changes: 2 additions & 1 deletion gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,8 @@ export default function App() {

<main className="main" inert={navOpen}>
<div className={`main-inner${page === "combos" ? " main-inner--combos" : ""}`}>
<div className="page-reveal" key={page}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Combos workspace loses its height constraint

The reveal wrapper becomes the direct child of .main-inner--combos, so .combos-workspace-shell no longer matches the direct-child sizing rule in styles.css. With populated Combos data, the wrapper and workspace expand to their content height while the fixed-height main container clips overflow. The intended detail-panel scrollbar has no overflow to scroll, leaving content below the viewport inaccessible. Keep the Combos shell as the direct child, or give the wrapper the same constrained flex sizing behavior.

Artifacts

▶ Recording of the check

  • Playwright renders the prior direct-child workspace, selects a combo, and scrolls the constrained detail panel; the workspace remains viewport-height and usable.

Poster frame of the prior direct-child Combos workspace

  • The pre-change workspace fills the 900px main area and keeps scrolling inside its detail region.

▶ Recording of the check

  • Playwright renders the candidate wrapper path and shows the workspace expanding beyond the clipped main viewport instead of maintaining the intended inner scrolling.

Poster frame of the candidate Combos workspace after page-reveal wrapper insertion

  • The candidate view demonstrates the affected wrapper path after its reveal animation has settled.

Evidence from the check

  • This executable fixture script serves the populated Combos route, compares direct-child and wrapper structures, performs a scroll attempt, and records layout metrics.

Command output from the check

  • The actual command output records exit code 0 and shows baseline 900px sizing versus candidate 2176px wrapper and workspace sizing, confirming the clipping and scrolling failure.

Evidence from the check

  • The generated metrics provide the exact computed boxes, overflow values, and scroll dimensions for the matched before and candidate runs.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: gui/src/App.tsx
Line: 291

Comment:
**Combos workspace loses its height constraint**

The reveal wrapper becomes the direct child of `.main-inner--combos`, so `.combos-workspace-shell` no longer matches the direct-child sizing rule in `styles.css`. With populated Combos data, the wrapper and workspace expand to their content height while the fixed-height main container clips overflow. The intended detail-panel scrollbar has no overflow to scroll, leaving content below the viewport inaccessible. Keep the Combos shell as the direct child, or give the wrapper the same constrained flex sizing behavior.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Cursor Fix in Codex Fix in Claude Code Fix in Conductor

<ErrorBoundary
key={page}
pageName={t(PAGE_TKEY[page])}
title={t("errorBoundary.title")}
message={t("errorBoundary.message")}
Expand All @@ -310,6 +310,7 @@ export default function App() {
{page === "claude" && <Claude apiBase={API_BASE} />}
{page === "grok" && <Grok apiBase={API_BASE} />}
</ErrorBoundary>
</div>
</div>
</main>
</div>
Expand Down
16 changes: 16 additions & 0 deletions gui/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import React from "react";
import ReactDOM from "react-dom/client";
// ChefGroep design language: Archivo (UI, the free General Sans equivalent) +
// JetBrains Mono (data only). Bundled locally so nothing depends on a CDN.
import "@fontsource-variable/archivo/wght.css";
import "@fontsource-variable/jetbrains-mono";
import App from "./App";
import { LanguageProvider } from "./i18n/provider";
import "./styles.css";

// ChefGroep design language ships two complete skins (design-system §14).
// devin (warm, default) is the :root skin; strak (cool, sharp) is opt-in via
// ?style=strak or a saved choice. Applied before first paint to avoid a flash.
try {
const params = new URLSearchParams(window.location.search);
const style = params.get("style") ?? localStorage.getItem("ocx-style");
if (style === "strak" || style === "devin") {
document.documentElement.setAttribute("data-style", style);
localStorage.setItem("ocx-style", style);
}
} catch { /* ignore */ }

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<LanguageProvider>
Expand Down
Loading
Loading