Create the QWERTZ keyboard component with finger-colored keys and the level definitions (stages 1-6). The keyboard is the primary visual feedback during typing exercises.
Purpose: The keyboard component is needed by the typing exercise (Plan 04) and the forest overview (Plan 05). Level definitions drive all gameplay.
Output: Renderable keyboard with finger colors, pulse/press animations, and complete level 1-6 data.
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
@.planning/PROJECT.md
@.planning/phases/01-grundger-st-tippmechanik/01-CONTEXT.md
@.planning/phases/01-grundger-st-tippmechanik/01-01-SUMMARY.md
@SPEC.md (sections 4.1, 8.1, 8.2, 8.3)
From src/types.ts:
```typescript
export type KeyboardLayout = 'de' | 'ch';
export interface Level {
level: number;
newKeys: string[];
allKeys: string[];
fingerMap: Record<string, string>;
}
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Level definitions with key sets and finger mappings</name>
<files>
src/game/levels.ts, src/game/levels.test.ts
</files>
<read_first>
src/types.ts
SPEC.md (section 4.1 for progression table, section 8.3 for finger assignments)
</read_first>
<behavior>
- Test: levels array has exactly 6 entries
- Test: Level 1 newKeys = ['f', 'j', ' '] (F, J, Space)
- Test: Level 2 newKeys = ['d', 'k']
- Test: Level 3 newKeys = ['s', 'l']
- Test: Level 4 newKeys = ['a', 'ö']
- Test: Level 5 newKeys = ['g', 'h']
- Test: Level 6 newKeys = ['e', 'i']
- Test: Level 3 allKeys includes all keys from levels 1-3
- Test: getLevelByNumber(1) returns level 1 object
- Test: getKeysUpToLevel(3) returns cumulative keys for levels 1-3
- Test: Each key in allKeys has a fingerMap entry
</behavior>
<action>
Create src/game/levels.ts with level definitions per spec section 4.1 and finger assignments per section 8.3:
```typescript
import type { Level } from '../types';
// Finger color CSS variable mapping per spec section 8.3
// Left hand: pinky=Q/A/Y/1, ring=W/S/X/2, mid=E/D/C/3, index=R/F/V/T/G/B/4/5
// Right hand: index=U/J/M/Z/H/N/6/7, mid=I/K/,/8, ring=O/L/./9, pinky=P/Ö/Ä/Ü/0/ß
// Thumb: Space
const fingerMap: Record<string, string> = {
// Left pinky
'q': '--finger-l-pinky', 'a': '--finger-l-pinky', 'y': '--finger-l-pinky',
// Left ring
'w': '--finger-l-ring', 's': '--finger-l-ring', 'x': '--finger-l-ring',
// Left middle
'e': '--finger-l-mid', 'd': '--finger-l-mid', 'c': '--finger-l-mid',
// Left index
'r': '--finger-l-index', 'f': '--finger-l-index', 'v': '--finger-l-index',
't': '--finger-l-index', 'g': '--finger-l-index', 'b': '--finger-l-index',
// Right index
'u': '--finger-r-index', 'j': '--finger-r-index', 'm': '--finger-r-index',
'z': '--finger-r-index', 'h': '--finger-r-index', 'n': '--finger-r-index',
// Right middle
'i': '--finger-r-mid', 'k': '--finger-r-mid',
// Right ring
'o': '--finger-r-ring', 'l': '--finger-r-ring',
// Right pinky
'p': '--finger-r-pinky', 'ö': '--finger-r-pinky', 'ä': '--finger-r-pinky', 'ü': '--finger-r-pinky',
// Thumb
' ': '--finger-thumb',
};
export const levels: Level[] = [
{ level: 1, newKeys: ['f', 'j', ' '], allKeys: ['f', 'j', ' '], fingerMap },
{ level: 2, newKeys: ['d', 'k'], allKeys: ['f', 'j', ' ', 'd', 'k'], fingerMap },
{ level: 3, newKeys: ['s', 'l'], allKeys: ['f', 'j', ' ', 'd', 'k', 's', 'l'], fingerMap },
{ level: 4, newKeys: ['a', 'ö'], allKeys: ['f', 'j', ' ', 'd', 'k', 's', 'l', 'a', 'ö'], fingerMap },
{ level: 5, newKeys: ['g', 'h'], allKeys: ['f', 'j', ' ', 'd', 'k', 's', 'l', 'a', 'ö', 'g', 'h'], fingerMap },
{ level: 6, newKeys: ['e', 'i'], allKeys: ['f', 'j', ' ', 'd', 'k', 's', 'l', 'a', 'ö', 'g', 'h', 'e', 'i'], fingerMap },
];
export function getLevelByNumber(n: number): Level | undefined {
return levels.find(l => l.level === n);
}
export function getKeysUpToLevel(n: number): string[] {
const level = getLevelByNumber(n);
return level ? level.allKeys : [];
}
```
Create src/game/levels.test.ts testing all behaviors listed above.
</action>
<verify>
<automated>cd /home/dev/workspace/zauberwald && npx vitest run src/game/levels.test.ts</automated>
</verify>
<acceptance_criteria>
- src/game/levels.ts exports `levels` (array of 6), `getLevelByNumber`, `getKeysUpToLevel`
- levels[0].newKeys deep equals ['f', 'j', ' ']
- levels[5].newKeys deep equals ['e', 'i']
- levels[5].allKeys contains 13 keys (f,j,space,d,k,s,l,a,ö,g,h,e,i)
- fingerMap maps 'f' to '--finger-l-index' and 'j' to '--finger-r-index'
- fingerMap maps ' ' to '--finger-thumb'
- `npx vitest run src/game/levels.test.ts` exits 0
</acceptance_criteria>
<done>Levels 1-6 defined with correct key sets per spec progression table, finger mapping matches spec section 8.3, all tests pass</done>
</task>
<task type="auto">
<name>Task 2: QWERTZ keyboard renderer with finger colors and animations</name>
<files>
src/game/keyboard.ts, src/styles/main.css
</files>
<read_first>
src/game/levels.ts
src/types.ts
src/styles/main.css
SPEC.md (section 8.1 for keyboard layout, section 8.2 for DE/CH differences)
</read_first>
<action>
1. Create src/game/keyboard.ts:
```typescript
import type { KeyboardLayout } from '../types';
import { getLevelByNumber } from './levels';
// QWERTZ rows — each row is an array of key labels
// For DE and CH layouts (identical for letters, differ on special chars above level 14)
const KEYBOARD_ROWS_DE = [
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'ß'],
['q', 'w', 'e', 'r', 't', 'z', 'u', 'i', 'o', 'p', 'ü'],
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ö', 'ä'],
['y', 'x', 'c', 'v', 'b', 'n', 'm'],
[' '], // Spacebar
];
const KEYBOARD_ROWS_CH = [
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
['q', 'w', 'e', 'r', 't', 'z', 'u', 'i', 'o', 'p', 'ü'],
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ö', 'ä'],
['y', 'x', 'c', 'v', 'b', 'n', 'm'],
[' '],
];
// Finger color mapping (shared with levels.ts fingerMap)
const fingerColorMap: Record<string, string> = {
'q': '--finger-l-pinky', 'a': '--finger-l-pinky', 'y': '--finger-l-pinky', '1': '--finger-l-pinky',
'w': '--finger-l-ring', 's': '--finger-l-ring', 'x': '--finger-l-ring', '2': '--finger-l-ring',
'e': '--finger-l-mid', 'd': '--finger-l-mid', 'c': '--finger-l-mid', '3': '--finger-l-mid',
'r': '--finger-l-index', 'f': '--finger-l-index', 'v': '--finger-l-index', '4': '--finger-l-index',
't': '--finger-l-index', 'g': '--finger-l-index', 'b': '--finger-l-index', '5': '--finger-l-index',
'z': '--finger-r-index', 'h': '--finger-r-index', 'n': '--finger-r-index', '6': '--finger-r-index',
'u': '--finger-r-index', 'j': '--finger-r-index', 'm': '--finger-r-index', '7': '--finger-r-index',
'i': '--finger-r-mid', 'k': '--finger-r-mid', '8': '--finger-r-mid',
'o': '--finger-r-ring', 'l': '--finger-r-ring', '9': '--finger-r-ring',
'p': '--finger-r-pinky', 'ö': '--finger-r-pinky', 'ä': '--finger-r-pinky',
'ü': '--finger-r-pinky', '0': '--finger-r-pinky', 'ß': '--finger-r-pinky',
' ': '--finger-thumb',
};
let keyboardContainer: HTMLElement | null = null;
let keyElements: Map<string, HTMLElement> = new Map();
export function renderKeyboard(
container: HTMLElement,
layout: KeyboardLayout,
currentLevel: number
): void {
keyboardContainer = container;
keyElements.clear();
container.innerHTML = '';
container.className = 'keyboard';
const rows = layout === 'ch' ? KEYBOARD_ROWS_CH : KEYBOARD_ROWS_DE;
const level = getLevelByNumber(currentLevel);
const activeKeys = level ? level.allKeys : [];
rows.forEach((row, rowIndex) => {
const rowEl = document.createElement('div');
rowEl.className = `keyboard__row keyboard__row--${rowIndex}`;
row.forEach(key => {
const keyEl = document.createElement('div');
const isActive = activeKeys.includes(key);
const colorVar = fingerColorMap[key];
keyEl.className = 'keyboard__key';
if (key === ' ') keyEl.classList.add('keyboard__key--space');
if (isActive && colorVar) {
keyEl.style.backgroundColor = `var(${colorVar})`;
keyEl.classList.add('keyboard__key--active');
} else {
keyEl.classList.add('keyboard__key--inactive');
}
keyEl.textContent = key === ' ' ? '' : key.toUpperCase();
keyEl.dataset.key = key;
keyElements.set(key, keyEl);
rowEl.appendChild(keyEl);
});
container.appendChild(rowEl);
});
}
export function highlightKey(key: string): void {
// Remove previous highlights
keyElements.forEach(el => el.classList.remove('keyboard__key--highlight'));
const el = keyElements.get(key.toLowerCase());
if (el) el.classList.add('keyboard__key--highlight');
}
export function pressKey(key: string): void {
const el = keyElements.get(key.toLowerCase());
if (el) {
el.classList.add('keyboard__key--pressed');
setTimeout(() => el.classList.remove('keyboard__key--pressed'), 150);
}
}
export function updateKeyboardForLevel(currentLevel: number): void {
const level = getLevelByNumber(currentLevel);
const activeKeys = level ? level.allKeys : [];
keyElements.forEach((el, key) => {
const colorVar = fingerColorMap[key];
if (activeKeys.includes(key) && colorVar) {
el.style.backgroundColor = `var(${colorVar})`;
el.classList.remove('keyboard__key--inactive');
el.classList.add('keyboard__key--active');
} else {
el.style.backgroundColor = '';
el.classList.remove('keyboard__key--active');
el.classList.add('keyboard__key--inactive');
}
});
}
```
2. Add keyboard CSS to src/styles/main.css:
```css
/* Keyboard */
.keyboard {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 1rem;
background: rgba(255, 255, 255, 0.6);
border-radius: 12px;
user-select: none;
}
.keyboard__row {
display: flex;
gap: 4px;
}
.keyboard__key {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 8px;
font-family: 'Quicksand', sans-serif;
font-weight: 700;
font-size: 1rem;
color: var(--text-dark);
background: #e0e0e0;
transition: transform 0.1s ease, background-color 0.2s ease, box-shadow 0.2s ease;
cursor: default;
}
.keyboard__key--space {
width: 280px;
}
.keyboard__key--inactive {
background: #e0e0e0;
color: var(--text-light);
opacity: 0.5;
}
.keyboard__key--active {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Pulse animation for the target key (KYBD-03) */
.keyboard__key--highlight {
animation: key-pulse 1.2s ease-in-out infinite;
box-shadow: 0 0 12px rgba(232, 184, 75, 0.6);
}
@keyframes key-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.08); }
}
/* Press animation (KYBD-03) */
.keyboard__key--pressed {
transform: scale(0.9) !important;
animation: none;
}
```
</action>
<verify>
<automated>cd /home/dev/workspace/zauberwald && npx tsc --noEmit && echo "PASS"</automated>
</verify>
<acceptance_criteria>
- src/game/keyboard.ts exports `renderKeyboard`, `highlightKey`, `pressKey`, `updateKeyboardForLevel`
- src/game/keyboard.ts contains KEYBOARD_ROWS_DE with 5 rows (numbers, top, home, bottom, space)
- src/game/keyboard.ts contains KEYBOARD_ROWS_CH with 5 rows
- src/game/keyboard.ts contains fingerColorMap with entries for 'f' -> '--finger-l-index', 'j' -> '--finger-r-index', ' ' -> '--finger-thumb'
- src/styles/main.css contains `.keyboard__key--highlight` with `animation: key-pulse`
- src/styles/main.css contains `@keyframes key-pulse`
- src/styles/main.css contains `.keyboard__key--pressed` with `transform: scale(0.9)`
- src/styles/main.css contains `.keyboard__key--inactive` with `opacity: 0.5`
- `npx tsc --noEmit` exits 0
</acceptance_criteria>
<done>Full QWERTZ keyboard renders with finger colors for active keys, grayed inactive keys, pulse animation on target key, press animation on keydown. Both DE and CH layouts supported.</done>
</task>
</tasks>
<verification>
- `npx vitest run src/game/levels.test.ts` passes
- `npx tsc --noEmit` passes
- Keyboard renders correctly when manually instantiated (verified in Plan 05 integration)
- Level data is correct per spec progression table
</verification>
<success_criteria>
- Levels 1-6 defined with correct key progressions matching spec section 4.1
- Keyboard renders full QWERTZ layout with finger colors from spec section 8.3
- Pulse and press animations work via CSS classes
- Both DE and CH layout data exist
</success_criteria>
<output>
After completion, create `.planning/phases/01-grundger-st-tippmechanik/01-03-SUMMARY.md`
</output>