feat: part 1 of redesign of open image dialog
This commit is contained in:
parent
0c8c4b1870
commit
25f519074a
7 changed files with 383 additions and 66 deletions
|
|
@ -12,6 +12,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pixi/react": "7",
|
"@pixi/react": "7",
|
||||||
"@pixi/tilemap": "4.1.0",
|
"@pixi/tilemap": "4.1.0",
|
||||||
|
"@radix-ui/react-checkbox": "^1.1.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.3",
|
"@radix-ui/react-dialog": "^1.1.3",
|
||||||
"@radix-ui/react-label": "^2.1.1",
|
"@radix-ui/react-label": "^2.1.1",
|
||||||
"@radix-ui/react-menubar": "^1.1.2",
|
"@radix-ui/react-menubar": "^1.1.2",
|
||||||
|
|
@ -19,6 +20,7 @@
|
||||||
"@radix-ui/react-separator": "^1.1.1",
|
"@radix-ui/react-separator": "^1.1.1",
|
||||||
"@radix-ui/react-slider": "^1.2.2",
|
"@radix-ui/react-slider": "^1.2.2",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.2",
|
||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.6",
|
"@radix-ui/react-tooltip": "^1.1.6",
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,29 @@
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from "react-dropzone";
|
||||||
|
|
||||||
import { CircleAlertIcon, UploadIcon } from "lucide-react";
|
import { CircleAlertIcon, LinkIcon, UploadIcon } from "lucide-react";
|
||||||
|
|
||||||
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
|
|
||||||
|
import { CanvasContext } from "@/context/Canvas";
|
||||||
import { ImageContext } from "@/context/Image";
|
import { ImageContext } from "@/context/Image";
|
||||||
import { LoadingContext } from "@/context/Loading";
|
import { LoadingContext } from "@/context/Loading";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { CheckedState } from "@radix-ui/react-checkbox";
|
||||||
|
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
|
|
||||||
|
import { getBlockData } from "@/utils/getBlockData";
|
||||||
|
|
||||||
|
import BlockSelector from "./open-image/BlockSelector";
|
||||||
|
|
||||||
function OpenImage({ close }: DialogProps) {
|
function OpenImage({ close }: DialogProps) {
|
||||||
|
const { version } = useContext(CanvasContext);
|
||||||
const { setLoading } = useContext(LoadingContext);
|
const { setLoading } = useContext(LoadingContext);
|
||||||
const { setImage: setContextImage, setImageDimensions: setContextImageDimensions } = useContext(ImageContext);
|
const { setImage: setContextImage, setImageDimensions: setContextImageDimensions } = useContext(ImageContext);
|
||||||
|
|
||||||
|
|
@ -21,9 +33,24 @@ function OpenImage({ close }: DialogProps) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const divRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [image, setImage] = useState<HTMLImageElement>();
|
const [image, setImage] = useState<HTMLImageElement>();
|
||||||
const [imageDimensions, setImageDimensions] = useState<Dimension>({ width: 0, height: 0 });
|
const [imageDimensions, setImageDimensions] = useState<Dimension>({ width: 0, height: 0 });
|
||||||
const [aspectRatio, setAspectRatio] = useState(1);
|
const [aspectRatio, setAspectRatio] = useState(1);
|
||||||
|
const [linkAspectRatio, setLinkAspectRatio] = useState(true);
|
||||||
|
|
||||||
|
const [searchInput, setSearchInput] = useState("");
|
||||||
|
const [stageWidth, setStageWidth] = useState(0);
|
||||||
|
|
||||||
|
const [selectedBlocks, setSelectedBlocks] = useState<string[]>(["stone"]);
|
||||||
|
const [blockTypeCheckboxesChecked, setBlockTypeCheckboxesChecked] = useState({
|
||||||
|
creative: false,
|
||||||
|
tile_entity: false,
|
||||||
|
fallable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const blockData = getBlockData(version);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (acceptedFiles[0]) {
|
if (acceptedFiles[0]) {
|
||||||
|
|
@ -41,15 +68,27 @@ function OpenImage({ close }: DialogProps) {
|
||||||
const newDimension = Number(e.target.value);
|
const newDimension = Number(e.target.value);
|
||||||
if (newDimension < 1 || newDimension > 10000) return;
|
if (newDimension < 1 || newDimension > 10000) return;
|
||||||
|
|
||||||
setImageDimensions(() => {
|
setImageDimensions((prev) => {
|
||||||
if (isWidth) {
|
if (isWidth)
|
||||||
return { width: newDimension, height: Math.round(newDimension / aspectRatio) };
|
return linkAspectRatio ? { width: newDimension, height: Math.round(newDimension / aspectRatio) } : { ...prev, width: newDimension };
|
||||||
} else {
|
return linkAspectRatio ? { width: Math.round(newDimension * aspectRatio), height: newDimension } : { ...prev, height: newDimension };
|
||||||
return { width: Math.round(newDimension * aspectRatio), height: newDimension };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onBlockTypeCheckedChange = (checked: CheckedState, property: keyof BlockData[string]) => {
|
||||||
|
const blocksWithProperty = Object.entries(blockData)
|
||||||
|
.filter(([, data]) => data[property] === true)
|
||||||
|
.map(([blockName]) => blockName);
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
setSelectedBlocks((prev) => [...prev, ...blocksWithProperty]);
|
||||||
|
} else {
|
||||||
|
setSelectedBlocks((prev) => prev.filter((block) => !blocksWithProperty.includes(block)));
|
||||||
|
}
|
||||||
|
|
||||||
|
setBlockTypeCheckboxesChecked((prev) => ({ ...prev, [property]: checked }));
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
if (image) {
|
if (image) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -63,74 +102,176 @@ function OpenImage({ close }: DialogProps) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Object.keys(blockTypeCheckboxesChecked).forEach((property) => {
|
||||||
|
const blocksWithProperty = Object.entries(blockData)
|
||||||
|
.filter(([, data]) => data[property as keyof BlockData[string]] === true)
|
||||||
|
.map(([blockName]) => blockName);
|
||||||
|
|
||||||
|
const propertyChecked = blocksWithProperty.every((block) => selectedBlocks.includes(block));
|
||||||
|
setBlockTypeCheckboxesChecked((prev) => ({ ...prev, [property]: propertyChecked }));
|
||||||
|
});
|
||||||
|
}, [selectedBlocks]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!divRef.current) return;
|
||||||
|
setStageWidth(divRef.current.clientWidth);
|
||||||
|
|
||||||
|
console.log(stageWidth);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent>
|
<DialogContent ref={divRef}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Open Image</DialogTitle>
|
<DialogTitle>Open Image</DialogTitle>
|
||||||
<DialogDescription>Open your image to load as blocks into the canvas</DialogDescription>
|
<DialogDescription>Open your image to load as blocks into the canvas</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div ref={divRef}>
|
||||||
<div
|
<Tabs defaultValue="image">
|
||||||
{...getRootProps({
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
className: "flex flex-col justify-center items-center gap-2 p-4 rounded border border-2 border-dashed select-none",
|
<TabsTrigger value="image">Image</TabsTrigger>
|
||||||
})}
|
<TabsTrigger value="blocks">Blocks</TabsTrigger>
|
||||||
>
|
</TabsList>
|
||||||
<input {...getInputProps({ multiple: false })} />
|
|
||||||
<UploadIcon />
|
|
||||||
<p>Drag and drop your image here</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-[auto,1fr] gap-2">
|
<TabsContent value="image" className="flex flex-col gap-2">
|
||||||
{image && acceptedFiles[0] && (
|
<div
|
||||||
<>
|
{...getRootProps({
|
||||||
<img
|
className: "flex flex-col justify-center items-center gap-2 p-4 rounded border border-2 border-dashed select-none",
|
||||||
src={image.src}
|
})}
|
||||||
alt="your image"
|
>
|
||||||
className="w-48 h-48 object-contain border rounded-lg"
|
<input {...getInputProps({ multiple: false })} />
|
||||||
style={{ background: "repeating-conic-gradient(#fff 0 90deg, #bbb 0 180deg) 0 0/25% 25%" }}
|
<UploadIcon />
|
||||||
/>
|
<p>Drag and drop your image here or click to open</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="grid grid-cols-[auto,1fr] gap-2">
|
||||||
<div>
|
{image && acceptedFiles[0] && (
|
||||||
<Label>File name</Label>
|
<>
|
||||||
<p>{acceptedFiles[0].name}</p>
|
<img
|
||||||
</div>
|
src={image.src}
|
||||||
|
alt="your image"
|
||||||
<div>
|
className="w-48 h-48 object-contain border rounded-lg"
|
||||||
<Label htmlFor="width">Width (blocks)</Label>
|
style={{ background: "repeating-conic-gradient(#fff 0 90deg, #bbb 0 180deg) 0 0/25% 25%" }}
|
||||||
<Input type="number" id="width" placeholder="Width" value={imageDimensions.width} onChange={(e) => onDimensionChange(e, true)} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="height">Height (blocks)</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
id="height"
|
|
||||||
placeholder="Height"
|
|
||||||
value={imageDimensions.height}
|
|
||||||
onChange={(e) => onDimensionChange(e, false)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="file-name">File name</Label>
|
||||||
|
<p id="file-name" className="text-wrap">
|
||||||
|
{acceptedFiles[0].name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-[1fr_auto_1fr] gap-1">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="width">Width (blocks)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
id="width"
|
||||||
|
placeholder="Width"
|
||||||
|
value={imageDimensions.width}
|
||||||
|
onChange={(e) => onDimensionChange(e, true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toggle
|
||||||
|
aria-label="Link aspect ratio"
|
||||||
|
variant="outline"
|
||||||
|
pressed={linkAspectRatio}
|
||||||
|
onPressedChange={() => setLinkAspectRatio(!linkAspectRatio)}
|
||||||
|
className="h-8 !min-w-8 p-0 mt-auto mb-1"
|
||||||
|
>
|
||||||
|
<LinkIcon />
|
||||||
|
</Toggle>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="height">Height (blocks)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
id="height"
|
||||||
|
placeholder="Height"
|
||||||
|
value={imageDimensions.height}
|
||||||
|
onChange={(e) => onDimensionChange(e, false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{imageDimensions.height > 384 && (
|
||||||
|
<div className="flex items-center gap-1 mt-auto mb-1">
|
||||||
|
<CircleAlertIcon className="text-red-400" size={22} />
|
||||||
|
<span className="text-red-400 text-sm">The height is above 384 blocks!</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="blocks" className="flex flex-col gap-2">
|
||||||
|
<div className="grid grid-cols-2">
|
||||||
|
<div className="grid grid-rows-3 gap-2 *:flex *:items-center *:gap-1">
|
||||||
|
<div>
|
||||||
|
<Checkbox
|
||||||
|
id="creative"
|
||||||
|
checked={blockTypeCheckboxesChecked.creative}
|
||||||
|
onCheckedChange={(value) => onBlockTypeCheckedChange(value, "creative")}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="creative">Creative only</Label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Checkbox
|
||||||
|
id="tile_entity"
|
||||||
|
checked={blockTypeCheckboxesChecked.tile_entity}
|
||||||
|
onCheckedChange={(value) => onBlockTypeCheckedChange(value, "tile_entity")}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="tile_entity">Tile entities</Label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Checkbox
|
||||||
|
id="fallable"
|
||||||
|
checked={blockTypeCheckboxesChecked.fallable}
|
||||||
|
onCheckedChange={(value) => onBlockTypeCheckedChange(value, "fallable")}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="fallable">Fallable</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
<div className="grid grid-rows-2 gap-1">
|
||||||
</div>
|
<Button className="h-8" onClick={() => setSelectedBlocks(Object.keys(blockData))}>
|
||||||
|
Check all blocks
|
||||||
|
</Button>
|
||||||
|
<Button className="h-8" onClick={() => setSelectedBlocks([])}>
|
||||||
|
Uncheck all blocks
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<Input placeholder="Search for blocks..." value={searchInput} onChange={(e) => setSearchInput(e.target.value)} />
|
||||||
|
|
||||||
|
<ScrollArea className="h-96">
|
||||||
|
<BlockSelector
|
||||||
|
stageWidth={stageWidth}
|
||||||
|
searchInput={searchInput}
|
||||||
|
selectedBlocks={selectedBlocks}
|
||||||
|
setSelectedBlocks={setSelectedBlocks}
|
||||||
|
/>
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="items-center">
|
<DialogFooter>
|
||||||
{imageDimensions.height > 384 && (
|
{/* todo: add version selector here */}
|
||||||
<div className="flex items-center gap-1 h-min mr-auto">
|
|
||||||
<CircleAlertIcon className="text-red-400" size={22} />
|
|
||||||
<span className="text-red-400 text-sm">The height is above 384 blocks!</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button variant="outline" onClick={close}>
|
<Button variant="outline" onClick={close}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" onClick={onSubmit}>
|
<Button type="submit" onClick={onSubmit}>
|
||||||
Submit
|
Open
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
||||||
95
src/components/dialogs/open-image/BlockSelector.tsx
Normal file
95
src/components/dialogs/open-image/BlockSelector.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import React, { useContext, useMemo, useState } from "react";
|
||||||
|
import { Container, Graphics, Sprite, Stage } from "@pixi/react";
|
||||||
|
|
||||||
|
import { CanvasContext } from "@/context/Canvas";
|
||||||
|
import { TexturesContext } from "@/context/Textures";
|
||||||
|
import { ThemeContext } from "@/context/Theme";
|
||||||
|
|
||||||
|
import { getBlockData } from "@/utils/getBlockData";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
stageWidth: number;
|
||||||
|
searchInput: string;
|
||||||
|
selectedBlocks: string[];
|
||||||
|
setSelectedBlocks: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BlockSelector({ stageWidth, searchInput, selectedBlocks, setSelectedBlocks }: Props) {
|
||||||
|
const { version } = useContext(CanvasContext);
|
||||||
|
const { missingTexture, textures } = useContext(TexturesContext);
|
||||||
|
const { isDark } = useContext(ThemeContext);
|
||||||
|
|
||||||
|
const blockData = getBlockData(version);
|
||||||
|
|
||||||
|
const [hoverPosition, setHoverPosition] = useState<Position | null>(null);
|
||||||
|
|
||||||
|
const filteredBlocks = useMemo(() => Object.keys(blockData).filter((value) => value.includes(searchInput)), [searchInput, blockData]);
|
||||||
|
const blocksPerColumn = Math.floor(462 / (32 + 2));
|
||||||
|
|
||||||
|
const onClick = (block: string) => {
|
||||||
|
if (selectedBlocks.includes(block)) {
|
||||||
|
setSelectedBlocks((prev) => prev.filter((blockName) => blockName !== block));
|
||||||
|
} else {
|
||||||
|
setSelectedBlocks((prev) => [...prev, block]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stage
|
||||||
|
width={462}
|
||||||
|
height={Math.ceil(Object.keys(blockData).length / blocksPerColumn) * (32 + 2)}
|
||||||
|
options={{ backgroundAlpha: 0 }}
|
||||||
|
onMouseLeave={() => setHoverPosition(null)}
|
||||||
|
>
|
||||||
|
<Container>
|
||||||
|
{filteredBlocks.map((block, index) => {
|
||||||
|
const texture = textures[`${block}.png`] ?? missingTexture;
|
||||||
|
const x = (index % blocksPerColumn) * (32 + 2) + 2;
|
||||||
|
const y = Math.floor(index / blocksPerColumn) * (32 + 2) + 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sprite
|
||||||
|
key={block}
|
||||||
|
texture={texture}
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
scale={2}
|
||||||
|
interactive={true}
|
||||||
|
pointerover={() => setHoverPosition({ x, y })}
|
||||||
|
click={() => onClick(block)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{selectedBlocks.includes(block) && (
|
||||||
|
<Graphics
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
draw={(g) => {
|
||||||
|
g.clear();
|
||||||
|
g.beginFill(0x000000, 0.5);
|
||||||
|
g.lineStyle(2, isDark ? 0xffffff : 0x000000, 1, 1);
|
||||||
|
g.drawRect(0, 0, 32, 32);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{hoverPosition && (
|
||||||
|
<Graphics
|
||||||
|
x={hoverPosition.x}
|
||||||
|
y={hoverPosition.y}
|
||||||
|
draw={(g) => {
|
||||||
|
g.clear();
|
||||||
|
g.lineStyle(2, isDark ? 0xffffff : 0x000000, 1, 1);
|
||||||
|
g.drawRect(0, 0, 32, 32);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</Stage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlockSelector;
|
||||||
28
src/components/ui/checkbox.tsx
Normal file
28
src/components/ui/checkbox.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
|
import { Check } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Checkbox = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"peer h-4 w-4 shrink-0 rounded-sm border border-zinc-200 border-zinc-900 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-zinc-900 data-[state=checked]:text-zinc-50 dark:border-zinc-800 dark:border-zinc-50 dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300 dark:data-[state=checked]:bg-zinc-50 dark:data-[state=checked]:text-zinc-900",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
className={cn("flex items-center justify-center text-current")}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
))
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
53
src/components/ui/tabs.tsx
Normal file
53
src/components/ui/tabs.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-md bg-zinc-100 p-1 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-white transition-all 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=active]:bg-white data-[state=active]:text-zinc-950 data-[state=active]:shadow-sm dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300 dark:data-[state=active]:bg-zinc-950 dark:data-[state=active]:text-zinc-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export const TexturesProvider = ({ children }: Props) => {
|
||||||
const [solidTextures, setSolidTextures] = useState<Record<string, PIXI.Texture>>({});
|
const [solidTextures, setSolidTextures] = useState<Record<string, PIXI.Texture>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Load missing texture
|
// Load missing texture through data string just incase of network errors
|
||||||
const missingBaseTexture = new PIXI.BaseTexture(
|
const missingBaseTexture = new PIXI.BaseTexture(
|
||||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAGUlEQVR42mPABX4w/MCKaKJhVMPgcOuoBgDZRfgBVl5QdQAAAABJRU5ErkJggg=="
|
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAGUlEQVR42mPABX4w/MCKaKJhVMPgcOuoBgDZRfgBVl5QdQAAAABJRU5ErkJggg=="
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue