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 atts = $derived(editor.cat.atts ?? [])
const attId = $derived(sel.kind === 'cat-atts' ? sel.attId : undefined) 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 creating = $state(false)
let newAttName = $state('') let newAttName = $state('')
@ -65,27 +55,34 @@ function cancelAdd() {
{:else} {:else}
{#each atts as att (att.id)} {#each atts as att (att.id)}
{@const isActive = attId === att.id} {@const isActive = attId === att.id}
<div <button
role="button" tabindex="0"
onclick={() => editor.select({ kind: 'cat-atts', attId: att.id })} onclick={() => editor.select({ kind: 'cat-atts', attId: att.id })}
onkeydown={e => e.key === 'Enter' && editor.select({ kind: 'cat-atts', attId: att.id })} class="w-full text-left px-4 py-2.5 flex items-center justify-between
class="px-4 py-2.5 flex items-center gap-2 text-sm cursor-pointer cursor-pointer transition-colors border-l-2
transition-colors group border-l-2
{isActive {isActive
? 'bg-indigo-600/20 text-indigo-300 border-indigo-500' ? 'bg-gray-800 border-blue-500'
: 'text-gray-400 hover:bg-gray-800 hover:text-gray-200 border-transparent'}" : 'border-transparent hover:bg-gray-800'}"
> >
<span class="text-xs px-1.5 py-0.5 rounded shrink-0 <div class="flex flex-col gap-0.5 min-w-0">
{domainBadge[att.value.domain.kind] ?? 'bg-gray-800 text-gray-500'}"> <span class="text-sm truncate {isActive ? 'text-gray-100' : 'text-gray-300'}">
{att.value.domain.kind.slice(0, 3)} {att.name || 'Sin nombre'}
</span> </span>
<span class="flex-1 truncate"> <span class="text-xs text-gray-600 font-mono truncate">{att.id}</span>
{#if att.name}{att.name}{:else}<span class="italic text-gray-600">Sin nombre</span>{/if} </div>
</span> <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 <button
onclick={e => { e.stopPropagation(); editor.removeCatAtt(att.id) }} onclick={() => editor.removeCatAtt(att.id)}
class="opacity-0 group-hover:opacity-100 text-gray-600 hover:text-red-400 transition-opacity text-xs" class="text-xs text-gray-700 hover:text-red-400 transition-colors"
>✕</button> >eliminar</button>
</div> </div>
{/each} {/each}
{/if} {/if}

@ -44,6 +44,25 @@ const sel = $derived(editor.selection)
let showJson = $state(false) let showJson = $state(false)
let showGraph = $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(() => { const graphResult = $derived.by(() => {
try { try {
const g = buildDependencyGraph(editor.cat) const g = buildDependencyGraph(editor.cat)
@ -118,10 +137,18 @@ const isValid = $derived(graphResult.graph !== null)
<div class="flex flex-1 min-h-0"> <div class="flex flex-1 min-h-0">
<!-- Left sidebar tree --> <!-- 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 /> <CatalogSidebar />
</div> </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 --> <!-- Main content -->
<div class="flex-1 min-w-0 overflow-hidden relative"> <div class="flex-1 min-w-0 overflow-hidden relative">
{#if sel.kind === 'cat-info'} <CatMetaPanel /> {#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 } ? { objId: sel.objId, obj: editor.findObj(sel.objId), attId: sel.attId }
: null) : 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 creating = $state(false)
let newAttName = $state('') let newAttName = $state('')
@ -72,30 +62,34 @@ function cancelAdd() {
{:else} {:else}
{#each obj.atts as att (att.id)} {#each obj.atts as att (att.id)}
{@const isActive = attId === att.id} {@const isActive = attId === att.id}
<div <button
role="button" tabindex="0"
onclick={() => editor.select({ kind: 'obj-atts', objId, attId: att.id })} 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="w-full text-left px-4 py-2.5 flex items-center justify-between
class="px-4 py-2.5 flex items-center gap-2 text-sm cursor-pointer cursor-pointer transition-colors border-l-2
transition-colors group border-l-2
{isActive {isActive
? 'bg-indigo-600/20 text-indigo-300 border-indigo-500' ? 'bg-gray-800 border-blue-500'
: 'text-gray-400 hover:bg-gray-800 hover:text-gray-200 border-transparent'}" : 'border-transparent hover:bg-gray-800'}"
> >
<span class="text-xs px-1.5 py-0.5 rounded shrink-0 <div class="flex flex-col gap-0.5 min-w-0">
{domainBadge[att.value.domain.kind] ?? 'bg-gray-800 text-gray-500'}"> <span class="text-sm truncate {isActive ? 'text-gray-100' : 'text-gray-300'}">
{att.value.domain.kind.slice(0, 3)} {att.name || 'Sin nombre'}
</span> </span>
<span class="flex-1 truncate"> <span class="text-xs text-gray-600 font-mono truncate">{att.id}</span>
{#if att.name}{att.name}{:else}<span class="italic text-gray-600">Sin nombre</span>{/if} </div>
</span> <div class="flex items-center gap-1 shrink-0 ml-2">
{#if att.rules?.rules?.length} <span class="text-xs px-1 py-0.5 rounded bg-gray-800 text-gray-500 border border-gray-700">
<span class="w-1.5 h-1.5 rounded-full bg-indigo-500 shrink-0" title="Tiene reglas"></span> {att.value.domain.kind}
{/if} </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 <button
onclick={e => { e.stopPropagation(); editor.removeAtt(objId, undefined, att.id) }} onclick={() => editor.removeAtt(objId, undefined, att.id)}
class="opacity-0 group-hover:opacity-100 text-gray-600 hover:text-red-400 transition-opacity text-xs" class="text-xs text-gray-700 hover:text-red-400 transition-colors"
>✕</button> >eliminar</button>
</div> </div>
{/each} {/each}
{/if} {/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 } ? { objId: sel.objId, secId: sel.secId, sec: editor.findSec(sel.objId, sel.secId), attId: sel.attId }
: null) : 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 creating = $state(false)
let newAttName = $state('') let newAttName = $state('')
@ -75,30 +65,34 @@ function cancelAdd() {
{:else} {:else}
{#each sec.atts as att (att.id)} {#each sec.atts as att (att.id)}
{@const isActive = attId === att.id} {@const isActive = attId === att.id}
<div <button
role="button" tabindex="0"
onclick={() => editor.select({ kind: 'sec-atts', objId, secId, attId: att.id })} 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="w-full text-left px-4 py-2.5 flex items-center justify-between
class="px-4 py-2.5 flex items-center gap-2 text-sm cursor-pointer cursor-pointer transition-colors border-l-2
transition-colors group border-l-2
{isActive {isActive
? 'bg-indigo-600/20 text-indigo-300 border-indigo-500' ? 'bg-gray-800 border-blue-500'
: 'text-gray-400 hover:bg-gray-800 hover:text-gray-200 border-transparent'}" : 'border-transparent hover:bg-gray-800'}"
> >
<span class="text-xs px-1.5 py-0.5 rounded shrink-0 <div class="flex flex-col gap-0.5 min-w-0">
{domainBadge[att.value.domain.kind] ?? 'bg-gray-800 text-gray-500'}"> <span class="text-sm truncate {isActive ? 'text-gray-100' : 'text-gray-300'}">
{att.value.domain.kind.slice(0, 3)} {att.name || 'Sin nombre'}
</span> </span>
<span class="flex-1 truncate"> <span class="text-xs text-gray-600 font-mono truncate">{att.id}</span>
{#if att.name}{att.name}{:else}<span class="italic text-gray-600">Sin nombre</span>{/if} </div>
</span> <div class="flex items-center gap-1 shrink-0 ml-2">
{#if att.rules?.rules?.length} <span class="text-xs px-1 py-0.5 rounded bg-gray-800 text-gray-500 border border-gray-700">
<span class="w-1.5 h-1.5 rounded-full bg-indigo-500 shrink-0"></span> {att.value.domain.kind}
{/if} </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 <button
onclick={e => { e.stopPropagation(); editor.removeAtt(objId, secId, att.id) }} onclick={() => editor.removeAtt(objId, secId, att.id)}
class="opacity-0 group-hover:opacity-100 text-gray-600 hover:text-red-400 transition-opacity text-xs" class="text-xs text-gray-700 hover:text-red-400 transition-colors"
>✕</button> >eliminar</button>
</div> </div>
{/each} {/each}
{/if} {/if}

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

Loading…
Cancel
Save

Powered by TurnKey Linux.