From de06203d31de217497d2c4eb060411988b28dd48 Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Thu, 12 Dec 2024 22:38:53 +0000 Subject: [PATCH] refactor: rewrite and organize project still need to add other features from before rewrite --- .prettierrc | 8 +- src/App.tsx | 225 ++---------------- src/assets/react.svg | 1 - src/components/Blocks.tsx | 82 +++++++ src/components/Canvas.tsx | 146 ++++++++++++ src/components/Cursor.tsx | 21 ++ ...-information.tsx => CursorInformation.tsx} | 26 +- src/components/Toolbar.tsx | 32 +++ src/components/blocks.tsx | 83 ------- src/components/cursor.tsx | 29 --- .../{theme-changer.tsx => ThemeChanger.tsx} | 4 +- src/components/menubar/index.tsx | 51 ++++ src/context/TexturesContext.tsx | 29 +++ .../ThemeContext.tsx} | 2 - src/context/ToolContext.tsx | 19 ++ .../blocks/programmer-art/average_colors.json | 0 .../blocks/programmer-art/spritesheet.json | 0 src/{lib => }/data/welcome.json | 0 src/main.tsx | 2 +- src/types.d.ts | 2 + 20 files changed, 416 insertions(+), 346 deletions(-) delete mode 100644 src/assets/react.svg create mode 100644 src/components/Blocks.tsx create mode 100644 src/components/Canvas.tsx create mode 100644 src/components/Cursor.tsx rename src/components/{cursor-information.tsx => CursorInformation.tsx} (58%) create mode 100644 src/components/Toolbar.tsx delete mode 100644 src/components/blocks.tsx delete mode 100644 src/components/cursor.tsx rename src/components/menubar/{theme-changer.tsx => ThemeChanger.tsx} (89%) create mode 100644 src/components/menubar/index.tsx create mode 100644 src/context/TexturesContext.tsx rename src/{components/theme-provider.tsx => context/ThemeContext.tsx} (97%) create mode 100644 src/context/ToolContext.tsx rename src/{lib => }/data/blocks/programmer-art/average_colors.json (100%) rename src/{lib => }/data/blocks/programmer-art/spritesheet.json (100%) rename src/{lib => }/data/welcome.json (100%) diff --git a/.prettierrc b/.prettierrc index c05953a..af4b6bc 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,5 @@ { - "printWidth": 150, - "tabWidth": 2, - "useTabs": true -} \ No newline at end of file + "printWidth": 150, + "tabWidth": 2, + "useTabs": true +} diff --git a/src/App.tsx b/src/App.tsx index abe9305..b419aa8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,218 +1,21 @@ -import React, { useEffect, useRef, useState } from "react"; -import { Eraser, Hand, Pencil } from "lucide-react"; +import { TexturesProvider } from "./context/TexturesContext"; +import { ToolProvider } from "./context/ToolContext"; -import * as PIXI from "pixi.js"; -import { Container, Stage } from "@pixi/react"; - -import { - Menubar, - MenubarContent, - MenubarItem, - MenubarMenu, - MenubarSeparator, - MenubarSub, - MenubarSubContent, - MenubarSubTrigger, - MenubarTrigger, -} from "@/components/ui/menubar"; -import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; - -import ThemeChanger from "./components/menubar/theme-changer"; -import Blocks from "./components/blocks"; -import Cursor from "./components/cursor"; -import CursorInformation from "./components/cursor-information"; - -import spritesheet from "@/lib/data/blocks/programmer-art/spritesheet.json"; - -// Set scale mode to NEAREST -PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; +import Menubar from "./components/menubar"; +import Toolbar from "./components/Toolbar"; +import Canvas from "./components/Canvas"; function App() { - const stageContainerRef = useRef(null); - - const [stageSize, setStageSize] = useState({ width: 0, height: 0 }); - const [coords, setCoords] = useState({ x: 0, y: 0 }); - const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); - const [localMousePosition, setLocalMousePosition] = useState({ x: 0, y: 0 }); - const [dragging, setDragging] = useState(false); - const [scale, setScale] = useState(1); - const [blocks, setBlocks] = useState([]); - const [textures, setTextures] = useState>({}); - - const [cssCursor, setCssCursor] = useState("grab"); - const [tool, setTool] = useState("hand"); - const [selectedBlock, setSelectedBlock] = useState("stone"); - - const onToolChange = (value: Tool) => { - setTool(value); - setCssCursor(value === "hand" ? "grab" : "pointer"); - }; - - const onMouseMove = (e: React.MouseEvent) => { - if (dragging) { - if (tool === "hand") { - setCoords((prevCoords) => ({ - x: prevCoords.x + e.movementX, - y: prevCoords.y + e.movementY, - })); - } - - onMouseDown(); - } - - const stageRect = stageContainerRef.current?.getBoundingClientRect(); - if (!stageRect) return; - - const mouseX = e.clientX - stageRect.left; - const mouseY = e.clientY - stageRect.top; - - setMousePosition({ - x: mouseX, - y: mouseY, - }); - setLocalMousePosition({ - x: (mouseX - coords.x) / scale, - y: (mouseY - coords.y) / scale, - }); - }; - - const onMouseDown = () => { - setDragging(true); - - const blockX = Math.floor(localMousePosition.x / 16); - const blockY = Math.floor(localMousePosition.y / 16); - const updatedBlocks = blocks.filter((b) => !(b.x === blockX && b.y === blockY)); - - switch (tool) { - case "hand": - setCssCursor("grabbing"); - break; - case "pencil": { - setBlocks([ - ...updatedBlocks, - { - name: selectedBlock, - x: blockX, - y: blockY, - }, - ]); - break; - } - case "eraser": - setBlocks(updatedBlocks); - break; - } - }; - - const onMouseUp = () => { - setDragging(false); - setCssCursor(tool === "hand" ? "grab" : "pointer"); - }; - - const onWheel = (e: React.WheelEvent) => { - e.preventDefault(); - - const scaleChange = e.deltaY > 0 ? -0.1 : 0.1; - const newScale = Math.min(Math.max(scale + scaleChange * scale, 0.25), 32); - - setScale(newScale); - setCoords({ - x: mousePosition.x - localMousePosition.x * newScale, - y: mousePosition.y - localMousePosition.y * newScale, - }); - }; - - useEffect(() => { - const resizeCanvas = () => { - if (stageContainerRef.current) { - setStageSize({ - width: stageContainerRef.current.offsetWidth, - height: stageContainerRef.current.offsetHeight, - }); - } - }; - - resizeCanvas(); - window.addEventListener("resize", resizeCanvas); - - const loadSpritesheet = async () => { - const sheet = new PIXI.Spritesheet(PIXI.BaseTexture.from("/blocks/programmer-art/spritesheet.png"), spritesheet); - await sheet.parse(); - setTextures(sheet.textures); - }; - loadSpritesheet(); - }, []); - return ( -
- - - - blockmatic - - - File - - Open Schematic - Open Image - - - - - Export to... - - .schematic - .litematic - image - - - - - - - More - - - - - - - - - - - - - - - - - - -
- - - {textures && } - - - - - -
-
+ + +
+ + + +
+
+
); } diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/Blocks.tsx b/src/components/Blocks.tsx new file mode 100644 index 0000000..ad70530 --- /dev/null +++ b/src/components/Blocks.tsx @@ -0,0 +1,82 @@ +import { useEffect } from "react"; +import { Sprite } from "@pixi/react"; + +import blocksData from "@/data/blocks/programmer-art/average_colors.json"; +import welcomeBlocksData from "@/data/welcome.json"; +import { Texture } from "pixi.js"; + +interface Props { + blocks: Block[]; + setBlocks: React.Dispatch>; + textures: Record; +} + +function Blocks({ blocks, setBlocks, textures }: Props) { + const findClosestBlock = (r: number, g: number, b: number, a: number) => { + let closestBlock = ""; + let closestDistance = Infinity; + + Object.entries(blocksData).forEach(([block, rgba]) => { + const distance = Math.sqrt(Math.pow(r - rgba[0], 2) + Math.pow(g - rgba[1], 2) + Math.pow(b - rgba[2], 2) + Math.pow(a - rgba[3], 3)); + if (distance < closestDistance) { + closestDistance = distance; + closestBlock = block; + } + }); + + return closestBlock; + }; + + useEffect(() => { + // // TESTING: convert image to blocks + // const image = new Image(); + // image.src = "/bliss.png"; + // image.addEventListener("load", () => { + // const canvas = document.createElement("canvas"); + // const ctx = canvas.getContext("2d"); + + // if (ctx) { + // canvas.width = image.width; + // canvas.height = image.height; + // ctx.drawImage(image, 0, 0, image.width / 4, image.height / 4); + + // const imageData = ctx.getImageData(0, 0, image.width / 4, image.height / 4); + // const newBlocks: Block[] = []; + + // for (let i = 0; i < imageData.data.length; i += 4) { + // const block = findClosestBlock(imageData.data[i], imageData.data[i + 1], imageData.data[i + 2], imageData.data[i + 3]); + + // const x = Math.floor((i / 4) % imageData.width); + // const y = Math.floor(i / 4 / imageData.width); + + // newBlocks.push({ + // name: block, + // x, + // y, + // }); + // } + + // setBlocks(newBlocks); + // } + // }); + + setBlocks(welcomeBlocksData); + console.log(textures) + }, [textures]); + + return ( + <> + {blocks.map((block, index) => { + const texture = textures[block.name]; + if (!texture) { + console.warn(`Texture not found for block: ${block.name}`); + return null; + } + + return ; + })} + + ); +} + +export default Blocks; diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx new file mode 100644 index 0000000..c1bef37 --- /dev/null +++ b/src/components/Canvas.tsx @@ -0,0 +1,146 @@ +import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { Container, Stage } from "@pixi/react"; +import * as PIXI from "pixi.js"; + +import { ToolContext } from "@/context/ToolContext"; +import { TexturesContext } from "@/context/TexturesContext"; + +import Blocks from "./Blocks"; +import Cursor from "./Cursor"; +import CursorInformation from "./CursorInformation"; + +// Set scale mode to NEAREST +PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; + +function Canvas() { + const { tool, selectedBlock } = useContext(ToolContext); + const textures = useContext(TexturesContext); + + const stageContainerRef = useRef(null); + const [stageSize, setStageSize] = useState({ width: 0, height: 0 }); + + const [coords, setCoords] = useState({ x: 0, y: 0 }); + const [scale, setScale] = useState(1); + + const [mouseCoords, setMouseCoords] = useState({ x: 0, y: 0 }); + const [dragging, setDragging] = useState(false); + + const [blocks, setBlocks] = useState([]); + + const updatedBlocks = useMemo(() => { + return blocks.filter((b) => !(b.x === mouseCoords.x && b.y === mouseCoords.y)); + }, [blocks, mouseCoords]); + + const onToolUse = useCallback(() => { + switch (tool) { + case "pencil": { + setBlocks([ + ...updatedBlocks, + { + name: selectedBlock, + x: mouseCoords.x, + y: mouseCoords.y, + }, + ]); + break; + } + case "eraser": + setBlocks(updatedBlocks); + break; + } + }, [tool, mouseCoords, selectedBlock, updatedBlocks]); + + const onMouseMove = useCallback( + (e: React.MouseEvent) => { + if (dragging) { + if (tool === "hand") { + setCoords((prevCoords) => ({ + x: prevCoords.x + e.movementX, + y: prevCoords.y + e.movementY, + })); + } + onToolUse(); + } + + if (!stageContainerRef.current) return; + + const rect = stageContainerRef.current.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + setMouseCoords({ + x: Math.floor((mouseX - coords.x) / (16 * scale)), + y: Math.floor((mouseY - coords.y) / (16 * scale)), + }); + }, + [dragging, coords, scale, tool, onToolUse] + ); + + const onMouseDown = useCallback(() => { + setDragging(true); + onToolUse(); + }, [onToolUse]); + + const onMouseUp = () => { + setDragging(false); + }; + + const onWheel = useCallback( + (e: React.WheelEvent) => { + e.preventDefault(); + + // if (!stageContainerRef.current) return; + + // const rect = stageContainerRef.current.getBoundingClientRect(); + // const mouseX = e.clientX - rect.left; + // const mouseY = e.clientY - rect.top; + + const scaleChange = e.deltaY > 0 ? -0.1 : 0.1; + const newScale = Math.min(Math.max(scale + scaleChange * scale, 0.25), 32); + + setScale(newScale); + // setCoords({ + // x: -(mouseX * scale) + e.clientX, + // y: -(mouseY * scale) + e.clientY, + // }); + }, + [scale] + ); + + useEffect(() => { + const resizeCanvas = () => { + if (stageContainerRef.current) { + setStageSize({ + width: stageContainerRef.current.offsetWidth, + height: stageContainerRef.current.offsetHeight, + }); + } + }; + + resizeCanvas(); + window.addEventListener("resize", resizeCanvas); + return () => window.removeEventListener("resize", resizeCanvas); + }, []); + + return ( +
+ + + + + + + + +
+ ); +} + +export default Canvas; diff --git a/src/components/Cursor.tsx b/src/components/Cursor.tsx new file mode 100644 index 0000000..31d456e --- /dev/null +++ b/src/components/Cursor.tsx @@ -0,0 +1,21 @@ +import { Graphics } from "@pixi/react"; + +interface Props { + mouseCoords: Position +} + +function Cursor({ mouseCoords }: Props) { + return ( + { + g.clear(); + g.lineStyle(1, 0xffffff, 1); + g.drawRect(0, 0, 16, 16); + }} + /> + ); +} + +export default Cursor; diff --git a/src/components/cursor-information.tsx b/src/components/CursorInformation.tsx similarity index 58% rename from src/components/cursor-information.tsx rename to src/components/CursorInformation.tsx index 0389b8f..ab95559 100644 --- a/src/components/cursor-information.tsx +++ b/src/components/CursorInformation.tsx @@ -1,22 +1,22 @@ import { useEffect, useState } from "react"; -function CursorInformation({ localMousePosition, blocks }: { localMousePosition: Position; blocks: Block[] }) { - const [position, setPosition] = useState({ x: 0, y: 0 }); +interface Props { + mouseCoords: Position; + blocks: Block[]; +} + +function CursorInformation({ mouseCoords, blocks }: Props) { + const [position, setPosition] = useState({ x: 0, y: 0 }); const [block, setBlock] = useState(); useEffect(() => { - if (localMousePosition) { - const x = Math.floor(localMousePosition.x / 16); - const y = Math.floor(localMousePosition.y / 16); + setPosition({ + x: mouseCoords.x, + y: mouseCoords.y, + }); - setPosition({ - x, - y, - }); - - setBlock(blocks.find((b) => b.x === x && b.y === y)); - } - }, [localMousePosition]); + setBlock(blocks.find((b) => b.x === mouseCoords.x && b.y === mouseCoords.y)); + }, [mouseCoords]); return (
diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx new file mode 100644 index 0000000..1058163 --- /dev/null +++ b/src/components/Toolbar.tsx @@ -0,0 +1,32 @@ +import { useContext } from "react"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; +import { Eraser, Hand, Pencil } from "lucide-react"; + +import { ToolContext } from "@/context/ToolContext"; + +function Toolbar() { + const { tool, setTool } = useContext(ToolContext); + + const onToolChange = (value: string) => setTool(value as Tool); + + return ( + + + + + + + + + + + + ); +} + +export default Toolbar; diff --git a/src/components/blocks.tsx b/src/components/blocks.tsx deleted file mode 100644 index f0542ac..0000000 --- a/src/components/blocks.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { useEffect } from "react"; -import { Texture } from "pixi.js"; -import { Sprite } from "@pixi/react"; - -import blocksData from "@/lib/data/blocks/programmer-art/average_colors.json"; -import welcomeBlocksData from "@/lib/data/welcome.json"; - -function Blocks({ - blocks, - setBlocks, - textures, -}: { - blocks: Block[]; - setBlocks: React.Dispatch>; - textures: Record; -}) { - const findClosestBlock = (r: number, g: number, b: number, a: number) => { - let closestBlock = ""; - let closestDistance = Infinity; - - Object.entries(blocksData).forEach(([block, rgba]) => { - const distance = Math.sqrt(Math.pow(r - rgba[0], 2) + Math.pow(g - rgba[1], 2) + Math.pow(b - rgba[2], 2) + Math.pow(a - rgba[3], 3)); - if (distance < closestDistance) { - closestDistance = distance; - closestBlock = block; - } - }); - - return closestBlock; - }; - - useEffect(() => { - // TESTING: convert image to blocks - const image = new Image(); - image.src = "/bliss.png"; - image.addEventListener("load", () => { - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - - if (ctx) { - canvas.width = image.width; - canvas.height = image.height; - ctx.drawImage(image, 0, 0, image.width / 4, image.height / 4); - - const imageData = ctx.getImageData(0, 0, image.width / 4, image.height / 4); - const newBlocks: Block[] = []; - - for (let i = 0; i < imageData.data.length; i += 4) { - const block = findClosestBlock(imageData.data[i], imageData.data[i + 1], imageData.data[i + 2], imageData.data[i + 3]); - - const x = Math.floor((i / 4) % imageData.width); - const y = Math.floor(i / 4 / imageData.width); - - newBlocks.push({ - name: block, - x, - y, - }); - } - - setBlocks(newBlocks); - } - }); - - setBlocks(welcomeBlocksData); - }, []); - - return ( - <> - {blocks.map((block, index) => { - const texture = textures[block.name]; - if (!texture) { - console.warn(`Texture not found for block: ${block.name}`); - return null; - } - - return ; - })} - - ); -} - -export default Blocks; diff --git a/src/components/cursor.tsx b/src/components/cursor.tsx deleted file mode 100644 index c17c88a..0000000 --- a/src/components/cursor.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useEffect, useState } from "react"; -import { Graphics } from "@pixi/react"; - -function Cursor({ localMousePosition }: { localMousePosition: Position }) { - const [position, setPosition] = useState({ x: 0, y: 0 }); - - useEffect(() => { - if (localMousePosition) { - const x = Math.floor(localMousePosition.x / 16) * 16; - const y = Math.floor(localMousePosition.y / 16) * 16; - - setPosition({ x, y }); - } - }, [localMousePosition]); - - return ( - { - g.clear(); - g.lineStyle(1, 0xffffff, 1); - g.drawRect(0, 0, 16, 16); - }} - /> - ); -} - -export default Cursor; diff --git a/src/components/menubar/theme-changer.tsx b/src/components/menubar/ThemeChanger.tsx similarity index 89% rename from src/components/menubar/theme-changer.tsx rename to src/components/menubar/ThemeChanger.tsx index 4ca1cf3..07062b7 100644 --- a/src/components/menubar/theme-changer.tsx +++ b/src/components/menubar/ThemeChanger.tsx @@ -1,5 +1,5 @@ import { MenubarRadioGroup, MenubarRadioItem, MenubarSub, MenubarSubContent, MenubarSubTrigger } from '@/components/ui/menubar'; -import { useTheme } from '../theme-provider'; +import { useTheme } from '@/context/ThemeContext'; function ThemeChanger() { const { setTheme, theme } = useTheme(); @@ -8,7 +8,7 @@ function ThemeChanger() { Set theme... - setTheme(value)}> + setTheme(value as Theme)}> Light Dark System diff --git a/src/components/menubar/index.tsx b/src/components/menubar/index.tsx new file mode 100644 index 0000000..6c5ad2f --- /dev/null +++ b/src/components/menubar/index.tsx @@ -0,0 +1,51 @@ +import { + Menubar as UIMenubar, + MenubarContent, + MenubarItem, + MenubarMenu, + MenubarSeparator, + MenubarSub, + MenubarSubContent, + MenubarSubTrigger, + MenubarTrigger, +} from "@/components/ui/menubar"; + +import ThemeChanger from "./ThemeChanger"; + +function Menubar() { + return ( + + + + blockmatic + + + File + + Open Schematic + Open Image + + + + + Export to... + + .schematic + .litematic + image + + + + + + + More + + + + + + ); +} + +export default Menubar; diff --git a/src/context/TexturesContext.tsx b/src/context/TexturesContext.tsx new file mode 100644 index 0000000..aeb504c --- /dev/null +++ b/src/context/TexturesContext.tsx @@ -0,0 +1,29 @@ +import { createContext, ReactNode, useEffect, useState } from "react"; +import { BaseTexture, Spritesheet, Texture } from "pixi.js"; + +import spritesheet from "@/data/blocks/programmer-art/spritesheet.json"; + +interface Props { + children: ReactNode; +} + +export const TexturesContext = createContext({} as Record); + +export const TexturesProvider = ({ children }: Props) => { + const [textures, setTextures] = useState>({}); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + const sheet = new Spritesheet(BaseTexture.from("/blocks/programmer-art/spritesheet.png"), spritesheet); + sheet.parse().then((t) => { + setTextures(t); + setLoaded(true); + }); + }, []); + + if (!loaded) { + return

Loading...

; + } + + return {children}; +}; diff --git a/src/components/theme-provider.tsx b/src/context/ThemeContext.tsx similarity index 97% rename from src/components/theme-provider.tsx rename to src/context/ThemeContext.tsx index 296cab7..b03b21e 100644 --- a/src/components/theme-provider.tsx +++ b/src/context/ThemeContext.tsx @@ -1,7 +1,5 @@ import { createContext, useContext, useEffect, useState } from "react"; -type Theme = "dark" | "light" | "system"; - type ThemeProviderProps = { children: React.ReactNode; defaultTheme?: Theme; diff --git a/src/context/ToolContext.tsx b/src/context/ToolContext.tsx new file mode 100644 index 0000000..16d3a00 --- /dev/null +++ b/src/context/ToolContext.tsx @@ -0,0 +1,19 @@ +import { createContext, ReactNode, useState } from "react"; + +interface Props { + children: ReactNode; +} + +export const ToolContext = createContext({ + tool: "hand" as Tool, + selectedBlock: "stone", + setTool: (tool: Tool) => {}, + setSelectedBlock: (block: string) => {}, +}); + +export const ToolProvider = ({ children }: Props) => { + const [tool, setTool] = useState("hand"); + const [selectedBlock, setSelectedBlock] = useState("stone"); + + return {children}; +}; diff --git a/src/lib/data/blocks/programmer-art/average_colors.json b/src/data/blocks/programmer-art/average_colors.json similarity index 100% rename from src/lib/data/blocks/programmer-art/average_colors.json rename to src/data/blocks/programmer-art/average_colors.json diff --git a/src/lib/data/blocks/programmer-art/spritesheet.json b/src/data/blocks/programmer-art/spritesheet.json similarity index 100% rename from src/lib/data/blocks/programmer-art/spritesheet.json rename to src/data/blocks/programmer-art/spritesheet.json diff --git a/src/lib/data/welcome.json b/src/data/welcome.json similarity index 100% rename from src/lib/data/welcome.json rename to src/data/welcome.json diff --git a/src/main.tsx b/src/main.tsx index 24bfac6..67d39e1 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,7 +2,7 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' -import { ThemeProvider } from './components/theme-provider.tsx' +import { ThemeProvider } from '@/context/ThemeContext.tsx'; createRoot(document.getElementById('root')!).render( diff --git a/src/types.d.ts b/src/types.d.ts index 43f237b..09da3a9 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,3 +1,5 @@ +type Theme = "dark" | "light" | "system"; + interface Position { x: number; y: number