feat: add toolbar

This commit is contained in:
trafficlunar 2024-12-05 21:17:46 +00:00
parent c836c26a05
commit f0445c208b
8 changed files with 199 additions and 18 deletions

View file

@ -11,6 +11,8 @@
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-menubar": "^1.1.2", "@radix-ui/react-menubar": "^1.1.2",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"konva": "^9.3.16", "konva": "^9.3.16",

View file

@ -11,6 +11,12 @@ importers:
'@radix-ui/react-menubar': '@radix-ui/react-menubar':
specifier: ^1.1.2 specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-toggle':
specifier: ^1.1.0
version: 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-toggle-group':
specifier: ^1.1.0
version: 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
class-variance-authority: class-variance-authority:
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.1 version: 0.7.1
@ -634,6 +640,32 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-toggle-group@1.1.0':
resolution: {integrity: sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-toggle@1.1.0':
resolution: {integrity: sha512-gwoxaKZ0oJ4vIgzsfESBuSgJNdc0rv12VhHgcqN0TEJmmZixXG/2XpsLK8kzNWYcnaoRIEEQc0bEi3dIvdUpjw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-use-callback-ref@1.1.0': '@radix-ui/react-use-callback-ref@1.1.0':
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
peerDependencies: peerDependencies:
@ -2279,6 +2311,32 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 18.3.12 '@types/react': 18.3.12
'@radix-ui/react-toggle-group@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-toggle': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-toggle@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.12)(react@18.3.1)': '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.12)(react@18.3.1)':
dependencies: dependencies:
react: 18.3.1 react: 18.3.1

View file

