feat: add dialogs to menubar

can't put dialogs inside menubar components so we import each dialog
dynamically
This commit is contained in:
trafficlunar 2024-12-14 21:40:14 +00:00
parent c4c7986a71
commit 370ec76c9b
7 changed files with 218 additions and 37 deletions

View file

@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@pixi/react": "7", "@pixi/react": "7",
"@radix-ui/react-dialog": "^1.1.3",
"@radix-ui/react-menubar": "^1.1.2", "@radix-ui/react-menubar": "^1.1.2",
"@radix-ui/react-slider": "^1.2.2", "@radix-ui/react-slider": "^1.2.2",
"@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0",

View file

@ -11,7 +11,7 @@ interface Props {
textures: Record<string, Texture>; textures: Record<string, Texture>;
} }
function Blocks({ blocks, setBlocks, textures }: Props) { function Blocks({ blocks, setBlocks, textures }: Props) {
const findClosestBlock = (r: number, g: number, b: number, a: number) => { const findClosestBlock = (r: number, g: number, b: number, a: number) => {
let closestBlock = ""; let closestBlock = "";
let closestDistance = Infinity; let closestDistance = Infinity;
@ -28,40 +28,39 @@ function Blocks({ blocks, setBlocks, textures }: Props) {
}; };
useEffect(() => { useEffect(() => {
// // TESTING: convert image to blocks // TESTING: convert image to blocks
// const image = new Image(); const image = new Image();
// image.src = "/bliss.png"; image.src = "/bliss.png";
// image.addEventListener("load", () => { image.addEventListener("load", () => {
// const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
// const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
// if (ctx) { if (ctx) {
// canvas.width = image.width; canvas.width = image.width;
// canvas.height = image.height; canvas.height = image.height;
// ctx.drawImage(image, 0, 0, image.width / 4, image.height / 4); ctx.drawImage(image, 0, 0, image.width / 4, image.height / 4);
// const imageData = ctx.getImageData(0, 0, image.width / 4, image.height / 4); const imageData = ctx.getImageData(0, 0, image.width / 4, image.height / 4);
// const newBlocks: Block[] = []; const newBlocks: Block[] = [];
// for (let i = 0; i < imageData.data.length; i += 4) { 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 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 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,
// }); });
// } }
// setBlocks(newBlocks); setBlocks(newBlocks);
// } }
// }); });
setBlocks(welcomeBlocksData); setBlocks(welcomeBlocksData);
console.log(textures)
}, [textures]); }, [textures]);
return ( return (

View file

@ -0,0 +1,18 @@
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
function OpenImage() {
return (
<DialogContent>
<DialogHeader>
<DialogTitle>Open image</DialogTitle>
<DialogDescription>Open your image to load as blocks into the canvas</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button type="submit">Submit</Button>
</DialogFooter>
</DialogContent>
);
}
export default OpenImage;

View file

@ -1,3 +1,5 @@
import { useContext } from "react";
import { import {
MenubarContent, MenubarContent,
MenubarItem, MenubarItem,
@ -9,14 +11,17 @@ import {
MenubarTrigger, MenubarTrigger,
} from "@/components/ui/menubar"; } from "@/components/ui/menubar";
import { DialogContext } from "@/context/DialogContext";
function FileMenu() { function FileMenu() {
const openDialog = useContext(DialogContext);
return ( return (
<MenubarMenu> <MenubarMenu>
<MenubarTrigger>File</MenubarTrigger> <MenubarTrigger>File</MenubarTrigger>
<MenubarContent> <MenubarContent>
<MenubarItem>Open Schematic</MenubarItem> <MenubarItem>Open Schematic</MenubarItem>
<MenubarItem>Open Image</MenubarItem> <MenubarItem onClick={() => openDialog("OpenImage")}>Open Image</MenubarItem>
<MenubarSeparator /> <MenubarSeparator />
<MenubarSub> <MenubarSub>

View file

@ -1,20 +1,24 @@
import { Menubar as UIMenubar } from "@/components/ui/menubar"; import { Menubar as UIMenubar } from "@/components/ui/menubar";
import { DialogProvider } from "@/context/DialogContext";
import FileMenu from "./FileMenu"; import FileMenu from "./FileMenu";
import ViewMenu from "./ViewMenu"; import ViewMenu from "./ViewMenu";
import MoreMenu from "./MoreMenu"; import MoreMenu from "./MoreMenu";
function Menubar() { function Menubar() {
return ( return (
<UIMenubar className="rounded-none border-t-0 border-x-0 col-span-2"> <DialogProvider>
<a href="https://github.com/trafficlunar/blockmatic" className="ml-4 mr-2"> <UIMenubar className="rounded-none border-t-0 border-x-0 col-span-2">
blockmatic <a href="https://github.com/trafficlunar/blockmatic" className="ml-4 mr-2">
</a> blockmatic
</a>
<FileMenu /> <FileMenu />
<ViewMenu /> <ViewMenu />
<MoreMenu /> <MoreMenu />
</UIMenubar> </UIMenubar>
</DialogProvider>
); );
} }

View file

@ -0,0 +1,120 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-zinc-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg dark:border-zinc-800 dark:bg-zinc-950",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-zinc-100 data-[state=open]:text-zinc-500 dark:ring-offset-zinc-950 dark:focus:ring-zinc-300 dark:data-[state=open]:bg-zinc-800 dark:data-[state=open]:text-zinc-400">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-zinc-500 dark:text-zinc-400", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View file

@ -0,0 +1,34 @@
import { Dialog } from "@/components/ui/dialog";
import { createContext, lazy, ReactNode, Suspense, useState } from "react";
interface Props {
children: ReactNode;
}
export const DialogContext = createContext((id: string) => {});
export const DialogProvider = ({ children }: Props) => {
const [open, setOpen] = useState(false);
const [id, setId] = useState("");
const openDialog = (id: string) => {
setId(id);
setOpen(true);
};
const LazyDialogContent = id ? lazy(() => import(`@/components/dialogs/${id}.tsx`)) : null;
return (
<DialogContext.Provider value={openDialog}>
<Dialog open={open} onOpenChange={(value) => setOpen(value)}>
{LazyDialogContent && (
<Suspense fallback={<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">Loading dialog...</div>}>
<LazyDialogContent />
</Suspense>
)}
</Dialog>
{children}
</DialogContext.Provider>
);
};