# Numbers

> Tweakit's numeric controls, live: slider, number field, dual-handle interval, and the 2D point pad.

The page: https://ryankiley.github.io/tweakit/numbers.html. Every page as Markdown: https://ryankiley.github.io/tweakit/llms.txt

Four ways to edit a number, or two, or four of them at once. Sliders are built in; `number`, `interval` and `point` load on first use on the code-split build.

## Slider

The workhorse. `[value, min, max, step]` as shorthand, or `{ type: "slider" }` for options. Drag anywhere on the track, or scrub with the arrow keys (⇧ for coarse steps). To type a value, hover the readout and click it, or press Enter on the focused track. With six or fewer stops the track snaps and shows rule lines. `soft: true` lets a typed or scripted value run past `min`/`max`; dragging stays within them. `unit: "px"` puts a unit after the readout; the param stays the bare number. For a readout of your own, see the next example.

```js
const tile = target.querySelector(".num-tile");
const panel = tweaks("Slider", {
  radius: { type: "slider", value: 16, min: 0, max: 66, step: 1, unit: "px" },   // continuous: smooth square → circle; reads "16 px"
  size: [4, 2, 6, 1],           // ≤6 stops: snaps, shows rule lines
});
mount.append(panel.el);

const apply = (p) => {
  tile.style.borderRadius = `${p.radius}px`;
  tile.style.transform = `scale(${0.55 + p.size * 0.11})`;
};
panel.on(apply);
apply(panel.params);
```

`mount` is the panel's slot inside the stage; `target` is the demo surface it controls. In your own page you'd just `document.body.append(panel.el)`.

## Formatted readout

`format` draws a slider's readout from its value, in place of its digits, and `parse` reads a typed readout back, so the inline editor shows and takes the same text. Here the value is minutes of the day and the readout is a clock. Like `unit`, it is display only: the param stays the number. A format that throws falls back to the digits. When `parse` can't read the text, have it return `NaN`, and the slider keeps its value.

```js
const face = target.querySelector(".num-clock");
// Minutes of the day as a clock, and a typed clock back to minutes. Text that
// isn't a clock like the readout's is NaN, which leaves the slider where it was.
const clock = (m) => {
  const h = Math.floor(m / 60) % 24;
  return (h % 12 || 12) + ":" + String(m % 60).padStart(2, "0") + (h < 12 ? " AM" : " PM");
};
const unclock = (text) => {
  const [hm, ap] = text.trim().toUpperCase().split(" ");
  const [h, m] = hm.split(":").map(Number);
  if (!(h >= 1 && h <= 12 && m >= 0 && m < 60) || (ap !== "AM" && ap !== "PM")) return NaN;
  return ((h % 12) + (ap === "PM" ? 12 : 0)) * 60 + m;
};
const panel = tweaks("Formatted", {
  time: { type: "slider", value: 450, min: 0, max: 1425, step: 15, format: clock, parse: unclock },
});
mount.append(panel.el);

const apply = (p) => { face.textContent = clock(p.time); };
panel.on(apply);
apply(panel.params);
```

## Number

`{ type: "number" }` is a plain numeric field with a drag-to-scrub grab handle, for values where a track makes no sense. Drag the handle to scrub it (⇧ ×10, ⌥ ×0.1), click the field to type, or step it with ↑/↓. `min`/`max` clamp; omit them for unbounded, or add `soft: true` to keep the bounds advisory rather than clamping. `unit` works here too.

```js
const card = target.querySelector(".num-rotor-card");
const panel = tweaks("Number", {
  angle: { type: "number", value: 12, min: -180, max: 180, step: 1, unit: "°" },
});
mount.append(panel.el);

const apply = (p) => {
  card.style.transform = `rotate(${p.angle}deg)`;
  card.textContent = `${p.angle}°`;
};
panel.on(apply);
panel.ready.then(() => apply(panel.params)); // number loads lazily
```

## Interval

A dual-handle range. Shorthand `[[lo, hi], min, max, step?]`: the first entry being a 2-tuple is what marks it as an interval. The value on `params` is a `[lo, hi]` pair; here it gates which bars count as “in range”.

```js
const strip = target.querySelector(".num-bars");
const heights = Array.from({ length: 28 }, (_, i) => 12 + 84 * Math.abs(Math.sin(i * 0.6 + 1)));
const bars = heights.map((h) => {
  const b = document.createElement("div");
  b.className = "num-bar";
  b.style.height = `${h}%`;
  strip.append(b);
  return b;
});
const panel = tweaks("Interval", {
  band: [[30, 70], 0, 100, 1],   // [[lo, hi], min, max, step]
});
mount.append(panel.el);

const apply = (p) => bars.forEach((b, i) =>
  b.classList.toggle("num-bar-hot", heights[i] >= p.band[0] && heights[i] <= p.band[1]));
panel.on(apply);
panel.ready.then(() => apply(panel.params)); // interval loads lazily
```

## Box sides

`{ type: "sides" }` is one row for a four-sided length: padding, margin, border width, or with `corners: true` a border radius. Linked, it is one number for all four; the button beside it opens the row in place, four fields in a line under it in CSS order, each under a glyph for its side or corner. The param is the four numbers plus the shorthand they collapse to, so `el.style.borderRadius = p.radius.css` is the whole wiring. Author it as one number, `[vertical, horizontal]`, the four, or a CSS string like `"1rem 2rem"`, whose unit the control keeps unless `unit` says otherwise.

```js
const card = target.querySelector(".sd-card");
const inner = target.querySelector(".sd-inner");
const panel = tweaks("Box", {
  radius: { type: "sides", value: 16, corners: true, max: 48 },
});
mount.append(panel.el);

const apply = (p) => {
  card.style.borderRadius = p.radius.css;
  // The inner card stays concentric: each corner 12px (the padding) less.
  const less = (n) => Math.max(0, n - 12);
  inner.style.borderRadius = p.radius.css.replace(/[\d.]+/g, less);
};
panel.on(apply);
panel.ready.then(() => apply(panel.params));
```

## Point

An n-dimensional vector: one scrubbable field per component, plus a draggable 2D pad for the first two (on by default; `pad: false` opts out). The value is a plain map of component keys. Add a third component for 3D, a fourth for 4D; the pad stays 2D, the fields stack. The pad points +Y up; `invertY: true` flips it for screen-space coordinates.

```js
const dot = target.querySelector(".num-dot");
const panel = tweaks("Point", {
  offset: { type: "point", components: [
    { key: "x", label: "X", value: 0, min: -1, max: 1, step: 0.01 },
    { key: "y", label: "Y", value: 0, min: -1, max: 1, step: 0.01 },
  ] },
});
mount.append(panel.el);

const apply = (p) => {
  dot.style.left = `${50 + p.offset.x * 45}%`;
  dot.style.top = `${50 + p.offset.y * 45}%`;
};
panel.on(apply);
panel.ready.then(() => apply(panel.params)); // point loads lazily
```
