Fix chord sequence display, add polish and resilience improvements

- 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 <noreply@anthropic.com>
This commit is contained in:
scheinriese
2026-03-10 13:19:22 +01:00
committed by Tienson Qin
parent e09230476d
commit 5b08feecfa
4 changed files with 89 additions and 24 deletions

View File

@@ -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))])))))

View File

@@ -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]})]))]])))))])])]]))

View File

@@ -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 {

View File

@@ -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