# Design — Project Timeline / Chart (visualisation layer above SmartTimeline)

**Author:** faraday (inventor)
**Date:** 2026-05-09
**Task:** t-paliad-177
**Issue:** m/paliad#35
**Status:** READY FOR REVIEW — m gates inventor → coder transition.

---

## 0. Premises verified live (before designing)

Before anchoring the design, I checked the live state — CLAUDE.md / memory / issue body can drift, the live system can't.

- **SmartTimeline data substrate is shipped through Slice 4.** `internal/services/projection_service.go:287 (For)` returns `([]TimelineEvent, ProjectionMeta, error)`. The wire envelope (`ResponseEnvelope`) is `{events: TimelineEvent[], lanes: LaneInfo[]}` — `Lanes` is the load-bearing primitive for parent-node aggregation (one column per direct child case / patent / litigation). `LevelPolicy` already differentiates `self_plus_ccr` (Case) / `child_case` (Patent) / `child_patent` (Litigation) / `child_litigation` (Client). Recent commits 7da8802, 7e57507, 7930ee0 confirm — design merge is on `main` (b4f4b3 baseline as of this branch).
- **Frontend renderer for the SmartTimeline is `frontend/src/client/views/shape-timeline.ts` (960 LoC, hand-rolled DOM via `document.createElement`).** It already implements: vertical flow, parallel-track CSS-grid for CCR (`renderParallelTracks`), lane-strip CSS-grid for parent-node aggregation (`renderLaneStrip`), click-to-anchor inline editor, `[Track ▼]` chip, lane-filter chip multiselect, lookahead toggle. The "horizontal Gantt" mode m's brief asks about does **not** exist.
- **No chart library is in the repo.** `package.json` has only `@types/bun`. No D3, no Chart.js, no Apache ECharts, no plotly, no chartjs-node-canvas. Frontend is hand-rolled DOM/SVG via the custom TSX renderer described in `.claude/CLAUDE.md`. Adding a runtime dep would need m's explicit approval (per global rules).
- **No PDF / image-export pipeline exists either.** `internal/services/caldav_ical.go` generates VCALENDAR strings (BEGIN:VCALENDAR / BEGIN:VEVENT) for CalDAV PUT bodies, but there is no public iCal-feed download endpoint, no headless-browser dep (`chromedp` not in `go.sum`), no Go PDF lib. The only existing `Content-Disposition: attachment` header is in `internal/handlers/files.go` for the Gitea Downloads proxy.
- **Custom Views render shapes are list / cards / calendar.** `internal/services/render_spec.go` declares `RenderShape` = `ShapeList | ShapeCards | ShapeCalendar`. **There is no `ShapeTimeline` registered yet** — t-paliad-169 §8.6 reserved the slot but didn't claim it. A new chart shape would extend this enum and grow `frontend/src/views.tsx` host accordingly.
- **Mobile breakpoints in use today are 640px / 720px / 768px / 1023px** (`frontend/src/styles/global.css`). Lime green primary token is `--color-accent: var(--hlc-lime)` with light/dark variants and a `--color-accent-fg` foreground token. There is `@media print` already in the stylesheet — printing is on the table.
- **Project hierarchy depth in prod = 4 levels, 11 projects total.** A loaded Patent at the upper end has 5 child cases; a hypothetical Client could have 100+ matters. Any chart layout must answer "how does this look on a page with 5 cases × 30 events" and "with 100+ matters" — see §10.

If the live state above contradicts a memory or issue note, the live state wins.

---

## 1. Vision + scope

m's brief (verbatim 2026-05-09 18:32):

> One could chose to show the timeline in one or in separate columns and with different colors even... bigger feature development but ... a project timeline / chart would be nice in general. So we need to make some considerations on how to design one. Another aspect to this is vertical or horizontal... and an export functionality would also be great.

The **Project Timeline / Chart** is the *visualisation layer* above the SmartTimeline data substrate. Where SmartTimeline answers "what is the data", the Chart answers "how does the lawyer want to see it today, on what surface, in what shape, exported to whom".

### What this design covers

| Axis | Choices |
|---|---|
| **Layout direction** | Vertical (today) / Horizontal Gantt-strip / Hybrid |
| **Column model** | Single-column flow / Multi-column (lanes — already in substrate) |
| **Visual customisation** | Color schemes per track / kind / status / party; density modes (compact/standard/spacious); status pill / kind chip / shape variants |
| **Export** | SVG (vector) / PNG (raster) / PDF (browser-print or rasterised) / CSV (data) / JSON (data) / iCal (deadlines+appointments feed) |
| **Surfaces** | Verlauf-tab embed (existing) / `/projects/{id}/chart` standalone full-page / `RenderShape="timeline"` Custom Views |

### What stays

- **`projection_service.go` is the only data source.** No new query path. The chart is a presentation-level concern; data composition is solved.
- **`shape-timeline.ts` (vertical DOM renderer) stays** as the embed default for the Verlauf tab. We add modes alongside it; we don't tear it out.
- **`paliad.deadlines`, `paliad.appointments`, `paliad.project_events`, `paliad.deadline_rules`** schemas — unchanged. Zero migrations in this design.
- **Color tokens (`--color-accent`, `--color-bg-lime-tint`, …)** — anchor every chart palette, light/dark mode + WCAG follow for free.

