Bugs from real-product integration: - Meter/LEDFader clamp geometry below breadth 14 (no negative SVG rects) - mixColors falls back to CSS color-mix() so theme tokens can be var()/oklch() - type-in editing fires a real edit gesture (Start/onChange/End, source 'edit') - ScrubField Enter/Escape no longer double-commit or commit-on-revert via blur - disabled dims the whole control uniformly, data-disabled on the wrapper New capabilities: - commitMode 'release': drags preview, emit once on pointer-up, Esc discards - Meter accepts value=[l, r, ...]; theme zoneGood/zoneWarn/zoneHot defaults - XYPad per-axis invert + detents; Fader valuePosition top|end|none - LED skins: unit labels, uncapped displayDecimals, aria-valuetext defaults New components: IndicatorLamp, SegmentSwitch, ScrubField, MeterBridge, PushButton led='dot'. Docs: change-contract table, recipes, gallery cards.
124 lines
3.5 KiB
TypeScript
124 lines
3.5 KiB
TypeScript
import * as React from 'react';
|
|
import { useKnobTheme } from '../core/theme';
|
|
import { Meter, type MeterProps } from './Meter';
|
|
|
|
export interface MeterBridgeChannel {
|
|
label: string;
|
|
value: number;
|
|
/** Peak signal for clip detection, when different from `value`. */
|
|
clipValue?: number;
|
|
}
|
|
|
|
export interface MeterBridgeProps
|
|
extends Omit<MeterProps, 'value' | 'clipValue' | 'label' | 'showValue' | 'aria-label'> {
|
|
channels: readonly MeterBridgeChannel[];
|
|
/** Numeric peak-hold text above each bar. Default: true. */
|
|
showPeakText?: boolean;
|
|
/** Decimals for the peak text. Default: 1. */
|
|
peakTextDecimals?: number;
|
|
/** Block caption under the bridge. */
|
|
label?: string;
|
|
'aria-label'?: string;
|
|
}
|
|
|
|
/**
|
|
* A labeled multi-strip meter block — n channels, per-channel peak text, one
|
|
* shared clip LED. The composition every mixer rebuilds, prebuilt.
|
|
*/
|
|
export const MeterBridge: React.FC<MeterBridgeProps> = ({
|
|
channels,
|
|
showPeakText = true,
|
|
peakTextDecimals = 1,
|
|
label,
|
|
min = 0,
|
|
max = 100,
|
|
breadth = 16,
|
|
peakHold = 1200,
|
|
className,
|
|
style,
|
|
...meterProps
|
|
}) => {
|
|
const theme = useKnobTheme();
|
|
const values = channels.map(c => c.value);
|
|
const clipValues = channels.map(c => c.clipValue ?? c.value);
|
|
const chGap = channels.length > 1 ? 3 : 0;
|
|
|
|
// Numeric peak-hold per channel for the text row.
|
|
const [peaks, setPeaks] = React.useState<readonly number[]>(values);
|
|
React.useEffect(() => {
|
|
if (peakHold === false) return;
|
|
setPeaks(prev =>
|
|
prev.length === values.length ? values.map((v, i) => Math.max(v, prev[i] ?? -Infinity)) : values,
|
|
);
|
|
const t = setTimeout(() => setPeaks(values), peakHold);
|
|
return () => clearTimeout(t);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [JSON.stringify(values), peakHold]);
|
|
|
|
const cell: React.CSSProperties = {
|
|
width: breadth,
|
|
textAlign: 'center',
|
|
fontFamily: theme.fontMono,
|
|
whiteSpace: 'nowrap',
|
|
overflow: 'visible',
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={className}
|
|
role="group"
|
|
aria-label={meterProps['aria-label' as never] ?? label}
|
|
style={{ display: 'inline-flex', flexDirection: 'column', alignItems: 'center', gap: 4, ...style }}
|
|
>
|
|
{showPeakText && (
|
|
<div style={{ display: 'flex', gap: chGap }}>
|
|
{peaks.map((p, i) => (
|
|
<span key={i} style={{ ...cell, fontSize: 9, color: theme.label }}>
|
|
{Math.max(p, min) <= min ? '—' : Math.min(p, max).toFixed(peakTextDecimals)}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
<Meter
|
|
{...meterProps}
|
|
min={min}
|
|
max={max}
|
|
breadth={breadth}
|
|
peakHold={peakHold}
|
|
value={values}
|
|
clipValue={clipValues}
|
|
aria-label={undefined}
|
|
/>
|
|
<div style={{ display: 'flex', gap: chGap }}>
|
|
{channels.map(c => (
|
|
<span
|
|
key={c.label}
|
|
style={{
|
|
...cell,
|
|
fontSize: 9.5,
|
|
fontFamily: theme.fontUI,
|
|
letterSpacing: '0.05em',
|
|
textTransform: 'uppercase',
|
|
color: theme.label,
|
|
}}
|
|
>
|
|
{c.label}
|
|
</span>
|
|
))}
|
|
</div>
|
|
{label && (
|
|
<span
|
|
style={{
|
|
fontFamily: theme.fontUI,
|
|
fontSize: 11,
|
|
letterSpacing: '0.08em',
|
|
textTransform: 'uppercase',
|
|
color: theme.label,
|
|
}}
|
|
>
|
|
{label}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|