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

@ -0,0 +1,134 @@
import * as React from 'react';
import { useKnobTheme } from '../core/theme';
export interface LampRowItem {
label: string;
on: boolean;
color?: string;
}
export interface LampRowProps {
lamps: readonly LampRowItem[];
/** Lamp diameter in px. Default: 9. */
size?: number;
/** Caption placement relative to each lamp. Default: 'right'. */
labelPosition?: 'right' | 'below';
/** Make lamps clickable (a compact indicator toggle bank). */
onToggle?: (index: number, on: boolean) => void;
/** Spacing between lamp cells in px. Default: 14. */
gap?: number;
disabled?: boolean;
className?: string;
style?: React.CSSProperties;
'aria-label'?: string;
}
/**
* A row of small indicator lamps with console-print captions the
* `● CUE ● MUTE` bank hardware panels use, standardized. Read-only status by
* default; pass `onToggle` to make the lamps pressable.
*/
export const LampRow: React.FC<LampRowProps> = ({
lamps,
size = 9,
labelPosition = 'right',
onToggle,
gap = 14,
disabled = false,
className,
style,
...aria
}) => {
const theme = useKnobTheme();
const below = labelPosition === 'below';
const interactive = !!onToggle;
return (
<div
className={className}
role="group"
aria-label={aria['aria-label']}
data-disabled={disabled ? '' : undefined}
style={{
display: 'inline-flex',
alignItems: 'center',
gap,
opacity: disabled ? 0.45 : 1,
...style,
}}
>
{lamps.map((lamp, i) => {
const accent = lamp.color ?? theme.accent ?? theme.zoneGood;
const dot = (
<span
aria-hidden="true"
style={{
width: size,
height: size,
flex: 'none',
borderRadius: '50%',
background: lamp.on ? accent : 'rgba(255,255,255,0.1)',
border: '1px solid rgba(0,0,0,0.5)',
boxShadow: lamp.on
? `0 0 ${size * 0.8}px ${accent}, inset 0 0 ${size * 0.25}px rgba(255,255,255,0.5)`
: 'inset 0 1px 2px rgba(0,0,0,0.6)',
transition: 'background 80ms, box-shadow 80ms',
}}
/>
);
const caption = (
<span
style={{
fontFamily: theme.fontUI,
fontSize: 9.5,
fontWeight: 600,
letterSpacing: '0.12em',
textTransform: 'uppercase',
color: lamp.on ? theme.text : theme.label,
whiteSpace: 'nowrap',
transition: 'color 80ms',
}}
>
{lamp.label}
</span>
);
const inner = (
<span
style={{
display: 'inline-flex',
flexDirection: below ? 'column' : 'row',
alignItems: 'center',
gap: below ? 4 : 6,
}}
>
{dot}
{caption}
</span>
);
return interactive ? (
<button
key={i}
type="button"
role="switch"
aria-checked={lamp.on}
aria-label={lamp.label}
disabled={disabled}
onClick={() => onToggle!(i, !lamp.on)}
style={{
background: 'none',
border: 'none',
padding: 0,
cursor: disabled ? 'not-allowed' : 'pointer',
font: 'inherit',
}}
>
{inner}
</button>
) : (
<React.Fragment key={i}>{inner}</React.Fragment>
);
})}
</div>
);
};