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,4 +1,4 @@
import type { ProfileSettings } from "@elysium/types";
import type { NumberFormat, ProfileSettings } from "@elysium/types";
import { DEFAULT_PROFILE_SETTINGS } from "@elysium/types";
import { useEffect, useState } from "react";
import { updateProfile } from "../../api/client.js";
@@ -20,12 +20,15 @@ const STAT_TOGGLES: { key: keyof ProfileSettings; label: string; icon: string }[
];
export const EditProfileModal = ({ onClose }: EditProfileModalProps): React.JSX.Element => {
const { state } = useGame();
const { state, numberFormat: currentNumberFormat, setNumberFormat } = useGame();
const player = state?.player;
const [characterName, setCharacterName] = useState(player?.characterName ?? "");
const [bio, setBio] = useState("");
const [settings, setSettings] = useState<ProfileSettings>({ ...DEFAULT_PROFILE_SETTINGS });
const [settings, setSettings] = useState<ProfileSettings>({
...DEFAULT_PROFILE_SETTINGS,
numberFormat: currentNumberFormat,
});
const [loadingProfile, setLoadingProfile] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -59,6 +62,7 @@ export const EditProfileModal = ({ onClose }: EditProfileModalProps): React.JSX.
setError(null);
try {
await updateProfile({ characterName, bio, profileSettings: settings });
setNumberFormat(settings.numberFormat);
setSaved(true);
setTimeout(onClose, 900);
} catch (err: unknown) {
@@ -139,6 +143,30 @@ export const EditProfileModal = ({ onClose }: EditProfileModalProps): React.JSX.
</div>
</div>
<div className="edit-profile-section">
<p className="edit-profile-label">Number Format</p>
<p className="edit-profile-sublabel">How large numbers appear across the game.</p>
<div className="number-format-picker">
{(
[
{ value: "suffix", label: "Suffix", example: "1.23Qa" },
{ value: "scientific", label: "Scientific", example: "1.23e15" },
{ value: "engineering", label: "Engineering", example: "1.23E15" },
] as { value: NumberFormat; label: string; example: string }[]
).map(({ value, label, example }) => (
<button
key={value}
className={`number-format-btn ${settings.numberFormat === value ? "number-format-active" : ""}`}
onClick={() => { setSettings((prev) => ({ ...prev, numberFormat: value })); }}
type="button"
>
<span className="number-format-label">{label}</span>
<span className="number-format-example">{example}</span>
</button>
))}
</div>
</div>
{error && <p className="edit-profile-error">{error}</p>}
<div className="edit-profile-actions">