213 lines
6.2 KiB
Svelte
213 lines
6.2 KiB
Svelte
<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>
|