keyboard container: public surface rebuild (Phase 0)
Created by: jedmund
Rebuilds `KeyboardContainer`'s public surface before the remaining four `SynthKeyboardView` callers migrate onto it. Follows plan-v3 Phase 0a+0b.
Summary
- Overlays become declarative. `config.overlays: [KeyboardOverlayItem]` replaces the trailing-closure overlay. Each item declares its slot (`.above(gap:)`), measured height, and a content builder that takes the measured keyboard width. The container stacks items above the keyboard with cumulative offsets so callers stop computing `.offset(y:)` themselves.
- Chrome slot. `config.chrome: KeyboardChrome = .default` reserves a hook for future chrome variations without churning callers later.
- Built-in scroll safety. `config.isScrollSafe` (default: true for docked, false for contained) wraps the container in the existing `ScrollDisabler` with an internal `activeKeyCount` counter (clamped with `max(0, ...)` and reset on `onDisappear`).
- `KeyboardEvent` input model. The primary init takes `onEvent: (KeyboardEvent) -> Void`. `.down` carries the normalized 0..1 touch point Tonic already provides; `.move` is intentionally absent. A pitch-only sugar init (`onKeyDown`/`onKeyUp`) preserves existing call-site shape.
- Migrations. The four already-migrated callers (PlaygroundScreen, ScaleExplorer, ModeExplorer, ScaleBuilder) are moved onto the structured overlay API.
`SynthKeyboardView`'s `onKeyDown` now takes `(Pitch, CGPoint)` so the container can forward Tonic's point instead of discarding it. The four remaining direct callers ignore the new arg for now and migrate onto `KeyboardContainer` in later phases.
Test plan
-
Playground (contained + docked): step arcs and degree labels render at correct positions; no offsets leaked to caller. -
ScaleExplorer: degree labels sit just above the docked keyboard. -
ModeExplorer: step arcs render correctly across mode changes. -
ScaleBuilder: step arcs accumulate on correct attempts; audio + feedback unaffected. -
Verify docked scroll-safety doesn't break anything (contained default remains off; nothing new opted in yet).