v1.1.2: action button, responsive faders, label scaling

Third integration-feedback release from building the streaming studio.

- New `Button` — a click-to-fire command button in the panel style, the
  non-latching sibling of PushButton. Implemented as PushButton's new
  `mode="action"` (never latches, press-down + brief LED flash, `onClick`);
  led="dot" (default) matches the corner-dot look, led={false} is plain.
- `fill` on LEDFader (parity with Fader) — faders stretch to fill their
  container in resizable panels instead of a fixed pixel length.
- `labelSize` on IndicatorLamp (and LampRow) — shrink the caption below its
  10px floor to match dense header rows.
- Docs: a dedicated "Buttons & switches" section with a decision guide
  (Button/PushButton/TransportButton/ToggleSwitch/SegmentSwitch) + live demos.

Library + docs typecheck and build; 38 tests pass.
This commit is contained in:
Dreamodus 2026-07-14 06:11:26 -07:00
parent 89c7a7f603
commit a6348d5c46
13 changed files with 372 additions and 27 deletions

View file

@ -1,6 +1,7 @@
import React from 'react';
import { AdvancedGuide } from './sections/AdvancedGuide';
import { ApiDocs } from './sections/ApiDocs';
import { Buttons } from './sections/Buttons';
import { Console } from './sections/Console';
import { Gallery } from './sections/Gallery';
import { GettingStarted } from './sections/GettingStarted';
@ -26,6 +27,7 @@ export const App: React.FC = () => (
<div className="nav-links">
<a href="#start">Start</a>
<a href="#gallery">Gallery</a>
<a href="#buttons">Buttons</a>
<a href="#synth">Synth</a>
<a href="#playground">Playground</a>
<a href="#guide">Guide</a>
@ -36,6 +38,7 @@ export const App: React.FC = () => (
<Hero />
<GettingStarted />
<Gallery />
<Buttons />
<Console />
<Synth />
<Playground />

View file

@ -129,13 +129,14 @@ export const ApiDocs: React.FC = () => (
<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'), 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="<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, fill (stretch to container)." />
<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, ...]), 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' | '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="<Button />" type="action" desc="Click-to-fire command button in the panel style (Regenerate, Add, Apply). PushButton locked to mode='action' — never latches, press-down + LED flash, onClick. led: 'dot' (default) | 'strip' | false." />
<Row name="<PushButton />" type="switch" desc="Panel button: mode 'toggle' | 'momentary' | 'action', 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 (labelSize to shrink the caption below its 10px floor) — 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 (now incl. : $ %), chars padding, align, weight, gap, glow, skew. Defaults to theme.ledAmber." />

View file

@ -0,0 +1,156 @@
import React from 'react';
import {
Button,
PushButton,
TransportButton,
ToggleSwitch,
SegmentSwitch,
} from 'dreamknob';
/** Small glyph for a button's leadingIcon (the docs app has no icon dep). */
const Glyph: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<span aria-hidden="true" style={{ fontSize: 14, lineHeight: 1 }}>
{children}
</span>
);
/** The button/switch family, side by side, with a "which do I use?" guide. */
export const Buttons: React.FC = () => {
const [playing, setPlaying] = React.useState(false);
const [armed, setArmed] = React.useState(false);
const [bypass, setBypass] = React.useState(false);
const [fresh, setFresh] = React.useState(true);
const [mode, setMode] = React.useState(0);
const [fired, setFired] = React.useState(0);
return (
<section className="block" id="buttons">
<div className="container">
<div className="section-kicker">Buttons &amp; switches</div>
<h2 className="section-title">Press, latch, or pick</h2>
<p className="section-sub">
Five components cover every panel control that isn&apos;t a dial. The
only question is what the press <em>means</em>:
</p>
<div className="guide-grid">
<GuideRow
code="<Button>"
when="Fire a one-off action"
eg="Regenerate · Add · Apply · Refresh"
/>
<GuideRow
code="<PushButton>"
when="Latch a boolean state (or hold it)"
eg="Mute · Solo · Bypass · push-to-talk"
/>
<GuideRow
code="<TransportButton>"
when="Transport / deck control"
eg="Play · Stop · Record · Panic"
/>
<GuideRow
code="<ToggleSwitch>"
when="An on/off setting that reads as a switch"
eg="Fresh seed · Monitor · Loop"
/>
<GuideRow
code="<SegmentSwitch>"
when="Pick one of a few modes"
eg="Linear · Radial · Conic"
/>
</div>
<div className="btn-demo">
<Demo label={`<Button> — action, fired ${fired}×`}>
<Button
color="#4cc2ff"
leadingIcon={<Glyph></Glyph>}
onClick={() => setFired(n => n + 1)}
aria-label="Regenerate"
>
Regenerate
</Button>
<Button
color="#3df2ad"
led="dot"
leadingIcon={<Glyph></Glyph>}
onClick={() => setFired(n => n + 1)}
aria-label="Fresh"
>
Fresh
</Button>
<Button
color="#ff4d6b"
led={false}
leadingIcon={<Glyph></Glyph>}
onClick={() => setFired(n => n + 1)}
aria-label="Clear"
>
Clear
</Button>
</Demo>
<Demo label="<PushButton> — latch / hold">
<PushButton
pressed={bypass}
onChange={setBypass}
color="#ffd23e"
aria-label="Bypass"
>
Bypass
</PushButton>
<PushButton
mode="momentary"
color="#4cc2ff"
led="dot"
size={30}
aria-label="Talk"
>
Talk
</PushButton>
</Demo>
<Demo label="<TransportButton>">
<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} />
</Demo>
<Demo label="<ToggleSwitch> · <SegmentSwitch>">
<ToggleSwitch on={fresh} onChange={setFresh} color="#c9a6ff" showStateLabel aria-label="Fresh seed" />
<SegmentSwitch
options={['LINEAR', 'RADIAL', 'CONIC']}
value={mode}
onChange={setMode}
color="#4cc2ff"
aria-label="Gradient kind"
/>
</Demo>
</div>
<p className="section-sub" style={{ marginTop: 20 }}>
<code>&lt;Button&gt;</code> is <code>&lt;PushButton mode=&quot;action&quot;&gt;</code>
it never latches, shows a press-down and a brief LED flash, and calls{' '}
<code>onClick</code>. <code>led=&quot;dot&quot;</code> gives the corner-dot
look; <code>led=&#123;false&#125;</code> a plain button.
</p>
</div>
</section>
);
};
const GuideRow: React.FC<{ code: string; when: string; eg: string }> = ({ code, when, eg }) => (
<div className="guide-row">
<code className="guide-code">{code}</code>
<span className="guide-when">{when}</span>
<span className="guide-eg">{eg}</span>
</div>
);
const Demo: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => (
<div className="btn-demo-cell">
<div className="btn-demo-label">{label}</div>
<div className="btn-demo-row">{children}</div>
</div>
);

