diff --git a/README.md b/README.md
index 546f3ec..04cf677 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@ See `packages/dreamknob/README.md` and the docs app for the full API.
The easiest way is the published release on our Forgejo:
```bash
-pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.1/dreamknob-1.1.1.tgz
+pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.2/dreamknob-1.1.2.tgz
```
Or build locally — the package builds to `packages/dreamknob/dist` (ESM + CJS + types):
diff --git a/apps/docs/src/App.tsx b/apps/docs/src/App.tsx
index 09bca6d..1d02c03 100644
--- a/apps/docs/src/App.tsx
+++ b/apps/docs/src/App.tsx
@@ -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 = () => (
Start
Gallery
+
Buttons
Synth
Playground
Guide
@@ -36,6 +38,7 @@ export const App: React.FC = () => (
+
diff --git a/apps/docs/src/sections/ApiDocs.tsx b/apps/docs/src/sections/ApiDocs.tsx
index 2d4f086..ac44fa2 100644
--- a/apps/docs/src/sections/ApiDocs.tsx
+++ b/apps/docs/src/sections/ApiDocs.tsx
@@ -129,13 +129,14 @@ export const ApiDocs: React.FC = () => (
|
-
|
+
|
-
|
-
|
+
|
+
|
+
|
diff --git a/apps/docs/src/sections/Buttons.tsx b/apps/docs/src/sections/Buttons.tsx
new file mode 100644
index 0000000..02676f4
--- /dev/null
+++ b/apps/docs/src/sections/Buttons.tsx
@@ -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 }) => (
+
+ {children}
+
+);
+
+/** 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 (
+
+ );
+};
+
+const GuideRow: React.FC<{ code: string; when: string; eg: string }> = ({ code, when, eg }) => (
+
+ {code}
+ {when}
+ {eg}
+
+);
+
+const Demo: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => (
+
+);
diff --git a/apps/docs/src/styles.css b/apps/docs/src/styles.css
index 1ea24c2..b17d254 100644
--- a/apps/docs/src/styles.css
+++ b/apps/docs/src/styles.css
@@ -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;
diff --git a/packages/dreamknob/CHANGELOG.md b/packages/dreamknob/CHANGELOG.md
index eb9e2ab..07ab660 100644
--- a/packages/dreamknob/CHANGELOG.md
+++ b/packages/dreamknob/CHANGELOG.md
@@ -1,5 +1,37 @@
# Changelog
+## 1.1.2 — 2026-07-14
+
+Third integration-feedback release — a general action button, responsive
+faders everywhere, and the label-scaling gap, all surfaced building a
+resizable streaming studio.
+
+### New components
+- `Button` — a normal **click-to-fire** command button in the panel style
+ (Regenerate, Add, Apply, Refresh), the non-latching sibling of `PushButton`.
+ It's `PushButton` locked to the new `mode="action"`: never latches, shows a
+ press-down + a brief LED flash, calls `onClick`. `led="dot"` (default) gives
+ the corner-dot look; `led={false}` a plain button.
+
+### Buttons
+- `PushButton` gains **`mode="action"`** (alongside `toggle`/`momentary`) and
+ an `onClick` prop — the stateless command-button behavior `Button` wraps.
+
+### Responsive faders
+- **`fill`** on `LEDFader` (matching `Fader`) — the fader stretches along its
+ travel axis to fill the container instead of a fixed `length`, so faders in
+ resizable dock panels size themselves with no wrapper measurement.
+
+### Label scaling
+- **`labelSize`** on `IndicatorLamp` — the caption otherwise tracks `size` but
+ is floored at 10px; set this to shrink it with a small lamp (e.g. matching a
+ dense header row). Also on `LampRow` for parity.
+
+### Docs
+- A dedicated **Buttons & switches** section with a "which do I use?" guide
+ (Button = action · PushButton = latch · TransportButton = transport ·
+ ToggleSwitch = boolean · SegmentSwitch = mode) and a live demo of each.
+
## 1.1.1 — 2026-07-13
Second integration-feedback release — the friction that showed up once the
diff --git a/packages/dreamknob/README.md b/packages/dreamknob/README.md
index 4687401..0a03e50 100644
--- a/packages/dreamknob/README.md
+++ b/packages/dreamknob/README.md
@@ -5,7 +5,7 @@ headless core, any number range, any decimal precision, and colors you fully con
Zero runtime dependencies.
```
-pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.1/dreamknob-1.1.1.tgz
+pnpm add https://git.dreamodus.software/Dreamodus/DreamKnob/releases/download/v1.1.2/dreamknob-1.1.2.tgz
```
(Latest release tarballs: https://git.dreamodus.software/Dreamodus/DreamKnob/releases)
diff --git a/packages/dreamknob/package.json b/packages/dreamknob/package.json
index be13c56..b587886 100644
--- a/packages/dreamknob/package.json
+++ b/packages/dreamknob/package.json
@@ -1,6 +1,6 @@
{
"name": "dreamknob",
- "version": "1.1.1",
+ "version": "1.1.2",
"description": "A feature-full, studio-grade React knob & fader library. Rotary, linear, 2D, 3D, digital and analog styles with a headless core.",
"license": "MIT",
"author": "Dreamodus Software Inc.",
diff --git a/packages/dreamknob/src/index.ts b/packages/dreamknob/src/index.ts
index 7f58d1c..f030813 100644
--- a/packages/dreamknob/src/index.ts
+++ b/packages/dreamknob/src/index.ts
@@ -70,7 +70,7 @@ export { ImageKnob, type ImageKnobProps } from './skins/ImageKnob';
export { Meter, type MeterProps } from './skins/Meter';
export { MeterBridge, type MeterBridgeProps, type MeterBridgeChannel } from './skins/MeterBridge';
export { Gauge, type GaugeProps } from './skins/Gauge';
-export { PushButton, type PushButtonProps } from './skins/PushButton';
+export { PushButton, type PushButtonProps, Button, type ButtonProps } from './skins/PushButton';
export { IndicatorLamp, type IndicatorLampProps } from './skins/IndicatorLamp';
export { LampRow, type LampRowProps, type LampRowItem } from './skins/LampRow';
export { ToggleSwitch, type ToggleSwitchProps } from './skins/ToggleSwitch';
diff --git a/packages/dreamknob/src/skins/IndicatorLamp.tsx b/packages/dreamknob/src/skins/IndicatorLamp.tsx
index 8154410..f02f48c 100644
--- a/packages/dreamknob/src/skins/IndicatorLamp.tsx
+++ b/packages/dreamknob/src/skins/IndicatorLamp.tsx
@@ -19,6 +19,12 @@ export interface IndicatorLampProps {
readOnly?: boolean;
/** Caption beside the lamp. */
label?: string;
+ /**
+ * Caption font size in px, independent of the lamp diameter. Without it the
+ * label tracks `size` but is floored at 10px; set this to shrink the caption
+ * with a small lamp (e.g. matching a dense header row's height).
+ */
+ labelSize?: number;
labelPosition?: 'right' | 'left';
disabled?: boolean;
/** Render a hidden form input carrying "on"/"off". */
@@ -45,6 +51,7 @@ export const IndicatorLamp = React.forwardRef
{
orientation?: 'vertical' | 'horizontal';
/** Travel length in px. Default: 160. */
length?: number;
+ /**
+ * Stretch along the travel axis to fill the container (horizontal fills
+ * width, vertical fills height) instead of a fixed `length` — the same
+ * responsive behavior as `Fader`, so faders in resizable dock panels size
+ * themselves instead of needing wrapper measurement.
+ */
+ fill?: boolean;
/** Cross-axis size in px. Default: 34. */
breadth?: number;
/** Number of LED segments. Default: 24. */
@@ -57,6 +64,7 @@ export interface LEDFaderProps extends Omit {
export const LEDFader = React.forwardRef(function LEDFader({
orientation = 'vertical',
length = 160,
+ fill = false,
breadth = 34,
segments = 24,
color: colorProp,
@@ -95,6 +103,12 @@ export const LEDFader = React.forwardRef(function
const w = vertical ? breadth : length;
const h = vertical ? length : breadth;
+ // Fill stretches the travel axis (via preserveAspectRatio='none'); the cross
+ // axis stays at `breadth`. The LED geometry is computed in the fixed viewBox
+ // and scaled to the element — pointer mapping measures the real element, so
+ // interaction stays correct at any rendered width.
+ const ctrlW = fill && !vertical ? '100%' : w;
+ const ctrlH = fill && vertical ? '100%' : h;
const zoneList: readonly LEDFaderZone[] = React.useMemo(() => {
if (zones) return [...zones].sort((a, b) => a.upTo - b.upTo);
if (color) return [{ upTo: 1, color }];
@@ -157,7 +171,8 @@ export const LEDFader = React.forwardRef(function
flexDirection: 'column',
alignItems: 'center',
gap: 6,
- width: w,
+ width: fill && !vertical ? '100%' : w,
+ height: fill && vertical ? '100%' : undefined,
opacity: core.disabled ? 0.45 : undefined,
...style,
}}
@@ -218,14 +233,21 @@ export const LEDFader = React.forwardRef(function
style={{
...knob.bind.style,
opacity: undefined, // the outer wrapper owns disabled dimming
- width: w,
- height: h,
- display: 'inline-flex',
+ width: ctrlW,
+ height: ctrlH,
+ display: fill ? 'flex' : 'inline-flex',
borderRadius: 6,
...(knob.isFocusVisible && ring ? { boxShadow: `0 0 0 2px ${ring}` } : undefined),
}}
>
-
+
= ({
lamps,
size = 9,
+ labelSize,
labelPosition = 'right',
onToggle,
gap = 14,
@@ -80,7 +83,7 @@ export const LampRow: React.FC = ({
void;
- /** 'toggle' latches; 'momentary' is only on while held. Default: 'toggle'. */
- mode?: 'toggle' | 'momentary';
+ /**
+ * 'toggle' latches; 'momentary' is on only while held; 'action' is a normal
+ * click-to-fire command button — it never latches, shows a press-down + a
+ * brief LED flash, and calls `onClick`. Default: 'toggle'.
+ */
+ mode?: 'toggle' | 'momentary' | 'action';
+ /** Fired on activation in `action` mode (click, Enter, or Space). */
+ onClick?: () => void;
/** LED / active color. */
color?: string;
/**
@@ -39,6 +45,7 @@ export const PushButton = React.forwardRef(
pressed,
defaultPressed = false,
onChange,
+ onClick,
mode = 'toggle',
color,
led = true,
@@ -59,16 +66,42 @@ export const PushButton = React.forwardRef(
const isControlled = pressed !== undefined;
const [internal, setInternal] = React.useState(defaultPressed);
const [focusVisible, setFocusVisible] = React.useState(false);
- const on = isControlled ? (pressed as boolean) : internal;
+ // Action mode is stateless: a transient `held` (pointer down) drives the
+ // press-down look and a short `flash` lights the LED on activation.
+ const [held, setHeld] = React.useState(false);
+ const [flash, setFlash] = React.useState(false);
+ const flashTimer = React.useRef>();
+ React.useEffect(() => () => clearTimeout(flashTimer.current), []);
+ const latched = isControlled ? (pressed as boolean) : internal;
+ const on = mode === 'action' ? held : latched; // press-down / background inset
+ const ledOn = mode === 'action' ? held || flash : latched;
const set = (next: boolean) => {
- if (next === on) return;
+ if (next === latched) return;
if (!isControlled) setInternal(next);
onChange?.(next);
};
+ const fire = () => {
+ onClick?.();
+ setFlash(true);
+ clearTimeout(flashTimer.current);
+ flashTimer.current = setTimeout(() => setFlash(false), 160);
+ };
+
const momentaryProps =
- mode === 'momentary'
+ mode === 'action'
+ ? {
+ onClick: fire,
+ onPointerDown: (e: React.PointerEvent) => {
+ e.currentTarget.setPointerCapture(e.pointerId);
+ setHeld(true);
+ },
+ onPointerUp: () => setHeld(false),
+ onPointerCancel: () => setHeld(false),
+ onPointerLeave: () => setHeld(false),
+ }
+ : mode === 'momentary'
? {
onPointerDown: (e: React.PointerEvent) => {
e.currentTarget.setPointerCapture(e.pointerId);
@@ -83,14 +116,14 @@ export const PushButton = React.forwardRef(
if (e.key === ' ' || e.key === 'Enter') set(false);
},
}
- : { onClick: () => set(!on) };
+ : { onClick: () => set(!latched) };
return (
(
width: '55%',
height: Math.max(3, size * 0.09),
borderRadius: 3,
- background: on ? accent : 'rgba(255,255,255,0.09)',
- boxShadow: on ? `0 0 ${size * 0.22}px ${accent}` : 'inset 0 1px 1px rgba(0,0,0,0.6)',
+ background: ledOn ? accent : 'rgba(255,255,255,0.09)',
+ boxShadow: ledOn ? `0 0 ${size * 0.22}px ${accent}` : 'inset 0 1px 1px rgba(0,0,0,0.6)',
transition: 'background 80ms, box-shadow 80ms',
}}
/>
@@ -164,8 +197,8 @@ export const PushButton = React.forwardRef(
width: Math.max(4, size * 0.14),
height: Math.max(4, size * 0.14),
borderRadius: '50%',
- background: on ? accent : 'rgba(255,255,255,0.09)',
- boxShadow: on ? `0 0 ${size * 0.25}px ${accent}` : 'inset 0 1px 1px rgba(0,0,0,0.6)',
+ background: ledOn ? accent : 'rgba(255,255,255,0.09)',
+ boxShadow: ledOn ? `0 0 ${size * 0.25}px ${accent}` : 'inset 0 1px 1px rgba(0,0,0,0.6)',
transition: 'background 80ms, box-shadow 80ms',
}}
/>
@@ -177,7 +210,7 @@ export const PushButton = React.forwardRef(
aria-hidden="true"
style={{
display: 'inline-flex',
- color: on ? accent : theme.label,
+ color: ledOn ? accent : theme.label,
transition: 'color 80ms',
}}
>
@@ -187,8 +220,26 @@ export const PushButton = React.forwardRef(
{children}
)}
- {name && }
+ {name && }
);
},
);
+
+export interface ButtonProps
+ extends Omit {
+ onClick?: () => void;
+}
+
+/**
+ * A normal click-to-fire command button in the studio-panel style — the
+ * non-latching sibling of `PushButton`. Use it for actions (Regenerate,
+ * Add, Apply, Refresh). It's `PushButton` locked to `mode="action"`; pass
+ * `led="dot"` for the corner-dot look (like a Fresh Seed button) or
+ * `led={false}` for a plain button.
+ */
+export const Button = React.forwardRef(
+ function Button({ led = 'dot', ...rest }, ref) {
+ return ;
+ },
+);