docs(phase-4): UI design contract for heatmap interaction
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
55acae8c8b
commit
63f483a173
1 changed files with 196 additions and 0 deletions
196
.planning/phases/phase-4/04-UI-SPEC.md
Normal file
196
.planning/phases/phase-4/04-UI-SPEC.md
Normal 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
|
||||||
Loading…
Add table
Reference in a new issue