### Out of scope (v1 of this feature)

- **Cross-matter chart on `/projects` list page** — bundled under the Custom-Views path (§8.3) once `RenderShape="timeline"` lands. Not v1.
- **Live collaborative cursors / annotation pins** — presentation features for a later phase, not for shipping the chart itself.
- **Rich-text editing of chart entries from inside the chart canvas** — clicks deep-link to existing detail pages. Edit-in-place is the SmartTimeline's anchor affordance and stays there.
- **Server-side PDF rendering via headless browser** — adding `chromedp` introduces a Chromium runtime dependency on the Dokploy compose host. Recommend client-side `window.print()` for v1; revisit only if user feedback says "PDFs differ across employees' browsers". See §7.3 for the trade-off in full.
- **Theming UI for end users to pick palettes** — v1 gives a small fixed palette set; a colour-picker is v2 nice-to-have only if real users ask for it.

---

## 2. Renderer choice — SVG for the Gantt mode, DOM for the flow mode

This is the load-bearing call. Five candidates surveyed:

| Renderer | Pros | Cons | Fit |
|---|---|---|---|
| **DOM/CSS grid** (existing) | Accessible by default; themable via CSS vars; free dark-mode + i18n; exportable via `window.print()` | Hard to do continuous date-axis math (Gantt scaling); heavy reflow on resize; html-to-PNG via foreignObject is browser-quirky | Best for vertical flow ✓ |
| **SVG hand-rolled** | Vector by construction → free SVG / PNG export via canvas drawImage; precise positioning math; one paint call; printable | Manual ARIA scaffolding; no automatic text-wrapping; need a layout pass | Best for horizontal Gantt ✓ |
| **`<canvas>`** | Top performance for 1000+ nodes | Zero accessibility; manual hit-testing for clicks; export needs separate path | Overkill for our scale (≤150 nodes typical) ✗ |
| **D3.js** | Battle-tested abstractions for axes / scales | ~250 KB minified, runtime data-driven DOM mutation conflicts with our IIFE-bundle pattern, would need m's package approval | Overkill, runtime cost ✗ |
| **SVG + foreignObject for text** | Vector with native HTML text wrapping | Spotty PDF and Safari support; defeats the export-for-free pitch | Avoid ✗ |

### 2.1 Recommendation

**Two renderers coexist.** Same data, different DOM:

- **`shape-timeline.ts`** (existing DOM/CSS grid, vertical) keeps powering the Verlauf-tab embed — it's small, accessible, themed.
- **`shape-timeline-chart.ts`** (new SVG) powers the standalone `/projects/{id}/chart` page in horizontal Gantt mode. Hand-rolled, no library, ~500 LoC for v1.

The horizontal Gantt page is also where the export buttons live (§7) — exporting a vertical DOM list is "open browser print and cmd-P" already, no new code needed; the Gantt is the genuinely new surface and brings PDF/SVG/PNG with it.

### 2.2 Why hand-rolled SVG over D3

We have ≤150 nodes per project, two axes (date + lane), three primitives (bar, dot, label) and one expanding need (zoom + pan, eventually). D3 ships ~250 KB to give us scales + axis generators + zoom. Our scale is `(date - earliestDate) / dayWidthPx`, a one-liner; our axis is a year/quarter tick generator, ~30 LoC; pan + zoom is `addEventListener("wheel"|"pointermove")`, ~50 LoC. The lift to write it ourselves is real but small, the runtime cost saving is real, and we keep the single-file IIFE bundle pattern intact.

If we ever hit "the layout math is too painful to maintain", D3-only-the-axis-helper or an `axes.ts` module is a refactor we can do then. v1 ships without.

### 2.3 What hand-rolled SVG looks like

One root SVG element, three layered groups:

```
<svg viewBox="0 0 W H">
  <defs>
    <pattern id="weekend"…/>      # weekend background stripe
    <linearGradient id="proj"…/>  # projected-row gradient
  </defs>
  <g class="chart-grid">          # lane separators + date-axis ticks + today rule
  <g class="chart-bars">          # one rect/g per event
  <g class="chart-labels">        # text labels (kind chip, title)
  <g class="chart-overlay">       # tooltip + selection scrim
</svg>
```

Coordinates are computed by a `layout(events, lanes, viewport)` pure function — testable, deterministic, the same on screen and on export.

---

## 3. Layout — vertical (existing) + horizontal (new)

### 3.1 Vertical (DOM, existing — no changes)

Embedded on `/projects/{id}` Verlauf tab. Today's `shape-timeline.ts` flow with date column / event card right column, "Heute →" rule, parallel tracks for CCR, lane-strip for parent-node aggregation. Nothing changes in this design — I'm explicit about that so the implementer doesn't accidentally rewrite working code.

### 3.2 Horizontal Gantt-strip (SVG, new)

The `/projects/{id}/chart` page. Time on the X axis, lanes on the Y axis. Each lane is a horizontal row; events plot as either a dot (point-in-time: deadline due-date, milestone, appointment) or a bar (range: future-projected sequence between two anchors, or appointment with end_at). Today's rule = vertical line.

