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