From e2ed6a488d6c36749847b8b74129795b2a606a97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?= Date: Thu, 21 May 2026 12:10:36 +0200 Subject: [PATCH] :sparkles: Polish workspace find and replace UX (#9687) * :sparkles: Polish workspace find and replace UX Co-authored-by: Cursor * :sparkles: Add toggle mode button This button toggles between search and search and replace modes * :recycle: Refactor and CSS cleanup --------- Co-authored-by: Cursor Co-authored-by: Luis de Dios --- frontend/src/app/main/data/workspace.cljs | 47 +- .../app/main/data/workspace/highlight.cljs | 26 + .../src/app/main/data/workspace/texts.cljs | 30 +- .../app/main/ui/components/search_bar.cljs | 9 +- .../src/app/main/ui/workspace/main_menu.cljs | 4 +- .../main/ui/workspace/sidebar/layer_item.cljs | 5 +- .../app/main/ui/workspace/sidebar/layers.cljs | 549 +++++++++++------- .../app/main/ui/workspace/sidebar/layers.scss | 431 +++++--------- .../main/ui/workspace/sidebar/shortcuts.cljs | 3 + frontend/translations/en.po | 21 +- frontend/translations/es.po | 11 + 11 files changed, 618 insertions(+), 518 deletions(-) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index d61ed1fded..98d66dc593 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1493,18 +1493,47 @@ (update [_ state] (assoc-in state [:workspace-global :clipboard-style] style)))) -(defn open-layers-search +(defn- layers-search-config [mode] - (ptk/reify ::open-layers-search - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-local :layers-panel-search] mode)))) + {:open? true + :mode mode + :scope (if (= mode :find-and-replace) :canvas :layers) + :find-replace-mode? (= mode :find-and-replace)}) -(def clear-layers-search - (ptk/reify ::clear-layers-search +(defn- layers-search-active? + [current target] + (and (:open? current false) + (= (:scope current) (:scope target)) + (= (:find-replace-mode? current) (:find-replace-mode? target)))) + +(defn open-layers-search + ([mode] (open-layers-search mode nil)) + ([mode options] + (let [force? (boolean (:force? options))] + (ptk/reify ::open-layers-search + ptk/UpdateEvent + (update [_ state] + (let [target (layers-search-config mode) + current (get-in state [:workspace-local :layers-search])] + (if (and (not force?) + (layers-search-active? current target)) + (update state :workspace-local dissoc :layers-search) + (assoc-in state [:workspace-local :layers-search] target)))))))) + +(def close-layers-search + (ptk/reify ::close-layers-search ptk/UpdateEvent (update [_ state] - (update state :workspace-local dissoc :layers-panel-search)))) + (update state :workspace-local dissoc :layers-search)))) + +(defn update-layers-search-scope + [scope] + (ptk/reify ::update-layers-search-scope + ptk/UpdateEvent + (update [_ state] + (if (get-in state [:workspace-local :layers-search]) + (assoc-in state [:workspace-local :layers-search :scope] scope) + state)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Exports @@ -1565,6 +1594,8 @@ ;; Highlight (dm/export dwh/highlight-shape) (dm/export dwh/dehighlight-shape) +(dm/export dwh/set-search-match-highlight) +(dm/export dwh/clear-search-match-highlight) ;; Shape flags (dm/export dwsh/update-shape-flags) diff --git a/frontend/src/app/main/data/workspace/highlight.cljs b/frontend/src/app/main/data/workspace/highlight.cljs index 6f91445bae..4cb252dffd 100644 --- a/frontend/src/app/main/data/workspace/highlight.cljs +++ b/frontend/src/app/main/data/workspace/highlight.cljs @@ -27,3 +27,29 @@ ptk/UpdateEvent (update [_ state] (update-in state [:workspace-local :highlighted] disj id)))) + +(defn set-search-match-highlight + "Highlight the active find/replace match on canvas and sidebar." + [current-id match-ids] + (dm/assert! (uuid? current-id)) + (let [match-ids (set match-ids)] + (ptk/reify ::set-search-match-highlight + ptk/UpdateEvent + (update [_ state] + (let [highlighted (-> (get-in state [:workspace-local :highlighted] #{}) + (set/difference match-ids) + (conj current-id))] + (-> state + (assoc-in [:workspace-local :search-match-highlight] current-id) + (assoc-in [:workspace-local :highlighted] highlighted))))))) + +(defn clear-search-match-highlight + [match-ids] + (let [match-ids (set match-ids)] + (ptk/reify ::clear-search-match-highlight + ptk/UpdateEvent + (update [_ state] + (-> state + (update-in [:workspace-local :highlighted] + #(set/difference (or % #{}) match-ids)) + (update :workspace-local dissoc :search-match-highlight)))))) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index d492a43b4b..6d21a3d129 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -1206,18 +1206,24 @@ [ids search replacement] (ptk/reify ::replace-text-in-shapes ptk/WatchEvent - (watch [_ _ _] - (let [undo-group (uuid/next)] - (rx/of - (dwsh/update-shapes - ids - (fn [shape] - (if (and (= :text (:type shape)) (some? (:content shape))) - (let [new-content (txt/replace-text-in-content (:content shape) search replacement) - new-name (txt/generate-shape-name (txt/content->text new-content))] - (-> shape (assoc :content new-content) (assoc :name new-name))) - shape)) - {:attrs #{:content :name} :undo-group undo-group})))))) + (watch [_ state _] + (let [undo-group (uuid/next) + update-event + (dwsh/update-shapes + ids + (fn [shape] + (if (and (= :text (:type shape)) (some? (:content shape))) + (let [new-content (txt/replace-text-in-content (:content shape) search replacement) + new-name (txt/generate-shape-name (txt/content->text new-content))] + (-> shape (assoc :content new-content) (assoc :name new-name))) + shape)) + {:attrs #{:content :name} :undo-group undo-group})] + (rx/concat + (rx/of update-event) + (if (features/active-feature? state "render-wasm/v1") + (->> (rx/from ids) + (rx/map #(dwwt/resize-wasm-text-debounce % {:undo-group undo-group}))) + (rx/empty))))))) ;; -- Text Editor v3 diff --git a/frontend/src/app/main/ui/components/search_bar.cljs b/frontend/src/app/main/ui/components/search_bar.cljs index bfd50df0ff..90fe0969ac 100644 --- a/frontend/src/app/main/ui/components/search_bar.cljs +++ b/frontend/src/app/main/ui/components/search_bar.cljs @@ -13,7 +13,8 @@ [rumext.v2 :as mf])) (mf/defc search-bar* - [{:keys [id class value placeholder icon-id auto-focus on-change on-clear on-submit children]}] + [{:keys [id class value placeholder icon-id auto-focus input-ref + on-change on-clear on-submit on-key-down children]}] (let [handle-change (mf/use-fn (mf/deps on-change) @@ -31,8 +32,11 @@ handle-key-down (mf/use-fn - (mf/deps on-submit) + (mf/deps on-submit on-key-down) (fn [event] + (when (fn? on-key-down) + (on-key-down event)) + (let [enter? (kbd/enter? event) esc? (kbd/esc? event) node (dom/get-target event)] @@ -53,6 +57,7 @@ :size "s" :class (stl/css :icon)}]) [:input {:id id + :ref input-ref :class (stl/css :search-input) :on-change handle-change :value value diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 7bdf91fb1f..ecd4b186bc 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -474,10 +474,10 @@ #(st/emit! (dw/select-all))) find - (mf/use-fn (fn [] (on-close) (st/emit! (dw/open-layers-search :find)))) + (mf/use-fn (fn [] (on-close) (st/emit! (dw/open-layers-search :find {:force? true})))) find-and-replace - (mf/use-fn (fn [] (on-close) (st/emit! (dw/open-layers-search :find-and-replace)))) + (mf/use-fn (fn [] (on-close) (st/emit! (dw/open-layers-search :find-and-replace {:force? true})))) undo (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs index 8ba48062fb..3b768a823b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs @@ -48,7 +48,10 @@ (let [{:keys [enter leave]} @sidebar-hover-queue enter (set/difference enter leave) - leave (set/difference leave enter)] + leave (set/difference leave enter) + search-match (get-in @st/state [:workspace-local :search-match-highlight]) + leave (cond-> leave + (some? search-match) (disj search-match))] (reset! sidebar-hover-queue {:enter #{} :leave #{}}) (reset! sidebar-hover-pending? false) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index df02e1d0d9..3b67ccc76b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -19,7 +19,9 @@ [app.main.store :as st] [app.main.ui.components.search-bar :refer [search-bar*]] [app.main.ui.components.title-bar :refer [title-bar*]] + [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.controls.input :refer [input*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.hooks :as hooks] [app.main.ui.notifications.badge :refer [badge-notification]] @@ -30,6 +32,7 @@ [app.util.keyboard :as kbd] [app.util.rxops :refer [throttle-fn]] [app.util.shape-icon :as usi] + [app.util.timers :as ts] [beicon.v2.core :as rx] [cuerdas.core :as str] [goog.events :as events] @@ -120,30 +123,28 @@ [:> hooks/sortable-container* {} (for [obj shapes] (if (cfh/frame-shape? obj) - [:> frame-wrapper* - {:item obj - :rename-id rename-id - :selected selected - :highlighted highlighted - :index (unchecked-get obj "__$__counter") - :objects objects - :key (dm/str (get obj :id)) - :is-sortable true - :is-filtered is-filtered - :parent-size parent-size - :depth -1}] - [:> layer-item* - {:item obj - :rename-id rename-id - :selected selected - :highlighted highlighted - :index (unchecked-get obj "__$__counter") - :objects objects - :key (dm/str (get obj :id)) - :is-sortable true - :is-filtered is-filtered - :depth -1 - :parent-size parent-size}]))]])) + [:> frame-wrapper* {:item obj + :rename-id rename-id + :selected selected + :highlighted highlighted + :index (unchecked-get obj "__$__counter") + :objects objects + :key (dm/str (get obj :id)) + :is-sortable true + :is-filtered is-filtered + :parent-size parent-size + :depth -1}] + [:> layer-item* {:item obj + :rename-id rename-id + :selected selected + :highlighted highlighted + :index (unchecked-get obj "__$__counter") + :objects objects + :key (dm/str (get obj :id)) + :is-sortable true + :is-filtered is-filtered + :depth -1 + :parent-size parent-size}]))]])) (mf/defc layers-tree-wrapper* {::mf/private true} @@ -175,21 +176,22 @@ {::mf/wrap [mf/memo #(mf/throttle % 300)] ::mf/private true} [{:keys [objects parent-size]}] - (let [selected (use-selected-shapes) - root (get objects uuid/zero)] + (let [selected (use-selected-shapes) + highlighted (mf/deref ref:highlighted-shapes) + root (get objects uuid/zero)] [:ul {:class (stl/css :element-list)} (for [[index id] (d/enumerate (:shapes root))] (when-let [obj (get objects id)] - [:> layer-item* - {:item obj - :selected selected - :index index - :objects objects - :key id - :is-sortable false - :is-filtered true - :depth -1 - :parent-size parent-size}]))])) + [:> layer-item* {:item obj + :selected selected + :highlighted highlighted + :index index + :objects objects + :key id + :is-sortable false + :is-filtered true + :depth -1 + :parent-size parent-size}]))])) (defn calc-reparented-objects [objects] @@ -208,8 +210,8 @@ ;; --- Layers Toolbox -(def ^:private ref:layers-panel-search - (l/derived (l/key :layers-panel-search) refs/workspace-local)) +(def ^:private ref:layers-search + (l/derived (l/key :layers-search) refs/workspace-local)) ;; FIXME: optimize (defn- match-filters? @@ -242,6 +244,21 @@ (false? (:masked-group shape)))) (and (contains? filters :mask) (true? (:masked-group shape)))))))) +(mf/defc radio-button* + {::mf/private true} + [{:keys [name checked text on-change]}] + [:label {:class (stl/css-case :radio-label true + :selected checked)} + [:span {:class (stl/css-case :radio-icon true + :checked checked)}] + [:input {:type "radio" + :name name + :class (stl/css :radio-input) + :checked checked + :on-change on-change}] + [:span {:class (stl/css :radio-text)} + text]]) + (defn use-search [page objects] (let [state* (mf/use-state @@ -254,7 +271,7 @@ :filters #{} :num-items 100 :current-match-idx 0})) - layers-search-request (mf/deref ref:layers-panel-search) + layers-search (mf/deref ref:layers-search) state (deref state*) current-filters (:filters state) current-items (:num-items state) @@ -265,13 +282,18 @@ find-replace-mode? (:find-replace-mode? state) search-scope (:search-scope state) current-match-idx (:current-match-idx state) + search-input-ref (mf/use-ref nil) clear-search-text (mf/use-fn - #(swap! state* assoc :search-text "" :num-items 100 :current-match-idx 0)) + #(swap! state* assoc + :search-text "" + :num-items 100 + :current-match-idx 0)) toggle-filters - (mf/use-fn #(swap! state* update :show-menu not)) + (mf/use-fn + #(swap! state* update :show-menu not)) on-toggle-filters-click (mf/use-fn @@ -280,51 +302,92 @@ (toggle-filters))) hide-menu - (mf/use-fn #(swap! state* assoc :show-menu false)) + (mf/use-fn + #(swap! state* assoc :show-menu false)) on-key-down - (mf/use-fn (fn [event] (when (kbd/esc? event) (hide-menu)))) + (mf/use-fn + (fn [event] + (when (kbd/esc? event) + (hide-menu)))) update-search-text (mf/use-fn - (fn [value _event] - (swap! state* assoc :search-text value :num-items 100 :current-match-idx 0))) + (fn [value] + (swap! state* assoc + :search-text value + :num-items 100 + :current-match-idx 0))) update-replace-text - (mf/use-fn (fn [value _event] (swap! state* assoc :replace-text value))) + (mf/use-fn + (fn [event] + (let [value (dom/get-target-val event)] + (swap! state* assoc :replace-text value)))) - clear-replace-text - (mf/use-fn #(swap! state* assoc :replace-text "")) + f-key? (kbd/is-key-ignore-case? "f") + h-key? (kbd/is-key-ignore-case? "h") + + handle-find-shortcut-keydown + (mf/use-fn + (fn [event] + (when (kbd/mod? event) + (cond + (f-key? event) + (do + (dom/prevent-default event) + (dom/stop-propagation event) + (st/emit! (dw/open-layers-search :find))) + + (h-key? event) + (do + (dom/prevent-default event) + (dom/stop-propagation event) + (st/emit! (dw/open-layers-search :find-and-replace))))))) set-search-scope (mf/use-fn (fn [scope] - (swap! state* assoc :search-scope scope :num-items 100 :current-match-idx 0))) + (swap! state* assoc + :search-scope scope + :num-items 100 + :current-match-idx 0) + (st/emit! (dw/update-layers-search-scope scope)))) + + toggle-mode + (mf/use-fn + (mf/deps find-replace-mode?) + (fn [] + (let [mode (if find-replace-mode? :find :find-and-replace)] + (st/emit! (dw/open-layers-search mode {:force? true}))))) toggle-search (mf/use-fn + (mf/deps show-search?) (fn [event] (let [node (dom/get-current-target event)] (dom/blur! node) - (swap! state* (fn [state] - (-> state - (assoc :search-text "" :replace-text "" :filters #{}) - (assoc :show-menu false :find-replace-mode? false) - (assoc :search-scope :layers :num-items 100 :current-match-idx 0) - (update :show-search not))))))) + (if show-search? + (st/emit! dw/close-layers-search) + (st/emit! (dw/open-layers-search :find {:force? true})))))) remove-filter (mf/use-fn (fn [event] - (let [fkey (-> (dom/get-current-target event) (dom/get-data "filter") (keyword))] + (let [fkey (-> (dom/get-current-target event) + (dom/get-data "filter") + (keyword))] (swap! state* (fn [state] - (-> state (update :filters disj fkey) (assoc :num-items 100))))))) + (-> state + (update :filters disj fkey) + (assoc :num-items 100))))))) add-filter (mf/use-fn (fn [event] (dom/stop-propagation event) - (let [key (-> (dom/get-current-target event) (dom/get-data "filter") (keyword))] + (let [key (-> (dom/get-current-target event) + (dom/get-data "filter") (keyword))] (swap! state* (fn [state] (-> state (update :filters conj key) @@ -374,7 +437,8 @@ (fn [_] (when (pos? text-match-count) (swap! state* update :current-match-idx - (fn [idx] (mod (inc idx) text-match-count)))))) + (fn [idx] + (mod (inc idx) text-match-count)))))) navigate-prev (mf/use-fn @@ -382,7 +446,8 @@ (fn [_] (when (pos? text-match-count) (swap! state* update :current-match-idx - (fn [idx] (mod (+ (dec idx) text-match-count) text-match-count)))))) + (fn [idx] + (mod (+ (dec idx) text-match-count) text-match-count)))))) handle-replace (mf/use-fn @@ -403,6 +468,24 @@ (st/emit! (dwt/replace-text-in-shapes text-match-ids current-search replace-text)) (st/emit! (dwt/replace-layer-names-in-shapes text-match-ids current-search replace-text)))))) + on-replace-keydown + (mf/use-fn + (mf/deps handle-replace) + (fn [event] + (when (or (kbd/enter? event) (kbd/space? event)) + (dom/prevent-default event) + (dom/stop-propagation event) + (handle-replace event)))) + + on-replace-all-keydown + (mf/use-fn + (mf/deps handle-replace-all) + (fn [event] + (when (or (kbd/enter? event) (kbd/space? event)) + (dom/prevent-default event) + (dom/stop-propagation event) + (handle-replace-all event)))) + filtered-objects (mf/with-memo [active? filtered-objects-all current-items] (when active? @@ -424,201 +507,242 @@ (events/unlistenByKey key1) (events/unlistenByKey key2)))) - (mf/with-effect [layers-search-request] - (when (some? layers-search-request) - (let [replace-mode? (= layers-search-request :find-and-replace)] + (mf/with-effect [layers-search] + (if-let [{:keys [open? find-replace-mode? scope]} layers-search] + (when open? (swap! state* (fn [s] - (-> s - (assoc :show-search true :find-replace-mode? replace-mode?) - (assoc :search-scope (if replace-mode? :canvas :layers)) - (assoc :search-text "" :replace-text "" :current-match-idx 0))))) - (st/emit! dw/clear-layers-search))) + (let [mode-changed? (not= (:find-replace-mode? s) find-replace-mode?) + opening? (not (:show-search s))] + (-> s + (assoc :show-search true + :find-replace-mode? find-replace-mode? + :search-scope scope) + (cond-> (or opening? mode-changed?) + (assoc :search-text "" :replace-text "" :current-match-idx 0))))))) + (swap! state* (fn [state] + (-> state + (assoc :search-text "" + :replace-text "" + :filters #{}) + (assoc :show-menu false + :find-replace-mode? false) + (assoc :search-scope :layers + :num-items 100 + :current-match-idx 0) + (assoc :show-search false)))))) + + (mf/with-effect [(get layers-search :scope)] + (when (and layers-search (:open? layers-search)) + (swap! state* assoc :search-scope (:scope layers-search)))) + + (mf/with-effect [layers-search show-search?] + (when (and layers-search (:open? layers-search) show-search?) + (ts/raf + (fn [] + (when-let [node (mf/ref-val search-input-ref)] + (dom/focus! node)))))) + + (mf/with-effect [find-replace-mode? show-search? safe-match-idx text-match-ids] + (let [match-ids text-match-ids] + (when (and find-replace-mode? show-search? (seq match-ids)) + (let [current-id (nth match-ids safe-match-idx)] + (st/emit! (dw/set-search-match-highlight current-id match-ids)))) + (fn [] + (when (seq match-ids) + (st/emit! (dw/clear-search-match-highlight match-ids)))))) [filtered-objects handle-show-more #(mf/html (if show-search? [:* - [:div {:class (stl/css :tool-window-bar :search)} - [:> search-bar* {:on-change update-search-text + [:div {:class (stl/css :tool-window-bar)} + [:> search-bar* {:input-ref search-input-ref + :class (stl/css :search-item) + :on-change update-search-text :value current-search :on-clear clear-search-text + :on-key-down handle-find-shortcut-keydown :placeholder (tr "workspace.sidebar.layers.search")} - [:button {:on-click on-toggle-filters-click - :class (stl/css-case :filter-button true :opened show-menu? :active active?)} - [:> icon* {:icon-id i/filter}]]] + [:> icon-button* {:variant "secondary" + :class (stl/css :filter-button) + :aria-pressed show-menu? + :aria-label (tr "workspace.sidebar.layers.filter") + :on-click on-toggle-filters-click + :icon i/filter}]] + [:> icon-button* {:variant "ghost" + :aria-pressed find-replace-mode? + :aria-label (tr "workspace.sidebar.layers.search-and-replace") + :on-click toggle-mode + :icon i/menu}] [:> icon-button* {:variant "ghost" :aria-label (tr "labels.close") :on-click toggle-search :icon i/close}]] - [:div {:class (stl/css :search-scope-row)} - [:label {:class (stl/css-case :scope-option true :scope-selected (= :canvas search-scope))} - [:span {:class (stl/css-case :scope-radio true :scope-radio-checked (= :canvas search-scope))}] - [:input {:type "radio" :name "search-scope" :class (stl/css :scope-radio-input) - :checked (= :canvas search-scope) - :on-change (fn [_] (set-search-scope :canvas))}] - [:span {:class (stl/css :scope-label)} - (tr "workspace.sidebar.layers.search-scope-canvas")]] - [:label {:class (stl/css-case :scope-option true :scope-selected (= :layers search-scope))} - [:span {:class (stl/css-case :scope-radio true :scope-radio-checked (= :layers search-scope))}] - [:input {:type "radio" :name "search-scope" :class (stl/css :scope-radio-input) - :checked (= :layers search-scope) - :on-change (fn [_] (set-search-scope :layers))}] - [:span {:class (stl/css :scope-label)} - (tr "workspace.sidebar.layers.search-scope-layers")]]] + [:div {:class (stl/css :replace-wrapper)} + (when ^boolean find-replace-mode? + [:div {:class (stl/css :replace-row)} + [:> input* {:type "text" + :placeholder (tr "workspace.sidebar.layers.replace-placeholder") + :on-key-down handle-find-shortcut-keydown + :on-change update-replace-text}] - (when ^boolean find-replace-mode? - [:* - [:div {:class (stl/css :tool-window-bar :replace-row)} - [:div {:class (stl/css :replace-input-wrapper)} - [:input {:class (stl/css :replace-input) - :value replace-text - :placeholder (tr "workspace.sidebar.layers.replace-placeholder") - :on-change (fn [event] - (update-replace-text (dom/get-target-val event) event))}] - (when (not= "" replace-text) - [:button {:class (stl/css :clear-icon) :on-click clear-replace-text} - [:> icon* {:icon-id i/delete-text :size "s"}]])] (when (d/not-empty? current-search) (if (pos? text-match-count) - [:div {:class (stl/css :match-navigation)} - [:span {:class (stl/css :match-count)} + [:div {:class (stl/css :replace-match-navigation)} + [:span {:class (stl/css :replace-match-count)} (dm/str (inc safe-match-idx) " / " text-match-count)] [:> icon-button* {:variant "ghost" :aria-label (tr "labels.previous") :on-click navigate-prev :icon i/arrow-up}] [:> icon-button* {:variant "ghost" :aria-label (tr "labels.next") :on-click navigate-next :icon i/arrow-down}]] - [:span {:class (stl/css :no-matches)} - (tr "workspace.sidebar.layers.no-matches")]))] + [:div {:class (stl/css :replace-match-count)} + (tr "workspace.sidebar.layers.no-matches")]))]) + + [:div {:class (stl/css :replace-scope-row)} + [:> radio-button* {:name "search-scope" + :checked (= :canvas search-scope) + :text (tr "workspace.sidebar.layers.search-scope-canvas") + :on-change (partial set-search-scope :canvas)}] + [:> radio-button* {:name "search-scope" + :checked (= :layers search-scope) + :text (tr "workspace.sidebar.layers.search-scope-layers") + :on-change (partial set-search-scope :layers)}]] + + (when ^boolean find-replace-mode? [:div {:class (stl/css :replace-actions-row)} - [:button {:class (stl/css :replace-button) - :on-click handle-replace - :disabled (or (zero? text-match-count) (str/empty? current-search))} + [:> button* {:variant "secondary" + :class (stl/css :replace-actions-button) + :on-click handle-replace + :on-key-down on-replace-keydown + :disabled (or (zero? text-match-count) (str/empty? current-search))} (tr "workspace.sidebar.layers.replace")] - [:button {:class (stl/css :replace-button) - :on-click handle-replace-all - :disabled (or (zero? text-match-count) (str/empty? current-search))} - (tr "workspace.sidebar.layers.replace-all")]]]) + [:> button* {:variant "secondary" + :class (stl/css :replace-actions-button) + :on-click handle-replace-all + :on-key-down on-replace-all-keydown + :disabled (or (zero? text-match-count) (str/empty? current-search))} + (tr "workspace.sidebar.layers.replace-all")]]) - [:div {:class (stl/css :active-filters)} - (for [fkey current-filters] - (let [fname (d/name fkey) + [:div {:class (stl/css :active-filters)} + (for [fkey current-filters] + (let [fname (d/name fkey) - name (case fkey - :frame (tr "workspace.sidebar.layers.frames") - :group (tr "workspace.sidebar.layers.groups") - :mask (tr "workspace.sidebar.layers.masks") - :component (tr "workspace.sidebar.layers.components") - :text (tr "workspace.sidebar.layers.texts") - :image (tr "workspace.sidebar.layers.images") - :shape (tr "workspace.sidebar.layers.shapes") - (tr fkey)) - filter-icon (usi/get-shape-icon-by-type fkey)] + name (case fkey + :frame (tr "workspace.sidebar.layers.frames") + :group (tr "workspace.sidebar.layers.groups") + :mask (tr "workspace.sidebar.layers.masks") + :component (tr "workspace.sidebar.layers.components") + :text (tr "workspace.sidebar.layers.texts") + :image (tr "workspace.sidebar.layers.images") + :shape (tr "workspace.sidebar.layers.shapes") + (tr fkey)) + filter-icon (usi/get-shape-icon-by-type fkey)] - [:button {:class (stl/css :layer-filter) - :key fname - :data-filter fname - :on-click remove-filter} - [:> icon* {:icon-id filter-icon :size "s" :class (stl/css :layer-filter-icon)}] - [:span {:class (stl/css :layer-filter-name)} - name] - [:> icon* {:icon-id i/close-small :class (stl/css :layer-filter-close)}]]))] + [:button {:class (stl/css :layer-filter) + :key fname + :data-filter fname + :on-click remove-filter} + [:> icon* {:icon-id filter-icon :size "s" :class (stl/css :layer-filter-icon)}] + [:span {:class (stl/css :layer-filter-name)} + name] + [:> icon* {:icon-id i/close-small :class (stl/css :layer-filter-close)}]]))] - (when ^boolean show-menu? - [:ul {:class (stl/css :filters-container)} - [:li {:class (stl/css-case :filter-menu-item true - :selected (contains? current-filters :frame)) - :data-filter "frame" - :on-click add-filter} - [:div {:class (stl/css :filter-menu-item-name-wrapper)} - [:> icon* {:icon-id i/board :size "s" :class (stl/css :filter-menu-item-icon)}] - [:span {:class (stl/css :filter-menu-item-name)} - (tr "workspace.sidebar.layers.frames")]] + (when ^boolean show-menu? + [:ul {:class (stl/css :filters-container)} + [:li {:class (stl/css-case :filter-menu-item true + :selected (contains? current-filters :frame)) + :data-filter "frame" + :on-click add-filter} + [:div {:class (stl/css :filter-menu-item-name-wrapper)} + [:> icon* {:icon-id i/board :size "s" :class (stl/css :filter-menu-item-icon)}] + [:span {:class (stl/css :filter-menu-item-name)} + (tr "workspace.sidebar.layers.frames")]] - (when (contains? current-filters :frame) - [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] + (when (contains? current-filters :frame) + [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] - [:li {:class (stl/css-case :filter-menu-item true - :selected (contains? current-filters :group)) - :data-filter "group" - :on-click add-filter} - [:div {:class (stl/css :filter-menu-item-name-wrapper)} - [:> icon* {:icon-id i/group :size "s" :class (stl/css :filter-menu-item-icon)}] - [:span {:class (stl/css :filter-menu-item-name)} - (tr "workspace.sidebar.layers.groups")]] + [:li {:class (stl/css-case :filter-menu-item true + :selected (contains? current-filters :group)) + :data-filter "group" + :on-click add-filter} + [:div {:class (stl/css :filter-menu-item-name-wrapper)} + [:> icon* {:icon-id i/group :size "s" :class (stl/css :filter-menu-item-icon)}] + [:span {:class (stl/css :filter-menu-item-name)} + (tr "workspace.sidebar.layers.groups")]] - (when (contains? current-filters :group) - [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] + (when (contains? current-filters :group) + [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] - [:li {:class (stl/css-case :filter-menu-item true - :selected (contains? current-filters :mask)) - :data-filter "mask" - :on-click add-filter} - [:div {:class (stl/css :filter-menu-item-name-wrapper)} - [:> icon* {:icon-id i/mask :size "s" :class (stl/css :filter-menu-item-icon)}] - [:span {:class (stl/css :filter-menu-item-name)} - (tr "workspace.sidebar.layers.masks")]] + [:li {:class (stl/css-case :filter-menu-item true + :selected (contains? current-filters :mask)) + :data-filter "mask" + :on-click add-filter} + [:div {:class (stl/css :filter-menu-item-name-wrapper)} + [:> icon* {:icon-id i/mask :size "s" :class (stl/css :filter-menu-item-icon)}] + [:span {:class (stl/css :filter-menu-item-name)} + (tr "workspace.sidebar.layers.masks")]] - (when (contains? current-filters :mask) - [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] + (when (contains? current-filters :mask) + [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] - [:li {:class (stl/css-case :filter-menu-item true - :selected (contains? current-filters :component)) - :data-filter "component" - :on-click add-filter} - [:div {:class (stl/css :filter-menu-item-name-wrapper)} - [:> icon* {:icon-id i/component :size "s" :class (stl/css :filter-menu-item-icon)}] - [:span {:class (stl/css :filter-menu-item-name)} - (tr "workspace.sidebar.layers.components")]] + [:li {:class (stl/css-case :filter-menu-item true + :selected (contains? current-filters :component)) + :data-filter "component" + :on-click add-filter} + [:div {:class (stl/css :filter-menu-item-name-wrapper)} + [:> icon* {:icon-id i/component :size "s" :class (stl/css :filter-menu-item-icon)}] + [:span {:class (stl/css :filter-menu-item-name)} + (tr "workspace.sidebar.layers.components")]] - (when (contains? current-filters :component) - [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] + (when (contains? current-filters :component) + [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] - [:li {:class (stl/css-case :filter-menu-item true - :selected (contains? current-filters :text)) - :data-filter "text" - :on-click add-filter} - [:div {:class (stl/css :filter-menu-item-name-wrapper)} - [:> icon* {:icon-id i/text :size "s" :class (stl/css :filter-menu-item-icon)}] - [:span {:class (stl/css :filter-menu-item-name)} - (tr "workspace.sidebar.layers.texts")]] + [:li {:class (stl/css-case :filter-menu-item true + :selected (contains? current-filters :text)) + :data-filter "text" + :on-click add-filter} + [:div {:class (stl/css :filter-menu-item-name-wrapper)} + [:> icon* {:icon-id i/text :size "s" :class (stl/css :filter-menu-item-icon)}] + [:span {:class (stl/css :filter-menu-item-name)} + (tr "workspace.sidebar.layers.texts")]] - (when (contains? current-filters :text) - [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] + (when (contains? current-filters :text) + [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] - [:li {:class (stl/css-case :filter-menu-item true - :selected (contains? current-filters :image)) - :data-filter "image" - :on-click add-filter} - [:div {:class (stl/css :filter-menu-item-name-wrapper)} - [:> icon* {:icon-id i/img :size "s" :class (stl/css :filter-menu-item-icon)}] - [:span {:class (stl/css :filter-menu-item-name)} - (tr "workspace.sidebar.layers.images")]] + [:li {:class (stl/css-case :filter-menu-item true + :selected (contains? current-filters :image)) + :data-filter "image" + :on-click add-filter} + [:div {:class (stl/css :filter-menu-item-name-wrapper)} + [:> icon* {:icon-id i/img :size "s" :class (stl/css :filter-menu-item-icon)}] + [:span {:class (stl/css :filter-menu-item-name)} + (tr "workspace.sidebar.layers.images")]] - (when (contains? current-filters :image) - [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] + (when (contains? current-filters :image) + [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] - [:li {:class (stl/css-case :filter-menu-item true - :selected (contains? current-filters :shape)) - :data-filter "shape" - :on-click add-filter} - [:div {:class (stl/css :filter-menu-item-name-wrapper)} - [:> icon* {:icon-id i/path :size "s" :class (stl/css :filter-menu-item-icon)}] - [:span {:class (stl/css :filter-menu-item-name)} - (tr "workspace.sidebar.layers.shapes")]] + [:li {:class (stl/css-case :filter-menu-item true + :selected (contains? current-filters :shape)) + :data-filter "shape" + :on-click add-filter} + [:div {:class (stl/css :filter-menu-item-name-wrapper)} + [:> icon* {:icon-id i/path :size "s" :class (stl/css :filter-menu-item-icon)}] + [:span {:class (stl/css :filter-menu-item-name)} + (tr "workspace.sidebar.layers.shapes")]] - (when (contains? current-filters :shape) - [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])]])] + (when (contains? current-filters :shape) + [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])]])]] [:div {:class (stl/css :tool-window-bar)} [:> title-bar* {:collapsable false + :class (stl/css :tool-window-bar-title) :title (:name page) :on-btn-click toggle-search :btn-icon "search" :btn-title (tr "labels.search")}]]))])) - (defn- on-scroll [event] (let [children (dom/get-elements-by-class "sticky-children") @@ -699,19 +823,24 @@ (mf/use-fn #(st/emit! (dw/toggle-focus-mode)))] - [:div#layers {:class (stl/css :layers) :data-testid "layer-tree"} + [:div {:id "layers" + :class (stl/css :layers) + :data-testid "layer-tree"} + (if (d/not-empty? focus) [:div {:class (stl/css :tool-window-bar)} [:button {:class (stl/css :focus-title) :on-click toogle-focus-mode} - [:span {:class (stl/css :back-button)} - [:> icon* {:icon-id i/arrow}]] + [:span {:class (stl/css :focus-back-button)} + [:> icon* {:icon-id i/arrow-left}]] [:div {:class (stl/css :focus-name)} (or title (tr "workspace.sidebar.layers"))] [:div {:class (stl/css :focus-mode-tag-wrapper)} - [:& badge-notification {:content (tr "workspace.focus.focus-mode") :size :small :is-focus true}]]]] + [:& badge-notification {:content (tr "workspace.focus.focus-mode") + :size :small + :is-focus true}]]]] (filter-component)) @@ -724,11 +853,11 @@ :key (dm/str page-id) :parent-size size-parent}] [:div {:ref lazy-load-ref}]] + [:div {:on-scroll on-scroll :class (stl/css :tool-window-content) :data-scroll-container true :style {:display (when (some? filtered-objects) "none")}} - [:> layers-tree-wrapper* {:objects filtered-objects :key (dm/str page-id) :is-filtered true diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.scss b/frontend/src/app/main/ui/workspace/sidebar/layers.scss index 048aa08549..babec39c95 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.scss @@ -5,144 +5,100 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; +@use "ds/borders.scss" as *; +@use "ds/mixins.scss" as *; +@use "ds/sizes.scss" as *; +@use "ds/spacing.scss" as *; +@use "ds/typography.scss" as t; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; + +.element-list { + display: grid; + position: relative; +} .tool-window-bar { display: flex; align-items: center; justify-content: space-between; - height: deprecated.$s-32; - min-height: deprecated.$s-32; - margin: deprecated.$s-8 0 deprecated.$s-4 deprecated.$s-8; - padding-right: deprecated.$s-12; - - &.search { - padding: 0 deprecated.$s-12 0 deprecated.$s-8; - gap: deprecated.$s-4; - - .filter-button { - @include deprecated.flex-center; - @include deprecated.button-style; - - height: deprecated.$s-32; - width: deprecated.$s-32; - margin: 0; - border: deprecated.$s-1 solid var(--color-background-tertiary); - border-radius: deprecated.$br-8 deprecated.$br-2 deprecated.$br-2 deprecated.$br-8; - background-color: var(--color-background-tertiary); - - svg { - height: deprecated.$s-16; - width: deprecated.$s-16; - stroke: var(--icon-foreground); - } - - &:focus { - border: deprecated.$s-1 solid var(--input-border-color-focus); - outline: 0; - background-color: var(--input-background-color-active); - color: var(--input-foreground-color-active); - - svg { - background-color: var(--input-background-color-active); - } - } - - &:hover { - border: deprecated.$s-1 solid var(--input-border-color-hover); - background-color: var(--input-background-color-hover); - - svg { - background-color: var(--input-background-color-hover); - stroke: var(--button-foreground-hover); - } - } - - &.opened { - @extend %button-icon-selected; - } - } - } + gap: var(--sp-xs); + padding: var(--sp-m) var(--sp-m) 0 var(--sp-m); } -.page-name { - @include deprecated.uppercase-title-typography; - - padding: 0 deprecated.$s-12; - color: var(--title-foreground-color); +.tool-window-bar-title { + margin-block-end: var(--sp-s); } -.icon-search { - @extend %button-tertiary; +.tool-window-content { + --calculated-height: calc(#{px2rem(136)} + var(--height, #{$sz-200})); - height: deprecated.$s-32; - width: deprecated.$s-28; - border-radius: deprecated.$br-8; - margin-right: deprecated.$s-8; - padding: 0; + display: flex; + flex-direction: column; + block-size: calc(100vh - var(--calculated-height)); + inline-size: calc(var(--left-sidebar-width) + var(--depth) * var(--layer-indentation-size)); + overflow: auto; + scrollbar-gutter: stable; +} - svg { - @extend %button-icon; - - stroke: var(--icon-foreground); - } +.filter-button { + border-start-end-radius: 0; + border-end-end-radius: 0; } .focus-title { - @include deprecated.button-style; - + border: none; + background: none; + block-size: var(--sp-xxxl); + inline-size: 100%; display: grid; grid-template-columns: auto 1fr auto; align-items: center; - width: 100%; + margin-block-end: var(--sp-s); padding: 0; } -.back-button { - @include deprecated.flex-center; - - height: deprecated.$s-32; - width: deprecated.$s-24; - padding: 0 deprecated.$s-4 0 deprecated.$s-8; - - svg { - @extend %button-icon-small; - - stroke: var(--icon-foreground); - transform: rotate(180deg); - } +.focus-back-button { + display: flex; + align-items: center; + justify-content: center; + block-size: var(--sp-xxxl); + padding-inline-start: var(--sp-s); + color: var(--icon-foreground); } .focus-name { - @include deprecated.text-ellipsis; - @include deprecated.body-small-typography; + @include t.use-typography("body-small"); + @include text-ellipsis; - padding-left: deprecated.$s-4; + padding-inline-end: var(--sp-xs); color: var(--title-foreground-color); } .focus-mode-tag-wrapper { - @include deprecated.flex-center; - - height: 100%; - margin-right: deprecated.$s-12; + display: flex; + align-items: center; + justify-content: center; + block-size: 100%; + margin-inline-end: var(--sp-m); } .active-filters { - @include deprecated.flex-row; - + display: flex; + align-items: center; + gap: var(--sp-xs); flex-wrap: wrap; - margin: 0 deprecated.$s-12; + margin: 0 var(--sp-m); } .layer-filter { @extend %button-tag; - gap: deprecated.$s-6; - height: deprecated.$s-24; - margin: deprecated.$s-2 0; - border-radius: deprecated.$br-6; + gap: px2rem(6); + block-size: var(--sp-xxl); + margin: var(--sp-xxs) 0; + border-radius: $br-6; background-color: var(--pill-background-color); - cursor: pointer; } .layer-filter-icon, @@ -151,9 +107,11 @@ } .layer-filter-name { - @include deprecated.flex-center; - @include deprecated.body-small-typography; + @include t.use-typography("body-small"); + display: flex; + align-items: center; + justify-content: center; color: var(--pill-foreground-color); } @@ -161,34 +119,117 @@ position: relative; } +.replace-wrapper { + display: flex; + flex-direction: column; + padding: var(--sp-xs) var(--sp-m); + gap: var(--sp-xs); +} + +.replace-row { + display: flex; + flex-direction: row; + gap: var(--sp-xs); +} + +.replace-scope-row { + display: flex; + gap: var(--sp-l); + align-items: center; + block-size: $sz-32; +} + +.replace-actions-row { + display: flex; + gap: var(--sp-xs); +} + +.replace-actions-button { + justify-content: center; + flex: 1 1 0; +} + +.replace-match-navigation { + display: flex; + align-items: center; + gap: var(--sp-xs); + flex-shrink: 0; +} + +.replace-match-count { + @include t.use-typography("body-small"); + + color: var(--color-foreground-secondary); + white-space: nowrap; + display: flex; + align-items: center; + padding: 0 var(--sp-xs); +} + +.radio-label { + display: flex; + align-items: center; + gap: px2rem(6); + + &.selected { + .radio-text { + color: var(--color-foreground-primary); + } + } +} + +.radio-icon { + block-size: var(--sp-m); + inline-size: var(--sp-m); + border: $b-1 solid var(--color-foreground-secondary); + border-radius: 50%; + background-color: transparent; + flex-shrink: 0; + + &.checked { + border-color: var(--color-accent-primary); + background-color: var(--color-accent-primary); + box-shadow: inset 0 0 0 var(--sp-xxs) var(--color-background-primary); + } +} + +.radio-input { + display: none; +} + +.radio-text { + @include t.use-typography("body-small"); + + color: var(--color-foreground-secondary); +} + .filters-container { @extend %menu-dropdown; position: absolute; - left: deprecated.$s-20; - width: deprecated.$s-192; + inline-size: $sz-192; .filter-menu-item { - @include deprecated.body-small-typography; + @include t.use-typography("body-small"); display: flex; align-items: center; justify-content: space-between; - width: 100%; - padding: deprecated.$s-6; - border-radius: deprecated.$br-8; + inline-size: 100%; + padding: px2rem(6); + border-radius: $br-8; .filter-menu-item-name-wrapper { display: flex; align-items: center; - gap: deprecated.$s-8; + gap: var(--sp-s); .filter-menu-item-icon { color: var(--menu-foreground-color); } .filter-menu-item-name { - padding-top: deprecated.$s-2; + padding-block-start: var(--sp-xxs); color: var(--menu-foreground-color); } } @@ -234,173 +275,3 @@ } } } - -.tool-window-content { - --calculated-height: calc(#{deprecated.$s-136} + var(--height, #{deprecated.$s-200})); - - display: flex; - flex-direction: column; - height: calc(100vh - var(--calculated-height)); - width: calc(var(--left-sidebar-width) + var(--depth) * var(--layer-indentation-size)); - overflow: auto; - scrollbar-gutter: stable; -} - -.replace-row { - padding: 0 deprecated.$s-12; - gap: deprecated.$s-4; -} - -.search-scope-row { - display: flex; - gap: deprecated.$s-16; - padding: deprecated.$s-4 deprecated.$s-12 deprecated.$s-8; - align-items: center; -} - -.scope-option { - display: flex; - align-items: center; - gap: deprecated.$s-6; - cursor: pointer; -} - -.scope-radio { - width: deprecated.$s-12; - height: deprecated.$s-12; - border: deprecated.$s-1 solid var(--color-foreground-secondary); - border-radius: 50%; - background-color: transparent; - flex-shrink: 0; -} - -.scope-radio-checked { - border-color: var(--color-accent-primary); - background-color: var(--color-accent-primary); - box-shadow: inset 0 0 0 deprecated.$s-2 var(--color-background-primary); -} - -.scope-radio-input { - display: none; -} - -.scope-label { - @include deprecated.body-small-typography; - - color: var(--color-foreground-secondary); - cursor: pointer; -} - -.scope-selected .scope-label { - color: var(--color-foreground-primary); -} - -.replace-actions-row { - display: flex; - gap: deprecated.$s-4; - padding: 0 deprecated.$s-12 deprecated.$s-8; -} - -.replace-input-wrapper { - @include deprecated.flex-center; - - flex: 1; - height: deprecated.$s-32; - border: deprecated.$s-1 solid var(--search-bar-input-border-color); - border-radius: deprecated.$br-8; - background-color: var(--search-bar-input-background-color); - - &:hover { - border: deprecated.$s-1 solid var(--input-border-color-hover); - background-color: var(--input-background-color-hover); - - .replace-input { - background-color: var(--input-background-color-hover); - } - } - - &:focus-within { - background-color: var(--input-background-color-active); - color: var(--input-foreground-color-active); - border: deprecated.$s-1 solid var(--input-border-color-focus); - - .replace-input { - background-color: var(--input-background-color-active); - } - } -} - -.replace-input { - width: 100%; - height: 100%; - margin: 0 deprecated.$s-8; - border: 0; - background-color: var(--input-background-color); - font-size: deprecated.$fs-12; - color: var(--input-foreground-color); - border-radius: deprecated.$br-8; - - &:focus { - outline: none; - } -} - -.replace-button { - @include deprecated.body-small-typography; - @include deprecated.button-style; - - flex: 1; - height: deprecated.$s-28; - padding: 0 deprecated.$s-8; - border: deprecated.$s-1 solid var(--color-background-tertiary); - border-radius: deprecated.$br-8; - background-color: var(--color-background-tertiary); - color: var(--color-foreground-primary); - white-space: nowrap; - text-transform: uppercase; - - &:hover:not(:disabled) { - border: deprecated.$s-1 solid var(--input-border-color-hover); - background-color: var(--input-background-color-hover); - } - - &:disabled { - opacity: 0.4; - cursor: default; - } -} - -.match-navigation { - display: flex; - align-items: center; - gap: deprecated.$s-2; - flex-shrink: 0; -} - -.match-count { - @include deprecated.body-small-typography; - - color: var(--color-foreground-secondary); - white-space: nowrap; -} - -.no-matches { - @include deprecated.body-small-typography; - - color: var(--color-foreground-secondary); - white-space: nowrap; - flex-shrink: 0; -} - -.clear-icon { - @extend %button-tag; - - flex: 0 0 deprecated.$s-32; - height: 100%; - color: var(--color-icon-default); -} - -.element-list { - display: grid; - position: relative; -} diff --git a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs index 8a363d56eb..f58ffab8d1 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs @@ -111,6 +111,8 @@ (tr "shortcuts.duplicate") (tr "shortcuts.escape") (tr "shortcuts.export-shapes") + (tr "shortcuts.find") + (tr "shortcuts.find-and-replace") (tr "shortcuts.fit-all") (tr "shortcuts.flip-horizontal") (tr "shortcuts.flip-vertical") @@ -160,6 +162,7 @@ (tr "shortcuts.open-viewer") (tr "shortcuts.open-workspace") (tr "shortcuts.paste") + (tr "shortcuts.paste-replace") (tr "shortcuts.prev-frame") (tr "shortcuts.redo") (tr "shortcuts.rename") diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 4107130ea7..942d7fe43b 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -4662,6 +4662,14 @@ msgstr "Cancel" msgid "shortcuts.export-shapes" msgstr "Export shapes" +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs +msgid "shortcuts.find" +msgstr "Find" + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs +msgid "shortcuts.find-and-replace" +msgstr "Find and replace" + #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:114 msgid "shortcuts.fit-all" msgstr "Zoom to fit all" @@ -4866,6 +4874,10 @@ msgstr " or " msgid "shortcuts.paste" msgstr "Paste" +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs +msgid "shortcuts.paste-replace" +msgstr "Paste and replace" + #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:111 #, unused msgid "shortcuts.paste-props" @@ -8148,7 +8160,10 @@ msgstr "Masks" #: src/app/main/ui/workspace/sidebar/layers.cljs:293 msgid "workspace.sidebar.layers.search" -msgstr "Search layers" +msgstr "Find…" + +msgid "workspace.sidebar.layers.search-and-replace" +msgstr "Find and replace" #: src/app/main/ui/workspace/sidebar/layers.cljs:316, src/app/main/ui/workspace/sidebar/layers.cljs:410 msgid "workspace.sidebar.layers.shapes" @@ -8180,11 +8195,11 @@ msgstr "No matches" #: src/app/main/ui/workspace/sidebar/layers.cljs msgid "workspace.sidebar.layers.search-scope-layers" -msgstr "Search layers" +msgstr "Layer names" #: src/app/main/ui/workspace/sidebar/layers.cljs msgid "workspace.sidebar.layers.search-scope-canvas" -msgstr "Search on canvas" +msgstr "Text content" #: src/app/main/ui/inspect/attributes/svg.cljs:56, src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs:101 msgid "workspace.sidebar.options.svg-attrs.title" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 82da6d88d3..edc4661521 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -5897,6 +5897,14 @@ msgstr "Rehacer" msgid "workspace.header.menu.select-all" msgstr "Seleccionar todo" +#: src/app/main/ui/workspace/main_menu.cljs +msgid "workspace.header.menu.find" +msgstr "Buscar" + +#: src/app/main/ui/workspace/main_menu.cljs +msgid "workspace.header.menu.find-and-replace" +msgstr "Buscar y reemplazar" + #: src/app/main/ui/workspace/main_menu.cljs:423 msgid "workspace.header.menu.show-artboard-names" msgstr "Mostrar nombres de tableros" @@ -7968,6 +7976,9 @@ msgstr "Máscaras" msgid "workspace.sidebar.layers.search" msgstr "Buscar capas" +msgid "workspace.sidebar.layers.search-and-replace" +msgstr "Buscar y reemplazar" + #: src/app/main/ui/workspace/sidebar/layers.cljs:316, src/app/main/ui/workspace/sidebar/layers.cljs:410 msgid "workspace.sidebar.layers.shapes" msgstr "Formas"