Zum Inhalt

src/ui/MatrixCell.tsx

import { type Component, createSignal } from 'solid-js';

interface Props {
  value: number;
  tint: string; // CSS color used to tint a positive (attraction) cell
  onChange: (value: number) => void;
}

const MIN = -2;
const MAX = 2;
const RANGE = MAX - MIN;

function clamp(v: number): number {
  return Math.max(MIN, Math.min(MAX, Math.round(v / 0.05) * 0.05));
}

export const MatrixCell: Component<Props> = (props) => {
  const [dragging, setDragging] = createSignal(false);

  const onPointerDown = (e: PointerEvent) => {
    e.preventDefault();
    const el = e.currentTarget as HTMLElement;
    el.setPointerCapture(e.pointerId);
    setDragging(true);
    const startY = e.clientY;
    const startVal = props.value;
    const onMove = (ev: PointerEvent) => {
      const dy = startY - ev.clientY;
      props.onChange(clamp(startVal + (dy / 200) * RANGE));
    };
    const onUp = (ev: PointerEvent) => {
      el.releasePointerCapture(ev.pointerId);
      setDragging(false);
      el.removeEventListener('pointermove', onMove);
      el.removeEventListener('pointerup', onUp);
    };
    el.addEventListener('pointermove', onMove);
    el.addEventListener('pointerup', onUp);
  };

  const onKeyDown = (e: KeyboardEvent) => {
    if (e.key === 'ArrowUp') {
      props.onChange(clamp(props.value + 0.1));
      e.preventDefault();
    } else if (e.key === 'ArrowDown') {
      props.onChange(clamp(props.value - 0.1));
      e.preventDefault();
    }
  };

  const intensity = () => Math.min(1, Math.abs(props.value) / 2);
  const bg = () =>
    props.value >= 0
      ? `color-mix(in oklab, ${props.tint} ${intensity() * 30}%, var(--surface-inset))`
      : `color-mix(in oklab, var(--signal-crimson) ${intensity() * 30}%, var(--surface-inset))`;

  return (
    <div
      role="slider"
      tabIndex={0}
      aria-valuenow={props.value}
      aria-valuemin={MIN}
      aria-valuemax={MAX}
      class="phys-num"
      onPointerDown={onPointerDown}
      onKeyDown={onKeyDown}
      style={{
        height: '34px',
        display: 'flex',
        'align-items': 'center',
        'justify-content': 'center',
        background: bg(),
        border: `1px solid ${dragging() ? 'var(--accent)' : 'var(--border-subtle)'}`,
        'border-radius': 'var(--radius-md)',
        'font-size': '11px',
        color: 'var(--fg-primary)',
        cursor: 'ns-resize',
        'user-select': 'none',
        'touch-action': 'none',
      }}
    >
      {props.value >= 0 ? '+' : ''}
      {props.value.toFixed(2)}
    </div>
  );
};