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:
parent
4193ebcaf4
commit
5eedff5bab
58 changed files with 3819 additions and 4765 deletions
|
|
@ -1,15 +1,16 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": false,
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
|
|
@ -17,5 +18,5 @@
|
|||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
"registries": {}
|
||||
}
|
||||
127
package.json
127
package.json
|
|
@ -1,67 +1,64 @@
|
|||
{
|
||||
"name": "blockmatic",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pixi/graphics-smooth": "^1.1.1",
|
||||
"@pixi/react": "^7.1.2",
|
||||
"@pixi/tilemap": "4.1.0",
|
||||
"@radix-ui/react-checkbox": "^1.2.3",
|
||||
"@radix-ui/react-dialog": "^1.1.11",
|
||||
"@radix-ui/react-label": "^2.1.4",
|
||||
"@radix-ui/react-menubar": "^1.1.12",
|
||||
"@radix-ui/react-popover": "^1.1.11",
|
||||
"@radix-ui/react-scroll-area": "^1.2.6",
|
||||
"@radix-ui/react-select": "^2.2.2",
|
||||
"@radix-ui/react-separator": "^1.1.4",
|
||||
"@radix-ui/react-slider": "^1.3.2",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@radix-ui/react-tabs": "^1.1.9",
|
||||
"@radix-ui/react-toggle": "^1.1.6",
|
||||
"@radix-ui/react-toggle-group": "^1.1.7",
|
||||
"@radix-ui/react-tooltip": "^1.2.4",
|
||||
"@uiw/react-color": "^2.5.0",
|
||||
"@use-gesture/react": "^10.3.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"lucide-react": "^0.464.0",
|
||||
"nbtify": "^2.2.0",
|
||||
"pixi.js": "^7.4.3",
|
||||
"react": "^18.3.1",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-router": "^7.5.2",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.1",
|
||||
"@types/node": "^22.15.2",
|
||||
"@types/react": "^18.3.20",
|
||||
"@types/react-dom": "^18.3.6",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^15.15.0",
|
||||
"postcss": "^8.5.3",
|
||||
"sharp": "^0.33.5",
|
||||
"svgo": "^3.3.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.6.3",
|
||||
"typescript-eslint": "^8.31.0",
|
||||
"vite": "^6.3.3",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
}
|
||||
"name": "blockmatic",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pixi/react": "^8.0.3",
|
||||
"@pixi/tilemap": "5.0.2",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-menubar": "^1.1.16",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@uiw/react-color": "^2.8.1",
|
||||
"@use-gesture/react": "^10.3.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"nbtify": "^2.2.0",
|
||||
"pixi.js": "^8.13.2",
|
||||
"react": "^19.1.1",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-router": "^7.9.3",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@tailwindcss/postcss": "^4.1.14",
|
||||
"@types/node": "^24.6.1",
|
||||
"@types/react": "^19.1.17",
|
||||
"@types/react-dom": "^19.1.11",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.22",
|
||||
"globals": "^16.4.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"vite": "^7.1.7",
|
||||
"vite-plugin-svgr": "^4.5.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5279
pnpm-lock.yaml
5279
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,5 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { numberToVersion, versionToNumber } from "@/utils/version";
|
|||
import { HistoryContext } from "@/context/History";
|
||||
|
||||
const versions = [
|
||||
"1.21.9",
|
||||
"1.21.4",
|
||||
"1.21",
|
||||
"1.20",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
|
||||
import * as PIXI from "pixi.js";
|
||||
import { useApp } from "@pixi/react";
|
||||
import { useApplication } from "@pixi/react";
|
||||
import { CompositeTilemap, settings } from "@pixi/tilemap";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -19,9 +19,9 @@ interface Props {
|
|||
settings.use32bitIndex = true;
|
||||
|
||||
function Blocks({ blocks, missingTexture, textures, coords, scale, version, isSelectionLayer }: Props) {
|
||||
const app = useApp();
|
||||
const tilemapRef = useRef<CompositeTilemap>();
|
||||
const containerRef = useRef<PIXI.Container>();
|
||||
const app = useApplication().app;
|
||||
const tilemapRef = useRef<CompositeTilemap | null>(null);
|
||||
const containerRef = useRef<PIXI.Container | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!tilemapRef.current) return;
|
||||
|
|
@ -29,7 +29,6 @@ function Blocks({ blocks, missingTexture, textures, coords, scale, version, isSe
|
|||
|
||||
tilemap.clear();
|
||||
|
||||
// Tile solid colors at smaller scales
|
||||
blocks.forEach((block) => {
|
||||
tilemap.tile(textures[block.name] ?? missingTexture, block.x * 16, block.y * 16);
|
||||
});
|
||||
|
|
@ -48,7 +47,7 @@ function Blocks({ blocks, missingTexture, textures, coords, scale, version, isSe
|
|||
tilemapRef.current = tilemap;
|
||||
container.addChild(tilemap);
|
||||
|
||||
if (isSelectionLayer) container.filters = [new PIXI.AlphaFilter(0.5)];
|
||||
if (isSelectionLayer) container.filters = [new PIXI.AlphaFilter({ alpha: 0.5 })];
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ import { useGesture } from "@use-gesture/react";
|
|||
import { isMobile } from "react-device-detect";
|
||||
|
||||
import * as PIXI from "pixi.js";
|
||||
import { Container, Stage } from "@pixi/react";
|
||||
import { Application } from "@pixi/react";
|
||||
|
||||
import { LoadingContext } from "@/context/Loading";
|
||||
import { CanvasContext } from "@/context/Canvas";
|
||||
import { HistoryContext } from "@/context/History";
|
||||
import { SelectionContext } from "@/context/Selection";
|
||||
|
|
@ -39,10 +40,12 @@ import CanvasInformation from "./information/Canvas";
|
|||
import SelectionBar from "./SelectionBar";
|
||||
|
||||
// Set scale mode to NEAREST
|
||||
PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST;
|
||||
PIXI.TextureSource.defaultOptions.scaleMode = "nearest";
|
||||
|
||||
function Canvas() {
|
||||
const { stageSize, canvasSize, blocks, coords, scale, version, setStageSize, setBlocks, setCoords, setScale } = useContext(CanvasContext);
|
||||
const { loading } = useContext(LoadingContext);
|
||||
const { stageSize, canvasSize, blocks, coords, scale, version, setStageSize, setBlocks, setCoords, setScale, centerCanvas } =
|
||||
useContext(CanvasContext);
|
||||
const { addHistory, undo, redo } = useContext(HistoryContext);
|
||||
const { selectionCoords, selectionLayerBlocks, setSelectionCoords, setSelectionLayerBlocks } = useContext(SelectionContext);
|
||||
const { settings } = useContext(SettingsContext);
|
||||
|
|
@ -59,10 +62,11 @@ function Canvas() {
|
|||
const mouseMovementRef = useRef<Position>({ x: 0, y: 0 });
|
||||
const dragging = useRef(false);
|
||||
const dragStartCoordsRef = useRef<Position>({ x: 0, y: 0 });
|
||||
const hasCenteredRef = useRef(false); // For centering canvas on startup
|
||||
|
||||
const holdingShiftRef = useRef(false);
|
||||
const holdingAltRef = useRef(false);
|
||||
const oldToolRef = useRef<Tool>();
|
||||
const oldToolRef = useRef<Tool>(null);
|
||||
const [cssCursor, setCssCursor] = useState("crosshair");
|
||||
|
||||
const startBlocksRef = useRef<Block[]>([]);
|
||||
|
|
@ -447,7 +451,7 @@ function Canvas() {
|
|||
|
||||
resizeCanvas();
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [stageContainerRef, setStageSize]);
|
||||
}, [loading, setStageSize]);
|
||||
|
||||
// Window events handler
|
||||
useEffect(() => {
|
||||
|
|
@ -478,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 (
|
||||
<div ref={stageContainerRef} className="relative">
|
||||
<Stage
|
||||
width={stageSize.width}
|
||||
height={stageSize.height}
|
||||
tabIndex={0}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onWheel={onWheel}
|
||||
options={{ backgroundAlpha: 0 }}
|
||||
style={{ cursor: cssCursor }}
|
||||
className="w-full h-full bg-zinc-100 dark:bg-black touch-none select-none"
|
||||
>
|
||||
<div
|
||||
ref={stageContainerRef}
|
||||
tabIndex={0}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onWheel={onWheel}
|
||||
style={{ cursor: cssCursor }}
|
||||
className="relative"
|
||||
>
|
||||
<Application resizeTo={stageContainerRef} backgroundAlpha={0} className="touch-none select-none">
|
||||
<Blocks blocks={visibleBlocks} missingTexture={missingTexture} textures={textures} coords={coords} scale={scale} version={version} />
|
||||
{/* Selection layer */}
|
||||
<Blocks
|
||||
|
|
@ -507,18 +519,18 @@ function Canvas() {
|
|||
version={version}
|
||||
/>
|
||||
|
||||
<Container x={coords.x} y={coords.y} scale={scale}>
|
||||
<pixiContainer x={coords.x} y={coords.y} scale={scale}>
|
||||
{settings.canvasBorder && <CanvasBorder canvasSize={canvasSize} isDark={isDark} />}
|
||||
<Cursor mouseCoords={mouseCoords} radius={radius} isDark={isDark} />
|
||||
<Selection selection={selectionCoords} coords={coords} scale={scale} isDark={isDark} />
|
||||
</Container>
|
||||
<Selection selection={selectionCoords} isDark={isDark} />
|
||||
</pixiContainer>
|
||||
|
||||
{settings.grid && (
|
||||
<Container filters={[new PIXI.AlphaFilter(0.1)]}>
|
||||
<pixiContainer filters={[new PIXI.AlphaFilter({ alpha: 0.1 })]}>
|
||||
<Grid stageSize={stageSize} coords={coords} scale={scale} isDark={isDark} />
|
||||
</Container>
|
||||
</pixiContainer>
|
||||
)}
|
||||
</Stage>
|
||||
</Application>
|
||||
|
||||
<CursorInformation mouseCoords={mouseCoords} />
|
||||
<CanvasInformation />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { Graphics } from "@pixi/react";
|
||||
|
||||
interface Props {
|
||||
canvasSize: BoundingBox;
|
||||
isDark: boolean;
|
||||
|
|
@ -7,11 +5,11 @@ interface Props {
|
|||
|
||||
function CanvasBorder({ canvasSize, isDark }: Props) {
|
||||
return (
|
||||
<Graphics
|
||||
<pixiGraphics
|
||||
draw={(g) => {
|
||||
g.clear();
|
||||
g.lineStyle(2, isDark ? 0xffffff : 0x000000, 0.25, 1);
|
||||
g.drawRect(canvasSize.minX * 16, canvasSize.minY * 16, (canvasSize.maxX - canvasSize.minX) * 16, (canvasSize.maxY - canvasSize.minY) * 16);
|
||||
g.rect(canvasSize.minX * 16, canvasSize.minY * 16, (canvasSize.maxX - canvasSize.minX) * 16, (canvasSize.maxY - canvasSize.minY) * 16);
|
||||
g.stroke({ width: 2, color: isDark ? 0xffffff : 0x000000, alpha: 0.25, alignment: 0 });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { Graphics } from "@pixi/react";
|
||||
|
||||
interface Props {
|
||||
mouseCoords: Position;
|
||||
radius: number;
|
||||
|
|
@ -14,13 +12,13 @@ function Cursor({ mouseCoords, radius, isDark }: Props) {
|
|||
const size = radius * 16;
|
||||
|
||||
return (
|
||||
<Graphics
|
||||
<pixiGraphics
|
||||
x={(mouseCoords.x + offset) * 16}
|
||||
y={(mouseCoords.y + offset) * 16}
|
||||
draw={(g) => {
|
||||
g.clear();
|
||||
g.lineStyle(1, isDark ? 0xffffff : 0x000000, 1);
|
||||
g.drawRect(0, 0, size, size);
|
||||
g.rect(0, 0, size, size);
|
||||
g.stroke({ width: 1, color: isDark ? 0xffffff : 0x000000, alignment: 1 });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { Graphics } from "@pixi/react";
|
||||
|
||||
interface Props {
|
||||
stageSize: Dimension;
|
||||
coords: Position;
|
||||
|
|
@ -9,10 +7,9 @@ interface Props {
|
|||
|
||||
function Grid({ stageSize, coords, scale, isDark }: Props) {
|
||||
return (
|
||||
<Graphics
|
||||
<pixiGraphics
|
||||
draw={(g) => {
|
||||
g.clear();
|
||||
g.lineStyle(1, isDark ? 0xffffff : 0x000000);
|
||||
|
||||
const tileSize = 16 * scale;
|
||||
|
||||
|
|
@ -25,6 +22,8 @@ function Grid({ stageSize, coords, scale, isDark }: Props) {
|
|||
g.moveTo(0, y);
|
||||
g.lineTo(stageSize.width, y);
|
||||
}
|
||||
|
||||
g.stroke({ width: 1, color: isDark ? 0xffffff : 0x000000 });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
selection: CoordinateArray;
|
||||
coords: Position;
|
||||
scale: number;
|
||||
isDark: boolean;
|
||||
}
|
||||
|
||||
const shader = new DashLineShader({ dash: 8, gap: 5 });
|
||||
const DASH_LENGTH = 8;
|
||||
const GAP_LENGTH = 5;
|
||||
|
||||
function Selection({ selection, coords, scale, isDark }: Props) {
|
||||
const app = useApp();
|
||||
const selectionRef = useRef<SmoothGraphics>();
|
||||
function Selection({ selection, isDark }: Props) {
|
||||
return (
|
||||
<pixiGraphics
|
||||
draw={(g) => {
|
||||
g.clear();
|
||||
|
||||
const drawSelection = () => {
|
||||
if (!selectionRef.current) return;
|
||||
const graphics = selectionRef.current;
|
||||
graphics.clear();
|
||||
graphics.lineStyle({ width: 1, color: isDark ? 0xffffff : 0x000000, shader });
|
||||
const edges = new Set<string>();
|
||||
|
||||
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]) => {
|
||||
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];
|
||||
// Add edges, or remove them if they already exist (shared edges)
|
||||
[top, bottom, left, right].forEach((edge) => {
|
||||
const stringified = JSON.stringify(edge);
|
||||
if (edges.has(stringified)) {
|
||||
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)
|
||||
[top, bottom, left, right].forEach((edge) => {
|
||||
const stringified = JSON.stringify(edge);
|
||||
if (edges.has(stringified)) {
|
||||
edges.delete(stringified); // Shared edge, remove it
|
||||
} else {
|
||||
edges.add(stringified); // Unique edge, add it
|
||||
}
|
||||
});
|
||||
});
|
||||
// Draw each remaining edge
|
||||
edges.forEach((edge) => {
|
||||
const [x1, y1, x2, y2] = JSON.parse(edge) as [number, number, number, number];
|
||||
|
||||
// Draw the remaining edges
|
||||
edges.forEach((edge) => {
|
||||
const [x1, y1, x2, y2] = JSON.parse(edge);
|
||||
graphics.moveTo(x1 * 16 * scale, y1 * 16 * scale);
|
||||
graphics.lineTo(x2 * 16 * scale, y2 * 16 * scale);
|
||||
});
|
||||
};
|
||||
// Draw dashed line
|
||||
const startX = x1 * 16;
|
||||
const startY = y1 * 16;
|
||||
const endX = x2 * 16;
|
||||
const endY = y2 * 16;
|
||||
|
||||
useEffect(() => {
|
||||
const graphics = new SmoothGraphics();
|
||||
selectionRef.current = graphics;
|
||||
drawSelection();
|
||||
app.stage.addChild(graphics);
|
||||
}, []);
|
||||
const dx = endX - startX;
|
||||
const dy = endY - startY;
|
||||
const lineLength = Math.hypot(dx, dy);
|
||||
const angle = Math.atan2(dy, dx);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectionRef.current) return;
|
||||
const graphics = selectionRef.current;
|
||||
let drawn = 0;
|
||||
while (drawn < lineLength) {
|
||||
const segmentLength = Math.min(DASH_LENGTH, lineLength - drawn);
|
||||
const sx = startX + Math.cos(angle) * drawn;
|
||||
const sy = startY + Math.sin(angle) * drawn;
|
||||
const ex = sx + Math.cos(angle) * segmentLength;
|
||||
const ey = sy + Math.sin(angle) * segmentLength;
|
||||
|
||||
graphics.x = coords.x;
|
||||
graphics.y = coords.y;
|
||||
drawSelection();
|
||||
}, [coords]);
|
||||
g.moveTo(sx, sy);
|
||||
g.lineTo(ex, ey);
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -58,11 +58,11 @@ function SelectionBar({ startBlocks, startSelectionCoords }: Props) {
|
|||
${isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-6 pointer-events-none"}
|
||||
`}
|
||||
>
|
||||
<Button variant="ghost" className="w-8 h-8" onClick={cancel}>
|
||||
<Button variant="ghost" className="size h-8" onClick={cancel}>
|
||||
<XIcon />
|
||||
</Button>
|
||||
<span className="mx-2 text-[0.85rem]">Confirm?</span>
|
||||
<Button variant="ghost" className="w-8 h-8" onClick={confirm}>
|
||||
<Button variant="ghost" className="size h-8" onClick={confirm}>
|
||||
<CheckIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ function CanvasInformation() {
|
|||
value={[Math.log10(scale)]}
|
||||
onValueChange={onValueChange}
|
||||
orientation="vertical"
|
||||
className="!h-32"
|
||||
className="h-32!"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ function OpenImage({ close }: DialogProps) {
|
|||
setLoading(true);
|
||||
|
||||
// Wait for loading indicator to appear
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
|
||||
// Load image through JS canvas
|
||||
const canvas = document.createElement("canvas");
|
||||
|
|
@ -150,9 +150,14 @@ function OpenImage({ close }: DialogProps) {
|
|||
}
|
||||
};
|
||||
|
||||
// Trying to center and close above doesn't work for some reason
|
||||
useEffect(() => {
|
||||
if (!isFinished.current) return;
|
||||
centerCanvas();
|
||||
|
||||
// Wrap in requestAnimationFrame() to fix bug where canvas is black
|
||||
requestAnimationFrame(() => {
|
||||
centerCanvas();
|
||||
});
|
||||
close();
|
||||
|
||||
return () => {
|
||||
|
|
@ -224,13 +229,13 @@ function OpenImage({ close }: DialogProps) {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[auto,1fr] gap-2">
|
||||
<div className="grid grid-cols-[auto_1fr] gap-2">
|
||||
{image && acceptedFiles[0] && (
|
||||
<>
|
||||
<img
|
||||
src={image.src}
|
||||
alt="your image"
|
||||
className="w-48 h-48 object-contain border rounded-lg"
|
||||
className="size-48 object-contain border rounded-lg"
|
||||
style={{ background: "repeating-conic-gradient(#fff 0 90deg, #bbb 0 180deg) 0 0/25% 25%" }}
|
||||
/>
|
||||
|
||||
|
|
@ -259,7 +264,7 @@ function OpenImage({ close }: DialogProps) {
|
|||
variant="outline"
|
||||
pressed={linkAspectRatio}
|
||||
onPressedChange={() => setLinkAspectRatio(!linkAspectRatio)}
|
||||
className="h-8 !min-w-8 p-0 mt-auto mb-1"
|
||||
className="h-8 min-w-8! p-0 mt-auto mb-1"
|
||||
>
|
||||
<LinkIcon />
|
||||
</Toggle>
|
||||
|
|
@ -344,7 +349,7 @@ function OpenImage({ close }: DialogProps) {
|
|||
</Tabs>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="!justify-between">
|
||||
<DialogFooter className="justify-between!">
|
||||
<VersionCombobox version={version} setVersion={setVersion} isContext />
|
||||
|
||||
<div className="flex gap-2">
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@ function SaveImage({ close, registerSubmit, dialogKeyHandler }: DialogProps) {
|
|||
const [fileName, setFileName] = useState("blockmatic");
|
||||
const textures = useTextures(version);
|
||||
|
||||
const onSubmit = () => {
|
||||
const onSubmit = async () => {
|
||||
const width = canvasSize.maxX - canvasSize.minX;
|
||||
const height = canvasSize.maxY - canvasSize.minY;
|
||||
|
||||
const renderer = new PIXI.Renderer({
|
||||
const app = new PIXI.Application();
|
||||
await app.init({
|
||||
width: 16 * width,
|
||||
height: 16 * height,
|
||||
backgroundAlpha: 1,
|
||||
|
|
@ -35,15 +36,10 @@ function SaveImage({ close, registerSubmit, dialogKeyHandler }: DialogProps) {
|
|||
container.addChild(sprite);
|
||||
});
|
||||
|
||||
const renderTexture = PIXI.RenderTexture.create({
|
||||
width: 16 * width,
|
||||
height: 16 * height,
|
||||
});
|
||||
app.stage.addChild(container);
|
||||
app.renderer.render(app.stage);
|
||||
|
||||
renderer.render(container, { renderTexture });
|
||||
|
||||
const canvas = renderer.extract.canvas(renderTexture);
|
||||
canvas.toBlob!((blob) => {
|
||||
app.canvas.toBlob!((blob) => {
|
||||
if (!blob) return;
|
||||
|
||||
const link = document.createElement("a");
|
||||
|
|
@ -52,10 +48,9 @@ function SaveImage({ close, registerSubmit, dialogKeyHandler }: DialogProps) {
|
|||
link.click();
|
||||
|
||||
URL.revokeObjectURL(link.href);
|
||||
});
|
||||
|
||||
renderer.destroy();
|
||||
}, "image/png");
|
||||
|
||||
app.destroy(true);
|
||||
close();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useContext, useMemo, useRef, useState } from "react";
|
||||
import { Container, Graphics, Sprite, Stage } from "@pixi/react";
|
||||
import React, { useContext, useMemo, useState } from "react";
|
||||
|
||||
import { CanvasContext } from "@/context/Canvas";
|
||||
import { ThemeContext } from "@/context/Theme";
|
||||
|
|
@ -7,13 +6,14 @@ import { TexturesContext } from "@/context/Textures";
|
|||
|
||||
import { useBlockData } from "@/hooks/useBlockData";
|
||||
import { useTextures } from "@/hooks/useTextures";
|
||||
import { Application } from "@pixi/react";
|
||||
|
||||
interface Props {
|
||||
stageWidth: number;
|
||||
searchInput: string;
|
||||
selectedBlocks: string[];
|
||||
setSelectedBlocks: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
userModifiedBlocks: React.MutableRefObject<boolean>;
|
||||
userModifiedBlocks: React.RefObject<boolean>;
|
||||
}
|
||||
|
||||
function BlockSelector({ stageWidth, searchInput, selectedBlocks, setSelectedBlocks, userModifiedBlocks }: Props) {
|
||||
|
|
@ -25,7 +25,6 @@ function BlockSelector({ stageWidth, searchInput, selectedBlocks, setSelectedBlo
|
|||
const textures = useTextures(version);
|
||||
|
||||
const [hoverPosition, setHoverPosition] = useState<Position | null>(null);
|
||||
const showStage = useRef(true);
|
||||
|
||||
const filteredBlocks = useMemo(() => Object.keys(blockData).filter((value) => value.includes(searchInput)), [searchInput, blockData]);
|
||||
const blocksPerColumn = Math.floor(stageWidth / (32 + 2));
|
||||
|
|
@ -40,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 (
|
||||
<Stage
|
||||
width={stageWidth}
|
||||
height={Math.ceil(Object.keys(blockData).length / blocksPerColumn) * (32 + 2)}
|
||||
options={{ backgroundAlpha: 0 }}
|
||||
onPointerLeave={() => setHoverPosition(null)}
|
||||
onUnmount={() => {
|
||||
// NOTE: this event gets called a couple times when run in development
|
||||
showStage.current = false;
|
||||
}}
|
||||
>
|
||||
<Container>
|
||||
{filteredBlocks.map((block, index) => {
|
||||
const x = (index % blocksPerColumn) * (32 + 2) + 2;
|
||||
const y = Math.floor(index / blocksPerColumn) * (32 + 2) + 2;
|
||||
<div onPointerLeave={() => setHoverPosition(null)}>
|
||||
<Application width={stageWidth} height={Math.ceil(Object.keys(blockData).length / blocksPerColumn) * (32 + 2)} backgroundAlpha={0}>
|
||||
<pixiContainer>
|
||||
{filteredBlocks.map((block, index) => {
|
||||
const x = (index % blocksPerColumn) * (32 + 2) + 2;
|
||||
const y = Math.floor(index / blocksPerColumn) * (32 + 2) + 2;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sprite
|
||||
key={block}
|
||||
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}
|
||||
return (
|
||||
<>
|
||||
<pixiSprite
|
||||
key={block}
|
||||
texture={textures[block] ?? missingTexture}
|
||||
x={x}
|
||||
y={y}
|
||||
draw={(g) => {
|
||||
g.clear();
|
||||
g.lineStyle(2, isDark ? 0xffffff : 0x000000, 0.4, 0);
|
||||
g.drawRect(0, 0, 32, 32);
|
||||
}}
|
||||
scale={2}
|
||||
eventMode={"static"}
|
||||
onPointerOver={() => setHoverPosition({ x, y })}
|
||||
onClick={() => onClick(block)}
|
||||
alpha={selectedBlocks.includes(block) ? 1 : 0.2}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
|
||||
{hoverPosition && (
|
||||
<Graphics
|
||||
x={hoverPosition.x}
|
||||
y={hoverPosition.y}
|
||||
draw={(g) => {
|
||||
g.clear();
|
||||
g.lineStyle(4, isDark ? 0xffffff : 0x000000, 1, 1);
|
||||
g.drawRect(0, 0, 32, 32);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</Stage>
|
||||
{selectedBlocks.includes(block) && (
|
||||
<pixiGraphics
|
||||
key={index}
|
||||
x={x}
|
||||
y={y}
|
||||
draw={(g) => {
|
||||
g.clear();
|
||||
g.rect(0, 0, 32, 32);
|
||||
g.stroke({ width: 2, color: isDark ? 0xffffff : 0x000000, alpha: 0.4, alignment: 0 });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ function ImageComparison() {
|
|||
return (
|
||||
<div
|
||||
onPointerMove={onPointerMove}
|
||||
className="relative select-none w-full aspect-[270/217] aspect flex justify-center rounded-xl border border-zinc-200 dark:border-zinc-800 shadow-md"
|
||||
className="relative select-none w-full aspect-270/217 aspect flex justify-center rounded-xl border border-zinc-200 dark:border-zinc-800 shadow-md"
|
||||
>
|
||||
<img
|
||||
src="/bliss/bliss_original.png"
|
||||
|
|
@ -40,7 +40,7 @@ function ImageComparison() {
|
|||
}}
|
||||
>
|
||||
<button className="bg-zinc-200 rounded hover:scale-110 transition-all w-5 h-10 select-none -translate-y-1/2 absolute top-1/2 -ml-2 z-30 cursor-ew-resize flex justify-center items-center">
|
||||
<GripVerticalIcon color="black" className="w-4 h-4" />
|
||||
<GripVerticalIcon color="black" className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ function Menubar() {
|
|||
<SelectMenu />
|
||||
<ViewMenu />
|
||||
|
||||
<div className="!ml-auto pl-4 grid grid-cols-3 items-center gap-1 min-w-20">
|
||||
<div className="ml-auto! pl-4 grid grid-cols-3 items-center gap-1 min-w-20">
|
||||
<ThemeIcon inApp />
|
||||
<a href="https://github.com/trafficlunar/blockmatic" className="w-5">
|
||||
<GithubIcon fill={isDark ? "white" : "black"} />
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { useContext, useEffect, useMemo, useState } from "react";
|
||||
import { Container, Graphics, Sprite, Stage } from "@pixi/react";
|
||||
import { BlocksIcon } from "lucide-react";
|
||||
|
||||
import { Application } from "@pixi/react";
|
||||
import * as PIXI from "pixi.js";
|
||||
|
||||
import { LoadingContext } from "@/context/Loading";
|
||||
import { CanvasContext } from "@/context/Canvas";
|
||||
import { ThemeContext } from "@/context/Theme";
|
||||
import { ToolContext } from "@/context/Tool";
|
||||
|
||||
import { useBlockData } from "@/hooks/useBlockData";
|
||||
import { useTextures } from "@/hooks/useTextures";
|
||||
import { Application } from "pixi.js";
|
||||
|
||||
interface Props {
|
||||
stageWidth: number;
|
||||
|
|
@ -16,11 +18,12 @@ interface Props {
|
|||
}
|
||||
|
||||
function BlockSelector({ stageWidth, searchInput }: Props) {
|
||||
const { loading } = useContext(LoadingContext);
|
||||
const { version } = useContext(CanvasContext);
|
||||
const { isDark } = useContext(ThemeContext);
|
||||
const { selectedBlock, setSelectedBlock } = useContext(ToolContext);
|
||||
|
||||
const [app, setApp] = useState<Application>();
|
||||
const [app, setApp] = useState<PIXI.Application>();
|
||||
|
||||
const [hoverPosition, setHoverPosition] = useState<Position | null>(null);
|
||||
const [selectedBlockPosition, setSelectedBlockPosition] = useState<Position | null>({ x: 0, y: 0 });
|
||||
|
|
@ -48,15 +51,17 @@ function BlockSelector({ stageWidth, searchInput }: Props) {
|
|||
}, [searchInput, selectedBlock]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!app?.renderer?.view?.style) return;
|
||||
if (!app?.renderer?.view?.canvas.style) return;
|
||||
|
||||
// Can't set it in props for some reason
|
||||
app.renderer.view.style.touchAction = "auto";
|
||||
app.renderer.view.canvas.style.touchAction = "auto";
|
||||
}, [app]);
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
if (filteredBlocks.length == 0) {
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col justify-center items-center gap-1 text-zinc-400">
|
||||
<div className="size-full flex flex-col justify-center items-center gap-1 text-zinc-400">
|
||||
<BlocksIcon size={40} />
|
||||
<span>No blocks found</span>
|
||||
</div>
|
||||
|
|
@ -64,64 +69,65 @@ function BlockSelector({ stageWidth, searchInput }: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Stage
|
||||
width={stageWidth}
|
||||
height={Math.ceil(Object.keys(blockData).length / blocksPerColumn) * (32 + 2) + 8}
|
||||
options={{ backgroundAlpha: 0 }}
|
||||
onMouseLeave={() => setHoverPosition(null)}
|
||||
onMount={setApp}
|
||||
>
|
||||
<Container>
|
||||
{filteredBlocks.map((block, index) => {
|
||||
const texture = textures[block];
|
||||
const { x, y } = getBlockPosition(index);
|
||||
<div onMouseLeave={() => setHoverPosition(null)} className="h-min">
|
||||
<Application
|
||||
width={stageWidth}
|
||||
height={Math.ceil(Object.keys(blockData).length / blocksPerColumn) * (32 + 2) + 8}
|
||||
backgroundAlpha={0}
|
||||
onInit={setApp}
|
||||
>
|
||||
<pixiContainer>
|
||||
{filteredBlocks.map((block, index) => {
|
||||
const texture = textures[block];
|
||||
const { x, y } = getBlockPosition(index);
|
||||
|
||||
const onClick = () => {
|
||||
setSelectedBlock(block);
|
||||
setSelectedBlockPosition({ x, y });
|
||||
};
|
||||
const onClick = () => {
|
||||
setSelectedBlock(block);
|
||||
setSelectedBlockPosition({ x, y });
|
||||
};
|
||||
|
||||
return (
|
||||
<Sprite
|
||||
key={block}
|
||||
texture={texture}
|
||||
x={x}
|
||||
y={y}
|
||||
scale={2}
|
||||
eventMode={"static"}
|
||||
mouseover={() => setHoverPosition({ x, y })}
|
||||
click={onClick}
|
||||
tap={onClick}
|
||||
alpha={selectedBlock == block ? 1 : 0.3}
|
||||
return (
|
||||
<pixiSprite
|
||||
key={block}
|
||||
texture={texture}
|
||||
x={x}
|
||||
y={y}
|
||||
scale={2}
|
||||
eventMode={"static"}
|
||||
onMouseOver={() => setHoverPosition({ x, y })}
|
||||
onClick={onClick}
|
||||
onTap={onClick}
|
||||
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 && (
|
||||
<Graphics
|
||||
x={hoverPosition.x}
|
||||
y={hoverPosition.y}
|
||||
draw={(g) => {
|
||||
g.clear();
|
||||
g.lineStyle(4, isDark ? 0xffffff : 0x000000, 1, 1);
|
||||
g.drawRect(0, 0, 32, 32);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedBlockPosition && (
|
||||
<Graphics
|
||||
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>
|
||||
{selectedBlockPosition && (
|
||||
<pixiGraphics
|
||||
x={selectedBlockPosition.x}
|
||||
y={selectedBlockPosition.y}
|
||||
draw={(g) => {
|
||||
g.clear();
|
||||
g.rect(0, 0, 32, 32);
|
||||
g.stroke({ width: 2, color: isDark ? 0xffffff : 0x000000, alpha: 0.75, alignment: 0 });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</pixiContainer>
|
||||
</Application>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { Container, Sprite, Stage } from "@pixi/react";
|
||||
|
||||
import { CanvasContext } from "@/context/Canvas";
|
||||
import { HistoryContext } from "@/context/History";
|
||||
|
|
@ -11,6 +10,7 @@ import { Label } from "@/components/ui/label";
|
|||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import { useTextures } from "@/hooks/useTextures";
|
||||
import { Application } from "@pixi/react";
|
||||
|
||||
function Replace() {
|
||||
const { version, setBlocks } = useContext(CanvasContext);
|
||||
|
|
@ -80,11 +80,11 @@ function Replace() {
|
|||
onClick={() => onClickBlockButton(1)}
|
||||
className="h-10 rounded-md border border-zinc-200 dark:border-zinc-800 dark:bg-zinc-950 flex justify-center items-center"
|
||||
>
|
||||
<Stage width={32} height={32} options={{ backgroundAlpha: 0 }}>
|
||||
<Container>
|
||||
<Sprite texture={textures[block1] ?? missingTexture} scale={2} />
|
||||
</Container>
|
||||
</Stage>
|
||||
<Application width={32} height={32} backgroundAlpha={0}>
|
||||
<pixiContainer>
|
||||
<pixiSprite texture={textures[block1] ?? missingTexture} scale={2} />
|
||||
</pixiContainer>
|
||||
</Application>
|
||||
</button>
|
||||
|
||||
<Label htmlFor="radius">Block 2</Label>
|
||||
|
|
@ -92,11 +92,11 @@ function Replace() {
|
|||
onClick={() => onClickBlockButton(2)}
|
||||
className="h-10 rounded-md border border-zinc-200 dark:border-zinc-800 dark:bg-zinc-950 flex justify-center items-center"
|
||||
>
|
||||
<Stage width={32} height={32} options={{ backgroundAlpha: 0 }}>
|
||||
<Container>
|
||||
<Sprite texture={textures[block2] ?? missingTexture} scale={2} />
|
||||
</Container>
|
||||
</Stage>
|
||||
<Application width={32} height={32} backgroundAlpha={0}>
|
||||
<pixiContainer>
|
||||
<pixiSprite texture={textures[block2] ?? missingTexture} scale={2} />
|
||||
</pixiContainer>
|
||||
</Application>
|
||||
</button>
|
||||
|
||||
<br />
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ function ToolSettings() {
|
|||
{tool === "shape" && (
|
||||
<>
|
||||
<Label htmlFor="shape">Shape</Label>
|
||||
<Select value={shape} onValueChange={(value) => setShape(value as Shape)}>
|
||||
<Select value={shape} onValueChange={(value: string) => setShape(value as Shape)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a shape" />
|
||||
</SelectTrigger>
|
||||
|
|
@ -39,7 +39,7 @@ function ToolSettings() {
|
|||
</Select>
|
||||
|
||||
<Label htmlFor="filled">Filled</Label>
|
||||
<Checkbox name="filled" checked={filled} onCheckedChange={(checked) => setFilled(!!checked)} className="w-6 h-6" />
|
||||
<Checkbox name="filled" checked={filled} onCheckedChange={(checked) => setFilled(!!checked)} className="size-6" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ function Sidebar() {
|
|||
{settings.blockSelector && (
|
||||
<>
|
||||
<Input placeholder="Search for blocks..." value={searchInput} onChange={(e) => setSearchInput(e.target.value)} />
|
||||
<ScrollArea ref={divRef} className={`w-full flex-1 pb-0 ${isMobileView ? "min-h-48" : ""}`}>
|
||||
<ScrollArea ref={divRef} className={`h-48 w-full flex-1 pb-0 ${isMobileView ? "min-h-48" : ""}`}>
|
||||
<BlockSelector stageWidth={stageWidth} searchInput={searchInput} />
|
||||
</ScrollArea>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { Container, Sprite, Stage } from "@pixi/react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { Application } from "@pixi/react";
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
||||
|
|
@ -18,7 +18,6 @@ function SelectedBlock() {
|
|||
const { selectedBlock } = useContext(ToolContext);
|
||||
|
||||
const textures = useTextures(version);
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [selectedBlockName, setSelectedBlockName] = useState("Stone");
|
||||
|
||||
|
|
@ -31,12 +30,12 @@ function SelectedBlock() {
|
|||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<div ref={divRef} className="absolute bottom-1 w-8 h-8 outline outline-1 outline-zinc-800 dark:outline-zinc-200 rounded">
|
||||
<Stage width={divRef.current?.clientWidth} height={divRef.current?.clientHeight}>
|
||||
<Container>
|
||||
<Sprite texture={textures[selectedBlock] ?? missingTexture} scale={2} />
|
||||
</Container>
|
||||
</Stage>
|
||||
<div className="absolute bottom-1 w-8 h-8 outline-solid outline-1 outline-zinc-800 dark:outline-zinc-200 rounded p-0!">
|
||||
<Application width={32} height={32}>
|
||||
<pixiContainer>
|
||||
<pixiSprite texture={textures[selectedBlock] ?? missingTexture} scale={2} />
|
||||
</pixiContainer>
|
||||
</Application>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
|
|
|
|||
|
|
@ -41,14 +41,16 @@ function Toolbar() {
|
|||
type="single"
|
||||
value={tool}
|
||||
onValueChange={onToolChange}
|
||||
className="flex flex-col justify-start py-1 border-r border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950"
|
||||
className="w-full flex flex-col items-center border-r border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 *:py-0.5"
|
||||
>
|
||||
{/* Hand */}
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<ToggleGroupItem value="hand" className="!p-0 !h-8 !min-w-8">
|
||||
<HandIcon />
|
||||
</ToggleGroupItem>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<ToggleGroupItem value="hand" className="p-0! h-8! min-w-8! rounded-xl">
|
||||
<HandIcon />
|
||||
</ToggleGroupItem>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Hand (H)</p>
|
||||
|
|
@ -57,10 +59,12 @@ function Toolbar() {
|
|||
|
||||
{/* Move */}
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<ToggleGroupItem value="move" className="!p-0 !h-8 !min-w-8">
|
||||
<MousePointer2Icon />
|
||||
</ToggleGroupItem>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<ToggleGroupItem value="move" className="p-0! h-8! min-w-8! rounded-xl">
|
||||
<MousePointer2Icon />
|
||||
</ToggleGroupItem>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Move (V)</p>
|
||||
|
|
@ -69,10 +73,12 @@ function Toolbar() {
|
|||
|
||||
{/* Rectangle Select */}
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<ToggleGroupItem value="rectangle-select" className="!p-0 !h-8 !min-w-8">
|
||||
<SquareDashedIcon />
|
||||
</ToggleGroupItem>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<ToggleGroupItem value="rectangle-select" className="p-0! h-8! min-w-8! rounded-xl">
|
||||
<SquareDashedIcon />
|
||||
</ToggleGroupItem>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Rectangle Select (M)</p>
|
||||
|
|
@ -81,10 +87,12 @@ function Toolbar() {
|
|||
|
||||
{/* Lasso */}
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<ToggleGroupItem value="lasso" className="!p-0 !h-8 !min-w-8">
|
||||
<LassoIcon />
|
||||
</ToggleGroupItem>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<ToggleGroupItem value="lasso" className="p-0! h-8! min-w-8! rounded-xl">
|
||||
<LassoIcon />
|
||||
</ToggleGroupItem>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Lasso (L)</p>
|
||||
|
|
@ -93,10 +101,12 @@ function Toolbar() {
|
|||
|
||||
{/* Magic Wand */}
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<ToggleGroupItem value="magic-wand" className="!p-0 !h-8 !min-w-8">
|
||||
<WandIcon />
|
||||
</ToggleGroupItem>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<ToggleGroupItem value="magic-wand" className="p-0! h-8! min-w-8! rounded-xl">
|
||||
<WandIcon />
|
||||
</ToggleGroupItem>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Magic Wand (W)</p>
|
||||
|
|
@ -105,10 +115,12 @@ function Toolbar() {
|
|||
|
||||
{/* Pencil */}
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<ToggleGroupItem value="pencil" className="!p-0 !h-8 !min-w-8">
|
||||
<PencilIcon />
|
||||
</ToggleGroupItem>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<ToggleGroupItem value="pencil" className="p-0! h-8! min-w-8! rounded-xl">
|
||||
<PencilIcon />
|
||||
</ToggleGroupItem>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Pencil (B)</p>
|
||||
|
|
@ -117,10 +129,12 @@ function Toolbar() {
|
|||
|
||||
{/* Eraser */}
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<ToggleGroupItem value="eraser" className="!p-0 !h-8 !min-w-8">
|
||||
<EraserIcon />
|
||||
</ToggleGroupItem>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<ToggleGroupItem value="eraser" className="p-0! h-8! min-w-8! rounded-xl">
|
||||
<EraserIcon />
|
||||
</ToggleGroupItem>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Eraser (E)</p>
|
||||
|
|
@ -129,10 +143,12 @@ function Toolbar() {
|
|||
|
||||
{/* Paint Bucket */}
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<ToggleGroupItem value="paint-bucket" className="!p-0 !h-8 !min-w-8">
|
||||
<PaintBucketIcon />
|
||||
</ToggleGroupItem>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<ToggleGroupItem value="paint-bucket" className="p-0! h-8! min-w-8! rounded-xl">
|
||||
<PaintBucketIcon />
|
||||
</ToggleGroupItem>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Paint Bucket (G)</p>
|
||||
|
|
@ -141,10 +157,12 @@ function Toolbar() {
|
|||
|
||||
{/* Shape */}
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<ToggleGroupItem value="shape" className="!p-0 !h-8 !min-w-8">
|
||||
{ShapeIconComponent && <ShapeIconComponent />}
|
||||
</ToggleGroupItem>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<ToggleGroupItem value="shape" className="p-0! h-8! min-w-8! rounded-xl">
|
||||
{ShapeIconComponent && <ShapeIconComponent />}
|
||||
</ToggleGroupItem>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Shape (U)</p>
|
||||
|
|
@ -153,10 +171,12 @@ function Toolbar() {
|
|||
|
||||
{/* Eyedropper */}
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<ToggleGroupItem value="eyedropper" className="!p-0 !h-8 !min-w-8">
|
||||
<PipetteIcon />
|
||||
</ToggleGroupItem>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<ToggleGroupItem value="eyedropper" className="p-0! h-8! min-w-8! rounded-xl">
|
||||
<PipetteIcon />
|
||||
</ToggleGroupItem>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Eyedropper (I)</p>
|
||||
|
|
@ -165,10 +185,12 @@ function Toolbar() {
|
|||
|
||||
{/* Zoom */}
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<ToggleGroupItem value="zoom" className="!p-0 !h-8 !min-w-8">
|
||||
<ZoomInIcon />
|
||||
</ToggleGroupItem>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<ToggleGroupItem value="zoom" className="p-0! h-8! min-w-8! rounded-xl">
|
||||
<ZoomInIcon />
|
||||
</ToggleGroupItem>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={10}>
|
||||
<p>Zoom (Z)</p>
|
||||
|
|
|
|||
|
|
@ -5,25 +5,26 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-zinc-900 text-zinc-50 hover:bg-zinc-900/90 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-50/90",
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-red-500 text-zinc-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-zinc-50 dark:hover:bg-red-900/90",
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border border-zinc-200 bg-white hover:bg-zinc-100 hover:text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-800 dark:hover:text-zinc-50",
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-zinc-100 text-zinc-900 hover:bg-zinc-100/80 dark:bg-zinc-800 dark:text-zinc-50 dark:hover:bg-zinc-800/80",
|
||||
ghost: "hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-50",
|
||||
link: "text-zinc-900 underline-offset-4 hover:underline dark:text-zinc-50",
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
@ -33,24 +34,25 @@ const buttonVariants = cva(
|
|||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
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 }
|
||||
|
|
|
|||
|
|
@ -40,124 +40,106 @@ function useCarousel() {
|
|||
return context
|
||||
}
|
||||
|
||||
const Carousel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
||||
>(
|
||||
(
|
||||
function Carousel({
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
plugins
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) {
|
||||
return
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) 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())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}, [])
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) return
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
React.useEffect(() => {
|
||||
if (!api) return
|
||||
onSelect(api)
|
||||
api.on("reInit", onSelect)
|
||||
api.on("select", onSelect)
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
return () => {
|
||||
api?.off("select", onSelect)
|
||||
}
|
||||
}, [api, onSelect])
|
||||
|
||||
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]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) {
|
||||
return
|
||||
}
|
||||
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
|
||||
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,
|
||||
}}
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
data-slot="carousel"
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
}
|
||||
)
|
||||
Carousel.displayName = "Carousel"
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const CarouselContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div ref={carouselRef} className="overflow-hidden">
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="overflow-hidden"
|
||||
data-slot="carousel-content"
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
|
|
@ -167,20 +149,16 @@ const CarouselContent = React.forwardRef<
|
|||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
CarouselContent.displayName = "CarouselContent"
|
||||
}
|
||||
|
||||
const CarouselItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
data-slot="carousel-item"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
|
|
@ -189,24 +167,25 @@ const CarouselItem = React.forwardRef<
|
|||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
CarouselItem.displayName = "CarouselItem"
|
||||
}
|
||||
|
||||
const CarouselPrevious = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
function CarouselPrevious({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
data-slot="carousel-previous"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
"absolute size-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-left-12 top-1/2 -translate-y-1/2"
|
||||
? "top-1/2 -left-12 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
|
|
@ -214,28 +193,29 @@ const CarouselPrevious = React.forwardRef<
|
|||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<ArrowLeft />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
CarouselPrevious.displayName = "CarouselPrevious"
|
||||
}
|
||||
|
||||
const CarouselNext = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
function CarouselNext({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
data-slot="carousel-next"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
"absolute size-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-right-12 top-1/2 -translate-y-1/2"
|
||||
? "top-1/2 -right-12 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
|
|
@ -243,12 +223,11 @@ const CarouselNext = React.forwardRef<
|
|||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
<ArrowRight />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
CarouselNext.displayName = "CarouselNext"
|
||||
}
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
|
|
|
|||
|
|
@ -1,28 +1,30 @@
|
|||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-zinc-200 border-zinc-900 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-zinc-900 data-[state=checked]:text-zinc-50 dark:border-zinc-800 dark:border-zinc-50 dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300 dark:data-[state=checked]:bg-zinc-50 dark:data-[state=checked]:text-zinc-900",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"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}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
|
|
|
|||
|
|
@ -1,31 +1,56 @@
|
|||
import * as React from "react"
|
||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
import { SearchIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-white text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
className,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string
|
||||
description?: string
|
||||
className?: string
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-zinc-500 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5 dark:[&_[cmdk-group-heading]]:text-zinc-400">
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent
|
||||
className={cn("overflow-hidden p-0", className)}
|
||||
showCloseButton={showCloseButton}
|
||||
>
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
|
|
@ -33,110 +58,116 @@ const CommandDialog = ({ children, ...props }: DialogProps) => {
|
|||
)
|
||||
}
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<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 = ({
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
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(
|
||||
"ml-auto text-xs tracking-widest text-zinc-500 dark:text-zinc-400",
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
CommandShortcut.displayName = "CommandShortcut"
|
||||
|
||||
export {
|
||||
Command,
|
||||
|
|
|
|||
|
|
@ -1,120 +1,143 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
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
|
||||
)}
|
||||
{...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,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DialogFooter = ({
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-zinc-500 dark:text-zinc-400", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,21 +2,20 @@ import * as React from "react"
|
|||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
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",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
|
|
|
|||
|
|
@ -1,24 +1,22 @@
|
|||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
|
|
|
|||
|
|
@ -1,234 +1,274 @@
|
|||
import * as React from "react"
|
||||
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const MenubarMenu = MenubarPrimitive.Menu
|
||||
|
||||
const MenubarGroup = MenubarPrimitive.Group
|
||||
|
||||
const MenubarPortal = MenubarPrimitive.Portal
|
||||
|
||||
const MenubarSub = MenubarPrimitive.Sub
|
||||
|
||||
const MenubarRadioGroup = MenubarPrimitive.RadioGroup
|
||||
|
||||
const Menubar = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<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 = ({
|
||||
function Menubar({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
|
||||
return (
|
||||
<span
|
||||
<MenubarPrimitive.Root
|
||||
data-slot="menubar"
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
MenubarShortcut.displayname = "MenubarShortcut"
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarPortal,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarGroup,
|
||||
MenubarSeparator,
|
||||
MenubarLabel,
|
||||
MenubarItem,
|
||||
MenubarShortcut,
|
||||
MenubarCheckboxItem,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarPortal,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
MenubarGroup,
|
||||
MenubarSub,
|
||||
MenubarShortcut,
|
||||
MenubarSubTrigger,
|
||||
MenubarSubContent,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,27 +3,44 @@ import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
}
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
}
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border border-zinc-200 bg-white p-4 text-zinc-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
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 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</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 }
|
||||
|
|
|
|||
|
|
@ -1,46 +1,58 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-zinc-200 dark:bg-zinc-800" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
|
|
|
|||
|
|
@ -1,158 +1,183 @@
|
|||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 dark:border-zinc-800 dark:bg-zinc-950 dark:ring-offset-zinc-950 dark:placeholder:text-zinc-400 dark:focus:ring-zinc-300",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{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}
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
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",
|
||||
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",
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
{children}
|
||||
<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(
|
||||
"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" &&
|
||||
"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}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-zinc-800 dark:focus:text-zinc-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: 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<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-zinc-100 dark:bg-zinc-800", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,28 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-zinc-200 dark:bg-zinc-800",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
|
|
|
|||
|
|
@ -1,26 +1,61 @@
|
|||
import * as React from "react";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none select-none items-center",
|
||||
"data-[orientation='vertical']:h-full data-[orientation='vertical']:w-2 data-[orientation='vertical']:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800">
|
||||
<SliderPrimitive.Range className={cn("absolute h-full bg-zinc-900 dark:bg-zinc-50", "data-[orientation='vertical']:w-full")} />
|
||||
</SliderPrimitive.Track>
|
||||
<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" />
|
||||
</SliderPrimitive.Root>
|
||||
)
|
||||
);
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
function Slider({
|
||||
className,
|
||||
defaultValue,
|
||||
value,
|
||||
min = 0,
|
||||
max = 100,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||
const _values = React.useMemo(
|
||||
() =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [min, max],
|
||||
[value, defaultValue, min, max]
|
||||
)
|
||||
|
||||
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 }
|
||||
|
|
|
|||
|
|
@ -3,51 +3,62 @@ import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-zinc-100 p-1 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-zinc-950 data-[state=active]:shadow-sm dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300 dark:data-[state=active]:bg-zinc-950 dark:data-[state=active]:text-zinc-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
|
|
|
|||
|
|
@ -12,39 +12,53 @@ const ToggleGroupContext = React.createContext<
|
|||
variant: "default",
|
||||
})
|
||||
|
||||
const ToggleGroup = React.forwardRef<
|
||||
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, children, ...props }, ref) => (
|
||||
<ToggleGroupPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("flex items-center justify-center gap-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
))
|
||||
function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<ToggleGroupPrimitive.Root
|
||||
data-slot="toggle-group"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
|
||||
|
||||
const ToggleGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, children, variant, size, ...props }, ref) => {
|
||||
function ToggleGroupItem({
|
||||
className,
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
const context = React.useContext(ToggleGroupContext)
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
ref={ref}
|
||||
data-slot="toggle-group-item"
|
||||
data-variant={context.variant || variant}
|
||||
data-size={context.size || size}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -52,8 +66,6 @@ const ToggleGroupItem = React.forwardRef<
|
|||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
)
|
||||
})
|
||||
|
||||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
|
||||
}
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
|
@ -5,18 +7,18 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||
import { cn } from "@/lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-white transition-colors hover:bg-zinc-100 hover:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-zinc-100 data-[state=on]:text-zinc-900 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2 dark:ring-offset-zinc-950 dark:hover:bg-zinc-800 dark:hover:text-zinc-400 dark:focus-visible:ring-zinc-300 dark:data-[state=on]:bg-zinc-800 dark:data-[state=on]:text-zinc-50",
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-zinc-200 bg-transparent hover:bg-zinc-100 hover:text-zinc-900 dark:border-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-50",
|
||||
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-3 min-w-10",
|
||||
sm: "h-9 px-2.5 min-w-9",
|
||||
lg: "h-11 px-5 min-w-11",
|
||||
default: "h-9 px-2 min-w-9",
|
||||
sm: "h-8 px-1.5 min-w-8",
|
||||
lg: "h-10 px-2.5 min-w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
@ -26,18 +28,20 @@ const toggleVariants = cva(
|
|||
}
|
||||
)
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName
|
||||
function Toggle({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive.Root
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
|
|
|
|||
|
|
@ -3,26 +3,57 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
|||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border border-zinc-200 bg-white px-3 py-1.5 text-sm text-zinc-950 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
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
|
||||
)}
|
||||
{...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 }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { createContext, ReactNode, useContext, useEffect, useMemo, useState } from "react";
|
||||
import React, { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { HistoryContext } from "./History";
|
||||
import welcomeBlocksData from "@/data/welcome.json";
|
||||
|
|
@ -22,6 +22,7 @@ interface Props {
|
|||
children: ReactNode;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const CanvasContext = createContext<Context>({} as Context);
|
||||
|
||||
export const CanvasProvider = ({ children }: Props) => {
|
||||
|
|
@ -31,7 +32,7 @@ export const CanvasProvider = ({ children }: Props) => {
|
|||
const [blocks, setBlocks] = useState<Block[]>(welcomeBlocksData);
|
||||
const [coords, setCoords] = useState<Position>({ x: 0, y: 0 });
|
||||
const [scale, setScale] = useState(1);
|
||||
const [version, setVersion] = useState(1214);
|
||||
const [version, setVersion] = useState(1219);
|
||||
|
||||
// Get the farthest away blocks in each direction
|
||||
const canvasSize = useMemo(() => {
|
||||
|
|
@ -64,7 +65,7 @@ export const CanvasProvider = ({ children }: Props) => {
|
|||
};
|
||||
}, [blocks]);
|
||||
|
||||
const centerCanvas = () => {
|
||||
const centerCanvas = useCallback(() => {
|
||||
// Margin of 8 blocks on each side
|
||||
const margin = 8 * 16;
|
||||
|
||||
|
|
@ -85,7 +86,7 @@ export const CanvasProvider = ({ children }: Props) => {
|
|||
|
||||
setScale(newScale);
|
||||
setCoords({ x: newX, y: newY });
|
||||
};
|
||||
}, [canvasSize, stageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
addHistory(
|
||||
|
|
@ -93,6 +94,7 @@ export const CanvasProvider = ({ children }: Props) => {
|
|||
() => setBlocks(welcomeBlocksData),
|
||||
() => setBlocks([])
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ interface Props {
|
|||
children: ReactNode;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const DialogContext = createContext<Context>({} as Context);
|
||||
|
||||
export const DialogProvider = ({ children }: Props) => {
|
||||
|
|
@ -34,7 +35,7 @@ export const DialogProvider = ({ children }: Props) => {
|
|||
<DialogContext.Provider value={openDialog}>
|
||||
<Dialog open={open} onOpenChange={(value) => setOpen(value)}>
|
||||
{LazyDialogContent && (
|
||||
<Suspense fallback={<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">Loading dialog...</div>}>
|
||||
<Suspense>
|
||||
<LazyDialogContent close={() => setOpen(false)} registerSubmit={(fn) => (onSubmitRef.current = fn)} dialogKeyHandler={dialogKeyHandler} />
|
||||
</Suspense>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ interface Props {
|
|||
children: ReactNode;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const HistoryContext = createContext<Context>({} as Context);
|
||||
|
||||
export const HistoryProvider = ({ children }: Props) => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ interface Props {
|
|||
children: ReactNode;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const LoadingContext = createContext<Context>({} as Context);
|
||||
|
||||
export const LoadingProvider = ({ children }: Props) => {
|
||||
|
|
@ -18,10 +19,10 @@ export const LoadingProvider = ({ children }: Props) => {
|
|||
return (
|
||||
<LoadingContext.Provider value={{ loading, setLoading }}>
|
||||
{loading && (
|
||||
<div className="absolute w-full h-full cursor-wait flex justify-center items-center">
|
||||
<div className="absolute size-full cursor-wait flex justify-center items-center">
|
||||
{/* Keep loading indicator outside of div with backdrop-filter due to Chrome */}
|
||||
<LoadingIndicator fill="white" className="w-16 h-16 z-[10000]" />
|
||||
<div className="absolute w-full h-full z-[9999] backdrop-brightness-50 flex justify-center items-center gap-4"></div>
|
||||
<LoadingIndicator fill="white" className="w-16 h-16 z-10000" />
|
||||
<div className="absolute size-full z-9999 backdrop-brightness-50 flex justify-center items-center gap-4"></div>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import React, { createContext, ReactNode, useRef, useState } from "react";
|
|||
interface Context {
|
||||
selectionCoords: CoordinateArray;
|
||||
selectionLayerBlocks: Block[];
|
||||
confirmHistoryEntryNameRef: React.MutableRefObject<string>;
|
||||
confirmHistoryEntryNameRef: React.RefObject<string>;
|
||||
setSelectionCoords: React.Dispatch<React.SetStateAction<CoordinateArray>>;
|
||||
setSelectionLayerBlocks: React.Dispatch<React.SetStateAction<Block[]>>;
|
||||
isInSelection: (x: number, y: number) => boolean;
|
||||
|
|
@ -13,6 +13,7 @@ interface Props {
|
|||
children: ReactNode;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const SelectionContext = createContext<Context>({} as Context);
|
||||
|
||||
export const SelectionProvider = ({ children }: Props) => {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ const defaultSettings: Settings = {
|
|||
blockSelector: true,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const SettingsContext = createContext<Context>({} as Context);
|
||||
|
||||
export const SettingsProvider = ({ children }: Props) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { createContext, ReactNode, useContext, useEffect, useRef } from "react";
|
||||
import * as PIXI from "pixi.js";
|
||||
import { Assets, Spritesheet, Texture } from "pixi.js";
|
||||
|
||||
import { LoadingContext } from "./Loading";
|
||||
|
||||
|
|
@ -7,48 +7,57 @@ import spritesheet from "@/data/blocks/spritesheet.json";
|
|||
import programmerArtSpritesheet from "@/data/blocks/programmer-art/spritesheet.json";
|
||||
|
||||
interface Context {
|
||||
missingTexture: PIXI.Texture;
|
||||
textures: Record<string, PIXI.Texture>;
|
||||
programmerArtTextures: Record<string, PIXI.Texture>;
|
||||
missingTexture: Texture;
|
||||
textures: Record<string, Texture>;
|
||||
programmerArtTextures: Record<string, Texture>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const TexturesContext = createContext<Context>({} as Context);
|
||||
|
||||
export const TexturesProvider = ({ children }: Props) => {
|
||||
const { setLoading } = useContext(LoadingContext);
|
||||
|
||||
// Load missing texture through data string just incase of network errors
|
||||
const missingTextureRef = useRef<PIXI.Texture>(
|
||||
new PIXI.Texture(
|
||||
new PIXI.BaseTexture(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAGUlEQVR42mPABX4w/MCKaKJhVMPgcOuoBgDZRfgBVl5QdQAAAABJRU5ErkJggg=="
|
||||
)
|
||||
)
|
||||
);
|
||||
const texturesRef = useRef<Record<string, PIXI.Texture>>({});
|
||||
const programmerArtTexturesRef = useRef<Record<string, PIXI.Texture>>({});
|
||||
// Load missing texture through data string just in case of network errors
|
||||
const missingTextureRef = useRef<Texture>(Texture.EMPTY);
|
||||
const texturesRef = useRef<Record<string, Texture>>({});
|
||||
const programmerArtTexturesRef = useRef<Record<string, Texture>>({});
|
||||
|
||||
// Load textures
|
||||
useEffect(() => {
|
||||
// Add air texture
|
||||
const airBaseTexture = new PIXI.BaseTexture("/blocks/air.png");
|
||||
const airTexture = new PIXI.Texture(airBaseTexture);
|
||||
const loadTextures = async () => {
|
||||
try {
|
||||
// Load base textures
|
||||
missingTextureRef.current = await Assets.load(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAGUlEQVR42mPABX4w/MCKaKJhVMPgcOuoBgDZRfgBVl5QdQAAAABJRU5ErkJggg=="
|
||||
);
|
||||
const airBaseTexture = await Assets.load("/blocks/air.png");
|
||||
const sheetTexture = await Assets.load("/blocks/spritesheet.png");
|
||||
const programmerArtSheetTexture = await Assets.load("/blocks/programmer-art/spritesheet.png");
|
||||
|
||||
const sheet = new PIXI.Spritesheet(PIXI.BaseTexture.from("/blocks/spritesheet.png"), spritesheet);
|
||||
sheet.parse().then((t) => {
|
||||
texturesRef.current = { ...t, "air.png": airTexture };
|
||||
});
|
||||
// Create and parse main spritesheet
|
||||
const sheet = new Spritesheet(sheetTexture, spritesheet);
|
||||
await sheet.parse();
|
||||
texturesRef.current = { ...sheet.textures, "air.png": airBaseTexture };
|
||||
|
||||
const programmerArtSheet = new PIXI.Spritesheet(PIXI.BaseTexture.from("/blocks/programmer-art/spritesheet.png"), programmerArtSpritesheet);
|
||||
programmerArtSheet.parse().then((t) => {
|
||||
programmerArtTexturesRef.current = { ...t, "air.png": airTexture };
|
||||
});
|
||||
setLoading(false);
|
||||
}, []);
|
||||
// Create and parse programmer art spritesheet
|
||||
const programmerArtSheet = new Spritesheet(programmerArtSheetTexture, programmerArtSpritesheet);
|
||||
await programmerArtSheet.parse();
|
||||
programmerArtTexturesRef.current = { ...programmerArtSheet.textures, "air.png": airBaseTexture };
|
||||
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to load textures:", error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadTextures();
|
||||
}, [setLoading]);
|
||||
|
||||
return (
|
||||
<TexturesContext.Provider
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ interface Context {
|
|||
setTheme: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const ThemeContext = createContext<Context>({} as Context);
|
||||
|
||||
export function ThemeProvider({ children, defaultTheme = "system", storageKey = "vite-ui-theme", ...props }: Props) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ interface Props {
|
|||
children: ReactNode;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const ToolContext = createContext<Context>({} as Context);
|
||||
|
||||
export const ToolProvider = ({ children }: Props) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"1219": 4554,
|
||||
"1214": 4189,
|
||||
"1210": 3953,
|
||||
"1200": 3463,
|
||||
|
|
|
|||
19
src/hooks/use-mobile.ts
Normal file
19
src/hooks/use-mobile.ts
Normal 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
|
||||
}
|
||||
209
src/index.css
209
src/index.css
|
|
@ -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 "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
font-family: Geist, Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
@theme {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
--radius-lg: var(--radius);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
|
||||
--color-blockmatic-green: #adc178;
|
||||
--color-blockmatic-brown: #a98467;
|
||||
|
||||
--font-inter: Inter, sans-serif;
|
||||
}
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
:root {
|
||||
font-family: Geist, Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
|
@ -30,3 +69,155 @@ body {
|
|||
.info-child {
|
||||
@apply bg-zinc-50 dark:bg-zinc-900 px-2 py-1 rounded shadow-xl w-fit border border-zinc-200 dark:border-zinc-800;
|
||||
}
|
||||
|
||||
/*
|
||||
---break---
|
||||
*/
|
||||
|
||||
:root {
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.015 286.067);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
}
|
||||
|
||||
/*
|
||||
---break---
|
||||
*/
|
||||
|
||||
.dark {
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.92 0.004 286.32);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
}
|
||||
|
||||
/*
|
||||
---break---
|
||||
*/
|
||||
|
||||
@theme inline {
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
/*
|
||||
---break---
|
||||
*/
|
||||
@keyframes accordion-down {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
}
|
||||
/*
|
||||
---break---
|
||||
*/
|
||||
@keyframes accordion-up {
|
||||
from {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@ import PrivacyPolicy from "./pages/PrivacyPolicy.tsx";
|
|||
import NotFound from "./pages/NotFound.tsx";
|
||||
|
||||
import "./index.css";
|
||||
import { extend } from "@pixi/react";
|
||||
import { Container, Graphics, Sprite } from "pixi.js";
|
||||
|
||||
extend({
|
||||
Container,
|
||||
Sprite,
|
||||
Graphics,
|
||||
});
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
|
|
|
|||
|
|
@ -29,10 +29,10 @@ function AppPage() {
|
|||
<MobileNotice />
|
||||
|
||||
<main
|
||||
className={`overflow-y-hidden h-[100svh] grid ${
|
||||
className={`overflow-y-hidden h-svh grid ${
|
||||
isMobileView
|
||||
? "grid-rows-[2.5rem_minmax(0,1fr)_auto] grid-cols-[2.5rem_minmax(0,1fr)]"
|
||||
: "grid-rows-[2.5rem_minmax(0,1fr)] grid-cols-[2.5rem_minmax(0,1fr)_auto]"
|
||||
? "grid-rows-[2.3rem_minmax(0,1fr)_auto] grid-cols-[2.3rem_minmax(0,1fr)]"
|
||||
: "grid-rows-[2.3rem_minmax(0,1fr)] grid-cols-[2.3rem_minmax(0,1fr)_auto]"
|
||||
}`}
|
||||
>
|
||||
<Menubar />
|
||||
|
|
|
|||
|
|
@ -56,9 +56,9 @@ function IndexPage() {
|
|||
</span>
|
||||
</h5>
|
||||
<Button className="w-min h-11 mt-4" asChild>
|
||||
<Link to={{ pathname: "/app" }} className="!text-base">
|
||||
<Link to={{ pathname: "/app" }} className="text-base!">
|
||||
Go to Editor
|
||||
<ChevronRightIcon className="!h-6 !w-6" />
|
||||
<ChevronRightIcon className="size-6!" />
|
||||
</Link>
|
||||
</Button>
|
||||
</section>
|
||||
|
|
@ -67,14 +67,14 @@ function IndexPage() {
|
|||
<img
|
||||
src={isDark ? "/home/blockmatic_screenshot_dark.png" : "/home/blockmatic_screenshot_light.png"}
|
||||
alt="app preview"
|
||||
className="max-w-[65rem] w-full rounded-xl border border-zinc-700"
|
||||
className="max-w-260 w-full rounded-xl border border-zinc-700"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col items-center mt-32 text-center mx-8">
|
||||
<h1 className="text-5xl font-bold mb-2">Pixel art made easy</h1>
|
||||
<p className="mb-8 text-lg">Blockmatic makes it easier to make changes and build by using schematics and the web editor.</p>
|
||||
<div className="max-w-[56rem] grid grid-cols-3 max-md:grid-cols-2 gap-4 *:flex *:flex-col *:items-center *:gap-1 *:text-center *:p-4 *:border *:border-zinc-300 *:dark:border-zinc-800 *:rounded-lg *:bg-zinc-100 *:dark:bg-zinc-900 *:text-black *:dark:text-white">
|
||||
<div className="max-w-4xl grid grid-cols-3 max-md:grid-cols-2 gap-4 *:flex *:flex-col *:items-center *:gap-1 *:text-center *:p-4 *:border *:border-zinc-300 dark:*:border-zinc-800 *:rounded-lg *:bg-zinc-100 dark:*:bg-zinc-900 *:text-black dark:*:text-white">
|
||||
<div>
|
||||
<img
|
||||
src="/home/shinji.png"
|
||||
|
|
@ -143,8 +143,8 @@ function IndexPage() {
|
|||
<p className="mb-8 text-lg text-center">Blockmatic is free and open source. Host it yourself, modify it, or contribute to its development.</p>
|
||||
|
||||
<Button className="w-min h-11" variant="outline" asChild>
|
||||
<a href="https://github.com/trafficlunar/blockmatic" className="!text-base">
|
||||
<LinkIcon className="!h-5 !w-5" />
|
||||
<a href="https://github.com/trafficlunar/blockmatic" className="text-base!">
|
||||
<LinkIcon className="size-5!" />
|
||||
Source code
|
||||
</a>
|
||||
</Button>
|
||||
|
|
@ -153,9 +153,9 @@ function IndexPage() {
|
|||
<Separator className="max-w-xl mt-20 mb-8" />
|
||||
|
||||
<Button className="w-min h-11" asChild>
|
||||
<Link to={{ pathname: "/app" }} className="!text-base">
|
||||
<Link to={{ pathname: "/app" }} className="text-base!">
|
||||
Go to Editor
|
||||
<ChevronRightIcon className="!h-6 !w-6" />
|
||||
<ChevronRightIcon className="size-6!" />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ function NotFound() {
|
|||
const { isDark } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<div className="absolute w-full h-full flex flex-col justify-center items-center">
|
||||
<div className="absolute size-full flex flex-col justify-center items-center">
|
||||
<div className="flex gap-2 items-center">
|
||||
<BlockmaticLogo className="h-8" fill={isDark ? "white" : "black"} />
|
||||
<BlockmaticText className="h-3.5" fill={isDark ? "white" : "black"} />
|
||||
|
|
@ -24,7 +24,7 @@ function NotFound() {
|
|||
<Button variant="outline" asChild>
|
||||
<Link to={{ pathname: "/" }}>
|
||||
Go back
|
||||
<ChevronLeftIcon className="!h-6 !w-6" />
|
||||
<ChevronLeftIcon className="size-6!" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
};
|
||||
Loading…
Reference in a new issue