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,104 @@
# `<XYPad>` — 2-axis touch surface
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`<XYPad>` is an accessible 2-axis touch surface that emits normalised
`{ x, y }` coordinates as the user drags a pointer or steps with the
arrow keys. The Y axis follows musical-software convention — the top
of the pad is `y = 1` and the bottom is `y = 0`. Both axes are
normalised to `[0, 1]` regardless of the pad's pixel size, so the
caller can map them onto any parameter range with a simple scale.
Common applications include morphing two modulation sources, mapping
filter cutoff against resonance, controlling pan + send, or any pair
of correlated parameters that benefit from a single gestural surface.
## Features
- Pointer-drag emission via Pointer Events with capture for off-pad drags
- Keyboard navigation — Arrows step by `step`, PageUp/PageDown by `step * 4`,
Home/End jump the X axis to the extremes
- Full Tab focusability with `role="application"`
- Clamping to `[0, 1]` on every emission, including out-of-bounds drags
- `disabled` removes the element from the Tab order and silences events
- CSS-variable themed background and cursor
- `data-x` / `data-y` deterministic attributes for E2E inspection
## Usage: Basic
```tsx
import { useState } from "react";
import { XYPad } from "@suaveplan/react-control-surface";
function Pad() {
const [v, setV] = useState({ x: 0.5, y: 0.5 });
return <XYPad value={v} onChange={setV} label="Morph" />;
}
```
## Usage: Advanced
```tsx
import { useState } from "react";
import { XYPad } from "@suaveplan/react-control-surface";
function FilterPad({
onCutoff,
onResonance,
}: {
onCutoff: (hz: number) => void;
onResonance: (q: number) => void;
}) {
const [v, setV] = useState({ x: 0.5, y: 0.5 });
return (
<XYPad
value={v}
onChange={(next) => {
setV(next);
onCutoff(40 + next.x * 19960);
onResonance(0.5 + next.y * 9.5);
}}
step={0.01}
width={320}
height={240}
label="Filter morph"
/>
);
}
```
## API Reference
| Prop | Type | Default | Description |
| ----------- | ---------------------------------------- | ------- | ------------------------------------------ |
| `value` | `{ x: number; y: number }` | req | Current normalised value, both in `[0, 1]`.|
| `onChange` | `(next: XYValue) => void` | req | Called for every drag and keyboard step. |
| `step` | `number` | `0.05` | Step size for arrow keys. |
| `width` | `number` | `200` | Render width in CSS pixels. |
| `height` | `number` | `200` | Render height in CSS pixels. |
| `disabled` | `boolean` | `false` | When `true`, ignores all input. |
| `label` | `string` | — | Visible + accessible label. |
| `ariaLabel` | `string` | derived | Override the accessible label. |
| `className` | `string` | — | Appended to `rcs-xy-pad`. |
The exported `pointerToValue(rect, clientX, clientY)` helper performs
the pure rect-to-`[0, 1]` conversion (including Y-inversion and
clamping) and is exported for re-use in custom pads.
## Implementation Notes
Pointer events are used uniformly across mouse + touch + pen. On
`pointerdown` the pad calls `setPointerCapture` so subsequent drags
outside the bounding rect continue to fire `pointermove` events. The
`draggingRef` flag avoids spurious moves before the user has pressed.
Keyboard handling is exhaustive: every recognised key calls
`preventDefault` and emits a clamped value; unrecognised keys do
nothing (the event bubbles for the consumer to handle). The
`aria-valuetext` attribute is updated on every render with the
current `x`/`y` truncated to two decimals, providing screen-reader
support without a polling cycle.