src/solar-system/blocks/scale-block.js

// src/solar-system/blocks/scale-block.js
//
// SCALE block — four sliders: body size, distance, exaggeration, lunarOrbit.
//
// mountScaleBlock(target, ctx) → teardown
// - target: an HTMLElement that will receive a `.section-block` child
// - ctx:    panelCtx (must expose `onScaleChange` and either
//           `scaleSetters.setScale` or be permissive — falls back to the
//           module-local setter)
// - returns a teardown function that removes subscribers and clears the target
//
// The block is responsible for its own DOM, listeners, and teardown. Each
// slider is a 0-100 range mapped to the underlying scaleState domain
// ([0, 1] for size/distance, [0, 2] for exaggeration); the block reads
// scaleState directly on mount and re-syncs on every onScaleChange so a
// voyage that drives setScale(...) keeps the sliders in lock-step.

import { t, onLangChange } from '../../i18n.js';
import { scaleState, setScale, onScaleChange } from '../state/scale.js';

const SLIDERS = [
  { axis: 'size', i18n: 'solarSystem.blocks.scale.size', range: 100 },
  { axis: 'distance', i18n: 'solarSystem.blocks.scale.distance', range: 100 },
  // Exaggeration domain is [0, 2]; the slider stays 0-200 so each tick
  // is one centi-unit of exaggeration (matches the legacy slider mapping).
  { axis: 'exaggeration', i18n: 'solarSystem.blocks.scale.exaggeration', range: 200 },
  // Lunar-orbit domain is [0, 2] (slider 0-200), decoupled from `distance`:
  // 0 = compressed moon orbits (default), 1 = true proportional, 2 = beyond.
  { axis: 'lunarOrbit', i18n: 'solarSystem.blocks.scale.lunarOrbit', range: 200 },
];

export function mountScaleBlock(target, ctx = {}) {
  const setScaleFn =
    ctx?.scaleSetters?.setScale ??
    ((delta, opts) => setScale(delta, { duration: 0, source: 'panel', ...opts }));
  const subscribe = ctx?.onScaleChange ?? onScaleChange;

  const root = document.createElement('div');
  root.classList.add('section-block', 'scale-block');

  const header = document.createElement('h3');
  header.classList.add('section-block__header');
  header.setAttribute('data-i18n', 'solarSystem.blocks.scale.title');
  header.textContent = t('solarSystem.blocks.scale.title') || 'ÉCHELLE';

  const body = document.createElement('div');
  body.classList.add('section-block__body', 'scale-block__body');

  const sliderWraps = []; // { axis, label, input, range }
  for (const s of SLIDERS) {
    const wrap = document.createElement('div');
    wrap.classList.add('scale-block__slider');
    wrap.dataset.axis = s.axis;

    const label = document.createElement('label');
    label.classList.add('scale-block__label');
    label.setAttribute('data-i18n', s.i18n);
    label.textContent = t(s.i18n);

    const input = document.createElement('input');
    input.type = 'range';
    input.min = '0';
    input.max = String(s.range);
    input.step = '1';
    input.dataset.axis = s.axis;
    input.value = String(Math.round((scaleState.global[s.axis] ?? 0) * 100));

    input.addEventListener('input', () => {
      const value = Number(input.value) / 100;
      setScaleFn({ [s.axis]: value }, { duration: 0, source: 'panel' });
    });

    wrap.appendChild(label);
    wrap.appendChild(input);
    body.appendChild(wrap);
    sliderWraps.push({ axis: s.axis, label, input, i18n: s.i18n });
  }

  root.append(header, body);
  target.appendChild(root);

  function refresh() {
    for (const sw of sliderWraps) {
      const next = String(Math.round((scaleState.global[sw.axis] ?? 0) * 100));
      // Don't override the slider while the user is dragging it (mirrors
      // dev-panel's _scaleCard pattern).
      if (document.activeElement !== sw.input && sw.input.value !== next) {
        sw.input.value = next;
      }
    }
  }

  const offScale = subscribe(refresh);
  const offLang = onLangChange(() => {
    header.textContent = t('solarSystem.blocks.scale.title') || 'ÉCHELLE';
    for (const sw of sliderWraps) sw.label.textContent = t(sw.i18n);
  });

  return () => {
    try {
      offScale?.();
    } catch {
      // best-effort
    }
    try {
      offLang?.();
    } catch {
      // best-effort
    }
    target.replaceChildren();
  };
}
← Astrarium