normalize biome config (lineWidth 80), rename mise.toml → .mise.toml, add CLAUDE.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 07:22:31 +01:00
parent e6fb09d4da
commit ef50a77b07
12 changed files with 97 additions and 28 deletions

4
.gitignore vendored
View File

@@ -2,3 +2,7 @@ dist/
node_modules/ node_modules/
.DS_Store .DS_Store
*.local *.local
.mise.local.toml
.env
.env.local
.env.*.local

34
CLAUDE.md Normal file
View File

@@ -0,0 +1,34 @@
# impstr — Imposter Party Game
Static PWA party game (like Werewolf/Mafia). No backend, no accounts — runs entirely in the browser.
## Stack
- **Frontend:** React 19, Vite, Tailwind CSS 4, TanStack Router, Zustand
- **Linting:** Biome (tabs, 80 chars, double quotes)
## Project Structure
```
src/
├── features/ ← game logic, UI components
├── routes/ ← TanStack Router file-based routes
└── shared/ ← shared components, utilities
```
## Local Development
```bash
bun install
bun run dev
```
## Deployment
```bash
./deploy.sh
```
Deploys to Uberspace (`serve.uber.space`):
- Static files → `/var/www/virtual/serve/html/impstr/`
- No backend, no database

View File

@@ -5,7 +5,7 @@
}, },
"formatter": { "formatter": {
"indentStyle": "tab", "indentStyle": "tab",
"lineWidth": 100 "lineWidth": 80
}, },
"linter": { "linter": {
"enabled": true, "enabled": true,

View File

@@ -24,7 +24,11 @@ export function FlipCard({ word, flipped, onFlip }: FlipCardProps) {
return ( return (
<div <div
className={cn("flip-card", flipped && "flipped", instant && "flip-card--instant")} className={cn(
"flip-card",
flipped && "flipped",
instant && "flip-card--instant",
)}
onClick={onFlip} onClick={onFlip}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") onFlip(); if (e.key === "Enter" || e.key === " ") onFlip();

View File

@@ -24,7 +24,9 @@ describe("SetupScreen", () => {
it("renders the setup screen", () => { it("renders the setup screen", () => {
render(<SetupScreen />); render(<SetupScreen />);
expect(screen.getByText("Imposter")).toBeInTheDocument(); expect(screen.getByText("Imposter")).toBeInTheDocument();
expect(screen.getByPlaceholderText("Spieler hinzufügen…")).toBeInTheDocument(); expect(
screen.getByPlaceholderText("Spieler hinzufügen…"),
).toBeInTheDocument();
}); });
it("adds a player via button click", () => { it("adds a player via button click", () => {

View File

@@ -39,7 +39,9 @@ export function SetupScreen() {
<header className="flex flex-col gap-3"> <header className="flex flex-col gap-3">
<Badge className="self-center">Partyspiel</Badge> <Badge className="self-center">Partyspiel</Badge>
<h1>Imposter</h1> <h1>Imposter</h1>
<p className="text-[#5b5b5b]">Findet heraus, wer das geheime Wort nicht kennt!</p> <p className="text-[#5b5b5b]">
Findet heraus, wer das geheime Wort nicht kennt!
</p>
</header> </header>
<div className="rounded-3xl bg-surface p-6 shadow-[0_2px_16px_rgba(0,0,0,0.06)] animate-fade-in"> <div className="rounded-3xl bg-surface p-6 shadow-[0_2px_16px_rgba(0,0,0,0.06)] animate-fade-in">
@@ -83,14 +85,21 @@ export function SetupScreen() {
Alle löschen Alle löschen
</Button> </Button>
)} )}
<Button variant="primary" onClick={startRound} disabled={!canStart} className="flex-1"> <Button
variant="primary"
onClick={startRound}
disabled={!canStart}
className="flex-1"
>
Spiel starten Spiel starten
</Button> </Button>
</div> </div>
</div> </div>
{!canStart && players.length > 0 && ( {!canStart && players.length > 0 && (
<p className="text-sm text-[#5b5b5b]">Mindestens {MIN_PLAYERS} Spieler benötigt</p> <p className="text-sm text-[#5b5b5b]">
Mindestens {MIN_PLAYERS} Spieler benötigt
</p>
)} )}
</div> </div>
); );

View File

@@ -1,7 +1,10 @@
import { cn } from "@/shared/lib/utils"; import { cn } from "@/shared/lib/utils";
import type { HTMLAttributes } from "react"; import type { HTMLAttributes } from "react";
export function Badge({ className, ...props }: HTMLAttributes<HTMLSpanElement>) { export function Badge({
className,
...props
}: HTMLAttributes<HTMLSpanElement>) {
return ( return (
<span <span
className={cn( className={cn(

View File

@@ -1,21 +1,22 @@
import { cn } from "@/shared/lib/utils"; import { cn } from "@/shared/lib/utils";
import { type InputHTMLAttributes, forwardRef } from "react"; import { type InputHTMLAttributes, forwardRef } from "react";
export const Input = forwardRef<HTMLInputElement, InputHTMLAttributes<HTMLInputElement>>( export const Input = forwardRef<
({ className, ...props }, ref) => { HTMLInputElement,
return ( InputHTMLAttributes<HTMLInputElement>
<input >(({ className, ...props }, ref) => {
ref={ref} return (
className={cn( <input
"w-full rounded-2xl border-2 border-[rgba(26,26,26,0.08)] bg-surface px-5 py-3.5", ref={ref}
"font-body text-base text-[#1a1a1a] placeholder:text-[#5b5b5b]", className={cn(
"outline-none transition-colors focus:border-primary", "w-full rounded-2xl border-2 border-[rgba(26,26,26,0.08)] bg-surface px-5 py-3.5",
className, "font-body text-base text-[#1a1a1a] placeholder:text-[#5b5b5b]",
)} "outline-none transition-colors focus:border-primary",
{...props} className,
/> )}
); {...props}
}, />
); );
});
Input.displayName = "Input"; Input.displayName = "Input";

View File

@@ -27,7 +27,9 @@ describe("game-store", () => {
it("adds a player and persists to localStorage", () => { it("adds a player and persists to localStorage", () => {
getState().addPlayer("Alice"); getState().addPlayer("Alice");
expect(getState().players).toEqual(["Alice"]); expect(getState().players).toEqual(["Alice"]);
expect(JSON.parse(localStorage.getItem("impstr-players") ?? "[]")).toEqual(["Alice"]); expect(
JSON.parse(localStorage.getItem("impstr-players") ?? "[]"),
).toEqual(["Alice"]);
}); });
it("trims whitespace", () => { it("trims whitespace", () => {
@@ -58,7 +60,9 @@ describe("game-store", () => {
getState().addPlayer("Bob"); getState().addPlayer("Bob");
getState().clearPlayers(); getState().clearPlayers();
expect(getState().players).toEqual([]); expect(getState().players).toEqual([]);
expect(JSON.parse(localStorage.getItem("impstr-players") ?? "[]")).toEqual([]); expect(
JSON.parse(localStorage.getItem("impstr-players") ?? "[]"),
).toEqual([]);
}); });
}); });
@@ -76,7 +80,10 @@ describe("game-store", () => {
}); });
it("filters out non-string entries", () => { it("filters out non-string entries", () => {
localStorage.setItem("impstr-players", JSON.stringify(["Alice", 42, null, "Bob"])); localStorage.setItem(
"impstr-players",
JSON.stringify(["Alice", 42, null, "Bob"]),
);
getState().loadPlayers(); getState().loadPlayers();
expect(getState().players).toEqual(["Alice", "Bob"]); expect(getState().players).toEqual(["Alice", "Bob"]);
}); });

View File

@@ -93,7 +93,9 @@ export const useGameStore = create<GameState & GameActions>((set, get) => ({
if (raw) { if (raw) {
const parsed = JSON.parse(raw); const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) { if (Array.isArray(parsed)) {
set({ players: parsed.filter((p): p is string => typeof p === "string") }); set({
players: parsed.filter((p): p is string => typeof p === "string"),
});
} }
} }
} catch { } catch {

View File

@@ -20,4 +20,7 @@ const localStorageMock: Storage = {
key: (index: number) => [...store.keys()][index] ?? null, key: (index: number) => [...store.keys()][index] ?? null,
}; };
Object.defineProperty(globalThis, "localStorage", { value: localStorageMock, writable: true }); Object.defineProperty(globalThis, "localStorage", {
value: localStorageMock,
writable: true,
});