Compare commits

...

49 Commits

Author SHA1 Message Date
Eva Marco
4a9fef12eb 🐛 Fix url params on invitation link (#6638) 2025-06-05 12:28:53 +02:00
andrés gonzález
e1adbdfd9f 📚 Update changelog (#6616) 2025-06-04 15:12:33 +02:00
andrés gonzález
1120c7c7fb 🐛 Fix copy in error message (#6615) 2025-06-03 12:28:49 +02:00
Andrey Antukh
2244bf6aa7 Merge remote-tracking branch 'origin/main' into staging 2025-06-03 10:43:39 +02:00
Andrey Antukh
f4ef4a705c Merge tag '2.7.2-RC1' 2025-06-03 10:43:14 +02:00
Alejandro Alonso
fe8d9fdd76 Merge pull request #6614 from penpot/niwinz-staging-backport-1
 Make the hash optional on binfile-v3
2025-06-03 08:13:42 +02:00
Andrey Antukh
3131eec271 Make the hash optional on binfile-v3
Backport the change from develop
2025-06-02 23:24:35 +02:00
Andrey Antukh
27ab910a64 📚 Update changelog 2025-06-02 12:36:47 +02:00
Andrey Antukh
2398c1fc2b Merge pull request #6604 from penpot/alotor-fix-sandbox-runtime
🐛 Add sandbox runtime
2025-06-02 12:30:51 +02:00
alonso.torres
a3e17047a4 🐛 Add sandbox runtime 2025-05-30 15:40:36 +02:00
andrés gonzález
2943f80db5 📚 Change help links at the Help Center (#6582) 2025-05-28 13:22:42 +02:00
Andrés Moya
050ffa235c ⬆️ Update cuerdas library (#6556) 2025-05-26 13:22:30 +02:00
Andrey Antukh
fdd6502671 📚 Update changelog 2025-05-26 12:41:34 +02:00
Alejandro Alonso
ac0b74e11a Merge pull request #6549 from penpot/niwinz-staging-hotfix-1
🐛 Fix incorrect relink operation for stroke image
2025-05-26 09:50:52 +02:00
Andrey Antukh
b5d96d312a 🐛 Fix incorrect relink operation for stroke image 2025-05-24 09:16:10 +02:00
Eva Marco
99fb905070 🐛 Fix at icon (#6540) 2025-05-22 13:15:09 +02:00
María Valderrama
faa68784af 💄 Add styles for external widgets on workspace (#6509)
* 💄 Add styles for Inkeep Chat at workspace

* 📎 Styles review
2025-05-22 11:56:45 +02:00
Alejandro Alonso
0748ef7267 Merge pull request #6518 from penpot/niwinz-staging-tokenslib-json-encoding
🐛 Add json encoding for tokenslib type
2025-05-21 10:17:37 +02:00
Andrey Antukh
9ca4fa752c 🐛 Add json encoding for tokenslib type 2025-05-21 09:59:28 +02:00
Alejandro Alonso
ff9c8f5929 Merge pull request #6483 from penpot/niwinz-staging-bugfixes-error-report
🐛 Several bugfixes
2025-05-20 13:54:54 +02:00
Alejandro Alonso
e4c563f917 Merge pull request #6479 from penpot/niwinz-develop-json-encoding-fix
🐛 Fix exception on rendering openapi.json
2025-05-20 13:46:16 +02:00
Andrey Antukh
2d3ad5a88f 📎 Update changelog 2025-05-20 13:30:04 +02:00
Andrey Antukh
1334d733cd 🐛 Fix openapi json generation for :re schemas 2025-05-20 13:29:44 +02:00
Andrey Antukh
004a9f17d3 Add minor js-like type schema formatting improvements 2025-05-20 13:29:44 +02:00
Andrey Antukh
c87fa4f723 Make the rpc doc generation lazy 2025-05-20 13:29:44 +02:00
Andrey Antukh
9378a5786f Replace json library used for generate openapi json 2025-05-20 13:29:44 +02:00
Andrey Antukh
3224ba26f1 ♻️ Replace :any schema with own ::sm/any
That a more specific, json friendly generator
2025-05-20 13:29:44 +02:00
Andrey Antukh
d33a5e6df1 Backport from develop partial improvements to sm/register! helper 2025-05-20 13:29:44 +02:00
Yamila Moreno
b6be416c7b 📎 Add wasm envvar to manage script 2025-05-20 12:15:14 +02:00
Aitor Moreno
aaa57cb17f 🐛 Fix inline styles in code tab (#6428) 2025-05-20 10:05:35 +02:00
Andrey Antukh
0b289153cb Add the ability to disable wasm on build script 2025-05-18 17:30:41 +02:00
Andrey Antukh
cf274099c4 Improve events/sse internal API
For make code cleaner and more evident for a quick view
2025-05-18 17:30:41 +02:00
Andrey Antukh
6524e75770 💄 Fix check-fn naming on types.container 2025-05-18 17:30:41 +02:00
Andrey Antukh
9b80f7c9b3 💄 Don't return unnecesary object from db query
the return value is already ignored
2025-05-18 17:30:41 +02:00
Andrey Antukh
bf76f328c8 Remove duplicate error logging on sse response 2025-05-18 17:30:41 +02:00
andrés gonzález
ddfd55261d :Books: Update design tokens doc (#6487) 2025-05-15 14:44:51 +02:00
Alonso Torres
ba25ce3098 🐛 Fix share button being displayed with no permissions (#6476)
* 🐛 Fix share button being displayed with no permissions

*  Simplify impl by accessing perms from teams directly

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2025-05-15 09:49:29 +02:00
Alejandro Alonso
f5f1316f0b Merge pull request #6474 from penpot/superalex-develop-paste-html-fix
🐛 Fix exception on paste invalid html
2025-05-14 16:16:53 +02:00
Andrey Antukh
79a164be6d 🐛 Fix exception on paste invalid html 2025-05-14 16:07:01 +02:00
alonso.torres
ecb85778bc 🐛 Fix problem with path edition of shapes 2025-05-14 14:45:29 +02:00
Andrés Moya
5b8d1c1ca6 Merge branch 'hiru-update-tech-guide' 2025-05-14 13:23:38 +02:00
Andrés Moya
24e2948407 📚 Update code samples 2025-05-14 13:22:49 +02:00
Andrés Moya
c569c71306 📚 Update Tech Guide about abstraction levels 2025-05-14 13:22:38 +02:00
Andrés Moya
2cdc241e68 Merge branch 'hiru-update-tech-guide' into staging 2025-05-14 12:00:03 +02:00
Andrés Moya
057bf9bf25 📚 Update code samples 2025-05-14 11:38:55 +02:00
Andrés Moya
2ddcd0ce15 📚 Update Tech Guide about abstraction levels 2025-05-14 11:37:28 +02:00
Alonso Torres
ca2891d441 🐛 Fix problem syncing library colors and typographies (#6467) 2025-05-13 13:28:16 +02:00
Elenzakaleidos
37abb7b237 💄 Update video in readme page (#6461)
Signed-off-by: Elenzakaleidos <elena.scilinguo@kaleidos.net>
2025-05-13 08:10:19 +02:00
Ramiro Andres Sanchez Balo
5fc2208c16 📚 Improve metadata descriptions (#6457) 2025-05-13 08:09:59 +02:00
103 changed files with 775 additions and 448 deletions

View File

@@ -1,6 +1,25 @@
# CHANGELOG
## 2.7.0 (Unreleased)
## 2.7.2 (Unreleased)
### :bug: Bugs fixed
- Update plugins runtime [Github #6604](https://github.com/penpot/penpot/pull/6604)
- Backport from develop a minor fix that enables import of files
generated by penpot library [Github #6614](https://github.com/penpot/penpot/pull/6614)
- Fix copy in error message [GitHub #6615](https://github.com/penpot/penpot/pull/6615)
- Fix url on invitation link [Taiga #11284](https://tree.taiga.io/project/penpot/issue/11284)
## 2.7.1
### :bug: Bugs fixed
- Fix incorrect handling of strokes with images on importing files
- Fix tokens disappearing after manual additions [Taiga #11063](https://tree.taiga.io/project/penpot/issue/11063)
## 2.7.0
### :rocket: Epics and highlights
@@ -21,6 +40,7 @@
### :bug: Bugs fixed
- Fix "at" icon to match all icons on app [Taiga #11136](https://tree.taiga.io/project/penpot/issue/11136)
- Fix problem in viewer with the back button [Taiga #10907](https://tree.taiga.io/project/penpot/issue/10907)
- Fix resize bar background on tokens panel [Taiga #10811](https://tree.taiga.io/project/penpot/issue/10811)
- Fix shortcut for history version panel [Taiga #11006](https://tree.taiga.io/project/penpot/issue/11006)
@@ -47,6 +67,13 @@
- Fix cannot rename Design Token Sets when group of same name exists [Taiga Issue #10773](https://tree.taiga.io/project/penpot/issue/10773)
- Fix problem when duplicating grid layout [Github #6391](https://github.com/penpot/penpot/issues/6391)
- Fix issue that makes workspace shortcuts stop working [Taiga #11062](https://tree.taiga.io/project/penpot/issue/11062)
- Fix problem while syncing library colors and typographies [Taiga #11068](https://tree.taiga.io/project/penpot/issue/11068)
- Fix problem with path edition of shapes [Taiga #9496](https://tree.taiga.io/project/penpot/issue/9496)
- Fix exception on paste invalid html [Taiga #11047](https://tree.taiga.io/project/penpot/issue/11047)
- Fix share button being displayed with no permissions [Taiga #11086](https://tree.taiga.io/project/penpot/issue/11086)
- Fix inline styles in code tab [Taiga Issue #7583](https://tree.taiga.io/project/penpot/issue/7583)
- Fix exception on returning openapi.json
- Fix json encoding of TokensLib [Taiga #10994](https://tree.taiga.io/project/penpot/issue/10994)
## 2.6.2

View File

@@ -34,7 +34,7 @@
<br />
[Penpot video](https://github.com/penpot/penpot/assets/5446186/b8ad0764-585e-4ddc-b098-9b4090d337cc)
[Penpot video](https://github.com/user-attachments/assets/08b83119-c090-4a74-86ed-7bfbdda9a793)
<br />

View File

@@ -13,7 +13,7 @@ This will automatically include {{requested-by|abbreviate:25}} in the team, so t
Click the link below to provide team access:
{{ public-uri }}/#/dashboard/members?team-id{{team-id}}&invite-email={{requested-by-email|urlescape}}
{{ public-uri }}/#/dashboard/members?team-id={{team-id}}&invite-email={{requested-by-email|urlescape}}

View File

@@ -73,7 +73,7 @@
[:size ::sm/int]
[:content-type :string]
[:bucket [::sm/one-of {:format :string} sto/valid-buckets]]
[:hash :string]])
[:hash {:optional true} :string]])
(def ^:private schema:file-thumbnail
[:map {:title "FileThumbnail"}
@@ -821,13 +821,14 @@
:expected-size (:size object)
:found-size (sto/get-size content)))
(when (not= (:hash object) (sto/get-hash content))
(ex/raise :type :validation
:code :inconsistent-penpot-file
:hint "found corrupted storage object: hash does not match"
:path path
:expected-hash (:hash object)
:found-hash (sto/get-hash content)))
(when-let [hash (get object :hash)]
(when (not= hash (sto/get-hash content))
(ex/raise :type :validation
:code :inconsistent-penpot-file
:hint "found corrupted storage object: hash does not match"
:path path
:expected-hash (:hash object)
:found-hash (sto/get-hash content))))
(let [params (-> object
(dissoc :id :size)

View File

@@ -9,7 +9,6 @@
(:refer-clojure :exclude [tap])
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.transit :as t]
[app.http.errors :as errors]
@@ -54,18 +53,20 @@
::yres/status 200
::yres/body (yres/stream-body
(fn [_ output]
(binding [events/*channel* (sp/chan :buf buf :xf (keep encode))]
(let [listener (events/start-listener
(partial write! output)
(partial pu/close! output))]
(try
(let [channel (sp/chan :buf buf :xf (keep encode))
listener (events/start-listener
channel
(partial write! output)
(partial pu/close! output))]
(try
(binding [events/*channel* channel]
(let [result (handler)]
(events/tap :end result))
(catch Throwable cause
(events/tap :error (errors/handle' cause request))
(when-not (ex/instance? java.io.EOFException cause)
(binding [l/*context* (errors/request->context request)]
(l/err :hint "unexpected error on processing sse response" :cause cause))))
(finally
(sp/close! events/*channel*)
(px/await! listener)))))))}))
(events/tap :end result)))
(catch Throwable cause
(let [result (errors/handle' cause request)]
(events/tap channel :error result)))
(finally
(sp/close! channel)
(px/await! listener))))))}))

View File

@@ -92,9 +92,9 @@
[:string {:max 250}]
[::sm/one-of {:format "string"} valid-event-types]]]
[:props
[:map-of :keyword :any]]
[:map-of :keyword ::sm/any]]
[:context {:optional true}
[:map-of :keyword :any]]])
[:map-of :keyword ::sm/any]]])
(def schema:push-audit-events
[:map {:title "push-audit-events"}

View File

@@ -115,7 +115,8 @@
(db/update! pool :project
{:modified-at (dt/now)}
{:id project-id})
{:id project-id}
{::db/return-keys false})
result))

View File

@@ -189,7 +189,7 @@
[:is-shared ::sm/boolean]
[:project-id ::sm/uuid]
[:created-at ::dt/instant]
[:data {:optional true} :any]])
[:data {:optional true} ::sm/any]])
(def schema:permissions-mixin
[:map {:title "PermissionsMixin"}

View File

@@ -80,9 +80,9 @@
(def ^:private schema:create-font-variant
[:map {:title "create-font-variant"}
[:team-id ::sm/uuid]
[:data [:map-of :string :any]]
[:data [:map-of ::sm/text ::sm/any]]
[:font-id ::sm/uuid]
[:font-family :string]
[:font-family ::sm/text]
[:font-weight [::sm/one-of {:format "number"} valid-weight]]
[:font-style [::sm/one-of {:format "string"} valid-style]]])

View File

@@ -9,6 +9,7 @@
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.json :as json]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.schema.desc-js-like :as smdj]
@@ -19,7 +20,6 @@
[app.http.sse :as-alias sse]
[app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc]
[app.util.json :as json]
[app.util.services :as sv]
[app.util.template :as tmpl]
[clojure.java.io :as io]
@@ -86,7 +86,7 @@
(fn [request]
(let [params (:query-params request)
pstyle (:type params "js")
context (assoc context :param-style pstyle)]
context (assoc @context :param-style pstyle)]
{::yres/status 200
::yres/body (-> (io/resource "app/templates/api-doc.tmpl")
@@ -178,8 +178,7 @@
(fn [_]
{::yres/status 200
::yres/headers {"content-type" "application/json; charset=utf-8"}
::yres/body (json/encode context)})
::yres/body (json/encode @context)})
(fn [_]
{::yres/status 404})))
@@ -209,7 +208,7 @@
(defmethod ig/init-key ::routes
[_ {:keys [::rpc/methods] :as cfg}]
[(let [context (prepare-doc-context methods)]
[(let [context (delay (prepare-doc-context methods))]
[["/_doc"
{:handler (doc-handler context)
:allowed-methods #{:get}}]
@@ -217,7 +216,7 @@
{:handler (doc-handler context)
:allowed-methods #{:get}}]])
(let [context (prepare-openapi-context methods)]
(let [context (delay (prepare-openapi-context methods))]
[["/openapi"
{:handler (openapi-handler)
:allowed-methods #{:get}}]

View File

@@ -10,7 +10,6 @@
to them. Mainly used in http.sse for progress reporting."
(:refer-clojure :exclude [tap run!])
(:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[promesa.exec :as px]
@@ -18,33 +17,30 @@
(def ^:dynamic *channel* nil)
(defn channel
[]
(sp/chan :buf 32))
(defn tap
[type data]
(when-let [channel *channel*]
(sp/put! channel [type data])
nil))
([type data]
(when-let [channel *channel*]
(sp/put! channel [type data])
nil))
([channel type data]
(when channel
(sp/put! channel [type data])
nil)))
(defn start-listener
[on-event on-close]
(dm/assert!
"expected active events channel"
(sp/chan? *channel*))
[channel on-event on-close]
(assert (sp/chan? channel) "expected active events channel")
(px/thread
{:virtual true}
(try
(loop []
(when-let [event (sp/take! *channel*)]
(when-let [event (sp/take! channel)]
(let [result (ex/try! (on-event event))]
(if (ex/exception? result)
(do
(l/wrn :hint "unexpected exception" :cause result)
(sp/close! *channel*))
(sp/close! channel))
(recur)))))
(finally
(on-close)))))
@@ -55,7 +51,7 @@
[f on-event]
(binding [*channel* (sp/chan :buf 32)]
(let [listener (start-listener on-event (constantly nil))]
(let [listener (start-listener *channel* on-event (constantly nil))]
(try
(f)
(finally

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 "2023.11.09-407"}
funcool/cuerdas {:mvn/version "2025.05.26-411"}
funcool/promesa
{:git/sha "0c5ed6ad033515a2df4b55addea044f60e9653d0"
:git/url "https://github.com/funcool/promesa"}

View File

@@ -47,14 +47,14 @@
[:type [:= :assign]]
;; NOTE: the full decoding is happening on the handler because it
;; needs a proper context of the current shape and its type
[:value [:map-of :keyword :any]]
[:value [:map-of :keyword ::sm/any]]
[:ignore-touched {:optional true} :boolean]
[:ignore-geometry {:optional true} :boolean]]]
[:set
[:map {:title "SetOperation"}
[:type [:= :set]]
[:attr :keyword]
[:val :any]
[:val ::sm/any]
[:ignore-touched {:optional true} :boolean]
[:ignore-geometry {:optional true} :boolean]]]
[:set-touched
@@ -238,9 +238,9 @@
[:component-id {:optional true} ::sm/uuid]
[:ignore-touched {:optional true} :boolean]
[:parent-id ::sm/uuid]
[:shapes :any]
[:shapes ::sm/any]
[:index {:optional true} [:maybe :int]]
[:after-shape {:optional true} :any]
[:after-shape {:optional true} ::sm/any]
[:component-swap {:optional true} :boolean]]]
[:reorder-children
@@ -250,14 +250,14 @@
[:component-id {:optional true} ::sm/uuid]
[:ignore-touched {:optional true} :boolean]
[:parent-id ::sm/uuid]
[:shapes :any]]]
[:shapes ::sm/any]]]
[:add-page
[:map {:title "AddPageChange"}
[:type [:= :add-page]]
[:id {:optional true} ::sm/uuid]
[:name {:optional true} :string]
[:page {:optional true} :any]]]
[:page {:optional true} ::sm/any]]]
[:mod-page
[:map {:title "ModPageChange"}
@@ -327,14 +327,14 @@
[:type [:= :add-component]]
[:id ::sm/uuid]
[:name :string]
[:shapes {:optional true} [:vector {:gen/max 3} :any]]
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
[:path {:optional true} :string]]]
[:mod-component
[:map {:title "ModCompoenentChange"}
[:type [:= :mod-component]]
[:id ::sm/uuid]
[:shapes {:optional true} [:vector {:gen/max 3} :any]]
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
[:name {:optional true} :string]
[:variant-id {:optional true} ::sm/uuid]
[:variant-properties {:optional true} [:vector ::ctv/variant-property]]]]
@@ -411,7 +411,7 @@
[:set-tokens-lib
[:map {:title "SetTokensLib"}
[:type [:= :set-tokens-lib]]
[:tokens-lib :any]]]
[:tokens-lib ::sm/any]]]
[:set-token-set
[:map {:title "SetTokenSetChange"}

View File

@@ -25,18 +25,19 @@
;; Auxiliary functions to help create a set of changes (undo + redo)
(sm/register!
^{::sm/type ::changes}
[:map {:title "changes"}
[:redo-changes vector?]
[:undo-changes seq?]
[:origin {:optional true} any?]
[:save-undo? {:optional true} boolean?]
[:stack-undo? {:optional true} boolean?]
[:undo-group {:optional true} any?]])
(def schema:changes
(sm/register!
^{::sm/type ::changes}
[:map {:title "changes"}
[:redo-changes vector?]
[:undo-changes seq?]
[:origin {:optional true} ::sm/any]
[:save-undo? {:optional true} boolean?]
[:stack-undo? {:optional true} boolean?]
[:undo-group {:optional true} ::sm/any]]))
(def check-changes!
(sm/check-fn ::changes))
(sm/check-fn schema:changes))
(defn empty-changes
([origin page-id]

View File

@@ -626,6 +626,9 @@
(map? (:fill-image form))
(update-in [:fill-image :id] lookup-index)
(map? (:stroke-image form))
(update-in [:stroke-image :id] lookup-index)
;; This covers old shapes and the new :fills.
(uuid? (:fill-color-ref-file form))
(update :fill-color-ref-file lookup-index)

View File

@@ -584,7 +584,7 @@
(generate-sync-shape-direct changes file libraries container shape-id false)))
(defmethod generate-sync-shape :colors
[_ changes library-id _ shape _ libraries _]
[_ changes library-id _ shape libraries _]
(shape-log :debug (:id shape) nil :msg "Sync colors of shape" :shape (:name shape))
;; Synchronize a shape that uses some colors of the library. The value of the
@@ -595,7 +595,7 @@
#(ctc/sync-shape-colors % library-id library-colors))))
(defmethod generate-sync-shape :typographies
[_ changes library-id container shape _ libraries _]
[_ changes library-id container shape libraries _]
(shape-log :debug (:id shape) nil :msg "Sync typographies of shape" :shape (:name shape))
;; Synchronize a shape that uses some typographies of the library. The attributes

View File

@@ -214,8 +214,7 @@
(defn lazy-validator
[s]
(let [s (schema s)
vfn (delay (validator s))]
(let [vfn (delay (validator s))]
(fn [v] (@vfn v))))
(defn lazy-explainer
@@ -318,11 +317,14 @@
([params]
(cond
(map? params)
(let [type (get params :type)]
(let [mdata (meta params)
type (or (get mdata ::id)
(get mdata ::type)
(get params :type))]
(assert (qualified-keyword? type) "expected qualified keyword for `type`")
(let [s (m/-simple-schema params)]
(swap! sr/registry assoc type s)
nil))
s))
(vector? params)
(let [mdata (meta params)
@@ -330,11 +332,12 @@
(get mdata ::type))]
(assert (qualified-keyword? type) "expected qualified keyword to be on metadata")
(swap! sr/registry assoc type params)
nil)
params)
(m/into-schema? params)
(let [type (m/-type params)]
(swap! sr/registry assoc type params))
(swap! sr/registry assoc type params)
params)
:else
(throw (ex-info "Invalid Arguments" {}))))
@@ -1042,6 +1045,8 @@
{:title "agent"
:description "instance of clojure agent"}}))
(register! ::any (mu/update-properties :any assoc :gen/gen sg/any))
;; ---- PREDICATES
(def valid-safe-number?

View File

@@ -7,6 +7,7 @@
(ns app.common.schema.desc-js-like
(:require
[app.common.data :as d]
[app.common.schema :as-alias sm]
[cuerdas.core :as str]
[malli.core :as m]
[malli.util :as mu]))
@@ -90,7 +91,7 @@
(defmethod visit :int [_ schema _ _] (str "integer" (-titled schema) (-min-max-suffix-number schema)))
(defmethod visit :double [_ schema _ _] (str "double" (-titled schema) (-min-max-suffix-number schema)))
(defmethod visit :select-keys [_ schema _ options] (describe* (m/deref schema) options))
(defmethod visit :and [_ s children _] (str (str/join ", and " children) (-titled s)))
(defmethod visit :and [_ s children _] (str (str/join " && " children) (-titled s)))
(defmethod visit :enum [_ s children _options] (str "enum" (-titled s) " of " (str/join ", " children)))
(defmethod visit :maybe [_ _ children _] (str (first children) " nullable"))
(defmethod visit :tuple [_ _ children _] (str "(" (str/join ", " children) ")"))
@@ -106,7 +107,8 @@
(defmethod visit :qualified-symbol [_ _ _ _] "qualified symbol")
(defmethod visit :uuid [_ _ _ _] "uuid")
(defmethod visit :boolean [_ _ _ _] "boolean")
(defmethod visit :keyword [_ _ _ _] "keyword")
(defmethod visit :keyword [_ _ _ _] "string")
(defmethod visit :fn [_ _ _ _] "FN")
(defmethod visit :vector [_ _ children _]
(str "[" (last children) "]"))
@@ -123,10 +125,12 @@
(defmethod visit :repeat [_ schema children _]
(str "repeat " (-diamond (first children)) (-repeat-suffix schema)))
(defmethod visit :set [_ schema children _]
(str "set[" (first children) "]" (minmax-suffix schema)))
(defmethod visit ::sm/set [_ schema children _]
(str "set[" (first children) "]" (minmax-suffix schema)))
(defmethod visit ::m/val [_ schema children _]
(let [suffix (minmax-suffix schema)]
(cond-> (first children)
@@ -152,7 +156,6 @@
(or (:title props)
"*")))
(defmethod visit :map
[_ schema children {:keys [::level ::max-level] :as options}]
(let [props (m/properties schema)
@@ -172,13 +175,11 @@
": " s)))
(str/join ",\n"))
header (cond-> (if (zero? level)
(str "type " title)
(str title))
header (cond-> (str "type " title)
closed? (str "!")
(some? title) (str " "))]
(str header "{\n" entries "\n" (pad "}" level))))))
(str (pad header level) "{\n" entries "\n" (pad "}\n" level))))))
(defmethod visit :multi
[_ s children {:keys [::level ::max-level] :as options}]
@@ -205,18 +206,18 @@
(defmethod visit :merge
[_ schema children _]
(let [entries (str/join " , " children)
(let [entries (str/join ",\n" children)
props (m/properties schema)
title (or (some-> (:title props) str/camel str/capital)
"<untitled>")]
(str "merge object " title " { " entries " }")))
(str "merge type " title " { \n" entries "\n}\n")))
(defmethod visit :app.common.schema/one-of
[_ _ children _]
(defmethod visit ::sm/one-of
[_ _ children _]
(let [elems (last children)]
(str "OneOf[" (->> elems
(map d/name)
(str/join ",")) "]")))
(str "string oneOf (" (->> elems
(map d/name)
(str/join "|")) ")")))
(defmethod visit :schema [_ schema children options]
(visit ::m/schema schema children options))

View File

@@ -5,7 +5,7 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.schema.generators
(:refer-clojure :exclude [set subseq uuid filter map let boolean])
(:refer-clojure :exclude [set subseq uuid filter map let boolean vector keyword int double])
#?(:cljs (:require-macros [app.common.schema.generators]))
(:require
[app.common.schema.registry :as sr]
@@ -38,10 +38,6 @@
([s opts]
(mg/generator s (assoc opts :registry sr/default-registry))))
(defn filter
[pred gen]
(tg/such-that pred gen 100))
(defn small-double
[& {:keys [min max] :or {min -100 max 100}}]
(tg/double* {:min min, :max max, :infinite? false, :NaN? false}))
@@ -61,7 +57,7 @@
(defn word-keyword
[]
(->> (word-string)
(tg/fmap keyword)))
(tg/fmap c/keyword)))
(defn email
[]
@@ -100,12 +96,11 @@
(c/map second))
(c/map list bools elements)))))))
(def any tg/any)
(def boolean tg/boolean)
(defn set
[g]
(tg/set g))
(defn map-of
([kg vg]
(tg/map kg vg {:min-elements 1 :max-elements 3}))
([kg vg opts]
(tg/map kg vg opts)))
(defn elements
[s]
@@ -119,6 +114,10 @@
[f g]
(tg/fmap f g))
(defn filter
[pred gen]
(tg/such-that pred gen 100))
(defn mcat
[f g]
(tg/bind g f))
@@ -126,3 +125,22 @@
(defn tuple
[& opts]
(apply tg/tuple opts))
(defn vector
[& opts]
(apply tg/vector opts))
(defn set
[g]
(tg/set g))
;; Static Generators
(def boolean tg/boolean)
(def text (word-string))
(def double (small-double))
(def int (small-int))
(def keyword (word-keyword))
(def any
(tg/one-of [text boolean double int keyword]))

View File

@@ -97,7 +97,8 @@
(defmethod visit :enum [_ _ children options] (merge (some-> (m/-infer children) (transform* options)) {:enum children}))
(defmethod visit :maybe [_ _ children _] {:oneOf (conj children {:type "null"})})
(defmethod visit :tuple [_ _ children _] {:type "array", :items children, :additionalItems false})
(defmethod visit :re [_ schema _ options] {:type "string", :pattern (first (m/children schema options))})
(defmethod visit :re [_ schema _ options]
{:type "string", :pattern (str (first (m/children schema options)))})
(defmethod visit :nil [_ _ _ _] {:type "null"})
(defmethod visit :string [_ schema _ _]

View File

@@ -35,7 +35,7 @@
(.. r (toString 16) (padStart 2 "0"))
(.. g (toString 16) (padStart 2 "0"))
(.. b (toString 16) (padStart 2 "0"))))))
sg/any))
sg/int))
(defn rgb-color-string?
[o]

View File

@@ -41,7 +41,7 @@
[:map-of {:gen/max 10} ::sm/uuid :map]]
[:plugin-data {:optional true} ::ctpg/plugin-data]])
(def check-container!
(def check-container
(sm/check-fn ::container))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -74,13 +74,9 @@
(defn get-shape
[container shape-id]
(dm/assert!
"expected valid container"
(check-container! container))
(dm/assert!
"expected valid uuid for `shape-id`"
(uuid? shape-id))
(assert (check-container container))
(assert (uuid? shape-id)
"expected valid uuid for `shape-id`")
(-> container
(get :objects)

View File

@@ -16,54 +16,56 @@
(def node-types #{"root" "paragraph-set" "paragraph"})
(sm/register!
^{::sm/type ::content}
[:map
[:type [:= "root"]]
[:key {:optional true} :string]
[:children
{:optional true}
[:maybe
[:vector {:min 1 :gen/max 2 :gen/min 1}
[:map
[:type [:= "paragraph-set"]]
[:key {:optional true} :string]
[:children
[:vector {:min 1 :gen/max 2 :gen/min 1}
[:map
[:type [:= "paragraph"]]
[:key {:optional true} :string]
[:fills {:optional true}
[:maybe
[:vector {:gen/max 2} ::shape/fill]]]
[:font-family {:optional true} :string]
[:font-size {:optional true} :string]
[:font-style {:optional true} :string]
[:font-weight {:optional true} :string]
[:direction {:optional true} :string]
[:text-decoration {:optional true} :string]
[:text-transform {:optional true} :string]
[:typography-ref-id {:optional true} [:maybe ::sm/uuid]]
[:typography-ref-file {:optional true} [:maybe ::sm/uuid]]
[:children
[:vector {:min 1 :gen/max 2 :gen/min 1}
[:map
[:text :string]
[:key {:optional true} :string]
[:fills {:optional true}
[:maybe
[:vector {:gen/max 2} ::shape/fill]]]
[:font-family {:optional true} :string]
[:font-size {:optional true} :string]
[:font-style {:optional true} :string]
[:font-weight {:optional true} :string]
[:direction {:optional true} :string]
[:text-decoration {:optional true} :string]
[:text-transform {:optional true} :string]
[:typography-ref-id {:optional true} [:maybe ::sm/uuid]]
[:typography-ref-file {:optional true} [:maybe ::sm/uuid]]]]]]]]]]]]])
(def schema:content
[:map
[:type [:= "root"]]
[:key {:optional true} :string]
[:children
{:optional true}
[:maybe
[:vector {:min 1 :gen/max 2 :gen/min 1}
[:map
[:type [:= "paragraph-set"]]
[:key {:optional true} :string]
[:children
[:vector {:min 1 :gen/max 2 :gen/min 1}
[:map
[:type [:= "paragraph"]]
[:key {:optional true} :string]
[:fills {:optional true}
[:maybe
[:vector {:gen/max 2} ::shape/fill]]]
[:font-family {:optional true} :string]
[:font-size {:optional true} :string]
[:font-style {:optional true} :string]
[:font-weight {:optional true} :string]
[:direction {:optional true} :string]
[:text-decoration {:optional true} :string]
[:text-transform {:optional true} :string]
[:typography-ref-id {:optional true} [:maybe ::sm/uuid]]
[:typography-ref-file {:optional true} [:maybe ::sm/uuid]]
[:children
[:vector {:min 1 :gen/max 2 :gen/min 1}
[:map
[:text :string]
[:key {:optional true} :string]
[:fills {:optional true}
[:maybe
[:vector {:gen/max 2} ::shape/fill]]]
[:font-family {:optional true} :string]
[:font-size {:optional true} :string]
[:font-style {:optional true} :string]
[:font-weight {:optional true} :string]
[:direction {:optional true} :string]
[:text-decoration {:optional true} :string]
[:text-transform {:optional true} :string]
[:typography-ref-id {:optional true} [:maybe ::sm/uuid]]
[:typography-ref-file {:optional true} [:maybe ::sm/uuid]]]]]]]]]]]]])
(sm/register! ::content schema:content)
(def valid-content?
(sm/lazy-validator schema:content))
(sm/register!
^{::sm/type ::position-data}

View File

@@ -7,6 +7,7 @@
(ns app.common.types.tokens-lib
(:require
#?(:clj [app.common.fressian :as fres])
#?(:clj [clojure.data.json :as json])
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
@@ -118,7 +119,7 @@
[:map {:title "Token"}
[:name cto/token-name-ref]
[:type [::sm/one-of cto/token-types]]
[:value :any]
[:value ::sm/any]
[:description {:optional true} :string]
[:modified-at {:optional true} ::sm/inst]])
@@ -389,7 +390,8 @@
[:description {:optional true} :string]
[:modified-at {:optional true} ::sm/inst]
[:tokens {:optional true
:gen/gen (->> (sg/generator [:map-of ::sm/text schema:token])
:gen/gen (->> (sg/map-of (sg/generator ::sm/text)
(sg/generator schema:token))
(sg/fmap #(into (d/ordered-map) %)))}
[:and
[:map-of {:gen/max 5
@@ -910,6 +912,12 @@ Will return a value that matches this schema:
"themes" (clj->js themes)
"active-themes" (clj->js active-themes)))])
#?@(:clj
[json/JSONWriter
(-write [this writter options] (json/-write (encode-dtcg this) writter options))])
ITokenSets
(add-set [_ token-set]
(let [path (get-token-set-prefixed-path token-set)

View File

@@ -3,7 +3,6 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{ description or metadata.description }}">
<link rel="stylesheet" href="{{ '/css/index.css' | url }}">
<link rel="stylesheet" href="{{ '/css/prism.css' | url }}">
<link rel="shortcut icon" href="/img/favicon.png">
@@ -15,7 +14,7 @@
<link href="https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
{% metagen
title=title or metadata.title,
desc=desc or metadata.desc,
desc=desc or metadata.desc or description or metadata.description,
url="https://help.penpot.app" + page.url,
img="https://help.penpot.app/img/th-help-center.jpg",
img_alt=alt,
@@ -73,7 +72,7 @@
<div class="pre-footer">
<a href="https://github.com/penpot/penpot/blob/main/docs/{{ page.inputPath }}">Edit this page on GitHub</a>
&nbsp;or ask a&nbsp;
<a href="https://github.com/penpot/penpot/issues/new/choose">question</a>.
<a href="https://penpot.app/talk-to-us" target="_blank">question</a>.
</div>
<footer class="footer">
<div class="footer-inside">

View File

@@ -1,5 +1,6 @@
---
title: 04· Code of Conduct
desc: Learn about contributing to the Penpot project! This page outlines the Code of Conduct, reporting bugs, translations, core code contributions, & more.
---
<h1 id="coc">Code of conduct</h1>

View File

@@ -1,5 +1,6 @@
---
title: 03· Core code contributions
desc: Learn how to contribute to Penpot's open-source design collaboration platform. Find guidelines for bug reporting, code contributions & more.
---
<h1 id="code-contributions">Core code contributions</h1>

View File

@@ -1,5 +1,6 @@
---
title: Contributing
desc: Learn how to contribute to Penpot, the open-source design collaboration platform! Find guides on bug reporting, translations, code contributions, and more.
eleventyNavigation:
key: Contributing
order: 3

View File

@@ -1,5 +1,6 @@
---
title: 05· Libraries & Templates
desc: Contribute to Penpot's libraries & templates! Learn how to share your files and access resources. Try Penpot - It's free!
---
<h1 id="libraries">Libraries & templates</h1>

View File

@@ -1,5 +1,6 @@
---
title: 01· Reporting bugs
desc: Learn how to contribute to Penpot, the open-source design and prototyping platform! Find guidelines for reporting bugs, translations, & code contributions.
---
<h1 id="reporting-bugs">Reporting bugs</h1>

View File

@@ -1,5 +1,6 @@
---
title: 02· Translations
desc: Contribute to Penpot! Learn how to translate Penpot into your language using Weblate. Add new translations, languages, or edit existing ones today.
---
<h1 id="translations">Translations</h1>

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -1,5 +1,6 @@
---
title: Help center
desc: Find user guides, technical documentation, plugin info, FAQs, and contributing guidelines in Penpot's help center. Join the open-source community!
layout: layouts/home.njk
twitter: "@penpotapp"
image: img/placeholder.png
@@ -56,5 +57,5 @@ eleventyNavigation:
<div class="contact-block">
<h2>Contact us</h2>
<p>Write us at <a href="mailto:support@penpot.app" target="_blank">support@penpot.app</a> or join our <a href="https://community.penpot.app/" target="_blank">community</a>.</p>
<p>Need help? <a href="https://penpot.app/talk-to-us" target="_blank">Talk to us</a> or join our <a href="https://community.penpot.app/" target="_blank">community</a>.</p>
</div>

View File

@@ -1,6 +1,7 @@
---
layout: layouts/plugins.njk
title: 4. API
desc: Create, deploy, and use the Penpot plugin API with our comprehensive documentation. Get started today and expand Penpot's capabilities.
---
# Penpot plugins API

View File

@@ -1,5 +1,7 @@
---
layout: layouts/plugins-no-sidebar.njk
title: Beta changelog
desc: See the Penpot plugin API changelog for version 1.0! Find breaking changes, deprecations, new features, and updated documentation. Try Penpot for free.
---
# Beta changelog

View File

@@ -1,6 +1,7 @@
---
layout: layouts/plugins.njk
title: 2. Create a Plugin
desc: Dive into Penpot plugin development! This guide covers creating plugins from scratch or using templates, libraries, API communication, & deployment.
---
# Create a Plugin

View File

@@ -1,6 +1,7 @@
---
layout: layouts/plugins.njk
title: 3. Deployment
desc: Deploy your free Penpot plugins! Learn about Netlify, Cloudflare, Surge & Penpot submission in this guide. Build and share your creations.
---
# Deployment

View File

@@ -1,6 +1,7 @@
---
layout: layouts/plugins.njk
title: 5. Examples and templates
desc: Learn to create shapes, text, layouts, components, themes, and interactive prototypes. Start building now! See Penpot plugins with examples & templates!
---
# Examples and templates
@@ -117,7 +118,6 @@ Just a friendly reminder that it's important to have the <b>comment permissions<
<a target="_blank" href="https://github.com/penpot/penpot-plugins-samples/tree/main/create-comments">Comments example</a>
## 5.2. Templates
As we mentioned in the <a target="_blank" href="/plugins/create-a-plugin/">Create a plugin</a> section, we've got two great options for you to get started with your plugin.

View File

@@ -1,6 +1,7 @@
---
layout: layouts/plugins.njk
title: 6. FAQ
desc: Find answers to common questions about plugin development, from choosing the right Node version to creating components. See Penpot plugins!
---
# FAQ

View File

@@ -1,6 +1,7 @@
---
layout: layouts/plugins.njk
title: 1. Getting started
desc: Dive into Penpot plugins! Extend Penpot's functionality by automating tasks and adding new features using JavaScript, HTML, & CSS. Get started now!
---
# Getting started

View File

@@ -1,6 +1,7 @@
---
layout: layouts/plugins-home.njk
title: Plugins
desc: "Get started with Penpot Plugins: Installation, development, and deployment. Access API documentation, examples, templates, and FAQs."
eleventyNavigation:
key: Plugins
order: 5

View File

@@ -1,5 +1,6 @@
---
title: 2. Penpot Configuration
desc: Learn about self-hosting, configuration via environment variables, and authentication providers. Try Penpot - It's free! See Penpot's technical guide.
---
# Penpot Configuration

View File

@@ -0,0 +1,359 @@
---
title: 3.07. Abstraction levels
---
# Code organization in abstraction levels
Initially, Penpot data model implementation was organized in a different way.
We are currently in a process of reorganization. The objective is to have data
manipulation code structured in abstraction layers, with well-defined
boundaries, and a hierarchical structure (each level may only use same or
lower levels, but not higher).
![Abstraction levels](/img/abstraction-levels/abstraction-levels.png)
At this moment the namespace structure is already organized as described here,
but there is much code that does not comply with these rules, and needs to be
moved or refactored. We expect to be refactoring existing modules incrementally,
each time we do an important functionality change.
## Basic data
```text
▾ common/
▾ src/app/common/
data.cljc
▾ src/app/data/
macros.cljc
```
A level for generic data structures or operations, that are not specifically part
of the domain model (e.g. trees, strings, maps, iterators, etc.). Also may belong
here some functions in <code>app.common.geom/</code> and <code>app.common.files.helpers.cljc</code>.
We need to create a new directory for this and move there all functions in this
leve.
## Abstract data types
```text
▾ common/
▾ src/app/common/
▾ types/
file.cljc
page.cljc
shape.cljc
color.cljc
component.cljc
tokens_lib.cljc
...
```
Namespaces here represent a single data entity of the domain model, or a
fragment of one, as an [Abstract Data Type](https://www.geeksforgeeks.org/abstract-data-types/).
An ADT is a data component that is defined through a series of properties and
operations, and that abstracts out the details of how it's implemented and what
is the internal structure. This allows to simplify the logic of the client
code, and also to make future changes in the ADT without affecting callers (if
the abstract interface does not change).
Each structure in this module has:
* A **schema spec** that defines the structure of the type and its values:
```clojure
(def schema:fill
[:map {:title "Fill"}
[:fill-color {:optional true} ::ctc/rgb-color]
[:fill-opacity {:optional true} ::sm/safe-number]
...)
(def schema:shape-base-attrs
[:map {:title "ShapeMinimalRecord"}
[:id ::sm/uuid]
[:name :string]
[:type [::sm/one-of shape-types]]
[:selrect ::grc/rect]
[:points schema:points]
...)
(def schema:token-set-attrs
[:map {:title "TokenSet"}
[:name :string]
[:description {:optional true} :string]
[:modified-at {:optional true} ::sm/inst]
[:tokens {:optional true} [:and
[:map-of {:gen/max 5}
:string
schema:token]
[:fn d/ordered-map?]]]])
```
* A **protocol** that define the external interface to be used for this entity.
(NOTE: this is currently only implemented in some subsystems like Design Tokens
and new path handling).
```clojure
(defprotocol ITokenSet
(update-name [_ set-name] "change a token set name while keeping the path")
(add-token [_ token] "add a token at the end of the list")
(update-token [_ token-name f] "update a token in the list")
(delete-token [_ token-name] "delete a token from the list")
(get-token [_ token-name] "return token by token-name")
(get-tokens [_] "return an ordered sequence of all tokens in the set"))
```
* A **custom data type** that implements this protocol. __Functions here are the only
ones allowed to modify the internal structure of the type__.
Clojure allows us two kinds of custom data types:
* [**`deftype`**](https://funcool.github.io/clojurescript-unraveled/#deftype). We'll
use it when we want the internal structure to be completely opaque and
data accessed through protocol functions. Clojure allows access to the
attributes with the <code class="language-clojure">(.-attr)</code>
syntax, but we prefer not to use it.
* [**`defrecord`**](https://funcool.github.io/clojurescript-unraveled/#defrecord).
We'll use it when we want the structure to be exposed as a plain clojure
map, and thus allowing to read attributes with <code
class="language-clojure">(:attr t)</code>, to use <code
class="language-clojure">get</code>, <code
class="language-clojure">keys</code>, <code
class="language-clojure">vals</code>, etc. Note that this also allows
modifying the object with <code class="language-clojure">assoc</code>,
<code class="language-clojure">update</code>, etc. But in general we
prefer to do all modification via protocol methods, because this way
it's easier to track down where the failure is if an invalid structure
is detected in a validation check, and add business logic like "update
<code class="language-clojure">modified-at</code> whenever any other
attribute is changed".
```clojure
(defrecord TokenSet [name description modified-at tokens]
ITokenSet
(add-token [_ token]
(let [token (check-token token)]
(TokenSet. name
description
(dt/now)
(assoc tokens (:name token) token))))
```
* **Additional helper functions** the protocol should be made as small and compact
as possible. If we need functions for business logic or complex queries that
do not need to directly access the internal structure, but can be implemented by
only calling the abstract procotol, they should be created as standalone functions.
At this level, they must be functions that operate only on instances of the given
domain model entity. They must always ensure the internal integrity of the data.
```clojure
(defn sets-tree-seq
"Get tokens sets tree as a flat list"
[token-sets]
...)
> IMPORTANT SUMMARY
> * Code in this level only knows about one domain model entity.
> * All functions ensure the internal integrity of the data.
> * For this, the schema is used, and the functions must check parameters
> and output values as needed.
> * No outside code should get any knowledge of the internal structure, so it
> can be changed in the future without breaking cliente code.
> * All modifications of the data should be done via protocol methods (even in
> <code class="language-clojure">defrecords</code>). This allows a) more
> control of the internal data dependencies, b) easier bug tracking of
> corrupted data, and c) easier refactoring when the structure is modified.
Currently most of Penpot code does not follow those requirements, but it
should do in new code or in any refactor.
## File operations
```text
▾ common/
▾ src/app/common/
▾ files/
helpers.cljc
shapes_helpers.cljc
...
```
Functions that modify a file object (or a part of it) in place, returning the
file object changed. They ensure the referential integrity within the file, or
between a file and its libraries.
**These functions are used when we need to manipulate objects of different
domain entities inside a file.**
```clojure
(defn resolve-component
"Retrieve the referenced component, from the local file or from a library"
[shape file libraries & {:keys [include-deleted?] :or {include-deleted? False}}]
(if (= (:component-file shape) (:id file))
(ctkl/get-component (:data file) (:component-id shape) include-deleted?)
(get-component libraries
(:component-file shape)
(:component-id shape)
:include-deleted? include-deleted?)))
(defn delete-component
"Mark a component as deleted and store the main instance shapes iside it, to
be able to be recovered later."
[file-data component-id skip-undelete? delta]
(let [delta (or delta (gpt/point 0 0))]
(if skip-undelete?
(ctkl/delete-component file-data component-id)
(-> file-data
(ctkl/update-component component-id #(load-component-objects file-data % delta))
(ctkl/mark-component-deleted component-id)))))
```
> This module is still needing an important refactor. Mainly to take functions
> from common.types and move them here.
### File validation and repair
There is a function in <code class="language-clojure">app.common.files.validate</code> that checks a file for
referential and semantic integrity. It's called automatically when file changes
are sent to backend, but may be invoked manually whenever it's needed.
## File changes objects
```text
▾ common/
▾ src/app/common/
▾ files/
changes_builder.cljc
changes.cljc
...
```
This layer is how we adopt the [Event sourcing pattern](https://www.geeksforgeeks.org/event-sourcing-pattern/).
Instead of directly modifying files, we create <code class="language-clojure">changes</code>
objects, that represent one modification, and that can be serialized, stored,
send to backend, logged, etc. Then it can be *materialized* by a **processing
function**, that takes a file and a change, and returns the updated file.
This also allows an important feature: undoing changes.
Processing functions should not contain business logic or algorithms. Just
adapt the change interface to the operations in **File** or **Abstract Data
Types** levels.
There exists a <code class="language-clojure">changes-builder</code> module
with helper functions to conveniently build changes objects, and to
automatically calculate the reverse undo change.
> IMPORTANT RULES
>
> All changes must satisfy two properties:
> * **[Idempotence](https://en.wikipedia.org/wiki/Idempotence)**. The event
> sourcing architecture and multiuser capability may cause that the same
> change may be applied more than once to a file. So changes must not, for
> example, be like *increment counter* but rather *set counter to value x*.
> * **Minimal scope**. To reduce conflicts, changes should only modify the
> relevant part of the domain entity. This way, if a concurrent change on
> the same entity arrives, from another user, and it modifies a different
> part of the data, they may ve processed without overriding.
## Business logic
```text
▾ common/
▾ src/app/common/
▾ logic/
shapes.cljc
libraries.cljc
```
At this level there are functions that implement high level user actions, in an
abstract way (independent of UI). Here may be complex business logic (eg. to
create a component copy we must clone all shapes, assign new ids, relink
parents, change the head structure to be a copy and link each shape in the copy
with the corresponding one in the main).
They don't directly modify files, but generate changes objects, that may be
executed in frontend or sent to backend.
Those functions may also be composed in even higher level actions. For example
a "update shape attr" action may use "unapply token" actions when the attribute
has an applied token.
```clojure
(defn generate-instantiate-component
"Generate changes to create a new instance from a component."
([changes objects file-id component-id position page libraries]
(generate-instantiate-component changes objects file-id component-id position page libraries nil nil nil {}))
([changes objects file-id component-id position page libraries old-id parent-id frame-id
{:keys [force-frame?]
:or {force-frame? false}}]
(let [component (ctf/get-component libraries file-id component-id)
library (get libraries file-id)
parent (when parent-id (get objects parent-id))
[...]
[new-shape new-shapes]
(ctn/make-component-instance page
component
(:data library)
position
(cond-> {}
force-frame?
(assoc :force-frame-id frame-id)))
[...]
changes
(reduce #(pcb/add-object %1 %2 {:ignore-touched true})
changes
(rest new-shapes))]
[new-shape changes])))
```
## Data events
```text
▾ frontend/
▾ src/app/main/data/
▾ dashboard/
▾ viewer/
▾ workspace/
```
This is the intersection of the logic and the presentation in Penpot. Data
events belong to the presentation interface and they manage the global state of
the application. But they may also work on loaded files by using **File** or
**Abstract Data Types** operations to query the data, and by creating and
commiting **changes** via the **Business logic** generate functions.
**IMPORTANT: data events must not contain business logic theirselves**, or
directly manipulate data structures. They only may modify or query the global
state, and delegate all logic to lower level functions.
In current Penpot code, there is some quantity of business logic in data events,
that should be progressively moved elsewhere as we keep refactoring.
```clojure
(defn detach-component
"Remove all references to components in the shape with the given id,
and all its children, at the current page."
[id]
(dm/assert! (uuid? id))
(ptk/reify ::detach-component
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
file-id (:current-file-id state)
fdata (dsh/lookup-file-data state file-id)
libraries (dsh/lookup-libraries state)
changes (-> (pcb/empty-changes it)
(cll/generate-detach-component id fdata page-id libraries))]
(rx/of (dch/commit-changes changes))))))
```

View File

@@ -1,5 +1,6 @@
---
title: Backend app
desc: Dive into self-hosting, configuration, developer insights (architecture, data model), integration, and troubleshooting. See Penpot's Technical Guide.
---
# Backend app

View File

@@ -1,5 +1,6 @@
---
title: Common code
desc: Learn about architecture, data models, and development environments. See Penpot's technical guide for developers. Dive into common code.
---
# Common code

View File

@@ -1,5 +1,6 @@
---
title: Exporter app
desc: Learn about self-hosting, configuration, architecture (backend, frontend), data model, and development environment. See Penpot's technical guide.
---
# Exporter app

View File

@@ -1,5 +1,6 @@
---
title: Frontend app
desc: Dive into the UI, data namespaces, ClojureScript, React, and worker app functionalities. View Penpot's frontend app architecture. Free to try!
---
### Frontend app

View File

@@ -1,5 +1,6 @@
---
title: 3.1. Architecture
title: 3.01. Architecture
desc: Dive into architecture, backend, frontend, data models, and development environments. Contribute and self-host for free! See Penpot's technical guide.
---
# Architecture

View File

@@ -1,5 +1,5 @@
---
title: 3.6. Backend Guide
title: 3.06. Backend Guide
---
# Backend guide #

View File

@@ -1,5 +1,6 @@
---
title: 3.4. Common Guide
title: 3.04. Common Guide
desc: "View Penpot's technical guide: self-hosting, configuration, developer insights, architecture, data model, integration, and troubleshooting."
---
# Common guide

View File

@@ -1,5 +1,6 @@
---
title: 3.7. Data Guide
title: 3.08. Data Guide
desc: Learn about data structures, code organization, file operations, migrations, shape editing, and component syncing. See Penpot's technical guide. Try it free!
---
# Data Guide
@@ -29,217 +30,6 @@ all of this is important in general.
Clojure (for example ending it with ? for boolean values), because this may
cause problems when exporting.
## Code organization in abstraction levels
Initially, Penpot data model implementation was organized in a different way.
We are currently in a process of reorganization. The objective is to have data
manipulation code structured in abstraction layers, with well-defined
boundaries.
At this moment the namespace structure is already organized as described here,
but there is much code that does not comply with these rules, and needs to be
moved or refactored. We expect to be refactoring existing modules incrementally,
each time we do an important functionality change.
### Abstract data types
```text
▾ common/
▾ src/app/common/
▾ types/
file.cljc
page.cljc
shape.cljc
color.cljc
component.cljc
...
```
Namespaces here represent a single data structure, or a fragment of one, as an
abstract data type. Each structure has:
* A **schema spec** that defines the structure of the type and its values:
```clojure
(sm/define! ::fill
[:map {:title "Fill"}
[:fill-color {:optional true} ::ctc/rgb-color]
[:fill-opacity {:optional true} ::sm/safe-number]
...)
(sm/define! ::shape-attrs
[:map {:title "ShapeAttrs"}
[:name {:optional true} :string]
[:selrect {:optional true} ::grc/rect]
[:points {:optional true} ::points]
[:blocked {:optional true} :boolean]
[:fills {:optional true}
[:vector {:gen/max 2} ::fill]]
...)
```
* **Helper functions** to create, query and manipulate the structure. Helpers
at this level only are allowed to see the internal attributes of a type.
Updaters receive an object of the type, and return a new object modified,
also ensuring the internal integrity of the data after the change.
```clojure
(defn setup-shape
"A function that initializes the geometric data of the shape. The props must
contain at least :x :y :width :height."
[{:keys [type] :as props}]
...)
(defn has-direction?
[interaction]
(#{:slide :push} (-> interaction :animation :animation-type)))
(defn set-direction
[interaction direction]
(dm/assert!
"expected valid interaction map"
(check-interaction! interaction))
(dm/assert!
"expected valid direction"
(contains? direction-types direction))
(dm/assert!
"expected compatible interaction map"
(has-direction? interaction))
(update interaction :animation assoc :direction direction))
```
> IMPORTANT: we should always use helper functions to access and modify these data
> structures. Avoid direct attribute read or using functions like <code class="language-clojure">assoc</code> or
> <code class="language-clojure">update</code>, even if the information is contained in a single attribute. This way
> it will be much simpler to add validation checks or to modify the internal
> representation of a type, and will be easier to search for places in the code
> where this data item is used.
>
> Currently much of Penpot code does not follow this requirement, but it
should do in new code or in any refactor.
### File operations
```text
▾ common/
▾ src/app/common/
▾ files/
helpers.cljc
shapes_helpers.cljc
...
```
Functions that modify a file object (or a part of it) in place, returning the
file object changed. They ensure the referential integrity within the file, or
between a file and its libraries.
```clojure
(defn resolve-component
"Retrieve the referenced component, from the local file or from a library"
[shape file libraries & {:keys [include-deleted?] :or {include-deleted? False}}]
(if (= (:component-file shape) (:id file))
(ctkl/get-component (:data file) (:component-id shape) include-deleted?)
(get-component libraries
(:component-file shape)
(:component-id shape)
:include-deleted? include-deleted?)))
(defn delete-component
"Mark a component as deleted and store the main instance shapes inside it, to
be able to be recovered later."
[file-data component-id skip-undelete? Main-instance]
(let [components-v2 (dm/get-in file-data [:options :components-v2])]
(if (or (not components-v2) skip-undelete?)
(ctkl/delete-component file-data component-id)
(let [set-main-instance ;; If there is a saved main-instance, restore it.
#(if main-instance
(assoc-in % [:objects (:main-instance-id %)] main-instance)
%)]
(-> file-data
(ctkl/update-component component-id load-component-objects)
(ctkl/update-component component-id set-main-instance)
(ctkl/mark-component-deleted component-id))))))
```
> This module is still needing an important refactor. Mainly to take functions
> from common.types and move them here.
#### File validation and repair
There is a function in <code class="language-clojure">app.common.files.validate</code> that checks a file for
referential and semantic integrity. It's called automatically when file changes
are sent to backend, but may be invoked manually whenever it's needed.
### File changes objects
```text
▾ common/
▾ src/app/common/
▾ files/
changes_builder.cljc
changes.cljc
...
```
Wrap the update functions in file operations module into <code class="language-clojure">changes</code> objects, that
may be serialized, stored, sent to backend and executed to actually modify a file
object. They should not contain business logic or algorithms. Only adapt the
interface to the file operations or types.
```clojure
(sm/define! ::changes
[:map {:title "changes"}
[:redo-changes vector?]
[:undo-changes seq?]
[:origin {:optional true} any?]
[:save-undo? {:optional true} boolean?]
[:stack-undo? {:optional true} boolean?]
[:undo-group {:optional true} any?]])
(defmethod process-change :add-component
[file-data params]
(ctkl/add-component file-data params))
```
### Business logic
```text
▾ common/
▾ src/app/common/
▾ logic/
shapes.cljc
libraries.cljc
```
Functions that implement semantic user actions, in an abstract way (independent
of UI). They don't directly modify files, but generate changes objects, that
may be executed in frontend or sent to backend.
```clojure
(defn generate-instantiate-component
"Generate changes to create a new instance from a component."
[changes objects file-id component-id position page libraries old-id parent-id
frame-id {:keys [force-frame?] :or {force-frame? False}}]
(let [component (ctf/get-component libraries file-id component-id)
parent (when parent-id (get objects parent-id))
library (get libraries file-id)
components-v2 (dm/get-in library [:data :options :components-v2])
[new-shape new-shapes]º
(ctn/make-component-instance page
Component
(:data library)
Position
Components-v2
(cond-> {}
force-frame? (assoc :force-frame-id frame-id)))
changes (cond-> (pcb/add-object changes first-shape {:ignore-touched true})
(some? old-id) (pcb/amend-last-change #(assoc % :old-id old-id)))
changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true})
changes
(rest new-shapes))]
[new-shape changes]))
```
## Data migrations
```text

View File

@@ -1,5 +1,6 @@
---
title: 3.2. Data model
title: 3.02. Data model
desc: Learn about self-hosting, configuration, developer tools, data models, architecture, and integrations. View Penpot's technical guide. Free to use!
---
# Penpot Data Model

View File

@@ -1,5 +1,6 @@
---
title: 3.3. Dev environment
title: 3.03. Dev environment
desc: Dive into Penpot's development environment. Learn about self-hosting, configuration, developer tools, architecture, and more. See the Penpot Technical Guide!
---
# Development environment
@@ -95,7 +96,7 @@ npx shadow-cljs cljs-repl main
### Storybook
The storybook local server is started on tmux **window 2** and will listen
for changes in the styles, components or stories defined in the folders
for changes in the styles, components or stories defined in the folders
under the design system namespace: `app.main.ui.ds`.
You can open the broser on http://localhost:6006/ to see it.

View File

@@ -1,5 +1,6 @@
---
title: 3.5. Frontend Guide
title: 3.05. Frontend Guide
desc: "See Penpot's technical guide: self-hosting, configuration, developer insights (architecture, data model), frontend, backend, and integrations & more!"
---
# Frontend Guide

View File

@@ -1,5 +1,6 @@
---
title: 3. Developer Guide
desc: Dive into architecture, data models, and more. Start building today! See Penpot's technical guide for self-hosting, configuration, and developer insights.
---
# Developer Guide

View File

@@ -1,5 +1,6 @@
---
title: Assets storage
desc: Learn about assets storage, API, object buckets, sharing, and garbage collection. See Penpot's technical guide for developers. Try Penpot - It's free.
---
# Assets storage

View File

@@ -1,5 +1,6 @@
---
title: Authentication
desc: Dive into Penpot today! Learn about self-hosting, configuration, developer insights, authentication, and more. View Penpot's technical guide. Try it free.
---
# User authentication

View File

@@ -1,5 +1,6 @@
---
title: 3.8. Penpot subsystems
title: 3.09. Penpot subsystems
desc: Learn about architecture, data models, and subsystems. View Penpot's technical guide for self-hosting, configuration, and development insights. Free!
---
# Penpot subsystems
@@ -12,4 +13,3 @@ implemented, over the whole app (backend, frontend or exporter), and points to
the most relevant source files to look at to start exploring it. When some
special considerations are needed (performance questions, limits, common
"gotchas", historic reasons of some decisions, etc.) they are also noted.

View File

@@ -1,5 +1,6 @@
---
title: 3.9. UI Guide
title: 3.10. UI Guide
desc: Learn UI development with React & Rumext, design system implementation, and performance considerations. See Penpot's technical guide. Free to use!
---
# UI Guide

View File

@@ -1,5 +1,6 @@
---
title: 1.3 Install with Docker
desc: This Penpot technical guide covers self-hosting, Docker installation, configuration, updates, backups, and proxy setup with NGINX and Caddy. Try Penpot!
---
<p class="advice">

View File

@@ -1,5 +1,6 @@
---
title: 1. Self-hosting Guide
desc: Customize your Penpot instance today. Learn how to install with Elestio, Docker, or Kubernetes from the technical guide for self-hosting options.
---
# Self-hosting Guide

View File

@@ -1,5 +1,6 @@
---
title: 1.4 Install with Kubernetes
desc: Learn how to install and configure Penpot on your Kubernetes cluster using Helm. Our technical guide provides step-by-step instructions for setup.
---
# Install with Kubernetes

View File

@@ -1,5 +1,6 @@
---
title: 1.1 Recommended storage
desc: Learn recommended self-hosting settings, Docker & Kubernetes installs, configuration, and troubleshooting tips in Penpot's technical guide.
---
# Recommended storage

View File

@@ -1,5 +1,6 @@
---
title: 1.5 Unofficial self-host options
desc: Find guides for Docker, Kubernetes, and more in Penpot's Technical Guide for self-hosting options! Discover unofficial self-host options too.
---
# Unofficial self-host options

View File

@@ -1,5 +1,6 @@
---
title: Technical Guide
desc: Get self-hosting instructions, integration details, and developer resources. Troubleshoot issues easily. Try Penpot free! See Penpot's technical guide.
eleventyNavigation:
key: Technical Guide
order: 4

View File

@@ -1,5 +1,6 @@
---
title: 4. Integration Guide
desc: Connect Penpot with other apps using webhooks and access tokens! Learn from Penpot's integration guide for seamless workflows. Try Penpot - It's free.
---
# Integration Guide

View File

@@ -1,5 +1,6 @@
---
title: 5. Troubleshooting Penpot
desc: Troubleshoot Penpot like a pro! Our technical guide offers tips and tricks for diagnosing issues, reading logs, and resolving problems. Get started now!
---
# Troubleshooting Penpot

View File

@@ -1,10 +1,11 @@
---
title: 11· Components
desc: Streamline your design workflow with Penpot's Components guide! Learn to create, duplicate, group, and manage reusable components.
---
<h1 id="components">Components</h1>
<p class="main-paragraph">Speed your workflow with the reusable power of components.</p>
<p>A component is an object or group of objects that can be reused multiple times across files. This can help you maintain consistency across a group of designs.</p>
<p>A component is an object or group of objects that can be reused multiple times across files. This can help you maintain consistency across a group of designs.</p>
<p>A component has two parts:</p>
<ul>
@@ -62,7 +63,7 @@ title: 11· Components
<h2 id="component-group">Group components</h2>
<h3>Create component groups</h3>
<p>At the Components section from the Assets library, there are two ways to create groups in a components library.</p>
<p>At the Components section from the Assets library, there are two ways to create groups in a components library.</p>
<ol>
<li><strong>Using slashes (/):</strong> Select one component and rename it as follows: "FOLDER NAME/COMPONENT NAME". For example, "Buttons/Alert Button".</li>
<li><strong>Using the "Group" option:</strong> Select one or more components at the Assets library, right click to show the menu and then select "Group".</li>

View File

@@ -1,5 +1,6 @@
---
title: 17· Custom fonts
desc: Penpot's guide on custom fonts! Upload, manage, and use custom fonts in Penpot! Enhance your designs with personalised typography.
---
<h1 id="customfonts">Custom fonts</h1>
@@ -13,9 +14,9 @@ title: 17· Custom fonts
<h3>To upload a local font:</h3>
<ol>
<li>Press “Add custom font”.</li>
<li>Inspect your local files to select one or more fonts that you want to upload. <strong>You can upload fonts with
<li>Inspect your local files to select one or more fonts that you want to upload. <strong>You can upload fonts with
the following formats: TTF, OTF and WOFF</strong>. Only one format will be needed.</li>
<li>Change the font name if needed. The font name is the name that will be shown in the font list at the workspace.
<li>Change the font name if needed. The font name is the name that will be shown in the font list at the workspace.
It is also what Penpot uses to group fonts in families. You can always edit it later.</li>
<li>Once ready, press upload. That's it. The font will be available at the font list of this teams files.</li>
</ol>

View File

@@ -285,11 +285,12 @@ title: 10· Design Tokens
<p>When a token set is selected, the tokens within the selected set are displayed on the panel below.</p>
<h3 id="design-tokens-sets-delete">Deleting and Renaming a Token Set</h3>
<p>Token sets can be renamed or deleted by right-clicking on the token set and:</p>
<h3 id="design-tokens-sets-edit">Deleting, Duplicating and Renaming a Token Set</h3>
<p>Right-click a token set to perform these quick actions:</p>
<ol>
<li>Selecting <strong>Rename</strong>, entering a new name, and hitting Enter.</li>
<li>Selecting <strong>Delete</strong>.</li>
<li><strong>Rename</strong>: Give the set a new name and press Enter.</li>
<li><strong>Duplicate</strong>: Make a copy of the set.</li>
<li><strong>Delete</strong>: Remove the set permanently.</li>
</ol>
<figure>
<img src="/img/design-tokens/14-tokens-sets-edit.webp" alt="Tokens sets edit" />
@@ -299,10 +300,10 @@ title: 10· Design Tokens
<p>Once you have created a token set, you can start creating tokens within that token set. To do so, simply select the token set and create a new token.</p>
<p class="advice">If a token with the same name already exists in another set, a new token can still be created in the current set.</p>
<h3 id="design-tokens-groups">Creating Token Groups</h3>
<p>You can create a token set group by simply naming your token sets to have a folder path. For example, you can create a <strong><i>Light</i></strong> group with a <strong><i>Global</i></strong> set and a <strong><i>Colors</i></strong> set using: <code class="language-js">Light/Global</code>, <code class="language-js">Light/Colors</code>. </p>
<h3 id="design-tokens-groups">Creating Token Set Folders</h3>
<p>To group token sets just use folder-style names. For example, naming your sets <code class="language-js">Light/Global</code> and <code class="language-js">Light/Colors</code> will create a folder called <strong><i>Light</i></strong> with two sets inside it: <strong><i>Global</i></strong> and <strong><i>Colors</i></strong>.</p>
<figure>
<img src="/img/design-tokens/15-tokens-sets-group.webp" alt="Tokens sets group" />
<img src="/img/design-tokens/15-tokens-sets-group.webp" alt="Tokens sets folder" />
</figure>
<h2 id="design-tokens-themes">Token Themes</h2>

View File

@@ -1,5 +1,6 @@
---
title: 07· Exporting objects
desc: Learn how to export objects in Penpot, the free, open-source design collaboration tool. This guide covers export presets, file formats, and more!
---
<h1 id="export">Exporting objects</h1>

View File

@@ -1,5 +1,6 @@
---
title: 08· Flexible Layouts
desc: Master responsive web design with Penpot's flexible and grid layouts! Learn Flexbox and CSS Grid standards. Explore tutorials, properties, and more.
---
<h1 id="layouts">Flexible Layouts</h1>

View File

@@ -1,5 +1,6 @@
---
title: 15· Import/export files
desc: Learn how to import and export files in Penpot, the free, open-source design tool. Discover file formats, backups, sharing, and library management.
---
<h1 id="import-export">Import and export files</h1>

View File

@@ -1,5 +1,6 @@
---
title: User guide
desc: Learn everything from interface basics to advanced features like prototyping and design sharing with Penpot's comprehensive user guide! Free access.
eleventyNavigation:
key: User guide
order: 2

View File

@@ -1,5 +1,6 @@
---
title: 14· Inspect designs
desc: Learn how to inspect designs in Penpot! This guide covers distances, properties, code snippets (CSS, SVG, HTML), & exporting assets for seamless collaboration.
---
<h1 id="inspect">Inspect designs</h1>

View File

@@ -1,5 +1,6 @@
---
title: 01· Introduction
desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorials. Learn the interface, layers, objects, styling, and more.
---
<h1 id="section-1">Introduction</h1>

View File

@@ -1,5 +1,6 @@
---
title: Tutorials & info
desc: Begin with Penpot's comprehensive user guide! Get tutorials, learn interface basics, and master design features. Discover FAQs and more.
---
<h1 id="section-1-1">Tutorials & info</h1>

View File

@@ -1,5 +1,6 @@
---
title: Quickstart
desc: Start instantly with Penpot, the open-source design and prototyping platform! Access our free user guide to learn now.
---
<h1 id="section-1-1">Quickstart</h1>

View File

@@ -1,5 +1,6 @@
---
title: Shortcuts
desc: Get quickstart tips, shortcuts, and tutorials for Penpot! Learn interface basics and more with this free, open-source design tool.
---
<h1 id="section-1-1">Shortcuts</h1>

View File

@@ -1,5 +1,6 @@
---
title: 04· Layer basics
desc: Master layer basics with Penpot's user guide! Learn to create, manipulate, and organize layers for stunning designs. Try Penpot, it's free!
---
<h1 id="layer-basics">Layer basics</h1>

View File

@@ -1,5 +1,6 @@
---
title: 09· Asset Libraries
desc: Use Penpot's asset libraries for reusable design elements! Learn to create, manage, and share components, colors, and typography. Try Penpot - it's free!
---
<h1 id="asset-libraries">Asset Libraries</h1>
@@ -112,7 +113,7 @@ title: 09· Asset Libraries
</figure>
<h4>Group assets</h4>
<p>There are two ways to create groups in a library.</p>
<p>There are two ways to create groups in a library.</p>
<ol>
<li><strong>With slashes (/):</strong> Select an asset and rename it as follows: "FOLDER NAME/ASSET NAME". For example, "Buttons/Alert Button".</li>
<li><strong>With the "Group" option:</strong> Select one or more assets at the library, then right click to show the menu and then select "Group".</li>
@@ -138,7 +139,7 @@ title: 09· Asset Libraries
<h2 id="libraries">Libraries</h2>
<h3 id="file-libraries">File libraries</h3>
<p>Each file has its own file library which is where the assets that belong to this file are stored.</p>
<p>Each file has its own file library which is where the assets that belong to this file are stored.</p>
<p>You have two ways to access the file library from the file <a href="/user-guide/the-interface/#interface-workspace">workspace</a>:</p>
<ul>
<li>Click the assets tab icon at the left sidebar.</li>

View File

@@ -1,5 +1,6 @@
---
title: 05· Objects
desc: "Work with Penpot's objects: boards, shapes, text, paths, and graphics. Learn to create, select, rename, and customize boards for optimal workflow."
---
<h1 id="objects">Objects</h1>
@@ -7,7 +8,7 @@ title: 05· Objects
available in Penpot, and how to get the most of them.</p>
<h2 id="Boards">Boards</h2>
<p>A Board is a layer typically used as a container for a design. Boards are useful if you want to design for a specific screen or print size. Boards can contain other boards. First level boards
<p>A Board is a layer typically used as a container for a design. Boards are useful if you want to design for a specific screen or print size. Boards can contain other boards. First level boards
are shown by default at the <a href="/user-guide/view-mode">View mode</a>, acting as screens of a design or pages of a document. Also, objects inside boards can be clipped. Boards are a powerful element at Penpot, opening up a ton of possibilities when creating and organizing your designs.</p>
<h3>Create boards</h3>
@@ -80,7 +81,7 @@ are shown by default at the <a href="/user-guide/view-mode">View mode</a>, actin
</figure>
<h3>Show in View mode</h3>
<p>Boards offer the option to be shown as a separate board/screen in the <a href="/user-guide/the-interface/#interface-viewmode">View mode</a>. Use this setting to decide what boards should be shown as individual items in your presentations.</p>
<p>Boards offer the option to be shown as a separate board/screen in the <a href="/user-guide/the-interface/#interface-viewmode">View mode</a>. Use this setting to decide what boards should be shown as individual items in your presentations.</p>
<p><strong>Defaults</strong></p>
<p>As it is very likely that the first level boards will be used as a screen and the interiors will not, there are different defaults for newly created boards.</p>
<ul>

View File

@@ -1,5 +1,6 @@
---
title: 18· Plugins
desc: Extend Penpot's functionality with plugins! Install from PenpotHub or via URL. Learn to install, use, and create your own plugins.
---
<h1 id="penpot-plugins">Penpot Plugins</h1>

View File

@@ -1,5 +1,6 @@
---
title: 12· Prototyping
desc: This Penpot user guide explains how to prototype interactions, connect boards, use triggers/actions, and animations. Learn to build flows and more!
---
<h1 id="prototype">Prototyping interactions</h1>

View File

@@ -1,13 +1,14 @@
---
title: 06· Styling
desc: Style your designs with Penpot's options! Learn about color fills, gradients, strokes, shadows, blur, opacity, blend modes, and property copying.
---
<h1 id="styling">Styling</h1>
<p class="main-paragraph">Penpot has a variety of styling options for each object. When selected, the styling options are displayed in the design panel on the right.</p>
<h2 id="fill">Color fills</h2>
<p>Color fills can be added to boards, shapes, texts and groups of layers.</p>
<p>You can add as fills:</p>
<p>Color fills can be added to boards, shapes, texts and groups of layers.</p>
<p>You can add as fills:</p>
<ul>
<li>Custom colors (hex).</li>
<li>Color <a href="/user-guide/libraries/#asset-types">assets</a>.</li>
@@ -169,7 +170,7 @@ title: 06· Styling
</figure>
<h2 id="blend">Opacity and blend</h2>
<p>Set the overal opacity for layers and their blend mode.</p>
<p>Set the overal opacity for layers and their blend mode.</p>
<p>Blend allows you to control how a layer interacts with the layers beneath it, determining how pixels from the current layer are combined with pixels in the underlying layers. Use blend to achive various effects, such as shading, highlights, or creative visual styles.</p>
<figure>
<img alt="Layer blend and opacity" src="/img/styling/blend-opacity.webp"/>
@@ -195,7 +196,7 @@ title: 06· Styling
</ul>
<h2 id="copy-paste-properties">Copy/Paste properties</h2>
<p>You can copy and apply properties, including fills, strokes, shadows, and others from one layer to another—or multiple layers with just a few clicks. You can do it using the layer's menu or shortcuts.</p>
<p>You can copy and apply properties, including fills, strokes, shadows, and others from one layer to another—or multiple layers with just a few clicks. You can do it using the layer's menu or shortcuts.</p>
<figure>
<video title="Apply blur to a layer" muted="" playsinline="" controls="" width="100%" poster="/img/styling/copy-properties.webp" height="auto">

View File

@@ -1,11 +1,12 @@
---
title: 16· Teams
desc: Manage teams and roles with Penpot's collaboration features! Learn how to manage teams, roles (Viewer, Editor, Admin, Owner), send invites and use webhooks.
---
<h1 id="teams">Teams</h1>
<p class="main-paragraph">A team is a group of members who collaborate on a collection of projects.
<p class="main-paragraph">A team is a group of members who collaborate on a collection of projects.
Team members are allowed to work with any project or file within the team. The actions that each team
member is allowed to do depends on their permissions.</p>
member is allowed to do depends on their permissions.</p>
<p class="main-paragraph">At Penpot you can create and join as many teams as you need and add all necessary stakeholders with no team size limits.</p>
<h2 id="teams-management">Manage teams</h2>

View File

@@ -1,5 +1,6 @@
---
title: 02· The interface
desc: Discover Penpot's free user guide! Learn the interface, workspace basics, flexible layouts, and prototyping. Master Penpot today.
---
<h1 id="the-interface">The interface</h1>

View File

@@ -1,5 +1,6 @@
---
title: 13· View mode
desc: Present designs and share prototypes with Penpot's View mode! Play interactions, navigate boards, zoom, and toggle fullscreen. Try it for free!
---
<h1 id="viewmode">View mode</h1>

View File

@@ -1,5 +1,6 @@
---
title: 03· Workspace basics
desc: Master Penpot's workspace basics! Learn interface navigation, zoom tools, dynamic alignment, rulers, guides, and shortcuts.
---
<h1 id="workspace-basics">Workspace basics</h1>

View File

@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14.499" height="14.507" viewBox="700.734 827.762 14.499 14.507"><path d="M707.948 827.762c-.193 0-.386.007-.578.022-2.311.181-4.574 1.452-5.839 3.982-1.686 3.372-.52 6.821 1.835 8.787 2.354 1.966 5.955 2.497 8.974.233a.566.566 0 1 0-.68-.906c-2.611 1.959-5.563 1.478-7.568-.196-2.004-1.675-3.005-4.495-1.546-7.412 1.458-2.917 4.314-3.808 6.856-3.208 2.543.599 4.698 2.672 4.698 5.936v.667c0 .525-.176.847-.435 1.076-.258.23-.624.357-.998.357s-.741-.127-.999-.357c-.258-.229-.435-.551-.435-1.076v-3.334a.567.567 0 0 0-1.133 0v.215a3.215 3.215 0 0 0-2.1-.781 3.241 3.241 0 0 0-3.233 3.233 3.241 3.241 0 0 0 3.233 3.233 3.23 3.23 0 0 0 2.482-1.168c.122.199.267.377.433.525a2.63 2.63 0 0 0 1.752.643c.626 0 1.259-.206 1.751-.643.492-.437.815-1.115.815-1.923V835c0-3.773-2.586-6.336-5.572-7.04a7.405 7.405 0 0 0-1.713-.198ZM708 832.9c1.167 0 2.1.933 2.1 2.1a2.09 2.09 0 0 1-2.1 2.1 2.09 2.09 0 0 1-2.1-2.1c0-1.167.933-2.1 2.1-2.1Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.667 5.333v3.334a2 2 0 1 0 4 0V8a6.667 6.667 0 1 0-2.614 5.293M10.667 8a2.667 2.667 0 1 1-5.334 0 2.667 2.667 0 0 1 5.334 0Z" />
</svg>

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 282 B

View File

@@ -5086,7 +5086,8 @@ class zc extends HTMLElement {
"allow-modals",
"allow-popups",
"allow-popups-to-escape-sandbox",
"allow-storage-access-by-user-activation"
"allow-storage-access-by-user-activation",
"allow-same-origin"
), o && l.sandbox.add("allow-downloads"), l.addEventListener("load", () => {
var d;
(d = this.shadowRoot) == null || d.dispatchEvent(

View File

@@ -5,6 +5,7 @@
set -ex
export INCLUDE_STORYBOOK=${BUILD_STORYBOOK:-no};
export INCLUDE_WASM=${BUILD_WASM:-yes};
export CURRENT_VERSION=$1;
export BUILD_DATE=$(date -R);
@@ -17,14 +18,18 @@ export TS=$(date +%s);
export NODE_ENV=production;
corepack enable;
corepack up || exit 1;
corepack install || exit 1;
yarn install || exit 1;
rm -rf resources/public;
rm -rf target/dist;
yarn run build:app:main --config-merge "{:release-version \"${CURRENT_HASH}-${TS}\"}" $EXTRA_PARAMS || exit 1
yarn run build:wasm || exit 1;
if [ "$INCLUDE_WASM" = "yes" ]; then
yarn run build:wasm || exit 1;
fi
yarn run build:app:libs || exit 1;
yarn run build:app:assets || exit 1;
@@ -36,7 +41,10 @@ sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/render.html;
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/rasterizer.html;
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/index.html;
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/rasterizer.html;
sed -i "s/version=develop/version=$CURRENT_VERSION/g" ./target/dist/js/render_wasm.js;
if [ "$INCLUDE_WASM" = "yes" ]; then
sed -i "s/version=develop/version=$CURRENT_VERSION/g" ./target/dist/js/render_wasm.js;
fi
if [ "$INCLUDE_STORYBOOK" = "yes" ]; then
# build storybook

View File

@@ -34,6 +34,7 @@
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl]
[app.common.types.shape.text :as types.text]
[app.common.types.typography :as ctt]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -2192,27 +2193,27 @@
(ptk/reify ::paste-html-text
ptk/WatchEvent
(watch [_ state _]
(let [root (dwtxt/create-root-from-html html)
content (tc/dom->cljs root)
(let [root (dwtxt/create-root-from-html html)
content (tc/dom->cljs root)]
(when (types.text/valid-content? content)
(let [id (uuid/next)
width (max 8 (min (* 7 (count text)) 700))
height 16
{:keys [x y]} (calculate-paste-position state)
id (uuid/next)
width (max 8 (min (* 7 (count text)) 700))
height 16
{:keys [x y]} (calculate-paste-position state)
shape {:id id
:type :text
:name (txt/generate-shape-name text)
:x x
:y y
:width width
:height height
:grow-type (if (> (count text) 100) :auto-height :auto-width)
:content content}
undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id)
(dwsh/create-and-add-shape :text x y shape)
(dwu/commit-undo-transaction undo-id))))))
shape {:id id
:type :text
:name (txt/generate-shape-name text)
:x x
:y y
:width width
:height height
:grow-type (if (> (count text) 100) :auto-height :auto-width)
:content content}
undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id)
(dwsh/create-and-add-shape :text x y shape)
(dwu/commit-undo-transaction undo-id))))))))
(defn- paste-text
[text]

View File

@@ -48,7 +48,7 @@
ptk/UpdateEvent
(update [_ state]
(-> state
(update :workspace-local dissoc :edition)
(update :workspace-local dissoc :edition :edit-path)
(update :workspace-drawing dissoc :tool :object :lock)
(dissoc :workspace-grid-edition)))

View File

@@ -22,6 +22,7 @@
[app.main.data.helpers :as dsh]
[app.main.data.modal :as md]
[app.main.data.workspace.collapse :as dwc]
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.specialized-panel :as-alias dwsp]
[app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.zoom :as dwz]
@@ -305,8 +306,9 @@
(watch [_ state _]
(let [params-without-board (-> (rt/get-params state)
(dissoc :board-id))]
(rx/of ::dwsp/interrupt)
(rx/of (rt/nav :workspace params-without-board {::rt/replace true}))))
(rx/of ::dwsp/interrupt
(dwe/clear-edition-mode)
(rt/nav :workspace params-without-board {::rt/replace true}))))
ptk/UpdateEvent
(update [_ state]

View File

@@ -147,6 +147,7 @@
(swap! state* assoc :width width)))
[:div {:class (stl/css :palette-wrapper)
:id "palette-wrapper"
:style (calculate-palette-padding rulers?)
:data-testid "palette"}
(when-not workspace-read-only?

View File

@@ -4,6 +4,10 @@
//
// Copyright (c) KALEIDOS INC
@use "../ds/spacing.scss" as *;
@use "../ds/z-index.scss" as *;
@use "../ds/_sizes.scss" as *;
@import "refactor/common-refactor.scss";
.palette-wrapper {
@@ -12,6 +16,12 @@
left: 0;
bottom: 0;
padding-bottom: $s-4;
/** Aligns AI Chat button **/
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-s);
}
.palettes {
@@ -164,3 +174,27 @@
padding-bottom: $s-8;
}
}
/** AI Chat button styles **/
.help-btn {
z-index: var(--z-index-panels);
flex-shrink: 0;
@extend .button-secondary;
inline-size: $sz-40;
block-size: $sz-40;
border-radius: $br-circle;
border: none;
&.selected {
@extend .button-icon-selected;
}
&:hover {
border: none;
}
}
.icon-help {
@extend .button-icon;
stroke: var(--icon-foreground);
inline-size: var(--sp-xxl);
block-size: var(--sp-xxl);
}

View File

@@ -129,6 +129,12 @@
input-ref (mf/use-ref nil)
team (mf/deref refs/team)
permissions (get team :permissions)
display-share-button?
(and (not (:is-default team))
(or (:is-admin permissions)
(:is-owner permissions)))
nav-to-viewer
(mf/use-fn
@@ -216,7 +222,7 @@
:on-click toggle-history}
i/history]])
(when (not (:is-default team))
(when display-share-button?
[:a {:class (stl/css :viewer-btn)
:title (tr "workspace.header.share")
:on-click open-share-dialog}

View File

@@ -38,7 +38,8 @@
indent))
(cfh/text-shape? shape)
(let [text-shape-html (rds/renderToStaticMarkup (mf/element text/text-shape #js {:shape shape :code? true}))]
(let [text-shape-html (rds/renderToStaticMarkup (mf/element text/text-shape #js {:shape shape :code? true}))
text-shape-html (str/replace text-shape-html #"style\s*=\s*[\"'][^\"']*[\"']" "")]
(dm/fmt "%<div class=\"%\">\n%\n%</div>"
indent
(dm/str "shape " (d/name (:type shape)) " "

Some files were not shown because too many files have changed in this diff Show More