components_hue_picker.js

import { hslToHex } from "../utilities/tools.js"

/**
* @param {object} options
* @param {function(string): void} options.onSelect  - called with hex color on each hue change
* @param {function(): void}       options.onCancel  - called when picker is dismissed without selection
*/
export class HuePicker {
  /**
   * @param {object} options - configuration callbacks for the hue picker
   * @param {function(string): void} options.onSelect  - called with hex color on each hue change
   * @param {function(): void}       options.onCancel  - called when picker is dismissed without selection
   */
  constructor({ onSelect, onCancel }) {
    this.onSelect = onSelect;
    this.onCancel = onCancel;
    this.el = null;
    this._picking = false;
    this._onMouseMove = (e) => { if (this._picking) this.#applyHue(e); };
    this._onMouseUp   = () => { this._picking = false; };
  }

  /**
   * Renders and positions the picker popover anchored below `anchorEl`.
   * @param {HTMLElement} anchorEl   - element to anchor below (e.g. a color swatch)
   * @param {number}      initialHue - hue (0–360) to pre-position the cursor at
   */
  open(anchorEl, initialHue = 0) {
    this.remove();

    const picker = document.createElement('div');
    picker.className = 'hue-picker';
    this.el = picker;

    const label = document.createElement('div');
    label.className = 'hue-picker-label';
    label.textContent = 'Speaker color';
    picker.appendChild(label);

    const stripWrap = document.createElement('div');
    stripWrap.className = 'hue-strip-wrap';

    const canvas = document.createElement('canvas');
    canvas.width = 256; canvas.height = 14;
    const ctx = canvas.getContext('2d');
    const grad = ctx.createLinearGradient(0, 0, 256, 0);
    for (let i = 0; i <= 12; i++) grad.addColorStop(i / 12, `hsl(${i * 30},100%,60%)`);
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, 256, 14);
    stripWrap.appendChild(canvas);

    const cursor = document.createElement('div');
    cursor.className = 'hue-cursor';
    cursor.style.left = (initialHue / 360 * 100) + '%';
    stripWrap.appendChild(cursor);
    this._cursor = cursor;
    this._stripWrap = stripWrap;

    stripWrap.addEventListener('mousedown', (e) => {
      this._picking = true;
      this.#applyHue(e);
      e.stopPropagation();
    });
    window.addEventListener('mousemove', this._onMouseMove);
    window.addEventListener('mouseup', this._onMouseUp);

    picker.appendChild(stripWrap);
    document.body.appendChild(picker);

    // Position popover below the anchor, clamped to viewport edges
    const sr = anchorEl.getBoundingClientRect();
    let top  = sr.bottom + 6;
    let left = sr.left;
    if (left + 180 > window.innerWidth)  left = window.innerWidth - 188;
    if (top  + 80  > window.innerHeight) top  = sr.top - 80;
    picker.style.top  = top  + 'px';
    picker.style.left = left + 'px';

    // Close on outside click (deferred so the opening mousedown doesn't immediately trigger it)
    setTimeout(() => {
      document.addEventListener('mousedown', this._outsideHandler = (ev) => {
        if (!picker.contains(ev.target) && ev.target !== anchorEl) {
          this.onCancel?.();
          this.remove();
        }
      });
    }, 0);
  }

  /**
   * Reads mouse position over the strip, computes a hue, and calls onSelect.
   * @param {MouseEvent} e - the mouse event used to determine cursor position on the strip
   */
  #applyHue(e) {
    const rect = this._stripWrap.getBoundingClientRect();
    const frac = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
    const hue  = Math.round(frac * 360);
    this._cursor.style.left = (frac * 100) + '%';
    this.onSelect?.(hslToHex(hue, 100, 60));
  }

  /** Removes the picker from the DOM and cleans up all listeners. */
  remove() {
    if (!this.el) return;
    this.el.remove();
    this.el = null;
    window.removeEventListener('mousemove', this._onMouseMove);
    window.removeEventListener('mouseup', this._onMouseUp);
    if (this._outsideHandler) {
      document.removeEventListener('mousedown', this._outsideHandler);
      this._outsideHandler = null;
    }
  }
}