diff --git a/CHANGES.md b/CHANGES.md index ebfd68450b..a7f7d2861a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,44 @@ - Show a read-only W × H size badge below the bounding box of the current selection (by @bittoby) [Github #9205](https://github.com/penpot/penpot/issues/9205) - Expose `variants` retrieval on `LibraryComponent` via `isVariant()` type guard in plugin API [Github #9185](https://github.com/penpot/penpot/issues/9185) +- Show alpha percentage next to library color values to distinguish colors that differ only in opacity (by @rockchris099) [Github #6328](https://github.com/penpot/penpot/issues/6328) +- Add "Clear artboard guides" option to right-click context menu for frames (by @eureka0928) [Github #6987](https://github.com/penpot/penpot/issues/6987) +- Add loader feedback while importing and exporting files [Github #9020](https://github.com/penpot/penpot/issues/9020) +- Allow duplicating color and typography styles (by @MkDev11) [Github #2912](https://github.com/penpot/penpot/issues/2912) +- Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248) +- Import Tokens from linked library (by @dfelinto) [Github #8391](https://github.com/penpot/penpot/pull/8391) +- Option to download custom fonts (by @dfelinto) [Github #8320](https://github.com/penpot/penpot/issues/8320) +- Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313) +- Add Tab/Shift+Tab navigation to rename layers sequentially (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8474) +- Copy and paste entire rows in existing table (by @bittoby) [Github #8498](https://github.com/penpot/penpot/pull/8498) +- Rename token group [Taiga #13137](https://tree.taiga.io/project/penpot/us/13137) +- Duplicate token group [Taiga #10653](https://tree.taiga.io/project/penpot/us/10653) +- Copy token name from contextual menu [Taiga #13568](https://tree.taiga.io/project/penpot/issue/13568) +- Add natural sorting on token names [Taiga #13713](https://tree.taiga.io/project/penpot/issue/13713) +- Add drag-to-change for numeric inputs in workspace sidebar [Github #2466](https://github.com/penpot/penpot/issues/2466) +- Add CSS linter [Taiga #13790](https://tree.taiga.io/project/penpot/us/13790) +- Save and restore selection state in undo/redo (by @eureka0928) [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 @eureka0928) [Github #5275](https://github.com/penpot/penpot/issues/5275) +- Add Find & Replace for text content and layer names (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 @eureka0928) [Github #1602](https://github.com/penpot/penpot/issues/1602) +- Add visibility toggle for strokes (by @eureka0928) [Github #7438](https://github.com/penpot/penpot/issues/7438) +- Sort asset library subfolders alphabetically at every nesting level (by @eureka0928) [Github #2572](https://github.com/penpot/penpot/issues/2572) +- Add Paste to replace (Cmd+Shift+V) to replace the selected shape with clipboard contents (by @eureka0928) [Github #4240](https://github.com/penpot/penpot/issues/4240) +- Differentiate incoming and outgoing interaction link colors (by @claytonlin1110) [Github #7794](https://github.com/penpot/penpot/issues/7794) +- Add guide locking and fix locked elements not selectable in viewer (by @Dexterity104) [Github #8358](https://github.com/penpot/penpot/issues/8358) +- Apply styles to selection (by @AzazelN28) [Taiga #13647](https://tree.taiga.io/project/penpot/task/13647) +- Reorder prototyping overlay options to show Position before Relative to (by @rockchris099) [Github #2910](https://github.com/penpot/penpot/issues/2910) +- Add customizable colors for ruler guides (by @Dexterity104) [Github #5199](https://github.com/penpot/penpot/issues/5199) +- Persist asset search query and section filter when switching sidebar tabs (by @eureka0928) [Github #2913](https://github.com/penpot/penpot/issues/2913) +- 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) +- 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) +- Add Shift+Numpad0/1/2 as aliases to Shift+0/1/2 for zoom shortcuts [Github #2457](https://github.com/penpot/penpot/issues/2457) +- Add search bar to prototype interaction destination dropdown (by @moorsecopers99) [Github #8618](https://github.com/penpot/penpot/issues/8618) ### :bug: Bugs fixed diff --git a/frontend/src/app/main/ui/components/select.cljs b/frontend/src/app/main/ui/components/select.cljs index 07c2a9db86..c6f01ec323 100644 --- a/frontend/src/app/main/ui/components/select.cljs +++ b/frontend/src/app/main/ui/components/select.cljs @@ -13,7 +13,9 @@ [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.icons :as deprecated-icon] [app.util.dom :as dom] + [app.util.i18n :refer [tr]] [app.util.keyboard :as kbd] + [cuerdas.core :as str] [rumext.v2 :as mf])) (defn- as-key-value @@ -47,7 +49,7 @@ (:value (nth options (rotate-index-backward index length)))) (mf/defc select - [{:keys [default-value options class dropdown-class is-open? on-change on-pointer-enter-option on-pointer-leave-option disabled data-direction]}] + [{:keys [default-value options class dropdown-class is-open? on-change on-pointer-enter-option on-pointer-leave-option disabled data-direction searchable? search-placeholder]}] (let [label-index (mf/with-memo [options] (into {} (map as-key-value) options)) @@ -63,6 +65,25 @@ is-open? (get state :is-open?) + ;; nil = the user is not actively searching (input shows the current + ;; selection's label); a string = the user has typed (input shows that + ;; string and options are filtered). + search* (mf/use-state nil) + search (deref search*) + searching? (and searchable? (some? search)) + search-input-ref (mf/use-ref nil) + + visible-options + (mf/with-memo [options search searchable?] + (if (and searchable? (some? search) (not (str/blank? search))) + (let [needle (str/lower search)] + (into [] (filter (fn [item] + (and (map? item) + (when-let [label (:label item)] + (str/includes? (str/lower (dm/str label)) needle))))) + options)) + options)) + node-ref (mf/use-ref nil) dropdown-direction* @@ -76,10 +97,10 @@ handle-key-up (mf/use-fn - (mf/deps disabled options current-value) + (mf/deps disabled visible-options current-value searchable?) (fn [e] (when-not disabled - (let [options (into [] (remove :disabled) options) + (let [options (into [] (remove :disabled) visible-options) length (count options) index (d/index-of-pred options #(= (:value %) current-value)) index (d/nilv index 0)] @@ -87,20 +108,26 @@ (cond (or (kbd/left-arrow? e) (kbd/up-arrow? e)) - (let [value (rotate-option-backward options index length)] - (swap! state* assoc :current-value value) - (when (fn? on-change) - (on-change value))) + (when (pos? length) + (let [value (rotate-option-backward options index length)] + (swap! state* assoc :current-value value) + (when (fn? on-change) + (on-change value)))) (or (kbd/right-arrow? e) (kbd/down-arrow? e)) - (let [value (rotate-option-forward options index)] - (swap! state* assoc :current-value value) - (when (fn? on-change) - (on-change value))) + (when (pos? length) + (let [value (rotate-option-forward options index)] + (swap! state* assoc :current-value value) + (when (fn? on-change) + (on-change value)))) - (or (kbd/enter? e) - (kbd/space? e)) + (kbd/enter? e) + (swap! state* assoc :is-open? false) + + ;; In searchable mode the input owns Space — let the user + ;; type spaces in the filter without closing the dropdown. + (and (not searchable?) (kbd/space? e)) (swap! state* assoc :is-open? false) (kbd/tab? e) @@ -108,13 +135,32 @@ :is-open? true :current-value (-> options first :value))))))) - open-dropdown + handle-search-change + (mf/use-fn + (fn [e] + (let [v (dom/get-target-val e)] + (reset! search* v) + (swap! state* assoc :is-open? true)))) + + handle-search-focus (mf/use-fn (mf/deps disabled) - (fn [] + (fn [_] (when-not disabled (swap! state* assoc :is-open? true)))) + open-dropdown + (mf/use-fn + (mf/deps disabled searchable?) + (fn [] + (when-not disabled + (swap! state* assoc :is-open? true) + ;; In searchable mode the input is the focus target — pull focus + ;; in case the click landed on the wrapper or the dropdown arrow. + (when searchable? + (when-let [el (mf/ref-val search-input-ref)] + (dom/focus! el)))))) + close-dropdown (mf/use-fn #(swap! state* assoc :is-open? false)) @@ -158,6 +204,10 @@ (reset! dropdown-direction* "down") (mf/set-ref-val! dropdown-direction-change* 0))) + (mf/with-effect [is-open?] + (when (and searchable? (not is-open?)) + (reset! search* nil))) + (mf/with-effect [is-open?] (let [dropdown-element (mf/ref-val node-ref)] (when (and (= 0 (mf/ref-val dropdown-direction-change*)) dropdown-element) @@ -167,25 +217,44 @@ (let [selected-option (first (filter #(= (:value %) default-value) options)) current-icon (:icon selected-option) - current-icon-ref (deprecated-icon/key->icon current-icon)] + current-icon-ref (deprecated-icon/key->icon current-icon) + input-value (if searching? search (or current-label ""))] [:div {:id (dm/str current-id) :on-click open-dropdown :on-key-up handle-key-up - :tab-index "0" + :tab-index (if searchable? "-1" "0") :role "combobox" :class (dm/str (stl/css-case :custom-select true + :searchable-select searchable? :disabled disabled :icon (some? current-icon-ref)) " " class)} (when (and current-icon current-icon-ref) [:span {:class (stl/css :current-icon)} current-icon-ref]) - [:span {:class (stl/css :current-label)} current-label] + (if searchable? + [:input {:ref search-input-ref + :type "text" + :class (stl/css :current-label-input) + :value input-value + :placeholder (or search-placeholder current-label) + :disabled disabled + :role "searchbox" + :aria-autocomplete "list" + :aria-label (or search-placeholder current-label) + :on-focus handle-search-focus + :on-click handle-search-focus + :on-change handle-search-change}] + [:span {:class (stl/css :current-label)} current-label]) [:span {:class (stl/css :dropdown-button)} deprecated-icon/arrow] [:& dropdown {:show is-open? :on-close close-dropdown} [:ul {:ref node-ref :data-direction (d/nilv data-direction dropdown-direction) :class (dm/str dropdown-class " " (stl/css :custom-select-dropdown))} - (for [[index item] (d/enumerate options)] + (when (and searchable? searching? (not (str/blank? search)) (empty? visible-options)) + [:li {:class (stl/css :custom-select-no-matches) + :role "presentation"} + (tr "labels.no-matches")]) + (for [[index item] (d/enumerate visible-options)] (if (= :separator item) [:li {:id (dm/str current-id "-" index) :key (dm/str current-id "-" index) diff --git a/frontend/src/app/main/ui/components/select.scss b/frontend/src/app/main/ui/components/select.scss index e75034f6f3..f0b1cab71c 100644 --- a/frontend/src/app/main/ui/components/select.scss +++ b/frontend/src/app/main/ui/components/select.scss @@ -4,28 +4,33 @@ // // Copyright (c) KALEIDOS INC -@use "refactor/common-refactor.scss" as deprecated; +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/typography.scss" as *; +@use "ds/colors.scss" as *; +@use "ds/mixins.scss" as *; +@use "ds/z-index.scss" as *; .custom-select { - --border-color: var(--menu-background-color); - --bg-color: var(--menu-background-color); - --icon-color: var(--icon-foreground); - --text-color: var(--menu-foreground-color); + @include custom-scrollbar; + @include use-typography("body-small"); - @extend %new-scrollbar; - @include deprecated.body-small-typography; + --border-color: var(--color-background-tertiary); + --bg-color: var(--color-background-tertiary); + --icon-color: var(--color-foreground-secondary); + --text-color: var(--color-foreground-primary); position: relative; display: grid; grid-template-columns: 1fr auto; align-items: center; - height: deprecated.$s-32; - width: 100%; + block-size: $sz-32; + inline-size: 100%; margin: 0; - padding: deprecated.$s-8; - border-radius: deprecated.$br-8; + padding: var(--sp-s); + border-radius: $br-8; background-color: var(--bg-color); - border: deprecated.$s-1 solid var(--border-color); + border: $b-1 solid var(--border-color); color: var(--text-color); cursor: pointer; @@ -34,106 +39,256 @@ } &:hover { - --bg-color: var(--menu-background-color-hover); - --border-color: var(--menu-background-color); - --icon-color: var(--menu-foreground-color-hover); + --bg-color: var(--color-background-quaternary); + --border-color: var(--color-background-tertiary); + --icon-color: var(--color-foreground-primary); } - &:focus { - --bg-color: var(--menu-background-color-focus); - --border-color: var(--menu-background-focus); + &:has(*:focus-visible) { + --bg-color: var(--color-background-tertiary); + --border-color: var(--color-accent-primary); + } +} + +.searchable-select { + --searchable-select-bg-color: var(--color-background-tertiary); + --searchable-select-icon-color: var(--color-foreground-secondary); + --searchable-select-outline-color: none; + + background: var(--searchable-select-bg-color); + outline: $b-1 solid var(--searchable-select-outline-color); + + &:hover { + --searchable-select-bg-color: var(--color-background-quaternary); + } + + &:has(*:focus-visible) { + --searchable-select-bg-color: var(--color-background-primary); + --searchable-select-outline-color: var(--color-accent-primary); + } + + &:has(*:disabled) { + --searchable-select-bg-color: var(--color-background-primary); + --searchable-select-outline-color: var(--color-background-quaternary); + } + + &[data-option-focused="true"]:has(*:focus-visible) { + --searchable-select-bg-color: var(--color-background-tertiary); + --searchable-select-outline-color: none; } } .disabled { - --bg-color: var(--menu-background-color-disabled); - --border-color: var(--menu-border-color-disabled); - --icon-color: var(--menu-foreground-color-disabled); - --text-color: var(--menu-foreground-color-disabled); + --bg-color: var(--color-background-primary); + --border-color: var(--color-background-quaternary); + --icon-color: var(--color-foreground-secondary); + --text-color: var(--color-foreground-secondary); pointer-events: none; cursor: default; } .dropdown-button { - @include deprecated.flex-center; - + display: flex; + justify-content: center; + align-items: center; margin-inline-end: var(--sp-xxs); svg { - @extend %button-icon-small; - + display: flex; + align-items: center; + gap: var(--sp-xxs); + color: transparent; + fill: none; + block-size: $sz-12; + inline-size: $sz-12; + stroke-width: 1.33px; + visibility: hidden; + stroke: var(--color-foreground-secondary); transform: rotate(90deg); - stroke: var(--icon-color); } } .current-icon { - @include deprecated.flex-center; - - width: deprecated.$s-24; - padding-right: deprecated.$s-4; + display: flex; + justify-content: center; + align-items: center; + inline-size: $sz-24; + padding-inline-end: var(--sp-xs); svg { - @extend %button-icon-small; - - stroke: var(--icon-foreground); + display: flex; + align-items: center; + gap: var(--sp-xxs); + color: transparent; + fill: none; + block-size: $sz-12; + inline-size: $sz-12; + stroke-width: 1.33px; + visibility: hidden; + stroke: var(--color-foreground-secondary); } } .custom-select-dropdown { - @extend %dropdown-wrapper; + box-shadow: 0 0 $sz-12 0 var(--color-shadow-dark); + position: absolute; + top: $sz-32; + left: 0; + inline-size: 100%; + max-block-size: var(--menu-max-height, #{px2rem(300)}); + padding: var(--sp-xxs); + margin: 0; + margin-block-start: px2rem(1); + border-radius: $br-8; + z-index: var(--z-index-dropdown); + overflow: hidden auto; + background-color: var(--color-background-tertiary); + color: var(--color-foreground-primary); + border: $b-2 solid var(--color-background-quaternary); .separator { margin: 0; - height: deprecated.$s-12; - border-block-start: deprecated.$s-1 solid var(--dropdown-separator-color); + block-size: $sz-12; + border-block-start: $b-1 solid var(--color-background-primary); } } +.current-label-input { + @include text-ellipsis; + + inline-size: 100%; + block-size: 100%; + margin: 0; + padding: 0; + border: none; + background-color: transparent; + color: inherit; + font: inherit; + outline: none; + cursor: pointer; + + &::placeholder { + color: inherit; + opacity: 1; + } + + &:focus { + cursor: text; + } + + &:disabled { + cursor: default; + } +} + +.custom-select-no-matches { + @include use-typography("body-small"); + + padding: var(--sp-s) var(--sp-m); + color: var(--color-foreground-secondary); + text-align: center; + pointer-events: none; +} + .custom-select-dropdown[data-direction="up"] { - bottom: deprecated.$s-32; + bottom: $sz-32; top: auto; } .checked-element { - @extend %dropdown-element-base; + @include use-typography("body-small"); - .icon { - @include deprecated.flex-center; + display: flex; + align-items: center; + gap: var(--sp-s); + block-size: $sz-32; + padding: 0 var(--sp-s); + border-radius: $br-6; + cursor: pointer; + color: var(--color-foreground-primary); - height: deprecated.$s-24; - width: deprecated.$s-24; - padding-right: deprecated.$s-4; + &:hover { + background-color: var(--color-background-quaternary); + color: var(--color-foreground-primary); svg { - @extend %button-icon; + stroke: var(--color-foreground-primary); + } + } - stroke: var(--icon-foreground); + span { + @include text-ellipsis; + + display: flex; + align-items: center; + + svg { + display: flex; + align-items: center; + gap: var(--sp-xxs); + color: transparent; + fill: none; + block-size: $sz-12; + inline-size: $sz-12; + stroke-width: 1.33px; + visibility: hidden; + stroke: var(--color-foreground-secondary); + } + } + + .icon { + display: flex; + justify-content: center; + align-items: center; + block-size: $sz-24; + inline-size: $sz-24; + padding-inline-end: var(--sp-xs); + + svg { + display: flex; + align-items: center; + gap: var(--sp-xxs); + color: transparent; + fill: none; + block-size: $sz-16; + width: $sz-16; + stroke-width: px2rem(1); + visibility: hidden; + stroke: var(--color-foreground-secondary); } } .label { flex-grow: 1; - width: 100%; + inline-size: 100%; } .check-icon { - @include deprecated.flex-center; + display: flex; + justify-content: center; + align-items: center; svg { - @extend %button-icon-small; - + display: flex; + align-items: center; + gap: var(--sp-xxs); + color: transparent; + fill: none; + block-size: $sz-12; + inline-size: $sz-12; + stroke-width: 1.33px; visibility: hidden; - stroke: var(--icon-foreground); + stroke: var(--color-foreground-secondary); } } &.is-selected { - color: var(--menu-foreground-color); + color: var(--color-accent-primary); + background-color: var(--color-background-quaternary); .check-icon svg { - stroke: var(--menu-foreground-color); + stroke: var(--color-foreground-primary); visibility: visible; } } @@ -144,5 +299,5 @@ } .current-label { - @include deprecated.text-ellipsis; + @include text-ellipsis; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs index c1a54d9764..7b5995fba5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs @@ -450,7 +450,9 @@ [:div {:class (stl/css :interaction-row-select)} [:& select {:default-value (str (:destination interaction)) :options destination-options - :on-change change-destination}]]]) + :on-change change-destination + :searchable? true + :search-placeholder (tr "workspace.options.interaction-destination")}]]]) ;; Preserve scroll (when (ctsi/has-preserve-scroll interaction) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 905b01048c..b06f29166c 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -2922,6 +2922,10 @@ msgstr "No pending invitations." msgid "labels.no-invitations-gather-people" msgstr "Gather your people and build great things together." +#: src/app/main/ui/components/select.cljs +msgid "labels.no-matches" +msgstr "No matches" + #: src/app/main/ui/static.cljs #, unused msgid "labels.not-found.desc-message"