Integrate illustrated UI icons, cartoon parchment and offline asset caching

This commit is contained in:
Admin 2026-10-07 00:28:25 +00:00
parent 6a02db18b1
commit 1af728830e
6 changed files with 117 additions and 15 deletions

View File

@ -1,4 +1,36 @@
const CACHE='character-shell-v1';
self.addEventListener('activate',e=>e.waitUntil((async()=>{for(const key of await caches.keys())if(key.startsWith('character-shell-')&&key!==CACHE)await caches.delete(key);await self.clients.claim()})()));
self.addEventListener('message',e=>{if(e.data==='CLEAR_AUTH')e.waitUntil(caches.delete(CACHE));if(e.data==='CACHE_SHELL')e.waitUntil((async()=>{const r=await fetch('/',{credentials:'same-origin',redirect:'manual'});if(r.ok&&r.headers.get('content-type')?.includes('text/html')){const html=await r.clone().text(),cache=await caches.open(CACHE);await cache.put('/',r);for(const match of html.matchAll(/(?:src|href)="(\/assets\/[^" ]+)"/g)){const response=await fetch(match[1]);if(response.ok)await cache.put(match[1],response)}}})())});
self.addEventListener('fetch',e=>{const url=new URL(e.request.url);if(url.origin!==self.location.origin||e.request.method!=='GET'||url.pathname.startsWith('/api/')||url.pathname.startsWith('/auth/'))return;if(e.request.mode==='navigate'&&url.pathname==='/'){e.respondWith((async()=>{try{const r=await fetch(e.request);if(r.ok&&!r.redirected&&r.headers.get('content-type')?.includes('text/html'))await(await caches.open(CACHE)).put('/',r.clone());return r}catch{return await caches.match('/')||new Response('Connect to the internet to sign in.',{status:503})}})());return}if(url.pathname.startsWith('/assets/'))e.respondWith((async()=>{const hit=await caches.match(e.request);if(hit)return hit;const r=await fetch(e.request);if(r.ok)await(await caches.open(CACHE)).put(e.request,r.clone());return r})())});
const CACHE='character-shell-v2';
async function cacheShell(){
const r=await fetch('/',{credentials:'same-origin',redirect:'manual'});
if(!r.ok||r.redirected||!r.headers.get('content-type')?.includes('text/html'))return;
const html=await r.clone().text(),cache=await caches.open(CACHE);
await cache.put('/',r);
const urls=new Set([...html.matchAll(/(?:src|href)="(\/assets\/[^" ]+)"/g)].map(match=>match[1]));
try{
const manifest=await fetch('/assets/ui-manifest.json',{cache:'no-store'});
if(manifest.ok){for(const path of await manifest.json())if(typeof path==='string'&&path.startsWith('/assets/'))urls.add(path)}
}catch{}
await Promise.all([...urls].map(async path=>{const response=await fetch(path);if(response.ok&&!response.redirected)await cache.put(path,response)}));
}
self.addEventListener('install',e=>e.waitUntil(self.skipWaiting()));
self.addEventListener('activate',e=>e.waitUntil((async()=>{
for(const key of await caches.keys())if(key.startsWith('character-shell-')&&key!==CACHE)await caches.delete(key);
await self.clients.claim();try{await cacheShell()}catch{}
})()));
self.addEventListener('message',e=>{
if(e.data==='CLEAR_AUTH')e.waitUntil(caches.delete(CACHE));
if(e.data==='CACHE_SHELL')e.waitUntil(cacheShell());
});
self.addEventListener('fetch',e=>{
const url=new URL(e.request.url);
if(url.origin!==self.location.origin||e.request.method!=='GET'||url.pathname.startsWith('/api/')||url.pathname.startsWith('/auth/'))return;
if(e.request.mode==='navigate'&&url.pathname==='/'){
e.respondWith((async()=>{
try{const r=await fetch(e.request);if(r.ok&&!r.redirected&&r.headers.get('content-type')?.includes('text/html'))await(await caches.open(CACHE)).put('/',r.clone());return r}
catch{return await caches.match('/')||new Response('Connect to the internet to sign in.',{status:503})}
})());return;
}
if(url.pathname.startsWith('/assets/'))e.respondWith((async()=>{
const hit=await caches.match(e.request);if(hit)return hit;
const r=await fetch(e.request);if(r.ok&&!r.redirected)await(await caches.open(CACHE)).put(e.request,r.clone());return r;
})());
});

View File

@ -1,6 +1,9 @@
<script setup lang="ts">
import {computed,onMounted,ref,watch} from 'vue';
import {useCharacters} from './store';
import UiIcon from './UiIcon.vue';
import type {IconName} from './icons';
const navigation: {label:string,icon:IconName}[]=[{label:'Sheet',icon:'sheet'},{label:'Actions',icon:'actions'},{label:'Spells',icon:'spells'},{label:'Gear',icon:'gear'},{label:'More',icon:'notes'}];
import {abilities,modifier,proficiency,signed,skills,skillBonus,changeHP,newCharacter,characterSchema,restPreview,applyRest,type Character} from './model';
const store=useCharacters(),activeId=ref(''),tab=ref('Sheet'),drawer=ref(''),error=ref(''),message=ref(''),user=ref<{sub:string,name:string,expiresAt:number}|null>(null),loading=ref(true),offline=ref(!navigator.onLine);
const c=computed(()=>store.all.find(x=>x.id===activeId.value));
@ -36,24 +39,24 @@ function openAction(action:Character['actions'][number]){selectedAction.value=ac
<main>
<div class="character-switch"><select v-if="store.all.length" v-model="activeId" aria-label="Choose character"><option v-for="character in store.all" :value="character.id">{{character.name}}</option></select><span v-else>Your character collection</span><button class="quiet" @click="create">+ New character</button></div>
<p v-if="store.saveError" class="notice danger" role="alert">{{store.saveError}}</p><p v-if="message" class="notice" role="status">{{message}} <button class="quiet" @click="message=''">Dismiss</button></p>
<section v-if="!c" class="empty"><span class="sigil">✦</span><p class="eyebrow">A BLANK PAGE. A NEW STORY.</p><h1>Who will you become?</h1><p>Create a character and keep everything you need<br>for game night in your pocket.</p><button class="primary" @click="create">Create your first character →</button><label class="button quiet">Import a character<input type="file" accept="application/json,.json" hidden @change="importCharacter"></label></section>
<section v-if="!c" class="empty"><UiIcon name="sheet" class="empty-icon" /><p class="eyebrow">A BLANK PAGE. A NEW STORY.</p><h1>Who will you become?</h1><p>Create a character and keep everything you need<br>for game night in your pocket.</p><button class="primary" @click="create">Create your first character →</button><label class="button quiet">Import a character<input type="file" accept="application/json,.json" hidden @change="importCharacter"></label></section>
<template v-else>
<section class="hero"><div class="monogram">{{c.name.slice(0,1).toUpperCase()}}<span>✦</span></div><div><p class="eyebrow">LEVEL {{c.level}} · {{c.className||'ADVENTURER'}}</p><h1>{{c.name}}</h1><p>{{[c.species,c.background].filter(Boolean).join(' · ')||'Your story is waiting to be written.'}}</p></div><button class="edit" @click="edit">Edit character ↗</button></section>
<template v-if="tab==='Sheet'">
<div class="combat-grid"><button class="hp-card" @click="drawer='Hit points'"><div class="card-label">HIT POINTS <span>+ / −</span></div><div class="hp-value">{{c.hp.current}}<small>/ {{c.hp.max}}</small></div><div class="hp-track"><i :style="{width:`${100*c.hp.current/c.hp.max}%`}"></i></div><div class="hp-footer"><span>{{c.hp.temp}} temporary</span><span>Tap to damage or heal</span></div></button><div class="stat-card"><span>ARMOUR CLASS</span><strong>{{c.ac}}</strong><small>AC</small></div><div class="stat-card"><span>INITIATIVE</span><strong>{{signed(modifier(c.abilities.dex))}}</strong><button class="quiet" @click="roll(modifier(c.abilities.dex));drawer='Dice'">Roll d20 ↗</button></div><div class="stat-card"><span>SPEED</span><strong>{{c.speed}}</strong><small>feet</small></div></div>
<div class="ability-row"><button v-for="a in abilities" @click="roll(modifier(c.abilities[a]));drawer='Dice'"><span>{{a.toUpperCase()}}</span><strong>{{signed(modifier(c.abilities[a]))}}</strong><small>{{c.abilities[a]}}</small></button></div>
<div class="two-column"><section class="panel"><div class="section-title"><h2>Ready for your turn</h2><button class="quiet" @click="tab='Actions'">All actions ↗</button></div><div v-if="!c.actions.length" class="hint">Add your weapons and actions to keep them close at hand.<button class="quiet" @click="drawer='Add action'">+ Add action</button></div><button class="action-row" v-for="action in c.actions.slice(0,4)" @click="openAction(action)"><span class="action-symbol">↗</span><span><b>{{action.name}}</b><small>{{action.damage||'Custom action'}}</small></span><strong>{{signed(action.bonus)}}</strong></button></section><section class="panel"><div class="section-title"><h2>Resources</h2><button class="quiet" @click="drawer='Add resource'">+ Add</button></div><p class="hint" v-if="!c.resources.length">Spell slots, class features and item charges — tracked your way.</p><div class="resource" v-for="r in c.resources"><div><b>{{r.name}}</b><small>{{r.current}} / {{r.max}} · {{r.reset.replace('-',' ')}}</small></div><div class="counter"><button @click="r.current=Math.max(0,r.current-1)" :disabled="r.current===0" :aria-label="`Spend ${r.name}`">−</button><strong>{{r.current}}</strong><button @click="r.current=Math.min(r.max,r.current+1)" :disabled="r.current===r.max" :aria-label="`Restore ${r.name}`">+</button></div></div></section></div>
<div class="combat-grid"><button class="hp-card" @click="drawer='Hit points'"><div class="card-label"><span class="label-with-icon"><UiIcon name="hp" /> HIT POINTS</span><span>+ / −</span></div><div class="hp-value">{{c.hp.current}}<small>/ {{c.hp.max}}</small></div><div class="hp-track"><i :style="{width:`${100*c.hp.current/c.hp.max}%`}"></i></div><div class="hp-footer"><span>{{c.hp.temp}} temporary</span><span>Tap to damage or heal</span></div></button><div class="stat-card"><UiIcon name="ac" /><span>ARMOUR CLASS</span><strong>{{c.ac}}</strong><small>AC</small></div><div class="stat-card"><UiIcon name="initiative" /><span>INITIATIVE</span><strong>{{signed(modifier(c.abilities.dex))}}</strong><button class="quiet" @click="roll(modifier(c.abilities.dex));drawer='Dice'">Roll d20 ↗</button></div><div class="stat-card"><UiIcon name="speed" /><span>SPEED</span><strong>{{c.speed}}</strong><small>feet</small></div></div>
<div class="ability-row"><button v-for="a in abilities" @click="roll(modifier(c.abilities[a]));drawer='Dice'"><UiIcon :name="a" /><span>{{a.toUpperCase()}}</span><strong>{{signed(modifier(c.abilities[a]))}}</strong><small>{{c.abilities[a]}}</small></button></div>
<div class="two-column"><section class="panel"><div class="section-title"><h2><UiIcon name="actions" />Ready for your turn</h2><button class="quiet" @click="tab='Actions'">All actions ↗</button></div><div v-if="!c.actions.length" class="hint">Add your weapons and actions to keep them close at hand.<button class="quiet" @click="drawer='Add action'">+ Add action</button></div><button class="action-row" v-for="action in c.actions.slice(0,4)" @click="openAction(action)"><UiIcon name="actions" class="action-symbol" /><span><b>{{action.name}}</b><small>{{action.damage||'Custom action'}}</small></span><strong>{{signed(action.bonus)}}</strong></button></section><section class="panel"><div class="section-title"><h2><UiIcon name="slots" />Resources</h2><button class="quiet" @click="drawer='Add resource'">+ Add</button></div><p class="hint" v-if="!c.resources.length">Spell slots, class features and item charges — tracked your way.</p><div class="resource" v-for="r in c.resources"><UiIcon name="charges" /><div class="resource-details"><b>{{r.name}}</b><small>{{r.current}} / {{r.max}} · {{r.reset.replace('-',' ')}}</small></div><div class="counter"><button @click="r.current=Math.max(0,r.current-1)" :disabled="r.current===0" :aria-label="`Spend ${r.name}`">−</button><strong>{{r.current}}</strong><button @click="r.current=Math.min(r.max,r.current+1)" :disabled="r.current===r.max" :aria-label="`Restore ${r.name}`">+</button></div></div></section></div>
<section class="panel"><div class="section-title"><h2>Skills & saving throws</h2><span class="chip">Proficiency {{signed(proficiency(c.level))}}</span></div><div class="save-row"><button v-for="a in abilities" @click="roll(modifier(c.abilities[a])+(c.saves.includes(a)?proficiency(c.level):0));drawer='Dice'"><span>{{c.saves.includes(a)?'●':'○'}} {{a.toUpperCase()}}</span><b>{{signed(modifier(c.abilities[a])+(c.saves.includes(a)?proficiency(c.level):0))}}</b></button></div><div class="skill-grid"><button v-for="(a,name) in skills" @click="roll(skillBonus(c,String(name)));drawer='Dice'"><span>{{c.skills[name]==='expert'?'◆':c.skills[name]==='proficient'?'●':'○'}} {{name}}</span><b>{{signed(skillBonus(c,String(name)))}}</b></button></div></section>
<div class="rest-bar"><span>Take a moment to recover.</span><button @click="rest('short-rest')">Short rest</button><button class="primary" @click="rest('long-rest')">Long rest ☾</button></div>
<div class="rest-bar"><span>Take a moment to recover.</span><button @click="rest('short-rest')"><UiIcon name="short-rest" />Short rest</button><button class="primary" @click="rest('long-rest')"><UiIcon name="long-rest" />Long rest</button></div>
</template>
<section class="panel" v-if="tab==='Actions'"><div class="section-title"><h2>Actions</h2><button @click="drawer='Add action'">+ Add action</button></div><p class="hint" v-if="!c.actions.length">Add attacks, features and custom actions with your own modifiers.</p><div class="list-entry" v-for="action in c.actions"><button class="action-row" @click="openAction(action)"><span><b>{{action.name}}</b><small>{{action.damage}} · {{action.notes}}</small></span><strong>{{signed(action.bonus)}}</strong></button><button class="quiet" @click="c.actions=c.actions.filter(a=>a.id!==action.id)" :aria-label="`Remove ${action.name}`">Remove</button></div></section>
<section class="panel" v-if="tab==='Spells'"><div class="section-title"><h2>Spellbook</h2><button @click="drawer='Add spell'">+ Add spell</button></div><p class="hint">Record your own spells. Track spell slots with resources on the Sheet tab.</p><div class="spell-row" v-for="spell in [...c.spells].sort((a,b)=>a.level-b.level)"><label><input type="checkbox" v-model="spell.prepared"><span><b>{{spell.name}}</b><small>{{spell.level?'Level '+spell.level:'Cantrip'}} · {{spell.prepared?'Prepared':'Not prepared'}}</small><p>{{spell.notes}}</p></span></label><button class="quiet" @click="c.spells=c.spells.filter(s=>s.id!==spell.id)">Remove</button></div></section>
<section class="panel" v-if="tab==='Gear'"><div class="section-title"><h2>Gear & inventory</h2><span class="chip">Saved automatically</span></div><p class="hint">Equipment, currency, attunement and anything you picked up along the way.</p><textarea v-model="c.inventory" rows="14" placeholder="Longbow, 20 arrows…" maxlength="12000"></textarea></section>
<template v-if="tab==='More'"><section class="panel"><div class="section-title"><h2>Your story</h2><span class="chip">Saved automatically</span></div><textarea v-model="c.notes" rows="8" placeholder="Backstory, allies, session notes…" maxlength="12000"></textarea></section><section class="panel"><h2>Character & account</h2><p class="hint">Signed in as {{user.name}}. Characters are saved on this device, separately for each Arrowstack account. Export backups to move them to another device.</p><div class="settings-actions"><button @click="edit">Edit character</button><button @click="exportCharacter">Export JSON backup</button><label class="button">Import JSON<input type="file" accept="application/json,.json" hidden @change="importCharacter"></label><button @click="create">New character</button><button @click="drawer='Manage resources'">Manage resources</button><button class="danger-text" @click="remove">Delete character</button><button @click="logout" :disabled="offline">Sign out</button></div><p class="hint">Offline play lasts until your 12-hour sign-in session expires. Reconnect to sign in again. Attack modifiers, spell details and resource reset rules are entered manually; automated class rules are a future feature.</p></section></template>
<section class="panel" v-if="tab==='Actions'"><div class="section-title"><h2><UiIcon name="actions" />Actions</h2><button @click="drawer='Add action'">+ Add action</button></div><p class="hint" v-if="!c.actions.length">Add attacks, features and custom actions with your own modifiers.</p><div class="list-entry" v-for="action in c.actions"><button class="action-row" @click="openAction(action)"><span><b>{{action.name}}</b><small>{{action.damage}} · {{action.notes}}</small></span><strong>{{signed(action.bonus)}}</strong></button><button class="quiet" @click="c.actions=c.actions.filter(a=>a.id!==action.id)" :aria-label="`Remove ${action.name}`">Remove</button></div></section>
<section class="panel" v-if="tab==='Spells'"><div class="section-title"><h2><UiIcon name="spells" />Spellbook</h2><button @click="drawer='Add spell'">+ Add spell</button></div><p class="hint">Record your own spells. Track spell slots with resources on the Sheet tab.</p><div class="spell-row" v-for="spell in [...c.spells].sort((a,b)=>a.level-b.level)"><label><input type="checkbox" v-model="spell.prepared"><span><b>{{spell.name}}</b><small>{{spell.level?'Level '+spell.level:'Cantrip'}} · {{spell.prepared?'Prepared':'Not prepared'}}</small><p>{{spell.notes}}</p></span></label><button class="quiet" @click="c.spells=c.spells.filter(s=>s.id!==spell.id)">Remove</button></div></section>
<section class="panel" v-if="tab==='Gear'"><div class="section-title"><h2><UiIcon name="gear" />Gear & inventory</h2><span class="chip">Saved automatically</span></div><p class="hint">Equipment, currency, attunement and anything you picked up along the way.</p><textarea v-model="c.inventory" rows="14" placeholder="Longbow, 20 arrows…" maxlength="12000"></textarea></section>
<template v-if="tab==='More'"><section class="panel"><div class="section-title"><h2><UiIcon name="notes" />Your story</h2><span class="chip">Saved automatically</span></div><textarea v-model="c.notes" rows="8" placeholder="Backstory, allies, session notes…" maxlength="12000"></textarea></section><section class="panel"><h2>Character & account</h2><p class="hint">Signed in as {{user.name}}. Characters are saved on this device, separately for each Arrowstack account. Export backups to move them to another device.</p><div class="settings-actions"><button @click="edit">Edit character</button><button @click="exportCharacter">Export JSON backup</button><label class="button">Import JSON<input type="file" accept="application/json,.json" hidden @change="importCharacter"></label><button @click="create">New character</button><button @click="drawer='Manage resources'">Manage resources</button><button class="danger-text" @click="remove">Delete character</button><button @click="logout" :disabled="offline">Sign out</button></div><p class="hint">Offline play lasts until your 12-hour sign-in session expires. Reconnect to sign in again. Attack modifiers, spell details and resource reset rules are entered manually; automated class rules are a future feature.</p></section></template>
</template>
<footer>✦ ARROWSTACK CHARACTER <span>v0.1 · Made for game night</span></footer>
</main>
<nav class="bottom-nav" v-if="c"><button v-for="(name,index) in ['Sheet','Actions','Spells','Gear','More']" :class="{active:tab===name}" @click="tab=name"><span>{{['◈','↗','✦','▣','•••'][index]}}</span>{{name}}</button></nav>
<nav class="bottom-nav" v-if="c"><button v-for="item in navigation" :key="item.label" :class="{active:tab===item.label}" :aria-current="tab===item.label?'page':undefined" @click="tab=item.label"><UiIcon :name="item.icon" />{{item.label}}</button></nav>
<div v-if="drawer" class="overlay" @click.self="drawer=''"><section class="drawer" role="dialog" aria-modal="true" :aria-label="drawer"><div class="section-title"><h2>{{drawer==='Rest'?(restType==='long-rest'?'Long rest':'Short rest'):drawer}}</h2><button class="close" @click="drawer=''" aria-label="Close dialog">×</button></div>
<template v-if="drawer==='Account'"><p class="hint">Signed in as {{user.name}}. Characters are stored on this device, separately for each Arrowstack account.</p><button class="primary wide" @click="logout" :disabled="offline">Sign out of Character</button><p class="hint" v-if="offline">Reconnect to sign out.</p></template>
<form v-if="drawer==='Character'&&draft" @submit.prevent="saveDraft"><label>Name<input v-model="draft.name" required maxlength="120"></label><div class="form-grid"><label>Class<input v-model="draft.className" maxlength="120"></label><label>Level<input type="number" v-model.number="draft.level" min="1" max="20" required></label><label>Species<input v-model="draft.species" maxlength="120"></label><label>Background<input v-model="draft.background" maxlength="120"></label></div><h3>Ability scores</h3><div class="form-grid three"><label v-for="a in abilities">{{a.toUpperCase()}}<input type="number" v-model.number="draft.abilities[a]" min="1" max="30" required></label></div><div class="form-grid three"><label>Max HP<input type="number" v-model.number="draft.hp.max" min="1" max="9999" required></label><label>Current HP<input type="number" v-model.number="draft.hp.current" min="0" :max="draft.hp.max" required></label><label>Temp HP<input type="number" v-model.number="draft.hp.temp" min="0" max="9999" required></label><label>AC<input type="number" v-model.number="draft.ac" min="0" max="9999" required></label><label>Speed (ft)<input type="number" v-model.number="draft.speed" min="0" max="9999" required></label></div><h3>Saving throw proficiency</h3><div class="check-row"><label v-for="a in abilities"><input type="checkbox" :value="a" v-model="draft.saves">{{a.toUpperCase()}}</label></div><h3>Skill proficiency</h3><div class="form-grid"><label v-for="(a,name) in skills">{{name}}<select v-model="draft.skills[name]"><option value="none">None</option><option value="proficient">Proficient</option><option value="expert">Expertise</option></select></label></div><button class="primary wide">Save character</button></form>

14
src/UiIcon.vue Normal file
View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import {computed,useId} from 'vue';
import {icons,type IconName} from './icons';
const props=defineProps<{name:IconName}>();
const icon=computed(()=>icons[props.name]);
const clip=computed(()=> 'clip' in icon.value ? icon.value.clip : undefined);
const clipId=useId();
</script>
<template>
<svg class="ui-icon" :viewBox="icon.box" aria-hidden="true" focusable="false">
<defs v-if="clip"><clipPath :id="clipId"><path :d="clip" /></clipPath></defs>
<image :href="icon.src" :width="icon.width" :height="icon.height" :clip-path="clip?`url(#${clipId})`:undefined" />
</svg>
</template>

26
src/icons.ts Normal file
View File

@ -0,0 +1,26 @@
import navigation from './assets/navigation.webp';
import combat from './assets/combat.webp';
import abilities from './assets/abilities.webp';
import resources from './assets/resources.webp';
export const icons = {
'sheet': {src: navigation, box: '79 154 354 406', width: 2172, height: 724},
'actions': {src: navigation, box: '476 162 371 383', width: 2172, height: 724},
'spells': {src: navigation, box: '882 141 407 419', width: 2172, height: 724},
'gear': {src: navigation, box: '1303 138 413 421', width: 2172, height: 724},
'notes': {src: navigation, box: '1772 171 343 382', width: 2172, height: 724},
'hp': {src: combat, box: '56 142 462 431', width: 2172, height: 724},
'ac': {src: combat, box: '586 96 436 495', width: 2172, height: 724},
'initiative': {src: combat, box: '1094 131 490 454', width: 2172, height: 724},
'speed': {src: combat, box: '1629 122 509 468', width: 2172, height: 724},
'str': {src: abilities, box: '37 162 372 403', width: 2172, height: 724, clip: 'M219 165 L206 168 L197 171 L190 174 L184 177 L178 180 L173 183 L170 186 L167 189 L164 192 L162 195 L159 198 L158 201 L155 204 L153 207 L151 210 L149 213 L147 216 L146 219 L143 222 L142 225 L140 228 L138 231 L136 234 L135 237 L133 240 L131 243 L130 246 L128 249 L127 252 L125 255 L124 258 L123 261 L123 264 L123 267 L123 270 L124 273 L126 276 L129 279 L132 282 L133 285 L133 288 L133 291 L134 294 L134 297 L134 300 L134 303 L134 306 L134 309 L135 312 L135 315 L135 318 L135 321 L134 324 L131 327 L128 330 L126 333 L124 336 L122 339 L121 342 L120 345 L117 348 L114 351 L111 354 L108 357 L104 360 L101 363 L98 366 L94 369 L91 372 L88 375 L85 378 L80 381 L74 384 L69 387 L65 390 L61 393 L58 396 L55 399 L52 402 L49 405 L47 408 L46 411 L44 414 L42 417 L41 420 L40 423 L40 426 L39 429 L39 432 L39 435 L39 438 L39 441 L40 444 L40 447 L41 450 L42 453 L44 456 L45 459 L47 462 L49 465 L51 468 L53 471 L56 474 L58 477 L61 480 L64 483 L67 486 L70 489 L73 492 L76 495 L79 498 L82 501 L85 504 L89 507 L92 510 L96 513 L100 516 L104 519 L108 522 L113 525 L118 528 L124 531 L129 534 L134 537 L140 540 L146 543 L153 546 L160 549 L168 552 L178 555 L188 558 L203 561 L243 561 L251 558 L255 555 L258 552 L261 549 L264 546 L267 543 L270 540 L273 537 L275 534 L277 531 L278 528 L280 525 L281 522 L281 519 L282 516 L282 513 L282 510 L282 507 L282 504 L282 501 L282 498 L283 495 L283 492 L284 489 L285 486 L285 483 L286 480 L287 477 L288 474 L288 471 L289 468 L290 465 L290 462 L291 459 L292 456 L293 453 L293 450 L294 447 L295 444 L296 441 L297 438 L299 435 L301 432 L303 429 L304 426 L305 423 L305 420 L310 417 L314 414 L319 411 L323 408 L327 405 L330 402 L334 399 L337 396 L340 393 L343 390 L346 387 L349 384 L352 381 L354 378 L357 375 L359 372 L362 369 L364 366 L367 363 L369 360 L371 357 L374 354 L376 351 L379 348 L381 345 L383 342 L385 339 L387 336 L388 333 L391 330 L393 327 L394 324 L396 321 L397 318 L398 315 L399 312 L401 309 L402 306 L402 303 L403 300 L404 297 L405 294 L406 291 L406 288 L406 285 L406 282 L406 279 L406 276 L405 273 L404 270 L403 267 L402 264 L400 261 L397 258 L394 255 L391 252 L388 249 L384 246 L383 243 L382 240 L381 237 L380 234 L379 231 L377 228 L375 225 L372 222 L369 219 L366 216 L363 213 L357 210 L351 207 L346 204 L344 201 L342 198 L339 195 L336 192 L333 189 L330 186 L327 183 L323 180 L313 177 L292 174 L288 171 L284 168 L277 165 Z'},
'dex': {src: abilities, box: '356 138 363 415', width: 2172, height: 724, clip: 'M695 141 L687 144 L681 147 L674 150 L668 153 L662 156 L656 159 L651 162 L645 165 L641 168 L636 171 L631 174 L627 177 L622 180 L618 183 L613 186 L610 189 L606 192 L602 195 L598 198 L595 201 L592 204 L588 207 L585 210 L582 213 L578 216 L575 219 L572 222 L569 225 L567 228 L564 231 L562 234 L560 237 L557 240 L555 243 L553 246 L551 249 L549 252 L547 255 L545 258 L542 261 L540 264 L538 267 L536 270 L534 273 L531 276 L529 279 L527 282 L525 285 L522 288 L520 291 L518 294 L516 297 L513 300 L511 303 L455 306 L450 309 L446 312 L442 315 L439 318 L436 321 L433 324 L430 327 L427 330 L425 333 L422 336 L419 339 L417 342 L415 345 L413 348 L411 351 L411 354 L411 357 L412 360 L414 363 L416 366 L419 369 L422 372 L425 375 L428 378 L437 381 L442 384 L442 387 L440 390 L440 393 L437 396 L435 399 L432 402 L430 405 L427 408 L425 411 L423 414 L420 417 L418 420 L416 423 L413 426 L411 429 L409 432 L406 435 L404 438 L402 441 L400 444 L383 447 L378 450 L374 453 L371 456 L368 459 L365 462 L362 465 L360 468 L359 471 L359 474 L358 477 L358 480 L358 483 L358 486 L358 489 L359 492 L359 495 L359 498 L360 501 L360 504 L361 507 L362 510 L362 513 L363 516 L364 519 L365 522 L366 525 L368 528 L369 531 L371 534 L374 537 L377 540 L380 543 L383 546 L391 549 L437 549 L444 546 L490 543 L503 540 L514 537 L525 534 L532 531 L540 528 L547 525 L554 522 L567 519 L574 516 L580 513 L586 510 L594 507 L600 504 L605 501 L610 498 L615 495 L619 492 L624 489 L628 486 L631 483 L635 480 L638 477 L642 474 L645 471 L648 468 L651 465 L653 462 L656 459 L658 456 L660 453 L662 450 L664 447 L665 444 L667 441 L667 438 L667 435 L665 432 L662 429 L659 426 L657 423 L659 420 L661 417 L663 414 L665 411 L666 408 L667 405 L669 402 L670 399 L671 396 L672 393 L673 390 L674 387 L675 384 L676 381 L677 378 L677 375 L678 372 L679 369 L679 366 L679 363 L680 360 L680 357 L680 354 L681 351 L681 348 L681 345 L681 342 L681 339 L681 336 L681 333 L680 330 L680 327 L680 324 L679 321 L679 318 L678 315 L678 312 L677 309 L676 306 L675 303 L674 300 L673 297 L672 294 L671 291 L669 288 L666 285 L663 282 L660 279 L661 276 L663 273 L664 270 L666 267 L667 264 L669 261 L671 258 L672 255 L674 252 L675 249 L676 246 L678 243 L679 240 L681 237 L682 234 L683 231 L685 228 L686 225 L687 222 L688 219 L689 216 L691 213 L692 210 L693 207 L695 204 L696 201 L697 198 L699 195 L700 192 L701 189 L702 186 L704 183 L705 180 L706 177 L708 174 L709 171 L711 168 L712 165 L713 162 L715 159 L716 156 L716 153 L715 150 L712 147 L709 144 L706 141 Z'},
'con': {src: abilities, box: '707 175 421 400', width: 2172, height: 724, clip: 'M822 178 L817 181 L813 184 L809 187 L805 190 L802 193 L799 196 L796 199 L793 202 L791 205 L787 208 L783 211 L778 214 L774 217 L770 220 L765 223 L761 226 L757 229 L753 232 L749 235 L743 238 L738 241 L734 244 L731 247 L728 250 L725 253 L722 256 L720 259 L718 262 L717 265 L716 268 L715 271 L715 274 L715 277 L715 280 L715 283 L715 286 L715 289 L715 292 L715 295 L716 298 L716 301 L717 304 L718 307 L718 310 L719 313 L718 316 L717 319 L717 322 L716 325 L715 328 L715 331 L714 334 L714 337 L713 340 L713 343 L713 346 L711 349 L710 352 L710 355 L709 358 L709 361 L709 364 L709 367 L709 370 L710 373 L711 376 L712 379 L713 382 L715 385 L718 388 L720 391 L723 394 L726 397 L729 400 L732 403 L735 406 L739 409 L754 412 L771 415 L773 418 L775 421 L777 424 L779 427 L781 430 L782 433 L783 436 L783 439 L783 442 L783 445 L782 448 L779 451 L777 454 L774 457 L773 460 L771 463 L770 466 L770 469 L769 472 L769 475 L769 478 L769 481 L769 484 L770 487 L771 490 L773 493 L775 496 L777 499 L780 502 L783 505 L786 508 L790 511 L793 514 L797 517 L801 520 L805 523 L809 526 L814 529 L820 532 L825 535 L831 538 L856 541 L859 544 L862 547 L867 550 L871 553 L876 556 L882 559 L888 562 L894 565 L901 568 L908 571 L928 571 L936 568 L943 565 L949 562 L955 559 L961 556 L967 553 L971 550 L975 547 L978 544 L981 541 L1006 538 L1012 535 L1017 532 L1022 529 L1027 526 L1031 523 L1036 520 L1039 517 L1043 514 L1046 511 L1049 508 L1052 505 L1055 502 L1058 499 L1061 496 L1063 493 L1065 490 L1066 487 L1067 484 L1067 481 L1067 478 L1067 475 L1066 472 L1066 469 L1066 466 L1065 463 L1063 460 L1061 457 L1059 454 L1056 451 L1053 448 L1053 445 L1053 442 L1053 439 L1053 436 L1053 433 L1054 430 L1056 427 L1059 424 L1061 421 L1062 418 L1065 415 L1081 412 L1096 409 L1100 406 L1103 403 L1106 400 L1109 397 L1112 394 L1115 391 L1118 388 L1120 385 L1122 382 L1123 379 L1124 376 L1125 373 L1125 370 L1125 367 L1125 364 L1125 361 L1125 358 L1125 355 L1124 352 L1123 349 L1121 346 L1121 343 L1121 340 L1121 337 L1120 334 L1120 331 L1119 328 L1119 325 L1118 322 L1118 319 L1117 316 L1116 313 L1116 310 L1117 307 L1118 304 L1118 301 L1119 298 L1120 295 L1120 292 L1120 289 L1120 286 L1120 283 L1120 280 L1120 277 L1120 274 L1120 271 L1119 268 L1119 265 L1117 262 L1116 259 L1113 256 L1110 253 L1107 250 L1104 247 L1100 244 L1096 241 L1091 238 L1085 235 L1081 232 L1078 229 L1073 226 L1069 223 L1065 220 L1061 217 L1056 214 L1052 211 L1047 208 L1043 205 L1041 202 L1038 199 L1035 196 L1032 193 L1029 190 L1026 187 L1022 184 L1018 181 L1013 178 Z'},
'int': {src: abilities, box: '1119 176 367 418', width: 2172, height: 724, clip: 'M1351 179 L1337 182 L1301 185 L1259 188 L1224 191 L1174 194 L1163 197 L1156 200 L1151 203 L1148 206 L1144 209 L1141 212 L1138 215 L1135 218 L1132 221 L1130 224 L1128 227 L1126 230 L1124 233 L1123 236 L1122 239 L1122 242 L1121 245 L1121 248 L1121 251 L1121 254 L1121 257 L1121 260 L1121 263 L1121 266 L1122 269 L1123 272 L1125 275 L1126 278 L1126 281 L1127 284 L1127 287 L1127 290 L1128 293 L1127 296 L1126 299 L1126 302 L1126 305 L1126 308 L1126 311 L1126 314 L1126 317 L1126 320 L1127 323 L1128 326 L1129 329 L1130 332 L1133 335 L1134 338 L1135 341 L1135 344 L1136 347 L1136 350 L1136 353 L1137 356 L1137 359 L1137 362 L1138 365 L1138 368 L1139 371 L1139 374 L1140 377 L1140 380 L1140 383 L1141 386 L1141 389 L1142 392 L1142 395 L1142 398 L1141 401 L1140 404 L1140 407 L1140 410 L1140 413 L1140 416 L1140 419 L1141 422 L1141 425 L1142 428 L1143 431 L1144 434 L1145 437 L1147 440 L1150 443 L1150 446 L1151 449 L1151 452 L1152 455 L1153 458 L1153 461 L1154 464 L1154 467 L1155 470 L1155 473 L1156 476 L1156 479 L1157 482 L1157 485 L1157 488 L1157 491 L1157 494 L1157 497 L1157 500 L1157 503 L1158 506 L1158 509 L1159 512 L1160 515 L1161 518 L1162 521 L1163 524 L1165 527 L1167 530 L1170 533 L1173 536 L1176 539 L1179 542 L1183 545 L1187 548 L1192 551 L1201 554 L1242 557 L1242 560 L1243 563 L1243 566 L1244 569 L1244 572 L1244 575 L1245 578 L1247 581 L1250 584 L1253 587 L1256 590 L1269 590 L1310 587 L1313 584 L1316 581 L1319 578 L1321 575 L1322 572 L1322 569 L1322 566 L1322 563 L1321 560 L1321 557 L1320 554 L1320 551 L1319 548 L1319 545 L1340 542 L1371 539 L1421 536 L1455 533 L1467 530 L1471 527 L1474 524 L1477 521 L1480 518 L1482 515 L1483 512 L1483 509 L1483 506 L1483 503 L1483 500 L1482 497 L1482 494 L1481 491 L1480 488 L1480 485 L1479 482 L1478 479 L1477 476 L1477 473 L1475 470 L1477 467 L1480 464 L1481 461 L1481 458 L1481 455 L1481 452 L1480 449 L1479 446 L1478 443 L1478 440 L1477 437 L1476 434 L1476 431 L1475 428 L1474 425 L1474 422 L1473 419 L1472 416 L1471 413 L1471 410 L1470 407 L1469 404 L1467 401 L1466 398 L1466 395 L1468 392 L1470 389 L1471 386 L1472 383 L1473 380 L1473 377 L1473 374 L1473 371 L1472 368 L1472 365 L1471 362 L1471 359 L1470 356 L1469 353 L1468 350 L1468 347 L1467 344 L1466 341 L1466 338 L1465 335 L1464 332 L1463 329 L1463 326 L1462 323 L1461 320 L1460 317 L1459 314 L1458 311 L1457 308 L1455 305 L1452 302 L1449 299 L1446 296 L1443 293 L1438 290 L1437 287 L1437 284 L1436 281 L1435 278 L1434 275 L1433 272 L1432 269 L1431 266 L1431 263 L1430 260 L1429 257 L1428 254 L1427 251 L1427 248 L1427 245 L1427 242 L1427 239 L1426 236 L1425 233 L1425 230 L1424 227 L1423 224 L1422 221 L1421 218 L1421 215 L1420 212 L1419 209 L1418 206 L1417 203 L1417 200 L1416 197 L1415 194 L1412 191 L1409 188 L1406 185 L1403 182 L1400 179 Z'},
'wis': {src: abilities, box: '1496 131 304 436', width: 2172, height: 724, clip: 'M1641 134 L1638 137 L1635 140 L1632 143 L1630 146 L1629 149 L1628 152 L1627 155 L1626 158 L1625 161 L1624 164 L1623 167 L1622 170 L1620 173 L1619 176 L1617 179 L1615 182 L1613 185 L1611 188 L1609 191 L1606 194 L1604 197 L1600 200 L1597 203 L1593 206 L1590 209 L1586 212 L1582 215 L1578 218 L1575 221 L1572 224 L1568 227 L1565 230 L1561 233 L1558 236 L1555 239 L1552 242 L1549 245 L1547 248 L1544 251 L1541 254 L1538 257 L1536 260 L1533 263 L1531 266 L1529 269 L1526 272 L1524 275 L1522 278 L1520 281 L1518 284 L1516 287 L1515 290 L1513 293 L1512 296 L1510 299 L1509 302 L1508 305 L1506 308 L1505 311 L1505 314 L1504 317 L1503 320 L1502 323 L1501 326 L1501 329 L1500 332 L1499 335 L1499 338 L1499 341 L1498 344 L1498 347 L1498 350 L1498 353 L1498 356 L1498 359 L1498 362 L1498 365 L1498 368 L1498 371 L1499 374 L1499 377 L1500 380 L1500 383 L1501 386 L1501 389 L1502 392 L1503 395 L1504 398 L1505 401 L1506 404 L1508 407 L1509 410 L1511 413 L1512 416 L1514 419 L1516 422 L1517 425 L1519 428 L1521 431 L1523 434 L1526 437 L1528 440 L1531 443 L1533 446 L1536 449 L1539 452 L1542 455 L1545 458 L1548 461 L1552 464 L1555 467 L1558 470 L1562 473 L1565 476 L1569 479 L1574 482 L1578 485 L1582 488 L1586 491 L1591 494 L1595 497 L1598 500 L1602 503 L1606 506 L1608 509 L1611 512 L1613 515 L1615 518 L1616 521 L1618 524 L1620 527 L1621 530 L1622 533 L1624 536 L1625 539 L1626 542 L1627 545 L1628 548 L1630 551 L1632 554 L1635 557 L1638 560 L1641 563 L1654 563 L1657 560 L1660 557 L1663 554 L1665 551 L1667 548 L1668 545 L1669 542 L1670 539 L1671 536 L1672 533 L1674 530 L1675 527 L1677 524 L1678 521 L1680 518 L1682 515 L1684 512 L1686 509 L1689 506 L1692 503 L1695 500 L1699 497 L1704 494 L1708 491 L1712 488 L1716 485 L1720 482 L1724 479 L1728 476 L1732 473 L1735 470 L1739 467 L1742 464 L1745 461 L1749 458 L1752 455 L1755 452 L1758 449 L1761 446 L1763 443 L1765 440 L1768 437 L1770 434 L1772 431 L1774 428 L1776 425 L1778 422 L1780 419 L1782 416 L1783 413 L1785 410 L1786 407 L1787 404 L1789 401 L1790 398 L1791 395 L1792 392 L1793 389 L1793 386 L1794 383 L1795 380 L1795 377 L1796 374 L1796 371 L1796 368 L1796 365 L1797 362 L1797 359 L1797 356 L1797 353 L1797 350 L1796 347 L1796 344 L1796 341 L1796 338 L1795 335 L1795 332 L1794 329 L1794 326 L1793 323 L1793 320 L1792 317 L1791 314 L1790 311 L1789 308 L1788 305 L1786 302 L1785 299 L1784 296 L1782 293 L1781 290 L1779 287 L1778 284 L1776 281 L1774 278 L1772 275 L1770 272 L1768 269 L1765 266 L1763 263 L1760 260 L1758 257 L1755 254 L1752 251 L1749 248 L1746 245 L1743 242 L1740 239 L1737 236 L1734 233 L1731 230 L1727 227 L1724 224 L1720 221 L1717 218 L1713 215 L1709 212 L1705 209 L1702 206 L1698 203 L1695 200 L1691 197 L1689 194 L1686 191 L1684 188 L1682 185 L1680 182 L1679 179 L1677 176 L1675 173 L1674 170 L1672 167 L1671 164 L1670 161 L1669 158 L1668 155 L1668 152 L1667 149 L1666 146 L1663 143 L1660 140 L1657 137 L1654 134 Z'},
'cha': {src: abilities, box: '1818 164 318 392', width: 2172, height: 724, clip: 'M2082 167 L2077 170 L2073 173 L2068 176 L2064 179 L2059 182 L2055 185 L2051 188 L2047 191 L2043 194 L2039 197 L2034 200 L2030 203 L2026 206 L2021 209 L2015 212 L2009 215 L1848 218 L1842 221 L1839 224 L1836 227 L1833 230 L1830 233 L1828 236 L1826 239 L1825 242 L1825 245 L1824 248 L1824 251 L1823 254 L1823 257 L1823 260 L1823 263 L1823 266 L1822 269 L1822 272 L1822 275 L1822 278 L1822 281 L1822 284 L1821 287 L1821 290 L1820 293 L1820 296 L1820 299 L1820 302 L1821 305 L1821 308 L1822 311 L1824 314 L1826 317 L1827 320 L1828 323 L1828 326 L1829 329 L1829 332 L1830 335 L1830 338 L1831 341 L1832 344 L1832 347 L1833 350 L1834 353 L1835 356 L1835 359 L1836 362 L1837 365 L1838 368 L1839 371 L1840 374 L1841 377 L1842 380 L1843 383 L1844 386 L1845 389 L1846 392 L1847 395 L1849 398 L1850 401 L1851 404 L1853 407 L1854 410 L1856 413 L1857 416 L1859 419 L1861 422 L1862 425 L1864 428 L1866 431 L1868 434 L1870 437 L1872 440 L1874 443 L1876 446 L1878 449 L1881 452 L1883 455 L1886 458 L1888 461 L1891 464 L1893 467 L1896 470 L1899 473 L1902 476 L1905 479 L1908 482 L1911 485 L1914 488 L1917 491 L1921 494 L1924 497 L1927 500 L1931 503 L1934 506 L1938 509 L1943 512 L1946 515 L1951 518 L1955 521 L1960 524 L1965 527 L1970 530 L1976 533 L1982 536 L1988 539 L1994 542 L2002 545 L2010 548 L2019 551 L2054 551 L2057 548 L2060 545 L2063 542 L2066 539 L2069 536 L2072 533 L2075 530 L2078 527 L2080 524 L2083 521 L2085 518 L2087 515 L2089 512 L2091 509 L2093 506 L2094 503 L2096 500 L2098 497 L2100 494 L2101 491 L2103 488 L2104 485 L2106 482 L2107 479 L2109 476 L2110 473 L2111 470 L2113 467 L2114 464 L2115 461 L2116 458 L2117 455 L2118 452 L2119 449 L2120 446 L2121 443 L2122 440 L2123 437 L2124 434 L2125 431 L2125 428 L2126 425 L2127 422 L2127 419 L2128 416 L2129 413 L2129 410 L2129 407 L2130 404 L2130 401 L2131 398 L2131 395 L2131 392 L2132 389 L2132 386 L2132 383 L2132 380 L2132 377 L2132 374 L2132 371 L2132 368 L2132 365 L2133 362 L2133 359 L2133 356 L2133 353 L2133 350 L2133 347 L2133 344 L2133 341 L2132 338 L2132 335 L2132 332 L2132 329 L2132 326 L2132 323 L2131 320 L2131 317 L2131 314 L2131 311 L2131 308 L2130 305 L2130 302 L2130 299 L2129 296 L2129 293 L2129 290 L2129 287 L2128 284 L2128 281 L2128 278 L2127 275 L2127 272 L2126 269 L2126 266 L2126 263 L2125 260 L2125 257 L2125 254 L2124 251 L2124 248 L2123 245 L2123 242 L2123 239 L2122 236 L2122 233 L2121 230 L2121 227 L2120 224 L2120 221 L2119 218 L2119 215 L2118 212 L2118 209 L2117 206 L2117 203 L2116 200 L2115 197 L2114 194 L2114 191 L2113 188 L2112 185 L2111 182 L2109 179 L2106 176 L2103 173 L2100 170 L2097 167 Z'},
'slots': {src: resources, box: '57 89 451 539', width: 2172, height: 724},
'charges': {src: resources, box: '577 94 459 531', width: 2172, height: 724},
'short-rest': {src: resources, box: '1111 102 493 530', width: 2172, height: 724},
'long-rest': {src: resources, box: '1684 108 448 499', width: 2172, height: 724},
} as const;
export type IconName = keyof typeof icons;

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,8 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({plugins:[vue()],server:{proxy:{'/api':'http://localhost:8080','/auth':'http://localhost:8080'}}});
export default defineConfig({plugins:[vue(),{
name:'offline-ui-assets',
generateBundle(_options,bundle){
this.emitFile({type:'asset',fileName:'assets/ui-manifest.json',source:JSON.stringify(Object.values(bundle).map(item=>`/${item.fileName}`))});
}
}],server:{proxy:{'/api':'http://localhost:8080','/auth':'http://localhost:8080'}}});