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:
@@ -527,8 +527,8 @@ export function CombatantRow({
|
||||
</button>
|
||||
|
||||
{/* Initiative */}
|
||||
{/* biome-ignore lint/a11y/noStaticElementInteractions: stopPropagation wrapper for interactive children */}
|
||||
{/* biome-ignore lint/a11y/noNoninteractiveElementInteractions: stopPropagation wrapper for interactive children */}
|
||||
{/* biome-ignore lint/a11y/noStaticElementInteractions: stopPropagation wrapper — no semantic role fits */}
|
||||
{/* biome-ignore lint/a11y/noNoninteractiveElementInteractions: stopPropagation wrapper — no semantic role fits */}
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
@@ -587,8 +587,8 @@ export function CombatantRow({
|
||||
</div>
|
||||
|
||||
{/* AC */}
|
||||
{/* biome-ignore lint/a11y/noStaticElementInteractions: stopPropagation wrapper for interactive children */}
|
||||
{/* biome-ignore lint/a11y/noNoninteractiveElementInteractions: stopPropagation wrapper for interactive children */}
|
||||
{/* biome-ignore lint/a11y/noStaticElementInteractions: stopPropagation wrapper — no semantic role fits */}
|
||||
{/* biome-ignore lint/a11y/noNoninteractiveElementInteractions: stopPropagation wrapper — no semantic role fits */}
|
||||
<div
|
||||
className={cn(dimmed && "opacity-50")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -598,8 +598,8 @@ export function CombatantRow({
|
||||
</div>
|
||||
|
||||
{/* HP */}
|
||||
{/* biome-ignore lint/a11y/noStaticElementInteractions: stopPropagation wrapper for interactive children */}
|
||||
{/* biome-ignore lint/a11y/noNoninteractiveElementInteractions: stopPropagation wrapper for interactive children */}
|
||||
{/* biome-ignore lint/a11y/noStaticElementInteractions: stopPropagation wrapper — no semantic role fits */}
|
||||
{/* biome-ignore lint/a11y/noNoninteractiveElementInteractions: stopPropagation wrapper — no semantic role fits */}
|
||||
<div
|
||||
className="flex items-center gap-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PlayerCharacter, PlayerCharacterId } from "@initiative/domain";
|
||||
import { Pencil, Plus, Trash2, X } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { PLAYER_COLOR_HEX, PLAYER_ICON_MAP } from "./player-icon-map";
|
||||
import { Button } from "./ui/button";
|
||||
import { ConfirmButton } from "./ui/confirm-button";
|
||||
@@ -22,102 +22,112 @@ export function PlayerManagement({
|
||||
onDelete,
|
||||
onCreate,
|
||||
}: Readonly<PlayerManagementProps>) {
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") onClose();
|
||||
}
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [open, onClose]);
|
||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||
|
||||
if (!open) return null;
|
||||
useEffect(() => {
|
||||
const dialog = dialogRef.current;
|
||||
if (!dialog) return;
|
||||
if (open && !dialog.open) {
|
||||
dialog.showModal();
|
||||
} else if (!open && dialog.open) {
|
||||
dialog.close();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
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]);
|
||||
|
||||
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">
|
||||
Player Characters
|
||||
</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<X size={20} />
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="font-semibold text-foreground text-lg">
|
||||
Player Characters
|
||||
</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<X size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{characters.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-3 py-8 text-center">
|
||||
<p className="text-muted-foreground">No player characters yet</p>
|
||||
<Button onClick={onCreate}>
|
||||
<Plus size={16} />
|
||||
Create your first player character
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{characters.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-3 py-8 text-center">
|
||||
<p className="text-muted-foreground">No player characters yet</p>
|
||||
<Button onClick={onCreate}>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
{characters.map((pc) => {
|
||||
const Icon = pc.icon ? PLAYER_ICON_MAP[pc.icon] : undefined;
|
||||
const color = pc.color ? PLAYER_COLOR_HEX[pc.color] : undefined;
|
||||
return (
|
||||
<div
|
||||
key={pc.id}
|
||||
className="group flex items-center gap-3 rounded-md px-3 py-2 hover:bg-hover-neutral-bg"
|
||||
>
|
||||
{!!Icon && (
|
||||
<Icon size={18} style={{ color }} className="shrink-0" />
|
||||
)}
|
||||
<span className="flex-1 truncate text-foreground text-sm">
|
||||
{pc.name}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs tabular-nums">
|
||||
AC {pc.ac}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs tabular-nums">
|
||||
HP {pc.maxHp}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => onEdit(pc)}
|
||||
className="text-muted-foreground"
|
||||
title="Edit"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</Button>
|
||||
<ConfirmButton
|
||||
icon={<Trash2 size={14} />}
|
||||
label="Delete player character"
|
||||
onConfirm={() => onDelete(pc.id)}
|
||||
size="icon-sm"
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="mt-2 flex justify-end">
|
||||
<Button onClick={onCreate} variant="ghost">
|
||||
<Plus size={16} />
|
||||
Create your first player character
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
{characters.map((pc) => {
|
||||
const Icon = pc.icon ? PLAYER_ICON_MAP[pc.icon] : undefined;
|
||||
const color = pc.color ? PLAYER_COLOR_HEX[pc.color] : undefined;
|
||||
return (
|
||||
<div
|
||||
key={pc.id}
|
||||
className="group flex items-center gap-3 rounded-md px-3 py-2 hover:bg-hover-neutral-bg"
|
||||
>
|
||||
{!!Icon && (
|
||||
<Icon size={18} style={{ color }} className="shrink-0" />
|
||||
)}
|
||||
<span className="flex-1 truncate text-foreground text-sm">
|
||||
{pc.name}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs tabular-nums">
|
||||
AC {pc.ac}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs tabular-nums">
|
||||
HP {pc.maxHp}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => onEdit(pc)}
|
||||
className="text-muted-foreground"
|
||||
title="Edit"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</Button>
|
||||
<ConfirmButton
|
||||
icon={<Trash2 size={14} />}
|
||||
label="Delete player character"
|
||||
onConfirm={() => onDelete(pc.id)}
|
||||
size="icon-sm"
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="mt-2 flex justify-end">
|
||||
<Button onClick={onCreate} variant="ghost">
|
||||
<Plus size={16} />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user