From 75778884bd1be7d135b2f5ea9b8a8e77a0149f7b Mon Sep 17 00:00:00 2001 From: Lukas Date: Fri, 13 Mar 2026 14:34:02 +0100 Subject: [PATCH] Hide top bar in empty state and animate it in with first combatant The turn navigation bar is now hidden when no combatants exist, keeping the empty state clean. It slides down from above when the first combatant is added, synchronized with the action bar settling animation. Co-Authored-By: Claude Opus 4.6 --- apps/web/src/App.tsx | 75 +++++++++++++++++++++++++++--------------- apps/web/src/index.css | 37 +++++++++++++++++++-- 2 files changed, 83 insertions(+), 29 deletions(-) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index ff5037d..9192dd8 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -3,7 +3,13 @@ import { rollInitiativeUseCase, } from "@initiative/application"; import type { CombatantId, Creature, CreatureId } from "@initiative/domain"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; import { ActionBar } from "./components/action-bar"; import { CombatantRow } from "./components/combatant-row"; import { CreatePlayerModal } from "./components/create-player-modal"; @@ -25,22 +31,37 @@ function useActionBarAnimation(combatantCount: number) { const wasEmptyRef = useRef(combatantCount === 0); const [settling, setSettling] = useState(false); const [rising, setRising] = useState(false); + const [topBarExiting, setTopBarExiting] = useState(false); - useEffect(() => { + useLayoutEffect(() => { const nowEmpty = combatantCount === 0; if (wasEmptyRef.current && !nowEmpty) { setSettling(true); } else if (!wasEmptyRef.current && nowEmpty) { setRising(true); + setTopBarExiting(true); } wasEmptyRef.current = nowEmpty; }, [combatantCount]); + 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 showTopBar = !empty || topBarExiting; + return { - settling, - rising, + risingClass, + settlingClass, + topBarClass, + showTopBar, onSettleEnd: () => setSettling(false), onRiseEnd: () => setRising(false), + onTopBarExitEnd: () => setTopBarExiting(false), }; } @@ -218,37 +239,31 @@ export function App() { }, [encounter.activeIndex, encounter.combatants, isLoaded]); const isEmpty = encounter.combatants.length === 0; - const risingClass = actionBarAnim.rising ? " animate-rise-to-center" : ""; - const settlingClass = actionBarAnim.settling - ? " animate-settle-to-bottom" - : ""; return (
-
- {/* Turn Navigation — fixed at top */} -
- setSourceManagerOpen((o) => !o)} - /> -
- - {sourceManagerOpen && ( -
- +
+ {actionBarAnim.showTopBar && ( +
+ setSourceManagerOpen((o) => !o)} + />
)} {isEmpty ? ( /* Empty state — ActionBar centered */ -
+
) : ( <> + {sourceManagerOpen && ( +
+ +
+ )} + {/* Scrollable area — combatant list */}
@@ -301,7 +322,7 @@ export function App() { {/* Action Bar — fixed at bottom */}