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;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
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