diff --git a/fabula-ultima-sheet.css b/fabula-ultima-sheet.css index f9445d3..2e558d3 100644 --- a/fabula-ultima-sheet.css +++ b/fabula-ultima-sheet.css @@ -964,6 +964,55 @@ input[type="number"] { font-weight: 600; } +/* ── URL BANNER ──────────────────────────────────── */ +.url-banner { + background: var(--surface2); + border-bottom: 1px solid var(--teal-dim); + padding: 10px 32px; + font-family: var(--font-body); + font-size: 0.9rem; + color: var(--text-dim); + display: flex; + align-items: center; + gap: 10px; +} + +.url-banner strong { + color: var(--text); +} + +.url-banner-icon { + color: var(--teal); + font-size: 0.85rem; + flex-shrink: 0; +} + +/* ── MANAGE PAGE ─────────────────────────────────── */ +.manage-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + max-width: 800px; +} + +.manage-desc { + font-size: 0.9rem; + color: var(--text-dim); + line-height: 1.6; + margin-bottom: 18px; +} + +.manage-btn-row { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.btn-lg { + font-size: 0.6rem; + padding: 11px 22px; +} + /* ── LIGHT THEME ─────────────────────────────────── */ [data-theme="light"] { --bg: #f4efe4; diff --git a/fabula-ultima-sheet.html b/fabula-ultima-sheet.html index 69062b8..3a86b25 100644 --- a/fabula-ultima-sheet.html +++ b/fabula-ultima-sheet.html @@ -20,21 +20,20 @@ +
- - -
- - - Saved! -
+ + @@ -430,6 +429,44 @@ + +
+
+ +
+
Local Save
+

Save your character sheet to your browser's local storage, or load a previously saved sheet.

+
+ + +
+
+ +
+
JSON File
+

Export your character to a JSON file for backup or sharing, or import from a previously exported file.

+
+ + + +
+
+ +
+
Share via URL
+

Encode your character's current state into a shareable link. Anyone who opens the link will see your character — auto-save is disabled for viewers.

+
+ + Copied! +
+
+ +
+
+ diff --git a/fabula-ultima-sheet.js b/fabula-ultima-sheet.js index 39ecb6a..e5cdaa3 100644 --- a/fabula-ultima-sheet.js +++ b/fabula-ultima-sheet.js @@ -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();