aboutsummaryrefslogtreecommitdiffstats
path: root/public/projects/js-small-apps
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-02-20 16:11:50 +0100
committerArmand Philippot <git@armandphilippot.com>2022-02-20 16:15:08 +0100
commit73a5c7fae9ffbe9ada721148c8c454a643aceebe (patch)
treec8fad013ed9b5dd589add87f8d45cf02bbfc6e91 /public/projects/js-small-apps
parentb01239fbdcc5bbc5921f73ec0e8fee7bedd5c8e8 (diff)
chore!: restructure repo
I separated public files from the config/dev files. It improves repo readability. I also moved dotenv helper to public/inc directory and extract the Matomo tracker in the same directory.
Diffstat (limited to 'public/projects/js-small-apps')
-rw-r--r--public/projects/js-small-apps/.gitignore6
-rwxr-xr-xpublic/projects/js-small-apps/.husky/commit-msg4
-rw-r--r--public/projects/js-small-apps/LICENSE21
-rw-r--r--public/projects/js-small-apps/README.md17
-rw-r--r--public/projects/js-small-apps/bin2dec/README.md13
-rw-r--r--public/projects/js-small-apps/bin2dec/app.js74
-rw-r--r--public/projects/js-small-apps/bin2dec/index.html41
-rw-r--r--public/projects/js-small-apps/bin2dec/style.css118
-rw-r--r--public/projects/js-small-apps/budget-app/README.md17
-rw-r--r--public/projects/js-small-apps/budget-app/app.js311
-rw-r--r--public/projects/js-small-apps/budget-app/index.html232
-rw-r--r--public/projects/js-small-apps/budget-app/lib/class-budget-app.js144
-rw-r--r--public/projects/js-small-apps/budget-app/lib/class-budget.js44
-rw-r--r--public/projects/js-small-apps/budget-app/lib/class-category.js37
-rw-r--r--public/projects/js-small-apps/budget-app/lib/class-notification.js89
-rw-r--r--public/projects/js-small-apps/budget-app/lib/class-transaction.js76
-rw-r--r--public/projects/js-small-apps/budget-app/lib/class-user.js79
-rw-r--r--public/projects/js-small-apps/budget-app/lib/utils/currency.js47
-rw-r--r--public/projects/js-small-apps/budget-app/style.css366
-rw-r--r--public/projects/js-small-apps/calculator/README.md13
-rw-r--r--public/projects/js-small-apps/calculator/app.js265
-rw-r--r--public/projects/js-small-apps/calculator/index.html103
-rw-r--r--public/projects/js-small-apps/calculator/style.css147
-rw-r--r--public/projects/js-small-apps/clock/README.md19
-rw-r--r--public/projects/js-small-apps/clock/app.js135
-rw-r--r--public/projects/js-small-apps/clock/index.html72
-rw-r--r--public/projects/js-small-apps/clock/style.css75
-rw-r--r--public/projects/js-small-apps/color-cycle/README.md13
-rw-r--r--public/projects/js-small-apps/color-cycle/app.js208
-rw-r--r--public/projects/js-small-apps/color-cycle/index.html117
-rw-r--r--public/projects/js-small-apps/color-cycle/style.css132
-rw-r--r--public/projects/js-small-apps/commitlint.config.js1
-rw-r--r--public/projects/js-small-apps/css-border-previewer/README.md13
-rw-r--r--public/projects/js-small-apps/css-border-previewer/app.js847
-rw-r--r--public/projects/js-small-apps/css-border-previewer/index.html625
-rw-r--r--public/projects/js-small-apps/css-border-previewer/style.css181
-rw-r--r--public/projects/js-small-apps/package.json28
-rw-r--r--public/projects/js-small-apps/rock-paper-scissors/README.md11
-rw-r--r--public/projects/js-small-apps/rock-paper-scissors/app.js115
-rw-r--r--public/projects/js-small-apps/rock-paper-scissors/index.html142
-rw-r--r--public/projects/js-small-apps/rock-paper-scissors/lib/class-game.js296
-rw-r--r--public/projects/js-small-apps/rock-paper-scissors/lib/class-player.js60
-rw-r--r--public/projects/js-small-apps/rock-paper-scissors/lib/class-rps-game.js230
-rw-r--r--public/projects/js-small-apps/rock-paper-scissors/lib/rps-instance.js70
-rw-r--r--public/projects/js-small-apps/rock-paper-scissors/style.css433
-rw-r--r--public/projects/js-small-apps/users-list/README.md18
-rw-r--r--public/projects/js-small-apps/users-list/app.js187
-rw-r--r--public/projects/js-small-apps/users-list/index.html29
-rw-r--r--public/projects/js-small-apps/users-list/style.css104
-rw-r--r--public/projects/js-small-apps/yarn.lock1795
50 files changed, 8220 insertions, 0 deletions
diff --git a/public/projects/js-small-apps/.gitignore b/public/projects/js-small-apps/.gitignore
new file mode 100644
index 0000000..a807e87
--- /dev/null
+++ b/public/projects/js-small-apps/.gitignore
@@ -0,0 +1,6 @@
+# Dependencies
+node_modules
+yarn-error*
+
+# Misc
+.vscode
diff --git a/public/projects/js-small-apps/.husky/commit-msg b/public/projects/js-small-apps/.husky/commit-msg
new file mode 100755
index 0000000..e8511ea
--- /dev/null
+++ b/public/projects/js-small-apps/.husky/commit-msg
@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npx --no-install commitlint --edit $1
diff --git a/public/projects/js-small-apps/LICENSE b/public/projects/js-small-apps/LICENSE
new file mode 100644
index 0000000..fb17359
--- /dev/null
+++ b/public/projects/js-small-apps/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Armand Philippot
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/public/projects/js-small-apps/README.md b/public/projects/js-small-apps/README.md
new file mode 100644
index 0000000..f167e65
--- /dev/null
+++ b/public/projects/js-small-apps/README.md
@@ -0,0 +1,17 @@
+# JS Small Apps
+
+A collection of small apps and exercises implemented with Javascript.
+
+## Description
+
+I want to keep track of some small apps or exercises but I don't think they deserve their own repository. So, I decided to gather them inside this repo.
+
+Most of the app ideas come from [@florinpop17's app-ideas repo](https://github.com/florinpop17/app-ideas).
+
+## Preview
+
+You can see a live preview of the apps here: https://demo.armandphilippot.com/
+
+## License
+
+This project is open-source and available under the [MIT License](./LICENSE).
diff --git a/public/projects/js-small-apps/bin2dec/README.md b/public/projects/js-small-apps/bin2dec/README.md
new file mode 100644
index 0000000..01a3756
--- /dev/null
+++ b/public/projects/js-small-apps/bin2dec/README.md
@@ -0,0 +1,13 @@
+# Bin2Dec
+
+An app to convert binary strings to decimal number.
+
+You can find more details about the implementation here: https://github.com/florinpop17/app-ideas/blob/master/Projects/1-Beginner/Bin2Dec-App.md
+
+## Preview
+
+You can see a live preview here: https://demo.armandphilippot.com/#bin2dec
+
+## License
+
+This project is open-source and available under the [MIT License](../LICENSE).
diff --git a/public/projects/js-small-apps/bin2dec/app.js b/public/projects/js-small-apps/bin2dec/app.js
new file mode 100644
index 0000000..ccdaee0
--- /dev/null
+++ b/public/projects/js-small-apps/bin2dec/app.js
@@ -0,0 +1,74 @@
+function setErrorBox() {
+ const main = document.querySelector(".main");
+ const form = document.querySelector(".form");
+ const errorBox = document.createElement("div");
+ errorBox.classList.add("error-box");
+ main.insertBefore(errorBox, form);
+}
+
+function getErrorBox() {
+ let errorBox = document.querySelector(".error-box");
+
+ if (!errorBox) {
+ setErrorBox();
+ errorBox = document.querySelector(".error-box");
+ }
+
+ return errorBox;
+}
+
+function removeErrorBox() {
+ const errorBox = getErrorBox();
+ errorBox.remove();
+}
+
+function printError(error) {
+ const errorBox = getErrorBox();
+ errorBox.textContent = error;
+}
+
+function isValidInput(key) {
+ return key === "0" || key === "1";
+}
+
+function hasInvalidChar(string) {
+ const regex = /(?![01])./g;
+ const invalid = string.search(regex);
+ return invalid === -1 ? false : true;
+}
+
+function handleInput(value) {
+ if (hasInvalidChar(value)) {
+ const error = "Error: valid characters are 0 or 1.";
+ printError(error);
+ } else {
+ removeErrorBox();
+ }
+}
+
+function convertBinToDec(bin) {
+ let result = 0;
+
+ for (const char of bin) {
+ result = result * 2 + Number(char);
+ }
+
+ return result;
+}
+
+function handleSubmit(e) {
+ e.preventDefault();
+ const input = document.querySelector(".form__input");
+ const result = document.getElementById("result");
+ result.textContent = convertBinToDec(input.value);
+}
+
+function init() {
+ const form = document.querySelector(".form");
+ const input = document.querySelector(".form__input");
+ handleInput(input.value);
+ form.addEventListener("submit", handleSubmit);
+ input.addEventListener("keyup", (e) => handleInput(e.target.value));
+}
+
+init();
diff --git a/public/projects/js-small-apps/bin2dec/index.html b/public/projects/js-small-apps/bin2dec/index.html
new file mode 100644
index 0000000..dd8951d
--- /dev/null
+++ b/public/projects/js-small-apps/bin2dec/index.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Bin2Dec</title>
+ <link rel="stylesheet" href="style.css" />
+ </head>
+ <body>
+ <header class="header"><h1 class="branding">Bin2Dec</h1></header>
+ <main class="main">
+ <form action="#" method="POST" class="form">
+ <fieldset class="form__fieldset">
+ <legend class="form__legend">
+ Convert a binary number to decimal
+ </legend>
+ <label class="form__label">Binary:</label>
+ <input
+ type="text"
+ name="binary-string"
+ id="input-field"
+ value=""
+ required
+ pattern="[0-1]{1,}"
+ class="form__input"
+ />
+ <button type="submit" class="btn">Convert</button>
+ </fieldset>
+ <div class="result-box">
+ <p class="result-box__label">Decimal:</p>
+ <p id="result"></p>
+ </div>
+ </form>
+ </main>
+ <footer class="footer">
+ <p class="copyright">Bin2Dec. MIT 2021. Armand Philippot.</p>
+ </footer>
+ <script src="app.js"></script>
+ </body>
+</html>
diff --git a/public/projects/js-small-apps/bin2dec/style.css b/public/projects/js-small-apps/bin2dec/style.css
new file mode 100644
index 0000000..22db30a
--- /dev/null
+++ b/public/projects/js-small-apps/bin2dec/style.css
@@ -0,0 +1,118 @@
+*,
+*::after,
+*::before {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ background: #fff;
+ color: #000;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 1rem;
+ line-height: 1.618;
+ display: flex;
+ flex-flow: column nowrap;
+ min-height: 100vh;
+}
+
+.header,
+.main,
+.footer {
+ width: min(calc(100vw - 2rem), 80ch);
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.header {
+ padding: 2rem 0 3rem;
+ text-align: center;
+}
+
+.main {
+ flex: 1;
+ display: flex;
+ flex-flow: column nowrap;
+ align-content: flex-start;
+}
+
+.footer {
+ margin-top: 2rem;
+ padding: 1rem 0;
+}
+
+.copyright {
+ font-size: 0.9rem;
+ text-align: center;
+}
+
+.form {
+ border: solid 2px hsl(219, 64%, 35%);
+ border-radius: 5px;
+ padding: 1rem;
+ margin: 0 auto;
+}
+
+.form__fieldset {
+ border: none;
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.form__legend {
+ color: hsl(219, 64%, 35%);
+ font-weight: 600;
+ font-size: 1.1rem;
+ margin-bottom: 2rem;
+}
+
+.form__label {
+ cursor: pointer;
+ font-weight: 600;
+}
+
+.form__input {
+ border: 1px solid hsl(219, 64%, 35%);
+ font: inherit;
+ line-height: inherit;
+ padding: 0.2rem 0.5rem;
+}
+
+.btn {
+ background: #fff;
+ border: 2px solid hsl(219, 64%, 35%);
+ color: hsl(219, 64%, 35%);
+ font: inherit;
+ font-weight: 600;
+ line-height: inherit;
+ padding: 0.2rem 0.5rem;
+ cursor: pointer;
+}
+
+.btn:hover,
+.btn:focus {
+ background: hsl(219, 64%, 35%);
+ color: #fff;
+}
+
+.error-box {
+ border: 1px solid hsl(0, 75%, 38%);
+ color: hsl(0, 75%, 38%);
+ font-weight: 600;
+ margin: 0 auto 2rem;
+ padding: 1rem;
+ width: max-content;
+}
+
+.result-box {
+ display: flex;
+ gap: 0.5rem;
+ margin-top: 2rem;
+}
+
+.result-box__label {
+ font-weight: 600;
+}
diff --git a/public/projects/js-small-apps/budget-app/README.md b/public/projects/js-small-apps/budget-app/README.md
new file mode 100644
index 0000000..fe8f09f
--- /dev/null
+++ b/public/projects/js-small-apps/budget-app/README.md
@@ -0,0 +1,17 @@
+# Budget app
+
+A simple budget app.
+
+## Description
+
+You can define some categories for your expenses and incomes. Then you can add/edit/remove each type of transaction.
+
+The app will show you the remaining budget and the total expenses based on the initial budget.
+
+## Preview
+
+You can see a live preview here: https://demo.armandphilippot.com/#budget-app
+
+## License
+
+This project is open-source and available under the [MIT License](../LICENSE).
diff --git a/public/projects/js-small-apps/budget-app/app.js b/public/projects/js-small-apps/budget-app/app.js
new file mode 100644
index 0000000..2fb0cba
--- /dev/null
+++ b/public/projects/js-small-apps/budget-app/app.js
@@ -0,0 +1,311 @@
+import BudgetApp from "./lib/class-budget-app.js";
+import Notification from "./lib/class-notification.js";
+import getCurrencyFormat from "./lib/utils/currency.js";
+
+const app = new BudgetApp("Budget", "Anonymous");
+const ui = {
+ budget: {
+ remaining: document.getElementById("budget-remaining"),
+ spent: document.getElementById("budget-spent"),
+ },
+ buttons: {
+ categories: {
+ add: document.querySelector(".manage-categories .btn--add"),
+ delete: document.querySelector(".manage-categories .btn--delete"),
+ rename: document.querySelector(".manage-categories .btn--rename"),
+ },
+ register: document.querySelector(".register .btn--register"),
+ reset: document.querySelector(".footer .btn--reset"),
+ transactions: {
+ update: document.querySelector(".manage-transactions .btn--update"),
+ },
+ },
+ form: {
+ categories: {
+ add: document.getElementById("add-category"),
+ rename: document.getElementById("rename-category"),
+ select: document.getElementById("select-category"),
+ },
+ register: {
+ budget: document.getElementById("register-budget"),
+ language: document.getElementById("register-locale"),
+ username: document.getElementById("register-username"),
+ },
+ transactions: {
+ amount: document.getElementById("transaction-amount"),
+ category: document.getElementById("transaction-category"),
+ date: document.getElementById("transaction-date"),
+ id: document.getElementById("transaction-id"),
+ name: document.getElementById("transaction-name"),
+ type: document.getElementById("transaction-type"),
+ },
+ },
+ history: {
+ body: document.querySelector(".app__history .table__body"),
+ },
+ title: document.querySelector(".branding__title"),
+};
+
+function initApp() {
+ const register = document.querySelector(".register");
+ const application = document.querySelector(".app");
+ const budget = ui.form.register.budget.value;
+ const locale = ui.form.register.language.value;
+ const username = ui.form.register.username.value;
+
+ if (budget && locale && username) {
+ app.user.username = username;
+ app.user.budget = budget;
+ app.user.locale = locale;
+ register.style.display = "none";
+ application.style.display = "block";
+ } else {
+ notify("You must complete all fields!", "error", 3000);
+ }
+}
+
+function notify(message, type, duration, position = "bottom") {
+ const notification = new Notification(message, type);
+ notification.duration = duration;
+ notification.position = position;
+ return notification.notify();
+}
+
+function getSelectOptions(select, options) {
+ select.innerHTML = "";
+ options.forEach((option) => {
+ select.add(new Option(option.name, option.id));
+ });
+
+ return select;
+}
+
+function findName(id, array) {
+ const object = array.find((item) => item.id === Number(id));
+ if (object) {
+ return object.name;
+ } else {
+ return "(deleted)";
+ }
+}
+
+function resetTransactionForm() {
+ ui.form.transactions.amount.value = "";
+ ui.form.transactions.category.value = "";
+ ui.form.transactions.date.value = "";
+ ui.form.transactions.id.value = "";
+ ui.form.transactions.name.value = "";
+ ui.form.transactions.type.value = "";
+}
+
+function setTransactionForm(transaction) {
+ ui.form.transactions.amount.value = transaction.amount;
+ ui.form.transactions.category.value = transaction.category;
+ ui.form.transactions.date.valueAsDate = transaction.date;
+ ui.form.transactions.id.value = transaction.id;
+ ui.form.transactions.name.value = transaction.name;
+ ui.form.transactions.type.value = transaction.type;
+}
+
+function manageHistory(target) {
+ const tr = target.parentElement.parentElement;
+ const transactionId = Number(tr.id.replace("transaction-", ""));
+ const array = tr.classList.contains("table__row--expense")
+ ? app.expenses
+ : app.incomes;
+ if (target.classList.contains("btn--delete")) {
+ const tbody = tr.parentElement;
+ app.remove(transactionId, array);
+ updateBudget();
+ updateHistory(tbody);
+ } else if (target.classList.contains("btn--edit")) {
+ const index = array.findIndex(
+ (transaction) => transaction.id === transactionId
+ );
+ setTransactionForm(array[index]);
+ }
+}
+
+function getTransactionButton(type) {
+ const btn = document.createElement("button");
+ let text = "";
+
+ switch (type) {
+ case "delete":
+ text = "Delete";
+ break;
+ case "edit":
+ text = "Edit";
+ break;
+ default:
+ break;
+ }
+
+ btn.textContent = text;
+ btn.classList.add("btn", `btn--${type}`);
+ btn.addEventListener("click", (event) => manageHistory(event.target));
+
+ return btn;
+}
+
+function getTransactionCell(data = "") {
+ const td = document.createElement("td");
+ td.classList.add("table__item");
+ td.textContent = data;
+
+ return td;
+}
+
+function getTransactionRow(transaction) {
+ const tr = document.createElement("tr");
+ const amount =
+ transaction.type === "expense"
+ ? transaction.amount * -1
+ : transaction.amount;
+ const localizedAmount = getCurrencyFormat(amount, app.user.locale);
+ const categoryName = findName(transaction.category, app.categories);
+ const date = transaction.date.toLocaleDateString(app.user.locale);
+
+ const dateCell = getTransactionCell(date);
+ const nameCell = getTransactionCell(transaction.name);
+ const categoryCell = getTransactionCell(categoryName);
+ const typeCell = getTransactionCell(transaction.type);
+ const amountCell = getTransactionCell(localizedAmount);
+ const manageCell = getTransactionCell();
+ const editButton = getTransactionButton("edit");
+ const deleteButton = getTransactionButton("delete");
+
+ manageCell.append(editButton, deleteButton);
+ tr.classList.add("table__row", `table__row--${transaction.type}`);
+ tr.id = `transaction-${transaction.id}`;
+ tr.append(dateCell, nameCell, categoryCell, typeCell, amountCell, manageCell);
+
+ return tr;
+}
+
+function getHistory(tbody) {
+ const transactions = app.getOrderedTransactions("oldest");
+ transactions.forEach((transaction) => {
+ tbody.appendChild(getTransactionRow(transaction));
+ });
+}
+
+function updateBudget() {
+ app.updateUserBudget();
+ ui.budget.remaining.textContent = getCurrencyFormat(
+ app.user.budget.remaining(),
+ app.user.locale
+ );
+ ui.budget.spent.textContent = getCurrencyFormat(
+ app.user.budget.spent,
+ app.user.locale
+ );
+}
+
+function updateCategories() {
+ getSelectOptions(ui.form.categories.select, app.categories);
+ getSelectOptions(ui.form.transactions.category, app.categories);
+}
+
+function updateHistory() {
+ ui.history.body.innerHTML = "";
+ getHistory(ui.history.body, app);
+}
+
+function updateAll() {
+ ui.title.textContent = `${app.user.username} ${app.title}`;
+ updateBudget();
+ updateCategories();
+ updateHistory();
+}
+
+function listen() {
+ for (const [name, element] of Object.entries(ui.buttons.categories)) {
+ element.addEventListener("click", (event) => {
+ event.preventDefault();
+ const id = Number(ui.form.categories.select.value);
+
+ switch (name) {
+ case "add":
+ if (ui.form.categories.add.value) {
+ app.addCategory(ui.form.categories.add.value);
+ ui.form.categories.add.value = "";
+ } else {
+ notify("Category name must be filled!", "error", 3000);
+ }
+ break;
+ case "delete":
+ if (id) {
+ app.remove(id, app.categories);
+ updateHistory();
+ } else {
+ notify("A category must be selected!", "error", 3000);
+ }
+ break;
+ case "rename":
+ const newName = ui.form.categories.rename.value;
+ if (newName && id) {
+ app.renameCategory(id, newName);
+ updateHistory();
+ ui.form.categories.rename.value = "";
+ } else {
+ notify(
+ "You need to select a category and enter a new name first!",
+ "error",
+ 3000
+ );
+ }
+ break;
+ default:
+ break;
+ }
+
+ updateCategories();
+ });
+ }
+
+ ui.buttons.transactions.update.addEventListener("click", (event) => {
+ event.preventDefault();
+ const transactionId = Number(ui.form.transactions.id.value);
+ const transaction = {
+ date: ui.form.transactions.date.value,
+ name: ui.form.transactions.name.value,
+ type: ui.form.transactions.type.value,
+ category: ui.form.transactions.category.value,
+ amount: ui.form.transactions.amount.value,
+ };
+ const error = [];
+ for (const value in transaction) {
+ const element = transaction[value];
+ !element ? error.push(value) : "";
+ }
+ if (error.length === 0) {
+ transactionId
+ ? app.editTransaction({ id: transactionId, ...transaction })
+ : app.addTransaction(transaction);
+ updateBudget();
+ updateHistory();
+ resetTransactionForm();
+ } else {
+ const errorMsg = `These fields (${error.join(", ")}) must be filled!`;
+ notify(errorMsg, "error", 3000);
+ }
+ });
+
+ ui.buttons.register.addEventListener("click", (event) => {
+ event.preventDefault();
+ initApp();
+ updateAll();
+ });
+
+ ui.buttons.reset.addEventListener("click", (event) => {
+ event.preventDefault();
+ if (confirm("Are you sure?")) {
+ notify("Reset!", "warning", 2000);
+ app.reset();
+ }
+ updateAll();
+ });
+}
+
+listen();
diff --git a/public/projects/js-small-apps/budget-app/index.html b/public/projects/js-small-apps/budget-app/index.html
new file mode 100644
index 0000000..90f5d12
--- /dev/null
+++ b/public/projects/js-small-apps/budget-app/index.html
@@ -0,0 +1,232 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Budget App</title>
+ <link rel="stylesheet" href="style.css" />
+ </head>
+ <body>
+ <header class="branding">
+ <h1 class="branding__title">Budget App</h1>
+ </header>
+ <main class="main">
+ <div class="register">
+ <form action="#" class="register__form form">
+ <p class="form__item">
+ <label for="register-username" class="form__label"
+ >Enter a username:</label
+ >
+ <input
+ type="text"
+ id="register-username"
+ name="register-username"
+ class="form__field"
+ required
+ />
+ </p>
+ <p class="form__item">
+ <label for="register-budget" class="form__label"
+ >Enter a budget:</label
+ >
+ <input
+ type="number"
+ id="register-budget"
+ name="register-budget"
+ class="form__field"
+ required
+ />
+ </p>
+ <p class="form__item">
+ <label for="register-locale" class="form__label"
+ >Select a language:</label
+ >
+ <select
+ name="register-locale"
+ id="register-locale"
+ class="form__select"
+ required
+ >
+ <option value="en-US">Anglais</option>
+ <option value="fr-FR">Français</option>
+ </select>
+ </p>
+ <button type="submit" class="btn btn--register">Register</button>
+ </form>
+ </div>
+ <div class="app">
+ <div class="app__management">
+ <div class="app__categories">
+ <h2 class="app__title">Categories</h2>
+ <form action="#" class="manage-categories form">
+ <fieldset class="form__fieldset">
+ <legend class="form__legend">Add a new category</legend>
+ <p class="form__item">
+ <label for="add-category" class="form__label"
+ >Category name:</label
+ >
+ <input
+ type="text"
+ name=""
+ id="add-category"
+ class="form__field"
+ required
+ />
+ <button type="submit" class="form__submit btn btn--add">
+ Add
+ </button>
+ </p>
+ </fieldset>
+ <fieldset class="form__fieldset">
+ <legend class="form__legend">Manage existing categories</legend>
+ <p class="form__item">
+ <label for="select-category" class="form__label"
+ >Select a category:</label
+ >
+ <select
+ name="select-category"
+ id="select-category"
+ class="form__select"
+ >
+ <option value="undefined" class="form__option">
+ Undefined
+ </option>
+ </select>
+ <button type="submit" class="form__submit btn btn--delete">
+ Delete
+ </button>
+ </p>
+ <p class="form__item">
+ <label for="rename-category" class="form__label"
+ >New name:</label
+ >
+ <input
+ type="text"
+ name="rename-category"
+ id="rename-category"
+ class="form__field"
+ />
+ <button type="submit" class="form__submit btn btn--rename">
+ Rename
+ </button>
+ </p>
+ </fieldset>
+ </form>
+ </div>
+ <div class="app__transactions">
+ <h2 class="app__title">Transactions</h2>
+ <form action="#" class="manage-transactions form">
+ <fieldset class="form__fieldset">
+ <legend class="form__legend">Manage transactions</legend>
+ <p class="form__item">
+ <label for="transaction-date" class="form__label"
+ >Select a date:</label
+ >
+ <input
+ type="date"
+ name="transaction-date"
+ id="transaction-date"
+ class="form__field"
+ required
+ />
+ </p>
+ <p class="form__item">
+ <label for="" class="form__label">Choose a type:</label>
+ <select
+ name="transaction-type"
+ id="transaction-type"
+ class="form__select"
+ >
+ <option value=""></option>
+ <option value="expense">Expense</option>
+ <option value="income">Income</option>
+ </select>
+ </p>
+ <p class="form__item">
+ <label for="" class="form__label">Choose a category:</label>
+ <select
+ name="transaction-category"
+ id="transaction-category"
+ class="form__select"
+ >
+ <option value="undefined">Undefined</option>
+ </select>
+ </p>
+ <p class="form__item">
+ <label for="transaction-name" class="form__label"
+ >Name:</label
+ >
+ <input
+ type="text"
+ name="transaction-name"
+ id="transaction-name"
+ class="form__field"
+ required
+ />
+ </p>
+ <p class="form__item">
+ <label for="transaction-amount" class="form__label"
+ >Amount:</label
+ >
+ <input
+ type="number"
+ name="transaction-amount"
+ id="transaction-amount"
+ step="0.01"
+ class="form__field"
+ required
+ />
+ </p>
+ <input
+ type="hidden"
+ name="transaction-id"
+ id="transaction-id"
+ />
+ <button type="submit" class="form__submit btn btn--update">
+ Update
+ </button>
+ </fieldset>
+ </form>
+ </div>
+ <div class="app__budget">
+ <h2 class="app__title">Budget state</h2>
+ <table class="table">
+ <thead class="table__header">
+ <tr>
+ <td class="table__item">Spent</td>
+ <td class="table__item">Remaining</td>
+ </tr>
+ </thead>
+ <tbody class="table__body">
+ <tr>
+ <td id="budget-spent" class="table__item">0</td>
+ <td id="budget-remaining" class="table__item">0</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <div class="app__history">
+ <h2 class="app__title">History</h2>
+ <table class="table">
+ <thead class="table__header">
+ <td class="table__item">Date</td>
+ <td class="table__item">Name</td>
+ <td class="table__item">Category</td>
+ <td class="table__item">Type</td>
+ <td class="table__item">Amount</td>
+ <td class="table__item">Manage</td>
+ </thead>
+ <tbody class="table__body"></tbody>
+ </table>
+ </div>
+ </div>
+ </main>
+ <footer class="footer container">
+ <button type="button" class="btn btn--reset">Reset</button>
+ <p class="footer__copyright">Budget App. MIT 2021. Armand Philippot.</p>
+ </footer>
+ <script src="app.js" type="module"></script>
+ </body>
+</html>
diff --git a/public/projects/js-small-apps/budget-app/lib/class-budget-app.js b/public/projects/js-small-apps/budget-app/lib/class-budget-app.js
new file mode 100644
index 0000000..45097a0
--- /dev/null
+++ b/public/projects/js-small-apps/budget-app/lib/class-budget-app.js
@@ -0,0 +1,144 @@
+import Category from "./class-category.js";
+import Transaction from "./class-transaction.js";
+import User from "./class-user.js";
+
+class BudgetApp {
+ #title = "Budget App";
+ #categoryId = 1;
+ #categories = [];
+ #transactionId = 1;
+ #incomes = [];
+ #expenses = [];
+ #userId = 1;
+ #user = {};
+
+ constructor(title, username) {
+ this.#title = title;
+ this.#user = new User(this.#userId++, username);
+ }
+
+ set title(string) {
+ this.#title = string;
+ }
+
+ get title() {
+ return this.#title;
+ }
+
+ set categories(array) {
+ this.#categories = array;
+ }
+
+ get categories() {
+ return this.#categories;
+ }
+
+ set incomes(array) {
+ this.#incomes = array;
+ }
+
+ get incomes() {
+ return this.#incomes;
+ }
+
+ set expenses(array) {
+ this.#expenses = array;
+ }
+
+ get expenses() {
+ return this.#expenses;
+ }
+
+ set user(username) {
+ this.#user = new User(this.#userId++, username);
+ }
+
+ get user() {
+ return this.#user;
+ }
+
+ remove(id, from) {
+ const index = from.findIndex((object) => object.id === id);
+ from.splice(index, 1);
+ }
+
+ addCategory(name) {
+ this.#categories.push(new Category(this.#categoryId++, name));
+ }
+
+ renameCategory(id, newName) {
+ const index = this.categories.findIndex((object) => object.id === id);
+ this.categories[index].name = newName;
+ }
+
+ addTransaction(transaction) {
+ const array =
+ transaction.type === "income" ? this.#incomes : this.#expenses;
+ array.push(
+ new Transaction(
+ this.#transactionId++,
+ transaction.date,
+ transaction.name,
+ transaction.type,
+ transaction.category,
+ transaction.amount
+ )
+ );
+ }
+
+ editTransaction(transaction) {
+ const array = transaction.type === "income" ? this.incomes : this.expenses;
+ const index = array.findIndex((object) => {
+ return object.id === Number(transaction.id);
+ });
+ if (index !== -1) {
+ array[index] = new Transaction(...Object.values(transaction));
+ } else {
+ const oldArray = array === this.incomes ? this.expenses : this.incomes;
+ array.push(new Transaction(...Object.values(transaction)));
+ this.remove(transaction.id, oldArray);
+ }
+ }
+
+ getOrderedTransactions(order) {
+ const transactions = [...this.expenses, ...this.incomes];
+
+ switch (order) {
+ case "newest":
+ transactions.sort((a, b) => b.date - a.date);
+ break;
+ case "oldest":
+ transactions.sort((a, b) => a.date - b.date);
+ break;
+ default:
+ break;
+ }
+
+ return transactions;
+ }
+
+ total(transaction) {
+ const array = transaction === "expense" ? this.#expenses : this.#incomes;
+ let total = 0;
+ array.forEach((item) => {
+ total += item.amount;
+ });
+ return total;
+ }
+
+ updateUserBudget() {
+ this.user.budget.spent = this.total("expense");
+ this.user.budget.profit = this.total("income");
+ }
+
+ reset() {
+ this.#categoryId = 1;
+ this.#transactionId = 1;
+ this.#categories = [];
+ this.#incomes = [];
+ this.#expenses = [];
+ this.updateUserBudget();
+ }
+}
+
+export default BudgetApp;
diff --git a/public/projects/js-small-apps/budget-app/lib/class-budget.js b/public/projects/js-small-apps/budget-app/lib/class-budget.js
new file mode 100644
index 0000000..cde22fd
--- /dev/null
+++ b/public/projects/js-small-apps/budget-app/lib/class-budget.js
@@ -0,0 +1,44 @@
+/**
+ * Budget class
+ *
+ * Create a new budget.
+ */
+class Budget {
+ #initial = 0;
+ #spent = 0;
+ #profit = 0;
+
+ constructor(initial) {
+ this.#initial = Number.parseFloat(initial);
+ }
+
+ set initial(number) {
+ this.#initial = Number.parseFloat(number);
+ }
+
+ get initial() {
+ return this.#initial;
+ }
+
+ set spent(number) {
+ this.#spent = Number.parseFloat(number);
+ }
+
+ get spent() {
+ return this.#spent;
+ }
+
+ set profit(number) {
+ this.#profit = Number.parseFloat(number);
+ }
+
+ get profit() {
+ return this.#profit;
+ }
+
+ remaining() {
+ return this.initial + this.profit - this.spent;
+ }
+}
+
+export default Budget;
diff --git a/public/projects/js-small-apps/budget-app/lib/class-category.js b/public/projects/js-small-apps/budget-app/lib/class-category.js
new file mode 100644
index 0000000..7b2f1b2
--- /dev/null
+++ b/public/projects/js-small-apps/budget-app/lib/class-category.js
@@ -0,0 +1,37 @@
+/**
+ * Category class.
+ *
+ * Create a new category with id, name and attachments.
+ */
+class Category {
+ #id = 0;
+ #name = "";
+ #attachments = [];
+
+ constructor(id, name) {
+ this.#id = Number(id);
+ this.#name = name;
+ }
+
+ set name(name) {
+ this.#name = name;
+ }
+
+ get name() {
+ return this.#name;
+ }
+
+ get id() {
+ return this.#id;
+ }
+
+ set attachments(attachment) {
+ this.#attachments.push(Number(attachment));
+ }
+
+ get attachments() {
+ return this.#attachments;
+ }
+}
+
+export default Category;
diff --git a/public/projects/js-small-apps/budget-app/lib/class-notification.js b/public/projects/js-small-apps/budget-app/lib/class-notification.js
new file mode 100644
index 0000000..17a32a0
--- /dev/null
+++ b/public/projects/js-small-apps/budget-app/lib/class-notification.js
@@ -0,0 +1,89 @@
+class Notification {
+ #id = 0;
+ #title = "";
+ #message = "";
+ #type = "";
+ #duration = 0;
+ #position = "";
+
+ constructor(message, type) {
+ this.#message = message;
+ this.#type = type;
+ }
+
+ get id() {
+ return this.#id;
+ }
+
+ set title(string) {
+ this.#title = string;
+ }
+
+ get title() {
+ return this.#title;
+ }
+
+ set message(text) {
+ this.#message = text;
+ }
+
+ get message() {
+ return this.#message;
+ }
+
+ set type(string) {
+ this.#type = string;
+ }
+
+ get type() {
+ return this.#type;
+ }
+
+ set duration(number) {
+ this.#duration = number;
+ }
+
+ get duration() {
+ return this.#duration;
+ }
+
+ set position(string) {
+ this.#position = string;
+ }
+
+ get position() {
+ return this.#position;
+ }
+
+ #getWrapper() {
+ let wrapper = document.getElementById("notifications-center");
+
+ if (!wrapper) {
+ wrapper = document.createElement("div");
+ wrapper.id = "notifications-center";
+ wrapper.classList = "notifications";
+ document.body.appendChild(wrapper);
+ }
+
+ return wrapper;
+ }
+
+ notify() {
+ const notification = document.createElement("div");
+ notification.textContent = this.message;
+ notification.classList.add("notification", `notification--${this.type}`);
+
+ const wrapper = this.#getWrapper();
+ document.body.style.position = "relative";
+ wrapper.style.cssText = `position: fixed;${this.position}: 1rem;right: 1rem;display: flex;flex-flow: column wrap;gap: 1rem;`;
+ wrapper.appendChild(notification);
+
+ if (this.duration && Number(this.duration) !== 0) {
+ setTimeout(() => {
+ notification.remove();
+ }, this.duration);
+ }
+ }
+}
+
+export default Notification;
diff --git a/public/projects/js-small-apps/budget-app/lib/class-transaction.js b/public/projects/js-small-apps/budget-app/lib/class-transaction.js
new file mode 100644
index 0000000..6807372
--- /dev/null
+++ b/public/projects/js-small-apps/budget-app/lib/class-transaction.js
@@ -0,0 +1,76 @@
+/**
+ * Transaction class
+ *
+ * Create a new transaction with id, date, name, type, category and amount.
+ */
+class Transaction {
+ #id = 0;
+ #date = new Date();
+ #name = "";
+ #type = "";
+ #category = 0;
+ #amount = 0;
+
+ constructor(id, date, name, type, category, amount) {
+ this.#id = Number(id);
+ this.#date = new Date(date);
+ this.#name = name;
+ this.#type = type;
+ this.#category = Number(category);
+ this.#amount = Number.parseFloat(amount);
+ }
+
+ get id() {
+ return this.#id;
+ }
+
+ set date(datetime) {
+ this.#date = datetime;
+ }
+
+ get date() {
+ return this.#date;
+ }
+
+ set name(string) {
+ this.#name = string;
+ }
+
+ get name() {
+ return this.#name;
+ }
+
+ set type(string) {
+ this.#type = string;
+ }
+
+ get type() {
+ return this.#type;
+ }
+
+ set category(string) {
+ this.#category = string;
+ }
+
+ get category() {
+ return this.#category;
+ }
+
+ set amount(number) {
+ this.#amount = number;
+ }
+
+ get amount() {
+ return this.#amount;
+ }
+
+ update(date, name, type, category, amount) {
+ this.date = date;
+ this.name = name;
+ this.type = type;
+ this.category = category;
+ this.amount = amount;
+ }
+}
+
+export default Transaction;
diff --git a/public/projects/js-small-apps/budget-app/lib/class-user.js b/public/projects/js-small-apps/budget-app/lib/class-user.js
new file mode 100644
index 0000000..e0e137b
--- /dev/null
+++ b/public/projects/js-small-apps/budget-app/lib/class-user.js
@@ -0,0 +1,79 @@
+import Budget from "./class-budget.js";
+
+class User {
+ #id = 0;
+ #username = "Anonymous";
+ #firstName = "John";
+ #lastName = "Doe";
+ #role = "admin";
+ #locale = "en-US";
+ #accountCreation = new Date();
+ #budget = 0;
+
+ constructor(id, username) {
+ this.#id = id;
+ this.#username = username;
+ }
+
+ get id() {
+ return this.#id;
+ }
+
+ set username(name) {
+ this.#username = name;
+ }
+
+ get username() {
+ return this.#username;
+ }
+
+ set firstName(name) {
+ this.#firstName = name;
+ }
+
+ get firstName() {
+ return this.#firstName;
+ }
+
+ set lastName(name) {
+ this.#lastName = name;
+ }
+
+ get lastName() {
+ return this.#lastName;
+ }
+
+ set role(string) {
+ this.#role = string;
+ }
+
+ get role() {
+ return this.#role;
+ }
+
+ set locale(code) {
+ this.#locale = code;
+ }
+
+ get locale() {
+ return this.#locale;
+ }
+
+ get accountCreation() {
+ return this.#accountCreation;
+ }
+
+ set budget(number) {
+ this.#budget = new Budget(number);
+ }
+
+ get budget() {
+ return this.#budget;
+ }
+
+ name() {
+ return `${this.#firstName} ${this.#lastName}`;
+ }
+}
+
+export default User;
diff --git a/public/projects/js-small-apps/budget-app/lib/utils/currency.js b/public/projects/js-small-apps/budget-app/lib/utils/currency.js
new file mode 100644
index 0000000..04d3ad0
--- /dev/null
+++ b/public/projects/js-small-apps/budget-app/lib/utils/currency.js
@@ -0,0 +1,47 @@
+/**
+ * Convert a number to fr_FR locale.
+ * @param {Number} number A number to format.
+ * @returns A number formatted with fr_FR locale.
+ */
+const getCurrencyFR = (number) => {
+ const formatted =
+ Number.parseFloat(number)
+ .toFixed(2)
+ .replace(".", ",")
+ .replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1 ") + " €";
+ return formatted;
+};
+
+/**
+ * Convert a number to en_US locale.
+ * @param {Number} number A number to format.
+ * @returns A number formatted with en_US locale.
+ */
+const getCurrencyUS = (number) => {
+ const formatted =
+ "$" +
+ Number.parseFloat(number)
+ .toFixed(2)
+ .replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
+ return formatted;
+};
+
+/**
+ * Get a number formatted based on a locale.
+ * @param {Number} number A number to format.
+ * @param {String} format A language code.
+ * @returns A formatted number.
+ */
+const getCurrencyFormat = (number, format) => {
+ switch (format) {
+ case "fr-FR":
+ return getCurrencyFR(number);
+ case "en-US":
+ return getCurrencyUS(number);
+ default:
+ console.log("Not supported!");
+ break;
+ }
+};
+
+export default getCurrencyFormat;
diff --git a/public/projects/js-small-apps/budget-app/style.css b/public/projects/js-small-apps/budget-app/style.css
new file mode 100644
index 0000000..009fbb7
--- /dev/null
+++ b/public/projects/js-small-apps/budget-app/style.css
@@ -0,0 +1,366 @@
+/*
+ * Base
+ */
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ background: hsl(207, 52%, 95%);
+ color: #000;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 1rem;
+ line-height: 1.618;
+ display: flex;
+ flex-flow: column nowrap;
+ min-height: 100vh;
+ max-width: 100vw;
+}
+
+button {
+ display: block;
+ font-family: inherit;
+ font-size: 100%;
+ line-height: 1.15;
+ cursor: pointer;
+}
+
+select {
+ cursor: pointer;
+}
+
+table {
+ width: 100%;
+}
+
+/*
+ * Layout
+ */
+
+.branding {
+ background: hsl(207, 35%, 90%);
+ border-bottom: 1px solid hsl(207, 35%, 80%);
+ box-shadow: 0 2px 0 1px hsl(207, 25%, 70%);
+ padding: 2rem 0;
+ margin-bottom: clamp(2rem, 3vw, 4rem);
+}
+
+.branding__title {
+ color: hsl(207, 85%, 27%);
+ font-size: clamp(1.9rem, 5vw, 2.2rem);
+ text-align: center;
+ text-shadow: 0 0 1px hsl(207, 100%, 50%);
+ text-transform: uppercase;
+}
+
+.footer {
+ background: hsl(207, 35%, 90%);
+ border-top: 1px solid hsl(207, 35%, 80%);
+ margin-top: clamp(3rem, 5vw, 4rem);
+ padding: 2rem 0 1.5rem;
+}
+
+.footer .btn--reset {
+ margin-bottom: 2rem;
+}
+
+.footer__copyright {
+ font-size: 0.9rem;
+ text-align: center;
+}
+
+.main {
+ flex: 1;
+ padding: 0 clamp(1rem, 3vw, 2rem);
+}
+
+.register {
+ display: flex;
+ flex-flow: column nowrap;
+ justify-content: center;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100vh;
+ background: hsl(202, 19%, 89%);
+}
+
+.register__form {
+ margin: auto;
+}
+
+.app {
+ display: none;
+}
+
+.app__title {
+ background: hsl(207, 25%, 92%);
+ border: 1px solid hsl(207, 25%, 70%);
+ box-shadow: 0 2px 0 1px hsl(207, 25%, 70%);
+ grid-column: 1 / -1;
+ color: hsl(207, 85%, 27%);
+ font-size: clamp(1.4rem, 4vw, 1.7rem);
+ margin-bottom: 1rem;
+ padding: 0.5rem 1rem;
+}
+
+.app__management {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, min(calc(100vw - 2 * 1rem), 21rem));
+ justify-content: center;
+ column-gap: 2rem;
+}
+
+.app__budget {
+ margin-bottom: clamp(1rem, 3vw, 2rem);
+}
+
+.form__fieldset {
+ border: 2px solid hsl(207, 85%, 27%);
+ margin-bottom: clamp(1rem, 3vw, 2rem);
+ padding: 0 1rem 1rem;
+}
+
+.form__legend {
+ background: hsl(207, 85%, 37%);
+ border: 2px solid hsl(207, 85%, 27%);
+ color: #fff;
+ font-weight: 600;
+ width: 100%;
+ margin-bottom: 1rem;
+ padding: 0.5rem 1rem;
+}
+
+.form__item:not(:last-child) {
+ margin-bottom: 1rem;
+}
+
+.form__label {
+ display: block;
+ cursor: pointer;
+ font-size: 0.8rem;
+ font-weight: 600;
+ letter-spacing: 1px;
+ text-transform: uppercase;
+ margin-bottom: 0.1rem;
+}
+
+.form__field,
+.form__select {
+ border: 1px solid hsl(207, 85%, 27%);
+ box-shadow: 2px 2px 0 0 hsl(0, 0%, 20%);
+ font-size: inherit;
+ padding: 0.5rem clamp(0.5rem, 3vw, 0.8rem);
+ width: 100%;
+ transition: all 0.3s ease-in-out 0s;
+}
+
+.form__field:hover,
+.form__field:focus,
+.form__select:hover,
+.form__select:focus {
+ transform: scaleX(1.05);
+}
+
+.form__field:focus,
+.form__select:focus {
+ outline: none;
+ box-shadow: 2px 2px 0 0 hsl(0, 0%, 20%), 1px 1px 0 4px hsl(207, 85%, 60%);
+}
+
+.manage-categories .form__item {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) max-content;
+ column-gap: 1rem;
+}
+
+.manage-categories .form__label {
+ grid-column: 1 / -1;
+}
+
+.manage-categories .form__select,
+.manage-categories .form__field {
+ grid-column: 1;
+}
+
+.manage-categories .form__submit {
+ grid-column: 2;
+}
+
+.table {
+ border: 2px solid hsl(207, 85%, 27%);
+ border-collapse: collapse;
+ width: 100%;
+ overflow-x: auto;
+}
+
+.table__header {
+ background: hsl(207, 85%, 37%);
+ color: #fff;
+ font-weight: 600;
+}
+
+.table__item {
+ border: 1px solid hsl(207, 85%, 27%);
+ padding: 0.5rem 0.7rem;
+ width: max-content;
+}
+
+.table__item .btn {
+ display: inline-flex;
+ margin: 0.5rem;
+}
+
+.app__budget .table {
+ table-layout: fixed;
+}
+
+.app__budget .table__item {
+ font-weight: 600;
+ text-align: center;
+}
+
+.app__budget .table__body {
+ font-size: 1.3rem;
+}
+
+.app__history {
+ max-width: 100%;
+ overflow-x: auto;
+}
+
+.app__history .app__title {
+ position: sticky;
+ left: 0;
+}
+
+/*
+ * Components
+ */
+
+.btn {
+ border-radius: 5px;
+ font-weight: 600;
+ margin: auto;
+ padding: 0.6rem 0.8rem;
+ transition: all 0.35s ease-in-out 0s;
+}
+
+.btn:hover,
+.btn:focus {
+ transform: scaleX(1.2) scaleY(1.1);
+}
+
+.btn:focus {
+ outline: none;
+}
+
+.btn:active {
+ transform: scale(0.95);
+}
+
+.btn--register {
+ background: hsl(202, 67%, 84%);
+ border: 2px solid hsl(202, 67%, 34%);
+ box-shadow: 2px 2px 0 0 hsl(202, 67%, 24%);
+}
+
+.btn--add,
+.btn--update {
+ background: hsl(120, 33%, 84%);
+ border: 2px solid hsl(120, 33%, 34%);
+ box-shadow: 2px 2px 0 0 hsl(120, 33%, 24%);
+}
+
+.btn--add:focus,
+.btn--update:focus {
+ box-shadow: 2px 2px 0 0 hsl(120, 33%, 24%), 1px 1px 0 4px hsl(120, 33%, 81%);
+}
+
+.btn--add:active,
+.btn--update:active {
+ box-shadow: 2px 2px 0 0 hsl(120, 33%, 24%);
+}
+
+.btn--delete,
+.btn--reset {
+ background: hsl(0, 33%, 84%);
+ border: 2px solid hsl(0, 33%, 34%);
+ box-shadow: 2px 2px 0 0 hsl(0, 33%, 24%);
+}
+
+.btn--delete:focus,
+.btn--reset:focus {
+ box-shadow: 2px 2px 0 0 hsl(120, 33%, 24%), 1px 1px 0 4px hsl(0, 33%, 81%);
+}
+
+.btn--delete:active,
+.btn--reset:active {
+ box-shadow: 2px 2px 0 0 hsl(0, 33%, 24%);
+}
+
+.btn--rename,
+.btn--edit {
+ background: hsl(193, 33%, 84%);
+ border: 2px solid hsl(193, 33%, 34%);
+ box-shadow: 2px 2px 0 0 hsl(193, 33%, 24%);
+}
+
+.btn--rename:focus,
+.btn--edit:focus {
+ box-shadow: 2px 2px 0 0 hsl(120, 33%, 24%), 1px 1px 0 4px hsl(193, 33%, 81%);
+}
+
+.btn--delete:active,
+.btn--edit:active {
+ box-shadow: 2px 2px 0 0 hsl(193, 33%, 24%);
+}
+
+.notification {
+ background: #fff;
+ border: 3px solid hsl(207, 85%, 27%);
+ border-radius: 3px;
+ box-shadow: 1px 1px 3px 0 hsla(0, 0%, 0%, 0.7);
+ font-weight: 600;
+ padding: 1rem;
+}
+
+.notification--error {
+ color: hsl(0, 49%, 39%);
+ border-color: hsl(0, 49%, 39%);
+}
+
+.notification--info {
+ color: hsl(212, 76%, 38%);
+ border-color: hsl(212, 76%, 38%);
+}
+
+.notification--warning {
+ color: hsl(32, 76%, 38%);
+ border-color: hsl(32, 76%, 38%);
+}
+
+/*
+ * Media Queries
+ */
+@media screen and (min-width: 1200px) {
+ .main {
+ margin: auto;
+ max-width: 1200px;
+ }
+ .app__management {
+ grid-template-columns: repeat(3, min(calc(100vw - 2 * 1rem), 21rem));
+ }
+
+ .app__budget {
+ position: sticky;
+ top: 1rem;
+ height: max-content;
+ }
+}
diff --git a/public/projects/js-small-apps/calculator/README.md b/public/projects/js-small-apps/calculator/README.md
new file mode 100644
index 0000000..ed8883b
--- /dev/null
+++ b/public/projects/js-small-apps/calculator/README.md
@@ -0,0 +1,13 @@
+# Calculator
+
+A calculator implementation using HTML, CSS and JS.
+
+You can find more details about the implementation here: https://github.com/florinpop17/app-ideas/blob/master/Projects/1-Beginner/Calculator-App.md
+
+## Preview
+
+You can see a live preview here: https://demo.armandphilippot.com/#calculator
+
+## License
+
+This project is open-source and available under the [MIT License](../LICENSE).
diff --git a/public/projects/js-small-apps/calculator/app.js b/public/projects/js-small-apps/calculator/app.js
new file mode 100644
index 0000000..683a1e7
--- /dev/null
+++ b/public/projects/js-small-apps/calculator/app.js
@@ -0,0 +1,265 @@
+const appHistory = ["0"];
+
+/**
+ * Check if a value is numeric.
+ * @param {String} value - A value to test.
+ * @returns {Boolean} True if value is numeric; false otherwise.
+ */
+function isNumeric(value) {
+ return !isNaN(value);
+}
+
+/**
+ * Check if a value is an operation (+, -, *, /, =).
+ * @param {String} value - A value to test.
+ * @returns {Boolean} True if value is an operation; false otherwise.
+ */
+function isOperation(value) {
+ return "+-*/=".includes(value);
+}
+
+/**
+ * Check if the value exceeds the limit of 8 characters.
+ * @param {String} value - The value to test.
+ * @returns True if the length is greater than 8; false otherwise.
+ */
+function isDigitsLimitReached(value) {
+ const digitsPart = value.split(".")[0];
+ return digitsPart?.length > 8 ? true : false;
+}
+
+/**
+ * Check if the decimal part exceeds the limit of 3 characters.
+ * @param {String} value - The value to test.
+ * @returns True if the decimal part is greater than 3; false otherwise.
+ */
+function isDecimalLimitReached(value) {
+ const decimalPart = value.split(".")[1];
+ return decimalPart?.length > 3 ? true : false;
+}
+
+/**
+ * Retrieve the last history value.
+ * @returns {String} The last history input.
+ */
+function getLastHistoryInput() {
+ return appHistory.slice(-1)[0];
+}
+
+/**
+ * Update the calculator display.
+ * @param {String} value - The value to print.
+ */
+function updateDisplay(value) {
+ const display = document.querySelector(".calculator__display");
+
+ if (isDigitsLimitReached(value) || isDecimalLimitReached(value)) {
+ display.textContent = "ERR";
+ } else {
+ display.textContent = value;
+ }
+}
+
+/**
+ * Calculate the result of an operation.
+ * @param {Number} number1 - The left number of operation.
+ * @param {Number} number2 - The right number of operation.
+ * @param {String} operation - An operation (+, -, *, /).
+ * @returns {Number} The operation result.
+ */
+function calculate(number1, number2, operation) {
+ let result;
+
+ switch (operation) {
+ case "+":
+ result = number1 + number2;
+ break;
+ case "-":
+ result = number1 - number2;
+ break;
+ case "*":
+ result = number1 * number2;
+ break;
+ case "/":
+ result = number1 / number2;
+ default:
+ break;
+ }
+
+ return result;
+}
+
+/**
+ * Get the result of an operation.
+ * @returns {Number} The operation result.
+ */
+function getResult() {
+ const historyCopy = appHistory.slice(0);
+ const number2 = Number(historyCopy.pop());
+ const operation = historyCopy.pop();
+ const number1 = Number(historyCopy.pop());
+ const result = calculate(number1, number2, operation);
+
+ return result;
+}
+
+/**
+ * Handle digit input.
+ * @param {String} value - The digit value.
+ */
+function handleDigits(value) {
+ const lastInput = getLastHistoryInput();
+ const beforeLastInput = appHistory.slice(-2)[0];
+ let newInput;
+
+ if (isNaN(lastInput) || beforeLastInput === "=") {
+ newInput = value;
+ } else {
+ appHistory.pop();
+ newInput = lastInput === "0" ? value : `${lastInput}${value}`;
+ }
+
+ if (isDigitsLimitReached(newInput) || isDecimalLimitReached(newInput)) {
+ newInput = newInput.slice(0, -1);
+ }
+
+ appHistory.push(newInput);
+ updateDisplay(newInput);
+}
+
+/**
+ * Handle operation input.
+ * @param {String} value - The operation.
+ * @returns {void}
+ */
+function handleOperation(value) {
+ const lastInput = getLastHistoryInput();
+
+ if (isOperation(lastInput)) return;
+
+ const result = getResult();
+
+ if (result) {
+ appHistory.push("=");
+ appHistory.push(`${result}`);
+ updateDisplay(`${result}`);
+ }
+
+ if (value !== "=") appHistory.push(value);
+}
+
+/**
+ * Handle number sign.
+ * @returns {void}
+ */
+function handleNumberSign() {
+ const lastInput = getLastHistoryInput();
+ if (isNaN(lastInput)) return;
+
+ const sign = Math.sign(lastInput);
+ if (sign === 0) return;
+
+ appHistory.pop();
+ let newInput;
+
+ if (sign === 1) {
+ newInput = -Math.abs(lastInput);
+ } else if (sign === -1) {
+ newInput = Math.abs(lastInput);
+ }
+
+ appHistory.push(`${newInput}`);
+ updateDisplay(`${newInput}`);
+}
+
+/**
+ * Handle decimal.
+ */
+function handleDecimal() {
+ const lastInput = getLastHistoryInput();
+
+ if (lastInput.indexOf(".") === -1) {
+ appHistory.pop();
+ const newInput = `${lastInput}.`;
+ appHistory.push(newInput);
+ updateDisplay(newInput);
+ }
+}
+
+/**
+ * Clear the last input.
+ */
+function clear() {
+ appHistory.pop();
+
+ if (appHistory.length === 0) {
+ appHistory.push("0");
+ updateDisplay("0");
+ } else {
+ const reversedHistory = appHistory.slice(0).reverse();
+ const lastNumericInput = reversedHistory.find((input) => isNumeric(input));
+ updateDisplay(lastNumericInput);
+
+ let lastInput = getLastHistoryInput();
+
+ while (lastNumericInput !== lastInput) {
+ appHistory.pop();
+ lastInput = getLastHistoryInput();
+ }
+ }
+}
+
+/**
+ * Reset the calculator.
+ */
+function clearAll() {
+ appHistory.length = 0;
+ appHistory.push("0");
+ updateDisplay("0");
+}
+
+/**
+ * Dispatch the event to the right function.
+ * @param {MouseEvent} e - The click event.
+ */
+function dispatch(e) {
+ const id = e.target.id;
+ const type = id.split("-")[0];
+ const value = e.target.textContent.trim();
+
+ switch (type) {
+ case "digit":
+ handleDigits(value);
+ break;
+ case "operation":
+ handleOperation(value);
+ break;
+ case "sign":
+ handleNumberSign();
+ break;
+ case "dot":
+ handleDecimal();
+ break;
+ case "clear":
+ clear();
+ break;
+ case "clearall":
+ clearAll();
+ break;
+ default:
+ break;
+ }
+}
+
+/**
+ * Listen all calculator buttons.
+ */
+function listen() {
+ const buttons = document.getElementsByClassName("btn");
+ const buttonsArray = Array.from(buttons);
+ buttonsArray.forEach((btn) => {
+ btn.addEventListener("click", dispatch);
+ });
+}
+
+listen();
diff --git a/public/projects/js-small-apps/calculator/index.html b/public/projects/js-small-apps/calculator/index.html
new file mode 100644
index 0000000..a93f0c4
--- /dev/null
+++ b/public/projects/js-small-apps/calculator/index.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Calculator</title>
+ <link rel="stylesheet" href="style.css" />
+ </head>
+ <body>
+ <header class="header">
+ <h1 class="branding">Calculator</h1>
+ </header>
+ <main class="main">
+ <div class="calculator">
+ <div class="calculator__display">0</div>
+ <div class="calculator__entry-pad">
+ <div class="calculator__clear">
+ <button type="button" id="clear" class="btn btn--clear">C</button>
+ <button type="button" id="clearall" class="btn btn--clear">
+ AC
+ </button>
+ </div>
+ <div class="calculator__digits">
+ <button type="button" id="digit-9" class="btn btn--digits">
+ 9
+ </button>
+ <button type="button" id="digit-8" class="btn btn--digits">
+ 8
+ </button>
+ <button type="button" id="digit-7" class="btn btn--digits">
+ 7
+ </button>
+ <button type="button" id="digit-6" class="btn btn--digits">
+ 6
+ </button>
+ <button type="button" id="digit-5" class="btn btn--digits">
+ 5
+ </button>
+ <button type="button" id="digit-4" class="btn btn--digits">
+ 4
+ </button>
+ <button type="button" id="digit-3" class="btn btn--digits">
+ 3
+ </button>
+ <button type="button" id="digit-2" class="btn btn--digits">
+ 2
+ </button>
+ <button type="button" id="digit-1" class="btn btn--digits">
+ 1
+ </button>
+ <button type="button" id="digit-0" class="btn btn--digits">
+ 0
+ </button>
+ <button type="button" id="sign" class="btn btn--digits">+/-</button>
+ <button type="button" id="dot" class="btn btn--digits">.</button>
+ </div>
+ <div class="calculator__operations">
+ <button
+ type="button"
+ id="operation-divide"
+ class="btn btn--operation"
+ >
+ /
+ </button>
+ <button
+ type="button"
+ id="operation-multiply"
+ class="btn btn--operation"
+ >
+ *
+ </button>
+ <button
+ type="button"
+ id="operation-minus"
+ class="btn btn--operation"
+ >
+ -
+ </button>
+ <button
+ type="button"
+ id="operation-plus"
+ class="btn btn--operation"
+ >
+ +
+ </button>
+ <button
+ type="button"
+ id="operation-equal"
+ class="btn btn--operation"
+ >
+ =
+ </button>
+ </div>
+ </div>
+ </div>
+ </main>
+ <footer class="footer">
+ <p class="copyright">Calculator. MIT 2021. Armand Philippot.</p>
+ </footer>
+ <script src="app.js"></script>
+ </body>
+</html>
diff --git a/public/projects/js-small-apps/calculator/style.css b/public/projects/js-small-apps/calculator/style.css
new file mode 100644
index 0000000..89b5b31
--- /dev/null
+++ b/public/projects/js-small-apps/calculator/style.css
@@ -0,0 +1,147 @@
+*,
+*::after,
+*::before {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
+ Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
+ font-size: 16px;
+ line-height: 1.618;
+ display: flex;
+ flex-flow: column nowrap;
+ min-height: 100vh;
+}
+
+.header,
+.main,
+.footer {
+ width: min(calc(100vw - 2rem), 350px);
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.header,
+.main {
+ border: 3px solid hsl(219, 64%, 35%);
+ border-radius: 5px;
+ box-shadow: 2px 2px 0 0 hsl(219, 64%, 35%), 2px 2px 2px 0 hsl(219, 64%, 30%),
+ 3px 3px 3px 0 hsla(219, 64%, 25%, 0.65);
+}
+
+.header {
+ background: hsl(219, 64%, 35%);
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ color: hsl(0, 0%, 100%);
+ margin-top: 2rem;
+ padding: 0.5rem 1rem;
+}
+
+.branding {
+ font-size: 1.5rem;
+ font-variant: small-caps;
+ letter-spacing: 1px;
+ text-align: center;
+ text-shadow: 1px 1px 0 hsl(0, 0%, 65%), 2px 2px 2px hsl(0, 0%, 0%);
+}
+
+.main {
+ background: hsl(0, 0%, 97%);
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+
+ margin-bottom: 1rem;
+}
+
+.footer {
+ font-size: 0.9rem;
+ text-align: center;
+ margin-top: auto;
+ padding: 1rem 0;
+}
+
+.calculator {
+ padding: 1rem;
+}
+
+.calculator__display {
+ background: hsl(0, 0%, 100%);
+ border: 1px solid hsl(0, 0%, 60%);
+ border-radius: 2px;
+ box-shadow: inset 0 0 2px 0 hsl(0, 0%, 70%), 0 0 0 1px hsl(0, 0%, 75%);
+ font-size: clamp(2rem, 3vw, 2.5rem);
+ font-weight: 600;
+ text-align: right;
+ width: 100%;
+ margin-bottom: 1rem;
+ padding: 0.2rem 1rem;
+}
+
+.calculator__entry-pad {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ grid-template-rows: 1fr 6fr;
+ gap: 1rem;
+ justify-items: end;
+}
+
+.calculator__clear {
+ grid-column: 3 / 5;
+ grid-row: 1;
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: flex-end;
+ gap: 1rem;
+}
+
+.calculator__digits {
+ grid-column: 1 / 4;
+ grid-row: 2;
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 1rem;
+}
+
+.calculator__operations {
+ grid-column: 4;
+ grid-row: 2;
+ display: flex;
+ flex-flow: column nowrap;
+ gap: 1rem;
+}
+
+.btn {
+ display: block;
+ padding: clamp(0.1rem, 5vw, 0.3rem) clamp(1rem, 6vw, 1.5rem);
+ background: hsl(0, 0%, 95%);
+ border: 1px solid hsl(0, 0%, 85%);
+ border-radius: 3px;
+ box-shadow: 0 0 2px hsl(0, 0%, 80%), 0 0 0 2px hsl(0, 0%, 60%),
+ 1px 1px 0 3px hsl(0, 0%, 50%);
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+ cursor: pointer;
+ transition: all 0.15s ease-in-out 0s;
+}
+
+.btn:hover,
+.btn:focus {
+ background: hsl(0, 0%, 100%);
+}
+
+.btn:focus {
+ outline: 3px solid hsl(219, 64%, 35%);
+}
+
+.btn:active {
+ background: hsl(0, 0%, 90%);
+ box-shadow: 0 0 0 hsl(0, 0%, 80%), 0 0 0 2px hsl(0, 0%, 80%),
+ 0 0 0 3px hsl(0, 0%, 50%);
+ outline: none;
+ transform: translateX(2px) translateY(2px) scale(0.96);
+}
diff --git a/public/projects/js-small-apps/clock/README.md b/public/projects/js-small-apps/clock/README.md
new file mode 100644
index 0000000..7a89cc1
--- /dev/null
+++ b/public/projects/js-small-apps/clock/README.md
@@ -0,0 +1,19 @@
+# Clock
+
+What time is it?
+
+## Description
+
+You can see the current time in three formats:
+
+- analog clock
+- digital clock
+- text based
+
+## Preview
+
+You can see a live preview here: https://demo.armandphilippot.com/#clock
+
+## License
+
+This project is open-source and available under the [MIT License](../LICENSE).
diff --git a/public/projects/js-small-apps/clock/app.js b/public/projects/js-small-apps/clock/app.js
new file mode 100644
index 0000000..7ae2702
--- /dev/null
+++ b/public/projects/js-small-apps/clock/app.js
@@ -0,0 +1,135 @@
+function setDate(day, month, year) {
+ const div = document.getElementById("date");
+ div.textContent = `${day}/${month}/${year}`;
+}
+
+function get12Rotation(int) {
+ const hour = int > 12 ? int - 12 : int;
+ return (360 / 12) * hour;
+}
+
+function get60Rotation(int) {
+ return (360 / 60) * int;
+}
+
+function setSvgClockHours(hours) {
+ const clockHours = document.getElementById("svg-clock_hours");
+ clockHours.style.transform = `rotate(${get12Rotation(hours)}deg)`;
+ clockHours.style.transformOrigin = "center";
+}
+
+function setSvgClockMinutes(minutes) {
+ const clockMinutes = document.getElementById("svg-clock_minutes");
+ clockMinutes.style.transform = `rotate(${get60Rotation(minutes)}deg)`;
+ clockMinutes.style.transformOrigin = "center";
+}
+
+function setSvgClockSeconds(seconds) {
+ const clockSeconds = document.getElementById("svg-clock_seconds");
+ clockSeconds.style.transform = `rotate(${get60Rotation(seconds)}deg)`;
+ clockSeconds.style.transformOrigin = "center";
+}
+
+function setDigitalClockHours(hours) {
+ const clockHours = document.getElementById("digital-clock_hours");
+ clockHours.textContent = hours;
+}
+
+function setDigitalClockMinutes(minutes) {
+ const formatted = minutes < 10 ? `0` + minutes : minutes;
+ const clockMinutes = document.getElementById("digital-clock_minutes");
+ clockMinutes.textContent = formatted;
+}
+
+function getHoursToString(hours) {
+ const hoursToText = [
+ "noon",
+ "one",
+ "two",
+ "three",
+ "four",
+ "five",
+ "six",
+ "seven",
+ "eight",
+ "nine",
+ "ten",
+ "eleven",
+ "twelve",
+ ];
+
+ return hoursToText[hours % 12];
+}
+
+function getMinutesToString(minutes) {
+ const ones = [
+ "",
+ "one",
+ "two",
+ "three",
+ "four",
+ "five",
+ "six",
+ "seven",
+ "eight",
+ "nine",
+ ];
+ const teens = [
+ "ten",
+ "eleven",
+ "twelve",
+ "thirteen",
+ "fourteen",
+ "fifteen",
+ "sixteen",
+ "seventeen",
+ "eighteen",
+ "nineteen",
+ ];
+ const tens = ["", "", "twenty", "thirty", "forty", "fifty"];
+ const minutesToArray = minutes.toString().split("");
+ let text = "";
+
+ if (minutes < 10) {
+ text = ones[minutes];
+ } else if (minutes < 20) {
+ text = teens[minutesToArray[1]];
+ } else {
+ text = `${tens[minutesToArray[0]]} ${ones[minutesToArray[1]]}`;
+ }
+
+ return text;
+}
+
+function setTextClock(hours, minutes) {
+ const div = document.getElementById("text-clock");
+ const meridiem = hours < 12 ? "am" : "pm";
+ div.textContent = `It's ${getHoursToString(hours)} ${getMinutesToString(
+ minutes
+ )} ${meridiem}.`;
+}
+
+function updateAll() {
+ const now = new Date();
+ const [month, day, year] = [
+ now.getMonth() + 1,
+ now.getDate(),
+ now.getFullYear(),
+ ];
+ const [hours, minutes, seconds] = [
+ now.getHours(),
+ now.getMinutes(),
+ now.getSeconds(),
+ ];
+
+ setDate(day, month, year);
+ setSvgClockHours(hours);
+ setSvgClockMinutes(minutes);
+ setSvgClockSeconds(seconds);
+ setDigitalClockHours(hours);
+ setDigitalClockMinutes(minutes);
+ setTextClock(hours, minutes);
+}
+
+updateAll();
+setInterval(updateAll, 1000);
diff --git a/public/projects/js-small-apps/clock/index.html b/public/projects/js-small-apps/clock/index.html
new file mode 100644
index 0000000..9174170
--- /dev/null
+++ b/public/projects/js-small-apps/clock/index.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>What time is it?</title>
+ <link rel="stylesheet" href="style.css" />
+ </head>
+ <body>
+ <header class="header">
+ <h1>What time is it?</h1>
+ </header>
+ <main class="app">
+ <div id="date"></div>
+ <svg
+ id="svg-clock"
+ viewBox="0 0 135.46667 135.46667"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ id="path846"
+ fill="#ffffff"
+ stroke="#000000"
+ stroke-width="2.5"
+ d="M 134.20638,67.73333 A 66.473045,66.473045 0 0 1 67.73333,134.20638 66.473045,66.473045 0 0 1 1.2602844,67.73333 66.473045,66.473045 0 0 1 67.73333,1.2602844 66.473045,66.473045 0 0 1 134.20638,67.73333 Z"
+ />
+ <path
+ d="m 71.242395,67.733337 a 3.509058,3.509058 0 0 1 -3.509058,3.509058 3.509058,3.509058 0 0 1 -3.509058,-3.509058 3.509058,3.509058 0 0 1 3.509058,-3.509058 3.509058,3.509058 0 0 1 3.509058,3.509058 z"
+ />
+ <path
+ id="svg-clock_seconds"
+ d="M 67.265175,4.7740932 H 68.20149 V 68.316688 h -0.936315 z"
+ />
+ <path
+ id="svg-clock_minutes"
+ d="m 66.797363,16.601835 h 1.871941 V 68.31669 h -1.871941 z"
+ />
+ <path
+ id="svg-clock_hours"
+ d="m 65.857079,29.393064 h 3.752515 V 68.31669 h -3.752515 z"
+ />
+ <path
+ id="path1522-7"
+ d="m 67.733338,127.0842 -3.736107,8.23531 h 7.472208 z"
+ />
+ <path
+ id="path1522-7-3"
+ d="M 8.3966895,67.733338 0.16137954,63.997231 v 7.472208 z"
+ />
+ <path
+ id="path1522-7-3-5"
+ d="m 127.04505,67.733338 8.23531,-3.736107 v 7.472208 z"
+ />
+ <path
+ id="path1522-7-5"
+ d="m 67.73333,8.4010979 -3.7361,-8.23531003 h 7.47221 z"
+ />
+ </svg>
+ <div id="digital-clock">
+ <div id="digital-clock_hours" class="digital-clock__value">00</div>
+ <div class="digital-clock__sep">:</div>
+ <div id="digital-clock_minutes" class="digital-clock__value">00</div>
+ </div>
+ <div id="text-clock"></div>
+ </main>
+ <footer class="footer">
+ What time is it?. MIT 2021. Armand Philippot.
+ </footer>
+ <script src="app.js"></script>
+ </body>
+</html>
diff --git a/public/projects/js-small-apps/clock/style.css b/public/projects/js-small-apps/clock/style.css
new file mode 100644
index 0000000..f1327e1
--- /dev/null
+++ b/public/projects/js-small-apps/clock/style.css
@@ -0,0 +1,75 @@
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ padding: 0;
+ margin: 0;
+}
+
+body {
+ display: flex;
+ flex-flow: column nowrap;
+ min-height: 100vh;
+ font-size: 16px;
+ font-size: 1rem;
+ font-family: Georgia, "Times New Roman", Times, serif;
+}
+
+.header,
+.app,
+.footer {
+ width: min(calc(100vw - 2rem), 800px);
+ margin: auto;
+}
+
+.header {
+ font-size: 2rem;
+ text-align: center;
+ margin-bottom: 2rem;
+ padding: 2rem 0;
+}
+
+.footer {
+ font-size: 0.9rem;
+ text-align: center;
+ margin-top: 3rem;
+ padding: 2rem 0;
+}
+
+.app {
+ flex: 1;
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ justify-content: center;
+ gap: clamp(1rem, 5vw, 3rem);
+}
+
+#date {
+ flex: 0 0 100%;
+ font-size: 1.5rem;
+ text-align: center;
+ padding: 1rem 0;
+}
+
+#svg-clock {
+ width: 20rem;
+ height: 20rem;
+}
+
+#digital-clock {
+ display: flex;
+ flex-flow: row nowrap;
+ gap: 0.5rem;
+ width: max-content;
+ padding: 2rem;
+ border: 5px solid #000;
+ font-family: Verdana, Geneva, Tahoma, sans-serif;
+ font-size: 2rem;
+ font-weight: 600;
+}
+
+#text-clock {
+ flex: 0 0 100%;
+ text-align: center;
+}
diff --git a/public/projects/js-small-apps/color-cycle/README.md b/public/projects/js-small-apps/color-cycle/README.md
new file mode 100644
index 0000000..e9d5970
--- /dev/null
+++ b/public/projects/js-small-apps/color-cycle/README.md
@@ -0,0 +1,13 @@
+# Color Cycle
+
+An app to cycle a color value through incremental changes. In other words: play with hexadecimal colors!
+
+You can find more details about the implementation here: https://github.com/florinpop17/app-ideas/blob/master/Projects/1-Beginner/Color-Cycle-App.md
+
+## Preview
+
+You can see a live preview here: https://demo.armandphilippot.com/#color-cycle
+
+## License
+
+This project is open-source and available under the [MIT License](../LICENSE).
diff --git a/public/projects/js-small-apps/color-cycle/app.js b/public/projects/js-small-apps/color-cycle/app.js
new file mode 100644
index 0000000..2ed8e7f
--- /dev/null
+++ b/public/projects/js-small-apps/color-cycle/app.js
@@ -0,0 +1,208 @@
+let isRunning = false;
+let intervalId;
+
+/**
+ * Check if the provided value is a valid hexadecimal.
+ * @param {String} value - Two hexadecimal symbols.
+ * @returns {Boolean} True if value is a valid hexadecimal ; false otherwise.
+ */
+function isValidHex(value) {
+ const hexSymbols = "0123456789ABCDEF";
+ const hexArray = value.split("");
+ if (hexArray.length !== 2) return false;
+ if (!hexSymbols.includes(hexArray[0].toUpperCase())) return false;
+ if (!hexSymbols.includes(hexArray[1].toUpperCase())) return false;
+ return true;
+}
+
+/**
+ * Print a message to notify user.
+ * @param {String} msg A message to print.
+ */
+function notify(msg) {
+ const body = document.querySelector(".body");
+ const notification = document.createElement("div");
+ notification.classList.add("notification");
+ notification.textContent = msg;
+ body.appendChild(notification);
+
+ setTimeout(() => {
+ notification.remove();
+ }, 3000);
+}
+
+/**
+ * Check if colors are set and are valid hexadecimal.
+ * @param {String} red - Hexadecimal string for red color.
+ * @param {String} green - Hexadecimal string for green color.
+ * @param {String} blue - Hexadecimal string for blue color.
+ * @returns {Boolean} True if colors are ready; false otherwise.
+ */
+function areColorsReady(red, green, blue) {
+ if (!red || !green || !blue) return false;
+
+ if (!isValidHex(red)) {
+ notify("Red is not hexadecimal.");
+ return false;
+ }
+
+ if (!isValidHex(green)) {
+ notify("Green is not hexadecimal.");
+ return false;
+ }
+
+ if (!isValidHex(blue)) {
+ notify("Blue is not hexadecimal.");
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Update the preview color with the provided color.
+ * @param {String} color - Hexadecimal string with a leading hash (CSS format).
+ */
+function updatePreviewColor(color) {
+ const preview = document.querySelector(".preview");
+ preview.style.backgroundColor = color;
+}
+
+/**
+ * Initialize the preview with user settings.
+ * @param {Object} ui - The different element corresponding to the user interface.
+ */
+function setPreview(ui) {
+ const red = ui.colors.red.value;
+ const green = ui.colors.green.value;
+ const blue = ui.colors.blue.value;
+
+ if (areColorsReady(red, green, blue)) {
+ const currentColor = `#${red}${green}${blue}`;
+ updatePreviewColor(currentColor);
+ }
+}
+
+/**
+ * Generate a new color.
+ * @param {Integer} color - An integer corresponding to a RGB value.
+ * @param {Integer} increment - Add this value to color argument.
+ */
+function* getColor(color, increment) {
+ let nextColor = color;
+ yield nextColor;
+
+ while (true) {
+ if (nextColor + increment > 255) {
+ nextColor = nextColor - 255 + increment;
+ } else {
+ nextColor = nextColor + increment;
+ }
+
+ yield nextColor;
+ }
+}
+
+/**
+ * Convert an RGB color to hexadecimal format.
+ * @param {Integer} red - An integer representing red color with RGB format.
+ * @param {Integer} green - An integer representing green color with RGB format.
+ * @param {Integer} blue - A value representing blue color with RGB format.
+ * @returns {String} The color in hexadecimal format with a leading hash.
+ */
+function getNexHexColor(red, green, blue) {
+ return `#${red.toString(16)}${green.toString(16)}${blue.toString(16)}`;
+}
+
+/**
+ * Disable or enable inputs.
+ * @param {Object} ui - The HTMLElements corresponding to user interface.
+ */
+function toggleInputs(ui) {
+ ui.colors.red.disabled = isRunning;
+ ui.colors.green.disabled = isRunning;
+ ui.colors.blue.disabled = isRunning;
+ ui.increments.red.disabled = isRunning;
+ ui.increments.green.disabled = isRunning;
+ ui.increments.blue.disabled = isRunning;
+ ui.interval.disabled = isRunning;
+}
+
+/**
+ * Start or stop the preview.
+ * @param {Object} ui - The HTMLElements corresponding to user interface.
+ */
+function start(ui) {
+ const red = ui.colors.red.value;
+ const green = ui.colors.green.value;
+ const blue = ui.colors.blue.value;
+
+ if (areColorsReady(red, green, blue)) {
+ isRunning = !isRunning;
+ ui.button.textContent = isRunning ? "Stop" : "Start";
+ } else {
+ notify("Colors are not correctly set.");
+ }
+
+ const redIncrement = Number(ui.increments.red.value);
+ const greenIncrement = Number(ui.increments.green.value);
+ const blueIncrement = Number(ui.increments.blue.value);
+ const redGenerator = getColor(parseInt(red, 16), redIncrement);
+ const greenGenerator = getColor(parseInt(green, 16), greenIncrement);
+ const blueGenerator = getColor(parseInt(blue, 16), blueIncrement);
+ const timing = ui.interval.value;
+
+ if (isRunning) {
+ toggleInputs(ui);
+ intervalId = setInterval(() => {
+ const nextRed = redGenerator.next().value;
+ const nextGreen = greenGenerator.next().value;
+ const nextBlue = blueGenerator.next().value;
+ const newColor = getNexHexColor(nextRed, nextGreen, nextBlue);
+ updatePreviewColor(newColor);
+ }, timing);
+ } else {
+ toggleInputs(ui);
+ clearInterval(intervalId);
+ }
+}
+
+/**
+ * Listen form, buttons and inputs.
+ * @param {Object} ui - The HTMLElements corresponding to user interface.
+ */
+function listen(ui) {
+ ui.form.addEventListener("submit", (e) => {
+ e.preventDefault();
+ });
+ ui.button.addEventListener("click", () => start(ui));
+ ui.colors.red.addEventListener("change", () => setPreview(ui));
+ ui.colors.green.addEventListener("change", () => setPreview(ui));
+ ui.colors.blue.addEventListener("change", () => setPreview(ui));
+}
+
+/**
+ * Initialize the app.
+ */
+function init() {
+ const ui = {
+ button: document.querySelector(".btn"),
+ form: document.querySelector(".form"),
+ colors: {
+ red: document.getElementById("color-red"),
+ green: document.getElementById("color-green"),
+ blue: document.getElementById("color-blue"),
+ },
+ increments: {
+ red: document.getElementById("increment-red"),
+ green: document.getElementById("increment-green"),
+ blue: document.getElementById("increment-blue"),
+ },
+ interval: document.getElementById("time-interval"),
+ };
+
+ setPreview(ui);
+ listen(ui);
+}
+
+init();
diff --git a/public/projects/js-small-apps/color-cycle/index.html b/public/projects/js-small-apps/color-cycle/index.html
new file mode 100644
index 0000000..bb99787
--- /dev/null
+++ b/public/projects/js-small-apps/color-cycle/index.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Color Cycle</title>
+ <link rel="stylesheet" href="style.css" />
+ </head>
+ <body class="body">
+ <header class="header">
+ <h1 class="branding">Color Cycle</h1>
+ </header>
+ <main class="main">
+ <div class="color-cycle">
+ <div class="color-cycle__preview">
+ <div class="preview"></div>
+ <button class="btn" type="button">Start</button>
+ </div>
+ <form action="" class="color-cycle__settings form">
+ <fieldset class="form__fieldset">
+ <legend class="form__legend">Hex Color</legend>
+ <div class="form__item">
+ <label for="color-red" class="form__label">Red</label
+ ><input
+ type="text"
+ name="color-red"
+ id="color-red"
+ class="form__input"
+ size="2"
+ value="FF"
+ />
+ </div>
+ <div class="form__item">
+ <label for="color-green" class="form__label">Green</label
+ ><input
+ type="text"
+ name="color-green"
+ id="color-green"
+ class="form__input"
+ size="2"
+ value="FF"
+ />
+ </div>
+ <div class="form__item">
+ <label for="color-blue" class="form__label">Blue</label
+ ><input
+ type="text"
+ name="color-blue"
+ id="color-blue"
+ class="form__input"
+ size="2"
+ value="FF"
+ />
+ </div>
+ </fieldset>
+ <fieldset class="form__fieldset">
+ <legend class="form__legend">Increment</legend>
+ <div class="form__item">
+ <label for="increment-red" class="form__label">Red</label
+ ><input
+ type="number"
+ name="increment-red"
+ id="increment-red"
+ class="form__input"
+ size="2"
+ value="0"
+ />
+ </div>
+ <div class="form__item">
+ <label for="increment-green" class="form__label">Green</label
+ ><input
+ type="number"
+ name="increment-green"
+ id="increment-green"
+ class="form__input"
+ size="2"
+ value="0"
+ />
+ </div>
+ <div class="form__item">
+ <label for="increment-blue" class="form__label">Blue</label
+ ><input
+ type="number"
+ name="increment-blue"
+ id="increment-blue"
+ class="form__input"
+ size="2"
+ value="0"
+ />
+ </div>
+ </fieldset>
+ <fieldset class="form__fieldset">
+ <legend class="form__legend">Time</legend>
+ <div class="form__item">
+ <label for="time-interval" class="form__label"
+ >Interval (ms)</label
+ >
+ <input
+ type="range"
+ name="time-interval"
+ id="time-interval"
+ min="1"
+ max="1000"
+ value="250"
+ />
+ </div>
+ </fieldset>
+ </form>
+ </div>
+ </main>
+ <footer class="footer">
+ <p class="copyright">Color Cycle. MIT 2021. Armand Philippot.</p>
+ </footer>
+ <script src="app.js"></script>
+ </body>
+</html>
diff --git a/public/projects/js-small-apps/color-cycle/style.css b/public/projects/js-small-apps/color-cycle/style.css
new file mode 100644
index 0000000..62e0e47
--- /dev/null
+++ b/public/projects/js-small-apps/color-cycle/style.css
@@ -0,0 +1,132 @@
+*,
+*::after,
+*::before {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ background: #fff;
+ color: #000;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
+ Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
+ font-size: 16px;
+ line-height: 1.618;
+ display: flex;
+ flex-flow: column nowrap;
+ min-height: 100vh;
+}
+
+.main {
+ flex: 1;
+ margin: 3rem;
+ position: relative;
+}
+
+.header,
+.main,
+.footer {
+ width: min(calc(100vw - 2rem), 80ch);
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.header,
+.footer {
+ padding: 1rem 0;
+}
+
+.branding {
+ color: hsl(219, 64%, 35%);
+ text-align: center;
+}
+
+.copyright {
+ font-size: 0.9rem;
+ text-align: center;
+}
+
+.color-cycle {
+ display: flex;
+ flex-flow: row wrap;
+ gap: 2rem;
+ align-items: center;
+}
+
+.color-cycle__preview,
+.color-cycle__settings {
+ flex: 1 1 min(calc(100vw - 2rem), calc(80ch / 2 - 1rem));
+}
+
+.preview {
+ border: 1px solid #ccc;
+ height: 20rem;
+ margin-bottom: 2rem;
+}
+
+.form__fieldset {
+ display: flex;
+ flex-flow: row wrap;
+ gap: 1rem;
+ margin: 1rem 0;
+ padding: 0.8rem 1rem 1.2rem;
+}
+
+.form__legend {
+ color: hsl(219, 64%, 35%);
+ font-size: 0.9rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ padding: 0 0.8rem;
+}
+
+.form__label {
+ display: block;
+ cursor: pointer;
+}
+
+.form__input {
+ padding: 0.2rem 0.5rem;
+}
+
+.btn,
+.form__input {
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+}
+
+.btn {
+ background: #fff;
+ border: 2px solid hsl(219, 64%, 35%);
+ border-radius: 5px;
+ color: hsl(219, 64%, 35%);
+ font-weight: 600;
+ cursor: pointer;
+ display: block;
+ margin: auto;
+ padding: 0.5rem 1rem;
+}
+
+.btn:hover,
+.btn:focus {
+ background: hsl(219, 64%, 35%);
+ color: #fff;
+}
+
+.btn:active {
+ background: hsl(219, 64%, 25%);
+ color: #fff;
+}
+
+.notification {
+ background: #fff;
+ border: 2px solid hsl(0, 64%, 35%);
+ color: hsl(0, 64%, 35%);
+ font-weight: 600;
+ padding: 1rem;
+ position: absolute;
+ right: 1rem;
+ bottom: 1rem;
+}
diff --git a/public/projects/js-small-apps/commitlint.config.js b/public/projects/js-small-apps/commitlint.config.js
new file mode 100644
index 0000000..28fe5c5
--- /dev/null
+++ b/public/projects/js-small-apps/commitlint.config.js
@@ -0,0 +1 @@
+module.exports = {extends: ['@commitlint/config-conventional']}
diff --git a/public/projects/js-small-apps/css-border-previewer/README.md b/public/projects/js-small-apps/css-border-previewer/README.md
new file mode 100644
index 0000000..24a4566
--- /dev/null
+++ b/public/projects/js-small-apps/css-border-previewer/README.md
@@ -0,0 +1,13 @@
+# CSS Border Previewer
+
+An app to preview how a shapes looks when you change CSS border properties.
+
+You can find more details about the implementation here: https://github.com/florinpop17/app-ideas/blob/master/Projects/1-Beginner/Border-Radius-Previewer.md but I have decided to not limit myself to border-radius.
+
+## Preview
+
+You can see a live preview here: https://demo.armandphilippot.com/#css-border-previewer
+
+## License
+
+This project is open-source and available under the [MIT License](../LICENSE).
diff --git a/public/projects/js-small-apps/css-border-previewer/app.js b/public/projects/js-small-apps/css-border-previewer/app.js
new file mode 100644
index 0000000..583703d
--- /dev/null
+++ b/public/projects/js-small-apps/css-border-previewer/app.js
@@ -0,0 +1,847 @@
+/**
+ * Retrieve the border color property name depending on direction.
+ * @param {String} direction - Either `top`, `right`, `left` or `bottom`.
+ * @returns {String} The CSS property name.
+ */
+function getBorderColorProperty(direction) {
+ let borderColorProperty;
+
+ switch (direction) {
+ case "top":
+ borderColorProperty = "borderTopColor";
+ break;
+ case "right":
+ borderColorProperty = "borderRightColor";
+ break;
+ case "bottom":
+ borderColorProperty = "borderBottomColor";
+ break;
+ case "left":
+ borderColorProperty = "borderLeftColor";
+ break;
+ default:
+ borderColorProperty = "borderColor";
+ break;
+ }
+
+ return borderColorProperty;
+}
+
+/**
+ * Retrieve the border style property name depending on direction.
+ * @param {String} direction - Either `top`, `right`, `bottom` or `left`.
+ * @returns {String} The CSS property name.
+ */
+function getBorderStyleProperty(direction) {
+ let borderStyleProperty;
+
+ switch (direction) {
+ case "top":
+ borderStyleProperty = "borderTopStyle";
+ break;
+ case "right":
+ borderStyleProperty = "borderRightStyle";
+ break;
+ case "bottom":
+ borderStyleProperty = "borderBottomStyle";
+ break;
+ case "left":
+ borderStyleProperty = "borderLeftStyle";
+ break;
+ default:
+ borderStyleProperty = "borderStyle";
+ break;
+ }
+
+ return borderStyleProperty;
+}
+
+/**
+ * Retrieve the border width property name depending on direction.
+ * @param {String} direction - Either `top`, `right`, `left` or `bottom`.
+ * @returns {String} The CSS property name.
+ */
+function getBorderWidthProperty(direction) {
+ let borderWidthProperty;
+
+ switch (direction) {
+ case "top":
+ borderWidthProperty = "borderTopWidth";
+ break;
+ case "right":
+ borderWidthProperty = "borderRightWidth";
+ break;
+ case "bottom":
+ borderWidthProperty = "borderBottomWidth";
+ break;
+ case "left":
+ borderWidthProperty = "borderLeftWidth";
+ break;
+ default:
+ borderWidthProperty = "borderWidth";
+ break;
+ }
+
+ return borderWidthProperty;
+}
+
+/**
+ * Apply the custom border to an element.
+ * @param {HTMLElement} el - Apply border to this element.
+ * @param {String} property - Either `color`, `style` or `width`.
+ * @param {String} value - The value to apply.
+ * @param {String} [direction] - Either `top`, `right`, `bottom` or `left`.
+ */
+function setBorder(el, property, value, direction = null) {
+ let borderProperty;
+
+ switch (property) {
+ case "color":
+ borderProperty = getBorderColorProperty(direction);
+ break;
+ case "style":
+ borderProperty = getBorderStyleProperty(direction);
+ break;
+ case "width":
+ borderProperty = getBorderWidthProperty(direction);
+ default:
+ break;
+ }
+
+ el.style[borderProperty] = value;
+}
+
+/**
+ * Apply the custom border radius to an element.
+ * @param {HTMLElement} el - Apply border radius to this element.
+ * @param {String} firstRadius - The first radius value.
+ * @param {String} [secondRadius] - The second radius value.
+ * @param {String} [x] - The horizontal direction: either `right` or `left`.
+ * @param {String} [y] - The vertical direction: either `top` or `bottom`.
+ */
+function setBorderRadius(el, firstRadius, secondRadius, x = null, y = null) {
+ const direction = `${x}-${y}`;
+ const value = `${firstRadius}${secondRadius ? ` / ${secondRadius}` : ""}`;
+ let borderRadiusProperty;
+
+ switch (direction) {
+ case "left-top":
+ borderRadiusProperty = "borderTopLeftRadius";
+ break;
+ case "right-top":
+ borderRadiusProperty = "borderTopRightRadius";
+ break;
+ case "left-bottom":
+ borderRadiusProperty = "borderBottomLeftRadius";
+ break;
+ case "right-bottom":
+ borderRadiusProperty = "borderBottomRightRadius";
+ break;
+ default:
+ borderRadiusProperty = "borderRadius";
+ break;
+ }
+
+ el.style[borderRadiusProperty] = value;
+}
+
+/**
+ * Display the corresponding border settings.
+ * @param {String} string - Either `common` or `individual`.
+ */
+function toggleBorderSettingsDisplay(string) {
+ const allBordersFieldset = document.getElementById("fieldset-borders");
+ const topBorderFieldset = document.getElementById("fieldset-border-top");
+ const rightBorderFieldset = document.getElementById("fieldset-border-right");
+ const bottomBorderFieldset = document.getElementById(
+ "fieldset-border-bottom"
+ );
+ const leftBorderFieldset = document.getElementById("fieldset-border-left");
+
+ if (string === "common") {
+ allBordersFieldset.style.display = "";
+ topBorderFieldset.style.display = "none";
+ rightBorderFieldset.style.display = "none";
+ bottomBorderFieldset.style.display = "none";
+ leftBorderFieldset.style.display = "none";
+ } else {
+ allBordersFieldset.style.display = "none";
+ topBorderFieldset.style.display = "";
+ rightBorderFieldset.style.display = "";
+ bottomBorderFieldset.style.display = "";
+ leftBorderFieldset.style.display = "";
+ }
+}
+
+/**
+ * Display the corresponding border-radius settings.
+ * @param {String} string - Either `common` or `individual`.
+ */
+function toggleBorderRadiusSettingsDisplay(string) {
+ const allBordersRadiusFieldset = document.getElementById(
+ "fieldset-borders-radius"
+ );
+ const topLeftBorderRadiusFieldset = document.getElementById(
+ "fieldset-border-top-left-radius"
+ );
+ const topRightBorderRadiusFieldset = document.getElementById(
+ "fieldset-border-top-right-radius"
+ );
+ const bottomLeftBorderRadiusFieldset = document.getElementById(
+ "fieldset-border-bottom-left-radius"
+ );
+ const bottomRightBorderRadiusFieldset = document.getElementById(
+ "fieldset-border-bottom-right-radius"
+ );
+
+ if (string === "common") {
+ allBordersRadiusFieldset.style.display = "";
+ topLeftBorderRadiusFieldset.style.display = "none";
+ topRightBorderRadiusFieldset.style.display = "none";
+ bottomLeftBorderRadiusFieldset.style.display = "none";
+ bottomRightBorderRadiusFieldset.style.display = "none";
+ } else {
+ allBordersRadiusFieldset.style.display = "none";
+ topLeftBorderRadiusFieldset.style.display = "";
+ topRightBorderRadiusFieldset.style.display = "";
+ bottomLeftBorderRadiusFieldset.style.display = "";
+ bottomRightBorderRadiusFieldset.style.display = "";
+ }
+}
+
+/**
+ * Print the generated code into the given element.
+ * @param {HTMLElement} el - The element where to print generated code.
+ */
+function printCode(el) {
+ const code = document.querySelector(".result__code");
+ let codeOutput = `
+.box {\n`;
+
+ for (const property of el.style) {
+ codeOutput += `\t${property}: ${el.style[property]};\n`;
+ }
+
+ codeOutput += "}";
+ code.textContent = codeOutput;
+}
+
+/**
+ * Check which type of settings is checked.
+ * @param {String} radioValue - The input radio value.
+ * @returns {Boolean} True if is individual; false if is common.
+ */
+function isIndividualSettings(radioValue) {
+ return radioValue === "true" ? true : false;
+}
+
+/**
+ * Set all borders to a given element.
+ * @param {HTMLElement} el - Apply border to this element.
+ */
+function setCommonBorder(el) {
+ const allBordersColorInput = document.getElementById("borders-color");
+ const allBordersStyleSelect = document.getElementById("borders-style");
+ const allBordersUnitSelect = document.getElementById("borders-unit");
+ const allBordersWidthInput = document.getElementById("borders-width");
+
+ setBorder(el, "color", allBordersColorInput.value);
+ setBorder(el, "style", allBordersStyleSelect.value);
+ setBorder(
+ el,
+ "width",
+ `${allBordersWidthInput.value}${allBordersUnitSelect.value}`
+ );
+
+ allBordersColorInput.addEventListener("input", () => {
+ setBorder(el, "color", allBordersColorInput.value);
+ printCode(el);
+ });
+
+ allBordersStyleSelect.addEventListener("input", () => {
+ setBorder(el, "style", allBordersStyleSelect.value);
+ printCode(el);
+ });
+
+ allBordersUnitSelect.addEventListener("input", () => {
+ setBorder(
+ el,
+ "width",
+ `${allBordersWidthInput.value}${allBordersUnitSelect.value}`
+ );
+ printCode(el);
+ });
+
+ allBordersWidthInput.addEventListener("input", () => {
+ setBorder(
+ el,
+ "width",
+ `${allBordersWidthInput.value}${allBordersUnitSelect.value}`
+ );
+ printCode(el);
+ });
+}
+
+/**
+ * Set the top border to the given element.
+ * @param {HTMLElement} el - Apply the top border to this element.
+ */
+function setTopBorder(el) {
+ const topBorderColorInput = document.getElementById("border-top-color");
+ const topBorderStyleSelect = document.getElementById("border-top-style");
+ const topBorderUnitSelect = document.getElementById("border-top-unit");
+ const topBorderWidthInput = document.getElementById("border-top-width");
+
+ setBorder(el, "color", topBorderColorInput.value, "top");
+ setBorder(el, "style", topBorderStyleSelect.value, "top");
+ setBorder(
+ el,
+ "width",
+ `${topBorderWidthInput.value}${topBorderUnitSelect.value}`,
+ "top"
+ );
+
+ topBorderColorInput.addEventListener("input", () => {
+ setBorder(el, "color", topBorderColorInput.value, "top");
+ printCode(el);
+ });
+
+ topBorderStyleSelect.addEventListener("input", () => {
+ setBorder(el, "style", topBorderStyleSelect.value, "top");
+ printCode(el);
+ });
+
+ topBorderUnitSelect.addEventListener("input", () => {
+ setBorder(
+ el,
+ "width",
+ `${topBorderWidthInput.value}${topBorderUnitSelect.value}`,
+ "top"
+ );
+ printCode(el);
+ });
+
+ topBorderWidthInput.addEventListener("input", () => {
+ setBorder(
+ el,
+ "width",
+ `${topBorderWidthInput.value}${topBorderUnitSelect.value}`,
+ "top"
+ );
+ printCode(el);
+ });
+}
+
+/**
+ * Set the right border to the given element.
+ * @param {HTMLElement} el - Apply the right border to this element.
+ */
+function setRightBorder(el) {
+ const rightBorderWidthInput = document.getElementById("border-right-width");
+ const rightBorderUnitSelect = document.getElementById("border-right-unit");
+ const rightBorderStyleSelect = document.getElementById("border-right-style");
+ const rightBorderColorInput = document.getElementById("border-right-color");
+
+ setBorder(el, "color", rightBorderColorInput.value, "right");
+ setBorder(el, "style", rightBorderStyleSelect.value, "right");
+ setBorder(
+ el,
+ "width",
+ `${rightBorderWidthInput.value}${rightBorderUnitSelect.value}`,
+ "right"
+ );
+
+ rightBorderColorInput.addEventListener("input", () => {
+ setBorder(el, "color", rightBorderColorInput.value, "right");
+ printCode(el);
+ });
+
+ rightBorderStyleSelect.addEventListener("input", () => {
+ setBorder(el, "style", rightBorderStyleSelect.value, "right");
+ printCode(el);
+ });
+
+ rightBorderUnitSelect.addEventListener("input", () => {
+ setBorder(
+ el,
+ "width",
+ `${rightBorderWidthInput.value}${rightBorderUnitSelect.value}`,
+ "right"
+ );
+ printCode(el);
+ });
+
+ rightBorderWidthInput.addEventListener("input", () => {
+ setBorder(
+ el,
+ "width",
+ `${rightBorderWidthInput.value}${rightBorderUnitSelect.value}`,
+ "right"
+ );
+ printCode(el);
+ });
+}
+
+/**
+ * Set the bottom border to the given element.
+ * @param {HTMLElement} el - Apply the bottom border to this element.
+ */
+function setBottomBorder(el) {
+ const bottomBorderWidthInput = document.getElementById("border-bottom-width");
+ const bottomBorderUnitSelect = document.getElementById("border-bottom-unit");
+ const bottomBorderStyleSelect = document.getElementById(
+ "border-bottom-style"
+ );
+ const bottomBorderColorInput = document.getElementById("border-bottom-color");
+
+ setBorder(el, "color", bottomBorderColorInput.value, "bottom");
+ setBorder(el, "style", bottomBorderStyleSelect.value, "bottom");
+ setBorder(
+ el,
+ "width",
+ `${bottomBorderWidthInput.value}${bottomBorderUnitSelect.value}`,
+ "bottom"
+ );
+
+ bottomBorderColorInput.addEventListener("input", () => {
+ setBorder(el, "color", bottomBorderColorInput.value, "bottom");
+ printCode(el);
+ });
+
+ bottomBorderStyleSelect.addEventListener("input", () => {
+ setBorder(el, "style", bottomBorderStyleSelect.value, "bottom");
+ printCode(el);
+ });
+
+ bottomBorderUnitSelect.addEventListener("input", () => {
+ setBorder(
+ el,
+ "width",
+ `${bottomBorderWidthInput.value}${bottomBorderUnitSelect.value}`,
+ "bottom"
+ );
+ printCode(el);
+ });
+
+ bottomBorderWidthInput.addEventListener("input", () => {
+ setBorder(
+ el,
+ "width",
+ `${bottomBorderWidthInput.value}${bottomBorderUnitSelect.value}`,
+ "bottom"
+ );
+ printCode(el);
+ });
+}
+
+/**
+ * Set the left border to the given element.
+ * @param {HTMLElement} el - Apply the left border to this element.
+ */
+function setLeftBorder(el) {
+ const leftBorderWidthInput = document.getElementById("border-left-width");
+ const leftBorderUnitSelect = document.getElementById("border-left-unit");
+ const leftBorderStyleSelect = document.getElementById("border-left-style");
+ const leftBorderColorInput = document.getElementById("border-left-color");
+
+ setBorder(el, "color", leftBorderColorInput.value, "left");
+ setBorder(el, "style", leftBorderStyleSelect.value, "left");
+ setBorder(
+ el,
+ "width",
+ `${leftBorderWidthInput.value}${leftBorderUnitSelect.value}`,
+ "left"
+ );
+
+ leftBorderColorInput.addEventListener("input", () => {
+ setBorder(el, "color", leftBorderColorInput.value, "left");
+ printCode(el);
+ });
+
+ leftBorderStyleSelect.addEventListener("input", () => {
+ setBorder(el, "style", leftBorderStyleSelect.value, "left");
+ printCode(el);
+ });
+
+ leftBorderUnitSelect.addEventListener("input", () => {
+ setBorder(
+ el,
+ "width",
+ `${leftBorderWidthInput.value}${leftBorderUnitSelect.value}`,
+ "left"
+ );
+ printCode(el);
+ });
+
+ leftBorderWidthInput.addEventListener("input", () => {
+ setBorder(
+ el,
+ "width",
+ `${leftBorderWidthInput.value}${leftBorderUnitSelect.value}`,
+ "left"
+ );
+ printCode(el);
+ });
+}
+
+/**
+ * Set all borders radius to the given element.
+ * @param {HTMLElement} el - Apply the border radius to this element.
+ */
+function setCommonBorderRadius(el) {
+ const borderCommonFirstRadius = document.getElementById(
+ "borders-first-radius"
+ );
+ const borderCommonFirstRadiusUnit = document.getElementById(
+ "borders-first-radius-unit"
+ );
+ const borderCommonSecondRadius = document.getElementById(
+ "borders-second-radius"
+ );
+ const borderCommonSecondRadiusUnit = document.getElementById(
+ "borders-second-radius-unit"
+ );
+ let firstRadius = `${borderCommonFirstRadius.value}${borderCommonFirstRadiusUnit.value}`;
+ let secondRadius = borderCommonSecondRadius.value
+ ? `${borderCommonSecondRadius.value}${borderCommonSecondRadiusUnit.value}`
+ : null;
+
+ setBorderRadius(el, firstRadius, secondRadius);
+
+ borderCommonFirstRadius.addEventListener("input", () => {
+ firstRadius = `${borderCommonFirstRadius.value}${borderCommonFirstRadiusUnit.value}`;
+ setBorderRadius(el, firstRadius, secondRadius);
+ printCode(el);
+ });
+
+ borderCommonFirstRadiusUnit.addEventListener("input", () => {
+ firstRadius = `${borderCommonFirstRadius.value}${borderCommonFirstRadiusUnit.value}`;
+ setBorderRadius(el, firstRadius, secondRadius);
+ printCode(el);
+ });
+
+ borderCommonSecondRadius.addEventListener("input", () => {
+ secondRadius = borderCommonSecondRadius.value
+ ? `${borderCommonSecondRadius.value}${borderCommonSecondRadiusUnit.value}`
+ : null;
+ setBorderRadius(el, firstRadius, secondRadius);
+ printCode(el);
+ });
+
+ borderCommonSecondRadiusUnit.addEventListener("input", () => {
+ secondRadius = borderCommonSecondRadius.value
+ ? `${borderCommonSecondRadius.value}${borderCommonSecondRadiusUnit.value}`
+ : null;
+ setBorderRadius(el, firstRadius, secondRadius);
+ printCode(el);
+ });
+}
+
+/**
+ * Set the top left border-radius to the given element.
+ * @param {HTMLElement} el - Apply the top left border-radius to this element.
+ */
+function setTopLeftBorderRadius(el) {
+ const borderTopLeftFirstRadius = document.getElementById(
+ "border-top-left-first-radius"
+ );
+ const borderTopLeftFirstRadiusUnit = document.getElementById(
+ "border-top-left-first-radius-unit"
+ );
+ const borderTopLeftSecondRadius = document.getElementById(
+ "border-top-left-second-radius"
+ );
+ const borderTopLeftSecondRadiusUnit = document.getElementById(
+ "border-top-left-second-radius-unit"
+ );
+ let firstRadius = `${borderTopLeftFirstRadius.value}${borderTopLeftFirstRadiusUnit.value}`;
+ let secondRadius = borderTopLeftSecondRadius.value
+ ? `${borderTopLeftSecondRadius.value}${borderTopLeftSecondRadiusUnit.value}`
+ : null;
+
+ setBorderRadius(el, firstRadius, secondRadius, "left", "top");
+
+ borderTopLeftFirstRadius.addEventListener("input", () => {
+ firstRadius = `${borderTopLeftFirstRadius.value}${borderTopLeftFirstRadiusUnit.value}`;
+ setBorderRadius(el, firstRadius, secondRadius, "left", "top");
+ printCode(el);
+ });
+
+ borderTopLeftFirstRadiusUnit.addEventListener("input", () => {
+ firstRadius = `${borderTopLeftFirstRadius.value}${borderTopLeftFirstRadiusUnit.value}`;
+ setBorderRadius(el, firstRadius, secondRadius, "left", "top");
+ printCode(el);
+ });
+
+ borderTopLeftSecondRadius.addEventListener("input", () => {
+ secondRadius = borderTopLeftSecondRadius.value
+ ? `${borderTopLeftSecondRadius.value}${borderTopLeftSecondRadiusUnit.value}`
+ : null;
+ setBorderRadius(el, firstRadius, secondRadius, "left", "top");
+ printCode(el);
+ });
+
+ borderTopLeftSecondRadiusUnit.addEventListener("input", () => {
+ secondRadius = borderTopLeftSecondRadius.value
+ ? `${borderTopLeftSecondRadius.value}${borderTopLeftSecondRadiusUnit.value}`
+ : null;
+ setBorderRadius(el, firstRadius, secondRadius, "left", "top");
+ printCode(el);
+ });
+}
+
+/**
+ * Set the top right border-radius to the given element.
+ * @param {HTMLElement} el - Apply the top right border-radius to this element.
+ */
+function setTopRightBorderRadius(el) {
+ const borderTopRightFirstRadius = document.getElementById(
+ "border-top-right-first-radius"
+ );
+ const borderTopRightFirstRadiusUnit = document.getElementById(
+ "border-top-right-first-radius-unit"
+ );
+ const borderTopRightSecondRadius = document.getElementById(
+ "border-top-right-second-radius"
+ );
+ const borderTopRightSecondRadiusUnit = document.getElementById(
+ "border-top-right-second-radius-unit"
+ );
+ const firstRadius = `${borderTopRightFirstRadius.value}${borderTopRightFirstRadiusUnit.value}`;
+ const secondRadius = borderTopRightSecondRadius.value
+ ? `${borderTopRightSecondRadius.value}${borderTopRightSecondRadiusUnit.value}`
+ : null;
+
+ setBorderRadius(el, firstRadius, secondRadius, "right", "top");
+
+ borderTopRightFirstRadius.addEventListener("input", () => {
+ firstRadius = `${borderTopRightFirstRadius.value}${borderTopRightFirstRadiusUnit.value}`;
+ setBorderRadius(el, firstRadius, secondRadius, "right", "top");
+ printCode(el);
+ });
+
+ borderTopRightFirstRadiusUnit.addEventListener("input", () => {
+ firstRadius = `${borderTopRightFirstRadius.value}${borderTopRightFirstRadiusUnit.value}`;
+ setBorderRadius(el, firstRadius, secondRadius, "right", "top");
+ printCode(el);
+ });
+
+ borderTopRightSecondRadius.addEventListener("input", () => {
+ secondRadius = borderTopRightSecondRadius.value
+ ? `${borderTopRightSecondRadius.value}${borderTopRightSecondRadiusUnit.value}`
+ : null;
+ setBorderRadius(el, firstRadius, secondRadius, "right", "top");
+ printCode(el);
+ });
+
+ borderTopRightSecondRadiusUnit.addEventListener("input", () => {
+ secondRadius = borderTopRightSecondRadius.value
+ ? `${borderTopRightSecondRadius.value}${borderTopRightSecondRadiusUnit.value}`
+ : null;
+ setBorderRadius(el, firstRadius, secondRadius, "right", "top");
+ printCode(el);
+ });
+}
+
+/**
+ * Set the bottom left border-radius to the given element.
+ * @param {HTMLElement} el - Apply bottom left border-radius to this element.
+ */
+function setBottomLeftBorderRadius(el) {
+ const borderBottomLeftFirstRadius = document.getElementById(
+ "border-bottom-left-first-radius"
+ );
+ const borderBottomLeftFirstRadiusUnit = document.getElementById(
+ "border-bottom-left-first-radius-unit"
+ );
+ const borderBottomLeftSecondRadius = document.getElementById(
+ "border-bottom-left-second-radius"
+ );
+ const borderBottomLeftSecondRadiusUnit = document.getElementById(
+ "border-bottom-left-second-radius-unit"
+ );
+ const firstRadius = `${borderBottomLeftFirstRadius.value}${borderBottomLeftFirstRadiusUnit.value}`;
+ const secondRadius = borderBottomLeftSecondRadius.value
+ ? `${borderBottomLeftSecondRadius.value}${borderBottomLeftSecondRadiusUnit.value}`
+ : null;
+
+ setBorderRadius(el, firstRadius, secondRadius, "left", "bottom");
+
+ borderBottomLeftFirstRadius.addEventListener("input", () => {
+ firstRadius = `${borderBottomLeftFirstRadius.value}${borderBottomLeftFirstRadiusUnit.value}`;
+ setBorderRadius(el, firstRadius, secondRadius, "left", "bottom");
+ printCode(el);
+ });
+
+ borderBottomLeftFirstRadiusUnit.addEventListener("input", () => {
+ firstRadius = `${borderBottomLeftFirstRadius.value}${borderBottomLeftFirstRadiusUnit.value}`;
+ setBorderRadius(el, firstRadius, secondRadius, "left", "bottom");
+ printCode(el);
+ });
+
+ borderBottomLeftSecondRadius.addEventListener("input", () => {
+ secondRadius = borderBottomLeftSecondRadius.value
+ ? `${borderBottomLeftSecondRadius.value}${borderBottomLeftSecondRadiusUnit.value}`
+ : null;
+ setBorderRadius(el, firstRadius, secondRadius, "left", "bottom");
+ printCode(el);
+ });
+
+ borderBottomLeftSecondRadiusUnit.addEventListener("input", () => {
+ secondRadius = borderBottomLeftSecondRadius.value
+ ? `${borderBottomLeftSecondRadius.value}${borderBottomLeftSecondRadiusUnit.value}`
+ : null;
+ setBorderRadius(el, firstRadius, secondRadius, "left", "bottom");
+ printCode(el);
+ });
+}
+
+/**
+ * Set the bottom right border-radius to the given element.
+ * @param {HTMLElement} el - Apply bottom right border-radius to this element.
+ */
+function setBottomRightBorderRadius(el) {
+ const borderBottomRightFirstRadius = document.getElementById(
+ "border-bottom-right-first-radius"
+ );
+ const borderBottomRightFirstRadiusUnit = document.getElementById(
+ "border-bottom-right-first-radius-unit"
+ );
+ const borderBottomRightSecondRadius = document.getElementById(
+ "border-bottom-right-second-radius"
+ );
+ const borderBottomRightSecondRadiusUnit = document.getElementById(
+ "border-bottom-right-second-radius-unit"
+ );
+ const firstRadius = `${borderBottomRightFirstRadius.value}${borderBottomRightFirstRadiusUnit.value}`;
+ const secondRadius = borderBottomRightSecondRadius.value
+ ? `${borderBottomRightSecondRadius.value}${borderBottomRightSecondRadiusUnit.value}`
+ : null;
+
+ setBorderRadius(el, firstRadius, secondRadius, "right", "bottom");
+
+ borderBottomRightFirstRadius.addEventListener("input", () => {
+ firstRadius = `${borderBottomRightFirstRadius.value}${borderBottomRightFirstRadiusUnit.value}`;
+ setBorderRadius(el, firstRadius, secondRadius, "right", "bottom");
+ printCode(el);
+ });
+
+ borderBottomRightFirstRadiusUnit.addEventListener("input", () => {
+ firstRadius = `${borderBottomRightFirstRadius.value}${borderBottomRightFirstRadiusUnit.value}`;
+ setBorderRadius(el, firstRadius, secondRadius, "right", "bottom");
+ printCode(el);
+ });
+
+ borderBottomRightSecondRadius.addEventListener("input", () => {
+ secondRadius = borderBottomRightSecondRadius.value
+ ? `${borderBottomRightSecondRadius.value}${borderBottomRightSecondRadiusUnit.value}`
+ : null;
+ setBorderRadius(el, firstRadius, secondRadius, "right", "bottom");
+ printCode(el);
+ });
+
+ borderBottomRightSecondRadiusUnit.addEventListener("input", () => {
+ secondRadius = borderBottomRightSecondRadius.value
+ ? `${borderBottomRightSecondRadius.value}${borderBottomRightSecondRadiusUnit.value}`
+ : null;
+ setBorderRadius(el, firstRadius, secondRadius, "right", "bottom");
+ printCode(el);
+ });
+}
+
+/**
+ * Display a message inside the given element.
+ * @param {HTMLElement} el - The element where to print the message.
+ * @param {String} msg - The message to display.
+ * @param {Number} [duration] - The message duration.
+ */
+function printMessage(el, msg, duration = 1000) {
+ const backupContent = el.textContent;
+
+ el.textContent = msg;
+ setTimeout(() => (el.textContent = backupContent), duration);
+}
+
+/**
+ * Copy code to the clipboard.
+ */
+function copyCode() {
+ const code = document.querySelector(".result__code");
+ navigator.clipboard.writeText(code.textContent);
+}
+
+/**
+ * Listen the button copy to clipboard.
+ */
+function listenCopyCodeBtn() {
+ const btn = document.getElementById("copy-code");
+
+ btn.addEventListener("click", () => {
+ copyCode();
+ printMessage(btn, "Copied to clipboard!");
+ });
+}
+
+/**
+ * Initialize borders settings and borders.
+ * @param {String} radioValue - The input radio value.
+ * @param {HTMLElement} el - The element where to apply borders.
+ */
+function initBorders(radioValue, el) {
+ if (isIndividualSettings(radioValue)) {
+ toggleBorderSettingsDisplay("individual");
+ setTopBorder(el);
+ setRightBorder(el);
+ setBottomBorder(el);
+ setLeftBorder(el);
+ } else {
+ toggleBorderSettingsDisplay("common");
+ setCommonBorder(el);
+ }
+}
+
+/**
+ * Initialize border-radius settings and border-radius.
+ * @param {String} radioValue - The input radio value.
+ * @param {HTMLElement} el - The element where to apply border-radius.
+ */
+function initBordersRadius(radioValue, el) {
+ if (isIndividualSettings(radioValue)) {
+ toggleBorderRadiusSettingsDisplay("individual");
+ setTopLeftBorderRadius(el);
+ setTopRightBorderRadius(el);
+ setBottomLeftBorderRadius(el);
+ setBottomRightBorderRadius(el);
+ } else {
+ toggleBorderRadiusSettingsDisplay("common");
+ setCommonBorderRadius(el);
+ }
+}
+
+/**
+ * Initialize the app.
+ */
+function init() {
+ const box = document.querySelector(".box");
+ const borderPropertyRadio = document.querySelectorAll(
+ 'input[name="border-property"]'
+ );
+ const borderRadiusPropertyRadio = document.querySelectorAll(
+ 'input[name="border-radius-property"]'
+ );
+
+ for (const radio of borderPropertyRadio) {
+ if (radio.checked) initBorders(radio.value, box);
+ radio.addEventListener("change", () => initBorders(radio.value, box));
+ }
+
+ for (const radio of borderRadiusPropertyRadio) {
+ if (radio.checked) initBordersRadius(radio.value, box);
+ radio.addEventListener("change", () => initBordersRadius(radio.value, box));
+ }
+
+ printCode(box);
+ listenCopyCodeBtn();
+}
+
+init();
diff --git a/public/projects/js-small-apps/css-border-previewer/index.html b/public/projects/js-small-apps/css-border-previewer/index.html
new file mode 100644
index 0000000..bf16fa5
--- /dev/null
+++ b/public/projects/js-small-apps/css-border-previewer/index.html
@@ -0,0 +1,625 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>CSS Border Previewer</title>
+ <link rel="stylesheet" href="style.css" />
+ </head>
+ <body>
+ <header class="header">
+ <h1 class="branding">CSS Border Previewer</h1>
+ </header>
+ <main class="main">
+ <div class="preview">
+ <div class="box"></div>
+ </div>
+ <form action="#" method="POST" class="settings form">
+ <fieldset class="form__fieldset">
+ <legend class="form__legend">Global settings</legend>
+ <div class="form__item">
+ Control border property separately?
+ <input
+ type="radio"
+ name="border-property"
+ id="border-property-no"
+ value="false"
+ checked
+ />
+ <label for="border-property-no">No</label>
+ <input
+ type="radio"
+ name="border-property"
+ id="border-property-yes"
+ value="true"
+ />
+ <label for="border-property-yes">Yes</label>
+ </div>
+ <div class="form__item">
+ Control border radius property separately?
+ <input
+ type="radio"
+ name="border-radius-property"
+ id="border-radius-property-no"
+ value="false"
+ checked
+ />
+ <label for="border-radius-property-no">No</label>
+ <input
+ type="radio"
+ name="border-radius-property"
+ id="border-radius-property-yes"
+ value="true"
+ />
+ <label for="border-radius-property-yes">Yes</label>
+ </div>
+ </fieldset>
+ <fieldset
+ id="fieldset-borders"
+ class="form__fieldset form__fieldset--flex"
+ >
+ <legend class="form__legend">Borders settings</legend>
+ <div class="form__item form__item--flex">
+ <label for="borders-width">Width</label>
+ <input
+ type="number"
+ name="borders-width"
+ id="borders-width"
+ size="3"
+ value="1"
+ class="form__input"
+ />
+ </div>
+ <div class="form__item form__item--flex">
+ <label for="borders-unit" class="form__label">Unit</label>
+ <select name="borders-unit" id="borders-unit" class="form__select">
+ <option value="px" selected>px</option>
+ <option value="cm">cm</option>
+ <option value="em">em</option>
+ <option value="rem">rem</option>
+ </select>
+ </div>
+ <div class="form__item form__item--flex">
+ <label for="borders-style">Style</label>
+ <select
+ name="borders-style"
+ id="borders-style"
+ class="form__select"
+ >
+ <option value="none">None</option>
+ <option value="dashed">Dashed</option>
+ <option value="dotted">Dotted</option>
+ <option value="double">Double</option>
+ <option value="groove">Groove</option>
+ <option value="hidden">Hidden</option>
+ <option value="inset">Inset</option>
+ <option value="outset">Outset</option>
+ <option value="ridge">Ridge</option>
+ <option value="solid" selected>Solid</option>
+ </select>
+ </div>
+ <div class="form__item form__item--flex">
+ <label for="borders-color">Color</label>
+ <input
+ type="color"
+ name="borders-color"
+ id="borders-color"
+ value="#000000"
+ class="form__input form__input--color"
+ />
+ </div>
+ </fieldset>
+ <fieldset
+ id="fieldset-border-top"
+ class="form__fieldset form__fieldset--flex"
+ >
+ <legend class="form__legend">Border-top settings</legend>
+ <div class="form__item form__item--flex">
+ <label for="border-top-width">Width</label>
+ <input
+ type="number"
+ name="border-top-width"
+ id="border-top-width"
+ size="3"
+ value="1"
+ class="form__input"
+ />
+ </div>
+ <div class="form__item form__item--flex">
+ <label for="border-top-unit" class="form__label">Unit</label>
+ <select
+ name="border-top-unit"
+ id="border-top-unit"
+ class="form__select"
+ >
+ <option value="px" selected>px</option>
+ <option value="cm">cm</option>
+ <option value="em">em</option>
+ <option value="rem">rem</option>
+ </select>
+ </div>
+ <div class="form__item form__item--flex">
+ <label for="border-top-style">Style</label>
+ <select
+ name="border-top-style"
+ id="border-top-style"
+ class="form__select"
+ >
+ <option value="none">None</option>
+ <option value="dashed">Dashed</option>
+ <option value="dotted">Dotted</option>
+ <option value="double">Double</option>
+ <option value="groove">Groove</option>
+ <option value="hidden">Hidden</option>
+ <option value="inset">Inset</option>
+ <option value="outset">Outset</option>
+ <option value="ridge">Ridge</option>
+ <option value="solid" selected>Solid</option>
+ </select>
+ </div>
+ <div class="form__item form__item--flex">
+ <label for="border-top-color">Color</label>
+ <input
+ type="color"
+ name="border-top-color"
+ id="border-top-color"
+ value="#000000"
+ class="form__input form__input--color"
+ />
+ </div>
+ </fieldset>
+ <fieldset
+ id="fieldset-border-right"
+ class="form__fieldset form__fieldset--flex"
+ >
+ <legend class="form__legend">Border-right settings</legend>
+ <div class="form__item form__item--flex">
+ <label for="border-right-width">Width</label>
+ <input
+ type="number"
+ name="border-right-width"
+ id="border-right-width"
+ size="3"
+ value="1"
+ class="form__input"
+ />
+ </div>
+ <div class="form__item form__item--flex">
+ <label for="border-right-unit" class="form__label">Unit</label>
+ <select
+ name="border-right-unit"
+ id="border-right-unit"
+ class="form__select"
+ >
+ <option value="px" selected>px</option>
+ <option value="cm">cm</option>
+ <option value="em">em</option>
+ <option value="rem">rem</option>
+ </select>
+ </div>
+ <div class="form__item form__item--flex">
+ <label for="border-right-style">Style</label>
+ <select
+ name="border-right-style"
+ id="border-right-style"
+ class="form__select"
+ >
+ <option value="none">None</option>
+ <option value="dashed">Dashed</option>
+ <option value="dotted">Dotted</option>
+ <option value="double">Double</option>
+ <option value="groove">Groove</option>
+ <option value="hidden">Hidden</option>
+ <option value="inset">Inset</option>
+ <option value="outset">Outset</option>
+ <option value="ridge">Ridge</option>
+ <option value="solid" selected>Solid</option>
+ </select>
+ </div>
+ <div class="form__item form__item--flex">
+ <label for="border-right-color">Color</label>
+ <input
+ type="color"
+ name="border-right-color"
+ id="border-right-color"
+ value="#000000"
+ class="form__input form__input--color"
+ />
+ </div>
+ </fieldset>
+ <fieldset
+ id="fieldset-border-bottom"
+ class="form__fieldset form__fieldset--flex"
+ >
+ <legend class="form__legend">Border-bottom settings</legend>
+ <div class="form__item form__item--flex">
+ <label for="border-bottom-width">Width</label>
+ <input
+ type="number"
+ name="border-bottom-width"
+ id="border-bottom-width"
+ size="3"
+ value="1"
+ class="form__input"
+ />
+ </div>
+ <div class="form__item form__item--flex">
+ <label for="border-bottom-unit" class="form__label">Unit</label>
+ <select
+ name="border-bottom-unit"
+ id="border-bottom-unit"
+ class="form__select"
+ >
+ <option value="px" selected>px</option>
+ <option value="cm">cm</option>
+ <option value="em">em</option>
+ <option value="rem">rem</option>
+ </select>
+ </div>
+ <div class="form__item form__item--flex">
+ <label for="border-bottom-style">Style</label>
+ <select
+ name="border-bottom-style"
+ id="border-bottom-style"
+ class="form__select"
+ >
+ <option value="none">None</option>
+ <option value="dashed">Dashed</option>
+ <option value="dotted">Dotted</option>
+ <option value="double">Double</option>
+ <option value="groove">Groove</option>
+ <option value="hidden">Hidden</option>
+ <option value="inset">Inset</option>
+ <option value="outset">Outset</option>
+ <option value="ridge">Ridge</option>
+ <option value="solid" selected>Solid</option>
+ </select>
+ </div>
+ <div class="form__item form__item--flex">
+ <label for="border-bottom-color">Color</label>
+ <input
+ type="color"
+ name="border-bottom-color"
+ id="border-bottom-color"
+ value="#000000"
+ class="form__input form__input--color"
+ />
+ </div>
+ </fieldset>
+ <fieldset
+ id="fieldset-border-left"
+ class="form__fieldset form__fieldset--flex"
+ >
+ <legend class="form__legend">Border-left settings</legend>
+ <div class="form__item form__item--flex">
+ <label for="border-left-width">Width</label>
+ <input
+ type="number"
+ name="border-left-width"
+ id="border-left-width"
+ size="3"
+ value="1"
+ class="form__input"
+ />
+ </div>
+ <div class="form__item form__item--flex">
+ <label for="border-left-unit" class="form__label">Unit</label>
+ <select
+ name="border-left-unit"
+ id="border-left-unit"
+ class="form__select"
+ >
+ <option value="px" selected>px</option>
+ <option value="cm">cm</option>
+ <option value="em">em</option>
+ <option value="rem">rem</option>
+ </select>
+ </div>
+ <div class="form__item form__item--flex">
+ <label for="border-left-style">Style</label>
+ <select
+ name="border-left-style"
+ id="border-left-style"
+ class="form__select"
+ >
+ <option value="none">None</option>
+ <option value="dashed">Dashed</option>
+ <option value="dotted">Dotted</option>
+ <option value="double">Double</option>
+ <option value="groove">Groove</option>
+ <option value="hidden">Hidden</option>
+ <option value="inset">Inset</option>
+ <option value="outset">Outset</option>
+ <option value="ridge">Ridge</option>
+ <option value="solid" selected>Solid</option>
+ </select>
+ </div>
+ <div class="form__item form__item--flex">
+ <label for="border-left-color">Color</label>
+ <input
+ type="color"
+ name="border-left-color"
+ id="border-left-color"
+ value="#000000"
+ class="form__input form__input--color"
+ />
+ </div>
+ </fieldset>
+ <fieldset id="fieldset-borders-radius" class="form__fieldset">
+ <legend class="form__legend">Border-radius settings</legend>
+ <div class="form__item">
+ <p>First radius:</p>
+ <label for="borders-first-radius">Value</label>
+ <input
+ type="number"
+ name="borders-first-radius"
+ id="borders-first-radius"
+ class="form__input"
+ />
+ <label for="borders-first-radius-unit" class="form__label"
+ >Unit</label
+ >
+ <select
+ name="borders-first-radius-unit"
+ id="borders-first-radius-unit"
+ class="form__select"
+ >
+ <option value="px" selected>px</option>
+ <option value="%">%</option>
+ <option value="em">em</option>
+ <option value="rem">rem</option>
+ </select>
+ </div>
+ <div class="form__item">
+ <p>Second radius:</p>
+ <label for="borders-second-radius">Value</label>
+ <input
+ type="number"
+ name="borders-second-radius"
+ id="borders-second-radius"
+ class="form__input"
+ />
+ <label for="borders-second-radius-unit" class="form__label"
+ >Unit</label
+ >
+ <select
+ name="borders-second-radius-unit"
+ id="borders-second-radius-unit"
+ class="form__select"
+ >
+ <option value="px" selected>px</option>
+ <option value="%">%</option>
+ <option value="em">em</option>
+ <option value="rem">rem</option>
+ </select>
+ </div>
+ </fieldset>
+ <fieldset id="fieldset-border-top-left-radius" class="form__fieldset">
+ <legend class="form__legend">Border-top-left-radius settings</legend>
+ <div class="form__item">
+ <p>First radius:</p>
+ <label for="border-top-left-first-radius">Value</label>
+ <input
+ type="number"
+ name="border-top-left-first-radius"
+ id="border-top-left-first-radius"
+ class="form__input"
+ />
+ <label for="border-top-left-first-radius-unit" class="form__label"
+ >Unit</label
+ >
+ <select
+ name="border-top-left-first-radius-unit"
+ id="border-top-left-first-radius-unit"
+ class="form__select"
+ >
+ <option value="px" selected>px</option>
+ <option value="%">%</option>
+ <option value="em">em</option>
+ <option value="rem">rem</option>
+ </select>
+ </div>
+ <div class="form__item">
+ <p>Second radius:</p>
+ <label for="border-top-left-second-radius">Value</label>
+ <input
+ type="number"
+ name="border-top-left-second-radius"
+ id="border-top-left-second-radius"
+ class="form__input"
+ />
+ <label for="border-top-left-second-radius-unit" class="form__label"
+ >Unit</label
+ >
+ <select
+ name="border-top-left-second-radius-unit"
+ id="border-top-left-second-radius-unit"
+ class="form__select"
+ >
+ <option value="px" selected>px</option>
+ <option value="%">%</option>
+ <option value="em">em</option>
+ <option value="rem">rem</option>
+ </select>
+ </div>
+ </fieldset>
+ <fieldset id="fieldset-border-top-right-radius" class="form__fieldset">
+ <legend class="form__legend">Border-top-right-radius settings</legend>
+ <div class="form__item">
+ <p>First radius:</p>
+ <label for="border-top-right-first-radius">Value</label>
+ <input
+ type="number"
+ name="border-top-right-first-radius"
+ id="border-top-right-first-radius"
+ class="form__input"
+ />
+ <label for="border-top-right-first-radius-unit" class="form__label"
+ >Unit</label
+ >
+ <select
+ name="border-top-right-first-radius-unit"
+ id="border-top-right-first-radius-unit"
+ class="form__select"
+ >
+ <option value="px" selected>px</option>
+ <option value="%">%</option>
+ <option value="em">em</option>
+ <option value="rem">rem</option>
+ </select>
+ </div>
+ <div class="form__item">
+ <p>Second radius:</p>
+ <label for="border-top-right-second-radius">Value</label>
+ <input
+ type="number"
+ name="border-top-right-second-radius"
+ id="border-top-right-second-radius"
+ class="form__input"
+ />
+ <label for="border-top-right-second-radius-unit" class="form__label"
+ >Unit</label
+ >
+ <select
+ name="border-top-right-second-radius-unit"
+ id="border-top-right-second-radius-unit"
+ class="form__select"
+ >
+ <option value="px" selected>px</option>
+ <option value="%">%</option>
+ <option value="em">em</option>
+ <option value="rem">rem</option>
+ </select>
+ </div>
+ </fieldset>
+ <fieldset
+ id="fieldset-border-bottom-left-radius"
+ class="form__fieldset"
+ >
+ <legend class="form__legend">
+ Border-bottom-left-radius settings
+ </legend>
+ <div class="form__item">
+ <p>First radius:</p>
+ <label for="border-bottom-left-first-radius">Value</label>
+ <input
+ type="number"
+ name="border-bottom-left-first-radius"
+ id="border-bottom-left-first-radius"
+ class="form__input"
+ />
+ <label
+ for="border-bottom-left-first-radius-unit"
+ class="form__label"
+ >Unit</label
+ >
+ <select
+ name="border-bottom-left-first-radius-unit"
+ id="border-bottom-left-first-radius-unit"
+ class="form__select"
+ >
+ <option value="px" selected>px</option>
+ <option value="%">%</option>
+ <option value="em">em</option>
+ <option value="rem">rem</option>
+ </select>
+ </div>
+ <div class="form__item">
+ <p>Second radius:</p>
+ <label for="border-bottom-left-second-radius">Value</label>
+ <input
+ type="number"
+ name="border-bottom-left-second-radius"
+ id="border-bottom-left-second-radius"
+ class="form__input"
+ />
+ <label
+ for="border-bottom-left-second-radius-unit"
+ class="form__label"
+ >Unit</label
+ >
+ <select
+ name="border-bottom-left-second-radius-unit"
+ id="border-bottom-left-second-radius-unit"
+ class="form__select"
+ >
+ <option value="px" selected>px</option>
+ <option value="%">%</option>
+ <option value="em">em</option>
+ <option value="rem">rem</option>
+ </select>
+ </div>
+ </fieldset>
+ <fieldset
+ id="fieldset-border-bottom-right-radius"
+ class="form__fieldset"
+ >
+ <legend class="form__legend">
+ Border-bottom-right-radius settings
+ </legend>
+ <div class="form__item">
+ <p>First radius:</p>
+ <label for="border-bottom-right-first-radius">Value</label>
+ <input
+ type="number"
+ name="border-bottom-right-first-radius"
+ id="border-bottom-right-first-radius"
+ class="form__input"
+ />
+ <label
+ for="border-bottom-right-first-radius-unit"
+ class="form__label"
+ >Unit</label
+ >
+ <select
+ name="border-bottom-right-first-radius-unit"
+ id="border-bottom-right-first-radius-unit"
+ class="form__select"
+ >
+ <option value="px" selected>px</option>
+ <option value="%">%</option>
+ <option value="em">em</option>
+ <option value="rem">rem</option>
+ </select>
+ </div>
+ <div class="form__item">
+ <p>Second radius:</p>
+ <label for="border-bottom-right-second-radius">Value</label>
+ <input
+ type="number"
+ name="border-bottom-right-second-radius"
+ id="border-bottom-right-second-radius"
+ class="form__input"
+ />
+ <label
+ for="border-bottom-right-second-radius-unit"
+ class="form__label"
+ >Unit</label
+ >
+ <select
+ name="border-bottom-right-second-radius-unit"
+ id="border-bottom-right-second-radius-unit"
+ class="form__select"
+ >
+ <option value="px" selected>px</option>
+ <option value="%">%</option>
+ <option value="em">em</option>
+ <option value="rem">rem</option>
+ </select>
+ </div>
+ </fieldset>
+ </form>
+ <div class="result">
+ <pre class="result__pre">
+ <code class="result__code"></code>
+ </pre>
+ <button id="copy-code" class="btn">Copy to the clipboard</button>
+ </div>
+ </main>
+ <footer class="footer">
+ <p class="copyright">CSS Border Previewer. MIT 2021. Armand Philippot.</p>
+ </footer>
+ <script src="app.js"></script>
+ </body>
+</html>
diff --git a/public/projects/js-small-apps/css-border-previewer/style.css b/public/projects/js-small-apps/css-border-previewer/style.css
new file mode 100644
index 0000000..458d31c
--- /dev/null
+++ b/public/projects/js-small-apps/css-border-previewer/style.css
@@ -0,0 +1,181 @@
+*,
+*::after,
+*::before {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ background: #fff;
+ color: #000;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 1rem;
+ line-height: 1.618;
+ display: flex;
+ flex-flow: column nowrap;
+ min-height: 100vh;
+}
+
+.header,
+.footer,
+.result {
+ width: min(calc(100vw - 2rem), calc(1200px - 2rem));
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.preview,
+.settings {
+ width: min(calc(100vw - 2rem), calc(1200px / 2 - 2rem));
+ margin-left: auto;
+ margin-right: auto;
+}
+
+@media screen and (min-width: 1200px) {
+ .preview {
+ margin-right: 0;
+ }
+
+ .settings {
+ margin-left: 0;
+ }
+}
+
+.header {
+ padding: 2rem 0 3rem;
+ text-align: center;
+}
+
+.branding {
+ color: hsl(219, 64%, 35%);
+}
+
+.main {
+ flex: 1;
+ display: flex;
+ flex-flow: row wrap;
+ gap: 1rem;
+ margin: auto;
+}
+
+.preview {
+ border: 1px solid #ccc;
+ padding: 3rem;
+ position: relative;
+}
+
+.box {
+ border: 1px solid #000;
+ width: 100%;
+ height: 20vh;
+ position: sticky;
+ top: 3rem;
+}
+
+.result {
+ max-width: 100%;
+}
+
+.result__pre {
+ background: #333;
+ color: #fff;
+ min-height: 20rem;
+ margin: 1rem auto;
+ padding: 0 1rem;
+ overflow-x: auto;
+ tab-size: 4;
+}
+
+.result .btn {
+ margin: auto;
+}
+
+@media screen and (min-width: 1440px) {
+ .result__pre {
+ margin: 1rem 0;
+ }
+}
+
+.footer {
+ margin-top: 2rem;
+ padding: 1rem 0;
+}
+
+.copyright {
+ font-size: 0.9rem;
+ text-align: center;
+}
+
+.form p {
+ font-weight: 600;
+}
+
+.form__fieldset {
+ border-color: hsl(219, 64%, 35%);
+ margin-bottom: 1rem;
+ padding: 0.5rem 1rem 1rem;
+}
+
+.form__fieldset--flex {
+ display: flex;
+ flex-flow: row wrap;
+ gap: 1rem;
+}
+
+.form__legend {
+ color: hsl(219, 64%, 35%);
+ font-size: 0.9rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ padding: 0 0.5rem;
+}
+
+.form__item--flex {
+ display: flex;
+ flex-flow: column wrap;
+}
+
+.form__item:not(.form__item--flex) + * {
+ margin-top: 1rem;
+}
+
+.form__input,
+.form__select {
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+}
+
+.form__input {
+ padding: 0.2rem 0.5rem;
+}
+
+.form__select {
+ padding: 0.4rem 0.5rem;
+}
+
+.btn {
+ background: #fff;
+ border: 2px solid hsl(219, 64%, 35%);
+ box-shadow: 1px 1px 2px hsl(219, 64%, 15%), 0 0 2px 1px hsl(219, 64%, 15%);
+ color: hsl(219, 64%, 35%);
+ font-family: inherit;
+ font-size: inherit;
+ font-weight: 600;
+ line-height: inherit;
+ display: block;
+ padding: 0.5rem;
+ cursor: pointer;
+ transition: all 0.15s ease-in-out 0s;
+}
+
+.btn:hover,
+.btn:focus {
+ background: hsl(219, 64%, 35%);
+ color: #fff;
+}
+
+.btn:active {
+ transform: translateX(1px) translateY(1px);
+}
diff --git a/public/projects/js-small-apps/package.json b/public/projects/js-small-apps/package.json
new file mode 100644
index 0000000..6f4c753
--- /dev/null
+++ b/public/projects/js-small-apps/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "js-small-apps",
+ "description": "A collection of small apps and exercises implemented with Javascript.",
+ "version": "1.0.0",
+ "license": "MIT",
+ "author": {
+ "name": "Armand Philippot",
+ "url": "https://www.armandphilippot.com"
+ },
+ "homepage": "https://github.com/ArmandPhilippot/js-small-apps#readme",
+ "repository": {
+ "type": "git",
+ "url": "git+ssh://git@github.com:ArmandPhilippot/js-small-apps.git"
+ },
+ "bugs": {
+ "url": "https://github.com/ArmandPhilippot/js-small-apps/issues"
+ },
+ "private": true,
+ "scripts": {
+ "release": "standard-version -s"
+ },
+ "devDependencies": {
+ "@commitlint/cli": "^16.2.1",
+ "@commitlint/config-conventional": "^16.2.1",
+ "husky": "^7.0.2",
+ "standard-version": "^9.3.1"
+ }
+}
diff --git a/public/projects/js-small-apps/rock-paper-scissors/README.md b/public/projects/js-small-apps/rock-paper-scissors/README.md
new file mode 100644
index 0000000..834c716
--- /dev/null
+++ b/public/projects/js-small-apps/rock-paper-scissors/README.md
@@ -0,0 +1,11 @@
+# Rock Paper Scissors
+
+A Javascript implementation of the game.
+
+## Preview
+
+You can see a live preview here: https://demo.armandphilippot.com/#rps-game
+
+## License
+
+This project is open-source and available under the [MIT License](../LICENSE).
diff --git a/public/projects/js-small-apps/rock-paper-scissors/app.js b/public/projects/js-small-apps/rock-paper-scissors/app.js
new file mode 100644
index 0000000..581b442
--- /dev/null
+++ b/public/projects/js-small-apps/rock-paper-scissors/app.js
@@ -0,0 +1,115 @@
+import RPSGame from "./lib/class-rps-game.js";
+
+function createPreview() {
+ const preview = document.querySelector(".preview__content");
+ const player1Wrapper = document.createElement("div");
+ const player2Wrapper = document.createElement("div");
+ const vsWrapper = document.createElement("div");
+ player1Wrapper.id = "player1Wrapper";
+ player2Wrapper.id = "player2Wrapper";
+ vsWrapper.textContent = "vs";
+ vsWrapper.style.fontWeight = 600;
+ preview.append(player1Wrapper, vsWrapper, player2Wrapper);
+}
+
+function getUserPreviewId(id) {
+ let userId;
+ if (id === "player1-name" || id === "player1-ia") {
+ userId = "player1Wrapper";
+ } else if (id === "player2-name" || id === "player2-ia") {
+ userId = "player2Wrapper";
+ }
+ return userId;
+}
+
+function fillPreview(target) {
+ const userId = getUserPreviewId(target.id);
+ if (userId) {
+ const dest = document.getElementById(userId);
+ dest.textContent = target.value;
+ }
+}
+
+function toggleIABadge(target) {
+ const userId = getUserPreviewId(target.id);
+ if (userId) {
+ const dest = document.getElementById(userId);
+ target.checked
+ ? dest.classList.add("ia-badge")
+ : dest.classList.remove("ia-badge");
+ }
+}
+
+function startGame(player1, player2, maxRound) {
+ const register = document.querySelector(".register");
+ const game = document.querySelector(".game");
+ const buttons = {
+ rock: document.querySelector(".btn--rock"),
+ paper: document.querySelector(".btn--paper"),
+ scissors: document.querySelector(".btn--scissors"),
+ newGame: document.querySelector(".btn--new-game"),
+ };
+ const p1Scoring = {
+ name: document.getElementById("player1username"),
+ value: document.getElementById("player1score"),
+ };
+ const p2Scoring = {
+ name: document.getElementById("player2username"),
+ value: document.getElementById("player2score"),
+ };
+ const messages = document.querySelector(".message-board");
+ const players = [];
+ players.push(player1);
+ players.push(player2);
+
+ const app = new RPSGame(players, buttons, p1Scoring, p2Scoring, messages);
+ app.init();
+ app.maxRound = maxRound && maxRound !== "0" ? maxRound : "";
+ register.style.display = "none";
+ game.style.display = "flex";
+}
+
+function listen() {
+ const inputP1Name = document.getElementById("player1-name");
+ const inputP2Name = document.getElementById("player2-name");
+ const checkboxP1IA = document.getElementById("player1-ia");
+ const checkboxP2IA = document.getElementById("player2-ia");
+ const inputMaxRound = document.getElementById("round-number");
+ const registerBtn = document.querySelector(".form__submit");
+
+ if (inputP1Name.value) fillPreview(inputP1Name);
+ if (inputP2Name.value) fillPreview(inputP2Name);
+ if (checkboxP1IA.checked) toggleIABadge(checkboxP1IA);
+ if (checkboxP2IA.checked) toggleIABadge(checkboxP2IA);
+
+ inputP1Name.addEventListener("keyup", (e) => fillPreview(e.target));
+ inputP2Name.addEventListener("keyup", (e) => fillPreview(e.target));
+ checkboxP1IA.addEventListener("change", (event) => {
+ toggleIABadge(event.target);
+ if (checkboxP2IA.checked && event.target.checked) {
+ checkboxP2IA.checked = false;
+ toggleIABadge(checkboxP2IA);
+ }
+ });
+ checkboxP2IA.addEventListener("change", (event) => {
+ toggleIABadge(event.target);
+ if (checkboxP1IA.checked && event.target.checked) {
+ checkboxP1IA.checked = false;
+ toggleIABadge(checkboxP1IA);
+ }
+ });
+ registerBtn.addEventListener("click", (event) => {
+ event.preventDefault();
+ const player1 = { username: inputP1Name.value, ia: checkboxP1IA.checked };
+ const player2 = { username: inputP2Name.value, ia: checkboxP2IA.checked };
+ const maxRound = inputMaxRound.value;
+ startGame(player1, player2, maxRound);
+ });
+}
+
+function init() {
+ createPreview();
+ listen();
+}
+
+init();
diff --git a/public/projects/js-small-apps/rock-paper-scissors/index.html b/public/projects/js-small-apps/rock-paper-scissors/index.html
new file mode 100644
index 0000000..19c9d7a
--- /dev/null
+++ b/public/projects/js-small-apps/rock-paper-scissors/index.html
@@ -0,0 +1,142 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Rock Paper Scissors - Game</title>
+ <link rel="stylesheet" href="style.css" />
+ </head>
+ <body>
+ <header class="branding">
+ <h1 class="branding__title">Rock Paper Scissors</h1>
+ </header>
+ <main class="main">
+ <div class="register">
+ <form action="#" class="form form--register">
+ <fieldset class="form__fieldset">
+ <legend class="form__legend">Player 1</legend>
+ <div class="form__item">
+ <label class="form__label" for="player1-name">Name:</label>
+ <input
+ type="text"
+ id="player1-name"
+ name="player1-name"
+ placeholder="Pseudo"
+ class="form__input"
+ />
+ </div>
+ <div class="form__item">
+ <input
+ type="checkbox"
+ name="player1-ia"
+ id="player1-ia"
+ class="form__checkbox"
+ />
+ <label class="form__label form__label--checkbox" for="player1-ia"
+ >IA?</label
+ >
+ </div>
+ </fieldset>
+ <fieldset class="form__fieldset">
+ <legend class="form__legend">Player 2</legend>
+ <div class="form__item">
+ <label class="form__label" for="player2-name">Name:</label>
+ <input
+ type="text"
+ id="player2-name"
+ name="player2-name"
+ placeholder="Pseudo"
+ class="form__input"
+ />
+ </div>
+ <div class="form__item">
+ <input
+ type="checkbox"
+ name="player2-ia"
+ id="player2-ia"
+ class="form__checkbox"
+ />
+ <label class="form__label form__label--checkbox" for="player2-ia"
+ >IA?</label
+ >
+ </div>
+ </fieldset>
+ <fieldset class="form__fieldset">
+ <legend class="form__legend">Settings</legend>
+ <div class="form__item">
+ <label class="form__label" for="round-number"
+ >Number of rounds:</label
+ >
+ <input
+ type="number"
+ name="round-number"
+ id="round-number"
+ class="form__input"
+ />
+ <p class="form__hint">
+ Note: If empty or 0, your party will never end!
+ </p>
+ </div>
+ </fieldset>
+ <button type="submit" class="form__submit btn btn--register">
+ Play!
+ </button>
+ </form>
+ <div class="register__preview">
+ <div class="preview">
+ <div class="preview__content"></div>
+ </div>
+ </div>
+ </div>
+ <div class="game">
+ <div class="scoring-board">
+ <div class="scoring-board__item">
+ <div id="player1username" class="scoring-board__username">
+ Player1
+ </div>
+ <div id="player1score" class="scoring-board__score">0</div>
+ </div>
+ <div class="scoring-board__item">
+ <div id="player2username" class="scoring-board__username">
+ Player2
+ </div>
+ <div id="player2score" class="scoring-board__score">0</div>
+ </div>
+ </div>
+ <div class="message-board">Ready? Let's play!</div>
+ <div class="actions">
+ <button type="button" class="actions__body btn btn--action btn--rock">
+ <span class="actions__txt screen-reader-txt">Rock</span>
+ <span class="actions__icon">✊</span>
+ </button>
+ <button
+ type="button"
+ class="actions__body btn btn--action btn--paper"
+ >
+ <span class="actions__txt screen-reader-txt">Paper</span>
+ <span class="actions__icon">✋</span>
+ </button>
+ <button
+ type="button"
+ class="actions__body btn btn--action btn--scissors"
+ >
+ <span class="actions__txt screen-reader-txt">Scissors</span>
+ <span class="actions__icon">✌️</span>
+ </button>
+ </div>
+ <div class="settings">
+ <button type="reset" class="settings__body btn btn--new-game">
+ New Game?
+ </button>
+ </div>
+ </div>
+ </main>
+ <footer class="footer">
+ <p class="footer__copyright">
+ Rock Paper Scissors. MIT 2021. Armand Philippot.
+ </p>
+ </footer>
+ <script type="module" src="./app.js"></script>
+ </body>
+</html>
diff --git a/public/projects/js-small-apps/rock-paper-scissors/lib/class-game.js b/public/projects/js-small-apps/rock-paper-scissors/lib/class-game.js
new file mode 100644
index 0000000..92898a1
--- /dev/null
+++ b/public/projects/js-small-apps/rock-paper-scissors/lib/class-game.js
@@ -0,0 +1,296 @@
+import Player from "./class-player.js";
+
+/**
+ * Game class.
+ */
+class Game {
+ #name = "My Game";
+ #language = "en-US";
+ #playerId = 0;
+ #players = [];
+ #roundWinners = [];
+ #roundLosers = [];
+ #gameWinners = [];
+ #gameLosers = [];
+ #state = "paused";
+ #turn;
+ #currentTurn = 1;
+ #maxTurn = 0;
+ #currentRound = 1;
+ #maxRound = 0;
+
+ /**
+ * Initialize a new Game instance.
+ * @param {String} name - The game name.
+ * @param {Object[]} players - The players.
+ * @param {String} players[].username - The player username.
+ * @param {Boolean} players[].ia - True to set the player as an IA.
+ */
+ constructor(name, players) {
+ this.#name = name;
+ players.forEach((player) => {
+ this.#players.push(
+ new Player(++this.#playerId, player.username, player.ia)
+ );
+ });
+ }
+
+ set name(string) {
+ this.#name = string;
+ }
+
+ get name() {
+ return this.#name;
+ }
+
+ set language(languageCode) {
+ this.#language = languageCode;
+ }
+
+ get language() {
+ return this.#language;
+ }
+
+ set players(array) {
+ array.forEach((player) =>
+ this.#players.push(
+ new Player(++this.#playerId, player.username, player.ia)
+ )
+ );
+ }
+
+ get players() {
+ return this.#players;
+ }
+
+ set roundWinners(array) {
+ if (array.length > 0) {
+ array.forEach((player) => this.#roundWinners.push(player));
+ } else {
+ this.#roundWinners = [];
+ }
+ }
+
+ get roundWinners() {
+ return this.#roundWinners;
+ }
+
+ set roundLosers(array) {
+ if (array.length > 0) {
+ array.forEach((player) => this.#roundLosers.push(player));
+ } else {
+ this.#roundLosers = [];
+ }
+ }
+
+ get roundLosers() {
+ return this.#roundLosers;
+ }
+
+ set gameWinners(array) {
+ if (array.length > 0) {
+ array.forEach((player) => this.#gameWinners.push(player));
+ } else {
+ this.#gameWinners = [];
+ }
+ }
+
+ get gameWinners() {
+ return this.#gameWinners;
+ }
+
+ set gameLosers(array) {
+ if (array.length > 0) {
+ array.forEach((player) => this.#gameLosers.push(player));
+ } else {
+ this.#gameLosers = [];
+ }
+ }
+
+ get gameLosers() {
+ return this.#gameLosers;
+ }
+
+ set state(string) {
+ this.#state = string;
+ }
+
+ get state() {
+ return this.#state;
+ }
+
+ set turn(generator) {
+ this.#turn = generator;
+ }
+
+ get turn() {
+ return this.#turn;
+ }
+
+ set currentTurn(int) {
+ this.#currentTurn = int;
+ }
+
+ get currentTurn() {
+ return this.#currentTurn;
+ }
+
+ set maxTurn(int = this.getPlayersNumber()) {
+ this.#maxTurn = int;
+ }
+
+ get maxTurn() {
+ return this.#maxTurn;
+ }
+
+ set currentRound(int) {
+ this.#currentRound = int;
+ }
+
+ get currentRound() {
+ return this.#currentRound;
+ }
+
+ set maxRound(int) {
+ this.#maxRound = int;
+ }
+
+ get maxRound() {
+ return this.#maxRound;
+ }
+
+ newPlayer(username) {
+ this.players.push(new Player(++this.#playerId, username));
+ }
+
+ getPlayersNumber() {
+ return this.players.length;
+ }
+
+ getPlayer(number) {
+ return this.players[number - 1];
+ }
+
+ getCurrentPlayer() {
+ return this.getPlayer(this.currentTurn);
+ }
+
+ getNextPlayer() {
+ if (this.currentTurn < this.maxTurn) {
+ return this.getPlayer(this.currentTurn + 1);
+ } else {
+ return this.getPlayer(1);
+ }
+ }
+
+ isFirstTurn() {
+ return this.currentTurn === 1;
+ }
+
+ isNewRound() {
+ return this.currentRound > 1 && this.isFirstTurn();
+ }
+
+ isGameOver() {
+ return this.state === "ended";
+ }
+
+ resetScore() {
+ this.players.forEach((player) => {
+ player.score = 0;
+ });
+ }
+
+ newGame() {
+ this.roundWinners = [];
+ this.roundLosers = [];
+ this.gameWinners = [];
+ this.gameLosers = [];
+ this.state = "paused";
+ this.turn = this.#generateTurns();
+ this.currentTurn = 1;
+ this.currentRound = 1;
+ this.maxTurn = this.getPlayersNumber();
+ this.resetScore();
+ }
+
+ setPlayerChoice(choice) {
+ if (this.state === "running") {
+ this.getCurrentPlayer().choice = choice;
+ }
+ }
+
+ *#generateTurns() {
+ this.currentRound = 1;
+ while (this.maxRound ? this.currentRound <= this.maxRound : true) {
+ this.currentTurn = 1;
+ while (this.currentTurn <= this.maxTurn) {
+ yield this.currentTurn;
+ this.currentTurn++;
+ }
+ this.currentRound++;
+ }
+ this.stop();
+ this.setGameWinners();
+ this.setGameLosers();
+ return;
+ }
+
+ /**
+ * Get a random choice from an array of choices.
+ * @param {Array} array - The choices.
+ * @returns {*} A random choice.
+ */
+ getRandomChoice(array) {
+ const randomIndex = Math.floor(Math.random() * array.length);
+ return array[randomIndex];
+ }
+
+ getOrderedScores() {
+ let scores = [];
+ this.players.forEach((player) => {
+ scores.push(player.score);
+ });
+ scores.sort((a, b) => a - b);
+
+ return scores;
+ }
+
+ setGameWinners() {
+ const scores = this.getOrderedScores();
+ const highestScore = scores.pop();
+ const winners = this.players.filter(
+ (player) => player.score === highestScore
+ );
+ this.gameWinners = winners;
+ }
+
+ setGameLosers() {
+ const scores = this.getOrderedScores();
+ const lowestScore = scores.shift();
+ const losers = this.players.filter(
+ (player) => player.score === lowestScore
+ );
+ this.gameLosers = losers;
+ }
+
+ resume() {
+ this.state = "running";
+ }
+
+ pause() {
+ this.state = "paused";
+ }
+
+ stop() {
+ this.state = "ended";
+ }
+
+ launch() {
+ this.newGame();
+ this.resume();
+ this.turn.next();
+ }
+}
+
+export default Game;
diff --git a/public/projects/js-small-apps/rock-paper-scissors/lib/class-player.js b/public/projects/js-small-apps/rock-paper-scissors/lib/class-player.js
new file mode 100644
index 0000000..8935581
--- /dev/null
+++ b/public/projects/js-small-apps/rock-paper-scissors/lib/class-player.js
@@ -0,0 +1,60 @@
+/**
+ * Player class.
+ */
+class Player {
+ #id;
+ #username = "Anonymous";
+ #choice = "";
+ #score = 0;
+ #ia = false;
+
+ /**
+ * Initialize a new Player instance.
+ * @param {Integer} id - The player id.
+ * @param {String} username - The player username.
+ * @param {Boolean} ia - True to set player as an IA.
+ */
+ constructor(id, username, ia) {
+ this.#id = id;
+ this.#username = username;
+ this.#ia = ia;
+ }
+
+ get id() {
+ return this.#id;
+ }
+
+ set username(name) {
+ this.#username = name;
+ }
+
+ get username() {
+ return this.#username;
+ }
+
+ set choice(choice) {
+ this.#choice = choice;
+ }
+
+ set score(score) {
+ this.#score = score;
+ }
+
+ get choice() {
+ return this.#choice;
+ }
+
+ get score() {
+ return this.#score;
+ }
+
+ set ia(boolean) {
+ this.#ia = boolean;
+ }
+
+ get ia() {
+ return this.#ia;
+ }
+}
+
+export default Player;
diff --git a/public/projects/js-small-apps/rock-paper-scissors/lib/class-rps-game.js b/public/projects/js-small-apps/rock-paper-scissors/lib/class-rps-game.js
new file mode 100644
index 0000000..fe517db
--- /dev/null
+++ b/public/projects/js-small-apps/rock-paper-scissors/lib/class-rps-game.js
@@ -0,0 +1,230 @@
+import Game from "./class-game.js";
+
+/**
+ * RPSGame class.
+ */
+class RPSGame extends Game {
+ #choices = ["rock", "paper", "scissors"];
+ #buttons = { rock: "", paper: "", scissors: "", newGame: "" };
+ #p1Scoring = { name: "", value: "" };
+ #p2Scoring = { name: "", value: "" };
+ #messages = "";
+ #messageIterator;
+ #timeoutId;
+
+ /**
+ * Initialize a new RPSGame instance.
+ * @param {Object[]} players - An array of player object.
+ * @param {String} players[].username - The player username.
+ * @param {Boolean} players[].ia - True to set the player as an IA.
+ * @param {Object} buttons - The buttons HTMLElement.
+ * @param {HTMLElement} buttons.rock - Button Element for rock choice.
+ * @param {HTMLElement} buttons.paper - Button Element for paper choice.
+ * @param {HTMLElement} buttons.scissors - Button Element for scissors choice.
+ * @param {HTMLElement} buttons.newGame - Button Element to start new game.
+ * @param {Object} p1Scoring - The player 1 scoring display.
+ * @param {HTMLElement} p1Scoring.name - Element to display player 1 name.
+ * @param {HTMLElement} p1Scoring.value - Element to display player 1 score.
+ * @param {Object} p2Scoring - The player 2 scoring display.
+ * @param {HTMLElement} p2Scoring.name - Element to display player 2 name.
+ * @param {HTMLElement} p2Scoring.value - Element to display player 2 score.
+ * @param {HTMLElement} messages - Element to display turn/game results.
+ */
+ constructor(
+ players,
+ buttons = { rock: "", paper: "", scissors: "", newGame: "" },
+ p1Scoring = { name: "", value: "" },
+ p2Scoring = { name: "", value: "" },
+ messages
+ ) {
+ super("Rock Paper Scissors", players);
+ this.#buttons = buttons;
+ this.#p1Scoring = p1Scoring;
+ this.#p2Scoring = p2Scoring;
+ this.#messages = messages;
+ }
+
+ get messages() {
+ return this.#messages;
+ }
+
+ set messageIterator(generator) {
+ this.#messageIterator = generator;
+ }
+
+ get messageIterator() {
+ return this.#messageIterator;
+ }
+
+ #updatePlayers() {
+ this.#p1Scoring.name.textContent = this.getPlayer(1).username;
+ this.#p2Scoring.name.textContent = this.getPlayer(2).username;
+ }
+
+ #updateScore() {
+ this.#p1Scoring.value.textContent = this.getPlayer(1).score;
+ this.#p2Scoring.value.textContent = this.getPlayer(2).score;
+ }
+
+ async #createMessage(msg, delay = 0) {
+ return new Promise(
+ (resolve) =>
+ (this.#timeoutId = setTimeout(() => {
+ resolve(msg);
+ }, delay))
+ );
+ }
+
+ async *#generateMessages() {
+ let msg;
+ msg = yield await this.#createMessage("New game, let's play!");
+
+ while (this.state === "running") {
+ for (let index = 0; index < this.getPlayersNumber(); index++) {
+ if (this.getCurrentPlayer().ia) {
+ msg = yield this.#createMessage(
+ `${this.getCurrentPlayer().username} is playing...`,
+ this.isFirstTurn() ? 1200 : 200
+ );
+ } else {
+ msg = yield this.#createMessage(
+ `${this.getCurrentPlayer().username}'s turn...`,
+ this.isFirstTurn() ? 1200 : 900
+ );
+ }
+ }
+ msg = yield this.#createMessage(
+ msg,
+ this.getCurrentPlayer().ia ? 1000 : 500
+ );
+ if (!this.isGameOver()) {
+ msg = yield this.#createMessage("New round...", 1500);
+ }
+ }
+ const winnersList = this.gameWinners.map((winner) => winner.username);
+ const losersList = this.gameLosers.map((loser) => loser.username);
+ msg = yield this.#createMessage(
+ `Winner: ${winnersList.join(", ")} / Loser: ${losersList.join(", ")}`
+ );
+ }
+
+ async printNextMessage(msg = null) {
+ if (!this.messageIterator) {
+ this.messageIterator = this.#generateMessages();
+ }
+ this.messages.textContent = await this.messageIterator
+ .next(msg)
+ .then((object) => object.value);
+ }
+
+ async #setTurnIssue() {
+ const choices = `${this.getPlayer(1).choice}-${this.getPlayer(2).choice}`;
+ let turnWinner;
+ let turnLoser;
+ let even = false;
+ let msg;
+
+ switch (choices) {
+ case "rock-paper":
+ case "paper-scissors":
+ case "scissors-rock":
+ turnWinner = this.getPlayer(2);
+ turnLoser = this.getPlayer(1);
+ break;
+ case "paper-rock":
+ case "rock-scissors":
+ case "scissors-paper":
+ turnWinner = this.getPlayer(1);
+ turnLoser = this.getPlayer(2);
+ break;
+ default:
+ even = true;
+ break;
+ }
+
+ if (!even) {
+ this.turnWinners = [turnWinner];
+ this.turnLosers = [turnLoser];
+ turnWinner.score++;
+ msg = `${turnWinner.username} wins! ${turnWinner.choice} beats ${turnLoser.choice}.`;
+ } else {
+ msg = `No winner. ${this.getPlayer(1).choice} equals to ${
+ this.getPlayer(2).choice
+ }.`;
+ }
+ await this.printNextMessage(msg);
+ this.#updateScore();
+ await this.printNextMessage();
+ }
+
+ async #getIAAction() {
+ if (this.currentTurn % 2 === 0) {
+ await this.#setTurnIssue();
+ this.turn.next();
+ await this.printNextMessage();
+ if (!this.isGameOver()) {
+ this.getCurrentPlayer().choice = this.getRandomChoice(this.#choices);
+ }
+ } else {
+ this.turn.next();
+ await this.printNextMessage();
+ this.getCurrentPlayer().choice = this.getRandomChoice(this.#choices);
+ await this.#setTurnIssue();
+ }
+ this.turn.next();
+ await this.printNextMessage();
+ }
+
+ async listen() {
+ for (const [name, element] of Object.entries(this.#buttons)) {
+ element.addEventListener("click", async (event) => {
+ event.preventDefault();
+ switch (name) {
+ case "rock":
+ case "paper":
+ case "scissors":
+ if (this.state === "running") {
+ this.setPlayerChoice(name);
+ if (this.currentTurn % 2 === 0) {
+ await this.#setTurnIssue();
+ }
+ if (this.getNextPlayer().ia) {
+ await this.#getIAAction();
+ } else {
+ this.turn.next();
+ await this.printNextMessage();
+ }
+ }
+ break;
+ case "newGame":
+ this.messageIterator = null;
+ clearTimeout(this.#timeoutId);
+ await this.launch();
+ default:
+ break;
+ }
+ });
+ }
+ }
+
+ async launch() {
+ super.launch();
+ this.#updatePlayers();
+ this.#updateScore();
+ await this.printNextMessage();
+ await this.printNextMessage();
+
+ if (this.getCurrentPlayer().ia) {
+ this.getCurrentPlayer().choice = this.getRandomChoice(this.#choices);
+ this.turn.next();
+ await this.printNextMessage();
+ }
+ }
+
+ async init() {
+ await this.launch();
+ this.listen();
+ }
+}
+
+export default RPSGame;
diff --git a/public/projects/js-small-apps/rock-paper-scissors/lib/rps-instance.js b/public/projects/js-small-apps/rock-paper-scissors/lib/rps-instance.js
new file mode 100644
index 0000000..73c5888
--- /dev/null
+++ b/public/projects/js-small-apps/rock-paper-scissors/lib/rps-instance.js
@@ -0,0 +1,70 @@
+import RPS_Game from "./rps-game.js";
+
+export default class RPS_Instance extends RPS_Game {
+ #buttons = {};
+ #scoring = { player1: '', player2: '' };
+ #resultBox = '';
+ #resultMsg = '';
+
+ constructor(player1, player2, buttons, scoring, result) {
+ super(player1, player2);
+ this.#buttons = buttons;
+ this.#scoring = scoring;
+ this.#resultBox = result;
+ this.#resultMsg = result.innerHTML;
+ }
+
+ updateScoring() {
+ for (const [name, element] of Object.entries(this.#scoring)) {
+ if ( 'player1' === name ) {
+ element.innerHTML = this.player1Score;
+ }
+
+ if ( 'player2' === name ) {
+ element.innerHTML = this.player2Score;
+ }
+ }
+ }
+
+ updateResult(result = '') {
+ let txt;
+ if ( result === 'player1' ) {
+ txt = this.player1Choice + ' beats ' + this.player2Choice + ".<br>You win!"
+ this.#resultBox.style.color = 'green';
+ } else if ( result === 'player2' ) {
+ txt = this.player1Choice + ' loses to ' + this.player2Choice + ".<br>You lose..."
+ this.#resultBox.style.color = 'red';
+ } else if ( result === 'even' ) {
+ txt = this.player1Choice + ' equals to ' + this.player2Choice + ".<br>No winner.";
+ this.#resultBox.style.color = 'black';
+ } else {
+ txt = this.#resultMsg;
+ this.#resultBox.style.color = '';
+ }
+
+ this.#resultBox.innerHTML = txt;
+ }
+
+ listen() {
+ for (const [name, element] of Object.entries(this.#buttons)) {
+ element.addEventListener('click', () => {
+ if ( 'reset' === name ) {
+ this.reset();
+ this.updateResult();
+ this.updateScoring();
+ } else {
+ this.player1Choice = name;
+ const result = this.calculateScore();
+ this.setScore(result);
+ this.updateResult(result);
+ this.updateScoring();
+ }
+ })
+ }
+ }
+
+ init() {
+ this.reset();
+ this.listen();
+ }
+}
diff --git a/public/projects/js-small-apps/rock-paper-scissors/style.css b/public/projects/js-small-apps/rock-paper-scissors/style.css
new file mode 100644
index 0000000..28d2f03
--- /dev/null
+++ b/public/projects/js-small-apps/rock-paper-scissors/style.css
@@ -0,0 +1,433 @@
+/*
+ * Base
+ */
+
+*,
+*::after,
+*::before {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ display: flex;
+ flex-flow: column nowrap;
+ background: hsl(180, 29%, 95%);
+ font-family: "Courier New", Courier, monospace;
+ font-size: 1rem;
+ line-height: 1.618;
+ min-height: 100vh;
+ max-width: 100vw;
+}
+
+button {
+ display: block;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: 100%;
+ line-height: 1.15;
+ margin: 0;
+}
+
+/*
+ * Helpers
+ */
+
+.screen-reader-txt {
+ border: 0;
+ clip: rect(1px, 1px, 1px, 1px);
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute !important;
+ word-break: normal;
+ word-wrap: normal !important;
+}
+
+/*
+ * Layout
+ */
+
+.branding {
+ margin: 2rem auto clamp(2rem, 5vh, 5rem);
+}
+
+.branding__title {
+ color: hsl(207, 85%, 47%);
+ font-size: clamp(2.5rem, 3vw, 3rem);
+ text-align: center;
+ text-shadow: 1px 1px 1px hsl(207, 85%, 27%);
+}
+
+.main {
+ display: flex;
+ flex-flow: column nowrap;
+ flex: 1;
+ margin: 3rem 0;
+}
+
+.register {
+ flex: 1;
+ display: grid;
+ grid-template-columns: repeat(
+ auto-fill,
+ min(calc(100vw - 2rem), calc(1200px / 2 - 4rem))
+ );
+ gap: 2rem;
+ align-items: center;
+ justify-content: center;
+ justify-items: center;
+ margin: auto;
+ width: min(calc(100vw - 2rem), calc(1200px - 2rem));
+}
+
+.form--register {
+ justify-self: stretch;
+}
+
+.form {
+ display: flex;
+ flex-flow: column wrap;
+ gap: clamp(1rem, 3vh, 2rem);
+}
+
+.form__fieldset {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: flex-end;
+ gap: 2rem;
+ border: 4px solid hsl(207, 85%, 45%);
+ padding: 1.7rem clamp(0.5rem, 3vw, 1.5rem);
+ position: relative;
+ margin-top: 2.5rem;
+}
+
+.form__legend {
+ background: hsl(180, 29%, 95%);
+ border: 4px solid hsl(207, 85%, 45%);
+ border-bottom-width: 0;
+ color: hsl(207, 85%, 28%);
+ font-size: 1.4rem;
+ font-weight: 600;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ padding: 0.5rem 1.3rem;
+ position: absolute;
+ top: -40px;
+ left: -4px;
+ right: -4px;
+}
+
+.form__item {
+ flex: 1 1 max-content;
+}
+
+.form__item:last-child:not(:only-of-type) {
+ margin-left: auto;
+ margin-bottom: 0.8rem;
+}
+
+.form__label {
+ display: block;
+ cursor: pointer;
+ position: relative;
+ letter-spacing: 1px;
+}
+
+.form__label--checkbox {
+ display: initial;
+}
+
+.form__label--checkbox::before {
+ position: absolute;
+ top: -1px;
+ left: -28px;
+ display: block;
+ content: "";
+ width: 1.2rem;
+ height: 1.2rem;
+ background: hsl(180, 29%, 95%);
+ border: 3px solid hsl(207, 85%, 45%);
+ color: hsl(207, 85%, 40%);
+ font-size: 1.1rem;
+ font-weight: 600;
+ line-height: 0.85;
+ text-align: center;
+}
+
+.form__input {
+ background: hsl(180, 29%, 95%);
+ border: 3px solid hsl(207, 85%, 45%);
+ font-size: inherit;
+ padding: 0.7rem 0.8rem;
+}
+
+.form__checkbox {
+ order: 2;
+ width: max-content;
+}
+
+.form__checkbox:checked ~ .form__label--checkbox::before {
+ content: "x";
+}
+
+.form__hint {
+ font-size: 0.85rem;
+ margin: 1rem 0 0;
+}
+
+.form__submit {
+ margin: 1rem 0 0 auto;
+}
+
+.preview {
+ background: hsl(207, 85%, 45%);
+ clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
+ width: clamp(18rem, 40vw, 30rem);
+ height: clamp(18rem, 40vw, 30rem);
+ position: relative;
+}
+
+.preview__content {
+ display: flex;
+ flex-flow: column wrap;
+ align-items: center;
+ justify-content: center;
+ gap: 1.5rem;
+ background: hsl(180, 29%, 95%);
+ border-radius: 50%;
+ font-size: clamp(1.2rem, 3vw, 2rem);
+ width: 80%;
+ height: 80%;
+ padding: 1rem;
+ position: relative;
+ top: 10%;
+ left: 10%;
+}
+
+.ia-badge {
+ position: relative;
+}
+
+.ia-badge::after {
+ display: block;
+ content: "IA";
+ position: absolute;
+ top: 0;
+ right: -2.5rem;
+ background: rgb(253, 206, 119);
+ border-radius: 50%;
+ font-size: 0.9rem;
+ padding: 4px 8px;
+ width: max-content;
+}
+
+.game {
+ flex: 1;
+ display: none;
+ flex-flow: column nowrap;
+ justify-content: space-between;
+ align-items: center;
+ gap: 2rem;
+ margin: auto;
+ padding: clamp(1rem, 3vh, 2rem) 0;
+ width: min(calc(100vw - 2rem), 600px);
+}
+
+.scoring-board {
+ font-family: "Courier New", Courier, monospace;
+ font-size: 1.5rem;
+ text-align: center;
+ width: 100%;
+}
+
+.scoring-board__item {
+ background: hsl(0, 0%, 100%);
+ border: 5px solid hsl(207, 85%, 47%);
+ border-radius: 3px;
+ display: inline-block;
+ margin-bottom: clamp(1rem, 3vh, 2rem);
+ padding: 1rem 0;
+ width: calc(50% - 1rem);
+ position: relative;
+}
+
+.scoring-board__item:first-child {
+ box-shadow: 1px 1px 0 2px hsl(207, 85%, 27%);
+}
+
+.scoring-board__item:not(:first-child) {
+ box-shadow: -1px 1px 0 2px hsl(207, 85%, 27%);
+ margin-left: 1rem;
+}
+
+.scoring-board__item:first-child::after {
+ content: "";
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ right: -50%;
+ background: hsl(207, 85%, 47%);
+ box-shadow: 0 3px 0 0 hsl(207, 85%, 27%);
+ height: 5px;
+ z-index: -1;
+}
+
+.scoring-board__username {
+ font-variant: small-caps;
+ letter-spacing: 1px;
+}
+
+.scoring-board__score {
+ font-size: 2rem;
+ font-weight: 600;
+ margin-top: 1rem;
+}
+
+.message-board {
+ font-size: 1.4rem;
+ text-align: center;
+ margin: 1rem auto;
+}
+
+.actions {
+ display: grid;
+ grid-template-columns: repeat(3, 33%);
+ justify-content: center;
+ gap: clamp(1rem, 3vw, 2rem);
+ font-size: 1.2rem;
+ margin-bottom: clamp(1rem, 3vh, 2rem);
+}
+
+.actions__body {
+ width: clamp(5rem, 15vw, 8rem);
+ height: clamp(4.5rem, 12vw, 7rem);
+ position: relative;
+}
+
+.actions__icon {
+ font-size: clamp(2.2rem, 7vw, 3.2rem);
+ filter: drop-shadow(1px 1px 1px hsl(207, 85%, 27%));
+ position: relative;
+ top: 4px;
+}
+
+.settings {
+ display: flex;
+ flex-flow: row wrap;
+ gap: 2rem;
+ justify-content: center;
+}
+
+.footer {
+ font-size: 0.9rem;
+ text-align: center;
+ margin: clamp(2rem, 5vh, 4rem) auto 2rem;
+}
+
+/*
+ * Components
+ */
+
+.btn {
+ font-weight: 600;
+ transition: all 0.2s ease-in-out 0s;
+}
+
+.btn:focus {
+ outline: none;
+}
+
+.btn--action {
+ background: hsl(207, 85%, 45%);
+ border: 2px solid hsl(207, 85%, 28%);
+ border-radius: 100%;
+ box-shadow: inset 0 0 0 3px hsl(207, 85%, 38%), 0 5px 0 0 hsl(207, 85%, 28%),
+ 0 5px 0 2px hsl(207, 85%, 20%);
+}
+
+.btn--action:hover,
+.btn--action:focus {
+ background: hsl(207, 85%, 52%);
+ transform: translateY(2px);
+}
+
+.btn--action:hover {
+ box-shadow: inset 0 0 0 3px hsl(207, 85%, 38%), 0 3px 0 0 hsl(207, 85%, 32%),
+ 0 3px 0 2px hsl(207, 85%, 22%);
+}
+
+.btn--action:focus {
+ box-shadow: inset 0 0 0 3px hsl(207, 85%, 38%), 0 3px 0 0 hsl(207, 85%, 32%),
+ 0 3px 0 2px hsl(207, 85%, 22%), 0 3px 0 8px hsl(204, 42%, 68%);
+}
+
+.btn--action:active {
+ box-shadow: inset 0 0 0 3px hsl(207, 85%, 38%), 0 0 0 0 hsl(207, 85%, 28%),
+ 0 0 0 1px hsl(207, 85%, 20%);
+ transform: translateY(8px);
+}
+
+.btn--new-game {
+ border: 2px solid hsl(0, 50%, 45%);
+ border-radius: 0.5rem;
+ color: hsl(0, 50%, 45%);
+ box-shadow: 1px 1px 0 1px hsl(0, 50%, 25%);
+ padding: 0.75rem 1rem;
+}
+
+.btn--new-game:hover,
+.btn--new-game:focus {
+ background: hsl(0, 35%, 90%);
+ color: hsl(0, 50%, 25%);
+ transform: translateX(-3px) translateY(-3px);
+}
+
+.btn--new-game:hover {
+ box-shadow: 3px 3px 0 1px hsl(0, 50%, 25%);
+}
+
+.btn--new-game:focus {
+ box-shadow: 3px 3px 0 1px hsl(0, 50%, 25%), 3px 3px 0 4px hsl(0, 11%, 75%);
+}
+
+.btn--new-game:active {
+ background: hsl(204, 14%, 93%);
+ color: hsl(0, 50%, 25%);
+ box-shadow: none;
+ transform: translateX(3px) translateY(3px);
+}
+
+.btn--register {
+ border: 3px solid hsl(207, 85%, 50%);
+ border-radius: 8px;
+ background-color: hsl(207, 85%, 40%);
+ box-shadow: inset 0 0 0 3px #fff, 1px 1px 0 1px hsl(207, 85%, 25%),
+ 3px 3px 0 2px hsl(207, 85%, 30%);
+ color: #fff;
+ font-size: 1.2rem;
+ font-variant: small-caps;
+ font-weight: 600;
+ text-shadow: 2px 2px 0 #000;
+ width: max-content;
+ padding: 1rem 2rem;
+}
+
+.btn--register:hover,
+.btn--register:focus {
+ background-color: hsl(207, 85%, 47%);
+ border-color: hsl(207, 85%, 57%);
+ box-shadow: inset 0 0 0 3px #fff, 1px 1px 0 1px hsl(207, 85%, 25%),
+ 5px 5px 0 2px hsl(207, 85%, 30%);
+ transform: translateY(-3px) translateX(-3px);
+}
+
+.btn--register:active {
+ background-color: hsl(207, 85%, 35%);
+ border-color: hsl(207, 85%, 45%);
+ box-shadow: inset 0 0 0 3px #fff, 1px 1px 0 1px hsl(207, 85%, 25%),
+ 0 0 0 1px hsl(207, 85%, 30%);
+ transform: translateY(3px) translateX(3px);
+}
diff --git a/public/projects/js-small-apps/users-list/README.md b/public/projects/js-small-apps/users-list/README.md
new file mode 100644
index 0000000..4c10232
--- /dev/null
+++ b/public/projects/js-small-apps/users-list/README.md
@@ -0,0 +1,18 @@
+# Users list
+
+An users list implementation using Javascript.
+
+The implementation is inspired by the [CauseEffect app from @florinpop17 repo](https://github.com/florinpop17/app-ideas/blob/master/Projects/1-Beginner/Cause-Effect-App.md).
+
+However, I made small changes:
+
+- Instead of hard coding the users objects, this implementation uses `fetch()` to retrieve data from an API.
+- I implemented "routing": user info can be displayed if URL contains the user parameter.
+
+## Preview
+
+You can see a live preview here: https://demo.armandphilippot.com/#users-list
+
+## License
+
+This project is open-source and available under the [MIT License](../LICENSE).
diff --git a/public/projects/js-small-apps/users-list/app.js b/public/projects/js-small-apps/users-list/app.js
new file mode 100644
index 0000000..c9f998d
--- /dev/null
+++ b/public/projects/js-small-apps/users-list/app.js
@@ -0,0 +1,187 @@
+/**
+ * Retrieve data from an API.
+ * @param {String} api - The API url.
+ * @returns {Promise} The result from api fetching.
+ */
+async function fetchData(api) {
+ const headers = new Headers();
+ const init = {
+ method: "GET",
+ headers,
+ mode: "cors",
+ cache: "default",
+ };
+ const response = await fetch(api, init);
+ const result = await response.json();
+ return result;
+}
+
+/**
+ * Create a list item with a link.
+ * @param {Object} data - An object containing an id and an username.
+ * @returns {HTMLElement} A list item containing a link.
+ */
+function getListItem(data) {
+ const item = document.createElement("li");
+ const link = document.createElement("a");
+ link.textContent = data.username;
+ link.href = `?user=${data.id}`;
+ link.id = `user-${data.id}`;
+ link.classList.add("users-list__link");
+ item.appendChild(link);
+ item.classList.add("users-list__item");
+ return item;
+}
+
+/**
+ * Print the users list.
+ * @param {Object[]} users - An array of user object.
+ */
+function printUsersList(users) {
+ const list = document.querySelector(".users-list");
+ users.map((user) => list.appendChild(getListItem(user)));
+}
+
+/**
+ * Create a description term element.
+ * @param {String} body - The description term body.
+ * @returns {HTMLElement} The description term element.
+ */
+function createUserInfoDt(body) {
+ const dt = document.createElement("dt");
+ dt.classList.add("user-info__label");
+ dt.textContent = body;
+ return dt;
+}
+
+/**
+ * Create a description details element.
+ * @param {String} body - The description details body.
+ * @param {Boolean} [hasHTML] - True if body contains HTML; false otherwise.
+ * @returns {HTMLElement} The description details element.
+ */
+function createUserInfoDd(body, hasHTML = false) {
+ const dd = document.createElement("dd");
+ dd.classList.add("user-info__content");
+ hasHTML ? (dd.innerHTML = body) : (dd.textContent = body);
+ return dd;
+}
+
+/**
+ * Get the markup to display info for a given user.
+ * @param {Object} user - An user with fullname, email address, phone & website.
+ * @returns {Object} Two HTMLElement: title and body.
+ */
+function getUserInfoTemplate(user) {
+ const userAddress = `${user.address.suite}<br />${user.address.street}<br />${user.address.zipcode} ${user.address.city}`;
+
+ const fullName = document.createElement("h2");
+ const userDetails = document.createElement("dl");
+ const emailLabel = createUserInfoDt("Email");
+ const email = createUserInfoDd(user.email);
+ const addressLabel = createUserInfoDt("Address");
+ const address = createUserInfoDd(userAddress, true);
+ const phoneLabel = createUserInfoDt("Phone");
+ const phone = createUserInfoDd(user.phone);
+ const websiteLabel = createUserInfoDt("Website");
+ const website = createUserInfoDd(user.website);
+
+ fullName.textContent = user.name;
+ fullName.classList.add("user-info__title");
+ userDetails.classList.add("user-info__body");
+ userDetails.append(
+ emailLabel,
+ email,
+ addressLabel,
+ address,
+ phoneLabel,
+ phone,
+ websiteLabel,
+ website
+ );
+
+ return {
+ title: fullName,
+ body: userDetails,
+ };
+}
+
+/**
+ * Print the info for a given user.
+ * @param {Integer} userId - The user id.
+ */
+async function printUserInfo(userId) {
+ const api = `https://jsonplaceholder.typicode.com/users/${userId}`;
+ const user = await fetchData(api).then((data) => data);
+ const userInfo = document.querySelector(".user-info");
+ const userInfoTemplate = getUserInfoTemplate(user);
+ userInfo.hasChildNodes()
+ ? userInfo.replaceChildren(userInfoTemplate.title, userInfoTemplate.body)
+ : userInfo.append(userInfoTemplate.title, userInfoTemplate.body);
+}
+
+/**
+ * Add a class "active" to a link.
+ * @param {Integer} linkId - The link id.
+ */
+function addActiveClassTo(linkId) {
+ const link = document.getElementById(linkId);
+ link.classList.add("active");
+}
+
+/**
+ * Print the user info on click.
+ * @param {MouseEvent} e - The click event.
+ * @param {NodeList} links - The users links.
+ */
+function handleClick(e, links) {
+ e.preventDefault();
+ const userId = e.target.id.split("user-")[1];
+ const linkId = `user-${userId}`;
+ history.pushState({ userId }, e.target.textContent, e.target.href);
+ printUserInfo(userId);
+
+ for (let i = 0; i < links.length; i++) {
+ links[i].classList.remove("active");
+ }
+
+ addActiveClassTo(linkId);
+}
+
+/**
+ * Listen all users links.
+ */
+function listenUsersLinks() {
+ const usersLinks = document.querySelectorAll(".users-list__link");
+ for (let i = 0; i < usersLinks.length; i++) {
+ const link = usersLinks[i];
+ link.addEventListener("click", (e) => handleClick(e, usersLinks));
+ }
+}
+
+/**
+ * Load user info when URL refers to an user page.
+ */
+function listenURL() {
+ const currentURL = window.location.href;
+ const isUserPage = currentURL.includes("user=");
+ if (isUserPage) {
+ const userId = currentURL.split("?user=")[1];
+ const linkId = `user-${userId}`;
+ printUserInfo(userId);
+ addActiveClassTo(linkId);
+ }
+}
+
+/**
+ * Init the app.
+ */
+async function init() {
+ const api = "https://jsonplaceholder.typicode.com/users";
+ const users = await fetchData(api).then((data) => data);
+ printUsersList(users);
+ listenUsersLinks();
+ listenURL();
+}
+
+init();
diff --git a/public/projects/js-small-apps/users-list/index.html b/public/projects/js-small-apps/users-list/index.html
new file mode 100644
index 0000000..6472ffe
--- /dev/null
+++ b/public/projects/js-small-apps/users-list/index.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>User list</title>
+ <link rel="stylesheet" href="style.css" />
+ </head>
+ <body>
+ <header class="header">
+ <h1 class="branding">Users list</h1>
+ </header>
+ <main class="main">
+ <div class="users">
+ <h2 class="users__title">Users</h2>
+ <p class="users__instructions">
+ Select an user to see its personal details.
+ </p>
+ <ul class="users-list"></ul>
+ </div>
+ <div class="user-info"></div>
+ </main>
+ <footer class="footer">
+ <p class="copyright">Users list. MIT 2021. Armand Philippot.</p>
+ </footer>
+ <script src="app.js"></script>
+ </body>
+</html>
diff --git a/public/projects/js-small-apps/users-list/style.css b/public/projects/js-small-apps/users-list/style.css
new file mode 100644
index 0000000..4923c6f
--- /dev/null
+++ b/public/projects/js-small-apps/users-list/style.css
@@ -0,0 +1,104 @@
+*,
+*::after,
+*::before {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ background: #fff;
+ color: #000;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 16px;
+ line-height: 1.618;
+ display: flex;
+ flex-flow: column;
+ min-height: 100vh;
+}
+
+.header,
+.main,
+.footer {
+ width: min(calc(100vw - 2rem), 100ch);
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.header,
+.footer {
+ padding: 1rem 0;
+ text-align: center;
+}
+
+.branding {
+ color: hsl(219, 64%, 35%);
+}
+
+.main {
+ flex: 1;
+ display: flex;
+ flex-flow: row wrap;
+ gap: 1rem;
+ margin: 2rem auto;
+}
+
+.users,
+.user-info {
+ border: 2px solid hsl(219, 64%, 35%);
+}
+
+.users {
+ flex: 1 1 35ch;
+ padding: 1rem 0;
+}
+
+.users__title,
+.users__instructions {
+ padding: 0 1rem;
+}
+
+.users-list {
+ list-style-type: none;
+ margin-top: 1rem;
+}
+
+.users-list__link {
+ display: block;
+ padding: 0.2rem 1rem;
+ color: hsl(219, 64%, 35%);
+}
+
+.users-list__link:hover,
+.users-list__link:focus {
+ background: hsl(219, 64%, 35%);
+ color: #fff;
+}
+
+.users-list__link:active {
+ text-decoration: none;
+}
+
+.users-list__link.active {
+ background: hsl(219, 64%, 25%);
+ color: #fff;
+ text-decoration: none;
+}
+
+.user-info {
+ flex: 1 1 60ch;
+ padding: 1rem;
+}
+
+.user-info__title {
+ margin-bottom: 1rem;
+}
+
+.user-info__label {
+ font-weight: 600;
+ margin-top: 1rem;
+}
+
+.copyright {
+ font-size: 0.9rem;
+}
diff --git a/public/projects/js-small-apps/yarn.lock b/public/projects/js-small-apps/yarn.lock
new file mode 100644
index 0000000..b3bfcdd
--- /dev/null
+++ b/public/projects/js-small-apps/yarn.lock
@@ -0,0 +1,1795 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0":
+ version "7.15.8"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.15.8.tgz#45990c47adadb00c03677baa89221f7cc23d2503"
+ integrity sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==
+ dependencies:
+ "@babel/highlight" "^7.14.5"
+
+"@babel/helper-validator-identifier@^7.14.5":
+ version "7.15.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389"
+ integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==
+
+"@babel/highlight@^7.14.5":
+ version "7.14.5"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9"
+ integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.14.5"
+ chalk "^2.0.0"
+ js-tokens "^4.0.0"
+
+"@commitlint/cli@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-16.2.1.tgz#ca4e557829a2755f0e1f0cd69b56b83ce2510173"
+ integrity sha512-zfKf+B9osuiDbxGMJ7bWFv7XFCW8wlQYPtCffNp7Ukdb7mdrep5R9e03vPUZysnwp8NX6hg05kPEvnD/wRIGWw==
+ dependencies:
+ "@commitlint/format" "^16.2.1"
+ "@commitlint/lint" "^16.2.1"
+ "@commitlint/load" "^16.2.1"
+ "@commitlint/read" "^16.2.1"
+ "@commitlint/types" "^16.2.1"
+ lodash "^4.17.19"
+ resolve-from "5.0.0"
+ resolve-global "1.0.0"
+ yargs "^17.0.0"
+
+"@commitlint/config-conventional@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/config-conventional/-/config-conventional-16.2.1.tgz#2cf47b505fb259777c063538c8498d8fd9b47779"
+ integrity sha512-cP9gArx7gnaj4IqmtCIcHdRjTYdRUi6lmGE+lOzGGjGe45qGOS8nyQQNvkNy2Ey2VqoSWuXXkD8zCUh6EHf1Ww==
+ dependencies:
+ conventional-changelog-conventionalcommits "^4.3.1"
+
+"@commitlint/config-validator@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/config-validator/-/config-validator-16.2.1.tgz#794e769afd4756e4cf1bfd823b6612932e39c56d"
+ integrity sha512-hogSe0WGg7CKmp4IfNbdNES3Rq3UEI4XRPB8JL4EPgo/ORq5nrGTVzxJh78omibNuB8Ho4501Czb1Er1MoDWpw==
+ dependencies:
+ "@commitlint/types" "^16.2.1"
+ ajv "^6.12.6"
+
+"@commitlint/ensure@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/ensure/-/ensure-16.2.1.tgz#0fc538173f95c1eb2694eeedb79cab478347f16f"
+ integrity sha512-/h+lBTgf1r5fhbDNHOViLuej38i3rZqTQnBTk+xEg+ehOwQDXUuissQ5GsYXXqI5uGy+261ew++sT4EA3uBJ+A==
+ dependencies:
+ "@commitlint/types" "^16.2.1"
+ lodash "^4.17.19"
+
+"@commitlint/execute-rule@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/execute-rule/-/execute-rule-16.2.1.tgz#60be73be4b9af97a41546e7ce59fdd33787c65f8"
+ integrity sha512-oSls82fmUTLM6cl5V3epdVo4gHhbmBFvCvQGHBRdQ50H/690Uq1Dyd7hXMuKITCIdcnr9umyDkr8r5C6HZDF3g==
+
+"@commitlint/format@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/format/-/format-16.2.1.tgz#6e673f710c799be78e68b2682323e04f75080d07"
+ integrity sha512-Yyio9bdHWmNDRlEJrxHKglamIk3d6hC0NkEUW6Ti6ipEh2g0BAhy8Od6t4vLhdZRa1I2n+gY13foy+tUgk0i1Q==
+ dependencies:
+ "@commitlint/types" "^16.2.1"
+ chalk "^4.0.0"
+
+"@commitlint/is-ignored@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/is-ignored/-/is-ignored-16.2.1.tgz#cc688ec73a3d204b90f8086821a08814da461e5e"
+ integrity sha512-exl8HRzTIfb1YvDJp2b2HU5z1BT+9tmgxR2XF0YEzkMiCIuEKh+XLeocPr1VcvAKXv3Cmv5X/OfNRp+i+/HIhQ==
+ dependencies:
+ "@commitlint/types" "^16.2.1"
+ semver "7.3.5"
+
+"@commitlint/lint@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/lint/-/lint-16.2.1.tgz#c773f082cd4f69cb7807b805b691d2a52c732f97"
+ integrity sha512-fNINQ3X2ZqsCkNB3Z0Z8ElmhewqrS3gy2wgBTx97BkcjOWiyPAGwDJ752hwrsUnWAVBRztgw826n37xPzxsOgg==
+ dependencies:
+ "@commitlint/is-ignored" "^16.2.1"
+ "@commitlint/parse" "^16.2.1"
+ "@commitlint/rules" "^16.2.1"
+ "@commitlint/types" "^16.2.1"
+
+"@commitlint/load@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/load/-/load-16.2.1.tgz#301bda1bff66b3e40a85819f854eda72538d8e24"
+ integrity sha512-oSpz0jTyVI/A1AIImxJINTLDOMB8YF7lWGm+Jg5wVWM0r7ucpuhyViVvpSRTgvL0z09oIxlctyFGWUQQpI42uw==
+ dependencies:
+ "@commitlint/config-validator" "^16.2.1"
+ "@commitlint/execute-rule" "^16.2.1"
+ "@commitlint/resolve-extends" "^16.2.1"
+ "@commitlint/types" "^16.2.1"
+ "@types/node" ">=12"
+ chalk "^4.0.0"
+ cosmiconfig "^7.0.0"
+ cosmiconfig-typescript-loader "^1.0.0"
+ lodash "^4.17.19"
+ resolve-from "^5.0.0"
+ typescript "^4.4.3"
+
+"@commitlint/message@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/message/-/message-16.2.1.tgz#bc6a0fa446a746ac2ca78cf372e4cec48daf620d"
+ integrity sha512-2eWX/47rftViYg7a3axYDdrgwKv32mxbycBJT6OQY/MJM7SUfYNYYvbMFOQFaA4xIVZt7t2Alyqslbl6blVwWw==
+
+"@commitlint/parse@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/parse/-/parse-16.2.1.tgz#50b359cb711ec566d2ee236a8e4c6baca07b77c0"
+ integrity sha512-2NP2dDQNL378VZYioLrgGVZhWdnJO4nAxQl5LXwYb08nEcN+cgxHN1dJV8OLJ5uxlGJtDeR8UZZ1mnQ1gSAD/g==
+ dependencies:
+ "@commitlint/types" "^16.2.1"
+ conventional-changelog-angular "^5.0.11"
+ conventional-commits-parser "^3.2.2"
+
+"@commitlint/read@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/read/-/read-16.2.1.tgz#e0539205d77cdb6879b560f95e5fb251e0c6f562"
+ integrity sha512-tViXGuaxLTrw2r7PiYMQOFA2fueZxnnt0lkOWqKyxT+n2XdEMGYcI9ID5ndJKXnfPGPppD0w/IItKsIXlZ+alw==
+ dependencies:
+ "@commitlint/top-level" "^16.2.1"
+ "@commitlint/types" "^16.2.1"
+ fs-extra "^10.0.0"
+ git-raw-commits "^2.0.0"
+
+"@commitlint/resolve-extends@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/resolve-extends/-/resolve-extends-16.2.1.tgz#2f7833a5a3a7aa79f508e59fcb0f1d33c45ed360"
+ integrity sha512-NbbCMPKTFf2J805kwfP9EO+vV+XvnaHRcBy6ud5dF35dxMsvdJqke54W3XazXF1ZAxC4a3LBy4i/GNVBAthsEg==
+ dependencies:
+ "@commitlint/config-validator" "^16.2.1"
+ "@commitlint/types" "^16.2.1"
+ import-fresh "^3.0.0"
+ lodash "^4.17.19"
+ resolve-from "^5.0.0"
+ resolve-global "^1.0.0"
+
+"@commitlint/rules@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/rules/-/rules-16.2.1.tgz#7264aa1c754e1c212aeceb27e5eb380cfa7bb233"
+ integrity sha512-ZFezJXQaBBso+BOTre/+1dGCuCzlWVaeLiVRGypI53qVgPMzQqZhkCcrxBFeqB87qeyzr4A4EoG++IvITwwpIw==
+ dependencies:
+ "@commitlint/ensure" "^16.2.1"
+ "@commitlint/message" "^16.2.1"
+ "@commitlint/to-lines" "^16.2.1"
+ "@commitlint/types" "^16.2.1"
+ execa "^5.0.0"
+
+"@commitlint/to-lines@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/to-lines/-/to-lines-16.2.1.tgz#42d000f34dc0406f514991e86237fdab5e8affd0"
+ integrity sha512-9/VjpYj5j1QeY3eiog1zQWY6axsdWAc0AonUUfyZ7B0MVcRI0R56YsHAfzF6uK/g/WwPZaoe4Lb1QCyDVnpVaQ==
+
+"@commitlint/top-level@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/top-level/-/top-level-16.2.1.tgz#bdaa53ab3d8970e0288879f1a342a8c2dfe01583"
+ integrity sha512-lS6GSieHW9y6ePL73ied71Z9bOKyK+Ib9hTkRsB8oZFAyQZcyRwq2w6nIa6Fngir1QW51oKzzaXfJL94qwImyw==
+ dependencies:
+ find-up "^5.0.0"
+
+"@commitlint/types@^16.2.1":
+ version "16.2.1"
+ resolved "https://registry.yarnpkg.com/@commitlint/types/-/types-16.2.1.tgz#f25d373b88b01e51fc3fa44488101361945a61bd"
+ integrity sha512-7/z7pA7BM0i8XvMSBynO7xsB3mVQPUZbVn6zMIlp/a091XJ3qAXRXc+HwLYhiIdzzS5fuxxNIHZMGHVD4HJxdA==
+ dependencies:
+ chalk "^4.0.0"
+
+"@cspotcode/source-map-consumer@0.8.0":
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
+ integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==
+
+"@cspotcode/source-map-support@0.7.0":
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5"
+ integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==
+ dependencies:
+ "@cspotcode/source-map-consumer" "0.8.0"
+
+"@hutson/parse-repository-url@^3.0.0":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340"
+ integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==
+
+"@tsconfig/node10@^1.0.7":
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9"
+ integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==
+
+"@tsconfig/node12@^1.0.7":
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c"
+ integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==
+
+"@tsconfig/node14@^1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2"
+ integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==
+
+"@tsconfig/node16@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
+ integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==
+
+"@types/minimist@^1.2.0":
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
+ integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
+
+"@types/node@>=12":
+ version "17.0.18"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.18.tgz#3b4fed5cfb58010e3a2be4b6e74615e4847f1074"
+ integrity sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==
+
+"@types/normalize-package-data@^2.4.0":
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
+ integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
+
+"@types/parse-json@^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
+ integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
+
+JSONStream@^1.0.4:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
+ integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==
+ dependencies:
+ jsonparse "^1.2.0"
+ through ">=2.2.7 <3"
+
+acorn-walk@^8.1.1:
+ version "8.2.0"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
+ integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
+
+acorn@^8.4.1:
+ version "8.7.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
+ integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
+
+add-stream@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa"
+ integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo=
+
+ajv@^6.12.6:
+ version "6.12.6"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+ integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+ dependencies:
+ fast-deep-equal "^3.1.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+ansi-regex@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+ integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+ dependencies:
+ color-convert "^1.9.0"
+
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+ integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+ dependencies:
+ color-convert "^2.0.1"
+
+arg@^4.1.0:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
+ integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
+
+array-ify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece"
+ integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=
+
+arrify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+ integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
+
+balanced-match@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+ integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+buffer-from@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+ integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+callsites@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+ integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+camelcase-keys@^6.2.2:
+ version "6.2.2"
+ resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
+ integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==
+ dependencies:
+ camelcase "^5.3.1"
+ map-obj "^4.0.0"
+ quick-lru "^4.0.1"
+
+camelcase@^5.3.1:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+ integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+chalk@^2.0.0, chalk@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+ integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+chalk@^4.0.0:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+ integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+ dependencies:
+ ansi-styles "^4.1.0"
+ supports-color "^7.1.0"
+
+cliui@^7.0.2:
+ version "7.0.4"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
+ integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
+ dependencies:
+ string-width "^4.2.0"
+ strip-ansi "^6.0.0"
+ wrap-ansi "^7.0.0"
+
+color-convert@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+ integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+ dependencies:
+ color-name "1.1.3"
+
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+ integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ dependencies:
+ color-name "~1.1.4"
+
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+ integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+compare-func@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3"
+ integrity sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==
+ dependencies:
+ array-ify "^1.0.0"
+ dot-prop "^5.1.0"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+concat-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1"
+ integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==
+ dependencies:
+ buffer-from "^1.0.0"
+ inherits "^2.0.3"
+ readable-stream "^3.0.2"
+ typedarray "^0.0.6"
+
+conventional-changelog-angular@^5.0.11, conventional-changelog-angular@^5.0.12:
+ version "5.0.13"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz#896885d63b914a70d4934b59d2fe7bde1832b28c"
+ integrity sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==
+ dependencies:
+ compare-func "^2.0.0"
+ q "^1.5.1"
+
+conventional-changelog-atom@^2.0.8:
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-atom/-/conventional-changelog-atom-2.0.8.tgz#a759ec61c22d1c1196925fca88fe3ae89fd7d8de"
+ integrity sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==
+ dependencies:
+ q "^1.5.1"
+
+conventional-changelog-codemirror@^2.0.8:
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-codemirror/-/conventional-changelog-codemirror-2.0.8.tgz#398e9530f08ce34ec4640af98eeaf3022eb1f7dc"
+ integrity sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==
+ dependencies:
+ q "^1.5.1"
+
+conventional-changelog-config-spec@2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-config-spec/-/conventional-changelog-config-spec-2.1.0.tgz#874a635287ef8b581fd8558532bf655d4fb59f2d"
+ integrity sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ==
+
+conventional-changelog-conventionalcommits@4.6.1, conventional-changelog-conventionalcommits@^4.3.1, conventional-changelog-conventionalcommits@^4.5.0:
+ version "4.6.1"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.1.tgz#f4c0921937050674e578dc7875f908351ccf4014"
+ integrity sha512-lzWJpPZhbM1R0PIzkwzGBCnAkH5RKJzJfFQZcl/D+2lsJxAwGnDKBqn/F4C1RD31GJNn8NuKWQzAZDAVXPp2Mw==
+ dependencies:
+ compare-func "^2.0.0"
+ lodash "^4.17.15"
+ q "^1.5.1"
+
+conventional-changelog-core@^4.2.1:
+ version "4.2.4"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz#e50d047e8ebacf63fac3dc67bf918177001e1e9f"
+ integrity sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==
+ dependencies:
+ add-stream "^1.0.0"
+ conventional-changelog-writer "^5.0.0"
+ conventional-commits-parser "^3.2.0"
+ dateformat "^3.0.0"
+ get-pkg-repo "^4.0.0"
+ git-raw-commits "^2.0.8"
+ git-remote-origin-url "^2.0.0"
+ git-semver-tags "^4.1.1"
+ lodash "^4.17.15"
+ normalize-package-data "^3.0.0"
+ q "^1.5.1"
+ read-pkg "^3.0.0"
+ read-pkg-up "^3.0.0"
+ through2 "^4.0.0"
+
+conventional-changelog-ember@^2.0.9:
+ version "2.0.9"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-ember/-/conventional-changelog-ember-2.0.9.tgz#619b37ec708be9e74a220f4dcf79212ae1c92962"
+ integrity sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==
+ dependencies:
+ q "^1.5.1"
+
+conventional-changelog-eslint@^3.0.9:
+ version "3.0.9"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.9.tgz#689bd0a470e02f7baafe21a495880deea18b7cdb"
+ integrity sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==
+ dependencies:
+ q "^1.5.1"
+
+conventional-changelog-express@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-express/-/conventional-changelog-express-2.0.6.tgz#420c9d92a347b72a91544750bffa9387665a6ee8"
+ integrity sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==
+ dependencies:
+ q "^1.5.1"
+
+conventional-changelog-jquery@^3.0.11:
+ version "3.0.11"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.11.tgz#d142207400f51c9e5bb588596598e24bba8994bf"
+ integrity sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==
+ dependencies:
+ q "^1.5.1"
+
+conventional-changelog-jshint@^2.0.9:
+ version "2.0.9"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.9.tgz#f2d7f23e6acd4927a238555d92c09b50fe3852ff"
+ integrity sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==
+ dependencies:
+ compare-func "^2.0.0"
+ q "^1.5.1"
+
+conventional-changelog-preset-loader@^2.3.4:
+ version "2.3.4"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz#14a855abbffd59027fd602581f1f34d9862ea44c"
+ integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==
+
+conventional-changelog-writer@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-5.0.0.tgz#c4042f3f1542f2f41d7d2e0d6cad23aba8df8eec"
+ integrity sha512-HnDh9QHLNWfL6E1uHz6krZEQOgm8hN7z/m7tT16xwd802fwgMN0Wqd7AQYVkhpsjDUx/99oo+nGgvKF657XP5g==
+ dependencies:
+ conventional-commits-filter "^2.0.7"
+ dateformat "^3.0.0"
+ handlebars "^4.7.6"
+ json-stringify-safe "^5.0.1"
+ lodash "^4.17.15"
+ meow "^8.0.0"
+ semver "^6.0.0"
+ split "^1.0.0"
+ through2 "^4.0.0"
+
+conventional-changelog@3.1.24:
+ version "3.1.24"
+ resolved "https://registry.yarnpkg.com/conventional-changelog/-/conventional-changelog-3.1.24.tgz#ebd180b0fd1b2e1f0095c4b04fd088698348a464"
+ integrity sha512-ed6k8PO00UVvhExYohroVPXcOJ/K1N0/drJHx/faTH37OIZthlecuLIRX/T6uOp682CAoVoFpu+sSEaeuH6Asg==
+ dependencies:
+ conventional-changelog-angular "^5.0.12"
+ conventional-changelog-atom "^2.0.8"
+ conventional-changelog-codemirror "^2.0.8"
+ conventional-changelog-conventionalcommits "^4.5.0"
+ conventional-changelog-core "^4.2.1"
+ conventional-changelog-ember "^2.0.9"
+ conventional-changelog-eslint "^3.0.9"
+ conventional-changelog-express "^2.0.6"
+ conventional-changelog-jquery "^3.0.11"
+ conventional-changelog-jshint "^2.0.9"
+ conventional-changelog-preset-loader "^2.3.4"
+
+conventional-commits-filter@^2.0.7:
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz#f8d9b4f182fce00c9af7139da49365b136c8a0b3"
+ integrity sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==
+ dependencies:
+ lodash.ismatch "^4.4.0"
+ modify-values "^1.0.0"
+
+conventional-commits-parser@^3.2.0, conventional-commits-parser@^3.2.2:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.2.3.tgz#fc43704698239451e3ef35fd1d8ed644f46bd86e"
+ integrity sha512-YyRDR7On9H07ICFpRm/igcdjIqebXbvf4Cff+Pf0BrBys1i1EOzx9iFXNlAbdrLAR8jf7bkUYkDAr8pEy0q4Pw==
+ dependencies:
+ JSONStream "^1.0.4"
+ is-text-path "^1.0.1"
+ lodash "^4.17.15"
+ meow "^8.0.0"
+ split2 "^3.0.0"
+ through2 "^4.0.0"
+
+conventional-recommended-bump@6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/conventional-recommended-bump/-/conventional-recommended-bump-6.1.0.tgz#cfa623285d1de554012f2ffde70d9c8a22231f55"
+ integrity sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw==
+ dependencies:
+ concat-stream "^2.0.0"
+ conventional-changelog-preset-loader "^2.3.4"
+ conventional-commits-filter "^2.0.7"
+ conventional-commits-parser "^3.2.0"
+ git-raw-commits "^2.0.8"
+ git-semver-tags "^4.1.1"
+ meow "^8.0.0"
+ q "^1.5.1"
+
+core-util-is@~1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
+ integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
+
+cosmiconfig-typescript-loader@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-1.0.5.tgz#22373003194a1887bbccbdfd05a13501397109a8"
+ integrity sha512-FL/YR1nb8hyN0bAcP3MBaIoZravfZtVsN/RuPnoo6UVjqIrDxSNIpXHCGgJe0ZWy5yImpyD6jq5wCJ5f1nUv8g==
+ dependencies:
+ cosmiconfig "^7"
+ ts-node "^10.5.0"
+
+cosmiconfig@^7, cosmiconfig@^7.0.0:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d"
+ integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==
+ dependencies:
+ "@types/parse-json" "^4.0.0"
+ import-fresh "^3.2.1"
+ parse-json "^5.0.0"
+ path-type "^4.0.0"
+ yaml "^1.10.0"
+
+create-require@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
+ integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
+
+cross-spawn@^7.0.3:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+ integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+ dependencies:
+ path-key "^3.1.0"
+ shebang-command "^2.0.0"
+ which "^2.0.1"
+
+dargs@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc"
+ integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==
+
+dateformat@^3.0.0:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
+ integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
+
+decamelize-keys@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
+ integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
+ dependencies:
+ decamelize "^1.1.0"
+ map-obj "^1.0.0"
+
+decamelize@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+ integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+
+detect-indent@^6.0.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6"
+ integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==
+
+detect-newline@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
+ integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
+
+diff@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
+ integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
+
+dot-prop@^5.1.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
+ integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==
+ dependencies:
+ is-obj "^2.0.0"
+
+dotgitignore@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/dotgitignore/-/dotgitignore-2.1.0.tgz#a4b15a4e4ef3cf383598aaf1dfa4a04bcc089b7b"
+ integrity sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==
+ dependencies:
+ find-up "^3.0.0"
+ minimatch "^3.0.4"
+
+emoji-regex@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+ integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+error-ex@^1.3.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+ integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+ dependencies:
+ is-arrayish "^0.2.1"
+
+escalade@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+ integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+ integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+execa@^5.0.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
+ integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
+ dependencies:
+ cross-spawn "^7.0.3"
+ get-stream "^6.0.0"
+ human-signals "^2.1.0"
+ is-stream "^2.0.0"
+ merge-stream "^2.0.0"
+ npm-run-path "^4.0.1"
+ onetime "^5.1.2"
+ signal-exit "^3.0.3"
+ strip-final-newline "^2.0.0"
+
+fast-deep-equal@^3.1.1:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+ integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+ integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+figures@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
+ integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
+ dependencies:
+ escape-string-regexp "^1.0.5"
+
+find-up@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+ integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
+ dependencies:
+ locate-path "^2.0.0"
+
+find-up@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+ integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+ dependencies:
+ locate-path "^3.0.0"
+
+find-up@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+ integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+ dependencies:
+ locate-path "^5.0.0"
+ path-exists "^4.0.0"
+
+find-up@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
+ integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+ dependencies:
+ locate-path "^6.0.0"
+ path-exists "^4.0.0"
+
+fs-access@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a"
+ integrity sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=
+ dependencies:
+ null-check "^1.0.0"
+
+fs-extra@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1"
+ integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==
+ dependencies:
+ graceful-fs "^4.2.0"
+ jsonfile "^6.0.1"
+ universalify "^2.0.0"
+
+function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+get-caller-file@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+ integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+get-pkg-repo@^4.0.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz#75973e1c8050c73f48190c52047c4cee3acbf385"
+ integrity sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==
+ dependencies:
+ "@hutson/parse-repository-url" "^3.0.0"
+ hosted-git-info "^4.0.0"
+ through2 "^2.0.0"
+ yargs "^16.2.0"
+
+get-stream@^6.0.0:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
+ integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
+
+git-raw-commits@^2.0.0, git-raw-commits@^2.0.8:
+ version "2.0.10"
+ resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.10.tgz#e2255ed9563b1c9c3ea6bd05806410290297bbc1"
+ integrity sha512-sHhX5lsbG9SOO6yXdlwgEMQ/ljIn7qMpAbJZCGfXX2fq5T8M5SrDnpYk9/4HswTildcIqatsWa91vty6VhWSaQ==
+ dependencies:
+ dargs "^7.0.0"
+ lodash "^4.17.15"
+ meow "^8.0.0"
+ split2 "^3.0.0"
+ through2 "^4.0.0"
+
+git-remote-origin-url@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz#5282659dae2107145a11126112ad3216ec5fa65f"
+ integrity sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=
+ dependencies:
+ gitconfiglocal "^1.0.0"
+ pify "^2.3.0"
+
+git-semver-tags@^4.0.0, git-semver-tags@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/git-semver-tags/-/git-semver-tags-4.1.1.tgz#63191bcd809b0ec3e151ba4751c16c444e5b5780"
+ integrity sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==
+ dependencies:
+ meow "^8.0.0"
+ semver "^6.0.0"
+
+gitconfiglocal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b"
+ integrity sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=
+ dependencies:
+ ini "^1.3.2"
+
+global-dirs@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445"
+ integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=
+ dependencies:
+ ini "^1.3.4"
+
+graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
+ version "4.2.8"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
+ integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
+
+handlebars@^4.7.6:
+ version "4.7.7"
+ resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1"
+ 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"
+ optionalDependencies:
+ uglify-js "^3.1.4"
+
+hard-rejection@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
+ integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+ integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-flag@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+ integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+hosted-git-info@^2.1.4:
+ version "2.8.9"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
+ integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
+
+hosted-git-info@^4.0.0, hosted-git-info@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961"
+ integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==
+ dependencies:
+ lru-cache "^6.0.0"
+
+human-signals@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
+ integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
+
+husky@^7.0.2:
+ version "7.0.4"
+ resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535"
+ integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==
+
+import-fresh@^3.0.0, import-fresh@^3.2.1:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
+ integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
+ dependencies:
+ parent-module "^1.0.0"
+ resolve-from "^4.0.0"
+
+indent-string@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
+ integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
+
+inherits@^2.0.3, inherits@~2.0.3:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+ini@^1.3.2, ini@^1.3.4:
+ version "1.3.8"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
+ integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+ integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
+
+is-core-module@^2.2.0, is-core-module@^2.5.0:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548"
+ integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==
+ dependencies:
+ has "^1.0.3"
+
+is-fullwidth-code-point@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+ integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-obj@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
+ integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
+
+is-plain-obj@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
+ integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
+
+is-stream@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
+ integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
+
+is-text-path@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e"
+ integrity sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=
+ dependencies:
+ text-extensions "^1.0.0"
+
+isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+ integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+js-tokens@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+json-parse-better-errors@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+ integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+json-parse-even-better-errors@^2.3.0:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+ integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-stringify-safe@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+ integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+
+jsonfile@^6.0.1:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
+ integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
+ dependencies:
+ universalify "^2.0.0"
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+jsonparse@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
+ integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
+
+kind-of@^6.0.3:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+ integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+lines-and-columns@^1.1.6:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
+ integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
+
+load-json-file@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
+ integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^4.0.0"
+ pify "^3.0.0"
+ strip-bom "^3.0.0"
+
+locate-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+ integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=
+ dependencies:
+ p-locate "^2.0.0"
+ path-exists "^3.0.0"
+
+locate-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+ integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+ dependencies:
+ p-locate "^3.0.0"
+ path-exists "^3.0.0"
+
+locate-path@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+ integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+ dependencies:
+ p-locate "^4.1.0"
+
+locate-path@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
+ integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+ dependencies:
+ p-locate "^5.0.0"
+
+lodash.ismatch@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37"
+ integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=
+
+lodash@^4.17.15, lodash@^4.17.19:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+lru-cache@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+ integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+ dependencies:
+ yallist "^4.0.0"
+
+make-error@^1.1.1:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
+ integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
+
+map-obj@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
+ integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
+
+map-obj@^4.0.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a"
+ integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==
+
+meow@^8.0.0:
+ version "8.1.2"
+ resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897"
+ integrity sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==
+ dependencies:
+ "@types/minimist" "^1.2.0"
+ camelcase-keys "^6.2.2"
+ decamelize-keys "^1.1.0"
+ hard-rejection "^2.1.0"
+ minimist-options "4.1.0"
+ normalize-package-data "^3.0.0"
+ read-pkg-up "^7.0.1"
+ redent "^3.0.0"
+ trim-newlines "^3.0.0"
+ type-fest "^0.18.0"
+ yargs-parser "^20.2.3"
+
+merge-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+ integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
+mimic-fn@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+ integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+
+min-indent@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
+ integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
+
+minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist-options@4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
+ integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
+ dependencies:
+ arrify "^1.0.1"
+ is-plain-obj "^1.1.0"
+ kind-of "^6.0.3"
+
+minimist@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+ integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+
+modify-values@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
+ integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
+
+neo-async@^2.6.0:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
+ integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+
+normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
+ integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
+ dependencies:
+ hosted-git-info "^2.1.4"
+ resolve "^1.10.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-package-data@^3.0.0:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e"
+ integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==
+ dependencies:
+ hosted-git-info "^4.0.1"
+ is-core-module "^2.5.0"
+ semver "^7.3.4"
+ validate-npm-package-license "^3.0.1"
+
+npm-run-path@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
+ integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
+ dependencies:
+ path-key "^3.0.0"
+
+null-check@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd"
+ integrity sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=
+
+onetime@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
+ integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
+ dependencies:
+ mimic-fn "^2.1.0"
+
+p-limit@^1.1.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
+ integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==
+ dependencies:
+ p-try "^1.0.0"
+
+p-limit@^2.0.0, p-limit@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+ integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+ dependencies:
+ p-try "^2.0.0"
+
+p-limit@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+ integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+ dependencies:
+ yocto-queue "^0.1.0"
+
+p-locate@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+ integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=
+ dependencies:
+ p-limit "^1.1.0"
+
+p-locate@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+ integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+ dependencies:
+ p-limit "^2.0.0"
+
+p-locate@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+ integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+ dependencies:
+ p-limit "^2.2.0"
+
+p-locate@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
+ integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
+ dependencies:
+ p-limit "^3.0.2"
+
+p-try@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
+ integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
+
+p-try@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+ integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+parent-module@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
+ integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+ dependencies:
+ callsites "^3.0.0"
+
+parse-json@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+ integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
+ dependencies:
+ error-ex "^1.3.1"
+ json-parse-better-errors "^1.0.1"
+
+parse-json@^5.0.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
+ integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ error-ex "^1.3.1"
+ json-parse-even-better-errors "^2.3.0"
+ lines-and-columns "^1.1.6"
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+ integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-key@^3.0.0, path-key@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+ integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+path-type@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+ integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
+ dependencies:
+ pify "^3.0.0"
+
+path-type@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+ integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
+pify@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+ integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
+
+pify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+ integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
+
+process-nextick-args@~2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+ integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
+punycode@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+ integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+q@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
+ integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
+
+quick-lru@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
+ integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
+
+read-pkg-up@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07"
+ integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=
+ dependencies:
+ find-up "^2.0.0"
+ read-pkg "^3.0.0"
+
+read-pkg-up@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
+ integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==
+ dependencies:
+ find-up "^4.1.0"
+ read-pkg "^5.2.0"
+ type-fest "^0.8.1"
+
+read-pkg@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
+ integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
+ dependencies:
+ load-json-file "^4.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^3.0.0"
+
+read-pkg@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
+ integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
+ dependencies:
+ "@types/normalize-package-data" "^2.4.0"
+ normalize-package-data "^2.5.0"
+ parse-json "^5.0.0"
+ type-fest "^0.6.0"
+
+readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+ integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+readable-stream@~2.3.6:
+ version "2.3.7"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
+ integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.1.1"
+ util-deprecate "~1.0.1"
+
+redent@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
+ integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
+ dependencies:
+ indent-string "^4.0.0"
+ strip-indent "^3.0.0"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+ integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+resolve-from@5.0.0, resolve-from@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+ integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
+resolve-from@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+ integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve-global@1.0.0, resolve-global@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-global/-/resolve-global-1.0.0.tgz#a2a79df4af2ca3f49bf77ef9ddacd322dad19255"
+ integrity sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==
+ dependencies:
+ global-dirs "^0.1.1"
+
+resolve@^1.10.0:
+ version "1.20.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
+ integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
+ dependencies:
+ is-core-module "^2.2.0"
+ path-parse "^1.0.6"
+
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-buffer@~5.2.0:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+"semver@2 || 3 || 4 || 5":
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+ integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+semver@7.3.5, semver@^7.1.1, semver@^7.3.4:
+ version "7.3.5"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
+ integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
+ dependencies:
+ lru-cache "^6.0.0"
+
+semver@^6.0.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+ integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+shebang-command@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+ integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+ dependencies:
+ shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+ integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+signal-exit@^3.0.3:
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f"
+ integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==
+
+source-map@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+spdx-correct@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
+ integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
+ dependencies:
+ spdx-expression-parse "^3.0.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-exceptions@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
+ integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
+
+spdx-expression-parse@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
+ integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
+ dependencies:
+ spdx-exceptions "^2.1.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-license-ids@^3.0.0:
+ version "3.0.10"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz#0d9becccde7003d6c658d487dd48a32f0bf3014b"
+ integrity sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==
+
+split2@^3.0.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f"
+ integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==
+ dependencies:
+ readable-stream "^3.0.0"
+
+split@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9"
+ integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==
+ dependencies:
+ through "2"
+
+standard-version@^9.3.1:
+ version "9.3.2"
+ resolved "https://registry.yarnpkg.com/standard-version/-/standard-version-9.3.2.tgz#28db8c1be66fd2d736f28f7c5de7619e64cd6dab"
+ integrity sha512-u1rfKP4o4ew7Yjbfycv80aNMN2feTiqseAhUhrrx2XtdQGmu7gucpziXe68Z4YfHVqlxVEzo4aUA0Iu3VQOTgQ==
+ dependencies:
+ chalk "^2.4.2"
+ conventional-changelog "3.1.24"
+ conventional-changelog-config-spec "2.1.0"
+ conventional-changelog-conventionalcommits "4.6.1"
+ conventional-recommended-bump "6.1.0"
+ detect-indent "^6.0.0"
+ detect-newline "^3.1.0"
+ dotgitignore "^2.1.0"
+ figures "^3.1.0"
+ find-up "^5.0.0"
+ fs-access "^1.0.1"
+ git-semver-tags "^4.0.0"
+ semver "^7.1.1"
+ stringify-package "^1.0.1"
+ yargs "^16.0.0"
+
+string-width@^4.1.0, string-width@^4.2.0:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string_decoder@^1.1.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+ integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+ dependencies:
+ safe-buffer "~5.2.0"
+
+string_decoder@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+ integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+stringify-package@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/stringify-package/-/stringify-package-1.0.1.tgz#e5aa3643e7f74d0f28628b72f3dad5cecfc3ba85"
+ integrity sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+ integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
+
+strip-final-newline@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
+ integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
+
+strip-indent@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
+ integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
+ dependencies:
+ min-indent "^1.0.0"
+
+supports-color@^5.3.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+ dependencies:
+ has-flag "^3.0.0"
+
+supports-color@^7.1.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+ integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+ dependencies:
+ has-flag "^4.0.0"
+
+text-extensions@^1.0.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26"
+ integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==
+
+through2@^2.0.0:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
+ integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
+ dependencies:
+ readable-stream "~2.3.6"
+ xtend "~4.0.1"
+
+through2@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764"
+ integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==
+ dependencies:
+ readable-stream "3"
+
+through@2, "through@>=2.2.7 <3":
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+ integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+
+trim-newlines@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
+ integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
+
+ts-node@^10.5.0:
+ version "10.5.0"
+ resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.5.0.tgz#618bef5854c1fbbedf5e31465cbb224a1d524ef9"
+ integrity sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw==
+ dependencies:
+ "@cspotcode/source-map-support" "0.7.0"
+ "@tsconfig/node10" "^1.0.7"
+ "@tsconfig/node12" "^1.0.7"
+ "@tsconfig/node14" "^1.0.0"
+ "@tsconfig/node16" "^1.0.2"
+ acorn "^8.4.1"
+ acorn-walk "^8.1.1"
+ arg "^4.1.0"
+ create-require "^1.1.0"
+ diff "^4.0.1"
+ make-error "^1.1.1"
+ v8-compile-cache-lib "^3.0.0"
+ yn "3.1.1"
+
+type-fest@^0.18.0:
+ version "0.18.1"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
+ integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==
+
+type-fest@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
+ integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
+
+type-fest@^0.8.1:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
+ integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
+
+typedarray@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+ integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
+
+typescript@^4.4.3:
+ version "4.4.4"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
+ integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
+
+uglify-js@^3.1.4:
+ version "3.14.2"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.2.tgz#d7dd6a46ca57214f54a2d0a43cad0f35db82ac99"
+ integrity sha512-rtPMlmcO4agTUfz10CbgJ1k6UAoXM2gWb3GoMPPZB/+/Ackf8lNWk11K4rYi2D0apgoFRLtQOZhb+/iGNJq26A==
+
+universalify@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
+ integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
+
+uri-js@^4.2.2:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+ integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+ dependencies:
+ punycode "^2.1.0"
+
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+v8-compile-cache-lib@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz#0582bcb1c74f3a2ee46487ceecf372e46bce53e8"
+ integrity sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
+ integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
+ dependencies:
+ spdx-correct "^3.0.0"
+ spdx-expression-parse "^3.0.0"
+
+which@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+ integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+ dependencies:
+ isexe "^2.0.0"
+
+wordwrap@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+ integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
+
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
+xtend@~4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+ integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
+y18n@^5.0.5:
+ version "5.0.8"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
+ integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
+
+yallist@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+ integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yaml@^1.10.0:
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
+ integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+
+yargs-parser@^20.2.2, yargs-parser@^20.2.3:
+ version "20.2.9"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
+ integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
+
+yargs@^16.0.0, yargs@^16.2.0:
+ version "16.2.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
+ integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
+ dependencies:
+ cliui "^7.0.2"
+ escalade "^3.1.1"
+ get-caller-file "^2.0.5"
+ require-directory "^2.1.1"
+ string-width "^4.2.0"
+ y18n "^5.0.5"
+ yargs-parser "^20.2.2"
+
+yargs@^17.0.0:
+ version "17.2.1"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.2.1.tgz#e2c95b9796a0e1f7f3bf4427863b42e0418191ea"
+ integrity sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==
+ dependencies:
+ cliui "^7.0.2"
+ escalade "^3.1.1"
+ get-caller-file "^2.0.5"
+ require-directory "^2.1.1"
+ string-width "^4.2.0"
+ y18n "^5.0.5"
+ yargs-parser "^20.2.2"
+
+yn@3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
+ integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
+
+yocto-queue@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+ integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==