263 lines
9.3 KiB
Markdown
263 lines
9.3 KiB
Markdown
---
|
|
phase: 7
|
|
slug: mode-switcher-week-mode
|
|
status: draft
|
|
shadcn_initialized: false
|
|
preset: none
|
|
created: 2026-04-09
|
|
---
|
|
|
|
# Phase 7 — UI Design Contract
|
|
|
|
> Visual and interaction contract for the mode switcher, week-mode renderer, and hours/count toggle. Generated by gsd-ui-researcher.
|
|
|
|
---
|
|
|
|
## Design System
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Tool | none (Kimai plugin -- no shadcn) |
|
|
| Preset | not applicable |
|
|
| Component library | Tabler UI (bundled with Kimai) |
|
|
| Icon library | none required this phase |
|
|
| Font | `var(--tblr-font-sans-serif)` (inherited from Kimai/Tabler) |
|
|
|
|
---
|
|
|
|
## Spacing Scale
|
|
|
|
Declared values (must be multiples of 4):
|
|
|
|
| Token | Value | Usage |
|
|
|-------|-------|-------|
|
|
| xs | 4px | Gap between week-mode cells and labels, segmented control internal padding |
|
|
| sm | 8px | Gap between mode switcher and metric toggle controls |
|
|
| md | 16px | Gap between SVG area and filter dropdown (existing), stats row padding-top at 12px is the one exception |
|
|
| lg | 24px | Not used this phase |
|
|
| xl | 32px | Not used this phase |
|
|
|
|
Exceptions: Stats row uses 12px padding-top (existing convention from `heatmap-stats`). Week-mode cell gap uses 4px between cells.
|
|
|
|
---
|
|
|
|
## Typography
|
|
|
|
| Role | Size | Weight | Line Height | CSS Source |
|
|
|------|------|--------|-------------|------------|
|
|
| Body / Stats | 13px (0.8125rem) | 400 | 1.5 | Existing `.heatmap-stats`, `.heatmap-tooltip` |
|
|
| Label (weekday, axis) | 10px | 400 | 1.2 | Existing `.heatmap-label` |
|
|
| Control text (segmented buttons) | 13px (0.8125rem) | 400 (normal), 600 (active) | 1.5 | Tabler `nav-link` default |
|
|
| Tooltip | 13px (0.8125rem) | 400 | 1.4 | Existing `.heatmap-tooltip` |
|
|
|
|
All fonts use `var(--tblr-font-sans-serif)`. No custom font declarations.
|
|
|
|
---
|
|
|
|
## Color
|
|
|
|
This phase introduces no new colors. All values come from Kimai's Tabler CSS variables and the existing heatmap color scale.
|
|
|
|
| Role | Value | Usage |
|
|
|------|-------|-------|
|
|
| Dominant (60%) | `var(--tblr-bg-surface)` | Widget card background, tooltip background |
|
|
| Secondary (30%) | `var(--tblr-bg-surface-secondary)` | Empty heatmap cells (year and week mode) |
|
|
| Accent (10%) | Green scale: `#9be9a8`, `#40c463`, `#30a14e`, `#216e39` | Heatmap cells with data (both year and week modes) |
|
|
| Border | `var(--tblr-border-color)` | Tooltip border |
|
|
| Text primary | `var(--tblr-body-color)` | Weekday labels, stat values, control text |
|
|
| Text secondary | `var(--tblr-secondary)` fallback `#6c757d` | Stats row labels |
|
|
| Active control | Tabler `nav-segmented .active` default styling | Active mode/metric button background highlight |
|
|
|
|
Accent reserved for: heatmap cells with tracked data only (cells colored by `buildColorScale()` quantize scale across 4 green buckets). Zero-data weekday cells use the secondary empty fill, not the lowest green bucket.
|
|
|
|
---
|
|
|
|
## Component Inventory
|
|
|
|
### 1. Mode Switcher (Segmented Control)
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Element | `<nav>` with Tabler `nav nav-segmented` classes |
|
|
| Placement | Inside `#heatmap-controls` div in card header, right-aligned via `margin-left: auto` |
|
|
| Items | "Year" (key: `year`), "Week" (key: `week`) |
|
|
| Active state | `.active` class on the selected `nav-link` button |
|
|
| ARIA | `role="tablist"` on nav, `role="tab"` on each button, `aria-selected` on active |
|
|
| Behavior | Click sets `state.mode`, calls `doRender()`. Does not re-fetch data. |
|
|
| Default | "Year" active on load (matches `createInitialState`) |
|
|
|
|
### 2. Metric Toggle (Compact Segmented Control)
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Element | `<nav>` with Tabler `nav nav-segmented nav-sm` classes |
|
|
| Placement | Adjacent to mode switcher inside `#heatmap-controls`, 8px gap |
|
|
| Items | "Hours" (key: `hours`), "Count" (key: `count`) |
|
|
| Active state | `.active` class on selected button |
|
|
| ARIA | Same pattern as mode switcher |
|
|
| Behavior | Click sets `state.metric`, calls `doRender()`. Does not re-fetch data. |
|
|
| Default | "Hours" active on load |
|
|
|
|
### 3. Controls Container (Twig Template Addition)
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Element | `<div id="heatmap-controls">` |
|
|
| Placement | Inside `{% block box_title %}`, wrapped in a flex container with the title |
|
|
| Layout | `display: flex; align-items: center; gap: 8px; margin-left: auto` |
|
|
| Parent flex | Title wrapper: `display: flex; align-items: center; gap: 12px; flex-wrap: wrap` |
|
|
|
|
### 4. Week-Mode Renderer (SVG)
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Cell width | 60px |
|
|
| Cell height | 40px |
|
|
| Cell gap | 4px |
|
|
| Cell border radius | `rx="2" ry="2"` (matches `.heatmap-cell`) |
|
|
| Label width | 50px (left of cells, for 3-letter weekday abbreviation) |
|
|
| Total SVG width | `50 + 7 * (60 + 4) = 498px` |
|
|
| Total SVG height | 40px (single row, no extra margin needed) |
|
|
| Cell class | `heatmap-cell` (reuses existing hover/cursor styles) |
|
|
| Empty cell class | `heatmap-empty` (reuses existing empty fill) |
|
|
| Label class | `heatmap-label` (reuses existing 10px label style) |
|
|
| Label position | Vertically centered beside each cell, 50px column left of cells |
|
|
| Day order | Follows `state.weekStart` -- Monday-first or Sunday-first |
|
|
| Click behavior | None for this phase (weekday aggregation has no single-date target) |
|
|
|
|
### 5. Week-Mode Tooltip
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Trigger | Hover on week-mode cell |
|
|
| Content format (hours metric) | `"Monday: 42.5h (23 entries)"` |
|
|
| Content format (count metric) | `"Monday: 23 entries (42.5h)"` |
|
|
| Styling | Existing `.heatmap-tooltip` class (no changes) |
|
|
| Position | Above cursor, shared `showTooltip`/`hideTooltip` utilities |
|
|
|
|
### 6. Stats Row Behavior
|
|
|
|
| Mode | Stats Row |
|
|
|------|-----------|
|
|
| Year | Shown as-is (streak, total, avg, busiest day) |
|
|
| Week | Hidden -- streak and busiest-date are year-specific concepts |
|
|
|
|
Implementation: `renderStats()` call in `doRender()` is conditional on `state.mode === 'year'`. When mode is `'week'`, remove any existing `.heatmap-stats` element.
|
|
|
|
---
|
|
|
|
## Interaction Contracts
|
|
|
|
### Mode Switch: Year to Week
|
|
|
|
1. User clicks "Week" in mode switcher
|
|
2. "Week" button gets `.active`, "Year" button loses `.active`
|
|
3. `state.mode` set to `'week'`
|
|
4. `doRender()` dispatches to `WeekModeRenderer`
|
|
5. SVG area clears and renders 7 horizontal weekday cells
|
|
6. Stats row is removed
|
|
7. Scroll position resets (no horizontal scroll needed for 7 cells)
|
|
8. Filter selection preserved (`state.filters` unchanged)
|
|
|
|
### Mode Switch: Week to Year
|
|
|
|
1. User clicks "Year" in mode switcher
|
|
2. Active states swap
|
|
3. `state.mode` set to `'year'`
|
|
4. `doRender()` dispatches to `YearModeRenderer`
|
|
5. SVG area clears and renders full year calendar grid
|
|
6. Stats row reappears
|
|
7. SVG area scrolls to right end (existing behavior)
|
|
|
|
### Metric Toggle
|
|
|
|
1. User clicks "Count" (or "Hours")
|
|
2. Active state swaps on metric toggle buttons
|
|
3. `state.metric` updated
|
|
4. `doRender()` re-renders current mode with new metric
|
|
5. Color scale recalculated via `buildColorScale(days, state.metric)`
|
|
6. No data re-fetch
|
|
|
|
### No Transition Animation
|
|
|
|
Mode switches are instant (clear + re-render). No CSS transitions or d3 transitions between modes. This keeps implementation simple and avoids visual glitches with different SVG structures.
|
|
|
|
---
|
|
|
|
## Copywriting Contract
|
|
|
|
| Element | Copy |
|
|
|---------|------|
|
|
| Mode switcher labels | "Year", "Week" |
|
|
| Metric toggle labels | "Hours", "Count" |
|
|
| Week tooltip (hours mode) | "{Weekday}: {N}h ({N} entries)" -- e.g. "Monday: 42.5h (23 entries)" |
|
|
| Week tooltip (count mode) | "{Weekday}: {N} entries ({N}h)" -- e.g. "Monday: 23 entries (42.5h)" |
|
|
| Week empty cell tooltip | "{Weekday}: No tracked time" |
|
|
| Data load error | "Failed to load heatmap data" (existing, unchanged) |
|
|
| Empty state (no data, week mode) | 7 cells all rendered with `.heatmap-empty` fill, no special empty-state message needed (the empty cells are self-explanatory) |
|
|
| Project filter empty (week mode) | "No tracking data for this project" (existing `emptyMessage`, rendered as text in SVG area) |
|
|
|
|
No destructive actions in this phase.
|
|
|
|
---
|
|
|
|
## Registry Safety
|
|
|
|
| Registry | Blocks Used | Safety Gate |
|
|
|----------|-------------|-------------|
|
|
| Not applicable | No shadcn, no registries | N/A |
|
|
|
|
This is a Symfony/Kimai plugin using Tabler CSS bundled with the host application. No component registry applies.
|
|
|
|
---
|
|
|
|
## CSS Additions
|
|
|
|
Only one new CSS rule needed:
|
|
|
|
```css
|
|
/* Week-mode cells: no pointer cursor since they are not clickable */
|
|
.heatmap-week-cell {
|
|
cursor: default;
|
|
}
|
|
```
|
|
|
|
All other styling reuses existing classes: `.heatmap-cell`, `.heatmap-empty`, `.heatmap-label`, `.heatmap-tooltip`. The Tabler `nav-segmented` classes handle control styling with zero custom CSS.
|
|
|
|
---
|
|
|
|
## Twig Template Change
|
|
|
|
The `{% block box_title %}` in `heatmap.html.twig` changes from:
|
|
|
|
```twig
|
|
{% block box_title %}
|
|
{{ title }}
|
|
{% endblock %}
|
|
```
|
|
|
|
To:
|
|
|
|
```twig
|
|
{% block box_title %}
|
|
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
|
|
{{ title }}
|
|
<div id="heatmap-controls" style="display: flex; gap: 8px; margin-left: auto;"></div>
|
|
</div>
|
|
{% endblock %}
|
|
```
|
|
|
|
JS populates `#heatmap-controls` at init time. The inline styles are intentional -- this is a minimal layout wrapper, not a reusable component.
|
|
|
|
---
|
|
|
|
## 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
|