Checkpoint SHD dashboard design system
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { getSessionUser } from '$lib/server/auth/session';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
export const load: LayoutServerLoad = async ({ cookies }) => {
|
||||
const user = await getSessionUser(cookies);
|
||||
if (!user) redirect(303, '/sign-in');
|
||||
if (user.role !== 'admin') redirect(303, '/account');
|
||||
return {};
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
let { children } = $props();
|
||||
const sections = [
|
||||
{ href: '/admin/stats', label: 'Stats & attributes', code: 'ATTR' },
|
||||
{ href: '/admin/talents', label: 'Talents & triggers', code: 'TLNT' },
|
||||
{ href: '/admin/equipment', label: 'Equipment types', code: 'EQPT', pending: true },
|
||||
{ href: '/admin/weapons', label: 'Weapons', code: 'WPN', pending: true }
|
||||
];
|
||||
</script>
|
||||
|
||||
<main class="admin-page">
|
||||
<header class="admin-command">
|
||||
<div class="command-title"><p class="eyebrow"><span></span> SHD DATA OPERATIONS</p><h1>ADMIN CONSOLE</h1><p>Embedded game-data control surface</p></div>
|
||||
<div class="command-status"><div><i></i><span>DATABASE</span><strong>ONLINE</strong></div><div><span>ACCESS</span><strong>ADMIN</strong></div><div><span>ENVIRONMENT</span><strong>LOCAL</strong></div></div>
|
||||
</header>
|
||||
<div class="admin-dashboard">
|
||||
<aside class="admin-sidebar">
|
||||
<div class="sidebar-label"><span>DATA MANAGEMENT</span><b>04 MODULES</b></div>
|
||||
<nav aria-label="Admin sections">
|
||||
{#each sections as section}
|
||||
<a class:active={page.url.pathname === section.href} class:pending={section.pending} href={section.href}>
|
||||
<small>{section.code}</small><span>{section.label}</span>{#if section.pending}<em>SOON</em>{:else}<b>›</b>{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
<div class="sidebar-foot"><span>SHD NODE</span><strong>NYC-01</strong><small>Schema synchronized</small></div>
|
||||
</aside>
|
||||
<section class="admin-content">{@render children()}</section>
|
||||
</div>
|
||||
</main>
|
||||
@@ -0,0 +1,2 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
export const load = () => redirect(303, '/admin/stats');
|
||||
@@ -0,0 +1,35 @@
|
||||
import { getSessionUser } from '$lib/server/auth/session';
|
||||
import { requireAdmin } from '$lib/server/auth/authorization';
|
||||
import { getDatabase } from '$lib/server/database';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const database = await getDatabase();
|
||||
return { stats: await (await database.prepare(`SELECT * FROM stat_attributes ORDER BY active DESC, name COLLATE NOCASE`)).all() };
|
||||
};
|
||||
export const actions: Actions = {
|
||||
save: async ({ request, cookies }) => {
|
||||
requireAdmin(await getSessionUser(cookies));
|
||||
const form = await request.formData();
|
||||
const id = String(form.get('id') ?? ''), name = String(form.get('name') ?? '').normalize('NFKC').trim();
|
||||
const type = String(form.get('type') ?? ''), category = String(form.get('category') ?? ''), calculation = String(form.get('calculationType') ?? ''), unit = String(form.get('unit') ?? 'flat');
|
||||
if (name.length < 2 || name.length > 80) return fail(400, { error: 'Attribute names must be 2–80 characters.' });
|
||||
if (!['Offensive','Defensive'].includes(type) || !['Weapon','Skill','Character'].includes(category) || !['Additive','Multiplicative'].includes(calculation)) return fail(400, { error: 'Choose valid attribute options.' });
|
||||
const number = (key: string) => { const raw = String(form.get(key) ?? '').trim(); if (!raw) return null; const value = Number(raw); if (!Number.isFinite(value) || value < 0) throw new Error(); return value; };
|
||||
let values: Array<number|null>;
|
||||
try { values = ['gearMin','gearMax','weaponMin','weaponMax','chipMin','chipMax','capValue'].map(number); } catch { return fail(400, { error: 'Ranges must be positive numbers.' }); }
|
||||
for (let index = 0; index < 6; index += 2) if ((values[index] === null) !== (values[index+1] === null) || (values[index] !== null && Number(values[index]) > Number(values[index+1]))) return fail(400, { error: 'Each enabled source needs a valid minimum and maximum.' });
|
||||
const database = await getDatabase();
|
||||
if (await (await database.prepare('SELECT id FROM stat_attributes WHERE name = ? COLLATE NOCASE AND id <> ?')).get(name, id || 'new')) return fail(409, { error: 'That attribute already exists.' });
|
||||
const now = new Date().toISOString();
|
||||
if (id) await (await database.prepare(`UPDATE stat_attributes SET name=?,type=?,category=?,calculation_type=?,unit=?,gear_min=?,gear_max=?,weapon_min=?,weapon_max=?,skill_chip_min=?,skill_chip_max=?,cap_value=?,updated_at=? WHERE id=?`)).run(name,type,category,calculation,unit,...values,now,id);
|
||||
else await (await database.prepare(`INSERT INTO stat_attributes(id,name,type,category,calculation_type,unit,gear_min,gear_max,weapon_min,weapon_max,skill_chip_min,skill_chip_max,cap_value,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)).run(randomUUID(),name,type,category,calculation,unit,...values,now,now);
|
||||
return { saved: true };
|
||||
},
|
||||
archive: async ({ request, cookies }) => {
|
||||
requireAdmin(await getSessionUser(cookies)); const id = String((await request.formData()).get('id') ?? '');
|
||||
await (await (await getDatabase()).prepare('UPDATE stat_attributes SET active=0,updated_at=? WHERE id=?')).run(new Date().toISOString(),id); return { archived: true };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import PendingForm from '$lib/components/PendingForm.svelte';
|
||||
let { data, form } = $props();
|
||||
let editing = $state<any>(null);
|
||||
const blank = () => ({id:'',name:'',type:'Offensive',category:'Weapon',calculation_type:'Additive',unit:'%',gear_min:'',gear_max:'',weapon_min:'',weapon_max:'',skill_chip_min:'',skill_chip_max:'',cap_value:''});
|
||||
const value = (input: unknown) => input ?? '';
|
||||
</script>
|
||||
<svelte:head><title>Stats & attributes — SHD.build admin</title></svelte:head>
|
||||
<div class="module-toolbar"><div><p class="eyebrow">ATTRIBUTE REGISTRY</p><h2>STATS & ATTRIBUTES</h2><p>Builder values, hidden calculation targets, source ranges, and caps.</p></div><button class="primary" onclick={() => editing=blank()}>ADD ATTRIBUTE <span>+</span></button></div>
|
||||
<div class="module-kpis"><div><span>TOTAL RECORDS</span><strong>{data.stats.length}</strong><small>All definitions</small></div><div><span>ACTIVE</span><strong>{data.stats.filter((item)=>Number(item.active)===1).length}</strong><small>Available to systems</small></div><div><span>OFFENSIVE</span><strong>{data.stats.filter((item)=>item.type==='Offensive').length}</strong><small>Damage-oriented</small></div><div><span>DEFENSIVE</span><strong>{data.stats.filter((item)=>item.type==='Defensive').length}</strong><small>Survival-oriented</small></div></div>
|
||||
{#if editing}<div class="dashboard-editor"><div class="editor-title"><div><p class="eyebrow">{editing.id?'MODIFY RECORD':'NEW RECORD'}</p><h3>{editing.id?editing.name:'ATTRIBUTE DEFINITION'}</h3></div><button aria-label="Close editor" onclick={() => editing=null}>×</button></div><PendingForm action="?/save" class="grid"><input type="hidden" name="id" value={editing.id}/><div class="form-grid"><label class="field">NAME<input name="name" bind:value={editing.name} required /></label><label class="field">TYPE<select name="type" bind:value={editing.type}><option>Offensive</option><option>Defensive</option></select></label><label class="field">CATEGORY<select name="category" bind:value={editing.category}><option>Weapon</option><option>Skill</option><option>Character</option></select></label><label class="field">CALCULATION<select name="calculationType" bind:value={editing.calculation_type}><option>Additive</option><option>Multiplicative</option></select></label><label class="field">UNIT<input name="unit" bind:value={editing.unit}/></label>{#each [['gear','GEAR'],['weapon','WEAPON'],['skill_chip','SKILL CHIP']] as source}<div class="field range-field"><span class="label">{source[1]} RANGE · OPTIONAL</span><div><input name={`${source[0]==='skill_chip'?'chip':source[0]}Min`} type="number" step="any" value={value(editing[`${source[0]}_min`])} placeholder="MIN"/><input name={`${source[0]==='skill_chip'?'chip':source[0]}Max`} type="number" step="any" value={value(editing[`${source[0]}_max`])} placeholder="MAX"/></div></div>{/each}<label class="field">OPTIONAL CAP<input name="capValue" type="number" step="any" value={value(editing.cap_value)}/></label></div>{#if form?.error}<p class="error">{form.error}</p>{/if}<div class="form-actions"><button type="button" class="secondary" onclick={() => editing=null}>CANCEL</button><button class="primary">SAVE ATTRIBUTE →</button></div></PendingForm></div>{/if}
|
||||
<div class="dashboard-table"><div class="dashboard-row table-heading"><span>ATTRIBUTE</span><span>CLASSIFICATION</span><span>CATEGORY</span><span>CALCULATION</span><span>STATE</span><span></span></div>{#each data.stats as stat}<div class="dashboard-row" class:archived={Number(stat.active)!==1}><div><strong>{stat.name}</strong><small>{stat.unit}</small></div><span class:type-offensive={stat.type==='Offensive'} class:type-defensive={stat.type==='Defensive'}>{stat.type}</span><span>{stat.category}</span><span>{stat.calculation_type}</span><b class="state-dot">{Number(stat.active)===1?'ACTIVE':'ARCHIVED'}</b><button onclick={() => editing={...stat}}>EDIT</button></div>{/each}</div>
|
||||
@@ -0,0 +1,59 @@
|
||||
import { getSessionUser } from '$lib/server/auth/session';
|
||||
import { requireAdmin } from '$lib/server/auth/authorization';
|
||||
import { getDatabase } from '$lib/server/database';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const db = await getDatabase();
|
||||
const [talents, tiers, triggers, links, effects, stats] = await Promise.all([
|
||||
(await db.prepare('SELECT * FROM talents ORDER BY active DESC,name COLLATE NOCASE')).all(),
|
||||
(await db.prepare('SELECT id,talent_id,tier,description FROM talent_tiers')).all(),
|
||||
(await db.prepare('SELECT * FROM talent_triggers ORDER BY active DESC,name COLLATE NOCASE')).all(),
|
||||
(await db.prepare('SELECT talent_tier_id,trigger_id FROM talent_tier_triggers')).all(),
|
||||
(await db.prepare('SELECT talent_tier_id,stat_attribute_id,value FROM talent_tier_effects')).all(),
|
||||
(await db.prepare('SELECT id,name,unit FROM stat_attributes WHERE active=1 ORDER BY name COLLATE NOCASE')).all()
|
||||
]);
|
||||
return { talents: talents.map((talent) => ({...talent, tiers: tiers.filter((tier) => tier.talent_id === talent.id).map((tier) => ({...tier, triggerIds: links.filter((link) => link.talent_tier_id === tier.id).map((link) => String(link.trigger_id)), effects: effects.filter((effect) => effect.talent_tier_id === tier.id)}))})), triggers, stats };
|
||||
};
|
||||
const parseEffects = (raw: FormDataEntryValue | null) => {
|
||||
const value = JSON.parse(String(raw ?? '[]')) as Array<{statId:string;value:string|number}>;
|
||||
if (!Array.isArray(value)) throw new Error();
|
||||
return value.map((effect) => ({statId:String(effect.statId),value:Number(effect.value)}));
|
||||
};
|
||||
export const actions: Actions = {
|
||||
saveTrigger: async ({request,cookies}) => {
|
||||
requireAdmin(await getSessionUser(cookies)); const form=await request.formData();
|
||||
const id=String(form.get('id')??''), name=String(form.get('name')??'').trim(), description=String(form.get('description')??'').trim(), uptime=form.get('supportsUptime')==='on';
|
||||
const min=uptime?Number(form.get('minUptime')):null,max=uptime?Number(form.get('maxUptime')):null;
|
||||
if(name.length<2||!description)return fail(400,{triggerError:'A trigger name and description are required.'});
|
||||
if(uptime&&(!Number.isFinite(min)||!Number.isFinite(max)||Number(min)<0||Number(max)>100||Number(min)>Number(max)))return fail(400,{triggerError:'Uptime must be a valid 0–100% range.'});
|
||||
const db=await getDatabase(); if(await(await db.prepare('SELECT id FROM talent_triggers WHERE name=? COLLATE NOCASE AND id<>?')).get(name,id||'new'))return fail(409,{triggerError:'That trigger already exists.'});
|
||||
const now=new Date().toISOString();
|
||||
if(id)await(await db.prepare('UPDATE talent_triggers SET name=?,description=?,supports_uptime=?,min_uptime=?,max_uptime=?,updated_at=? WHERE id=?')).run(name,description,uptime?1:0,min,max,now,id);
|
||||
else await(await db.prepare('INSERT INTO talent_triggers(id,name,description,supports_uptime,min_uptime,max_uptime,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?)')).run(randomUUID(),name,description,uptime?1:0,min,max,now,now);
|
||||
return {triggerSaved:true};
|
||||
},
|
||||
saveTalent: async ({request,cookies}) => {
|
||||
requireAdmin(await getSessionUser(cookies)); const form=await request.formData();
|
||||
const id=String(form.get('id')??''),name=String(form.get('name')??'').trim(),category=String(form.get('category')??'');
|
||||
const descriptions={I:String(form.get('tierIDescription')??'').trim(),II:String(form.get('tierIIDescription')??'').trim()};
|
||||
const triggerIds={I:form.getAll('tierITriggerIds').map(String),II:form.getAll('tierIITriggerIds').map(String)};
|
||||
let tierEffects:{I:Array<{statId:string;value:number}>;II:Array<{statId:string;value:number}>};
|
||||
try{tierEffects={I:parseEffects(form.get('tierIEffects')),II:parseEffects(form.get('tierIIEffects'))};}catch{return fail(400,{talentError:'Effects could not be read.'});}
|
||||
if(name.length<2||!['Weapon','Backpack','Vest'].includes(category))return fail(400,{talentError:'Provide a valid talent name and category.'});
|
||||
for(const tier of ['I','II'] as const)if(!descriptions[tier]||!triggerIds[tier].length||!tierEffects[tier].length||tierEffects[tier].some((effect)=>!effect.statId||!Number.isFinite(effect.value)))return fail(400,{talentError:`Tier ${tier} requires a description, trigger, and valid attribute effects.`});
|
||||
const db=await getDatabase(); if(await(await db.prepare('SELECT id FROM talents WHERE name=? COLLATE NOCASE AND category=? AND id<>?')).get(name,category,id||'new'))return fail(409,{talentError:'That talent already exists in this category.'});
|
||||
const talentId=id||randomUUID(),now=new Date().toISOString();
|
||||
if(id)await(await db.prepare('UPDATE talents SET name=?,category=?,updated_at=? WHERE id=?')).run(name,category,now,id);else await(await db.prepare('INSERT INTO talents(id,name,category,created_at,updated_at) VALUES(?,?,?,?,?)')).run(talentId,name,category,now,now);
|
||||
for(const tier of ['I','II'] as const){
|
||||
await(await db.prepare(`INSERT INTO talent_tiers(id,talent_id,tier,description,trigger_text,effect_text,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?) ON CONFLICT(talent_id,tier) DO UPDATE SET description=excluded.description,updated_at=excluded.updated_at`)).run(randomUUID(),talentId,tier,descriptions[tier],'','',now,now);
|
||||
const tierRow=await(await db.prepare('SELECT id FROM talent_tiers WHERE talent_id=? AND tier=?')).get(talentId,tier); const tierId=String(tierRow?.id);
|
||||
await(await db.prepare('DELETE FROM talent_tier_triggers WHERE talent_tier_id=?')).run(tierId); await(await db.prepare('DELETE FROM talent_tier_effects WHERE talent_tier_id=?')).run(tierId);
|
||||
for(const triggerId of triggerIds[tier])await(await db.prepare('INSERT INTO talent_tier_triggers(talent_tier_id,trigger_id) VALUES(?,?)')).run(tierId,triggerId);
|
||||
for(const effect of tierEffects[tier])await(await db.prepare('INSERT INTO talent_tier_effects(id,talent_tier_id,stat_attribute_id,value,created_at) VALUES(?,?,?,?,?)')).run(randomUUID(),tierId,effect.statId,effect.value,now);
|
||||
}
|
||||
return {talentSaved:true};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import PendingForm from '$lib/components/PendingForm.svelte';
|
||||
let {data,form}=$props();
|
||||
type Effect={statId:string;value:string};
|
||||
let talent=$state<any>(null),trigger=$state<any>(null),uptime=$state(false);
|
||||
const blankTalent=()=>({id:'',name:'',category:'Weapon',I:{description:'',triggerIds:[],effects:[{statId:'',value:''}]},II:{description:'',triggerIds:[],effects:[{statId:'',value:''}]}});
|
||||
const editTalent=(row:any)=>{const tier=(name:string)=>row.tiers.find((item:any)=>item.tier===name)??{description:'',triggerIds:[],effects:[]};const one=tier('I'),two=tier('II');talent={...row,I:{description:one.description,triggerIds:[...one.triggerIds],effects:one.effects.map((e:any)=>({statId:String(e.stat_attribute_id),value:String(e.value)}))},II:{description:two.description,triggerIds:[...two.triggerIds],effects:two.effects.map((e:any)=>({statId:String(e.stat_attribute_id),value:String(e.value)}))}}};
|
||||
const blankTrigger=()=>{uptime=false;trigger={id:'',name:'',description:'',min_uptime:'',max_uptime:''}};
|
||||
const editTrigger=(row:any)=>{uptime=Number(row.supports_uptime)===1;trigger={...row}};
|
||||
const toggle=(tier:'I'|'II',id:string,checked:boolean)=>talent[tier].triggerIds=checked?[...talent[tier].triggerIds,id]:talent[tier].triggerIds.filter((value:string)=>value!==id);
|
||||
const addEffect=(tier:'I'|'II')=>talent[tier].effects=[...talent[tier].effects,{statId:'',value:''}];
|
||||
const removeEffect=(tier:'I'|'II',index:number)=>talent[tier].effects=talent[tier].effects.filter((_:Effect,row:number)=>row!==index);
|
||||
const configured=(tiers:Array<{tier:unknown}>,name:string)=>tiers.some((item)=>item.tier===name);
|
||||
</script>
|
||||
<svelte:head><title>Talents & triggers — SHD.build admin</title></svelte:head>
|
||||
<div class="module-toolbar"><div><p class="eyebrow">TALENT REGISTRY</p><h2>TALENTS & TRIGGERS</h2><p>Tier behavior, reusable activation conditions, uptime, and attribute effects.</p></div><div><button class="secondary" onclick={blankTrigger}>ADD TRIGGER +</button><button class="primary" onclick={()=>talent=blankTalent()}>ADD TALENT +</button></div></div>
|
||||
<div class="module-kpis"><div><span>TALENTS</span><strong>{data.talents.length}</strong><small>Registered definitions</small></div><div><span>TRIGGERS</span><strong>{data.triggers.length}</strong><small>Reusable conditions</small></div><div><span>WEAPON</span><strong>{data.talents.filter((item)=>item.category==='Weapon').length}</strong><small>Weapon talents</small></div><div><span>GEAR</span><strong>{data.talents.filter((item)=>item.category!=='Weapon').length}</strong><small>Vest and backpack</small></div></div>
|
||||
{#if trigger}<div class="dashboard-editor"><div class="editor-title"><div><p class="eyebrow">{trigger.id?'MODIFY TRIGGER':'NEW TRIGGER'}</p><h3>{trigger.id?trigger.name:'TRIGGER DEFINITION'}</h3></div><button aria-label="Close trigger editor" onclick={()=>trigger=null}>×</button></div><PendingForm action="?/saveTrigger"><input type="hidden" name="id" value={trigger.id}/><div class="form-grid"><label class="field">NAME<input name="name" bind:value={trigger.name} required/></label><label class="field">DESCRIPTION<textarea name="description" bind:value={trigger.description} required></textarea></label><label class="field uptime-field"><span><input name="supportsUptime" type="checkbox" bind:checked={uptime}/> ENABLE THEORETICAL UPTIME</span><div><input name="minUptime" type="number" min="0" max="100" bind:value={trigger.min_uptime} disabled={!uptime} placeholder="MIN %"/><input name="maxUptime" type="number" min="0" max="100" bind:value={trigger.max_uptime} disabled={!uptime} placeholder="MAX %"/></div></label><p class="script-note"><b>CALCULATION SCRIPT</b><span>Reserved for the future calculation engine</span></p></div>{#if form?.triggerError}<p class="error">{form.triggerError}</p>{/if}<div class="form-actions"><button type="button" class="secondary" onclick={()=>trigger=null}>CANCEL</button><button class="primary">SAVE TRIGGER →</button></div></PendingForm></div>{/if}
|
||||
{#if talent}<div class="dashboard-editor"><div class="editor-title"><div><p class="eyebrow">{talent.id?'MODIFY TALENT':'NEW TALENT'}</p><h3>{talent.id?talent.name:'TALENT DEFINITION'}</h3></div><button aria-label="Close talent editor" onclick={()=>talent=null}>×</button></div><PendingForm action="?/saveTalent"><input type="hidden" name="id" value={talent.id}/><input type="hidden" name="tierIEffects" value={JSON.stringify(talent.I.effects)}/><input type="hidden" name="tierIIEffects" value={JSON.stringify(talent.II.effects)}/><div class="form-grid talent-identity"><label class="field">NAME<input name="name" bind:value={talent.name} required/></label><label class="field">CATEGORY<select name="category" bind:value={talent.category}><option>Weapon</option><option>Backpack</option><option>Vest</option></select></label></div><div class="tier-grid">{#each ['I','II'] as tier}<fieldset class="tier-card"><legend>TIER {tier}</legend><label class="field">DISPLAY DESCRIPTION<textarea name={tier==='I'?'tierIDescription':'tierIIDescription'} bind:value={talent[tier].description} required></textarea></label><p class="label">ACTIVATION TRIGGERS</p><div class="trigger-list">{#each data.triggers.filter((item)=>Number(item.active)===1) as item}<div><label><input name={tier==='I'?'tierITriggerIds':'tierIITriggerIds'} type="checkbox" value={item.id} checked={talent[tier].triggerIds.includes(String(item.id))} onchange={(event)=>toggle(tier as 'I'|'II',String(item.id),event.currentTarget.checked)}/> <span>{item.name}</span></label><button type="button" onclick={()=>editTrigger(item)}>EDIT</button></div>{/each}</div><div class="effects-head"><p class="label">ATTRIBUTE EFFECTS</p><button type="button" onclick={()=>addEffect(tier as 'I'|'II')}>ADD EFFECT +</button></div>{#each talent[tier].effects as effect,index}<div class="talent-effect"><select bind:value={effect.statId} required><option value="">Select attribute…</option>{#each data.stats as stat}<option value={stat.id}>{stat.name}</option>{/each}</select><input type="number" step="any" bind:value={effect.value} placeholder="VALUE" required/><button type="button" onclick={()=>removeEffect(tier as 'I'|'II',index)}>×</button></div>{/each}</fieldset>{/each}</div>{#if form?.talentError}<p class="error">{form.talentError}</p>{/if}<div class="form-actions"><button type="button" class="secondary" onclick={()=>talent=null}>CANCEL</button><button class="primary">SAVE TALENT →</button></div></PendingForm></div>{/if}
|
||||
<div class="dashboard-table talent-dashboard-table"><div class="dashboard-row table-heading"><span>TALENT</span><span>CATEGORY</span><span>TIER I</span><span>TIER II</span><span>STATE</span><span></span></div>{#each data.talents as item}<div class="dashboard-row"><div><strong>{item.name}</strong><small>Tiered talent</small></div><span>{item.category}</span><span class:configured={configured(item.tiers,'I')}>{configured(item.tiers,'I')?'CONFIGURED':'MISSING'}</span><span class:configured={configured(item.tiers,'II')}>{configured(item.tiers,'II')?'CONFIGURED':'MISSING'}</span><b class="state-dot">ACTIVE</b><button onclick={()=>editTalent(item)}>EDIT</button></div>{/each}</div>
|
||||
Reference in New Issue
Block a user