🎉 Add first version of the docsite

This commit is contained in:
Andrés Moya 2021-02-11 17:10:10 +01:00
parent aee4341b5d
commit d497b3d3f7
50 changed files with 2888 additions and 1918 deletions

View File

@ -1,19 +1,22 @@
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 pluginNavigation = require("@11ty/eleventy-navigation");
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("assassin-custom-plantuml");
const metagen = require('eleventy-plugin-metagen');
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(pluginNavigation);
eleventyConfig.addPlugin(pluginAncestry);
eleventyConfig.addPlugin(metagen);
eleventyConfig.addPlugin(pluginTOC, {
tags: ['h1', 'h2', 'h3']
@ -32,6 +35,12 @@ module.exports = function(eleventyConfig) {
return DateTime.fromJSDate(dateObj, {zone: 'utc'}).toFormat('yyyy-LL-dd');
});
// Remove trailing # in automatic generated toc, because of
// anchors added at the end of the titles.
eleventyConfig.addFilter('stripHash', (toc) => {
return toc.replace(/ #\<\/a\>/g, "</a>");
});
// Get the first `n` elements of a collection.
eleventyConfig.addFilter("head", (array, n) => {
if( n < 0 ) {
@ -41,17 +50,41 @@ module.exports = function(eleventyConfig) {
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.inputContent,
});
});
return index.toJSON();
});
eleventyConfig.addPassthroughCopy("img");
eleventyConfig.addPassthroughCopy("css");
eleventyConfig.addPassthroughCopy("js");
/* Markdown Overrides */
let markdownLibrary = markdownIt({
html: true,
breaks: true,
breaks: false,
linkify: true
}).use(markdownItAnchor, {
permalink: true,
@ -65,7 +98,7 @@ module.exports = function(eleventyConfig) {
eleventyConfig.setBrowserSyncConfig({
callbacks: {
ready: function(err, browserSync) {
const content_404 = fs.readFileSync('_site/404.html');
const content_404 = fs.readFileSync('_dist/404.html');
browserSync.addMiddleware("*", (req, res) => {
// Provides the 404 content without redirect.
@ -105,7 +138,7 @@ module.exports = function(eleventyConfig) {
input: ".",
includes: "_includes",
data: "_data",
output: "_site"
output: "_dist"
}
};
};

View File

@ -1 +1,2 @@
README.md
user-guide

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
# Distribution files
_dist/*
# Logs
logs
*.log

View File

@ -1,2 +1,35 @@
# penpot-docs
Website and documentation about Penpot
Penpot documentation website
## Usage
To view this site locally, first set up the environment:
```
nvm install (if necessary)
nvm use
npm install
```
And launch a development server:
```
npm start
```
You can then point a browser to [http://localhost:8081](http://localhost:8081).
## 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).

View File

@ -1,5 +1,5 @@
{
"title": "Penpot documentation site",
"title": "Help center",
"url": "https://docs.penpot.app/",
"description": "Design freedom for teams.",
"feed": {

File diff suppressed because one or more lines are too long

View File

@ -1,15 +1,34 @@
---
layout: layouts/base.njk
templateClass: tmpl-home
templateClass: tmpl-developer-guide
---
<div class="main-container">
<aside class="sidebar">
{{ content | toc | safe }}
{%- macro show_children(item) -%}
{%- for child in item | children | sorted('data.title') %}
{%- if loop.first -%}<ul>{%- endif -%}
<li>
<a href="{{ child.url }}">{{ child.data.title }}</a>
{{ show_children(child) }}
{%- if child.url == page.url -%}
{{ content | toc(tags=['h2', 'h3']) | stripHash | safe }}
{%- endif -%}
</li>
{%- if loop.last -%}</ul>{%- endif -%}
{%- endfor %}
{%- endmacro -%}
<div class="main-container with-sidebar">
<aside id="stickySidebar" class="sidebar">
{%- set root = '/developer-guide/index' | find -%}
<div id="toc">
<div class="header mobile" id="toc-title">{{ root.data.title }}</div>
<a class="header" href="{{ root.url }}">{{ root.data.title }}</a>
{{ show_children(root) }}
</div>
</aside>
<content class="main-content">
{{ content | safe }}
</content>
</div>
</div>

View File

@ -0,0 +1,10 @@
---
layout: layouts/base.njk
templateClass: tmpl-faqs
---
<div class="main-container">
<div class="main-content">
{{ content | safe }}
</div>
</div>

View File

@ -2,4 +2,6 @@
layout: layouts/base.njk
templateClass: tmpl-home
---
{{ content | safe }}
<div class="main-container">
{{ content | safe }}
</div>

View File

@ -0,0 +1,34 @@
---
layout: layouts/base.njk
templateClass: tmpl-user-guide
---
{%- macro show_children(item) -%}
{%- for child in item | children | sorted('data.title') %}
{%- if loop.first -%}<ul>{%- endif -%}
<li>
<a href="{{ child.url }}">{{ child.data.title }}</a>
{{ show_children(child) }}
{%- if child.url == page.url -%}
{{ content | toc(tags=['h2', 'h3']) | safe }}
{%- endif -%}
</li>
{%- if loop.last -%}</ul>{%- endif -%}
{%- endfor %}
{%- endmacro -%}
<div class="main-container with-sidebar">
<aside class="sidebar">
{%- set root = '/user-guide/index' | find -%}
<div id="toc-title">Table of contents</div>
<div id="toc">
<a href="{{ root.url }}">{{ root.data.title }}</a>
{{ show_children(root) }}
</div>
</aside>
<content class="main-content">
{{ content | safe }}
</content>
</div>

View File

@ -1,44 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Penpot documentation site</title>
<meta name="description" content="Design freedom for teams.">
<link rel="stylesheet" href="/css/index.css">
<link rel="stylesheet" href="/css/prism-base16-monokai.dark.css">
<link rel="alternate" href="/feed/feed.xml" type="application/atom+xml" title="Penpot documentation site">
<link rel="alternate" href="/feed/feed.json" type="application/json" title="Penpot documentation site">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Penpot documentation site</title>
<meta property="og:title" content="Penpot documentation site">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@penpotapp">
<meta name="twitter:title" content="Penpot documentation site">
</head>
<body>
<header>
<h1 class="home"><a href="/">Penpot documentation site</a></h1>
<ul class="nav">
<li class="nav-item"><a href="/">Home</a></li>
</ul>
</header>
<main class="tmpl-home">
<h1 id="content-not-found.">Content not found. <a class="direct-link" href="#content-not-found.">#</a></h1>
<p>Go <a href="/">home</a>.</p>
</main>
<footer>
<a href="https://github.com/penpot/penpot-docs/blob/main/./404.md">Edit this page on GitHub</a>
</footer>
<!-- Current page: /404.html -->
</body>
</html>

View File

@ -1,249 +0,0 @@
:root {
--red: #C5004A;
--darkred: #7F0036;
--lightgray: #e0e0e0;
--gray: #C0C0C0;
--darkgray: #333;
--navy: #17050F;
--blue: #082840;
--white: #fff;
}
* {
box-sizing: border-box;
}
html,
body {
padding: 0;
margin: 0;
font-family: system-ui, sans-serif;
color: var(--darkgray);
background-color: var(--white);
}
p:last-child {
margin-bottom: 0;
}
p,
.tmpl-post li,
img {
max-width: 37.5em; /* 600px /16 */
}
p,
.tmpl-post li {
line-height: 1.45;
}
a[href] {
color: var(--blue);
}
a[href]:visited {
color: var(--navy);
}
main {
padding: 1rem;
}
main :first-child {
margin-top: 0;
}
header {
border-bottom: 1px dashed var(--lightgray);
}
header:after {
content: "";
display: table;
clear: both;
}
table {
margin: 1em 0;
}
table td,
table th {
padding-right: 1em;
}
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: #f6f6f6;
}
.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 */
.home {
padding: 0 1rem;
float: left;
margin: 1rem 0; /* 16px /16 */
font-size: 1em; /* 16px /16 */
}
.home :link:not(:hover) {
text-decoration: none;
}
/* Nav */
.nav {
padding: 0;
list-style: none;
float: left;
margin-left: 1em;
}
.nav-item {
display: inline-block;
margin-right: 1em;
}
.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(--darkgray);
}
.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;
}
footer {
padding: 1rem;
display: flex;
justify-content: center;
}
/* 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: var(--red);
color: var(--white);
border-radius: 0.25em; /* 3px /12 */
text-decoration: none;
}
a[href].post-tag,
a[href].post-tag:visited {
color: #fff;
}
/* Warning */
.warning {
background-color: #ffc;
padding: 1em 0.625em; /* 16px 10px /16 */
}
.warning ol:only-child {
margin: 0;
}
/* Direct Links / Markdown Headers */
.direct-link {
font-family: sans-serif;
text-decoration: none;
font-style: normal;
margin-left: .1em;
}
a[href].direct-link,
a[href].direct-link:visited {
color: transparent;
}
a[href].direct-link:focus,
a[href].direct-link:focus:visited,
:hover > a[href].direct-link,
:hover > a[href].direct-link:visited {
color: #aaa;
}
/* Particular classes */
.main-title {
text-align: center;
}
.media-container {
width: 100%;
display: flex;
justify-content: center;
}
.main-container {
display: grid;
grid-template-columns: minmax(100px, 25%) 1fr;
}

View File

@ -1,91 +0,0 @@
/* 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;
}
pre[class*="language-"] {
padding: 1.5em 0;
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;
}

View File

@ -1,2 +0,0 @@
# For Apache, to show `feed.xml` when browsing to directory /feed/ (hide the file!)
DirectoryIndex feed.xml

View File

@ -1,13 +0,0 @@
{
"version": "https://jsonfeed.org/version/1",
"title": "Penpot documentation site",
"home_page_url": "https://docs.penpot.app/",
"feed_url": "https://docs.penpot.app/feed/feed.json",
"description": "Design freedom for teams.",
"author": {
"name": "Penpot",
"url": "https://penpot.app"
},
"items": [
]
}

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Penpot documentation site</title>
<subtitle>Penpot: design freedom for teams.</subtitle>
<link href="https://docs.penpot.app/feed/feed.xml" rel="self"/>
<link href="https://docs.penpot.app/"/>
<updated>2021-02-11T13:10:17Z</updated>
<id>https://docs.penpot.app/</id>
<author>
<name>Penpot</name>
<email>hello@penpot.app</email>
</author>
</feed>

View File

@ -1,49 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Penpot documentation site</title>
<meta name="description" content="Design freedom for teams.">
<link rel="stylesheet" href="/css/index.css">
<link rel="stylesheet" href="/css/prism-base16-monokai.dark.css">
<link rel="alternate" href="/feed/feed.xml" type="application/atom+xml" title="Penpot documentation site">
<link rel="alternate" href="/feed/feed.json" type="application/json" title="Penpot documentation site">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Penpot documentation site</title>
<meta property="og:title" content="Penpot documentation site">
<meta property="og:type" content="website">
<meta property="og:image" content="img/placeholder.png">
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@penpotapp">
<meta name="twitter:title" content="Penpot documentation site">
<meta name="twitter:image" content="img/placeholder.png">
</head>
<body>
<header>
<h1 class="home"><a href="/">Penpot documentation site</a></h1>
<ul class="nav">
<li class="nav-item nav-item-active"><a href="/">Home</a></li>
</ul>
</header>
<main class="tmpl-home">
<h1 class="main-title">Penpot documentation site</h1>
<p>Lorem ipsum dolor...</p>
</main>
<footer>
<a href="https://github.com/penpot/penpot-docs/blob/main/./index.njk">Edit this page on GitHub</a>
</footer>
<!-- Current page: / -->
</body>
</html>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://docs.penpot.app/</loc>
<lastmod>2021-02-11</lastmod>
</url>
</urlset>

View File

@ -1,12 +1,13 @@
:root {
--red: #C5004A;
--darkred: #7F0036;
--lightgray: #e0e0e0;
--gray: #C0C0C0;
--darkgray: #333;
--navy: #17050F;
--blue: #082840;
--darkest: #000000;
--graydark: #1F1F1F;
--graymedium: #7B7D85;
--graylight: #E3E3E3;
--primary: #31EFB8;
--primarydark: #00af7d;
--white: #fff;
--advice: #dafffb;
}
* {
box-sizing: border-box;
@ -15,42 +16,69 @@ html,
body {
padding: 0;
margin: 0;
font-family: system-ui, sans-serif;
color: var(--darkgray);
font-family: 'Source Sans Pro',system-ui, sans-serif;
color: var(--graydark);
}
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 {
max-width: 37.5em; /* 600px /16 */
img,
h1, h2, h3,
.main-content {
max-width: 42rem;
position: relative;
}
p,
.tmpl-post li {
line-height: 1.45;
li {
line-height: 1.6;
}
ul, ol {
margin-bottom: 2rem;
}
li {
margin-bottom: 1rem;
}
a[href] {
color: var(--blue);
color: var(--primarydark);
}
a[href]:hover {
color: var(--darkest);
}
a[href]:visited {
color: var(--navy);
/* color: var(--primarydark); */
}
main {
padding: 1rem;
}
main :first-child {
margin-top: 0;
background-image: url(/img/bg.png);
background-repeat: no-repeat;
background-position: center 120px;
}
header {
border-bottom: 1px dashed var(--lightgray);
display: flex;
border-bottom: 1px solid var(--graylight);
padding: 2rem;
position: fixed;
width: 100%;
background-color: white;
z-index: 10;
}
header:after {
content: "";
display: table;
clear: both;
}
header a[href] {
color: var(--graydark);
}
table {
margin: 1em 0;
}
@ -59,6 +87,40 @@ table th {
padding-right: 1em;
}
h1 {
font-size: 5rem;
}
h2 {
font-size: 3rem;
margin-top: 6rem;
}
h3 {
font-size: 2rem;
margin-top: 4rem;
margin-bottom: 1rem;
}
p, li {
font-size: 1.1rem;
word-wrap: break-word;
}
.main-paragraph {
font-size: 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;
}
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;
@ -116,25 +178,41 @@ pre {
/* Header */
.home {
padding: 0 1rem;
float: left;
margin: 1rem 0; /* 16px /16 */
position: absolute;
display: flex;
top: 26px;
left: 2rem;
font-size: 1em; /* 16px /16 */
text-decoration: none;
}
.home svg {
height: 40px;
width: 130px;
}
.home :link:not(:hover) {
text-decoration: none;
}
.home span {
font-size: 1.4rem;
display: inline;
margin: 6px 0 0 16px;
font-weight: normal;
color: var(--graymedium);
}
/* Nav */
.nav {
padding: 0;
display: flex;
list-style: none;
float: left;
margin-left: 1em;
margin: 0 auto;
padding: 0;
}
.nav-item {
display: inline-block;
margin-right: 1em;
margin-left: 1.4rem;
margin-right: 1.4rem;
margin-bottom: 0;
font-size: 1.2rem;
}
.nav-item a[href]:not(:hover) {
text-decoration: none;
@ -162,7 +240,7 @@ pre {
.postlist-date,
.postlist-item:before {
font-size: 0.8125em; /* 13px /16 */
color: var(--darkgray);
color: var(--graydark);
}
.postlist-date {
word-spacing: -0.5px;
@ -179,10 +257,62 @@ pre {
font-weight: 700;
}
footer {
padding: 1rem;
.pre-footer {
padding: 2rem;
margin-top: 2rem;
display: flex;
justify-content: center;
border-top: 1px dashed var(--graylight);
}
footer {
align-items: center;
background-color: var(--graydark);
display: flex;
flex-direction: column;
padding: 4rem 2rem;
width: 100%;
}
.footer-inside {
display: flex;
flex-direction: row;
max-width: 1400px;
width: 100%;
}
.footer-inside .main-logo {
margin-right: auto;
}
.footer-inside .main-logo svg {
fill: #fff;
height: 50px;
width: 140px;
}
.footer-block {
display: flex;
flex-direction: column;
min-width: 20%;
margin: 1rem 0;
}
.footer-block li {
padding: .2rem 0;
}
.footer-block li a {
color: #fff;
font-size: 1.125rem;
text-decoration: none;
transition: color 200ms;
}
.footer-block li a:hover {
color: var(--primary);
}
.footer-text {
align-items: center;
display: flex;
margin-top: 2rem;
}
.footer-text span {
font-size: 1.125rem;
color: var(--graymedium);
}
@ -194,7 +324,7 @@ footer {
font-size: 0.625em; /* 10px /16 */
padding: 2px 4px;
margin-left: 0.8em; /* 8px /10 */
background-color: var(--red);
background-color: red;
color: var(--white);
border-radius: 0.25em; /* 3px /12 */
text-decoration: none;
@ -218,7 +348,9 @@ a[href].post-tag:visited {
font-family: sans-serif;
text-decoration: none;
font-style: normal;
margin-left: .1em;
margin-left: 1rem;
position: absolute;
/* top: 0.4rem; */
}
a[href].direct-link,
a[href].direct-link:visited {
@ -228,22 +360,405 @@ a[href].direct-link:focus,
a[href].direct-link:focus:visited,
:hover > a[href].direct-link,
:hover > a[href].direct-link:visited {
color: #aaa;
color: var(--primarydark);
}
:hover > a[href].direct-link:hover {
color: var(--primary);
}
/* Particular classes */
.main-title {
text-align: center;
}
.media-container {
width: 100%;
display: flex;
justify-content: center;
}
.main-container {
display: grid;
grid-template-columns: minmax(100px, 25%) 1fr;
margin: auto;
max-width: 80rem;
}
.main-container.with-sidebar {
/* display: grid;
grid-template-columns: minmax(100px, 25%) 1fr; */
display: flex;
flex-wrap: wrap;
}
.main-content {
margin: 0 auto;
}
/* .main-container.with-sidebar .main-content {
margin-left: 4rem;
} */
.main-container .sidebar {
padding-top: 4rem;
padding-right: 2rem;
position: sticky;
top: 4rem;
align-self: start;
}
.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;
}
.search {
position: absolute;
right: 2rem;
top: 2rem;
z-index: 1;
}
.search input {
border: 2px solid var(--graylight);
padding: 0.5rem 2rem 0.5rem 0.5rem;
width: 8rem;
transition: width 200ms 200ms;
}
.search label {
position: absolute;
color: var(--graymedium);
left: 10px;
top: 7px;
display: none;
}
.search input:focus {
width: 18rem;
}
.search-icon {
position: absolute;
right: 8px;
top: 9px;
fill: var(--graymedium);
pointer-events: none;
}
.search input:focus + .search-icon {
fill: var(--darkest);
}
#search-results {
background: white;
border: 1px solid var(--graylight);
list-style: none;
margin: 0;
padding: 0;
box-shadow: 0 0 10px rgba(68, 68, 68, 0.1);
height: 85vh;
overflow: auto;
}
#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(--graylight);
color: var(--graymedium);
list-style: none;
margin: 0;
padding: 2rem;
box-shadow: 0 0 10px rgba(68, 68, 68, 0.1);
}
#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%;
background-color: white;
box-shadow: 0px 8px 20px rgb(28 56 71 / 5%);
border: 1px solid var(--graylight);
border-radius: 8px;
min-height: 12rem;
}
.help-sections li:hover {
border-color: var(--primary);
}
.help-sections li > a {
display: block;
padding: 2.6rem;
color: var(--graydark);
text-decoration: none;
}
.help-sections li h3 {
margin-top: 0;
padding-top: 0;
line-height: 1.2;
}
.help-sections li p {
color: var(--graymedium);
font-size: 1.2rem;
}
.help-sections li.no-link {
padding: 2.6rem;
}
.help-sections li.soon:hover {
border-color: var(--graylight);
}
.help-sections li.soon h3 {
opacity: 0.4;
}
.help-sections li .advice {
padding: 1rem;
position: absolute;
top: 1rem;
right: 1rem;
}
.help-sections li.github {
min-height: auto;
border: none;
border-radius: 0;
box-shadow: none;
border-top: 1px dashed var(--graylight);
margin-top: 4rem;
}
.help-sections li.github a:hover h3 {
color: var(--primarydark);
transition: color 200ms ease-in-out;
}
.help-sections li.github a:hover svg {
fill: var(--primarydark);
transition: fill 300ms 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;
}
.main-container h1 {
margin-top: 0rem;
padding-top: 6rem;
}
.main-container h2 {
margin-top: 0rem;
padding-top: 7rem;
}
.main-container h3 {
margin-top: 0rem;
padding-top: 7rem;
}
@media (max-width: 900px) {
hr {
margin-top: 5rem;
}
.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;
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: 6rem;
max-width: none;
z-index: 2;
}
.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;
}
}
@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 {
padding: 1rem;
position: relative;
}
header .nav-item {
margin-left: 0.6rem;
margin-right: 0.6rem;
font-size: 1rem;
}
.search {
top: 0.6rem;
right: 0.6rem;
}
.search input {
width: 2rem;
padding: 0.5rem;
}
.search input:focus {
width: calc(100vw - 1.4rem);
}
.search input:focus + .search-icon {
display: none;
}
.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 {
padding: 1.4rem;
}
}
/* todo: fijar sidebar y header, imgs de bg */

View File

@ -17,9 +17,10 @@ code[class*="language-"], pre[class*="language-"] {
hyphens: none;
background: #272822;
color: #f8f8f2;
max-width: 40rem;
}
pre[class*="language-"] {
padding: 1.5em 0;
padding: 1.5em 1em;
margin: .5em 0;
overflow: auto;
}

View File

@ -0,0 +1,119 @@
---
title: 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
PosgreSQL 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 below.
@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", "Clojure / JVM", "", "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
## 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.
## Worker
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.
## 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.
## Broker
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 send to the user via the websocket.
## Exporter
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.

View File

@ -0,0 +1,253 @@
---
title: 2. Configuration guide
---
# Configuration Guide #
This section intends to explain all available configuration options.
## Backend ##
The default approach for pass options to backend application is using
environment variables. Almost all environment variables starts with
the `PENPOT_` prefix.
NOTE: All the examples that comes with values, they represent the
**default** values.
### Configuration Options
#### Database Connection
```bash
PENPOT_DATABASE_USERNAME=penpot
PENPOT_DATABASE_PASSWORD=penpot
PENPOT_DATABASE_URI=postgresql://127.0.0.1/penpot
```
The username and password are optional.
#### Email (SMTP)
```bash
PENPOT_SMTP_DEFAULT_REPLY_TO=no-reply@example.com
PENPOT_SMTP_DEFAULT_FROM=no-reply@example.com
# When not enabled, the emails are printed to the console.
PENPOT_SMTP_ENABLED=false
PENPOT_SMTP_HOST=<host>
PENPOT_SMTP_PORT=25
PENPOT_SMTP_USER=<username>
PENPOT_SMTP_PASSWORD=<password>
PENPOT_SMTP_SSL=false
PENPOT_SMTP_TLS=false
```
#### Storage (assets)
Assets storage is implemented using "plugable" backends. Currently
there are three backends available: `db`, `fs` and `s3` (for AWS S3).
##### fs backend
The default backend is: **fs**.
```bash
PENPOT_STORAGE_BACKEND=fs
PENPOT_STORAGE_FS_DIRECTORY=resources/public/assets`
```
The fs backend is hightly coupled with nginx way to serve files using
`x-accel-redirect` and for correctly configuring it you will need to
touch your nginx config for correctly expose the directory specified
in `PENPOT_STORAGE_FS_DIRECTORY` environment.
For more concrete example look at the devenv nginx configurtion
located in `<repo-root>/docker/devenv/files/nginx.conf`.
**NOTE**: The **fs** storage backend is used for store temporal files
when a user uploads an image and that image need to be processed for
creating thumbnails. So is **hightly recommeded** setting up a correct
directory for this backend independently if it is used as main backend
or not.
##### db backend
In some circumstances or just for convenience you can use the `db`
backend that stores all media uploaded by the user directly inside the
database. This backend, at expenses of some overhead, facilitates the
backups, because with this backend all that you need to backup is the
postgresql database. Convenient for small installations and personal
use.
```bash
PENPOT_STORAGE_BACKEND=db
```
##### s3 backend
And finally, you can use AWS S3 service as backend for assets
storage. For this you will need to have AWS credentials, an bucket and
the region of the bucket.
```bash
AWS_ACCESS_KEY_ID=<you-access-key-id-here>
AWS_SECRET_ACCESS_KEY=<your-secret-access-key-here>
PENPOT_STORAGE_BACKEND=s3
PENPOT_STORAGE_S3_REGION=<aws-region>
PENPOT_STORAGE_S3_BUCKET=<bucket-name>
```
Right now, only `eu-central-1` region is supported. If you need others, open an issue.
#### Redis
The redis configuration is very simple, just provide with a valid redis URI. Redis is used
mainly for websocket notifications coordination.
```bash
PENPOT_REDIS_URI=redis://localhost/0
```
#### HTTP Server
```bash
PENPOT_HTTP_SERVER_PORT=6060
PENPOT_PUBLIC_URI=http://localhost:3449
PENPOT_REGISTRATION_ENABLED=true
# comma-separated domains, defaults to `""` which means that all domains are allowed)
PENPOT_REGISTRATION_DOMAIN_WHITELIST=""
```
When disabling registation, remember to also do it [in frontend](#auth-with-3rd-party-2).
#### Server REPL
The production environment by default starts a server REPL where you
can connect and perform diagnosis operations. For this you will need
`netcat` or `telnet` installed in the server.
```bash
$ rlwrap netcat localhost 6062
user=>
```
The default configuration is:
```bash
PENPOT_SREPL_HOST=127.0.0.1
PENPOT_SREPL_PORT=6062
```
#### Auth with 3rd party
**NOTE**: a part of setting this configuration on backend, frontend
application will also require configuration tweaks for make it work.
##### Google
```bash
PENPOT_GOOGLE_CLIENT_ID=<client-id>
PENPOT_GOOGLE_CLIENT_SECRET=<client-secret>
```
##### Gitlab
```bash
PENPOT_GITLAB_BASE_URI=https://gitlab.com
PENPOT_GITLAB_CLIENT_ID=<client-id>
PENPOT_GITLAB_CLIENT_SECRET=<client-secret>
```
##### Github
```bash
PENPOT_GITHUB_CLIENT_ID=<client-id>
PENPOT_GITHUB_CLIENT_SECRET=<client-secret>
```
##### LDAP
```bash
PENPOT_LDAP_AUTH_HOST=
PENPOT_LDAP_AUTH_PORT=
PENPOT_LDAP_AUTH_VERSION=3
PENPOT_LDAP_BIND_DN=
PENPOT_LDAP_BIND_PASSWORD=
PENPOT_LDAP_AUTH_SSL=false
PENPOT_LDAP_AUTH_STARTTLS=false
PENPOT_LDAP_AUTH_BASE_DN=
PENPOT_LDAP_AUTH_USER_QUERY=(|(uid=$username)(mail=$username))
PENPOT_LDAP_AUTH_USERNAME_ATTRIBUTE=uid
PENPOT_LDAP_AUTH_EMAIL_ATTRIBUTE=mail
PENPOT_LDAP_AUTH_FULLNAME_ATTRIBUTE=displayName
PENPOT_LDAP_AUTH_AVATAR_ATTRIBUTE=jpegPhoto
```
## Frontend ##
In comparison with backend frontend only has a few number of runtime
configuration options and are located in the
`<dist-root>/js/config.js` file. This file is completly optional; if
it exists, it is loaded by the main index.html.
The `config.js` consists in a bunch of globar variables that are read
by the frontend application on the bootstrap.
### Auth with 3rd party
If any of the following variables are defined, they will enable the
corresponding auth button in the login page
```js
var penpotGoogleClientID = "<google-client-id-here>";
var penpotGitlabClientID = "<gitlab-client-id-here>";
var penpotGithubClientID = "<github-client-id-here>";
var penpotLoginWithLDAP = <true|false>;
```
You can disable new user registration, if you want only 3rd party
authorized users:
```js
var penpotRegistrationEnabled = <true|false>;
```
**NOTE:** The configuration should match the backend configuration for
respective services.
### Demo warning and Demo users
It is possible to display a warning message on a demo environment and
disable/enable demo users:
```js
var penpotDemoWarning = <true|false>;
var penpotAllowDemoUsers = <true|false>;
```
**NOTE:** The configuration for demo users should match the backend
configuration.
## Exporter ##
The exporter application only have a single configuration option and
it can be provided using environment variables in the same way as
backend.
```bash
PENPOT_PUBLIC_URI=http://pubic-domain
```
This environment variable indicates where the exporter can access to
the public frontend application (because it uses special pages from it
to render the shapes in the underlying headless web browser).

View File

@ -0,0 +1,81 @@
---
title: 3.3. Backend guide
---
# Backend guide
This guide intends to explain the essential details of the backend
application.
## 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 executing the
`bin/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 aplication. 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.
If you have a REPL access to the running process, you can execute it
from there:
```clojure
(require 'app.cli.fixtures)
(app.cli.fixtures/run :small)
```
To access to the running process repl you usually will execute this
command:
```bash
rlwrap netcat localhost 6062
```
## 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:
```
XXXX-<add|mod|del|drop|[...verb...]>-<table-name>-<any-additional-text>
```
Examples:
```
0025-del-generic-tokens-table
0026-mod-profile-table-add-is-active-field
```
**NOTE**: if table name has more than one words, we still use `-` 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
```

View File

@ -0,0 +1,65 @@
---
title: 3.4. Common guide
---
# Common guide
This section intends to have articles that related to both frontend
and backend, such as: code style hints, architecture dicisions, etc...
## 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 on development mode. On production
environment all assets like this will be ignored by runtime.
**spec/assert**: using the `app.common.spec/assert` macro.
Also, if you are using clojure.spec, you have the spec based
`clojure.spec.alpha/assert` macro. In the same way as the
`clojure.core/assert`, on production environment this asserts will be
removed by the compiler/runtime.
Example:
````clojure
(require '[clojure.spec.alpha :as s]
'[app.common.spec :as us])
(s/def ::number number?)
(us/assert ::number 3)
```
In the same way as the `assert` macro, this performs the spec
assertion only on development build. On production this code will
completely removed.
**spec/verify**: An assertion type that is executed always.
Example:
```clojure
(require '[app.common.spec :as us])
(us/verify ::number 3)
```
This macro enables you have assetions on production code.
**Why don't use the `clojure.spec.alpha/assert` instead of the `app.common.spec/assert`?**
The Penpot variant does not peforms additional runtime checks for know
if asserts are disabled in "runtime". As a result it generates much
simplier code at development and production builds.

View File

@ -0,0 +1,138 @@
---
title: 3.1. Development environment
---
# Development environment
## System requirements
You should have `docker` and `docker-compose` installed in your system
in order to set up properly the development enviroment.
In debian like linux distributions you can install it executing:
```bash
sudo apt-get install docker docker-compose
```
Start and enable docker environment:
```bash
sudo systemctl start docker
sudo systemctl enable docker
```
Add your user to the docker group:
```bash
sudo usermod -aG docker $USER
```
And finally, increment user watches:
```
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
```
NOTE: you probably need to login again for group change take the effect.
## Start the devenv
**Requires a minimum knowledge of tmux usage in order to use that
development environment.**
For start it, staying in this repository, execute:
```bash
./manage.sh pull-devenv
./manage.sh run-devenv
```
This will do the following:
- Pulls the latest devenv image.
- Starts all the containers in the background.
- Attaches to the **devenv** container and executes the tmux session.
- The tmux session automatically starts all the necessary services.
You can execute the individual steps manully if you want:
```bash
./manage.sh build-devenv # builds the devenv docker image (not necessary in normal sircumstances)
./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 volumes, containers and networks used by the devenv
```
Now having the the container running and tmux open inside the
container, you are free to execute any commands and open 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/
### Inside the tmux session
#### gulp
The styles and many related tasks are executed thanks to gulp and they are
executed in the tmux **window 0**. This is a normal gulp watcher with some
additional tasks.
#### shadow-cljs
The frontend build process is located on the tmux **window 1**.
**Shadow-cljs** is used for build and serve the frontend code. For
more information, please refer to `02-Frontend-Developer-Guide.md`.
By default the **window 1** executes the shadow-cljs watch process,
that starts a new JVM/Clojure instance if there is no one running.
Finally, you can start a REPL linked to the instance and the current
connected browser, by opening a third window with `Ctrl+c` and running
`npx shadow-cljs cljs-repl main`.
#### exporter
The exporter app (clojurescript app running in nodejs) is located in
**window 2**, and you can go directly to it using `ctrl+b 2` shortcut.
There you will found the window split in two slices. On the top slice
you will have the build process (using shadow-cljs in the same way as
frontend application), and on the bot slice the script that launeches
the node process.
If some reason scripts does not stars correctly, you can manually
execute `node target/app.js ` to start the exporter app.
#### backend
The backend related environment is located in the tmux **window 3**,
and you can go directly to it using `ctrl+b 2` shortcut.
By default the backend will be started in non-interactive mode for
convenience but you can just press `Ctrl+c` and execute `./scripts/repl`
for start the repl.
On the REPL you have this 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 some exception is raised when code is reloaded, just use
`(repl/refresh-all)` in order to finish correctly the code swaping and
later use `(restart)` again.
For more information, please refer to: `03-Backend-Guide.md`.

View File

@ -0,0 +1,286 @@
---
title: 3.2. Frontend guide
---
# Frontend guide
This guide intends to explain the essential details of the frontend
application.
## Visual debug mode and utilities
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.
To activate it, open the javascript console and type
```javascript
app.util.debug.toggle_debug("option")
```
Current options are `bounding-boxes`, `group`, `events` and
`rotation-handler`.
You can also activate or deactivate all visual aids with
```javascript
app.util.debug.debug_all()
app.util.debug.debug_none()
```
## Logging, Tracing & Debugging
As a traditional way for debugging and tracing you have the followimg approach:
Print data to the devtool console using clojurescript helper:
**prn**. This helper automatically formats the clojure and js data
structures as plain EDN for easy visual inspection of the data and the
type of the data.
```clojure
(prn "message" expression)
```
An alternative is using the pprint function, usefull for pretty
printing a medium-big data sturcture for completly understand it.
```clojure
(:require [cljs.pprint :refer [pprint]])
(pprint expression)
; Outputs a clojure value as a string, nicely formatted and with data type information.
```
Use the js native functions for printing data. The clj->js converts
the clojure data sturcture to js data sturcture and it is
inspeccionable in the devtools console.
```clojure
(js/console.log "message" (clj->js expression))
```
Also we can insert breakpoints in the code with this function:
```clojure
(js-debugger)
```
You can also set a breakpoint from the sources tab in devtools. 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.
### Logging framework
Additionally to the traditional way of putting traces in the code, we
have a logging framework with steroids. It is usefull for casual
debugging (as replacement for a `prn` and `js/console.log`) and as a
permanent traces in the code.
You have the ability to specify the logging level per namespace and
all logging is ellided in production build.
Lets start with a simple example:
```clojure
(ns some.ns
(:require [app.util.logging :as log]))
;; This function sets the level to the current namespace; messages
;; with level behind this will not be printed.
(log/set-level! :info)
;; Log some data; The `app.util.logging` has the following
;; functions/macros:
(log/error :msg "error message")
(log/warn :msg "warn message")
(log/info :msg "info message")
(log/debug :msg "debug message")
(log/trace :msg "trace message")
```
Each macro accept arbitrary number of key values pairs:
```clojure
(log/info :foo "bar" :msg "test" :value 1 :items #{1 2 3})
```
Some keys ara treated as special cases for helping in debugging:
```clojure
;; The special case for :js/whatever; if you namespace the key
;; with `js/`, the variable will be printed as javascript
;; inspectionable object.
(let [foobar {:a 1 :b 2}]
(log/info :msg "Some data" :js/data foobar))
;; The special case for `:err`; If you attach this key, the
;; exception stack trace is printed as additional log entry.
```
## Access to clojure from javascript 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 state and objects
There are also some useful functions to visualize the global state or
any complex object. To use them from clojure:
```clojure
(ns app.util.debug)
(logjs <msg> <var>) ; to print the value of a variable
(tap <fn>) ; to include a function with side effect (e.g. logjs) in a transducer.
(ns app.main.store)
(dump-state) ; to print in console all the global state
(dump-objects) ; to print in console all objects in workspace
```
But last ones are most commonly used from javscript console:
```javascript
app.main.store.dump_state()
app.main.store.dump_objects()
```
And we have also exported `pprint` and `clj->js` functions for the console:
```javascript
pp(js_expression) // equivalent to cljs.pprint.pprint(js_expression)
dbg(js_expression) // equivalent to cljs.core.clj__GT_js(js_expression)
```
## Icons & Assets
The icons used on the frontend application are loaded using svgsprite
(properly handled by the gulp watch task). All icons should be on SVG
format located in `resources/images/icons`. The gulp task will
generate the sprite and the embedd it into the `index.html`.
Then, you can reference the icon from the sprite using the
`app.builtins.icons/icon-xref` macro:
```clojure
(ns some.namespace
(:require-macros [app.main.ui.icons :refer [icon-xref]]))
(icon-xref :arrow)
```
For performance reasons, all used icons are statically defined in the
`src/app/main/ui/icons.cljs` file.
## Translations (I18N) ##
### How it Works ###
All the translation strings of this application are stored in
`resources/locales.json` file. It has a self explanatory format that
looks like this:
```json
{
"auth.email-or-username" : {
"used-in" : [ "src/app/main/ui/auth/login.cljs:61" ],
"translations" : {
"en" : "Email or Username",
"fr" : "adresse email ou nom d'utilisateur"
}
},
"ds.num-projects" : {
"translations": {
"en": ["1 project", "%s projects"]
}
},
}
```
For development convenience, you can forget about the specific format
of that file, and just add a simple key-value entry pairs like this:
```
{
[...],
"foo1": "bar1",
"foo2": "bar2"
}
```
The file is 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 for avoid load 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
for refresh the translations in the running the application.
If you have used the short (key-value) format, the watch process will
automatically convert it to the apropriate format before generate the
`index.html`.
Finally, when you have finished to adding texts, execute the following
command for reformat the file, and track the usage locations (the
"used-in" list) before commiting the file into the repository:
```bash
clojure -Adev locales.clj collect src/app/main/ resources/locales.json
```
NOTE: Later, we will need to think and implement the way to export and
import to other formats (mainly for transifex and similar services
compatibility).
### How to use it ###
You have two aproaches for translate strings: one for general purpose
and other specific for React components (that leverages reactivity for
language changes).
The `app.util.i18n/tr` is the general purpose function. This is a
simple use case example:
```clojure
(require '[app.util.i18n :refer [tr])
(tr "auth.email-or-username")
;; => "Email or Username"
```
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:
```clojure
(require '[app.util.i18n :as i18n :refer [tr]])
(tr "ds.num-projects" (i18n/c 10))
;; => "10 projects"
(tr "ds.num-projects" (i18n/c 1))
;; => "1 project"
```

View File

@ -0,0 +1,17 @@
---
title: 3. Core developer guide
---
# Core developer guide
This is a generic "getting started" guide for the Penpot platform. It
intends to explain how to get the development environment up and
running with many additional tips.
The main development environment consists in a docker compose
configuration that starts the external services and the development
container (called **devenv**).
We use tmux script in order to multiplex the single terminal and run
both the backend and frontend in the same container.

View File

@ -0,0 +1,170 @@
---
title: 3.6. Miscellaneous
---
# Miscellaneous
## Collaborative Edition & Persistence protocol
This is a collection of design notes for collaborative edition feature
and persistence protocol (STILL IN CONSTRUCTION).
### Persistence Operations
This is a page data structure:
```clojure
{:version 2
:options {}
:rmap
{:id1 :default
:id2 :default
:id3 :id1}
:objects
{:root
{:type :root
:shapes [:id1 :id2]}
:id1
{:type :canvas
:shapes [:id3]}
:id2 {:type :rect}
:id3 {:type :circle}}}
```
This is a potential list of persistent ops:
```clojure
{:type :mod-obj
:operations [<op>, ...]
{:type :add-obj
:id <uuid>
:parent <uuid>
:obj <shape-object>}
{:type :mod-obj
:id <uuid>
:operations [<op>, ...]}
{:type :mov-obj
:id <uuid>
:frame-id <uuid>}
{:type :del-obj
:id <uuid>}
```
This is a potential list of operations:
```clojure
{:type :set
:attr <any>
:val <any>}
{:type :abs-order
:id <uuid>
:index <int>}
{:type :rel-order
:id <uuid>
:loc <one-of:up,down,top,bottom>}
```
### Ephemeral communication (Websocket protocol)
#### `join`
Sent by clients for notify joining a concrete page-id inside a file.
```clojure
{:type :join
:page-id <id>
:version <number>
}
```
Will cause:
- A posible `:page-changes`.
- Broadcast `:joined` message to all users of the file.
The `joined` message has this aspect:
```clojure
{:type :joined
:page-id <id>
:user-id <id>
}
```
#### `who`
Sent by clients for request the list of users in the channel.
```clojure
{:type :who}
```
Will cause:
- Reply to the client with the current users list:
```clojure
{:type :who
:users #{<id>,...}}
```
This will be sent all the time user joins or leaves the channel for
maintain the frontend updated with the lates participants. This
message is also sent at the beggining of connection from server to
client.
#### `pointer-update`
This is sent by client to server and then, broadcasted to the rest of
channel participants.
```clojure
{:type :pointer-update
:page-id <id>
:x <number>
:y <number>
}
```
The server broadcast message will look like:
```clojure
{:type :pointer-update
:user-id <id>
:page-id <id>
:x <number>
:y <number>
}
```
#### `:page-snapshot`
A message that server sends to client for notify page changes. It can be sent
on `join` and when a page change is commited to the database.
```clojure
{:type :page-snapshot
:user-id <id>
:page-id <id>
:version <number>
:operations [<op>, ...]
}
```
This message is only sent to users that does not perform this change.

View File

@ -0,0 +1,50 @@
---
title: 3.5. Testing guide
---
# Testing guide
## Backend / Common
You can run the tests directly with:
```bash
~/penpot/backend$ clojure -M:dev:tests
```
Alternatively, you can run them from a REPL. First starting a REPL.
```bash
~/penpot/backend$ scripts/repl
```
And then:
```bash
user=> (run-tests)
user=> (run-tests 'namespace)
user=> (run-tests 'namespace/test)
```
## Frontend
Frontend tests have to be compiled first, and then run with node.
```bash
npx shadow-cljs compile tests && node target/tests.js
```
Or run the watch (that automatically runs the test):
```bash
npx shadow-cljs watch tests
```
## Linter
We can execute the linter for the whole codebase with the following command:
```bash
clj-kondo --lint common:backend/src:frontend/src
```

View File

@ -0,0 +1,4 @@
{
"layout": "layouts/developer-guide.njk",
"tags": "developer-guide"
}

72
developer-guide/index.md Normal file
View File

@ -0,0 +1,72 @@
---
title: Developer guide
eleventyNavigation:
key: Developer guide
order: 3
---
# Developer guide
This documentation intends to explain how to get penpot application and run it
locally, to test it or make changes to it.
> If you only want to run it or change external parts, the simplest approach is
to use the official docker image, as explained below.
> If you want to modify the core application, see instead the
[Development Environment guide](../core_developer/development-environment).
## Install Docker ##
Skip this section if you already have docker installed, up and running.
You can install docker and its dependencies from your distribution repository
with:
```bash
sudo apt-get install docker docker-compose
```
Or follow installation instructions from docker.com; (for Debian
https://docs.docker.com/engine/install/debian/).
Ensure that the docker is started and optionally enable it to start with the
system:
```bash
sudo systemctl start docker
sudo systemctl enable docker
```
And finally, add your user to the docker group:
```bash
sudo usermod -aG docker $USER
```
This will make use of the docker without `sudo` command all the time.
NOTE: probably you will need to re-login again to make this change take effect.
## Start penpot application ##
You can create it from scratch or take a base from the [penpot repository][1]
[1]: https://raw.githubusercontent.com/penpot/penpot/develop/docker/images/docker-compose.yaml
```bash
wget https://raw.githubusercontent.com/penpot/penpot/develop/docker/images/docker-compose.yaml
```
And then:
```bash
docker-compose -p penpot -f docker-compose.yaml up
```
The docker compose file contains the essential configuration for getting the
application running, and many essential configurations already explained in the
comments. All other configuration options are explained in the [Configuration
guide](../configuration).

355
faqs.njk Normal file
View File

@ -0,0 +1,355 @@
---
title: FAQs
layout: layouts/faqs.njk
eleventyNavigation:
key: FAQs
order: 4
---
<h1 class="main-title">Frequently asked questions</h1>
<p class="main-paragraph">
You probably arrived here because you either wanted to understand the Why Penpot or the How Penpot questions. Both are fine and we have grouped those questions attending to that soft categorisation. This FAQ will continue to evolve and, at some point, will probably require more ergonomics, filters and search. For now, lets see how it goes.
</p>
<p class="advice">
Missing a question? This is a work in progress, for any suggestions please write us at <a href="mailto:info@penpot.app" target="_blank">info@penpot.app</a>
</p>
<hr/>
<h2 id="why-penpot" class="secondary-title">
Why Penpot?
<a class="direct-link" href="#why-penpot">#</a>
</h2>
<p>
<p class="main-paragraph">
These are questions related to the fact that Penpot even exists or what is particularly different and new about it.
</p>
<h3 id="who-created-penpot-why" class="secondary-title">
Who created Penpot? Why?
<a class="direct-link" href="#who-created-penpot-why">#</a>
</h3>
<p>
There is a company called <a href="https://kaleidos.net" target="_blank">Kaleidos Open Source</a> that has been long known for its commitment to free & open source software and a more diverse and inclusive workplace where cross-domain teams really enjoy working together. Kaleidos launched Taiga [https://taiga.io] a few years ago to deal with the absence of a truly agile open source project management tool. The next major pain in our ranked list of outrageous open source absentees was a design & prototype tool the likes of Figma, Sketch or Invision.
</p>
<p>
At Kaleidos we believe that the tools that we use to build end products should be as accessible to everyone, regardless of their background, skills or purchasing power. Also, not having a free & open source UX/UI tool that would make devs participate in the design process and bridge the gap between UX/UI and code was a terrible itch for us.
</p>
<p>
<p>
We created Penpot out of the need to enjoy design freedom for cross-domain teams.
</p>
<h3 id="why-open-source" class="secondary-title">
Why specifically Open Source?
<a class="direct-link" href="#why-open-source">#</a>
</h3>
<p>
Its our very personal choice to make sure that we create tools that inject more freedom into the system. Open Source means pursuing a fairer society, where opportunities are more evenly distributed. Software Technology has the unique advantage, compared to other industries and intellectual property, of having almost zero cost to replicate itself, thus providing a wonderful chance to massively distribute the tools for a more digitally sovereign society.
</p>
<p>
Besides the pure license aspect of it and its legal framework, Open Source fosters more engaging communities where the lines between user and contributor are often blurred.
</p>
<p>
In particular, we chose the very respected Mozilla Public License 2.0 because it made software delivered through the web or a service subject to the same rules as software that runs natively on your operating system.
</p>
<h3 id="why-svg-as-its-native-format" class="secondary-title">
Why SVG as its native format?
<a class="direct-link" href="#why-svg-as-its-native-format">#</a>
</h3>
<p>
SVG, Scalar Vector Graphics is a widely used Open Standard by the <a href="https://www.w3.org/TR/SVG2/" target="_blank">W3C</a>. It permeates the web, the mobile world and visualisation outputs across a myriad of platforms.
</p>
<p>
Embracing SVG was a technical challenge but it was a huge opportunity too. It makes a Penpot design, itself valid code already. Moreover, the potential for integrations and interoperability are infinite.
</p>
<p>
When you are certain that a part of your design is SVG and you live-export that SVG as part of your code repository, you can have back and forth changes through the SVG files. Also, a “continuous design” process could be finally at hand.
</p>
<p>
Using SVG means that there is no translation between a design and its “mathematical” representation. The design is the SVG and the SVG is the design. This is a triumph for cross-domain collaboration.
</p>
<h3 id="why-is-penpot-different-from-proprietary-products" class="secondary-title">
Why is Penpot different from [X proprietary product]
<a class="direct-link" href="#why-is-penpot-different-from-proprietary-products">#</a>
</h3>
<p>
It would be tempting to go on a feature-comparison mode here. Thats not going to happen. In terms of functionality Penpot is just fine already and with every new month, we will be bridging the gap between other well-known tools (founded between 2010 and 2012) and our hectic release cycle.
</p>
<p>
What is really different is the combination of four key elements:
</p>
<ol>
<li><strong>Open Source</strong>. This is a game-changer for the design and prototype world. It means you can trust your designs are your own. You can adapt or improve the code or benefit from others doing so. You can host your own Penpot instance (also, native app in the works). You can integrate it the way you want. Theres no limit with technology sovereignty. </li>
<li><strong>SVG</strong>. The fact that we dont go for yet another proprietary format is a blow of fresh air. SVG is what code needs and wants, making the cooperation between design and code suddenly much easier. </li>
<li><strong>Cross-domain team focus</strong>. Teams that are able to integrate many different skills and backgrounds and still enjoy a fruitful conversation (and project) are amazing to watch. Penpot aims to deliver the perfect tool for visual designers that is wholeheartedly embraced by developers too.</li>
<li><strong>No platform dependencies</strong>. Penpot requires a browser, thats it. If you want to host your own Penpot instance, thats fine too. We plan to release a native app bundle later this year.</li>
</ol>
<p>
There is a theme here. Universal access. Thats why we love to call our product Penpot, theres nothing more personal and yet more universal than a pot full of pens. Its all about choice.
</p>
<h3 id="why-clojure-and-clojurescript" class="secondary-title">
Why did you develop Penpot using Clojure and Clojurescript?
<a class="direct-link" href="#why-clojure-and-clojurescript">#</a>
</h3>
<p>
Penpot is a very specific type of tool that demands high performance on the web. Penpot also has the challenging task of seamlessly manipulating zillions of mathematical objects with no information loss. Functional programming languages like Clojure excel at this.
</p>
<p>
It is one of those instances where you absolutely need a specific technology to be able to achieve a robust tool like Penpot in a short period of time.
</p>
<p>
Since Clojure is not (yet?) a mainstream programming language, we are making sure that there will be ways to extend Penpot using other more common languages such as Javascript. Also, at the backend level, appropriate APIs will be in place to easily connect Penpot with other platforms.
</p>
<h3 id="why-release-penpot-as-alpha" class="secondary-title">
Why did you release Penpot as an Alpha?
<a class="direct-link" href="#why-release-penpot-as-alpha">#</a>
</h3>
<p>
We couldnt wait any longer. Back in February 2020 we promised that it would take as a year to develop a sort of 1.0. The “alpha” tag might be misleading, its quite stable and feature-rich but in some ways its still immature with regards to our vision.
</p>
<p>
Anyway, enjoy it as what it is, the first public iteration of our hopeful plans.
</p>
<hr/>
<h2 id="how-penpot" class="secondary-title">
How Penpot?
<a class="direct-link" href="#how-penpot">#</a>
</h2>
<p>
<p class="main-paragraph">
These are questions related to practical aspects of Penpot or its capabilities.
</p>
<h3 id="how-is-penpot-sustainable" class="secondary-title">
How do you plan to make Penpots development sustainable?
<a class="direct-link" href="#how-is-penpot-sustainable">#</a>
</h3>
<p>
<a href="https://kaleidos.net" target="_blank">Kaleidos Open Source</a>, the company behind Penpot, has the resources and the team needed to do that. If Penpot really succeeds and demands more and more resources, a bigger team and a bigger infrastructure, we will need to find ways to monetize some aspects of Penpot. Many Open Source platforms have been very successful at that, without reverting to closing up the source code.
</p>
<p>
SaaS subscriptions offer a quite valid and straightforward business model on top of Open Source. We are also considering marketplace models à-la-Wordpress or big-enterprise-focused features for supported Penpot deployments à-la-Gitlab. At the moment, though, this is something we dont plan to address until 2022.
</p>
<p>
If you would like to know more about our track record, just take a look at Taiga and its AGPL 3.0 licensing model.
</p>
<h3 id="how-to-download-and-install" class="secondary-title">
How can I download and install Penpot for my team?
<a class="direct-link" href="#how-to-download-and-install">#</a>
</h3>
<p>
Current private Penpot instances are only requiring basic Docker knowledge. You can run your own Penpot server following these instructions.
</p>
<p>
<a href="https://help.penpot.app/developer-guide/configuration/">https://help.penpot.app/developer-guide/configuration/</a>
</p>
<h3 id="how-can-penpot-work-offline" class="secondary-title">
How can I make Penpot work offline?
<a class="direct-link" href="#how-can-penpot-work-offline">#</a>
</h3>
<p>
At the moment that would require that you install Penpot locally and so you connect to localhost. Instructions for this using Docker can be found here: <a href="https://help.penpot.app/developer-guide/configuration/">https://help.penpot.app/developer-guide/configuration/</a>
</p>
<p>
For a simpler approach, more akin to downloading a piece of software and running it on top of your operating system, please wait for our Electron-based app bundle later this year.
</p>
<h3 id="where-is-penpot-code" class="secondary-title">
Where can I find the code?
<a class="direct-link" href="#where-is-penpot-code">#</a>
</h3>
<p>
You can download the code or clone the repository at <a href="https://github.com/penpot" target="_blank">Github</a>.
</p>
<h3 id="best-browser-experience" class="secondary-title">
How can I enjoy the best Penpot browser experience?
<a class="direct-link" href="#best-browser-experience">#</a>
</h3>
<p>
Cross-browser support is a key aspect of Penpot so we give much thought and care to it.
</p>
<p>
Our current preference solely in terms of experience is:
</p>
<ol>
<li>Chrome (or Chromium/Blink based browsers/Edge)</li>
<li>Firefox</li>
<li>WebKit (Safari / Epiphany)</li>
</ol>
<h3 id="how-enjoy-penpot-on-the-cloud" class="secondary-title">
How can I enjoy Penpot on the cloud?
<a class="direct-link" href="#how-enjoy-penpot-on-the-cloud">#</a>
</h3>
<p>
The easiest way to enjoy Penpot is quite simple. You simply go to <a href="https://penpot.app" target="_blank">https://penpot.app</a> and 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.
</p>
<h3 id="how-work-with-other-people" class="secondary-title">
How can I work together with other people?
<a class="direct-link" href="#how-work-with-other-people">#</a>
</h3>
<p>
The easiest way to do that in Penpot is to create a team and add projects as you need them. All those projects and files will be available for your team and you will be able to work either asynchronously or simultaneously.
</p>
<p>
A Penpot user can be part of many different teams and have access to all their available projects.
</p>
<p>
Of course, Penpot also gives you the possibility to work on your private projects or drafts.
</p>
<h3 id="how-share-designs-with-external-stakeholders" class="secondary-title">
How can I share my designs with external stakeholders?
<a class="direct-link" href="#how-share-designs-with-external-stakeholders">#</a>
</h3>
<p>
If you want to have a shareable URL to show your designs you can do it from the <VIEWER> mode. Launch it using the <PLAY> button at the top right of the file, there you'll find a <SHARE URL> button to create the link. Copy and send that link for other people to access the design.
</p>
<p>
You can always invalidate a shared URL if you dont wish to continue to make it accessible through such link.
</p>
<h3 id="how-to-stay-up-to-date-with-penpot-news" class="secondary-title">
How can I stay up-to-date with the latest Penpot news and what's to come?
<a class="direct-link" href="#how-to-stay-up-to-date-with-penpot-news">#</a>
</h3>
<p>
How or where can I stay up-to-date with the latest Penpot news and what's to come? We will be sharing our progress and news through different channels:
</p>
<ul>
<li>Penpot Social Networks: <a href="https://twitter.com/penpotapp" target="_blank">Twitter</a>, <a href="https://fosstodon.org/@penpot/" target="_blank">Mastodon</a>, <a href="https://www.instagram.com/penpotapp/" target="_blank">Instagram</a>, <a href="https://www.linkedin.com/company/penpot/" target="_blank">Linkedin</a>. </li>
<li><a href="https://www.youtube.com/channel/UCAqS8G72uv9P5HG1IfgnQ9g" target="_blank">Penpot Youtube channel</a>: Subscribe to get updates when we upload new Penpot tutorials, demos of features and talks.</li>
<li><a href="https://penpot.app/#newsletter" target="_blank">Newsletter</a>: Dont miss the important stuff subscribing to our low-traffic newsletter.</li>
<li><a href="https://github.com/penpot/penpot" target="_blank">Penpot Github Project</a>: Follow us to keep up to date with the project progress and get involved in the discussions.</li>
</ul>
<h3 id="how-join-community-chat" class="secondary-title">
How can I join the community chat?
<a class="direct-link" href="#how-join-community-chat">#</a>
</h3>
<p>
You can jump in right here <a href="https://gitter.im/penpot/community" target="_blank">https://gitter.im/penpot/community</a>
</p>
<p>
Say hello and introduce yourself! Happy to have you!
</p>
<h3 id="how-use-penpot-with-inkscape" class="secondary-title">
How can I use Penpot together with Inkscape?
<a class="direct-link" href="#how-use-penpot-with-inkscape">#</a>
</h3>
<p>
Inkscape is a very powerful Open Source vector drawing tool. At Penpot we use it daily for our SVG creations. A very common use we make is to use Inkscape to work with vector illustrations, icon sets and other graphic assets. Then we import them into Penpot to create more complete and visually attractive interface designs. There are other tools that can complete your workflow such as Quant-UX which is very good to learn about your users interactions with your product.
</p>
<h3 id="learn-how-to-use-penpot" class="secondary-title">
How can I learn how to use Penpot?
<a class="direct-link" href="#learn-how-to-use-penpot">#</a>
</h3>
<p>
Right now, our suggested approach would be to take a look at our <a href="https://www.youtube.com/channel/UCAqS8G72uv9P5HG1IfgnQ9g" target="_blank">Youtube channel</a> and enjoy the quick tutorials on many aspects of Penpot.
</p>
<h3 id="how-can-contribute" class="secondary-title">
How can I contribute to Penpot?
<a class="direct-link" href="#how-can-contribute">#</a>
</h3>
<p>
We are working on a comprehensive guide on how to contribute to Penpot. There are many ways this can be achieved. Designs, code, testing, reporting bugs, improving documentation, sharing Penpot designs with #MadeWithPenpot, translating Penpot to your favourite language, set up training sessions, etc.
</p>
<p>
For now, we have a rather limited <a href="https://github.com/penpot/penpot/blob/develop/CONTRIBUTING.md" target="_blank">contribution guide</a> that you can already use but we plan to improve this very soon.
</p>
<hr/>
<h2 id="other-faqs" class="secondary-title">
Other frequently asked questions
<a class="direct-link" href="#other-faqs">#</a>
</h2>
<p>
<p class="main-paragraph">
Here we include some other interesting questions people have asked or we would like to directly address.
</p>
<h3 id="roadmap-short-mid-term" class="secondary-title">
What is in Penpots roadmap for the short-mid term?
<a class="direct-link" href="#roadmap-short-mid-term">#</a>
</h3>
<p>
Some of the things planned for the near future:
</p>
<ul>
<li>Using local fonts as well as web fonts</li>
<li>Boolean operators</li>
<li>SVG export integration with other tools and processes</li>
<li>More advanced interactive mockup transitions</li>
<li>SVG import and edition</li>
</ul>
<p>
You can check the advance of the project at <a href="https://tree.taiga.io/project/penpot/backlog" target="_blank">Taiga</a>.
</p>
<h3 id="design-patterns" class="secondary-title">
Did the team find a pattern (design, interaction, tool) that could be improved but decided to keep it because the reference tools already made it familiar to the user?
<a class="direct-link" href="#design-patterns">#</a>
</h3>
<p>
Short answer would be no, we do not keep things from improving because of the potential loss of familiarity.
</p>
<p>
It is true that we are using known patterns to ease the learning curve, this is part of Penpots strategy. Given that, we could say that “familiar” is included in our definition of success, but this might be for a different, also interesting, conversation. We do not hide that we have kept an eye not only on Figma but also on many others “usual suspects” (Sketch, Adobe XD, Inkscape, Webflow, Blender...) to study common patterns. However, our design decisions are based not only on previous research but also on testing solutions with users. Therefore, if we were to find that something works better for us in a different way, wed go for it. Masks and toolbars are some examples of this approach.
</p>

0
img/.gitkeep Normal file
View File

BIN
img/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

1
img/caret-down.svg Normal file
View File

@ -0,0 +1 @@
<svg height="500" viewBox="0 0 500 500.00001" width="500" xmlns="http://www.w3.org/2000/svg"><path d="m138.95104 552.39877c-15.97981-.91956-29.97031 15.64781-25.60739 31.33414 4.13074 8.64975 24.56735 29.78594 24.56735 29.78594s124.53922 124.60864 185.12839 188.55293c2.98762 7.37445-5.2414 11.74615-9.09921 16.46818-65.67593 65.77624-134.53948 128.31983-199.74642 194.55784-10.04086 14.0513-.63252 36.5051 16.6162 38.8691 11.18564 2.1327 21.43639-4.6772 28.26602-12.9034 69.31825-65.46486 137.62693-132.00453 206.41188-198.03973 7.65756-8.14176 16.96875-15.29428 22.61013-25.01415 5.09335-12.02529-1.09218-25.6828-11.04128-33.08286-74.27352-75.37761-148.37953-150.94757-224.06956-224.8979-3.90281-3.30297-8.78051-5.91159-14.03611-5.63009z" transform="matrix(0 1 -1 0 1052.3622 .000003)"/></svg>

After

Width:  |  Height:  |  Size: 795 B

BIN
img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,4 +1,5 @@
---
title: Help center
layout: layouts/home.njk
twitter: "@penpotapp"
image: img/placeholder.png
@ -7,7 +8,43 @@ eleventyNavigation:
order: 1
---
<h1 class="main-title">Penpot documentation site</h1>
<p>Lorem ipsum dolor...</p>
<h1 class="main-title">Help center</h1>
<ul class="help-sections">
<li>
<a href="/developer-guide/">
<h3>Developer guide →</h3>
<p>Comprehensive documentation about architecture, configuration and core.</p>
</a>
</li>
<li>
<a href="/faqs">
<h3>Frequently asked questions →</h3>
<p>Get quick answers to usual questions about "why and how" Penpot.</p>
</a>
</li>
<li class="no-link">
<!-- a href="" -->
<h3>Contact us →</h3>
<p>Write us at <a href="mailto:info@penpot.app" target="_blank">info@penpot.app</a> or join the <a href="https://github.com/penpot/penpot/discussions" target="_blank">team discusions</a>.</p>
<!-- p>Write us an email, join team discussions or chat with us.</p-->
<!-- /a -->
</li>
<li class="no-link soon">
<h3>User guide →</h3>
<!-- p>Everything you need to know about how Penpot works.</p -->
<p>While we finish the first version of the user guide you can watch tutorials at our <a href="https://www.youtube.com/channel/UCAqS8G72uv9P5HG1IfgnQ9g" target="_blank">Youtube channel</a>.</p>
<span class="advice"> Coming soon </span>
</li>
<li class="github">
<a href="https://github.com/penpot/">
<svg width="357.782" height="349.03" viewBox="0 0 335.421 327.216">
<path d="M312.923 83.527c-14.996-25.696-35.339-46.04-61.033-61.035C226.193 7.495 198.14 0 167.709 0c-30.426 0-58.49 7.5-84.181 22.493-25.696 14.995-46.038 35.34-61.035 61.035C7.498 109.222 0 137.281 0 167.704c0 36.544 10.662 69.406 31.991 98.593 21.327 29.19 48.879 49.388 82.652 60.597 3.931.73 6.842.216 8.734-1.527 1.893-1.745 2.838-3.93 2.838-6.549 0-.436-.037-4.365-.11-11.79-.075-7.427-.11-13.905-.11-19.433l-5.023.87c-3.202.585-7.242.834-12.12.764-4.875-.068-9.936-.58-15.176-1.53-5.242-.94-10.118-3.124-14.631-6.546-4.511-3.42-7.714-7.899-9.607-13.427l-2.183-5.025c-1.456-3.346-3.747-7.062-6.878-11.136-3.13-4.077-6.296-6.84-9.498-8.297l-1.53-1.094a16.031 16.031 0 01-2.838-2.623c-.873-1.018-1.527-2.037-1.964-3.057-.437-1.02-.075-1.859 1.092-2.516 1.166-.657 3.274-.976 6.332-.976l4.366.653c2.912.583 6.514 2.326 10.81 5.24 4.294 2.91 7.823 6.695 10.59 11.352 3.35 5.97 7.386 10.52 12.12 13.65 4.73 3.13 9.498 4.693 14.302 4.693 4.803 0 8.951-.364 12.447-1.088 3.491-.729 6.767-1.823 9.826-3.278 1.31-9.758 4.877-17.254 10.698-22.494-8.297-.872-15.756-2.185-22.382-3.93-6.622-1.75-13.465-4.587-20.525-8.52-7.063-3.93-12.923-8.81-17.58-14.63-4.658-5.824-8.48-13.469-11.463-22.929-2.983-9.465-4.475-20.382-4.475-32.756 0-17.618 5.751-32.61 17.252-44.986-5.387-13.246-4.879-28.094 1.528-44.545 4.222-1.31 10.483-.327 18.78 2.947 8.298 3.276 14.374 6.082 18.234 8.41 3.86 2.325 6.951 4.296 9.281 5.894 13.542-3.783 27.516-5.675 41.928-5.675 14.41 0 28.388 1.892 41.93 5.675l8.299-5.238c5.674-3.495 12.375-6.699 20.086-9.61 7.716-2.91 13.616-3.712 17.694-2.4 6.549 16.45 7.132 31.3 1.743 44.544 11.5 12.376 17.254 27.372 17.254 44.986 0 12.374-1.497 23.326-4.476 32.863-2.983 9.538-6.839 17.176-11.569 22.93-4.735 5.752-10.632 10.592-17.691 14.52-7.061 3.932-13.907 6.77-20.529 8.518-6.625 1.748-14.085 3.062-22.382 3.936 7.568 6.55 11.352 16.885 11.352 31.006v46.072c0 2.618.91 4.803 2.732 6.55 1.82 1.742 4.693 2.255 8.625 1.525 33.778-11.208 61.329-31.406 82.655-60.596 21.324-29.187 31.99-62.05 31.99-98.594-.008-30.418-7.51-58.475-22.497-84.17z"></path>
</svg>
<div class="content">
<h3>Collaborate at Github →</h3>
<p>Penpot is open source. Get involved on the community or contribute to the project.</p>
</div>
</a>
</li>
</ul>

10
js/elasticlunr.min.js vendored Normal file

File diff suppressed because one or more lines are too long

59
js/search.js Normal file
View File

@ -0,0 +1,59 @@
(function (window, document) {
"use strict";
const search = (e) => {
const results = window.searchIndex.search(e.target.value, {
bool: "AND",
expand: true,
});
const resEl = document.getElementById("search-results");
const noResultsEl = document.getElementById("no-results-found");
resEl.innerHTML = "";
if (results.length > 0) {
resEl.style.display = "block";
noResultsEl.style.display = "none";
results.map((r) => {
const { id, title, description } = r.doc;
const el = document.createElement("li");
resEl.appendChild(el);
const h3 = document.createElement("h3");
el.appendChild(h3);
const a = document.createElement("a");
// TODO: highlight the search term in the dest page
a.setAttribute("href", id);
a.textContent = title;
h3.appendChild(a);
// TODO: show an excerpt of the found page
// const p = document.createElement("p");
// p.textContent = description;
// el.appendChild(p);
});
} else {
resEl.style.display = "none";
noResultsEl.style.display = "block";
}
};
const hideSearch = (e) => {
setTimeout(() => {
e.target.value = "";
const resEl = document.getElementById("search-results");
const noResultsEl = document.getElementById("no-results-found");
resEl.style.display = "none";
noResultsEl.style.display = "none";
}, 200);
};
fetch("/search-index.json").then((response) =>
response.json().then((rawIndex) => {
window.searchIndex = elasticlunr.Index.load(rawIndex);
document.getElementById("search-field").addEventListener("input", search);
document.getElementById("search-field").addEventListener("blur", hideSearch);
})
);
})(window, document);

16
js/toc.js Normal file
View File

@ -0,0 +1,16 @@
(function (window, document) {
"use strict";
let titleEl;
let tocEl;
const titleClicked = (e) => {
tocEl.classList.toggle('open');
};
window.addEventListener('load', () => {
titleEl = document.getElementById("toc-title");
tocEl = document.getElementById("toc");
titleEl.addEventListener("click", titleClicked);
});
})(window, document);

1669
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,22 +23,20 @@
"url": "https://github.com/penpot/penpot-docs/issues"
},
"homepage": "https://docs.penpot.app",
"devDependencies": {
"dependencies": {
"@11ty/eleventy": "^0.11.1",
"@11ty/eleventy-navigation": "^0.1.6",
"@11ty/eleventy-plugin-rss": "^1.1.0",
"@11ty/eleventy-plugin-syntaxhighlight": "^3.0.6",
"@akebifiky/remark-simple-plantuml": "^1.0.2",
"@code-blocks/eleventy-plugin": "^0.1.7",
"@code-blocks/prism": "^0.1.6",
"@fec/eleventy-plugin-remark": "^2.1.0",
"assassin-custom-plantuml": "^2.1.5",
"@tigersway/eleventy-plugin-ancestry": "^0.5.0",
"elasticlunr": "^0.9.5",
"eleventy-plugin-metagen": "^1.1.0",
"eleventy-plugin-nesting-toc": "^1.2.0",
"eleventy-plugin-youtube-embed": "^1.5.0",
"luxon": "^1.25.0",
"markdown-it": "^8.4.2",
"markdown-it-anchor": "^5.2.5"
"markdown-it-anchor": "^5.2.5",
"markdown-it-plantuml": "^1.4.1"
},
"dependencies": {}
"devDependencies": {}
}

8
scripts/build.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
source ~/.bashrc
set -ex
rm -rf ./_dist
npm install
npm run build

6
search-index.json.njk Normal file
View File

@ -0,0 +1,6 @@
---
permalink: /search-index.json
---
{{ collections.all | search | dump | safe }}

10
user-guide/index.njk Normal file
View File

@ -0,0 +1,10 @@
---
title: User guide
eleventyNavigation:
key: User guide
order: 2
---
<h1 id="user-guide">User guide</h1>
<p>(USER) hola Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>

View File

@ -0,0 +1,8 @@
---
title: Section 1
---
<h1 id="section-1">Section 1</h1>
<p>Hello...</p>

View File

@ -0,0 +1,8 @@
---
title: Section 1.1
---
<h1 id="section-1-1">Section 1.1</h1>
<p>Hello...</p>

View File

@ -0,0 +1,12 @@
---
title: Section 2
---
<h1 id="section-2">Section 2</h1>
<h2 id="section-2-sub-1">Section 2 sub 1</h2>
<p>Hello...</p>
<h2 id="section-2-sub-2">Section 2 sub 2</h2>
<p>Hello...</p>

15
user-guide/section-3.njk Normal file
View File

@ -0,0 +1,15 @@
---
title: Section 3
---
<h1 id="section-3">Section 3</h1>
<h2 id="section-3-sub-1">Section 3 sub 1</h2>
<p>Hello...</p>
<h2 id="section-3-sub-2">Section 3 sub 2</h2>
<p>Hello...</p>
<h2 id="section-3-sub-3">Section 3 sub 3</h2>
<p>Hello...</p>

View File

@ -0,0 +1,4 @@
{
"layout": "layouts/user-guide.njk",
"tags": "user-guide"
}