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.
137 lines
3.7 KiB
TypeScript
137 lines
3.7 KiB
TypeScript
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 font size in px. Default: 9.5. */
|
|
labelSize?: 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,
|
|
labelSize,
|
|
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: labelSize ?? 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>
|
|
);
|
|
};
|