Compare commits

..

1 Commits

Author SHA1 Message Date
Eva Marco
9e2bd5c38c 🐛 Fix hover position of lock proportion tooltip 2025-10-16 10:52:49 +02:00
464 changed files with 17257 additions and 18519 deletions

View File

@@ -84,10 +84,8 @@ jobs:
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
📦 *[PENPOT] Error building penpot bundles.*
❌ *[PENPOT] Error during the execution of the job*
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
Bundle version: `${{ steps.vars.outputs.bundle_version }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra

View File

@@ -34,26 +34,18 @@ jobs:
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
- name: Download Penpot Bundles
id: bundles
env:
FILE_NAME: penpot-${{ steps.vars.outputs.gh_ref }}.zip
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
tmp=$(aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "$FILE_NAME" \
--query 'Metadata."bundle-version"' \
--output text)
echo "bundle_version=$tmp" >> $GITHUB_OUTPUT
pushd docker/images
aws s3 cp s3://${{ secrets.S3_BUCKET }}/$FILE_NAME .
unzip $FILE_NAME > /dev/null
mv penpot/backend bundle-backend
mv penpot/frontend bundle-frontend
mv penpot/exporter bundle-exporter
mv penpot/storybook bundle-storybook
popd
- name: Set up Docker Buildx
@@ -66,18 +58,6 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images:
frontend
backend
exporter
storybook
labels: |
bundle_version=${{ steps.bundles.outputs.bundle_version }}
- name: Build and push Backend Docker image
uses: docker/build-push-action@v6
env:
@@ -89,7 +69,6 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
@@ -104,7 +83,6 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
@@ -119,34 +97,5 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Build and push Storybook Docker image
uses: docker/build-push-action@v6
env:
DOCKER_IMAGE: 'storybook'
BUNDLE_PATH: './bundle-storybook'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.storybook
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 🐳 *[PENPOT] Error building penpot docker images.*
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
📦 Bundle: `${{ steps.bundles.outputs.bundle_version }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra

View File

@@ -26,7 +26,7 @@ jobs:
- name: Check Commit Type
uses: gsactions/commit-message-checker@v2
with:
pattern: '^(((:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle|rewind|construction_worker):)\s[A-Z].*[^.])|(Merge|Revert).+[^.])$'
pattern: '^(Merge|Revert|:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle|rewind|construction_worker):)\s["A-Z].*[^.]$'
flags: 'gm'
error: 'Commit should match CONTRIBUTING.md guideline'
checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request

View File

@@ -60,12 +60,12 @@ jobs:
EXTRA_TAGS=("main" "latest")
for image in "${IMAGES[@]}"; do
docker pull "$REGISTRY/$image:$TAG"
docker tag "$REGISTRY/$image:$TAG" "penpotapp/$image:$TAG"
docker pull "$REGISTRY/penpotapp/$image:$TAG"
docker tag "$REGISTRY/penpotapp/$image:$TAG" "penpotapp/$image:$TAG"
docker push "penpotapp/$image:$TAG"
for tag in "${EXTRA_TAGS[@]}"; do
docker tag "$REGISTRY/$image:$TAG" "penpotapp/$image:$tag"
docker tag "$REGISTRY/penpotapp/$image:$TAG" "penpotapp/$image:$tag"
docker push "penpotapp/$image:$tag"
done
done
@@ -93,15 +93,3 @@ jobs:
tag_name: ${{ steps.vars.outputs.gh_ref }}
name: ${{ steps.vars.outputs.gh_ref }}
body: ${{ steps.extract_release_notes.outputs.release_notes }}
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 🚀 *[PENPOT] Error releasing penpot.*
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra

View File

@@ -9,20 +9,11 @@
### :heart: Community contributions (Thank you!)
### :sparkles: New features & Enhancements
- Select boards to export as PDF [Taiga #12320](https://tree.taiga.io/project/penpot/issue/12320)
### :bug: Bugs fixed
- Fix text line-height values are wrong [Taiga #12252](https://tree.taiga.io/project/penpot/issue/12252)
- Fix an error translation [Taiga #12402](https://tree.taiga.io/project/penpot/issue/12402)
- Fix pan cursor not disabling viewport guides [Github #6985](https://github.com/penpot/penpot/issues/6985)
- Fix viewport resize on locked shapes [Taiga #11974](https://tree.taiga.io/project/penpot/issue/11974)
- Fix nested variant in a component doesn't keep inherited overrides [Taiga #12299](https://tree.taiga.io/project/penpot/issue/12299)
- Fix on copy instance inside a components chain touched are missing [Taiga #12371](https://tree.taiga.io/project/penpot/issue/12371)
- Fix problem with multiple selection and shadows [Github #7437](https://github.com/penpot/penpot/issues/7437)
- Fix search shortcut [Taiga #10265](https://tree.taiga.io/project/penpot/issue/10265)
- Fix shortcut conflict in text editor (increase/decrease font size vs word selection)
- Fix problem with plugins generating code for pages different than current one [Taiga #12312](https://tree.taiga.io/project/penpot/issue/12312)
## 2.11.0 (Unreleased)
@@ -65,7 +56,6 @@
- Alternative ways of creating variants - Button Viewport [Taiga #11931](https://tree.taiga.io/project/penpot/us/11931)
- Reorder properties for a component [Taiga #10225](https://tree.taiga.io/project/penpot/us/10225)
- File Data storage layout refactor [Github #7345](https://github.com/penpot/penpot/pull/7345)
- Make several queries optimization on comment threads [Github #7506](https://github.com/penpot/penpot/pull/7506)
### :bug: Bugs fixed
@@ -80,26 +70,7 @@
- Fix auto-width changes to fixed when switching variants [Taiga #12172](https://tree.taiga.io/project/penpot/issue/12172)
- Fix component number has no singular translation string [Taiga #12106](https://tree.taiga.io/project/penpot/issue/12106)
- Fix adding/removing identical text fills [Taiga #12287](https://tree.taiga.io/project/penpot/issue/12287)
- Fix scroll on the inspect tab [Taiga #12293](https://tree.taiga.io/project/penpot/issue/12293)
- Fix lock proportion tooltip [Taiga #12326](https://tree.taiga.io/project/penpot/issue/12326)
- Fix internal Error when selecting a set by name in the token theme editor [Taiga #12310](https://tree.taiga.io/project/penpot/issue/12310)
- Fix drag & drop functionality is swapping instead or reordering [Taiga #12254](https://tree.taiga.io/project/penpot/issue/12254)
- Fix variants not syncronizing tokens on switch [Taiga #12290](https://tree.taiga.io/project/penpot/issue/12290)
- Fix incorrect behavior of Alt + Drag for variants [Taiga #12309](https://tree.taiga.io/project/penpot/issue/12309)
- Fix text override is lost after switch [Taiga #12269](https://tree.taiga.io/project/penpot/issue/12269)
- Fix exporting a board crashing the app [Taiga #12384](https://tree.taiga.io/project/penpot/issue/12384)
- Fix nested variant in a component doesn't keep inherited overrides [Taiga #12299](https://tree.taiga.io/project/penpot/issue/12299)
- Fix selected colors not showing colors from children shapes in multiple selection [Taiga #12384](https://tree.taiga.io/project/penpot/issue/12385)
- Fix scrollbar issue in design tab [Taiga #12367](https://tree.taiga.io/project/penpot/issue/12367)
- Fix library update notificacions showing when they should not [Taiga #12397](https://tree.taiga.io/project/penpot/issue/12397)
- Fix remove flex button doesnt work within variant [Taiga #12314](https://tree.taiga.io/project/penpot/issue/12314)
- Fix an error translation [Taiga #12402](https://tree.taiga.io/project/penpot/issue/12402)
- Fix problem with certain text input in some editable labels (pages, components, tokens...) being in conflict with the drag/drop functionality [Taiga #12316](https://tree.taiga.io/project/penpot/issue/12316)
- Fix not controlled theme renaming [Taiga #12411](https://tree.taiga.io/project/penpot/issue/12411)
- Fix paste without selection sends the new element in the back [Taiga #12382](https://tree.taiga.io/project/penpot/issue/12382)
- Fix options button does not work for comments created in the lower part of the screen [Taiga #12422](https://tree.taiga.io/project/penpot/issue/12422)
- Fix problem when checking usage with removed teams [Taiga #12442](https://tree.taiga.io/project/penpot/issue/12442)
- Fix focus mode persisting across page/file navigation [Taiga #12469](https://tree.taiga.io/project/penpot/issue/12469)
## 2.10.1
@@ -107,10 +78,12 @@
- Improve workpace file loading [Github 7366](https://github.com/penpot/penpot/pull/7366)
### :bug: Bugs fixed
- Fix regression with text shapes creation with Plugins API [Taiga #12244](https://tree.taiga.io/project/penpot/issue/12244)
## 2.10.0
### :rocket: Epics and highlights
@@ -126,7 +99,7 @@
- Add efficiency enhancements to right sidebar [Github #7182](https://github.com/penpot/penpot/pull/7182)
- Add defaults for artboard drawing [Taiga #494](https://tree.taiga.io/project/penpot/us/494?milestone=465047)
- Continuous display of distances between elements when moving a layer with the keyboard [Taiga #1780](https://tree.taiga.io/project/penpot/us/1780)
- New Number token - unitless values [Taiga #10936](https://tree.taiga.io/project/penpot/us/10936)
- New Number token - unitless values [Taiga #10936](https://tree.taiga.io/project/penpot/us/10936)
- New font-family token [Taiga #10937](https://tree.taiga.io/project/penpot/us/10937)
- New text case token [Taiga #10942](https://tree.taiga.io/project/penpot/us/10942)
- New text-decoration token [Taiga #10941](https://tree.taiga.io/project/penpot/us/10941)
@@ -207,6 +180,7 @@
- Add info to apply-token event [Taiga #11710](https://tree.taiga.io/project/penpot/task/11710)
- Fix double click on set name input [Taiga #11747](https://tree.taiga.io/project/penpot/issue/11747)
### :bug: Bugs fixed
- Copying font size does not copy the unit [Taiga #11143](https://tree.taiga.io/project/penpot/issue/11143)

View File

@@ -28,8 +28,8 @@
com.google.guava/guava {:mvn/version "33.4.8-jre"}
funcool/yetti
{:git/tag "v11.8"
:git/sha "1d1b33f"
{:git/tag "v11.6"
:git/sha "94dc017"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}

View File

@@ -8,41 +8,38 @@
<body>
<p>
<strong>Feedback from:</strong><br />
<span>
<span>Name: </span>
<span><code>{{profile.fullname|abbreviate:25}}</code></span>
</span>
<br />
<span>
<span>Email: </span>
<span>{{profile.email}}</span>
</span>
<br />
<span>
<span>ID: </span>
<span><code>{{profile.id}}</code></span>
</span>
{% if profile %}
<span>
<span>Name: </span>
<span><code>{{profile.fullname|abbreviate:25}}</code></span>
</span>
<br />
<span>
<span>Email: </span>
<span>{{profile.email}}</span>
</span>
<br />
<span>
<span>ID: </span>
<span><code>{{profile.id}}</code></span>
</span>
{% else %}
<span>
<span>Email: </span>
<span>{{profile.email}}</span>
</span>
{% endif %}
</p>
<p>
<strong>Subject:</strong><br />
<span>{{feedback-subject|abbreviate:300}}</span>
<span>{{subject|abbreviate:300}}</span>
</p>
<p>
<strong>Type:</strong><br />
<span>{{feedback-type|abbreviate:300}}</span>
</p>
{% if feedback-error-href %}
<p>
<strong>Error HREF:</strong><br />
<span>{{feedback-error-href|abbreviate:500}}</span>
</p>
{% endif %}
<p>
<strong>Message:</strong><br />
{{feedback-content|linebreaks-br}}
{{content|linebreaks-br|safe}}
</p>
</body>
</html>

View File

@@ -1 +1 @@
[PENPOT FEEDBACK]: {{feedback-subject}}
[PENPOT FEEDBACK]: {{subject}}

View File

@@ -1,10 +1,9 @@
From: {{profile.fullname}} <{{profile.email}}> / {{profile.id}}
Subject: {{feedback-subject}}
Type: {{feedback-type}}
{%- if feedback-error-href %}
HREF: {{feedback-error-href}}
{% endif -%}
{% if profile %}
Feedback profile: {{profile.fullname}} <{{profile.email}}> / {{profile.id}}
{% else %}
Feedback from: {{email}}
{% endif %}
Message:
Subject: {{subject}}
{{feedback-content}}
{{content}}

View File

@@ -20,7 +20,6 @@ export PENPOT_FLAGS="\
enable-audit-log \
enable-transit-readable-response \
enable-demo-users \
enable-user-feedback \
disable-secure-session-cookies \
enable-smtp \
enable-prepl-server \
@@ -47,8 +46,6 @@ export PENPOT_MEDIA_MAX_FILE_SIZE=104857600
# Setup default multipart upload size to 300MiB
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=314572800
export PENPOT_USER_FEEDBACK_DESTINATION="support@example.com"
export AWS_ACCESS_KEY_ID=penpot-devenv
export AWS_SECRET_ACCESS_KEY=penpot-devenv
export PENPOT_OBJECTS_STORAGE_BACKEND=s3

View File

@@ -550,7 +550,7 @@
[cfg data file-id]
(let [library-ids (get-libraries cfg [file-id])]
(reduce (fn [data library-id]
(if-let [library (get-file cfg library-id :include-deleted? true)]
(if-let [library (get-file cfg library-id)]
(ctf/absorb-assets data (:data library))
data))
data
@@ -749,7 +749,7 @@
l.version
FROM libs AS l
INNER JOIN project AS p ON (p.id = l.project_id)
WHERE l.deleted_at IS NULL;")
WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
(defn get-file-libraries
[conn file-id]

View File

@@ -228,7 +228,6 @@
(db/tx-run! cfg (fn [cfg]
(cond-> (bfc/get-file cfg file-id
{:realize? true
:include-deleted? true
:lock-for-update? true})
detach?
(-> (ctf/detach-external-references file-id)
@@ -286,12 +285,14 @@
(let [file (cond-> (select-keys file bfc/file-attrs)
(:options data)
(assoc :options (:options data)))
(assoc :options (:options data))
file (-> file
(dissoc :data)
(dissoc :deleted-at)
(encode-file))
:always
(dissoc :data))
file (cond-> file
:always
(encode-file))
path (str "files/" file-id ".json")]
(write-entry! output path file))

View File

@@ -319,9 +319,5 @@
([key default]
(c/get config key default)))
(defn logging-context
[]
{:version/backend (:full version)})
;; Set value for all new threads bindings.
(alter-var-root #'*assert* (constantly (contains? flags :backend-asserts)))

View File

@@ -704,12 +704,6 @@
(and (sql-exception? cause)
(= "40001" (.getSQLState ^java.sql.SQLException cause))))
(defn duplicate-key-error?
[cause]
(and (sql-exception? cause)
(= "23505" (.getSQLState ^java.sql.SQLException cause))))
(extend-protocol jdbc.prepare/SettableParameter
clojure.lang.Keyword
(set-parameter [^clojure.lang.Keyword v ^PreparedStatement s ^long i]

View File

@@ -7,7 +7,6 @@
(ns app.email
"Main api for send emails."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
@@ -94,44 +93,36 @@
headers)))
(defn- assign-body
[^MimeMessage mmsg {:keys [body charset attachments] :or {charset "utf-8"}}]
(let [mixed-mpart (MimeMultipart. "mixed")]
[^MimeMessage mmsg {:keys [body charset] :or {charset "utf-8"}}]
(let [mpart (MimeMultipart. "mixed")]
(cond
(string? body)
(let [text-part (MimeBodyPart.)]
(.setText text-part ^String body ^String charset)
(.addBodyPart mixed-mpart text-part))
(let [bpart (MimeBodyPart.)]
(.setContent bpart ^String body (str "text/plain; charset=" charset))
(.addBodyPart mpart bpart))
(vector? body)
(let [mmp (MimeMultipart. "alternative")
mbp (MimeBodyPart.)]
(.addBodyPart mpart mbp)
(.setContent mbp mmp)
(doseq [item body]
(let [mbp (MimeBodyPart.)]
(.setContent mbp
^String (:content item)
^String (str (:type item "text/plain") "; charset=" charset))
(.addBodyPart mmp mbp))))
(map? body)
(let [content-part (MimeBodyPart.)
alternative-mpart (MimeMultipart. "alternative")]
(when-let [content (get body "text/html")]
(let [html-part (MimeBodyPart.)]
(.setContent html-part ^String content
(str "text/html; charset=" charset))
(.addBodyPart alternative-mpart html-part)))
(when-let [content (get body "text/plain")]
(let [text-part (MimeBodyPart.)]
(.setText text-part ^String content ^String charset)
(.addBodyPart alternative-mpart text-part)))
(.setContent content-part alternative-mpart)
(.addBodyPart mixed-mpart content-part))
(let [bpart (MimeBodyPart.)]
(.setContent bpart
^String (:content body)
^String (str (:type body "text/plain") "; charset=" charset))
(.addBodyPart mpart bpart))
:else
(throw (IllegalArgumentException. "invalid email body provided")))
(doseq [[name content] attachments]
(prn "attachment" name)
(let [attachment-part (MimeBodyPart.)]
(.setFileName attachment-part ^String name)
(.setContent attachment-part ^String content (str "text/plain; charset=" charset))
(.addBodyPart mixed-mpart attachment-part)))
(.setContent mmsg mixed-mpart)
(throw (ex-info "Unsupported type" {:body body})))
(.setContent mmsg mpart)
mmsg))
(defn- opts->props
@@ -219,26 +210,24 @@
(ex/raise :type :internal
:code :missing-email-templates))
{:subject subj
:body (d/without-nils
{"text/plain" text
"text/html" html})}))
:body (into
[{:type "text/plain"
:content text}]
(when html
[{:type "text/html"
:content html}]))}))
(def ^:private schema:params
[:map {:title "Email Params"}
(def ^:private schema:context
[:map
[:to [:or ::sm/email [::sm/vec ::sm/email]]]
[:reply-to {:optional true} ::sm/email]
[:from {:optional true} ::sm/email]
[:lang {:optional true} ::sm/text]
[:subject {:optional true} ::sm/text]
[:priority {:optional true} [:enum :high :low]]
[:extra-data {:optional true} ::sm/text]
[:body {:optional true}
[:or :string [:map-of :string :string]]]
[:attachments {:optional true}
[:map-of :string :string]]])
[:extra-data {:optional true} ::sm/text]])
(def ^:private check-params
(sm/check-fn schema:params))
(def ^:private check-context
(sm/check-fn schema:context))
(defn template-factory
[& {:keys [id schema]}]
@@ -246,9 +235,9 @@
(let [check-fn (if schema
(sm/check-fn schema)
(constantly nil))]
(fn [params]
(let [params (-> params check-params check-fn)
email (build-email-template id params)]
(fn [context]
(let [context (-> context check-context check-fn)
email (build-email-template id context)]
(when-not email
(ex/raise :type :internal
:code :email-template-does-not-exists
@@ -256,40 +245,35 @@
:template-id id))
(cond-> (assoc email :id (name id))
(:extra-data params)
(assoc :extra-data (:extra-data params))
(:extra-data context)
(assoc :extra-data (:extra-data context))
(seq (:attachments params))
(assoc :attachments (:attachments params))
(:from context)
(assoc :from (:from context))
(:from params)
(assoc :from (:from params))
(:reply-to context)
(assoc :reply-to (:reply-to context))
(:reply-to params)
(assoc :reply-to (:reply-to params))
(:to params)
(assoc :to (:to params)))))))
(:to context)
(assoc :to (:to context)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC HIGH-LEVEL API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn render
[email-factory params]
(email-factory params))
[email-factory context]
(email-factory context))
(defn send!
"Schedule an already defined email to be sent using asynchronously
using worker task."
[{:keys [::conn ::factory] :as params}]
[{:keys [::conn ::factory] :as context}]
(assert (db/connectable? conn) "expected a valid database connection or pool")
(let [email (if factory
(factory params)
(-> params
(dissoc params)
(check-params)))]
(factory context)
(dissoc context ::conn))]
(wrk/submit! {::wrk/task :sendmail
::wrk/delay 0
::wrk/max-retries 4
@@ -359,10 +343,8 @@
(def ^:private schema:feedback
[:map
[:feedback-subject ::sm/text]
[:feedback-type ::sm/text]
[:feedback-content ::sm/text]
[:profile :map]])
[:subject ::sm/text]
[:content ::sm/text]])
(def user-feedback
"A profile feedback email."

View File

@@ -25,14 +25,15 @@
(let [claims (-> {}
(into (::session/token-claims request))
(into (::actoken/token-claims request)))]
(-> (cf/logging-context)
(assoc :request/path (:path request))
(assoc :request/method (:method request))
(assoc :request/params (:params request))
(assoc :request/user-agent (yreq/get-header request "user-agent"))
(assoc :request/ip-addr (inet/parse-request request))
(assoc :request/profile-id (:uid claims))
(assoc :version/frontend (or (yreq/get-header request "x-frontend-version") "unknown")))))
{:request/path (:path request)
:request/method (:method request)
:request/params (:params request)
:request/user-agent (yreq/get-header request "user-agent")
:request/ip-addr (inet/parse-request request)
:request/profile-id (:uid claims)
:version/frontend (or (yreq/get-header request "x-frontend-version") "unknown")
:version/backend (:full cf/version)}))
(defmulti handle-error
(fn [cause _ _]

View File

@@ -64,13 +64,9 @@
(let [mdata (meta result)
response (if (fn? result)
(result request)
(let [result (rph/unwrap result)
status (::http/status mdata 200)
headers (cond-> (::http/headers mdata {})
(yres/stream-body? result)
(assoc "content-type" "application/octet-stream"))]
{::yres/status status
::yres/headers headers
(let [result (rph/unwrap result)]
{::yres/status (::http/status mdata 200)
::yres/headers (::http/headers mdata {})
::yres/body result}))]
(-> response
(handle-response-transformation request mdata)

View File

@@ -315,13 +315,16 @@
(-> (db/insert! conn :profile params)
(profile/decode-row))
(catch org.postgresql.util.PSQLException cause
(if (db/duplicate-key-error? cause)
(ex/raise :type :validation
:code :email-already-exists
:hint "email already exists"
:cause cause)
(throw cause))))))
(let [state (.getSQLState cause)]
(if (not= state "23505")
(throw cause)
(do
(l/error :hint "not an error" :cause cause)
(ex/raise :type :validation
:code :email-already-exists
:hint "email already exists"
:cause cause))))))))
(defn create-profile-rels!
[conn {:keys [id] :as profile}]

View File

@@ -25,10 +25,10 @@
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.tasks.file-gc]
[app.util.services :as sv]
[app.worker :as-alias wrk]))
[app.worker :as-alias wrk]
[yetti.response :as yres]))
(set! *warn-on-reflection* true)
@@ -44,7 +44,7 @@
(defn stream-export-v1
[cfg {:keys [file-id include-libraries embed-assets] :as params}]
(rph/stream
(yres/stream-body
(fn [_ output-stream]
(try
(-> cfg
@@ -59,7 +59,7 @@
(defn stream-export-v3
[cfg {:keys [file-id include-libraries embed-assets] :as params}]
(rph/stream
(yres/stream-body
(fn [_ output-stream]
(try
(-> cfg
@@ -79,11 +79,16 @@
::sm/params schema:export-binfile}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id version file-id] :as params}]
(files/check-read-permissions! pool profile-id file-id)
(let [version (or version 1)]
(case (int version)
1 (stream-export-v1 cfg params)
2 (throw (ex-info "not-implemented" {}))
3 (stream-export-v3 cfg params))))
(fn [_]
(let [version (or version 1)
body (case (int version)
1 (stream-export-v1 cfg params)
2 (throw (ex-info "not-implemented" {}))
3 (stream-export-v3 cfg params))]
{::yres/status 200
::yres/headers {"content-type" "application/octet-stream"}
::yres/body body})))
;; --- Command: import-binfile

View File

@@ -234,39 +234,36 @@
(files/check-comment-permissions! conn profile-id file-id share-id)
(get-comment-threads conn profile-id file-id))))
(defn- get-comment-threads-sql
[where]
(str/ffmt
"SELECT DISTINCT ON (ct.id)
ct.*,
pf.fullname AS owner_fullname,
pf.email AS owner_email,
pf.photo_id AS owner_photo_id,
p.team_id AS team_id,
f.name AS file_name,
f.project_id AS project_id,
first_value(c.content) OVER w AS content,
(SELECT count(1)
FROM comment AS c
WHERE c.thread_id = ct.id) AS count_comments,
(SELECT count(1)
FROM comment AS c
WHERE c.thread_id = ct.id
AND c.created_at >= coalesce(cts.modified_at, ct.created_at)) AS count_unread_comments
FROM comment_thread AS ct
INNER JOIN comment AS c ON (c.thread_id = ct.id)
INNER JOIN file AS f ON (f.id = ct.file_id)
INNER JOIN project AS p ON (p.id = f.project_id)
LEFT JOIN comment_thread_status AS cts ON (cts.thread_id = ct.id AND cts.profile_id = ?)
LEFT JOIN profile AS pf ON (ct.owner_id = pf.id)
WHERE f.deleted_at IS NULL
AND p.deleted_at IS NULL
%1
WINDOW w AS (PARTITION BY c.thread_id ORDER BY c.created_at ASC)"
where))
(def ^:private sql:comment-threads
"SELECT DISTINCT ON (ct.id)
ct.*,
pf.fullname AS owner_fullname,
pf.email AS owner_email,
pf.photo_id AS owner_photo_id,
p.team_id AS team_id,
f.name AS file_name,
f.project_id AS project_id,
first_value(c.content) OVER w AS content,
(SELECT count(1)
FROM comment AS c
WHERE c.thread_id = ct.id) AS count_comments,
(SELECT count(1)
FROM comment AS c
WHERE c.thread_id = ct.id
AND c.created_at >= coalesce(cts.modified_at, ct.created_at)) AS count_unread_comments
FROM comment_thread AS ct
INNER JOIN comment AS c ON (c.thread_id = ct.id)
INNER JOIN file AS f ON (f.id = ct.file_id)
INNER JOIN project AS p ON (p.id = f.project_id)
LEFT JOIN comment_thread_status AS cts ON (cts.thread_id = ct.id AND cts.profile_id = ?)
LEFT JOIN profile AS pf ON (ct.owner_id = pf.id)
WHERE f.deleted_at IS NULL
AND p.deleted_at IS NULL
WINDOW w AS (PARTITION BY c.thread_id ORDER BY c.created_at ASC)")
(def ^:private sql:comment-threads-by-file-id
(get-comment-threads-sql "AND ct.file_id = ?"))
(str "WITH threads AS (" sql:comment-threads ")"
"SELECT * FROM threads WHERE file_id = ?"))
(defn- get-comment-threads
[conn profile-id file-id]
@@ -276,29 +273,34 @@
;; --- COMMAND: Get Unread Comment Threads
(def ^:private sql:unread-all-comment-threads-by-team
(str "WITH threads AS ("
(get-comment-threads-sql "AND p.team_id = ?")
")"
"SELECT t.* FROM threads AS t
WHERE t.count_unread_comments > 0"))
(str "WITH threads AS (" sql:comment-threads ")"
"SELECT * FROM threads WHERE count_unread_comments > 0 AND team_id = ?"))
;; The partial configuration will retrieve only comments created by the user and
;; threads that have a mention to the user.
(def ^:private sql:unread-partial-comment-threads-by-team
(str "WITH threads AS ("
(get-comment-threads-sql "AND p.team_id = ? AND (ct.owner_id = ? OR ? = ANY(ct.mentions))")
")"
"SELECT t.* FROM threads AS t
WHERE t.count_unread_comments > 0"))
(str "WITH threads AS (" sql:comment-threads ")"
"SELECT * FROM threads
WHERE count_unread_comments > 0
AND team_id = ?
AND (owner_id = ? OR ? = ANY(mentions))"))
(defn- get-unread-comment-threads
[cfg profile-id team-id]
(let [profile (-> (db/get cfg :profile {:id profile-id} ::db/remove-deleted false)
(let [profile (-> (db/get cfg :profile {:id profile-id})
(profile/decode-row))
notify (or (-> profile :props :notifications :dashboard-comments) :all)
result (case notify
:all (db/exec! cfg [sql:unread-all-comment-threads-by-team profile-id team-id])
:partial (db/exec! cfg [sql:unread-partial-comment-threads-by-team profile-id team-id profile-id profile-id])
[])]
(into [] xf-decode-row result)))
notify (or (-> profile :props :notifications :dashboard-comments) :all)]
(case notify
:all
(->> (db/exec! cfg [sql:unread-all-comment-threads-by-team profile-id team-id])
(into [] xf-decode-row))
:partial
(->> (db/exec! cfg [sql:unread-partial-comment-threads-by-team profile-id team-id profile-id profile-id])
(into [] xf-decode-row))
[])))
(def ^:private
schema:get-unread-comment-threads
@@ -321,17 +323,16 @@
[:id ::sm/uuid]
[:share-id {:optional true} [:maybe ::sm/uuid]]])
(def ^:private sql:get-comment-thread
(get-comment-threads-sql "AND ct.file_id = ? AND ct.id = ?"))
(sv/defmethod ::get-comment-thread
{::doc/added "1.15"
::sm/params schema:get-comment-thread}
[cfg {:keys [::rpc/profile-id file-id id share-id] :as params}]
(db/run! cfg (fn [{:keys [::db/conn]}]
(files/check-comment-permissions! conn profile-id file-id share-id)
(some-> (db/exec-one! conn [sql:get-comment-thread profile-id file-id id])
(decode-row)))))
(let [sql (str "WITH threads AS (" sql:comment-threads ")"
"SELECT * FROM threads WHERE id = ? AND file_id = ?")]
(-> (db/exec-one! conn [sql profile-id id file-id])
(decode-row))))))
;; --- COMMAND: Retrieve Comments

View File

@@ -45,7 +45,6 @@
params {:email email
:fullname fullname
:is-active true
:is-demo true
:deleted-at (ct/in-future (cf/get-deletion-delay))
:password (derive-password password)
:props {}}

View File

@@ -7,7 +7,6 @@
(ns app.rpc.commands.feedback
"A general purpose feedback module."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.schema :as sm]
[app.config :as cf]
@@ -22,11 +21,8 @@
(def ^:private schema:send-user-feedback
[:map {:title "send-user-feedback"}
[:subject [:string {:max 500}]]
[:content [:string {:max 2500}]]
[:type {:optional true} :string]
[:error-href {:optional true} [:string {:max 2500}]]
[:error-report {:optional true} :string]])
[:subject [:string {:max 400}]]
[:content [:string {:max 2500}]]])
(sv/defmethod ::send-user-feedback
{::doc/added "1.18"
@@ -43,26 +39,16 @@
(defn- send-user-feedback!
[pool profile params]
(let [destination
(or (cf/get :user-feedback-destination)
;; LEGACY
(cf/get :feedback-destination))
attachments
(d/without-nils
{"error-report.txt" (:error-report params)})]
(let [dest (or (cf/get :user-feedback-destination)
;; LEGACY
(cf/get :feedback-destination))]
(eml/send! {::eml/conn pool
::eml/factory eml/user-feedback
:from (cf/get :smtp-default-from)
:to destination
:from dest
:to dest
:profile profile
:reply-to (:email profile)
:email (:email profile)
:attachments attachments
:feedback-subject (:subject params)
:feedback-type (:type params "not-specified")
:feedback-content (:content params)
:feedback-error-href (:error-href params)
:profile profile})
:subject (:subject params)
:content (:content params)})
nil))

View File

@@ -26,7 +26,6 @@
[app.db.sql :as-alias sql]
[app.features.fdata :as feat.fdata]
[app.features.logical-deletion :as ldel]
[app.http.sse :as sse]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.msgbus :as mbus]
@@ -39,7 +38,6 @@
[app.rpc.helpers :as rph]
[app.rpc.permissions :as perms]
[app.util.blob :as blob]
[app.util.events :as events]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.worker :as wrk]
@@ -355,8 +353,9 @@
::sm/params schema:get-project-files
::sm/result schema:files}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id]}]
(projects/check-read-permissions! pool profile-id project-id)
(get-project-files pool project-id))
(dm/with-open [conn (db/open pool)]
(projects/check-read-permissions! conn profile-id project-id)
(get-project-files conn project-id)))
;; --- COMMAND QUERY: has-file-libraries
@@ -425,6 +424,7 @@
;; --- QUERY COMMAND: get-page
(defn- prune-objects
"Given the page data and the object-id returns the page data with all
other not needed objects removed from the `:objects` data
@@ -765,54 +765,6 @@
(teams/check-read-permissions! conn profile-id team-id)
(get-team-recent-files conn team-id)))
;; --- COMMAND QUERY: get-team-deleted-files
(def sql:team-deleted-files
"WITH deleted_files AS (
SELECT f.id,
f.revn,
f.vern,
f.project_id,
f.created_at,
f.modified_at,
f.name,
f.is_shared,
f.deleted_at AS will_be_deleted_at,
ft.media_id AS thumbnail_id,
row_number() OVER w AS row_num,
p.team_id
FROM file AS f
INNER JOIN project AS p ON (p.id = f.project_id)
LEFT JOIN file_thumbnail AS ft on (ft.file_id = f.id
AND ft.revn = f.revn
AND ft.deleted_at is null)
WHERE p.team_id = ?
AND (p.deleted_at > ?::timestamptz OR
f.deleted_at > ?::timestamptz)
WINDOW w AS (PARTITION BY f.project_id
ORDER BY f.modified_at DESC)
ORDER BY f.modified_at DESC
)
SELECT * FROM deleted_files")
(defn get-team-deleted-files
[conn team-id]
(let [now (ct/now)]
(db/exec! conn [sql:team-deleted-files team-id now now])))
(def ^:private schema:get-team-deleted-files
[:map {:title "get-team-deleted-files"}
[:team-id ::sm/uuid]])
(sv/defmethod ::get-team-deleted-files
{::doc/added "2.12"
::sm/params schema:get-team-deleted-files}
[cfg {:keys [::rpc/profile-id team-id]}]
(db/run! cfg (fn [{:keys [::db/conn]}]
(teams/check-read-permissions! conn profile-id team-id)
(get-team-deleted-files conn team-id))))
;; --- COMMAND QUERY: get-file-info
@@ -1161,118 +1113,3 @@
(check-edition-permissions! conn profile-id file-id)
(-> (ignore-sync conn params)
(update :features db/decode-pgarray #{})))
;; --- MUTATION COMMAND: delete-files-immediatelly
(def ^:private sql:delete-team-files
"UPDATE file AS uf SET deleted_at = ?::timestamptz
FROM (
SELECT f.id
FROM file AS f
JOIN project AS p ON (p.id = f.project_id)
JOIN team AS t ON (t.id = p.team_id)
WHERE t.deleted_at IS NULL
AND t.id = ?
AND f.id = ANY(?::uuid[])
) AS subquery
WHERE uf.id = subquery.id
RETURNING uf.id, uf.deleted_at;")
(def ^:private schema:permanently-delete-team-files
[:map {:title "permanently-delete-team-files"}
[:team-id ::sm/uuid]
[:ids [::sm/set ::sm/uuid]]])
(sv/defmethod ::permanently-delete-team-files
"Mark the specified files to be deleted immediatelly on the
specified team. The team-id on params will be used to filter and
check writable permissons on team."
{::doc/added "2.12"
::sm/params schema:permanently-delete-team-files
::db/transaction true}
[{:keys [::db/conn]} {:keys [::rpc/profile-id ::rpc/request-at team-id ids]}]
(teams/check-edition-permissions! conn profile-id team-id)
(reduce (fn [acc {:keys [id deleted-at]}]
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :file
:deleted-at deleted-at
:id id}})
(conj acc id))
#{}
(db/plan conn [sql:delete-team-files request-at team-id
(db/create-array conn "uuid" ids)])))
;; --- MUTATION COMMAND: restore-files-immediatelly
(def ^:private sql:resolve-editable-files
"SELECT f.id
FROM file AS f
JOIN project AS p ON (p.id = f.project_id)
JOIN team AS t ON (t.id = p.team_id)
WHERE t.deleted_at IS NULL
AND t.id = ?
AND f.id = ANY(?::uuid[])")
(defn- restore-file
[conn file-id]
(db/update! conn :file
{:deleted-at nil
:has-media-trimmed false}
{:id file-id}
{::db/return-keys false})
(db/update! conn :file-media-object
{:deleted-at nil}
{:file-id file-id}
{::db/return-keys false})
(db/update! conn :file-change
{:deleted-at nil}
{:file-id file-id}
{::db/return-keys false})
(db/update! conn :file-data
{:deleted-at nil}
{:file-id file-id}
{::db/return-keys false})
(db/update! conn :file-thumbnail
{:deleted-at nil}
{:file-id file-id}
{::db/return-keys false})
(db/update! conn :file-tagged-object-thumbnail
{:deleted-at nil}
{:file-id file-id}
{::db/return-keys false}))
(defn- restore-deleted-team-files
[{:keys [::db/conn]} {:keys [::rpc/profile-id team-id ids]}]
(teams/check-edition-permissions! conn profile-id team-id)
(reduce (fn [affected {:keys [id]}]
(let [index (inc (count affected))]
(events/tap :progress {:file-id id :index index :total (count ids)})
(restore-file conn id)
(conj affected id)))
#{}
(db/plan conn [sql:resolve-editable-files team-id
(db/create-array conn "uuid" ids)])))
(def ^:private schema:restore-deleted-team-files
[:map {:title "restore-deleted-team-files"}
[:team-id ::sm/uuid]
[:ids [::sm/set ::sm/uuid]]])
(sv/defmethod ::restore-deleted-team-files
"Removes the deletion mark from the specified files (and respective projects)."
{::doc/added "2.12"
::sse/stream? true
::sm/params schema:restore-deleted-team-files}
[cfg params]
(sse/response #(db/tx-run! cfg restore-deleted-team-files params)))

View File

@@ -107,9 +107,7 @@
(defn get-profile
"Get profile by id. Throws not-found exception if no profile found."
[conn id & {:as opts}]
;; NOTE: We need to set ::db/remove-deleted to false because demo profiles
;; are created with a set deleted-at value
(-> (db/get-by-id conn :profile id (assoc opts ::db/remove-deleted false))
(-> (db/get-by-id conn :profile id opts)
(decode-row)))
;; --- MUTATION: Update Profile (own)
@@ -475,16 +473,13 @@
p.fullname AS name,
p.email AS email
FROM team_profile_rel AS tpr1
JOIN team as t
ON tpr1.team_id = t.id
JOIN team_profile_rel AS tpr2
ON (tpr1.team_id = tpr2.team_id)
JOIN profile AS p
ON (tpr2.profile_id = p.id)
WHERE tpr1.profile_id = ?
AND tpr1.is_owner IS true
AND tpr2.can_edit IS true
AND t.deleted_at IS NULL")
AND tpr2.can_edit IS true")
(sv/defmethod ::get-subscription-usage
{::doc/added "2.9"}

View File

@@ -70,27 +70,7 @@
;; --- QUERY: Get projects
(def ^:private sql:projects
"SELECT p.*,
coalesce(tpp.is_pinned, false) as is_pinned,
(SELECT count(*) FROM file AS f
WHERE f.project_id = p.id
AND f.deleted_at is null) AS count,
(SELECT count(*) FROM file AS f
WHERE f.project_id = p.id) AS total_count
FROM project AS p
INNER JOIN team AS t ON (t.id = p.team_id)
LEFT JOIN team_project_profile_rel AS tpp
ON (tpp.project_id = p.id AND
tpp.team_id = p.team_id AND
tpp.profile_id = ?)
WHERE p.team_id = ?
AND t.deleted_at is null
ORDER BY p.modified_at DESC")
(defn get-projects
[conn profile-id team-id]
(db/exec! conn [sql:projects profile-id team-id]))
(declare get-projects)
(def ^:private schema:get-projects
[:map {:title "get-projects"}
@@ -98,11 +78,32 @@
(sv/defmethod ::get-projects
{::doc/added "1.18"
::doc/changes [["2.12" "This endpoint now return deleted but recoverable projects"]]
::sm/params schema:get-projects}
[cfg {:keys [::rpc/profile-id team-id]}]
(teams/check-read-permissions! cfg profile-id team-id)
(get-projects cfg profile-id team-id))
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id]}]
(dm/with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(get-projects conn profile-id team-id)))
(def sql:projects
"select p.*,
coalesce(tpp.is_pinned, false) as is_pinned,
(select count(*) from file as f
where f.project_id = p.id
and deleted_at is null) as count
from project as p
inner join team as t on (t.id = p.team_id)
left join team_project_profile_rel as tpp
on (tpp.project_id = p.id and
tpp.team_id = p.team_id and
tpp.profile_id = ?)
where p.team_id = ?
and p.deleted_at is null
and t.deleted_at is null
order by p.modified_at desc")
(defn get-projects
[conn profile-id team-id]
(db/exec! conn [sql:projects profile-id team-id]))
;; --- QUERY: Get all projects

View File

@@ -37,14 +37,14 @@
;; --- Helpers & Specs
(def ^:private sql:team-permissions
"SELECT tpr.is_owner,
"select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
FROM team_profile_rel AS tpr
JOIN team AS t ON (t.id = tpr.team_id)
WHERE tpr.profile_id = ?
AND tpr.team_id = ?
AND t.deleted_at IS NULL")
from team_profile_rel as tpr
join team as t on (t.id = tpr.team_id)
where tpr.profile_id = ?
and tpr.team_id = ?
and t.deleted_at is null")
(defn get-permissions
[conn profile-id team-id]

View File

@@ -11,7 +11,7 @@
[app.common.data.macros :as dm]
[app.http :as-alias http]
[app.rpc :as-alias rpc]
[yetti.response :as yres]))
[yetti.response :as-alias yres]))
;; A utilty wrapper object for wrap service responses that does not
;; implements the IObj interface that make possible attach metadata to
@@ -78,8 +78,3 @@
(let [exp (if (integer? max-age) max-age (inst-ms max-age))
val (dm/fmt "max-age=%" (int (/ exp 1000.0)))]
(update response ::yres/headers assoc "cache-control" val)))))
(defn stream
"A convenience allias for yetti.response/stream-body"
[f]
(yres/stream-body f))

View File

@@ -102,7 +102,8 @@
::wrk/label "quotes-notification"
::wrk/params {:to (vec admins)
:subject subject
:body content}}))))
:body [{:type "text/plain"
:content content}]}}))))
(defn- generic-check!
[{:keys [::db/conn ::incr ::quote-sql ::count-sql ::default ::target] :or {incr 1} :as params}]

View File

@@ -14,9 +14,7 @@
[integrant.core :as ig])
(:import
java.time.Clock
java.time.Duration
java.time.Instant
java.time.ZoneId))
java.time.Duration))
(defonce current
(atom {:clock (Clock/systemDefaultZone)
@@ -38,12 +36,6 @@
[_ _]
(remove-watch current ::common))
(defn fixed
"Get fixed clock, mainly used in tests"
[instant]
(Clock/fixed ^Instant (ct/inst instant)
^ZoneId (ZoneId/of "Z")))
(defn set-offset!
[duration]
(swap! current assoc :offset (some-> duration ct/duration)))

View File

@@ -567,12 +567,48 @@
:id file-id})))
:deleted))
(defn- restore-file*
[{:keys [::db/conn]} file-id]
(db/update! conn :file
{:deleted-at nil
:has-media-trimmed false}
{:id file-id}
{::db/return-keys false})
(db/update! conn :file-media-object
{:deleted-at nil}
{:file-id file-id}
{::db/return-keys false})
(db/update! conn :file-change
{:deleted-at nil}
{:file-id file-id}
{::db/return-keys false})
(db/update! conn :file-data
{:deleted-at nil}
{:file-id file-id}
{::db/return-keys false})
;; Mark thumbnails to be deleted
(db/update! conn :file-thumbnail
{:deleted-at nil}
{:file-id file-id}
{::db/return-keys false})
(db/update! conn :file-tagged-object-thumbnail
{:deleted-at nil}
{:file-id file-id}
{::db/return-keys false})
:restored)
(defn restore-file!
"Mark a file and all related objects as not deleted"
[file-id]
(let [file-id (h/parse-uuid file-id)]
(db/tx-run! main/system
(fn [{:keys [::db/conn] :as system}]
(fn [system]
(when-let [file (db/get* system :file
{:id file-id}
{::db/remove-deleted false
@@ -586,9 +622,7 @@
:cause "explicit call to restore-file!"}
::audit/tracked-at (ct/now)})
(#'files/restore-file conn file-id))
:restored))))
(restore-file* system file-id))))))
(defn delete-project!
"Mark a project for deletion"
@@ -621,7 +655,7 @@
(doseq [{:keys [id]} (db/query conn :file
{:project-id project-id}
{::sql/columns [:id]})]
(#'files/restore-file conn id))
(restore-file* cfg id))
:restored)

View File

@@ -13,7 +13,6 @@
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.transit :as t]
[app.config :as cf]
[app.db :as db]
[app.metrics :as mtx]
[app.redis :as rds]
@@ -61,8 +60,7 @@
(defn get-error-context
[_ item]
(-> (cf/logging-context)
(assoc :params item)))
{:params item})
(defn- get-task
[{:keys [::db/pool]} task-id]
@@ -133,11 +131,6 @@
[{:keys [::id ::timeout] :as cfg} task-id scheduled-at]
(loop [task (get-task cfg task-id)]
(cond
(nil? task)
(l/wrn :hint "no task found on the database"
:runner-id id
:task-id task-id)
(ex/exception? task)
(if (or (db/connection-error? task)
(db/serialization-error? task))
@@ -160,6 +153,11 @@
:task-id task-id
:runner-id id)
(nil? task)
(l/wrn :hint "no task found on the database"
:runner-id id
:task-id task-id)
:else
(let [result (run-task cfg task)]
(with-meta result
@@ -215,7 +213,6 @@
:payload payload)))
(catch Throwable cause
(l/err :hint "unable to decode payload"
::l/context (cf/logging-context)
:payload payload
:length (alength ^String/1 payload)
:cause cause))))
@@ -227,11 +224,11 @@
"failed" (handle-task-failure result)
"completed" (handle-task-completion result)
(throw (IllegalArgumentException.
(str "invalid status received: '" status "'"))))))
(str "invalid status received: " status))))))
(run-task-loop [[task-id scheduled-at]]
(loop [result (run-task! cfg task-id scheduled-at)]
(when-let [cause (some-> result process-result)]
(when-let [cause (process-result result)]
(if (or (db/connection-error? cause)
(db/serialization-error? cause))
(do
@@ -239,9 +236,9 @@
:cause cause)
(px/sleep timeout)
(recur result))
(l/err :hint "unhandled exception on processing task result"
::l/context (cf/logging-context)
:cause cause)))))]
(do
(l/err :hint "unhandled exception on processing task result"
:cause cause))))))]
(try
(let [key (str/ffmt "penpot.worker.queue:%" queue)
@@ -257,14 +254,11 @@
(if (rds/timeout-exception? cause)
(do
(l/err :hint "redis pop operation timeout, consider increasing redis timeout (will retry in some instants)"
::l/context (cf/logging-context)
:timeout timeout
:cause cause)
(px/sleep timeout))
(l/err :hint "unhandled exception"
::l/context (cf/logging-context)
:cause cause))))))
(l/err :hint "unhandled exception" :cause cause))))))
(defn- start-thread!
[{:keys [::id ::queue ::wrk/tenant] :as cfg}]
@@ -290,7 +284,6 @@
:queue queue))
(catch Throwable cause
(l/err :hint "unexpected exception"
::l/context (cf/logging-context)
:id id
:queue queue
:cause cause))

