diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..c2658d7d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..6819e800
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Jongtion
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..d1b15a5c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "FEDC4-5_Project_Notion_VanillaJS",
+ "version": "1.0.0",
+ "main": "index.js",
+ "repository": "https://github.com/prgrms-fe-devcourse/FEDC4-5_Project_Notion_VanillaJS.git",
+ "author": "jgjgill ",
+ "license": "MIT",
+ "scripts": {
+ "dev": "yarn serve -s"
+ },
+ "devDependencies": {
+ "serve": "^14.2.0"
+ }
+}
diff --git a/src/App.js b/src/App.js
new file mode 100644
index 00000000..a3083682
--- /dev/null
+++ b/src/App.js
@@ -0,0 +1,149 @@
+import { getDocuments, putDocument } from "./api/document.js";
+import Layout from "./components/common/Layout.js";
+import { PATH } from "./constants/path.js";
+import { initRouter, push } from "./utils/route.js";
+import {
+ TrieDocument,
+ addChildDocument,
+ editTitleDocument,
+ insertAllDocument,
+ findChildDocuments,
+ removeDocument,
+} from "./utils/document.js";
+import NotFound from "./components/common/NotFound.js";
+import { debounce } from "./utils/debounce.js";
+import DocumentList from "./components/domain/document/DocumentList.js";
+import DocumentEditor from "./components/domain/document/DocumentEditor.js";
+import Home from "./components/domain/home/Home.js";
+import RecurDocumentList from "./components/domain/document/template/RecurDocumentList.js";
+
+/**
+ * @param {{appElement: Element | null}}
+ */
+
+export default function App({ appElement }) {
+ if (!new.target) return new App(...arguments);
+
+ const wrapperContainer = document.createElement("div");
+ const sidebarContainer = document.createElement("div");
+ const contentsContainer = document.createElement("div");
+ const sidebarListContainer = document.createElement("div");
+
+ wrapperContainer.className = "wrapper-container";
+ sidebarContainer.className = "sidebar-container";
+ contentsContainer.className = "contents-container";
+ sidebarListContainer.className = "sidebar-list-container";
+
+ const trie = new TrieDocument();
+
+ const processEdit = debounce(async (documentId, docunemt) => {
+ await putDocument({ documentId, data: docunemt }).catch((err) =>
+ alert(err.message)
+ );
+ }, 1000);
+
+ this.state = [];
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ documentListComponent.render();
+ };
+
+ this.editorSetState = (nextState) => {
+ this.state = nextState;
+ documentEditorComponent.render();
+ };
+
+ const layoutComponent = new Layout({ parentElement: sidebarContainer });
+
+ const documentListComponent = new DocumentList({
+ parentElement: sidebarListContainer,
+ renderItemComponent: (parentElement) => {
+ RecurDocumentList({
+ rootDocuments: this.state,
+ parentElement,
+ childRender: (parentId, newDocument) => {
+ const nextState = addChildDocument(parentId, this.state, newDocument);
+ this.setState(nextState);
+ },
+ removeRender: (documentId) => {
+ const newState = removeDocument(documentId, this.state);
+ const stringDocumentId = window.location.pathname.split("/")[2];
+
+ Number(stringDocumentId) !== documentId
+ ? this.editorSetState(newState)
+ : push(PATH.HOME);
+
+ this.setState(newState);
+ },
+ depthCount: 1,
+ });
+ },
+ onAddButtonClick: (newDocument) => {
+ const nextState = [...this.state, newDocument];
+ this.setState(nextState);
+ },
+ });
+
+ const homeComponent = new Home({
+ parentElement: contentsContainer,
+ search: (text) => trie.search(text),
+ });
+
+ const documentEditorComponent = new DocumentEditor({
+ parentElement: contentsContainer,
+ onEditing: (document) => {
+ const { documentId, title, isChangeTitle } = document;
+
+ if (isChangeTitle) {
+ const newState = editTitleDocument(documentId, this.state, title);
+ this.setState(newState);
+ }
+
+ processEdit(documentId, document);
+ },
+ getChildDocuments: (documentId) =>
+ findChildDocuments(this.state, documentId),
+ });
+
+ const notFoundComponent = new NotFound({
+ parentCompoent: contentsContainer,
+ });
+
+ window.addEventListener("popstate", () => {
+ this.route();
+ });
+
+ initRouter(() => this.route());
+
+ this.init = async () => {
+ appElement.append(wrapperContainer);
+ wrapperContainer.append(sidebarContainer, contentsContainer);
+
+ layoutComponent.render();
+ sidebarContainer.append(sidebarListContainer);
+
+ const newState = await getDocuments();
+ this.setState(newState);
+
+ insertAllDocument(newState, (title, id) => trie.insert(title, id));
+
+ this.route();
+ };
+
+ this.route = () => {
+ const { pathname } = window.location;
+ contentsContainer.innerHTML = ``;
+
+ if (pathname === PATH.HOME) {
+ trie.reset();
+ insertAllDocument(this.state, (title, id) => trie.insert(title, id));
+
+ homeComponent.render();
+ } else if (pathname.split("/")[1] === "documents") {
+ documentEditorComponent.render();
+ } else {
+ notFoundComponent.render();
+ }
+ };
+}
diff --git a/src/api/document.js b/src/api/document.js
new file mode 100644
index 00000000..fd37d64d
--- /dev/null
+++ b/src/api/document.js
@@ -0,0 +1,34 @@
+import { PATH } from "../constants/path.js";
+import { request } from "./request.js";
+
+export const postDocument = async (data) => {
+ return await request(PATH.DOCUMENTS, {
+ method: "POST",
+ body: JSON.stringify(data),
+ });
+};
+
+export const getDocuments = async () => {
+ return await request(PATH.DOCUMENTS);
+};
+
+export const getDocument = async (documentId) => {
+ return await request(`${PATH.DOCUMENTS}/${documentId}`).catch((err) => {
+ throw new Error("현재 존재하지 않는 문서입니다..!");
+ });
+};
+
+export const putDocument = async ({ documentId, data }) => {
+ return await request(`${PATH.DOCUMENTS}/${documentId}`, {
+ method: "PUT",
+ body: JSON.stringify(data),
+ }).catch(() => {
+ throw new Error("작성 중에 에러가 발생했습니다..!");
+ });
+};
+
+export const deleteDocument = async (documentId) => {
+ return await request(`${PATH.DOCUMENTS}/${documentId}`, {
+ method: "DELETE",
+ });
+};
diff --git a/src/api/request.js b/src/api/request.js
new file mode 100644
index 00000000..bdb9a11c
--- /dev/null
+++ b/src/api/request.js
@@ -0,0 +1,24 @@
+import { PATH } from "../constants/path.js";
+import { push } from "../utils/route.js";
+
+const API_END_POINT = "https://kdt-frontend.programmers.co.kr";
+
+export const request = async (path, options = {}) => {
+ try {
+ const res = await fetch(`${API_END_POINT}${path}`, {
+ ...options,
+ headers: {
+ "Content-Type": "application/json",
+ "x-username": "jgjgill",
+ },
+ });
+
+ if (res.ok) {
+ return await res.json();
+ }
+
+ throw new Error("API 에러가 발생했습니다!");
+ } catch (err) {
+ throw new Error(err.message);
+ }
+};
diff --git a/src/components/common/AddButton.js b/src/components/common/AddButton.js
new file mode 100644
index 00000000..fe34c670
--- /dev/null
+++ b/src/components/common/AddButton.js
@@ -0,0 +1,36 @@
+import Tooltip from "./Tooltip.js";
+
+export default function AddButton({
+ parentElement,
+ onClick,
+ text,
+ tooltipText,
+}) {
+ const buttonElement = document.createElement("button");
+ buttonElement.className = "add-button";
+
+ const tooltipElement = new Tooltip({ text: tooltipText });
+
+ buttonElement.addEventListener("click", () => {
+ onClick();
+ });
+
+ buttonElement.addEventListener("mouseover", (e) => {
+ if (!e.target.closest(".text")) return;
+
+ tooltipElement.toggle(e.target);
+ });
+
+ buttonElement.addEventListener("mouseout", (e) => {
+ if (!e.target.closest(".text")) return;
+
+ tooltipElement.toggle(e.target);
+ });
+
+ this.render = () => {
+ parentElement.append(buttonElement);
+ buttonElement.innerHTML = tooltipElement.render(
+ `${text}
`
+ );
+ };
+}
diff --git a/src/components/common/Layout.js b/src/components/common/Layout.js
new file mode 100644
index 00000000..c2d4e31a
--- /dev/null
+++ b/src/components/common/Layout.js
@@ -0,0 +1,21 @@
+import { PATH } from "../../constants/path.js";
+import { push } from "../../utils/route.js";
+
+export default function Layout({ parentElement }) {
+ if (!new.target) return new Layout(...arguments);
+
+ const containerElement = document.createElement("div");
+
+ containerElement.addEventListener("click", (e) => {
+ if (e.target.closest("h1")) {
+ push(PATH.HOME);
+ }
+ });
+
+ this.render = () => {
+ parentElement.append(containerElement);
+ containerElement.innerHTML = `
+ Jongtion
+ `;
+ };
+}
diff --git a/src/components/common/NotFound.js b/src/components/common/NotFound.js
new file mode 100644
index 00000000..db1e1537
--- /dev/null
+++ b/src/components/common/NotFound.js
@@ -0,0 +1,22 @@
+import { PATH } from "../../constants/path.js";
+import { push } from "../../utils/route.js";
+
+export default function NotFound({ parentCompoent }) {
+ const containerElement = document.createElement("div");
+ containerElement.className = "not-found-container";
+
+ containerElement.addEventListener("click", (e) => {
+ if (!e.target.closest(".home-button")) return;
+
+ push(PATH.HOME);
+ });
+
+ this.render = () => {
+ parentCompoent.append(containerElement);
+
+ containerElement.innerHTML = `
+ 잘못된 경로입니다!
+
+ `;
+ };
+}
diff --git a/src/components/common/Tooltip.js b/src/components/common/Tooltip.js
new file mode 100644
index 00000000..78594891
--- /dev/null
+++ b/src/components/common/Tooltip.js
@@ -0,0 +1,14 @@
+export default function Tooltip({ text }) {
+ this.toggle = (targetElement) => {
+ targetElement.nextElementSibling.classList.toggle("toggle");
+ };
+
+ this.render = (content) => {
+ return `
+
+ `;
+ };
+}
diff --git a/src/components/domain/document/DocumentEditor.js b/src/components/domain/document/DocumentEditor.js
new file mode 100644
index 00000000..dc05be70
--- /dev/null
+++ b/src/components/domain/document/DocumentEditor.js
@@ -0,0 +1,73 @@
+import { getDocument } from "../../../api/document.js";
+import { PATH } from "../../../constants/path.js";
+import { push } from "../../../utils/route.js";
+import RecurChildDocument from "./template/RecurChildDocument.js";
+
+export default function DocumentEditor({
+ parentElement,
+ onEditing,
+ getChildDocuments,
+}) {
+ const containerElement = document.createElement("div");
+ containerElement.className = "editor-container";
+
+ this.state = { title: "", content: "", documentId: "" };
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ };
+
+ containerElement.addEventListener("input", (e) => {
+ if (e.target.closest(".editor-title")) {
+ this.setState({
+ ...this.state,
+ title: e.target.value,
+ isChangeTitle: true,
+ });
+ }
+
+ if (e.target.closest(".editor-content")) {
+ this.setState({
+ ...this.state,
+ content: e.target.innerHTML,
+ isChangeTitle: false,
+ });
+ }
+
+ onEditing(this.state);
+ });
+
+ containerElement.addEventListener("click", (e) => {
+ if (!e.target.closest(".child-document")) return;
+
+ push(`${PATH.DOCUMENTS}/${e.target.dataset.id}`);
+ });
+
+ this.render = async () => {
+ const { pathname } = window.location;
+ const documentId = Number(pathname.split("/")[2]);
+
+ if (!documentId) return;
+
+ parentElement.append(containerElement);
+
+ const childDocuments = getChildDocuments(documentId);
+ const data = await getDocument(documentId).catch((err) => {
+ alert(err.message);
+ push(PATH.HOME);
+ });
+ const { title, content } = data;
+
+ this.setState({ title, content, documentId });
+
+ containerElement.innerHTML = `
+
+ ${content ?? ""}
+ ${RecurChildDocument(
+ childDocuments
+ )}
+ `;
+ };
+}
diff --git a/src/components/domain/document/DocumentItem.js b/src/components/domain/document/DocumentItem.js
new file mode 100644
index 00000000..12d0dc74
--- /dev/null
+++ b/src/components/domain/document/DocumentItem.js
@@ -0,0 +1,83 @@
+import Tooltip from "../../common/Tooltip.js";
+
+export default function DocumentItem({
+ parentElement,
+ getChildDocument,
+ onClickChildButton,
+ onClickRemoveButton,
+ onClickRoute,
+ depthCount,
+ ...documentData
+}) {
+ const containerElement = document.createElement("div");
+ containerElement.className = "document-container toggle";
+ containerElement.style.setProperty("--depth", depthCount > 8 ? 0 : "20px");
+
+ const tooltipChildAddElement = new Tooltip({ text: "하위 페이지 추가" });
+ const tooltipRemoveElement = new Tooltip({ text: "삭제" });
+
+ containerElement.addEventListener("click", (e) => {
+ if (Number(e.target.closest("li").id) !== documentData.id) return;
+
+ if (e.target.closest(".child-button")) {
+ return onClickChildButton(documentData.id);
+ }
+
+ if (e.target.closest(".remove-button")) {
+ return onClickRemoveButton(documentData.id);
+ }
+
+ if (e.target.closest(".toggle-button")) {
+ containerElement.classList.toggle("toggle");
+ return;
+ }
+
+ onClickRoute(documentData.id);
+ });
+
+ containerElement.addEventListener("mouseover", (e) => {
+ if (Number(e.target.dataset.id) !== documentData.id) return;
+
+ if (e.target.closest(".child-button")) {
+ tooltipChildAddElement.toggle(e.target);
+ }
+
+ if (e.target.closest(".remove-button")) {
+ tooltipRemoveElement.toggle(e.target);
+ }
+ });
+
+ containerElement.addEventListener("mouseout", (e) => {
+ if (Number(e.target.dataset.id) !== documentData.id) return;
+
+ if (e.target.closest(".child-button")) {
+ tooltipChildAddElement.toggle(e.target);
+ }
+
+ if (e.target.closest(".remove-button")) {
+ tooltipRemoveElement.toggle(e.target);
+ }
+ });
+
+ this.render = () => {
+ const { id, title } = documentData;
+ parentElement.append(containerElement);
+
+ containerElement.innerHTML = `
+
+
+ ${title ?? "제목 없음"}
+
+
+ `;
+
+ getChildDocument(containerElement);
+ };
+}
diff --git a/src/components/domain/document/DocumentList.js b/src/components/domain/document/DocumentList.js
new file mode 100644
index 00000000..cf02d482
--- /dev/null
+++ b/src/components/domain/document/DocumentList.js
@@ -0,0 +1,40 @@
+import { postDocument } from "../../../api/document.js";
+import { PATH } from "../../../constants/path.js";
+import { push } from "../../../utils/route.js";
+import AddButton from "../../common/AddButton.js";
+
+export default function DocumentList({
+ parentElement,
+ renderItemComponent,
+ onAddButtonClick,
+}) {
+ if (!new.target) return new DocumentList(...arguments);
+
+ const containerElement = document.createElement("div");
+ const wrapperTopElement = document.createElement("div");
+
+ containerElement.className = "document-list";
+ wrapperTopElement.className = "document-list-top";
+
+ const addButtonComponent = new AddButton({
+ parentElement: wrapperTopElement,
+ onClick: async () => {
+ const newDocument = await postDocument({ titls: null, parent: null });
+ onAddButtonClick({ ...newDocument, documents: [] });
+ push(`${PATH.DOCUMENTS}/${newDocument.id}`);
+ },
+ text: "+",
+ tooltipText: "페이지 추가",
+ });
+
+ this.render = () => {
+ containerElement.innerHTML = ``;
+ wrapperTopElement.innerHTML = `페이지 목록`;
+
+ parentElement.append(containerElement);
+ containerElement.append(wrapperTopElement);
+
+ addButtonComponent.render();
+ renderItemComponent(containerElement);
+ };
+}
diff --git a/src/components/domain/document/template/RecurChildDocument.js b/src/components/domain/document/template/RecurChildDocument.js
new file mode 100644
index 00000000..929aa2f6
--- /dev/null
+++ b/src/components/domain/document/template/RecurChildDocument.js
@@ -0,0 +1,17 @@
+/**
+ * @description 현재 문서의 자식 문서
+ */
+
+export default function RecurChildDocument(rootDocument) {
+ return rootDocument
+ .map(
+ ({ id, title, documents }) =>
+ `
+ ${
+ title ?? "제목 없음"
+ }
+ ${RecurChildDocument(documents)}
+
`
+ )
+ .join("");
+}
diff --git a/src/components/domain/document/template/RecurDocumentList.js b/src/components/domain/document/template/RecurDocumentList.js
new file mode 100644
index 00000000..7c3a3108
--- /dev/null
+++ b/src/components/domain/document/template/RecurDocumentList.js
@@ -0,0 +1,50 @@
+import { deleteDocument, postDocument } from "../../../../api/document.js";
+import { PATH } from "../../../../constants/path.js";
+import { push } from "../../../../utils/route.js";
+import DocumentItem from "../DocumentItem.js";
+
+/**
+ * @description 트리 형태로 문서를 불러오는 함수
+ */
+
+export default function RecurDocumentList({
+ parentElement,
+ rootDocuments,
+ childRender,
+ removeRender,
+ depthCount,
+}) {
+ rootDocuments.map((rootDocument) =>
+ new DocumentItem({
+ parentElement,
+ getChildDocument:
+ rootDocument.documents.length === 0
+ ? () => {}
+ : (innerParentElement) =>
+ RecurDocumentList({
+ rootDocuments: rootDocument.documents,
+ parentElement: innerParentElement,
+ childRender,
+ removeRender,
+ depthCount: depthCount + 1,
+ }),
+ onClickChildButton: async (documentId) => {
+ const newDocument = await postDocument({
+ title: null,
+ parent: documentId,
+ });
+ childRender(documentId, { ...newDocument, documents: [] });
+ push(`${PATH.DOCUMENTS}/${newDocument.id}`);
+ },
+ onClickRemoveButton: async (documentId) => {
+ await deleteDocument(documentId);
+ removeRender(documentId);
+ },
+ onClickRoute: async (documentId) => {
+ push(`${PATH.DOCUMENTS}/${documentId}`);
+ },
+ depthCount,
+ ...rootDocument,
+ }).render()
+ );
+}
diff --git a/src/components/domain/home/Home.js b/src/components/domain/home/Home.js
new file mode 100644
index 00000000..470f6f7b
--- /dev/null
+++ b/src/components/domain/home/Home.js
@@ -0,0 +1,94 @@
+import { PATH } from "../../../constants/path.js";
+import { debounce } from "../../../utils/debounce.js";
+import { push } from "../../../utils/route.js";
+import {
+ RECENT_SEARCH_LIST,
+ getItem,
+ setItem,
+} from "../../../utils/storage.js";
+
+export default function Home({ parentElement, search }) {
+ if (!new.target) return new Home(...arguments);
+
+ const containerElement = document.createElement("div");
+ containerElement.className = "home-container";
+
+ const processSearch = debounce((value) => {
+ if (value === "") {
+ this.setState({ text: value, list: [] });
+ return;
+ }
+
+ const newState = { text: value, list: search(value) };
+ this.setState(newState);
+ });
+
+ this.state = { text: "", list: [] };
+
+ this.setState = (nextState) => {
+ this.state = nextState;
+ this.render();
+ };
+
+ containerElement.addEventListener("input", (e) => {
+ if (!e.target.closest(".search")) return;
+
+ processSearch(e.target.value);
+ });
+
+ containerElement.addEventListener("click", (e) => {
+ if (!e.target.closest(".search-result-item")) return;
+
+ const { dataset, innerText } = e.target;
+ const newSearchItem = { id: dataset.id, title: innerText };
+
+ push(`${PATH.DOCUMENTS}/${dataset.id}`);
+
+ setItem(
+ RECENT_SEARCH_LIST,
+ [newSearchItem, ...getItem(RECENT_SEARCH_LIST, [])].slice(0, 5)
+ );
+ });
+
+ this.render = () => {
+ parentElement.append(containerElement);
+ containerElement.innerHTML = `
+
+
+ ${
+ this.state.list.length === 0 && this.state.text !== ""
+ ? `
${this.state.text}와(과) 일치하는 검색결과가 없습니다.
`
+ : `
${this.state.list
+ .map(
+ (item) =>
+ `- ${
+ item.title === "" ? "제목 없음" : item.title
+ }
`
+ )
+ .join("")}
+
`
+ }
+
+
+
최근 검색 문서 목록
+
+ ${getItem(RECENT_SEARCH_LIST, [])
+ .map(
+ (item) =>
+ `- ${item.title}
`
+ )
+ .join("")}
+
+
+ `;
+
+ const searchElement = containerElement.querySelector(".search");
+
+ searchElement.focus();
+ const val = searchElement.value;
+ searchElement.value = "";
+ searchElement.value = val;
+ };
+}
diff --git a/src/constants/path.js b/src/constants/path.js
new file mode 100644
index 00000000..3efa6953
--- /dev/null
+++ b/src/constants/path.js
@@ -0,0 +1,5 @@
+export const PATH = {
+ HOME: "/",
+ EDIT: "/edit",
+ DOCUMENTS: "/documents",
+};
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 00000000..a580cc85
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,4 @@
+import App from "./App.js";
+
+const appElement = document.querySelector("#app");
+new App({ appElement }).init();
diff --git a/src/styles/styles.css b/src/styles/styles.css
new file mode 100644
index 00000000..845da7f0
--- /dev/null
+++ b/src/styles/styles.css
@@ -0,0 +1,340 @@
+/* reset */
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ color: #6e6d68;
+}
+
+input {
+ &:focus {
+ outline: none;
+ }
+}
+
+ul {
+ list-style: none;
+}
+
+button {
+ background-color: transparent;
+}
+
+/* reset */
+
+.edit {
+ border: 2px solid black;
+}
+.wrapper-container {
+ display: flex;
+ height: 100vh;
+ width: 100vw;
+}
+
+.sidebar-container {
+ display: flex;
+ flex-direction: column;
+ background-color: #f6f5f4;
+}
+
+.sidebar-list-container {
+ width: 300px;
+ overflow: auto;
+}
+
+.logo {
+ cursor: pointer;
+ width: 100%;
+ height: 100%;
+ border: 2px;
+ padding: 10px;
+ box-sizing: border-box;
+ color: #37352f;
+
+ &:hover {
+ background-color: #ebebea;
+ }
+}
+
+.document-list {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ padding: 10px;
+ width: 100%;
+
+ > .document-container {
+ padding-left: 0;
+ }
+}
+
+.document-list-top {
+ display: flex;
+ justify-content: space-between;
+ padding: 10px 5px;
+}
+
+.document-container {
+ display: flex;
+ flex-direction: column;
+ padding-left: var(--depth);
+}
+
+.toggle-button {
+ display: flex;
+ border-radius: 5px;
+
+ &::before {
+ content: ">";
+ text-align: center;
+ font-size: larger;
+ display: inline-block;
+ transform: rotate(90deg);
+ width: 20px;
+ height: 20px;
+ transition: 0.3s;
+ cursor: pointer;
+ }
+
+ &:hover {
+ background-color: #dededd;
+ }
+}
+
+.document-container.toggle > .document-container {
+ display: none;
+}
+
+.document-container.toggle .toggle-button::before {
+ transform: rotate(0);
+}
+
+.document-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ cursor: pointer;
+ border-radius: 5px;
+ padding: 0 5px;
+
+ &:hover {
+ background-color: #ebebea;
+ }
+}
+
+.document-title {
+ flex: 1;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.button-group-container {
+ display: flex;
+ gap: 5px;
+}
+
+.child-button {
+ width: 100%;
+ padding: 2px;
+ border-radius: 5px;
+ width: 20px;
+ height: 20px;
+ text-align: center;
+
+ &:hover {
+ background-color: #dededd;
+ }
+}
+
+.remove-button {
+ padding: 2px;
+ border-radius: 5px;
+ width: 20px;
+ height: 20px;
+ text-align: center;
+
+ &:hover {
+ background-color: #dededd;
+ }
+}
+
+.contents-container {
+ padding: 50px;
+}
+
+/* home */
+
+.home-container {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ width: 60vw;
+
+ height: 100%;
+}
+
+.search-container {
+ height: 50%;
+ overflow: auto;
+}
+
+.search {
+ border: none;
+ padding: 10px;
+ padding-left: 0;
+ font-size: xx-large;
+ font-weight: 700;
+}
+
+.search-result-item {
+ cursor: pointer;
+ font-size: x-large;
+ padding: 10px;
+
+ &:hover {
+ background-color: #ebebea;
+ }
+}
+
+.recent-search-container {
+ height: 50%;
+ overflow: auto;
+}
+
+.recent-search-title {
+ font-size: xx-large;
+ font-weight: 700;
+}
+
+.recent-search-item {
+ cursor: pointer;
+ font-size: x-large;
+ padding: 10px;
+
+ &:hover {
+ background-color: #ebebea;
+ }
+}
+
+/* document-editor */
+.editor-container {
+ display: flex;
+ flex-direction: column;
+ width: 60vw;
+
+ height: 100%;
+}
+
+.editor-title {
+ border: none;
+ padding: 10px;
+ font-size: xx-large;
+ font-weight: 700;
+
+ &::placeholder {
+ color: #e1e1e0;
+ }
+}
+
+.editor-content {
+ font-size: x-large;
+ height: 100%;
+ flex: 1;
+
+ padding: 10px;
+
+ &:focus {
+ outline: none;
+ }
+}
+
+.container-child-document {
+ width: 60vw;
+ height: 100px;
+ overflow: auto;
+ border-top: 2px solid #dededd;
+ padding-top: 10px;
+}
+
+/* recur-child-document */
+
+.child-document {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+}
+
+.child-document-title {
+ cursor: pointer;
+
+ &:hover {
+ background-color: #ebebea;
+ }
+}
+
+/* add-button */
+.add-button {
+ width: 20px;
+ height: 20px;
+ border-radius: 5px;
+ text-align: center;
+ cursor: pointer;
+
+ &:hover {
+ background-color: #dededd;
+ }
+}
+
+/* not-found */
+.not-found-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 20px;
+ height: 100%;
+ padding-top: 50px;
+}
+
+.not-found-title {
+ font-size: xx-large;
+ font-weight: 700;
+}
+
+.home-button {
+ font-size: x-large;
+ cursor: pointer;
+ padding: 10px;
+ border-radius: 10px;
+
+ &:hover {
+ background-color: #ebebea;
+ }
+}
+
+/* tooltip */
+
+.tooltip {
+ position: relative;
+}
+
+.tooltip-text {
+ position: absolute;
+ display: none;
+
+ transform: translate(-50%);
+ bottom: -30px;
+}
+
+.tooltip-text.toggle {
+ display: block;
+ width: max-content;
+ padding: 5px;
+ background-color: black;
+ color: white;
+ border-radius: 10px;
+ font-size: small;
+ z-index: 9999;
+}
diff --git a/src/utils/debounce.js b/src/utils/debounce.js
new file mode 100644
index 00000000..2ee5dbe1
--- /dev/null
+++ b/src/utils/debounce.js
@@ -0,0 +1,10 @@
+export function debounce(func, timeout = 300) {
+ let timer;
+
+ return (...args) => {
+ clearTimeout(timer);
+ timer = setTimeout(() => {
+ func.apply(this, args);
+ }, timeout);
+ };
+}
diff --git a/src/utils/document.js b/src/utils/document.js
new file mode 100644
index 00000000..4b7f2e2d
--- /dev/null
+++ b/src/utils/document.js
@@ -0,0 +1,178 @@
+export function addChildDocument(parentId, state, newDocument) {
+ const stack = [];
+ const tempState = structuredClone(state);
+
+ for (const temp of tempState) {
+ stack.push(temp);
+ }
+
+ while (stack.length !== 0) {
+ const current = stack.pop();
+ if (current.id === parentId) {
+ current.documents.push(newDocument);
+ return tempState;
+ }
+
+ for (const document of current.documents) {
+ stack.push(document);
+ }
+ }
+
+ return tempState;
+}
+
+export function removeDocument(documentId, state) {
+ const stack = [];
+ const tempState = structuredClone(state);
+
+ stack.push(tempState);
+
+ while (stack.length !== 0) {
+ const current = stack.pop();
+
+ for (let i = 0; i < current.length; i++) {
+ if (current[i].id === documentId) {
+ const temp = current.splice(i, 1);
+ if (temp[0].documents.length !== 0) {
+ tempState.push(...temp[0].documents);
+ tempState.sort((a, b) => a.id - b.id);
+ }
+
+ return tempState;
+ }
+
+ stack.push(current[i].documents);
+ }
+ }
+
+ return tempState;
+}
+
+export function editTitleDocument(documentId, state, documentTitle) {
+ const stack = [];
+ const tempState = structuredClone(state);
+
+ for (const temp of tempState) {
+ stack.push(temp);
+ }
+
+ while (stack.length !== 0) {
+ const current = stack.pop();
+
+ if (current.id === documentId) {
+ current.title = documentTitle;
+ return tempState;
+ }
+
+ for (const document of current.documents) {
+ stack.push(document);
+ }
+ }
+
+ return tempState;
+}
+
+export function insertAllDocument(state, insert) {
+ const stack = [];
+ const tempState = structuredClone(state);
+
+ for (const temp of tempState) {
+ stack.push(temp);
+ }
+
+ while (stack.length !== 0) {
+ const current = stack.pop();
+
+ insert(current.title ?? "", current.id);
+
+ for (const document of current.documents) {
+ stack.push(document);
+ }
+ }
+}
+
+export function findChildDocuments(state, documentId) {
+ const stack = [];
+ const tempState = structuredClone(state);
+
+ for (const temp of tempState) {
+ stack.push(temp);
+ }
+
+ while (stack.length !== 0) {
+ const current = stack.pop();
+
+ if (current.id === documentId) {
+ return current.documents;
+ }
+
+ for (const document of current.documents) {
+ stack.push(document);
+ }
+ }
+
+ return [];
+}
+
+function Node(value = "") {
+ this.value = value;
+ this.children = new Map();
+ this.isWord = false;
+ this.list = [];
+}
+
+export function TrieDocument() {
+ this.root = new Node();
+
+ this.insert = (string, id) => {
+ let currentNode = this.root;
+
+ for (const char of string) {
+ if (!currentNode.children.has(char)) {
+ currentNode.children.set(char, new Node(currentNode.value + char));
+ }
+ currentNode = currentNode.children.get(char);
+ }
+
+ currentNode.isWord = true;
+ currentNode.list.push({ title: string, id });
+ };
+
+ this.search = (value) => {
+ const queue = [];
+ const searchList = [];
+ let index = 0;
+ let currentNode = this.root;
+
+ for (const char of value) {
+ if (!currentNode.children.has(char)) {
+ return [];
+ }
+
+ currentNode = currentNode.children.get(char);
+ }
+
+ queue.push(currentNode);
+
+ while (index < queue.length) {
+ const currentNode = queue[index];
+ index += 1;
+
+ if (currentNode.isWord) {
+ for (const item of currentNode.list) {
+ searchList.push(item);
+ }
+ }
+
+ for (const [key, child] of currentNode.children) {
+ queue.push(child);
+ }
+ }
+
+ return searchList;
+ };
+
+ this.reset = () => {
+ this.root = new Node();
+ };
+}
diff --git a/src/utils/route.js b/src/utils/route.js
new file mode 100644
index 00000000..831df5a0
--- /dev/null
+++ b/src/utils/route.js
@@ -0,0 +1,18 @@
+const ROUTE_CHANGE_EVENT_NAME = "route-change";
+
+export const initRouter = (onRoute) => {
+ window.addEventListener(ROUTE_CHANGE_EVENT_NAME, (e) => {
+ const { nextUrl } = e.detail;
+
+ if (nextUrl) {
+ history.pushState(null, null, nextUrl);
+ onRoute();
+ }
+ });
+};
+
+export const push = (nextUrl) => {
+ window.dispatchEvent(
+ new CustomEvent(ROUTE_CHANGE_EVENT_NAME, { detail: { nextUrl } })
+ );
+};
diff --git a/src/utils/storage.js b/src/utils/storage.js
new file mode 100644
index 00000000..9bc12342
--- /dev/null
+++ b/src/utils/storage.js
@@ -0,0 +1,20 @@
+const storage = window.localStorage;
+
+export const RECENT_SEARCH_LIST = "recent-search-list";
+
+export const getItem = (key, defaultValue) => {
+ try {
+ const storedValue = storage.getItem(key);
+ return storedValue ? JSON.parse(storedValue) : defaultValue;
+ } catch (err) {
+ return defaultValue;
+ }
+};
+
+export const setItem = (key, value) => {
+ storage.setItem(key, JSON.stringify(value));
+};
+
+export const removeItem = (key) => {
+ storage.removeItem(key);
+};
diff --git a/yarn.lock b/yarn.lock
new file mode 100644
index 00000000..87b1733d
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,593 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@zeit/schemas@2.29.0":
+ version "2.29.0"
+ resolved "https://registry.yarnpkg.com/@zeit/schemas/-/schemas-2.29.0.tgz#a59ae6ebfdf4ddc66a876872dd736baa58b6696c"
+ integrity sha512-g5QiLIfbg3pLuYUJPlisNKY+epQJTcMDsOnVNkscrDP1oi7vmJnzOANYJI/1pZcVJ6umUkBv3aFtlg1UvUHGzA==
+
+accepts@~1.3.5:
+ version "1.3.8"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
+ integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
+ dependencies:
+ mime-types "~2.1.34"
+ negotiator "0.6.3"
+
+ajv@8.11.0:
+ version "8.11.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
+ integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
+ dependencies:
+ fast-deep-equal "^3.1.1"
+ json-schema-traverse "^1.0.0"
+ require-from-string "^2.0.2"
+ uri-js "^4.2.2"
+
+ansi-align@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59"
+ integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==
+ dependencies:
+ string-width "^4.1.0"
+
+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-regex@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a"
+ integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==
+
+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"
+
+ansi-styles@^6.1.0:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
+ integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
+
+arch@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
+ integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
+
+arg@5.0.2:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
+ integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
+
+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==
+
+boxen@7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/boxen/-/boxen-7.0.0.tgz#9e5f8c26e716793fc96edcf7cf754cdf5e3fbf32"
+ integrity sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==
+ dependencies:
+ ansi-align "^3.0.1"
+ camelcase "^7.0.0"
+ chalk "^5.0.1"
+ cli-boxes "^3.0.0"
+ string-width "^5.1.2"
+ type-fest "^2.13.0"
+ widest-line "^4.0.1"
+ wrap-ansi "^8.0.1"
+
+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"
+
+bytes@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
+ integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==
+
+camelcase@^7.0.0:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-7.0.1.tgz#f02e50af9fd7782bc8b88a3558c32fd3a388f048"
+ integrity sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==
+
+chalk-template@0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/chalk-template/-/chalk-template-0.4.0.tgz#692c034d0ed62436b9062c1707fadcd0f753204b"
+ integrity sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==
+ dependencies:
+ chalk "^4.1.2"
+
+chalk@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.0.1.tgz#ca57d71e82bb534a296df63bbacc4a1c22b2a4b6"
+ integrity sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==
+
+chalk@^4.1.2:
+ 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"
+
+chalk@^5.0.1:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
+ integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==
+
+cli-boxes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145"
+ integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==
+
+clipboardy@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-3.0.0.tgz#f3876247404d334c9ed01b6f269c11d09a5e3092"
+ integrity sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==
+ dependencies:
+ arch "^2.2.0"
+ execa "^5.1.1"
+ is-wsl "^2.2.0"
+
+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.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==
+
+compressible@~2.0.16:
+ version "2.0.18"
+ resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
+ integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
+ dependencies:
+ mime-db ">= 1.43.0 < 2"
+
+compression@1.7.4:
+ version "1.7.4"
+ resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f"
+ integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==
+ dependencies:
+ accepts "~1.3.5"
+ bytes "3.0.0"
+ compressible "~2.0.16"
+ debug "2.6.9"
+ on-headers "~1.0.2"
+ safe-buffer "5.1.2"
+ vary "~1.1.2"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+content-disposition@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
+ integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==
+
+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"
+
+debug@2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ dependencies:
+ ms "2.0.0"
+
+deep-extend@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+ integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+
+eastasianwidth@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
+ integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
+
+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==
+
+emoji-regex@^9.2.2:
+ version "9.2.2"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
+ integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
+
+execa@^5.1.1:
+ 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-url-parser@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d"
+ integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==
+ dependencies:
+ punycode "^1.3.2"
+
+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==
+
+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==
+
+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==
+
+ini@~1.3.0:
+ version "1.3.8"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
+ integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
+
+is-docker@^2.0.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
+ integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
+
+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-port-reachable@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-port-reachable/-/is-port-reachable-4.0.0.tgz#dac044091ef15319c8ab2f34604d8794181f8c2d"
+ integrity sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==
+
+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-wsl@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
+ integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
+ dependencies:
+ is-docker "^2.0.0"
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+json-schema-traverse@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
+ integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
+
+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==
+
+mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
+ version "1.52.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-db@~1.33.0:
+ version "1.33.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
+ integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==
+
+mime-types@2.1.18:
+ version "2.1.18"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
+ integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==
+ dependencies:
+ mime-db "~1.33.0"
+
+mime-types@~2.1.34:
+ version "2.1.35"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
+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==
+
+minimatch@3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@^1.2.0:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+ integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+ integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
+
+negotiator@0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
+ integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
+
+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"
+
+on-headers@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
+ integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
+
+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"
+
+path-is-inside@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+ integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==
+
+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-to-regexp@2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45"
+ integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==
+
+punycode@^1.3.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+ integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==
+
+punycode@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
+ integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
+
+range-parser@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
+ integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==
+
+rc@^1.0.1, rc@^1.1.6:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
+ integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+ dependencies:
+ deep-extend "^0.6.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+registry-auth-token@3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.2.tgz#851fd49038eecb586911115af845260eec983f20"
+ integrity sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==
+ dependencies:
+ rc "^1.1.6"
+ safe-buffer "^5.0.1"
+
+registry-url@3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942"
+ integrity sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==
+ dependencies:
+ rc "^1.0.1"
+
+require-from-string@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+ integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
+safe-buffer@5.1.2:
+ 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.0.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+serve-handler@6.1.5:
+ version "6.1.5"
+ resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.5.tgz#a4a0964f5c55c7e37a02a633232b6f0d6f068375"
+ integrity sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg==
+ dependencies:
+ bytes "3.0.0"
+ content-disposition "0.5.2"
+ fast-url-parser "1.1.3"
+ mime-types "2.1.18"
+ minimatch "3.1.2"
+ path-is-inside "1.0.2"
+ path-to-regexp "2.2.1"
+ range-parser "1.2.0"
+
+serve@^14.2.0:
+ version "14.2.0"
+ resolved "https://registry.yarnpkg.com/serve/-/serve-14.2.0.tgz#3d768e88fa13ad8644f2393599189707176e66b8"
+ integrity sha512-+HOw/XK1bW8tw5iBilBz/mJLWRzM8XM6MPxL4J/dKzdxq1vfdEWSwhaR7/yS8EJp5wzvP92p1qirysJvnEtjXg==
+ dependencies:
+ "@zeit/schemas" "2.29.0"
+ ajv "8.11.0"
+ arg "5.0.2"
+ boxen "7.0.0"
+ chalk "5.0.1"
+ chalk-template "0.4.0"
+ clipboardy "3.0.0"
+ compression "1.7.4"
+ is-port-reachable "4.0.0"
+ serve-handler "6.1.5"
+ update-check "1.5.4"
+
+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.7"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
+ integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+
+string-width@^4.1.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-width@^5.0.1, string-width@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
+ integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
+ dependencies:
+ eastasianwidth "^0.2.0"
+ emoji-regex "^9.2.2"
+ strip-ansi "^7.0.1"
+
+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-ansi@^7.0.1:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
+ integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
+ dependencies:
+ ansi-regex "^6.0.1"
+
+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-json-comments@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+ integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
+
+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"
+
+type-fest@^2.13.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
+ integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
+
+update-check@1.5.4:
+ version "1.5.4"
+ resolved "https://registry.yarnpkg.com/update-check/-/update-check-1.5.4.tgz#5b508e259558f1ad7dbc8b4b0457d4c9d28c8743"
+ integrity sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==
+ dependencies:
+ registry-auth-token "3.3.2"
+ registry-url "3.1.0"
+
+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"
+
+vary@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+ integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
+
+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"
+
+widest-line@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-4.0.1.tgz#a0fc673aaba1ea6f0a0d35b3c2795c9a9cc2ebf2"
+ integrity sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==
+ dependencies:
+ string-width "^5.0.1"
+
+wrap-ansi@^8.0.1:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
+ integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
+ dependencies:
+ ansi-styles "^6.1.0"
+ string-width "^5.0.1"
+ strip-ansi "^7.0.1"