From 7fe49f6910a41625ca0a268b36d6ddb970635acd Mon Sep 17 00:00:00 2001 From: Admin Date: Wed, 7 Oct 2026 19:36:13 +0000 Subject: [PATCH] Mirror Spellbook folio from f051a7741b0e34532b33aaec2c228055b02524e4 --- src/App.vue | 25 +++++++++++++++++++------ src/style.css | 7 +++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/App.vue b/src/App.vue index 74c08c7..d3d78f5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -12,7 +12,8 @@ const c=computed(()=>store.all.find(x=>x.id===activeId.value)); const draft=ref(null),hpAmount=ref(1),restType=ref<'short-rest'|'long-rest'>('short-rest'),rollResult=ref(''),selectedAction=ref(null); const drawerIcon=computed(()=>({Character:'sheet',Account:'notes','Hit points':'hp','Add action':'actions','Add resource':'charges','Manage resources':'slots','Add spell':'spells',Roll:'actions',Dice:'initiative',Rest:restType.value==='long-rest'?'long-rest':'short-rest'} as Record)[drawer.value]||'sheet'); const restChanges=computed(()=>c.value?restPreview(c.value,restType.value):[]); -const editingActionId=ref(null); +const editingActionId=ref(null),editingSpellId=ref(null); +const spellLevels=computed(()=>c.value?[...new Set(c.value.spells.map(s=>s.level))].sort((a,b)=>a-b):[]); const actionDraft=ref({name:'',bonus:0,damage:'',notes:''}),resourceDraft=ref({name:'',current:1,max:1,reset:'long-rest' as 'short-rest'|'long-rest'|'manual'}),spellDraft=ref({name:'',level:1,prepared:true,notes:''}); watch(c,()=>{if(c.value&&user.value)store.persist(c.value)},{deep:true}); async function authenticate(){try{const response=await fetch('/api/session',{cache:'no-store'});if(!response.ok){localStorage.removeItem('character-identity');navigator.serviceWorker.controller?.postMessage('CLEAR_AUTH');throw new Error('Please sign in with Arrowstack to open your characters.')}const identity=await response.json();if(store.owner&&store.owner!==identity.sub){await store.flush();location.reload();return}user.value=identity;error.value='';localStorage.setItem('character-identity',JSON.stringify(user.value))}catch(e){if(e instanceof TypeError){try{const cached=JSON.parse(localStorage.getItem('character-identity')||'null');if(cached?.sub&&cached.expiresAt>Date.now()){user.value=cached;offline.value=true}else throw new Error()}catch{error.value='Connect to the internet to sign in with Arrowstack.'}}else error.value=(e as Error).message}} @@ -24,7 +25,8 @@ function hp(amount:number){if(c.value)changeHP(c.value,amount)} function actionEditor(action?:Character['actions'][number]){editingActionId.value=action?.id||null;actionDraft.value=action?{name:action.name,bonus:action.bonus,damage:action.damage,notes:action.notes}:{name:'',bonus:0,damage:'',notes:''};drawer.value='Add action'} function addAction(){if(!c.value||!actionDraft.value.name.trim())return;const value={...actionDraft.value,name:actionDraft.value.name.trim(),id:editingActionId.value||crypto.randomUUID()};if(editingActionId.value){const index=c.value.actions.findIndex(a=>a.id===editingActionId.value);if(index<0){message.value='This action changed. Close the editor and reload it.';return}c.value.actions[index]=value}else c.value.actions.push(value);editingActionId.value=null;actionDraft.value={name:'',bonus:0,damage:'',notes:''};drawer.value=''} function addResource(){if(c.value&&resourceDraft.value.name.trim()&&resourceDraft.value.max>=0&&resourceDraft.value.current>=0&&resourceDraft.value.current<=resourceDraft.value.max){c.value.resources.push({...resourceDraft.value,id:crypto.randomUUID()});resourceDraft.value={name:'',current:1,max:1,reset:'long-rest'};drawer.value=''}else message.value='Remaining uses must be between zero and maximum.'} -function addSpell(){if(c.value&&spellDraft.value.name.trim()){c.value.spells.push({...spellDraft.value,id:crypto.randomUUID()});spellDraft.value={name:'',level:1,prepared:true,notes:''};drawer.value=''}} +function spellEditor(spell?:Character['spells'][number]){editingSpellId.value=spell?.id||null;spellDraft.value=spell?{name:spell.name,level:spell.level,prepared:spell.prepared,notes:spell.notes}:{name:'',level:1,prepared:true,notes:''};drawer.value='Add spell'} +function addSpell(){if(!c.value||!spellDraft.value.name.trim())return;const value={...spellDraft.value,name:spellDraft.value.name.trim(),id:editingSpellId.value||crypto.randomUUID()};if(editingSpellId.value){const index=c.value.spells.findIndex(s=>s.id===editingSpellId.value);if(index<0){message.value='This spell changed. Close the editor and reload it.';return}c.value.spells[index]=value}else c.value.spells.push(value);editingSpellId.value=null;spellDraft.value={name:'',level:1,prepared:true,notes:''};drawer.value=''} function rest(type:'short-rest'|'long-rest'){restType.value=type;drawer.value='Rest'} function confirmRest(){if(c.value)applyRest(c.value,restType.value);drawer.value='';message.value='Rest applied. Your sheet is saved.'} function die(sides:number){const range=4294967296-(4294967296%sides);let n:number;do{n=crypto.getRandomValues(new Uint32Array(1))[0]}while(n>=range);return n%sides+1} @@ -64,14 +66,25 @@ function openAction(action:Character['actions'][number]){selectedAction.value=ac
Notes & options

{{action.notes}}

-

Spellbook

Record your own spells. Track spell slots with resources on the Sheet tab.

+
+

Spellbook

+

A little magic starts here.

Record your spells and cantrips, prepare your favourites and keep their notes close.

+ +

Gear & inventory

Saved automatically

Equipment, currency, attunement and anything you picked up along the way.

-
✦ ARROWSTACK CHARACTER v0.3 · Made for game night
+
✦ ARROWSTACK CHARACTER v0.3 · Made for game night
-