Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
Andrey Antukh
2025-08-27 13:19:53 +02:00
13 changed files with 296 additions and 83 deletions

View File

@@ -30,6 +30,7 @@
- Fix issue where Alt + arrow keys shortcut interferes with letter-spacing when moving text layers [Taiga #11552](https://tree.taiga.io/project/penpot/issue/11771)
- Fix consistency issues on how font variants are visualized [Taiga #11499](https://tree.taiga.io/project/penpot/us/11499)
- Fix parsing rx and ry SVG values for rect radius [Taiga #11861](https://tree.taiga.io/project/penpot/issue/11861)
- Misleading affordance in saved versions [Taiga #11887](https://tree.taiga.io/project/penpot/issue/11887)
## 2.9.0

View File

@@ -821,6 +821,13 @@
(let [path-split (split-path path)]
(merge-path-item (first path-split) name)))
(defn inside-path? [child parent]
(let [child-path (split-path child)
parent-path (split-path parent)]
(and (<= (count parent-path) (count child-path))
(= parent-path (take (count parent-path) child-path)))))
(defn split-by-last-period
"Splits a string into two parts:

View File

@@ -131,7 +131,8 @@
:hide-release-modal
:subscriptions
:subscriptions-old
:frontend-binary-fills})
:frontend-binary-fills
:inspect-styles})
(def all-flags
(set/union email login varia))

View File

