feat(vcen-edit): sidebar redimensionable y nuevo diseño de lista de atributos

- CatalogLayout: sidebar izquierdo redimensionable por arrastre (min 160px, max 480px);
  separador de 1px con feedback visual al hover y al arrastrar
- ObjAttsPanel / CatAttsPanel / SecAttsPanel: nuevo diseño de item de atributo
  inspirado en el sidebar de AttEditor demo — nombre (text-sm) + ID mono debajo,
  badge de dominio neutro a la derecha con nombre completo, indicador ⚡ para reglas,
  selección azul (border-blue-500), botón 'eliminar' independiente bajo cada item

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
master
dev 7 months ago
parent 12430c780b
commit eb46479092

@ -8,16 +8,6 @@ const sel = $derived(editor.selection)
const atts = $derived(editor.cat.atts ?? [])
const attId = $derived(sel.kind === 'cat-atts' ? sel.attId : undefined)
const domainBadge: Record<string, string> = {
string: 'bg-blue-900/40 text-blue-400',
number: 'bg-green-900/40 text-green-400',
currency: 'bg-emerald-900/40 text-emerald-400',
boolean: 'bg-purple-900/40 text-purple-400',
option: 'bg-amber-900/40 text-amber-400',
date: 'bg-rose-900/40 text-rose-400',
time: 'bg-rose-900/40 text-rose-400',
datetime: 'bg-rose-900/40 text-rose-400',
}
let creating = $state(false)
let newAttName = $state('')
@ -65,27 +55,34 @@ function cancelAdd() {
{:else}
{#each atts as att (att.id)}
{@const isActive = attId === att.id}
<div
role="button" tabindex="0"
<button
onclick={() => editor.select({ kind: 'cat-atts', attId: att.id })}
onkeydown={e => e.key === 'Enter' && editor.select({ kind: 'cat-atts', attId: att.id })}
class="px-4 py-2.5 flex items-center gap-2 text-sm cursor-pointer
transition-colors group border-l-2
class="w-full text-left px-4 py-2.5 flex items-center justify-between
cursor-pointer transition-colors border-l-2
{isActive
? 'bg-indigo-600/20 text-indigo-300 border-indigo-500'
: 'text-gray-400 hover:bg-gray-800 hover:text-gray-200 border-transparent'}"
? 'bg-gray-800 border-blue-500'
: 'border-transparent hover:bg-gray-800'}"
>
<span class="text-xs px-1.5 py-0.5 rounded shrink-0
{domainBadge[att.value.domain.kind] ?? 'bg-gray-800 text-gray-500'}">
{att.value.domain.kind.slice(0, 3)}
</span>
<span class="flex-1 truncate">
{#if att.name}{att.name}{:else}<span class="italic text-gray-600">Sin nombre</span>{/if}
</span>
<div class="flex flex-col gap-0.5 min-w-0">
<span class="text-sm truncate {isActive ? 'text-gray-100' : 'text-gray-300'}">
{att.name || 'Sin nombre'}
</span>
<span class="text-xs text-gray-600 font-mono truncate">{att.id}</span>
</div>
<div class="flex items-center gap-1 shrink-0 ml-2">
<span class="text-xs px-1 py-0.5 rounded bg-gray-800 text-gray-500 border border-gray-700">
{att.value.domain.kind}
</span>
{#if att.rules?.rules?.length}
<span class="text-xs text-blue-400" title="Tiene reglas">⚡</span>
{/if}
</div>
</button>
<div class="px-4 pb-1 flex justify-end">
<button
onclick={e => { e.stopPropagation(); editor.removeCatAtt(att.id) }}
class="opacity-0 group-hover:opacity-100 text-gray-600 hover:text-red-400 transition-opacity text-xs"
>✕</button>
onclick={() => editor.removeCatAtt(att.id)}
class="text-xs text-gray-700 hover:text-red-400 transition-colors"
>eliminar</button>
</div>
{/each}
{/if}

@ -44,6 +44,25 @@ const sel = $derived(editor.selection)
let showJson = $state(false)
let showGraph = $state(false)
let sidebarW = $state(256)
let dragging = $state(false)
function onHandleMousedown(e: MouseEvent) {
dragging = true
const startX = e.clientX
const startW = sidebarW
function onMove(ev: MouseEvent) {
sidebarW = Math.max(160, Math.min(480, startW + ev.clientX - startX))
}
function onUp() {
dragging = false
window.removeEventListener('mousemove', onMove)
window.removeEventListener('mouseup', onUp)
}
window.addEventListener('mousemove', onMove)
window.addEventListener('mouseup', onUp)
}
const graphResult = $derived.by(() => {
try {
const g = buildDependencyGraph(editor.cat)
@ -118,10 +137,18 @@ const isValid = $derived(graphResult.graph !== null)
<div class="flex flex-1 min-h-0">
<!-- Left sidebar tree -->
<div class="w-64 shrink-0 border-r border-gray-800 bg-gray-950 overflow-y-auto">
<div class="shrink-0 border-r border-gray-800 bg-gray-950 overflow-y-auto"
style="width: {sidebarW}px">
<CatalogSidebar />
</div>
<!-- Resize handle -->
<div
onmousedown={onHandleMousedown}
class="w-1 shrink-0 cursor-col-resize transition-colors
{dragging ? 'bg-indigo-500/50' : 'hover:bg-indigo-500/30 bg-transparent'}"
></div>
<!-- Main content -->
<div class="flex-1 min-w-0 overflow-hidden relative">
{#if sel.kind === 'cat-info'} <CatMetaPanel />

@ -9,16 +9,6 @@ const ctx = $derived(sel.kind === 'obj-atts'
? { objId: sel.objId, obj: editor.findObj(sel.objId), attId: sel.attId }
: null)
const domainBadge: Record<string, string> = {
string: 'bg-blue-900/40 text-blue-400',
number: 'bg-green-900/40 text-green-400',
currency: 'bg-emerald-900/40 text-emerald-400',
boolean: 'bg-purple-900/40 text-purple-400',
option: 'bg-amber-900/40 text-amber-400',
date: 'bg-rose-900/40 text-rose-400',
time: 'bg-rose-900/40 text-rose-400',
datetime: 'bg-rose-900/40 text-rose-400',
}
let creating = $state(false)
let newAttName = $state('')
@ -72,30 +62,34 @@ function cancelAdd() {
{:else}
{#each obj.atts as att (att.id)}
{@const isActive = attId === att.id}
<div
role="button" tabindex="0"
<button
onclick={() => editor.select({ kind: 'obj-atts', objId, attId: att.id })}
onkeydown={e => e.key === 'Enter' && editor.select({ kind: 'obj-atts', objId, attId: att.id })}
class="px-4 py-2.5 flex items-center gap-2 text-sm cursor-pointer
transition-colors group border-l-2
class="w-full text-left px-4 py-2.5 flex items-center justify-between
cursor-pointer transition-colors border-l-2
{isActive
? 'bg-indigo-600/20 text-indigo-300 border-indigo-500'
: 'text-gray-400 hover:bg-gray-800 hover:text-gray-200 border-transparent'}"
? 'bg-gray-800 border-blue-500'
: 'border-transparent hover:bg-gray-800'}"
>
<span class="text-xs px-1.5 py-0.5 rounded shrink-0
{domainBadge[att.value.domain.kind] ?? 'bg-gray-800 text-gray-500'}">
{att.value.domain.kind.slice(0, 3)}
</span>
<span class="flex-1 truncate">
{#if att.name}{att.name}{:else}<span class="italic text-gray-600">Sin nombre</span>{/if}
</span>
{#if att.rules?.rules?.length}
<span class="w-1.5 h-1.5 rounded-full bg-indigo-500 shrink-0" title="Tiene reglas"></span>
{/if}
<div class="flex flex-col gap-0.5 min-w-0">
<span class="text-sm truncate {isActive ? 'text-gray-100' : 'text-gray-300'}">
{att.name || 'Sin nombre'}
</span>
<span class="text-xs text-gray-600 font-mono truncate">{att.id}</span>
</div>
<div class="flex items-center gap-1 shrink-0 ml-2">
<span class="text-xs px-1 py-0.5 rounded bg-gray-800 text-gray-500 border border-gray-700">
{att.value.domain.kind}
</span>
{#if att.rules?.rules?.length}
<span class="text-xs text-blue-400" title="Tiene reglas">⚡</span>
{/if}
</div>
</button>
<div class="px-4 pb-1 flex justify-end">
<button
onclick={e => { e.stopPropagation(); editor.removeAtt(objId, undefined, att.id) }}
class="opacity-0 group-hover:opacity-100 text-gray-600 hover:text-red-400 transition-opacity text-xs"
>✕</button>
onclick={() => editor.removeAtt(objId, undefined, att.id)}
class="text-xs text-gray-700 hover:text-red-400 transition-colors"
>eliminar</button>
</div>
{/each}
{/if}

@ -9,16 +9,6 @@ const ctx = $derived(sel.kind === 'sec-atts'
? { objId: sel.objId, secId: sel.secId, sec: editor.findSec(sel.objId, sel.secId), attId: sel.attId }
: null)
const domainBadge: Record<string, string> = {
string: 'bg-blue-900/40 text-blue-400',
number: 'bg-green-900/40 text-green-400',
currency: 'bg-emerald-900/40 text-emerald-400',
boolean: 'bg-purple-900/40 text-purple-400',
option: 'bg-amber-900/40 text-amber-400',
date: 'bg-rose-900/40 text-rose-400',
time: 'bg-rose-900/40 text-rose-400',
datetime: 'bg-rose-900/40 text-rose-400',
}
let creating = $state(false)
let newAttName = $state('')
@ -75,30 +65,34 @@ function cancelAdd() {
{:else}
{#each sec.atts as att (att.id)}
{@const isActive = attId === att.id}
<div
role="button" tabindex="0"
<button
onclick={() => editor.select({ kind: 'sec-atts', objId, secId, attId: att.id })}
onkeydown={e => e.key === 'Enter' && editor.select({ kind: 'sec-atts', objId, secId, attId: att.id })}
class="px-4 py-2.5 flex items-center gap-2 text-sm cursor-pointer
transition-colors group border-l-2
class="w-full text-left px-4 py-2.5 flex items-center justify-between
cursor-pointer transition-colors border-l-2
{isActive
? 'bg-indigo-600/20 text-indigo-300 border-indigo-500'
: 'text-gray-400 hover:bg-gray-800 hover:text-gray-200 border-transparent'}"
? 'bg-gray-800 border-blue-500'
: 'border-transparent hover:bg-gray-800'}"
>
<span class="text-xs px-1.5 py-0.5 rounded shrink-0
{domainBadge[att.value.domain.kind] ?? 'bg-gray-800 text-gray-500'}">
{att.value.domain.kind.slice(0, 3)}
</span>
<span class="flex-1 truncate">
{#if att.name}{att.name}{:else}<span class="italic text-gray-600">Sin nombre</span>{/if}
</span>
{#if att.rules?.rules?.length}
<span class="w-1.5 h-1.5 rounded-full bg-indigo-500 shrink-0"></span>
{/if}
<div class="flex flex-col gap-0.5 min-w-0">
<span class="text-sm truncate {isActive ? 'text-gray-100' : 'text-gray-300'}">
{att.name || 'Sin nombre'}
</span>
<span class="text-xs text-gray-600 font-mono truncate">{att.id}</span>
</div>
<div class="flex items-center gap-1 shrink-0 ml-2">
<span class="text-xs px-1 py-0.5 rounded bg-gray-800 text-gray-500 border border-gray-700">
{att.value.domain.kind}
</span>
{#if att.rules?.rules?.length}
<span class="text-xs text-blue-400" title="Tiene reglas">⚡</span>
{/if}
</div>
</button>
<div class="px-4 pb-1 flex justify-end">
<button
onclick={e => { e.stopPropagation(); editor.removeAtt(objId, secId, att.id) }}
class="opacity-0 group-hover:opacity-100 text-gray-600 hover:text-red-400 transition-opacity text-xs"
>✕</button>
onclick={() => editor.removeAtt(objId, secId, att.id)}
class="text-xs text-gray-700 hover:text-red-400 transition-colors"
>eliminar</button>
</div>
{/each}
{/if}

@ -114,7 +114,7 @@ export function evalRuleSetState(
)
if (!action) continue
if (fires) return action.ifTrue
if (fires) return (action as any).ifTrue
if (fallback === null) fallback = (action as any).ifFalse ?? null
}

Loading…
Cancel
Save

Powered by TurnKey Linux.