View File

@@ -22,4 +22,4 @@
(t/is (contains? result :body))
(t/is (contains? result :to))
#_(t/is (contains? result :reply-to))
(t/is (map? (:body result)))))
(t/is (vector? (:body result)))))

View File

@@ -19,7 +19,6 @@
[app.http :as http]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.setup.clock :as clock]
[app.storage :as sto]
[backend-tests.helpers :as th]
[clojure.test :as t]
@@ -143,112 +142,126 @@
(t/is (= 0 (count result))))))))
(t/deftest file-gc-with-fragments
(let [profile (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id profile)
:project-id (:default-project-id profile)
:is-shared false})
(letfn [(update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}]
(let [params {::th/type :update-file
::rpc/profile-id profile-id
:id file-id
:session-id (uuid/random)
:revn revn
:vern 0
:features cfeat/supported-features
:changes changes}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(:result out)))]
page-id (uuid/random)
shape-id (uuid/random)]
(let [profile (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id profile)
:project-id (:default-project-id profile)
:is-shared false})
;; Preventive file-gc
(t/is (true? (th/run-task! :file-gc {:file-id (:id file) :revn (:revn file)})))
page-id (uuid/random)
shape-id (uuid/random)]
;; Check the number of fragments before adding the page
(let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 2 (count rows))))
;; Preventive file-gc
(t/is (true? (th/run-task! :file-gc {:file-id (:id file) :revn (:revn file)})))
;; Add page
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-page
:name "test"
:id page-id}])
;; Check the number of fragments before adding the page
(let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 2 (count rows))))
;; Check the number of fragments before adding the page
(let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 3 (count rows))))
;; Add page
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-page
:name "test"
:id page-id}])
;; The file-gc should mark for remove unused fragments
(t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
;; Check the number of fragments before adding the page
(let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 3 (count rows))))
;; Check the number of fragments
(let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 5 (count rows)))
(t/is (= 3 (count (filterv :deleted-at rows)))))
;; The file-gc should mark for remove unused fragments
(t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
;; The objects-gc should remove unused fragments
(let [res (th/run-task! :objects-gc {})]
(t/is (= 3 (:processed res))))
;; Check the number of fragments
(let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 5 (count rows)))
(t/is (= 3 (count (filterv :deleted-at rows)))))
;; Check the number of fragments
(let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 2 (count rows))))
;; The objects-gc should remove unused fragments
(let [res (th/run-task! :objects-gc {})]
(t/is (= 3 (:processed res))))
;; Add shape to page that should add a new fragment
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-obj
:page-id page-id
:id shape-id
:parent-id uuid/zero
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id shape-id
:name "image"
:frame-id uuid/zero
:parent-id uuid/zero
:type :rect})}])
;; Check the number of fragments
(let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 2 (count rows))))
;; Check the number of fragments
(let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 3 (count rows))))
;; Add shape to page that should add a new fragment
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-obj
:page-id page-id
:id shape-id
:parent-id uuid/zero
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id shape-id
:name "image"
:frame-id uuid/zero
:parent-id uuid/zero
:type :rect})}])
;; The file-gc should mark for remove unused fragments
(t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
;; Check the number of fragments
(let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 3 (count rows))))
;; The objects-gc should remove unused fragments
(let [res (th/run-task! :objects-gc {})]
(t/is (= 3 (:processed res))))
;; The file-gc should mark for remove unused fragments
(t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
;; Check the number of fragments;
(let [rows (th/db-query :file-data {:file-id (:id file)
:type "fragment"
:deleted-at nil})]
(t/is (= 2 (count rows))))
;; The objects-gc should remove unused fragments
(let [res (th/run-task! :objects-gc {})]
(t/is (= 3 (:processed res))))
;; Lets proceed to delete all changes
(th/db-delete! :file-change {:file-id (:id file)})
(th/db-delete! :file-data {:file-id (:id file) :type "snapshot"})
;; Check the number of fragments;
(let [rows (th/db-query :file-data {:file-id (:id file)
:type "fragment"
:deleted-at nil})]
(t/is (= 2 (count rows))))
(th/db-update! :file
{:has-media-trimmed false}
{:id (:id file)})
;; Lets proceed to delete all changes
(th/db-delete! :file-change {:file-id (:id file)})
(th/db-delete! :file-data {:file-id (:id file) :type "snapshot"})
;; The file-gc should remove fragments related to changes
;; snapshots previously deleted.
(t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
(th/db-update! :file
{:has-media-trimmed false}
{:id (:id file)})
;; Check the number of fragments;
(let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
;; (pp/pprint rows)
(t/is (= 4 (count rows)))
(t/is (= 2 (count (remove :deleted-at rows)))))
;; The file-gc should remove fragments related to changes
;; snapshots previously deleted.
(t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
(let [res (th/run-task! :objects-gc {})]
(t/is (= 2 (:processed res))))
;; Check the number of fragments;
(let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
;; (pp/pprint rows)
(t/is (= 4 (count rows)))
(t/is (= 2 (count (remove :deleted-at rows)))))
(let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 2 (count rows))))))
(let [res (th/run-task! :objects-gc {})]
(t/is (= 2 (:processed res))))
(let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 2 (count rows)))))))
(t/deftest file-gc-with-thumbnails
(letfn [(add-file-media-object [& {:keys [profile-id file-id]}]
@@ -266,6 +279,20 @@
;; (th/print-result! out)
(t/is (nil? (:error out)))
(:result out)))
(update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}]
(let [params {::th/type :update-file
::rpc/profile-id profile-id
:id file-id
:session-id (uuid/random)
:revn revn
:vern 0
:features cfeat/supported-features
:changes changes}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(:result out)))]
(let [storage (:app.storage/storage th/*system*)
@@ -1866,125 +1893,3 @@
(t/is (= (:id file-2) (:file-id (get rows 0))))
(t/is (nil? (:deleted-at (get rows 0)))))))
(t/deftest deleted-files-permanently-delete
(let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
file-id (uuid/next)
now (ct/inst "2025-10-31T00:00:00Z")]
(binding [ct/*clock* (clock/fixed now)]
(let [data {::th/type :create-file
::rpc/profile-id (:id prof)
:project-id proj-id
:id file-id
:name "foobar"
:is-shared false
:components-v2 true}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= (:name data) (:name result)))
(t/is (= proj-id (:project-id result)))))
(let [data {::th/type :delete-file
:id file-id
::rpc/profile-id (:id prof)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
;; get deleted files
(let [data {::th/type :get-team-deleted-files
::rpc/profile-id (:id prof)
:team-id team-id}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [[row1 :as result] (:result out)]
(t/is (= 1 (count result)))
(t/is (= (:will-be-deleted-at row1) #penpot/inst "2025-11-07T00:00:00Z"))
(t/is (= (:created-at row1) #penpot/inst "2025-10-31T00:00:00Z"))
(t/is (= (:modified-at row1) #penpot/inst "2025-10-31T00:00:00Z"))))
(let [data {::th/type :permanently-delete-team-files
::rpc/profile-id (:id prof)
:team-id team-id
:ids #{file-id}}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= (:ids data) result)))
(let [row (th/db-exec-one! ["select * from file where id = ?" file-id])]
(t/is (= (:deleted-at row) now)))))))
(t/deftest deleted-files-restore
(let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
file-id (uuid/next)
now (ct/inst "2025-10-31T00:00:00Z")]
(binding [ct/*clock* (clock/fixed now)]
(let [data {::th/type :create-file
::rpc/profile-id (:id prof)
:project-id proj-id
:id file-id
:name "foobar"
:is-shared false
:components-v2 true}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= (:name data) (:name result)))
(t/is (= proj-id (:project-id result)))))
(let [data {::th/type :delete-file
:id file-id
::rpc/profile-id (:id prof)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
;; get deleted files
(let [data {::th/type :get-team-deleted-files
::rpc/profile-id (:id prof)
:team-id team-id}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [[row1 :as result] (:result out)]
(t/is (= 1 (count result)))
(t/is (= (:will-be-deleted-at row1) #penpot/inst "2025-11-07T00:00:00Z"))
(t/is (= (:created-at row1) #penpot/inst "2025-10-31T00:00:00Z"))
(t/is (= (:modified-at row1) #penpot/inst "2025-10-31T00:00:00Z"))))
(let [data {::th/type :restore-deleted-team-files
::rpc/profile-id (:id prof)
:team-id team-id
:ids #{file-id}}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (fn? result))
(let [events (th/consume-sse result)]
;; (pp/pprint events)
(t/is (= 2 (count events)))
(t/is (= :end (first (last events))))
(t/is (= (:ids data) (last (last events)))))))
(let [row (th/db-exec-one! ["select * from file where id = ?" file-id])]
(t/is (nil? (:deleted-at row)))))))

View File

@@ -104,8 +104,7 @@
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 1 (count (remove :deleted-at result))))
(t/is (= 2 (count result)))))))
(t/is (= 1 (count result)))))))
(t/deftest permissions-checks-create-project
(let [profile1 (th/create-profile* 1)
@@ -208,8 +207,7 @@
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= 2 (count result)))
(t/is (= 1 (count (remove :deleted-at result))))))
(t/is (= 1 (count result)))))
;; run permanent deletion (should be noop)
(let [result (th/run-task! :objects-gc {})]

View File

@@ -1024,29 +1024,6 @@
:clj
(sort comp-fn items))))
(defn reorder
"Reorder a vector by moving one of their items from some position to some space between positions.
It clamps the position numbers to a valid range."
[v from-pos to-space-between-pos]
(let [max-space-pos (count v)
max-prop-pos (dec max-space-pos)
from-pos (max 0 (min max-prop-pos from-pos))
to-space-between-pos (max 0 (min max-space-pos to-space-between-pos))]
(if (= from-pos to-space-between-pos)
v
(let [elem (nth v from-pos)
without-elem (-> []
(into (subvec v 0 from-pos))
(into (subvec v (inc from-pos))))
insert-pos (if (< from-pos to-space-between-pos)
(dec to-space-between-pos)
to-space-between-pos)]
(-> []
(into (subvec without-elem 0 insert-pos))
(into [elem])
(into (subvec without-elem insert-pos)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; String Functions

View File

@@ -517,7 +517,8 @@
(when verify?
(check-changes items))
(binding [*touched-changes* (volatile! #{})]
(binding [*touched-changes* (volatile! #{})
cts/*wasm-sync* true]
(let [result (reduce #(or (process-change %1 %2) %1) data items)
result (reduce process-touched-change result @*touched-changes*)]
;; Validate result shapes (only on the backend)

View File

@@ -638,7 +638,6 @@
(reduce add-undo-change-shape $ ids)))
(apply-changes-local)))))
;; FIXME: PERFORMANCE
(defn resize-parents
[changes ids]
(assert-page-id! changes)

View File

@@ -1413,27 +1413,17 @@
(defmethod migrate-data "0006-fix-old-texts-fills"
[data _]
(letfn [(fix-fills [node]
(let [;; In the old format refs were strings
sanitize-uuid
(fn [o]
(if (uuid? o)
o
(uuid/parse* o)))
fills
(if (and (not (seq (:fills node)))
(or (some? (:fill-color node))
(some? (:fill-opacity node))
(some? (:fill-color-gradient node))))
[(-> (select-keys node types.fills/fill-attrs)
(update :fill-color-ref-file sanitize-uuid)
(update :fill-color-ref-id sanitize-uuid)
(d/without-nils))]
(:fills node))]
(reduce dissoc
(assoc node :fills fills)
types.fills/fill-attrs)))
(let [fills (if (and (not (seq (:fills node)))
(or (some? (:fill-color node))
(some? (:fill-opacity node))
(some? (:fill-color-gradient node))))
[(d/without-nils (select-keys node [:fill-color :fill-opacity :fill-color-gradient
:fill-color-ref-id :fill-color-ref-file]))]
(:fills node))]
(-> node
(assoc :fills fills)
(dissoc :fill-color :fill-opacity :fill-color-gradient
:fill-color-ref-id :fill-color-ref-file))))
(update-object [object]
(if (cfh/text-shape? object)
@@ -1640,125 +1630,6 @@
;; as value; this migration fixes it.
(d/update-when data :components d/update-vals d/without-nils))
(defmethod migrate-data "0015-fix-text-attrs-blank-strings"
[data _]
;; After making text validation more restrictive (using ::sm/text
;; instead of :string), we need to fix text attributes that contain
;; empty or blank strings. These should be replaced with default
;; values from default-text-attrs.
(letfn [(blank-or-empty? [v]
(or (nil? v)
(and (string? v)
(or (str/empty? v)
(str/blank? v)))))
(get-default-value [attr]
(let [defaults types.text/default-text-attrs]
(case attr
;; direction in content maps to text-direction in defaults
:direction (:text-direction defaults)
;; For other attrs, get directly from defaults
(get defaults attr))))
(fix-text-attrs [node]
;; These are the attributes that were changed to ::sm/text in the schema
(let [text-attrs [:font-family :font-size :font-style :font-weight
:direction :text-decoration :text-transform]]
(reduce
(fn [node attr]
(if (and (contains? node attr)
(blank-or-empty? (get node attr)))
;; Replace blank/empty value with default
(if-let [default-val (get-default-value attr)]
(assoc node attr default-val)
;; If no default, remove the attribute
(dissoc node attr))
node))
node
text-attrs)))
(fix-position-data [position-data]
(mapv fix-text-attrs position-data))
(fix-text-content [content]
(types.text/transform-nodes types.text/is-content-node? fix-text-attrs content))
(update-shape [object]
(if (cfh/text-shape? object)
(-> object
(d/update-when :content fix-text-content)
(d/update-when :position-data fix-position-data))
object))
(update-container [container]
(d/update-when container :objects d/update-vals update-shape))]
(-> data
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
;; Copy fills from position-data to text nodes when all text nodes lack fills,
;; all position-data have fills, and the counts match
(defmethod migrate-data "0016-copy-fills-from-position-data-to-text-node"
[data _]
(letfn [(get-text-nodes [content]
;; Get all leaf text nodes from the content tree
(when content
(->> (types.text/node-seq types.text/is-text-node? content)
(seq))))
(update-content [content fills-map]
;; Transform the content tree to update text nodes with their corresponding fills
;; fills-map is a map from text node to its fills
(types.text/transform-nodes
types.text/is-text-node?
(fn [text-node]
(if-let [fills (get fills-map text-node)]
(assoc text-node :fills fills)
text-node))
content))
(update-object [object]
(if (cfh/text-shape? object)
(let [content (:content object)
position-data (:position-data object)
text-nodes (get-text-nodes content)]
;; Check if conditions are met:
;; 1. Has at least one text node
;; 2. All text nodes have no fills or empty fills
;; 3. Has at least one position-data entry
;; 4. All position-data have fills
;; 5. The number of text nodes matches the number of position-data
(if (and (seq text-nodes)
(seq position-data)
(= (count text-nodes) (count position-data))
(every? (fn [text-node]
(let [fills (:fills text-node)]
(or (nil? fills) (empty? fills))))
text-nodes)
(every? (fn [pd]
(let [fills (:fills pd)]
(and (some? fills) (seq fills))))
position-data))
;; Apply the migration: create a map from each text node to its corresponding fills
(let [fills-map (zipmap text-nodes (map :fills position-data))]
(update object :content #(update-content % fills-map)))
;; Don't modify if conditions aren't met
object))
;; Not a text shape, return as-is
object))
(update-container [container]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(def available-migrations
(into (d/ordered-set)
["legacy-2"
@@ -1830,6 +1701,4 @@
"0013-fix-component-path"
"0013-clear-invalid-strokes-and-fills"
"0014-fix-tokens-lib-duplicate-ids"
"0014-clear-components-nil-objects"
"0015-fix-text-attrs-blank-strings"
"0016-copy-fills-from-position-data-to-text-node"]))
"0014-clear-components-nil-objects"]))

View File

@@ -44,7 +44,7 @@
(let [attr? (set attributes)]
(->> (remove (fn [[k v]]
(and (attr? k)
(= v (or (token-identifier token) token))))
(= v (token-identifier token))))
applied-tokens)
(into {}))))

View File

@@ -466,20 +466,19 @@
children (map #(ctst/get-shape page %) shapes)
prop-names (cfv/extract-properties-names (first children) (:data file))]
(doseq [child children]
(when child
(if (not (ctk/is-variant? child))
(report-error :not-a-variant
(str/ffmt "Shape % should be a variant" (:id child))
child file page)
(do
(when (not= (:variant-id child) shape-id)
(report-error :invalid-variant-id
(str/ffmt "Variant % has invalid variant-id %" (:id child) (:variant-id child))
child file page))
(when (not= prop-names (cfv/extract-properties-names child (:data file)))
(report-error :invalid-variant-properties
(str/ffmt "Variant % has invalid properties %" (:id child) (vec prop-names))
child file page))))))))
(if (not (ctk/is-variant? child))
(report-error :not-a-variant
(str/ffmt "Shape % should be a variant" (:id child))
child file page)
(do
(when (not= (:variant-id child) shape-id)
(report-error :invalid-variant-id
(str/ffmt "Variant % has invalid variant-id %" (:id child) (:variant-id child))
child file page))
(when (not= prop-names (cfv/extract-properties-names child (:data file)))
(report-error :invalid-variant-properties
(str/ffmt "Variant % has invalid properties %" (:id child) (vec prop-names))
child file page)))))))
(defn- check-variant
"Shape is a variant, so
@@ -628,8 +627,7 @@
main-component (if (:deleted component)
(dm/get-in component [:objects (:main-instance-id component)])
(ctst/get-shape component-page (:main-instance-id component)))]
(when (and main-component
(not (ctk/is-variant? main-component)))
(when-not (ctk/is-variant? main-component)
(report-error :not-a-variant
(str/ffmt "Shape % should be a variant" (:id main-component))
main-component file component-page))))

View File

@@ -10,23 +10,16 @@
[app.common.types.components-list :as ctcl]
[app.common.types.variant :as ctv]))
(defn find-variant-components
"Find a list of the components thet belongs to this variant-id"
([data variant-id]
(let [page-id (->> data
:components
vals
(filter #(= (:variant-id %) variant-id))
first
:main-instance-page)
objects (dm/get-in data [:pages-index page-id :objects])]
(find-variant-components data objects variant-id)))
([data objects variant-id]
;; We can't simply filter components, because we need to maintain the order
(->> (dm/get-in objects [variant-id :shapes])
(map #(dm/get-in objects [% :component-id]))
(map #(ctcl/get-component data % true))
reverse)))
[data objects variant-id]
;; We can't simply filter components, because we need to maintain the order
(->> (dm/get-in objects [variant-id :shapes])
(map #(dm/get-in objects [% :component-id]))
(map #(ctcl/get-component data % true))
reverse))
(defn extract-properties-names
[shape data]
@@ -35,6 +28,7 @@
:variant-properties
(map :name)))
(defn extract-properties-values
"Get a map of properties associated to their possible values"
[data objects variant-id]
@@ -56,6 +50,7 @@
(get :objects))]
(dm/get-in objects [variant-id :shapes]))))
(defn is-secondary-variant?
[component data]
(let [shapes (get-variant-mains component data)]

View File

@@ -123,7 +123,6 @@
:token-color
:token-typography-types
:token-typography-composite
:token-shadow
:transit-readable-response
:user-feedback
;; TODO: remove this flag.

View File

@@ -1642,8 +1642,7 @@
(pcb/apply-changes-local)))))
(defn- generate-update-tokens
[changes container dest-shape origin-shape touched omit-touched? valid-attrs]
;; valid-attrs is a set of attrs to consider on the update. If it is nil, it will consider all the attrs
[changes container dest-shape origin-shape touched omit-touched?]
(let [attrs (->> (seq (keys ctk/sync-attrs))
;; We don't update the flex-child attrs
(remove #(= :layout-grid-cells %)))
@@ -1651,8 +1650,8 @@
applied-tokens (reduce (fn [applied-tokens attr]
(let [attr-group (get ctk/sync-attrs attr)
token-attrs (cto/shape-attr->token-attrs attr)]
(if (and (or (not omit-touched?) (not (touched attr-group)))
(or (empty? valid-attrs) (contains? valid-attrs attr)))
(if (not (and (touched attr-group)
omit-touched?))
(into applied-tokens token-attrs)
applied-tokens)))
#{}
@@ -1809,7 +1808,7 @@
:always
(check-detached-main dest-shape origin-shape)
:always
(generate-update-tokens container dest-shape origin-shape touched omit-touched? nil))
(generate-update-tokens container dest-shape origin-shape touched omit-touched?))
(let [attr-group (get ctk/sync-attrs attr)
;; position-data is a special case because can be affected by
@@ -1992,12 +1991,6 @@
;; If the values are already equal, don't copy them
(= (get previous-shape attr) (get current-shape attr))
;; If the value is the same as the origin, don't copy it
(= (get previous-shape attr) (get origin-ref-shape attr))
;; If the attr is not touched, don't copy it
(not (touched attr-group))
;; If both variants (origin and destiny) don't have the same value
;; for that attribute, don't copy it.
;; Exceptions: :points :selrect and :content can be different
@@ -2013,7 +2006,10 @@
(not= (get origin-ref-shape attr) (get current-shape attr)))
;; The :content attr cant't be copied to elements of different type
(and (= attr :content) (not= (:type previous-shape) (:type current-shape))))
(and (= attr :content) (not= (:type previous-shape) (:type current-shape)))
;; If the attr is not touched, don't copy it
(not (touched attr-group)))
;; On texts, both text (the actual letters)
;; and attrs (bold, font, etc) are in the same attr :content.
@@ -2086,14 +2082,12 @@
(recur (next attrs)
roperations'
uoperations'))
(cond-> changes
(> (count roperations) 1)
(add-update-attr-changes current-shape container roperations uoperations)
(let [updated-attrs (into #{} (comp (filter #(= :set (:type %)))
(map :attr))
roperations)]
(cond-> changes
(> (count roperations) 1)
(-> (add-update-attr-changes current-shape container roperations uoperations)
(generate-update-tokens container current-shape previous-shape touched false updated-attrs))))))))
:always
(generate-update-tokens container current-shape previous-shape touched false))))))
(defn- propagate-attrs
"Helper that puts the origin attributes (attrs) into dest but only if
@@ -2804,7 +2798,7 @@
(defn generate-duplicate-changes
"Prepare objects to duplicate: generate new id, give them unique names,
move to the desired position, and recalculate parents and frames as needed."
[changes all-objects page ids delta libraries library-data file-id & {:keys [variant-props alt-duplication?]}]
[changes all-objects page ids delta libraries library-data file-id & {:keys [variant-props]}]
(let [shapes (map (d/getf all-objects) ids)
unames (volatile! (cfh/get-used-names (:objects page)))
update-unames! (fn [new-name] (vswap! unames conj new-name))
@@ -2814,22 +2808,10 @@
;; we calculate a new one because the components will have created new shapes.
ids-map (into {} (map #(vector % (uuid/next))) all-ids)
;; If there is an alt-duplication of a variant, change its parent to root
;; so the copy is made as a child of root
;; This is because inside a variant-container can't be a copy
shapes (map (fn [shape]
(if (and alt-duplication? (ctk/is-variant? shape))
(assoc shape :parent-id uuid/zero :frame-id nil)
shape))
shapes)
changes (-> changes
(pcb/with-page page)
(pcb/with-objects all-objects)
(pcb/with-library-data library-data))
changes
(->> shapes
(reduce #(generate-duplicate-shape-change %1

View File

@@ -28,7 +28,11 @@
(pcb/update-component
changes (:id component)
(fn [component]
(d/update-in-when component [:variant-properties pos] #(assoc % :name new-name)))
(d/update-in-when component [:variant-properties pos]
(fn [property]
(-> property
(assoc :name new-name)
(with-meta nil)))))
{:apply-changes-local-library? true}))
changes
related-components)))
@@ -38,21 +42,18 @@
[changes variant-id pos]
(let [data (pcb/get-library-data changes)
objects (pcb/get-objects changes)
related-components (cfv/find-variant-components data objects variant-id)
props (-> related-components first :variant-properties)]
(if (and (seq props) (<= 0 pos) (< pos (count props)))
(reduce (fn [changes component]
(let [props (:variant-properties component)
props (d/remove-at-index props pos)
main-id (:main-instance-id component)
name (ctv/properties-to-name props)]
(-> changes
(pcb/update-component (:id component) #(assoc % :variant-properties props)
{:apply-changes-local-library? true})
(pcb/update-shapes [main-id] #(assoc % :variant-name name)))))
changes
related-components)
changes)))
related-components (cfv/find-variant-components data objects variant-id)]
(reduce (fn [changes component]
(let [props (:variant-properties component)
props (d/remove-at-index props pos)
main-id (:main-instance-id component)
name (ctv/properties-to-name props)]
(-> changes
(pcb/update-component (:id component) #(assoc % :variant-properties props)
{:apply-changes-local-library? true})
(pcb/update-shapes [main-id] #(assoc % :variant-name name)))))
changes
related-components)))
(defn generate-update-property-value
@@ -87,7 +88,7 @@
related-components (cfv/find-variant-components data objects variant-id)]
(reduce (fn [changes component]
(let [props (:variant-properties component)
props (d/reorder props from-pos to-space-between-pos)
props (ctv/reorder-by-moving-to-position props from-pos to-space-between-pos)
main-id (:main-instance-id component)
name (ctv/properties-to-name props)]
(-> changes

View File

@@ -67,6 +67,7 @@
[[] {}]
shapes))))
(defn- keep-swapped-item
"As part of the keep-touched process on a switch, given a child on the original
copy that was swapped (orig-swapped-child), and its related shape on the new copy
@@ -87,6 +88,7 @@
current-parent (get objects (:parent-id related-shape-in-new))
pos (d/index-of (:shapes current-parent) (:id related-shape-in-new))]
(-> (pcb/concat-changes before-changes changes)
;; Move the previous shape to the new parent
@@ -120,29 +122,6 @@
(subvec (vec ancestors) 1 (dec num-ancestors)))]
(some ctk/get-swap-slot ancestors)))
(defn- find-shape-ref-child-of
"Get the shape referenced by the shape-ref of the near main of the shape,
recursively repeated until find a shape-ref with parent-id as ancestor.
It will return the shape or nil if it doesn't found any"
[container libraries shape parent-id]
(let [ref-shape (ctf/find-ref-shape nil container libraries shape
:with-context? true)
ref-shape-container (when ref-shape (:container (meta ref-shape)))
ref-shape-parents-set (when ref-shape
(->> (cfh/get-parents-with-self (:objects ref-shape-container) (:id ref-shape))
(into #{} d/xf:map-id)))]
(if (or (nil? ref-shape) (contains? ref-shape-parents-set parent-id))
ref-shape
(find-shape-ref-child-of ref-shape-container libraries ref-shape parent-id))))
(defn- add-touched-from-ref-chain
"Adds to the :touched attr of a shape the content of
the :touched of all its chain of ref shapes"
[container libraries shape]
(let [new-touched (ctf/get-touched-from-ref-chain-until-target-ref container libraries shape nil)]
(assoc shape :touched new-touched)))
(defn generate-keep-touched
"This is used as part of the switch process, when you switch from
@@ -162,10 +141,7 @@
;; Ignore children of swapped items, because
;; they will be moved without change when
;; managing their swapped ancestor
orig-touched (->> original-shapes
;; Add to each shape also the touched of its ref chain
(map #(add-touched-from-ref-chain container libraries %))
(filter (comp seq :touched))
orig-touched (->> (filter (comp seq :touched) original-shapes)
(remove
#(child-of-swapped? %
page-objects
@@ -182,19 +158,20 @@
;; The original-shape is in a copy. For the relation rules, we need the referenced
;; shape on the main component
orig-base-ref-shape (ctf/find-remote-shape container libraries original-shape {:with-context? true})
orig-ref-objects (:objects (:container (meta orig-base-ref-shape)))
orig-ref-shape (ctf/find-ref-shape nil container libraries original-shape {:with-context? true})
orig-ref-objects (:objects (:container (meta orig-ref-shape)))
;; Adds a :shape-path attribute to the children of the orig-ref-shape,
;; that contains the type of its ancestors and its name
o-ref-shapes-wp (add-unique-path
(reverse (cfh/get-children-with-self orig-ref-objects (:id orig-base-ref-shape)))
(reverse (cfh/get-children-with-self orig-ref-objects (:id orig-ref-shape)))
orig-ref-objects
(:id orig-base-ref-shape))
(:id orig-ref-shape))
;; Creates a map to quickly find a child of the orig-ref-shape by its shape-path
o-ref-shapes-p-map (into {} (map (juxt :id :shape-path)) o-ref-shapes-wp)
;; Process each touched children of the original-shape
[changes parents-of-swapped]
(reduce
@@ -205,7 +182,8 @@
;; orig-child-touched is in a copy. Get the referenced shape on the main component
;; If there is a swap slot, we will get the referenced shape in another way
orig-ref-shape (when-not swap-slot
(find-shape-ref-child-of container libraries orig-child-touched (:id orig-base-ref-shape)))
;; TODO Maybe just get it from o-ref-shapes-wp
(ctf/find-ref-shape nil container libraries orig-child-touched))
orig-ref-id (if swap-slot
;; If there is a swap slot, find the referenced shape id
@@ -215,11 +193,9 @@
;; Get the shape path of the referenced main
shape-path (get o-ref-shapes-p-map orig-ref-id)
;; Get its related shape in the children of new-shape: the one that
;; has the same shape-path
related-shape-in-new (get new-shapes-map shape-path)
parents-of-swapped (if related-shape-in-new
(conj parent-of-swapped (:parent-id related-shape-in-new))
parent-of-swapped)

View File

@@ -14,14 +14,13 @@
(defn add-variant
[file variant-label component1-label root1-label component2-label root2-label
& {:keys [variant1-params variant2-params]
:or {variant1-params {} variant2-params {}}}]
& {:keys []}]
(let [file (ths/add-sample-shape file variant-label :type :frame :is-variant-container true)
variant-id (thi/id variant-label)]
(-> file
(ths/add-sample-shape root2-label (assoc variant2-params :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value2"))
(ths/add-sample-shape root1-label (assoc variant1-params :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value1"))
(ths/add-sample-shape root2-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value2")
(ths/add-sample-shape root1-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value1")
(thc/make-component component1-label root1-label)
(thc/update-component component1-label {:variant-id variant-id :variant-properties [{:name "Property 1" :value "Value1"}]})
(thc/make-component component2-label root2-label)
@@ -43,8 +42,7 @@
(defn add-variant-with-child
[file variant-label component1-label root1-label component2-label root2-label child1-label child2-label
& {:keys [child1-params child2-params]
:or {child1-params {} child2-params {}}}]
& {:keys [child1-params child2-params]}]
(let [file (ths/add-sample-shape file variant-label :type :frame :is-variant-container true)
variant-id (thi/id variant-label)]
(-> file

View File

@@ -286,7 +286,7 @@
(fn [touched]
(into #{} (remove #(str/starts-with? (name %) "swap-slot-") touched)))))
(defn get-deleted-component-root
(defn get-component-root
[component]
(if (some? (:main-instance-id component))
(get-in component [:objects (:main-instance-id component)])

View File

@@ -32,7 +32,6 @@
[app.common.types.typographies-list :as ctyl]
[app.common.types.typography :as cty]
[app.common.uuid :as uuid]
[clojure.set :as set]
[cuerdas.core :as str]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -277,7 +276,7 @@
(-> file-data
(get-component-page component)
(ctn/get-shape (:main-instance-id component)))
(ctk/get-deleted-component-root component)))
(ctk/get-component-root component)))
(defn get-component-shape
"Retrieve one shape in the component by id. If with-context? is true, add the
@@ -356,7 +355,7 @@
(defn find-remote-shape
"Recursively go back by the :shape-ref of the shape until find the correct shape of the original component"
[container libraries shape & {:keys [with-context?] :or {with-context? false}}]
[container libraries shape]
(let [top-instance (ctn/get-component-shape (:objects container) shape)
component-file (get-in libraries [(:component-file top-instance) :data])
component (ctkl/get-component component-file (:component-id top-instance) true)
@@ -376,12 +375,8 @@
(if (nil? remote-shape)
nil
(if (nil? (:shape-ref remote-shape))
(cond-> remote-shape
(and remote-shape with-context?)
(with-meta {:file {:id (:id file-data)
:data file-data}
:container component-container}))
(find-remote-shape component-container libraries remote-shape :with-context? with-context?)))))
remote-shape
(find-remote-shape component-container libraries remote-shape)))))
(defn direct-copy?
"Check if the shape is in a direct copy of the component (i.e. the shape-ref points to shapes inside
@@ -906,7 +901,7 @@
(println))
(when (seq (:objects component))
(let [root (ctk/get-deleted-component-root component)]
(let [root (ctk/get-component-root component)]
(dump-shape (:id root)
1
(:objects component)
@@ -1120,29 +1115,3 @@
(defn set-base-font-size
[file-data base-font-size]
(assoc-in file-data [:options :base-font-size] base-font-size))
;; Ref Chains
(defn get-ref-chain-until-target-ref
"Returns a vector with the shape ref chain until target-ref, including itself"
[container libraries shape target-ref]
(loop [chain [shape]
current shape]
(if (= current target-ref)
chain
(if-let [ref (find-ref-shape nil container libraries current :with-context? true)]
(recur (conj chain ref) ref)
chain))))
(defn get-touched-from-ref-chain-until-target-ref
"Returns a set with the :touched of all the items on the shape
ref chain until target-ref, including itself"
[container libraries shape target-ref]
(let [chain (get-ref-chain-until-target-ref container libraries shape target-ref)
more-touched (->> chain
(map :touched)
(remove nil?)
(apply set/union)
(remove ctk/swap-slot?)
set)]
(set/union (or (:touched shape) #{}) more-touched)))

View File

@@ -301,17 +301,11 @@
IHeapWritable
(-get-byte-size [_]
;; Include the 4-byte header with the fill count
(+ 4 (* size FILL-U8-SIZE)))
(- (.-byteLength dbuffer) 4))
(-write-to [_ heap offset]
(let [buffer' (.-buffer ^js/DataView dbuffer)
;; Calculate byte size: 4 bytes header + (size * FILL-U8-SIZE)
byte-size (+ 4 (* size FILL-U8-SIZE))
;; Create Uint32Array with exact size needed (convert bytes to u32 elements)
u32-array (js/Uint32Array. buffer' 0 (/ byte-size 4))]
;; Copy from offset 0 to include the header with fill count
(.set heap u32-array offset)))
(let [buffer' (.-buffer ^js/DataView dbuffer)]
(.set heap (js/Uint32Array. buffer' 4) offset)))
IBinaryFills
(-get-image-ids [_]

View File

@@ -498,10 +498,10 @@
[:map
[:x schema:safe-number]
[:y schema:safe-number]
[:c1x {:optional true} schema:safe-number]
[:c1y {:optional true} schema:safe-number]
[:c2x {:optional true} schema:safe-number]
[:c2y {:optional true} schema:safe-number]]]])
[:c1x schema:safe-number]
[:c1y schema:safe-number]
[:c2x schema:safe-number]
[:c2y schema:safe-number]]]])
(def ^:private schema:segment
[:multi {:title "PathSegment"

View File

@@ -36,7 +36,8 @@
[app.common.uuid :as uuid]
[clojure.set :as set]))
(defonce ^:dynamic *shape-changes* nil)
(defonce ^:dynamic *wasm-sync* false)
(defonce wasm-enabled? false)
(defonce wasm-create-shape (constantly nil))

View File

@@ -34,13 +34,13 @@
[:fills {:optional true}
[:maybe
[:vector {:gen/max 2} schema:fill]]]
[:font-family {:optional true} ::sm/text]
[:font-size {:optional true} ::sm/text]
[:font-style {:optional true} ::sm/text]
[:font-weight {:optional true} ::sm/text]
[:direction {:optional true} ::sm/text]
[:text-decoration {:optional true} ::sm/text]
[:text-transform {:optional true} ::sm/text]
[: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
@@ -51,13 +51,13 @@
[:fills {:optional true}
[:maybe
[:vector {:gen/max 2} schema:fill]]]
[:font-family {:optional true} ::sm/text]
[:font-size {:optional true} ::sm/text]
[:font-style {:optional true} ::sm/text]
[:font-weight {:optional true} ::sm/text]
[:direction {:optional true} ::sm/text]
[:text-decoration {:optional true} ::sm/text]
[:text-transform {:optional true} ::sm/text]
[: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]]]]]]]]]]]]])
@@ -72,11 +72,11 @@
[:width ::sm/safe-number]
[:height ::sm/safe-number]
[:fills [:vector {:gen/max 2} schema:fill]]
[:font-family {:optional true} ::sm/text]
[:font-size {:optional true} ::sm/text]
[:font-style {:optional true} ::sm/text]
[:font-weight {:optional true} ::sm/text]
[:font-family {:optional true} :string]
[:font-size {:optional true} :string]
[:font-style {:optional true} :string]
[:font-weight {:optional true} :string]
[:rtl {:optional true} :boolean]
[:text {:optional true} :string]
[:text-decoration {:optional true} ::sm/text]
[:text-transform {:optional true} ::sm/text]]])
[:text-decoration {:optional true} :string]
[:text-transform {:optional true} :string]]])

View File

@@ -249,16 +249,12 @@
(defn equal-attrs?
"Given a text structure, and a map of attrs, check that all the internal attrs in
paragraphs and sentences have the same attrs"
([item attrs]
;; Ignore the root attrs of the content. We only want to check paragraphs and sentences
(equal-attrs? item attrs true))
([item attrs ignore?]
(let [item-attrs (dissoc item :text :type :key :children)]
(and
(or ignore?
(empty? item-attrs)
(= attrs (dissoc item :text :type :key :children)))
(every? #(equal-attrs? % attrs false) (:children item))))))
[item attrs]
(let [item-attrs (dissoc item :text :type :key :children)]
(and
(or (empty? item-attrs)
(= attrs (dissoc item :text :type :key :children)))
(every? #(equal-attrs? % attrs) (:children item)))))
(defn get-first-paragraph-text-attrs
"Given a content text structure, extract it's first paragraph

View File

@@ -54,7 +54,6 @@
(def token-type->dtcg-token-type
{:boolean "boolean"
:border-radius "borderRadius"
:shadow "shadow"
:color "color"
:dimensions "dimension"
:font-family "fontFamilies"
@@ -78,8 +77,7 @@
;; Allow these properties to be imported with singular key names for backwards compability
(assoc "fontWeight" :font-weight
"fontSize" :font-size
"fontFamily" :font-family
"boxShadow" :shadow)))
"fontFamily" :font-family)))
(def composite-token-type->dtcg-token-type
"Custom set of conversion keys for composite typography token with `:line-height` available.
@@ -117,12 +115,6 @@
(def border-radius-keys (schema-keys schema:border-radius))
(def ^:private schema:shadow
[:map {:title "ShadowTokenAttrs"}
[:shadow {:optional true} token-name-ref]])
(def shadow-keys (schema-keys schema:shadow))
(def ^:private schema:stroke-width
[:map
[:stroke-width {:optional true} token-name-ref]])
@@ -279,7 +271,6 @@
(def all-keys (set/union color-keys
border-radius-keys
shadow-keys
stroke-width-keys
sizing-keys
opacity-keys
@@ -298,7 +289,6 @@
[:merge {:title "AppliedTokens"}
schema:tokens
schema:border-radius
schema:shadow
schema:sizing
schema:spacing
schema:rotation
@@ -344,7 +334,6 @@
(font-weight-keys shape-attr) #{shape-attr :typography}
(border-radius-keys shape-attr) #{shape-attr}
(shadow-keys shape-attr) #{shape-attr}
(sizing-keys shape-attr) #{shape-attr}
(opacity-keys shape-attr) #{shape-attr}
(spacing-keys shape-attr) #{shape-attr}
@@ -372,7 +361,6 @@
rotation-keys
sizing-keys
opacity-keys
shadow-keys
position-attributes))
(def rect-attributes
@@ -456,30 +444,6 @@
spacing-margin-keys)]
(unapply-token-id shape layout-item-attrs)))
(def tokens-by-input
"A map from input name to applicable token for that input."
{:width #{:sizing :dimensions}
:height #{:sizing :dimensions}
:max-width #{:sizing :dimensions}
:max-height #{:sizing :dimensions}
:x #{:spacing :dimensions}
:y #{:spacing :dimensions}
:rotation #{:number :rotation}
:border-radius #{:border-radius :dimensions}
:row-gap #{:spacing :dimensions}
:column-gap #{:spacing :dimensions}
:horizontal-padding #{:spacing :dimensions}
:vertical-padding #{:spacing :dimensions}
:sided-paddings #{:spacing :dimensions}
:horizontal-margin #{:spacing :dimensions}
:vertical-margin #{:spacing :dimensions}
:sided-margins #{:spacing :dimensions}
:line-height #{:line-height :number}
:font-size #{:font-size}
:letter-spacing #{:letter-spacing}
:fill #{:color}
:stroke-color #{:color}})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TYPOGRAPHY
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -550,11 +514,26 @@
[token-value]
(string? token-value))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SHADOW
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn shadow-composite-token-reference?
"Predicate if a shadow composite token is a reference value - a string pointing to another reference token."
[token-value]
(string? token-value))
(def tokens-by-input
"A map from input name to applicable token for that input."
{:width #{:sizing :dimensions}
:height #{:sizing :dimensions}
:max-width #{:sizing :dimensions}
:max-height #{:sizing :dimensions}
:x #{:spacing :dimensions}
:y #{:spacing :dimensions}
:rotation #{:number :rotation}
:border-radius #{:border-radius :dimensions}
:row-gap #{:spacing :dimensions}
:column-gap #{:spacing :dimensions}
:horizontal-padding #{:spacing :dimensions}
:vertical-padding #{:spacing :dimensions}
:sided-paddings #{:spacing :dimensions}
:horizontal-margin #{:spacing :dimensions}
:vertical-margin #{:spacing :dimensions}
:sided-margins #{:spacing :dimensions}
:line-height #{:line-height :number}
:font-size #{:font-size}
:letter-spacing #{:letter-spacing}
:fill #{:color}
:stroke-color #{:color}})

View File

@@ -1552,46 +1552,6 @@ Will return a value that matches this schema:
;; Reference value
value))
(defn- convert-dtcg-shadow-composite
"Convert shadow token value from DTCG format to internal format."
[value]
(let [process-shadow (fn [shadow]
(if (map? shadow)
(let [legacy-shadow-type (get "type" shadow)]
(-> shadow
(set/rename-keys {"x" :offsetX
"offsetX" :offsetX
"y" :offsetY
"offsetY" :offsetY
"blur" :blur
"spread" :spread
"color" :color
"inset" :inset})
(update :inset #(cond
(boolean? %) %
(= "true" %) true
(= "false" %) false
(= legacy-shadow-type "innerShadow") true
:else false))
(select-keys [:offsetX :offsetY :blur :spread :color :inset])))
shadow))]
(cond
;; Reference value - keep as string
(string? value)
value
;; Array of shadows - process each
(sequential? value)
(mapv process-shadow value)
;; Single shadow object - wrap in vector
(map? value)
[(process-shadow value)]
;; Fallback - keep as is
:else
value)))
(defn- flatten-nested-tokens-json
"Convert a tokens tree in the decoded json fragment into a flat map,
being the keys the token paths after joining the keys with '.'."
@@ -1614,7 +1574,6 @@ Will return a value that matches this schema:
(case token-type
:font-family (convert-dtcg-font-family token-value)
:typography (convert-dtcg-typography-composite token-value)
:shadow (convert-dtcg-shadow-composite token-value)
token-value))
:description (get v "$description")))
;; Discard unknown type tokens
@@ -1780,32 +1739,11 @@ Will return a value that matches this schema:
{} value)
value))
(defn- shadow-token->dtcg-token
"Convert shadow token value from internal format to DTCG format."
[value]
(if (sequential? value)
(mapv (fn [shadow]
(if (map? shadow)
(-> shadow
(set/rename-keys {:offsetX "offsetX"
:offsetY "offsetY"
:blur "blur"
:spread "spread"
:color "color"
:inset "inset"})
(select-keys ["offsetX" "offsetY" "blur" "spread" "color" "inset"]))
shadow))
value)
value))
(defn- token->dtcg-token [token]
(cond-> {"$value" (cond-> (:value token)
;; Transform typography token values
(= :typography (:type token))
typography-token->dtcg-token
;; Transform shadow token values
(= :shadow (:type token))
shadow-token->dtcg-token)
typography-token->dtcg-token)
"$type" (cto/token-type->dtcg-token-type (:type token))}
(:description token) (assoc "$description" (:description token))))
@@ -2053,19 +1991,13 @@ Will return a value that matches this schema:
#?(:clj
(defn- migrate-to-v1-4
"Migrate the TokensLib data structure internals to v1.4 version; it
"Migrate the TokensLib data structure internals to v1.2 version; it
expects input from v1.3 version"
[params]
(let [migrate-set-node
(fn recurse [node]
(cond
(token-set-legacy? node)
(if (token-set-legacy? node)
(make-token-set node)
(token-set? node)
node
:else
(d/update-vals node recurse)))]
(update params :sets d/update-vals migrate-set-node))))

View File

@@ -310,3 +310,27 @@
the real name of the shape joined by the properties values separated by '/'"
[variant]
(cpn/merge-path-item (:name variant) (str/replace (:variant-name variant) #", " " / ")))
(defn reorder-by-moving-to-position
"Reorder a vector by moving one of their items from some position to some space between positions.
It clamps the position numbers to a valid range."
[props from-pos to-space-between-pos]
(let [max-space-pos (count props)
max-prop-pos (dec max-space-pos)
from-pos (max 0 (min max-prop-pos from-pos))
to-space-between-pos (max 0 (min max-space-pos to-space-between-pos))]
(if (= from-pos to-space-between-pos)
props
(let [elem (nth props from-pos)
without-elem (-> []
(into (subvec props 0 from-pos))
(into (subvec props (inc from-pos))))
insert-pos (if (< from-pos to-space-between-pos)
(dec to-space-between-pos)
to-space-between-pos)]
(-> []
(into (subvec without-elem 0 insert-pos))
(into [elem])
(into (subvec without-elem insert-pos)))))))

View File

@@ -102,14 +102,3 @@
(t/is (= (d/insert-at-index [:a :b :c :d] 1 [:a])
[:a :b :c :d])))
(t/deftest reorder
(let [v ["a" "b" "c" "d"]]
(t/is (= (d/reorder v 0 2) ["b" "a" "c" "d"]))
(t/is (= (d/reorder v 0 3) ["b" "c" "a" "d"]))
(t/is (= (d/reorder v 0 4) ["b" "c" "d" "a"]))
(t/is (= (d/reorder v 3 0) ["d" "a" "b" "c"]))
(t/is (= (d/reorder v 3 2) ["a" "b" "d" "c"]))
(t/is (= (d/reorder v 0 5) ["b" "c" "d" "a"]))
(t/is (= (d/reorder v 3 -1) ["d" "a" "b" "c"]))
(t/is (= (d/reorder v 5 -1) ["d" "a" "b" "c"]))
(t/is (= (d/reorder v -1 5) ["b" "c" "d" "a"]))))

View File

@@ -18,29 +18,6 @@
(t/use-fixtures :each thi/test-fixture)
(t/deftest test-basic-switch
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant
:v01 :c01 :m01 :c02 :m02
{:variant1-params {:width 5}
:variant2-params {:width 15}})
(thc/instantiate-component :c01
:copy01))
copy01 (ths/get-shape file :copy01)
;; ==== Action
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
copy01' (ths/get-shape file' :copy02)]
(thf/dump-file file :keys [:width])
;; The copy had width 5 before the switch
(t/is (= (:width copy01) 5))
;; The rect has width 15 after the switch
(t/is (= (:width copy01') 15))))
(t/deftest test-simple-switch
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -69,40 +46,6 @@
(t/is (= (:width rect02') 15))))
(t/deftest test-basic-switch-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant
:v01 :c01 :m01 :c02 :m02
{:variant1-params {:width 5}
:variant2-params {:width 5}})
(thc/instantiate-component :c01
:copy01))
copy01 (ths/get-shape file :copy01)
;; Change width of copy
page (thf/current-page file)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id copy01)}
(fn [shape]
(assoc shape :width 25))
(:objects page)
{})
file (thf/apply-changes file changes)
copy01 (ths/get-shape file :copy01)
;; ==== Action
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
copy01' (ths/get-shape file' :copy02)]
(thf/dump-file file :keys [:width])
;; The copy had width 25 before the switch
(t/is (= (:width copy01) 25))
;; The override is keept: The copy still has width 25 after the switch
(t/is (= (:width copy01') 25))))
(t/deftest test-switch-with-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -182,10 +125,12 @@
;; The rect has width 15 after the switch
(t/is (= (:width rect02') 15))))
(def font-size-path-paragraph [:content :children 0 :children 0 :font-size])
(def font-size-path-0 [:content :children 0 :children 0 :children 0 :font-size])
(def font-size-path-1 [:content :children 0 :children 0 :children 1 :font-size])
(def text-path-0 [:content :children 0 :children 0 :children 0 :text])
(def text-path-1 [:content :children 0 :children 0 :children 1 :text])
(def text-lines-path [:content :children 0 :children 0 :children])
@@ -243,8 +188,6 @@
;; The copy clean has no overrides
copy-clean (ths/get-shape file :copy-clean)
copy-clean-t (ths/get-shape file :copy-clean-t)
@@ -266,8 +209,6 @@
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
(tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
@@ -293,8 +234,6 @@
;; Before the switch:
;; * font size 14
;; * text "hello world"
(t/is (= (get-in copy-clean-t font-size-path-0) "14"))
(t/is (= (get-in copy-clean-t text-path-0) "hello world"))
@@ -309,8 +248,6 @@
;; Before the switch:
;; * font size 25
;; * text "hello world"
(t/is (= (get-in copy-font-size-t font-size-path-0) "25"))
(t/is (= (get-in copy-font-size-t text-path-0) "hello world"))
@@ -369,8 +306,6 @@
;; The copy clean has no overrides
copy-clean (ths/get-shape file :copy-clean)
copy-clean-t (ths/get-shape file :copy-clean-t)
@@ -392,8 +327,6 @@
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
(tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
@@ -419,8 +352,6 @@
;; Before the switch:
;; * font size 14
;; * text "hello world"
(t/is (= (get-in copy-clean-t font-size-path-0) "14"))
(t/is (= (get-in copy-clean-t text-path-0) "hello world"))
@@ -435,8 +366,6 @@
;; Before the switch:
;; * font size 25
;; * text "hello world"
(t/is (= (get-in copy-font-size-t font-size-path-0) "25"))
(t/is (= (get-in copy-font-size-t text-path-0) "hello world"))
@@ -472,6 +401,7 @@
(t/is (= (get-in copy-both-t' font-size-path-0) "50"))
(t/is (= (get-in copy-both-t' text-path-0) "text overriden"))))
(t/deftest test-switch-with-different-text-text-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -493,8 +423,6 @@
;; The copy clean has no overrides
copy-clean (ths/get-shape file :copy-clean)
copy-clean-t (ths/get-shape file :copy-clean-t)
@@ -516,8 +444,6 @@
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
(tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
@@ -543,8 +469,6 @@
;; Before the switch:
;; * font size 14
;; * text "hello world"
(t/is (= (get-in copy-clean-t font-size-path-0) "14"))
(t/is (= (get-in copy-clean-t text-path-0) "hello world"))
@@ -559,8 +483,6 @@
;; Before the switch:
;; * font size 25
;; * text "hello world"
(t/is (= (get-in copy-font-size-t font-size-path-0) "25"))
(t/is (= (get-in copy-font-size-t text-path-0) "hello world"))
@@ -596,6 +518,7 @@
(t/is (= (get-in copy-both-t' font-size-path-0) "25"))
(t/is (= (get-in copy-both-t' text-path-0) "bye"))))
(t/deftest test-switch-with-different-text-and-prop-text-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -619,8 +542,6 @@
;; The copy clean has no overrides
copy-clean (ths/get-shape file :copy-clean)
copy-clean-t (ths/get-shape file :copy-clean-t)
@@ -642,8 +563,6 @@
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
(tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
@@ -669,8 +588,6 @@
;; Before the switch:
;; * font size 14
;; * text "hello world"
(t/is (= (get-in copy-clean-t font-size-path-0) "14"))
(t/is (= (get-in copy-clean-t text-path-0) "hello world"))
@@ -685,8 +602,6 @@
;; Before the switch:
;; * font size 25
;; * text "hello world"
(t/is (= (get-in copy-font-size-t font-size-path-0) "25"))
(t/is (= (get-in copy-font-size-t text-path-0) "hello world"))
@@ -722,6 +637,7 @@
(t/is (= (get-in copy-both-t' font-size-path-0) "50"))
(t/is (= (get-in copy-both-t' text-path-0) "bye"))))
(t/deftest test-switch-with-identical-structure-text-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -741,8 +657,6 @@
;; Duplicate a text line in copy-structure-clean
file (change-structure file :copy-structure-clean-t)
copy-structure-clean (ths/get-shape file :copy-structure-clean)
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
@@ -764,8 +678,6 @@
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
(tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
@@ -851,6 +763,7 @@
(t/is (= (get-in copy-structure-mixed-t' font-size-path-1) "40"))
(t/is (= (get-in copy-structure-mixed-t' text-path-1) "new line 2"))))
(t/deftest test-switch-with-different-prop-structure-text-override
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -871,8 +784,6 @@
;; Duplicate a text line in copy-structure-clean
file (change-structure file :copy-structure-clean-t)
copy-structure-clean (ths/get-shape file :copy-structure-clean)
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
@@ -894,8 +805,6 @@
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
(tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
@@ -997,8 +906,6 @@
;; Duplicate a text line in copy-structure-clean
file (change-structure file :copy-structure-clean-t)
copy-structure-clean (ths/get-shape file :copy-structure-clean)
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
@@ -1020,8 +927,6 @@
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
(tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
@@ -1066,8 +971,6 @@
;; Second line:
;; * font size 25
;; * text "new line 2"
(t/is (= (get-in copy-structure-unif-t font-size-path-0) "25"))
(t/is (= (get-in copy-structure-unif-t text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-unif-t font-size-path-1) "25"))
@@ -1089,8 +992,6 @@
;; Before the switch, second line:
;; * font size 40
;; * text "new line 2"
(t/is (= (get-in copy-structure-mixed-t font-size-path-0) "35"))
(t/is (= (get-in copy-structure-mixed-t text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-mixed-t font-size-path-1) "40"))
@@ -1124,8 +1025,6 @@
;; Duplicate a text line in copy-structure-clean
file (change-structure file :copy-structure-clean-t)
copy-structure-clean (ths/get-shape file :copy-structure-clean)
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
@@ -1147,8 +1046,6 @@
;; ==== Action: Switch all the copies
file' (-> file
(tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
(tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
@@ -1193,8 +1090,6 @@
;; Second line:
;; * font size 25
;; * text "new line 2"
(t/is (= (get-in copy-structure-unif-t font-size-path-0) "25"))
(t/is (= (get-in copy-structure-unif-t text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-unif-t font-size-path-1) "25"))
@@ -1216,8 +1111,6 @@
;; Before the switch, second line:
;; * font size 40
;; * text "new line 2"
(t/is (= (get-in copy-structure-mixed-t font-size-path-0) "35"))
(t/is (= (get-in copy-structure-mixed-t text-path-0) "new line 1"))
(t/is (= (get-in copy-structure-mixed-t font-size-path-1) "40"))
@@ -1231,6 +1124,7 @@
(t/is (= (get-in copy-structure-mixed-t' text-path-0) "bye"))
(t/is (nil? (get-in copy-structure-mixed-t' font-size-path-1)))))
(t/deftest test-switch-variant-for-other-with-same-nested-component
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -1250,8 +1144,6 @@
;; On :copy-cp01, change the width of the rect
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{copy-cp01-rect-id}
(fn [shape]
@@ -1274,6 +1166,8 @@
;; The width of copy-cp02-rect' is 25 (change is preserved)
(t/is (= (:width copy-cp02-rect') 25))))
(t/deftest test-switch-variant-that-has-swaped-copy
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -1299,6 +1193,7 @@
;; Switch :c01 for :c02
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
copy02' (ths/get-shape file' :copy02)
copy-cp02' (ths/get-shape file' :copy-cp02)]
(thf/dump-file file')
@@ -1312,6 +1207,7 @@
;;copy-02' had copy-cp02' as child
(t/is (= (-> copy02' :shapes first) (:id copy-cp02')))))
(t/deftest test-switch-variant-that-has-swaped-copy-with-changed-attr
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
@@ -1348,6 +1244,7 @@
;; Switch :c01 for :c02
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
copy02' (ths/get-shape file' :copy02)
copy-cp02' (ths/get-shape file' :copy-cp02)
copy-cp02-rect' (ths/get-shape-by-id file' (-> copy-cp02' :shapes first))]
@@ -1365,58 +1262,3 @@
(t/is (= (-> copy02' :shapes first) (:id copy-cp02')))
;; The width of copy-cp02-rect' is 25 (change is preserved)
(t/is (= (:width copy-cp02-rect') 25))))
(t/deftest test-switch-variant-without-touched-but-touched-parent
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 5}
:child2-params {:width 5}})
(tho/add-simple-component :external01 :external01-root :external01-child)
(thc/instantiate-component :c01
:c01-in-root
:children-labels [:r01-in-c01-in-root]
:parent-label :external01-root))
;; Make a change on r01-in-c01-in-root so it is touched
page (thf/current-page file)
r01-in-c01-in-root (ths/get-shape file :r01-in-c01-in-root)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id r01-in-c01-in-root)}
(fn [shape]
(assoc shape :width 25))
(:objects page)
{})
file (thf/apply-changes file changes)
;; Instantiate the component :external01
file (thc/instantiate-component file
:external01
:external-copy01
:children-labels [:external-copy01-rect :c01-in-copy])
page (thf/current-page file)
c01-in-copy (ths/get-shape file :c01-in-copy)
rect01 (get-in page [:objects (-> c01-in-copy :shapes first)])
;; ==== Action
file' (tho/swap-component file c01-in-copy :c02 {:new-shape-label :c02-in-copy :keep-touched? true})
page' (thf/current-page file')
c02-in-copy' (ths/get-shape file' :c02-in-copy)
rect02' (get-in page' [:objects (-> c02-in-copy' :shapes first)])]
(thf/dump-file file :keys [:width :touched])
;; The rect had width 25 before the switch
(t/is (= (:width rect01) 25))
;; The rect still has width 25 after the switch
(t/is (= (:width rect02') 25))))

View File

@@ -45,7 +45,6 @@
[common-tests.types.path-data-test]
[common-tests.types.shape-decode-encode-test]
[common-tests.types.shape-interactions-test]
[common-tests.types.token-test]
[common-tests.types.tokens-lib-test]
[common-tests.uuid-test]))
@@ -99,5 +98,4 @@
'common-tests.types.shape-decode-encode-test
'common-tests.types.shape-interactions-test
'common-tests.types.tokens-lib-test
'common-tests.types.token-test
'common-tests.uuid-test))

View File

@@ -1,60 +0,0 @@
{
"test": {
"shadow-single": {
"$value": {
"x": "0",
"y": "2px",
"blur": "4px",
"spread": "0",
"color": "#000"
},
"$type": "boxShadow"
},
"shadow-multiple": {
"$value": [
{
"x": "0",
"y": "2px",
"blur": "4px",
"spread": "0",
"color": "#000",
"inset": true
},
{
"x": "0",
"y": "8px",
"blur": "16px",
"spread": "0",
"color": "#000",
"inset": "true"
}
],
"$type": "boxShadow"
},
"shadow-ref": {
"$value": "{shadow-single}",
"$type": "boxShadow"
},
"shadow-with-type": {
"$value": {
"x": "0",
"y": "4px",
"blur": "8px",
"spread": "0",
"color": "rgba(0,0,0,0.2)",
"type": "innerShadow"
},
"$type": "boxShadow"
},
"shadow-with-description": {
"$value": {
"x": "1px",
"y": "1px",
"blur": "3px",
"color": "gray"
},
"$type": "boxShadow",
"$description": "A simple shadow token"
}
}
}

View File

@@ -116,27 +116,6 @@
(t/is (= sample-content
(vec pdata)))))
;; Test the specific case where cuve-to commands comes without the
;; optional attrs
(t/deftest path-data-plain-to-binary-2
(let [plain-content
[{:command :move-to :params {:x 480.0 :y 839.0}}
{:command :line-to :params {:x 439.0 :y 802.0}}
{:command :curve-to :params {:x 264.0 :y 634.0}}
{:command :curve-to :params {:x 154.0 :y 508.0}}]
binary-content
(path/content plain-content)]
#?(:clj
(t/is (= "M480.0,839.0L439.0,802.0C264.0,634.0,264.0,634.0,264.0,634.0C154.0,508.0,154.0,508.0,154.0,508.0"
(str binary-content)))
:cljs
(t/is (= "M480,839L439,802C264,634,264,634,264,634C154,508,154,508,154,508"
(str binary-content))))))
(t/deftest path-data-from-binary
(let [barray #?(:clj (byte-array sample-bytes)
:cljs (js/Int8Array.from sample-bytes))

View File

@@ -1,27 +0,0 @@
;; 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 common-tests.types.token-test
(:require
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.uuid :as uuid]
[clojure.test :as t]))
(t/deftest test-valid-token-name-schema
;; Allow regular namespace token names
(t/is (true? (sm/validate cto/token-name-ref "Foo")))
(t/is (true? (sm/validate cto/token-name-ref "foo")))
(t/is (true? (sm/validate cto/token-name-ref "FOO")))
(t/is (true? (sm/validate cto/token-name-ref "Foo.Bar.Baz")))
;; Disallow trailing tokens
(t/is (false? (sm/validate cto/token-name-ref "Foo.Bar.Baz....")))
;; Disallow multiple separator dots
(t/is (false? (sm/validate cto/token-name-ref "Foo..Bar.Baz")))
;; Disallow any special characters
(t/is (false? (sm/validate cto/token-name-ref "Hey Foo.Bar")))
(t/is (false? (sm/validate cto/token-name-ref "Hey😈Foo.Bar")))
(t/is (false? (sm/validate cto/token-name-ref "Hey%Foo.Bar"))))

View File

@@ -1362,7 +1362,9 @@
{:name "button.primary.background"
:type :color
:value "{accent.default}"
:description ""}))))))
:description ""})))
(t/testing "invalid tokens got discarded"
(t/is (nil? (ctob/get-token-by-name lib "theme" "boxShadow.default")))))))
#?(:clj
(t/deftest parse-multi-set-dtcg-json
@@ -1390,7 +1392,9 @@
{:name "button.primary.background"
:type :color
:value "{accent.default}"
:description ""}))))))
:description ""})))
(t/testing "invalid tokens got discarded"
(t/is (nil? (ctob/get-token-by-name lib "theme" "boxShadow.default")))))))
#?(:clj
(t/deftest parse-multi-set-dtcg-json-default-team
@@ -1889,130 +1893,3 @@
(t/is (some? imported-single))
(t/is (= (:type imported-single) (:type original-single)))
(t/is (= (:value imported-single) (:value original-single))))))))
#?(:clj
(t/deftest parse-shadow-tokens
(let [json (-> (slurp "test/common_tests/types/data/tokens-shadow-example.json")
(json/decode {:key-fn identity}))
lib (ctob/parse-decoded-json json "shadow-test")]
(t/testing "single shadow token"
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-single")]
(t/is (some? token))
(t/is (= :shadow (:type token)))
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
(:value token)))))
(t/testing "multiple shadow token"
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-multiple")]
(t/is (some? token))
(t/is (= :shadow (:type token)))
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset true}
{:offsetX "0", :offsetY "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
(:value token)))))
(t/testing "shadow token with reference"
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-ref")]
(t/is (some? token))
(t/is (= :shadow (:type token)))
(t/is (= "{shadow-single}" (:value token)))))
(t/testing "shadow token with type"
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-type")]
(t/is (some? token))
(t/is (= :shadow (:type token)))
(t/is (= [{:offsetX "0", :offsetY "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
(:value token)))))
(t/testing "shadow token with description"
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-description")]
(t/is (some? token))
(t/is (= :shadow (:type token)))
(t/is (= "A simple shadow token" (:description token))))))))
#?(:clj
(t/deftest export-shadow-tokens
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set
(ctob/make-token-set
:name "shadow-set"
:tokens {"shadow.single"
(ctob/make-token
{:name "shadow.single"
:type :shadow
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}]
:description "A single shadow"})
"shadow.multiple"
(ctob/make-token
{:name "shadow.multiple"
:type :shadow
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}
{:offsetX "0" :offsetY "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
"shadow.ref"
(ctob/make-token
{:name "shadow.ref"
:type :shadow
:value "{shadow.single}"})
"shadow.empty"
(ctob/make-token
{:name "shadow.empty"
:type :shadow
:value {}})})))
result (ctob/export-dtcg-json tokens-lib)
shadow-set (get result "shadow-set")]
(t/testing "single shadow token export"
(let [single-token (get-in shadow-set ["shadow" "single"])]
(t/is (= "shadow" (get single-token "$type")))
(t/is (= [{"offsetX" "0" "offsetY" "2px" "blur" "4px" "spread" "0" "color" "#0000001A"}] (get single-token "$value")))
(t/is (= "A single shadow" (get single-token "$description")))))
(t/testing "multiple shadow token export"
(let [multiple-token (get-in shadow-set ["shadow" "multiple"])]
(t/is (= "shadow" (get multiple-token "$type")))
(t/is (= [{"offsetX" "0" "offsetY" "2px" "blur" "4px" "spread" "0" "color" "#0000001A"}
{"offsetX" "0" "offsetY" "8px" "blur" "16px" "spread" "0" "color" "#0000001A"}]
(get multiple-token "$value")))))
(t/testing "reference shadow token export"
(let [ref-token (get-in shadow-set ["shadow" "ref"])]
(t/is (= "shadow" (get ref-token "$type")))
(t/is (= "{shadow.single}" (get ref-token "$value")))))
(t/testing "empty shadow token export"
(let [empty-token (get-in shadow-set ["shadow" "empty"])]
(t/is (= "shadow" (get empty-token "$type")))
(t/is (= {} (get empty-token "$value"))))))))
#?(:clj
(t/deftest shadow-token-round-trip
(let [original-lib (-> (ctob/make-tokens-lib)
(ctob/add-set
(ctob/make-token-set
:name "test-set"
:tokens {"shadow.test"
(ctob/make-token
{:name "shadow.test"
:type :shadow
:value [{:offsetX "1" :offsetY "1" :blur "1" :spread "1" :color "red" :inset true}]
:description "Round trip test"})
"shadow.ref"
(ctob/make-token
{:name "shadow.ref"
:type :shadow
:value "{shadow.test}"})})))
exported (ctob/export-dtcg-json original-lib)
imported-lib (ctob/parse-decoded-json exported "")]
(t/testing "round trip preserves shadow tokens"
(let [original-token (ctob/get-token-by-name original-lib "test-set" "shadow.test")
imported-token (ctob/get-token-by-name imported-lib "test-set" "shadow.test")]
(t/is (some? imported-token))
(t/is (= (:type original-token) (:type imported-token)))
(t/is (= (:value original-token) (:value imported-token)))
(t/is (= (:description original-token) (:description imported-token))))
(let [original-ref (ctob/get-token-by-name original-lib "test-set" "shadow.ref")
imported-ref (ctob/get-token-by-name imported-lib "test-set" "shadow.ref")]
(t/is (some? imported-ref))
(t/is (= (:type original-ref) (:type imported-ref)))
(t/is (= (:value imported-ref) (:value original-ref))))))))

View File

@@ -159,3 +159,48 @@
(t/testing "update-number-in-repeated-prop-names"
(t/is (= (ctv/update-number-in-repeated-prop-names props) numbered-props)))))
(t/deftest reorder-by-moving-to-position
(let [props [{:name "border" :value "no"}
{:name "color" :value "blue"}
{:name "shadow" :value "yes"}
{:name "background" :value "none"}]]
(t/testing "reorder-by-moving-to-position"
(t/is (= (ctv/reorder-by-moving-to-position props 0 2) [{:name "color" :value "blue"}
{:name "border" :value "no"}
{:name "shadow" :value "yes"}
{:name "background" :value "none"}]))
(t/is (= (ctv/reorder-by-moving-to-position props 0 3) [{:name "color" :value "blue"}
{:name "shadow" :value "yes"}
{:name "border" :value "no"}
{:name "background" :value "none"}]))
(t/is (= (ctv/reorder-by-moving-to-position props 0 4) [{:name "color" :value "blue"}
{:name "shadow" :value "yes"}
{:name "background" :value "none"}
{:name "border" :value "no"}]))
(t/is (= (ctv/reorder-by-moving-to-position props 3 0) [{:name "background" :value "none"}
{:name "border" :value "no"}
{:name "color" :value "blue"}
{:name "shadow" :value "yes"}]))
(t/is (= (ctv/reorder-by-moving-to-position props 3 2) [{:name "border" :value "no"}
{:name "color" :value "blue"}
{:name "background" :value "none"}
{:name "shadow" :value "yes"}]))
(t/is (= (ctv/reorder-by-moving-to-position props 0 5) [{:name "color" :value "blue"}
{:name "shadow" :value "yes"}
{:name "background" :value "none"}
{:name "border" :value "no"}]))
(t/is (= (ctv/reorder-by-moving-to-position props 3 -1) [{:name "background" :value "none"}
{:name "border" :value "no"}
{:name "color" :value "blue"}
{:name "shadow" :value "yes"}]))
(t/is (= (ctv/reorder-by-moving-to-position props 5 -1) [{:name "background" :value "none"}
{:name "border" :value "no"}
{:name "color" :value "blue"}
{:name "shadow" :value "yes"}]))
(t/is (= (ctv/reorder-by-moving-to-position props -1 5) [{:name "color" :value "blue"}
{:name "shadow" :value "yes"}
{:name "background" :value "none"}
{:name "border" :value "no"}])))))

View File

@@ -73,7 +73,7 @@ RUN set -eux; \
FROM base AS setup-node
ENV NODE_VERSION=v22.21.1 \
ENV NODE_VERSION=v22.19.0 \
PATH=/opt/node/bin:$PATH
RUN set -eux; \
@@ -113,12 +113,12 @@ RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
ESUM='8c5321f16d9f1d8149f83e4e9ff8ca5d9e94320b92d205e6db42a604de3d1140'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_aarch64.tar.gz'; \
ESUM='b60eb9d54c97ba4159547834a98cc5d016281dd2b3e60e7475cba4911324bcb4'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.28.85-ca-jdk25.0.0-linux_aarch64.tar.gz'; \
;; \
amd64|x86_64) \
ESUM='471b3e62bdffaed27e37005d842d8639f10d244ccce1c7cdebf7abce06c8313e'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_x64.tar.gz'; \
ESUM='164d901e5a240b8c18516f5ab55bc11fc9689ab6e829045aea8467356dcdb340'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.28.85-ca-jdk25.0.0-linux_x64.tar.gz'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \
@@ -149,24 +149,18 @@ FROM base AS setup-rust
ENV PATH=/opt/cargo/bin:$PATH \
RUSTUP_HOME=/opt/rustup \
CARGO_HOME=/opt/cargo \
RUSTUP_VERSION=1.28.2 \
RUST_VERSION=1.91.0 \
RUSTUP_VERSION=1.27.1 \
RUST_VERSION=1.85.0 \
EMSCRIPTEN_VERSION=4.0.6
WORKDIR /opt
RUN set -eux; \
# Same steps as in Rust official Docker image https://github.com/rust-lang/docker-rust/blob/9f287282d513a84cb7c7f38f197838f15d37b6a9/1.81.0/bookworm/Dockerfile
arch="$(dpkg --print-architecture)"; \
case "$arch" in \
'amd64') \
rustArch='x86_64-unknown-linux-gnu'; \
rustupSha256='20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c'; \
;; \
'arm64') \
rustArch='aarch64-unknown-linux-gnu'; \
rustupSha256='e3853c5a252fca15252d07cb23a1bdd9377a8c6f3efa01531109281ae47f841c'; \
;; \
dpkgArch="$(dpkg --print-architecture)"; \
case "${dpkgArch##*-}" in \
amd64) rustArch='x86_64-unknown-linux-gnu'; rustupSha256='6aeece6993e902708983b209d04c0d1dbb14ebb405ddb87def578d41f920f56d' ;; \
arm64) rustArch='aarch64-unknown-linux-gnu'; rustupSha256='1cffbf51e63e634c746f741de50649bbbcbd9dbe1de363c9ecef64e278dba2b2' ;; \
*) echo >&2 "unsupported architecture: ${dpkgArch}"; exit 1 ;; \
esac; \
wget "https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${rustArch}/rustup-init"; \

View File

@@ -5,7 +5,7 @@ ENV LANG='C.UTF-8' \
LC_ALL='C.UTF-8' \
JAVA_HOME="/opt/jdk" \
DEBIAN_FRONTEND=noninteractive \
NODE_VERSION=v22.21.1 \
NODE_VERSION=v22.19.0 \
TZ=Etc/UTC
RUN set -ex; \
@@ -46,12 +46,12 @@ RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
ESUM='8c5321f16d9f1d8149f83e4e9ff8ca5d9e94320b92d205e6db42a604de3d1140'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_aarch64.tar.gz'; \
ESUM='6f8725d186d05c627176db9c46c732a6ef3ba41d9e9b3775c4727fc8ac642bb2'; \
BINARY_URL='https://github.com/adoptium/temurin24-binaries/releases/download/jdk-24.0.2%2B12/OpenJDK24U-jdk_aarch64_linux_hotspot_24.0.2_12.tar.gz'; \
;; \
amd64|x86_64) \
ESUM='471b3e62bdffaed27e37005d842d8639f10d244ccce1c7cdebf7abce06c8313e'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_x64.tar.gz'; \
ESUM='aea1cc55e51cf651c85f2f00ad021603fe269c4bb6493fa97a321ad770c9b096'; \
BINARY_URL='https://github.com/adoptium/temurin24-binaries/releases/download/jdk-24.0.2%2B12/OpenJDK24U-jdk_x64_linux_hotspot_24.0.2_12.tar.gz'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \

View File

@@ -3,7 +3,7 @@ LABEL maintainer="Penpot <docker@penpot.app>"
ENV LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 \
NODE_VERSION=v22.21.1 \
NODE_VERSION=v22.19.0 \
DEBIAN_FRONTEND=noninteractive \
PATH=/opt/node/bin:$PATH

View File

@@ -1,20 +0,0 @@
FROM nginxinc/nginx-unprivileged:1.29.1
LABEL maintainer="Penpot <docker@penpot.app>"
USER root
RUN set -ex; \
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot;
ARG BUNDLE_PATH="./bundle-storybook/"
COPY $BUNDLE_PATH /var/www/
COPY ./files/nginx.storybook.conf /etc/nginx/conf.d/default.conf
RUN chown -R 1001:0 /var/cache/nginx; \
chmod -R g+w /var/cache/nginx; \
chown -R 1001:0 /etc/nginx; \
chmod -R g+w /etc/nginx; \
chown -R 1001:0 /var/www; \
chmod -R g+w /var/www;
USER penpot:penpot

View File

@@ -247,11 +247,6 @@ services:
networks:
- penpot
environment:
# You can increase the max memory size if you have sufficient resources,
# although this should not be necessary.
- VALKEY_EXTRA_FLAGS=--maxmemory 128mb --maxmemory-policy volatile-lfu
## A mailcatch service, used as temporal SMTP server. You can access via HTTP to the
## port 1080 for read all emails the penpot platform has sent. Should be only used as a
## temporal solution while no real SMTP provider is configured.

View File

@@ -1,27 +0,0 @@
server {
listen 8080 default_server;
server_name _;
charset utf-8;
etag off;
gzip on;
gzip_static on;
gzip_types text/plain text/css application/javascript application/json application/vnd.api+json application/xml application/x-javascript text/xml image/svg+xml;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_vary on;
error_log /dev/stderr;
access_log /dev/stdout;
root /var/www;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@@ -1,27 +0,0 @@
server {
listen 8080 default_server;
server_name _;
charset utf-8;
etag off;
gzip on;
gzip_static on;
gzip_types text/plain text/css application/javascript application/json application/vnd.api+json application/xml application/x-javascript text/xml image/svg+xml;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_vary on;
error_log /dev/stderr;
access_log /dev/stdout;
root /var/www;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@@ -81,9 +81,6 @@ module.exports = function(eleventyConfig) {
eleventyConfig.addPassthroughCopy("css");
eleventyConfig.addPassthroughCopy("js");
// Redirects (for Cloudflare)
eleventyConfig.addPassthroughCopy({"_redirects": "_redirects" });
/* Markdown Overrides */
let markdownLibrary = markdownIt({
html: true,

View File

@@ -4,7 +4,7 @@ templateClass: tmpl-user-guide
---
{%- macro show_children(item) -%}
{%- for child in item | children | sorted('data.order') %}
{%- for child in item | children | sorted('data.title') %}
{%- if loop.first -%}<ul>{%- endif -%}
<li>
<a href="{{ child.url }}">{{ child.data.title }}</a>

View File

@@ -1,239 +0,0 @@
/user-guide/introduction/ /user-guide/first-steps/
/user-guide/introduction/quickstart/ /user-guide/first-steps/cloud-selfhost/
/user-guide/introduction/shortcuts/ /user-guide/first-steps/shortcuts/
/user-guide/introduction/shortcuts/#workspace-section /user-guide/first-steps/shortcuts/#workspace-section
/user-guide/introduction/shortcuts/#alignment /user-guide/first-steps/shortcuts/#alignment
/user-guide/introduction/shortcuts/#edit /user-guide/first-steps/shortcuts/#edit
/user-guide/introduction/shortcuts/#main-menu /user-guide/first-steps/shortcuts/#main-menu
/user-guide/introduction/shortcuts/#modify-layers /user-guide/first-steps/shortcuts/#modify-layers
/user-guide/introduction/shortcuts/#goto-screens-workspace /user-guide/first-steps/shortcuts/#goto-screens-workspace
/user-guide/introduction/shortcuts/#panels /user-guide/first-steps/shortcuts/#panels
/user-guide/introduction/shortcuts/#path-editor /user-guide/first-steps/shortcuts/#path-editor
/user-guide/introduction/shortcuts/#shapes /user-guide/first-steps/shortcuts/#shapes
/user-guide/introduction/shortcuts/#tools /user-guide/first-steps/shortcuts/#tools
/user-guide/introduction/shortcuts/#zoom-workspace /user-guide/first-steps/shortcuts/#zoom-workspace
/user-guide/introduction/shortcuts/#text /user-guide/first-steps/shortcuts/#text
/user-guide/introduction/shortcuts/#dashboard-section /user-guide/first-steps/shortcuts/#dashboard-section
/user-guide/introduction/shortcuts/#generic-dashboard /user-guide/first-steps/shortcuts/#generic-dashboard
/user-guide/introduction/shortcuts/#navigation-dashboard /user-guide/first-steps/shortcuts/#navigation-dashboard
/user-guide/introduction/shortcuts/#viewer-section /user-guide/first-steps/shortcuts/#viewer-section
/user-guide/introduction/shortcuts/#generic-viewer /user-guide/first-steps/shortcuts/#generic-viewer
/user-guide/introduction/shortcuts/#navigation-viewer /user-guide/first-steps/shortcuts/#navigation-viewer
/user-guide/introduction/shortcuts/#zoom-viewer /user-guide/first-steps/shortcuts/#zoom-viewer
/user-guide/introduction/info/ /user-guide/first-steps/info/
/user-guide/introduction/info/#dev-diaries /user-guide/first-steps/info/
/user-guide/introduction/info/#video-tutorials /user-guide/first-steps/info/
/user-guide/introduction/info/#faqs /user-guide/first-steps/info/
/user-guide/the-interface/ /user-guide/first-steps/the-interface/
/user-guide/the-interface/#interface-workspace /user-guide/first-steps/the-interface/#interface-workspace
/user-guide/the-interface/#interface-viewmode /user-guide/first-steps/the-interface/#interface-viewmode
/user-guide/the-interface/#interface-dashboard /user-guide/first-steps/the-interface/#interface-dashboard
/user-guide/the-interface/#your-account /user-guide/account-teams/your-account/
/user-guide/the-interface/#interface-ui-theme /user-guide/account-teams/your-account/#interface-ui-theme
/user-guide/workspace-basics/ /user-guide/designing/workspace-basics/
/user-guide/workspace-basics/#viewport /user-guide/designing/workspace-basics/#viewport
/user-guide/workspace-basics/#workspace-menu /user-guide/designing/workspace-basics/#workspace-menu
/user-guide/workspace-basics/#zoom /user-guide/designing/workspace-basics/#zoom
/user-guide/workspace-basics/#dynamic-alignment /user-guide/designing/workspace-basics/#dynamic-alignment
/user-guide/workspace-basics/#rulers /user-guide/designing/workspace-basics/#rulers
/user-guide/workspace-basics/#ruler-guides /user-guide/designing/workspace-basics/#ruler-guides
/user-guide/workspace-basics/#guides /user-guide/designing/workspace-basics/#guides
/user-guide/workspace-basics/#add-guides /user-guide/designing/workspace-basics/#add-guides
/user-guide/workspace-basics/#hide-remove-guides /user-guide/designing/workspace-basics/#hide-remove-guides
/user-guide/workspace-basics/#square-guides /user-guide/designing/workspace-basics/#square-guides
/user-guide/workspace-basics/#row-guides /user-guide/designing/workspace-basics/#row-guides
/user-guide/workspace-basics/#column-guides /user-guide/designing/workspace-basics/#column-guides
/user-guide/workspace-basics/#guides-defaults /user-guide/designing/workspace-basics/#guides-defaults
/user-guide/workspace-basics/#guides-visibility /user-guide/designing/workspace-basics/#guides-visibility
/user-guide/workspace-basics/#guides-snap /user-guide/designing/workspace-basics/#guides-snap
/user-guide/workspace-basics/#snap-to-pixel /user-guide/designing/workspace-basics/#snap-to-pixel
/user-guide/workspace-basics/#nudge-amount /user-guide/designing/workspace-basics/#nudge-amount
/user-guide/workspace-basics/#shortcuts-panel /user-guide/designing/workspace-basics/#shortcuts-panel
/user-guide/workspace-basics/#history /user-guide/designing/workspace-basics/#history
/user-guide/workspace-basics/#comments /user-guide/designing/workspace-basics/#comments
/user-guide/layer-basics/ /user-guide/designing/layers/
/user-guide/layer-basics/#pages /user-guide/designing/workspace-basics/#layer-basics
/user-guide/layer-basics/#layers-panel /user-guide/layer-basics/#layers-panel
/user-guide/layer-basics/#hide-lock /user-guide/designing/layers/#hide-lock
/user-guide/layer-basics/#creating-layers /user-guide/designing/layers/#creating-layers
/user-guide/layer-basics/#duplicating-layers /user-guide/designing/layers/#duplicating-layers
/user-guide/layer-basics/#delete-layers /user-guide/designing/layers/#delete-layers
/user-guide/layer-basics/#select-layers /user-guide/designing/layers/#select-layers
/user-guide/layer-basics/#group-layers /user-guide/designing/layers/#group-layers
/user-guide/layer-basics/#mask-layers /user-guide/designing/layers/#mask-layers
/user-guide/layer-basics/#move-layers /user-guide/designing/layers/#move-layers
/user-guide/layer-basics/#resize-layers /user-guide/designing/layers/#resize-layers
/user-guide/layer-basics/#rotate-layers /user-guide/designing/layers/#rotate-layers
/user-guide/layer-basics/#flip-layers /user-guide/designing/layers/#flip-layers
/user-guide/layer-basics/#scale-elements /user-guide/designing/layers/#scale-elements
/user-guide/layer-basics/#aling-distribute-layers /user-guide/designing/layers/#aling-distribute-layers
/user-guide/layer-basics/#layers-search /user-guide/designing/workspace-basics/#layer-basics
/user-guide/layer-basics/#collapse-groups /user-guide/designing/workspace-basics/#layer-basics
/user-guide/layer-basics/#boolean-operators /user-guide/designing/layers/#boolean-operators
/user-guide/layer-basics/#constraints /user-guide/designing/layers/#constraints
/user-guide/layer-basics/#focus-mode /user-guide/designing/workspace-basics/#focus-mode
/user-guide/layer-basics/#rtl-support /user-guide/designing/text-typo/#rtl-support
/user-guide/objects/ /user-guide/designing/layers/
/user-guide/objects/#Boards /user-guide/designing/layers/#Boards
/user-guide/objects/#rectangles-ellipses /user-guide/designing/layers/#rectangles-ellipses
/user-guide/objects/#text /user-guide/designing/layers/#text
/user-guide/objects/#curves /user-guide/designing/layers/#curves
/user-guide/objects/#paths /user-guide/designing/layers/#paths
/user-guide/objects/#images /user-guide/designing/layers/#images
/user-guide/styling/ /user-guide/designing/layers/#styling-layers
/user-guide/styling/#fill /user-guide/designing/color-stroke/#fill
/user-guide/styling/#color-picker /user-guide/designing/color-stroke/#color-picker
/user-guide/styling/#color-picker-gradients /user-guide/designing/color-stroke/#color-picker-gradients
/user-guide/styling/#color-palette /user-guide/designing/color-stroke/#color-palette
/user-guide/styling/#selected-colors /user-guide/designing/color-stroke/#selected-colors
/user-guide/styling/#strokes /user-guide/designing/color-stroke/#strokes
/user-guide/styling/#stroke-caps /user-guide/designing/color-stroke/#stroke-caps
/user-guide/styling/#radius /user-guide/designing/layers/#radius
/user-guide/styling/#shadow /user-guide/designing/layers/#shadow
/user-guide/styling/#blur /user-guide/designing/layers/#blur
/user-guide/styling/#blend /user-guide/designing/layers/#blend
/user-guide/styling/#copy-paste-properties /user-guide/designing/layers/#copy-paste-properties
/user-guide/exporting/ /user-guide/export-import/exporting-layers/
/user-guide/exporting/#export-howto /user-guide/export-import/exporting-layers/#export-howto
/user-guide/exporting/#export-options /user-guide/export-import/exporting-layers/#export-options
/user-guide/exporting/#export-multiple-elements /user-guide/export-import/exporting-layers/#export-multiple-elements
/user-guide/exporting/#export-artboards-pdf /user-guide/export-import/exporting-layers/#export-artboards-pdf
/user-guide/exporting/#export-technical /user-guide/export-import/exporting-layers/#export-technical
/user-guide/flexible-layouts/ /user-guide/designing/flexible-layouts/
/user-guide/flexible-layouts/#layouts-flex /user-guide/designing/flexible-layouts/#layouts-flex
/user-guide/flexible-layouts/#layouts-flex-css /user-guide/designing/flexible-layouts/#layouts-flex
/user-guide/flexible-layouts/#layouts-flex-add /user-guide/designing/flexible-layouts/#layouts-flex-add
/user-guide/flexible-layouts/#layouts-flex-arrange-reorder /user-guide/designing/flexible-layouts/#layouts-flex-arrange-reorder
/user-guide/flexible-layouts/#layouts-flex-properties /user-guide/designing/flexible-layouts/#layouts-flex-properties
/user-guide/flexible-layouts/#layouts-flex-elements /user-guide/designing/flexible-layouts/#layouts-flex-elements
/user-guide/flexible-layouts/#layouts-flex-spacing /user-guide/designing/flexible-layouts/#layouts-flex-spacing
/user-guide/flexible-layouts/#layouts-flex-code /user-guide/designing/flexible-layouts/#layouts-flex-code
/user-guide/flexible-layouts/#layouts-flex-examples /user-guide/designing/flexible-layouts/#layouts-flex-examples
/user-guide/flexible-layouts/#layouts-grid /user-guide/designing/flexible-layouts/#layouts-grid
/user-guide/flexible-layouts/#layouts-flex-css /user-guide/designing/flexible-layouts/#layouts-grid
/user-guide/flexible-layouts/#layouts-grid-add /user-guide/designing/flexible-layouts/#layouts-grid-add
/user-guide/flexible-layouts/#layouts-grid-terminology /user-guide/designing/flexible-layouts/#layouts-grid-terminology
/user-guide/flexible-layouts/#layouts-grid-properties /user-guide/designing/flexible-layouts/#layouts-grid-properties
/user-guide/flexible-layouts/#layouts-grid-elements /user-guide/designing/flexible-layouts/#layouts-grid-elements
/user-guide/flexible-layouts/#layouts-grid-colsrows /user-guide/designing/flexible-layouts/#layouts-grid-colsrows
/user-guide/flexible-layouts/#layouts-grid-units /user-guide/designing/flexible-layouts/#layouts-grid-units
/user-guide/flexible-layouts/#layouts-grid-areas /user-guide/designing/flexible-layouts/#layouts-grid-areas
/user-guide/flexible-layouts/#layouts-grid-code /user-guide/designing/flexible-layouts/#layouts-grid-code
/user-guide/libraries/ /user-guide/design-systems/assets/
/user-guide/libraries/#assets /user-guide/design-systems/assets/
/user-guide/libraries/#asset-types /user-guide/design-systems/assets/#asset-types
/user-guide/libraries/#add-assets-to-library /user-guide/design-systems/assets/#add-assets-to-library
/user-guide/libraries/#edit-assets /user-guide/design-systems/assets/#edit-assets
/user-guide/libraries/#use-assets /user-guide/design-systems/assets/#use-assets
/user-guide/libraries/#organize-assets /user-guide/design-systems/assets/#organize-assets
/user-guide/libraries/#libraries /user-guide/design-systems/libraries/
/user-guide/libraries/#file-libraries /user-guide/design-systems/libraries/#file-libraries
/user-guide/libraries/#shared-libraries /user-guide/design-systems/libraries/#shared-libraries
/user-guide/design-tokens/ /user-guide/design-systems/design-tokens/
/user-guide/design-tokens/#design-tokens-why /user-guide/design-systems/design-tokens/
/user-guide/design-tokens/#design-tokens-format /user-guide/design-systems/design-tokens/
/user-guide/design-tokens/#design-tokens-use /user-guide/design-systems/design-tokens/#design-tokens-use-create
/user-guide/design-tokens/#design-tokens-use-create /user-guide/design-systems/design-tokens/#design-tokens-use-create
/user-guide/design-tokens/#design-tokens-aliases /user-guide/design-systems/design-tokens/#design-tokens-aliases
/user-guide/design-tokens/#design-tokens-equations /user-guide/design-systems/design-tokens/#design-tokens-equations
/user-guide/design-tokens/#design-tokens-edit /user-guide/design-systems/design-tokens/#design-tokens-edit
/user-guide/design-tokens/#design-tokens-duplicate /user-guide/design-systems/design-tokens/#design-tokens-duplicate
/user-guide/design-tokens/#design-tokens-delete /user-guide/design-systems/design-tokens/#design-tokens-delete
/user-guide/design-tokens/#design-tokens-available /user-guide/design-systems/design-tokens/#design-tokens-available
/user-guide/design-tokens/#design-tokens-radius /user-guide/design-systems/design-tokens/#design-tokens-radius
/user-guide/design-tokens/#design-tokens-color /user-guide/design-systems/design-tokens/#design-tokens-color
/user-guide/design-tokens/#design-tokens-dimensions /user-guide/design-systems/design-tokens/#design-tokens-dimensions
/user-guide/design-tokens/#design-tokens-opacity /user-guide/design-systems/design-tokens/#design-tokens-opacity
/user-guide/design-tokens/#design-tokens-rotation /user-guide/design-systems/design-tokens/#design-tokens-rotation
/user-guide/design-tokens/#design-tokens-sizing /user-guide/design-systems/design-tokens/#design-tokens-sizing
/user-guide/design-tokens/#design-tokens-spacing /user-guide/design-systems/design-tokens/#design-tokens-spacing
/user-guide/design-tokens/#design-tokens-stroke-width /user-guide/design-systems/design-tokens/#design-tokens-stroke-width
/user-guide/design-tokens/#design-tokens-number /user-guide/design-systems/design-tokens/#design-tokens-number
/user-guide/design-tokens/#design-tokens-typography /user-guide/design-systems/design-tokens/#design-tokens-typography
/user-guide/design-tokens/#design-tokens-sets /user-guide/design-systems/design-tokens/#design-tokens-sets
/user-guide/design-tokens/#design-tokens-sets-create /user-guide/design-systems/design-tokens/#design-tokens-sets
/user-guide/design-tokens/#design-tokens-sets-edit /user-guide/design-systems/design-tokens/#design-tokens-sets
/user-guide/design-tokens/#design-tokens-groups /user-guide/design-systems/design-tokens/#design-tokens-sets
/user-guide/design-tokens/#design-tokens-themes /user-guide/design-systems/design-tokens/#design-tokens-themes
/user-guide/design-tokens/#design-tokens-themes-create /user-guide/design-systems/design-tokens/#design-tokens-themes
/user-guide/design-tokens/#design-tokens-themes-edit /user-guide/design-systems/design-tokens/#design-tokens-themes
/user-guide/design-tokens/#design-tokens-themes-group /user-guide/design-systems/design-tokens/#design-tokens-themes
/user-guide/design-tokens/#design-tokens-import-export /user-guide/design-systems/design-tokens/#design-tokens-import-export
/user-guide/design-tokens/#design-tokens-import-options /user-guide/design-systems/design-tokens/#design-tokens-import-export
/user-guide/design-tokens/#design-tokens-export-options /user-guide/design-systems/design-tokens/#design-tokens-import-export
/user-guide/components/ /user-guide/design-systems/components/
/user-guide/components/#components-basics /user-guide/design-systems/components/
/user-guide/components/#component-create /user-guide/design-systems/components/#component-create
/user-guide/components/#component-find /user-guide/design-systems/components/#component-find
/user-guide/components/#component-main-components-page /user-guide/design-systems/components/#component-main-components-page
/user-guide/components/#working-with-components /user-guide/design-systems/components/#component-group
/user-guide/components/#component-group /user-guide/design-systems/components/#component-group
/user-guide/components/#component-detach /user-guide/design-systems/components/#component-detach
/user-guide/components/#component-annotate /user-guide/design-systems/components/#component-annotate
/user-guide/components/#component-overrides-relationships /user-guide/design-systems/components/#component-overrides
/user-guide/components/#component-overrides /user-guide/design-systems/components/#component-overrides
/user-guide/components/#component-update /user-guide/design-systems/components/#component-update
/user-guide/components/#component-swap /user-guide/design-systems/components/#component-swap
/user-guide/components/#component-variants /user-guide/design-systems/variants/
/user-guide/components/#component-variants-why-are-variants-important /user-guide/design-systems/variants/#component-variants-why-are-variants-important
/user-guide/components/#component-understanding-variants-properties-and-values /user-guide/design-systems/variants/#component-understanding-variants-properties-and-values
/user-guide/components/#component-create-and-modify-variants /user-guide/design-systems/variants/#component-create-and-modify-variants
/user-guide/components/#component-use-variants /user-guide/design-systems/variants/#component-use-variants
/user-guide/prototyping/ /user-guide/prototyping-testing/prototyping/
/user-guide/prototyping/#prototyping-connection /user-guide/prototyping-testing/prototyping/#prototyping-connection
/user-guide/prototyping/#prototype-anatomy /user-guide/prototyping-testing/prototyping/#prototype-anatomy
/user-guide/prototyping/#interaction-triggers /user-guide/prototyping-testing/prototyping/#interaction-triggers
/user-guide/prototyping/#prototyping-actions /user-guide/prototyping-testing/prototyping/#prototyping-actions
/user-guide/prototyping/#prototyping-actions-navigate /user-guide/prototyping-testing/prototyping/#prototyping-actions-navigate
/user-guide/prototyping/#prototyping-actions-overlay /user-guide/prototyping-testing/prototyping/#prototyping-actions-overlay
/user-guide/prototyping/#prototyping-actions-overlay-toggle /user-guide/prototyping-testing/prototyping/#prototyping-actions-overlay-toggle
/user-guide/prototyping/#prototyping-actions-overlay-close /user-guide/prototyping-testing/prototyping/#prototyping-actions-overlay-close
/user-guide/prototyping/#prototyping-actions-previous /user-guide/prototyping-testing/prototyping/#prototyping-actions-previous
/user-guide/prototyping/#prototyping-actions-url /user-guide/prototyping-testing/prototyping/#prototyping-actions-url
/user-guide/prototyping/#prototyping-animations /user-guide/prototyping-testing/prototyping/#prototyping-animations
/user-guide/prototyping/#prototyping-animations-dissolve /user-guide/prototyping-testing/prototyping/#prototyping-animations-dissolve
/user-guide/prototyping/#prototyping-animations-Slide /user-guide/prototyping-testing/prototyping/#prototyping-animations-Slide
/user-guide/prototyping/#prototyping-animations-push /user-guide/prototyping-testing/prototyping/#prototyping-animations-push
/user-guide/prototyping/#prototyping-flows /user-guide/prototyping-testing/prototyping/#prototyping-flows
/user-guide/prototyping/#prototyping-flows-starting /user-guide/prototyping-testing/prototyping/#prototyping-flows-starting
/user-guide/prototyping/#prototyping-flows-multiple /user-guide/prototyping-testing/prototyping/#prototyping-flows-multiple
/user-guide/prototyping/#prototyping-fix-scroll /user-guide/prototyping-testing/prototyping/#prototyping-fix-scroll
/user-guide/view-mode/ /user-guide/prototyping-testing/testing-view-mode/
/user-guide/view-mode/#viewmode-interface /user-guide/prototyping-testing/testing-view-mode/#viewmode-interface
/user-guide/view-mode/#viewmode-launch /user-guide/prototyping-testing/testing-view-mode/#viewmode-launch
/user-guide/view-mode/#viewmode-features /user-guide/prototyping-testing/testing-view-mode/#viewmode-features
/user-guide/view-mode/#viewmode-comments /user-guide/prototyping-testing/testing-view-mode/#viewmode-comments
/user-guide/view-mode/#viewmode-sharing /user-guide/prototyping-testing/testing-view-mode/#viewmode-sharing
/user-guide/view-mode/#viewmode-inspect /user-guide/prototyping-testing/testing-view-mode/#viewmode-inspect
/user-guide/inspect/ /user-guide/dev-tools/
/user-guide/inspect/#inspect-activate /user-guide/dev-tools/#inspect-design
/user-guide/inspect/#inspect-viewmode /user-guide/dev-tools/#inspect-design
/user-guide/inspect/#inspect-workspace /user-guide/dev-tools/#inspect-design
/user-guide/inspect/#inspect-measure /user-guide/dev-tools/#inspect-measure
/user-guide/inspect/#inspect-info /user-guide/dev-tools/#inspect-info
/user-guide/inspect/#inspect-copy /user-guide/dev-tools/#inspect-copy
/user-guide/inspect/#inspect-code /user-guide/dev-tools/#inspect-code
/user-guide/inspect/#inspect-export /user-guide/dev-tools/#inspect-export
/user-guide/import-export/ /user-guide/export-import/export-import-files/
/user-guide/import-export/#files-export /user-guide/export-import/export-import-files/#files-export
/user-guide/import-export/#export-penpot-files /user-guide/export-import/export-import-files/#files-export
/user-guide/import-export/#files-import /user-guide/export-import/export-import-files/#files-import
/user-guide/import-export/#penpot-formats /user-guide/export-import/export-import-files/#penpot-formats
/user-guide/teams/ /user-guide/account-teams/teams/
/user-guide/teams/#teams-management /user-guide/account-teams/teams/#teams-management
/user-guide/teams/#teams-members /user-guide/account-teams/teams/#teams-members
/user-guide/teams/#teams-invites /user-guide/account-teams/teams/#teams-invites
/user-guide/teams/#teams-webhooks /user-guide/plugins-integrations/#teams-webhooks
/user-guide/custom-fonts/ /user-guide/designing/text-typo/#custom-fonts
/user-guide/custom-fonts/#customfonts-upload /user-guide/designing/text-typo/#customfonts-upload
/user-guide/custom-fonts/#customfonts-families /user-guide/designing/text-typo/#customfonts-families
/user-guide/custom-fonts/#customfonts-edit /user-guide/designing/text-typo/#customfonts-edit
/user-guide/custom-fonts/#customfonts-using /user-guide/designing/text-typo/#customfonts-using
/user-guide/plugins/ /user-guide/plugins-integrations/
/user-guide/plugins/#plugins /user-guide/plugins-integrations/
/user-guide/plugins/#installation /user-guide/plugins-integrations/#installation
/user-guide/plugins/#hub-installation /user-guide/plugins-integrations/#installation
/user-guide/plugins/#url-installation /user-guide/plugins-integrations/#installation
/user-guide/plugins/#plugin-manager /user-guide/plugins-integrations/#plugin-manager
/user-guide/plugins/#using-plugins /user-guide/plugins-integrations/#using-plugins
/user-guide/plugins/#create-plugin /user-guide/plugins-integrations/#create-plugin

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -23,7 +23,7 @@ Flags and evironment variables are also used together; for example:
```bash
# This flag enables the use of SMTP email
PENPOT_FLAGS: [...] enable-smtp
PENPOT_FLAGS: enable-smtp
# These environment variables configure the specific SMPT service
# Backend
@@ -36,7 +36,7 @@ the exporter, or all of them; on the other hand, **environment variables** are c
each specific service. For example:
```bash
PENPOT_FLAGS: [...] enable-login-with-google
PENPOT_FLAGS: enable-login-with-google
# Backend
PENPOT_GOOGLE_CLIENT_ID: <client-id>
@@ -56,7 +56,7 @@ Penpot uses anonymous telemetries from the self-hosted instances to improve the
Consider sharing these anonymous telemetries enabling the corresponding flag:
```bash
PENPOT_FLAGS: [...] enable-telemetries
PENPOT_FLAGS: enable-telemetries
```
## Registration and authentication
@@ -402,7 +402,7 @@ This is implemented as specific locations in the penpot-front Nginx. If your org
in a 100% air-gapped environment, you can use the following configuration:
```bash
PENPOT_FLAGS: [...] enable-air-gapped-conf
PENPOT_FLAGS: enable-air-gapped-conf
```
When Penpot starts, it will leave out the Nginx configuration related to external requests. This means that,
@@ -459,15 +459,11 @@ POSTGRES_PASSWORD: penpot
### Storage
Storage refers to storing the user uploaded different objects in Penpot (assets, file data,...).
Storage refers to storing the user uploaded assets.
Objects storage is implemented using "plugable" backends. Currently there are two
Assets storage is implemented using "plugable" backends. Currently there are two
backends available: <code class="language-bash">fs</code> and <code class="language-bash">s3</code> (for AWS S3).
__Since version 2.11.0__
The configuration variables related to storage has been renamed, `PENPOT_STORAGE_ASSETS_*` are now `PENPOT_OBJECTS_STORAGE_*`.
`PENPOT_ASSETS_STORAGE_BACKEND` becomes `PENPOT_OBJECTS_STORAGE_BACKEND` and its values now are `fs` and `s3` instead of `assets-fs` or `assets-s3`.
#### FS Backend (default)
This is the default backend when you use the official docker images and the default
@@ -475,8 +471,8 @@ configuration looks like this:
```bash
# Backend
PENPOT_OBJECTS_STORAGE_BACKEND: fs
PENPOT_OBJECTS_STORAGE_FS_DIRECTORY: /opt/data/objects
PENPOT_ASSETS_STORAGE_BACKEND: assets-fs
PENPOT_STORAGE_ASSETS_FS_DIRECTORY: /opt/data/assets
```
The main downside of this backend is the hard dependency on nginx approach to serve files
@@ -489,7 +485,7 @@ configuration file][4] used in the docker images.
#### AWS S3 Backend
This backend uses AWS S3 bucket for store the user uploaded objects. For use it you should
This backend uses AWS S3 bucket for store the user uploaded assets. For use it you should
have an appropriate account on AWS cloud and have the credentials, region and the bucket.
This is how configuration looks for S3 backend:
@@ -498,36 +494,18 @@ This is how configuration looks for S3 backend:
# Backend
AWS_ACCESS_KEY_ID: <you-access-key-id-here>
AWS_SECRET_ACCESS_KEY: <your-secret-access-key-here>
PENPOT_OBJECTS_STORAGE_BACKEND: s3
PENPOT_OBJECTS_STORAGE_S3_REGION: <aws-region>
PENPOT_OBJECTS_STORAGE_S3_BUCKET: <bucket-name>
PENPOT_ASSETS_STORAGE_BACKEND: assets-s3
PENPOT_STORAGE_ASSETS_S3_REGION: <aws-region>
PENPOT_STORAGE_ASSETS_S3_BUCKET: <bucket-name>
# Optional if you want to use it with non AWS, S3 compatible service:
PENPOT_OBJECTS_STORAGE_S3_ENDPOINT: <endpoint-uri>
PENPOT_STORAGE_ASSETS_S3_ENDPOINT: <endpoint-uri>
```
<p class="advice">
These settings are equally useful if you have a Minio storage system.
</p>
### File Data Storage
__Since version 2.11.0__
You can change the default file data storage backend with `PENPOT_FILE_DATA_BACKEND` environment variable. Possible values are:
- `legacy-db`: the current default backend, continues storing the file data of files and snapshots in the same location as previous versions of Penpot (< 2.11.0), this is a conservative default behaviour and will be changed to `db` in next versions.
- `db`: stores the file data on an specific table (the future default backend).
- `storage`: stores the file data using the objects storage system (S3 or FS, depending on which one is configured)
This also comes with an additional feature that allows offload the "inactive" files on file storage backend and leaves the database only for the active files. To enable it, you should use the `enable-tiered-file-data-storage` flag and `db` as file data storage backend.
```bash
# Backend
PENPOT_FLAGS: [...] enable-tiered-file-data-storage
PENPOT_FILE_DATA_BACKEND: db
```
### Autosave
By default, Penpot stores manually saved versions indefinitely; these can be found in the History tab and can be renamed, restored, deleted, etc. Additionally, the default behavior of on-premise instances is to not keep automatic version history. This automatic behavior can be modified and adapted to each on-premise installation with the corresponding configuration.
@@ -539,7 +517,7 @@ You need to be very careful when configuring automatic versioning, as it can sig
This is how configuration looks for auto-file-snapshot
```bash
PENPOT_FLAGS: [...] enable-auto-file-snapshot # Enable automatic version saving
PENPOT_FLAGS: enable-auto-file-snapshot # Enable automatic version saving
# Backend
PENPOT_AUTO_FILE_SNAPSHOT_EVERY: 5 # How many save operations trigger the auto-save-version?

View File

@@ -1,5 +1,5 @@
---
title: 1.1 Recommended settings
title: 1.1 Recommended storage
desc: Learn recommended self-hosting settings, Docker & Kubernetes installs, configuration, and troubleshooting tips in Penpot's technical guide.
---
@@ -10,33 +10,3 @@ Disk requirements depend on your usage, with the primary factors being database
As a rule of thumb, start with a **minimum** database size of **50GB** to **100GB** with elastic sizing capability — this configuration should adequately support up to 10 editors. For environments with **more than 10 users**, we recommend adding approximately **5GB** of capacity per additional editor.
Keep in mind that database size doesn't grow strictly proportionally with user count, as it depends heavily on how Penpot is used and the complexity of files created. Most organizations begin with this baseline and elastic sizing approach, then monitor usage patterns monthly until resource requirements stabilize.
# About Valkey / Redis requirements
"Valkey is mainly used for coordinating websocket notifications and, since Penpot 2.11, as a cache. Therefore, disk storage will not be necessary as it will use the instance's RAM.
To prevent the cache from hogging all the system's RAM usage, it is recommended to use two configuration parameters which, both in the docker-compose.yaml provided by Penpot and in the official Helm Chart, come with default parameters that should be sufficient for most deployments:
```bash
## Recommended values for most Penpot instances.
## You can modify this value to follow your policies.
# Set maximum memory Valkey/Redis will use.
# Accepted units: b, k, kb, m, mb, g, gb
maxmemory 128mb
# Choose an eviction policy (see Valkey docs:
# https://valkey.io/topics/memory-optimization/ or for Redis
# https://redis.io/docs/latest/develop/reference/eviction/
# Common choices:
# noeviction, allkeys-lru, volatile-lru, allkeys-random, volatile-random,
# volatile-ttl, volatile-lfu, allkeys-lfu
#
# For Penpot, volatile-lfu is recommended
maxmemory-policy volatile-lfu
```
The `maxmemory` configuration directive specifies the maximum amount of memory to use for the cache data. If you are using a dedicated instance to host Valkey/Redis, we do not recommend using more than 60% of the available RAM.
With `maxmemory-policy` configuration directive, you can select the eviction policy you want to use when the limit set by `maxmemory` is reached. Penpot works fine with `volatile-lfu`, which evicts the least frequently used keys that have been marked as expired.

View File

@@ -1,14 +0,0 @@
---
title: Comments
order: 4
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="comments">Comments</h1>
<p class="main-paragraph">Comments allow the team to have one priceless conversation getting and providing feedback right over the designs and prototypes.<p>
<h2 id="comment-workspace">At the workspace</h2>
<p>At the workspace, activate the comment tool by clicking the comment icon in the navbar or pressing the <kbd>C</kbd> key. <a href="/user-guide/designing/workspace-basics/#comments">More about comments at the Workspace</a></p>
<h2 id="comment-viewmode">At the View mode</h2>
<p>You can activate comments at the View mode by pressing the comments icon at the top navbar. <a href="/user-guide/prototyping-testing/testing-view-mode/#viewmode-comments">More about comments at the View mode</a>.</p>

View File

@@ -1,28 +0,0 @@
---
title: Account & teams
order: 8
desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorials. Learn the interface, layers, objects, styling, and more.
---
<h1 id="section-1">Account & teams</h1>
<ul class="intro-sections">
<li>
<a href="/user-guide/account-teams/your-account">
<h2>Your account →</h2>
<p>Ways to start with Penpot</p>
</a>
</li>
<li>
<a href="/user-guide/account-teams/teams">
<h2>Teams →</h2>
<p>Info of interest about Penpot</p>
</a>
</li>
<li>
<a href="/user-guide/account-teams/comments/">
<h2>Comments →</h2>
<p>Info of interest about Penpot</p>
</a>
</li>
</ul>

View File

@@ -1,74 +0,0 @@
---
title: Your account
order: 1
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="account">Your account</h1>
<p class="main-paragraph">Your account settings can be changed at the user area, in <b>Your account</b>. Here you can make changes to your profile, password or account language, as well as generate personal access tokens and access release notes.</p>
<h3 id="your-account-profile">Profile
<a class="direct-link" href="#your-account-profile">#</a>
</h3>
<p>If you want to change the email address associated to your account or remove your account entirely, this can be done in the <b>Profile</b> section.</p>
<figure>
<img src="/img/interface/youraccount-profile.webp" alt="Penpot's profile" />
</figure>
<h3 id="your-account-password">Password
<a class="direct-link" href="#your-account-password">#</a>
</h3>
<p>If you want to change your password to a new one, this can be done in the <b>Password</b> section.</p>
<figure>
<img src="/img/interface/youraccount-password.webp" alt="Penpot's password" />
</figure>
<h3 id="your-account-notifications">Notifications
<a class="direct-link" href="#your-account-notifications">#</a>
</h3>
<p>At the <strong>Notifications</strong> section you can configure the email and dashboard notifications.</p>
<figure>
<img src="/img/interface/youraccount-notifications.webp" alt="Penpot's notifications" />
</figure>
<h3 id="your-account-settings">Settings
<a class="direct-link" href="#your-account-settings">#</a>
</h3>
<p>At the <strong>Settings</strong> section you can change the language and the UI color theme.</p>
<figure>
<img src="/img/interface/youraccount-settings.webp" alt="Penpot's settings" />
</figure>
<h3 id="interface-ui-theme">UI Theme</h3>
<p>Penpot's default interface is dark but you can switch anytime to a light option. You have 2 ways to change the theme:</p>
<ul>
<li>From "Your account" > "Settings".</li>
<li>Using the shortcut <kbd>Alt/⌥</kbd> + <kbd>M</kbd>.</li>
</ul>
<figure>
<a href="/img/interface/dashboard-light.webp" target="_blank">
<img src="/img/interface/dashboard-light.webp" alt="Penpot's dashboard" />
</a>
<figcaption>Penpot's dashboard in light mode</figcaption>
</figure>
<figure>
<a href="/img/interface/workspace-light.webp" target="_blank">
<img src="/img/interface/workspace-light.webp" alt="Penpot's workspace" />
</a>
<figcaption>Penpot's workspace in light mode</figcaption>
</figure>
<figure>
<a href="/img/interface/viewmode-light.webp" target="_blank">
<img src="/img/interface/viewmode-light.webp" alt="Penpot's view mode" />
</a>
<figcaption>Penpot's view mode in light mode</figcaption>
</figure>
<h3 id="your-account-accesstokens">Access tokens
<a class="direct-link" href="#your-account-accesstokens">#</a>
</h3>
<p>At the <strong>Asset tokens</strong> section you can manage your access tokens. <a href="https://help.penpot.app/technical-guide/integration/#access-tokens" target="_blank">Read more about access tokens here</a>.</p>

View File

@@ -0,0 +1,338 @@
---
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>
<h2 id="components-basics">Components basics</h2>
<p>A component consists of two elements:</p>
<ul>
<li><strong>Main component</strong>: The original source of truth. It defines the core properties of the component.</li>
<li><strong>Component copy</strong> (also known as instance): A duplicate of the main component that inherits its properties.</li>
</ul>
<figure>
<img src="/img/components/components-main-copy.webp" alt="Components main and copy" />
<figcaption>Mains and copies have different icons. Mains also have a title header at the viewport.</figcaption>
</figure>
<p>All component copies used in a file are linked in a way that updates made to the Main component can reflect in their component copies. You can override properties for component copies, so that you can manage singularities while maintaining properties in common.</p>
<h3 id="component-create">Create components</h3>
<h4>Create a component</h4>
<ol>
<li>Select an object or a group of them.</li>
<li>Press <kbd>Ctrl</kbd> + <kbd>K</kbd> or right click and select the option “Create component” at the object menu.</li>
</ol>
<figure>
<video title="Creating a component" muted="" playsinline="" controls="" width="auto" poster="/img/components/components-create.webp" height="auto">
<source src="/img/components/components-create.mp4" type="video/mp4">
</video>
</figure>
<h4>Duplicate a component</h4>
<p>You can duplicate a component <a href="/user-guide/layer-basics/#duplicating-layers">the same way</a> you can duplicate any other layer. When duplicating a component, you are creating a component copy that will be linked to its main component.</p>
<h4>Duplicate as main component</h4>
<p>You can duplicate a component as a new main component from the assets sidebar. Just select the component at the library, open the menu with right click and select the option "Duplicate main".</p>
<figure>
<video title="Duplicate main component" muted="" playsinline="" controls="" width="auto" poster="/img/components/components-duplicate-main.webp" height="auto">
<source src="/img/components/components-duplicate-main.mp4" type="video/mp4">
</video>
</figure>
<h4>Delete a main component</h4>
<p>You can delete main components and its copies anytime <a href="/user-guide/layer-basics/#deleting-layers">the same way</a> you can delete any other layer.</p>
<p>Deleting a main component at the viewport means deleting it at the assets library and viceversa, so be careful!</p>
<figure>
<video title="Deleting main components" muted="" playsinline="" controls="" width="auto" poster="/img/components/components-delete.webp" height="auto">
<source src="/img/components/components-delete.mp4" type="video/mp4">
</video>
</figure>
<h4>Restore a main component</h4>
<p>If a main component has been deleted and you have access to a copy of it, you can use the copy to restore its main. There are two ways to do it:</p>
<ul>
<li>From the <strong>viewport menu</strong>: Select the component copy of a deleted main component, right click and press the option "Restore main component".</li>
<li>From the <strong>sidebar menu</strong>: Open the sidebar menu of the component copy and press the option "Restore main component".</li>
</ul>
<figure>
<img src="/img/components/components-restore.webp" alt="Components main and copy" />
<figcaption>Mains and copies have different icons. Mains also have a title header at the viewport.</figcaption>
</figure>
<h3 id="component-find">Find main components</h3>
<p>Where's my component? There are ways to find main components at the assets panel and at the design viewport.</p>
<h4>Find a main component at the assets panel</h4>
<p>Select a main component at the viewport and then press "Show in assets panel" at the options of the right sidebar.</p>
<figure>
<video title="Show main component in the assets library" muted="" playsinline="" controls="" width="100%" poster="/img/components/components-show-asset.webp" height="auto">
<source src="/img/components/components-show-asset.mp4" type="video/mp4">
</video>
</figure>
<h4>Find a main component at the viewport</h4>
<p>Select a component copy and then press "Show main component" at the viewport menu or the right sidebar menu.</p>
<figure>
<video title="Show main component" muted="" playsinline="" controls="" width="100%" poster="/img/components/components-show-main.webp" height="auto">
<source src="/img/components/components-show-main.mp4" type="video/mp4">
</video>
</figure>
<h3 id="component-main-components-page">Main components page</h3>
<p>If you find a page at a file called "Main components" this will probably mean that the file had assets with the previous components system and has been migrated to the current components system. The previous system didn't have the components as layers at the design file, only at the assets library, so when migrating a file to the new version Penpot automatically creates a page where to place all the components, grouping them using the library groups structure.</p>
<figure>
<img src="/img/components/components-page-main.webp" alt="Main components page" />
</figure>
<h2 id="working-with-components">Working with components</h2>
<h3 id="component-group">Group components</h3>
<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: "<i>FOLDER NAME/COMPONENT NAME</i>". For example, "<i>Buttons/Alert Button</i>".</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>
</ol>
<figure>
<video title="Grouping components" muted="" playsinline="" controls="" width="100%" poster="/img/components/components-group.webp" height="auto">
<source src="/img/components/components-group.mp4" type="video/mp4">
</video>
</figure>
<h4>Ungroup components</h4>
<p>You can ungroup the components the same ways you can group them, via the menu option ("Ungroup" in this case) or renaming them.</p>
<h4>Drag components to groups</h4>
<p>One very direct way to move components between groups at the assets library is by dragging them.</p>
<figure>
<video title="Drag components" muted="" playsinline="" controls="" width="auto" poster="/img/components/components-drag.webp" height="auto">
<source src="/img/components/components-drag.mp4" type="video/mp4">
</video>
</figure>
<h3 id="component-detach">Detach components</h3>
<p>Detach a component copy to unlink it from its Main component and transform it into a group layer. Press <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>K</kbd> or right click and select the option “Detach instance” at the component menu.</p>
<p>You can also detach components in bulk by selecting several components and performing the same action.</p>
<h3 id="component-annotate">Annotate components</h3>
<p>You can add text annotations to main components. The annotations are shown in every component copy. It is extremely useful to attach specifications that can be read at each component copy.</p>
<figure>
<video title="Annotating components at Penpot" muted="" playsinline="" controls="" width="auto" poster="/img/components/components-annotation.webp" height="auto">
<source src="/img/components/components-annotation.mp4" type="video/mp4">
</video>
</figure>
<p>The annotations are also shown at the <a href="/user-guide/inspect">Inspect tab</a>, as another option to improve communication between designers and developers.</p>
<figure>
<img src="/img/components/components-annotations-inspect.webp" alt="Annotations at inspect tab" />
</figure>
<h2 id="component-overrides-relationships">Component Overrides & Relationships</h2>
<h3 id="component-overrides">Component overrides</h3>
<p>Main components represent the more generic information of an element in a design system. You will usually need to change specific things (like a text, a color or an icon) in a component while maintaining the inheritance of the rest of it properties. Component overrides allows you to do that in Penpot.</p>
<p>Overrides are modifications made in a specific copy that are not in its main component. With overrides you can keep changes at the component copies while maintaining synchronization with the Main component.</p>
<figure>
<img src="/img/components/components-overrides.webp" alt="Components overrides" />
</figure>
<h4>Reset overrides</h4>
<p>Right click and select the option “Reset overrides” at the component menu to get it to the state of the Main component.</p>
<figure>
<video title="Reset component overrides" muted="" playsinline="" controls="" width="auto" poster="/img/components/components-reset-overrides.webp" height="auto">
<source src="/img/components/components-reset-overrides.mp4" type="video/mp4">
</video>
</figure>
<h3 id="component-update">Update main from copies</h3>
<p>You can push changes made at a component copy to a main component:</p>
<ol>
<li>Select a component copy that has changes that override one or more properties of its main component.</li>
<li>Right click and select the option “Update main component” at the component menu. You can find this option at the viewport menu and at the sidebar menu.</li>
</ol>
<figure>
<img src="/img/components/components-update.webp" alt="Updating a main component from a copy" />
</figure>
<p>If the component that is about to be updated is located in a different file which is connected to this file as a <a href="/user-guide/libraries/#shared-libraries">shared library</a>, a notification will be shown offering the options to update or dismiss.</p>
<figure>
<img src="/img/components/components-update-shared.webp" alt="Prompt shown to update a main component that is in a shared library" />
</figure>
<h3 id="component-swap">Swap components</h3>
<p>Penpot allows you to easily substitute component copies with other component copies.</p>
<ol>
<li>Select a component <strong>copy</strong>. You can not swap main components.</li>
<li>At the right sidebar, press the component name to launch the swap menu.</li>
<li>Choose the component you want to swap with and click on it.</li>
</ol>
<p class="advice"><strong>Tip:</strong> The first options shown to swap a component are the ones at the same level inside the assets library, so group them properly.</p>
<figure>
<video title="Swapping components at Penpot" muted="" playsinline="" controls="" width="auto" poster="/img/components/components-swap.webp" height="auto">
<source src="/img/components/components-swap.mp4" type="video/mp4">
</video>
</figure>
<h2 id="component-variants">Component Variants</h2>
<p>Variants allow you to group similar components, such as buttons, icons, or toggles, into a single, customizable component. Rather than navigating through separate components for every possible state, size, or style, you can manage them all from one unified component using clearly defined properties.</p>
<p>Imagine a single button component that can switch between primary and secondary styles, active and disabled states, and small to large sizes. Useful, right? Thats the power of Variants.</p>
<h3 id="component-variants-why-are-variants-important">Why are Variants Important?</h3>
<ul>
<li><strong>Cleaner libraries</strong><br>
Keep related designs organized in the Design viewport, Layers panel, and swap menu. Variants streamline your components into tidy, manageable sets, allowing you to retain overrides when switching between them.
</li>
<li><strong>Faster design workflows</strong><br>
Make it easier to find and select the right version by quickly switching between states or styles using simple property controls.
</li>
<li><strong>Better team collaboration</strong><br>
With variants, you can match the way states are handled in code, helping designers and developers stay in sync, fostering better collaboration between design and development teams.
</li>
</ul>
<figure>
<iframe
width="672px"
height="378px"
src="https://peertube.kaleidos.net/videos/embed/v9Yh79hom5otcBEnqondBY"
title="Penpot Variants Demo"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
<figcaption>Penpot Variants release</figcaption>
</figure>
<h3 id="component-understanding-variants-properties-and-values">Understanding variants: properties and values</h3>
<p>A components variants are organized by properties and their values.</p>
<ul>
<li><strong>Properties</strong> define the dimensions that distinguish your variants (for example: <em>Color</em>, <em>Size</em>, <em>State</em>).</li>
<li><strong>Values</strong> are the specific options within a property (for example: <em>Primary/Secondary</em>, <em>Small/Large, Default/Hover/Pressed</em>)</li>
</ul>
<p>Each variant is simply one unique combination of values across all properties (for example, <code class="language-js">Color=Primary + Size=Small + State=Hover</code>).</p>
<p>Variants must have at least one property, and property values should be kept consistent to make switching predictable and to preserve overrides across connected layers.</p>
<h3 id="component-create-and-modify-variants">Create and modify variants</h3>
<h4 id="component-create-variants">Create variants</h4>
<p>You can create variants from an existing component or from another variant:</p>
<ul>
<li><strong>From a component:</strong> Press Ctrl + K or right-click and select the menu option <strong>Create variant</strong>.</li>
<li><strong>From a variant:</strong> Select the variant and press Ctrl + K or right-click and select the menu option <strong>Create variant</strong>.</li>
<li><strong>By dragging:</strong> Drag a main component into an existing component with variants to add it as a new variant.</li>
<li><strong>From the Design tab</strong> (right sidebar): Select a component or a variant, open the context menu next to the component name and select the menu option <strong>Create variant</strong>.</li>
</ul>
<figure>
<img src="/img/variants/01-variants-create.webp" alt="Variants creation button" />
</figure>
<p><strong>When a variant is created:</strong></p>
<ul>
<li>It appears next to the original in a dedicated variant area (by default in horizontal flex layout).</li>
<li>Shared layers between variants are automatically connected (<a href="#component-understanding-overrides">connection conditions</a>) so that overrides can be preserved.</li>
<li>Variants are named using their property values (e.g., <em>Primary / Hover</em>).</li>
</ul>
<figure>
<img src="/img/variants/02-variants-created.webp" alt="Variant created" />
</figure>
<h4 id="component-manage-variant-properties">Manage variant properties</h4>
<p>Properties are key to defining and differentiating your variants. They appear in the Design tab when a variant or component with variants is selected.</p>
<figure>
<img src="/img/variants/03-variants-property-add.webp" alt="Add variant property" />
</figure>
<h5>Add new properties</h5>
<ul>
<li><strong>From the Design tab:</strong> When the component or one of its variants is selected, you can add a new property via a menu. This property will be added to all existing variants with a default value (e.g., <em>Value 1</em>).</li>
<li><strong>From the Layers panel:</strong> using the formula <code class="language-js">[property_name]=[value]</code>.</li>
</ul>
<h5>Edit properties</h5>
<ul>
<li><strong>From the Design tab:</strong> Select a component or a variant, then click on the property name to edit its name and/or value.</li>
<li><strong>From the Layers panel:</strong> using the formula <code class="language-js">[property_name]=[value]</code>.</li>
</ul>
<figure>
<img src="/img/variants/04-variants-properties-edit.webp" alt="Edit variant property" />
</figure>
<h5>Delete properties</h5>
<ul>
<li><strong>From the Design tab:</strong> Select the main component (not an individual variant) and press the minus button next to the property.</li>
<li><strong>From the Layers panel:</strong> You can delete a property by editing the names of all variants so that none of them contain that property in their formula.</li>
</ul>
<p class="advice">Variants must have at least one property. You cant delete the last one.</p>
<p>When <strong>multiple variants are selected</strong>, the Design tab will show all their properties and values. If a property has different values across the selected variants, it will display “Mixed,” allowing you to override them collectively.</p>
<h4 id="component-delete-variants">Delete Variants</h4>
<ul>
<li>Select the variant, press right-click, and select the menu option <strong>Delete</strong>.</li>
<li><strong>Dragging</strong> a variant outside its component turns it into an independent component instead of deleting it.</li>
</ul>
<p class="advice">If you delete the last variant, the entire component is removed.</p>
<h4 id="component-restore-variants">Restore Variants</h4>
<p>If you have a copy of a variant whose original was deleted, you can restore it:</p>
<ul>
<li>Select the variant copy, press right-click, and select the menu option <strong>Restore variant</strong> (will show if the main component still exists).</li>
</ul>
<h3 id="component-use-variants">Use variants</h3>
<p>Once you have created your variants, you can place a copy of a component with variants into your design and then switch between its different versions.</p>
<h4 id="component-from-the-assets-tab">From the Assets tab</h4>
<p>Drag and drop a component with variants from the Assets tab onto the design viewport, just like you would with any other component. Once placed, you can then use its properties in the Design tab to switch to the desired variant.</p>
<h4 id="component-from-the-design-tab">From the Design tab</h4>
<p>When a variant is selected:</p>
<ul>
<li>Youll see its properties and values.</li>
<li>Change one or more property values to switch to another variant.</li>
<li>If your chosen combination doesnt exist, Penpot will suggest the closest match.</li>
</ul>
<figure>
<img src="/img/variants/05-variants-use.webp" alt="Using variants" />
</figure>
<h4 id="component-understanding-overrides">Understanding overrides</h4>
<p>A key benefit of variants is the ability to <strong>preserve overrides when you switch between them</strong>. An override is a specific change you make to a component instance that diverges from its original definition (e.g., changing text content or a specific color).</p>
<p>Layers between variants are considered connected if they:</p>
<ol>
<li>Share the <strong>same name</strong>.</li>
<li><strong>Are the same type</strong>. Rectangle, ellipse, paths, and boolean operations count as the same type.</li>
<li><strong>Have the same hierarchy level.</strong> Groups, boards, and layouts are considered equivalent.</li>
</ol>
<figure>
<img src="/img/variants/variants-connections-conditions.png" alt="Variants connections conditions" />
</figure>
<p><strong>Example:</strong> If <code class="language-js">Variant 1</code> has a text layer named <em>label</em> with red color, and you change its content to <em>Click here</em> in an instance, then switch to <code class="language-js">Variant 2</code> (which also has a <em>label</em> text layer), the <em>Click here</em> content will be preserved, and <code class="language-js">Variant 2</code>s color will be applied.</p>
<p><strong>Changing any of these</strong> (e.g., renaming or grouping a layer) breaks the connection, but reverting the change will restore it.</p>
<h4 id="component-bulk-converting-components-to-variants">Bulk converting components to variants</h4>
<p>If you already have multiple related components, you can combine them into a single component with variants:</p>
<ul>
<li><strong>From Assets tab</strong>: Select components in the same group and right-click → <strong>Combine as variants</strong>.</li>
<li><strong>From viewport</strong>: Select multiple components → Right-click → <strong>Combine as variants</strong>.</li>
<li><strong>From Design tab</strong>: If conditions are met, a Combine as variants button appears on the component card.</li>
</ul>
<figure>
<img src="/img/variants/06-variants-combine.webp" alt="Combining components as variants" />
</figure>
<p><strong>Conditions:</strong></p>
<ul>
<li>Components must be on the same page.</li>
<li>Components that already have variants cannot be combined.</li>
</ul>
<p><strong>When combined:</strong></p>
<ul>
<li>A variant area is created containing all former components.</li>
<li>Property names and values are generated from the component names.</li>
</ul>
<h4 id="component-transforming-variants-back-into-components">Transforming Variants Back into Components</h4>
<p>To turn a variant into an independent component:</p>
<ul>
<li>Drag it outside the variant area (Design viewport or Layers panel).</li>
<li>Cut and paste it outside the variant area.</li>
</ul>
<p>The new components name includes the original component name and the variants property values.</p>

View File

@@ -0,0 +1,38 @@
---
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>
<p class="main-paragraph">If you have purchased, personal or libre fonts that are not included in the catalog provided by Penpot, you can upload them from your computer and use them across the files of a team. <p>
<h2 id="customfonts-upload">Upload local fonts</h2>
<p>To use a font that you have on your local machine, first you need to upload it to the Penpot team where you want to use it.</p>
<p>You can find the “Fonts” section in the dashboard menu, at the left sidebar.</p>
<p><a href="/img/customfonts.png" target="_blank"><img src="/img/customfonts.png" alt="local fonts" /></a></p>
<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
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.
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>
<p><a href="/img/customfonts-upload.png" target="_blank"><img src="/img/customfonts-upload.png" alt="local fonts" /></a></p>
<h2 id="customfonts-families">Group fonts in font families</h2>
<p>Fonts with the same font family name will be grouped as a single font family. That means that at the font list that you will use at the files they will be shown as only one font with different variants available. </p>
<p>If you want to add a font variant (eg: Light) to a font family (eg: Helvetica) you only need to ensure during the upload process that it has the same font family name.</p>
<p><a href="/img/customfonts-families.png" target="_blank"><img src="/img/customfonts-families.png" alt="local fonts" /></a></p>
<h2 id="customfonts-edit">Edit custom fonts</h2>
<p>At the right side of a font family of the custom fonts list you can find a menu that allows you to edit the name of a font family and delete it.</p>
<h2 id="customfonts-using">Using custom fonts</h2>
<p>Custom fonts are added to the fonts catalog of a team and can be used at the workspace from the font list at the design sidebar.</p>
<p><img src="/img/customfonts-use.gif" alt="local fonts" /></p>
<h2>Fonts Licensing and Usage</h2>
<p>You should only upload fonts you own or have license to use in Penpot. Find out more in the Content rights section of <a href="https://penpot.app/terms" target="_blank">Penpot's Terms of Service</a>. You also might want to read about <a href="https://www.typography.com/faq" target="_blank">font licensing</a>.</p>

View File

@@ -1,172 +0,0 @@
---
title: Components
order: 3
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 a layer or group of layers that can be reused multiple times across files. This can help you maintain consistency across a group of designs.</p>
<p>A component consists of two elements:</p>
<ul>
<li><strong>Main component</strong>: The original source of truth. It defines the core properties of the component.</li>
<li><strong>Component copy</strong> (also known as instance): A duplicate of the main component that inherits its properties.</li>
</ul>
<figure>
<img src="/img/components/components-main-copy.webp" alt="Components main and copy" />
<figcaption>Mains and copies have different icons. Mains also have a title header at the viewport.</figcaption>
</figure>
<p>All component copies used in a file are linked in a way that updates made to the Main component can reflect in their component copies. You can override properties for component copies, so that you can manage singularities while maintaining properties in common.</p>
<h3 id="component-create">Create components</h3>
<h4>Create a component</h4>
<ol>
<li>Select a layer or a group of them.</li>
<li>Press <kbd>Ctrl</kbd> + <kbd>K</kbd> or right click and select the option “Create component” at the layer menu.</li>
</ol>
<figure>
<video title="Creating a component" muted="" playsinline="" controls="" width="auto" poster="/img/components/components-create.webp" height="auto">
<source src="/img/components/components-create.mp4" type="video/mp4">
</video>
</figure>
<h4>Duplicate a component</h4>
<p>You can duplicate a component <a href="/user-guide/designing/layers/#duplicating-layers">the same way</a> you can duplicate any other layer. When duplicating a component, you are creating a component copy that will be linked to its main component.</p>
<h4>Duplicate as main component</h4>
<p>You can duplicate a component as a new main component from the assets sidebar. Just select the component at the library, open the menu with right click and select the option "Duplicate main".</p>
<figure>
<video title="Duplicate main component" muted="" playsinline="" controls="" width="auto" poster="/img/components/components-duplicate-main.webp" height="auto">
<source src="/img/components/components-duplicate-main.mp4" type="video/mp4">
</video>
</figure>
<h4>Delete a main component</h4>
<p>You can delete main components and its copies anytime <a href="/user-guide/designing/layers/#delete-layers">the same way</a> you can delete any other layer.</p>
<p>Deleting a main component at the viewport means deleting it at the assets library and viceversa, so be careful!</p>
<figure>
<video title="Deleting main components" muted="" playsinline="" controls="" width="auto" poster="/img/components/components-delete.webp" height="auto">
<source src="/img/components/components-delete.mp4" type="video/mp4">
</video>
</figure>
<h4>Restore a main component</h4>
<p>If a main component has been deleted and you have access to a copy of it, you can use the copy to restore its main. There are two ways to do it:</p>
<ul>
<li>From the <strong>viewport menu</strong>: Select the component copy of a deleted main component, right click and press the option "Restore main component".</li>
<li>From the <strong>sidebar menu</strong>: Open the sidebar menu of the component copy and press the option "Restore main component".</li>
</ul>
<figure>
<img src="/img/components/components-restore.webp" alt="Components main and copy" />
<figcaption>Mains and copies have different icons. Mains also have a title header at the viewport.</figcaption>
</figure>
<h3 id="component-find">Find main components</h3>
<p>Where's my component? There are ways to find main components at the assets panel and at the design viewport.</p>
<h4>Find a main component at the assets panel</h4>
<p>Select a main component at the viewport and then press "Show in assets panel" at the options of the right sidebar.</p>
<figure>
<video title="Show main component in the assets library" muted="" playsinline="" controls="" width="100%" poster="/img/components/components-show-asset.webp" height="auto">
<source src="/img/components/components-show-asset.mp4" type="video/mp4">
</video>
</figure>
<h4>Find a main component at the viewport</h4>
<p>Select a component copy and then press "Show main component" at the viewport menu or the right sidebar menu.</p>
<figure>
<video title="Show main component" muted="" playsinline="" controls="" width="100%" poster="/img/components/components-show-main.webp" height="auto">
<source src="/img/components/components-show-main.mp4" type="video/mp4">
</video>
</figure>
<h3 id="component-main-components-page">Main components page</h3>
<p>If you find a page at a file called "Main components" this will probably mean that the file had assets with the previous components system and has been migrated to the current components system. The previous system didn't have the components as layers at the design file, only at the assets library, so when migrating a file to the new version Penpot automatically creates a page where to place all the components, grouping them using the library groups structure.</p>
<figure>
<img src="/img/components/components-page-main.webp" alt="Main components page" />
</figure>
<h3 id="component-group">Group components</h3>
<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: "<i>FOLDER NAME/COMPONENT NAME</i>". For example, "<i>Buttons/Alert Button</i>".</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>
</ol>
<figure>
<video title="Grouping components" muted="" playsinline="" controls="" width="100%" poster="/img/components/components-group.webp" height="auto">
<source src="/img/components/components-group.mp4" type="video/mp4">
</video>
</figure>
<h4>Ungroup components</h4>
<p>You can ungroup the components the same ways you can group them, via the menu option ("Ungroup" in this case) or renaming them.</p>
<h4>Drag components to groups</h4>
<p>One very direct way to move components between groups at the assets library is by dragging them.</p>
<figure>
<video title="Drag components" muted="" playsinline="" controls="" width="auto" poster="/img/components/components-drag.webp" height="auto">
<source src="/img/components/components-drag.mp4" type="video/mp4">
</video>
</figure>
<h3 id="component-detach">Detach components</h3>
<p>Detach a component copy to unlink it from its Main component and transform it into a group layer. Press <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>K</kbd> or right click and select the option “Detach instance” at the component menu.</p>
<p>You can also detach components in bulk by selecting several components and performing the same action.</p>
<h3 id="component-annotate">Annotate components</h3>
<p>You can add text annotations to main components. The annotations are shown in every component copy. It is extremely useful to attach specifications that can be read at each component copy.</p>
<figure>
<video title="Annotating components at Penpot" muted="" playsinline="" controls="" width="auto" poster="/img/components/components-annotation.webp" height="auto">
<source src="/img/components/components-annotation.mp4" type="video/mp4">
</video>
</figure>
<p>The annotations are also shown at the <a href="/user-guide/dev-tools/#inspect-design">Inspect tab</a>, as another option to improve communication between designers and developers.</p>
<figure>
<img src="/img/components/components-annotations-inspect.webp" alt="Annotations at inspect tab" />
</figure>
<h3 id="component-overrides">Component overrides</h3>
<p>Main components represent the more generic information of an element in a design system. You will usually need to change specific things (like a text, a color or an icon) in a component while maintaining the inheritance of the rest of it properties. Component overrides allows you to do that in Penpot.</p>
<p>Overrides are modifications made in a specific copy that are not in its main component. With overrides you can keep changes at the component copies while maintaining synchronization with the Main component.</p>
<figure>
<img src="/img/components/components-overrides.webp" alt="Components overrides" />
</figure>
<h4>Reset overrides</h4>
<p>Right click and select the option “Reset overrides” at the component menu to get it to the state of the Main component.</p>
<figure>
<video title="Reset component overrides" muted="" playsinline="" controls="" width="auto" poster="/img/components/components-reset-overrides.webp" height="auto">
<source src="/img/components/components-reset-overrides.mp4" type="video/mp4">
</video>
</figure>
<h3 id="component-update">Update main from copies</h3>
<p>You can push changes made at a component copy to a main component:</p>
<ol>
<li>Select a component copy that has changes that override one or more properties of its main component.</li>
<li>Right click and select the option “Update main component” at the component menu. You can find this option at the viewport menu and at the sidebar menu.</li>
</ol>
<figure>
<img src="/img/components/components-update.webp" alt="Updating a main component from a copy" />
</figure>
<p>If the component that is about to be updated is located in a different file which is connected to this file as a <a href="/user-guide/design-systems/libraries/#shared-libraries">shared library</a>, a notification will be shown offering the options to update or dismiss.</p>
<figure>
<img src="/img/components/components-update-shared.webp" alt="Prompt shown to update a main component that is in a shared library" />
</figure>
<h3 id="component-swap">Swap components</h3>
<p>Penpot allows you to easily substitute component copies with other component copies.</p>
<ol>
<li>Select a component <strong>copy</strong>. You can not swap main components.</li>
<li>At the right sidebar, press the component name to launch the swap menu.</li>
<li>Choose the component you want to swap with and click on it.</li>
</ol>
<p class="advice"><strong>Tip:</strong> The first options shown to swap a component are the ones at the same level inside the assets library, so group them properly.</p>
<figure>
<video title="Swapping components at Penpot" muted="" playsinline="" controls="" width="auto" poster="/img/components/components-swap.webp" height="auto">
<source src="/img/components/components-swap.mp4" type="video/mp4">
</video>
</figure>

View File

@@ -1,40 +0,0 @@
---
title: Design Systems
order: 3
desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorials. Learn the interface, layers, objects, styling, and more.
---
<h1 id="section-3">Design Systems</h1>
<ul class="intro-sections">
<li>
<a href="/user-guide/design-systems/assets">
<h2>Assets →</h2>
<p>Ways to start with Penpot</p>
</a>
</li>
<li>
<a href="/user-guide/design-systems/libraries">
<h2>Libraries →</h2>
<p>Info of interest about Penpot</p>
</a>
</li>
<li>
<a href="/user-guide/design-systems/components">
<h2>Components →</h2>
<p>Speed your design workflow</p>
</a>
</li>
<li>
<a href="/user-guide/design-systems/variants">
<h2>Variants →</h2>
<p>Info of interest about Penpot</p>
</a>
</li>
<li>
<a href="/user-guide/design-systems/design-tokens">
<h2>Design Tokens →</h2>
<p>Info of interest about Penpot</p>
</a>
</li>
</ul>

View File

@@ -1,72 +0,0 @@
---
title: Libraries
order: 2
desc: Use Penpot's libraries for reusable design elements! Learn to create, manage, and share components, colors, and typography. Try Penpot - it's free!
---
<h1 id="libraries">Libraries</h1>
<p class="main-paragraph">Libraries may include components, graphics, colors and typographies. Learn how to create and manage them to better organize the pieces of your designs and speed your workflow.</p>
<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>You have two ways to access the file library from the file <a href="/user-guide/first-steps/the-interface/#interface-workspace">workspace</a>:</p>
<ul>
<li>Click the assets tab icon at the left sidebar.</li>
<li>Press <kbd>Alt/⌥</kbd> + <kbd>i</kbd>.</li>
</ul>
<figure>
<img src="/img/libraries/assets-tab.webp" alt="Library assets tab">
</figure>
<h3 id="shared-libraries">Shared libraries</h3>
<h4>Publish as shared library</h4>
<p>You can publish any regular file as a shared library. This means that the file library of this file will be available to be connected to other files that exist in the same team, so its library assets can be reused.</p>
<p>There are two ways to publish a library:</p>
<ul>
<li>Using the file main menu.</li>
<li>From the libraries panel, that you can launch by clicking on the "Libraries" button that is found at the assets tab.</li>
</ul>
<figure>
<img src="/img/libraries/libraries-publish-menu.webp" alt="Publish library">
<figcaption>Publishing a library from the main menu</figcaption>
</figure>
<figure>
<img src="/img/libraries/libraries-publish-panel.webp" alt="Publish library">
<figcaption>Publishing a library from the libraries panel</figcaption>
</figure>
<h4>Unpublish a shared library</h4>
<p>You can unpublish any library anytime the same way you can publish it, both from the file menu and the libraries panel.</p>
<p>Unpublishing a library will disconnect it from the files where it was connected. The assets that have already been used in other files will remain, but no longer linked with the now unpublished library.</p>
<h4>Library updates</h4>
<p></p>
<figure>
<img src="/img/libraries/libraries-updates.webp" alt="Update libraries">
</figure>
<h4>Connect shared libraries</h4>
<p>To add a Shared Library from another file, launch the libraries panel, then search and select the available libraries. If you see the message "There are no Shared Libraries available", start by <a href="/user-guide/design-systems/libraries/#shared-libraries">publishing other files as a shared library</a> or add from our <a href="https://penpot.app/libraries-templates">Libraries & templates</a>.</p>
<figure>
<video title="Connecting a shared library" muted="" playsinline="" controls="" width="100%" poster="/img/libraries/libraries-launch.webp" height="auto">
<source src="/img/libraries/libraries-launch.mp4" type="video/mp4">
</video>
</figure>
<h4>Disconnect shared library</h4>
<p>You can disconnect any library anytime from the libraries panel just by clicking on the disconnect button.</p>
<figure>
<img src="/img/libraries/libraries-disconnect.webp" alt="Disconnect libraries">
</figure>
<h4>Use shared libraries</h4>
<p>Shared libraries will be listed at the assets panel, at the workspace left sidebar. You can expand and collapse them to access the assets of each connected shared library.</p>
<figure>
<img src="/img/libraries/libraries-sidebar.webp" alt="Connected libraries list">
</figure>
<h4>Open shared library file</h4>
<p>Click on the arrow icon at the right of a shared library name to go to the file where the library is and edit its contents.</p>
<figure>
<img src="/img/libraries/libraries-open.webp" alt="Open libraries">
</figure>

View File

@@ -1,168 +0,0 @@
---
title: Variants
order: 4
desc: Streamline your design workflow with Penpot's Components guide! Learn to create, duplicate, group, and manage reusable components.
---
<h1 id="variants">Variants</h1>
<p class="main-paragraph">Variants allow you to group similar components, such as buttons, icons, or toggles, into a single, customizable component. Rather than navigating through separate components for every possible state, size, or style, you can manage them all from one unified component using clearly defined properties.</p>
<p class="main-paragraph">Imagine a single button component that can switch between primary and secondary styles, active and disabled states, and small to large sizes. Useful, right? Thats the power of Variants.</p>
<h3 id="component-variants-why-are-variants-important">Why are Variants Important?</h3>
<ul>
<li><strong>Cleaner libraries</strong><br>
Keep related designs organized in the Design viewport, Layers panel, and swap menu. Variants streamline your components into tidy, manageable sets, allowing you to retain overrides when switching between them.
</li>
<li><strong>Faster design workflows</strong><br>
Make it easier to find and select the right version by quickly switching between states or styles using simple property controls.
</li>
<li><strong>Better team collaboration</strong><br>
With variants, you can match the way states are handled in code, helping designers and developers stay in sync, fostering better collaboration between design and development teams.
</li>
</ul>
<figure>
<iframe
width="672px"
height="378px"
src="https://peertube.kaleidos.net/videos/embed/v9Yh79hom5otcBEnqondBY"
title="Penpot Variants Demo"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
<figcaption>Penpot Variants release</figcaption>
</figure>
<h3 id="component-understanding-variants-properties-and-values">Understanding variants: properties and values</h3>
<p>A components variants are organized by properties and their values.</p>
<ul>
<li><strong>Properties</strong> define the dimensions that distinguish your variants (for example: <em>Color</em>, <em>Size</em>, <em>State</em>).</li>
<li><strong>Values</strong> are the specific options within a property (for example: <em>Primary/Secondary</em>, <em>Small/Large, Default/Hover/Pressed</em>)</li>
</ul>
<p>Each variant is simply one unique combination of values across all properties (for example, <code class="language-js">Color=Primary + Size=Small + State=Hover</code>).</p>
<p>Variants must have at least one property, and property values should be kept consistent to make switching predictable and to preserve overrides across connected layers.</p>
<h3 id="component-create-and-modify-variants">Create and modify variants</h3>
<h4 id="component-create-variants">Create variants</h4>
<p>You can create variants from an existing component or from another variant:</p>
<ul>
<li><strong>From a component:</strong> Press Ctrl + K or right-click and select the menu option <strong>Create variant</strong>.</li>
<li><strong>From a variant:</strong> Select the variant and press Ctrl + K or right-click and select the menu option <strong>Create variant</strong>.</li>
<li><strong>By dragging:</strong> Drag a main component into an existing component with variants to add it as a new variant.</li>
<li><strong>From the Design tab</strong> (right sidebar): Select a component or a variant, open the context menu next to the component name and select the menu option <strong>Create variant</strong>.</li>
</ul>
<figure>
<img src="/img/variants/01-variants-create.webp" alt="Variants creation button" />
</figure>
<p><strong>When a variant is created:</strong></p>
<ul>
<li>It appears next to the original in a dedicated variant area (by default in horizontal flex layout).</li>
<li>Shared layers between variants are automatically connected (<a href="#component-use-variants">connection conditions</a>) so that overrides can be preserved.</li>
<li>Variants are named using their property values (e.g., <em>Primary / Hover</em>).</li>
</ul>
<figure>
<img src="/img/variants/02-variants-created.webp" alt="Variant created" />
</figure>
<h4 id="component-manage-variant-properties">Manage variant properties</h4>
<p>Properties are key to defining and differentiating your variants. They appear in the Design tab when a variant or component with variants is selected.</p>
<figure>
<img src="/img/variants/03-variants-property-add.webp" alt="Add variant property" />
</figure>
<h5>Add new properties</h5>
<ul>
<li><strong>From the Design tab:</strong> When the component or one of its variants is selected, you can add a new property via a menu. This property will be added to all existing variants with a default value (e.g., <em>Value 1</em>).</li>
<li><strong>From the Layers panel:</strong> using the formula <code class="language-js">[property_name]=[value]</code>.</li>
</ul>
<h5>Edit properties</h5>
<ul>
<li><strong>From the Design tab:</strong> Select a component or a variant, then click on the property name to edit its name and/or value.</li>
<li><strong>From the Layers panel:</strong> using the formula <code class="language-js">[property_name]=[value]</code>.</li>
</ul>
<figure>
<img src="/img/variants/04-variants-properties-edit.webp" alt="Edit variant property" />
</figure>
<h5>Delete properties</h5>
<ul>
<li><strong>From the Design tab:</strong> Select the main component (not an individual variant) and press the minus button next to the property.</li>
<li><strong>From the Layers panel:</strong> You can delete a property by editing the names of all variants so that none of them contain that property in their formula.</li>
</ul>
<p class="advice">Variants must have at least one property. You cant delete the last one.</p>
<p>When <strong>multiple variants are selected</strong>, the Design tab will show all their properties and values. If a property has different values across the selected variants, it will display “Mixed,” allowing you to override them collectively.</p>
<h4 id="component-delete-variants">Delete Variants</h4>
<ul>
<li>Select the variant, press right-click, and select the menu option <strong>Delete</strong>.</li>
<li><strong>Dragging</strong> a variant outside its component turns it into an independent component instead of deleting it.</li>
</ul>
<p class="advice">If you delete the last variant, the entire component is removed.</p>
<h4 id="component-restore-variants">Restore Variants</h4>
<p>If you have a copy of a variant whose original was deleted, you can restore it:</p>
<ul>
<li>Select the variant copy, press right-click, and select the menu option <strong>Restore variant</strong> (will show if the main component still exists).</li>
</ul>
<h3 id="component-use-variants">Use variants</h3>
<p>Once you have created your variants, you can place a copy of a component with variants into your design and then switch between its different versions.</p>
<h4 id="component-from-the-assets-tab">From the Assets tab</h4>
<p>Drag and drop a component with variants from the Assets tab onto the design viewport, just like you would with any other component. Once placed, you can then use its properties in the Design tab to switch to the desired variant.</p>
<h4 id="component-from-the-design-tab">From the Design tab</h4>
<p>When a variant is selected:</p>
<ul>
<li>Youll see its properties and values.</li>
<li>Change one or more property values to switch to another variant.</li>
<li>If your chosen combination doesnt exist, Penpot will suggest the closest match.</li>
</ul>
<figure>
<img src="/img/variants/05-variants-use.webp" alt="Using variants" />
</figure>
<h4 id="component-understanding-overrides">Understanding overrides</h4>
<p>A key benefit of variants is the ability to <strong>preserve overrides when you switch between them</strong>. An override is a specific change you make to a component instance that diverges from its original definition (e.g., changing text content or a specific color).</p>
<p>Layers between variants are considered connected if they:</p>
<ol>
<li>Share the <strong>same name</strong>.</li>
<li><strong>Are the same type</strong>. Rectangle, ellipse, paths, and boolean operations count as the same type.</li>
<li><strong>Have the same hierarchy level.</strong> Groups, boards, and layouts are considered equivalent.</li>
</ol>
<figure>
<img src="/img/variants/variants-connections-conditions.png" alt="Variants connections conditions" />
</figure>
<p><strong>Example:</strong> If <code class="language-js">Variant 1</code> has a text layer named <em>label</em> with red color, and you change its content to <em>Click here</em> in an instance, then switch to <code class="language-js">Variant 2</code> (which also has a <em>label</em> text layer), the <em>Click here</em> content will be preserved, and <code class="language-js">Variant 2</code>s color will be applied.</p>
<p><strong>Changing any of these</strong> (e.g., renaming or grouping a layer) breaks the connection, but reverting the change will restore it.</p>
<h4 id="component-bulk-converting-components-to-variants">Bulk converting components to variants</h4>
<p>If you already have multiple related components, you can combine them into a single component with variants:</p>
<ul>
<li><strong>From Assets tab</strong>: Select components in the same group and right-click → <strong>Combine as variants</strong>.</li>
<li><strong>From viewport</strong>: Select multiple components → Right-click → <strong>Combine as variants</strong>.</li>
<li><strong>From Design tab</strong>: If conditions are met, a Combine as variants button appears on the component card.</li>
</ul>
<figure>
<img src="/img/variants/06-variants-combine.webp" alt="Combining components as variants" />
</figure>
<p><strong>Conditions:</strong></p>
<ul>
<li>Components must be on the same page.</li>
<li>Components that already have variants cannot be combined.</li>
</ul>
<p><strong>When combined:</strong></p>
<ul>
<li>A variant area is created containing all former components.</li>
<li>Property names and values are generated from the component names.</li>
</ul>
<h4 id="component-transforming-variants-back-into-components">Transforming Variants Back into Components</h4>
<p>To turn a variant into an independent component:</p>
<ul>
<li>Drag it outside the variant area (Design viewport or Layers panel).</li>
<li>Cut and paste it outside the variant area.</li>
</ul>
<p>The new components name includes the original component name and the variants property values.</p>

View File

@@ -1,24 +1,24 @@
---
title: Design Tokens
order: 5
title: 10· Design Tokens
---
<h1 id="design-tokens">Design Tokens</h1>
<p class="main-paragraph">Design tokens are the building blocks of all UI elements, the same tokens are used in designs, tools, and code. They include colors, typography, spacing, shadows, and any visual element that affects a layer: all these properties collectively make up a design system or a visual inheritance.</p>
<p class="main-paragraph">Design tokens are the building blocks of all UI elements, the same tokens are used in designs, tools, and code. They include colors, typography, spacing, shadows, and any visual element that affects an object: all these properties collectively make up a design system or a visual inheritance.</p>
<figure>
<img src="/img/design-tokens/01-tokens-cover.webp" alt="Tokens cover" />
</figure>
<h3>Why Design Tokens?</h3>
<h3 id="design-tokens-why">Why Design Tokens?</h3>
<p>Design tokens act as a single source of truth, a common language that can be translated and used in any other tool or framework capable of reading the token format. With Design Tokens, you can create, manage, and synchronize these visual elements within Penpot and across other design tools, keeping your designs consistent and making your workflows faster and easier to maintain.</p>
<p>You can also integrate Design Tokens with other core Penpot features, such as components and grid & flex layout, plus plugins will be able to access the tokens API (coming soon) making it even more powerful.</p>
<h3>W3C DTCG Format</h3>
<h3 id="design-tokens-format">W3C DTCG Format</h3>
<p>Penpot Design Tokens adhere to the <a href="https://design-tokens.github.io/community-group/format/" target="_blank">Design Tokens Format Module</a> and <a href="https://www.designtokens.org/glossary/" target="_blank">its definitions</a>, a draft by the <a href="https://www.w3.org/community/design-tokens/" target="_blank">W3C DTCG</a>. Penpot ensures compatibility across various disciplines, tools, and technologies by following the most standardized approach available for design tokens.</p>
<p>Tokens can be exported from Penpot or integrated into other tools directly, without conversion. Additionally, the knowledge gained from using Design Tokens in Penpot remains valuable, regardless of whether you continue using Penpot or a different tool or technology.</p>
<h2 id="design-tokens-use-create">Creating a token</h2>
<h2 id="design-tokens-use">Using Tokens</h2>
<h3 id="design-tokens-use-create">Creating a token</h3>
<p>You can create reusable and semantic tokens to be referenced in your designs at the <strong>Tokens</strong> panel. In this panel, youll find all the available types of tokens in Penpot arranged alphabetically, with existing tokens being shown at the top of the list.</p>
<figure>
<img src="/img/design-tokens/02-tokens-create.webp" alt="Tokens create" />
@@ -31,7 +31,7 @@ order: 5
</ul>
<p>Once you have named the token and assigned it a value, click <strong>Save</strong> to store the token and start referencing it.</p>
<h2 id="design-tokens-aliases">Referencing tokens into values (aliases)</h2>
<h3 id="design-tokens-aliases">Referencing tokens into values (aliases)</h3>
<p>When assigning a value to a token, you can reference existing tokens - these are called aliases at the <a href="https://www.designtokens.org/glossary/" target="_blank">DTCG Glossary</a>.</p>
<figure>
<img src="/img/design-tokens/03-tokens-aliases.webp" alt="Tokens aliases" />
@@ -41,7 +41,7 @@ order: 5
<p>If the value of the referenced token changes, this will also change the value of the tokens where it is referenced.</p>
<p class="advice">References to existing tokens are case sensitive.</p>
<h2 id="design-tokens-equations">Using equations</h2>
<h3 id="design-tokens-equations">Using equations</h3>
<p>Token types with numerical values also accept mathematical equations. If, for example, you create a <strong>spacing.small</strong> token with the value of <strong>2</strong>, and you then want to create a <strong>spacing.medium</strong> token that is twice as large, you could do so by writing <code class="language-js">{spacing.small} * 2</code> in its value. As a result, <strong>spacing.medium</strong> would have a value of <strong>4</strong>.</p>
<p>Say you have a <strong>spacing.scale</strong> token with a value of <strong>2</strong>. You could also use this token in the equation to calculate the value of <strong>spacing.medium</strong> by writing <code class="language-js">{spacing.small} * {spacing.scale}</code> in its value.</p>
<figure>
@@ -55,21 +55,21 @@ order: 5
<li><code class="language-js">/</code> for division.</li>
</ul>
<h2 id="design-tokens-edit">Editing a token</h2>
<h3 id="design-tokens-edit">Editing a token</h3>
<p>Tokens can be edited by right-clicking the token and selecting <strong>Edit token</strong>. This will allow you to change the tokens name, value and description. Once the changes are made, click <strong>Save</strong>.</p>
<figure>
<img src="/img/design-tokens/05-tokens-edit.webp" alt="Tokens edit" />
</figure>
<p class="advice">Renaming tokens will break any references to their old names. If a token is already applied somewhere, you'll need to reapply it after renaming. This can lead to extra work, so rename with caution. We're actively working on a solution to handle this automatically, ensuring renamed tokens stay linked to their properties without additional effort.</p>
<h2 id="design-tokens-duplicate">Duplicating a token</h2>
<h3 id="design-tokens-duplicate">Duplicating a token</h3>
<p>Tokens can be duplicated by right-clicking the token you wish to duplicate and selecting <strong>Duplicate token</strong>. This will create a copy of the selected token within the same set, with <code class="language-js">-copy</code> added to its name.</p>
<h2 id="design-tokens-delete">Deleting a token</h2>
<h3 id="design-tokens-delete">Deleting a token</h3>
<p>Tokens can be deleted by right-clicking the token you wish to delete and selecting <strong>Delete token</strong>.</p>
<h2 id="design-tokens-available">Available tokens</h2>
<p>You can apply tokens to the properties of any <a href="/user-guide/designing/layers/" target="_blank">layer</a>. There are two ways to apply tokens to a selection:</p>
<p>You can apply tokens to the properties of any <a href="/user-guide/objects/" target="_blank">object</a>. There are two ways to apply tokens to a selection:</p>
<ul>
<li>Right-click on tokens to specify a particular property that you want to apply.</li>
<li>Left-click on tokens to apply the assumtion. Assumptions can vary across different token types. For example, for the <strong>color</strong> type the assumtion is that you want to apply the token as a <strong>fill</strong>.</li>
@@ -322,7 +322,7 @@ Applying a Typography Token will detach any Typography Style previously applied,
<h4 id="design-tokens-font-size">Font Size</h4>
<p>Font size tokens define the vertical size of glyphs/characters as an individual property. In typography, the letter spacing and line height properties are related to the font size.</p>
<p>Font sizes are typically defined using a proportional scale. You can use math operations with references in order to create dynamic typography scales that follow a particular multiplier, like Golden Ratio.</p>
<p><strong>Tip:</strong> Use <a href="#design-tokens-number" target="_blank">Number Tokens</a> (unitless token) to create stunning typographic scales though design tokens:</p>
<p><strong>Tip:</strong> Use <a href="/user-guide/design-tokens/#design-tokens-number" target="_blank">Number Tokens</a> (unitless token) to create stunning typographic scales though design tokens:</p>
<figure>
<img src="/img/design-tokens/30-tokens-type-font-size-scale.webp" alt="Font size scale" />
</figure>
@@ -423,40 +423,6 @@ ExtraBold Italic
<p>This token can be applied directly to a text element or be used as a reference in a Typography Composite Token.</p>
<h3 id="typography-composite-tokens">Typography composite token</h3>
<p><strong>Typography tokens</strong> are composite entities that group several text properties into a single token definition. They allow you to define and reuse complete text styles in a consistent way.</p>
<p>Each property within a typography token can either reference an existing <a href="#design-tokens-typography">individual typography token</a> (for example, <em>font-size</em> or <em>font-family</em>) or use a hardcoded value. The behavior and syntax of individual typography tokens are described in the previous section of this guide.</p>
<figure>
<img src="/img/design-tokens/36-tokens-composite-typography.webp" alt="Typography composite token" />
</figure>
<h4 id="reference-composite-token">Reference another Typography Composite Token</h4>
<p>You can also reference another existing <strong>Typography Composite Token</strong> instead of defining each property manually. When doing so, Penpot resolves all individual properties from the referenced token.</p>
<figure>
<img src="/img/design-tokens/34-tokens-composite-typography-alias.webp" alt="Typography composite token" />
</figure>
<h4 id="line-height-property">Line height property</h4>
<p>The <strong>Typography Token</strong> includes a <em>line-height</em> property, which is not available as an individual token. This is because line-height depends on the font size to be calculated properly. Make sure the <em>font-size</em> property is defined before setting <em>line-height</em>.</p>
<figure>
<img src="/img/design-tokens/35-tokens-composite-typography-lineheight.webp" alt="Typography composite token" />
</figure>
<p>Accepted values for the line-height input:</p>
<ul>
<li><strong>Unitless number:</strong> interpreted as a multiplier of the font size. This is Penpots default behavior.</li>
<li><strong>Percentage (%):</strong> converted internally to a multiplier.</li>
<li><strong>Pixel (px) or rem value:</strong> if using rem, Penpot calculates the proportion relative to the font size and converts it to a multiplier.</li>
<li><strong>References:</strong> you can also reference <a href="#design-tokens-number">number</a> or <a href="#design-tokens-dimensions">dimension</a> tokens.</li>
</ul>
<h4 id="apply-typography-token">Apply a Typography token</h4>
<p>A <strong>Typography composite token</strong> can be applied to a full text layer to set all typography properties at once. This lets you manage complete text styles using a single token instead of combining multiple individual ones.</p>
<p>When applying a Typography composite token to a layer, any previously applied <em>Typography composite token</em> or <em>style</em> will be detached. The same happens in reverse. Only one of them can be active at a time.</p>
<h2 id="design-tokens-sets">Token Sets</h2>
<p>Token Sets allow you to split your tokens up into multiple files in order to create organized groups or collections of tokens. It enables efficient management and customization within design files. For example you can group all your color sets, sizing sets or platform-specific sets. The purpose of tokens sets is to organize them in a way that matches your needs.</p>
<figure>
@@ -467,7 +433,7 @@ ExtraBold Italic
<p>When creating a token set, its recommended that you assign it a unique name to ensure clarity. Token set names are not included in individual token names by default so it is possible to have tokens with the same name belonging to different token sets.</p>
<p>Token sets can be enabled or disabled. If a set is disabled, its tokens will be excluded from the token resolution process.</p>
<h3>Creating Token Sets</h3>
<h3 id="design-tokens-sets-create">Creating Token Sets</h3>
<p>There are two ways to create a token set at the <strong>Tokens</strong> tab:</p>
<ol>
<li>Click on the <strong>+</strong> next to <strong>Sets</strong>;</li>
@@ -477,7 +443,7 @@ ExtraBold Italic
<p>When a token set is selected, the tokens within the selected set are displayed on the panel below.</p>
<h3>Editing Token Sets</h3>
<h3 id="design-tokens-sets-edit">Editing Token Sets</h3>
<p>Right-click a token set to perform these quick actions:</p>
<ol>
<li><strong>Rename</strong>: Give the set a new name and press Enter.</li>
@@ -492,7 +458,7 @@ ExtraBold Italic
<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>Creating Token Set Folders</h3>
<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 folder" />
@@ -518,14 +484,14 @@ ExtraBold Italic
<p>When you have various themes inside a group, only one of the themes in this group can be active.</p>
<p>Having your sets clubbed under groups makes it more accessible to switch from a matrix of themes.</p>
<h3>Creating Token Themes</h3>
<h3 id="design-tokens-themes-create">Creating Token Themes</h3>
<p>To create a new theme, click the <strong>Create one</strong> button in the Themes section. You can create a group (this is optional) or add an existing one, and then you then need to assign a name to your theme and click on <strong>Save Theme</strong>.</p>
<p>Your new theme will now appear on the Theme lists. Youll need to enable the tokens sets that you want to include in the theme, clicking on the button “no active sets”. Here you can also activate and deactivate it, as well as delete the theme.</p>
<figure>
<img src="/img/design-tokens/17-tokens-themes-create.webp" alt="Tokens themes create" />
</figure>
<h3>Editing Themes</h3>
<h3 id="design-tokens-themes-edit">Editing Themes</h3>
<p>In the <strong>Themes</strong> section, you can find a dropdown to activate and deactivate themes. If there are no active themes, the dropdown shows a message of: “no theme active”.</p>
<figure>
<img src="/img/design-tokens/19-tokens-themes-edit.webp" alt="Tokens themes edit" />
@@ -542,7 +508,7 @@ ExtraBold Italic
<li>Creates a new theme.</li>
</ol>
<h3>Grouping Themes</h3>
<h3 id="design-tokens-themes-group">Grouping Themes</h3>
<p>You can categorize your themes into groups. This allows you to generate a matrix of potential combinations involving color themes, brands, modes, and more.</p>
<figure>
<img src="/img/design-tokens/20-tokens-themes-group.webp" alt="Tokens themes group" />
@@ -569,7 +535,7 @@ ExtraBold Italic
<li><strong>Export:</strong> Click <strong>Tools</strong>, then select <strong>Export</strong> to view export options.</li>
</ol>
<h3>Import Options</h3>
<h3 id="design-tokens-import-options">Import Options</h3>
<h4>ZIP file</h4>
<p>You can import tokens from a <strong>.zip</strong> file. This file can either contain a single JSON file or a folder structure with multiple files. The ZIP import option provides flexibility for organizing your tokens before importing them into Penpot.</p>
@@ -638,7 +604,7 @@ ExtraBold Italic
</pre>
<figcaption>The main folder name wont be used to build token set names, so in this example, <strong>folder</strong> will be ignored in the set names.</figcaption>
<h3>Export Options</h3>
<h3 id="design-tokens-export-options">Export Options</h3>
<p>Just like with importing, you can export tokens, themes and sets either in a single JSON file or in multiple files. There is no difference in the content being exported; the choice depends on your team's preferences for file organization: a single file with all the tokens, sets and themes, or a folder structure with separated JSON files organized by sets.</p>
<p>In both cases you can preview the result of the export options:</p>
@@ -679,4 +645,4 @@ ExtraBold Italic
</figure>
<p>Here you can view and change the value.</p>
<p>The input accepts values with px and unitless, which will be interpreted as pixels.</p>
-->
-->

View File

@@ -1,40 +0,0 @@
---
title: Designing
order: 2
desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorials. Learn the interface, layers, objects, styling, and more.
---
<h1 id="section-2">Designing</h1>
<ul class="intro-sections">
<li>
<a href="/user-guide/designing/workspace-basics">
<h2>Workspace basics →</h2>
<p>Workspace basics</p>
</a>
</li>
<li>
<a href="/user-guide/designing/layers">
<h2>Layers →</h2>
<p>Info of interest about Penpot</p>
</a>
</li>
<li>
<a href="/user-guide/designing/color-stroke/">
<h2>Color & Strokes→</h2>
<p>Info of interest about Penpot</p>
</a>
</li>
<li>
<a href="/user-guide/designing/text-typo">
<h2>Text & Typography→</h2>
<p>Info of interest about Penpot</p>
</a>
</li>
<li>
<a href="/user-guide/designing/flexible-layouts">
<h2>Flexible layouts →</h2>
<p>Info of interest about Penpot</p>
</a>
</li>
</ul>

View File

@@ -1,519 +0,0 @@
---
title: Layers
order: 2
desc: "Work with Penpot's layers: boards, shapes, text, paths, and graphics. Learn to create, select, rename, and customize boards for optimal workflow."
---
<h1 id="layers">Layers</h1>
<p class="main-paragraph">Layers are objects or items that you can place in the viewport. Boards, shapes, texts, paths and graphics are layers. The following describes the different layers that you have
available in Penpot, and how to get the most of them.</p>
<h2 id="available-layers">Available layers</h2>
<h3 id="Boards">Boards</h3>
<p>Boards are layers that serve as your high-level containers for content organization and layout. 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/prototyping-testing/testing-view-mode/">View mode</a>, acting as screens of a design or pages of a document. Also, layers 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>
<h4>Create boards</h4>
<p>To create a board, use the board tool at the toolbar or the shortcut <kbd>B</kbd>.</p>
<figure>
<img src="/img/objects/board-tool.webp" alt="Board tool">
</figure>
<p>Then, with the board tool selected, you have two options:</p>
<ul>
<li><strong>Select a board size upfront</strong>. You can choose one of the provided presets with the most common resolution for devices and standard print sizes</li>
<li><strong>Click-and-drag</strong> to draw a frame of approximate size, then immediately edit its width/height values to be precise.</li>
</ul>
<figure>
<video title="Create board" muted="" playsinline="" controls="" width="100%" poster="/img/objects/board-create.webp" height="auto">
<source src="/img/objects/board-creation.mp4" type="video/mp4">
</video>
</figure>
<p><strong>TIP:</strong> Create a board around one or more selected layers using the option "Selection to board" at the menu or the shortcut <kbd>Ctrl/⌘</kbd> + <kbd>Alt</kbd> + <kbd>G</kbd>.</p>
<h4>Select boards</h4>
<p>There are two different cases in terms of selecting boards:</p>
<ul>
<li>For first level boards that have at least one inside, click on the board name or <kbd>Ctrl/⌘</kbd> + click on the board area to select it and then drag</li>
<li>For the rest (empty first level boards and inside boards) just click to select.</li>
</ul>
<figure>
<video title="Select board" muted="" playsinline="" controls="" width="auto" poster="/img/objects/board-select.webp" height="auto">
<source src="/img/objects/board-select.mp4" type="video/mp4">
</video>
</figure>
<h4>Rename boards</h4>
<p>There several ways to rename boards:</p>
<ul>
<li>Double click on the board name at the workspace viewport.</li>
<li>Double click on the board name at the layers panel.</li>
<li>Press <kbd>Alt/⌥</kbd> + <kbd>N</kbd> to rename the board at the layers panel.</li>
<li>Right click to show the menu and select "Rename".</li>
</ul>
<figure>
<video title="Rename board" muted="" playsinline="" controls="" width="auto" poster="/img/objects/board-rename.webp" height="auto">
<source src="/img/objects/board-rename.webm" type="video/webm">
</video>
</figure>
<h4>Set board as thumbnail</h4>
<p>At the workspace, select a specific board to be the file thumbnail that will be shown at the dashboard in the file card.</p>
<p>To set a custom thumbnail:</p>
<ol>
<li>Select a board.</li>
<li>Right click to show the menu and select "Set as thumbnail" or press <kbd>Shift</kbd> <kbd>T</kbd>.</li>
</ol>
<figure>
<video title="Set board as thumbnail" muted="" playsinline="" controls="" width="auto" poster="/img/objects/board-thumbnail.webp" height="auto">
<source src="/img/objects/board-thumbnail.mp4" type="video/mp4">
</video>
</figure>
<h4>Copy link to board</h4>
<p>You can get the link to each individual board, making it easy to share them with team members or include direct links in documentation.</p>
<figure>
<img src="/img/objects/board-copy-link.webp" alt="copy link to board">
</figure>
<p>There are two ways to copy a direct link to a board:</p>
<ul>
<li>Using the menu: Select the board, right click and select the "Copy link" option.</li>
<li>Using the shortcut: Select the board and press <kbd>Shift/⇧</kbd> + <kbd>Alt/⌥</kbd> + <kbd>C</kbd>.</li>
</ul>
<h4>Clip content</h4>
<p>Boards offer the option to clip its content (or not).</p>
<figure>
<video title="Clip board" muted="" playsinline="" controls="" width="100%" poster="/img/objects/board-clip.webp" height="auto">
<source src="/img/objects/board-clip.mp4" type="video/mp4">
</video>
</figure>
<h4>Show in View mode</h4>
<p>Boards offer the option to be shown as a separate board/screen in the <a href="/user-guide/first-steps/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>
<li>Boards created at first level (<a href="/user-guide/designing/workspace-basics/#viewport">the viewport</a>): shown by default.</li>
<li>Boards created inside other boards: not shown by default.</li>
</ul>
<figure>
<img src="/img/objects/board-show.webp" alt="board show in view mode">
</figure>
<h4>Show fill in exports</h4>
<p> Sometimes you dont need the artboards to be part of your designs, but only their support to work on them.
Penpot allows you to decide if the fill of an artboard will be shown in exports, you just have to check/uncheck the "Show in exports" option which is below the fill setting.</p>
<figure>
<img src="/img/objects/board-fill.webp" alt="show board fill in exports">
</figure>
<h4>Resize board to fit to content</h4>
<p>You can adjust the board size to fit its content by clicking the icon in the design sidebar.</p>
<figure>
<img src="/img/objects/board-fit.webp" alt="Resize board to fit to content button">
</figure>
<h4>Board guides</h4>
<p>You can set guides on boards that will assist with aligning layers.</p>
<p>Read more about <a href="/user-guide/designing/workspace-basics/#guides">guides</a>.</p>
<figure>
<img src="/img/objects/board-guides.webp" alt="board guides">
</figure>
<h4>Prototyping boards</h4>
<p>You can connect boards with other boards to create rich interactions.</p>
<p>Read more about <a href="/user-guide/prototyping-testing/prototyping/">prototyping</a>.</p>
<figure>
<img src="/img/objects/board-prototyping.webp" alt="prototyping with boards">
</figure>
<h3 id="rectangles-ellipses">Rectangles and ellipses</h3>
<p>Rectangle and ellipses are two basic “primitive” geometric shapes that are useful when starting
a design.</p>
<p>The shortcut keys are <kbd>E</kbd> for ellipses and <kbd>R</kbd> for rectangles.</p>
<p>To find out more about how to edit and modify these shapes go to <a href="/user-guide/designing/layers/#layer-actions">Layer basics</a>.</p>
<figure>
<video title="Rectangles and ellipses" muted="" playsinline="" controls="" width="auto" poster="/img/objects/rectangles-ellipses.webp" height="auto">
<source src="/img/objects/rectangles-ellipses.mp4" type="video/mp4">
</video>
</figure>
<h3 id="text">Text</h3>
<p> (NOTA: El grosso de este contenido está en su propia sección. Aquí vendría un texto introductorio y un link a la <a href="/user-guide/designing/text-typo/">sección en cuestión</a>. )</p>
<h3 id="curves">Curves (freehand)</h3>
<p>The curve tool allows a path to be created directly in a freehand mode.
Select the curve tool by clicking on the icon at the toolbar or pressing <kbd>Shift/⇧</kbd> + <kbd>c</kbd>.
<p>The path created will contain a lot of points, but it is edited the same way as any other curve.</p>
<h3 id="paths">Paths (bezier)</h3>
<p>A path is composed of two or more nodes and the line segments between them, which may also be curved. To draw a new path you have to select the path tool by clicking on the icon at the toolbar or pressing <kbd>P</kbd>. Then you have two ways to create the path:</p>
<ol>
<li><strong>Click</strong> to create a new corner node.</li>
<li><strong>Click and drag</strong> to create a curved node.</li>
</ol>
<p>To finish the path:</p>
<ol>
<li><strong>Close it</strong> clicking over the starting node.</li>
<li><strong>Leave it open</strong> pressing <kbd>Esc</kbd> or <kbd>Enter</kbd> to stop editing. Then press <kbd>Esc</kbd> to exit the edit mode.</li>
</ol>
<p><strong>Tip:</strong> If you hold <kbd>Shift/⇧</kbd> while adding nodes the angle between the current and the next will change in 45 degree increments.</p>
<figure>
<video title="Paths" muted="" playsinline="" controls="" width="100%" poster="/img/objects/path-create.webp" height="auto">
<source src="/img/objects/path-create.mp4" type="video/mp4">
</video>
</figure>
<h4>Edit nodes</h4>
<p>To edit a node double click on a path or select and press <kbd>Enter</kbd>.
You can choose to edit individual nodes or create new ones. Press <kbd>Esc</kbd> to exit node edition. </p>
<figure>
<video title="Edit nodes" muted="" playsinline="" controls="" width="100%" poster="/img/objects/nodes-edit.webp" height="auto">
<source src="/img/objects/nodes-edit.mp4" type="video/mp4">
</video>
</figure><h4>Node types</h4>
<p>There are two types of nodes: curve or corner (straight). The type of a selected node can be changed at the bezier menu. Curved nodes have bezier handles that allow the curvature of a path to be modified.</p>
<figure>
<video title="Node types" muted="" playsinline="" controls="" width="100%" poster="/img/objects/node-types.webp" height="auto">
<source src="/img/objects/node-types.mp4" type="video/mp4">
</video>
</figure>
<h3 id="images">Images</h3>
<h4>Insert images</h4>
<p>There are several options for inserting an image into a Penpot file:</p>
<ul>
<li>Use the <strong>image tool</strong> at the toolbar or press <kbd>K</kbd> to insert images in your file system.</li>
<li><strong>Drag</strong> an image from your computer to the viewport.</li>
<li>Copy an image & paste it or drag it right from a <strong>browser</strong>.</li>
<li>Drag an image from a Penpot <strong>library</strong>.</li>
</ul>
<h4>Images aspect ratio</h4>
<p>Images fill the layer backgrounds by default, so they take up the entire layer while maintaining the aspect ratio. This is great for flexible designs because the images can adapt to different sizes.</p>
<p>However, if you don't want an image to keep its aspect ratio when resizing, you just have to uncheck the option in the image settings.</p>
<figure>
<video title="Changing the aspect ratio of an image" muted="" playsinline="" controls="" width="100%" poster="/img/objects/image-ratio.webp" height="auto">
<source src="/img/objects/image-ratio.mp4" type="video/mp4">
</video>
</figure>
<h2 id="layer-actions">Layer actions</h2>
<h3 id="creating-layers">Create</h3>
<p>To create a layer you have to select the type of layer by clicking the selected tool (board, rectangle, ellipse, text, image, path or curve) at the toolbar. Then you usually have to click and drag your mouse on the viewport. </p>
<p>Hold <kbd>Shift/⇧</kbd> while creating an ellipse or a rectangle to maintain equal width and height.</p>
<figure>
<video title="Layers create" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-create.webp" height="auto">
<source src="/img/layers/layers-create.mp4" type="video/mp4">
</video>
</figure>
<h3 id="duplicating-layers">Duplicate</h3>
<p>There are several ways to duplicate a layer:</p>
<ol>
<li>You can press <kbd>Ctrl/⌘</kbd> + <kbd>D</kbd> to duplicate a layer right over a selected layer. </li>
<li>If you press right click over a selected layer at the viewport or at the layers panel you can use the option at the layer menu. </li>
<li>You can also select a layer and drag while pressing <kbd>Alt/⌥</kbd> so you can simultaneously duplicate and drag the new layer.</li>
</ol>
<figure>
<video title="Duplicate layers" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-duplicate.webp" height="auto">
<source src="/img/layers/layers-duplicate.mp4" type="video/mp4">
</video>
</figure>
<h3 id="delete-layers">Delete</h3>
<p>There are a couple ways to delete a layer. </p>
<ol>
<li>You can press <kbd>Supr/⌫</kbd> to delete a selected layer. </li>
<li>If you press right click over a selected layer at the viewport or at the layers panel you can use the option at the layer menu.</li>
</ol>
<h3 id="move-layers">Move</h3>
<p>To move one or more layers on the viewport you have to select them first and then click and drag the selection where you want to place them. You can also use the design panel to set a precise position relative to the viewport or the board.</p>
<figure>
<video title="Move layers" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-move.webp" height="auto">
<source src="/img/layers/layers-move.mp4" type="video/mp4">
</video>
</figure>
<h3 id="select-layers">Select</h3>
<p>The simplest way to select a layer is to click on it. Make sure that you have the “move” pointer selected at the toolbar. </p>
<p>To select multiple layers you can click and drag around the layers you want to select. You can also click more than one layer while pressing <kbd>Shift/⇧</kbd>. If you hold <kbd>Shift/⇧</kbd> and click you can deselect layers individually.</p>
<figure>
<video title="Select layers" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-select.webp" height="auto">
<source src="/img/layers/layers-select.mp4" type="video/mp4">
</video>
</figure>
<h4>Selecting layers at the layers panel</h4>
<ol>
<li>Click a layer to do a single selection.</li>
<li>Press <kbd>Ctrl/⌘</kbd> while clicking two or more layers to do a multiple selection.</li>
<li>If you press <kbd>Shift/⇧</kbd> while selecting two or more layers all the layers within the selection area will be selected.</li>
</ol>
<figure>
<video title="Select layers" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-multiselect.webp" height="auto">
<source src="/img/layers/layers-multiselect.mp4" type="video/mp4">
</video>
</figure>
<h4>Select layers ignoring groups (deep selection)</h4>
<p>If you want to select an element that is difficult to reach because it is under a group of elements, hold <kbd>Ctrl/⌘</kbd> to make the selection ignore group areas and treat all the layers as being at the same level.</p>
<figure>
<video title="Select layers" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-deepselect.webp" height="auto">
<source src="/img/layers/layers-deepselect.mp4" type="video/mp4">
</video>
</figure>
<h4>Select layers inside groups</h4>
<p>To <strong>select a layer inside a group</strong> you do double click. First click selects the group, second click selects a layer.</p>
<h4>Select layer menu</h4>
<p>At the dropdown menu (right click on a layer to show it) there's the option "Select layer" that allows the user to select one layer among the ones that are under the cursor's location.</p>
<p><img src="/img/layers-select-menu.gif" alt="layers select" /></p>
<h3 id="hide-lock">Hide and lock layers</h3>
<h4>Hide and show layers</h4>
<p>You can control the visibility of any layer by clicking the eye icon next to it in the Layers panel. When a layer is hidden, it will not appear on the canvas, but you can still select it in the Layers panel, move its order, or modify its properties. The eye icon always indicates whether a layer is visible or hidden, making it easy to manage complex designs.</p>
<h4>Lock and unlock</h4>
<p>Locking a layer helps prevent accidental changes or movement on the canvas. When a layer is locked, it cannot be moved or edited directly in the canvas area. However, you can still select a locked layer in the Layers panel and adjust its properties, such as color, effects, or name. The lock icon next to the layers name shows its locked status, helping you keep your design organized and protected.</p>
<figure>
<video title="Layers hide and lock" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-hide-lock.webp" height="auto">
<source src="/img/layers/layers-hide-lock.mp4" type="video/mp4">
</video>
</figure>
<h3 id="group-layers">Group</h3>
<p>Grouped layers can be moved, transformed or styled at the same time. </p>
<ul>
<li><strong>Group:</strong> To group two or more layers, select them and then press <kbd>Ctrl/⌘</kbd> + <kbd>G</kbd>. You can also use the option at the layers menu that you can open with right click.</li>
<li><strong>Ungroup:</strong> Press <kbd>Shift/⇧</kbd> + <kbd>G</kbd> or use the option at the layers menu that you can open with right click over the selected group.</li>
</ul>
<figure>
<video title="Group layers" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-group.webp" height="auto">
<source src="/img/layers/layers-group.mp4" type="video/mp4">
</video>
</figure>
<h3 id="mask-layers">Mask</h3>
<p>A mask is a layer that does a clipping and only shows parts of a layer or multiple layers that fall within its shape. </p>
<ul>
<li><strong>Mask layers:</strong> Select more than one layer or a group of them. Then you can apply the masking using the option at the layers menu or by pressing <kbd>Ctrl/⌘</kbd> + <kbd>M</kbd>. The shape that is at the lowest level at the layer list will be used as a mask. </li>
<li><strong>Unmask layers:</strong> Select a mask and then press <kbd>Shift/⇧</kbd> + <kbd>Ctrl/⌘</kbd> + <kbd>M</kbd> or use the option at the layers menu.</li>
</ul>
<figure>
<video title="Mask layers" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-mask.webp" height="auto">
<source src="/img/layers/layers-mask.mp4" type="video/mp4">
</video>
</figure>
<h3 id="resize-layers">Resize</h3>
<p>To resize a selected layer you can use the handles at the edges of the selection box. Make sure the cursor is in resizing mode. You can also use the design panel where you can link width and height.</p>
<ul>
<li>Hold <kbd>Shift/⇧</kbd> while resizing the layer to preserve its aspect ratio.</li>
<li>Hold <kbd>Alt/⌥</kbd> while resizing the layer to do it from the center and resize simultaneously two opposite sides.</li>
</ul>
<figure>
<video title="Resize layers" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-resize.webp" height="auto">
<source src="/img/layers/layers-resize.mp4" type="video/mp4">
</video>
</figure>
<h3 id="scale-elements">Scale elements, texts and properties</h3>
<p>Activate the scale tool by pressing <kbd>K</kbd> or from the main file menu to scale elements while maintaining their visual aspect. Once it is activated you can resize texts, layers and groups and preserve their aspect ratio while scaling their properties proportionally, including strokes, shadows, blurs and corners.
<figure>
<video title="Scale layers" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-scale.webp" height="auto">
<source src="/img/layers/layers-scale.mp4" type="video/mp4">
</video>
</figure>
<h3 id="rotate-layers">Rotate</h3>
<p>To rotate selected layers you can use the handles at the edges of the selection box. Make sure the cursor is in rotation mode. If you hold <kbd>Ctrl/⌘</kbd> while rotation the angle will change in 45 degree increments. You can also find this option at the design panel.</p>
<figure>
<video title="Rotate layers" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-rotate.webp" height="auto">
<source src="/img/layers/layers-rotate.mp4" type="video/mp4">
</video>
</figure>
<h3 id="flip-layers">Flip</h3>
<p>You can find the options to flip layers in their contextual menu (select the layer and right click). You also have shortcuts to do this:</p>
<ul>
<li><strong>Flip layers horizontally:</strong> Select the layer and press <kbd>Shift/⇧</kbd> + <kbd>H</kbd></li>
<li><strong>Flip layers vertically:</strong> Select the layer and then press <kbd>Shift/⇧</kbd> + <kbd>V</kbd>.</li>
</ul>
<figure>
<video title="Flip layers" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-flip.webp" height="auto">
<source src="/img/layers/layers-flip.mp4" type="video/mp4">
</video>
</figure>
<h3 id="aling-distribute-layers">Align and distribute</h3>
<p>Aligning and distributing layers can be found at the top of the Design panel. </p>
<h4>Align layers</h4>
<p>Aligning will move all the selected layers to a position relative to one of them. For instance, aligning top will align the elements with the edge of the top-most element.</p>
<figure>
<video title="Align layers" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-align.webp" height="auto">
<source src="/img/layers/layers-align.mp4" type="video/mp4">
</video>
</figure>
<h4>Distribute layers</h4>
<p>Distributing layers to position them vertically and horizontally with equal distances between them.</p>
<figure>
<video title="Distribute layers" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-distribute.webp" height="auto">
<source src="/img/layers/layers-distribute.mp4" type="video/mp4">
</video>
</figure>
<h3 id="boolean-operators">Boolean operators</h3>
<p>It is possible to combine shapes in a group in different ways to create more complex layers by using
"boolean" operators. Boolean operators are non destructive and the original shapes remain grouped and available for more editing. There are five boolean operations available: union, difference, intersection, exclusion and flatten. Using boolean operations allows many graphic options and possibilities for your designs.</p>
<figure>
<video title="Boolean operators" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-boolean.webp" height="auto">
<source src="/img/layers/layers-boolean.mp4" type="video/mp4">
</video>
</figure>
<ul>
<li><strong>Union:</strong> the resulting combination is the sum of the shapes.</li>
<li><strong>Difference:</strong> the opposite of union, the resulting layer has the area of the shape on top has been cut out from the shape at the bottom.</li>
<li><strong>Intersection:</strong> creates a group whose shape is the overlapping area between the shapes.</li>
<li><strong>Exclusion:</strong> the opposite of intersection, the resulting shape is the area that does not overlap between the shapes.</li>
<li><strong>Flatten:</strong> Tecnically not a boolean operator, this is the way to permanently combine the paths of a group of shapes in a single one.</li>
</ul>
<h3 id="constraints">Resizing constraints</h3>
<p>Constraints allow you to decide how layers will behave when resizing its container.</p>
<h4>Apply constraints</h4>
<p>Constraints allow you to decide how layers will behave when resizing its parent container. You can apply horizontal and vertical constraints for every layer.</p>
<p>To apply constraints select a layer and use the constraints map or the constraints selectors at the design panel.</p>
<figure>
<video title="Constraints" muted="" playsinline="" controls="" width="auto" poster="/img/layers/layers-constraints.webp" height="auto">
<source src="/img/layers/layers-constraints.mp4" type="video/mp4">
</video>
</figure>
<p>Constraints are set to “Scale” by default, but you have other options.</p>
<h5>Horizontal constraints</h5>
<ul>
<li><strong>Left</strong>: The layer maintains its size and position relative to the left side of its parent container.</li>
<li><strong>Right</strong>: The layer maintains its size and position relative to the right side of its parent container.</li>
<li><strong>Left & right</strong>: The layer resizes while maintaining its distance to both horizontal sides of its parent container.</li>
<li><strong>Center</strong>: The layer maintains its size and position relative to the horizontal center of its parent container.</li>
<li><strong>Scale</strong>: The layer will horizontally resize proportionally to its parent container size.</li>
</ul>
<figure>
<img src="/img/layers/layers-constraints-h.webp" alt="Horizontal constraints">
</figure>
<h5>Vertical constraints</h5>
<ul>
<li><strong>Top</strong>: The layer maintains its size and position relative to the top side of its parent container.</li>
<li><strong>Bottom</strong>: The layer maintains its size and position relative to the bottom side of its parent container.</li>
<li><strong>Top & bottom</strong>: The layer resizes while maintaining its distance to both vertical sides of its parent container.</li>
<li><strong>Center</strong>: The layer maintains its size and position relative to the vertical center of its parent container.</li>
<li><strong>Scale</strong>: The layer will vertically resize proportionally to its parent container size.</li>
</ul>
<figure>
<img src="/img/layers/layers-constraints-v.webp" alt="Vettical constraints">
</figure>
<h2 id="styling-layers">Styling layers</h2>
<p>Penpot has a variety of properties for each layer. When selected, the options are displayed in the design panel on the right.</p>
<h3 id="radius">Border radius</h3>
<p>You can customize the border radius of rectangles and images, with the option to customize each corner individually.</p>
<figure>
<video title="Border radius" muted="" playsinline="" controls="" width="100%" poster="/img/styling/corners.webp" height="auto">
<source src="/img/styling/corners.mp4" type="video/mp4">
</video>
</figure>
<h3 id="shadow">Shadow</h3>
<p>Adding shadows is easy from the design panel. You can add as many as you want.</p>
<figure>
<img alt="Layer shadows" src="/img/styling/shadow.webp"/>
</figure>
<p>Shadow options are:</p>
<ul>
<li><strong>Type</strong> - Drop (outside the layer), inner (inside the layer)</li>
<li><strong>Horizontal position</strong> (X)</li>
<li><strong>Vertical position</strong> (Y)</li>
<li><strong>Blur</strong></li>
<li><strong>Spread</strong></li>
<li><strong>Color and opacity</strong></li>
</ul>
<h3 id="blur">Blur</h3>
<p>You can set a blur for each and every layer at Penpot.</p>
<p><strong></strong>Applying a lot and/or big values for blurs can affect Penpots performance as it requires a lot from the browser.</p>
<figure>
<video title="Apply blur to a layer" muted="" playsinline="" controls="" width="100%" poster="/img/styling/blur.webp" height="auto">
<source src="/img/styling/blur.mp4" type="video/mp4">
</video>
</figure>
<h3 id="blend">Opacity and blend</h3>
<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"/>
</figure>
<p>Blend options available:</p>
<ul>
<li><strong>Normal</strong></li>
<li><strong>Darken</strong></li>
<li><strong>Multiply</strong></li>
<li><strong>Color burn</strong></li>
<li><strong>Lighten</strong></li>
<li><strong>Screen</strong></li>
<li><strong>Color dodge</strong></li>
<li><strong>Overlay</strong></li>
<li><strong>Soft light</strong></li>
<li><strong>Hard light</strong></li>
<li><strong>Difference</strong></li>
<li><strong>Exclusion</strong></li>
<li><strong>Hue</strong></li>
<li><strong>Saturation</strong></li>
<li><strong>Color</strong></li>
<li><strong>Luminosity</strong></li>
</ul>
<h3 id="copy-paste-properties">Copy/Paste properties</h3>
<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">
<source src="/img/styling/copy-properties.mp4" type="video/mp4">
</video>
</figure>
<p>Using the layer menu</p>
<ol>
<li>Select one layer.</li>
<li>Right click to show the layer menu.</li>
<li>Press <strong>Copy/Paste as... > Copy properties</strong>.</li>
<li>Select one or more other layers.</li>
<li>Right click to show the layer/s menu.</li>
<li>Press <strong>Copy/Paste as... > Paste properties</strong>.</li>
</ol>
<p>Using Shortcuts</p>
<ul>
<li><strong>Copy properties</strong>: <kbd>Ctrl/⌘</kbd> + <kbd>Alt/⌥</kbd> + <kbd>C</kbd></li>
<li><strong>Paste properties</strong>: <kbd>Ctrl/⌘</kbd> + <kbd>Alt/⌥</kbd> + <kbd>V</kbd></li>
</ul>

View File

@@ -1,89 +0,0 @@
---
title: Text & Typography
order: 5
desc: Penpot's guide on custom fonts! Upload, manage, and use custom fonts in Penpot! Enhance your designs with personalised typography.
---
<h1 id="text-typo">Text & Typography</h1>
<h2 id="text">Text</h2>
<p>To insert text you have to activate the text tool by first clicking on the icon at the toolbar or pressing <kbd>T</kbd>. Then you have two ways to create a text layer:</p>
<ol>
<li><strong>Click</strong> to create a textbox without any specific dimensions.</li>
<li><strong>Drag</strong> to create a textbox with a fixed size.</li>
</ol>
<figure>
<video title="Create text" muted="" playsinline="" controls="" width="auto" poster="/img/objects/text-create.webp" height="auto">
<source src="/img/objects/text-create.mp4" type="video/mp4">
</video>
</figure>
<p><strong>Tips for resizing</strong></p>
<ul>
<li>Double-click on the right side of the bounding box to set the resize setting to auto-width.</li>
<li>Double-click on the bottom side of the bounding box to set the resize setting to auto-height.</li>
</ul>
<h4>Edit and style text content</h4>
<p>Press <kbd>Enter</kbd> with a text layer selected to start editing the text content. You can style parts of the text content as rich text.</p>
<figure>
<img src="/img/objects/text-edit.webp" alt="editing text">
</figure>
<h3>Text options</h3>
<figure>
<img src="/img/objects/text-options.webp" alt="text options">
</figure>
<ol>
<li><strong>Font family.</strong> Penpot includes by default the <a href="https://fonts.google.com/" target=”_blank”>Google Fonts</a> cataloge. You can also <a href="/user-guide/designing/text-typo/#custom-fonts">install your own fonts</a>.</li>
<li><strong>Font size.</strong></li>
<li><strong>Font type.</strong></li>
<li><strong>Line height</strong> (in pixels).</li>
<li><strong>Letter spacing</strong> (in pixels).</li>
<li><strong>Text case:</strong> none, uppercase, lowercase, titlecase.</li>
<li><strong>Horizontal alignment:</strong> left, center, right, justify.</li>
<li><strong>Sizing:</strong> auto height, auto width, fixed size.</li>
<li><strong>Vertical alignment:</strong> top, center, bottom.</li>
<li><strong>Decoration:</strong> none, underline, strikethrough.</li>
<li><strong>Direction:</strong> LTR (left to right), RTL (right to left).</li>
</ol>
<h2 id="rtl-support">RTL support</h2>
<p>Diversity and inclusion is a major Penpot concern and that's why we love to give support to RTL languages, unlike most design tools.</p>
<p>If you write in arabic, hebrew or other RTL language text direction will be automatically detected in text layers.</p>
<figure>
<img src="/img/layers/layers-rtl.webp" alt="RTL support">
</figure>
<h2 id="custom-fonts">Custom fonts</h2>
<p>If you have purchased, personal or libre fonts that are not included in the catalog provided by Penpot, you can upload them from your computer and use them across the files of a team. <p>
<h3 id="customfonts-upload">Upload local fonts</h3>
<p>To use a font that you have on your local machine, first you need to upload it to the Penpot team where you want to use it.</p>
<p>You can find the “Fonts” section in the dashboard menu, at the left sidebar.</p>
<p><a href="/img/customfonts.png" target="_blank"><img src="/img/customfonts.png" alt="local fonts" /></a></p>
<h4>To upload a local font:</h4>
<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
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.
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>
<p><a href="/img/customfonts-upload.png" target="_blank"><img src="/img/customfonts-upload.png" alt="local fonts" /></a></p>
<h3 id="customfonts-families">Group fonts in font families</h3>
<p>Fonts with the same font family name will be grouped as a single font family. That means that at the font list that you will use at the files they will be shown as only one font with different variants available. </p>
<p>If you want to add a font variant (eg: Light) to a font family (eg: Helvetica) you only need to ensure during the upload process that it has the same font family name.</p>
<p><a href="/img/customfonts-families.png" target="_blank"><img src="/img/customfonts-families.png" alt="local fonts" /></a></p>
<h3 id="customfonts-edit">Edit custom fonts</h3>
<p>At the right side of a font family of the custom fonts list you can find a menu that allows you to edit the name of a font family and delete it.</p>
<h3 id="customfonts-using">Using custom fonts</h3>
<p>Custom fonts are added to the fonts catalog of a team and can be used at the workspace from the font list at the design sidebar.</p>
<p><img src="/img/customfonts-use.gif" alt="local fonts" /></p>
<h3>Fonts Licensing and Usage</h3>
<p>You should only upload fonts you own or have license to use in Penpot. Find out more in the Content rights section of <a href="https://penpot.app/terms" target="_blank">Penpot's Terms of Service</a>. You also might want to read about <a href="https://www.typography.com/faq" target="_blank">font licensing</a>.</p>

View File

@@ -1,22 +0,0 @@
---
title: Export & Import
order: 7
desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorials. Learn the interface, layers, objects, styling, and more.
---
<h1 id="export-import">Export & Import</h1>
<ul class="intro-sections">
<li>
<a href="/user-guide/export-import/export-import-files/">
<h2>Export/Import Penpot files →</h2>
<p>Ways to start with Penpot</p>
</a>
</li>
<li>
<a href="/user-guide/export-import/exporting-layers/">
<h2>Exporting layers →</h2>
<p>Exporting layers</p>
</a>
</li>
</ul>

View File

@@ -1,28 +1,27 @@
---
title: Exporting layers
order: 2
desc: Learn how to export layers in Penpot, the free, open-source design collaboration tool. This guide covers export presets, file formats, and more!
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 layers</h1>
<h1 id="export">Exporting objects</h1>
<p class="main-paragraph">In Penpot you can setup export presets for different file formats and scales.</p>
<h2 id="export-howto">How to export</h2>
<p>You can set up different export configurations to suit your needs. Each export configuration is called "export preset".</p>
<h3>Create export preset</h3>
<p>To export a layer you need to select it and at the Design panel add an export preset pressing the “+” button of the Export section.</p>
<p>Export presets can be also found at the <strong><a href="/user-guide/first-steps/the-interface/#interface-viewmode" target="_blank">View mode</a></strong> with the code tab activated.</p>
<p>To export an object you need to select it and at the Design panel add an export preset pressing the “+” button of the Export section.</p>
<p>Export presets can be also found at the <strong><a href="/user-guide/the-interface/#interface-viewmode" target="_blank">View mode</a></strong> with the code tab activated.</p>
<figure>
<video title="Export presets" muted="" playsinline="" controls="" width="100%" poster="/img/export/export-presets.webp" height="auto">
<source src="/img/export/export-presets.mp4" type="video/mp4">
</video>
</figure>
<p>You can set as many export presets as you need for the same layer. Set multiple exports to get the same layer in different scales and/or formats with just one click.</p>
<p>You can set as many export presets as you need for the same object. Set multiple exports to get the same object in different scales and/or formats with just one click.</p>
<h3>Remove export preset</h3>
<p>To <strong>remove an export preset</strong> you have to select the layer and then press the “-” button at the export preset you want to remove.</p>
<p>To <strong>remove an export preset</strong> you have to select the object and then press the “-” button at the export preset you want to remove.</p>
<h2 id="export-options">Export options</h2>
<p>The options of an export:</p>
@@ -58,7 +57,7 @@ desc: Learn how to export layers in Penpot, the free, open-source design collabo
<h2 id="export-artboards-pdf">Export boards to PDF</h2>
<p>If you have a presentation made at Penpot you might want to create a document that can be shared with anyone, regardless of having a Penpot account, or just to be able to use your presentation offline (essential for talks and classes). You can easily export all the artboards of a page to a single PDF file from the file menu.</p>
<p><a href="#export-technical">Technical note</a> about the PDF format.</p>
<p><a href="#pdf-note">Technical note</a> about the PDF format.</p>
<figure>
<img src="/img/export/export-pdf.webp" alt="Export PDF">
@@ -66,10 +65,10 @@ desc: Learn how to export layers in Penpot, the free, open-source design collabo
<h2 id="export-technical">Technical notes about exports</h2>
<p class="advice">
Exported PDF files try to leverage the capabilities of PDF vectorial format (unpixelated zoom, select & copy texts, etc.), but cannot guarantee that 100% of SVG features will be converted perfectly to PDF. You may see differences between a layer displayed inside Penpot and in the exported file. If you need an exact match, a workaround is to export the layer into PNG and convert it to PDF with some of the many tools that exist for it.<br /><br />
Exported PDF files try to leverage the capabilities of PDF vectorial format (unpixelated zoom, select & copy texts, etc.), but cannot guarantee that 100% of SVG features will be converted perfectly to PDF. You may see differences between an object displayed inside Penpot and in the exported file. If you need an exact match, a workaround is to export the object into PNG and convert it to PDF with some of the many tools that exist for it.<br /><br />
</p>
<p class="advice">
<a name="pdf-note"></a>
<strong>Currently known issue:</strong>
When exporting layers with masks, the mask does not work when opening the PDF file with some open source tools (e.g. evince or inkscape). This is not Penpot's fault, but <a href="https://gitlab.freedesktop.org/poppler/poppler/-/issues/1210" target="_blank">a bug in poppler</a>, a library used by many of the open source tools. If you open the file with an official Adobe viewer, or a tool like okular, or in a browser like Chrome or Firefox, you can see it properly.
When exporting objects with masks, the mask does not work when opening the PDF file with some open source tools (e.g. evince or inkscape). This is not Penpot's fault, but <a href="https://gitlab.freedesktop.org/poppler/poppler/-/issues/1210" target="_blank">a bug in poppler</a>, a library used by many of the open source tools. If you open the file with an official Adobe viewer, or a tool like okular, or in a browser like Chrome or Firefox, you can see it properly.
</p>

View File

@@ -1,34 +0,0 @@
---
title: First Steps
order: 1
desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorials. Learn the interface, layers, objects, styling, and more.
---
<h1 id="section-1">First Steps</h1>
<ul class="intro-sections">
<li>
<a href="/user-guide/first-steps/cloud-selfhost">
<h2>Cloud or Selfhost →</h2>
<p>Ways to start with Penpot</p>
</a>
</li>
<li>
<a href="/user-guide/first-steps/the-interface">
<h2>Interface tour →</h2>
<p>Info of interest about Penpot</p>
</a>
</li>
<li>
<a href="/user-guide/first-steps/shortcuts">
<h2>Shortcuts →</h2>
<p>Speed your design workflow</p>
</a>
</li>
<li>
<a href="/user-guide/first-steps/info">
<h2>Tutorials & info →</h2>
<p>Info of interest about Penpot</p>
</a>
</li>
</ul>

View File

@@ -1,6 +1,5 @@
---
title: Flexible Layouts
order: 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.
---
@@ -15,7 +14,7 @@ desc: Master responsive web design with Penpot's flexible and grid layouts! Lear
To help you learn the fundamentals of Flex Layout <a href="https://penpot.app/design/layout" target="_blank">heres a dedicated website</a> where you will find a <strong>video tutorial</strong> and a <strong>playground template</strong>.
</p>
<h3>Flex Layout is based on Flexbox CSS standard</h3>
<h3 id="layouts-flex-css">Flex Layout is based on Flexbox CSS standard</h3>
<p>Penpot's Flex Layout is built over Flexbox, a CSS module that provides a more efficient way to lay out, align and distribute space among items in a container. There are many comprehensive explanations about Flexbox, if you are interested we recommend you to read the one at <a href="https://css-tricks.com/snippets/css/a-guide-to-flexbox/" target="_blank">CSS Tricks</a>.</p>
<figure>
<p><img src="/img/csstricks-00-basic-terminology.svg" alt="Flex Layout" /></p>
@@ -33,8 +32,8 @@ desc: Master responsive web design with Penpot's flexible and grid layouts! Lear
<figure><img src="/img/flexible-layouts/layouts-add.webp" alt="Adding Layouts" /></figure>
<h3 id="layouts-flex-arrange-reorder">Arrange and reorder layers to a Flex Layout</h3>
<p>To add a layer to a Flex Layout you can just drag it at the position of your choice. You can also create or paste elements like in any regular board.</p>
<h3 id="layouts-flex-arrange-reorder">Arrange and reorder objects to a Flex Layout</h3>
<p>To add an object to a Flex Layout you can just drag it at the position of your choice. You can also create or paste elements like in any regular board.</p>
<p>To reorder elements you can drag them or use the <kbd>UP/DOWN</kbd> keystrokes.</p>
<figure>
<video title="A video showing a layer being dragged to and moved through a flex flow" muted="" playsinline="" controls="" width="100%" poster="/img/flexible-layouts/layouts-flex-arrange.webp" height="auto">
@@ -58,7 +57,7 @@ desc: Master responsive web design with Penpot's flexible and grid layouts! Lear
<li><strong>Sizing:</strong> Fix/fit width, Fix/fit height.</li>
</ul>
<h3 id="layouts-flex-elements">Positioning flex elements</h3>
<h3 id="layouts-flex-elements">Positioning Flex elements</h3>
<h4>Position static:</h4>
<p>Static position is the default option for flex elements, meaning that they will be included in the flex flow, using flex properties.</p>
<figure><img src="/img/flexible-layouts/flex-properties-element.webp" alt="Flex Layout properties" /></figure>
@@ -95,7 +94,7 @@ desc: Master responsive web design with Penpot's flexible and grid layouts! Lear
<h3 id="layouts-flex-code">Get code and specifications</h3>
<p>Designing with Flex Layout generates <em>ready for production</em> code. Select the flex board or its inner elements and then open the <a href="/user-guide/dev-tools/#inspect-design" target="_blank">Inspect tab</a> to obtain its properties, detailed info and raw code.</p>
<p>Designing with Flex Layout generates <em>ready for production</em> code. Select the flex board or its inner elements and then open the <a href="/user-guide/inspect" target="_blank">Inspect tab</a> to obtain its properties, detailed info and raw code.</p>
<figure><img src="/img/flexible-layouts/layouts-flex-code.gif" alt="Inspecting code at Penpot" /></figure>
<h3 id="layouts-flex-examples">Flex Layout basic examples</h3>
@@ -133,7 +132,7 @@ desc: Master responsive web design with Penpot's flexible and grid layouts! Lear
<figcaption>Rearranging cells in Grid Layout</figcaption>
</figure>
<h3>Grid Layout is based on CSS Grid standard</h3>
<h3 id="layouts-flex-css">Grid Layout is based on CSS Grid standard</h3>
<p>Penpot's Grid Layout is built over CSS Grid, a fairly new CSS module. If you are interested to know more about this CSS module we recommend checking out the comprehensive explanation <a href="https://css-tricks.com/snippets/css/complete-guide-grid/" target="_blank">Guide to CSS Grid</a> at CSS Tricks.</p>
@@ -275,7 +274,7 @@ desc: Master responsive web design with Penpot's flexible and grid layouts! Lear
<p>To turn areas back to regular cells, just select the "Auto" option at the grid cell properties (right sidebar).</p>
<h3 id="layouts-grid-code">Grid code and specifications</h3>
<p>Grid layout at Penpot behaves just like CSS Grid because it is actually using the CSS Grid standard. This means that you can just switch to <a href="/user-guide/dev-tools/#inspect-design" target="_blank">Inspect mode</a>, get the code and use it in real websites.</p>
<p>Grid layout at Penpot behaves just like CSS Grid because it is actually using the CSS Grid standard. This means that you can just switch to <a href="/user-guide/inspect" target="_blank">Inspect mode</a>, get the code and use it in real websites.</p>
<figure><img src="/img/flexible-layouts/layouts-grid-code.gif" alt="Inspecting code at Penpot" /></figure>

View File

@@ -1,24 +1,23 @@
---
title: Export/Import Penpot files
order: 1
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="export-import-files">Export and import Penpot files</h1>
<h1 id="import-export">Import and export files</h1>
<p class="main-paragraph">You can export Penpot files to your computer and import them from your computer to your projects.</p>
<h2 id="files-export">Export Penpot files</h2>
<p>Exporting files is useful for many reasons. Sometimes you want to have a backup of your files and sometimes it is useful to share Penpot files with a user that does not belong to one of your teams, or you want to have a backup of your files outside Penpot, both SaaS (design.penpot.app) or at a self-hosted instance.</p>
<h3>How to export Penpot files</h3>
<h3 id="export-penpot-files">How to export Penpot files</h3>
<h4>Export a single file</h4>
<p>You can download (export) files from the workspace and from the dashboard.</p>
<p>
<strong>From the <a href="/user-guide/first-steps/the-interface/#interface-dashboard">dashboard</a></strong>: Select the download option at the file card menu.
<strong>From the <a href="/user-guide/the-interface/#interface-dashboard">dashboard</a></strong>: Select the download option at the file card menu.
<figure><img src="/img/import-export/export-card.webp" alt="Export penpot file" /></figure>
</p>
<p>
<strong>From the <a href="/user-guide/first-steps/the-interface/#interface-workspace">workspace</a></strong>: Select the download option at the main menu.
<strong>From the <a href="/user-guide/the-interface/#interface-workspace">workspace</a></strong>: Select the download option at the main menu.
<figure><img src="/img/import-export/export-menu.webp" alt="Export penpot file" /></figure>
</p>
@@ -35,7 +34,7 @@ desc: Learn how to import and export files in Penpot, the free, open-source desi
<ul>
<li><strong>Export shared libraries</strong>: Files with shared libraries will be included in the export, maintaining their linkage.</li>
<li><strong>Include shared library assets in file libraries</strong>: Files will be exported with all external assets merged into the file library.</li>
<li><strong>Treat shared library assets as basic layers</strong>: Shared libraries will not be included in the export and no assets will be added to the library.</li>
<li><strong>Treat shared library assets as basic objects</strong>: Shared libraries will not be included in the export and no assets will be added to the library.</li>
</ul>
<figure><img src="/img/import-export/export-libraries.webp" alt="Export penpot file" /></figure>
@@ -68,6 +67,4 @@ desc: Learn how to import and export files in Penpot, the free, open-source desi
<li>✅ Allows some automations and integrations.</li>
<li>✅ Is a transparent, existing, open standard format.</li>
<li>❌ Highly inefficient in terms of memory and transfer time when exporting and importing (this is because SVG).</li>
</ul>
</ul>

View File

@@ -16,55 +16,17 @@ eleventyNavigation:
<p class="advice">This documentation is a work in progress that will be updated frequently. If you have a suggestion, see something is missing or find anything that needs correcting, please write to us at <a href="mailto:support@penpot.app" target="_blank">support@penpot.app</a>.</p>
<p class="main-paragraph">Explore our featured topics:</p>
<ul class="intro-sections">
<li>
<a href="/user-guide/designing/layers/">
<h2>Layers</h2>
<a href="/user-guide/introduction/quickstart">
<h2>Quickstart</h2>
<p>Ways to start with Penpot</p>
</a>
</li>
<li>
<a href="/user-guide/designing/flexible-layouts/">
<h2>Flexible layouts</h2>
<p>Create designs that adapt automatically.</p>
</a>
</li>
<li>
<a href="/user-guide/design-systems/components/">
<h2>Components</h2>
<p>Ways to start with Penpot</p>
</a>
</li>
<li>
<a href="/user-guide/design-systems/variants/">
<h2>Variants</h2>
<a href="/user-guide/the-interface/">
<h2>The interface</h2>
<p>Penpot's main areas and features</p>
</a>
</li>
<li>
<a href="/user-guide/design-systems/design-tokens/">
<h2>Design Tokens</h2>
<p>Penpot's main areas and features</p>
</a>
</li>
<li>
<a href="/user-guide/dev-tools/#inspect-design">
<h2>Inspect design</h2>
<p>Ways to start with Penpot</p>
</a>
</li>
<li>
<a href="/user-guide/prototyping-testing/prototyping/">
<h2>Prototyping</h2>
<p>Ways to start with Penpot</p>
</a>
</li>
<li>
<a href="/user-guide/design-systems/libraries/">
<h2>Libraries</h2>
<p>Ways to start with Penpot</p>
</a>
</li>
</ul>

View File

@@ -1,24 +1,23 @@
---
title: Dev tools
order: 5
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="dev-tools">Dev tools</h1>
<h1 id="inspect">Inspect designs</h1>
<p class="main-paragraph">At Penpot, you can inspect designs to get measures, view properties, export assets and get production-ready code. <p>
<h2 id="inspect-design">Inspect design</h2>
<p>You can activate the Inspect mode both at the <a href="/user-guide/first-steps/the-interface/#interface-viewmode">View mode</a> and at the <a href="/user-guide/first-steps/the-interface/#interface-workspace">Workspace</a>.</p>
<h2 id="inspect-activate">How to inspect designs</h2>
<p>You can activate the Inspect mode both at the <a href="/user-guide/the-interface/#interface-viewmode">View mode</a> and at the <a href="/user-guide/the-interface/#interface-workspace">Workspace</a>.</p>
<h3>At the View mode</h3>
<h3 id="inspect-viewmode">At the View mode</h3>
<figure>
<video title="A video showing how to activate Inspect at the View mode" muted="" playsinline="" controls="" width="100%" poster="/img/inspect/inspect-viewmode.webp" height="auto">
<source src="/img/inspect/inspect-viewmode.mp4" type="video/mp4">
</video>
</figure>
<p>Go to the <a href="/user-guide/prototyping-testing/testing-view-mode/#viewmode-inspect">Inspect designs at the View mode section</a> to know how to activate inspect mode at the View mode.</p>
<p>Go to the <a href="/user-guide/view-mode/#viewmode-inspect">Inspect designs at the View mode section</a> to know how to activate inspect mode at the View mode.</p>
<h3>At the Workspace</h3>
<h3 id="inspect-workspace">At the Workspace</h3>
<figure>
<video title="A video showing how to activate Inspect at the Workspace" muted="" playsinline="" controls="" width="100%" poster="/img/inspect/inspect-workspace.webp" height="auto">
<source src="/img/inspect/inspect-workspace.mp4" type="video/mp4">
@@ -27,13 +26,13 @@ desc: Learn how to inspect designs in Penpot! This guide covers distances, prope
<p>At the Workspace, select the Inspect tab at the right sidebar to enter inspect mode.</p>
<p>Inspect mode provides a safer <strong>view-only</strong> mode so developers can work at the Workspace without the fear of breaking things ;)</p>
<h2 id="inspect-measure">Get distances and measurements</h2>
<p>You can easily get measurements and distances between a layer and other layers or board edges.</p>
<h2 id="inspect-measure">How to get distances and measurements</h2>
<p>You can easily get measurements and distances between an object and other objects or board edges.</p>
<p>To get distances:</p>
<ul>
<li>Click on a layer or select it at the layers panel.</li>
<li>Hover over other layers to see the distances between them and the selected one.</li>
<li>Hover over a free space on the board or the area around it to see the distances from the layer to the board edges.</li>
<li>Click on an object or select it at the layers panel.</li>
<li>Hover over other objects to see the distances between them and the selected one.</li>
<li>Hover over a free space on the board or the area around it to see the distances from the object to the board edges.</li>
</ul>
<figure>
<video title="A video showing how to get Inspect measures" muted="" playsinline="" controls="" width="100%" poster="/img/inspect/inspect-measures.webp" height="auto">
@@ -41,15 +40,15 @@ desc: Learn how to inspect designs in Penpot! This guide covers distances, prope
</video>
</figure>
<h2 id="inspect-info">Get properties info</h2>
<p>At the Info panel you can see specifications about style and content of a layer. Different types of layers can have different sets of properties.</p>
<h2 id="inspect-info">How to get properties info</h2>
<p>At the Info panel you can see specifications about style and content of an object. Different types of objects can have different sets of properties.</p>
<figure>
<video title="A video showing how to get Inspect properties" muted="" playsinline="" controls="" width="100%" poster="/img/inspect/inspect-properties.webp" height="auto">
<source src="/img/inspect/inspect-properties.mp4" type="video/mp4">
</video>
</figure>
<h2 id="inspect-copy">Copy properties info</h2>
<h2 id="inspect-copy">How to copy properties info</h2>
<p>You can copy the value of one property or full sections of properties pressing the copy buttons that are shown at the right when hovering. For example you could copy all the layout properties or only the width.</p>
<figure>
<video title="A video showing how to copy properties" muted="" playsinline="" controls="" width="100%" poster="/img/inspect/inspect-copy.webp" height="auto">
@@ -57,27 +56,13 @@ desc: Learn how to inspect designs in Penpot! This guide covers distances, prope
</video>
</figure>
<h2 id="copy-css-properties">Copy CSS properties from layers</h2>
<p>To copy CSS properties from layers:</p>
<ol>
<li>Select one or more layers.</li>
<li>Right click to show the layer menu.</li>
<li>Press <strong>Copy/Paste as... > Copy as CSS</strong> in case you only want to get the CSS properties from the selected layer/s.</li>
<li>Press <strong>Copy/Paste as... > Copy as CSS (nested layers)</strong> in case you only want to get the CSS properties from the selected layer/s and all the contained layers.</li>
</ol>
<figure>
<img alt="Copy CSS properties" src="/img/layers/copy-css.webp"/>
</figure>
<h2 id="inspect-code">Get code</h2>
<p>Press the code tab to get actual code snippets. Select a layer to get ready to use code for markup (SVG and HTML) and styles (currently CSS only but more are coming).</p>
<h2 id="inspect-code">How to get code</h2>
<p>Press the code tab to get actual code snippets. Select an object to get ready to use code for markup (SVG and HTML) and styles (currently CSS only but more are coming).</p>
<figure>
<video title="A video showing how to get code" muted="" playsinline="" controls="" width="100%" poster="/img/inspect/inspect-code.webp" height="auto">
<source src="/img/inspect/inspect-code.mp4" type="video/mp4">
</video>
</figure>
<h2 id="inspect-export">Export assets</h2>
<p>Export option is available at the bottom of the Info panel. The same export presets that have been set at the workspace will be available at the View mode inspect. New export presets can be added at the Code mode but will not persist. Read more about <a href="/user-guide/export-import/exporting-layers/">exporting assets</a>.</p>
<h2 id="inspect-export">How to export assets</h2>
<p>Export option is available at the bottom of the Info panel. The same export presets that have been set at the workspace will be available at the View mode inspect. New export presets can be added at the Code mode but will not persist. Read more about <a href="/user-guide/exporting/">exporting assets</a>.</p>

View File

@@ -0,0 +1,27 @@
---
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>
<ul class="intro-sections">
<li>
<a href="/user-guide/introduction/quickstart">
<h2>Quickstart →</h2>
<p>Ways to start with Penpot</p>
</a>
</li>
<li>
<a href="/user-guide/introduction/shortcuts">
<h2>Shortcuts →</h2>
<p>Speed your design workflow</p>
</a>
</li>
<li>
<a href="/user-guide/introduction/info">
<h2>Info & tutorials →</h2>
<p>Info of interest about Penpot</p>
</a>
</li>
</ul>

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