Files
fabula-ultima-html/fabula-ultima-sheet.html
Drew Malzahn 52ee6ea26b 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>
2026-06-05 02:28:12 +00:00

473 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fabula Ultima — Character Sheet</title>
<link
href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Crimson+Text:ital,wght@0,400;0,600;1,400&family=Inconsolata:wght@400;600&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="fabula-ultima-sheet.css">
<script>(function () { var t = localStorage.getItem('fabulaUltimaTheme') || (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'); document.documentElement.dataset.theme = t; })()</script>
</head>
<body>
<header>
<div class="logo">Fabula Ultima</div>
<div class="tabs">
<button class="tab active" onclick="switchTab('main')">Character</button>
<button class="tab" onclick="switchTab('classes')">Classes</button>
<button class="tab" onclick="switchTab('spells')">Arcana & Spells</button>
<button class="tab" onclick="switchTab('manage')">Manage</button>
</div>
<div class="toolbar">
<span class="save-status" id="saveStatus">Saved!</span>
<button class="btn-theme" id="themeToggle" onclick="toggleTheme()">☀ Light</button>
</div>
</header>
<div id="urlBanner" class="url-banner" style="display:none">
<span class="url-banner-icon"></span>
Viewing a shared character — auto-save is disabled.
Use <strong>Manage → Save to Browser</strong> to keep any changes.
</div>
<!-- ══════════════════════════════════════════════════
PAGE 1: MAIN CHARACTER
══════════════════════════════════════════════════ -->
<div class="page active" id="page-main">
<!-- Row 1: Identity + Level -->
<div class="grid-2" style="margin-bottom:20px;">
<!-- Identity -->
<div class="section">
<div class="section-title"><span class="icon"></span> Identity & Traits</div>
<div class="field-row">
<div class="field" style="flex:2">
<label>Name</label>
<input type="text" id="charName" placeholder="Character name…">
</div>
<div class="field">
<label>Pronouns</label>
<input type="text" id="charPronouns" placeholder="they/them">
</div>
</div>
<div class="field">
<label>Identity</label>
<input type="text" id="charIdentity" placeholder="Who are you?">
</div>
<div class="field-row">
<div class="field">
<label>Theme</label>
<input type="text" id="charTheme" placeholder="Your theme…">
</div>
<div class="field">
<label>Origin</label>
<input type="text" id="charOrigin" placeholder="Where from?">
</div>
</div>
<div class="field">
<label>Traits (comma-separated)</label>
<textarea id="charTraits" placeholder="Brave, Reckless, Loyal to a fault…"
style="min-height:55px;"></textarea>
</div>
</div>
<!-- Level + XP + Defenses -->
<div style="display:flex; flex-direction:column; gap:16px;">
<div class="section">
<div class="section-title"><span class="icon"></span> Level & Experience</div>
<div class="grid-2" style="gap:14px;">
<div class="level-display">
<span class="level-num" id="levelDisplay">1</span>
<span class="level-text">Character Level</span>
<button class="add-btn" style="margin-top:10px; width:100%; justify-content:center;"
onclick="adjustLevel(1)">+ Level Up</button>
<button class="add-btn"
style="margin-top:4px; width:100%; justify-content:center; border-color:var(--border-bright); color:var(--text-dim);"
onclick="adjustLevel(-1)"> Level Down</button>
</div>
<div>
<div class="field">
<label>Experience Points (XP)</label>
<input type="number" id="xpCurrent" value="0" min="0" oninput="updateXPBar()">
</div>
<div class="xp-bar-wrap">
<div class="xp-bar" id="xpBar" style="width:0%"></div>
</div>
<div class="xp-label">
<span id="xpVal">0 XP</span>
<span>10 XP = Level</span>
</div>
<div class="field" style="margin-top:12px;">
<label>Zenit (currency)</label>
<input type="number" id="zenit" value="0" min="0">
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title"><span class="icon"></span> Defenses</div>
<div class="def-row">
<div class="def-block">
<label>Initiative Mod</label>
<input type="number" id="initMod" value="0">
</div>
<div class="def-block">
<label>Defense</label>
<input type="number" id="defense" value="0">
</div>
<div class="def-block">
<label>Magic Defense</label>
<input type="number" id="magDef" value="0">
</div>
</div>
</div>
</div>
</div>
<!-- Row 2: Attributes + Status + HP/MP/IP -->
<div class="grid-3" style="margin-bottom:20px;">
<!-- Attributes -->
<div class="section">
<div class="section-title"><span class="icon"></span> Attributes</div>
<div class="attr-grid">
<div class="attr-block">
<div class="attr-name">Dexterity</div>
<div class="attr-inputs">
<div style="flex:1">
<label style="font-size:0.48rem;">Base</label>
<input type="number" id="dex-base" value="6" min="6" max="12" oninput="recalcStats()">
</div>
<div class="attr-sep"></div>
<div style="flex:1">
<label style="font-size:0.48rem;">Current</label>
<input type="number" id="dex-cur" value="6" min="6" max="12">
</div>
</div>
</div>
<div class="attr-block">
<div class="attr-name">Insight</div>
<div class="attr-inputs">
<div style="flex:1">
<label style="font-size:0.48rem;">Base</label>
<input type="number" id="ins-base" value="6" min="6" max="12" oninput="recalcStats()">
</div>
<div class="attr-sep"></div>
<div style="flex:1">
<label style="font-size:0.48rem;">Current</label>
<input type="number" id="ins-cur" value="6" min="6" max="12">
</div>
</div>
</div>
<div class="attr-block">
<div class="attr-name">Might</div>
<div class="attr-inputs">
<div style="flex:1">
<label style="font-size:0.48rem;">Base</label>
<input type="number" id="mig-base" value="6" min="6" max="12" oninput="recalcStats()">
</div>
<div class="attr-sep"></div>
<div style="flex:1">
<label style="font-size:0.48rem;">Current</label>
<input type="number" id="mig-cur" value="6" min="6" max="12">
</div>
</div>
</div>
<div class="attr-block">
<div class="attr-name">Willpower</div>
<div class="attr-inputs">
<div style="flex:1">
<label style="font-size:0.48rem;">Base</label>
<input type="number" id="wlp-base" value="6" min="6" max="12" oninput="recalcStats()">
</div>
<div class="attr-sep"></div>
<div style="flex:1">
<label style="font-size:0.48rem;">Current</label>
<input type="number" id="wlp-cur" value="6" min="6" max="12">
</div>
</div>
</div>
</div>
</div>
<!-- Status Effects -->
<div class="section">
<div class="section-title"><span class="icon"></span> Status Effects</div>
<div class="status-grid" id="statusGrid">
<!-- generated by JS -->
</div>
</div>
<!-- HP / MP / IP -->
<div class="section">
<div class="section-title"><span class="icon"></span> Hit, Mind & Inventory Points</div>
<div class="vital-block">
<div class="vital-label hp">HP</div>
<div style="flex:1">
<div class="vital-formula">MIG×5 + Level + Other</div>
<div class="vital-inputs">
<input type="number" id="hpMax" placeholder="Max" oninput="updateCrisis()">
<div class="vital-sep">/ </div>
<input type="number" id="hpCur" placeholder="Cur" oninput="updateCrisis()">
<div class="crisis-badge" id="crisisBadge" style="display:none">CRISIS</div>
</div>
</div>
<button class="add-btn" style="padding:4px 8px;" onclick="calcHP()"
title="Auto-calculate from Might">Calc</button>
</div>
<div class="vital-block">
<div class="vital-label mp">MP</div>
<div style="flex:1">
<div class="vital-formula">WLP×5 + Level + Other</div>
<div class="vital-inputs">
<input type="number" id="mpMax" placeholder="Max">
<div class="vital-sep">/</div>
<input type="number" id="mpCur" placeholder="Cur">
</div>
</div>
<button class="add-btn" style="padding:4px 8px;" onclick="calcMP()"
title="Auto-calculate from Willpower">Calc</button>
</div>
<div class="vital-block">
<div class="vital-label ip">IP</div>
<div style="flex:1">
<div class="vital-formula">6 + Other</div>
<div class="vital-inputs">
<input type="number" id="ipMax" value="6" placeholder="Max">
<div class="vital-sep">/</div>
<input type="number" id="ipCur" value="6" placeholder="Cur">
</div>
</div>
</div>
</div>
</div>
<!-- Row 3: Fabula Points + Bonds -->
<div class="grid-2" style="margin-bottom:20px;">
<!-- Fabula Points -->
<div class="section">
<div class="section-title"><span class="icon"></span> Fabula Points</div>
<div style="display:flex; align-items:center; gap:14px; flex-wrap:wrap;">
<div>
<label>Current FP</label>
<input type="number" id="fpCount" value="0" min="0" max="20"
style="width:70px; text-align:center; font-size:1.4rem; font-family:var(--font-mono);"
oninput="renderFP()">
</div>
<div class="fp-pips" id="fpPips"></div>
</div>
<div class="fp-rules">
<div class="fp-rule"><strong>+1 FP</strong> if you have none at start of session.</div>
<div class="fp-rule"><strong>+1 FP</strong> when a Villain makes an entrance.</div>
<div class="fp-rule"><strong>+1 FP</strong> when you fumble a Check.</div>
<div class="fp-rule"><strong>+2 FP</strong> if you surrender at zero HP.</div>
<div class="fp-rule" style="margin-top:6px;"><strong>Spend 1 FP</strong> to invoke a trait: reroll one or both
dice.</div>
<div class="fp-rule"><strong>Spend 1 FP</strong> to invoke a bond: add its strength to the result.</div>
<div class="fp-rule"><strong>Spend 1 FP</strong> to alter the story.</div>
</div>
</div>
<!-- Bonds -->
<div class="section">
<div class="section-title"><span class="icon"></span> Bonds</div>
<div id="bondsContainer"></div>
</div>
</div>
<!-- Row 4: Equipment + Backpack -->
<div class="grid-2" style="margin-bottom:20px;">
<!-- Equipment -->
<div class="section">
<div class="section-title"><span class="icon"></span> Equipment</div>
<div class="martial-row" id="martialRow">
<div class="martial-item" onclick="toggleMartial(this)">
<div class="martial-box"></div><span>Martial Armor</span>
</div>
<div class="martial-item" onclick="toggleMartial(this)">
<div class="martial-box"></div><span>Martial Shields</span>
</div>
<div class="martial-item" onclick="toggleMartial(this)">
<div class="martial-box"></div><span>Martial Melee</span>
</div>
<div class="martial-item" onclick="toggleMartial(this)">
<div class="martial-box"></div><span>Martial Ranged</span>
</div>
</div>
<div style="margin-top:14px;">
<div class="equip-row">
<div class="equip-slot">Accessory</div>
<div class="equip-fields">
<input type="text" id="acc-name" placeholder="Item name">
<input type="text" id="acc-desc" placeholder="Description / effect">
</div>
</div>
<div class="equip-row">
<div class="equip-slot">Armor</div>
<div class="equip-fields">
<input type="text" id="arm-name" placeholder="Item name">
<input type="text" id="arm-desc" placeholder="Defense bonus / effect">
</div>
</div>
<div class="equip-row">
<div class="equip-slot">Main Hand</div>
<div class="equip-fields">
<input type="text" id="mh-name" placeholder="Weapon name">
<input type="text" id="mh-desc" placeholder="Damage / effect">
</div>
</div>
<div class="equip-row">
<div class="equip-slot">Off-Hand</div>
<div class="equip-fields">
<input type="text" id="oh-name" placeholder="Weapon / shield">
<input type="text" id="oh-desc" placeholder="Damage / effect">
</div>
</div>
</div>
</div>
<!-- Backpack & Notes -->
<div class="section">
<div class="section-title"><span class="icon"></span> Backpack & Notes</div>
<textarea id="backpack" placeholder="Items, notes, lore…" style="min-height:200px;"></textarea>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════════════
PAGE 2: CLASSES
══════════════════════════════════════════════════ -->
<div class="page" id="page-classes">
<div class="grid-2" style="margin-bottom:20px;">
<div class="section">
<div class="section-title"><span class="icon"></span> Primary Classes (up to 3 levels each)</div>
<div id="primaryClassContainer"></div>
<button class="add-btn" onclick="addPrimaryClass()">+ Add Primary Class</button>
</div>
<div class="section">
<div class="section-title"><span class="icon"></span> Other Classes (max 3 non-mastered)</div>
<div id="otherClassContainer"></div>
<button class="add-btn" onclick="addOtherClass()">+ Add Class</button>
</div>
</div>
<div class="section">
<div class="section-title"><span class="icon"></span> Heroic Skills</div>
<textarea id="heroicSkills" placeholder="Record your heroic skill abilities here…"
style="min-height:100px;"></textarea>
</div>
</div>
<!-- ══════════════════════════════════════════════════
PAGE 3: ARCANA & SPELLS
══════════════════════════════════════════════════ -->
<div class="page" id="page-spells">
<div class="section" style="margin-bottom:20px;">
<div class="section-title"><span class="icon"></span> Arcana & Spells</div>
<table class="spells-table" id="spellsTable">
<thead>
<tr>
<th class="spell-name-col">Name / Notes</th>
<th class="spell-mp-col">MP Cost</th>
<th class="spell-targets-col">Targets</th>
<th class="spell-dur-col">Duration</th>
<th class="spell-del-col"></th>
</tr>
</thead>
<tbody id="spellsBody"></tbody>
</table>
<button class="add-btn" onclick="addSpell()" style="margin-top:10px;">+ Add Spell / Arcana</button>
</div>
<div class="section">
<div class="section-title"><span class="icon"></span> Rituals</div>
<div class="disciplines-row" id="disciplinesRow">
<div class="disc-item" onclick="toggleDisc(this)">
<div class="disc-box"></div><span>Arcanism</span>
</div>
<div class="disc-item" onclick="toggleDisc(this)">
<div class="disc-box"></div><span>Chimerism</span>
</div>
<div class="disc-item" onclick="toggleDisc(this)">
<div class="disc-box"></div><span>Elementalism</span>
</div>
<div class="disc-item" onclick="toggleDisc(this)">
<div class="disc-box"></div><span>Entropism</span>
</div>
<div class="disc-item" onclick="toggleDisc(this)">
<div class="disc-box"></div><span>Ritualism</span>
</div>
<div class="disc-item" onclick="toggleDisc(this)">
<div class="disc-box"></div><span>Spiritism</span>
</div>
</div>
<textarea id="ritualsNotes" placeholder="Record ritual details, components, and notes here…"
style="min-height:120px;"></textarea>
</div>
</div>
<!-- ══════════════════════════════════════════════════
PAGE 4: MANAGE
══════════════════════════════════════════════════ -->
<div class="page" id="page-manage">
<div class="manage-grid">
<div class="section">
<div class="section-title"><span class="icon"></span> Local Save</div>
<p class="manage-desc">Save your character sheet to your browser's local storage, or load a previously saved sheet.</p>
<div class="manage-btn-row">
<button class="btn-save btn-lg" onclick="saveSheet()">✦ Save to Browser</button>
<button class="btn-load btn-lg" onclick="loadSheet()">↑ Load from Browser</button>
</div>
</div>
<div class="section">
<div class="section-title"><span class="icon"></span> JSON File</div>
<p class="manage-desc">Export your character to a JSON file for backup or sharing, or import from a previously exported file.</p>
<div class="manage-btn-row">
<button class="btn-save btn-export btn-lg" onclick="exportSheet()">↓ Export JSON</button>
<button class="btn-load btn-import btn-lg" onclick="importSheet()">↑ Import JSON</button>
<input type="file" id="importFileInput" accept=".json,application/json" style="display:none"
onchange="handleImportFile(this)">
</div>
</div>
<div class="section col-span-2">
<div class="section-title"><span class="icon"></span> Share via URL</div>
<p class="manage-desc">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.</p>
<div class="manage-btn-row">
<button class="btn-save btn-export btn-lg" onclick="copyShareURL()">⎘ Copy URL</button>
<span class="save-status" id="copyStatus">Copied!</span>
</div>
</div>
</div>
</div>
<script src="fabula-ultima-sheet.js"></script>
</body>
</html>