From e936aadb76716cb2974a0f1529d065bc5b22ca9e Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Sat, 9 Mar 2024 09:53:54 +0100 Subject: [PATCH] the tree is now shown with a resizeable curtain-slide view --- ci/build_and_deploy_to.sh | 3 +- src/components/CurtainLayout.tsx | 72 ++++++ src/components/Rack.tsx | 25 +- src/components/arrows/BendableArrow.tsx | 87 ++++--- src/components/editor/BasketCourt.tsx | 2 +- src/components/editor/CourtAction.tsx | 4 + src/components/editor/StepsTree.tsx | 11 +- src/editor/StepsDomain.ts | 4 +- src/editor/TacticContentDomains.ts | 58 +++-- src/pages/Editor.tsx | 329 +++++++++++++----------- src/style/editor.css | 22 +- src/style/steps_tree.css | 1 + test/api/tactics.test.ts | 33 ++- vite.config.ts | 2 +- 14 files changed, 389 insertions(+), 264 deletions(-) create mode 100644 src/components/CurtainLayout.tsx diff --git a/ci/build_and_deploy_to.sh b/ci/build_and_deploy_to.sh index a5a6601..eb5d387 100755 --- a/ci/build_and_deploy_to.sh +++ b/ci/build_and_deploy_to.sh @@ -1,5 +1,4 @@ -set -e - +#!/usr/bin/env bash set -xeu export OUTPUT=$1 diff --git a/src/components/CurtainLayout.tsx b/src/components/CurtainLayout.tsx new file mode 100644 index 0000000..25e6103 --- /dev/null +++ b/src/components/CurtainLayout.tsx @@ -0,0 +1,72 @@ +import { ReactNode, useCallback, useEffect, useRef, useState } from "react" + +export interface SlideLayoutProps { + children: [ReactNode, ReactNode] + rightWidth: number, + onRightWidthChange: (w: number) => void +} + +export default function CurtainLayout({ children, rightWidth, onRightWidthChange }: SlideLayoutProps) { + const curtainRef = useRef(null) + const sliderRef = useRef(null) + + const resize = useCallback( + (e: MouseEvent) => { + const sliderPosX = e.clientX + const curtainWidth = + curtainRef.current!.getBoundingClientRect().width + + onRightWidthChange((sliderPosX / curtainWidth) * 100) + }, + [curtainRef, onRightWidthChange], + ) + + const [resizing, setResizing] = useState(false) + + useEffect(() => { + const curtain = curtainRef.current! + const slider = sliderRef.current! + + if (resizing) { + const handleMouseUp = () => setResizing(false) + + curtain.addEventListener("mousemove", resize) + curtain.addEventListener("mouseup", handleMouseUp) + return () => { + curtain.removeEventListener("mousemove", resize) + curtain.removeEventListener("mouseup", handleMouseUp) + } + } + + const handleMouseDown = () => setResizing(true) + + slider.addEventListener("mousedown", handleMouseDown) + + return () => { + slider.removeEventListener("mousedown", handleMouseDown) + } + }, [sliderRef, curtainRef, resizing, setResizing, resize]) + + return ( +
+
+ {children[0]} +
+
+ +
+ {children[1]} +
+
+ ) +} diff --git a/src/components/Rack.tsx b/src/components/Rack.tsx index fac282f..2e4e75a 100644 --- a/src/components/Rack.tsx +++ b/src/components/Rack.tsx @@ -20,13 +20,13 @@ interface RackItemProps { * A container of draggable objects * */ export function Rack({ - id, - objects, - onChange, - canDetach, - onElementDetached, - render, - }: RackProps) { + id, + objects, + onChange, + canDetach, + onElementDetached, + render, +}: RackProps) { return (
({ const index = objects.findIndex( (o) => o.key === element.key, ) - if (onChange) - onChange(objects.toSpliced(index, 1)) + if (onChange) onChange(objects.toSpliced(index, 1)) onElementDetached(ref, element) }} @@ -56,10 +55,10 @@ export function Rack({ } function RackItem({ - item, - onTryDetach, - render, - }: RackItemProps) { + item, + onTryDetach, + render, +}: RackItemProps) { const divRef = useRef(null) return ( diff --git a/src/components/arrows/BendableArrow.tsx b/src/components/arrows/BendableArrow.tsx index 18aeea3..2f1483e 100644 --- a/src/components/arrows/BendableArrow.tsx +++ b/src/components/arrows/BendableArrow.tsx @@ -39,6 +39,11 @@ export interface BendableArrowProps { startRadius?: number endRadius?: number + /** + * A way to force the arrow to render. + */ + renderDependency?: object | string | number + onDeleteRequested?: () => void style?: ArrowStyle @@ -92,6 +97,7 @@ function constraintInCircle(center: Pos, reference: Pos, radius: number): Pos { * @param onSegmentsChanges * @param wavy * @param forceStraight + * @param renderDependency * @param style * @param startRadius * @param endRadius @@ -99,20 +105,22 @@ function constraintInCircle(center: Pos, reference: Pos, radius: number): Pos { * @constructor */ export default function BendableArrow({ - area, - startPos, + area, + startPos, + + segments, + onSegmentsChanges, - segments, - onSegmentsChanges, + forceStraight, + wavy, - forceStraight, - wavy, + renderDependency, - style, - startRadius = 0, - endRadius = 0, - onDeleteRequested, -}: BendableArrowProps) { + style, + startRadius = 0, + endRadius = 0, + onDeleteRequested, + }: BendableArrowProps) { const containerRef = useRef(null) const svgRef = useRef(null) const pathRef = useRef(null) @@ -266,8 +274,8 @@ export default function BendableArrow({ const endPrevious = forceStraight ? startRelative : lastSegment.controlPoint - ? posWithinBase(lastSegment.controlPoint, parentBase) - : getPosWithinBase(lastSegment.start, parentBase) + ? posWithinBase(lastSegment.controlPoint, parentBase) + : getPosWithinBase(lastSegment.start, parentBase) const tailPos = constraintInCircle( startRelative, @@ -305,12 +313,12 @@ export default function BendableArrow({ const segmentsRelatives = ( forceStraight ? [ - { - start: startPos, - controlPoint: undefined, - end: lastSegment.end, - }, - ] + { + start: startPos, + controlPoint: undefined, + end: lastSegment.end, + }, + ] : internalSegments ).map(({ start, controlPoint, end }) => { const svgPosRelativeToBase = { x: left, y: top } @@ -326,9 +334,9 @@ export default function BendableArrow({ const controlPointRelative = controlPoint && !forceStraight ? relativeTo( - posWithinBase(controlPoint, parentBase), - svgPosRelativeToBase, - ) + posWithinBase(controlPoint, parentBase), + svgPosRelativeToBase, + ) : middle(startRelative, nextRelative) return { @@ -384,11 +392,12 @@ export default function BendableArrow({ ]) // Will update the arrow when the props change - useEffect(update, [update]) + useEffect(update, [update, renderDependency]) useEffect(() => { const observer = new MutationObserver(update) const config = { attributes: true } + if (typeof startPos == "string") { observer.observe(document.getElementById(startPos)!, config) } @@ -454,13 +463,13 @@ export default function BendableArrow({ const smoothCp = beforeSegment ? add( - currentPos, - minus( - currentPos, - beforeSegment.controlPoint ?? - middle(beforeSegmentPos, currentPos), - ), - ) + currentPos, + minus( + currentPos, + beforeSegment.controlPoint ?? + middle(beforeSegmentPos, currentPos), + ), + ) : segmentCp const result = searchOnSegment( @@ -639,7 +648,7 @@ function wavyBezier( // 3 : down to middle let phase = 0 - for (let t = step; t <= 1; ) { + for (let t = step; t <= 1;) { const pos = cubicBeziers(start, cp1, cp2, end, t) const amplification = getVerticalDerivativeProjectionAmplification(t) @@ -757,14 +766,14 @@ function searchOnSegment( * @constructor */ function ArrowPoint({ - className, - posRatio, - parentBase, - onMoves, - onPosValidated, - onRemove, - radius = 7, -}: ControlPointProps) { + className, + posRatio, + parentBase, + onMoves, + onPosValidated, + onRemove, + radius = 7, + }: ControlPointProps) { const ref = useRef(null) const pos = posWithinBase(posRatio, parentBase) diff --git a/src/components/editor/BasketCourt.tsx b/src/components/editor/BasketCourt.tsx index 68021f3..c699ce4 100644 --- a/src/components/editor/BasketCourt.tsx +++ b/src/components/editor/BasketCourt.tsx @@ -1,4 +1,4 @@ -import { ReactElement, ReactNode, RefObject } from "react" +import { ReactElement, ReactNode, RefObject, useEffect } from "react" import { Action } from "../../model/tactic/Action" import { CourtAction } from "./CourtAction" diff --git a/src/components/editor/CourtAction.tsx b/src/components/editor/CourtAction.tsx index 84f7fd5..b37c6c8 100644 --- a/src/components/editor/CourtAction.tsx +++ b/src/components/editor/CourtAction.tsx @@ -11,6 +11,8 @@ export interface CourtActionProps { onActionDeleted: () => void courtRef: RefObject isInvalid: boolean + + renderDependency?: number } export function CourtAction({ @@ -20,6 +22,7 @@ export function CourtAction({ onActionDeleted, courtRef, isInvalid, + renderDependency }: CourtActionProps) { const color = isInvalid ? "red" : "black" @@ -56,6 +59,7 @@ export function CourtAction({ endRadius={action.target ? 26 : 17} startRadius={10} onDeleteRequested={onActionDeleted} + renderDependency={renderDependency} style={{ head, dashArray, diff --git a/src/components/editor/StepsTree.tsx b/src/components/editor/StepsTree.tsx index 5a4af5e..f9c9a07 100644 --- a/src/components/editor/StepsTree.tsx +++ b/src/components/editor/StepsTree.tsx @@ -63,7 +63,12 @@ function StepsTreeNode({ key={child.id} area={ref} startPos={"step-piece-" + stepId} - segments={[{ next: "step-piece-" + getStepName(rootNode, child.id)}]} + segments={[ + { + next: + "step-piece-" + getStepName(rootNode, child.id), + }, + ]} onSegmentsChanges={() => {}} forceStraight={true} wavy={false} @@ -77,7 +82,9 @@ function StepsTreeNode({ isSelected={selectedStepId === node.id} onAddButtonClicked={() => onAddChildren(node)} onRemoveButtonClicked={ - rootNode.id === node.id ? undefined : () => onRemoveNode(node) + rootNode.id === node.id + ? undefined + : () => onRemoveNode(node) } onSelected={() => onStepSelected(node)} /> diff --git a/src/editor/StepsDomain.ts b/src/editor/StepsDomain.ts index 5ae681e..15de927 100644 --- a/src/editor/StepsDomain.ts +++ b/src/editor/StepsDomain.ts @@ -19,14 +19,12 @@ export function addStepNode( } export function getStepName(root: StepInfoNode, step: number): string { - let ord = 1 const nodes = [root] while (nodes.length > 0) { const node = nodes.pop()! - if (node.id === step) - break + if (node.id === step) break ord++ nodes.push(...[...node.children].reverse()) diff --git a/src/editor/TacticContentDomains.ts b/src/editor/TacticContentDomains.ts index 9f05e14..de631ca 100644 --- a/src/editor/TacticContentDomains.ts +++ b/src/editor/TacticContentDomains.ts @@ -200,9 +200,9 @@ export function moveComponent( phantomIdx == 0 ? origin : getComponent( - originPathItems[phantomIdx - 1], - content.components, - ) + originPathItems[phantomIdx - 1], + content.components, + ) // detach the action from the screen target and transform it to a regular move action to the phantom. content = updateComponent( { @@ -210,18 +210,18 @@ export function moveComponent( actions: playerBeforePhantom.actions.map((a) => a.target === referent ? { - ...a, - segments: a.segments.toSpliced( - a.segments.length - 2, - 1, - { - ...a.segments[a.segments.length - 1], - next: component.id, - }, - ), - target: component.id, - type: ActionKind.MOVE, - } + ...a, + segments: a.segments.toSpliced( + a.segments.length - 2, + 1, + { + ...a.segments[a.segments.length - 1], + next: component.id, + }, + ), + target: component.id, + type: ActionKind.MOVE, + } : a, ), }, @@ -234,9 +234,9 @@ export function moveComponent( ...component, pos: isPhantom ? { - type: "fixed", - ...newPos, - } + type: "fixed", + ...newPos, + } : newPos, }, content, @@ -315,15 +315,15 @@ export function computeTerminalState( content.components.filter((c) => c.type !== "phantom") as ( | Player | CourtObject - )[] + )[] const componentsTargetedState = nonPhantomComponents.map((comp) => comp.type === "player" ? getPlayerTerminalState(comp, content, computedPositions) : { - ...comp, - frozen: true, - }, + ...comp, + frozen: true, + }, ) return { @@ -399,8 +399,6 @@ export function drainTerminalStateOnChildContent( parentTerminalState: StepContent, childContent: StepContent, ): StepContent | null { - - let gotUpdated = false for (const parentComponent of parentTerminalState.components) { @@ -435,11 +433,17 @@ export function drainTerminalStateOnChildContent( if (newContentResult) { gotUpdated = true childContent = newContentResult - childComponent = getComponent(childComponent.id, newContentResult?.components) + childComponent = getComponent( + childComponent.id, + newContentResult?.components, + ) } // update the position of the player if it has been moved // also force update if the child component is not frozen (the component was introduced previously by the child step but the parent added it afterward) - if (!childComponent.frozen || !equals(childComponent.pos, parentComponent.pos)) { + if ( + !childComponent.frozen || + !equals(childComponent.pos, parentComponent.pos) + ) { gotUpdated = true childContent = updateComponent( { @@ -460,7 +464,7 @@ export function drainTerminalStateOnChildContent( (comp) => comp.type === "phantom" || !comp.frozen || - tryGetComponent(comp.id, parentTerminalState.components) + tryGetComponent(comp.id, parentTerminalState.components), ), } diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index da6a3d2..e6ab37f 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -92,6 +92,7 @@ import { getStepNode, removeStepNode, } from "../editor/StepsDomain" +import CurtainLayout from "../components/CurtainLayout" const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -186,7 +187,7 @@ function GuestModeEditor() { const content = JSON.parse( localStorage.getItem( GUEST_MODE_STEP_CONTENT_STORAGE_KEY + - stepId, + stepId, )!, ) const courtBounds = @@ -360,10 +361,7 @@ function UserModeEditor() { const { name, courtType } = await infoResponse.json() const { root } = await treeResponse.json() - if ( - infoResponse.status == 401 || - treeResponse.status == 401 - ) { + if (infoResponse.status == 401 || treeResponse.status == 401) { navigation("/login") return } @@ -372,7 +370,6 @@ function UserModeEditor() { `tactics/${tacticId}/steps/${root.id}`, ) - const contentResponse = await contentResponsePromise if (contentResponse.status == 401) { @@ -485,18 +482,18 @@ export interface EditorViewProps { } function EditorPage({ - tactic: { name, rootStepNode: initialStepsNode, courtType }, - currentStepId, - setCurrentStepContent: setContent, - currentStepContent: content, - saveState, - onNameChange, - selectStep, - onRemoveStep, - onAddStep, - - courtRef, -}: EditorViewProps) { + tactic: { name, rootStepNode: initialStepsNode, courtType }, + currentStepId, + setCurrentStepContent: setContent, + currentStepContent: content, + saveState, + onNameChange, + selectStep, + onRemoveStep, + onAddStep, + + courtRef, + }: EditorViewProps) { const [titleStyle, setTitleStyle] = useState({}) const [rootStepsNode, setRootStepsNode] = useState(initialStepsNode) @@ -519,6 +516,8 @@ function EditorPage({ [courtRef], ) + const [editorContentCurtainWidth, setEditorContentCurtainWidth] = useState(80) + const relativePositions = useMemo(() => { const courtBounds = courtRef.current?.getBoundingClientRect() return courtBounds @@ -635,15 +634,15 @@ function EditorPage({ /> ), !isFrozen && - (info.ballState === BallState.HOLDS_ORIGIN || - info.ballState === BallState.PASSED_ORIGIN) && ( - { - doMoveBall(ballBounds, player) - }} - /> - ), + (info.ballState === BallState.HOLDS_ORIGIN || + info.ballState === BallState.PASSED_ORIGIN) && ( + { + doMoveBall(ballBounds, player) + }} + /> + ), ] }, [content, courtRef, doMoveBall, previewAction?.isInvalid, setContent], @@ -700,7 +699,15 @@ function EditorPage({ /> ) }, - [courtRef, content, relativePositions, courtBounds, renderAvailablePlayerActions, validatePlayerPosition, doRemovePlayer], + [ + courtRef, + content, + relativePositions, + courtBounds, + renderAvailablePlayerActions, + validatePlayerPosition, + doRemovePlayer, + ], ) const doDeleteAction = useCallback( @@ -771,10 +778,111 @@ function EditorPage({ onActionChanges={(action) => doUpdateAction(component, action, i) } + renderDependency={editorContentCurtainWidth} /> ) }), - [courtRef, doDeleteAction, doUpdateAction], + [courtRef, doDeleteAction, doUpdateAction, editorContentCurtainWidth], + ) + + const contentNode = ( +
+
+ + + + overlaps( + courtBounds(), + div.getBoundingClientRect(), + ), + [courtBounds], + )} + onElementDetached={useCallback( + (r, e: RackedCourtObject) => + setContent((content) => + placeObjectAt( + r.getBoundingClientRect(), + courtBounds(), + e, + content, + ), + ), + [courtBounds, setContent], + )} + render={renderCourtObject} + /> + + +
+
+
+ } + courtRef={courtRef} + previewAction={previewAction} + renderComponent={renderComponent} + renderActions={renderActions} + /> +
+
+
+ ) + + const stepsTreeNode = ( + { + const addedNode = await onAddStep( + parent, + computeTerminalState(content, relativePositions), + ) + if (addedNode == null) { + console.error( + "could not add step : onAddStep returned null node", + ) + return + } + selectStep(addedNode.id) + setRootStepsNode((root) => + addStepNode(root, parent, addedNode), + ) + }, + [content, onAddStep, selectStep, relativePositions], + )} + onRemoveNode={useCallback( + async (removed) => { + const isOk = await onRemoveStep(removed) + selectStep(getParent(rootStepsNode, removed)!.id) + if (isOk) + setRootStepsNode( + (root) => removeStepNode(root, removed)!, + ) + }, + [rootStepsNode, onRemoveStep, selectStep], + )} + onStepSelected={useCallback( + (node) => selectStep(node.id), + [selectStep], + )} + /> ) return ( @@ -798,117 +906,31 @@ function EditorPage({ />
-
-
-
- - - - overlaps( - courtBounds(), - div.getBoundingClientRect(), - ), - [courtBounds], - )} - onElementDetached={useCallback( - (r, e: RackedCourtObject) => - setContent((content) => - placeObjectAt( - r.getBoundingClientRect(), - courtBounds(), - e, - content, - ), - ), - [courtBounds, setContent], - )} - render={renderCourtObject} - /> - - -
-
-
- } - courtRef={courtRef} - previewAction={previewAction} - renderComponent={renderComponent} - renderActions={renderActions} - /> -
-
-
- { - const addedNode = await onAddStep( - parent, - computeTerminalState( - content, - relativePositions, - ), - ) - if (addedNode == null) { - console.error( - "could not add step : onAddStep returned null node", - ) - return - } - selectStep(addedNode.id) - setRootStepsNode((root) => - addStepNode(root, parent, addedNode), - ) - }, - [content, onAddStep, selectStep, relativePositions], - )} - onRemoveNode={useCallback( - async (removed) => { - const isOk = await onRemoveStep(removed) - selectStep(getParent(rootStepsNode, removed)!.id) - if (isOk) - setRootStepsNode( - (root) => removeStepNode(root, removed)!, - ) - }, - [rootStepsNode, onRemoveStep, selectStep], - )} - onStepSelected={useCallback( - (node) => selectStep(node.id), - [selectStep], - )} - /> + {isStepsTreeVisible ? ( + + {contentNode} + {stepsTreeNode} + + ) : ( + contentNode + )}
) } interface EditorStepsTreeProps { - isVisible: boolean selectedStepId: number root: StepInfoNode onAddChildren: (parent: StepInfoNode) => void @@ -917,19 +939,14 @@ interface EditorStepsTreeProps { } function EditorStepsTree({ - isVisible, - selectedStepId, - root, - onAddChildren, - onRemoveNode, - onStepSelected, -}: EditorStepsTreeProps) { + selectedStepId, + root, + onAddChildren, + onRemoveNode, + onStepSelected, + }: EditorStepsTreeProps) { return ( -
+
courtRef.current!.getBoundingClientRect(), [courtRef], @@ -1011,15 +1028,15 @@ interface CourtPlayerArrowActionProps { } function CourtPlayerArrowAction({ - playerInfo, - player, - isInvalid, - - content, - setContent, - setPreviewAction, - courtRef, -}: CourtPlayerArrowActionProps) { + playerInfo, + player, + isInvalid, + + content, + setContent, + setPreviewAction, + courtRef, + }: CourtPlayerArrowActionProps) { const courtBounds = useCallback( () => courtRef.current!.getBoundingClientRect(), [courtRef], diff --git a/src/style/editor.css b/src/style/editor.css index b66ed67..38b7c2b 100644 --- a/src/style/editor.css +++ b/src/style/editor.css @@ -50,28 +50,25 @@ } #content-div, -#editor-div { +#editor-div, +#steps-div { height: 100%; width: 100%; } #content-div { + overflow: hidden; +} + +.curtain { width: 100%; } #steps-div { background-color: var(--editor-tree-background); - width: 20%; - - transform: translateX(100%); - transition: transform 500ms; overflow: scroll; } -#steps-div::-webkit-scrollbar { - display: none; -} - #allies-rack, #opponent-rack { width: 125px; @@ -154,6 +151,13 @@ font-family: monospace; } +.save-state, +#show-steps-button { + user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + .save-state-error { color: red; } diff --git a/src/style/steps_tree.css b/src/style/steps_tree.css index 462b930..e8a92bb 100644 --- a/src/style/steps_tree.css +++ b/src/style/steps_tree.css @@ -75,6 +75,7 @@ flex-direction: column; align-items: center; + top: 0; width: 100%; height: 100%; } diff --git a/test/api/tactics.test.ts b/test/api/tactics.test.ts index 552bfaa..16f5ac8 100644 --- a/test/api/tactics.test.ts +++ b/test/api/tactics.test.ts @@ -3,7 +3,10 @@ import { fetchAPI } from "../../src/Fetcher" import { saveSession } from "../../src/api/session" async function login() { - const response = await fetchAPI("auth/token/", { email: "maxime@mail.com", password: "123456" }) + const response = await fetchAPI("auth/token/", { + email: "maxime@mail.com", + password: "123456", + }) expect(response.status).toBe(200) const { token, expirationDate } = await response.json() saveSession({ auth: { token, expirationDate: Date.parse(expirationDate) } }) @@ -13,28 +16,36 @@ beforeAll(login) test("create tactic", async () => { await login() - const response = await fetchAPI("tactics", { courtType: "PLAIN", name: "test tactic" }) + const response = await fetchAPI("tactics", { + courtType: "PLAIN", + name: "test tactic", + }) expect(response.status).toBe(200) }) test("spam step creation test", async () => { - const createTacticResponse = await fetchAPI("tactics", { courtType: "PLAIN", name: "test tactic" }) + const createTacticResponse = await fetchAPI("tactics", { + courtType: "PLAIN", + name: "test tactic", + }) expect(createTacticResponse.status).toBe(200) const { id } = await createTacticResponse.json() - const tasks = Array.from({length: 200}) - .map(async () => { - const response = await fetchAPI(`tactics/${id}/steps`, { parentId: 1, content: { components: [] } }) - expect(response.status).toBe(200) - const { stepId } = await response.json() - return stepId + const tasks = Array.from({ length: 200 }).map(async () => { + const response = await fetchAPI(`tactics/${id}/steps`, { + parentId: 1, + content: { components: [] }, }) + expect(response.status).toBe(200) + const { stepId } = await response.json() + return stepId + }) const steps = [] for (const task of tasks) { steps.push(await task) } steps.sort((a, b) => a - b) - const expected = Array.from({length: 200}, (_, i) => i + 2) + const expected = Array.from({ length: 200 }, (_, i) => i + 2) expect(steps).toEqual(expected) -}) \ No newline at end of file +}) diff --git a/vite.config.ts b/vite.config.ts index 4126e3b..8d932e7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ target: "es2021", }, test: { - environment: "jsdom" + environment: "jsdom", }, plugins: [ react(),