Files
LaurensBartelds be69db04a5 Add SVG icons and TypeScript configuration files
- Introduced a new favicon.svg file for the application.
- Added icons.svg containing various SVG symbols for UI components.
- Created tsconfig.app.json for frontend TypeScript configuration with ES2023 target and bundler mode.
- Established tsconfig.json to reference app and node configurations.
- Implemented tsconfig.node.json for Node.js specific TypeScript settings.
2026-04-21 22:19:06 +02:00
..

Handoff: BrainWave Practice Exam Platform

Overview

BrainWave is a web-based practice exam platform that lets a user upload a JSON file describing an exam and take it interactively. It supports subjects from mathematics (with full LaTeX rendering) to language learning (translation questions) to general knowledge (multiple choice, essays, etc.). Results are scored automatically where possible, explained in a review screen, and persisted locally so users can resume in-progress exams and see a history of past attempts.

About the Design Files

The files in this bundle are design references created in HTML — a working prototype showing the intended look, behavior, and data model, not production code to copy directly. The task is to recreate these HTML designs in the target codebase's existing environment (React, Vue, Next.js, SvelteKit, native mobile, etc.) using its established patterns, component library, and state-management conventions. If no environment exists yet, pick the framework best-suited to the project and implement the designs there.

The prototype is implemented as a single-page React app using in-browser Babel for convenience; production should use a real build pipeline.

Fidelity

High-fidelity (hifi). The mocks are pixel-level specifications with final colors, typography, spacing, and interactions. All eight question renderers, the results/review flow, history, persistence, and keyboard shortcuts are implemented and working. Recreate the UI pixel-perfectly using the codebase's existing libraries and patterns.


Screens / Views

1. Landing / Upload

Purpose: User lands here to load an exam. Supports drag-and-drop JSON upload, curated sample exams, resume of in-progress attempt, and past-attempt history.

Layout:

  • Max width 1080px, centered; padding 72px 32px 120px.
  • Eyebrow label (uppercase monospace, 11px, letter-spacing 0.12em).
  • H1 title — Fraunces serif, clamp(40px, 6vw, 68px), line-height 1.02, letter-spacing -0.028em. Last two words wrapped in <em> and colored var(--accent).
  • Subtitle paragraph, 17px, max-width 58ch, color var(--ink-2).
  • Two-column grid (1.2fr / 1fr, gap 48px):
    • Left: Dropzone + error banner + resume card + supported-types list.
    • Right: "Try a sample" section with 3 sample cards.
  • Below the grid (when history exists): "Past attempts" list.

Components:

  • Dropzone: 1.5px dashed border, radius 14px, padding 40px 32px, background var(--paper-2). On hover/drag-over: border → accent, background → var(--accent-weak). Contains a 40×40 icon (upward arrow inside rounded square), h3 (16px, weight 500), caption (13.5px, var(--ink-3)).
  • Resume card: 1px border, radius 10px, padding 16px, var(--paper-2) background. Title + subcaption + "Resume →" primary button.
  • Sample card: flex row, gap 16px, 1px border, radius 10px, padding 16px. 36×36 mark tile (serif glyph), title (14.5px, weight 500), meta (12.5px, var(--ink-3)), "→" arrow on the right.
  • History row: 4-column grid 1fr auto auto auto, 14px items separated by 1px bottom borders. Title + timestamp caption, elapsed time (monospace), score pill (pass/fail colored), "Review" ghost button.

2. Exam Runner

Purpose: Take the exam, one question at a time, with a navigator sidebar and metadata rail.

Layout: 3-column grid 280px 1fr 280px, full height minus topbar (57px).

  • Sidebar (left, sticky): Exam title (Fraunces serif, 22px) + subtitle + "Questions" section label + nav list + progress bar + Submit/Exit buttons.
  • Main (center): Max-width 920px, padding 48px 64px 120px. Question header, prompt, media, renderer, optional inline feedback block, footer with nav buttons + keyboard-hints strip.
  • Meta rail (right, sticky): Elapsed, Remaining (if timed), Points possible, Mode, Author — each in a "meta block" with uppercase mono label and Fraunces serif value.

