From 208dddee07eb5928e4d79bde543181abf9070676 Mon Sep 17 00:00:00 2001 From: Olive Vaughn Date: Sun, 4 Oct 2026 03:16:09 -0400 Subject: [PATCH] Put the top bar back to words MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/src/arthur/ui/layout.cljs | 25 ++--- frontend/src/arthur/ui/openmenu.cljs | 72 ++++++++------ frontend/src/arthur/ui/snapshots.cljs | 58 +++++++----- frontend/src/arthur/ui/topbar.cljs | 131 +++++++++----------------- frontend/src/arthur/ui/undo.cljs | 48 ++++++---- frontend/test/browser/take.mjs | 3 +- static/arthur/app.css | 38 ++------ 7 files changed, 168 insertions(+), 207 deletions(-) diff --git a/frontend/src/arthur/ui/layout.cljs b/frontend/src/arthur/ui/layout.cljs index 2440881..027367c 100644 --- a/frontend/src/arthur/ui/layout.cljs +++ b/frontend/src/arthur/ui/layout.cljs @@ -121,26 +121,21 @@ :on-pointer-cancel (fn [_] (reset! sizing nil))}])) (defn panes - "Which panes are open, as three switches at the left of the top bar, each a - picture of the window with its pane filled in. In the top bar because a shut - pane has no head left to carry its own handle, and because on a narrow screen - this is the whole of the navigation." + "Which panes are open, as a segmented control in the top bar. In the top bar + because a shut pane has no head left to carry its own handle, and because on a + narrow screen this is the whole of the navigation." [] (let [{:keys [shut]} @(rf/subscribe [::state])] - [:div.icons.panes {:aria-label "panes"} + [:div.seg.panes {:aria-label "panes"} (doall - (for [[k label strip] [[:pool "media pool" {:x 1.5 :y 2.5 :width 4 :height 11}] - [:time "timeline" {:x 1.5 :y 9.5 :width 13 :height 4}] - [:params "inspector" {:x 10.5 :y 2.5 :width 4 :height 11}]]] + (for [[k label] [[:pool "pool"] [:params "inspector"] [:time "timeline"]]] (let [open? (not (contains? shut k))] ^{:key k} - [:button.ico {:class (when open? "on") - :aria-pressed open? :aria-label label - :title (str (if open? "hide the " "show the ") label) - :on-click #(rf/dispatch [::toggle k])} - [:svg {:view-box "0 0 16 16"} - [:rect.frame {:x 1.5 :y 2.5 :width 13 :height 11 :rx 1}] - [:rect (assoc strip :rx 0.5)]]])))])) + [:button {:class (when open? "on") + :aria-pressed open? + :title (str (if open? "hide the " "show the ") label) + :on-click #(rf/dispatch [::toggle k])} + label])))])) (defn- stepped [k z out?] ;; Geometric steps work below 100% as well as above it. diff --git a/frontend/src/arthur/ui/openmenu.cljs b/frontend/src/arthur/ui/openmenu.cljs index 4450b10..5f10bd3 100644 --- a/frontend/src/arthur/ui/openmenu.cljs +++ b/frontend/src/arthur/ui/openmenu.cljs @@ -14,7 +14,9 @@ (:require [arthur.db :as db] [arthur.events.playback :as pb] [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 "The ISO stamp the server sends, as a date. Truncated rather than formatted: @@ -33,30 +35,44 @@ label (when sub [:span.sub sub])]) -(defn panel - "The list, under the top bar's file menu. `close!` shuts it." - [close!] - (let [{:keys [items loading?]} @(rf/subscribe [::project/listing]) - choose! (fn [event] (close!) (rf/dispatch event))] - [:div.menu.menu-left - [:h2 "projects"] - (cond - loading? [:div.dim "…"] - (empty? items) [:div.dim "none saved yet"] - :else - (doall - (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])}]])) +(defn view [] + (r/with-let [open? (r/atom false)] + (let [{:keys [items loading?]} @(rf/subscribe [::project/listing]) + busy? (:busy? @(rf/subscribe [::playback/project])) + choose! (fn [event] (reset! open? false) (rf/dispatch event))] + [:div.menu-wrap + [:button {:disabled busy? + :class (when @open? "on") + :on-click (fn [] + (when-not @open? (rf/dispatch [::project/list])) + (swap! open? not))} + "open ▾"] + (when @open? + [:<> + ;; A full-page catcher behind the panel, so clicking anywhere else + ;; dismisses it. Cheaper and more predictable than a document-level + ;; listener that has to be added, removed and told to ignore the click + ;; that opened the menu. + [:div.menu-scrim {:on-click #(reset! open? false)}] + [:div.menu + [:h2 "projects"] + (cond + loading? [:div.dim "…"] + (empty? items) [:div.dim "none saved yet"] + :else + (doall + (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])}]]])]))) diff --git a/frontend/src/arthur/ui/snapshots.cljs b/frontend/src/arthur/ui/snapshots.cljs index c220ba2..bb2d64b 100644 --- a/frontend/src/arthur/ui/snapshots.cljs +++ b/frontend/src/arthur/ui/snapshots.cljs @@ -7,32 +7,38 @@ [re-frame.core :as rf] [reagent.core :as r])) -(defn panel - "Named versions, under the top bar's file menu. `close!` shuts it." - [close!] - (r/with-let [draft (r/atom "")] +(defn view [] + (r/with-let [open? (r/atom false) + draft (r/atom "")] (let [{:keys [can-edit?]} @(rf/subscribe [::playback/project]) rows @(rf/subscribe [::collab/snapshot-list]) editable? (not (false? can-edit?))] - [:div.menu.menu-left - (when editable? - [:form.row {:on-submit (fn [e] - (.preventDefault e) - (rf/dispatch [::collab/snapshot - (or (not-empty (str/trim @draft)) "snapshot")]) - (reset! draft ""))} - [:input {:placeholder "name this version" :value @draft :auto-focus true - :on-change #(reset! draft (.. % -target -value))}] - [:button {:type "submit"} "take snapshot"]]) - [:h2 "snapshots"] - (if (empty? rows) - [:div.dim "none yet"] - (doall - (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 [] (close!) - (rf/dispatch [::collab/restore row]))} - "restore"])])))]))) + [:div.menu-wrap + [:button {:class (when @open? "on") + :on-click (fn [] (when-not @open? (rf/dispatch [::collab/snapshots])) + (swap! open? not))} + "snapshots ▾"] + (when @open? + [:<> [:div.menu-scrim {:on-click #(reset! open? false)}] + [:div.menu.menu-left + (when editable? + [:form.row {:on-submit (fn [e] + (.preventDefault e) + (rf/dispatch [::collab/snapshot + (or (not-empty (str/trim @draft)) "snapshot")]) + (reset! draft ""))} + [:input {:placeholder "name this version" :value @draft :auto-focus true + :on-change #(reset! draft (.. % -target -value))}] + [:button {:type "submit"} "take snapshot"]]) + [:h2 "snapshots"] + (if (empty? rows) + [:div.dim "none yet"] + (doall + (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"])])))]])]))) diff --git a/frontend/src/arthur/ui/topbar.cljs b/frontend/src/arthur/ui/topbar.cljs index 4d6c3b3..f1bc2f4 100644 --- a/frontend/src/arthur/ui/topbar.cljs +++ b/frontend/src/arthur/ui/topbar.cljs @@ -1,20 +1,18 @@ (ns arthur.ui.topbar - "The strip across the top: everything you do to the whole document, on the - surface rather than in menus, and as pictures rather than words so there is - 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. + "The strip across the top: what document this is, and the three things you can + do to the whole of it. - Open, snapshots and undo history are lists, so they drop from their button; - only one of them is open at a time." + Save, open and export are here rather than in a pane because none of them is a + property of a selection — they act on the document, and the document is the + window." (:require [arthur.events.collab :as collab] [arthur.events.export :as export] - [arthur.events.history :as history] [arthur.events.project :as project] [arthur.events.ui :as ui] [arthur.subs.playback :as playback] [arthur.subs.ui :as sub] [arthur.ui.layout :as layout] + [arthur.ui.menu :as menu] [arthur.ui.openmenu :as openmenu] [arthur.ui.share :as share] [arthur.ui.snapshots :as snapshots] @@ -26,6 +24,7 @@ (let [{:keys [target zoom busy? done total]} @(rf/subscribe [::export/state]) targets @(rf/subscribe [::export/targets])] [:<> + [:button {:disabled busy? :on-click #(reset! open? true)} "export…"] (when @open? [:<> [:div.export-scrim {:on-click #(when-not busy? (reset! open? false))}] [:section.export-dialog {:role "dialog" :aria-modal true :aria-label "Export"} @@ -49,53 +48,16 @@ (if busy? (str "rendering " done "/" total) "start export")] [: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 [] (r/with-let [renaming? (r/atom false) draft (r/atom "") - panel (r/atom nil) export-open? (r/atom false)] (let [{project-name :name project-seq :seq :keys [busy? status]} @(rf/subscribe [::playback/project]) {footage-status :status} @(rf/subscribe [::playback/footage]) - {export-status :status export-busy? :busy?} @(rf/subscribe [::export/state]) - {:keys [done undone]} @(rf/subscribe [::history/steps]) - selected? (boolean (seq @(rf/subscribe [::sub/selections]))) + {export-status :status} @(rf/subscribe [::export/state]) + selections @(rf/subscribe [::sub/selections]) clipboard @(rf/subscribe [::sub/clipboard]) + selected? (boolean (seq selections)) title (or project-name "untitled") commit! (fn [] (rf/dispatch [::project/rename @draft]) @@ -107,46 +69,37 @@ ;; this strip scrolls, and which panes are open is the one control that ;; must not be the thing off the end of it. [layout/panes] - [:span.sep] - [:div.icons - [btn :new "new project" {:disabled busy? :on-click #(rf/dispatch [::collab/create])}] - [dropper panel :open :open "open a project" #(rf/dispatch [::project/list]) openmenu/panel]] - [:span.sep] - [:div.icons - [btn :undo (if (seq done) (str "undo " (first done) " (⌘Z)") "nothing to undo") - {:disabled (empty? done) :on-click #(rf/dispatch [::history/undo])}] - [dropper panel :history :history "undo history" identity undo/panel (empty? done)] - [btn :redo (if (seq undone) (str "redo " (first undone) " (⇧⌘Z)") "nothing to redo") - {:disabled (empty? undone) :on-click #(rf/dispatch [::history/redo])}]] - [:span.sep] - [:div.icons - [btn :cut "cut selection (⌘X)" {:disabled (not selected?) :on-click #(rf/dispatch [::ui/cut])}] - [btn :copy "copy selection (⌘C)" {:disabled (not selected?) :on-click #(rf/dispatch [::ui/copy])}] - [btn :paste "paste at the playhead into the active row (⌘V)" - {:disabled (nil? clipboard) :on-click #(rf/dispatch [::ui/paste])}] - [btn :duplicate "duplicate beside the selection; lanes repeat forward (⌘D)" - {:disabled (not selected?) :on-click #(rf/dispatch [::ui/duplicate])}] - [btn :unique "duplicate unique — with a private copy of its nested symbols (⇧⌘D)" - {:disabled (not selected?) :on-click #(rf/dispatch [::ui/duplicate-unique])}]] - [:span.sep] - [dropper panel :snapshots :snapshots "snapshots — name this version, or go back to one" - #(rf/dispatch [::collab/snapshots]) snapshots/panel] - [:div.doc - (if @renaming? - [:input.project-name {:auto-focus true :value @draft - :on-change #(reset! draft (.. % -target -value)) - :on-blur (fn [_] (commit!)) - :on-key-down (fn [e] - (case (.-key e) - "Enter" (do (.preventDefault e) (commit!)) - "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)}] + [:button {:disabled busy? :on-click #(rf/dispatch [::collab/create])} "new"] + [openmenu/view] + [undo/view] + [menu/view + {:label "edit" :title "selection and clipboard" + :items [{:label "copy" :sub "Copy selection · ⌘/Ctrl C" + :disabled? (not selected?) :on-click #(rf/dispatch [::ui/copy])} + {:label "cut" :sub "Cut selection · ⌘/Ctrl X" + :disabled? (not selected?) :on-click #(rf/dispatch [::ui/cut])} + {:label "paste" :sub "Paste at playhead into the active row · ⌘/Ctrl V" + :disabled? (nil? clipboard) :on-click #(rf/dispatch [::ui/paste])} + {:label "duplicate" :sub "Duplicate beside selection; lanes repeat forward · ⌘/Ctrl D" + :disabled? (not selected?) :on-click #(rf/dispatch [::ui/duplicate])} + {:label "duplicate unique" + :sub "Duplicate with a private nested symbol graph · ⇧⌘/Ctrl D" + :disabled? (not selected?) + :on-click #(rf/dispatch [::ui/duplicate-unique])}]}] + [snapshots/view] + (if @renaming? + [:input.project-name {:auto-focus true :value @draft + :on-change #(reset! draft (.. % -target -value)) + :on-blur (fn [_] (commit!)) + :on-key-down (fn [e] + (case (.-key e) + "Enter" (do (.preventDefault e) (commit!)) + "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 (str " r" project-seq))]) + [:span.status (or export-status status footage-status)] [exporter export-open?] [share/view]]))) diff --git a/frontend/src/arthur/ui/undo.cljs b/frontend/src/arthur/ui/undo.cljs index 821f765..18ac7bb 100644 --- a/frontend/src/arthur/ui/undo.cljs +++ b/frontend/src/arthur/ui/undo.cljs @@ -1,22 +1,32 @@ (ns arthur.ui.undo - "The list of what undo would take off — newest first, so choosing the third - row undoes three steps. Undo and redo themselves are the top bar's." + "Undo, redo, and the list of what undo would take off — newest first, so + choosing the third row undoes three steps." (:require [arthur.events.history :as history] - [re-frame.core :as rf])) + [re-frame.core :as rf] + [reagent.core :as r])) -(defn panel - "What undo would take off, newest first, under the edit menu. `close!` shuts it." - [close!] - (let [{:keys [done]} @(rf/subscribe [::history/steps])] - [:div.menu.menu-left - [:h2 "undo history"] - (if (empty? done) - [:div.dim "nothing to undo"] - (doall - (map-indexed - (fn [i label] - ^{:key i} - [:button.menu-item {:on-click (fn [] (close!) - (rf/dispatch [::history/undo (inc i)]))} - label (when (pos? i) [:span.sub (str (inc i) " steps")])]) - (take 30 done))))])) +(defn view [] + (r/with-let [open? (r/atom false)] + (let [{:keys [done undone]} @(rf/subscribe [::history/steps])] + [:div.menu-wrap.undo + [:button {:disabled (empty? done) :title (if (seq done) (str "undo " (first done) " (⌘Z)") "nothing to undo") + :on-click #(rf/dispatch [::history/undo])} + "undo"] + [:button.undo-list {:disabled (empty? done) :class (when @open? "on") + :title "undo history" :on-click #(swap! open? not)} + "▾"] + [:button {:disabled (empty? undone) :title (if (seq undone) (str "redo " (first undone) " (⇧⌘Z)") "nothing to redo") + :on-click #(rf/dispatch [::history/redo])} + "redo"] + (when (and @open? (seq done)) + [:<> [:div.menu-scrim {:on-click #(reset! open? false)}] + [: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)))]])]))) diff --git a/frontend/test/browser/take.mjs b/frontend/test/browser/take.mjs index 95a38c6..039c147 100644 --- a/frontend/test/browser/take.mjs +++ b/frontend/test/browser/take.mjs @@ -251,7 +251,8 @@ 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.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; b.click(); return true; diff --git a/static/arthur/app.css b/static/arthur/app.css index 16d9dd3..4ae29f5 100644 --- a/static/arthur/app.css +++ b/static/arthur/app.css @@ -155,6 +155,8 @@ audio { display: none; } /* 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 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 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; } -/* 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 { + flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; @@ -341,13 +322,7 @@ button.retry { border-color: var(--sel); color: var(--sel); } color: var(--dim); } -.project-title { - 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-title { max-width: 22vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .project-name { width: 180px; } .export-scrim { position: fixed; inset: 0; z-index: 50; background: rgba(0, 0, 0, .45); } .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); } 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; } /* A menu dropping out of a PANE HEAD. Fixed and placed by `ui/menu` against the