mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
✨ Add Find & Replace for text content and layer names (#8899)
* ✨ Add Find & Replace for text content and layer names * 💄 Fix cross-browser styling for Find & Replace radio buttons and action buttons * 💄 Fix stylelint empty line before declaration in layers.scss * ⚡ Improve match-filters and match-ids efficiency --------- Signed-off-by: Andrey Antukh <niwi@niwi.nz> Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
parent
19b9c696fc
commit
d90e7f8164
@ -27,6 +27,7 @@
|
||||
- Save and restore selection state in undo/redo (by @eureka928) [Github #6007](https://github.com/penpot/penpot/issues/6007)
|
||||
- Fix warnings for unsupported token $type (by @Dexterity104) [Github #8790](https://github.com/penpot/penpot/issues/8790)
|
||||
- Add per-group add button for typographies (by @eureka928) [Github #5275](https://github.com/penpot/penpot/issues/5275)
|
||||
- Add Find & Replace for text content (by @statxc) [Github #7108](https://github.com/penpot/penpot/issues/7108)
|
||||
- Use page name for multi-export ZIP/PDF downloads (by @Dexterity104) [Github #8773](https://github.com/penpot/penpot/issues/8773)
|
||||
- Make links in comments clickable (by @eureka928) [Github #1602](https://github.com/penpot/penpot/issues/1602)
|
||||
- Add visibility toggle for strokes (by @eureka928) [Github #7438](https://github.com/penpot/penpot/issues/7438)
|
||||
|
||||
@ -354,6 +354,32 @@
|
||||
[k (get attrs k v)]))))
|
||||
|
||||
|
||||
(defn content-has-text?
|
||||
[content search]
|
||||
(let [search-lower (str/lower search)]
|
||||
(->> (node-seq is-text-node? content)
|
||||
(some #(str/includes? (str/lower (:text %)) search-lower))
|
||||
(boolean))))
|
||||
|
||||
(defn replace-all-case-insensitive
|
||||
[text search replacement]
|
||||
(let [text-lower (str/lower text)
|
||||
search-lower (str/lower search)
|
||||
search-len (count search)]
|
||||
(loop [result "" idx 0]
|
||||
(let [found (str/index-of text-lower search-lower idx)]
|
||||
(if (nil? found)
|
||||
(str result (subs text idx))
|
||||
(recur (str result (subs text idx found) replacement)
|
||||
(+ found search-len)))))))
|
||||
|
||||
(defn replace-text-in-content
|
||||
[content search replacement]
|
||||
(transform-nodes
|
||||
is-text-node?
|
||||
(fn [node] (update node :text replace-all-case-insensitive search replacement))
|
||||
content))
|
||||
|
||||
(defn content->text
|
||||
"Given a root node of a text content extracts the texts with its associated styles"
|
||||
[content]
|
||||
|
||||
@ -1420,6 +1420,19 @@
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-global :clipboard-style] style))))
|
||||
|
||||
(defn open-layers-search
|
||||
[mode]
|
||||
(ptk/reify ::open-layers-search
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :layers-panel-search] mode))))
|
||||
|
||||
(def clear-layers-search
|
||||
(ptk/reify ::clear-layers-search
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-local dissoc :layers-panel-search))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Exports
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@ -146,6 +146,11 @@
|
||||
:subsections [:edit]
|
||||
:fn #(st/emit! esc-pressed)}
|
||||
|
||||
:find {:tooltip (ds/meta "F") :command (ds/c-mod "f") :subsections [:edit]
|
||||
:fn #(st/emit! (dw/open-layers-search :find))}
|
||||
:find-and-replace {:tooltip (ds/meta "H") :command (ds/c-mod "h") :subsections [:edit]
|
||||
:fn #(st/emit! (dw/open-layers-search :find-and-replace))}
|
||||
|
||||
;; MODIFY LAYERS
|
||||
|
||||
:rename {:tooltip (ds/alt "N")
|
||||
|
||||
@ -1155,6 +1155,35 @@
|
||||
(gsh/transform-shape (ctm/change-size shape width height))))))
|
||||
{:undo-group (when new-shape? id)})))))))
|
||||
|
||||
(defn replace-layer-names-in-shapes
|
||||
[ids search replacement]
|
||||
(ptk/reify ::replace-layer-names-in-shapes
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [undo-group (uuid/next)]
|
||||
(rx/of
|
||||
(dwsh/update-shapes
|
||||
ids
|
||||
(fn [shape] (update shape :name txt/replace-all-case-insensitive search replacement))
|
||||
{:attrs #{:name} :undo-group undo-group}))))))
|
||||
|
||||
(defn replace-text-in-shapes
|
||||
[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}))))))
|
||||
|
||||
;; -- Text Editor v3
|
||||
|
||||
;; @see texts_v3.cljs
|
||||
|
||||
@ -454,6 +454,12 @@
|
||||
(mf/use-fn
|
||||
#(st/emit! (dw/select-all)))
|
||||
|
||||
find
|
||||
(mf/use-fn (fn [] (on-close) (st/emit! (dw/open-layers-search :find))))
|
||||
|
||||
find-and-replace
|
||||
(mf/use-fn (fn [] (on-close) (st/emit! (dw/open-layers-search :find-and-replace))))
|
||||
|
||||
undo
|
||||
(mf/use-fn
|
||||
#(st/emit! dwu/undo))
|
||||
@ -476,6 +482,20 @@
|
||||
(tr "workspace.header.menu.select-all")]
|
||||
[:> shortcuts* {:id :select-all}]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item)
|
||||
:on-click find
|
||||
:on-key-down (fn [event] (when (kbd/enter? event) (find event)))
|
||||
:id "file-menu-find"}
|
||||
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.find")]
|
||||
[:> shortcuts* {:id :find}]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item)
|
||||
:on-click find-and-replace
|
||||
:on-key-down (fn [event] (when (kbd/enter? event) (find-and-replace event)))
|
||||
:id "file-menu-find-and-replace"}
|
||||
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.find-and-replace")]
|
||||
[:> shortcuts* {:id :find-and-replace}]]
|
||||
|
||||
(when can-edit
|
||||
[:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item)
|
||||
:on-click undo
|
||||
|
||||
@ -11,8 +11,10 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.search-bar :refer [search-bar*]]
|
||||
@ -205,61 +207,70 @@
|
||||
|
||||
;; --- Layers Toolbox
|
||||
|
||||
(def ^:private ref:layers-panel-search
|
||||
(l/derived (l/key :layers-panel-search) refs/workspace-local))
|
||||
|
||||
;; FIXME: optimize
|
||||
(defn- match-filters?
|
||||
[state [id shape]]
|
||||
(let [search (:search-text state)
|
||||
scope (:search-scope state)
|
||||
filters (:filters state)
|
||||
filters (cond-> filters
|
||||
(contains? filters :shape)
|
||||
(conj :rect :circle :path :bool))]
|
||||
(conj :rect :circle :path :bool))
|
||||
text-match? (case scope
|
||||
:canvas (and (= :text (:type shape))
|
||||
(some? (:content shape))
|
||||
(txt/content-has-text? (:content shape) search))
|
||||
(or (str/includes? (str/lower (:name shape)) (str/lower search))
|
||||
(str/includes? (str/lower (:variant-name shape)) (str/lower search))
|
||||
;; Dev-only: allow search by id
|
||||
(and *assert* (str/includes? (dm/str (:id shape)) (str/lower search)))))]
|
||||
(or (= uuid/zero id)
|
||||
(and (or (str/includes? (str/lower (:name shape)) (str/lower search))
|
||||
(str/includes? (str/lower (:variant-name shape)) (str/lower search))
|
||||
;; Only for local development we allow search for ids. Otherwise will be hard
|
||||
;; search for numbers or single letter shape names (ie: "A")
|
||||
(and *assert*
|
||||
(str/includes? (dm/str (:id shape)) (str/lower search))))
|
||||
(and text-match?
|
||||
(or (empty? filters)
|
||||
(and (contains? filters :component)
|
||||
(contains? shape :component-id))
|
||||
(and (contains? filters :image)
|
||||
(some? (cts/has-images? shape)))
|
||||
|
||||
(and (contains? filters :component) (contains? shape :component-id))
|
||||
(and (contains? filters :image) (some? (cts/has-images? shape)))
|
||||
(let [direct-filters (into #{} (filter #{:frame :rect :circle :path :bool :text}) filters)]
|
||||
(contains? direct-filters (:type shape)))
|
||||
(and (contains? filters :group)
|
||||
(and (cfh/group-shape? shape)
|
||||
(not (contains? shape :component-id))
|
||||
(or (not (contains? shape :masked-group))
|
||||
(false? (:masked-group shape)))))
|
||||
(and (contains? filters :mask)
|
||||
(true? (:masked-group shape))))))))
|
||||
(cfh/group-shape? shape)
|
||||
(not (contains? shape :component-id))
|
||||
(or (not (contains? shape :masked-group))
|
||||
(false? (:masked-group shape))))
|
||||
(and (contains? filters :mask) (true? (:masked-group shape))))))))
|
||||
|
||||
(defn use-search
|
||||
[page objects]
|
||||
(let [state* (mf/use-state
|
||||
#(do {:show-search false
|
||||
:show-menu false
|
||||
:search-text ""
|
||||
:filters #{}
|
||||
:num-items 100}))
|
||||
|
||||
state (deref state*)
|
||||
current-filters (:filters state)
|
||||
current-items (:num-items state)
|
||||
current-search (:search-text state)
|
||||
show-menu? (:show-menu state)
|
||||
show-search? (:show-search state)
|
||||
(let [state* (mf/use-state
|
||||
#(do {:show-search false
|
||||
:find-replace-mode? false
|
||||
:search-scope :layers
|
||||
:show-menu false
|
||||
:search-text ""
|
||||
:replace-text ""
|
||||
:filters #{}
|
||||
:num-items 100
|
||||
:current-match-idx 0}))
|
||||
layers-search-request (mf/deref ref:layers-panel-search)
|
||||
state (deref state*)
|
||||
current-filters (:filters state)
|
||||
current-items (:num-items state)
|
||||
current-search (:search-text state)
|
||||
replace-text (:replace-text state)
|
||||
show-menu? (:show-menu state)
|
||||
show-search? (:show-search state)
|
||||
find-replace-mode? (:find-replace-mode? state)
|
||||
search-scope (:search-scope state)
|
||||
current-match-idx (:current-match-idx state)
|
||||
|
||||
clear-search-text
|
||||
(mf/use-fn
|
||||
#(swap! state* assoc :search-text "" :num-items 100))
|
||||
|
||||
#(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
|
||||
@ -268,18 +279,26 @@
|
||||
(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)))
|
||||
(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)))
|
||||
|
||||
clear-replace-text
|
||||
(mf/use-fn #(swap! state* assoc :replace-text ""))
|
||||
|
||||
set-search-scope
|
||||
(mf/use-fn
|
||||
(fn [scope]
|
||||
(swap! state* assoc :search-scope scope :num-items 100 :current-match-idx 0)))
|
||||
|
||||
toggle-search
|
||||
(mf/use-fn
|
||||
@ -288,30 +307,23 @@
|
||||
(dom/blur! node)
|
||||
(swap! state* (fn [state]
|
||||
(-> state
|
||||
(assoc :search-text "")
|
||||
(assoc :filters #{})
|
||||
(assoc :show-menu false)
|
||||
(assoc :num-items 100)
|
||||
(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)))))))
|
||||
|
||||
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)
|
||||
@ -331,6 +343,65 @@
|
||||
filtered-objects-total
|
||||
(count filtered-objects-all)
|
||||
|
||||
canvas-match-ids
|
||||
(mf/with-memo [objects current-search search-scope]
|
||||
(when (and (= :canvas search-scope) (d/not-empty? current-search))
|
||||
(reduce-kv (fn [acc id shape]
|
||||
(cond-> acc
|
||||
(and (= :text (:type shape))
|
||||
(some? (:content shape))
|
||||
(txt/content-has-text? (:content shape) current-search))
|
||||
(conj id)))
|
||||
[] objects)))
|
||||
|
||||
layer-match-ids
|
||||
(mf/with-memo [objects current-search search-scope]
|
||||
(when (and (= :layers search-scope) (d/not-empty? current-search))
|
||||
(reduce-kv (fn [acc id shape]
|
||||
(cond-> acc
|
||||
(str/includes? (str/lower (:name shape)) (str/lower current-search))
|
||||
(conj id)))
|
||||
[] objects)))
|
||||
|
||||
text-match-ids (if (= :canvas search-scope) canvas-match-ids layer-match-ids)
|
||||
text-match-count (count text-match-ids)
|
||||
safe-match-idx (if (pos? text-match-count) (mod current-match-idx text-match-count) 0)
|
||||
|
||||
navigate-next
|
||||
(mf/use-fn
|
||||
(mf/deps text-match-count)
|
||||
(fn [_]
|
||||
(when (pos? text-match-count)
|
||||
(swap! state* update :current-match-idx
|
||||
(fn [idx] (mod (inc idx) text-match-count))))))
|
||||
|
||||
navigate-prev
|
||||
(mf/use-fn
|
||||
(mf/deps text-match-count)
|
||||
(fn [_]
|
||||
(when (pos? text-match-count)
|
||||
(swap! state* update :current-match-idx
|
||||
(fn [idx] (mod (+ (dec idx) text-match-count) text-match-count))))))
|
||||
|
||||
handle-replace
|
||||
(mf/use-fn
|
||||
(mf/deps text-match-ids safe-match-idx replace-text current-search search-scope)
|
||||
(fn [_]
|
||||
(when (and (pos? text-match-count) (d/not-empty? current-search))
|
||||
(let [id (nth text-match-ids safe-match-idx)]
|
||||
(if (= :canvas search-scope)
|
||||
(st/emit! (dwt/replace-text-in-shapes [id] current-search replace-text))
|
||||
(st/emit! (dwt/replace-layer-names-in-shapes [id] current-search replace-text)))))))
|
||||
|
||||
handle-replace-all
|
||||
(mf/use-fn
|
||||
(mf/deps text-match-ids replace-text current-search search-scope)
|
||||
(fn [_]
|
||||
(when (and (pos? text-match-count) (d/not-empty? current-search))
|
||||
(if (= :canvas search-scope)
|
||||
(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))))))
|
||||
|
||||
filtered-objects
|
||||
(mf/with-memo [active? filtered-objects-all current-items]
|
||||
(when active?
|
||||
@ -352,6 +423,16 @@
|
||||
(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)]
|
||||
(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)))
|
||||
|
||||
[filtered-objects
|
||||
handle-show-more
|
||||
#(mf/html
|
||||
@ -363,17 +444,62 @@
|
||||
:on-clear clear-search-text
|
||||
: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?)}
|
||||
:class (stl/css-case :filter-button true :opened show-menu? :active active?)}
|
||||
[:> icon* {:icon-id i/filter}]]]
|
||||
|
||||
[:> 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")]]]
|
||||
|
||||
(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)}
|
||||
(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-actions-row)}
|
||||
[:button {:class (stl/css :replace-button)
|
||||
:on-click handle-replace
|
||||
: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")]]])
|
||||
|
||||
[:div {:class (stl/css :active-filters)}
|
||||
(for [fkey current-filters]
|
||||
(let [fname (d/name fkey)
|
||||
|
||||
@ -246,6 +246,160 @@
|
||||
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.bodySmallTypography;
|
||||
|
||||
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.flexCenter;
|
||||
|
||||
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.bodySmallTypography;
|
||||
@include deprecated.buttonStyle;
|
||||
|
||||
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.bodySmallTypography;
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.no-matches {
|
||||
@include deprecated.bodySmallTypography;
|
||||
|
||||
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;
|
||||
|
||||
@ -5863,6 +5863,14 @@ msgstr "Redo"
|
||||
msgid "workspace.header.menu.select-all"
|
||||
msgstr "Select all"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.find"
|
||||
msgstr "Find"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.find-and-replace"
|
||||
msgstr "Find and Replace"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:423
|
||||
msgid "workspace.header.menu.show-artboard-names"
|
||||
msgstr "Show boards names"
|
||||
@ -7966,6 +7974,34 @@ msgstr "Shapes"
|
||||
msgid "workspace.sidebar.layers.texts"
|
||||
msgstr "Texts"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/layers.cljs
|
||||
msgid "workspace.sidebar.layers.replace"
|
||||
msgstr "Replace"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/layers.cljs
|
||||
msgid "workspace.sidebar.layers.replace-all"
|
||||
msgstr "Replace all"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/layers.cljs
|
||||
msgid "workspace.sidebar.layers.replace-placeholder"
|
||||
msgstr "Replace with..."
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/layers.cljs
|
||||
msgid "workspace.sidebar.layers.match-count"
|
||||
msgstr "%s of %s"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/layers.cljs
|
||||
msgid "workspace.sidebar.layers.no-matches"
|
||||
msgstr "No matches"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/layers.cljs
|
||||
msgid "workspace.sidebar.layers.search-scope-layers"
|
||||
msgstr "Search layers"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/layers.cljs
|
||||
msgid "workspace.sidebar.layers.search-scope-canvas"
|
||||
msgstr "Search on canvas"
|
||||
|
||||
#: 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"
|
||||
msgstr "Imported SVG Attributes"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user