```
                       ←──────── 2026 ────────→ 2027 ─────→
               ┌────────────────────────────────────────┐
   Self        │  ✓ ●─────●────────────● ░──░──░──░ │
   Hauptverf.  │     Klage  Antw.       HV   R29a R29c   │
               │                  ↑Heute                 │
               ├────────────────────────────────────────┤
   Widerklage  │                  ⊕──────░───░──░     │
   (CCR)       │                  Filed   R29d R32  │
               │                                          │
               └────────────────────────────────────────┘
   Date axis:    Q1   Q2   Q3   Q4   Q1   Q2   Q3
                 │              │
                 └ year border  └ Today rule (lime)
```

### 3.3 Layout invariants (both modes)

These rules must hold across both renderers — they're the contract that lets us swap modes without surprising the user:

1. **Past = left/below; Future = right/above; Today = lime separator.** Vertical: future at top per existing convention. Horizontal: future on right per Gantt convention. The convention flip is fine because the "today" lime separator orients the user instantly.
2. **One row = one event** in vertical; **one bar/dot = one event** in horizontal. We never group two events into one mark. Lane (column in horizontal, parallel-track-column in vertical) is the only grouping primitive.
3. **`Kind` drives shape / glyph; `Status` drives color saturation; `Track` drives column placement.** This composes orthogonally — see §5.

### 3.4 Hybrid not in v1

A "compact horizontal-strip-on-top + vertical-detail-below" hybrid (think Gmail conversation view but for matters) is a tempting third mode. **Not in v1** — adds a third renderer with no clear user request behind it. Revisit if a partner asks "I want both at once".

### 3.5 Single-column vs multi-column on horizontal

Multi-column = lanes, identical to the substrate's `LaneInfo` already. The horizontal Gantt **always multi-lanes** when there's more than one lane; collapsing all events into one row just to give a "single-column" version produces visual chaos with overlapping bars on the same date. The `[Track ▼]` filter (existing) lets the user collapse to a single track if they want a single-row view. So:

- **Substrate has 1 lane** (Case-level, no CCR): single horizontal row.
- **Substrate has 2+ lanes** (Case + CCR sub-project, OR Patent / Litigation / Client level): horizontal multi-lane Gantt with one row per lane.

This mirrors the lane-mode the vertical renderer already uses (`renderLaneStrip`) — same data shape, different rendering.

---

## 4. Column model — extend `LaneInfo`, no new substrate concept

The substrate already discriminates lanes via `levelPolicy(projectType)` returning `LaneAxis`. The chart inherits that vocabulary for free.

### 4.1 What the chart adds

Two read-only filters at chart mount time, both client-side (no backend changes):

```ts
interface ChartViewState {
  layout:    "vertical" | "horizontal";  // default "horizontal" on /chart, "vertical" on Verlauf
  columns:   "auto" | "single" | "lanes";  // "auto" reads lanes.length from substrate
  density:   "compact" | "standard" | "spacious";
  palette:   "default" | "high-contrast" | "print" | "kind-coded" | "track-coded";
  zoom:      number;  // px-per-day; default 4
  range?:    { from: string; to: string };  // ISO; defaults to substrate's earliest..latest+30d
}
```

`columns="auto"` is the default — the substrate decides. `columns="single"` collapses everything into one row (useful when comparing dates across CCR + parent on horizontal). `columns="lanes"` forces lane mode even when only one lane exists (useful for screenshot consistency).

### 4.2 What the chart does not add to the substrate

**No new lane axis.** If the brief later wants "lanes per party" (claimant vs defendant) or "lanes per court country", that becomes a new `LaneAxis` value in `levelPolicy` — substrate work, not chart work. The chart is a render of whatever lanes the substrate produced.

This boundary is important: the chart can be improved / re-skinned / re-renderered without touching the data layer, and substrate improvements (new lane axes, new event kinds) automatically reach both renderers.

---

## 5. Color schemes

The brief asks for *"different colors even"*. Three palette dimensions are useful — and they're orthogonal, so a user picks one at a time.

### 5.1 Palette presets (built-in, fixed)

| Preset | What's color-coded by | Use case |
|---|---|---|
| **`default`** | Lane (`--color-accent` for parent, neutral grey for CCR/parent_context) | Embed in Verlauf, partner glance |
| **`kind-coded`** | Event kind (deadline = blue, appointment = amber, milestone = lime, projected = soft-grey) | "Show me what's a hearing vs a deadline at a glance" |
| **`track-coded`** | Track tag (parent / counterclaim / parent_context — three distinct hues) | CCR-heavy projects where the track is the most important axis |
| **`high-contrast`** | Status only (done = green ✓; open = amber; overdue = red; predicted = light-grey) | Print-friendly, accessibility-first, screenshot for client |
| **`print`** | Black / white / one-stripe-pattern (no color at all) | Faxable, b&w-printable, redactable |

All five palettes are CSS custom-property *swaps* on the chart root — the renderer reads `var(--chart-bar-deadline)`, the palette CSS file defines what each is. No JS branching in the renderer.

### 5.2 Token surface (CSS vars)

