feat: add number format config, resource cap, and modal scroll fix

- Add user-configurable number format (suffix/scientific/engineering)
  - Suffix: K/M/B/T through Dc (1e33), then letter-based a/b/c... indefinitely
  - Scientific: 1.23e15 style via toExponential
  - Engineering: exponent always a multiple of 3 (1.23E15)
  - Stored in ProfileSettings, fetched from profile API on load
  - Picker UI in EditProfileModal with live examples
- Cap all resource accumulation at 1e300 (RESOURCE_CAP constant)
  - Per-resource FULL badge with tooltip in ResourceBar
  - Amber notice strip when any resource is at cap
  - handleClick also respects the cap
- Make EditProfileModal scrollable with viewport margin
  - Flex column layout with sticky header, scrollable form body
  - Bio textarea preserved as resizable with min-height
- Fix ReferenceError: formatNumber not defined in BossPanel/AchievementPanel
  - Pass formatNumber as prop to BossCard and AchievementCard
  - Pass formatNumber as parameter to conditionDescription
This commit is contained in:
2026-03-06 18:59:43 -08:00
committed by Naomi Carrigan
parent 24beaf3131
commit 5ad2c44399
15 changed files with 290 additions and 65 deletions
@@ -1,10 +1,9 @@
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 conditionDescription = (achievement: Achievement, formatNumber: (n: number) => string): string => {
const { condition } = achievement;
switch (condition.type) {
case "totalGoldEarned":
@@ -26,9 +25,10 @@ const conditionDescription = (achievement: Achievement): string => {
interface AchievementCardProps {
achievement: Achievement;
formatNumber: (n: number) => string;
}
const AchievementCard = ({ achievement }: AchievementCardProps): React.JSX.Element => {
const AchievementCard = ({ achievement, formatNumber }: AchievementCardProps): React.JSX.Element => {
const isUnlocked = achievement.unlockedAt !== null;
return (
@@ -37,7 +37,7 @@ const AchievementCard = ({ achievement }: AchievementCardProps): React.JSX.Eleme
<div className="achievement-info">
<h3>{achievement.name}</h3>
<p>{achievement.description}</p>
<p className="achievement-condition">{conditionDescription(achievement)}</p>
<p className="achievement-condition">{conditionDescription(achievement, formatNumber)}</p>
{achievement.reward?.crystals != null && (
<p className="achievement-reward">💎 +{achievement.reward.crystals} Crystals</p>
)}
@@ -54,7 +54,7 @@ const AchievementCard = ({ achievement }: AchievementCardProps): React.JSX.Eleme
};
export const AchievementPanel = (): React.JSX.Element => {
const { state } = useGame();
const { state, formatNumber } = useGame();
const [showLocked, setShowLocked] = useState(true);
if (!state) return <section className="panel"><p>Loading...</p></section>;
@@ -79,7 +79,7 @@ export const AchievementPanel = (): React.JSX.Element => {
</p>
<div className="achievement-list">
{visible.map((achievement) => (
<AchievementCard key={achievement.id} achievement={achievement} />
<AchievementCard key={achievement.id} achievement={achievement} formatNumber={formatNumber} />
))}
</div>
</section>