feat(06-01): add type contracts, state module, and renderer registry

This commit is contained in:
Christopher Mühl 2026-04-09 09:32:47 +02:00
parent a0d9bbfcc3
commit fe24e8bdd7
No known key found for this signature in database
GPG key ID: 925AC7D69955293F
6 changed files with 123 additions and 0 deletions

View file

@ -0,0 +1,17 @@
import type { ModeRenderer } from './types';
const renderers = new Map<string, ModeRenderer>();
export function registerRenderer(renderer: ModeRenderer): void {
renderers.set(renderer.mode, renderer);
}
export function getRenderer(mode: string): ModeRenderer {
const r = renderers.get(mode);
if (!r) throw new Error(`Unknown heatmap mode: ${mode}`);
return r;
}
export function clearRegistry(): void {
renderers.clear();
}

View file

@ -0,0 +1,16 @@
import type { HeatmapData, HeatmapConfig } from '../types';
import type { HeatmapState } from '../state';
export interface RenderContext {
container: HTMLElement;
data: HeatmapData;
state: HeatmapState;
config: HeatmapConfig;
onCellClick?: (dateStr: string) => void;
}
export interface ModeRenderer {
readonly mode: string;
render(ctx: RenderContext): void;
destroy?(): void;
}

21
assets/src/state.ts Normal file
View file

@ -0,0 +1,21 @@
import type { HeatmapData, HeatmapMode, DisplayMetric, FilterState } from './types';
export type { HeatmapMode, DisplayMetric, FilterState };
export interface HeatmapState {
mode: HeatmapMode;
metric: DisplayMetric;
filters: FilterState;
weekStart: string;
data: HeatmapData | null;
}
export function createInitialState(weekStart: string): HeatmapState {
return {
mode: 'year',
metric: 'hours',
filters: { projectId: null, customerId: null, activityId: null },
weekStart,
data: null,
};
}

View file

@ -24,3 +24,12 @@ export interface ProjectOption {
id: number; id: number;
name: string; name: string;
} }
export type DisplayMetric = 'hours' | 'count';
export type HeatmapMode = 'year' | 'week' | 'day' | 'combined';
export interface FilterState {
projectId: number | null;
customerId: number | null;
activityId: number | null;
}

View file

@ -0,0 +1,34 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { registerRenderer, getRenderer, clearRegistry } from '../src/renderers/registry';
import type { ModeRenderer, RenderContext } from '../src/renderers/types';
function makeMockRenderer(mode: string): ModeRenderer {
return {
mode,
render(_ctx: RenderContext): void {},
};
}
describe('renderer registry', () => {
beforeEach(() => {
clearRegistry();
});
it('registers and retrieves a renderer by mode', () => {
const renderer = makeMockRenderer('year');
registerRenderer(renderer);
expect(getRenderer('year')).toBe(renderer);
});
it('throws for unknown mode', () => {
expect(() => getRenderer('nonexistent')).toThrowError('Unknown heatmap mode');
});
it('overwrites renderer for same mode', () => {
const first = makeMockRenderer('year');
const second = makeMockRenderer('year');
registerRenderer(first);
registerRenderer(second);
expect(getRenderer('year')).toBe(second);
});
});

26
assets/test/state.test.ts Normal file
View file

@ -0,0 +1,26 @@
import { describe, it, expect } from 'vitest';
import { createInitialState } from '../src/state';
describe('createInitialState', () => {
it('returns correct defaults for monday start', () => {
const state = createInitialState('monday');
expect(state).toEqual({
mode: 'year',
metric: 'hours',
filters: { projectId: null, customerId: null, activityId: null },
weekStart: 'monday',
data: null,
});
});
it('returns correct defaults for sunday start', () => {
const state = createInitialState('sunday');
expect(state).toEqual({
mode: 'year',
metric: 'hours',
filters: { projectId: null, customerId: null, activityId: null },
weekStart: 'sunday',
data: null,
});
});
});