DreamKnob/packages/dreamknob/src/skins/RubberKnob.tsx
Dreamodus 5b97091902 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.
2026-07-13 19:20:23 -07:00

116 lines
3.2 KiB
TypeScript

import * as React from 'react';
import { Knob } from '../components/Knob';
import { Arc } from '../primitives/Arc';
import { GlowFilter } from '../primitives/GlowFilter';
import { Pointer } from '../primitives/Pointer';
import { KnobLabel, KnobValue } from '../primitives/Text';
import { useKnobTheme } from '../core/theme';
import type { SkinProps } from './shared';
export interface RubberKnobProps extends SkinProps {
/** Glow/accent color. */
color?: string;
trackColor?: string;
textColor?: string;
labelColor?: string;
arcFrom?: 'min' | 'center';
}
/** Soft-touch rubber knob with a glowing halo — modern synth hardware. */
export const RubberKnob = React.forwardRef<HTMLDivElement, RubberKnobProps>(function RubberKnob({
size = 90,
color: colorProp,
trackColor,
textColor,
labelColor,
arcFrom = 'min',
label,
sublabel,
labelSize,
showValue = false,
unit,
format,
focusRing,
className,
style,
...core
}, ref) {
const theme = useKnobTheme();
const color = colorProp ?? theme.accent ?? '#ff9640';
const id = React.useId();
const bodyId = `${id}-body`;
const glowId = `${id}-glow`;
const c = size / 2;
const bodyR = size / 2 - size * 0.17;
return (
<Knob ref={ref} size={size} className={className} style={style} focusRing={focusRing} {...core} getAriaValueText={core.getAriaValueText ?? (format ? (v: number) => format(v) : unit ? (v: number) => `${v}${unit}` : undefined)}>
<defs>
<radialGradient id={bodyId} cx="0.38" cy="0.3" r="1">
<stop offset="0" stopColor="#3a3b41" />
<stop offset="0.55" stopColor="#232428" />
<stop offset="1" stopColor="#0e0f12" />
</radialGradient>
<GlowFilter id={glowId} blur={size * 0.02} />
</defs>
<Arc
radius={size / 2 - size * 0.05}
thickness={Math.max(3, size * 0.045)}
color={color}
trackColor={trackColor ?? theme.track}
from={arcFrom}
arcProps={{ filter: `url(#${glowId})` }}
/>
<circle cx={c} cy={c} r={bodyR} fill={`url(#${bodyId})`} stroke="#000" strokeWidth="1" />
{/* rubber grip */}
<circle
cx={c}
cy={c}
r={bodyR - size * 0.03}
fill="none"
stroke="rgba(0,0,0,0.5)"
strokeWidth={size * 0.035}
strokeDasharray={`${size * 0.02} ${size * 0.03}`}
/>
{/* top sheen */}
<ellipse
cx={c}
cy={c - bodyR * 0.45}
rx={bodyR * 0.6}
ry={bodyR * 0.28}
fill="rgba(255,255,255,0.07)"
/>
<Pointer
radius={bodyR - size * 0.045}
length={bodyR * 0.5}
width={Math.max(2.5, size * 0.04)}
color={color}
shapeProps={{ filter: `url(#${glowId})` }}
/>
{showValue && (
<KnobValue
color={textColor ?? theme.text}
unit={unit}
format={format}
fontSize={size * 0.15}
fontFamily={theme.fontMono}
dy={size * 0.02}
/>
)}
{label && (
<KnobLabel
color={labelColor ?? theme.label}
fontFamily={theme.fontUI}
dy={size * 0.44}
fontSize={labelSize ?? size * 0.1}
sublabel={sublabel}
>
{label}
</KnobLabel>
)}
</Knob>
);
});