DreamKnob/apps/docs/src/sections/Gallery.tsx
Dreamodus 5b97091902 v1.1.1: second integration-feedback release
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.
2026-07-13 19:20:23 -07:00

646 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);