diff --git a/src/ling/svelte/LingInput.svelte b/src/ling/svelte/LingInput.svelte index 8c9123d..37d5976 100644 --- a/src/ling/svelte/LingInput.svelte +++ b/src/ling/svelte/LingInput.svelte @@ -85,7 +85,7 @@ {#if label}
@@ -140,15 +140,15 @@ aria-label={`Idioma ${locale}`} onclick={() => activateLocale(locale)} class={[ - 'relative flex h-4 w-8 items-center rounded px-1.5 font-mono text-[10px] font-bold uppercase', + 'li-locale-btn relative flex h-4 w-8 items-center rounded px-1.5 font-mono text-[10px] font-bold uppercase', 'tracking-wider transition-all duration-150 focus:outline-none', active ? invalid ? (dark ? 'bg-red-700 text-white' : 'bg-rose-500 text-white') : (dark ? 'bg-indigo-600 text-white' : 'bg-violet-600 text-white') : filled - ? (dark ? 'bg-gray-700 text-gray-300 hover:bg-gray-600' : 'bg-zinc-100 text-zinc-600 hover:bg-zinc-200') - : (dark ? 'text-gray-600 hover:bg-gray-800 hover:text-gray-400' : 'text-zinc-300 hover:bg-zinc-100 hover:text-zinc-500'), + ? (dark ? 'bg-gray-700 text-gray-300 hover:bg-gray-600' : 'bg-zinc-200 text-zinc-700 hover:bg-zinc-300') + : (dark ? 'bg-gray-800 text-gray-500 hover:bg-gray-700 hover:text-gray-300' : 'bg-zinc-100 text-zinc-500 hover:bg-zinc-200 hover:text-zinc-700'), ].join(' ')} > {locale} @@ -227,3 +227,23 @@ + + diff --git a/src/vcen-edit/app.css b/src/vcen-edit/app.css index 67717c3..4f587d4 100644 --- a/src/vcen-edit/app.css +++ b/src/vcen-edit/app.css @@ -179,3 +179,147 @@ html { scroll-behavior: smooth; } .catalog-editor *::-webkit-scrollbar-thumb:hover { background: #374151; } .catalog-editor *::-webkit-scrollbar-corner { background: transparent; } .catalog-editor * { scrollbar-width: thin; scrollbar-color: #1f2937 transparent; } + +/* ═══════════════════════════════════════════════════════════════════ + TEMA LIGHT + Estrategia: overrides explícitos por clase + CSS tokens. + Los overrides de clase funcionan independientemente de cómo + Tailwind compila los utilities (con var() o valores estáticos). + ═══════════════════════════════════════════════════════════════════ */ + +/* ── Tokens y raíz ───────────────────────────────────────────────── */ +.catalog-editor.ve-light { + --ve-bg: #ffffff; + --ve-surface: #ffffff; + --ve-surface-alt: #f4f6f9; + --ve-border: #dde3ec; + --ve-border-mid: #b8c8d8; + --ve-text: #0d1c2a; + --ve-text-muted: #4d6880; + --ve-text-subtle: #dde3ec; + --ve-accent-light: #4338ca; + background: #f4f6f9; + color: #0d1c2a; +} + +/* ── Fondos: grises (bg-gray-*) ──────────────────────────────────── */ +.catalog-editor.ve-light [class*="bg-gray-950"] { background-color: #f4f6f9 !important; } +.catalog-editor.ve-light [class*="bg-gray-900"] { background-color: #ffffff !important; } +.catalog-editor.ve-light [class*="bg-gray-800"] { background-color: #edf1f7 !important; } +/* bg-gray-700 más oscuro para que los botones secundarios de locale destaquen sobre el fondo blanco */ +.catalog-editor.ve-light [class*="bg-gray-700"] { background-color: #c3d3e0 !important; } + +/* ── Texto: grises (text-gray-*) — contraste mínimo 4.5:1 sobre blanco */ +.catalog-editor.ve-light [class*="text-gray-100"] { color: #0d1c2a !important; } /* 17:1 */ +.catalog-editor.ve-light [class*="text-gray-200"] { color: #1a2d3e !important; } /* 13:1 */ +.catalog-editor.ve-light [class*="text-gray-300"] { color: #2d4255 !important; } /* 9:1 */ +.catalog-editor.ve-light [class*="text-gray-400"] { color: #415d76 !important; } /* 6:1 */ +.catalog-editor.ve-light [class*="text-gray-500"] { color: #4d6880 !important; } /* 5.2:1 */ +.catalog-editor.ve-light [class*="text-gray-600"] { color: #607d95 !important; } /* 4.5:1 */ +.catalog-editor.ve-light [class*="text-gray-700"] { color: #8fa8bf !important; } /* decorativo */ + +/* ── Bordes: grises (border-gray-*) ─────────────────────────────── */ +.catalog-editor.ve-light [class*="border-gray-800"] { border-color: #dde3ec !important; } +.catalog-editor.ve-light [class*="border-gray-700"] { border-color: #b8c8d8 !important; } +.catalog-editor.ve-light [class*="border-gray-600"] { border-color: #94afc4 !important; } + +/* ── Divide ──────────────────────────────────────────────────────── */ +.catalog-editor.ve-light [class*="divide-gray-800"] > * + * { border-color: #dde3ec !important; } + +/* ── Indigo: activos y acentos ───────────────────────────────────── */ +.catalog-editor.ve-light [class*="text-indigo-400"] { color: #4f46e5 !important; } +.catalog-editor.ve-light [class*="text-indigo-300"] { color: #4338ca !important; } +.catalog-editor.ve-light [class*="bg-indigo-950"] { background-color: rgba(238,242,255,.8) !important; } +.catalog-editor.ve-light [class*="bg-indigo-900"] { background-color: rgba(224,231,255,.7) !important; } +/* NOTA: NO se override bg-indigo-600 ni border-indigo-600: + - bg-indigo-600 se usa en botones activos (locale, tabs…) y funciona correcto sin override. + - border-indigo-600 aparece como "focus-within:border-indigo-600" en el class attr de LingInput, + y overridearlo con [class*="border-indigo-600"] haría que el borde sea siempre azul. */ +.catalog-editor.ve-light [class*="border-indigo-900"] { border-color: rgba(165,180,252,.5) !important; } +.catalog-editor.ve-light [class*="border-indigo-700"] { border-color: #6366f1 !important; } + +/* ── Verde: disponible / activo ──────────────────────────────────── */ +.catalog-editor.ve-light [class*="text-green-400"] { color: #16a34a !important; } +.catalog-editor.ve-light [class*="text-green-500"] { color: #15803d !important; } +.catalog-editor.ve-light [class*="bg-green-900"] { background-color: rgba(220,252,231,.7) !important; } +.catalog-editor.ve-light [class*="bg-green-500"] { background-color: #22c55e !important; } +.catalog-editor.ve-light [class*="border-green-800"] { border-color: #86efac !important; } +.catalog-editor.ve-light [class*="border-green-700"] { border-color: #4ade80 !important; } + +/* ── Rojo: prohibido / peligro ───────────────────────────────────── */ +.catalog-editor.ve-light [class*="text-red-400"] { color: #dc2626 !important; } +.catalog-editor.ve-light [class*="text-red-200"] { color: #991b1b !important; } +.catalog-editor.ve-light [class*="bg-red-950"] { background-color: #fff1f2 !important; } +.catalog-editor.ve-light [class*="bg-red-900"] { background-color: rgba(254,226,226,.8) !important; } +.catalog-editor.ve-light [class*="border-red-900"] { border-color: #fca5a5 !important; } + +/* ── Azul: requerido ─────────────────────────────────────────────── */ +.catalog-editor.ve-light [class*="text-blue-400"] { color: #2563eb !important; } +.catalog-editor.ve-light [class*="bg-blue-900"] { background-color: rgba(219,234,254,.8) !important; } +.catalog-editor.ve-light [class*="border-blue-800"] { border-color: #bfdbfe !important; } +.catalog-editor.ve-light [class*="border-blue-700"] { border-color: #93c5fd !important; } + +/* ── Ámbar: modificado / advertencia ─────────────────────────────── */ +.catalog-editor.ve-light [class*="text-amber-400"] { color: #b45309 !important; } +.catalog-editor.ve-light [class*="bg-amber-900"] { background-color: rgba(254,243,199,.9) !important; } +.catalog-editor.ve-light [class*="border-amber-900"] { border-color: rgba(251,191,36,.6) !important; } +.catalog-editor.ve-light [class*="border-amber-800"] { border-color: rgba(251,191,36,.5) !important; } + +/* ── Botones: light ──────────────────────────────────────────────── */ +.catalog-editor.ve-light .ve-action-btn, +.catalog-editor.ve-light .ve-btn, +.catalog-editor.ve-light .ve-card-btn { border-color: #b8c8d8; } + +.catalog-editor.ve-light .ve-action-btn { color: #4d6880; } +.catalog-editor.ve-light .ve-action-btn:hover { color: #0d1c2a; border-color: #7a9ab5; background: #edf1f7; } +.catalog-editor.ve-light .ve-action-btn.primary { background: #4f46e5; border-color: #4f46e5; color: #fff; } +.catalog-editor.ve-light .ve-action-btn.primary:hover { background: #4338ca; border-color: #4338ca; } +.catalog-editor.ve-light .ve-action-btn.danger:hover { color: #dc2626; border-color: #fca5a5; background: rgba(220,38,38,.08); } +.catalog-editor.ve-light .ve-action-btn.discard:hover { color: #dc2626; border-color: #fca5a5; background: rgba(220,38,38,.08); } +.catalog-editor.ve-light .ve-action-btn.active { border-color: #4f46e5; background: rgba(79,70,229,.1); color: #4338ca; } + +.catalog-editor.ve-light .ve-btn { color: #4d6880; } +.catalog-editor.ve-light .ve-btn:hover { color: #0d1c2a; border-color: #7a9ab5; background: #edf1f7; } +.catalog-editor.ve-light .ve-btn.primary { background: #4f46e5; border-color: #4f46e5; color: #fff; } +.catalog-editor.ve-light .ve-btn.primary:hover { background: #4338ca; border-color: #4338ca; } +.catalog-editor.ve-light .ve-btn.danger { background: #fee2e2; border-color: #fca5a5; color: #dc2626; } +.catalog-editor.ve-light .ve-btn.danger:hover { background: #dc2626; border-color: #dc2626; color: #fff; } +.catalog-editor.ve-light .ve-btn.discard:hover { color: #dc2626; border-color: #fca5a5; background: rgba(220,38,38,.08); } +.catalog-editor.ve-light .ve-btn.active { border-color: #4f46e5; background: rgba(79,70,229,.1); color: #4338ca; } + +.catalog-editor.ve-light .ve-card-btn { color: #4d6880; } +.catalog-editor.ve-light .ve-card-btn:hover { color: #0d1c2a; border-color: #7a9ab5; background: #edf1f7; } +.catalog-editor.ve-light .ve-card-btn.danger { border-color: #b8c8d8; color: #4d6880; } +.catalog-editor.ve-light .ve-card-btn.danger:hover { color: #dc2626; border-color: #fca5a5; background: rgba(220,38,38,.08); } + +.catalog-editor.ve-light .ve-btn-link { color: #4d6880; } +.catalog-editor.ve-light .ve-btn-link:hover { color: #0d1c2a; } +.catalog-editor.ve-light .ve-btn-link.accent { color: #4f46e5; } +.catalog-editor.ve-light .ve-btn-link.accent:hover { color: #4338ca; } +.catalog-editor.ve-light .ve-btn-link.danger:hover { color: #dc2626; } + +/* ── Sidebar add btn ─────────────────────────────────────────────── */ +.catalog-editor.ve-light .ve-sidebar-add-btn { color: #7a9ab5; } +.catalog-editor.ve-light .ve-sidebar-add-btn:hover { color: #4338ca; background: rgba(79,70,229,.1); } + +/* ── Status badges ───────────────────────────────────────────────── */ +.catalog-editor.ve-light .ve-status.available { background: rgba(20,83,45,.08); color: #16a34a; border-color: rgba(20,83,45,.25); } +.catalog-editor.ve-light .ve-status.required { background: rgba(30,58,138,.08); color: #2563eb; border-color: rgba(30,58,138,.25); } +.catalog-editor.ve-light .ve-status.hidden { background: #f4f6f9; color: #4d6880; border-color: #dde3ec; } +.catalog-editor.ve-light .ve-status.forbidden { background: rgba(153,27,27,.08); color: #dc2626; border-color: rgba(153,27,27,.25); } +.catalog-editor.ve-light .ve-status.error { background: rgba(153,27,27,.08); color: #dc2626; border-color: rgba(153,27,27,.25); } + +/* ── Label ───────────────────────────────────────────────────────── */ +.catalog-editor.ve-light .ve-label { color: #4d6880; } + +/* ── select chevron ──────────────────────────────────────────────── */ +.catalog-editor.ve-light select.field-input { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%234d6880' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); +} + +/* Locale tab buttons — gestionados en LingInput.svelte via diff --git a/src/vcen-edit/comps/CatalogLayout.svelte b/src/vcen-edit/comps/CatalogLayout.svelte index bf073ca..6e71627 100644 --- a/src/vcen-edit/comps/CatalogLayout.svelte +++ b/src/vcen-edit/comps/CatalogLayout.svelte @@ -47,6 +47,15 @@ const sel = $derived(editor.selection) let showJson = $state(false) let showGraph = $state(false) +const THEME_KEY = 've-theme' +let theme = $state<'dark' | 'light'>( + (typeof localStorage !== 'undefined' ? localStorage.getItem(THEME_KEY) : null) === 'light' ? 'light' : 'dark' +) +$effect(() => { + if (typeof localStorage !== 'undefined') localStorage.setItem(THEME_KEY, theme) +}) +function toggleTheme() { theme = theme === 'dark' ? 'light' : 'dark' } + let sidebarW = $state(256) let dragging = $state(false) @@ -78,7 +87,7 @@ const graphResult = $derived.by(() => { const isValid = $derived(graphResult.graph !== null) -
+
@@ -94,6 +103,17 @@ const isValid = $derived(graphResult.graph !== null)
+ diff --git a/src/vcen-edit/comps/EditorMetaTree.svelte b/src/vcen-edit/comps/EditorMetaTree.svelte index b67fd1a..4603831 100644 --- a/src/vcen-edit/comps/EditorMetaTree.svelte +++ b/src/vcen-edit/comps/EditorMetaTree.svelte @@ -229,9 +229,9 @@ const selectedField = $derived(selectedPath.length > 0 ? getFieldAtPath(selected const selectedKey = $derived(selectedPath.length > 0 ? selectedPath[selectedPath.length - 1] : '') -
+
-
+
@@ -300,7 +300,7 @@ const selectedKey = $derived(selectedPath.length > 0 ? selectedPath[selectedPa
@@ -347,31 +347,20 @@ const selectedKey = $derived(selectedPath.length > 0 ? selectedPath[selectedPa {#if showDeleteConfirm && selectedField} -
- -
-
-
- - - -
-
-

{glob.t('vcen.editor.mtas.field.deleteConfirm')}

-

{glob.t('vcen.editor.mtas.field.deleteConfirmMsg')}

-
- - {selectedKey} -
-
+
showDeleteConfirm = false}> +
e.stopPropagation()}> +

{glob.t('vcen.editor.mtas.field.deleteConfirm')}

+
+ + {selectedKey}
-
- -
diff --git a/src/vcen-edit/comps/EditorTreeNodes.svelte b/src/vcen-edit/comps/EditorTreeNodes.svelte index 81adbd5..7268220 100644 --- a/src/vcen-edit/comps/EditorTreeNodes.svelte +++ b/src/vcen-edit/comps/EditorTreeNodes.svelte @@ -86,17 +86,13 @@ function groupValue(f: MetadataField): Metadata { return f.type === 'group' ? f. + class="flex items-center justify-center w-5 h-5 rounded text-xs text-gray-500 hover:text-gray-300 hover:bg-gray-700 transition-colors disabled:opacity-20" + title={glob.t('vcen.editor.mtas.field.moveUp')}>↑ + class="flex items-center justify-center w-5 h-5 rounded text-xs text-gray-500 hover:text-gray-300 hover:bg-gray-700 transition-colors disabled:opacity-20" + title={glob.t('vcen.editor.mtas.field.moveDown')}>↓
diff --git a/src/vcen-edit/comps/MetaPanel.svelte b/src/vcen-edit/comps/MetaPanel.svelte index e0d9b66..767904a 100644 --- a/src/vcen-edit/comps/MetaPanel.svelte +++ b/src/vcen-edit/comps/MetaPanel.svelte @@ -22,7 +22,7 @@ const defaultLocale = $derived(editor.cat.lang as string) const tags = $derived((editor.cat as any).metaTags as string[] ?? []) -
+
('catalog-editor') const glob = getContext('editor-glob') @@ -22,7 +23,10 @@ const objId = $derived( ? (sel as any).objId : null) const obj = $derived(objId ? editor.findObj(objId) ?? null : null) const isNew = $derived(editor.newEntityId !== null && editor.newEntityId === (objId as string)) -const tab = $derived(sel.kind === 'obj-atts' ? 'atts' : sel.kind === 'obj-meta' ? 'meta' : sel.kind === 'obj-issues' ? 'issues' : 'info') +const tabFromSel = $derived(sel.kind === 'obj-atts' ? 'atts' : sel.kind === 'obj-meta' ? 'meta' : sel.kind === 'obj-issues' ? 'issues' : 'info') +let extraTab = $state<'visual' | null>(null) +$effect(() => { void sel; extraTab = null }) +const tab = $derived(extraTab ?? tabFromSel) const computedState = $derived.by(() => { if (!obj) return null @@ -52,7 +56,9 @@ function cancelNew() { editor.select({ kind: 'cat-info' }) } -function goTab(t: 'info' | 'atts' | 'issues' | 'meta') { +function goTab(t: 'info' | 'atts' | 'issues' | 'meta' | 'visual') { + if (t === 'visual') { extraTab = 'visual'; return } + extraTab = null if (!objId) return if (t === 'info') editor.select({ kind: 'obj-info', objId }) else if (t === 'atts') editor.select({ kind: 'obj-atts', objId }) @@ -97,13 +103,14 @@ function goTab(t: 'info' | 'atts' | 'issues' | 'meta') { {#each [ { id: 'info', label: 'General' }, { id: 'atts', label: 'Atributos' }, + { id: 'visual', label: 'Visual' }, { id: 'issues', label: 'Reglas/Incidencias' }, { id: 'meta', label: 'Metadatos' }, ] as t (t.id)} {@const isActive = tab === t.id} @@ -235,30 +242,22 @@ function goTab(t: 'info' | 'atts' | 'issues' | 'meta') { {/if} - -
-

Visual

-
- - editor.updateObjVisual(objId, { basePath: e.currentTarget.value || undefined })} - placeholder={(cat as any).visualDefaults?.basePath || 'Heredado del catálogo'} - class="field-input text-xs font-mono" /> -
-
- - editor.updateObjVisual(objId, { fallback: e.currentTarget.value || undefined })} - placeholder={(cat as any).visualDefaults?.fallback || 'Heredado del catálogo'} - class="field-input text-xs font-mono" /> -
-
-
{:else if tab === 'atts'} + {:else if tab === 'visual'} +
+ editor.setObjVisual(objId, v)} + /> +
+ {:else if tab === 'issues'}
diff --git a/src/vcen-edit/comps/SecPanel.svelte b/src/vcen-edit/comps/SecPanel.svelte index 90ecf32..6e0a620 100644 --- a/src/vcen-edit/comps/SecPanel.svelte +++ b/src/vcen-edit/comps/SecPanel.svelte @@ -9,6 +9,7 @@ import RuleList from './RuleList.svelte' import IssueSection from './IssueSection.svelte' import SecAttsPanel from './SecAttsPanel.svelte' import MetaPanel from './MetaPanel.svelte' +import VisualPanel from './VisualPanel.svelte' const editor = getContext('catalog-editor') const glob = getContext('editor-glob') @@ -23,7 +24,10 @@ const ctx = $derived( : null) const isNew = $derived(editor.newEntityId !== null && editor.newEntityId === (ctx?.secId as string | undefined)) -const tab = $derived(sel.kind === 'sec-atts' ? 'atts' : sel.kind === 'sec-meta' ? 'meta' : sel.kind === 'sec-issues' ? 'issues' : 'info') +const tabFromSel = $derived(sel.kind === 'sec-atts' ? 'atts' : sel.kind === 'sec-meta' ? 'meta' : sel.kind === 'sec-issues' ? 'issues' : 'info') +let extraTab = $state<'visual' | null>(null) +$effect(() => { void sel; extraTab = null }) +const tab = $derived(extraTab ?? tabFromSel) let editId = $state('') let idError = $state('') @@ -50,7 +54,9 @@ function cancelNew() { editor.select({ kind: 'obj-info', objId: ctx.objId }) } -function goTab(t: 'info' | 'atts' | 'issues' | 'meta') { +function goTab(t: 'info' | 'atts' | 'issues' | 'meta' | 'visual') { + if (t === 'visual') { extraTab = 'visual'; return } + extraTab = null if (!ctx) return const { objId, secId } = ctx if (t === 'info') editor.select({ kind: 'sec-info', objId, secId }) @@ -93,13 +99,14 @@ function goTab(t: 'info' | 'atts' | 'issues' | 'meta') { {#each [ { id: 'info', label: 'General' }, { id: 'atts', label: 'Atributos' }, + { id: 'visual', label: 'Visual' }, { id: 'issues', label: 'Reglas/Incidencias' }, { id: 'meta', label: 'Metadatos' }, ] as t (t.id)} {@const isActive = tab === t.id} @@ -229,30 +236,22 @@ function goTab(t: 'info' | 'atts' | 'issues' | 'meta') { {/if} - -
-

Visual

-
- - editor.updateSecVisual(objId, secId, { basePath: e.currentTarget.value || undefined })} - placeholder={(cat as any).visualDefaults?.basePath || 'Heredado del catálogo'} - class="field-input text-xs font-mono" /> -
-
- - editor.updateSecVisual(objId, secId, { fallback: e.currentTarget.value || undefined })} - placeholder={(cat as any).visualDefaults?.fallback || 'Heredado del catálogo'} - class="field-input text-xs font-mono" /> -
-
-
{:else if tab === 'atts'} + {:else if tab === 'visual'} +
+ editor.setSecVisual(objId, secId, v)} + /> +
+ {:else if tab === 'issues'}
diff --git a/src/vcen-edit/comps/VisualPanel.svelte b/src/vcen-edit/comps/VisualPanel.svelte new file mode 100644 index 0000000..0c1a559 --- /dev/null +++ b/src/vcen-edit/comps/VisualPanel.svelte @@ -0,0 +1,879 @@ + + +
+ + +
+ + + + + +
+ Vistas + +
+ + {#if views.length === 0} +
Sin vistas
+ {:else} + {#each views as view, idx (view.id)} + {@const isViewSel = selected !== 'general' && (selected as any).viewId === view.id} + {@const isViewActive = isViewSel && (selected as any).kind === 'view'} + + +
+ + + + +
+ + + +
+
+ + {#each (view.hotspots ?? []) as hs (hs.id)} + {@const isHsSel = selected !== 'general' + && (selected as any).kind === 'hotspot' + && (selected as any).viewId === view.id + && (selected as any).hotspotId === hs.id} + {@const hsLabel = getLabelStr(hs.label)} +
+ +
+ {/each} + {/each} + {/if} +
+ + +
+ + {#if selected === 'general'} +
+

Configuración general

+ +
+ + patchVisual({ basePath: e.currentTarget.value || undefined })} + placeholder={catDefaults.basePath || 'Heredado del catálogo'} + class="field-input text-xs font-mono" /> +
+ +
+ + patchVisual({ fallback: e.currentTarget.value || undefined })} + placeholder={catDefaults.fallback || 'Heredado del catálogo'} + class="field-input text-xs font-mono" /> +
+ + {#if views.length > 0} +
+ + +
+ {:else} +

Añade vistas desde el panel lateral.

+ {/if} +
+ + {:else if selectedView && (selected as any).kind === 'view'} + {@const view = selectedView} + {@const isNew = newViewId === view.id} +
+ + +
+ {isNew ? 'Nueva vista' : (getLabelStr(view.label) || view.code)} +
+ {#if isNew} + + + {:else} + + {/if} +
+
+ + +
+ + {#if isNew} +
+ vw_ + viewIdError = ''} + onkeydown={e => { if (e.key === 'Enter') saveViewNew() }} + placeholder="frontal, detalle-rueda…" + class={['field-input font-mono text-xs flex-1', viewIdError ? 'border-red-600' : ''].join(' ')} /> +
+ {#if viewIdError}{viewIdError}{/if} + {:else} + {view.id} + {/if} +
+ + +
+
+ + { + updateView(view.id, { code: e.currentTarget.value }) + validateViewField('code', e.currentTarget.value) + }} + placeholder="front, detail-wheel…" + class={['field-input font-mono text-xs', codeError ? 'border-red-600' : ''].join(' ')} /> + {#if codeError}{codeError}{/if} +
+
+ + +
+
+ + +
+
+ + { + const v = parseInt(e.currentTarget.value) || 0 + updateView(view.id, { width: v }) + validateViewField('width', v) + }} + placeholder="800" + class={['field-input text-xs', widthError ? 'border-red-600' : ''].join(' ')} /> + {#if widthError}{widthError}{/if} +
+
+ + { + const v = parseInt(e.currentTarget.value) || 0 + updateView(view.id, { height: v }) + validateViewField('height', v) + }} + placeholder="600" + class={['field-input text-xs', heightError ? 'border-red-600' : ''].join(' ')} /> + {#if heightError}{heightError}{/if} +
+
+ + + updateView(view.id, { label: v as any })} /> + + +
+ + updateView(view.id, { icon: e.currentTarget.value || undefined })} + placeholder="car-front, image, eye…" + class="field-input text-xs font-mono" /> +
+ + +
+
+ + +
+ + {#if view.config.strategy === 'static'} +
+ + updateView(view.id, { config: { ...view.config, url: e.currentTarget.value } as any })} + placeholder="images/front.jpg" + class="field-input font-mono text-xs" /> +
+ {:else if view.config.strategy === 'dynamic'} +
+ + updateView(view.id, { config: { ...view.config, template: e.currentTarget.value } as any })} + placeholder={"car_{at_color}.jpg"} + class="field-input font-mono text-xs" /> +
+ {:else if view.config.strategy === 'api'} +
+ + updateView(view.id, { config: { ...view.config, endpoint: e.currentTarget.value } as any })} + placeholder="https://render.example.com" + class="field-input font-mono text-xs" /> +
+ {:else if view.config.strategy === 'd3'} +
+ + updateView(view.id, { config: { ...view.config, modelUrl: e.currentTarget.value } as any })} + placeholder="models/object.glb" + class="field-input font-mono text-xs" /> +
+ {:else} +
+ + Editable en modo avanzado +
+ {/if} +
+ + +
+ +
+ updateView(view.id, { + metadata: Object.keys(m).filter(k => k !== 'order').length === 0 ? undefined : m + })} /> +
+
+ +
+ + {:else if selectedView && selectedHotspot && (selected as any).kind === 'hotspot'} + {@const view = selectedView} + {@const hs = selectedHotspot} + {@const isNew = newHotspotId === hs.id} +
+ + +
+ {isNew ? 'Nuevo hotspot' : (getLabelStr(hs.label) || hs.id)} +
+ {#if isNew} + + + {:else} + + {/if} +
+
+ + +
+ + {#if isNew} +
+ hs_ + hotspotIdError = ''} + onkeydown={e => { if (e.key === 'Enter') saveHotspotNew() }} + placeholder="rueda, interior…" + class={['field-input font-mono text-xs flex-1', hotspotIdError ? 'border-red-600' : ''].join(' ')} /> +
+ {#if hotspotIdError}{hotspotIdError}{/if} + {:else} + {hs.id} + {/if} +
+ + + updateHotspot(view.id, hs.id, { label: v as any })} /> + + +
+
+ + +
+
+ + updateHotspot(view.id, hs.id, { targetAtt: (e.currentTarget.value || undefined) as any })} + placeholder="at_color…" + class="field-input font-mono text-xs" /> +
+
+ + +
+
+ + { + const v = clampPos(parseFloat(e.currentTarget.value) || 0) + updateHotspot(view.id, hs.id, { posX: v }) + validateHotspotField('posX', v) + }} + placeholder="50" + class={['field-input font-mono text-xs', posXError ? 'border-red-600' : ''].join(' ')} /> + {#if posXError}{posXError}{/if} +
+
+ + { + const v = clampPos(parseFloat(e.currentTarget.value) || 0) + updateHotspot(view.id, hs.id, { posY: v }) + validateHotspotField('posY', v) + }} + placeholder="50" + class={['field-input font-mono text-xs', posYError ? 'border-red-600' : ''].join(' ')} /> + {#if posYError}{posYError}{/if} +
+ +
+ + +
+
+ +
+ { + updateHotspot(view.id, hs.id, { color: e.currentTarget.value }) + validateHotspotField('color', e.currentTarget.value) + }} + class="h-8 w-10 rounded border border-gray-800 bg-transparent cursor-pointer p-0.5" /> + { + updateHotspot(view.id, hs.id, { color: e.currentTarget.value || undefined }) + validateHotspotField('color', e.currentTarget.value) + }} + placeholder="#4f46e5" + class={['field-input font-mono text-xs flex-1', colorError ? 'border-red-600' : ''].join(' ')} /> +
+ {#if colorError}{colorError}{/if} +
+
+ + updateHotspot(view.id, hs.id, { radius: parseInt(e.currentTarget.value) || undefined })} + placeholder="16" + class="field-input text-xs" /> +
+
+ + updateHotspot(view.id, hs.id, { zIndex: parseInt(e.currentTarget.value) || undefined })} + placeholder="10" + class="field-input text-xs" /> +
+
+ + + updateHotspot(view.id, hs.id, { tooltip: (Object.keys(v).length ? v : undefined) as any })} /> + + +
+ +
+ updateHotspot(view.id, hs.id, { + metadata: Object.keys(m).filter(k => k !== 'order').length === 0 ? undefined : m + })} /> +
+
+ +
+ {/if} + +
+ + + {#if showPreview && previewView} +
+
+ Preview +
+ +
+ + +
+ {#if previewUrl} + {getLabelStr(previewView.label) + {:else} +
+ + + + + + {pickingPos ? 'Haz click para posicionar' : 'Sin imagen'} +
+ {/if} + + + {#each (previewView.hotspots ?? []) as marker (marker.id)} + {@const isActive = selectedHotspot?.id === marker.id} + {@const mX = typeof marker.posX === 'number' ? marker.posX : parseFloat(String(marker.posX)) || 0} + {@const mY = typeof marker.posY === 'number' ? marker.posY : parseFloat(String(marker.posY)) || 0} + {@const size = (marker.radius ?? 8) * 2} +
+ {/each} +
+ + +
+ {previewView.width || '?'} x {previewView.height || '?'} px + {#if pickingPos} + Click en imagen para posicionar + {/if} +
+
+
+ {/if} + +
diff --git a/src/vcen-edit/stores/catalog-editor.svelte.ts b/src/vcen-edit/stores/catalog-editor.svelte.ts index e980771..d2fcf8d 100644 --- a/src/vcen-edit/stores/catalog-editor.svelte.ts +++ b/src/vcen-edit/stores/catalog-editor.svelte.ts @@ -348,6 +348,13 @@ export function createCatalogEditorStore(initial: Cat) { }); } + function setObjVisual(objId: ObjID, visual: Visual | undefined) { + mutate(() => { + const obj = findObj(objId); + if (obj) obj.visual = visual; + }); + } + function setObjIssues(objId: ObjID, issues: Issue[]) { mutate(() => { const obj = findObj(objId); @@ -458,6 +465,13 @@ export function createCatalogEditorStore(initial: Cat) { }); } + function setSecVisual(objId: ObjID, secId: SecID, visual: Visual | undefined) { + mutate(() => { + const sec = findSec(objId, secId); + if (sec) sec.visual = visual; + }); + } + function setSecIssues(objId: ObjID, secId: SecID, issues: Issue[]) { mutate(() => { const sec = findSec(objId, secId); @@ -501,6 +515,7 @@ export function createCatalogEditorStore(initial: Cat) { } else { select({ kind: 'obj-atts', objId, attId: att.id }); } + newEntityId = att.id; return att.id; } @@ -509,6 +524,7 @@ export function createCatalogEditorStore(initial: Cat) { mutate(() => { cat.atts.push(att); }); logr.info('catalog-editor', { es: 'Atributo de catálogo creado' }, { id: att.id }); select({ kind: 'cat-atts', attId: att.id }); + newEntityId = att.id; return att.id; } @@ -731,6 +747,7 @@ export function createCatalogEditorStore(initial: Cat) { if ('attId' in s && s.attId === attId) { selection = { ...s, attId: newId } as Selection; } + if (newEntityId === (attId as string)) newEntityId = newId as string; } // ── Opts globales ─────────────────────────────────────────────────────── @@ -977,12 +994,12 @@ export function createCatalogEditorStore(initial: Cat) { // Obj addObj, cloneObj, renameObj, setObjLabel, removeObj, moveObj, setObjRuleSet, updateObjMeta, - updateObjInfo, updateObjPricing, updateObjVisual, setObjIssues, + updateObjInfo, updateObjPricing, updateObjVisual, setObjVisual, setObjIssues, // Sec addSec, cloneSec, renameSec, setSecLabel, removeSec, moveSec, setSecRuleSet, updateSecMeta, - updateSecInfo, updateSecPricing, updateSecVisual, setSecIssues, + updateSecInfo, updateSecPricing, updateSecVisual, setSecVisual, setSecIssues, // Att (obj/sec level) addAtt, addCatAtt, renameAtt, setAttLabel, removeAtt, moveAtt, diff --git a/src/vcen/lib/types/att.ts b/src/vcen/lib/types/att.ts index 8cf05dd..34b200c 100644 --- a/src/vcen/lib/types/att.ts +++ b/src/vcen/lib/types/att.ts @@ -89,7 +89,7 @@ export interface AttOpt { optId: OptID; /** Override de etiqueta para este att concreto */ - label?: LingString; + label?: LingRecord; /** override el code para este att concreto */ code?: string; diff --git a/src/vcen/lib/types/view.ts b/src/vcen/lib/types/view.ts index 8ca21f2..c47cb27 100644 --- a/src/vcen/lib/types/view.ts +++ b/src/vcen/lib/types/view.ts @@ -334,6 +334,10 @@ export interface View { code: string; viewType: ViewType; config: ViewConfig; + + width : number; + height: number; + /** Orden de presentación en la UI. Menor = primero */ order?: number; icon?: string;