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

@ -3,7 +3,8 @@ import { Knob } from '../components/Knob';
import { Pointer } from '../primitives/Pointer';
import { Ticks } from '../primitives/Ticks';
import { KnobLabel } from '../primitives/Text';
import { MONO_FONT, UI_FONT, type SkinProps } from './shared';
import { useKnobTheme } from '../core/theme';
import type { SkinProps } from './shared';
export interface SteppedKnobProps extends SkinProps {
/** Position names, e.g. ['LP','BP','HP']. Sets the number of detents. */
@ -22,23 +23,35 @@ export const SteppedKnob: React.FC<SteppedKnobProps> = ({
size = 90,
positions,
steps,
color = '#ffd23e',
tickColor = 'rgba(255,255,255,0.18)',
color: colorProp,
tickColor,
faceColor = '#1c1d22',
textColor = 'rgba(255,255,255,0.92)',
labelColor = 'rgba(255,255,255,0.45)',
textColor,
labelColor,
label,
showValue = true,
focusRing,
className,
style,
...core
}) => {
const theme = useKnobTheme();
const color = colorProp ?? theme.accent ?? '#ffd23e';
const detents = positions ? positions.length : steps ?? 5;
const min = core.min ?? 0;
const max = core.max ?? (positions ? positions.length - 1 : 100);
return (
<Knob size={size} className={className} style={style} {...core} min={min} max={max} steps={detents}>
<Knob
size={size}
className={className}
style={style}
focusRing={focusRing}
{...core}
min={min}
max={max}
steps={detents}
>
{ctx => {
const index = Math.round(ctx.normalized * (detents - 1));
return (
@ -48,7 +61,7 @@ export const SteppedKnob: React.FC<SteppedKnobProps> = ({
radius={size / 2 - 1}
length={size * 0.07}
width={Math.max(1.5, size * 0.022)}
color={tickColor}
color={tickColor ?? theme.ticks}
getTickProps={i => (i === index ? { stroke: color } : undefined)}
/>
<circle
@ -73,8 +86,8 @@ export const SteppedKnob: React.FC<SteppedKnobProps> = ({
textAnchor="middle"
dominantBaseline="central"
fontSize={size * (positions ? 0.15 : 0.17)}
fill={textColor}
fontFamily={MONO_FONT}
fill={textColor ?? theme.text}
fontFamily={theme.fontMono}
fontWeight={600}
style={{ pointerEvents: 'none' }}
>
@ -82,7 +95,12 @@ export const SteppedKnob: React.FC<SteppedKnobProps> = ({
</text>
)}
{label && (
<KnobLabel color={labelColor} fontFamily={UI_FONT} dy={size * 0.45} fontSize={size * 0.095}>
<KnobLabel
color={labelColor ?? theme.label}
fontFamily={theme.fontUI}
dy={size * 0.45}
fontSize={size * 0.095}
>
{label}
</KnobLabel>
)}