Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e369b70aeb | ||
|
|
c3970255e6 | ||
|
|
7823eaf890 | ||
|
|
ec0079461e | ||
|
|
70a1a7a5ea | ||
|
|
c3dc165c4c | ||
|
|
5a3619c737 | ||
|
|
227f06c1ec | ||
|
|
946dac3c9f | ||
|
|
b160ba1793 | ||
|
|
33d51a51d1 | ||
|
|
ab4be85669 | ||
|
|
6c0dce580d | ||
|
|
59050a7bc6 | ||
|
|
3334fb0e99 | ||
|
|
24268bbf33 | ||
|
|
cd3f8f0c43 | ||
|
|
d3a8954605 | ||
|
|
1cda61e230 | ||
|
|
aca3e3db4f | ||
|
|
74f9166f3d | ||
|
|
977a2090fb | ||
|
|
14e6ea9393 | ||
|
|
3eb35f0aa6 | ||
|
|
92b7a35c58 | ||
|
|
99807b4cd4 | ||
|
|
bff415c7cd | ||
|
|
1d84835fd5 | ||
|
|
88296480ec | ||
|
|
4f5bc77379 | ||
|
|
3932054ea6 | ||
|
|
243fd17305 | ||
|
|
4b8febd7dc | ||
|
|
7c73e44ab8 | ||
|
|
e533762f33 | ||
|
|
40c118df55 | ||
|
|
f43fc282d3 | ||
|
|
8616e2f25c | ||
|
|
4299fd28f0 | ||
|
|
302ff92b31 | ||
|
|
b62cc9c8e9 | ||
|
|
225c2ca6e6 | ||
|
|
e5bdd852ca | ||
|
|
591788403a | ||
|
|
f1b82e289d | ||
|
|
f4ae8ea5ac | ||
|
|
d9310d651a | ||
|
|
6b817d102b | ||
|
|
08a9371322 | ||
|
|
f96da090d6 | ||
|
|
8d8f203b8a | ||
|
|
f40ffacfbd |
2
.gitignore
vendored
@@ -74,3 +74,5 @@ node_modules
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/render-wasm/target/
|
||||
/**/.yarn/*
|
||||
|
||||
20
CHANGES.md
@@ -1,5 +1,25 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.3.2
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix null pointer exception on number checking functions
|
||||
- Fix problem with grid layout ordering after moving [Taiga #9179](https://tree.taiga.io/project/penpot/issue/9179)
|
||||
|
||||
### :books: Documentation
|
||||
|
||||
- Add initial documentation for Kubernetes
|
||||
|
||||
|
||||
## 2.3.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix unexpected issue on interaction between plugins sandbox and
|
||||
internal impl of promise
|
||||
|
||||
|
||||
## 2.3.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
13
README.md
@@ -8,10 +8,12 @@
|
||||
<img alt="penpot header image" src="https://penpot.app/images/readme/github-light-mode.png">
|
||||
</picture>
|
||||
|
||||
<p align="center"><a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img src="https://camo.githubusercontent.com/3fcf3d6b678ea15fde3cf7d6af0e242160366282d62a7c182d83a50bfee3f45e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d504c2d322e302d626c75652e737667" alt="License: MPL-2.0" data-canonical-src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
|
||||
<a href="https://gitter.im/penpot/community" rel="nofollow"><img src="https://camo.githubusercontent.com/5b0aecb33434f82a7b158eab7247544235ada0cf7eeb9ce8e52562dd67f614b7/68747470733a2f2f6261646765732e6769747465722e696d2f736572656e6f2d78797a2f636f6d6d756e6974792e737667" alt="Gitter" data-canonical-src="https://badges.gitter.im/sereno-xyz/community.svg" style="max-width:100%;"></a>
|
||||
<a href="https://tree.taiga.io/project/penpot/" title="Managed with Taiga.io" rel="nofollow"><img src="https://camo.githubusercontent.com/4a1d1112f0272e3393b1e8da312ff4435418e9e2eb4c0964881e3680f90a653c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d616e61676564253230776974682d54414947412e696f2d3730396631342e737667" alt="Managed with Taiga.io" data-canonical-src="https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg" style="max-width:100%;"></a>
|
||||
<a href="https://gitpod.io/#https://github.com/penpot/penpot" rel="nofollow"><img src="https://camo.githubusercontent.com/daadb4894128d1e19b72d80236f5959f1f2b47f9fe081373f3246131f0189f6c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476974706f642d72656164792d2d746f2d2d636f64652d626c75653f6c6f676f3d676974706f64" alt="Gitpod ready-to-code" data-canonical-src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod" style="max-width:100%;"></a></p>
|
||||
<p align="center">
|
||||
<a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img alt="License: MPL-2.0" src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
|
||||
<a href="https://gitter.im/penpot/community" rel="nofollow"><img alt="Gitter" src="https://badges.gitter.im/sereno-xyz/community.svg" style="max-width:100%;"></a>
|
||||
<a href="https://tree.taiga.io/project/penpot/" title="Managed with Taiga.io" rel="nofollow"><img alt="Managed with Taiga.io" src="https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg" style="max-width:100%;"></a>
|
||||
<a href="https://gitpod.io/#https://github.com/penpot/penpot" rel="nofollow"><img alt="Gitpod ready-to-code" src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod" style="max-width:100%;"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://penpot.app/"><b>Website</b></a> •
|
||||
@@ -58,6 +60,9 @@ Penpot’s latest [huge release 2.0](https://penpot.app/dev-diaries), takes the
|
||||
|
||||
Penpot expresses designs as code. Designers can do their best work and see it will be beautifully implemented by developers in a two-way collaboration.
|
||||
|
||||
### Plugin system ###
|
||||
[Penpot plugins](https://penpot.app/penpothub/plugins) let you expand the platform's capabilities, give you the flexibility to integrate it with other apps, and design custom solutions.
|
||||
|
||||
### Designed for developers ###
|
||||
Penpot was built to serve both designers and developers and create a fluid design-code process. You have the choice to enjoy real-time collaboration or play "solo".
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
[{:id "wireframing-kit"
|
||||
:name "Wireframe library"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/wireframing-kit.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Wireframing%20kit%20v1.1.penpot"}
|
||||
{:id "prototype-examples"
|
||||
:name "Prototype template"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/prototype-examples.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Prototype%20examples%20v1.1.penpot"}
|
||||
{:id "plants-app"
|
||||
:name "UI mockup example"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Plants-app.penpot"}
|
||||
{:id "penpot-design-system"
|
||||
:name "Design system example"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Penpot-Design-system.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Penpot%20-%20Design%20System%20v2.1.penpot"}
|
||||
{:id "tutorial-for-beginners"
|
||||
:name "Tutorial for beginners"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/tutorial-for-beginners.penpot"}
|
||||
@@ -36,7 +36,7 @@
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Open%20Color%20Scheme%20(v1.9.1).penpot"}
|
||||
{:id "flex-layout-playground"
|
||||
:name "Flex Layout Playground"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Flex%20Layout%20Playground.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Flex%20Layout%20Playground%20v2.0.penpot"}
|
||||
{:id "welcome"
|
||||
:name "Welcome"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/welcome.penpot"}]
|
||||
|
||||
@@ -7,7 +7,7 @@ Debug Main Page
|
||||
{% block content %}
|
||||
<nav>
|
||||
<div class="title">
|
||||
<h1>ADMIN DEBUG INTERFACE</h1>
|
||||
<h1>ADMIN DEBUG INTERFACE (VERSION: {{version}})</h1>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="dashboard">
|
||||
|
||||
@@ -68,6 +68,7 @@ export AWS_SECRET_ACCESS_KEY=penpot-devenv
|
||||
export PENPOT_OBJECTS_STORAGE_BACKEND=s3
|
||||
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
|
||||
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
|
||||
export PENPOT_OBJECTS_STORAGE_FS_DIRECTORY="assets"
|
||||
|
||||
export OPTIONS="
|
||||
-A:jmx-remote -A:dev \
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/html"}
|
||||
::rres/body (-> (io/resource "app/templates/debug.tmpl")
|
||||
(tmpl/render {}))})
|
||||
(tmpl/render {:version (:full cf/version)}))})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; FILE CHANGES
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
(ex/format-throwable cause :data? false :explain? false :header? false :summary? false))}
|
||||
|
||||
(when-let [params (or (:request/params context) (:params context))]
|
||||
{:params (pp/pprint-str params :length 30 :level 12)})
|
||||
{:params (pp/pprint-str params :length 30 :level 13)})
|
||||
|
||||
(when-let [value (:value context)]
|
||||
{:value (pp/pprint-str value :length 30 :level 12)})
|
||||
|
||||
@@ -475,7 +475,8 @@
|
||||
::sto.s3/bucket (or (cf/get :storage-assets-s3-bucket)
|
||||
(cf/get :objects-storage-s3-bucket))
|
||||
::sto.s3/io-threads (or (cf/get :storage-assets-s3-io-threads)
|
||||
(cf/get :objects-storage-s3-io-threads))}
|
||||
(cf/get :objects-storage-s3-io-threads))
|
||||
::wrk/executor (ig/ref ::wrk/executor)}
|
||||
|
||||
:app.storage.fs/backend
|
||||
{::sto.fs/directory (or (cf/get :storage-assets-fs-directory)
|
||||
|
||||
@@ -356,7 +356,7 @@
|
||||
f.name,
|
||||
f.revn,
|
||||
f.is_shared,
|
||||
ft.media_id
|
||||
ft.media_id AS thumbnail_id
|
||||
from file as f
|
||||
left join file_thumbnail as ft on (ft.file_id = f.id
|
||||
and ft.revn = f.revn
|
||||
@@ -367,13 +367,7 @@
|
||||
|
||||
(defn get-project-files
|
||||
[conn project-id]
|
||||
(->> (db/exec! conn [sql:project-files project-id])
|
||||
(mapv (fn [row]
|
||||
(if-let [media-id (:media-id row)]
|
||||
(-> row
|
||||
(dissoc :media-id)
|
||||
(assoc :thumbnail-uri (resolve-public-uri media-id)))
|
||||
(dissoc row :media-id))))))
|
||||
(db/exec! conn [sql:project-files project-id]))
|
||||
|
||||
(def schema:get-project-files
|
||||
[:map {:title "get-project-files"}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -19,6 +20,7 @@
|
||||
[app.storage.s3 :as ss3]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.fs :as fs]
|
||||
[integrant.core :as ig])
|
||||
(:import
|
||||
@@ -30,7 +32,7 @@
|
||||
(case name
|
||||
:assets-fs :fs
|
||||
:assets-s3 :s3
|
||||
:fs)))
|
||||
nil)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Storage Module State
|
||||
@@ -52,11 +54,19 @@
|
||||
|
||||
(defmethod ig/init-key ::storage
|
||||
[_ {:keys [::backends ::db/pool] :as cfg}]
|
||||
(-> (d/without-nils cfg)
|
||||
(assoc ::backends (d/without-nils backends))
|
||||
(assoc ::backend (or (get-legacy-backend)
|
||||
(cf/get :objects-storage-backend :fs)))
|
||||
(assoc ::db/connectable pool)))
|
||||
(let [backend (or (get-legacy-backend)
|
||||
(cf/get :objects-storage-backend)
|
||||
:fs)
|
||||
backends (d/without-nils backends)]
|
||||
|
||||
(l/dbg :hint "initialize"
|
||||
:default (d/name backend)
|
||||
:available (str/join "," (map d/name (keys backends))))
|
||||
|
||||
(-> (d/without-nils cfg)
|
||||
(assoc ::backends backends)
|
||||
(assoc ::backend backend)
|
||||
(assoc ::db/connectable pool))))
|
||||
|
||||
(s/def ::backend keyword?)
|
||||
(s/def ::storage
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
[app.storage.impl :as impl]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[datoteka.fs :as fs]
|
||||
@@ -27,17 +28,15 @@
|
||||
java.io.FilterInputStream
|
||||
java.io.InputStream
|
||||
java.net.URI
|
||||
java.nio.ByteBuffer
|
||||
java.nio.file.Path
|
||||
java.time.Duration
|
||||
java.util.Collection
|
||||
java.util.Optional
|
||||
java.util.concurrent.Semaphore
|
||||
org.reactivestreams.Subscriber
|
||||
org.reactivestreams.Subscription
|
||||
software.amazon.awssdk.core.ResponseBytes
|
||||
software.amazon.awssdk.core.async.AsyncRequestBody
|
||||
software.amazon.awssdk.core.async.AsyncResponseTransformer
|
||||
software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody
|
||||
software.amazon.awssdk.core.client.config.ClientAsyncConfiguration
|
||||
software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption
|
||||
software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient
|
||||
@@ -59,6 +58,20 @@
|
||||
software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest
|
||||
software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest))
|
||||
|
||||
(def ^:private max-retries
|
||||
"A maximum number of retries on internal operations"
|
||||
3)
|
||||
|
||||
(def ^:private max-concurrency
|
||||
"Maximum concurrent request to S3 service"
|
||||
128)
|
||||
|
||||
(def ^:private max-pending-connection-acquires
|
||||
20000)
|
||||
|
||||
(def default-timeout
|
||||
(dt/duration {:seconds 30}))
|
||||
|
||||
(declare put-object)
|
||||
(declare get-object-bytes)
|
||||
(declare get-object-data)
|
||||
@@ -80,7 +93,7 @@
|
||||
(s/def ::io-threads ::us/integer)
|
||||
|
||||
(defmethod ig/pre-init-spec ::backend [_]
|
||||
(s/keys :opt [::region ::bucket ::prefix ::endpoint ::io-threads]))
|
||||
(s/keys :opt [::region ::bucket ::prefix ::endpoint ::io-threads ::wrk/executor]))
|
||||
|
||||
(defmethod ig/prep-key ::backend
|
||||
[_ {:keys [::prefix ::region] :as cfg}]
|
||||
@@ -128,18 +141,29 @@
|
||||
[backend object]
|
||||
(us/assert! ::backend backend)
|
||||
|
||||
(let [result (p/await (get-object-data backend object))]
|
||||
(if (ex/exception? result)
|
||||
(cond
|
||||
(ex/instance? NoSuchKeyException result)
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "s3 object not found"
|
||||
:cause result)
|
||||
:else
|
||||
(throw result))
|
||||
(loop [result (get-object-data backend object)
|
||||
retryn 0]
|
||||
|
||||
result)))
|
||||
(let [result (p/await result)]
|
||||
(if (ex/exception? result)
|
||||
(cond
|
||||
(ex/instance? NoSuchKeyException result)
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "s3 object not found"
|
||||
:object-id (:id object)
|
||||
:object-path (impl/id->path (:id object))
|
||||
:cause result)
|
||||
|
||||
(and (ex/instance? java.nio.file.FileAlreadyExistsException result)
|
||||
(< retryn max-retries))
|
||||
(recur (get-object-data backend object)
|
||||
(inc retryn))
|
||||
|
||||
:else
|
||||
(throw result))
|
||||
|
||||
result))))
|
||||
|
||||
(defmethod impl/get-object-bytes :s3
|
||||
[backend object]
|
||||
@@ -163,18 +187,14 @@
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
(def default-timeout
|
||||
(dt/duration {:seconds 30}))
|
||||
|
||||
(defn- lookup-region
|
||||
^Region
|
||||
[region]
|
||||
(Region/of (name region)))
|
||||
|
||||
(defn- build-s3-client
|
||||
[{:keys [::region ::endpoint ::io-threads]}]
|
||||
(let [executor (px/resolve-executor :virtual)
|
||||
aconfig (-> (ClientAsyncConfiguration/builder)
|
||||
[{:keys [::region ::endpoint ::io-threads ::wrk/executor]}]
|
||||
(let [aconfig (-> (ClientAsyncConfiguration/builder)
|
||||
(.advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR executor)
|
||||
(.build))
|
||||
|
||||
@@ -190,6 +210,8 @@
|
||||
(.connectionTimeout default-timeout)
|
||||
(.readTimeout default-timeout)
|
||||
(.writeTimeout default-timeout)
|
||||
(.maxConcurrency (int max-concurrency))
|
||||
(.maxPendingConnectionAcquires (int max-pending-connection-acquires))
|
||||
(.build))
|
||||
|
||||
client (let [builder (S3AsyncClient/builder)
|
||||
@@ -223,69 +245,38 @@
|
||||
(.serviceConfiguration ^S3Configuration config)
|
||||
(.build))))
|
||||
|
||||
(defn- upload-thread
|
||||
[id subscriber sem content]
|
||||
(px/thread
|
||||
{:name "penpot/s3/uploader"
|
||||
:virtual true
|
||||
:daemon true}
|
||||
(l/trace :hint "start upload thread"
|
||||
:object-id (str id)
|
||||
:size (impl/get-size content)
|
||||
::l/sync? true)
|
||||
(let [stream (io/input-stream content)
|
||||
bsize (* 1024 64)
|
||||
tpoint (dt/tpoint)]
|
||||
(try
|
||||
(loop []
|
||||
(.acquire ^Semaphore sem 1)
|
||||
(let [buffer (byte-array bsize)
|
||||
readed (.read ^InputStream stream buffer)]
|
||||
(when (pos? readed)
|
||||
(let [data (ByteBuffer/wrap ^bytes buffer 0 readed)]
|
||||
(.onNext ^Subscriber subscriber ^ByteBuffer data)
|
||||
(when (= readed bsize)
|
||||
(recur))))))
|
||||
(.onComplete ^Subscriber subscriber)
|
||||
(catch InterruptedException _
|
||||
(l/trace :hint "interrupted upload thread"
|
||||
:object-:id (str id)
|
||||
::l/sync? true)
|
||||
nil)
|
||||
(catch Throwable cause
|
||||
(.onError ^Subscriber subscriber cause))
|
||||
(finally
|
||||
(l/trace :hint "end upload thread"
|
||||
:object-id (str id)
|
||||
:elapsed (dt/format-duration (tpoint))
|
||||
::l/sync? true)
|
||||
(.close ^InputStream stream))))))
|
||||
(defn- write-input-stream
|
||||
[delegate input]
|
||||
(try
|
||||
(.writeInputStream ^BlockingInputStreamAsyncRequestBody delegate
|
||||
^InputStream input)
|
||||
(catch Throwable cause
|
||||
(l/error :hint "encountered error while writing input stream to service"
|
||||
:cause cause))
|
||||
(finally
|
||||
(.close ^InputStream input))))
|
||||
|
||||
(defn- make-request-body
|
||||
[id content]
|
||||
(reify
|
||||
AsyncRequestBody
|
||||
(contentLength [_]
|
||||
(Optional/of (long (impl/get-size content))))
|
||||
|
||||
(^void subscribe [_ ^Subscriber subscriber]
|
||||
(let [sem (Semaphore. 0)
|
||||
thr (upload-thread id subscriber sem content)]
|
||||
(.onSubscribe subscriber
|
||||
(reify Subscription
|
||||
(cancel [_]
|
||||
(px/interrupt! thr)
|
||||
(.release sem 1))
|
||||
(request [_ n]
|
||||
(.release sem (int n)))))))))
|
||||
[executor content]
|
||||
(let [size (impl/get-size content)]
|
||||
(reify
|
||||
AsyncRequestBody
|
||||
(contentLength [_]
|
||||
(Optional/of (long size)))
|
||||
|
||||
(^void subscribe [_ ^Subscriber subscriber]
|
||||
(let [delegate (AsyncRequestBody/forBlockingInputStream (long size))
|
||||
input (io/input-stream content)]
|
||||
(px/run! executor (partial write-input-stream delegate input))
|
||||
(.subscribe ^BlockingInputStreamAsyncRequestBody delegate
|
||||
^Subscriber subscriber))))))
|
||||
|
||||
(defn- put-object
|
||||
[{:keys [::client ::bucket ::prefix]} {:keys [id] :as object} content]
|
||||
[{:keys [::client ::bucket ::prefix ::wrk/executor]} {:keys [id] :as object} content]
|
||||
(let [path (dm/str prefix (impl/id->path id))
|
||||
mdata (meta object)
|
||||
mtype (:content-type mdata "application/octet-stream")
|
||||
rbody (make-request-body id content)
|
||||
rbody (make-request-body executor content)
|
||||
request (.. (PutObjectRequest/builder)
|
||||
(bucket bucket)
|
||||
(contentType mtype)
|
||||
|
||||
@@ -11,13 +11,16 @@
|
||||
permanently delete these files (look at systemd-tempfiles)."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[datoteka.fs :as fs]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px]
|
||||
[promesa.exec.csp :as sp]))
|
||||
[promesa.exec.csp :as sp])
|
||||
(:import
|
||||
java.nio.file.Files))
|
||||
|
||||
(def default-tmp-dir "/tmp/penpot")
|
||||
|
||||
@@ -76,11 +79,9 @@
|
||||
[& {:keys [suffix prefix min-age]
|
||||
:or {prefix "penpot."
|
||||
suffix ".tmp"}}]
|
||||
(let [path (fs/create-tempfile
|
||||
:perms "rw-r--r--"
|
||||
:dir default-tmp-dir
|
||||
:suffix suffix
|
||||
:prefix prefix)]
|
||||
(let [attrs (fs/make-permissions "rw-r--r--")
|
||||
path (fs/join default-tmp-dir (str prefix (uuid/next) suffix))
|
||||
path (Files/createFile path attrs)]
|
||||
(fs/delete-on-exit! path)
|
||||
(sp/offer! queue [path (some-> min-age dt/duration)])
|
||||
path))
|
||||
|
||||
@@ -17,12 +17,11 @@
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px])
|
||||
(:import
|
||||
java.util.concurrent.Executor
|
||||
java.util.concurrent.ThreadPoolExecutor))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(s/def ::wrk/executor #(instance? Executor %))
|
||||
(s/def ::wrk/executor #(instance? ThreadPoolExecutor %))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; EXECUTOR
|
||||
@@ -36,30 +35,22 @@
|
||||
(let [factory (px/thread-factory :prefix "penpot/default/")
|
||||
executor (px/cached-executor :factory factory :keepalive 60000)]
|
||||
(l/inf :hint "executor started")
|
||||
(reify
|
||||
java.lang.AutoCloseable
|
||||
(close [_]
|
||||
(l/inf :hint "stoping executor")
|
||||
(px/shutdown! executor))
|
||||
|
||||
clojure.lang.IDeref
|
||||
(deref [_]
|
||||
{:active (.getPoolSize ^ThreadPoolExecutor executor)
|
||||
:running (.getActiveCount ^ThreadPoolExecutor executor)
|
||||
:completed (.getCompletedTaskCount ^ThreadPoolExecutor executor)})
|
||||
|
||||
Executor
|
||||
(execute [_ runnable]
|
||||
(.execute ^Executor executor ^Runnable runnable)))))
|
||||
executor))
|
||||
|
||||
(defmethod ig/halt-key! ::wrk/executor
|
||||
[_ instance]
|
||||
(.close ^java.lang.AutoCloseable instance))
|
||||
(px/shutdown! instance))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; MONITOR
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- get-stats
|
||||
[^ThreadPoolExecutor executor]
|
||||
{:active (.getPoolSize ^ThreadPoolExecutor executor)
|
||||
:running (.getActiveCount ^ThreadPoolExecutor executor)
|
||||
:completed (.getCompletedTaskCount ^ThreadPoolExecutor executor)})
|
||||
|
||||
(s/def ::name ::us/keyword)
|
||||
|
||||
(defmethod ig/pre-init-spec ::wrk/monitor [_]
|
||||
@@ -74,7 +65,7 @@
|
||||
[_ {:keys [::wrk/executor ::mtx/metrics ::interval ::wrk/name]}]
|
||||
(letfn [(monitor! [executor prev-completed]
|
||||
(let [labels (into-array String [(d/name name)])
|
||||
stats (deref executor)
|
||||
stats (get-stats executor)
|
||||
|
||||
completed (:completed stats)
|
||||
completed-inc (- completed prev-completed)
|
||||
|
||||
@@ -418,7 +418,8 @@
|
||||
(cts/shape? shape-new))
|
||||
(ex/raise :type :assertion
|
||||
:code :data-validation
|
||||
:hint "invalid shape found after applying changes")))))]
|
||||
:hint "invalid shape found after applying changes"
|
||||
::sm/explain (cts/explain-shape shape-new))))))]
|
||||
|
||||
(->> (into #{} (map :page-id) items)
|
||||
(mapcat (fn [page-id]
|
||||
@@ -466,7 +467,7 @@
|
||||
#?(:clj (validate-shapes! data result items))
|
||||
result))))
|
||||
|
||||
;; DEPRECATED: remove before 2.3 release
|
||||
;; DEPRECATED: remove after 2.3 release
|
||||
(defmethod process-change :set-option
|
||||
[data _]
|
||||
data)
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
|
||||
(ns app.common.files.defaults)
|
||||
|
||||
(def version 55)
|
||||
(def version 57)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.files.defaults :as cfd]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.shapes.path :as gsp]
|
||||
@@ -1075,6 +1076,60 @@
|
||||
|
||||
(update data :pages-index d/update-vals update-page)))
|
||||
|
||||
(defn migrate-up-56
|
||||
[data]
|
||||
(letfn [(fix-fills [object]
|
||||
(d/update-when object :fills (partial filterv valid-fill?)))
|
||||
|
||||
(update-object [object]
|
||||
(-> object
|
||||
(fix-fills)
|
||||
|
||||
;; If shape contains shape-ref but has a nil value, we
|
||||
;; should remove it from shape object
|
||||
(cond-> (and (contains? object :shape-ref)
|
||||
(nil? (get object :shape-ref)))
|
||||
(dissoc :shape-ref))
|
||||
|
||||
;; The text shape also can has fills on the text
|
||||
;; fragments so we need to fix fills there
|
||||
(cond-> (cfh/text-shape? object)
|
||||
(update :content (partial txt/transform-nodes identity fix-fills)))))
|
||||
|
||||
(update-container [container]
|
||||
(d/update-when container :objects update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
|
||||
|
||||
(defn migrate-up-57
|
||||
[data]
|
||||
(letfn [(fix-thread-positions [positions]
|
||||
(reduce-kv (fn [result id {:keys [position] :as data}]
|
||||
(let [data (cond
|
||||
(gpt/point? position)
|
||||
data
|
||||
|
||||
(and (map? position)
|
||||
(gpt/valid-point-attrs? position))
|
||||
(assoc data :position (gpt/point position))
|
||||
|
||||
:else
|
||||
(assoc data :position (gpt/point 0 0)))]
|
||||
(assoc result id data)))
|
||||
positions
|
||||
positions))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :comment-thread-positions fix-thread-positions))]
|
||||
|
||||
(-> data
|
||||
(update :pages (fn [pages] (into [] (remove nil?) pages)))
|
||||
(update :pages-index dissoc nil)
|
||||
(update :pages-index update-vals update-page))))
|
||||
|
||||
(def migrations
|
||||
"A vector of all applicable migrations"
|
||||
[{:id 2 :migrate-up migrate-up-2}
|
||||
@@ -1121,4 +1176,7 @@
|
||||
{:id 52 :migrate-up migrate-up-52}
|
||||
{:id 53 :migrate-up migrate-up-26}
|
||||
{:id 54 :migrate-up migrate-up-54}
|
||||
{:id 55 :migrate-up migrate-up-55}])
|
||||
{:id 55 :migrate-up migrate-up-55}
|
||||
{:id 56 :migrate-up migrate-up-56}
|
||||
{:id 57 :migrate-up migrate-up-57}])
|
||||
|
||||
|
||||
@@ -56,6 +56,9 @@
|
||||
[:x ::sm/safe-number]
|
||||
[:y ::sm/safe-number]])
|
||||
|
||||
(def valid-point-attrs?
|
||||
(sm/validator schema:point-attrs))
|
||||
|
||||
(def valid-point?
|
||||
(sm/validator
|
||||
[:and [:fn point?] schema:point-attrs]))
|
||||
|
||||
@@ -391,13 +391,14 @@
|
||||
(-> (pcb/update-shapes
|
||||
[parent-id]
|
||||
(fn [frame objects]
|
||||
(-> frame
|
||||
;; Assign the cell when pushing into a specific grid cell
|
||||
(cond-> (some? cell)
|
||||
(-> (ctl/free-cell-shapes ids)
|
||||
(ctl/push-into-cell ids (:row cell) (:column cell))
|
||||
(ctl/assign-cells objects)))
|
||||
(ctl/assign-cell-positions objects)))
|
||||
(let [[row column] cell]
|
||||
(-> frame
|
||||
;; Assign the cell when pushing into a specific grid cell
|
||||
(cond-> (some? cell)
|
||||
(-> (ctl/free-cell-shapes ids)
|
||||
(ctl/push-into-cell ids row column)
|
||||
(ctl/assign-cells objects)))
|
||||
(ctl/assign-cell-positions objects))))
|
||||
{:with-objects? true})
|
||||
(pcb/reorder-grid-children [parent-id])))
|
||||
|
||||
|
||||
@@ -681,13 +681,13 @@
|
||||
(let [pred int?
|
||||
pred (if (some? min)
|
||||
(fn [v]
|
||||
(and (>= v min)
|
||||
(pred v)))
|
||||
(and (pred v)
|
||||
(>= v min)))
|
||||
pred)
|
||||
pred (if (some? max)
|
||||
(fn [v]
|
||||
(and (>= max v)
|
||||
(pred v)))
|
||||
(and (pred v)
|
||||
(>= max v)))
|
||||
pred)]
|
||||
|
||||
{:pred pred
|
||||
@@ -719,13 +719,13 @@
|
||||
(let [pred double?
|
||||
pred (if (some? min)
|
||||
(fn [v]
|
||||
(and (>= v min)
|
||||
(pred v)))
|
||||
(and (pred v)
|
||||
(>= v min)))
|
||||
pred)
|
||||
pred (if (some? max)
|
||||
(fn [v]
|
||||
(and (>= max v)
|
||||
(pred v)))
|
||||
(and (pred v)
|
||||
(>= max v)))
|
||||
pred)]
|
||||
|
||||
{:pred pred
|
||||
@@ -749,13 +749,13 @@
|
||||
(let [pred number?
|
||||
pred (if (some? min)
|
||||
(fn [v]
|
||||
(and (>= v min)
|
||||
(pred v)))
|
||||
(and (pred v)
|
||||
(>= v min)))
|
||||
pred)
|
||||
pred (if (some? max)
|
||||
(fn [v]
|
||||
(and (>= max v)
|
||||
(pred v)))
|
||||
(and (pred v)
|
||||
(>= max v)))
|
||||
pred)
|
||||
|
||||
gen (sg/one-of
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
(def schema:image-color
|
||||
[:map {:title "ImageColor"}
|
||||
[:name {:optional true} :string]
|
||||
[:width :int]
|
||||
[:height :int]
|
||||
[:width ::sm/int]
|
||||
[:height ::sm/int]
|
||||
[:mtype {:optional true} [:maybe :string]]
|
||||
[:id ::sm/uuid]
|
||||
[:keep-aspect-ratio {:optional true} :boolean]])
|
||||
|
||||
@@ -224,8 +224,8 @@
|
||||
[:map {:title "ImageAttrs"}
|
||||
[:metadata
|
||||
[:map
|
||||
[:width {:gen/gen (sg/small-int :min 1)} :int]
|
||||
[:height {:gen/gen (sg/small-int :min 1)} :int]
|
||||
[:width {:gen/gen (sg/small-int :min 1)} ::sm/int]
|
||||
[:height {:gen/gen (sg/small-int :min 1)} ::sm/int]
|
||||
[:mtype {:optional true
|
||||
:gen/gen (sg/elements ["image/jpeg"
|
||||
"image/png"])}
|
||||
@@ -361,6 +361,9 @@
|
||||
(def valid-shape?
|
||||
(sm/lazy-validator schema:shape))
|
||||
|
||||
(def explain-shape
|
||||
(sm/lazy-explainer schema:shape))
|
||||
|
||||
(defn has-images?
|
||||
[{:keys [fills strokes]}]
|
||||
(or (some :fill-image fills)
|
||||
|
||||
9
docs/.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
charset = utf-8
|
||||
144
docs/.eleventy.js
Normal file
@@ -0,0 +1,144 @@
|
||||
const { DateTime } = require("luxon");
|
||||
const fs = require("fs");
|
||||
const pluginNavigation = require("@11ty/eleventy-navigation");
|
||||
const pluginRss = require("@11ty/eleventy-plugin-rss");
|
||||
const pluginSyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
|
||||
const pluginAncestry = require("@tigersway/eleventy-plugin-ancestry");
|
||||
const metagen = require('eleventy-plugin-metagen');
|
||||
const pluginTOC = require('eleventy-plugin-nesting-toc');
|
||||
const markdownIt = require("markdown-it");
|
||||
const markdownItAnchor = require("markdown-it-anchor");
|
||||
const markdownItPlantUML = require("markdown-it-plantuml");
|
||||
const elasticlunr = require("elasticlunr");
|
||||
|
||||
|
||||
module.exports = function(eleventyConfig) {
|
||||
eleventyConfig.addPlugin(pluginNavigation);
|
||||
eleventyConfig.addPlugin(pluginRss);
|
||||
eleventyConfig.addPlugin(pluginSyntaxHighlight);
|
||||
eleventyConfig.addPlugin(pluginAncestry);
|
||||
eleventyConfig.addPlugin(metagen);
|
||||
eleventyConfig.addPlugin(pluginTOC, {
|
||||
tags: ['h1', 'h2', 'h3']
|
||||
});
|
||||
|
||||
eleventyConfig.setDataDeepMerge(true);
|
||||
|
||||
eleventyConfig.addLayoutAlias("post", "layouts/post.njk");
|
||||
|
||||
eleventyConfig.addFilter("readableDate", dateObj => {
|
||||
return DateTime.fromJSDate(dateObj, {zone: 'utc'}).toFormat("dd LLL yyyy");
|
||||
});
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-date-string
|
||||
eleventyConfig.addFilter('htmlDateString', (dateObj) => {
|
||||
return DateTime.fromJSDate(dateObj, {zone: 'utc'}).toFormat('yyyy-LL-dd');
|
||||
});
|
||||
|
||||
// Remove trailing # in automatic generated toc, because of
|
||||
// anchors added at the end of the titles.
|
||||
eleventyConfig.addFilter('stripHash', (toc) => {
|
||||
return toc.replace(/ #\<\/a\>/g, "</a>");
|
||||
});
|
||||
|
||||
// Get the first `n` elements of a collection.
|
||||
eleventyConfig.addFilter("head", (array, n) => {
|
||||
if( n < 0 ) {
|
||||
return array.slice(n);
|
||||
}
|
||||
|
||||
return array.slice(0, n);
|
||||
});
|
||||
|
||||
// Get the lowest in a list of numbers.
|
||||
eleventyConfig.addFilter("min", (...numbers) => {
|
||||
return Math.min.apply(null, numbers);
|
||||
});
|
||||
|
||||
// Build a search index
|
||||
eleventyConfig.addFilter("search", (collection) => {
|
||||
// What fields we'd like our index to consist of
|
||||
// TODO: remove html tags from content
|
||||
var index = elasticlunr(function () {
|
||||
this.addField("title");
|
||||
this.addField("content");
|
||||
this.setRef("id");
|
||||
});
|
||||
|
||||
// loop through each page and add it to the index
|
||||
collection.forEach((page) => {
|
||||
index.addDoc({
|
||||
id: page.url,
|
||||
title: page.template.frontMatter.data.title,
|
||||
content: page.template.frontMatter.content,
|
||||
});
|
||||
});
|
||||
|
||||
return index.toJSON();
|
||||
});
|
||||
|
||||
eleventyConfig.addPassthroughCopy("img");
|
||||
eleventyConfig.addPassthroughCopy("css");
|
||||
eleventyConfig.addPassthroughCopy("js");
|
||||
|
||||
/* Markdown Overrides */
|
||||
let markdownLibrary = markdownIt({
|
||||
html: true,
|
||||
breaks: false,
|
||||
linkify: true
|
||||
}).use(markdownItAnchor, {
|
||||
permalink: true,
|
||||
permalinkClass: "direct-link",
|
||||
permalinkSymbol: "#"
|
||||
}).use(markdownItPlantUML, {
|
||||
});
|
||||
eleventyConfig.setLibrary("md", markdownLibrary);
|
||||
|
||||
// Browsersync Overrides
|
||||
eleventyConfig.setBrowserSyncConfig({
|
||||
callbacks: {
|
||||
ready: function(err, browserSync) {
|
||||
const content_404 = fs.readFileSync('_dist/404.html');
|
||||
|
||||
browserSync.addMiddleware("*", (req, res) => {
|
||||
// Provides the 404 content without redirect.
|
||||
res.write(content_404);
|
||||
res.end();
|
||||
});
|
||||
},
|
||||
},
|
||||
ui: false,
|
||||
ghostMode: false
|
||||
});
|
||||
|
||||
return {
|
||||
templateFormats: [
|
||||
"md",
|
||||
"njk",
|
||||
"html",
|
||||
"liquid"
|
||||
],
|
||||
|
||||
// If your site lives in a different subdirectory, change this.
|
||||
// Leading or trailing slashes are all normalized away, so don’t worry about those.
|
||||
|
||||
// If you don’t have a subdirectory, use "" or "/" (they do the same thing)
|
||||
// This is only used for link URLs (it does not affect your file structure)
|
||||
// Best paired with the `url` filter: https://www.11ty.dev/docs/filters/url/
|
||||
|
||||
// You can also pass this in on the command line using `--pathprefix`
|
||||
// pathPrefix: "/",
|
||||
|
||||
markdownTemplateEngine: "liquid",
|
||||
htmlTemplateEngine: "njk",
|
||||
dataTemplateEngine: "njk",
|
||||
|
||||
// These are all optional, defaults are shown:
|
||||
dir: {
|
||||
input: ".",
|
||||
includes: "_includes",
|
||||
data: "_data",
|
||||
output: "_dist"
|
||||
}
|
||||
};
|
||||
};
|
||||
1
docs/.eleventyignore
Normal file
@@ -0,0 +1 @@
|
||||
README.md
|
||||
118
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
# Distribution files
|
||||
_dist/*
|
||||
|
||||
# yarn
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
.idea
|
||||
5
docs/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"editor.rulers": [
|
||||
80
|
||||
]
|
||||
}
|
||||
11
docs/.yarnrc.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
enableGlobalCache: true
|
||||
|
||||
enableImmutableCache: false
|
||||
|
||||
enableImmutableInstalls: false
|
||||
|
||||
enableTelemetry: false
|
||||
|
||||
httpTimeout: 600000
|
||||
|
||||
nodeLinker: node-modules
|
||||
17
docs/404.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
layout: layouts/home.njk
|
||||
permalink: 404.html
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
# Content not found.
|
||||
|
||||
Go <a href="{{ '/' | url }}">home</a>.
|
||||
|
||||
{% comment %}
|
||||
Read more: https://www.11ty.dev/docs/quicktips/not-found/
|
||||
|
||||
This will work for both GitHub pages and Netlify:
|
||||
|
||||
* https://help.github.com/articles/creating-a-custom-404-page-for-your-github-pages-site/
|
||||
* https://www.netlify.com/docs/redirects/#custom-404
|
||||
{% endcomment %}
|
||||
38
docs/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Penpot Docs
|
||||
|
||||
Penpot documentation website.
|
||||
|
||||
## Usage
|
||||
|
||||
To view this site locally, first set up the environment:
|
||||
|
||||
```sh
|
||||
# only if necessary
|
||||
nvm install
|
||||
nvm use
|
||||
# only if necessary
|
||||
corepack enable
|
||||
|
||||
yarn install
|
||||
```
|
||||
|
||||
And launch a development server:
|
||||
|
||||
```sh
|
||||
yarn start
|
||||
```
|
||||
|
||||
You can then point a browser to [http://localhost:8080](http://localhost:8080).
|
||||
|
||||
## Tooling
|
||||
|
||||
* [Eleventy (11ty)](https://www.11ty.dev/docs)
|
||||
* [Diagrams](https://github.com/gmunguia/markdown-it-plantuml) with
|
||||
[plantuml](https://plantuml.com). See also
|
||||
[real-world-plantuml](https://real-world-plantuml.com).
|
||||
* [Diagrams](https://github.com/agoose77/markdown-it-diagrams) with
|
||||
[svgbob](https://github.com/ivanceras/svgbob) and
|
||||
[mermaid](https://github.com/mermaid-js/mermaid).
|
||||
* [arc42](https://arc42.org/overview) template.
|
||||
* [c4model](https://c4model.com) for software architecture, and an
|
||||
[implementation in plantuml](https://github.com/plantuml-stdlib/C4-PlantUML).
|
||||
21
docs/_data/metadata.json
Executable file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"title": "Help center",
|
||||
"url": "https://docs.penpot.app/",
|
||||
"description": "Design freedom for teams.",
|
||||
"feed": {
|
||||
"subtitle": "Penpot: design freedom for teams.",
|
||||
"filename": "feed.xml",
|
||||
"path": "/feed/feed.xml",
|
||||
"id": "https://docs.penpot.app/"
|
||||
},
|
||||
"jsonfeed": {
|
||||
"path": "/feed/feed.json",
|
||||
"url": "https://docs.penpot.app/feed/feed.json"
|
||||
},
|
||||
"author": {
|
||||
"name": "Penpot",
|
||||
"email": "hello@penpot.app",
|
||||
"url": "https://penpot.app"
|
||||
},
|
||||
"twitter": "@penpotapp"
|
||||
}
|
||||
165
docs/_includes/layouts/base.njk
Normal file
36
docs/_includes/layouts/contributing-guide.njk
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
layout: layouts/base.njk
|
||||
templateClass: tmpl-contributing-guide
|
||||
---
|
||||
|
||||
{%- macro show_children(item) -%}
|
||||
{%- for child in item | children | sorted('data.title') %}
|
||||
{%- if loop.first -%}<ul>{%- endif -%}
|
||||
<li>
|
||||
<a href="{{ child.url }}">{{ child.data.title }}</a>
|
||||
{%- if page.url.includes(child.url) -%}
|
||||
{{ show_children(child) }}
|
||||
{%- endif -%}
|
||||
{%- if child.url == page.url -%}
|
||||
{{ content | toc(tags=['h2', 'h3']) | safe }}
|
||||
{%- endif -%}
|
||||
</li>
|
||||
{%- if loop.last -%}</ul>{%- endif -%}
|
||||
{%- endfor %}
|
||||
{%- endmacro -%}
|
||||
|
||||
<div class="main-container with-sidebar">
|
||||
<aside id="stickySidebar" class="sidebar">
|
||||
{%- set root = '/contributing-guide/index' | find -%}
|
||||
<div id="toc">
|
||||
<div class="header mobile" id="toc-title">{{ root.data.title }}</div>
|
||||
<a class="header" href="{{ root.url }}">{{ root.data.title }}</a>
|
||||
{{ show_children(root) }}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<content class="main-content">
|
||||
{{ content | safe }}
|
||||
</content>
|
||||
|
||||
</div>
|
||||
7
docs/_includes/layouts/home.njk
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
layout: layouts/base.njk
|
||||
templateClass: tmpl-home
|
||||
---
|
||||
<div class="main-container">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
27
docs/_includes/layouts/plugins-home.njk
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
layout: layouts/base.njk
|
||||
templateClass: tmpl-plugins-guide
|
||||
---
|
||||
|
||||
{%- macro show_children(item) -%}
|
||||
{%- for child in item | children | sorted('data.title') %}
|
||||
{%- if loop.first -%}<ul>{%- endif -%}
|
||||
<li>
|
||||
<a href="{{ child.url }}">{{ child.data.title }}</a>
|
||||
{%- if page.url.includes(child.url) -%}
|
||||
{{ show_children(child) }}
|
||||
{%- endif -%}
|
||||
{%- if child.url == page.url -%}
|
||||
{{ content | toc(tags=['h2', 'h3']) | stripHash | safe }}
|
||||
{%- endif -%}
|
||||
</li>
|
||||
{%- if loop.last -%}</ul>{%- endif -%}
|
||||
{%- endfor %}
|
||||
{%- endmacro -%}
|
||||
|
||||
<div class="main-container">
|
||||
<content class="main-content plugins">
|
||||
{{ content | safe }}
|
||||
</content>
|
||||
|
||||
</div>
|
||||
28
docs/_includes/layouts/plugins-no-sidebar.njk
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
layout: layouts/base.njk
|
||||
templateClass: tmpl-plugins-guide
|
||||
---
|
||||
|
||||
{%- macro show_children(item) -%}
|
||||
{%- for child in item | children | sorted('data.title') %}
|
||||
{%- if loop.first -%}<ul>{%- endif -%}
|
||||
<li>
|
||||
<a href="{{ child.url }}">{{ child.data.title }}</a>
|
||||
{%- if page.url.includes(child.url) -%}
|
||||
{{ show_children(child) }}
|
||||
{%- endif -%}
|
||||
{%- if child.url == page.url -%}
|
||||
{{ content | toc(tags=['h2', 'h3']) | stripHash | safe }}
|
||||
{%- endif -%}
|
||||
</li>
|
||||
{%- if loop.last -%}</ul>{%- endif -%}
|
||||
{%- endfor %}
|
||||
{%- endmacro -%}
|
||||
|
||||
<div class="main-container with-sidebar">
|
||||
|
||||
<content class="main-content">
|
||||
{{ content | safe }}
|
||||
</content>
|
||||
|
||||
</div>
|
||||
36
docs/_includes/layouts/plugins.njk
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
layout: layouts/base.njk
|
||||
templateClass: tmpl-plugins-guide
|
||||
---
|
||||
|
||||
{%- macro show_children(item) -%}
|
||||
{%- for child in item | children | sorted('data.title') %}
|
||||
{%- if loop.first -%}<ul>{%- endif -%}
|
||||
<li>
|
||||
<a href="{{ child.url }}">{{ child.data.title }}</a>
|
||||
{%- if page.url.includes(child.url) -%}
|
||||
{{ show_children(child) }}
|
||||
{%- endif -%}
|
||||
{%- if child.url == page.url -%}
|
||||
{{ content | toc(tags=['h2', 'h3']) | stripHash | safe }}
|
||||
{%- endif -%}
|
||||
</li>
|
||||
{%- if loop.last -%}</ul>{%- endif -%}
|
||||
{%- endfor %}
|
||||
{%- endmacro -%}
|
||||
|
||||
<div class="main-container with-sidebar">
|
||||
<aside id="stickySidebar" class="sidebar">
|
||||
{%- set root = '/plugins/index' | find -%}
|
||||
<div id="toc">
|
||||
<div class="header mobile" id="toc-title">{{ root.data.title }}</div>
|
||||
<a class="header" href="{{ root.url }}">{{ root.data.title }}</a>
|
||||
{{ show_children(root) }}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<content class="main-content">
|
||||
{{ content | safe }}
|
||||
</content>
|
||||
|
||||
</div>
|
||||
36
docs/_includes/layouts/technical-guide.njk
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
layout: layouts/base.njk
|
||||
templateClass: tmpl-developer-guide
|
||||
---
|
||||
|
||||
{%- macro show_children(item) -%}
|
||||
{%- for child in item | children | sorted('data.title') %}
|
||||
{%- if loop.first -%}<ul>{%- endif -%}
|
||||
<li>
|
||||
<a href="{{ child.url }}">{{ child.data.title }}</a>
|
||||
{%- if page.url.includes(child.url) -%}
|
||||
{{ show_children(child) }}
|
||||
{%- endif -%}
|
||||
{%- if child.url == page.url -%}
|
||||
{{ content | toc(tags=['h2', 'h3']) | stripHash | safe }}
|
||||
{%- endif -%}
|
||||
</li>
|
||||
{%- if loop.last -%}</ul>{%- endif -%}
|
||||
{%- endfor %}
|
||||
{%- endmacro -%}
|
||||
|
||||
<div class="main-container with-sidebar">
|
||||
<aside id="stickySidebar" class="sidebar">
|
||||
{%- set root = '/technical-guide/index' | find -%}
|
||||
<div id="toc">
|
||||
<div class="header mobile" id="toc-title">{{ root.data.title }}</div>
|
||||
<a class="header" href="{{ root.url }}">{{ root.data.title }}</a>
|
||||
{{ show_children(root) }}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<content class="main-content">
|
||||
{{ content | safe }}
|
||||
</content>
|
||||
|
||||
</div>
|
||||
36
docs/_includes/layouts/user-guide.njk
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
layout: layouts/base.njk
|
||||
templateClass: tmpl-user-guide
|
||||
---
|
||||
|
||||
{%- macro show_children(item) -%}
|
||||
{%- for child in item | children | sorted('data.title') %}
|
||||
{%- if loop.first -%}<ul>{%- endif -%}
|
||||
<li>
|
||||
<a href="{{ child.url }}">{{ child.data.title }}</a>
|
||||
{%- if page.url.includes(child.url) -%}
|
||||
{{ show_children(child) }}
|
||||
{%- endif -%}
|
||||
{%- if child.url == page.url -%}
|
||||
{{ content | toc(tags=['h2', 'h3']) | safe }}
|
||||
{%- endif -%}
|
||||
</li>
|
||||
{%- if loop.last -%}</ul>{%- endif -%}
|
||||
{%- endfor %}
|
||||
{%- endmacro -%}
|
||||
|
||||
<div class="main-container with-sidebar">
|
||||
<aside id="stickySidebar" class="sidebar">
|
||||
{%- set root = '/user-guide/index' | find -%}
|
||||
<div id="toc">
|
||||
<div class="header mobile" id="toc-title">{{ root.data.title }}</div>
|
||||
<a class="header" href="{{ root.url }}">{{ root.data.title }}</a>
|
||||
{{ show_children(root) }}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<content class="main-content">
|
||||
{{ content | safe }}
|
||||
</content>
|
||||
|
||||
</div>
|
||||
13
docs/contributing-guide/coc/index.njk
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: 04· Code of Conduct
|
||||
---
|
||||
|
||||
<h1 id="coc">Code of conduct</h1>
|
||||
|
||||
<p>As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.</p>
|
||||
<p>We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.</p>
|
||||
<p>Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.</p>
|
||||
<p>Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.</p>
|
||||
<p>This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.</p>
|
||||
<p>Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.</p>
|
||||
<p>This Code of Conduct is adapted from the Contributor Covenant, version 1.1.0, available from <a href="http://contributor-covenant.org/version/1/1/0/" target="_blank">http://contributor-covenant.org/version/1/1/0/</a></p>
|
||||
108
docs/contributing-guide/code-contributions/index.njk
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: 03· Core code contributions
|
||||
---
|
||||
|
||||
<h1 id="code-contributions">Core code contributions</h1>
|
||||
|
||||
<p class="main-paragraph">Details to know how to improve Penpot's core code</p>
|
||||
|
||||
<p class="advice">
|
||||
Thinking of contributing to Penpot core but not sure where to start? We’ve made a curated selection of enhancements to help you with that. We believe that these tasks should be a great way to get started with Penpot development and quickly become an active contributor.
|
||||
<br><br>
|
||||
<a href="https://github.com/penpot/penpot/contribute" target="_blank">Here’s the list of enhancements labeled as "good first issue"</a>
|
||||
</p>
|
||||
|
||||
<h3 id="code-contributions-techguide">Technical guide</h3>
|
||||
<p>Go to the <a href="/technical-guide">Technical guide</a> to get detailed explanations about how to get Penpot application and run it locally, to test it or make changes to it.</p>
|
||||
|
||||
<h3 id="code-contributions-pull-requests">Pull requests</h3>
|
||||
<p>If you want propose a change or bug fix with the Pull-Request system firstly you should carefully read the <a href="#code-contributions-dco">DCO section</a> and format your commits accordingly.</p>
|
||||
<p>If you intend to fix a bug it's fine to submit a pull request right away but we still recommend to file an issue detailing what you're fixing. This is helpful in case we don't accept that specific fix but want to keep track of the issue.</p>
|
||||
<p>If you want to implement or start working in a new feature, please open a <b>question</b> / <b>discussion</b> issue for it. No pull-request will be accepted without previous chat about the changes, independently if it is a new feature, already planned feature or small quick win.</p>
|
||||
<p>If is going to be your first pull request, You can learn how from <a href="https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github" target="_blank">this free video series</a>.</p>
|
||||
<p>We will use the <code class="language-bash">easy fix</code> mark for tag for indicate issues that are easy for beginners.</p>
|
||||
|
||||
<h3 id="code-contributions-commits">Commit message guidelines</h3>
|
||||
<p>We have very precise rules over how our git commit messages can be formatted.</p>
|
||||
<p>The commit message format is:</p>
|
||||
<pre>
|
||||
<code class="language-md">
|
||||
<type> <subject>
|
||||
|
||||
[body]
|
||||
|
||||
[footer]
|
||||
</code>
|
||||
</pre>
|
||||
<p>Where type is:</p>
|
||||
<ul>
|
||||
<li><g-emoji class="g-emoji" alias="bug" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f41b.png"><img class="emoji" alt="bug" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f41b.png"></g-emoji> <code>:bug:</code> a commit that fixes a bug</li>
|
||||
<li><g-emoji class="g-emoji" alias="sparkles" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/2728.png"><img class="emoji" alt="sparkles" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/2728.png"></g-emoji> <code>:sparkles:</code> a commit that adds an improvement</li>
|
||||
<li><g-emoji class="g-emoji" alias="tada" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f389.png"><img class="emoji" alt="tada" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f389.png"></g-emoji> <code>:tada:</code> a commit with new feature</li>
|
||||
<li><g-emoji class="g-emoji" alias="recycle" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/267b.png"><img class="emoji" alt="recycle" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/267b.png"></g-emoji> <code>:recycle:</code> a commit that introduces a refactor</li>
|
||||
<li><g-emoji class="g-emoji" alias="lipstick" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f484.png"><img class="emoji" alt="lipstick" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f484.png"></g-emoji> <code>:lipstick:</code> a commit with cosmetic changes</li>
|
||||
<li><g-emoji class="g-emoji" alias="ambulance" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f691.png"><img class="emoji" alt="ambulance" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f691.png"></g-emoji> <code>:ambulance:</code> a commit that fixes critical bug</li>
|
||||
<li><g-emoji class="g-emoji" alias="books" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f4da.png"><img class="emoji" alt="books" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f4da.png"></g-emoji> <code>:books:</code> a commit that improves or adds documentation</li>
|
||||
<li><g-emoji class="g-emoji" alias="construction" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f6a7.png"><img class="emoji" alt="construction" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f6a7.png"></g-emoji> <code>:construction:</code> a wip commit</li>
|
||||
<li><g-emoji class="g-emoji" alias="construction_worker" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f477.png"><img class="emoji" alt="construction_worker" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f477.png"></g-emoji> <code>:construction_worker:</code> a commit with CI related stuff</li>
|
||||
<li><g-emoji class="g-emoji" alias="boom" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png"><img class="emoji" alt="boom" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png"></g-emoji> <code>:boom:</code> a commit with breaking changes</li>
|
||||
<li><g-emoji class="g-emoji" alias="wrench" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f527.png"><img class="emoji" alt="wrench" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f527.png"></g-emoji> <code>:wrench:</code> a commit for config updates</li>
|
||||
<li><g-emoji class="g-emoji" alias="zap" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/26a1.png"><img class="emoji" alt="zap" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/26a1.png"></g-emoji> <code>:zap:</code> a commit with performance improvements</li>
|
||||
<li><g-emoji class="g-emoji" alias="whale" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f433.png"><img class="emoji" alt="whale" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f433.png"></g-emoji> <code>:whale:</code> a commit for docker related stuff</li>
|
||||
<li><g-emoji class="g-emoji" alias="rewind" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/23ea.png"><img class="emoji" alt="rewind" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/23ea.png"></g-emoji> <code>:rewind:</code> a commit that reverts changes</li>
|
||||
<li><g-emoji class="g-emoji" alias="paperclip" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f4ce.png"><img class="emoji" alt="paperclip" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f4ce.png"></g-emoji> <code>:paperclip:</code> a commit with other not relevant changes</li>
|
||||
<li><g-emoji class="g-emoji" alias="arrow_up" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/2b06.png"><img class="emoji" alt="arrow_up" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/2b06.png"></g-emoji> <code>:arrow_up:</code> a commit with dependencies updates</li>
|
||||
</ul>
|
||||
<p>More info:</p>
|
||||
<ul>
|
||||
<li><a href="https://gist.github.com/parmentf/035de27d6ed1dce0b36a">https://gist.github.com/parmentf/035de27d6ed1dce0b36a</a></li>
|
||||
<li><a href="https://gist.github.com/rxaviers/7360908">https://gist.github.com/rxaviers/7360908</a></li>
|
||||
</ul>
|
||||
<p>The subject should be:</p>
|
||||
<ul>
|
||||
<li>Use the imperative mood.</li>
|
||||
<li>Capitalize the first letter.</li>
|
||||
<li>Don't put a period at the end of the subject line.</li>
|
||||
<li>Put a blank line between the subject line and the body.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="code-contributions-dco">Developer's Certificate of Origin (DCO)</h3>
|
||||
<p>By submitting code you are agree and can certify the below:</p>
|
||||
<pre>
|
||||
<code class="language-markdown">
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>Then, all your code patches (<b>documentation are excluded</b>) should contain a sign-off at the end of the patch/commit description body. It can be automatically added on adding <code class="language-bash">-s</code> parameter to <code class="language-bash">git commit</code>.</p>
|
||||
<p>This is an example of the aspect of the line:</p>
|
||||
<pre>
|
||||
<code class="language-markdown">
|
||||
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
|
||||
</code>
|
||||
</pre>
|
||||
<p>Please, use your real name (sorry, no pseudonyms or anonymous contributions are allowed).</p>
|
||||
4
docs/contributing-guide/contributing-guide.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"layout": "layouts/contributing-guide.njk",
|
||||
"tags": "contributing-guide"
|
||||
}
|
||||
47
docs/contributing-guide/index.njk
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: Contributing
|
||||
eleventyNavigation:
|
||||
key: Contributing
|
||||
order: 3
|
||||
---
|
||||
|
||||
<div class="main-illus">
|
||||
<img src="/img/home-contributing.png" alt="User guide" border="0">
|
||||
</div>
|
||||
|
||||
<h1 id="contributing-guide">Contributing guide.</h1>
|
||||
|
||||
<p class="main-paragraph">In this documentation you will find (almost) everything you need to know about how to contribute at Penpot.</p>
|
||||
|
||||
<ul class="intro-sections">
|
||||
<li>
|
||||
<a href="/contributing-guide/reporting-bugs">
|
||||
<h2>Reporting bugs</h2>
|
||||
<p>Easy steps to bug hunting</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contributing-guide/translations">
|
||||
<h2>Translations</h2>
|
||||
<p>How to become a Penpot translator</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contributing-guide/code-contributions">
|
||||
<h2>Core code contributions</h2>
|
||||
<p>Help Penpot improve its code</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contributing-guide/coc">
|
||||
<h2>Code of conduct</h2>
|
||||
<p>Rules, values and principles</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="illus illus-libraries">
|
||||
<a href="https://penpot.app/libraries-templates" target="_blank">
|
||||
<h2>Libraries & templates</h2>
|
||||
<p>Share your libraries and templates or download the ones you like.</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
15
docs/contributing-guide/libraries-templates/index.njk
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: 05· Libraries & Templates
|
||||
---
|
||||
|
||||
<h1 id="libraries">Libraries & templates</h1>
|
||||
|
||||
<img src="/img/contributing-libraries.png" alt="libraries and templates" border="0">
|
||||
|
||||
<p>There are published Penpot files ready to use made by community members and Penpot core team members.</p>
|
||||
|
||||
<ul>
|
||||
<li>Here you can find the complete list of <a href="https://penpot.app/libraries-templates" target="_blank">available Libraries & Templates</a>.</li>
|
||||
<li>Are you willing to contribute sharing a Penpot file with the Penpot community? Here's a <a href="https://penpot.app/how-to-contribute" target="_blank">how you can do it</a>.</li>
|
||||
<li>If you are in doubt about how to use one of these files, here you can <a href="https://penpot.app/libraries-templates#how-to-use" target="_blank">watch a video explanation</a>.</li>
|
||||
</ul>
|
||||
23
docs/contributing-guide/reporting-bugs/index.njk
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: 01· Reporting bugs
|
||||
---
|
||||
|
||||
<h1 id="reporting-bugs">Reporting bugs</h1>
|
||||
|
||||
<p class="main-paragraph">Bug hunting is not difficult if you know how.</p>
|
||||
|
||||
<h2 id="reporting-bugs-howto">How to report a bug</h2>
|
||||
|
||||
<p>We are using <a href="https://github.com/penpot/penpot/issues" target="_blank">GitHub Issues</a> for our public bugs. We keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn't already exist.</p>
|
||||
|
||||
<p>If you found a bug, please report it, as far as possible including:</p>
|
||||
|
||||
<ul>
|
||||
<li>a detailed explanation of steps to reproduce the error.</li>
|
||||
<li>a browser and the browser version used.</li>
|
||||
<li>a dev tools console exception stack trace (if it is available).</li>
|
||||
</ul>
|
||||
|
||||
<p>Consider sending us an email first at <a href="mailto:support@penpot.app">support@penpot.app</a> if you discovered a bug that you'd prefer to discuss in confidence (such as a security bug).</p>
|
||||
|
||||
<p><strong>We don't have formal bug bounty program for security reports; this is an open source application and your contribution will be recognized in the changelog.</strong></p>
|
||||
58
docs/contributing-guide/translations/index.njk
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: 02· Translations
|
||||
---
|
||||
|
||||
<h1 id="translations">Translations</h1>
|
||||
|
||||
<p class="main-paragraph">Thank you for interest in contribute translating Penpot. Here you will find ways to do it.</p>
|
||||
|
||||
<h2 id="translations-howto">How to become a Penpot translator</h2>
|
||||
<p>We are using <a href="https://hosted.weblate.org/projects/penpot/frontend/" target="_blank">Weblate</a> as translation platform, so the first thing you need to be a Penpot translator is to have a Weblate account (you can <a href="https://hosted.weblate.org/accounts/register/" target="_blank">register here</a>).</p>
|
||||
<p>To start translating at Penpot:</p>
|
||||
<ol>
|
||||
<li>Open a <a href="https://github.com/penpot/penpot/issues" target="_blank">github issue</a> giving details about the language you want to translate (language), the type of translation (new language, new translation or change an existing translation) and your Weblate user.</li>
|
||||
<li>If everything is correct we will get back to you providing you permissions to the actions needed.</li>
|
||||
<li>You also might want to take a look at the guide for <a href="https://docs.weblate.org/en/latest/user/translating.html" target="_blank">Translating using Weblate</a>.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="translations-howto">Add a new language</h2>
|
||||
<p>To add a language that is still not among the Penpot language options:</p>
|
||||
<ol>
|
||||
<li>Go to the <a href="https://hosted.weblate.org/projects/penpot/frontend/" target="_blank">languages list</a>.</li>
|
||||
<li>Press the "Start new translation" button.</li>
|
||||
<li>Choose the language you want to translate to.</li>
|
||||
<li>Press the "Start new translation" button at the start new translation page.</li>
|
||||
<li>Start translating strings for the new language :)</li>
|
||||
</ol>
|
||||
<p><img src="/img/translations-start.png" alt="translations" /></p>
|
||||
<p><img src="/img/translations-start-translation.png" alt="translations" /></p>
|
||||
|
||||
|
||||
<h2 id="translations-howto">Add a new translation</h2>
|
||||
<p>To add a new translation (a string with a lacking translation for a certain language) follow the next steps:</p>
|
||||
<ol>
|
||||
<li>Go to the <a href="https://hosted.weblate.org/projects/penpot/frontend/" target="_blank">languages list</a>.</li>
|
||||
<li>Click the edit button (pencil icon) close to the name of the language where you want to add the missing translation or translations.</li>
|
||||
<li>Find and select the translation/s to complete.</li>
|
||||
<li>Complete the translation in the required input field.</li>
|
||||
<li>Press the "Save· button.</li>
|
||||
<li>Repeat the action with as many translation strings you can / you want ;)</li>
|
||||
</ol>
|
||||
<p>Saved new translations will automatically get the status "waiting for review". Our team will periodically check strings waiting for review and, if considered correct, will approve them.</p>
|
||||
<p><img src="/img/translations-lang-list.png" alt="translations" /></p>
|
||||
<p><img src="/img/translations-strings-list.png" alt="translations" /></p>
|
||||
|
||||
|
||||
<h2 id="translations-howto">Change an approved translation</h2>
|
||||
<p>To edit an already approved translation string follow the next steps:</p>
|
||||
<ol>
|
||||
<li>Go to the <a href="https://hosted.weblate.org/projects/penpot/frontend/" target="_blank">languages list</a>.</li>
|
||||
<li>Click the name of the language where is the translation you want to change.</li>
|
||||
<li>Click the Browse button.</li>
|
||||
<li>Find and select the translation/s to complete.</li>
|
||||
<li>Change the translation in the input field.</li>
|
||||
<li>Press the "Save" button if you have permissions.</li>
|
||||
<li>If you don't have permissions to Save you can still press "Suggest" to make a suggestion.</li>
|
||||
</ol>
|
||||
<p>Saved editions will get the status "Waiting for review". Suggestions will get the status "Approved strings with suggestions". Our team will periodically check strings waiting for review and, if considered correct, will approve them.</p>
|
||||
<p><img src="/img/translations-lang-state.png" alt="translations" /></p>
|
||||
1210
docs/css/index.css
Normal file
92
docs/css/prism-base16-monokai.dark.css
Normal file
@@ -0,0 +1,92 @@
|
||||
/* Styles for syntax highlighting inside code blocks */
|
||||
|
||||
code[class*="language-"], pre[class*="language-"] {
|
||||
font-size: 14px;
|
||||
line-height: 1.375;
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
-moz-tab-size: 2;
|
||||
-o-tab-size: 2;
|
||||
tab-size: 2;
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
background: #272822;
|
||||
color: #f8f8f2;
|
||||
max-width: 40rem;
|
||||
}
|
||||
pre[class*="language-"] {
|
||||
padding: 1.5em 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
}
|
||||
.token.comment, .token.prolog, .token.doctype, .token.cdata {
|
||||
color: #75715e;
|
||||
}
|
||||
.token.punctuation {
|
||||
color: #f8f8f2;
|
||||
}
|
||||
.token.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
.token.operator, .token.boolean, .token.number {
|
||||
color: #fd971f;
|
||||
}
|
||||
.token.property {
|
||||
color: #f4bf75;
|
||||
}
|
||||
.token.tag {
|
||||
color: #66d9ef;
|
||||
}
|
||||
.token.string {
|
||||
color: #a1efe4;
|
||||
}
|
||||
.token.selector {
|
||||
color: #ae81ff;
|
||||
}
|
||||
.token.attr-name {
|
||||
color: #fd971f;
|
||||
}
|
||||
.token.entity, .token.url, .language-css .token.string, .style .token.string {
|
||||
color: #a1efe4;
|
||||
}
|
||||
.token.attr-value, .token.keyword, .token.control, .token.directive, .token.unit {
|
||||
color: #a6e22e;
|
||||
}
|
||||
.token.statement, .token.regex, .token.atrule {
|
||||
color: #a1efe4;
|
||||
}
|
||||
.token.placeholder, .token.variable {
|
||||
color: #66d9ef;
|
||||
}
|
||||
.token.deleted {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.token.inserted {
|
||||
border-bottom: 1px dotted #f9f8f5;
|
||||
text-decoration: none;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.token.important, .token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.token.important {
|
||||
color: #f92672;
|
||||
}
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
pre > code.highlight {
|
||||
outline: 0.4em solid #f92672;
|
||||
outline-offset: .4em;
|
||||
}
|
||||
122
docs/css/prism.css
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* GHColors theme by Avi Aryan (http://aviaryan.in)
|
||||
* Inspired by Github syntax coloring
|
||||
*/
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #393A34;
|
||||
font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace;
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
font-size: .9em;
|
||||
line-height: 1.2em;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre > code[class*="language-"] {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
|
||||
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
|
||||
code[class*="language-"]::selection, code[class*="language-"] ::selection {
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
border: 1px solid #dddddd;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: .2em;
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
background: #f8f8f8;
|
||||
border: 1px solid #dddddd;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #999988;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.token.string,
|
||||
.token.attr-value {
|
||||
color: #e3116c;
|
||||
}
|
||||
|
||||
.token.punctuation,
|
||||
.token.operator {
|
||||
color: #393A34; /* no highlight */
|
||||
}
|
||||
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.token.symbol,
|
||||
.token.number,
|
||||
.token.boolean,
|
||||
.token.variable,
|
||||
.token.constant,
|
||||
.token.property,
|
||||
.token.regex,
|
||||
.token.inserted {
|
||||
color: #36acaa;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.keyword,
|
||||
.token.attr-name,
|
||||
.language-autohotkey .token.selector {
|
||||
color: #00a4db;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.deleted,
|
||||
.language-autohotkey .token.tag {
|
||||
color: #9a050f;
|
||||
}
|
||||
|
||||
.token.tag,
|
||||
.token.selector,
|
||||
.language-autohotkey .token.keyword {
|
||||
color: #00009f;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.function,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
29
docs/feed/feed.njk
Executable file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
# Metadata comes from _data/metadata.json
|
||||
permalink: "{{ metadata.feed.path }}"
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>{{ metadata.title }}</title>
|
||||
<subtitle>{{ metadata.feed.subtitle }}</subtitle>
|
||||
{% set absoluteUrl %}{{ metadata.feed.path | url | absoluteUrl(metadata.url) }}{% endset %}
|
||||
<link href="{{ absoluteUrl }}" rel="self"/>
|
||||
<link href="{{ metadata.url }}"/>
|
||||
<updated>{{ collections.posts | rssLastUpdatedDate }}</updated>
|
||||
<id>{{ metadata.feed.id }}</id>
|
||||
<author>
|
||||
<name>{{ metadata.author.name }}</name>
|
||||
<email>{{ metadata.author.email }}</email>
|
||||
</author>
|
||||
{%- for post in collections.posts | reverse %}
|
||||
{% set absolutePostUrl %}{{ post.url | url | absoluteUrl(metadata.url) }}{% endset %}
|
||||
<entry>
|
||||
<title>{{ post.data.title }}</title>
|
||||
<link href="{{ absolutePostUrl }}"/>
|
||||
<updated>{{ post.date | rssDate }}</updated>
|
||||
<id>{{ absolutePostUrl }}</id>
|
||||
<content type="html">{{ post.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }}</content>
|
||||
</entry>
|
||||
{%- endfor %}
|
||||
</feed>
|
||||
6
docs/feed/htaccess.njk
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
permalink: feed/.htaccess
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
# For Apache, to show `{{ metadata.feed.filename }}` when browsing to directory /feed/ (hide the file!)
|
||||
DirectoryIndex {{ metadata.feed.filename }}
|
||||
31
docs/feed/json.njk
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
# Metadata comes from _data/metadata.json
|
||||
permalink: "{{ metadata.jsonfeed.path }}"
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
{
|
||||
"version": "https://jsonfeed.org/version/1",
|
||||
"title": "{{ metadata.title }}",
|
||||
"home_page_url": "{{ metadata.url }}",
|
||||
"feed_url": "{{ metadata.jsonfeed.url }}",
|
||||
"description": "{{ metadata.description }}",
|
||||
"author": {
|
||||
"name": "{{ metadata.author.name }}",
|
||||
"url": "{{ metadata.author.url }}"
|
||||
},
|
||||
"items": [
|
||||
{%- for post in collections.posts | reverse %}
|
||||
{%- set absolutePostUrl %}{{ post.url | url | absoluteUrl(metadata.url) }}{% endset -%}
|
||||
{
|
||||
"id": "{{ absolutePostUrl }}",
|
||||
"url": "{{ absolutePostUrl }}",
|
||||
"title": "{{ post.data.title }}",
|
||||
"content_html": {% if post.templateContent %}{{ post.templateContent | dump | safe }}{% else %}""{% endif %},
|
||||
"date_published": "{{ post.date | rssDate }}"
|
||||
}
|
||||
{%- if not loop.last -%}
|
||||
,
|
||||
{%- endif -%}
|
||||
{%- endfor %}
|
||||
]
|
||||
}
|
||||
0
docs/img/.gitkeep
Normal file
BIN
docs/img/a11y-tree-btn.webp
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
docs/img/accesstokens-created.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/img/accesstokens-empty.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
docs/img/accesstokens-generate-0.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/img/accesstokens-generate-1.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/img/accesstokens-generate-2.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/img/artboards-creation.gif
Normal file
|
After Width: | Height: | Size: 368 KiB |
BIN
docs/img/artboards-grid-int.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/img/artboards-move.gif
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
docs/img/assets-add-typo.gif
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/img/assets-add.gif
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
docs/img/assets-edit.gif
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
docs/img/assets-filter.gif
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
docs/img/assets-order.gif
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
docs/img/assets-search.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/img/assets-use.gif
Normal file
|
After Width: | Height: | Size: 581 KiB |
BIN
docs/img/assets-viewmode.gif
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/img/bg.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
docs/img/board-clip-content.gif
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
docs/img/board-create.gif
Normal file
|
After Width: | Height: | Size: 289 KiB |
BIN
docs/img/board-show-fill.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
docs/img/board-show-viewmode.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
docs/img/booleans.gif
Normal file
|
After Width: | Height: | Size: 113 KiB |
1
docs/img/caret-down.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="500" width="500" xmlns="http://www.w3.org/2000/svg"><path d="M499.963 138.951c.92-15.98-15.647-29.97-31.334-25.607-8.65 4.13-29.786 24.567-29.786 24.567S314.235 262.451 250.29 323.039c-7.374 2.988-11.746-5.241-16.468-9.099-65.776-65.676-128.32-134.54-194.558-199.746C25.214 104.153 2.76 113.56.395 130.81c-2.132 11.186 4.678 21.436 12.904 28.266 65.465 69.318 132.004 137.627 198.04 206.412 8.141 7.657 15.294 16.969 25.014 22.61 12.025 5.093 25.682-1.092 33.082-11.041 75.378-74.274 150.948-148.38 224.898-224.07 3.303-3.903 5.912-8.78 5.63-14.036z"/></svg>
|
||||
|
After Width: | Height: | Size: 571 B |
BIN
docs/img/codemode-activate.gif
Normal file
|
After Width: | Height: | Size: 319 KiB |
BIN
docs/img/codemode-code.png
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
docs/img/codemode-copy.gif
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/img/codemode-measures.gif
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
docs/img/codemode-properties.gif
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
docs/img/collapsing-groups.gif
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
docs/img/color-palette.gif
Normal file
|
After Width: | Height: | Size: 366 KiB |
BIN
docs/img/color-picker.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
docs/img/color-selection.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/img/comments-add.gif
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
docs/img/comments-dashboard.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
docs/img/comments-thread.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
docs/img/comments-view.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
docs/img/components-create-folder.gif
Normal file
|
After Width: | Height: | Size: 253 KiB |
BIN
docs/img/components-create.gif
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
docs/img/components-overrides.gif
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
docs/img/components-update.gif
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
docs/img/components-updatenotification.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
docs/img/components/components-annotation.mp4
Normal file
BIN
docs/img/components/components-annotation.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
docs/img/components/components-annotations-inspect.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |