Files
tutortool/docs/design_handoff/README.md
T
2026-04-29 04:38:26 +02:00

326 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Handoff: Tutormanager — Anwesenheit & Notizen
## Overview
Tutormanager is an attendance + per-student notes tool for tutorien. The flagship use case is the FP-Tutorium (~19 students, Thursdays). Tutors run a short check-in routine at the start of each Projektstunde: open a slot, project a check-in URL on the beamer, watch students fill the seat map from their phones/laptops, lock the slot, and use the rest of the hour with a live seat plan that lets them jot per-student observations as they go.
The full architecture is described in `SPEC.md` (your existing approved spec). This handoff covers **only the frontend**. The HTML prototypes in this folder are **design references** — pixel-accurate mocks of the intended look and behaviour. The task is to **recreate them in SvelteKit** (`adapter-static`, SPA) per the approved stack, talking to the Rust/Axum backend defined in the spec.
## About the Design Files
The files in this bundle are design references created with React + inline JSX in static HTML. Don't ship them. Re-implement the same screens as Svelte components in the SvelteKit app, using your own component split and your own state stores. The HTML/CSS choices (paper background, ruled notebook, oxblood accent, Source Serif 4 + Inter + JetBrains Mono + Caveat) should transfer 1:1.
Open `Tutormanager.html` in a browser to view the canvas with all screens. `styles.css` holds the design tokens.
## Fidelity
**High-fidelity.** Colors, typography, spacing, and component states are final. Recreate pixel-perfectly in Svelte using the design tokens listed below.
---
## Design Tokens
All in `styles.css` as CSS custom properties. Bring them in verbatim (e.g. as a `app.css` imported in `+layout.svelte`).
### Colors
| Token | Value | Usage |
|---|---|---|
| `--paper` | `#f4efe6` | App background |
| `--paper-2` | `#ebe4d6` | Subtle alt rows, avatar bg |
| `--paper-3` | `#ded4c0` | Slightly stronger paper |
| `--rule` | `#c9bfa9` | Borders, dividers |
| `--rule-soft` | `#d9d0bb` | Softer dividers |
| `--ink` | `#1f1b16` | Primary text, primary buttons |
| `--ink-2` | `#3a342b` | Body text |
| `--ink-3` | `#6b6356` | Secondary / small |
| `--ink-4` | `#968b7a` | Tertiary, placeholders |
| `--accent` | `#8a2c1f` | Oxblood — accents, "absent", lock, stamps |
| `--accent-soft` | `#c66a5b` | — |
| `--highlight` | `#f1d36a` | Highlighter yellow |
| `--highlight-soft` | `#f5e3a4` | Marker underline fill |
| `--green` | `#4a6b3a` | "anwesend" / present |
| `--red` | `#8a2c1f` | "fehlt" / locked |
| `--amber` | `#b07d2a` | "offen" / open slot |
Card surface is `#fbf7ee` (slightly lighter than paper). Seat map background is `#f7f1e3`. Tables in seat map are `#e8dec5` with `var(--ink-2)` border.
### Typography
| Token | Stack |
|---|---|
| `--serif` | `"Source Serif 4", "Source Serif Pro", "EB Garamond", Georgia, serif` |
| `--sans` | `"Inter", "Helvetica Neue", Helvetica, Arial, sans-serif` |
| `--mono` | `"JetBrains Mono", "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace` |
| Marginalia | `"Caveat"` (used only for handwritten notes — see `.handwritten`) |
Load via Google Fonts (single `<link>`):
```
Source Serif 4 (300700, italic), Inter (400/500/600), JetBrains Mono (400/500/600), Caveat (400/500/600)
```
Type scale (utility classes in `styles.css`):
- `.h1` — Source Serif 4, 44/1.05, weight 500, letter-spacing -0.02em
- `.h2` — Source Serif 4, 28/1.15, weight 500, letter-spacing -0.01em
- `.h3` — Source Serif 4, 20/1.2, weight 500
- `.eyebrow` — JetBrains Mono, 11px, uppercase, 0.14em tracking, `--ink-3`
- `.body` — Inter, 14/1.5
- `.small` — Inter, 12, `--ink-3`
- `.tiny` — Inter, 11, `--ink-3`
### Spacing
8px-aligned grid. Common values seen in the design: 4, 6, 8, 10, 12, 14, 16, 18, 22, 24, 28, 32, 36.
### Radii
- 34px — most cards, inputs, buttons (paper feel — keep tight)
- 999px — pills
- 50% — avatars, seat circles
### Shadows
Avoid heavy shadows. Use a `1px 0 rgba(0,0,0,.03)` underline on cards. The seat-map container uses a subtle inset border instead of a box shadow.
### Special textures
- `.paper-bg` — paper grain via inline SVG noise + 2 radial highlights. Applied to app root and large surfaces.
- `.ruled` — repeating-linear-gradient producing 28px-spaced horizontal rules. Used inside the note textarea so handwritten text sits on lines.
- `.marker` — gradient that paints `--highlight-soft` over the lower 32% of inline text — looks like a hand-drawn highlighter stroke. Use sparingly on h1/h2 keywords (one word per heading).
- `.stamp` — rotated -4°, oxblood border + tint, monospace caps. Used for "PRÄSENT".
- `.handwritten` — Caveat in oxblood, 18px. For marginalia / sketch annotations only.
- `UnderlineStroke` — a tiny SVG hand-drawn underline beneath section titles. See `shared.jsx`.
---
## Information Architecture
```
/ → redirect to /admin or /admin/login
/admin/login → Login (anonymous)
/admin → Dashboard (slots overview)
/admin/live/:slotId → Hero · live seat map + notes panel
/admin/attendance → Per-student / per-week matrix + export
/admin/rooms → Layout editor (rooms list / canvas / props)
/admin/rooms/:roomId → Same, with active room
/admin/students → Students CRUD
/s/:code → Student check-in (mobile + desktop responsive)
```
The student check-in screen MUST be responsive: phone gets a single-column compact layout; ≥ 900px viewport gets the two-column laptop layout (map left, side panel right). Both are in this bundle.
---
## Screens
### 1. Login (`/admin/login`)
Centered 420px-wide card on `.paper-bg`. Brand wordmark "Tutor·manager" with oxblood `·`. Card title is `.h2` "Willkommen zurück" with an `UnderlineStroke` (110px wide). Two stacked inputs (E-Mail, Passwort), full-width primary button "Anmelden". Below the card a small Caveat marginalia "~ Donnerstags ab 14 Uhr ~".
### 2. Tutor shell
Two columns: 220px sidebar + main content.
**Sidebar** (`background: rgba(0,0,0,0.015)`, right border `--rule`):
- Brand wordmark + version
- "Kurs" card (course name + semester + weekday)
- Nav list: Dashboard / Live · Sitzplan / Anwesenheit / Räume / Studierende. Active item: 4px oxblood dot + `rgba(31,27,22,0.08)` background.
- Bottom: avatar circle + tutor name + email.
### 3. Dashboard (`/admin`)
- Header: eyebrow "Dashboard", `.h1` "Diese Woche, **Woche 04**" (the week number gets `.marker`), one-line subtitle. Right side: "Export" (ghost) + "Neuer Slot" (primary) buttons.
- 4 stat cards in a row: "Anwesend gerade 14 / 19", "Ø Anwesenheit 84%", "Bonus vergeben 186 Punkte", "Offene Notizen 7". Big numbers in serif 32px, accent color when meaningful.
- Slots table: columns Woche / Datum / Zeit / Raum / Status / Code / Eingecheckt / Aktionen. Status pills (open / closed / locked). Eingecheckt cell shows "n / total" + a 48×4px progress bar. Per-row primary action depends on status (Sperren / Öffnen / Anzeigen).
### 4. Live seat map + notes (HERO — `/admin/live/:slotId`)
Two-column grid: `1fr 380px`.
**Left column — seat map:**
- Header row: "Sitzplan" (`.h3` + UnderlineStroke 70px) on the left; legend dots on the right (anwesend / frei / ausgewählt).
- The seat map itself (see "Seat map component" below) at scale 0.85.
- Tally row at bottom: 3 stat blocks (Anwesend `n/total`, Fehlt n, Bonus heute "+n Punkte") + spacer + ghost button "Manuell eintragen".
**Right column — roster + note editor (a card):**
- Top: "Studierende" with present/absent count.
- Scrollable roster (max-height ~220px). Each row: 22px ink avatar circle, name, oxblood dot if has note, monospace check-in time (or "—" for absent). Selected row: `rgba(31,27,22,0.06)` bg + 3px ink left border. Absent rows: 0.55 opacity, dashed avatar border, strikethrough name.
- Bottom — note editor on `#fbf7ee`:
- 32px ink avatar + selected student name (`.h3`) + "Sitzplatz {seat} · seit {time}" + "Präsent" stamp on the right.
- "Notiz · Woche 04" eyebrow.
- Textarea with `.ruled` background, Source Serif 4 15/28px, no border. Placeholder "Beobachtungen für diese Woche…".
- Quick-tag chips below: "+ aktiv beteiligt", "+ stille:r Kämpfer:in", "+ verstanden ✓", "+ nochmal aufgreifen", "+ Rückfrage offen", "+ elegante Lösung".
- Footer: "Auto-gespeichert · 14:23" left, "Notizen vergangener Wochen ↗" link right.
Page header above the grid:
- Left side: eyebrow "Tutor:innen-Ansicht · Live", title "Woche 04 · **Donnerstag, 30. April 2026**" (date with `.marker`), subtitle line: course / room / time.
- Right side: monospace check-in code "K7QJ-MX2P" with the URL beneath, status pill (offen), Kopieren / Sperren buttons.
### 5. Anwesenheit (`/admin/attendance`)
- Header `.h1` "Kursmatrix · **SS 2026**". Right: 3 ghost export buttons (CSV / Markdown / SQLite Backup).
- Card with tab bar: "Pro Studierende:r" (active) / "Pro Woche" / "Notizen".
- Matrix table: # / Studierende:r / W01 / W02 / W03 / W04 / Anwesend / Bonus / arrow. Present cell is a 22px square with green check on `rgba(74,107,58,0.14)`. Absent: em-dash. Bonus column shows a single 24px oxblood circled check (no number — only "got bonus or not").
### 6. Räume — Layout-Editor (`/admin/rooms/:roomId`)
Three-column grid: 210px / 1fr / 240px.
- **Left**: rooms list (selectable, oxblood left border on active), "+ Neuer Raum" button at bottom.
- **Center** — toolbar + canvas + status bar:
- Toolbar: 6 tool buttons (Auswählen / Sitz / Tisch / Tür / Fenster / Lücke), each with a small monospace glyph + label. Active tool is filled ink. Right side: zoom controls ( 78% +), "Raster: 24px".
- Canvas: `#efe6d2` background with rulers along top (px ticks) and left, a centred seat map at scale 0.78. Selected element shows a rotating dashed oxblood ring + 4 small white drag-handle squares. Two Caveat marginalia annotations.
- Bottom status bar: element counts, auto-save timestamp.
- **Right**: stacked cards.
- "Auswahl" card: shows kind + id ("Sitz T2-3"), then editable fields (Bezeichnung, Tisch, X / Y in px, ∅ / Rotation). Below: "Aktionen" — Duplizieren (⌘D), An Tisch ausrichten, Löschen (⌫, oxblood text).
- "Ebenen" card: tree-like list of layers — walls, 4 tables with their seats, podium, beamer, door, window. Selected layer (T2 group) gets oxblood text + tinted bg.
### 7. Studierende (`/admin/students`)
Header with title + search input + "CSV importieren" + "Hinzufügen". Card with table: # / Name (avatar + name) / Anwesend (n/4) / Bonus (single circled check or em-dash) / Notizen (oxblood dot + count) / Letzte Sitzung / arrow.
### 8. Student check-in — phone (`/s/:code`, viewport < 900px)
Four states (each on `.paper-bg`, ~360×760 viewport):
1. **Name picker**: eyebrow "Check-in · K7QJ-MX2P", title "Wer bist du?", search input, scrollable name list (avatar + name, divider lines). Footer hint "Nicht in der Liste? Sprich die Tutor:in an."
2. **Seat picker**: "Hallo, Carla 👋" eyebrow, title "Wähle deinen Sitz", scaled seat map (0.46), legend, info card "Du kannst deinen Sitz wechseln, solange der Slot offen ist."
3. **Confirmed**: title "Du sitzt auf **T1-3**" (T1-3 with `.marker`), "Präsent" stamp top-right, scaled map showing only own seat highlighted oxblood, status card with "OFFEN" pill + lock-time hint, ghost "Sitz wechseln" button.
4. **Locked / read-only**: title "Anwesenheit erfasst", scaled map (own seat still oxblood), info card with lock icon + "Check-in für diesen Slot ist geschlossen. Bonus wurde gutgeschrieben.", footer with next session date.
### 9. Student check-in — laptop (`/s/:code`, viewport ≥ 900px)
Three states:
1. **Name picker**: centered 560px column. Header eyebrow + .h1 "Wer bist du?" + subtitle. Card with autofocused search and a 2-column grid of name buttons. Footer keyboard-shortcut tip.
2. **Seat picker**: two-column `1fr 360px`. Left: large seat map (scale 0.78) + legend below. Right side panel (subtle bg, left border): "Sitzung" block (course / date+time / room), "Eingecheckt als" with avatar + "wechseln" link, "Hinweise" bullet list, bottom yellow-tinted card "Bonus +3 Punkte sobald du einen Sitz wählst."
3. **Confirmed (with season)**: same two-column. Left: header "Du sitzt auf **T1-3**" + stamp, large map with own seat highlighted, two ghost buttons (Sitz wechseln / Drucken). Right side: "Slot-Status" with open pill, "Deine Saison" — 4-row weekly history list (W01W04, date, seat code, green check or em-dash; current week in oxblood), oxblood-tinted card "Bonus gesamt 9 / 12 Punkte · 3 von 4 Tutorien besucht".
---
## Components to build
| Svelte component | Notes |
|---|---|
| `<StatusPill status="open" \| "closed" \| "locked" \| "present" \| "absent" />` | See `.pill` and `.pill.{state}` in `styles.css`. |
| `<UnderlineStroke width={110} color="..." />` | Inline SVG path — see `shared.jsx`. |
| `<Stamp>Präsent</Stamp>` | `.stamp` class. |
| `<Eyebrow>` / `<H1>` / `<H2>` / `<H3>` | Or just utility classes. |
| `<Tally label value total suffix accent />` | Stat block in serif. |
| `<StatCard label value suffix hint accent />` | Card variant for the dashboard. |
| `<Field label suffix mono />` | Labelled input with optional unit suffix in mono. |
| `<TutorShell active>` | Sidebar + main slot. |
| `<SeatMap variant="tutor" \| "student" \| "student-self" assignments selectedStudent ownSeat onSeatClick scale />` | The big one — see below. |
| `<NoteEditor studentId weekNr />` | Roster + ruled textarea + tag chips. |
| `<MarkerText>` | Inline span with `.marker`. |
### `<SeatMap>` — the central component
Top-down floor plan, absolutely-positioned layers inside a fixed-size design space (760×460 px) that gets transform-scaled by the `scale` prop.
**Static structural layers** (drawn in this order):
1. Inner ruled grid (24px graph paper, very low alpha).
2. Walls — 2px ink-2 border rounded 2px.
3. Window — 6×220px tall blue-tinted strip on the left wall with a horizontal split line. Mono "Fenster" label rotated -90°.
4. Door — gap in bottom wall (70×4px) covered with paper colour, an SVG door arc (`stroke-dasharray="2 2"`), and a "Tür" label.
5. Beamer — small black bar at top.
6. Podium / Pult — 190×38 ruled rectangle, mono caps "Pult · Tutor:in".
7. Tables — 4 rectangles (200×70px) at fixed coords. Filled `#e8dec5` with ink-2 border + italic serif label "T1""T4" centred at 0.35 alpha.
**Seats** — generated from tables: 5 per table.
- Top edge (y = table.y 22): 2 seats at x = table.x + table.w*{0.28, 0.72}.
- Bottom edge (y = table.y + table.h + 22): 2 seats, same x ratios.
- Head (short edge, x = table.x + table.w + 26): 1 seat at table.y + table.h/2.
Seat = 36px circle button, 1.5px border. Per `variant`:
- **tutor**: occupied seat shows ink avatar with student initials in white. Selected: filled ink + 3px highlight-soft outer ring. Hover: `#e0d4b6` fill. Free: paper bg + ink-4 border.
- **student**: occupied = `#d6cdb5` (no name shown — privacy). Free = paper bg + ink-2 border. Own seat (after pick) = oxblood fill, white star ★ glyph.
- **student-self** (read-only): same colours, no clicks, own seat = oxblood + ★, others greyed if occupied.
Compass mark in bottom-right (mono "N" + small SVG arrow).
The layout JSON the backend stores must round-trip these elements: `[{id, label, x, y, width, height, type}]` with `type ∈ "seat" | "table" | "gap" | "door" | "window" | "wall" | "beamer" | "podium"`. The component takes that JSON as input — don't hardcode the room.
---
## Interactions & state
- **Slot status transitions** (tutor): `closed → open → locked`. Opening generates the 8-char code (server-side per spec). Closed/locked rows show no code. The UI must atomically reflect "open ⇒ has code".
- **Seat click (tutor live view)**: selects that student in the roster; note editor scrolls/loads. Double-click could open the per-student timeline.
- **Note editor**: debounced auto-save (~600ms) → PUT note. Show "Auto-gespeichert · HH:MM" timestamp on success.
- **Quick tag chips**: on click, append the tag (with leading newline if note non-empty) to the note text, then save.
- **Student seat pick**: optimistic UI; on HTTP 409 ("seat taken"), revert and toast "Platz schon vergeben — bitte einen anderen wählen."
- **Seat change**: while `open`, click another free seat → confirmation toast → previous seat goes free immediately.
- **Locked**: pointer-events: none on seats; only own seat remains oxblood-highlighted.
- **Live polling**: poll the slot every 58s while open to refresh assignments and check-in count.
- **Marker animation**: the selection ring in the room editor uses `@keyframes spin 12s linear infinite`. Disable in `prefers-reduced-motion`.
---
## State management
Suggest one Svelte store per domain:
- `auth` — JWT token, current tutor.
- `course` — currently selected course (and tutors' available courses).
- `slots` — slots for the active course/week.
- `liveSlot` — assignments + notes for the slot currently shown in the live view; reactive on poll.
- `room` — layout JSON of the room being edited; also handles selection + tool state for the editor.
The check-in page is its own minimal app state: `{ code, slot, room, ownSeat, ownStudentId }`, no auth store.
---
## Copy
All UI is in **German**. Specific strings used in the design:
- "Anwesend", "Fehlt", "Bonus heute", "Eingecheckt", "Sitzplan", "Studierende", "Notiz · Woche {n}".
- "Manuell eintragen", "Sperren", "Öffnen", "Anzeigen", "Kopieren", "Speichern".
- "Wer bist du?", "Wähle deinen Sitz", "Du sitzt auf {seat}", "Anwesenheit erfasst".
- "Nicht in der Liste? Sprich die Tutor:in an.", "Du kannst deinen Sitz wechseln, solange der Slot offen ist."
- Status pill labels: OFFEN / GESCHL. / GESPERRT / ANWESEND / FEHLT.
- Stamp: PRÄSENT.
The sample course is "Funktionale Programmierung", semester "SS 2026", room "BC2 1.103", weekday "Donnerstag", tutorin "Lina Puchstein". Replace with real data from the backend.
---
## Assets
- **Fonts**: Source Serif 4, Inter, JetBrains Mono, Caveat — all from Google Fonts.
- **Icons**: tiny inline SVGs in `shared.jsx` (`Icon.check`, `Icon.x`, `Icon.lock`, `Icon.open`, `Icon.copy`, `Icon.edit`, `Icon.download`, `Icon.arrow`, `Icon.search`, `Icon.plus`). Re-use them as Svelte components or swap to Lucide if you prefer — they're trivial.
- **Paper grain**: inline SVG noise filter in `.paper-bg`. Copy verbatim.
- **No image assets**.
---
## Files in this bundle
| File | What's in it |
|---|---|
| `Tutormanager.html` | Entry point — design canvas hosting all artboards. Open in a browser. |
| `styles.css` | All design tokens + utility classes. Copy into the SvelteKit app. |
| `shared.jsx` | Sample course + students + room layout + `StatusPill`, `UnderlineStroke`, `Icon.*` |
| `seatmap.jsx` | `<SeatMap>` and the `<TutorLiveView>` hero — main reference. |
| `admin.jsx` | Tutor shell, Dashboard, Anwesenheit-Matrix, Studierende, Login. |
| `rooms.jsx` | Layout editor with toolbar, canvas, properties + layers panel. |
| `student.jsx` | Phone variants of the four student states. |
| `student-desktop.jsx` | Laptop variant of the three student states. |
| `design-canvas.jsx`, `browser-window.jsx`, `ios-frame.jsx` | Presentation chrome — for the design canvas only, do not port. |
The design canvas grouping (sections / artboards) is purely a presentation device. The actual app has the route structure listed under "Information Architecture".
---
## Implementation tips
- Start with `styles.css` and the type/colour tokens. Wire fonts.
- Build `<SeatMap>` early — it's the centrepiece and the only non-trivial component.
- The room editor's drag-and-drop is real work — the spec says layouts are stored as JSON. A DnD library (`svelte-dnd-action`) is fine; snap to a 24px grid. The visual shown is the *finished* state with one seat selected.
- Do **not** add visual flourishes (more icons, gradients, hero images). The look comes from restraint + the paper texture + the marker/stamp accents.
- Respect `prefers-reduced-motion` — disable the room-editor selection-ring spin and any hover scale.
- The handwritten Caveat marginalia is a flavour element. Keep them sparse — at most one per screen.