View file

@ -353,6 +353,76 @@ h2.section-title {
text-transform: uppercase;
}
/* Buttons section: decision guide + live demo grid */
.guide-grid {
display: flex;
flex-direction: column;
gap: 1px;
margin: 8px 0 28px;
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--line);
background: var(--line);
}
.guide-row {
display: grid;
grid-template-columns: 190px 1fr;
gap: 4px 18px;
align-items: baseline;
padding: 12px 16px;
background: #101219;
}
.guide-code {
font-family: var(--font-mono);
font-size: 13px;
color: #4cc2ff;
}
.guide-when {
font-size: 14px;
color: var(--text);
}
.guide-eg {
grid-column: 2;
font-size: 12.5px;
color: var(--text-dim);
}
.btn-demo {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 12px;
}
.btn-demo-cell {
display: flex;
flex-direction: column;
gap: 14px;
padding: 18px;
border-radius: 12px;
background: linear-gradient(180deg, #16181f, #0e0f14);
border: 1px solid var(--line-strong);
}
.btn-demo-label {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.08em;
color: var(--text-dim);
text-transform: uppercase;
}
.btn-demo-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
min-height: 40px;
}
@media (max-width: 560px) {
.guide-row {
grid-template-columns: 1fr;
}
.guide-eg {
grid-column: 1;
}
}
/* ---- synth panel ---- */
.synth-rows {
display: flex;