Improve empty encounter UX with interactive add button
All checks were successful
CI / check (push) Successful in 44s
CI / build-image (push) Successful in 22s

Replace the static "No combatants yet" text with a centered, breathing
"+" icon that focuses the action bar input on click, guiding users to
add their first combatant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-12 10:56:56 +01:00
parent 7feaf90eab
commit 768e7a390f
3 changed files with 40 additions and 5 deletions

View File

@@ -3,6 +3,7 @@ import {
rollInitiativeUseCase,
} from "@initiative/application";
import type { CombatantId, Creature, CreatureId } from "@initiative/domain";
import { Plus } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { ActionBar } from "./components/action-bar";
import { CombatantRow } from "./components/combatant-row";
@@ -160,6 +161,8 @@ export function App() {
setPinnedCreatureId(null);
}, []);
const actionBarInputRef = useRef<HTMLInputElement>(null);
// Auto-scroll to the active combatant when the turn changes
const activeRowRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -205,11 +208,17 @@ export function App() {
{/* Scrollable area — combatant list */}
<div className="flex-1 overflow-y-auto min-h-0">
<div className="flex flex-col px-2 py-2">
<div
className={`flex flex-col px-2 py-2${encounter.combatants.length === 0 ? " h-full items-center justify-center" : ""}`}
>
{encounter.combatants.length === 0 ? (
<p className="py-12 text-center text-sm text-muted-foreground">
No combatants yet add one to get started
</p>
<button
type="button"
onClick={() => actionBarInputRef.current?.focus()}
className="animate-breathe cursor-pointer text-muted-foreground transition-colors hover:text-primary"
>
<Plus className="size-14" />
</button>
) : (
encounter.combatants.map((c, i) => (
<CombatantRow
@@ -249,6 +258,7 @@ export function App() {
onViewStatBlock={handleViewStatBlock}
onBulkImport={handleBulkImport}
bulkImportDisabled={bulkImport.state.status === "loading"}
inputRef={actionBarInputRef}
/>
</div>
</div>

View File

@@ -1,5 +1,11 @@
import { Check, Eye, Import, Minus, Plus } from "lucide-react";
import { type FormEvent, useEffect, useRef, useState } from "react";
import {
type FormEvent,
type RefObject,
useEffect,
useRef,
useState,
} from "react";
import type { SearchResult } from "../hooks/use-bestiary.js";
import { Button } from "./ui/button.js";
import { Input } from "./ui/input.js";
@@ -20,6 +26,7 @@ interface ActionBarProps {
onViewStatBlock?: (result: SearchResult) => void;
onBulkImport?: () => void;
bulkImportDisabled?: boolean;
inputRef?: RefObject<HTMLInputElement | null>;
}
function creatureKey(r: SearchResult): string {
@@ -34,6 +41,7 @@ export function ActionBar({
onViewStatBlock,
onBulkImport,
bulkImportDisabled,
inputRef,
}: ActionBarProps) {
const [nameInput, setNameInput] = useState("");
const [suggestions, setSuggestions] = useState<SearchResult[]>([]);
@@ -222,6 +230,7 @@ export function ActionBar({
>
<div className="relative flex-1">
<Input
ref={inputRef}
type="text"
value={nameInput}
onChange={(e) => handleNameChange(e.target.value)}

View File

@@ -80,6 +80,22 @@
}
}
@keyframes breathe {
0%,
100% {
opacity: 0.4;
scale: 0.9;
}
50% {
opacity: 1;
scale: 1.1;
}
}
@utility animate-breathe {
animation: breathe 3s ease-in-out infinite;
}
@custom-variant pointer-coarse (@media (pointer: coarse));
@utility animate-confirm-pulse {