generated from nhcarrigan/template
chore: community feedback fixes and UI improvements (#102)
## Summary Addresses all community feedback tickets from the last deploy, plus several UI improvements made during the same session. ### Bug fixes & balance - **#97** — Fix auto-adventurer tier priority: sort by combat power instead of current cost so the highest-tier affordable unit is always purchased - **#98** — Add Dark Templar adventurer (80k CP) to bridge the Volcanic Depths progression wall; rewire upgrade and quest rewards accordingly - **#99** — Reorder and buff Shadow Assassin (55k CP, level 12) so Witch Coven feels rewarding rather than a regression - **#100** — Display effective Gold/s (all multipliers applied) in the resource bar - **#101** — Add Peasant tier 2 (10x, essence) and tier 3 (50x, crystals) upgrades for meaningful late-game scaling ### Other fixes - Sync game state to server before auto-boss challenges (matching manual challenge behaviour) - Refresh Discord avatar hash on every game load via bot token so stale CDN URLs are corrected automatically ### UI improvements - Replace Donate / Discord / Support / View Profile / Edit Profile buttons with a single avatar dropdown menu - Collapse all resources except Gold into a click-to-toggle dropdown; orange alert dot appears when a hidden resource is capped ## Closes Closes #97 Closes #98 Closes #99 Closes #100 Closes #101 Reviewed-on: #102 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #102.
This commit is contained in:
@@ -135,7 +135,6 @@ const GameLayout = (): JSX.Element => {
|
||||
);
|
||||
}
|
||||
|
||||
const profileUrl = `/profile/${state.player.discordId}`;
|
||||
const codexBadgeCount = pendingCodexEntryIds.length;
|
||||
const storyBadgeCount = pendingStoryChapterIds.length;
|
||||
|
||||
@@ -160,7 +159,6 @@ const GameLayout = (): JSX.Element => {
|
||||
onEditProfile={handleOpenEditProfile}
|
||||
onForceSync={forceSync}
|
||||
prestigeCount={state.prestige.count}
|
||||
profileUrl={profileUrl}
|
||||
resources={state.resources}
|
||||
runestones={state.prestige.runestones}
|
||||
transcendenceCount={state.transcendence?.count ?? 0}
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
* @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 */
|
||||
import { useState, type FocusEvent, type JSX } from "react";
|
||||
import { useGame } from "../../context/gameContext.js";
|
||||
import { RESOURCE_CAP } from "../../engine/tick.js";
|
||||
import { RESOURCE_CAP, computeGoldPerSecond } from "../../engine/tick.js";
|
||||
import type { Resource } from "@elysium/types";
|
||||
import type { JSX } from "react";
|
||||
|
||||
interface ResourceBarProperties {
|
||||
readonly resources: Resource;
|
||||
@@ -17,7 +19,6 @@ interface ResourceBarProperties {
|
||||
readonly prestigeCount: number;
|
||||
readonly transcendenceCount: number;
|
||||
readonly apotheosisCount: number;
|
||||
readonly profileUrl: string;
|
||||
readonly onEditProfile: ()=> void;
|
||||
readonly lastSavedAt: number | null;
|
||||
readonly isSyncing: boolean;
|
||||
@@ -58,7 +59,6 @@ const resourceFullTooltip = [
|
||||
* @param props.prestigeCount - The number of prestiges completed.
|
||||
* @param props.transcendenceCount - The number of transcendences completed.
|
||||
* @param props.apotheosisCount - The number of apotheoses completed.
|
||||
* @param props.profileUrl - The URL of the player's public profile.
|
||||
* @param props.onEditProfile - Callback to open the edit profile modal.
|
||||
* @param props.lastSavedAt - Timestamp of the last cloud save.
|
||||
* @param props.isSyncing - Whether a sync is currently in progress.
|
||||
@@ -71,84 +71,168 @@ const ResourceBar = ({
|
||||
prestigeCount,
|
||||
transcendenceCount,
|
||||
apotheosisCount,
|
||||
profileUrl,
|
||||
onEditProfile,
|
||||
lastSavedAt,
|
||||
isSyncing,
|
||||
onForceSync,
|
||||
}: 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;
|
||||
let goldPerSecond = 0;
|
||||
if (state !== null) {
|
||||
for (const adventurer of state.adventurers) {
|
||||
const contribution = adventurer.combatPower * adventurer.count;
|
||||
partyCombatPower = partyCombatPower + contribution;
|
||||
}
|
||||
goldPerSecond = computeGoldPerSecond(state);
|
||||
}
|
||||
const resourceValues = [ gold, essence, crystals ];
|
||||
const anyFull = resourceValues.some((v) => {
|
||||
return v >= RESOURCE_CAP;
|
||||
});
|
||||
|
||||
let avatarUrl: string | null = null;
|
||||
if (state !== null) {
|
||||
avatarUrl = state.player.avatar === null
|
||||
? `https://cdn.discordapp.com/embed/avatars/${String(Number.parseInt(state.player.discordId, 10) % 5)}.png`
|
||||
: `https://cdn.discordapp.com/avatars/${state.player.discordId}/${state.player.avatar}.png?size=64`;
|
||||
}
|
||||
const profileUrl = state === null
|
||||
? "#"
|
||||
: `/profile/${state.player.discordId}`;
|
||||
|
||||
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<HTMLDivElement>): void {
|
||||
if (!event.currentTarget.contains(event.relatedTarget)) {
|
||||
setIsResourcesOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleProfile(): void {
|
||||
setIsProfileOpen((previous) => {
|
||||
return !previous;
|
||||
});
|
||||
}
|
||||
|
||||
function handleProfileBlur(event: FocusEvent<HTMLDivElement>): void {
|
||||
if (!event.currentTarget.contains(event.relatedTarget)) {
|
||||
setIsProfileOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditProfile(): void {
|
||||
setIsProfileOpen(false);
|
||||
onEditProfile();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="resource-bar">
|
||||
<div className={`resource${goldFull
|
||||
? " resource-full"
|
||||
: ""}`}>
|
||||
<span className="resource-icon">{"🪙"}</span>
|
||||
<span className="resource-value">{formatNumber(gold)}</span>
|
||||
<span className="resource-label">{"Gold"}</span>
|
||||
{goldFull
|
||||
? <span className="resource-cap-badge" title={resourceFullTooltip}>
|
||||
{"FULL"}
|
||||
</span>
|
||||
<div
|
||||
className="resource-menu"
|
||||
onBlur={handleResourceBlur}
|
||||
>
|
||||
<button
|
||||
className={`resource resource-toggle${goldFull
|
||||
? " resource-full"
|
||||
: ""}`}
|
||||
onClick={handleToggleResources}
|
||||
title="Click to see all resources"
|
||||
type="button"
|
||||
>
|
||||
<span className="resource-icon">{"🪙"}</span>
|
||||
<span className="resource-value">{formatNumber(gold)}</span>
|
||||
<span className="resource-label">{"Gold"}</span>
|
||||
{goldFull
|
||||
? <span
|
||||
className="resource-cap-badge"
|
||||
title={resourceFullTooltip}
|
||||
>
|
||||
{"FULL"}
|
||||
</span>
|
||||
: null}
|
||||
{hiddenResourcesFull
|
||||
? <span
|
||||
className="resource-alert-dot"
|
||||
title={"One or more resources are full!"}
|
||||
/>
|
||||
: null}
|
||||
</button>
|
||||
{isResourcesOpen
|
||||
? <div className="resources-dropdown">
|
||||
<div className="resource">
|
||||
<span className="resource-icon">{"📈"}</span>
|
||||
<span className="resource-value">
|
||||
{formatNumber(goldPerSecond)}
|
||||
</span>
|
||||
<span className="resource-label">{"Gold/s"}</span>
|
||||
</div>
|
||||
<div className={`resource${essenceFull
|
||||
? " resource-full"
|
||||
: ""}`}>
|
||||
<span className="resource-icon">{"✨"}</span>
|
||||
<span className="resource-value">
|
||||
{formatNumber(essence)}
|
||||
</span>
|
||||
<span className="resource-label">{"Essence"}</span>
|
||||
{essenceFull
|
||||
? <span
|
||||
className="resource-cap-badge"
|
||||
title={resourceFullTooltip}
|
||||
>
|
||||
{"FULL"}
|
||||
</span>
|
||||
: null}
|
||||
</div>
|
||||
<div className={`resource${crystalsFull
|
||||
? " resource-full"
|
||||
: ""}`}>
|
||||
<span className="resource-icon">{"💎"}</span>
|
||||
<span className="resource-value">
|
||||
{formatNumber(crystals)}
|
||||
</span>
|
||||
<span className="resource-label">{"Crystals"}</span>
|
||||
{crystalsFull
|
||||
? <span
|
||||
className="resource-cap-badge"
|
||||
title={resourceFullTooltip}
|
||||
>
|
||||
{"FULL"}
|
||||
</span>
|
||||
: null}
|
||||
</div>
|
||||
<div className="resource">
|
||||
<span className="resource-icon">{"🔮"}</span>
|
||||
<span className="resource-value">
|
||||
{formatNumber(runestones)}
|
||||
</span>
|
||||
<span className="resource-label">{"Runestones"}</span>
|
||||
</div>
|
||||
<div className="resource">
|
||||
<span className="resource-icon">{"⚔️"}</span>
|
||||
<span className="resource-value">
|
||||
{formatNumber(partyCombatPower)}
|
||||
</span>
|
||||
<span className="resource-label">{"Combat Power"}</span>
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
<div className={`resource${essenceFull
|
||||
? " resource-full"
|
||||
: ""}`}>
|
||||
<span className="resource-icon">{"✨"}</span>
|
||||
<span className="resource-value">{formatNumber(essence)}</span>
|
||||
<span className="resource-label">{"Essence"}</span>
|
||||
{essenceFull
|
||||
? <span className="resource-cap-badge" title={resourceFullTooltip}>
|
||||
{"FULL"}
|
||||
</span>
|
||||
: null}
|
||||
</div>
|
||||
<div className={`resource${crystalsFull
|
||||
? " resource-full"
|
||||
: ""}`}>
|
||||
<span className="resource-icon">{"💎"}</span>
|
||||
<span className="resource-value">{formatNumber(crystals)}</span>
|
||||
<span className="resource-label">{"Crystals"}</span>
|
||||
{crystalsFull
|
||||
? <span className="resource-cap-badge" title={resourceFullTooltip}>
|
||||
{"FULL"}
|
||||
</span>
|
||||
: null}
|
||||
</div>
|
||||
<div className="resource">
|
||||
<span className="resource-icon">{"🔮"}</span>
|
||||
<span className="resource-value">{formatNumber(runestones)}</span>
|
||||
<span className="resource-label">{"Runestones"}</span>
|
||||
</div>
|
||||
<div className="resource">
|
||||
<span className="resource-icon">{"⚔️"}</span>
|
||||
<span className="resource-value">
|
||||
{formatNumber(partyCombatPower)}
|
||||
</span>
|
||||
<span className="resource-label">{"Combat Power"}</span>
|
||||
</div>
|
||||
{apotheosisCount > 0
|
||||
&& <div className="apotheosis-badge">
|
||||
{"✨ Apotheosis "}
|
||||
@@ -167,34 +251,7 @@ const ResourceBar = ({
|
||||
{prestigeCount}
|
||||
</div>
|
||||
}
|
||||
<div className="profile-buttons">
|
||||
<a
|
||||
className="profile-link-button"
|
||||
href="https://donate.nhcarrigan.com"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
title="Support the developer"
|
||||
>
|
||||
{"💜"} <span className="btn-label">{"Donate"}</span>
|
||||
</a>
|
||||
<a
|
||||
className="profile-link-button"
|
||||
href="https://chat.nhcarrigan.com"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
title="Join our Discord"
|
||||
>
|
||||
{"💬"} <span className="btn-label">{"Discord"}</span>
|
||||
</a>
|
||||
<a
|
||||
className="profile-link-button"
|
||||
href="https://support.nhcarrigan.com"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
title="Get support on our forum"
|
||||
>
|
||||
{"🆘"} <span className="btn-label">{"Support"}</span>
|
||||
</a>
|
||||
<div className="resource-bar-actions">
|
||||
{syncError === null
|
||||
? null
|
||||
: <span className="save-status save-error" title={syncError}>
|
||||
@@ -221,23 +278,69 @@ const ResourceBar = ({
|
||||
? "⏳"
|
||||
: "💾"}
|
||||
</button>
|
||||
<a
|
||||
className="profile-link-button"
|
||||
href={profileUrl}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
title="View your public profile"
|
||||
>
|
||||
{"👤"} <span className="btn-label">{"Profile"}</span>
|
||||
</a>
|
||||
<button
|
||||
className="profile-edit-button"
|
||||
onClick={onEditProfile}
|
||||
title="Edit your profile"
|
||||
type="button"
|
||||
>
|
||||
{"✏️"}
|
||||
</button>
|
||||
{avatarUrl === null
|
||||
? null
|
||||
: <div
|
||||
className="profile-menu"
|
||||
onBlur={handleProfileBlur}
|
||||
>
|
||||
<button
|
||||
className="profile-avatar-button"
|
||||
onClick={handleToggleProfile}
|
||||
title="Account"
|
||||
type="button"
|
||||
>
|
||||
<img
|
||||
alt="Profile"
|
||||
className="profile-avatar-img"
|
||||
src={avatarUrl}
|
||||
/>
|
||||
</button>
|
||||
{isProfileOpen
|
||||
? <div className="profile-dropdown">
|
||||
<a
|
||||
className="profile-dropdown-item"
|
||||
href={profileUrl}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{"👤 View Profile"}
|
||||
</a>
|
||||
<button
|
||||
className="profile-dropdown-item"
|
||||
onClick={handleEditProfile}
|
||||
type="button"
|
||||
>
|
||||
{"✏️ Edit Profile"}
|
||||
</button>
|
||||
<hr className="profile-dropdown-divider" />
|
||||
<a
|
||||
className="profile-dropdown-item"
|
||||
href="https://donate.nhcarrigan.com"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{"💜 Donate"}
|
||||
</a>
|
||||
<a
|
||||
className="profile-dropdown-item"
|
||||
href="https://chat.nhcarrigan.com"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{"💬 Discord"}
|
||||
</a>
|
||||
<a
|
||||
className="profile-dropdown-item"
|
||||
href="https://support.nhcarrigan.com"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{"🆘 Support"}
|
||||
</a>
|
||||
</div>
|
||||
: null}
|
||||
</div>}
|
||||
</div>
|
||||
</header>
|
||||
{anyFull
|
||||
|
||||
@@ -1094,11 +1094,7 @@ export const GameProvider = ({
|
||||
return adventurer.unlocked && next.resources.gold >= cost;
|
||||
}).
|
||||
sort((adventurerA, adventurerB) => {
|
||||
const costA
|
||||
= adventurerA.baseCost * Math.pow(1.15, adventurerA.count);
|
||||
const costB
|
||||
= adventurerB.baseCost * Math.pow(1.15, adventurerB.count);
|
||||
return costB - costA;
|
||||
return adventurerB.combatPower - adventurerA.combatPower;
|
||||
});
|
||||
if (bestAdventurer !== undefined) {
|
||||
const purchaseCost
|
||||
@@ -1285,7 +1281,26 @@ export const GameProvider = ({
|
||||
if (availableBoss !== undefined) {
|
||||
const { id: bossId, name: bossName } = availableBoss;
|
||||
isAutoBossingReference.current = true;
|
||||
void challengeBossApi({ bossId }).
|
||||
const syncBeforeBoss
|
||||
= stateReference.current !== null && !isSyncingReference.current
|
||||
? saveGame({
|
||||
state: stateReference.current,
|
||||
...signatureReference.current === null
|
||||
? {}
|
||||
: { signature: signatureReference.current },
|
||||
}).then((response) => {
|
||||
if (response.signature !== undefined) {
|
||||
signatureReference.current = response.signature;
|
||||
localStorage.setItem(
|
||||
"elysium_save_signature",
|
||||
response.signature,
|
||||
);
|
||||
}
|
||||
})
|
||||
: Promise.resolve();
|
||||
void syncBeforeBoss.then(async() => {
|
||||
return await challengeBossApi({ bossId });
|
||||
}).
|
||||
then((result) => {
|
||||
setState((previous) => {
|
||||
if (previous === null) {
|
||||
|
||||
@@ -123,6 +123,78 @@ const capResource = (value: number): number => {
|
||||
return Math.min(value, RESOURCE_CAP);
|
||||
};
|
||||
|
||||
/**
|
||||
* Pure function — applies one game tick to the state.
|
||||
* DeltaSeconds: time elapsed since last tick.
|
||||
* Returns a new GameState (does not mutate the original).
|
||||
* @param state - The current game state.
|
||||
* @param deltaSeconds - Time elapsed since last tick in seconds.
|
||||
* @returns A new GameState with the tick applied.
|
||||
*/
|
||||
/**
|
||||
* Computes the effective gold earned per second across all adventurers,
|
||||
* including all active multipliers (upgrades, prestige, equipment, etc.).
|
||||
* @param state - The current game state.
|
||||
* @returns Gold per second as a number.
|
||||
*/
|
||||
export const computeGoldPerSecond = (state: GameState): number => {
|
||||
const equippedItems: Array<Equipment> = state.equipment.filter((item) => {
|
||||
return item.equipped;
|
||||
});
|
||||
const equipmentGoldMultiplier = equippedItems.reduce((mult, item) => {
|
||||
return mult * (item.bonus.goldMultiplier ?? 1);
|
||||
}, 1);
|
||||
const setGoldMultiplier = computeSetBonuses(
|
||||
equippedItems.map((item) => {
|
||||
return item.id;
|
||||
}),
|
||||
EQUIPMENT_SETS,
|
||||
).goldMultiplier;
|
||||
|
||||
const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1;
|
||||
const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1;
|
||||
const craftedGoldMultiplier = state.exploration?.craftedGoldMultiplier ?? 1;
|
||||
const companionBonus = getActiveCompanionBonus(
|
||||
state.companions?.activeCompanionId,
|
||||
state.companions?.unlockedCompanionIds ?? [],
|
||||
);
|
||||
const companionGoldMult
|
||||
= companionBonus?.type === "passiveGold"
|
||||
? 1 + companionBonus.value
|
||||
: 1;
|
||||
|
||||
let goldPerSecond = 0;
|
||||
for (const adventurer of state.adventurers) {
|
||||
if (!adventurer.unlocked || adventurer.count === 0) {
|
||||
continue;
|
||||
}
|
||||
const upgradeMultiplier = state.upgrades.
|
||||
filter((upgrade) => {
|
||||
const isGlobal = upgrade.target === "global";
|
||||
const isThisAdventurer
|
||||
= upgrade.target === "adventurer"
|
||||
&& upgrade.adventurerId === adventurer.id;
|
||||
return upgrade.purchased && (isGlobal || isThisAdventurer);
|
||||
}).
|
||||
reduce((mult, upgrade) => {
|
||||
return mult * upgrade.multiplier;
|
||||
}, 1);
|
||||
const contribution
|
||||
= adventurer.goldPerSecond
|
||||
* adventurer.count
|
||||
* upgradeMultiplier
|
||||
* state.prestige.productionMultiplier
|
||||
* runestonesIncome
|
||||
* echoIncome
|
||||
* equipmentGoldMultiplier
|
||||
* setGoldMultiplier
|
||||
* craftedGoldMultiplier
|
||||
* companionGoldMult;
|
||||
goldPerSecond = goldPerSecond + contribution;
|
||||
}
|
||||
return goldPerSecond;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pure function — applies one game tick to the state.
|
||||
* DeltaSeconds: time elapsed since last tick.
|
||||
|
||||
+124
-43
@@ -116,6 +116,66 @@ body::before {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── Resource toggle + dropdown ─────────────────────────────────────────── */
|
||||
|
||||
.resource-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.resource-toggle {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(147, 51, 234, 0.4);
|
||||
border-radius: 0.5rem;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
padding: 0.3rem 0.6rem;
|
||||
position: relative;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.resource-toggle:hover {
|
||||
background: rgba(147, 51, 234, 0.2);
|
||||
border-color: var(--colour-primary);
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -1492,57 +1552,87 @@ body::before {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* ── Profile buttons in ResourceBar ────────────────────────────────────── */
|
||||
/* ── Resource bar actions (save + profile menu) ─────────────────────────── */
|
||||
|
||||
.profile-buttons {
|
||||
.resource-bar-actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.profile-link-button {
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(147, 51, 234, 0.4);
|
||||
border-radius: 1rem;
|
||||
color: var(--colour-text-muted);
|
||||
display: flex;
|
||||
font-size: 0.8rem;
|
||||
gap: 0.3rem;
|
||||
padding: 0.3rem 0.8rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
.profile-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.profile-link-button:hover {
|
||||
background: rgba(147, 51, 234, 0.2);
|
||||
border-color: var(--colour-primary);
|
||||
color: var(--colour-text);
|
||||
}
|
||||
|
||||
.profile-edit-button {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(147, 51, 234, 0.4);
|
||||
.profile-avatar-button {
|
||||
background: none;
|
||||
border: 2px solid rgba(147, 51, 234, 0.4);
|
||||
border-radius: 50%;
|
||||
color: var(--colour-text-muted);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
height: 2rem;
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
transition: all 0.2s;
|
||||
transition: border-color 0.2s;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.profile-edit-button:hover {
|
||||
background: rgba(147, 51, 234, 0.2);
|
||||
.profile-avatar-button:hover {
|
||||
border-color: var(--colour-primary);
|
||||
}
|
||||
|
||||
.profile-avatar-img {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.profile-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;
|
||||
min-width: 10rem;
|
||||
padding: 0.25rem;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 0.4rem);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.profile-dropdown-item {
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0.35rem;
|
||||
color: var(--colour-text-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-family: inherit;
|
||||
font-size: 0.85rem;
|
||||
gap: 0.4rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.profile-dropdown-item:hover {
|
||||
background: rgba(147, 51, 234, 0.15);
|
||||
color: var(--colour-text);
|
||||
}
|
||||
|
||||
.profile-dropdown-divider {
|
||||
border: none;
|
||||
border-top: 1px solid rgba(147, 51, 234, 0.2);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.save-status {
|
||||
color: var(--colour-text-muted);
|
||||
font-size: 0.75rem;
|
||||
@@ -3167,10 +3257,10 @@ body::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Profile buttons fill their own row, aligned right */
|
||||
.profile-buttons {
|
||||
margin-left: 0;
|
||||
/* Resource bar actions fill their own row, aligned right */
|
||||
.resource-bar-actions {
|
||||
justify-content: flex-end;
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -3240,15 +3330,6 @@ body::before {
|
||||
|
||||
/* --- Small mobile (≤ 480px) --------------------------- */
|
||||
@media (max-width: 480px) {
|
||||
/* Icon-only profile link buttons to save horizontal space */
|
||||
.btn-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profile-link-button {
|
||||
padding: 0.3rem 0.5rem;
|
||||
}
|
||||
|
||||
/* Slightly smaller tab buttons */
|
||||
.tab-button {
|
||||
font-size: 0.8rem;
|
||||
|
||||
Reference in New Issue
Block a user