first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user