diff --git a/docs/.editorconfig b/docs/.editorconfig new file mode 100644 index 000000000..d41540467 --- /dev/null +++ b/docs/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 diff --git a/docs/.eleventy.js b/docs/.eleventy.js new file mode 100644 index 000000000..2f6a76352 --- /dev/null +++ b/docs/.eleventy.js @@ -0,0 +1,144 @@ +const { DateTime } = require("luxon"); +const fs = require("fs"); +const pluginNavigation = require("@11ty/eleventy-navigation"); +const pluginRss = require("@11ty/eleventy-plugin-rss"); +const pluginSyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight"); +const pluginAncestry = require("@tigersway/eleventy-plugin-ancestry"); +const metagen = require('eleventy-plugin-metagen'); +const pluginTOC = require('eleventy-plugin-nesting-toc'); +const markdownIt = require("markdown-it"); +const markdownItAnchor = require("markdown-it-anchor"); +const markdownItPlantUML = require("markdown-it-plantuml"); +const elasticlunr = require("elasticlunr"); + + +module.exports = function(eleventyConfig) { + eleventyConfig.addPlugin(pluginNavigation); + eleventyConfig.addPlugin(pluginRss); + eleventyConfig.addPlugin(pluginSyntaxHighlight); + eleventyConfig.addPlugin(pluginAncestry); + eleventyConfig.addPlugin(metagen); + eleventyConfig.addPlugin(pluginTOC, { + tags: ['h1', 'h2', 'h3'] + }); + + eleventyConfig.setDataDeepMerge(true); + + eleventyConfig.addLayoutAlias("post", "layouts/post.njk"); + + eleventyConfig.addFilter("readableDate", dateObj => { + return DateTime.fromJSDate(dateObj, {zone: 'utc'}).toFormat("dd LLL yyyy"); + }); + + // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-date-string + eleventyConfig.addFilter('htmlDateString', (dateObj) => { + return DateTime.fromJSDate(dateObj, {zone: 'utc'}).toFormat('yyyy-LL-dd'); + }); + + // Remove trailing # in automatic generated toc, because of + // anchors added at the end of the titles. + eleventyConfig.addFilter('stripHash', (toc) => { + return toc.replace(/ #\<\/a\>/g, ""); + }); + + // Get the first `n` elements of a collection. + eleventyConfig.addFilter("head", (array, n) => { + if( n < 0 ) { + return array.slice(n); + } + + return array.slice(0, n); + }); + + // Get the lowest in a list of numbers. + eleventyConfig.addFilter("min", (...numbers) => { + return Math.min.apply(null, numbers); + }); + + // Build a search index + eleventyConfig.addFilter("search", (collection) => { + // What fields we'd like our index to consist of + // TODO: remove html tags from content + var index = elasticlunr(function () { + this.addField("title"); + this.addField("content"); + this.setRef("id"); + }); + + // loop through each page and add it to the index + collection.forEach((page) => { + index.addDoc({ + id: page.url, + title: page.template.frontMatter.data.title, + content: page.template.frontMatter.content, + }); + }); + + return index.toJSON(); + }); + + eleventyConfig.addPassthroughCopy("img"); + eleventyConfig.addPassthroughCopy("css"); + eleventyConfig.addPassthroughCopy("js"); + + /* Markdown Overrides */ + let markdownLibrary = markdownIt({ + html: true, + breaks: false, + linkify: true + }).use(markdownItAnchor, { + permalink: true, + permalinkClass: "direct-link", + permalinkSymbol: "#" + }).use(markdownItPlantUML, { + }); + eleventyConfig.setLibrary("md", markdownLibrary); + + // Browsersync Overrides + eleventyConfig.setBrowserSyncConfig({ + callbacks: { + ready: function(err, browserSync) { + const content_404 = fs.readFileSync('_dist/404.html'); + + browserSync.addMiddleware("*", (req, res) => { + // Provides the 404 content without redirect. + res.write(content_404); + res.end(); + }); + }, + }, + ui: false, + ghostMode: false + }); + + return { + templateFormats: [ + "md", + "njk", + "html", + "liquid" + ], + + // If your site lives in a different subdirectory, change this. + // Leading or trailing slashes are all normalized away, so don’t worry about those. + + // If you don’t have a subdirectory, use "" or "/" (they do the same thing) + // This is only used for link URLs (it does not affect your file structure) + // Best paired with the `url` filter: https://www.11ty.dev/docs/filters/url/ + + // You can also pass this in on the command line using `--pathprefix` + // pathPrefix: "/", + + markdownTemplateEngine: "liquid", + htmlTemplateEngine: "njk", + dataTemplateEngine: "njk", + + // These are all optional, defaults are shown: + dir: { + input: ".", + includes: "_includes", + data: "_data", + output: "_dist" + } + }; +}; diff --git a/docs/.eleventyignore b/docs/.eleventyignore new file mode 100644 index 000000000..b43bf86b5 --- /dev/null +++ b/docs/.eleventyignore @@ -0,0 +1 @@ +README.md diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..5fbef93cf --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,118 @@ +# Distribution files +_dist/* + +# yarn +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port +.idea diff --git a/docs/.nvmrc b/docs/.nvmrc new file mode 100644 index 000000000..ee09fac75 --- /dev/null +++ b/docs/.nvmrc @@ -0,0 +1 @@ +v20.11.1 diff --git a/docs/.vscode/settings.json b/docs/.vscode/settings.json new file mode 100644 index 000000000..1e915ede1 --- /dev/null +++ b/docs/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.rulers": [ + 80 + ] +} diff --git a/docs/.yarnrc.yml b/docs/.yarnrc.yml new file mode 100644 index 000000000..5a0ce9a8b --- /dev/null +++ b/docs/.yarnrc.yml @@ -0,0 +1,11 @@ +enableGlobalCache: true + +enableImmutableCache: false + +enableImmutableInstalls: false + +enableTelemetry: false + +httpTimeout: 600000 + +nodeLinker: node-modules diff --git a/docs/404.md b/docs/404.md new file mode 100644 index 000000000..0d39dd3a3 --- /dev/null +++ b/docs/404.md @@ -0,0 +1,17 @@ +--- +layout: layouts/home.njk +permalink: 404.html +eleventyExcludeFromCollections: true +--- +# Content not found. + +Go home. + +{% comment %} +Read more: https://www.11ty.dev/docs/quicktips/not-found/ + +This will work for both GitHub pages and Netlify: + +* https://help.github.com/articles/creating-a-custom-404-page-for-your-github-pages-site/ +* https://www.netlify.com/docs/redirects/#custom-404 +{% endcomment %} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..5e0800e05 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,38 @@ +# Penpot Docs + +Penpot documentation website. + +## Usage + +To view this site locally, first set up the environment: + +```sh +# only if necessary +nvm install +nvm use +# only if necessary +corepack enable + +yarn install +``` + +And launch a development server: + +```sh +yarn start +``` + +You can then point a browser to [http://localhost:8080](http://localhost:8080). + +## Tooling + +* [Eleventy (11ty)](https://www.11ty.dev/docs) +* [Diagrams](https://github.com/gmunguia/markdown-it-plantuml) with +[plantuml](https://plantuml.com). See also +[real-world-plantuml](https://real-world-plantuml.com). +* [Diagrams](https://github.com/agoose77/markdown-it-diagrams) with +[svgbob](https://github.com/ivanceras/svgbob) and +[mermaid](https://github.com/mermaid-js/mermaid). +* [arc42](https://arc42.org/overview) template. +* [c4model](https://c4model.com) for software architecture, and an +[implementation in plantuml](https://github.com/plantuml-stdlib/C4-PlantUML). diff --git a/docs/_data/metadata.json b/docs/_data/metadata.json new file mode 100755 index 000000000..69eabe9f6 --- /dev/null +++ b/docs/_data/metadata.json @@ -0,0 +1,21 @@ +{ + "title": "Help center", + "url": "https://docs.penpot.app/", + "description": "Design freedom for teams.", + "feed": { + "subtitle": "Penpot: design freedom for teams.", + "filename": "feed.xml", + "path": "/feed/feed.xml", + "id": "https://docs.penpot.app/" + }, + "jsonfeed": { + "path": "/feed/feed.json", + "url": "https://docs.penpot.app/feed/feed.json" + }, + "author": { + "name": "Penpot", + "email": "hello@penpot.app", + "url": "https://penpot.app" + }, + "twitter": "@penpotapp" +} diff --git a/docs/_includes/layouts/base.njk b/docs/_includes/layouts/base.njk new file mode 100644 index 000000000..724393deb --- /dev/null +++ b/docs/_includes/layouts/base.njk @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + {% metagen + title=title or metadata.title, + desc=desc or metadata.desc, + url="https://help.penpot.app" + page.url, + img="https://help.penpot.app/img/th-help-center.jpg", + img_alt=alt, + twitterHandle=twitter or metadata.twitter, + name=name + %} + + + + + + + + +
+
+ + + + + + + + Try Penpot - It's free + +
+
+
+
+ + {{ metadata.title }} + + + +
+
+ + + {{ content | safe }} + + + + + + + + + + + + diff --git a/docs/_includes/layouts/contributing-guide.njk b/docs/_includes/layouts/contributing-guide.njk new file mode 100644 index 000000000..861211e1b --- /dev/null +++ b/docs/_includes/layouts/contributing-guide.njk @@ -0,0 +1,36 @@ +--- +layout: layouts/base.njk +templateClass: tmpl-contributing-guide +--- + +{%- macro show_children(item) -%} + {%- for child in item | children | sorted('data.title') %} + {%- if loop.first -%}{%- endif -%} + {%- endfor %} +{%- endmacro -%} + +
+ + + + {{ content | safe }} + + +
diff --git a/docs/_includes/layouts/home.njk b/docs/_includes/layouts/home.njk new file mode 100644 index 000000000..1aab3bc6a --- /dev/null +++ b/docs/_includes/layouts/home.njk @@ -0,0 +1,7 @@ +--- +layout: layouts/base.njk +templateClass: tmpl-home +--- +
+ {{ content | safe }} +
diff --git a/docs/_includes/layouts/plugins-home.njk b/docs/_includes/layouts/plugins-home.njk new file mode 100644 index 000000000..ce169b92c --- /dev/null +++ b/docs/_includes/layouts/plugins-home.njk @@ -0,0 +1,27 @@ +--- +layout: layouts/base.njk +templateClass: tmpl-plugins-guide +--- + +{%- macro show_children(item) -%} + {%- for child in item | children | sorted('data.title') %} + {%- if loop.first -%}{%- endif -%} + {%- endfor %} +{%- endmacro -%} + +
+ + {{ content | safe }} + + +
diff --git a/docs/_includes/layouts/plugins-no-sidebar.njk b/docs/_includes/layouts/plugins-no-sidebar.njk new file mode 100644 index 000000000..c2e2f2d8a --- /dev/null +++ b/docs/_includes/layouts/plugins-no-sidebar.njk @@ -0,0 +1,28 @@ +--- +layout: layouts/base.njk +templateClass: tmpl-plugins-guide +--- + +{%- macro show_children(item) -%} + {%- for child in item | children | sorted('data.title') %} + {%- if loop.first -%}{%- endif -%} + {%- endfor %} +{%- endmacro -%} + +
+ + + {{ content | safe }} + + +
diff --git a/docs/_includes/layouts/plugins.njk b/docs/_includes/layouts/plugins.njk new file mode 100644 index 000000000..2d7b84183 --- /dev/null +++ b/docs/_includes/layouts/plugins.njk @@ -0,0 +1,36 @@ +--- +layout: layouts/base.njk +templateClass: tmpl-plugins-guide +--- + +{%- macro show_children(item) -%} + {%- for child in item | children | sorted('data.title') %} + {%- if loop.first -%}{%- endif -%} + {%- endfor %} +{%- endmacro -%} + +
+ + + + {{ content | safe }} + + +
diff --git a/docs/_includes/layouts/technical-guide.njk b/docs/_includes/layouts/technical-guide.njk new file mode 100644 index 000000000..646b6fdc6 --- /dev/null +++ b/docs/_includes/layouts/technical-guide.njk @@ -0,0 +1,36 @@ +--- +layout: layouts/base.njk +templateClass: tmpl-developer-guide +--- + +{%- macro show_children(item) -%} + {%- for child in item | children | sorted('data.title') %} + {%- if loop.first -%}{%- endif -%} + {%- endfor %} +{%- endmacro -%} + +
+ + + + {{ content | safe }} + + +
diff --git a/docs/_includes/layouts/user-guide.njk b/docs/_includes/layouts/user-guide.njk new file mode 100644 index 000000000..5ebf34313 --- /dev/null +++ b/docs/_includes/layouts/user-guide.njk @@ -0,0 +1,36 @@ +--- +layout: layouts/base.njk +templateClass: tmpl-user-guide +--- + +{%- macro show_children(item) -%} + {%- for child in item | children | sorted('data.title') %} + {%- if loop.first -%}{%- endif -%} + {%- endfor %} +{%- endmacro -%} + +
+ + + + {{ content | safe }} + + +
diff --git a/docs/contributing-guide/coc/index.njk b/docs/contributing-guide/coc/index.njk new file mode 100644 index 000000000..b9e3ffa86 --- /dev/null +++ b/docs/contributing-guide/coc/index.njk @@ -0,0 +1,13 @@ +--- +title: 04· Code of Conduct +--- + +

Code of conduct

+ +

As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.

+

We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.

+

Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.

+

Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.

+

This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.

+

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.

+

This Code of Conduct is adapted from the Contributor Covenant, version 1.1.0, available from http://contributor-covenant.org/version/1/1/0/

diff --git a/docs/contributing-guide/code-contributions/index.njk b/docs/contributing-guide/code-contributions/index.njk new file mode 100644 index 000000000..619295579 --- /dev/null +++ b/docs/contributing-guide/code-contributions/index.njk @@ -0,0 +1,108 @@ +--- +title: 03· Core code contributions +--- + +

Core code contributions

+ +

Details to know how to improve Penpot's core code

+ +

+Thinking of contributing to Penpot core but not sure where to start? We’ve made a curated selection of enhancements to help you with that. We believe that these tasks should be a great way to get started with Penpot development and quickly become an active contributor. +

+Here’s the list of enhancements labeled as "good first issue" +

+ +

Technical guide

+

Go to the Technical guide to get detailed explanations about how to get Penpot application and run it locally, to test it or make changes to it.

+ +

Pull requests

+

If you want propose a change or bug fix with the Pull-Request system firstly you should carefully read the DCO section and format your commits accordingly.

+

If you intend to fix a bug it's fine to submit a pull request right away but we still recommend to file an issue detailing what you're fixing. This is helpful in case we don't accept that specific fix but want to keep track of the issue.

+

If you want to implement or start working in a new feature, please open a question / discussion issue for it. No pull-request will be accepted without previous chat about the changes, independently if it is a new feature, already planned feature or small quick win.

+

If is going to be your first pull request, You can learn how from this free video series.

+

We will use the easy fix mark for tag for indicate issues that are easy for beginners.

+ +

Commit message guidelines

+

We have very precise rules over how our git commit messages can be formatted.

+

The commit message format is:

+
+
+<type>  <subject>
+
+[body]
+
+[footer]
+
+
+

Where type is:

+ +

More info:

+ +

The subject should be:

+ + +

Developer's Certificate of Origin (DCO)

+

By submitting code you are agree and can certify the below:

+
+
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+    have the right to submit it under the open source license
+    indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+    of my knowledge, is covered under an appropriate open source
+    license and I have the right under that license to submit that
+    work with modifications, whether created in whole or in part
+    by me, under the same open source license (unless I am
+    permitted to submit under a different license), as indicated
+    in the file; or
+
+(c) The contribution was provided directly to me by some other
+    person who certified (a), (b) or (c) and I have not modified
+    it.
+
+(d) I understand and agree that this project and the contribution
+    are public and that a record of the contribution (including all
+    personal information I submit with it, including my sign-off) is
+    maintained indefinitely and may be redistributed consistent with
+    this project or the open source license(s) involved.
+
+
+ +

Then, all your code patches (documentation are excluded) should contain a sign-off at the end of the patch/commit description body. It can be automatically added on adding -s parameter to git commit.

+

This is an example of the aspect of the line:

+
+
+Signed-off-by: Andrey Antukh 
+
+
+

Please, use your real name (sorry, no pseudonyms or anonymous contributions are allowed).

diff --git a/docs/contributing-guide/contributing-guide.json b/docs/contributing-guide/contributing-guide.json new file mode 100644 index 000000000..e257ef414 --- /dev/null +++ b/docs/contributing-guide/contributing-guide.json @@ -0,0 +1,4 @@ +{ + "layout": "layouts/contributing-guide.njk", + "tags": "contributing-guide" +} diff --git a/docs/contributing-guide/index.njk b/docs/contributing-guide/index.njk new file mode 100644 index 000000000..d6b0a74ca --- /dev/null +++ b/docs/contributing-guide/index.njk @@ -0,0 +1,47 @@ +--- +title: Contributing +eleventyNavigation: + key: Contributing + order: 3 +--- + +
+ User guide +
+ +

Contributing guide.

+ +

In this documentation you will find (almost) everything you need to know about how to contribute at Penpot.

+ + diff --git a/docs/contributing-guide/libraries-templates/index.njk b/docs/contributing-guide/libraries-templates/index.njk new file mode 100644 index 000000000..ce99f3a0f --- /dev/null +++ b/docs/contributing-guide/libraries-templates/index.njk @@ -0,0 +1,15 @@ +--- +title: 05· Libraries & Templates +--- + +

Libraries & templates

+ +libraries and templates + +

There are published Penpot files ready to use made by community members and Penpot core team members.

+ + diff --git a/docs/contributing-guide/reporting-bugs/index.njk b/docs/contributing-guide/reporting-bugs/index.njk new file mode 100644 index 000000000..2b385f3e0 --- /dev/null +++ b/docs/contributing-guide/reporting-bugs/index.njk @@ -0,0 +1,23 @@ +--- +title: 01· Reporting bugs +--- + +

Reporting bugs

+ +

Bug hunting is not difficult if you know how.

+ +

How to report a bug

+ +

We are using GitHub Issues for our public bugs. We keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn't already exist.

+ +

If you found a bug, please report it, as far as possible including:

+ + + +

Consider sending us an email first at support@penpot.app if you discovered a bug that you'd prefer to discuss in confidence (such as a security bug).

+ +

We don't have formal bug bounty program for security reports; this is an open source application and your contribution will be recognized in the changelog.

diff --git a/docs/contributing-guide/translations/index.njk b/docs/contributing-guide/translations/index.njk new file mode 100644 index 000000000..2d5bfae2b --- /dev/null +++ b/docs/contributing-guide/translations/index.njk @@ -0,0 +1,58 @@ +--- +title: 02· Translations +--- + +

Translations

+ +

Thank you for interest in contribute translating Penpot. Here you will find ways to do it.

+ +

How to become a Penpot translator

+

We are using Weblate as translation platform, so the first thing you need to be a Penpot translator is to have a Weblate account (you can register here).

+

To start translating at Penpot:

+
    +
  1. Open a github issue giving details about the language you want to translate (language), the type of translation (new language, new translation or change an existing translation) and your Weblate user.
  2. +
  3. If everything is correct we will get back to you providing you permissions to the actions needed.
  4. +
  5. You also might want to take a look at the guide for Translating using Weblate.
  6. +
+ +

Add a new language

+

To add a language that is still not among the Penpot language options:

+
    +
  1. Go to the languages list.
  2. +
  3. Press the "Start new translation" button.
  4. +
  5. Choose the language you want to translate to.
  6. +
  7. Press the "Start new translation" button at the start new translation page.
  8. +
  9. Start translating strings for the new language :)
  10. +
+

translations

+

translations

+ + +

Add a new translation

+

To add a new translation (a string with a lacking translation for a certain language) follow the next steps:

+
    +
  1. Go to the languages list.
  2. +
  3. Click the edit button (pencil icon) close to the name of the language where you want to add the missing translation or translations.
  4. +
  5. Find and select the translation/s to complete.
  6. +
  7. Complete the translation in the required input field.
  8. +
  9. Press the "Save· button.
  10. +
  11. Repeat the action with as many translation strings you can / you want ;)
  12. +
+

Saved new translations will automatically get the status "waiting for review". Our team will periodically check strings waiting for review and, if considered correct, will approve them.

+

translations

+

translations

+ + +

Change an approved translation

+

To edit an already approved translation string follow the next steps:

+
    +
  1. Go to the languages list.
  2. +
  3. Click the name of the language where is the translation you want to change.
  4. +
  5. Click the Browse button.
  6. +
  7. Find and select the translation/s to complete.
  8. +
  9. Change the translation in the input field.
  10. +
  11. Press the "Save" button if you have permissions.
  12. +
  13. If you don't have permissions to Save you can still press "Suggest" to make a suggestion.
  14. +
+

Saved editions will get the status "Waiting for review". Suggestions will get the status "Approved strings with suggestions". Our team will periodically check strings waiting for review and, if considered correct, will approve them.

+

translations

diff --git a/docs/css/index.css b/docs/css/index.css new file mode 100644 index 000000000..9b6b5c308 --- /dev/null +++ b/docs/css/index.css @@ -0,0 +1,1210 @@ +:root { + + --darkest: #000000; + --graydark: #1F1F1F; + --graymedium: #7B7D85; + --graylight: #E3E3E3; + --primary: #31EFB8; + --primarydark: #00af7d; + --white: #fff; + --advice: #dafffb; + + /* 2023 color pallete from redesigned Penpot web */ + --color-gray-light: #F5F8FB; + --color-gray-medium: #DDE3E9; + --color-gray-dark: #1C2022; +} +* { + box-sizing: border-box; +} +html, +body { + padding: 0; + margin: 0; + font-family: 'Work Sans',system-ui, sans-serif; + color: var(--color-gray-dark); +} +body { + background-color: var(--white); + /* background-image: linear-gradient(rgba(68, 68, 68, 0.05), white); + background-size: 100% 300px; + background-repeat: no-repeat; */ +} +p:last-child { + margin-bottom: 0; +} +p, +.tmpl-post li, +img, +h1, h2, h3, +.main-content { + max-width: 42rem; + position: relative; +} + +@media (max-width: 997px) { + .main-content { + max-width: 40rem; + } +} + +@media (max-width: 966px) { + .main-content { + max-width: 38rem; + } +} + +@media (max-width: 933px) { + .main-content { + max-width: 36rem; + } +} + +@media (max-width: 901px) { + .main-content { + max-width: 35rem; + } +} + +img { + max-width: 100%; +} +p, +li { + line-height: 1.6; +} +ul, ol { + margin-bottom: 2rem; + padding-left: 1rem; +} +li { + margin-bottom: 1rem; +} + +a[href] { + color: var(--primarydark); +} +a[href]:hover { + color: var(--darkest); +} +a[href]:visited { + /* color: var(--primarydark); */ +} + +/* href with code in the text */ +:not(pre) > a > code[class*="language-"] { + text-decoration: underline; +} + +main { + padding: 1rem; + /* background-image: url(/img/bg.png); + background-repeat: no-repeat; + background-position: center 120px; */ +} +header { + display: flex; + border-bottom: 1px solid var(--color-gray-medium); + padding: 1.6rem; + position: sticky; + top: 0; + width: 100%; + background-color: white; + backdrop-filter: blur(6px); + background: #FFFFFFD9; + z-index: 10; +} +header:after { + content: ""; + display: table; + clear: both; +} +header a[href] { + color: var(--graydark); +} +table { + margin: 1em 0; +} +table td, +table th { + padding-right: 1em; +} +kbd { + border: 1px solid var(--graylight); + border-radius: 4px; + padding: 0.4rem 0.6rem; + box-shadow: 0 1px 0px 1px rgb(0 0 0 / 10%); + background-color: var(--white); + margin-left: 0.16rem; + margin-right: 0.16rem; + white-space: nowrap; +} + +h1 { + font-size: 4.5rem; /* 72px */ +} +h2 { + font-size: 3rem; + margin-top: 6rem; +} +h3 { + font-size: 1.6rem; + margin-top: 4rem; + margin-bottom: 1rem; +} +h4 { + font-size: 1.2rem; + margin-top: 2rem; + margin-bottom: 1rem; +} +h5 { + font-size: 1.1rem; + margin-top: 2rem; + margin-bottom: 1rem; +} +p { + margin-bottom: 2rem; +} +p, li { + font-size: 1rem; + word-wrap: break-word; +} +.main-paragraph { + font-size: 1.25rem; /* Original 1.5rem */ +} + +hr { + background-color: var(--graylight); + margin-top: 6rem; + height: 1px; + border: none; +} +blockquote { + border-left: 4px solid var(--graylight); + margin-top: 2rem; + margin-left: 2rem; + padding-left: 2rem; +} + +figure { + padding: 0; + margin: 0 0 2rem 0; +} +figcaption { + color: var(--graymedium); +} + +pre, +code { + font-family: Consolas, Menlo, Monaco, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", "Courier New", Courier, monospace; + line-height: 1.5; +} +pre { + font-size: 14px; + line-height: 1.375; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; + padding: 1em; + margin: .5em 0; + background-color: var(--color-gray-light); + overflow: auto; +} +.highlight-line { + display: block; + padding: 0.125em 1em; + text-decoration: none; /* override del, ins, mark defaults */ + color: inherit; /* override del, ins, mark defaults */ +} + +/* allow highlighting empty lines */ +.highlight-line:empty:before { + content: " "; +} +/* avoid double line breaks when using display: block; */ +.highlight-line + br { + display: none; +} + +.highlight-line-isdir { + color: #b0b0b0; + background-color: #222; +} +.highlight-line-active { + background-color: #444; + background-color: hsla(0, 0%, 27%, .8); +} +.highlight-line-add { + background-color: #45844b; +} +.highlight-line-remove { + background-color: #902f2f; +} + +/* Header */ +.preheader { + background: var(--graydark); + padding: 1rem 1.6rem; + display: flex; + align-items: center; +} +.preheader .home { + font-size: 1em; /* 16px /16 */ + text-decoration: none; + flex: 1; +} +.preheader .home svg { + height: 36px; + width: 116px; + fill: var(--white); +} +.preheader .home :link:not(:hover) { + text-decoration: none; +} +.header-inner { + display: flex; + align-items: center; + max-width: 1400px; + width: 100%; + margin: auto; + position: relative; +} +.header-title { + font-size: 1.4rem; + display: inline; + font-weight: normal; + color: var(--graymedium); + text-decoration: none; + font-weight: 300; +} + +.button { + align-items: center; + background: var(--primary); + color: var(--graydark) !important; + border-radius: 8px; + cursor: pointer; + display: flex; + font-size: 1rem; + justify-content: center; + padding: 0.75rem 1.25rem; /* 12px 20px */ + text-align: center; + text-decoration: none; + transition-property: background; + transition-duration: .1s; + min-height: 3rem; /* 48px */ + line-height: 1; +} +.button:hover { + background: white; +} + + +/* Nav */ +.nav { + display: flex; + list-style: none; + margin: 0 auto; + padding: 0; + align-items: center; +} +.nav-item { + display: inline-block; + margin-left: 1.4rem; + margin-right: 1.4rem; + margin-bottom: 0; + font-size: 1rem; /* original value 1.1rem */ + font-weight: 500; +} +.nav-item a[href]:not(:hover) { + text-decoration: none; +} +.nav-item-active { + font-weight: 700; + text-decoration: underline; +} + +/* Posts list */ +.postlist { + list-style: none; + padding: 0; +} +.postlist-item { + counter-increment: start-from -1; +} +.postlist-item:before { + display: inline-block; + pointer-events: none; + content: "" counter(start-from, decimal-leading-zero) ". "; + line-height: 100%; + text-align: right; +} +.postlist-date, +.postlist-item:before { + font-size: 0.8125em; /* 13px /16 */ + color: var(--graydark); +} +.postlist-date { + word-spacing: -0.5px; +} +.postlist-link { + display: inline-block; + padding: 0.25em 0.1875em; /* 4px 3px /16 */ +} +.postlist-item-active .postlist-link { + font-weight: bold; +} +.tmpl-home .postlist-link { + font-size: 1.1875em; /* 19px /16 */ + font-weight: 700; +} + +.pre-footer { + padding: 2rem; + margin-top: 2rem; + display: flex; + justify-content: center; + border-top: 1px dashed var(--color-gray-medium); +} + +footer { + align-items: center; + background-color: var(--graydark); + display: flex; + flex-direction: column; + padding: 4rem 2rem; + width: 100%; +} +.footer-logo { + display: flex; + flex-direction: column; + color: var(--white); +} +.footer-inside { + display: grid; + grid-template-columns: 1.3fr 1fr 1fr 1fr 1fr; + max-width: 1400px; + width: 100%; +} +.footer-inside .main-logo { + margin-right: auto; +} +.footer-inside .main-logo svg { + fill: var(--white); + height: 50px; + width: 140px; +} +.footer-block { + display: flex; + flex-direction: column; + min-width: 20%; + margin: 1rem 0; +} +.footer-block--title { + color: var(--white); + font-weight: bold; + padding: 0; + list-style: none; +} +.footer-block li { + /* padding: .2rem 0; */ + padding: 0; +} +.footer-block li a { + color: var(--white); + /* font-size: 1.125rem; */ + font-size: 1rem; + text-decoration: none; + transition: color 200ms; +} +.footer-block li a:hover { + color: var(--primary); +} +.footer-text { + align-items: center; + display: flex; +} +.footer-text span { + color: var(--graymedium); +} +.footer-bottom { + max-width: 1400px; + width: 100%; + display: grid; + grid-template-columns: auto 1fr auto; + margin-top: 118px; + margin-bottom: 32px; +} +.footer-icon-list { + display: flex; + flex-direction: row; + fill: var(--white); + gap: 20px; + margin: 0; + height: 32px; +} +.footer-icon-list li { + margin-block-end: 0; + height: 32px; +} +.footer-icon-list a img{ + fill: var(--white); + width: 32px; + height: 32px; +} +.footer-icon-list a img:hover { + fill: var(--graylight); +} +.github-widget { + height: 28px; + display: grid; + grid-template-columns: auto auto; + align-content: center; + justify-content: center; +} +.github-link { + display: flex; + align-items: center; + justify-content: center; + height: 28px; + width: fit-content; + padding: 5px 10px; + font-size: 12px; + line-height: 16px; + border-radius: .25em 0 0 .25em; + font-weight: bold; + background-color: #ebf0f4; + border: 1px solid rgba(31, 35, 40, .15); + background-image: linear-gradient(180deg, #f6f8fa, #ebf0f4 90%); +} +.github-link[href]{ + color: #24292f; + text-decoration: none; +} +.github-link[href]:hover{ + color: var(--primarydark); +} +.widget-icon { + height: 16px; + width: 16px; +} +.github-link svg { + height: 16px; + width: 16px; + fill: #24292f; +} +.social-count { + display: flex; + align-items: center; + justify-content: center; + height: 28px; + width: fit-content; + padding: 5px 10px; + font-size: 12px; + line-height: 16px; + font-weight: bold; + background-color: #fff; + border-radius: 0 .25em .25em 0; + border: 1px solid rgba(31, 35, 40, .15); +} +.social-count[href]{ + color: #24292f; + text-decoration: none; +} +.social-count[href]:hover{ + color: var(--primarydark); +} +/* Tags */ +.post-tag { + display: inline-block; + vertical-align: text-top; + text-transform: uppercase; + font-size: 0.625em; /* 10px /16 */ + padding: 2px 4px; + margin-left: 0.8em; /* 8px /10 */ + background-color: red; + color: var(--white); + border-radius: 0.25em; /* 3px /12 */ + text-decoration: none; +} +a[href].post-tag, +a[href].post-tag:visited { + color: var(--white); +} + +/* Warning */ +.warning { + background-color: #ffc; + padding: 1em 0.625em; /* 16px 10px /16 */ +} +.warning ol:only-child { + margin: 0; +} + +/* Direct Links / Markdown Headers */ +.main-content .direct-link { + font-family: sans-serif; + text-decoration: none; + font-style: normal; + margin-left: 1rem; + position: absolute; + /* top: 0.4rem; */ +} +.main-content a[href].direct-link, +.main-content a[href].direct-link:visited { + color: transparent; + border-bottom: none; +} +.main-content a[href].direct-link:focus, +.main-content a[href].direct-link:focus:visited, +:hover > a[href].direct-link, +:hover > a[href].direct-link:visited { + color: var(--primarydark); +} +:hover > a[href].direct-link:hover { + color: var(--primary); + border-bottom: none; +} + +[id]::before { + visibility: hidden; + content: ''; +} + +:target[id]::before { + content: ''; + display: block; + visibility: hidden; + height: 94px; +} + +/* Particular classes */ + +.media-container { + width: 100%; + display: flex; + justify-content: center; +} +.main-container { + margin: auto; + max-width: 80rem; + min-height: 60vh; +} +.main-container.with-sidebar { + /* display: grid; + grid-template-columns: minmax(100px, 25%) 1fr; */ + display: flex; + flex-wrap: wrap; +} +.main-content { + margin: 0 auto; +} +.main-illus { + text-align: center; +} +.main-illus img { + margin-top: 2rem; + width: 60%; + max-width: 22rem; +} + +.main-content a[href], +.pre-footer a[href], +.contact-block a[href] { + color: var(--graydark); + text-decoration-line: none; + border-bottom: 2px solid var(--primary); +} +.main-content a[href]:hover, +.pre-footer a[href]:hover, +.contact-block a[href]:hover { + border-bottom: 2px solid var(--primarydark); + color: var(--darkest); +} +.main-content.plugins a[href], +.main-content a[href]:has(img), +.main-content a[href]:has(img):hover{ + border-bottom: none; +} + + +.main-container .sidebar { + padding-top: 0; + padding-right: 2rem; + position: sticky; + top: 8rem; + align-self: start; + max-width: 320px; +} +.main-container .sidebar a[href] { + color: var(--darkest); + text-decoration: none; +} +.main-container .sidebar .header { + font-size: 1.6rem; + font-weight: bold; + display: block; + margin-bottom: 2rem; +} +.main-container.with-sidebar .sidebar .header.mobile { + display: none; +} +.main-container .sidebar ul, +.main-container .sidebar ol { + list-style: none; + padding-left: 0; + margin: 0; + padding-top: 0.2rem; +} +.main-container .sidebar #toc > ul ul, +.main-container .sidebar #toc > ul ol { + padding-left: 1rem; +} +/* first level */ +.main-container .sidebar ul li, +.main-container .sidebar ul li a { + font-weight: 600; + padding-top: 1.2rem; + line-height: 1.2; + margin: 0; +} +.main-container .sidebar #toc > ul > li, +.main-container .sidebar #toc > ol > li { + padding-bottom: 1rem; + padding-top: 1rem; +} +/* second level */ +.main-container .sidebar ul li li, +.main-container .sidebar ul li li a { + font-weight: normal; + font-size: 1rem; +} +/* third level */ +.main-container .sidebar .toc li { +} + +.main-container .sidebar #toc { + /* position: fixed; */ + max-width: 300px; + max-height: calc(100vh - 8rem); + overflow: auto; + padding-right: 1rem; +} + +.advice { + background-color: var(--advice); + padding: 2rem; + border-radius: 4px; +} +.hint { + font-size: 0.9rem; + margin-bottom: 2rem; +} + +.search { + align-items: center; + display: flex; + flex-direction: column; + position: absolute; + right: 0; + top: -6px; + z-index: 1; +} +.search input { + border: 1px solid var(--color-gray-medium); + padding: 0.5rem 2rem 0.5rem 0.75rem; + width: 8rem; + transition: width 200ms 200ms; + height: 40px; + border-radius: 8px; +} +.search input::placeholder { + letter-spacing: .5px; +} +.search label { + position: absolute; + color: var(--graymedium); + left: 10px; + top: 7px; + display: none; +} +.search input:focus { + width: 18rem; +} +.search-icon { + position: absolute; + right: 13px; + top: 12px; + fill: var(--color-gray-dark); + pointer-events: none; +} +.search input:focus + .search-icon { + fill: var(--darkest); +} + + +#search-results { + background: white; + border: 1px solid var(--color-gray-medium); + list-style: none; + margin: 0; + padding: 0; + box-shadow: 0 0 10px rgba(68, 68, 68, 0.1); + max-height: 85vh; + overflow: auto; + width: 18rem; +} +#search-results li { + padding: 0; + margin: 0; +} +#search-results a { + display: block; + padding: 1rem; + font-weight: 600; +} +#search-results li:hover { + background-color: var(--advice); +} + +#search-results h3 { + font-size: 1rem; + font-weight: normal; + padding: 0; + margin: 0; +} + +#search-results a { + text-decoration: none; +} + +#no-results-found { + background: white; + border: 1px solid var(--color-gray-medium); + color: var(--color-gray-dark); + list-style: none; + margin: 0; + padding: 1rem 2rem; + box-shadow: 0 0 10px rgba(68, 68, 68, 0.1); + width: 18rem; +} + +#no-results-found p { + margin: 0; +} + +.help-sections { + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 100%; + padding: 0 0 4rem 0; + list-style: none; +} +.help-sections > li { + position: relative; + margin: 1rem; + background-color: #eee; + flex: 47%; + /* box-shadow: 0px 8px 20px rgb(28 56 71 / 5%); */ + border: 1px solid var(--color-gray-medium); + border-radius: 8px; + max-width: 47%; + min-height: 12rem; + background-color: white; +} +.illus { + background-size: auto 75%; + background-position: 0.6rem center; + padding-left: 12rem; + background-repeat: no-repeat; +} +.help-sections > li.no-link.illus-contact { + padding-left: 14.6rem; +} +.illus-userguide { + background-image: url(/img/home-userguide.png); +} +.illus-faq { + background-image: url(/img/home-faq.png); +} +.illus-techguide { + background-image: url(/img/home-techguide.png); +} +.illus-plugins { + background-image: url(/img/home-plugins.png); +} +.illus-contributing { + background-image: url(/img/home-contributing.png); +} +.illus-contact { + background-image: url(/img/home-contact.png); +} +.illus-libraries { + padding-left: 13rem; + background-size: auto 90%; + background-position: 0.6rem center; + background-image: url(/img/contributing-libraries.png); +} +.help-sections > li:hover { + box-shadow: rgb(0 0 0 / 5%) 0px 4px 16px; +} +.help-sections > li > a { + display: block; + padding: 2.6rem 2.2rem; + color: var(--graydark); + text-decoration: none; +} +.help-sections > li h2, +.help-sections > li h3 { + margin-top: 0; + margin-bottom: 0; + padding-top: 0; + line-height: 1.2; + font-size: 2rem; +} +.help-sections > li p { + color: var(--color-gray-dark); + font-size: 1rem; +} +.help-sections > li.no-link { + padding: 2.6rem; +} +.help-sections > li.soon:hover { + border-color: var(--graylight); +} +.help-sections > li.soon h2, +.help-sections > li.soon h3 { + opacity: 0.4; +} +.help-sections > li.github { + min-height: auto; + border: none; + border-radius: 0; + box-shadow: none; + border-top: 1px dashed var(--color-gray-medium); + border-bottom: 1px dashed var(--color-gray-medium); + margin-top: 4rem; + flex-basis: 100%; + max-width: 100%; +} +.help-sections > li.github a:hover h2, +.help-sections > li.github a:hover h3 { + color: var(--primarydark); + transition: color 50ms ease-in-out; +} +.help-sections > li.github a:hover svg { + fill: var(--primarydark); + transition: fill 50ms ease-in-out; +} +.help-sections > li.github a { + display: flex; + align-items: center; +} +.help-sections > li.github .content { + margin-left: 2rem; +} +.help-sections > li.github svg { + width: 72px; + height: 72px; +} +.contact-block { + text-align: center; + padding-top: 12rem; + margin-bottom: 6rem; + background-size: auto 75%; + background-position: center top; + background-repeat: no-repeat; + background-image: url(/img/home-contact.png); +} +.contact-block p, +.contact-block h2, +.contact-block h3 { + max-width: initial; +} + +.main-container h1 { + margin-top: 0rem; + padding-top: 5rem; +} +.main-container h2 { + margin-top: 0rem; + padding-top: 4rem; +} +.main-container h3 { + margin-top: 0rem; + padding-top: 2rem; +} + + +.intro-sections { + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 100%; + padding: 0 0 4rem 0; + list-style: none; + gap: 2rem; + margin: 2rem 0; +} +.intro-sections li { + position: relative; + margin: 0; + flex: 33%; + background-color: white; + /* box-shadow: 0px 4px 10px rgb(28 56 71 / 5%); */ + border: 1px solid var(--color-gray-medium); + border-radius: 8px; +} +.intro-sections li:hover { + box-shadow: rgb(0 0 0 / 5%) 0px 4px 16px; +} +.intro-sections li > a[href] { + display: block; + padding: 2.6rem; + color: var(--color-gray-dark); + text-decoration: none; + border-bottom: none; +} +.intro-sections li > a[href]:hover { + border-bottom: none; +} +.intro-sections li h2, +.intro-sections li h3 { + margin-top: 0; + padding-top: 0; + line-height: 1.2; + font-size: 2rem; + margin-bottom: 1rem; +} +.intro-sections li p { + font-size: 1.2rem; + line-height: 1.45; +} + +.plugins .illus-getting-started { + background-image: url(/img/plugins/getting_started.png); +} +.plugins .illus-create-plugin { + background-image: url(/img/plugins/create_plugin.png); +} +.plugins .illus-deployment { + background-image: url(/img/plugins/deployment.png); +} +.plugins .illus-api { + background-image: url(/img/plugins/api.png); +} +.plugins .illus-examples { + background-image: url(/img/plugins/examples.png); +} +.plugins .illus-faq { + background-image: url(/img/plugins/faqs.png); +} + +table, tr, th, td { + color: var(--graydark); + padding: 1rem; + vertical-align: top; + border: 1px solid var(--graylight); + border-collapse: collapse; + /* font-size: 1.10rem; */ + font-size: 1rem; +} +table { + background-color: white; + box-shadow: 0px 4px 20px rgb(28 56 71 / 5%); +} +/* th { + background: rgba(0,0,0,.06); +} */ +table>thead>tr:first-child>th { + font-weight: bold; + background: none repeat scroll 0 0 #ededed; +} +table tr:nth-child(odd) { + background: rgba(0,0,0,.02); +} + +@media (max-width: 1114px) { + .help-sections > li { + max-width: none; + } +} + +@media (max-width: 1024px) { + hr { + margin-top: 5rem; + } + .header-title { + display: none; + } + header { + padding: 1rem; + position: relative; + } + header .nav-item:first-child { + display: none; + } + .header-inner { + display: block; + } + .search { + position: relative; + width: 100%; + margin-top: 2rem; + } + .search input { + width: 100%; + font-size: 1rem; + } + .search input:focus { + /* width: calc(100vw - 1.4rem); */ + width: 100%; + } + .search input:focus + .search-icon { + display: none; + } + + #search-results { + width: 100%; + } + + #no-results-found { + width: 100%; + } + + + .preheader { + padding: 1rem; + } + .preheader .button { + width: 100%; + } + .home { + display: none; + } + .nav { + margin-left: 0; + } + .main-container.with-sidebar { + display: block; + } + .main-container.with-sidebar .sidebar { + padding: 0; + top: 0; + position: relative; + z-index: 1; + } + .main-container.with-sidebar .sidebar .header { + display: none; + } + .main-container.with-sidebar .sidebar .header.mobile { + display: block; + border: 1px solid var(--graylight); + border-radius: 4px; + padding: 1rem 1rem; + margin: 0; + position: relative; + background: white; + } + .main-container.with-sidebar .sidebar .header:before { + content: ""; + position: absolute; + visibility: visible; + z-index: 1; + width: 16px; + height: 16px; + top: 26px; + right: 20px; + background-image: url(/img/caret-down.svg); + background-size: 16px 16px; + background-repeat: no-repeat; + transition: transform 200ms ease-in-out; + } + + .main-container.with-sidebar .sidebar #toc { + position: relative; + margin-top: 0; + max-width: none; + z-index: 2; + padding-right: 0; + } + .main-container.with-sidebar .sidebar #toc > ul { + background: white; + border: 1px solid var(--graylight); + color: var(--graymedium); + list-style: none; + margin: 0; + padding: 2rem; + box-shadow: 2px 0 10px rgba(68, 68, 68, 0.1); + display: none; + } + .main-container.with-sidebar .sidebar #toc.open > ul{ + display: block; + } + .main-container.with-sidebar .sidebar .open .header:before { + transform: rotate(180deg); + } + .footer-inside { + flex-direction: column; + } + :target[id]::before { + display: inline; + } + +} +@media (max-width: 600px) { + hr { + margin-top: 4rem; + } + .main-container h1 { + padding-top: 3rem; + font-size: 3rem; + } + .main-container h2 { + padding-top: 2rem; + font-size: 2rem; + } + .main-container h3 { + padding-top: 1.4rem; + font-size: 1.4rem; + } + header .nav-item { + margin-left: 0.6rem; + margin-right: 0.6rem; + font-size: 1rem; + } + .main-container.with-sidebar .sidebar #toc { + padding: 0; + margin: 0; + } + .help-sections { + flex-direction: column; + } + .help-sections > li { + margin: 0 0 1rem 0; + } + .help-sections > li > a, + .help-sections .illus, + .help-sections > li.no-link.illus-contact { + padding: 1.4rem; + min-height: auto; + } + .help-sections .illus { + background-image: none; + } + .intro-sections .illus { + background-image: none; + padding-left: 0; +} + +} diff --git a/docs/css/prism-base16-monokai.dark.css b/docs/css/prism-base16-monokai.dark.css new file mode 100644 index 000000000..01e802439 --- /dev/null +++ b/docs/css/prism-base16-monokai.dark.css @@ -0,0 +1,92 @@ +/* Styles for syntax highlighting inside code blocks */ + +code[class*="language-"], pre[class*="language-"] { + font-size: 14px; + line-height: 1.375; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; + background: #272822; + color: #f8f8f2; + max-width: 40rem; +} +pre[class*="language-"] { + padding: 1.5em 1em; + margin: .5em 0; + overflow: auto; +} +:not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; +} +.token.comment, .token.prolog, .token.doctype, .token.cdata { + color: #75715e; +} +.token.punctuation { + color: #f8f8f2; +} +.token.namespace { + opacity: .7; +} +.token.operator, .token.boolean, .token.number { + color: #fd971f; +} +.token.property { + color: #f4bf75; +} +.token.tag { + color: #66d9ef; +} +.token.string { + color: #a1efe4; +} +.token.selector { + color: #ae81ff; +} +.token.attr-name { + color: #fd971f; +} +.token.entity, .token.url, .language-css .token.string, .style .token.string { + color: #a1efe4; +} +.token.attr-value, .token.keyword, .token.control, .token.directive, .token.unit { + color: #a6e22e; +} +.token.statement, .token.regex, .token.atrule { + color: #a1efe4; +} +.token.placeholder, .token.variable { + color: #66d9ef; +} +.token.deleted { + text-decoration: line-through; +} +.token.inserted { + border-bottom: 1px dotted #f9f8f5; + text-decoration: none; +} +.token.italic { + font-style: italic; +} +.token.important, .token.bold { + font-weight: bold; +} +.token.important { + color: #f92672; +} +.token.entity { + cursor: help; +} +pre > code.highlight { + outline: 0.4em solid #f92672; + outline-offset: .4em; +} diff --git a/docs/css/prism.css b/docs/css/prism.css new file mode 100644 index 000000000..01819a853 --- /dev/null +++ b/docs/css/prism.css @@ -0,0 +1,122 @@ +/** + * GHColors theme by Avi Aryan (http://aviaryan.in) + * Inspired by Github syntax coloring + */ + +code[class*="language-"], +pre[class*="language-"] { + color: #393A34; + font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + font-size: .9em; + line-height: 1.2em; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre > code[class*="language-"] { + font-size: 1em; +} + +pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { + background: #b3d4fc; +} + +pre[class*="language-"]::selection, pre[class*="language-"] ::selection, +code[class*="language-"]::selection, code[class*="language-"] ::selection { + background: #b3d4fc; +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; + border: 1px solid #dddddd; + background-color: white; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .2em; + padding-top: 1px; + padding-bottom: 1px; + background: #f8f8f8; + border: 1px solid #dddddd; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: #999988; + font-style: italic; +} + +.token.namespace { + opacity: .7; +} + +.token.string, +.token.attr-value { + color: #e3116c; +} + +.token.punctuation, +.token.operator { + color: #393A34; /* no highlight */ +} + +.token.entity, +.token.url, +.token.symbol, +.token.number, +.token.boolean, +.token.variable, +.token.constant, +.token.property, +.token.regex, +.token.inserted { + color: #36acaa; +} + +.token.atrule, +.token.keyword, +.token.attr-name, +.language-autohotkey .token.selector { + color: #00a4db; +} + +.token.function, +.token.deleted, +.language-autohotkey .token.tag { + color: #9a050f; +} + +.token.tag, +.token.selector, +.language-autohotkey .token.keyword { + color: #00009f; +} + +.token.important, +.token.function, +.token.bold { + font-weight: bold; +} + +.token.italic { + font-style: italic; +} diff --git a/docs/feed/feed.njk b/docs/feed/feed.njk new file mode 100755 index 000000000..161fc9362 --- /dev/null +++ b/docs/feed/feed.njk @@ -0,0 +1,29 @@ +--- +# Metadata comes from _data/metadata.json +permalink: "{{ metadata.feed.path }}" +eleventyExcludeFromCollections: true +--- + + + {{ metadata.title }} + {{ metadata.feed.subtitle }} + {% set absoluteUrl %}{{ metadata.feed.path | url | absoluteUrl(metadata.url) }}{% endset %} + + + {{ collections.posts | rssLastUpdatedDate }} + {{ metadata.feed.id }} + + {{ metadata.author.name }} + {{ metadata.author.email }} + + {%- for post in collections.posts | reverse %} + {% set absolutePostUrl %}{{ post.url | url | absoluteUrl(metadata.url) }}{% endset %} + + {{ post.data.title }} + + {{ post.date | rssDate }} + {{ absolutePostUrl }} + {{ post.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }} + + {%- endfor %} + diff --git a/docs/feed/htaccess.njk b/docs/feed/htaccess.njk new file mode 100644 index 000000000..ac4a3c5f8 --- /dev/null +++ b/docs/feed/htaccess.njk @@ -0,0 +1,6 @@ +--- +permalink: feed/.htaccess +eleventyExcludeFromCollections: true +--- +# For Apache, to show `{{ metadata.feed.filename }}` when browsing to directory /feed/ (hide the file!) +DirectoryIndex {{ metadata.feed.filename }} diff --git a/docs/feed/json.njk b/docs/feed/json.njk new file mode 100644 index 000000000..0658efa34 --- /dev/null +++ b/docs/feed/json.njk @@ -0,0 +1,31 @@ +--- +# Metadata comes from _data/metadata.json +permalink: "{{ metadata.jsonfeed.path }}" +eleventyExcludeFromCollections: true +--- +{ + "version": "https://jsonfeed.org/version/1", + "title": "{{ metadata.title }}", + "home_page_url": "{{ metadata.url }}", + "feed_url": "{{ metadata.jsonfeed.url }}", + "description": "{{ metadata.description }}", + "author": { + "name": "{{ metadata.author.name }}", + "url": "{{ metadata.author.url }}" + }, + "items": [ + {%- for post in collections.posts | reverse %} + {%- set absolutePostUrl %}{{ post.url | url | absoluteUrl(metadata.url) }}{% endset -%} + { + "id": "{{ absolutePostUrl }}", + "url": "{{ absolutePostUrl }}", + "title": "{{ post.data.title }}", + "content_html": {% if post.templateContent %}{{ post.templateContent | dump | safe }}{% else %}""{% endif %}, + "date_published": "{{ post.date | rssDate }}" + } + {%- if not loop.last -%} + , + {%- endif -%} + {%- endfor %} + ] +} diff --git a/docs/img/.gitkeep b/docs/img/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/docs/img/a11y-tree-btn.webp b/docs/img/a11y-tree-btn.webp new file mode 100644 index 000000000..02fdccf74 Binary files /dev/null and b/docs/img/a11y-tree-btn.webp differ diff --git a/docs/img/accesstokens-created.png b/docs/img/accesstokens-created.png new file mode 100644 index 000000000..a578f3b2d Binary files /dev/null and b/docs/img/accesstokens-created.png differ diff --git a/docs/img/accesstokens-empty.png b/docs/img/accesstokens-empty.png new file mode 100644 index 000000000..7306ccb25 Binary files /dev/null and b/docs/img/accesstokens-empty.png differ diff --git a/docs/img/accesstokens-generate-0.png b/docs/img/accesstokens-generate-0.png new file mode 100644 index 000000000..06eadd4d0 Binary files /dev/null and b/docs/img/accesstokens-generate-0.png differ diff --git a/docs/img/accesstokens-generate-1.png b/docs/img/accesstokens-generate-1.png new file mode 100644 index 000000000..8640179b9 Binary files /dev/null and b/docs/img/accesstokens-generate-1.png differ diff --git a/docs/img/accesstokens-generate-2.png b/docs/img/accesstokens-generate-2.png new file mode 100644 index 000000000..e649c2698 Binary files /dev/null and b/docs/img/accesstokens-generate-2.png differ diff --git a/docs/img/artboards-creation.gif b/docs/img/artboards-creation.gif new file mode 100644 index 000000000..3241512bf Binary files /dev/null and b/docs/img/artboards-creation.gif differ diff --git a/docs/img/artboards-grid-int.png b/docs/img/artboards-grid-int.png new file mode 100644 index 000000000..29aae8834 Binary files /dev/null and b/docs/img/artboards-grid-int.png differ diff --git a/docs/img/artboards-move.gif b/docs/img/artboards-move.gif new file mode 100644 index 000000000..1418237ca Binary files /dev/null and b/docs/img/artboards-move.gif differ diff --git a/docs/img/assets-add-typo.gif b/docs/img/assets-add-typo.gif new file mode 100644 index 000000000..500e43039 Binary files /dev/null and b/docs/img/assets-add-typo.gif differ diff --git a/docs/img/assets-add.gif b/docs/img/assets-add.gif new file mode 100644 index 000000000..211f2e1c9 Binary files /dev/null and b/docs/img/assets-add.gif differ diff --git a/docs/img/assets-edit.gif b/docs/img/assets-edit.gif new file mode 100644 index 000000000..dbb2ce3ea Binary files /dev/null and b/docs/img/assets-edit.gif differ diff --git a/docs/img/assets-filter.gif b/docs/img/assets-filter.gif new file mode 100644 index 000000000..474dc550f Binary files /dev/null and b/docs/img/assets-filter.gif differ diff --git a/docs/img/assets-order.gif b/docs/img/assets-order.gif new file mode 100644 index 000000000..bec4a510f Binary files /dev/null and b/docs/img/assets-order.gif differ diff --git a/docs/img/assets-search.png b/docs/img/assets-search.png new file mode 100644 index 000000000..76df44504 Binary files /dev/null and b/docs/img/assets-search.png differ diff --git a/docs/img/assets-use.gif b/docs/img/assets-use.gif new file mode 100644 index 000000000..e3e413615 Binary files /dev/null and b/docs/img/assets-use.gif differ diff --git a/docs/img/assets-viewmode.gif b/docs/img/assets-viewmode.gif new file mode 100644 index 000000000..839a015b7 Binary files /dev/null and b/docs/img/assets-viewmode.gif differ diff --git a/docs/img/bg.png b/docs/img/bg.png new file mode 100644 index 000000000..2dea762f6 Binary files /dev/null and b/docs/img/bg.png differ diff --git a/docs/img/board-clip-content.gif b/docs/img/board-clip-content.gif new file mode 100644 index 000000000..75c072907 Binary files /dev/null and b/docs/img/board-clip-content.gif differ diff --git a/docs/img/board-create.gif b/docs/img/board-create.gif new file mode 100644 index 000000000..815af3cc3 Binary files /dev/null and b/docs/img/board-create.gif differ diff --git a/docs/img/board-show-fill.png b/docs/img/board-show-fill.png new file mode 100644 index 000000000..34cef3d06 Binary files /dev/null and b/docs/img/board-show-fill.png differ diff --git a/docs/img/board-show-viewmode.png b/docs/img/board-show-viewmode.png new file mode 100644 index 000000000..c1ea4b1e1 Binary files /dev/null and b/docs/img/board-show-viewmode.png differ diff --git a/docs/img/booleans.gif b/docs/img/booleans.gif new file mode 100644 index 000000000..3fece39b0 Binary files /dev/null and b/docs/img/booleans.gif differ diff --git a/docs/img/caret-down.svg b/docs/img/caret-down.svg new file mode 100644 index 000000000..da33d2ee9 --- /dev/null +++ b/docs/img/caret-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/img/codemode-activate.gif b/docs/img/codemode-activate.gif new file mode 100644 index 000000000..0a5587041 Binary files /dev/null and b/docs/img/codemode-activate.gif differ diff --git a/docs/img/codemode-code.png b/docs/img/codemode-code.png new file mode 100644 index 000000000..a65e86be1 Binary files /dev/null and b/docs/img/codemode-code.png differ diff --git a/docs/img/codemode-copy.gif b/docs/img/codemode-copy.gif new file mode 100644 index 000000000..5e7351b69 Binary files /dev/null and b/docs/img/codemode-copy.gif differ diff --git a/docs/img/codemode-measures.gif b/docs/img/codemode-measures.gif new file mode 100644 index 000000000..8bee7cdfe Binary files /dev/null and b/docs/img/codemode-measures.gif differ diff --git a/docs/img/codemode-properties.gif b/docs/img/codemode-properties.gif new file mode 100644 index 000000000..6525b9d76 Binary files /dev/null and b/docs/img/codemode-properties.gif differ diff --git a/docs/img/collapsing-groups.gif b/docs/img/collapsing-groups.gif new file mode 100644 index 000000000..f053e342f Binary files /dev/null and b/docs/img/collapsing-groups.gif differ diff --git a/docs/img/color-palette.gif b/docs/img/color-palette.gif new file mode 100644 index 000000000..f54511dd3 Binary files /dev/null and b/docs/img/color-palette.gif differ diff --git a/docs/img/color-picker.png b/docs/img/color-picker.png new file mode 100644 index 000000000..9004c2dfe Binary files /dev/null and b/docs/img/color-picker.png differ diff --git a/docs/img/color-selection.png b/docs/img/color-selection.png new file mode 100644 index 000000000..6c0862306 Binary files /dev/null and b/docs/img/color-selection.png differ diff --git a/docs/img/comments-add.gif b/docs/img/comments-add.gif new file mode 100644 index 000000000..3ac4118b8 Binary files /dev/null and b/docs/img/comments-add.gif differ diff --git a/docs/img/comments-dashboard.png b/docs/img/comments-dashboard.png new file mode 100644 index 000000000..85e26e68d Binary files /dev/null and b/docs/img/comments-dashboard.png differ diff --git a/docs/img/comments-thread.png b/docs/img/comments-thread.png new file mode 100644 index 000000000..31d5ee9ef Binary files /dev/null and b/docs/img/comments-thread.png differ diff --git a/docs/img/comments-view.png b/docs/img/comments-view.png new file mode 100644 index 000000000..8e9fa10dc Binary files /dev/null and b/docs/img/comments-view.png differ diff --git a/docs/img/components-create-folder.gif b/docs/img/components-create-folder.gif new file mode 100644 index 000000000..8a7556989 Binary files /dev/null and b/docs/img/components-create-folder.gif differ diff --git a/docs/img/components-create.gif b/docs/img/components-create.gif new file mode 100644 index 000000000..5f3d37c4a Binary files /dev/null and b/docs/img/components-create.gif differ diff --git a/docs/img/components-overrides.gif b/docs/img/components-overrides.gif new file mode 100644 index 000000000..00bc03d9b Binary files /dev/null and b/docs/img/components-overrides.gif differ diff --git a/docs/img/components-update.gif b/docs/img/components-update.gif new file mode 100644 index 000000000..7059ccc57 Binary files /dev/null and b/docs/img/components-update.gif differ diff --git a/docs/img/components-updatenotification.png b/docs/img/components-updatenotification.png new file mode 100644 index 000000000..d9428bf86 Binary files /dev/null and b/docs/img/components-updatenotification.png differ diff --git a/docs/img/components/components-annotation.mp4 b/docs/img/components/components-annotation.mp4 new file mode 100644 index 000000000..660981d38 Binary files /dev/null and b/docs/img/components/components-annotation.mp4 differ diff --git a/docs/img/components/components-annotation.webp b/docs/img/components/components-annotation.webp new file mode 100644 index 000000000..0ec1693e3 Binary files /dev/null and b/docs/img/components/components-annotation.webp differ diff --git a/docs/img/components/components-annotations-inspect.webp b/docs/img/components/components-annotations-inspect.webp new file mode 100644 index 000000000..fab1a3890 Binary files /dev/null and b/docs/img/components/components-annotations-inspect.webp differ diff --git a/docs/img/components/components-create.mp4 b/docs/img/components/components-create.mp4 new file mode 100644 index 000000000..ccaab5c7c Binary files /dev/null and b/docs/img/components/components-create.mp4 differ diff --git a/docs/img/components/components-create.webp b/docs/img/components/components-create.webp new file mode 100644 index 000000000..615c13ea0 Binary files /dev/null and b/docs/img/components/components-create.webp differ diff --git a/docs/img/components/components-delete.mp4 b/docs/img/components/components-delete.mp4 new file mode 100644 index 000000000..b0dd39404 Binary files /dev/null and b/docs/img/components/components-delete.mp4 differ diff --git a/docs/img/components/components-delete.webp b/docs/img/components/components-delete.webp new file mode 100644 index 000000000..255ee2059 Binary files /dev/null and b/docs/img/components/components-delete.webp differ diff --git a/docs/img/components/components-drag.mp4 b/docs/img/components/components-drag.mp4 new file mode 100644 index 000000000..a71c4e87d Binary files /dev/null and b/docs/img/components/components-drag.mp4 differ diff --git a/docs/img/components/components-drag.webp b/docs/img/components/components-drag.webp new file mode 100644 index 000000000..aeb8de475 Binary files /dev/null and b/docs/img/components/components-drag.webp differ diff --git a/docs/img/components/components-duplicate-main.mp4 b/docs/img/components/components-duplicate-main.mp4 new file mode 100644 index 000000000..eae853111 Binary files /dev/null and b/docs/img/components/components-duplicate-main.mp4 differ diff --git a/docs/img/components/components-duplicate-main.webp b/docs/img/components/components-duplicate-main.webp new file mode 100644 index 000000000..992676c16 Binary files /dev/null and b/docs/img/components/components-duplicate-main.webp differ diff --git a/docs/img/components/components-group.mp4 b/docs/img/components/components-group.mp4 new file mode 100644 index 000000000..8ffd1fe6a Binary files /dev/null and b/docs/img/components/components-group.mp4 differ diff --git a/docs/img/components/components-group.webp b/docs/img/components/components-group.webp new file mode 100644 index 000000000..617b3630d Binary files /dev/null and b/docs/img/components/components-group.webp differ diff --git a/docs/img/components/components-main-copy.webp b/docs/img/components/components-main-copy.webp new file mode 100644 index 000000000..1fa634e59 Binary files /dev/null and b/docs/img/components/components-main-copy.webp differ diff --git a/docs/img/components/components-overrides.webp b/docs/img/components/components-overrides.webp new file mode 100644 index 000000000..2a8e3fc0e Binary files /dev/null and b/docs/img/components/components-overrides.webp differ diff --git a/docs/img/components/components-page-main.webp b/docs/img/components/components-page-main.webp new file mode 100644 index 000000000..348f7d152 Binary files /dev/null and b/docs/img/components/components-page-main.webp differ diff --git a/docs/img/components/components-reset-overrides.mp4 b/docs/img/components/components-reset-overrides.mp4 new file mode 100644 index 000000000..f30dcc4ea Binary files /dev/null and b/docs/img/components/components-reset-overrides.mp4 differ diff --git a/docs/img/components/components-reset-overrides.webp b/docs/img/components/components-reset-overrides.webp new file mode 100644 index 000000000..16effe02e Binary files /dev/null and b/docs/img/components/components-reset-overrides.webp differ diff --git a/docs/img/components/components-restore.webp b/docs/img/components/components-restore.webp new file mode 100644 index 000000000..2efcf3059 Binary files /dev/null and b/docs/img/components/components-restore.webp differ diff --git a/docs/img/components/components-show-asset.mp4 b/docs/img/components/components-show-asset.mp4 new file mode 100644 index 000000000..d1c16a599 Binary files /dev/null and b/docs/img/components/components-show-asset.mp4 differ diff --git a/docs/img/components/components-show-asset.webp b/docs/img/components/components-show-asset.webp new file mode 100644 index 000000000..ecc7c34d6 Binary files /dev/null and b/docs/img/components/components-show-asset.webp differ diff --git a/docs/img/components/components-show-main.mp4 b/docs/img/components/components-show-main.mp4 new file mode 100644 index 000000000..cba4d46ed Binary files /dev/null and b/docs/img/components/components-show-main.mp4 differ diff --git a/docs/img/components/components-show-main.webp b/docs/img/components/components-show-main.webp new file mode 100644 index 000000000..bd36169c9 Binary files /dev/null and b/docs/img/components/components-show-main.webp differ diff --git a/docs/img/components/components-swap.mp4 b/docs/img/components/components-swap.mp4 new file mode 100644 index 000000000..16bbb0925 Binary files /dev/null and b/docs/img/components/components-swap.mp4 differ diff --git a/docs/img/components/components-swap.webp b/docs/img/components/components-swap.webp new file mode 100644 index 000000000..b7b033369 Binary files /dev/null and b/docs/img/components/components-swap.webp differ diff --git a/docs/img/components/components-update-shared.webp b/docs/img/components/components-update-shared.webp new file mode 100644 index 000000000..57a1a224a Binary files /dev/null and b/docs/img/components/components-update-shared.webp differ diff --git a/docs/img/components/components-update.webp b/docs/img/components/components-update.webp new file mode 100644 index 000000000..cd3ebcb56 Binary files /dev/null and b/docs/img/components/components-update.webp differ diff --git a/docs/img/constraints-horizontal.png b/docs/img/constraints-horizontal.png new file mode 100644 index 000000000..fe1225d37 Binary files /dev/null and b/docs/img/constraints-horizontal.png differ diff --git a/docs/img/constraints-vertical.png b/docs/img/constraints-vertical.png new file mode 100644 index 000000000..29bcc1376 Binary files /dev/null and b/docs/img/constraints-vertical.png differ diff --git a/docs/img/constraints.gif b/docs/img/constraints.gif new file mode 100644 index 000000000..eb2785c70 Binary files /dev/null and b/docs/img/constraints.gif differ diff --git a/docs/img/contributing-libraries.png b/docs/img/contributing-libraries.png new file mode 100644 index 000000000..c309e9775 Binary files /dev/null and b/docs/img/contributing-libraries.png differ diff --git a/docs/img/csstricks-00-basic-terminology.svg b/docs/img/csstricks-00-basic-terminology.svg new file mode 100644 index 000000000..3ac8d22ef --- /dev/null +++ b/docs/img/csstricks-00-basic-terminology.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/img/customfonts-families.png b/docs/img/customfonts-families.png new file mode 100644 index 000000000..9f2bc0a15 Binary files /dev/null and b/docs/img/customfonts-families.png differ diff --git a/docs/img/customfonts-upload.png b/docs/img/customfonts-upload.png new file mode 100644 index 000000000..301456584 Binary files /dev/null and b/docs/img/customfonts-upload.png differ diff --git a/docs/img/customfonts-use.gif b/docs/img/customfonts-use.gif new file mode 100644 index 000000000..4416b268f Binary files /dev/null and b/docs/img/customfonts-use.gif differ diff --git a/docs/img/customfonts.png b/docs/img/customfonts.png new file mode 100644 index 000000000..fb7b86852 Binary files /dev/null and b/docs/img/customfonts.png differ diff --git a/docs/img/dynamic-alignment-measurement.gif b/docs/img/dynamic-alignment-measurement.gif new file mode 100644 index 000000000..27f471db4 Binary files /dev/null and b/docs/img/dynamic-alignment-measurement.gif differ diff --git a/docs/img/dynamic-alignment.gif b/docs/img/dynamic-alignment.gif new file mode 100644 index 000000000..f8e1513bb Binary files /dev/null and b/docs/img/dynamic-alignment.gif differ diff --git a/docs/img/export-artboards-pdf.png b/docs/img/export-artboards-pdf.png new file mode 100644 index 000000000..f1be5afa6 Binary files /dev/null and b/docs/img/export-artboards-pdf.png differ diff --git a/docs/img/export-howto.gif b/docs/img/export-howto.gif new file mode 100644 index 000000000..28beba256 Binary files /dev/null and b/docs/img/export-howto.gif differ diff --git a/docs/img/export-menu.png b/docs/img/export-menu.png new file mode 100644 index 000000000..0037520c5 Binary files /dev/null and b/docs/img/export-menu.png differ diff --git a/docs/img/export-multiple.png b/docs/img/export-multiple.png new file mode 100644 index 000000000..133241a1c Binary files /dev/null and b/docs/img/export-multiple.png differ diff --git a/docs/img/export-progress.png b/docs/img/export-progress.png new file mode 100644 index 000000000..2b858ddb4 Binary files /dev/null and b/docs/img/export-progress.png differ diff --git a/docs/img/export-remove.gif b/docs/img/export-remove.gif new file mode 100644 index 000000000..5a9f2ba88 Binary files /dev/null and b/docs/img/export-remove.gif differ diff --git a/docs/img/export-selection-window.png b/docs/img/export-selection-window.png new file mode 100644 index 000000000..2511a9085 Binary files /dev/null and b/docs/img/export-selection-window.png differ diff --git a/docs/img/export-selection.png b/docs/img/export-selection.png new file mode 100644 index 000000000..e53e93a76 Binary files /dev/null and b/docs/img/export-selection.png differ diff --git a/docs/img/export/export-main-menu.webp b/docs/img/export/export-main-menu.webp new file mode 100644 index 000000000..9844752af Binary files /dev/null and b/docs/img/export/export-main-menu.webp differ diff --git a/docs/img/export/export-multiple.webp b/docs/img/export/export-multiple.webp new file mode 100644 index 000000000..2abcb8697 Binary files /dev/null and b/docs/img/export/export-multiple.webp differ diff --git a/docs/img/export/export-pdf.webp b/docs/img/export/export-pdf.webp new file mode 100644 index 000000000..d239f8ebb Binary files /dev/null and b/docs/img/export/export-pdf.webp differ diff --git a/docs/img/export/export-presets.mp4 b/docs/img/export/export-presets.mp4 new file mode 100644 index 000000000..72f5277ce Binary files /dev/null and b/docs/img/export/export-presets.mp4 differ diff --git a/docs/img/export/export-presets.webp b/docs/img/export/export-presets.webp new file mode 100644 index 000000000..0faf8ab3e Binary files /dev/null and b/docs/img/export/export-presets.webp differ diff --git a/docs/img/export/export-progress.webp b/docs/img/export/export-progress.webp new file mode 100644 index 000000000..3aebaf542 Binary files /dev/null and b/docs/img/export/export-progress.webp differ diff --git a/docs/img/export/export-selection.webp b/docs/img/export/export-selection.webp new file mode 100644 index 000000000..260eecc40 Binary files /dev/null and b/docs/img/export/export-selection.webp differ diff --git a/docs/img/favicon.png b/docs/img/favicon.png new file mode 100644 index 000000000..fb0056b4f Binary files /dev/null and b/docs/img/favicon.png differ diff --git a/docs/img/flexible-layouts/flex-properties-element-absolute.webp b/docs/img/flexible-layouts/flex-properties-element-absolute.webp new file mode 100644 index 000000000..2bfcad76c Binary files /dev/null and b/docs/img/flexible-layouts/flex-properties-element-absolute.webp differ diff --git a/docs/img/flexible-layouts/flex-properties-element.webp b/docs/img/flexible-layouts/flex-properties-element.webp new file mode 100644 index 000000000..ee12772a7 Binary files /dev/null and b/docs/img/flexible-layouts/flex-properties-element.webp differ diff --git a/docs/img/flexible-layouts/flex-properties.webp b/docs/img/flexible-layouts/flex-properties.webp new file mode 100644 index 000000000..a6bf85409 Binary files /dev/null and b/docs/img/flexible-layouts/flex-properties.webp differ diff --git a/docs/img/flexible-layouts/grid-properties-cell.webp b/docs/img/flexible-layouts/grid-properties-cell.webp new file mode 100644 index 000000000..aaba8133a Binary files /dev/null and b/docs/img/flexible-layouts/grid-properties-cell.webp differ diff --git a/docs/img/flexible-layouts/grid-properties-container.webp b/docs/img/flexible-layouts/grid-properties-container.webp new file mode 100644 index 000000000..45e65b96f Binary files /dev/null and b/docs/img/flexible-layouts/grid-properties-container.webp differ diff --git a/docs/img/flexible-layouts/layouts-add.webp b/docs/img/flexible-layouts/layouts-add.webp new file mode 100644 index 000000000..10806bb25 Binary files /dev/null and b/docs/img/flexible-layouts/layouts-add.webp differ diff --git a/docs/img/flexible-layouts/layouts-flex-arrange.mp4 b/docs/img/flexible-layouts/layouts-flex-arrange.mp4 new file mode 100644 index 000000000..d36588810 Binary files /dev/null and b/docs/img/flexible-layouts/layouts-flex-arrange.mp4 differ diff --git a/docs/img/flexible-layouts/layouts-flex-arrange.webp b/docs/img/flexible-layouts/layouts-flex-arrange.webp new file mode 100644 index 000000000..53c331c10 Binary files /dev/null and b/docs/img/flexible-layouts/layouts-flex-arrange.webp differ diff --git a/docs/img/flexible-layouts/layouts-flex-button.mp4 b/docs/img/flexible-layouts/layouts-flex-button.mp4 new file mode 100644 index 000000000..0e3e1f0c8 Binary files /dev/null and b/docs/img/flexible-layouts/layouts-flex-button.mp4 differ diff --git a/docs/img/flexible-layouts/layouts-flex-button.webp b/docs/img/flexible-layouts/layouts-flex-button.webp new file mode 100644 index 000000000..233d81094 Binary files /dev/null and b/docs/img/flexible-layouts/layouts-flex-button.webp differ diff --git a/docs/img/flexible-layouts/layouts-flex-code.gif b/docs/img/flexible-layouts/layouts-flex-code.gif new file mode 100644 index 000000000..eaf3c555d Binary files /dev/null and b/docs/img/flexible-layouts/layouts-flex-code.gif differ diff --git a/docs/img/flexible-layouts/layouts-flex-list.mp4 b/docs/img/flexible-layouts/layouts-flex-list.mp4 new file mode 100644 index 000000000..1723f2b02 Binary files /dev/null and b/docs/img/flexible-layouts/layouts-flex-list.mp4 differ diff --git a/docs/img/flexible-layouts/layouts-flex-list.webp b/docs/img/flexible-layouts/layouts-flex-list.webp new file mode 100644 index 000000000..22362377f Binary files /dev/null and b/docs/img/flexible-layouts/layouts-flex-list.webp differ diff --git a/docs/img/flexible-layouts/layouts-flex-spacing-add.mp4 b/docs/img/flexible-layouts/layouts-flex-spacing-add.mp4 new file mode 100644 index 000000000..057067f8a Binary files /dev/null and b/docs/img/flexible-layouts/layouts-flex-spacing-add.mp4 differ diff --git a/docs/img/flexible-layouts/layouts-flex-spacing-add.webp b/docs/img/flexible-layouts/layouts-flex-spacing-add.webp new file mode 100644 index 000000000..832cdc72d Binary files /dev/null and b/docs/img/flexible-layouts/layouts-flex-spacing-add.webp differ diff --git a/docs/img/flexible-layouts/layouts-flex-spacing.mp4 b/docs/img/flexible-layouts/layouts-flex-spacing.mp4 new file mode 100644 index 000000000..502dac6ff Binary files /dev/null and b/docs/img/flexible-layouts/layouts-flex-spacing.mp4 differ diff --git a/docs/img/flexible-layouts/layouts-flex-spacing.webp b/docs/img/flexible-layouts/layouts-flex-spacing.webp new file mode 100644 index 000000000..9a18e6508 Binary files /dev/null and b/docs/img/flexible-layouts/layouts-flex-spacing.webp differ diff --git a/docs/img/flexible-layouts/layouts-flex-wrap.mp4 b/docs/img/flexible-layouts/layouts-flex-wrap.mp4 new file mode 100644 index 000000000..01cd3736d Binary files /dev/null and b/docs/img/flexible-layouts/layouts-flex-wrap.mp4 differ diff --git a/docs/img/flexible-layouts/layouts-flex-wrap.webp b/docs/img/flexible-layouts/layouts-flex-wrap.webp new file mode 100644 index 000000000..0ea58122a Binary files /dev/null and b/docs/img/flexible-layouts/layouts-flex-wrap.webp differ diff --git a/docs/img/flexible-layouts/layouts-flex-zindex.mp4 b/docs/img/flexible-layouts/layouts-flex-zindex.mp4 new file mode 100644 index 000000000..9be396ffb Binary files /dev/null and b/docs/img/flexible-layouts/layouts-flex-zindex.mp4 differ diff --git a/docs/img/flexible-layouts/layouts-flex-zindex.webp b/docs/img/flexible-layouts/layouts-flex-zindex.webp new file mode 100644 index 000000000..c17236e92 Binary files /dev/null and b/docs/img/flexible-layouts/layouts-flex-zindex.webp differ diff --git a/docs/img/flexible-layouts/layouts-grid-area.mp4 b/docs/img/flexible-layouts/layouts-grid-area.mp4 new file mode 100644 index 000000000..33fff760e Binary files /dev/null and b/docs/img/flexible-layouts/layouts-grid-area.mp4 differ diff --git a/docs/img/flexible-layouts/layouts-grid-area.webp b/docs/img/flexible-layouts/layouts-grid-area.webp new file mode 100644 index 000000000..6fbe793b4 Binary files /dev/null and b/docs/img/flexible-layouts/layouts-grid-area.webp differ diff --git a/docs/img/flexible-layouts/layouts-grid-code.gif b/docs/img/flexible-layouts/layouts-grid-code.gif new file mode 100644 index 000000000..d66ce011a Binary files /dev/null and b/docs/img/flexible-layouts/layouts-grid-code.gif differ diff --git a/docs/img/flexible-layouts/layouts-grid-edit-sidebar.mp4 b/docs/img/flexible-layouts/layouts-grid-edit-sidebar.mp4 new file mode 100644 index 000000000..8a2415133 Binary files /dev/null and b/docs/img/flexible-layouts/layouts-grid-edit-sidebar.mp4 differ diff --git a/docs/img/flexible-layouts/layouts-grid-edit-sidebar.webp b/docs/img/flexible-layouts/layouts-grid-edit-sidebar.webp new file mode 100644 index 000000000..e22b37070 Binary files /dev/null and b/docs/img/flexible-layouts/layouts-grid-edit-sidebar.webp differ diff --git a/docs/img/flexible-layouts/layouts-grid-edit.mp4 b/docs/img/flexible-layouts/layouts-grid-edit.mp4 new file mode 100644 index 000000000..5d5c15e0b Binary files /dev/null and b/docs/img/flexible-layouts/layouts-grid-edit.mp4 differ diff --git a/docs/img/flexible-layouts/layouts-grid-edit.webp b/docs/img/flexible-layouts/layouts-grid-edit.webp new file mode 100644 index 000000000..d8ff92e7d Binary files /dev/null and b/docs/img/flexible-layouts/layouts-grid-edit.webp differ diff --git a/docs/img/flexible-layouts/layouts-grid-main.mp4 b/docs/img/flexible-layouts/layouts-grid-main.mp4 new file mode 100644 index 000000000..2510d02f5 Binary files /dev/null and b/docs/img/flexible-layouts/layouts-grid-main.mp4 differ diff --git a/docs/img/flexible-layouts/layouts-grid-main.webp b/docs/img/flexible-layouts/layouts-grid-main.webp new file mode 100644 index 000000000..10a61c743 Binary files /dev/null and b/docs/img/flexible-layouts/layouts-grid-main.webp differ diff --git a/docs/img/flexible-layouts/layouts-grid-place.mp4 b/docs/img/flexible-layouts/layouts-grid-place.mp4 new file mode 100644 index 000000000..ec77a65fa Binary files /dev/null and b/docs/img/flexible-layouts/layouts-grid-place.mp4 differ diff --git a/docs/img/flexible-layouts/layouts-grid-place.webp b/docs/img/flexible-layouts/layouts-grid-place.webp new file mode 100644 index 000000000..0f7f506e2 Binary files /dev/null and b/docs/img/flexible-layouts/layouts-grid-place.webp differ diff --git a/docs/img/flexlayout-add.gif b/docs/img/flexlayout-add.gif new file mode 100644 index 000000000..cd859c569 Binary files /dev/null and b/docs/img/flexlayout-add.gif differ diff --git a/docs/img/flexlayout-arrange.gif b/docs/img/flexlayout-arrange.gif new file mode 100644 index 000000000..e56bb046e Binary files /dev/null and b/docs/img/flexlayout-arrange.gif differ diff --git a/docs/img/flexlayout-button.gif b/docs/img/flexlayout-button.gif new file mode 100644 index 000000000..1f7520119 Binary files /dev/null and b/docs/img/flexlayout-button.gif differ diff --git a/docs/img/flexlayout-inspect.gif b/docs/img/flexlayout-inspect.gif new file mode 100644 index 000000000..7522b3f6f Binary files /dev/null and b/docs/img/flexlayout-inspect.gif differ diff --git a/docs/img/flexlayout-list.gif b/docs/img/flexlayout-list.gif new file mode 100644 index 000000000..4ad8cab5d Binary files /dev/null and b/docs/img/flexlayout-list.gif differ diff --git a/docs/img/flexlayout-position-absolute.png b/docs/img/flexlayout-position-absolute.png new file mode 100644 index 000000000..1e1fdbede Binary files /dev/null and b/docs/img/flexlayout-position-absolute.png differ diff --git a/docs/img/flexlayout-position-static.png b/docs/img/flexlayout-position-static.png new file mode 100644 index 000000000..d66f01938 Binary files /dev/null and b/docs/img/flexlayout-position-static.png differ diff --git a/docs/img/flexlayout-position-z.gif b/docs/img/flexlayout-position-z.gif new file mode 100644 index 000000000..d387fd67d Binary files /dev/null and b/docs/img/flexlayout-position-z.gif differ diff --git a/docs/img/flexlayout-properties.png b/docs/img/flexlayout-properties.png new file mode 100644 index 000000000..7e634402f Binary files /dev/null and b/docs/img/flexlayout-properties.png differ diff --git a/docs/img/flexlayout-spacing-add.gif b/docs/img/flexlayout-spacing-add.gif new file mode 100644 index 000000000..5b242036c Binary files /dev/null and b/docs/img/flexlayout-spacing-add.gif differ diff --git a/docs/img/flexlayout-spacing.gif b/docs/img/flexlayout-spacing.gif new file mode 100644 index 000000000..b22b3438f Binary files /dev/null and b/docs/img/flexlayout-spacing.gif differ diff --git a/docs/img/flexlayout-wrap.gif b/docs/img/flexlayout-wrap.gif new file mode 100644 index 000000000..90085841a Binary files /dev/null and b/docs/img/flexlayout-wrap.gif differ diff --git a/docs/img/focus-mode.gif b/docs/img/focus-mode.gif new file mode 100644 index 000000000..4f7318809 Binary files /dev/null and b/docs/img/focus-mode.gif differ diff --git a/docs/img/github.svg b/docs/img/github.svg new file mode 100644 index 000000000..caf11c515 --- /dev/null +++ b/docs/img/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/img/grids-columns.gif b/docs/img/grids-columns.gif new file mode 100644 index 000000000..26c678173 Binary files /dev/null and b/docs/img/grids-columns.gif differ diff --git a/docs/img/grids-create.gif b/docs/img/grids-create.gif new file mode 100644 index 000000000..47f06848c Binary files /dev/null and b/docs/img/grids-create.gif differ diff --git a/docs/img/grids-defaults.gif b/docs/img/grids-defaults.gif new file mode 100644 index 000000000..f204dc3ac Binary files /dev/null and b/docs/img/grids-defaults.gif differ diff --git a/docs/img/grids-hide-remove.gif b/docs/img/grids-hide-remove.gif new file mode 100644 index 000000000..f96c73f3c Binary files /dev/null and b/docs/img/grids-hide-remove.gif differ diff --git a/docs/img/grids-rows.gif b/docs/img/grids-rows.gif new file mode 100644 index 000000000..3847f776f Binary files /dev/null and b/docs/img/grids-rows.gif differ diff --git a/docs/img/grids-snap-visibility.gif b/docs/img/grids-snap-visibility.gif new file mode 100644 index 000000000..77aaa566c Binary files /dev/null and b/docs/img/grids-snap-visibility.gif differ diff --git a/docs/img/grids-square.gif b/docs/img/grids-square.gif new file mode 100644 index 000000000..90b0e520b Binary files /dev/null and b/docs/img/grids-square.gif differ diff --git a/docs/img/guides.gif b/docs/img/guides.gif new file mode 100644 index 000000000..887893dfb Binary files /dev/null and b/docs/img/guides.gif differ diff --git a/docs/img/handoff-code.png b/docs/img/handoff-code.png new file mode 100644 index 000000000..7c259d59b Binary files /dev/null and b/docs/img/handoff-code.png differ diff --git a/docs/img/handoff-info.png b/docs/img/handoff-info.png new file mode 100644 index 000000000..70a344a07 Binary files /dev/null and b/docs/img/handoff-info.png differ diff --git a/docs/img/history-item.gif b/docs/img/history-item.gif new file mode 100644 index 000000000..7acd1eba7 Binary files /dev/null and b/docs/img/history-item.gif differ diff --git a/docs/img/history-navigate.gif b/docs/img/history-navigate.gif new file mode 100644 index 000000000..2cc8dfae2 Binary files /dev/null and b/docs/img/history-navigate.gif differ diff --git a/docs/img/home-contact.png b/docs/img/home-contact.png new file mode 100644 index 000000000..9fc29008d Binary files /dev/null and b/docs/img/home-contact.png differ diff --git a/docs/img/home-contributing.png b/docs/img/home-contributing.png new file mode 100644 index 000000000..d44451bde Binary files /dev/null and b/docs/img/home-contributing.png differ diff --git a/docs/img/home-faq.png b/docs/img/home-faq.png new file mode 100644 index 000000000..587edde42 Binary files /dev/null and b/docs/img/home-faq.png differ diff --git a/docs/img/home-plugins.png b/docs/img/home-plugins.png new file mode 100644 index 000000000..98372a12e Binary files /dev/null and b/docs/img/home-plugins.png differ diff --git a/docs/img/home-techguide.png b/docs/img/home-techguide.png new file mode 100644 index 000000000..171245938 Binary files /dev/null and b/docs/img/home-techguide.png differ diff --git a/docs/img/home-userguide.png b/docs/img/home-userguide.png new file mode 100644 index 000000000..882851932 Binary files /dev/null and b/docs/img/home-userguide.png differ diff --git a/docs/img/import-export-menu.png b/docs/img/import-export-menu.png new file mode 100644 index 000000000..20411ba8c Binary files /dev/null and b/docs/img/import-export-menu.png differ diff --git a/docs/img/import-export.png b/docs/img/import-export.png new file mode 100644 index 000000000..065fd3a8c Binary files /dev/null and b/docs/img/import-export.png differ diff --git a/docs/img/import-export/export-card.webp b/docs/img/import-export/export-card.webp new file mode 100644 index 000000000..7d07149e6 Binary files /dev/null and b/docs/img/import-export/export-card.webp differ diff --git a/docs/img/import-export/export-libraries.webp b/docs/img/import-export/export-libraries.webp new file mode 100644 index 000000000..4c7a19daa Binary files /dev/null and b/docs/img/import-export/export-libraries.webp differ diff --git a/docs/img/import-export/export-menu.webp b/docs/img/import-export/export-menu.webp new file mode 100644 index 000000000..4977929b3 Binary files /dev/null and b/docs/img/import-export/export-menu.webp differ diff --git a/docs/img/import-export/export-multiple.mp4 b/docs/img/import-export/export-multiple.mp4 new file mode 100644 index 000000000..8dabf8535 Binary files /dev/null and b/docs/img/import-export/export-multiple.mp4 differ diff --git a/docs/img/import-export/export-multiple.webp b/docs/img/import-export/export-multiple.webp new file mode 100644 index 000000000..ea1f829dd Binary files /dev/null and b/docs/img/import-export/export-multiple.webp differ diff --git a/docs/img/import-export/import-menu.webp b/docs/img/import-export/import-menu.webp new file mode 100644 index 000000000..096b48bf3 Binary files /dev/null and b/docs/img/import-export/import-menu.webp differ diff --git a/docs/img/import-export/import-selection.webp b/docs/img/import-export/import-selection.webp new file mode 100644 index 000000000..498cd8f65 Binary files /dev/null and b/docs/img/import-export/import-selection.webp differ diff --git a/docs/img/importing-export-file.png b/docs/img/importing-export-file.png new file mode 100644 index 000000000..f3e6b9241 Binary files /dev/null and b/docs/img/importing-export-file.png differ diff --git a/docs/img/importing-export.gif b/docs/img/importing-export.gif new file mode 100644 index 000000000..67336b65d Binary files /dev/null and b/docs/img/importing-export.gif differ diff --git a/docs/img/importing-export.png b/docs/img/importing-export.png new file mode 100644 index 000000000..fbcee3458 Binary files /dev/null and b/docs/img/importing-export.png differ diff --git a/docs/img/importing-import-options.png b/docs/img/importing-import-options.png new file mode 100644 index 000000000..e404a1194 Binary files /dev/null and b/docs/img/importing-import-options.png differ diff --git a/docs/img/importing-import.png b/docs/img/importing-import.png new file mode 100644 index 000000000..be4da01c7 Binary files /dev/null and b/docs/img/importing-import.png differ diff --git a/docs/img/importing-libraries.png b/docs/img/importing-libraries.png new file mode 100644 index 000000000..9a162fd2e Binary files /dev/null and b/docs/img/importing-libraries.png differ diff --git a/docs/img/inspect-code.png b/docs/img/inspect-code.png new file mode 100644 index 000000000..60465451f Binary files /dev/null and b/docs/img/inspect-code.png differ diff --git a/docs/img/inspect-copy.gif b/docs/img/inspect-copy.gif new file mode 100644 index 000000000..1347fab01 Binary files /dev/null and b/docs/img/inspect-copy.gif differ diff --git a/docs/img/inspect-info.gif b/docs/img/inspect-info.gif new file mode 100644 index 000000000..d4c7b3704 Binary files /dev/null and b/docs/img/inspect-info.gif differ diff --git a/docs/img/inspect-workspace.gif b/docs/img/inspect-workspace.gif new file mode 100644 index 000000000..1a2ced093 Binary files /dev/null and b/docs/img/inspect-workspace.gif differ diff --git a/docs/img/inspect/inspect-code.mp4 b/docs/img/inspect/inspect-code.mp4 new file mode 100644 index 000000000..2bccfea37 Binary files /dev/null and b/docs/img/inspect/inspect-code.mp4 differ diff --git a/docs/img/inspect/inspect-code.webp b/docs/img/inspect/inspect-code.webp new file mode 100644 index 000000000..64603683e Binary files /dev/null and b/docs/img/inspect/inspect-code.webp differ diff --git a/docs/img/inspect/inspect-copy.mp4 b/docs/img/inspect/inspect-copy.mp4 new file mode 100644 index 000000000..656676e1d Binary files /dev/null and b/docs/img/inspect/inspect-copy.mp4 differ diff --git a/docs/img/inspect/inspect-copy.webp b/docs/img/inspect/inspect-copy.webp new file mode 100644 index 000000000..8cd7a9bb8 Binary files /dev/null and b/docs/img/inspect/inspect-copy.webp differ diff --git a/docs/img/inspect/inspect-measures.mp4 b/docs/img/inspect/inspect-measures.mp4 new file mode 100644 index 000000000..4c3312d39 Binary files /dev/null and b/docs/img/inspect/inspect-measures.mp4 differ diff --git a/docs/img/inspect/inspect-measures.webp b/docs/img/inspect/inspect-measures.webp new file mode 100644 index 000000000..2cdb94847 Binary files /dev/null and b/docs/img/inspect/inspect-measures.webp differ diff --git a/docs/img/inspect/inspect-properties.mp4 b/docs/img/inspect/inspect-properties.mp4 new file mode 100644 index 000000000..ccaa5ad74 Binary files /dev/null and b/docs/img/inspect/inspect-properties.mp4 differ diff --git a/docs/img/inspect/inspect-properties.webp b/docs/img/inspect/inspect-properties.webp new file mode 100644 index 000000000..d84d045c4 Binary files /dev/null and b/docs/img/inspect/inspect-properties.webp differ diff --git a/docs/img/inspect/inspect-viewmode.mp4 b/docs/img/inspect/inspect-viewmode.mp4 new file mode 100644 index 000000000..4ac8f9f05 Binary files /dev/null and b/docs/img/inspect/inspect-viewmode.mp4 differ diff --git a/docs/img/inspect/inspect-viewmode.webp b/docs/img/inspect/inspect-viewmode.webp new file mode 100644 index 000000000..5adfebe36 Binary files /dev/null and b/docs/img/inspect/inspect-viewmode.webp differ diff --git a/docs/img/inspect/inspect-workspace.mp4 b/docs/img/inspect/inspect-workspace.mp4 new file mode 100644 index 000000000..aa177167f Binary files /dev/null and b/docs/img/inspect/inspect-workspace.mp4 differ diff --git a/docs/img/inspect/inspect-workspace.webp b/docs/img/inspect/inspect-workspace.webp new file mode 100644 index 000000000..ea0a67b55 Binary files /dev/null and b/docs/img/inspect/inspect-workspace.webp differ diff --git a/docs/img/instagram.svg b/docs/img/instagram.svg new file mode 100644 index 000000000..eddbfc8f6 --- /dev/null +++ b/docs/img/instagram.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/img/interactions-add.gif b/docs/img/interactions-add.gif new file mode 100644 index 000000000..e9cdc6621 Binary files /dev/null and b/docs/img/interactions-add.gif differ diff --git a/docs/img/interactions-remove.png b/docs/img/interactions-remove.png new file mode 100644 index 000000000..8dc603957 Binary files /dev/null and b/docs/img/interactions-remove.png differ diff --git a/docs/img/interactions-view-options.png b/docs/img/interactions-view-options.png new file mode 100644 index 000000000..b9581569c Binary files /dev/null and b/docs/img/interactions-view-options.png differ diff --git a/docs/img/interactions-view.gif b/docs/img/interactions-view.gif new file mode 100644 index 000000000..9365e392b Binary files /dev/null and b/docs/img/interactions-view.gif differ diff --git a/docs/img/interface-viewmode.png b/docs/img/interface-viewmode.png new file mode 100644 index 000000000..01141b83f Binary files /dev/null and b/docs/img/interface-viewmode.png differ diff --git a/docs/img/interface-workspace.png b/docs/img/interface-workspace.png new file mode 100644 index 000000000..601c321d8 Binary files /dev/null and b/docs/img/interface-workspace.png differ diff --git a/docs/img/interface/dashboard-dark.webp b/docs/img/interface/dashboard-dark.webp new file mode 100644 index 000000000..c0a3d53cb Binary files /dev/null and b/docs/img/interface/dashboard-dark.webp differ diff --git a/docs/img/interface/dashboard-light.webp b/docs/img/interface/dashboard-light.webp new file mode 100644 index 000000000..e04bbbd4b Binary files /dev/null and b/docs/img/interface/dashboard-light.webp differ diff --git a/docs/img/interface/viewmode-dark.webp b/docs/img/interface/viewmode-dark.webp new file mode 100644 index 000000000..59ff3668f Binary files /dev/null and b/docs/img/interface/viewmode-dark.webp differ diff --git a/docs/img/interface/viewmode-light.webp b/docs/img/interface/viewmode-light.webp new file mode 100644 index 000000000..0f9c6ba5c Binary files /dev/null and b/docs/img/interface/viewmode-light.webp differ diff --git a/docs/img/interface/workspace-dark.webp b/docs/img/interface/workspace-dark.webp new file mode 100644 index 000000000..6c15bb526 Binary files /dev/null and b/docs/img/interface/workspace-dark.webp differ diff --git a/docs/img/interface/workspace-light.webp b/docs/img/interface/workspace-light.webp new file mode 100644 index 000000000..83606806d Binary files /dev/null and b/docs/img/interface/workspace-light.webp differ diff --git a/docs/img/layer-create.gif b/docs/img/layer-create.gif new file mode 100644 index 000000000..b7204d29e Binary files /dev/null and b/docs/img/layer-create.gif differ diff --git a/docs/img/layer-delete.gif b/docs/img/layer-delete.gif new file mode 100644 index 000000000..149425296 Binary files /dev/null and b/docs/img/layer-delete.gif differ diff --git a/docs/img/layer-duplicate.gif b/docs/img/layer-duplicate.gif new file mode 100644 index 000000000..af25ca334 Binary files /dev/null and b/docs/img/layer-duplicate.gif differ diff --git a/docs/img/layers-align.gif b/docs/img/layers-align.gif new file mode 100644 index 000000000..d7ad7746a Binary files /dev/null and b/docs/img/layers-align.gif differ diff --git a/docs/img/layers-distribute.gif b/docs/img/layers-distribute.gif new file mode 100644 index 000000000..b1808cb09 Binary files /dev/null and b/docs/img/layers-distribute.gif differ diff --git a/docs/img/layers-flipping.gif b/docs/img/layers-flipping.gif new file mode 100644 index 000000000..f9580bc10 Binary files /dev/null and b/docs/img/layers-flipping.gif differ diff --git a/docs/img/layers-grouping.gif b/docs/img/layers-grouping.gif new file mode 100644 index 000000000..e3d532624 Binary files /dev/null and b/docs/img/layers-grouping.gif differ diff --git a/docs/img/layers-hide-lock.gif b/docs/img/layers-hide-lock.gif new file mode 100644 index 000000000..1ef9ca067 Binary files /dev/null and b/docs/img/layers-hide-lock.gif differ diff --git a/docs/img/layers-masking.gif b/docs/img/layers-masking.gif new file mode 100644 index 000000000..71b5df2f4 Binary files /dev/null and b/docs/img/layers-masking.gif differ diff --git a/docs/img/layers-moving.gif b/docs/img/layers-moving.gif new file mode 100644 index 000000000..3c733d2a6 Binary files /dev/null and b/docs/img/layers-moving.gif differ diff --git a/docs/img/layers-panel.gif b/docs/img/layers-panel.gif new file mode 100644 index 000000000..a43c6f27a Binary files /dev/null and b/docs/img/layers-panel.gif differ diff --git a/docs/img/layers-resizing.gif b/docs/img/layers-resizing.gif new file mode 100644 index 000000000..89283cf9d Binary files /dev/null and b/docs/img/layers-resizing.gif differ diff --git a/docs/img/layers-rotating.gif b/docs/img/layers-rotating.gif new file mode 100644 index 000000000..1e7913bbe Binary files /dev/null and b/docs/img/layers-rotating.gif differ diff --git a/docs/img/layers-search.gif b/docs/img/layers-search.gif new file mode 100644 index 000000000..14ef20def Binary files /dev/null and b/docs/img/layers-search.gif differ diff --git a/docs/img/layers-select-menu.gif b/docs/img/layers-select-menu.gif new file mode 100644 index 000000000..365c1d940 Binary files /dev/null and b/docs/img/layers-select-menu.gif differ diff --git a/docs/img/layers-selecting-1.gif b/docs/img/layers-selecting-1.gif new file mode 100644 index 000000000..19a277c58 Binary files /dev/null and b/docs/img/layers-selecting-1.gif differ diff --git a/docs/img/layers-selecting-2.gif b/docs/img/layers-selecting-2.gif new file mode 100644 index 000000000..ce08fffe0 Binary files /dev/null and b/docs/img/layers-selecting-2.gif differ diff --git a/docs/img/layers-selecting-3.gif b/docs/img/layers-selecting-3.gif new file mode 100644 index 000000000..f74a0b149 Binary files /dev/null and b/docs/img/layers-selecting-3.gif differ diff --git a/docs/img/layers/layers-align.mp4 b/docs/img/layers/layers-align.mp4 new file mode 100644 index 000000000..e5de93523 Binary files /dev/null and b/docs/img/layers/layers-align.mp4 differ diff --git a/docs/img/layers/layers-align.webp b/docs/img/layers/layers-align.webp new file mode 100644 index 000000000..e0bc3a7f7 Binary files /dev/null and b/docs/img/layers/layers-align.webp differ diff --git a/docs/img/layers/layers-boolean.mp4 b/docs/img/layers/layers-boolean.mp4 new file mode 100644 index 000000000..44021c711 Binary files /dev/null and b/docs/img/layers/layers-boolean.mp4 differ diff --git a/docs/img/layers/layers-boolean.webp b/docs/img/layers/layers-boolean.webp new file mode 100644 index 000000000..d79be372d Binary files /dev/null and b/docs/img/layers/layers-boolean.webp differ diff --git a/docs/img/layers/layers-collapse.mp4 b/docs/img/layers/layers-collapse.mp4 new file mode 100644 index 000000000..f2bc9ce5a Binary files /dev/null and b/docs/img/layers/layers-collapse.mp4 differ diff --git a/docs/img/layers/layers-collapse.webp b/docs/img/layers/layers-collapse.webp new file mode 100644 index 000000000..77c9ca90c Binary files /dev/null and b/docs/img/layers/layers-collapse.webp differ diff --git a/docs/img/layers/layers-constraints-h.webp b/docs/img/layers/layers-constraints-h.webp new file mode 100644 index 000000000..cbe2c7acf Binary files /dev/null and b/docs/img/layers/layers-constraints-h.webp differ diff --git a/docs/img/layers/layers-constraints-v.webp b/docs/img/layers/layers-constraints-v.webp new file mode 100644 index 000000000..3e745c8f4 Binary files /dev/null and b/docs/img/layers/layers-constraints-v.webp differ diff --git a/docs/img/layers/layers-constraints.mp4 b/docs/img/layers/layers-constraints.mp4 new file mode 100644 index 000000000..8de0da045 Binary files /dev/null and b/docs/img/layers/layers-constraints.mp4 differ diff --git a/docs/img/layers/layers-constraints.webp b/docs/img/layers/layers-constraints.webp new file mode 100644 index 000000000..a4ba647b6 Binary files /dev/null and b/docs/img/layers/layers-constraints.webp differ diff --git a/docs/img/layers/layers-create.mp4 b/docs/img/layers/layers-create.mp4 new file mode 100644 index 000000000..e5b30e933 Binary files /dev/null and b/docs/img/layers/layers-create.mp4 differ diff --git a/docs/img/layers/layers-create.webp b/docs/img/layers/layers-create.webp new file mode 100644 index 000000000..87ea73635 Binary files /dev/null and b/docs/img/layers/layers-create.webp differ diff --git a/docs/img/layers/layers-deepselect.mp4 b/docs/img/layers/layers-deepselect.mp4 new file mode 100644 index 000000000..623b721a7 Binary files /dev/null and b/docs/img/layers/layers-deepselect.mp4 differ diff --git a/docs/img/layers/layers-deepselect.webp b/docs/img/layers/layers-deepselect.webp new file mode 100644 index 000000000..dfcbb9cf2 Binary files /dev/null and b/docs/img/layers/layers-deepselect.webp differ diff --git a/docs/img/layers/layers-distribute.mp4 b/docs/img/layers/layers-distribute.mp4 new file mode 100644 index 000000000..1130a78f3 Binary files /dev/null and b/docs/img/layers/layers-distribute.mp4 differ diff --git a/docs/img/layers/layers-distribute.webp b/docs/img/layers/layers-distribute.webp new file mode 100644 index 000000000..923f1a082 Binary files /dev/null and b/docs/img/layers/layers-distribute.webp differ diff --git a/docs/img/layers/layers-duplicate.mp4 b/docs/img/layers/layers-duplicate.mp4 new file mode 100644 index 000000000..5563dbc9c Binary files /dev/null and b/docs/img/layers/layers-duplicate.mp4 differ diff --git a/docs/img/layers/layers-duplicate.webp b/docs/img/layers/layers-duplicate.webp new file mode 100644 index 000000000..3ff932e12 Binary files /dev/null and b/docs/img/layers/layers-duplicate.webp differ diff --git a/docs/img/layers/layers-flip.mp4 b/docs/img/layers/layers-flip.mp4 new file mode 100644 index 000000000..39baa8174 Binary files /dev/null and b/docs/img/layers/layers-flip.mp4 differ diff --git a/docs/img/layers/layers-flip.webp b/docs/img/layers/layers-flip.webp new file mode 100644 index 000000000..7c29b5554 Binary files /dev/null and b/docs/img/layers/layers-flip.webp differ diff --git a/docs/img/layers/layers-focus.mp4 b/docs/img/layers/layers-focus.mp4 new file mode 100644 index 000000000..1f4db7bb3 Binary files /dev/null and b/docs/img/layers/layers-focus.mp4 differ diff --git a/docs/img/layers/layers-focus.webp b/docs/img/layers/layers-focus.webp new file mode 100644 index 000000000..084dd35ac Binary files /dev/null and b/docs/img/layers/layers-focus.webp differ diff --git a/docs/img/layers/layers-group.mp4 b/docs/img/layers/layers-group.mp4 new file mode 100644 index 000000000..ad6e66701 Binary files /dev/null and b/docs/img/layers/layers-group.mp4 differ diff --git a/docs/img/layers/layers-group.webp b/docs/img/layers/layers-group.webp new file mode 100644 index 000000000..fd5194829 Binary files /dev/null and b/docs/img/layers/layers-group.webp differ diff --git a/docs/img/layers/layers-hide-lock.mp4 b/docs/img/layers/layers-hide-lock.mp4 new file mode 100644 index 000000000..b03acb02e Binary files /dev/null and b/docs/img/layers/layers-hide-lock.mp4 differ diff --git a/docs/img/layers/layers-hide-lock.webp b/docs/img/layers/layers-hide-lock.webp new file mode 100644 index 000000000..865182252 Binary files /dev/null and b/docs/img/layers/layers-hide-lock.webp differ diff --git a/docs/img/layers/layers-mask.mp4 b/docs/img/layers/layers-mask.mp4 new file mode 100644 index 000000000..be5cb2271 Binary files /dev/null and b/docs/img/layers/layers-mask.mp4 differ diff --git a/docs/img/layers/layers-mask.webp b/docs/img/layers/layers-mask.webp new file mode 100644 index 000000000..4006abd5f Binary files /dev/null and b/docs/img/layers/layers-mask.webp differ diff --git a/docs/img/layers/layers-move.mp4 b/docs/img/layers/layers-move.mp4 new file mode 100644 index 000000000..3fb809e8b Binary files /dev/null and b/docs/img/layers/layers-move.mp4 differ diff --git a/docs/img/layers/layers-move.webp b/docs/img/layers/layers-move.webp new file mode 100644 index 000000000..7067bd324 Binary files /dev/null and b/docs/img/layers/layers-move.webp differ diff --git a/docs/img/layers/layers-multiselect.mp4 b/docs/img/layers/layers-multiselect.mp4 new file mode 100644 index 000000000..ba05f5d07 Binary files /dev/null and b/docs/img/layers/layers-multiselect.mp4 differ diff --git a/docs/img/layers/layers-multiselect.webp b/docs/img/layers/layers-multiselect.webp new file mode 100644 index 000000000..4dcfdf78d Binary files /dev/null and b/docs/img/layers/layers-multiselect.webp differ diff --git a/docs/img/layers/layers-panel.mp4 b/docs/img/layers/layers-panel.mp4 new file mode 100644 index 000000000..7905ccc37 Binary files /dev/null and b/docs/img/layers/layers-panel.mp4 differ diff --git a/docs/img/layers/layers-panel.webp b/docs/img/layers/layers-panel.webp new file mode 100644 index 000000000..dc7609bdf Binary files /dev/null and b/docs/img/layers/layers-panel.webp differ diff --git a/docs/img/layers/layers-resize.mp4 b/docs/img/layers/layers-resize.mp4 new file mode 100644 index 000000000..b6757f2fc Binary files /dev/null and b/docs/img/layers/layers-resize.mp4 differ diff --git a/docs/img/layers/layers-resize.webp b/docs/img/layers/layers-resize.webp new file mode 100644 index 000000000..0f95cd8f6 Binary files /dev/null and b/docs/img/layers/layers-resize.webp differ diff --git a/docs/img/layers/layers-rotate.mp4 b/docs/img/layers/layers-rotate.mp4 new file mode 100644 index 000000000..4e1651aac Binary files /dev/null and b/docs/img/layers/layers-rotate.mp4 differ diff --git a/docs/img/layers/layers-rotate.webp b/docs/img/layers/layers-rotate.webp new file mode 100644 index 000000000..dc83f82c1 Binary files /dev/null and b/docs/img/layers/layers-rotate.webp differ diff --git a/docs/img/layers/layers-rtl.webp b/docs/img/layers/layers-rtl.webp new file mode 100644 index 000000000..f99ddaebd Binary files /dev/null and b/docs/img/layers/layers-rtl.webp differ diff --git a/docs/img/layers/layers-scale.mp4 b/docs/img/layers/layers-scale.mp4 new file mode 100644 index 000000000..699776682 Binary files /dev/null and b/docs/img/layers/layers-scale.mp4 differ diff --git a/docs/img/layers/layers-scale.webp b/docs/img/layers/layers-scale.webp new file mode 100644 index 000000000..20d03efa9 Binary files /dev/null and b/docs/img/layers/layers-scale.webp differ diff --git a/docs/img/layers/layers-search.mp4 b/docs/img/layers/layers-search.mp4 new file mode 100644 index 000000000..0c5b2f838 Binary files /dev/null and b/docs/img/layers/layers-search.mp4 differ diff --git a/docs/img/layers/layers-search.webp b/docs/img/layers/layers-search.webp new file mode 100644 index 000000000..98c52c7c5 Binary files /dev/null and b/docs/img/layers/layers-search.webp differ diff --git a/docs/img/layers/layers-select.mp4 b/docs/img/layers/layers-select.mp4 new file mode 100644 index 000000000..9a6d23941 Binary files /dev/null and b/docs/img/layers/layers-select.mp4 differ diff --git a/docs/img/layers/layers-select.webp b/docs/img/layers/layers-select.webp new file mode 100644 index 000000000..7966b7888 Binary files /dev/null and b/docs/img/layers/layers-select.webp differ diff --git a/docs/img/layers/pages-create.mp4 b/docs/img/layers/pages-create.mp4 new file mode 100644 index 000000000..a5d1ffb2b Binary files /dev/null and b/docs/img/layers/pages-create.mp4 differ diff --git a/docs/img/layers/pages-create.webp b/docs/img/layers/pages-create.webp new file mode 100644 index 000000000..1568ca45f Binary files /dev/null and b/docs/img/layers/pages-create.webp differ diff --git a/docs/img/libraries-asset-types.png b/docs/img/libraries-asset-types.png new file mode 100644 index 000000000..c250da942 Binary files /dev/null and b/docs/img/libraries-asset-types.png differ diff --git a/docs/img/libraries-launch.gif b/docs/img/libraries-launch.gif new file mode 100644 index 000000000..9c57c4451 Binary files /dev/null and b/docs/img/libraries-launch.gif differ diff --git a/docs/img/libraries/add-color.mp4 b/docs/img/libraries/add-color.mp4 new file mode 100644 index 000000000..f5b063698 Binary files /dev/null and b/docs/img/libraries/add-color.mp4 differ diff --git a/docs/img/libraries/add-color.webp b/docs/img/libraries/add-color.webp new file mode 100644 index 000000000..21f99547e Binary files /dev/null and b/docs/img/libraries/add-color.webp differ diff --git a/docs/img/libraries/add-component.mp4 b/docs/img/libraries/add-component.mp4 new file mode 100644 index 000000000..932f1a0f9 Binary files /dev/null and b/docs/img/libraries/add-component.mp4 differ diff --git a/docs/img/libraries/add-component.webp b/docs/img/libraries/add-component.webp new file mode 100644 index 000000000..d36432d6f Binary files /dev/null and b/docs/img/libraries/add-component.webp differ diff --git a/docs/img/libraries/add-typography.mp4 b/docs/img/libraries/add-typography.mp4 new file mode 100644 index 000000000..a389d2f19 Binary files /dev/null and b/docs/img/libraries/add-typography.mp4 differ diff --git a/docs/img/libraries/add-typography.webp b/docs/img/libraries/add-typography.webp new file mode 100644 index 000000000..92d3908a2 Binary files /dev/null and b/docs/img/libraries/add-typography.webp differ diff --git a/docs/img/libraries/asset-list.mp4 b/docs/img/libraries/asset-list.mp4 new file mode 100644 index 000000000..4c421fca5 Binary files /dev/null and b/docs/img/libraries/asset-list.mp4 differ diff --git a/docs/img/libraries/asset-list.webp b/docs/img/libraries/asset-list.webp new file mode 100644 index 000000000..e7306d121 Binary files /dev/null and b/docs/img/libraries/asset-list.webp differ diff --git a/docs/img/libraries/asset-multiselect.mp4 b/docs/img/libraries/asset-multiselect.mp4 new file mode 100644 index 000000000..c1e3fa1b8 Binary files /dev/null and b/docs/img/libraries/asset-multiselect.mp4 differ diff --git a/docs/img/libraries/asset-multiselect.webp b/docs/img/libraries/asset-multiselect.webp new file mode 100644 index 000000000..3938e117f Binary files /dev/null and b/docs/img/libraries/asset-multiselect.webp differ diff --git a/docs/img/libraries/asset-options.mp4 b/docs/img/libraries/asset-options.mp4 new file mode 100644 index 000000000..14c92ee7f Binary files /dev/null and b/docs/img/libraries/asset-options.mp4 differ diff --git a/docs/img/libraries/asset-options.webp b/docs/img/libraries/asset-options.webp new file mode 100644 index 000000000..e55e1fc55 Binary files /dev/null and b/docs/img/libraries/asset-options.webp differ diff --git a/docs/img/libraries/asset-order.mp4 b/docs/img/libraries/asset-order.mp4 new file mode 100644 index 000000000..b0c34f95a Binary files /dev/null and b/docs/img/libraries/asset-order.mp4 differ diff --git a/docs/img/libraries/asset-order.webp b/docs/img/libraries/asset-order.webp new file mode 100644 index 000000000..77a40e241 Binary files /dev/null and b/docs/img/libraries/asset-order.webp differ diff --git a/docs/img/libraries/asset-search.mp4 b/docs/img/libraries/asset-search.mp4 new file mode 100644 index 000000000..90a670e4b Binary files /dev/null and b/docs/img/libraries/asset-search.mp4 differ diff --git a/docs/img/libraries/asset-search.webp b/docs/img/libraries/asset-search.webp new file mode 100644 index 000000000..9fa0029f3 Binary files /dev/null and b/docs/img/libraries/asset-search.webp differ diff --git a/docs/img/libraries/asset-use.mp4 b/docs/img/libraries/asset-use.mp4 new file mode 100644 index 000000000..b61f4d02f Binary files /dev/null and b/docs/img/libraries/asset-use.mp4 differ diff --git a/docs/img/libraries/asset-use.webp b/docs/img/libraries/asset-use.webp new file mode 100644 index 000000000..5908ef736 Binary files /dev/null and b/docs/img/libraries/asset-use.webp differ diff --git a/docs/img/libraries/assets-tab.webp b/docs/img/libraries/assets-tab.webp new file mode 100644 index 000000000..5c412d733 Binary files /dev/null and b/docs/img/libraries/assets-tab.webp differ diff --git a/docs/img/libraries/libraries-disconnect.webp b/docs/img/libraries/libraries-disconnect.webp new file mode 100644 index 000000000..2438b2b88 Binary files /dev/null and b/docs/img/libraries/libraries-disconnect.webp differ diff --git a/docs/img/libraries/libraries-launch.mp4 b/docs/img/libraries/libraries-launch.mp4 new file mode 100644 index 000000000..10dbc5b3a Binary files /dev/null and b/docs/img/libraries/libraries-launch.mp4 differ diff --git a/docs/img/libraries/libraries-launch.webp b/docs/img/libraries/libraries-launch.webp new file mode 100644 index 000000000..10c15fcdc Binary files /dev/null and b/docs/img/libraries/libraries-launch.webp differ diff --git a/docs/img/libraries/libraries-open.webp b/docs/img/libraries/libraries-open.webp new file mode 100644 index 000000000..7578eec33 Binary files /dev/null and b/docs/img/libraries/libraries-open.webp differ diff --git a/docs/img/libraries/libraries-publish-menu.webp b/docs/img/libraries/libraries-publish-menu.webp new file mode 100644 index 000000000..0472eafa1 Binary files /dev/null and b/docs/img/libraries/libraries-publish-menu.webp differ diff --git a/docs/img/libraries/libraries-publish-panel.webp b/docs/img/libraries/libraries-publish-panel.webp new file mode 100644 index 000000000..1dbad6674 Binary files /dev/null and b/docs/img/libraries/libraries-publish-panel.webp differ diff --git a/docs/img/libraries/libraries-sidebar.webp b/docs/img/libraries/libraries-sidebar.webp new file mode 100644 index 000000000..e4458888e Binary files /dev/null and b/docs/img/libraries/libraries-sidebar.webp differ diff --git a/docs/img/libraries/libraries-updates.webp b/docs/img/libraries/libraries-updates.webp new file mode 100644 index 000000000..00dcd098d Binary files /dev/null and b/docs/img/libraries/libraries-updates.webp differ diff --git a/docs/img/library scroll.gif b/docs/img/library scroll.gif new file mode 100644 index 000000000..ab7a4601f Binary files /dev/null and b/docs/img/library scroll.gif differ diff --git a/docs/img/library-drag-asset.gif b/docs/img/library-drag-asset.gif new file mode 100644 index 000000000..c19c502f5 Binary files /dev/null and b/docs/img/library-drag-asset.gif differ diff --git a/docs/img/library-groups.png b/docs/img/library-groups.png new file mode 100644 index 000000000..5fda3de85 Binary files /dev/null and b/docs/img/library-groups.png differ diff --git a/docs/img/linkedin.svg b/docs/img/linkedin.svg new file mode 100644 index 000000000..2c1829a4c --- /dev/null +++ b/docs/img/linkedin.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/img/locate_by_label.webp b/docs/img/locate_by_label.webp new file mode 100644 index 000000000..7376290ef Binary files /dev/null and b/docs/img/locate_by_label.webp differ diff --git a/docs/img/locate_by_label2.webp b/docs/img/locate_by_label2.webp new file mode 100644 index 000000000..71d0f78f0 Binary files /dev/null and b/docs/img/locate_by_label2.webp differ diff --git a/docs/img/locate_by_text.webp b/docs/img/locate_by_text.webp new file mode 100644 index 000000000..82321fc58 Binary files /dev/null and b/docs/img/locate_by_text.webp differ diff --git a/docs/img/login-btn.webp b/docs/img/login-btn.webp new file mode 100644 index 000000000..32d862a6e Binary files /dev/null and b/docs/img/login-btn.webp differ diff --git a/docs/img/login-locators.webp b/docs/img/login-locators.webp new file mode 100644 index 000000000..d9f89129e Binary files /dev/null and b/docs/img/login-locators.webp differ diff --git a/docs/img/mastodon.svg b/docs/img/mastodon.svg new file mode 100644 index 000000000..0e33315e5 --- /dev/null +++ b/docs/img/mastodon.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/img/new-icon.svg b/docs/img/new-icon.svg new file mode 100644 index 000000000..3e7c41e7f --- /dev/null +++ b/docs/img/new-icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/docs/img/nudge-amount.png b/docs/img/nudge-amount.png new file mode 100644 index 000000000..c7b386125 Binary files /dev/null and b/docs/img/nudge-amount.png differ diff --git a/docs/img/objects/board-clip.mp4 b/docs/img/objects/board-clip.mp4 new file mode 100644 index 000000000..33134523b Binary files /dev/null and b/docs/img/objects/board-clip.mp4 differ diff --git a/docs/img/objects/board-clip.webp b/docs/img/objects/board-clip.webp new file mode 100644 index 000000000..86a03a570 Binary files /dev/null and b/docs/img/objects/board-clip.webp differ diff --git a/docs/img/objects/board-create.mp4 b/docs/img/objects/board-create.mp4 new file mode 100644 index 000000000..7e27cac68 Binary files /dev/null and b/docs/img/objects/board-create.mp4 differ diff --git a/docs/img/objects/board-create.webp b/docs/img/objects/board-create.webp new file mode 100644 index 000000000..fb57f671a Binary files /dev/null and b/docs/img/objects/board-create.webp differ diff --git a/docs/img/objects/board-fill.webp b/docs/img/objects/board-fill.webp new file mode 100644 index 000000000..a6c6d78b6 Binary files /dev/null and b/docs/img/objects/board-fill.webp differ diff --git a/docs/img/objects/board-guides.webp b/docs/img/objects/board-guides.webp new file mode 100644 index 000000000..909b99738 Binary files /dev/null and b/docs/img/objects/board-guides.webp differ diff --git a/docs/img/objects/board-prototyping.webp b/docs/img/objects/board-prototyping.webp new file mode 100644 index 000000000..4dc29841a Binary files /dev/null and b/docs/img/objects/board-prototyping.webp differ diff --git a/docs/img/objects/board-select.mp4 b/docs/img/objects/board-select.mp4 new file mode 100644 index 000000000..fc7168785 Binary files /dev/null and b/docs/img/objects/board-select.mp4 differ diff --git a/docs/img/objects/board-select.webp b/docs/img/objects/board-select.webp new file mode 100644 index 000000000..6e0a6c687 Binary files /dev/null and b/docs/img/objects/board-select.webp differ diff --git a/docs/img/objects/board-show.webp b/docs/img/objects/board-show.webp new file mode 100644 index 000000000..8f2685ebd Binary files /dev/null and b/docs/img/objects/board-show.webp differ diff --git a/docs/img/objects/board-thumbnail.mp4 b/docs/img/objects/board-thumbnail.mp4 new file mode 100644 index 000000000..bf07a9228 Binary files /dev/null and b/docs/img/objects/board-thumbnail.mp4 differ diff --git a/docs/img/objects/board-thumbnail.webp b/docs/img/objects/board-thumbnail.webp new file mode 100644 index 000000000..92b60d5f3 Binary files /dev/null and b/docs/img/objects/board-thumbnail.webp differ diff --git a/docs/img/objects/image-ratio.mp4 b/docs/img/objects/image-ratio.mp4 new file mode 100644 index 000000000..d80e9ecf7 Binary files /dev/null and b/docs/img/objects/image-ratio.mp4 differ diff --git a/docs/img/objects/image-ratio.webp b/docs/img/objects/image-ratio.webp new file mode 100644 index 000000000..c1c68b6d5 Binary files /dev/null and b/docs/img/objects/image-ratio.webp differ diff --git a/docs/img/objects/node-types.mp4 b/docs/img/objects/node-types.mp4 new file mode 100644 index 000000000..d2ea1590d Binary files /dev/null and b/docs/img/objects/node-types.mp4 differ diff --git a/docs/img/objects/node-types.webp b/docs/img/objects/node-types.webp new file mode 100644 index 000000000..e8867296e Binary files /dev/null and b/docs/img/objects/node-types.webp differ diff --git a/docs/img/objects/nodes-edit.mp4 b/docs/img/objects/nodes-edit.mp4 new file mode 100644 index 000000000..2f32e0412 Binary files /dev/null and b/docs/img/objects/nodes-edit.mp4 differ diff --git a/docs/img/objects/nodes-edit.webp b/docs/img/objects/nodes-edit.webp new file mode 100644 index 000000000..9b748af23 Binary files /dev/null and b/docs/img/objects/nodes-edit.webp differ diff --git a/docs/img/objects/path-create.mp4 b/docs/img/objects/path-create.mp4 new file mode 100644 index 000000000..6a7772500 Binary files /dev/null and b/docs/img/objects/path-create.mp4 differ diff --git a/docs/img/objects/path-create.webp b/docs/img/objects/path-create.webp new file mode 100644 index 000000000..d8ac65d7b Binary files /dev/null and b/docs/img/objects/path-create.webp differ diff --git a/docs/img/objects/rectangles-ellipses.mp4 b/docs/img/objects/rectangles-ellipses.mp4 new file mode 100644 index 000000000..3ced3b26e Binary files /dev/null and b/docs/img/objects/rectangles-ellipses.mp4 differ diff --git a/docs/img/objects/rectangles-ellipses.webp b/docs/img/objects/rectangles-ellipses.webp new file mode 100644 index 000000000..67d2d3496 Binary files /dev/null and b/docs/img/objects/rectangles-ellipses.webp differ diff --git a/docs/img/objects/text-create.mp4 b/docs/img/objects/text-create.mp4 new file mode 100644 index 000000000..c798e755c Binary files /dev/null and b/docs/img/objects/text-create.mp4 differ diff --git a/docs/img/objects/text-create.webp b/docs/img/objects/text-create.webp new file mode 100644 index 000000000..466f1c507 Binary files /dev/null and b/docs/img/objects/text-create.webp differ diff --git a/docs/img/objects/text-edit.webp b/docs/img/objects/text-edit.webp new file mode 100644 index 000000000..75e4f5ff1 Binary files /dev/null and b/docs/img/objects/text-edit.webp differ diff --git a/docs/img/objects/text-options.webp b/docs/img/objects/text-options.webp new file mode 100644 index 000000000..7f72309ee Binary files /dev/null and b/docs/img/objects/text-options.webp differ diff --git a/docs/img/overlay-toggle.gif b/docs/img/overlay-toggle.gif new file mode 100644 index 000000000..42ae38f69 Binary files /dev/null and b/docs/img/overlay-toggle.gif differ diff --git a/docs/img/page-item-locator1.webp b/docs/img/page-item-locator1.webp new file mode 100644 index 000000000..ec70757d9 Binary files /dev/null and b/docs/img/page-item-locator1.webp differ diff --git a/docs/img/page-item-locator2.webp b/docs/img/page-item-locator2.webp new file mode 100644 index 000000000..729237f32 Binary files /dev/null and b/docs/img/page-item-locator2.webp differ diff --git a/docs/img/pages.gif b/docs/img/pages.gif new file mode 100644 index 000000000..b3119c5b8 Binary files /dev/null and b/docs/img/pages.gif differ diff --git a/docs/img/paths-create.gif b/docs/img/paths-create.gif new file mode 100644 index 000000000..e748d44f5 Binary files /dev/null and b/docs/img/paths-create.gif differ diff --git a/docs/img/paths-edit.gif b/docs/img/paths-edit.gif new file mode 100644 index 000000000..8d7255cfc Binary files /dev/null and b/docs/img/paths-edit.gif differ diff --git a/docs/img/paths-nodes.gif b/docs/img/paths-nodes.gif new file mode 100644 index 000000000..1877540b3 Binary files /dev/null and b/docs/img/paths-nodes.gif differ diff --git a/docs/img/plugins/00_PenpotHub_plugins.png b/docs/img/plugins/00_PenpotHub_plugins.png new file mode 100644 index 000000000..f875ce678 Binary files /dev/null and b/docs/img/plugins/00_PenpotHub_plugins.png differ diff --git a/docs/img/plugins/01_OAuth_permissions.png b/docs/img/plugins/01_OAuth_permissions.png new file mode 100644 index 000000000..619c361e8 Binary files /dev/null and b/docs/img/plugins/01_OAuth_permissions.png differ diff --git a/docs/img/plugins/02_install_url.png b/docs/img/plugins/02_install_url.png new file mode 100644 index 000000000..498bbc425 Binary files /dev/null and b/docs/img/plugins/02_install_url.png differ diff --git a/docs/img/plugins/03_toolbar_plugins.png b/docs/img/plugins/03_toolbar_plugins.png new file mode 100644 index 000000000..3cd5a8d51 Binary files /dev/null and b/docs/img/plugins/03_toolbar_plugins.png differ diff --git a/docs/img/plugins/04_menu_plugins.png b/docs/img/plugins/04_menu_plugins.png new file mode 100644 index 000000000..d546948d2 Binary files /dev/null and b/docs/img/plugins/04_menu_plugins.png differ diff --git a/docs/img/plugins/05_plugin_manager.png b/docs/img/plugins/05_plugin_manager.png new file mode 100644 index 000000000..f9197e715 Binary files /dev/null and b/docs/img/plugins/05_plugin_manager.png differ diff --git a/docs/img/plugins/06_manager_plugins_installed.png b/docs/img/plugins/06_manager_plugins_installed.png new file mode 100644 index 000000000..4d777c9aa Binary files /dev/null and b/docs/img/plugins/06_manager_plugins_installed.png differ diff --git a/docs/img/plugins/angular_dist.png b/docs/img/plugins/angular_dist.png new file mode 100644 index 000000000..35c8a760e Binary files /dev/null and b/docs/img/plugins/angular_dist.png differ diff --git a/docs/img/plugins/api.png b/docs/img/plugins/api.png new file mode 100644 index 000000000..75a836b54 Binary files /dev/null and b/docs/img/plugins/api.png differ diff --git a/docs/img/plugins/build_settings.png b/docs/img/plugins/build_settings.png new file mode 100644 index 000000000..54ca7c29e Binary files /dev/null and b/docs/img/plugins/build_settings.png differ diff --git a/docs/img/plugins/cf_build_settings.png b/docs/img/plugins/cf_build_settings.png new file mode 100644 index 000000000..6b3cc2ff9 Binary files /dev/null and b/docs/img/plugins/cf_build_settings.png differ diff --git a/docs/img/plugins/cf_new_page.png b/docs/img/plugins/cf_new_page.png new file mode 100644 index 000000000..96766af30 Binary files /dev/null and b/docs/img/plugins/cf_new_page.png differ diff --git a/docs/img/plugins/cf_upload_files.png b/docs/img/plugins/cf_upload_files.png new file mode 100644 index 000000000..a1bd21a84 Binary files /dev/null and b/docs/img/plugins/cf_upload_files.png differ diff --git a/docs/img/plugins/create_plugin.png b/docs/img/plugins/create_plugin.png new file mode 100644 index 000000000..260c9fa16 Binary files /dev/null and b/docs/img/plugins/create_plugin.png differ diff --git a/docs/img/plugins/deploy-cloudflare-dragdrop.mp4 b/docs/img/plugins/deploy-cloudflare-dragdrop.mp4 new file mode 100644 index 000000000..b10e2f8b1 Binary files /dev/null and b/docs/img/plugins/deploy-cloudflare-dragdrop.mp4 differ diff --git a/docs/img/plugins/deploy-cloudflare-dragdrop.png b/docs/img/plugins/deploy-cloudflare-dragdrop.png new file mode 100644 index 000000000..789432dc7 Binary files /dev/null and b/docs/img/plugins/deploy-cloudflare-dragdrop.png differ diff --git a/docs/img/plugins/deploy-cloudflare-repo.mp4 b/docs/img/plugins/deploy-cloudflare-repo.mp4 new file mode 100644 index 000000000..def532032 Binary files /dev/null and b/docs/img/plugins/deploy-cloudflare-repo.mp4 differ diff --git a/docs/img/plugins/deploy-cloudflare-repo.png b/docs/img/plugins/deploy-cloudflare-repo.png new file mode 100644 index 000000000..c5caac1b9 Binary files /dev/null and b/docs/img/plugins/deploy-cloudflare-repo.png differ diff --git a/docs/img/plugins/deploy-netlify-dragdrop.mp4 b/docs/img/plugins/deploy-netlify-dragdrop.mp4 new file mode 100644 index 000000000..4171600a3 Binary files /dev/null and b/docs/img/plugins/deploy-netlify-dragdrop.mp4 differ diff --git a/docs/img/plugins/deploy-netlify-dragdrop.png b/docs/img/plugins/deploy-netlify-dragdrop.png new file mode 100644 index 000000000..1f49ab28b Binary files /dev/null and b/docs/img/plugins/deploy-netlify-dragdrop.png differ diff --git a/docs/img/plugins/deploy-netlify-repo.mp4 b/docs/img/plugins/deploy-netlify-repo.mp4 new file mode 100644 index 000000000..40d4c29f2 Binary files /dev/null and b/docs/img/plugins/deploy-netlify-repo.mp4 differ diff --git a/docs/img/plugins/deploy-netlify-repo.png b/docs/img/plugins/deploy-netlify-repo.png new file mode 100644 index 000000000..789432dc7 Binary files /dev/null and b/docs/img/plugins/deploy-netlify-repo.png differ diff --git a/docs/img/plugins/deployment.png b/docs/img/plugins/deployment.png new file mode 100644 index 000000000..9ac2f51f7 Binary files /dev/null and b/docs/img/plugins/deployment.png differ diff --git a/docs/img/plugins/examples.png b/docs/img/plugins/examples.png new file mode 100644 index 000000000..7fc502c02 Binary files /dev/null and b/docs/img/plugins/examples.png differ diff --git a/docs/img/plugins/faqs.png b/docs/img/plugins/faqs.png new file mode 100644 index 000000000..0c357c3bb Binary files /dev/null and b/docs/img/plugins/faqs.png differ diff --git a/docs/img/plugins/getting_started.png b/docs/img/plugins/getting_started.png new file mode 100644 index 000000000..a93103ffd Binary files /dev/null and b/docs/img/plugins/getting_started.png differ diff --git a/docs/img/plugins/install_cloudflare.png b/docs/img/plugins/install_cloudflare.png new file mode 100644 index 000000000..91891136a Binary files /dev/null and b/docs/img/plugins/install_cloudflare.png differ diff --git a/docs/img/plugins/install_netlify.png b/docs/img/plugins/install_netlify.png new file mode 100644 index 000000000..40c59cb09 Binary files /dev/null and b/docs/img/plugins/install_netlify.png differ diff --git a/docs/img/plugins/plugin-manager.png b/docs/img/plugins/plugin-manager.png new file mode 100644 index 000000000..94efff7ef Binary files /dev/null and b/docs/img/plugins/plugin-manager.png differ diff --git a/docs/img/plugins/plugin-menu.png b/docs/img/plugins/plugin-menu.png new file mode 100644 index 000000000..61f3b25b6 Binary files /dev/null and b/docs/img/plugins/plugin-menu.png differ diff --git a/docs/img/plugins/plugin-toolbar.png b/docs/img/plugins/plugin-toolbar.png new file mode 100644 index 000000000..003fabbac Binary files /dev/null and b/docs/img/plugins/plugin-toolbar.png differ diff --git a/docs/img/plugins/plugins-menu.mp4 b/docs/img/plugins/plugins-menu.mp4 new file mode 100644 index 000000000..e0fb36067 Binary files /dev/null and b/docs/img/plugins/plugins-menu.mp4 differ diff --git a/docs/img/plugins/plugins-menu.png b/docs/img/plugins/plugins-menu.png new file mode 100644 index 000000000..1230554d3 Binary files /dev/null and b/docs/img/plugins/plugins-menu.png differ diff --git a/docs/img/plugins/plugins-toolbar.mp4 b/docs/img/plugins/plugins-toolbar.mp4 new file mode 100644 index 000000000..0b99b9c08 Binary files /dev/null and b/docs/img/plugins/plugins-toolbar.mp4 differ diff --git a/docs/img/plugins/plugins-toolbar.png b/docs/img/plugins/plugins-toolbar.png new file mode 100644 index 000000000..c8212d32d Binary files /dev/null and b/docs/img/plugins/plugins-toolbar.png differ diff --git a/docs/img/plugins/plugint-types-example.gif b/docs/img/plugins/plugint-types-example.gif new file mode 100644 index 000000000..2ee30df9f Binary files /dev/null and b/docs/img/plugins/plugint-types-example.gif differ diff --git a/docs/img/plugins/vue_dist.png b/docs/img/plugins/vue_dist.png new file mode 100644 index 000000000..60d767465 Binary files /dev/null and b/docs/img/plugins/vue_dist.png differ diff --git a/docs/img/prototype/interface-dashboard.webp b/docs/img/prototype/interface-dashboard.webp new file mode 100644 index 000000000..4819cc195 Binary files /dev/null and b/docs/img/prototype/interface-dashboard.webp differ diff --git a/docs/img/prototype/prototype-action.webp b/docs/img/prototype/prototype-action.webp new file mode 100644 index 000000000..5a6f4e3a2 Binary files /dev/null and b/docs/img/prototype/prototype-action.webp differ diff --git a/docs/img/prototype/prototype-anatomy.webp b/docs/img/prototype/prototype-anatomy.webp new file mode 100644 index 000000000..3f2b6a2c9 Binary files /dev/null and b/docs/img/prototype/prototype-anatomy.webp differ diff --git a/docs/img/prototype/prototype-animation.webp b/docs/img/prototype/prototype-animation.webp new file mode 100644 index 000000000..812b4278e Binary files /dev/null and b/docs/img/prototype/prototype-animation.webp differ diff --git a/docs/img/prototype/prototype-connect.mp4 b/docs/img/prototype/prototype-connect.mp4 new file mode 100644 index 000000000..d74ee21f7 Binary files /dev/null and b/docs/img/prototype/prototype-connect.mp4 differ diff --git a/docs/img/prototype/prototype-connect.webp b/docs/img/prototype/prototype-connect.webp new file mode 100644 index 000000000..3c2a6a554 Binary files /dev/null and b/docs/img/prototype/prototype-connect.webp differ diff --git a/docs/img/prototype/prototype-fix-scroll.webp b/docs/img/prototype/prototype-fix-scroll.webp new file mode 100644 index 000000000..b5b2742b6 Binary files /dev/null and b/docs/img/prototype/prototype-fix-scroll.webp differ diff --git a/docs/img/prototype/prototype-flow-add.mp4 b/docs/img/prototype/prototype-flow-add.mp4 new file mode 100644 index 000000000..4d8d42fc2 Binary files /dev/null and b/docs/img/prototype/prototype-flow-add.mp4 differ diff --git a/docs/img/prototype/prototype-flow-add.webp b/docs/img/prototype/prototype-flow-add.webp new file mode 100644 index 000000000..4f299bd3b Binary files /dev/null and b/docs/img/prototype/prototype-flow-add.webp differ diff --git a/docs/img/prototype/prototype-flow-menu.webp b/docs/img/prototype/prototype-flow-menu.webp new file mode 100644 index 000000000..d696e74bc Binary files /dev/null and b/docs/img/prototype/prototype-flow-menu.webp differ diff --git a/docs/img/prototype/prototype-flow-new.mp4 b/docs/img/prototype/prototype-flow-new.mp4 new file mode 100644 index 000000000..4db61054f Binary files /dev/null and b/docs/img/prototype/prototype-flow-new.mp4 differ diff --git a/docs/img/prototype/prototype-flow-new.webp b/docs/img/prototype/prototype-flow-new.webp new file mode 100644 index 000000000..12ed32822 Binary files /dev/null and b/docs/img/prototype/prototype-flow-new.webp differ diff --git a/docs/img/prototype/prototype-flows-multiple.webp b/docs/img/prototype/prototype-flows-multiple.webp new file mode 100644 index 000000000..5ab53c555 Binary files /dev/null and b/docs/img/prototype/prototype-flows-multiple.webp differ diff --git a/docs/img/prototype/prototype-flows-viewmode.webp b/docs/img/prototype/prototype-flows-viewmode.webp new file mode 100644 index 000000000..0c8234cfd Binary files /dev/null and b/docs/img/prototype/prototype-flows-viewmode.webp differ diff --git a/docs/img/prototype/prototype-overlay-relative.webp b/docs/img/prototype/prototype-overlay-relative.webp new file mode 100644 index 000000000..c27c5972d Binary files /dev/null and b/docs/img/prototype/prototype-overlay-relative.webp differ diff --git a/docs/img/prototype/prototype-overlay.mp4 b/docs/img/prototype/prototype-overlay.mp4 new file mode 100644 index 000000000..b5d14a121 Binary files /dev/null and b/docs/img/prototype/prototype-overlay.mp4 differ diff --git a/docs/img/prototype/prototype-overlay.webp b/docs/img/prototype/prototype-overlay.webp new file mode 100644 index 000000000..ee3e1d252 Binary files /dev/null and b/docs/img/prototype/prototype-overlay.webp differ diff --git a/docs/img/prototype/prototype-previous.webp b/docs/img/prototype/prototype-previous.webp new file mode 100644 index 000000000..4f5439e28 Binary files /dev/null and b/docs/img/prototype/prototype-previous.webp differ diff --git a/docs/img/prototype/prototype-trigger.webp b/docs/img/prototype/prototype-trigger.webp new file mode 100644 index 000000000..8a8390699 Binary files /dev/null and b/docs/img/prototype/prototype-trigger.webp differ diff --git a/docs/img/prototype/prototyping-animations-dissolve.gif b/docs/img/prototype/prototyping-animations-dissolve.gif new file mode 100644 index 000000000..dd0e7a672 Binary files /dev/null and b/docs/img/prototype/prototyping-animations-dissolve.gif differ diff --git a/docs/img/prototype/prototyping-animations-push.gif b/docs/img/prototype/prototyping-animations-push.gif new file mode 100644 index 000000000..e32e0b517 Binary files /dev/null and b/docs/img/prototype/prototyping-animations-push.gif differ diff --git a/docs/img/prototype/prototyping-animations-slide.gif b/docs/img/prototype/prototyping-animations-slide.gif new file mode 100644 index 000000000..17cf39fd4 Binary files /dev/null and b/docs/img/prototype/prototyping-animations-slide.gif differ diff --git a/docs/img/prototype/prototyping-scroll.gif b/docs/img/prototype/prototyping-scroll.gif new file mode 100644 index 000000000..1ae4be26e Binary files /dev/null and b/docs/img/prototype/prototyping-scroll.gif differ diff --git a/docs/img/prototyping-actions.png b/docs/img/prototyping-actions.png new file mode 100644 index 000000000..36ec1a441 Binary files /dev/null and b/docs/img/prototyping-actions.png differ diff --git a/docs/img/prototyping-anatomy.png b/docs/img/prototyping-anatomy.png new file mode 100644 index 000000000..72814bfa7 Binary files /dev/null and b/docs/img/prototyping-anatomy.png differ diff --git a/docs/img/prototyping-animations.png b/docs/img/prototyping-animations.png new file mode 100644 index 000000000..4d408bcd1 Binary files /dev/null and b/docs/img/prototyping-animations.png differ diff --git a/docs/img/prototyping-connection.gif b/docs/img/prototyping-connection.gif new file mode 100644 index 000000000..e74e1e542 Binary files /dev/null and b/docs/img/prototyping-connection.gif differ diff --git a/docs/img/prototyping-fix.png b/docs/img/prototyping-fix.png new file mode 100644 index 000000000..867df7bcc Binary files /dev/null and b/docs/img/prototyping-fix.png differ diff --git a/docs/img/prototyping-flows-connection.gif b/docs/img/prototyping-flows-connection.gif new file mode 100644 index 000000000..44808a94f Binary files /dev/null and b/docs/img/prototyping-flows-connection.gif differ diff --git a/docs/img/prototyping-flows-menu.png b/docs/img/prototyping-flows-menu.png new file mode 100644 index 000000000..c0c4f8a30 Binary files /dev/null and b/docs/img/prototyping-flows-menu.png differ diff --git a/docs/img/prototyping-flows-multiple.png b/docs/img/prototyping-flows-multiple.png new file mode 100644 index 000000000..91727c8d7 Binary files /dev/null and b/docs/img/prototyping-flows-multiple.png differ diff --git a/docs/img/prototyping-flows-start.gif b/docs/img/prototyping-flows-start.gif new file mode 100644 index 000000000..334bc7260 Binary files /dev/null and b/docs/img/prototyping-flows-start.gif differ diff --git a/docs/img/prototyping-flows-viewmode.png b/docs/img/prototyping-flows-viewmode.png new file mode 100644 index 000000000..76bf5d74a Binary files /dev/null and b/docs/img/prototyping-flows-viewmode.png differ diff --git a/docs/img/prototyping-overlay.gif b/docs/img/prototyping-overlay.gif new file mode 100644 index 000000000..2f2996575 Binary files /dev/null and b/docs/img/prototyping-overlay.gif differ diff --git a/docs/img/prototyping-previous.png b/docs/img/prototyping-previous.png new file mode 100644 index 000000000..9e7758561 Binary files /dev/null and b/docs/img/prototyping-previous.png differ diff --git a/docs/img/prototyping-triggers.png b/docs/img/prototyping-triggers.png new file mode 100644 index 000000000..8dcd1a611 Binary files /dev/null and b/docs/img/prototyping-triggers.png differ diff --git a/docs/img/rect-ellipses.gif b/docs/img/rect-ellipses.gif new file mode 100644 index 000000000..dab27e359 Binary files /dev/null and b/docs/img/rect-ellipses.gif differ diff --git a/docs/img/reddit.svg b/docs/img/reddit.svg new file mode 100644 index 000000000..d4871ccf8 --- /dev/null +++ b/docs/img/reddit.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/img/rtl-support.gif b/docs/img/rtl-support.gif new file mode 100644 index 000000000..ba1b5f54b Binary files /dev/null and b/docs/img/rtl-support.gif differ diff --git a/docs/img/rtl-support.png b/docs/img/rtl-support.png new file mode 100644 index 000000000..5b3dd7c15 Binary files /dev/null and b/docs/img/rtl-support.png differ diff --git a/docs/img/rulers.gif b/docs/img/rulers.gif new file mode 100644 index 000000000..8e6d9071c Binary files /dev/null and b/docs/img/rulers.gif differ diff --git a/docs/img/scale-groups-texts.gif b/docs/img/scale-groups-texts.gif new file mode 100644 index 000000000..5c5463bca Binary files /dev/null and b/docs/img/scale-groups-texts.gif differ diff --git a/docs/img/set-thumbnail.gif b/docs/img/set-thumbnail.gif new file mode 100644 index 000000000..ecd69ba24 Binary files /dev/null and b/docs/img/set-thumbnail.gif differ diff --git a/docs/img/share-prototype-flow.gif b/docs/img/share-prototype-flow.gif new file mode 100644 index 000000000..4c8c5226e Binary files /dev/null and b/docs/img/share-prototype-flow.gif differ diff --git a/docs/img/share-prototype-permissions.gif b/docs/img/share-prototype-permissions.gif new file mode 100644 index 000000000..b2f6d6ddb Binary files /dev/null and b/docs/img/share-prototype-permissions.gif differ diff --git a/docs/img/shared-libraries.gif b/docs/img/shared-libraries.gif new file mode 100644 index 000000000..be80f42a0 Binary files /dev/null and b/docs/img/shared-libraries.gif differ diff --git a/docs/img/shared-libraries.png b/docs/img/shared-libraries.png new file mode 100644 index 000000000..a8461feed Binary files /dev/null and b/docs/img/shared-libraries.png differ diff --git a/docs/img/shortcuts-panel.gif b/docs/img/shortcuts-panel.gif new file mode 100644 index 000000000..906538bce Binary files /dev/null and b/docs/img/shortcuts-panel.gif differ diff --git a/docs/img/sidebar-form.png b/docs/img/sidebar-form.png new file mode 100644 index 000000000..b1a091fc4 Binary files /dev/null and b/docs/img/sidebar-form.png differ diff --git a/docs/img/snap-grid.gif b/docs/img/snap-grid.gif new file mode 100644 index 000000000..4ea48291a Binary files /dev/null and b/docs/img/snap-grid.gif differ diff --git a/docs/img/snap-pixel.png b/docs/img/snap-pixel.png new file mode 100644 index 000000000..c0c51f622 Binary files /dev/null and b/docs/img/snap-pixel.png differ diff --git a/docs/img/styling-blur.gif b/docs/img/styling-blur.gif new file mode 100644 index 000000000..856b7799e Binary files /dev/null and b/docs/img/styling-blur.gif differ diff --git a/docs/img/styling-fill-apply.gif b/docs/img/styling-fill-apply.gif new file mode 100644 index 000000000..ea70a10fd Binary files /dev/null and b/docs/img/styling-fill-apply.gif differ diff --git a/docs/img/styling-fill-remove.gif b/docs/img/styling-fill-remove.gif new file mode 100644 index 000000000..5632e6f3d Binary files /dev/null and b/docs/img/styling-fill-remove.gif differ diff --git a/docs/img/styling-multiple-fills.png b/docs/img/styling-multiple-fills.png new file mode 100644 index 000000000..c203a2328 Binary files /dev/null and b/docs/img/styling-multiple-fills.png differ diff --git a/docs/img/styling-multiple-strokes.png b/docs/img/styling-multiple-strokes.png new file mode 100644 index 000000000..bd7c43a1b Binary files /dev/null and b/docs/img/styling-multiple-strokes.png differ diff --git a/docs/img/styling-radius.gif b/docs/img/styling-radius.gif new file mode 100644 index 000000000..eef68cee3 Binary files /dev/null and b/docs/img/styling-radius.gif differ diff --git a/docs/img/styling-shadow.gif b/docs/img/styling-shadow.gif new file mode 100644 index 000000000..d34f65d97 Binary files /dev/null and b/docs/img/styling-shadow.gif differ diff --git a/docs/img/styling-stroke-caps.gif b/docs/img/styling-stroke-caps.gif new file mode 100644 index 000000000..653fbf365 Binary files /dev/null and b/docs/img/styling-stroke-caps.gif differ diff --git a/docs/img/styling-stroke-caps.png b/docs/img/styling-stroke-caps.png new file mode 100644 index 000000000..fd6de3410 Binary files /dev/null and b/docs/img/styling-stroke-caps.png differ diff --git a/docs/img/styling-stroke.gif b/docs/img/styling-stroke.gif new file mode 100644 index 000000000..441e93f97 Binary files /dev/null and b/docs/img/styling-stroke.gif differ diff --git a/docs/img/styling/blur.mp4 b/docs/img/styling/blur.mp4 new file mode 100644 index 000000000..89371d976 Binary files /dev/null and b/docs/img/styling/blur.mp4 differ diff --git a/docs/img/styling/blur.webp b/docs/img/styling/blur.webp new file mode 100644 index 000000000..3e0ad2d15 Binary files /dev/null and b/docs/img/styling/blur.webp differ diff --git a/docs/img/styling/color-library.mp4 b/docs/img/styling/color-library.mp4 new file mode 100644 index 000000000..3ffdec0d0 Binary files /dev/null and b/docs/img/styling/color-library.mp4 differ diff --git a/docs/img/styling/color-library.webp b/docs/img/styling/color-library.webp new file mode 100644 index 000000000..a839c57ed Binary files /dev/null and b/docs/img/styling/color-library.webp differ diff --git a/docs/img/styling/color-opacity.mp4 b/docs/img/styling/color-opacity.mp4 new file mode 100644 index 000000000..e8c9fc110 Binary files /dev/null and b/docs/img/styling/color-opacity.mp4 differ diff --git a/docs/img/styling/color-opacity.webp b/docs/img/styling/color-opacity.webp new file mode 100644 index 000000000..5457cc9ba Binary files /dev/null and b/docs/img/styling/color-opacity.webp differ diff --git a/docs/img/styling/color-palette.mp4 b/docs/img/styling/color-palette.mp4 new file mode 100644 index 000000000..81d1ecae4 Binary files /dev/null and b/docs/img/styling/color-palette.mp4 differ diff --git a/docs/img/styling/color-palette.webp b/docs/img/styling/color-palette.webp new file mode 100644 index 000000000..bf060b294 Binary files /dev/null and b/docs/img/styling/color-palette.webp differ diff --git a/docs/img/styling/color-picker.webp b/docs/img/styling/color-picker.webp new file mode 100644 index 000000000..6c7e9584e Binary files /dev/null and b/docs/img/styling/color-picker.webp differ diff --git a/docs/img/styling/color-selected.webp b/docs/img/styling/color-selected.webp new file mode 100644 index 000000000..ca97927d2 Binary files /dev/null and b/docs/img/styling/color-selected.webp differ diff --git a/docs/img/styling/corners.mp4 b/docs/img/styling/corners.mp4 new file mode 100644 index 000000000..a07c53321 Binary files /dev/null and b/docs/img/styling/corners.mp4 differ diff --git a/docs/img/styling/corners.webp b/docs/img/styling/corners.webp new file mode 100644 index 000000000..345017121 Binary files /dev/null and b/docs/img/styling/corners.webp differ diff --git a/docs/img/styling/fill-multiple.webp b/docs/img/styling/fill-multiple.webp new file mode 100644 index 000000000..573ed8d71 Binary files /dev/null and b/docs/img/styling/fill-multiple.webp differ diff --git a/docs/img/styling/fill-remove.webp b/docs/img/styling/fill-remove.webp new file mode 100644 index 000000000..da96e89e5 Binary files /dev/null and b/docs/img/styling/fill-remove.webp differ diff --git a/docs/img/styling/shadow.webp b/docs/img/styling/shadow.webp new file mode 100644 index 000000000..ba7a71ed5 Binary files /dev/null and b/docs/img/styling/shadow.webp differ diff --git a/docs/img/styling/stroke-cap.mp4 b/docs/img/styling/stroke-cap.mp4 new file mode 100644 index 000000000..468f90dd2 Binary files /dev/null and b/docs/img/styling/stroke-cap.mp4 differ diff --git a/docs/img/styling/stroke-cap.webp b/docs/img/styling/stroke-cap.webp new file mode 100644 index 000000000..3b76f2f27 Binary files /dev/null and b/docs/img/styling/stroke-cap.webp differ diff --git a/docs/img/styling/stroke-multiple.webp b/docs/img/styling/stroke-multiple.webp new file mode 100644 index 000000000..7621b8b08 Binary files /dev/null and b/docs/img/styling/stroke-multiple.webp differ diff --git a/docs/img/styling/stroke.mp4 b/docs/img/styling/stroke.mp4 new file mode 100644 index 000000000..07c8a9b82 Binary files /dev/null and b/docs/img/styling/stroke.mp4 differ diff --git a/docs/img/styling/stroke.webp b/docs/img/styling/stroke.webp new file mode 100644 index 000000000..fae047502 Binary files /dev/null and b/docs/img/styling/stroke.webp differ diff --git a/docs/img/teams-create.png b/docs/img/teams-create.png new file mode 100644 index 000000000..fccfd25b0 Binary files /dev/null and b/docs/img/teams-create.png differ diff --git a/docs/img/teams-delete.png b/docs/img/teams-delete.png new file mode 100644 index 000000000..6409979f3 Binary files /dev/null and b/docs/img/teams-delete.png differ diff --git a/docs/img/teams-invitations-actions.png b/docs/img/teams-invitations-actions.png new file mode 100644 index 000000000..7e6f8a4aa Binary files /dev/null and b/docs/img/teams-invitations-actions.png differ diff --git a/docs/img/teams-invitations.png b/docs/img/teams-invitations.png new file mode 100644 index 000000000..f76532ea6 Binary files /dev/null and b/docs/img/teams-invitations.png differ diff --git a/docs/img/teams-invite.png b/docs/img/teams-invite.png new file mode 100644 index 000000000..8b1bbaf4a Binary files /dev/null and b/docs/img/teams-invite.png differ diff --git a/docs/img/teams-members.png b/docs/img/teams-members.png new file mode 100644 index 000000000..e6efb3134 Binary files /dev/null and b/docs/img/teams-members.png differ diff --git a/docs/img/teams-permissions.png b/docs/img/teams-permissions.png new file mode 100644 index 000000000..a0a44abf8 Binary files /dev/null and b/docs/img/teams-permissions.png differ diff --git a/docs/img/teams-settings.png b/docs/img/teams-settings.png new file mode 100644 index 000000000..d802050f6 Binary files /dev/null and b/docs/img/teams-settings.png differ diff --git a/docs/img/teams-transfer.png b/docs/img/teams-transfer.png new file mode 100644 index 000000000..51d6e9f65 Binary files /dev/null and b/docs/img/teams-transfer.png differ diff --git a/docs/img/teams-webhooks.png b/docs/img/teams-webhooks.png new file mode 100644 index 000000000..44204089f Binary files /dev/null and b/docs/img/teams-webhooks.png differ diff --git a/docs/img/teams-yourpenpot.png b/docs/img/teams-yourpenpot.png new file mode 100644 index 000000000..228f5fe02 Binary files /dev/null and b/docs/img/teams-yourpenpot.png differ diff --git a/docs/img/teams/team-invitations-actions.webp b/docs/img/teams/team-invitations-actions.webp new file mode 100644 index 000000000..75063dc2a Binary files /dev/null and b/docs/img/teams/team-invitations-actions.webp differ diff --git a/docs/img/teams/team-invitations.webp b/docs/img/teams/team-invitations.webp new file mode 100644 index 000000000..3d08bf807 Binary files /dev/null and b/docs/img/teams/team-invitations.webp differ diff --git a/docs/img/teams/team-invite.webp b/docs/img/teams/team-invite.webp new file mode 100644 index 000000000..7319e3a2f Binary files /dev/null and b/docs/img/teams/team-invite.webp differ diff --git a/docs/img/teams/team-members.webp b/docs/img/teams/team-members.webp new file mode 100644 index 000000000..2c5c4836d Binary files /dev/null and b/docs/img/teams/team-members.webp differ diff --git a/docs/img/teams/team-selector-delete.webp b/docs/img/teams/team-selector-delete.webp new file mode 100644 index 000000000..4c3f181d7 Binary files /dev/null and b/docs/img/teams/team-selector-delete.webp differ diff --git a/docs/img/teams/team-selector-projects.webp b/docs/img/teams/team-selector-projects.webp new file mode 100644 index 000000000..e1696680e Binary files /dev/null and b/docs/img/teams/team-selector-projects.webp differ diff --git a/docs/img/teams/team-selector.webp b/docs/img/teams/team-selector.webp new file mode 100644 index 000000000..c29357d26 Binary files /dev/null and b/docs/img/teams/team-selector.webp differ diff --git a/docs/img/teams/team-transfer.webp b/docs/img/teams/team-transfer.webp new file mode 100644 index 000000000..6117d5c1a Binary files /dev/null and b/docs/img/teams/team-transfer.webp differ diff --git a/docs/img/teams/teams-permissions.webp b/docs/img/teams/teams-permissions.webp new file mode 100644 index 000000000..9157eb01d Binary files /dev/null and b/docs/img/teams/teams-permissions.webp differ diff --git a/docs/img/teams/webhook.webp b/docs/img/teams/webhook.webp new file mode 100644 index 000000000..b2858c635 Binary files /dev/null and b/docs/img/teams/webhook.webp differ diff --git a/docs/img/teams/webhooks.webp b/docs/img/teams/webhooks.webp new file mode 100644 index 000000000..e33544b22 Binary files /dev/null and b/docs/img/teams/webhooks.webp differ diff --git a/docs/img/tech-guide/access-tokens-create-1.webp b/docs/img/tech-guide/access-tokens-create-1.webp new file mode 100644 index 000000000..8aff3a665 Binary files /dev/null and b/docs/img/tech-guide/access-tokens-create-1.webp differ diff --git a/docs/img/tech-guide/access-tokens-create-2.webp b/docs/img/tech-guide/access-tokens-create-2.webp new file mode 100644 index 000000000..c7176a3d4 Binary files /dev/null and b/docs/img/tech-guide/access-tokens-create-2.webp differ diff --git a/docs/img/tech-guide/access-tokens-create-3.webp b/docs/img/tech-guide/access-tokens-create-3.webp new file mode 100644 index 000000000..2e84e1536 Binary files /dev/null and b/docs/img/tech-guide/access-tokens-create-3.webp differ diff --git a/docs/img/tech-guide/access-tokens.webp b/docs/img/tech-guide/access-tokens.webp new file mode 100644 index 000000000..6a7d616d8 Binary files /dev/null and b/docs/img/tech-guide/access-tokens.webp differ diff --git a/docs/img/tech-guide/add-webhook.webp b/docs/img/tech-guide/add-webhook.webp new file mode 100644 index 000000000..ee47a389e Binary files /dev/null and b/docs/img/tech-guide/add-webhook.webp differ diff --git a/docs/img/tech-guide/playwright-projects.webp b/docs/img/tech-guide/playwright-projects.webp new file mode 100644 index 000000000..c073bf115 Binary files /dev/null and b/docs/img/tech-guide/playwright-projects.webp differ diff --git a/docs/img/tech-guide/webhook-call.webp b/docs/img/tech-guide/webhook-call.webp new file mode 100644 index 000000000..26599f4c9 Binary files /dev/null and b/docs/img/tech-guide/webhook-call.webp differ diff --git a/docs/img/tech-guide/webhooks.webp b/docs/img/tech-guide/webhooks.webp new file mode 100644 index 000000000..b95469f1b Binary files /dev/null and b/docs/img/tech-guide/webhooks.webp differ diff --git a/docs/img/text-creation.gif b/docs/img/text-creation.gif new file mode 100644 index 000000000..fe79bf1d6 Binary files /dev/null and b/docs/img/text-creation.gif differ diff --git a/docs/img/text-edit.gif b/docs/img/text-edit.gif new file mode 100644 index 000000000..6fedd5e36 Binary files /dev/null and b/docs/img/text-edit.gif differ diff --git a/docs/img/text-options.png b/docs/img/text-options.png new file mode 100644 index 000000000..ee8b2b207 Binary files /dev/null and b/docs/img/text-options.png differ diff --git a/docs/img/th-help-center.jpg b/docs/img/th-help-center.jpg new file mode 100644 index 000000000..f3589d28e Binary files /dev/null and b/docs/img/th-help-center.jpg differ diff --git a/docs/img/traces1.png b/docs/img/traces1.png new file mode 100644 index 000000000..aa895196b Binary files /dev/null and b/docs/img/traces1.png differ diff --git a/docs/img/traces2.png b/docs/img/traces2.png new file mode 100644 index 000000000..a45110cbd Binary files /dev/null and b/docs/img/traces2.png differ diff --git a/docs/img/traces3.png b/docs/img/traces3.png new file mode 100644 index 000000000..5bb4803b3 Binary files /dev/null and b/docs/img/traces3.png differ diff --git a/docs/img/translations-lang-list.png b/docs/img/translations-lang-list.png new file mode 100644 index 000000000..07d9c34f4 Binary files /dev/null and b/docs/img/translations-lang-list.png differ diff --git a/docs/img/translations-lang-state.png b/docs/img/translations-lang-state.png new file mode 100644 index 000000000..7e7365ac9 Binary files /dev/null and b/docs/img/translations-lang-state.png differ diff --git a/docs/img/translations-start-translation.png b/docs/img/translations-start-translation.png new file mode 100644 index 000000000..27b5fa193 Binary files /dev/null and b/docs/img/translations-start-translation.png differ diff --git a/docs/img/translations-start.png b/docs/img/translations-start.png new file mode 100644 index 000000000..8359c0198 Binary files /dev/null and b/docs/img/translations-start.png differ diff --git a/docs/img/translations-strings-list.png b/docs/img/translations-strings-list.png new file mode 100644 index 000000000..0dee14ff7 Binary files /dev/null and b/docs/img/translations-strings-list.png differ diff --git a/docs/img/twitter-card-help.png b/docs/img/twitter-card-help.png new file mode 100644 index 000000000..34bcd1bfd Binary files /dev/null and b/docs/img/twitter-card-help.png differ diff --git a/docs/img/viewmode-artboards.png b/docs/img/viewmode-artboards.png new file mode 100644 index 000000000..ab4652ff3 Binary files /dev/null and b/docs/img/viewmode-artboards.png differ diff --git a/docs/img/viewmode-inspect.gif b/docs/img/viewmode-inspect.gif new file mode 100644 index 000000000..7f63b2435 Binary files /dev/null and b/docs/img/viewmode-inspect.gif differ diff --git a/docs/img/viewmode-pages.png b/docs/img/viewmode-pages.png new file mode 100644 index 000000000..c51dd0c30 Binary files /dev/null and b/docs/img/viewmode-pages.png differ diff --git a/docs/img/viewmode-play.png b/docs/img/viewmode-play.png new file mode 100644 index 000000000..cb79fc2d2 Binary files /dev/null and b/docs/img/viewmode-play.png differ diff --git a/docs/img/viewmode-share-link-options.gif b/docs/img/viewmode-share-link-options.gif new file mode 100644 index 000000000..43cc2b71b Binary files /dev/null and b/docs/img/viewmode-share-link-options.gif differ diff --git a/docs/img/viewmode-share.png b/docs/img/viewmode-share.png new file mode 100644 index 000000000..52cc55bc5 Binary files /dev/null and b/docs/img/viewmode-share.png differ diff --git a/docs/img/viewmode/viewmode-boards.webp b/docs/img/viewmode/viewmode-boards.webp new file mode 100644 index 000000000..85d851f1b Binary files /dev/null and b/docs/img/viewmode/viewmode-boards.webp differ diff --git a/docs/img/viewmode/viewmode-comment.webp b/docs/img/viewmode/viewmode-comment.webp new file mode 100644 index 000000000..85b4af207 Binary files /dev/null and b/docs/img/viewmode/viewmode-comment.webp differ diff --git a/docs/img/viewmode/viewmode-interactions.webp b/docs/img/viewmode/viewmode-interactions.webp new file mode 100644 index 000000000..b8b6eb066 Binary files /dev/null and b/docs/img/viewmode/viewmode-interactions.webp differ diff --git a/docs/img/viewmode/viewmode-pages.webp b/docs/img/viewmode/viewmode-pages.webp new file mode 100644 index 000000000..2b57ce350 Binary files /dev/null and b/docs/img/viewmode/viewmode-pages.webp differ diff --git a/docs/img/viewmode/viewmode-play.webp b/docs/img/viewmode/viewmode-play.webp new file mode 100644 index 000000000..49ae997ff Binary files /dev/null and b/docs/img/viewmode/viewmode-play.webp differ diff --git a/docs/img/viewmode/viewmode-share-copy.webp b/docs/img/viewmode/viewmode-share-copy.webp new file mode 100644 index 000000000..986c44289 Binary files /dev/null and b/docs/img/viewmode/viewmode-share-copy.webp differ diff --git a/docs/img/viewmode/viewmode-share-create.webp b/docs/img/viewmode/viewmode-share-create.webp new file mode 100644 index 000000000..242b12266 Binary files /dev/null and b/docs/img/viewmode/viewmode-share-create.webp differ diff --git a/docs/img/viewmode/viewmode-share-destroy.webp b/docs/img/viewmode/viewmode-share-destroy.webp new file mode 100644 index 000000000..03d5a25d1 Binary files /dev/null and b/docs/img/viewmode/viewmode-share-destroy.webp differ diff --git a/docs/img/viewmode/viewmode-share-options.webp b/docs/img/viewmode/viewmode-share-options.webp new file mode 100644 index 000000000..9b33d393f Binary files /dev/null and b/docs/img/viewmode/viewmode-share-options.webp differ diff --git a/docs/img/viewport-navigate.gif b/docs/img/viewport-navigate.gif new file mode 100644 index 000000000..3ac9838e6 Binary files /dev/null and b/docs/img/viewport-navigate.gif differ diff --git a/docs/img/viewport.png b/docs/img/viewport.png new file mode 100644 index 000000000..5f3bfdae5 Binary files /dev/null and b/docs/img/viewport.png differ diff --git a/docs/img/webhook-call.png b/docs/img/webhook-call.png new file mode 100644 index 000000000..373f66ba9 Binary files /dev/null and b/docs/img/webhook-call.png differ diff --git a/docs/img/webhooks.png b/docs/img/webhooks.png new file mode 100644 index 000000000..241d42826 Binary files /dev/null and b/docs/img/webhooks.png differ diff --git a/docs/img/workspace-basics/comments-create.mp4 b/docs/img/workspace-basics/comments-create.mp4 new file mode 100644 index 000000000..f599322f4 Binary files /dev/null and b/docs/img/workspace-basics/comments-create.mp4 differ diff --git a/docs/img/workspace-basics/comments-create.webp b/docs/img/workspace-basics/comments-create.webp new file mode 100644 index 000000000..1fe5a3a5d Binary files /dev/null and b/docs/img/workspace-basics/comments-create.webp differ diff --git a/docs/img/workspace-basics/comments-dashboard.webp b/docs/img/workspace-basics/comments-dashboard.webp new file mode 100644 index 000000000..5fa1f3f19 Binary files /dev/null and b/docs/img/workspace-basics/comments-dashboard.webp differ diff --git a/docs/img/workspace-basics/comments-mark.webp b/docs/img/workspace-basics/comments-mark.webp new file mode 100644 index 000000000..01a42c089 Binary files /dev/null and b/docs/img/workspace-basics/comments-mark.webp differ diff --git a/docs/img/workspace-basics/dynamic-alignment-measurement.mp4 b/docs/img/workspace-basics/dynamic-alignment-measurement.mp4 new file mode 100644 index 000000000..5a802881d Binary files /dev/null and b/docs/img/workspace-basics/dynamic-alignment-measurement.mp4 differ diff --git a/docs/img/workspace-basics/dynamic-alignment-measurement.webp b/docs/img/workspace-basics/dynamic-alignment-measurement.webp new file mode 100644 index 000000000..bbfcccfb2 Binary files /dev/null and b/docs/img/workspace-basics/dynamic-alignment-measurement.webp differ diff --git a/docs/img/workspace-basics/dynamic-alignment.mp4 b/docs/img/workspace-basics/dynamic-alignment.mp4 new file mode 100644 index 000000000..21f5d0bb4 Binary files /dev/null and b/docs/img/workspace-basics/dynamic-alignment.mp4 differ diff --git a/docs/img/workspace-basics/dynamic-alignment.webp b/docs/img/workspace-basics/dynamic-alignment.webp new file mode 100644 index 000000000..5c1c99254 Binary files /dev/null and b/docs/img/workspace-basics/dynamic-alignment.webp differ diff --git a/docs/img/workspace-basics/guides-column.mp4 b/docs/img/workspace-basics/guides-column.mp4 new file mode 100644 index 000000000..d03404d25 Binary files /dev/null and b/docs/img/workspace-basics/guides-column.mp4 differ diff --git a/docs/img/workspace-basics/guides-column.webp b/docs/img/workspace-basics/guides-column.webp new file mode 100644 index 000000000..6aa226ea0 Binary files /dev/null and b/docs/img/workspace-basics/guides-column.webp differ diff --git a/docs/img/workspace-basics/guides-create.mp4 b/docs/img/workspace-basics/guides-create.mp4 new file mode 100644 index 000000000..5b9dcda9f Binary files /dev/null and b/docs/img/workspace-basics/guides-create.mp4 differ diff --git a/docs/img/workspace-basics/guides-create.webp b/docs/img/workspace-basics/guides-create.webp new file mode 100644 index 000000000..6914b38c9 Binary files /dev/null and b/docs/img/workspace-basics/guides-create.webp differ diff --git a/docs/img/workspace-basics/guides-default.mp4 b/docs/img/workspace-basics/guides-default.mp4 new file mode 100644 index 000000000..207acadd1 Binary files /dev/null and b/docs/img/workspace-basics/guides-default.mp4 differ diff --git a/docs/img/workspace-basics/guides-default.webp b/docs/img/workspace-basics/guides-default.webp new file mode 100644 index 000000000..18c43ca6d Binary files /dev/null and b/docs/img/workspace-basics/guides-default.webp differ diff --git a/docs/img/workspace-basics/guides-delete-hide.mp4 b/docs/img/workspace-basics/guides-delete-hide.mp4 new file mode 100644 index 000000000..803708d09 Binary files /dev/null and b/docs/img/workspace-basics/guides-delete-hide.mp4 differ diff --git a/docs/img/workspace-basics/guides-delete-hide.webp b/docs/img/workspace-basics/guides-delete-hide.webp new file mode 100644 index 000000000..8b043c081 Binary files /dev/null and b/docs/img/workspace-basics/guides-delete-hide.webp differ diff --git a/docs/img/workspace-basics/guides-rows.mp4 b/docs/img/workspace-basics/guides-rows.mp4 new file mode 100644 index 000000000..73dd86b7d Binary files /dev/null and b/docs/img/workspace-basics/guides-rows.mp4 differ diff --git a/docs/img/workspace-basics/guides-rows.webp b/docs/img/workspace-basics/guides-rows.webp new file mode 100644 index 000000000..103f95ccf Binary files /dev/null and b/docs/img/workspace-basics/guides-rows.webp differ diff --git a/docs/img/workspace-basics/guides-snap.mp4 b/docs/img/workspace-basics/guides-snap.mp4 new file mode 100644 index 000000000..447d385ae Binary files /dev/null and b/docs/img/workspace-basics/guides-snap.mp4 differ diff --git a/docs/img/workspace-basics/guides-snap.webp b/docs/img/workspace-basics/guides-snap.webp new file mode 100644 index 000000000..821db4edb Binary files /dev/null and b/docs/img/workspace-basics/guides-snap.webp differ diff --git a/docs/img/workspace-basics/guides-square.mp4 b/docs/img/workspace-basics/guides-square.mp4 new file mode 100644 index 000000000..c4f074021 Binary files /dev/null and b/docs/img/workspace-basics/guides-square.mp4 differ diff --git a/docs/img/workspace-basics/guides-square.webp b/docs/img/workspace-basics/guides-square.webp new file mode 100644 index 000000000..0dc7b8cd1 Binary files /dev/null and b/docs/img/workspace-basics/guides-square.webp differ diff --git a/docs/img/workspace-basics/history-navigate.mp4 b/docs/img/workspace-basics/history-navigate.mp4 new file mode 100644 index 000000000..393f9a129 Binary files /dev/null and b/docs/img/workspace-basics/history-navigate.mp4 differ diff --git a/docs/img/workspace-basics/history-navigate.webp b/docs/img/workspace-basics/history-navigate.webp new file mode 100644 index 000000000..31faae2de Binary files /dev/null and b/docs/img/workspace-basics/history-navigate.webp differ diff --git a/docs/img/workspace-basics/history.webp b/docs/img/workspace-basics/history.webp new file mode 100644 index 000000000..b4498af99 Binary files /dev/null and b/docs/img/workspace-basics/history.webp differ diff --git a/docs/img/workspace-basics/main-menu.webp b/docs/img/workspace-basics/main-menu.webp new file mode 100644 index 000000000..8f91da0fa Binary files /dev/null and b/docs/img/workspace-basics/main-menu.webp differ diff --git a/docs/img/workspace-basics/nudge-menu.webp b/docs/img/workspace-basics/nudge-menu.webp new file mode 100644 index 000000000..cbae6c708 Binary files /dev/null and b/docs/img/workspace-basics/nudge-menu.webp differ diff --git a/docs/img/workspace-basics/nudge.webp b/docs/img/workspace-basics/nudge.webp new file mode 100644 index 000000000..025148282 Binary files /dev/null and b/docs/img/workspace-basics/nudge.webp differ diff --git a/docs/img/workspace-basics/ruler-guides.mp4 b/docs/img/workspace-basics/ruler-guides.mp4 new file mode 100644 index 000000000..3e9835426 Binary files /dev/null and b/docs/img/workspace-basics/ruler-guides.mp4 differ diff --git a/docs/img/workspace-basics/ruler-guides.webp b/docs/img/workspace-basics/ruler-guides.webp new file mode 100644 index 000000000..819078c3f Binary files /dev/null and b/docs/img/workspace-basics/ruler-guides.webp differ diff --git a/docs/img/workspace-basics/rulers.mp4 b/docs/img/workspace-basics/rulers.mp4 new file mode 100644 index 000000000..f9a51ffdb Binary files /dev/null and b/docs/img/workspace-basics/rulers.mp4 differ diff --git a/docs/img/workspace-basics/rulers.webp b/docs/img/workspace-basics/rulers.webp new file mode 100644 index 000000000..858d6200c Binary files /dev/null and b/docs/img/workspace-basics/rulers.webp differ diff --git a/docs/img/workspace-basics/shortcuts.webp b/docs/img/workspace-basics/shortcuts.webp new file mode 100644 index 000000000..5dcfc2279 Binary files /dev/null and b/docs/img/workspace-basics/shortcuts.webp differ diff --git a/docs/img/workspace-basics/viewport-navigate.mp4 b/docs/img/workspace-basics/viewport-navigate.mp4 new file mode 100644 index 000000000..7aafa68e7 Binary files /dev/null and b/docs/img/workspace-basics/viewport-navigate.mp4 differ diff --git a/docs/img/workspace-basics/viewport-navigate.webp b/docs/img/workspace-basics/viewport-navigate.webp new file mode 100644 index 000000000..8830e32f4 Binary files /dev/null and b/docs/img/workspace-basics/viewport-navigate.webp differ diff --git a/docs/img/workspace-basics/viewport.webp b/docs/img/workspace-basics/viewport.webp new file mode 100644 index 000000000..1a8997000 Binary files /dev/null and b/docs/img/workspace-basics/viewport.webp differ diff --git a/docs/img/workspace-basics/workspace-snap.webp b/docs/img/workspace-basics/workspace-snap.webp new file mode 100644 index 000000000..3ea9a225a Binary files /dev/null and b/docs/img/workspace-basics/workspace-snap.webp differ diff --git a/docs/img/workspace-basics/workspace-zoom-layer.mp4 b/docs/img/workspace-basics/workspace-zoom-layer.mp4 new file mode 100644 index 000000000..c8eee212d Binary files /dev/null and b/docs/img/workspace-basics/workspace-zoom-layer.mp4 differ diff --git a/docs/img/workspace-basics/workspace-zoom-layer.webp b/docs/img/workspace-basics/workspace-zoom-layer.webp new file mode 100644 index 000000000..fe5b5114b Binary files /dev/null and b/docs/img/workspace-basics/workspace-zoom-layer.webp differ diff --git a/docs/img/workspace-basics/workspace-zoom-lense.mp4 b/docs/img/workspace-basics/workspace-zoom-lense.mp4 new file mode 100644 index 000000000..6b2222b35 Binary files /dev/null and b/docs/img/workspace-basics/workspace-zoom-lense.mp4 differ diff --git a/docs/img/workspace-basics/workspace-zoom-lense.webp b/docs/img/workspace-basics/workspace-zoom-lense.webp new file mode 100644 index 000000000..7577bbdb6 Binary files /dev/null and b/docs/img/workspace-basics/workspace-zoom-lense.webp differ diff --git a/docs/img/workspace-basics/workspace-zoom.webp b/docs/img/workspace-basics/workspace-zoom.webp new file mode 100644 index 000000000..691b36728 Binary files /dev/null and b/docs/img/workspace-basics/workspace-zoom.webp differ diff --git a/docs/img/workspace-menu.gif b/docs/img/workspace-menu.gif new file mode 100644 index 000000000..0ae6d42f7 Binary files /dev/null and b/docs/img/workspace-menu.gif differ diff --git a/docs/img/x.svg b/docs/img/x.svg new file mode 100644 index 000000000..f2f74078d --- /dev/null +++ b/docs/img/x.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/img/youtube.svg b/docs/img/youtube.svg new file mode 100644 index 000000000..756621f75 --- /dev/null +++ b/docs/img/youtube.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/img/zoom-layer.gif b/docs/img/zoom-layer.gif new file mode 100644 index 000000000..2adb881e3 Binary files /dev/null and b/docs/img/zoom-layer.gif differ diff --git a/docs/img/zoom-lense.gif b/docs/img/zoom-lense.gif new file mode 100644 index 000000000..284232e0d Binary files /dev/null and b/docs/img/zoom-lense.gif differ diff --git a/docs/img/zoom-menu.png b/docs/img/zoom-menu.png new file mode 100644 index 000000000..e75ab6135 Binary files /dev/null and b/docs/img/zoom-menu.png differ diff --git a/docs/index.njk b/docs/index.njk new file mode 100644 index 000000000..f5b601f7f --- /dev/null +++ b/docs/index.njk @@ -0,0 +1,60 @@ +--- +title: Help center +layout: layouts/home.njk +twitter: "@penpotapp" +image: img/placeholder.png +eleventyNavigation: + key: Home + order: 1 +--- + +

Help center

+ + + +
+

Contact us

+

Write us at support@penpot.app or join our community.

+
diff --git a/docs/js/elasticlunr.min.js b/docs/js/elasticlunr.min.js new file mode 100644 index 000000000..94b20dd2e --- /dev/null +++ b/docs/js/elasticlunr.min.js @@ -0,0 +1,10 @@ +/** + * elasticlunr - http://weixsong.github.io + * Lightweight full-text search engine in Javascript for browser search and offline search. - 0.9.5 + * + * Copyright (C) 2017 Oliver Nightingale + * Copyright (C) 2017 Wei Song + * MIT Licensed + * @license + */ +!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o { + try { + const response = await fetch("https://api.github.com/repos/penpot/penpot"); + const data = await response.json(); + return data.stargazers_count; + } catch (error) { + console.error("Error fetching repository data:", error); + return null; + } + }; + + const updateStarsCount = async () => { + const starsCount = await getRepoStars(); + if (starsCount !== null) { + const starsEl = document.getElementById("repo-stars"); + if (starsEl) { + starsEl.textContent = `${starsCount}`; + } + } + }; + + window.addEventListener('load', () => { + updateStarsCount(); + }); + + })(window, document); \ No newline at end of file diff --git a/docs/js/search.js b/docs/js/search.js new file mode 100644 index 000000000..286e092ca --- /dev/null +++ b/docs/js/search.js @@ -0,0 +1,61 @@ +(function (window, document) { + "use strict"; + + const search = (e) => { + const results = window.searchIndex.search(e.target.value, { + bool: "AND", + expand: true, + }); + + const cleanResults = results.filter((r) => r.doc.title !== undefined) + + const resEl = document.getElementById("search-results"); + const noResultsEl = document.getElementById("no-results-found"); + + resEl.innerHTML = ""; + if (cleanResults.length > 0) { + resEl.style.display = "block"; + noResultsEl.style.display = "none"; + cleanResults.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); diff --git a/docs/js/toc.js b/docs/js/toc.js new file mode 100644 index 000000000..a915b8e50 --- /dev/null +++ b/docs/js/toc.js @@ -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); diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 000000000..c479986b0 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,2602 @@ +{ + "name": "penpot-docs", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "penpot-docs", + "version": "0.1.0", + "license": "SEE LICENSE IN ", + "dependencies": { + "@11ty/eleventy": "^2.0.1", + "@11ty/eleventy-navigation": "^0.3.5", + "@11ty/eleventy-plugin-rss": "^1.2.0", + "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0", + "@tigersway/eleventy-plugin-ancestry": "^1.0.3", + "@types/markdown-it": "14.1.0", + "elasticlunr": "^0.9.5", + "eleventy-plugin-metagen": "^1.8.3", + "eleventy-plugin-nesting-toc": "^1.3.0", + "eleventy-plugin-youtube-embed": "^1.10.2", + "luxon": "^3.4.4", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^9.0.1", + "markdown-it-plantuml": "^1.4.1" + } + }, + "node_modules/@11ty/dependency-tree": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@11ty/dependency-tree/-/dependency-tree-2.0.1.tgz", + "integrity": "sha512-5R+DsT9LJ9tXiSQ4y+KLFppCkQyXhzAm1AIuBWE/sbU0hSXY5pkhoqQYEcPJQFg/nglL+wD55iv2j+7O96UAvg==" + }, + "node_modules/@11ty/eleventy": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@11ty/eleventy/-/eleventy-2.0.1.tgz", + "integrity": "sha512-t8XVUbCJByhVEa1RzO0zS2QzbL3wPY8ot1yUw9noqiSHxJWUwv6jiwm1/MZDPTYtkZH2ZHvdQIRQ5/SjG9XmLw==", + "dependencies": { + "@11ty/dependency-tree": "^2.0.1", + "@11ty/eleventy-dev-server": "^1.0.4", + "@11ty/eleventy-utils": "^1.0.1", + "@11ty/lodash-custom": "^4.17.21", + "@iarna/toml": "^2.2.5", + "@sindresorhus/slugify": "^1.1.2", + "bcp-47-normalize": "^1.1.1", + "chokidar": "^3.5.3", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "dependency-graph": "^0.11.0", + "ejs": "^3.1.9", + "fast-glob": "^3.2.12", + "graceful-fs": "^4.2.11", + "gray-matter": "^4.0.3", + "hamljs": "^0.6.2", + "handlebars": "^4.7.7", + "is-glob": "^4.0.3", + "iso-639-1": "^2.1.15", + "kleur": "^4.1.5", + "liquidjs": "^10.7.0", + "luxon": "^3.3.0", + "markdown-it": "^13.0.1", + "micromatch": "^4.0.5", + "minimist": "^1.2.8", + "moo": "^0.5.2", + "multimatch": "^5.0.0", + "mustache": "^4.2.0", + "normalize-path": "^3.0.0", + "nunjucks": "^3.2.3", + "path-to-regexp": "^6.2.1", + "please-upgrade-node": "^3.2.0", + "posthtml": "^0.16.6", + "posthtml-urls": "^1.0.0", + "pug": "^3.0.2", + "recursive-copy": "^2.0.14", + "semver": "^7.3.8", + "slugify": "^1.6.6" + }, + "bin": { + "eleventy": "cmd.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-dev-server": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-dev-server/-/eleventy-dev-server-1.0.4.tgz", + "integrity": "sha512-qVBmV2G1KF/0o5B/3fITlrrDHy4bONUI2YuN3/WJ3BNw4NU1d/we8XhKrlgq13nNvHoBx5czYp3LZt8qRG53Fg==", + "dependencies": { + "@11ty/eleventy-utils": "^1.0.1", + "chokidar": "^3.5.3", + "debug": "^4.3.4", + "dev-ip": "^1.0.1", + "finalhandler": "^1.2.0", + "mime": "^3.0.0", + "minimist": "^1.2.8", + "morphdom": "^2.7.0", + "please-upgrade-node": "^3.2.0", + "ssri": "^8.0.1", + "ws": "^8.13.0" + }, + "bin": { + "eleventy-dev-server": "cmd.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-fetch/-/eleventy-fetch-4.0.1.tgz", + "integrity": "sha512-yIiLM5ziBmg86i4TlXpBdcIygJHvh/GgPJyAiFOckO9H4y9cQDM8eIcJCUQ4Mum0NEVui/OjhEut2R08xw0vlQ==", + "dependencies": { + "debug": "^4.3.4", + "flat-cache": "^3.0.4", + "node-fetch": "^2.6.7", + "p-queue": "^6.6.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-navigation": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-navigation/-/eleventy-navigation-0.3.5.tgz", + "integrity": "sha512-4aKW5aIQDFed8xs1G1pWcEiFPcDSwZtA4IH1eERtoJ+Xy+/fsoe0pzbDmw84bHZ9ACny5jblENhfZhcCxklqQw==", + "dependencies": { + "dependency-graph": "^0.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-plugin-rss": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-rss/-/eleventy-plugin-rss-1.2.0.tgz", + "integrity": "sha512-YzFnSH/5pObcFnqZ2sAQ782WmpOZHj1+xB9ydY/0j7BZ2jUNahn53VmwCB/sBRwXA/Fbwwj90q1MLo01Ru0UaQ==", + "dependencies": { + "debug": "^4.3.4", + "posthtml": "^0.16.6", + "posthtml-urls": "1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-plugin-syntaxhighlight": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-syntaxhighlight/-/eleventy-plugin-syntaxhighlight-5.0.0.tgz", + "integrity": "sha512-y9BUmP1GofmbJgxM1+ky/UpFCpD8JSOeLeKItUs0WApgnrHk9haHziW7lS86lbArX5SiCVo4zTTw9x53gvRCaA==", + "dependencies": { + "prismjs": "^1.29.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-utils/-/eleventy-utils-1.0.1.tgz", + "integrity": "sha512-HPpCTz4PzudcQU+i+x6GSNHVqgnvRhnVYg5dLKaAoRWLN966odAGsBxKSyhF8i1MdlOPtsytYb2AGWP7jISC5w==", + "dependencies": { + "normalize-path": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/@11ty/eleventy/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@11ty/eleventy/node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/@11ty/eleventy/node_modules/markdown-it": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz", + "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/@11ty/eleventy/node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + }, + "node_modules/@11ty/eleventy/node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, + "node_modules/@11ty/lodash-custom": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@11ty/lodash-custom/-/lodash-custom-4.17.21.tgz", + "integrity": "sha512-Mqt6im1xpb1Ykn3nbcCovWXK3ggywRJa+IXIdoz4wIIK+cvozADH63lexcuPpGS/gJ6/m2JxyyXDyupkMr5DHw==", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", + "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", + "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz", + "integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz", + "integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==", + "dependencies": { + "@babel/helper-string-parser": "^7.18.10", + "@babel/helper-validator-identifier": "^7.18.6", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sindresorhus/slugify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-1.1.2.tgz", + "integrity": "sha512-V9nR/W0Xd9TSGXpZ4iFUcFGhuOJtZX82Fzxj1YISlbSgKvIiNa7eLEZrT0vAraPOt++KHauIVNYgGRgjc13dXA==", + "dependencies": { + "@sindresorhus/transliterate": "^0.1.1", + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/transliterate": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-0.1.2.tgz", + "integrity": "sha512-5/kmIOY9FF32nicXH+5yLNTX4NJ4atl7jRgqAJuIn/iyDFXBktOKDxCvyGE/EzmF4ngSUvjXxQUQlQiZ5lfw+w==", + "dependencies": { + "escape-string-regexp": "^2.0.0", + "lodash.deburr": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/transliterate/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@tigersway/eleventy-plugin-ancestry": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tigersway/eleventy-plugin-ancestry/-/eleventy-plugin-ancestry-1.0.3.tgz", + "integrity": "sha512-ZdJTrD7+B2L+kGGGj+rqqP/NhScXzrlgYqe40o2fkTDmG17ToOvO7DajAQcxmvRRVoWjf9VZn5nZRpAYdlzAIA==", + "peerDependencies": { + "@11ty/eleventy": ">=0.12.1" + } + }, + "node_modules/@types/linkify-it": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-B/o5iMXxpIAl1+792pO4ryhrNNiAus9HhrTs+Gt2m+6SIdEUEvyOIs0iqDNjTSRgbelWvyeek3UxfYYOKDaWfA==", + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==" + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==" + }, + "node_modules/a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==" + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-0.1.0.tgz", + "integrity": "sha512-lqzY9o+BbeGHRCOyxQkt/Tgvz0IZhTmQiA+LxQW8wSNpcTbj8K+0cZiSEvbpNZZP9/11Gy7dnLO3GNWUXO4d1g==" + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "node_modules/assert-never": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz", + "integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==" + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "dependencies": { + "@babel/types": "^7.9.6" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bcp-47": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-1.0.8.tgz", + "integrity": "sha512-Y9y1QNBBtYtv7hcmoX0tR+tUNSFZGZ6OL6vKPObq8BbOhkCoyayF6ogfLTgAli/KuAEbsYHYUNq2AQuY6IuLag==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-match": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-1.0.3.tgz", + "integrity": "sha512-LggQ4YTdjWQSKELZF5JwchnBa1u0pIQSZf5lSdOHEdbVP55h0qICA/FUp3+W99q0xqxYa1ZQizTUH87gecII5w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-normalize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bcp-47-normalize/-/bcp-47-normalize-1.1.1.tgz", + "integrity": "sha512-jWZ1Jdu3cs0EZdfCkS0UE9Gg01PtxnChjEBySeB+Zo6nkqtFfnvtoQQgP1qU1Oo4qgJgxhTI6Sf9y/pZIhPs0A==", + "dependencies": { + "bcp-47": "^1.0.0", + "bcp-47-match": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "dependencies": { + "is-regex": "^1.0.3" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", + "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", + "dependencies": { + "cheerio-select": "^1.5.0", + "dom-serializer": "^1.3.2", + "domhandler": "^4.2.0", + "htmlparser2": "^6.1.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz", + "integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==", + "dependencies": { + "css-select": "^4.1.3", + "css-what": "^5.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0", + "domutils": "^2.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/constantinople": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "dependencies": { + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", + "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^5.0.0", + "domhandler": "^4.2.0", + "domutils": "^2.6.0", + "nth-check": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz", + "integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/dev-ip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", + "integrity": "sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==", + "bin": { + "dev-ip": "lib/dev-ip.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==" + }, + "node_modules/dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/elasticlunr": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/elasticlunr/-/elasticlunr-0.9.5.tgz", + "integrity": "sha1-ZVQbswnd3Qz5Ty0ciGGyvmUbsNU= sha512-5YM9LFQgVYfuLNEoqMqVWIBuF2UNCA+xu/jz1TyryLN/wmBcQSb+GNAwvLKvEpGESwgGN8XA1nbLAt6rKlyHYQ==" + }, + "node_modules/eleventy-plugin-metagen": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/eleventy-plugin-metagen/-/eleventy-plugin-metagen-1.8.3.tgz", + "integrity": "sha512-PP0pQrY/2Jeo5BgBW3xJyBkNhMW0/xOOMBaJkZTJMCT1uOaEFnN0vYPZ3FgD3rvkmhAAVSBwBh172x/E/Y6uQw==", + "dependencies": { + "@11ty/eleventy": "^2.0.1", + "meta-generator": "^0.1.5" + } + }, + "node_modules/eleventy-plugin-nesting-toc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eleventy-plugin-nesting-toc/-/eleventy-plugin-nesting-toc-1.3.0.tgz", + "integrity": "sha512-WZzVkz28nw3A0DpJFQWXZzQxniyjvOZKxVix5x7WVAS0H1OD1hYJbR0go/Lr1kK2SrmxfbsUHUlekR+mQPvqMA==", + "dependencies": { + "cheerio": "^1.0.0-rc.3" + } + }, + "node_modules/eleventy-plugin-youtube-embed": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/eleventy-plugin-youtube-embed/-/eleventy-plugin-youtube-embed-1.10.2.tgz", + "integrity": "sha512-N0068hshDe+sG50SEk4zLbwClaTeyl000WiqmNACRG2Iwb2/iTVV4IGTv7d1yWz/5DGVpsHMniboHJSyChvc9w==", + "dependencies": { + "@11ty/eleventy-fetch": "^4.0.0", + "deepmerge": "^4.3.1", + "lite-youtube-embed": "^0.3.0", + "string-replace-async": "^3.0.2" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tag": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/get-tag/-/get-tag-0.1.10.tgz", + "integrity": "sha512-WizKzwUyC0hopc1JcXj6oP/Iq402XWbxaDJdz2ON/uW4ae66XTIAAULryeCLnozRXGsqc7/EQJKnhHcs26S/aw==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/hamljs": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/hamljs/-/hamljs-0.6.2.tgz", + "integrity": "sha512-/chXRp4WpL47I+HX1vCCdSbEXAljEG2FBMmgO7Am0bYsqgnEjreeWzUdX1onXqwZtcfgxbCg5WtEYYvuZ5muBg==" + }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-equiv-refresh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/http-equiv-refresh/-/http-equiv-refresh-1.0.0.tgz", + "integrity": "sha512-TScO04soylRN9i/QdOdgZyhydXg9z6XdaGzEyOgDKycePeDeTT4KvigjBcI+tgfTlieLWauGORMq5F1eIDa+1w==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "dependencies": { + "acorn": "^7.1.1", + "object-assign": "^4.1.1" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-json": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz", + "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/iso-639-1": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-2.1.15.tgz", + "integrity": "sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", + "dependencies": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "node_modules/junk": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/junk/-/junk-1.0.3.tgz", + "integrity": "sha512-3KF80UaaSSxo8jVnRYtMKNGFOoVPBdkkVPsw+Ad0y4oxKXPduS6G6iHkrf69yJVff/VAaYXkV42rtZ7daJxU3w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/liquidjs": { + "version": "10.16.1", + "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.16.1.tgz", + "integrity": "sha512-1JFL/Y7ONoajrfwav37yuz5yQHU3+Pgz1XWsg9E/2T8Fp65KalNfMF8QZ3+tNETqGUIB66waOSLOi64niYZE9A==", + "dependencies": { + "commander": "^10.0.0" + }, + "bin": { + "liquid": "bin/liquid.js", + "liquidjs": "bin/liquid.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/liquidjs" + } + }, + "node_modules/liquidjs/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, + "node_modules/list-to-array": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/list-to-array/-/list-to-array-1.1.0.tgz", + "integrity": "sha512-+dAZZ2mM+/m+vY9ezfoueVvrgnHIGi5FvgSymbIgJOFwiznWyA59mav95L+Mc6xPtL3s9gm5eNTlNtxJLbNM1g==" + }, + "node_modules/lite-youtube-embed": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/lite-youtube-embed/-/lite-youtube-embed-0.3.2.tgz", + "integrity": "sha512-b1dgKyF4PHhinonmr3PB172Nj0qQgA/7DE9EmeIXHR1ksnFEC2olWjNJyJGdsN2cleKHRjjsmrziKlwXtPlmLQ==" + }, + "node_modules/lodash.deburr": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-4.1.0.tgz", + "integrity": "sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==" + }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-9.0.1.tgz", + "integrity": "sha512-cBt7aAzmkfX8X7FqAe8EBryiKmToXgMQEEMqkXzWCm0toDtfDYIGboKeTKd8cpNJArJtutrf+977wFJTsvNGmQ==", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/markdown-it-plantuml": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/markdown-it-plantuml/-/markdown-it-plantuml-1.4.1.tgz", + "integrity": "sha512-13KgnZaGYTHBp4iUmGofzZSBz+Zj6cyqfR0SXUIc9wgWTto5Xhn7NjaXYxY0z7uBeTUMlc9LMQq5uP4OM5xCHg==" + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/maximatch": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/maximatch/-/maximatch-0.1.0.tgz", + "integrity": "sha512-9ORVtDUFk4u/NFfo0vG/ND/z7UQCVZBL539YW0+U1I7H1BkZwizcPx5foFv7LCPcBnm2U6RjFnQOsIvN4/Vm2A==", + "dependencies": { + "array-differ": "^1.0.0", + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "minimatch": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/maximatch/node_modules/array-differ": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha512-LeZY+DZDRnvP7eMuQ6LHfCzUGxAAIViUBliK24P3hWXL6y4SortgR6Nim6xrkfSLlmH0+k+9NYNwVC2s53ZrYQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/maximatch/node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/maximatch/node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/meta-generator": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/meta-generator/-/meta-generator-0.1.5.tgz", + "integrity": "sha512-stImEDLa5k2TfIMfpFomJKM9LuYzwIIsxS/ejBop0CifPqc4nHoLdTQkABnjrzzkZgLhmXdN144R7OT4dnbKqw==", + "dependencies": { + "get-tag": "^0.1.10" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, + "node_modules/morphdom": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.4.tgz", + "integrity": "sha512-ATTbWMgGa+FaMU3FhnFYB6WgulCqwf6opOll4CBzmVDTLvPMmUPrEv8CudmLPK0MESa64+6B89fWOxP3+YIlxQ==" + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nunjucks": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.3.tgz", + "integrity": "sha512-psb6xjLj47+fE76JdZwskvwG4MYsQKXUtMsPh6U0YMvmyjRtKRFcxnlXGWglNybtNTNVmGdp94K62/+NjF5FDQ==", + "dependencies": { + "a-sync-waterfall": "^1.0.0", + "asap": "^2.0.3", + "commander": "^5.1.0" + }, + "bin": { + "nunjucks-precompile": "bin/precompile" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "chokidar": "^3.3.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-to-regexp": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dependencies": { + "semver-compare": "^1.0.0" + } + }, + "node_modules/posthtml": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.6.tgz", + "integrity": "sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ==", + "dependencies": { + "posthtml-parser": "^0.11.0", + "posthtml-render": "^3.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/posthtml-parser": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.11.0.tgz", + "integrity": "sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==", + "dependencies": { + "htmlparser2": "^7.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/posthtml-parser/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/posthtml-parser/node_modules/htmlparser2": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", + "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "entities": "^3.0.1" + } + }, + "node_modules/posthtml-render": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-3.0.0.tgz", + "integrity": "sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==", + "dependencies": { + "is-json": "^2.0.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/posthtml-urls": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/posthtml-urls/-/posthtml-urls-1.0.0.tgz", + "integrity": "sha512-CMJ0L009sGQVUuYM/g6WJdscsq6ooAwhUuF6CDlYPMLxKp2rmCYVebEU+wZGxnQstGJhZPMvXsRhtqekILd5/w==", + "dependencies": { + "http-equiv-refresh": "^1.0.0", + "list-to-array": "^1.1.0", + "parse-srcset": "^1.0.2", + "promise-each": "^2.2.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/promise-each": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/promise-each/-/promise-each-2.2.0.tgz", + "integrity": "sha512-67roqt1k3QDA41DZ8xi0V+rF3GoaMiX7QilbXu0vXimut+9RcKBNZ/t60xCRgcsihmNUsEjh48xLfNqOrKblUg==", + "dependencies": { + "any-promise": "^0.1.0" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==" + }, + "node_modules/pug": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.2.tgz", + "integrity": "sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==", + "dependencies": { + "pug-code-gen": "^3.0.2", + "pug-filters": "^4.0.0", + "pug-lexer": "^5.0.1", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" + } + }, + "node_modules/pug-attrs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "dependencies": { + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" + } + }, + "node_modules/pug-code-gen": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.2.tgz", + "integrity": "sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==", + "dependencies": { + "constantinople": "^4.0.1", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.0.0", + "pug-runtime": "^3.0.0", + "void-elements": "^3.1.0", + "with": "^7.0.0" + } + }, + "node_modules/pug-error": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.0.0.tgz", + "integrity": "sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==" + }, + "node_modules/pug-filters": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", + "dependencies": { + "constantinople": "^4.0.1", + "jstransformer": "1.0.0", + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0", + "resolve": "^1.15.1" + } + }, + "node_modules/pug-lexer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", + "dependencies": { + "character-parser": "^2.2.0", + "is-expression": "^4.0.0", + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-linker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "dependencies": { + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-load": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "dependencies": { + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "dependencies": { + "pug-error": "^2.0.0", + "token-stream": "1.0.0" + } + }, + "node_modules/pug-runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==" + }, + "node_modules/pug-strip-comments": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "dependencies": { + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recursive-copy": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/recursive-copy/-/recursive-copy-2.0.14.tgz", + "integrity": "sha512-K8WNY8f8naTpfbA+RaXmkaQuD1IeW9EgNEfyGxSqqTQukpVtoOKros9jUqbpEsSw59YOmpd8nCBgtqJZy5nvog==", + "dependencies": { + "errno": "^0.1.2", + "graceful-fs": "^4.1.4", + "junk": "^1.0.1", + "maximatch": "^0.1.0", + "mkdirp": "^0.5.1", + "pify": "^2.3.0", + "promise": "^7.0.1", + "rimraf": "^2.7.1", + "slash": "^1.0.0" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-replace-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/string-replace-async/-/string-replace-async-3.0.2.tgz", + "integrity": "sha512-s6hDtXJ7FKyRap/amefqrOMpkEQvxUDueyvJygQeHxCK5Za90dOMgdibCCrPdfdAYAkr8imrZ1PPXW7DOf0RzQ==", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/token-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + }, + "node_modules/uglify-js": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.0.tgz", + "integrity": "sha512-aTeNPVmgIMPpm1cxXr2Q/nEbvkmV8yq66F3om7X3P/cvOXQ0TMQ64Wk63iyT1gPlmdmGzjGpyLh1f3y8MZWXGg==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/with": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "dependencies": { + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 000000000..49319b13e --- /dev/null +++ b/docs/package.json @@ -0,0 +1,43 @@ +{ + "name": "penpot-docs", + "version": "0.1.0", + "description": "Documentation about Penpot project.", + "scripts": { + "build": "eleventy", + "watch": "eleventy --watch", + "serve": "eleventy --serve", + "start": "eleventy --serve", + "debug": "DEBUG=* eleventy" + }, + "repository": { + "type": "git", + "url": "git://github.com/penpot/penpot.git" + }, + "author": { + "name": "Penpot", + "email": "hello@penpot.app", + "url": "https://penpot.app/" + }, + "license": "SEE LICENSE IN <../LICENSE>", + "bugs": { + "url": "https://github.com/penpot/penpot/issues" + }, + "homepage": "https://docs.penpot.app", + "dependencies": { + "@11ty/eleventy": "^2.0.1", + "@11ty/eleventy-navigation": "^0.3.5", + "@11ty/eleventy-plugin-rss": "^1.2.0", + "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0", + "@tigersway/eleventy-plugin-ancestry": "^1.0.3", + "@types/markdown-it": "14.1.0", + "elasticlunr": "^0.9.5", + "eleventy-plugin-metagen": "^1.8.3", + "eleventy-plugin-nesting-toc": "^1.3.0", + "eleventy-plugin-youtube-embed": "^1.10.2", + "luxon": "^3.4.4", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^9.0.1", + "markdown-it-plantuml": "^1.4.1" + }, + "packageManager": "yarn@4.2.2" +} diff --git a/docs/plugins/api.md b/docs/plugins/api.md new file mode 100644 index 000000000..e378b6a72 --- /dev/null +++ b/docs/plugins/api.md @@ -0,0 +1,8 @@ +--- +layout: layouts/plugins.njk +title: 4. API +--- + +# Penpot plugins API + +We've got all the documentation you need for the API right here. diff --git a/docs/plugins/beta-changelog.md b/docs/plugins/beta-changelog.md new file mode 100644 index 000000000..599f2bb97 --- /dev/null +++ b/docs/plugins/beta-changelog.md @@ -0,0 +1,73 @@ +--- +layout: layouts/plugins-no-sidebar.njk +--- + +# Beta changelog + +### boom Epics and highlights +- This marks the release of version 1.0, and from this point forward, we’ll do our best to avoid making any more breaking changes (or make deprecations backward compatible). +- We’ve redone the documentation. You can check the API here: +[https://penpot-plugins-api-doc.pages.dev/](https://penpot-plugins-api-doc.pages.dev/) +- New samples repository with lots of samples to use the API: +[https://github.com/penpot/penpot-plugins-samples](https://github.com/penpot/penpot-plugins-samples) + +### boom Breaking changes & Deprecations + +- Changed types names to remove the Penpot prefix. So for example: PenpotShape becomes Shape; PenpotFile becomes File, and so on. Check the [API documentation](https://penpot-plugins-api-doc.pages.dev/) for more details. +- Changes on the penpot.on and penpot.off methods. +Previously you had to send the original callback to the off method in order to remove an event listener. Now, penpot.on will return an *id* that you can pass to the penpot.off method in order to remove the listener. + +Previously: +```js +penpot.on(‘pagechange’, myListener); // Register an event listener +penpot.off(‘pagechange’, myListener); // Remove previously registered listener +``` + +Now: +```js +const id = penpot.on(‘pagechange’, myListener); +penpot.off(id); +``` + +We’ve deprecated the old behavior in favor of the new one, this means that the behavior will work in the next version, but will be removed further down the line. + +- Change some names to better align with the names in Penpot's UI. + - type frame is now board: + - PenpotFrame is now Board + - penpot.createFrame changed to penpot.createBoard + - shape.frameX / shape.frameY changed toshape.boardX / shape.boardY + - PenpotFrameGuideX now GuideX + - typerect is rectangle + - PenpotRectangle is now Rectangle + - typecircle isellipse + - PenpotCircle is now Ellipse + - penpot.createCircle changed to penpot.createEllipse + - typebool isboolean + - PenpotBool is now Boolean +- Removed the following methods + - getPage, you can use now the property currentPage + - getFile, you can use now the property currentFile + - getTheme, you can use now the property theme + - getSelected, you can use the property selection + - getSelectedShapes, you can use the property selection + +### sparkles New features + +- Support for comments +- Support for export files +- Support for undo blocks +- Support for ruler guides +- Support for prototype functionality access +- New geometry utils: + - shape.bounds + - shape.center +- New events + - contentsave + - shapechange +- Adds property file.pages +- Adds parent reference to shape +- Add root shape reference to page +- Add detach shape to component method +- Adds method to createPage and openPage +- Adds shape.visible property +- Adds method penpot.viewport.zoomToShapes to change the viewport to the shapes. diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md new file mode 100644 index 000000000..5363fa9be --- /dev/null +++ b/docs/plugins/create-a-plugin.md @@ -0,0 +1,277 @@ +--- +layout: layouts/plugins.njk +title: 2. Create a Plugin +--- + +# Create a Plugin + +This guide covers the creation of a Penpot plugin. Penpot offers two ways to kickstart your development: + +1. Using a Template: + + - **Typescript template**: Using the Penpot Plugin Starter Template: A basic template with the required files for quickstarting your plugin. This template uses Typescript and Vite. + - **Framework templates**: These templates already have everything you need to start developing a plugin using a JavaScript framework. + +

+In case you'll use any of these templates, you can skip to step 2.7 +

+ +2. Creating a plugin from scratch using a major framework. + + Follow to the next section to understand how to bootstrap a new plugin using one of the three major JavaScript frameworks. + +## 2.1. Step 1. Create a project + +Create your own app with the framework of your choice. See examples for each framework here + +| Framework | Command | Version\* | +| --------- | ----------------------------------------------------------- | --------- | +| Angular | ng new plugin-name | 18.0.0 | +| React | npm create vite@latest plugin-name -- --template react-ts | 18.2.0 | +| Vue | npm create vue@latest | 3.4.21 | + +_\*: version we used in the examples._ + +## 2.2. Step 2. Install Penpot libraries + +There are two libraries that can help you with your plugin's development. They are @penpot/plugin-styles and @penpot/plugin-types. + +### Plugin styles + +@penpot/plugin-styles contains styles to help build the UI for Penpot plugins. To check the styles go to Plugin styles. + +```bash +npm install @penpot/plugin-styles +``` + +You can add the styles to your global CSS file. + +```css +@import "@penpot/plugin-styles/styles.css"; +``` + +### Plugin types + +@penpot/plugin-types contains the typings for the Penpot Plugin API. + +```bash +npm install @penpot/plugin-types +``` + +If you're using typescript, don't forget to add @penpot/plugin-types to your typings in your tsconfig.json. + +```json +{ + "compilerOptions": { + [...] + "typeRoots": ["./node_modules/@types", "./node_modules/@penpot"], + "types": ["plugin-types"], + } +} +``` + +## 2.3. Step 3. Create a plugin file + +A plugin file is needed to interact with Penpot and its API. You can use either javascript or typescript and it can be placed wherever you like. It normally goes alongside the main files inside the src/ folder. We highly recommend labeling your creation as plugin.js or plugin.ts, depending upon your preferred language. + +You can start with something like this: + +```ts +penpot.ui.open("Plugin name", "", { + width: 500, + height: 600, +}); +``` + +The sizing values are optional. By default, the plugin will open with a size of 285x540 pixels. + +## 2.4. Step 4. Connect API and plugin interface + +To enable interaction between your plugin and the Penpot API, you'll need to implement message-based communication using JavaScript events. This communication occurs between the main Penpot application and your plugin, which runs in an iframe. The window object facilitates this communication by sending and receiving messages between the two. + +### Sending messages from Penpot to your plugin + +To send a message from the Penpot API to your plugin interface, use the following command in plugin.ts: + +```js +penpot.ui.sendMessage(message); +``` + +Here, message can be any data or instruction you want to pass to your plugin. This message is dispatched from Penpot and is received by your plugin's iframe. + +### Receiving Messages in Your Plugin Interface + +Your plugin can capture incoming messages from Penpot using the window object's message event. To do this, set up an event listener in your plugin like this: + +```js +window.addEventListener("message", (event) => { + // Handle the incoming message + console.log(event.data); +}); +``` + +Theevent.data object contains the message sent from Penpot. You can use this data to update your plugin's interface or trigger specific actions within your plugin. + +### Two-Way Communication + +This setup allows for two-way communication between Penpot and your plugin. Penpot can send messages to your plugin, and your plugin can respond or send messages back to Penpot using the samepostMessage API. For example: + +```js +// Sending a message back to Penpot from your plugin +parent.postMessage(responseMessage, targetOrigin); +``` + +-responseMessage is the data you want to send back to Penpot. +-targetOrigin should be the origin of the Penpot application to ensure messages are only sent to the intended recipient. You can use'*' to allow all. + +### Summary + +By using these message-based events, any data retrieved through the Penpot API can be communicated to and from your plugin interface seamlessly. + +For more detailed information, refer to the [Penpot Plugins API Documentation](https://penpot-plugins-api-doc.pages.dev/). + +## 2.5. Step 5. Build the plugin file + +
+

This step is only for local serving. +For a detailed guide about building and deploying you can check the documentation at Deployment

+

You can skip this step if working exclusively with JavaScript by simply moving plugin.js to your public/ directory.

+
+ +If you wish to run your plugin locally and test it live you need to make your plugin file reachable. Right now, your plugin.ts file is somewhere in the src/ folder, and you can't access it through http:\/\/localhost:XXXX/plugin.js. + +You can achieve this through multiple solutions, but we offer two simple ways of doing so. Of course, you can come up with your own. + +#### Vite + +If you're using Vite you can simply edit the configuration file and add the build to your vite.config.ts. + +```ts +export default defineConfig({ +[...] + build: { + rollupOptions: { + input: { + plugin: "src/plugin.ts", + index: "./index.html", + }, + output: { + entryFileNames: "[name].js", + }, + }, + }, + preview: { + port: XXXX, + }, +}); +``` + +And then add the following scripts to your package.json: + +```json +"scripts": { + "dev": "vite build --watch & vite preview", + "build": "tsc && vite build", + [...] +} +``` + +#### Esbuild + +```bash +$ npm i -D esbuild # install as dev dependency +``` + +Now you can use esbuild to parse and move your plugin file. + +```bash +esbuild your-folder/plugin.ts --minify --outfile=your-folder/public/plugin.js +``` + +You can add it to your package.json scripts so you don't need to manually re-run the build: + +```json + "scripts": { + "start": "npm run build:plugin && ng serve", + "build:plugin": "esbuild your-folder/plugin.ts --minify --outfile=your-folder/public/plugin.js" + [...] + }, +``` + +Keep in mind that you'll need to build again your plugin file if you modify it mid-serve. + +## 2.6. Step 6. Configure the manifest file + +Now that everything is in place you need a manifest.json file to provide Penpot with your plugin data. Remember to make it reachable by placing it in the public/ folder. + +```json +{ + "name": "Plugin name", + "description": "Plugin description", + "code": "/plugin.js", + "icon": "/icon.png", + "permissions": [ + "content:read", + "content:write", + "library:read", + "library:write", + "user:read", + "comment:read", + "comment:write", + "allow:downloads" + ] +} +``` + +### Icon + +The plugin icon must be an image file. All image formats are valid, so you can use whichever format works best for your needs. Although there is no specific size requirement, it is recommended that the icon be 56x56 pixels in order to ensure its optimal appearance across all devices. + +### Types of permissions + +- content:read: Allows reading of content-related data. Grants read access to all endpoints and operations dealing with content. Typical use cases: viewing shapes, pages, or other design elements in a project; accessing the properties and settings of content within the application. + +- content:write: Allows writing or modifying content-related data. Grants write access to all endpoints and operations dealing with content modifications, except those marked as read-only. Typical use cases: adding, updating, or deleting shapes and elements in a design; uploading media or other assets to the project. + +- user:read: Allows reading of user-related data. Grants read access to all endpoints and operations dealing with user data. Typical use cases: viewing user profiles and their associated information or listing active users in a particular context or project. + +- library:read: Allows reading of library-related data and assets. Grants read access to all endpoints and operations dealing with the library context. Typical use cases: accessing shared design elements and components from a library or viewing the details and properties of library assets. + +- library:write: Allows writing or modifying library-related data and assets. Grants write access to all endpoints and operations dealing with library modifications. Typical use cases: adding new components or assets to the library or updating or removing existing library elements. + +- comment:read: Allows reading of comment-related data. Grants read access to all endpoints and operations dealing with comments. +Typical use cases: viewing comments on pages; accessing feedback or annotations provided by collaborators in the project. + +- comment:write: Allows writing or modifying comment-related data. Grants write access to all endpoints and operations dealing with creating, replying, or deleting comments. +Typical use cases: adding new comments to pages; deleting existing comments; replying to comments within the project's context. + +- allow:downloads: Allows downloading of the project file. Grants access to endpoints and operations that enable the downloading of the entire project file. +Typical use cases: downloading the full project file for backup or sharing. + +_Note: Write permissions automatically includes its corresponding read permission (e.g.,content:write includes content:read) because reading is required to perform write or modification actions._ + +## 2.7. Step 7. Load the Plugin in Penpot + +

Serving an application: This refers to making your application accessible over a network, typically for testing or development purposes.

When using a tool like live-server, a local web server is created on your machine, which serves your application files over HTTP. Most modern frameworks offer their own methods for serving applications, and there are build tools like Vite and Webpack that can handle this process as well.

+ +**You don't need to deploy your plugin just to test it**. Locally serving your plugin is compatible with https:\/\/penpot.app/. However, be mindful of potential CORS (Cross-Origin Resource Sharing) issues. To avoid these, ensure your plugin includes the appropriate cross-origin headers. (Find more info about this at the Deployment step) + +Serving your plugin will generate a URL that looks something like http:\/\/localhost:XXXX, where XXXX represents the port number on which the plugin is served. Ensure that both http:\/\/localhost:XXXX/manifest.json and http:\/\/localhost:XXXX/plugin.js are accessible. If these files are inside a specific folder, the URL should be adjusted accordingly (e.g., http:\/\/localhost:XXXX/folder/manifest.json). + +Once your plugin is served you are ready to load it into Penpot. You can use the shortcut Ctrl + Alt + P to open the Plugin Manager modal. In this modal, provide the URL to your plugin's manifest file (e.g., http:\/\/localhost:XXXX/manifest.json) for installation. If everything is set up correctly, the plugin will be installed, and you can launch it whenever needed. + +You can also open the Plugin manager modal via: + +- Menu +
+ +
+ +- Toolbar +
+ +
diff --git a/docs/plugins/deployment.md b/docs/plugins/deployment.md new file mode 100644 index 000000000..5b8951dd6 --- /dev/null +++ b/docs/plugins/deployment.md @@ -0,0 +1,218 @@ +--- +layout: layouts/plugins.njk +title: 3. Deployment +--- + +# Deployment + +When it comes to deploying your plugin there are several platforms to choose from. Each platform has its unique features and benefits, so the choice depends on you. + +In this guide you will found some options for static sites that have free plans. + +## 3.1. Building your project + +The building may vary between frameworks but if you had previously configured your scripts in package.json, npm run build should work. + +The resulting build should be located somewhere in the dist/ folder, maybe somewhere else if you have configured so. + +Be wary that some framework's builders can add additional folders like apps/project-name/, project-name/ or browser/. + +Examples: + +![Vue dist example](/img/plugins/vue_dist.png) +![Angular dist example](/img/plugins/angular_dist.png) + +## 3.2. Netlify + +### Create an account + +You need a Netlify account if you don't already have one. You can sign up with Github, GItlab, BItbucket or via email and password. + +### CORS issues + +To avoid these issues you can add a _headers file to your plugin. Place it in the public/ folder or alongside the main files. + +```js +/* + Access-Control-Allow-Origin: * +``` + +### Connect to Git + +Netlify allows you to import an existing project from GitHub, GitLab, Bitbucket or Azure DevOps. + +- Configure builds. + +#### How to deploy + +
+ +
+ +1. Go to Start and connect with your repository. Allow Netlify to be installed in either all your projects or just the selected ones. + +![Netlify git installation](/img/plugins/install_netlify.png) + +2. Configure your build settings. Netlify auto-detects your framework and offers a basic configuration. This is usually enough. + +![Netlify git configuration](/img/plugins/build_settings.png) + +3. Deploy your plugin. + +### Drag and drop + +Netlify offers a simple drag and drop method. Check Netlify Drop. + +#### How to deploy + +
+ +
+ +1. Build your project + +```bash +npm run build +``` + +2. Go to Netlify Drop. + +3. Drag and drop the build folder into Netlify Sites. Dropping the whole dist may not work, you should drop the folder where the main files are located. + +4. Done! + +## 3.3. Cloudflare + +### Create an account + +You need a Cloudflare account if you don't already have one. You can sign up via email and password. + +### CORS issues + +To avoid these issues you can add a _headers file to your plugin. Place it in the public/ folder or alongside the main files. + +```js +/* + Access-Control-Allow-Origin: * +``` + +### Connect to Git + +Cloudflare allows you to import an existing project from GitHub or GitLab. + +- Git integration + +#### How to deploy + +
+ +
+ + +1. Go to Workers & Pages > Create > Page > Connect to git + +2. Select a repository. Allow Cloudflare to be installed in either all your projects or just the selected ones. + +![Cloudflare git installation](/img/plugins/install_cloudflare.png) + +4. Configure your build settings. + +![Cloudflare git configuration](/img/plugins/cf_build_settings.png) + +5. Save and deploy. + +### Direct upload + +You can directly upload your plugin folder. + +- Direct upload + +#### How to deploy + +
+ +
+ +1. Build your plugin. + +```bash +npm run build +``` + +2. Go to Workers & Pages > Create > Page > Upload assets. + +3. Create a new page. + +![Cloudflare new page](/img/plugins/cf_new_page.png) + +4. Upload your plugin files. You can drag and drop or select the folder. + +![Cloudflare page upload files](/img/plugins/cf_upload_files.png) + +5. Deploy site. + +## 3.4. Surge + +Surge provides a CLI tool for easy deployment. + +- Getting Started. + +### CORS issues + +To avoid these issues you can add a CORS file to your plugin. Place it in the public/ folder or alongside the main files. + +The CORS can contain a * for any domain, or a list of specific domains. + +Check Enabling Cross-Origin Resources sharing. + +### How to deploy + +1. Install surge CLI globally and log into your account or create one. + +```bash +npm install --global surge +surge login +# or +surge signup +``` + +2. Create a CORS file to allow all sites. + +```bash +echo '*' > public/CORS +``` + +3. Build your project. + +```bash +npm run build +``` + +4. Start surge deployment + +```bash +surge + +# Your plugin build folder +project: /home/user/example-plugin/dist/ + +# your domain, surge offers a free .surge.sh domain and free ssl +domain: https://example-plugin-penpot.surge.sh + +upload: [====================] 100% eta: 0.0s (10 files, 305761 bytes) +CDN: [====================] 100% +encryption: *.surge.sh, surge.sh (346 days) +IP: XXX.XXX.XXX.XXX + +Success! - Published to example-plugin-penpot.surge.sh +``` + +5. Done! diff --git a/docs/plugins/examples-templates.md b/docs/plugins/examples-templates.md new file mode 100644 index 000000000..c5c1105f4 --- /dev/null +++ b/docs/plugins/examples-templates.md @@ -0,0 +1,128 @@ +--- +layout: layouts/plugins.njk +title: 5. Examples and templates +--- + +# Examples and templates + +## 5.1. Examples + +We've put together a handy list of some of the most common actions you can perform in penpot, and we've also included a helpful example for each one. We hope this makes it easier for you to create your plugins! + +

+If you just want to get to the examples, you can go straight to the repository here +

+ +### Create a shape + +One of the most basic things you can do in design is create a shape. It's really simple. In this example, we'll show you how to make a rectangle, but you can use the same principles to make other shapes. This makes it easy for you to add different shapes to your design, which is great for building more complex elements and layouts. + +```js +// just replace +penpot.createRectangle(); + +// for one of these other options: +penpot.createEllipse(); +penpot.createPath(); +penpot.createBoard(); +``` + +Shape example + +### Create a text + +You'll learn how to insert text and explore a variety of styling options to customize it to your exact preferences. You'll discover how to adjust font, size, color, and more to create visually engaging text elements. You can also apply multiple styles within a single text string by styling different sections individually, giving you even greater control over your text design and allowing for creative, dynamic typographic effects. + +Text example + +### Group and ungroup shapes + +It's really important to keep your layers organized if you want to keep your workflow clean and efficient. Grouping shapes together makes it much easier to manage and manipulate multiple elements as a single unit. This not only makes your design process much more streamlined, but it also helps you maintain a structured and organized layer hierarchy. When you need to make individual adjustments, you can easily ungroup these shapes, which gives you flexibility while keeping your workspace tidy and well-organized. + +Group and ungroup example + +### Create flex layout + +Flex Layout makes it simple to create designs that adapt and respond to different screens and devices. It automatically adjusts the positioning and sizing of content and containers, so you can resize, align, and distribute elements without any manual intervention. + +Flex layout example + +### Create grid layout + +Grid Layout lets you create flexible designs that automatically adapt to different screen sizes and content changes. You can easily resize, fit, and fill content and containers with this feature, so you don't have to make manual adjustments and you get a seamless, responsive design experience. + +Grid layout example + +### Create a component + +Using components is a great way to reuse objects or groups of objects, making sure everything looks the same and works well across your designs. This example shows you how to create a component, which lets you make your workflow easier by defining reusable design elements. + +

+Just a friendly reminder that it's important to have the library permissions in the manifest.json. +

+ +Components example + +### Create a colors library + +Having quick access to your go-to colors and fonts can really help you work more efficiently, letting you build a solid set of assets with minimal effort. In this example, you'll see how to add a color to your library, so you'll have instant access whenever you need it. The same goes for typography assets—just replace createColor with createTypography. This flexibility means your most commonly used design elements are always at your fingertips, ready to enhance your creative workflow. + +

+Just a friendly reminder that it's important to have the library permissions in the manifest.json. +

+ +```js +// just replace +penpot.library.local.createColor(); + +// for +penpot.library.local.createTypography(); +``` + +Colors library example + +### Theme + +Penpot has dark and light modes, and you can easily add this to your plugin so your interface adapts to both themes. When you add theme support, your plugin will automatically sync with Penpot's interface settings, so the user experience is consistent no matter which mode is selected. This makes your plugin look better and also ensures it stays in line with Penpot's overall design. + +Just a heads-up: if you use the plugin-styles library, many elements will automatically adapt to dark or light mode without any extra effort from you. However, if you need to customize specific elements, be sure to use the selectors provided in the styles.css of the example. + +Theme example + +### Use of third party API + +Often, we want to make our plugins better by adding external libraries, new features, and functionalities. Here's an example of how to use the Picsum library. It shows how you can use third-party APIs to make your plugin development better. Use this as a reference to explore how you can add external resources to your projects. + +Third party API example + +### Interactive prototype + +With the ability to create an interactive prototype, you can turn your design from a static layout into a dynamic, navigable experience. This lets users interact with the design in a more seamless way and gives them a better preview of the final product. + +Interactive prototype example + +### Add ruler guides + +Ruler guides are great for aligning elements exactly where you want them. Check out how to add horizontal and vertical guides to your page or boards. This makes it easier to keep your design looking the same from one place to the next. + +Ruler guides example + +### Create a comment + +Comments are a great way for designers and team members to give each other feedback on a design right away. This example shows how to add comments to specific parts of a design, which makes it easier for everyone to work together and improve their workflow. + +

+Just a friendly reminder that it's important to have the comment permissions in the manifest.json. +

+ +Comments example + + +## 5.2. Templates + +As we mentioned in the Create a plugin section, we've got two great options for you to get started with your plugin. +The first is a basic **Typescript** template with all the essential structure you'll need. +The second is the same, but uses one of the most popular frameworks like **Angular, Vue, or React**. We've included links to the repositories below: + +- Plugin Starter Template with plain Typescript
+- Plugin Starter Template using a framework diff --git a/docs/plugins/faq.md b/docs/plugins/faq.md new file mode 100644 index 000000000..ea8aab8f2 --- /dev/null +++ b/docs/plugins/faq.md @@ -0,0 +1,72 @@ +--- +layout: layouts/plugins.njk +title: 6. FAQ +--- + +# FAQ + +### Which Node version should I use? + +Currently we are using the v22.2.0 + +### Should I create my plugin for dark and light themes? + +It’s not obligatory but keep in mind that the containing modal will change colors automatically to match Penpot’s theme. Check this example on how to apply dark and light themes to your plugin. + +### Should I always host my plugin? + +By the time being any and all plugins must be hosted independently and outside the Penpot environment. Check the documentation for a guide on how to deploy your plugin on some deployment services like Netlify or Cloudflare. + +### Is there any way to export my figma plugins to penpot? + +No. The feature set of figma and penpot are not the same so it’s not compatible. + +### What is the recommended size for my plugin icon? + +You can make it any size since it will be automatically adjusted to 56x56 px in the plugin manager modal. Just make sure to keep it square size. + +### Are there any naming conventions for the plugin name? + +The name of the plugin should be short and followed by the suffix ‘-plugins’, like ‘shape-remover-plugin’. + +### Which framework do you recommend for creating the plugin? + +Any framework you are familiar with would be a good choice. Our examples are in vue, angular and react. Check the documentation + +### Is it necessary to use the plugin styles library? + +The plugin styles library is not obligatory, although we recommend its use because it'll help you with the dark and light theming and to maintain the Penpot look-and-feel. + +### Is the API ready to use the prototyping features? + +Absolutely! You can definitely create flows and interactions in the same elements as in the interface, like frames, shapes, and groups. Just check out the API documentation for the methods: createFlow, addInteraction, or removeInteraction. And if you need more help, you can always check out the PenpotFlow or PenpotInteraction interfaces. + +### Are there any security or quality criteria I should be aware of? + +There are no set requirements. However, we can recommend the use of eslint or prettier, which is what we use. + +### Is it necessary to create plugins with a UI? + +No, it’s completely optional, in fact, we have an example of a plugin without UI. Try the plugin using this url to install it: https:\/\/create-palette-penpot-plugin.pages.dev/assets/manifest.json or check the code here + +### Can I create components? + +Yes, it is possible to create components using: + +```js +createComponent(shapes: Shape[]): LibraryComponent; +``` + +Take a look at the Penpot Library methods in the API documentation or this simple example. + +### Is there a place where I can share my plugin? + +You will be able to share your plugin with the Penpot community. In the future, we plan to create a place where we will publish the plugins we know about, but this is still something we have to define. + +### My plugin works on my local machine, but I couldn’t install it on Penpot. What could be the problem? + +The url you that you need to provide in the plugin manager should look like this: https:\/\/yourdomain.com/assents/manifest.json + +### Where can I get support if I find a bug or an unexpected behavior? + +You can report a problem or request support at support@penpot.app. diff --git a/docs/plugins/getting-started.md b/docs/plugins/getting-started.md new file mode 100644 index 000000000..89f42f61c --- /dev/null +++ b/docs/plugins/getting-started.md @@ -0,0 +1,199 @@ +--- +layout: layouts/plugins.njk +title: 1. Getting started +--- + +# Getting started + +## 1.1. Introduction + +Welcome to Penpot Plugins! + +Plugins are the perfect tool to easily extend Penpot's functionality, you can automate repetitive tasks, add new features and much more. + +Plugins can be created with your favorite framework or with not framework at all. Feel free to use whatever you want because Plugins are independent from Penpot's code and therefore you don't need any extra knowledge. + +The plugins will be hosted outside Penpot, and each creator need to host theirs. + +## 1.2. Pre-requisites + +- Basic experience with Penpot. +- Basic experience with JavaScript, HTML and CSS. +- Node and npm (How to install Node.js). +- A text editor, ideally an IDE like Visual Studio Code or similar. + +Nice to have: + +- Git basic knowledge. +- A Github account or a similar service to host and share your plugin code. +- TypeScript basic knowledge. +- Experience with any front framework (angular, react, vue...) for complex user interfaces. +- A hosting service of your choice for plugin's deployment. + +## 1.3. Installation + +With the plugins system enabled, you need to go to any project to open the plugin manager. + +You can open the plugin manager in any project via: + +##### Shortcut + +| Linux and Windows | macOS | +| ----------------------------------------- | -------------------------------------- | +| CtrlAltP | AltP | + +##### Menu + +
+ +
+ +##### Toolbar + +
+ +
+ +The plugin manager looks like this: + +![Penpot's plugin manager](/img/plugins/plugin-manager.png) + +You need to provide the plugin's manifest URL for the installation. If there are no issues the plugin will be installed and then you would be able to open it whenever you like. + +### Examples + +| Name | URL | +| ------------- | ------------------------------------------------------------------- | +| Lorem Ipsum | https://lorem-ipsum-penpot-plugin.pages.dev/assets/manifest.json | +| Contrast | https://contrast-penpot-plugin.pages.dev/assets/manifest.json | +| Feather icons | https://icons-penpot-plugin.pages.dev/assets/manifest.json | +| Tables | https://table-penpot-plugin.pages.dev/assets/manifest.json | +| Color palette | https://create-palette-penpot-plugin.pages.dev/assets/manifest.json | +| Rename layers | https://rename-layers-penpot-plugin.pages.dev/assets/manifest.json | + +## 1.4. Plugin's basics + +### How does it work? + +Penpot's plugin system allows you to add new features to the platform through independent modules called plugins. These plugins run separately from the main Penpot app, inside iframes, which are like small, isolated browser windows within the app. + +Plugins communicate with Penpot by sending and receiving messages through the iframe. + +@startuml + +skinparam state { +BackgroundColor transparent +BorderColor black +ArrowColor black +} + +Penpot_App -down-> WebComponent +WebComponent -up-> Penpot_App : Write / Read +WebComponent : - Create API +WebComponent : - Create sandbox (ses) +WebComponent : - Read plugin manifest +WebComponent : - Run plugin code +WebComponent -right-> Plugin_code +Plugin_code : penpot.ui.open('Example plugin', ''); +Plugin_code : +Plugin_code : penpot.ui.onMessage((message) => { +Plugin_code : console.log('iframe message', message); +Plugin_code : }); +Plugin_code -right-> Iframe +Iframe -left-> Plugin_code +Iframe : +Iframe : +Iframe : + +CSS_Library -down-> Iframe +External_API -up-> Iframe +Iframe -down-> External_API + +@enduml + +### What is manifest.json file? + +The manifest.json file contains the basic information about the plugin. It defines the plugin's name, description, the main code file, and the permissions it requires. The structure of the manifest.json file looks like this: + +```json +{ + "name": "Your plugin name", + "description": "Your plugin description", + "code": "plugin.js", + "icon": "Your icon", + "permissions": [ + "content:read", + "content:write", + "library:read", + "library:write", + "user:read", + "comment:read", + "comment:write", + "allow:downloads" + ] +} +``` + +#### Properties + +- **Name and description**: your plugin's basic information, which will be displayed in the plugin manager modal. +- **Code**: your plugin's file location. It needs to be compiled to JavaScript and reachable. +- **Icon**: your plugin's icon, which will be also displayed in the plugin manager modal. It'll be a tag so you can use whichever image format works better for you. **It's recommended to use a 56x56 pixel icon for the best appearance on all devices**. +- **Permissions**: your plugin's permissions, which allow access to different parts of the Penpot API. + +#### Types of permissions + +- content:read: Allows reading of content-related data. Grants read access to all endpoints and operations dealing with content. Typical use cases: viewing shapes, pages, or other design elements in a project; accessing the properties and settings of content within the application. + +- content:write: Allows writing or modifying content-related data. Grants write access to all endpoints and operations dealing with content modifications, except those marked as read-only. Typical use cases: adding, updating, or deleting shapes and elements in a design; uploading media or other assets to the project. + +- user:read: Allows reading of user-related data. Grants read access to all endpoints and operations dealing with user data. Typical use cases: viewing user profiles and their associated information or listing active users in a particular context or project. + +- library:read: Allows reading of library-related data and assets. Grants read access to all endpoints and operations dealing with the library context. Typical use cases: accessing shared design elements and components from a library or viewing the details and properties of library assets. + +- library:write: Allows writing or modifying library-related data and assets. Grants write access to all endpoints and operations dealing with library modifications. Typical use cases: adding new components or assets to the library or updating or removing existing library elements. + +- comment:read: Allows reading of comment-related data. Grants read access to all endpoints and operations dealing with comments. +Typical use cases: viewing comments on pages; accessing feedback or annotations provided by collaborators in the project. + +- comment:write: Allows writing or modifying comment-related data. Grants write access to all endpoints and operations dealing with creating, replying, or deleting comments. +Typical use cases: adding new comments to pages; deleting existing comments; replying to comments within the project's context. + +- allow:downloads: Allows downloading of the project file. Grants access to endpoints and operations that enable the downloading of the entire project file. +Typical use cases: downloading the full project file for backup or sharing. + +_Note: Write permissions automatically includes its corresponding read permission (e.g., content:write includes content:read) because reading is required to perform write or modification actions._ + +### What are plugin.ts and plugin.js files? + +The plugin.ts file is where you write code to interact with the Penpot API using TypeScript. This file is then compiled into plugin.js which is the final JavaScript code that runs the plugin. You don't write plugin.js directly; it's generated from the plugin.ts file. + +

+This is also the only file where you can use the Penpot object. Do not try to use the Penpot object in your plugin interface scripts. +

+ +You can check some samples in: + +- Penpot plugin samples. + +### What is TypeScript? + +You may have noticed that we're using TypeScript in our plugin files, but what is it, and why? + +TypeScript is like JavaScript with extra rules. These rules help you catch mistakes early, before you run your code. It makes your code more reliable and easier to manage, especially in big projects. + +We're using TypeScript to make working with the Penpot API easier, as it provides autocompletion and instant access to documentation. However, even with TypeScript’s powerful features, you'll still need to include the @penpot/plugin-types npm package, which contains the typings for the Penpot Plugin API. This ensures that TypeScript can fully understand and work with the API. + +![plugin-types example](/img/plugins/plugint-types-example.gif) + +You can install the package in any project with npm install @penpot/plugin-types. You can check the details in [@penpot/plugin-types package](https://www.npmjs.com/package/@penpot/plugin-types). diff --git a/docs/plugins/index.njk b/docs/plugins/index.njk new file mode 100644 index 000000000..3390db4b3 --- /dev/null +++ b/docs/plugins/index.njk @@ -0,0 +1,48 @@ +--- +layout: layouts/plugins-home.njk +title: Plugins +eleventyNavigation: + key: Plugins + order: 5 +--- + +

Plugins

+ + diff --git a/docs/scripts/build.sh b/docs/scripts/build.sh new file mode 100755 index 000000000..5204eda4c --- /dev/null +++ b/docs/scripts/build.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +source ~/.bashrc + +set -ex +rm -rf ./_dist +yarn +yarn run build diff --git a/docs/search-index.json.njk b/docs/search-index.json.njk new file mode 100644 index 000000000..6293c5574 --- /dev/null +++ b/docs/search-index.json.njk @@ -0,0 +1,6 @@ +--- +permalink: /search-index.json +--- + +{{ collections.all | search | dump | safe }} + diff --git a/docs/sitemap.xml.njk b/docs/sitemap.xml.njk new file mode 100644 index 000000000..0a744805b --- /dev/null +++ b/docs/sitemap.xml.njk @@ -0,0 +1,14 @@ +--- +permalink: /sitemap.xml +eleventyExcludeFromCollections: true +--- + + +{%- for page in collections.all %} + {% set absoluteUrl %}{{ page.url | url | absoluteUrl(metadata.url) }}{% endset %} + + {{ absoluteUrl }} + {{ page.date | htmlDateString }} + +{%- endfor %} + diff --git a/docs/technical-guide/configuration.md b/docs/technical-guide/configuration.md new file mode 100644 index 000000000..ba2380a49 --- /dev/null +++ b/docs/technical-guide/configuration.md @@ -0,0 +1,482 @@ +--- +title: 2. Penpot Configuration +--- + +# Penpot Configuration # + +This section intends to explain all available configuration options, when you +are self-hosting Penpot or also if you are using the Penpot developer setup. + +Penpot is configured using environment variables. All variables start with PENPOT_ +prefix. + +Variables are initialized in the docker-compose.yaml file, as explained in the +Self-hosting guide with [Elestio][1] or [Docker][2]. + +Additionally, if you are using the developer environment, you may override their values in +the startup scripts, as explained in the [Developer Guide][3]. + +**NOTE**: All the examples that have values represent the **default** values, and the +examples that do not have values are optional, and inactive by default. + + +## Common ## + +This section will list all common configuration between backend and frontend. + +There are two types of configuration: options (properties that require some value) and +flags (that just enables or disables something). All flags are set in a single +PENPOT_FLAGS environment variable. The envvar is a list of strings using this +format: -\. For example: + +```bash +PENPOT_FLAGS: enable-smpt disable-registration disable-email-verification +``` + +### Registration ### + +Penpot comes with an option to completely disable the registration process; +for this, use the following variable: + +```bash +PENPOT_FLAGS: [...] disable-registration +``` + +You may also want to restrict the registrations to a closed list of domains: + +```bash +# comma separated list of domains (backend only) +PENPOT_REGISTRATION_DOMAIN_WHITELIST: + +# OR (backend only) +PENPOT_EMAIL_DOMAIN_WHITELIST: path/to/whitelist.txt +``` + +**NOTE**: Since version 2.1, email whitelisting should be explicitly +enabled with enable-email-whitelist flag. For backward compatibility, we +autoenable it when PENPOT_REGISTRATION_DOMAIN_WHITELIST is set with +not-empty content. + +### Demo users ### + +Penpot comes with facilities for fast creation of demo users without the need of a +registration process. The demo users by default have an expiration time of 7 days, and +once expired they are completely deleted with all the generated content. Very useful for +testing or demonstration purposes. + +You can enable demo users using the following variable: + +```bash +PENPOT_FLAGS: [...] enable-demo-users +``` + +### Authentication Providers + +To configure the authentication with third-party auth providers you will need to +configure Penpot and set the correct callback of your Penpot instance in the auth-provider +configuration. + +The callback has the following format: + +```html +https:///api/auth/oauth//callback +``` + + +You will need to change and according to your setup. +This is how it looks with Gitlab provider: + +```html +https:///api/auth/oauth/gitlab/callback +``` + +#### Penpot + +Consists on registration and authentication via email / password. It is enabled by default, +but login can be disabled with the following flags: + +```bash +PENPOT_FLAGS: [...] disable-login-with-password +``` + +And the registration also can be disabled with: + +```bash +PENPOT_FLAGS: [...] disable-registration +``` + + +#### Google + +Allows integrating with Google as OAuth provider: + +```bash +PENPOT_FLAGS: [...] enable-login-with-google + +# Backend only: +PENPOT_GOOGLE_CLIENT_ID: +PENPOT_GOOGLE_CLIENT_SECRET: +``` + +#### GitLab + +Allows integrating with GitLab as OAuth provider: + +```bash +PENPOT_FLAGS: [...] enable-login-with-gitlab + +# Backend only +PENPOT_GITLAB_BASE_URI: https://gitlab.com +PENPOT_GITLAB_CLIENT_ID: +PENPOT_GITLAB_CLIENT_SECRET: +``` + +#### GitHub + +Allows integrating with GitHub as OAuth provider: + +```bash +PENPOT_FLAGS: [...] enable-login-with-github + +# Backend only +PENPOT_GITHUB_CLIENT_ID: +PENPOT_GITHUB_CLIENT_SECRET: +``` + +#### OpenID Connect + +**NOTE:** Since version 1.5.0 + +Allows integrating with a generic authentication provider that implements the OIDC +protocol (usually used for SSO). + +All the other options are backend only: + +```bash +PENPOT_FLAGS: [...] enable-login-with-oidc + +## Backend only +PENPOT_OIDC_CLIENT_ID: + +# Mainly used for auto discovery the openid endpoints +PENPOT_OIDC_BASE_URI: +PENPOT_OIDC_CLIENT_SECRET: + +# Optional backend variables, used mainly if you want override; they are +# autodiscovered using the standard openid-connect mechanism. +PENPOT_OIDC_AUTH_URI: +PENPOT_OIDC_TOKEN_URI: +PENPOT_OIDC_USER_URI: + +# Optional list of roles that users are required to have. If no role +# is provided, roles checking disabled. +PENPOT_OIDC_ROLES: "role1 role2" + +# Attribute to use for lookup roles on the user object. Optional, if +# not provided, the roles checking will be disabled. +PENPOT_OIDC_ROLES_ATTR: +``` +
+ +__Since version 1.6.0__ + +Added the ability to specify custom OIDC scopes. + +```bash +# This settings allow overwrite the required scopes, use with caution +# because Penpot requres at least `name` and `email` attrs found on the +# user info. Optional, defaults to `openid profile`. +PENPOT_OIDC_SCOPES: "scope1 scope2" +``` +
+ +__Since version 1.12.0__ + +Added the ability to specify the name and email attribute to use from +the userinfo object for the profile creation. + +```bash +# Attribute to use for lookup the name on the user object. Optional, +# if not perovided, the `name` prop will be used. +PENPOT_OIDC_NAME_ATTR: + +# Attribute to use for lookup the email on the user object. Optional, +# if not perovided, the `email` prop will be used. +PENPOT_OIDC_EMAIL_ATTR: +``` +
+ +__Since version 1.19.0__ + +Introduced the ability to lookup the user info from the token instead +of making a request to the userinfo endpoint. This reduces the latency +of OIDC login operations and increases compatibility with some +providers that exposes some claims on tokens but not in userinfo +endpoint. + +```bash +# Set the default USER INFO source. Can be `token` or `userinfo`. By default +# is unset (both will be tried, starting with token). + +PENPOT_OIDC_USER_INFO_SOURCE: +``` +
+ +__Since version 2.1.2__ + +Allows users to register and login with oidc without having to previously +register with another method. + +```bash +PENPOT_FLAGS: [...] enable-oidc-registration +``` + + +#### Azure Active Directory using OpenID Connect + +Allows integrating with Azure Active Directory as authentication provider: + +```bash +# Backend & Frontend +PENPOT_OIDC_CLIENT_ID: + +## Backend only +PENPOT_OIDC_BASE_URI: https://login.microsoftonline.com//v2.0/ +PENPOT_OIDC_CLIENT_SECRET: +``` + +### LDAP ### + +Penpot comes with support for *Lightweight Directory Access Protocol* (LDAP). This is the +example configuration we use internally for testing this authentication backend. + +```bash +PENPOT_FLAGS: [...] enable-login-with-ldap + +## Backend only +PENPOT_LDAP_HOST: ldap +PENPOT_LDAP_PORT: 10389 +PENPOT_LDAP_SSL: false +PENPOT_LDAP_STARTTLS: false +PENPOT_LDAP_BASE_DN: ou=people,dc=planetexpress,dc=com +PENPOT_LDAP_BIND_DN: cn=admin,dc=planetexpress,dc=com +PENPOT_LDAP_BIND_PASSWORD: GoodNewsEveryone +PENPOT_LDAP_USER_QUERY: (&(|(uid=:username)(mail=:username))(memberOf=cn=penpot,ou=groups,dc=my-domain,dc=com)) +PENPOT_LDAP_ATTRS_USERNAME: uid +PENPOT_LDAP_ATTRS_EMAIL: mail +PENPOT_LDAP_ATTRS_FULLNAME: cn +PENPOT_LDAP_ATTRS_PHOTO: jpegPhoto +``` + +If you miss something, please open an issue and we discuss it. + + +## Backend ## + +This section enumerates the backend only configuration variables. + + +### Database + +We only support PostgreSQL and we highly recommend >=13 version. If you are using official +docker images this is already solved for you. + +Essential database configuration: + +```bash +# Backend +PENPOT_DATABASE_USERNAME: penpot +PENPOT_DATABASE_PASSWORD: penpot +PENPOT_DATABASE_URI: postgresql://127.0.0.1/penpot +``` + +The username and password are optional. These settings should be compatible with the ones +in the postgres configuration: + +```bash +# Postgres +POSTGRES_DATABASE: penpot +POSTGRES_USER: penpot +POSTGRES_PASSWORD: penpot +``` + +### Email (SMTP) + +By default, smpt flag is disabled, the email will be +printed to the console, which means that the emails will be shown in the stdout. + +Note that if you plan to invite members to a team, it is recommended that you enable SMTP +as they will need to login to their account after recieving the invite link sent an in email. +It is currently not possible to just add someone to a team without them accepting an +invatation email. + +If you have an SMTP service, uncomment the appropriate settings section in +docker-compose.yml and configure those +environment variables. + +Setting up the default FROM and REPLY-TO: + +```bash +# Backend +PENPOT_SMTP_DEFAULT_REPLY_TO: Penpot +PENPOT_SMTP_DEFAULT_FROM: Penpot +``` + +Enable SMTP: + +```bash +PENPOT_FLAGS: [...] enable-smtp +# Backend +PENPOT_SMTP_HOST: +PENPOT_SMTP_PORT: 587 +PENPOT_SMTP_USERNAME: +PENPOT_SMTP_PASSWORD: +PENPOT_SMTP_TLS: true +``` + +### Storage + +Storage refers to storage used for store the user uploaded assets. + +Assets storage is implemented using "plugable" backends. Currently there are three +backends available: fs and s3 (for AWS S3). + +#### FS Backend (default) #### + +This is the default backend when you use the official docker images and the default +configuration looks like this: + +```bash +# Backend +PENPOT_ASSETS_STORAGE_BACKEND: assets-fs +PENPOT_STORAGE_ASSETS_FS_DIRECTORY: /opt/data/assets +``` + +The main downside of this backend is the hard dependency on nginx approach to serve files +managed by an application (not a simple directory serving static files). But you should +not worry about this unless you want to install it outside the docker container and +configure the nginx yourself. + +In case you want understand how it internally works, you can take a look on the [nginx +configuration file][4] used in the docker images. + + +#### AWS S3 Backend #### + +This backend uses AWS S3 bucket for store the user uploaded assets. For use it you should +have an appropriate account on AWS cloud and have the credentials, region and the bucket. + +This is how configuration looks for S3 backend: + +```bash +# AWS Credentials +AWS_ACCESS_KEY_ID: +AWS_SECRET_ACCESS_KEY: + +# Backend configuration +PENPOT_ASSETS_STORAGE_BACKEND: assets-s3 +PENPOT_STORAGE_ASSETS_S3_REGION: +PENPOT_STORAGE_ASSETS_S3_BUCKET: + +# Optional if you want to use it with non AWS, S3 compatible service: +PENPOT_STORAGE_ASSETS_S3_ENDPOINT: +``` + +### Redis + +The redis configuration is very simple, just provide with a valid redis URI. Redis is used +mainly for websocket notifications coordination. + +```bash +# Backend +PENPOT_REDIS_URI: redis://localhost/0 +``` + +If you are using the official docker compose file, this is already configured. + + +### HTTP + +You will need to set the PENPOT_PUBLIC_URI environment +variable in case you go to serve Penpot to the users; it should point to public URI +where users will access the application: + +```bash +PENPOT_PUBLIC_URI: http://localhost:9001 +``` + +## Frontend ## + +In comparison with backend, frontend only has a small number of runtime configuration +options, and they are located in the \/js/config.js file. + +If you are using the official docker images, the best approach to set any configuration is +using environment variables, and the image automatically generates the config.js from +them. + +**NOTE**: many frontend related configuration variables are explained in the +[Common](#common) section, this section explains **frontend only** options. + +But in case you have a custom setup you probably need setup the following environment +variables on the frontend container: + +To connect the frontend to the exporter and backend, you need to fill out these environment variables. + +```bash +# Frontend +PENPOT_BACKEND_URI: http://your-penpot-backend +PENPOT_EXPORTER_URI: http://your-penpot-exporter +``` + +These variables are used for generate correct nginx.conf file on container startup. + + +### Demo warning ### + +If you want to show a warning in the register and login page saying that this is a +demonstration purpose instance (no backups, periodical data wipe, ...), set the following +variable: + +```bash +PENPOT_FLAGS: [...] enable-demo-warning +``` + +## Other flags + +- enable-cors: Enables the default cors cofiguration that allows all domains + (this configuration is designed only for dev purposes right now) +- enable-backend-api-doc: Enables the /api/doc + endpoint that lists all rpc methods available on backend +- disable-email-verification: Deactivates the email verification process + (only recommended for local or internal setups) +- disable-secure-session-cookies: By default, Penpot uses the + secure flag on cookies, this flag disables it; + it is useful if you plan to serve Penpot under different + domain than localhost without HTTPS +- disable-login-with-password: allows disable password based login form +- disable-registration: disables registration (still enabled for invitations only). +- enable-prepl-server: enables PREPL server, used by manage.py and other additional + tools for communicate internally with Penpot backend + +__Since version 1.13.0__ + +- enable-log-invitation-tokens: for cases where you don't have email configured, this + will log to console the invitation tokens +- enable-log-emails: if you want to log in console send emails. This only works if smtp + is not configured + +__Since version 2.0.0__ + +- disable-onboarding-team: for disable onboarding team creation modal +- disable-onboarding-newsletter: for disable onboarding newsletter modal +- disable-onboarding-questions: for disable onboarding survey +- disable-onboarding: for disable onboarding modal +- disable-dashboard-templates-section: for hide the templates section from dashboard +- enable-webhooks: for enable webhooks +- enable-access-tokens: for enable access tokens +- disable-google-fonts-provider: disables the google fonts provider (frontend) + +[1]: /technical-guide/getting-started#configure-penpot-with-elestio +[2]: /technical-guide/getting-started#configure-penpot-with-docker +[3]: /technical-guide/developer/common#dev-environment +[4]: https://github.com/penpot/penpot/blob/main/docker/images/files/nginx.conf diff --git a/docs/technical-guide/developer/architecture/backend.md b/docs/technical-guide/developer/architecture/backend.md new file mode 100644 index 000000000..1fd2ab2a7 --- /dev/null +++ b/docs/technical-guide/developer/architecture/backend.md @@ -0,0 +1,136 @@ +--- +title: Backend app +--- + +# Backend app + +This app is in charge of CRUD of data, integrity validation and persistence +into a database and also into a file system for media attachments. + +To handle deletions it uses a garbage collector mechanism: no object in the +database is deleted instantly. Instead, a field deleted_at is set with the +date and time of the deletion, and every query ignores db rows that have this +field set. Then, an async task that runs periodically, locates rows whose +deletion date is older than a given threshold and permanently deletes them. + +For this, and other possibly slow tasks, there is an internal async tasks +worker, that may be used to queue tasks to be scheduled and executed when the +backend is idle. Other tasks are email sending, collecting data for telemetry +and detecting unused media attachment, for removing them from the file storage. + +## Backend structure + +Penpot backend app code resides under backend/src/app path in the main repository. + +@startuml BackendGeneral +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml +!define DEVICONS https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/devicons +!include DEVICONS/react.puml +!include DEVICONS/java.puml +!include DEVICONS/clojure.puml +!include DEVICONS/postgresql.puml +!include DEVICONS/redis.puml +!include DEVICONS/chrome.puml + +HIDE_STEREOTYPE() + +Container(frontend_app, "Frontend app", "React / ClojureScript", "", "react") + +System_Boundary(backend, "Backend") { + Container(backend_app, "Backend app", "Clojure / JVM", "", "clojure") + ContainerDb(db, "Database", "PostgreSQL", "", "postgresql") + ContainerDb(redis, "Broker", "Redis", "", "redis") +} + +BiRel(frontend_app, backend_app, "Open", "websocket") +Rel(frontend_app, backend_app, "Uses", "RPC API") +Rel(backend_app, db, "Uses", "SQL") +Rel(redis, backend_app, "Subscribes", "pub/sub") +Rel(backend_app, redis, "Notifies", "pub/sub") + +@enduml + +``` + ▾ backend/src/app/ + ▸ cli/ + ▸ http/ + ▸ migrations/ + ▸ rpc/ + ▸ setup/ + ▸ srepl/ + ▸ util/ + ▸ tasks/ + main.clj + config.clj + http.clj + metrics.clj + migrations.clj + notifications.clj + rpc.clj + setup.clj + srepl.clj + worker.clj + ... +``` + +* main.clj defines the app global settings and the main entry point of the + application, served by a JVM. +* config.clj defines of the configuration options read from linux + environment. +* http contains the HTTP server and the backend routes list. +* migrations contains the SQL scripts that define the database schema, in + the form of a sequence of migrations. +* rpc is the main module to handle the RPC API calls. +* notifications.clj is the main module that manages the websocket. It allows + clients to subscribe to open files, intercepts update RPC calls and notify + them to all subscribers of the file. +* setup initializes the environment (loads config variables, sets up the + database, executes migrations, loads initial data, etc). +* srepl sets up an interactive REPL shell, with some useful commands to be + used to debug a running instance. +* cli sets a command-line interface, with some more maintenance commands. +* metrics.clj has some interceptors that watches RPC calls, calculate + statistics and other metrics, and send them to external systems to store and + analyze. +* worker.clj and tasks define some async tasks that are executed in + parallel to the main http server (using java threads), and scheduled in a + cron-like table. They are useful to do some garbage collection, data packing + and similar periodic maintenance tasks. +* db.clj, emails.clj, media.clj, msgbus.clj, storage.clj, + rlimits.clj are general libraries to use I/O resources (SQL database, + send emails, handle multimedia objects, use REDIS messages, external file + storage and semaphores). +* util/ has a collection of generic utility functions. + +### RPC calls + +The RPC (Remote Procedure Call) subsystem consists of a mechanism that allows +to expose clojure functions as an HTTP endpoint. We take advantage of being +using Clojure at both front and back ends, to avoid needing complex data +conversions. + + 1. Frontend initiates a "query" or "mutation" call to :xxx method, and + passes a Clojure object as params. + 2. Params are string-encoded using + [transit](https://github.com/cognitect/transit-clj), a format similar to + JSON but more powerful. + 3. The call is mapped to /api/rpc/query/xxx or + /api/rpc/mutation/xxx. + 4. The rpc module receives the call, decode the parameters and executes the + corresponding method inside src/app/rpc/queries/ or src/app/rpc/mutations/. + We have created a defmethod macro to declare an RPC method and its + parameter specs. + 5. The result value is also transit-encoded and returned to the frontend. + +This way, frontend can execute backend calls like it was calling an async function, +with all the power of Clojure data structures. + +### PubSub + +To manage subscriptions to a file, to be notified of changes, we use a redis +server as a pub/sub broker. Whenever a user visits a file and opens a +websocket, the backend creates a subscription in redis, with a topic that has +the id of the file. If the user sends any change to the file, backend sends a +notification to this topic, that is received by all subscribers. Then the +notification is retrieved and sent to the user via the websocket. + diff --git a/docs/technical-guide/developer/architecture/common.md b/docs/technical-guide/developer/architecture/common.md new file mode 100644 index 000000000..c6f2c7a55 --- /dev/null +++ b/docs/technical-guide/developer/architecture/common.md @@ -0,0 +1,84 @@ +--- +title: Common code +--- + +# Common code + +In penpot, we take advantage of using the same language in frontend and +backend, to have a bunch of shared code. + +Sometimes, we use conditional compilation, for small chunks of code that +are different in a Clojure+Java or ClojureScript+JS environments. We use +the #? construct, like this, for example: + +```clojure +(defn ordered-set? + [o] + #?(:cljs (instance? lks/LinkedSet o) + :clj (instance? LinkedSet o))) +``` + +```text + ▾ common/src/app/common/ + ▸ geom/ + ▸ pages/ + ▸ path/ + ▸ types/ + ... +``` + +Some of the modules need some refactoring, to organize them more cleanly. + +## Data model and business logic + +* **geom** contains functions to manage 2D geometric entities. + - **point** defines the 2D Point type and many geometric transformations. + - **matrix** defines the [2D transformation + matrix](https://www.alanzucconi.com/2016/02/10/tranfsormation-matrix/) + type and its operations. + - **shapes** manages shapes as a collection of points with a bounding + rectangle. +* **path** contains functions to manage SVG paths, transform them and also + convert other types of shapes into paths. +* **pages** contains the definition of the [Penpot data model](/technical-guide/developer/data-model/) and + the conceptual business logic (transformations of the model entities, + independent of the user interface or data storage). + - **spec** has the definitions of data structures of files and shapes, and + also of the transformation operations in **changes** module. Uses [Clojure + spec](https://github.com/clojure/spec.alpha) to define the structure and + validators. + - **init** defines the default content of files, pages and shapes. + - **helpers** are some functions to help manipulating the data structures. + - **migrations** is in charge to manage the evolution of the data model + structure over time. It contains a function that gets a file data + content, identifies its version, and applies the needed migrations. Much + like the SQL database migrations scripts. + - **changes** and **changes_builder** define a set of transactional + operations, that receive a file data content, and perform a semantic + operation following the business logic (add a page or a shape, change a + shape attribute, modify some file asset, etc.). +* **types** we are currently in process of refactoring **pages** module, to + organize it in a way more compliant of [Abstract Data + Types](https://en.wikipedia.org/wiki/Abstract_data_type) paradigm. We are + approaching the process incrementally, rewriting one module each time, as + needed. + +## Utilities + +The main ones are: + +* **data** basic data structures and utility functions that could be added to + Clojure standard library. +* **math** some mathematic functions that could also be standard. +* **file_builder** functions to parse the content of a .penpot exported file + and build a File data structure from it. +* **logging** functions to generate traces for debugging and usage analysis. +* **text** an adapter layer over the [DraftJS editor](https://draftjs.org) that + we use to edit text shapes in workspace. +* **transit** functions to encode/decode Clojure objects into + [transit](https://github.com/cognitect/transit-clj), a format similar to JSON + but more powerful. +* **uuid** functions to generate [Universally Unique Identifiers + (UUID)](https://en.wikipedia.org/wiki/Universally_unique_identifier), used + over all Penpot models to have identifiers for objects that are practically + ensured to be unique, without having a central control. diff --git a/docs/technical-guide/developer/architecture/exporter.md b/docs/technical-guide/developer/architecture/exporter.md new file mode 100644 index 000000000..1846fceee --- /dev/null +++ b/docs/technical-guide/developer/architecture/exporter.md @@ -0,0 +1,68 @@ +--- +title: Exporter app +--- + +# Exporter app + +When exporting file contents to a file, we want the result to be exactly the +same as the user sees in screen. To achieve this, we use a headless browser +installed in the backend host, and controled via puppeteer automation. The +browser loads the frontend app from the static webserver, and executes it like +a normal user browser. It visits a special endpoint that renders one shape +inside a file. Then, if takes a screenshot if we are exporting to a bitmap +image, or extract the svg from the DOM if we want a vectorial export, and write +it to a file that the user can download. + +## Exporter structure + +Penpot exporter app code resides under exporter/src/app path in the main repository. + +@startuml Exporter +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml +!define DEVICONS https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/devicons +!include DEVICONS/react.puml +!include DEVICONS/clojure.puml +!include DEVICONS/chrome.puml + +HIDE_STEREOTYPE() + +Container(frontend_app, "Frontend app", "React / ClojureScript", "", "react") + +System_Boundary(backend, "Backend") { + Container(exporter, "Exporter", "ClojureScript / nodejs", "", "clojure") + Container(browser, "Headless browser", "Chrome", "", "chrome") +} + +Rel_D(frontend_app, exporter, "Uses", "HTTPS") +Rel_R(exporter, browser, "Uses", "puppeteer") +Rel_U(browser, frontend_app, "Uses", "HTTPS") + +@enduml + +```text + ▾ exporter/src/app/ + ▸ http/ + ▸ renderer/ + ▸ util/ + core.cljs + http.cljs + browser.cljs + config.cljs +``` + +## Exporter namespaces + +* **core** has the setup and run functions of the nodejs app. + +* **http** exposes a basic http server, with endpoints to export a shape or a + file. + +* **browser** has functions to control a local Chromium browser via + [puppeteer](https://puppeteer.github.io/puppeteer). + +* **renderer** has functions to tell the browser to render an object and make a + screenshot, and then convert it to bitmap, pdf or svg as needed. + +* **config** gets configuration settings from the linux environment. + +* **util** has some generic utility functions. diff --git a/docs/technical-guide/developer/architecture/frontend.md b/docs/technical-guide/developer/architecture/frontend.md new file mode 100644 index 000000000..8dc3178f9 --- /dev/null +++ b/docs/technical-guide/developer/architecture/frontend.md @@ -0,0 +1,258 @@ +--- +title: Frontend app +--- + +### Frontend app + +The main application, with the user interface and the presentation logic. + +To talk with backend, it uses a custom RPC-style API: some functions in the +backend are exposed through an HTTP server. When the front wants to execute a +query or data mutation, it sends a HTTP request, containing the name of the +function to execute, and the ascii-encoded arguments. The resulting data is +also encoded and returned. This way we don't need any data type conversion, +besides the transport encoding, as there is Clojure at both ends. + +When the user opens any file, a persistent websocket is opened with the backend +and associated to the file id. It is used to send presence events, such as +connection, disconnection and mouse movements. And also to receive changes made +by other users that are editing the same file, so it may be updated in real +time. + +## Frontend structure + +Penpot frontend app code resides under frontend/src/app path in the main repository. + +@startuml FrontendGeneral +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml +!define DEVICONS https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/devicons +!include DEVICONS/react.puml + +HIDE_STEREOTYPE() + +Person(user, "User") +System_Boundary(frontend, "Frontend") { + Container(frontend_app, "Frontend app", "React / ClojureScript", "", "react") + Container(worker, "Worker", "Web worker") +} + +Rel(user, frontend_app, "Uses", "HTTPS") +BiRel_L(frontend_app, worker, "Works with") + +@enduml + +```text + ▾ frontend/src/app/ + ▸ main/ + ▸ util/ + ▸ worker/ + main.cljs + worker.cljs +``` + +* main.cljs and main/ contain the main frontend app, written in + ClojureScript language and using React framework, wrapped in [rumext + library](https://github.com/funcool/rumext). +* worker.cljs and worker/ contain the web worker, to make expensive + calculations in background. +* util/ contains many generic utilities, non dependant on the user + interface. + +@startuml FrontendMain +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml + +HIDE_STEREOTYPE() + +Component(ui, "ui", "main web component") +Component(store, "store", "module") +Component(refs, "refs", "module") +Component(repo, "repo", "module") +Component(streams, "streams", "module") +Component(errors, "errors", "module") + +Boundary(ui_namespaces, "ui namespaces") { + Component(ui_auth, "auth", "web component") + Component(ui_settings, "settings", "web component") + Component(ui_dashboard, "dashboard", "web component") + Component(ui_workspace, "workspace", "web component") + Component(ui_viewer, "viewer", "web component") + Component(ui_render, "render", "web component") + Component(ui_exports, "exports", "web component") + Component(ui_shapes, "shapes", "component library") + Component(ui_components, "components", "component library") +} + +Boundary(data_namespaces, "data namespaces") { + Component(data_common, "common", "events") + Component(data_users, "users", "events") + Component(data_dashboard, "dashboard", "events") + Component(data_workspace, "workspace", "events") + Component(data_viewer, "viewer", "events") + Component(data_comments, "comments", "events") + Component(data_fonts, "fonts", "events") + Component(data_messages, "messages", "events") + Component(data_modal, "modal", "events") + Component(data_shortcuts, "shortcuts", "utilities") +} + +Lay_D(ui_exports, data_viewer) +Lay_D(ui_settings, ui_components) +Lay_D(data_viewer, data_common) +Lay_D(data_fonts, data_messages) +Lay_D(data_dashboard, data_modal) +Lay_D(data_workspace, data_shortcuts) +Lay_L(data_dashboard, data_fonts) +Lay_L(data_workspace, data_comments) + +Rel_Up(refs, store, "Watches") +Rel_Up(streams, store, "Watches") + +Rel(ui, ui_auth, "Routes") +Rel(ui, ui_settings, "Routes") +Rel(ui, ui_dashboard, "Routes") +Rel(ui, ui_workspace, "Routes") +Rel(ui, ui_viewer, "Routes") +Rel(ui, ui_render, "Routes") + +Rel(ui_render, ui_exports, "Uses") +Rel(ui_workspace, ui_shapes, "Uses") +Rel(ui_viewer, ui_shapes, "Uses") +Rel_Right(ui_exports, ui_shapes, "Uses") + +Rel(ui_auth, data_users, "Uses") +Rel(ui_settings, data_users, "Uses") +Rel(ui_dashboard, data_dashboard, "Uses") +Rel(ui_dashboard, data_fonts, "Uses") +Rel(ui_workspace, data_workspace, "Uses") +Rel(ui_workspace, data_comments, "Uses") +Rel(ui_viewer, data_viewer, "Uses") + +@enduml + +### General namespaces + +* **store** contains the global state of the application. Uses an event loop + paradigm, similar to Redux, with a global state object and a stream of events + that modify it. Made with [potok library](https://funcool.github.io/potok/latest/). + +* **refs** has the collection of references or lenses: RX streams that you can + use to subscribe to parts of the global state, and be notified when they + change. + +* **streams** has some streams, derived from the main event stream, for keyboard + and mouse events. Used mainly from the workspace viewport. + +* **repo** contains the functions to make calls to backend. + +* **errors** has functions with global error handlers, to manage exceptions or other + kinds of errors in the ui or the data events, notify the user in a useful way, + and allow to recover and continue working. + +### UI namespaces + +* **ui** is the root web component. It reads the current url and mounts the needed + subcomponent depending on the route. + +* **auth** has the web components for the login, register, password recover, + etc. screens. + +* **settings** has the web comonents for the user profile and settings screens. + +* **dashboard** has the web components for the dashboard and its subsections. + +* **workspace** has the web components for the file workspace and its subsections. + +* **viewer** has the web components for the viewer and its subsections. + +* **render** contain special web components to render one page or one specific + shape, to be used in exports. + +* **export** contain basic web components that display one shape or frame, to + be used from exports render or else from dashboard and viewer thumbnails and + other places. + +* **shapes** is the basic collection of web components that convert all types of + shapes in the corresponding svg elements, without adding any extra function. + +* **components** a library of generic UI widgets, to be used as building blocks + of penpot screens (text or numeric inputs, selects, forms, buttons...). + + +### Data namespaces + +* **users** has events to login and register, fetch the user profile and update it. + +* **dashboard** has events to fetch and modify teams, projects and files. + +* **fonts** has some extra events to manage uploaded fonts from dashboard. + +* **workspace** has a lot of events to manage the current file and do all kinds of + edits and updates. + +* **comments** has some extra events to manage design comments. + +* **viewer** has events to fetch a file contents to display, and manage the + interactive behavior and hand-off. + +* **common** has some events used from several places. + +* **modal** has some events to show modal popup windows. + +* **messages** has some events to show non-modal informative messages. + +* **shortcuts** has some utility functions, used in other modules to setup the + keyboard shortcuts. + + +## Worker app + +Some operations are costly to make in real time, so we leave them to be +executed asynchronously in a web worker. This way they don't impact the user +experience. Some of these operations are generating file thumbnails for the +dashboard and maintaining some geometric indexes to speed up snap points while +drawing. + +@startuml FrontendWorker +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml + +HIDE_STEREOTYPE() + +Component(worker, "worker", "worker entry point") + +Boundary(worker_namespaces, "worker namespaces") { + Component(thumbnails, "thumbnails", "worker methods") + Component(snaps, "snaps", "worker methods") + Component(selection, "selection", "worker methods") + Component(impl, "impl", "worker methods") + Component(import, "import", "worker methods") + Component(export, "export", "worker methods") +} + +Rel(worker, thumbnails, "Uses") +Rel(worker, impl, "Uses") +Rel(worker, import, "Uses") +Rel(worker, export, "Uses") +Rel(impl, snaps, "Uses") +Rel(impl, selection, "Uses") + +@enduml + +* **worker** contains the worker setup code and the global handler that receives + requests from the main app, and process them. + +* **thumbnails** has a method to generate the file thumbnails used in dashboard. + +* **snaps** manages a distance index of shapes, and has a method to get + other shapes near a given one, to be used in snaps while drawing. + +* **selection** manages a geometric index of shapes, with methods to get what + shapes are under the cursor at a given moment, for select. + +* **impl** has a simple method to update all indexes in a page at once. + +* **import** has a method to import a whole file from an external .penpot archive. + +* **export** has a method to export a whole file to an external .penpot archive. + diff --git a/docs/technical-guide/developer/architecture/index.md b/docs/technical-guide/developer/architecture/index.md new file mode 100644 index 000000000..b4920cb55 --- /dev/null +++ b/docs/technical-guide/developer/architecture/index.md @@ -0,0 +1,65 @@ +--- +title: 3.1. Architecture +--- + +# Architecture + +This section gives an overall structure of the system. + +Penpot has the architecture of a typical SPA. There is a frontend application, +written in ClojureScript and using React framework, and served from a static +web server. It talks to a backend application, that persists data on a +PostgreSQL database. + +The backend is written in Clojure, so front and back can share code and data +structures without problem. Then, the code is compiled into JVM bytecode and +run in a JVM environment. + +There are some additional components, explained in subsections. + +@startuml C4_Elements +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml +!define DEVICONS https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/devicons +!include DEVICONS/react.puml +!include DEVICONS/java.puml +!include DEVICONS/clojure.puml +!include DEVICONS/postgresql.puml +!include DEVICONS/redis.puml +!include DEVICONS/chrome.puml + +HIDE_STEREOTYPE() + +Person(user, "User") +System_Boundary(frontend, "Frontend") { + Container(frontend_app, "Frontend app", "React / ClojureScript", "", "react") + Container(worker, "Worker", "Web worker") +} + +System_Boundary(backend, "Backend") { + Container(backend_app, "Backend app", "Clojure / JVM", "", "clojure") + ContainerDb(db, "Database", "PostgreSQL", "", "postgresql") + ContainerDb(redis, "Broker", "Redis", "", "redis") + Container(exporter, "Exporter", "ClojureScript / nodejs", "", "clojure") + Container(browser, "Headless browser", "Chrome", "", "chrome") +} + +Rel(user, frontend_app, "Uses", "HTTPS") +BiRel_L(frontend_app, worker, "Works with") +BiRel(frontend_app, backend_app, "Open", "websocket") +Rel(frontend_app, backend_app, "Uses", "RPC API") +Rel(backend_app, db, "Uses", "SQL") +Rel(redis, backend_app, "Subscribes", "pub/sub") +Rel(backend_app, redis, "Notifies", "pub/sub") +Rel(frontend_app, exporter, "Uses", "HTTPS") +Rel(exporter, browser, "Uses", "puppeteer") +Rel(browser, frontend_app, "Uses", "HTTPS") + +@enduml + +See more at + + * [Frontend app](/technical-guide/developer/architecture/frontend/) + * [Backend app](/technical-guide/developer/architecture/backend/) + * [Exporter app](/technical-guide/developer/architecture/exporter/) + * [Common code](/technical-guide/developer/architecture/common/) + diff --git a/docs/technical-guide/developer/backend.md b/docs/technical-guide/developer/backend.md new file mode 100644 index 000000000..672b71879 --- /dev/null +++ b/docs/technical-guide/developer/backend.md @@ -0,0 +1,111 @@ +--- +title: 3.6. Backend Guide +--- + +# Backend guide # + +This guide intends to explain the essential details of the backend +application. + + +## REPL ## + +In the devenv environment you can execute scripts/repl to open a +Clojure interactive shell ([REPL](https://codewith.mu/en/tutorials/1.0/repl)). + +Once there, you can execute (restart) to load and execute the backend +process, or to reload it after making changes to the source code. + +Then you have access to all backend code. You can import and use any function +o read any global variable: + +```clojure +(require '[app.some.namespace :as some]) +(some/your-function arg1 arg2) +``` + +There is a specific namespace app.srepl with some functions useful to be +executed from the repl and perform some tasks manually. Most of them accept +a system parameter. There is a global variable with this name, that contains +the runtime information and configuration needed for the functions to run. + +For example: + +```clojure +(require '[app.srepl.main :as srepl]) +(srepl/send-test-email! system "test@example.com") +``` + + +## Fixtures ## + +This is a development feature that allows populate the database with a +good amount of content (usually used for just test the application or +perform performance tweaks on queries). + +In order to load fixtures, enter to the REPL environment with the scripts/repl +script, and then execute (app.cli.fixtures/run {:preset :small}). + +You also can execute this as a standalone script with: + +```bash +clojure -Adev -X:fn-fixtures +``` + +NOTE: It is an optional step because the application can start with an +empty database. + +This by default will create a bunch of users that can be used to login +in the application. All users uses the following pattern: + +- Username: profileN@example.com +- Password: 123123 + +Where N is a number from 0 to 5 on the default fixture parameters. + + +## Migrations ## + +The database migrations are located in two directories: + +- src/app/migrations (contains migration scripts in clojure) +- src/app/migrations/sql (contains the pure SQL migrations) + +The SQL migration naming consists in the following: + +```bash +XXXX--- +``` + +Examples: + +```bash +0025-del-generic-tokens-table +0026-mod-profile-table-add-is-active-field +``` + +**NOTE**: if table name has more than one word, 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 +``` + +## Linter ## + +There are no watch process for the linter; you will need to execute it +manually. We use [clj-kondo][kondo] for linting purposes and the +repository already comes with base configuration. + +[kondo]: https://github.com/clj-kondo/clj-kondo + +You can run **clj-kondo** as-is (is included in the devenv image): + +```bash +cd penpot/backend; +clj-kondo --lint src +``` + diff --git a/docs/technical-guide/developer/common.md b/docs/technical-guide/developer/common.md new file mode 100644 index 000000000..0b8dfc6f1 --- /dev/null +++ b/docs/technical-guide/developer/common.md @@ -0,0 +1,494 @@ +--- +title: 3.4. Common Guide +--- + +# Common guide + +This section has articles related to all submodules (frontend, backend and +exporter) such as: code style hints, architecture decisions, etc... + + +## Configuration + +Both in the backend, the frontend and the exporter subsystems, there are an +app.config namespace that defines the global configuration variables, +their specs and the default values. + +All variables have a conservative default, meaning that you can set up a Penpot +instance without changing any configuration, and it will be reasonably safe +and useful. + +In backend and exporter, to change the runtime values you need to set them in +the process environment, following the rule that an environment variable in the +form PENPOT_ correspond to a configuration +variable named variable-name-in-lowercase. Example: + +```bash +(env) +PENPOT_ASSETS_STORAGE_BACKEND=assets-s3 + +(config) +assets-storage-backend :assets-s3 +``` + +In frontend, the main resources/public/index.html file includes (if it +exists) a file named js/config.js, where you can set configuration values +as javascript global variables. The file is not created by default, so if +you need it you must create it blank, and set the variables you want, in +the form penpot\: + +```js +(js/config.js) +var penpotPublicURI = "https://penpot.example.com"; + +(config) +public-uri "https://penpot.example.com" +``` + +### On premise instances + +If you use the official Penpot docker images, as explained in the [Getting +Started](/technical-guide/getting-started/#start-penpot) section, there is a +[config.env](https://github.com/penpot/penpot/blob/develop/docker/images/config.env) +file that sets the configuration environment variables. It's the same file for +backend, exporter and frontend. + +For this last one, there is a script +[nginx-entrypoint.sh](https://github.com/penpot/penpot/blob/develop/docker/images/files/nginx-entrypoint.sh) +that reads the environment and generates the js/config.js when the container +is started. This way all configuration is made in the single config.env file. + + +### Dev environment + +If you use the [developer docker images](/technical-guide/developer/devenv/), +the [docker-compose.yaml](https://github.com/penpot/penpot/blob/develop/docker/devenv/docker-compose.yaml) +directly sets the environment variables more appropriate for backend and +exporter development. + +Additionally, the backend [start script](https://github.com/penpot/penpot/blob/develop/backend/scripts/start-dev) +and [repl script](https://github.com/penpot/penpot/blob/develop/backend/scripts/repl) set +some more variables. + +The frontend uses only the defaults. + +If you want to change any variable for your local environment, you can change +docker-compose.yaml and shut down and start again the container. Or you can +modify the start script or directly set the environment variable in your +session, and restart backend or exporter processes. + +For frontend, you can manually create resources/public/js/config.js (it's +ignored in git) and define your settings there. Then, just reload the page. + +## System logging + +In [app.common.logging](https://github.com/penpot/penpot/blob/develop/common/src/app/common/logging.cljc) +we have a general system logging utility, that may be used throughout all our +code to generate execution traces, mainly for debugging. + +You can add a trace anywhere, specifying the log level (trace, debug, +info, warn, error) and any number of key-values: + +```clojure +(ns app.main.data.workspace.libraries-helpers + (:require [app.common.logging :as log])) + +(log/set-level! :warn) + +... + +(defn generate-detach-instance + [changes container shape-id] + (log/debug :msg "Detach instance" + :shape-id shape-id + :container (:id container)) + ...) +``` + +The current namespace is tracked within the log message, and you can configure +at runtime, by namespace, the log level (by default :warn). Any trace below +this level will be ignored. + +Some keys have a special meaning: + * :msg is the main trace message. + * ::log/raw outputs the value without any processing or prettifying. + * ::log/context append metadata to the trace (not printed, it's to be + processed by other tools). + * ::log/cause (only in backend) attach a java exception object that will + be printed in a readable way with the stack trace. + * ::log/async (only in backend) if set to false, makes the log processing + synchronous. If true (the default), it's executed in a separate thread. + * :js/\ (only in frontend) if you prefix the key with the js/ + namespace, the value will be printed as a javascript interactively + inspectionable object. + * :err (only in frontend) attach a javascript exception object, and it + will be printed in a readable way with the stack trace. + +### backend + +The logging utility uses a different library for Clojure and Clojurescript. In +the first case we use [log4j2](https://logging.apache.org/log4j/2.x) to have +much flexibility. + +The configuration is made in [log4j2.xml](https://github.com/penpot/penpot/blob/develop/backend/resources/log4j2.xml) +file. The Logger used for this is named "app" (there are other loggers for +other subsystems). The default configuration just outputs all traces of level +debug or higher to the console standard output. + +There is a different [log4j2-devenv](https://github.com/penpot/penpot/blob/develop/backend/resources/log4j2-devenv.xml) +for the development environment. This one outputs traces of level trace or +higher to a file, and debug or higher to a zmq queue, that may be +subscribed for other parts of the application for further processing. + +The ouput for a trace in logs/main.log uses the format + +```bash +[] : - ... +``` + +Example: + +```bash +[2022-04-27 06:59:08.820] T app.rpc - action="register", name="update-file" +``` + +The zmq queue is not used in the default on premise or devenv setups, but there +are a number of handlers you can use in custom instances to save errors in the +database, or send them to a [Sentry](https://sentry.io/welcome/) or similar +service, for example. + +### frontend and exporter + +In the Clojurescript subservices, we use [goog.log](https://google.github.io/closure-library/api/goog.log.html) +library. This is much simpler, and basically outputs the traces to the console +standard output (the devtools in the browser or the console in the nodejs +exporter). + +In the browser, we have an utility [debug function](/technical-guide/developer/frontend/#console-debug-utility) +that enables you to change the logging level of any namespace (or of the whole +app) in a live environment: + +```javascript +debug.set_logging("namespace", "level") +``` + +## Assertions + +Penpot source code has this types of assertions: + +### **assert** + +Just using the clojure builtin `assert` macro. + +Example: + +```clojure +(assert (number? 3) "optional message") +``` + +This asserts are only executed in development mode. In production +environment all asserts like this will be ignored by runtime. + +### **spec/assert** + +Using the app.common.spec/assert macro. + +This macro is based in cojure.spec.alpha/assert macro, and it's +also ignored in a production environment. + +The Penpot variant doesn't have any runtime checks to know if asserts +are disabled. Instead, the assert calls are completely removed by the +compiler/runtime, thus generating simpler and faster code in production +builds. + +Example: + +```clojure +(require '[clojure.spec.alpha :as s] + '[app.common.spec :as us]) + +(s/def ::number number?) + +(us/assert ::number 3) +``` + +### **spec/verify** + +An assertion type that is always executed. + +Example: + +```clojure +(require '[app.common.spec :as us]) + +(us/verify ::number 3) +``` + +This macro enables you to have assertions on production code, that +generate runtime exceptions when failed (make sure you handle them +appropriately). + +## Unit tests + +We expect all Penpot code (either in frontend, backend or common subsystems) to +have unit tests, i.e. the ones that test a single unit of code, in isolation +from other blocks. Currently we are quite far from that objective, but we are +working to improve this. + +### Running tests with kaocha + +Unit tests are executed inside the [development environment](/technical-guide/developer/devenv). + +We can use [kaocha test runner](https://cljdoc.org/d/lambdaisland/kaocha/), and +we have prepared, for convenience, some aliases in deps.edn files. To run +them, just go to backend, frontend or common and execute: + +```bash +# To run all tests once +clojure -M:dev:test + +# To run all tests and keep watching for changes +clojure -M:dev:test --watch + +# To run a single tests module +clojure -M:dev:test --focus common-tests.logic.comp-sync-test + +# To run a single test +clojure -M:dev:test --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute +``` + +Watch mode runs all tests when some file changes, except if some tests failed +previously. In this case it only runs the failed tests. When they pass, then +runs all of them again. + +You can also mark tests in the code by adding metadata: + +```clojure +;; To skip a test, for example when is not working or too slow +(deftest ^:kaocha/skip bad-test + (is (= 2 1))) + +;; To skip it but warn you during test run, so you don't forget it +(deftest ^:kaocha/pending bad-test + (is (= 2 1))) +``` + +Please refer to the [kaocha manual](https://cljdoc.org/d/lambdaisland/kaocha/1.91.1392/doc/6-focusing-and-skipping) +for how to define custom metadata and other ways of selecting tests. + +**NOTE**: in frontend we still can't use kaocha to run the tests. We are on +it, but for now we use shadow-cljs with package.json scripts: + +```bash +yarn run test +yarn run test:watch +``` + +#### Test output + +The default kaocha reporter outputs a summary for the test run. There is a pair +of brackets [ ] for each suite, a pair of parentheses ( ) for each test, +and a dot . for each assertion t/is inside tests. + +```bash +penpot@c261c95d4623:~/penpot/common$ clojure -M:dev:test +[(...)(............................................................ +.............................)(.................................... +..)(..........)(.................................)(.)(............. +.......................................................)(.......... +.....)(......)(.)(......)(......................................... +..............................................)(............)] +190 tests, 3434 assertions, 0 failures. +``` + +All standard output from the tests is captured and hidden, except if some test +fails. In this case, the output for the failing test is shown in a box: + +```bash +FAIL in sample-test/stdout-fail-test (sample_test.clj:10) +Expected: + :same +Actual: + -:same +:not-same +╭───── Test output ─────────────────────────────────────────────────────── +│ Can you see this? +╰───────────────────────────────────────────────────────────────────────── +2 tests, 2 assertions, 1 failures. +``` + +You can bypass the capture with the command line: + +```bash +clojure -M:dev:test --no-capture-output +``` + +Or for some specific output: + +```clojure +(ns sample-test + (:require [clojure.test :refer :all] + [kaocha.plugin.capture-output :as capture])) + +(deftest stdout-pass-test + (capture/bypass + (println "This message should be displayed")) + (is (= :same :same))) +``` + +### Running tests in the REPL + +An alternative way of running tests is to do it from inside the +[REPL](/technical-guide/developer/backend/#repl) you can use in the backend and +common apps in the development environment. + +We have a helper function (run-tests) that refreshes the environment (to avoid +having [stale tests](https://practical.li/clojure/testing/unit-testing/#command-line-test-runners)) +and runs all tests or a selection. It is defined in backend/dev/user.clj and +common/dev/user.clj, so it's available without importing anything. + +First start a REPL: + +```bash +~/penpot/backend$ scripts/repl +``` + +And then: + +```clojure +;; To run all tests +(run-tests) + +;; To run all tests in one namespace +(run-tests 'some.namespace) + +;; To run a single test +(run-tests 'some.namespace/some-test) + +;; To run all tests in one or several namespaces, +;; selected by a regular expression +(run-tests #"^backend-tests.rpc.*") +``` + +### Writing unit tests + +We write tests using the standard [Clojure test +API](https://clojure.github.io/clojure/clojure.test-api.html). You can find a +[guide to writing unit tests](https://practical.li/clojure/testing/unit-testing) at Practicalli +Clojure, that we follow as much as possible. + +#### Sample files helpers + +An important issue when writing tests in Penpot is to have files with the +specific configurations we need to test. For this, we have defined a namespace +of helpers to easily create files and its elements with sample data. + +To make handling of uuids more convenient, those functions have a uuid +registry. Whenever you create an object, you may give a :label, and the id of +the object will be stored in the registry associated with this label, so you +can easily recover it later. + +You have functions to create files, pages and shapes, to connect them and +specify their attributes, having all of them default values if not set. + +Files also store in metadata the **current page**, so you can control in what +page the add- and get- functions will operate. + +```clojure +(ns common-tests.sample-helpers-test + (:require + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [clojure.test :as t])) + +(t/deftest test-create-file + (let [;; Create a file with one page + f1 (thf/sample-file :file1) + + ;; Same but define the label of the page, to retrieve it later + f2 (thf/sample-file :file2 :page-label :page1) + + ;; Set the :name attribute of the created file + f3 (thf/sample-file :file3 :name "testing file") + + ;; Create an isolated page + p2 (thf/sample-page :page2 :name "testing page") + + ;; Create a second page and add to the file + f4 (-> (thf/sample-file :file4 :page-label :page3) + (thf/add-sample-page :page4 :name "other testing page")) + + ;; Create an isolated shape + p2 (thf/sample-shape :shape1 :type :rect :name "testing shape") + + ;; Add a couple of shapes to a previous file, in different pages + f5 (-> f4 + (ths/add-sample-shape :shape2) + (thf/switch-to-page :page4) + (ths/add-sample-shape :shape3 :name "other testing shape" + :width 100)) + + ;; Retrieve created shapes + s1 (ths/get-shape f4 :shape1) + s2 (ths/get-shape f5 :shape2 :page-label :page3) + s3 (ths/get-shape f5 :shape3)] + + ;; Check some values + (t/is (= (:name f1) "Test file")) + (t/is (= (:name f3) "testing file")) + (t/is (= (:id f2) (thi/id :file2))) + (t/is (= (:id (thf/current-page f2)) (thi/id :page1))) + (t/is (= (:id s1) (thi/id :shape1))) + (t/is (= (:name s1) "Rectangle")) + (t/is (= (:name s3) "testing shape")) + (t/is (= (:width s3) 100)) + (t/is (= (:width (:selrect s3)) 100)))) +``` + +Also there are functions to make some transformations, like creating a +component, instantiating it or swapping a copy. + +```clojure +(ns app.common-tests.sample-components-test + (:require + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.shapes :as ths])) + +(t/deftest test-create-component + (let [;; Create a file with one component + f1 (-> (thf/sample-file :file1) + (ths/add-sample-shape :frame1 :type :frame) + (ths/add-sample-shape :rect1 :type :rect + :parent-label :frame1) + (thc/make-component :component1 :frame1))])) +``` + +Finally, there are composition helpers, to build typical structures with a +single line of code. And the files module has some functions to display the +contents of a file, in a way similar to `debug/dump-tree` but showing labels +instead of ids: + +```clojure +(ns app.common-tests.sample-compositions-test + (:require + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf])) + +(t/deftest test-create-composition + (let [f1 (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root))] + (ctf/dump-file f1 :show-refs? true))) + +;; {:main-root} [:name Frame1] # [Component :component1] +;; :main-child [:name Rect1] +;; +;; :copy-root [:name Frame1] #--> [Component :component1] :main-root +;; [:name Rect1] ---> :main-child +``` + +You can see more examples of usage by looking at the existing unit tests. + diff --git a/docs/technical-guide/developer/data-guide.md b/docs/technical-guide/developer/data-guide.md new file mode 100644 index 000000000..96922a671 --- /dev/null +++ b/docs/technical-guide/developer/data-guide.md @@ -0,0 +1,441 @@ +--- +title: 3.7. Data Guide +--- + +# Data Guide + +The data structures are one of the most complex and important parts of Penpot. +It's critical that the data integrity is always maintained throughout the whole +usage, and also file exports & imports and data model evolution. + +To modify the data structure (the most typical case will be to add a new attribute +to the shapes), this list must be checked. This is not an exhaustive list, but +all of this is important in general. + +## General considerations + +* We prefer that the page and shape attributes are optional. I.E. there is a + default object behavior, that occurs when the attribute is not present, and + its presence activates some feature (example: if there is no fill-color, + the shape is not filled). When you revert to the default state, it's better + to remove the attribute than leaving it with null value. There are some + process (for example import & export) that filter out and remove all + attributes that are null. + +* So never expect that attribute with null value is a different state that + without the attribute. + +* In objects attribute names we don't use special symbols that are allowed by + Clojure (for example ending it with ? for boolean values), because this may + cause problems when exporting. + +## Code organization in abstraction levels + +Initially, Penpot data model implementation was organized in a different way. +We are currently in a process of reorganization. The objective is to have data +manipulation code structured in abstraction layers, with well-defined +boundaries. + +At this moment the namespace structure is already organized as described here, +but there is much code that does not comply with these rules, and needs to be +moved or refactored. We expect to be refactoring existing modules incrementally, +each time we do an important functionality change. + +### Abstract data types + +```text +▾ common/ + ▾ src/app/common/ + ▾ types/ + file.cljc + page.cljc + shape.cljc + color.cljc + component.cljc + ... +``` + +Namespaces here represent a single data structure, or a fragment of one, as an +abstract data type. Each structure has: + +* A **schema spec** that defines the structure of the type and its values: + + ```clojure + (sm/define! ::fill + [:map {:title "Fill"} + [:fill-color {:optional true} ::ctc/rgb-color] + [:fill-opacity {:optional true} ::sm/safe-number] + ...) + + (sm/define! ::shape-attrs + [:map {:title "ShapeAttrs"} + [:name {:optional true} :string] + [:selrect {:optional true} ::grc/rect] + [:points {:optional true} ::points] + [:blocked {:optional true} :boolean] + [:fills {:optional true} + [:vector {:gen/max 2} ::fill]] + ...) + ``` + +* **Helper functions** to create, query and manipulate the structure. Helpers + at this level only are allowed to see the internal attributes of a type. + Updaters receive an object of the type, and return a new object modified, + also ensuring the internal integrity of the data after the change. + + ```clojure + (defn setup-shape + "A function that initializes the geometric data of the shape. The props must + contain at least :x :y :width :height." + [{:keys [type] :as props}] + ...) + + (defn has-direction? + [interaction] + (#{:slide :push} (-> interaction :animation :animation-type))) + + (defn set-direction + [interaction direction] + (dm/assert! + "expected valid interaction map" + (check-interaction! interaction)) + (dm/assert! + "expected valid direction" + (contains? direction-types direction)) + (dm/assert! + "expected compatible interaction map" + (has-direction? interaction)) + (update interaction :animation assoc :direction direction)) + ``` + +> IMPORTANT: we should always use helper functions to access and modify these data +> structures. Avoid direct attribute read or using functions like assoc or +> update, even if the information is contained in a single attribute. This way +> it will be much simpler to add validation checks or to modify the internal +> representation of a type, and will be easier to search for places in the code +> where this data item is used. +> +> Currently much of Penpot code does not follow this requirement, but it +should do in new code or in any refactor. + +### File operations + +```text +▾ common/ + ▾ src/app/common/ + ▾ files/ + helpers.cljc + shapes_helpers.cljc + ... +``` + +Functions that modify a file object (or a part of it) in place, returning the +file object changed. They ensure the referential integrity within the file, or +between a file and its libraries. + +```clojure +(defn resolve-component + "Retrieve the referenced component, from the local file or from a library" + [shape file libraries & {:keys [include-deleted?] :or {include-deleted? False}}] + (if (= (:component-file shape) (:id file)) + (ctkl/get-component (:data file) (:component-id shape) include-deleted?) + (get-component libraries + (:component-file shape) + (:component-id shape) + :include-deleted? include-deleted?))) + +(defn delete-component +"Mark a component as deleted and store the main instance shapes inside it, to +be able to be recovered later." +[file-data component-id skip-undelete? Main-instance] +(let [components-v2 (dm/get-in file-data [:options :components-v2])] + (if (or (not components-v2) skip-undelete?) + (ctkl/delete-component file-data component-id) + (let [set-main-instance ;; If there is a saved main-instance, restore it. + #(if main-instance + (assoc-in % [:objects (:main-instance-id %)] main-instance) + %)] + (-> file-data + (ctkl/update-component component-id load-component-objects) + (ctkl/update-component component-id set-main-instance) + (ctkl/mark-component-deleted component-id)))))) +``` + +> This module is still needing an important refactor. Mainly to take functions +> from common.types and move them here. + +#### File validation and repair + +There is a function in app.common.files.validate that checks a file for +referential and semantic integrity. It's called automatically when file changes +are sent to backend, but may be invoked manually whenever it's needed. + +### File changes objects + +```text +▾ common/ + ▾ src/app/common/ + ▾ files/ + changes_builder.cljc + changes.cljc + ... +``` + +Wrap the update functions in file operations module into changes objects, that +may be serialized, stored, sent to backend and executed to actually modify a file +object. They should not contain business logic or algorithms. Only adapt the +interface to the file operations or types. + +```clojure +(sm/define! ::changes + [:map {:title "changes"} + [:redo-changes vector?] + [:undo-changes seq?] + [:origin {:optional true} any?] + [:save-undo? {:optional true} boolean?] + [:stack-undo? {:optional true} boolean?] + [:undo-group {:optional true} any?]]) + +(defmethod process-change :add-component + [file-data params] + (ctkl/add-component file-data params)) +``` + +### Business logic + +```text +▾ common/ + ▾ src/app/common/ + ▾ logic/ + shapes.cljc + libraries.cljc +``` + +Functions that implement semantic user actions, in an abstract way (independent +of UI). They don't directly modify files, but generate changes objects, that +may be executed in frontend or sent to backend. + +```clojure +(defn generate-instantiate-component +"Generate changes to create a new instance from a component." +[changes objects file-id component-id position page libraries old-id parent-id + frame-id {:keys [force-frame?] :or {force-frame? False}}] + (let [component (ctf/get-component libraries file-id component-id) + parent (when parent-id (get objects parent-id)) + library (get libraries file-id) + components-v2 (dm/get-in library [:data :options :components-v2]) + [new-shape new-shapes]º + (ctn/make-component-instance page + Component + (:data library) + Position + Components-v2 + (cond-> {} + force-frame? (assoc :force-frame-id frame-id))) + changes (cond-> (pcb/add-object changes first-shape {:ignore-touched true}) + (some? old-id) (pcb/amend-last-change #(assoc % :old-id old-id))) + changes (reduce #(pcb/add-object %1 %2 {:ignore-touched true}) + changes + (rest new-shapes))] +[new-shape changes])) +``` + +## Data migrations + +```text +▾ common/ + ▾ src/app/common/ + ▾ files/ + migrations.cljc +``` + +When changing the model it's essential to take into account that the existing +Penpot files must keep working without changes. If you follow the general +considerations stated above, usually this is automatic, since the objects +already in the database just have the default behavior, that should be the same +as before the change. And the new features apply to new or edited objects. + +But if this is not possible, and we are talking of a breaking change, you can +write a data migration. Just define a new data version and a migration script +in migrations.cljc and increment file-version in common.cljc. + +From then on, every time a file is loaded from the database, if its version +number is lower than the current version in the app, the file data will be +handled to all the needed migration functions. If you later modify and save +the file, it will be now updated in database. + +## Shape edit forms + +![Sidebar edit form](/img/sidebar-form.png) + +```text +▾ frontend/ + ▾ src/ + ▾ app/ + ▾ main/ + ▾ ui/ + ▾ workspace/ + ▾ sidebar/ + ▾ options/ + ▸ menus/ + ▸ rows/ + ▾ shapes/ + bool.cljs + circle.cljs + frame.cljs + group.cljs + image.cljs + multiple.cljs + path.cljs + rect.cljs + svg_raw.cljs + text.cljs +``` + +* In shapes/*.cljs there are the components that show the edit menu of each + shape type. + +* In menus/*.cljs there are the building blocks of these menus. + +* And in rows/*.cljs there are some pieces, for example color input and + picker. + +## Multiple edit + +When modifying the shape edit forms, you must take into account that these +forms may edit several shapes at the same time, even of different types. + +When more than one shape is selected, the form inside multiple.cljs is used. +At the top of this module, a couple of maps define what attributes may be edited +and how, for each type of shape. + +Then, the blocks in menus/*.cljs are used, but they are not given a shape, but +a values map. For each attribute, if all shapes have the same value, it is taken; +if not, the attribute will have the value :multiple. + +The form blocks must be prepared for this value, display something useful to +the user in this case, and do a meaningful action when changing the value. +Usually this will be to set the attribute to a fixed value in all selected +shapes, but **only** those that may have the attribute (for example, only text +shapes have font attributes, or only rects has border radius). + +## Component synchronization + +```text +▾ common/ + ▾ src/app/common/ + ▾ types/ + component.cljc +``` + +For all shape attributes, you must take into account what happens when the +attribute in a main component is changed and then the copies are synchronized. + +In component.cljc there is a structure sync-attrs that maps shape +attributes to sync groups. When an attribute is changed in a main component, +the change will be propagated to its copies. If the change occurs in a copy, +the group will be marked as *touched* in the copy shape, and from then on, +further changes in the main to this attribute, or others in the same group, +will not be propagated. + +Any attribute that is not in this map will be ignored in synchronizations. + +## Render shapes, export & import + +```text +▾ frontend/ + ▾ src/ + ▾ app/ + ▾ main/ + ▾ ui/ + ▾ shapes/ + ▸ text/ + attrs.cljs + bool.cljs + circle.cljs + custom_stroke.cljs + embed.cljs + export.cljs + fill_image.cljs + filters.cljs + frame.cljs + gradients.cljs + group.cljs + image.cljs + mask.cljs + path.cljs + rect.cljs + shape.cljs + svg_defs.cljs + svg_raw.cljs + text.cljs + ▾ worker/ + ▾ import/ + parser.cljs +``` + +To export a penpot file, basically we use the same system that is used to +display shapes in the workspace or viewer. In shapes/*.cljs there are +components that render one shape of each type into a SVG node. + +But to be able to import the file later, some attributes that not match +directly to SVG properties need to be added as metadata (for example, +proportion locks, constraints, stroke alignment...). This is done in the +export.cljs module. + +Finally, to import a file, we make use of parser.cljs, a module that +contains the parse-data function. It receives a SVG node (possibly with +children) and converts it into a Penpot shape object. There are auxiliary +functions to read and convert each group of attributes, from the node +properties or the metadata (with the get-meta function). + +Any attribute that is not included in the export and import functions +will not be exported and will be lost if reimporting the file again. + +## Code generation + +```text +▾ frontend/ + ▾ src/ + ▾ app/ + ▾ main/ + ▾ ui/ + ▾ viewer/ + ▾ inspect/ + ▾ attributes/ + blur.cljs + common.cljs + fill.cljs + image.cljs + layout.cljs + shadow.cljs + stroke.cljs + svg.cljs + text.cljs + attributes.cljs + code.cljs + ▾ util/ + code_gen.cljs + markup_html.cljs + markup_svg.cljs + style_css.cljs + style_css_formats.cljs + style_css_values.cljs +``` + +In the inspect panel we have two modes: + +![Inspect info](/img/handoff-info.png) + +For the Info tab, the attributes.cljs module and all modules under +attributes/*.cljs have the components that extract the attributes to inspect +each type of shape. + +![Inspect code](/img/handoff-code.png) + +For the Code tab, the util/code_gen.cljs module is in charge. It calls the +other modules in util/ depending on the format. + +For HTML and CSS, there are functions that generate the code as needed from the +shapes. For SVG, it simply takes the nodes from the viewer main viewport and +prettily formats it. diff --git a/docs/technical-guide/developer/data-model/index.md b/docs/technical-guide/developer/data-model/index.md new file mode 100644 index 000000000..8981d6957 --- /dev/null +++ b/docs/technical-guide/developer/data-model/index.md @@ -0,0 +1,199 @@ +--- +title: 3.2. Data model +--- + +# Penpot Data Model + +This is the conceptual data model. The actual representations of those entities +slightly differ, depending on the environment (frontend app, backend RPC calls +or the SQL database, for example). But the concepts are always the same. + +The diagrams use [basic UML notation with PlantUML](https://plantuml.com/en/class-diagram). + +## Users, teams and projects + +@startuml TeamModel + +hide members + +class Profile +class Team +class Project +class File +class StorageObject +class CommentThread +class Comment +class ShareLink + +Profile "*" -right- "*" Team +Team *--> "*" Project +Profile "*" -- "*" Project +Project *--> "*" File +Profile "*" -- "*" File +File "*" <-- "*" File : libraries +File *--> "*" StorageObject : media_objects +File *--> "*" CommentThread : comment_threads +CommentThread *--> "*" Comment +File *--> "*" ShareLink : share_links + +@enduml + +A Profile holds the personal info of any user of the system. Users belongs to +Teams and may create Projects inside them. + +Inside the projects, there are Files. All users of a team may see the projects +and files inside the team. Also, any project and file has at least one user that +is the owner, but may have more relationships with users with other roles. + +Files may use other files as shared libraries. + +The main content of the file is in the "file data" attribute (see next section). +But there are some objects that reside in separate entities: + + * A StorageObject represents a file in an external storage, that is embedded + into a file (currently images and SVG icons, but we may add other media + types in the future). + + * CommentThreadsand Comments are the comments that any user may add to a + file. + + * A ShareLink contains a token, an URL and some permissions to share the file + with external users. + +## File data + +@startuml FileModel + +hide members + +class File +class Page +class Component +class Color +class MediaItem +class Typography + +File *--> "*" Page : pages +(File, Page) .. PagesList + +File *--> "*" Component : components +(File, Component) .. ComponentsList + +File *--> "*" Color : colors +(File, Color) .. ColorsList + +File *--> "*" MediaItem : colors +(File, MediaItem) .. MediaItemsList + +File *--> "*" Typography : colors +(File, Typography) .. TypographiesList + +@enduml + +The data attribute contains the Pages and the library assets in the file +(Components, MediaItems, Colors and Typographies). + +The lists of pages and assets are modelled also as entities because they have a +lot of functions and business logic. + +## Pages and components + +@startuml PageModel + +hide members + +class Container +class Page +class Component +class Shape + +Container <|-left- Page +Container <|-right- Component + +Container *--> "*" Shape : objects +(Container, Shape) .. ShapeTree + +Shape <-- Shape : parent + +@enduml + +Both Pages and Components contains a tree of shapes, and share many +functions and logic. So, we have modelled a Container entity, that is an +abstraction that represents both a page or a component, to use it whenever we +have code that fits the two. + +A ShapeTree represents a set of shapes that are hierarchically related: the top +frame contains top-level shapes (frames and other shapes). Frames and groups may +contain any non frame shape. + +## Shapes + +@startuml ShapeModel + +hide members + +class Shape +class Selrect +class Transform +class Constraints +class Interactions +class Fill +class Stroke +class Shadow +class Blur +class Font +class Content +class Exports + +Shape o--> Selrect +Shape o--> Transform +Shape o--> Constraints +Shape o--> Interactions +Shape o--> Fill +Shape o--> Stroke +Shape o--> Shadow +Shape o--> Blur +Shape o--> Font +Shape o--> Content +Shape o--> Exports + +Shape <-- Shape : parent + +@enduml + +A Shape is the most important entity of the model. Represents one of the +[layers of our design](https://help.penpot.app/user-guide/layer-basics), and it +corresponds with one SVG node, augmented with Penpot special features. + +We have code to render a Shape into a SVG tag, with more or less additions +depending on the environment (editable in the workspace, interactive in the +viewer, minimal in the shape exporter or the handoff, or with metadata in the +file export). + +Also have code that imports any SVG file and convert elements back into shapes. +If it's a SVG exported by Penpot, it reads the metadata to reconstruct the +shapes exactly as they were. If not, it infers the atributes with a best effort +approach. + +In addition to the identifier ones (the id, the name and the type of element), +a shape has a lot of attributes. We tend to group them in related clusters. +Those are the main ones: + + * Selrect and other geometric attributes (x, y, width, height...) define the + position in the diagram and the bounding box. + * Transform is a [2D transformation matrix](https://www.alanzucconi.com/2016/02/10/tranfsormation-matrix/) + to rotate or stretch the shape. + * Constraints explains how the shape changes when the container shape resizes + (kind of "responsive" behavior). + * Interactions describe the interactive behavior when the shape is displayed + in the viewer. + * Fill contains the shape fill color and options. + * Stroke contains the shape stroke color and options. + * Shadow contains the shape shadow options. + * Blur contains the shape blur options. + * Font contains the font options for a shape of type text. + * Content contains the text blocks for a shape of type text. + * Exports are the defined export settings for the shape. + +Also a shape contains a reference to its containing shape (parent) and of all +the children. diff --git a/docs/technical-guide/developer/devenv.md b/docs/technical-guide/developer/devenv.md new file mode 100644 index 000000000..dff96f0fa --- /dev/null +++ b/docs/technical-guide/developer/devenv.md @@ -0,0 +1,146 @@ +--- +title: 3.3. Dev environment +--- + +# Development environment + +## System requirements + +You need to have docker and docker-compose V2 installed on your system +in order to correctly set up the development environment. + +You can [look here][1] for complete instructions. + +[1]: /technical-guide/getting-started/#install-with-docker + + +Optionally, to improve performance, you can also increase the maximum number of +user files able to be watched for changes with inotify: + +```bash +echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p +``` + + +## Getting Started + +**The interactive development environment requires some familiarity of [tmux](https://github.com/tmux/tmux/wiki).** + +To start it, clone penpot repository, and execute: + +```bash +./manage.sh pull-devenv +./manage.sh run-devenv +``` + +This will do the following: + +1. Pull the latest devenv image from dockerhub. +2. Start all the containers in the background. +3. Attach the terminal to the **devenv** container and execute the tmux session. +4. The tmux session automatically starts all the necessary services. + +This is an incomplete list of devenv related subcommands found on +manage.sh script: + +```bash +./manage.sh build-devenv # builds the devenv docker image (called by run-devenv automatically when needed) +./manage.sh start-devenv # starts background running containers +./manage.sh run-devenv # enters to new tmux session inside of one of the running containers +./manage.sh stop-devenv # stops background running containers +./manage.sh drop-devenv # removes all the containers, volumes and networks used by the devenv +``` + +Having the container running and tmux opened inside the container, +you are free to execute commands and open as many shells as you want. + +You can create a new shell just pressing the **Ctr+b c** shortcut. And +**Ctrl+b w** for switch between windows, **Ctrl+b &** for kill the +current window. + +For more info: https://tmuxcheatsheet.com/ + +It may take a minute or so, but once all of the services have started, you can +connect to penpot by browsing to http://localhost:3449 . + + + + + + + + + + + + +### Frontend + +The frontend build process is located on the tmux **window 0** and +**window 1**. On the **window 0** we have the gulp process responsible +of watching and building styles, fonts, icon-spreads and templates. + +On the **window 1** we can found the **shadow-cljs** process that is +responsible on watch and build frontend clojurescript code. + +Additionally to the watch process you probably want to be able open a REPL +process on the frontend application, for this case you can split the window +and execute this: + +```bash +npx shadow-cljs cljs-repl main +``` + +### Exporter + +The exporter build process is located in the **window 2** and in the +same way as frontend application, it is built and watched using +**shadow-cljs**. + +The main difference is that exporter will be executed in a nodejs, on +the server side instead of browser. + +The window is split into two slices. The top slice shows the build process and +on the bottom slice has a shell ready to execute the generated bundle. + +You can start the exporter process executing: + +```bash +node target/app.js +``` + +This process does not start automatically. + + +### Backend + +The backend related process is located in the tmux **window 3**, and +you can go directly to it using ctrl+b 3 shortcut. + +By default the backend will be started in a non-interactive mode for convenience +but you can press Ctrl+c to exit and execute the following to start the repl: + +```bash +./scripts/repl +``` + +On the REPL you have these helper functions: +- (start): start all the environment +- (stop): stops the environment +- (restart): stops, reload and start again. + +And many other that are defined in the dev/user.clj file. + +If an exception is raised or an error occurs when code is reloaded, just use +(repl/refresh-all) to finish loading the code correctly and then use +(restart) again. + +## Email + +To test email sending, the devenv includes [MailCatcher](https://mailcatcher.me/), +a SMTP server that is used for develop. It does not send any mail outbounds. +Instead, it stores them in memory and allows to browse them via a web interface +similar to a webmail client. Simply navigate to: + +[http://localhost:1080](http://localhost:1080) + diff --git a/docs/technical-guide/developer/frontend.md b/docs/technical-guide/developer/frontend.md new file mode 100644 index 000000000..129105976 --- /dev/null +++ b/docs/technical-guide/developer/frontend.md @@ -0,0 +1,647 @@ +--- +title: 3.5. Frontend Guide +--- + +# Frontend Guide + +This guide intends to explain the essential details of the frontend +application. + +## UI + +Please refer to the [UI Guide](/technical-guide/developer/ui) to learn about implementing UI components and our design system. + +## Logging, Tracing & Debugging + +### Logging framework + +To trace and debug the execution of the code, one method is to enable the log +traces that currently are in the code using the [Logging +framework](/technical-guide/developer/common/#system-logging). You can edit a +module and set a lower log level, to see more traces in console. Search for +this kind of line and change to :info or :debug: + +```clojure +(ns some.ns + (:require [app.util.logging :as log])) + +(log/set-level! :info) +``` + +Or you can change it live with the debug utility (see below): + +```javascript +debug.set_logging("namespace", "level"); +``` + +### Temporary traces + +Of course, you have the traditional way of inserting temporary traces inside +the code to output data to the devtools console. There are several ways of +doing this. + +#### Use clojurescript helper prn + +This helper automatically formats the clojure and js data structures as plain +[EDN](https://clojuredocs.org/clojure.edn) for visual inspection and to know +the exact type of the data. + +```clojure +(prn "message" expression) +``` + +![prn example](/img/traces1.png) + +#### Use pprint function + +We have set up a wrapper over [fipp](https://github.com/brandonbloom/fipp) +pprint function, that gives a human-readable formatting to the data, useful +for easy understanding of larger data structures. + +The wrapper allows to easily specify level, length and width parameters, +with reasonable defaults, to control the depth level of objects to print, the +number of attributes to show and the display width. + +```clojure +(:require [app.common.pprint :refer [pprint]]) + +;; On the code +(pprint shape {:level 2 + :length 21 + :width 30}) +``` + +![pprint example](/img/traces2.png) + +#### Use the js native functions + +The clj->js function converts the clojure data structure into a javacript +object, interactively inspectable in the devtools.console. + +```clojure +(js/console.log "message" (clj->js expression)) +``` + +![clj->js example](/img/traces3.png) + +### Breakpoints + +You can insert standard javascript debugger breakpoints in the code, with this +function: + +```clojure +(js-debugger) +``` + +The Clojurescript environment generates source maps to trace your code step by +step and inspect variable values. You may also insert breakpoints from the +sources tab, like when you debug javascript code. + +One way of locating a source file is to output a trace with (js/console.log) +and then clicking in the source link that shows in the console at the right +of the trace. + +### Access to clojure from js console + +The penpot namespace of the main application is exported, so that is +accessible from javascript console in Chrome developer tools. Object +names and data types are converted to javascript style. For example +you can emit the event to reset zoom level by typing this at the +console (there is autocompletion for help): + +```javascript +app.main.store.emit_BANG_(app.main.data.workspace.reset_zoom); +``` + +### Debug utility + +We have defined, at src/debug.cljs, a debug namespace with many functions +easily accesible from devtools console. + +#### Change log level + +You can change the [log level](/technical-guide/developer/common/#system-logging) +of one namespace without reloading the page: + +```javascript +debug.set_logging("namespace", "level"); +``` + +#### Dump state and objects + +There are some functions to inspect the global state or parts of it: + +```javascript +// print the whole global state +debug.dump_state(); + +// print the latest events in the global stream +debug.dump_buffer(); + +// print a key of the global state +debug.get_state(":workspace-data :pages 0"); + +// print the objects list of the current page +debug.dump_objects(); + +// print a single object by name +debug.dump_object("Rect-1"); + +// print the currently selected objects +debug.dump_selected(); + +// print all objects in the current page and local library components. +// Objects are displayed as a tree in the same order of the +// layers tree, and also links to components are shown. +debug.dump_tree(); + +// This last one has two optional flags. The first one displays the +// object ids, and the second one the {touched} state. +debug.dump_tree(true, true); +``` + +And a bunch of other utilities (see the file for more). + +## Workspace visual debug + +Debugging a problem in the viewport algorithms for grouping and +rotating is difficult. We have set a visual debug mode that displays +some annotations on screen, to help understanding what's happening. +This is also in the debug namespace. + +To activate it, open the javascript console and type: + +```js +debug.toggle_debug("option"); +``` + +Current options are bounding-boxes, group, events and +rotation-handler. + +You can also activate or deactivate all visual aids with + +```js +debug.debug_all(); +debug.debug_none(); +``` + +## Translations (I18N) + +### How it works + +All the translation strings of this application are stored in +standard _gettext_ files in frontend/translations/*.po. + +They have a self explanatory format that looks like this: + +```bash +#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs +msgid "auth.create-demo-account" +msgstr "Create demo account" +``` + +The files are automatically bundled into the index.html file on +compile time (in development and production). The bundled content is a +simplified version of this data structure to avoid loading unnecesary +data. The development environment has a watch process that detect +changes on that file and recompiles the index.html. + +**There are no hot reload for translations strings**, you just need to +refresh the browser tab to refresh the translations in the running the +application. + +Finally, when you have finished adding texts, execute the following command +inside the devenv, to reformat the file before commiting the file into the +repository: + +```bash +# cd /frontend +yarn run validate-translations +``` + +At Penpot core team we maintain manually the english and spanish .po files. All +the others are managed in https://weblate.org. + +**When a new language is available in weblate**, to enable it in the application +you need to add it in two places: + +```bash +frontend/src/app/util/i18n.cljs (supported-locales) +frontend/gulpfile.js (const langs) +``` + +### How to use it + +You need to use the app.util.i18n/tr function for lookup translation +strings: + +```clojure +(require [app.util.i18n :as i18n :refer [tr]]) + +(tr "auth.create-demo-account") +;; => "Create demo account" +``` + +If you want to insert a variable into a translated text, use %s as +placeholder, and then pass the variable value to the (tr ...) call.: + +```bash +#: src/app/main/ui/settings/change_email.cljs +msgid "notifications.validation-email-sent" +msgstr "Verification email sent to %s. Check your email!" +``` + +```clojure +(require [app.util.i18n :as i18n :refer [tr]]) + +(tr "notifications.validation-email-sent" email) +;; => "Verification email sent to test@example.com. Check your email!" +``` + +If you have defined plurals for some translation resource, then you +need to pass an additional parameter marked as counter in order to +allow the system know when to show the plural: + +```bash +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.num-of-projects" +msgid_plural "labels.num-of-projects" +msgstr[0] "1 project" +msgstr[1] "%s projects" +``` + +```clojure +(require [app.util.i18n :as i18n :refer [tr]]) + +(tr "labels.num-of-projects" (i18n/c 10)) +;; => "10 projects" + +(tr "labels.num-of-projects" (i18n/c 1)) +;; => "1 project" +``` + +## Integration tests + +### Setup + +To run integration tests locally, follow these steps. + +Ensure your development environment docker image is up to date. + +1. If it is not up to date, run: + +```bash +./manage.sh pull-devenv +``` + +2. Once the update is complete, start the environment: + +```bash +./manage.sh start-devenv +``` + +**NOTE** You can learn more about how to set up, start and stop our development environment [here](/technical-guide/developer/devenv) + +### Running the integration tests + +#### Headless mode + +Here's how to run the tests with a headless browser (i.e. within the terminal, no UI): + +1. With the developer environment tmux session opened, create a new tab with Ctrl + b c. + +2. Go to the frontend folder: + +```bash +cd penpot/frontend +``` + +3. Run the tests with yarn: + +```bash +yarn e2e:test +``` + +> 💡 **TIP:** By default, the tests will _not_ run in parallel. You can set the amount of workers to run the tests with --workers. Note that, depending on your machine, this might make some tests flaky. + +```bash +# run in parallel with 4 workers +yarn e2e:test --workers 4 +``` + +#### Running the tests in Chromium + +To access the testing UI and run the tests in a real browser, follow these steps: + +1. In a terminal _in your host machine_, navigate to the frontend folder, then run: + +```bash +# cd /frontend +npx playwright test --ui +``` + +> ⚠️ **WARNING:** It is important to be in the right folder (frontend) to launch the command above, or you may have errors trying to run the tests. + +> ❗️ **IMPORTANT**: You might need to [install Playwright's browsers and dependencies](https://playwright.dev/docs/intro) in your host machine with: npx playwright install --with-deps. In case you are using a Linux distribution other than Ubuntu, [you might need to install the dependencies manually](https://github.com/microsoft/playwright/issues/11122). + +### How to write a test + +When writing integration tests, we are simulating user actions and events triggered by them, in other to mirror real-world user interactions. The difference with fully end-to-end tests is that here we are faking the backend by intercepting the network requests, so tests can run faster and more tied to the front-end. + +Keep in mind: + +- **Use Realistic User Scenarios:** Design test cases that mimic real user scenarios and interactions with the application. + +- **Simulate User Inputs**: Such as mouse clicks, keyboard inputs, form submissions, or touch gestures, using the testing framework's API. Mimic user interactions as closely as possible to accurately simulate user behavior. + +- **Intercept the network**: Playwright offers ways to fake network responses to API calls, websocket messages, etc. Remember that there is no backend here, so you will need to intercept every request made by the front-end app. + +#### Page Object Model + +When writing a significant number of tests, encountering repetitive code and common actions is typical. To address this issue, we recommend leveraging **Page Object Models** (POM), which is a single class that encapsulates common locators, user interactions, etc. + +POMs do not necessarily refer to entire pages but can also represent specific regions of a page that are the focus of our tests. For example, we may have a POM for the login form, or the projects section. + +In a POM, we can define locators in the constructor itself — remember that locators will be accessed when interacted with (with a click(), for instance) or when asserting expectations. + +```js +class LoginPage { + constructor(page) { + super(page); + this.loginButton = page.getByRole("button", { name: "Login" }); + this.passwordInput = page.getByLabel("Password"); + this.emailInput = page.getByLabel("Email"); + } + + // ... +} +``` + +We can later use this POM and its locators: + +```js +test("Sample test", async ({ page }) => { + const loginPage = new loginPage(page); + // ... + await expect(loginPage.loginButton).toBeVisible(); +}); +``` + +> 💡 **TIP**: Locators that are generic and meant to be used in multiple tests should be part of the POM. +> +> If your locator is ad-hoc for a specific test, there's no need to add it to the POM. + +In addition to locators, POMs also include methods that perform common actions on those elements, like filling out a group of related input fields. + +```js +class LoginPage { + // ... + async fillEmailAndPasswordInputs(email, password) { + await this.emailInput.fill(email); + await this.passwordInput.fill(password); + } +} +``` + +POMs can also include the interception of network requests (but only include interceptions commont to multiple tests in the POM): + +```js +class LoginPage { + // ... + async setupLoginSuccess() { + await this.mockRPC( + "login-with-password", + "logged-in-user/login-with-password-success.json" + ); + } +} +``` + +Here's an example of a test that uses a POM: + +```js +test("User submits a wrong formatted email", async ({ page }) => { + const loginPage = new LoginPage(page); + await loginPage.setupLoginSuccess(); + + await loginPage.fillEmailAndPasswordInputs("foo", "lorenIpsum"); + + await expect(loginPage.errorLoginMessage).toBeVisible(); +}); +``` + +#### Mocking the back-end + +In the penpot repository there are some POMs that are meant to be extended by more specific pages. These include methods that should be useful when you write your own POMs. + +- BasePage contains methods to intercept network requests and return JSON data fixtures. + +- BaseWebSocketPage also can intercept websocket connections, which are a must for tests in the workspace, or any other Penpot page that uses a WebSocket. + +##### API calls + +In order to mock API calls we just need to extend from the BasePage POM and then call its method mockRPC: + +```js +export class FooPage extends BasePage { + setupNetworkResponses() { + this.mockRPC("lorem/ipsum", "json-file-with-fake-response.json"); + + // Regexes are supported too + this.mockRPC( + /a\-regex$/ + "json-file-with-fake-response.json" + ); + + // ...You can also pass custom status code and override other options + this.mockRPC("something/not/found", "json-file-with-fake-response.json", { + status: 404, + }); + } +} +``` + +> ❗️ **IMPORTANT:** The mockRPC method is meant to intercept calls to Penpot's RPC API, and already prefixes the path you provide with /api/rpc/command/. So, if you need to intercept /api/rpc/command/get-profile you would just need to call mockRPC("get-profile", "json-data.json"). + +##### WebSockets + +Any Penpot page that uses a WebSocket requires it to be intercepted and mocked. To do that, you can extend from the POM BaseWebSocketPage _and_ call its initWebSockets() methods before each test. + +Here's an an actual example from the Penpot repository: + +```js +// frontend/playwright/ui/pages/WorkspacePage.js +export class WorkspacePage extends BaseWebSocketPage { + static async init(page) { + await BaseWebSocketPage.init(page); + // ... + } +} +``` + +```js +// frontend/playwright/ui/specs/workspace.spec.js +test.beforeEach(async ({ page }) => { + await WorkspacePage.init(page); +}); +``` + +BaseWebSocketPage also includes methods to wait for a specific WebSocket connection and to fake sending/receiving messages. + +When testing the workspace, you will want to wait for the /ws/notifications WebSocket. There's a convenience method, waitForNotificationsWebSocket to do that: + +```js +// frontend/playwright/ui/pages/WorkspacePage.js +export class WorkspacePage extends BaseWebSocketPage { + // ... + + // browses to the Workspace and waits for the /ws/notifications socket to be ready + // to be listened to. + async goToWorkspace() { + // ... + this.#ws = await this.waitForNotificationsWebSocket(); + await this.#ws.mockOpen(); + // ... + } + + // sends a message over the notifications websocket + async sendPresenceMessage(fixture) { + await this.#ws.mockMessage(JSON.stringify(fixture)); + } + + // ... +} +``` + +```js +// frontend/playwright/ui/specs/workspace.spec.js +test("User receives presence notifications updates in the workspace", async ({ + page, +}) => { + const workspacePage = new WorkspacePage(page); + // ... + + await workspacePage.goToWorkspace(); + await workspacePage.sendPresenceMessage(presenceFixture); + + await expect( + page.getByTestId("active-users-list").getByAltText("Princesa Leia") + ).toHaveCount(2); +}); +``` + +### Best practices for writing tests + +Our best practices are based on [Testing library documentation](https://testing-library.com/docs/). + +This is a summary of the most important points to take into account: + +#### Query priority for locators + +For our integration tests we use Playwright, you can find more info about this library and the different locators [here](https://playwright.dev/docs/intro). + +Locator queries are the methods to find DOM elements in the page. Your test should simulate as closely as possible the way users interact with the application. Depending on the content of the page and the element to be selected, we will choose one method or the other following these priorities: + +1. **Queries accessible to everyone**: Queries that simulate the experience of visual users or use assistive technologies. + +- [page.getByRole](https://playwright.dev/docs/locators#locate-by-role): To locate exposed elements in the [accessibility tree](https://developer.mozilla.org/en-US/docs/Glossary/Accessibility_tree). + +- [page.getByLabel](https://playwright.dev/docs/locators#locate-by-label): For querying form fields. + +- [page.getByPlaceholder](https://playwright.dev/docs/locators#locate-by-placeholder): For when the placeholder text is more relevant than the label (or the label does not exist). + +- [page.getByText](https://playwright.dev/docs/locators#locate-by-text): For the non-form elements that also do not have a role in the accesibility tree, but have a distintive text. + +2. **Semantic queries**: Less preferable than the above, since the user experience when interacting with these attributes may differ significantly depending on the browser and assistive technology being used. + +- [page.byAltText](https://playwright.dev/docs/locators#locate-by-alt-text): For elements that support alt text (\, \, a custom element, etc.). + +- [page.byTitle](https://playwright.dev/docs/locators#locate-by-title): For elements with a title. + +3. **Test IDs**: If none of the queries above are feasible, we can locate by the data-testid attribute. This locator is the least preffered since it's not user-interaction oriented. + +- [page.getByTestId](https://playwright.dev/docs/locators#locate-by-test-id): For elements with a data-testid attribute. + +#### A practical example for using locator queries. + +Given this DOM structure: + +```html +
+

Penpot is the free open-...

+ + + + +
+``` + +The DOM above represents this part of the app: + +![Login page](/img/login-locators.webp) + +Our first task will be to locate the **login button**: + +![Login Button](/img/login-btn.webp) + +Our initial approach involves following the instructions of the first group of locators, "Queries accessible to everyone". To achieve this, we inspect the accessibility tree to gather information: + +![Accessibility tree Login Button](/img/a11y-tree-btn.webp) + +Having examined the accessibility tree, we identify that the button can be located by its role and name, which is our primary option: + +```js +page.getByRole("button", { name: "Login" }); +``` + +For selecting the \ within the form, we opt for getByLabel, as it is the recommended method for locating form inputs: + +![Password input](/img/locate_by_label.webp) + +```js +page.getByLabel("Password"); +``` + +If we need to locate a text with no specific role, we can use the getByText method: + +```js +page.getByText("Penpot is the free open-"); +``` + +To locate the rest of the elements we continue exploring the list of queries according to the order of priority. If none of the above options match the item, we resort to getByTestId as a last resort. + +#### Assertions + +Assertions use Playwright's expect method. Here are some tips for writing your assertions: + +- **Keep assertions clear and concise:** Each assertion should verify a single expected behavior or outcome. Avoid combining multiple assertions into a single line, to maintain clarity and readability. + +- **Use descriptive assertions:** Use assertion messages that clearly communicate the purpose of the assertion. + +- **Favor writing assertions from the user's point of view:** For instance, whenever possible, assert things about elements that the user can see or interact with. + +- **Cover the error state of a page**: Verify that the application handles errors gracefully by asserting the presence of error messages. We do not have to cover all error cases, that will be taken care of by the unit tests. + +- **Prefer positive assertions:** Avoid using .not in your assertions (i.e. expect(false).not.toBeTruthy()) —it helps with readability. + +#### Naming tests + +- **User-centric approach:** Tests should be named from the perspective of user actions. For instance, "User logs in successfully" instead of "Test login". + +- **Descriptive names:** Test names should be descriptive, clearly indicating the action being tested. + +- **Clarity and conciseness:** Keep test names clear and concise. + +- **Use action verbs:** Start test names with action verbs to denote the action being tested. Example: "Adds a new file to the project". diff --git a/docs/technical-guide/developer/index.md b/docs/technical-guide/developer/index.md new file mode 100644 index 000000000..1e635f559 --- /dev/null +++ b/docs/technical-guide/developer/index.md @@ -0,0 +1,21 @@ +--- +title: 3. Developer Guide +--- + +# Developer Guide + +This section is intended for people wanting to mess with the code or the inners +of Penpot application. + +The [Architecture][1] and [Data model][2] sections provide a bird's eye view of +the whole system, to better understand how is structured. + +The [Dev Env][3] section explains how to setup the development enviroment that +we (the core team) use internally. + +And the rest of sections are a list categorized of probably not deeply +related HOWTO articles about dev-centric subjects. + +[1]: /technical-guide/developer/architecture/ +[2]: /technical-guide/developer/data-model/ +[3]: /technical-guide/developer/devenv/ diff --git a/docs/technical-guide/developer/subsystems/assets-storage.md b/docs/technical-guide/developer/subsystems/assets-storage.md new file mode 100644 index 000000000..62000d5a8 --- /dev/null +++ b/docs/technical-guide/developer/subsystems/assets-storage.md @@ -0,0 +1,119 @@ +--- +title: Assets storage +--- + +# Assets storage + +The [storage.clj](https://github.com/penpot/penpot/blob/develop/backend/src/app/storage.clj) +is a module that manages storage of binary objects. It's a generic utility +that may be used for any kind of user uploaded files. Currently: + + * Image assets in Penpot files. + * Uploaded fonts. + * Profile photos of users and teams. + +There is an abstract interface and several implementations (or **backends**), +depending on where the objects are actually stored: + + * :assets-fs stores ojects in the file system, under a given base path. + * :assets-s3 stores them in any cloud storage with an AWS-S3 compatible + interface. + * :assets-db stores them inside the PostgreSQL database, in a special table + with a binary column. + +## Storage API + +The **StorageObject** record represents one stored object. It contains the +metadata, that is always stored in the database (table storage_object), +while the actual object data goes to the backend. + + * :id is the identifier you use to reference the object, may be stored + in other places to represent the relationship with other element. + * :backend points to the backend where the object data resides. + * :created-at is the date/time of object creation. + * :deleted-at is the date/time of object marked for deletion (see below). + * :expired-at allows to create objects that are automatically deleted + at some time (useful for temporary objects). + * :touched-at is used to check objects that may need to be deleted (see + below). + +Also more metadata may be attached to objects, such as the :content-type or +the :bucket (see below). + +You can use the API functions to manipulate objects. For example put-object! +to create a new one, get-object to retrieve the StorageObject, +get-object-data or get-object-bytes to read the binary contents, etc. + +For profile photos or fonts, the object id is stored in the related table, +without further ado. But for file images, one more indirection is used. The +**file-media-object** is an abstraction that represents one image uploaded +by the user (in the future we may support other multimedia types). It has its +own database table, and references two StorageObjects, one for the original +file and another one for the thumbnail. Image shapes contains the id of the +file-media-object with the :is-local property as true. Image assets in the +file library also have a file-media-object with :is-local false, +representing that the object may be being used in other files. + +## Serving objects + +Stored objects are always served by Penpot (even if they have a public URL, +like when :s3 storage are used). We have an endpoint /assets with three +variants: + +```bash +/assets/by-id/ +/assets/by-file-media-id/ +/assets/by-file-media-id//thumbnail +``` + +They take an object and retrieve its data to the user. For :db backend, the +data is extracted from the database and served by the app. For the other ones, +we calculate the real url of the object, and pass it to our **nginx** server, +via special HTTP headers, for it to retrieve the data and serve it to the user. + +This is the same in all environments (devenv, production or on premise). + +## Object buckets + +Obects may be organized in **buckets**, that are a kind of "intelligent" folders +(not related to AWS-S3 buckets, this is a Penpot internal concept). + +The storage module may use the bucket (hardcoded) to make special treatment to +object, such as storing in a different path, or guessing how to know if an object +is referenced from other place. + +## Sharing and deleting objects + +To save storage space, duplicated objects wre shared. So, if for example +several users upload the same image, or a library asset is instantiated many +times, even by different users, the object data is actuall stored only once. + +To achieve this, when an object is uploaded, its content is hashed, and the +hash compared with other objects in the same bucket. If there is a match, +the StorabeObject is reused. Thus, there may be different, unrelated, shapes +or library assets whose :object-id is the same. + +### Garbage collector and reference count + +Of course, when objects are shared, we cannot delete it directly when the +associated item is removed or unlinked. Instead, we need some mechanism to +track the references, and a garbage collector that deletes any object that +is no longer referenced. + +We don't use explicit reference counts or indexes. Instead, the storage system +is intelligent enough to search, depending on the bucket (one for profile +photos, other for file media objects, etc.) if there is any element that is +using the object. For example, in the first case we look for user or team +profiles where the :photo-id field matches the object id. + +When one item stops using one storage object (e. g. an image shape is deleted), +we mark the object as :touched. A periodic task revises all touched objectsm +checking if they are still referenced in other places. If not, they are marked +as :deleted. They're preserved in this state for some time (to allow "undeletion" +if the user undoes the change), and eventually, another garbage collection task +definitively deletes it, both in the backend and in the database table. + +For file-media-objects, there is another collector, that periodically checks +if a media object is referenced by any shape or asset in its file. If not, it +marks the object as :touched triggering the process described above. + diff --git a/docs/technical-guide/developer/subsystems/authentication.md b/docs/technical-guide/developer/subsystems/authentication.md new file mode 100644 index 000000000..97fa3a9e4 --- /dev/null +++ b/docs/technical-guide/developer/subsystems/authentication.md @@ -0,0 +1,149 @@ +--- +title: Authentication +--- + +# User authentication + +Users in Penpot may register via several different methods (if enabled in the +configuration of the Penpot instance). We have implemented this as a series +of "authentication backends" in our code: + + * **penpot**: internal registration with email and password. + * **ldap**: authentication over an external LDAP directory. + * **oidc**, **google**, **github**, **gitlab**: authentication over an external + service using the [OpenID Connect](https://openid.net/connect) protocol. We + have a generic handler, and other ones already preconfigured for popular + services. + +The main logic resides in the following files: + +```text +backend/src/app/rpc/mutations/profile.clj +backend/src/app/rpc/mutations/ldap.clj +backend/src/app/rpc/mutations/verify-token.clj +backend/src/app/http/oauth.clj +backend/src/app/http/session.clj +frontend/src/app/main/ui/auth/verify-token.cljs +``` + +We store in the user profiles in the database the auth backend used to register +first time (mainly for audit). A user may login with other methods later, if the +email is the same. + +## Register and login + +The code is organized to try to reuse functions and unify processes as much as +possible for the different auth systems. + + +### Penpot backend + +When a user types an email and password in the basic Penpot registration page, +frontend calls :prepare-register-profile method. It generates a "register +token", a temporary JWT token that includes the login data. + +This is used in the second registration page, that finally calls +:register-profile with the token and the rest of profile data. This function +is reused in all the registration methods, and it's responsible of creating the +user profile in the database. Then, it sends the confirmation email if using +penpot backend, or directly opens a session (see below) for othe methods or if +the user has been invited from a team. + +The confirmation email has a link to /auth/verify-token, that has a handler +in frontend, that is a hub for different kinds of tokens (registration email, +email change and invitation link). This view uses :verify-token RPC call and +redirects to the corresponding page with the result. + +To login with the penpot backend, the user simply types the email and password +and they are sent to :login method to check and open session. + +### OIDC backend + +When the user press one of the "Log in with XXX" button, frontend calls +/auth/oauth/:provider (provider is google, github or gitlab). The handler +generates a request token and redirects the user to the service provider to +authenticate in it. + +If succesful, the provider redirects to the/auth/oauth/:provider/callback. +This verifies the call with the request token, extracts another access token +from the auth response, and uses it to request the email and full name from the +service provider. + +Then checks if this is an already registered profile or not. In the first case +it opens a session, and in the second one calls:register-profile to create a +new user in the sytem. + +For the known service providers, the addresses of the protocol endpoints are +hardcoded. But for a generic OIDC service, there is a discovery protocol to ask +the provider for them, or the system administrator may set them via configuration +variables. + +### LDAP + +Registration is not possible by LDAP (we use an external user directory managed +outside of Penpot). Typically when LDAP registration is enabled, the plain user +& password login is disabled. + +When the user types their user & password and presses "Login with LDAP" button, +the :login-with-ldap method is called. It connects with the LDAP service to +validate credentials and retrieve email and full name. + +Similarly as the OIDC backend, it checks if the profile exists, and calls +:login or :register-profile as needed. + +## Sessions + +User sessions are created when a user logs in via any one of the backends. A +session token is generated (a JWT token that does not currently contain any data) +and returned to frontend as a cookie. + +Normally the session is stored in a DB table with the information of the user +profile and the session expiration. But if a frontend connects to the backend in +"read only" mode (for example, to debug something in production with the local +devenv), sessions are stored in memory (may be lost if the backend restarts). + +## Team invitations + +The invitation link has a call to /auth/verify-token frontend view (explained +above) with a token that includes the invited email. + +When a user follows it, the token is verified and then the corresponding process +is routed, depending if the email corresponds to an existing account or not. The +:register-profile or :login services are used, and the invitation token is +attached so that the profile is linked to the team at the end. + +## Handling unfinished registrations and bouncing users + +All tokens have an expiration date, and when they are put in a permanent +storage, a garbage colector task visits it periodically to cleand old items. + +Also our email sever registers email bounces and spam complaint reportings +(see backend/src/app/emails.clj). When the email of one profile receives too +many notifications, it becames blocked. From this on, the user cannot login or +register with this email, and no message will be sent to it. If it recovers +later, it needs to be unlocked manually in the database. + +## How to test in devenv + +To test all normal registration process you can use the devenv [Mail +catcher](/technical-guide/developer/devenv/#email) utility. + +To test OIDC, you need to register an application in one of the providers: + +* [Github](https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app) +* [Gitlab](https://docs.gitlab.com/ee/integration/oauth_provider.html) +* [Google](https://support.google.com/cloud/answer/6158849) + +The URL of the app will be the devenv frontend: [http://localhost:3449](). + +And then put the credentials in backend/scripts/repl and +frontend/resources/public/js/config.js. + +Finally, to test LDAP, in the devenv we include a [test LDAP](https://github.com/rroemhild/docker-test-openldap) +server, that is already configured, and only needs to be enabled in frontend +config.js: + +```js +var penpotFlags = "enable-login-with-ldap"; +``` + diff --git a/docs/technical-guide/developer/subsystems/index.md b/docs/technical-guide/developer/subsystems/index.md new file mode 100644 index 000000000..a2894fe65 --- /dev/null +++ b/docs/technical-guide/developer/subsystems/index.md @@ -0,0 +1,15 @@ +--- +title: 3.8. Penpot subsystems +--- + +# Penpot subsystems + +This section groups articles about several Penpot subsystems that have enough +complexity not to be easy to understand by only looking at the source code. + +Each article gives an overview of how a particular functionality has been +implemented, over the whole app (backend, frontend or exporter), and points to +the most relevant source files to look at to start exploring it. When some +special considerations are needed (performance questions, limits, common +"gotchas", historic reasons of some decisions, etc.) they are also noted. + diff --git a/docs/technical-guide/developer/ui.md b/docs/technical-guide/developer/ui.md new file mode 100644 index 000000000..fbcf300ca --- /dev/null +++ b/docs/technical-guide/developer/ui.md @@ -0,0 +1,756 @@ +--- +title: 3.9. UI Guide +--- + +# UI Guide + +These are the guidelines for developing UI in Penpot, including the design system. + +## React & Rumext + +The UI in Penpot uses React v18 , with the help of [rumext](https://github.com/funcool/rumext) for providing Clojure bindings. See [Rumext's User Guide](https://funcool.github.io/rumext/latest/user-guide.html) to learn how to create React components with Clojure. + +## General guidelines + +We want to hold our UI code to the same quality standards of the rest of the codebase. In practice, this means: + +- UI components should be easy to maintain over time, especially because our design system is ever-changing. +- UI components should be accessible, and use the relevant HTML elements and/or Aria roles when applicable. +- We need to apply the rules for good software design: + - The code should adhere to common patterns. + - UI components should offer an ergonomic "API" (i.e. props). + - UI components should favor composability. + - Try to have loose coupling. + +### Composability + +**Composability is a common pattern** in the Web. We can see it in the standard HTML elements, which are made to be nested one inside another to craft more complex content. Standard Web components also offer slots to make composability more flexible. + +Our UI components must be composable. In React, this is achieved via the children prop, in addition to pass slotted components via regular props. + +#### Use of children + +> **⚠️ NOTE**: Avoid manipulating children in your component. See [React docs](https://react.dev/reference/react/Children#alternatives) about the topic. + +✅ **DO: Use children when we need to enable composing** + +```clojure +(mf/defc primary-button* + {::mf/props :obj} + [{:keys [children] :rest props}] + [:> "button" props children]) +``` + +❓**Why?** + +By using children, we are signaling the users of the component that they can put things _inside_, vs a regular prop that only works with text, etc. For example, it’s obvious that we can do things like this: + +```clojure +[:> button* {} + [:* + "Subscribe for " + [:& money-amount {:currency "EUR" amount: 3000}]]] +``` + +#### Use of slotted props + +When we need to either: + +- Inject multiple (and separate) groups of elements. +- Manipulate the provided components to add, remove, filter them, etc. + +Instead of children, we can pass the component(s) via a regular a prop. + +#### When _not_ to pass a component via a prop + +It's about **ownership**. By allowing the passing of a full component, the responsibility of styling and handling the events of that component belong to whoever instantiated that component and passed it to another one. + +For instance, here the user would be in total control of the icon component for styling (and for choosing which component to use as an icon, be it another React component, or a plain SVG, etc.) + +```clojure +(mf/defc button* + {::mf/props :obj} + [{:keys [icon children] :rest props}] + [:> "button" props + icon + children]) +``` + +However, we might want to control the aspect of the icons, or limit which icons are available for this component, or choose which specific React component should be used. In this case, instead of passing the component via a prop, we'd want to provide the data we need for the icon component to be instantiated: + +```clojure +(mf/defc button* + {::mf/props :obj} + [{:keys [icon children] :rest props}] + (assert (or (nil? icon) (contains? valid-icon-list icon) "expected valid icon id")) + [:> "button" props + (when icon [:> icon* {:id icon :size "m"}]) + children]) +``` + +### Our components should have a clear responsibility + +It's important we are aware of: + +- What are the **boundaries** of our component (i.e. what it can and cannot do) + - Like in regular programming, it's good to keep all the inner elements at the same level of abstraction. + - If a component grows too big, we can split it in several ones. Note that we can mark components as private with the ::mf/private true meta tag. +- Which component is **responsible for what**. + +As a rule of thumb: + +- Components own the stuff they instantiate themselves. +- Slotted components or children belong to the place they have been instantiated. + +This ownership materializes in other areas, like **styles**. For instance, parent components are usually reponsible for placing their children into a layout. Or, as mentioned earlier, we should avoid manipulating the styles of a component we don't have ownership over. + +## Styling components + +We use **CSS modules** and **Sass** to style components. Use the (stl/css) and (stl/css-case) functions to generate the class names for the CSS modules. + +### Allow passing a class name + +Our components should allow some customization by whoever is instantiating them. This is useful for positioning elements in a layout, providing CSS properties, etc. + +This is achieved by accepting a class prop (equivalent to className in JSX). Then, we need to join the class name we have received as a prop with our own class name for CSS modules. + +```clojure +(mf/defc button* + {::mf/props :obj} + [{:keys [children class] :rest props}] + (let [class (dm/str class " " (stl/css :primary-button)) + props (mf/spread-props props {:class class})] + [:> "button" props children])) +``` + +### About nested selectors + +Nested styles for DOM elements that are not instantiated by our component should be avoided. Otherwise, we would be leaking CSS out of the component scope, which can lead to hard-to-maintain code. + +❌ **AVOID: Styling elements that don’t belong to the component** + +```clojure +(mf/defc button* + {::mf/props :obj} + [{:keys [children] :rest props}] + (let [props (mf/spread-props props {:class (stl/css :primary-button)})] + ;; note that we are NOT instantiating a here. + [:> "button" props children])) + +;; later in code +[:> button* {} + [:> icon {:id "foo"}] + "Lorem ipsum"] +``` + +```scss +.button { + // ... + svg { + fill: var(--icon-color); + } +} +``` + +✅ **DO: Take ownership of instantiating the component we need to style** + +```clojure +(mf/defc button* + {::mf/props :obj} + [{:keys [icon children class] :rest props}] + (let [props (mf/spread-props props {:class (stl/css :button)})] + [:> "button" props + (when icon [:> icon* {:id icon :size "m"}]) + [:span {:class (stl/css :label-wrapper)} children]])) + +;; later in code +[:> button* {:icon "foo"} "Lorem ipsum"] +``` + +```scss +.button { + // ... +} + +.icon { + fill: var(--icon-color); +} +``` + +### Favor lower specificity + +This helps with maintanibility, since lower [specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) styles are easier to override. + +Remember that nesting selector increases specificity, and it's usually not needed. However, pseudo-classes and pseudo-elements don't. + +❌ **AVOID: Using a not-needed high specificity** + +```scss +.btn { + // ... + .icon { + fill: var(--icon-color); + } +} +``` + +✅ **DO: Choose selectors with low specificity** + +```scss +.btn { + // ... +} + +.icon { + fill: var(--icon-color); +} +``` + +## Accessibility + +### Let the browser do the heavy lifting + +Whenever possible, leverage HTML semantic elements, which have been implemented by browsers and are accessible out of the box. + +This includes: + +- Using \ for link (navigation, downloading files, sending e-mails via mailto:, etc.) +- Using \