```css
.smart-timeline-chart {
  --chart-bar-deadline:     var(--color-accent);
  --chart-bar-appointment:  #f5a623;
  --chart-bar-milestone:    var(--hlc-midnight);
  --chart-bar-projected:    var(--color-text-subtle);
  --chart-bar-overdue:      #d62828;

  --chart-track-parent:           var(--color-accent);
  --chart-track-counterclaim:     #6e8a8c;       /* desaturated teal */
  --chart-track-parent-context:   var(--color-text-subtle);

  --chart-today-rule:       var(--color-accent);
  --chart-grid-line:        var(--color-border);
  --chart-bg:               var(--color-bg);
  --chart-bg-weekend:       var(--color-bg-subtle);
}

.smart-timeline-chart[data-palette="kind-coded"] {
  /* override --chart-bar-* — track tokens stay neutral so kind dominates */
  --chart-track-parent:        var(--color-text-subtle);
  --chart-track-counterclaim:  var(--color-text-subtle);
}

.smart-timeline-chart[data-palette="print"] {
  --chart-bar-deadline:    #000;
  --chart-bar-appointment: #555;
  --chart-bar-milestone:   #000;
  --chart-bar-projected:   #aaa;
  /* …and so on; the palette is a pure CSS swap */
}
```

### 5.3 Why no per-user color picker in v1

A per-user palette picker is a feature with a long tail (storage in user prefs, defaults vs overrides, migration when palette tokens change names, theme conflicts with light/dark). The fixed-preset surface answers 90 % of "I want different colors" with 10 % of the cost. If real users say "I want my-firm-blue", we add a v2 admin-level palette override (`paliad.firm_palette` row keyed by `FIRM_NAME`).

### 5.4 Light / dark / print

Existing dark-mode flip works automatically — the chart palette tokens *reference* `--color-*` family which is already dark-mode-aware. No extra surface. `@media print` overrides force the `print` palette regardless of the user-selected one — a print-out is always b&w-friendly.

---

## 6. Density + visual variants

### 6.1 Density modes

```ts
type Density = "compact" | "standard" | "spacious";
```

- `compact`: lane height 24px, bar height 12px, label inline-only (no description). Use for "1000-row birds-eye" lane mode.
- `standard` (default): lane height 40px, bar height 20px, label + status pill.
- `spacious`: lane height 64px, bar height 28px, label + pill + description below.

CSS-driven via `[data-density="…"]` on the chart root. The bar & dot SVG geometry is computed from a single `--lane-height` var; switching density is a re-layout pass, not a re-render.

### 6.2 Status / kind / shape variants

The visual encoding stays consistent with `shape-timeline.ts`:

| Kind | Vertical glyph | Horizontal mark |
|---|---|---|
| `deadline` | `…` / `!` (open / overdue) | Filled circle on due date; ring around it for "open" |
| `appointment` | `▢` | Bar from `start_at` to `end_at` (or fixed-width if same-day) |
| `milestone` | `⊕` | Diamond at the date |
| `projected` | `░` | Hatched circle (predicted), dashed-circle (court_set), amber-outlined (predicted_overdue) |

Colour saturation drives `Status` independently: done = full color; open = lighter; predicted = 50% opacity; overdue = red overlay.

The CSS for the vertical mode already has these variants — the SVG mode replicates them via `<circle>` / `<rect>` + `fill` / `stroke-dasharray` attributes. Same visual language across modes is a non-negotiable.

---

## 7. Export pipeline

This is the most-requested part of the brief. Five formats; client-side only (no Go PDF dep, no headless browser).

### 7.1 The five formats

| Format | Content | Path | Why this path |
|---|---|---|---|
| **SVG** | Vector chart as-rendered | Browser: `new XMLSerializer().serializeToString(svgEl)` → Blob → download | Free — SVG IS our render. |
| **PNG** | Raster chart at 2× device pixel ratio | Browser: SVG → `<img>` → `<canvas>.drawImage` → `canvas.toBlob()` | One stdlib API call chain. |
| **PDF** | Print-formatted page | `window.print()` with `@media print` stylesheet; user picks "Save as PDF" | Reuses browser's hardened PDF engine — no Go PDF dep, no Chromium pinned to Dokploy. |
| **CSV** | Tabular data, flat | Server: `GET /api/projects/{id}/timeline.csv` → text/csv | Cleanest for "Excel this" use case. |
| **JSON** | Data-as-stored | Server: `GET /api/projects/{id}/timeline?format=json` (existing endpoint, alt content type) | Zero new code beyond a `Content-Disposition: attachment`. |
| **iCal** | Deadlines + appointments as VEVENT | Server: `GET /api/projects/{id}/timeline.ics` reusing `caldav_ical.go` formatter | Lawyers can subscribe in Outlook / Apple Calendar. |

### 7.2 Why client-side for SVG/PNG/PDF, server-side for CSV/JSON/iCal

- **SVG/PNG/PDF need the rendered pixel layout.** Client has it, server doesn't (without a headless browser). Doing it on the client is a 30 LoC flow per format using stdlib browser APIs.
- **CSV/JSON/iCal are pure data.** Server-side they hit the existing `ProjectionService` and stream straight to the client. CSV is `encoding/csv`; JSON is `json.Marshal`; iCal reuses the existing string-builder. Three new handlers, ~120 LoC total.

### 7.3 Why NOT server-side PDF

