From 776f5abc0acb09e72d6979e0e8bc4d8556b30dc1 Mon Sep 17 00:00:00 2001 From: scheinriese Date: Mon, 9 Mar 2026 17:59:02 +0100 Subject: [PATCH] Replace external toasts with inline undo feedback in shortcut popover Move undo notifications from bottom-right shui toasts into the popover's existing feedback banner system, keeping the user's focus in one place. Add subtle background colors to success and muted feedback variants for visual consistency with error and warning banners. Co-Authored-By: Claude Opus 4.6 --- src/main/frontend/components/shortcut.cljs | 151 +++++++++++---------- src/main/frontend/components/shortcut.css | 18 ++- 2 files changed, 92 insertions(+), 77 deletions(-) diff --git a/src/main/frontend/components/shortcut.cljs b/src/main/frontend/components/shortcut.cljs index d9ce0aa501..f7125b9b68 100644 --- a/src/main/frontend/components/shortcut.cljs +++ b/src/main/frontend/components/shortcut.cljs @@ -359,22 +359,6 @@ (shortcut/persist-user-shortcut! action-id previous-binding)) (js/setTimeout #(do (shortcut/refresh!) (refresh-shortcuts-list!)) 50)) -(defn- show-undo-toast! - [description snapshot set-current-binding! self-id] - (shui/toast! - {:description description - :action (fn [{:keys [dismiss!]}] - [:button.font-medium.underline.cursor-pointer - {:on-click (fn [] - (execute-undo! snapshot) - ;; Update local state if dialog is still open - (when-let [own (some #(when (= (:action-id %) self-id) %) (:entries snapshot))] - (set-current-binding! (:previous-binding own))) - (dismiss!))} - "Undo"]) - :duration 6000} - :default)) - (defn- conflict-action-names "Extract human-readable action names from a key-conflicts map." [key-conflicts] @@ -397,6 +381,7 @@ [key-conflicts set-key-conflicts!] (rum/use-state nil) [rec-state set-rec-state!] (rum/use-state :idle) [accepted-info set-accepted-info!] (rum/use-state nil) + [undo-snapshot set-undo-snapshot!] (rum/use-state nil) *auto-accept-timer (rum/use-ref nil) *fade-timer (rum/use-ref nil) ;; Refs to avoid stale closures in mount-only key handler effect @@ -426,9 +411,8 @@ (set-current-binding! binding) (shortcut/persist-user-shortcut! k nil) (js/setTimeout #(do (shortcut/refresh!) (saved-cb)) 50) - (show-undo-toast! "Reset to default" - {:entries undo-entries} - set-current-binding! k))) + (set-undo-snapshot! {:entries undo-entries}) + (set-rec-state! :reset))) override-fn! (fn [] @@ -462,11 +446,8 @@ (set-current-binding! new-binding) (persist-binding! new-binding) - ;; Undo toast - (show-undo-toast! - (str "Reassigned from " (conflict-action-names conflicts)) - {:entries undo-entries} - set-current-binding! k) + ;; Store undo snapshot for inline undo + (set-undo-snapshot! {:entries undo-entries}) ;; Transition to :accepted with reassign info (set-accepted-info! {:key accepted-key :from (conflict-action-names conflicts)}) @@ -528,10 +509,11 @@ ;; Auto-fade for transient states: conflict-same, esc-hint, accepted (hooks/use-effect! (fn [] - (when (#{:conflict-same :esc-hint :accepted} rec-state) + (when (#{:conflict-same :esc-hint :accepted :removed :reset} rec-state) (let [ms (case rec-state :esc-hint 2000 :accepted (if (:cross-context? accepted-info) 6000 3000) + (:removed :reset) 10000 3000) timer (js/setTimeout #(set-rec-state! :idle) @@ -574,6 +556,8 @@ :idle (close-fn) :accepted (close-fn) :esc-hint (close-fn) + :removed (close-fn) + :reset (close-fn) :recording (do (set-keystroke! "") (set-key-conflicts! nil) (set-rec-state! :esc-hint)) @@ -587,7 +571,7 @@ ;; Backspace in idle/accepted: remove last committed binding (and is-backspace? - (#{:idle :accepted} state) + (#{:idle :accepted :removed :reset} state) (string/blank? (rum/deref *keystroke-ref))) (let [cur-binding (rum/deref *current-binding-ref)] (when (seq (filter string? cur-binding)) @@ -595,10 +579,8 @@ undo-entries [{:action-id k :previous-binding cur-binding}]] (set-current-binding! new-binding) (persist-binding! new-binding) - (set-rec-state! :idle) - (show-undo-toast! "Shortcut removed" - {:entries undo-entries} - set-current-binding! k)))) + (set-undo-snapshot! {:entries undo-entries}) + (set-rec-state! :removed)))) ;; Conflict-cross + Cmd+Enter => override (and is-cmd-enter? (= state :conflict-cross)) @@ -614,8 +596,8 @@ (set-rec-state! :recording) (set-keystroke! (util/trim-safe kn))) - ;; Idle / accepted + key => start recording - (#{:idle :accepted} state) + ;; Idle / accepted / removed / reset + key => start recording + (#{:idle :accepted :removed :reset} state) (when-let [kn (shortcut/keyname e)] (set-rec-state! :recording) (set-keystroke! (util/trim-safe kn))) @@ -652,7 +634,7 @@ :when (string? x)] [:div.shortcut-input-binding {:key x} (shui/shortcut x) - (when (#{:idle :accepted :esc-hint} rec-state) + (when (#{:idle :accepted :esc-hint :removed :reset} rec-state) [:a.shortcut-binding-remove {:on-click (fn [^js e] (.stopPropagation e) @@ -661,10 +643,8 @@ undo-entries [{:action-id k :previous-binding current-binding}]] (set-current-binding! new-binding) (persist-binding! new-binding) - (set-rec-state! :idle) - (show-undo-toast! "Shortcut removed" - {:entries undo-entries} - set-current-binding! k)))} + (set-undo-snapshot! {:entries undo-entries}) + (set-rec-state! :removed)))} (ui/icon "x" {:size 12})])]) ;; Recording in progress — dashed keys (uncommitted) (when (and (#{:recording :conflict-cross :conflict-same} rec-state) @@ -678,57 +658,78 @@ (cancel-fn!))} (ui/icon "x" {:size 12})])]) ;; Placeholder - (when (#{:idle :recording :accepted} rec-state) + (when (#{:idle :recording :accepted :removed :reset} rec-state) [:span.shortcut-input-placeholder "Press a shortcut\u2026"])] ;; FEEDBACK BANNER (conditional) - (case rec-state - :conflict-cross - [:div.shortcut-feedback.shortcut-feedback--error - [:span "Used by " - [:span.shortcut-feedback-name (str "\u201c" (conflict-action-names key-conflicts) "\u201d")]] - (ui/tooltip - (shui/button {:variant :destructive - :size :xs - :on-click override-fn!} - "Reassign") - "Remove from the other action and assign here")] + (let [undo-link + (when undo-snapshot + [:a.shortcut-feedback-action + {:on-click (fn [] + (execute-undo! undo-snapshot) + (when-let [own (some #(when (= (:action-id %) k) %) (:entries undo-snapshot))] + (set-current-binding! (:previous-binding own))) + (set-undo-snapshot! nil) + (set-rec-state! :idle))} + "Undo"])] + (case rec-state + :conflict-cross + [:div.shortcut-feedback.shortcut-feedback--error + [:span "Used by " + [:span.shortcut-feedback-name (str "\u201c" (conflict-action-names key-conflicts) "\u201d")]] + (ui/tooltip + (shui/button {:variant :destructive + :size :xs + :on-click override-fn!} + "Reassign") + "Remove from the other action and assign here")] - :conflict-same - [:div.shortcut-feedback.shortcut-feedback--error - [:span "Already bound to this action"]] + :conflict-same + [:div.shortcut-feedback.shortcut-feedback--error + [:span "Already bound to this action"]] - :accepted - (cond - (:cross-context? accepted-info) - [:div.shortcut-feedback.shortcut-feedback--warning - [:span "Also used for " - [:span.shortcut-feedback-name - (str "\u201c" (:cross-action-name accepted-info) "\u201d")] - (when-let [ctx (:cross-context-label accepted-info)] - (str " in " ctx))]] + :accepted + (cond + (:cross-context? accepted-info) + [:div.shortcut-feedback.shortcut-feedback--warning + [:span "Also used for " + [:span.shortcut-feedback-name + (str "\u201c" (:cross-action-name accepted-info) "\u201d")] + (when-let [ctx (:cross-context-label accepted-info)] + (str " in " ctx))]] - (:from accepted-info) - [:div.shortcut-feedback.shortcut-feedback--success - [:span "Reassigned from " - [:span.shortcut-feedback-name (str "\u201c" (:from accepted-info) "\u201d")]]] + (:from accepted-info) + [:div.shortcut-feedback.shortcut-feedback--success + [:span "Reassigned from " + [:span.shortcut-feedback-name (str "\u201c" (:from accepted-info) "\u201d")]] + undo-link] - :else - [:div.shortcut-feedback.shortcut-feedback--success - [:span "Shortcut added"]]) + :else + [:div.shortcut-feedback.shortcut-feedback--success + [:span "Shortcut added"]]) - :esc-hint - [:div.shortcut-feedback.shortcut-feedback--muted - [:span "Esc is reserved"]] + :removed + [:div.shortcut-feedback.shortcut-feedback--muted + [:span "Shortcut removed"] + undo-link] - nil) + :reset + [:div.shortcut-feedback.shortcut-feedback--muted + [:span "Reset to default"] + undo-link] + + :esc-hint + [:div.shortcut-feedback.shortcut-feedback--muted + [:span "Esc is reserved"]] + + nil)) ;; SEPARATOR + TOOLBAR (shui/separator) [:div.shortcut-toolbar [:div.shortcut-toolbar-left ;; Reset (only when changed from default) - (when (and (#{:idle :accepted} rec-state) + (when (and (#{:idle :accepted :removed} rec-state) (not= current-binding binding)) [:a.shortcut-toolbar-action {:on-click reset-fn!} @@ -740,8 +741,8 @@ [:span.shortcut-toolbar-hint "Reassign " (shui/shortcut (if util/mac? "meta+enter" "ctrl+enter") {:style :compact})]) - ;; Remove hint (idle/accepted with bindings, or conflict states) - (when (or (and (#{:idle :accepted} rec-state) has-bindings?) + ;; Remove hint (idle/accepted/removed/reset with bindings, or conflict states) + (when (or (and (#{:idle :accepted :removed :reset} rec-state) has-bindings?) (#{:conflict-cross :conflict-same} rec-state)) [:span.shortcut-toolbar-hint "Remove " diff --git a/src/main/frontend/components/shortcut.css b/src/main/frontend/components/shortcut.css index da64888d32..96dd789d63 100644 --- a/src/main/frontend/components/shortcut.css +++ b/src/main/frontend/components/shortcut.css @@ -247,12 +247,18 @@ color: var(--rx-red-11, #dc2626); background-color: var(--rx-red-03-alpha, rgb(239 68 68 / 0.08)); } - &--success { color: var(--rx-green-11, #16a34a); } + &--success { + color: var(--rx-green-11, #16a34a); + background-color: var(--rx-green-03-alpha, rgb(34 197 94 / 0.08)); + } &--warning { color: var(--rx-amber-11, #b45309); background-color: var(--rx-amber-03-alpha, hsla(44, 100%, 50%, 0.1)); } - &--muted { color: var(--rx-gray-09, var(--ls-secondary-text-color)); } + &--muted { + color: var(--rx-gray-09, var(--ls-secondary-text-color)); + background-color: var(--rx-gray-03-alpha, rgba(0, 0, 0, 0.04)); + } } .shortcut-feedback-name { @@ -272,6 +278,14 @@ @apply cursor-pointer font-medium whitespace-nowrap; color: var(--rx-red-11, #dc2626); &:hover { text-decoration: underline; } + + .shortcut-feedback--muted & { + color: var(--rx-gray-11, var(--ls-secondary-text-color)); + } + + .shortcut-feedback--success & { + color: var(--rx-green-11, #16a34a); + } } /* Toolbar */