Tier 2: drag feel, wheel overhaul, refs/meta/forms, Meter, gradients
- rotaryMode: 'absolute' | 'relative' (angle deltas, no grab jump) |
'pickup' (MIDI-style: engages when the pointer sweeps past the value)
- dragAcceleration: velocity gain for relative drags (Shift bypasses);
hideCursorOnDrag hides the cursor mid-gesture
- Wheel: trackpad delta accumulation (flicks can't rocket the value,
one notch per wheel click), wheelStep override, wheelRequiresFocus
opt-out of the hover scroll-trap
- onChange/onChangeStart/onChangeEnd now receive { source: 'drag' |
'wheel' | 'keyboard' | 'reset' | 'api' }
- Every component forwards a ref to its root; name prop renders a
hidden form input; SVG primitives carry data-part attributes
- Arc gradient: position-anchored color stops rendered as sliced arcs
(mixColors/sampleGradient exported, unit-tested)
- New read-only Meter: LED level meter with peak hold, zones, readout
- Fader capColor/capLength for custom handle styling
- Docs: Meter/gradient/drag-feel gallery cards, white-cap fader demo,
API rows, README
This commit is contained in:
parent
804a85cca6
commit
0aee48b749
27 changed files with 756 additions and 134 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from 'react';
|
||||
import { useKnobContext } from '../core/context';
|
||||
import { describeArc } from '../core/math';
|
||||
import { sampleGradient, type GradientStop } from '../core/colors';
|
||||
|
||||
export interface ArcProps {
|
||||
/** Arc radius. Defaults to (size - thickness) / 2. */
|
||||
|
|
@ -8,6 +9,13 @@ export interface ArcProps {
|
|||
thickness?: number;
|
||||
/** Value arc color. */
|
||||
color?: string;
|
||||
/**
|
||||
* Position-anchored gradient for the value arc, e.g. green→yellow→red:
|
||||
* `[{at: 0, color: '#3df2ad'}, {at: 0.8, color: '#ffd23e'}, {at: 1, color: '#ff4d6b'}]`.
|
||||
* Colors stay fixed in space (the red zone is always at the top of travel).
|
||||
* Overrides `color`.
|
||||
*/
|
||||
gradient?: readonly GradientStop[];
|
||||
/** Background track color. Omit to skip the track. */
|
||||
trackColor?: string;
|
||||
/**
|
||||
|
|
@ -27,6 +35,7 @@ export const Arc: React.FC<ArcProps> = ({
|
|||
radius,
|
||||
thickness = 4,
|
||||
color = 'currentColor',
|
||||
gradient,
|
||||
trackColor,
|
||||
from = 'min',
|
||||
cap = 'round',
|
||||
|
|
@ -49,19 +58,39 @@ export const Arc: React.FC<ArcProps> = ({
|
|||
end = angleOffset + normalized * angleRange;
|
||||
}
|
||||
|
||||
return (
|
||||
<g opacity={opacity}>
|
||||
{trackColor && (
|
||||
<path
|
||||
d={describeArc(center, center, r, angleOffset, angleOffset + angleRange)}
|
||||
stroke={trackColor}
|
||||
strokeWidth={thickness}
|
||||
strokeLinecap={cap}
|
||||
fill="none"
|
||||
/>
|
||||
)}
|
||||
{end - start > 0.0001 && (
|
||||
let valueArc: React.ReactNode = null;
|
||||
if (end - start > 0.0001) {
|
||||
if (gradient && gradient.length > 0) {
|
||||
// SVG can't run a gradient along a path, so approximate with short
|
||||
// butt-capped slices sampled from the gradient at their absolute
|
||||
// position across the travel.
|
||||
const sweep = end - start;
|
||||
const slices = Math.max(2, Math.ceil(sweep / 4));
|
||||
const parts: React.ReactNode[] = [];
|
||||
for (let i = 0; i < slices; i++) {
|
||||
const a0 = start + (i / slices) * sweep;
|
||||
const a1 = start + ((i + 1) / slices) * sweep + (i < slices - 1 ? 0.35 : 0);
|
||||
const mid = (a0 + a1) / 2;
|
||||
parts.push(
|
||||
<path
|
||||
key={i}
|
||||
d={describeArc(center, center, r, a0, a1)}
|
||||
stroke={sampleGradient(gradient, (mid - angleOffset) / angleRange)}
|
||||
strokeWidth={thickness}
|
||||
strokeLinecap="butt"
|
||||
fill="none"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
valueArc = (
|
||||
<g data-part="arc" {...(arcProps as React.SVGProps<SVGGElement>)}>
|
||||
{parts}
|
||||
</g>
|
||||
);
|
||||
} else {
|
||||
valueArc = (
|
||||
<path
|
||||
data-part="arc"
|
||||
d={describeArc(center, center, r, start, end)}
|
||||
stroke={color}
|
||||
strokeWidth={thickness}
|
||||
|
|
@ -69,7 +98,23 @@ export const Arc: React.FC<ArcProps> = ({
|
|||
fill="none"
|
||||
{...arcProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<g opacity={opacity}>
|
||||
{trackColor && (
|
||||
<path
|
||||
data-part="track"
|
||||
d={describeArc(center, center, r, angleOffset, angleOffset + angleRange)}
|
||||
stroke={trackColor}
|
||||
strokeWidth={thickness}
|
||||
strokeLinecap={cap}
|
||||
fill="none"
|
||||
/>
|
||||
)}
|
||||
{valueArc}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,5 +9,7 @@ export interface FaceProps extends React.SVGProps<SVGCircleElement> {
|
|||
/** The knob body: a centered circle. Style via fill/stroke/filter props. */
|
||||
export const Face: React.FC<FaceProps> = ({ radius, ...rest }) => {
|
||||
const { size, center } = useKnobContext();
|
||||
return <circle cx={center} cy={center} r={radius ?? size / 2 - 6} {...rest} />;
|
||||
return (
|
||||
<circle data-part="face" cx={center} cy={center} r={radius ?? size / 2 - 6} {...rest} />
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -69,5 +69,9 @@ export const Pointer: React.FC<PointerProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
return <g transform={`rotate(${angle} ${center} ${center})`}>{shape}</g>;
|
||||
return (
|
||||
<g data-part="pointer" transform={`rotate(${angle} ${center} ${center})`}>
|
||||
{shape}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export const KnobValue: React.FC<KnobValueProps> = ({
|
|||
}${unit ?? ''}`;
|
||||
return (
|
||||
<text
|
||||
data-part="value"
|
||||
x={ctx.center}
|
||||
y={ctx.center + dy}
|
||||
textAnchor="middle"
|
||||
|
|
@ -80,6 +81,7 @@ export const KnobLabel: React.FC<KnobLabelProps> = ({
|
|||
const { size, center } = useKnobContext();
|
||||
return (
|
||||
<text
|
||||
data-part="label"
|
||||
x={center}
|
||||
y={center + (dy ?? size * 0.38)}
|
||||
textAnchor="middle"
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const TickLabels: React.FC<TickLabelsProps> = ({
|
|||
const { size, center: c, angleOffset, angleRange } = useKnobContext();
|
||||
const r = radius ?? size / 2 - size * 0.02;
|
||||
return (
|
||||
<g>
|
||||
<g data-part="tick-labels">
|
||||
{labels.map((text, i) => {
|
||||
if (!text) return null;
|
||||
const t = labels.length === 1 ? 0 : i / (labels.length - 1);
|
||||
|
|
|
|||
|
|
@ -51,5 +51,5 @@ export const Ticks: React.FC<TicksProps> = ({
|
|||
/>,
|
||||
);
|
||||
}
|
||||
return <g>{ticks}</g>;
|
||||
return <g data-part="ticks">{ticks}</g>;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue