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

This commit is contained in:
Andrey Antukh 2025-06-16 16:51:14 +02:00
commit df84396fea
46 changed files with 904 additions and 636 deletions

View File

@ -26,6 +26,13 @@ The initial prototype is completly reworked for provide a more consistent API
and to have proper validation and params decoding. All the details can be found and to have proper validation and params decoding. All the details can be found
on [its own changelog](library/CHANGES.md) on [its own changelog](library/CHANGES.md)
**Penpot migrate from Redis to Valkey**
As [Valkey](https://valkey.io/) is an opne-souce fork of [Redis](https://redis.io/)
version 7.2.4, this version of Penpot will be compatible with Redis but may diverge
in future versions. Therefore, **migration from Redis to ValKey is recommended for all
on-premises instances** that want to keep up to date.
### :heart: Community contributions (Thank you!) ### :heart: Community contributions (Thank you!)
### :sparkles: New features & Enhancements ### :sparkles: New features & Enhancements
@ -45,6 +52,7 @@ on [its own changelog](library/CHANGES.md)
- Update google fonts (at 2025/05/19) [Taiga 10792](https://tree.taiga.io/project/penpot/us/10792) - Update google fonts (at 2025/05/19) [Taiga 10792](https://tree.taiga.io/project/penpot/us/10792)
- Add tooltip component to DS [Taiga 9220](https://tree.taiga.io/project/penpot/us/9220) - Add tooltip component to DS [Taiga 9220](https://tree.taiga.io/project/penpot/us/9220)
- Allow multi file token export [Taiga #10144](https://tree.taiga.io/project/penpot/us/10144) - Allow multi file token export [Taiga #10144](https://tree.taiga.io/project/penpot/us/10144)
- Add Serbian language
### :bug: Bugs fixed ### :bug: Bugs fixed
@ -54,7 +62,9 @@ on [its own changelog](library/CHANGES.md)
- Fix element positioning on the right side to adjust to grid [#11073](https://tree.taiga.io/project/penpot/issue/11073) - Fix element positioning on the right side to adjust to grid [#11073](https://tree.taiga.io/project/penpot/issue/11073)
- Fix palette is over sidebar [#11160](https://tree.taiga.io/project/penpot/issue/11160) - Fix palette is over sidebar [#11160](https://tree.taiga.io/project/penpot/issue/11160)
- Fix font size input not displaying "mixed" when multiple texts are selected [Taiga #11177](https://tree.taiga.io/project/penpot/issue/11177) - Fix font size input not displaying "mixed" when multiple texts are selected [Taiga #11177](https://tree.taiga.io/project/penpot/issue/11177)
- Misalignments at Create account [Taiga #11315](https://tree.taiga.io/project/penpot/issue/11315)
- Fix issue with importing files where flex/grid is used [Taiga #11334](https://tree.taiga.io/project/penpot/issue/11334)
- Fix wrong color in the export progress bar [Taiga #11299](https://tree.taiga.io/project/penpot/issue/11299)
## 2.7.2 ## 2.7.2

View File

@ -28,7 +28,7 @@
integrant/integrant {:mvn/version "0.13.1"} integrant/integrant {:mvn/version "0.13.1"}
funcool/tubax {:mvn/version "2021.05.20-0"} funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2025.05.26-411"} funcool/cuerdas {:mvn/version "2025.06.16-414"}
funcool/promesa funcool/promesa
{:git/sha "f52f58cfacf62f59eab717e2637f37729d0cc383" {:git/sha "f52f58cfacf62f59eab717e2637f37729d0cc383"
:git/url "https://github.com/funcool/promesa"} :git/url "https://github.com/funcool/promesa"}

View File

@ -91,10 +91,10 @@
parent-id (or parent-id (get selected-obj :parent-id)) parent-id (or parent-id (get selected-obj :parent-id))
base-parent (get objects parent-id) base-parent (get objects parent-id)
layout-props layout-attrs
(when (and (= 1 (count selected)) (when (and (= 1 (count selected))
(ctl/any-layout? base-parent)) (ctl/any-layout? base-parent))
(select-keys selected-obj ctl/layout-item-props)) (select-keys selected-obj ctl/layout-child-attrs))
target-cell-id target-cell-id
(if (and (nil? target-cell-id) (if (and (nil? target-cell-id)
@ -129,8 +129,8 @@
:parent-id parent-id :parent-id parent-id
:shapes (into [] selected)) :shapes (into [] selected))
(some? layout-props) (some? layout-attrs)
(d/patch-object layout-props) (d/patch-object layout-attrs)
;; Frames from shapes will not be displayed in viewer and no clipped ;; Frames from shapes will not be displayed in viewer and no clipped
(or (not= frame-id uuid/zero) without-fill?) (or (not= frame-id uuid/zero) without-fill?)

View File

@ -104,36 +104,11 @@
(impl/path-data (impl/path-data
(reduce apply-to-index (vec content) modifiers)))) (reduce apply-to-index (vec content) modifiers))))
(defn- transform-content-legacy
[content transform]
(if (some? transform)
(let [set-tr
(fn [params px py]
(let [tr-point (-> (gpt/point (get params px) (get params py))
(gpt/transform transform))]
(assoc params
px (:x tr-point)
py (:y tr-point))))
transform-params
(fn [{:keys [x c1x c2x] :as params}]
(cond-> params
(some? x) (set-tr :x :y)
(some? c1x) (set-tr :c1x :c1y)
(some? c2x) (set-tr :c2x :c2y)))]
(into []
(map #(update % :params transform-params))
content))
content))
(defn transform-content (defn transform-content
"Applies a transformation matrix over content and returns a new "Applies a transformation matrix over content and returns a new
content as PathData instance." content as PathData instance."
[content transform] [content transform]
#_(segment/transform-content content transform) (segment/transform-content content transform))
(some-> (transform-content-legacy (vec content) transform)
(impl/from-plain)))
(defn move-content (defn move-content
[content move-vec] [content move-vec]

View File

@ -65,34 +65,34 @@
(let [t (buf/read-short buffer offset)] (let [t (buf/read-short buffer offset)]
(case t (case t
(1 2) (1 2)
(let [x (buf/read-float buffer (+ offset 20)) (let [x (buf/read-float buffer (+ offset 20))
y (buf/read-float buffer (+ offset 24)) y (buf/read-float buffer (+ offset 24))
x (+ (* x a) (* y c) e) x' (+ (* x a) (* y c) e)
y (+ (* x b) (* y d) f)] y' (+ (* x b) (* y d) f)]
(buf/write-float buffer (+ offset 20) x) (buf/write-float buffer (+ offset 20) x')
(buf/write-float buffer (+ offset 24) y)) (buf/write-float buffer (+ offset 24) y'))
3 3
(let [c1x (buf/read-float buffer (+ offset 4)) (let [c1x (buf/read-float buffer (+ offset 4))
c1y (buf/read-float buffer (+ offset 8)) c1y (buf/read-float buffer (+ offset 8))
c2x (buf/read-float buffer (+ offset 12)) c2x (buf/read-float buffer (+ offset 12))
c2y (buf/read-float buffer (+ offset 16)) c2y (buf/read-float buffer (+ offset 16))
x (buf/read-float buffer (+ offset 20)) x (buf/read-float buffer (+ offset 20))
y (buf/read-float buffer (+ offset 24)) y (buf/read-float buffer (+ offset 24))
c1x (+ (* c1x a) (* c1y c) e) c1x' (+ (* c1x a) (* c1y c) e)
c1y (+ (* c1x b) (* c1y d) f) c1y' (+ (* c1x b) (* c1y d) f)
c2x (+ (* c2x a) (* c2y c) e) c2x' (+ (* c2x a) (* c2y c) e)
c2y (+ (* c2x b) (* c2y d) f) c2y' (+ (* c2x b) (* c2y d) f)
x (+ (* x a) (* y c) e) x' (+ (* x a) (* y c) e)
y (+ (* x b) (* y d) f)] y' (+ (* x b) (* y d) f)]
(buf/write-float buffer (+ offset 4) c1x) (buf/write-float buffer (+ offset 4) c1x')
(buf/write-float buffer (+ offset 8) c1y) (buf/write-float buffer (+ offset 8) c1y')
(buf/write-float buffer (+ offset 12) c2x) (buf/write-float buffer (+ offset 12) c2x')
(buf/write-float buffer (+ offset 16) c2y) (buf/write-float buffer (+ offset 16) c2y')
(buf/write-float buffer (+ offset 20) x) (buf/write-float buffer (+ offset 20) x')
(buf/write-float buffer (+ offset 24) y)) (buf/write-float buffer (+ offset 24) y'))
nil))) nil)))

View File

@ -313,7 +313,7 @@
:title "Shape"} :title "Shape"}
[:group [:group
[:merge {:title "GroupShape"} [:merge {:title "GroupShape"}
ctsl/schema:layout-attrs ctsl/schema:layout-child-attrs
schema:group-attrs schema:group-attrs
schema:shape-generic-attrs schema:shape-generic-attrs
schema:shape-geom-attrs schema:shape-geom-attrs
@ -321,8 +321,8 @@
[:frame [:frame
[:merge {:title "FrameShape"} [:merge {:title "FrameShape"}
ctsl/schema:layout-child-attrs
ctsl/schema:layout-attrs ctsl/schema:layout-attrs
::ctsl/layout-attrs
schema:frame-attrs schema:frame-attrs
schema:shape-generic-attrs schema:shape-generic-attrs
schema:shape-geom-attrs schema:shape-geom-attrs
@ -332,14 +332,14 @@
[:bool [:bool
[:merge {:title "BoolShape"} [:merge {:title "BoolShape"}
ctsl/schema:layout-attrs ctsl/schema:layout-child-attrs
schema:bool-attrs schema:bool-attrs
schema:shape-generic-attrs schema:shape-generic-attrs
schema:shape-base-attrs]] schema:shape-base-attrs]]
[:rect [:rect
[:merge {:title "RectShape"} [:merge {:title "RectShape"}
ctsl/schema:layout-attrs ctsl/schema:layout-child-attrs
schema:rect-attrs schema:rect-attrs
schema:shape-generic-attrs schema:shape-generic-attrs
schema:shape-geom-attrs schema:shape-geom-attrs
@ -347,7 +347,7 @@
[:circle [:circle
[:merge {:title "CircleShape"} [:merge {:title "CircleShape"}
ctsl/schema:layout-attrs ctsl/schema:layout-child-attrs
schema:circle-attrs schema:circle-attrs
schema:shape-generic-attrs schema:shape-generic-attrs
schema:shape-geom-attrs schema:shape-geom-attrs
@ -355,7 +355,7 @@
[:image [:image
[:merge {:title "ImageShape"} [:merge {:title "ImageShape"}
ctsl/schema:layout-attrs ctsl/schema:layout-child-attrs
schema:image-attrs schema:image-attrs
schema:shape-generic-attrs schema:shape-generic-attrs
schema:shape-geom-attrs schema:shape-geom-attrs
@ -363,7 +363,7 @@
[:svg-raw [:svg-raw
[:merge {:title "SvgRawShape"} [:merge {:title "SvgRawShape"}
ctsl/schema:layout-attrs ctsl/schema:layout-child-attrs
schema:svg-raw-attrs schema:svg-raw-attrs
schema:shape-generic-attrs schema:shape-generic-attrs
schema:shape-geom-attrs schema:shape-geom-attrs
@ -371,14 +371,14 @@
[:path [:path
[:merge {:title "PathShape"} [:merge {:title "PathShape"}
ctsl/schema:layout-attrs ctsl/schema:layout-child-attrs
schema:path-attrs schema:path-attrs
schema:shape-generic-attrs schema:shape-generic-attrs
schema:shape-base-attrs]] schema:shape-base-attrs]]
[:text [:text
[:merge {:title "TextShape"} [:merge {:title "TextShape"}
ctsl/schema:layout-attrs ctsl/schema:layout-child-attrs
schema:text-attrs schema:text-attrs
schema:shape-generic-attrs schema:shape-generic-attrs
schema:shape-geom-attrs schema:shape-geom-attrs
@ -682,23 +682,6 @@
:r3 :r3
:r4}) :r4})
(def ^:private layout-extract-props
#{:layout
:layout-flex-dir
:layout-gap-type
:layout-gap
:layout-wrap-type
:layout-align-items
:layout-align-content
:layout-justify-items
:layout-justify-content
:layout-padding-type
:layout-padding
:layout-grid-dir
:layout-grid-rows
:layout-grid-columns
:layout-grid-cells})
(defn extract-props (defn extract-props
"Retrieves an object with the 'pasteable' properties for a shape." "Retrieves an object with the 'pasteable' properties for a shape."
[shape] [shape]
@ -729,9 +712,8 @@
(assoc-props node txt/text-node-attrs))) (assoc-props node txt/text-node-attrs)))
props))) props)))
(extract-layout-props (extract-layout-attrs [props shape]
[props shape] (d/patch-object props (select-keys shape ctsl/layout-attrs)))]
(d/patch-object props (select-keys shape layout-extract-props)))]
(let [;; For texts we don't extract the fill (let [;; For texts we don't extract the fill
extract-props extract-props
@ -739,7 +721,7 @@
(-> shape (-> shape
(select-keys extract-props) (select-keys extract-props)
(cond-> (cfh/text-shape? shape) (extract-text-props shape)) (cond-> (cfh/text-shape? shape) (extract-text-props shape))
(cond-> (ctsl/any-layout? shape) (extract-layout-props shape)))))) (cond-> (ctsl/any-layout? shape) (extract-layout-attrs shape))))))
(defn patch-props (defn patch-props
"Given the object of `extract-props` applies it to a shape. Adapt the shape if necesary" "Given the object of `extract-props` applies it to a shape. Adapt the shape if necesary"
@ -764,7 +746,7 @@
(d/patch-object (select-keys props txt/text-node-attrs)))))))))) (d/patch-object (select-keys props txt/text-node-attrs))))))))))
(patch-layout-props [shape props] (patch-layout-props [shape props]
(let [shape (d/patch-object shape (select-keys props layout-extract-props))] (let [shape (d/patch-object shape (select-keys props ctsl/layout-attrs))]
(cond-> shape (cond-> shape
(ctsl/grid-layout? shape) (ctsl/grid-layout? shape)
(ctsl/assign-cells objects))))] (ctsl/assign-cells objects))))]

View File

@ -43,7 +43,6 @@
;; :layout-item-absolute ;; boolean ;; :layout-item-absolute ;; boolean
;; :layout-item-z-index ;; int ;; :layout-item-z-index ;; int
(def layout-types (def layout-types
#{:flex :grid}) #{:flex :grid})
@ -74,49 +73,6 @@
(def justify-items-types (def justify-items-types
#{:start :end :center :stretch}) #{:start :end :center :stretch})
(def layout-item-props
[:layout-item-margin
:layout-item-margin-type
:layout-item-h-sizing
:layout-item-v-sizing
:layout-item-max-h
:layout-item-min-h
:layout-item-max-w
:layout-item-min-w
:layout-item-absolute
:layout-item-z-index])
(sm/register!
^{::sm/type ::layout-attrs}
[:map {:title "LayoutAttrs"}
[:layout {:optional true} [::sm/one-of layout-types]]
[:layout-flex-dir {:optional true} [::sm/one-of flex-direction-types]]
[:layout-gap {:optional true}
[:map
[:row-gap {:optional true} ::sm/safe-number]
[:column-gap {:optional true} ::sm/safe-number]]]
[:layout-gap-type {:optional true} [::sm/one-of gap-types]]
[:layout-wrap-type {:optional true} [::sm/one-of wrap-types]]
[:layout-padding-type {:optional true} [::sm/one-of padding-type]]
[:layout-padding {:optional true}
[:map
[:p1 ::sm/safe-number]
[:p2 ::sm/safe-number]
[:p3 ::sm/safe-number]
[:p4 ::sm/safe-number]]]
[:layout-justify-content {:optional true} [::sm/one-of justify-content-types]]
[:layout-justify-items {:optional true} [::sm/one-of justify-items-types]]
[:layout-align-content {:optional true} [::sm/one-of align-content-types]]
[:layout-align-items {:optional true} [::sm/one-of align-items-types]]
[:layout-grid-dir {:optional true} [::sm/one-of grid-direction-types]]
[:layout-grid-rows {:optional true}
[:vector {:gen/max 2} ::grid-track]]
[:layout-grid-columns {:optional true}
[:vector {:gen/max 2} ::grid-track]]
[:layout-grid-cells {:optional true}
[:map-of {:gen/max 5} ::sm/uuid ::grid-cell]]])
;; Grid types ;; Grid types
(def grid-track-types (def grid-track-types
#{:percent :flex :auto :fixed}) #{:percent :flex :auto :fixed})
@ -130,29 +86,59 @@
(def grid-cell-justify-self-types (def grid-cell-justify-self-types
#{:auto :start :center :end :stretch}) #{:auto :start :center :end :stretch})
(sm/register! (def ^:private schema:grid-cell
^{::sm/type ::grid-cell} [:map {:title "GridCell"}
[:map {:title "GridCell"} [:id ::sm/uuid]
[:id ::sm/uuid] [:area-name {:optional true} :string]
[:area-name {:optional true} :string] [:row ::sm/safe-int]
[:row ::sm/safe-int] [:row-span ::sm/safe-int]
[:row-span ::sm/safe-int] [:column ::sm/safe-int]
[:column ::sm/safe-int] [:column-span ::sm/safe-int]
[:column-span ::sm/safe-int] [:position {:optional true} [::sm/one-of grid-position-types]]
[:position {:optional true} [::sm/one-of grid-position-types]] [:align-self {:optional true} [::sm/one-of grid-cell-align-self-types]]
[:align-self {:optional true} [::sm/one-of grid-cell-align-self-types]] [:justify-self {:optional true} [::sm/one-of grid-cell-justify-self-types]]
[:justify-self {:optional true} [::sm/one-of grid-cell-justify-self-types]] [:shapes
[:shapes [:vector {:gen/max 1} ::sm/uuid]]])
[:vector {:gen/max 1} ::sm/uuid]]])
(sm/register! (def ^:private schema:grid-track
^{::sm/type ::grid-track} [:map {:title "GridTrack"}
[:map {:title "GridTrack"} [:type [::sm/one-of grid-track-types]]
[:type [::sm/one-of grid-track-types]] [:value {:optional true} [:maybe ::sm/safe-number]]])
[:value {:optional true} [:maybe ::sm/safe-number]]])
(def check-grid-track! (def schema:layout-attrs
(sm/check-fn ::grid-track)) [:map {:title "LayoutAttrs"}
[:layout {:optional true} [::sm/one-of layout-types]]
[:layout-flex-dir {:optional true} [::sm/one-of flex-direction-types]]
[:layout-gap {:optional true}
[:map
[:row-gap {:optional true} ::sm/safe-number]
[:column-gap {:optional true} ::sm/safe-number]]]
[:layout-gap-type {:optional true} [::sm/one-of gap-types]]
[:layout-wrap-type {:optional true} [::sm/one-of wrap-types]]
[:layout-padding-type {:optional true} [::sm/one-of padding-type]]
[:layout-padding {:optional true}
[:map
[:p1 ::sm/safe-number]
[:p2 ::sm/safe-number]
[:p3 ::sm/safe-number]
[:p4 ::sm/safe-number]]]
[:layout-justify-content {:optional true} [::sm/one-of justify-content-types]]
[:layout-justify-items {:optional true} [::sm/one-of justify-items-types]]
[:layout-align-content {:optional true} [::sm/one-of align-content-types]]
[:layout-align-items {:optional true} [::sm/one-of align-items-types]]
[:layout-grid-dir {:optional true} [::sm/one-of grid-direction-types]]
[:layout-grid-rows {:optional true}
[:vector {:gen/max 2} schema:grid-track]]
[:layout-grid-columns {:optional true}
[:vector {:gen/max 2} schema:grid-track]]
[:layout-grid-cells {:optional true}
[:map-of {:gen/max 5} ::sm/uuid schema:grid-cell]]])
(def ^:private check-grid-track
(sm/check-fn schema:grid-track))
(def layout-attrs
(sm/keys schema:layout-attrs))
;; LAYOUT CHILDREN ;; LAYOUT CHILDREN
@ -168,7 +154,7 @@
(def item-align-self-types (def item-align-self-types
#{:start :end :center :stretch}) #{:start :end :center :stretch})
(def schema:layout-attrs (def schema:layout-child-attrs
[:map {:title "LayoutChildAttrs"} [:map {:title "LayoutChildAttrs"}
[:layout-item-margin-type {:optional true} [::sm/one-of item-margin-types]] [:layout-item-margin-type {:optional true} [::sm/one-of item-margin-types]]
[:layout-item-margin {:optional true} [:layout-item-margin {:optional true}
@ -187,6 +173,9 @@
[:layout-item-absolute {:optional true} :boolean] [:layout-item-absolute {:optional true} :boolean]
[:layout-item-z-index {:optional true} ::sm/safe-number]]) [:layout-item-z-index {:optional true} ::sm/safe-number]])
(def layout-child-attrs
(sm/keys schema:layout-child-attrs))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMAS ;; SCHEMAS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -194,8 +183,6 @@
(def valid-layouts (def valid-layouts
#{:flex :grid}) #{:flex :grid})
(sm/register! ::layout [::sm/one-of valid-layouts])
(defn flex-layout? (defn flex-layout?
([objects id] ([objects id]
(flex-layout? (get objects id))) (flex-layout? (get objects id)))
@ -754,9 +741,7 @@
([type parent value] ([type parent value]
(add-grid-track type parent value nil)) (add-grid-track type parent value nil))
([type parent value index] ([type parent value index]
(dm/assert! (assert (check-grid-track value))
"expected a valid grid definition for `value`"
(check-grid-track! value))
(let [[tracks-prop tracks-prop-other prop prop-other prop-span prop-span-other] (let [[tracks-prop tracks-prop-other prop prop-other prop-span prop-span-other]
(if (= type :column) (if (= type :column)

View File

@ -25,6 +25,15 @@
{:command :curve-to :params {:c1x 368.0 :c1y 737.0 :c2x 310.0 :c2y 681.0 :x 264.0 :y 634.0}} {:command :curve-to :params {:c1x 368.0 :c1y 737.0 :c2x 310.0 :c2y 681.0 :x 264.0 :y 634.0}}
{:command :close-path :params {}}]) {:command :close-path :params {}}])
(def sample-content-square
[{:command :move-to, :params {:x 0, :y 0}}
{:command :line-to, :params {:x 10, :y 0}}
{:command :line-to, :params {:x 10, :y 10}}
{:command :line-to, :params {:x 10, :y 0}}
{:command :line-to, :params {:x 0, :y 10}}
{:command :line-to, :params {:x 0, :y 0}}
{:command :close-path :params {}}])
(def sample-content-large (def sample-content-large
[{:command :move-to :params {:x 480.0 :y 839.0}} [{:command :move-to :params {:x 480.0 :y 839.0}}
{:command :line-to :params {:x 439.0 :y 802.0}} {:command :line-to :params {:x 439.0 :y 802.0}}
@ -179,6 +188,42 @@
(t/is (= (vec result1) result2)) (t/is (= (vec result1) result2))
(t/is (= result2 result3)))) (t/is (= result2 result3))))
(t/deftest path-transform-3
(let [matrix (gmt/rotate-matrix 42 (gpt/point 0 0))
content (path/content sample-content-square)
result1 (path/transform-content content matrix)
result2 (transform-plain-content sample-content-square matrix)
result3 (transform-plain-content content matrix)]
(t/is (= (count result1) (count result2)))
(doseq [[seg-a seg-b] (map vector result1 result2)]
(t/is (= (:command seg-a)
(:command seg-b)))
(let [params-a (get seg-a :params)
params-b (get seg-b :params)]
(t/is (mth/close? (get params-a :x 0)
(get params-b :x 0)))
(t/is (mth/close? (get params-a :y 0)
(get params-b :y 0)))))
(doseq [[seg-a seg-b] (map vector result1 result3)]
(t/is (= (:command seg-a)
(:command seg-b)))
(let [params-a (get seg-a :params)
params-b (get seg-b :params)]
(t/is (mth/close? (get params-a :x 0)
(get params-b :x 0)))
(t/is (mth/close? (get params-a :y 0)
(get params-b :y 0)))))))
(defn- content->points (defn- content->points
"Given a content return all points. "Given a content return all points.
@ -278,15 +323,6 @@
(t/is (= result2 expect)) (t/is (= result2 expect))
(t/is (= result3 expect)))) (t/is (= result3 expect))))
(def sample-content-square
[{:command :move-to, :params {:x 0, :y 0}}
{:command :line-to, :params {:x 10, :y 0}}
{:command :line-to, :params {:x 10, :y 10}}
{:command :line-to, :params {:x 10, :y 0}}
{:command :line-to, :params {:x 0, :y 10}}
{:command :line-to, :params {:x 0, :y 0}}
{:command :close-path :params {}}])
(t/deftest points-to-content (t/deftest points-to-content
(let [initial [(gpt/point 0.0 0.0) (let [initial [(gpt/point 0.0 0.0)
(gpt/point 10.0 10.0) (gpt/point 10.0 10.0)

View File

@ -1,5 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
EMSDK_QUIET=1 . /usr/local/emsdk/emsdk_env.sh;
alias l='ls --color -GFlh' alias l='ls --color -GFlh'
alias rm='rm -r' alias rm='rm -r'
alias ls='ls --color -F' alias ls='ls --color -F'

View File

@ -111,7 +111,7 @@ services:
depends_on: depends_on:
penpot-postgres: penpot-postgres:
condition: service_healthy condition: service_healthy
penpot-redis: penpot-valkey:
condition: service_healthy condition: service_healthy
networks: networks:
@ -148,10 +148,10 @@ services:
PENPOT_DATABASE_USERNAME: penpot PENPOT_DATABASE_USERNAME: penpot
PENPOT_DATABASE_PASSWORD: penpot PENPOT_DATABASE_PASSWORD: penpot
## Redis is used for the websockets notifications. Don't touch unless the redis ## Valkey (or previously redis) is used for the websockets notifications. Don't touch
## container has different parameters or different name. ## unless the valkey container has different parameters or different name.
PENPOT_REDIS_URI: redis://penpot-redis/0 PENPOT_REDIS_URI: redis://penpot-valkey/0
## Default configuration for assets storage: using filesystem based with all files ## Default configuration for assets storage: using filesystem based with all files
## stored in a docker volume. ## stored in a docker volume.
@ -195,7 +195,7 @@ services:
restart: always restart: always
depends_on: depends_on:
penpot-redis: penpot-valkey:
condition: service_healthy condition: service_healthy
networks: networks:
@ -206,8 +206,8 @@ services:
# communicate with the frontend. # communicate with the frontend.
PENPOT_PUBLIC_URI: http://penpot-frontend:8080 PENPOT_PUBLIC_URI: http://penpot-frontend:8080
## Redis is used for the websockets notifications. ## Valkey (or previowsly Redis) is used for the websockets notifications.
PENPOT_REDIS_URI: redis://penpot-redis/0 PENPOT_REDIS_URI: redis://penpot-valkey/0
penpot-postgres: penpot-postgres:
image: "postgres:15" image: "postgres:15"
@ -233,12 +233,12 @@ services:
- POSTGRES_USER=penpot - POSTGRES_USER=penpot
- POSTGRES_PASSWORD=penpot - POSTGRES_PASSWORD=penpot
penpot-redis: penpot-valkey:
image: redis:7.2 image: valkey/valkey:8.1
restart: always restart: always
healthcheck: healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"] test: ["CMD-SHELL", "valkey-cli ping | grep PONG"]
interval: 1s interval: 1s
timeout: 3s timeout: 3s
retries: 5 retries: 5

View File

@ -140,7 +140,8 @@ The <code class="language-js">manifest.json</code> file contains the basic infor
"user:read", "user:read",
"comment:read", "comment:read",
"comment:write", "comment:write",
"allow:downloads" "allow:downloads",
"allow:localstorage"
] ]
} }
``` ```
@ -173,6 +174,9 @@ Typical use cases: adding new comments to pages; deleting existing comments; rep
- <code class="language-js">allow:downloads</code>: Allows downloading of the project file. Grants access to endpoints and operations that enable the downloading of the entire project file. - <code class="language-js">allow:downloads</code>: Allows downloading of the project file. Grants access to endpoints and operations that enable the downloading of the entire project file.
Typical use cases: downloading the full project file for backup or sharing. Typical use cases: downloading the full project file for backup or sharing.
- <code class="language-js">allow:localstorage</code>: Allows the access to the local storage proxy to store information. This info is only available for the plugin installation but be aware that a user can see this information in the browser.
Typical use cases: storing authentication tokens for a plugin login
_Note: Write permissions automatically includes its corresponding read permission (e.g., <code class="language-js">content:write</code> includes <code class="language-js">content:read</code>) because reading is required to perform write or modification actions._ _Note: Write permissions automatically includes its corresponding read permission (e.g., <code class="language-js">content:write</code> includes <code class="language-js">content:read</code>) because reading is required to perform write or modification actions._
### What are plugin.ts and plugin.js files? ### What are plugin.ts and plugin.js files?

View File

@ -353,9 +353,9 @@ If you are not using SMTP configuration and want to log the emails in the consol
PENPOT_FLAGS: [...] enable-log-emails PENPOT_FLAGS: [...] enable-log-emails
``` ```
## Redis ## Valkey
The Redis configuration is very simple, just provide a valid redis URI. Redis is used The Valkey configuration is very simple, just provide a valid redis URI. Valkey is used
mainly for websocket notifications coordination. mainly for websocket notifications coordination.
```bash ```bash

View File

@ -72,7 +72,7 @@ argument to helm install. For example,
```bash ```bash
helm install my-release \ helm install my-release \
--set global.postgresqlEnabled=true \ --set global.postgresqlEnabled=true \
--set global.redisEnabled=true \ --set global.valkeyEnabled=true \
--set persistence.assets.enabled=true \ --set persistence.assets.enabled=true \
penpot/penpot penpot/penpot
``` ```

View File

@ -47,7 +47,7 @@
(s/def ::params (s/def ::params
(s/keys :req-un [::exports ::profile-id] (s/keys :req-un [::exports ::profile-id]
:opt-un [::wait ::name])) :opt-un [::wait ::name ::skip-children]))
(defn handler (defn handler
[{:keys [:request/auth-token] :as exchange} {:keys [exports] :as params}] [{:keys [:request/auth-token] :as exchange} {:keys [exports] :as params}]
@ -60,7 +60,7 @@
(handle-multiple-export exchange (assoc params :exports exports))))) (handle-multiple-export exchange (assoc params :exports exports)))))
(defn- handle-single-export (defn- handle-single-export
[exchange {:keys [export wait profile-id name] :as params}] [exchange {:keys [export wait profile-id name skip-children] :as params}]
(let [topic (str profile-id) (let [topic (str profile-id)
resource (rsc/create (:type export) (or name (:name export))) resource (rsc/create (:type export) (or name (:name export)))
@ -90,7 +90,7 @@
:resource-id (:id resource) :resource-id (:id resource)
:status "error" :status "error"
:cause (ex-message cause)}))) :cause (ex-message cause)})))
export (assoc export :skip-children skip-children)
proc (-> (rd/render export on-progress) proc (-> (rd/render export on-progress)
(p/then (constantly resource)) (p/then (constantly resource))
(p/catch on-error))] (p/catch on-error))]
@ -99,7 +99,7 @@
(assoc exchange :response/body (dissoc resource :path))))) (assoc exchange :response/body (dissoc resource :path)))))
(defn- handle-multiple-export (defn- handle-multiple-export
[exchange {:keys [exports wait profile-id name] :as params}] [exchange {:keys [exports wait profile-id name skip-children] :as params}]
(let [resource (rsc/create :zip (or name (-> exports first :name))) (let [resource (rsc/create :zip (or name (-> exports first :name)))
total (count exports) total (count exports)
topic (str profile-id) topic (str profile-id)
@ -141,7 +141,8 @@
proc (-> (p/do proc (-> (p/do
(p/loop [exports (seq exports)] (p/loop [exports (seq exports)]
(when-let [export (first exports)] (when-let [export (-> (first exports)
(assoc :skip-children skip-children))]
(p/do (p/do
(rd/render export append) (rd/render export append)
(p/recur (rest exports))))) (p/recur (rest exports)))))

View File

@ -17,7 +17,7 @@
[promesa.core :as p])) [promesa.core :as p]))
(defn render (defn render
[{:keys [file-id page-id share-id token scale type objects] :as params} on-object] [{:keys [file-id page-id share-id token scale type objects skip-children] :as params} on-object]
(letfn [(prepare-options [uri] (letfn [(prepare-options [uri]
#js {:screen #js {:width bw/default-viewport-width #js {:screen #js {:width bw/default-viewport-width
:height bw/default-viewport-height} :height bw/default-viewport-height}
@ -56,7 +56,8 @@
:page-id page-id :page-id page-id
:share-id share-id :share-id share-id
:object-id (mapv :id objects) :object-id (mapv :id objects)
:route "objects"} :route "objects"
:skip-children skip-children}
uri (-> (cf/get :public-uri) uri (-> (cf/get :public-uri)
(assoc :path "/render.html") (assoc :path "/render.html")
(assoc :query (u/map->query-string params)))] (assoc :query (u/map->query-string params)))]

File diff suppressed because it is too large Load Diff

View File

@ -274,6 +274,7 @@ async function readTranslations() {
"fa", "fa",
"fr", "fr",
"he", "he",
"sr",
"nb_NO", "nb_NO",
"pl", "pl",
"pt_BR", "pt_BR",

View File

@ -444,14 +444,16 @@
(mf/defc object-svg (mf/defc object-svg
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[{:keys [objects object-id embed] [{:keys [objects object-id embed skip-children]
:or {embed false} :or {embed false}
:as props}] :as props}]
(let [object (get objects object-id) (let [object (get objects object-id)
object (cond-> object object (cond-> object
(:hide-fill-on-export object) (:hide-fill-on-export object)
(assoc :fills [])) (assoc :fills [])
skip-children
(assoc :shapes []))
{:keys [width height] :as bounds} (gsb/get-object-bounds objects object {:ignore-margin? false}) {:keys [width height] :as bounds} (gsb/get-object-bounds objects object {:ignore-margin? false})
vbox (format-viewbox bounds) vbox (format-viewbox bounds)

View File

@ -35,6 +35,7 @@
:content (tr "onboarding-v2.newsletter.updates")}])] :content (tr "onboarding-v2.newsletter.updates")}])]
[:div {:class (stl/css :fields-row :input-visible :newsletter-option-wrapper)} [:div {:class (stl/css :fields-row :input-visible :newsletter-option-wrapper)}
[:& fm/input {:name :accept-newsletter-updates [:& fm/input {:name :accept-newsletter-updates
:class (stl/css :checkbox-newsletter-updates)
:type "checkbox" :type "checkbox"
:default-checked false :default-checked false
:label updates-label}]])) :label updates-label}]]))
@ -53,6 +54,7 @@
[:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)} [:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)}
[:& fm/input {:name :accept-terms-and-privacy [:& fm/input {:name :accept-terms-and-privacy
:show-error false
:class (stl/css :checkbox-terms-and-privacy) :class (stl/css :checkbox-terms-and-privacy)
:type "checkbox" :type "checkbox"
:default-checked false :default-checked false

View File

@ -14,9 +14,11 @@
} }
} }
.checkbox-terms-and-privacy { .checkbox-terms-and-privacy,
.checkbox-newsletter-updates {
align-items: flex-start; align-items: flex-start;
} }
.register-form { .register-form {
gap: $s-24; gap: $s-24;
} }

View File

@ -4,9 +4,11 @@
// //
// Copyright (c) KALEIDOS INC // Copyright (c) KALEIDOS INC
@use "../ds/typography.scss" as t;
@import "refactor/common-refactor.scss"; @import "refactor/common-refactor.scss";
.code-display { .code-display {
@include t.use-typography("code-font");
user-select: text; user-select: text;
border-radius: $br-8; border-radius: $br-8;
margin-top: $s-8; margin-top: $s-8;

View File

@ -170,6 +170,9 @@
(rx/filter some?) (rx/filter some?)
(rx/subs! (rx/subs!
(fn [message] (fn [message]
(when (some? (:error message))
(st/emit! (ptk/data-event ::ev/event {::ev/name "import-files-error"
:error (:error message)})))
(swap! state update-with-analyze-result message)))))) (swap! state update-with-analyze-result message))))))
(defn- import-files (defn- import-files
@ -467,6 +470,7 @@
(when (and (= :analyze status) errors?) (when (and (= :analyze status) errors?)
[:& context-notification [:& context-notification
{:level :warning {:level :warning
:class (stl/css :context-notification-error)
:content (tr "dashboard.import.import-warning")}]) :content (tr "dashboard.import.import-warning")}])
(when (= :import-success status) (when (= :import-success status)
@ -480,12 +484,12 @@
:class (stl/css :context-notification-error) :class (stl/css :context-notification-error)
:content (tr "dashboard.import.import-error.disclaimer")}]) :content (tr "dashboard.import.import-error.disclaimer")}])
(if (= :import-error status) (if (or (= :import-error status) (and (= :analyze status) errors?))
[:div {:class (stl/css :import-error-disclaimer)} [:div {:class (stl/css :import-error-disclaimer)}
[:div (tr "dashboard.import.import-error.message1")] [:div (tr "dashboard.import.import-error.message1")]
[:ul {:class (stl/css :import-error-list)} [:ul {:class (stl/css :import-error-list)}
(for [entry entries] (for [entry entries]
(when (= :import-error (:status entry)) (when (contains? #{:import-error :analyze-error} (:status entry))
[:li {:class (stl/css :import-error-list-enry)} (:name entry)]))] [:li {:class (stl/css :import-error-list-enry)} (:name entry)]))]
[:div (tr "dashboard.import.import-error.message2")]] [:div (tr "dashboard.import.import-error.message2")]]

View File

@ -6,11 +6,11 @@
(ns app.main.ui.ds.buttons.icon-button (ns app.main.ui.ds.buttons.icon-button
(:require-macros (:require-macros
[app.common.data.macros :as dm]
[app.main.style :as stl]) [app.main.style :as stl])
(:require (:require
[app.common.data :as d]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]] [app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
[app.main.ui.ds.tooltip.tooltip :refer [tooltip*]] [app.main.ui.ds.tooltip :refer [tooltip*]]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def ^:private schema:icon-button (def ^:private schema:icon-button
@ -24,18 +24,30 @@
[:maybe [:enum "primary" "secondary" "ghost" "destructive" "action"]]]]) [:maybe [:enum "primary" "secondary" "ghost" "destructive" "action"]]]])
(mf/defc icon-button* (mf/defc icon-button*
{::mf/schema schema:icon-button} {::mf/schema schema:icon-button
::mf/memo true}
[{:keys [class icon icon-class variant aria-label children] :rest props}] [{:keys [class icon icon-class variant aria-label children] :rest props}]
(let [variant (or variant "primary") (let [variant
tooltip-id (mf/use-id) (d/nilv variant "primary")
class (dm/str class " " (stl/css-case :icon-button true
:icon-button-primary (= variant "primary") tooltip-id
:icon-button-secondary (= variant "secondary") (mf/use-id)
:icon-button-ghost (= variant "ghost")
:icon-button-action (= variant "action") button-class
:icon-button-destructive (= variant "destructive"))) (stl/css-case :icon-button true
props (mf/spread-props props {:class class :icon-button-primary (identical? variant "primary")
:aria-labelledby tooltip-id})] :icon-button-secondary (identical? variant "secondary")
[:> tooltip* {:tooltip-content aria-label :icon-button-ghost (identical? variant "ghost")
:icon-button-action (identical? variant "action")
:icon-button-destructive (identical? variant "destructive"))
props
(mf/spread-props props
{:class [class button-class]
:aria-labelledby tooltip-id})]
[:> tooltip* {:content aria-label
:id tooltip-id} :id tooltip-id}
[:> "button" props [:> icon* {:icon-id icon :aria-hidden true :class icon-class}] children]])) [:> :button props
[:> icon* {:icon-id icon :aria-hidden true :class icon-class}]
children]]))

View File

@ -0,0 +1,12 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.ds.tooltip
(:require
[app.common.data.macros :as dm]
[app.main.ui.ds.tooltip.tooltip :as impl]))
(dm/export impl/tooltip*)

View File

@ -15,21 +15,49 @@
[app.util.timers :as ts] [app.util.timers :as ts]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(defn- calculate-tooltip-rect [tooltip trigger-rect placement offset] (def ^:private ^:const arrow-height 12)
(def ^:private ^:const half-arrow-height (/ arrow-height 2))
(def ^:private ^:const overlay-offset 32)
(defn- clear-schedule
[ref]
(when-let [schedule (mf/ref-val ref)]
(ts/dispose! schedule)
(mf/set-ref-val! ref nil)))
(defn- add-schedule
[ref delay f]
(mf/set-ref-val! ref (ts/schedule delay f)))
(defn- show-popover
[node]
(when (.-isConnected ^js node)
(.showPopover ^js node)))
(defn- hide-popover
[node]
(dom/unset-css-property! node "block-size")
(dom/unset-css-property! node "display")
(.hidePopover ^js node))
(defn- calculate-placement-bounding-rect
"Given a placement, calcultates the bounding rect for it taking in
account provided tooltip bounding rect and the origin bounding
rect."
[placement tooltip-brect origin-brect offset]
(let [{trigger-top :top (let [{trigger-top :top
trigger-left :left trigger-left :left
trigger-right :right trigger-right :right
trigger-bottom :bottom trigger-bottom :bottom
trigger-width :width trigger-width :width
trigger-height :height} trigger-rect trigger-height :height}
origin-brect
{tooltip-width :width {tooltip-width :width
tooltip-height :height} (dom/get-bounding-rect tooltip) tooltip-height :height}
tooltip-brect
offset (d/nilv offset 2) offset (d/nilv offset 2)]
arrow-height 12
half-arrow-height (/ arrow-height 2)
overlay-offset 32]
(case placement (case placement
"bottom" "bottom"
@ -95,7 +123,10 @@
:width tooltip-width :width tooltip-width
:height tooltip-height}))) :height tooltip-height})))
(defn- get-fallback-order [placement] (defn- get-fallback-order
"Get a vector of placement followed with ordered fallback pacements
for the specified placement"
[placement]
(case placement (case placement
"top" ["top" "right" "bottom" "left" "top-right" "bottom-right" "bottom-left" "top-left"] "top" ["top" "right" "bottom" "left" "top-right" "bottom-right" "bottom-left" "top-left"]
"bottom" ["bottom" "left" "top" "right" "bottom-right" "bottom-left" "top-left" "top-right"] "bottom" ["bottom" "left" "top" "right" "bottom-right" "bottom-left" "top-left" "top-right"]
@ -106,65 +137,93 @@
"bottom-left" ["bottom-left" "left" "top" "right" "bottom" "top-left" "top-right" "bottom-right"] "bottom-left" ["bottom-left" "left" "top" "right" "bottom" "top-left" "top-right" "bottom-right"]
"top-left" ["top-left" "top" "right" "bottom" "left" "bottom-left" "top-right" "bottom-right"])) "top-left" ["top-left" "top" "right" "bottom" "left" "bottom-left" "top-right" "bottom-right"]))
(defn- find-matching-placement
"Algorithm for find a correct placement and placement-brect for the
provided placement, if the current placement does not matches, it
uses the predefined fallbacks. Returns an array of matched placement
and its bounding rect."
[placement tooltip-brect origin-brect window-size offset]
(loop [placements (seq (get-fallback-order placement))]
(when-let [placement (first placements)]
(let [placement-brect (calculate-placement-bounding-rect placement tooltip-brect origin-brect offset)]
(if (dom/is-bounding-rect-outside? placement-brect window-size)
(recur (rest placements))
#js [placement placement-brect])))))
(defn- update-tooltip-position
"Update the tooltip position having in account the current window
size, placement. It calculates the appropriate placement and updates
the dom with the result."
[tooltip placement origin-brect offset]
(show-popover tooltip)
(let [tooltip-brect (dom/get-bounding-rect tooltip)
window-size (dom/get-window-size)]
(when-let [[placement placement-rect] (find-matching-placement placement tooltip-brect origin-brect window-size offset)]
(let [height (if (or (= placement "right") (= placement "left"))
(- (:height placement-rect) arrow-height)
(:height placement-rect))]
(dom/set-css-property! tooltip "display" "grid")
(dom/set-css-property! tooltip "block-size" (dm/str height "px"))
(dom/set-css-property! tooltip "inset-block-start" (dm/str (:top placement-rect) "px"))
(dom/set-css-property! tooltip "inset-inline-start" (dm/str (:left placement-rect) "px")))
placement)))
(def ^:private schema:tooltip (def ^:private schema:tooltip
[:map [:map
[:class {:optional true} :string] [:class {:optional true} :string]
[:id {:optional true} :string] [:id {:optional true} :string]
[:offset {:optional true} :int] [:offset {:optional true} :int]
[:delay {:optional true} :int] [:delay {:optional true} :int]
[:content [:or fn? :string [:fn mf/element?]]]
[:placement {:optional true} [:placement {:optional true}
[:maybe [:enum "top" "bottom" "left" "right" "top-right" "bottom-right" "bottom-left" "top-left"]]]]) [:maybe [:enum "top" "bottom" "left" "right" "top-right" "bottom-right" "bottom-left" "top-left"]]]])
(mf/defc tooltip* (mf/defc tooltip*
{::mf/schema schema:tooltip} {::mf/schema schema:tooltip}
[{:keys [class id children tooltip-content placement offset delay] :rest props}] [{:keys [class id children content placement offset delay] :rest props}]
(let [id (or id (mf/use-id)) (let [internal-id
placement* (mf/use-state #(d/nilv placement "top")) (mf/use-id)
placement (deref placement*)
delay (d/nilv delay 300)
schedule-ref (mf/use-ref nil) id
(d/nilv id internal-id)
position-tooltip placement*
(fn [^js tooltip trigger-rect] (mf/use-state #(d/nilv placement "top"))
(let [all-placements (get-fallback-order placement)]
(when (.-isConnected tooltip) placement
(.showPopover ^js tooltip)) (deref placement*)
(loop [[current-placement & remaining-placements] all-placements]
(when current-placement delay
(reset! placement* current-placement) (d/nilv delay 300)
(let [tooltip-rect (calculate-tooltip-rect tooltip trigger-rect current-placement offset)]
(if (dom/is-bounding-rect-outside? tooltip-rect) schedule-ref
(recur remaining-placements) (mf/use-ref nil)
(do (dom/set-css-property! tooltip "display" "grid")
(dom/set-css-property! tooltip "inset-block-start" (dm/str (:top tooltip-rect) "px"))
(dom/set-css-property! tooltip "inset-inline-start" (dm/str (:left tooltip-rect) "px")))))))))
on-show on-show
(mf/use-fn (mf/use-fn
(mf/deps id placement) (mf/deps id placement offset)
(fn [event] (fn [event]
(when-let [schedule (mf/ref-val schedule-ref)] (clear-schedule schedule-ref)
(ts/dispose! schedule)
(mf/set-ref-val! schedule-ref nil))
(when-let [tooltip (dom/get-element id)] (when-let [tooltip (dom/get-element id)]
(let [trigger-rect (->> (dom/get-target event) (let [origin-brect
(dom/get-bounding-rect))] (->> (dom/get-target event)
(mf/set-ref-val! (dom/get-bounding-rect))
schedule-ref
(ts/schedule update-position
delay (fn []
#(position-tooltip tooltip trigger-rect))))))) (let [placement (update-tooltip-position tooltip placement origin-brect offset)]
(reset! placement* placement)))]
(add-schedule schedule-ref delay update-position)))))
on-hide on-hide
(mf/use-fn (mf/use-fn
(mf/deps id) (mf/deps id)
(fn [] (when-let [tooltip (dom/get-element id)] (fn []
(when-let [schedule (mf/ref-val schedule-ref)] (when-let [tooltip (dom/get-element id)]
(ts/dispose! schedule) (clear-schedule schedule-ref)
(mf/set-ref-val! schedule-ref nil)) (hide-popover tooltip))))
(dom/unset-css-property! tooltip "display")
(.hidePopover ^js tooltip))))
handle-key-down handle-key-down
(mf/use-fn (mf/use-fn
@ -173,33 +232,38 @@
(when (kbd/esc? event) (when (kbd/esc? event)
(on-hide)))) (on-hide))))
class (d/append-class class (stl/css-case tooltip-class
:tooltip true (stl/css-case
:tooltip-top (= placement "top") :tooltip true
:tooltip-bottom (= placement "bottom") :tooltip-top (identical? placement "top")
:tooltip-left (= placement "left") :tooltip-bottom (identical? placement "bottom")
:tooltip-right (= placement "right") :tooltip-left (identical? placement "left")
:tooltip-top-right (= placement "top-right") :tooltip-right (identical? placement "right")
:tooltip-bottom-right (= placement "bottom-right") :tooltip-top-right (identical? placement "top-right")
:tooltip-bottom-left (= placement "bottom-left") :tooltip-bottom-right (identical? placement "bottom-right")
:tooltip-top-left (= placement "top-left"))) :tooltip-bottom-left (identical? placement "bottom-left")
:tooltip-top-left (identical? placement "top-left"))
props
(mf/spread-props props
{:on-mouse-enter on-show
:on-mouse-leave on-hide
:on-focus on-show
:on-blur on-hide
:on-key-down handle-key-down
:class (stl/css :tooltip-trigger)
:aria-describedby id})
content
(if (fn? content)
(content)
content)]
props (mf/spread-props props {:on-mouse-enter on-show
:on-mouse-leave on-hide
:on-focus on-show
:on-blur on-hide
:on-key-down handle-key-down
:class (stl/css :tooltip-trigger)
:aria-describedby id})]
[:> :div props [:> :div props
children children
[:div {:class class [:div {:class [class tooltip-class]
:id id :id id
:popover "auto" :popover "auto"
:role "tooltip"} :role "tooltip"}
[:div {:class (stl/css :tooltip-content)} [:div {:class (stl/css :tooltip-content)} content]
(if (fn? tooltip-content)
(tooltip-content)
tooltip-content)]
[:div {:class (stl/css :tooltip-arrow) [:div {:class (stl/css :tooltip-arrow)
:id "tooltip-arrow"}]]])) :id "tooltip-arrow"}]]]))

View File

@ -32,7 +32,7 @@ If `placement` is not provided, the tooltip will default to "top".
[:> tlp/tooltip* {:id "test-tooltip" [:> tlp/tooltip* {:id "test-tooltip"
:placement "bottom" :placement "bottom"
:tooltip-content "Tooltip content"} :content "Tooltip content"}
[:div "Trigger component"]]) [:div "Trigger component"]])
``` ```
@ -44,8 +44,7 @@ Tooltip content can include HTML elements:
[:> tlp/tooltip* {:id "test-tooltip" [:> tlp/tooltip* {:id "test-tooltip"
:placement "bottom" :placement "bottom"
:tooltip-content (mf/html :content (mf/html [:span "Tooltip content"])}
[:span "Tooltip content"])}
[:div "Trigger component"]]) [:div "Trigger component"]])
``` ```

View File

@ -16,6 +16,7 @@ $arrow-side: 12px;
background-color: transparent; background-color: transparent;
overflow: hidden; overflow: hidden;
inline-size: fit-content; inline-size: fit-content;
block-size: fit-content;
} }
.tooltip-arrow { .tooltip-arrow {
@ -145,6 +146,7 @@ $arrow-side: 12px;
border: $b-1 solid var(--color-accent-primary-muted); border: $b-1 solid var(--color-accent-primary-muted);
padding: var(--sp-s) var(--sp-m); padding: var(--sp-s) var(--sp-m);
grid-area: content; grid-area: content;
block-size: fit-content;
} }
.tooltip-trigger { .tooltip-trigger {

View File

@ -37,10 +37,10 @@ export default {
}, },
args: { args: {
children: ( children: (
<button popovertarget="popover-example">Hover this element</button> <button popoverTarget="popover-example">Hover this element</button>
), ),
id: "popover-example", id: "popover-example",
tooltipContent: "This is the tooltip content", content: "This is the tooltip content",
delay: 300, delay: 300,
}, },
render: ({ children, ...args }) => ( render: ({ children, ...args }) => (
@ -74,18 +74,18 @@ export const Corners = {
> >
<Tooltip <Tooltip
id="popover-example10" id="popover-example10"
tooltipContent="This is the tooltip content, and must be shown in two lines." content="This is the tooltip content, it's very long, and must be shown in three lines to check how it respond to different sizes."
style={{ style={{
placeSelf: "start start", placeSelf: "start start",
width: "fit-content", width: "fit-content",
height: "fit-content", height: "fit-content",
}} }}
> >
<button popovertarget="popover-example10">Hover here</button> <button popoverTarget="popover-example10">Hover here</button>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
id="popover-example2" id="popover-example2"
tooltipContent="This is the tooltip content, and must be shown in two lines." content="This is the tooltip content, it's very long, and must be shown in three lines to check how it respond to different sizes."
style={{ style={{
alignSelf: "start", alignSelf: "start",
justifySelf: "center", justifySelf: "center",
@ -94,7 +94,7 @@ export const Corners = {
}} }}
> >
<button <button
popovertarget="popover-example2" popoverTarget="popover-example2"
style={{ style={{
alignSelf: "start", alignSelf: "start",
justifySelf: "center", justifySelf: "center",
@ -106,7 +106,7 @@ export const Corners = {
</Tooltip> </Tooltip>
<Tooltip <Tooltip
id="popover-example3" id="popover-example3"
tooltipContent="This is the tooltip content, and must be shown in two lines." content="This is the tooltip content, it's very long, and must be shown in three lines to check how it respond to different sizes."
style={{ style={{
alignSelf: "start", alignSelf: "start",
justifySelf: "end", justifySelf: "end",
@ -114,11 +114,11 @@ export const Corners = {
height: "fit-content", height: "fit-content",
}} }}
> >
<button popovertarget="popover-example3">Hover here</button> <button popoverTarget="popover-example3">Hover here</button>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
id="popover-example4" id="popover-example4"
tooltipContent="This is the tooltip content, and must be shown in two lines." content="This is the tooltip content, it's very long, and must be shown in three lines to check how it respond to different sizes."
style={{ style={{
alignSelf: "center", alignSelf: "center",
justifySelf: "start", justifySelf: "start",
@ -126,11 +126,11 @@ export const Corners = {
height: "fit-content", height: "fit-content",
}} }}
> >
<button popovertarget="popover-example4">Hover here</button> <button popoverTarget="popover-example4">Hover here</button>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
id="popover-example5" id="popover-example5"
tooltipContent="This is the tooltip content, and must be shown in two lines." content="This is the tooltip content, it's very long, and must be shown in three lines to check how it respond to different sizes."
style={{ style={{
alignSelf: "center", alignSelf: "center",
justifySelf: "center", justifySelf: "center",
@ -138,11 +138,11 @@ export const Corners = {
height: "fit-content", height: "fit-content",
}} }}
> >
<button popovertarget="popover-example5">Hover here</button> <button popoverTarget="popover-example5">Hover here</button>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
id="popover-example6" id="popover-example6"
tooltipContent="This is the tooltip content, and must be shown in two lines." content="This is the tooltip content, it's very long, and must be shown in three lines to check how it respond to different sizes."
style={{ style={{
alignSelf: "center", alignSelf: "center",
justifySelf: "end", justifySelf: "end",
@ -150,11 +150,11 @@ export const Corners = {
height: "fit-content", height: "fit-content",
}} }}
> >
<button popovertarget="popover-example6">Hover here</button> <button popoverTarget="popover-example6">Hover here</button>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
id="popover-example7" id="popover-example7"
tooltipContent="This is the tooltip content, and must be shown in two lines." content="This is the tooltip content, it's very long, and must be shown in three lines to check how it respond to different sizes."
style={{ style={{
alignSelf: "end", alignSelf: "end",
justifySelf: "start", justifySelf: "start",
@ -162,11 +162,11 @@ export const Corners = {
height: "fit-content", height: "fit-content",
}} }}
> >
<button popovertarget="popover-example7">Hover here</button> <button popoverTarget="popover-example7">Hover here</button>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
id="popover-example8" id="popover-example8"
tooltipContent="This is the tooltip content, and must be shown in two lines." content="This is the tooltip content, it's very long, and must be shown in three lines to check how it respond to different sizes."
style={{ style={{
alignSelf: "end", alignSelf: "end",
justifySelf: "center", justifySelf: "center",
@ -174,11 +174,11 @@ export const Corners = {
height: "fit-content", height: "fit-content",
}} }}
> >
<button popovertarget="popover-example8">Hover here</button> <button popoverTarget="popover-example8">Hover here</button>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
id="popover-example9" id="popover-example9"
tooltipContent="This is the tooltip content, and must be shown in two lines." content="This is the tooltip content, it's very long, and must be shown in three lines to check how it respond to different sizes."
style={{ style={{
alignSelf: "end", alignSelf: "end",
justifySelf: "end", justifySelf: "end",
@ -186,7 +186,7 @@ export const Corners = {
height: "fit-content", height: "fit-content",
}} }}
> >
<button popovertarget="popover-example9">Hover here</button> <button popoverTarget="popover-example9">Hover here</button>
</Tooltip> </Tooltip>
</div> </div>
), ),

View File

@ -21,6 +21,7 @@
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr c]] [app.util.i18n :as i18n :refer [tr c]]
[app.util.strings :as ust] [app.util.strings :as ust]
[app.util.theme :as theme]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@ -208,8 +209,8 @@
[] []
(let [state (mf/deref refs/export) (let [state (mf/deref refs/export)
profile (mf/deref refs/profile) profile (mf/deref refs/profile)
theme (or (:theme profile) "default") theme (or (:theme profile) theme/default)
is-default-theme? (= "default" theme) is-default-theme? (= theme/default theme)
error? (:error state) error? (:error state)
healthy? (:healthy? state) healthy? (:healthy? state)
detail-visible? (:detail-visible state) detail-visible? (:detail-visible state)

View File

@ -275,7 +275,14 @@
[:div {:class (stl/css :permissions-list-entry)} [:div {:class (stl/css :permissions-list-entry)}
i/oauth-1 i/oauth-1
[:p {:class (stl/css :permissions-list-text)} [:p {:class (stl/css :permissions-list-text)}
(tr "workspace.plugins.permissions.allow-download")]])]) (tr "workspace.plugins.permissions.allow-download")]])
(cond
(contains? permissions "allow:localstorage")
[:div {:class (stl/css :permissions-list-entry)}
i/oauth-1
[:p {:class (stl/css :permissions-list-text)}
(tr "workspace.plugins.permissions.allow-localstorage")]])])
(mf/defc plugins-permissions-dialog (mf/defc plugins-permissions-dialog
{::mf/register modal/components {::mf/register modal/components

View File

@ -9,8 +9,10 @@
(:require (:require
[app.common.json :as json] [app.common.json :as json]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.main.data.event :as ev]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.code-block :refer [code-block]] [app.main.ui.components.code-block :refer [code-block]]
[app.main.ui.ds.buttons.button :refer [button*]] [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*]]
@ -22,6 +24,7 @@
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.webapi :as wapi] [app.util.webapi :as wapi]
[app.util.zip :as zip] [app.util.zip :as zip]
[potok.v2.core :as ptk]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(mf/defc export-tab* (mf/defc export-tab*
@ -59,6 +62,7 @@
(mf/deps tokens-json) (mf/deps tokens-json)
(fn [] (fn []
(when tokens-json (when tokens-json
(st/emit! (ptk/data-event ::ev/event {::ev/name "export-tokens" :type "single"}))
(->> (wapi/create-blob (or tokens-json "{}") "application/json") (->> (wapi/create-blob (or tokens-json "{}") "application/json")
(dom/trigger-download "tokens.json")))))] (dom/trigger-download "tokens.json")))))]
[:> export-tab* {:is-disabled is-disabled [:> export-tab* {:is-disabled is-disabled
@ -85,6 +89,7 @@
(mf/use-fn (mf/use-fn
(mf/deps files) (mf/deps files)
(fn [] (fn []
(st/emit! (ptk/data-event ::ev/event {::ev/name "export-tokens" :type "multiple"}))
(download-tokens-zip! files)))] (download-tokens-zip! files)))]
[:> export-tab* {:on-export on-export [:> export-tab* {:on-export on-export
:is-disabled is-disabled} :is-disabled is-disabled}

View File

@ -38,6 +38,7 @@
flex-direction: column; flex-direction: column;
gap: var(--sp-m); gap: var(--sp-m);
padding-top: var(--sp-m); padding-top: var(--sp-m);
max-width: calc(var(--modal-width) - var(--modal-padding) * 2);
} }
.preview-label { .preview-label {
@ -79,8 +80,10 @@
flex-grow: 1; flex-grow: 1;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
padding: var(--sp-xs);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
padding: var(--sp-s);
} }
.export-actions { .export-actions {
@ -108,7 +111,6 @@
overflow-x: auto; overflow-x: auto;
word-wrap: normal; word-wrap: normal;
white-space: pre; white-space: pre;
max-width: calc(var(--modal-width) - var(--modal-padding) * 2);
} }
.disabled-message { .disabled-message {

View File

@ -10,7 +10,6 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.config :as cf] [app.config :as cf]
[app.main.data.event :as ev]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.style-dictionary :as sd] [app.main.data.style-dictionary :as sd]
[app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.tokens.application :as dwta]
@ -37,7 +36,6 @@
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[okulary.core :as l] [okulary.core :as l]
[potok.v2.core :as ptk]
[rumext.v2 :as mf] [rumext.v2 :as mf]
[shadow.resource])) [shadow.resource]))
@ -384,7 +382,6 @@
on-export on-export
(mf/use-fn (mf/use-fn
(fn [] (fn []
(st/emit! (ptk/data-event ::ev/event {::ev/name "export-tokens"}))
(modal/show! :tokens/export {}))) (modal/show! :tokens/export {})))
on-modal-show on-modal-show

View File

@ -47,29 +47,32 @@
:class (stl/css-case :check-icon true :class (stl/css-case :check-icon true
:check-icon-visible selected?)}]])])) :check-icon-visible selected?)}]])]))
(defn- open-tokens-theme-modal
[]
(modal/show! :tokens/themes {}))
(mf/defc theme-options (mf/defc theme-options
[{:keys [active-theme-paths themes on-close]}] [{:keys [active-theme-paths themes on-close]}]
(let [on-edit-click #(modal/show! :tokens/themes {})] [:ul {:class (stl/css :theme-options :custom-select-dropdown)
[:ul {:class (stl/css :theme-options :custom-select-dropdown) :role "listbox"}
:role "listbox"} (for [[group themes] themes]
(for [[group themes] themes] [:li {:key group
[:li {:key group :aria-labelledby (dm/str group "-label")
:aria-labelledby (dm/str group "-label") :role "group"}
:role "group"} (when (seq group)
(when (seq group) [:> text* {:as "span" :typography "headline-small" :class (stl/css :group) :id (dm/str (str/kebab group) "-label") :title group} group])
[:> text* {:as "span" :typography "headline-small" :class (stl/css :group) :id (dm/str (str/kebab group) "-label") :title group} group]) [:& themes-list {:themes themes
[:& themes-list {:themes themes :active-theme-paths active-theme-paths
:active-theme-paths active-theme-paths :on-close on-close
:on-close on-close :grouped? true}]])
:grouped? true}]]) [:li {:class (stl/css :separator)
[:li {:class (stl/css :separator) :aria-hidden true}]
:aria-hidden true}] [:li {:class (stl/css-case :checked-element true
[:li {:class (stl/css-case :checked-element true :checked-element-button true)
:checked-element-button true) :role "option"
:role "option" :on-click open-tokens-theme-modal}
:on-click on-edit-click} [:> text* {:as "span" :typography "body-small"} (tr "workspace.tokens.edit-themes")]
[:> text* {:as "span" :typography "body-small"} (tr "workspace.tokens.edit-themes")] [:> icon* {:icon-id i/arrow-right :aria-hidden true}]]])
[:> icon* {:icon-id i/arrow-right :aria-hidden true}]]]))
(mf/defc theme-select (mf/defc theme-select
[{:keys []}] [{:keys []}]

View File

@ -25,14 +25,17 @@
[app.main.data.workspace.groups :as dwg] [app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.media :as dwm] [app.main.data.workspace.media :as dwm]
[app.main.data.workspace.selection :as dws] [app.main.data.workspace.selection :as dws]
[app.main.fonts :refer [fetch-font-css]]
[app.main.router :as rt] [app.main.router :as rt]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.shapes.text.fontfaces :refer [shapes->fonts]]
[app.plugins.events :as events] [app.plugins.events :as events]
[app.plugins.file :as file] [app.plugins.file :as file]
[app.plugins.fonts :as fonts] [app.plugins.fonts :as fonts]
[app.plugins.format :as format] [app.plugins.format :as format]
[app.plugins.history :as history] [app.plugins.history :as history]
[app.plugins.library :as library] [app.plugins.library :as library]
[app.plugins.local-storage :as local-storage]
[app.plugins.page :as page] [app.plugins.page :as page]
[app.plugins.parser :as parser] [app.plugins.parser :as parser]
[app.plugins.shape :as shape] [app.plugins.shape :as shape]
@ -41,7 +44,8 @@
[app.plugins.viewport :as viewport] [app.plugins.viewport :as viewport]
[app.util.code-gen :as cg] [app.util.code-gen :as cg]
[app.util.object :as obj] [app.util.object :as obj]
[beicon.v2.core :as rx])) [beicon.v2.core :as rx]
[cuerdas.core :as str]))
;; ;;
;; PLUGINS PUBLIC API - The plugins will able to access this functions ;; PLUGINS PUBLIC API - The plugins will able to access this functions
@ -85,6 +89,11 @@
{:this true {:this true
:get #(.getTheme ^js %)} :get #(.getTheme ^js %)}
:localStorage
{:this true
:get
(fn [_] (local-storage/local-storage-proxy plugin-id))}
:selection :selection
{:this true {:this true
:get #(.getSelectedShapes ^js %) :get #(.getSelectedShapes ^js %)
@ -428,6 +437,24 @@
(cg/generate-style-code (cg/generate-style-code
objects type shapes shapes-with-children {:with-prelude? prelude?}))))) objects type shapes shapes-with-children {:with-prelude? prelude?})))))
:generateFontFaces
(fn [shapes]
(js/Promise.
(fn [resolve reject]
(let [objects (u/locate-objects)
all-children
(->> shapes
(map #(obj/get % "$id"))
(cfh/selected-with-children objects)
(map (d/getf objects)))
fonts (shapes->fonts all-children)]
(->> (rx/from fonts)
(rx/merge-map fetch-font-css)
(rx/reduce conj [])
(rx/map #(str/join "\n" %))
(rx/first)
(rx/subs! #(resolve %) reject))))))
:openViewer :openViewer
(fn [] (fn []
(let [params {:page-id (:current-page-id @st/state) (let [params {:page-id (:current-page-id @st/state)

View File

@ -0,0 +1,71 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.plugins.local-storage
(:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.plugins.register :as r]
[app.plugins.utils :as u]
[app.util.globals :as g]
[app.util.object :as obj]
[cuerdas.core :as str]))
(defonce ^:private local-storage
(ex/ignoring (unchecked-get g/global "localStorage")))
(defn prefix-key
[plugin-id key]
(dm/str "penpot-plugins:" plugin-id "/" key))
(defn local-storage-proxy
[plugin-id]
(obj/reify {:name "LocalStorageProxy"}
:$plugin {:enumerable false :get (fn [] plugin-id)}
:getItem
(fn [key]
(cond
(not (r/check-permission plugin-id "allow:localstorage"))
(u/display-not-valid :getItem "Plugin doesn't have 'allow:localstorage' permission")
(not (string? key))
(u/display-not-valid :getItem "The key must be a string")
:else
(.getItem ^js local-storage (prefix-key plugin-id key))))
:setItem
(fn [key value]
(cond
(not (r/check-permission plugin-id "allow:localstorage"))
(u/display-not-valid :setItem "Plugin doesn't have 'allow:localstorage' permission")
(not (string? key))
(u/display-not-valid :setItem "The key must be a string")
:else
(.setItem ^js local-storage (prefix-key plugin-id key) value)))
:removeItem
(fn [key]
(cond
(not (r/check-permission plugin-id "allow:localstorage"))
(u/display-not-valid :removeItem "Plugin doesn't have 'allow:localstorage' permission")
(not (string? key))
(u/display-not-valid :removeItem "The key must be a string")
:else
(.getItem ^js local-storage (prefix-key plugin-id key))))
:getKeys
(fn []
(->> (.keys js/Object local-storage)
(filter #(str/starts-with? % (prefix-key plugin-id "")))
(map #(str/replace % (prefix-key plugin-id "") ""))
(apply array)))))

View File

@ -253,7 +253,8 @@
(d/without-nils (d/without-nils
{:type (-> (obj/get export "type") parse-keyword) {:type (-> (obj/get export "type") parse-keyword)
:scale (obj/get export "scale" 1) :scale (obj/get export "scale" 1)
:suffix (obj/get export "suffix" "")}))) :suffix (obj/get export "suffix" "")
:skip-children (obj/get export "skipChildren" false)})))
(defn parse-exports (defn parse-exports
[^js exports] [^js exports]

View File

@ -1138,6 +1138,7 @@
{:cmd :export-shapes {:cmd :export-shapes
:profile-id (:profile-id @st/state) :profile-id (:profile-id @st/state)
:wait true :wait true
:skip-children (:skip-children value false)
:exports [{:file-id file-id :exports [{:file-id file-id
:page-id page-id :page-id page-id
:object-id id :object-id id
@ -1230,6 +1231,39 @@
(cond-> (cfh/frame-shape? data) (cond-> (cfh/frame-shape? data)
(-> (crc/add-properties! (-> (crc/add-properties!
{:name "clipContent"
:get
(fn [self]
(-> self u/proxy->shape :show-content not))
:set
(fn [_ value]
(cond
(not (boolean? value))
(u/display-not-valid :clipContent value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :clipContent "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :show-content (not value))))))}
{:name "showInViewMode"
:get
(fn [self]
(-> self u/proxy->shape :hide-in-viewer not))
:set
(fn [_ value]
(cond
(not (boolean? value))
(u/display-not-valid :showInViewMode value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :showInViewMode "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :hide-in-viewer (not value))))))}
{:name "grid" {:name "grid"
:get :get
(fn [self] (fn [self]

View File

@ -62,7 +62,7 @@
(mf/defc object-svg (mf/defc object-svg
{::mf/wrap-props false} {::mf/wrap-props false}
[{:keys [object-id embed]}] [{:keys [object-id embed skip-children]}]
(let [objects (mf/deref ref:objects)] (let [objects (mf/deref ref:objects)]
;; Set the globa CSS to assign the page size, needed for PDF ;; Set the globa CSS to assign the page size, needed for PDF
@ -79,11 +79,12 @@
[:& render/object-svg [:& render/object-svg
{:objects objects {:objects objects
:object-id object-id :object-id object-id
:embed embed}]))) :embed embed
:skip-children skip-children}])))
(mf/defc objects-svg (mf/defc objects-svg
{::mf/wrap-props false} {::mf/wrap-props false}
[{:keys [object-ids embed]}] [{:keys [object-ids embed skip-children]}]
(when-let [objects (mf/deref ref:objects)] (when-let [objects (mf/deref ref:objects)]
(for [object-id object-ids] (for [object-id object-ids]
(let [objects (render/adapt-objects-for-shape objects object-id)] (let [objects (render/adapt-objects-for-shape objects object-id)]
@ -91,7 +92,8 @@
{:objects objects {:objects objects
:key (str object-id) :key (str object-id)
:object-id object-id :object-id object-id
:embed embed}])))) :embed embed
:skip-children skip-children}]))))
(defn- fetch-objects-bundle (defn- fetch-objects-bundle
[& {:keys [file-id page-id share-id object-id] :as options}] [& {:keys [file-id page-id share-id object-id] :as options}]
@ -121,6 +123,7 @@
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:share-id {:optional true} ::sm/uuid] [:share-id {:optional true} ::sm/uuid]
[:embed {:optional true} :boolean] [:embed {:optional true} :boolean]
[:skip-children {:optional true} :boolean]
[:object-id [:object-id
[:or [:or
::sm/uuid ::sm/uuid
@ -135,7 +138,7 @@
(defn- render-objects (defn- render-objects
[params] [params]
(let [{:keys [file-id page-id embed share-id object-id] :as params} (render-objects-decoder params)] (let [{:keys [file-id page-id embed share-id object-id skip-children] :as params} (render-objects-decoder params)]
(if-not (render-objects-validator params) (if-not (render-objects-validator params)
(do (do
(js/console.error "invalid arguments") (js/console.error "invalid arguments")
@ -152,7 +155,8 @@
:page-id page-id :page-id page-id
:share-id share-id :share-id share-id
:object-id object-id :object-id object-id
:embed embed}]) :embed embed
:skip-children skip-children}])
(mf/html (mf/html
[:& objects-svg [:& objects-svg
@ -160,7 +164,8 @@
:page-id page-id :page-id page-id
:share-id share-id :share-id share-id
:object-ids (into #{} object-id) :object-ids (into #{} object-id)
:embed embed}])))))) :embed embed
:skip-children skip-children}]))))))
;; ---- COMPONENTS SPRITE ;; ---- COMPONENTS SPRITE

View File

@ -421,17 +421,16 @@
:height (.-height ^js rect)})) :height (.-height ^js rect)}))
(defn is-bounding-rect-outside? (defn is-bounding-rect-outside?
[rect] [{:keys [left top right bottom]} {:keys [width height]}]
(let [{:keys [left top right bottom]} rect (or (< left 0)
{:keys [width height]} (get-window-size)] (< top 0)
(or (< left 0) (> right width)
(< top 0) (> bottom height)))
(> right width)
(> bottom height))))
(defn is-element-outside? (defn is-element-outside?
[element] [element]
(is-bounding-rect-outside? (get-bounding-rect element))) (is-bounding-rect-outside? (get-bounding-rect element)
(get-window-size)))
(defn bounding-rect->rect (defn bounding-rect->rect
[rect] [rect]

View File

@ -9,6 +9,7 @@
extensions" extensions"
(:require (:require
[promesa.impl :as pi]) [promesa.impl :as pi])
(:import goog.async.Deferred)) (:import
goog.async.Deferred))
(pi/extend-promise! Deferred) (pi/extend-promise! Deferred)

View File

@ -43,6 +43,7 @@
{:label "Украї́нська мо́ва (community)" :value "uk"} {:label "Украї́нська мо́ва (community)" :value "uk"}
{:label "Český jazyk (community)" :value "cs"} {:label "Český jazyk (community)" :value "cs"}
{:label "Latviešu valoda (community)" :value "lv"} {:label "Latviešu valoda (community)" :value "lv"}
{:label "Српски (community)" :value "sr"}
{:label "Føroyskt mál (community)" :value "fo"} {:label "Føroyskt mál (community)" :value "fo"}
{:label "Korean (community)" :value "ko"} {:label "Korean (community)" :value "ko"}
{:label "עִבְרִית (community)" :value "he"} {:label "עִבְרִית (community)" :value "he"}

View File

@ -176,10 +176,10 @@
(t/is (= (-> (. shape -exports) (aget 0) (aget "type")) "pdf")) (t/is (= (-> (. shape -exports) (aget 0) (aget "type")) "pdf"))
(t/is (= (-> (. shape -exports) (aget 0) (aget "scale")) 2)) (t/is (= (-> (. shape -exports) (aget 0) (aget "scale")) 2))
(t/is (= (-> (. shape -exports) (aget 0) (aget "suffix")) "test")) (t/is (= (-> (. shape -exports) (aget 0) (aget "suffix")) "test"))
(t/is (= (get-in @store (get-shape-path :exports)) [{:type :pdf :scale 2 :suffix "test"}])) (t/is (= (get-in @store (get-shape-path :exports)) [{:type :pdf :scale 2 :suffix "test" :skip-children false}]))
(set! (.-exports shape) #js [#js {:type 10 :scale 2 :suffix "test"}]) (set! (.-exports shape) #js [#js {:type 10 :scale 2 :suffix "test"}])
(t/is (= (get-in @store (get-shape-path :exports)) [{:type :pdf :scale 2 :suffix "test"}]))) (t/is (= (get-in @store (get-shape-path :exports)) [{:type :pdf :scale 2 :suffix "test" :skip-children false}])))
(t/testing " - flipX" (t/testing " - flipX"
(set! (.-flipX shape) true) (set! (.-flipX shape) true)

View File

@ -6670,6 +6670,9 @@ msgstr ""
msgid "workspace.plugins.permissions.allow-download" msgid "workspace.plugins.permissions.allow-download"
msgstr "Start file downloads." msgstr "Start file downloads."
msgid "workspace.plugins.permissions.allow-localstorage"
msgstr "Store data in the browser."
#: src/app/main/ui/workspace/plugins.cljs:271 #: src/app/main/ui/workspace/plugins.cljs:271
msgid "workspace.plugins.permissions.comment-read" msgid "workspace.plugins.permissions.comment-read"
msgstr "Read your comments and replies." msgstr "Read your comments and replies."

View File

@ -6696,6 +6696,9 @@ msgstr ""
msgid "workspace.plugins.permissions.allow-download" msgid "workspace.plugins.permissions.allow-download"
msgstr "Comenzar descargas de ficheros." msgstr "Comenzar descargas de ficheros."
msgid "workspace.plugins.permissions.allow-localstorage"
msgstr "Guardar datos en el navegador."
#: src/app/main/ui/workspace/plugins.cljs:271 #: src/app/main/ui/workspace/plugins.cljs:271
msgid "workspace.plugins.permissions.comment-read" msgid "workspace.plugins.permissions.comment-read"
msgstr "Leer tus comentarios y respuestas." msgstr "Leer tus comentarios y respuestas."