Tier 1: origin anchor, detents, theming, type-in editing, ImageKnob

- origin: bipolar anchor value — Arc from=center, Fader fill and
  LEDFader segments draw from origin instead of min/mid-travel
- detents + detentSize: magnetic snap while dragging (normalized
  space, wheel/keyboard unaffected); applyDetents exported
- DreamknobProvider + KnobTheme: accent/track/face/text/label/ticks/
  focusRing/fonts tokens with dark and light bases; every skin
  resolves defaults from the theme, instance props win
- editable + parseValue: double-click knobs (or click fader readouts)
  to type exact values; Enter/blur commits, Esc cancels, 1.2k parses;
  ValueInput + defaultParseValue exported; setValue added to context
- ImageKnob: film-strip sprite knob (KnobMan-style strips), vertical
  or horizontal, with focus ring, editing, value/label
- Docs: LED gallery cards for film-strip/bipolar/editing/theming,
  playground editable toggle, theming API section, README updates
This commit is contained in:
Dreamodus 2026-07-12 15:05:46 -07:00
parent d8992408fa
commit 804a85cca6
25 changed files with 999 additions and 162 deletions

View file

@ -4,7 +4,8 @@ import { Arc } from '../primitives/Arc';
import { GlowFilter } from '../primitives/GlowFilter';
import { Pointer } from '../primitives/Pointer';
import { KnobLabel, KnobValue } from '../primitives/Text';
import { MONO_FONT, UI_FONT, type SkinProps } from './shared';
import { useKnobTheme } from '../core/theme';
import type { SkinProps } from './shared';
export interface RubberKnobProps extends SkinProps {
/** Glow/accent color. */
@ -18,19 +19,22 @@ export interface RubberKnobProps extends SkinProps {
/** Soft-touch rubber knob with a glowing halo — modern synth hardware. */
export const RubberKnob: React.FC<RubberKnobProps> = ({
size = 90,
color = '#ff9640',
trackColor = 'rgba(255,255,255,0.08)',
textColor = 'rgba(255,255,255,0.92)',
labelColor = 'rgba(255,255,255,0.45)',
color: colorProp,
trackColor,
textColor,
labelColor,
arcFrom = 'min',
label,
showValue = false,
unit,
format,
focusRing,
className,
style,
...core
}) => {
const theme = useKnobTheme();
const color = colorProp ?? theme.accent ?? '#ff9640';
const id = React.useId();
const bodyId = `${id}-body`;
const glowId = `${id}-glow`;
@ -38,7 +42,7 @@ export const RubberKnob: React.FC<RubberKnobProps> = ({
const bodyR = size / 2 - size * 0.17;
return (
<Knob size={size} className={className} style={style} {...core}>
<Knob size={size} className={className} style={style} focusRing={focusRing} {...core}>
<defs>
<radialGradient id={bodyId} cx="0.38" cy="0.3" r="1">
<stop offset="0" stopColor="#3a3b41" />
@ -52,7 +56,7 @@ export const RubberKnob: React.FC<RubberKnobProps> = ({
radius={size / 2 - size * 0.05}
thickness={Math.max(3, size * 0.045)}
color={color}
trackColor={trackColor}
trackColor={trackColor ?? theme.track}
from={arcFrom}
arcProps={{ filter: `url(#${glowId})` }}
/>
@ -86,16 +90,21 @@ export const RubberKnob: React.FC<RubberKnobProps> = ({
/>
{showValue && (
<KnobValue
color={textColor}
color={textColor ?? theme.text}
unit={unit}
format={format}
fontSize={size * 0.15}
fontFamily={MONO_FONT}
fontFamily={theme.fontMono}
dy={size * 0.02}
/>
)}
{label && (
<KnobLabel color={labelColor} fontFamily={UI_FONT} dy={size * 0.44} fontSize={size * 0.1}>
<KnobLabel
color={labelColor ?? theme.label}
fontFamily={theme.fontUI}
dy={size * 0.44}
fontSize={size * 0.1}
>
{label}
</KnobLabel>
)}