fix(gui-extensions): keep the Knobs panel open while tuning (#57701)

This commit is contained in:
Kit Langton
2026-10-21 10:44:50 -07:00
committed by GitHub
Unverified
parent 31f2e302db
commit 1744121801
5 changed files with 40 additions and 9 deletions
@@ -62,6 +62,33 @@ it("offers Snappy, Smooth and Calm for every spring, with one Global card and on
expect(image()).toEqual({ duration: 0.3, bounce: 0 })
})
it("closes only from its own close button or Esc inside it, never from the app around it", async () => {
let closed = 0
const outside = document.createElement("button")
const container = document.createElement("div")
document.body.append(outside, container)
cleanups.push(mountMotionPanel(container, { onClose: () => closed++ }))
const close = await waitFor(() => container.querySelector<HTMLButtonElement>(".knobs-close") ?? undefined)
outside.focus()
outside.click()
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }))
document.dispatchEvent(new KeyboardEvent("keydown", { key: "p", metaKey: true, bubbles: true }))
expect(closed).toBe(0)
const random = await waitFor(
() => container.querySelector<HTMLButtonElement>('button[aria-label="Random"]') ?? undefined,
)
random.focus()
random.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }))
expect(closed).toBe(1)
close.click()
expect(closed).toBe(2)
})
async function waitFor<T>(find: () => T | undefined) {
for (let i = 0; i < 200; i++) {
const found = find()
+3 -3
View File
@@ -34,9 +34,9 @@ export default {
"mem.label": "MEM",
"mem.tipUnavailable": "Used JS heap vs heap limit. Chromium only.",
"mem.tip": "Used JS heap vs heap limit. {{used}} of {{limit}}.",
"motion.label": "MOTION",
"motion.title": "Motion",
"motion.tip": "Tune animation springs live. Changes persist on this device.",
"motion.label": "KNOBS",
"motion.title": "Knobs",
"motion.tip": "Tune live. Changes persist on this device.",
"speed.label": "SPEED",
"speed.tip": "Global animation speed. Click to step through 0.5× and 0.25× back to 1×.",
"slow.label": "SLOW",
+2 -2
View File
@@ -5,7 +5,7 @@ import { Portal } from "solid-js/web"
const key = "opencode.motion.panel"
// The Motion workspace, docked above the debug bar's right end until its header is dragged. knobs loads once it opens.
export default function Motion(props: { title: string }) {
export default function Motion(props: { title: string; onClose: () => void }) {
let container!: HTMLDivElement
let unmount: (() => void) | undefined
let disposed = false
@@ -76,7 +76,7 @@ export default function Motion(props: { title: string }) {
})
void import("@opencode/ui/motion-tuning-panel").then(({ mountMotionPanel }) => {
if (!disposed) unmount = mountMotionPanel(container, { title: props.title })
if (!disposed) unmount = mountMotionPanel(container, { title: props.title, onClose: props.onClose })
})
return (
@@ -50,10 +50,10 @@ const setup: Setup<typeof Debug> = (ctx) => {
</Suspense>
</Show>
{Motion && (
// The Motion workspace opens only from the debug bar's MOTION cell, and shows while the bar does.
// The Knobs workspace opens only from the debug bar's KNOBS cell, and shows while the bar does.
<Show when={visible() && motion()}>
<Suspense>
<Motion title={ctx.t("motion.title")} />
<Motion title={ctx.t("motion.title")} onClose={toggleMotion} />
</Suspense>
</Show>
)}
+6 -2
View File
@@ -10,7 +10,10 @@ const global = "Global"
* mount, so importing this module costs nothing until then. Works anywhere a surface registered its knobs, including
* Storybook stories. Returns its removal.
*/
export function mountMotionPanel(container: HTMLElement, options: { title?: string; section?: string } = {}) {
export function mountMotionPanel(
container: HTMLElement,
options: { title?: string; section?: string; onClose?: () => void } = {},
) {
let unmount: (() => void) | undefined
let disposed = false
let section = options.section
@@ -113,7 +116,8 @@ export function mountMotionPanel(container: HTMLElement, options: { title?: stri
const stopPanel = controls.mount(container, {
theme: "auto",
title: options.title ?? "Motion",
title: options.title ?? "Knobs",
onClose: options.onClose,
tabs: sections,
tab: section,
onTab: (id) => (section = id),