feat: update to react 19, pixi.js v8, tailwind v4, shad-cn

Also:

- center canvas on startup
- add version 1.21.9
This commit is contained in:
trafficlunar 2025-10-03 21:34:12 +01:00
parent 4193ebcaf4
commit 5eedff5bab
58 changed files with 3819 additions and 4765 deletions

View file

@ -1,15 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"config": "",
"css": "src/index.css",
"baseColor": "zinc",
"cssVariables": false,
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
@ -17,5 +18,5 @@
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
"registries": {}
}

View file

@ -10,58 +10,55 @@
"preview": "vite preview"
},
"dependencies": {
"@pixi/graphics-smooth": "^1.1.1",
"@pixi/react": "^7.1.2",
"@pixi/tilemap": "4.1.0",
"@radix-ui/react-checkbox": "^1.2.3",
"@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-menubar": "^1.1.12",
"@radix-ui/react-popover": "^1.1.11",
"@radix-ui/react-scroll-area": "^1.2.6",
"@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-separator": "^1.1.4",
"@radix-ui/react-slider": "^1.3.2",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.9",
"@radix-ui/react-toggle": "^1.1.6",
"@radix-ui/react-toggle-group": "^1.1.7",
"@radix-ui/react-tooltip": "^1.2.4",
"@uiw/react-color": "^2.5.0",
"@pixi/react": "^8.0.3",
"@pixi/tilemap": "5.0.2",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@uiw/react-color": "^2.8.1",
"@use-gesture/react": "^10.3.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"cmdk": "1.1.1",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.464.0",
"lucide-react": "^0.544.0",
"nbtify": "^2.2.0",
"pixi.js": "^7.4.3",
"react": "^18.3.1",
"pixi.js": "^8.13.2",
"react": "^19.1.1",
"react-device-detect": "^2.2.3",
"react-dom": "^18.3.1",
"react-dom": "^19.1.1",
"react-dropzone": "^14.3.8",
"react-router": "^7.5.2",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
"react-router": "^7.9.3",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@eslint/js": "^9.25.1",
"@types/node": "^22.15.2",
"@types/react": "^18.3.20",
"@types/react-dom": "^18.3.6",
"@vitejs/plugin-react": "^4.4.1",
"autoprefixer": "^10.4.21",
"eslint": "^9.25.1",
"@eslint/js": "^9.36.0",
"@tailwindcss/postcss": "^4.1.14",
"@types/node": "^24.6.1",
"@types/react": "^19.1.17",
"@types/react-dom": "^19.1.11",
"@vitejs/plugin-react": "^5.0.4",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^15.15.0",
"postcss": "^8.5.3",
"sharp": "^0.33.5",
"svgo": "^3.3.2",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.3",
"typescript-eslint": "^8.31.0",
"vite": "^6.3.3",
"vite-plugin-svgr": "^4.3.0"
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.45.0",
"vite": "^7.1.7",
"vite-plugin-svgr": "^4.5.0"
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
'@tailwindcss/postcss': {},
},
}

View file

