From d4bb140ad672079721f043830edbb56949a15320 Mon Sep 17 00:00:00 2001 From: Hikari Date: Mon, 23 Mar 2026 15:11:11 -0700 Subject: [PATCH] feat: collapse resources into a gold-toggle dropdown (#106) Only Gold is visible in the resource bar by default. Clicking the gold display opens a dropdown showing Gold/s, Essence, Crystals, Runestones, and Combat Power. An orange alert dot appears on the gold toggle when Essence or Crystals are capped, so the player always knows to check even with the panel closed. --- apps/web/src/components/ui/resourceBar.tsx | 159 ++++++++++++++------- apps/web/src/styles.css | 57 ++++++++ 2 files changed, 161 insertions(+), 55 deletions(-) diff --git a/apps/web/src/components/ui/resourceBar.tsx b/apps/web/src/components/ui/resourceBar.tsx index 351d690..cb76ba0 100644 --- a/apps/web/src/components/ui/resourceBar.tsx +++ b/apps/web/src/components/ui/resourceBar.tsx @@ -4,6 +4,7 @@ * @license Naomi's Public License * @author Naomi Carrigan */ +/* eslint-disable max-lines -- Resource bar has many resource and action elements */ /* eslint-disable max-lines-per-function -- Large header with many resource and action elements */ /* eslint-disable max-statements -- Resource bar requires many local computations and handlers */ /* eslint-disable complexity -- Many conditional resource and badge render paths */ @@ -77,6 +78,7 @@ const ResourceBar = ({ }: ResourceBarProperties): JSX.Element => { const { formatNumber, syncError, state } = useGame(); const [ isProfileOpen, setIsProfileOpen ] = useState(false); + const [ isResourcesOpen, setIsResourcesOpen ] = useState(false); const { gold, essence, crystals } = resources; let partyCombatPower = 0; @@ -99,18 +101,28 @@ const ResourceBar = ({ ? "#" : `/profile/${state.player.discordId}`; - const resourceValues = [ gold, essence, crystals ]; - const anyFull = resourceValues.some((v) => { - return v >= RESOURCE_CAP; - }); const goldFull = gold >= RESOURCE_CAP; const essenceFull = essence >= RESOURCE_CAP; const crystalsFull = crystals >= RESOURCE_CAP; + const anyFull = goldFull || essenceFull || crystalsFull; + const hiddenResourcesFull = essenceFull || crystalsFull; function handleForceSync(): void { void onForceSync(); } + function handleToggleResources(): void { + setIsResourcesOpen((previous) => { + return !previous; + }); + } + + function handleResourceBlur(event: FocusEvent): void { + if (!event.currentTarget.contains(event.relatedTarget)) { + setIsResourcesOpen(false); + } + } + function handleToggleProfile(): void { setIsProfileOpen((previous) => { return !previous; @@ -131,59 +143,96 @@ const ResourceBar = ({ return ( <>
-
- {"🪙"} - {formatNumber(gold)} - {"Gold"} - {goldFull - ? - {"FULL"} - +
+ + {isResourcesOpen + ?
+
+ {"📈"} + + {formatNumber(goldPerSecond)} + + {"Gold/s"} +
+
+ {"✨"} + + {formatNumber(essence)} + + {"Essence"} + {essenceFull + ? + {"FULL"} + + : null} +
+
+ {"💎"} + + {formatNumber(crystals)} + + {"Crystals"} + {crystalsFull + ? + {"FULL"} + + : null} +
+
+ {"🔮"} + + {formatNumber(runestones)} + + {"Runestones"} +
+
+ {"⚔️"} + + {formatNumber(partyCombatPower)} + + {"Combat Power"} +
+
: null}
-
- {"📈"} - {formatNumber(goldPerSecond)} - {"Gold/s"} -
-
- {"✨"} - {formatNumber(essence)} - {"Essence"} - {essenceFull - ? - {"FULL"} - - : null} -
-
- {"💎"} - {formatNumber(crystals)} - {"Crystals"} - {crystalsFull - ? - {"FULL"} - - : null} -
-
- {"🔮"} - {formatNumber(runestones)} - {"Runestones"} -
-
- {"⚔️"} - - {formatNumber(partyCombatPower)} - - {"Combat Power"} -
{apotheosisCount > 0 &&
{"✨ Apotheosis "} diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index fd46462..75608f0 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -116,6 +116,63 @@ body::before { text-align: center; } +/* ── Resource toggle + dropdown ─────────────────────────────────────────── */ + +.resource-menu { + position: relative; +} + +.resource-toggle { + background: none; + border: none; + border-radius: 0.4rem; + cursor: pointer; + font-family: inherit; + padding: 0.2rem 0.4rem; + position: relative; + transition: background 0.15s; +} + +.resource-toggle:hover { + background: rgba(255, 255, 255, 0.07); +} + +.resource-alert-dot { + background: var(--colour-warning, #f59e0b); + border-radius: 50%; + height: 0.45rem; + position: absolute; + right: 0; + top: 0; + width: 0.45rem; +} + +.resources-dropdown { + background: var(--colour-surface); + border: 1px solid rgba(147, 51, 234, 0.4); + border-radius: 0.5rem; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); + display: flex; + flex-direction: column; + gap: 0.1rem; + left: 0; + padding: 0.4rem; + position: absolute; + top: calc(100% + 0.4rem); + z-index: 100; +} + +.resources-dropdown .resource { + border-radius: 0.35rem; + gap: 0.5rem; + padding: 0.3rem 0.5rem; + white-space: nowrap; +} + +.resources-dropdown .resource:hover { + background: rgba(255, 255, 255, 0.04); +} + /* ===================== GAME LAYOUT ===================== */ .game-layout { display: flex;