--- 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 | `` 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