Put the top bar back to words

The icon run in the top bar read worse than what it replaced. Restore the
earlier top bar — pane words, new, open ▾, undo ▾ redo, the edit menu,
snapshots ▾, title, status, export… — and its CSS. The stage bar's overlay
switches, the palette chooser and `menu/popover` stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Olive Vaughn 2026-10-04 03:16:09 -04:00
parent f238ff4f62
commit 208dddee07
7 changed files with 168 additions and 207 deletions

View file

@ -121,26 +121,21 @@
:on-pointer-cancel (fn [_] (reset! sizing nil))}])) :on-pointer-cancel (fn [_] (reset! sizing nil))}]))
(defn panes (defn panes
"Which panes are open, as three switches at the left of the top bar, each a "Which panes are open, as a segmented control in the top bar. In the top bar
picture of the window with its pane filled in. In the top bar because a shut because a shut pane has no head left to carry its own handle, and because on a
pane has no head left to carry its own handle, and because on a narrow screen narrow screen this is the whole of the navigation."
this is the whole of the navigation."
[] []
(let [{:keys [shut]} @(rf/subscribe [::state])] (let [{:keys [shut]} @(rf/subscribe [::state])]
[:div.icons.panes {:aria-label "panes"} [:div.seg.panes {:aria-label "panes"}
(doall (doall
(for [[k label strip] [[:pool "media pool" {:x 1.5 :y 2.5 :width 4 :height 11}] (for [[k label] [[:pool "pool"] [:params "inspector"] [:time "timeline"]]]
[:time "timeline" {:x 1.5 :y 9.5 :width 13 :height 4}]
[:params "inspector" {:x 10.5 :y 2.5 :width 4 :height 11}]]]
(let [open? (not (contains? shut k))] (let [open? (not (contains? shut k))]
^{:key k} ^{:key k}
[:button.ico {:class (when open? "on") [:button {:class (when open? "on")
:aria-pressed open? :aria-label label :aria-pressed open?
:title (str (if open? "hide the " "show the ") label) :title (str (if open? "hide the " "show the ") label)
:on-click #(rf/dispatch [::toggle k])} :on-click #(rf/dispatch [::toggle k])}
[:svg {:view-box "0 0 16 16"} label])))]))
[:rect.frame {:x 1.5 :y 2.5 :width 13 :height 11 :rx 1}]
[:rect (assoc strip :rx 0.5)]]])))]))
(defn- stepped [k z out?] (defn- stepped [k z out?]
;; Geometric steps work below 100% as well as above it. ;; Geometric steps work below 100% as well as above it.

View file

@ -14,7 +14,9 @@
(:require [arthur.db :as db] (:require [arthur.db :as db]
[arthur.events.playback :as pb] [arthur.events.playback :as pb]
[arthur.events.project :as project] [arthur.events.project :as project]
[re-frame.core :as rf])) [arthur.subs.playback :as playback]
[re-frame.core :as rf]
[reagent.core :as r]))
(defn- when-said (defn- when-said
"The ISO stamp the server sends, as a date. Truncated rather than formatted: "The ISO stamp the server sends, as a date. Truncated rather than formatted:
@ -33,30 +35,44 @@
label label
(when sub [:span.sub sub])]) (when sub [:span.sub sub])])
(defn panel (defn view []
"The list, under the top bar's file menu. `close!` shuts it." (r/with-let [open? (r/atom false)]
[close!] (let [{:keys [items loading?]} @(rf/subscribe [::project/listing])
(let [{:keys [items loading?]} @(rf/subscribe [::project/listing]) busy? (:busy? @(rf/subscribe [::playback/project]))
choose! (fn [event] (close!) (rf/dispatch event))] choose! (fn [event] (reset! open? false) (rf/dispatch event))]
[:div.menu.menu-left [:div.menu-wrap
[:h2 "projects"] [:button {:disabled busy?
(cond :class (when @open? "on")
loading? [:div.dim "…"] :on-click (fn []
(empty? items) [:div.dim "none saved yet"] (when-not @open? (rf/dispatch [::project/list]))
:else (swap! open? not))}
(doall "open ▾"]
(for [{:keys [id name seq updated]} items] (when @open?
^{:key id} [:<>
[item {:label (or name "untitled") ;; A full-page catcher behind the panel, so clicking anywhere else
:sub (str "r" seq " · " (when-said updated)) ;; dismisses it. Cheaper and more predictable than a document-level
:on-click #(choose! [::project/open id])}]))) ;; listener that has to be added, removed and told to ignore the click
[:h2 "built-in examples"] ;; that opened the menu.
[:div.dim "compiled in, not saved documents"] [:div.menu-scrim {:on-click #(reset! open? false)}]
(doall [:div.menu
(for [[id {:keys [label]}] db/clips] [:h2 "projects"]
^{:key id} (cond
[item {:label label :example? true loading? [:div.dim "…"]
:on-click #(choose! [::pb/select-clip id])}])) (empty? items) [:div.dim "none saved yet"]
[item {:label "8625 stage study" :example? true :else
:sub "composed from a locally saved project" (doall
:on-click #(choose! [::project/load-stage])}]])) (for [{:keys [id name seq updated]} items]
^{:key id}
[item {:label (or name "untitled")
:sub (str "r" seq " · " (when-said updated))
:on-click #(choose! [::project/open id])}])))
[:h2 "built-in examples"]
[:div.dim "compiled in, not saved documents"]
(doall
(for [[id {:keys [label]}] db/clips]
^{:key id}
[item {:label label :example? true
:on-click #(choose! [::pb/select-clip id])}]))
[item {:label "8625 stage study" :example? true
:sub "composed from a locally saved project"
:on-click #(choose! [::project/load-stage])}]]])])))

View file

@ -7,32 +7,38 @@
[re-frame.core :as rf] [re-frame.core :as rf]
[reagent.core :as r])) [reagent.core :as r]))
(defn panel (defn view []
"Named versions, under the top bar's file menu. `close!` shuts it." (r/with-let [open? (r/atom false)
[close!] draft (r/atom "")]
(r/with-let [draft (r/atom "")]
(let [{:keys [can-edit?]} @(rf/subscribe [::playback/project]) (let [{:keys [can-edit?]} @(rf/subscribe [::playback/project])
rows @(rf/subscribe [::collab/snapshot-list]) rows @(rf/subscribe [::collab/snapshot-list])
editable? (not (false? can-edit?))] editable? (not (false? can-edit?))]
[:div.menu.menu-left [:div.menu-wrap
(when editable? [:button {:class (when @open? "on")
[:form.row {:on-submit (fn [e] :on-click (fn [] (when-not @open? (rf/dispatch [::collab/snapshots]))
(.preventDefault e) (swap! open? not))}
(rf/dispatch [::collab/snapshot "snapshots ▾"]
(or (not-empty (str/trim @draft)) "snapshot")]) (when @open?
(reset! draft ""))} [:<> [:div.menu-scrim {:on-click #(reset! open? false)}]
[:input {:placeholder "name this version" :value @draft :auto-focus true [:div.menu.menu-left
:on-change #(reset! draft (.. % -target -value))}] (when editable?
[:button {:type "submit"} "take snapshot"]]) [:form.row {:on-submit (fn [e]
[:h2 "snapshots"] (.preventDefault e)
(if (empty? rows) (rf/dispatch [::collab/snapshot
[:div.dim "none yet"] (or (not-empty (str/trim @draft)) "snapshot")])
(doall (reset! draft ""))}
(for [{:keys [id name author created] :as row} rows] [:input {:placeholder "name this version" :value @draft :auto-focus true
^{:key id} :on-change #(reset! draft (.. % -target -value))}]
[:div.menu-item.static [:button {:type "submit"} "take snapshot"]])
[:span name [:span.sub (str author " · " (subs (str created) 0 16))]] [:h2 "snapshots"]
(when editable? (if (empty? rows)
[:button.link {:on-click (fn [] (close!) [:div.dim "none yet"]
(rf/dispatch [::collab/restore row]))} (doall
"restore"])])))]))) (for [{:keys [id name author created] :as row} rows]
^{:key id}
[:div.menu-item.static
[:span name [:span.sub (str author " · " (subs (str created) 0 16))]]
(when editable?
[:button.link {:on-click (fn [] (reset! open? false)
(rf/dispatch [::collab/restore row]))}
"restore"])])))]])])))

View file

@ -1,20 +1,18 @@
(ns arthur.ui.topbar (ns arthur.ui.topbar
"The strip across the top: everything you do to the whole document, on the "The strip across the top: what document this is, and the three things you can
surface rather than in menus, and as pictures rather than words so there is do to the whole of it.
room for all of it. Each group is a run of the same plain icon button — panes,
file, history, clipboard, versions — then the document's name in the middle,
then export and the people on the right. Every icon says what it is on hover.
Open, snapshots and undo history are lists, so they drop from their button; Save, open and export are here rather than in a pane because none of them is a
only one of them is open at a time." property of a selection — they act on the document, and the document is the
window."
(:require [arthur.events.collab :as collab] (:require [arthur.events.collab :as collab]
[arthur.events.export :as export] [arthur.events.export :as export]
[arthur.events.history :as history]
[arthur.events.project :as project] [arthur.events.project :as project]
[arthur.events.ui :as ui] [arthur.events.ui :as ui]
[arthur.subs.playback :as playback] [arthur.subs.playback :as playback]
[arthur.subs.ui :as sub] [arthur.subs.ui :as sub]
[arthur.ui.layout :as layout] [arthur.ui.layout :as layout]
[arthur.ui.menu :as menu]
[arthur.ui.openmenu :as openmenu] [arthur.ui.openmenu :as openmenu]
[arthur.ui.share :as share] [arthur.ui.share :as share]
[arthur.ui.snapshots :as snapshots] [arthur.ui.snapshots :as snapshots]
@ -26,6 +24,7 @@
(let [{:keys [target zoom busy? done total]} @(rf/subscribe [::export/state]) (let [{:keys [target zoom busy? done total]} @(rf/subscribe [::export/state])
targets @(rf/subscribe [::export/targets])] targets @(rf/subscribe [::export/targets])]
[:<> [:<>
[:button {:disabled busy? :on-click #(reset! open? true)} "export…"]
(when @open? (when @open?
[:<> [:div.export-scrim {:on-click #(when-not busy? (reset! open? false))}] [:<> [:div.export-scrim {:on-click #(when-not busy? (reset! open? false))}]
[:section.export-dialog {:role "dialog" :aria-modal true :aria-label "Export"} [:section.export-dialog {:role "dialog" :aria-modal true :aria-label "Export"}
@ -49,53 +48,16 @@
(if busy? (str "rendering " done "/" total) "start export")] (if busy? (str "rendering " done "/" total) "start export")]
[:button {:disabled busy? :on-click #(reset! open? false)} "close"]]]])])) [:button {:disabled busy? :on-click #(reset! open? false)} "close"]]]])]))
(defn- icon
"A 16-unit glyph, stroked in the button's own colour."
[& paths]
(into [:svg {:view-box "0 0 16 16"}] paths))
(def ^:private glyphs
{:new (icon [:path {:d "M4 1.5h5.5L12.5 4.5V14.5H4Z"}] [:path {:d "M8.25 7v5M5.75 9.5h5"}])
:open (icon [:path {:d "M1.5 3.5h4.5l1.5 1.5h7v8.5h-13Z"}])
:undo (icon [:path {:d "M5.5 3 2.5 6l3 3"}] [:path {:d "M2.5 6h7a4 4 0 0 1 0 8H6"}])
:redo (icon [:path {:d "M10.5 3l3 3-3 3"}] [:path {:d "M13.5 6h-7a4 4 0 0 0 0 8H10"}])
:history (icon [:path {:d "M4.5 6.5 8 10l3.5-3.5"}])
:cut (icon [:circle {:cx 4.5 :cy 12 :r 2}] [:circle {:cx 11.5 :cy 12 :r 2}]
[:path {:d "M5.8 10.5 11.5 2M10.2 10.5 4.5 2"}])
:copy (icon [:rect {:x 2 :y 2 :width 8 :height 8 :rx 1}] [:rect {:x 6 :y 6 :width 8 :height 8 :rx 1}])
:paste (icon [:path {:d "M5.5 2.5h-2v12h9v-12h-2"}] [:rect {:x 5.5 :y 1.5 :width 5 :height 2.5 :rx 0.5}])
:duplicate (icon [:rect.fill {:x 2 :y 2 :width 8 :height 8 :rx 1}] [:rect {:x 6 :y 6 :width 8 :height 8 :rx 1}])
:unique (icon [:rect.fill {:x 2 :y 2 :width 8 :height 8 :rx 1}]
[:rect {:x 6 :y 6 :width 8 :height 8 :rx 1 :stroke-dasharray "2 1.5"}])
:snapshots (icon [:path {:d "M4 1.5h8v13l-4-3-4 3Z"}])
:export (icon [:path {:d "M8 10V1.5M4.5 5 8 1.5 11.5 5"}] [:path {:d "M2 9.5v5h12v-5"}])})
(defn- btn [glyph title attrs]
[:button.ico (merge {:title title :aria-label title} attrs) (glyphs glyph)])
(defn- dropper
"An icon button with a list hanging from it: projects, snapshots, undo steps."
[panel k glyph title on-open body & [disabled?]]
[:div.menu-wrap
[btn glyph title {:class (when (= k @panel) "on") :disabled disabled?
:on-click (fn [] (if (= k @panel)
(reset! panel nil)
(do (on-open) (reset! panel k))))}]
(when (= k @panel)
[:<> [:div.menu-scrim {:on-click #(reset! panel nil)}]
[body #(reset! panel nil)]])])
(defn view [] (defn view []
(r/with-let [renaming? (r/atom false) (r/with-let [renaming? (r/atom false)
draft (r/atom "") draft (r/atom "")
panel (r/atom nil)
export-open? (r/atom false)] export-open? (r/atom false)]
(let [{project-name :name project-seq :seq :keys [busy? status]} @(rf/subscribe [::playback/project]) (let [{project-name :name project-seq :seq :keys [busy? status]} @(rf/subscribe [::playback/project])
{footage-status :status} @(rf/subscribe [::playback/footage]) {footage-status :status} @(rf/subscribe [::playback/footage])
{export-status :status export-busy? :busy?} @(rf/subscribe [::export/state]) {export-status :status} @(rf/subscribe [::export/state])
{:keys [done undone]} @(rf/subscribe [::history/steps]) selections @(rf/subscribe [::sub/selections])
selected? (boolean (seq @(rf/subscribe [::sub/selections])))
clipboard @(rf/subscribe [::sub/clipboard]) clipboard @(rf/subscribe [::sub/clipboard])
selected? (boolean (seq selections))
title (or project-name "untitled") title (or project-name "untitled")
commit! (fn [] commit! (fn []
(rf/dispatch [::project/rename @draft]) (rf/dispatch [::project/rename @draft])
@ -107,46 +69,37 @@
;; this strip scrolls, and which panes are open is the one control that ;; this strip scrolls, and which panes are open is the one control that
;; must not be the thing off the end of it. ;; must not be the thing off the end of it.
[layout/panes] [layout/panes]
[:span.sep] [:button {:disabled busy? :on-click #(rf/dispatch [::collab/create])} "new"]
[:div.icons [openmenu/view]
[btn :new "new project" {:disabled busy? :on-click #(rf/dispatch [::collab/create])}] [undo/view]
[dropper panel :open :open "open a project" #(rf/dispatch [::project/list]) openmenu/panel]] [menu/view
[:span.sep] {:label "edit" :title "selection and clipboard"
[:div.icons :items [{:label "copy" :sub "Copy selection · ⌘/Ctrl C"
[btn :undo (if (seq done) (str "undo " (first done) " (⌘Z)") "nothing to undo") :disabled? (not selected?) :on-click #(rf/dispatch [::ui/copy])}
{:disabled (empty? done) :on-click #(rf/dispatch [::history/undo])}] {:label "cut" :sub "Cut selection · ⌘/Ctrl X"
[dropper panel :history :history "undo history" identity undo/panel (empty? done)] :disabled? (not selected?) :on-click #(rf/dispatch [::ui/cut])}
[btn :redo (if (seq undone) (str "redo " (first undone) " (⇧⌘Z)") "nothing to redo") {:label "paste" :sub "Paste at playhead into the active row · ⌘/Ctrl V"
{:disabled (empty? undone) :on-click #(rf/dispatch [::history/redo])}]] :disabled? (nil? clipboard) :on-click #(rf/dispatch [::ui/paste])}
[:span.sep] {:label "duplicate" :sub "Duplicate beside selection; lanes repeat forward · ⌘/Ctrl D"
[:div.icons :disabled? (not selected?) :on-click #(rf/dispatch [::ui/duplicate])}
[btn :cut "cut selection (⌘X)" {:disabled (not selected?) :on-click #(rf/dispatch [::ui/cut])}] {:label "duplicate unique"
[btn :copy "copy selection (⌘C)" {:disabled (not selected?) :on-click #(rf/dispatch [::ui/copy])}] :sub "Duplicate with a private nested symbol graph · ⇧⌘/Ctrl D"
[btn :paste "paste at the playhead into the active row (⌘V)" :disabled? (not selected?)
{:disabled (nil? clipboard) :on-click #(rf/dispatch [::ui/paste])}] :on-click #(rf/dispatch [::ui/duplicate-unique])}]}]
[btn :duplicate "duplicate beside the selection; lanes repeat forward (⌘D)" [snapshots/view]
{:disabled (not selected?) :on-click #(rf/dispatch [::ui/duplicate])}] (if @renaming?
[btn :unique "duplicate unique — with a private copy of its nested symbols (⇧⌘D)" [:input.project-name {:auto-focus true :value @draft
{:disabled (not selected?) :on-click #(rf/dispatch [::ui/duplicate-unique])}]] :on-change #(reset! draft (.. % -target -value))
[:span.sep] :on-blur (fn [_] (commit!))
[dropper panel :snapshots :snapshots "snapshots — name this version, or go back to one" :on-key-down (fn [e]
#(rf/dispatch [::collab/snapshots]) snapshots/panel] (case (.-key e)
[:div.doc "Enter" (do (.preventDefault e) (commit!))
(if @renaming? "Escape" (reset! renaming? false)
[:input.project-name {:auto-focus true :value @draft nil))}]
:on-change #(reset! draft (.. % -target -value)) [:button.project-title {:disabled busy? :title "click to rename project"
:on-blur (fn [_] (commit!)) :on-click (fn [] (reset! draft title)
:on-key-down (fn [e] (reset! renaming? true))}
(case (.-key e) title (when project-seq (str " r" project-seq))])
"Enter" (do (.preventDefault e) (commit!)) [:span.status (or export-status status footage-status)]
"Escape" (reset! renaming? false)
nil))}]
[:button.project-title {:disabled busy? :title "click to rename project"
:on-click (fn [] (reset! draft title)
(reset! renaming? true))}
title (when project-seq [:span.dim (str " r" project-seq)])])
[:span.status (or export-status status footage-status)]]
[btn :export "export a PNG sequence and audio"
{:disabled export-busy? :on-click #(reset! export-open? true)}]
[exporter export-open?] [exporter export-open?]
[share/view]]))) [share/view]])))

View file

@ -1,22 +1,32 @@
(ns arthur.ui.undo (ns arthur.ui.undo
"The list of what undo would take off — newest first, so choosing the third "Undo, redo, and the list of what undo would take off — newest first, so
row undoes three steps. Undo and redo themselves are the top bar's." choosing the third row undoes three steps."
(:require [arthur.events.history :as history] (:require [arthur.events.history :as history]
[re-frame.core :as rf])) [re-frame.core :as rf]
[reagent.core :as r]))
(defn panel (defn view []
"What undo would take off, newest first, under the edit menu. `close!` shuts it." (r/with-let [open? (r/atom false)]
[close!] (let [{:keys [done undone]} @(rf/subscribe [::history/steps])]
(let [{:keys [done]} @(rf/subscribe [::history/steps])] [:div.menu-wrap.undo
[:div.menu.menu-left [:button {:disabled (empty? done) :title (if (seq done) (str "undo " (first done) " (⌘Z)") "nothing to undo")
[:h2 "undo history"] :on-click #(rf/dispatch [::history/undo])}
(if (empty? done) "undo"]
[:div.dim "nothing to undo"] [:button.undo-list {:disabled (empty? done) :class (when @open? "on")
(doall :title "undo history" :on-click #(swap! open? not)}
(map-indexed "▾"]
(fn [i label] [:button {:disabled (empty? undone) :title (if (seq undone) (str "redo " (first undone) " (⇧⌘Z)") "nothing to redo")
^{:key i} :on-click #(rf/dispatch [::history/redo])}
[:button.menu-item {:on-click (fn [] (close!) "redo"]
(rf/dispatch [::history/undo (inc i)]))} (when (and @open? (seq done))
label (when (pos? i) [:span.sub (str (inc i) " steps")])]) [:<> [:div.menu-scrim {:on-click #(reset! open? false)}]
(take 30 done))))])) [:div.menu.menu-left
[:h2 "undo"]
(doall
(map-indexed
(fn [i label]
^{:key i}
[:button.menu-item {:on-click (fn [] (reset! open? false)
(rf/dispatch [::history/undo (inc i)]))}
label (when (pos? i) [:span.sub (str (inc i) " steps")])])
(take 30 done)))]])])))

View file

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

View file

@ -155,6 +155,8 @@ audio { display: none; }
/* Which panes are open, in the top bar. Lower-case words rather than icons: the /* Which panes are open, in the top bar. Lower-case words rather than icons: the
three panes have names and the names are what the rest of the chrome calls three panes have names and the names are what the rest of the chrome calls
them. */ them. */
.panes > button { color: var(--dim); }
.panes > button.on { color: var(--fg); }
/* A zoom readout that is also the way back to 1:1, so it is a button that looks /* A zoom readout that is also the way back to 1:1, so it is a button that looks
like the number it shows. Tabular figures, because the + beside it must not like the number it shows. Tabular figures, because the + beside it must not
@ -311,29 +313,8 @@ button.retry { border-color: var(--sel); color: var(--sel); }
} }
.top .brand { color: var(--dim); letter-spacing: .14em; text-transform: uppercase; } .top .brand { color: var(--dim); letter-spacing: .14em; text-transform: uppercase; }
/* The top bar's icon runs: plain glyphs until pointed at, accented when on.
A run is one group; `.sep` divides one run from the next. */
.icons { display: flex; align-items: center; gap: 1px; }
.top button.ico { border-color: transparent; background: none; padding: 3px 5px; }
.top button.ico:hover:not(:disabled) { background: var(--sunk); border-color: var(--hair); }
.top button.ico.on { background: var(--sel-bg); border-color: var(--sel); color: var(--sel); }
.top button.ico > svg {
width: 16px; height: 16px;
fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round;
}
.top button.ico svg .fill, .panes svg rect:not(.frame) { fill: currentColor; stroke: none; }
.top .sep { height: 16px; }
/* What this is and what it is doing: the middle of the bar, and the room in it. */
.top .doc {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 9px;
}
.top .status { .top .status {
flex: 1;
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -341,13 +322,7 @@ button.retry { border-color: var(--sel); color: var(--sel); }
color: var(--dim); color: var(--dim);
} }
.project-title { .project-title { max-width: 22vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
max-width: 28vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
border-color: transparent; background: none; font-weight: 600;
}
.project-title:hover:not(:disabled) { border-color: var(--line); }
.panes button.ico:not(.on) { color: var(--dim); }
.project-name { width: 180px; } .project-name { width: 180px; }
.export-scrim { position: fixed; inset: 0; z-index: 50; background: rgba(0, 0, 0, .45); } .export-scrim { position: fixed; inset: 0; z-index: 50; background: rgba(0, 0, 0, .45); }
.export-dialog { .export-dialog {
@ -442,6 +417,11 @@ button.link { border: 0; background: none; padding: 0; color: var(--sel); cursor
.index-list a:hover { color: var(--sel); } .index-list a:hover { color: var(--sel); }
a.brand { text-decoration: none; } a.brand { text-decoration: none; }
/* Undo, its list, redo: one group. */
.undo { display: flex; gap: 0; }
.undo > button { border-radius: 0; }
.undo > button:first-child { border-radius: 2px 0 0 2px; }
.undo > .undo-list { padding: 0 4px; margin-right: 4px; border-left: 0; border-radius: 0 2px 2px 0; }
.menu.menu-left { left: 0; right: auto; } .menu.menu-left { left: 0; right: auto; }
/* A menu dropping out of a PANE HEAD. Fixed and placed by `ui/menu` against the /* A menu dropping out of a PANE HEAD. Fixed and placed by `ui/menu` against the