diff --git a/.planning/phases/phase-4/04-UI-SPEC.md b/.planning/phases/phase-4/04-UI-SPEC.md new file mode 100644 index 0000000..927b026 --- /dev/null +++ b/.planning/phases/phase-4/04-UI-SPEC.md @@ -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 | `` 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