Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
# `<DigitalDisplay>`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Read-only 7-segment / LCD-style readout for the control surface. The
component is a typed information sink: it accepts a discriminated
`mode` (`numeric` | `text` | `scrolling`) and renders the appropriate
visualisation. Although `<DigitalDisplay>` implements the same
`BoundControlProps` contract as input controls (so it can drop into
the same schema slots), it never emits `onChange` — the display is
pure output.
## Features
- Three modes (`numeric` with precision + optional units, `text`,
`scrolling` with CSS-keyframe overflow scroll).
- Scrolling mode honours `prefers-reduced-motion` by disabling the
animation via a `@media` block injected alongside the keyframes.
- `role="status"` with `aria-live="polite"` so screen readers receive
numeric updates without taking focus.
- A hidden screen-reader-only span echoes the numeric / text content
outside the clipped visual area, so SR users get the full value
even when the visible LCD is too short.
- CSS-variable theming: `--cs-display-font`, `--cs-display-color`,
`--cs-display-bg`.
- NaN / Infinity render as `—` (em-dash).
## Usage: Basic
```tsx
import { DigitalDisplay } from "@suaveplan/react-control-surface";
export function TempoReadout({ bpm }: { bpm: number }) {
return (
<DigitalDisplay
mode={{ kind: "numeric", precision: 1, units: "BPM" }}
value={bpm}
min={20}
max={300}
onChange={() => undefined}
label="Tempo"
/>
);
}
```
## Usage: Advanced
A patch-name readout that scrolls when the patch name is longer than
the LCD window:
```tsx
import { DigitalDisplay } from "@suaveplan/react-control-surface";
export function PatchName({ name }: { name: string }) {
return (
<DigitalDisplay
mode={{
kind: "scrolling",
text: name,
windowChars: 8,
speedPxPerSec: 40,
}}
value={0}
min={0}
max={1}
onChange={() => undefined}
widthPx={160}
heightPx={28}
label="Patch Name"
/>
);
}
```
## API Reference
| Prop | Type | Default |
| ----------- | ----------------------------------------------------------------- | -------- |
| `mode` | `{kind: "numeric" \| "text" \| "scrolling", ...}` | required |
| `value` | `number` | required |
| `min`/`max` | `number` | required |
| `onChange` | `(v: number) => void` | required |
| `disabled` | `boolean` | `false` |
| `label` | `string` | — |
| `ariaLabel` | `string` | — |
| `widthPx` | `number` | `120` |
| `heightPx` | `number` | `32` |
| `className` | `string` | — |
`numeric` mode adds: `precision?: number` (default 0), `units?: string`.
`text` mode adds: `text: string`.
`scrolling` mode adds: `text: string`, `speedPxPerSec?: number`
(default 30), `windowChars?: number` (default 8).
## Implementation Notes
Scrolling mode injects an inline `<style>` block with a unique keyframe
name (`cs-display-scroll`); the keyframe duration is derived from
text length × per-glyph width / configured speed, so longer strings
scroll at a constant linear-px-per-second rate regardless of length.
The `@media (prefers-reduced-motion: reduce)` rule short-circuits the
animation to `none`, satisfying WAI-ARIA APG guidance for moving
content. The component intentionally does not use `requestAnimationFrame`
or React state ticks — CSS handles the animation so the React render
graph stays static.