Components:

  • Topbar: 14px × 28px padding, sticky, 1px bottom border. 22×22 brand mark (1.5px border square with inset accent), brand name "BrainWave" (weight 600, -0.02em), faint "/ practice" qualifier. Right side: current exam title + Exit button.
  • Nav item: 3-column grid 28px 1fr auto, padding 7px 10px, radius 6px. Mono 2-digit number + category/type title + 7×7 status dot. Dot colors: var(--line-2) (blank), var(--accent) (answered), var(--warn) (flagged), var(--correct) (correct in immediate mode), var(--wrong) (wrong in immediate mode).
  • Question header: flex row, 16px gap, 32px bottom margin, 24px padding-bottom, 1px bottom border. Mono "Question 01 / 07" + category chip (12px, mono, var(--paper-3) bg, 4px radius, 2×8 padding) + right-aligned points label.
  • Question prompt: Fraunces serif, 26px, line-height 1.35, -0.015em, weight 400. Renders LaTeX inline.
  • Question footer: flex row, 48px top margin, 32px padding-top, 1px top border. Previous / Flag / (Check answer) / Next or Submit buttons. Spacer between middle and right groups.

3. Question Renderers (8 types)

All renderers share these hooks: value (current answer), onChange(next), locked (read-only), graded (show correctness coloring).

multiple-choice / multiple-select:

  • Vertical list of .choice buttons. 1px border, radius 10px, padding 16px 20px, 15px text, line-height 1.5.
  • 22×22 round marker (square for multi-select), 1.5px border.
  • When selected: marker fills with var(--ink); inner 8px dot (or checkmark for square).
  • Mono "choice-key" (A–D) between marker and text.
  • Graded-correct: border & bg → var(--correct) / var(--correct-weak). Graded-wrong: var(--wrong) / var(--wrong-weak).

short-answer / translation:

  • Single <input class="input"> — 100% width, padding 10px 12px, 1px border, radius 10px. Focus: border → accent, 3px focus ring.
  • For translation: subtitle "Answer in {targetLanguage}" in faint mono, 11px.

math-input:

  • Monospace input + "Preview" block underneath showing live KaTeX render of the LaTeX the user types.
  • Preview block: var(--paper-2) bg, 1px border, radius 10px, padding 14px 16px, min-height 48px, left-aligned mono uppercase label + rendered output.

essay:

  • Textarea, min-height 120px, 16px padding, radius 10px.
  • Below: word count + "Self-graded · see rubric on review". If q.minWords set and below threshold, word count is colored var(--wrong).

match:

  • 2-column grid, 32px gap. Left = "Items", right = "Matches".
  • Click a left item → activates it (var(--accent) border + 3px accent-weak ring). Click a right item → creates pairing; paired items show a small badge with the other side's index letter/number.
  • Click paired left to clear it; click paired right to clear pairing.

order:

  • Draggable list. Each item: 14×16 padding, 1px border, radius 10px. Shows ⋮⋮ handle + mono position number + text + up/down buttons (ghost, small).
  • While dragging: opacity 0.4. Drop target highlighted with accent-colored top or bottom border.
  • Initial order is a deterministic shuffle seeded from the question id.

4. Results / Review

Purpose: Summary of performance + per-category breakdown + detailed review of every question.

Layout: Max-width 960px, centered, padding 64px 32px 120px.

  • Eyebrow label → "Exam complete · {title}" (or "Reviewing past attempt · ...").
  • H1 title — Fraunces, 56px, -0.025em, line-height 1.05. "You scored {pct}%" with pct in accent. Subline in 0.5em size, var(--ink-3), announces pass/fail against passingScore (default 60).
  • Score grid: 3 equal columns (1fr 1fr 1fr), 1px top and bottom borders with internal 1px separators. Each cell: uppercase mono label + Fraunces value (48px) + mono unit suffix.
  • Category breakdown (if more than one category): 3-col grid rows 180px 1fr 60px — label + 4px bar + right-aligned percent.
  • Review list: Collapsible items, 1px border, radius 10px, 16px margin-between. Header: mono number + truncated prompt + status pill (correct/wrong/ungraded) + +/− toggle. Open body: re-renders full question with locked graded, shows explanation and rubric.
  • Footer buttons: Retake exam (primary) + Back to home (ghost).

5. Tweaks Panel (floating)

Fixed bottom-right, 280px wide, 1px border, 10px radius, var(--shadow-md).

  • Header: uppercase mono "Tweaks" + close button.
  • Body rows: each row has a mono label + segmented control (.tweak-control background with pill-selected active button).
  • Controls: Theme (Light/Dark), Text size (S/M/L), Feedback (Auto/Immediate/End).

Interactions & Behavior

Upload

  • File input accepts application/json.
  • On drop or select: read file → JSON.parse → validateExam() → loadExam().
  • validateExam checks for title, non-empty questions array, and every question having id, type, prompt.
  • Error banner renders in var(--wrong-weak) with var(--wrong) border for 1px/8px radius.

