From 5b08feecfa62c04edc4b251fddc0f06038f9717c Mon Sep 17 00:00:00 2001 From: scheinriese Date: Tue, 10 Mar 2026 13:19:22 +0100 Subject: [PATCH] Fix chord sequence display, add polish and resilience improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add chord-sequence-keys component to shui shortcut for multi-step bindings (e.g. ⌘C then ⌘R) that were previously truncated to just the first key. Detects nested binding groups and renders each combo with a "then" separator and proper glow styling. - Update normalize-binding to handle chord sequences by joining groups with spaces instead of flattening into a single combo string. - Cap keystroke recording at 5 keys to prevent app crashes from excessively long bindings being persisted to localStorage. - Add try/catch around localStorage shortcut reads for resilience against corrupt data on startup. - Fix vertical centering of shortcut badges using display:contents wrappers and removing padding-top from label-wrap/action-wrap. - Style Reset button with accent color and underline-on-hover. - Add white-space:nowrap to button reset block to prevent text wrapping. - Use gap instead of margin for shortcut-input-binding spacing. - Minor CSS refinements: search input padding, keystroke border color, empty state sizing, toolbar action specificity. Co-Authored-By: Claude Opus 4.6 --- deps/shui/src/logseq/shui/shortcut.cljs | 59 ++++++++++++++++++++-- src/main/frontend/components/shortcut.cljs | 20 +++++--- src/main/frontend/components/shortcut.css | 31 ++++++++---- src/main/frontend/state.cljs | 3 +- 4 files changed, 89 insertions(+), 24 deletions(-) diff --git a/deps/shui/src/logseq/shui/shortcut.cljs b/deps/shui/src/logseq/shui/shortcut.cljs index 02f1de58a3..7b7211ac0c 100644 --- a/deps/shui/src/logseq/shui/shortcut.cljs +++ b/deps/shui/src/logseq/shui/shortcut.cljs @@ -107,8 +107,14 @@ normalized)) (coll? binding) - (let [keys (flatten-keys binding)] - (string/join "+" (map normalize-key keys))) + (if (and (coll? (first binding)) (> (count binding) 1) (every? coll? binding)) + ;; Chord sequence: normalize each group separately, join with space + (string/join " " (map (fn [group] + (string/join "+" (map normalize-key (flatten-keys group)))) + binding)) + ;; Single combo group + (let [keys (flatten-keys binding)] + (string/join "+" (map normalize-key keys)))) (keyword? binding) (name binding) @@ -261,6 +267,44 @@ :margin-right "2px"}} key-text])])) +(rum/defc chord-sequence-keys + "Renders a chord sequence (multi-step key combinations) with 'then' separators. + E.g., [['⌘' 'c'] ['⌘' 'r']] renders as: [⌘ C] then [⌘ R]" + [groups binding {:keys [aria-label aria-hidden? glow?]}] + (let [normalized-binding (normalize-binding binding) + container-attrs (cond-> {:class "shui-shortcut-chord" + :data-shortcut-binding normalized-binding + :style {:white-space "nowrap" + :display "inline-flex" + :align-items "center" + :gap "8px"}} + aria-label (assoc :aria-label aria-label) + aria-hidden? (assoc :aria-hidden "true"))] + [:div container-attrs + (for [[gi group] (map-indexed vector groups)] + (list + (when (> gi 0) + [:span.shui-shortcut-chord-sep + {:key (str "chord-sep-" gi) + :style {:font-size "10px" + :opacity 0.45}} + "then"]) + (let [key-elements (map print-shortcut-key group)] + [:span + {:key (str "chord-group-" gi) + :class (str "shui-shortcut-combo" (when glow? " shui-shortcut-glow")) + :style {:display "inline-flex" + :align-items "center" + :white-space "nowrap"}} + (for [[ki key-text] (map-indexed vector key-elements)] + (list + (when (< 0 ki) + [:span.shui-shortcut-separator {:key (str "gsep-" gi "-" ki)}]) + [:kbd.shui-shortcut-key + {:key (str "chord-key-" gi "-" ki) + :aria-hidden (if aria-label "true" "false")} + key-text]))])))])) + (rum/defc root "Main shortcut component with automatic style detection. @@ -287,7 +331,11 @@ :aria-hidden? aria-hidden? :glow? glow?}] (for [[index binding] (map-indexed vector shortcuts)] - (let [detected-style (if (= style :auto) + (let [;; Chord sequence: multiple nested groups like [["⌘" "c"] ["⌘" "r"]] + chord-sequence? (and (coll? binding) + (> (count binding) 1) + (every? coll? binding)) + detected-style (if (= style :auto) (detect-style binding) style) keys (cond @@ -330,10 +378,11 @@ {:key (str "shortcut-" index) :style {:display "inline-flex" :align-items "center" - :min-height "20px" :white-space "nowrap"}} (when (< 0 index) [:span.text-gray-11.text-sm {:key (str "sep-" index) :style {:margin "0 4px"}} "|"]) - (render-fn keys binding-for-data opts)]))))) + (if chord-sequence? + (chord-sequence-keys binding binding-for-data opts) + (render-fn keys binding-for-data opts))]))))) diff --git a/src/main/frontend/components/shortcut.cljs b/src/main/frontend/components/shortcut.cljs index fac1916ef9..c9dfa97308 100644 --- a/src/main/frontend/components/shortcut.cljs +++ b/src/main/frontend/components/shortcut.cljs @@ -617,11 +617,15 @@ (set-rec-state! :recording) (set-keystroke! (util/trim-safe kn))) - ;; Recording + key => accumulate + ;; Recording + key => accumulate (max 5 keys) (= state :recording) (when-let [kn (shortcut/keyname e)] - (set-key-conflicts! nil) - (set-keystroke! #(util/trim-safe (str % kn)))))))) + (let [cur (rum/deref *keystroke-ref) + parts (string/split (string/trim cur) #" ") + at-limit? (and (seq (first parts)) (>= (count parts) 5))] + (when-not at-limit? + (set-key-conflicts! nil) + (set-keystroke! #(util/trim-safe (str % kn)))))))))) (js/setTimeout #(.focus el) 128) @@ -749,7 +753,7 @@ ;; Reset (only when changed from default) (when (and (#{:idle :accepted :removed} rec-state) (not= current-binding binding)) - [:button.shortcut-toolbar-action + [:button.shortcut-toolbar-action.shortcut-toolbar-reset {:on-click reset-fn!} (ui/icon "rotate" {:size 12}) [:span "Reset"]])] @@ -908,8 +912,8 @@ (when (and ready? no-results?) [:div.shortcut-empty-state - (ui/icon "search" {:size 24}) - [:span "No matching shortcuts"]]) + (ui/icon "list-search" {:size 24}) + [:span.text-sm "No matching shortcuts"]]) (when (and ready? (not no-results?)) [:ul.list-none.m-0.py-3 @@ -983,12 +987,12 @@ [:span.shortcut-status-label (t :keymap/disabled)] (for [b user-binding :when (string? b)] - [:span {:key b} + [:span {:key b :style {:display "contents"}} (shui/shortcut b)]))] :else (for [b binding :when (string? b)] - [:span {:key b} + [:span {:key b :style {:display "contents"}} (shui/shortcut (dh/binding-for-display id b) {:raw-binding [b]})]))]])))))])])]])) diff --git a/src/main/frontend/components/shortcut.css b/src/main/frontend/components/shortcut.css index 7321577b29..48bc4d2114 100644 --- a/src/main/frontend/components/shortcut.css +++ b/src/main/frontend/components/shortcut.css @@ -28,6 +28,7 @@ button.shortcut-feedback-action { cursor: pointer; display: inline-flex; align-items: center; + white-space: nowrap; } .cp__shortcut-page-x { @@ -50,7 +51,7 @@ button.shortcut-feedback-action { } input.form-input { - @apply w-full pl-7 text-sm mt-0; + @apply w-full pl-7 pr-7 text-sm mt-0; border-radius: 6px; &:focus { @@ -59,7 +60,7 @@ button.shortcut-feedback-action { } } - a.x { + .x { @apply flex items-center absolute right-1 px-1 cursor-pointer; top: 50%; transform: translateY(-50%); @@ -130,7 +131,7 @@ button.shortcut-feedback-action { padding: 0 10px; min-width: 140px; border-radius: 6px; - border: 1px solid var(--lx-gray-06, var(--ls-quaternary-background-color, var(--rx-gray-06))); + border: 1px solid var(--lx-gray-07, var(--ls-quaternary-background-color, var(--rx-gray-07))); background-color: var(--lx-gray-02, var(--ls-secondary-background-color, var(--rx-gray-02))); color: var(--lx-gray-12, var(--rx-gray-12)); transition: background-color 150ms ease; @@ -183,7 +184,8 @@ button.shortcut-feedback-action { } .shortcut-empty-state { - @apply flex flex-col items-center justify-center gap-2 py-16 select-none; + @apply flex flex-col items-center justify-center gap-2 select-none; + min-height: calc(60dvh - var(--shortcut-header-h, 120px)); color: var(--lx-gray-09, var(--rx-gray-09)); } @@ -225,7 +227,6 @@ button.shortcut-feedback-action { .label-wrap { @apply flex flex-1; min-width: 120px; - padding-top: 1px; } .action-wrap { @@ -234,7 +235,6 @@ button.shortcut-feedback-action { column-gap: 16px; row-gap: 6px; max-width: 55%; - padding-top: 1px; &.disabled { @apply opacity-60 cursor-default; @@ -333,11 +333,12 @@ button.shortcut-feedback-action { /* Each binding grouped in a subtle container */ .shortcut-input-binding { @apply inline-flex items-center flex-wrap rounded-md p-1; + gap: 4px; background-color: var(--lx-gray-04-alpha, var(--rx-gray-04-alpha)); max-width: 100%; .shortcut-binding-remove { - @apply flex items-center ml-1 cursor-pointer select-none; + @apply flex items-center cursor-pointer select-none; color: var(--lx-gray-10, var(--rx-gray-10)); &:hover { @@ -432,9 +433,19 @@ button.shortcut-feedback-action { margin-top: auto; } -.shortcut-toolbar-action { - @apply cursor-pointer flex items-center gap-1; - &:hover { color: var(--lx-gray-12, var(--rx-gray-12)); } +.shortcut-toolbar button.shortcut-toolbar-action { + gap: 4px; + &:hover { + color: var(--lx-gray-12, var(--rx-gray-12)); + text-decoration: underline; + } +} + +.shortcut-toolbar button.shortcut-toolbar-reset { + color: var(--lx-accent-11, var(--ls-link-text-color, hsl(var(--primary) / 0.8))); + &:hover { + color: var(--lx-accent-12, var(--ls-link-text-hover-color, hsl(var(--primary)))); + } } .shortcut-toolbar-hint { diff --git a/src/main/frontend/state.cljs b/src/main/frontend/state.cljs index b22ad16ce9..61814c5206 100644 --- a/src/main/frontend/state.cljs +++ b/src/main/frontend/state.cljs @@ -475,7 +475,8 @@ should be done through this fn in order to get global config and config defaults "MMM do, yyyy")) (defn custom-shortcuts [] - (merge (storage/get :ls-shortcuts) + (merge (try (storage/get :ls-shortcuts) + (catch :default _ nil)) (:shortcuts (get-config)))) (defn get-commands