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 ` +
+ ${content} +
${text}
+
+ `; + }; +} 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 ?? "제목 없음"} +
    + ${tooltipChildAddElement.render( + `
    +
    ` + )} + ${tooltipRemoveElement.render( + `
    x
    ` + )} +
    +
  • + `; + + 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}와(과) 일치하는 검색결과가 없습니다.

    ` + : `` + } +
    +
    +

    최근 검색 문서 목록

    + +
    + `; + + 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"