first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
# `<PianoKey>`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
A single piano key. Two visual kinds (`white` / `black`) and three
visual states (`idle` / `pressed` / `highlighted`). Consumers compose
many keys to build a full keyboard component — `<PianoKey>` itself
deliberately renders a single rect so it stays compositional, theming
flexible, and reusable in non-piano contexts (e.g. step sequencer cells
themed as keys). The component implements `BoundControlProps` and
emits `onChange(max)` on press, `onChange(min)` on release.
## Features
- Pointer and keyboard activation (Space, Enter).
- Pointer-leave release (no stuck keys when you glide off the key).
- Key-auto-repeat suppression via active-ref tracking.
- Explicit `state` prop overrides derived state so callers can
paint a key as `highlighted` (e.g. for chord-shape display) without
changing `value`.
- Default dimensions tuned per `kind` (white: 32×140, black: 20×90).
- `role="button"` with `aria-pressed` reflecting the current state.
- CSS-variable theming: `--cs-piano-white`, `--cs-piano-white-pressed`,
`--cs-piano-white-highlight`, `--cs-piano-white-stroke`,
`--cs-piano-black`, `--cs-piano-black-pressed`,
`--cs-piano-black-highlight`, `--cs-piano-black-stroke`.
## Usage: Basic
```tsx
import { useState } from "react";
import { PianoKey } from "@suaveplan/react-control-surface";
export function MiddleC() {
const [v, setV] = useState(0);
return (
<PianoKey
value={v}
onChange={setV}
min={0}
max={1}
note={60}
/>
);
}
```
## Usage: Advanced
Compose an octave with black keys overlaid on whites:
```tsx
import { useState } from "react";
import { PianoKey } from "@suaveplan/react-control-surface";
export function Octave() {
const [pressed, setPressed] = useState<Set<number>>(new Set());
const flip = (n: number, on: boolean) =>
setPressed((p) => {
const next = new Set(p);
if (on) next.add(n);
else next.delete(n);
return next;
});
const whites = [0, 2, 4, 5, 7, 9, 11];
const blacks = [{ off: 1, x: 22 }, { off: 3, x: 54 }, { off: 6, x: 118 }];
return (
<div style={{ position: "relative", height: 140, width: 224 }}>
{whites.map((o, i) => (
<div key={o} style={{ position: "absolute", left: i * 32 }}>
<PianoKey
value={pressed.has(60 + o) ? 1 : 0}
onChange={(v) => flip(60 + o, v > 0)}
min={0}
max={1}
note={60 + o}
/>
</div>
))}
{blacks.map(({ off, x }) => (
<div key={off} style={{ position: "absolute", left: x }}>
<PianoKey
kind="black"
value={pressed.has(60 + off) ? 1 : 0}
onChange={(v) => flip(60 + off, v > 0)}
min={0}
max={1}
note={60 + off}
/>
</div>
))}
</div>
);
}
```
## API Reference
| Prop | Type | Default |
| ----------- | ----------------------------- | -------------- |
| `value` | `number` | required |
| `onChange` | `(v: number) => void` | required |
| `min`/`max` | `number` | required |
| `disabled` | `boolean` | `false` |
| `kind` | `"white" \| "black"` | `"white"` |
| `state` | `"idle" \| "pressed" \| "highlighted"` | derived |
| `note` | `number` | — |
| `label` | `string` | — |
| `ariaLabel` | `string` | — |
| `widthPx` | `number` | per-kind |
| `heightPx` | `number` | per-kind |
| `className` | `string` | — |
## Implementation Notes
The active state tracks an internal ref rather than React state so the
press / release pair is event-loop-stable and immune to auto-repeat
storms from held keys. Pointer capture is set on `pointerdown` and
released on the matching `pointerup` / `pointercancel` / `pointerleave`,
which gives correct release semantics even when the pointer drifts off
the SVG (touch glissando). The explicit `state` prop fully overrides
the derived state so the component can be driven from an external
signal (incoming MIDI from another player, computer-aided
visualisation) without coupling `value` to the visual state.