Files
shd.build/src/routes/isac/images/+page.svelte
T

92 lines
5.7 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 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>