@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Layer, Stage } from "react-konva"; import { Layer, Stage } from "react-konva";
import { Hand } from "lucide-react";
import { import {
Menubar, Menubar,
@ -12,6 +13,7 @@ import {
MenubarSubTrigger, MenubarSubTrigger,
MenubarTrigger, MenubarTrigger,
} from "@/components/ui/menubar"; } from "@/components/ui/menubar";
import { ToggleGroup, ToggleGroupItem } from "./components/ui/toggle-group";
import ThemeChanger from "./components/menubar/theme-changer"; import ThemeChanger from "./components/menubar/theme-changer";
import Blocks from "./components/blocks"; import Blocks from "./components/blocks";
@ -31,6 +33,7 @@ function App() {
const [stageCoords, setStageCoords] = useState<Position>({ x: 0, y: 0 }); const [stageCoords, setStageCoords] = useState<Position>({ x: 0, y: 0 });
const [mousePosition, setMousePosition] = useState<Position>({ x: 0, y: 0 }); const [mousePosition, setMousePosition] = useState<Position>({ x: 0, y: 0 });
const [tool, setTool] = useState<Tool>("hand");
const [blocks, setBlocks] = useState<Block[]>([]); const [blocks, setBlocks] = useState<Block[]>([]);
const onMouseMove = (e) => { const onMouseMove = (e) => {
@ -68,10 +71,12 @@ function App() {
}, []); }, []);
return ( return (
<main className="h-screen grid grid-rows-[2.5rem_1fr]"> <main className="h-screen grid grid-rows-[2.5rem_1fr] grid-cols-[2.5rem_1fr]">
<Menubar className="rounded-none border-t-0 border-x-0"> <Menubar className="rounded-none border-t-0 border-x-0 col-span-2">
<MenubarMenu> <MenubarMenu>
<a href="https://github.com/trafficlunar/blockmatic" className="ml-4 mr-2">blockmatic</a> <a href="https://github.com/trafficlunar/blockmatic" className="ml-4 mr-2">
blockmatic
</a>
<MenubarTrigger>File</MenubarTrigger> <MenubarTrigger>File</MenubarTrigger>
<MenubarContent> <MenubarContent>
@ -99,6 +104,12 @@ function App() {
</MenubarMenu> </MenubarMenu>
</Menubar> </Menubar>
<ToggleGroup type="single" size={"sm"} value={tool} className="flex flex-col justify-start py-0.5 border-r border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950">
<ToggleGroupItem value="hand">
<Hand />
</ToggleGroupItem>
</ToggleGroup>
<div ref={stageContainerRef} className="relative w-full h-full"> <div ref={stageContainerRef} className="relative w-full h-full">
<Stage <Stage
width={stageSize.width} width={stageSize.width}

View file

@ -3,7 +3,7 @@ import { Image as KonvaImage } from "react-konva";
import blocksData from "@/lib/blocks/programmer-art/average_colors.json"; import blocksData from "@/lib/blocks/programmer-art/average_colors.json";
function Blocks({ blocks, setBlocks }: { blocks: Block[], setBlocks: React.Dispatch<React.SetStateAction<Block[]>> }) { function Blocks({ blocks, setBlocks }: { blocks: Block[]; setBlocks: React.Dispatch<React.SetStateAction<Block[]>> }) {
const [images, setImages] = useState<{ [key: string]: HTMLImageElement }>({}); const [images, setImages] = useState<{ [key: string]: HTMLImageElement }>({});
const findClosestBlock = (r: number, g: number, b: number) => { const findClosestBlock = (r: number, g: number, b: number) => {
@ -52,12 +52,12 @@ function Blocks({ blocks, setBlocks }: { blocks: Block[], setBlocks: React.Dispa
const block = findClosestBlock(imageData.data[i], imageData.data[i + 1], imageData.data[i + 2]); const block = findClosestBlock(imageData.data[i], imageData.data[i + 1], imageData.data[i + 2]);
const x = Math.floor((i / 4) % imageData.width); const x = Math.floor((i / 4) % imageData.width);
const y = Math.floor((i / 4) / imageData.width); const y = Math.floor(i / 4 / imageData.width);
newBlocks.push({ newBlocks.push({
name: block, name: block,
x, x,
y y,
}); });
} }

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
function CursorInformation({ mousePosition, blocks }: { mousePosition: Position, blocks: Block[] }) { function CursorInformation({ mousePosition, blocks }: { mousePosition: Position; blocks: Block[] }) {
const [position, setPosition] = useState({ x: 0, y: 0 }); const [position, setPosition] = useState({ x: 0, y: 0 });
const [block, setBlock] = useState<Block>(); const [block, setBlock] = useState<Block>();
useEffect(() => { useEffect(() => {
@ -14,18 +14,22 @@ function CursorInformation({ mousePosition, blocks }: { mousePosition: Position,
y: snappedY, y: snappedY,
}); });
setBlock(blocks.find(b => b.x === snappedX && b.y === snappedY)); setBlock(blocks.find((b) => b.x === snappedX && b.y === snappedY));
} }
}, [mousePosition]); }, [mousePosition]);
return <div className="absolute left-4 bottom-4 flex flex-col gap-1"> return (
<div className="bg-zinc-900 px-2 py-1 rounded shadow-xl w-fit">{block?.name ?? "air"}</div> <div className="absolute left-4 bottom-4 flex flex-col gap-1">
<div className="bg-white dark:bg-zinc-950 px-2 py-1 rounded shadow-xl w-fit border border-zinc-200 dark:border-zinc-800">
{block?.name ?? "air"}
</div>
<div className="flex gap-4 bg-zinc-900 px-2 py-1 rounded shadow-xl w-fit"> <div className="flex gap-4 bg-white dark:bg-zinc-950 px-2 py-1 rounded shadow-xl w-fit border border-zinc-200 dark:border-zinc-800">
<span>X: {position.x}</span> <span>X: {position.x}</span>
<span>Y: {position.y}</span> <span>Y: {position.y}</span>
</div> </div>
</div> </div>
);
} }
export default CursorInformation export default CursorInformation;

View file

@ -0,0 +1,59 @@
import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import { type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
})
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center justify-center gap-1", className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
))
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
})
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
export { ToggleGroup, ToggleGroupItem }

View file

@ -0,0 +1,45 @@
"use client"
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-white transition-colors hover:bg-zinc-100 hover:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-zinc-100 data-[state=on]:text-zinc-900 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2 dark:ring-offset-zinc-950 dark:hover:bg-zinc-800 dark:hover:text-zinc-400 dark:focus-visible:ring-zinc-300 dark:data-[state=on]:bg-zinc-800 dark:data-[state=on]:text-zinc-50",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-zinc-200 bg-transparent hover:bg-zinc-100 hover:text-zinc-900 dark:border-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-50",
},
size: {
default: "h-10 px-3 min-w-10",
sm: "h-9 px-2.5 min-w-9",
lg: "h-11 px-5 min-w-11",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
))
Toggle.displayName = TogglePrimitive.Root.displayName
export { Toggle, toggleVariants }

2
src/types.d.ts vendored
View file

@ -6,3 +6,5 @@ interface Position {
interface Block extends Position { interface Block extends Position {
name: string; name: string;
} }
type Tool = "hand";