Mirror Actions folio patch from cf7fd11b3332ef9a59f50bb929bc86c7ad58585f
This commit is contained in:
parent
95abf05250
commit
41278b03f9
23
src/App.vue
23
src/App.vue
@ -12,6 +12,7 @@ const c=computed(()=>store.all.find(x=>x.id===activeId.value));
|
||||
const draft=ref<Character|null>(null),hpAmount=ref(1),restType=ref<'short-rest'|'long-rest'>('short-rest'),rollResult=ref(''),selectedAction=ref<Character['actions'][number]|null>(null);
|
||||
const drawerIcon=computed<IconName>(()=>({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<string,IconName>)[drawer.value]||'sheet');
|
||||
const restChanges=computed(()=>c.value?restPreview(c.value,restType.value):[]);
|
||||
const editingActionId=ref<string|null>(null);
|
||||
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}}
|
||||
@ -20,7 +21,8 @@ function create(){draft.value=newCharacter();drawer.value='Character'}
|
||||
function edit(){if(c.value){draft.value=structuredClone(JSON.parse(JSON.stringify(c.value)));drawer.value='Character'}}
|
||||
function saveDraft(){try{const valid=characterSchema.parse(draft.value);const index=store.all.findIndex(x=>x.id===valid.id);if(index>=0)store.all[index]=valid;else store.all.push(valid);activeId.value=valid.id;store.persist(valid);drawer.value='';error.value=''}catch{message.value='Check your values: level 1–20, abilities 1–30, HP within maximum.'}}
|
||||
function hp(amount:number){if(c.value)changeHP(c.value,amount)}
|
||||
function addAction(){if(c.value&&actionDraft.value.name.trim()){c.value.actions.push({...actionDraft.value,id:crypto.randomUUID()});actionDraft.value={name:'',bonus:0,damage:'',notes:''};drawer.value=''}}
|
||||
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 rest(type:'short-rest'|'long-rest'){restType.value=type;drawer.value='Rest'}
|
||||
@ -40,7 +42,7 @@ function openAction(action:Character['actions'][number]){selectedAction.value=ac
|
||||
<div v-else class="app-shell">
|
||||
<header class="topbar"><a class="brand" href="/">✦ <span>CHARACTER<span class="brand-sub">BY ARROWSTACK</span></span></a><span class="status">{{offline?'Offline session':store.saved?store.syncStatus:'Saving…'}}</span><button class="quiet" @click="drawer='Account'" aria-label="Account and settings">{{user.name.split(' ')[0]}}</button></header>
|
||||
<main>
|
||||
<nav class="workspace-tabs" aria-label="Workspace"><button :class="{active:!campaignMode}" @click="campaignMode=false"><UiIcon name="sheet" />Characters</button><button :class="{active:campaignMode}" @click="campaignMode=true"><UiIcon name="notes" />Campaigns</button></nav>
|
||||
<nav class="workspace-tabs" aria-label="Workspace"><button :class="{active:!campaignMode}" :aria-pressed="!campaignMode" @click="campaignMode=false"><UiIcon name="sheet" />Characters</button><button :class="{active:campaignMode}" :aria-pressed="campaignMode" @click="campaignMode=true"><UiIcon name="notes" />Campaigns</button></nav>
|
||||
<CampaignsView v-if="campaignMode" :characters="store.all" :sync-characters="store.flush" />
|
||||
<div class="character-switch" v-if="!campaignMode"><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>
|
||||
@ -50,19 +52,26 @@ function openAction(action:Character['actions'][number]){selectedAction.value=ac
|
||||
<template v-if="tab==='Sheet'">
|
||||
<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>
|
||||
<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="actionEditor()">+ 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')"><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><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="actions-folio" v-if="tab==='Actions'" aria-labelledby="actions-heading">
|
||||
<div class="folio-heading"><h2 id="actions-heading"><UiIcon name="actions" />Actions</h2><button class="quiet folio-add" @click="actionEditor()">+ Add</button></div>
|
||||
<div class="folio-empty" v-if="!c.actions.length"><UiIcon name="actions" /><h3>Your next move starts here.</h3><p>Add weapon attacks, features and custom actions with your own modifiers.</p><button class="primary" @click="actionEditor()">+ Add your first action</button></div>
|
||||
<article class="folio-action" v-for="action in c.actions" :key="action.id">
|
||||
<div class="folio-action-main"><UiIcon name="actions" class="folio-action-icon" /><div class="folio-action-copy"><h3>{{action.name}}</h3><p class="folio-modifier">{{signed(action.bonus)}} {{action.damage?'to hit':'modifier'}}</p><p v-if="action.damage" class="folio-damage">{{action.damage}}</p></div><button class="primary folio-roll" @click="openAction(action)" :aria-label="`Roll ${action.name}`"><UiIcon name="initiative" />Roll</button></div>
|
||||
<details class="folio-options"><summary>Notes & options</summary><p v-if="action.notes" class="folio-action-notes">{{action.notes}}</p><div class="folio-tools"><button class="quiet" @click="actionEditor(action)">Edit action</button><button class="quiet danger-text" @click="c.actions=c.actions.filter(a=>a.id!==action.id)" :aria-label="`Remove ${action.name}`">Remove</button></div></details>
|
||||
</article>
|
||||
</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 sync to your Arrowstack account and stay available on this device for offline play. Export backups to keep an extra copy.</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.3 · Made for game night</span></footer>
|
||||
<footer v-if="campaignMode||tab!=='Actions'">✦ ARROWSTACK CHARACTER <span>v0.3 · Made for game night</span></footer>
|
||||
</main>
|
||||
<nav class="bottom-nav" v-if="c&&!campaignMode"><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" :class="{'character-drawer':drawer==='Character'}" role="dialog" aria-modal="true" :aria-label="drawer"><div class="section-title drawer-heading"><h2><UiIcon :name="drawerIcon" />{{drawer==='Rest'?(restType==='long-rest'?'Long rest':'Short rest'):drawer}}</h2><button class="close" @click="drawer=''" aria-label="Close dialog">×</button></div>
|
||||
<div v-if="drawer" class="overlay" @click.self="drawer=''"><section class="drawer" :class="{'character-drawer':drawer==='Character'}" role="dialog" aria-modal="true" :aria-label="drawer==='Add action'&&editingActionId?'Edit action':drawer"><div class="section-title drawer-heading"><h2><UiIcon :name="drawerIcon" />{{drawer==='Rest'?(restType==='long-rest'?'Long rest':'Short rest'):drawer==='Add action'&&editingActionId?'Edit action':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 sync to your account and remain available on this device for offline play.</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" class="character-form" @submit.prevent="saveDraft">
|
||||
<div class="editor-intro"><UiIcon name="sheet" /><div><p class="eyebrow">THE NEXT CHAPTER IS YOURS</p><h3>Bring your hero to life.</h3><p>A name, a calling, a story worth telling.</p></div></div>
|
||||
@ -74,7 +83,7 @@ function openAction(action:Character['actions'][number]){selectedAction.value=ac
|
||||
<div class="folio-submit"><button class="primary wide"><UiIcon name="sheet" />Save character<span aria-hidden="true">✦</span></button></div>
|
||||
</form>
|
||||
<template v-if="drawer==='Hit points'&&c"><p class="big-number">{{c.hp.current}} <small>/ {{c.hp.max}}</small></p><div class="quick-hp"><button v-for="n in [-10,-5,-1,1,5,10]" @click="hp(n)">{{signed(n)}}</button></div><label>Amount<input type="number" v-model.number="hpAmount" min="1" max="9999"></label><div class="form-grid"><button class="danger-text" @click="hp(-Math.max(0,Math.trunc(hpAmount||0)))">Take damage</button><button class="primary" @click="hp(Math.max(0,Math.trunc(hpAmount||0)))">Heal</button></div><label>Temporary HP<input type="number" :value="c.hp.temp" @change="c.hp.temp=Math.max(0,Math.min(9999,Math.trunc(Number(($event.target as HTMLInputElement).value)||0)))" min="0" max="9999"></label><p class="hint">Damage uses temporary HP first. Healing cannot exceed your maximum.</p></template>
|
||||
<form v-if="drawer==='Add action'" @submit.prevent="addAction"><label>Name<input v-model="actionDraft.name" required maxlength="120"></label><div class="form-grid"><label>Attack bonus<input type="number" v-model.number="actionDraft.bonus" min="-50" max="100" required></label><label>Damage<input v-model="actionDraft.damage" placeholder="1d8 + 4 piercing" maxlength="120"></label></div><label>Notes<textarea v-model="actionDraft.notes" maxlength="12000"></textarea></label><button class="primary wide">Add action</button></form>
|
||||
<form v-if="drawer==='Add action'" @submit.prevent="addAction"><label>Name<input v-model="actionDraft.name" required maxlength="120"></label><div class="form-grid"><label>Attack bonus<input type="number" v-model.number="actionDraft.bonus" min="-50" max="100" required></label><label>Damage<input v-model="actionDraft.damage" placeholder="1d8 + 4 piercing" maxlength="120"></label></div><label>Notes<textarea v-model="actionDraft.notes" maxlength="12000"></textarea></label><button class="primary wide">{{editingActionId?'Save action':'Add action'}}</button></form>
|
||||
<form v-if="drawer==='Add resource'" @submit.prevent="addResource"><label>Name<input v-model="resourceDraft.name" placeholder="Level 1 spell slots" required maxlength="120"></label><div class="form-grid"><label>Remaining<input type="number" v-model.number="resourceDraft.current" min="0" :max="resourceDraft.max" required></label><label>Maximum<input type="number" v-model.number="resourceDraft.max" min="0" max="9999" required></label></div><label>Recovery<select v-model="resourceDraft.reset"><option value="short-rest">Short or long rest</option><option value="long-rest">Long rest</option><option value="manual">Manual only</option></select></label><button class="primary wide">Add resource</button></form>
|
||||
<template v-if="drawer==='Manage resources'&&c"><div class="resource" v-for="r in c.resources"><b>{{r.name}}</b><button class="quiet danger-text" @click="c.resources=c.resources.filter(x=>x.id!==r.id)">Remove</button></div><button @click="drawer='Add resource'">+ Add resource</button></template>
|
||||
<form v-if="drawer==='Add spell'" @submit.prevent="addSpell"><label>Name<input v-model="spellDraft.name" required maxlength="120"></label><label>Level (0 = cantrip)<input type="number" v-model.number="spellDraft.level" min="0" max="9" required></label><label>Spell notes<textarea v-model="spellDraft.notes" rows="6" maxlength="12000" placeholder="Casting time, range, components, duration, effects…"></textarea></label><button class="primary wide">Add spell</button></form>
|
||||
|
||||
@ -73,3 +73,26 @@ input[type=checkbox]{accent-color:#3b603d}
|
||||
.bottom-nav .ui-icon{width:35px;height:35px}
|
||||
.workspace-tabs{display:flex;gap:10px;margin-bottom:24px}.workspace-tabs button{display:flex;align-items:center;justify-content:center;gap:9px;flex:1;background:#faeac47d;box-shadow:none;border-color:#b19459}.workspace-tabs button.active{background:#35543a;color:#fff0c8;border-color:#9e8648}.workspace-tabs .ui-icon{width:30px;height:30px}.campaign-welcome{text-align:center}.campaign-welcome>.ui-icon{width:75px;height:75px;margin:10px}.campaign-welcome h1{font-size:36px}.campaign-card{display:flex;align-items:center;gap:15px;width:100%;text-align:left;margin-bottom:14px}.campaign-card>.ui-icon{width:44px;height:44px}.campaign-card>span:nth-child(2){flex:1}.campaign-card small,.member-entry small,.encounter-entry small{display:block;font-size:12px;font-weight:400;margin-top:6px;color:#6b674a}.campaign-toolbar{display:flex;justify-content:space-between;margin-bottom:20px}.campaign-emblem{width:75px;height:80px}.campaign-tabs{display:flex;gap:8px;overflow-x:auto;padding-bottom:12px;margin-bottom:12px;scrollbar-width:thin}.campaign-tabs button{white-space:nowrap;font-size:12px;box-shadow:none;background:#f8e9c3}.campaign-tabs button.active{background:#d9ddb1;border-color:#6c7b48}.campaign-actions{display:flex;align-items:center;flex-wrap:wrap;gap:9px;margin:13px 0}.campaign-actions button{font-size:12px;padding:9px 12px}.campaign-actions>label{margin:0}.campaign-actions>input{width:60%;flex:1}.campaign-prose{white-space:pre-wrap;overflow-wrap:anywhere;font-size:14px;line-height:1.75}.party-entry,.member-entry,.encounter-entry{border-top:1px solid #b4915359;padding:15px 0}.party-entry h3{font-family:Georgia,serif;font-size:24px;font-weight:400;margin:0}.party-stats{display:flex;flex-wrap:wrap;gap:18px}.party-stats>span{display:flex;gap:7px;align-items:center;font-size:14px}.party-stats .ui-icon{width:27px;height:27px}.party-stats small{font-size:10px}.inline-check{display:flex;align-items:center;gap:9px;line-height:1.6}.inline-check input{flex-shrink:0}.member-entry{display:flex;justify-content:space-between;gap:12px;align-items:center}.encounter-entry.current{background:#dce0b5;border:1px solid #9ba969;border-radius:8px;padding:13px;margin-top:8px}.invite-code{display:block;user-select:all;overflow-wrap:anywhere;padding:15px;margin-top:18px;background:#f5e6bf;border:1px solid #b38e4d;border-radius:6px;font-size:12px}.campaign-workspace .panel{background:linear-gradient(120deg,#fff7dfed,#f8e8bdeb)}
|
||||
@media(max-width:700px){.workspace-tabs button{font-size:12px;padding:10px}.campaign-welcome h1{font-size:30px}.campaign-emblem{width:55px;height:65px}.campaign-tabs{margin-right:-16px;padding-right:16px}.member-entry{align-items:flex-start;flex-wrap:wrap}.member-entry .campaign-actions{margin:5px 0 0}.party-entry .section-title{align-items:flex-start;flex-wrap:wrap}.campaign-workspace .section-title h2{font-size:22px}.campaign-workspace .panel>h2{margin-bottom:18px}}
|
||||
|
||||
/* Open folio: restrained navigation and parchment action entries. */
|
||||
.workspace-tabs{display:flex;gap:16px;border-bottom:1px solid #a8884d;margin-bottom:16px;padding:0 4px 9px;position:relative}
|
||||
.workspace-tabs:after,.folio-heading:after{content:'◇';position:absolute;left:50%;bottom:-9px;transform:translateX(-50%);color:#94733e;background:#f5e2b5;padding:0 8px;font-size:16px;line-height:16px}
|
||||
.workspace-tabs button{flex:1;border:0;border-radius:0;background:transparent;box-shadow:none;padding:8px 6px;min-height:48px;color:#6f6343;position:relative}
|
||||
.workspace-tabs button:hover{background:#ac86391a}.workspace-tabs button.active{background:transparent;color:#253c32;box-shadow:none}
|
||||
.workspace-tabs button.active:after{content:'';position:absolute;bottom:0;left:28%;right:28%;height:3px;background:#37583f;border-radius:2px}
|
||||
.character-switch{margin-bottom:22px}.hero{margin-bottom:26px}.hero .edit{min-height:44px}
|
||||
.actions-folio{margin:0 0 26px;padding:0;min-width:0}
|
||||
.folio-heading{display:flex;justify-content:space-between;align-items:center;gap:12px;position:relative;border-top:1px solid #ad8d51;border-bottom:1px solid #ad8d51;padding:18px 0;margin-bottom:10px}
|
||||
.folio-heading h2{display:flex;align-items:center;gap:14px;font-size:30px;line-height:1.2}.folio-heading h2 .ui-icon{width:48px;height:48px}
|
||||
.folio-add{font-size:15px;color:#284e3d;flex-shrink:0;min-height:44px;padding:8px}
|
||||
.folio-empty{text-align:center;max-width:440px;margin:auto;padding:34px 12px 38px}.folio-empty>.ui-icon{width:88px;height:88px;margin-bottom:12px}
|
||||
.folio-empty h3{font-family:Georgia,serif;font-size:25px;font-weight:400;line-height:1.3;margin:0 0 12px}.folio-empty p{font-size:14px;line-height:1.7;color:#6e6549;margin:0 auto 24px;max-width:340px}.folio-empty .primary{padding:12px 18px}
|
||||
.folio-action{border-bottom:1px solid #b49761;padding:20px 0 10px}.folio-action-main{display:flex;gap:15px;align-items:center}.folio-action-icon{width:42px;height:42px}.folio-action-copy{flex:1;min-width:0;overflow-wrap:anywhere}
|
||||
.folio-action h3{font-family:Georgia,serif;font-size:25px;font-weight:400;line-height:1.2;margin:0 0 6px}.folio-action p{margin:4px 0}.folio-modifier{font-size:13px;color:#6e6549}.folio-damage{font-family:Georgia,serif;font-size:17px;line-height:1.4}
|
||||
.folio-roll{display:flex;align-items:center;justify-content:center;gap:7px;flex-shrink:0;min-height:44px;padding:9px 13px}.folio-roll .ui-icon{width:22px;height:22px}
|
||||
.folio-options{margin-left:57px;font-size:13px;color:#635b43}.folio-options summary{cursor:pointer;min-height:44px;padding:14px 0;width:fit-content}.folio-options summary:focus-visible{outline:3px solid #b78a3e;outline-offset:3px}
|
||||
.folio-options .folio-action-notes{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.7;background:#c19d4420;border-left:2px solid #a3874e;padding:12px 15px;color:#4e503d}.folio-tools{display:flex;gap:12px}.folio-tools .quiet{min-height:44px;padding-left:0}
|
||||
.bottom-nav{border-radius:0;border-left:0;border-right:0;border-top:1px solid #ad8d51;box-shadow:none;backdrop-filter:none;padding-top:6px;padding-bottom:max(8px,env(safe-area-inset-bottom));background:#fff3d3f7}
|
||||
.bottom-nav button,.bottom-nav button:hover{box-shadow:none;background:transparent;min-height:62px}.bottom-nav button.active{background:#dce0b4;box-shadow:none;border-radius:8px}
|
||||
@media(max-width:700px){.workspace-tabs{gap:10px;padding-bottom:9px}.workspace-tabs button{font-size:13px;padding:8px 4px}.workspace-tabs .ui-icon{width:32px;height:32px}.hero{gap:12px;margin-bottom:22px}.hero .edit{margin-top:0;padding:8px 0}.folio-heading{padding:14px 0}.folio-heading h2{font-size:27px;gap:10px}.folio-heading h2 .ui-icon{width:40px;height:40px}.folio-action-main{gap:10px}.folio-action-icon{width:33px;height:33px}.folio-action h3{font-size:22px}.folio-options{margin-left:43px}.folio-roll{padding:9px 11px;font-size:13px}.folio-roll .ui-icon{width:20px;height:20px}.folio-empty{padding:28px 6px 32px}.folio-empty h3{font-size:24px}.bottom-nav{width:100%}.bottom-nav .ui-icon{width:34px;height:34px}}
|
||||
@media(max-width:360px){.folio-action-icon{display:none}.folio-options{margin-left:0}.folio-roll .ui-icon{display:none}.folio-action h3{font-size:21px}}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user