Tweakit v0.7.2

Numbers

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.

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.

7:30 AM
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.

12°
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”.

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.

Card
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.

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