The clean alternative is "spin up `chromedp` on the Dokploy compose host, render the chart page, return PDF". Trade-off:

- Pro: one canonical PDF render, works the same regardless of user's browser.
- Con: adds a Chromium runtime dep to the paliad Docker image (~150 MB), spins up a child process per export, opens an attack surface (someone exports a hostile SVG → Chromium handles it → CVE), and needs a queue (PDF render is 1-3s; a clicky user can DoS the box).

Browser print, by contrast, is in-process, free, sandboxed, and produces fine-looking PDFs. It loses pixel-perfect cross-browser parity, but lawyers care about content, not subpixel kerning.

**Recommend client-side print for v1.** Revisit if lawyers complain about cross-browser PDF differences. Adding `chromedp` later is a one-PR move; designing it into v1 risks shipping infra weight we may never need.

### 7.4 Print-mode CSS

The PDF path needs a robust `@media print`:

- Fix the chart to fit on landscape A4 (1100 × 760 px viewport).
- Force `palette="print"`.
- Hide chrome (sidebar, footer, header → `.print-hide` class on existing layout).
- Show project metadata (title, parties, court, proceeding type) as a printed header.
- Page-break logic: each lane group fits on one page; if a lane has too many events, split horizontally by year.

This print stylesheet can be extracted as `frontend/src/styles/chart-print.css` so it's auditable separately from the screen styles.

### 7.5 Export menu UI

Single button on the chart page header opens a menu:

```
[ ⤓ Export ▼ ]
   ├─ SVG (Vektorgrafik)
   ├─ PNG (Bild, 2× HiDPI)
   ├─ PDF (Drucken)
   ├─ ───
   ├─ CSV (Excel-Tabelle)
   ├─ JSON (Rohdaten)
   └─ iCal (.ics — Outlook / Apple)
```

Translated via existing i18n (`projects.detail.chart.export.*`). One menu, one keyboard shortcut (`Cmd+E` / `Ctrl+E`) opens it.

### 7.6 What's exported in CSV

Flat schema, one row per `TimelineEvent`:

```
project_id,project_title,kind,status,track,lane_id,lane_label,date,
title,description,rule_code,depends_on_rule_code,depends_on_date,
sub_project_id,sub_project_title,bubble_up,deadline_id,appointment_id,
project_event_id
```

Columns mirror the wire `TimelineEvent` struct. UTF-8 with BOM (Excel-DE compat). Date format ISO-8601.

### 7.7 What's exported in JSON

The wire `ResponseEnvelope` directly: `{events: TimelineEvent[], lanes: LaneInfo[], meta: ProjectionMeta, exported_at, exported_by, project_id}`. Stable JSON schema; `meta` lets a future re-importer reconstruct the projection state exactly.

### 7.8 What's exported in iCal

Only `kind IN ("deadline", "appointment")` (projected rows are not stable enough to commit to a calendar). VEVENT block per row reuses `caldav_ical.go` formatter; UID is `paliad-deadline-<id>@paliad.de` so re-export overwrites prior subscription. Future projected rows omitted by design — they would clutter every lawyer's Outlook with rule_code-derived events that may or may not fire on the predicted date.

---

## 8. Surfaces — three places the chart shows up

### 8.1 Verlauf tab embed (`/projects/{id}` — existing)

Vertical DOM mode only (existing `shape-timeline.ts`). Density `standard`. Palette `default`. Lane count obeys substrate. **No changes** in this design — the embed stays exactly as it is. The chart-mode opt-in lives below the tab.

A new "**Als Chart anzeigen ↗**" link in the SmartTimeline header opens `/projects/{id}/chart` in a new tab. Optionally (Q3 below) we could host a chart inline with a `[Layout: ▽ Vertikal | ▷ Horizontal]` toggle.

### 8.2 Standalone `/projects/{id}/chart` (new)

Full-page surface optimized for the horizontal SVG renderer. Layout:

```
┌───────────────────────────────────────────────────────────────────────┐
│ Siemens AG ./. Huawei — EP3456789 — UPC-CFI München                  │
│ Verfahrenstyp: UPC-Verletzung   Anker: Klageschrift @ 2026-04-29     │
│                                                                       │
│ [Layout ▷] [Spalten Auto] [Dichte Standard] [Palette Default] [Export ⤓]│
├───────────────────────────────────────────────────────────────────────┤
│ ━━━━ FilterBar (existing primitive) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
├───────────────────────────────────────────────────────────────────────┤
│                                                                       │
│             ┌──── Horizontal SVG chart (full bleed) ───┐              │
│             │                                          │              │
│             │     ←─── 2026 ────→ 2027 ────→            │              │
│             │  Self  ●─●───●──── ░──░──░                │              │
│             │  CCR     ⊕────░───░──░                    │              │
│             │                                          │              │
│             └──────────────────────────────────────────┘              │
│                                                                       │
└───────────────────────────────────────────────────────────────────────┘
```

URL convention: `/projects/{id}/chart?layout=horizontal&palette=default&density=standard&zoom=4`. State persists in URL so the link is shareable and copy-pasteable. localStorage caches the last chosen state per user as the default.

### 8.3 Custom Views shape (`shape="timeline"`)

