feat: Add Manage page, light/dark theme toggle, and URL sharing

- Move Save, Load, Export, Import buttons to a dedicated Manage page
- Add light theme with warm parchment palette; auto-detect via prefers-color-scheme, persisted to localStorage
- Add URL sharing: sheet state is deflate-raw compressed, base64-encoded, and stored in ?c= parameter; auto-save is suppressed when viewing a shared URL
- Extract JS to fabula-ultima-sheet.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 02:28:12 +00:00
parent 9ade0820d6
commit 52ee6ea26b
3 changed files with 153 additions and 12 deletions

View File

@@ -1,4 +1,5 @@
// ── STATE ──────────────────────────────────────────
let urlMode = false;
let level = 1;
let fpCount = 0;
let primaryClasses = [];
@@ -25,7 +26,7 @@ function switchTab(tab) {
}
// ── INIT ───────────────────────────────────────────
function init() {
async function init() {
const savedTheme = localStorage.getItem('fabulaUltimaTheme')
|| (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
document.getElementById('themeToggle').textContent = savedTheme === 'light' ? '☾ Dark' : '☀ Light';
@@ -36,7 +37,50 @@ function init() {
renderOtherClasses();
renderSpells();
updateXPBar();
tryAutoLoad();
if (!await tryLoadFromURL()) tryAutoLoad();
}
// ── URL STATE ──────────────────────────────────────
async function compressToBase64(str) {
const stream = new CompressionStream('deflate-raw');
const writer = stream.writable.getWriter();
writer.write(new TextEncoder().encode(str));
writer.close();
const buf = await new Response(stream.readable).arrayBuffer();
const bytes = new Uint8Array(buf);
let binary = '';
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
return btoa(binary);
}
async function decompressFromBase64(b64) {
try {
const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
const stream = new DecompressionStream('deflate-raw');
const writer = stream.writable.getWriter();
writer.write(bytes);
writer.close();
const buf = await new Response(stream.readable).arrayBuffer();
return new TextDecoder().decode(buf);
} catch {
// Fall back to legacy uncompressed URLs
return decodeURIComponent(escape(atob(b64)));
}
}
async function tryLoadFromURL() {
const encoded = new URLSearchParams(window.location.search).get('c');
if (!encoded) return false;
try {
const json = await decompressFromBase64(encoded);
applyData(JSON.parse(json));
urlMode = true;
document.getElementById('urlBanner').style.display = 'block';
return true;
} catch (e) {
console.warn('Could not load state from URL:', e);
return false;
}
}
// ── STATUS EFFECTS ─────────────────────────────────
@@ -236,7 +280,8 @@ function collectData() {
};
}
function saveSheet() {
function saveSheet(isAuto = false) {
if (isAuto && urlMode) return;
const data = collectData();
localStorage.setItem('fabulaUltimaSheet', JSON.stringify(data));
const st = document.getElementById('saveStatus');
@@ -350,6 +395,16 @@ function handleImportFile(input) {
reader.readAsText(file);
}
async function copyShareURL() {
const json = JSON.stringify(collectData());
const encoded = await compressToBase64(json);
const url = window.location.origin + window.location.pathname + '?c=' + encodeURIComponent(encoded);
await navigator.clipboard.writeText(url);
const st = document.getElementById('copyStatus');
st.classList.add('show');
setTimeout(() => st.classList.remove('show'), 2000);
}
// ── THEME ──────────────────────────────────────────
function toggleTheme() {
const html = document.documentElement;
@@ -360,6 +415,6 @@ function toggleTheme() {
}
// Auto-save every 30s
setInterval(saveSheet, 30000);
setInterval(() => saveSheet(true), 30000);
init();