Use Readonly props and optional chaining/nullish coalescing
Mark component props as Readonly<> across 15 component files and simplify edit-player-character field access with optional chaining and nullish coalescing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
||||
Plus,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { type RefObject, useDeferredValue, useState } from "react";
|
||||
import React, { type RefObject, useDeferredValue, useState } from "react";
|
||||
import type { SearchResult } from "../hooks/use-bestiary.js";
|
||||
import { cn } from "../lib/utils.js";
|
||||
import { D20Icon } from "./d20-icon.js";
|
||||
@@ -62,7 +62,7 @@ function AddModeSuggestions({
|
||||
onConfirmQueued,
|
||||
onAddFromPlayerCharacter,
|
||||
onClear,
|
||||
}: {
|
||||
}: Readonly<{
|
||||
nameInput: string;
|
||||
suggestions: SearchResult[];
|
||||
pcMatches: PlayerCharacter[];
|
||||
@@ -75,7 +75,7 @@ function AddModeSuggestions({
|
||||
onSetQueued: (q: QueuedCreature | null) => void;
|
||||
onConfirmQueued: () => void;
|
||||
onAddFromPlayerCharacter?: (pc: PlayerCharacter) => void;
|
||||
}) {
|
||||
}>) {
|
||||
return (
|
||||
<div className="absolute bottom-full z-50 mb-1 w-full max-w-xs rounded-md border border-border bg-card shadow-lg">
|
||||
<button
|
||||
@@ -263,7 +263,7 @@ export function ActionBar({
|
||||
rollAllInitiativeDisabled,
|
||||
onOpenSourceManager,
|
||||
autoFocus,
|
||||
}: ActionBarProps) {
|
||||
}: Readonly<ActionBarProps>) {
|
||||
const [nameInput, setNameInput] = useState("");
|
||||
const [suggestions, setSuggestions] = useState<SearchResult[]>([]);
|
||||
const [pcMatches, setPcMatches] = useState<PlayerCharacter[]>([]);
|
||||
|
||||
@@ -18,7 +18,7 @@ export function BulkImportPrompt({
|
||||
importState,
|
||||
onStartImport,
|
||||
onDone,
|
||||
}: BulkImportPromptProps) {
|
||||
}: Readonly<BulkImportPromptProps>) {
|
||||
const [baseUrl, setBaseUrl] = useState(DEFAULT_BASE_URL);
|
||||
const baseUrlId = useId();
|
||||
const totalSources = getAllSourceCodes().length;
|
||||
|
||||
@@ -11,7 +11,7 @@ export function BulkImportToasts({
|
||||
state,
|
||||
visible,
|
||||
onReset,
|
||||
}: BulkImportToastsProps) {
|
||||
}: Readonly<BulkImportToastsProps>) {
|
||||
if (!visible) return null;
|
||||
|
||||
if (state.status === "loading") {
|
||||
|
||||
@@ -9,7 +9,7 @@ interface ColorPaletteProps {
|
||||
|
||||
const COLORS = [...VALID_PLAYER_COLORS] as string[];
|
||||
|
||||
export function ColorPalette({ value, onChange }: ColorPaletteProps) {
|
||||
export function ColorPalette({ value, onChange }: Readonly<ColorPaletteProps>) {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{COLORS.map((color) => (
|
||||
|
||||
@@ -50,13 +50,13 @@ function EditableName({
|
||||
onRename,
|
||||
onShowStatBlock,
|
||||
color,
|
||||
}: {
|
||||
}: Readonly<{
|
||||
name: string;
|
||||
combatantId: CombatantId;
|
||||
onRename: (id: CombatantId, newName: string) => void;
|
||||
onShowStatBlock?: () => void;
|
||||
color?: string;
|
||||
}) {
|
||||
}>) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draft, setDraft] = useState(name);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -154,10 +154,10 @@ function EditableName({
|
||||
function MaxHpDisplay({
|
||||
maxHp,
|
||||
onCommit,
|
||||
}: {
|
||||
}: Readonly<{
|
||||
maxHp: number | undefined;
|
||||
onCommit: (value: number | undefined) => void;
|
||||
}) {
|
||||
}>) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draft, setDraft] = useState(maxHp?.toString() ?? "");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -215,12 +215,12 @@ function ClickableHp({
|
||||
maxHp,
|
||||
onAdjust,
|
||||
dimmed,
|
||||
}: {
|
||||
}: Readonly<{
|
||||
currentHp: number | undefined;
|
||||
maxHp: number | undefined;
|
||||
onAdjust: (delta: number) => void;
|
||||
dimmed?: boolean;
|
||||
}) {
|
||||
}>) {
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const status = deriveHpStatus(currentHp, maxHp);
|
||||
|
||||
@@ -265,10 +265,10 @@ function ClickableHp({
|
||||
function AcDisplay({
|
||||
ac,
|
||||
onCommit,
|
||||
}: {
|
||||
}: Readonly<{
|
||||
ac: number | undefined;
|
||||
onCommit: (value: number | undefined) => void;
|
||||
}) {
|
||||
}>) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draft, setDraft] = useState(ac?.toString() ?? "");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -319,13 +319,13 @@ function InitiativeDisplay({
|
||||
dimmed,
|
||||
onSetInitiative,
|
||||
onRollInitiative,
|
||||
}: {
|
||||
}: Readonly<{
|
||||
initiative: number | undefined;
|
||||
combatantId: CombatantId;
|
||||
dimmed: boolean;
|
||||
onSetInitiative: (id: CombatantId, value: number | undefined) => void;
|
||||
onRollInitiative?: (id: CombatantId) => void;
|
||||
}) {
|
||||
}>) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draft, setDraft] = useState(initiative?.toString() ?? "");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -61,7 +61,7 @@ export function ConditionPicker({
|
||||
activeConditions,
|
||||
onToggle,
|
||||
onClose,
|
||||
}: ConditionPickerProps) {
|
||||
}: Readonly<ConditionPickerProps>) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [flipped, setFlipped] = useState(false);
|
||||
const [maxHeight, setMaxHeight] = useState<number | undefined>(undefined);
|
||||
|
||||
@@ -60,7 +60,7 @@ export function ConditionTags({
|
||||
conditions,
|
||||
onRemove,
|
||||
onOpenPicker,
|
||||
}: ConditionTagsProps) {
|
||||
}: Readonly<ConditionTagsProps>) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-0.5">
|
||||
{conditions?.map((condId) => {
|
||||
|
||||
@@ -24,7 +24,7 @@ export function CreatePlayerModal({
|
||||
onClose,
|
||||
onSave,
|
||||
playerCharacter,
|
||||
}: CreatePlayerModalProps) {
|
||||
}: Readonly<CreatePlayerModalProps>) {
|
||||
const [name, setName] = useState("");
|
||||
const [ac, setAc] = useState("10");
|
||||
const [maxHp, setMaxHp] = useState("10");
|
||||
|
||||
@@ -10,7 +10,7 @@ interface IconGridProps {
|
||||
|
||||
const ICONS = [...VALID_PLAYER_ICONS] as PlayerIcon[];
|
||||
|
||||
export function IconGrid({ value, onChange }: IconGridProps) {
|
||||
export function IconGrid({ value, onChange }: Readonly<IconGridProps>) {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{ICONS.map((iconId) => {
|
||||
|
||||
@@ -21,7 +21,7 @@ export function PlayerManagement({
|
||||
onEdit,
|
||||
onDelete,
|
||||
onCreate,
|
||||
}: PlayerManagementProps) {
|
||||
}: Readonly<PlayerManagementProps>) {
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
|
||||
@@ -18,7 +18,7 @@ export function SourceFetchPrompt({
|
||||
fetchAndCacheSource,
|
||||
onSourceLoaded,
|
||||
onUploadSource,
|
||||
}: SourceFetchPromptProps) {
|
||||
}: Readonly<SourceFetchPromptProps>) {
|
||||
const [url, setUrl] = useState(() => getDefaultFetchUrl(sourceCode));
|
||||
const [status, setStatus] = useState<"idle" | "fetching" | "error">("idle");
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
@@ -8,7 +8,9 @@ interface SourceManagerProps {
|
||||
onCacheCleared: () => void;
|
||||
}
|
||||
|
||||
export function SourceManager({ onCacheCleared }: SourceManagerProps) {
|
||||
export function SourceManager({
|
||||
onCacheCleared,
|
||||
}: Readonly<SourceManagerProps>) {
|
||||
const [sources, setSources] = useState<CachedSourceInfo[]>([]);
|
||||
const [optimisticSources, applyOptimistic] = useOptimistic(
|
||||
sources,
|
||||
|
||||
@@ -46,11 +46,11 @@ function CollapsedTab({
|
||||
creatureName,
|
||||
side,
|
||||
onToggleCollapse,
|
||||
}: {
|
||||
}: Readonly<{
|
||||
creatureName: string;
|
||||
side: "left" | "right";
|
||||
onToggleCollapse: () => void;
|
||||
}) {
|
||||
}>) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -73,13 +73,13 @@ function PanelHeader({
|
||||
onToggleCollapse,
|
||||
onPin,
|
||||
onUnpin,
|
||||
}: {
|
||||
}: Readonly<{
|
||||
panelRole: "browse" | "pinned";
|
||||
showPinButton: boolean;
|
||||
onToggleCollapse: () => void;
|
||||
onPin: () => void;
|
||||
onUnpin: () => void;
|
||||
}) {
|
||||
}>) {
|
||||
return (
|
||||
<div className="flex items-center justify-between border-border border-b px-4 py-2">
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -133,7 +133,7 @@ function DesktopPanel({
|
||||
onPin,
|
||||
onUnpin,
|
||||
children,
|
||||
}: {
|
||||
}: Readonly<{
|
||||
isCollapsed: boolean;
|
||||
side: "left" | "right";
|
||||
creatureName: string;
|
||||
@@ -143,7 +143,7 @@ function DesktopPanel({
|
||||
onPin: () => void;
|
||||
onUnpin: () => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
}>) {
|
||||
const sideClasses = side === "left" ? "left-0 border-r" : "right-0 border-l";
|
||||
const collapsedTranslate =
|
||||
side === "right"
|
||||
@@ -179,10 +179,10 @@ function DesktopPanel({
|
||||
function MobileDrawer({
|
||||
onDismiss,
|
||||
children,
|
||||
}: {
|
||||
}: Readonly<{
|
||||
onDismiss: () => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
}>) {
|
||||
const { offsetX, isSwiping, handlers } = useSwipeToDismiss(onDismiss);
|
||||
|
||||
return (
|
||||
@@ -239,7 +239,7 @@ export function StatBlockPanel({
|
||||
onStartBulkImport,
|
||||
onBulkImportDone,
|
||||
sourceManagerMode,
|
||||
}: StatBlockPanelProps) {
|
||||
}: Readonly<StatBlockPanelProps>) {
|
||||
const [isDesktop, setIsDesktop] = useState(
|
||||
() => globalThis.matchMedia("(min-width: 1024px)").matches,
|
||||
);
|
||||
|
||||
@@ -16,10 +16,10 @@ function abilityMod(score: number): string {
|
||||
function PropertyLine({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
}: Readonly<{
|
||||
label: string;
|
||||
value: string | undefined;
|
||||
}) {
|
||||
}>) {
|
||||
if (!value) return null;
|
||||
return (
|
||||
<div className="text-sm">
|
||||
@@ -34,7 +34,7 @@ function SectionDivider() {
|
||||
);
|
||||
}
|
||||
|
||||
export function StatBlock({ creature }: StatBlockProps) {
|
||||
export function StatBlock({ creature }: Readonly<StatBlockProps>) {
|
||||
const abilities = [
|
||||
{ label: "STR", score: creature.abilities.str },
|
||||
{ label: "DEX", score: creature.abilities.dex },
|
||||
|
||||
@@ -15,7 +15,7 @@ export function TurnNavigation({
|
||||
onAdvanceTurn,
|
||||
onRetreatTurn,
|
||||
onClearEncounter,
|
||||
}: TurnNavigationProps) {
|
||||
}: Readonly<TurnNavigationProps>) {
|
||||
const hasCombatants = encounter.combatants.length > 0;
|
||||
const isAtStart = encounter.roundNumber === 1 && encounter.activeIndex === 0;
|
||||
const activeCombatant = encounter.combatants[encounter.activeIndex];
|
||||
|
||||
Reference in New Issue
Block a user