From 229f361702acfcd0f2f6b88ba341395488d51bb7 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 3 Oct 2026 23:36:26 +0300 Subject: [PATCH] =?UTF-8?q?feat(ui):=20Sampler=20quick-access=20popover=20?= =?UTF-8?q?over=20the=20timeline=20(same=20settings=20as=20Engine=20?= =?UTF-8?q?=E2=96=B8=20Sampling)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- app/ui/features/placeholders/early.js | 5 ---- app/ui/features/sampler/index.js | 39 +++++++++++++++++++++++++++ app/ui/modules.js | 2 +- 3 files changed, 40 insertions(+), 6 deletions(-) delete mode 100644 app/ui/features/placeholders/early.js create mode 100644 app/ui/features/sampler/index.js diff --git a/app/ui/features/placeholders/early.js b/app/ui/features/placeholders/early.js deleted file mode 100644 index 3eed2146..00000000 --- a/app/ui/features/placeholders/early.js +++ /dev/null @@ -1,5 +0,0 @@ -// Stand-ins that sit BEFORE a real feature in the same row (see index.js). -import { composer as c } from "../../composer/composer.js"; - -export default [{ id: "placeholder:sampler", mount: "timeline.status", - setup: ({ host }) => host.append(c.button.sm({ label: "⏱ Sampler", tone: "neutral", disabled: true }).node) }]; diff --git a/app/ui/features/sampler/index.js b/app/ui/features/sampler/index.js new file mode 100644 index 00000000..bafed0da --- /dev/null +++ b/app/ui/features/sampler/index.js @@ -0,0 +1,39 @@ +// "⏱ Sampler": steps, scheduler and sampler one click away, in a thin window over the timeline. Same settings as Engine ▸ Sampling, +// same values tree, so the two cannot disagree. Absent when no installed module has a sampling setting. +import { composer as c } from "../../composer/composer.js"; +import { settingRow, whenSatisfied } from "../settings/field.js"; + +export default { + id: "sampler", + mount: "timeline.status", + needs: ["pipeline"], + setup({ host, app }) { + const ps = app.pipeline; + let pop = null; + const sampling = () => ps.activeModules().filter((m) => (m.category || "") === "sampling" && Object.keys(m.settings || {}).length); + const button = c.button.sm({ label: "⏱ Sampler", tone: "neutral", title: "Steps, scheduler and sampler: one click, no modal.", onClick: () => (pop ? pop.close() : open()) }); + button.node.hidden = true; + host.append(button.node); + + const body = c.region.stack({ gap: "md" }); + const draw = () => { + if (ps.loading()) return body.set([c.hint.default({ text: "Loading…" })]); + if (ps.loadError()) return body.set([c.banner.warn({ text: `Could not load: ${ps.loadError()}` })]); + const mods = sampling(), values = ps.currentValues(); + const change = async (id, name, value) => { await ps.setModuleValue(id, name, value); draw(); }; + body.set(mods.flatMap((m) => [mods.length > 1 ? c.label.section({ text: m.title || m.id }) : null, + ...Object.entries(m.settings).filter(([, spec]) => whenSatisfied(values, m.id, spec.when)).map(([name, spec]) => settingRow(m.id, name, spec, values, change))]).filter(Boolean)); + }; + const typing = () => body.node.contains(document.activeElement) && /^(input|textarea)$/i.test(document.activeElement.tagName); + const off = ps.subscribe(() => { if (pop && !typing()) draw(); }); + const show = () => { button.node.hidden = !sampling().length; if (button.node.hidden && pop) pop.close(); }; + ps.ensureLoaded().then(show); + const offShow = ps.subscribe(show); + + function open() { + draw(); + pop = c.popover.anchored({ anchor: button, body, side: "top", align: "end", onClose: () => { pop = null; } }); + } + return () => { off(); offShow(); if (pop) pop.close(); button.node.remove(); }; + }, +}; diff --git a/app/ui/modules.js b/app/ui/modules.js index f9e3cde6..dde32086 100644 --- a/app/ui/modules.js +++ b/app/ui/modules.js @@ -4,7 +4,7 @@ export default [ "./features/log/index.js", "./features/projects/index.js", "./features/media/index.js", - "./features/placeholders/early.js", + "./features/sampler/index.js", "./features/dock/index.js", "./features/keys/index.js", "./features/timeline/index.js",