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 type and
box are two folders over one tag, read as params.type.size
and params.box.radius. Folders nest as deep as you'd ever want.
const tag = target.querySelector(".fld-tag");
const panel = tweaks("Folder", {
label: "Folder",
type: { // nested object → folder
size: { type: "slider", value: 15, min: 10, max: 40, step: 1, unit: "px" },
tracking: { type: "slider", value: 0, min: -2, max: 8, step: 1, unit: "px" },
caps: false,
},
box: { // a second folder beside the first
padding: { type: "slider", value: 12, min: 4, max: 40, step: 1, unit: "px" },
radius: { type: "slider", value: 10, min: 0, max: 40, step: 1, unit: "px" },
},
});
mount.append(panel.el);
const apply = (p) => {
tag.textContent = p.label;
tag.style.fontSize = `${p.type.size}px`;
tag.style.letterSpacing = `${p.type.tracking}px`;
tag.style.textTransform = p.type.caps ? "uppercase" : "none";
tag.style.padding = `${p.box.padding}px ${Math.round(p.box.padding * 1.6)}px`;
tag.style.borderRadius = `${p.box.radius}px`;
};
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 lazilyButton, 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);