Put the top bar on the surface as icons, and give palettes a real chooser

Top bar: every document command is an icon button in a run — panes, new/open,
undo/history/redo, cut/copy/paste/duplicate, snapshots — with the title and
status in the middle and export, share and account on the right. Pane toggles
are pictures of the window with that pane filled in.

Stage bar: tracing, onion and passepartout are all the same toggle-plus-settings
split button, followed by one zoom group, with fit inside it.

Popovers: one `menu/popover`, measured against its button and dismissed by a
scrim, replaces the <details> popouts that never closed on an outside click.

Palette: the strip ends in a palette button whose popover lists every palette
with its swatch strip, renames the open one in place, and marks the ones on
the stage. An info badge says when the palette being edited is not one the
stage draws in at the playhead; both ends of a palette-lane blend count.
`clip/palette-at` is the resolver's root palette lookup, pulled out so the
strip asks the same question the renderer answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Olive Vaughn 2026-10-04 03:08:21 -04:00
parent eca1a96b82
commit f238ff4f62
15 changed files with 564 additions and 313 deletions

View file

@ -441,6 +441,9 @@
extended (nest/resize-out document :palettes [:change] 1 false)]
(resolve 2)
(is (= {:from :day :to :night :t 0.5} (clip/active-palette resolve)))
(is (= (clip/active-palette resolve)
(clip/palette-at document nil :day (clip/symbol document :main) 2 nil))
"the palette strip asks the same question the resolver answers: both ends of a blend")
(is (= [128 128 128]
(nth (pal/effective-ramp context (clip/active-palette resolve))
(pal/render-index context :day 1))))

View file

@ -72,14 +72,16 @@ try {
})()`);
await sleep(250);
assert.equal(await evaluate(`getComputedStyle(document.querySelector('.onion-controls summary')).listStyleType`), 'none');
assert.equal(await evaluate(`document.querySelector('.onion-controls summary').textContent`), '▾');
assert.equal(await evaluate(`document.querySelector('.onion-controls .menu-wrap > button').textContent`), '▾');
await evaluate(`document.querySelector('.onion-controls > button').click()`);
await sleep(150);
assert.equal(await evaluate(`document.querySelector('.onion-controls > button').getAttribute('aria-pressed')`), 'true');
await evaluate(`document.querySelector('.onion-controls summary').click()`);
await evaluate(`document.querySelector('.onion-controls .menu-wrap > button').click()`);
await sleep(100);
assert.equal(await evaluate(`document.querySelector('.onion-controls details').open`), true);
assert.equal(await evaluate(`!!document.querySelector('.menu.popout')`), true);
await evaluate(`document.querySelector('.menu-scrim').click()`);
await sleep(100);
assert.equal(await evaluate(`!!document.querySelector('.menu.popout')`), false, 'a click outside shuts the onion settings');
await evaluate(`(() => {
const c = cljs.core, k = c.keyword, m = (...xs) => c.hash_map(...xs), v = (...xs) => c.vector(...xs);
const db = c.deref(re_frame.db.app_db);

View file

@ -251,8 +251,7 @@ const CLICK = (label) => `(() => {
// turn of the event loop between them, as it is for anybody using the app.
const MENU_OPEN = `(() => {
if (document.querySelector('.menu')) return true;
const b = [...document.querySelectorAll('button')]
.find((b) => b.textContent.trim() === 'open \u25be');
const b = document.querySelector('.top button[aria-label="open a project"]');
if (!b || b.disabled) return false;
b.click();
return true;