Files
shd.build/src/lib/components/WeaponModSlotIconPanel.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

213 lines
6.2 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import IconUploadField from '$lib/components/IconUploadField.svelte';
import PendingForm from '$lib/components/PendingForm.svelte';
import WeaponModSlotIcon from '$lib/components/WeaponModSlotIcon.svelte';
type SlotIcon = {
id: string;
name: string;
iconUrl: string;
updatedAt: string;
};
let {
slots,
error = '',
saved = false,
resultToken = ''
}: {
slots: SlotIcon[];
error?: string;
saved?: boolean;
resultToken?: string;
} = $props();
let editing = $state<SlotIcon | null>(null);
let handledResultToken = $state('');
$effect(() => {
if (saved && resultToken && resultToken !== handledResultToken) {
handledResultToken = resultToken;
editing = null;
}
});
</script>
<section class="slot-icon-registry" aria-labelledby="slot-icon-registry-title">
<div class="selection-head">
<div>
<span id="slot-icon-registry-title">SHARED MOD SLOT ICONS</span>
<small>ONE CANONICAL ICON FOR EACH WEAPON MOD SLOT</small>
</div>
<b>{slots.every((slot) => slot.iconUrl) ? 'READY' : 'CONFIG REQUIRED'}</b>
</div>
<div class="slot-icon-grid">
{#each slots as slot}
<button
type="button"
class:configured={Boolean(slot.iconUrl)}
aria-pressed={editing?.id === slot.id}
onclick={() => editing = slot}
>
<span class="slot-icon-preview">
{#if slot.iconUrl}
<WeaponModSlotIcon slot={slot.name} iconUrl={slot.iconUrl}/>
{:else}
<i aria-hidden="true">?</i>
{/if}
</span>
<span><strong>{slot.name}</strong><small>{slot.iconUrl ? 'ICON REGISTERED' : 'ICON REQUIRED'}</small></span>
<b>EDIT</b>
</button>
{/each}
</div>
</section>
{#if editing}
<section class="dashboard-editor slot-icon-editor" aria-labelledby="slot-icon-editor-title">
<div class="editor-title">
<div><p class="eyebrow">SHARED MOD SLOT ARTWORK</p><h3 id="slot-icon-editor-title">{editing.name.toUpperCase()}</h3></div>
<button aria-label="Close slot icon editor" onclick={() => editing = null}>×</button>
</div>
<PendingForm action="?/saveSlotIcon" enctype="multipart/form-data">
<input type="hidden" name="id" value={editing.id}/>
<input type="hidden" name="name" value={`${editing.name} Slot`}/>
<input type="hidden" name="slotType" value={editing.name}/>
<input type="hidden" name="baseUpdatedAt" value={editing.updatedAt}/>
<div class="slot-icon-editor-body">
<div class="slot-icon-editor-identity">
<span class="slot-icon-preview large">
{#if editing.iconUrl}
<WeaponModSlotIcon slot={editing.name} iconUrl={editing.iconUrl} size={56}/>
{:else}
<i aria-hidden="true">?</i>
{/if}
</span>
<div><small>CANONICAL SLOT</small><strong>{editing.name}</strong><p>Weapon cards use this artwork anywhere the slot is available.</p></div>
</div>
<IconUploadField currentUrl={editing.iconUrl} label={`${editing.name.toUpperCase()} SLOT ICON`} resetToken={`${editing.id}:${editing.updatedAt}`}/>
</div>
{#if error}<p class="error" role="alert">{error}</p>{/if}
<div class="form-actions"><button type="button" class="secondary" onclick={() => editing = null}>CANCEL</button><button class="primary">SAVE SLOT ICON →</button></div>
</PendingForm>
</section>
{/if}
<style>
.slot-icon-registry {
margin: 10px 0;
padding: 10px;
border: 1px solid var(--shd-border);
border-left: 2px solid var(--shd-info);
background: #0d1011;
}
.slot-icon-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 7px;
margin-top: 8px;
}
.slot-icon-grid > button {
min-height: 62px;
display: grid;
grid-template-columns: 38px minmax(0, 1fr) auto;
align-items: center;
gap: 9px;
padding: 8px;
border: 1px solid var(--shd-border);
background: #101415;
color: var(--shd-text);
text-align: left;
cursor: pointer;
}
.slot-icon-grid > button:hover,
.slot-icon-grid > button:focus-visible,
.slot-icon-grid > button[aria-pressed='true'] {
border-color: var(--shd-orange);
background: #191712;
}
.slot-icon-grid > button:focus-visible {
outline: 2px solid var(--shd-orange);
outline-offset: 2px;
}
.slot-icon-grid > button > span:nth-child(2) {
min-width: 0;
display: grid;
gap: 3px;
}
.slot-icon-grid strong,
.slot-icon-grid small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.slot-icon-grid strong {
font-size: var(--shd-type-label);
letter-spacing: .04em;
}
.slot-icon-grid small,
.slot-icon-grid > button > b {
color: var(--shd-text-muted);
font-size: var(--shd-type-micro);
letter-spacing: .07em;
}
.slot-icon-grid > button.configured small,
.slot-icon-grid > button.configured > b {
color: var(--shd-info);
}
.slot-icon-preview {
width: 38px;
height: 38px;
display: grid;
place-items: center;
border: 1px solid #313839;
background: #090b0c;
}
.slot-icon-preview i {
color: #586164;
font-size: 20px;
font-style: normal;
font-weight: 900;
}
.slot-icon-editor {
border-top-color: var(--shd-info);
}
.slot-icon-editor-body {
display: grid;
grid-template-columns: minmax(260px, .7fr) minmax(360px, 1.3fr);
gap: 12px;
}
.slot-icon-editor-identity {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border: 1px solid var(--shd-border);
background: #0d1011;
}
.slot-icon-preview.large {
width: 76px;
height: 76px;
flex: 0 0 76px;
}
.slot-icon-editor-identity > div {
display: grid;
gap: 5px;
}
.slot-icon-editor-identity small,
.slot-icon-editor-identity p {
color: var(--shd-text-muted);
font-size: var(--shd-type-telemetry);
}
.slot-icon-editor-identity p {
margin: 0;
}
@media (max-width: 900px) {
.slot-icon-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.slot-icon-editor-body { grid-template-columns: 1fr; }
}
@media (max-width: 480px) {
.slot-icon-grid { grid-template-columns: 1fr; }
}
</style>