Readout legibility (the #1 ask): - bold own-cell decimal point + colon in seven/fourteen-seg (not hairlines) - weight prop thickens strokes; gap controls inter-cell kerning Display colors: - SegmentDisplay defaults to theme.ledGreen, AlphaDisplay to theme.ledAmber - valueColor fn + value-space zones on SegmentDisplay (self-coloring readouts) - charset: colon in both, $ and % in 14-seg; disabled + data-disabled Knob captions: sublabel (second line) + labelSize (independent of dial size). New components: - ToggleSwitch (rocker/slider boolean, role=switch) - Gauge (read-only radial arc meter, round sibling of Meter) - TransportButton (play/stop/record/pause/panic, record blinks) - LampRow (console-print lamp bank), LabeledField, Rack (panel chrome) Responsiveness: fill on Meter + Fader (stretch to container). Niceties: PushButton leadingIcon; theme ledGreen/ledAmber/panel tokens. 54 -> 63 exports. 38 unit tests. Browser-verified.
646 lines
25 KiB
TypeScript
646 lines
25 KiB
TypeScript
import React from 'react';
|
||
import {
|
||
AlphaDisplay,
|
||
Arc,
|
||
DreamknobProvider,
|
||
Fader,
|
||
FlatKnob,
|
||
Gauge,
|
||
ImageKnob,
|
||
Knob,
|
||
KnobValue,
|
||
IndicatorLamp,
|
||
LabeledField,
|
||
LampRow,
|
||
LEDFader,
|
||
LEDKnob,
|
||
MetalKnob,
|
||
Meter,
|
||
MeterBridge,
|
||
NeonKnob,
|
||
Pointer,
|
||
PushButton,
|
||
Rack,
|
||
RubberKnob,
|
||
ScrubField,
|
||
SegmentSwitch,
|
||
SegmentDisplay,
|
||
SteppedKnob,
|
||
Ticks,
|
||
ToggleSwitch,
|
||
TransportButton,
|
||
VintageKnob,
|
||
XYPad,
|
||
logTaper,
|
||
} from 'dreamknob';
|
||
|
||
const PRESETS = [
|
||
{ name: 'INIT PATCH', cutoff: 50 },
|
||
{ name: 'WARM PAD', cutoff: 22 },
|
||
{ name: 'ACID 303', cutoff: 84 },
|
||
];
|
||
|
||
const PresetDemo: React.FC = () => {
|
||
const [preset, setPreset] = React.useState(0);
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 14 }}>
|
||
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||
<FlatKnob
|
||
size={84}
|
||
value={PRESETS[preset].cutoff}
|
||
onChange={() => undefined}
|
||
readOnly
|
||
animateChanges={{ duration: 350 }}
|
||
color="#3df2ad"
|
||
label="Cutoff"
|
||
aria-label="Preset cutoff"
|
||
/>
|
||
<AlphaDisplay value={PRESETS[preset].name} chars={10} height={16} color="#3df2ad" />
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 8 }}>
|
||
{PRESETS.map((p, i) => (
|
||
<PushButton
|
||
key={p.name}
|
||
pressed={preset === i}
|
||
onChange={on => on && setPreset(i)}
|
||
color="#3df2ad"
|
||
size={34}
|
||
aria-label={`Preset ${p.name}`}
|
||
>
|
||
{String.fromCharCode(65 + i)}
|
||
</PushButton>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/** Draw a film-strip of a simple hardware knob at runtime (stand-in for a KnobMan PNG). */
|
||
const useGeneratedStrip = (frames = 31, fs = 120): string | null => {
|
||
const [src, setSrc] = React.useState<string | null>(null);
|
||
React.useEffect(() => {
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = fs;
|
||
canvas.height = fs * frames;
|
||
const g = canvas.getContext('2d');
|
||
if (!g) return;
|
||
for (let i = 0; i < frames; i++) {
|
||
const cx = fs / 2;
|
||
const cy = i * fs + fs / 2;
|
||
const angle = ((225 + (i / (frames - 1)) * 270) * Math.PI) / 180;
|
||
const grad = g.createRadialGradient(cx - fs * 0.15, cy - fs * 0.18, fs * 0.05, cx, cy, fs * 0.45);
|
||
grad.addColorStop(0, '#5c5f69');
|
||
grad.addColorStop(0.65, '#26282e');
|
||
grad.addColorStop(1, '#0e0f12');
|
||
g.fillStyle = grad;
|
||
g.beginPath();
|
||
g.arc(cx, cy, fs * 0.4, 0, Math.PI * 2);
|
||
g.fill();
|
||
g.strokeStyle = 'rgba(0,0,0,0.65)';
|
||
g.lineWidth = fs * 0.025;
|
||
g.stroke();
|
||
g.strokeStyle = '#ffd23e';
|
||
g.lineWidth = fs * 0.05;
|
||
g.lineCap = 'round';
|
||
g.beginPath();
|
||
g.moveTo(cx + Math.sin(angle) * fs * 0.16, cy - Math.cos(angle) * fs * 0.16);
|
||
g.lineTo(cx + Math.sin(angle) * fs * 0.33, cy - Math.cos(angle) * fs * 0.33);
|
||
g.stroke();
|
||
}
|
||
setSrc(canvas.toDataURL('image/png'));
|
||
}, [frames, fs]);
|
||
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;
|
||
// Occasional hot transient so the clip LED gets to do its job.
|
||
const spike = Math.random() < 0.04 ? 9 : 0;
|
||
setLevels([
|
||
-13 + Math.sin(t) * 6 + Math.random() * 7 + spike,
|
||
-14 + Math.sin(t * 1.3 + 1) * 6 + Math.random() * 7 + spike,
|
||
]);
|
||
}, 90);
|
||
return () => clearInterval(id);
|
||
}, []);
|
||
return (
|
||
<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>
|
||
);
|
||
};
|
||
|
||
const RESOURCE_ZONES = [
|
||
{ upTo: 0.75, color: '#3df2ad' },
|
||
{ upTo: 0.9, color: '#ffd23e' },
|
||
{ upTo: 1, color: '#ff4d6b' },
|
||
];
|
||
|
||
const GaugeDemo: React.FC = () => {
|
||
const [load, setLoad] = React.useState<[number, number]>([38, 61]);
|
||
React.useEffect(() => {
|
||
let t = 0;
|
||
const id = setInterval(() => {
|
||
t += 0.12;
|
||
setLoad([
|
||
Math.max(2, Math.min(100, 45 + Math.sin(t) * 30 + Math.random() * 12)),
|
||
Math.max(2, Math.min(100, 62 + Math.sin(t * 0.7 + 2) * 26 + Math.random() * 10)),
|
||
]);
|
||
}, 140);
|
||
return () => clearInterval(id);
|
||
}, []);
|
||
return (
|
||
<div style={{ display: 'flex', gap: 18, alignItems: 'center' }}>
|
||
<Gauge value={load[0]} min={0} max={100} size={104} unit="%" label="CPU" zones={RESOURCE_ZONES} />
|
||
<Gauge value={load[1]} min={0} max={100} size={104} unit="%" label="GPU" zones={RESOURCE_ZONES} needle={false} />
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const SmallReadoutDemo: React.FC = () => {
|
||
const [t, setT] = React.useState(0);
|
||
React.useEffect(() => {
|
||
const id = setInterval(() => setT(v => v + 1), 1000);
|
||
return () => clearInterval(id);
|
||
}, []);
|
||
const hh = String(Math.floor(t / 3600) % 100).padStart(2, '0');
|
||
const mm = String(Math.floor(t / 60) % 60).padStart(2, '0');
|
||
const ss = String(t % 60).padStart(2, '0');
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, alignItems: 'center' }}>
|
||
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||
<SegmentDisplay value={17.5} digits={3} decimals={1} height={13} weight={1.5} />
|
||
<SegmentDisplay value={`${hh}:${mm}:${ss}`} height={13} weight={1.4} />
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||
<SegmentDisplay value={82} digits={3} height={16} weight={1.4} zones={[
|
||
{ upTo: 74, color: '#3df2ad' },
|
||
{ upTo: 89, color: '#ffd23e' },
|
||
{ upTo: Infinity, color: '#ff4d6b' },
|
||
]} />
|
||
<AlphaDisplay value="$4.20" height={15} weight={1.3} />
|
||
<AlphaDisplay value="47%" height={15} weight={1.3} color="#3df2ad" />
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const PanelDemo: React.FC = () => {
|
||
const [fresh, setFresh] = React.useState(true);
|
||
const [mode, setMode] = React.useState(0);
|
||
const [playing, setPlaying] = React.useState(false);
|
||
const [armed, setArmed] = React.useState(false);
|
||
return (
|
||
<Rack orientation="column" title="Performer" accentColor="#c9a6ff">
|
||
<div style={{ display: 'flex', gap: 16, alignItems: 'center', justifyContent: 'center' }}>
|
||
<FlatKnob size={62} defaultValue={7} min={0} max={64} step={1} color="#c9a6ff"
|
||
label="Seed" sublabel="world #7" labelSize={9} aria-label="Seed" />
|
||
<FlatKnob size={62} defaultValue={40} min={0} max={100} color="#c9a6ff"
|
||
label="Refresh" sublabel="every 40" labelSize={9} aria-label="Refresh" />
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||
<TransportButton kind="play" active={playing} onClick={() => setPlaying(p => !p)} size={34} />
|
||
<TransportButton kind="stop" onClick={() => setPlaying(false)} size={34} />
|
||
<TransportButton kind="record" active={armed} onClick={() => setArmed(a => !a)} size={34} />
|
||
<TransportButton kind="panic" onClick={() => { setPlaying(false); setArmed(false); }} size={34} />
|
||
</div>
|
||
<LabeledField label="Fresh seed" labelWidth={78}>
|
||
<ToggleSwitch on={fresh} onChange={setFresh} color="#c9a6ff" showStateLabel aria-label="Fresh seed" />
|
||
</LabeledField>
|
||
<SegmentSwitch options={['SOLID', 'GRAD', 'NOISE']} value={mode} onChange={setMode} color="#c9a6ff" aria-label="Fill mode" />
|
||
<LampRow
|
||
lamps={[
|
||
{ label: 'Cue', on: playing, color: '#3df2ad' },
|
||
{ label: 'Rec', on: armed, color: '#ff4d6b' },
|
||
{ label: 'Seed', on: fresh, color: '#c9a6ff' },
|
||
]}
|
||
/>
|
||
</Rack>
|
||
);
|
||
};
|
||
|
||
const ImageKnobDemo: React.FC = () => {
|
||
const src = useGeneratedStrip();
|
||
if (!src) return null;
|
||
return (
|
||
<ImageKnob
|
||
src={src}
|
||
frames={31}
|
||
size={92}
|
||
defaultValue={40}
|
||
step={1}
|
||
showValue
|
||
label="Sprite"
|
||
aria-label="Film-strip demo"
|
||
/>
|
||
);
|
||
};
|
||
|
||
const Card: React.FC<{
|
||
title: string;
|
||
desc: string;
|
||
tag: string;
|
||
children: React.ReactNode;
|
||
}> = ({ title, desc, tag, children }) => (
|
||
<div className="card">
|
||
<h3>{title}</h3>
|
||
<div className="card-desc">{desc}</div>
|
||
<div className="demo">{children}</div>
|
||
<span className="tag">{tag}</span>
|
||
</div>
|
||
);
|
||
|
||
export const Gallery: React.FC = () => (
|
||
<section className="block" id="gallery">
|
||
<div className="container">
|
||
<div className="section-kicker">Gallery</div>
|
||
<h2 className="section-title">Every style in the studio</h2>
|
||
<p className="section-sub">
|
||
A full rack of prebuilt controls — every color themeable per instance. All of
|
||
them share the same interaction core: rotary or drag adjustment, scroll-wheel
|
||
nudge, keyboard, fine control and double-click reset.
|
||
</p>
|
||
|
||
<div className="grid">
|
||
<Card title="Flat" desc="Clean 2D arc knob for modern plugin UIs." tag="<FlatKnob />">
|
||
<FlatKnob size={86} defaultValue={64} label="Level" aria-label="Flat demo" />
|
||
<FlatKnob size={86} defaultValue={0} min={-50} max={50} arcFrom="center" color="#3df2ad" label="Pan" aria-label="Pan demo" />
|
||
<FlatKnob size={86} defaultValue={30} color="#ffd23e" trackColor="rgba(255,210,62,0.15)" label="Send" aria-label="Send demo" />
|
||
</Card>
|
||
|
||
<Card title="Metal" desc="Brushed aluminum with a knurled rim — 3D hi-fi feel." tag="<MetalKnob />">
|
||
<MetalKnob size={92} defaultValue={40} label="Input" aria-label="Metal demo" />
|
||
<MetalKnob size={92} defaultValue={75} tone="dark" color="#ff9640" label="Drive" aria-label="Dark metal demo" />
|
||
</Card>
|
||
|
||
<Card title="Rubber" desc="Soft-touch synth knob with a glowing halo." tag="<RubberKnob />">
|
||
<RubberKnob size={92} defaultValue={72} label="Cutoff" aria-label="Rubber demo" />
|
||
<RubberKnob size={92} defaultValue={30} color="#4cc2ff" label="Res" aria-label="Rubber resonance demo" />
|
||
</Card>
|
||
|
||
<Card title="Vintage" desc="Chicken-head bakelite over a printed scale." tag="<VintageKnob />">
|
||
<VintageKnob size={96} defaultValue={7} min={0} max={10} step={0.5} label="Volume" scaleLabels={['0', '', '', '', '', '5', '', '', '', '', '10']} aria-label="Vintage demo" />
|
||
<VintageKnob size={96} defaultValue={4} min={0} max={10} bodyColor="#26221f" label="Tone" aria-label="Bakelite demo" />
|
||
</Card>
|
||
|
||
<Card title="LED" desc="Segmented ring + true seven-segment readout." tag="<LEDKnob />">
|
||
<LEDKnob size={96} defaultValue={120} min={40} max={240} step={1} label="BPM" aria-label="LED demo" />
|
||
<LEDKnob size={96} defaultValue={-12.5} min={-60} max={0} step={0.5} digits={4} color="#ff4d6b" label="Thresh" aria-label="Threshold demo" />
|
||
</Card>
|
||
|
||
<Card title="Neon" desc="A glowing arc for dark, futuristic interfaces." tag="<NeonKnob />">
|
||
<NeonKnob size={90} defaultValue={35} label="Space" aria-label="Neon demo" />
|
||
<NeonKnob size={90} defaultValue={80} color="#3df2ad" label="Shine" aria-label="Neon green demo" />
|
||
</Card>
|
||
|
||
<Card title="Stepped" desc="Detented selector with named or numeric positions." tag="<SteppedKnob />">
|
||
<SteppedKnob size={92} positions={['LP', 'BP', 'HP', 'NT']} defaultValue={0} label="Filter" aria-label="Filter type demo" />
|
||
<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, 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>
|
||
|
||
<Card title="LED fader" desc="Segmented meter-style fader with color zones." tag="<LEDFader />">
|
||
<LEDFader
|
||
length={130}
|
||
defaultValue={70}
|
||
step={1}
|
||
zones={[
|
||
{ upTo: 0.6, color: '#3df2ad' },
|
||
{ upTo: 0.85, color: '#ffd23e' },
|
||
{ upTo: 1, color: '#ff4d6b' },
|
||
]}
|
||
label="Level"
|
||
aria-label="LED fader demo"
|
||
/>
|
||
<LEDFader
|
||
orientation="horizontal"
|
||
length={140}
|
||
defaultValue={40}
|
||
step={1}
|
||
color="#4cc2ff"
|
||
label="Pos"
|
||
aria-label="Horizontal LED fader demo"
|
||
/>
|
||
</Card>
|
||
|
||
<Card title="Segment display" desc="The seven-segment engine, standalone." tag="<SegmentDisplay />">
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, alignItems: 'center' }}>
|
||
<SegmentDisplay value={128.5} digits={4} decimals={1} />
|
||
<SegmentDisplay value={-42} digits={4} color="#ff4d6b" />
|
||
</div>
|
||
</Card>
|
||
|
||
<Card title="Log taper" desc="Audio-true frequency sweep: equal ratios per turn." tag="taper={logTaper}">
|
||
<FlatKnob
|
||
size={92}
|
||
min={20}
|
||
max={20000}
|
||
defaultValue={632}
|
||
taper={logTaper}
|
||
decimals={0}
|
||
color="#3df2ad"
|
||
format={v => (v >= 1000 ? `${(v / 1000).toFixed(1)}k` : `${Math.round(v)}`)}
|
||
label="Freq"
|
||
aria-label="Frequency demo"
|
||
/>
|
||
</Card>
|
||
|
||
<Card title="Film-strip" desc="Photoreal sprite knobs — any KnobMan-style PNG strip works. This one is drawn at runtime." tag="<ImageKnob />">
|
||
<ImageKnobDemo />
|
||
</Card>
|
||
|
||
<Card title="Bipolar origin + detent" desc="Fill anchors at value 0 (not mid-travel) and the drag snaps magnetically at 0." tag="origin={0} detents={[0]}">
|
||
<FlatKnob
|
||
size={92}
|
||
min={-60}
|
||
max={12}
|
||
step={0.5}
|
||
defaultValue={0}
|
||
origin={0}
|
||
detents={[0]}
|
||
arcFrom="center"
|
||
color="#3df2ad"
|
||
format={v => `${v > 0 ? '+' : ''}${v.toFixed(1)}`}
|
||
label="Gain"
|
||
aria-label="Bipolar gain demo"
|
||
/>
|
||
<Fader
|
||
length={130}
|
||
min={-60}
|
||
max={12}
|
||
step={0.5}
|
||
defaultValue={0}
|
||
origin={0}
|
||
detents={[0]}
|
||
color="#3df2ad"
|
||
format={v => `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`}
|
||
label="Trim"
|
||
aria-label="Bipolar fader demo"
|
||
/>
|
||
</Card>
|
||
|
||
<Card title="Type-in editing" desc="Double-click the knob (or click a fader readout) and type the exact value — '1.2k' works too." tag="editable">
|
||
<FlatKnob
|
||
size={92}
|
||
min={20}
|
||
max={20000}
|
||
taper={logTaper}
|
||
decimals={0}
|
||
defaultValue={440}
|
||
editable
|
||
color="#4cc2ff"
|
||
format={v => (v >= 1000 ? `${(v / 1000).toFixed(1)}k` : `${Math.round(v)}`)}
|
||
label="Freq"
|
||
aria-label="Editable demo"
|
||
/>
|
||
<Fader
|
||
length={130}
|
||
min={-60}
|
||
max={12}
|
||
step={0.5}
|
||
defaultValue={-6}
|
||
editable
|
||
unit=" dB"
|
||
label="Out"
|
||
aria-label="Editable fader demo"
|
||
/>
|
||
</Card>
|
||
|
||
<Card title="Theming" desc="One provider restyles every control — accent, tracks, text, fonts. Light backgrounds too." tag="<DreamknobProvider />">
|
||
<DreamknobProvider theme={{ accent: '#ff4d6b' }}>
|
||
<FlatKnob size={70} defaultValue={62} label="A" aria-label="Themed A" />
|
||
<RubberKnob size={70} defaultValue={35} label="B" aria-label="Themed B" />
|
||
</DreamknobProvider>
|
||
<div
|
||
style={{
|
||
background: '#eceef2',
|
||
borderRadius: 10,
|
||
padding: '12px 16px',
|
||
display: 'flex',
|
||
gap: 14,
|
||
alignItems: 'center',
|
||
}}
|
||
>
|
||
<DreamknobProvider base="light" theme={{ accent: '#0868c8' }}>
|
||
<FlatKnob size={70} defaultValue={62} label="Light" aria-label="Light themed" />
|
||
</DreamknobProvider>
|
||
</div>
|
||
</Card>
|
||
|
||
<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="Gauge" desc="Read-only radial arc meter — the round sibling of Meter. Colors itself green→amber→red as load climbs." tag="<Gauge />">
|
||
<GaugeDemo />
|
||
</Card>
|
||
|
||
<Card title="Small readouts" desc="Bold own-cell decimal + colon, weight to thicken strokes, and value-colored zones — legible at status-bar sizes. Colons and $/% are in the charset." tag="weight · zones · : $ %">
|
||
<SmallReadoutDemo />
|
||
</Card>
|
||
|
||
<Card title="Panel: strip + switches" desc="Rack chrome, ToggleSwitch, TransportButton, SegmentSwitch and a LampRow — a console section, declarative." tag="<Rack /> · <ToggleSwitch /> · <TransportButton />">
|
||
<PanelDemo />
|
||
</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="Animated presets + buttons" desc="animateChanges tweens programmatic changes; PushButton latches or is momentary." tag="animateChanges · <PushButton />">
|
||
<PresetDemo />
|
||
</Card>
|
||
|
||
<Card title="XY pad" desc="Two parameters at once — cutoff/resonance, vector mixing, FX morphing." tag="<XYPad />">
|
||
<XYPad
|
||
width={190}
|
||
height={140}
|
||
x={{ min: 20, max: 20000, step: 1, defaultValue: 800 }}
|
||
y={{ min: 0, max: 100, step: 0.5, defaultValue: 30 }}
|
||
formatX={v => (v >= 1000 ? `${(v / 1000).toFixed(1)}k` : `${Math.round(v)}`)}
|
||
color="#e44cff"
|
||
label="Filter"
|
||
aria-label="Filter XY pad"
|
||
/>
|
||
</Card>
|
||
|
||
<Card title="Endless encoder" desc="wrap makes the value roll around min↔max — full-turn phase and offset dials." tag="wrap · angleRange={360}">
|
||
<NeonKnob
|
||
size={92}
|
||
min={0}
|
||
max={360}
|
||
step={1}
|
||
defaultValue={90}
|
||
wrap
|
||
angleOffset={0}
|
||
angleRange={360}
|
||
unit="°"
|
||
color="#4cc2ff"
|
||
label="Phase"
|
||
aria-label="Phase encoder"
|
||
/>
|
||
</Card>
|
||
|
||
<Card title="Drag bubble" desc="A floating readout follows the gesture — keep the knob face clean." tag="valueBubble">
|
||
<MetalKnob
|
||
size={92}
|
||
defaultValue={64}
|
||
step={1}
|
||
valueBubble
|
||
bubbleFormat={v => `${v} %`}
|
||
label="Blend"
|
||
aria-label="Bubble demo"
|
||
/>
|
||
</Card>
|
||
|
||
<Card title="Alpha display" desc="Fourteen-segment alphanumeric LED — preset names, modes, messages." tag="<AlphaDisplay />">
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, alignItems: 'center' }}>
|
||
<AlphaDisplay value="DREAMKNOB" height={20} />
|
||
<AlphaDisplay value="LP-24 SAT+2" height={16} color="#4cc2ff" />
|
||
</div>
|
||
</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" />
|
||
<Arc radius={36} thickness={2.5} color="#ffd23e" trackColor="rgba(255,255,255,0.08)" cap="butt" />
|
||
<Pointer type="triangle" radius={33} length={9} width={8} color="#ffd23e" />
|
||
<KnobValue color="#fff" fontSize={15} />
|
||
</Knob>
|
||
</Card>
|
||
|
||
<Card title="Vertical-drag mode" desc="Prefer the up/down plugin feel? One prop." tag="interaction='vertical'">
|
||
<RubberKnob size={92} defaultValue={50} interaction="vertical" color="#3df2ad" label="Depth" showValue aria-label="Vertical drag demo" />
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|