@ -10,6 +10,7 @@ import { numberToVersion, versionToNumber } from "@/utils/version";
import { HistoryContext } from "@/context/History";
const versions = [
"1.21.9",
"1.21.4",
"1.21",
"1.20",

View file

@ -2,7 +2,7 @@
import { useEffect, useRef } from "react";
import * as PIXI from "pixi.js";
import { useApp } from "@pixi/react";
import { useApplication } from "@pixi/react";
import { CompositeTilemap, settings } from "@pixi/tilemap";
interface Props {
@ -19,9 +19,9 @@ interface Props {
settings.use32bitIndex = true;
function Blocks({ blocks, missingTexture, textures, coords, scale, version, isSelectionLayer }: Props) {
const app = useApp();
const tilemapRef = useRef<CompositeTilemap>();
const containerRef = useRef<PIXI.Container>();
const app = useApplication().app;
const tilemapRef = useRef<CompositeTilemap | null>(null);
const containerRef = useRef<PIXI.Container | null>(null);
useEffect(() => {
if (!tilemapRef.current) return;
@ -29,7 +29,6 @@ function Blocks({ blocks, missingTexture, textures, coords, scale, version, isSe
tilemap.clear();
// Tile solid colors at smaller scales
blocks.forEach((block) => {
tilemap.tile(textures[block.name] ?? missingTexture, block.x * 16, block.y * 16);
});
@ -48,7 +47,7 @@ function Blocks({ blocks, missingTexture, textures, coords, scale, version, isSe
tilemapRef.current = tilemap;
container.addChild(tilemap);
if (isSelectionLayer) container.filters = [new PIXI.AlphaFilter(0.5)];
if (isSelectionLayer) container.filters = [new PIXI.AlphaFilter({ alpha: 0.5 })];
}, []);
useEffect(() => {

View file

@ -3,8 +3,9 @@ import { useGesture } from "@use-gesture/react";
import { isMobile } from "react-device-detect";
import * as PIXI from "pixi.js";
import { Container, Stage } from "@pixi/react";
import { Application } from "@pixi/react";
import { LoadingContext } from "@/context/Loading";
import { CanvasContext } from "@/context/Canvas";
import { HistoryContext } from "@/context/History";
import { SelectionContext } from "@/context/Selection";
@ -39,10 +40,12 @@ import CanvasInformation from "./information/Canvas";
import SelectionBar from "./SelectionBar";
// Set scale mode to NEAREST
PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST;
PIXI.TextureSource.defaultOptions.scaleMode = "nearest";
function Canvas() {
const { stageSize, canvasSize, blocks, coords, scale, version, setStageSize, setBlocks, setCoords, setScale } = useContext(CanvasContext);
const { loading } = useContext(LoadingContext);
const { stageSize, canvasSize, blocks, coords, scale, version, setStageSize, setBlocks, setCoords, setScale, centerCanvas } =
useContext(CanvasContext);
const { addHistory, undo, redo } = useContext(HistoryContext);
const { selectionCoords, selectionLayerBlocks, setSelectionCoords, setSelectionLayerBlocks } = useContext(SelectionContext);
const { settings } = useContext(SettingsContext);
@ -59,10 +62,11 @@ function Canvas() {
const mouseMovementRef = useRef<Position>({ x: 0, y: 0 });
const dragging = useRef(false);
const dragStartCoordsRef = useRef<Position>({ x: 0, y: 0 });
const hasCenteredRef = useRef(false); // For centering canvas on startup
const holdingShiftRef = useRef(false);
const holdingAltRef = useRef(false);
const oldToolRef = useRef<Tool>();
const oldToolRef = useRef<Tool>(null);
const [cssCursor, setCssCursor] = useState("crosshair");
const startBlocksRef = useRef<Block[]>([]);
@ -447,7 +451,7 @@ function Canvas() {
resizeCanvas();
return () => resizeObserver.disconnect();
}, [stageContainerRef, setStageSize]);
}, [loading, setStageSize]);
// Window events handler
useEffect(() => {
@ -478,11 +482,19 @@ function Canvas() {
}
);
// Center canvas on startup
useEffect(() => {
if (hasCenteredRef.current || loading) return;
centerCanvas();
hasCenteredRef.current = true;
}, [centerCanvas]);
if (loading) return null;
return (
<div ref={stageContainerRef} className="relative">
<Stage
width={stageSize.width}
height={stageSize.height}
<div
ref={stageContainerRef}
tabIndex={0}
onClick={onClick}
onKeyDown={onKeyDown}
@ -491,10 +503,10 @@ function Canvas() {
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onWheel={onWheel}
options={{ backgroundAlpha: 0 }}
style={{ cursor: cssCursor }}
className="w-full h-full bg-zinc-100 dark:bg-black touch-none select-none"
className="relative"
>
<Application resizeTo={stageContainerRef} backgroundAlpha={0} className="touch-none select-none">
<Blocks blocks={visibleBlocks} missingTexture={missingTexture} textures={textures} coords={coords} scale={scale} version={version} />
{/* Selection layer */}
<Blocks
@ -507,18 +519,18 @@ function Canvas() {
version={version}
/>
<Container x={coords.x} y={coords.y} scale={scale}>
<pixiContainer x={coords.x} y={coords.y} scale={scale}>
{settings.canvasBorder && <CanvasBorder canvasSize={canvasSize} isDark={isDark} />}
<Cursor mouseCoords={mouseCoords} radius={radius} isDark={isDark} />
<Selection selection={selectionCoords} coords={coords} scale={scale} isDark={isDark} />
</Container>
<Selection selection={selectionCoords} isDark={isDark} />
</pixiContainer>
{settings.grid && (
<Container filters={[new PIXI.AlphaFilter(0.1)]}>
<pixiContainer filters={[new PIXI.AlphaFilter({ alpha: 0.1 })]}>
<Grid stageSize={stageSize} coords={coords} scale={scale} isDark={isDark} />
</Container>
</pixiContainer>
)}
</Stage>
</Application>
<CursorInformation mouseCoords={mouseCoords} />
<CanvasInformation />

View file

@ -1,5 +1,3 @@
import { Graphics } from "@pixi/react";
interface Props {
canvasSize: BoundingBox;
isDark: boolean;
@ -7,11 +5,11 @@ interface Props {
function CanvasBorder({ canvasSize, isDark }: Props) {
return (
<Graphics
<pixiGraphics
draw={(g) => {
g.clear();
g.lineStyle(2, isDark ? 0xffffff : 0x000000, 0.25, 1);
g.drawRect(canvasSize.minX * 16, canvasSize.minY * 16, (canvasSize.maxX - canvasSize.minX) * 16, (canvasSize.maxY - canvasSize.minY) * 16);
g.rect(canvasSize.minX * 16, canvasSize.minY * 16, (canvasSize.maxX - canvasSize.minX) * 16, (canvasSize.maxY - canvasSize.minY) * 16);
g.stroke({ width: 2, color: isDark ? 0xffffff : 0x000000, alpha: 0.25, alignment: 0 });
}}
/>
);

View file

@ -1,5 +1,3 @@
import { Graphics } from "@pixi/react";
interface Props {
mouseCoords: Position;
radius: number;
@ -14,13 +12,13 @@ function Cursor({ mouseCoords, radius, isDark }: Props) {
const size = radius * 16;
return (
<Graphics
<pixiGraphics
x={(mouseCoords.x + offset) * 16}
y={(mouseCoords.y + offset) * 16}
draw={(g) => {
g.clear();
g.lineStyle(1, isDark ? 0xffffff : 0x000000, 1);
g.drawRect(0, 0, size, size);
g.rect(0, 0, size, size);
g.stroke({ width: 1, color: isDark ? 0xffffff : 0x000000, alignment: 1 });
}}
/>
);

View file

@ -1,5 +1,3 @@
import { Graphics } from "@pixi/react";
interface Props {
stageSize: Dimension;
coords: Position;
@ -9,10 +7,9 @@ interface Props {
function Grid({ stageSize, coords, scale, isDark }: Props) {
return (
<Graphics
<pixiGraphics
draw={(g) => {
g.clear();
g.lineStyle(1, isDark ? 0xffffff : 0x000000);
const tileSize = 16 * scale;
@ -25,6 +22,8 @@ function Grid({ stageSize, coords, scale, isDark }: Props) {
g.moveTo(0, y);
g.lineTo(stageSize.width, y);
}
g.stroke({ width: 1, color: isDark ? 0xffffff : 0x000000 });
}}
/>
);

View file

@ -1,26 +1,16 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useRef } from "react";
import { useApp } from "@pixi/react";
import { DashLineShader, SmoothGraphics } from "@pixi/graphics-smooth";
interface Props {
selection: CoordinateArray;
coords: Position;
scale: number;
isDark: boolean;
}
const shader = new DashLineShader({ dash: 8, gap: 5 });
const DASH_LENGTH = 8;
const GAP_LENGTH = 5;
function Selection({ selection, coords, scale, isDark }: Props) {
const app = useApp();
const selectionRef = useRef<SmoothGraphics>();
const drawSelection = () => {
if (!selectionRef.current) return;
const graphics = selectionRef.current;
graphics.clear();
graphics.lineStyle({ width: 1, color: isDark ? 0xffffff : 0x000000, shader });
function Selection({ selection, isDark }: Props) {
return (
<pixiGraphics
draw={(g) => {
g.clear();
const edges = new Set<string>();
@ -41,33 +31,41 @@ function Selection({ selection, coords, scale, isDark }: Props) {
});
});
// Draw the remaining edges
// Draw each remaining edge
edges.forEach((edge) => {
const [x1, y1, x2, y2] = JSON.parse(edge);
graphics.moveTo(x1 * 16 * scale, y1 * 16 * scale);
graphics.lineTo(x2 * 16 * scale, y2 * 16 * scale);
const [x1, y1, x2, y2] = JSON.parse(edge) as [number, number, number, number];
// Draw dashed line
const startX = x1 * 16;
const startY = y1 * 16;
const endX = x2 * 16;
const endY = y2 * 16;
const dx = endX - startX;
const dy = endY - startY;
const lineLength = Math.hypot(dx, dy);
const angle = Math.atan2(dy, dx);
let drawn = 0;
while (drawn < lineLength) {
const segmentLength = Math.min(DASH_LENGTH, lineLength - drawn);
const sx = startX + Math.cos(angle) * drawn;
const sy = startY + Math.sin(angle) * drawn;
const ex = sx + Math.cos(angle) * segmentLength;
const ey = sy + Math.sin(angle) * segmentLength;
g.moveTo(sx, sy);
g.lineTo(ex, ey);
drawn += DASH_LENGTH + GAP_LENGTH;
}
});
};
useEffect(() => {
const graphics = new SmoothGraphics();
selectionRef.current = graphics;
drawSelection();
app.stage.addChild(graphics);
}, []);
useEffect(() => {
if (!selectionRef.current) return;
const graphics = selectionRef.current;
graphics.x = coords.x;
graphics.y = coords.y;
drawSelection();
}, [coords]);
useEffect(drawSelection, [selection]);
return null;
// Render the lines
g.stroke({ width: 2, color: isDark ? 0xffffff : 0x000000, alignment: 0 });
}}
/>
);
}
export default Selection;

View file

@ -58,11 +58,11 @@ function SelectionBar({ startBlocks, startSelectionCoords }: Props) {
${isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-6 pointer-events-none"}
`}
>
<Button variant="ghost" className="w-8 h-8" onClick={cancel}>
<Button variant="ghost" className="size h-8" onClick={cancel}>
<XIcon />
</Button>
<span className="mx-2 text-[0.85rem]">Confirm?</span>
<Button variant="ghost" className="w-8 h-8" onClick={confirm}>
<Button variant="ghost" className="size h-8" onClick={confirm}>
<CheckIcon />
</Button>
</div>

View file

@ -38,7 +38,7 @@ function CanvasInformation() {
value={[Math.log10(scale)]}
onValueChange={onValueChange}
orientation="vertical"
className="!h-32"
className="h-32!"
/>
</div>
</div>

View file

@ -108,7 +108,7 @@ function OpenImage({ close }: DialogProps) {
setLoading(true);
// Wait for loading indicator to appear
await new Promise((resolve) => setTimeout(resolve, 1));
await new Promise((resolve) => setTimeout(resolve, 5));
// Load image through JS canvas
const canvas = document.createElement("canvas");
@ -150,9 +150,14 @@ function OpenImage({ close }: DialogProps) {
}
};
// Trying to center and close above doesn't work for some reason
useEffect(() => {
if (!isFinished.current) return;
// Wrap in requestAnimationFrame() to fix bug where canvas is black
requestAnimationFrame(() => {
centerCanvas();
});
close();
return () => {
@ -224,13 +229,13 @@ function OpenImage({ close }: DialogProps) {
</p>
</div>
<div className="grid grid-cols-[auto,1fr] gap-2">
<div className="grid grid-cols-[auto_1fr] gap-2">
{image && acceptedFiles[0] && (
<>
<img
src={image.src}
alt="your image"
className="w-48 h-48 object-contain border rounded-lg"
className="size-48 object-contain border rounded-lg"
style={{ background: "repeating-conic-gradient(#fff 0 90deg, #bbb 0 180deg) 0 0/25% 25%" }}
/>
@ -259,7 +264,7 @@ function OpenImage({ close }: DialogProps) {
variant="outline"
pressed={linkAspectRatio}
onPressedChange={() => setLinkAspectRatio(!linkAspectRatio)}
className="h-8 !min-w-8 p-0 mt-auto mb-1"
className="h-8 min-w-8! p-0 mt-auto mb-1"
>
<LinkIcon />
</Toggle>
@ -344,7 +349,7 @@ function OpenImage({ close }: DialogProps) {
</Tabs>
</div>
<DialogFooter className="!justify-between">
<DialogFooter className="justify-between!">
<VersionCombobox version={version} setVersion={setVersion} isContext />
<div className="flex gap-2">

View file

@ -17,11 +17,12 @@ function SaveImage({ close, registerSubmit, dialogKeyHandler }: DialogProps) {
const [fileName, setFileName] = useState("blockmatic");
const textures = useTextures(version);
const onSubmit = () => {
const onSubmit = async () => {
const width = canvasSize.maxX - canvasSize.minX;
const height = canvasSize.maxY - canvasSize.minY;
const renderer = new PIXI.Renderer({
const app = new PIXI.Application();
await app.init({
width: 16 * width,
height: 16 * height,
backgroundAlpha: 1,
@ -35,15 +36,10 @@ function SaveImage({ close, registerSubmit, dialogKeyHandler }: DialogProps) {
container.addChild(sprite);
});
const renderTexture = PIXI.RenderTexture.create({
width: 16 * width,
height: 16 * height,
});
app.stage.addChild(container);
app.renderer.render(app.stage);
renderer.render(container, { renderTexture });
const canvas = renderer.extract.canvas(renderTexture);
canvas.toBlob!((blob) => {
app.canvas.toBlob!((blob) => {
if (!blob) return;
const link = document.createElement("a");
@ -52,10 +48,9 @@ function SaveImage({ close, registerSubmit, dialogKeyHandler }: DialogProps) {
link.click();
URL.revokeObjectURL(link.href);
});
renderer.destroy();
}, "image/png");
app.destroy(true);
close();
};

View file

@ -1,5 +1,4 @@
import React, { useContext, useMemo, useRef, useState } from "react";
import { Container, Graphics, Sprite, Stage } from "@pixi/react";
import React, { useContext, useMemo, useState } from "react";
import { CanvasContext } from "@/context/Canvas";
import { ThemeContext } from "@/context/Theme";
@ -7,13 +6,14 @@ import { TexturesContext } from "@/context/Textures";
import { useBlockData } from "@/hooks/useBlockData";
import { useTextures } from "@/hooks/useTextures";
import { Application } from "@pixi/react";
interface Props {
stageWidth: number;
searchInput: string;
selectedBlocks: string[];
setSelectedBlocks: React.Dispatch<React.SetStateAction<string[]>>;
userModifiedBlocks: React.MutableRefObject<boolean>;
userModifiedBlocks: React.RefObject<boolean>;
}
function BlockSelector({ stageWidth, searchInput, selectedBlocks, setSelectedBlocks, userModifiedBlocks }: Props) {
@ -25,7 +25,6 @@ function BlockSelector({ stageWidth, searchInput, selectedBlocks, setSelectedBlo
const textures = useTextures(version);
const [hoverPosition, setHoverPosition] = useState<Position | null>(null);
const showStage = useRef(true);
const filteredBlocks = useMemo(() => Object.keys(blockData).filter((value) => value.includes(searchInput)), [searchInput, blockData]);
const blocksPerColumn = Math.floor(stageWidth / (32 + 2));
@ -40,48 +39,37 @@ function BlockSelector({ stageWidth, searchInput, selectedBlocks, setSelectedBlo
}
};
// Fixes issue #1 - entire app crashing when closing dialog with Stage mounted
if (!showStage.current) return null;
return (
<Stage
width={stageWidth}
height={Math.ceil(Object.keys(blockData).length / blocksPerColumn) * (32 + 2)}
options={{ backgroundAlpha: 0 }}
onPointerLeave={() => setHoverPosition(null)}
onUnmount={() => {
// NOTE: this event gets called a couple times when run in development
showStage.current = false;
}}
>
<Container>
<div onPointerLeave={() => setHoverPosition(null)}>
<Application width={stageWidth} height={Math.ceil(Object.keys(blockData).length / blocksPerColumn) * (32 + 2)} backgroundAlpha={0}>
<pixiContainer>
{filteredBlocks.map((block, index) => {
const x = (index % blocksPerColumn) * (32 + 2) + 2;
const y = Math.floor(index / blocksPerColumn) * (32 + 2) + 2;
return (
<>
<Sprite
<pixiSprite
key={block}
texture={textures[block] ?? missingTexture}
x={x}
y={y}
scale={2}
eventMode={"static"}
pointerover={() => setHoverPosition({ x, y })}
click={() => onClick(block)}
onPointerOver={() => setHoverPosition({ x, y })}
onClick={() => onClick(block)}
alpha={selectedBlocks.includes(block) ? 1 : 0.2}
/>
{selectedBlocks.includes(block) && (
<Graphics
<pixiGraphics
key={index}
x={x}
y={y}
draw={(g) => {
g.clear();
g.lineStyle(2, isDark ? 0xffffff : 0x000000, 0.4, 0);
g.drawRect(0, 0, 32, 32);
g.rect(0, 0, 32, 32);
g.stroke({ width: 2, color: isDark ? 0xffffff : 0x000000, alpha: 0.4, alignment: 0 });
}}
/>
)}
@ -90,18 +78,19 @@ function BlockSelector({ stageWidth, searchInput, selectedBlocks, setSelectedBlo
})}
{hoverPosition && (
<Graphics
<pixiGraphics
x={hoverPosition.x}
y={hoverPosition.y}
draw={(g) => {
g.clear();
g.lineStyle(4, isDark ? 0xffffff : 0x000000, 1, 1);
g.drawRect(0, 0, 32, 32);
g.rect(0, 0, 32, 32);
g.stroke({ width: 4, color: isDark ? 0xffffff : 0x000000, alignment: 1 });
}}
/>
)}
</Container>
</Stage>
</pixiContainer>
</Application>
</div>
);
}

View file

@ -15,7 +15,7 @@ function ImageComparison() {
return (
<div
onPointerMove={onPointerMove}
className="relative select-none w-full aspect-[270/217] aspect flex justify-center rounded-xl border border-zinc-200 dark:border-zinc-800 shadow-md"
className="relative select-none w-full aspect-270/217 aspect flex justify-center rounded-xl border border-zinc-200 dark:border-zinc-800 shadow-md"
>
<img
src="/bliss/bliss_original.png"
@ -40,7 +40,7 @@ function ImageComparison() {
}}
>
<button className="bg-zinc-200 rounded hover:scale-110 transition-all w-5 h-10 select-none -translate-y-1/2 absolute top-1/2 -ml-2 z-30 cursor-ew-resize flex justify-center items-center">
<GripVerticalIcon color="black" className="w-4 h-4" />
<GripVerticalIcon color="black" className="size-4" />
</button>
</div>

View file

@ -35,7 +35,7 @@ function Menubar() {
<SelectMenu />
<ViewMenu />
<div className="!ml-auto pl-4 grid grid-cols-3 items-center gap-1 min-w-20">
<div className="ml-auto! pl-4 grid grid-cols-3 items-center gap-1 min-w-20">
<ThemeIcon inApp />
<a href="https://github.com/trafficlunar/blockmatic" className="w-5">
<GithubIcon fill={isDark ? "white" : "black"} />

View file

@ -1,14 +1,16 @@
import { useContext, useEffect, useMemo, useState } from "react";
import { Container, Graphics, Sprite, Stage } from "@pixi/react";
import { BlocksIcon } from "lucide-react";
import { Application } from "@pixi/react";
import * as PIXI from "pixi.js";
import { LoadingContext } from "@/context/Loading";
import { CanvasContext } from "@/context/Canvas";
import { ThemeContext } from "@/context/Theme";
import { ToolContext } from "@/context/Tool";
import { useBlockData } from "@/hooks/useBlockData";
import { useTextures } from "@/hooks/useTextures";
import { Application } from "pixi.js";
interface Props {
stageWidth: number;
@ -16,11 +18,12 @@ interface Props {
}
function BlockSelector({ stageWidth, searchInput }: Props) {
const { loading } = useContext(LoadingContext);
const { version } = useContext(CanvasContext);
const { isDark } = useContext(ThemeContext);
const { selectedBlock, setSelectedBlock } = useContext(ToolContext);
const [app, setApp] = useState<Application>();
const [app, setApp] = useState<PIXI.Application>();
const [hoverPosition, setHoverPosition] = useState<Position | null>(null);
const [selectedBlockPosition, setSelectedBlockPosition] = useState<Position | null>({ x: 0, y: 0 });
@ -48,15 +51,17 @@ function BlockSelector({ stageWidth, searchInput }: Props) {
}, [searchInput, selectedBlock]);
useEffect(() => {
if (!app?.renderer?.view?.style) return;
if (!app?.renderer?.view?.canvas.style) return;
// Can't set it in props for some reason
app.renderer.view.style.touchAction = "auto";
app.renderer.view.canvas.style.touchAction = "auto";
}, [app]);
if (loading) return null;
if (filteredBlocks.length == 0) {
return (
<div className="w-full h-full flex flex-col justify-center items-center gap-1 text-zinc-400">
<div className="size-full flex flex-col justify-center items-center gap-1 text-zinc-400">
<BlocksIcon size={40} />
<span>No blocks found</span>
</div>
@ -64,14 +69,14 @@ function BlockSelector({ stageWidth, searchInput }: Props) {
}
return (
<Stage
<div onMouseLeave={() => setHoverPosition(null)} className="h-min">
<Application
width={stageWidth}
height={Math.ceil(Object.keys(blockData).length / blocksPerColumn) * (32 + 2) + 8}
options={{ backgroundAlpha: 0 }}
onMouseLeave={() => setHoverPosition(null)}
onMount={setApp}
backgroundAlpha={0}
onInit={setApp}
>
<Container>
<pixiContainer>
{filteredBlocks.map((block, index) => {
const texture = textures[block];
const { x, y } = getBlockPosition(index);
@ -82,46 +87,47 @@ function BlockSelector({ stageWidth, searchInput }: Props) {
};
return (
<Sprite
<pixiSprite
key={block}
texture={texture}
x={x}
y={y}
scale={2}
eventMode={"static"}
mouseover={() => setHoverPosition({ x, y })}
click={onClick}
tap={onClick}
onMouseOver={() => setHoverPosition({ x, y })}
onClick={onClick}
onTap={onClick}
alpha={selectedBlock == block ? 1 : 0.3}
/>
);
})}
{hoverPosition && (
<Graphics
<pixiGraphics
x={hoverPosition.x}
y={hoverPosition.y}
draw={(g) => {
g.clear();
g.lineStyle(4, isDark ? 0xffffff : 0x000000, 1, 1);
g.drawRect(0, 0, 32, 32);
g.rect(0, 0, 32, 32);
g.stroke({ width: 2, color: isDark ? 0xffffff : 0x000000, alignment: 1 });
}}
/>
)}
{selectedBlockPosition && (
<Graphics
<pixiGraphics
x={selectedBlockPosition.x}
y={selectedBlockPosition.y}
draw={(g) => {
g.clear();
g.lineStyle(2, isDark ? 0xffffff : 0x000000, 0.75, 0);
g.drawRect(0, 0, 32, 32);
g.rect(0, 0, 32, 32);
g.stroke({ width: 2, color: isDark ? 0xffffff : 0x000000, alpha: 0.75, alignment: 0 });
}}
/>
)}
</Container>
</Stage>
</pixiContainer>
</Application>
</div>
);
}

View file

@ -1,5 +1,4 @@
import { useContext, useEffect, useState } from "react";
import { Container, Sprite, Stage } from "@pixi/react";
import { CanvasContext } from "@/context/Canvas";
import { HistoryContext } from "@/context/History";
@ -11,6 +10,7 @@ import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { useTextures } from "@/hooks/useTextures";
import { Application } from "@pixi/react";
function Replace() {
const { version, setBlocks } = useContext(CanvasContext);
@ -80,11 +80,11 @@ function Replace() {
onClick={() => onClickBlockButton(1)}
className="h-10 rounded-md border border-zinc-200 dark:border-zinc-800 dark:bg-zinc-950 flex justify-center items-center"
>
<Stage width={32} height={32} options={{ backgroundAlpha: 0 }}>
<Container>
<Sprite texture={textures[block1] ?? missingTexture} scale={2} />
</Container>
</Stage>
<Application width={32} height={32} backgroundAlpha={0}>
<pixiContainer>
<pixiSprite texture={textures[block1] ?? missingTexture} scale={2} />
</pixiContainer>
</Application>
</button>
<Label htmlFor="radius">Block 2</Label>
@ -92,11 +92,11 @@ function Replace() {
onClick={() => onClickBlockButton(2)}
className="h-10 rounded-md border border-zinc-200 dark:border-zinc-800 dark:bg-zinc-950 flex justify-center items-center"
>
<Stage width={32} height={32} options={{ backgroundAlpha: 0 }}>
<Container>
<Sprite texture={textures[block2] ?? missingTexture} scale={2} />
</Container>
</Stage>
<Application width={32} height={32} backgroundAlpha={0}>
<pixiContainer>
<pixiSprite texture={textures[block2] ?? missingTexture} scale={2} />
</pixiContainer>
</Application>
</button>
<br />

View file

@ -25,7 +25,7 @@ function ToolSettings() {
{tool === "shape" && (
<>
<Label htmlFor="shape">Shape</Label>
<Select value={shape} onValueChange={(value) => setShape(value as Shape)}>
<Select value={shape} onValueChange={(value: string) => setShape(value as Shape)}>
<SelectTrigger>
<SelectValue placeholder="Select a shape" />
</SelectTrigger>
@ -39,7 +39,7 @@ function ToolSettings() {
</Select>
<Label htmlFor="filled">Filled</Label>
<Checkbox name="filled" checked={filled} onCheckedChange={(checked) => setFilled(!!checked)} className="w-6 h-6" />
<Checkbox name="filled" checked={filled} onCheckedChange={(checked) => setFilled(!!checked)} className="size-6" />
</>
)}
</div>

View file

@ -153,7 +153,7 @@ function Sidebar() {
{settings.blockSelector && (
<>
<Input placeholder="Search for blocks..." value={searchInput} onChange={(e) => setSearchInput(e.target.value)} />
<ScrollArea ref={divRef} className={`w-full flex-1 pb-0 ${isMobileView ? "min-h-48" : ""}`}>
<ScrollArea ref={divRef} className={`h-48 w-full flex-1 pb-0 ${isMobileView ? "min-h-48" : ""}`}>
<BlockSelector stageWidth={stageWidth} searchInput={searchInput} />
</ScrollArea>
</>

View file

@ -1,5 +1,5 @@
import { useContext, useEffect, useRef, useState } from "react";
import { Container, Sprite, Stage } from "@pixi/react";
import { useContext, useEffect, useState } from "react";
import { Application } from "@pixi/react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
@ -18,7 +18,6 @@ function SelectedBlock() {
const { selectedBlock } = useContext(ToolContext);
const textures = useTextures(version);
const divRef = useRef<HTMLDivElement>(null);
const [selectedBlockName, setSelectedBlockName] = useState("Stone");
@ -31,12 +30,12 @@ function SelectedBlock() {
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<div ref={divRef} className="absolute bottom-1 w-8 h-8 outline outline-1 outline-zinc-800 dark:outline-zinc-200 rounded">
<Stage width={divRef.current?.clientWidth} height={divRef.current?.clientHeight}>
<Container>
<Sprite texture={textures[selectedBlock] ?? missingTexture} scale={2} />
</Container>
</Stage>
<div className="absolute bottom-1 w-8 h-8 outline-solid outline-1 outline-zinc-800 dark:outline-zinc-200 rounded p-0!">
<Application width={32} height={32}>
<pixiContainer>
<pixiSprite texture={textures[selectedBlock] ?? missingTexture} scale={2} />
</pixiContainer>
</Application>
</div>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={10}>

View file

@ -41,14 +41,16 @@ function Toolbar() {
type="single"
value={tool}
onValueChange={onToolChange}
className="flex flex-col justify-start py-1 border-r border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950"
className="w-full flex flex-col items-center border-r border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 *:py-0.5"
>
{/* Hand */}
<Tooltip delayDuration={0}>
<TooltipTrigger>
<ToggleGroupItem value="hand" className="!p-0 !h-8 !min-w-8">
<TooltipTrigger asChild>
<span>
<ToggleGroupItem value="hand" className="p-0! h-8! min-w-8! rounded-xl">
<HandIcon />
</ToggleGroupItem>
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={10}>
<p>Hand (H)</p>
@ -57,10 +59,12 @@ function Toolbar() {
{/* Move */}
<Tooltip delayDuration={0}>
<TooltipTrigger>
<ToggleGroupItem value="move" className="!p-0 !h-8 !min-w-8">
<TooltipTrigger asChild>
<span>
<ToggleGroupItem value="move" className="p-0! h-8! min-w-8! rounded-xl">
<MousePointer2Icon />
</ToggleGroupItem>
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={10}>
<p>Move (V)</p>
@ -69,10 +73,12 @@ function Toolbar() {
{/* Rectangle Select */}
<Tooltip delayDuration={0}>
<TooltipTrigger>
<ToggleGroupItem value="rectangle-select" className="!p-0 !h-8 !min-w-8">
<TooltipTrigger asChild>
<span>
<ToggleGroupItem value="rectangle-select" className="p-0! h-8! min-w-8! rounded-xl">
<SquareDashedIcon />
</ToggleGroupItem>
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={10}>
<p>Rectangle Select (M)</p>
@ -81,10 +87,12 @@ function Toolbar() {
{/* Lasso */}
<Tooltip delayDuration={0}>
<TooltipTrigger>
<ToggleGroupItem value="lasso" className="!p-0 !h-8 !min-w-8">
<TooltipTrigger asChild>
<span>
<ToggleGroupItem value="lasso" className="p-0! h-8! min-w-8! rounded-xl">
<LassoIcon />
</ToggleGroupItem>
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={10}>
<p>Lasso (L)</p>
@ -93,10 +101,12 @@ function Toolbar() {
{/* Magic Wand */}
<Tooltip delayDuration={0}>
<TooltipTrigger>
<ToggleGroupItem value="magic-wand" className="!p-0 !h-8 !min-w-8">
<TooltipTrigger asChild>
<span>
<ToggleGroupItem value="magic-wand" className="p-0! h-8! min-w-8! rounded-xl">
<WandIcon />
</ToggleGroupItem>
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={10}>
<p>Magic Wand (W)</p>
@ -105,10 +115,12 @@ function Toolbar() {
{/* Pencil */}
<Tooltip delayDuration={0}>
<TooltipTrigger>
<ToggleGroupItem value="pencil" className="!p-0 !h-8 !min-w-8">
<TooltipTrigger asChild>
<span>
<ToggleGroupItem value="pencil" className="p-0! h-8! min-w-8! rounded-xl">
<PencilIcon />
</ToggleGroupItem>
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={10}>
<p>Pencil (B)</p>
@ -117,10 +129,12 @@ function Toolbar() {
{/* Eraser */}
<Tooltip delayDuration={0}>
<TooltipTrigger>
<ToggleGroupItem value="eraser" className="!p-0 !h-8 !min-w-8">
<TooltipTrigger asChild>
<span>
<ToggleGroupItem value="eraser" className="p-0! h-8! min-w-8! rounded-xl">
<EraserIcon />
</ToggleGroupItem>
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={10}>
<p>Eraser (E)</p>
@ -129,10 +143,12 @@ function Toolbar() {
{/* Paint Bucket */}
<Tooltip delayDuration={0}>
<TooltipTrigger>
<ToggleGroupItem value="paint-bucket" className="!p-0 !h-8 !min-w-8">
<TooltipTrigger asChild>
<span>
<ToggleGroupItem value="paint-bucket" className="p-0! h-8! min-w-8! rounded-xl">
<PaintBucketIcon />
</ToggleGroupItem>
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={10}>
<p>Paint Bucket (G)</p>
@ -141,10 +157,12 @@ function Toolbar() {
{/* Shape */}
<Tooltip delayDuration={0}>
<TooltipTrigger>
<ToggleGroupItem value="shape" className="!p-0 !h-8 !min-w-8">
<TooltipTrigger asChild>
<span>
<ToggleGroupItem value="shape" className="p-0! h-8! min-w-8! rounded-xl">
{ShapeIconComponent && <ShapeIconComponent />}
</ToggleGroupItem>
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={10}>
<p>Shape (U)</p>
@ -153,10 +171,12 @@ function Toolbar() {
{/* Eyedropper */}
<Tooltip delayDuration={0}>
<TooltipTrigger>
<ToggleGroupItem value="eyedropper" className="!p-0 !h-8 !min-w-8">
<TooltipTrigger asChild>
<span>
<ToggleGroupItem value="eyedropper" className="p-0! h-8! min-w-8! rounded-xl">
<PipetteIcon />
</ToggleGroupItem>
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={10}>
<p>Eyedropper (I)</p>
@ -165,10 +185,12 @@ function Toolbar() {
{/* Zoom */}
<Tooltip delayDuration={0}>
<TooltipTrigger>
<ToggleGroupItem value="zoom" className="!p-0 !h-8 !min-w-8">
<TooltipTrigger asChild>
<span>
<ToggleGroupItem value="zoom" className="p-0! h-8! min-w-8! rounded-xl">
<ZoomInIcon />
</ToggleGroupItem>
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={10}>
<p>Zoom (Z)</p>

View file

@ -5,25 +5,26 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-zinc-900 text-zinc-50 hover:bg-zinc-900/90 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-50/90",
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-red-500 text-zinc-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-zinc-50 dark:hover:bg-red-900/90",
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border border-zinc-200 bg-white hover:bg-zinc-100 hover:text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-800 dark:hover:text-zinc-50",
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-zinc-100 text-zinc-900 hover:bg-zinc-100/80 dark:bg-zinc-800 dark:text-zinc-50 dark:hover:bg-zinc-800/80",
ghost: "hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-50",
link: "text-zinc-900 underline-offset-4 hover:underline dark:text-zinc-50",
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
@ -33,24 +34,25 @@ const buttonVariants = cva(
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View file

@ -40,12 +40,7 @@ function useCarousel() {
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
function Carousel({
orientation = "horizontal",
opts,
setApi,
@ -53,9 +48,7 @@ const Carousel = React.forwardRef<
className,
children,
...props
},
ref
) => {
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
@ -67,10 +60,7 @@ const Carousel = React.forwardRef<
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return
}
if (!api) return
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
@ -97,18 +87,12 @@ const Carousel = React.forwardRef<
)
React.useEffect(() => {
if (!api || !setApi) {
return
}
if (!api || !setApi) return
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
if (!api) return
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
@ -133,11 +117,11 @@ const Carousel = React.forwardRef<
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
@ -145,19 +129,17 @@ const Carousel = React.forwardRef<
</CarouselContext.Provider>
)
}
)
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
@ -167,20 +149,16 @@ const CarouselContent = React.forwardRef<
/>
</div>
)
})
CarouselContent.displayName = "CarouselContent"
}
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
@ -189,24 +167,25 @@ const CarouselItem = React.forwardRef<
{...props}
/>
)
})
CarouselItem.displayName = "CarouselItem"
}
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref}
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
"absolute size-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
@ -214,28 +193,29 @@ const CarouselPrevious = React.forwardRef<
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = "CarouselPrevious"
}
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref}
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
"absolute size-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
@ -243,12 +223,11 @@ const CarouselNext = React.forwardRef<
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
)
})
CarouselNext.displayName = "CarouselNext"
}
export {
type CarouselApi,

View file

@ -1,28 +1,30 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { CheckIcon } 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) => (
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
ref={ref}
data-slot="checkbox"
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",
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<Check className="h-4 w-4" />
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
)
}
export { Checkbox }

View file

@ -1,31 +1,56 @@
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
ref={ref}
data-slot="command"
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-white text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50",
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
)
}
const CommandDialog = ({ children, ...props }: DialogProps) => {
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
}) {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-zinc-500 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5 dark:[&_[cmdk-group-heading]]:text-zinc-400">
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn("overflow-hidden p-0", className)}
showCloseButton={showCloseButton}
>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
@ -33,110 +58,116 @@ const CommandDialog = ({ children, ...props }: DialogProps) => {
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
data-slot="command-input"
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-zinc-500 disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-zinc-400",
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
)
}
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-zinc-950 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-zinc-500 dark:text-zinc-50 dark:[&_[cmdk-group-heading]]:text-zinc-400",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-zinc-200 dark:bg-zinc-800", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-zinc-100 data-[selected=true]:text-zinc-900 data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:data-[selected='true']:bg-zinc-800 dark:data-[selected=true]:text-zinc-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
function CommandList({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<span
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"ml-auto text-xs tracking-widest text-zinc-500 dark:text-zinc-400",
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}
/>
)
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
)
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,

View file

@ -1,120 +1,143 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
const DialogTrigger = DialogPrimitive.Trigger
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
const DialogPortal = DialogPrimitive.Portal
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
const DialogClose = DialogPrimitive.Close
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
ref={ref}
data-slot="dialog-overlay"
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",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
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>
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
data-slot="dialog-content"
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",
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
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" />
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
)
}
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
}
const DialogFooter = ({
function DialogTitle({
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) => (
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
)
}
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-zinc-500 dark:text-zinc-400", className)}
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
)
}
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View file

@ -2,21 +2,20 @@ import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"flex h-10 w-full rounded-md border border-zinc-200 bg-white px-3 py-2 text-base ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-zinc-950 placeholder:text-zinc-500 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 md:text-sm dark:border-zinc-800 dark:bg-zinc-950 dark:ring-offset-zinc-950 dark:file:text-zinc-50 dark:placeholder:text-zinc-400 dark:focus-visible:ring-zinc-300",
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View file

@ -1,24 +1,22 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
)
}
export { Label }

View file

@ -1,234 +1,274 @@
import * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { Check, ChevronRight, Circle } from "lucide-react"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
const MenubarMenu = MenubarPrimitive.Menu
const MenubarGroup = MenubarPrimitive.Group
const MenubarPortal = MenubarPrimitive.Portal
const MenubarSub = MenubarPrimitive.Sub
const MenubarRadioGroup = MenubarPrimitive.RadioGroup
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
function Menubar({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
return (
<MenubarPrimitive.Root
ref={ref}
data-slot="menubar"
className={cn(
"flex h-10 items-center space-x-1 rounded-md border border-zinc-200 bg-white p-1 dark:border-zinc-800 dark:bg-zinc-950",
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
className
)}
{...props}
/>
))
Menubar.displayName = MenubarPrimitive.Root.displayName
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-zinc-100 focus:text-zinc-900 data-[state=open]:bg-zinc-100 data-[state=open]:text-zinc-900 dark:focus:bg-zinc-800 dark:focus:text-zinc-50 dark:data-[state=open]:bg-zinc-800 dark:data-[state=open]:text-zinc-50",
className
)}
{...props}
/>
))
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
)
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-zinc-100 focus:text-zinc-900 data-[state=open]:bg-zinc-100 data-[state=open]:text-zinc-900 dark:focus:bg-zinc-800 dark:focus:text-zinc-50 dark:data-[state=open]:bg-zinc-800 dark:data-[state=open]:text-zinc-50",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
))
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
function MenubarMenu({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
}
function MenubarGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
}
function MenubarPortal({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />
}
function MenubarRadioGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
)
}
function MenubarTrigger({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
return (
<MenubarPrimitive.Trigger
data-slot="menubar-trigger"
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-zinc-200 bg-white p-1 text-zinc-950 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
className
)}
{...props}
/>
))
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
)
}
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(
(
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
ref
) => (
<MenubarPrimitive.Portal>
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
return (
<MenubarPortal>
<MenubarPrimitive.Content
ref={ref}
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border border-zinc-200 bg-white p-1 text-zinc-950 shadow-md data-[state=open]:animate-in 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
"bg-popover text-popover-foreground data-[state=open]:animate-in 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</MenubarPrimitive.Portal>
</MenubarPortal>
)
)
MenubarContent.displayName = MenubarPrimitive.Content.displayName
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
function MenubarItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<MenubarPrimitive.Item
ref={ref}
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-zinc-800 dark:focus:text-zinc-50",
inset && "pl-8",
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
))
MenubarItem.displayName = MenubarPrimitive.Item.displayName
)
}
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
function MenubarCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
return (
<MenubarPrimitive.CheckboxItem
ref={ref}
data-slot="menubar-checkbox-item"
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-zinc-800 dark:focus:text-zinc-50",
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
<CheckIcon className="size-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
))
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
)
}
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
function MenubarRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
return (
<MenubarPrimitive.RadioItem
ref={ref}
data-slot="menubar-radio-item"
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-zinc-800 dark:focus:text-zinc-50",
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
<CircleIcon className="size-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
))
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean
)
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-zinc-100 dark:bg-zinc-800", className)}
{...props}
/>
))
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
const MenubarShortcut = ({
function MenubarLabel({
className,
inset,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean
}) {
return (
<span
<MenubarPrimitive.Label
data-slot="menubar-label"
data-inset={inset}
className={cn(
"ml-auto text-xs tracking-widest text-zinc-500 dark:text-zinc-400",
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function MenubarSeparator({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
return (
<MenubarPrimitive.Separator
data-slot="menubar-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function MenubarShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="menubar-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function MenubarSub({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
}
function MenubarSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.SubTrigger
data-slot="menubar-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
)
}
function MenubarSubContent({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
return (
<MenubarPrimitive.SubContent
data-slot="menubar-sub-content"
className={cn(
"bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
MenubarShortcut.displayname = "MenubarShortcut"
export {
Menubar,
MenubarPortal,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarGroup,
MenubarSeparator,
MenubarLabel,
MenubarItem,
MenubarShortcut,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
MenubarSubTrigger,
MenubarSubContent,
}

View file

@ -3,27 +3,44 @@ import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
const PopoverTrigger = PopoverPrimitive.Trigger
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border border-zinc-200 bg-white p-4 text-zinc-950 shadow-md outline-none 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
"bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
)
}
export { Popover, PopoverTrigger, PopoverContent }
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View file

@ -1,46 +1,58 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
)
}
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-zinc-200 dark:bg-zinc-800" />
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
)
}
export { ScrollArea, ScrollBar }

View file

@ -1,79 +1,65 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
const SelectGroup = SelectPrimitive.Group
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
const SelectValue = SelectPrimitive.Value
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
ref={ref}
data-slot="select-trigger"
data-size={size}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 dark:border-zinc-800 dark:bg-zinc-950 dark:ring-offset-zinc-950 dark:placeholder:text-zinc-400 dark:focus:ring-zinc-300",
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
)
}
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
data-slot="select-content"
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-zinc-200 bg-white text-zinc-950 shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
"bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
@ -86,7 +72,7 @@ const SelectContent = React.forwardRef<
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
@ -94,65 +80,104 @@ const SelectContent = React.forwardRef<
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
)
}
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
)
}
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
ref={ref}
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-zinc-800 dark:focus:text-zinc-50",
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
)
}
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-zinc-100 dark:bg-zinc-800", className)}
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectGroup,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View file

@ -1,29 +1,28 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
ref={ref}
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-zinc-200 dark:bg-zinc-800",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
}
export { Separator }

View file

@ -1,26 +1,61 @@
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>>(
({ className, ...props }, ref) => (
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
const _values = React.useMemo(
() =>
Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max]
)
return (
<SliderPrimitive.Root
ref={ref}
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
className={cn(
"relative flex w-full touch-none select-none items-center",
"data-[orientation='vertical']:h-full data-[orientation='vertical']:w-2 data-[orientation='vertical']:flex-col",
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800">
<SliderPrimitive.Range className={cn("absolute h-full bg-zinc-900 dark:bg-zinc-50", "data-[orientation='vertical']:w-full")} />
<SliderPrimitive.Track
data-slot="slider-track"
className={cn(
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
)}
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn(
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
)}
/>
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-zinc-900 bg-white ring-offset-white transition-colors 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 dark:border-zinc-50 dark:bg-zinc-950 dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300" />
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/>
))}
</SliderPrimitive.Root>
)
);
Slider.displayName = SliderPrimitive.Root.displayName;
}
export { Slider };
export { Slider }

View file

@ -3,51 +3,62 @@ import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
ref={ref}
data-slot="tabs-list"
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",
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
)
}
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
ref={ref}
data-slot="tabs-trigger"
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",
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
)
}
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<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
)}
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }

View file

@ -12,39 +12,53 @@ const ToggleGroupContext = React.createContext<
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) => (
function ToggleGroup({
className,
variant,
size,
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center justify-center gap-1", className)}
data-slot="toggle-group"
data-variant={variant}
data-size={size}
className={cn(
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
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) => {
function ToggleGroupItem({
className,
children,
variant,
size,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
ref={ref}
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
className
)}
{...props}
@ -52,8 +66,6 @@ const ToggleGroupItem = React.forwardRef<
{children}
</ToggleGroupPrimitive.Item>
)
})
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
}
export { ToggleGroup, ToggleGroupItem }

View file

@ -1,3 +1,5 @@
"use client"
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
@ -5,18 +7,18 @@ 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",
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
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",
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
},
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",
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
@ -26,18 +28,20 @@ const toggleVariants = cva(
}
)
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
function Toggle({
className,
variant,
size,
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
ref={ref}
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
))
Toggle.displayName = TogglePrimitive.Root.displayName
)
}
export { Toggle, toggleVariants }

View file

@ -3,26 +3,57 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
const Tooltip = TooltipPrimitive.Root
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
const TooltipTrigger = TooltipPrimitive.Trigger
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border border-zinc-200 bg-white px-3 py-1.5 text-sm text-zinc-950 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
>
{children}
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View file

@ -1,4 +1,4 @@
import React, { createContext, ReactNode, useContext, useEffect, useMemo, useState } from "react";
import React, { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { HistoryContext } from "./History";
import welcomeBlocksData from "@/data/welcome.json";
@ -22,6 +22,7 @@ interface Props {
children: ReactNode;
}
// eslint-disable-next-line react-refresh/only-export-components
export const CanvasContext = createContext<Context>({} as Context);
export const CanvasProvider = ({ children }: Props) => {
@ -31,7 +32,7 @@ export const CanvasProvider = ({ children }: Props) => {
const [blocks, setBlocks] = useState<Block[]>(welcomeBlocksData);
const [coords, setCoords] = useState<Position>({ x: 0, y: 0 });
const [scale, setScale] = useState(1);
const [version, setVersion] = useState(1214);
const [version, setVersion] = useState(1219);
// Get the farthest away blocks in each direction
const canvasSize = useMemo(() => {
@ -64,7 +65,7 @@ export const CanvasProvider = ({ children }: Props) => {
};
}, [blocks]);
const centerCanvas = () => {
const centerCanvas = useCallback(() => {
// Margin of 8 blocks on each side
const margin = 8 * 16;
@ -85,7 +86,7 @@ export const CanvasProvider = ({ children }: Props) => {
setScale(newScale);
setCoords({ x: newX, y: newY });
};
}, [canvasSize, stageSize]);
useEffect(() => {
addHistory(
@ -93,6 +94,7 @@ export const CanvasProvider = ({ children }: Props) => {
() => setBlocks(welcomeBlocksData),
() => setBlocks([])
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (

View file

@ -7,6 +7,7 @@ interface Props {
children: ReactNode;
}
// eslint-disable-next-line react-refresh/only-export-components
export const DialogContext = createContext<Context>({} as Context);
export const DialogProvider = ({ children }: Props) => {
@ -34,7 +35,7 @@ export const DialogProvider = ({ children }: Props) => {
<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>}>
<Suspense>
<LazyDialogContent close={() => setOpen(false)} registerSubmit={(fn) => (onSubmitRef.current = fn)} dialogKeyHandler={dialogKeyHandler} />
</Suspense>
)}

View file

@ -15,6 +15,7 @@ interface Props {
children: ReactNode;
}
// eslint-disable-next-line react-refresh/only-export-components
export const HistoryContext = createContext<Context>({} as Context);
export const HistoryProvider = ({ children }: Props) => {

View file

@ -10,6 +10,7 @@ interface Props {
children: ReactNode;
}
// eslint-disable-next-line react-refresh/only-export-components
export const LoadingContext = createContext<Context>({} as Context);
export const LoadingProvider = ({ children }: Props) => {
@ -18,10 +19,10 @@ export const LoadingProvider = ({ children }: Props) => {
return (
<LoadingContext.Provider value={{ loading, setLoading }}>
{loading && (
<div className="absolute w-full h-full cursor-wait flex justify-center items-center">
<div className="absolute size-full cursor-wait flex justify-center items-center">
{/* Keep loading indicator outside of div with backdrop-filter due to Chrome */}
<LoadingIndicator fill="white" className="w-16 h-16 z-[10000]" />
<div className="absolute w-full h-full z-[9999] backdrop-brightness-50 flex justify-center items-center gap-4"></div>
<LoadingIndicator fill="white" className="w-16 h-16 z-10000" />
<div className="absolute size-full z-9999 backdrop-brightness-50 flex justify-center items-center gap-4"></div>
</div>
)}
{children}

View file

@ -3,7 +3,7 @@ import React, { createContext, ReactNode, useRef, useState } from "react";
interface Context {
selectionCoords: CoordinateArray;
selectionLayerBlocks: Block[];
confirmHistoryEntryNameRef: React.MutableRefObject<string>;
confirmHistoryEntryNameRef: React.RefObject<string>;
setSelectionCoords: React.Dispatch<React.SetStateAction<CoordinateArray>>;
setSelectionLayerBlocks: React.Dispatch<React.SetStateAction<Block[]>>;
isInSelection: (x: number, y: number) => boolean;
@ -13,6 +13,7 @@ interface Props {
children: ReactNode;
}
// eslint-disable-next-line react-refresh/only-export-components
export const SelectionContext = createContext<Context>({} as Context);
export const SelectionProvider = ({ children }: Props) => {

View file

@ -19,6 +19,7 @@ const defaultSettings: Settings = {
blockSelector: true,
};
// eslint-disable-next-line react-refresh/only-export-components
export const SettingsContext = createContext<Context>({} as Context);
export const SettingsProvider = ({ children }: Props) => {

View file

@ -1,5 +1,5 @@
import { createContext, ReactNode, useContext, useEffect, useRef } from "react";
import * as PIXI from "pixi.js";
import { Assets, Spritesheet, Texture } from "pixi.js";
import { LoadingContext } from "./Loading";
@ -7,48 +7,57 @@ import spritesheet from "@/data/blocks/spritesheet.json";
import programmerArtSpritesheet from "@/data/blocks/programmer-art/spritesheet.json";
interface Context {
missingTexture: PIXI.Texture;
textures: Record<string, PIXI.Texture>;
programmerArtTextures: Record<string, PIXI.Texture>;
missingTexture: Texture;
textures: Record<string, Texture>;
programmerArtTextures: Record<string, Texture>;
}
interface Props {
children: ReactNode;
}
// eslint-disable-next-line react-refresh/only-export-components
export const TexturesContext = createContext<Context>({} as Context);
export const TexturesProvider = ({ children }: Props) => {
const { setLoading } = useContext(LoadingContext);
// Load missing texture through data string just in case of network errors
const missingTextureRef = useRef<PIXI.Texture>(
new PIXI.Texture(
new PIXI.BaseTexture(
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAGUlEQVR42mPABX4w/MCKaKJhVMPgcOuoBgDZRfgBVl5QdQAAAABJRU5ErkJggg=="
)
)
);
const texturesRef = useRef<Record<string, PIXI.Texture>>({});
const programmerArtTexturesRef = useRef<Record<string, PIXI.Texture>>({});
const missingTextureRef = useRef<Texture>(Texture.EMPTY);
const texturesRef = useRef<Record<string, Texture>>({});
const programmerArtTexturesRef = useRef<Record<string, Texture>>({});
// Load textures
useEffect(() => {
// Add air texture
const airBaseTexture = new PIXI.BaseTexture("/blocks/air.png");
const airTexture = new PIXI.Texture(airBaseTexture);
const loadTextures = async () => {
try {
// Load base textures
missingTextureRef.current = await Assets.load(
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAGUlEQVR42mPABX4w/MCKaKJhVMPgcOuoBgDZRfgBVl5QdQAAAABJRU5ErkJggg=="
);
const airBaseTexture = await Assets.load("/blocks/air.png");
const sheetTexture = await Assets.load("/blocks/spritesheet.png");
const programmerArtSheetTexture = await Assets.load("/blocks/programmer-art/spritesheet.png");
const sheet = new PIXI.Spritesheet(PIXI.BaseTexture.from("/blocks/spritesheet.png"), spritesheet);
sheet.parse().then((t) => {
texturesRef.current = { ...t, "air.png": airTexture };
});
// Create and parse main spritesheet
const sheet = new Spritesheet(sheetTexture, spritesheet);
await sheet.parse();
texturesRef.current = { ...sheet.textures, "air.png": airBaseTexture };
// Create and parse programmer art spritesheet
const programmerArtSheet = new Spritesheet(programmerArtSheetTexture, programmerArtSpritesheet);
await programmerArtSheet.parse();
programmerArtTexturesRef.current = { ...programmerArtSheet.textures, "air.png": airBaseTexture };
const programmerArtSheet = new PIXI.Spritesheet(PIXI.BaseTexture.from("/blocks/programmer-art/spritesheet.png"), programmerArtSpritesheet);
programmerArtSheet.parse().then((t) => {
programmerArtTexturesRef.current = { ...t, "air.png": airTexture };
});
setLoading(false);
}, []);
} catch (error) {
console.error("Failed to load textures:", error);
setLoading(false);
}
};
loadTextures();
}, [setLoading]);
return (
<TexturesContext.Provider

View file

@ -12,6 +12,7 @@ interface Context {
setTheme: (theme: Theme) => void;
}
// eslint-disable-next-line react-refresh/only-export-components
export const ThemeContext = createContext<Context>({} as Context);
export function ThemeProvider({ children, defaultTheme = "system", storageKey = "vite-ui-theme", ...props }: Props) {

View file

@ -17,6 +17,7 @@ interface Props {
children: ReactNode;
}
// eslint-disable-next-line react-refresh/only-export-components
export const ToolContext = createContext<Context>({} as Context);
export const ToolProvider = ({ children }: Props) => {

View file

@ -1,4 +1,5 @@
{
"1219": 4554,
"1214": 4189,
"1210": 3953,
"1200": 3463,

19
src/hooks/use-mobile.ts Normal file
View file

@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}

View file

@ -1,9 +1,47 @@
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
@import "tailwindcss";
@import "tw-animate-css";
@tailwind base;
@tailwind components;
@tailwind utilities;
@custom-variant dark (&:is(.dark *));
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--color-blockmatic-green: #adc178;
--color-blockmatic-brown: #a98467;
--font-inter: Inter, sans-serif;
}
/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
}
}
@layer utilities {
:root {
font-family: Geist, Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
@ -12,6 +50,7 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer base {
:root {
@ -30,3 +69,155 @@ body {
.info-child {
@apply bg-zinc-50 dark:bg-zinc-900 px-2 py-1 rounded shadow-xl w-fit border border-zinc-200 dark:border-zinc-800;
}
/*
---break---
*/
:root {
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
}
/*
---break---
*/
.dark {
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
}
/*
---break---
*/
@theme inline {
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
/*
---break---
*/
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
/*
---break---
*/
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View file

@ -10,6 +10,14 @@ import PrivacyPolicy from "./pages/PrivacyPolicy.tsx";
import NotFound from "./pages/NotFound.tsx";
import "./index.css";
import { extend } from "@pixi/react";
import { Container, Graphics, Sprite } from "pixi.js";
extend({
Container,
Sprite,
Graphics,
});
createRoot(document.getElementById("root")!).render(
<StrictMode>

View file

@ -29,10 +29,10 @@ function AppPage() {
<MobileNotice />
<main
className={`overflow-y-hidden h-[100svh] grid ${
className={`overflow-y-hidden h-svh grid ${
isMobileView
? "grid-rows-[2.5rem_minmax(0,1fr)_auto] grid-cols-[2.5rem_minmax(0,1fr)]"
: "grid-rows-[2.5rem_minmax(0,1fr)] grid-cols-[2.5rem_minmax(0,1fr)_auto]"
? "grid-rows-[2.3rem_minmax(0,1fr)_auto] grid-cols-[2.3rem_minmax(0,1fr)]"
: "grid-rows-[2.3rem_minmax(0,1fr)] grid-cols-[2.3rem_minmax(0,1fr)_auto]"
}`}
>
<Menubar />

View file

@ -56,9 +56,9 @@ function IndexPage() {
</span>
</h5>
<Button className="w-min h-11 mt-4" asChild>
<Link to={{ pathname: "/app" }} className="!text-base">
<Link to={{ pathname: "/app" }} className="text-base!">
Go to Editor
<ChevronRightIcon className="!h-6 !w-6" />
<ChevronRightIcon className="size-6!" />
</Link>
</Button>
</section>
@ -67,14 +67,14 @@ function IndexPage() {
<img
src={isDark ? "/home/blockmatic_screenshot_dark.png" : "/home/blockmatic_screenshot_light.png"}
alt="app preview"
className="max-w-[65rem] w-full rounded-xl border border-zinc-700"
className="max-w-260 w-full rounded-xl border border-zinc-700"
/>
</section>
<section className="flex flex-col items-center mt-32 text-center mx-8">
<h1 className="text-5xl font-bold mb-2">Pixel art made easy</h1>
<p className="mb-8 text-lg">Blockmatic makes it easier to make changes and build by using schematics and the web editor.</p>
<div className="max-w-[56rem] grid grid-cols-3 max-md:grid-cols-2 gap-4 *:flex *:flex-col *:items-center *:gap-1 *:text-center *:p-4 *:border *:border-zinc-300 *:dark:border-zinc-800 *:rounded-lg *:bg-zinc-100 *:dark:bg-zinc-900 *:text-black *:dark:text-white">
<div className="max-w-4xl grid grid-cols-3 max-md:grid-cols-2 gap-4 *:flex *:flex-col *:items-center *:gap-1 *:text-center *:p-4 *:border *:border-zinc-300 dark:*:border-zinc-800 *:rounded-lg *:bg-zinc-100 dark:*:bg-zinc-900 *:text-black dark:*:text-white">
<div>
<img
src="/home/shinji.png"
@ -143,8 +143,8 @@ function IndexPage() {
<p className="mb-8 text-lg text-center">Blockmatic is free and open source. Host it yourself, modify it, or contribute to its development.</p>
<Button className="w-min h-11" variant="outline" asChild>
<a href="https://github.com/trafficlunar/blockmatic" className="!text-base">
<LinkIcon className="!h-5 !w-5" />
<a href="https://github.com/trafficlunar/blockmatic" className="text-base!">
<LinkIcon className="size-5!" />
Source code
</a>
</Button>
@ -153,9 +153,9 @@ function IndexPage() {
<Separator className="max-w-xl mt-20 mb-8" />
<Button className="w-min h-11" asChild>
<Link to={{ pathname: "/app" }} className="!text-base">
<Link to={{ pathname: "/app" }} className="text-base!">
Go to Editor
<ChevronRightIcon className="!h-6 !w-6" />
<ChevronRightIcon className="size-6!" />
</Link>
</Button>

View file

@ -12,7 +12,7 @@ function NotFound() {
const { isDark } = useContext(ThemeContext);
return (
<div className="absolute w-full h-full flex flex-col justify-center items-center">
<div className="absolute size-full flex flex-col justify-center items-center">
<div className="flex gap-2 items-center">
<BlockmaticLogo className="h-8" fill={isDark ? "white" : "black"} />
<BlockmaticText className="h-3.5" fill={isDark ? "white" : "black"} />
@ -24,7 +24,7 @@ function NotFound() {
<Button variant="outline" asChild>
<Link to={{ pathname: "/" }}>
Go back
<ChevronLeftIcon className="!h-6 !w-6" />
<ChevronLeftIcon className="size-6!" />
</Link>
</Button>
</div>

View file

@ -1,24 +0,0 @@
import tailwindcssAnimate from "tailwindcss-animate";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
colors: {
"blockmatic-green": "#adc178",
"blockmatic-brown": "#a98467",
},
fontFamily: {
inter: ["Inter", "sans-serif"],
},
},
},
plugins: [tailwindcssAnimate],
};