From 27159f53d3314427fd043ea3a66170990e4539ef Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 4 Aug 2022 20:05:12 +0200 Subject: [PATCH 01/32] Update readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 5ecff80..3f089ad 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # SVG Layout Designer React +[![Build Status](https://drone.siklos-chaneru.duckdns.org/api/badges/Siklos/svg-layout-designer-react/status.svg)](https://drone.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react) + +[![Build Status](https://drone.siklos-chaneru.duckdns.org/api/badges/Siklos/svg-layout-designer-react/status.svg?ref=refs/heads/dev)](https://drone.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react) + An svg layout designer. # Getting Started From 8e34d6b72afbf50e2b10302da601a022110dc8a5 Mon Sep 17 00:00:00 2001 From: Siklos Date: Fri, 5 Aug 2022 12:10:58 -0400 Subject: [PATCH 02/32] Implement export as SVG (#14) Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/14 --- .../FloatingButton/FloatingButton.tsx | 30 ++++++++++++- src/Components/SVG/SVG.tsx | 44 ++++++++++--------- src/Editor.tsx | 43 ++++++++++++++---- src/index.scss | 5 ++- 4 files changed, 90 insertions(+), 32 deletions(-) diff --git a/src/Components/FloatingButton/FloatingButton.tsx b/src/Components/FloatingButton/FloatingButton.tsx index fb5fc1c..544c2bd 100644 --- a/src/Components/FloatingButton/FloatingButton.tsx +++ b/src/Components/FloatingButton/FloatingButton.tsx @@ -1,10 +1,38 @@ import * as React from 'react'; +import { MenuIcon, XIcon } from '@heroicons/react/outline'; interface IFloatingButtonProps { + children: React.ReactNode[] | React.ReactNode + className: string } +const toggleState = ( + isHidden: boolean, + setHidden: React.Dispatch> +) => { + setHidden(!isHidden); +}; + const FloatingButton: React.FC = (props: IFloatingButtonProps) => { - return <>; + const [isHidden, setHidden] = React.useState(true); + const buttonListClasses = isHidden ? 'invisible opacity-0' : 'visible opacity-100'; + const icon = isHidden + ? + : ; + + return ( +
+
+ { props.children } +
+ +
); }; export default FloatingButton; diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx index c11e39b..bf8704b 100644 --- a/src/Components/SVG/SVG.tsx +++ b/src/Components/SVG/SVG.tsx @@ -18,7 +18,7 @@ interface ISVGState { export class SVG extends React.PureComponent { public state: ISVGState; - public svg: React.RefObject; + public static ID = 'svg'; constructor(props: ISVGProps) { super(props); @@ -29,7 +29,6 @@ export class SVG extends React.PureComponent { } as Value, tool: TOOL_PAN }; - this.svg = React.createRef(); } render() { @@ -49,25 +48,28 @@ export class SVG extends React.PureComponent { } return ( - this.setState({ value })} - tool={this.state.tool} onChangeTool={tool => this.setState({ tool })} - miniatureProps={{ - position: 'left', - background: '#616264', - width: window.innerWidth - 12, - height: 120 - }} - > - - { children } - - - +
+ this.setState({ value })} + tool={this.state.tool} onChangeTool={tool => this.setState({ tool })} + miniatureProps={{ + position: 'left', + background: '#616264', + width: window.innerWidth - 12, + height: 120 + }} + > + + { children } + + + +
+ ); }; } diff --git a/src/Editor.tsx b/src/Editor.tsx index 482b91a..89c38aa 100644 --- a/src/Editor.tsx +++ b/src/Editor.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { UploadIcon } from '@heroicons/react/outline'; +import { UploadIcon, PhotographIcon } from '@heroicons/react/outline'; import './Editor.scss'; import Sidebar from './Components/Sidebar/Sidebar'; import { ElementsSidebar } from './Components/ElementsSidebar/ElementsSidebar'; @@ -9,6 +9,7 @@ import { History } from './Components/History/History'; import { ContainerModel, findContainerById, IContainerModel, MakeIterator } from './Interfaces/ContainerModel'; import Properties from './Interfaces/Properties'; import { IHistoryState } from './App'; +import FloatingButton from './Components/FloatingButton/FloatingButton'; interface IEditorProps { configuration: Configuration, @@ -257,7 +258,7 @@ class Editor extends React.Component { } as IEditorState); } - public SaveEditor() { + public SaveEditorAsJSON() { const exportName = 'state'; const spaces = import.meta.env.DEV ? 4 : 0; const data = JSON.stringify(this.state, getCircularReplacer(), spaces); @@ -270,6 +271,20 @@ class Editor extends React.Component { downloadAnchorNode.remove(); } + public SaveEditorAsSVG() { + const svgWrapper = document.getElementById(SVG.ID) as HTMLElement; + const svg = svgWrapper.querySelector('svg') as SVGSVGElement; + const preface = '\r\n'; + const svgBlob = new Blob([preface, svg.outerHTML], { type: 'image/svg+xml;charset=utf-8' }); + const svgUrl = URL.createObjectURL(svgBlob); + const downloadLink = document.createElement('a'); + downloadLink.href = svgUrl; + downloadLink.download = 'newesttree.svg'; + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + } + /** * Render the application * @returns {JSX.Element} Rendered JSX element @@ -334,13 +349,23 @@ class Editor extends React.Component { > { current.MainContainer } - + + + + + ); } diff --git a/src/index.scss b/src/index.scss index c7e0d50..ad410c8 100644 --- a/src/index.scss +++ b/src/index.scss @@ -10,9 +10,12 @@ @apply pl-6 pr-6 pt-2 pb-2 w-full } .close-button { - @apply transition-all w-full h-auto p-4 flex + @apply transition-all w-full h-auto p-4 flex } .mainmenu-btn { @apply transition-all bg-blue-100 hover:bg-blue-200 text-blue-700 text-lg font-semibold p-8 rounded-lg } + .floating-btn { + @apply h-full w-full text-white align-middle items-center justify-center + } } \ No newline at end of file From 293af451448d486dc2cea67e75635aa1f962f5dc Mon Sep 17 00:00:00 2001 From: Siklos Date: Fri, 5 Aug 2022 15:38:44 -0400 Subject: [PATCH 03/32] Refactor Editor and module functions (#15) Moved all module functions to separate utils modules Replaced standard with standard with typescript Extracted UI elements to separate component Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/15 --- .eslintrc.cjs | 11 +- package-lock.json | 29 +++ package.json | 1 + src/App.tsx | 136 ++++++-------- .../ElementsSidebar/ElementsSidebar.tsx | 19 +- .../FloatingButton/FloatingButton.tsx | 2 +- src/Components/History/History.tsx | 12 +- src/Components/MainMenu/MainMenu.tsx | 2 +- src/Components/Properties/Properties.tsx | 6 +- src/Components/SVG/Elements/Container.tsx | 7 +- src/Components/SVG/Elements/Dimension.tsx | 18 +- .../SVG/Elements/DimensionLayer.tsx | 7 +- src/Components/SVG/Elements/Selector.tsx | 7 +- src/Components/SVG/SVG.tsx | 49 ++--- src/Components/Sidebar/Sidebar.tsx | 8 +- src/Components/UI/UI.tsx | 139 +++++++++++++++ src/Editor.tsx | 168 ++++-------------- src/Enums/AddingBehavior.ts | 4 +- src/Enums/XPositionReference.ts | 6 +- src/Interfaces/Configuration.ts | 6 +- src/Interfaces/ContainerModel.ts | 70 +------- src/Interfaces/Image.ts | 8 +- src/Interfaces/Properties.ts | 6 +- src/utils/itertools.ts | 65 +++++++ src/utils/saveload.ts | 54 ++++++ src/utils/test-utils.tsx | 4 +- 26 files changed, 477 insertions(+), 367 deletions(-) create mode 100644 src/Components/UI/UI.tsx create mode 100644 src/utils/itertools.ts create mode 100644 src/utils/saveload.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 86a1866..b72be75 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -5,7 +5,7 @@ module.exports = { }, extends: [ 'plugin:react/recommended', - 'standard' + 'standard-with-typescript' ], parser: '@typescript-eslint/parser', parserOptions: { @@ -13,7 +13,8 @@ module.exports = { jsx: true }, ecmaVersion: 'latest', - sourceType: 'module' + sourceType: 'module', + project: './tsconfig.json' }, plugins: [ 'react', @@ -21,9 +22,11 @@ module.exports = { ], rules: { 'space-before-function-paren': ['error', 'never'], + '@typescript-eslint/space-before-function-paren': ['error', 'never'], indent: ['warn', 2, { SwitchCase: 1 }], - semi: ['warn', 'always'], + semi: 'off', + '@typescript-eslint/semi': ['warn', 'always'], 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': 'error' + '@typescript-eslint/no-unused-vars': 'error', } }; diff --git a/package-lock.json b/package-lock.json index 83e5fcb..285432c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "autoprefixer": "^10.4.8", "eslint": "^8.20.0", "eslint-config-standard": "^17.0.0", + "eslint-config-standard-with-typescript": "^22.0.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-n": "^15.2.4", "eslint-plugin-promise": "^6.0.0", @@ -2697,6 +2698,24 @@ "eslint-plugin-promise": "^6.0.0" } }, + "node_modules/eslint-config-standard-with-typescript": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-22.0.0.tgz", + "integrity": "sha512-VA36U7UlFpwULvkdnh6MQj5GAV2Q+tT68ALLAwJP0ZuNXU2m0wX07uxX4qyLRdHgSzH4QJ73CveKBuSOYvh7vQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint-config-standard": "17.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0", + "eslint-plugin-promise": "^6.0.0", + "typescript": "*" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", @@ -8093,6 +8112,16 @@ "dev": true, "requires": {} }, + "eslint-config-standard-with-typescript": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-22.0.0.tgz", + "integrity": "sha512-VA36U7UlFpwULvkdnh6MQj5GAV2Q+tT68ALLAwJP0ZuNXU2m0wX07uxX4qyLRdHgSzH4QJ73CveKBuSOYvh7vQ==", + "dev": true, + "requires": { + "@typescript-eslint/parser": "^5.0.0", + "eslint-config-standard": "17.0.0" + } + }, "eslint-import-resolver-node": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", diff --git a/package.json b/package.json index 149d76f..b4e99f5 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "autoprefixer": "^10.4.8", "eslint": "^8.20.0", "eslint-config-standard": "^17.0.0", + "eslint-config-standard-with-typescript": "^22.0.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-n": "^15.2.4", "eslint-plugin-promise": "^6.0.0", diff --git a/src/App.tsx b/src/App.tsx index 2b8c7fd..705e40d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,25 +1,27 @@ import * as React from 'react'; import './App.scss'; import { MainMenu } from './Components/MainMenu/MainMenu'; -import { ContainerModel, findContainerById, IContainerModel, MakeIterator } from './Interfaces/ContainerModel'; +import { ContainerModel, IContainerModel } from './Interfaces/ContainerModel'; import Editor, { IEditorState } from './Editor'; -import { AvailableContainer } from './Interfaces/AvailableContainer'; import { Configuration } from './Interfaces/Configuration'; +import { Revive } from './utils/saveload'; export interface IHistoryState { - MainContainer: IContainerModel | null, - SelectedContainer: IContainerModel | null, - SelectedContainerId: string, + MainContainer: IContainerModel | null + SelectedContainer: IContainerModel | null + SelectedContainerId: string TypeCounters: Record } +// App will never have props +// eslint-disable-next-line @typescript-eslint/no-empty-interface interface IAppProps { } interface IAppState { - configuration: Configuration, - history: IHistoryState[], - historyCurrentStep: number, + configuration: Configuration + history: IHistoryState[] + historyCurrentStep: number isLoaded: boolean } @@ -32,7 +34,12 @@ export class App extends React.Component { configuration: { AvailableContainers: [], AvailableSymbols: [], - MainContainer: {} as AvailableContainer + MainContainer: { + Type: 'EmptyContainer', + Width: 3000, + Height: 200, + Style: {} + } }, history: [], historyCurrentStep: 0, @@ -40,7 +47,7 @@ export class App extends React.Component { }; } - componentDidMount() { + componentDidMount(): void { const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const state = urlParams.get('state'); @@ -50,35 +57,39 @@ export class App extends React.Component { } fetch(state) - .then((response) => response.json()) + .then( + async(response) => await response.json(), + (error) => { throw new Error(error); } + ) .then((data: IEditorState) => { this.LoadState(data); - }); + }, (error) => { throw new Error(error); }); } - public NewEditor() { + public NewEditor(): void { // Fetch the configuration from the API - fetchConfiguration().then((configuration: Configuration) => { + fetchConfiguration() + .then((configuration: Configuration) => { // Set the main container from the given properties of the API - const MainContainer = new ContainerModel( - null, - { - id: 'main', - parentId: 'null', - x: 0, - y: 0, - width: configuration.MainContainer.Width, - height: configuration.MainContainer.Height, - fillOpacity: 0, - stroke: 'black' - } - ); + const MainContainer = new ContainerModel( + null, + { + id: 'main', + parentId: 'null', + x: 0, + y: 0, + width: configuration.MainContainer.Width, + height: configuration.MainContainer.Height, + fillOpacity: 0, + stroke: 'black' + } + ); - // Save the configuration and the new MainContainer - // and default the selected container to it - this.setState({ - configuration, - history: + // Save the configuration and the new MainContainer + // and default the selected container to it + this.setState({ + configuration, + history: [ { MainContainer, @@ -86,13 +97,14 @@ export class App extends React.Component { TypeCounters: {} } ], - historyCurrentStep: 0, - isLoaded: true - } as IAppState); - }); + historyCurrentStep: 0, + isLoaded: true + }); + }, (error) => { throw new Error(error); } + ); } - public LoadEditor(files: FileList | null) { + public LoadEditor(files: FileList | null): void { if (files === null) { return; } @@ -107,7 +119,7 @@ export class App extends React.Component { reader.readAsText(file); } - private LoadState(editorState: IEditorState) { + private LoadState(editorState: IEditorState): void { Revive(editorState); this.setState({ @@ -115,10 +127,10 @@ export class App extends React.Component { history: editorState.history, historyCurrentStep: editorState.historyCurrentStep, isLoaded: true - } as IAppState); + }); } - public render() { + public render(): JSX.Element { if (this.state.isLoaded) { return (
@@ -149,16 +161,17 @@ export class App extends React.Component { export async function fetchConfiguration(): Promise { const url = `${import.meta.env.VITE_API_URL}`; // The test library cannot use the Fetch API - // @ts-ignore + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (window.fetch) { return await fetch(url, { method: 'POST' }) - .then((response) => - response.json() + .then(async(response) => + await response.json() ) as Configuration; } - return new Promise((resolve) => { + return await new Promise((resolve) => { const xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.onreadystatechange = function() { // Call a function when the state changes. @@ -169,38 +182,3 @@ export async function fetchConfiguration(): Promise { xhr.send(); }); } - -/** - * Revive the Editor state - * by setting the containers references to their parent - * @param editorState Editor state - */ -function Revive(editorState: IEditorState): void { - const history = editorState.history; - for (const state of history) { - if (state.MainContainer === null || state.MainContainer === undefined) { - continue; - } - - const it = MakeIterator(state.MainContainer); - for (const container of it) { - const parentId = container.properties.parentId; - if (parentId === null) { - container.parent = null; - continue; - } - const parent = findContainerById(state.MainContainer, parentId); - if (parent === undefined) { - continue; - } - container.parent = parent; - } - - const selected = findContainerById(state.MainContainer, state.SelectedContainerId); - if (selected === undefined) { - state.SelectedContainer = null; - continue; - } - state.SelectedContainer = selected; - } -} diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index c4ea51a..caa6081 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -1,31 +1,32 @@ import * as React from 'react'; import { motion } from 'framer-motion'; import { Properties } from '../Properties/Properties'; -import { IContainerModel, getDepth, MakeIterator } from '../../Interfaces/ContainerModel'; +import { IContainerModel } from '../../Interfaces/ContainerModel'; +import { getDepth, MakeIterator } from '../../utils/itertools'; interface IElementsSidebarProps { - MainContainer: IContainerModel | null, - isOpen: boolean, + MainContainer: IContainerModel | null + isOpen: boolean isHistoryOpen: boolean - SelectedContainer: IContainerModel | null, - onClick: () => void, - onPropertyChange: (key: string, value: string) => void, + SelectedContainer: IContainerModel | null + onClick: () => void + onPropertyChange: (key: string, value: string) => void selectContainer: (container: IContainerModel) => void } export class ElementsSidebar extends React.PureComponent { public iterateChilds(handleContainer: (container: IContainerModel) => void): React.ReactNode { - if (!this.props.MainContainer) { + if (this.props.MainContainer == null) { return null; } const it = MakeIterator(this.props.MainContainer); for (const container of it) { - handleContainer(container as IContainerModel); + handleContainer(container); } } - public render() { + public render(): JSX.Element { let isOpenClasses = '-right-64'; if (this.props.isOpen) { isOpenClasses = this.props.isHistoryOpen diff --git a/src/Components/FloatingButton/FloatingButton.tsx b/src/Components/FloatingButton/FloatingButton.tsx index 544c2bd..3145f45 100644 --- a/src/Components/FloatingButton/FloatingButton.tsx +++ b/src/Components/FloatingButton/FloatingButton.tsx @@ -9,7 +9,7 @@ interface IFloatingButtonProps { const toggleState = ( isHidden: boolean, setHidden: React.Dispatch> -) => { +): void => { setHidden(!isHidden); }; diff --git a/src/Components/History/History.tsx b/src/Components/History/History.tsx index e77a8c7..7487de1 100644 --- a/src/Components/History/History.tsx +++ b/src/Components/History/History.tsx @@ -2,19 +2,19 @@ import * as React from 'react'; import { IHistoryState } from '../../App'; interface IHistoryProps { - history: IHistoryState[], - historyCurrentStep: number, - isOpen: boolean, - onClick: () => void, + history: IHistoryState[] + historyCurrentStep: number + isOpen: boolean + onClick: () => void jumpTo: (move: number) => void } export class History extends React.PureComponent { - public render() { + public render(): JSX.Element { const isOpenClasses = this.props.isOpen ? 'right-0' : '-right-64'; const states = this.props.history.map((step, move) => { - const desc = move + const desc = move > 0 ? `Go to modification n°${move}` : 'Go to the beginning'; diff --git a/src/Components/MainMenu/MainMenu.tsx b/src/Components/MainMenu/MainMenu.tsx index 92b19d5..a9b3da2 100644 --- a/src/Components/MainMenu/MainMenu.tsx +++ b/src/Components/MainMenu/MainMenu.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; interface IMainMenuProps { - newEditor: () => void; + newEditor: () => void loadEditor: (files: FileList | null) => void } diff --git a/src/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index 65cfefd..0d30d3d 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -2,12 +2,12 @@ import * as React from 'react'; import ContainerProperties from '../../Interfaces/Properties'; interface IPropertiesProps { - properties?: ContainerProperties, + properties?: ContainerProperties onChange: (key: string, value: string) => void } export class Properties extends React.PureComponent { - public render() { + public render(): JSX.Element { if (this.props.properties === undefined) { return
; } @@ -27,7 +27,7 @@ export class Properties extends React.PureComponent { public handleProperties = ( [key, value]: [string, string | number], groupInput: React.ReactNode[] - ) => { + ): void => { const id = `property-${key}`; const type = 'text'; const isDisabled = key === 'id' || key === 'parentId'; // hardcoded diff --git a/src/Components/SVG/Elements/Container.tsx b/src/Components/SVG/Elements/Container.tsx index a352bcd..e64739c 100644 --- a/src/Components/SVG/Elements/Container.tsx +++ b/src/Components/SVG/Elements/Container.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import { getDepth, IContainerModel } from '../../../Interfaces/ContainerModel'; +import { IContainerModel } from '../../../Interfaces/ContainerModel'; +import { getDepth } from '../../../utils/itertools'; import { Dimension } from './Dimension'; export interface IContainerProps { @@ -20,11 +21,11 @@ export class Container extends React.PureComponent { const transform = `translate(${Number(this.props.model.properties.x)}, ${Number(this.props.model.properties.y)})`; // g style - const defaultStyle = { + const defaultStyle: React.CSSProperties = { transitionProperty: 'all', transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', transitionDuration: '150ms' - } as React.CSSProperties; + }; // Rect style const style = Object.assign( diff --git a/src/Components/SVG/Elements/Dimension.tsx b/src/Components/SVG/Elements/Dimension.tsx index 96ab2c9..612b3f7 100644 --- a/src/Components/SVG/Elements/Dimension.tsx +++ b/src/Components/SVG/Elements/Dimension.tsx @@ -1,19 +1,19 @@ import * as React from 'react'; interface IDimensionProps { - id: string; - xStart: number; - xEnd: number; - y: number; - text: string; - strokeWidth: number; + id: string + xStart: number + xEnd: number + y: number + text: string + strokeWidth: number } export class Dimension extends React.PureComponent { - public render() { - const style = { + public render(): JSX.Element { + const style: React.CSSProperties = { stroke: 'black' - } as React.CSSProperties; + }; return ( = (props) => { props.selected.properties.width, props.selected.properties.height ]; - const style = { + const style: React.CSSProperties = { stroke: '#3B82F6', // tw blue-500 strokeWidth: 4, fillOpacity: 0, @@ -26,7 +27,7 @@ export const Selector: React.FC = (props) => { transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', transitionDuration: '150ms', animation: 'fadein 750ms ease-in alternate infinite' - } as React.CSSProperties; + }; return ( { - public state: ISVGState; public static ID = 'svg'; + public state: ISVGState; constructor(props: ISVGProps) { super(props); this.state = { - value: { - viewerWidth: window.innerWidth, - viewerHeight: window.innerHeight - } as Value, - tool: TOOL_PAN + viewerWidth: window.innerWidth, + viewerHeight: window.innerHeight }; } - render() { + resizeViewBox(): void { + this.setState({ + viewerWidth: window.innerWidth, + viewerHeight: window.innerHeight + }); + } + + componentDidMount(): void { + window.addEventListener('resize', this.resizeViewBox.bind(this)); + } + + componentWillUnmount(): void { + window.removeEventListener('resize', this.resizeViewBox.bind(this)); + } + + render(): JSX.Element { const xmlns = ''; const properties = { @@ -49,13 +61,11 @@ export class SVG extends React.PureComponent { return (
- this.setState({ value })} - tool={this.state.tool} onChangeTool={tool => this.setState({ tool })} miniatureProps={{ position: 'left', background: '#616264', @@ -67,9 +77,8 @@ export class SVG extends React.PureComponent { { children } - +
- ); }; } diff --git a/src/Components/Sidebar/Sidebar.tsx b/src/Components/Sidebar/Sidebar.tsx index dd34812..b474cc9 100644 --- a/src/Components/Sidebar/Sidebar.tsx +++ b/src/Components/Sidebar/Sidebar.tsx @@ -3,13 +3,13 @@ import { AvailableContainer } from '../../Interfaces/AvailableContainer'; interface ISidebarProps { componentOptions: AvailableContainer[] - isOpen: boolean; - onClick: () => void; - buttonOnClick: (type: string) => void; + isOpen: boolean + onClick: () => void + buttonOnClick: (type: string) => void } export default class Sidebar extends React.PureComponent { - public render() { + public render(): JSX.Element { const listElements = this.props.componentOptions.map(componentOption => + + this.ToggleElementsSidebar()} + onPropertyChange={this.props.OnPropertyChange} + selectContainer={this.props.SelectContainer} + /> + + + this.ToggleHistory()} + jumpTo={this.props.LoadState} + /> + + + + + + + + ); + } +} diff --git a/src/Editor.tsx b/src/Editor.tsx index 89c38aa..8cb75f2 100644 --- a/src/Editor.tsx +++ b/src/Editor.tsx @@ -1,28 +1,22 @@ import React from 'react'; -import { UploadIcon, PhotographIcon } from '@heroicons/react/outline'; import './Editor.scss'; -import Sidebar from './Components/Sidebar/Sidebar'; -import { ElementsSidebar } from './Components/ElementsSidebar/ElementsSidebar'; import { Configuration } from './Interfaces/Configuration'; import { SVG } from './Components/SVG/SVG'; -import { History } from './Components/History/History'; -import { ContainerModel, findContainerById, IContainerModel, MakeIterator } from './Interfaces/ContainerModel'; -import Properties from './Interfaces/Properties'; +import { ContainerModel, IContainerModel } from './Interfaces/ContainerModel'; +import { findContainerById, MakeIterator } from './utils/itertools'; import { IHistoryState } from './App'; -import FloatingButton from './Components/FloatingButton/FloatingButton'; +import { getCircularReplacer } from './utils/saveload'; +import { UI } from './Components/UI/UI'; interface IEditorProps { - configuration: Configuration, - history: Array, + configuration: Configuration + history: IHistoryState[] historyCurrentStep: number } export interface IEditorState { - isSidebarOpen: boolean, - isElementsSidebarOpen: boolean, - isHistoryOpen: boolean, - history: Array, - historyCurrentStep: number, + history: IHistoryState[] + historyCurrentStep: number // do not use it, use props.configuration // only used for serialization purpose configuration: Configuration @@ -34,50 +28,20 @@ class Editor extends React.Component { constructor(props: IEditorProps) { super(props); this.state = { - isSidebarOpen: true, - isElementsSidebarOpen: false, - isHistoryOpen: false, configuration: Object.assign({}, props.configuration), history: [...props.history], historyCurrentStep: props.historyCurrentStep - } as IEditorState; + }; } public getCurrentHistory = (): IHistoryState[] => this.state.history.slice(0, this.state.historyCurrentStep + 1); public getCurrentHistoryState = (): IHistoryState => this.state.history[this.state.historyCurrentStep]; - /** - * Toggle the components sidebar - */ - public ToggleSidebar() { - this.setState({ - isSidebarOpen: !this.state.isSidebarOpen - } as IEditorState); - } - - /** - * Toggle the elements - */ - public ToggleElementsSidebar() { - this.setState({ - isElementsSidebarOpen: !this.state.isElementsSidebarOpen - } as IEditorState); - } - - /** - * Toggle the elements - */ - public ToggleHistory() { - this.setState({ - isHistoryOpen: !this.state.isHistoryOpen - } as IEditorState); - } - /** * Select a container * @param container Selected container */ - public SelectContainer(container: ContainerModel) { + public SelectContainer(container: ContainerModel): void { const history = this.getCurrentHistory(); const current = history[history.length - 1]; @@ -100,7 +64,7 @@ class Editor extends React.Component { SelectedContainerId: SelectedContainer.properties.id }]), historyCurrentStep: history.length - } as IEditorState); + }); } /** @@ -134,7 +98,7 @@ class Editor extends React.Component { TypeCounters: Object.assign({}, current.TypeCounters) }]), historyCurrentStep: history.length - } as IEditorState); + }); return; } @@ -156,7 +120,7 @@ class Editor extends React.Component { TypeCounters: Object.assign({}, current.TypeCounters) }]), historyCurrentStep: history.length - } as IEditorState); + }); } /** @@ -196,8 +160,7 @@ class Editor extends React.Component { const count = newCounters[type]; // Create maincontainer model - const structure: IContainerModel = structuredClone(current.MainContainer); - const clone = Object.assign(new ContainerModel(null, {} as Properties), structure); + const clone: IContainerModel = structuredClone(current.MainContainer); // Find the parent const it = MakeIterator(clone); @@ -230,7 +193,7 @@ class Editor extends React.Component { width: properties?.Width, height: parent.properties.height, ...properties.Style - } as Properties, + }, [], { type @@ -249,16 +212,16 @@ class Editor extends React.Component { SelectedContainerId: parent.properties.id }]), historyCurrentStep: history.length - } as IEditorState); + }); } - public jumpTo(move: number): void { + public LoadState(move: number): void { this.setState({ historyCurrentStep: move - } as IEditorState); + }); } - public SaveEditorAsJSON() { + public SaveEditorAsJSON(): void { const exportName = 'state'; const spaces = import.meta.env.DEV ? 4 : 0; const data = JSON.stringify(this.state, getCircularReplacer(), spaces); @@ -271,7 +234,7 @@ class Editor extends React.Component { downloadAnchorNode.remove(); } - public SaveEditorAsSVG() { + public SaveEditorAsSVG(): void { const svgWrapper = document.getElementById(SVG.ID) as HTMLElement; const svg = svgWrapper.querySelector('svg') as SVGSVGElement; const preface = '\r\n'; @@ -289,59 +252,22 @@ class Editor extends React.Component { * Render the application * @returns {JSX.Element} Rendered JSX element */ - render() { + render(): JSX.Element { const current = this.getCurrentHistoryState(); - let buttonRightOffsetClasses = 'right-12'; - if (this.state.isElementsSidebarOpen || this.state.isHistoryOpen) { - buttonRightOffsetClasses = 'right-72'; - } - if (this.state.isHistoryOpen && this.state.isElementsSidebarOpen) { - buttonRightOffsetClasses = 'right-[544px]'; - } return (
- this.ToggleSidebar()} - buttonOnClick={(type: string) => this.AddContainer(type)} - /> - - - this.ToggleElementsSidebar()} - onPropertyChange={(key: string, value: string) => this.OnPropertyChange(key, value)} - selectContainer={(container: ContainerModel) => this.SelectContainer(container)} - /> - - - this.ToggleHistory()} - jumpTo={(move) => { this.jumpTo(move); }} + AvailableContainers={this.state.configuration.AvailableContainers} + SelectContainer={(container) => this.SelectContainer(container)} + OnPropertyChange={(key, value) => this.OnPropertyChange(key, value)} + AddContainer={(type) => this.AddContainer(type)} + SaveEditorAsJSON={() => this.SaveEditorAsJSON()} + SaveEditorAsSVG={() => this.SaveEditorAsSVG()} + LoadState={(move) => this.LoadState(move)} /> - - { > { current.MainContainer } - - - - -
); } } -const getCircularReplacer = () => { - const seen = new WeakSet(); - return (key: any, value: object | null) => { - if (key === 'parent') { - return; - } - - if (typeof value === 'object' && value !== null) { - if (seen.has(value)) { - return; - } - seen.add(value); - } - return value; - }; -}; - export default Editor; diff --git a/src/Enums/AddingBehavior.ts b/src/Enums/AddingBehavior.ts index ddf722e..fb6ae67 100644 --- a/src/Enums/AddingBehavior.ts +++ b/src/Enums/AddingBehavior.ts @@ -1,4 +1,4 @@ export enum AddingBehavior { - InsertInto, - Replace + InsertInto, + Replace } diff --git a/src/Enums/XPositionReference.ts b/src/Enums/XPositionReference.ts index ec8108a..8571167 100644 --- a/src/Enums/XPositionReference.ts +++ b/src/Enums/XPositionReference.ts @@ -1,5 +1,5 @@ export enum XPositionReference { - Left, - Center, - Right + Left, + Center, + Right } diff --git a/src/Interfaces/Configuration.ts b/src/Interfaces/Configuration.ts index 2127299..f8d4854 100644 --- a/src/Interfaces/Configuration.ts +++ b/src/Interfaces/Configuration.ts @@ -3,7 +3,7 @@ import { AvailableSymbolModel } from './AvailableSymbol'; /** Model of configuration for the application to configure it */ export interface Configuration { - AvailableContainers: AvailableContainer[]; - AvailableSymbols: AvailableSymbolModel[]; - MainContainer: AvailableContainer; + AvailableContainers: AvailableContainer[] + AvailableSymbols: AvailableSymbolModel[] + MainContainer: AvailableContainer } diff --git a/src/Interfaces/ContainerModel.ts b/src/Interfaces/ContainerModel.ts index 787cf91..1c70ae3 100644 --- a/src/Interfaces/ContainerModel.ts +++ b/src/Interfaces/ContainerModel.ts @@ -1,9 +1,9 @@ import Properties from './Properties'; export interface IContainerModel { - children: IContainerModel[], - parent: IContainerModel | null, - properties: Properties, + children: IContainerModel[] + parent: IContainerModel | null + properties: Properties userData: Record } @@ -24,67 +24,3 @@ export class ContainerModel implements IContainerModel { this.userData = userData; } }; - -/** - * Returns a Generator iterating of over the children depth-first - */ -export function * MakeIterator(root: IContainerModel): Generator { - const queue: IContainerModel[] = [root]; - const visited = new Set(queue); - while (queue.length > 0) { - const container = queue.pop() as IContainerModel; - - yield container; - - // if this reverse() gets costly, replace it by a simple for - container.children.forEach((child) => { - if (visited.has(child)) { - return; - } - visited.add(child); - queue.push(child); - }); - } -} - -/** - * Returns the depth of the container - * @returns The depth of the container - */ -export function getDepth(parent: IContainerModel) { - let depth = 0; - - let current: IContainerModel | null = parent; - while (current != null) { - depth++; - current = current.parent; - } - - return depth; -} - -/** - * Returns the absolute position by iterating to the parent - * @returns The absolute position of the container - */ -export function getAbsolutePosition(container: IContainerModel): [number, number] { - let x = Number(container.properties.x); - let y = Number(container.properties.y); - let current = container.parent; - while (current != null) { - x += Number(current.properties.x); - y += Number(current.properties.y); - current = current.parent; - } - return [x, y]; -} - -export function findContainerById(root: IContainerModel, id: string): IContainerModel | undefined { - const it = MakeIterator(root); - for (const container of it) { - if (container.properties.id === id) { - return container; - } - } - return undefined; -} diff --git a/src/Interfaces/Image.ts b/src/Interfaces/Image.ts index 723d04b..b839b09 100644 --- a/src/Interfaces/Image.ts +++ b/src/Interfaces/Image.ts @@ -1,7 +1,7 @@ /** Model of an image with multiple source */ export interface Image { - Name: string; - Url: string; - Base64Image: string; - Svg: string; + Name: string + Url: string + Base64Image: string + Svg: string } diff --git a/src/Interfaces/Properties.ts b/src/Interfaces/Properties.ts index 79f4f47..ed185f2 100644 --- a/src/Interfaces/Properties.ts +++ b/src/Interfaces/Properties.ts @@ -1,8 +1,8 @@ import * as React from 'react'; export default interface Properties extends React.CSSProperties { - id: string, - parentId: string | null, - x: number, + id: string + parentId: string | null + x: number y: number } diff --git a/src/utils/itertools.ts b/src/utils/itertools.ts new file mode 100644 index 0000000..031b497 --- /dev/null +++ b/src/utils/itertools.ts @@ -0,0 +1,65 @@ +import { IContainerModel } from '../Interfaces/ContainerModel'; + +/** + * Returns a Generator iterating of over the children depth-first + */ +export function * MakeIterator(root: IContainerModel): Generator { + const queue: IContainerModel[] = [root]; + const visited = new Set(queue); + while (queue.length > 0) { + const container = queue.pop() as IContainerModel; + + yield container; + + // if this reverse() gets costly, replace it by a simple for + container.children.forEach((child) => { + if (visited.has(child)) { + return; + } + visited.add(child); + queue.push(child); + }); + } +} + +/** + * Returns the depth of the container + * @returns The depth of the container + */ +export function getDepth(parent: IContainerModel): number { + let depth = 0; + + let current: IContainerModel | null = parent; + while (current != null) { + depth++; + current = current.parent; + } + + return depth; +} + +/** + * Returns the absolute position by iterating to the parent + * @returns The absolute position of the container + */ +export function getAbsolutePosition(container: IContainerModel): [number, number] { + let x = Number(container.properties.x); + let y = Number(container.properties.y); + let current = container.parent; + while (current != null) { + x += Number(current.properties.x); + y += Number(current.properties.y); + current = current.parent; + } + return [x, y]; +} + +export function findContainerById(root: IContainerModel, id: string): IContainerModel | undefined { + const it = MakeIterator(root); + for (const container of it) { + if (container.properties.id === id) { + return container; + } + } + return undefined; +} diff --git a/src/utils/saveload.ts b/src/utils/saveload.ts new file mode 100644 index 0000000..8993ad4 --- /dev/null +++ b/src/utils/saveload.ts @@ -0,0 +1,54 @@ +import { findContainerById, MakeIterator } from './itertools'; +import { IEditorState } from '../Editor'; + +/** + * Revive the Editor state + * by setting the containers references to their parent + * @param editorState Editor state + */ +export function Revive(editorState: IEditorState): void { + const history = editorState.history; + for (const state of history) { + if (state.MainContainer === null || state.MainContainer === undefined) { + continue; + } + + const it = MakeIterator(state.MainContainer); + for (const container of it) { + const parentId = container.properties.parentId; + if (parentId === null) { + container.parent = null; + continue; + } + const parent = findContainerById(state.MainContainer, parentId); + if (parent === undefined) { + continue; + } + container.parent = parent; + } + + const selected = findContainerById(state.MainContainer, state.SelectedContainerId); + if (selected === undefined) { + state.SelectedContainer = null; + continue; + } + state.SelectedContainer = selected; + } +} + +export const getCircularReplacer = (): (key: any, value: object | null) => object | null | undefined => { + const seen = new WeakSet(); + return (key: any, value: object | null) => { + if (key === 'parent') { + return; + } + + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return; + } + seen.add(value); + } + return value; + }; +}; diff --git a/src/utils/test-utils.tsx b/src/utils/test-utils.tsx index bb5dc8b..139850e 100644 --- a/src/utils/test-utils.tsx +++ b/src/utils/test-utils.tsx @@ -1,13 +1,13 @@ /* eslint-disable import/export */ import * as React from 'react'; -import { cleanup, render } from '@testing-library/react'; +import { cleanup, render, RenderResult } from '@testing-library/react'; import { afterEach } from 'vitest'; afterEach(() => { cleanup(); }); -const customRender = (ui: React.ReactElement, options = {}) => +const customRender = (ui: React.ReactElement, options = {}): RenderResult => render(ui, { // wrap provider(s) here if needed wrapper: ({ children }) => children, From ccdaaeec73bf6c49f18f09fb8b70882f761d20bb Mon Sep 17 00:00:00 2001 From: Siklos Date: Fri, 5 Aug 2022 17:29:16 -0400 Subject: [PATCH 04/32] Implement ctrl-z/ctrl-y (#16) Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/16 --- src/Components/SVG/SVG.tsx | 4 ++-- src/Editor.tsx | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx index d2cc1b2..9ebe3af 100644 --- a/src/Components/SVG/SVG.tsx +++ b/src/Components/SVG/SVG.tsx @@ -36,11 +36,11 @@ export class SVG extends React.PureComponent { } componentDidMount(): void { - window.addEventListener('resize', this.resizeViewBox.bind(this)); + window.addEventListener('resize', () => this.resizeViewBox()); } componentWillUnmount(): void { - window.removeEventListener('resize', this.resizeViewBox.bind(this)); + window.removeEventListener('resize', () => this.resizeViewBox()); } render(): JSX.Element { diff --git a/src/Editor.tsx b/src/Editor.tsx index 8cb75f2..5bdca49 100644 --- a/src/Editor.tsx +++ b/src/Editor.tsx @@ -34,6 +34,30 @@ class Editor extends React.Component { }; } + componentDidMount(): void { + window.addEventListener('keyup', (event) => this.onKeyDown(event)); + } + + componentWillUnmount(): void { + window.removeEventListener('keyup', (event) => this.onKeyDown(event)); + } + + public onKeyDown(event: KeyboardEvent): void { + event.preventDefault(); + if (event.isComposing || event.keyCode === 229) { + return; + } + if (event.key === 'z' && + event.ctrlKey && + this.state.historyCurrentStep > 0) { + this.LoadState(this.state.historyCurrentStep - 1); + } else if (event.key === 'y' && + event.ctrlKey && + this.state.historyCurrentStep < this.state.history.length - 1) { + this.LoadState(this.state.historyCurrentStep + 1); + } + } + public getCurrentHistory = (): IHistoryState[] => this.state.history.slice(0, this.state.historyCurrentStep + 1); public getCurrentHistoryState = (): IHistoryState => this.state.history[this.state.historyCurrentStep]; From 126b34fda46607a101e779fa8bb46785f0169ebc Mon Sep 17 00:00:00 2001 From: Siklos Date: Sat, 6 Aug 2022 15:24:01 +0200 Subject: [PATCH 05/32] Added Sidebar test --- src/Components/Sidebar/Sidebar.test.tsx | 53 ++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/src/Components/Sidebar/Sidebar.test.tsx b/src/Components/Sidebar/Sidebar.test.tsx index c17f48e..3613fc8 100644 --- a/src/Components/Sidebar/Sidebar.test.tsx +++ b/src/Components/Sidebar/Sidebar.test.tsx @@ -1,19 +1,60 @@ import * as React from 'react'; -import { describe, test, expect } from 'vitest'; -import { render, screen } from '../../utils/test-utils'; +import { describe, test, expect, vi } from 'vitest'; +import { findByText, fireEvent, render, screen } from '../../utils/test-utils'; import Sidebar from './Sidebar'; -describe('Sidebar test', () => { - test('Start empty', () => { +describe.concurrent('Sidebar open', () => { + it('Start default', async() => { + const handleClick = vi.fn(); render( {}} + onClick={handleClick} buttonOnClick={() => {}} /> ); + const stuff = screen.queryByText(/stuff/i); + const close = screen.getByText(/close/i); - expect(screen.getByText(/Components/i)).toBeDefined(); + expect(screen.getByText(/Components/i).classList.contains('left-0')).toBeDefined(); + expect(stuff).toBeNull(); + fireEvent.click(close); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it('Start close', async() => { + render( {}} + buttonOnClick={() => {}} + />); + + const stuff = screen.queryByText(/stuff/i); + expect(screen.getByText(/Components/i).classList.contains('-left-64')).toBeDefined(); + expect(stuff).toBeNull(); + }); + + it('With stuff', async() => { + const handleButtonClick = vi.fn(); + render( {}} + buttonOnClick={handleButtonClick} + />); + const stuff = screen.getByText(/stuff/i); + + expect(stuff).toBeDefined(); + fireEvent.click(stuff); + expect(handleButtonClick).toHaveBeenCalledTimes(1); }); }); From 7e4dbd9e2db4ec78320cc696677c8dc21e17fbc3 Mon Sep 17 00:00:00 2001 From: Siklos Date: Sat, 6 Aug 2022 15:27:57 +0200 Subject: [PATCH 06/32] Update some tests --- src/Components/Sidebar/Sidebar.test.tsx | 7 +++++-- src/test/api.test.tsx | 8 ++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Components/Sidebar/Sidebar.test.tsx b/src/Components/Sidebar/Sidebar.test.tsx index 3613fc8..be836a9 100644 --- a/src/Components/Sidebar/Sidebar.test.tsx +++ b/src/Components/Sidebar/Sidebar.test.tsx @@ -37,11 +37,14 @@ describe.concurrent('Sidebar open', () => { }); it('With stuff', async() => { - const handleButtonClick = vi.fn(); + const Type = 'stuff'; + const handleButtonClick = vi.fn((type: string) => { + expect(type).toBe(Type); + }); render( { - test('Load environment', () => { +describe.concurrent('API test', () => { + it('Load environment', () => { const url = import.meta.env.VITE_API_URL; expect(url).toBe('http://localhost:5000'); }); - test('Fetch configuration', async() => { + it('Fetch configuration', async() => { const configuration = await fetchConfiguration(); expect(configuration.MainContainer).toBeDefined(); expect(configuration.MainContainer.Height).toBeGreaterThan(0); From 548b70f951b0fa65734cc7b17f61d5d0fcfd4b4c Mon Sep 17 00:00:00 2001 From: Siklos Date: Sat, 6 Aug 2022 16:29:32 +0200 Subject: [PATCH 07/32] Added test for properties --- src/Components/Properties/Properties.test.tsx | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/Components/Properties/Properties.test.tsx diff --git a/src/Components/Properties/Properties.test.tsx b/src/Components/Properties/Properties.test.tsx new file mode 100644 index 0000000..05949a0 --- /dev/null +++ b/src/Components/Properties/Properties.test.tsx @@ -0,0 +1,75 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import * as React from 'react'; +import { describe, it, vi } from 'vitest'; +import { Properties } from './Properties'; + +describe.concurrent('Properties', () => { + it('No properties', () => { + render( {}} + />); + + expect(screen.queryByText(/property-/i)).toBeNull(); + }); + + it('Some properties', () => { + const prop = { + id: 'stuff', + parentId: 'parentId', + x: 1, + y: 1 + }; + + const handleChange = vi.fn((key, value) => { + (prop as any)[key] = value; + }); + + const { container, rerender } = render(); + + let propertyId = container.querySelector('#property-id'); + let propertyParentId = container.querySelector('#property-parentId'); + let propertyX = container.querySelector('#property-x'); + let propertyY = container.querySelector('#property-y'); + expect(propertyId).toBeDefined(); + expect((propertyId as HTMLInputElement).value).toBe('stuff'); + expect(propertyParentId).toBeDefined(); + expect((propertyParentId as HTMLInputElement).value).toBe('parentId'); + expect(propertyX).toBeDefined(); + expect((propertyX as HTMLInputElement).value).toBe('1'); + expect(propertyY).toBeDefined(); + expect((propertyY as HTMLInputElement).value).toBe('1'); + + fireEvent.change(propertyId as Element, { target: { value: 'stuffed' } }); + fireEvent.change(propertyParentId as Element, { target: { value: 'parentedId' } }); + fireEvent.change(propertyX as Element, { target: { value: '2' } }); + fireEvent.change(propertyY as Element, { target: { value: '2' } }); + expect(handleChange).toBeCalledTimes(4); + + expect(prop.id).toBe('stuffed'); + expect(prop.parentId).toBe('parentedId'); + expect(prop.x).toBe('2'); + expect(prop.y).toBe('2'); + rerender(); + + + propertyId = container.querySelector('#property-id'); + propertyParentId = container.querySelector('#property-parentId'); + propertyX = container.querySelector('#property-x'); + propertyY = container.querySelector('#property-y'); + expect(propertyId).toBeDefined(); + expect((propertyId as HTMLInputElement).value).toBe('stuffed'); + expect(propertyParentId).toBeDefined(); + expect((propertyParentId as HTMLInputElement).value).toBe('parentedId'); + expect(propertyX).toBeDefined(); + expect((propertyX as HTMLInputElement).value).toBe('2'); + expect(propertyY).toBeDefined(); + expect((propertyY as HTMLInputElement).value).toBe('2'); + }); +}); From db0c2fe051357a24c47153cecb34516589d4a538 Mon Sep 17 00:00:00 2001 From: Siklos Date: Sat, 6 Aug 2022 16:29:51 +0200 Subject: [PATCH 08/32] Remove expect calls in callback functions --- src/Components/Sidebar/Sidebar.test.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Components/Sidebar/Sidebar.test.tsx b/src/Components/Sidebar/Sidebar.test.tsx index be836a9..8f283eb 100644 --- a/src/Components/Sidebar/Sidebar.test.tsx +++ b/src/Components/Sidebar/Sidebar.test.tsx @@ -3,8 +3,8 @@ import { describe, test, expect, vi } from 'vitest'; import { findByText, fireEvent, render, screen } from '../../utils/test-utils'; import Sidebar from './Sidebar'; -describe.concurrent('Sidebar open', () => { - it('Start default', async() => { +describe.concurrent('Sidebar', () => { + it('Start default', () => { const handleClick = vi.fn(); render( { expect(handleClick).toHaveBeenCalledTimes(1); }); - it('Start close', async() => { + it('Start close', () => { render( { expect(stuff).toBeNull(); }); - it('With stuff', async() => { + it('With stuff', () => { const Type = 'stuff'; - const handleButtonClick = vi.fn((type: string) => { - expect(type).toBe(Type); - }); + const handleButtonClick = vi.fn(); render( Date: Sat, 6 Aug 2022 20:31:42 +0200 Subject: [PATCH 09/32] Improve Properties tests --- src/Components/Properties/Properties.test.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Components/Properties/Properties.test.tsx b/src/Components/Properties/Properties.test.tsx index 05949a0..1ce9d25 100644 --- a/src/Components/Properties/Properties.test.tsx +++ b/src/Components/Properties/Properties.test.tsx @@ -10,7 +10,10 @@ describe.concurrent('Properties', () => { onChange={() => {}} />); - expect(screen.queryByText(/property-/i)).toBeNull(); + expect(screen.queryByText('id')).toBeNull(); + expect(screen.queryByText('parentId')).toBeNull(); + expect(screen.queryByText('x')).toBeNull(); + expect(screen.queryByText('y')).toBeNull(); }); it('Some properties', () => { @@ -30,6 +33,11 @@ describe.concurrent('Properties', () => { onChange={handleChange} />); + expect(screen.queryByText('id')).toBeDefined(); + expect(screen.queryByText('parentId')).toBeDefined(); + expect(screen.queryByText('x')).toBeDefined(); + expect(screen.queryByText('y')).toBeDefined(); + let propertyId = container.querySelector('#property-id'); let propertyParentId = container.querySelector('#property-parentId'); let propertyX = container.querySelector('#property-x'); @@ -58,7 +66,6 @@ describe.concurrent('Properties', () => { onChange={handleChange} />); - propertyId = container.querySelector('#property-id'); propertyParentId = container.querySelector('#property-parentId'); propertyX = container.querySelector('#property-x'); From 4efbc33893f85ae9729c7c1d69764e6f38468d5d Mon Sep 17 00:00:00 2001 From: Siklos Date: Sun, 7 Aug 2022 15:20:17 +0200 Subject: [PATCH 10/32] Allow to start from scratch with a default config --- src/App.tsx | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 705e40d..263827b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -100,8 +100,39 @@ export class App extends React.Component { historyCurrentStep: 0, isLoaded: true }); - }, (error) => { throw new Error(error); } - ); + }, (error) => { + // TODO: Implement an alert component + console.warn('[NewEditor] Could not fetch resource from API. Returning default.', error); + const MainContainer = new ContainerModel( + null, + { + id: 'main', + parentId: 'null', + x: 0, + y: 0, + width: DEFAULT_CONFIG.MainContainer.Width, + height: DEFAULT_CONFIG.MainContainer.Height, + fillOpacity: DEFAULT_CONFIG.MainContainer.Style.fillOpacity, + stroke: DEFAULT_CONFIG.MainContainer.Style.stroke, + } + ); + + // Save the configuration and the new MainContainer + // and default the selected container to it + this.setState({ + configuration: DEFAULT_CONFIG, + history: + [ + { + MainContainer, + SelectedContainer: MainContainer, + TypeCounters: {} + } + ], + historyCurrentStep: 0, + isLoaded: true + }); + }); } public LoadEditor(files: FileList | null): void { @@ -182,3 +213,28 @@ export async function fetchConfiguration(): Promise { xhr.send(); }); } + + +const DEFAULT_CONFIG: Configuration = { + AvailableContainers: [ + { + Type: 'Container', + Width: 75, + Height: 100, + Style: { + fillOpacity: 0, + stroke: 'green' + } + } + ], + AvailableSymbols: [], + MainContainer: { + Type: 'Container', + Width: 2000, + Height: 100, + Style: { + fillOpacity: 0, + stroke: 'black' + } + } +} From 2d048df3fe47dd645d5c96d3a9431990307313e0 Mon Sep 17 00:00:00 2001 From: Siklos Date: Sun, 7 Aug 2022 15:59:16 +0200 Subject: [PATCH 11/32] Add tests for elements sidebar --- .../ElementsSidebar/ElementsSidebar.test.tsx | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 src/Components/ElementsSidebar/ElementsSidebar.test.tsx diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx new file mode 100644 index 0000000..d52fe97 --- /dev/null +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -0,0 +1,242 @@ +import { describe, test, expect, vi } from 'vitest'; +import * as React from 'react'; +import { fireEvent, render, screen } from '../../utils/test-utils'; +import { ElementsSidebar } from './ElementsSidebar'; +import { IContainerModel } from '../../Interfaces/ContainerModel'; + +describe.concurrent('Elements sidebar', () => { + it('No elements', () => { + render( {}} + onPropertyChange={() => {}} + selectContainer={() => {}} + />); + + expect(screen.getByText(/Elements/i)); + expect(screen.queryByText('id')).toBeNull(); + expect(screen.queryByText(/main/i)).toBeNull(); + }); + + it('With a MainContainer', () => { + render( {}} + onPropertyChange={() => {}} + selectContainer={() => {}} + />); + + expect(screen.getByText(/Elements/i)); + expect(screen.queryByText('id')).toBeNull(); + expect(screen.getByText(/main/i)); + }); + + it('With a selected MainContainer', () => { + const MainContainer = { + children: [], + parent: null, + properties: { + id: 'main', + parentId: '', + x: 0, + y: 0, + width: 2000, + height: 100 + }, + userData: {} + }; + + const { container } = render( {}} + onPropertyChange={() => {}} + selectContainer={() => {}} + />); + + expect(screen.getByText(/Elements/i)); + expect(screen.getByText(/main/i)); + expect(screen.queryByText('id')).toBeDefined(); + expect(screen.queryByText('parentId')).toBeDefined(); + expect(screen.queryByText('x')).toBeDefined(); + expect(screen.queryByText('y')).toBeDefined(); + expect(screen.queryByText('width')).toBeDefined(); + expect(screen.queryByText('height')).toBeDefined(); + const propertyId = container.querySelector('#property-id'); + const propertyParentId = container.querySelector('#property-parentId'); + const propertyX = container.querySelector('#property-x'); + const propertyY = container.querySelector('#property-y'); + const propertyWidth = container.querySelector('#property-width'); + const propertyHeight = container.querySelector('#property-height'); + expect((propertyId as HTMLInputElement).value).toBe(MainContainer.properties.id.toString()); + expect(propertyParentId).toBeDefined(); + expect((propertyParentId as HTMLInputElement).value).toBe(''); + expect(propertyX).toBeDefined(); + expect((propertyX as HTMLInputElement).value).toBe(MainContainer.properties.x.toString()); + expect(propertyY).toBeDefined(); + expect((propertyY as HTMLInputElement).value).toBe(MainContainer.properties.y.toString()); + expect(propertyWidth).toBeDefined(); + expect((propertyWidth as HTMLInputElement).value).toBe(MainContainer.properties.width.toString()); + expect(propertyHeight).toBeDefined(); + expect((propertyHeight as HTMLInputElement).value).toBe(MainContainer.properties.height.toString()); + }); + + it('With multiple containers', () => { + const children: IContainerModel[] = []; + const MainContainer = { + children, + parent: null, + properties: { + id: 'main', + parentId: '', + x: 0, + y: 0, + width: 2000, + height: 100 + }, + userData: {} + }; + + children.push( + { + children: [], + parent: MainContainer, + properties: { + id: 'child-1', + parentId: 'main', + x: 0, + y: 0, + width: 0, + height: 0 + }, + userData: {} + } + ); + + children.push( + { + children: [], + parent: MainContainer, + properties: { + id: 'child-2', + parentId: 'main', + x: 0, + y: 0, + width: 0, + height: 0 + }, + userData: {} + } + ); + + render( {}} + onPropertyChange={() => {}} + selectContainer={() => {}} + />); + + expect(screen.getByText(/Elements/i)); + expect(screen.queryByText('id')).toBeDefined(); + expect(screen.getByText(/main/i)); + expect(screen.getByText(/child-1/i)); + expect(screen.getByText(/child-2/i)); + }); + + it('With multiple containers, change selection', () => { + const children: IContainerModel[] = []; + const MainContainer: IContainerModel = { + children, + parent: null, + properties: { + id: 'main', + parentId: '', + x: 0, + y: 0, + width: 2000, + height: 100 + }, + userData: {} + }; + + const child1Model: IContainerModel = { + children: [], + parent: MainContainer, + properties: { + id: 'child-1', + parentId: 'main', + x: 0, + y: 0, + width: 0, + height: 0 + }, + userData: {} + }; + children.push(child1Model); + + let SelectedContainer = MainContainer; + const selectContainer = vi.fn((container: IContainerModel) => { + SelectedContainer = container; + }); + + const { container, rerender } = render( {}} + onPropertyChange={() => {}} + selectContainer={selectContainer} + />); + + expect(screen.getByText(/Elements/i)); + expect(screen.queryByText('id')).toBeDefined(); + expect(screen.getByText(/main/i)); + const child1 = screen.getByText(/child-1/i); + expect(child1); + const propertyId = container.querySelector('#property-id'); + const propertyParentId = container.querySelector('#property-parentId'); + expect((propertyId as HTMLInputElement).value).toBe(MainContainer.properties.id.toString()); + expect((propertyParentId as HTMLInputElement).value).toBe(''); + + fireEvent.click(child1); + + rerender( {}} + onPropertyChange={() => {}} + selectContainer={selectContainer} + />); + + expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy(); + expect((propertyParentId as HTMLInputElement).value === '').toBeFalsy(); + expect((propertyId as HTMLInputElement).value).toBe(child1Model.properties.id.toString()); + expect((propertyParentId as HTMLInputElement).value).toBe(child1Model.properties.parentId?.toString()); + }); +}); From 3fa33161574d33bef312dc191e1dee9089228d10 Mon Sep 17 00:00:00 2001 From: Siklos Date: Sun, 7 Aug 2022 16:06:20 +0200 Subject: [PATCH 12/32] Fix Floating button hidden behind editor --- src/Components/UI/UI.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index 6476af5..7748c67 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -117,7 +117,7 @@ export class UI extends React.PureComponent { ☰ History - + + ); +}; diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index d52fe97..2dd5080 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -1,4 +1,4 @@ -import { describe, test, expect, vi } from 'vitest'; +import { describe, expect, vi } from 'vitest'; import * as React from 'react'; import { fireEvent, render, screen } from '../../utils/test-utils'; import { ElementsSidebar } from './ElementsSidebar'; @@ -11,7 +11,6 @@ describe.concurrent('Elements sidebar', () => { isOpen={true} isHistoryOpen={false} SelectedContainer={null} - onClick={() => {}} onPropertyChange={() => {}} selectContainer={() => {}} />); @@ -39,7 +38,6 @@ describe.concurrent('Elements sidebar', () => { isOpen={true} isHistoryOpen={false} SelectedContainer={null} - onClick={() => {}} onPropertyChange={() => {}} selectContainer={() => {}} />); @@ -69,7 +67,6 @@ describe.concurrent('Elements sidebar', () => { isOpen={true} isHistoryOpen={false} SelectedContainer={MainContainer} - onClick={() => {}} onPropertyChange={() => {}} selectContainer={() => {}} />); @@ -154,7 +151,6 @@ describe.concurrent('Elements sidebar', () => { isOpen={true} isHistoryOpen={false} SelectedContainer={MainContainer} - onClick={() => {}} onPropertyChange={() => {}} selectContainer={() => {}} />); @@ -207,7 +203,6 @@ describe.concurrent('Elements sidebar', () => { isOpen={true} isHistoryOpen={false} SelectedContainer={SelectedContainer} - onClick={() => {}} onPropertyChange={() => {}} selectContainer={selectContainer} />); @@ -229,7 +224,6 @@ describe.concurrent('Elements sidebar', () => { isOpen={true} isHistoryOpen={false} SelectedContainer={SelectedContainer} - onClick={() => {}} onPropertyChange={() => {}} selectContainer={selectContainer} />); diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index caa6081..bbbcba5 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -9,7 +9,6 @@ interface IElementsSidebarProps { isOpen: boolean isHistoryOpen: boolean SelectedContainer: IContainerModel | null - onClick: () => void onPropertyChange: (key: string, value: string) => void selectContainer: (container: IContainerModel) => void } @@ -42,8 +41,8 @@ export class ElementsSidebar extends React.PureComponent const selectedClass: string = this.props.SelectedContainer !== undefined && this.props.SelectedContainer !== null && this.props.SelectedContainer.properties.id === container.properties.id - ? 'bg-blue-500 hover:bg-blue-600' - : 'bg-slate-400 hover:bg-slate-600'; + ? 'border-l-4 border-blue-500 bg-slate-400/60 hover:bg-slate-400' + : 'bg-slate-300/60 hover:bg-slate-300'; containerRows.push( }); return ( -
- -
+
+
Elements
-
+
{ containerRows }
diff --git a/src/Components/FloatingButton/FloatingButton.tsx b/src/Components/FloatingButton/FloatingButton.tsx index 3145f45..d55f01b 100644 --- a/src/Components/FloatingButton/FloatingButton.tsx +++ b/src/Components/FloatingButton/FloatingButton.tsx @@ -21,7 +21,7 @@ const FloatingButton: React.FC = (props: IFloatingButtonPr : ; return ( -
+
{ props.children }
diff --git a/src/Components/History/History.tsx b/src/Components/History/History.tsx index 7487de1..c01298d 100644 --- a/src/Components/History/History.tsx +++ b/src/Components/History/History.tsx @@ -5,7 +5,6 @@ interface IHistoryProps { history: IHistoryState[] historyCurrentStep: number isOpen: boolean - onClick: () => void jumpTo: (move: number) => void } @@ -46,12 +45,9 @@ export class History extends React.PureComponent { states.reverse(); return ( -
- -
- History +
+
+ Timeline
{ states } diff --git a/src/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index 0d30d3d..adcebe6 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -18,7 +18,7 @@ export class Properties extends React.PureComponent { .forEach((pair) => this.handleProperties(pair, groupInput)); return ( -
+
{ groupInput }
); @@ -33,12 +33,12 @@ export class Properties extends React.PureComponent { const isDisabled = key === 'id' || key === 'parentId'; // hardcoded groupInput.push(
- + void buttonOnClick: (type: string) => void } export default class Sidebar extends React.PureComponent { public render(): JSX.Element { const listElements = this.props.componentOptions.map(componentOption => - ); - const isOpenClasses = this.props.isOpen ? 'left-0' : '-left-64'; + const isOpenClasses = this.props.isOpen ? 'left-16' : '-left-64'; return ( -
- -
+
+
Components
- {listElements} +
+ {listElements} +
); } diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index 7748c67..37420bd 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -7,6 +7,7 @@ import { ContainerModel } from '../../Interfaces/ContainerModel'; import { IHistoryState } from '../../App'; import { PhotographIcon, UploadIcon } from '@heroicons/react/outline'; import FloatingButton from '../FloatingButton/FloatingButton'; +import { Bar } from '../Bar/Bar'; interface IUIProps { current: IHistoryState @@ -58,7 +59,7 @@ export class UI extends React.PureComponent { /** * Toggle the elements */ - public ToggleHistory(): void { + public ToggleTimeline(): void { this.setState({ isHistoryOpen: !this.state.isHistoryOpen }); @@ -75,47 +76,34 @@ export class UI extends React.PureComponent { return ( <> + this.ToggleElementsSidebar()} + ToggleSidebar={() => this.ToggleSidebar()} + ToggleTimeline={() => this.ToggleTimeline()} + /> + this.ToggleSidebar()} buttonOnClick={(type: string) => this.props.AddContainer(type)} /> - - this.ToggleElementsSidebar()} onPropertyChange={this.props.OnPropertyChange} selectContainer={this.props.SelectContainer} /> - - this.ToggleHistory()} jumpTo={this.props.LoadState} /> - diff --git a/src/assets/fonts/RobotoFlex-Regular.ttf b/src/assets/fonts/RobotoFlex-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f857ae9c569eb2caa8700cab80905ae355e41ecb GIT binary patch literal 109860 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 Date: Mon, 8 Aug 2022 11:31:33 +0200 Subject: [PATCH 14/32] Fix svg viewer position --- src/Components/Bar/Bar.tsx | 2 ++ src/Components/SVG/SVG.tsx | 13 +++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Components/Bar/Bar.tsx b/src/Components/Bar/Bar.tsx index 672f80f..1738a3f 100644 --- a/src/Components/Bar/Bar.tsx +++ b/src/Components/Bar/Bar.tsx @@ -11,6 +11,8 @@ interface IBarProps { ToggleTimeline: () => void } +export const BAR_WIDTH = 64; // 4rem + export const Bar: React.FC = (props) => { return (

diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx index 9ebe3af..a2e0e20 100644 --- a/src/Components/SVG/SVG.tsx +++ b/src/Components/SVG/SVG.tsx @@ -3,6 +3,7 @@ import { UncontrolledReactSVGPanZoom } from 'react-svg-pan-zoom'; import { Container } from './Elements/Container'; import { ContainerModel } from '../../Interfaces/ContainerModel'; import { Selector } from './Elements/Selector'; +import { BAR_WIDTH } from '../Bar/Bar'; interface ISVGProps { width: number @@ -12,8 +13,8 @@ interface ISVGProps { } interface ISVGState { - viewerWidth: number, - viewerHeight: number, + viewerWidth: number + viewerHeight: number } export class SVG extends React.PureComponent { @@ -23,14 +24,14 @@ export class SVG extends React.PureComponent { constructor(props: ISVGProps) { super(props); this.state = { - viewerWidth: window.innerWidth, + viewerWidth: window.innerWidth - BAR_WIDTH, viewerHeight: window.innerHeight }; } resizeViewBox(): void { this.setState({ - viewerWidth: window.innerWidth, + viewerWidth: window.innerWidth - BAR_WIDTH, viewerHeight: window.innerHeight }); } @@ -60,7 +61,7 @@ export class SVG extends React.PureComponent { } return ( -
+
{ miniatureProps={{ position: 'left', background: '#616264', - width: window.innerWidth - 12, + width: window.innerWidth - 12 - BAR_WIDTH, height: 120 }} > From 6b8531d3ae0cd0680e63f154a82423a4f988e5f1 Mon Sep 17 00:00:00 2001 From: Siklos Date: Mon, 8 Aug 2022 11:33:46 +0200 Subject: [PATCH 15/32] Update Sidebar test with new UI --- src/Components/Sidebar/Sidebar.test.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Components/Sidebar/Sidebar.test.tsx b/src/Components/Sidebar/Sidebar.test.tsx index 8f283eb..0810741 100644 --- a/src/Components/Sidebar/Sidebar.test.tsx +++ b/src/Components/Sidebar/Sidebar.test.tsx @@ -5,29 +5,23 @@ import Sidebar from './Sidebar'; describe.concurrent('Sidebar', () => { it('Start default', () => { - const handleClick = vi.fn(); render( {}} /> ); const stuff = screen.queryByText(/stuff/i); - const close = screen.getByText(/close/i); expect(screen.getByText(/Components/i).classList.contains('left-0')).toBeDefined(); expect(stuff).toBeNull(); - fireEvent.click(close); - expect(handleClick).toHaveBeenCalledTimes(1); }); it('Start close', () => { render( {}} buttonOnClick={() => {}} />); @@ -49,7 +43,6 @@ describe.concurrent('Sidebar', () => { } ]} isOpen={true} - onClick={() => {}} buttonOnClick={handleButtonClick} />); const stuff = screen.getByText(/stuff/i); From 7b23283201321681a6d10a1323b5509d12b74dfb Mon Sep 17 00:00:00 2001 From: Siklos Date: Mon, 8 Aug 2022 13:32:39 +0200 Subject: [PATCH 16/32] Improve iteration in MakeIterator --- src/utils/itertools.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/itertools.ts b/src/utils/itertools.ts index 031b497..67cd40c 100644 --- a/src/utils/itertools.ts +++ b/src/utils/itertools.ts @@ -11,14 +11,14 @@ export function * MakeIterator(root: IContainerModel): Generator { + for (let i = container.children.length - 1; i >= 0; i--) { + const child = container.children[i]; if (visited.has(child)) { return; } visited.add(child); queue.push(child); - }); + } } } From 49a558589c94e5bf2bbbc02244fd63aab923497b Mon Sep 17 00:00:00 2001 From: Siklos Date: Mon, 8 Aug 2022 14:29:45 +0200 Subject: [PATCH 17/32] Implement deletion + context menu --- .../ElementsSidebar/ElementsSidebar.tsx | 81 ++++++++++++++++++- src/Components/Menu/Menu.tsx | 23 ++++++ src/Components/Menu/MenuItem.tsx | 16 ++++ src/Components/UI/UI.tsx | 2 + src/Editor.tsx | 44 ++++++++++ src/index.scss | 4 + 6 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/Components/Menu/Menu.tsx create mode 100644 src/Components/Menu/MenuItem.tsx diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index bbbcba5..621316b 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -3,6 +3,8 @@ import { motion } from 'framer-motion'; import { Properties } from '../Properties/Properties'; import { IContainerModel } from '../../Interfaces/ContainerModel'; import { getDepth, MakeIterator } from '../../utils/itertools'; +import { Menu } from '../Menu/Menu'; +import { MenuItem } from '../Menu/MenuItem'; interface IElementsSidebarProps { MainContainer: IContainerModel | null @@ -11,9 +13,77 @@ interface IElementsSidebarProps { SelectedContainer: IContainerModel | null onPropertyChange: (key: string, value: string) => void selectContainer: (container: IContainerModel) => void + deleteContainer: (containerid: string) => void +} + +interface Point { + x: number + y: number +} + +interface IElementsSidebarState { + isContextMenuOpen: boolean + contextMenuPosition: Point + onClickContainerId: string } export class ElementsSidebar extends React.PureComponent { + public state: IElementsSidebarState; + public elementRef: React.RefObject; + + constructor(props: IElementsSidebarProps) { + super(props); + this.state = { + isContextMenuOpen: false, + contextMenuPosition: { + x: 0, + y: 0 + }, + onClickContainerId: '' + }; + this.elementRef = React.createRef(); + } + + componentDidMount(): void { + this.elementRef.current?.addEventListener('contextmenu', (event) => this.handleRightClick(event)); + window.addEventListener('click', (event) => this.handleLeftClick(event)); + } + + componentWillUnmount(): void { + this.elementRef.current?.removeEventListener('contextmenu', (event) => this.handleRightClick(event)); + window.removeEventListener('click', (event) => this.handleLeftClick(event)); + } + + public handleRightClick(event: MouseEvent): void { + event.preventDefault(); + + if (!(event.target instanceof HTMLButtonElement)) { + this.setState({ + isContextMenuOpen: false, + onClickContainerId: '' + }); + return; + } + + const contextMenuPosition: Point = { x: event.pageX, y: event.pageY }; + this.setState({ + isContextMenuOpen: true, + contextMenuPosition, + onClickContainerId: event.target.id + }); + } + + public handleLeftClick(event: MouseEvent): void { + if (!this.state.isContextMenuOpen) { + return; + } + + this.setState({ + isContextMenuOpen: false, + onClickContainerId: '' + }); + } + public iterateChilds(handleContainer: (container: IContainerModel) => void): React.ReactNode { if (this.props.MainContainer == null) { return null; @@ -56,6 +126,7 @@ export class ElementsSidebar extends React.PureComponent `w-full elements-sidebar-row whitespace-pre text-left text-sm font-medium transition-all ${selectedClass}` } + id={key} key={key} onClick={() => this.props.selectContainer(container)}> { text } @@ -68,9 +139,17 @@ export class ElementsSidebar extends React.PureComponent
Elements
-
+
{ containerRows }
+ + this.props.deleteContainer(this.state.onClickContainerId)} /> +
); diff --git a/src/Components/Menu/Menu.tsx b/src/Components/Menu/Menu.tsx new file mode 100644 index 0000000..fde491f --- /dev/null +++ b/src/Components/Menu/Menu.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; + +interface IMenuProps { + className?: string + x: number + y: number + isOpen: boolean + children: React.ReactNode[] | React.ReactNode +} + +export const Menu: React.FC = (props) => { + const visible = props.isOpen ? 'visible opacity-1' : 'invisible opacity-0'; + return ( +
+ { props.children } +
+ ); +}; diff --git a/src/Components/Menu/MenuItem.tsx b/src/Components/Menu/MenuItem.tsx new file mode 100644 index 0000000..9edc8ac --- /dev/null +++ b/src/Components/Menu/MenuItem.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; + +interface IMenuItemProps { + className?: string + text: string + onClick: () => void +} + +export const MenuItem: React.FC = (props) => { + return ( + + ); +}; diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index 37420bd..46ae454 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -15,6 +15,7 @@ interface IUIProps { historyCurrentStep: number AvailableContainers: AvailableContainer[] SelectContainer: (container: ContainerModel) => void + DeleteContainer: (containerId: string) => void OnPropertyChange: (key: string, value: string) => void AddContainer: (type: string) => void SaveEditorAsJSON: () => void @@ -97,6 +98,7 @@ export class UI extends React.PureComponent { isHistoryOpen={this.state.isHistoryOpen} onPropertyChange={this.props.OnPropertyChange} selectContainer={this.props.SelectContainer} + deleteContainer={this.props.DeleteContainer} /> { }); } + public DeleteContainer(containerId: string): void { + const history = this.getCurrentHistory(); + const current = history[this.state.historyCurrentStep]; + + if (current.MainContainer === null) { + throw new Error('[DeleteContainer] Error: Tried to delete a container without a main container'); + } + + const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); + const container = findContainerById(mainContainerClone, containerId); + + if (container === undefined) { + throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`); + } + + if (container === mainContainerClone) { + // TODO: Implement alert + throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed !'); + } + + if (container === null || container === undefined) { + throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); + } + + if (container.parent != null) { + const index = container.parent.children.indexOf(container); + if (index > -1) { + container.parent.children.splice(index, 1); + } + } + + this.setState( + { + history: history.concat([{ + SelectedContainer: null, + SelectedContainerId: '', + MainContainer: mainContainerClone, + TypeCounters: Object.assign({}, current.TypeCounters) + }]), + historyCurrentStep: history.length + }); + } + /** * Handled the property change event in the properties form * @param key Property name @@ -286,6 +329,7 @@ class Editor extends React.Component { historyCurrentStep={this.state.historyCurrentStep} AvailableContainers={this.state.configuration.AvailableContainers} SelectContainer={(container) => this.SelectContainer(container)} + DeleteContainer={(containerId: string) => this.DeleteContainer(containerId)} OnPropertyChange={(key, value) => this.OnPropertyChange(key, value)} AddContainer={(type) => this.AddContainer(type)} SaveEditorAsJSON={() => this.SaveEditorAsJSON()} diff --git a/src/index.scss b/src/index.scss index 6b98e1e..1bc3361 100644 --- a/src/index.scss +++ b/src/index.scss @@ -44,4 +44,8 @@ text-xs font-bold transition-all duration-100 scale-0 origin-left; } + + .contextmenu-item { + @apply px-2 py-1 hover:bg-slate-300 text-left + } } \ No newline at end of file From 60247d6f45db3c01ffd8e149d723f7ad721e2dbf Mon Sep 17 00:00:00 2001 From: Eric Nguyen Date: Mon, 8 Aug 2022 14:14:12 +0000 Subject: [PATCH 18/32] Merged PR 10: Set up CI with Azure Pipelines --- .drone.yml | 4 ++-- azure-pipelines.yml | 25 +++++++++++++++++++++++++ package.json | 1 + 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 azure-pipelines.yml diff --git a/.drone.yml b/.drone.yml index 18d98d2..0a843af 100644 --- a/.drone.yml +++ b/.drone.yml @@ -8,7 +8,7 @@ steps: commands: - node ./test-server/node-http.js & - npm install - - npm test + - npm run test:nowatch --- kind: pipeline @@ -20,4 +20,4 @@ steps: commands: - node ./test-server/node-http.js & - npm install - - npm test \ No newline at end of file + - npm run test:nowatch \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..40364ea --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,25 @@ +# Node.js with React +# Build a Node.js project that uses React. +# Add steps that analyze code, save build artifacts, deploy, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript + +trigger: +- master +- dev +- dev* + +pool: + vmImage: ubuntu-latest + +steps: +- task: NodeTool@0 + inputs: + versionSpec: '16.x' + displayName: 'Install Node.js' + +- script: | + node --version + node ./test-server/node-http.js & + npm install + npm run test:nowatch + displayName: 'npm install and test' diff --git a/package.json b/package.json index b4e99f5..be45a0f 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "preview": "vite preview", "test": "vitest", "test:ui": "vitest --ui", + "test:nowatch": "vitest run", "coverage": "vitest run coverage" }, "dependencies": { From ed3dcf811258570bc8e14545edc7c70e67e9f658 Mon Sep 17 00:00:00 2001 From: Eric Nguyen Date: Mon, 8 Aug 2022 14:38:01 +0000 Subject: [PATCH 19/32] Merged PR 12: Added Node Latest to tests --- azure-pipelines.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 40364ea..5419a73 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -15,11 +15,23 @@ steps: - task: NodeTool@0 inputs: versionSpec: '16.x' - displayName: 'Install Node.js' + displayName: 'Install Node.js 16.x LTS' - script: | node --version node ./test-server/node-http.js & - npm install + npm ci npm run test:nowatch - displayName: 'npm install and test' + displayName: 'Test on Node.js 16.x LTS' + +- task: NodeTool@0 + inputs: + versionSpec: '>=18.7.0' + displayName: 'Install Node.js Latest' + +- script: | + node --version + node ./test-server/node-http.js & + npm ci + npm run test:nowatch + displayName: 'Test on Node.js 18.x Latest' \ No newline at end of file From 87369ee90d0bad4dc041789550a6ce3a7ce9c5a7 Mon Sep 17 00:00:00 2001 From: Siklos Date: Mon, 8 Aug 2022 16:45:22 +0200 Subject: [PATCH 20/32] azure-pipeline: Clean node process --- azure-pipelines.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5419a73..9a751fc 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -20,8 +20,10 @@ steps: - script: | node --version node ./test-server/node-http.js & + jobs npm ci npm run test:nowatch + killall node displayName: 'Test on Node.js 16.x LTS' - task: NodeTool@0 @@ -32,6 +34,8 @@ steps: - script: | node --version node ./test-server/node-http.js & + jobs npm ci npm run test:nowatch + killall node displayName: 'Test on Node.js 18.x Latest' \ No newline at end of file From 9dcfc5f2260dde6f0e75acca9c9c6e1b99e0ae5b Mon Sep 17 00:00:00 2001 From: Eric Nguyen Date: Mon, 8 Aug 2022 14:55:40 +0000 Subject: [PATCH 21/32] Merged PR 13: Fix azure-pipeline node kill We use killall to stop node from running but az pipe catch SIGTERM. Let's rather use SIGINT --- .drone.yml | 4 +++- azure-pipelines.yml | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.drone.yml b/.drone.yml index 0a843af..8d7a147 100644 --- a/.drone.yml +++ b/.drone.yml @@ -9,6 +9,7 @@ steps: - node ./test-server/node-http.js & - npm install - npm run test:nowatch + - kill -2 %1 2>/dev/null --- kind: pipeline @@ -20,4 +21,5 @@ steps: commands: - node ./test-server/node-http.js & - npm install - - npm run test:nowatch \ No newline at end of file + - npm run test:nowatch + - kill -2 %1 2>/dev/null \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9a751fc..28f4504 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -23,7 +23,7 @@ steps: jobs npm ci npm run test:nowatch - killall node + kill -2 %1 2>/dev/null displayName: 'Test on Node.js 16.x LTS' - task: NodeTool@0 @@ -37,5 +37,5 @@ steps: jobs npm ci npm run test:nowatch - killall node + kill -2 %1 2>/dev/null displayName: 'Test on Node.js 18.x Latest' \ No newline at end of file From 2c66ff197a6f16096588f9541bd51ad9ffa3a309 Mon Sep 17 00:00:00 2001 From: Siklos Date: Mon, 8 Aug 2022 16:57:40 +0200 Subject: [PATCH 22/32] Revert kill on drone.yml because it doesn't work --- .drone.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.drone.yml b/.drone.yml index 8d7a147..0a843af 100644 --- a/.drone.yml +++ b/.drone.yml @@ -9,7 +9,6 @@ steps: - node ./test-server/node-http.js & - npm install - npm run test:nowatch - - kill -2 %1 2>/dev/null --- kind: pipeline @@ -21,5 +20,4 @@ steps: commands: - node ./test-server/node-http.js & - npm install - - npm run test:nowatch - - kill -2 %1 2>/dev/null \ No newline at end of file + - npm run test:nowatch \ No newline at end of file From bcf84b1f399945c21543ee9107733adb3a29c297 Mon Sep 17 00:00:00 2001 From: Siklos Date: Mon, 8 Aug 2022 17:19:04 +0200 Subject: [PATCH 23/32] Fix ElementsSidebar.test.tsx --- src/Components/ElementsSidebar/ElementsSidebar.test.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index 2dd5080..c56e372 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -13,6 +13,7 @@ describe.concurrent('Elements sidebar', () => { SelectedContainer={null} onPropertyChange={() => {}} selectContainer={() => {}} + deleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -40,6 +41,7 @@ describe.concurrent('Elements sidebar', () => { SelectedContainer={null} onPropertyChange={() => {}} selectContainer={() => {}} + deleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -69,6 +71,7 @@ describe.concurrent('Elements sidebar', () => { SelectedContainer={MainContainer} onPropertyChange={() => {}} selectContainer={() => {}} + deleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -153,6 +156,7 @@ describe.concurrent('Elements sidebar', () => { SelectedContainer={MainContainer} onPropertyChange={() => {}} selectContainer={() => {}} + deleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -205,6 +209,7 @@ describe.concurrent('Elements sidebar', () => { SelectedContainer={SelectedContainer} onPropertyChange={() => {}} selectContainer={selectContainer} + deleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -226,6 +231,7 @@ describe.concurrent('Elements sidebar', () => { SelectedContainer={SelectedContainer} onPropertyChange={() => {}} selectContainer={selectContainer} + deleteContainer={() => {}} />); expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy(); From fd4cd082191e92122cc175b92b75bfdd74e01b83 Mon Sep 17 00:00:00 2001 From: Siklos Date: Mon, 8 Aug 2022 17:49:43 +0200 Subject: [PATCH 24/32] Fix ElementsSidebar.test.tsx --- src/Components/ElementsSidebar/ElementsSidebar.test.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index 2dd5080..c56e372 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -13,6 +13,7 @@ describe.concurrent('Elements sidebar', () => { SelectedContainer={null} onPropertyChange={() => {}} selectContainer={() => {}} + deleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -40,6 +41,7 @@ describe.concurrent('Elements sidebar', () => { SelectedContainer={null} onPropertyChange={() => {}} selectContainer={() => {}} + deleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -69,6 +71,7 @@ describe.concurrent('Elements sidebar', () => { SelectedContainer={MainContainer} onPropertyChange={() => {}} selectContainer={() => {}} + deleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -153,6 +156,7 @@ describe.concurrent('Elements sidebar', () => { SelectedContainer={MainContainer} onPropertyChange={() => {}} selectContainer={() => {}} + deleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -205,6 +209,7 @@ describe.concurrent('Elements sidebar', () => { SelectedContainer={SelectedContainer} onPropertyChange={() => {}} selectContainer={selectContainer} + deleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -226,6 +231,7 @@ describe.concurrent('Elements sidebar', () => { SelectedContainer={SelectedContainer} onPropertyChange={() => {}} selectContainer={selectContainer} + deleteContainer={() => {}} />); expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy(); From 1613617c3f9d4482aeb478e2db5e080ca52bcd9a Mon Sep 17 00:00:00 2001 From: Siklos Date: Mon, 8 Aug 2022 17:52:11 +0200 Subject: [PATCH 25/32] Added missing peer-dependency --- package.json | 1 + pnpm-lock.yaml | 3607 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 3608 insertions(+) create mode 100644 pnpm-lock.yaml diff --git a/package.json b/package.json index be45a0f..148f302 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "react-svg-pan-zoom": "^3.11.0" }, "devDependencies": { + "@testing-library/dom": "^8.16.1", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^14.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..f9bce2d --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3607 @@ +lockfileVersion: 5.4 + +specifiers: + '@heroicons/react': ^1.0.6 + '@testing-library/dom': ^8.16.1 + '@testing-library/jest-dom': ^5.16.4 + '@testing-library/react': ^13.3.0 + '@testing-library/user-event': ^14.4.1 + '@types/react': ^18.0.15 + '@types/react-dom': ^18.0.6 + '@types/react-svg-pan-zoom': ^3.3.5 + '@typescript-eslint/eslint-plugin': ^5.31.0 + '@typescript-eslint/parser': ^5.31.0 + '@vitejs/plugin-react': ^2.0.0 + '@vitest/ui': ^0.20.3 + autoprefixer: ^10.4.8 + eslint: ^8.20.0 + eslint-config-standard: ^17.0.0 + eslint-config-standard-with-typescript: ^22.0.0 + eslint-plugin-import: ^2.26.0 + eslint-plugin-n: ^15.2.4 + eslint-plugin-promise: ^6.0.0 + eslint-plugin-react: ^7.30.1 + framer-motion: ^6.5.1 + jsdom: ^20.0.0 + postcss: ^8.4.14 + react: ^18.2.0 + react-dom: ^18.2.0 + react-svg-pan-zoom: ^3.11.0 + sass: ^1.54.0 + tailwindcss: ^3.1.7 + typescript: ^4.6.4 + vite: ^3.0.0 + vitest: ^0.20.3 + +dependencies: + '@heroicons/react': 1.0.6_react@18.2.0 + framer-motion: 6.5.1_biqbaboplfbrettd7655fr4n2y + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-svg-pan-zoom: 3.11.0_react@18.2.0 + +devDependencies: + '@testing-library/dom': 8.16.1 + '@testing-library/jest-dom': 5.16.5 + '@testing-library/react': 13.3.0_biqbaboplfbrettd7655fr4n2y + '@testing-library/user-event': 14.4.2_znfriv3ismgf3ybh2woqwlpfea + '@types/react': 18.0.17 + '@types/react-dom': 18.0.6 + '@types/react-svg-pan-zoom': 3.3.5 + '@typescript-eslint/eslint-plugin': 5.32.0_iosr3hrei2tubxveewluhu5lhy + '@typescript-eslint/parser': 5.32.0_qugx7qdu5zevzvxaiqyxfiwquq + '@vitejs/plugin-react': 2.0.0_vite@3.0.4 + '@vitest/ui': 0.20.3 + autoprefixer: 10.4.8_postcss@8.4.16 + eslint: 8.21.0 + eslint-config-standard: 17.0.0_dfwa53o44x4e5xhsfv5mvfhk5a + eslint-config-standard-with-typescript: 22.0.0_mfupvx5msz6are6ggwiepter3m + eslint-plugin-import: 2.26.0_wuikv5nqgdfyng42xxm7lklfmi + eslint-plugin-n: 15.2.4_eslint@8.21.0 + eslint-plugin-promise: 6.0.0_eslint@8.21.0 + eslint-plugin-react: 7.30.1_eslint@8.21.0 + jsdom: 20.0.0 + postcss: 8.4.16 + sass: 1.54.3 + tailwindcss: 3.1.8 + typescript: 4.7.4 + vite: 3.0.4_sass@1.54.3 + vitest: 0.20.3_hymhw3vkyr5yfvzfskw3x5v26q + +packages: + + /@adobe/css-tools/4.0.1: + resolution: {integrity: sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==} + dev: true + + /@ampproject/remapping/2.2.0: + resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.1.1 + '@jridgewell/trace-mapping': 0.3.14 + dev: true + + /@babel/code-frame/7.18.6: + resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.18.6 + dev: true + + /@babel/compat-data/7.18.8: + resolution: {integrity: sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core/7.18.10: + resolution: {integrity: sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.0 + '@babel/code-frame': 7.18.6 + '@babel/generator': 7.18.12 + '@babel/helper-compilation-targets': 7.18.9_@babel+core@7.18.10 + '@babel/helper-module-transforms': 7.18.9 + '@babel/helpers': 7.18.9 + '@babel/parser': 7.18.11 + '@babel/template': 7.18.10 + '@babel/traverse': 7.18.11 + '@babel/types': 7.18.10 + convert-source-map: 1.8.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.1 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator/7.18.12: + resolution: {integrity: sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.18.10 + '@jridgewell/gen-mapping': 0.3.2 + jsesc: 2.5.2 + dev: true + + /@babel/helper-annotate-as-pure/7.18.6: + resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.18.10 + dev: true + + /@babel/helper-compilation-targets/7.18.9_@babel+core@7.18.10: + resolution: {integrity: sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/compat-data': 7.18.8 + '@babel/core': 7.18.10 + '@babel/helper-validator-option': 7.18.6 + browserslist: 4.21.3 + semver: 6.3.0 + dev: true + + /@babel/helper-environment-visitor/7.18.9: + resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-function-name/7.18.9: + resolution: {integrity: sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.18.10 + '@babel/types': 7.18.10 + dev: true + + /@babel/helper-hoist-variables/7.18.6: + resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.18.10 + dev: true + + /@babel/helper-module-imports/7.18.6: + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.18.10 + dev: true + + /@babel/helper-module-transforms/7.18.9: + resolution: {integrity: sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-simple-access': 7.18.6 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/helper-validator-identifier': 7.18.6 + '@babel/template': 7.18.10 + '@babel/traverse': 7.18.11 + '@babel/types': 7.18.10 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-plugin-utils/7.18.9: + resolution: {integrity: sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-simple-access/7.18.6: + resolution: {integrity: sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.18.10 + dev: true + + /@babel/helper-split-export-declaration/7.18.6: + resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.18.10 + dev: true + + /@babel/helper-string-parser/7.18.10: + resolution: {integrity: sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier/7.18.6: + resolution: {integrity: sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option/7.18.6: + resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helpers/7.18.9: + resolution: {integrity: sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.18.10 + '@babel/traverse': 7.18.11 + '@babel/types': 7.18.10 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/highlight/7.18.6: + resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.18.6 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@babel/parser/7.18.11: + resolution: {integrity: sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.18.10 + dev: true + + /@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.18.10: + resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.10 + '@babel/helper-plugin-utils': 7.18.9 + dev: true + + /@babel/plugin-transform-react-jsx-development/7.18.6_@babel+core@7.18.10: + resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.10 + '@babel/plugin-transform-react-jsx': 7.18.10_@babel+core@7.18.10 + dev: true + + /@babel/plugin-transform-react-jsx-self/7.18.6_@babel+core@7.18.10: + resolution: {integrity: sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.10 + '@babel/helper-plugin-utils': 7.18.9 + dev: true + + /@babel/plugin-transform-react-jsx-source/7.18.6_@babel+core@7.18.10: + resolution: {integrity: sha512-utZmlASneDfdaMh0m/WausbjUjEdGrQJz0vFK93d7wD3xf5wBtX219+q6IlCNZeguIcxS2f/CvLZrlLSvSHQXw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.10 + '@babel/helper-plugin-utils': 7.18.9 + dev: true + + /@babel/plugin-transform-react-jsx/7.18.10_@babel+core@7.18.10: + resolution: {integrity: sha512-gCy7Iikrpu3IZjYZolFE4M1Sm+nrh1/6za2Ewj77Z+XirT4TsbJcvOFOyF+fRPwU6AKKK136CZxx6L8AbSFG6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.10 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-plugin-utils': 7.18.9 + '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.18.10 + '@babel/types': 7.18.10 + dev: true + + /@babel/runtime/7.18.9: + resolution: {integrity: sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.9 + dev: true + + /@babel/template/7.18.10: + resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/parser': 7.18.11 + '@babel/types': 7.18.10 + dev: true + + /@babel/traverse/7.18.11: + resolution: {integrity: sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/generator': 7.18.12 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-function-name': 7.18.9 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/parser': 7.18.11 + '@babel/types': 7.18.10 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types/7.18.10: + resolution: {integrity: sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.18.10 + '@babel/helper-validator-identifier': 7.18.6 + to-fast-properties: 2.0.0 + dev: true + + /@emotion/is-prop-valid/0.8.8: + resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} + requiresBuild: true + dependencies: + '@emotion/memoize': 0.7.4 + dev: false + optional: true + + /@emotion/memoize/0.7.4: + resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} + dev: false + optional: true + + /@esbuild/linux-loong64/0.14.54: + resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@eslint/eslintrc/1.3.0: + resolution: {integrity: sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.3.3 + globals: 13.17.0 + ignore: 5.2.0 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@heroicons/react/1.0.6_react@18.2.0: + resolution: {integrity: sha512-JJCXydOFWMDpCP4q13iEplA503MQO3xLoZiKum+955ZCtHINWnx26CUxVxxFQu/uLb4LW3ge15ZpzIkXKkJ8oQ==} + peerDependencies: + react: '>= 16' + dependencies: + react: 18.2.0 + dev: false + + /@humanwhocodes/config-array/0.10.4: + resolution: {integrity: sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/gitignore-to-minimatch/1.0.2: + resolution: {integrity: sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==} + dev: true + + /@humanwhocodes/object-schema/1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: true + + /@jest/schemas/28.1.3: + resolution: {integrity: sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@sinclair/typebox': 0.24.27 + dev: true + + /@jridgewell/gen-mapping/0.1.1: + resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + + /@jridgewell/gen-mapping/0.3.2: + resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/trace-mapping': 0.3.14 + dev: true + + /@jridgewell/resolve-uri/3.1.0: + resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array/1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec/1.4.14: + resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + dev: true + + /@jridgewell/trace-mapping/0.3.14: + resolution: {integrity: sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + + /@motionone/animation/10.13.2: + resolution: {integrity: sha512-YGWss58IR2X4lOjW89rv1Q+/Nq/QhfltaggI7i8sZTpKC1yUvM+XYDdvlRpWc6dk8LviMBrddBJAlLdbaqeRmw==} + dependencies: + '@motionone/easing': 10.13.2 + '@motionone/types': 10.13.2 + '@motionone/utils': 10.13.2 + tslib: 2.4.0 + dev: false + + /@motionone/dom/10.12.0: + resolution: {integrity: sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==} + dependencies: + '@motionone/animation': 10.13.2 + '@motionone/generators': 10.13.2 + '@motionone/types': 10.13.2 + '@motionone/utils': 10.13.2 + hey-listen: 1.0.8 + tslib: 2.4.0 + dev: false + + /@motionone/easing/10.13.2: + resolution: {integrity: sha512-3HqctS5NyDfDQ+8+cZqc3Pu7I6amFCt9zDUjcozHyFXHh4PKYHK4+GJDFjJIS8bCAF2BrJmpmduDQ2V7lFEYeQ==} + dependencies: + '@motionone/utils': 10.13.2 + tslib: 2.4.0 + dev: false + + /@motionone/generators/10.13.2: + resolution: {integrity: sha512-QMoXV1MXEEhR6D3dct/RMMS1FwJlAsW+kMPbFGzBA4NbweblgeYQCft9DcDAVpV9wIwD6qvlBG9u99sOXLfHiA==} + dependencies: + '@motionone/types': 10.13.2 + '@motionone/utils': 10.13.2 + tslib: 2.4.0 + dev: false + + /@motionone/types/10.13.2: + resolution: {integrity: sha512-yYV4q5v5F0iADhab4wHfqaRJnM/eVtQLjUPhyEcS72aUz/xyOzi09GzD/Gu+K506BDfqn5eULIilUI77QNaqhw==} + dev: false + + /@motionone/utils/10.13.2: + resolution: {integrity: sha512-6Lw5bDA/w7lrPmT/jYWQ76lkHlHs9fl2NZpJ22cVy1kKDdEH+Cl1U6hMTpdphO6VQktQ6v2APngag91WBKLqlA==} + dependencies: + '@motionone/types': 10.13.2 + hey-listen: 1.0.8 + tslib: 2.4.0 + dev: false + + /@nodelib/fs.scandir/2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat/2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk/1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.13.0 + dev: true + + /@polka/url/1.0.0-next.21: + resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} + dev: true + + /@sinclair/typebox/0.24.27: + resolution: {integrity: sha512-K7C7IlQ3zLePEZleUN21ceBA2aLcMnLHTLph8QWk1JK37L90obdpY+QGY8bXMKxf1ht1Z0MNewvXxWv0oGDYFg==} + dev: true + + /@testing-library/dom/8.16.1: + resolution: {integrity: sha512-XEV2mBxgv6DKjL3+U3WEUzBgT2CjYksoXGlLrrJXYP8OvRfGkBonvelkorazpFlp8tkEecO06r43vN4DIEyegQ==} + engines: {node: '>=12'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/runtime': 7.18.9 + '@types/aria-query': 4.2.2 + aria-query: 5.0.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.14 + lz-string: 1.4.4 + pretty-format: 27.5.1 + dev: true + + /@testing-library/jest-dom/5.16.5: + resolution: {integrity: sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==} + engines: {node: '>=8', npm: '>=6', yarn: '>=1'} + dependencies: + '@adobe/css-tools': 4.0.1 + '@babel/runtime': 7.18.9 + '@types/testing-library__jest-dom': 5.14.5 + aria-query: 5.0.0 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.5.14 + lodash: 4.17.21 + redent: 3.0.0 + dev: true + + /@testing-library/react/13.3.0_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-DB79aA426+deFgGSjnf5grczDPiL4taK3hFaa+M5q7q20Kcve9eQottOG5kZ74KEr55v0tU2CQormSSDK87zYQ==} + engines: {node: '>=12'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@babel/runtime': 7.18.9 + '@testing-library/dom': 8.16.1 + '@types/react-dom': 18.0.6 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: true + + /@testing-library/user-event/14.4.2_znfriv3ismgf3ybh2woqwlpfea: + resolution: {integrity: sha512-1gVTWtueNimveOjcm2ApFCnCTeky7WqY3EX31/GRKLWyCd+HfH+Gd2l1J8go9FpDNe+0Mx8X4zbQHTg0WWNJwg==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + dependencies: + '@testing-library/dom': 8.16.1 + dev: true + + /@tootallnate/once/2.0.0: + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + dev: true + + /@types/aria-query/4.2.2: + resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==} + dev: true + + /@types/chai-subset/1.3.3: + resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} + dependencies: + '@types/chai': 4.3.3 + dev: true + + /@types/chai/4.3.3: + resolution: {integrity: sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==} + dev: true + + /@types/jest/28.1.6: + resolution: {integrity: sha512-0RbGAFMfcBJKOmqRazM8L98uokwuwD5F8rHrv/ZMbrZBwVOWZUyPG6VFNscjYr/vjM3Vu4fRrCPbOs42AfemaQ==} + dependencies: + jest-matcher-utils: 28.1.3 + pretty-format: 28.1.3 + dev: true + + /@types/json-schema/7.0.11: + resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} + dev: true + + /@types/json5/0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: true + + /@types/node/18.6.4: + resolution: {integrity: sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg==} + dev: true + + /@types/prop-types/15.7.5: + resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + dev: true + + /@types/react-dom/18.0.6: + resolution: {integrity: sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==} + dependencies: + '@types/react': 18.0.17 + dev: true + + /@types/react-svg-pan-zoom/3.3.5: + resolution: {integrity: sha512-W8GRFCDy7raSDr5OXGjSyvX5KmdWlIQfv0NLa1jfAYVUO4ClVbgorWeAAom7nY3Pl+4h9blXE1Bnu2CW1iMEvQ==} + dependencies: + '@types/react': 18.0.17 + dev: true + + /@types/react/18.0.17: + resolution: {integrity: sha512-38ETy4tL+rn4uQQi7mB81G7V1g0u2ryquNmsVIOKUAEIDK+3CUjZ6rSRpdvS99dNBnkLFL83qfmtLacGOTIhwQ==} + dependencies: + '@types/prop-types': 15.7.5 + '@types/scheduler': 0.16.2 + csstype: 3.1.0 + dev: true + + /@types/scheduler/0.16.2: + resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} + dev: true + + /@types/testing-library__jest-dom/5.14.5: + resolution: {integrity: sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==} + dependencies: + '@types/jest': 28.1.6 + dev: true + + /@typescript-eslint/eslint-plugin/5.32.0_iosr3hrei2tubxveewluhu5lhy: + resolution: {integrity: sha512-CHLuz5Uz7bHP2WgVlvoZGhf0BvFakBJKAD/43Ty0emn4wXWv5k01ND0C0fHcl/Im8Td2y/7h44E9pca9qAu2ew==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/parser': 5.32.0_qugx7qdu5zevzvxaiqyxfiwquq + '@typescript-eslint/scope-manager': 5.32.0 + '@typescript-eslint/type-utils': 5.32.0_qugx7qdu5zevzvxaiqyxfiwquq + '@typescript-eslint/utils': 5.32.0_qugx7qdu5zevzvxaiqyxfiwquq + debug: 4.3.4 + eslint: 8.21.0 + functional-red-black-tree: 1.0.1 + ignore: 5.2.0 + regexpp: 3.2.0 + semver: 7.3.7 + tsutils: 3.21.0_typescript@4.7.4 + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser/5.32.0_qugx7qdu5zevzvxaiqyxfiwquq: + resolution: {integrity: sha512-IxRtsehdGV9GFQ35IGm5oKKR2OGcazUoiNBxhRV160iF9FoyuXxjY+rIqs1gfnd+4eL98OjeGnMpE7RF/NBb3A==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.32.0 + '@typescript-eslint/types': 5.32.0 + '@typescript-eslint/typescript-estree': 5.32.0_typescript@4.7.4 + debug: 4.3.4 + eslint: 8.21.0 + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager/5.32.0: + resolution: {integrity: sha512-KyAE+tUON0D7tNz92p1uetRqVJiiAkeluvwvZOqBmW9z2XApmk5WSMV9FrzOroAcVxJZB3GfUwVKr98Dr/OjOg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.32.0 + '@typescript-eslint/visitor-keys': 5.32.0 + dev: true + + /@typescript-eslint/type-utils/5.32.0_qugx7qdu5zevzvxaiqyxfiwquq: + resolution: {integrity: sha512-0gSsIhFDduBz3QcHJIp3qRCvVYbqzHg8D6bHFsDMrm0rURYDj+skBK2zmYebdCp+4nrd9VWd13egvhYFJj/wZg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/utils': 5.32.0_qugx7qdu5zevzvxaiqyxfiwquq + debug: 4.3.4 + eslint: 8.21.0 + tsutils: 3.21.0_typescript@4.7.4 + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types/5.32.0: + resolution: {integrity: sha512-EBUKs68DOcT/EjGfzywp+f8wG9Zw6gj6BjWu7KV/IYllqKJFPlZlLSYw/PTvVyiRw50t6wVbgv4p9uE2h6sZrQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@typescript-eslint/typescript-estree/5.32.0_typescript@4.7.4: + resolution: {integrity: sha512-ZVAUkvPk3ITGtCLU5J4atCw9RTxK+SRc6hXqLtllC2sGSeMFWN+YwbiJR9CFrSFJ3w4SJfcWtDwNb/DmUIHdhg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.32.0 + '@typescript-eslint/visitor-keys': 5.32.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.3.7 + tsutils: 3.21.0_typescript@4.7.4 + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils/5.32.0_qugx7qdu5zevzvxaiqyxfiwquq: + resolution: {integrity: sha512-W7lYIAI5Zlc5K082dGR27Fczjb3Q57ECcXefKU/f0ajM5ToM0P+N9NmJWip8GmGu/g6QISNT+K6KYB+iSHjXCQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@types/json-schema': 7.0.11 + '@typescript-eslint/scope-manager': 5.32.0 + '@typescript-eslint/types': 5.32.0 + '@typescript-eslint/typescript-estree': 5.32.0_typescript@4.7.4 + eslint: 8.21.0 + eslint-scope: 5.1.1 + eslint-utils: 3.0.0_eslint@8.21.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys/5.32.0: + resolution: {integrity: sha512-S54xOHZgfThiZ38/ZGTgB2rqx51CMJ5MCfVT2IplK4Q7hgzGfe0nLzLCcenDnc/cSjP568hdeKfeDcBgqNHD/g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.32.0 + eslint-visitor-keys: 3.3.0 + dev: true + + /@vitejs/plugin-react/2.0.0_vite@3.0.4: + resolution: {integrity: sha512-zHkRR+X4zqEPNBbKV2FvWSxK7Q6crjMBVIAYroSU8Nbb4M3E5x4qOiLoqJBHtXgr27kfednXjkwr3lr8jS6Wrw==} + engines: {node: '>=14.18.0'} + peerDependencies: + vite: ^3.0.0 + dependencies: + '@babel/core': 7.18.10 + '@babel/plugin-transform-react-jsx': 7.18.10_@babel+core@7.18.10 + '@babel/plugin-transform-react-jsx-development': 7.18.6_@babel+core@7.18.10 + '@babel/plugin-transform-react-jsx-self': 7.18.6_@babel+core@7.18.10 + '@babel/plugin-transform-react-jsx-source': 7.18.6_@babel+core@7.18.10 + magic-string: 0.26.2 + react-refresh: 0.14.0 + vite: 3.0.4_sass@1.54.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@vitest/ui/0.20.3: + resolution: {integrity: sha512-Rlg+y3PtE5IcGPVmViF/BXM7euY7LG0yjfIvXKlF0L3OnNSVS8+esgLlAhaYftSJXtcunqa/cYXiQ+qFVTaBGw==} + dependencies: + sirv: 2.0.2 + dev: true + + /abab/2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + dev: true + + /acorn-globals/6.0.0: + resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} + dependencies: + acorn: 7.4.1 + acorn-walk: 7.2.0 + dev: true + + /acorn-jsx/5.3.2_acorn@8.8.0: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.8.0 + dev: true + + /acorn-node/1.8.2: + resolution: {integrity: sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==} + dependencies: + acorn: 7.4.1 + acorn-walk: 7.2.0 + xtend: 4.0.2 + dev: true + + /acorn-walk/7.2.0: + resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn/7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /acorn/8.8.0: + resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /agent-base/6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /ajv/6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ansi-regex/5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles/3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles/4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /ansi-styles/5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + + /anymatch/3.1.2: + resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /arg/5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true + + /argparse/2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /aria-query/5.0.0: + resolution: {integrity: sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==} + engines: {node: '>=6.0'} + dev: true + + /array-includes/3.1.5: + resolution: {integrity: sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.1 + get-intrinsic: 1.1.2 + is-string: 1.0.7 + dev: true + + /array-union/2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /array.prototype.flat/1.3.0: + resolution: {integrity: sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.1 + es-shim-unscopables: 1.0.0 + dev: true + + /array.prototype.flatmap/1.3.0: + resolution: {integrity: sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.1 + es-shim-unscopables: 1.0.0 + dev: true + + /assertion-error/1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + + /asynckit/0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: true + + /autoprefixer/10.4.8_postcss@8.4.16: + resolution: {integrity: sha512-75Jr6Q/XpTqEf6D2ltS5uMewJIx5irCU1oBYJrWjFenq/m12WRRrz6g15L1EIoYvPLXTbEry7rDOwrcYNj77xw==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.21.3 + caniuse-lite: 1.0.30001374 + fraction.js: 4.2.0 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.16 + postcss-value-parser: 4.2.0 + dev: true + + /balanced-match/1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /binary-extensions/2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /brace-expansion/1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /braces/3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /browser-process-hrtime/1.0.0: + resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} + dev: true + + /browserslist/4.21.3: + resolution: {integrity: sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001374 + electron-to-chromium: 1.4.211 + node-releases: 2.0.6 + update-browserslist-db: 1.0.5_browserslist@4.21.3 + dev: true + + /builtins/5.0.1: + resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} + dependencies: + semver: 7.3.7 + dev: true + + /call-bind/1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.1.2 + dev: true + + /callsites/3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /camelcase-css/2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: true + + /caniuse-lite/1.0.30001374: + resolution: {integrity: sha512-mWvzatRx3w+j5wx/mpFN5v5twlPrabG8NqX2c6e45LCpymdoGqNvRkRutFUqpRTXKFQFNQJasvK0YT7suW6/Hw==} + dev: true + + /chai/4.3.6: + resolution: {integrity: sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.2 + deep-eql: 3.0.1 + get-func-name: 2.0.0 + loupe: 2.3.4 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + + /chalk/2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk/3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chalk/4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /check-error/1.0.2: + resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + dev: true + + /chokidar/3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.2 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /color-convert/1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert/2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name/1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name/1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /combined-stream/1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: true + + /concat-map/0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /convert-source-map/1.8.0: + resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==} + dependencies: + safe-buffer: 5.1.2 + dev: true + + /cross-spawn/7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /css.escape/1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + dev: true + + /cssesc/3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /cssom/0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + dev: true + + /cssom/0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + dev: true + + /cssstyle/2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + dependencies: + cssom: 0.3.8 + dev: true + + /csstype/3.1.0: + resolution: {integrity: sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==} + dev: true + + /data-urls/3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + dev: true + + /debug/2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: true + + /debug/3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + + /debug/4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /decimal.js/10.3.1: + resolution: {integrity: sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==} + dev: true + + /deep-eql/3.0.1: + resolution: {integrity: sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==} + engines: {node: '>=0.12'} + dependencies: + type-detect: 4.0.8 + dev: true + + /deep-is/0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /define-properties/1.1.4: + resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==} + engines: {node: '>= 0.4'} + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: true + + /defined/1.0.0: + resolution: {integrity: sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==} + dev: true + + /delayed-stream/1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: true + + /detective/5.2.1: + resolution: {integrity: sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==} + engines: {node: '>=0.8.0'} + hasBin: true + dependencies: + acorn-node: 1.8.2 + defined: 1.0.0 + minimist: 1.2.6 + dev: true + + /didyoumean/1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + + /diff-sequences/28.1.1: + resolution: {integrity: sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dev: true + + /dir-glob/3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /dlv/1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + + /doctrine/2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /doctrine/3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /dom-accessibility-api/0.5.14: + resolution: {integrity: sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==} + dev: true + + /domexception/4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + dependencies: + webidl-conversions: 7.0.0 + dev: true + + /electron-to-chromium/1.4.211: + resolution: {integrity: sha512-BZSbMpyFQU0KBJ1JG26XGeFI3i4op+qOYGxftmZXFZoHkhLgsSv4DHDJfl8ogII3hIuzGt51PaZ195OVu0yJ9A==} + dev: true + + /entities/4.3.1: + resolution: {integrity: sha512-o4q/dYJlmyjP2zfnaWDUC6A3BQFmVTX+tZPezK7k0GLSU9QYCauscf5Y+qcEPzKL+EixVouYDgLQK5H9GrLpkg==} + engines: {node: '>=0.12'} + dev: true + + /es-abstract/1.20.1: + resolution: {integrity: sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + es-to-primitive: 1.2.1 + function-bind: 1.1.1 + function.prototype.name: 1.1.5 + get-intrinsic: 1.1.2 + get-symbol-description: 1.0.0 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-symbols: 1.0.3 + internal-slot: 1.0.3 + is-callable: 1.2.4 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-weakref: 1.0.2 + object-inspect: 1.12.2 + object-keys: 1.1.1 + object.assign: 4.1.3 + regexp.prototype.flags: 1.4.3 + string.prototype.trimend: 1.0.5 + string.prototype.trimstart: 1.0.5 + unbox-primitive: 1.0.2 + dev: true + + /es-shim-unscopables/1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.3 + dev: true + + /es-to-primitive/1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.4 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /esbuild-android-64/0.14.54: + resolution: {integrity: sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-android-arm64/0.14.54: + resolution: {integrity: sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-64/0.14.54: + resolution: {integrity: sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-arm64/0.14.54: + resolution: {integrity: sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-64/0.14.54: + resolution: {integrity: sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-arm64/0.14.54: + resolution: {integrity: sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-32/0.14.54: + resolution: {integrity: sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-64/0.14.54: + resolution: {integrity: sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm/0.14.54: + resolution: {integrity: sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm64/0.14.54: + resolution: {integrity: sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-mips64le/0.14.54: + resolution: {integrity: sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-ppc64le/0.14.54: + resolution: {integrity: sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-riscv64/0.14.54: + resolution: {integrity: sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-s390x/0.14.54: + resolution: {integrity: sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-netbsd-64/0.14.54: + resolution: {integrity: sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-openbsd-64/0.14.54: + resolution: {integrity: sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-sunos-64/0.14.54: + resolution: {integrity: sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-32/0.14.54: + resolution: {integrity: sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-64/0.14.54: + resolution: {integrity: sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-arm64/0.14.54: + resolution: {integrity: sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild/0.14.54: + resolution: {integrity: sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/linux-loong64': 0.14.54 + esbuild-android-64: 0.14.54 + esbuild-android-arm64: 0.14.54 + esbuild-darwin-64: 0.14.54 + esbuild-darwin-arm64: 0.14.54 + esbuild-freebsd-64: 0.14.54 + esbuild-freebsd-arm64: 0.14.54 + esbuild-linux-32: 0.14.54 + esbuild-linux-64: 0.14.54 + esbuild-linux-arm: 0.14.54 + esbuild-linux-arm64: 0.14.54 + esbuild-linux-mips64le: 0.14.54 + esbuild-linux-ppc64le: 0.14.54 + esbuild-linux-riscv64: 0.14.54 + esbuild-linux-s390x: 0.14.54 + esbuild-netbsd-64: 0.14.54 + esbuild-openbsd-64: 0.14.54 + esbuild-sunos-64: 0.14.54 + esbuild-windows-32: 0.14.54 + esbuild-windows-64: 0.14.54 + esbuild-windows-arm64: 0.14.54 + dev: true + + /escalade/3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp/1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /escape-string-regexp/4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /escodegen/2.0.0: + resolution: {integrity: sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.6.1 + dev: true + + /eslint-config-standard-with-typescript/22.0.0_mfupvx5msz6are6ggwiepter3m: + resolution: {integrity: sha512-VA36U7UlFpwULvkdnh6MQj5GAV2Q+tT68ALLAwJP0ZuNXU2m0wX07uxX4qyLRdHgSzH4QJ73CveKBuSOYvh7vQ==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.0.0 + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: ^15.0.0 + eslint-plugin-promise: ^6.0.0 + typescript: '*' + dependencies: + '@typescript-eslint/eslint-plugin': 5.32.0_iosr3hrei2tubxveewluhu5lhy + '@typescript-eslint/parser': 5.32.0_qugx7qdu5zevzvxaiqyxfiwquq + eslint: 8.21.0 + eslint-config-standard: 17.0.0_dfwa53o44x4e5xhsfv5mvfhk5a + eslint-plugin-import: 2.26.0_wuikv5nqgdfyng42xxm7lklfmi + eslint-plugin-n: 15.2.4_eslint@8.21.0 + eslint-plugin-promise: 6.0.0_eslint@8.21.0 + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-config-standard/17.0.0_dfwa53o44x4e5xhsfv5mvfhk5a: + resolution: {integrity: sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==} + peerDependencies: + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: ^15.0.0 + eslint-plugin-promise: ^6.0.0 + dependencies: + eslint: 8.21.0 + eslint-plugin-import: 2.26.0_wuikv5nqgdfyng42xxm7lklfmi + eslint-plugin-n: 15.2.4_eslint@8.21.0 + eslint-plugin-promise: 6.0.0_eslint@8.21.0 + dev: true + + /eslint-import-resolver-node/0.3.6: + resolution: {integrity: sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==} + dependencies: + debug: 3.2.7 + resolve: 1.22.1 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-module-utils/2.7.3_gjpiwexkhexdr4bbgrtzf23bg4: + resolution: {integrity: sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 5.32.0_qugx7qdu5zevzvxaiqyxfiwquq + debug: 3.2.7 + eslint-import-resolver-node: 0.3.6 + find-up: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-es/4.1.0_eslint@8.21.0: + resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=4.19.1' + dependencies: + eslint: 8.21.0 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + dev: true + + /eslint-plugin-import/2.26.0_wuikv5nqgdfyng42xxm7lklfmi: + resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 5.32.0_qugx7qdu5zevzvxaiqyxfiwquq + array-includes: 3.1.5 + array.prototype.flat: 1.3.0 + debug: 2.6.9 + doctrine: 2.1.0 + eslint: 8.21.0 + eslint-import-resolver-node: 0.3.6 + eslint-module-utils: 2.7.3_gjpiwexkhexdr4bbgrtzf23bg4 + has: 1.0.3 + is-core-module: 2.10.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.values: 1.1.5 + resolve: 1.22.1 + tsconfig-paths: 3.14.1 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-plugin-n/15.2.4_eslint@8.21.0: + resolution: {integrity: sha512-tjnVMv2fiXYMnuiIFI8QMtyUFI42SckEEWvi8h68SWGWshfqO6SSCASy24dGMGAiy7NUk6DZt90DM0iNUsmQ5w==} + engines: {node: '>=12.22.0'} + peerDependencies: + eslint: '>=7.0.0' + dependencies: + builtins: 5.0.1 + eslint: 8.21.0 + eslint-plugin-es: 4.1.0_eslint@8.21.0 + eslint-utils: 3.0.0_eslint@8.21.0 + ignore: 5.2.0 + is-core-module: 2.10.0 + minimatch: 3.1.2 + resolve: 1.22.1 + semver: 7.3.7 + dev: true + + /eslint-plugin-promise/6.0.0_eslint@8.21.0: + resolution: {integrity: sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + eslint: 8.21.0 + dev: true + + /eslint-plugin-react/7.30.1_eslint@8.21.0: + resolution: {integrity: sha512-NbEvI9jtqO46yJA3wcRF9Mo0lF9T/jhdHqhCHXiXtD+Zcb98812wvokjWpU7Q4QH5edo6dmqrukxVvWWXHlsUg==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + array-includes: 3.1.5 + array.prototype.flatmap: 1.3.0 + doctrine: 2.1.0 + eslint: 8.21.0 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.2 + minimatch: 3.1.2 + object.entries: 1.1.5 + object.fromentries: 2.0.5 + object.hasown: 1.1.1 + object.values: 1.1.5 + prop-types: 15.8.1 + resolve: 2.0.0-next.4 + semver: 6.3.0 + string.prototype.matchall: 4.0.7 + dev: true + + /eslint-scope/5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /eslint-scope/7.1.1: + resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-utils/2.1.0: + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} + dependencies: + eslint-visitor-keys: 1.3.0 + dev: true + + /eslint-utils/3.0.0_eslint@8.21.0: + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + dependencies: + eslint: 8.21.0 + eslint-visitor-keys: 2.1.0 + dev: true + + /eslint-visitor-keys/1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + dev: true + + /eslint-visitor-keys/2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + dev: true + + /eslint-visitor-keys/3.3.0: + resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint/8.21.0: + resolution: {integrity: sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint/eslintrc': 1.3.0 + '@humanwhocodes/config-array': 0.10.4 + '@humanwhocodes/gitignore-to-minimatch': 1.0.2 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.1.1 + eslint-utils: 3.0.0_eslint@8.21.0 + eslint-visitor-keys: 3.3.0 + espree: 9.3.3 + esquery: 1.4.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + functional-red-black-tree: 1.0.1 + glob-parent: 6.0.2 + globals: 13.17.0 + globby: 11.1.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.0 + import-fresh: 3.3.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.1 + regexpp: 3.2.0 + strip-ansi: 6.0.1 + strip-json-comments: 3.1.1 + text-table: 0.2.0 + v8-compile-cache: 2.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree/9.3.3: + resolution: {integrity: sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.8.0 + acorn-jsx: 5.3.2_acorn@8.8.0 + eslint-visitor-keys: 3.3.0 + dev: true + + /esprima/4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /esquery/1.4.0: + resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse/4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse/4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true + + /estraverse/5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /esutils/2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /fast-deep-equal/3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-glob/3.2.11: + resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify/2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein/2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq/1.13.0: + resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} + dependencies: + reusify: 1.0.4 + dev: true + + /file-entry-cache/6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.0.4 + dev: true + + /fill-range/7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-up/2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + dependencies: + locate-path: 2.0.0 + dev: true + + /find-up/5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache/3.0.4: + resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.6 + rimraf: 3.0.2 + dev: true + + /flatted/3.2.6: + resolution: {integrity: sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==} + dev: true + + /form-data/4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: true + + /fraction.js/4.2.0: + resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} + dev: true + + /framer-motion/6.5.1_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==} + peerDependencies: + react: '>=16.8 || ^17.0.0 || ^18.0.0' + react-dom: '>=16.8 || ^17.0.0 || ^18.0.0' + dependencies: + '@motionone/dom': 10.12.0 + framesync: 6.0.1 + hey-listen: 1.0.8 + popmotion: 11.0.3 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + style-value-types: 5.0.0 + tslib: 2.4.0 + optionalDependencies: + '@emotion/is-prop-valid': 0.8.8 + dev: false + + /framesync/6.0.1: + resolution: {integrity: sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==} + dependencies: + tslib: 2.4.0 + dev: false + + /fs.realpath/1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents/2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind/1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + + /function.prototype.name/1.1.5: + resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.1 + functions-have-names: 1.2.3 + dev: true + + /functional-red-black-tree/1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + dev: true + + /functions-have-names/1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + + /gensync/1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-func-name/2.0.0: + resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + dev: true + + /get-intrinsic/1.1.2: + resolution: {integrity: sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-symbols: 1.0.3 + dev: true + + /get-symbol-description/1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.2 + dev: true + + /glob-parent/5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent/6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob/7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /globals/11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globals/13.17.0: + resolution: {integrity: sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globby/11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.2.11 + ignore: 5.2.0 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /grapheme-splitter/1.0.4: + resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} + dev: true + + /has-bigints/1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag/3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag/4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors/1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.1.2 + dev: true + + /has-symbols/1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /has-tostringtag/1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /has/1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + + /hey-listen/1.0.8: + resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} + dev: false + + /html-encoding-sniffer/3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + dependencies: + whatwg-encoding: 2.0.0 + dev: true + + /http-proxy-agent/5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /https-proxy-agent/5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /iconv-lite/0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /ignore/5.2.0: + resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} + engines: {node: '>= 4'} + dev: true + + /immutable/4.1.0: + resolution: {integrity: sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==} + dev: true + + /import-fresh/3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /imurmurhash/0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /indent-string/4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true + + /inflight/1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits/2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /internal-slot/1.0.3: + resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.1.2 + has: 1.0.3 + side-channel: 1.0.4 + dev: true + + /is-bigint/1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-binary-path/2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-boolean-object/1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-callable/1.2.4: + resolution: {integrity: sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module/2.10.0: + resolution: {integrity: sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==} + dependencies: + has: 1.0.3 + dev: true + + /is-date-object/1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-extglob/2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob/4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-negative-zero/2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object/1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-number/7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-potential-custom-element-name/1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: true + + /is-regex/1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-shared-array-buffer/1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: true + + /is-string/1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-symbol/1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-weakref/1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: true + + /isexe/2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /jest-diff/28.1.3: + resolution: {integrity: sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 28.1.1 + jest-get-type: 28.0.2 + pretty-format: 28.1.3 + dev: true + + /jest-get-type/28.0.2: + resolution: {integrity: sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dev: true + + /jest-matcher-utils/28.1.3: + resolution: {integrity: sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 28.1.3 + jest-get-type: 28.0.2 + pretty-format: 28.1.3 + dev: true + + /js-tokens/4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + /js-yaml/4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsdom/20.0.0: + resolution: {integrity: sha512-x4a6CKCgx00uCmP+QakBDFXwjAJ69IkkIWHmtmjd3wvXPcdOS44hfX2vqkOQrVrq8l9DhNNADZRXaCEWvgXtVA==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + abab: 2.0.6 + acorn: 8.8.0 + acorn-globals: 6.0.0 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.3.1 + domexception: 4.0.0 + escodegen: 2.0.0 + form-data: 4.0.0 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.1 + parse5: 7.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.0.0 + w3c-hr-time: 1.0.2 + w3c-xmlserializer: 3.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.8.1 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /jsesc/2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /json-schema-traverse/0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-stable-stringify-without-jsonify/1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5/1.0.1: + resolution: {integrity: sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==} + hasBin: true + dependencies: + minimist: 1.2.6 + dev: true + + /json5/2.2.1: + resolution: {integrity: sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /jsx-ast-utils/3.3.2: + resolution: {integrity: sha512-4ZCADZHRkno244xlNnn4AOG6sRQ7iBZ5BbgZ4vW4y5IZw7cVUD1PPeblm1xx/nfmMxPdt/LHsXZW8z/j58+l9Q==} + engines: {node: '>=4.0'} + dependencies: + array-includes: 3.1.5 + object.assign: 4.1.3 + dev: true + + /levn/0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + dev: true + + /levn/0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /lilconfig/2.0.6: + resolution: {integrity: sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==} + engines: {node: '>=10'} + dev: true + + /local-pkg/0.4.2: + resolution: {integrity: sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==} + engines: {node: '>=14'} + dev: true + + /locate-path/2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + dev: true + + /locate-path/6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.merge/4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash/4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + + /loose-envify/1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + + /loupe/2.3.4: + resolution: {integrity: sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==} + dependencies: + get-func-name: 2.0.0 + dev: true + + /lru-cache/6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /lz-string/1.4.4: + resolution: {integrity: sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==} + hasBin: true + dev: true + + /magic-string/0.26.2: + resolution: {integrity: sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==} + engines: {node: '>=12'} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /merge2/1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch/4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mime-db/1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: true + + /mime-types/2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + + /min-indent/1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + + /minimatch/3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimist/1.2.6: + resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} + dev: true + + /mrmime/1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} + dev: true + + /ms/2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: true + + /ms/2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /ms/2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /nanoid/3.3.4: + resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /natural-compare/1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /node-releases/2.0.6: + resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==} + dev: true + + /normalize-path/3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /normalize-range/0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: true + + /nwsapi/2.2.1: + resolution: {integrity: sha512-JYOWTeFoS0Z93587vRJgASD5Ut11fYl5NyihP3KrYBvMe1FRRs6RN7m20SA/16GM4P6hTnZjT+UmDOt38UeXNg==} + dev: true + + /object-assign/4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + /object-hash/3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + + /object-inspect/1.12.2: + resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} + dev: true + + /object-keys/1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign/4.1.3: + resolution: {integrity: sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.entries/1.1.5: + resolution: {integrity: sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.1 + dev: true + + /object.fromentries/2.0.5: + resolution: {integrity: sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.1 + dev: true + + /object.hasown/1.1.1: + resolution: {integrity: sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==} + dependencies: + define-properties: 1.1.4 + es-abstract: 1.20.1 + dev: true + + /object.values/1.1.5: + resolution: {integrity: sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.1 + dev: true + + /once/1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /optionator/0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.3 + dev: true + + /optionator/0.9.1: + resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.3 + dev: true + + /p-limit/1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + dependencies: + p-try: 1.0.0 + dev: true + + /p-limit/3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate/2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + dependencies: + p-limit: 1.3.0 + dev: true + + /p-locate/5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-try/1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + dev: true + + /parent-module/1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /parse5/7.0.0: + resolution: {integrity: sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g==} + dependencies: + entities: 4.3.1 + dev: true + + /path-exists/3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + dev: true + + /path-exists/4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute/1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key/3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-parse/1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-type/4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /pathval/1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + + /picocolors/1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch/2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pify/2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /popmotion/11.0.3: + resolution: {integrity: sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==} + dependencies: + framesync: 6.0.1 + hey-listen: 1.0.8 + style-value-types: 5.0.0 + tslib: 2.4.0 + dev: false + + /postcss-import/14.1.0_postcss@8.4.16: + resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} + engines: {node: '>=10.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.16 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.1 + dev: true + + /postcss-js/4.0.0_postcss@8.4.16: + resolution: {integrity: sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.3.3 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.16 + dev: true + + /postcss-load-config/3.1.4_postcss@8.4.16: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.0.6 + postcss: 8.4.16 + yaml: 1.10.2 + dev: true + + /postcss-nested/5.0.6_postcss@8.4.16: + resolution: {integrity: sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.16 + postcss-selector-parser: 6.0.10 + dev: true + + /postcss-selector-parser/6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-value-parser/4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss/8.4.16: + resolution: {integrity: sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /prelude-ls/1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + dev: true + + /prelude-ls/1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /pretty-format/27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + dev: true + + /pretty-format/28.1.3: + resolution: {integrity: sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/schemas': 28.1.3 + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + + /prop-types/15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + /psl/1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + dev: true + + /punycode/2.1.1: + resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} + engines: {node: '>=6'} + dev: true + + /queue-microtask/1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /quick-lru/5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: true + + /react-dom/18.2.0_react@18.2.0: + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + + /react-is/16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + /react-is/17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: true + + /react-is/18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: true + + /react-refresh/0.14.0: + resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} + engines: {node: '>=0.10.0'} + dev: true + + /react-svg-pan-zoom/3.11.0_react@18.2.0: + resolution: {integrity: sha512-xK2tpfp4YksHOfyMZH5zXP52ARLSBgkoJgWNJmJ1B+6O1tkuf23TQp7Q4m9GG5IRSK5KWO0JEGEWlNYG9+iiug==} + peerDependencies: + react: '>=17.0.0' + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + transformation-matrix: 2.12.0 + dev: false + + /react/18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + + /read-cache/1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: true + + /readdirp/3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /redent/3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + dev: true + + /regenerator-runtime/0.13.9: + resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} + dev: true + + /regexp.prototype.flags/1.4.3: + resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + functions-have-names: 1.2.3 + dev: true + + /regexpp/3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + dev: true + + /resolve-from/4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve/1.22.1: + resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} + hasBin: true + dependencies: + is-core-module: 2.10.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /resolve/2.0.0-next.4: + resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} + hasBin: true + dependencies: + is-core-module: 2.10.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify/1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf/3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rollup/2.77.2: + resolution: {integrity: sha512-m/4YzYgLcpMQbxX3NmAqDvwLATZzxt8bIegO78FZLl+lAgKJBd1DRAOeEiZcKOIOPjxE6ewHWHNgGEalFXuz1g==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /run-parallel/1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-buffer/5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: true + + /safer-buffer/2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: true + + /sass/1.54.3: + resolution: {integrity: sha512-fLodey5Qd41Pxp/Tk7Al97sViYwF/TazRc5t6E65O7JOk4XF8pzwIW7CvCxYVOfJFFI/1x5+elDyBIixrp+zrw==} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + chokidar: 3.5.3 + immutable: 4.1.0 + source-map-js: 1.0.2 + dev: true + + /saxes/6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: true + + /scheduler/0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + + /semver/6.3.0: + resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + hasBin: true + dev: true + + /semver/7.3.7: + resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /shebang-command/2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex/3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /side-channel/1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.2 + object-inspect: 1.12.2 + dev: true + + /sirv/2.0.2: + resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.21 + mrmime: 1.0.1 + totalist: 3.0.0 + dev: true + + /slash/3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /source-map-js/1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map/0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + requiresBuild: true + dev: true + optional: true + + /sourcemap-codec/1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + dev: true + + /string.prototype.matchall/4.0.7: + resolution: {integrity: sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.1 + get-intrinsic: 1.1.2 + has-symbols: 1.0.3 + internal-slot: 1.0.3 + regexp.prototype.flags: 1.4.3 + side-channel: 1.0.4 + dev: true + + /string.prototype.trimend/1.0.5: + resolution: {integrity: sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.1 + dev: true + + /string.prototype.trimstart/1.0.5: + resolution: {integrity: sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.1 + dev: true + + /strip-ansi/6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-bom/3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-indent/3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + dependencies: + min-indent: 1.0.1 + dev: true + + /strip-json-comments/3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /style-value-types/5.0.0: + resolution: {integrity: sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==} + dependencies: + hey-listen: 1.0.8 + tslib: 2.4.0 + dev: false + + /supports-color/5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color/7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag/1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /symbol-tree/3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: true + + /tailwindcss/3.1.8: + resolution: {integrity: sha512-YSneUCZSFDYMwk+TGq8qYFdCA3yfBRdBlS7txSq0LUmzyeqRe3a8fBQzbz9M3WS/iFT4BNf/nmw9mEzrnSaC0g==} + engines: {node: '>=12.13.0'} + hasBin: true + dependencies: + arg: 5.0.2 + chokidar: 3.5.3 + color-name: 1.1.4 + detective: 5.2.1 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.2.11 + glob-parent: 6.0.2 + is-glob: 4.0.3 + lilconfig: 2.0.6 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.16 + postcss-import: 14.1.0_postcss@8.4.16 + postcss-js: 4.0.0_postcss@8.4.16 + postcss-load-config: 3.1.4_postcss@8.4.16 + postcss-nested: 5.0.6_postcss@8.4.16 + postcss-selector-parser: 6.0.10 + postcss-value-parser: 4.2.0 + quick-lru: 5.1.1 + resolve: 1.22.1 + transitivePeerDependencies: + - ts-node + dev: true + + /text-table/0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /tinypool/0.2.4: + resolution: {integrity: sha512-Vs3rhkUH6Qq1t5bqtb816oT+HeJTXfwt2cbPH17sWHIYKTotQIFPk3tf2fgqRrVyMDVOc1EnPgzIxfIulXVzwQ==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy/1.0.0: + resolution: {integrity: sha512-FI5B2QdODQYDRjfuLF+OrJ8bjWRMCXokQPcwKm0W3IzcbUmBNv536cQc7eXGoAuXphZwgx1DFbqImwzz08Fnhw==} + engines: {node: '>=14.0.0'} + dev: true + + /to-fast-properties/2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /to-regex-range/5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /totalist/3.0.0: + resolution: {integrity: sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==} + engines: {node: '>=6'} + dev: true + + /tough-cookie/4.0.0: + resolution: {integrity: sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.1.1 + universalify: 0.1.2 + dev: true + + /tr46/3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + dependencies: + punycode: 2.1.1 + dev: true + + /transformation-matrix/2.12.0: + resolution: {integrity: sha512-BbzXM7el7rNwIr1s87m8tcffH5qgY+HYROLn3BStRU9Y6vYTL37YZKadfNPEvGbP813iA1h8qflo4pa2TomkyQ==} + dev: false + + /tsconfig-paths/3.14.1: + resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.1 + minimist: 1.2.6 + strip-bom: 3.0.0 + dev: true + + /tslib/1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: true + + /tslib/2.4.0: + resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} + dev: false + + /tsutils/3.21.0_typescript@4.7.4: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 4.7.4 + dev: true + + /type-check/0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + dev: true + + /type-check/0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-detect/4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + + /type-fest/0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /typescript/4.7.4: + resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /unbox-primitive/1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /universalify/0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: true + + /update-browserslist-db/1.0.5_browserslist@4.21.3: + resolution: {integrity: sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.21.3 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: true + + /uri-js/4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.1.1 + dev: true + + /util-deprecate/1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /v8-compile-cache/2.3.0: + resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} + dev: true + + /vite/3.0.4_sass@1.54.3: + resolution: {integrity: sha512-NU304nqnBeOx2MkQnskBQxVsa0pRAH5FphokTGmyy8M3oxbvw7qAXts2GORxs+h/2vKsD+osMhZ7An6yK6F1dA==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + less: '*' + sass: '*' + stylus: '*' + terser: ^5.4.0 + peerDependenciesMeta: + less: + optional: true + sass: + optional: true + stylus: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.14.54 + postcss: 8.4.16 + resolve: 1.22.1 + rollup: 2.77.2 + sass: 1.54.3 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /vitest/0.20.3_hymhw3vkyr5yfvzfskw3x5v26q: + resolution: {integrity: sha512-cXMjTbZxBBUUuIF3PUzEGPLJWtIMeURBDXVxckSHpk7xss4JxkiiWh5cnIlfGyfJne2Ii3QpbiRuFL5dMJtljw==} + engines: {node: '>=v14.16.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + c8: '*' + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + c8: + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/chai': 4.3.3 + '@types/chai-subset': 1.3.3 + '@types/node': 18.6.4 + '@vitest/ui': 0.20.3 + chai: 4.3.6 + debug: 4.3.4 + jsdom: 20.0.0 + local-pkg: 0.4.2 + tinypool: 0.2.4 + tinyspy: 1.0.0 + vite: 3.0.4_sass@1.54.3 + transitivePeerDependencies: + - less + - sass + - stylus + - supports-color + - terser + dev: true + + /w3c-hr-time/1.0.2: + resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} + dependencies: + browser-process-hrtime: 1.0.0 + dev: true + + /w3c-xmlserializer/3.0.0: + resolution: {integrity: sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==} + engines: {node: '>=12'} + dependencies: + xml-name-validator: 4.0.0 + dev: true + + /webidl-conversions/7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: true + + /whatwg-encoding/2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + dependencies: + iconv-lite: 0.6.3 + dev: true + + /whatwg-mimetype/3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: true + + /whatwg-url/11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + dev: true + + /which-boxed-primitive/1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which/2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /word-wrap/1.2.3: + resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} + engines: {node: '>=0.10.0'} + dev: true + + /wrappy/1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /ws/8.8.1: + resolution: {integrity: sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /xml-name-validator/4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + dev: true + + /xmlchars/2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: true + + /xtend/4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: true + + /yallist/4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true + + /yaml/1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: true + + /yocto-queue/0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true From 161a2cfb3e8b762daac98ed8c121d4ef3165c796 Mon Sep 17 00:00:00 2001 From: Eric Nguyen Date: Mon, 8 Aug 2022 16:12:49 +0000 Subject: [PATCH 26/32] Merged PR 15: Azure-Pipelines: Use pnpm to accelerate npm install command --- azure-pipelines.yml | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 28f4504..7b6f59f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -11,7 +11,21 @@ trigger: pool: vmImage: ubuntu-latest +variables: + pnpm_config_cache: $(Pipeline.Workspace)/.pnpm-store + steps: +- task: Cache@2 + inputs: + key: 'pnpm | "$(Agent.OS)" | pnpm-lock.yaml' + path: $(pnpm_config_cache) + displayName: Cache pnpm + +- script: | + curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7 + pnpm config set store-dir $(pnpm_config_cache) + displayName: "Setup pnpm" + - task: NodeTool@0 inputs: versionSpec: '16.x' @@ -21,8 +35,8 @@ steps: node --version node ./test-server/node-http.js & jobs - npm ci - npm run test:nowatch + pnpm i + pnpm run test:nowatch kill -2 %1 2>/dev/null displayName: 'Test on Node.js 16.x LTS' @@ -35,7 +49,7 @@ steps: node --version node ./test-server/node-http.js & jobs - npm ci - npm run test:nowatch + pnpm i + pnpm run test:nowatch kill -2 %1 2>/dev/null displayName: 'Test on Node.js 18.x Latest' \ No newline at end of file From 900e92553148bc338005c8c20c6f278cdf4f9b72 Mon Sep 17 00:00:00 2001 From: Siklos Date: Mon, 8 Aug 2022 18:15:56 +0200 Subject: [PATCH 27/32] Add pnpm to drone.yml --- .drone.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.drone.yml b/.drone.yml index 0a843af..759892f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -6,9 +6,10 @@ steps: - name: test image: node:16 commands: + - curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7 - node ./test-server/node-http.js & - - npm install - - npm run test:nowatch + - pnpm install + - pnpm run test:nowatch --- kind: pipeline @@ -18,6 +19,7 @@ steps: - name: test image: node commands: + - curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7 - node ./test-server/node-http.js & - - npm install - - npm run test:nowatch \ No newline at end of file + - pnpm install + - pnpm run test:nowatch \ No newline at end of file From f1e23260731b65cb5e8820777d346cff6b071063 Mon Sep 17 00:00:00 2001 From: Siklos Date: Mon, 8 Aug 2022 21:54:49 +0200 Subject: [PATCH 28/32] Fix tests imports --- src/Components/ElementsSidebar/ElementsSidebar.test.tsx | 2 +- src/Components/Properties/Properties.test.tsx | 2 +- src/Components/Sidebar/Sidebar.test.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index c56e372..0e58142 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -1,4 +1,4 @@ -import { describe, expect, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import * as React from 'react'; import { fireEvent, render, screen } from '../../utils/test-utils'; import { ElementsSidebar } from './ElementsSidebar'; diff --git a/src/Components/Properties/Properties.test.tsx b/src/Components/Properties/Properties.test.tsx index 1ce9d25..86a80e2 100644 --- a/src/Components/Properties/Properties.test.tsx +++ b/src/Components/Properties/Properties.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react'; import * as React from 'react'; -import { describe, it, vi } from 'vitest'; +import { expect, describe, it, vi } from 'vitest'; import { Properties } from './Properties'; describe.concurrent('Properties', () => { diff --git a/src/Components/Sidebar/Sidebar.test.tsx b/src/Components/Sidebar/Sidebar.test.tsx index 0810741..0020c9e 100644 --- a/src/Components/Sidebar/Sidebar.test.tsx +++ b/src/Components/Sidebar/Sidebar.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { describe, test, expect, vi } from 'vitest'; -import { findByText, fireEvent, render, screen } from '../../utils/test-utils'; +import { describe, it, expect, vi } from 'vitest'; +import { fireEvent, render, screen } from '../../utils/test-utils'; import Sidebar from './Sidebar'; describe.concurrent('Sidebar', () => { From 822cd4107de3d079141d8c4ba1839d9dfa4a672d Mon Sep 17 00:00:00 2001 From: Siklos Date: Mon, 8 Aug 2022 23:07:55 +0200 Subject: [PATCH 29/32] Add some simple documentation --- docs/Dependencies.md | 73 +++++++++++++++++++++++++++++++++++++++ docs/Project_Structure.md | 38 ++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 docs/Dependencies.md create mode 100644 docs/Project_Structure.md diff --git a/docs/Dependencies.md b/docs/Dependencies.md new file mode 100644 index 0000000..626458e --- /dev/null +++ b/docs/Dependencies.md @@ -0,0 +1,73 @@ +# Dependencies + +This document briefly explains the different dependencies located in the `package.json`. + +This document will not explain how to use them. You can read their documentation for that and the codebase have exemples for references. + + +# [React](https://reactjs.org/) + +Main framework to build the js application. + +It depends on Vite in order to build the project. + +Others dependencies: +- [react-dom](https://reactjs.org/docs/react-dom.html): library used to inject the app to `#root` html element. +- [react-svg-pan-zoom](https://www.npmjs.com/package/react-svg-pan-zoom): component that offers pan + zoom to a svg element + + +# [Vite](https://vitejs.dev/) + +Vite is the main tool to develop the react application. + +Its uses the following files to configure the project : +- `vite.config.ts` +- `.env*` +- `src/vite-env.d.ts` + +Others dependencies: +- @vitejs/plugin-react + + +# [Tailwind CSS](https://tailwindcss.com/) + +CSS framework designed around constraints with utility classes in order to reduce dead css code. + +Its uses the following files to configure the project : +- `src/index.scss` +- `tailwind.config.cjs` +- `postcss.config.cjs` + +Other dependencies: +- postcss +- sass +- autoprefixer + + +# [Heroicons](https://heroicons.com/) + +SVG Icons that can be used as JSX elements with Tailwind CSS + + +# Testing + +- [Vitest](https://vitest.dev/) +- [Testing Library](https://testing-library.com/) +- [jsdom](https://github.com/jsdom/jsdom) + + +# [eslint](https://eslint.org/) + +A Linter. Used for error checking, syntax checking and code style enforcing. + +Currently using `standard-with-typescript` with a few modification. + +See the `.eslintrc.cjs` for more informations. + +Other dependencies: +- typescript-eslint/eslint-plugin +- typescript-eslint/parser +- eslint-plugin-import +- eslint-plugin-n +- eslint-plugin-promise +- eslint-plugin-react diff --git a/docs/Project_Structure.md b/docs/Project_Structure.md new file mode 100644 index 0000000..890daf8 --- /dev/null +++ b/docs/Project_Structure.md @@ -0,0 +1,38 @@ +# Project Structure + +The project is structured this way + +``` +. +├── docs Documentation folder +├── public Public folder in which the index.html +│ import its resources +├── src Source folder for the react app +│ ├── assets Assets folder in which the react app +│ │ import its resources +│ ├── Components Components folder +│ ├── Enums Enums folder +│ ├── Interfaces Interface (+ types folder) +│ ├── test Setup folder for the tests +│ ├── tests Other tests + resources +│ ├── utils Utilities folder +│ ├── index.scss Tailwind CSS extends +│ ├── main.tsx Entrypoint for App injection +│ └── vite-env.d.ts Types for .env files +├── test-server Tests servers to test the API +│ ├── http.js Test server for bun.sh +│ └── node-http.js Test server for Node.js +├── azure-pipelines.yml Azure Pipelines YAML config file +├── index.html HTML Page +├── package-lock.json Describe the node_modules tree for npm +├── package.json Node.JS config file +├── pnpm-lock.yaml Describe the node_modules tree for pnpm +├── postcss.config.cjs Postcss config file for SCSS processing +├── README.md +├── tailwind.config.cjs Tailwind CSS config file +├── tsconfig.json Typescript config file +├── tsconfig.node.json Typescript config file for Node modules +├── vite.config.ts Vite config file +└── vitest.config.ts Vitest config file +``` + From ceaea43288865801e36e85138e7f29db42f7b43c Mon Sep 17 00:00:00 2001 From: Siklos Date: Mon, 8 Aug 2022 23:15:26 +0200 Subject: [PATCH 30/32] Added CICD doc + update README.md --- README.md | 3 ++- docs/CICD.md | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 docs/CICD.md diff --git a/README.md b/README.md index ef6d1ce..635d01b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ An svg layout designer. Requierements : - NodeJS -- NPM +- npm +- pnpm (optional but recommanded unless you prefer having a huge `node_modules` directory) # Developping diff --git a/docs/CICD.md b/docs/CICD.md new file mode 100644 index 0000000..7c3543f --- /dev/null +++ b/docs/CICD.md @@ -0,0 +1,12 @@ +# Azure Pipelines + +This project uses Azure Pipelines to runs automatic tests. + +Its `azure-pipelines.yml` configuration file can be found at the root project folder. + + +# Drone.io + +Due to the limitations of Azure Pipelines (limited free usage, no parallel, no dockerhub...), it might be more useful to use Drone.io. + +Its config file can be found in `.drone.yml`. \ No newline at end of file From 1fc11adbaa52eabd3cbb818df95cf186dd338378 Mon Sep 17 00:00:00 2001 From: Siklos Date: Tue, 9 Aug 2022 06:08:04 -0400 Subject: [PATCH 31/32] Implement drag and drop (#21) Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/21 --- .../ElementsSidebar/ElementsSidebar.test.tsx | 24 ++-- .../ElementsSidebar/ElementsSidebar.tsx | 121 +++++++++++++++++- src/Components/Sidebar/Sidebar.tsx | 7 + src/Components/UI/UI.tsx | 10 +- src/Editor.tsx | 55 ++++---- 5 files changed, 172 insertions(+), 45 deletions(-) diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index 0e58142..bca12ba 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -12,8 +12,8 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={null} onPropertyChange={() => {}} - selectContainer={() => {}} - deleteContainer={() => {}} + SelectContainer={() => {}} + DeleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -40,8 +40,8 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={null} onPropertyChange={() => {}} - selectContainer={() => {}} - deleteContainer={() => {}} + SelectContainer={() => {}} + DeleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -70,8 +70,8 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={MainContainer} onPropertyChange={() => {}} - selectContainer={() => {}} - deleteContainer={() => {}} + SelectContainer={() => {}} + DeleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -155,8 +155,8 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={MainContainer} onPropertyChange={() => {}} - selectContainer={() => {}} - deleteContainer={() => {}} + SelectContainer={() => {}} + DeleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -208,8 +208,8 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={SelectedContainer} onPropertyChange={() => {}} - selectContainer={selectContainer} - deleteContainer={() => {}} + SelectContainer={selectContainer} + DeleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -230,8 +230,8 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={SelectedContainer} onPropertyChange={() => {}} - selectContainer={selectContainer} - deleteContainer={() => {}} + SelectContainer={selectContainer} + DeleteContainer={() => {}} />); expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy(); diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index 621316b..0159b4d 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { motion } from 'framer-motion'; import { Properties } from '../Properties/Properties'; import { IContainerModel } from '../../Interfaces/ContainerModel'; -import { getDepth, MakeIterator } from '../../utils/itertools'; +import { findContainerById, getDepth, MakeIterator } from '../../utils/itertools'; import { Menu } from '../Menu/Menu'; import { MenuItem } from '../Menu/MenuItem'; @@ -12,8 +12,9 @@ interface IElementsSidebarProps { isHistoryOpen: boolean SelectedContainer: IContainerModel | null onPropertyChange: (key: string, value: string) => void - selectContainer: (container: IContainerModel) => void - deleteContainer: (containerid: string) => void + SelectContainer: (container: IContainerModel) => void + DeleteContainer: (containerid: string) => void + AddContainer: (index: number, type: string, parent: string) => void } interface Point { @@ -84,6 +85,97 @@ export class ElementsSidebar extends React.PureComponent }); } + public handleDragOver(event: React.DragEvent): void { + event.preventDefault(); + const target: HTMLButtonElement = event.target as HTMLButtonElement; + const rect = target.getBoundingClientRect(); + const y = event.clientY - rect.top; // y position within the element. + + if (this.props.MainContainer === null) { + throw new Error('[handleOnDrop] Tried to drop into the tree without a required MainContainer!'); + } + + if (target.id === this.props.MainContainer.properties.id) { + target.classList.add('border-8'); + target.classList.remove('border-t-8'); + target.classList.remove('border-b-8'); + return; + } + + if (y < 12) { + target.classList.add('border-t-8'); + target.classList.remove('border-b-8'); + target.classList.remove('border-8'); + } else if (y < 24) { + target.classList.add('border-8'); + target.classList.remove('border-t-8'); + target.classList.remove('border-b-8'); + } else { + target.classList.add('border-b-8'); + target.classList.remove('border-8'); + target.classList.remove('border-t-8'); + } + } + + public handleOnDrop(event: React.DragEvent): void { + event.preventDefault(); + const type = event.dataTransfer.getData('type'); + const target: HTMLButtonElement = event.target as HTMLButtonElement; + removeBorderClasses(target); + + if (this.props.MainContainer === null) { + throw new Error('[handleOnDrop] Tried to drop into the tree without a required MainContainer!'); + } + + const targetContainer: IContainerModel | undefined = findContainerById( + this.props.MainContainer, + target.id + ); + + if (targetContainer === undefined) { + throw new Error('[handleOnDrop] Tried to drop onto a unknown container!'); + } + + if (targetContainer === this.props.MainContainer) { + // if the container is the root, only add type as child + this.props.AddContainer( + targetContainer.children.length, + type, + targetContainer.properties.id); + return; + } + + if (targetContainer.parent === null || + targetContainer.parent === undefined) { + throw new Error('[handleDrop] Tried to drop into a child container without a parent!'); + } + + const rect = target.getBoundingClientRect(); + const y = event.clientY - rect.top; // y position within the element. + + // locate the hitboxes + if (y < 12) { + const index = targetContainer.parent.children.indexOf(targetContainer); + this.props.AddContainer( + index, + type, + targetContainer.parent.properties.id + ); + } else if (y < 24) { + this.props.AddContainer( + targetContainer.children.length, + type, + targetContainer.properties.id); + } else { + const index = targetContainer.parent.children.indexOf(targetContainer); + this.props.AddContainer( + index + 1, + type, + targetContainer.parent.properties.id + ); + } + } + public iterateChilds(handleContainer: (container: IContainerModel) => void): React.ReactNode { if (this.props.MainContainer == null) { return null; @@ -111,7 +203,7 @@ export class ElementsSidebar extends React.PureComponent const selectedClass: string = this.props.SelectedContainer !== undefined && this.props.SelectedContainer !== null && this.props.SelectedContainer.properties.id === container.properties.id - ? 'border-l-4 border-blue-500 bg-slate-400/60 hover:bg-slate-400' + ? 'border-l-4 bg-slate-400/60 hover:bg-slate-400' : 'bg-slate-300/60 hover:bg-slate-300'; containerRows.push( duration: 0.150 }} className={ - `w-full elements-sidebar-row whitespace-pre + `w-full border-blue-500 elements-sidebar-row whitespace-pre text-left text-sm font-medium transition-all ${selectedClass}` } id={key} key={key} - onClick={() => this.props.selectContainer(container)}> + onDrop={(event) => this.handleOnDrop(event)} + onDragOver={(event) => this.handleDragOver(event)} + onDragLeave={(event) => handleDragLeave(event)} + onClick={() => this.props.SelectContainer(container)} + > { text } ); @@ -148,10 +244,21 @@ export class ElementsSidebar extends React.PureComponent y={this.state.contextMenuPosition.y} isOpen={this.state.isContextMenuOpen} > - this.props.deleteContainer(this.state.onClickContainerId)} /> + this.props.DeleteContainer(this.state.onClickContainerId)} />
); } } + +function removeBorderClasses(target: HTMLButtonElement): void { + target.classList.remove('border-t-8'); + target.classList.remove('border-8'); + target.classList.remove('border-b-8'); +} + +function handleDragLeave(event: React.DragEvent): void { + const target: HTMLButtonElement = event.target as HTMLButtonElement; + removeBorderClasses(target); +} diff --git a/src/Components/Sidebar/Sidebar.tsx b/src/Components/Sidebar/Sidebar.tsx index c50730e..de06e61 100644 --- a/src/Components/Sidebar/Sidebar.tsx +++ b/src/Components/Sidebar/Sidebar.tsx @@ -8,14 +8,21 @@ interface ISidebarProps { buttonOnClick: (type: string) => void } +function handleDragStart(event: React.DragEvent): void { + event.dataTransfer.setData('type', (event.target as HTMLButtonElement).id); +} + export default class Sidebar extends React.PureComponent { public render(): JSX.Element { const listElements = this.props.componentOptions.map(componentOption => diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index 46ae454..e0d9b6c 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -17,7 +17,8 @@ interface IUIProps { SelectContainer: (container: ContainerModel) => void DeleteContainer: (containerId: string) => void OnPropertyChange: (key: string, value: string) => void - AddContainer: (type: string) => void + AddContainerToSelectedContainer: (type: string) => void + AddContainer: (index: number, type: string, parentId: string) => void SaveEditorAsJSON: () => void SaveEditorAsSVG: () => void LoadState: (move: number) => void @@ -89,7 +90,7 @@ export class UI extends React.PureComponent { this.props.AddContainer(type)} + buttonOnClick={(type: string) => this.props.AddContainerToSelectedContainer(type)} /> { isOpen={this.state.isElementsSidebarOpen} isHistoryOpen={this.state.isHistoryOpen} onPropertyChange={this.props.OnPropertyChange} - selectContainer={this.props.SelectContainer} - deleteContainer={this.props.DeleteContainer} + SelectContainer={this.props.SelectContainer} + DeleteContainer={this.props.DeleteContainer} + AddContainer={this.props.AddContainer} /> { * @param type The type of container * @returns void */ - public AddContainer(type: string): void { + public AddContainerToSelectedContainer(type: string): void { const history = this.getCurrentHistory(); const current = history[history.length - 1]; @@ -204,13 +204,22 @@ class Editor extends React.Component { return; } + const parent = current.SelectedContainer; + this.AddContainer(parent.children.length, type, parent.properties.id); + } + + public AddContainer(index: number, type: string, parentId: string): void { + const history = this.getCurrentHistory(); + const current = history[history.length - 1]; + if (current.MainContainer === null || current.MainContainer === undefined) { return; } // Get the preset properties from the API - const properties = this.props.configuration.AvailableContainers.find(option => option.Type === type); + const properties = this.props.configuration.AvailableContainers + .find(option => option.Type === type); if (properties === undefined) { throw new Error(`[AddContainer] Object type not found. Found: ${type}`); @@ -230,35 +239,32 @@ class Editor extends React.Component { const clone: IContainerModel = structuredClone(current.MainContainer); // Find the parent - const it = MakeIterator(clone); - let parent: ContainerModel | null = null; - for (const child of it) { - if (child.properties.id === current.SelectedContainer.properties.id) { - parent = child as ContainerModel; - break; - } - } + const parentClone: IContainerModel | undefined = findContainerById( + clone, parentId + ); - if (parent === null) { - throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); + if (parentClone === null || parentClone === undefined) { + throw new Error('[AddContainer] Container model was not found among children of the main container!'); } let x = 0; - const lastChild: IContainerModel | undefined = parent.children.at(-1); - if (lastChild !== undefined) { - x = lastChild.properties.x + Number(lastChild.properties.width); + if (index !== 0) { + const lastChild: IContainerModel | undefined = parentClone.children.at(index - 1); + if (lastChild !== undefined) { + x = lastChild.properties.x + Number(lastChild.properties.width); + } } // Create the container const newContainer = new ContainerModel( - parent, + parentClone, { id: `${type}-${count}`, - parentId: parent.properties.id, + parentId: parentClone.properties.id, x, y: 0, width: properties?.Width, - height: parent.properties.height, + height: parentClone.properties.height, ...properties.Style }, [], @@ -268,15 +274,19 @@ class Editor extends React.Component { ); // And push it the the parent children - parent.children.push(newContainer); + if (index === parentClone.children.length) { + parentClone.children.push(newContainer); + } else { + parentClone.children.splice(index, 0, newContainer); + } // Update the state this.setState({ history: history.concat([{ MainContainer: clone, TypeCounters: newCounters, - SelectedContainer: parent, - SelectedContainerId: parent.properties.id + SelectedContainer: parentClone, + SelectedContainerId: parentClone.properties.id }]), historyCurrentStep: history.length }); @@ -331,7 +341,8 @@ class Editor extends React.Component { SelectContainer={(container) => this.SelectContainer(container)} DeleteContainer={(containerId: string) => this.DeleteContainer(containerId)} OnPropertyChange={(key, value) => this.OnPropertyChange(key, value)} - AddContainer={(type) => this.AddContainer(type)} + AddContainerToSelectedContainer={(type) => this.AddContainerToSelectedContainer(type)} + AddContainer={(index, type, parentId) => this.AddContainer(index, type, parentId)} SaveEditorAsJSON={() => this.SaveEditorAsJSON()} SaveEditorAsSVG={() => this.SaveEditorAsSVG()} LoadState={(move) => this.LoadState(move)} From d9e06537e8a5d35ff2dc960a325c740a80c08438 Mon Sep 17 00:00:00 2001 From: Eric Nguyen Date: Tue, 9 Aug 2022 15:15:56 +0000 Subject: [PATCH 32/32] Merged PR 16: Transform every single class components into functional component This improve greatly the performance and the code cleaning. It allows us to separate the inseparable class methods into modules functions --- .drone.yml | 4 +- azure-pipelines.yml | 4 +- src/App.tsx | 240 ------------ src/{test => Components/API}/api.test.tsx | 2 +- src/Components/API/api.ts | 30 ++ src/{ => Components/App}/App.scss | 0 src/Components/App/App.tsx | 79 ++++ src/Components/App/Load.ts | 13 + src/Components/App/MenuActions.ts | 71 ++++ src/Components/Editor/ContainerOperations.ts | 269 +++++++++++++ src/{ => Components/Editor}/Editor.scss | 0 src/Components/Editor/Editor.tsx | 115 ++++++ src/Components/Editor/Save.ts | 41 ++ src/Components/Editor/Shortcuts.ts | 23 ++ .../ElementsSidebar/ElementsSidebar.test.tsx | 21 +- .../ElementsSidebar/ElementsSidebar.tsx | 367 ++++++------------ .../ElementsSidebar/MouseEventHandlers.ts | 137 +++++++ .../FloatingButton/FloatingButton.tsx | 4 +- src/Components/History/History.tsx | 86 ++-- src/Components/MainMenu/MainMenu.tsx | 12 - src/Components/Properties/Properties.tsx | 75 ++-- src/Components/SVG/Elements/Container.tsx | 122 +++--- src/Components/SVG/Elements/Dimension.tsx | 80 ++-- src/Components/SVG/SVG.tsx | 125 +++--- src/Components/Sidebar/Sidebar.test.tsx | 2 +- src/Components/Sidebar/Sidebar.tsx | 52 ++- src/Components/UI/UI.tsx | 170 ++++---- src/Editor.tsx | 362 ----------------- src/Interfaces/HistoryState.ts | 8 + src/Interfaces/Point.ts | 4 + src/main.tsx | 2 +- src/utils/default.ts | 37 ++ src/utils/saveload.ts | 2 +- 33 files changed, 1298 insertions(+), 1261 deletions(-) delete mode 100644 src/App.tsx rename src/{test => Components/API}/api.test.tsx (93%) create mode 100644 src/Components/API/api.ts rename src/{ => Components/App}/App.scss (100%) create mode 100644 src/Components/App/App.tsx create mode 100644 src/Components/App/Load.ts create mode 100644 src/Components/App/MenuActions.ts create mode 100644 src/Components/Editor/ContainerOperations.ts rename src/{ => Components/Editor}/Editor.scss (100%) create mode 100644 src/Components/Editor/Editor.tsx create mode 100644 src/Components/Editor/Save.ts create mode 100644 src/Components/Editor/Shortcuts.ts create mode 100644 src/Components/ElementsSidebar/MouseEventHandlers.ts delete mode 100644 src/Editor.tsx create mode 100644 src/Interfaces/HistoryState.ts create mode 100644 src/Interfaces/Point.ts create mode 100644 src/utils/default.ts diff --git a/.drone.yml b/.drone.yml index 759892f..e3b1f91 100644 --- a/.drone.yml +++ b/.drone.yml @@ -10,6 +10,7 @@ steps: - node ./test-server/node-http.js & - pnpm install - pnpm run test:nowatch + - pnpm run build --- kind: pipeline @@ -22,4 +23,5 @@ steps: - curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7 - node ./test-server/node-http.js & - pnpm install - - pnpm run test:nowatch \ No newline at end of file + - pnpm run test:nowatch + - pnpm run build \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7b6f59f..5c52dac 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -37,7 +37,8 @@ steps: jobs pnpm i pnpm run test:nowatch - kill -2 %1 2>/dev/null + pnpm run build + kill -2 %1 2>/dev/null displayName: 'Test on Node.js 16.x LTS' - task: NodeTool@0 @@ -51,5 +52,6 @@ steps: jobs pnpm i pnpm run test:nowatch + pnpm run build kill -2 %1 2>/dev/null displayName: 'Test on Node.js 18.x Latest' \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index 263827b..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import * as React from 'react'; -import './App.scss'; -import { MainMenu } from './Components/MainMenu/MainMenu'; -import { ContainerModel, IContainerModel } from './Interfaces/ContainerModel'; -import Editor, { IEditorState } from './Editor'; -import { Configuration } from './Interfaces/Configuration'; -import { Revive } from './utils/saveload'; - -export interface IHistoryState { - MainContainer: IContainerModel | null - SelectedContainer: IContainerModel | null - SelectedContainerId: string - TypeCounters: Record -} - -// App will never have props -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface IAppProps { -} - -interface IAppState { - configuration: Configuration - history: IHistoryState[] - historyCurrentStep: number - isLoaded: boolean -} - -export class App extends React.Component { - public state: IAppState; - - constructor(props: IAppProps) { - super(props); - this.state = { - configuration: { - AvailableContainers: [], - AvailableSymbols: [], - MainContainer: { - Type: 'EmptyContainer', - Width: 3000, - Height: 200, - Style: {} - } - }, - history: [], - historyCurrentStep: 0, - isLoaded: false - }; - } - - componentDidMount(): void { - const queryString = window.location.search; - const urlParams = new URLSearchParams(queryString); - const state = urlParams.get('state'); - - if (state === null) { - return; - } - - fetch(state) - .then( - async(response) => await response.json(), - (error) => { throw new Error(error); } - ) - .then((data: IEditorState) => { - this.LoadState(data); - }, (error) => { throw new Error(error); }); - } - - public NewEditor(): void { - // Fetch the configuration from the API - fetchConfiguration() - .then((configuration: Configuration) => { - // Set the main container from the given properties of the API - const MainContainer = new ContainerModel( - null, - { - id: 'main', - parentId: 'null', - x: 0, - y: 0, - width: configuration.MainContainer.Width, - height: configuration.MainContainer.Height, - fillOpacity: 0, - stroke: 'black' - } - ); - - // Save the configuration and the new MainContainer - // and default the selected container to it - this.setState({ - configuration, - history: - [ - { - MainContainer, - SelectedContainer: MainContainer, - TypeCounters: {} - } - ], - historyCurrentStep: 0, - isLoaded: true - }); - }, (error) => { - // TODO: Implement an alert component - console.warn('[NewEditor] Could not fetch resource from API. Returning default.', error); - const MainContainer = new ContainerModel( - null, - { - id: 'main', - parentId: 'null', - x: 0, - y: 0, - width: DEFAULT_CONFIG.MainContainer.Width, - height: DEFAULT_CONFIG.MainContainer.Height, - fillOpacity: DEFAULT_CONFIG.MainContainer.Style.fillOpacity, - stroke: DEFAULT_CONFIG.MainContainer.Style.stroke, - } - ); - - // Save the configuration and the new MainContainer - // and default the selected container to it - this.setState({ - configuration: DEFAULT_CONFIG, - history: - [ - { - MainContainer, - SelectedContainer: MainContainer, - TypeCounters: {} - } - ], - historyCurrentStep: 0, - isLoaded: true - }); - }); - } - - public LoadEditor(files: FileList | null): void { - if (files === null) { - return; - } - const file = files[0]; - const reader = new FileReader(); - reader.addEventListener('load', () => { - const result = reader.result as string; - const editorState: IEditorState = JSON.parse(result); - - this.LoadState(editorState); - }); - reader.readAsText(file); - } - - private LoadState(editorState: IEditorState): void { - Revive(editorState); - - this.setState({ - configuration: editorState.configuration, - history: editorState.history, - historyCurrentStep: editorState.historyCurrentStep, - isLoaded: true - }); - } - - public render(): JSX.Element { - if (this.state.isLoaded) { - return ( -
- -
- ); - } else { - return ( -
- this.NewEditor()} - loadEditor={(files: FileList | null) => this.LoadEditor(files)} - /> -
- ); - } - } -} - -/** - * Fetch the configuration from the API - * @returns {Configation} The model of the configuration for the application - */ -export async function fetchConfiguration(): Promise { - const url = `${import.meta.env.VITE_API_URL}`; - // The test library cannot use the Fetch API - // @ts-expect-error - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (window.fetch) { - return await fetch(url, { - method: 'POST' - }) - .then(async(response) => - await response.json() - ) as Configuration; - } - return await new Promise((resolve) => { - const xhr = new XMLHttpRequest(); - xhr.open('POST', url, true); - xhr.onreadystatechange = function() { // Call a function when the state changes. - if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { - resolve(JSON.parse(this.responseText)); - } - }; - xhr.send(); - }); -} - - -const DEFAULT_CONFIG: Configuration = { - AvailableContainers: [ - { - Type: 'Container', - Width: 75, - Height: 100, - Style: { - fillOpacity: 0, - stroke: 'green' - } - } - ], - AvailableSymbols: [], - MainContainer: { - Type: 'Container', - Width: 2000, - Height: 100, - Style: { - fillOpacity: 0, - stroke: 'black' - } - } -} diff --git a/src/test/api.test.tsx b/src/Components/API/api.test.tsx similarity index 93% rename from src/test/api.test.tsx rename to src/Components/API/api.test.tsx index ed3a996..55d63b1 100644 --- a/src/test/api.test.tsx +++ b/src/Components/API/api.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { fetchConfiguration } from '../App'; +import { fetchConfiguration } from './api'; describe.concurrent('API test', () => { it('Load environment', () => { diff --git a/src/Components/API/api.ts b/src/Components/API/api.ts new file mode 100644 index 0000000..e113cc9 --- /dev/null +++ b/src/Components/API/api.ts @@ -0,0 +1,30 @@ +import { Configuration } from '../../Interfaces/Configuration'; + +/** + * Fetch the configuration from the API + * @returns {Configation} The model of the configuration for the application + */ +export async function fetchConfiguration(): Promise { + const url = `${import.meta.env.VITE_API_URL}`; + // The test library cannot use the Fetch API + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (window.fetch) { + return await fetch(url, { + method: 'POST' + }) + .then(async(response) => + await response.json() + ) as Configuration; + } + return await new Promise((resolve) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + xhr.onreadystatechange = function() { // Call a function when the state changes. + if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { + resolve(JSON.parse(this.responseText)); + } + }; + xhr.send(); + }); +} diff --git a/src/App.scss b/src/Components/App/App.scss similarity index 100% rename from src/App.scss rename to src/Components/App/App.scss diff --git a/src/Components/App/App.tsx b/src/Components/App/App.tsx new file mode 100644 index 0000000..fa725cf --- /dev/null +++ b/src/Components/App/App.tsx @@ -0,0 +1,79 @@ +import React, { useEffect, useState } from 'react'; +import './App.scss'; +import { MainMenu } from '../MainMenu/MainMenu'; +import { ContainerModel } from '../../Interfaces/ContainerModel'; +import Editor, { IEditorState } from '../Editor/Editor'; +import { LoadState } from './Load'; +import { LoadEditor, NewEditor } from './MenuActions'; +import { DEFAULT_CONFIG, DEFAULT_MAINCONTAINER_PROPS } from '../../utils/default'; + +// App will never have props +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface IAppProps { +} + +export const App: React.FunctionComponent = (props) => { + const [isLoaded, setLoaded] = useState(false); + + const defaultMainContainer = new ContainerModel( + null, + DEFAULT_MAINCONTAINER_PROPS + ); + + const [editorState, setEditorState] = useState({ + configuration: DEFAULT_CONFIG, + history: [{ + MainContainer: defaultMainContainer, + SelectedContainer: defaultMainContainer, + SelectedContainerId: defaultMainContainer.properties.id, + TypeCounters: {} + }], + historyCurrentStep: 0 + }); + + useEffect(() => { + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + const state = urlParams.get('state'); + + if (state === null) { + return; + } + + fetch(state) + .then( + async(response) => await response.json(), + (error) => { throw new Error(error); } + ) + .then((data: IEditorState) => { + LoadState(data, setEditorState, setLoaded); + }, (error) => { throw new Error(error); }); + }); + + if (isLoaded) { + return ( +
+ +
+ ); + } + + return ( +
+ NewEditor( + setEditorState, setLoaded + )} + loadEditor={(files: FileList | null) => LoadEditor( + files, + setEditorState, + setLoaded + )} + /> +
+ ); +}; diff --git a/src/Components/App/Load.ts b/src/Components/App/Load.ts new file mode 100644 index 0000000..b5459c7 --- /dev/null +++ b/src/Components/App/Load.ts @@ -0,0 +1,13 @@ +import { Dispatch, SetStateAction } from 'react'; +import { Revive } from '../../utils/saveload'; +import { IEditorState } from '../Editor/Editor'; + +export function LoadState( + editorState: IEditorState, + setEditorState: Dispatch>, + setLoaded: Dispatch> +): void { + Revive(editorState); + setEditorState(editorState); + setLoaded(true); +} diff --git a/src/Components/App/MenuActions.ts b/src/Components/App/MenuActions.ts new file mode 100644 index 0000000..ce0cd7d --- /dev/null +++ b/src/Components/App/MenuActions.ts @@ -0,0 +1,71 @@ +import { Dispatch, SetStateAction } from 'react'; +import { Configuration } from '../../Interfaces/Configuration'; +import { ContainerModel } from '../../Interfaces/ContainerModel'; +import { fetchConfiguration } from '../API/api'; +import { IEditorState } from '../Editor/Editor'; +import { LoadState } from './Load'; + +export function NewEditor( + setEditorState: Dispatch>, + setLoaded: Dispatch> +): void { + // Fetch the configuration from the API + fetchConfiguration() + .then((configuration: Configuration) => { + // Set the main container from the given properties of the API + const MainContainer = new ContainerModel( + null, + { + id: 'main', + parentId: 'null', + x: 0, + y: 0, + width: configuration.MainContainer.Width, + height: configuration.MainContainer.Height, + fillOpacity: 0, + stroke: 'black' + } + ); + + // Save the configuration and the new MainContainer + // and default the selected container to it + const editorState: IEditorState = { + configuration, + history: + [ + { + MainContainer, + SelectedContainer: MainContainer, + SelectedContainerId: MainContainer.properties.id, + TypeCounters: {} + } + ], + historyCurrentStep: 0 + }; + setEditorState(editorState); + setLoaded(true); + }, (error) => { + // TODO: Implement an alert component + console.warn('[NewEditor] Could not fetch resource from API. Using default.', error); + setLoaded(true); + }); +} + +export function LoadEditor( + files: FileList | null, + setEditorState: Dispatch>, + setLoaded: Dispatch> +): void { + if (files === null) { + return; + } + const file = files[0]; + const reader = new FileReader(); + reader.addEventListener('load', () => { + const result = reader.result as string; + const editorState: IEditorState = JSON.parse(result); + + LoadState(editorState, setEditorState, setLoaded); + }); + reader.readAsText(file); +} diff --git a/src/Components/Editor/ContainerOperations.ts b/src/Components/Editor/ContainerOperations.ts new file mode 100644 index 0000000..d9bdea5 --- /dev/null +++ b/src/Components/Editor/ContainerOperations.ts @@ -0,0 +1,269 @@ +import { Dispatch, SetStateAction } from 'react'; +import { HistoryState } from "../../Interfaces/HistoryState"; +import { Configuration } from '../../Interfaces/Configuration'; +import { ContainerModel, IContainerModel } from '../../Interfaces/ContainerModel'; +import { findContainerById } from '../../utils/itertools'; +import { getCurrentHistory } from './Editor'; + +/** + * Select a container + * @param container Selected container + */ +export function SelectContainer( + container: ContainerModel, + fullHistory: HistoryState[], + historyCurrentStep: number, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> +): void { + const history = getCurrentHistory(fullHistory, historyCurrentStep); + const current = history[history.length - 1]; + + if (current.MainContainer === null) { + throw new Error('[SelectContainer] Tried to select a container while there is no main container!'); + } + + const mainContainerClone = structuredClone(current.MainContainer); + const SelectedContainer = findContainerById(mainContainerClone, container.properties.id); + + if (SelectedContainer === undefined) { + throw new Error('[SelectContainer] Cannot find container among children of main container!'); + } + + setHistory(history.concat([{ + MainContainer: mainContainerClone, + TypeCounters: Object.assign({}, current.TypeCounters), + SelectedContainer, + SelectedContainerId: SelectedContainer.properties.id + }])); + setHistoryCurrentStep(history.length); +} + +export function DeleteContainer( + containerId: string, + fullHistory: HistoryState[], + historyCurrentStep: number, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> +): void { + const history = getCurrentHistory(fullHistory, historyCurrentStep); + const current = history[historyCurrentStep]; + + if (current.MainContainer === null) { + throw new Error('[DeleteContainer] Error: Tried to delete a container without a main container'); + } + + const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); + const container = findContainerById(mainContainerClone, containerId); + + if (container === undefined) { + throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`); + } + + if (container === mainContainerClone) { + // TODO: Implement alert + throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed !'); + } + + if (container === null || container === undefined) { + throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); + } + + if (container.parent != null) { + const index = container.parent.children.indexOf(container); + if (index > -1) { + container.parent.children.splice(index, 1); + } + } + + setHistory(history.concat([{ + SelectedContainer: null, + SelectedContainerId: '', + MainContainer: mainContainerClone, + TypeCounters: Object.assign({}, current.TypeCounters) + }])); + setHistoryCurrentStep(history.length); +} + +/** + * Add a new container to a selected container + * @param type The type of container + * @returns void + */ +export function AddContainerToSelectedContainer( + type: string, + configuration: Configuration, + fullHistory: HistoryState[], + historyCurrentStep: number, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> +): void { + const history = getCurrentHistory(fullHistory, historyCurrentStep); + const current = history[history.length - 1]; + + if (current.SelectedContainer === null || + current.SelectedContainer === undefined) { + return; + } + + const parent = current.SelectedContainer; + AddContainer( + parent.children.length, + type, + parent.properties.id, + configuration, + fullHistory, + historyCurrentStep, + setHistory, + setHistoryCurrentStep + ); +} + +export function AddContainer( + index: number, + type: string, + parentId: string, + configuration: Configuration, + fullHistory: HistoryState[], + historyCurrentStep: number, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> +): void { + const history = getCurrentHistory(fullHistory, historyCurrentStep); + const current = history[history.length - 1]; + + if (current.MainContainer === null || + current.MainContainer === undefined) { + return; + } + + // Get the preset properties from the API + const properties = configuration.AvailableContainers + .find(option => option.Type === type); + + if (properties === undefined) { + throw new Error(`[AddContainer] Object type not found. Found: ${type}`); + } + + // Set the counter of the object type in order to assign an unique id + const newCounters = Object.assign({}, current.TypeCounters); + if (newCounters[type] === null || + newCounters[type] === undefined) { + newCounters[type] = 0; + } else { + newCounters[type]++; + } + const count = newCounters[type]; + + // Create maincontainer model + const clone: IContainerModel = structuredClone(current.MainContainer); + + // Find the parent + const parentClone: IContainerModel | undefined = findContainerById( + clone, parentId + ); + + if (parentClone === null || parentClone === undefined) { + throw new Error('[AddContainer] Container model was not found among children of the main container!'); + } + + let x = 0; + if (index !== 0) { + const lastChild: IContainerModel | undefined = parentClone.children.at(index - 1); + if (lastChild !== undefined) { + x = lastChild.properties.x + Number(lastChild.properties.width); + } + } + + // Create the container + const newContainer = new ContainerModel( + parentClone, + { + id: `${type}-${count}`, + parentId: parentClone.properties.id, + x, + y: 0, + width: properties?.Width, + height: parentClone.properties.height, + ...properties.Style + }, + [], + { + type + } + ); + + // And push it the the parent children + if (index === parentClone.children.length) { + parentClone.children.push(newContainer); + } else { + parentClone.children.splice(index, 0, newContainer); + } + + // Update the state + setHistory(history.concat([{ + MainContainer: clone, + TypeCounters: newCounters, + SelectedContainer: parentClone, + SelectedContainerId: parentClone.properties.id + }])); + setHistoryCurrentStep(history.length); +} + +/** + * Handled the property change event in the properties form + * @param key Property name + * @param value New value of the property + * @returns void + */ +export function OnPropertyChange( + key: string, + value: string | number, + fullHistory: HistoryState[], + historyCurrentStep: number, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> +): void { + const history = getCurrentHistory(fullHistory, historyCurrentStep); + const current = history[history.length - 1]; + + if (current.SelectedContainer === null || + current.SelectedContainer === undefined) { + throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); + } + + if (current.MainContainer === null || + current.MainContainer === undefined) { + throw new Error('[OnPropertyChange] Property was changed before the main container was added'); + } + + if (parent === null) { + const selectedContainerClone: IContainerModel = structuredClone(current.SelectedContainer); + (selectedContainerClone.properties as any)[key] = value; + setHistory(history.concat([{ + SelectedContainer: selectedContainerClone, + SelectedContainerId: selectedContainerClone.properties.id, + MainContainer: selectedContainerClone, + TypeCounters: Object.assign({}, current.TypeCounters) + }])); + setHistoryCurrentStep(history.length); + return; + } + + const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); + const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); + + if (container === null || container === undefined) { + throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); + } + + (container.properties as any)[key] = value; + + setHistory(history.concat([{ + SelectedContainer: container, + SelectedContainerId: container.properties.id, + MainContainer: mainContainerClone, + TypeCounters: Object.assign({}, current.TypeCounters) + }])); + setHistoryCurrentStep(history.length); +} diff --git a/src/Editor.scss b/src/Components/Editor/Editor.scss similarity index 100% rename from src/Editor.scss rename to src/Components/Editor/Editor.scss diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx new file mode 100644 index 0000000..cf67ed7 --- /dev/null +++ b/src/Components/Editor/Editor.tsx @@ -0,0 +1,115 @@ +import React from 'react'; +import './Editor.scss'; +import { Configuration } from '../../Interfaces/Configuration'; +import { SVG } from '../SVG/SVG'; +import { HistoryState } from '../../Interfaces/HistoryState'; +import { UI } from '../UI/UI'; +import { SelectContainer, DeleteContainer, OnPropertyChange, AddContainerToSelectedContainer, AddContainer } from './ContainerOperations'; +import { SaveEditorAsJSON, SaveEditorAsSVG } from './Save'; +import { onKeyDown } from './Shortcuts'; + +interface IEditorProps { + configuration: Configuration + history: HistoryState[] + historyCurrentStep: number +} + +export interface IEditorState { + history: HistoryState[] + historyCurrentStep: number + configuration: Configuration +} + +export const getCurrentHistory = (history: HistoryState[], historyCurrentStep: number): HistoryState[] => history.slice(0, historyCurrentStep + 1); +export const getCurrentHistoryState = (history: HistoryState[], historyCurrentStep: number): HistoryState => history[historyCurrentStep]; + +const Editor: React.FunctionComponent = (props) => { + const [history, setHistory] = React.useState([...props.history]); + const [historyCurrentStep, setHistoryCurrentStep] = React.useState(0); + + React.useEffect(() => { + window.addEventListener('keyup', (event) => onKeyDown( + event, + history, + historyCurrentStep, + setHistoryCurrentStep + )); + + return () => { + window.removeEventListener('keyup', (event) => onKeyDown( + event, + history, + historyCurrentStep, + setHistoryCurrentStep + )); + }; + }); + + const configuration = props.configuration; + const current = getCurrentHistoryState(history, historyCurrentStep); + return ( +
+ SelectContainer( + container, + history, + historyCurrentStep, + setHistory, + setHistoryCurrentStep + )} + DeleteContainer={(containerId: string) => DeleteContainer( + containerId, + history, + historyCurrentStep, + setHistory, + setHistoryCurrentStep + )} + OnPropertyChange={(key, value) => OnPropertyChange( + key, value, + history, + historyCurrentStep, + setHistory, + setHistoryCurrentStep + )} + AddContainerToSelectedContainer={(type) => AddContainerToSelectedContainer( + type, + configuration, + history, + historyCurrentStep, + setHistory, + setHistoryCurrentStep + )} + AddContainer={(index, type, parentId) => AddContainer( + index, + type, + parentId, + configuration, + history, + historyCurrentStep, + setHistory, + setHistoryCurrentStep + )} + SaveEditorAsJSON={() => SaveEditorAsJSON( + history, + historyCurrentStep, + configuration + )} + SaveEditorAsSVG={() => SaveEditorAsSVG()} + LoadState={(move) => setHistoryCurrentStep(move)} + /> + + { current.MainContainer } + +
+ ); +}; + +export default Editor; diff --git a/src/Components/Editor/Save.ts b/src/Components/Editor/Save.ts new file mode 100644 index 0000000..91cee2f --- /dev/null +++ b/src/Components/Editor/Save.ts @@ -0,0 +1,41 @@ +import { HistoryState } from "../../Interfaces/HistoryState"; +import { Configuration } from '../../Interfaces/Configuration'; +import { getCircularReplacer } from '../../utils/saveload'; +import { ID } from '../SVG/SVG'; +import { IEditorState } from './Editor'; + +export function SaveEditorAsJSON( + history: HistoryState[], + historyCurrentStep: number, + configuration: Configuration +): void { + const exportName = 'state'; + const spaces = import.meta.env.DEV ? 4 : 0; + const editorState: IEditorState = { + history, + historyCurrentStep, + configuration + }; + const data = JSON.stringify(editorState, getCircularReplacer(), spaces); + const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`; + const downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.setAttribute('href', dataStr); + downloadAnchorNode.setAttribute('download', `${exportName}.json`); + document.body.appendChild(downloadAnchorNode); // required for firefox + downloadAnchorNode.click(); + downloadAnchorNode.remove(); +} + +export function SaveEditorAsSVG(): void { + const svgWrapper = document.getElementById(ID) as HTMLElement; + const svg = svgWrapper.querySelector('svg') as SVGSVGElement; + const preface = '\r\n'; + const svgBlob = new Blob([preface, svg.outerHTML], { type: 'image/svg+xml;charset=utf-8' }); + const svgUrl = URL.createObjectURL(svgBlob); + const downloadLink = document.createElement('a'); + downloadLink.href = svgUrl; + downloadLink.download = 'newesttree.svg'; + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); +} diff --git a/src/Components/Editor/Shortcuts.ts b/src/Components/Editor/Shortcuts.ts new file mode 100644 index 0000000..00b449e --- /dev/null +++ b/src/Components/Editor/Shortcuts.ts @@ -0,0 +1,23 @@ +import { Dispatch, SetStateAction } from 'react'; +import { HistoryState } from "../../Interfaces/HistoryState"; + +export function onKeyDown( + event: KeyboardEvent, + history: HistoryState[], + historyCurrentStep: number, + setHistoryCurrentStep: Dispatch> +): void { + event.preventDefault(); + if (event.isComposing || event.keyCode === 229) { + return; + } + if (event.key === 'z' && + event.ctrlKey && + historyCurrentStep > 0) { + setHistoryCurrentStep(historyCurrentStep - 1); + } else if (event.key === 'y' && + event.ctrlKey && + historyCurrentStep < history.length - 1) { + setHistoryCurrentStep(historyCurrentStep + 1); + } +} diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index bca12ba..230949d 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -5,22 +5,6 @@ import { ElementsSidebar } from './ElementsSidebar'; import { IContainerModel } from '../../Interfaces/ContainerModel'; describe.concurrent('Elements sidebar', () => { - it('No elements', () => { - render( {}} - SelectContainer={() => {}} - DeleteContainer={() => {}} - />); - - expect(screen.getByText(/Elements/i)); - expect(screen.queryByText('id')).toBeNull(); - expect(screen.queryByText(/main/i)).toBeNull(); - }); - it('With a MainContainer', () => { render( { onPropertyChange={() => {}} SelectContainer={() => {}} DeleteContainer={() => {}} + AddContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -72,6 +57,7 @@ describe.concurrent('Elements sidebar', () => { onPropertyChange={() => {}} SelectContainer={() => {}} DeleteContainer={() => {}} + AddContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -157,6 +143,7 @@ describe.concurrent('Elements sidebar', () => { onPropertyChange={() => {}} SelectContainer={() => {}} DeleteContainer={() => {}} + AddContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -210,6 +197,7 @@ describe.concurrent('Elements sidebar', () => { onPropertyChange={() => {}} SelectContainer={selectContainer} DeleteContainer={() => {}} + AddContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -232,6 +220,7 @@ describe.concurrent('Elements sidebar', () => { onPropertyChange={() => {}} SelectContainer={selectContainer} DeleteContainer={() => {}} + AddContainer={() => {}} />); expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy(); diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index 0159b4d..f449960 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -2,12 +2,14 @@ import * as React from 'react'; import { motion } from 'framer-motion'; import { Properties } from '../Properties/Properties'; import { IContainerModel } from '../../Interfaces/ContainerModel'; -import { findContainerById, getDepth, MakeIterator } from '../../utils/itertools'; +import { getDepth, MakeIterator } from '../../utils/itertools'; import { Menu } from '../Menu/Menu'; import { MenuItem } from '../Menu/MenuItem'; +import { handleDragLeave, handleDragOver, handleLeftClick, handleOnDrop, handleRightClick } from './MouseEventHandlers'; +import { Point } from '../../Interfaces/Point'; interface IElementsSidebarProps { - MainContainer: IContainerModel | null + MainContainer: IContainerModel isOpen: boolean isHistoryOpen: boolean SelectedContainer: IContainerModel | null @@ -17,248 +19,131 @@ interface IElementsSidebarProps { AddContainer: (index: number, type: string, parent: string) => void } -interface Point { - x: number - y: number -} +function createRows( + container: IContainerModel, + props: IElementsSidebarProps, + containerRows: React.ReactNode[] +): void { + const depth: number = getDepth(container); + const key = container.properties.id.toString(); + const text = '|\t'.repeat(depth) + key; + const selectedClass: string = props.SelectedContainer !== undefined && + props.SelectedContainer !== null && + props.SelectedContainer.properties.id === container.properties.id + ? 'border-l-4 bg-slate-400/60 hover:bg-slate-400' + : 'bg-slate-300/60 hover:bg-slate-300'; -interface IElementsSidebarState { - isContextMenuOpen: boolean - contextMenuPosition: Point - onClickContainerId: string -} - -export class ElementsSidebar extends React.PureComponent { - public state: IElementsSidebarState; - public elementRef: React.RefObject; - - constructor(props: IElementsSidebarProps) { - super(props); - this.state = { - isContextMenuOpen: false, - contextMenuPosition: { - x: 0, - y: 0 - }, - onClickContainerId: '' - }; - this.elementRef = React.createRef(); - } - - componentDidMount(): void { - this.elementRef.current?.addEventListener('contextmenu', (event) => this.handleRightClick(event)); - window.addEventListener('click', (event) => this.handleLeftClick(event)); - } - - componentWillUnmount(): void { - this.elementRef.current?.removeEventListener('contextmenu', (event) => this.handleRightClick(event)); - window.removeEventListener('click', (event) => this.handleLeftClick(event)); - } - - public handleRightClick(event: MouseEvent): void { - event.preventDefault(); - - if (!(event.target instanceof HTMLButtonElement)) { - this.setState({ - isContextMenuOpen: false, - onClickContainerId: '' - }); - return; - } - - const contextMenuPosition: Point = { x: event.pageX, y: event.pageY }; - this.setState({ - isContextMenuOpen: true, - contextMenuPosition, - onClickContainerId: event.target.id - }); - } - - public handleLeftClick(event: MouseEvent): void { - if (!this.state.isContextMenuOpen) { - return; - } - - this.setState({ - isContextMenuOpen: false, - onClickContainerId: '' - }); - } - - public handleDragOver(event: React.DragEvent): void { - event.preventDefault(); - const target: HTMLButtonElement = event.target as HTMLButtonElement; - const rect = target.getBoundingClientRect(); - const y = event.clientY - rect.top; // y position within the element. - - if (this.props.MainContainer === null) { - throw new Error('[handleOnDrop] Tried to drop into the tree without a required MainContainer!'); - } - - if (target.id === this.props.MainContainer.properties.id) { - target.classList.add('border-8'); - target.classList.remove('border-t-8'); - target.classList.remove('border-b-8'); - return; - } - - if (y < 12) { - target.classList.add('border-t-8'); - target.classList.remove('border-b-8'); - target.classList.remove('border-8'); - } else if (y < 24) { - target.classList.add('border-8'); - target.classList.remove('border-t-8'); - target.classList.remove('border-b-8'); - } else { - target.classList.add('border-b-8'); - target.classList.remove('border-8'); - target.classList.remove('border-t-8'); - } - } - - public handleOnDrop(event: React.DragEvent): void { - event.preventDefault(); - const type = event.dataTransfer.getData('type'); - const target: HTMLButtonElement = event.target as HTMLButtonElement; - removeBorderClasses(target); - - if (this.props.MainContainer === null) { - throw new Error('[handleOnDrop] Tried to drop into the tree without a required MainContainer!'); - } - - const targetContainer: IContainerModel | undefined = findContainerById( - this.props.MainContainer, - target.id - ); - - if (targetContainer === undefined) { - throw new Error('[handleOnDrop] Tried to drop onto a unknown container!'); - } - - if (targetContainer === this.props.MainContainer) { - // if the container is the root, only add type as child - this.props.AddContainer( - targetContainer.children.length, - type, - targetContainer.properties.id); - return; - } - - if (targetContainer.parent === null || - targetContainer.parent === undefined) { - throw new Error('[handleDrop] Tried to drop into a child container without a parent!'); - } - - const rect = target.getBoundingClientRect(); - const y = event.clientY - rect.top; // y position within the element. - - // locate the hitboxes - if (y < 12) { - const index = targetContainer.parent.children.indexOf(targetContainer); - this.props.AddContainer( - index, - type, - targetContainer.parent.properties.id - ); - } else if (y < 24) { - this.props.AddContainer( - targetContainer.children.length, - type, - targetContainer.properties.id); - } else { - const index = targetContainer.parent.children.indexOf(targetContainer); - this.props.AddContainer( - index + 1, - type, - targetContainer.parent.properties.id - ); - } - } - - public iterateChilds(handleContainer: (container: IContainerModel) => void): React.ReactNode { - if (this.props.MainContainer == null) { - return null; - } - - const it = MakeIterator(this.props.MainContainer); - for (const container of it) { - handleContainer(container); - } - } - - public render(): JSX.Element { - let isOpenClasses = '-right-64'; - if (this.props.isOpen) { - isOpenClasses = this.props.isHistoryOpen - ? 'right-64' - : 'right-0'; - } - - const containerRows: React.ReactNode[] = []; - this.iterateChilds((container: IContainerModel) => { - const depth: number = getDepth(container); - const key = container.properties.id.toString(); - const text = '|\t'.repeat(depth) + key; - const selectedClass: string = this.props.SelectedContainer !== undefined && - this.props.SelectedContainer !== null && - this.props.SelectedContainer.properties.id === container.properties.id - ? 'border-l-4 bg-slate-400/60 hover:bg-slate-400' - : 'bg-slate-300/60 hover:bg-slate-300'; - containerRows.push( - this.handleOnDrop(event)} - onDragOver={(event) => this.handleDragOver(event)} - onDragLeave={(event) => handleDragLeave(event)} - onClick={() => this.props.SelectContainer(container)} - > - { text } - - ); - }); + } + id={key} + key={key} + onDrop={(event) => handleOnDrop(event, props.MainContainer, props.AddContainer)} + onDragOver={(event) => handleDragOver(event, props.MainContainer)} + onDragLeave={(event) => handleDragLeave(event)} + onClick={() => props.SelectContainer(container)} + > + { text } + + ); +}; - return ( -
-
- Elements -
-
- { containerRows } -
- - this.props.DeleteContainer(this.state.onClickContainerId)} /> - - -
+export const ElementsSidebar: React.FC = (props: IElementsSidebarProps): JSX.Element => { + // States + const [isContextMenuOpen, setIsContextMenuOpen] = React.useState(false); + const [onClickContainerId, setOnClickContainerId] = React.useState(''); + const [contextMenuPosition, setContextMenuPosition] = React.useState({ + x: 0, + y: 0 + }); + + const elementRef = React.useRef(null); + + // Event listeners + React.useEffect(() => { + elementRef.current?.addEventListener( + 'contextmenu', + (event) => handleRightClick( + event, + setIsContextMenuOpen, + setOnClickContainerId, + setContextMenuPosition + )); + + window.addEventListener( + 'click', + (event) => handleLeftClick( + isContextMenuOpen, + setIsContextMenuOpen, + setOnClickContainerId + )); + + return () => { + elementRef.current?.addEventListener( + 'contextmenu', + (event) => handleRightClick( + event, + setIsContextMenuOpen, + setOnClickContainerId, + setContextMenuPosition + )); + + window.removeEventListener( + 'click', + (event) => handleLeftClick( + isContextMenuOpen, + setIsContextMenuOpen, + setOnClickContainerId + )); + }; + }, []); + + // Render + let isOpenClasses = '-right-64'; + if (props.isOpen) { + isOpenClasses = props.isHistoryOpen + ? 'right-64' + : 'right-0'; + } + + const containerRows: React.ReactNode[] = []; + + const it = MakeIterator(props.MainContainer); + for (const container of it) { + createRows( + container, + props, + containerRows ); } -} -function removeBorderClasses(target: HTMLButtonElement): void { - target.classList.remove('border-t-8'); - target.classList.remove('border-8'); - target.classList.remove('border-b-8'); -} - -function handleDragLeave(event: React.DragEvent): void { - const target: HTMLButtonElement = event.target as HTMLButtonElement; - removeBorderClasses(target); -} + return ( +
+
+ Elements +
+
+ { containerRows } +
+ + props.DeleteContainer(onClickContainerId)} /> + + +
+ ); +}; diff --git a/src/Components/ElementsSidebar/MouseEventHandlers.ts b/src/Components/ElementsSidebar/MouseEventHandlers.ts new file mode 100644 index 0000000..53fb14e --- /dev/null +++ b/src/Components/ElementsSidebar/MouseEventHandlers.ts @@ -0,0 +1,137 @@ +import { IContainerModel } from '../../Interfaces/ContainerModel'; +import { Point } from '../../Interfaces/Point'; +import { findContainerById } from '../../utils/itertools'; + +export function handleRightClick( + event: MouseEvent, + setIsContextMenuOpen: React.Dispatch>, + setOnClickContainerId: React.Dispatch>, + setContextMenuPosition: React.Dispatch> +): void { + event.preventDefault(); + + if (!(event.target instanceof HTMLButtonElement)) { + setIsContextMenuOpen(false); + setOnClickContainerId(''); + return; + } + + const contextMenuPosition: Point = { x: event.pageX, y: event.pageY }; + setIsContextMenuOpen(true); + setOnClickContainerId(event.target.id); + setContextMenuPosition(contextMenuPosition); +} + +export function handleLeftClick( + isContextMenuOpen: boolean, + setIsContextMenuOpen: React.Dispatch>, + setOnClickContainerId: React.Dispatch> +): void { + if (!isContextMenuOpen) { + return; + } + + setIsContextMenuOpen(false); + setOnClickContainerId(''); +} + +export function removeBorderClasses(target: HTMLButtonElement): void { + target.classList.remove('border-t-8'); + target.classList.remove('border-8'); + target.classList.remove('border-b-8'); +} + +export function handleDragLeave(event: React.DragEvent): void { + const target: HTMLButtonElement = event.target as HTMLButtonElement; + removeBorderClasses(target); +} + +export function handleDragOver( + event: React.DragEvent, + mainContainer: IContainerModel +): void { + event.preventDefault(); + const target: HTMLButtonElement = event.target as HTMLButtonElement; + const rect = target.getBoundingClientRect(); + const y = event.clientY - rect.top; // y position within the element. + + if (target.id === mainContainer.properties.id) { + target.classList.add('border-8'); + target.classList.remove('border-t-8'); + target.classList.remove('border-b-8'); + return; + } + + if (y < 12) { + target.classList.add('border-t-8'); + target.classList.remove('border-b-8'); + target.classList.remove('border-8'); + } else if (y < 24) { + target.classList.add('border-8'); + target.classList.remove('border-t-8'); + target.classList.remove('border-b-8'); + } else { + target.classList.add('border-b-8'); + target.classList.remove('border-8'); + target.classList.remove('border-t-8'); + } +} + +export function handleOnDrop( + event: React.DragEvent, + mainContainer: IContainerModel, + addContainer: (index: number, type: string, parent: string) => void +): void { + event.preventDefault(); + const type = event.dataTransfer.getData('type'); + const target: HTMLButtonElement = event.target as HTMLButtonElement; + removeBorderClasses(target); + + const targetContainer: IContainerModel | undefined = findContainerById( + mainContainer, + target.id + ); + + if (targetContainer === undefined) { + throw new Error('[handleOnDrop] Tried to drop onto a unknown container!'); + } + + if (targetContainer === mainContainer) { + // if the container is the root, only add type as child + addContainer( + targetContainer.children.length, + type, + targetContainer.properties.id); + return; + } + + if (targetContainer.parent === null || + targetContainer.parent === undefined) { + throw new Error('[handleDrop] Tried to drop into a child container without a parent!'); + } + + const rect = target.getBoundingClientRect(); + const y = event.clientY - rect.top; // y position within the element. + + // locate the hitboxes + if (y < 12) { + const index = targetContainer.parent.children.indexOf(targetContainer); + addContainer( + index, + type, + targetContainer.parent.properties.id + ); + } else if (y < 24) { + addContainer( + targetContainer.children.length, + type, + targetContainer.properties.id); + } else { + const index = targetContainer.parent.children.indexOf(targetContainer); + addContainer( + index + 1, + type, + targetContainer.parent.properties.id + ); + } +} diff --git a/src/Components/FloatingButton/FloatingButton.tsx b/src/Components/FloatingButton/FloatingButton.tsx index d55f01b..65f491e 100644 --- a/src/Components/FloatingButton/FloatingButton.tsx +++ b/src/Components/FloatingButton/FloatingButton.tsx @@ -13,7 +13,7 @@ const toggleState = ( setHidden(!isHidden); }; -const FloatingButton: React.FC = (props: IFloatingButtonProps) => { +export const FloatingButton: React.FC = (props: IFloatingButtonProps) => { const [isHidden, setHidden] = React.useState(true); const buttonListClasses = isHidden ? 'invisible opacity-0' : 'visible opacity-100'; const icon = isHidden @@ -34,5 +34,3 @@ const FloatingButton: React.FC = (props: IFloatingButtonPr
); }; - -export default FloatingButton; diff --git a/src/Components/History/History.tsx b/src/Components/History/History.tsx index c01298d..067a867 100644 --- a/src/Components/History/History.tsx +++ b/src/Components/History/History.tsx @@ -1,58 +1,56 @@ import * as React from 'react'; -import { IHistoryState } from '../../App'; +import { HistoryState } from "../../Interfaces/HistoryState"; interface IHistoryProps { - history: IHistoryState[] + history: HistoryState[] historyCurrentStep: number isOpen: boolean jumpTo: (move: number) => void } -export class History extends React.PureComponent { - public render(): JSX.Element { - const isOpenClasses = this.props.isOpen ? 'right-0' : '-right-64'; +export const History: React.FC = (props: IHistoryProps) => { + const isOpenClasses = props.isOpen ? 'right-0' : '-right-64'; - const states = this.props.history.map((step, move) => { - const desc = move > 0 - ? `Go to modification n°${move}` - : 'Go to the beginning'; + const states = props.history.map((step, move) => { + const desc = move > 0 + ? `Go to modification n°${move}` + : 'Go to the beginning'; - const isCurrent = move === this.props.historyCurrentStep; + const isCurrent = move === props.historyCurrentStep; - const selectedClass = isCurrent - ? 'bg-blue-500 hover:bg-blue-600' - : 'bg-slate-500 hover:bg-slate-700'; - - const isCurrentText = isCurrent - ? ' (current)' - : ''; - return ( - - - ); - }); - - // recent first - states.reverse(); + const selectedClass = isCurrent + ? 'bg-blue-500 hover:bg-blue-600' + : 'bg-slate-500 hover:bg-slate-700'; + const isCurrentText = isCurrent + ? ' (current)' + : ''; return ( -
-
- Timeline -
-
- { states } -
-
+ + ); - } -} + }); + + // recent first + states.reverse(); + + return ( +
+
+ Timeline +
+
+ { states } +
+
+ ); +}; diff --git a/src/Components/MainMenu/MainMenu.tsx b/src/Components/MainMenu/MainMenu.tsx index a9b3da2..1d84f9a 100644 --- a/src/Components/MainMenu/MainMenu.tsx +++ b/src/Components/MainMenu/MainMenu.tsx @@ -36,18 +36,6 @@ export const MainMenu: React.FC = (props) => { "/> - {/* */}
+ ); +}; diff --git a/src/Components/SVG/Elements/Container.tsx b/src/Components/SVG/Elements/Container.tsx index e64739c..02dea6f 100644 --- a/src/Components/SVG/Elements/Container.tsx +++ b/src/Components/SVG/Elements/Container.tsx @@ -9,70 +9,68 @@ export interface IContainerProps { const GAP = 50; -export class Container extends React.PureComponent { - /** - * Render the container - * @returns Render the container - */ - public render(): React.ReactNode { - const containersElements = this.props.model.children.map(child => ); - const xText = Number(this.props.model.properties.width) / 2; - const yText = Number(this.props.model.properties.height) / 2; - const transform = `translate(${Number(this.props.model.properties.x)}, ${Number(this.props.model.properties.y)})`; +/** + * Render the container + * @returns Render the container + */ +export const Container: React.FC = (props: IContainerProps) => { + const containersElements = props.model.children.map(child => ); + const xText = Number(props.model.properties.width) / 2; + const yText = Number(props.model.properties.height) / 2; + const transform = `translate(${Number(props.model.properties.x)}, ${Number(props.model.properties.y)})`; - // g style - const defaultStyle: React.CSSProperties = { - transitionProperty: 'all', - transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', - transitionDuration: '150ms' - }; + // g style + const defaultStyle: React.CSSProperties = { + transitionProperty: 'all', + transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', + transitionDuration: '150ms' + }; - // Rect style - const style = Object.assign( - JSON.parse(JSON.stringify(defaultStyle)), - this.props.model.properties - ); - style.x = 0; - style.y = 0; - delete style.height; - delete style.width; + // Rect style + const style = Object.assign( + JSON.parse(JSON.stringify(defaultStyle)), + props.model.properties + ); + style.x = 0; + style.y = 0; + delete style.height; + delete style.width; - // Dimension props - const id = `dim-${this.props.model.properties.id}`; - const xStart: number = 0; - const xEnd = Number(this.props.model.properties.width); - const y = -(GAP * (getDepth(this.props.model) + 1)); - const strokeWidth = 1; - const text = (this.props.model.properties.width ?? 0).toString(); + // Dimension props + const id = `dim-${props.model.properties.id}`; + const xStart: number = 0; + const xEnd = Number(props.model.properties.width); + const y = -(GAP * (getDepth(props.model) + 1)); + const strokeWidth = 1; + const text = (props.model.properties.width ?? 0).toString(); - return ( - + + - - - - - {this.props.model.properties.id} - - { containersElements } - - ); - } -} + + + {props.model.properties.id} + + { containersElements } + + ); +}; diff --git a/src/Components/SVG/Elements/Dimension.tsx b/src/Components/SVG/Elements/Dimension.tsx index 612b3f7..ec51873 100644 --- a/src/Components/SVG/Elements/Dimension.tsx +++ b/src/Components/SVG/Elements/Dimension.tsx @@ -9,44 +9,42 @@ interface IDimensionProps { strokeWidth: number } -export class Dimension extends React.PureComponent { - public render(): JSX.Element { - const style: React.CSSProperties = { - stroke: 'black' - }; - return ( - - - - - - {this.props.text} - - - ); - } -} +export const Dimension: React.FC = (props: IDimensionProps) => { + const style: React.CSSProperties = { + stroke: 'black' + }; + return ( + + + + + + {props.text} + + + ); +}; diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx index a2e0e20..aaa3b22 100644 --- a/src/Components/SVG/SVG.tsx +++ b/src/Components/SVG/SVG.tsx @@ -12,74 +12,69 @@ interface ISVGProps { selected: ContainerModel | null } -interface ISVGState { +interface Viewer { viewerWidth: number viewerHeight: number } -export class SVG extends React.PureComponent { - public static ID = 'svg'; - public state: ISVGState; +export const ID = 'svg'; - constructor(props: ISVGProps) { - super(props); - this.state = { - viewerWidth: window.innerWidth - BAR_WIDTH, - viewerHeight: window.innerHeight - }; - } - - resizeViewBox(): void { - this.setState({ - viewerWidth: window.innerWidth - BAR_WIDTH, - viewerHeight: window.innerHeight - }); - } - - componentDidMount(): void { - window.addEventListener('resize', () => this.resizeViewBox()); - } - - componentWillUnmount(): void { - window.removeEventListener('resize', () => this.resizeViewBox()); - } - - render(): JSX.Element { - const xmlns = ''; - - const properties = { - width: this.props.width, - height: this.props.height, - xmlns - }; - - let children: React.ReactNode | React.ReactNode[] = []; - if (Array.isArray(this.props.children)) { - children = this.props.children.map(child => ); - } else if (this.props.children !== null) { - children = ; - } - - return ( -
- - - { children } - - - -
- ); - }; +function resizeViewBox( + setViewer: React.Dispatch> +): void { + setViewer({ + viewerWidth: window.innerWidth - BAR_WIDTH, + viewerHeight: window.innerHeight + }); } + +export const SVG: React.FC = (props: ISVGProps) => { + const [viewer, setViewer] = React.useState({ + viewerWidth: window.innerWidth, + viewerHeight: window.innerHeight + }); + + React.useEffect(() => { + window.addEventListener('resize', () => resizeViewBox(setViewer)); + + return () => { + window.addEventListener('resize', () => resizeViewBox(setViewer)); + }; + }); + + const xmlns = ''; + const properties = { + width: props.width, + height: props.height, + xmlns + }; + + let children: React.ReactNode | React.ReactNode[] = []; + if (Array.isArray(props.children)) { + children = props.children.map(child => ); + } else if (props.children !== null) { + children = ; + } + + return ( +
+ + + { children } + + + +
+ ); +}; diff --git a/src/Components/Sidebar/Sidebar.test.tsx b/src/Components/Sidebar/Sidebar.test.tsx index 0020c9e..886496e 100644 --- a/src/Components/Sidebar/Sidebar.test.tsx +++ b/src/Components/Sidebar/Sidebar.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { describe, it, expect, vi } from 'vitest'; import { fireEvent, render, screen } from '../../utils/test-utils'; -import Sidebar from './Sidebar'; +import { Sidebar } from './Sidebar'; describe.concurrent('Sidebar', () => { it('Start default', () => { diff --git a/src/Components/Sidebar/Sidebar.tsx b/src/Components/Sidebar/Sidebar.tsx index de06e61..dc2f1a7 100644 --- a/src/Components/Sidebar/Sidebar.tsx +++ b/src/Components/Sidebar/Sidebar.tsx @@ -12,35 +12,33 @@ function handleDragStart(event: React.DragEvent): void { event.dataTransfer.setData('type', (event.target as HTMLButtonElement).id); } -export default class Sidebar extends React.PureComponent { - public render(): JSX.Element { - const listElements = this.props.componentOptions.map(componentOption => - - ); +export const Sidebar: React.FC = (props: ISidebarProps) => { + const listElements = props.componentOptions.map(componentOption => + + ); - const isOpenClasses = this.props.isOpen ? 'left-16' : '-left-64'; - return ( -
-
+
Components -
-
- {listElements} -
- ); - } -} +
+ {listElements} +
+
+ ); +}; diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index e0d9b6c..7422471 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -1,17 +1,17 @@ import * as React from 'react'; import { ElementsSidebar } from '../ElementsSidebar/ElementsSidebar'; -import Sidebar from '../Sidebar/Sidebar'; +import { Sidebar } from '../Sidebar/Sidebar'; import { History } from '../History/History'; import { AvailableContainer } from '../../Interfaces/AvailableContainer'; import { ContainerModel } from '../../Interfaces/ContainerModel'; -import { IHistoryState } from '../../App'; +import { HistoryState } from "../../Interfaces/HistoryState"; import { PhotographIcon, UploadIcon } from '@heroicons/react/outline'; -import FloatingButton from '../FloatingButton/FloatingButton'; +import { FloatingButton } from '../FloatingButton/FloatingButton'; import { Bar } from '../Bar/Bar'; interface IUIProps { - current: IHistoryState - history: IHistoryState[] + current: HistoryState + history: HistoryState[] historyCurrentStep: number AvailableContainers: AvailableContainer[] SelectContainer: (container: ContainerModel) => void @@ -24,108 +24,70 @@ interface IUIProps { LoadState: (move: number) => void } -interface IUIState { - isSidebarOpen: boolean - isElementsSidebarOpen: boolean - isHistoryOpen: boolean -} +export const UI: React.FunctionComponent = (props: IUIProps) => { + const [isSidebarOpen, setIsSidebarOpen] = React.useState(true); + const [isElementsSidebarOpen, setIsElementsSidebarOpen] = React.useState(false); + const [isHistoryOpen, setIsHistoryOpen] = React.useState(false); -export class UI extends React.PureComponent { - constructor(props: IUIProps) { - super(props); - this.state = { - isSidebarOpen: true, - isElementsSidebarOpen: false, - isHistoryOpen: false - }; + let buttonRightOffsetClasses = 'right-12'; + if (isElementsSidebarOpen || isHistoryOpen) { + buttonRightOffsetClasses = 'right-72'; + } + if (isHistoryOpen && isElementsSidebarOpen) { + buttonRightOffsetClasses = 'right-[544px]'; } - /** - * Toggle the components sidebar - */ - public ToggleSidebar(): void { - this.setState({ - isSidebarOpen: !this.state.isSidebarOpen - }); - } + return ( + <> + setIsElementsSidebarOpen(!isElementsSidebarOpen)} + ToggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)} + ToggleTimeline={() => setIsHistoryOpen(!isHistoryOpen)} + /> - /** - * Toggle the elements - */ - public ToggleElementsSidebar(): void { - this.setState({ - isElementsSidebarOpen: !this.state.isElementsSidebarOpen - }); - } + props.AddContainerToSelectedContainer(type)} + /> + + - /** - * Toggle the elements - */ - public ToggleTimeline(): void { - this.setState({ - isHistoryOpen: !this.state.isHistoryOpen - }); - } + + + + + + ); +}; - public render(): JSX.Element { - let buttonRightOffsetClasses = 'right-12'; - if (this.state.isElementsSidebarOpen || this.state.isHistoryOpen) { - buttonRightOffsetClasses = 'right-72'; - } - if (this.state.isHistoryOpen && this.state.isElementsSidebarOpen) { - buttonRightOffsetClasses = 'right-[544px]'; - } - - return ( - <> - this.ToggleElementsSidebar()} - ToggleSidebar={() => this.ToggleSidebar()} - ToggleTimeline={() => this.ToggleTimeline()} - /> - - this.props.AddContainerToSelectedContainer(type)} - /> - - - - - - - - - ); - } -} +export default UI; diff --git a/src/Editor.tsx b/src/Editor.tsx deleted file mode 100644 index b983744..0000000 --- a/src/Editor.tsx +++ /dev/null @@ -1,362 +0,0 @@ -import React from 'react'; -import './Editor.scss'; -import { Configuration } from './Interfaces/Configuration'; -import { SVG } from './Components/SVG/SVG'; -import { ContainerModel, IContainerModel } from './Interfaces/ContainerModel'; -import { findContainerById, MakeIterator } from './utils/itertools'; -import { IHistoryState } from './App'; -import { getCircularReplacer } from './utils/saveload'; -import { UI } from './Components/UI/UI'; - -interface IEditorProps { - configuration: Configuration - history: IHistoryState[] - historyCurrentStep: number -} - -export interface IEditorState { - history: IHistoryState[] - historyCurrentStep: number - // do not use it, use props.configuration - // only used for serialization purpose - configuration: Configuration -} - -class Editor extends React.Component { - public state: IEditorState; - - constructor(props: IEditorProps) { - super(props); - this.state = { - configuration: Object.assign({}, props.configuration), - history: [...props.history], - historyCurrentStep: props.historyCurrentStep - }; - } - - componentDidMount(): void { - window.addEventListener('keyup', (event) => this.onKeyDown(event)); - } - - componentWillUnmount(): void { - window.removeEventListener('keyup', (event) => this.onKeyDown(event)); - } - - public onKeyDown(event: KeyboardEvent): void { - event.preventDefault(); - if (event.isComposing || event.keyCode === 229) { - return; - } - if (event.key === 'z' && - event.ctrlKey && - this.state.historyCurrentStep > 0) { - this.LoadState(this.state.historyCurrentStep - 1); - } else if (event.key === 'y' && - event.ctrlKey && - this.state.historyCurrentStep < this.state.history.length - 1) { - this.LoadState(this.state.historyCurrentStep + 1); - } - } - - public getCurrentHistory = (): IHistoryState[] => this.state.history.slice(0, this.state.historyCurrentStep + 1); - public getCurrentHistoryState = (): IHistoryState => this.state.history[this.state.historyCurrentStep]; - - /** - * Select a container - * @param container Selected container - */ - public SelectContainer(container: ContainerModel): void { - const history = this.getCurrentHistory(); - const current = history[history.length - 1]; - - if (current.MainContainer === null) { - throw new Error('[SelectContainer] Tried to select a container while there is no main container!'); - } - - const mainContainerClone = structuredClone(current.MainContainer); - const SelectedContainer = findContainerById(mainContainerClone, container.properties.id); - - if (SelectedContainer === undefined) { - throw new Error('[SelectContainer] Cannot find container among children of main container!'); - } - - this.setState({ - history: history.concat([{ - MainContainer: mainContainerClone, - TypeCounters: Object.assign({}, current.TypeCounters), - SelectedContainer, - SelectedContainerId: SelectedContainer.properties.id - }]), - historyCurrentStep: history.length - }); - } - - public DeleteContainer(containerId: string): void { - const history = this.getCurrentHistory(); - const current = history[this.state.historyCurrentStep]; - - if (current.MainContainer === null) { - throw new Error('[DeleteContainer] Error: Tried to delete a container without a main container'); - } - - const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); - const container = findContainerById(mainContainerClone, containerId); - - if (container === undefined) { - throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`); - } - - if (container === mainContainerClone) { - // TODO: Implement alert - throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed !'); - } - - if (container === null || container === undefined) { - throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); - } - - if (container.parent != null) { - const index = container.parent.children.indexOf(container); - if (index > -1) { - container.parent.children.splice(index, 1); - } - } - - this.setState( - { - history: history.concat([{ - SelectedContainer: null, - SelectedContainerId: '', - MainContainer: mainContainerClone, - TypeCounters: Object.assign({}, current.TypeCounters) - }]), - historyCurrentStep: history.length - }); - } - - /** - * Handled the property change event in the properties form - * @param key Property name - * @param value New value of the property - * @returns void - */ - public OnPropertyChange(key: string, value: string | number): void { - const history = this.getCurrentHistory(); - const current = history[history.length - 1]; - - if (current.SelectedContainer === null || - current.SelectedContainer === undefined) { - throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); - } - - if (current.MainContainer === null || - current.MainContainer === undefined) { - throw new Error('[OnPropertyChange] Property was changed before the main container was added'); - } - - if (parent === null) { - const selectedContainerClone: IContainerModel = structuredClone(current.SelectedContainer); - (selectedContainerClone.properties as any)[key] = value; - this.setState({ - history: history.concat([{ - SelectedContainer: selectedContainerClone, - SelectedContainerId: selectedContainerClone.properties.id, - MainContainer: selectedContainerClone, - TypeCounters: Object.assign({}, current.TypeCounters) - }]), - historyCurrentStep: history.length - }); - return; - } - - const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); - const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); - - if (container === null || container === undefined) { - throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); - } - - (container.properties as any)[key] = value; - - this.setState( - { - history: history.concat([{ - SelectedContainer: container, - SelectedContainerId: container.properties.id, - MainContainer: mainContainerClone, - TypeCounters: Object.assign({}, current.TypeCounters) - }]), - historyCurrentStep: history.length - }); - } - - /** - * Add a new container to a selected container - * @param type The type of container - * @returns void - */ - public AddContainerToSelectedContainer(type: string): void { - const history = this.getCurrentHistory(); - const current = history[history.length - 1]; - - if (current.SelectedContainer === null || - current.SelectedContainer === undefined) { - return; - } - - const parent = current.SelectedContainer; - this.AddContainer(parent.children.length, type, parent.properties.id); - } - - public AddContainer(index: number, type: string, parentId: string): void { - const history = this.getCurrentHistory(); - const current = history[history.length - 1]; - - if (current.MainContainer === null || - current.MainContainer === undefined) { - return; - } - - // Get the preset properties from the API - const properties = this.props.configuration.AvailableContainers - .find(option => option.Type === type); - - if (properties === undefined) { - throw new Error(`[AddContainer] Object type not found. Found: ${type}`); - } - - // Set the counter of the object type in order to assign an unique id - const newCounters = Object.assign({}, current.TypeCounters); - if (newCounters[type] === null || - newCounters[type] === undefined) { - newCounters[type] = 0; - } else { - newCounters[type]++; - } - const count = newCounters[type]; - - // Create maincontainer model - const clone: IContainerModel = structuredClone(current.MainContainer); - - // Find the parent - const parentClone: IContainerModel | undefined = findContainerById( - clone, parentId - ); - - if (parentClone === null || parentClone === undefined) { - throw new Error('[AddContainer] Container model was not found among children of the main container!'); - } - - let x = 0; - if (index !== 0) { - const lastChild: IContainerModel | undefined = parentClone.children.at(index - 1); - if (lastChild !== undefined) { - x = lastChild.properties.x + Number(lastChild.properties.width); - } - } - - // Create the container - const newContainer = new ContainerModel( - parentClone, - { - id: `${type}-${count}`, - parentId: parentClone.properties.id, - x, - y: 0, - width: properties?.Width, - height: parentClone.properties.height, - ...properties.Style - }, - [], - { - type - } - ); - - // And push it the the parent children - if (index === parentClone.children.length) { - parentClone.children.push(newContainer); - } else { - parentClone.children.splice(index, 0, newContainer); - } - - // Update the state - this.setState({ - history: history.concat([{ - MainContainer: clone, - TypeCounters: newCounters, - SelectedContainer: parentClone, - SelectedContainerId: parentClone.properties.id - }]), - historyCurrentStep: history.length - }); - } - - public LoadState(move: number): void { - this.setState({ - historyCurrentStep: move - }); - } - - public SaveEditorAsJSON(): void { - const exportName = 'state'; - const spaces = import.meta.env.DEV ? 4 : 0; - const data = JSON.stringify(this.state, getCircularReplacer(), spaces); - const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`; - const downloadAnchorNode = document.createElement('a'); - downloadAnchorNode.setAttribute('href', dataStr); - downloadAnchorNode.setAttribute('download', `${exportName}.json`); - document.body.appendChild(downloadAnchorNode); // required for firefox - downloadAnchorNode.click(); - downloadAnchorNode.remove(); - } - - public SaveEditorAsSVG(): void { - const svgWrapper = document.getElementById(SVG.ID) as HTMLElement; - const svg = svgWrapper.querySelector('svg') as SVGSVGElement; - const preface = '\r\n'; - const svgBlob = new Blob([preface, svg.outerHTML], { type: 'image/svg+xml;charset=utf-8' }); - const svgUrl = URL.createObjectURL(svgBlob); - const downloadLink = document.createElement('a'); - downloadLink.href = svgUrl; - downloadLink.download = 'newesttree.svg'; - document.body.appendChild(downloadLink); - downloadLink.click(); - document.body.removeChild(downloadLink); - } - - /** - * Render the application - * @returns {JSX.Element} Rendered JSX element - */ - render(): JSX.Element { - const current = this.getCurrentHistoryState(); - return ( -
- this.SelectContainer(container)} - DeleteContainer={(containerId: string) => this.DeleteContainer(containerId)} - OnPropertyChange={(key, value) => this.OnPropertyChange(key, value)} - AddContainerToSelectedContainer={(type) => this.AddContainerToSelectedContainer(type)} - AddContainer={(index, type, parentId) => this.AddContainer(index, type, parentId)} - SaveEditorAsJSON={() => this.SaveEditorAsJSON()} - SaveEditorAsSVG={() => this.SaveEditorAsSVG()} - LoadState={(move) => this.LoadState(move)} - /> - - { current.MainContainer } - -
- ); - } -} - -export default Editor; diff --git a/src/Interfaces/HistoryState.ts b/src/Interfaces/HistoryState.ts new file mode 100644 index 0000000..aa51638 --- /dev/null +++ b/src/Interfaces/HistoryState.ts @@ -0,0 +1,8 @@ +import { IContainerModel } from './ContainerModel'; + +export interface HistoryState { + MainContainer: IContainerModel + SelectedContainer: IContainerModel | null + SelectedContainerId: string + TypeCounters: Record +} diff --git a/src/Interfaces/Point.ts b/src/Interfaces/Point.ts new file mode 100644 index 0000000..43fd673 --- /dev/null +++ b/src/Interfaces/Point.ts @@ -0,0 +1,4 @@ +export interface Point { + x: number + y: number +} diff --git a/src/main.tsx b/src/main.tsx index 6893f34..5399af1 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import { App } from './App'; +import { App } from './Components/App/App'; import './index.scss'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( diff --git a/src/utils/default.ts b/src/utils/default.ts new file mode 100644 index 0000000..60371fc --- /dev/null +++ b/src/utils/default.ts @@ -0,0 +1,37 @@ +import { Configuration } from '../Interfaces/Configuration'; +import Properties from '../Interfaces/Properties'; + +export const DEFAULT_CONFIG: Configuration = { + AvailableContainers: [ + { + Type: 'Container', + Width: 75, + Height: 100, + Style: { + fillOpacity: 0, + stroke: 'green' + } + } + ], + AvailableSymbols: [], + MainContainer: { + Type: 'Container', + Width: 2000, + Height: 100, + Style: { + fillOpacity: 0, + stroke: 'black' + } + } +}; + +export const DEFAULT_MAINCONTAINER_PROPS: Properties = { + id: 'main', + parentId: 'null', + x: 0, + y: 0, + width: DEFAULT_CONFIG.MainContainer.Width, + height: DEFAULT_CONFIG.MainContainer.Height, + fillOpacity: 0, + stroke: 'black' +}; diff --git a/src/utils/saveload.ts b/src/utils/saveload.ts index 8993ad4..de8b835 100644 --- a/src/utils/saveload.ts +++ b/src/utils/saveload.ts @@ -1,5 +1,5 @@ import { findContainerById, MakeIterator } from './itertools'; -import { IEditorState } from '../Editor'; +import { IEditorState } from '../Components/Editor/Editor'; /** * Revive the Editor state