Files
shd.build/src/lib/components/DatabaseItemCard.svelte
T
jb 57068b61cd
Validate and deploy production / validate (push) Canceled after 0s
Validate and deploy production / deploy (push) Canceled after 0s
Validate and deploy staging / validate (push) Successful in 28s
Validate and deploy staging / deploy (push) Successful in 1m3s
feat: expand ISAC content modeling
2026-08-03 01:23:20 -04:00

128 lines
9.7 KiB
Svelte

<script lang="ts">
import GameIcon from './GameIcon.svelte';
import ItemIcon from './ItemIcon.svelte';
import ItemIconRail from './ItemIconRail.svelte';
import DatabaseFacts from './DatabaseFacts.svelte';
import type { GameIconName } from '$lib/game-icons';
import type { DatabaseItem } from '$lib/database/items';
import MarkdownContent from './MarkdownContent.svelte';
import WeaponModSlotIcon from './WeaponModSlotIcon.svelte';
let {
item,
hideDiscussionUtility = false,
anchor = true
}: {
item: DatabaseItem & { gameIcon?: GameIconName };
hideDiscussionUtility?: boolean;
anchor?: boolean;
} = $props();
let protocolLevel = $state(1);
let talentUpgrades = $state(0);
const number = (value: number) => Number.isInteger(value) ? String(value) : value.toFixed(2).replace(/\.?0+$/, '');
const valueWithUnit = (value: number, unit: string) => `${number(value)}${unit ? (unit === '%' ? '%' : ` ${unit}`) : ''}`;
const protocolPrimaryValue = () => item.protocolPreview
? item.protocolPreview.primaryBaseValue * (1 + (protocolLevel - 1) / 49)
: 0;
const talentDescription = () => item.protocolPreview?.talentDescription?.replace(/\{(\d+)\}/g, (placeholder, raw) => {
const effect = item.protocolPreview?.talentEffects[Number(raw) - 1];
return effect ? valueWithUnit(effect.baseValue * (1 + talentUpgrades / 5), effect.unit) : placeholder;
});
</script>
<article id={anchor ? `database-item-${item.id}` : undefined} class="database-item-card" class:talent-card={item.variant === 'talent'} class:protocol-quality-standard={item.topBorderTone === 'standard'} class:protocol-quality-specialized={item.topBorderTone === 'specialized'} class:protocol-quality-superior={item.topBorderTone === 'superior'} class:protocol-quality-highend={item.topBorderTone === 'highend'}>
<header>
{#if item.renderUrl}<img class="database-item-render" src={item.renderUrl} alt="" loading="lazy"/>{/if}
<div class="database-item-icon">
{#if item.gameIcon}<GameIcon name={item.gameIcon} decorative/>{:else}<ItemIcon src={item.iconUrl} alt=""/>{/if}
</div>
<div class="database-item-identity">
<small>{item.kicker}</small>
<h2>{item.name}</h2>
</div>
{#if item.headerBadges?.length || item.headerBadge || item.headerMetaIcon}
<div class="database-header-status">
{#if item.headerMetaIcon}<GameIcon name={item.headerMetaIcon}/>{/if}
{#each item.headerBadges ?? [] as badge}<span class="database-header-badge" class:exotic={badge === 'EXOTIC'}>{badge}</span>{/each}
{#if item.headerBadge}<span class="database-header-badge" class:protocol-badge-standard={item.headerBadgeTone === 'standard'} class:protocol-badge-specialized={item.headerBadgeTone === 'specialized'} class:protocol-badge-superior={item.headerBadgeTone === 'superior'} class:protocol-badge-highend={item.headerBadgeTone === 'highend'}>{item.headerBadge}</span>{/if}
</div>
{/if}
</header>
{#if item.badges?.length}<div class="database-item-badges">{#each item.badges as badge}<span class={badge.tone ?? ''}>{badge.label}</span>{/each}</div>{/if}
{#if item.protocolPreview}
<section class="database-protocol-preview">
<div class="database-protocol-primary"><span>{item.protocolPreview.primaryStat}</span><b>{Number.isFinite(protocolPrimaryValue()) ? valueWithUnit(protocolPrimaryValue(), item.protocolPreview.primaryUnit) : 'NOT CONFIGURED'}</b></div>
{#if item.protocolPreview.talentDescription}
<div class="database-protocol-talent"><p>{talentDescription()}</p></div>
{/if}
<div class="database-protocol-controls"><label class="database-protocol-level"><span>LVL {protocolLevel}</span><input type="range" min="1" max="50" step="1" bind:value={protocolLevel} aria-label={`${item.name} level`}/></label>{#if item.protocolPreview.talentDescription}<div class="database-protocol-diamonds" role="group" aria-label={`${item.name} talent upgrades`}>{#each [1, 2, 3, 4, 5] as upgrade}<button type="button" class:active={talentUpgrades >= upgrade} aria-label={`Set talent upgrades to ${upgrade}`} aria-pressed={talentUpgrades === upgrade} onclick={() => talentUpgrades = talentUpgrades === upgrade ? 0 : upgrade}>◆</button>{/each}</div>{/if}</div>
</section>
{:else}
<DatabaseFacts facts={item.facts} layout={item.factLayout}/>
{/if}
{#if item.weaponModSlots}
<div class="database-weapon-mod-slots">
<div><span>MOD SLOTS</span></div>
{#if item.weaponModSlots.length}
<ul aria-label={`${item.name} Weapon Mod slots`}>
{#each item.weaponModSlots as modSlot}
<li class:upgrade-slot={modSlot.unlockedAtTierOne} class:has-slot-icon={Boolean(modSlot.iconUrl)} title={`${modSlot.slot}${modSlot.unlockedAtTierOne ? ' - Unlocked when this weapon upgrades to Tier 1' : ''}`}>
{#if modSlot.iconUrl}<WeaponModSlotIcon slot={modSlot.slot} iconUrl={modSlot.iconUrl}/><span class="sr-only">{modSlot.slot}</span>{:else}<span>{modSlot.slot}</span>{/if}
{#if modSlot.unlockedAtTierOne}<small>T1+</small>{/if}
</li>
{/each}
</ul>
{:else}<p>MOD SLOT DATA NOT CONFIGURED</p>{/if}
</div>
{/if}
{#if item.lines?.length}
<div class="database-item-lines">{#each item.lines as line}<div><span>{line.label}</span><b>{line.value}</b></div>{/each}</div>
{/if}
{#if item.iconGroups?.length}
<div class="database-item-icon-groups">
{#each item.iconGroups as group}
<div><span>{group.label}</span><ItemIconRail label={group.label} items={group.items} emptyLabel={group.emptyLabel}/></div>
{/each}
</div>
{/if}
{#if item.variant === 'talent'}
<div class="database-talent-tiers">
<section><span>TIER I</span><p>{item.description ?? 'Description is not available.'}</p></section>
<section><span>TIER II</span><p>{item.secondaryDescription ?? 'Description is not available.'}</p></section>
</div>
{:else if !item.protocolPreview}
{#if item.description}<p class:data-description={item.descriptionStyle === 'data'}>{item.description}</p>{/if}
{#if item.secondaryDescription}<p class="secondary-description" class:data-description={item.descriptionStyle === 'data'}>{item.secondaryDescription}</p>{/if}
{/if}
{#if item.afterLines?.length}
<div class="database-item-lines after-description">{#each item.afterLines as line}<div><span>{line.label}</span><b>{line.value}</b></div>{/each}</div>
{/if}
{#if item.pinnedCommunityNoteHtml}<aside class="database-community-note"><strong>COMMUNITY NOTE</strong><MarkdownContent html={item.pinnedCommunityNoteHtml} compact/></aside>{/if}
{#if item.actions?.length || item.correctionHref || (!hideDiscussionUtility && item.discussionHref)}
<footer class="database-item-footer">
{#if item.actions?.length}<div class="database-item-actions">{#each item.actions as action}<a href={action.href}>{action.label} →</a>{/each}</div>{/if}
<div class="database-item-utilities" aria-label="Community actions">
{#if item.correctionHref}
<a class="database-utility-button correction" href={item.correctionHref} aria-label={`Report an inaccuracy in ${item.name}`} title="Report an inaccuracy">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 21V4M6 5h11l-2.75 3L17 11H6"/></svg>
</a>
{/if}
{#if !hideDiscussionUtility && item.discussionHref}
<a class="database-utility-button discussion" href={item.discussionHref} aria-label={`Open discussion for ${item.name}${item.discussionCount ? `, ${item.discussionCount} messages` : ', no messages yet'}`} title="Open discussion" aria-haspopup="dialog">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5.75h14v9.5H9.25L5 18.75v-13Z"/><path d="M8.25 9h7.5M8.25 12h5"/></svg>
{#if item.discussionCount}<span>{item.discussionCount}</span>{/if}
</a>
{/if}
</div>
</footer>
{/if}
</article>
<style>
.database-community-note{display:grid;gap:5px;margin:12px 0 0;padding:10px;border:1px solid var(--shd-border);border-left:2px solid var(--shd-armor-border);background:linear-gradient(90deg,var(--shd-armor-surface),var(--shd-surface-raised))}.database-community-note>strong{color:var(--shd-armor-label);font-size:var(--shd-type-telemetry);letter-spacing:.7px}.database-community-note :global(.markdown-content){color:var(--shd-text-muted);font-size:var(--shd-type-supporting)}.database-community-note :global(.markdown-content a){color:var(--shd-armor-text)}
.database-item-footer{display:flex;align-items:flex-end;justify-content:space-between;gap:8px;margin:10px 12px 12px;padding-top:9px;border-top:1px solid var(--shd-border)}.database-item-footer .database-item-actions{min-width:0;display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0;border:0}.database-item-utilities{display:flex;align-items:center;gap:4px;margin-left:auto}.database-utility-button{height:32px;min-width:32px;display:inline-flex;align-items:center;justify-content:center;gap:4px;padding:0 7px;border:1px solid var(--shd-border);background:var(--shd-surface);color:var(--shd-text-dim)}.database-utility-button svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:square;stroke-linejoin:miter}.database-utility-button span{min-width:10px;color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:800;line-height:1;text-align:center}.database-utility-button:hover,.database-utility-button:focus-visible{border-color:var(--shd-border-strong);background:var(--shd-surface-active);color:var(--shd-text)}.database-utility-button:focus-visible{outline:2px solid var(--shd-orange);outline-offset:2px}.database-utility-button:active{transform:translateY(1px);filter:brightness(.84)}
@media(max-width:650px){.database-item-footer{align-items:stretch}.database-item-utilities{align-self:end}.database-utility-button{width:44px;height:44px;padding:0}.database-utility-button.discussion:has(span){width:auto;min-width:44px;padding:0 10px}}
@media(prefers-reduced-motion:reduce){.database-utility-button{transition:none}}
</style>