docs(07): research phase domain
This commit is contained in:
parent
d47af3dbca
commit
79e15a1232
1 changed files with 395 additions and 0 deletions
395
.planning/phases/07-mode-switcher-week-mode/07-RESEARCH.md
Normal file
395
.planning/phases/07-mode-switcher-week-mode/07-RESEARCH.md
Normal file
|
|
@ -0,0 +1,395 @@
|
||||||
|
# Phase 7: Mode Switcher + Week Mode - Research
|
||||||
|
|
||||||
|
**Researched:** 2026-04-09
|
||||||
|
**Domain:** d3.js visualization modes, Tabler UI segmented controls, client-side data aggregation
|
||||||
|
**Confidence:** HIGH
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Phase 7 adds two UI controls (mode switcher and hours/count toggle) and one new renderer (WeekModeRenderer) to the existing strategy-pattern architecture from Phase 6. The infrastructure is solid -- `ModeRenderer` interface, renderer registry, `HeatmapState` with `mode` and `metric` fields, and shared utilities (color scale, tooltip, date-utils) are all in place. The new work is: (1) render two Tabler `nav-segmented` controls in the widget header, (2) wire their click handlers to update `state.mode` and `state.metric` then call `doRender()`, and (3) implement `WeekModeRenderer` that aggregates `DayEntry[]` by weekday and renders 7 colored cells.
|
||||||
|
|
||||||
|
Tabler provides a native `nav-segmented` component that matches the "segmented control" design from CONTEXT.md decisions. No custom CSS needed for the control itself -- just Tabler classes. The week-mode renderer is straightforward d3 work: group existing data by day-of-week (0-6), sum hours/count per weekday, render 7 rectangles with the shared `buildColorScale()`. The `getDayLabels()` and weekday-index calculation in `date-utils.ts` already handle start-of-week preference.
|
||||||
|
|
||||||
|
**Primary recommendation:** Build controls in JS (not Twig) since they interact with `HeatmapState`; keep the Twig template unchanged. Register `WeekModeRenderer` alongside `YearModeRenderer` at module level.
|
||||||
|
|
||||||
|
<user_constraints>
|
||||||
|
## User Constraints (from CONTEXT.md)
|
||||||
|
|
||||||
|
### Locked Decisions
|
||||||
|
- D-01: Segmented control in widget header row (Kimai Tabler card header area), next to title
|
||||||
|
- D-02: "Year" and "Week" as the two mode options (Day/Combined in Phase 9)
|
||||||
|
- D-03: Active mode highlighted using Kimai/Tabler CSS conventions
|
||||||
|
- D-04: Week-mode horizontal layout -- 7 cells, one per weekday, colored by aggregated metric
|
||||||
|
- D-05: Day labels alongside cells, respecting user's start-of-week from `state.weekStart`
|
||||||
|
- D-06: Client-side aggregation of existing DayEntry[] grouped by weekday (no backend changes)
|
||||||
|
- D-07: Tooltip on hover: weekday name + aggregated value
|
||||||
|
- D-08: Hours/Count toggle is a separate small segmented control
|
||||||
|
- D-09: Placed adjacent to mode switcher in header
|
||||||
|
- D-10: Toggles `state.metric` between 'hours' and 'count', triggers re-render without re-fetch
|
||||||
|
- D-11: Affects color scale in both year and week modes
|
||||||
|
|
||||||
|
### Claude's Discretion
|
||||||
|
- Exact sizing/spacing of week-mode cells
|
||||||
|
- Whether week-mode cells are clickable (and what click-through shows)
|
||||||
|
- Stats row behavior when in week mode
|
||||||
|
- Exact Tabler CSS classes for segmented controls
|
||||||
|
- Animation/transition when switching modes
|
||||||
|
|
||||||
|
### Deferred Ideas (OUT OF SCOPE)
|
||||||
|
None
|
||||||
|
</user_constraints>
|
||||||
|
|
||||||
|
<phase_requirements>
|
||||||
|
## Phase Requirements
|
||||||
|
|
||||||
|
| ID | Description | Research Support |
|
||||||
|
|----|-------------|------------------|
|
||||||
|
| VIZ-01 | Mode switcher UI allows toggling between year, week, day, and combined views | Tabler `nav-segmented` component; only Year/Week for this phase per D-02 |
|
||||||
|
| VIZ-02 | Week-mode renders day-of-week aggregation showing busiest weekdays | WeekModeRenderer with client-side DayEntry grouping by weekday |
|
||||||
|
| VIZ-05 | Hours vs entry-count toggle switches color scale metric across all modes | Separate `nav-segmented` control toggling `state.metric`; `buildColorScale()` already metric-aware |
|
||||||
|
| TEST-01 | Vitest tests for mode switcher, each renderer, and display toggle | Test patterns established in existing 9 test files; jsdom environment |
|
||||||
|
</phase_requirements>
|
||||||
|
|
||||||
|
## Standard Stack
|
||||||
|
|
||||||
|
### Core (already installed -- no new packages)
|
||||||
|
| Library | Version | Purpose | Why Standard |
|
||||||
|
|---------|---------|---------|--------------|
|
||||||
|
| d3-selection | ^3.0.0 | DOM manipulation for week cells | Already in use for year renderer |
|
||||||
|
| d3-scale | ^4.0.2 | Color scale for week cells | `buildColorScale()` already wraps this |
|
||||||
|
| d3-array | ^3.2.4 | `max()` for scale domain | Already in shared/color-scale.ts |
|
||||||
|
| vitest | ^4.1.3 | Test runner | Already configured with jsdom |
|
||||||
|
|
||||||
|
No new npm packages required. [VERIFIED: package.json in codebase]
|
||||||
|
|
||||||
|
### Tabler CSS (bundled with Kimai)
|
||||||
|
| Component | Classes | Purpose |
|
||||||
|
|-----------|---------|---------|
|
||||||
|
| Segmented control | `nav nav-segmented`, `nav-link`, `active` | Mode switcher and metric toggle |
|
||||||
|
| Small variant | `nav-sm` | Compact toggle for hours/count |
|
||||||
|
|
||||||
|
[CITED: docs.tabler.io/ui/components/segmented-control]
|
||||||
|
|
||||||
|
## Architecture Patterns
|
||||||
|
|
||||||
|
### Project Structure (new files only)
|
||||||
|
```
|
||||||
|
assets/
|
||||||
|
src/
|
||||||
|
renderers/
|
||||||
|
week.ts # NEW: WeekModeRenderer
|
||||||
|
ui/
|
||||||
|
controls.ts # NEW: mode switcher + metric toggle DOM builders
|
||||||
|
test/
|
||||||
|
week.test.ts # NEW: WeekModeRenderer tests
|
||||||
|
controls.test.ts # NEW: UI control interaction tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 1: WeekModeRenderer (Strategy Pattern Extension)
|
||||||
|
|
||||||
|
**What:** New renderer implementing `ModeRenderer` interface, registered via `registerRenderer()`.
|
||||||
|
|
||||||
|
**When to use:** Rendering the week-mode aggregation view.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```typescript
|
||||||
|
// assets/src/renderers/week.ts
|
||||||
|
import type { ModeRenderer, RenderContext } from './types';
|
||||||
|
import { registerRenderer } from './registry';
|
||||||
|
import { buildColorScale } from '../shared/color-scale';
|
||||||
|
import { createTooltip, showTooltip, hideTooltip } from '../shared/tooltip';
|
||||||
|
|
||||||
|
interface WeekdayAggregate {
|
||||||
|
dayIndex: number; // 0-6, relative to weekStart
|
||||||
|
label: string; // "Mon", "Tue", etc.
|
||||||
|
totalHours: number;
|
||||||
|
totalCount: number;
|
||||||
|
dayCount: number; // number of occurrences for averaging
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WeekModeRenderer implements ModeRenderer {
|
||||||
|
readonly mode = 'week';
|
||||||
|
private tooltip: HTMLDivElement | null = null;
|
||||||
|
|
||||||
|
render(ctx: RenderContext): void {
|
||||||
|
// 1. Aggregate DayEntry[] by weekday
|
||||||
|
// 2. Build 7-cell horizontal SVG
|
||||||
|
// 3. Color via buildColorScale() using ctx.state.metric
|
||||||
|
// 4. Attach tooltip handlers
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.tooltip?.remove();
|
||||||
|
this.tooltip = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module-level registration
|
||||||
|
registerRenderer(new WeekModeRenderer());
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: UI Controls (JS-driven, not Twig)
|
||||||
|
|
||||||
|
**What:** Build mode switcher and metric toggle as DOM elements in `heatmap.ts` init, inserted into the widget header area.
|
||||||
|
|
||||||
|
**Why JS not Twig:** Controls must interact with `HeatmapState` and call `doRender()`. Putting them in Twig would require a separate event-binding pass and Twig has no knowledge of JS state. Building them in JS during init keeps the coupling clean.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```typescript
|
||||||
|
// assets/src/ui/controls.ts
|
||||||
|
export interface ControlCallbacks {
|
||||||
|
onModeChange: (mode: string) => void;
|
||||||
|
onMetricChange: (metric: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createModeControl(
|
||||||
|
activeMode: string,
|
||||||
|
modes: Array<{ key: string; label: string }>,
|
||||||
|
onChange: (mode: string) => void,
|
||||||
|
): HTMLElement {
|
||||||
|
const nav = document.createElement('nav');
|
||||||
|
nav.className = 'nav nav-segmented';
|
||||||
|
nav.setAttribute('role', 'tablist');
|
||||||
|
|
||||||
|
for (const m of modes) {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'nav-link' + (m.key === activeMode ? ' active' : '');
|
||||||
|
btn.setAttribute('role', 'tab');
|
||||||
|
btn.textContent = m.label;
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
nav.querySelectorAll('.nav-link').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
onChange(m.key);
|
||||||
|
});
|
||||||
|
nav.appendChild(btn);
|
||||||
|
}
|
||||||
|
return nav;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMetricControl(
|
||||||
|
activeMetric: string,
|
||||||
|
onChange: (metric: string) => void,
|
||||||
|
): HTMLElement {
|
||||||
|
// Same pattern, nav-sm for compact size
|
||||||
|
const nav = document.createElement('nav');
|
||||||
|
nav.className = 'nav nav-segmented nav-sm';
|
||||||
|
// ... buttons for 'Hours' and 'Count'
|
||||||
|
return nav;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Weekday Aggregation (Client-Side)
|
||||||
|
|
||||||
|
**What:** Group `DayEntry[]` by day-of-week index, summing hours and counts.
|
||||||
|
|
||||||
|
**Key detail:** Must respect `state.weekStart`. The existing `date-utils.ts` calculates `dayOfWeek` as `(jsDay + 6) % 7` for Monday start and `jsDay` for Sunday start. Reuse the same logic.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function aggregateByWeekday(
|
||||||
|
days: DayEntry[],
|
||||||
|
weekStart: string,
|
||||||
|
): WeekdayAggregate[] {
|
||||||
|
const buckets = new Array(7).fill(null).map((_, i) => ({
|
||||||
|
dayIndex: i,
|
||||||
|
label: '', // filled from getDayLabels or full-name variant
|
||||||
|
totalHours: 0,
|
||||||
|
totalCount: 0,
|
||||||
|
dayCount: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const d of days) {
|
||||||
|
const jsDay = new Date(d.date + 'T00:00:00').getDay();
|
||||||
|
const idx = weekStart === 'sunday' ? jsDay : (jsDay + 6) % 7;
|
||||||
|
buckets[idx].totalHours += d.hours;
|
||||||
|
buckets[idx].totalCount += d.count;
|
||||||
|
buckets[idx].dayCount += 1;
|
||||||
|
}
|
||||||
|
return buckets;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: Control Placement in Widget Header
|
||||||
|
|
||||||
|
**What:** Insert controls into the card header alongside the existing title.
|
||||||
|
|
||||||
|
**Approach:** The Twig template uses `@theme/embeds/card.html.twig`. The `{% block box_title %}` currently holds just `{{ title }}`. Two options:
|
||||||
|
|
||||||
|
1. **Modify Twig** to add a placeholder `<div id="heatmap-controls">` in `box_title` block -- then JS populates it.
|
||||||
|
2. **Pure JS** -- find the `.card-header` parent of `#heatmap-container` and inject controls.
|
||||||
|
|
||||||
|
**Recommendation:** Option 1 -- add a controls placeholder in Twig. This is cleaner because it ensures the layout structure is correct even before JS loads, and avoids fragile DOM traversal.
|
||||||
|
|
||||||
|
```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 %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in `heatmap.ts`, build controls and append to `#heatmap-controls`.
|
||||||
|
|
||||||
|
### Anti-Patterns to Avoid
|
||||||
|
- **Merging mode switcher and metric toggle into one control:** CONTEXT.md D-08 explicitly says separate controls.
|
||||||
|
- **Re-fetching data on metric toggle:** D-10 says re-render without re-fetch. The data already has both `hours` and `count` fields.
|
||||||
|
- **Building week renderer from scratch without shared utilities:** `buildColorScale()`, tooltip helpers, and `getDayLabels()` are all reusable.
|
||||||
|
|
||||||
|
## Don't Hand-Roll
|
||||||
|
|
||||||
|
| Problem | Don't Build | Use Instead | Why |
|
||||||
|
|---------|-------------|-------------|-----|
|
||||||
|
| Segmented control UI | Custom toggle buttons with manual styling | Tabler `nav-segmented` classes | Native Kimai theme integration, accessible markup |
|
||||||
|
| Color scale | New scale for week cells | `buildColorScale()` from shared/color-scale.ts | Already metric-aware, consistent across modes |
|
||||||
|
| Tooltips | New tooltip logic | `createTooltip/showTooltip/hideTooltip` from shared/tooltip.ts | Consistent behavior, already tested |
|
||||||
|
| Weekday ordering | Hard-coded day arrays | Reuse `(jsDay + 6) % 7` pattern from date-utils.ts | Already handles Monday/Sunday start |
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### Pitfall 1: Weekday Index Mismatch Between Aggregation and Labels
|
||||||
|
**What goes wrong:** Aggregation uses one weekday ordering, labels use another, so Tuesday's data shows on Monday's cell.
|
||||||
|
**Why it happens:** JS `Date.getDay()` returns 0=Sunday. The existing code remaps for Monday-start. If aggregation and label generation use different mappings, they desync.
|
||||||
|
**How to avoid:** Use the exact same index formula as `generateCells()` in date-utils.ts: `weekStart === 'sunday' ? jsDay : (jsDay + 6) % 7`. Map labels using the same index.
|
||||||
|
**Warning signs:** Tests with known weekday data showing wrong labels.
|
||||||
|
|
||||||
|
### Pitfall 2: Empty Aggregate Buckets Confuse Color Scale
|
||||||
|
**What goes wrong:** Weekdays with zero entries get passed to `buildColorScale()` which sets domain `[0, maxVal]`, and zero values map to the lowest color bucket instead of showing as empty.
|
||||||
|
**Why it happens:** `buildColorScale` maps any value in `[0, maxVal]` to a color. Zero is technically in-domain.
|
||||||
|
**How to avoid:** Either (a) filter out zero-value weekdays before passing to color scale and render them as empty cells, or (b) treat weekdays with `dayCount === 0` as empty (no fill), similar to how year-mode handles null entries.
|
||||||
|
**Warning signs:** All 7 cells colored even when user tracked on only 3 weekdays.
|
||||||
|
|
||||||
|
### Pitfall 3: Controls Not Reflecting State After Re-render
|
||||||
|
**What goes wrong:** Mode or metric changes correctly, but if `doRender()` rebuilds the entire container, controls get destroyed.
|
||||||
|
**Why it happens:** `heatmap.ts`'s `doRender()` calls `renderer.render(ctx)` which does `ctx.container.innerHTML = ''` on the SVG area. If controls are inside the SVG area, they vanish.
|
||||||
|
**How to avoid:** Controls live outside the SVG area container (in the card header via `#heatmap-controls`). The renderer only clears `svgArea`, not the card header.
|
||||||
|
**Warning signs:** Controls disappear after first interaction.
|
||||||
|
|
||||||
|
### Pitfall 4: Stats Row Showing Year-Specific Data in Week Mode
|
||||||
|
**What goes wrong:** Streak counter and "busiest day" stats show in week mode where they don't make contextual sense.
|
||||||
|
**Why it happens:** `renderStats()` is called unconditionally after every `doRender()`.
|
||||||
|
**How to avoid:** Either skip `renderStats()` when mode is 'week', or show week-appropriate stats (e.g., busiest weekday, average per weekday). Simplest: hide stats in week mode for now.
|
||||||
|
**Warning signs:** "Busiest: Mon, Jan 13" showing below a weekday aggregation view.
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
### Weekday Aggregation with Full Labels
|
||||||
|
```typescript
|
||||||
|
// Full weekday names for tooltips (D-07 wants "Monday: 42.5h")
|
||||||
|
const WEEKDAY_NAMES_MONDAY = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||||
|
const WEEKDAY_NAMES_SUNDAY = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||||
|
|
||||||
|
function getWeekdayNames(weekStart: string): string[] {
|
||||||
|
return weekStart === 'sunday' ? WEEKDAY_NAMES_SUNDAY : WEEKDAY_NAMES_MONDAY;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Week-Mode SVG Layout (7 horizontal cells)
|
||||||
|
```typescript
|
||||||
|
// Larger cells since only 7 vs 365
|
||||||
|
const cellWidth = 60;
|
||||||
|
const cellHeight = 40;
|
||||||
|
const cellGap = 4;
|
||||||
|
const labelWidth = 50; // space for "Mon", "Tue" labels
|
||||||
|
const svgWidth = labelWidth + 7 * (cellWidth + cellGap);
|
||||||
|
const svgHeight = cellHeight + 20; // room for value text below
|
||||||
|
|
||||||
|
// Layout: label | rect | label | rect | ...
|
||||||
|
// Or: 7 rects in a row with labels below/beside
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wiring Controls to State in heatmap.ts
|
||||||
|
```typescript
|
||||||
|
// In init(), after building wrapper:
|
||||||
|
const controlsContainer = document.getElementById('heatmap-controls');
|
||||||
|
if (controlsContainer) {
|
||||||
|
const modeControl = createModeControl(state.mode, [
|
||||||
|
{ key: 'year', label: 'Year' },
|
||||||
|
{ key: 'week', label: 'Week' },
|
||||||
|
], (mode) => {
|
||||||
|
state.mode = mode as HeatmapMode;
|
||||||
|
doRender();
|
||||||
|
});
|
||||||
|
|
||||||
|
const metricControl = createMetricControl(state.metric, (metric) => {
|
||||||
|
state.metric = metric as DisplayMetric;
|
||||||
|
doRender();
|
||||||
|
});
|
||||||
|
|
||||||
|
controlsContainer.appendChild(modeControl);
|
||||||
|
controlsContainer.appendChild(metricControl);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation Architecture
|
||||||
|
|
||||||
|
### Test Framework
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Framework | Vitest 4.1.3 + jsdom |
|
||||||
|
| Config file | `vitest.config.ts` |
|
||||||
|
| Quick run command | `npm test` |
|
||||||
|
| Full suite command | `npm test` |
|
||||||
|
|
||||||
|
### Phase Requirements to Test Map
|
||||||
|
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
||||||
|
|--------|----------|-----------|-------------------|-------------|
|
||||||
|
| VIZ-01 | Mode switcher renders Year/Week buttons, click changes active state | unit | `npx vitest run assets/test/controls.test.ts` | Wave 0 |
|
||||||
|
| VIZ-02 | Week renderer aggregates by weekday, renders 7 cells with correct colors | unit | `npx vitest run assets/test/week.test.ts` | Wave 0 |
|
||||||
|
| VIZ-05 | Metric toggle switches state.metric, re-renders with different color values | unit | `npx vitest run assets/test/controls.test.ts` | Wave 0 |
|
||||||
|
| TEST-01 | All mode/renderer/toggle tests pass | suite | `npm test` | Wave 0 |
|
||||||
|
|
||||||
|
### Sampling Rate
|
||||||
|
- **Per task commit:** `npm test`
|
||||||
|
- **Per wave merge:** `npm test` (single suite)
|
||||||
|
- **Phase gate:** Full suite green before `/gsd-verify-work`
|
||||||
|
|
||||||
|
### Wave 0 Gaps
|
||||||
|
- [ ] `assets/test/week.test.ts` -- WeekModeRenderer rendering and aggregation tests
|
||||||
|
- [ ] `assets/test/controls.test.ts` -- mode switcher and metric toggle interaction tests
|
||||||
|
|
||||||
|
## Security Domain
|
||||||
|
|
||||||
|
No security-relevant ASVS categories apply to this phase. It is purely client-side UI rendering with no authentication, input handling from users, cryptography, or access control changes. Data comes from the existing authenticated API endpoint.
|
||||||
|
|
||||||
|
## Assumptions Log
|
||||||
|
|
||||||
|
| # | Claim | Section | Risk if Wrong |
|
||||||
|
|---|-------|---------|---------------|
|
||||||
|
| A1 | Tabler `nav-segmented` classes are available in Kimai's bundled Tabler version | Architecture Patterns | Controls would need custom CSS; verify in running Kimai instance |
|
||||||
|
| A2 | The `card-header` area in Kimai's card embed template has enough space for controls next to the title | Architecture Patterns | May need to adjust Twig layout or use a different placement |
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. **Stats row in week mode**
|
||||||
|
- What we know: Year-mode shows streak, total hours, avg hours, busiest day
|
||||||
|
- What's unclear: Whether these make sense in week mode (streak is year-concept)
|
||||||
|
- Recommendation: Hide stats in week mode initially; can add week-specific stats later if wanted
|
||||||
|
|
||||||
|
2. **Week-mode cell click behavior**
|
||||||
|
- What we know: Year-mode clicks navigate to timesheet for that date
|
||||||
|
- What's unclear: What clicking "Monday" in week-mode should do (filter timesheet by weekday? no-op?)
|
||||||
|
- Recommendation: No click handler for week cells initially -- the aggregation isn't tied to a single date
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
### Primary (HIGH confidence)
|
||||||
|
- Codebase: `assets/src/renderers/types.ts`, `registry.ts`, `year.ts` -- ModeRenderer contract and reference implementation
|
||||||
|
- Codebase: `assets/src/state.ts`, `assets/src/types.ts` -- HeatmapState with mode/metric fields already defined
|
||||||
|
- Codebase: `assets/src/shared/color-scale.ts` -- buildColorScale already metric-aware
|
||||||
|
- Codebase: `assets/src/shared/date-utils.ts` -- weekday index calculation with start-of-week support
|
||||||
|
|
||||||
|
### Secondary (MEDIUM confidence)
|
||||||
|
- [Tabler segmented control docs](https://docs.tabler.io/ui/components/segmented-control) -- `nav nav-segmented` markup pattern [CITED: docs.tabler.io/ui/components/segmented-control]
|
||||||
|
|
||||||
|
### Tertiary (LOW confidence)
|
||||||
|
- None
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
**Confidence breakdown:**
|
||||||
|
- Standard stack: HIGH -- no new packages, all already installed and tested
|
||||||
|
- Architecture: HIGH -- extends well-established strategy pattern from Phase 6
|
||||||
|
- Pitfalls: HIGH -- based on direct codebase analysis of existing patterns
|
||||||
|
|
||||||
|
**Research date:** 2026-04-09
|
||||||
|
**Valid until:** 2026-05-09 (stable -- no external dependency changes expected)
|
||||||
Loading…
Add table
Reference in a new issue