dev 7 months ago
parent d1fbf455cf
commit 4bf0c9c678

@ -41,7 +41,9 @@ function makeGlob(locale = 'es') {
numr : { minDecimals: 0, maxDecimals: 2 }, numr : { minDecimals: 0, maxDecimals: 2 },
curr : { definitions: currDefinitions, selectedCurrency: 'EUR' }, curr : { definitions: currDefinitions, selectedCurrency: 'EUR' },
dati : { selectedTimeFormat: '24h' }, dati : { selectedTimeFormat: '24h' },
unit : { selectedSystem: 'metric' }, unit : {
selectedSystem: 'metric'
},
}); });
} }

@ -0,0 +1,132 @@
import type { LingRecord } from '@/ling/lib';
import { p } from '@/ling/lib';
/**
* Traducciones del editor de metadata.
* Namespace: vcen.editor.mtas
*
* Para registrar en ling:
* ling.extend('vcen', { editor: { mtas: mtasTranslations } })
*
* Los componentes reciben la instancia glob completa:
* <MetadataEditor {glob} ... />
*
* Y acceden a las traducciones via:
* glob.t('vcen.editor.mtas.header.title')
* glob.t('vcen.editor.mtas.field.nestedCount', { count: 3 })
* glob.numr.f(1234.5)
* glob.curr.f(99.5)
* glob.dati.f(new Date('2024-01-15'))
*/
export const translations = {
// ── Tipos de campo ────────────────────────────────────────────────
types: {
string: { es: 'Texto', en: 'Text' } satisfies LingRecord,
number: { es: 'Número', en: 'Number' } satisfies LingRecord,
boolean: { es: 'Bool', en: 'Bool' } satisfies LingRecord,
ling: { es: 'i18n', en: 'i18n' } satisfies LingRecord,
date: { es: 'Fecha', en: 'Date' } satisfies LingRecord,
datetime: { es: 'DateTime', en: 'DateTime' } satisfies LingRecord,
url: { es: 'URL', en: 'URL' } satisfies LingRecord,
currency: { es: 'Moneda', en: 'Currency' } satisfies LingRecord,
image: { es: 'Imagen', en: 'Image' } satisfies LingRecord,
video: { es: 'Vídeo', en: 'Video' } satisfies LingRecord,
audio: { es: 'Audio', en: 'Audio' } satisfies LingRecord,
pdf: { es: 'PDF', en: 'PDF' } satisfies LingRecord,
file: { es: 'Fichero', en: 'File' } satisfies LingRecord,
html: { es: 'HTML', en: 'HTML' } satisfies LingRecord,
group: { es: 'Grupo', en: 'Group' } satisfies LingRecord,
},
// ── Grupos del menú ───────────────────────────────────────────────
typeGroups: {
text: { es: 'Texto', en: 'Text' } satisfies LingRecord,
number: { es: 'Número', en: 'Number' } satisfies LingRecord,
date: { es: 'Fecha', en: 'Date' } satisfies LingRecord,
logical: { es: 'Lógico', en: 'Boolean' } satisfies LingRecord,
media: { es: 'Media', en: 'Media' } satisfies LingRecord,
group: { es: 'Grupo', en: 'Group' } satisfies LingRecord,
},
// ── Encabezado ────────────────────────────────────────────────────
header: {
title: { es: 'Metadata', en: 'Metadata' } satisfies LingRecord,
addField: { es: 'Añadir', en: 'Add' } satisfies LingRecord,
addFieldTitle: { es: 'Añadir campo', en: 'Add field' } satisfies LingRecord,
fieldCount: p({
es: { one: '{{count}} campo', other: '{{count}} campos' },
en: { one: '{{count}} field', other: '{{count}} fields' },
}),
},
// ── Validación ────────────────────────────────────────────────────
validation: {
valid: { es: 'Válido', en: 'Valid' } satisfies LingRecord,
errorsTitle: { es: 'Errores de validación', en: 'Validation errors' } satisfies LingRecord,
errorCount: p({
es: { one: '{{count}} error', other: '{{count}} errores' },
en: { one: '{{count}} error', other: '{{count}} errors' },
}),
},
// ── Fila de campo ─────────────────────────────────────────────────
field: {
moveUp: { es: 'Subir', en: 'Move up' } satisfies LingRecord,
moveDown: { es: 'Bajar', en: 'Move down' } satisfies LingRecord,
edit: { es: 'Editar', en: 'Edit' } satisfies LingRecord,
delete: { es: 'Eliminar', en: 'Delete' } satisfies LingRecord,
expand: { es: 'Expandir', en: 'Expand' } satisfies LingRecord,
collapse: { es: 'Colapsar', en: 'Collapse' } satisfies LingRecord,
hidden: { es: 'oculto', en: 'hidden' } satisfies LingRecord,
fieldValid: { es: 'Campo válido', en: 'Valid field' } satisfies LingRecord,
fieldInvalid: { es: 'Campo inválido', en: 'Invalid field' } satisfies LingRecord,
boolTrue: { es: 'Verdadero', en: 'True' } satisfies LingRecord,
boolFalse: { es: 'Falso', en: 'False' } satisfies LingRecord,
empty: { es: 'Usa el botón Añadir para crear campos', en: 'Use the Add button to create fields' } satisfies LingRecord,
emptyGroup: { es: 'Grupo vacío — añade campos con el botón +', en: 'Empty group — add fields with the + button' } satisfies LingRecord,
nestedCount: p({
es: { one: '{{count}} campo anidado', other: '{{count}} campos anidados' },
en: { one: '{{count}} nested field', other: '{{count}} nested fields' },
}),
},
// ── Editor de campo ───────────────────────────────────────────────
editor: {
keyLabel: { es: 'Clave del campo', en: 'Field key' } satisfies LingRecord,
keyPlaceholder: { es: 'nombre_campo', en: 'field_name' } satisfies LingRecord,
keyEmpty: { es: 'La clave no puede estar vacía', en: 'Key cannot be empty' } satisfies LingRecord,
keyExists: { es: '"{{key}}" ya existe', en: '"{{key}}" already exists' } satisfies LingRecord,
hideField: { es: 'Ocultar campo', en: 'Hide field' } satisfies LingRecord,
valueLing: { es: 'Valor multiidioma', en: 'Multilingual value' } satisfies LingRecord,
valueHtml: { es: 'Contenido HTML', en: 'HTML content' } satisfies LingRecord,
amountLabel: { es: 'Importe', en: 'Amount' } satisfies LingRecord,
currencyLabel: { es: 'Divisa', en: 'Currency' } satisfies LingRecord,
mediaUrlLabel: { es: 'URL del recurso', en: 'Resource URL' } satisfies LingRecord,
},
// ── Drawer ────────────────────────────────────────────────────────
drawer: {
title: { es: 'Añadir campo', en: 'Add field' } satisfies LingRecord,
cancel: { es: 'Cancelar', en: 'Cancel' } satisfies LingRecord,
confirm: { es: 'Añadir campo', en: 'Add field' } satisfies LingRecord,
},
// ── Paginación ────────────────────────────────────────────────────
pagination: {
rowsPerPage: { es: 'Filas por página', en: 'Rows per page' } satisfies LingRecord,
all: { es: 'Todos', en: 'All' } satisfies LingRecord,
prevPage: { es: 'Página anterior', en: 'Previous page' } satisfies LingRecord,
nextPage: { es: 'Página siguiente', en: 'Next page' } satisfies LingRecord,
pageN: { es: 'Página {{page}}', en: 'Page {{page}}' } satisfies LingRecord,
range: { es: '{{from}}–{{to}} de {{total}}', en: '{{from}}–{{to}} of {{total}}' } satisfies LingRecord,
},
// ── LingInput ─────────────────────────────────────────────────────
lingInput: {
requiredLocale: { es: 'El campo {{locale}} es obligatorio', en: 'The {{locale}} field is required' } satisfies LingRecord,
},
} as const;
export type Translations = typeof translations;

@ -158,5 +158,5 @@
</div> </div>
<!-- Backdrop --> <!-- Backdrop -->
<div class="modal-backdrop bg-black/40" onclick={onClose}></div> <div class="modal-backdrop bg-gray/40" onclick={onClose}></div>
</dialog> </dialog>

@ -0,0 +1,60 @@
<script lang="ts">
import type { MetadataFieldType } from '../../lib';
import FieldTypeIcon from './FieldTypeIcon.svelte';
let {
onSelect,
label = 'Añadir',
iconOnly = false,
}: {
onSelect: (type: MetadataFieldType) => void;
label?: string;
iconOnly?: boolean;
} = $props();
const typeLabels: Record<MetadataFieldType, string> = {
string: 'Texto', number: 'Número', boolean: 'Bool', ling: 'i18n',
date: 'Fecha', datetime: 'DateTime', url: 'URL', currency: 'Moneda',
image: 'Imagen', video: 'Vídeo', audio: 'Audio', pdf: 'PDF',
file: 'Fichero', html: 'HTML', group: 'Grupo',
};
const typeGroups: { label: string; types: MetadataFieldType[] }[] = [
{ label: 'Texto', types: ['string', 'ling', 'html', 'url'] },
{ label: 'Número', types: ['number', 'currency'] },
{ label: 'Fecha', types: ['date', 'datetime'] },
{ label: 'Lógico', types: ['boolean'] },
{ label: 'Media', types: ['image', 'video', 'audio', 'pdf', 'file'] },
{ label: 'Grupo', types: ['group'] },
];
</script>
<div class="dropdown dropdown-end">
<button tabindex="0" type="button"
class={iconOnly ? 'btn btn-ghost btn-xs btn-square' : 'btn btn-sm btn-ghost gap-1'}
title="Añadir campo">
<svg class="h-4 w-4" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2">
<path d="M8 3v10M3 8h10"/>
</svg>
{#if !iconOnly}<span class="text-xs">{label}</span>{/if}
</button>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<div tabindex="0" class="dropdown-content z-10 card card-compact bg-base-100 border border-base-200 shadow-lg p-3 w-124">
<div class="flex flex-col gap-2">
{#each typeGroups as group}
<div class="flex flex-wrap items-center gap-1">
<span class="text-base-content/30 text-xs w-10 shrink-0">{group.label}</span>
{#each group.types as type}
<button type="button"
onclick={() => onSelect(type)}
class="btn btn-ghost btn-xs gap-1 px-1.5"
title={typeLabels[type]}>
<FieldTypeIcon {type} size={14} />
<span class="text-xs">{typeLabels[type]}</span>
</button>
{/each}
</div>
{/each}
</div>
</div>
</div>

@ -5,6 +5,7 @@
import FieldEditor from './FieldEditor.svelte'; import FieldEditor from './FieldEditor.svelte';
import FieldRowRecursive from './FieldRow.svelte'; import FieldRowRecursive from './FieldRow.svelte';
import AddFieldDrawer from "@/mtas/svelte/components/AddFieldDrawer.svelte"; import AddFieldDrawer from "@/mtas/svelte/components/AddFieldDrawer.svelte";
import AddFieldMenu from "@/mtas/svelte/components/AddFieldMenu.svelte";
let { let {
fieldKey, field, locales, defaultLocale, fieldKey, field, locales, defaultLocale,
@ -150,7 +151,7 @@
id="field-row-{fieldKey}" id="field-row-{fieldKey}"
class={[ class={[
'card bg-base-100 border transition-all duration-150 flex-row focus-within:border-base-content w-full', 'card bg-base-100 border transition-all duration-150 flex-row focus-within:border-base-content w-full',
depth > 0 ? 'ml-5' : '', depth > 0 ? 'w-full' : '',
!validationResult.valid ? 'border-error' !validationResult.valid ? 'border-error'
: expanded ? 'border-base-300 shadow-sm' : expanded ? 'border-base-300 shadow-sm'
: 'border-base-200 hover:border-base-300', : 'border-base-200 hover:border-base-300',
@ -279,7 +280,8 @@
<!-- ── Group children ────────────────────────────────────────── --> <!-- ── Group children ────────────────────────────────────────── -->
{#if field.type === 'group'} {#if field.type === 'group'}
<div class="border-t border-base-200"> <div class="border-t border-base-200">
<div class="flex flex-wrap items-center gap-2 bg-base-200/40 px-4 py-2.5"> <div class="flex items-center gap-2 bg-base-200/40 px-4 py-2">
<!-- Toggle colapsar/expandir grupo -->
<button type="button" onclick={() => groupOpen = !groupOpen} <button type="button" onclick={() => groupOpen = !groupOpen}
class="btn btn-ghost btn-xs gap-1.5"> class="btn btn-ghost btn-xs gap-1.5">
<svg aria-hidden="true" <svg aria-hidden="true"
@ -289,16 +291,13 @@
</svg> </svg>
{Object.keys(field.value).length} campos anidados {Object.keys(field.value).length} campos anidados
</button> </button>
<div class="divider divider-horizontal mx-0 h-4"></div>
{#each ALL_TYPES as type (type)} <div class="ml-auto">
<button type="button" title="Añadir {type}" onclick={() => openChildDrawer(type)} <AddFieldMenu onSelect={openChildDrawer} iconOnly={true} />
class="btn btn-ghost btn-xs p-0.5"> </div>
<TypeBadge {type} showLabel={false} />
</button>
{/each}
</div> </div>
{#if groupOpen} {#if groupOpen}
<div class="flex flex-col gap-2 p-3"> <div class="flex flex-col gap-2 py-3 pl-4 pr-3 overflow-hidden">
{#if Object.keys(field.value).length === 0} {#if Object.keys(field.value).length === 0}
<div class="rounded-box flex items-center justify-center border-2 border-dashed border-base-300 py-6"> <div class="rounded-box flex items-center justify-center border-2 border-dashed border-base-300 py-6">
<span class="text-base-content/30">Grupo vacío — añade campos con los botones de arriba</span> <span class="text-base-content/30">Grupo vacío — añade campos con los botones de arriba</span>

@ -1,6 +1,6 @@
<!-- FieldTypeIcon.svelte — icono SVG por tipo de campo --> <!-- FieldTypeIcon.svelte — icono SVG por tipo de campo -->
<script lang="ts"> <script lang="ts">
import type { MetadataFieldType } from 'meta-content'; import type { MetadataFieldType } from '../../lib';
let { let {
type, type,
@ -20,7 +20,8 @@
{:else if type === 'number'} {:else if type === 'number'}
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={cls}> <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={cls}>
<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/> <line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/>
<line x1="10" y1="3" x2="8" y2="21"/><line x1="16" y1="3" x2="14" y2="21"/>
</svg> </svg>
{:else if type === 'boolean'} {:else if type === 'boolean'}
@ -53,7 +54,7 @@
{:else if type === 'currency'} {:else if type === 'currency'}
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={cls}> <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={cls}>
<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/> <line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
</svg> </svg>
{:else if type === 'image'} {:else if type === 'image'}

@ -6,6 +6,7 @@
import AddFieldDrawer from './AddFieldDrawer.svelte'; import AddFieldDrawer from './AddFieldDrawer.svelte';
import {makeLingRecord} from "@/ling/lib"; import {makeLingRecord} from "@/ling/lib";
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import AddFieldMenu from "@/mtas/svelte/components/AddFieldMenu.svelte";
let { let {
value = $bindable<Metadata>({}), value = $bindable<Metadata>({}),
@ -13,6 +14,7 @@
defaultLocale = 'es', defaultLocale = 'es',
dark = false, dark = false,
attributes = [], attributes = [],
pagination = true,
onUpdate, onUpdate,
}: { }: {
value?: Metadata; value?: Metadata;
@ -20,6 +22,7 @@
defaultLocale?: string; defaultLocale?: string;
dark?: boolean; dark?: boolean;
attributes?: string[]; attributes?: string[];
pagination?: boolean;
onUpdate?: (metadata: Metadata) => void; onUpdate?: (metadata: Metadata) => void;
} = $props(); } = $props();
@ -44,10 +47,8 @@
let rangeStart = $derived(pageSize === 0 ? 1 : (page - 1) * pageSize + 1); let rangeStart = $derived(pageSize === 0 ? 1 : (page - 1) * pageSize + 1);
let rangeEnd = $derived(pageSize === 0 ? keyOrder.length : Math.min(page * pageSize, keyOrder.length)); let rangeEnd = $derived(pageSize === 0 ? keyOrder.length : Math.min(page * pageSize, keyOrder.length));
// Mantener page dentro de los límites
$effect(() => { if (page > totalPages) page = Math.max(1, totalPages); }); $effect(() => { if (page > totalPages) page = Math.max(1, totalPages); });
// Páginas a mostrar en la barra (con elipsis = -1)
let paginationPages = $derived((() => { let paginationPages = $derived((() => {
if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1); if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1);
const pages: number[] = [1]; const pages: number[] = [1];
@ -60,21 +61,7 @@
let validation = $derived(validateMetadata(value)); let validation = $derived(validateMetadata(value));
const typeLabels: Record<MetadataFieldType, string> = {
string: 'Texto', number: 'Número', boolean: 'Bool', ling: 'i18n',
date: 'Fecha', datetime: 'DateTime', url: 'URL', currency: 'Moneda',
image: 'Imagen', video: 'Vídeo', audio: 'Audio', pdf: 'PDF',
file: 'Fichero', html: 'HTML', group: 'Grupo',
};
const typeGroups: { label: string; types: MetadataFieldType[] }[] = [
{ label: 'Texto', types: ['string', 'ling', 'html', 'url'] },
{ label: 'Número', types: ['number', 'currency'] },
{ label: 'Fecha', types: ['date', 'datetime'] },
{ label: 'Lógico', types: ['boolean'] },
{ label: 'Media', types: ['image', 'video', 'audio', 'pdf', 'file'] },
{ label: 'Grupo', types: ['group'] },
];
function emit(): void { onUpdate?.(value); } function emit(): void { onUpdate?.(value); }
@ -142,7 +129,6 @@
newOrder.splice(newIdx, 0, key); newOrder.splice(newIdx, 0, key);
value = { ...value, order: newOrder }; value = { ...value, order: newOrder };
emit(); emit();
// Seguir al campo si cruza un límite de página
if (pageSize > 0) { if (pageSize > 0) {
const newPage = Math.floor(newIdx / pageSize) + 1; const newPage = Math.floor(newIdx / pageSize) + 1;
if (newPage !== page) page = newPage; if (newPage !== page) page = newPage;
@ -150,49 +136,34 @@
} }
</script> </script>
<div data-theme={dark ? 'dark' : 'light'}> <div data-theme={dark ? 'dark' : 'light'} class="h-full flex flex-col min-h-0">
<div class="card bg-base-100 border border-base-200 shadow-sm w-full"> <div class="card bg-base-100 border border-base-200 shadow-sm w-full flex flex-col flex-1 min-h-0">
<!-- ── Header ──────────────────────────────────────────────────── --> <!-- ── Header ──────────────────────────────────────────────────── -->
<div class="card-body gap-4 pb-3 border-b border-base-200"> <div class="flex items-center gap-2 px-5 py-3 border-b border-base-200 shrink-0">
<div class="flex items-center gap-3"> <div role="img"
<h2 class="card-title">Metadata</h2> aria-label={validation.valid ? 'Válido' : 'Con errores'}
<span class="text-base-content/50"> title={validation.valid ? 'Válido' : `${validation.errors.length} errores`}
{keyOrder.length} {keyOrder.length === 1 ? 'campo' : 'campos'} class={['h-2 w-2 shrink-0 rounded-full', validation.valid ? 'bg-success' : 'bg-error'].join(' ')}
</span> ></div>
<div class={['badge badge-sm badge-soft ml-auto', validation.valid ? 'badge-success' : 'badge-error'].join(' ')}> <h2 class="font-semibold text-base">Metadata</h2>
<div class={['h-1.5 w-1.5 rounded-full mr-1', validation.valid ? 'bg-success' : 'bg-error'].join(' ')}></div> <span class="text-base-content/40 text-sm">
{validation.valid ? 'Válido' : `${validation.errors.length} errores`} {keyOrder.length} {keyOrder.length === 1 ? 'campo' : 'campos'}
</div> </span>
</div> <div class="ml-auto">
<AddFieldMenu onSelect={openDrawer} />
<div class="flex flex-wrap gap-x-4 gap-y-2">
{#each typeGroups as group}
<div class="flex items-center gap-1">
<span class="text-base-content/30 text-xs">{group.label}</span>
{#each group.types as type}
<button type="button"
onclick={() => openDrawer(type)}
class="btn btn-ghost btn-xs gap-1 px-1.5"
title="Añadir {typeLabels[type]}">
<FieldTypeIcon {type} size={14} />
<span class="text-xs">{typeLabels[type]}</span>
</button>
{/each}
</div>
{/each}
</div> </div>
</div> </div>
<!-- ── Field list ─────────────────────────────────────────────── --> <!-- ── Field list ─────────────────────────────────────────────── -->
<div class="card-body gap-2 pt-3"> <div class="flex flex-col gap-2 p-4 overflow-y-auto flex-1 min-h-0">
{#if keyOrder.length === 0} {#if keyOrder.length === 0}
<div class="rounded-box flex flex-col items-center justify-center gap-3 border-2 border-dashed border-base-300 py-16"> <div class="rounded-box flex flex-col items-center justify-center gap-3 border-2 border-dashed border-base-300 py-16">
<svg class="h-10 w-10 text-base-content/10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> <svg class="h-10 w-10 text-base-content/10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="4"/> <rect x="3" y="3" width="18" height="18" rx="4"/>
<path d="M12 8v8M8 12h8"/> <path d="M12 8v8M8 12h8"/>
</svg> </svg>
<p class="text-base-content/30">Añade campos usando los botones de arriba</p> <p class="text-base-content/30">Usa el botón Añadir para crear campos</p>
</div> </div>
{:else} {:else}
{#each pageKeys as key (key)} {#each pageKeys as key (key)}
@ -217,7 +188,7 @@
</div> </div>
{#if !validation.valid} {#if !validation.valid}
<div class="card-body pt-0"> <div class="px-4 pb-3">
<div class="alert alert-error"> <div class="alert alert-error">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span>Errores de validación</span> <span>Errores de validación</span>
@ -230,16 +201,16 @@
{/if} {/if}
<!-- ── Footer: paginación ──────────────────────────────────────── --> <!-- ── Footer: paginación ──────────────────────────────────────── -->
{#if keyOrder.length > 0} {#if pagination && keyOrder.length > 0}
<div class="flex flex-wrap items-center justify-between gap-3 border-t border-base-200 px-5 py-3 bg-amber-50 border-gray-300"> <div class="flex flex-wrap items-center justify-between gap-3 border-t border-base-200 px-5 py-3">
<!-- Filas por página --> <!-- Filas por página -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-xs text-base-content/40">Filas por página</span> <span class="text-xs text-base-content/40">Filas por página</span>
<select <select
class="select select-bordered select-xs" class="select select-bordered select-xs"
value={pageSize} bind:value={pageSize}
onchange={(e) => { pageSize = parseInt((e.currentTarget as HTMLSelectElement).value); page = 1; }} onchange={() => { page = 1; }}
> >
{#each PAGE_SIZES as s} {#each PAGE_SIZES as s}
<option value={s}>{s}</option> <option value={s}>{s}</option>
@ -251,20 +222,13 @@
<!-- Barra de páginas --> <!-- Barra de páginas -->
{#if totalPages > 1} {#if totalPages > 1}
<div class="join"> <div class="join">
<button <button type="button" class="join-item btn btn-xs" disabled={page === 1}
type="button" onclick={() => page--} aria-label="Página anterior">‹</button>
class="join-item btn btn-xs"
disabled={page === 1}
onclick={() => page--}
aria-label="Página anterior"
>‹</button>
{#each paginationPages as p (p === -1 ? `ellipsis-${p}` : p)} {#each paginationPages as p (p === -1 ? `ellipsis-${p}` : p)}
{#if p === -1} {#if p === -1}
<button type="button" class="join-item btn btn-xs btn-disabled" tabindex="-1">…</button> <button type="button" class="join-item btn btn-xs btn-disabled" tabindex="-1">…</button>
{:else} {:else}
<button <button type="button"
type="button"
class={['join-item btn btn-xs', p === page ? 'btn-active' : ''].join(' ')} class={['join-item btn btn-xs', p === page ? 'btn-active' : ''].join(' ')}
onclick={() => page = p} onclick={() => page = p}
aria-label="Página {p}" aria-label="Página {p}"
@ -272,14 +236,8 @@
>{p}</button> >{p}</button>
{/if} {/if}
{/each} {/each}
<button type="button" class="join-item btn btn-xs" disabled={page === totalPages}
<button onclick={() => page++} aria-label="Página siguiente">›</button>
type="button"
class="join-item btn btn-xs"
disabled={page === totalPages}
onclick={() => page++}
aria-label="Página siguiente"
>›</button>
</div> </div>
{/if} {/if}

@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
import './app.css'; import './app.css';
import MetadataEditor from '../components/MetadataEditor.svelte'; import MetadataEditor from '../components/MetadataEditor.svelte';
import { ling as lingInstance } from '@/ling/lib'
import LingInput from '@/ling/svelte/LingInput.svelte'; import LingInput from '@/ling/svelte/LingInput.svelte';
import type { Metadata } from '../../lib'; import {, type Metadata} from '../../lib';
import type { LingRecord } from '@/ling/lib'; import {createLing, type LingNode, type LingRecord, p} from '@/ling/lib';
import {createGlob, type CurrDefinition} from '@/glob/lib'
import {unitData} from "@/glob/lib/unit_data.ts";
// ── Demo metadata con todos los tipos ─────────────────────────── // ── Demo metadata con todos los tipos ───────────────────────────
@ -37,6 +39,34 @@
const locales = ['es', 'en', 'fr']; const locales = ['es', 'en', 'fr'];
const defaultLocale = 'es'; const defaultLocale = 'es';
const currDefinitions: Record<string, CurrDefinition> = {
EUR: {
symbol : '€',
code : 'EUR',
name : p({
es: { one: 'euro', other: 'euros' },
en: { one: 'euro', other: 'euros' } }) ,
conversionValue: 1,
},
USD: {
symbol : '$',
code : 'USD',
name : p({ es: { one: 'dólar', other: 'dólares' }, en: { one: 'dollar', other: 'dollars' } }),
conversionValue: 1.08,
},
};
const glob = createGlob({
locale: defaultLocale,
ling : lingInstance,
numr : { minDecimals: 0, maxDecimals: 2 },
curr : { definitions: currDefinitions, selectedCurrency: 'EUR' },
dati : { selectedTimeFormat: '24h' },
unit : { selectedSystem: 'metric', categories: unitData },
});
</script> </script>
<svelte:head> <svelte:head>
@ -140,10 +170,10 @@
</div> </div>
{:else} {:else}
<div class="grid grid-cols-1 gap-8 lg:grid-cols-[minmax(0,1fr)_320px]"> <div class="grid grid-cols-1 gap-8 lg:grid-cols-[minmax(0,1fr)_320px] h-[600px]">
<!-- ── Left: MetadataEditor ──────────────────────── --> <!-- ── Left: MetadataEditor ──────────────────────── -->
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2 min-h-0">
<div class="flex items-center gap-2 px-0.5"> <div class="flex items-center gap-2 px-0.5">
<span class="font-mono text-[10px] font-bold uppercase tracking-[0.16em] text-zinc-400 dark:text-zinc-600"> <span class="font-mono text-[10px] font-bold uppercase tracking-[0.16em] text-zinc-400 dark:text-zinc-600">
MetadataEditor MetadataEditor

Loading…
Cancel
Save

Powered by TurnKey Linux.