@@ -569,57 +569,56 @@
combine
(fn [current-page]
(let [objects (dsh/lookup-page-objects state current-page)
selected (or selected
(->> (dsh/lookup-selected state)
(cfh/clean-loops objects)
(remove (fn [id]
(let [shape (get objects id)]
(or (not (ctc/main-instance? shape))
(ctc/is-variant? shape)))))))
shapes (mapv #(get objects %) selected)
rect (bounding-rect shapes)
prefix (->> shapes
(mapv #(cfh/split-path (:name %)))
(common-prefix))
;; When the common parent is root, add a wrapper
add-wrapper? (= prefix [])
first-shape (first shapes)
delta (gpt/point (- (:x rect) (:x first-shape) 30)
(- (:y rect) (:y first-shape) 30))
common-parent (->> selected
(mapv #(-> (cfh/get-parent-ids objects %) reverse))
common-prefix
last)
index (-> (get objects common-parent)
:shapes
count
inc)
variant-id (uuid/next)
undo-id (js/Symbol)]
selected (->> (or selected (dsh/lookup-selected state))
(cfh/clean-loops objects)
(remove (fn [id]
(let [shape (get objects id)]
(or (not (ctc/main-instance? shape))
(ctc/is-variant? shape))))))]
(when (> (count selected) 1)
(let [shapes (mapv #(get objects %) selected)
rect (bounding-rect shapes)
prefix (->> shapes
(mapv #(cfh/split-path (:name %)))
(common-prefix))
;; When the common parent is root, add a wrapper
add-wrapper? (empty? prefix)
first-shape (first shapes)
delta (gpt/point (- (:x rect) (:x first-shape) 30)
(- (:y rect) (:y first-shape) 30))
common-parent (->> selected
(mapv #(-> (cfh/get-parent-ids objects %) reverse))
common-prefix
last)
index (-> (get objects common-parent)
:shapes
count
inc)
variant-id (uuid/next)
undo-id (js/Symbol)]
(rx/concat
(if (and page-id (not= current-page page-id))
(rx/of (dcm/go-to-workspace :page-id page-id))
(rx/empty))
(rx/concat
(if (and page-id (not= current-page page-id))
(rx/of (dcm/go-to-workspace :page-id page-id))
(rx/empty))
(rx/of (dwu/start-undo-transaction undo-id)
(transform-in-variant (first selected) variant-id delta prefix add-wrapper? false false)
(dwsh/relocate-shapes (into #{} (-> selected rest reverse)) variant-id 0)
(dwsh/update-shapes selected #(-> %
(assoc :constraints-h :left)
(assoc :constraints-v :top)
(assoc :fixed-scroll false)))
(dwsh/relocate-shapes #{variant-id} common-parent index)
(dwt/update-dimensions [variant-id] :width (+ (:width rect) 60))
(dwt/update-dimensions [variant-id] :height (+ (:height rect) 60)))
(rx/of (dwu/start-undo-transaction undo-id)
(transform-in-variant (first selected) variant-id delta prefix add-wrapper? false false)
(dwsh/relocate-shapes (into #{} (-> selected rest reverse)) variant-id 0)
(dwsh/update-shapes selected #(-> %
(assoc :constraints-h :left)
(assoc :constraints-v :top)
(assoc :fixed-scroll false)))
(dwsh/relocate-shapes #{variant-id} common-parent index)
(dwt/update-dimensions [variant-id] :width (+ (:width rect) 60))
(dwt/update-dimensions [variant-id] :height (+ (:height rect) 60)))
;; NOTE: we need to schedule a commit into a
;; microtask for ensure that all the scheduled
;; microtask of previous events execute before the
;; commit
(->> (rx/of (dwu/commit-undo-transaction undo-id))
(rx/observe-on :async)))))
;; NOTE: we need to schedule a commit into a
;; microtask for ensure that all the scheduled
;; microtask of previous events execute before the
;; commit
(->> (rx/of (dwu/commit-undo-transaction undo-id))
(rx/observe-on :async)))))))
redirect-to-page
(fn [page-id]

View File

@@ -12,7 +12,6 @@
border: $b-1 solid var(--border-color, transparent);
border-radius: $br-8;
cursor: pointer;
background: var(--color-background-primary);
display: grid;
@@ -74,3 +73,7 @@
display: flex;
padding-right: var(--sp-xs);
}
.menu-button {
cursor: pointer;
}

View File

@@ -12,7 +12,6 @@
border: $b-1 solid var(--border-color, transparent);
border-radius: $br-8;
cursor: pointer;
background: var(--color-background-primary);
display: grid;

View File

@@ -8,15 +8,18 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.types.component :as ctk]
[app.config :as cf]
[app.main.data.event :as ev]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.ds.controls.select :refer [select*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
[app.main.ui.icons :as i]
[app.main.ui.inspect.attributes :refer [attributes]]
[app.main.ui.inspect.code :refer [code]]
[app.main.ui.inspect.selection-feedback :refer [resolve-shapes]]
[app.main.ui.inspect.styles :refer [styles-tab*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.shape-icon :as usi]
@@ -39,11 +42,13 @@
(mf/defc right-sidebar
[{:keys [frame page objects file selected shapes page-id file-id share-id from on-change-section on-expand]
:or {from :viewer}}]
(let [section (mf/use-state #(do :info))
(let [color-space* (mf/use-state "hex")
color-space (deref color-space*)
section (mf/use-state #(if (contains? cf/flags :inspect-styles) :styles :info))
objects (or objects (:objects page))
shapes (or shapes
(resolve-shapes objects selected))
first-shape (first shapes)
page-id (or page-id (:id page))
file-id (or file-id (:id file))
@@ -82,20 +87,42 @@
(fn []
(dom/open-new-window "https://help.penpot.app/user-guide/inspect/")))
handle-change-color-space
(mf/use-fn
(fn [color-space]
(reset! color-space* color-space)))
color-spaces
(mf/with-memo []
[{:label (tr "inspect.attributes.color.hex")
:id "hex"}
{:label (tr "inspect.attributes.color.rgba")
:id "rgba"}
{:label (tr "inspect.attributes.color.hsla")
:id "hsla"}])
tabs
(mf/with-memo []
[{:label (tr "inspect.tabs.info")
:id "info"}
{:label (tr "inspect.tabs.code")
:data-testid "code"
:id "code"}])]
(if (contains? cf/flags :inspect-styles)
[{:label (tr "inspect.tabs.styles")
:id "styles"}
{:label (tr "inspect.tabs.computed")
:id "computed"}
{:label (tr "inspect.tabs.code")
:data-testid "code"
:id "code"}]
[{:label (tr "inspect.tabs.info")
:id "info"}
{:label (tr "inspect.tabs.code")
:data-testid "code"
:id "code"}]))]
(mf/use-effect
(mf/deps shapes handle-change-tab)
(fn []
(if (seq shapes)
(st/emit! (ptk/event ::ev/event {::ev/name "inspect-mode-click-element"}))
(handle-change-tab :info))))
(handle-change-tab (if (contains? cf/flags :inspect-styles) :styles :info)))))
[:aside {:class (stl/css-case :settings-bar-right true
:viewer-code (= from :viewer))}
@@ -133,27 +160,63 @@
[:div {:class (stl/css :layer-title)} (:name first-shape)]])]])]
[:div {:class (stl/css :inspect-content)}
(if (contains? cf/flags :inspect-styles)
[:div {:class (stl/css :inspect-tab-switcher)}
[:span {:class (stl/css :inspect-tab-switcher-label)} (tr "inspect.tabs.switcher.label")]
[:div {:class (stl/css :inspect-tab-switcher-controls)}
[:div {:class (stl/css :inspect-tab-switcher-controls-color-space)}
[:> select* {:options color-spaces
:default-selected "hex"
:on-change handle-change-color-space}]]
[:div {:class (stl/css :inspect-tab-switcher-controls-tab)}
[:> select* {:options tabs
:default-selected (name @section)
:on-change handle-change-tab}]]]]
nil)
[:> tab-switcher* {:tabs tabs
:selected (name @section)
:on-change handle-change-tab
:class (stl/css :viewer-tab-switcher)}
(case @section
:info
[:& attributes {:page-id page-id
:objects objects
:file-id file-id
:frame frame
:shapes shapes
:from from
:libraries libraries
:share-id share-id}]
(if (contains? cf/flags :inspect-styles)
[:div {:class (stl/css :inspect-tab :viewer-tab-switcher :viewer-tab-switcher-layout)}
(case @section
:styles
[:> styles-tab* {:color-space color-space
:shapes shapes
:libraries libraries
:file-id file-id}]
:computed
[:& attributes {:page-id page-id
:objects objects
:file-id file-id
:frame frame
:shapes shapes
:from from
:libraries libraries
:share-id share-id}]
:code
[:& code {:frame frame
:shapes shapes
:on-expand handle-expand
:from from}])]]]
:code
[:& code {:frame frame
:shapes shapes
:on-expand handle-expand
:from from}])]
[:> tab-switcher* {:tabs tabs
:selected (name @section)
:on-change handle-change-tab
:class (stl/css :viewer-tab-switcher)}
(case @section
:info
[:& attributes {:page-id page-id
:objects objects
:file-id file-id
:frame frame
:shapes shapes
:from from
:libraries libraries
:share-id share-id}]
:code
[:& code {:frame frame
:shapes shapes
:on-expand handle-expand
:from from}])])]]
[:div {:class (stl/css :empty)}
[:div {:class (stl/css :code-info)}
[:span {:class (stl/css :placeholder-icon)}

View File

@@ -4,6 +4,7 @@
//
// Copyright (c) KALEIDOS INC
@use "../ds/typography.scss" as *;
@import "refactor/common-refactor.scss";
.settings-bar-right {
@@ -106,6 +107,34 @@
padding: $s-8 $s-24;
}
.inspect-tab-switcher {
display: flex;
justify-content: space-between;
align-items: center;
padding-block: var(--sp-s);
padding-inline-end: var(--sp-m);
}
.inspect-tab-switcher-label {
@include use-typography("body-medium");
flex: 1;
}
.inspect-tab-switcher-controls {
display: flex;
align-items: center;
flex: 2;
gap: var(--sp-s);
}
.inspect-tab-switcher-controls-color-space {
flex: 1;
}
.inspect-tab-switcher-controls-tab {
flex: 2;
}
.inspect-content {
flex: 1;
overflow: hidden;
@@ -115,3 +144,7 @@
--tabs-nav-padding-inline-start: 0;
--tabs-nav-padding-inline-end: var(--sp-m);
}
.viewer-tab-switcher-layout {
display: grid;
}

View File

@@ -0,0 +1,61 @@
(ns app.main.ui.inspect.styles
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.common.types.component :as ctc]
[app.common.types.components-list :as ctkl]
[app.main.ui.inspect.styles.style-box :refer [style-box*]]
[rumext.v2 :as mf]))
(def type->options
{:multiple [:fill :stroke :text :shadow :blur :layout-element]
:frame [:visibility :geometry :fill :stroke :shadow :blur :layout :layout-element]
:group [:visibility :geometry :svg :layout-element]
:rect [:visibility :geometry :fill :stroke :shadow :blur :svg :layout-element]
:circle [:visibility :geometry :fill :stroke :shadow :blur :svg :layout-element]
:path [:visibility :geometry :fill :stroke :shadow :blur :svg :layout-element]
:text [:visibility :geometry :text :shadow :blur :stroke :layout-element]
:variant [:variant :geometry :fill :stroke :shadow :blur :layout :layout-element]})
(defn- get-shape-type
[shapes first-shape first-component]
(cond
(and (= (count shapes) 1)
(or (ctc/is-variant-container? first-shape)
(ctc/is-variant? first-component)))
:variant
(= (count shapes) 1)
(:type first-shape)
:else
:multiple))
(mf/defc styles-tab*
[{:keys [color-space shapes libraries file-id]}]
(let [data (dm/get-in libraries [file-id :data])
first-shape (first shapes)
first-component (ctkl/get-component data (:component-id first-shape))
type (get-shape-type shapes first-shape first-component)
options (type->options type)]
[:div {:class (stl/css :element-options)}
(for [[idx option] (map-indexed vector options)]
[:> style-box* {:key idx :attribute option} color-space])]))
;; WIP
;; Panel list as stylebox children
#_(case option
:geometry [:> geometry-panel {}]
:layout [:> layout-panel {}]
:layout-element [:> layout-element-panel {}]
:fill [:> fill-panel {:color-space color-space}]
:stroke [:> stroke-panel {:color-space color-space}]
:text [:> text-panel {:color-space color-space}]
:shadow [:> shadow-panel {}]
:blur [:> blur-panel {}]
:svg [:> svg-panel {}]
:variant [:> variant-panel* {}]
:visibility [:> visibility-panel* {}])