Registers `ShapeTimeline RenderShape = "timeline"` in `internal/services/render_spec.go` and adds a corresponding `frontend/src/client/views/shape-timeline-chart.ts` view-host wrapper that adapts a `ViewRow[]` → `TimelineEvent[]` array. This unlocks **cross-project timelines** as a Custom View — "all my UPC matters" or "everything where I'm in the team" rendered as one chart.

ViewRow → TimelineEvent is a lossy shim: `kind` and `track` map directly; `date` reuses `event_date`; cross-project lanes are auto-derived from `project_id`. Projected rows are not surfaced from `ViewService` (it doesn't run the calculator) — Custom Views show actuals only. We document that limitation, ship the shape, and revisit later if needed.

This is **§8.3's gating**: the standalone page (§8.2) and embed (§8.1) ship before the Custom Views shape. The shape is Slice 4 — last, optional, lower-priority.

---

## 9. Mobile behaviour

Three breakpoints, one rule:

| Width | Vertical embed | Standalone chart |
|---|---|---|
| ≥1024 px (desktop) | Existing | Horizontal SVG, full-bleed |
| 640–1023 px (tablet) | Existing | Horizontal SVG, narrower viewport, density auto-switches to compact |
| <640 px (phone) | Existing | **Force vertical** — horizontal Gantt on phone is unreadable |

The "force vertical on phone" rule is enforced server-side via the Accept-CH `Sec-CH-UA-Mobile` header (defensive) and client-side via `window.matchMedia("(max-width: 640px)")`. The user can override but the default flips.

A horizontal-on-phone variant with `overflow-x: scroll` is technically possible but UX-poor — date axis disappears off-screen, lawyer can't see context. Force vertical, force collapsing of lanes into stacked sections, keep the export menu reachable.

---

## 10. Performance

### 10.1 Current numbers

- Patent (5 child cases × 30 events) = 150 nodes typical
- Client (100+ matters) = 100s of lane rows; aggregation already sub-filters to milestones-only at Client level → <500 nodes
- Backend projection cost: ~285 ms cold cache for one project (per t-paliad-169 §13). Backend is not the bottleneck.

### 10.2 Where each renderer caps

| Renderer | Comfortable | Stressed | Breaks |
|---|---|---|---|
| DOM grid (vertical) | ≤300 nodes | 300-1000 (sluggish reflow) | 1000+ (frame drops on scroll) |
| Hand-rolled SVG | ≤1000 nodes | 1000-3000 (slow zoom / pan) | 3000+ (paint cost) |
| Canvas (not chosen) | ≤10 000 nodes | — | — |

We're sitting in the **comfortable band for both** for any plausible Paliad project. Numbers above 1000 happen only in pathological "show all my Client's matters" scenarios — and those are bound by levelPolicy aggregation already (Client-level Custom Views).

### 10.3 Mitigations if a real project exceeds the comfort zone

- **Lookahead cap** (existing): `?lookahead=N` keeps projected nodes capped at 7 by default (50 max). Future-only, doesn't help if there are 1000 actuals.
- **Date-range filter**: chart shows only events in a date window (defaults `earliest..latest+30d` — no implicit cap). For pathological cases, user can narrow the range.
- **Lane filter** (existing): hide / dim selected lanes on multi-lane render.

If a single matter genuinely has 1000+ actuals, the user has a deeper data-discipline problem and the right answer is to escalate, not to optimize a chart for it.

### 10.4 SVG paint budget

A 200-event chart in horizontal mode is ~600 SVG primitives (200 bars/dots × 3 elements: shape + label + tooltip-trigger). One initial paint = <50 ms on a low-end laptop. Subsequent zoom / pan re-runs the layout fn (10 ms) and re-attributes existing nodes (no re-create) — fast. We do not need virtualization in v1.

---

## 11. Phasing — 4 sequential slices

Each slice independently shippable. m's go/no-go gate after each.

### Slice 1 — Standalone `/projects/{id}/chart` page + horizontal SVG renderer (no exports yet)

What lands:

- New page route `GET /projects/{id}/chart` (handler `internal/handlers/chart_pages.go`, ~50 LoC). Reuses existing project gate.
- New `frontend/src/projects-chart.tsx` page TSX (renders shell + mount target). ~100 LoC.
- New `frontend/src/client/views/shape-timeline-chart.ts` SVG renderer (~500 LoC). Pure-function `layout(events, lanes, viewport)` + `paint(layout, palette, root)`.
- Reuses the existing `GET /api/projects/{id}/timeline` endpoint — no backend change.
- Mode toggle on Verlauf tab: `[Als Chart anzeigen ↗]` link → opens `/chart`.
- Default palette + standard density + auto columns. **No** export, **no** palette picker, **no** density picker yet — controls render as inert chips.

What it gives m: the horizontal Gantt rendering, end-to-end. Lawyer can open `/chart`, see the matter in horizontal layout, share the URL.

### Slice 2 — Export pipeline (SVG / PNG / PDF / CSV / JSON / iCal)

What lands:

- Client-side: `frontend/src/client/views/chart-export.ts` (~150 LoC) handling SVG → PNG conversion, PDF print invocation, blob downloads. Three new i18n keys per format.
- Server-side: `internal/handlers/projection.go` gains 3 new handlers — `handleProjectTimelineCSV`, `handleProjectTimelineJSON` (alt `?format=json` on existing), `handleProjectTimelineICS`. Each ~30 LoC.
- New `frontend/src/styles/chart-print.css` for `@media print` and palette swap.
- Export menu UI on chart page header.

What it gives m: every export format the brief asked for, no infra additions, lawyer-shareable PDFs.

### Slice 3 — Density + palette + zoom controls

What lands:

- Density toggle (`compact / standard / spacious`) — pure CSS-var + `[data-density]` attr swap, no re-fetch.
- Palette picker (`default / kind-coded / track-coded / high-contrast / print`) — same pattern.
- Zoom in / out controls + pan (mousewheel + drag).
- Date-range narrower (FilterBar `time` axis already exists — wire it to chart viewport).
- localStorage persistence per-user-per-project.

What it gives m: full visual customisation per the brief.

### Slice 4 — Custom Views integration (`shape="timeline"`)

What lands:

- Register `ShapeTimeline RenderShape = "timeline"` in `internal/services/render_spec.go` + validator.
- New `frontend/src/client/views/shape-timeline-cv.ts` view-host adapter. Reuses Slice 1's renderer; adapts `ViewRow[]` to `TimelineEvent[]`.
- `frontend/src/views.tsx` shape-switcher gets the 4th button.
- Documented limitation: projected rows not surfaced in Custom Views.

What it gives m: "all my UPC matters as one chart" via Custom Views — cross-project chart on the existing CV substrate.

### What's NOT in any slice (v2 nice-to-haves)

- Per-user palette picker beyond fixed presets.
- Server-side PDF render via `chromedp`.
- Live collaborative cursors / annotation pins.
- Animation / transitions when zoom changes.
- Hybrid layouts (compact-strip + detail-list).
- Color-coding with custom user-defined rules.

---

## 12. Files implementer will touch (Slice 1 only)

**Backend (Go):**
- `internal/handlers/chart_pages.go` — new, ~50 LoC. `handleProjectChartPage(w, r)` returns the rendered TSX shell. Auth + project visibility gates as on `/projects/{id}`.
- `internal/handlers/handlers.go` — register `GET /projects/{id}/chart`.

**Frontend (TS / TSX):**
- `frontend/src/projects-chart.tsx` — new, ~100 LoC. Page shell with mount target + page-level controls scaffold (chips inert in Slice 1).
- `frontend/src/client/views/shape-timeline-chart.ts` — new, ~500 LoC. SVG renderer:
  - `layout(events: TimelineEvent[], lanes: LaneInfo[], viewport: Viewport): ChartLayout` — pure function returning bar/dot positions + axis ticks + today-rule x.
  - `paint(layout: ChartLayout, palette: Palette, root: SVGSVGElement): void` — DOM-mutates the root.
  - `mount(host: HTMLElement, opts: ChartMountOpts): ChartHandle` — composes layout + paint + interaction (click → deep-link, hover → tooltip).
- `frontend/src/client/projects-chart.ts` — new, ~150 LoC. Page boot: fetch `/api/projects/{id}/timeline`, mount renderer, wire URL state ↔ control chips (inert), wire SmartTimeline embed's `[Als Chart anzeigen ↗]` link from `frontend/src/client/projects-detail.ts`.
- `frontend/src/styles/global.css` — `.smart-timeline-chart-*` CSS additions, ~120 LoC. Including the palette token swap CSS but not yet wired to a picker.
- `frontend/src/client/i18n.ts` — ~25 keys under `projects.detail.chart.*` (page title, control labels, default-palette-name, etc.) DE+EN.
- `frontend/build.ts` — register the new page bundle.

**Tests:**
- `frontend/src/client/views/shape-timeline-chart.test.ts` — new, pure-function tests for `layout()` (ranges, tick generation, lane stacking, today-rule positioning, undated-row handling).

Slices 2-4 are scoped in §11; coder picks them up after m's gate.

---

## 13. Trade-offs flagged

- **SVG accessibility.** Hand-rolled SVG needs explicit ARIA scaffolding (`role="img"` + `<title>` + `<desc>` per group, `aria-label` per event mark) to be screen-reader-readable. This is real implementation work — DOM mode gets it for free. Mitigation: lockdown `role` and label conventions in the renderer and test with VoiceOver / NVDA before Slice 1 merges.
- **Print-CSS quirks.** `window.print()` PDFs will look slightly different across Chrome / Safari / Firefox. Lawyers comparing two exports may notice. Mitigation: documentation states "use Chrome for archival exports". Pursue chromedp only if real complaints surface.
- **No virtualization in v1.** A 1000-event chart is not virtualized — every node is in the DOM/SVG tree. Mitigation: existing levelPolicy aggregation + lookahead caps keep node counts bounded for plausible projects. Add virtualization only if a real project exceeds the comfort band.
- **Two renderers means two paths to maintain.** A bug in vertical-mode rendering doesn't auto-fix the horizontal mode. Mitigation: both render the **same** `TimelineEvent` / `LaneInfo` data; the discriminator is just the layout fn. Rendering bugs tend to be in shared event-mark visual tokens (color, status pill) which CSS-token-swap centralizes anyway.
- **Custom Views adapter is lossy.** Cross-project chart in CV doesn't show projected rows. Some users might expect them. Mitigation: in-page tooltip on first CV-chart open: "Custom Views show actual events only. Open the project's `/chart` for projected rules." A future v2 could push the projection through ViewService but the substrate redesign is non-trivial.
- **Date-range default.** Defaulting to `earliest_event..latest_event+30d` means a matter with one ancient deadline forces the whole span on every render. Mitigation: clamp default range to `today-1y..today+1y`, with a chip for "Alles anzeigen" to expand. Keeps the typical render compact.
- **`/chart` URL collision.** `/projects/{id}/chart` doesn't conflict with any existing route, but adding `/chart` at the project level forces the route table to stay tidy. Defensive: implementer greps `internal/handlers/handlers.go` before adding to confirm no collision.
- **Browser-print PDF on Safari shows the menu bar.** Cosmetic; print stylesheet's `@page` directive helps, but Safari ignores some rules. Mitigation: documentation; lawyer-facing exports recommend Chrome.

---

## 14. Open questions for m

Listed with my (inventor) pick where I have one — m decides.

**Q1 — Default landing on `/projects/{id}/chart`: horizontal Gantt or vertical (with a toggle)?**
My pick: horizontal Gantt as the default. The whole reason `/chart` exists is the horizontal mode; defaulting to vertical would make it a duplicate of Verlauf. Add a `[Layout ▷|▽]` toggle for users who want vertical-on-bigscreen.

**Q2 — Should the chart page replace Verlauf when accessed at desktop width, or stay a separate URL?**
My pick: separate URL. Verlauf is the "scan & action" tab (click rows to mark deadlines done, add notes). Chart is the "share & overview" surface. Conflating them risks losing the inline-action affordance Verlauf was built for.

**Q3 — Should the chart be embeddable inside the Verlauf tab (with a layout toggle), or only standalone?**
My pick: standalone in Slice 1; if user feedback says "I want to see horizontal on the project page directly", add the embed in a follow-up slice. Embedding doubles render cost on every project page open and creates layout pressure on the existing tab UI.

**Q4 — Chromedp / server-side PDF: rule out for v1, or design in?**
My pick: rule out. Browser-print PDFs are good enough; Chromium-on-Dokploy is a heavy dep. Keep the door open by abstracting the export-button handler so a future server-side path is a one-route addition.

**Q5 — Color palette presets: ship the full 5 in Slice 3, or just `default` + `print` for safety?**
My pick: ship all 5. The palette mechanism is just CSS-var swaps; adding the other three is hours of design polish, not weeks of work. More options give more lawyers their preferred read.

**Q6 — iCal export: only deadlines + appointments (recommendation), or include projected too?**
My pick: only deadlines + appointments. Subscribing to a calendar that fills with rule_code-derived predicted dates that never fire would erode trust. Future projected = visualisation only, never calendar artifacts.

**Q7 — Custom Views integration (`shape="timeline"`): Slice 4 priority, or descope?**
My pick: keep as Slice 4 but explicit go/no-go after Slice 3 ships. The cross-project chart is a *cool* demo but not in the original brief — descoping if real users haven't asked is fine.

**Q8 — Date-range default on `/chart`: data-driven (`earliest..latest+30d`) or fixed (`today-1y..today+1y`)?**
My pick: fixed `today-1y..today+1y`, with a chip "Alles anzeigen" expanding. Old matters with one historical deadline shouldn't force a 5-year span on first render.

**Q9 — Should the chart support project comparison (chart 2-3 projects side-by-side)?**
My pick: no — out of scope for this feature. That's a Custom Views job (multi-project query → chart shape), not a per-project surface concern.

**Q10 — Should we expose a permalink that captures *zoom + range + palette + density + lane-filter*?**
My pick: yes, via URL query params (already designed in §8.2). Sharing a chart-URL via WhatsApp / email then renders the same view for the recipient.

**Q11 — Mobile: vertical-only fallback, or horizontal-with-scroll?**
My pick: vertical-only on phones (<640px). Horizontal-with-scroll loses the date axis off-screen. Tablet (640-1023px) keeps horizontal in compact density.

**Q12 — On the SmartTimeline (Verlauf embed), do we also add an inline horizontal mode (Q3 follow-up)?**
My pick: NO in v1. The standalone `/chart` is the new surface; Verlauf stays vertical. Adding both modes inline-Verlauf doubles the test matrix without clear user demand yet.

---

## 15. Recommendation for implementer

Pattern-fluent Sonnet coder. Slice 1 is the heaviest (new SVG renderer, new page, new TSX shell). Slice 2 needs careful CSS print-mode tuning — best paired with browser-screenshot iteration. Slice 3 is mostly CSS-token plumbing + UI controls. Slice 4 is the lightest if Slice 1 left the renderer well-decomposed.

Before Slice 1, the coder should sketch the `layout(events, lanes, viewport)` function on paper / a tests file — that's where the math lives, and getting it right deterministically is the difference between "works" and "subtle render glitches in obscure date ranges". Pure-function with table-driven tests for `layout()` is the correct approach.

Faraday (this worktree) parks. Not pre-emptively flipping to coder — m gates.

---

**DESIGN READY FOR REVIEW**
