Kaynağa Gözat

Add --lx/--rx color fallbacks for OG theme compatibility

Ensure shortcut component and shui shortcut badges work across both
Radix and legacy Logseq color themes by adding proper fallback chains:

- shortcut.css: Add --lx-* → --rx-* fallbacks for gray scale (08-12),
  --lx-* → --ls-* → --rx-* for background steps (01-04, 06), use
  opacity-based row dimming instead of color-based for theme-agnostic
  muting, and use --color-level-6 for icon link color
- shui.css: Add --rx-* fallbacks to bare --lx-* variables on shortcut
  key badges (background, border, text color, separator)

Co-Authored-By: Claude Opus 4.6 <[email protected]>
scheinriese 1 ay önce
ebeveyn
işleme
ef215dccfc

+ 9 - 9
resources/css/shui.css

@@ -274,8 +274,8 @@ div[data-radix-popper-content-wrapper] {
 /* Combo Keys - simultaneous key combinations with separator */
 .shui-shortcut-combo {
   @apply flex items-start relative rounded;
-  background-color: var(--lx-gray-06-alpha);
-  border: 1px solid rgba(0, 0, 0, 0.1);
+  background-color: var(--lx-gray-06-alpha, var(--rx-gray-06-alpha));
+  border: 1px solid var(--lx-gray-06-alpha, var(--rx-gray-06-alpha));
   box-sizing: border-box;
   white-space: nowrap;
 }
@@ -301,7 +301,7 @@ div[data-radix-popper-content-wrapper] {
 }
 
 .shui-shortcut-separator {
-  background-color: var(--lx-gray-07-alpha);
+  background-color: var(--lx-gray-07-alpha, var(--rx-gray-07-alpha));
   align-self: stretch;
   flex-shrink: 0;
   width: 1px;
@@ -316,8 +316,8 @@ div[data-radix-popper-content-wrapper] {
 
 .shui-shortcut-separate kbd.shui-shortcut-key {
   @apply flex flex-col items-center justify-center px-1 py-0.5 relative rounded shrink-0;
-  background-color: var(--lx-gray-06-alpha);
-  border: 1px solid rgba(0, 0, 0, 0.1);
+  background-color: var(--lx-gray-06-alpha, var(--rx-gray-06-alpha));
+  border: 1px solid var(--lx-gray-06-alpha, var(--rx-gray-06-alpha));
   box-sizing: border-box;
   min-width: 20px;
 }
@@ -329,7 +329,7 @@ div[data-radix-popper-content-wrapper] {
   font-weight: normal;
   line-height: 16px;
   font-style: normal;
-  color: var(--lx-gray-12);
+  color: var(--lx-gray-12, var(--rx-gray-12));
   font-size: 12px;
   text-align: center;
   letter-spacing: -0.5px;
@@ -342,7 +342,7 @@ kbd.shui-shortcut-key,
 .shui-shortcut-key {
   @apply text-xs font-normal h-5 flex items-center justify-center;
   font-family: 'Inter', sans-serif;
-  color: var(--lx-gray-12);
+  color: var(--lx-gray-12, var(--rx-gray-12));
   font-size: 12px;
   text-align: center;
   letter-spacing: -0.5px;
@@ -356,8 +356,8 @@ kbd.shui-shortcut-key,
 /* Keys in separate containers get their own styling */
 .shui-shortcut-separate kbd.shui-shortcut-key {
   @apply rounded;
-  background-color: var(--lx-gray-06-alpha);
-  border: 1px solid rgba(0, 0, 0, 0.1);
+  background-color: var(--lx-gray-06-alpha, var(--rx-gray-06-alpha));
+  border: 1px solid var(--lx-gray-06-alpha, var(--rx-gray-06-alpha));
   box-sizing: border-box;
 }
 

+ 166 - 141
src/main/frontend/components/shortcut.cljs

@@ -143,10 +143,13 @@
                                           (start-commit-timer! kn-trimmed))
                                       ;; During accumulation, append
                                       (let [cur (rum/deref *keystroke-ref)
-                                            new-ks (util/trim-safe (str cur kn))]
-                                        (set-accumulating! true)
-                                        (set-keystroke! new-ks)
-                                        (start-commit-timer! new-ks))))))))))
+                                            parts (string/split (string/trim cur) #" ")
+                                            at-limit? (and (seq (first parts)) (>= (count parts) 5))]
+                                        (when-not at-limit?
+                                          (let [new-ks (util/trim-safe (str cur kn))]
+                                            (set-accumulating! true)
+                                            (set-keystroke! new-ks)
+                                            (start-commit-timer! new-ks))))))))))))
 
          (js/setTimeout #(.focus el) 128)
 
@@ -196,91 +199,99 @@
         "Close " (shui/shortcut "escape" {:style :compact})]]]]))
 
 (rum/defc pane-controls
-  [q set-q! filters set-filters! keystroke set-keystroke! toggle-categories-fn]
-  (let [*search-ref (rum/use-ref nil)]
+  [q set-q! filter-key set-filter-key! keystroke set-keystroke! toggle-categories-fn pill-counts]
+  (let [*search-ref (rum/use-ref nil)
+        in-keystroke? (not (string/blank? keystroke))]
     [:div.cp__shortcut-page-x-pane-controls
 
-     ;; search input — first and widest element
-     [:span.search-input-wrap
-      [:span.search-icon (ui/icon "search" {:size 15})]
-      [:input.form-input.is-small
-       {:placeholder "Search shortcuts..."
-        :ref         *search-ref
-        :value       (or q "")
-        :auto-focus  true
-        :on-key-down #(when (= 27 (.-keyCode %))
-                        (util/stop %)
-                        (if (string/blank? q)
-                          (some-> (rum/deref *search-ref) (.blur))
-                          (set-q! "")))
-        :on-change   #(let [v (util/evalue %)]
-                        (set-q! v))}]
-
-      (when-not (string/blank? q)
-        [:a.x
-         {:on-click (fn []
-                      (set-q! "")
-                      (js/setTimeout #(some-> (rum/deref *search-ref) (.focus)) 50))}
-         (ui/icon "x" {:size 14})])]
-
-     ;; toggle fold/unfold categories
-     [:a.flex.items-center.icon-link
-      {:on-click toggle-categories-fn
-       :title "Toggle categories pane"}
-      (ui/icon "fold")]
-
-     ;; refresh
-     [:a.flex.items-center.icon-link
-      {:on-click refresh-shortcuts-list!
-       :title "Refresh all"}
-      (ui/icon "refresh")]
-
-     ;; keyboard filter
-     (let [filter-popup-id :shortcut-keystroke-filter
-           open-filter! (fn [^js e]
-                          (shui/popup-show!
-                           (.-currentTarget e)
-                           (fn [_]
-                             (keyboard-filter-record-inner
-                              keystroke set-keystroke!
-                              #(shui/popup-hide! filter-popup-id)))
-                           {:id filter-popup-id
-                            :force-popover? true
-                            :align "end"
-                            :content-props
-                            {:class "shortcut-filter-popover-content p-0 w-auto"
-                             :collision-padding 12
-                             :onOpenAutoFocus #(.preventDefault %)
-                             :onCloseAutoFocus #(.preventDefault %)
-                             :onEscapeKeyDown (fn [_] false)
-                             :onPointerDownOutside (fn [_] nil)}}))]
-       [:a.flex.items-center.icon-link.relative
-        {:on-click open-filter!
-         :title "Filter by keystroke"}
-        (ui/icon "keyboard")
-        (when-not (string/blank? keystroke)
-          (ui/point "bg-red-600.absolute" 4 {:style {:right -2 :top -2}}))])
-
-     ;; category filter
-     (ui/dropdown-with-links
-      (fn [{:keys [toggle-fn]}]
-        [:a.flex.items-center.icon-link.relative
-         {:on-click toggle-fn
-          :title "Filter by status"}
-         (ui/icon "filter")
-
-         (when (seq filters)
-           (ui/point "bg-red-600.absolute" 4 {:style {:right -2 :top -2}}))])
-
-      (for [k [:All :Disabled :Unset :Custom]
-            :let [all? (= k :All)
-                  checked? (or (contains? filters k) (and all? (nil? (seq filters))))]]
-
-        {:title   (if all? (t :keymap/all) (t (keyword :keymap (string/lower-case (name k)))))
-         :icon    (ui/icon (if checked? "checkbox" "square"))
-         :options {:on-click #(set-filters! (if all? #{} (let [f (if checked? disj conj)] (f filters k))))}})
-
-      nil)]))
+     ;; Row 1: search + keystroke button
+     [:div.shortcut-toolbar-row
+      [:span.search-input-wrap
+       [:span.search-icon (ui/icon "search" {:size 15})]
+       [:input.form-input.is-small
+        {:placeholder "Search shortcuts..."
+         :ref         *search-ref
+         :value       (or q "")
+         :auto-focus  true
+         :on-key-down #(when (= 27 (.-keyCode %))
+                         (util/stop %)
+                         (if (string/blank? q)
+                           (some-> (rum/deref *search-ref) (.blur))
+                           (set-q! "")))
+         :on-change   #(let [v (util/evalue %)]
+                         (when-not (string/blank? v)
+                           (set-keystroke! ""))
+                         (set-q! v))}]
+
+       (when-not (string/blank? q)
+         [:a.x
+          {:on-click (fn []
+                       (set-q! "")
+                       (js/setTimeout #(some-> (rum/deref *search-ref) (.focus)) 50))}
+          (ui/icon "x" {:size 12})])]
+
+      ;; keystroke filter button
+      (let [filter-popup-id :shortcut-keystroke-filter
+            open-filter! (fn [^js e]
+                           (set-q! "")
+                           (shui/popup-show!
+                            (.-currentTarget e)
+                            (fn [_]
+                              (keyboard-filter-record-inner
+                               keystroke set-keystroke!
+                               #(shui/popup-hide! filter-popup-id)))
+                            {:id filter-popup-id
+                             :force-popover? true
+                             :align "end"
+                             :content-props
+                             {:class "shortcut-filter-popover-content p-0 w-auto"
+                              :collision-padding 12
+                              :onOpenAutoFocus #(.preventDefault %)
+                              :onCloseAutoFocus #(.preventDefault %)
+                              :onEscapeKeyDown (fn [_] false)
+                              :onPointerDownOutside (fn [_] nil)}}))]
+        (if in-keystroke?
+          [:button.shortcut-keystroke-active
+           {:on-click open-filter!}
+           [:span.shortcut-keystroke-keys
+            (ui/icon "keyboard" {:size 14})
+            (shui/shortcut keystroke)]
+           [:a.shortcut-keystroke-clear
+            {:on-click (fn [^js e]
+                         (.stopPropagation e)
+                         (set-keystroke! ""))}
+            (ui/icon "x" {:size 12})]]
+          [:button.shortcut-keystroke-inactive
+           {:on-click open-filter!}
+           (ui/icon "keyboard" {:size 14})
+           [:span "Search by keys"]]))]
+
+     ;; Row 2: filter pills + fold + refresh
+     [:div.shortcut-pills-row
+      [:div.shortcut-filter-pills
+       (for [k [:All :Custom :Unset :Disabled]
+             :let [active? (or (and (= k :All) (nil? filter-key))
+                               (= filter-key k))
+                   cnt (get pill-counts k 0)
+                   title (if (= k :All) "All" (name k))]]
+         [:button.shortcut-filter-pill
+          {:key   (name k)
+           :class (when active? "shortcut-filter-pill--active")
+           :on-click #(set-filter-key! (when-not (or (= k :All) (= filter-key k)) k))}
+          [:span.shortcut-filter-pill-title title]
+          [:span.shortcut-filter-pill-count (str " \u00B7 " cnt)]])]
+
+      (when (string/blank? q)
+        [:div.flex.items-center.gap-2
+         [:a.flex.items-center.icon-link
+          {:on-click toggle-categories-fn
+           :title "Toggle categories pane"}
+          (ui/icon "fold")]
+
+         [:a.flex.items-center.icon-link
+          {:on-click refresh-shortcuts-list!
+           :title "Refresh all"}
+          (ui/icon "refresh")]])]]))
 
 (rum/defc shortcut-desc-label
   [id binding-map]
@@ -759,27 +770,55 @@
         (if (= :recording rec-state) "Cancel " "Close ")
         (shui/shortcut "escape" {:style :compact})]]]]))
 
+(defn- classify-shortcut
+  "Return a set of category keywords (:Custom, :Disabled, :Unset) for a shortcut."
+  [{:keys [binding user-binding]}]
+  (let [binding (to-vector binding)
+        user-binding (and user-binding (to-vector user-binding))
+        custom? (not (nil? user-binding))
+        disabled? (or (false? user-binding)
+                      (false? (first binding)))
+        unset? (and (not disabled?)
+                    (or (= user-binding [])
+                        (and (nil? binding) (nil? user-binding))
+                        (and (= binding [])
+                             (nil? user-binding))))]
+    (cond-> #{}
+      custom? (conj :Custom)
+      disabled? (conj :Disabled)
+      unset? (conj :Unset))))
+
+(defn- count-shortcuts-by-filter
+  "Count shortcuts per filter category in result-list-map.
+   Returns {:All n :Custom n :Unset n :Disabled n}."
+  [result-list-map]
+  (let [all-bindings (mapcat (fn [[_c bm]] (vals bm)) result-list-map)]
+    (reduce (fn [acc m]
+              (let [cats (classify-shortcut m)]
+                (-> acc
+                    (update :All inc)
+                    (cond->
+                     (contains? cats :Custom) (update :Custom inc)
+                     (contains? cats :Disabled) (update :Disabled inc)
+                     (contains? cats :Unset) (update :Unset inc)))))
+            {:All 0 :Custom 0 :Unset 0 :Disabled 0}
+            all-bindings)))
+
 (defn- count-visible-shortcuts
-  "Count shortcuts visible after applying category filters and keystroke filter."
-  [result-list-map filters in-keystroke? keystroke]
+  "Count shortcuts visible after applying category filter and keystroke filter."
+  [result-list-map filter-key in-keystroke? keystroke]
   (->> result-list-map
        (mapcat
         (fn [[_c binding-map]]
-          (for [[id {:keys [binding user-binding]}] binding-map
-                :let [binding (to-vector binding)
-                      user-binding (and user-binding (to-vector user-binding))
-                      custom? (not (nil? user-binding))
-                      disabled? (or (false? user-binding)
-                                    (false? (first binding)))
-                      unset? (and (not disabled?)
-                                  (or (= user-binding [])
-                                      (and (nil? binding) (nil? user-binding))
-                                      (and (= binding [])
-                                           (nil? user-binding))))]
-                :when (or (nil? (seq filters))
-                          (when (contains? filters :Custom) custom?)
-                          (when (contains? filters :Disabled) disabled?)
-                          (when (contains? filters :Unset) unset?))
+          (for [[id m] binding-map
+                :let [cats (classify-shortcut m)
+                      binding (to-vector (:binding m))
+                      user-binding (and (:user-binding m) (to-vector (:user-binding m)))
+                      disabled? (contains? cats :Disabled)
+                      unset? (contains? cats :Unset)]
+                :when (or (= filter-key :All)
+                          (nil? filter-key)
+                          (contains? cats filter-key))
                 :when (or (not in-keystroke?)
                           (and (not disabled?) (not unset?)
                                (let [binding' (or user-binding binding)
@@ -803,13 +842,13 @@
         _ (r/use-atom shortcut-config/*category)
         _ (r/use-atom *refresh-sentry)
         [ready?, set-ready!] (rum/use-state false)
-        [filters, set-filters!] (rum/use-state #{})
+        [filter-key, set-filter-key!] (rum/use-state nil)
         [keystroke, set-keystroke!] (rum/use-state "")
         [q set-q!] (rum/use-state nil)
 
         categories-list-map (build-categories-map)
         all-categories (into #{} (map first categories-list-map))
-        in-filters? (boolean (seq filters))
+        in-filter? (some? filter-key)
         in-query? (not (string/blank? (util/trim-safe q)))
         in-keystroke? (not (string/blank? keystroke))
 
@@ -830,23 +869,11 @@
                               (set-folded-categories! #{})
                               (set-folded-categories! all-categories))
 
-        total-count (apply + (map #(count (second %)) categories-list-map))
-        visible-count (if (or in-filters? in-keystroke?)
-                        (count-visible-shortcuts result-list-map filters in-keystroke? keystroke)
+        pill-counts (count-shortcuts-by-filter result-list-map)
+        visible-count (if (or in-filter? in-keystroke?)
+                        (count-visible-shortcuts result-list-map filter-key in-keystroke? keystroke)
                         (apply + (map #(count (second %)) result-list-map)))
-        shortcuts-word (fn [n] (if (= n 1) "shortcut" "shortcuts"))
-        filter-qualifier (cond
-                           (and in-filters? (contains? filters :Custom)) "custom "
-                           (and in-filters? (contains? filters :Disabled)) "disabled "
-                           (and in-filters? (contains? filters :Unset)) "unset "
-                           :else "")
-        status-text (cond
-                      (not ready?) "..."
-                      (zero? visible-count) "No matching shortcuts"
-                      (or in-query? in-keystroke? in-filters?)
-                      (str visible-count " " filter-qualifier (shortcuts-word visible-count))
-                      :else
-                      (str total-count " " (shortcuts-word total-count)))]
+        no-results? (and ready? (zero? visible-count))]
 
     (hooks/use-effect!
      (fn []
@@ -868,21 +895,25 @@
                  (let [h (.-offsetHeight header)]
                    (.setProperty (.-style el) "--shortcut-header-h" (str h "px"))))))}
      [:header
-      (pane-controls q set-q! filters set-filters! keystroke set-keystroke! toggle-categories!)
-      [:div.shortcut-status-line status-text]]
+      (pane-controls q set-q! filter-key set-filter-key! keystroke set-keystroke! toggle-categories! pill-counts)]
 
      [:article
       (when-not ready?
         [:p.py-8.flex.justify-center (ui/loading "")])
 
-      (when ready?
+      (when (and ready? no-results?)
+        [:div.shortcut-empty-state
+         (ui/icon "search" {:size 24})
+         [:span "No matching shortcuts"]])
+
+      (when (and ready? (not no-results?))
         [:ul.list-none.m-0.py-3
          (for [[c binding-map] result-list-map
                :let [folded? (contains? folded-categories c)]]
            [:<>
             ;; category row
             (when (and (not in-query?)
-                       (not in-filters?)
+                       (not in-filter?)
                        (not in-keystroke?))
               [:li.flex.justify-between.th
                {:key      (str c)
@@ -892,25 +923,19 @@
                [:i.flex.items-center
                 (ui/icon (if folded? "chevron-left" "chevron-down"))]])
 
-            ;; binding row
-            (when (or in-query? in-filters? (not folded?))
+            ;; binding rows
+            (when (or in-query? in-filter? (not folded?))
               (for [[id {:keys [binding user-binding] :as m}] binding-map
                     :let [binding (to-vector binding)
                           user-binding (and user-binding (to-vector user-binding))
                           label (shortcut-desc-label id m)
-                          custom? (not (nil? user-binding))
-                          disabled? (or (false? user-binding)
-                                        (false? (first binding)))
-                          unset? (and (not disabled?)
-                                      (or (= user-binding [])
-                                          (and (nil? binding) (nil? user-binding))
-                                          (and (= binding [])
-                                               (nil? user-binding))))]]
-
-                (when (or (nil? (seq filters))
-                          (when (contains? filters :Custom) custom?)
-                          (when (contains? filters :Disabled) disabled?)
-                          (when (contains? filters :Unset) unset?))
+                          cats (classify-shortcut m)
+                          custom? (contains? cats :Custom)
+                          disabled? (contains? cats :Disabled)
+                          unset? (contains? cats :Unset)]]
+
+                (when (or (nil? filter-key)
+                          (contains? cats filter-key))
 
                   ;; keystrokes filter
                   (when (or (not in-keystroke?)

+ 134 - 27
src/main/frontend/components/shortcut.css

@@ -17,7 +17,11 @@
   @apply relative;
 
   &-pane-controls {
-    @apply flex gap-3 items-center;
+    @apply flex flex-col gap-2;
+
+    .shortcut-toolbar-row {
+      @apply flex gap-3 items-center;
+    }
 
     .search-input-wrap {
       @apply relative flex-1 min-w-0;
@@ -30,18 +34,23 @@
 
       input.form-input {
         @apply w-full pl-7 text-sm mt-0;
+        border-radius: 6px;
 
         &:focus {
           @apply outline-none border-gray-04 ring-2 ring-ring ring-offset-2;
-          --tw-ring-offset-color: var(--ls-primary-background-color, hsl(var(--background)));
+          --tw-ring-offset-color: var(--lx-gray-01, var(--ls-primary-background-color, var(--rx-gray-01)));
         }
       }
 
       a.x {
-        @apply flex items-center absolute right-1 px-1 opacity-60
-        hover:opacity-90;
+        @apply flex items-center absolute right-1 px-1 cursor-pointer;
         top: 50%;
         transform: translateY(-50%);
+        color: var(--lx-gray-09, var(--rx-gray-09));
+
+        &:hover {
+          color: var(--lx-gray-12, var(--rx-gray-12));
+        }
       }
     }
 
@@ -50,22 +59,115 @@
     }
 
     a.icon-link {
-      @apply opacity-80 hover:opacity-100 active:opacity-40 select-none;
+      @apply hover:opacity-80 active:opacity-40 select-none;
+
+      color: var(--lx-gray-09, var(--color-level-6, var(--rx-gray-09)));
+    }
+
+    .shortcut-pills-row {
+      @apply flex items-center justify-between gap-2;
+    }
+
+    .shortcut-filter-pills {
+      @apply flex items-center gap-1 flex-wrap;
+    }
+
+    .shortcut-filter-pill {
+      @apply text-xs px-2 py-0.5 rounded-full cursor-pointer select-none
+      border;
+      color: var(--lx-gray-09, var(--rx-gray-09));
+      background-color: transparent;
+      border-color: var(--lx-gray-06, var(--ls-quaternary-background-color, var(--rx-gray-06)));
+      transition: all 100ms ease;
+
+      &:hover {
+        background-color: var(--lx-gray-05-alpha, var(--rx-gray-05-alpha));
+        color: var(--lx-gray-12, var(--rx-gray-12));
+      }
+
+      &-title {
+        font-weight: 400;
+      }
+
+      &-count {
+        font-weight: 400;
+        opacity: 0.6;
+      }
+
+      &--active {
+        background-color: var(--lx-gray-06-alpha, var(--rx-gray-06-alpha));
+        border-color: transparent;
+        color: var(--lx-gray-12, var(--rx-gray-12));
+
+        .shortcut-filter-pill-title {
+          font-weight: 500;
+        }
+      }
+    }
+
+    .shortcut-keystroke-inactive,
+    .shortcut-keystroke-active {
+      @apply flex items-center gap-1.5 text-sm cursor-pointer
+      select-none flex-shrink-0;
+      height: 30px;
+      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)));
+      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;
+      outline: none;
+
+      &:hover {
+        background-color: var(--lx-gray-04, var(--ls-quaternary-background-color, var(--rx-gray-04)));
+      }
 
-      color: var(--ls-secondary-text-color);
+      &:focus-visible {
+        @apply ring-2 ring-ring ring-offset-2;
+        --tw-ring-offset-color: var(--lx-gray-01, var(--ls-primary-background-color, var(--rx-gray-01)));
+      }
+    }
+
+    .shortcut-keystroke-active {
+      max-width: 50%;
+
+      .shui-shortcut-key {
+        animation: shortcut-badge-in 150ms ease-out;
+      }
+    }
+
+    .shortcut-keystroke-keys {
+      @apply flex items-center gap-1.5;
+      flex: 1;
+      min-width: 0;
+      overflow: hidden;
+      mask-image: linear-gradient(to right, black 80%, transparent);
+      -webkit-mask-image: linear-gradient(to right, black 80%, transparent);
+    }
+
+    .shortcut-keystroke-clear {
+      @apply flex items-center cursor-pointer;
+      color: var(--lx-gray-09, var(--rx-gray-09));
+      margin-left: auto;
+
+      &:hover {
+        color: var(--lx-gray-12, var(--rx-gray-12));
+      }
     }
 
   }
 
   > header {
-    @apply px-4 pb-2 flex flex-col gap-2 sticky top-0 z-10;
+    @apply px-4 flex flex-col gap-2 sticky top-0 z-10;
     padding-top: 20px;
+    padding-bottom: 20px;
     background-color: hsl(var(--background));
   }
 
-  .shortcut-status-line {
-    @apply text-xs select-none;
-    color: var(--rx-gray-09, var(--ls-secondary-text-color));
+  .shortcut-empty-state {
+    @apply flex flex-col items-center justify-center gap-2 py-16 select-none;
+    color: var(--lx-gray-09, var(--rx-gray-09));
   }
 
   > article {
@@ -79,10 +181,10 @@
 
         &.shortcut-row {
           @apply rounded-md cursor-pointer select-none py-1 px-2 -mx-1;
-          transition: background-color 100ms ease, color 100ms ease;
+          transition: background-color 100ms ease, opacity 100ms ease;
 
           &:hover {
-            background-color: var(--rx-gray-04, rgba(255, 255, 255, 0.06));
+            background-color: var(--lx-gray-04-alpha, var(--rx-gray-04-alpha));
           }
 
           &:active {
@@ -91,7 +193,7 @@
 
           /* Active row (popover open) — stronger than hover so it stays visible */
           &.active {
-            background-color: var(--rx-gray-05, rgba(255, 255, 255, 0.1));
+            background-color: var(--lx-gray-05-alpha, var(--rx-gray-05-alpha));
           }
         }
 
@@ -100,7 +202,7 @@
           select-none active:opacity-80 px-2 py-1 z-[1];
           top: var(--shortcut-header-h, 0px);
 
-          background-color: var(--ls-tertiary-background-color);
+          background-color: var(--lx-gray-03, var(--ls-tertiary-background-color, var(--rx-gray-03)));
         }
 
         .label-wrap {
@@ -135,15 +237,15 @@
 
       /* CSS-only hover dimming: dim all rows except hovered */
       &:hover li.shortcut-row:not(:hover):not(.active) {
-        color: var(--rx-gray-11);
+        opacity: 0.5;
       }
 
       /* When popover is open: dim non-active rows, but restore on hover */
       &:has(.active) li.shortcut-row:not(.active) {
-        color: var(--rx-gray-11);
+        opacity: 0.5;
 
         &:hover {
-          color: var(--rx-gray-12);
+          opacity: 0.85;
         }
       }
     }
@@ -179,7 +281,7 @@
   @apply px-4 font-medium text-xs select-none;
   padding-top: 16px;
   padding-bottom: 2px;
-  color: var(--rx-gray-11, var(--ls-secondary-text-color));
+  color: var(--lx-gray-11, var(--rx-gray-11));
 }
 
 /* Input field — borderless, content sits directly on popover surface */
@@ -214,15 +316,15 @@
 /* Each binding grouped in a subtle container */
 .shortcut-input-binding {
   @apply inline-flex items-center flex-wrap rounded-md p-1;
-  background-color: var(--rx-gray-04-alpha, rgba(0, 0, 0, 0.07));
+  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;
-    color: var(--rx-gray-10);
+    color: var(--lx-gray-10, var(--rx-gray-10));
 
     &:hover {
-      color: var(--rx-gray-12);
+      color: var(--lx-gray-12, var(--rx-gray-12));
     }
   }
 }
@@ -274,8 +376,8 @@
     background-color: var(--rx-amber-03-alpha, hsla(44, 100%, 50%, 0.1));
   }
   &--muted {
-    color: var(--rx-gray-09, var(--ls-secondary-text-color));
-    background-color: var(--rx-gray-03-alpha, rgba(0, 0, 0, 0.04));
+    color: var(--lx-gray-09, var(--rx-gray-09));
+    background-color: var(--lx-gray-03-alpha, var(--rx-gray-03-alpha));
   }
 }
 
@@ -298,7 +400,7 @@
   &:hover { text-decoration: underline; }
 
   .shortcut-feedback--muted & {
-    color: var(--rx-gray-11, var(--ls-secondary-text-color));
+    color: var(--lx-gray-11, var(--rx-gray-11));
   }
 
   .shortcut-feedback--success & {
@@ -309,13 +411,13 @@
 /* Toolbar */
 .shortcut-toolbar {
   @apply flex items-center justify-between px-4 py-1.5 text-xs select-none;
-  color: var(--rx-gray-08, rgba(0, 0, 0, 0.4));
+  color: var(--lx-gray-08, var(--rx-gray-08));
   margin-top: auto;
 }
 
 .shortcut-toolbar-action {
   @apply cursor-pointer flex items-center gap-1;
-  &:hover { color: var(--rx-gray-12, var(--ls-primary-text-color)); }
+  &:hover { color: var(--lx-gray-12, var(--rx-gray-12)); }
 }
 
 .shortcut-toolbar-hint {
@@ -324,6 +426,11 @@
 }
 
 /* Animations */
+@keyframes shortcut-badge-in {
+  from { opacity: 0; transform: scale(0.85); }
+  to { opacity: 1; transform: scale(1); }
+}
+
 @keyframes shortcut-fade-in {
   from { opacity: 0; transform: translateY(4px); }
   to { opacity: 1; transform: translateY(0); }
@@ -376,7 +483,7 @@
 
 .sidebar-item .cp__shortcut-page-x {
   padding: 12px 0 0 0;
-  background-color: var(--color-level-2);
+  background-color: var(--color-level-2, var(--lx-gray-02, var(--rx-gray-02)));
 }
 
 .sidebar-item article {