v1.1.0: integration-feedback release
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.
This commit is contained in:
parent
b3eaa604c2
commit
d8b7b55505
31 changed files with 1471 additions and 223 deletions
124
packages/dreamknob/src/skins/MeterBridge.tsx
Normal file
124
packages/dreamknob/src/skins/MeterBridge.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue