You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

299 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<script lang="ts">
import type { Metadata, MetadataField, MetadataFieldType } from '../../lib';
import { validateMetadata, field as f } from '../../lib';
import FieldRow from './FieldRow.svelte';
import AddFieldModal from './MetaAddFieldModal.svelte';
import AddFieldMenu from "@/mtas/svelte/components/MetaAddFieldMenu.svelte";
import {makeLingRecord} from "@/ling/lib";
import { flip } from 'svelte/animate';
import type { GlobState } from '@/glob/adapters/svelte';
let {
value = $bindable<Metadata>({}),
dark = false,
attributes = [],
pagination = true,
glob,
locales = ['es'],
defaultLocale = 'es',
onUpdate,
}: {
value?: Metadata;
dark?: boolean;
attributes?: string[];
pagination?: boolean;
glob: GlobState;
locales?: string[];
defaultLocale?: string;
onUpdate?: (metadata: Metadata) => void;
} = $props();
let drawerOpen = $state(false);
let drawerType = $state<MetadataFieldType>('string');
// ── Pagination ────────────────────────────────────────────────
let page = $state(1);
let pageSize = $state(10);
const PAGE_SIZES = [5, 10, 20, 50];
let keyOrder = $derived(
value.order ?? Object.keys(value).filter(k => k !== 'order')
);
let totalPages = $derived(
pageSize === 0 ? 1 : Math.max(1, Math.ceil(keyOrder.length / pageSize))
);
let pageKeys = $derived(
pageSize === 0 ? keyOrder : keyOrder.slice((page - 1) * pageSize, page * pageSize)
);
let rangeStart = $derived(pageSize === 0 ? 1 : (page - 1) * pageSize + 1);
let rangeEnd = $derived(pageSize === 0 ? keyOrder.length : Math.min(page * pageSize, keyOrder.length));
$effect(() => { if (page > totalPages) page = Math.max(1, totalPages); });
let paginationPages = $derived((() => {
if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1);
const pages: number[] = [1];
if (page > 3) pages.push(-1);
for (let p = Math.max(2, page - 1); p <= Math.min(totalPages - 1, page + 1); p++) pages.push(p);
if (page < totalPages - 2) pages.push(-1);
pages.push(totalPages);
return pages;
})());
let validation = $derived(validateMetadata(value));
function emit(): void { onUpdate?.(value); }
function defaultField(type: MetadataFieldType): MetadataField {
switch (type) {
case 'string': return f.string('');
case 'number': return f.number(0);
case 'boolean': return f.boolean(false);
case 'ling': return f.ling(makeLingRecord(''));
case 'date': return f.date('');
case 'datetime': return f.datetime('');
case 'url': return f.url('');
case 'currency': return f.currency({ amount: 0, currency: 'EUR' });
case 'image': return f.image({ url: '' });
case 'video': return f.video({ url: '' });
case 'audio': return f.audio({ url: '' });
case 'pdf': return f.pdf({ url: '' });
case 'file': return f.file({ url: '' });
case 'html': return f.html(makeLingRecord(''));
case 'color': return f.color({ hex: '#000000' });
case 'select': return f.select(null, []);
case 'multiselect': return f.multiselect([], []);
case 'email': return f.email('');
case 'phone': return f.phone({ number: '' });
case 'slug': return f.slug('');
case 'coordinates': return f.coordinates({ lat: 0, lng: 0 });
case 'markdown': return f.markdown(makeLingRecord(''));
case 'group': return f.group({});
}
}
function openDrawer(type: MetadataFieldType): void { drawerType = type; drawerOpen = true; }
function handleDrawerConfirm(key: string, field: MetadataField): void {
value = { ...value, [key]: field, order: [...keyOrder, key] };
emit();
}
function updateFieldKey(oldKey: string, newKey: string): void {
if (newKey === oldKey || newKey in value) return;
const next: Metadata = {};
for (const k of Object.keys(value)) {
if (k === 'order') continue;
next[k === oldKey ? newKey : k] = value[k];
}
next.order = keyOrder.map(k => k === oldKey ? newKey : k);
value = next;
emit();
}
function updateField(key: string, field: MetadataField): void {
value = { ...value, [key]: field };
emit();
}
function deleteField(key: string): void {
const next = { ...value };
delete next[key];
next.order = keyOrder.filter(k => k !== key);
value = next;
emit();
}
function getField(key: string): MetadataField { return value[key] as MetadataField; }
function moveField(key: string, dir: -1 | 1): void {
const idx = keyOrder.indexOf(key);
const newIdx = idx + dir;
if (newIdx < 0 || newIdx >= keyOrder.length) return;
const newOrder = [...keyOrder];
newOrder.splice(idx, 1);
newOrder.splice(newIdx, 0, key);
value = { ...value, order: newOrder };
emit();
if (pageSize > 0) {
const newPage = Math.floor(newIdx / pageSize) + 1;
if (newPage !== page) page = newPage;
}
}
</script>
<div class="h-full flex flex-col min-h-0">
<div class="rounded-lg border border-gray-800 bg-gray-900 shadow-md w-full flex flex-col flex-1 min-h-0">
<!-- ── Header ─────────────────────────────────────────────── -->
<div class="flex items-center gap-3 px-4 py-3 border-b border-gray-700 bg-gray-800/60 shrink-0">
<!-- Validation indicator -->
<div
title={validation.valid
? glob.t('vcen.editor.mtas.validation.valid')
: glob.t('vcen.editor.mtas.validation.errorCount', { count: validation.errors.length })}
>
<div class={[
'h-2 w-2 rounded-full shrink-0 transition-colors',
validation.valid ? 'bg-green-500' : 'bg-red-500 animate-pulse',
].join(' ')}></div>
</div>
<h2 class="font-semibold text-sm text-gray-200">
{glob.t('vcen.editor.mtas.header.title')}
</h2>
<span class="inline-flex items-center rounded px-1.5 py-0.5 text-xs bg-gray-800 text-gray-500 border border-gray-700">
{glob.t('vcen.editor.mtas.header.fieldCount', { count: keyOrder.length })}
</span>
<div class="ml-auto">
<AddFieldMenu onSelect={openDrawer} {glob} />
</div>
</div>
<!-- ── Field list ──────────────────────────────────────────── -->
<div class="flex flex-col gap-2 p-3 overflow-y-auto flex-1 min-h-0">
{#if keyOrder.length === 0}
<div class="rounded-2xl flex flex-col items-center justify-center gap-3 border-2 border-dashed border-gray-700 py-16 text-gray-700">
<svg class="h-10 w-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
<rect x="3" y="3" width="8" height="8" rx="2"/>
<rect x="13" y="3" width="8" height="8" rx="2"/>
<rect x="3" y="13" width="8" height="8" rx="2"/>
<rect x="13" y="13" width="8" height="8" rx="2" opacity=".4"/>
</svg>
<div class="flex flex-col items-center gap-1">
<span class="text-sm font-medium">{glob.t('vcen.editor.mtas.field.empty')}</span>
</div>
</div>
{:else}
{#each pageKeys as key (key)}
{@const gi = keyOrder.indexOf(key)}
<div animate:flip={{ duration: 180 }} class="w-full">
<FieldRow
fieldKey={key}
field={getField(key)}
{attributes} {glob} {locales} {defaultLocale}
canMoveUp={gi > 0}
canMoveDown={gi < keyOrder.length - 1}
onKeyChange={(newKey) => updateFieldKey(key, newKey)}
onFieldChange={(field) => updateField(key, field)}
onDelete={() => deleteField(key)}
onMoveUp={() => moveField(key, -1)}
onMoveDown={() => moveField(key, 1)}
/>
</div>
{/each}
{/if}
</div>
<!-- ── Validation errors ───────────────────────────────────── -->
{#if !validation.valid}
<div class="px-3 pb-3 shrink-0">
<div class="flex items-start gap-2 rounded border border-red-900 bg-red-950 px-3 py-2.5 text-red-400">
<svg class="h-4 w-4 shrink-0" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm0 3.5a.75.75 0 0 1 .75.75v3a.75.75 0 0 1-1.5 0v-3A.75.75 0 0 1 8 4.5zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</svg>
<div class="flex flex-col gap-0.5">
<span class="font-medium text-sm">
{glob.t('vcen.editor.mtas.validation.errorsTitle')}
</span>
{#each validation.errors as error}
<span class="text-xs opacity-80">· {error}</span>
{/each}
</div>
</div>
</div>
{/if}
<!-- ── Pagination ──────────────────────────────────────────── -->
{#if pagination && keyOrder.length > 0}
<div class="flex items-center justify-between gap-3 border-t border-gray-700 bg-gray-800/40 px-4 py-2.5 shrink-0">
<div class="flex items-center gap-2 shrink-0">
<span class="text-xs text-gray-600 whitespace-nowrap">
{glob.t('vcen.editor.mtas.pagination.rowsPerPage')}
</span>
<select
class="rounded border border-gray-700 bg-gray-800 px-1.5 py-1 text-xs text-gray-200 focus:outline-none"
bind:value={pageSize}
onchange={() => { page = 1; }}
>
{#each PAGE_SIZES as s}
<option value={s}>{s}</option>
{/each}
<option value={0}>{glob.t('vcen.editor.mtas.pagination.all')}</option>
</select>
</div>
{#if totalPages > 1}
<div class="flex">
<button
type="button"
class="px-2 py-1 text-xs border border-gray-700 text-gray-400 hover:bg-gray-800 hover:text-gray-200 transition-colors first:rounded-l last:rounded-r -ml-px disabled:opacity-40"
disabled={page === 1}
onclick={() => page--}
aria-label={glob.t('vcen.editor.mtas.pagination.prevPage')}
>‹</button>
{#each paginationPages as p (p === -1 ? `e-${p}` : p)}
{#if p === -1}
<button type="button" class="px-2 py-1 text-xs border border-gray-700 text-gray-400 -ml-px opacity-40 cursor-default" tabindex="-1">…</button>
{:else}
<button
type="button"
class={['px-2 py-1 text-xs border border-gray-700 text-gray-400 hover:bg-gray-800 hover:text-gray-200 transition-colors first:rounded-l last:rounded-r -ml-px', p === page ? 'bg-gray-700 text-gray-200' : ''].join(' ')}
onclick={() => page = p}
aria-label={glob.t('vcen.editor.mtas.pagination.pageN', { page: p })}
aria-current={p === page ? 'page' : undefined}
>{p}</button>
{/if}
{/each}
<button
type="button"
class="px-2 py-1 text-xs border border-gray-700 text-gray-400 hover:bg-gray-800 hover:text-gray-200 transition-colors first:rounded-l last:rounded-r -ml-px disabled:opacity-40"
disabled={page === totalPages}
onclick={() => page++}
aria-label={glob.t('vcen.editor.mtas.pagination.nextPage')}
>›</button>
</div>
{/if}
<span class="text-xs text-gray-600 shrink-0">
{glob.t('vcen.editor.mtas.pagination.range', { from: rangeStart, to: rangeEnd, total: keyOrder.length })}
</span>
</div>
{/if}
</div>
</div>
<AddFieldModal
open={drawerOpen}
selectedType={drawerType}
existingKeys={keyOrder} {attributes} {dark} {glob} {locales} {defaultLocale}
onConfirm={handleDrawerConfirm}
onClose={() => drawerOpen = false}
/>

Powered by TurnKey Linux.