diff --git a/CHANGES.md b/CHANGES.md index 18e71ee6c4..5311f312bf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,7 @@ - Add delete and duplicate buttons to typography dialog (by @eureka0928) [Github #5270](https://github.com/penpot/penpot/issues/5270) - Edit ruler guide position by double-clicking the guide pill (by @eureka0928) [Github #2311](https://github.com/penpot/penpot/issues/2311) - Add a search bar to filter colors in the color palette toolbar (by @eureka0928) [Github #7653](https://github.com/penpot/penpot/issues/7653) +- Add a search bar to filter board size presets (by @eureka0928) [Github #4658](https://github.com/penpot/penpot/issues/4658) - Allow customising the OIDC login button label (by @wdeveloper16) [Github #7027](https://github.com/penpot/penpot/issues/7027) - Add page separators in Workspace [Taiga #13611](https://tree.taiga.io/project/penpot/us/13611?milestone=262806) - Preserve vector content when pasting from external tools such as Inkscape: recognise SVG sent as text/plain (with optional XML declaration and HTML comments), skip the raster preview when an SVG sibling is on the clipboard, and ignore empty SVG blobs that some tools advertise alongside the real payload, so pasted graphics arrive editable without spurious "SVG is invalid" warnings [Github #546](https://github.com/penpot/penpot/issues/546) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs index 275ad11e8d..fa40189182 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs @@ -13,8 +13,10 @@ [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.components.search-bar :refer [search-bar*]] [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.icons :as deprecated-icon] + [app.main.ui.workspace.sidebar.options.menus.measures :as measures] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) @@ -31,11 +33,35 @@ selected-preset-name (deref selected-preset-name*) - on-open - (mf/use-fn (fn [] (reset! show* true))) + search-term* + (mf/use-state "") + + search-term + (deref search-term*) + + container-ref + (mf/use-ref nil) + + on-toggle + (mf/use-fn + (fn [] + (swap! show* not) + (reset! search-term* ""))) on-close - (mf/use-fn (fn [] (reset! show* false))) + (mf/use-fn + (fn [] + (reset! show* false) + (reset! search-term* ""))) + + on-search-change + (mf/use-fn + (fn [value _event] + (reset! search-term* value))) + + filtered-presets + (mf/with-memo [search-term] + (measures/filter-size-presets search-term size-presets)) on-preset-selected (mf/use-fn @@ -48,7 +74,9 @@ (d/read-string))] (reset! selected-preset-name* name) - (st/emit! (dwd/set-default-size width height))))) + (st/emit! (dwd/set-default-size width height)) + (reset! show* false) + (reset! search-term* "")))) orientation (when (:width drawing-state) @@ -65,35 +93,49 @@ [:div {:class (stl/css :presets)} [:div {:class (stl/css-case :presets-wrapper true :opened show?) - :on-click on-open} + :ref container-ref + :on-click on-toggle} [:span {:class (stl/css :select-name)} (or selected-preset-name (tr "workspace.options.size-presets"))] [:span {:class (stl/css :collapsed-icon)} deprecated-icon/arrow] [:& dropdown {:show show? - :on-close on-close} - [:ul {:class (stl/css :custom-select-dropdown)} - (for [preset size-presets] - (if-not (:width preset) - [:li {:key (:name preset) - :class (stl/css-case :dropdown-element true - :disabled true)} - [:span {:class (stl/css :preset-name)} (:name preset)]] + :on-close on-close + :container container-ref} + [:div {:class (stl/css :custom-select-dropdown) + :on-click dom/stop-propagation} + [:div {:class (stl/css :preset-search)} + [:> search-bar* {:on-change on-search-change + :value search-term + :auto-focus true + :placeholder (tr "workspace.options.search-size-preset")}]] + [:ul {:class (stl/css :preset-list)} + (if (empty? filtered-presets) + [:li {:class (stl/css-case :dropdown-element true + :disabled true)} + [:span {:class (stl/css :preset-name)} + (tr "workspace.options.no-size-preset-results")]] + (for [preset filtered-presets] + (if-not (:width preset) + [:li {:key (:name preset) + :class (stl/css-case :dropdown-element true + :disabled true)} + [:span {:class (stl/css :preset-name)} (:name preset)]] - (let [preset-match (and (= (:width preset) (:width drawing-state)) - (= (:height preset) (:height drawing-state)))] - [:li {:key (:name preset) - :class (stl/css-case :dropdown-element true - :match preset-match) - :data-width (str (:width preset)) - :data-height (str (:height preset)) - :data-name (:name preset) - :on-click on-preset-selected} - [:div {:class (stl/css :name-wrapper)} - [:span {:class (stl/css :preset-name)} (:name preset)] - [:span {:class (stl/css :preset-size)} (:width preset) " x " (:height preset)]] - (when preset-match - [:span {:class (stl/css :check-icon)} deprecated-icon/tick])])))]]] + (let [preset-match (and (= (:width preset) (:width drawing-state)) + (= (:height preset) (:height drawing-state)))] + [:li {:key (:name preset) + :class (stl/css-case :dropdown-element true + :match preset-match) + :data-width (str (:width preset)) + :data-height (str (:height preset)) + :data-name (:name preset) + :on-click on-preset-selected} + [:div {:class (stl/css :name-wrapper)} + [:span {:class (stl/css :preset-name)} (:name preset)] + [:span {:class (stl/css :preset-size)} (:width preset) " x " (:height preset)]] + (when preset-match + [:span {:class (stl/css :check-icon)} deprecated-icon/tick])]))))]]]] [:& radio-buttons {:selected (or (d/name orientation) "") :on-change on-orientation-change diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss index 1599bcad25..53221cd2a5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss @@ -64,6 +64,23 @@ margin-top: deprecated.$s-2; max-height: 70vh; width: deprecated.$s-252; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.preset-search { + padding: deprecated.$s-4; + border-bottom: deprecated.$s-1 solid var(--menu-border-color-rest, transparent); +} + +.preset-list { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + margin: 0; + padding: 0; + list-style: none; .dropdown-element { @extend %dropdown-element-base; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index ef9936d90e..3d1c4741b9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -26,6 +26,7 @@ [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.numeric-input :as deprecated-input] [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.components.search-bar :refer [search-bar*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.icons :as deprecated-icon] @@ -34,6 +35,7 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [clojure.set :as set] + [cuerdas.core :as str] [rumext.v2 :as mf])) (def measure-attrs @@ -105,6 +107,29 @@ (number? value) (parse-double (.toFixed value decimals))))) +(defn filter-size-presets + "Filter the `size-presets` list by `term`, preserving category headers only + when at least one of their following presets matches." + [term presets] + (if (str/blank? term) + presets + (let [lterm (str/lower term) + matches? (fn [p] (and (:width p) + (str/includes? (str/lower (:name p)) lterm)))] + (loop [remaining presets + acc []] + (if-let [head (first remaining)] + (if (:width head) + (recur (rest remaining) + (cond-> acc (matches? head) (conj head))) + (let [[items tail] (split-with :width (rest remaining)) + matching-items (filter matches? items)] + (recur tail + (if (seq matching-items) + (into (conj acc head) matching-items) + acc)))) + acc))))) + (mf/defc measures-menu* [{:keys [ids values applied-tokens type shapes]}] (let [token-numeric-inputs @@ -235,17 +260,36 @@ show-presets-dropdown? (deref preset-state*) - open-presets + preset-search-term* + (mf/use-state "") + + preset-search-term + (deref preset-search-term*) + + preset-container-ref + (mf/use-ref nil) + + toggle-presets (mf/use-fn - (mf/deps show-presets-dropdown?) (fn [] - (reset! preset-state* true))) + (swap! preset-state* not) + (reset! preset-search-term* ""))) close-presets (mf/use-fn (mf/deps show-presets-dropdown?) (fn [] - (reset! preset-state* false))) + (reset! preset-state* false) + (reset! preset-search-term* ""))) + + on-preset-search-change + (mf/use-fn + (fn [value _event] + (reset! preset-search-term* value))) + + filtered-size-presets + (mf/with-memo [preset-search-term] + (filter-size-presets preset-search-term size-presets)) on-preset-selected (mf/use-fn @@ -258,7 +302,9 @@ (dom/get-data "height") (d/read-string))] (st/emit! (udw/update-dimensions ids :width width) - (udw/update-dimensions ids :height height))))) + (udw/update-dimensions ids :height height)) + (reset! preset-state* false) + (reset! preset-search-term* "")))) ;; ORIENTATION @@ -379,33 +425,47 @@ [:div {:class (stl/css :presets)} [:div {:class (stl/css-case :presets-wrapper true :opened show-presets-dropdown?) - :on-click open-presets} + :ref preset-container-ref + :on-click toggle-presets} [:span {:class (stl/css :select-name)} (tr "workspace.options.size-presets")] [:span {:class (stl/css :collapsed-icon)} deprecated-icon/arrow] [:& dropdown {:show show-presets-dropdown? - :on-close close-presets} - [:ul {:class (stl/css :custom-select-dropdown)} - (for [size-preset size-presets] - (if-not (:width size-preset) - [:li {:key (:name size-preset) - :class (stl/css-case :dropdown-element true - :disabled true)} - [:span {:class (stl/css :preset-name)} (:name size-preset)]] + :on-close close-presets + :container preset-container-ref} + [:div {:class (stl/css :custom-select-dropdown) + :on-click dom/stop-propagation} + [:div {:class (stl/css :preset-search)} + [:> search-bar* {:on-change on-preset-search-change + :value preset-search-term + :auto-focus true + :placeholder (tr "workspace.options.search-size-preset")}]] + [:ul {:class (stl/css :preset-list)} + (if (empty? filtered-size-presets) + [:li {:class (stl/css-case :dropdown-element true + :disabled true)} + [:span {:class (stl/css :preset-name)} + (tr "workspace.options.no-size-preset-results")]] + (for [size-preset filtered-size-presets] + (if-not (:width size-preset) + [:li {:key (:name size-preset) + :class (stl/css-case :dropdown-element true + :disabled true)} + [:span {:class (stl/css :preset-name)} (:name size-preset)]] - (let [preset-match (and (= (:width size-preset) (d/parse-integer (:width values) 0)) - (= (:height size-preset) (d/parse-integer (:height values) 0)))] - [:li {:key (:name size-preset) - :class (stl/css-case :dropdown-element true - :match preset-match) - :data-width (str (:width size-preset)) - :data-height (str (:height size-preset)) - :on-click on-preset-selected} - [:div {:class (stl/css :name-wrapper)} - [:span {:class (stl/css :preset-name)} (:name size-preset)] - [:span {:class (stl/css :preset-size)} (:width size-preset) " x " (:height size-preset)]] - (when preset-match - [:span {:class (stl/css :check-icon)} deprecated-icon/tick])])))]]] + (let [preset-match (and (= (:width size-preset) (d/parse-integer (:width values) 0)) + (= (:height size-preset) (d/parse-integer (:height values) 0)))] + [:li {:key (:name size-preset) + :class (stl/css-case :dropdown-element true + :match preset-match) + :data-width (str (:width size-preset)) + :data-height (str (:height size-preset)) + :on-click on-preset-selected} + [:div {:class (stl/css :name-wrapper)} + [:span {:class (stl/css :preset-name)} (:name size-preset)] + [:span {:class (stl/css :preset-size)} (:width size-preset) " x " (:height size-preset)]] + (when preset-match + [:span {:class (stl/css :check-icon)} deprecated-icon/tick])]))))]]]] [:& radio-buttons {:selected (or (d/name orientation) "") :on-change on-orientation-change diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss index e3605152d8..357df42145 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss @@ -75,6 +75,23 @@ margin-top: deprecated.$s-2; max-height: 70vh; width: deprecated.$s-252; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.preset-search { + padding: deprecated.$s-4; + border-bottom: deprecated.$s-1 solid var(--menu-border-color-rest, transparent); +} + +.preset-list { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + margin: 0; + padding: 0; + list-style: none; .dropdown-element { @extend %dropdown-element-base; diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 9426a51036..e04a501a56 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -7351,6 +7351,14 @@ msgstr "Size" msgid "workspace.options.size-presets" msgstr "Size presets" +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.search-size-preset" +msgstr "Search size preset" + +#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +msgid "workspace.options.no-size-preset-results" +msgstr "No matching size preset" + #: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:469 msgid "workspace.options.size.lock" msgstr "Lock ratio"