Tier 3: bubbles, animation, wrap mode, XYPad, AlphaDisplay, PushButton
- valueBubble/bubbleFormat: floating readout above knobs while dragging
(Knob-based skins + ImageKnob)
- animateChanges: rAF ease-out tween of the pointer on programmatic
value changes; user gestures snap; prefers-reduced-motion disables
- wrap: endless encoder mode — values and relative drags roll around
min↔max; pairs with angleOffset={0} angleRange={360}
- XYPad: two-parameter pad with absolute drag, arrow keys (Shift
coarse), Escape cancel, double-click reset, grid/crosshair, focus
ring, hidden form inputs, change meta
- AlphaDisplay: fourteen-segment alphanumeric LED (A-Z, 0-9, symbols,
decimal points, chars padding/align, glow/skew)
- PushButton: studio panel button with LED strip — toggle or momentary,
controlled/uncontrolled, aria-pressed, hidden form input
- Docs: five new gallery cards incl. animated preset demo, API rows,
README
This commit is contained in:
parent
0aee48b749
commit
e9e52e43ed
12 changed files with 936 additions and 8 deletions
142
packages/dreamknob/src/skins/PushButton.tsx
Normal file
142
packages/dreamknob/src/skins/PushButton.tsx
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import * as React from 'react';
|
||||
import { useKnobTheme } from '../core/theme';
|
||||
|
||||
export interface PushButtonProps {
|
||||
/** Controlled pressed state. Pair with `onChange`. */
|
||||
pressed?: boolean;
|
||||
defaultPressed?: boolean;
|
||||
onChange?: (pressed: boolean) => void;
|
||||
/** 'toggle' latches; 'momentary' is only on while held. Default: 'toggle'. */
|
||||
mode?: 'toggle' | 'momentary';
|
||||
/** LED / active color. */
|
||||
color?: string;
|
||||
/** Show the LED strip. Default: true. */
|
||||
led?: boolean;
|
||||
/** Button height in px. Default: 36. */
|
||||
size?: number;
|
||||
/** Explicit width; defaults to content. */
|
||||
width?: number;
|
||||
disabled?: boolean;
|
||||
/** Render a hidden form input carrying "on"/"off". */
|
||||
name?: string;
|
||||
/** Caption, e.g. "MUTE". */
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
'aria-label'?: string;
|
||||
}
|
||||
|
||||
/** Studio panel button with an LED strip — mute/solo/bypass in matching style. */
|
||||
export const PushButton = React.forwardRef<HTMLButtonElement, PushButtonProps>(
|
||||
function PushButton(
|
||||
{
|
||||
pressed,
|
||||
defaultPressed = false,
|
||||
onChange,
|
||||
mode = 'toggle',
|
||||
color,
|
||||
led = true,
|
||||
size = 36,
|
||||
width,
|
||||
disabled = false,
|
||||
name,
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
...aria
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const theme = useKnobTheme();
|
||||
const accent = color ?? theme.accent ?? '#4cc2ff';
|
||||
const isControlled = pressed !== undefined;
|
||||
const [internal, setInternal] = React.useState(defaultPressed);
|
||||
const on = isControlled ? (pressed as boolean) : internal;
|
||||
|
||||
const set = (next: boolean) => {
|
||||
if (next === on) return;
|
||||
if (!isControlled) setInternal(next);
|
||||
onChange?.(next);
|
||||
};
|
||||
|
||||
const momentaryProps =
|
||||
mode === 'momentary'
|
||||
? {
|
||||
onPointerDown: (e: React.PointerEvent) => {
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
set(true);
|
||||
},
|
||||
onPointerUp: () => set(false),
|
||||
onPointerCancel: () => set(false),
|
||||
onKeyDown: (e: React.KeyboardEvent) => {
|
||||
if ((e.key === ' ' || e.key === 'Enter') && !e.repeat) set(true);
|
||||
},
|
||||
onKeyUp: (e: React.KeyboardEvent) => {
|
||||
if (e.key === ' ' || e.key === 'Enter') set(false);
|
||||
},
|
||||
}
|
||||
: { onClick: () => set(!on) };
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
aria-pressed={on}
|
||||
aria-label={aria['aria-label']}
|
||||
data-pressed={on ? '' : undefined}
|
||||
className={className}
|
||||
{...momentaryProps}
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 3,
|
||||
height: size,
|
||||
width,
|
||||
minWidth: size * 1.4,
|
||||
padding: '0 12px',
|
||||
borderRadius: 7,
|
||||
border: '1px solid rgba(0,0,0,0.7)',
|
||||
background: on
|
||||
? 'linear-gradient(180deg, #17181d, #232429)'
|
||||
: 'linear-gradient(180deg, #35363d, #1d1e24)',
|
||||
boxShadow: on
|
||||
? 'inset 0 2px 5px rgba(0,0,0,0.65)'
|
||||
: 'inset 0 1px 0 rgba(255,255,255,0.09), 0 2px 4px rgba(0,0,0,0.45)',
|
||||
color: on ? theme.text : theme.label,
|
||||
fontFamily: theme.fontUI,
|
||||
fontSize: Math.max(10, size * 0.28),
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase',
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
opacity: disabled ? 0.45 : 1,
|
||||
userSelect: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
touchAction: 'manipulation',
|
||||
transition: 'background 80ms, box-shadow 80ms, color 80ms',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{led && (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
width: '55%',
|
||||
height: Math.max(3, size * 0.09),
|
||||
borderRadius: 3,
|
||||
background: on ? accent : 'rgba(255,255,255,0.09)',
|
||||
boxShadow: on ? `0 0 ${size * 0.22}px ${accent}` : 'inset 0 1px 1px rgba(0,0,0,0.6)',
|
||||
transition: 'background 80ms, box-shadow 80ms',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
{name && <input type="hidden" name={name} value={on ? 'on' : 'off'} readOnly />}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue