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.
This commit is contained in:
Dreamodus 2026-07-13 19:20:23 -07:00
parent d8b7b55505
commit 5b97091902
32 changed files with 1535 additions and 133 deletions

View file

@ -128,24 +128,32 @@ export const ApiDocs: React.FC = () => (
<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, valuePosition ('top' | 'end' | 'none' — 'end' gives one-line inspector rows)." />
<Row name="<Fader />" type="linear" desc="Channel fader. orientation, length, breadth, color, tickCount, showFill, unit, format, valuePosition ('top' | 'end' | 'none'), fill (stretch to container)." />
<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 (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="<Meter />" type="display" desc="Read-only LED level meter: value (number or [l, r, ...]), zones (theme green/amber/red), peakHold, peakColor, orientation, unit, fill (stretch to container), disabled, seven-segment readout. Clip LED: showClip, clipThreshold, clipHold (ms or 'latch'), 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="<PushButton />" type="switch" desc="Panel button: toggle or momentary, controlled/uncontrolled, aria-pressed. led: 'strip' | 'dot'. leadingIcon slot tinted to the LED." />
<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: AZ 09 and symbols, chars padding, align, glow, skew." />
<Row name="<SegmentDisplay />" type="digital" desc="Standalone seven-segment display. digits, decimals, color, glow, skew, ghostOpacity." />
<Row name="<AlphaDisplay />" type="digital" desc="Fourteen-segment alphanumeric LED: AZ 09 and symbols (now incl. : $ %), chars padding, align, weight, gap, glow, skew. Defaults to theme.ledAmber." />
<Row name="<SegmentDisplay />" type="digital" desc="Standalone seven-segment display. digits, decimals, weight (thicker strokes), gap (kerning), valueColor / zones (color by value), bold own-cell decimal + colon, disabled. Defaults to theme.ledGreen." />
<Row name="<Gauge />" type="display" desc="Read-only radial arc meter — the round sibling of Meter. value/min/max/taper, zones or valueColor, needle, center readout, unit. For CPU/RAM/load." />
<Row name="<ToggleSwitch />" type="switch" desc="Rocker/slider boolean (role='switch'): the knob slides OFF↔ON. label, showStateLabel, ←/→ keys, controlled/uncontrolled, name." />
<Row name="<TransportButton />" type="switch" desc="play / pause / stop / record / rewind / forward / panic, styled to match the controls beside it. active lights it; record blinks (reduced-motion aware)." />
<Row name="<LampRow />" type="display" desc="Row of small indicator lamps with console-print captions (● CUE ● MUTE). Read-only status, or pass onToggle to make them pressable." />
<Row name="<LabeledField />" type="layout" desc="One aligned inspector row: label · control · unit. Share labelWidth across a stack and the controls line up." />
<Row name="<Rack />" type="layout" desc="Channel-strip / rack container owning panel chrome + spacing: orientation row|column, title, nestable. Declarative console sections." />
</Table>
<p className="api-note">
Every knob skin and fader also takes <code>label</code>, <code>showValue</code>,{' '}
<code>unit</code>, <code>format</code>, <code>className</code>,{' '}
<code>style</code>, <code>name</code> plus every core prop above, and forwards a
ref to its root element (<code>VintageKnob</code> has no readout, so it omits{' '}
Every knob skin and fader also takes <code>label</code>, <code>sublabel</code>{' '}
(a second caption line <code>SEED</code> over <code>world #7</code>),{' '}
<code>labelSize</code> (caption size independent of the dial diameter),{' '}
<code>showValue</code>, <code>unit</code>, <code>format</code>,{' '}
<code>className</code>, <code>style</code>, <code>name</code> plus every core prop
above, and forwards a ref to its root element (<code>VintageKnob</code> has no readout, so it omits{' '}
<code>showValue</code>/<code>unit</code>/<code>format</code>; the display
components <code>Meter</code>/<code>SegmentDisplay</code>/<code>AlphaDisplay</code>{' '}
have their own smaller prop sets). Primitive-drawn parts carry{' '}
@ -180,8 +188,11 @@ export const ApiDocs: React.FC = () => (
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
zoneWarn: '#ffd23e', // used by Meter, MeterBridge, Gauge
zoneHot: '#ff4d6b', // and LEDFader when no zones given
ledGreen: '#3df2ad', // SegmentDisplay default lit color
ledAmber: '#ffb84d', // AlphaDisplay default lit color
panel: 'rgba(255,255,255,0.03)', // Rack / panel chrome
}}
>
<FlatKnob ... /> <Fader ... /> <LEDKnob ... />
@ -234,6 +245,18 @@ function MyKnob() {
}`}
/>
<h3 className="api-h">Small readouts</h3>
<p className="api-note">
Segment displays keep their proportions down to about{' '}
<strong>11&nbsp;px</strong> tall; below that, raise <code>weight</code> to thicken
the strokes rather than shrinking further. The decimal point and colon render as
bold cells with their own width (not hairlines between digits), so{' '}
<code>17.5</code> and <code>1:23:45</code> stay readable in a status bar. Widen{' '}
<code>gap</code> when packed tokens like <code>18.1/24</code> crowd. Colons and{' '}
<code>$</code>/<code>%</code> are in the charset, so a whole token can live in one
display instead of text beside it.
</p>
<h3 className="api-h">Recipes</h3>
<p className="api-note">
The prop combinations we reach for in real consoles and inspectors.
@ -265,7 +288,34 @@ function MyKnob() {
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} />`}
<Meter value={[left, right]} min={-60} max={6} clipThreshold={0} />
// Resource gauge — colors itself green → amber → red as load climbs
<Gauge value={cpu} min={0} max={100} unit="%" label="CPU" size={96}
zones={[{upTo: 0.75, color: '#3df2ad'},
{upTo: 0.9, color: '#ffd23e'},
{upTo: 1, color: '#ff4d6b'}]} />
// Self-coloring resource readout — no app-side color math
<SegmentDisplay value={cpu} digits={3} unit="%" weight={1.4}
zones={[{upTo: 74, color: '#3df2ad'},
{upTo: 89, color: '#ffd23e'},
{upTo: Infinity, color: '#ff4d6b'}]} />
// Aligned inspector rows — share labelWidth so controls line up
<LabeledField label="Radius" unit="px" labelWidth={64}>
<ScrubField defaultValue={12} min={0} max={200} />
</LabeledField>
<LabeledField label="Fresh seed" labelWidth={64}>
<ToggleSwitch defaultOn showStateLabel />
</LabeledField>
// A declarative channel strip
<Rack orientation="column" title="Channel 1">
<FlatKnob label="Gain" sublabel="dB" />
<Fader orientation="horizontal" fill valuePosition="end" />
<Meter value={level} min={-60} max={6} fill orientation="horizontal" />
</Rack>`}
/>
<h3 className="api-h">Precision &amp; number handling</h3>

View file

@ -5,10 +5,13 @@ import {
DreamknobProvider,
Fader,
FlatKnob,
Gauge,
ImageKnob,
Knob,
KnobValue,
IndicatorLamp,
LabeledField,
LampRow,
LEDFader,
LEDKnob,
MetalKnob,
@ -17,12 +20,15 @@ import {
NeonKnob,
Pointer,
PushButton,
Rack,
RubberKnob,
ScrubField,
SegmentSwitch,
SegmentDisplay,
SteppedKnob,
Ticks,
ToggleSwitch,
TransportButton,
VintageKnob,
XYPad,
logTaper,
@ -197,6 +203,95 @@ const CommitModeDemo: React.FC = () => {
);
};
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;
@ -429,6 +524,18 @@ export const Gallery: React.FC = () => (
<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

View file

@ -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.1.0/dreamknob-1.1.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.1/dreamknob-1.1.1.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.1.0 release
v1.1.1 release
</a>{' '}
on our Forgejo. Zero runtime dependencies, ESM + CJS, full TypeScript types. No
CSS file to import everything is SVG and inline styles.

View file

@ -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.1.0/dreamknob-1.1.0.tgz</span>
<span>pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.1/dreamknob-1.1.1.tgz</span>
<button
onClick={() => {
navigator.clipboard.writeText('pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.0/dreamknob-1.1.0.tgz');
navigator.clipboard.writeText('pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.1/dreamknob-1.1.1.tgz');
setCopied(true);
setTimeout(() => setCopied(false), 1400);
}}