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:
parent
b3eaa604c2
commit
d8b7b55505
31 changed files with 1471 additions and 223 deletions
|
|
@ -67,8 +67,9 @@ export const ApiDocs: React.FC = () => (
|
|||
<Row name="wheelStep" type="number" def="step (else 1% of range)" desc="Value change per wheel notch, independent of the snapping step." />
|
||||
<Row name="wheelRequiresFocus" type="boolean" def="false" desc="Only respond to the wheel while focused — hovering no longer captures page scroll." />
|
||||
<Row name="doubleClickReset" type="boolean" def="auto" desc="Double-click resets to defaultValue." />
|
||||
<Row name="onChange" type="(v, meta) => void" desc="Fires for every distinct snapped value. meta.source tells you what caused it: 'drag' | 'wheel' | 'keyboard' | 'reset' | 'api'." />
|
||||
<Row name="onChangeStart / onChangeEnd" type="(v, meta) => void" desc="Gesture boundaries (drag, wheel burst, key press, reset) — ideal for undo history or parameter automation." />
|
||||
<Row name="onChange" type="(v, meta) => void" desc="Fires for every distinct snapped value. meta.source tells you what caused it: 'drag' | 'wheel' | 'keyboard' | 'reset' | 'edit' | 'api'." />
|
||||
<Row name="onChangeStart / onChangeEnd" type="(v, meta) => void" desc="Gesture boundaries (drag, wheel burst, key press, reset, type-in edit) — ideal for undo history or parameter automation." />
|
||||
<Row name="commitMode" type="'live' | 'release'" def="'live'" desc="'release' previews drags visually but emits onChange only once, on pointer-up — for expensive parameters (reverb sizes, engine restarts). Wheel/keyboard still emit live; Esc discards the preview." />
|
||||
<Row name="◇ name" type="string" desc="Renders a hidden form input carrying the value — plain <form> posts and form libraries just work (XYPad uses nameX/nameY)." />
|
||||
<Row name="disabled / readOnly" type="boolean" def="false" desc="Disable interaction (readOnly stays focusable)." />
|
||||
<Row name="◇ editable / parseValue" type="boolean / fn" def="false" desc="Type-in editing: double-click a knob (or click a fader readout), type a value, Enter commits, Esc cancels. '1.2k' → 1200." />
|
||||
|
|
@ -87,21 +88,56 @@ export const ApiDocs: React.FC = () => (
|
|||
via pointer capture · every control shows a keyboard-only focus ring.
|
||||
</p>
|
||||
|
||||
<h3 className="api-h">The change contract</h3>
|
||||
<p className="api-note">
|
||||
Every way a value can move maps to a <code>ChangeMeta.source</code>, and every
|
||||
user gesture is bracketed by exactly one <code>onChangeStart</code> /{' '}
|
||||
<code>onChangeEnd</code> pair — wire undo snapshots to Start and history commits
|
||||
to End and the ledger always balances.
|
||||
</p>
|
||||
<table className="api-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>source</th>
|
||||
<th>Emitted by</th>
|
||||
<th>Start/End?</th>
|
||||
<th>onChange cadence</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>'drag'</td><td>pointer drags (rotary, linear, track, XY, scrub)</td><td>yes — one pair per drag</td><td>each snapped value ('live') or once on release ('release')</td></tr>
|
||||
<tr><td>'wheel'</td><td>scroll wheel / trackpad</td><td>yes — one pair per burst (150–250 ms settle)</td><td>each notch</td></tr>
|
||||
<tr><td>'keyboard'</td><td>arrows, PageUp/Down, Home/End</td><td>yes — one pair per press</td><td>each step</td></tr>
|
||||
<tr><td>'reset'</td><td>double-click reset</td><td>yes</td><td>once</td></tr>
|
||||
<tr><td>'edit'</td><td>type-in editors (editable, ScrubField)</td><td>yes</td><td>once, on commit</td></tr>
|
||||
<tr><td>'api'</td><td>setValue() from your code</td><td><strong>no</strong> — programmatic moves are not gestures</td><td>each call that lands on a new value</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p className="api-note">
|
||||
A cancelled gesture (Esc, or <code>disabled</code> flipping mid-drag) restores the
|
||||
start value and still fires <code>onChangeEnd</code>, so Start/End counts always
|
||||
match. <code>onChange</code> never repeats a value within a gesture.
|
||||
</p>
|
||||
|
||||
<h3 className="api-h">Prebuilt skins</h3>
|
||||
<Table>
|
||||
<Row name="<FlatKnob />" type="2D" desc="Modern arc knob. color, trackColor, faceColor, pointerColor, textColor, arcFrom ('min' | 'center'), arcThickness." />
|
||||
<Row name="<MetalKnob />" type="3D" desc="Brushed aluminum. tone ('silver' | 'dark'), color, tickCount, showArc, indicatorColor." />
|
||||
<Row name="<RubberKnob />" type="3D" desc="Soft-touch synth knob with glow halo. color, trackColor, arcFrom." />
|
||||
<Row name="<VintageKnob />" type="analog" desc="Chicken-head pointer. bodyColor (cream/bakelite/any), scaleColor, scaleLabels." />
|
||||
<Row name="<LEDKnob />" type="digital" desc="Segmented LED ring + seven-segment readout. color, offColor, segments, digits, displayDecimals." />
|
||||
<Row name="<LEDKnob />" type="digital" desc="Segmented LED ring + seven-segment readout. color, offColor, segments, digits, displayDecimals (any precision), unit label under the digits." />
|
||||
<Row name="<NeonKnob />" type="2D" desc="Glowing arc + dot pointer. color, trackColor, arcFrom." />
|
||||
<Row name="<SteppedKnob />" type="selector" desc="Detented switch. positions (named) or steps (numeric), color, faceColor." />
|
||||
<Row name="<Fader />" type="linear" desc="Channel fader. orientation, length, breadth, color, tickCount, showFill, unit, format." />
|
||||
<Row name="<LEDFader />" type="linear · digital" desc="Segmented LED meter-fader. orientation, length, segments, color or zones ([{upTo, color}]), glow, digits." />
|
||||
<Row name="<Fader />" type="linear" desc="Channel fader. orientation, length, breadth, color, tickCount, showFill, unit, format, valuePosition ('top' | 'end' | 'none' — 'end' gives one-line inspector rows)." />
|
||||
<Row name="<LEDFader />" type="linear · digital" desc="Segmented LED meter-fader. orientation, length, segments, color or zones ([{upTo, color}] — defaults to the theme's green/amber/red), glow, digits, unit." />
|
||||
<Row name="<ImageKnob />" type="sprite" desc="Film-strip knob (KnobMan-style PNG strips): src, frames, stripOrientation. Photoreal skins with zero drawing code." />
|
||||
<Row name="<Meter />" type="display" desc="Read-only LED level meter: value, zones, peakHold (ms), peakColor, orientation, seven-segment readout. Clip LED: showClip (default on), clipThreshold (default max), clipHold (ms or 'latch', click to clear), clipColor, onClip." />
|
||||
<Row name="<XYPad />" type="2D control" desc="Two-parameter pad: x/y axis configs, drag + arrow keys, grid, crosshair, Escape cancel, double-click reset." />
|
||||
<Row name="<PushButton />" type="switch" desc="Panel button with LED strip: toggle or momentary, controlled/uncontrolled, aria-pressed." />
|
||||
<Row name="<Meter />" type="display" desc="Read-only LED level meter: value (number or [l, r, ...] for multi-channel), zones (defaults to theme green/amber/red), peakHold (ms), peakColor, orientation, unit, seven-segment readout. Clip LED: showClip (default on), clipThreshold (default max), clipHold (ms or 'latch', click to clear), clipColor, onClip." />
|
||||
<Row name="<MeterBridge />" type="display" desc="Labeled multi-strip meter block: channels ([{label, value, clipValue?}]), per-channel peak-hold text, shared clip LED — the mixer meter row, prebuilt." />
|
||||
<Row name="<XYPad />" type="2D control" desc="Two-parameter pad: x/y axis configs (each with min/max/step/invert/detents), drag + arrow keys (arrows follow the visual direction), grid, crosshair, Escape cancel, double-click reset." />
|
||||
<Row name="<PushButton />" type="switch" desc="Panel button: toggle or momentary, controlled/uncontrolled, aria-pressed. led: 'strip' (default) or 'dot' — the corner dot scales down for small icon-only toggles." />
|
||||
<Row name="<IndicatorLamp />" type="switch · display" desc="Panel LED lamp: pressable toggle (role='switch') or read-only status light, blink, momentary, label either side — the checkbox, in hardware form." />
|
||||
<Row name="<SegmentSwitch />" type="selector" desc="Segmented option switch (radiogroup): options list, LED tick on the active cell, arrow/Home/End keys, controlled/uncontrolled index." />
|
||||
<Row name="<ScrubField />" type="numeric field" desc="After-Effects-style number field: drag the label to scrub (Shift = fine), click to type (Enter commits, Esc reverts), arrows step, wheel nudges. role='spinbutton'." />
|
||||
<Row name="<AlphaDisplay />" type="digital" desc="Fourteen-segment alphanumeric LED: A–Z 0–9 and symbols, chars padding, align, glow, skew." />
|
||||
<Row name="<SegmentDisplay />" type="digital" desc="Standalone seven-segment display. digits, decimals, color, glow, skew, ghostOpacity." />
|
||||
</Table>
|
||||
|
|
@ -118,13 +154,19 @@ export const ApiDocs: React.FC = () => (
|
|||
<code>label</code>) and every interactive root exposes{' '}
|
||||
<code>data-dragging</code>/<code>data-disabled</code>/<code>data-focus-visible</code>{' '}
|
||||
— style any state or part with plain CSS. <code><Arc></code> additionally
|
||||
accepts <code>gradient</code> (position-anchored color stops).
|
||||
accepts <code>gradient</code> (position-anchored color stops). Geometry is
|
||||
self-defending: <code>Meter</code>/<code>LEDFader</code> clamp their internal
|
||||
padding below <code>breadth</code> 14 and never emit negative dimensions, so
|
||||
4-px status slivers render fine.
|
||||
</p>
|
||||
|
||||
<h3 className="api-h">Theming</h3>
|
||||
<p className="api-note">
|
||||
Every skin resolves its color and font defaults from the active theme; per-instance
|
||||
props always win. Use <code>base="light"</code> for light backgrounds.
|
||||
props always win. Use <code>base="light"</code> for light backgrounds. Any CSS
|
||||
color works — hex, <code>rgb()</code>, <code>oklch()</code>, even{' '}
|
||||
<code>var(--accent)</code>: gradient and glow math uses hex fast-paths where it
|
||||
can and falls back to CSS <code>color-mix()</code> everywhere else.
|
||||
</p>
|
||||
<CodeBlock
|
||||
code={`import { DreamknobProvider } from 'dreamknob'
|
||||
|
|
@ -137,6 +179,9 @@ export const ApiDocs: React.FC = () => (
|
|||
text: '#fff',
|
||||
label: 'rgba(255,255,255,0.5)',
|
||||
fontMono: '"IBM Plex Mono", monospace',
|
||||
zoneGood: '#3df2ad', // meter zones: green / amber / red
|
||||
zoneWarn: '#ffd23e', // used by Meter, MeterBridge and
|
||||
zoneHot: '#ff4d6b', // LEDFader when no zones are given
|
||||
}}
|
||||
>
|
||||
<FlatKnob ... /> <Fader ... /> <LEDKnob ... />
|
||||
|
|
@ -189,6 +234,40 @@ function MyKnob() {
|
|||
}`}
|
||||
/>
|
||||
|
||||
<h3 className="api-h">Recipes</h3>
|
||||
<p className="api-note">
|
||||
The prop combinations we reach for in real consoles and inspectors.
|
||||
</p>
|
||||
<CodeBlock
|
||||
code={`// Bipolar gain — arc grows from center, double-click back to 0
|
||||
<FlatKnob min={-12} max={12} step={0.1} defaultValue={0}
|
||||
origin={0} detents={[0]} unit=" dB" aria-label="Gain" />
|
||||
|
||||
// Angle dial — endless, wraps 0↔360, full-circle travel
|
||||
<FlatKnob min={0} max={360} step={1} wrap
|
||||
angleOffset={0} angleRange={360} unit="°" aria-label="Angle" />
|
||||
|
||||
// Expensive parameter — drag previews, engine hears one change on release
|
||||
<MetalKnob min={0.1} max={10} step={0.1} commitMode="release"
|
||||
onChange={v => rebuildReverbTail(v)} aria-label="Room size" />
|
||||
|
||||
// Wide-range filter cutoff — log taper puts the musical range under your thumb
|
||||
<RubberKnob min={20} max={20000} taper={logTaper} step={1}
|
||||
format={v => v >= 1000 ? (v / 1000).toFixed(1) + ' kHz' : v + ' Hz'}
|
||||
aria-label="Cutoff" />
|
||||
|
||||
// Inspector knob — relative drag (no grab-jump), flick fast / drag slow
|
||||
<FlatKnob size={40} rotaryMode="relative" dragAcceleration={0.5}
|
||||
wheelRequiresFocus editable aria-label="Opacity" />
|
||||
|
||||
// One-line inspector row — fader with the readout beside it
|
||||
<Fader orientation="horizontal" length={140} valuePosition="end"
|
||||
editable unit="px" aria-label="Radius" />
|
||||
|
||||
// Stereo meter — one component, two channels, shared clip LED
|
||||
<Meter value={[left, right]} min={-60} max={6} clipThreshold={0} />`}
|
||||
/>
|
||||
|
||||
<h3 className="api-h">Precision & number handling</h3>
|
||||
<p className="api-note">
|
||||
Values are snapped to <code>step</code> (anchored at <code>min</code>) and rounded
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -30,12 +30,12 @@ export const GettingStarted: React.FC = () => (
|
|||
|
||||
<Step n={1} title="Install">
|
||||
<CodeBlock
|
||||
code={`pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.0.0/dreamknob-1.0.0.tgz\n# (or npm install <same url>) · peer deps: react >= 18, react-dom >= 18`}
|
||||
code={`pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.0/dreamknob-1.1.0.tgz\n# (or npm install <same url>) · peer deps: react >= 18, react-dom >= 18`}
|
||||
/>
|
||||
<p className="api-note" style={{ marginTop: 10 }}>
|
||||
Installs straight from the{' '}
|
||||
<a href="https://git.dreamodus.software/Dreamodus/DreamKnob/releases">
|
||||
v1.0.0 release
|
||||
v1.1.0 release
|
||||
</a>{' '}
|
||||
on our Forgejo. Zero runtime dependencies, ESM + CJS, full TypeScript types. No
|
||||
CSS file to import — everything is SVG and inline styles.
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ export const Hero: React.FC = () => {
|
|||
range, any decimal precision, and colors you fully control.
|
||||
</p>
|
||||
<div className="install">
|
||||
<span>pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.0.0/dreamknob-1.0.0.tgz</span>
|
||||
<span>pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.0/dreamknob-1.1.0.tgz</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText('pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.0.0/dreamknob-1.0.0.tgz');
|
||||
navigator.clipboard.writeText('pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.0/dreamknob-1.1.0.tgz');
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1400);
|
||||
}}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue