feat(06-01): add type contracts, state module, and renderer registry
This commit is contained in:
parent
a0d9bbfcc3
commit
fe24e8bdd7
6 changed files with 123 additions and 0 deletions
17
assets/src/renderers/registry.ts
Normal file
17
assets/src/renderers/registry.ts
Normal 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();
|
||||
}
|
||||
16
assets/src/renderers/types.ts
Normal file
16
assets/src/renderers/types.ts
Normal 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
21
assets/src/state.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
|
|
@ -24,3 +24,12 @@ export interface ProjectOption {
|
|||
id: number;
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
34
assets/test/registry.test.ts
Normal file
34
assets/test/registry.test.ts
Normal 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
26
assets/test/state.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue