From 4dcdbcc44cd2bc85e063e3855c6f195844c79e2b Mon Sep 17 00:00:00 2001 From: scheinriese Date: Wed, 11 Mar 2026 17:11:55 +0100 Subject: [PATCH] Replace flat row flash with accent shimmer sweep and fix combo key press animation - Row highlight on shortcut trigger now uses a horizontal gradient sweep (background-position animation) instead of a static background flash, providing a distinct visual language from the focus ring - Shimmer uses theme-aware accent color via color-mix on keymap page, with a neutral fallback in shui.css base styles - Guard against animation spam: clearTimeout+reset pattern prevents stale timeout accumulation during rapid key repeat; reflow only forced on first trigger, class stays applied until last trigger ends - Fix combo key press animation: animate the container (the keycap) instead of individual kbd elements, so translateY and box-shadow follow the container's border-radius correctly - Scope row shimmer to .shui-shortcut-row/.shortcut-row elements to prevent accidental shimmer on standalone badge containers - Respect prefers-reduced-motion for all new animations Co-Authored-By: Claude Opus 4.6 --- deps/shui/src/logseq/shui/shortcut.cljs | 62 +++++++++++++++++------ resources/css/shui.css | 58 ++++++++++++++------- src/main/frontend/components/shortcut.css | 19 ++++++- 3 files changed, 104 insertions(+), 35 deletions(-) diff --git a/deps/shui/src/logseq/shui/shortcut.cljs b/deps/shui/src/logseq/shui/shortcut.cljs index 4a52a2ade7..624a73fbec 100644 --- a/deps/shui/src/logseq/shui/shortcut.cljs +++ b/deps/shui/src/logseq/shui/shortcut.cljs @@ -157,30 +157,58 @@ %))))))) (def ^:private press-animation-ms 160) +(def ^:private row-shimmer-ms 750) (defn- highlight-row! - "Add or remove the row highlight class on the closest shortcut row ancestor." + "Add or remove the row highlight class on the closest shortcut row ancestor. + On first trigger, forces a reflow to start the CSS animation. + On repeated triggers, the class stays applied (no reflow) and the + removal timer is reset so it fires after the last trigger." [^js container add?] (when-let [^js row (or (.closest container ".shui-shortcut-row, .shortcut-row") (.-parentElement container))] (if add? - (.add (.-classList row) "shui-shortcut-row--pressed") - (.remove (.-classList row) "shui-shortcut-row--pressed")))) + (let [already? (.contains (.-classList row) "shui-shortcut-row--pressed")] + ;; Cancel any pending removal + (when-let [t (.-__sweepTimer row)] + (js/clearTimeout t) + (set! (.-__sweepTimer row) nil)) + ;; Only force reflow on first trigger to start the animation + (when-not already? + (.add (.-classList row) "shui-shortcut-row--pressed")) + ;; Schedule removal after the last trigger + (set! (.-__sweepTimer row) + (js/setTimeout + (fn [] + (.remove (.-classList row) "shui-shortcut-row--pressed") + (set! (.-__sweepTimer row) nil)) + row-shimmer-ms))) + (do + (when-let [t (.-__sweepTimer row)] + (js/clearTimeout t) + (set! (.-__sweepTimer row) nil)) + (.remove (.-classList row) "shui-shortcut-row--pressed"))))) (defn- animate-element! - "Add pressed class, optionally highlight row, then auto-reset after animation." + "Add pressed class, optionally highlight row, then auto-reset after animation. + Key badge uses a simple clearTimeout+reset pattern to avoid stale removals." [^js el ^js container highlight-row?] (.add (.-classList el) "shui-shortcut-key-pressed") (when highlight-row? (highlight-row! container true)) - (js/setTimeout - (fn [] - (.remove (.-classList el) "shui-shortcut-key-pressed") - (when highlight-row? (highlight-row! container false))) - press-animation-ms)) + ;; Clear any pending badge removal, then schedule a new one + (when-let [t (.-__badgeTimer el)] + (js/clearTimeout t)) + (set! (.-__badgeTimer el) + (js/setTimeout + (fn [] + (.remove (.-classList el) "shui-shortcut-key-pressed") + (set! (.-__badgeTimer el) nil)) + press-animation-ms))) (defn shortcut-press! "Central helper to trigger key press animation. - Finds all nodes with matching data-shortcut-binding and animates individual keys. + For combo keys, animates the container (the whole keycap depresses). + For separate keys, animates individual kbd elements. Optionally highlights parent row. Args: @@ -192,11 +220,15 @@ selector (str "[data-shortcut-binding=\"" normalized "\"]") containers (.querySelectorAll js/document selector)] (doseq [^js container (array-seq containers)] - (let [keys (.querySelectorAll container "kbd.shui-shortcut-key")] - (if (> (.-length keys) 0) - (doseq [^js key-el (array-seq keys)] - (animate-element! key-el container highlight-row?)) - (animate-element! container container highlight-row?))))))) + (if (.contains (.-classList container) "shui-shortcut-combo") + ;; Combo keys: animate the container as a unit (one keycap) + (animate-element! container container highlight-row?) + ;; Separate keys: animate each kbd individually + (let [keys (.querySelectorAll container "kbd.shui-shortcut-key")] + (if (> (.-length keys) 0) + (doseq [^js key-el (array-seq keys)] + (animate-element! key-el container highlight-row?)) + (animate-element! container container highlight-row?)))))))) (rum/defc combo-keys "Renders combo keys (simultaneous key combinations) with separator." diff --git a/resources/css/shui.css b/resources/css/shui.css index c7f2444d99..eed6fc75e7 100644 --- a/resources/css/shui.css +++ b/resources/css/shui.css @@ -279,6 +279,7 @@ div[data-radix-popper-content-wrapper] { box-sizing: border-box; white-space: nowrap; min-width: 0; + transition: transform 140ms ease-out, box-shadow 140ms ease-out; } /* Glow effect for combo and separate keys */ @@ -377,33 +378,52 @@ kbd.shui-shortcut-key, color: hsl(var(--primary-foreground)); } -/* Key press animation */ +/* Key press animation — separate keys: animate individual kbd elements */ kbd.shui-shortcut-key-pressed, .shui-shortcut-key-pressed { transform: translateY(1px); } -/* Key press animation with glow - preserve glow effect */ -/* Combo keys: animate the container */ -.shui-shortcut-combo.shui-shortcut-glow.shui-shortcut-key-pressed { - box-shadow: rgba(255, 255, 255, 0.15) 0px 2px 0px 0px inset, rgba(0, 0, 0, 0.15) 0px 0px 0px 0px inset, 0 1px 2px rgba(0, 0, 0, 0.2); -} - -/* Separate keys: animate individual keys */ .shui-shortcut-separate.shui-shortcut-glow kbd.shui-shortcut-key-pressed { box-shadow: rgba(255, 255, 255, 0.15) 0px 2px 0px 0px inset, rgba(0, 0, 0, 0.15) 0px 0px 0px 0px inset, 0 1px 2px rgba(0, 0, 0, 0.2); } -/* Key press animation without glow */ -.shui-shortcut-combo:not(.shui-shortcut-glow) kbd.shui-shortcut-key-pressed, .shui-shortcut-separate:not(.shui-shortcut-glow) kbd.shui-shortcut-key-pressed { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); } -/* Row highlight animation */ -.shui-shortcut-row--pressed { - background-color: rgba(223, 239, 254, 0.1); - transition: background-color 160ms ease-out; +/* Key press animation — combo keys: animate the container (the whole keycap) */ +.shui-shortcut-combo.shui-shortcut-key-pressed { + transform: translateY(1px); +} + +.shui-shortcut-combo.shui-shortcut-glow.shui-shortcut-key-pressed { + box-shadow: rgba(255, 255, 255, 0.15) 0px 2px 0px 0px inset, rgba(0, 0, 0, 0.15) 0px 0px 0px 0px inset, 0 1px 2px rgba(0, 0, 0, 0.2); +} + +.shui-shortcut-combo:not(.shui-shortcut-glow).shui-shortcut-key-pressed { + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} + +/* Row highlight animation — accent band sweep (only on actual row elements) */ +.shui-shortcut-row.shui-shortcut-row--pressed, +.shortcut-row.shui-shortcut-row--pressed { + background-image: linear-gradient( + 90deg, + transparent 0%, + transparent 35%, + rgba(223, 239, 254, 0.08) 50%, + transparent 65%, + transparent 100% + ); + background-size: 300% 100%; + background-repeat: no-repeat; + animation: shortcut-row-sweep 700ms ease-out forwards; +} + +@keyframes shortcut-row-sweep { + from { background-position: -50% 0; } + to { background-position: 150% 0; } } /* Ensure consistent height for shortcut containers */ @@ -417,14 +437,16 @@ kbd.shui-shortcut-key-pressed, @media (prefers-reduced-motion: reduce) { kbd.shui-shortcut-key, .shui-shortcut-key, - .shui-shortcut-row--pressed { + .shui-shortcut-combo { transition: none; transform: none; box-shadow: 0 0 0 transparent; } - - .shui-shortcut-row--pressed { - background-color: transparent; + + .shui-shortcut-row.shui-shortcut-row--pressed, + .shortcut-row.shui-shortcut-row--pressed { + animation: none !important; + background-image: none !important; } } diff --git a/src/main/frontend/components/shortcut.css b/src/main/frontend/components/shortcut.css index 7826059e82..8d2e6ad932 100644 --- a/src/main/frontend/components/shortcut.css +++ b/src/main/frontend/components/shortcut.css @@ -215,9 +215,19 @@ button.shortcut-feedback-action { background-color: var(--lx-gray-05-alpha, var(--rx-gray-05-alpha)); } - /* Shortcut keypress highlight — briefly flashes the row when the user presses a shortcut */ + /* Shortcut keypress shimmer — accent band sweeps left-to-right across the row */ &.shui-shortcut-row--pressed { - background-color: var(--lx-gray-05-alpha, var(--rx-gray-05-alpha)); + background-image: linear-gradient( + 90deg, + transparent 0%, + transparent 35%, + color-mix(in srgb, var(--lx-accent-09, hsl(var(--primary))) 10%, transparent) 50%, + transparent 65%, + transparent 100% + ); + background-size: 300% 100%; + background-repeat: no-repeat; + animation: shortcut-row-sweep 700ms ease-out forwards; } /* Keyboard navigation focus ring — two levels above hover for clear distinction */ @@ -289,6 +299,11 @@ button.shortcut-feedback-action { } +@keyframes shortcut-row-sweep { + from { background-position: -50% 0; } + to { background-position: 150% 0; } +} + /* === V3 SHORTCUT POPOVER === */ .shortcut-popover { @apply flex flex-col;