92 lines
5.7 KiB
Svelte
92 lines
5.7 KiB
Svelte
<script lang="ts">
|
||
import PendingForm from '$lib/components/PendingForm.svelte';
|
||
import ImageUploadInput from '$lib/components/ImageUploadInput.svelte';
|
||
import RegistryPagination from '$lib/components/RegistryPagination.svelte';
|
||
let { data, form } = $props();
|
||
let adding = $state(false);
|
||
let editing = $state<any>(null);
|
||
let isIcon = $state(true);
|
||
let resetToken = $state(0);
|
||
let consumedToken = $state<string | null>(null);
|
||
const size = (bytes: unknown) => {
|
||
const value = Number(bytes ?? 0);
|
||
return value < 1024 * 1024 ? `${(value / 1024).toFixed(0)} KiB` : `${(value / 1024 / 1024).toFixed(1)} MiB`;
|
||
};
|
||
$effect(() => {
|
||
if (form?.imageSaved && form?.resultToken && form.resultToken !== consumedToken) {
|
||
consumedToken = form.resultToken;
|
||
if (form.editorMode === 'edit') editing = null;
|
||
else {
|
||
adding = true;
|
||
isIcon = true;
|
||
resetToken += 1;
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<svelte:head><title>Image library — ISAC</title></svelte:head>
|
||
<div class="module-toolbar">
|
||
<div><p class="eyebrow">SYSTEM CONFIGURATION</p><h2>IMAGE LIBRARY</h2><p>Process, classify, and locate reusable SHD.build visual assets.</p></div>
|
||
<div><button class="primary" type="button" onclick={() => { adding = true; editing = null; }}>UPLOAD IMAGE +</button></div>
|
||
</div>
|
||
<div class="module-kpis">
|
||
<div><span>ASSETS</span><strong>{Number(data.stats.total)}</strong><small>Managed files</small></div>
|
||
<div><span>ICONS</span><strong>{Number(data.stats.icons)}</strong><small>Normalized assets</small></div>
|
||
<div><span>CATEGORIES</span><strong>{Number(data.stats.categories)}</strong><small>Assignments in use</small></div>
|
||
<div><span>STORAGE</span><strong>{size(data.stats.bytes)}</strong><small>Processed WebP</small></div>
|
||
</div>
|
||
|
||
{#if adding}
|
||
<section class="horizontal-editor image-library-editor">
|
||
<div class="editor-title"><div><p class="eyebrow">NEW MANAGED ASSET</p><h3>UPLOAD + PROCESS</h3></div><button type="button" aria-label="Cancel image upload" onclick={() => adding = false}>×</button></div>
|
||
<PendingForm action="?/upload" enctype="multipart/form-data" successMessage="Image added to the library.">
|
||
<div class="image-library-form">
|
||
<div class="image-library-upload"><ImageUploadInput name="image" accept="image/svg+xml,image/png,image/webp,image/avif,image/jpeg" required {resetToken} emptyLabel="SELECT IMAGE"/></div>
|
||
<div class="image-library-metadata">
|
||
<label class="field">ASSET NAME<input name="name" maxlength="80" required placeholder="e.g. Weapon critical hit"/></label>
|
||
<label class="field">ASSIGNMENT CATEGORY<select name="category" required>{#each data.categories as category}<option>{category}</option>{/each}</select></label>
|
||
<label class="field full">TAGS · COMMA SEPARATED<input name="tags" maxlength="620" placeholder="critical, weapon, offensive"/></label>
|
||
<div class="image-processing-options">
|
||
<label><input name="isIcon" type="checkbox" bind:checked={isIcon}/><span><b>NORMALIZE AS ICON</b><small>Remove an opaque background when needed, trim margins, and fit within 256 × 256.</small></span></label>
|
||
<label class:disabled={!isIcon}><input name="forceWhite" type="checkbox" checked disabled={!isIcon}/><span><b>FORCE WHITE</b><small>Replace visible icon color while preserving transparency.</small></span></label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{#if form?.imageError}<p class="error">{form.imageError}</p>{/if}
|
||
<div class="form-actions"><button class="secondary" type="button" onclick={() => adding = false}>CANCEL</button><button class="primary">PROCESS + SAVE →</button></div>
|
||
</PendingForm>
|
||
</section>
|
||
{/if}
|
||
|
||
{#if editing}
|
||
<section class="horizontal-editor image-library-editor compact">
|
||
<div class="editor-title"><div><p class="eyebrow">EDIT LIBRARY METADATA</p><h3>{editing.name}</h3></div><button type="button" aria-label="Cancel image editing" onclick={() => editing = null}>×</button></div>
|
||
<PendingForm action="?/update" successMessage="Image metadata updated.">
|
||
<input type="hidden" name="id" value={editing.id}/>
|
||
<div class="image-metadata-edit">
|
||
<img src={editing.url} alt=""/>
|
||
<label class="field">ASSET NAME<input name="name" value={editing.name} maxlength="80" required/></label>
|
||
<label class="field">ASSIGNMENT CATEGORY<select name="category">{#each data.categories as category}<option selected={category === editing.category}>{category}</option>{/each}</select></label>
|
||
<label class="field">TAGS<input name="tags" value={editing.tags.join(', ')} maxlength="620"/></label>
|
||
</div>
|
||
{#if form?.imageError}<p class="error">{form.imageError}</p>{/if}
|
||
<div class="form-actions"><button class="secondary" type="button" onclick={() => editing = null}>CANCEL</button><button class="primary">SAVE CHANGES →</button></div>
|
||
</PendingForm>
|
||
</section>
|
||
{/if}
|
||
|
||
<RegistryPagination pagination={data.pagination} search={data.search} searchPlaceholder="SEARCH NAMES, TAGS, OR CATEGORIES"/>
|
||
<section class="image-library-grid" aria-label="Managed image library">
|
||
{#each data.images as image}
|
||
<article class:icon={image.is_icon}>
|
||
<div class="image-library-preview"><img src={image.url} alt="Preview of {image.name}"/></div>
|
||
<header><div><strong>{image.name}</strong><span>{image.category}</span></div><b>{image.is_icon ? 'ICON' : 'IMAGE'}</b></header>
|
||
<div class="image-library-tags">{#each image.tags as tag}<span>{tag}</span>{/each}{#if !image.tags.length}<small>NO TAGS</small>{/if}</div>
|
||
<footer><span>{image.width} × {image.height} · {size(image.byte_size)}</span><button class="table-action" type="button" onclick={() => { editing = image; adding = false; }}>EDIT →</button></footer>
|
||
</article>
|
||
{:else}
|
||
<div class="image-library-empty"><strong>NO MATCHING ASSETS</strong><span>Upload the first image or revise the registry search.</span></div>
|
||
{/each}
|
||
</section>
|