diff --git a/apps/web/src/components/game/AchievementPanel.tsx b/apps/web/src/components/game/AchievementPanel.tsx index 9f808ff..4b21fcf 100644 --- a/apps/web/src/components/game/AchievementPanel.tsx +++ b/apps/web/src/components/game/AchievementPanel.tsx @@ -1,6 +1,8 @@ import type { Achievement } from "@elysium/types"; +import { useState } from "react"; import { useGame } from "../../context/GameContext.js"; import { formatNumber } from "../../utils/format.js"; +import { LockToggle } from "../ui/LockToggle.js"; const conditionDescription = (achievement: Achievement): string => { const { condition } = achievement; @@ -53,20 +55,30 @@ const AchievementCard = ({ achievement }: AchievementCardProps): React.JSX.Eleme export const AchievementPanel = (): React.JSX.Element => { const { state } = useGame(); + const [showLocked, setShowLocked] = useState(true); if (!state) return

Loading...

; const achievements = state.achievements ?? []; - const unlocked = achievements.filter((a) => a.unlockedAt !== null).length; + const unlocked = achievements.filter((a) => a.unlockedAt !== null); + const locked = achievements.filter((a) => a.unlockedAt === null); + const visible = showLocked ? achievements : unlocked; return (
-

Achievements

+
+

Achievements

+ { setShowLocked((v) => !v); }} + /> +

- {unlocked} / {achievements.length} unlocked + {unlocked.length} / {achievements.length} unlocked

- {achievements.map((achievement) => ( + {visible.map((achievement) => ( ))}
diff --git a/apps/web/src/components/game/BossPanel.tsx b/apps/web/src/components/game/BossPanel.tsx index 0bf2910..8a35996 100644 --- a/apps/web/src/components/game/BossPanel.tsx +++ b/apps/web/src/components/game/BossPanel.tsx @@ -2,6 +2,7 @@ import type { Boss } from "@elysium/types"; import { useState } from "react"; import { useGame } from "../../context/GameContext.js"; import { formatNumber } from "../../utils/format.js"; +import { LockToggle } from "../ui/LockToggle.js"; import { ZoneSelector } from "./ZoneSelector.js"; interface BossCardProps { @@ -89,6 +90,7 @@ export const BossPanel = (): React.JSX.Element => { const { state, challengeBoss } = useGame(); const [challengingBossId, setChallengingBossId] = useState(null); const [activeZoneId, setActiveZoneId] = useState("verdant_vale"); + const [showLocked, setShowLocked] = useState(true); if (!state) return

Loading...

; @@ -139,10 +141,21 @@ export const BossPanel = (): React.JSX.Element => { const zones = state.zones ?? []; const zoneBosses = state.bosses.filter((b) => b.zoneId === activeZoneId); + const lockedCount = zoneBosses.filter((b) => b.status === "locked").length; + const visibleBosses = showLocked + ? zoneBosses + : zoneBosses.filter((b) => b.status !== "locked"); return (
-

Boss Encounters

+
+

Boss Encounters

+ { setShowLocked((v) => !v); }} + /> +
{
- {zoneBosses.map((boss) => ( + {visibleBosses.map((boss) => ( { }} /> ))} - {zoneBosses.length === 0 && ( -

No bosses in this zone yet.

+ {visibleBosses.length === 0 && ( +

No bosses to show in this zone.

)}
diff --git a/apps/web/src/components/game/EquipmentPanel.tsx b/apps/web/src/components/game/EquipmentPanel.tsx index 3807914..134c1f0 100644 --- a/apps/web/src/components/game/EquipmentPanel.tsx +++ b/apps/web/src/components/game/EquipmentPanel.tsx @@ -1,5 +1,7 @@ import type { Equipment, EquipmentType } from "@elysium/types"; +import { useState } from "react"; import { useGame } from "../../context/GameContext.js"; +import { LockToggle } from "../ui/LockToggle.js"; const RARITY_LABEL: Record = { common: "Common", @@ -72,20 +74,31 @@ const SLOT_LABEL: Record = { export const EquipmentPanel = (): React.JSX.Element => { const { state } = useGame(); + const [showLocked, setShowLocked] = useState(true); if (!state) return

Loading...

; const equipment = state.equipment ?? []; + const unownedCount = equipment.filter((e) => !e.owned).length; return (
-

Equipment

+
+

Equipment

+ { setShowLocked((v) => !v); }} + /> +

Equipment drops from bosses and grants passive bonuses. Only one item per slot can be equipped at a time.

{SLOT_ORDER.map((slotType) => { - const items = equipment.filter((e) => e.type === slotType); + const items = equipment.filter( + (e) => e.type === slotType && (showLocked || e.owned), + ); return (

{SLOT_LABEL[slotType]}

@@ -93,6 +106,9 @@ export const EquipmentPanel = (): React.JSX.Element => { {items.map((item) => ( ))} + {items.length === 0 && ( +

No items to show in this slot.

+ )}
); diff --git a/apps/web/src/components/game/QuestPanel.tsx b/apps/web/src/components/game/QuestPanel.tsx index 39c503d..00e73ec 100644 --- a/apps/web/src/components/game/QuestPanel.tsx +++ b/apps/web/src/components/game/QuestPanel.tsx @@ -1,6 +1,7 @@ import type { Quest } from "@elysium/types"; import { useState } from "react"; import { useGame } from "../../context/GameContext.js"; +import { LockToggle } from "../ui/LockToggle.js"; import { ZoneSelector } from "./ZoneSelector.js"; const formatDuration = (seconds: number): string => { @@ -65,15 +66,27 @@ const QuestCard = ({ quest }: QuestCardProps): React.JSX.Element => { export const QuestPanel = (): React.JSX.Element => { const { state } = useGame(); const [activeZoneId, setActiveZoneId] = useState("verdant_vale"); + const [showLocked, setShowLocked] = useState(true); if (!state) return

Loading...

; const zones = state.zones ?? []; const zoneQuests = state.quests.filter((q) => q.zoneId === activeZoneId); + const lockedCount = zoneQuests.filter((q) => q.status === "locked").length; + const visibleQuests = showLocked + ? zoneQuests + : zoneQuests.filter((q) => q.status !== "locked"); return (
-

Quests

+
+

Quests

+ { setShowLocked((v) => !v); }} + /> +
{ />
- {zoneQuests.map((quest) => ( + {visibleQuests.map((quest) => ( ))} - {zoneQuests.length === 0 && ( -

No quests in this zone yet.

+ {visibleQuests.length === 0 && ( +

No quests to show in this zone.

)}
diff --git a/apps/web/src/components/game/UpgradePanel.tsx b/apps/web/src/components/game/UpgradePanel.tsx index f2e2ac6..3759ce3 100644 --- a/apps/web/src/components/game/UpgradePanel.tsx +++ b/apps/web/src/components/game/UpgradePanel.tsx @@ -1,5 +1,7 @@ import type { Upgrade } from "@elysium/types"; +import { useState } from "react"; import { useGame } from "../../context/GameContext.js"; +import { LockToggle } from "../ui/LockToggle.js"; interface UpgradeCardProps { upgrade: Upgrade; @@ -63,6 +65,7 @@ const UpgradeCard = ({ upgrade, currentGold, currentEssence }: UpgradeCardProps) export const UpgradePanel = (): React.JSX.Element => { const { state } = useGame(); + const [showLocked, setShowLocked] = useState(true); if (!state) return

Loading...

; @@ -72,7 +75,14 @@ export const UpgradePanel = (): React.JSX.Element => { return (
-

Upgrades

+
+

Upgrades

+ { setShowLocked((v) => !v); }} + /> +

{purchased.length} / {state.upgrades.length} purchased

{state.upgrades.length === 0 ? (

No upgrades available yet — keep adventuring!

@@ -94,7 +104,7 @@ export const UpgradePanel = (): React.JSX.Element => { currentEssence={state.resources.essence} /> ))} - {locked.map((upgrade) => ( + {showLocked && locked.map((upgrade) => ( void; + lockedCount: number; +} + +export const LockToggle = ({ + showLocked, + onToggle, + lockedCount, +}: LockToggleProps): React.JSX.Element => ( + +); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index fe9f23f..8f9cd4b 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -1107,6 +1107,47 @@ body { font-size: 0.85rem; } +/* ── Panel Header (title + lock toggle row) ────────────────────────────── */ + +.panel-header { + align-items: center; + display: flex; + gap: 1rem; + justify-content: space-between; + margin-bottom: 0.25rem; +} + +.panel-header h2 { + margin: 0; +} + +.lock-toggle { + align-items: center; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(147, 51, 234, 0.3); + border-radius: 1rem; + color: var(--colour-text-muted); + cursor: pointer; + display: flex; + font-family: inherit; + font-size: 0.75rem; + gap: 0.3rem; + padding: 0.3rem 0.75rem; + transition: all 0.2s; + white-space: nowrap; +} + +.lock-toggle:hover { + background: rgba(147, 51, 234, 0.15); + border-color: rgba(147, 51, 234, 0.6); + color: var(--colour-text); +} + +.lock-toggle-on { + border-color: rgba(147, 51, 234, 0.5); + color: var(--colour-text); +} + /* ── Zone Selector ─────────────────────────────────────────────────────── */ .zone-selector {