docs(phase-4): UI design contract for heatmap interaction

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christopher Mühl 2026-04-08 15:12:19 +02:00
parent 55acae8c8b
commit 63f483a173
No known key found for this signature in database
GPG key ID: 925AC7D69955293F

View file

@ -0,0 +1,196 @@
---
phase: 4
slug: heatmap-interaction
status: draft
shadcn_initialized: false
preset: none
created: 2026-04-08
---
# Phase 4 — UI Design Contract
> Visual and interaction contract for Heatmap Interaction phase. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none (Kimai plugin — not a standalone frontend app) |
| Preset | not applicable |
| Component library | Tabler UI (via Kimai's tabler-bundle) |
| Icon library | Tabler Icons (available via Kimai host) |
| Font | `var(--tblr-font-sans-serif)` — inherited from Kimai/Tabler theme |
---
## Spacing Scale
Declared values (must be multiples of 4):
| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Cell gap between heatmap rects |
| sm | 8px | Padding within filter dropdown area |
| md | 16px | Gap between heatmap SVG and filter dropdown |
| lg | 24px | Card body padding (inherited from Tabler card) |
Exceptions: Heatmap cell size is dynamic (computed to fill width, capped at 18px) — not a spacing token. Cell gap is 2px (established in Phase 3, maintained for continuity).
---
## Typography
| Role | Size | Weight | Line Height |
|------|------|--------|-------------|
| Body | 13px (0.8125rem) | 400 | 1.5 |
| Label | 10px | 400 | 1.2 |
| Dropdown | 13px (0.8125rem) | 400 | 1.5 |
Source: Phase 3 established 13px for tooltip text and 10px for SVG labels. This phase inherits those values. Dropdown text uses Tabler's `form-select-sm` which renders at 0.8125rem.
---
## Color
| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | `var(--tblr-bg-surface)` | Card background, tooltip background |
| Secondary (30%) | `var(--tblr-bg-surface-secondary)` | Empty heatmap cells, dropdown background |
| Accent (10%) | GitHub green scale: `#9be9a8`, `#40c463`, `#30a14e`, `#216e39` | Heatmap cells with data only |
| Hover highlight | `rgba(255,255,255,0.15)` (dark) / `rgba(0,0,0,0.08)` (light) | Cell hover state for click affordance |
| Border | `var(--tblr-border-color)` | Tooltip border, dropdown border |
Accent reserved for: heatmap data cells only. The hover highlight is an overlay opacity shift on all cells (both data and empty) to signal clickability.
No destructive color needed in this phase.
---
## Interaction Contract
### Cell Click Navigation
| Property | Value |
|----------|-------|
| Cursor | `pointer` on all `.heatmap-cell` rects |
| Hover effect | Opacity shift: cell gets `opacity: 0.75` on hover (from default `1.0` for data cells, visually shifts empty cells too) |
| Click target | Full cell rect area (cellSize x cellSize) |
| Navigation URL | `/en/timesheet/?daterange={YYYY-MM-DD}` |
| With project filter | `/en/timesheet/?daterange={YYYY-MM-DD}&projects[]={projectId}` |
| Empty cell behavior | Clickable — navigates to timesheet for that date (user may want to add time) |
| Navigation method | `window.location.href` assignment (full page navigation, not SPA) |
### Filter Dropdown
| Property | Value |
|----------|-------|
| Position | Right side of heatmap SVG, using spare horizontal space in the card |
| Layout | Flexbox row: `[heatmap SVG flex:1] [filter area width:auto]` |
| Element | `<select>` with Tabler class `form-select form-select-sm` |
| Default option | "All Projects" (value: empty string, no filter applied) |
| Option format | Project name as text, project ID as value |
| Width | `auto` with `min-width: 140px`, `max-width: 200px` |
| Vertical alignment | Top-aligned with the heatmap SVG |
| Label | No visible label — the "All Projects" default option serves as the label. Add `aria-label="Filter by project"` for accessibility. |
### Filter Behavior
| Property | Value |
|----------|-------|
| Trigger | `change` event on the `<select>` element |
| API call | `GET {data-url}?project={selectedValue}` (omit param when "All Projects") |
| Loading state | No spinner — the re-render is fast enough on local data. If fetch takes >0ms, existing heatmap stays visible until new data arrives. |
| Re-render | Call `renderHeatmap(container, newData)` — this clears and redraws (established pattern) |
| Color scale | Recalculates on filtered data (max hours changes per project) |
| Error handling | On fetch failure, keep current heatmap visible. Log error to console. |
| URL state | Filter selection is NOT persisted in browser URL (widget is embedded in dashboard) |
### Project List Population
| Property | Value |
|----------|-------|
| Source | New data attribute `data-projects` on the container div, JSON-encoded array of `{id, name}` |
| Populated by | Twig template, from a controller/service that queries the user's projects |
| Format | `[{"id": 1, "name": "Project A"}, {"id": 2, "name": "Project B"}]` |
| Sort order | Alphabetical by name |
---
## Copywriting Contract
| Element | Copy |
|---------|------|
| Filter default option | "All Projects" |
| Filter aria-label | "Filter by project" |
| Empty state (no data) | "No tracking data available" (established in Phase 3, unchanged) |
| Empty state (filtered, no data) | "No tracking data for this project" |
| Error state (fetch failure) | "Failed to load heatmap data" (established in Phase 3, unchanged) |
| Tooltip format | `{Day, Mon D, YYYY}` + newline + `{N.N}h ({N} entries)` (established in Phase 3) |
No destructive actions in this phase.
---
## CSS Additions
New rules to add to `Resources/public/heatmap.css`:
```css
/* Click affordance */
.heatmap-cell {
cursor: pointer;
transition: opacity 0.1s ease;
}
.heatmap-cell:hover {
opacity: 0.75;
}
/* Layout: heatmap + filter side by side */
.heatmap-wrapper {
display: flex;
align-items: flex-start;
gap: 16px;
}
.heatmap-wrapper .heatmap-svg-area {
flex: 1;
min-width: 0;
overflow-x: auto;
}
.heatmap-wrapper .heatmap-filter {
flex-shrink: 0;
padding-top: 20px; /* align with heatmap grid, below month labels */
}
.heatmap-filter select {
min-width: 140px;
max-width: 200px;
}
```
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| Not applicable | N/A | N/A |
This project uses no component registry. All UI is hand-built SVG (d3.js) and native HTML with Tabler CSS classes.
---
## Checker Sign-Off
- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS
**Approval:** pending