View File

@@ -0,0 +1,26 @@
(ns app.main.ui.inspect.styles.style-box
(:require-macros [app.main.style :as stl])
(:require
[rumext.v2 :as mf]))
(defn- attribute->title
[type]
(case type
:variant "Variant properties"
:token "Token Sets & Themes"
:geometry "Size & Position"
:fill "Fill"
:stroke "Stroke"
:text "Text"
:shadow "Shadow"
:layout "Layout"
:layout-element "Layout Element"
:visibility "Visibility"
:svg "SVG"
nil))
(mf/defc style-box*
[{:keys [attribute children]}]
[:div {:class (stl/css :style-box)}
[:h3 {:class (stl/css :style-box-header)} (attribute->title attribute)]
[:div {:class (stl/css :style-box-content)} children]])

View File

@@ -460,7 +460,7 @@
(st/emit! (dwu/start-undo-transaction undo-id))
(run! st/emit!
(->> components
(filter #(str/starts-with? (:path %) path))
(filter #(cfh/inside-path? (:path %) path))
(map #(dwv/rename-comp-or-variant-and-main
(:id %)
(cmm/rename-group % path last-path)))))
@@ -491,7 +491,7 @@
(st/emit! (dwu/start-undo-transaction undo-id))
(run! st/emit!
(->> components
(filter #(str/starts-with? (:path %) path))
(filter #(cfh/inside-path? (:path %) path))
(map #(dwv/rename-comp-or-variant-and-main (:id %) (cmm/ungroup % path)))))
(st/emit! (dwu/commit-undo-transaction undo-id)))))
@@ -501,7 +501,7 @@
(fn [path]
(on-clear-selection)
(let [comps (->> components
(filter #(str/starts-with? (:path %) path)))
(filter #(cfh/inside-path? (:path %) path)))
ids (into #{} (map :main-instance-id comps))
page-id (->> comps first :main-instance-page)]

View File

@@ -1761,8 +1761,12 @@ msgid "inspect.subtitle.main"
msgstr "Main component"
#: src/app/main/ui/inspect/right_sidebar.cljs:59
msgid "inspect.subtitle.variant"
msgstr "Variant"
msgid "inspect.tabs.switcher.label"
msgstr "Layer info"
#: src/app/main/ui/inspect/right_sidebar.cljs:101
msgid "inspect.tabs.computed"
msgstr "Computed"
#: src/app/main/ui/inspect/right_sidebar.cljs:105
msgid "inspect.tabs.code"
@@ -1820,6 +1824,10 @@ msgstr "Text"
msgid "inspect.tabs.info"
msgstr "Info"
#: src/app/main/ui/inspect/right_sidebar.cljs:101
msgid "inspect.tabs.styles"
msgstr "Styles"
#: src/app/main/ui/dashboard/comments.cljs:95
msgid "label.mark-all-as-read"
msgstr "Mark all as read"

View File

@@ -1768,6 +1768,14 @@ msgstr "Componente principal"
msgid "inspect.subtitle.variant"
msgstr "Variante"
#: src/app/main/ui/inspect/right_sidebar.cljs:59
msgid "inspect.tabs.switcher.label"
msgstr "Información sobre la capa"
#: src/app/main/ui/inspect/right_sidebar.cljs:101
msgid "inspect.tabs.computed"
msgstr "Calculado"
#: src/app/main/ui/inspect/right_sidebar.cljs:105
msgid "inspect.tabs.code"
msgstr "Código"
@@ -1824,6 +1832,10 @@ msgstr "Texto"
msgid "inspect.tabs.info"
msgstr "Información"
#: src/app/main/ui/inspect/right_sidebar.cljs:101
msgid "inspect.tabs.styles"
msgstr "Estilos"
#: src/app/main/ui/dashboard/comments.cljs:95
msgid "label.mark-all-as-read"
msgstr "Marcar todo como leído"