Polish workspace find and replace UX (#9687)

*  Polish workspace find and replace UX

Co-authored-by: Cursor <cursoragent@cursor.com>

*  Add toggle mode button

This button toggles between search and search and replace modes

* ♻️ Refactor and CSS cleanup

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
This commit is contained in:
andrés gonzález 2026-05-21 12:10:36 +02:00 committed by GitHub
parent 3cfd1e1a48
commit e2ed6a488d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 618 additions and 518 deletions

View File

@ -1493,18 +1493,47 @@
(update [_ state] (update [_ state]
(assoc-in state [:workspace-global :clipboard-style] style)))) (assoc-in state [:workspace-global :clipboard-style] style))))
(defn open-layers-search (defn- layers-search-config
[mode] [mode]
(ptk/reify ::open-layers-search {:open? true
ptk/UpdateEvent :mode mode
(update [_ state] :scope (if (= mode :find-and-replace) :canvas :layers)
(assoc-in state [:workspace-local :layers-panel-search] mode)))) :find-replace-mode? (= mode :find-and-replace)})
(def clear-layers-search (defn- layers-search-active?
(ptk/reify ::clear-layers-search [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 ptk/UpdateEvent
(update [_ state] (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 ;; Exports
@ -1565,6 +1594,8 @@
;; Highlight ;; Highlight
(dm/export dwh/highlight-shape) (dm/export dwh/highlight-shape)
(dm/export dwh/dehighlight-shape) (dm/export dwh/dehighlight-shape)
(dm/export dwh/set-search-match-highlight)
(dm/export dwh/clear-search-match-highlight)
;; Shape flags ;; Shape flags
(dm/export dwsh/update-shape-flags) (dm/export dwsh/update-shape-flags)

View File

@ -27,3 +27,29 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update-in state [:workspace-local :highlighted] disj id)))) (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))))))

View File

