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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,3 @@
import { Graphics } from "@pixi/react";
interface Props { interface Props {
canvasSize: BoundingBox; canvasSize: BoundingBox;
isDark: boolean; isDark: boolean;
@ -7,11 +5,11 @@ interface Props {
function CanvasBorder({ canvasSize, isDark }: Props) { function CanvasBorder({ canvasSize, isDark }: Props) {
return ( return (
<Graphics <pixiGraphics
draw={(g) => { draw={(g) => {
g.clear(); g.clear();
g.lineStyle(2, isDark ? 0xffffff : 0x000000, 0.25, 1); g.rect(canvasSize.minX * 16, canvasSize.minY * 16, (canvasSize.maxX - canvasSize.minX) * 16, (canvasSize.maxY - canvasSize.minY) * 16);
g.drawRect(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 { interface Props {
mouseCoords: Position; mouseCoords: Position;
radius: number; radius: number;
@ -14,13 +12,13 @@ function Cursor({ mouseCoords, radius, isDark }: Props) {
const size = radius * 16; const size = radius * 16;
return ( return (
<Graphics <pixiGraphics
x={(mouseCoords.x + offset) * 16} x={(mouseCoords.x + offset) * 16}
y={(mouseCoords.y + offset) * 16} y={(mouseCoords.y + offset) * 16}
draw={(g) => { draw={(g) => {
g.clear(); g.clear();
g.lineStyle(1, isDark ? 0xffffff : 0x000000, 1); g.rect(0, 0, size, size);
g.drawRect(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 { interface Props {
stageSize: Dimension; stageSize: Dimension;
coords: Position; coords: Position;
@ -9,10 +7,9 @@ interface Props {
function Grid({ stageSize, coords, scale, isDark }: Props) { function Grid({ stageSize, coords, scale, isDark }: Props) {
return ( return (
<Graphics <pixiGraphics
draw={(g) => { draw={(g) => {
g.clear(); g.clear();
g.lineStyle(1, isDark ? 0xffffff : 0x000000);
const tileSize = 16 * scale; const tileSize = 16 * scale;
@ -25,6 +22,8 @@ function Grid({ stageSize, coords, scale, isDark }: Props) {
g.moveTo(0, y); g.moveTo(0, y);
g.lineTo(stageSize.width, y); g.lineTo(stageSize.width, y);
} }
g.stroke({ width: 1, color: isDark ? 0xffffff : 0x000000 });
}} }}
/> />
); );

View file

@ -1,73 +1,71 @@
/* 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 { interface Props {
selection: CoordinateArray; selection: CoordinateArray;
coords: Position;
scale: number;
isDark: boolean; 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) { function Selection({ selection, isDark }: Props) {
const app = useApp(); return (
const selectionRef = useRef<SmoothGraphics>(); <pixiGraphics
draw={(g) => {
g.clear();
const drawSelection = () => { const edges = new Set<string>();
if (!selectionRef.current) return;
const graphics = selectionRef.current;
graphics.clear();
graphics.lineStyle({ width: 1, color: isDark ? 0xffffff : 0x000000, shader });
const edges = new Set<string>(); selection.forEach(([x, y]) => {
const top = [x, y, x + 1, y];
const bottom = [x, y + 1, x + 1, y + 1];
const left = [x, y, x, y + 1];
const right = [x + 1, y, x + 1, y + 1];
selection.forEach(([x, y]) => { // Add edges, or remove them if they already exist (shared edges)
const top = [x, y, x + 1, y]; [top, bottom, left, right].forEach((edge) => {
const bottom = [x, y + 1, x + 1, y + 1]; const stringified = JSON.stringify(edge);
const left = [x, y, x, y + 1]; if (edges.has(stringified)) {
const right = [x + 1, y, x + 1, y + 1]; edges.delete(stringified); // Shared edge, remove it
} else {
edges.add(stringified); // Unique edge, add it
}
});
});
// Add edges, or remove them if they already exist (shared edges) // Draw each remaining edge
[top, bottom, left, right].forEach((edge) => { edges.forEach((edge) => {
const stringified = JSON.stringify(edge); const [x1, y1, x2, y2] = JSON.parse(edge) as [number, number, number, number];
if (edges.has(stringified)) {
edges.delete(stringified); // Shared edge, remove it
} else {
edges.add(stringified); // Unique edge, add it
}
});
});
// Draw the remaining edges // Draw dashed line
edges.forEach((edge) => { const startX = x1 * 16;
const [x1, y1, x2, y2] = JSON.parse(edge); const startY = y1 * 16;
graphics.moveTo(x1 * 16 * scale, y1 * 16 * scale); const endX = x2 * 16;
graphics.lineTo(x2 * 16 * scale, y2 * 16 * scale); const endY = y2 * 16;
});
};
useEffect(() => { const dx = endX - startX;
const graphics = new SmoothGraphics(); const dy = endY - startY;
selectionRef.current = graphics; const lineLength = Math.hypot(dx, dy);
drawSelection(); const angle = Math.atan2(dy, dx);
app.stage.addChild(graphics);
}, []);
useEffect(() => { let drawn = 0;
if (!selectionRef.current) return; while (drawn < lineLength) {
const graphics = selectionRef.current; 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;
graphics.x = coords.x; g.moveTo(sx, sy);
graphics.y = coords.y; g.lineTo(ex, ey);
drawSelection();
}, [coords]);
useEffect(drawSelection, [selection]); drawn += DASH_LENGTH + GAP_LENGTH;
}
});
return null; // Render the lines
g.stroke({ width: 2, color: isDark ? 0xffffff : 0x000000, alignment: 0 });
}}
/>
);
} }
export default Selection; 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"} ${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 /> <XIcon />
</Button> </Button>
<span className="mx-2 text-[0.85rem]">Confirm?</span> <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 /> <CheckIcon />
</Button> </Button>
</div> </div>

View file

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

View file

@ -108,7 +108,7 @@ function OpenImage({ close }: DialogProps) {
setLoading(true); setLoading(true);
// Wait for loading indicator to appear // 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 // Load image through JS canvas
const canvas = document.createElement("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(() => { useEffect(() => {
if (!isFinished.current) return; if (!isFinished.current) return;
centerCanvas();
// Wrap in requestAnimationFrame() to fix bug where canvas is black
requestAnimationFrame(() => {
centerCanvas();
});
close(); close();
return () => { return () => {
@ -224,13 +229,13 @@ function OpenImage({ close }: DialogProps) {
</p> </p>
</div> </div>
<div className="grid grid-cols-[auto,1fr] gap-2"> <div className="grid grid-cols-[auto_1fr] gap-2">
{image && acceptedFiles[0] && ( {image && acceptedFiles[0] && (
<> <>
<img <img
src={image.src} src={image.src}
alt="your image" 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%" }} 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" variant="outline"
pressed={linkAspectRatio} pressed={linkAspectRatio}
onPressedChange={() => setLinkAspectRatio(!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 /> <LinkIcon />
</Toggle> </Toggle>
@ -344,7 +349,7 @@ function OpenImage({ close }: DialogProps) {
</Tabs> </Tabs>
</div> </div>
<DialogFooter className="!justify-between"> <DialogFooter className="justify-between!">
<VersionCombobox version={version} setVersion={setVersion} isContext /> <VersionCombobox version={version} setVersion={setVersion} isContext />
<div className="flex gap-2"> <div className="flex gap-2">

View file

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

View file

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

View file

@ -15,7 +15,7 @@ function ImageComparison() {
return ( return (
<div <div
onPointerMove={onPointerMove} 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 <img
src="/bliss/bliss_original.png" 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"> <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> </button>
</div> </div>

View file

@ -35,7 +35,7 @@ function Menubar() {
<SelectMenu /> <SelectMenu />
<ViewMenu /> <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 /> <ThemeIcon inApp />
<a href="https://github.com/trafficlunar/blockmatic" className="w-5"> <a href="https://github.com/trafficlunar/blockmatic" className="w-5">
<GithubIcon fill={isDark ? "white" : "black"} /> <GithubIcon fill={isDark ? "white" : "black"} />

View file

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

View file

@ -1,5 +1,4 @@
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { Container, Sprite, Stage } from "@pixi/react";
import { CanvasContext } from "@/context/Canvas"; import { CanvasContext } from "@/context/Canvas";
import { HistoryContext } from "@/context/History"; import { HistoryContext } from "@/context/History";
@ -11,6 +10,7 @@ import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useTextures } from "@/hooks/useTextures"; import { useTextures } from "@/hooks/useTextures";
import { Application } from "@pixi/react";
function Replace() { function Replace() {
const { version, setBlocks } = useContext(CanvasContext); const { version, setBlocks } = useContext(CanvasContext);
@ -80,11 +80,11 @@ function Replace() {
onClick={() => onClickBlockButton(1)} 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" 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 }}> <Application width={32} height={32} backgroundAlpha={0}>
<Container> <pixiContainer>
<Sprite texture={textures[block1] ?? missingTexture} scale={2} /> <pixiSprite texture={textures[block1] ?? missingTexture} scale={2} />
</Container> </pixiContainer>
</Stage> </Application>
</button> </button>
<Label htmlFor="radius">Block 2</Label> <Label htmlFor="radius">Block 2</Label>
@ -92,11 +92,11 @@ function Replace() {
onClick={() => onClickBlockButton(2)} 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" 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 }}> <Application width={32} height={32} backgroundAlpha={0}>
<Container> <pixiContainer>
<Sprite texture={textures[block2] ?? missingTexture} scale={2} /> <pixiSprite texture={textures[block2] ?? missingTexture} scale={2} />
</Container> </pixiContainer>
</Stage> </Application>
</button> </button>
<br /> <br />

View file

@ -25,7 +25,7 @@ function ToolSettings() {
{tool === "shape" && ( {tool === "shape" && (
<> <>
<Label htmlFor="shape">Shape</Label> <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> <SelectTrigger>
<SelectValue placeholder="Select a shape" /> <SelectValue placeholder="Select a shape" />
</SelectTrigger> </SelectTrigger>
@ -39,7 +39,7 @@ function ToolSettings() {
</Select> </Select>
<Label htmlFor="filled">Filled</Label> <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> </div>

View file

@ -153,7 +153,7 @@ function Sidebar() {
{settings.blockSelector && ( {settings.blockSelector && (
<> <>
<Input placeholder="Search for blocks..." value={searchInput} onChange={(e) => setSearchInput(e.target.value)} /> <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} /> <BlockSelector stageWidth={stageWidth} searchInput={searchInput} />
</ScrollArea> </ScrollArea>
</> </>

View file

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

View file

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

View file

@ -5,25 +5,26 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const buttonVariants = cva( 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: { variants: {
variant: { 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: 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: 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: 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", "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-50", ghost:
link: "text-zinc-900 underline-offset-4 hover:underline dark:text-zinc-50", "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-10 px-4 py-2", default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-9 rounded-md px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-11 rounded-md px-8", lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "h-10 w-10", icon: "size-9",
}, },
}, },
defaultVariants: { defaultVariants: {
@ -33,24 +34,25 @@ const buttonVariants = cva(
} }
) )
export interface ButtonProps function Button({
extends React.ButtonHTMLAttributes<HTMLButtonElement>, className,
VariantProps<typeof buttonVariants> { variant,
asChild?: boolean size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants } export { Button, buttonVariants }

View file

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

View file

@ -1,28 +1,30 @@
import * as React from "react" import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox" import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react" import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef< function Checkbox({
React.ElementRef<typeof CheckboxPrimitive.Root>, className,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
<CheckboxPrimitive.Root return (
ref={ref} <CheckboxPrimitive.Root
className={cn( data-slot="checkbox"
"peer h-4 w-4 shrink-0 rounded-sm border border-zinc-200 border-zinc-900 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-zinc-900 data-[state=checked]:text-zinc-50 dark:border-zinc-800 dark:border-zinc-50 dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300 dark:data-[state=checked]:bg-zinc-50 dark:data-[state=checked]:text-zinc-900", className={cn(
className "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} )}
> {...props}
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
> >
<Check className="h-4 w-4" /> <CheckboxPrimitive.Indicator
</CheckboxPrimitive.Indicator> data-slot="checkbox-indicator"
</CheckboxPrimitive.Root> className="flex items-center justify-center text-current transition-none"
)) >
Checkbox.displayName = CheckboxPrimitive.Root.displayName <CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox } export { Checkbox }

View file

@ -1,31 +1,56 @@
import * as React from "react" import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk" import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react" import { SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils" 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< function Command({
React.ElementRef<typeof CommandPrimitive>, className,
React.ComponentPropsWithoutRef<typeof CommandPrimitive> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof CommandPrimitive>) {
<CommandPrimitive return (
ref={ref} <CommandPrimitive
className={cn( data-slot="command"
"flex h-full w-full flex-col overflow-hidden rounded-md bg-white text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50", className={cn(
className "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
)} className
{...props} )}
/> {...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 ( return (
<Dialog {...props}> <Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg"> <DialogHeader className="sr-only">
<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"> <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} {children}
</Command> </Command>
</DialogContent> </DialogContent>
@ -33,110 +58,116 @@ const CommandDialog = ({ children, ...props }: DialogProps) => {
) )
} }
const CommandInput = React.forwardRef< function CommandInput({
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" />
<CommandPrimitive.Input
ref={ref}
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",
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 = ({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLSpanElement>) => { }: React.ComponentProps<typeof CommandPrimitive.Input>) {
return ( return (
<span <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
data-slot="command-input"
className={cn(
"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>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn( 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 className
)} )}
{...props} {...props}
/> />
) )
} }
CommandShortcut.displayName = "CommandShortcut"
export { export {
Command, Command,

View file

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

View file

@ -2,21 +2,20 @@ import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>( function Input({ className, type, ...props }: React.ComponentProps<"input">) {
({ className, type, ...props }, ref) => { return (
return ( <input
<input type={type}
type={type} data-slot="input"
className={cn( 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",
className "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",
ref={ref} className
{...props} )}
/> {...props}
) />
} )
) }
Input.displayName = "Input"
export { Input } export { Input }

View file

@ -1,24 +1,22 @@
import * as React from "react" import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const labelVariants = cva( function Label({
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" className,
) ...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const Label = React.forwardRef< return (
React.ElementRef<typeof LabelPrimitive.Root>, <LabelPrimitive.Root
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & data-slot="label"
VariantProps<typeof labelVariants> className={cn(
>(({ className, ...props }, ref) => ( "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",
<LabelPrimitive.Root className
ref={ref} )}
className={cn(labelVariants(), className)} {...props}
{...props} />
/> )
)) }
Label.displayName = LabelPrimitive.Root.displayName
export { Label } export { Label }

View file

@ -1,234 +1,274 @@
import * as React from "react" import * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar" 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" import { cn } from "@/lib/utils"
const MenubarMenu = MenubarPrimitive.Menu function Menubar({
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) => (
<MenubarPrimitive.Root
ref={ref}
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",
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}
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",
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>
<MenubarPrimitive.Content
ref={ref}
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",
className
)}
{...props}
/>
</MenubarPrimitive.Portal>
)
)
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) => (
<MenubarPrimitive.Item
ref={ref}
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",
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) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
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",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-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) => (
<MenubarPrimitive.RadioItem
ref={ref}
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",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-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 = ({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLSpanElement>) => { }: React.ComponentProps<typeof MenubarPrimitive.Root>) {
return ( return (
<span <MenubarPrimitive.Root
data-slot="menubar"
className={cn( className={cn(
"ml-auto text-xs tracking-widest text-zinc-500 dark:text-zinc-400", "bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
className
)}
{...props}
/>
)
}
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(
"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}
/>
)
}
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
return (
<MenubarPortal>
<MenubarPrimitive.Content
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"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}
/>
</MenubarPortal>
)
}
function MenubarItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<MenubarPrimitive.Item
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
className={cn(
"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}
/>
)
}
function MenubarCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
return (
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(
"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="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
)
}
function MenubarRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
return (
<MenubarPrimitive.RadioItem
data-slot="menubar-radio-item"
className={cn(
"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="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
)
}
function MenubarLabel({
className,
inset,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.Label
data-slot="menubar-label"
data-inset={inset}
className={cn(
"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 className
)} )}
{...props} {...props}
/> />
) )
} }
MenubarShortcut.displayname = "MenubarShortcut"
export { export {
Menubar, Menubar,
MenubarPortal,
MenubarMenu, MenubarMenu,
MenubarTrigger, MenubarTrigger,
MenubarContent, MenubarContent,
MenubarItem, MenubarGroup,
MenubarSeparator, MenubarSeparator,
MenubarLabel, MenubarLabel,
MenubarItem,
MenubarShortcut,
MenubarCheckboxItem, MenubarCheckboxItem,
MenubarRadioGroup, MenubarRadioGroup,
MenubarRadioItem, MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub, MenubarSub,
MenubarShortcut, MenubarSubTrigger,
MenubarSubContent,
} }

View file

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

View file

@ -1,158 +1,183 @@
import * as React from "react" import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select" 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" 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< function SelectTrigger({
React.ElementRef<typeof SelectPrimitive.Trigger>, className,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> size = "default",
>(({ className, children, ...props }, ref) => ( children,
<SelectPrimitive.Trigger ...props
ref={ref} }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
className={cn( size?: "sm" | "default"
"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", }) {
className return (
)} <SelectPrimitive.Trigger
{...props} data-slot="select-trigger"
> data-size={size}
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-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) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn( 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", "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",
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 className
)} )}
position={position}
{...props} {...props}
> >
<SelectScrollUpButton /> {children}
<SelectPrimitive.Viewport <SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn( className={cn(
"p-1", "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" && position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]" "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
)} )}
position={position}
{...props}
> >
{children} <SelectScrollUpButton />
</SelectPrimitive.Viewport> <SelectPrimitive.Viewport
<SelectScrollDownButton /> className={cn(
</SelectPrimitive.Content> "p-1",
</SelectPrimitive.Portal> position === "popper" &&
)) "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
SelectContent.displayName = SelectPrimitive.Content.displayName )}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
const SelectLabel = React.forwardRef< function SelectLabel({
React.ElementRef<typeof SelectPrimitive.Label>, className,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof SelectPrimitive.Label>) {
<SelectPrimitive.Label return (
ref={ref} <SelectPrimitive.Label
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} data-slot="select-label"
{...props} className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
/> {...props}
)) />
SelectLabel.displayName = SelectPrimitive.Label.displayName )
}
const SelectItem = React.forwardRef< function SelectItem({
React.ElementRef<typeof SelectPrimitive.Item>, className,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> children,
>(({ className, children, ...props }, ref) => ( ...props
<SelectPrimitive.Item }: React.ComponentProps<typeof SelectPrimitive.Item>) {
ref={ref} return (
className={cn( <SelectPrimitive.Item
"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", data-slot="select-item"
className className={cn(
)} "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",
{...props} className
> )}
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> {...props}
<SelectPrimitive.ItemIndicator> >
<Check className="h-4 w-4" /> <span className="absolute right-2 flex size-3.5 items-center justify-center">
</SelectPrimitive.ItemIndicator> <SelectPrimitive.ItemIndicator>
</span> <CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> function SelectSeparator({
</SelectPrimitive.Item> className,
)) ...props
SelectItem.displayName = SelectPrimitive.Item.displayName }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
const SelectSeparator = React.forwardRef< function SelectScrollUpButton({
React.ElementRef<typeof SelectPrimitive.Separator>, className,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
<SelectPrimitive.Separator return (
ref={ref} <SelectPrimitive.ScrollUpButton
className={cn("-mx-1 my-1 h-px bg-zinc-100 dark:bg-zinc-800", className)} data-slot="select-scroll-up-button"
{...props} className={cn(
/> "flex cursor-default items-center justify-center py-1",
)) className
SelectSeparator.displayName = SelectPrimitive.Separator.displayName )}
{...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 { export {
Select, Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent, SelectContent,
SelectLabel, SelectGroup,
SelectItem, SelectItem,
SelectSeparator, SelectLabel,
SelectScrollUpButton,
SelectScrollDownButton, SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
} }

View file

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

View file

@ -1,26 +1,61 @@
import * as React from "react"; import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"; 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>>( function Slider({
({ className, ...props }, ref) => ( className,
<SliderPrimitive.Root defaultValue,
ref={ref} value,
className={cn( min = 0,
"relative flex w-full touch-none select-none items-center", max = 100,
"data-[orientation='vertical']:h-full data-[orientation='vertical']:w-2 data-[orientation='vertical']:flex-col", ...props
className }: React.ComponentProps<typeof SliderPrimitive.Root>) {
)} const _values = React.useMemo(
{...props} () =>
> Array.isArray(value)
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800"> ? value
<SliderPrimitive.Range className={cn("absolute h-full bg-zinc-900 dark:bg-zinc-50", "data-[orientation='vertical']:w-full")} /> : Array.isArray(defaultValue)
</SliderPrimitive.Track> ? defaultValue
<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" /> : [min, max],
</SliderPrimitive.Root> [value, defaultValue, min, max]
) )
);
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider }; return (
<SliderPrimitive.Root
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
className={cn(
"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
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>
{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>
)
}
export { Slider }

View file

@ -3,51 +3,62 @@ import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils" 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< function TabsList({
React.ElementRef<typeof TabsPrimitive.List>, className,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof TabsPrimitive.List>) {
<TabsPrimitive.List return (
ref={ref} <TabsPrimitive.List
className={cn( data-slot="tabs-list"
"inline-flex h-10 items-center justify-center rounded-md bg-zinc-100 p-1 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400", className={cn(
className "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
)} className
{...props} )}
/> {...props}
)) />
TabsList.displayName = TabsPrimitive.List.displayName )
}
const TabsTrigger = React.forwardRef< function TabsTrigger({
React.ElementRef<typeof TabsPrimitive.Trigger>, className,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
<TabsPrimitive.Trigger return (
ref={ref} <TabsPrimitive.Trigger
className={cn( data-slot="tabs-trigger"
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-zinc-950 data-[state=active]:shadow-sm dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300 dark:data-[state=active]:bg-zinc-950 dark:data-[state=active]:text-zinc-50", className={cn(
className "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} )}
/> {...props}
)) />
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName )
}
const TabsContent = React.forwardRef< function TabsContent({
React.ElementRef<typeof TabsPrimitive.Content>, className,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof TabsPrimitive.Content>) {
<TabsPrimitive.Content return (
ref={ref} <TabsPrimitive.Content
className={cn( data-slot="tabs-content"
"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={cn("flex-1 outline-none", className)}
className {...props}
)} />
{...props} )
/> }
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent } export { Tabs, TabsList, TabsTrigger, TabsContent }

View file

@ -12,39 +12,53 @@ const ToggleGroupContext = React.createContext<
variant: "default", variant: "default",
}) })
const ToggleGroup = React.forwardRef< function ToggleGroup({
React.ElementRef<typeof ToggleGroupPrimitive.Root>, className,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & variant,
VariantProps<typeof toggleVariants> size,
>(({ className, variant, size, children, ...props }, ref) => ( children,
<ToggleGroupPrimitive.Root ...props
ref={ref} }: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
className={cn("flex items-center justify-center gap-1", className)} VariantProps<typeof toggleVariants>) {
{...props} return (
> <ToggleGroupPrimitive.Root
<ToggleGroupContext.Provider value={{ variant, size }}> data-slot="toggle-group"
{children} data-variant={variant}
</ToggleGroupContext.Provider> data-size={size}
</ToggleGroupPrimitive.Root> 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 function ToggleGroupItem({
className,
const ToggleGroupItem = React.forwardRef< children,
React.ElementRef<typeof ToggleGroupPrimitive.Item>, variant,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & size,
VariantProps<typeof toggleVariants> ...props
>(({ className, children, variant, size, ...props }, ref) => { }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext) const context = React.useContext(ToggleGroupContext)
return ( return (
<ToggleGroupPrimitive.Item <ToggleGroupPrimitive.Item
ref={ref} data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
className={cn( className={cn(
toggleVariants({ toggleVariants({
variant: context.variant || variant, variant: context.variant || variant,
size: context.size || size, 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 className
)} )}
{...props} {...props}
@ -52,8 +66,6 @@ const ToggleGroupItem = React.forwardRef<
{children} {children}
</ToggleGroupPrimitive.Item> </ToggleGroupPrimitive.Item>
) )
}) }
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
export { ToggleGroup, ToggleGroupItem } export { ToggleGroup, ToggleGroupItem }

View file

@ -1,3 +1,5 @@
"use client"
import * as React from "react" import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle" import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority"
@ -5,18 +7,18 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const toggleVariants = cva( 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: { variants: {
variant: { variant: {
default: "bg-transparent", default: "bg-transparent",
outline: 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: { size: {
default: "h-10 px-3 min-w-10", default: "h-9 px-2 min-w-9",
sm: "h-9 px-2.5 min-w-9", sm: "h-8 px-1.5 min-w-8",
lg: "h-11 px-5 min-w-11", lg: "h-10 px-2.5 min-w-10",
}, },
}, },
defaultVariants: { defaultVariants: {
@ -26,18 +28,20 @@ const toggleVariants = cva(
} }
) )
const Toggle = React.forwardRef< function Toggle({
React.ElementRef<typeof TogglePrimitive.Root>, className,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & variant,
VariantProps<typeof toggleVariants> size,
>(({ className, variant, size, ...props }, ref) => ( ...props
<TogglePrimitive.Root }: React.ComponentProps<typeof TogglePrimitive.Root> &
ref={ref} VariantProps<typeof toggleVariants>) {
className={cn(toggleVariants({ variant, size, className }))} return (
{...props} <TogglePrimitive.Root
/> data-slot="toggle"
)) className={cn(toggleVariants({ variant, size, className }))}
{...props}
Toggle.displayName = TogglePrimitive.Root.displayName />
)
}
export { Toggle, toggleVariants } export { Toggle, toggleVariants }

View file

@ -3,26 +3,57 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils" 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< function TooltipContent({
React.ElementRef<typeof TooltipPrimitive.Content>, className,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> sideOffset = 0,
>(({ className, sideOffset = 4, ...props }, ref) => ( children,
<TooltipPrimitive.Content ...props
ref={ref} }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
sideOffset={sideOffset} return (
className={cn( <TooltipPrimitive.Portal>
"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", <TooltipPrimitive.Content
className data-slot="tooltip-content"
)} sideOffset={sideOffset}
{...props} className={cn(
/> "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
TooltipContent.displayName = TooltipPrimitive.Content.displayName )}
{...props}
>
{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 } 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 { HistoryContext } from "./History";
import welcomeBlocksData from "@/data/welcome.json"; import welcomeBlocksData from "@/data/welcome.json";
@ -22,6 +22,7 @@ interface Props {
children: ReactNode; children: ReactNode;
} }
// eslint-disable-next-line react-refresh/only-export-components
export const CanvasContext = createContext<Context>({} as Context); export const CanvasContext = createContext<Context>({} as Context);
export const CanvasProvider = ({ children }: Props) => { export const CanvasProvider = ({ children }: Props) => {
@ -31,7 +32,7 @@ export const CanvasProvider = ({ children }: Props) => {
const [blocks, setBlocks] = useState<Block[]>(welcomeBlocksData); const [blocks, setBlocks] = useState<Block[]>(welcomeBlocksData);
const [coords, setCoords] = useState<Position>({ x: 0, y: 0 }); const [coords, setCoords] = useState<Position>({ x: 0, y: 0 });
const [scale, setScale] = useState(1); const [scale, setScale] = useState(1);
const [version, setVersion] = useState(1214); const [version, setVersion] = useState(1219);
// Get the farthest away blocks in each direction // Get the farthest away blocks in each direction
const canvasSize = useMemo(() => { const canvasSize = useMemo(() => {
@ -64,7 +65,7 @@ export const CanvasProvider = ({ children }: Props) => {
}; };
}, [blocks]); }, [blocks]);
const centerCanvas = () => { const centerCanvas = useCallback(() => {
// Margin of 8 blocks on each side // Margin of 8 blocks on each side
const margin = 8 * 16; const margin = 8 * 16;
@ -85,7 +86,7 @@ export const CanvasProvider = ({ children }: Props) => {
setScale(newScale); setScale(newScale);
setCoords({ x: newX, y: newY }); setCoords({ x: newX, y: newY });
}; }, [canvasSize, stageSize]);
useEffect(() => { useEffect(() => {
addHistory( addHistory(
@ -93,6 +94,7 @@ export const CanvasProvider = ({ children }: Props) => {
() => setBlocks(welcomeBlocksData), () => setBlocks(welcomeBlocksData),
() => setBlocks([]) () => setBlocks([])
); );
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
return ( return (

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
{ {
"1219": 4554,
"1214": 4189, "1214": 4189,
"1210": 3953, "1210": 3953,
"1200": 3463, "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,16 +1,55 @@
@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 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; @custom-variant dark (&:is(.dark *));
@tailwind components;
@tailwind utilities;
:root { @theme {
font-family: Geist, Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; --color-background: var(--background);
--color-foreground: var(--foreground);
font-synthesis: none; --radius-lg: var(--radius);
text-rendering: optimizeLegibility; --radius-md: calc(var(--radius) - 2px);
-webkit-font-smoothing: antialiased; --radius-sm: calc(var(--radius) - 4px);
-moz-osx-font-smoothing: grayscale;
--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;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
} }
@layer base { @layer base {
@ -30,3 +69,155 @@ body {
.info-child { .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; @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 NotFound from "./pages/NotFound.tsx";
import "./index.css"; import "./index.css";
import { extend } from "@pixi/react";
import { Container, Graphics, Sprite } from "pixi.js";
extend({
Container,
Sprite,
Graphics,
});
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>

View file

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

View file

@ -56,9 +56,9 @@ function IndexPage() {
</span> </span>
</h5> </h5>
<Button className="w-min h-11 mt-4" asChild> <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 Go to Editor
<ChevronRightIcon className="!h-6 !w-6" /> <ChevronRightIcon className="size-6!" />
</Link> </Link>
</Button> </Button>
</section> </section>
@ -67,14 +67,14 @@ function IndexPage() {
<img <img
src={isDark ? "/home/blockmatic_screenshot_dark.png" : "/home/blockmatic_screenshot_light.png"} src={isDark ? "/home/blockmatic_screenshot_dark.png" : "/home/blockmatic_screenshot_light.png"}
alt="app preview" 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>
<section className="flex flex-col items-center mt-32 text-center mx-8"> <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> <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> <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> <div>
<img <img
src="/home/shinji.png" 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> <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> <Button className="w-min h-11" variant="outline" asChild>
<a href="https://github.com/trafficlunar/blockmatic" className="!text-base"> <a href="https://github.com/trafficlunar/blockmatic" className="text-base!">
<LinkIcon className="!h-5 !w-5" /> <LinkIcon className="size-5!" />
Source code Source code
</a> </a>
</Button> </Button>
@ -153,9 +153,9 @@ function IndexPage() {
<Separator className="max-w-xl mt-20 mb-8" /> <Separator className="max-w-xl mt-20 mb-8" />
<Button className="w-min h-11" asChild> <Button className="w-min h-11" asChild>
<Link to={{ pathname: "/app" }} className="!text-base"> <Link to={{ pathname: "/app" }} className="text-base!">
Go to Editor Go to Editor
<ChevronRightIcon className="!h-6 !w-6" /> <ChevronRightIcon className="size-6!" />
</Link> </Link>
</Button> </Button>

View file

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