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

Also:

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

View file

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

View file

@ -7,6 +7,7 @@ interface Props {
children: ReactNode;
}
// eslint-disable-next-line react-refresh/only-export-components
export const DialogContext = createContext<Context>({} as Context);
export const DialogProvider = ({ children }: Props) => {
@ -34,7 +35,7 @@ export const DialogProvider = ({ children }: Props) => {
<DialogContext.Provider value={openDialog}>
<Dialog open={open} onOpenChange={(value) => setOpen(value)}>
{LazyDialogContent && (
<Suspense fallback={<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">Loading dialog...</div>}>
<Suspense>
<LazyDialogContent close={() => setOpen(false)} registerSubmit={(fn) => (onSubmitRef.current = fn)} dialogKeyHandler={dialogKeyHandler} />
</Suspense>
)}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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