From 36768d3aa17b1c8113f47b1df8da1f019377af93 Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 14 Mar 2026 14:25:09 +0100 Subject: [PATCH] Upgrade Biome to 2.4.7 and enable 54 additional lint rules Add rules covering bug prevention (noLeakedRender, noFloatingPromises, noImportCycles, noReactForwardRef), security (noScriptUrl, noAlert), performance (noAwaitInLoops, useTopLevelRegex), and code style (noNestedTernary, useGlobalThis, useNullishCoalescing, useSortedClasses, plus ~40 more). Fix all violations: extract top-level regex constants, guard React && renders with boolean coercion, refactor nested ternaries, replace window with globalThis, sort Tailwind classes, and introduce expectDomainError test helper to eliminate conditional expects. Co-Authored-By: Claude Opus 4.6 --- apps/web/src/App.tsx | 23 ++-- .../web/src/__tests__/confirm-button.test.tsx | 1 + .../stat-block-collapse-pin.test.tsx | 9 +- apps/web/src/adapters/bestiary-adapter.ts | 4 +- apps/web/src/adapters/strip-tags.ts | 2 +- apps/web/src/components/ac-shield.tsx | 6 +- apps/web/src/components/action-bar.tsx | 35 +++--- .../web/src/components/bulk-import-prompt.tsx | 18 ++-- apps/web/src/components/color-palette.tsx | 2 +- apps/web/src/components/combatant-row.tsx | 64 +++++------ apps/web/src/components/condition-tags.tsx | 4 +- .../src/components/create-player-modal.tsx | 18 ++-- apps/web/src/components/hp-adjust-popover.tsx | 4 +- apps/web/src/components/icon-grid.tsx | 2 +- .../components/player-character-section.tsx | 18 ++-- apps/web/src/components/player-management.tsx | 12 ++- .../src/components/source-fetch-prompt.tsx | 15 +-- apps/web/src/components/source-manager.tsx | 12 +-- apps/web/src/components/stat-block-panel.tsx | 31 +++--- apps/web/src/components/stat-block.tsx | 20 ++-- apps/web/src/components/toast.tsx | 2 +- apps/web/src/components/turn-navigation.tsx | 4 +- apps/web/src/components/ui/confirm-button.tsx | 11 +- apps/web/src/components/ui/input.tsx | 32 +++--- apps/web/src/components/ui/overflow-menu.tsx | 6 +- apps/web/src/hooks/use-bestiary.ts | 2 +- apps/web/src/hooks/use-bulk-import.ts | 3 +- apps/web/src/hooks/use-encounter.ts | 8 +- apps/web/src/hooks/use-side-panel-state.ts | 4 +- biome.json | 102 ++++++++++++++++-- package.json | 2 +- .../src/__tests__/add-combatant.test.ts | 23 ++-- .../domain/src/__tests__/adjust-hp.test.ts | 21 +--- .../domain/src/__tests__/advance-turn.test.ts | 6 +- .../__tests__/create-player-character.test.ts | 46 ++------ .../__tests__/delete-player-character.test.ts | 6 +- .../src/__tests__/edit-combatant.test.ts | 21 +--- .../__tests__/edit-player-character.test.ts | 36 ++----- .../src/__tests__/remove-combatant.test.ts | 6 +- .../domain/src/__tests__/retreat-turn.test.ts | 11 +- .../src/__tests__/roll-initiative.test.ts | 11 +- packages/domain/src/__tests__/set-ac.test.ts | 16 +-- packages/domain/src/__tests__/set-hp.test.ts | 27 ++--- .../src/__tests__/set-initiative.test.ts | 11 +- packages/domain/src/__tests__/test-helpers.ts | 9 ++ .../__tests__/toggle-concentration.test.ts | 6 +- .../src/__tests__/toggle-condition.test.ts | 11 +- packages/domain/src/advance-turn.ts | 3 +- packages/domain/src/auto-number.ts | 1 + packages/domain/src/edit-player-character.ts | 18 ++-- packages/domain/src/set-ac.ts | 14 ++- packages/domain/src/set-hp.ts | 14 ++- packages/domain/src/set-initiative.ts | 2 +- pnpm-lock.yaml | 74 ++++++------- 54 files changed, 428 insertions(+), 441 deletions(-) create mode 100644 packages/domain/src/__tests__/test-helpers.ts diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 25ef71d..f1d5717 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -55,11 +55,10 @@ function useActionBarAnimation(combatantCount: number) { const empty = combatantCount === 0; const risingClass = rising ? " animate-rise-to-center" : ""; const settlingClass = settling ? " animate-settle-to-bottom" : ""; - const topBarClass = settling - ? " animate-slide-down-in" - : topBarExiting - ? " absolute inset-x-0 top-0 z-10 px-4 animate-slide-up-out" - : ""; + const exitingClass = topBarExiting + ? " absolute inset-x-0 top-0 z-10 px-4 animate-slide-up-out" + : ""; + const topBarClass = settling ? " animate-slide-down-in" : exitingClass; const showTopBar = !empty || topBarExiting; return { @@ -194,7 +193,7 @@ export function App() { block: "nearest", behavior: "smooth", }); - }, [encounter.activeIndex]); + }, []); // Auto-show stat block for the active combatant when turn changes, // but only when the viewport is wide enough to show it alongside the tracker. @@ -203,7 +202,7 @@ export function App() { useEffect(() => { if (prevActiveIndexRef.current === encounter.activeIndex) return; prevActiveIndexRef.current = encounter.activeIndex; - if (!window.matchMedia("(min-width: 1024px)").matches) return; + if (!globalThis.matchMedia("(min-width: 1024px)").matches) return; const active = encounter.combatants[encounter.activeIndex]; if (!active?.creatureId || !isLoaded) return; sidePanel.showCreature(active.creatureId as CreatureId); @@ -216,8 +215,8 @@ export function App() { return (
-
- {actionBarAnim.showTopBar && ( +
+ {!!actionBarAnim.showTopBar && (
+
{/* Scrollable area — combatant list */} -
+
{encounter.combatants.map((c, i) => ( {/* Pinned Stat Block Panel (left) */} - {sidePanel.pinnedCreatureId && sidePanel.isWideDesktop && ( + {!!sidePanel.pinnedCreatureId && sidePanel.isWideDesktop && ( { const parentHandler = vi.fn(); render( // biome-ignore lint/a11y/noStaticElementInteractions: test wrapper + // biome-ignore lint/a11y/noNoninteractiveElementInteractions: test wrapper
} diff --git a/apps/web/src/__tests__/stat-block-collapse-pin.test.tsx b/apps/web/src/__tests__/stat-block-collapse-pin.test.tsx index 5608abe..84311c7 100644 --- a/apps/web/src/__tests__/stat-block-collapse-pin.test.tsx +++ b/apps/web/src/__tests__/stat-block-collapse-pin.test.tsx @@ -6,6 +6,9 @@ import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { StatBlockPanel } from "../components/stat-block-panel"; +const CLOSE_REGEX = /close/i; +const COLLAPSE_REGEX = /collapse/i; + const CREATURE_ID = "srd:goblin" as CreatureId; const CREATURE: Creature = { id: CREATURE_ID, @@ -26,7 +29,7 @@ const CREATURE: Creature = { }; function mockMatchMedia(matches: boolean) { - Object.defineProperty(window, "matchMedia", { + Object.defineProperty(globalThis, "matchMedia", { writable: true, value: vi.fn().mockImplementation((query: string) => ({ matches, @@ -92,7 +95,7 @@ describe("Stat Block Panel Collapse/Expand and Pin", () => { screen.getByRole("button", { name: "Collapse stat block panel" }), ).toBeInTheDocument(); expect( - screen.queryByRole("button", { name: /close/i }), + screen.queryByRole("button", { name: CLOSE_REGEX }), ).not.toBeInTheDocument(); }); @@ -247,7 +250,7 @@ describe("Stat Block Panel Collapse/Expand and Pin", () => { it("pinned panel has no collapse button", () => { renderPanel({ panelRole: "pinned", side: "left" }); expect( - screen.queryByRole("button", { name: /collapse/i }), + screen.queryByRole("button", { name: COLLAPSE_REGEX }), ).not.toBeInTheDocument(); }); diff --git a/apps/web/src/adapters/bestiary-adapter.ts b/apps/web/src/adapters/bestiary-adapter.ts index 03a0e35..ba2a5b8 100644 --- a/apps/web/src/adapters/bestiary-adapter.ts +++ b/apps/web/src/adapters/bestiary-adapter.ts @@ -9,6 +9,8 @@ import type { import { creatureId, proficiencyBonus } from "@initiative/domain"; import { stripTags } from "./strip-tags.js"; +const LEADING_DIGITS_REGEX = /^(\d+)/; + // --- Raw 5etools types (minimal, for parsing) --- interface RawMonster { @@ -168,7 +170,7 @@ function extractAc(ac: RawMonster["ac"]): { } if ("special" in first) { // Variable AC (e.g. spell summons) — parse leading number if possible - const match = first.special.match(/^(\d+)/); + const match = first.special.match(LEADING_DIGITS_REGEX); return { value: match ? Number(match[1]) : 0, source: first.special, diff --git a/apps/web/src/adapters/strip-tags.ts b/apps/web/src/adapters/strip-tags.ts index 9aae1ed..3ab37f9 100644 --- a/apps/web/src/adapters/strip-tags.ts +++ b/apps/web/src/adapters/strip-tags.ts @@ -53,7 +53,7 @@ export function stripTags(text: string): string { // {@atkr type} → mapped attack roll text result = result.replace(/\{@atkr\s+([^}]+)\}/g, (_, type: string) => { - return ATKR_MAP[type.trim()] ?? `Attack Roll:`; + return ATKR_MAP[type.trim()] ?? "Attack Roll:"; }); // {@actSave ability} → "Ability saving throw" diff --git a/apps/web/src/components/ac-shield.tsx b/apps/web/src/components/ac-shield.tsx index e276c41..4d8a8d4 100644 --- a/apps/web/src/components/ac-shield.tsx +++ b/apps/web/src/components/ac-shield.tsx @@ -12,7 +12,7 @@ export function AcShield({ value, onClick, className }: AcShieldProps) { type="button" onClick={onClick} className={cn( - "relative inline-flex items-center justify-center text-sm tabular-nums text-muted-foreground transition-colors hover:text-hover-neutral", + "relative inline-flex items-center justify-center text-muted-foreground text-sm tabular-nums transition-colors hover:text-hover-neutral", className, )} style={{ width: 28, height: 32 }} @@ -29,8 +29,8 @@ export function AcShield({ value, onClick, className }: AcShieldProps) { > - - {value !== undefined ? value : "\u2014"} + + {value == null ? "\u2014" : String(value)} ); diff --git a/apps/web/src/components/action-bar.tsx b/apps/web/src/components/action-bar.tsx index 8aeea6d..3ec7afb 100644 --- a/apps/web/src/components/action-bar.tsx +++ b/apps/web/src/components/action-bar.tsx @@ -85,20 +85,20 @@ function AddModeSuggestions({
{pcMatches.length > 0 && ( <> -
+
Players
    @@ -113,18 +113,18 @@ function AddModeSuggestions({
  • @@ -144,19 +144,18 @@ function AddModeSuggestions({
  • @@ -578,7 +577,7 @@ export function ActionBar({ {!browseMode && nameInput.length >= 2 && !hasSuggestions && ( )} - {showRollAllInitiative && onRollAllInitiative && ( + {showRollAllInitiative && !!onRollAllInitiative && ( @@ -54,7 +55,7 @@ export function BulkImportPrompt({ return (
    -
    +
    Loading sources... {processed}/{importState.total}
    @@ -74,23 +75,20 @@ export function BulkImportPrompt({ return (
    -

    +

    Import All Sources

    -

    +

    Load stat block data for all {totalSources} sources at once.

    -