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);