Add biome-ignore backpressure script, convert modals to native <dialog>
Adds scripts/check-lint-ignores.mjs with four enforcement mechanisms: ratcheting count cap (12 source / 3 test), banned rule prefixes, required justification, and separate test thresholds. Wired into pnpm check. Converts player-management and create-player-modal from div-based modals to native <dialog> with showModal()/close(), removing 8 biome-ignore comments. Remaining ignores are legitimate (Biome false positives or stopPropagation wrappers with no fitting role). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { PlayerCharacter } from "@initiative/domain";
|
||||
import { X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ColorPalette } from "./color-palette";
|
||||
import { IconGrid } from "./icon-grid";
|
||||
import { Button } from "./ui/button";
|
||||
@@ -25,6 +25,7 @@ export function CreatePlayerModal({
|
||||
onSave,
|
||||
playerCharacter,
|
||||
}: Readonly<CreatePlayerModalProps>) {
|
||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||
const [name, setName] = useState("");
|
||||
const [ac, setAc] = useState("10");
|
||||
const [maxHp, setMaxHp] = useState("10");
|
||||
@@ -54,15 +55,32 @@ export function CreatePlayerModal({
|
||||
}, [open, playerCharacter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") onClose();
|
||||
const dialog = dialogRef.current;
|
||||
if (!dialog) return;
|
||||
if (open && !dialog.open) {
|
||||
dialog.showModal();
|
||||
} else if (!open && dialog.open) {
|
||||
dialog.close();
|
||||
}
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [open, onClose]);
|
||||
}, [open]);
|
||||
|
||||
if (!open) return null;
|
||||
useEffect(() => {
|
||||
const dialog = dialogRef.current;
|
||||
if (!dialog) return;
|
||||
function handleCancel(e: Event) {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
}
|
||||
function handleBackdropClick(e: MouseEvent) {
|
||||
if (e.target === dialog) onClose();
|
||||
}
|
||||
dialog.addEventListener("cancel", handleCancel);
|
||||
dialog.addEventListener("mousedown", handleBackdropClick);
|
||||
return () => {
|
||||
dialog.removeEventListener("cancel", handleCancel);
|
||||
dialog.removeEventListener("mousedown", handleBackdropClick);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
@@ -86,106 +104,89 @@ export function CreatePlayerModal({
|
||||
};
|
||||
|
||||
return (
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: backdrop click to close
|
||||
// biome-ignore lint/a11y/noNoninteractiveElementInteractions: backdrop click to close
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||
onMouseDown={onClose}
|
||||
<dialog
|
||||
ref={dialogRef}
|
||||
className="w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-xl backdrop:bg-black/50"
|
||||
>
|
||||
{/* biome-ignore lint/a11y/noStaticElementInteractions: prevent close when clicking modal content */}
|
||||
{/* biome-ignore lint/a11y/noNoninteractiveElementInteractions: prevent close when clicking modal content */}
|
||||
<div
|
||||
className="w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-xl"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="font-semibold text-foreground text-lg">
|
||||
{isEdit ? "Edit Player" : "Create Player"}
|
||||
</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<X size={20} />
|
||||
</Button>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="font-semibold text-foreground text-lg">
|
||||
{isEdit ? "Edit Player" : "Create Player"}
|
||||
</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<X size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<div>
|
||||
<span className="mb-1 block text-muted-foreground text-sm">Name</span>
|
||||
<Input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
setError("");
|
||||
}}
|
||||
placeholder="Character name"
|
||||
aria-label="Name"
|
||||
autoFocus
|
||||
/>
|
||||
{!!error && <p className="mt-1 text-destructive text-sm">{error}</p>}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<div>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-1">
|
||||
<span className="mb-1 block text-muted-foreground text-sm">AC</span>
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={ac}
|
||||
onChange={(e) => setAc(e.target.value)}
|
||||
placeholder="AC"
|
||||
aria-label="AC"
|
||||
className="text-center"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<span className="mb-1 block text-muted-foreground text-sm">
|
||||
Name
|
||||
Max HP
|
||||
</span>
|
||||
<Input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
setError("");
|
||||
}}
|
||||
placeholder="Character name"
|
||||
aria-label="Name"
|
||||
autoFocus
|
||||
inputMode="numeric"
|
||||
value={maxHp}
|
||||
onChange={(e) => setMaxHp(e.target.value)}
|
||||
placeholder="Max HP"
|
||||
aria-label="Max HP"
|
||||
className="text-center"
|
||||
/>
|
||||
{!!error && (
|
||||
<p className="mt-1 text-destructive text-sm">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-1">
|
||||
<span className="mb-1 block text-muted-foreground text-sm">
|
||||
AC
|
||||
</span>
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={ac}
|
||||
onChange={(e) => setAc(e.target.value)}
|
||||
placeholder="AC"
|
||||
aria-label="AC"
|
||||
className="text-center"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<span className="mb-1 block text-muted-foreground text-sm">
|
||||
Max HP
|
||||
</span>
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={maxHp}
|
||||
onChange={(e) => setMaxHp(e.target.value)}
|
||||
placeholder="Max HP"
|
||||
aria-label="Max HP"
|
||||
className="text-center"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="mb-2 block text-muted-foreground text-sm">
|
||||
Color
|
||||
</span>
|
||||
<ColorPalette value={color} onChange={setColor} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="mb-2 block text-muted-foreground text-sm">
|
||||
Color
|
||||
</span>
|
||||
<ColorPalette value={color} onChange={setColor} />
|
||||
</div>
|
||||
<div>
|
||||
<span className="mb-2 block text-muted-foreground text-sm">Icon</span>
|
||||
<IconGrid value={icon} onChange={setIcon} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="mb-2 block text-muted-foreground text-sm">
|
||||
Icon
|
||||
</span>
|
||||
<IconGrid value={icon} onChange={setIcon} />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">{isEdit ? "Save" : "Create"}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">{isEdit ? "Save" : "Create"}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user