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
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!)
### :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)
- 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)
- Add Serbian language
### :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 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)
- 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

View File

@ -28,7 +28,7 @@
integrant/integrant {:mvn/version "0.13.1"}
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
{:git/sha "f52f58cfacf62f59eab717e2637f37729d0cc383"
:git/url "https://github.com/funcool/promesa"}

View File

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

View File

@ -104,36 +104,11 @@
(impl/path-data
(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
"Applies a transformation matrix over content and returns a new
content as PathData instance."
[content transform]
#_(segment/transform-content content transform)
(some-> (transform-content-legacy (vec content) transform)
(impl/from-plain)))
(segment/transform-content content transform))
(defn move-content
[content move-vec]

View File

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

View File

@ -313,7 +313,7 @@
:title "Shape"}
[:group
[:merge {:title "GroupShape"}
ctsl/schema:layout-attrs
ctsl/schema:layout-child-attrs
schema:group-attrs
schema:shape-generic-attrs
schema:shape-geom-attrs
@ -321,8 +321,8 @@
[:frame
[:merge {:title "FrameShape"}
ctsl/schema:layout-child-attrs
ctsl/schema:layout-attrs
::ctsl/layout-attrs
schema:frame-attrs
schema:shape-generic-attrs
schema:shape-geom-attrs
@ -332,14 +332,14 @@
[:bool
[:merge {:title "BoolShape"}
ctsl/schema:layout-attrs
ctsl/schema:layout-child-attrs
schema:bool-attrs
schema:shape-generic-attrs
schema:shape-base-attrs]]
[:rect
[:merge {:title "RectShape"}
ctsl/schema:layout-attrs
ctsl/schema:layout-child-attrs
schema:rect-attrs
schema:shape-generic-attrs
schema:shape-geom-attrs
@ -347,7 +347,7 @@
[:circle
[:merge {:title "CircleShape"}
ctsl/schema:layout-attrs
ctsl/schema:layout-child-attrs
schema:circle-attrs
schema:shape-generic-attrs
schema:shape-geom-attrs
@ -355,7 +355,7 @@
[:image
[:merge {:title "ImageShape"}
ctsl/schema:layout-attrs
ctsl/schema:layout-child-attrs
schema:image-attrs
schema:shape-generic-attrs
schema:shape-geom-attrs
@ -363,7 +363,7 @@
[:svg-raw
[:merge {:title "SvgRawShape"}
ctsl/schema:layout-attrs
ctsl/schema:layout-child-attrs
schema:svg-raw-attrs
schema:shape-generic-attrs
schema:shape-geom-attrs
@ -371,14 +371,14 @@
[:path
[:merge {:title "PathShape"}
ctsl/schema:layout-attrs
ctsl/schema:layout-child-attrs
schema:path-attrs
schema:shape-generic-attrs
schema:shape-base-attrs]]
[:text
[:merge {:title "TextShape"}
ctsl/schema:layout-attrs
ctsl/schema:layout-child-attrs
schema:text-attrs
schema:shape-generic-attrs
schema:shape-geom-attrs
@ -682,23 +682,6 @@
:r3
: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
"Retrieves an object with the 'pasteable' properties for a shape."
[shape]
@ -729,9 +712,8 @@
(assoc-props node txt/text-node-attrs)))
props)))
(extract-layout-props
[props shape]
(d/patch-object props (select-keys shape layout-extract-props)))]
(extract-layout-attrs [props shape]
(d/patch-object props (select-keys shape ctsl/layout-attrs)))]
(let [;; For texts we don't extract the fill
extract-props
@ -739,7 +721,7 @@
(-> shape
(select-keys extract-props)
(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
"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))))))))))
(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
(ctsl/grid-layout? shape)
(ctsl/assign-cells objects))))]

View File

@ -43,7 +43,6 @@
;; :layout-item-absolute ;; boolean
;; :layout-item-z-index ;; int
(def layout-types
#{:flex :grid})
@ -74,49 +73,6 @@
(def justify-items-types
#{: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
(def grid-track-types
#{:percent :flex :auto :fixed})
@ -130,29 +86,59 @@
(def grid-cell-justify-self-types
#{:auto :start :center :end :stretch})
(sm/register!
^{::sm/type ::grid-cell}
[:map {:title "GridCell"}
[:id ::sm/uuid]
[:area-name {:optional true} :string]
[:row ::sm/safe-int]
[:row-span ::sm/safe-int]
[:column ::sm/safe-int]
[:column-span ::sm/safe-int]
[:position {:optional true} [::sm/one-of grid-position-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]]
[:shapes
[:vector {:gen/max 1} ::sm/uuid]]])
(def ^:private schema:grid-cell
[:map {:title "GridCell"}
[:id ::sm/uuid]
[:area-name {:optional true} :string]
[:row ::sm/safe-int]
[:row-span ::sm/safe-int]
[:column ::sm/safe-int]
[:column-span ::sm/safe-int]
[:position {:optional true} [::sm/one-of grid-position-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]]
[:shapes
[:vector {:gen/max 1} ::sm/uuid]]])
(sm/register!
^{::sm/type ::grid-track}
[:map {:title "GridTrack"}
[:type [::sm/one-of grid-track-types]]
[:value {:optional true} [:maybe ::sm/safe-number]]])
(def ^:private schema:grid-track
[:map {:title "GridTrack"}
[:type [::sm/one-of grid-track-types]]
[:value {:optional true} [:maybe ::sm/safe-number]]])
(def check-grid-track!
(sm/check-fn ::grid-track))
(def schema: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} 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
@ -168,7 +154,7 @@
(def item-align-self-types
#{:start :end :center :stretch})
(def schema:layout-attrs
(def schema:layout-child-attrs
[:map {:title "LayoutChildAttrs"}
[:layout-item-margin-type {:optional true} [::sm/one-of item-margin-types]]
[:layout-item-margin {:optional true}
@ -187,6 +173,9 @@
[:layout-item-absolute {:optional true} :boolean]
[:layout-item-z-index {:optional true} ::sm/safe-number]])
(def layout-child-attrs
(sm/keys schema:layout-child-attrs))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMAS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -194,8 +183,6 @@
(def valid-layouts
#{:flex :grid})
(sm/register! ::layout [::sm/one-of valid-layouts])
(defn flex-layout?
([objects id]
(flex-layout? (get objects id)))
@ -754,9 +741,7 @@
([type parent value]
(add-grid-track type parent value nil))
([type parent value index]
(dm/assert!
"expected a valid grid definition for `value`"
(check-grid-track! value))
(assert (check-grid-track value))
(let [[tracks-prop tracks-prop-other prop prop-other prop-span prop-span-other]
(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 :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
[{:command :move-to :params {:x 480.0 :y 839.0}}
{:command :line-to :params {:x 439.0 :y 802.0}}
@ -179,6 +188,42 @@
(t/is (= (vec result1) result2))
(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
"Given a content return all points.
@ -278,15 +323,6 @@
(t/is (= result2 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
(let [initial [(gpt/point 0.0 0.0)
(gpt/point 10.0 10.0)

View File

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

View File

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

View File

@ -140,7 +140,8 @@ The <code class="language-js">manifest.json</code> file contains the basic infor
"user:read",
"comment:read",
"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.
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._
### 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
```
## 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.
```bash

View File

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

View File

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

View File

@ -17,7 +17,7 @@
[promesa.core :as p]))
(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]
#js {:screen #js {:width bw/default-viewport-width
:height bw/default-viewport-height}
@ -56,7 +56,8 @@
:page-id page-id
:share-id share-id
:object-id (mapv :id objects)
:route "objects"}
:route "objects"
:skip-children skip-children}
uri (-> (cf/get :public-uri)
(assoc :path "/render.html")
(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",
"fr",
"he",
"sr",
"nb_NO",
"pl",
"pt_BR",

View File

@ -444,14 +444,16 @@
(mf/defc object-svg
{::mf/wrap [mf/memo]}
[{:keys [objects object-id embed]
[{:keys [objects object-id embed skip-children]
:or {embed false}
:as props}]
(let [object (get objects object-id)
object (cond-> 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})
vbox (format-viewbox bounds)

View File

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

View File

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

View File

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

View File

@ -170,6 +170,9 @@
(rx/filter some?)
(rx/subs!
(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))))))
(defn- import-files
@ -467,6 +470,7 @@
(when (and (= :analyze status) errors?)
[:& context-notification
{:level :warning
:class (stl/css :context-notification-error)
:content (tr "dashboard.import.import-warning")}])
(when (= :import-success status)
@ -480,12 +484,12 @@
:class (stl/css :context-notification-error)
: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 (tr "dashboard.import.import-error.message1")]
[:ul {:class (stl/css :import-error-list)}
(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)]))]
[:div (tr "dashboard.import.import-error.message2")]]

View File

@ -36,4 +36,4 @@
(on-ref node)))})]
[:> "button" props
(when icon [:> icon* {:icon-id icon :size "m"}])
[:span {:class (stl/css :label-wrapper)} children]]))
[:span {:class (stl/css :label-wrapper)} children]]))

View File

@ -6,11 +6,11 @@
(ns app.main.ui.ds.buttons.icon-button
(:require-macros
[app.common.data.macros :as dm]
[app.main.style :as stl])
(:require
[app.common.data :as d]
[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]))
(def ^:private schema:icon-button
@ -24,18 +24,30 @@
[:maybe [:enum "primary" "secondary" "ghost" "destructive" "action"]]]])
(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}]
(let [variant (or variant "primary")
tooltip-id (mf/use-id)
class (dm/str class " " (stl/css-case :icon-button true
:icon-button-primary (= variant "primary")
:icon-button-secondary (= variant "secondary")
:icon-button-ghost (= variant "ghost")
:icon-button-action (= variant "action")
:icon-button-destructive (= variant "destructive")))
props (mf/spread-props props {:class class
:aria-labelledby tooltip-id})]
[:> tooltip* {:tooltip-content aria-label
(let [variant
(d/nilv variant "primary")
tooltip-id
(mf/use-id)
button-class
(stl/css-case :icon-button true
:icon-button-primary (identical? variant "primary")
:icon-button-secondary (identical? variant "secondary")
: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}
[:> "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]
[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
trigger-left :left
trigger-right :right
trigger-bottom :bottom
trigger-width :width
trigger-height :height} trigger-rect
trigger-height :height}
origin-brect
{tooltip-width :width
tooltip-height :height} (dom/get-bounding-rect tooltip)
tooltip-height :height}
tooltip-brect
offset (d/nilv offset 2)
arrow-height 12
half-arrow-height (/ arrow-height 2)
overlay-offset 32]
offset (d/nilv offset 2)]
(case placement
"bottom"
@ -95,7 +123,10 @@
:width tooltip-width
: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
"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"]
@ -106,65 +137,93 @@
"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"]))
(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
[:map
[:class {:optional true} :string]
[:id {:optional true} :string]
[:offset {:optional true} :int]
[:delay {:optional true} :int]
[:content [:or fn? :string [:fn mf/element?]]]
[:placement {:optional true}
[:maybe [:enum "top" "bottom" "left" "right" "top-right" "bottom-right" "bottom-left" "top-left"]]]])
(mf/defc tooltip*
{::mf/schema schema:tooltip}
[{:keys [class id children tooltip-content placement offset delay] :rest props}]
(let [id (or id (mf/use-id))
placement* (mf/use-state #(d/nilv placement "top"))
placement (deref placement*)
delay (d/nilv delay 300)
[{:keys [class id children content placement offset delay] :rest props}]
(let [internal-id
(mf/use-id)
schedule-ref (mf/use-ref nil)
id
(d/nilv id internal-id)
position-tooltip
(fn [^js tooltip trigger-rect]
(let [all-placements (get-fallback-order placement)]
(when (.-isConnected tooltip)
(.showPopover ^js tooltip))
(loop [[current-placement & remaining-placements] all-placements]
(when current-placement
(reset! placement* current-placement)
(let [tooltip-rect (calculate-tooltip-rect tooltip trigger-rect current-placement offset)]
(if (dom/is-bounding-rect-outside? tooltip-rect)
(recur remaining-placements)
(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")))))))))
placement*
(mf/use-state #(d/nilv placement "top"))
placement
(deref placement*)
delay
(d/nilv delay 300)
schedule-ref
(mf/use-ref nil)
on-show
(mf/use-fn
(mf/deps id placement)
(mf/deps id placement offset)
(fn [event]
(when-let [schedule (mf/ref-val schedule-ref)]
(ts/dispose! schedule)
(mf/set-ref-val! schedule-ref nil))
(clear-schedule schedule-ref)
(when-let [tooltip (dom/get-element id)]
(let [trigger-rect (->> (dom/get-target event)
(dom/get-bounding-rect))]
(mf/set-ref-val!
schedule-ref
(ts/schedule
delay
#(position-tooltip tooltip trigger-rect)))))))
(let [origin-brect
(->> (dom/get-target event)
(dom/get-bounding-rect))
update-position
(fn []
(let [placement (update-tooltip-position tooltip placement origin-brect offset)]
(reset! placement* placement)))]
(add-schedule schedule-ref delay update-position)))))
on-hide
(mf/use-fn
(mf/deps id)
(fn [] (when-let [tooltip (dom/get-element id)]
(when-let [schedule (mf/ref-val schedule-ref)]
(ts/dispose! schedule)
(mf/set-ref-val! schedule-ref nil))
(dom/unset-css-property! tooltip "display")
(.hidePopover ^js tooltip))))
(fn []
(when-let [tooltip (dom/get-element id)]
(clear-schedule schedule-ref)
(hide-popover tooltip))))
handle-key-down
(mf/use-fn
@ -173,33 +232,38 @@
(when (kbd/esc? event)
(on-hide))))
class (d/append-class class (stl/css-case
:tooltip true
:tooltip-top (= placement "top")
:tooltip-bottom (= placement "bottom")
:tooltip-left (= placement "left")
:tooltip-right (= placement "right")
:tooltip-top-right (= placement "top-right")
:tooltip-bottom-right (= placement "bottom-right")
:tooltip-bottom-left (= placement "bottom-left")
:tooltip-top-left (= placement "top-left")))
tooltip-class
(stl/css-case
:tooltip true
:tooltip-top (identical? placement "top")
:tooltip-bottom (identical? placement "bottom")
:tooltip-left (identical? placement "left")
:tooltip-right (identical? placement "right")
:tooltip-top-right (identical? placement "top-right")
:tooltip-bottom-right (identical? placement "bottom-right")
: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
children
[:div {:class class
[:div {:class [class tooltip-class]
:id id
:popover "auto"
:role "tooltip"}
[:div {:class (stl/css :tooltip-content)}
(if (fn? tooltip-content)
(tooltip-content)
tooltip-content)]
[:div {:class (stl/css :tooltip-content)} content]
[: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"
:placement "bottom"
:tooltip-content "Tooltip content"}
:content "Tooltip content"}
[:div "Trigger component"]])
```
@ -44,8 +44,7 @@ Tooltip content can include HTML elements:
[:> tlp/tooltip* {:id "test-tooltip"
:placement "bottom"
:tooltip-content (mf/html
[:span "Tooltip content"])}
:content (mf/html [:span "Tooltip content"])}
[:div "Trigger component"]])
```

View File

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

View File

@ -37,10 +37,10 @@ export default {
},
args: {
children: (
<button popovertarget="popover-example">Hover this element</button>
<button popoverTarget="popover-example">Hover this element</button>
),
id: "popover-example",
tooltipContent: "This is the tooltip content",
content: "This is the tooltip content",
delay: 300,
},
render: ({ children, ...args }) => (
@ -74,18 +74,18 @@ export const Corners = {
>
<Tooltip
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={{
placeSelf: "start start",
width: "fit-content",
height: "fit-content",
}}
>
<button popovertarget="popover-example10">Hover here</button>
<button popoverTarget="popover-example10">Hover here</button>
</Tooltip>
<Tooltip
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={{
alignSelf: "start",
justifySelf: "center",
@ -94,7 +94,7 @@ export const Corners = {
}}
>
<button
popovertarget="popover-example2"
popoverTarget="popover-example2"
style={{
alignSelf: "start",
justifySelf: "center",
@ -106,7 +106,7 @@ export const Corners = {
</Tooltip>
<Tooltip
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={{
alignSelf: "start",
justifySelf: "end",
@ -114,11 +114,11 @@ export const Corners = {
height: "fit-content",
}}
>
<button popovertarget="popover-example3">Hover here</button>
<button popoverTarget="popover-example3">Hover here</button>
</Tooltip>
<Tooltip
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={{
alignSelf: "center",
justifySelf: "start",
@ -126,11 +126,11 @@ export const Corners = {
height: "fit-content",
}}
>
<button popovertarget="popover-example4">Hover here</button>
<button popoverTarget="popover-example4">Hover here</button>
</Tooltip>
<Tooltip
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={{
alignSelf: "center",
justifySelf: "center",
@ -138,11 +138,11 @@ export const Corners = {
height: "fit-content",
}}
>
<button popovertarget="popover-example5">Hover here</button>
<button popoverTarget="popover-example5">Hover here</button>
</Tooltip>
<Tooltip
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={{
alignSelf: "center",
justifySelf: "end",
@ -150,11 +150,11 @@ export const Corners = {
height: "fit-content",
}}
>
<button popovertarget="popover-example6">Hover here</button>
<button popoverTarget="popover-example6">Hover here</button>
</Tooltip>
<Tooltip
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={{
alignSelf: "end",
justifySelf: "start",
@ -162,11 +162,11 @@ export const Corners = {
height: "fit-content",
}}
>
<button popovertarget="popover-example7">Hover here</button>
<button popoverTarget="popover-example7">Hover here</button>
</Tooltip>
<Tooltip
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={{
alignSelf: "end",
justifySelf: "center",
@ -174,11 +174,11 @@ export const Corners = {
height: "fit-content",
}}
>
<button popovertarget="popover-example8">Hover here</button>
<button popoverTarget="popover-example8">Hover here</button>
</Tooltip>
<Tooltip
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={{
alignSelf: "end",
justifySelf: "end",
@ -186,7 +186,7 @@ export const Corners = {
height: "fit-content",
}}
>
<button popovertarget="popover-example9">Hover here</button>
<button popoverTarget="popover-example9">Hover here</button>
</Tooltip>
</div>
),

View File

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

View File

@ -275,7 +275,14 @@
[:div {:class (stl/css :permissions-list-entry)}
i/oauth-1
[: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/register modal/components

View File

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

View File

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

View File

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

View File

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

View File

@ -25,14 +25,17 @@
[app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.media :as dwm]
[app.main.data.workspace.selection :as dws]
[app.main.fonts :refer [fetch-font-css]]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.shapes.text.fontfaces :refer [shapes->fonts]]
[app.plugins.events :as events]
[app.plugins.file :as file]
[app.plugins.fonts :as fonts]
[app.plugins.format :as format]
[app.plugins.history :as history]
[app.plugins.library :as library]
[app.plugins.local-storage :as local-storage]
[app.plugins.page :as page]
[app.plugins.parser :as parser]
[app.plugins.shape :as shape]
@ -41,7 +44,8 @@
[app.plugins.viewport :as viewport]
[app.util.code-gen :as cg]
[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
@ -85,6 +89,11 @@
{:this true
:get #(.getTheme ^js %)}
:localStorage
{:this true
:get
(fn [_] (local-storage/local-storage-proxy plugin-id))}
:selection
{:this true
:get #(.getSelectedShapes ^js %)
@ -428,6 +437,24 @@
(cg/generate-style-code
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
(fn []
(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
{:type (-> (obj/get export "type") parse-keyword)
: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
[^js exports]

View File

@ -1138,6 +1138,7 @@
{:cmd :export-shapes
:profile-id (:profile-id @st/state)
:wait true
:skip-children (:skip-children value false)
:exports [{:file-id file-id
:page-id page-id
:object-id id
@ -1230,6 +1231,39 @@
(cond-> (cfh/frame-shape? data)
(-> (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"
:get
(fn [self]

View File

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

View File

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

View File

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

View File

@ -43,6 +43,7 @@
{:label "Украї́нська мо́ва (community)" :value "uk"}
{:label "Český jazyk (community)" :value "cs"}
{:label "Latviešu valoda (community)" :value "lv"}
{:label "Српски (community)" :value "sr"}
{:label "Føroyskt mál (community)" :value "fo"}
{:label "Korean (community)" :value "ko"}
{: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 "scale")) 2))
(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"}])
(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"
(set! (.-flipX shape) true)

View File

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

View File

@ -6696,6 +6696,9 @@ msgstr ""
msgid "workspace.plugins.permissions.allow-download"
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
msgid "workspace.plugins.permissions.comment-read"
msgstr "Leer tus comentarios y respuestas."