v1.1.0: integration-feedback release

Bugs from real-product integration:
- Meter/LEDFader clamp geometry below breadth 14 (no negative SVG rects)
- mixColors falls back to CSS color-mix() so theme tokens can be var()/oklch()
- type-in editing fires a real edit gesture (Start/onChange/End, source 'edit')
- ScrubField Enter/Escape no longer double-commit or commit-on-revert via blur
- disabled dims the whole control uniformly, data-disabled on the wrapper

New capabilities:
- commitMode 'release': drags preview, emit once on pointer-up, Esc discards
- Meter accepts value=[l, r, ...]; theme zoneGood/zoneWarn/zoneHot defaults
- XYPad per-axis invert + detents; Fader valuePosition top|end|none
- LED skins: unit labels, uncapped displayDecimals, aria-valuetext defaults

New components: IndicatorLamp, SegmentSwitch, ScrubField, MeterBridge,
PushButton led='dot'. Docs: change-contract table, recipes, gallery cards.
This commit is contained in:
Dreamodus 2026-07-13 12:07:52 -07:00
parent b3eaa604c2
commit d8b7b55505
31 changed files with 1471 additions and 223 deletions

View file

@ -8,14 +8,18 @@ import {
ImageKnob,
Knob,
KnobValue,
IndicatorLamp,
LEDFader,
LEDKnob,
MetalKnob,
Meter,
MeterBridge,
NeonKnob,
Pointer,
PushButton,
RubberKnob,
ScrubField,
SegmentSwitch,
SegmentDisplay,
SteppedKnob,
Ticks,
@ -117,15 +121,78 @@ const MeterDemo: React.FC = () => {
}, 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 style={{ display: 'flex', gap: 22, alignItems: 'flex-end' }}>
<Meter
value={levels}
min={-60}
max={6}
length={132}
peakColor="#fff"
label="L / R"
aria-label="Stereo level"
/>
<MeterBridge
channels={[
{ label: 'L', value: levels[0] },
{ label: 'R', value: levels[1] },
{ label: 'M', value: (levels[0] + levels[1]) / 2 },
{ label: 'S', value: (levels[0] - levels[1]) / 2 - 20 },
]}
min={-60}
max={6}
length={132}
breadth={20}
peakTextDecimals={0}
label="Bridge"
aria-label="Meter bridge"
/>
</div>
);
};
const LampSwitchDemo: React.FC = () => {
const [mode, setMode] = React.useState(1);
const [armed, setArmed] = React.useState(true);
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 }}>
<SegmentSwitch
options={['LP', 'BP', 'HP', 'NOTCH']}
value={mode}
onChange={setMode}
color="#4cc2ff"
aria-label="Filter type"
/>
<div style={{ display: 'flex', gap: 18, alignItems: 'center' }}>
<IndicatorLamp on={armed} onChange={setArmed} color="#ff4d6b" label="REC" aria-label="Record arm" />
<IndicatorLamp on={armed} readOnly blink color="#ff4d6b" label="SIG" aria-label="Signal lamp" />
<PushButton led="dot" size={30} color="#3df2ad" defaultPressed aria-label="Snap toggle">
</PushButton>
</div>
</div>
);
};
const CommitModeDemo: React.FC = () => {
const [commits, setCommits] = React.useState(0);
return (
<div style={{ display: 'flex', gap: 22, alignItems: 'center' }}>
<FlatKnob
size={84}
defaultValue={35}
color="#ffd23e"
label="Release"
commitMode="release"
onChange={() => setCommits(n => n + 1)}
aria-label="Release commit demo"
/>
<div data-commit-count={commits} style={{ display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center' }}>
<SegmentDisplay value={commits} digits={3} decimals={0} height={20} color="#ffd23e" />
<span style={{ fontSize: 10, letterSpacing: '0.08em', color: 'rgba(232,234,240,0.45)', textTransform: 'uppercase' }}>
onChange calls
</span>
</div>
</div>
);
};
@ -343,10 +410,25 @@ export const Gallery: React.FC = () => (
</div>
</Card>
<Card title="Meter" desc="Read-only LED metering with peak hold and a latching clip LED — click it to clear." tag="<Meter clipHold={1500} />">
<Card title="Meters" desc="Stereo metering from one component (value={[l, r]}), or a full labeled bridge with per-channel peak text. Click the clip LED to clear it." tag="<Meter /> · <MeterBridge />">
<MeterDemo />
</Card>
<Card title="Lamps & switches" desc="A segmented option switch, pressable indicator lamps (blink included) and a corner-dot button — the whole panel family." tag="<SegmentSwitch /> · <IndicatorLamp />">
<LampSwitchDemo />
</Card>
<Card title="Scrub field" desc="After-Effects-style numbers: drag the label to scrub, click to type, Shift for fine. '1.2k' parses too." tag="<ScrubField />">
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<ScrubField label="Width" defaultValue={1280} min={0} max={4096} unit="px" aria-label="Width" />
<ScrubField label="Scale" defaultValue={1} min={0} max={10} step={0.01} widthCh={5} unit="×" aria-label="Scale" />
</div>
</Card>
<Card title="Release commit" desc="commitMode='release' previews the drag but your app hears one onChange, on pointer-up — for parameters that are expensive to apply." tag="commitMode='release'">
<CommitModeDemo />
</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