Exam flow

  • On load, state initializes { currentIdx: 0, answers: {}, flags: {}, revealed: {}, startedAt: Date.now(), feedbackMode }.
  • feedbackMode resolves: if Tweak is "auto", use exam.feedbackMode (default "end"); otherwise use Tweak override.
  • Every state change saveState({exam,state}) to localStorage under brainwave.exam.state.
  • Timer (exam.timeLimitMinutes): counts down from timeLimitMinutes*60_000 - elapsed. Last 60s: value colored var(--wrong). At 0: auto-submit.
  • Submit: compute graded snapshot, append to brainwave.exam.history (capped 30 entries), clear active-state key, advance to Results.

Grading

  • multiple-choice: user answer === q.answer (string id).
  • multiple-select: unordered array equality.
  • short-answer / translation: normalize both using {caseSensitive, ignorePunctuation, ignoreAccents} options, then compare against [answer, ...acceptedForms].
  • math-input: strip whitespace, \left/\right, \,, braces around single chars, lowercase — then exact match against accepted forms.
  • match: every key in q.answer must have matching user pairing.
  • order: ordered array equality.
  • essay: ungraded by default; optional selfGrade (0–1) yields partial points.

Keyboard

  • ←/k: previous question.
  • →/j: next question.
  • f: toggle flag.
  • 1–9: select choice for multiple-choice questions.
  • Disabled when focus is inside <input>/<textarea>.

Persistence

  • localStorage["brainwave.exam.state"]: active exam snapshot (exam JSON + state).
  • localStorage["brainwave.exam.history"]: array of completed attempts, newest first, capped at 30.

Tweaks protocol

  • Defaults wrapped in /*EDITMODE-BEGIN*/{...}/*EDITMODE-END*/ JSON marker so host can rewrite.
  • On change: window.parent.postMessage({type:'__edit_mode_set_keys', edits:{...}},'*').
  • Listens for __activate_edit_mode / __deactivate_edit_mode.
  • On mount, posts __edit_mode_available.

State Management

Top-level app state:

  • screen: 'landing' | 'exam' | 'results' | 'review'
  • exam: parsed JSON object (or null)
  • state: { currentIdx, answers: Record<qId, any>, flags: Record<qId, boolean>, revealed: Record<qId, boolean>, startedAt: number, completedAt?: number, feedbackMode: 'immediate'|'end' }
  • history: array of completed-attempt entries
  • tweaks: { theme, fontSize, questionLayout, feedbackMode }
  • tweaksOpen: boolean

Data fetching: None beyond the initial JSON upload. Sample exams are static JSON files bundled with the app.


Design Tokens

Colors (OKLCH, with fallback hex for reference)

--paper        oklch(0.985 0.004 80)   # warm white background
--paper-2      oklch(0.965 0.005 80)   # subtle tint
--paper-3      oklch(0.945 0.006 80)   # stronger tint
--ink          oklch(0.22 0.012 260)   # primary text / primary button bg
--ink-2        oklch(0.36 0.012 260)   # secondary text
--ink-3        oklch(0.52 0.012 260)   # muted
--ink-4        oklch(0.68 0.008 260)   # faint
--line         oklch(0.9 0.006 80)     # default 1px border
--line-2       oklch(0.86 0.008 80)    # stronger border
--line-3       oklch(0.78 0.01 80)     # hover border
--accent       oklch(0.55 0.15 265)    # indigo accent
--accent-weak  oklch(0.94 0.03 265)    # accent background
--accent-ink   oklch(0.38 0.14 265)    # accent text on weak bg
--correct      oklch(0.58 0.13 150)    # green
--correct-weak oklch(0.95 0.03 150)
--wrong        oklch(0.58 0.18 25)     # red-orange
--wrong-weak   oklch(0.96 0.03 25)
--warn         oklch(0.72 0.15 75)     # amber (flag)

Dark theme: swap neutrals (paper ↔ ink scale), accent stays vivid at oklch(0.72 0.13 265).

Typography

  • Sans: Inter, weights 400/500/600, features ss01 + cv11.
  • Serif: Fraunces, italics available; used for headlines and question prompts.
  • Mono: JetBrains Mono, feature zero enabled.
  • Base body: 15px / 1.55, letter-spacing -0.005em.
  • Serif title scale: 22 / 26 / 28 / 48 / 56 / clamp(40–68) — letter-spacing -0.015 to -0.028em.
  • Uppercase mono labels: 10.5–11px, text-transform uppercase, letter-spacing 0.12em, weight 500.

Spacing

  • Core scale: 4, 6, 8, 10, 12, 14, 16, 20, 24, 28, 32, 48, 64, 72px.
  • Rail padding: 24px 20px. Main content padding: 48px 64px 120px.
  • Card padding: 16–20px. Button padding: 9px 14px (sm: 6px 10px).

