Collaborate at Github →
+Penpot is open source. Get involved on the community or contribute to the project.
+diff --git a/docs/.editorconfig b/docs/.editorconfig new file mode 100644 index 000000000..d41540467 --- /dev/null +++ b/docs/.editorconfig @@ -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 diff --git a/docs/.eleventy.js b/docs/.eleventy.js new file mode 100644 index 000000000..2f6a76352 --- /dev/null +++ b/docs/.eleventy.js @@ -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, ""); + }); + + // 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" + } + }; +}; diff --git a/docs/.eleventyignore b/docs/.eleventyignore new file mode 100644 index 000000000..b43bf86b5 --- /dev/null +++ b/docs/.eleventyignore @@ -0,0 +1 @@ +README.md diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..5fbef93cf --- /dev/null +++ b/docs/.gitignore @@ -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 diff --git a/docs/.nvmrc b/docs/.nvmrc new file mode 100644 index 000000000..ee09fac75 --- /dev/null +++ b/docs/.nvmrc @@ -0,0 +1 @@ +v20.11.1 diff --git a/docs/.vscode/settings.json b/docs/.vscode/settings.json new file mode 100644 index 000000000..1e915ede1 --- /dev/null +++ b/docs/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.rulers": [ + 80 + ] +} diff --git a/docs/.yarnrc.yml b/docs/.yarnrc.yml new file mode 100644 index 000000000..5a0ce9a8b --- /dev/null +++ b/docs/.yarnrc.yml @@ -0,0 +1,11 @@ +enableGlobalCache: true + +enableImmutableCache: false + +enableImmutableInstalls: false + +enableTelemetry: false + +httpTimeout: 600000 + +nodeLinker: node-modules diff --git a/docs/404.md b/docs/404.md new file mode 100644 index 000000000..0d39dd3a3 --- /dev/null +++ b/docs/404.md @@ -0,0 +1,17 @@ +--- +layout: layouts/home.njk +permalink: 404.html +eleventyExcludeFromCollections: true +--- +# Content not found. + +Go home. + +{% 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 %} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..5e0800e05 --- /dev/null +++ b/docs/README.md @@ -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). diff --git a/docs/_data/metadata.json b/docs/_data/metadata.json new file mode 100755 index 000000000..69eabe9f6 --- /dev/null +++ b/docs/_data/metadata.json @@ -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" +} diff --git a/docs/_includes/layouts/base.njk b/docs/_includes/layouts/base.njk new file mode 100644 index 000000000..724393deb --- /dev/null +++ b/docs/_includes/layouts/base.njk @@ -0,0 +1,165 @@ + + +
+ + + + + + + + + + + + + {% metagen + title=title or metadata.title, + desc=desc or metadata.desc, + url="https://help.penpot.app" + page.url, + img="https://help.penpot.app/img/th-help-center.jpg", + img_alt=alt, + twitterHandle=twitter or metadata.twitter, + name=name + %} + + + + + + + + +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.
+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.
+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.
+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.
+This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
+This Code of Conduct is adapted from the Contributor Covenant, version 1.1.0, available from http://contributor-covenant.org/version/1/1/0/
diff --git a/docs/contributing-guide/code-contributions/index.njk b/docs/contributing-guide/code-contributions/index.njk new file mode 100644 index 000000000..619295579 --- /dev/null +++ b/docs/contributing-guide/code-contributions/index.njk @@ -0,0 +1,108 @@ +--- +title: 03· Core code contributions +--- + +Details to know how to improve Penpot's core code
+ +
+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.
+
+Here’s the list of enhancements labeled as "good first issue"
+
Go to the Technical guide to get detailed explanations about how to get Penpot application and run it locally, to test it or make changes to it.
+ +If you want propose a change or bug fix with the Pull-Request system firstly you should carefully read the DCO section and format your commits accordingly.
+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.
+If you want to implement or start working in a new feature, please open a question / discussion 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.
+If is going to be your first pull request, You can learn how from this free video series.
+We will use the easy fix
mark for tag for indicate issues that are easy for beginners.
We have very precise rules over how our git commit messages can be formatted.
+The commit message format is:
+
+
+<type> <subject>
+
+[body]
+
+[footer]
+
+
+Where type is:
+:bug:
a commit that fixes a bug:sparkles:
a commit that adds an improvement:tada:
a commit with new feature:recycle:
a commit that introduces a refactor:lipstick:
a commit with cosmetic changes:ambulance:
a commit that fixes critical bug:books:
a commit that improves or adds documentation:construction:
a wip commit:construction_worker:
a commit with CI related stuff:boom:
a commit with breaking changes:wrench:
a commit for config updates:zap:
a commit with performance improvements:whale:
a commit for docker related stuff:rewind:
a commit that reverts changes:paperclip:
a commit with other not relevant changes:arrow_up:
a commit with dependencies updatesMore info:
+ +The subject should be:
+By submitting code you are agree and can certify the below:
+
+
+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.
+
+
+
+Then, all your code patches (documentation are excluded) should contain a sign-off at the end of the patch/commit description body. It can be automatically added on adding -s
parameter to git commit
.
This is an example of the aspect of the line:
+
+
+Signed-off-by: Andrey Antukh
+
+
+Please, use your real name (sorry, no pseudonyms or anonymous contributions are allowed).
diff --git a/docs/contributing-guide/contributing-guide.json b/docs/contributing-guide/contributing-guide.json new file mode 100644 index 000000000..e257ef414 --- /dev/null +++ b/docs/contributing-guide/contributing-guide.json @@ -0,0 +1,4 @@ +{ + "layout": "layouts/contributing-guide.njk", + "tags": "contributing-guide" +} diff --git a/docs/contributing-guide/index.njk b/docs/contributing-guide/index.njk new file mode 100644 index 000000000..d6b0a74ca --- /dev/null +++ b/docs/contributing-guide/index.njk @@ -0,0 +1,47 @@ +--- +title: Contributing +eleventyNavigation: + key: Contributing + order: 3 +--- + +In this documentation you will find (almost) everything you need to know about how to contribute at Penpot.
+ +Easy steps to bug hunting
+ +How to become a Penpot translator
+ +Help Penpot improve its code
+ +Rules, values and principles
+ +Share your libraries and templates or download the ones you like.
+ +There are published Penpot files ready to use made by community members and Penpot core team members.
+ +Bug hunting is not difficult if you know how.
+ +We are using GitHub Issues 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.
+ +If you found a bug, please report it, as far as possible including:
+ +Consider sending us an email first at support@penpot.app if you discovered a bug that you'd prefer to discuss in confidence (such as a security bug).
+ +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.
diff --git a/docs/contributing-guide/translations/index.njk b/docs/contributing-guide/translations/index.njk new file mode 100644 index 000000000..2d5bfae2b --- /dev/null +++ b/docs/contributing-guide/translations/index.njk @@ -0,0 +1,58 @@ +--- +title: 02· Translations +--- + +Thank you for interest in contribute translating Penpot. Here you will find ways to do it.
+ +We are using Weblate as translation platform, so the first thing you need to be a Penpot translator is to have a Weblate account (you can register here).
+To start translating at Penpot:
+To add a language that is still not among the Penpot language options:
+To add a new translation (a string with a lacking translation for a certain language) follow the next steps:
+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.
+ + + + +To edit an already approved translation string follow the next steps:
+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.
+ diff --git a/docs/css/index.css b/docs/css/index.css new file mode 100644 index 000000000..9b6b5c308 --- /dev/null +++ b/docs/css/index.css @@ -0,0 +1,1210 @@ +:root { + + --darkest: #000000; + --graydark: #1F1F1F; + --graymedium: #7B7D85; + --graylight: #E3E3E3; + --primary: #31EFB8; + --primarydark: #00af7d; + --white: #fff; + --advice: #dafffb; + + /* 2023 color pallete from redesigned Penpot web */ + --color-gray-light: #F5F8FB; + --color-gray-medium: #DDE3E9; + --color-gray-dark: #1C2022; +} +* { + box-sizing: border-box; +} +html, +body { + padding: 0; + margin: 0; + font-family: 'Work Sans',system-ui, sans-serif; + color: var(--color-gray-dark); +} +body { + background-color: var(--white); + /* background-image: linear-gradient(rgba(68, 68, 68, 0.05), white); + background-size: 100% 300px; + background-repeat: no-repeat; */ +} +p:last-child { + margin-bottom: 0; +} +p, +.tmpl-post li, +img, +h1, h2, h3, +.main-content { + max-width: 42rem; + position: relative; +} + +@media (max-width: 997px) { + .main-content { + max-width: 40rem; + } +} + +@media (max-width: 966px) { + .main-content { + max-width: 38rem; + } +} + +@media (max-width: 933px) { + .main-content { + max-width: 36rem; + } +} + +@media (max-width: 901px) { + .main-content { + max-width: 35rem; + } +} + +img { + max-width: 100%; +} +p, +li { + line-height: 1.6; +} +ul, ol { + margin-bottom: 2rem; + padding-left: 1rem; +} +li { + margin-bottom: 1rem; +} + +a[href] { + color: var(--primarydark); +} +a[href]:hover { + color: var(--darkest); +} +a[href]:visited { + /* color: var(--primarydark); */ +} + +/* href with code in the text */ +:not(pre) > a > code[class*="language-"] { + text-decoration: underline; +} + +main { + padding: 1rem; + /* background-image: url(/img/bg.png); + background-repeat: no-repeat; + background-position: center 120px; */ +} +header { + display: flex; + border-bottom: 1px solid var(--color-gray-medium); + padding: 1.6rem; + position: sticky; + top: 0; + width: 100%; + background-color: white; + backdrop-filter: blur(6px); + background: #FFFFFFD9; + z-index: 10; +} +header:after { + content: ""; + display: table; + clear: both; +} +header a[href] { + color: var(--graydark); +} +table { + margin: 1em 0; +} +table td, +table th { + padding-right: 1em; +} +kbd { + border: 1px solid var(--graylight); + border-radius: 4px; + padding: 0.4rem 0.6rem; + box-shadow: 0 1px 0px 1px rgb(0 0 0 / 10%); + background-color: var(--white); + margin-left: 0.16rem; + margin-right: 0.16rem; + white-space: nowrap; +} + +h1 { + font-size: 4.5rem; /* 72px */ +} +h2 { + font-size: 3rem; + margin-top: 6rem; +} +h3 { + font-size: 1.6rem; + margin-top: 4rem; + margin-bottom: 1rem; +} +h4 { + font-size: 1.2rem; + margin-top: 2rem; + margin-bottom: 1rem; +} +h5 { + font-size: 1.1rem; + margin-top: 2rem; + margin-bottom: 1rem; +} +p { + margin-bottom: 2rem; +} +p, li { + font-size: 1rem; + word-wrap: break-word; +} +.main-paragraph { + font-size: 1.25rem; /* Original 1.5rem */ +} + +hr { + background-color: var(--graylight); + margin-top: 6rem; + height: 1px; + border: none; +} +blockquote { + border-left: 4px solid var(--graylight); + margin-top: 2rem; + margin-left: 2rem; + padding-left: 2rem; +} + +figure { + padding: 0; + margin: 0 0 2rem 0; +} +figcaption { + color: var(--graymedium); +} + +pre, +code { + font-family: Consolas, Menlo, Monaco, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", "Courier New", Courier, monospace; + line-height: 1.5; +} +pre { + 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; + padding: 1em; + margin: .5em 0; + background-color: var(--color-gray-light); + overflow: auto; +} +.highlight-line { + display: block; + padding: 0.125em 1em; + text-decoration: none; /* override del, ins, mark defaults */ + color: inherit; /* override del, ins, mark defaults */ +} + +/* allow highlighting empty lines */ +.highlight-line:empty:before { + content: " "; +} +/* avoid double line breaks when using display: block; */ +.highlight-line + br { + display: none; +} + +.highlight-line-isdir { + color: #b0b0b0; + background-color: #222; +} +.highlight-line-active { + background-color: #444; + background-color: hsla(0, 0%, 27%, .8); +} +.highlight-line-add { + background-color: #45844b; +} +.highlight-line-remove { + background-color: #902f2f; +} + +/* Header */ +.preheader { + background: var(--graydark); + padding: 1rem 1.6rem; + display: flex; + align-items: center; +} +.preheader .home { + font-size: 1em; /* 16px /16 */ + text-decoration: none; + flex: 1; +} +.preheader .home svg { + height: 36px; + width: 116px; + fill: var(--white); +} +.preheader .home :link:not(:hover) { + text-decoration: none; +} +.header-inner { + display: flex; + align-items: center; + max-width: 1400px; + width: 100%; + margin: auto; + position: relative; +} +.header-title { + font-size: 1.4rem; + display: inline; + font-weight: normal; + color: var(--graymedium); + text-decoration: none; + font-weight: 300; +} + +.button { + align-items: center; + background: var(--primary); + color: var(--graydark) !important; + border-radius: 8px; + cursor: pointer; + display: flex; + font-size: 1rem; + justify-content: center; + padding: 0.75rem 1.25rem; /* 12px 20px */ + text-align: center; + text-decoration: none; + transition-property: background; + transition-duration: .1s; + min-height: 3rem; /* 48px */ + line-height: 1; +} +.button:hover { + background: white; +} + + +/* Nav */ +.nav { + display: flex; + list-style: none; + margin: 0 auto; + padding: 0; + align-items: center; +} +.nav-item { + display: inline-block; + margin-left: 1.4rem; + margin-right: 1.4rem; + margin-bottom: 0; + font-size: 1rem; /* original value 1.1rem */ + font-weight: 500; +} +.nav-item a[href]:not(:hover) { + text-decoration: none; +} +.nav-item-active { + font-weight: 700; + text-decoration: underline; +} + +/* Posts list */ +.postlist { + list-style: none; + padding: 0; +} +.postlist-item { + counter-increment: start-from -1; +} +.postlist-item:before { + display: inline-block; + pointer-events: none; + content: "" counter(start-from, decimal-leading-zero) ". "; + line-height: 100%; + text-align: right; +} +.postlist-date, +.postlist-item:before { + font-size: 0.8125em; /* 13px /16 */ + color: var(--graydark); +} +.postlist-date { + word-spacing: -0.5px; +} +.postlist-link { + display: inline-block; + padding: 0.25em 0.1875em; /* 4px 3px /16 */ +} +.postlist-item-active .postlist-link { + font-weight: bold; +} +.tmpl-home .postlist-link { + font-size: 1.1875em; /* 19px /16 */ + font-weight: 700; +} + +.pre-footer { + padding: 2rem; + margin-top: 2rem; + display: flex; + justify-content: center; + border-top: 1px dashed var(--color-gray-medium); +} + +footer { + align-items: center; + background-color: var(--graydark); + display: flex; + flex-direction: column; + padding: 4rem 2rem; + width: 100%; +} +.footer-logo { + display: flex; + flex-direction: column; + color: var(--white); +} +.footer-inside { + display: grid; + grid-template-columns: 1.3fr 1fr 1fr 1fr 1fr; + max-width: 1400px; + width: 100%; +} +.footer-inside .main-logo { + margin-right: auto; +} +.footer-inside .main-logo svg { + fill: var(--white); + height: 50px; + width: 140px; +} +.footer-block { + display: flex; + flex-direction: column; + min-width: 20%; + margin: 1rem 0; +} +.footer-block--title { + color: var(--white); + font-weight: bold; + padding: 0; + list-style: none; +} +.footer-block li { + /* padding: .2rem 0; */ + padding: 0; +} +.footer-block li a { + color: var(--white); + /* font-size: 1.125rem; */ + font-size: 1rem; + text-decoration: none; + transition: color 200ms; +} +.footer-block li a:hover { + color: var(--primary); +} +.footer-text { + align-items: center; + display: flex; +} +.footer-text span { + color: var(--graymedium); +} +.footer-bottom { + max-width: 1400px; + width: 100%; + display: grid; + grid-template-columns: auto 1fr auto; + margin-top: 118px; + margin-bottom: 32px; +} +.footer-icon-list { + display: flex; + flex-direction: row; + fill: var(--white); + gap: 20px; + margin: 0; + height: 32px; +} +.footer-icon-list li { + margin-block-end: 0; + height: 32px; +} +.footer-icon-list a img{ + fill: var(--white); + width: 32px; + height: 32px; +} +.footer-icon-list a img:hover { + fill: var(--graylight); +} +.github-widget { + height: 28px; + display: grid; + grid-template-columns: auto auto; + align-content: center; + justify-content: center; +} +.github-link { + display: flex; + align-items: center; + justify-content: center; + height: 28px; + width: fit-content; + padding: 5px 10px; + font-size: 12px; + line-height: 16px; + border-radius: .25em 0 0 .25em; + font-weight: bold; + background-color: #ebf0f4; + border: 1px solid rgba(31, 35, 40, .15); + background-image: linear-gradient(180deg, #f6f8fa, #ebf0f4 90%); +} +.github-link[href]{ + color: #24292f; + text-decoration: none; +} +.github-link[href]:hover{ + color: var(--primarydark); +} +.widget-icon { + height: 16px; + width: 16px; +} +.github-link svg { + height: 16px; + width: 16px; + fill: #24292f; +} +.social-count { + display: flex; + align-items: center; + justify-content: center; + height: 28px; + width: fit-content; + padding: 5px 10px; + font-size: 12px; + line-height: 16px; + font-weight: bold; + background-color: #fff; + border-radius: 0 .25em .25em 0; + border: 1px solid rgba(31, 35, 40, .15); +} +.social-count[href]{ + color: #24292f; + text-decoration: none; +} +.social-count[href]:hover{ + color: var(--primarydark); +} +/* Tags */ +.post-tag { + display: inline-block; + vertical-align: text-top; + text-transform: uppercase; + font-size: 0.625em; /* 10px /16 */ + padding: 2px 4px; + margin-left: 0.8em; /* 8px /10 */ + background-color: red; + color: var(--white); + border-radius: 0.25em; /* 3px /12 */ + text-decoration: none; +} +a[href].post-tag, +a[href].post-tag:visited { + color: var(--white); +} + +/* Warning */ +.warning { + background-color: #ffc; + padding: 1em 0.625em; /* 16px 10px /16 */ +} +.warning ol:only-child { + margin: 0; +} + +/* Direct Links / Markdown Headers */ +.main-content .direct-link { + font-family: sans-serif; + text-decoration: none; + font-style: normal; + margin-left: 1rem; + position: absolute; + /* top: 0.4rem; */ +} +.main-content a[href].direct-link, +.main-content a[href].direct-link:visited { + color: transparent; + border-bottom: none; +} +.main-content a[href].direct-link:focus, +.main-content a[href].direct-link:focus:visited, +:hover > a[href].direct-link, +:hover > a[href].direct-link:visited { + color: var(--primarydark); +} +:hover > a[href].direct-link:hover { + color: var(--primary); + border-bottom: none; +} + +[id]::before { + visibility: hidden; + content: ''; +} + +:target[id]::before { + content: ''; + display: block; + visibility: hidden; + height: 94px; +} + +/* Particular classes */ + +.media-container { + width: 100%; + display: flex; + justify-content: center; +} +.main-container { + margin: auto; + max-width: 80rem; + min-height: 60vh; +} +.main-container.with-sidebar { + /* display: grid; + grid-template-columns: minmax(100px, 25%) 1fr; */ + display: flex; + flex-wrap: wrap; +} +.main-content { + margin: 0 auto; +} +.main-illus { + text-align: center; +} +.main-illus img { + margin-top: 2rem; + width: 60%; + max-width: 22rem; +} + +.main-content a[href], +.pre-footer a[href], +.contact-block a[href] { + color: var(--graydark); + text-decoration-line: none; + border-bottom: 2px solid var(--primary); +} +.main-content a[href]:hover, +.pre-footer a[href]:hover, +.contact-block a[href]:hover { + border-bottom: 2px solid var(--primarydark); + color: var(--darkest); +} +.main-content.plugins a[href], +.main-content a[href]:has(img), +.main-content a[href]:has(img):hover{ + border-bottom: none; +} + + +.main-container .sidebar { + padding-top: 0; + padding-right: 2rem; + position: sticky; + top: 8rem; + align-self: start; + max-width: 320px; +} +.main-container .sidebar a[href] { + color: var(--darkest); + text-decoration: none; +} +.main-container .sidebar .header { + font-size: 1.6rem; + font-weight: bold; + display: block; + margin-bottom: 2rem; +} +.main-container.with-sidebar .sidebar .header.mobile { + display: none; +} +.main-container .sidebar ul, +.main-container .sidebar ol { + list-style: none; + padding-left: 0; + margin: 0; + padding-top: 0.2rem; +} +.main-container .sidebar #toc > ul ul, +.main-container .sidebar #toc > ul ol { + padding-left: 1rem; +} +/* first level */ +.main-container .sidebar ul li, +.main-container .sidebar ul li a { + font-weight: 600; + padding-top: 1.2rem; + line-height: 1.2; + margin: 0; +} +.main-container .sidebar #toc > ul > li, +.main-container .sidebar #toc > ol > li { + padding-bottom: 1rem; + padding-top: 1rem; +} +/* second level */ +.main-container .sidebar ul li li, +.main-container .sidebar ul li li a { + font-weight: normal; + font-size: 1rem; +} +/* third level */ +.main-container .sidebar .toc li { +} + +.main-container .sidebar #toc { + /* position: fixed; */ + max-width: 300px; + max-height: calc(100vh - 8rem); + overflow: auto; + padding-right: 1rem; +} + +.advice { + background-color: var(--advice); + padding: 2rem; + border-radius: 4px; +} +.hint { + font-size: 0.9rem; + margin-bottom: 2rem; +} + +.search { + align-items: center; + display: flex; + flex-direction: column; + position: absolute; + right: 0; + top: -6px; + z-index: 1; +} +.search input { + border: 1px solid var(--color-gray-medium); + padding: 0.5rem 2rem 0.5rem 0.75rem; + width: 8rem; + transition: width 200ms 200ms; + height: 40px; + border-radius: 8px; +} +.search input::placeholder { + letter-spacing: .5px; +} +.search label { + position: absolute; + color: var(--graymedium); + left: 10px; + top: 7px; + display: none; +} +.search input:focus { + width: 18rem; +} +.search-icon { + position: absolute; + right: 13px; + top: 12px; + fill: var(--color-gray-dark); + pointer-events: none; +} +.search input:focus + .search-icon { + fill: var(--darkest); +} + + +#search-results { + background: white; + border: 1px solid var(--color-gray-medium); + list-style: none; + margin: 0; + padding: 0; + box-shadow: 0 0 10px rgba(68, 68, 68, 0.1); + max-height: 85vh; + overflow: auto; + width: 18rem; +} +#search-results li { + padding: 0; + margin: 0; +} +#search-results a { + display: block; + padding: 1rem; + font-weight: 600; +} +#search-results li:hover { + background-color: var(--advice); +} + +#search-results h3 { + font-size: 1rem; + font-weight: normal; + padding: 0; + margin: 0; +} + +#search-results a { + text-decoration: none; +} + +#no-results-found { + background: white; + border: 1px solid var(--color-gray-medium); + color: var(--color-gray-dark); + list-style: none; + margin: 0; + padding: 1rem 2rem; + box-shadow: 0 0 10px rgba(68, 68, 68, 0.1); + width: 18rem; +} + +#no-results-found p { + margin: 0; +} + +.help-sections { + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 100%; + padding: 0 0 4rem 0; + list-style: none; +} +.help-sections > li { + position: relative; + margin: 1rem; + background-color: #eee; + flex: 47%; + /* box-shadow: 0px 8px 20px rgb(28 56 71 / 5%); */ + border: 1px solid var(--color-gray-medium); + border-radius: 8px; + max-width: 47%; + min-height: 12rem; + background-color: white; +} +.illus { + background-size: auto 75%; + background-position: 0.6rem center; + padding-left: 12rem; + background-repeat: no-repeat; +} +.help-sections > li.no-link.illus-contact { + padding-left: 14.6rem; +} +.illus-userguide { + background-image: url(/img/home-userguide.png); +} +.illus-faq { + background-image: url(/img/home-faq.png); +} +.illus-techguide { + background-image: url(/img/home-techguide.png); +} +.illus-plugins { + background-image: url(/img/home-plugins.png); +} +.illus-contributing { + background-image: url(/img/home-contributing.png); +} +.illus-contact { + background-image: url(/img/home-contact.png); +} +.illus-libraries { + padding-left: 13rem; + background-size: auto 90%; + background-position: 0.6rem center; + background-image: url(/img/contributing-libraries.png); +} +.help-sections > li:hover { + box-shadow: rgb(0 0 0 / 5%) 0px 4px 16px; +} +.help-sections > li > a { + display: block; + padding: 2.6rem 2.2rem; + color: var(--graydark); + text-decoration: none; +} +.help-sections > li h2, +.help-sections > li h3 { + margin-top: 0; + margin-bottom: 0; + padding-top: 0; + line-height: 1.2; + font-size: 2rem; +} +.help-sections > li p { + color: var(--color-gray-dark); + font-size: 1rem; +} +.help-sections > li.no-link { + padding: 2.6rem; +} +.help-sections > li.soon:hover { + border-color: var(--graylight); +} +.help-sections > li.soon h2, +.help-sections > li.soon h3 { + opacity: 0.4; +} +.help-sections > li.github { + min-height: auto; + border: none; + border-radius: 0; + box-shadow: none; + border-top: 1px dashed var(--color-gray-medium); + border-bottom: 1px dashed var(--color-gray-medium); + margin-top: 4rem; + flex-basis: 100%; + max-width: 100%; +} +.help-sections > li.github a:hover h2, +.help-sections > li.github a:hover h3 { + color: var(--primarydark); + transition: color 50ms ease-in-out; +} +.help-sections > li.github a:hover svg { + fill: var(--primarydark); + transition: fill 50ms ease-in-out; +} +.help-sections > li.github a { + display: flex; + align-items: center; +} +.help-sections > li.github .content { + margin-left: 2rem; +} +.help-sections > li.github svg { + width: 72px; + height: 72px; +} +.contact-block { + text-align: center; + padding-top: 12rem; + margin-bottom: 6rem; + background-size: auto 75%; + background-position: center top; + background-repeat: no-repeat; + background-image: url(/img/home-contact.png); +} +.contact-block p, +.contact-block h2, +.contact-block h3 { + max-width: initial; +} + +.main-container h1 { + margin-top: 0rem; + padding-top: 5rem; +} +.main-container h2 { + margin-top: 0rem; + padding-top: 4rem; +} +.main-container h3 { + margin-top: 0rem; + padding-top: 2rem; +} + + +.intro-sections { + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 100%; + padding: 0 0 4rem 0; + list-style: none; + gap: 2rem; + margin: 2rem 0; +} +.intro-sections li { + position: relative; + margin: 0; + flex: 33%; + background-color: white; + /* box-shadow: 0px 4px 10px rgb(28 56 71 / 5%); */ + border: 1px solid var(--color-gray-medium); + border-radius: 8px; +} +.intro-sections li:hover { + box-shadow: rgb(0 0 0 / 5%) 0px 4px 16px; +} +.intro-sections li > a[href] { + display: block; + padding: 2.6rem; + color: var(--color-gray-dark); + text-decoration: none; + border-bottom: none; +} +.intro-sections li > a[href]:hover { + border-bottom: none; +} +.intro-sections li h2, +.intro-sections li h3 { + margin-top: 0; + padding-top: 0; + line-height: 1.2; + font-size: 2rem; + margin-bottom: 1rem; +} +.intro-sections li p { + font-size: 1.2rem; + line-height: 1.45; +} + +.plugins .illus-getting-started { + background-image: url(/img/plugins/getting_started.png); +} +.plugins .illus-create-plugin { + background-image: url(/img/plugins/create_plugin.png); +} +.plugins .illus-deployment { + background-image: url(/img/plugins/deployment.png); +} +.plugins .illus-api { + background-image: url(/img/plugins/api.png); +} +.plugins .illus-examples { + background-image: url(/img/plugins/examples.png); +} +.plugins .illus-faq { + background-image: url(/img/plugins/faqs.png); +} + +table, tr, th, td { + color: var(--graydark); + padding: 1rem; + vertical-align: top; + border: 1px solid var(--graylight); + border-collapse: collapse; + /* font-size: 1.10rem; */ + font-size: 1rem; +} +table { + background-color: white; + box-shadow: 0px 4px 20px rgb(28 56 71 / 5%); +} +/* th { + background: rgba(0,0,0,.06); +} */ +table>thead>tr:first-child>th { + font-weight: bold; + background: none repeat scroll 0 0 #ededed; +} +table tr:nth-child(odd) { + background: rgba(0,0,0,.02); +} + +@media (max-width: 1114px) { + .help-sections > li { + max-width: none; + } +} + +@media (max-width: 1024px) { + hr { + margin-top: 5rem; + } + .header-title { + display: none; + } + header { + padding: 1rem; + position: relative; + } + header .nav-item:first-child { + display: none; + } + .header-inner { + display: block; + } + .search { + position: relative; + width: 100%; + margin-top: 2rem; + } + .search input { + width: 100%; + font-size: 1rem; + } + .search input:focus { + /* width: calc(100vw - 1.4rem); */ + width: 100%; + } + .search input:focus + .search-icon { + display: none; + } + + #search-results { + width: 100%; + } + + #no-results-found { + width: 100%; + } + + + .preheader { + padding: 1rem; + } + .preheader .button { + width: 100%; + } + .home { + display: none; + } + .nav { + margin-left: 0; + } + .main-container.with-sidebar { + display: block; + } + .main-container.with-sidebar .sidebar { + padding: 0; + top: 0; + position: relative; + z-index: 1; + } + .main-container.with-sidebar .sidebar .header { + display: none; + } + .main-container.with-sidebar .sidebar .header.mobile { + display: block; + border: 1px solid var(--graylight); + border-radius: 4px; + padding: 1rem 1rem; + margin: 0; + position: relative; + background: white; + } + .main-container.with-sidebar .sidebar .header:before { + content: ""; + position: absolute; + visibility: visible; + z-index: 1; + width: 16px; + height: 16px; + top: 26px; + right: 20px; + background-image: url(/img/caret-down.svg); + background-size: 16px 16px; + background-repeat: no-repeat; + transition: transform 200ms ease-in-out; + } + + .main-container.with-sidebar .sidebar #toc { + position: relative; + margin-top: 0; + max-width: none; + z-index: 2; + padding-right: 0; + } + .main-container.with-sidebar .sidebar #toc > ul { + background: white; + border: 1px solid var(--graylight); + color: var(--graymedium); + list-style: none; + margin: 0; + padding: 2rem; + box-shadow: 2px 0 10px rgba(68, 68, 68, 0.1); + display: none; + } + .main-container.with-sidebar .sidebar #toc.open > ul{ + display: block; + } + .main-container.with-sidebar .sidebar .open .header:before { + transform: rotate(180deg); + } + .footer-inside { + flex-direction: column; + } + :target[id]::before { + display: inline; + } + +} +@media (max-width: 600px) { + hr { + margin-top: 4rem; + } + .main-container h1 { + padding-top: 3rem; + font-size: 3rem; + } + .main-container h2 { + padding-top: 2rem; + font-size: 2rem; + } + .main-container h3 { + padding-top: 1.4rem; + font-size: 1.4rem; + } + header .nav-item { + margin-left: 0.6rem; + margin-right: 0.6rem; + font-size: 1rem; + } + .main-container.with-sidebar .sidebar #toc { + padding: 0; + margin: 0; + } + .help-sections { + flex-direction: column; + } + .help-sections > li { + margin: 0 0 1rem 0; + } + .help-sections > li > a, + .help-sections .illus, + .help-sections > li.no-link.illus-contact { + padding: 1.4rem; + min-height: auto; + } + .help-sections .illus { + background-image: none; + } + .intro-sections .illus { + background-image: none; + padding-left: 0; +} + +} diff --git a/docs/css/prism-base16-monokai.dark.css b/docs/css/prism-base16-monokai.dark.css new file mode 100644 index 000000000..01e802439 --- /dev/null +++ b/docs/css/prism-base16-monokai.dark.css @@ -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; +} diff --git a/docs/css/prism.css b/docs/css/prism.css new file mode 100644 index 000000000..01819a853 --- /dev/null +++ b/docs/css/prism.css @@ -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; +} diff --git a/docs/feed/feed.njk b/docs/feed/feed.njk new file mode 100755 index 000000000..161fc9362 --- /dev/null +++ b/docs/feed/feed.njk @@ -0,0 +1,29 @@ +--- +# Metadata comes from _data/metadata.json +permalink: "{{ metadata.feed.path }}" +eleventyExcludeFromCollections: true +--- + +Everything you need to know about how Penpot works.
+ +How to report bugs, add translations and more.
+ +Installation, configuration and architecture.
+ +All about Penpot plugins.
+ +Get quick answers to usual questions about "why and how" Penpot.
+ +Penpot is open source. Get involved on the community or contribute to the project.
+Write us at support@penpot.app or join our community.
+PenpotShape
becomes Shape
; PenpotFile
becomes File
, and so on. Check the [API documentation](https://penpot-plugins-api-doc.pages.dev/) for more details.
+- Changes on the penpot.on
and penpot.off
methods.
+Previously you had to send the original callback to the off method in order to remove an event listener. Now, penpot.on
will return an *id* that you can pass to the penpot.off
method in order to remove the listener.
+
+Previously:
+```js
+penpot.on(‘pagechange’, myListener); // Register an event listener
+penpot.off(‘pagechange’, myListener); // Remove previously registered listener
+```
+
+Now:
+```js
+const id = penpot.on(‘pagechange’, myListener);
+penpot.off(id);
+```
+
+We’ve deprecated the old behavior in favor of the new one, this means that the behavior will work in the next version, but will be removed further down the line.
+
+- Change some names to better align with the names in Penpot's UI.
+ - type frame
is now board
:
+ - PenpotFrame
is now Board
+ - penpot.createFrame
changed to penpot.createBoard
+ - shape.frameX
/ shape.frameY
changed toshape.boardX
/ shape.boardY
+ - PenpotFrameGuideX
now GuideX
+ - typerect
is rectangle
+ - PenpotRectangle
is now Rectangle
+ - typecircle
isellipse
+ - PenpotCircle
is now Ellipse
+ - penpot.createCircle
changed to penpot.createEllipse
+ - typebool
isboolean
+ - PenpotBool
is now Boolean
+- Removed the following methods
+ - getPage
, you can use now the property currentPage
+ - getFile
, you can use now the property currentFile
+ - getTheme
, you can use now the property theme
+ - getSelected
, you can use the property selection
+ - getSelectedShapes
, you can use the property selection
+
+### +In case you'll use any of these templates, you can skip to step 2.7 +
+ +2. Creating a plugin from scratch using a major framework. + + Follow to the next section to understand how to bootstrap a new plugin using one of the three major JavaScript frameworks. + +## 2.1. Step 1. Create a project + +Create your own app with the framework of your choice. See examples for each framework here + +| Framework | Command | Version\* | +| --------- | ----------------------------------------------------------- | --------- | +| Angular | ng new plugin-name | 18.0.0 | +| React | npm create vite@latest plugin-name -- --template react-ts | 18.2.0 | +| Vue | npm create vue@latest | 3.4.21 | + +_\*: version we used in the examples._ + +## 2.2. Step 2. Install Penpot libraries + +There are two libraries that can help you with your plugin's development. They are@penpot/plugin-styles
and @penpot/plugin-types
.
+
+### Plugin styles
+
+@penpot/plugin-styles
contains styles to help build the UI for Penpot plugins. To check the styles go to Plugin styles.
+
+```bash
+npm install @penpot/plugin-styles
+```
+
+You can add the styles to your global CSS file.
+
+```css
+@import "@penpot/plugin-styles/styles.css";
+```
+
+### Plugin types
+
+@penpot/plugin-types
contains the typings for the Penpot Plugin API.
+
+```bash
+npm install @penpot/plugin-types
+```
+
+If you're using typescript, don't forget to add @penpot/plugin-types
to your typings in your tsconfig.json
.
+
+```json
+{
+ "compilerOptions": {
+ [...]
+ "typeRoots": ["./node_modules/@types", "./node_modules/@penpot"],
+ "types": ["plugin-types"],
+ }
+}
+```
+
+## 2.3. Step 3. Create a plugin file
+
+A plugin file is needed to interact with Penpot and its API. You can use either javascript or typescript and it can be placed wherever you like. It normally goes alongside the main files inside the src/
folder. We highly recommend labeling your creation as plugin.js
or plugin.ts
, depending upon your preferred language.
+
+You can start with something like this:
+
+```ts
+penpot.ui.open("Plugin name", "", {
+ width: 500,
+ height: 600,
+});
+```
+
+The sizing values are optional. By default, the plugin will open with a size of 285x540 pixels.
+
+## 2.4. Step 4. Connect API and plugin interface
+
+To enable interaction between your plugin and the Penpot API, you'll need to implement message-based communication using JavaScript events. This communication occurs between the main Penpot application and your plugin, which runs in an iframe. The window
object facilitates this communication by sending and receiving messages between the two.
+
+### Sending messages from Penpot to your plugin
+
+To send a message from the Penpot API to your plugin interface, use the following command in plugin.ts
:
+
+```js
+penpot.ui.sendMessage(message);
+```
+
+Here, message
can be any data or instruction you want to pass to your plugin. This message is dispatched from Penpot and is received by your plugin's iframe.
+
+### Receiving Messages in Your Plugin Interface
+
+Your plugin can capture incoming messages from Penpot using the window
object's message
event. To do this, set up an event listener in your plugin like this:
+
+```js
+window.addEventListener("message", (event) => {
+ // Handle the incoming message
+ console.log(event.data);
+});
+```
+
+Theevent.data
object contains the message sent from Penpot. You can use this data to update your plugin's interface or trigger specific actions within your plugin.
+
+### Two-Way Communication
+
+This setup allows for two-way communication between Penpot and your plugin. Penpot can send messages to your plugin, and your plugin can respond or send messages back to Penpot using the samepostMessage
API. For example:
+
+```js
+// Sending a message back to Penpot from your plugin
+parent.postMessage(responseMessage, targetOrigin);
+```
+
+-responseMessage
is the data you want to send back to Penpot.
+-targetOrigin
should be the origin of the Penpot application to ensure messages are only sent to the intended recipient. You can use'*'
to allow all.
+
+### Summary
+
+By using these message-based events, any data retrieved through the Penpot API can be communicated to and from your plugin interface seamlessly.
+
+For more detailed information, refer to the [Penpot Plugins API Documentation](https://penpot-plugins-api-doc.pages.dev/).
+
+## 2.5. Step 5. Build the plugin file
+
+This step is only for local serving. +For a detailed guide about building and deploying you can check the documentation at Deployment
+You can skip this step if working exclusively with JavaScript by simply moving plugin.js
to your public/
directory.
plugin.ts
file is somewhere in the src/
folder, and you can't access it through http:\/\/localhost:XXXX/plugin.js
.
+
+You can achieve this through multiple solutions, but we offer two simple ways of doing so. Of course, you can come up with your own.
+
+#### Vite
+
+If you're using Vite you can simply edit the configuration file and add the build to your vite.config.ts
.
+
+```ts
+export default defineConfig({
+[...]
+ build: {
+ rollupOptions: {
+ input: {
+ plugin: "src/plugin.ts",
+ index: "./index.html",
+ },
+ output: {
+ entryFileNames: "[name].js",
+ },
+ },
+ },
+ preview: {
+ port: XXXX,
+ },
+});
+```
+
+And then add the following scripts to your package.json
:
+
+```json
+"scripts": {
+ "dev": "vite build --watch & vite preview",
+ "build": "tsc && vite build",
+ [...]
+}
+```
+
+#### Esbuild
+
+```bash
+$ npm i -D esbuild # install as dev dependency
+```
+
+Now you can use esbuild to parse and move your plugin file.
+
+```bash
+esbuild your-folder/plugin.ts --minify --outfile=your-folder/public/plugin.js
+```
+
+You can add it to your package.json
scripts so you don't need to manually re-run the build:
+
+```json
+ "scripts": {
+ "start": "npm run build:plugin && ng serve",
+ "build:plugin": "esbuild your-folder/plugin.ts --minify --outfile=your-folder/public/plugin.js"
+ [...]
+ },
+```
+
+Keep in mind that you'll need to build again your plugin file if you modify it mid-serve.
+
+## 2.6. Step 6. Configure the manifest file
+
+Now that everything is in place you need a manifest.json
file to provide Penpot with your plugin data. Remember to make it reachable by placing it in the public/
folder.
+
+```json
+{
+ "name": "Plugin name",
+ "description": "Plugin description",
+ "code": "/plugin.js",
+ "icon": "/icon.png",
+ "permissions": [
+ "content:read",
+ "content:write",
+ "library:read",
+ "library:write",
+ "user:read",
+ "comment:read",
+ "comment:write",
+ "allow:downloads"
+ ]
+}
+```
+
+### Icon
+
+The plugin icon must be an image file. All image formats are valid, so you can use whichever format works best for your needs. Although there is no specific size requirement, it is recommended that the icon be 56x56 pixels in order to ensure its optimal appearance across all devices.
+
+### Types of permissions
+
+- content:read
: Allows reading of content-related data. Grants read access to all endpoints and operations dealing with content. Typical use cases: viewing shapes, pages, or other design elements in a project; accessing the properties and settings of content within the application.
+
+- content:write
: Allows writing or modifying content-related data. Grants write access to all endpoints and operations dealing with content modifications, except those marked as read-only. Typical use cases: adding, updating, or deleting shapes and elements in a design; uploading media or other assets to the project.
+
+- user:read
: Allows reading of user-related data. Grants read access to all endpoints and operations dealing with user data. Typical use cases: viewing user profiles and their associated information or listing active users in a particular context or project.
+
+- library:read
: Allows reading of library-related data and assets. Grants read access to all endpoints and operations dealing with the library context. Typical use cases: accessing shared design elements and components from a library or viewing the details and properties of library assets.
+
+- library:write
: Allows writing or modifying library-related data and assets. Grants write access to all endpoints and operations dealing with library modifications. Typical use cases: adding new components or assets to the library or updating or removing existing library elements.
+
+- comment:read
: Allows reading of comment-related data. Grants read access to all endpoints and operations dealing with comments.
+Typical use cases: viewing comments on pages; accessing feedback or annotations provided by collaborators in the project.
+
+- comment:write
: Allows writing or modifying comment-related data. Grants write access to all endpoints and operations dealing with creating, replying, or deleting comments.
+Typical use cases: adding new comments to pages; deleting existing comments; replying to comments within the project's context.
+
+- allow:downloads
: Allows downloading of the project file. Grants access to endpoints and operations that enable the downloading of the entire project file.
+Typical use cases: downloading the full project file for backup or sharing.
+
+_Note: Write permissions automatically includes its corresponding read permission (e.g.,content:write
includes content:read
) because reading is required to perform write or modification actions._
+
+## 2.7. Step 7. Load the Plugin in Penpot
+
+Serving an application: This refers to making your application accessible over a network, typically for testing or development purposes.
When using a tool like live-server, a local web server is created on your machine, which serves your application files over HTTP. Most modern frameworks offer their own methods for serving applications, and there are build tools like Vite and Webpack that can handle this process as well.
https:\/\/penpot.app/
. However, be mindful of potential CORS (Cross-Origin Resource Sharing) issues. To avoid these, ensure your plugin includes the appropriate cross-origin headers. (Find more info about this at the Deployment step)
+
+Serving your plugin will generate a URL that looks something like http:\/\/localhost:XXXX
, where XXXX
represents the port number on which the plugin is served. Ensure that both http:\/\/localhost:XXXX/manifest.json
and http:\/\/localhost:XXXX/plugin.js
are accessible. If these files are inside a specific folder, the URL should be adjusted accordingly (e.g., http:\/\/localhost:XXXX/folder/manifest.json
).
+
+Once your plugin is served you are ready to load it into Penpot. You can use the shortcut Ctrl + Alt + P
to open the Plugin Manager modal. In this modal, provide the URL to your plugin's manifest file (e.g., http:\/\/localhost:XXXX/manifest.json
) for installation. If everything is set up correctly, the plugin will be installed, and you can launch it whenever needed.
+
+You can also open the Plugin manager modal via:
+
+- Menu
+
+
+- Toolbar
+
diff --git a/docs/plugins/deployment.md b/docs/plugins/deployment.md
new file mode 100644
index 000000000..5b8951dd6
--- /dev/null
+++ b/docs/plugins/deployment.md
@@ -0,0 +1,218 @@
+---
+layout: layouts/plugins.njk
+title: 3. Deployment
+---
+
+# Deployment
+
+When it comes to deploying your plugin there are several platforms to choose from. Each platform has its unique features and benefits, so the choice depends on you.
+
+In this guide you will found some options for static sites that have free plans.
+
+## 3.1. Building your project
+
+The building may vary between frameworks but if you had previously configured your scripts in package.json
, npm run build
should work.
+
+The resulting build should be located somewhere in the dist/
folder, maybe somewhere else if you have configured so.
+
+Be wary that some framework's builders can add additional folders like apps/project-name/
, project-name/
or browser/
.
+
+Examples:
+
+![Vue dist example](/img/plugins/vue_dist.png)
+![Angular dist example](/img/plugins/angular_dist.png)
+
+## 3.2. Netlify
+
+### Create an account
+
+You need a Netlify account if you don't already have one. You can sign up with Github, GItlab, BItbucket or via email and password.
+
+### CORS issues
+
+To avoid these issues you can add a _headers
file to your plugin. Place it in the public/
folder or alongside the main files.
+
+```js
+/*
+ Access-Control-Allow-Origin: *
+```
+
+### Connect to Git
+
+Netlify allows you to import an existing project from GitHub, GitLab, Bitbucket or Azure DevOps.
+
+- Configure builds.
+
+#### How to deploy
+
+
+
+1. Go to Start and connect with your repository. Allow Netlify to be installed in either all your projects or just the selected ones.
+
+![Netlify git installation](/img/plugins/install_netlify.png)
+
+2. Configure your build settings. Netlify auto-detects your framework and offers a basic configuration. This is usually enough.
+
+![Netlify git configuration](/img/plugins/build_settings.png)
+
+3. Deploy your plugin.
+
+### Drag and drop
+
+Netlify offers a simple drag and drop method. Check Netlify Drop.
+
+#### How to deploy
+
+
+
+1. Build your project
+
+```bash
+npm run build
+```
+
+2. Go to Netlify Drop.
+
+3. Drag and drop the build folder into Netlify Sites. Dropping the whole dist may not work, you should drop the folder where the main files are located.
+
+4. Done!
+
+## 3.3. Cloudflare
+
+### Create an account
+
+You need a Cloudflare account if you don't already have one. You can sign up via email and password.
+
+### CORS issues
+
+To avoid these issues you can add a _headers
file to your plugin. Place it in the public/
folder or alongside the main files.
+
+```js
+/*
+ Access-Control-Allow-Origin: *
+```
+
+### Connect to Git
+
+Cloudflare allows you to import an existing project from GitHub or GitLab.
+
+- Git integration
+
+#### How to deploy
+
+
+
+
+1. Go to Workers & Pages > Create > Page > Connect to git
+
+2. Select a repository. Allow Cloudflare to be installed in either all your projects or just the selected ones.
+
+![Cloudflare git installation](/img/plugins/install_cloudflare.png)
+
+4. Configure your build settings.
+
+![Cloudflare git configuration](/img/plugins/cf_build_settings.png)
+
+5. Save and deploy.
+
+### Direct upload
+
+You can directly upload your plugin folder.
+
+- Direct upload
+
+#### How to deploy
+
+
+
+1. Build your plugin.
+
+```bash
+npm run build
+```
+
+2. Go to Workers & Pages > Create > Page > Upload assets.
+
+3. Create a new page.
+
+![Cloudflare new page](/img/plugins/cf_new_page.png)
+
+4. Upload your plugin files. You can drag and drop or select the folder.
+
+![Cloudflare page upload files](/img/plugins/cf_upload_files.png)
+
+5. Deploy site.
+
+## 3.4. Surge
+
+Surge provides a CLI tool for easy deployment.
+
+- Getting Started.
+
+### CORS issues
+
+To avoid these issues you can add a CORS
file to your plugin. Place it in the public/
folder or alongside the main files.
+
+The CORS
can contain a *
for any domain, or a list of specific domains.
+
+Check Enabling Cross-Origin Resources sharing.
+
+### How to deploy
+
+1. Install surge CLI globally and log into your account or create one.
+
+```bash
+npm install --global surge
+surge login
+# or
+surge signup
+```
+
+2. Create a CORS file to allow all sites.
+
+```bash
+echo '*' > public/CORS
+```
+
+3. Build your project.
+
+```bash
+npm run build
+```
+
+4. Start surge deployment
+
+```bash
+surge
+
+# Your plugin build folder
+project: /home/user/example-plugin/dist/
+
+# your domain, surge offers a free .surge.sh domain and free ssl
+domain: https://example-plugin-penpot.surge.sh
+
+upload: [====================] 100% eta: 0.0s (10 files, 305761 bytes)
+CDN: [====================] 100%
+encryption: *.surge.sh, surge.sh (346 days)
+IP: XXX.XXX.XXX.XXX
+
+Success! - Published to example-plugin-penpot.surge.sh
+```
+
+5. Done!
diff --git a/docs/plugins/examples-templates.md b/docs/plugins/examples-templates.md
new file mode 100644
index 000000000..c5c1105f4
--- /dev/null
+++ b/docs/plugins/examples-templates.md
@@ -0,0 +1,128 @@
+---
+layout: layouts/plugins.njk
+title: 5. Examples and templates
+---
+
+# Examples and templates
+
+## 5.1. Examples
+
+We've put together a handy list of some of the most common actions you can perform in penpot, and we've also included a helpful example for each one. We hope this makes it easier for you to create your plugins!
+
++If you just want to get to the examples, you can go straight to the repository here +
+ +### Create a shape + +One of the most basic things you can do in design is create a shape. It's really simple. In this example, we'll show you how to make a rectangle, but you can use the same principles to make other shapes. This makes it easy for you to add different shapes to your design, which is great for building more complex elements and layouts. + +```js +// just replace +penpot.createRectangle(); + +// for one of these other options: +penpot.createEllipse(); +penpot.createPath(); +penpot.createBoard(); +``` + +Shape example + +### Create a text + +You'll learn how to insert text and explore a variety of styling options to customize it to your exact preferences. You'll discover how to adjust font, size, color, and more to create visually engaging text elements. You can also apply multiple styles within a single text string by styling different sections individually, giving you even greater control over your text design and allowing for creative, dynamic typographic effects. + +Text example + +### Group and ungroup shapes + +It's really important to keep your layers organized if you want to keep your workflow clean and efficient. Grouping shapes together makes it much easier to manage and manipulate multiple elements as a single unit. This not only makes your design process much more streamlined, but it also helps you maintain a structured and organized layer hierarchy. When you need to make individual adjustments, you can easily ungroup these shapes, which gives you flexibility while keeping your workspace tidy and well-organized. + +Group and ungroup example + +### Create flex layout + +Flex Layout makes it simple to create designs that adapt and respond to different screens and devices. It automatically adjusts the positioning and sizing of content and containers, so you can resize, align, and distribute elements without any manual intervention. + +Flex layout example + +### Create grid layout + +Grid Layout lets you create flexible designs that automatically adapt to different screen sizes and content changes. You can easily resize, fit, and fill content and containers with this feature, so you don't have to make manual adjustments and you get a seamless, responsive design experience. + +Grid layout example + +### Create a component + +Using components is a great way to reuse objects or groups of objects, making sure everything looks the same and works well across your designs. This example shows you how to create a component, which lets you make your workflow easier by defining reusable design elements. + +
+Just a friendly reminder that it's important to have the library permissions in the manifest.json
.
+
+Just a friendly reminder that it's important to have the library permissions in the manifest.json
.
+
styles.css
of the example.
+
+Theme example
+
+### Use of third party API
+
+Often, we want to make our plugins better by adding external libraries, new features, and functionalities. Here's an example of how to use the Picsum library. It shows how you can use third-party APIs to make your plugin development better. Use this as a reference to explore how you can add external resources to your projects.
+
+Third party API example
+
+### Interactive prototype
+
+With the ability to create an interactive prototype, you can turn your design from a static layout into a dynamic, navigable experience. This lets users interact with the design in a more seamless way and gives them a better preview of the final product.
+
+Interactive prototype example
+
+### Add ruler guides
+
+Ruler guides are great for aligning elements exactly where you want them. Check out how to add horizontal and vertical guides to your page or boards. This makes it easier to keep your design looking the same from one place to the next.
+
+Ruler guides example
+
+### Create a comment
+
+Comments are a great way for designers and team members to give each other feedback on a design right away. This example shows how to add comments to specific parts of a design, which makes it easier for everyone to work together and improve their workflow.
+
+
+Just a friendly reminder that it's important to have the comment permissions in the manifest.json
.
+
https:\/\/create-palette-penpot-plugin.pages.dev/assets/manifest.json
or check the code here
+
+### Can I create components?
+
+Yes, it is possible to create components using:
+
+```js
+createComponent(shapes: Shape[]): LibraryComponent;
+```
+
+Take a look at the Penpot Library methods in the API documentation or this simple example.
+
+### Is there a place where I can share my plugin?
+
+You will be able to share your plugin with the Penpot community. In the future, we plan to create a place where we will publish the plugins we know about, but this is still something we have to define.
+
+### My plugin works on my local machine, but I couldn’t install it on Penpot. What could be the problem?
+
+The url you that you need to provide in the plugin manager should look like this: https:\/\/yourdomain.com/assents/manifest.json
+
+### Where can I get support if I find a bug or an unexpected behavior?
+
+You can report a problem or request support at support@penpot.app.
diff --git a/docs/plugins/getting-started.md b/docs/plugins/getting-started.md
new file mode 100644
index 000000000..89f42f61c
--- /dev/null
+++ b/docs/plugins/getting-started.md
@@ -0,0 +1,199 @@
+---
+layout: layouts/plugins.njk
+title: 1. Getting started
+---
+
+# Getting started
+
+## 1.1. Introduction
+
+Welcome to Penpot Plugins!
+
+Plugins are the perfect tool to easily extend Penpot's functionality, you can automate repetitive tasks, add new features and much more.
+
+Plugins can be created with your favorite framework or with not framework at all. Feel free to use whatever you want because Plugins are independent from Penpot's code and therefore you don't need any extra knowledge.
+
+The plugins will be hosted outside Penpot, and each creator need to host theirs.
+
+## 1.2. Pre-requisites
+
+- Basic experience with Penpot.
+- Basic experience with JavaScript, HTML and CSS.
+- Node and npm (How to install Node.js).
+- A text editor, ideally an IDE like Visual Studio Code or similar.
+
+Nice to have:
+
+- Git basic knowledge.
+- A Github account or a similar service to host and share your plugin code.
+- TypeScript basic knowledge.
+- Experience with any front framework (angular, react, vue...) for complex user interfaces.
+- A hosting service of your choice for plugin's deployment.
+
+## 1.3. Installation
+
+With the plugins system enabled, you need to go to any project to open the plugin manager.
+
+You can open the plugin manager in any project via:
+
+##### Shortcut
+
+| Linux and Windows | macOS |
+| ----------------------------------------- | -------------------------------------- |
+| CtrlAltP | ⌘AltP |
+
+##### Menu
+
+
+
+##### Toolbar
+
+
+
+The plugin manager looks like this:
+
+![Penpot's plugin manager](/img/plugins/plugin-manager.png)
+
+You need to provide the plugin's manifest URL for the installation. If there are no issues the plugin will be installed and then you would be able to open it whenever you like.
+
+### Examples
+
+| Name | URL |
+| ------------- | ------------------------------------------------------------------- |
+| Lorem Ipsum | https://lorem-ipsum-penpot-plugin.pages.dev/assets/manifest.json |
+| Contrast | https://contrast-penpot-plugin.pages.dev/assets/manifest.json |
+| Feather icons | https://icons-penpot-plugin.pages.dev/assets/manifest.json |
+| Tables | https://table-penpot-plugin.pages.dev/assets/manifest.json |
+| Color palette | https://create-palette-penpot-plugin.pages.dev/assets/manifest.json |
+| Rename layers | https://rename-layers-penpot-plugin.pages.dev/assets/manifest.json |
+
+## 1.4. Plugin's basics
+
+### How does it work?
+
+Penpot's plugin system allows you to add new features to the platform through independent modules called plugins. These plugins run separately from the main Penpot app, inside iframes, which are like small, isolated browser windows within the app.
+
+Plugins communicate with Penpot by sending and receiving messages through the iframe.
+
+@startuml
+
+skinparam state {
+BackgroundColor transparent
+BorderColor black
+ArrowColor black
+}
+
+Penpot_App -down-> WebComponent
+WebComponent -up-> Penpot_App : Write / Read
+WebComponent : - Create API
+WebComponent : - Create sandbox (ses)
+WebComponent : - Read plugin manifest
+WebComponent : - Run plugin code
+WebComponent -right-> Plugin_code
+Plugin_code : penpot.ui.open('Example plugin', '');
+Plugin_code :
+Plugin_code : penpot.ui.onMessage((message) => {
+Plugin_code : console.log('iframe message', message);
+Plugin_code : });
+Plugin_code -right-> Iframe
+Iframe -left-> Plugin_code
+Iframe :
+Iframe :
+Iframe :
+
+CSS_Library -down-> Iframe
+External_API -up-> Iframe
+Iframe -down-> External_API
+
+@enduml
+
+### What is manifest.json file?
+
+The manifest.json
file contains the basic information about the plugin. It defines the plugin's name, description, the main code file, and the permissions it requires. The structure of the manifest.json
file looks like this:
+
+```json
+{
+ "name": "Your plugin name",
+ "description": "Your plugin description",
+ "code": "plugin.js",
+ "icon": "Your icon",
+ "permissions": [
+ "content:read",
+ "content:write",
+ "library:read",
+ "library:write",
+ "user:read",
+ "comment:read",
+ "comment:write",
+ "allow:downloads"
+ ]
+}
+```
+
+#### Properties
+
+- **Name and description**: your plugin's basic information, which will be displayed in the plugin manager modal.
+- **Code**: your plugin's file location. It needs to be compiled to JavaScript and reachable.
+- **Icon**: your plugin's icon, which will be also displayed in the plugin manager modal. It'll be a
tag so you can use whichever image format works better for you. **It's recommended to use a 56x56 pixel icon for the best appearance on all devices**.
+- **Permissions**: your plugin's permissions, which allow access to different parts of the Penpot API.
+
+#### Types of permissions
+
+- content:read
: Allows reading of content-related data. Grants read access to all endpoints and operations dealing with content. Typical use cases: viewing shapes, pages, or other design elements in a project; accessing the properties and settings of content within the application.
+
+- content:write
: Allows writing or modifying content-related data. Grants write access to all endpoints and operations dealing with content modifications, except those marked as read-only. Typical use cases: adding, updating, or deleting shapes and elements in a design; uploading media or other assets to the project.
+
+- user:read
: Allows reading of user-related data. Grants read access to all endpoints and operations dealing with user data. Typical use cases: viewing user profiles and their associated information or listing active users in a particular context or project.
+
+- library:read
: Allows reading of library-related data and assets. Grants read access to all endpoints and operations dealing with the library context. Typical use cases: accessing shared design elements and components from a library or viewing the details and properties of library assets.
+
+- library:write
: Allows writing or modifying library-related data and assets. Grants write access to all endpoints and operations dealing with library modifications. Typical use cases: adding new components or assets to the library or updating or removing existing library elements.
+
+- comment:read
: Allows reading of comment-related data. Grants read access to all endpoints and operations dealing with comments.
+Typical use cases: viewing comments on pages; accessing feedback or annotations provided by collaborators in the project.
+
+- comment:write
: Allows writing or modifying comment-related data. Grants write access to all endpoints and operations dealing with creating, replying, or deleting comments.
+Typical use cases: adding new comments to pages; deleting existing comments; replying to comments within the project's context.
+
+- allow:downloads
: Allows downloading of the project file. Grants access to endpoints and operations that enable the downloading of the entire project file.
+Typical use cases: downloading the full project file for backup or sharing.
+
+_Note: Write permissions automatically includes its corresponding read permission (e.g., content:write
includes content:read
) because reading is required to perform write or modification actions._
+
+### What are plugin.ts and plugin.js files?
+
+The plugin.ts
file is where you write code to interact with the Penpot API using TypeScript. This file is then compiled into plugin.js
which is the final JavaScript code that runs the plugin. You don't write plugin.js
directly; it's generated from the plugin.ts
file.
+
++This is also the only file where you can use the Penpot object. Do not try to use the Penpot object in your plugin interface scripts. +
+ +You can check some samples in: + +- Penpot plugin samples. + +### What is TypeScript? + +You may have noticed that we're using TypeScript in our plugin files, but what is it, and why? + +TypeScript is like JavaScript with extra rules. These rules help you catch mistakes early, before you run your code. It makes your code more reliable and easier to manage, especially in big projects. + +We're using TypeScript to make working with the Penpot API easier, as it provides autocompletion and instant access to documentation. However, even with TypeScript’s powerful features, you'll still need to include the@penpot/plugin-types
npm package, which contains the typings for the Penpot Plugin API. This ensures that TypeScript can fully understand and work with the API.
+
+![plugin-types example](/img/plugins/plugint-types-example.gif)
+
+You can install the package in any project with npm install @penpot/plugin-types
. You can check the details in [@penpot/plugin-types package](https://www.npmjs.com/package/@penpot/plugin-types).
diff --git a/docs/plugins/index.njk b/docs/plugins/index.njk
new file mode 100644
index 000000000..3390db4b3
--- /dev/null
+++ b/docs/plugins/index.njk
@@ -0,0 +1,48 @@
+---
+layout: layouts/plugins-home.njk
+title: Plugins
+eleventyNavigation:
+ key: Plugins
+ order: 5
+---
+
+Learn the basics, including introduction, pre-requisites, installation, and plugin fundamentals.
+ +How to report bugs, add translations and more.
+ +Discover different methods and steps to deploy your plugin.
+ +Access detailed documentation on the plugins API for integration and development.
+ +Find examples and starter templates in different frameworks.
+ +Get quick answers to common questions and troubleshooting tips.
+ +PENPOT_
+prefix.
+
+Variables are initialized in the docker-compose.yaml
file, as explained in the
+Self-hosting guide with [Elestio][1] or [Docker][2].
+
+Additionally, if you are using the developer environment, you may override their values in
+the startup scripts, as explained in the [Developer Guide][3].
+
+**NOTE**: All the examples that have values represent the **default** values, and the
+examples that do not have values are optional, and inactive by default.
+
+
+## Common ##
+
+This section will list all common configuration between backend and frontend.
+
+There are two types of configuration: options (properties that require some value) and
+flags (that just enables or disables something). All flags are set in a single
+PENPOT_FLAGS
environment variable. The envvar is a list of strings using this
+format: -\
. For example:
+
+```bash
+PENPOT_FLAGS: enable-smpt disable-registration disable-email-verification
+```
+
+### Registration ###
+
+Penpot comes with an option to completely disable the registration process;
+for this, use the following variable:
+
+```bash
+PENPOT_FLAGS: [...] disable-registration
+```
+
+You may also want to restrict the registrations to a closed list of domains:
+
+```bash
+# comma separated list of domains (backend only)
+PENPOT_REGISTRATION_DOMAIN_WHITELIST:
+
+# OR (backend only)
+PENPOT_EMAIL_DOMAIN_WHITELIST: path/to/whitelist.txt
+```
+
+**NOTE**: Since version 2.1, email whitelisting should be explicitly
+enabled with enable-email-whitelist
flag. For backward compatibility, we
+autoenable it when PENPOT_REGISTRATION_DOMAIN_WHITELIST
is set with
+not-empty content.
+
+### Demo users ###
+
+Penpot comes with facilities for fast creation of demo users without the need of a
+registration process. The demo users by default have an expiration time of 7 days, and
+once expired they are completely deleted with all the generated content. Very useful for
+testing or demonstration purposes.
+
+You can enable demo users using the following variable:
+
+```bash
+PENPOT_FLAGS: [...] enable-demo-users
+```
+
+### Authentication Providers
+
+To configure the authentication with third-party auth providers you will need to
+configure Penpot and set the correct callback of your Penpot instance in the auth-provider
+configuration.
+
+The callback has the following format:
+
+```html
+https://smpt
flag is disabled, the email will be
+printed to the console, which means that the emails will be shown in the stdout.
+
+Note that if you plan to invite members to a team, it is recommended that you enable SMTP
+as they will need to login to their account after recieving the invite link sent an in email.
+It is currently not possible to just add someone to a team without them accepting an
+invatation email.
+
+If you have an SMTP service, uncomment the appropriate settings section in
+docker-compose.yml
and configure those
+environment variables.
+
+Setting up the default FROM and REPLY-TO:
+
+```bash
+# Backend
+PENPOT_SMTP_DEFAULT_REPLY_TO: Penpot fs
and s3
(for AWS S3).
+
+#### FS Backend (default) ####
+
+This is the default backend when you use the official docker images and the default
+configuration looks like this:
+
+```bash
+# Backend
+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
+managed by an application (not a simple directory serving static files). But you should
+not worry about this unless you want to install it outside the docker container and
+configure the nginx yourself.
+
+In case you want understand how it internally works, you can take a look on the [nginx
+configuration file][4] used in the docker images.
+
+
+#### AWS S3 Backend ####
+
+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:
+
+```bash
+# AWS Credentials
+AWS_ACCESS_KEY_ID: PENPOT_PUBLIC_URI
environment
+variable in case you go to serve Penpot to the users; it should point to public URI
+where users will access the application:
+
+```bash
+PENPOT_PUBLIC_URI: http://localhost:9001
+```
+
+## Frontend ##
+
+In comparison with backend, frontend only has a small number of runtime configuration
+options, and they are located in the \/js/config.js
file.
+
+If you are using the official docker images, the best approach to set any configuration is
+using environment variables, and the image automatically generates the config.js
from
+them.
+
+**NOTE**: many frontend related configuration variables are explained in the
+[Common](#common) section, this section explains **frontend only** options.
+
+But in case you have a custom setup you probably need setup the following environment
+variables on the frontend container:
+
+To connect the frontend to the exporter and backend, you need to fill out these environment variables.
+
+```bash
+# Frontend
+PENPOT_BACKEND_URI: http://your-penpot-backend
+PENPOT_EXPORTER_URI: http://your-penpot-exporter
+```
+
+These variables are used for generate correct nginx.conf file on container startup.
+
+
+### Demo warning ###
+
+If you want to show a warning in the register and login page saying that this is a
+demonstration purpose instance (no backups, periodical data wipe, ...), set the following
+variable:
+
+```bash
+PENPOT_FLAGS: [...] enable-demo-warning
+```
+
+## Other flags
+
+- enable-cors
: Enables the default cors cofiguration that allows all domains
+ (this configuration is designed only for dev purposes right now)
+- enable-backend-api-doc
: Enables the /api/doc
+ endpoint that lists all rpc methods available on backend
+- disable-email-verification
: Deactivates the email verification process
+ (only recommended for local or internal setups)
+- disable-secure-session-cookies
: By default, Penpot uses the
+ secure
flag on cookies, this flag disables it;
+ it is useful if you plan to serve Penpot under different
+ domain than localhost
without HTTPS
+- disable-login-with-password
: allows disable password based login form
+- disable-registration
: disables registration (still enabled for invitations only).
+- enable-prepl-server
: enables PREPL server, used by manage.py and other additional
+ tools for communicate internally with Penpot backend
+
+__Since version 1.13.0__
+
+- enable-log-invitation-tokens
: for cases where you don't have email configured, this
+ will log to console the invitation tokens
+- enable-log-emails
: if you want to log in console send emails. This only works if smtp
+ is not configured
+
+__Since version 2.0.0__
+
+- disable-onboarding-team
: for disable onboarding team creation modal
+- disable-onboarding-newsletter
: for disable onboarding newsletter modal
+- disable-onboarding-questions
: for disable onboarding survey
+- disable-onboarding
: for disable onboarding modal
+- disable-dashboard-templates-section
: for hide the templates section from dashboard
+- enable-webhooks
: for enable webhooks
+- enable-access-tokens
: for enable access tokens
+- disable-google-fonts-provider
: disables the google fonts provider (frontend)
+
+[1]: /technical-guide/getting-started#configure-penpot-with-elestio
+[2]: /technical-guide/getting-started#configure-penpot-with-docker
+[3]: /technical-guide/developer/common#dev-environment
+[4]: https://github.com/penpot/penpot/blob/main/docker/images/files/nginx.conf
diff --git a/docs/technical-guide/developer/architecture/backend.md b/docs/technical-guide/developer/architecture/backend.md
new file mode 100644
index 000000000..1fd2ab2a7
--- /dev/null
+++ b/docs/technical-guide/developer/architecture/backend.md
@@ -0,0 +1,136 @@
+---
+title: Backend app
+---
+
+# Backend app
+
+This app is in charge of CRUD of data, integrity validation and persistence
+into a database and also into a file system for media attachments.
+
+To handle deletions it uses a garbage collector mechanism: no object in the
+database is deleted instantly. Instead, a field deleted_at
is set with the
+date and time of the deletion, and every query ignores db rows that have this
+field set. Then, an async task that runs periodically, locates rows whose
+deletion date is older than a given threshold and permanently deletes them.
+
+For this, and other possibly slow tasks, there is an internal async tasks
+worker, that may be used to queue tasks to be scheduled and executed when the
+backend is idle. Other tasks are email sending, collecting data for telemetry
+and detecting unused media attachment, for removing them from the file storage.
+
+## Backend structure
+
+Penpot backend app code resides under backend/src/app
path in the main repository.
+
+@startuml BackendGeneral
+!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
+!define DEVICONS https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/devicons
+!include DEVICONS/react.puml
+!include DEVICONS/java.puml
+!include DEVICONS/clojure.puml
+!include DEVICONS/postgresql.puml
+!include DEVICONS/redis.puml
+!include DEVICONS/chrome.puml
+
+HIDE_STEREOTYPE()
+
+Container(frontend_app, "Frontend app", "React / ClojureScript", "", "react")
+
+System_Boundary(backend, "Backend") {
+ Container(backend_app, "Backend app", "Clojure / JVM", "", "clojure")
+ ContainerDb(db, "Database", "PostgreSQL", "", "postgresql")
+ ContainerDb(redis, "Broker", "Redis", "", "redis")
+}
+
+BiRel(frontend_app, backend_app, "Open", "websocket")
+Rel(frontend_app, backend_app, "Uses", "RPC API")
+Rel(backend_app, db, "Uses", "SQL")
+Rel(redis, backend_app, "Subscribes", "pub/sub")
+Rel(backend_app, redis, "Notifies", "pub/sub")
+
+@enduml
+
+```
+ ▾ backend/src/app/
+ ▸ cli/
+ ▸ http/
+ ▸ migrations/
+ ▸ rpc/
+ ▸ setup/
+ ▸ srepl/
+ ▸ util/
+ ▸ tasks/
+ main.clj
+ config.clj
+ http.clj
+ metrics.clj
+ migrations.clj
+ notifications.clj
+ rpc.clj
+ setup.clj
+ srepl.clj
+ worker.clj
+ ...
+```
+
+* main.clj
defines the app global settings and the main entry point of the
+ application, served by a JVM.
+* config.clj
defines of the configuration options read from linux
+ environment.
+* http
contains the HTTP server and the backend routes list.
+* migrations
contains the SQL scripts that define the database schema, in
+ the form of a sequence of migrations.
+* rpc
is the main module to handle the RPC API calls.
+* notifications.clj
is the main module that manages the websocket. It allows
+ clients to subscribe to open files, intercepts update RPC calls and notify
+ them to all subscribers of the file.
+* setup
initializes the environment (loads config variables, sets up the
+ database, executes migrations, loads initial data, etc).
+* srepl
sets up an interactive REPL shell, with some useful commands to be
+ used to debug a running instance.
+* cli
sets a command-line interface, with some more maintenance commands.
+* metrics.clj
has some interceptors that watches RPC calls, calculate
+ statistics and other metrics, and send them to external systems to store and
+ analyze.
+* worker.clj
and tasks
define some async tasks that are executed in
+ parallel to the main http server (using java threads), and scheduled in a
+ cron-like table. They are useful to do some garbage collection, data packing
+ and similar periodic maintenance tasks.
+* db.clj
, emails.clj
, media.clj
, msgbus.clj
, storage.clj
,
+ rlimits.clj
are general libraries to use I/O resources (SQL database,
+ send emails, handle multimedia objects, use REDIS messages, external file
+ storage and semaphores).
+* util/
has a collection of generic utility functions.
+
+### RPC calls
+
+The RPC (Remote Procedure Call) subsystem consists of a mechanism that allows
+to expose clojure functions as an HTTP endpoint. We take advantage of being
+using Clojure at both front and back ends, to avoid needing complex data
+conversions.
+
+ 1. Frontend initiates a "query" or "mutation" call to :xxx
method, and
+ passes a Clojure object as params.
+ 2. Params are string-encoded using
+ [transit](https://github.com/cognitect/transit-clj), a format similar to
+ JSON but more powerful.
+ 3. The call is mapped to /api/rpc/query/xxx
or
+ /api/rpc/mutation/xxx
.
+ 4. The rpc
module receives the call, decode the parameters and executes the
+ corresponding method inside src/app/rpc/queries/
or src/app/rpc/mutations/
.
+ We have created a defmethod
macro to declare an RPC method and its
+ parameter specs.
+ 5. The result value is also transit-encoded and returned to the frontend.
+
+This way, frontend can execute backend calls like it was calling an async function,
+with all the power of Clojure data structures.
+
+### PubSub
+
+To manage subscriptions to a file, to be notified of changes, we use a redis
+server as a pub/sub broker. Whenever a user visits a file and opens a
+websocket, the backend creates a subscription in redis, with a topic that has
+the id of the file. If the user sends any change to the file, backend sends a
+notification to this topic, that is received by all subscribers. Then the
+notification is retrieved and sent to the user via the websocket.
+
diff --git a/docs/technical-guide/developer/architecture/common.md b/docs/technical-guide/developer/architecture/common.md
new file mode 100644
index 000000000..c6f2c7a55
--- /dev/null
+++ b/docs/technical-guide/developer/architecture/common.md
@@ -0,0 +1,84 @@
+---
+title: Common code
+---
+
+# Common code
+
+In penpot, we take advantage of using the same language in frontend and
+backend, to have a bunch of shared code.
+
+Sometimes, we use conditional compilation, for small chunks of code that
+are different in a Clojure+Java or ClojureScript+JS environments. We use
+the #?
construct, like this, for example:
+
+```clojure
+(defn ordered-set?
+ [o]
+ #?(:cljs (instance? lks/LinkedSet o)
+ :clj (instance? LinkedSet o)))
+```
+
+```text
+ ▾ common/src/app/common/
+ ▸ geom/
+ ▸ pages/
+ ▸ path/
+ ▸ types/
+ ...
+```
+
+Some of the modules need some refactoring, to organize them more cleanly.
+
+## Data model and business logic
+
+* **geom** contains functions to manage 2D geometric entities.
+ - **point** defines the 2D Point type and many geometric transformations.
+ - **matrix** defines the [2D transformation
+ matrix](https://www.alanzucconi.com/2016/02/10/tranfsormation-matrix/)
+ type and its operations.
+ - **shapes** manages shapes as a collection of points with a bounding
+ rectangle.
+* **path** contains functions to manage SVG paths, transform them and also
+ convert other types of shapes into paths.
+* **pages** contains the definition of the [Penpot data model](/technical-guide/developer/data-model/) and
+ the conceptual business logic (transformations of the model entities,
+ independent of the user interface or data storage).
+ - **spec** has the definitions of data structures of files and shapes, and
+ also of the transformation operations in **changes** module. Uses [Clojure
+ spec](https://github.com/clojure/spec.alpha) to define the structure and
+ validators.
+ - **init** defines the default content of files, pages and shapes.
+ - **helpers** are some functions to help manipulating the data structures.
+ - **migrations** is in charge to manage the evolution of the data model
+ structure over time. It contains a function that gets a file data
+ content, identifies its version, and applies the needed migrations. Much
+ like the SQL database migrations scripts.
+ - **changes** and **changes_builder** define a set of transactional
+ operations, that receive a file data content, and perform a semantic
+ operation following the business logic (add a page or a shape, change a
+ shape attribute, modify some file asset, etc.).
+* **types** we are currently in process of refactoring **pages** module, to
+ organize it in a way more compliant of [Abstract Data
+ Types](https://en.wikipedia.org/wiki/Abstract_data_type) paradigm. We are
+ approaching the process incrementally, rewriting one module each time, as
+ needed.
+
+## Utilities
+
+The main ones are:
+
+* **data** basic data structures and utility functions that could be added to
+ Clojure standard library.
+* **math** some mathematic functions that could also be standard.
+* **file_builder** functions to parse the content of a .penpot
exported file
+ and build a File data structure from it.
+* **logging** functions to generate traces for debugging and usage analysis.
+* **text** an adapter layer over the [DraftJS editor](https://draftjs.org) that
+ we use to edit text shapes in workspace.
+* **transit** functions to encode/decode Clojure objects into
+ [transit](https://github.com/cognitect/transit-clj), a format similar to JSON
+ but more powerful.
+* **uuid** functions to generate [Universally Unique Identifiers
+ (UUID)](https://en.wikipedia.org/wiki/Universally_unique_identifier), used
+ over all Penpot models to have identifiers for objects that are practically
+ ensured to be unique, without having a central control.
diff --git a/docs/technical-guide/developer/architecture/exporter.md b/docs/technical-guide/developer/architecture/exporter.md
new file mode 100644
index 000000000..1846fceee
--- /dev/null
+++ b/docs/technical-guide/developer/architecture/exporter.md
@@ -0,0 +1,68 @@
+---
+title: Exporter app
+---
+
+# Exporter app
+
+When exporting file contents to a file, we want the result to be exactly the
+same as the user sees in screen. To achieve this, we use a headless browser
+installed in the backend host, and controled via puppeteer automation. The
+browser loads the frontend app from the static webserver, and executes it like
+a normal user browser. It visits a special endpoint that renders one shape
+inside a file. Then, if takes a screenshot if we are exporting to a bitmap
+image, or extract the svg from the DOM if we want a vectorial export, and write
+it to a file that the user can download.
+
+## Exporter structure
+
+Penpot exporter app code resides under exporter/src/app
path in the main repository.
+
+@startuml Exporter
+!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
+!define DEVICONS https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/devicons
+!include DEVICONS/react.puml
+!include DEVICONS/clojure.puml
+!include DEVICONS/chrome.puml
+
+HIDE_STEREOTYPE()
+
+Container(frontend_app, "Frontend app", "React / ClojureScript", "", "react")
+
+System_Boundary(backend, "Backend") {
+ Container(exporter, "Exporter", "ClojureScript / nodejs", "", "clojure")
+ Container(browser, "Headless browser", "Chrome", "", "chrome")
+}
+
+Rel_D(frontend_app, exporter, "Uses", "HTTPS")
+Rel_R(exporter, browser, "Uses", "puppeteer")
+Rel_U(browser, frontend_app, "Uses", "HTTPS")
+
+@enduml
+
+```text
+ ▾ exporter/src/app/
+ ▸ http/
+ ▸ renderer/
+ ▸ util/
+ core.cljs
+ http.cljs
+ browser.cljs
+ config.cljs
+```
+
+## Exporter namespaces
+
+* **core** has the setup and run functions of the nodejs app.
+
+* **http** exposes a basic http server, with endpoints to export a shape or a
+ file.
+
+* **browser** has functions to control a local Chromium browser via
+ [puppeteer](https://puppeteer.github.io/puppeteer).
+
+* **renderer** has functions to tell the browser to render an object and make a
+ screenshot, and then convert it to bitmap, pdf or svg as needed.
+
+* **config** gets configuration settings from the linux environment.
+
+* **util** has some generic utility functions.
diff --git a/docs/technical-guide/developer/architecture/frontend.md b/docs/technical-guide/developer/architecture/frontend.md
new file mode 100644
index 000000000..8dc3178f9
--- /dev/null
+++ b/docs/technical-guide/developer/architecture/frontend.md
@@ -0,0 +1,258 @@
+---
+title: Frontend app
+---
+
+### Frontend app
+
+The main application, with the user interface and the presentation logic.
+
+To talk with backend, it uses a custom RPC-style API: some functions in the
+backend are exposed through an HTTP server. When the front wants to execute a
+query or data mutation, it sends a HTTP request, containing the name of the
+function to execute, and the ascii-encoded arguments. The resulting data is
+also encoded and returned. This way we don't need any data type conversion,
+besides the transport encoding, as there is Clojure at both ends.
+
+When the user opens any file, a persistent websocket is opened with the backend
+and associated to the file id. It is used to send presence events, such as
+connection, disconnection and mouse movements. And also to receive changes made
+by other users that are editing the same file, so it may be updated in real
+time.
+
+## Frontend structure
+
+Penpot frontend app code resides under frontend/src/app
path in the main repository.
+
+@startuml FrontendGeneral
+!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
+!define DEVICONS https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/devicons
+!include DEVICONS/react.puml
+
+HIDE_STEREOTYPE()
+
+Person(user, "User")
+System_Boundary(frontend, "Frontend") {
+ Container(frontend_app, "Frontend app", "React / ClojureScript", "", "react")
+ Container(worker, "Worker", "Web worker")
+}
+
+Rel(user, frontend_app, "Uses", "HTTPS")
+BiRel_L(frontend_app, worker, "Works with")
+
+@enduml
+
+```text
+ ▾ frontend/src/app/
+ ▸ main/
+ ▸ util/
+ ▸ worker/
+ main.cljs
+ worker.cljs
+```
+
+* main.cljs
and main/
contain the main frontend app, written in
+ ClojureScript language and using React framework, wrapped in [rumext
+ library](https://github.com/funcool/rumext).
+* worker.cljs
and worker/
contain the web worker, to make expensive
+ calculations in background.
+* util/
contains many generic utilities, non dependant on the user
+ interface.
+
+@startuml FrontendMain
+!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml
+!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml
+
+HIDE_STEREOTYPE()
+
+Component(ui, "ui", "main web component")
+Component(store, "store", "module")
+Component(refs, "refs", "module")
+Component(repo, "repo", "module")
+Component(streams, "streams", "module")
+Component(errors, "errors", "module")
+
+Boundary(ui_namespaces, "ui namespaces") {
+ Component(ui_auth, "auth", "web component")
+ Component(ui_settings, "settings", "web component")
+ Component(ui_dashboard, "dashboard", "web component")
+ Component(ui_workspace, "workspace", "web component")
+ Component(ui_viewer, "viewer", "web component")
+ Component(ui_render, "render", "web component")
+ Component(ui_exports, "exports", "web component")
+ Component(ui_shapes, "shapes", "component library")
+ Component(ui_components, "components", "component library")
+}
+
+Boundary(data_namespaces, "data namespaces") {
+ Component(data_common, "common", "events")
+ Component(data_users, "users", "events")
+ Component(data_dashboard, "dashboard", "events")
+ Component(data_workspace, "workspace", "events")
+ Component(data_viewer, "viewer", "events")
+ Component(data_comments, "comments", "events")
+ Component(data_fonts, "fonts", "events")
+ Component(data_messages, "messages", "events")
+ Component(data_modal, "modal", "events")
+ Component(data_shortcuts, "shortcuts", "utilities")
+}
+
+Lay_D(ui_exports, data_viewer)
+Lay_D(ui_settings, ui_components)
+Lay_D(data_viewer, data_common)
+Lay_D(data_fonts, data_messages)
+Lay_D(data_dashboard, data_modal)
+Lay_D(data_workspace, data_shortcuts)
+Lay_L(data_dashboard, data_fonts)
+Lay_L(data_workspace, data_comments)
+
+Rel_Up(refs, store, "Watches")
+Rel_Up(streams, store, "Watches")
+
+Rel(ui, ui_auth, "Routes")
+Rel(ui, ui_settings, "Routes")
+Rel(ui, ui_dashboard, "Routes")
+Rel(ui, ui_workspace, "Routes")
+Rel(ui, ui_viewer, "Routes")
+Rel(ui, ui_render, "Routes")
+
+Rel(ui_render, ui_exports, "Uses")
+Rel(ui_workspace, ui_shapes, "Uses")
+Rel(ui_viewer, ui_shapes, "Uses")
+Rel_Right(ui_exports, ui_shapes, "Uses")
+
+Rel(ui_auth, data_users, "Uses")
+Rel(ui_settings, data_users, "Uses")
+Rel(ui_dashboard, data_dashboard, "Uses")
+Rel(ui_dashboard, data_fonts, "Uses")
+Rel(ui_workspace, data_workspace, "Uses")
+Rel(ui_workspace, data_comments, "Uses")
+Rel(ui_viewer, data_viewer, "Uses")
+
+@enduml
+
+### General namespaces
+
+* **store** contains the global state of the application. Uses an event loop
+ paradigm, similar to Redux, with a global state object and a stream of events
+ that modify it. Made with [potok library](https://funcool.github.io/potok/latest/).
+
+* **refs** has the collection of references or lenses: RX streams that you can
+ use to subscribe to parts of the global state, and be notified when they
+ change.
+
+* **streams** has some streams, derived from the main event stream, for keyboard
+ and mouse events. Used mainly from the workspace viewport.
+
+* **repo** contains the functions to make calls to backend.
+
+* **errors** has functions with global error handlers, to manage exceptions or other
+ kinds of errors in the ui or the data events, notify the user in a useful way,
+ and allow to recover and continue working.
+
+### UI namespaces
+
+* **ui** is the root web component. It reads the current url and mounts the needed
+ subcomponent depending on the route.
+
+* **auth** has the web components for the login, register, password recover,
+ etc. screens.
+
+* **settings** has the web comonents for the user profile and settings screens.
+
+* **dashboard** has the web components for the dashboard and its subsections.
+
+* **workspace** has the web components for the file workspace and its subsections.
+
+* **viewer** has the web components for the viewer and its subsections.
+
+* **render** contain special web components to render one page or one specific
+ shape, to be used in exports.
+
+* **export** contain basic web components that display one shape or frame, to
+ be used from exports render or else from dashboard and viewer thumbnails and
+ other places.
+
+* **shapes** is the basic collection of web components that convert all types of
+ shapes in the corresponding svg elements, without adding any extra function.
+
+* **components** a library of generic UI widgets, to be used as building blocks
+ of penpot screens (text or numeric inputs, selects, forms, buttons...).
+
+
+### Data namespaces
+
+* **users** has events to login and register, fetch the user profile and update it.
+
+* **dashboard** has events to fetch and modify teams, projects and files.
+
+* **fonts** has some extra events to manage uploaded fonts from dashboard.
+
+* **workspace** has a lot of events to manage the current file and do all kinds of
+ edits and updates.
+
+* **comments** has some extra events to manage design comments.
+
+* **viewer** has events to fetch a file contents to display, and manage the
+ interactive behavior and hand-off.
+
+* **common** has some events used from several places.
+
+* **modal** has some events to show modal popup windows.
+
+* **messages** has some events to show non-modal informative messages.
+
+* **shortcuts** has some utility functions, used in other modules to setup the
+ keyboard shortcuts.
+
+
+## Worker app
+
+Some operations are costly to make in real time, so we leave them to be
+executed asynchronously in a web worker. This way they don't impact the user
+experience. Some of these operations are generating file thumbnails for the
+dashboard and maintaining some geometric indexes to speed up snap points while
+drawing.
+
+@startuml FrontendWorker
+!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml
+!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml
+
+HIDE_STEREOTYPE()
+
+Component(worker, "worker", "worker entry point")
+
+Boundary(worker_namespaces, "worker namespaces") {
+ Component(thumbnails, "thumbnails", "worker methods")
+ Component(snaps, "snaps", "worker methods")
+ Component(selection, "selection", "worker methods")
+ Component(impl, "impl", "worker methods")
+ Component(import, "import", "worker methods")
+ Component(export, "export", "worker methods")
+}
+
+Rel(worker, thumbnails, "Uses")
+Rel(worker, impl, "Uses")
+Rel(worker, import, "Uses")
+Rel(worker, export, "Uses")
+Rel(impl, snaps, "Uses")
+Rel(impl, selection, "Uses")
+
+@enduml
+
+* **worker** contains the worker setup code and the global handler that receives
+ requests from the main app, and process them.
+
+* **thumbnails** has a method to generate the file thumbnails used in dashboard.
+
+* **snaps** manages a distance index of shapes, and has a method to get
+ other shapes near a given one, to be used in snaps while drawing.
+
+* **selection** manages a geometric index of shapes, with methods to get what
+ shapes are under the cursor at a given moment, for select.
+
+* **impl** has a simple method to update all indexes in a page at once.
+
+* **import** has a method to import a whole file from an external .penpot
archive.
+
+* **export** has a method to export a whole file to an external .penpot
archive.
+
diff --git a/docs/technical-guide/developer/architecture/index.md b/docs/technical-guide/developer/architecture/index.md
new file mode 100644
index 000000000..b4920cb55
--- /dev/null
+++ b/docs/technical-guide/developer/architecture/index.md
@@ -0,0 +1,65 @@
+---
+title: 3.1. Architecture
+---
+
+# Architecture
+
+This section gives an overall structure of the system.
+
+Penpot has the architecture of a typical SPA. There is a frontend application,
+written in ClojureScript and using React framework, and served from a static
+web server. It talks to a backend application, that persists data on a
+PostgreSQL database.
+
+The backend is written in Clojure, so front and back can share code and data
+structures without problem. Then, the code is compiled into JVM bytecode and
+run in a JVM environment.
+
+There are some additional components, explained in subsections.
+
+@startuml C4_Elements
+!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
+!define DEVICONS https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/devicons
+!include DEVICONS/react.puml
+!include DEVICONS/java.puml
+!include DEVICONS/clojure.puml
+!include DEVICONS/postgresql.puml
+!include DEVICONS/redis.puml
+!include DEVICONS/chrome.puml
+
+HIDE_STEREOTYPE()
+
+Person(user, "User")
+System_Boundary(frontend, "Frontend") {
+ Container(frontend_app, "Frontend app", "React / ClojureScript", "", "react")
+ Container(worker, "Worker", "Web worker")
+}
+
+System_Boundary(backend, "Backend") {
+ Container(backend_app, "Backend app", "Clojure / JVM", "", "clojure")
+ ContainerDb(db, "Database", "PostgreSQL", "", "postgresql")
+ ContainerDb(redis, "Broker", "Redis", "", "redis")
+ Container(exporter, "Exporter", "ClojureScript / nodejs", "", "clojure")
+ Container(browser, "Headless browser", "Chrome", "", "chrome")
+}
+
+Rel(user, frontend_app, "Uses", "HTTPS")
+BiRel_L(frontend_app, worker, "Works with")
+BiRel(frontend_app, backend_app, "Open", "websocket")
+Rel(frontend_app, backend_app, "Uses", "RPC API")
+Rel(backend_app, db, "Uses", "SQL")
+Rel(redis, backend_app, "Subscribes", "pub/sub")
+Rel(backend_app, redis, "Notifies", "pub/sub")
+Rel(frontend_app, exporter, "Uses", "HTTPS")
+Rel(exporter, browser, "Uses", "puppeteer")
+Rel(browser, frontend_app, "Uses", "HTTPS")
+
+@enduml
+
+See more at
+
+ * [Frontend app](/technical-guide/developer/architecture/frontend/)
+ * [Backend app](/technical-guide/developer/architecture/backend/)
+ * [Exporter app](/technical-guide/developer/architecture/exporter/)
+ * [Common code](/technical-guide/developer/architecture/common/)
+
diff --git a/docs/technical-guide/developer/backend.md b/docs/technical-guide/developer/backend.md
new file mode 100644
index 000000000..672b71879
--- /dev/null
+++ b/docs/technical-guide/developer/backend.md
@@ -0,0 +1,111 @@
+---
+title: 3.6. Backend Guide
+---
+
+# Backend guide #
+
+This guide intends to explain the essential details of the backend
+application.
+
+
+## REPL ##
+
+In the devenv environment you can execute scripts/repl
to open a
+Clojure interactive shell ([REPL](https://codewith.mu/en/tutorials/1.0/repl)).
+
+Once there, you can execute (restart)
to load and execute the backend
+process, or to reload it after making changes to the source code.
+
+Then you have access to all backend code. You can import and use any function
+o read any global variable:
+
+```clojure
+(require '[app.some.namespace :as some])
+(some/your-function arg1 arg2)
+```
+
+There is a specific namespace app.srepl
with some functions useful to be
+executed from the repl and perform some tasks manually. Most of them accept
+a system
parameter. There is a global variable with this name, that contains
+the runtime information and configuration needed for the functions to run.
+
+For example:
+
+```clojure
+(require '[app.srepl.main :as srepl])
+(srepl/send-test-email! system "test@example.com")
+```
+
+
+## Fixtures ##
+
+This is a development feature that allows populate the database with a
+good amount of content (usually used for just test the application or
+perform performance tweaks on queries).
+
+In order to load fixtures, enter to the REPL environment with the scripts/repl
+script, and then execute (app.cli.fixtures/run {:preset :small})
.
+
+You also can execute this as a standalone script with:
+
+```bash
+clojure -Adev -X:fn-fixtures
+```
+
+NOTE: It is an optional step because the application can start with an
+empty database.
+
+This by default will create a bunch of users that can be used to login
+in the application. All users uses the following pattern:
+
+- Username: profileN@example.com
+- Password: 123123
+
+Where N
is a number from 0 to 5 on the default fixture parameters.
+
+
+## Migrations ##
+
+The database migrations are located in two directories:
+
+- src/app/migrations
(contains migration scripts in clojure)
+- src/app/migrations/sql
(contains the pure SQL migrations)
+
+The SQL migration naming consists in the following:
+
+```bash
+XXXX--
as a separator.
+
+If you need to have a global overview of the all schema of the database you can extract it
+using postgresql:
+
+```bash
+# (in the devenv environment)
+pg_dump -h postgres -s > schema.sql
+```
+
+## Linter ##
+
+There are no watch process for the linter; you will need to execute it
+manually. We use [clj-kondo][kondo] for linting purposes and the
+repository already comes with base configuration.
+
+[kondo]: https://github.com/clj-kondo/clj-kondo
+
+You can run **clj-kondo** as-is (is included in the devenv image):
+
+```bash
+cd penpot/backend;
+clj-kondo --lint src
+```
+
diff --git a/docs/technical-guide/developer/common.md b/docs/technical-guide/developer/common.md
new file mode 100644
index 000000000..0b8dfc6f1
--- /dev/null
+++ b/docs/technical-guide/developer/common.md
@@ -0,0 +1,494 @@
+---
+title: 3.4. Common Guide
+---
+
+# Common guide
+
+This section has articles related to all submodules (frontend, backend and
+exporter) such as: code style hints, architecture decisions, etc...
+
+
+## Configuration
+
+Both in the backend, the frontend and the exporter subsystems, there are an
+app.config
namespace that defines the global configuration variables,
+their specs and the default values.
+
+All variables have a conservative default, meaning that you can set up a Penpot
+instance without changing any configuration, and it will be reasonably safe
+and useful.
+
+In backend and exporter, to change the runtime values you need to set them in
+the process environment, following the rule that an environment variable in the
+form PENPOT_
correspond to a configuration
+variable named variable-name-in-lowercase
. Example:
+
+```bash
+(env)
+PENPOT_ASSETS_STORAGE_BACKEND=assets-s3
+
+(config)
+assets-storage-backend :assets-s3
+```
+
+In frontend, the main resources/public/index.html
file includes (if it
+exists) a file named js/config.js
, where you can set configuration values
+as javascript global variables. The file is not created by default, so if
+you need it you must create it blank, and set the variables you want, in
+the form penpot\
:
+
+```js
+(js/config.js)
+var penpotPublicURI = "https://penpot.example.com";
+
+(config)
+public-uri "https://penpot.example.com"
+```
+
+### On premise instances
+
+If you use the official Penpot docker images, as explained in the [Getting
+Started](/technical-guide/getting-started/#start-penpot) section, there is a
+[config.env](https://github.com/penpot/penpot/blob/develop/docker/images/config.env)
+file that sets the configuration environment variables. It's the same file for
+backend, exporter and frontend.
+
+For this last one, there is a script
+[nginx-entrypoint.sh](https://github.com/penpot/penpot/blob/develop/docker/images/files/nginx-entrypoint.sh)
+that reads the environment and generates the js/config.js
when the container
+is started. This way all configuration is made in the single config.env
file.
+
+
+### Dev environment
+
+If you use the [developer docker images](/technical-guide/developer/devenv/),
+the [docker-compose.yaml](https://github.com/penpot/penpot/blob/develop/docker/devenv/docker-compose.yaml)
+directly sets the environment variables more appropriate for backend and
+exporter development.
+
+Additionally, the backend [start script](https://github.com/penpot/penpot/blob/develop/backend/scripts/start-dev)
+and [repl script](https://github.com/penpot/penpot/blob/develop/backend/scripts/repl) set
+some more variables.
+
+The frontend uses only the defaults.
+
+If you want to change any variable for your local environment, you can change
+docker-compose.yaml
and shut down and start again the container. Or you can
+modify the start script or directly set the environment variable in your
+session, and restart backend or exporter processes.
+
+For frontend, you can manually create resources/public/js/config.js
(it's
+ignored in git) and define your settings there. Then, just reload the page.
+
+## System logging
+
+In [app.common.logging](https://github.com/penpot/penpot/blob/develop/common/src/app/common/logging.cljc)
+we have a general system logging utility, that may be used throughout all our
+code to generate execution traces, mainly for debugging.
+
+You can add a trace anywhere, specifying the log level (trace
, debug
,
+info
, warn
, error
) and any number of key-values:
+
+```clojure
+(ns app.main.data.workspace.libraries-helpers
+ (:require [app.common.logging :as log]))
+
+(log/set-level! :warn)
+
+...
+
+(defn generate-detach-instance
+ [changes container shape-id]
+ (log/debug :msg "Detach instance"
+ :shape-id shape-id
+ :container (:id container))
+ ...)
+```
+
+The current namespace is tracked within the log message, and you can configure
+at runtime, by namespace, the log level (by default :warn
). Any trace below
+this level will be ignored.
+
+Some keys have a special meaning:
+ * :msg
is the main trace message.
+ * ::log/raw
outputs the value without any processing or prettifying.
+ * ::log/context
append metadata to the trace (not printed, it's to be
+ processed by other tools).
+ * ::log/cause
(only in backend) attach a java exception object that will
+ be printed in a readable way with the stack trace.
+ * ::log/async
(only in backend) if set to false, makes the log processing
+ synchronous. If true (the default), it's executed in a separate thread.
+ * :js/\
(only in frontend) if you prefix the key with the js/
+ namespace, the value will be printed as a javascript interactively
+ inspectionable object.
+ * :err
(only in frontend) attach a javascript exception object, and it
+ will be printed in a readable way with the stack trace.
+
+### backend
+
+The logging utility uses a different library for Clojure and Clojurescript. In
+the first case we use [log4j2](https://logging.apache.org/log4j/2.x) to have
+much flexibility.
+
+The configuration is made in [log4j2.xml](https://github.com/penpot/penpot/blob/develop/backend/resources/log4j2.xml)
+file. The Logger used for this is named "app" (there are other loggers for
+other subsystems). The default configuration just outputs all traces of level
+debug
or higher to the console standard output.
+
+There is a different [log4j2-devenv](https://github.com/penpot/penpot/blob/develop/backend/resources/log4j2-devenv.xml)
+for the development environment. This one outputs traces of level trace
or
+higher to a file, and debug
or higher to a zmq
queue, that may be
+subscribed for other parts of the application for further processing.
+
+The ouput for a trace in logs/main.log
uses the format
+
+```bash
+[zmq
queue is not used in the default on premise or devenv setups, but there
+are a number of handlers you can use in custom instances to save errors in the
+database, or send them to a [Sentry](https://sentry.io/welcome/) or similar
+service, for example.
+
+### frontend and exporter
+
+In the Clojurescript subservices, we use [goog.log](https://google.github.io/closure-library/api/goog.log.html)
+library. This is much simpler, and basically outputs the traces to the console
+standard output (the devtools in the browser or the console in the nodejs
+exporter).
+
+In the browser, we have an utility [debug function](/technical-guide/developer/frontend/#console-debug-utility)
+that enables you to change the logging level of any namespace (or of the whole
+app) in a live environment:
+
+```javascript
+debug.set_logging("namespace", "level")
+```
+
+## Assertions
+
+Penpot source code has this types of assertions:
+
+### **assert**
+
+Just using the clojure builtin `assert` macro.
+
+Example:
+
+```clojure
+(assert (number? 3) "optional message")
+```
+
+This asserts are only executed in development mode. In production
+environment all asserts like this will be ignored by runtime.
+
+### **spec/assert**
+
+Using the app.common.spec/assert
macro.
+
+This macro is based in cojure.spec.alpha/assert
macro, and it's
+also ignored in a production environment.
+
+The Penpot variant doesn't have any runtime checks to know if asserts
+are disabled. Instead, the assert calls are completely removed by the
+compiler/runtime, thus generating simpler and faster code in production
+builds.
+
+Example:
+
+```clojure
+(require '[clojure.spec.alpha :as s]
+ '[app.common.spec :as us])
+
+(s/def ::number number?)
+
+(us/assert ::number 3)
+```
+
+### **spec/verify**
+
+An assertion type that is always executed.
+
+Example:
+
+```clojure
+(require '[app.common.spec :as us])
+
+(us/verify ::number 3)
+```
+
+This macro enables you to have assertions on production code, that
+generate runtime exceptions when failed (make sure you handle them
+appropriately).
+
+## Unit tests
+
+We expect all Penpot code (either in frontend, backend or common subsystems) to
+have unit tests, i.e. the ones that test a single unit of code, in isolation
+from other blocks. Currently we are quite far from that objective, but we are
+working to improve this.
+
+### Running tests with kaocha
+
+Unit tests are executed inside the [development environment](/technical-guide/developer/devenv).
+
+We can use [kaocha test runner](https://cljdoc.org/d/lambdaisland/kaocha/), and
+we have prepared, for convenience, some aliases in deps.edn
files. To run
+them, just go to backend
, frontend
or common
and execute:
+
+```bash
+# To run all tests once
+clojure -M:dev:test
+
+# To run all tests and keep watching for changes
+clojure -M:dev:test --watch
+
+# To run a single tests module
+clojure -M:dev:test --focus common-tests.logic.comp-sync-test
+
+# To run a single test
+clojure -M:dev:test --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute
+```
+
+Watch mode runs all tests when some file changes, except if some tests failed
+previously. In this case it only runs the failed tests. When they pass, then
+runs all of them again.
+
+You can also mark tests in the code by adding metadata:
+
+```clojure
+;; To skip a test, for example when is not working or too slow
+(deftest ^:kaocha/skip bad-test
+ (is (= 2 1)))
+
+;; To skip it but warn you during test run, so you don't forget it
+(deftest ^:kaocha/pending bad-test
+ (is (= 2 1)))
+```
+
+Please refer to the [kaocha manual](https://cljdoc.org/d/lambdaisland/kaocha/1.91.1392/doc/6-focusing-and-skipping)
+for how to define custom metadata and other ways of selecting tests.
+
+**NOTE**: in frontend
we still can't use kaocha to run the tests. We are on
+it, but for now we use shadow-cljs with package.json
scripts:
+
+```bash
+yarn run test
+yarn run test:watch
+```
+
+#### Test output
+
+The default kaocha reporter outputs a summary for the test run. There is a pair
+of brackets [ ]
for each suite, a pair of parentheses ( )
for each test,
+and a dot .
for each assertion t/is
inside tests.
+
+```bash
+penpot@c261c95d4623:~/penpot/common$ clojure -M:dev:test
+[(...)(............................................................
+.............................)(....................................
+..)(..........)(.................................)(.)(.............
+.......................................................)(..........
+.....)(......)(.)(......)(.........................................
+..............................................)(............)]
+190 tests, 3434 assertions, 0 failures.
+```
+
+All standard output from the tests is captured and hidden, except if some test
+fails. In this case, the output for the failing test is shown in a box:
+
+```bash
+FAIL in sample-test/stdout-fail-test (sample_test.clj:10)
+Expected:
+ :same
+Actual:
+ -:same +:not-same
+╭───── Test output ───────────────────────────────────────────────────────
+│ Can you see this?
+╰─────────────────────────────────────────────────────────────────────────
+2 tests, 2 assertions, 1 failures.
+```
+
+You can bypass the capture with the command line:
+
+```bash
+clojure -M:dev:test --no-capture-output
+```
+
+Or for some specific output:
+
+```clojure
+(ns sample-test
+ (:require [clojure.test :refer :all]
+ [kaocha.plugin.capture-output :as capture]))
+
+(deftest stdout-pass-test
+ (capture/bypass
+ (println "This message should be displayed"))
+ (is (= :same :same)))
+```
+
+### Running tests in the REPL
+
+An alternative way of running tests is to do it from inside the
+[REPL](/technical-guide/developer/backend/#repl) you can use in the backend and
+common apps in the development environment.
+
+We have a helper function (run-tests)
that refreshes the environment (to avoid
+having [stale tests](https://practical.li/clojure/testing/unit-testing/#command-line-test-runners))
+and runs all tests or a selection. It is defined in backend/dev/user.clj
and
+common/dev/user.clj
, so it's available without importing anything.
+
+First start a REPL:
+
+```bash
+~/penpot/backend$ scripts/repl
+```
+
+And then:
+
+```clojure
+;; To run all tests
+(run-tests)
+
+;; To run all tests in one namespace
+(run-tests 'some.namespace)
+
+;; To run a single test
+(run-tests 'some.namespace/some-test)
+
+;; To run all tests in one or several namespaces,
+;; selected by a regular expression
+(run-tests #"^backend-tests.rpc.*")
+```
+
+### Writing unit tests
+
+We write tests using the standard [Clojure test
+API](https://clojure.github.io/clojure/clojure.test-api.html). You can find a
+[guide to writing unit tests](https://practical.li/clojure/testing/unit-testing) at Practicalli
+Clojure, that we follow as much as possible.
+
+#### Sample files helpers
+
+An important issue when writing tests in Penpot is to have files with the
+specific configurations we need to test. For this, we have defined a namespace
+of helpers to easily create files and its elements with sample data.
+
+To make handling of uuids more convenient, those functions have a uuid
+registry. Whenever you create an object, you may give a :label
, and the id of
+the object will be stored in the registry associated with this label, so you
+can easily recover it later.
+
+You have functions to create files, pages and shapes, to connect them and
+specify their attributes, having all of them default values if not set.
+
+Files also store in metadata the **current page**, so you can control in what
+page the add-
and get-
functions will operate.
+
+```clojure
+(ns common-tests.sample-helpers-test
+ (:require
+ [app.common.test-helpers.files :as thf]
+ [app.common.test-helpers.ids-map :as thi]
+ [app.common.test-helpers.shapes :as ths]
+ [clojure.test :as t]))
+
+(t/deftest test-create-file
+ (let [;; Create a file with one page
+ f1 (thf/sample-file :file1)
+
+ ;; Same but define the label of the page, to retrieve it later
+ f2 (thf/sample-file :file2 :page-label :page1)
+
+ ;; Set the :name attribute of the created file
+ f3 (thf/sample-file :file3 :name "testing file")
+
+ ;; Create an isolated page
+ p2 (thf/sample-page :page2 :name "testing page")
+
+ ;; Create a second page and add to the file
+ f4 (-> (thf/sample-file :file4 :page-label :page3)
+ (thf/add-sample-page :page4 :name "other testing page"))
+
+ ;; Create an isolated shape
+ p2 (thf/sample-shape :shape1 :type :rect :name "testing shape")
+
+ ;; Add a couple of shapes to a previous file, in different pages
+ f5 (-> f4
+ (ths/add-sample-shape :shape2)
+ (thf/switch-to-page :page4)
+ (ths/add-sample-shape :shape3 :name "other testing shape"
+ :width 100))
+
+ ;; Retrieve created shapes
+ s1 (ths/get-shape f4 :shape1)
+ s2 (ths/get-shape f5 :shape2 :page-label :page3)
+ s3 (ths/get-shape f5 :shape3)]
+
+ ;; Check some values
+ (t/is (= (:name f1) "Test file"))
+ (t/is (= (:name f3) "testing file"))
+ (t/is (= (:id f2) (thi/id :file2)))
+ (t/is (= (:id (thf/current-page f2)) (thi/id :page1)))
+ (t/is (= (:id s1) (thi/id :shape1)))
+ (t/is (= (:name s1) "Rectangle"))
+ (t/is (= (:name s3) "testing shape"))
+ (t/is (= (:width s3) 100))
+ (t/is (= (:width (:selrect s3)) 100))))
+```
+
+Also there are functions to make some transformations, like creating a
+component, instantiating it or swapping a copy.
+
+```clojure
+(ns app.common-tests.sample-components-test
+ (:require
+ [app.common.test-helpers.components :as thc]
+ [app.common.test-helpers.files :as thf]
+ [app.common.test-helpers.shapes :as ths]))
+
+(t/deftest test-create-component
+ (let [;; Create a file with one component
+ f1 (-> (thf/sample-file :file1)
+ (ths/add-sample-shape :frame1 :type :frame)
+ (ths/add-sample-shape :rect1 :type :rect
+ :parent-label :frame1)
+ (thc/make-component :component1 :frame1))]))
+```
+
+Finally, there are composition helpers, to build typical structures with a
+single line of code. And the files module has some functions to display the
+contents of a file, in a way similar to `debug/dump-tree` but showing labels
+instead of ids:
+
+```clojure
+(ns app.common-tests.sample-compositions-test
+ (:require
+ [app.common.test-helpers.compositions :as tho]
+ [app.common.test-helpers.files :as thf]))
+
+(t/deftest test-create-composition
+ (let [f1 (-> (thf/sample-file :file1)
+ (tho/add-simple-component-with-copy :component1
+ :main-root
+ :main-child
+ :copy-root))]
+ (ctf/dump-file f1 :show-refs? true)))
+
+;; {:main-root} [:name Frame1] # [Component :component1]
+;; :main-child [:name Rect1]
+;;
+;; :copy-root [:name Frame1] #--> [Component :component1] :main-root
+;; fill-color
,
+ the shape is not filled). When you revert to the default state, it's better
+ to remove the attribute than leaving it with null
value. There are some
+ process (for example import & export) that filter out and remove all
+ attributes that are null
.
+
+* So never expect that attribute with null
value is a different state that
+ without the attribute.
+
+* In objects attribute names we don't use special symbols that are allowed by
+ Clojure (for example ending it with ? for boolean values), because this may
+ cause problems when exporting.
+
+## Code organization in abstraction levels
+
+Initially, Penpot data model implementation was organized in a different way.
+We are currently in a process of reorganization. The objective is to have data
+manipulation code structured in abstraction layers, with well-defined
+boundaries.
+
+At this moment the namespace structure is already organized as described here,
+but there is much code that does not comply with these rules, and needs to be
+moved or refactored. We expect to be refactoring existing modules incrementally,
+each time we do an important functionality change.
+
+### Abstract data types
+
+```text
+▾ common/
+ ▾ src/app/common/
+ ▾ types/
+ file.cljc
+ page.cljc
+ shape.cljc
+ color.cljc
+ component.cljc
+ ...
+```
+
+Namespaces here represent a single data structure, or a fragment of one, as an
+abstract data type. Each structure has:
+
+* A **schema spec** that defines the structure of the type and its values:
+
+ ```clojure
+ (sm/define! ::fill
+ [:map {:title "Fill"}
+ [:fill-color {:optional true} ::ctc/rgb-color]
+ [:fill-opacity {:optional true} ::sm/safe-number]
+ ...)
+
+ (sm/define! ::shape-attrs
+ [:map {:title "ShapeAttrs"}
+ [:name {:optional true} :string]
+ [:selrect {:optional true} ::grc/rect]
+ [:points {:optional true} ::points]
+ [:blocked {:optional true} :boolean]
+ [:fills {:optional true}
+ [:vector {:gen/max 2} ::fill]]
+ ...)
+ ```
+
+* **Helper functions** to create, query and manipulate the structure. Helpers
+ at this level only are allowed to see the internal attributes of a type.
+ Updaters receive an object of the type, and return a new object modified,
+ also ensuring the internal integrity of the data after the change.
+
+ ```clojure
+ (defn setup-shape
+ "A function that initializes the geometric data of the shape. The props must
+ contain at least :x :y :width :height."
+ [{:keys [type] :as props}]
+ ...)
+
+ (defn has-direction?
+ [interaction]
+ (#{:slide :push} (-> interaction :animation :animation-type)))
+
+ (defn set-direction
+ [interaction direction]
+ (dm/assert!
+ "expected valid interaction map"
+ (check-interaction! interaction))
+ (dm/assert!
+ "expected valid direction"
+ (contains? direction-types direction))
+ (dm/assert!
+ "expected compatible interaction map"
+ (has-direction? interaction))
+ (update interaction :animation assoc :direction direction))
+ ```
+
+> IMPORTANT: we should always use helper functions to access and modify these data
+> structures. Avoid direct attribute read or using functions like assoc
or
+> update
, even if the information is contained in a single attribute. This way
+> it will be much simpler to add validation checks or to modify the internal
+> representation of a type, and will be easier to search for places in the code
+> where this data item is used.
+>
+> Currently much of Penpot code does not follow this requirement, but it
+should do in new code or in any refactor.
+
+### File operations
+
+```text
+▾ common/
+ ▾ src/app/common/
+ ▾ files/
+ helpers.cljc
+ shapes_helpers.cljc
+ ...
+```
+
+Functions that modify a file object (or a part of it) in place, returning the
+file object changed. They ensure the referential integrity within the file, or
+between a file and its libraries.
+
+```clojure
+(defn resolve-component
+ "Retrieve the referenced component, from the local file or from a library"
+ [shape file libraries & {:keys [include-deleted?] :or {include-deleted? False}}]
+ (if (= (:component-file shape) (:id file))
+ (ctkl/get-component (:data file) (:component-id shape) include-deleted?)
+ (get-component libraries
+ (:component-file shape)
+ (:component-id shape)
+ :include-deleted? include-deleted?)))
+
+(defn delete-component
+"Mark a component as deleted and store the main instance shapes inside it, to
+be able to be recovered later."
+[file-data component-id skip-undelete? Main-instance]
+(let [components-v2 (dm/get-in file-data [:options :components-v2])]
+ (if (or (not components-v2) skip-undelete?)
+ (ctkl/delete-component file-data component-id)
+ (let [set-main-instance ;; If there is a saved main-instance, restore it.
+ #(if main-instance
+ (assoc-in % [:objects (:main-instance-id %)] main-instance)
+ %)]
+ (-> file-data
+ (ctkl/update-component component-id load-component-objects)
+ (ctkl/update-component component-id set-main-instance)
+ (ctkl/mark-component-deleted component-id))))))
+```
+
+> This module is still needing an important refactor. Mainly to take functions
+> from common.types and move them here.
+
+#### File validation and repair
+
+There is a function in app.common.files.validate
that checks a file for
+referential and semantic integrity. It's called automatically when file changes
+are sent to backend, but may be invoked manually whenever it's needed.
+
+### File changes objects
+
+```text
+▾ common/
+ ▾ src/app/common/
+ ▾ files/
+ changes_builder.cljc
+ changes.cljc
+ ...
+```
+
+Wrap the update functions in file operations module into changes
objects, that
+may be serialized, stored, sent to backend and executed to actually modify a file
+object. They should not contain business logic or algorithms. Only adapt the
+interface to the file operations or types.
+
+```clojure
+(sm/define! ::changes
+ [:map {:title "changes"}
+ [:redo-changes vector?]
+ [:undo-changes seq?]
+ [:origin {:optional true} any?]
+ [:save-undo? {:optional true} boolean?]
+ [:stack-undo? {:optional true} boolean?]
+ [:undo-group {:optional true} any?]])
+
+(defmethod process-change :add-component
+ [file-data params]
+ (ctkl/add-component file-data params))
+```
+
+### Business logic
+
+```text
+▾ common/
+ ▾ src/app/common/
+ ▾ logic/
+ shapes.cljc
+ libraries.cljc
+```
+
+Functions that implement semantic user actions, in an abstract way (independent
+of UI). They don't directly modify files, but generate changes objects, that
+may be executed in frontend or sent to backend.
+
+```clojure
+(defn generate-instantiate-component
+"Generate changes to create a new instance from a component."
+[changes objects file-id component-id position page libraries old-id parent-id
+ frame-id {:keys [force-frame?] :or {force-frame? False}}]
+ (let [component (ctf/get-component libraries file-id component-id)
+ parent (when parent-id (get objects parent-id))
+ library (get libraries file-id)
+ components-v2 (dm/get-in library [:data :options :components-v2])
+ [new-shape new-shapes]º
+ (ctn/make-component-instance page
+ Component
+ (:data library)
+ Position
+ Components-v2
+ (cond-> {}
+ force-frame? (assoc :force-frame-id frame-id)))
+ changes (cond-> (pcb/add-object changes first-shape {:ignore-touched true})
+ (some? old-id) (pcb/amend-last-change #(assoc % :old-id old-id)))
+ changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true})
+ changes
+ (rest new-shapes))]
+[new-shape changes]))
+```
+
+## Data migrations
+
+```text
+▾ common/
+ ▾ src/app/common/
+ ▾ files/
+ migrations.cljc
+```
+
+When changing the model it's essential to take into account that the existing
+Penpot files must keep working without changes. If you follow the general
+considerations stated above, usually this is automatic, since the objects
+already in the database just have the default behavior, that should be the same
+as before the change. And the new features apply to new or edited objects.
+
+But if this is not possible, and we are talking of a breaking change, you can
+write a data migration. Just define a new data version and a migration script
+in migrations.cljc
and increment file-version
in common.cljc
.
+
+From then on, every time a file is loaded from the database, if its version
+number is lower than the current version in the app, the file data will be
+handled to all the needed migration functions. If you later modify and save
+the file, it will be now updated in database.
+
+## Shape edit forms
+
+![Sidebar edit form](/img/sidebar-form.png)
+
+```text
+▾ frontend/
+ ▾ src/
+ ▾ app/
+ ▾ main/
+ ▾ ui/
+ ▾ workspace/
+ ▾ sidebar/
+ ▾ options/
+ ▸ menus/
+ ▸ rows/
+ ▾ shapes/
+ bool.cljs
+ circle.cljs
+ frame.cljs
+ group.cljs
+ image.cljs
+ multiple.cljs
+ path.cljs
+ rect.cljs
+ svg_raw.cljs
+ text.cljs
+```
+
+* In shapes/*.cljs
there are the components that show the edit menu of each
+ shape type.
+
+* In menus/*.cljs
there are the building blocks of these menus.
+
+* And in rows/*.cljs
there are some pieces, for example color input and
+ picker.
+
+## Multiple edit
+
+When modifying the shape edit forms, you must take into account that these
+forms may edit several shapes at the same time, even of different types.
+
+When more than one shape is selected, the form inside multiple.cljs
is used.
+At the top of this module, a couple of maps define what attributes may be edited
+and how, for each type of shape.
+
+Then, the blocks in menus/*.cljs
are used, but they are not given a shape, but
+a values map. For each attribute, if all shapes have the same value, it is taken;
+if not, the attribute will have the value :multiple
.
+
+The form blocks must be prepared for this value, display something useful to
+the user in this case, and do a meaningful action when changing the value.
+Usually this will be to set the attribute to a fixed value in all selected
+shapes, but **only** those that may have the attribute (for example, only text
+shapes have font attributes, or only rects has border radius).
+
+## Component synchronization
+
+```text
+▾ common/
+ ▾ src/app/common/
+ ▾ types/
+ component.cljc
+```
+
+For all shape attributes, you must take into account what happens when the
+attribute in a main component is changed and then the copies are synchronized.
+
+In component.cljc
there is a structure sync-attrs
that maps shape
+attributes to sync groups. When an attribute is changed in a main component,
+the change will be propagated to its copies. If the change occurs in a copy,
+the group will be marked as *touched* in the copy shape, and from then on,
+further changes in the main to this attribute, or others in the same group,
+will not be propagated.
+
+Any attribute that is not in this map will be ignored in synchronizations.
+
+## Render shapes, export & import
+
+```text
+▾ frontend/
+ ▾ src/
+ ▾ app/
+ ▾ main/
+ ▾ ui/
+ ▾ shapes/
+ ▸ text/
+ attrs.cljs
+ bool.cljs
+ circle.cljs
+ custom_stroke.cljs
+ embed.cljs
+ export.cljs
+ fill_image.cljs
+ filters.cljs
+ frame.cljs
+ gradients.cljs
+ group.cljs
+ image.cljs
+ mask.cljs
+ path.cljs
+ rect.cljs
+ shape.cljs
+ svg_defs.cljs
+ svg_raw.cljs
+ text.cljs
+ ▾ worker/
+ ▾ import/
+ parser.cljs
+```
+
+To export a penpot file, basically we use the same system that is used to
+display shapes in the workspace or viewer. In shapes/*.cljs
there are
+components that render one shape of each type into a SVG node.
+
+But to be able to import the file later, some attributes that not match
+directly to SVG properties need to be added as metadata (for example,
+proportion locks, constraints, stroke alignment...). This is done in the
+export.cljs
module.
+
+Finally, to import a file, we make use of parser.cljs
, a module that
+contains the parse-data
function. It receives a SVG node (possibly with
+children) and converts it into a Penpot shape object. There are auxiliary
+functions to read and convert each group of attributes, from the node
+properties or the metadata (with the get-meta
function).
+
+Any attribute that is not included in the export and import functions
+will not be exported and will be lost if reimporting the file again.
+
+## Code generation
+
+```text
+▾ frontend/
+ ▾ src/
+ ▾ app/
+ ▾ main/
+ ▾ ui/
+ ▾ viewer/
+ ▾ inspect/
+ ▾ attributes/
+ blur.cljs
+ common.cljs
+ fill.cljs
+ image.cljs
+ layout.cljs
+ shadow.cljs
+ stroke.cljs
+ svg.cljs
+ text.cljs
+ attributes.cljs
+ code.cljs
+ ▾ util/
+ code_gen.cljs
+ markup_html.cljs
+ markup_svg.cljs
+ style_css.cljs
+ style_css_formats.cljs
+ style_css_values.cljs
+```
+
+In the inspect panel we have two modes:
+
+![Inspect info](/img/handoff-info.png)
+
+For the Info tab, the attributes.cljs
module and all modules under
+attributes/*.cljs
have the components that extract the attributes to inspect
+each type of shape.
+
+![Inspect code](/img/handoff-code.png)
+
+For the Code tab, the util/code_gen.cljs
module is in charge. It calls the
+other modules in util/
depending on the format.
+
+For HTML and CSS, there are functions that generate the code as needed from the
+shapes. For SVG, it simply takes the nodes from the viewer main viewport and
+prettily formats it.
diff --git a/docs/technical-guide/developer/data-model/index.md b/docs/technical-guide/developer/data-model/index.md
new file mode 100644
index 000000000..8981d6957
--- /dev/null
+++ b/docs/technical-guide/developer/data-model/index.md
@@ -0,0 +1,199 @@
+---
+title: 3.2. Data model
+---
+
+# Penpot Data Model
+
+This is the conceptual data model. The actual representations of those entities
+slightly differ, depending on the environment (frontend app, backend RPC calls
+or the SQL database, for example). But the concepts are always the same.
+
+The diagrams use [basic UML notation with PlantUML](https://plantuml.com/en/class-diagram).
+
+## Users, teams and projects
+
+@startuml TeamModel
+
+hide members
+
+class Profile
+class Team
+class Project
+class File
+class StorageObject
+class CommentThread
+class Comment
+class ShareLink
+
+Profile "*" -right- "*" Team
+Team *--> "*" Project
+Profile "*" -- "*" Project
+Project *--> "*" File
+Profile "*" -- "*" File
+File "*" <-- "*" File : libraries
+File *--> "*" StorageObject : media_objects
+File *--> "*" CommentThread : comment_threads
+CommentThread *--> "*" Comment
+File *--> "*" ShareLink : share_links
+
+@enduml
+
+A Profile
holds the personal info of any user of the system. Users belongs to
+Teams
and may create Projects
inside them.
+
+Inside the projects, there are Files
. All users of a team may see the projects
+and files inside the team. Also, any project and file has at least one user that
+is the owner, but may have more relationships with users with other roles.
+
+Files may use other files as shared libraries
.
+
+The main content of the file is in the "file data" attribute (see next section).
+But there are some objects that reside in separate entities:
+
+ * A StorageObject
represents a file in an external storage, that is embedded
+ into a file (currently images and SVG icons, but we may add other media
+ types in the future).
+
+ * CommentThreads
and Comments
are the comments that any user may add to a
+ file.
+
+ * A ShareLink
contains a token, an URL and some permissions to share the file
+ with external users.
+
+## File data
+
+@startuml FileModel
+
+hide members
+
+class File
+class Page
+class Component
+class Color
+class MediaItem
+class Typography
+
+File *--> "*" Page : pages
+(File, Page) .. PagesList
+
+File *--> "*" Component : components
+(File, Component) .. ComponentsList
+
+File *--> "*" Color : colors
+(File, Color) .. ColorsList
+
+File *--> "*" MediaItem : colors
+(File, MediaItem) .. MediaItemsList
+
+File *--> "*" Typography : colors
+(File, Typography) .. TypographiesList
+
+@enduml
+
+The data attribute contains the Pages
and the library assets in the file
+(Components
, MediaItems
, Colors
and Typographies
).
+
+The lists of pages and assets are modelled also as entities because they have a
+lot of functions and business logic.
+
+## Pages and components
+
+@startuml PageModel
+
+hide members
+
+class Container
+class Page
+class Component
+class Shape
+
+Container <|-left- Page
+Container <|-right- Component
+
+Container *--> "*" Shape : objects
+(Container, Shape) .. ShapeTree
+
+Shape <-- Shape : parent
+
+@enduml
+
+Both Pages
and Components
contains a tree of shapes, and share many
+functions and logic. So, we have modelled a Container
entity, that is an
+abstraction that represents both a page or a component, to use it whenever we
+have code that fits the two.
+
+A ShapeTree
represents a set of shapes that are hierarchically related: the top
+frame contains top-level shapes (frames and other shapes). Frames and groups may
+contain any non frame shape.
+
+## Shapes
+
+@startuml ShapeModel
+
+hide members
+
+class Shape
+class Selrect
+class Transform
+class Constraints
+class Interactions
+class Fill
+class Stroke
+class Shadow
+class Blur
+class Font
+class Content
+class Exports
+
+Shape o--> Selrect
+Shape o--> Transform
+Shape o--> Constraints
+Shape o--> Interactions
+Shape o--> Fill
+Shape o--> Stroke
+Shape o--> Shadow
+Shape o--> Blur
+Shape o--> Font
+Shape o--> Content
+Shape o--> Exports
+
+Shape <-- Shape : parent
+
+@enduml
+
+A Shape
is the most important entity of the model. Represents one of the
+[layers of our design](https://help.penpot.app/user-guide/layer-basics), and it
+corresponds with one SVG node, augmented with Penpot special features.
+
+We have code to render a Shape
into a SVG tag, with more or less additions
+depending on the environment (editable in the workspace, interactive in the
+viewer, minimal in the shape exporter or the handoff, or with metadata in the
+file export).
+
+Also have code that imports any SVG file and convert elements back into shapes.
+If it's a SVG exported by Penpot, it reads the metadata to reconstruct the
+shapes exactly as they were. If not, it infers the atributes with a best effort
+approach.
+
+In addition to the identifier ones (the id, the name and the type of element),
+a shape has a lot of attributes. We tend to group them in related clusters.
+Those are the main ones:
+
+ * Selrect
and other geometric attributes (x, y, width, height...) define the
+ position in the diagram and the bounding box.
+ * Transform
is a [2D transformation matrix](https://www.alanzucconi.com/2016/02/10/tranfsormation-matrix/)
+ to rotate or stretch the shape.
+ * Constraints
explains how the shape changes when the container shape resizes
+ (kind of "responsive" behavior).
+ * Interactions
describe the interactive behavior when the shape is displayed
+ in the viewer.
+ * Fill
contains the shape fill color and options.
+ * Stroke
contains the shape stroke color and options.
+ * Shadow
contains the shape shadow options.
+ * Blur
contains the shape blur options.
+ * Font
contains the font options for a shape of type text.
+ * Content
contains the text blocks for a shape of type text.
+ * Exports
are the defined export settings for the shape.
+
+Also a shape contains a reference to its containing shape (parent) and of all
+the children.
diff --git a/docs/technical-guide/developer/devenv.md b/docs/technical-guide/developer/devenv.md
new file mode 100644
index 000000000..dff96f0fa
--- /dev/null
+++ b/docs/technical-guide/developer/devenv.md
@@ -0,0 +1,146 @@
+---
+title: 3.3. Dev environment
+---
+
+# Development environment
+
+## System requirements
+
+You need to have docker
and docker-compose V2
installed on your system
+in order to correctly set up the development environment.
+
+You can [look here][1] for complete instructions.
+
+[1]: /technical-guide/getting-started/#install-with-docker
+
+
+Optionally, to improve performance, you can also increase the maximum number of
+user files able to be watched for changes with inotify:
+
+```bash
+echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
+```
+
+
+## Getting Started
+
+**The interactive development environment requires some familiarity of [tmux](https://github.com/tmux/tmux/wiki).**
+
+To start it, clone penpot repository, and execute:
+
+```bash
+./manage.sh pull-devenv
+./manage.sh run-devenv
+```
+
+This will do the following:
+
+1. Pull the latest devenv image from dockerhub.
+2. Start all the containers in the background.
+3. Attach the terminal to the **devenv** container and execute the tmux session.
+4. The tmux session automatically starts all the necessary services.
+
+This is an incomplete list of devenv related subcommands found on
+manage.sh script:
+
+```bash
+./manage.sh build-devenv # builds the devenv docker image (called by run-devenv automatically when needed)
+./manage.sh start-devenv # starts background running containers
+./manage.sh run-devenv # enters to new tmux session inside of one of the running containers
+./manage.sh stop-devenv # stops background running containers
+./manage.sh drop-devenv # removes all the containers, volumes and networks used by the devenv
+```
+
+Having the container running and tmux opened inside the container,
+you are free to execute commands and open as many shells as you want.
+
+You can create a new shell just pressing the **Ctr+b c** shortcut. And
+**Ctrl+b w** for switch between windows, **Ctrl+b &** for kill the
+current window.
+
+For more info: https://tmuxcheatsheet.com/
+
+It may take a minute or so, but once all of the services have started, you can
+connect to penpot by browsing to http://localhost:3449 .
+
+
+
+
+
+
+
+
+
+
+
+
+### Frontend
+
+The frontend build process is located on the tmux **window 0** and
+**window 1**. On the **window 0** we have the gulp process responsible
+of watching and building styles, fonts, icon-spreads and templates.
+
+On the **window 1** we can found the **shadow-cljs** process that is
+responsible on watch and build frontend clojurescript code.
+
+Additionally to the watch process you probably want to be able open a REPL
+process on the frontend application, for this case you can split the window
+and execute this:
+
+```bash
+npx shadow-cljs cljs-repl main
+```
+
+### Exporter
+
+The exporter build process is located in the **window 2** and in the
+same way as frontend application, it is built and watched using
+**shadow-cljs**.
+
+The main difference is that exporter will be executed in a nodejs, on
+the server side instead of browser.
+
+The window is split into two slices. The top slice shows the build process and
+on the bottom slice has a shell ready to execute the generated bundle.
+
+You can start the exporter process executing:
+
+```bash
+node target/app.js
+```
+
+This process does not start automatically.
+
+
+### Backend
+
+The backend related process is located in the tmux **window 3**, and
+you can go directly to it using ctrl+b 3
shortcut.
+
+By default the backend will be started in a non-interactive mode for convenience
+but you can press Ctrl+c
to exit and execute the following to start the repl:
+
+```bash
+./scripts/repl
+```
+
+On the REPL you have these helper functions:
+- (start)
: start all the environment
+- (stop)
: stops the environment
+- (restart)
: stops, reload and start again.
+
+And many other that are defined in the dev/user.clj
file.
+
+If an exception is raised or an error occurs when code is reloaded, just use
+(repl/refresh-all)
to finish loading the code correctly and then use
+(restart)
again.
+
+## Email
+
+To test email sending, the devenv includes [MailCatcher](https://mailcatcher.me/),
+a SMTP server that is used for develop. It does not send any mail outbounds.
+Instead, it stores them in memory and allows to browse them via a web interface
+similar to a webmail client. Simply navigate to:
+
+[http://localhost:1080](http://localhost:1080)
+
diff --git a/docs/technical-guide/developer/frontend.md b/docs/technical-guide/developer/frontend.md
new file mode 100644
index 000000000..129105976
--- /dev/null
+++ b/docs/technical-guide/developer/frontend.md
@@ -0,0 +1,647 @@
+---
+title: 3.5. Frontend Guide
+---
+
+# Frontend Guide
+
+This guide intends to explain the essential details of the frontend
+application.
+
+## UI
+
+Please refer to the [UI Guide](/technical-guide/developer/ui) to learn about implementing UI components and our design system.
+
+## Logging, Tracing & Debugging
+
+### Logging framework
+
+To trace and debug the execution of the code, one method is to enable the log
+traces that currently are in the code using the [Logging
+framework](/technical-guide/developer/common/#system-logging). You can edit a
+module and set a lower log level, to see more traces in console. Search for
+this kind of line and change to :info
or :debug
:
+
+```clojure
+(ns some.ns
+ (:require [app.util.logging :as log]))
+
+(log/set-level! :info)
+```
+
+Or you can change it live with the debug utility (see below):
+
+```javascript
+debug.set_logging("namespace", "level");
+```
+
+### Temporary traces
+
+Of course, you have the traditional way of inserting temporary traces inside
+the code to output data to the devtools console. There are several ways of
+doing this.
+
+#### Use clojurescript helper prn
+
+This helper automatically formats the clojure and js data structures as plain
+[EDN](https://clojuredocs.org/clojure.edn) for visual inspection and to know
+the exact type of the data.
+
+```clojure
+(prn "message" expression)
+```
+
+![prn example](/img/traces1.png)
+
+#### Use pprint
function
+
+We have set up a wrapper over [fipp](https://github.com/brandonbloom/fipp)
+pprint
function, that gives a human-readable formatting to the data, useful
+for easy understanding of larger data structures.
+
+The wrapper allows to easily specify level
, length
and width
parameters,
+with reasonable defaults, to control the depth level of objects to print, the
+number of attributes to show and the display width.
+
+```clojure
+(:require [app.common.pprint :refer [pprint]])
+
+;; On the code
+(pprint shape {:level 2
+ :length 21
+ :width 30})
+```
+
+![pprint example](/img/traces2.png)
+
+#### Use the js native functions
+
+The clj->js
function converts the clojure data structure into a javacript
+object, interactively inspectable in the devtools.console.
+
+```clojure
+(js/console.log "message" (clj->js expression))
+```
+
+![clj->js example](/img/traces3.png)
+
+### Breakpoints
+
+You can insert standard javascript debugger breakpoints in the code, with this
+function:
+
+```clojure
+(js-debugger)
+```
+
+The Clojurescript environment generates source maps to trace your code step by
+step and inspect variable values. You may also insert breakpoints from the
+sources tab, like when you debug javascript code.
+
+One way of locating a source file is to output a trace with (js/console.log)
+and then clicking in the source link that shows in the console at the right
+of the trace.
+
+### Access to clojure from js console
+
+The penpot namespace of the main application is exported, so that is
+accessible from javascript console in Chrome developer tools. Object
+names and data types are converted to javascript style. For example
+you can emit the event to reset zoom level by typing this at the
+console (there is autocompletion for help):
+
+```javascript
+app.main.store.emit_BANG_(app.main.data.workspace.reset_zoom);
+```
+
+### Debug utility
+
+We have defined, at src/debug.cljs
, a debug
namespace with many functions
+easily accesible from devtools console.
+
+#### Change log level
+
+You can change the [log level](/technical-guide/developer/common/#system-logging)
+of one namespace without reloading the page:
+
+```javascript
+debug.set_logging("namespace", "level");
+```
+
+#### Dump state and objects
+
+There are some functions to inspect the global state or parts of it:
+
+```javascript
+// print the whole global state
+debug.dump_state();
+
+// print the latest events in the global stream
+debug.dump_buffer();
+
+// print a key of the global state
+debug.get_state(":workspace-data :pages 0");
+
+// print the objects list of the current page
+debug.dump_objects();
+
+// print a single object by name
+debug.dump_object("Rect-1");
+
+// print the currently selected objects
+debug.dump_selected();
+
+// print all objects in the current page and local library components.
+// Objects are displayed as a tree in the same order of the
+// layers tree, and also links to components are shown.
+debug.dump_tree();
+
+// This last one has two optional flags. The first one displays the
+// object ids, and the second one the {touched} state.
+debug.dump_tree(true, true);
+```
+
+And a bunch of other utilities (see the file for more).
+
+## Workspace visual debug
+
+Debugging a problem in the viewport algorithms for grouping and
+rotating is difficult. We have set a visual debug mode that displays
+some annotations on screen, to help understanding what's happening.
+This is also in the debug
namespace.
+
+To activate it, open the javascript console and type:
+
+```js
+debug.toggle_debug("option");
+```
+
+Current options are bounding-boxes
, group
, events
and
+rotation-handler
.
+
+You can also activate or deactivate all visual aids with
+
+```js
+debug.debug_all();
+debug.debug_none();
+```
+
+## Translations (I18N)
+
+### How it works
+
+All the translation strings of this application are stored in
+standard _gettext_ files in frontend/translations/*.po
.
+
+They have a self explanatory format that looks like this:
+
+```bash
+#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
+msgid "auth.create-demo-account"
+msgstr "Create demo account"
+```
+
+The files are automatically bundled into the index.html
file on
+compile time (in development and production). The bundled content is a
+simplified version of this data structure to avoid loading unnecesary
+data. The development environment has a watch process that detect
+changes on that file and recompiles the index.html
.
+
+**There are no hot reload for translations strings**, you just need to
+refresh the browser tab to refresh the translations in the running the
+application.
+
+Finally, when you have finished adding texts, execute the following command
+inside the devenv, to reformat the file before commiting the file into the
+repository:
+
+```bash
+# cd app.util.i18n/tr
function for lookup translation
+strings:
+
+```clojure
+(require [app.util.i18n :as i18n :refer [tr]])
+
+(tr "auth.create-demo-account")
+;; => "Create demo account"
+```
+
+If you want to insert a variable into a translated text, use %s
as
+placeholder, and then pass the variable value to the (tr ...)
call.:
+
+```bash
+#: src/app/main/ui/settings/change_email.cljs
+msgid "notifications.validation-email-sent"
+msgstr "Verification email sent to %s. Check your email!"
+```
+
+```clojure
+(require [app.util.i18n :as i18n :refer [tr]])
+
+(tr "notifications.validation-email-sent" email)
+;; => "Verification email sent to test@example.com. Check your email!"
+```
+
+If you have defined plurals for some translation resource, then you
+need to pass an additional parameter marked as counter in order to
+allow the system know when to show the plural:
+
+```bash
+#: src/app/main/ui/dashboard/team.cljs
+msgid "labels.num-of-projects"
+msgid_plural "labels.num-of-projects"
+msgstr[0] "1 project"
+msgstr[1] "%s projects"
+```
+
+```clojure
+(require [app.util.i18n :as i18n :refer [tr]])
+
+(tr "labels.num-of-projects" (i18n/c 10))
+;; => "10 projects"
+
+(tr "labels.num-of-projects" (i18n/c 1))
+;; => "1 project"
+```
+
+## Integration tests
+
+### Setup
+
+To run integration tests locally, follow these steps.
+
+Ensure your development environment docker image is up to date.
+
+1. If it is not up to date, run:
+
+```bash
+./manage.sh pull-devenv
+```
+
+2. Once the update is complete, start the environment:
+
+```bash
+./manage.sh start-devenv
+```
+
+**NOTE** You can learn more about how to set up, start and stop our development environment [here](/technical-guide/developer/devenv)
+
+### Running the integration tests
+
+#### Headless mode
+
+Here's how to run the tests with a headless browser (i.e. within the terminal, no UI):
+
+1. With the developer environment tmux session opened, create a new tab with Ctrl + b c
.
+
+2. Go to the frontend folder:
+
+```bash
+cd penpot/frontend
+```
+
+3. Run the tests with yarn
:
+
+```bash
+yarn e2e:test
+```
+
+> 💡 **TIP:** By default, the tests will _not_ run in parallel. You can set the amount of workers to run the tests with --workers
. Note that, depending on your machine, this might make some tests flaky.
+
+```bash
+# run in parallel with 4 workers
+yarn e2e:test --workers 4
+```
+
+#### Running the tests in Chromium
+
+To access the testing UI and run the tests in a real browser, follow these steps:
+
+1. In a terminal _in your host machine_, navigate to the frontend
folder, then run:
+
+```bash
+# cd frontend
) to launch the command above, or you may have errors trying to run the tests.
+
+> ❗️ **IMPORTANT**: You might need to [install Playwright's browsers and dependencies](https://playwright.dev/docs/intro) in your host machine with: npx playwright install --with-deps
. In case you are using a Linux distribution other than Ubuntu, [you might need to install the dependencies manually](https://github.com/microsoft/playwright/issues/11122).
+
+### How to write a test
+
+When writing integration tests, we are simulating user actions and events triggered by them, in other to mirror real-world user interactions. The difference with fully end-to-end tests is that here we are faking the backend by intercepting the network requests, so tests can run faster and more tied to the front-end.
+
+Keep in mind:
+
+- **Use Realistic User Scenarios:** Design test cases that mimic real user scenarios and interactions with the application.
+
+- **Simulate User Inputs**: Such as mouse clicks, keyboard inputs, form submissions, or touch gestures, using the testing framework's API. Mimic user interactions as closely as possible to accurately simulate user behavior.
+
+- **Intercept the network**: Playwright offers ways to fake network responses to API calls, websocket messages, etc. Remember that there is no backend here, so you will need to intercept every request made by the front-end app.
+
+#### Page Object Model
+
+When writing a significant number of tests, encountering repetitive code and common actions is typical. To address this issue, we recommend leveraging **Page Object Models** (POM), which is a single class that encapsulates common locators, user interactions, etc.
+
+POMs do not necessarily refer to entire pages but can also represent specific regions of a page that are the focus of our tests. For example, we may have a POM for the login form, or the projects section.
+
+In a POM, we can define locators in the constructor itself — remember that locators will be accessed when interacted with (with a click()
, for instance) or when asserting expectations.
+
+```js
+class LoginPage {
+ constructor(page) {
+ super(page);
+ this.loginButton = page.getByRole("button", { name: "Login" });
+ this.passwordInput = page.getByLabel("Password");
+ this.emailInput = page.getByLabel("Email");
+ }
+
+ // ...
+}
+```
+
+We can later use this POM and its locators:
+
+```js
+test("Sample test", async ({ page }) => {
+ const loginPage = new loginPage(page);
+ // ...
+ await expect(loginPage.loginButton).toBeVisible();
+});
+```
+
+> 💡 **TIP**: Locators that are generic and meant to be used in multiple tests should be part of the POM.
+>
+> If your locator is ad-hoc for a specific test, there's no need to add it to the POM.
+
+In addition to locators, POMs also include methods that perform common actions on those elements, like filling out a group of related input fields.
+
+```js
+class LoginPage {
+ // ...
+ async fillEmailAndPasswordInputs(email, password) {
+ await this.emailInput.fill(email);
+ await this.passwordInput.fill(password);
+ }
+}
+```
+
+POMs can also include the interception of network requests (but only include interceptions commont to multiple tests in the POM):
+
+```js
+class LoginPage {
+ // ...
+ async setupLoginSuccess() {
+ await this.mockRPC(
+ "login-with-password",
+ "logged-in-user/login-with-password-success.json"
+ );
+ }
+}
+```
+
+Here's an example of a test that uses a POM:
+
+```js
+test("User submits a wrong formatted email", async ({ page }) => {
+ const loginPage = new LoginPage(page);
+ await loginPage.setupLoginSuccess();
+
+ await loginPage.fillEmailAndPasswordInputs("foo", "lorenIpsum");
+
+ await expect(loginPage.errorLoginMessage).toBeVisible();
+});
+```
+
+#### Mocking the back-end
+
+In the penpot repository there are some POMs that are meant to be extended by more specific pages. These include methods that should be useful when you write your own POMs.
+
+- BasePage
contains methods to intercept network requests and return JSON data fixtures.
+
+- BaseWebSocketPage
also can intercept websocket connections, which are a must for tests in the workspace, or any other Penpot page that uses a WebSocket.
+
+##### API calls
+
+In order to mock API calls we just need to extend from the BasePage
POM and then call its method mockRPC
:
+
+```js
+export class FooPage extends BasePage {
+ setupNetworkResponses() {
+ this.mockRPC("lorem/ipsum", "json-file-with-fake-response.json");
+
+ // Regexes are supported too
+ this.mockRPC(
+ /a\-regex$/
+ "json-file-with-fake-response.json"
+ );
+
+ // ...You can also pass custom status code and override other options
+ this.mockRPC("something/not/found", "json-file-with-fake-response.json", {
+ status: 404,
+ });
+ }
+}
+```
+
+> ❗️ **IMPORTANT:** The mockRPC
method is meant to intercept calls to Penpot's RPC API, and already prefixes the path you provide with /api/rpc/command/
. So, if you need to intercept /api/rpc/command/get-profile
you would just need to call mockRPC("get-profile", "json-data.json")
.
+
+##### WebSockets
+
+Any Penpot page that uses a WebSocket requires it to be intercepted and mocked. To do that, you can extend from the POM BaseWebSocketPage
_and_ call its initWebSockets()
methods before each test.
+
+Here's an an actual example from the Penpot repository:
+
+```js
+// frontend/playwright/ui/pages/WorkspacePage.js
+export class WorkspacePage extends BaseWebSocketPage {
+ static async init(page) {
+ await BaseWebSocketPage.init(page);
+ // ...
+ }
+}
+```
+
+```js
+// frontend/playwright/ui/specs/workspace.spec.js
+test.beforeEach(async ({ page }) => {
+ await WorkspacePage.init(page);
+});
+```
+
+BaseWebSocketPage
also includes methods to wait for a specific WebSocket connection and to fake sending/receiving messages.
+
+When testing the workspace, you will want to wait for the /ws/notifications
WebSocket. There's a convenience method, waitForNotificationsWebSocket
to do that:
+
+```js
+// frontend/playwright/ui/pages/WorkspacePage.js
+export class WorkspacePage extends BaseWebSocketPage {
+ // ...
+
+ // browses to the Workspace and waits for the /ws/notifications socket to be ready
+ // to be listened to.
+ async goToWorkspace() {
+ // ...
+ this.#ws = await this.waitForNotificationsWebSocket();
+ await this.#ws.mockOpen();
+ // ...
+ }
+
+ // sends a message over the notifications websocket
+ async sendPresenceMessage(fixture) {
+ await this.#ws.mockMessage(JSON.stringify(fixture));
+ }
+
+ // ...
+}
+```
+
+```js
+// frontend/playwright/ui/specs/workspace.spec.js
+test("User receives presence notifications updates in the workspace", async ({
+ page,
+}) => {
+ const workspacePage = new WorkspacePage(page);
+ // ...
+
+ await workspacePage.goToWorkspace();
+ await workspacePage.sendPresenceMessage(presenceFixture);
+
+ await expect(
+ page.getByTestId("active-users-list").getByAltText("Princesa Leia")
+ ).toHaveCount(2);
+});
+```
+
+### Best practices for writing tests
+
+Our best practices are based on [Testing library documentation](https://testing-library.com/docs/).
+
+This is a summary of the most important points to take into account:
+
+#### Query priority for locators
+
+For our integration tests we use Playwright, you can find more info about this library and the different locators [here](https://playwright.dev/docs/intro).
+
+Locator queries are the methods to find DOM elements in the page. Your test should simulate as closely as possible the way users interact with the application. Depending on the content of the page and the element to be selected, we will choose one method or the other following these priorities:
+
+1. **Queries accessible to everyone**: Queries that simulate the experience of visual users or use assistive technologies.
+
+- [page.getByRole
](https://playwright.dev/docs/locators#locate-by-role): To locate exposed elements in the [accessibility tree](https://developer.mozilla.org/en-US/docs/Glossary/Accessibility_tree).
+
+- [page.getByLabel
](https://playwright.dev/docs/locators#locate-by-label): For querying form fields.
+
+- [page.getByPlaceholder
](https://playwright.dev/docs/locators#locate-by-placeholder): For when the placeholder text is more relevant than the label (or the label does not exist).
+
+- [page.getByText
](https://playwright.dev/docs/locators#locate-by-text): For the non-form elements that also do not have a role in the accesibility tree, but have a distintive text.
+
+2. **Semantic queries**: Less preferable than the above, since the user experience when interacting with these attributes may differ significantly depending on the browser and assistive technology being used.
+
+- [page.byAltText
](https://playwright.dev/docs/locators#locate-by-alt-text): For elements that support alt
text (\
, \
, a custom element, etc.).
+
+- [page.byTitle
](https://playwright.dev/docs/locators#locate-by-title): For elements with a title
.
+
+3. **Test IDs**: If none of the queries above are feasible, we can locate by the data-testid
attribute. This locator is the least preffered since it's not user-interaction oriented.
+
+- [page.getByTestId
](https://playwright.dev/docs/locators#locate-by-test-id): For elements with a data-testid
attribute.
+
+#### A practical example for using locator queries.
+
+Given this DOM structure:
+
+```html
+
+```
+
+The DOM above represents this part of the app:
+
+![Login page](/img/login-locators.webp)
+
+Our first task will be to locate the **login button**:
+
+![Login Button](/img/login-btn.webp)
+
+Our initial approach involves following the instructions of the first group of locators, "Queries accessible to everyone". To achieve this, we inspect the accessibility tree to gather information:
+
+![Accessibility tree Login Button](/img/a11y-tree-btn.webp)
+
+Having examined the accessibility tree, we identify that the button can be located by its role and name, which is our primary option:
+
+```js
+page.getByRole("button", { name: "Login" });
+```
+
+For selecting the \
within the form, we opt for getByLabel
, as it is the recommended method for locating form inputs:
+
+![Password input](/img/locate_by_label.webp)
+
+```js
+page.getByLabel("Password");
+```
+
+If we need to locate a text with no specific role, we can use the getByText
method:
+
+```js
+page.getByText("Penpot is the free open-");
+```
+
+To locate the rest of the elements we continue exploring the list of queries according to the order of priority. If none of the above options match the item, we resort to getByTestId
as a last resort.
+
+#### Assertions
+
+Assertions use Playwright's expect
method. Here are some tips for writing your assertions:
+
+- **Keep assertions clear and concise:** Each assertion should verify a single expected behavior or outcome. Avoid combining multiple assertions into a single line, to maintain clarity and readability.
+
+- **Use descriptive assertions:** Use assertion messages that clearly communicate the purpose of the assertion.
+
+- **Favor writing assertions from the user's point of view:** For instance, whenever possible, assert things about elements that the user can see or interact with.
+
+- **Cover the error state of a page**: Verify that the application handles errors gracefully by asserting the presence of error messages. We do not have to cover all error cases, that will be taken care of by the unit tests.
+
+- **Prefer positive assertions:** Avoid using .not
in your assertions (i.e. expect(false).not.toBeTruthy()
) —it helps with readability.
+
+#### Naming tests
+
+- **User-centric approach:** Tests should be named from the perspective of user actions. For instance, "User logs in successfully"
instead of "Test login"
.
+
+- **Descriptive names:** Test names should be descriptive, clearly indicating the action being tested.
+
+- **Clarity and conciseness:** Keep test names clear and concise.
+
+- **Use action verbs:** Start test names with action verbs to denote the action being tested. Example: "Adds a new file to the project"
.
diff --git a/docs/technical-guide/developer/index.md b/docs/technical-guide/developer/index.md
new file mode 100644
index 000000000..1e635f559
--- /dev/null
+++ b/docs/technical-guide/developer/index.md
@@ -0,0 +1,21 @@
+---
+title: 3. Developer Guide
+---
+
+# Developer Guide
+
+This section is intended for people wanting to mess with the code or the inners
+of Penpot application.
+
+The [Architecture][1] and [Data model][2] sections provide a bird's eye view of
+the whole system, to better understand how is structured.
+
+The [Dev Env][3] section explains how to setup the development enviroment that
+we (the core team) use internally.
+
+And the rest of sections are a list categorized of probably not deeply
+related HOWTO articles about dev-centric subjects.
+
+[1]: /technical-guide/developer/architecture/
+[2]: /technical-guide/developer/data-model/
+[3]: /technical-guide/developer/devenv/
diff --git a/docs/technical-guide/developer/subsystems/assets-storage.md b/docs/technical-guide/developer/subsystems/assets-storage.md
new file mode 100644
index 000000000..62000d5a8
--- /dev/null
+++ b/docs/technical-guide/developer/subsystems/assets-storage.md
@@ -0,0 +1,119 @@
+---
+title: Assets storage
+---
+
+# Assets storage
+
+The [storage.clj](https://github.com/penpot/penpot/blob/develop/backend/src/app/storage.clj)
+is a module that manages storage of binary objects. It's a generic utility
+that may be used for any kind of user uploaded files. Currently:
+
+ * Image assets in Penpot files.
+ * Uploaded fonts.
+ * Profile photos of users and teams.
+
+There is an abstract interface and several implementations (or **backends**),
+depending on where the objects are actually stored:
+
+ * :assets-fs
stores ojects in the file system, under a given base path.
+ * :assets-s3
stores them in any cloud storage with an AWS-S3 compatible
+ interface.
+ * :assets-db
stores them inside the PostgreSQL database, in a special table
+ with a binary column.
+
+## Storage API
+
+The **StorageObject** record represents one stored object. It contains the
+metadata, that is always stored in the database (table storage_object
),
+while the actual object data goes to the backend.
+
+ * :id
is the identifier you use to reference the object, may be stored
+ in other places to represent the relationship with other element.
+ * :backend
points to the backend where the object data resides.
+ * :created-at
is the date/time of object creation.
+ * :deleted-at
is the date/time of object marked for deletion (see below).
+ * :expired-at
allows to create objects that are automatically deleted
+ at some time (useful for temporary objects).
+ * :touched-at
is used to check objects that may need to be deleted (see
+ below).
+
+Also more metadata may be attached to objects, such as the :content-type
or
+the :bucket
(see below).
+
+You can use the API functions to manipulate objects. For example put-object!
+to create a new one, get-object
to retrieve the StorageObject,
+get-object-data
or get-object-bytes
to read the binary contents, etc.
+
+For profile photos or fonts, the object id is stored in the related table,
+without further ado. But for file images, one more indirection is used. The
+**file-media-object** is an abstraction that represents one image uploaded
+by the user (in the future we may support other multimedia types). It has its
+own database table, and references two StorageObjects
, one for the original
+file and another one for the thumbnail. Image shapes contains the id of the
+file-media-object
with the :is-local
property as true. Image assets in the
+file library also have a file-media-object
with :is-local
false,
+representing that the object may be being used in other files.
+
+## Serving objects
+
+Stored objects are always served by Penpot (even if they have a public URL,
+like when :s3
storage are used). We have an endpoint /assets
with three
+variants:
+
+```bash
+/assets/by-id/:db
backend, the
+data is extracted from the database and served by the app. For the other ones,
+we calculate the real url of the object, and pass it to our **nginx** server,
+via special HTTP headers, for it to retrieve the data and serve it to the user.
+
+This is the same in all environments (devenv, production or on premise).
+
+## Object buckets
+
+Obects may be organized in **buckets**, that are a kind of "intelligent" folders
+(not related to AWS-S3 buckets, this is a Penpot internal concept).
+
+The storage module may use the bucket (hardcoded) to make special treatment to
+object, such as storing in a different path, or guessing how to know if an object
+is referenced from other place.
+
+## Sharing and deleting objects
+
+To save storage space, duplicated objects wre shared. So, if for example
+several users upload the same image, or a library asset is instantiated many
+times, even by different users, the object data is actuall stored only once.
+
+To achieve this, when an object is uploaded, its content is hashed, and the
+hash compared with other objects in the same bucket. If there is a match,
+the StorabeObject
is reused. Thus, there may be different, unrelated, shapes
+or library assets whose :object-id
is the same.
+
+### Garbage collector and reference count
+
+Of course, when objects are shared, we cannot delete it directly when the
+associated item is removed or unlinked. Instead, we need some mechanism to
+track the references, and a garbage collector that deletes any object that
+is no longer referenced.
+
+We don't use explicit reference counts or indexes. Instead, the storage system
+is intelligent enough to search, depending on the bucket (one for profile
+photos, other for file media objects, etc.) if there is any element that is
+using the object. For example, in the first case we look for user or team
+profiles where the :photo-id
field matches the object id.
+
+When one item stops using one storage object (e. g. an image shape is deleted),
+we mark the object as :touched
. A periodic task revises all touched objectsm
+checking if they are still referenced in other places. If not, they are marked
+as :deleted. They're preserved in this state for some time (to allow "undeletion"
+if the user undoes the change), and eventually, another garbage collection task
+definitively deletes it, both in the backend and in the database table.
+
+For file-media-objects
, there is another collector, that periodically checks
+if a media object is referenced by any shape or asset in its file. If not, it
+marks the object as :touched
triggering the process described above.
+
diff --git a/docs/technical-guide/developer/subsystems/authentication.md b/docs/technical-guide/developer/subsystems/authentication.md
new file mode 100644
index 000000000..97fa3a9e4
--- /dev/null
+++ b/docs/technical-guide/developer/subsystems/authentication.md
@@ -0,0 +1,149 @@
+---
+title: Authentication
+---
+
+# User authentication
+
+Users in Penpot may register via several different methods (if enabled in the
+configuration of the Penpot instance). We have implemented this as a series
+of "authentication backends" in our code:
+
+ * **penpot**: internal registration with email and password.
+ * **ldap**: authentication over an external LDAP directory.
+ * **oidc**, **google**, **github**, **gitlab**: authentication over an external
+ service using the [OpenID Connect](https://openid.net/connect) protocol. We
+ have a generic handler, and other ones already preconfigured for popular
+ services.
+
+The main logic resides in the following files:
+
+```text
+backend/src/app/rpc/mutations/profile.clj
+backend/src/app/rpc/mutations/ldap.clj
+backend/src/app/rpc/mutations/verify-token.clj
+backend/src/app/http/oauth.clj
+backend/src/app/http/session.clj
+frontend/src/app/main/ui/auth/verify-token.cljs
+```
+
+We store in the user profiles in the database the auth backend used to register
+first time (mainly for audit). A user may login with other methods later, if the
+email is the same.
+
+## Register and login
+
+The code is organized to try to reuse functions and unify processes as much as
+possible for the different auth systems.
+
+
+### Penpot backend
+
+When a user types an email and password in the basic Penpot registration page,
+frontend calls :prepare-register-profile
method. It generates a "register
+token", a temporary JWT token that includes the login data.
+
+This is used in the second registration page, that finally calls
+:register-profile
with the token and the rest of profile data. This function
+is reused in all the registration methods, and it's responsible of creating the
+user profile in the database. Then, it sends the confirmation email if using
+penpot backend, or directly opens a session (see below) for othe methods or if
+the user has been invited from a team.
+
+The confirmation email has a link to /auth/verify-token
, that has a handler
+in frontend, that is a hub for different kinds of tokens (registration email,
+email change and invitation link). This view uses :verify-token
RPC call and
+redirects to the corresponding page with the result.
+
+To login with the penpot backend, the user simply types the email and password
+and they are sent to :login
method to check and open session.
+
+### OIDC backend
+
+When the user press one of the "Log in with XXX" button, frontend calls
+/auth/oauth/:provider
(provider is google, github or gitlab). The handler
+generates a request token and redirects the user to the service provider to
+authenticate in it.
+
+If succesful, the provider redirects to the/auth/oauth/:provider/callback
.
+This verifies the call with the request token, extracts another access token
+from the auth response, and uses it to request the email and full name from the
+service provider.
+
+Then checks if this is an already registered profile or not. In the first case
+it opens a session, and in the second one calls:register-profile
to create a
+new user in the sytem.
+
+For the known service providers, the addresses of the protocol endpoints are
+hardcoded. But for a generic OIDC service, there is a discovery protocol to ask
+the provider for them, or the system administrator may set them via configuration
+variables.
+
+### LDAP
+
+Registration is not possible by LDAP (we use an external user directory managed
+outside of Penpot). Typically when LDAP registration is enabled, the plain user
+& password login is disabled.
+
+When the user types their user & password and presses "Login with LDAP" button,
+the :login-with-ldap
method is called. It connects with the LDAP service to
+validate credentials and retrieve email and full name.
+
+Similarly as the OIDC backend, it checks if the profile exists, and calls
+:login
or :register-profile
as needed.
+
+## Sessions
+
+User sessions are created when a user logs in via any one of the backends. A
+session token is generated (a JWT token that does not currently contain any data)
+and returned to frontend as a cookie.
+
+Normally the session is stored in a DB table with the information of the user
+profile and the session expiration. But if a frontend connects to the backend in
+"read only" mode (for example, to debug something in production with the local
+devenv), sessions are stored in memory (may be lost if the backend restarts).
+
+## Team invitations
+
+The invitation link has a call to /auth/verify-token
frontend view (explained
+above) with a token that includes the invited email.
+
+When a user follows it, the token is verified and then the corresponding process
+is routed, depending if the email corresponds to an existing account or not. The
+:register-profile
or :login
services are used, and the invitation token is
+attached so that the profile is linked to the team at the end.
+
+## Handling unfinished registrations and bouncing users
+
+All tokens have an expiration date, and when they are put in a permanent
+storage, a garbage colector task visits it periodically to cleand old items.
+
+Also our email sever registers email bounces and spam complaint reportings
+(see backend/src/app/emails.clj
). When the email of one profile receives too
+many notifications, it becames blocked. From this on, the user cannot login or
+register with this email, and no message will be sent to it. If it recovers
+later, it needs to be unlocked manually in the database.
+
+## How to test in devenv
+
+To test all normal registration process you can use the devenv [Mail
+catcher](/technical-guide/developer/devenv/#email) utility.
+
+To test OIDC, you need to register an application in one of the providers:
+
+* [Github](https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app)
+* [Gitlab](https://docs.gitlab.com/ee/integration/oauth_provider.html)
+* [Google](https://support.google.com/cloud/answer/6158849)
+
+The URL of the app will be the devenv frontend: [http://localhost:3449]().
+
+And then put the credentials in backend/scripts/repl
and
+frontend/resources/public/js/config.js
.
+
+Finally, to test LDAP, in the devenv we include a [test LDAP](https://github.com/rroemhild/docker-test-openldap)
+server, that is already configured, and only needs to be enabled in frontend
+config.js
:
+
+```js
+var penpotFlags = "enable-login-with-ldap";
+```
+
diff --git a/docs/technical-guide/developer/subsystems/index.md b/docs/technical-guide/developer/subsystems/index.md
new file mode 100644
index 000000000..a2894fe65
--- /dev/null
+++ b/docs/technical-guide/developer/subsystems/index.md
@@ -0,0 +1,15 @@
+---
+title: 3.8. Penpot subsystems
+---
+
+# Penpot subsystems
+
+This section groups articles about several Penpot subsystems that have enough
+complexity not to be easy to understand by only looking at the source code.
+
+Each article gives an overview of how a particular functionality has been
+implemented, over the whole app (backend, frontend or exporter), and points to
+the most relevant source files to look at to start exploring it. When some
+special considerations are needed (performance questions, limits, common
+"gotchas", historic reasons of some decisions, etc.) they are also noted.
+
diff --git a/docs/technical-guide/developer/ui.md b/docs/technical-guide/developer/ui.md
new file mode 100644
index 000000000..fbcf300ca
--- /dev/null
+++ b/docs/technical-guide/developer/ui.md
@@ -0,0 +1,756 @@
+---
+title: 3.9. UI Guide
+---
+
+# UI Guide
+
+These are the guidelines for developing UI in Penpot, including the design system.
+
+## React & Rumext
+
+The UI in Penpot uses React v18 , with the help of [rumext](https://github.com/funcool/rumext) for providing Clojure bindings. See [Rumext's User Guide](https://funcool.github.io/rumext/latest/user-guide.html) to learn how to create React components with Clojure.
+
+## General guidelines
+
+We want to hold our UI code to the same quality standards of the rest of the codebase. In practice, this means:
+
+- UI components should be easy to maintain over time, especially because our design system is ever-changing.
+- UI components should be accessible, and use the relevant HTML elements and/or Aria roles when applicable.
+- We need to apply the rules for good software design:
+ - The code should adhere to common patterns.
+ - UI components should offer an ergonomic "API" (i.e. props).
+ - UI components should favor composability.
+ - Try to have loose coupling.
+
+### Composability
+
+**Composability is a common pattern** in the Web. We can see it in the standard HTML elements, which are made to be nested one inside another to craft more complex content. Standard Web components also offer slots to make composability more flexible.
+
+Our UI components must be composable. In React, this is achieved via the children
prop, in addition to pass slotted components via regular props.
+
+#### Use of children
+
+> **⚠️ NOTE**: Avoid manipulating children
in your component. See [React docs](https://react.dev/reference/react/Children#alternatives) about the topic.
+
+✅ **DO: Use children when we need to enable composing**
+
+```clojure
+(mf/defc primary-button*
+ {::mf/props :obj}
+ [{:keys [children] :rest props}]
+ [:> "button" props children])
+```
+
+❓**Why?**
+
+By using children, we are signaling the users of the component that they can put things _inside_, vs a regular prop that only works with text, etc. For example, it’s obvious that we can do things like this:
+
+```clojure
+[:> button* {}
+ [:*
+ "Subscribe for "
+ [:& money-amount {:currency "EUR" amount: 3000}]]]
+```
+
+#### Use of slotted props
+
+When we need to either:
+
+- Inject multiple (and separate) groups of elements.
+- Manipulate the provided components to add, remove, filter them, etc.
+
+Instead of children
, we can pass the component(s) via a regular a prop.
+
+#### When _not_ to pass a component via a prop
+
+It's about **ownership**. By allowing the passing of a full component, the responsibility of styling and handling the events of that component belong to whoever instantiated that component and passed it to another one.
+
+For instance, here the user would be in total control of the icon
component for styling (and for choosing which component to use as an icon, be it another React component, or a plain SVG, etc.)
+
+```clojure
+(mf/defc button*
+ {::mf/props :obj}
+ [{:keys [icon children] :rest props}]
+ [:> "button" props
+ icon
+ children])
+```
+
+However, we might want to control the aspect of the icons, or limit which icons are available for this component, or choose which specific React component should be used. In this case, instead of passing the component via a prop, we'd want to provide the data we need for the icon component to be instantiated:
+
+```clojure
+(mf/defc button*
+ {::mf/props :obj}
+ [{:keys [icon children] :rest props}]
+ (assert (or (nil? icon) (contains? valid-icon-list icon) "expected valid icon id"))
+ [:> "button" props
+ (when icon [:> icon* {:id icon :size "m"}])
+ children])
+```
+
+### Our components should have a clear responsibility
+
+It's important we are aware of:
+
+- What are the **boundaries** of our component (i.e. what it can and cannot do)
+ - Like in regular programming, it's good to keep all the inner elements at the same level of abstraction.
+ - If a component grows too big, we can split it in several ones. Note that we can mark components as private with the ::mf/private true
meta tag.
+- Which component is **responsible for what**.
+
+As a rule of thumb:
+
+- Components own the stuff they instantiate themselves.
+- Slotted components or children
belong to the place they have been instantiated.
+
+This ownership materializes in other areas, like **styles**. For instance, parent components are usually reponsible for placing their children into a layout. Or, as mentioned earlier, we should avoid manipulating the styles of a component we don't have ownership over.
+
+## Styling components
+
+We use **CSS modules** and **Sass** to style components. Use the (stl/css)
and (stl/css-case)
functions to generate the class names for the CSS modules.
+
+### Allow passing a class name
+
+Our components should allow some customization by whoever is instantiating them. This is useful for positioning elements in a layout, providing CSS properties, etc.
+
+This is achieved by accepting a class
prop (equivalent to className
in JSX). Then, we need to join the class name we have received as a prop with our own class name for CSS modules.
+
+```clojure
+(mf/defc button*
+ {::mf/props :obj}
+ [{:keys [children class] :rest props}]
+ (let [class (dm/str class " " (stl/css :primary-button))
+ props (mf/spread-props props {:class class})]
+ [:> "button" props children]))
+```
+
+### About nested selectors
+
+Nested styles for DOM elements that are not instantiated by our component should be avoided. Otherwise, we would be leaking CSS out of the component scope, which can lead to hard-to-maintain code.
+
+❌ **AVOID: Styling elements that don’t belong to the component**
+
+```clojure
+(mf/defc button*
+ {::mf/props :obj}
+ [{:keys [children] :rest props}]
+ (let [props (mf/spread-props props {:class (stl/css :primary-button)})]
+ ;; note that we are NOT instantiating a \
for link (navigation, downloading files, sending e-mails via mailto:
, etc.)
+- Using \
for triggering actions (submitting a form, closing a modal, selecting a tool, etc.)
+- Using the proper heading level.
+- Etc.
+
+Also, elements **should be focusable** with keyboard. Pay attention to tabindex
and the use of focus.
+
+### Aria roles
+
+If you cannot use a native element because of styling (like a \
for a dropdown menu), consider either adding one that is hidden (except for assistive software) or use relevant [aria roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles) in your custom markup.
+
+When using images as icons, they should have an aria-label
, alt
, or similar if they are not decorative and there's no text around to tag the button. Think, for instance, of a generic toolbar without text labels, just icon buttons.
+
+For decorative images, they don't need to be anounced to assistive devices and should have aria-hidden
set to true
.
+
+## Clojure / Rumext implementation notes
+
+Please refer to the [Rumext User Guide](https://funcool.github.io/rumext/latest/user-guide.html) for important information, like naming conventions, available functions and macros, transformations done to props, etc.
+
+Some things to have in mind:
+
+- When you want to use JavaScript props, use the meta {::mf/props :obj}
. In this case, avoid using ?
for boolean props, since they don't get a clean translation to JavaScript.
+- You can use type hints such as ^boolean
to get JS semantics.
+- Split big components into smaller ones. You can mark components as private with the ::mf/private true
meta.
+
+### Delegating props
+
+There is a mechanism to [delegate props](https://react.dev/learn/passing-props-to-a-component#forwarding-props-with-the-jsx-spread-syntax) equivalent to this:
+
+```jsx
+const Button => ({children, ...other}) {
+ return
+};
+```
+
+We just need to use `:rest ` when declaring the component props.
+
+```clojure
+(mf/defc button*
+ {::mf/props :obj}
+ [{:keys [children] :rest other}]
+ [:> "button" other children])
+```
+
+If we need to augment this props object, we can use spread-props
and the usual transformations that Rumext does (like class
-> className
, for instance) will be applied too.
+
+```clojure
+(mf/defc button*
+ {::mf/props :obj}
+ [{:keys [children class] :rest props}]
+ (let [class (dm/str class " " (stl/css :button))
+ props (mf/spread-props props {:class class})]
+ [:> "button" props children]))
+```
+
+### Performance considerations
+
+For components that belong to the “hot path” of rendering (like those in the sidebar, for instance), it’s worth avoiding some pitfalls that make rendering slower and/or will trigger a re-render.
+
+Most of this techniques revolve around achieving one of these:
+
+- Avoid creating brand new objects and functions in each render.
+- Avoid needlessly operations that can be costly.
+- Avoid a re-render.
+
+#### Use of a JS object as props
+
+It's faster to use a JS Object for props instead of a native Clojure map, because then that conversion will not happen in runtime in each re-render.
+
+✅ **DO: Use the metadata ::mf/props :obj
when creating a component**
+
+```clojure
+(mf/defc icon*
+ {::mf/props :obj}
+ [props]
+ ;; ...
+ )
+```
+
+#### Split large and complex components into smaller parts
+
+This can help to avoid full re-renders.
+
+#### Avoid creating anonymous functions as callback handlers, etc.
+
+This creates a brand new function every render. Instead, create the function on its own and memoize it when needed.
+
+❌ **AVOID: Creating anonymous functions for handlers**
+
+```clojure
+(mf/defc login-button {::mf/props obj} []
+ [:button {:on-click (fn []
+ ;; emit event to login, etc.
+ )}
+ "Login"])
+```
+
+✅ **DO: Use named functions as callback handlers**
+
+```clojure
+(defn- login []
+ ;; ...
+ )
+
+(mf/defc login-button
+ {::mf/props :obj}
+ []
+ [:button {:on-click login} "Login"])
+
+```
+
+#### Avoid defining functions inside of a component (via let
)
+
+When we do this inside of a component, a brand new function is created in every render.
+
+❌ \*\*AVOID: Using let
to define functions
+
+```clojure
+(mf/defc login-button
+ {::mf/props :obj}
+ []
+ (let [click-handler (fn []
+ ;; ...
+ )]
+ [:button {:on-click click-handler} "Login"]))
+```
+
+✅ **DO: Define functions outside of the component**
+
+```clojure
+(defn- login []
+ ;; ...
+ )
+
+(mf/defc login-button
+ {::mf/props :obj}
+ []
+ [:button {:on-click login} "Login"])
+```
+
+#### Avoid defining functions with partial
inside of components
+
+partial
returns a brand new anonymous function, so we should avoid using it in each render. For callback handlers that need parameters, a work around is to store these as data-*
attributes and retrieve them inside the function.
+
+❌ **AVOID: Using `partial` inside of a component**
+
+```clojure
+(defn- set-margin [side value]
+ ;; ...
+ )
+
+(mf/defc margins []
+ [:*
+ [:> numeric-input* {:on-change (partial set-margin :left)}]
+ [:> numeric-input* {:on-change (partial set-margin :right)}] ])
+```
+
+✅ **DO: Use data-*
attributes to modify a function (many uses)**
+
+```clojure
+(defn- set-margin [value event]
+ (let [side -> (dom/get-current-target event)
+ (dom/get-data "side")
+ (keyword)]
+ ;; ...
+)
+
+(defc margins []
+ [:*
+ [:> numeric-input* {:data-side "left" :on-change set-margin}]
+ [:> numeric-input* {:data-side "right" :on-change set-margin}]
+ [:> numeric-input* {:data-side "top" :on-change set-margin}]
+ [:> numeric-input* {:data-side "bottom" :on-change set-margin}]])
+
+```
+
+✅ **DO: Store the returned function from partial
(few uses)**
+
+```clojure
+(defn- set-padding [sides value]
+ ;; ...
+ )
+
+(def set-block-padding (partial set-padding :block))
+(def set-inline-padding (partial set-padding :inline))
+
+(defc paddings []
+ [:*
+ [:> numeric-input* {:on-change set-block-padding}]
+ [:> numeric-input* {:on-change set-inline-padding}]])
+```
+
+#### Store values you need to use multiple times
+
+Often we need to access values from props. It's best if we destructure them (because it can be costly, especially if this adds up and we need to access them multiple times) and store them in variables.
+
+##### Destructuring props
+
+✅ **DO: Destructure props with :keys
**
+
+```clojure
+(defc icon
+ {::mf/props :obj}
+ [{:keys [size img] :as props]
+ [:svg {:width size
+ :height size
+ :class (stl/css-case icon true
+ icon-large (> size 16))}
+ [:use {:href img}]])
+```
+
+❌ **AVOID: Accessing the object each time**
+
+```clojure
+(defc icon
+ {::mf/props :obj}
+ [props]
+ [:svg {:width (unchecked-get props "size")
+ :height (unchecked-get props "size")
+ :class (stl/css-case icon true
+ icon-large (> (unchecked-get props "size") 16))}
+ [:use {:href (unchecked-get props "img")}]])
+```
+
+##### Storing state values
+
+We can avoid multiple calls to (deref)
if we store the value in a variable.
+
+✅ **DO: store state values**
+
+```clojure
+(defc accordion
+ {::mf/props :obj}
+ [{:keys [^boolean default-open title children] :as props]
+
+ (let [
+ open* (mf/use-state default-open)
+ open? (deref open*)]
+ [:details {:open open?}
+ [:summary title]
+ children]))
+```
+
+##### Unroll loops
+
+Creating an array of static elements and iterating over it to generate DOM may be more costly than manually unrolling the loop.
+
+❌ **AVOID: iterating over a static array**
+
+```clojure
+(defc shape-toolbar []
+ (let tools ["rect" "circle" "text"]
+ (for tool tools [:> tool-button {:tool tool}])))
+```
+
+✅ **DO: unroll the loop**
+
+```clojure
+(defc shape-toolbar []
+ [:*
+ [:> tool-button {:tool "rect"}]
+ [:> tool-button {:tool "circle"}]
+ [:> tool-button {:tool "text"}]])
+```
+
+## Penpot Design System
+
+Penpot has started to use a **design system**, which is located at frontend/src/app/main/ui/ds
. The components of the design system is published in a Storybook at [hourly.penpot.dev/storybook/](https://hourly.penpot.dev/storybook/) with the contents of the develop
branch of the repository.
+
+When a UI component is **available in the design system**, use it!. If it's not available but it's part of the Design System (ask the design folks if you are unsure), then do add it to the design system and Storybook.
+
+### Adding a new component
+
+In order to implement a new component for the design system, you need to:
+
+- Add a new \.cljs
file within the ds/
folder tree. This contains the CLJS implementation of the component, and related code (props schemas, private components, etc.).
+- Add a \.css
file with the styles for the component. This is a CSS Module file, and the selectors are scoped to this component.
+- Add a \.stories.jsx
Storybook file (see the _Storybook_ section below).
+- (Optional) When available docs, add a \.mdx
doc file (see _Storybook_ section below).
+
+In addition to the above, you also need to **specifically export the new component** with a JavaScript-friendly name in frontend/src/app/main/ui/ds.cljs
.
+
+### Tokens
+
+We use three **levels of tokens**:
+
+- **Primary** tokens, referring to raw values (i.e. pixels, hex colors, etc.) of color, sizes, borders, etc. These are implemented as Sass variables. Examples are: $mint-250
, $sz-16
, $br-circle
, etc.
+
+- **Semantic** tokens, used mainly for theming. These are implemented with **CSS custom properties**. Depending on the theme, these semantic tokens would have different primary tokens as values. For instance, --color-accent-primary
is $purple-700
when the light theme is active, but $mint-150
in the default theme. These custom properties have **global scope**.
+
+- **Component** tokens, defined at component level as **CSS custom properties**. These are very useful when implementing variants. Examples include --button-bg-color
or --toast-icon-color
. These custom properties are constrained to the **local scope** of its component.
+
+### Implementing variants
+
+We can leverage component tokens to easily implement variants, by overriding their values in each component variant.
+
+For instance, this is how we handle the styles of \
, which have a different style depending on the level of the message (default, info, error, etc.)
+
+```scss
+.toast {
+ // common styles for all toasts
+ // ...
+
+ --toast-bg-color: var(--color-background-primary);
+ --toast-icon-color: var(--color-foreground-secondary);
+ // ... more variables here
+
+ background-color: var(--toast-bg-color);
+}
+
+.toast-icon {
+ color: var(--toast-bg-color);
+}
+
+.toast-info {
+ --toast-bg-color: var(--color-background-info);
+ --toast-icon-color: var(--color-accent-info);
+ // ... override more variables here
+}
+
+.toast-error {
+ --toast-bg-color: var(--color-background-error);
+ --toast-icon-color: var(--color-accent-error);
+ // ... override more variables here
+}
+
+// ... more variants here
+```
+
+### Using icons and SVG assets
+
+Please refer to the Storybook [documentation for icons](https://hourly.penpot.dev/storybook/?path=/docs/foundations-assets-icon--docs) and other [SVG assets](https://hourly.penpot.dev/storybook/?path=/docs/foundations-assets-rawsvg--docs) (logos, illustrations, etc.).
+
+### Storybook
+
+We use [Storybook](https://storybook.js.org/) to implement and showcase the components of the Design System.
+
+The Storybook is available at the /storybook
path in the URL for each environment. For instance, the one built out of our develop
branch is available at [hourly.penpot.dev/storybook](https://hourly.penpot.dev/storybook).
+
+#### Local development
+
+Use yarn watch:storybook
to develop the Design System components with the help of Storybook.
+
+> **⚠️ WARNING**: Do stop any existing Shadow CLJS and asset compilation jobs (like the ones running at tabs 0
and 1
in the devenv tmux), because watch:storybook
will spawn their own.
+
+#### Writing stories
+
+You should add a Storybook file for each design system component you implement. This is a .jsx
file located at the same place as your component file, with the same name. For instance, a component defined in loader.cljs
should have a loader.stories.jsx
files alongside.
+
+A **story showcases how to use** a component. For the most relevant props of your component, it's important to have at least one story to show how it's used and what effect it has.
+
+Things to take into account when considering which stories to add and how to write them:
+
+- Stories show have a Default
story that showcases how the component looks like with default values for all the props.
+
+- If a component has variants, we should show each one in its own story.
+
+- Leverage setting base prop values in args
and common rendering code in render
to reuse those in the stories and avoid code duplication.
+
+For instance, the stories file for the button*
component looks like this:
+
+```jsx
+// ...
+
+export default {
+ title: "Buttons/Button",
+ component: Components.Button,
+ // These are the props of the component, and we set here default values for
+ // all stories.
+ args: {
+ children: "Lorem ipsum",
+ disabled: false,
+ variant: undefined,
+ },
+ // ...
+ render: ({ ...args }) => ,
+};
+
+export const Default = {};
+
+// An important prop: `icon`
+export const WithIcon = {
+ args: {
+ icon: "effects",
+ },
+};
+
+// A variant
+export const Primary = {
+ args: {
+ variant: "primary",
+ },
+};
+
+// Another variant
+export const Secondary = {
+ args: {
+ variant: "secondary",
+ },
+};
+
+// More variants here…
+```
+
+In addition to the above, please **use the [Controls addon](https://storybook.js.org/docs/essentials/controls)** to let users change props and see their effect on the fly.
+
+Controls are customized with argTypes
, and you can control which ones to show / hide with parameters.controls.exclude
. For instance, for the button*
stories file, its relevant control-related code looks like this:
+
+```jsx
+// ...
+const { icons } = Components.meta;
+
+export default {
+ // ...
+ argTypes: {
+ // Use the `icons` array for possible values for the `icon` prop, and
+ // display them in a dropdown select
+ icon: {
+ options: icons,
+ control: { type: "select" },
+ },
+ // Use a toggle for the `disabled` flag prop
+ disabled: { control: "boolean" },
+ // Show these values in a dropdown for the `variant` prop.
+ variant: {
+ options: ["primary", "secondary", "ghost", "destructive"],
+ control: { type: "select" },
+ },
+ },
+ parameters: {
+ // Always hide the `children` controls.
+ controls: { exclude: ["children"] },
+ },
+ // ...
+};
+```
+
+#### Adding docs
+
+Often, Design System components come along extra documentation provided by Design. Furthermore, they might be technical things to be aware of. For this, you can add documentation in [MDX format](https://storybook.js.org/docs/writing-docs/mdx).
+
+You can use Storybook's \
element to showcase specific stories to enrich the documentation.
+
+When including codeblocks, please add code in Clojure syntax (not JSX).
+
+You can find an example MDX file in the [Buttons docs](https://hourly.penpot.dev/storybook/?path=/docs/buttons-docs--docs).
+
+### Replacing a deprecated component
+
+#### Run visual regression tests
+
+We need to generate the screenshots for the visual regression tests _before_ making
+any changes, so we can compare the "before substitution" and "after substitution" states.
+
+
+Execute the tests in the playwright's ds
project. In order to do so, stop the Shadow CLJS compiler in tmux tab #1
and run;
+```bash
+clojure -M:dev:shadow-cljs release main
+```
+This will package the frontend in release mode so the tests run faster.
+
+In your terminal, in the frontend folder, run:
+```bash
+npx playwright test --ui --project=ds
+```
+This will open the test runner UI in the selected project.
+
+![Playwright UI](/img/tech-guide/playwright-projects.webp)
+
+The first time you run these tests they'll fail because there are no screenshots yet, but the second time, they should pass.
+
+#### Import the new component
+
+In the selected file add the new namespace from the ds
folder in alphabetical order:
+
+```clojure
+[app.main.ui.ds.tab-switcher :refer [tab-switcher*]]
+...
+
+[:> tab-switcher* {}]
+```
+
+> **⚠️ NOTE**: Components with a *
suffix are meant to be used with the [:>
handler.
+
+Please refer to [Rumext User Guide](https://funcool.github.io/rumext/latest/user-guide.html) for more information.
+
+#### Pass props to the component
+
+Check the props schema in the component’s source file
+
+```clojure
+(def ^:private schema:tab-switcher
+ [:map
+ [:class {:optional true} :string]
+ [:action-button-position {:optional true}
+ [:enum "start" "end"]]
+ [:default-selected {:optional true} :string]
+ [:tabs [:vector {:min 1} schema:tab]]])
+
+
+(mf/defc tab-switcher*
+ {::mf/props :obj
+ ::mf/schema schema:tab-switcher}...)
+```
+This schema shows which props are required and which are optional, so you can
+include the necessary values with the correct types.
+
+Populate the component with the required props.
+
+```clojure
+(let [tabs
+ #js [#js {:label (tr "inspect.tabs.info")
+ :id "info"
+ :content info-content}
+
+ #js {:label (tr "inspect.tabs.code")
+ :data-testid "code"
+ :id "code"
+ :content code-content}]]
+
+ [:> tab-switcher* {:tabs tabs
+ :default-selected "info"
+ :on-change-tab handle-change-tab
+ :class (stl/css :viewer-tab-switcher)}])
+```
+
+Once the component is rendering correctly, remove the old component and its imports.
+
+#### Check tests after changes
+
+Verify that everything looks the same after making the changes. To do this, run
+the visual tests again as previously described.
+
+If the design hasn’t changed, the tests should pass without issues.
+
+However, there are cases where the design might have changed from the original.
+In this case, first check the diff
files provided by the test runner to ensure
+that the differences are expected (e.g., positioning, size, etc.).
+
+Once confirmed, inform the QA team about these changes so they can review and take any necessary actions.
diff --git a/docs/technical-guide/getting-started.md b/docs/technical-guide/getting-started.md
new file mode 100644
index 000000000..38071cc0f
--- /dev/null
+++ b/docs/technical-guide/getting-started.md
@@ -0,0 +1,269 @@
+---
+title: 1. Self-hosting Guide
+---
+
+# Self-hosting Guide
+
+This guide explains how to get your own Penpot instance, running on a machine you control, to test it, use it by you or your team, or even customize and extend it any way you like.
+
+If you need more context you can look at the post
+about self-hosting in Penpot community.
+
+**There is absolutely no difference between our SaaS offer for Penpot and your
+self-hosted Penpot platform!**
+
+There are two main options for creating a Penpot instance:
+
+1. Using the platform of our partner Elestio.
+2. Using Docker tool.
+
++The recommended way is to use Elestio, since it's simpler, fully automatic and still greatly flexible. Use Docker if you already know the tool, if need full control of the process or have extra requirements and do not want to depend on any external provider, or need to do any special customization. +
+ +Or you can try other options, +offered by Penpot community. + +## Install with Elestio + +This section explains how to get Penpot up and running using Elestio. + +This platform offers a fully managed service for on-premise instances of a selection of +open-source software! This means you can deploy a dedicated instance of Penpot in just 3 +minutes. You’ll be relieved of the need to worry about DNS configuration, SMTP, backups, +SSL certificates, OS & Penpot upgrades, and much more. + +It uses the same Docker configuration as the other installation option, below, so all +customization options are the same. + +### Get an Elestio account + ++Skip this section if you already have an Elestio account. +
+ +To create your Elestio account click here. You can choose to deploy on any one of five leading cloud +providers or on-premise. + +### Deploy Penpot using Elestio + +Now you can Create your service in “Services”: +1. Look for Penpot. +2. Select a Service Cloud Provider. +3. Select Service Cloud Region. +4. Select Service Plan (for a team of 20 you should be fine with 2GB RAM). +5. Select Elestio Service Support. +6. Provide Service Name (this will show in the URL of your instance) & Admin email (used + to create the admin account). +7. Select Advanced Configuration options (you can also do this later). +8. Hit “Create Service” on the bottom right. + +It will take a couple of minutes to get the instance launched. When the status turns to +“Service is running” you are ready to get started. + +By clicking on the Service you go to all the details and configuration options. + +In Network/CNAME you can find the URL of your instance. Copy and paste this into a browser +and start using Penpot. + +### Configure Penpot with Elestio + +If you want to make changes to your Penpot setup click on the “Update config” button in +Software. Here you can see the “Docker compose” used to create the instance. In “ENV” top +middle left you can make configuration changes that will be reflected in the Docker +compose. + +In this file, a “#” at the start of the line means it is text and not considered part of +the configuration. This means you will need to delete it to get some of the configuration +options to work. Once you made all your changes hit “Update & restart”. After a couple of +minutes, your changes will be active. + +You can find all configuration options in the [Configuration][1] section. + +Get in contact with us through support@penpot.app +if you have any questions or need help. + + +### Update Penpot + +Elestio will update your instance automatically to the latest release unless you don't +want this. In that case you need to “Disable auto updates” in Software auto updates. + + +## Install with Docker + +This section details everything you need to know to get Penpot up and running in +production environments using Docker. For this, we provide a series of *Dockerfiles* and a +*docker-compose* file that orchestrate all. + +### Install Docker + ++Skip this section if you already have docker installed, up and running. +
+ +Currently, Docker comes into two different flavours: + +#### Docker Desktop + +This is the only option to have Docker in a Windows or MacOS. Recently it's also available +for Linux, in the most popular distributions (Debian, Ubuntu and Fedora). + +You can install it following the official guide. + +Docker Desktop has a graphical control panel (GUI) to manage the service and view the +containers, images and volumes. But need the command line (Terminal in Linux and Mac, or +PowerShell in Windows) to build and run the containers, and execute other operations. + +It already includes **docker compose** utility, needed by Penpot. + +#### Docker Engine + +This is the classic and default Docker setup for Linux machines, and the only option for a +Linux VPS without graphical interface. + +You can install it following the official guide. + +And you also need the [docker +compose](https://docs.docker.com/compose/cli-command/#installing-compose-v2) (V2) +plugin. You can use the old **docker-compose** tool, but all the documentation supposes +you are using the V2. + +You can easily check which version of **docker compose** you have. If you can execute +docker compose
command, then you have V2. If you need to write docker-compose
(with a
+-
) for it to work, you have the old version.
+
+### Start Penpot
+
+As first step you will need to obtain the docker-compose.yaml
file. You can download it
+from Penpot repository.
+
+```bash
+wget https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml
+```
+or
+```bash
+curl -o docker-compose.yaml https://raw.githubusercontent.com/penpot/penpot/main/docker/images/docker-compose.yaml
+```
+
+Then simply launch composer:
+
+```bash
+docker compose -p penpot -f docker-compose.yaml up -d
+```
+
+At the end it will start listening on http://localhost:9001
+
+
+### Stop Penpot
+
+If you want to stop running Penpot, just type
+
+```bash
+docker compose -p penpot -f docker-compose.yaml down
+```
+
+
+### Configure Penpot with Docker
+
+The configuration is defined using environment variables in the docker-compose.yaml
+file. The default downloaded file already comes with the essential variables already set,
+and other ones commented out with some explanations.
+
+#### Create users using CLI
+
+By default (or when disable-email-verification
flag is used), the email verification process
+is completely disabled for new registrations but it is highly recommended enabling email
+verification or disabling registration if you are going to expose your penpot instance to
+the internet.
+
+
+If you have registration disabled, you can create additional profiles using the
+command line interface:
+
+```bash
+docker exec -ti penpot-penpot-backend-1 python3 manage.py create-profile
+```
+
+**NOTE:** the exact container name depends on your docker version and platform.
+For example it could be penpot-penpot-backend-1
or penpot_penpot-backend-1
.
+You can check the correct name executing docker ps
.
+
+**NOTE:** This script only will works when you properly have the enable-prepl-server
+flag set on backend (is set by default on the latest docker-compose.yaml file)
+
+You can find all configuration options in the [Configuration][1] section.
+
+
+### Update Penpot
+
+To get the latest version of Penpot in your local installation, you just need to
+execute:
+
+```bash
+docker compose -f docker-compose.yaml pull
+```
+
+This will fetch the latest images. When you do docker compose up
again, the containers will be recreated with the latest version.
+
+
+**Important: Upgrade from version 1.x to 2.0**
+
+The migration to version 2.0, due to the incorporation of the new v2
+components, includes an additional process that runs automatically as
+soon as the application starts. If your on-premises Penpot instance
+contains a significant amount of data (such as hundreds of penpot
+files, especially those utilizing SVG components and assets
+extensively), this process may take a few minutes.
+
+In some cases, such as when the script encounters an error, it may be
+convenient to run the process manually. To do this, you can disable
+the automatic migration process using the disable-v2-migration
flag
+in PENPOT_FLAGS
environment variable. You can then execute the
+migration process manually with the following command:
+
+```bash
+docker exec -ti WEBHOOK
trigger webhook calls, if
+appropriate, with an equivalent payload.
+
+The payload content is specified as Clojure Spec predicates:
+
+![Example of a RPC call](/img/tech-guide/webhook-call.webp)
+
+The listed spec details all required (:req
or :req-un
) and optional
+(:opt-un
) attributes of the RPC parameters.
+
+The payload of the webhook is similar, but there may be some changes (some
+parameters ommited or others added). The recommended way of understanding the
+webhook calls is by using Webhook.site.
+Generate a site URL and set it into Penpot. Then you can inspect the calls received.
+
+
+## Access tokens
+
+Personal access tokens function like an alternative to our login/password authentication system and can be used to allow an application to access the internal Penpot API.
+
+Important: Treat your access tokens like passwords as they provide access to our account.
+ +### Manage access tokens +In Penpot, access tokens are configured at user account level. To manage your access tokens, go to Your account > Access tokens. + +![Access tokens](/img/tech-guide/access-tokens.webp) + +### Generate access tokens + +1. Press the "Generate new token" button. + +![Creating token](/img/tech-guide/access-tokens-create-1.webp) + +2. Fill the name of the token. Descriptive names are recommended. + +3. Choose an expiration date. Current options are: Never, 30 days, 60 days, 90 days or 180 days. + +![Token expiration](/img/tech-guide/access-tokens-create-2.webp) + +4. Once you're happy with the name and the expiration date, press "Create token". At this step you will be able to copy the token to your clipboard. + +![Token created](/img/tech-guide/access-tokens-create-3.webp) + +### Delete access tokens + +You can delete tokens anytime. You'll find the option at the menu at the right side of each token of the tokens list. + + +### Using access tokens + +Having a personal token will allow you to use it instead of your password. + +This is an example of a curl command that you can run at the console to access your Penpot profile using an access token: + +```bash +curl -H "Authorization: TokenSpeed your workflow with the reusable power of components.
+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.
+ +A component has two parts:
+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.
+ +You can duplicate a component the same way you can duplicate any other layer. When duplicating a component, you are creating a component copy that will be linked to its main component.
+ +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".
+ + +You can delete main components and its copies anytime the same way you can delete any other layer.
+Deleting a main component at the viewport means deleting it at the assets library and viceversa, so be careful!
+ + +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:
+At the Components section from the Assets library, there are two ways to create groups in a components library.
+You can ungroup the components the same ways you can group them, via the menu option ("Ungroup" in this case) or renaming them.
+ +One very direct way to move components between groups at the assets library is by dragging them.
+ + +Where's my component? There are ways to find some components at the assets panel and at the design viewport.
+ +Select a main component at the viewport and then press "Show in assets panel" at the options of the right sidebar.
+ + +Select a component copy and then press "Show main component" at the viewport menu or the right sidebar menu.
+ + +You can push changes made at a component copy to a main component:
+If the component that is about to be updated is located in a different file which is connected to this file as a shared library, a notification will be shown offering the options to update or dismiss.
+ + +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.
+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.
+ + +Right click and select the option “Reset overrides” at the component menu to get it to the state of the Main component.
+ + +Detach a component copy to unlink it from its Main component and transform it into a group layer. Press Ctrl + Shift + K or right click and select the option “Detach instance” at the component menu.
+You can also detach components in bulk by selecting several components and performing the same action.
+ + +Penpot allows you to easily substitute component copies with other component copies.
+Tip: The first options shown to swap a component are the ones at the same level inside the assets library, so group them properly.
+ + + +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.
+ + +The annotations are also shown at the Inspect tab, as another option to improve communication between designers and developers.
+ + +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.
+ diff --git a/docs/user-guide/custom-fonts/index.njk b/docs/user-guide/custom-fonts/index.njk new file mode 100644 index 000000000..0b8965b1c --- /dev/null +++ b/docs/user-guide/custom-fonts/index.njk @@ -0,0 +1,37 @@ +--- +title: 16· Custom fonts +--- + +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.
+ +
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.
+You can find the “Fonts” section in the dashboard menu, at the left sidebar.
+ + +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.
+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.
+ + +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.
+ +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.
+ + +You should only upload fonts you own or have license to use in Penpot. Find out more in the Content rights section of Penpot's Terms of Service. You also might want to read about font licensing.
diff --git a/docs/user-guide/exporting/index.njk b/docs/user-guide/exporting/index.njk new file mode 100644 index 000000000..63f9d9fb2 --- /dev/null +++ b/docs/user-guide/exporting/index.njk @@ -0,0 +1,73 @@ +--- +title: 07· Exporting objects +--- + +In Penpot you can setup export presets for different file formats and scales.
+ +You can set up different export configurations to suit your needs. Each export configuration is called "export preset".
+ +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.
+Export presets can be also found at the View mode with the code tab activated.
+ +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.
+ + +To remove an export preset you have to select the object and then press the “-” button at the export preset you want to remove.
+ +The options of an export:
+To export multiple elements you first have to select the elements you'd like to export. If the selected elements already have export settings those will be included in the export. You can also add new export settings for multiselections.
+ +You can also launch the export for a page from the main menu or using the shortcut Ctrl + Shift + E. This option will include in the export all the export pressets set among a selection or in the page if nothing is selected.
+ + +Before confirming your export for multiple elements you will have the option to check their names, sizes and formats and a last chance to deselect the export settings you don't want to go on this batch.
+ + +A popup will show the exporting progress and will show errors if any.
+ + +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.
+ +Technical note about the PDF format.
+ + + +
+ 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.
+
+ + Currently known issue: + 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 bug in poppler, 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. +
diff --git a/docs/user-guide/flexible-layouts/index.njk b/docs/user-guide/flexible-layouts/index.njk new file mode 100644 index 000000000..b1290b780 --- /dev/null +++ b/docs/user-guide/flexible-layouts/index.njk @@ -0,0 +1,280 @@ +--- +title: 08· Flexible Layouts +--- + +Penpot's proposal tries to get as close as possible to the final output that we will see on the web. Design and development speak the same language in order to embrace web standards and improve communication between roles. At Penpot you have unique ways to create and manage adaptative layouts that have all the advantages of CSS standards.
+ + +Penpot's unique Flex Layout allows you to create flexible designs that can adapt automatically. Resize, fit, and fill content and containers without the need to do it manually.
+ ++ To help you learn the fundamentals of Flex Layout here’s a dedicated website where you will find a video tutorial and a playground template. +
+ +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 CSS Tricks.
+ + + +You can add Flex Layout to any layer, group, board or a selection including any of these. Once Flex Layout Flex is added the selected elements will be contained into a board with the Flex Layout properties. You have several ways to do this:
+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.
+To reorder elements you can drag them or use the UP/DOWN keystrokes.
+ + + +You have properties for direction, align, justify, gap, padding, margin and sizing. Those are the same properties that you can use with CSS Flexbox. You can read here detailed explanations about Flexbox properties.
+ + +Static position is the default option for flex elements, meaning that they will be included in the flex flow, using flex properties.
+ +Selecting absolute positioning will exclude the element from the Flex layout flow allowing you to freely position an element in a specific place regardless of the size of the layout where it belongs.
+ +With the z-index option you can decide the order of overlapping elements while maintaining the layers order.
+ + +When creating Flex layouts, the spacing is predicted, helping you to maintain your design composition.
+ + +Set paddings, margins and gaps using their respective numeric inputs.
+You can also click and drag to resize them while visualizing the value that is being edited:
+Designing with Flex Layout generates ready for production code. Select the flex board or its inner elements and then open the Inspect tab to obtain its properties, detailed info and raw code.
+ + +A classic example that will help you create flexible buttons that grow depending on its content.
+ + +Extremely useful for ordering list items. Remember to set fit height to the container so it can adapt to the number of items.
+ + +Use the wrap property along with row direction to get the elements positioned in multiple lines.
+ + + +Grid Layout allows you to efficiently organize, align, and distribute items in 2-dimensional layouts. You can create rows and columns of elements, giving you fine-grained control over their expansion, alignment, and responsiveness to various screen sizes. It's a powerful tool for creating responsive designs.
+ + +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 Guide to CSS Grid at CSS Tricks.
+ + +You can add Grid Layout to any layer, group, board or selection. Once Grid Layout Flex is added the selected elements will be contained into a board that handles its space through Grid Layout properties. You have several ways to add Grid Layout:
+There are properties both for Grid containers and Grid items (cells, rows, cols). Those are the same properties that you can use with CSS Grid. You can read here detailed explanations about CSS Grid properties.
+ +These are different ways to manage the element's position that therefore have different code representation.
+grid-column
and grid-row
.Align self (vertically and horizontally): Start, center, end, stretch.
+ + +To place elements inside a grid layout, just drag them or paste them in a cell or area.
+Tip: Drag an element over a grid and then press Ctrl to place it as auto. That way the layer will be positioned automatically in the first available cell or area.
+ + +To edit grid layouts (rows, columns, units, cells, areas, etc) you can either select the board and press the "Edit grid" button or double click over the board.
+You have several ways to edit rows and columns:
+ +From the design sidebar you will be able to:
+Tip: You cand drag columns and rows while leaving the elements in the same position if you perform the action while pressing Ctrl.
+ + +From the design viewport you will be able to:
+Tip: You can drag columns and rows while leaving the elements in the same position if you perform the action while pressing Ctrl.
+ +To launch the contextual menu of rows and columns you can right click over a column or row header or left click on the menu button.
+From the contextual menu of rows and columns you will be able to:
+You can use different units at your grid columns and cells:
+Areas are compositions of any number of grid cells.
+ + +You have two ways to create areas:
+1. Select more than one cell pressing left click while holding Ctrl, then press right click to open the menu and then press the option "Merge cells".
+ +2. Select one cell pressing left click, then hover near the limit to the cell you want to merge the selected cell with until the cursor changes. Then drag the cursor to merge the selected area to other areas in the same direction.
+ + +To name an area, select the "Area" option at the grid cell properties (right sidebar) and fill the name of the area.
+ +To turn areas back to regular cells, just select the "Auto" option at the grid cell properties (right sidebar).
+ +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 Inspect mode, get the code and use it in real websites.
+ + + + diff --git a/docs/user-guide/import-export/index.njk b/docs/user-guide/import-export/index.njk new file mode 100644 index 000000000..901df3c5c --- /dev/null +++ b/docs/user-guide/import-export/index.njk @@ -0,0 +1,66 @@ +--- +title: 14· Import/export files +--- + +You can export Penpot files to your computer and import them from your computer to your projects.
+ +There are two different formats in which you can import/export Penpot files. A standard one and a binary one. You always have the chance to use both for any file.
+The fast one. Binary Penpot specific.
+The open one. A compressed file that includes SVG and JSON.
+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.
+ +You can download (export) files from the workspace and from the dashboard.
+Select multiple files to export them at the same time. An overlay will show you the progress of the different exports.
+ + +Exported files linked to shared libraries provide different ways to export their assets. Choose the one that suits you better.
+Importing files from other tools and services is among the main priorities of the Penpot team. Related features are coming soon.
+ +The import option is at the projects menu. Press “Import files” and then select one or more .penpot files to import. You can import a .zip file as well.
+ +Right before importing the files to your project, you’ll still have the opportunity to review the items to be imported, have the information about the ones that can not be imported and also the chance to discard files.
+ + + + +In this documentation you will find (almost) everything you need to know about how to work with Penpot, from the interface basics to advanced functionality. Learn the details on topics such as prototyping, organizing your designs, or sharing, to get the most out of Penpot.
+ +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 support@penpot.app.
+ +Ways to start with Penpot
+ +Penpot's main areas and features
+ +At Penpot, you can inspect designs to get measures, view properties, export assets and get production-ready code.
+ +
You can activate the Inspect mode both at the View mode and at the Workspace.
+ +Go to the Inspect designs at the View mode section to know how to activate inspect mode at the View mode.
+ +At the Workspace, select the Inspect tab at the right sidebar to enter inspect mode.
+Inspect mode provides a safer view-only mode so developers can work at the Workspace without the fear of breaking things ;)
+ +You can easily get measurements and distances between an object and other objects or board edges.
+To get distances:
+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.
+ + +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.
+ + +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).
+ + +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 exporting assets.
diff --git a/docs/user-guide/introduction/index.njk b/docs/user-guide/introduction/index.njk new file mode 100644 index 000000000..d38e35b01 --- /dev/null +++ b/docs/user-guide/introduction/index.njk @@ -0,0 +1,26 @@ +--- +title: 01· Introduction +--- + +Ways to start with Penpot
+ +Speed your design workflow
+ +Info of interest about Penpot
+ +Some useful links to better understand Penpot through answers and tutorials.
+ +The Dev Diaries are our release notes, where we jot down every new feature, enhancement and fix included in every release. There are also kudos to community contributors <3
+ +Wanna know what's new? take a look at our Dev Diaries.
+ +Suscribe to the Penpot Youtube channel to get updates when we upload new Penpot tutorials, demos of features and talks.
+ +If you have questions about the "Whys" or the "Hows" of Penpot we have collected and answered a bunch of the most common questions that we've been asked so far at our Frequently Asked Questions.
+ +We launched a community space to allow for everyone to be part of the conversation. Join the community here.
diff --git a/docs/user-guide/introduction/quickstart.njk b/docs/user-guide/introduction/quickstart.njk new file mode 100644 index 000000000..6b012d15e --- /dev/null +++ b/docs/user-guide/introduction/quickstart.njk @@ -0,0 +1,15 @@ +--- +title: Quickstart +--- + +You can start using Penpot right in your browser or installing it in a server of your own.
+ +To use Penpot online point your browser to design.penpot.app and start designing. Use the latest Google Chrome or Mozilla Firefox for the best experience. We also provide specific support to WebKit (Safari / Epiphany).
+You can also go to penpot.app if you want to read more about Penpot and our new releases. There, click on the Signup button. You will be asked to create an account. We only ask for an email. There are some authentication providers available too.
+ +Currently, private Penpot instances only require basic Docker knowledge.
+You can run your own Penpot server following the instructions at the Technical Guide
diff --git a/docs/user-guide/introduction/shortcuts.njk b/docs/user-guide/introduction/shortcuts.njk new file mode 100644 index 000000000..4e503bf18 --- /dev/null +++ b/docs/user-guide/introduction/shortcuts.njk @@ -0,0 +1,935 @@ +--- +title: Shortcuts +--- + +Penpot provides shortcuts that help to speed your workflow. Many keyboard shortcuts appear next to the command names in menus.
+ +Here you can find a list of all keyboard shortcuts that you can use in Penpot. Bear in mind that most of them are at the workspace, where they are more needed.
+ +You can also check the most updated list of shortcuts at the GitHub file.
+ +The Workspace is where the designs are actually created. More about the Workspace.
+Shortcut | +Linux and Windows | +macOS | +
---|---|---|
Align bottom | +AltS | +⌥S | +
Align center horizontally | +AltH | +⌥H | +
Align center vertically | +AltV | +⌥V | +
Align left | +AltA | +⌥A | +
Align right | +AltD | +⌥D | +
Align top | +AltW | +⌥W | +
Distribute horizontally | +CtrlShiftAltH | +⌘⇧⌥H | +
Distribute vertically | +CtrlShiftAltV | +⌘⇧⌥V | +
Shortcut | +Linux and Windows | +macOS | +
---|---|---|
Cancel | +Esc | +Esc | +
Clear undo | +CtrlQ | +AltQ | +
Copy | +CtrlC | +⌘C | +
Cut | +CtrlX | +⌘X | +
Delete | +Supr or Delete | +⌫ | +
Duplicate | +CtrlD | +⌘D | +
Paste | +CtrlV | +⌘V | +
Redo | +CtrlShiftZ | +⌘⇧Z | +
Start/Stop measurement | +Alt or . | +⌥ or . | +
Start editing | +Enter | +Enter | +
Undo | +CtrlZ | +⌘Z | +
Shortcut | +Linux and Windows | +macOS | +
---|---|---|
Switch color theme | +AltM | +⌥M | +
Export shapes | +CtrlShiftE | +⌘⇧E | +
Select all | +CtrlA | +⌘A | +
Set thumbnails | +ShiftT | +⇧T | +
Show/hide grid | +Ctrl' | +⌘' | +
Show/hide pixel grid | +Shift, | +⇧, | +
Show/hide rulers | +CtrlShiftR | +⌘⇧R | +
Show/hide shortcuts | +? | +? | +
Snap to grid | +CtrlShift' | +⌘⇧' | +
Snap to guides | +CtrlShiftG | +⌘⇧G | +
Snap to pixel grid | +, | +, | +
Toggle dynamic alignment | +Ctrl\ | +⌘\ | +
Toggle scale tool | +K | +K | +
Shortcut | +Linux and Windows | +macOS | +
---|---|---|
Bring forward | +Ctrl↑ | +⌘↑ | +
Bring to front | +CtrlShift↑ | +⌘⇧↑ | +
Create artboard from selection | +CtrlAltG | +⌘⌥G | +
Create component | +CtrlK | +⌘K | +
Detach component | +CtrlShiftK | +⌘⇧K | +
Flip horizontal | +CtrlH | +⌘H | +
Flip vertical | +CtrlV | +⌘V | +
Group | +CtrlG | +⌘G | +
Mask | +CtrlM | +⌘M | +
Move down | +↓ | +↓ | +
Move down fast | +Shift↓ | +⇧↓ | +
Move left | +← | +← | +
Move left fast | +Shift← | +⇧← | +
Move right | +→ | +→ | +
Move right fast | +Shift→ | +⇧→ | +
Move up | +↑ | +↑ | +
Move up fast | +Shift↑ | +⇧↑ | +
Send backwards | +Ctrl↓ | +⌘↓ | +
Send to back | +CtrlShift↓ | +⌘⇧↓ | +
Set opacity to 10% | +1 | +1 | +
Set opacity to 20% | +2 | +2 | +
Set opacity to 30% | +3 | +3 | +
Set opacity to 40% | +4 | +4 | +
Set opacity to 50% | +5 | +5 | +
Set opacity to 60% | +6 | +6 | +
Set opacity to 70% | +7 | +7 | +
Set opacity to 80% | +8 | +8 | +
Set opacity to 90% | +9 | +9 | +
Set opacity to 100% | +0 | +0 | +
Ungroup | +ShiftG | +⇧G | +
Unmask | +CtrlShiftM | +⌘⇧M | +
Shortcut | +Linux and Windows | +macOS | +
---|---|---|
Go to dashboard | +G and then D | +G and then D | +
Go to view mode inspect section | +G and then H | +G and then H | +
Shortcut | +Linux and Windows | +macOS | +
---|---|---|
Assets | +AltI | +⌥I | +
Color palette | +AltP | +⌥P | +
History | +AltH | +⌥H | +
Layers | +AltL | +⌥L | +
Show/hide UI | +\ | +\ | +
Text palette | +AltT | +⌥T | +
Shortcut | +Linux and Windows | +macOS | +
---|---|---|
Add node | +Shift+ | +⇧+ | +
Delete node | +Supr or Delete | +⌫ | +
Draw Path | +P | +P | +
Join nodes | +J | +J | +
Make corner | +X | +X | +
Make curve | +C | +C | +
Merge nodes | +CtrlJ | +⌘J | +
Move Nodes | +M | +M | +
Separate nodes | +K | +K | +
Snap to nodes | +Ctrl' | +⌘' | +
Shortcut | +Linux and Windows | +macOS | +
---|---|---|
Boolean difference | +CtrlAltD | +⌘⌥D | +
Boolean exclude | +CtrlAltE | +⌘⌥E | +
Boolean intersection | +CtrlAltI | +⌘⌥I | +
Boolean union | +CtrlAltU | +⌘⌥U | +
Shortcut | +Linux and Windows | +macOS | +
---|---|---|
Board | +B | +B | +
Curve | +CtrlC | +⌘C | +
Ellipse | +E | +E | +
Image | +ShiftK | +⇧K | +
Path | +P | +P | +
Rectangle | +R | +R | +
Text | +T | +T | +
Flex Layout | +ShiftA | +ShiftA | +
Grid Layout | +CtrlShiftA | +⌘ShiftA | +
Color picker | +I | +I | +
Comments | +C | +C | +
Lock Proportions | +ShiftL | +⇧L | +
Lock selected | +CtrlShiftL | +⌘⇧L | +
Move | +V | +V | +
Toggle focus mode | +F | +F | +
Toggle scale text | +K | +K | +
Toggle visibility | +CtrlShiftH | +⌘⇧H | +
Shortcut | +Linux and Windows | +macOS | +
---|---|---|
Reset zoom to 100% | +Shift 0 | +⌘ ⇧ 0 | +
Zoom in | ++ Ctrl Scrollwheel Pinch out (trackpad) |
+ + Option Scrollwheel Option-swipe (Magic Mouse) Pinch out (trackpad) |
+
Zoom to fit all | +Shift 1 | +⌘ ⇧ 1 | +
Zoom to selected | +Shift 2 | +⌘ ⇧ 2 | +
Zoom out | +- Ctrl Scrollwheel Pinch in (trackpad) |
+ - Option Scrollwheel Option-swipe (Magic Mouse) Pinch in (trackpad) |
+
Zoom lense in | +Z | +Z | +
Zoom lense out | +Alt Z | +⌥ Z | +
Shortcut | +Linux and Windows | +macOS | +
---|---|---|
Bold | +Ctrl B | +⌘ B | +
Italic | +Ctrl I | +⌘ I | +
Underline | +Ctrl U | +⌘ U | +
Strikethrough | +Shift Alt 5 | +⇧ ⌥ 5 | +
Increase font size | +Ctrl Shift RIGHT | +⌘ ⇧ RIGHT | +
Decrease font size | +Ctrl Shift LEFT | +⌘ ⇧ LEFT | +
Increase letter spacing | +Alt UP | +⌥ UP | +
Decrease letter spacing | +Alt DOWN | +⌥ DOWN | +
Increase line height | +Shift Alt UP | +⇧ ⌥ UP | +
Decrease line height | +Shift Alt DOWN | +⇧ ⌥ DOWN | +
Align left | +Ctrl Alt L | +⌘ ⌥ L | +
Align right | +Ctrl Alt R | +⌘ ⌥ R | +
Align center | +Ctrl Alt T | +⌘ ⌥ T | +
Justify | +Ctrl Alt J | +⌘ ⌥ J | +
The Dashboard is the place where you will be able to organize your files, libraries, projects and teams. More about the Dashboard.
+ +Shortcut | +Linux and Windows | +macOS | +
---|---|---|
Switch color theme | +AltM | +⌥M | +
Create new project | ++ | ++ | +
Create new file (Inside project) | ++ | ++ | +
Shortcut | +Linux and Windows | +macOS | +
---|---|---|
Go to shared libraries | +G and then L | +G and then L | +
Go to drafts | +G and then D | +G and then D | +
Search | +CtrlF | +⌘F | +
The View mode is the area to present and share designs and play the proptotype interactions. More about the View mode.
+ +Shortcut | +Linux and Windows | +macOS | +
---|---|---|
Next frame | +→ | +→ | +
Previous frame | +← | +← | +
Select all | +CtrlA | +⌘A | +
Shortcut | +Linux and Windows | +macOS | +
---|---|---|
Go to comment section | +G and then C | +G and then C | +
Go to inspect section | +G and then H | +G and then H | +
Go to interactions section | +G and then V | +G and then V | +
Go to workspace | +G and then W | +G and then W | +
Shortcut | +Linux and Windows | +macOS | +
---|---|---|
Zoom in | ++ Ctrl Scrollwheel Pinch out (trackpad) |
+ + Option Scrollwheel Option-swipe (Magic Mouse) Pinch out (trackpad) |
+
Zoom out | +- Ctrl Scrollwheel Pinch in (trackpad) |
+ - Option Scrollwheel Option-swipe (Magic Mouse) Pinch in (trackpad) |
+
Reset zoom to 100% | +Shift 0 | +⇧ 0 | +
Toggle zoom style | +F | +F | +
Toggle fullscreen | +Shift F | +⇧ F | +
Every object you create in Penpot’s viewport is a layer. Rectangles, ellipses, boards or text boxes are layers that you can use to build your design.
+ +Pages allow you to organize layers into separate sections inside a file, and are shown in separate tabs. Subdividing a file into pages gives you the ability to split your file into logically different sections so that you can organise your work. For instance, you can use pages to separate stages of the design process but keep them in the same document. Different screen sizes, features or atomic design categories are other common ways to use pages.
+ +You can add, remove or rename pages to suit your needs.
+ + +Layers: Layers are the different objects that you can place at the design viewport. At the layers panel you can see all the layers of a file page. Drag the layers to arrange them to different positions.
+ +Layers are displayed from the bottom to the top of the layer stack, with layers above on the stack being shown on top in the image.
+ +Click on the eye icon to change the visibility of a layer. Click on the lock icon to lock or unlock a layer. A locked layer can not be modified.
+ + +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.
+Hold Shift/⇧ while creating an ellipse or a rectangle to maintain equal width and height.
+ + +There are several ways to duplicate a layer:
+There are a couple ways to delete a layer.
+The simplest way to select a layer is to click on it. Make sure that you have the “move” pointer selected at the toolbar.
+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 Shift/⇧. If you hold Shift/⇧ and click you can deselect layers individually.
+ + +If you want to select an element that is difficult to reach because it is under a group of elements, hold Ctrl/⌘ to make the selection ignore group areas and treat all the objects as being at the same level.
+ +To select a layer inside a group you do double click. First click selects the group, second click selects a layer.
+ +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.
+ + + +Grouped layers can be moved, transformed or styled at the same time.
+A mask is a layer that does a clipping and only shows parts of a layer or multiple layers that fall within its shape.
+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.
+ + + +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.
+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 Ctrl/⌘ while rotation the angle will change in 45 degree increments. You can also find this option at the design panel.
+ + +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:
+Activate the scale tool by pressing K 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. + + +
Aligning and distributing layers can be found at the top of the Design panel.
+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.
+ +Distributing objects to position them vertically and horizontally with equal distances between them.
+ + +Reach specific layers with a simple search. You can also filter the layers list per layer type (board, group, mask, component, text, image and shape).
+ + +Groups and boards can have their contents expanded and collapsed. Click on the arrow at the +right side to toggle the visibility of their contents.
+To collapse all the layers, and just display the boards, +press Shift/⇧ + left click over the right arrow of a group or a board to collapse them all.
+ + +It is possible to combine shapes in a group in different ways to create more complex objects 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.
+ +Constraints allow you to decide how layers will behave when resizing its container.
+ +Constraints allow you to decide how layers will behave when resizing its parent container. You can apply horizontal and vertical constraints for every layer.
+To apply constraints select a layer and use the constraints map or the constraints selectors at the design panel.
+ +Constraints are set to “Scale” by default, but you have other options.
+ +Select the elements of a page you want to work with in a specific moment hiding the rest so they don’t get in the way of your attention. This option is also useful to improve the performance in cases where the page has a large number of elements.
+To activate focus mode:
+Diversity and inclusion is a major Penpot concern and that's why we love to give support to RTL languages, unlike most design tools.
+If you write in arabic, hebrew or other RTL language text direction will be automatically detected in text layers.
+ diff --git a/docs/user-guide/libraries/index.njk b/docs/user-guide/libraries/index.njk new file mode 100644 index 000000000..f821ff1da --- /dev/null +++ b/docs/user-guide/libraries/index.njk @@ -0,0 +1,202 @@ +--- +title: 09· Asset Libraries +--- + +Asset Libraries allow you to store elements and styles so that they can be easily reused. 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.
+ +In Penpot you can store different type of assets:
+You can use the “+” icon to add assets. Each of the categories have their own specific adding action.
+There are two ways to add components to an assets library:
+Click the “+” to launch the color picker and add a color to the library. Learn more about managing color
+ +All typography styles created from the text properties (at the right sidebar) are automatically stored at the library. You can also click the “+” to create a new typography style from scratch.
+ +Tip: If you select a text layer with certain properties (font family, size, line height...) and click the "+" at the Typographies section at the assets library (left sidebar), the created typography style will include the properties of the text layer.
+ +Press left click over any asset of the library to show the options menu. Some options are available only for certain assets.
+Learn how to better view and organize your assets:
+ +You can switch between list and grid views.
+Click the sort button to change the alphabetical order.
+ + +Use the Search assets box to filter the assets with names that match what you write.
+ + +You can decide whether to show all asset types or only one of your choice (components, colors or typographies).
+ +There are two ways to create groups in a library.
+You can ungroup the assets the same ways you can group them, via the menu option ("Ungroup" in this case) or renaming them.
+ +One very direct way to move assets between groups at the libraries is by dragging them.
+ + + +Each file has its own file library which is where the assets that belong to this file are stored.
+You have two ways to access the file library from the file workspace:
+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.
+There are two ways to publish a library:
+You can unpublish any library anytime the same way you can publish it, both from the file menu and the libraries panel.
+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.
+ +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 publishing other files as a shared library or add from our Libraries & templates.
+ + +You can disconnect any library anytime from the libraries panel just by clicking on the disconnect button.
+ + +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.
+ + +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.
+ diff --git a/docs/user-guide/objects/index.njk b/docs/user-guide/objects/index.njk new file mode 100644 index 000000000..b8392712f --- /dev/null +++ b/docs/user-guide/objects/index.njk @@ -0,0 +1,192 @@ +--- +title: 05· Objects +--- + +Objects are items that you can place in the viewport. Boards, shapes, texts, paths and graphics are objects. The following describes the different objects that you have +available in Penpot, and how to get the most of them.
+ +A Board is a layer typically used as a container for a design. Boards are useful if you want to design for a specific screen or print size. Boards can contain other boards. First level boards +are shown by default at the View mode, acting as screens of a design or pages of a document. Also, objects inside boards can be clipped. Boards are a powerful element at Penpot, opening up a ton of possibilities when creating and organizing your designs.
+ +You can create a board using the board tool at the toolbar or the shortcut B.
+Set a custom size or choose one of the provided presets with the most common resolution for devices and standard print sizes.
+ + +TIP: Create a board around one or more selected objects using the option "Selection to board" at the menu or the shortcut Ctrl/⌘ + Alt + G.
+ +There are two different cases in terms of selecting boards:
+Select a specific board to be the file thumbnail that will be shown at the dashboard in the file card.
+To set a custom thumbnail:
+Boards offer the option to clip its content (or not).
+ + +Boards offer the option to be shown as a separate board/screen in the View mode. Use this setting to decide what boards should be shown as individual items in your presentations.
+Defaults
+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.
+Sometimes you don’t 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.
+ + +You can set guides on boards that will assist with aligning objects.
+Read more about guides.
+ + +You can connect boards with other boards to create rich interactions.
+Read more about prototyping.
+ + +Rectangle and ellipses are two basic “primitive” geometric shapes that are useful when starting +a design.
+The shortcut keys are E for ellipses and R for rectangles.
+To find out more about how to edit and modify these shapes go to Layer basics.
+ + + +To insert text you have to activate the text tool by first clicking on the icon at the toolbar or pressing T. Then you have two ways to create a text layer:
+Press Enter with a text layer selected to start editing the text content. You can style parts of the text content as rich text.
+ +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 Ctrl/⌘ + c. +
The path created will contain a lot of points, but it is edited the same way as any other curve.
+ +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 P. Then you have two ways to create the path:
+To finish the path:
+Tip: If you hold Shift/⇧ while adding nodes the angle between the current and the next will change in 45 degree increments.
+ + +To edit a node double click on a path or select and press Enter. +You can choose to edit individual nodes or create new ones. Press Esc to exit node edition.
+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.
+ + + +There are several options for inserting an image into a Penpot file:
+Images fill the layer backgrounds by default, so they take up the entire object while maintaining the aspect ratio. This is great for flexible designs because the images can adapt to different sizes.
+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.
+ diff --git a/docs/user-guide/plugins/index.njk b/docs/user-guide/plugins/index.njk new file mode 100644 index 000000000..7ed67f339 --- /dev/null +++ b/docs/user-guide/plugins/index.njk @@ -0,0 +1,49 @@ +--- +title: 17· Plugins +--- + +Plugins are the perfect tool to extend Penpot's functionalities. You can install available plugins or create one that fits your needs!
+ +You can find available Plugins at the Penpot Hub and install plugins directly from the Plugins page by clicking on Install next to the desired plugin.
+ + + +Another way to install a plugin is by copying the URL (with .json extension) provided by the plugin’s author and pasting it on to the Plugin Manager.
+ + + +Please note that you'll need to grant access to your files. Plugins may be created by external parties, so ensure you trust it before granting access. Your data privacy and security are important to us. If you have any concerns, please contact support.
+If a plugin is later updated by the author and requires new permissions, we will notify you so that you can update it.
+ + + +Once a plugin has been installed you can access it on your files, as long as you have permissions to access it, in any of your teams. If another member of your team wants to use the plugin, they will need to install it individually.
+ +To start using Plugins you first need to open the Plugin Manager. There are a few different ways to access the Plugins Manager in the Worskpace:
+To use a plugin, go to the Plugin Manager, and click on Open next to the desired plugin, and that’s it, enjoy!
+ + + +You can create your own plugin from scratch or use a Template to get started. You can find the complete guide to creating Plugins at the Technical Guide.
diff --git a/docs/user-guide/prototyping/index.njk b/docs/user-guide/prototyping/index.njk new file mode 100644 index 000000000..5242877ba --- /dev/null +++ b/docs/user-guide/prototyping/index.njk @@ -0,0 +1,206 @@ +--- +title: 11· Prototyping +--- + +Learn how to build interactive prototypes to visualize how users navigate through your screens and mimic your product behaviour.
+ +
Penpot allows you to prototype interactions by connecting boards, which can act as screens. Once the prototype is prepared with interactions and/or flows, it can be used at the View mode and shared through a link.
+ +
The simplest and most usual way to prototype an interaction at Penpot is connecting boards. Add interactions following this simple steps:
++ 1) Hotspot (origin connection) + 2) Connector wire + 3) Destination + 4) Prototype mode tab + 5) View mode launcher + 6) Add new interaction + 7) Interaction trigger + 8) Interaction action + 9) Interaction animation + 10) Flow indicator and launcher +
+ +A trigger defines the user action that will start the interaction. Penpot currently provides the following triggers (more of them will come):
+The action defines what will happen when the interaction is triggered. Penpot currently provides the following actions (more of them will come):
+The classic, most usual of the prototyping actions. It takes the user from one board to the destination set in the interaction.
+ +It opens a board right over the current board. This action is typically used to display tooltips, modal windows or notifications.
+ +You have several presets for positioning the overlay (center, top left, top right...) but you can also do it manually. Just select the “Manual” option and drag the “ghost” (an area with the size of the destination board) to the place you want the overlay to show up.
+There are also a couple of options that facilitates to mimic typical overlay behaviours:
+TIP: You can select to open the overlay relative to the element that triggers it using the field "relative to". This is extremely useful for hover effects.
+ +It opens an overlay if it is not already opened or closes it if it is already opened.
+ +This action will close a targeted board that is opened as an overlay. It can be also set to “self” so the board can be closed from itself.
+ +It takes back to the last board shown. This action is typically used for back buttons, when the same screen can be accessed from different origins.
+ +This action opens an URL in a new tab. This is useful to add external links outside of the prototype.
+ + +The animation defines the transition between boards when the interaction is triggered. Penpot currently provides the following actions (more of them will come):
+Dissolve means that the destination board fades in while the origin board fades out.
+Options:
+Slide animation will move the destination frame in or out of the view.
+Options:
+Offset effect means that the origin board will slightly fade and move in the same direction of the destination board.
+ +Push animations will move the destination board into view pushing out the origin board.
+Design projects usually need to define multiple casuistics for different devices and user journeys. Flows allow you to define one or multiple starting points within the same page so you can better organize and present your prototypes.
+ +A flow is defined by a starting board for an interaction. Flows can be selected independently at the view mode. Each flow has its own shareable link at the View mode.
+ + +A starting point is a board selected to be the first screen of a flow. You could set a board as a starting point just because you want this board to be the first one to be shown in the view mode, but you can also set more starting points to define different user journeys.
+There are several ways to create starting points:
+You can add as many flows as you want. The complete list of flows is shown at the prototype sidebar when no shape is selected.
+ + +At the view mode there’s a menu where you can access to all the flows set and easily switch between them.
+ + +You can fix the position of an object when scrolling at the presentation view. This is tipically useful for prototyping fixed headers, navbars and floating buttons.
+ +Select an element and check the option "Fix when scrolling" that you can find inside the Constraints section at the Design sidebar (right side).
+ +Launch the View Mode to see the elements with a fixed scroll position.
diff --git a/docs/user-guide/styling/index.njk b/docs/user-guide/styling/index.njk new file mode 100644 index 000000000..4da6ff2d3 --- /dev/null +++ b/docs/user-guide/styling/index.njk @@ -0,0 +1,158 @@ +--- +title: 06· Styling +--- + +Penpot has a variety of styling options for each object. When selected, the styling options are displayed in the design panel on the right.
+ +Color fills can be added to boards, shapes, texts and groups of layers.
+You can add as fills:
+To apply a fill you can use: the color picker, the color palette or a color style.
+You can also set the opacity for custom fill colors.
+TIP: Select an element and press numbers from 0 to 9 to set their fill opacity. 1 to get 10%, 2 to get 20% and so on. You can set precise opacity by pressing two numbers consecutively in less than a second (for example 5 and 4 to set 54% opacity).
+ +You can add as many fills as you want to the same layer. This opens endless graphic possibilities like combining gradients and blending modes in the same element to create unique visual effects.
+ + +To remove a fill from a selected object, press the “-” button in the fill section.
+ + +Here you have the anatomy of the color picker:
+ +The color palette allows you to have a selected color library in plain sight.
+ +There are three ways to show/hide the color palette:
+Use the menu to easily switch between libraries.
+Switch between big and small thumbnails sizes.
+ +All of the colors that are contained within a selection of objects are showcased at the sidebar so you can play with the colors of a group without the hassles of individual selection.
+ + +Strokes can be added to most of the objects at Penpot (rectangles, ellipses, boards, curves and images).
+ +Stroke options are:
+You can add as many strokes as you want to the same layer.
+ + +Ever needed an arrow to point something? You can style the ends of any open paths selecting different styles for each end of an open path.
+ + +The stroke caps options are:
+You can set values for corner radius to rectangles and images. There’s also the option to edit each corner individually.
+ + + +Adding shadows is easy from the design panel. You can add as many as you want.
+ +Shadow options are:
+You can set a blur for each and every object at Penpot.
+Applying a lot and/or big values for blurs can affect Penpot’s performance as it requires a lot from the browser.
+ \ No newline at end of file diff --git a/docs/user-guide/teams/index.njk b/docs/user-guide/teams/index.njk new file mode 100644 index 000000000..96d796813 --- /dev/null +++ b/docs/user-guide/teams/index.njk @@ -0,0 +1,73 @@ +--- +title: 15· Teams +--- + +A team is a group of members who collaborate on a collection of projects. +Team members are allowed to work with any project or file within the team. The actions that each team +member is allowed to do depends on their permissions.
+At Penpot you can create and join as many teams as you need and add all necessary stakeholders with no team size limits.
+ +At Penpot you can create as many teams as you need and be invited to teams owned by others. Learn how to manage them.
+ +At the top left of the dashboard you can find the team selector.
+ +"Your Penpot" is the name of your personal space at Penpot. It is like any other team but in which no members can be invited so that you will always have your own private dashboard. Create or join other teams to collaborate with other Penpot users.
+ + +To create a new team go to the bottom of the team selector and press "+ Create new team". Then you will be asked to enter a team name and that's it. Once a new team is created you are able to invite new team members.
+ + +All members can leave the team anytime from the same menu.
+Only the team owner can delete a team. The option can be found at the team menu (the three dots at the right side). When deleting a team all projects and files belonging to it will be permanently deleted.
+ + +At the team settings you can see the information about how many members, projects and files belong to it. Team name and profile picture can be updated.
+ +At the team members area you can view all the users that are inside the team and manage them according to your permissions.
+ + +These are the team roles currently available at Penpot:
+More team roles will be eventually available, as well as fine grained permissions management to control members access and actions.
+ +An owner can transfer their ownership to another team member anytime and is requested to transfer it before leaving the team.
+ + +You can invite people to join the team using the "Invite to team" window. Add their emails separated by comma, select the role that will be assigned to them and press "Send invitation". An invitation will be sent to each of the added emails that still will need to be accepted.
+ + +Check the status of a team invitations at the "Invitations" section. Invitations can be pending (still valid but not accepted yet) or expired.
+ + +You can perform the following actions over existing invitations:
+Webhooks allow other websites and apps to be notified when certain events happen on Penpot, ensuring to create integrations with other services. While we are still working on a plugin system, this is a clever and simple way to create integrations with other services.
+ + +You can find detailed info about Penpot webhooks at the Technical Guide.
diff --git a/docs/user-guide/the-interface/index.njk b/docs/user-guide/the-interface/index.njk new file mode 100644 index 000000000..469c1aea9 --- /dev/null +++ b/docs/user-guide/the-interface/index.njk @@ -0,0 +1,173 @@ +--- +title: 02· The interface +--- + +The Penpot interface has three main areas: Dashboard, Workspace and View mode. Lets take a look at their composition and main features.
+ + +The Dashboard is the place where you will be able to organize your files, libraries, projects and teams.
+ + ++ 1) Teams + 2) Search files + 3) Projects + 4) Drafts + 5) Shared Libraries + 6) Custom fonts + 7) Pinned projects + 8) User area + 9) Comment notifications + 10) Create project + 11) File card + 12) Libraries & Templates module +
+ +Your account settings can be changed at the user area, in Your account. Here you can make changes to your profile, password or account language, as well as generate personal access tokens and access release notes.
+If you want to change the email address associated to your account or remove your account entirely, this can be done in the Profile section.
+ + + +The Workspace is where you actually create your designs. You have an infinite canvas where you can work directly but you also have the ability to create and work inside boards that will help you to create pages and exportation units.
+ + + ++ 1) Viewport + 2) Toolbar + 3) Main menu + 4) Pages + 5) Layers + 6) Rulers + 7) Color palette + 8) Typography palette + 9) Design properties + 10) Prototype mode + 11) Inspect mode + 12) View mode + 13) History panel + 14) Comments + 15) Zoom + 16) File status + 17) Users + 18) Assets panel +
+ +Launch the view mode to present and share your designs, comment on them and play with the interactions set at the workspace. You also have an Inspect mode where you can get properties specifications and code snippets. More about the View mode.
+ + + ++ 1) Pages selector + 2) Boards selector + 3) Play mode + 4) Comments mode + 5) Inspect mode + 6) Interactions settings + 7) Zoom options + 8) Edit file + 9) Full screen + 10) Share prototype + 11) Boards counter + 12) Reset prototype + 13) Navigation button +
+ +Penpot's default interface is dark but you can switch anytime to a light option. You have 2 ways to change the theme:
+At Penpot, the View mode is the best place to present your designs. You will also be able to share them and play the interactions.
+ +
Take a look at the anatomy of the View mode at this section: View mode interface.
+ + +To view your designs from the workspace at View mode click the play button at the top right of the navbar or press G V.
+Note: the View mode shows only boards and their contents. Anything outside a board will not be shown at the View mode.
+ + +You'll find different capabilities and options to help you present and test your designs.
+You can navigate through boards by pressing the → and ← keyboard keys or the navigation buttons at the right and left of the screen.
+ +The View mode is the place where you will be able to play prototype interactions. (More about prototyping interactions).
+ +This setting allows you to decide how to show a visual cue for interactions: always, on click or just don't show.
+ + +Display the boards list by clicking on the board name at the header and have a nice overview of all the available boards at this page.
+ + +Click at the page name at the header to display the pages menu and change between them.
+ + +Toogle fullscreen Shift+F
+ +Activate the comments by pressing its button at the top navbar.
+Comments mode will not always be available in view mode shared links, this will depend on the link settings.
+ +Activate the inspect mode by pressing its button at the top navbar.
+Inspect mode will not always be available in view mode shared links, this will depend on the link settings.
+ + +You can activate comments at the View mode by pressing the comments icon at the top navbar.
+At the View mode only boards are shown so the comments that are placed outside boards will not be shown here.
+ + + +A "Share prototype link" is a public url that you can share so that someone can see the prototype regardless of whether they have a Penpot account.
+The Share prototype window can be found at the View mode and you can launch it using the "Share button".
+To create a link press the button "Get link" and the link will be automatically created.
+ + +To copy the link you can copy the url or press the copy button.
+ + +To destroy a link press the button "Destroy link" and confirm the action. The link will cease to exist and will be no longer available, so be careful if you've already shared it. However, you can always create a new one.
+ + +Tip: Add your preferred "Interactions" setting (show / don't show / show on click) before creating the Share prototype link. This way the link will keep your selection.
+ +You can create a different link for each set of permissions. Click on "Manage permissions" to edit the link permissions.
+Allowing to share a "view only" workspace is in our plans and will come soon.
+ + +You can activate the Inspect mode from the View mode. Click the code button at the middle of the navbar. Then you will see two sidebars:
+The Workspace is where you create designs. You have an infinite canvas where you can directly work but you also have the ability to create pages and boards that will help you to create exportable components.
+ +Surrounded by panels, header and toolbars, in the middle of the workspace, you can find the viewport. The viewport is the design area of a file page. It is practically infinite. If what you need is a frame with specific, limited dimensions, you can create a board.
+ +Press space and drag to pan (move around the viewport). If you are using a trackpad you can do two finger scrolling.
+You can also use the scrollbars, which are specially useful for those who love using graphic tablets.
+ + + +There's a main menu at the workspace where you will find groups with all the actions that you can do at file level. File, View, Edit, Preferences and Help.
+ + +To zoom in and out hold Ctrl (or ⌘ if using macOS) and use the scroll wheel on your mouse. You also have a bunch of useful shortcuts for the most common zoom levels that you can find at the zoom menu in the navigation bar.
+ + + +Press left click while pressing Z to zoom in to a specific point and Alt/⌥ + Z to zoom out.
+ + + +Double click over a layer icon to zoom to the layer.
+ + +While moving objects at the viewport Penpot will show alignment guides for the edges and the center of the layers at sight. Dynamic alignment also snaps the object that is being moved to those guides to help you align to the center of the edges of other objects.
+ +If there are more than two objects nearby and you drag one of them Penpot will show their distance to help you distribute them equally.
+ + +Penpot has rulers that measure in pixels.
+ + +To create ruler guides click anywhere on the ruler an drag to some point of the viewport. Click on the vertical ruler to create a vertical guide and the horizontal ruler to you know what.
+To delete ruler guides drag the guide to the ruler or select the guide and press delete / supr.
+To show/hide ruler guides use the same shortcut as for rulers: Shift/CMD + Ctrl + R
+ +Guides are design aids that are used to help you to align content to a +geometric structure. In Penpot there are three types of guides: +square, columns and rows.
+ +Note: Guides are only visible in the viewport and will never be shown on exports.
+ +Guides can be added at board level. With a board selected, in the design sidebar you'll find the section "Guides". Click the "+" button to add a guide to the selected board. You can add as many guides as you want.
+ +You can hide a specific guide by clicking at the eye button of a guide configuration. If you want to remove a guide, use the "-" button at the right side of the guide settings.
+ + +The options for square guides are:
+