diff --git a/src/vcen-edit/comps/OptDetail.svelte b/src/vcen-edit/comps/OptDetail.svelte index bea0474..a048e71 100644 --- a/src/vcen-edit/comps/OptDetail.svelte +++ b/src/vcen-edit/comps/OptDetail.svelte @@ -103,7 +103,7 @@ const labelStr = $derived(typeof opt.label === 'string' ? opt.label : (opt.lab onclick={() => editor.updateOpt(opt.id, { value: { domain: { kind, mode: 'input' as any }, value: kind === 'number' ? 0 : kind === 'boolean' ? false : '' } })} - class="px-3 py-1.5 text-xs rounded border transition-colors + class="px-3 py-1.5 text-xs rounded border transition-colors {opt.value.kind === kind ? 'bg-indigo-600/20 border-indigo-500 text-indigo-300' : 'border-gray-700 text-gray-500 hover:border-gray-600 hover:text-gray-300'}" diff --git a/src/vcen-edit/comps/catalog-editor.svelte.ts b/src/vcen-edit/comps/catalog-editor.svelte.ts index 443ad3e..e44e7e0 100644 --- a/src/vcen-edit/comps/catalog-editor.svelte.ts +++ b/src/vcen-edit/comps/catalog-editor.svelte.ts @@ -283,7 +283,7 @@ export function createCatalogEditorStore(initial: Cat) { function updateAttDefaultValue(objId: ObjID, secId: SecID | undefined, attId: AttID, value: unknown) { mutate(() => { const att = findAtt(objId, secId, attId); - if (att) (att.value as any).value = value; + if (att) att.value = { ...att.value, value } as typeof att.value; }); } diff --git a/src/vcen-edit/demos/RuntimePanel.svelte b/src/vcen-edit/demos/RuntimePanel.svelte index 727e2ea..8e39bc0 100644 --- a/src/vcen-edit/demos/RuntimePanel.svelte +++ b/src/vcen-edit/demos/RuntimePanel.svelte @@ -96,17 +96,19 @@ - {:else if domain.kind === 'option'} + {:else if domain.kind === 'option' && domain.mode === 'list'} + {@const optList = domain.list ?? []} diff --git a/src/vcen-edit/demos/demo.store.svelte.ts b/src/vcen-edit/demos/demo.store.svelte.ts index 4734d0c..89a0eae 100644 --- a/src/vcen-edit/demos/demo.store.svelte.ts +++ b/src/vcen-edit/demos/demo.store.svelte.ts @@ -1,6 +1,6 @@ import { createSession } from '@/vcen/lib/session.ts'; import { buildDependencyGraph } from '@/vcen/lib/graph.ts'; -import type { Cat, Obj, Att, Sec } from '@/vcen/lib/types'; +import type { Cat, Obj, Att, Sec, Opt, OptID } from '@/vcen/lib/types'; import type { Rule } from '@/vcen/lib/types'; import type { DependencyGraph } from '@/vcen/lib/types'; import type { Session } from '@/vcen/lib/session.ts'; @@ -54,20 +54,32 @@ export function createDemoStore() { // ── Cat mutations ───────────────────────────────────────────────────────── function addAtt(kind: DomainKind) { - const id = `at_${Date.now()}` as Att['id']; - const options: string[] = kind === 'option' ? ['opcion1', 'opcion2'] : []; - const att: Att = { - id, - name: 'Nuevo atributo', - value: { domain: makeDomain(kind, options), value: defaultValue(kind, options) }, - }; - cat = { - ...cat, - objs: cat.objs.map((o, i) => i === 0 - ? { ...o, atts: [...o.atts, att] } - : o - ), - }; + const id = `at_${Date.now()}` as Att['id']; + if (kind === 'option') { + const defaultStrs = ['opcion1', 'opcion2']; + const newOpts = defaultStrs.map(strToOpt); + const optIds = newOpts.map(o => o.id); + const att: Att = { + id, + name: 'Nuevo atributo', + value: { domain: makeDomain('option', optIds), value: optIds[0] ?? null }, + }; + cat = { + ...cat, + opts: [...cat.opts, ...newOpts], + objs: cat.objs.map((o, i) => i === 0 ? { ...o, atts: [...o.atts, att] } : o), + }; + } else { + const att: Att = { + id, + name: 'Nuevo atributo', + value: { domain: makeDomain(kind, []), value: defaultValue(kind) }, + }; + cat = { + ...cat, + objs: cat.objs.map((o, i) => i === 0 ? { ...o, atts: [...o.atts, att] } : o), + }; + } selectedAttId = id; rebuildSession(); } @@ -94,28 +106,49 @@ export function createDemoStore() { } function updateAttDomain(attId: string, kind: DomainKind) { - cat = updateAtt(cat, attId, a => { - const options = kind === 'option' ? ((a.value.domain as any).options ?? []) : []; - return { - ...a, - value: { domain: makeDomain(kind, options), value: defaultValue(kind, options) }, + if (kind === 'option') { + // Crear opts por defecto y referenciarlas en el dominio + const defaultStrs = ['opcion1', 'opcion2']; + const newOpts = defaultStrs.map(strToOpt); + const newOptIds = newOpts.map(o => o.id); + cat = { + ...updateAtt(cat, attId, a => ({ + ...a, + value: { domain: makeDomain('option', newOptIds), value: newOptIds[0] ?? null }, + })), + opts: [...cat.opts, ...newOpts], }; - }); + } else { + cat = updateAtt(cat, attId, a => ({ + ...a, + value: { domain: makeDomain(kind, []), value: defaultValue(kind) }, + })); + } rebuildSession(); } - function updateAttOptions(attId: string, options: string[]) { - cat = updateAtt(cat, attId, a => { - const currentVal = a.value.value; - const value = options.includes(currentVal as string) ? currentVal : (options[0] ?? ''); - return { - ...a, - value: { - domain: { ...a.value.domain, options } as any, - value, - }, - }; - }); + function updateAttOptions(attId: string, optStrs: string[]) { + // Convertir strings a Opt + ids + const existingByVal = new Map(cat.opts.map(o => [String(o.value.value), o])); + const updatedOpts: Opt[] = []; + const optIds: OptID[] = []; + for (const s of optStrs) { + const existing = existingByVal.get(s); + const opt = existing ?? strToOpt(s); + updatedOpts.push(opt); + optIds.push(opt.id); + } + // Merge: mantener opts no afectadas + agregar/sustituir las de este att + const affectedIds = new Set(optIds); + const otherOpts = cat.opts.filter(o => !affectedIds.has(o.id)); + cat = { + ...updateAtt(cat, attId, a => { + const currentVal = a.value.value as OptID | null; + const value = optIds.includes(currentVal as OptID) ? currentVal : (optIds[0] ?? null); + return { ...a, value: { domain: makeDomain('option', optIds), value } }; + }), + opts: [...otherOpts, ...updatedOpts], + }; rebuildSession(); } @@ -190,9 +223,18 @@ export type DemoStore = ReturnType; // ─── Helpers ────────────────────────────────────────────────────────────────── +function strToOpt(s: string): Opt { + return { + id: `op_${s.replace(/\s+/g, '_')}` as OptID, + value: { kind: 'string', value: s }, + }; +} + function makeFreshCat(): Cat { + const colorOpts = ['rojo', 'azul', 'negro'].map(strToOpt); + const colorIds = colorOpts.map(o => o.id); return { - opts : [], + opts : colorOpts, optCategories: [], id: 'ct_demo' as Cat['id'], name: 'Catálogo demo', @@ -204,12 +246,12 @@ function makeFreshCat(): Cat { { id: 'at_color' as Att['id'], name: 'Color', - value: { domain: { kind: 'option', options: ['rojo', 'azul', 'negro'] } as any, value: 'rojo' }, + value: { domain: { kind: 'option', mode: 'list', list: colorIds.map(id => ({ optId: id })) }, value: colorIds[0] }, }, { id: 'at_precio' as Att['id'], name: 'Precio', - value: { domain: { kind: 'number' } as any, value: 0 }, + value: { domain: { kind: 'number', mode: 'input' }, value: 0 }, }, ], secs: [], @@ -217,15 +259,18 @@ function makeFreshCat(): Cat { }; } -function makeDomain(kind: DomainKind, options: string[] = []): any { - if (kind === 'option') return { kind, options }; - return { kind }; +function makeDomain(kind: DomainKind, optIds: OptID[] = []): any { + if (kind === 'option') return { kind, mode: 'list', list: optIds.map(id => ({ optId: id })) }; + if (kind === 'number' || kind === 'currency') return { kind, mode: 'input' }; + if (kind === 'boolean') return { kind, mode: 'input' }; + if (kind === 'date' || kind === 'time' || kind === 'datetime') return { kind, mode: 'input' }; + return { kind, mode: 'input' }; } -function defaultValue(kind: DomainKind, options: string[] = []): any { +function defaultValue(kind: DomainKind): any { if (kind === 'number' || kind === 'currency') return 0; if (kind === 'boolean') return false; - if (kind === 'option') return options[0] ?? ''; + if (kind === 'option') return null; if (kind === 'date') return new Date().toISOString().split('T')[0]; if (kind === 'time') return '08:00'; if (kind === 'datetime') return new Date().toISOString().slice(0, 16); diff --git a/src/vcen-edit/stores/catalog-editor.svelte.ts b/src/vcen-edit/stores/catalog-editor.svelte.ts index 1e90298..79a79eb 100644 --- a/src/vcen-edit/stores/catalog-editor.svelte.ts +++ b/src/vcen-edit/stores/catalog-editor.svelte.ts @@ -282,7 +282,7 @@ export function createCatalogEditorStore(initial: Cat) { function updateAttDefaultValue(objId: ObjID, secId: SecID | undefined, attId: AttID, value: unknown) { mutate(() => { const att = findAtt(objId, secId, attId); - if (att) (att.value as any).value = value; + if (att) att.value = { ...att.value, value } as typeof att.value; }); } @@ -456,13 +456,20 @@ export function createCatalogEditorStore(initial: Cat) { selection = { kind: 'cat' }; } + // ── Opt activa (para OptsPanel / OptDetail) ────────────────────────────── + + let activeOptId = $state(null); + // ── API pública ────────────────────────────────────────────────────────── return { // Estado reactivo - get cat() { return cat; }, - get selection() { return selection; }, - get dirty() { return dirty; }, + get cat() { return cat; }, + get selection() { return selection; }, + get dirty() { return dirty; }, + get activeOptId() { return activeOptId; }, + get activeOpt() { return activeOptId ? cat.opts.find(o => o.id === activeOptId) ?? null : null; }, + setActiveOpt(id: string) { activeOptId = id; }, // Selección select, diff --git a/src/vcen-edit/stores/session.svelte.ts b/src/vcen-edit/stores/session.svelte.ts index eb5f142..2ce522b 100644 --- a/src/vcen-edit/stores/session.svelte.ts +++ b/src/vcen-edit/stores/session.svelte.ts @@ -55,7 +55,7 @@ export function createSessionStore( cat: Cat, engine: JsonLogicEngine | null = null, ) { - const session = createSession(cat, engine); + const session = createSession(cat, null, engine); // ── Estado reactivo ─────────────────────────────────────────────────────── // Un único $state que contiene el EvaluationResult completo. diff --git a/src/vcen/lib/session.ts b/src/vcen/lib/session.ts index b67f023..044a157 100644 --- a/src/vcen/lib/session.ts +++ b/src/vcen/lib/session.ts @@ -1,4 +1,7 @@ import type {GlobInstance} from "@/glob/lib"; +// GlobInstance puede ser null cuando la sesión se usa solo para evaluación +// (sin pricing ni view resolution) +type GlobOrNull = GlobInstance | null; import { JsonLogicEngine } from '@/jslg/lib'; import type { Cat, @@ -55,7 +58,7 @@ export class Session { private readonly cat: Cat; private readonly graph: DependencyGraph; private readonly engine: JsonLogicEngine; - private readonly glob: GlobInstance; + private readonly glob: GlobOrNull; private readonly initialResult: EvaluationResult; private current: EvaluationResult; private readonly listeners: Set = new Set(); @@ -64,7 +67,7 @@ export class Session { constructor( cat: Cat, - glob: GlobInstance, + glob: GlobOrNull = null, engine: JsonLogicEngine | null = null, ) { this.cat = cat; @@ -130,6 +133,7 @@ export class Session { * @returns `ObjPriceResult` o `undefined` si el objId no existe en el Cat */ getPrice(objId: string): ObjPriceResult | undefined { + if (!this.glob) return undefined; const obj = this.cat.objs.find(o => o.id === objId); if (!obj) return undefined; @@ -149,8 +153,9 @@ export class Session { * @returns Array de `ObjPriceResult`, uno por cada Obj del Cat */ getAllPrices(): ObjPriceResult[] { + if (!this.glob) return []; return this.cat.objs.map(obj => - computeObjPrice(obj, this.cat.opts, this.cat.pricingDefaults, this.configState, this.glob, this.engine) + computeObjPrice(obj, this.cat.opts, this.cat.pricingDefaults, this.configState, this.glob!, this.engine) ); } @@ -172,7 +177,8 @@ export class Session { obj: Obj, secVisual?: Visual, objVisual?: Visual, - ): ViewResolution { + ): ViewResolution | undefined { + if (!this.glob) return undefined; return resolveViewFn( view, obj, secVisual, objVisual, this.cat, this.configState, this.engine, ); @@ -192,6 +198,7 @@ export class Session { secVisual?: Visual, objVisual?: Visual, ): ViewResolution[] { + if (!this.glob) return []; return resolveVisualFn( visual, obj, secVisual, objVisual, this.cat, this.configState, this.engine, ); @@ -209,6 +216,7 @@ export class Session { secVisual?: Visual, objVisual?: Visual, ): ViewResolution | undefined { + if (!this.glob) return undefined; return resolveDefaultViewFn( visual, obj, secVisual, objVisual, this.cat, this.configState, this.engine, ); @@ -338,7 +346,7 @@ export class Session { */ export function createSession( cat: Cat, - glob: GlobInstance, + glob: GlobInstance | null = null, engine: JsonLogicEngine | null = null, ): Session { return new Session(cat, glob, engine);