Tweakit

Structure

Past a handful of controls, a flat panel stops scanning. Folders and tabs group; buttons and separators punctuate. Folders, buttons, button groups and separators are built in; tabs load lazily.

Folder

Any nested plain object becomes a collapsible folder, and its children land on params as a nested object — here the whole shadow folder composes one box-shadow. Folders nest as deep as you'd ever want.

Stacked
const card = target.querySelector(".fld-card");
const panel = tweaks("Folder", {
  label: "Stacked",
  shadow: {                    // nested object → folder
    x: [0, -40, 40, 1],
    y: [12, -40, 40, 1],
    blur: [32, 0, 90, 1],
    alpha: [0.5, 0, 1, 0.01],
  },
});
mount.append(panel.el);

const apply = (p) => {
  card.textContent = p.label;
  card.style.boxShadow = `${p.shadow.x}px ${p.shadow.y}px ${p.shadow.blur}px rgba(0, 0, 0, ${p.shadow.alpha})`;
};
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).

Tabs

{ type: "tabs", pages: { … } } splits a panel into pages — each page is just another schema. Params nest by page: params.look.fill.color, params.look.stroke.width.

const shape = target.querySelector(".tab-shape path");
const panel = tweaks("Tabs", {
  look: { type: "tabs", pages: {
    Fill: { color: "#7C5CFF", opacity: [1, 0, 1, 0.01] },
    Stroke: { color: "#888888", width: [2, 0, 14, 1], dashed: false },
  } },
});
mount.append(panel.el);

const apply = (p) => {
  shape.setAttribute("fill", p.look.fill.color);
  shape.setAttribute("fill-opacity", p.look.fill.opacity);
  shape.setAttribute("stroke", p.look.stroke.color);
  shape.setAttribute("stroke-width", p.look.stroke.width);
  shape.setAttribute("stroke-dasharray", p.look.stroke.dashed ? "10 8" : "0");
};
panel.on(apply);
panel.ready.then(() => apply(panel.params)); // tabs (and color) load lazily

Button, button group & separator

A bare { action: fn } is a button; buttongroup packs several into one row; { type: "separator" } draws the line between concerns. Buttons don't produce params — they just fire.

const orbit = target.querySelector(".act-orbit");
const planet = target.querySelector(".act-planet");
const panel = tweaks("Actions", {
  pulse: { type: "button", label: "Pulse", action: () => {
    planet.classList.remove("act-pulse");
    requestAnimationFrame(() => planet.classList.add("act-pulse"));
  } },
  playback: { type: "buttongroup", buttons: {
    Play: () => { orbit.style.animationPlayState = "running"; },
    Pause: () => { orbit.style.animationPlayState = "paused"; },
  } },
  line: { type: "separator" },
  seconds: [6, 1, 12, 0.5],
});
mount.append(panel.el);

const apply = (p) => { orbit.style.animationDuration = `${p.seconds}s`; };
panel.on(apply);
apply(panel.params);