@ -1206,18 +1206,24 @@
[ids search replacement] [ids search replacement]
(ptk/reify ::replace-text-in-shapes (ptk/reify ::replace-text-in-shapes
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ state _]
(let [undo-group (uuid/next)] (let [undo-group (uuid/next)
(rx/of update-event
(dwsh/update-shapes (dwsh/update-shapes
ids ids
(fn [shape] (fn [shape]
(if (and (= :text (:type shape)) (some? (:content shape))) (if (and (= :text (:type shape)) (some? (:content shape)))
(let [new-content (txt/replace-text-in-content (:content shape) search replacement) (let [new-content (txt/replace-text-in-content (:content shape) search replacement)
new-name (txt/generate-shape-name (txt/content->text new-content))] new-name (txt/generate-shape-name (txt/content->text new-content))]
(-> shape (assoc :content new-content) (assoc :name new-name))) (-> shape (assoc :content new-content) (assoc :name new-name)))
shape)) shape))
{:attrs #{:content :name} :undo-group undo-group})))))) {: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 ;; -- Text Editor v3

View File

@ -13,7 +13,8 @@
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(mf/defc search-bar* (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 (let [handle-change
(mf/use-fn (mf/use-fn
(mf/deps on-change) (mf/deps on-change)
@ -31,8 +32,11 @@
handle-key-down handle-key-down
(mf/use-fn (mf/use-fn
(mf/deps on-submit) (mf/deps on-submit on-key-down)
(fn [event] (fn [event]
(when (fn? on-key-down)
(on-key-down event))
(let [enter? (kbd/enter? event) (let [enter? (kbd/enter? event)
esc? (kbd/esc? event) esc? (kbd/esc? event)
node (dom/get-target event)] node (dom/get-target event)]
@ -53,6 +57,7 @@
:size "s" :size "s"
:class (stl/css :icon)}]) :class (stl/css :icon)}])
[:input {:id id [:input {:id id
:ref input-ref
:class (stl/css :search-input) :class (stl/css :search-input)
:on-change handle-change :on-change handle-change
:value value :value value

View File

@ -474,10 +474,10 @@
#(st/emit! (dw/select-all))) #(st/emit! (dw/select-all)))
find 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 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 undo
(mf/use-fn (mf/use-fn

View File

@ -48,7 +48,10 @@
(let [{:keys [enter leave]} @sidebar-hover-queue (let [{:keys [enter leave]} @sidebar-hover-queue
enter (set/difference enter leave) 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-queue {:enter #{} :leave #{}})
(reset! sidebar-hover-pending? false) (reset! sidebar-hover-pending? false)

View File

@ -19,7 +19,9 @@
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.search-bar :refer [search-bar*]] [app.main.ui.components.search-bar :refer [search-bar*]]
[app.main.ui.components.title-bar :refer [title-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.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.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
[app.main.ui.notifications.badge :refer [badge-notification]] [app.main.ui.notifications.badge :refer [badge-notification]]
@ -30,6 +32,7 @@
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.rxops :refer [throttle-fn]] [app.util.rxops :refer [throttle-fn]]
[app.util.shape-icon :as usi] [app.util.shape-icon :as usi]
[app.util.timers :as ts]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cuerdas.core :as str] [cuerdas.core :as str]
[goog.events :as events] [goog.events :as events]
@ -120,30 +123,28 @@
[:> hooks/sortable-container* {} [:> hooks/sortable-container* {}
(for [obj shapes] (for [obj shapes]
(if (cfh/frame-shape? obj) (if (cfh/frame-shape? obj)
[:> frame-wrapper* [:> frame-wrapper* {:item obj
{:item obj :rename-id rename-id
:rename-id rename-id :selected selected
:selected selected :highlighted highlighted
:highlighted highlighted :index (unchecked-get obj "__$__counter")
:index (unchecked-get obj "__$__counter") :objects objects
:objects objects :key (dm/str (get obj :id))
:key (dm/str (get obj :id)) :is-sortable true
:is-sortable true :is-filtered is-filtered
:is-filtered is-filtered :parent-size parent-size
:parent-size parent-size :depth -1}]
:depth -1}] [:> layer-item* {:item obj
[:> layer-item* :rename-id rename-id
{:item obj :selected selected
:rename-id rename-id :highlighted highlighted
:selected selected :index (unchecked-get obj "__$__counter")
:highlighted highlighted :objects objects
:index (unchecked-get obj "__$__counter") :key (dm/str (get obj :id))
:objects objects :is-sortable true
:key (dm/str (get obj :id)) :is-filtered is-filtered
:is-sortable true :depth -1
:is-filtered is-filtered :parent-size parent-size}]))]]))
:depth -1
:parent-size parent-size}]))]]))
(mf/defc layers-tree-wrapper* (mf/defc layers-tree-wrapper*
{::mf/private true} {::mf/private true}
@ -175,21 +176,22 @@
{::mf/wrap [mf/memo #(mf/throttle % 300)] {::mf/wrap [mf/memo #(mf/throttle % 300)]
::mf/private true} ::mf/private true}
[{:keys [objects parent-size]}] [{:keys [objects parent-size]}]
(let [selected (use-selected-shapes) (let [selected (use-selected-shapes)
root (get objects uuid/zero)] highlighted (mf/deref ref:highlighted-shapes)
root (get objects uuid/zero)]
[:ul {:class (stl/css :element-list)} [:ul {:class (stl/css :element-list)}
(for [[index id] (d/enumerate (:shapes root))] (for [[index id] (d/enumerate (:shapes root))]
(when-let [obj (get objects id)] (when-let [obj (get objects id)]
[:> layer-item* [:> layer-item* {:item obj
{:item obj :selected selected
:selected selected :highlighted highlighted
:index index :index index
:objects objects :objects objects
:key id :key id
:is-sortable false :is-sortable false
:is-filtered true :is-filtered true
:depth -1 :depth -1
:parent-size parent-size}]))])) :parent-size parent-size}]))]))
(defn calc-reparented-objects (defn calc-reparented-objects
[objects] [objects]
@ -208,8 +210,8 @@
;; --- Layers Toolbox ;; --- Layers Toolbox
(def ^:private ref:layers-panel-search (def ^:private ref:layers-search
(l/derived (l/key :layers-panel-search) refs/workspace-local)) (l/derived (l/key :layers-search) refs/workspace-local))
;; FIXME: optimize ;; FIXME: optimize
(defn- match-filters? (defn- match-filters?
@ -242,6 +244,21 @@
(false? (:masked-group shape)))) (false? (:masked-group shape))))
(and (contains? filters :mask) (true? (: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 (defn use-search
[page objects] [page objects]
(let [state* (mf/use-state (let [state* (mf/use-state
@ -254,7 +271,7 @@
:filters #{} :filters #{}
:num-items 100 :num-items 100
:current-match-idx 0})) :current-match-idx 0}))
layers-search-request (mf/deref ref:layers-panel-search) layers-search (mf/deref ref:layers-search)
state (deref state*) state (deref state*)
current-filters (:filters state) current-filters (:filters state)
current-items (:num-items state) current-items (:num-items state)
@ -265,13 +282,18 @@
find-replace-mode? (:find-replace-mode? state) find-replace-mode? (:find-replace-mode? state)
search-scope (:search-scope state) search-scope (:search-scope state)
current-match-idx (:current-match-idx state) current-match-idx (:current-match-idx state)
search-input-ref (mf/use-ref nil)
clear-search-text clear-search-text
(mf/use-fn (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 toggle-filters
(mf/use-fn #(swap! state* update :show-menu not)) (mf/use-fn
#(swap! state* update :show-menu not))
on-toggle-filters-click on-toggle-filters-click
(mf/use-fn (mf/use-fn
@ -280,51 +302,92 @@
(toggle-filters))) (toggle-filters)))
hide-menu hide-menu
(mf/use-fn #(swap! state* assoc :show-menu false)) (mf/use-fn
#(swap! state* assoc :show-menu false))
on-key-down 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 update-search-text
(mf/use-fn (mf/use-fn
(fn [value _event] (fn [value]
(swap! state* assoc :search-text value :num-items 100 :current-match-idx 0))) (swap! state* assoc
:search-text value
:num-items 100
:current-match-idx 0)))
update-replace-text 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 f-key? (kbd/is-key-ignore-case? "f")
(mf/use-fn #(swap! state* assoc :replace-text "")) 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 set-search-scope
(mf/use-fn (mf/use-fn
(fn [scope] (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 toggle-search
(mf/use-fn (mf/use-fn
(mf/deps show-search?)
(fn [event] (fn [event]
(let [node (dom/get-current-target event)] (let [node (dom/get-current-target event)]
(dom/blur! node) (dom/blur! node)
(swap! state* (fn [state] (if show-search?
(-> state (st/emit! dw/close-layers-search)
(assoc :search-text "" :replace-text "" :filters #{}) (st/emit! (dw/open-layers-search :find {:force? true}))))))
(assoc :show-menu false :find-replace-mode? false)
(assoc :search-scope :layers :num-items 100 :current-match-idx 0)
(update :show-search not)))))))
remove-filter remove-filter
(mf/use-fn (mf/use-fn
(fn [event] (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] (swap! state* (fn [state]
(-> state (update :filters disj fkey) (assoc :num-items 100))))))) (-> state
(update :filters disj fkey)
(assoc :num-items 100)))))))
add-filter add-filter
(mf/use-fn (mf/use-fn
(fn [event] (fn [event]
(dom/stop-propagation 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] (swap! state* (fn [state]
(-> state (-> state
(update :filters conj key) (update :filters conj key)
@ -374,7 +437,8 @@
(fn [_] (fn [_]
(when (pos? text-match-count) (when (pos? text-match-count)
(swap! state* update :current-match-idx (swap! state* update :current-match-idx
(fn [idx] (mod (inc idx) text-match-count)))))) (fn [idx]
(mod (inc idx) text-match-count))))))
navigate-prev navigate-prev
(mf/use-fn (mf/use-fn
@ -382,7 +446,8 @@
(fn [_] (fn [_]
(when (pos? text-match-count) (when (pos? text-match-count)
(swap! state* update :current-match-idx (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 handle-replace
(mf/use-fn (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-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)))))) (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 filtered-objects
(mf/with-memo [active? filtered-objects-all current-items] (mf/with-memo [active? filtered-objects-all current-items]
(when active? (when active?
@ -424,201 +507,242 @@
(events/unlistenByKey key1) (events/unlistenByKey key1)
(events/unlistenByKey key2)))) (events/unlistenByKey key2))))
(mf/with-effect [layers-search-request] (mf/with-effect [layers-search]
(when (some? layers-search-request) (if-let [{:keys [open? find-replace-mode? scope]} layers-search]
(let [replace-mode? (= layers-search-request :find-and-replace)] (when open?
(swap! state* (fn [s] (swap! state* (fn [s]
(-> s (let [mode-changed? (not= (:find-replace-mode? s) find-replace-mode?)
(assoc :show-search true :find-replace-mode? replace-mode?) opening? (not (:show-search s))]
(assoc :search-scope (if replace-mode? :canvas :layers)) (-> s
(assoc :search-text "" :replace-text "" :current-match-idx 0))))) (assoc :show-search true
(st/emit! dw/clear-layers-search))) :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 [filtered-objects
handle-show-more handle-show-more
#(mf/html #(mf/html
(if show-search? (if show-search?
[:* [:*
[:div {:class (stl/css :tool-window-bar :search)} [:div {:class (stl/css :tool-window-bar)}
[:> search-bar* {:on-change update-search-text [:> search-bar* {:input-ref search-input-ref
:class (stl/css :search-item)
:on-change update-search-text
:value current-search :value current-search
:on-clear clear-search-text :on-clear clear-search-text
:on-key-down handle-find-shortcut-keydown
:placeholder (tr "workspace.sidebar.layers.search")} :placeholder (tr "workspace.sidebar.layers.search")}
[:button {:on-click on-toggle-filters-click [:> icon-button* {:variant "secondary"
:class (stl/css-case :filter-button true :opened show-menu? :active active?)} :class (stl/css :filter-button)
[:> icon* {:icon-id i/filter}]]] :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" [:> icon-button* {:variant "ghost"
:aria-label (tr "labels.close") :aria-label (tr "labels.close")
:on-click toggle-search :on-click toggle-search
:icon i/close}]] :icon i/close}]]
[:div {:class (stl/css :search-scope-row)} [:div {:class (stl/css :replace-wrapper)}
[:label {:class (stl/css-case :scope-option true :scope-selected (= :canvas search-scope))} (when ^boolean find-replace-mode?
[:span {:class (stl/css-case :scope-radio true :scope-radio-checked (= :canvas search-scope))}] [:div {:class (stl/css :replace-row)}
[:input {:type "radio" :name "search-scope" :class (stl/css :scope-radio-input) [:> input* {:type "text"
:checked (= :canvas search-scope) :placeholder (tr "workspace.sidebar.layers.replace-placeholder")
:on-change (fn [_] (set-search-scope :canvas))}] :on-key-down handle-find-shortcut-keydown
[:span {:class (stl/css :scope-label)} :on-change update-replace-text}]
(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")]]]
(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) (when (d/not-empty? current-search)
(if (pos? text-match-count) (if (pos? text-match-count)
[:div {:class (stl/css :match-navigation)} [:div {:class (stl/css :replace-match-navigation)}
[:span {:class (stl/css :match-count)} [:span {:class (stl/css :replace-match-count)}
(dm/str (inc safe-match-idx) " / " text-match-count)] (dm/str (inc safe-match-idx) " / " text-match-count)]
[:> icon-button* {:variant "ghost" :aria-label (tr "labels.previous") [:> icon-button* {:variant "ghost" :aria-label (tr "labels.previous")
:on-click navigate-prev :icon i/arrow-up}] :on-click navigate-prev :icon i/arrow-up}]
[:> icon-button* {:variant "ghost" :aria-label (tr "labels.next") [:> icon-button* {:variant "ghost" :aria-label (tr "labels.next")
:on-click navigate-next :icon i/arrow-down}]] :on-click navigate-next :icon i/arrow-down}]]
[:span {:class (stl/css :no-matches)} [:div {:class (stl/css :replace-match-count)}
(tr "workspace.sidebar.layers.no-matches")]))] (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)} [:div {:class (stl/css :replace-actions-row)}
[:button {:class (stl/css :replace-button) [:> button* {:variant "secondary"
:on-click handle-replace :class (stl/css :replace-actions-button)
:disabled (or (zero? text-match-count) (str/empty? current-search))} :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")] (tr "workspace.sidebar.layers.replace")]
[:button {:class (stl/css :replace-button) [:> button* {:variant "secondary"
:on-click handle-replace-all :class (stl/css :replace-actions-button)
:disabled (or (zero? text-match-count) (str/empty? current-search))} :on-click handle-replace-all
(tr "workspace.sidebar.layers.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)} [:div {:class (stl/css :active-filters)}
(for [fkey current-filters] (for [fkey current-filters]
(let [fname (d/name fkey) (let [fname (d/name fkey)
name (case fkey name (case fkey
:frame (tr "workspace.sidebar.layers.frames") :frame (tr "workspace.sidebar.layers.frames")
:group (tr "workspace.sidebar.layers.groups") :group (tr "workspace.sidebar.layers.groups")
:mask (tr "workspace.sidebar.layers.masks") :mask (tr "workspace.sidebar.layers.masks")
:component (tr "workspace.sidebar.layers.components") :component (tr "workspace.sidebar.layers.components")
:text (tr "workspace.sidebar.layers.texts") :text (tr "workspace.sidebar.layers.texts")
:image (tr "workspace.sidebar.layers.images") :image (tr "workspace.sidebar.layers.images")
:shape (tr "workspace.sidebar.layers.shapes") :shape (tr "workspace.sidebar.layers.shapes")
(tr fkey)) (tr fkey))
filter-icon (usi/get-shape-icon-by-type fkey)] filter-icon (usi/get-shape-icon-by-type fkey)]
[:button {:class (stl/css :layer-filter) [:button {:class (stl/css :layer-filter)
:key fname :key fname
:data-filter fname :data-filter fname
:on-click remove-filter} :on-click remove-filter}
[:> icon* {:icon-id filter-icon :size "s" :class (stl/css :layer-filter-icon)}] [:> icon* {:icon-id filter-icon :size "s" :class (stl/css :layer-filter-icon)}]
[:span {:class (stl/css :layer-filter-name)} [:span {:class (stl/css :layer-filter-name)}
name] name]
[:> icon* {:icon-id i/close-small :class (stl/css :layer-filter-close)}]]))] [:> icon* {:icon-id i/close-small :class (stl/css :layer-filter-close)}]]))]
(when ^boolean show-menu? (when ^boolean show-menu?
[:ul {:class (stl/css :filters-container)} [:ul {:class (stl/css :filters-container)}
[:li {:class (stl/css-case :filter-menu-item true [:li {:class (stl/css-case :filter-menu-item true
:selected (contains? current-filters :frame)) :selected (contains? current-filters :frame))
:data-filter "frame" :data-filter "frame"
:on-click add-filter} :on-click add-filter}
[:div {:class (stl/css :filter-menu-item-name-wrapper)} [:div {:class (stl/css :filter-menu-item-name-wrapper)}
[:> icon* {:icon-id i/board :size "s" :class (stl/css :filter-menu-item-icon)}] [:> icon* {:icon-id i/board :size "s" :class (stl/css :filter-menu-item-icon)}]
[:span {:class (stl/css :filter-menu-item-name)} [:span {:class (stl/css :filter-menu-item-name)}
(tr "workspace.sidebar.layers.frames")]] (tr "workspace.sidebar.layers.frames")]]
(when (contains? current-filters :frame) (when (contains? current-filters :frame)
[:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])]
[:li {:class (stl/css-case :filter-menu-item true [:li {:class (stl/css-case :filter-menu-item true
:selected (contains? current-filters :group)) :selected (contains? current-filters :group))
:data-filter "group" :data-filter "group"
:on-click add-filter} :on-click add-filter}
[:div {:class (stl/css :filter-menu-item-name-wrapper)} [:div {:class (stl/css :filter-menu-item-name-wrapper)}
[:> icon* {:icon-id i/group :size "s" :class (stl/css :filter-menu-item-icon)}] [:> icon* {:icon-id i/group :size "s" :class (stl/css :filter-menu-item-icon)}]
[:span {:class (stl/css :filter-menu-item-name)} [:span {:class (stl/css :filter-menu-item-name)}
(tr "workspace.sidebar.layers.groups")]] (tr "workspace.sidebar.layers.groups")]]
(when (contains? current-filters :group) (when (contains? current-filters :group)
[:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])]
[:li {:class (stl/css-case :filter-menu-item true [:li {:class (stl/css-case :filter-menu-item true
:selected (contains? current-filters :mask)) :selected (contains? current-filters :mask))
:data-filter "mask" :data-filter "mask"
:on-click add-filter} :on-click add-filter}
[:div {:class (stl/css :filter-menu-item-name-wrapper)} [:div {:class (stl/css :filter-menu-item-name-wrapper)}
[:> icon* {:icon-id i/mask :size "s" :class (stl/css :filter-menu-item-icon)}] [:> icon* {:icon-id i/mask :size "s" :class (stl/css :filter-menu-item-icon)}]
[:span {:class (stl/css :filter-menu-item-name)} [:span {:class (stl/css :filter-menu-item-name)}
(tr "workspace.sidebar.layers.masks")]] (tr "workspace.sidebar.layers.masks")]]
(when (contains? current-filters :mask) (when (contains? current-filters :mask)
[:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])]
[:li {:class (stl/css-case :filter-menu-item true [:li {:class (stl/css-case :filter-menu-item true
:selected (contains? current-filters :component)) :selected (contains? current-filters :component))
:data-filter "component" :data-filter "component"
:on-click add-filter} :on-click add-filter}
[:div {:class (stl/css :filter-menu-item-name-wrapper)} [:div {:class (stl/css :filter-menu-item-name-wrapper)}
[:> icon* {:icon-id i/component :size "s" :class (stl/css :filter-menu-item-icon)}] [:> icon* {:icon-id i/component :size "s" :class (stl/css :filter-menu-item-icon)}]
[:span {:class (stl/css :filter-menu-item-name)} [:span {:class (stl/css :filter-menu-item-name)}
(tr "workspace.sidebar.layers.components")]] (tr "workspace.sidebar.layers.components")]]
(when (contains? current-filters :component) (when (contains? current-filters :component)
[:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])]
[:li {:class (stl/css-case :filter-menu-item true [:li {:class (stl/css-case :filter-menu-item true
:selected (contains? current-filters :text)) :selected (contains? current-filters :text))
:data-filter "text" :data-filter "text"
:on-click add-filter} :on-click add-filter}
[:div {:class (stl/css :filter-menu-item-name-wrapper)} [:div {:class (stl/css :filter-menu-item-name-wrapper)}
[:> icon* {:icon-id i/text :size "s" :class (stl/css :filter-menu-item-icon)}] [:> icon* {:icon-id i/text :size "s" :class (stl/css :filter-menu-item-icon)}]
[:span {:class (stl/css :filter-menu-item-name)} [:span {:class (stl/css :filter-menu-item-name)}
(tr "workspace.sidebar.layers.texts")]] (tr "workspace.sidebar.layers.texts")]]
(when (contains? current-filters :text) (when (contains? current-filters :text)
[:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])]
[:li {:class (stl/css-case :filter-menu-item true [:li {:class (stl/css-case :filter-menu-item true
:selected (contains? current-filters :image)) :selected (contains? current-filters :image))
:data-filter "image" :data-filter "image"
:on-click add-filter} :on-click add-filter}
[:div {:class (stl/css :filter-menu-item-name-wrapper)} [:div {:class (stl/css :filter-menu-item-name-wrapper)}
[:> icon* {:icon-id i/img :size "s" :class (stl/css :filter-menu-item-icon)}] [:> icon* {:icon-id i/img :size "s" :class (stl/css :filter-menu-item-icon)}]
[:span {:class (stl/css :filter-menu-item-name)} [:span {:class (stl/css :filter-menu-item-name)}
(tr "workspace.sidebar.layers.images")]] (tr "workspace.sidebar.layers.images")]]
(when (contains? current-filters :image) (when (contains? current-filters :image)
[:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])] [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])]
[:li {:class (stl/css-case :filter-menu-item true [:li {:class (stl/css-case :filter-menu-item true
:selected (contains? current-filters :shape)) :selected (contains? current-filters :shape))
:data-filter "shape" :data-filter "shape"
:on-click add-filter} :on-click add-filter}
[:div {:class (stl/css :filter-menu-item-name-wrapper)} [:div {:class (stl/css :filter-menu-item-name-wrapper)}
[:> icon* {:icon-id i/path :size "s" :class (stl/css :filter-menu-item-icon)}] [:> icon* {:icon-id i/path :size "s" :class (stl/css :filter-menu-item-icon)}]
[:span {:class (stl/css :filter-menu-item-name)} [:span {:class (stl/css :filter-menu-item-name)}
(tr "workspace.sidebar.layers.shapes")]] (tr "workspace.sidebar.layers.shapes")]]
(when (contains? current-filters :shape) (when (contains? current-filters :shape)
[:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])]])] [:> icon* {:icon-id i/tick :size "s" :class (stl/css :filter-menu-item-tick)}])]])]]
[:div {:class (stl/css :tool-window-bar)} [:div {:class (stl/css :tool-window-bar)}
[:> title-bar* {:collapsable false [:> title-bar* {:collapsable false
:class (stl/css :tool-window-bar-title)
:title (:name page) :title (:name page)
:on-btn-click toggle-search :on-btn-click toggle-search
:btn-icon "search" :btn-icon "search"
:btn-title (tr "labels.search")}]]))])) :btn-title (tr "labels.search")}]]))]))
(defn- on-scroll (defn- on-scroll
[event] [event]
(let [children (dom/get-elements-by-class "sticky-children") (let [children (dom/get-elements-by-class "sticky-children")
@ -699,19 +823,24 @@
(mf/use-fn (mf/use-fn
#(st/emit! (dw/toggle-focus-mode)))] #(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) (if (d/not-empty? focus)
[:div {:class (stl/css :tool-window-bar)} [:div {:class (stl/css :tool-window-bar)}
[:button {:class (stl/css :focus-title) [:button {:class (stl/css :focus-title)
:on-click toogle-focus-mode} :on-click toogle-focus-mode}
[:span {:class (stl/css :back-button)} [:span {:class (stl/css :focus-back-button)}
[:> icon* {:icon-id i/arrow}]] [:> icon* {:icon-id i/arrow-left}]]
[:div {:class (stl/css :focus-name)} [:div {:class (stl/css :focus-name)}
(or title (tr "workspace.sidebar.layers"))] (or title (tr "workspace.sidebar.layers"))]
[:div {:class (stl/css :focus-mode-tag-wrapper)} [: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)) (filter-component))
@ -724,11 +853,11 @@
:key (dm/str page-id) :key (dm/str page-id)
:parent-size size-parent}] :parent-size size-parent}]
[:div {:ref lazy-load-ref}]] [:div {:ref lazy-load-ref}]]
[:div {:on-scroll on-scroll [:div {:on-scroll on-scroll
:class (stl/css :tool-window-content) :class (stl/css :tool-window-content)
:data-scroll-container true :data-scroll-container true
:style {:display (when (some? filtered-objects) "none")}} :style {:display (when (some? filtered-objects) "none")}}
[:> layers-tree-wrapper* {:objects filtered-objects [:> layers-tree-wrapper* {:objects filtered-objects
:key (dm/str page-id) :key (dm/str page-id)
:is-filtered true :is-filtered true

View File

@ -5,144 +5,100 @@
// Copyright (c) KALEIDOS INC // Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated; @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 { .tool-window-bar {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
height: deprecated.$s-32; gap: var(--sp-xs);
min-height: deprecated.$s-32; padding: var(--sp-m) var(--sp-m) 0 var(--sp-m);
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;
}
}
}
} }
.page-name { .tool-window-bar-title {
@include deprecated.uppercase-title-typography; margin-block-end: var(--sp-s);
padding: 0 deprecated.$s-12;
color: var(--title-foreground-color);
} }
.icon-search { .tool-window-content {
@extend %button-tertiary; --calculated-height: calc(#{px2rem(136)} + var(--height, #{$sz-200}));
height: deprecated.$s-32; display: flex;
width: deprecated.$s-28; flex-direction: column;
border-radius: deprecated.$br-8; block-size: calc(100vh - var(--calculated-height));
margin-right: deprecated.$s-8; inline-size: calc(var(--left-sidebar-width) + var(--depth) * var(--layer-indentation-size));
padding: 0; overflow: auto;
scrollbar-gutter: stable;
}
svg { .filter-button {
@extend %button-icon; border-start-end-radius: 0;
border-end-end-radius: 0;
stroke: var(--icon-foreground);
}
} }
.focus-title { .focus-title {
@include deprecated.button-style; border: none;
background: none;
block-size: var(--sp-xxxl);
inline-size: 100%;
display: grid; display: grid;
grid-template-columns: auto 1fr auto; grid-template-columns: auto 1fr auto;
align-items: center; align-items: center;
width: 100%; margin-block-end: var(--sp-s);
padding: 0; padding: 0;
} }
.back-button { .focus-back-button {
@include deprecated.flex-center; display: flex;
align-items: center;
height: deprecated.$s-32; justify-content: center;
width: deprecated.$s-24; block-size: var(--sp-xxxl);
padding: 0 deprecated.$s-4 0 deprecated.$s-8; padding-inline-start: var(--sp-s);
color: var(--icon-foreground);
svg {
@extend %button-icon-small;
stroke: var(--icon-foreground);
transform: rotate(180deg);
}
} }
.focus-name { .focus-name {
@include deprecated.text-ellipsis; @include t.use-typography("body-small");
@include deprecated.body-small-typography; @include text-ellipsis;
padding-left: deprecated.$s-4; padding-inline-end: var(--sp-xs);
color: var(--title-foreground-color); color: var(--title-foreground-color);
} }
.focus-mode-tag-wrapper { .focus-mode-tag-wrapper {
@include deprecated.flex-center; display: flex;
align-items: center;
height: 100%; justify-content: center;
margin-right: deprecated.$s-12; block-size: 100%;
margin-inline-end: var(--sp-m);
} }
.active-filters { .active-filters {
@include deprecated.flex-row; display: flex;
align-items: center;
gap: var(--sp-xs);
flex-wrap: wrap; flex-wrap: wrap;
margin: 0 deprecated.$s-12; margin: 0 var(--sp-m);
} }
.layer-filter { .layer-filter {
@extend %button-tag; @extend %button-tag;
gap: deprecated.$s-6; gap: px2rem(6);
height: deprecated.$s-24; block-size: var(--sp-xxl);
margin: deprecated.$s-2 0; margin: var(--sp-xxs) 0;
border-radius: deprecated.$br-6; border-radius: $br-6;
background-color: var(--pill-background-color); background-color: var(--pill-background-color);
cursor: pointer;
} }
.layer-filter-icon, .layer-filter-icon,
@ -151,9 +107,11 @@
} }
.layer-filter-name { .layer-filter-name {
@include deprecated.flex-center; @include t.use-typography("body-small");
@include deprecated.body-small-typography;
display: flex;
align-items: center;
justify-content: center;
color: var(--pill-foreground-color); color: var(--pill-foreground-color);
} }
@ -161,34 +119,117 @@
position: relative; 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 { .filters-container {
@extend %menu-dropdown; @extend %menu-dropdown;
position: absolute; position: absolute;
left: deprecated.$s-20; inline-size: $sz-192;
width: deprecated.$s-192;
.filter-menu-item { .filter-menu-item {
@include deprecated.body-small-typography; @include t.use-typography("body-small");
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: 100%; inline-size: 100%;
padding: deprecated.$s-6; padding: px2rem(6);
border-radius: deprecated.$br-8; border-radius: $br-8;
.filter-menu-item-name-wrapper { .filter-menu-item-name-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
gap: deprecated.$s-8; gap: var(--sp-s);
.filter-menu-item-icon { .filter-menu-item-icon {
color: var(--menu-foreground-color); color: var(--menu-foreground-color);
} }
.filter-menu-item-name { .filter-menu-item-name {
padding-top: deprecated.$s-2; padding-block-start: var(--sp-xxs);
color: var(--menu-foreground-color); 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;
}

View File

@ -111,6 +111,8 @@
(tr "shortcuts.duplicate") (tr "shortcuts.duplicate")
(tr "shortcuts.escape") (tr "shortcuts.escape")
(tr "shortcuts.export-shapes") (tr "shortcuts.export-shapes")
(tr "shortcuts.find")
(tr "shortcuts.find-and-replace")
(tr "shortcuts.fit-all") (tr "shortcuts.fit-all")
(tr "shortcuts.flip-horizontal") (tr "shortcuts.flip-horizontal")
(tr "shortcuts.flip-vertical") (tr "shortcuts.flip-vertical")
@ -160,6 +162,7 @@
(tr "shortcuts.open-viewer") (tr "shortcuts.open-viewer")
(tr "shortcuts.open-workspace") (tr "shortcuts.open-workspace")
(tr "shortcuts.paste") (tr "shortcuts.paste")
(tr "shortcuts.paste-replace")
(tr "shortcuts.prev-frame") (tr "shortcuts.prev-frame")
(tr "shortcuts.redo") (tr "shortcuts.redo")
(tr "shortcuts.rename") (tr "shortcuts.rename")

View File

@ -4662,6 +4662,14 @@ msgstr "Cancel"
msgid "shortcuts.export-shapes" msgid "shortcuts.export-shapes"
msgstr "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 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:114
msgid "shortcuts.fit-all" msgid "shortcuts.fit-all"
msgstr "Zoom to fit all" msgstr "Zoom to fit all"
@ -4866,6 +4874,10 @@ msgstr " or "
msgid "shortcuts.paste" msgid "shortcuts.paste"
msgstr "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 #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:111
#, unused #, unused
msgid "shortcuts.paste-props" msgid "shortcuts.paste-props"
@ -8148,7 +8160,10 @@ msgstr "Masks"
#: src/app/main/ui/workspace/sidebar/layers.cljs:293 #: src/app/main/ui/workspace/sidebar/layers.cljs:293
msgid "workspace.sidebar.layers.search" 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 #: src/app/main/ui/workspace/sidebar/layers.cljs:316, src/app/main/ui/workspace/sidebar/layers.cljs:410
msgid "workspace.sidebar.layers.shapes" msgid "workspace.sidebar.layers.shapes"
@ -8180,11 +8195,11 @@ msgstr "No matches"
#: src/app/main/ui/workspace/sidebar/layers.cljs #: src/app/main/ui/workspace/sidebar/layers.cljs
msgid "workspace.sidebar.layers.search-scope-layers" msgid "workspace.sidebar.layers.search-scope-layers"
msgstr "Search layers" msgstr "Layer names"
#: src/app/main/ui/workspace/sidebar/layers.cljs #: src/app/main/ui/workspace/sidebar/layers.cljs
msgid "workspace.sidebar.layers.search-scope-canvas" 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 #: 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" msgid "workspace.sidebar.options.svg-attrs.title"

View File

@ -5897,6 +5897,14 @@ msgstr "Rehacer"
msgid "workspace.header.menu.select-all" msgid "workspace.header.menu.select-all"
msgstr "Seleccionar todo" 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 #: src/app/main/ui/workspace/main_menu.cljs:423
msgid "workspace.header.menu.show-artboard-names" msgid "workspace.header.menu.show-artboard-names"
msgstr "Mostrar nombres de tableros" msgstr "Mostrar nombres de tableros"
@ -7968,6 +7976,9 @@ msgstr "Máscaras"
msgid "workspace.sidebar.layers.search" msgid "workspace.sidebar.layers.search"
msgstr "Buscar capas" 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 #: src/app/main/ui/workspace/sidebar/layers.cljs:316, src/app/main/ui/workspace/sidebar/layers.cljs:410
msgid "workspace.sidebar.layers.shapes" msgid "workspace.sidebar.layers.shapes"
msgstr "Formas" msgstr "Formas"