Tier 2: drag feel, wheel overhaul, refs/meta/forms, Meter, gradients

- rotaryMode: 'absolute' | 'relative' (angle deltas, no grab jump) |
  'pickup' (MIDI-style: engages when the pointer sweeps past the value)
- dragAcceleration: velocity gain for relative drags (Shift bypasses);
  hideCursorOnDrag hides the cursor mid-gesture
- Wheel: trackpad delta accumulation (flicks can't rocket the value,
  one notch per wheel click), wheelStep override, wheelRequiresFocus
  opt-out of the hover scroll-trap
- onChange/onChangeStart/onChangeEnd now receive { source: 'drag' |
  'wheel' | 'keyboard' | 'reset' | 'api' }
- Every component forwards a ref to its root; name prop renders a
  hidden form input; SVG primitives carry data-part attributes
- Arc gradient: position-anchored color stops rendered as sliced arcs
  (mixColors/sampleGradient exported, unit-tested)
- New read-only Meter: LED level meter with peak hold, zones, readout
- Fader capColor/capLength for custom handle styling
- Docs: Meter/gradient/drag-feel gallery cards, white-cap fader demo,
  API rows, README
This commit is contained in:
Dreamodus 2026-07-12 15:18:54 -07:00
parent 804a85cca6
commit 0aee48b749
27 changed files with 756 additions and 134 deletions

View file

@ -10,6 +10,7 @@ import {
LEDFader,
LEDKnob,
MetalKnob,
Meter,
NeonKnob,
Pointer,
RubberKnob,
@ -57,6 +58,32 @@ const useGeneratedStrip = (frames = 31, fs = 120): string | null => {
return src;
};
const MeterDemo: React.FC = () => {
const [levels, setLevels] = React.useState<[number, number]>([-18, -20]);
React.useEffect(() => {
let t = 0;
const id = setInterval(() => {
t += 0.09;
setLevels([
-13 + Math.sin(t) * 6 + Math.random() * 7,
-14 + Math.sin(t * 1.3 + 1) * 6 + Math.random() * 7,
]);
}, 90);
return () => clearInterval(id);
}, []);
const zones = [
{ upTo: 0.72, color: '#3df2ad' },
{ upTo: 0.9, color: '#ffd23e' },
{ upTo: 1, color: '#ff4d6b' },
];
return (
<div style={{ display: 'flex', gap: 12 }}>
<Meter value={levels[0]} min={-60} max={6} length={132} zones={zones} peakColor="#fff" label="L" aria-label="Left level" />
<Meter value={levels[1]} min={-60} max={6} length={132} zones={zones} peakColor="#fff" label="R" aria-label="Right level" />
</div>
);
};
const ImageKnobDemo: React.FC = () => {
const src = useGeneratedStrip();
if (!src) return null;
@ -136,8 +163,9 @@ export const Gallery: React.FC = () => (
<SteppedKnob size={92} min={-24} max={24} steps={9} color="#4cc2ff" defaultValue={0} label="Semi" aria-label="Semitones demo" />
</Card>
<Card title="Fader" desc="Absolute-position linear control, both orientations." tag="<Fader />">
<Card title="Fader" desc="Absolute-position linear control, both orientations, custom caps." tag="<Fader />">
<Fader length={130} defaultValue={-6} min={-60} max={12} step={0.5} unit=" dB" label="Main" aria-label="Fader demo" />
<Fader length={130} defaultValue={20} step={1} capColor="#e8e8ec" capLength={26} color="#3df2ad" label="Aux" aria-label="White cap fader demo" />
<Fader orientation="horizontal" length={150} defaultValue={35} step={1} color="#e44cff" label="X-Fade" aria-label="Crossfade demo" />
</Card>
@ -269,6 +297,49 @@ export const Gallery: React.FC = () => (
</div>
</Card>
<Card title="Meter" desc="Read-only LED metering with peak hold — the display sibling of LEDFader." tag="<Meter peakHold />">
<MeterDemo />
</Card>
<Card title="Gradient arc" desc="Position-anchored gradient — the red zone stays at the top of travel." tag="<Arc gradient={...} />">
<Knob size={96} min={-60} max={6} step={0.5} defaultValue={-8} aria-label="Gradient arc demo">
<Arc
thickness={7}
trackColor="rgba(255,255,255,0.08)"
gradient={[
{ at: 0, color: '#3df2ad' },
{ at: 0.72, color: '#3df2ad' },
{ at: 0.9, color: '#ffd23e' },
{ at: 1, color: '#ff4d6b' },
]}
/>
<Pointer type="line" radius={33} length={12} width={3} color="#e8eaf0" />
<KnobValue unit=" dB" color="#e8eaf0" fontSize={14} />
</Knob>
</Card>
<Card title="Drag feel options" desc="Pickup mode never jumps on grab; acceleration makes fast vertical drags cover more range." tag="rotaryMode · dragAcceleration">
<FlatKnob
size={88}
defaultValue={70}
rotaryMode="pickup"
color="#ffd23e"
label="Pickup"
aria-label="Pickup mode demo"
/>
<RubberKnob
size={88}
defaultValue={40}
interaction="vertical"
dragAcceleration={2}
hideCursorOnDrag
showValue
color="#4cc2ff"
label="Accel"
aria-label="Acceleration demo"
/>
</Card>
<Card title="Compose your own" desc="Primitives + headless core = any design you want." tag="<Knob> + primitives">
<Knob size={96} defaultValue={42} aria-label="Composed demo">
<Ticks count={28} radius={47} length={5} width={1.5} color="rgba(255,255,255,0.12)" activeColor="#ffd23e" />