kimai-plugin-heatmap/.planning/phases/phase-4/04-UI-SPEC.md
Christopher Mühl 63f483a173
docs(phase-4): UI design contract for heatmap interaction
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 15:12:19 +02:00

6.4 KiB

phase slug status shadcn_initialized preset created
4 heatmap-interaction draft false none 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:

/* 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