Border radius

  • --r-xs: 4px
  • --r-sm: 6px
  • --r-md: 10px (default for buttons, inputs, cards)
  • --r-lg: 14px (dropzone, major panels)
  • Pills: 999px.

Shadows

  • --shadow-sm: 0 1px 0 oklch(0.9 0.005 80 / 0.6)
  • --shadow-md: 0 1px 2px oklch(0 0 0 / 0.04), 0 4px 16px oklch(0 0 0 / 0.04)

Motion

  • All transitions 100–200ms with ease.
  • Category-bar width transitions at 400ms on initial render.
  • No complex animations; everything is a simple state change.

Assets

  • Fonts are loaded from Google Fonts (Inter, Fraunces, JetBrains Mono). Swap for self-hosted equivalents in production.
  • KaTeX (0.16.11) loaded from jsDelivr CDN. Bundle locally in production.
  • Sample exam images: one Unsplash URL in mixed-demo.json (city skyline) — replace with app-owned assets.
  • Icons: all UI marks (brand mark, upload icon, status dots, drag handle ⋮⋮, etc.) are pure CSS or Unicode glyphs — no icon-library dependency.

JSON Schema

Users upload a JSON file matching this shape (see docs/schema.md in the prototype for a longer version):

{
  "title": "string (required)",
  "subtitle": "string",
  "author": "string",
  "timeLimitMinutes": 45,
  "feedbackMode": "end" | "immediate",
  "passingScore": 70,
  "categories": ["Topic 1", "Topic 2"],
  "questions": [
    {
      "id": "q1",
      "type": "multiple-choice",
      "category": "Topic 1",
      "points": 2,
      "prompt": "Question text. LaTeX via $...$ and $$...$$",
      "explanation": "Shown after grading",
      "image":  { "src": "url", "alt": "...", "caption": "..." },
      "audio":  { "src": "url" },
      "code":   { "language": "python", "content": "..." },
      "table":  { "headers": ["..."], "rows": [["..."]] },
      "svg":    "<svg>...</svg>",
      "choices": [{ "id": "a", "text": "..." }],
      "answer":  "a"
    }
  ]
}

Answer shape by type:

  • multiple-choice: string id
  • multiple-select: array of ids
  • short-answer / translation: string + acceptedForms[] + caseSensitive / ignorePunctuation / ignoreAccents
  • math-input: LaTeX string + acceptedForms[]
  • essay: no answer; optional rubric[] + minWords
  • match: left[], right[], answer: { [leftId]: rightId }
  • order: items[] and answer[] (ordered ids)

See the three sample files (calculus-101.json, spanish-a2.json, mixed-demo.json) for complete worked examples covering every question type and media block.


Files

Bundled in this handoff (copied from the prototype):

  • BrainWave.html — HTML shell with font + KaTeX + React loading
  • styles.css — full design-token / component stylesheet
  • app/utils.js — normalization, grading, formatting, LaTeX rendering helper, persistence
  • app/questions.jsx — the 8 question renderers + media block
  • app/screens.jsx — Landing, ExamRunner, Results components
  • app/app.jsx — top-level App + Tweaks panel
  • samples/calculus-101.json — math exam sample
  • samples/spanish-a2.json — language exam sample
  • samples/mixed-demo.json — mixed-types demo
  • docs/schema.md — JSON schema reference

Implementation Notes for Claude Code

  1. Framework choice: React (with Next.js, Remix, or Vite) is the most direct port. Vue + Pinia or SvelteKit would also work well given the state is mostly local.
  2. LaTeX: Use the real katex npm package, not a CDN. react-katex is a convenient wrapper.
  3. Persistence: Replace raw localStorage with a typed wrapper (e.g. Zustand's persist middleware or a custom hook). Consider IndexedDB if users should store large exam JSONs.
  4. Validation: Replace hand-rolled validateExam with a schema library (Zod, Valibot). Define the exam schema as the source of truth.
  5. Grading: Keep grading logic pure and unit-test it — each question type's gradeQuestion branch is a good test seam.
  6. Drag-and-drop ordering: HTML5 DnD works but is fiddly. Swap for @dnd-kit/sortable or react-sortable-hoc in production.
  7. Math input: Consider a proper math editor (MathLive, MathQuill) so users don't have to type raw LaTeX.
  8. Accessibility: Audit focus order, ARIA roles for radio/checkbox groups, timer live-region, and keyboard access for match/order questions.
  9. Internationalization: Strings are hard-coded in the prototype. Extract to a resource file before shipping.
  10. Theming: The token system is CSS-variable-based; it maps cleanly to any CSS-in-JS or design-token pipeline (Tailwind, Panda, vanilla-extract).