mirror of
https://github.com/penpot/penpot-docs.git
synced 2024-07-06 05:41:47 +00:00
🎉 Add first version of the docsite
This commit is contained in:
parent
aee4341b5d
commit
d497b3d3f7
47
.eleventy.js
47
.eleventy.js
|
@ -1,19 +1,22 @@
|
||||||
const { DateTime } = require("luxon");
|
const { DateTime } = require("luxon");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const pluginNavigation = require("@11ty/eleventy-navigation");
|
||||||
const pluginRss = require("@11ty/eleventy-plugin-rss");
|
const pluginRss = require("@11ty/eleventy-plugin-rss");
|
||||||
const pluginSyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
|
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 pluginTOC = require('eleventy-plugin-nesting-toc');
|
||||||
const markdownIt = require("markdown-it");
|
const markdownIt = require("markdown-it");
|
||||||
const markdownItAnchor = require("markdown-it-anchor");
|
const markdownItAnchor = require("markdown-it-anchor");
|
||||||
const markdownItPlantUML = require("assassin-custom-plantuml");
|
const markdownItPlantUML = require("markdown-it-plantuml");
|
||||||
const metagen = require('eleventy-plugin-metagen');
|
const elasticlunr = require("elasticlunr");
|
||||||
|
|
||||||
|
|
||||||
module.exports = function(eleventyConfig) {
|
module.exports = function(eleventyConfig) {
|
||||||
|
eleventyConfig.addPlugin(pluginNavigation);
|
||||||
eleventyConfig.addPlugin(pluginRss);
|
eleventyConfig.addPlugin(pluginRss);
|
||||||
eleventyConfig.addPlugin(pluginSyntaxHighlight);
|
eleventyConfig.addPlugin(pluginSyntaxHighlight);
|
||||||
eleventyConfig.addPlugin(pluginNavigation);
|
eleventyConfig.addPlugin(pluginAncestry);
|
||||||
eleventyConfig.addPlugin(metagen);
|
eleventyConfig.addPlugin(metagen);
|
||||||
eleventyConfig.addPlugin(pluginTOC, {
|
eleventyConfig.addPlugin(pluginTOC, {
|
||||||
tags: ['h1', 'h2', 'h3']
|
tags: ['h1', 'h2', 'h3']
|
||||||
|
@ -32,6 +35,12 @@ module.exports = function(eleventyConfig) {
|
||||||
return DateTime.fromJSDate(dateObj, {zone: 'utc'}).toFormat('yyyy-LL-dd');
|
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.
|
// Get the first `n` elements of a collection.
|
||||||
eleventyConfig.addFilter("head", (array, n) => {
|
eleventyConfig.addFilter("head", (array, n) => {
|
||||||
if( n < 0 ) {
|
if( n < 0 ) {
|
||||||
|
@ -41,17 +50,41 @@ module.exports = function(eleventyConfig) {
|
||||||
return array.slice(0, n);
|
return array.slice(0, n);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get the lowest in a list of numbers.
|
||||||
eleventyConfig.addFilter("min", (...numbers) => {
|
eleventyConfig.addFilter("min", (...numbers) => {
|
||||||
return Math.min.apply(null, 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("img");
|
||||||
eleventyConfig.addPassthroughCopy("css");
|
eleventyConfig.addPassthroughCopy("css");
|
||||||
|
eleventyConfig.addPassthroughCopy("js");
|
||||||
|
|
||||||
/* Markdown Overrides */
|
/* Markdown Overrides */
|
||||||
let markdownLibrary = markdownIt({
|
let markdownLibrary = markdownIt({
|
||||||
html: true,
|
html: true,
|
||||||
breaks: true,
|
breaks: false,
|
||||||
linkify: true
|
linkify: true
|
||||||
}).use(markdownItAnchor, {
|
}).use(markdownItAnchor, {
|
||||||
permalink: true,
|
permalink: true,
|
||||||
|
@ -65,7 +98,7 @@ module.exports = function(eleventyConfig) {
|
||||||
eleventyConfig.setBrowserSyncConfig({
|
eleventyConfig.setBrowserSyncConfig({
|
||||||
callbacks: {
|
callbacks: {
|
||||||
ready: function(err, browserSync) {
|
ready: function(err, browserSync) {
|
||||||
const content_404 = fs.readFileSync('_site/404.html');
|
const content_404 = fs.readFileSync('_dist/404.html');
|
||||||
|
|
||||||
browserSync.addMiddleware("*", (req, res) => {
|
browserSync.addMiddleware("*", (req, res) => {
|
||||||
// Provides the 404 content without redirect.
|
// Provides the 404 content without redirect.
|
||||||
|
@ -105,7 +138,7 @@ module.exports = function(eleventyConfig) {
|
||||||
input: ".",
|
input: ".",
|
||||||
includes: "_includes",
|
includes: "_includes",
|
||||||
data: "_data",
|
data: "_data",
|
||||||
output: "_site"
|
output: "_dist"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
README.md
|
README.md
|
||||||
|
user-guide
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,3 +1,6 @@
|
||||||
|
# Distribution files
|
||||||
|
_dist/*
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|
35
README.md
35
README.md
|
@ -1,2 +1,35 @@
|
||||||
# penpot-docs
|
# 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).
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"title": "Penpot documentation site",
|
"title": "Help center",
|
||||||
"url": "https://docs.penpot.app/",
|
"url": "https://docs.penpot.app/",
|
||||||
"description": "Design freedom for teams.",
|
"description": "Design freedom for teams.",
|
||||||
"feed": {
|
"feed": {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,15 +1,34 @@
|
||||||
---
|
---
|
||||||
layout: layouts/base.njk
|
layout: layouts/base.njk
|
||||||
templateClass: tmpl-home
|
templateClass: tmpl-developer-guide
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="main-container">
|
{%- macro show_children(item) -%}
|
||||||
<aside class="sidebar">
|
{%- for child in item | children | sorted('data.title') %}
|
||||||
{{ content | toc | safe }}
|
{%- 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>
|
</aside>
|
||||||
|
|
||||||
<content class="main-content">
|
<content class="main-content">
|
||||||
{{ content | safe }}
|
{{ content | safe }}
|
||||||
</content>
|
</content>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
10
_includes/layouts/faqs.njk
Normal file
10
_includes/layouts/faqs.njk
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
layout: layouts/base.njk
|
||||||
|
templateClass: tmpl-faqs
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="main-container">
|
||||||
|
<div class="main-content">
|
||||||
|
{{ content | safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -2,4 +2,6 @@
|
||||||
layout: layouts/base.njk
|
layout: layouts/base.njk
|
||||||
templateClass: tmpl-home
|
templateClass: tmpl-home
|
||||||
---
|
---
|
||||||
{{ content | safe }}
|
<div class="main-container">
|
||||||
|
{{ content | safe }}
|
||||||
|
</div>
|
||||||
|
|
34
_includes/layouts/user-guide.njk
Normal file
34
_includes/layouts/user-guide.njk
Normal 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>
|
|
@ -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>
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
# For Apache, to show `feed.xml` when browsing to directory /feed/ (hide the file!)
|
|
||||||
DirectoryIndex feed.xml
|
|
|
@ -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": [
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
593
css/index.css
593
css/index.css
|
@ -1,12 +1,13 @@
|
||||||
:root {
|
:root {
|
||||||
--red: #C5004A;
|
|
||||||
--darkred: #7F0036;
|
--darkest: #000000;
|
||||||
--lightgray: #e0e0e0;
|
--graydark: #1F1F1F;
|
||||||
--gray: #C0C0C0;
|
--graymedium: #7B7D85;
|
||||||
--darkgray: #333;
|
--graylight: #E3E3E3;
|
||||||
--navy: #17050F;
|
--primary: #31EFB8;
|
||||||
--blue: #082840;
|
--primarydark: #00af7d;
|
||||||
--white: #fff;
|
--white: #fff;
|
||||||
|
--advice: #dafffb;
|
||||||
}
|
}
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -15,42 +16,69 @@ html,
|
||||||
body {
|
body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: system-ui, sans-serif;
|
font-family: 'Source Sans Pro',system-ui, sans-serif;
|
||||||
color: var(--darkgray);
|
color: var(--graydark);
|
||||||
|
}
|
||||||
|
body {
|
||||||
background-color: var(--white);
|
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 {
|
p:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
p,
|
p,
|
||||||
.tmpl-post li,
|
.tmpl-post li,
|
||||||
img {
|
img,
|
||||||
max-width: 37.5em; /* 600px /16 */
|
h1, h2, h3,
|
||||||
|
.main-content {
|
||||||
|
max-width: 42rem;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
p,
|
p,
|
||||||
.tmpl-post li {
|
li {
|
||||||
line-height: 1.45;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
ul, ol {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
a[href] {
|
a[href] {
|
||||||
color: var(--blue);
|
color: var(--primarydark);
|
||||||
|
}
|
||||||
|
a[href]:hover {
|
||||||
|
color: var(--darkest);
|
||||||
}
|
}
|
||||||
a[href]:visited {
|
a[href]:visited {
|
||||||
color: var(--navy);
|
/* color: var(--primarydark); */
|
||||||
}
|
}
|
||||||
main {
|
main {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
background-image: url(/img/bg.png);
|
||||||
main :first-child {
|
background-repeat: no-repeat;
|
||||||
margin-top: 0;
|
background-position: center 120px;
|
||||||
}
|
}
|
||||||
header {
|
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 {
|
header:after {
|
||||||
content: "";
|
content: "";
|
||||||
display: table;
|
display: table;
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
header a[href] {
|
||||||
|
color: var(--graydark);
|
||||||
|
}
|
||||||
table {
|
table {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
|
@ -59,6 +87,40 @@ table th {
|
||||||
padding-right: 1em;
|
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,
|
pre,
|
||||||
code {
|
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;
|
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 */
|
/* Header */
|
||||||
.home {
|
.home {
|
||||||
padding: 0 1rem;
|
position: absolute;
|
||||||
float: left;
|
display: flex;
|
||||||
margin: 1rem 0; /* 16px /16 */
|
top: 26px;
|
||||||
|
left: 2rem;
|
||||||
font-size: 1em; /* 16px /16 */
|
font-size: 1em; /* 16px /16 */
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.home svg {
|
||||||
|
height: 40px;
|
||||||
|
width: 130px;
|
||||||
}
|
}
|
||||||
.home :link:not(:hover) {
|
.home :link:not(:hover) {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
.home span {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
display: inline;
|
||||||
|
margin: 6px 0 0 16px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--graymedium);
|
||||||
|
}
|
||||||
|
|
||||||
/* Nav */
|
/* Nav */
|
||||||
.nav {
|
.nav {
|
||||||
padding: 0;
|
display: flex;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
float: left;
|
margin: 0 auto;
|
||||||
margin-left: 1em;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.nav-item {
|
.nav-item {
|
||||||
display: inline-block;
|
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) {
|
.nav-item a[href]:not(:hover) {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -162,7 +240,7 @@ pre {
|
||||||
.postlist-date,
|
.postlist-date,
|
||||||
.postlist-item:before {
|
.postlist-item:before {
|
||||||
font-size: 0.8125em; /* 13px /16 */
|
font-size: 0.8125em; /* 13px /16 */
|
||||||
color: var(--darkgray);
|
color: var(--graydark);
|
||||||
}
|
}
|
||||||
.postlist-date {
|
.postlist-date {
|
||||||
word-spacing: -0.5px;
|
word-spacing: -0.5px;
|
||||||
|
@ -179,10 +257,62 @@ pre {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
.pre-footer {
|
||||||
padding: 1rem;
|
padding: 2rem;
|
||||||
|
margin-top: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
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 */
|
font-size: 0.625em; /* 10px /16 */
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
margin-left: 0.8em; /* 8px /10 */
|
margin-left: 0.8em; /* 8px /10 */
|
||||||
background-color: var(--red);
|
background-color: red;
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
border-radius: 0.25em; /* 3px /12 */
|
border-radius: 0.25em; /* 3px /12 */
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -218,7 +348,9 @@ a[href].post-tag:visited {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
margin-left: .1em;
|
margin-left: 1rem;
|
||||||
|
position: absolute;
|
||||||
|
/* top: 0.4rem; */
|
||||||
}
|
}
|
||||||
a[href].direct-link,
|
a[href].direct-link,
|
||||||
a[href].direct-link:visited {
|
a[href].direct-link:visited {
|
||||||
|
@ -228,22 +360,405 @@ a[href].direct-link:focus,
|
||||||
a[href].direct-link:focus:visited,
|
a[href].direct-link:focus:visited,
|
||||||
:hover > a[href].direct-link,
|
:hover > a[href].direct-link,
|
||||||
:hover > a[href].direct-link:visited {
|
:hover > a[href].direct-link:visited {
|
||||||
color: #aaa;
|
color: var(--primarydark);
|
||||||
}
|
}
|
||||||
|
:hover > a[href].direct-link:hover {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Particular classes */
|
/* Particular classes */
|
||||||
|
|
||||||
.main-title {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-container {
|
.media-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-container {
|
.main-container {
|
||||||
display: grid;
|
margin: auto;
|
||||||
grid-template-columns: minmax(100px, 25%) 1fr;
|
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 */
|
||||||
|
|
|
@ -17,9 +17,10 @@ code[class*="language-"], pre[class*="language-"] {
|
||||||
hyphens: none;
|
hyphens: none;
|
||||||
background: #272822;
|
background: #272822;
|
||||||
color: #f8f8f2;
|
color: #f8f8f2;
|
||||||
|
max-width: 40rem;
|
||||||
}
|
}
|
||||||
pre[class*="language-"] {
|
pre[class*="language-"] {
|
||||||
padding: 1.5em 0;
|
padding: 1.5em 1em;
|
||||||
margin: .5em 0;
|
margin: .5em 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
119
developer-guide/architecture.md
Normal file
119
developer-guide/architecture.md
Normal 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.
|
||||||
|
|
253
developer-guide/configuration.md
Normal file
253
developer-guide/configuration.md
Normal 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).
|
81
developer-guide/core-developer/backend.md
Normal file
81
developer-guide/core-developer/backend.md
Normal 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
|
||||||
|
```
|
||||||
|
|
65
developer-guide/core-developer/common.md
Normal file
65
developer-guide/core-developer/common.md
Normal 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.
|
||||||
|
|
138
developer-guide/core-developer/development-environment.md
Normal file
138
developer-guide/core-developer/development-environment.md
Normal 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`.
|
286
developer-guide/core-developer/frontend.md
Normal file
286
developer-guide/core-developer/frontend.md
Normal 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"
|
||||||
|
```
|
||||||
|
|
17
developer-guide/core-developer/index.md
Normal file
17
developer-guide/core-developer/index.md
Normal 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.
|
||||||
|
|
170
developer-guide/core-developer/misc.md
Normal file
170
developer-guide/core-developer/misc.md
Normal 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.
|
||||||
|
|
50
developer-guide/core-developer/testing.md
Normal file
50
developer-guide/core-developer/testing.md
Normal 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
|
||||||
|
```
|
||||||
|
|
4
developer-guide/developer-guide.json
Normal file
4
developer-guide/developer-guide.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"layout": "layouts/developer-guide.njk",
|
||||||
|
"tags": "developer-guide"
|
||||||
|
}
|
72
developer-guide/index.md
Normal file
72
developer-guide/index.md
Normal 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
355
faqs.njk
Normal 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, let’s 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>
|
||||||
|
It’s 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. That’s 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. There’s no limit with technology sovereignty. </li>
|
||||||
|
<li><strong>SVG</strong>. The fact that we don’t 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, that’s it. If you want to host your own Penpot instance, that’s fine too. We plan to release a native app bundle later this year.</li>
|
||||||
|
</ol>
|
||||||
|
<p>
|
||||||
|
There is a theme here. Universal access. That’s why we love to call our product Penpot, there’s nothing more personal and yet more universal than a pot full of pens. It’s 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 couldn’t 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, it’s quite stable and feature-rich but in some ways it’s 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 Penpot’s 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 don’t 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 don’t 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>: Don’t 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 Penpot’s 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 Penpot’s 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, we’d go for it. Masks and toolbars are some examples of this approach.
|
||||||
|
</p>
|
0
img/.gitkeep
Normal file
0
img/.gitkeep
Normal file
BIN
img/bg.png
Normal file
BIN
img/bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
1
img/caret-down.svg
Normal file
1
img/caret-down.svg
Normal 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
BIN
img/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
43
index.njk
43
index.njk
|
@ -1,4 +1,5 @@
|
||||||
---
|
---
|
||||||
|
title: Help center
|
||||||
layout: layouts/home.njk
|
layout: layouts/home.njk
|
||||||
twitter: "@penpotapp"
|
twitter: "@penpotapp"
|
||||||
image: img/placeholder.png
|
image: img/placeholder.png
|
||||||
|
@ -7,7 +8,43 @@ eleventyNavigation:
|
||||||
order: 1
|
order: 1
|
||||||
---
|
---
|
||||||
|
|
||||||
<h1 class="main-title">Penpot documentation site</h1>
|
<h1 class="main-title">Help center</h1>
|
||||||
|
|
||||||
<p>Lorem ipsum dolor...</p>
|
|
||||||
|
|
||||||
|
<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
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
59
js/search.js
Normal 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
16
js/toc.js
Normal 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
1669
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
|
@ -23,22 +23,20 @@
|
||||||
"url": "https://github.com/penpot/penpot-docs/issues"
|
"url": "https://github.com/penpot/penpot-docs/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://docs.penpot.app",
|
"homepage": "https://docs.penpot.app",
|
||||||
"devDependencies": {
|
"dependencies": {
|
||||||
"@11ty/eleventy": "^0.11.1",
|
"@11ty/eleventy": "^0.11.1",
|
||||||
"@11ty/eleventy-navigation": "^0.1.6",
|
"@11ty/eleventy-navigation": "^0.1.6",
|
||||||
"@11ty/eleventy-plugin-rss": "^1.1.0",
|
"@11ty/eleventy-plugin-rss": "^1.1.0",
|
||||||
"@11ty/eleventy-plugin-syntaxhighlight": "^3.0.6",
|
"@11ty/eleventy-plugin-syntaxhighlight": "^3.0.6",
|
||||||
"@akebifiky/remark-simple-plantuml": "^1.0.2",
|
"@tigersway/eleventy-plugin-ancestry": "^0.5.0",
|
||||||
"@code-blocks/eleventy-plugin": "^0.1.7",
|
"elasticlunr": "^0.9.5",
|
||||||
"@code-blocks/prism": "^0.1.6",
|
|
||||||
"@fec/eleventy-plugin-remark": "^2.1.0",
|
|
||||||
"assassin-custom-plantuml": "^2.1.5",
|
|
||||||
"eleventy-plugin-metagen": "^1.1.0",
|
"eleventy-plugin-metagen": "^1.1.0",
|
||||||
"eleventy-plugin-nesting-toc": "^1.2.0",
|
"eleventy-plugin-nesting-toc": "^1.2.0",
|
||||||
"eleventy-plugin-youtube-embed": "^1.5.0",
|
"eleventy-plugin-youtube-embed": "^1.5.0",
|
||||||
"luxon": "^1.25.0",
|
"luxon": "^1.25.0",
|
||||||
"markdown-it": "^8.4.2",
|
"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
8
scripts/build.sh
Executable 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
6
search-index.json.njk
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
permalink: /search-index.json
|
||||||
|
---
|
||||||
|
|
||||||
|
{{ collections.all | search | dump | safe }}
|
||||||
|
|
10
user-guide/index.njk
Normal file
10
user-guide/index.njk
Normal 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>
|
8
user-guide/section-1/index.njk
Normal file
8
user-guide/section-1/index.njk
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
title: Section 1
|
||||||
|
---
|
||||||
|
|
||||||
|
<h1 id="section-1">Section 1</h1>
|
||||||
|
|
||||||
|
<p>Hello...</p>
|
||||||
|
|
8
user-guide/section-1/section-1-1.njk
Normal file
8
user-guide/section-1/section-1-1.njk
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
title: Section 1.1
|
||||||
|
---
|
||||||
|
|
||||||
|
<h1 id="section-1-1">Section 1.1</h1>
|
||||||
|
|
||||||
|
<p>Hello...</p>
|
||||||
|
|
12
user-guide/section-2/index.njk
Normal file
12
user-guide/section-2/index.njk
Normal 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
15
user-guide/section-3.njk
Normal 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>
|
||||||
|
|
4
user-guide/user-guide.json
Normal file
4
user-guide/user-guide.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"layout": "layouts/user-guide.njk",
|
||||||
|
"tags": "user-guide"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user