Просмотр исходного кода

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 <[email protected]>
scheinriese 1 месяц назад
Родитель
Сommit
4dcdbcc44c

+ 47 - 15
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."

+ 39 - 17
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 {
+.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);
 }
 
-/* Separate keys: animate individual keys */
-.shui-shortcut-separate.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);
+}
+
+/* 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);
 }
 
-/* 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 {
+.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 */
-.shui-shortcut-row--pressed {
-  background-color: rgba(223, 239, 254, 0.1);
-  transition: background-color 160ms ease-out;
+/* 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;
   }
 }
 

+ 17 - 2
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;