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

@ -10,7 +10,11 @@ export interface ArcProps {
color?: string;
/** Background track color. Omit to skip the track. */
trackColor?: string;
/** Draw the value arc from the minimum ('min') or from the travel center ('center', pan-style). */
/**
* Draw the value arc from the minimum ('min') or from the anchor ('center',
* pan-style). The anchor is the knob's `origin` value when set, otherwise
* the travel midpoint.
*/
from?: 'min' | 'center';
cap?: 'butt' | 'round';
/** Extra SVG props for the value arc path (e.g. filter for glow). */
@ -29,16 +33,17 @@ export const Arc: React.FC<ArcProps> = ({
arcProps,
opacity,
}) => {
const { size, center, normalized, angleOffset, angleRange } = useKnobContext();
const { size, center, normalized, originNormalized, angleOffset, angleRange } =
useKnobContext();
const r = radius ?? (size - thickness) / 2;
let start: number;
let end: number;
if (from === 'center') {
const mid = angleOffset + angleRange / 2;
const anchor = angleOffset + (originNormalized ?? 0.5) * angleRange;
const now = angleOffset + normalized * angleRange;
start = Math.min(mid, now);
end = Math.max(mid, now);
start = Math.min(anchor, now);
end = Math.max(anchor, now);
} else {
start = angleOffset;
end = angleOffset + normalized * angleRange;