svelte adapters first aprox

master
dev 8 months ago
parent 9bf050f990
commit aa34f88cae

230
package-lock.json generated

@ -8,10 +8,15 @@
"name": "visual-configuration-engine",
"version": "2.0.0",
"license": "EULA",
"dependencies": {
"svelte": "^5.53.0"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@types/node": "^25.2.3",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.18"
}
},
@ -470,11 +475,50 @@
"node": ">=18"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping/node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@ -484,7 +528,6 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
@ -855,6 +898,54 @@
"dev": true,
"license": "MIT"
},
"node_modules/@sveltejs/acorn-typescript": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz",
"integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==",
"license": "MIT",
"peerDependencies": {
"acorn": "^8.9.0"
}
},
"node_modules/@sveltejs/vite-plugin-svelte": {
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz",
"integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
"deepmerge": "^4.3.1",
"magic-string": "^0.30.21",
"obug": "^2.1.0",
"vitefu": "^1.1.1"
},
"engines": {
"node": "^20.19 || ^22.12 || >=24"
},
"peerDependencies": {
"svelte": "^5.0.0",
"vite": "^6.3.0 || ^7.0.0"
}
},
"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz",
"integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==",
"dev": true,
"license": "MIT",
"dependencies": {
"obug": "^2.1.0"
},
"engines": {
"node": "^20.19 || ^22.12 || >=24"
},
"peerDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.0.0-next.0",
"svelte": "^5.0.0",
"vite": "^6.3.0 || ^7.0.0"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
@ -905,7 +996,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
@ -918,6 +1008,12 @@
"undici-types": "~7.16.0"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
"node_modules/@vitest/expect": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
@ -1033,7 +1129,6 @@
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@ -1062,6 +1157,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@ -1072,6 +1176,15 @@
"node": ">=12"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@ -1082,6 +1195,15 @@
"node": ">=18"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@ -1089,6 +1211,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/devalue": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz",
"integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==",
"license": "MIT"
},
"node_modules/diff": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
@ -1148,6 +1286,21 @@
"@esbuild/win32-x64": "0.27.3"
}
},
"node_modules/esm-env": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"license": "MIT"
},
"node_modules/esrap": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz",
"integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
}
},
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@ -1201,11 +1354,25 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/is-reference": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.6"
}
},
"node_modules/locate-character": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"license": "MIT"
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
@ -1380,6 +1547,33 @@
"dev": true,
"license": "MIT"
},
"node_modules/svelte": {
"version": "5.53.0",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.0.tgz",
"integrity": "sha512-7dhHkSamGS2vtoBmIW2hRab+gl5Z60alEHZB4910ePqqJNxAWnDAxsofVmlZ2tREmWyHNE+A1nCKwICAquoD2A==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.6.3",
"esm-env": "^1.2.1",
"esrap": "^2.2.2",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
"zimmerframe": "^1.1.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@ -1571,6 +1765,26 @@
}
}
},
"node_modules/vitefu": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz",
"integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==",
"dev": true,
"license": "MIT",
"workspaces": [
"tests/deps/*",
"tests/projects/*",
"tests/projects/workspace/packages/*"
],
"peerDependencies": {
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
}
},
"node_modules/vitest": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
@ -1675,6 +1889,12 @@
"engines": {
"node": ">=6"
}
},
"node_modules/zimmerframe": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"license": "MIT"
}
}
}

@ -18,9 +18,14 @@
"author": "ACTIVE THING",
"license": "EULA",
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@types/node": "^25.2.3",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.18"
},
"dependencies": {
"svelte": "^5.53.0"
}
}

@ -1,346 +1,327 @@
<!--
============================================================================
CONFIGURATOR - Example Svelte Component
CONFIGURATOR - Svelte 5 Component
============================================================================
Ejemplo de cómo usar el ConfigurationStore en un componente Svelte.
-->
<script lang="ts">
import { createConfigurationStore } from '@/svelte/stores';
import type { ConfigurationCatalog } from '@/types';
// Props
export let catalog: ConfigurationCatalog;
export let objectId: string;
// Crear el store
const config = createConfigurationStore({
catalog,
objectId,
autoValidate: true
});
// Acceso reactivo al estado
$: currentSection = $config.currentSection;
$: currentView = $config.currentView;
$: selection = $config.selection;
$: validation = $config.validation;
$: isValid = $config.isValid;
// Handlers
function handleOptionSelect(attrId: string, optionId: string) {
config.selectOption(attrId, optionId);
}
function handleSectionChange(sectionId: string) {
config.setCurrentSection(sectionId);
}
function handleViewChange(viewId: string) {
config.setCurrentView(viewId);
}
function handleReset() {
config.reset();
}
function handleUndo() {
config.undo();
}
import { createConfigurationStore } from '@/svelte/stores/configuration.svelte';
import type {AttrID, ConfigurationCatalog, ObjectID, OptionID, SectionID, ViewID} from '@/types';
let { catalog, objectId }: {
catalog : ConfigurationCatalog;
objectId: ObjectID;
} = $props();
const config = createConfigurationStore({
catalog ,
objectId,
autoValidate: true
});
// Valores reactivos
const currentSection = $derived(config.current.currentSection);
const currentView = $derived(config.current.currentView);
const selection = $derived(config.current.selection);
const validation = $derived(config.current.validation);
const isValid = $derived(config.current.isValid);
const object = $derived(config.current.object);
// Helper de traducción
const t = (i18nString: any) => config.t(i18nString);
// Handlers con tipos correctos
function handleOptionSelect(attrId: AttrID, optionId: string) {
config.selectOption(attrId, optionId as OptionID);
}
function handleSectionChange(sectionId: SectionID) {
config.setCurrentSection(sectionId);
}
function handleViewChange(viewId: ViewID) {
config.setCurrentView(viewId);
}
</script>
<div class="configurator">
<!-- Header -->
<header class="configurator-header">
<h1>{$config.object.name.es}</h1>
<p>{$config.object.description.es}</p>
<div class="actions">
<button on:click={handleUndo}>
⬅️ Deshacer
</button>
<button on:click={handleReset}>
🔄 Resetear
</button>
<button disabled={!isValid}>
✅ Confirmar configuración
</button>
</div>
</header>
<div class="configurator-content">
<!-- Section Navigation -->
<nav class="section-nav">
<h2>Secciones</h2>
<ul>
{#each Object.values($config.object.sections) as section}
<li>
<button
class:active={section.id === currentSection?.id}
on:click={() => handleSectionChange(section.id)}
>
{section.icon || '📦'} {section.name.es}
</button>
</li>
{/each}
</ul>
</nav>
<!-- Attributes Panel -->
<div class="attributes-panel">
{#if currentSection}
<h2>{currentSection.name.es}</h2>
<p>{currentSection.description.es}</p>
<!-- Atributos de la sección -->
{#each currentSection.attrs as attr}
{#if attr.type === 'dynamic' && attr.display?.uiVisible}
<div class="attribute">
<label for={attr.id}>
{attr.name.es}
{#if config.isRequired(attr.id)}
<span class="required">*</span>
{/if}
</label>
<select
id={attr.id}
value={selection[attr.id] || ''}
on:change={(e) => handleOptionSelect(attr.id, e.target.value)}
class:error={config.hasErrorsForAttribute(attr.id)}
>
<option value="">-- Seleccionar --</option>
{#each attr.options as opt}
{@const option = catalog.options[opt.optionId]}
{#if option}
<option value={option.id}>
{option.name.es}
{#if option.pricing?.baseAmount}
(+{option.pricing.baseAmount}€)
{/if}
</option>
{/if}
<header class="configurator-header">
<h1>{t(object.name)}</h1>
<p>{t(object.description)}</p>
<div class="actions">
<button onclick={() => config.undo()}>⬅️ Deshacer</button>
<button onclick={() => config.reset()}>🔄 Resetear</button>
<button disabled={!isValid}>✅ Confirmar</button>
</div>
</header>
<div class="configurator-content">
<!-- Section Nav -->
<nav class="section-nav">
<h2>Secciones</h2>
<ul>
{#each Object.values(object.sections) as section}
<li>
<button
class:active={section.id === currentSection?.id}
onclick={() => handleSectionChange(section.id)}
>
{section.icon || '📦'} {t(section.name)}
</button>
</li>
{/each}
</select>
</ul>
</nav>
<!-- Attributes -->
<div class="attributes-panel">
{#if currentSection}
<h2>{t(currentSection.name)}</h2>
<p>{t(currentSection.description)}</p>
{#each currentSection.attrs as attr}
{@const selectedValue = selection[attr.id] || ''}
{#if attr.type === 'dynamic' && attr.display?.uiVisible}
<div class="attribute">
<label for={attr.id}>
{t(attr.name)}
{#if config.isRequired(attr.id)}
<span class="required">*</span>
{/if}
</label>
<select
id={attr.id}
onchange={(e) => handleOptionSelect(attr.id, e.currentTarget.value)}
class:error={config.hasErrorsForAttribute(attr.id)}
>
<option value="" selected={!selectedValue}></option>
{#each attr.options as opt}
{@const option = catalog.options[opt.optionId]}
{#if option}
<option value={option.id} selected={option.id === selectedValue}>
{t(option.name)}
{#if option.pricing?.baseAmount}
(+{option.pricing.baseAmount}€)
{/if}
</option>
{/if}
{/each}
</select>
{#each config.getViolationsForAttribute(attr.id) as violation}
<div class="violation {violation.severity}">
{t(violation.rule.message)}
</div>
{/each}
</div>
{/if}
{/each}
{/if}
</div>
<!-- Violaciones -->
{#each config.getViolationsForAttribute(attr.id) as violation}
<div class="violation {violation.severity}">
{violation.rule.message.es}
<!-- Visual -->
<div class="visual-panel">
{#if currentView}
<div class="view-selector">
<h3>Vistas</h3>
{#each Object.values(currentSection?.views || {}) as view}
<button
class:active={view.id === currentView.id}
onclick={() => handleViewChange(view.id)}
>
{view.icon || '📷'} {t(view.name)}
</button>
{/each}
</div>
{/each}
</div>
{/if}
{/each}
{/if}
</div>
<!-- Visual Preview -->
<div class="visual-panel">
{#if currentView}
<div class="view-selector">
<h3>Vistas</h3>
{#each Object.values(currentSection.views || {}) as view}
<button
class:active={view.id === currentView.id}
on:click={() => handleViewChange(view.id)}
>
{view.icon || '📷'} {view.name.es}
</button>
{/each}
</div>
<div class="preview">
<h3>{currentView.name.es}</h3>
<!-- Aquí iría el renderizado de la imagen -->
<div class="image-placeholder">
<p>Vista: {currentView.id}</p>
<p>Template: {currentView.visualConfig.imageUrlTemplate}</p>
</div>
</div>
{:else}
<div class="no-visual">
<p>Esta sección no tiene visualización</p>
<div class="preview">
<h3>{t(currentView.name)}</h3>
<div class="image-placeholder">
<p>Vista: {currentView.id}</p>
<p>Template: {currentView.visualConfig.imageUrlTemplate}</p>
</div>
</div>
{:else}
<div class="no-visual">
<p>Esta sección no tiene visualización</p>
</div>
{/if}
</div>
{/if}
</div>
</div>
<!-- Validation Summary -->
{#if validation}
<footer class="validation-summary">
{#if validation.hasErrors}
<div class="errors">
❌ {validation.violations.filter(v => v.severity === 'error').length} errores
</div>
{/if}
{#if validation.hasWarnings}
<div class="warnings">
⚠️ {validation.violations.filter(v => v.severity === 'warning').length} advertencias
</div>
{/if}
{#if validation.isValid}
<div class="success">
✅ Configuración válida
</div>
{/if}
</footer>
{/if}
{#if validation}
<footer class="validation-summary">
{#if validation.hasErrors}
<div class="errors">
❌ {validation.violations.filter(v => v.severity === 'error').length} errores
</div>
{/if}
{#if validation.hasWarnings}
<div class="warnings">
⚠️ {validation.violations.filter(v => v.severity === 'warning').length} advertencias
</div>
{/if}
{#if validation.isValid}
<div class="success">✅ Configuración válida</div>
{/if}
</footer>
{/if}
</div>
<style>
.configurator {
display: flex;
flex-direction: column;
height: 100vh;
font-family: system-ui, -apple-system, sans-serif;
}
.configurator-header {
padding: 1rem 2rem;
border-bottom: 1px solid #e0e0e0;
}
.actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.configurator-content {
display: grid;
grid-template-columns: 200px 1fr 1fr;
flex: 1;
overflow: hidden;
}
.section-nav {
border-right: 1px solid #e0e0e0;
padding: 1rem;
overflow-y: auto;
}
.section-nav ul {
list-style: none;
padding: 0;
}
.section-nav button {
width: 100%;
text-align: left;
padding: 0.75rem;
border: none;
background: transparent;
cursor: pointer;
}
.section-nav button:hover {
background: #f5f5f5;
}
.section-nav button.active {
background: #007bff;
color: white;
}
.attributes-panel {
padding: 2rem;
overflow-y: auto;
}
.attribute {
margin-bottom: 1.5rem;
}
.attribute label {
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
}
.required {
color: red;
}
.attribute select {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.attribute select.error {
border-color: red;
}
.violation {
margin-top: 0.5rem;
padding: 0.5rem;
border-radius: 4px;
font-size: 0.875rem;
}
.violation.error {
background: #fee;
color: #c00;
}
.violation.warning {
background: #ffc;
color: #840;
}
.visual-panel {
padding: 2rem;
background: #f9f9f9;
overflow-y: auto;
}
.view-selector {
margin-bottom: 1rem;
}
.view-selector button {
margin-right: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid #ccc;
background: white;
cursor: pointer;
}
.view-selector button.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.image-placeholder {
background: #ddd;
padding: 2rem;
text-align: center;
border-radius: 8px;
}
.validation-summary {
display: flex;
gap: 1rem;
padding: 1rem 2rem;
border-top: 1px solid #e0e0e0;
}
.errors {
color: #c00;
font-weight: 600;
}
.warnings {
color: #840;
font-weight: 600;
}
.success {
color: #080;
font-weight: 600;
}
.configurator {
display: flex;
flex-direction: column;
height: 100vh;
font-family: system-ui, -apple-system, sans-serif;
}
.configurator-header {
padding: 1rem 2rem;
border-bottom: 1px solid #e0e0e0;
}
.actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.configurator-content {
display: grid;
grid-template-columns: 200px 1fr 1fr;
flex: 1;
overflow: hidden;
}
.section-nav {
border-right: 1px solid #e0e0e0;
padding: 1rem;
overflow-y: auto;
}
.section-nav ul {
list-style: none;
padding: 0;
}
.section-nav button {
width: 100%;
text-align: left;
padding: 0.75rem;
border: none;
background: transparent;
cursor: pointer;
}
.section-nav button:hover {
background: #f5f5f5;
}
.section-nav button.active {
background: #007bff;
color: white;
}
.attributes-panel {
padding: 2rem;
overflow-y: auto;
}
.attribute {
margin-bottom: 1.5rem;
}
.attribute label {
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
}
.required {
color: red;
}
.attribute select {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.attribute select.error {
border-color: red;
}
.violation {
margin-top: 0.5rem;
padding: 0.5rem;
border-radius: 4px;
font-size: 0.875rem;
}
.violation.error {
background: #fee;
color: #c00;
}
.violation.warning {
background: #ffc;
color: #840;
}
.visual-panel {
padding: 2rem;
background: #f9f9f9;
overflow-y: auto;
}
.view-selector {
margin-bottom: 1rem;
}
.view-selector button {
margin-right: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid #ccc;
background: white;
cursor: pointer;
}
.view-selector button.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.image-placeholder {
background: #ddd;
padding: 2rem;
text-align: center;
border-radius: 8px;
}
.validation-summary {
display: flex;
gap: 1rem;
padding: 1rem 2rem;
border-top: 1px solid #e0e0e0;
}
.errors {
color: #c00;
font-weight: 600;
}
.warnings {
color: #840;
font-weight: 600;
}
.success {
color: #080;
font-weight: 600;
}
</style>

@ -0,0 +1,190 @@
/**
* ============================================================================
* SVELTE 5 CONFIGURATION STORE
* ============================================================================
*
* Adapter que expone ConfigurationState usando Svelte 5 runes.
*/
import type {
ConfigurableObject,
ConfigurationCatalog,
AttrID,
OptionID,
SectionID,
ViewID,
Section,
SectionView
} from '@/types';
import {
ConfigurationState,
type ConfigurationStateOptions
} from '@/state/ConfigurationState';
import type { ValidationResult } from '@/state/ValidationState';
import type { SelectionStateData } from '@/state/SelectionState';
// ============================================================================
// STORE STATE INTERFACE
// ============================================================================
export interface ConfigurationStoreState {
catalog : ConfigurationCatalog;
object : ConfigurableObject;
selection : SelectionStateData;
validation : ValidationResult | null;
currentSection : Section | null;
currentSectionId: SectionID | null;
currentView : SectionView | null;
currentViewId : ViewID | null;
isValid : boolean;
locale : string; // Idioma actual
}
// ============================================================================
// CONFIGURATION STORE (SVELTE 5 RUNES)
// ============================================================================
/**
* Crea un store de configuración reactivo para Svelte 5
*/
export function createConfigurationStore(options: ConfigurationStateOptions) {
// Estado core (framework agnostic)
const coreState = new ConfigurationState(options);
// Función helper para obtener el estado actual
function getStoreState(): ConfigurationStoreState {
return {
catalog : coreState.getCatalog(),
object : coreState.getObject(),
selection : coreState.getAllSelections(),
validation : coreState.getValidationResult(),
currentSection : coreState.getCurrentSection(),
currentSectionId: coreState.getCurrentSectionId(),
currentView : coreState.getCurrentView(),
currentViewId : coreState.getCurrentViewId(),
isValid : coreState.isValid(),
locale : coreState.getLocale()
};
}
// ✅ SVELTE 5: Usar $state() para reactividad
let state = $state<ConfigurationStoreState>(getStoreState());
// Suscribirse a cambios del estado core
coreState.subscribe(() => {
state = getStoreState();
});
// ========================================================================
// PUBLIC API
// ========================================================================
return {
// ✅ SVELTE 5: Getter reactivo
get current() {
return state;
},
// Selection actions
selectOption(attrId: AttrID, optionId: OptionID) {
coreState.selectOption(attrId, optionId);
},
clearSelection(attrId: AttrID) {
coreState.clearSelection(attrId);
},
// Navigation actions
setCurrentSection(sectionId: SectionID) {
coreState.setCurrentSection(sectionId);
},
setCurrentView(viewId: ViewID) {
coreState.setCurrentView(viewId);
},
// Locale actions
getLocale() {
return coreState.getLocale();
},
setLocale(locale: string) {
coreState.setLocale(locale as any);
},
// Translation helper
t(i18nString: any) {
const locale = coreState.getLocale();
if (typeof i18nString === 'string') return i18nString;
return i18nString[locale] || i18nString.es || i18nString.en || '';
},
// Validation actions
validate() {
return coreState.validate();
},
// Helpers
getSelection(attrId: AttrID) {
return coreState.getSelection(attrId);
},
getAllowedValues(attrId: AttrID) {
return coreState.getAllowedValues(attrId);
},
isRequired(attrId: AttrID) {
return coreState.isRequired(attrId);
},
hasErrorsForAttribute(attrId: AttrID) {
return coreState.hasErrorsForAttribute(attrId);
},
getViolationsForAttribute(attrId: AttrID) {
return coreState.getViolationsForAttribute(attrId);
},
// Persistence
toJSON() {
return coreState.toJSON();
},
fromJSON(data: ReturnType<typeof coreState.toJSON>) {
coreState.fromJSON(data);
},
// Utilities
reset() {
coreState.reset();
},
undo() {
return coreState.undo();
},
// Direct access to state (for advanced use)
_coreState: coreState
};
}
// ============================================================================
// DERIVED VALUES (SVELTE 5 $DERIVED)
// ============================================================================
/**
* Helper para crear valores derivados en Svelte 5
*
* Uso en componentes:
* ```ts
* const config = createConfigurationStore({...});
* const currentSection = $derived(config.current.currentSection);
* const isValid = $derived(config.current.isValid);
* ```
*/
// ============================================================================
// TYPE EXPORTS
// ============================================================================
export type ConfigurationStore = ReturnType<typeof createConfigurationStore>;

@ -1,197 +0,0 @@
/**
* ============================================================================
* SVELTE CONFIGURATION STORE
* ============================================================================
*
* Adapter que expone ConfigurationState como Svelte store.
*/
import { writable, derived, type Readable } from 'svelte/store';
import type {
ConfigurableObject,
ConfigurationCatalog,
AttrID,
OptionID,
SectionID,
ViewID,
Section,
SectionView
} from '@/types';
import {
ConfigurationState,
type ConfigurationStateOptions
} from '@/state/ConfigurationState';
import type { ValidationResult } from '@/state/ValidationState';
import type { SelectionStateData } from '@/state/SelectionState';
// ============================================================================
// STORE STATE INTERFACE
// ============================================================================
export interface ConfigurationStoreState {
catalog : ConfigurationCatalog;
object : ConfigurableObject;
selection : SelectionStateData;
validation : ValidationResult | null;
currentSection : Section | null;
currentSectionId: SectionID | null;
currentView : SectionView | null;
currentViewId : ViewID | null;
isValid : boolean;
}
// ============================================================================
// CONFIGURATION STORE
// ============================================================================
/**
* Crea un store de configuración reactivo para Svelte
*/
export function createConfigurationStore(options: ConfigurationStateOptions) {
// Estado core (framework agnostic)
const state = new ConfigurationState(options);
// Store interno con versión para forzar actualizaciones
const { subscribe, set, update } = writable(getStoreState());
// Función helper para obtener el estado actual del store
function getStoreState(): ConfigurationStoreState {
return {
catalog : state.getCatalog(),
object : state.getObject(),
selection : state.getAllSelections(),
validation : state.getValidationResult(),
currentSection : state.getCurrentSection(),
currentSectionId: state.getCurrentSectionId(),
currentView : state.getCurrentView(),
currentViewId : state.getCurrentViewId(),
isValid : state.isValid()
};
}
// Suscribirse a cambios del estado core
state.subscribe(() => {
set(getStoreState());
});
// ========================================================================
// PUBLIC API
// ========================================================================
return {
subscribe,
// Selection actions
selectOption: (attrId: AttrID, optionId: OptionID) => {
state.selectOption(attrId, optionId);
},
clearSelection: (attrId: AttrID) => {
state.clearSelection(attrId);
},
// Navigation actions
setCurrentSection: (sectionId: SectionID) => {
state.setCurrentSection(sectionId);
},
setCurrentView: (viewId: ViewID) => {
state.setCurrentView(viewId);
},
// Validation actions
validate: () => {
return state.validate();
},
// Helpers
getSelection: (attrId: AttrID) => {
return state.getSelection(attrId);
},
getAllowedValues: (attrId: AttrID) => {
return state.getAllowedValues(attrId);
},
isRequired: (attrId: AttrID) => {
return state.isRequired(attrId);
},
hasErrorsForAttribute: (attrId: AttrID) => {
return state.hasErrorsForAttribute(attrId);
},
getViolationsForAttribute: (attrId: AttrID) => {
return state.getViolationsForAttribute(attrId);
},
// Persistence
toJSON: () => state.toJSON(),
fromJSON: (data: ReturnType<typeof state.toJSON>) => {
state.fromJSON(data);
},
// Utilities
reset: () => state.reset(),
undo: () => state.undo(),
// Direct access to state (for advanced use)
_state: state
};
}
// ============================================================================
// DERIVED STORES (COMPUTED VALUES)
// ============================================================================
/**
* Derived store para la sección actual
*/
export function deriveCurrentSection(
configStore: ReturnType<typeof createConfigurationStore>
): Readable<Section | null> {
return derived(configStore, $config => $config.currentSection);
}
/**
* Derived store para la vista actual
*/
export function deriveCurrentView(
configStore: ReturnType<typeof createConfigurationStore>
): Readable<SectionView | null> {
return derived(configStore, $config => $config.currentView);
}
/**
* Derived store para validación
*/
export function deriveValidation(
configStore: ReturnType<typeof createConfigurationStore>
): Readable<ValidationResult | null> {
return derived(configStore, $config => $config.validation);
}
/**
* Derived store para verificar si es válido
*/
export function deriveIsValid(
configStore: ReturnType<typeof createConfigurationStore>
): Readable<boolean> {
return derived(configStore, $config => $config.isValid);
}
/**
* Derived store para selección
*/
export function deriveSelection(
configStore: ReturnType<typeof createConfigurationStore>
): Readable<SelectionStateData> {
return derived(configStore, $config => $config.selection);
}
// ============================================================================
// TYPE EXPORTS
// ============================================================================
export type ConfigurationStore = ReturnType<typeof createConfigurationStore>;

@ -4,7 +4,7 @@
* ============================================================================
*/
import type {JsonLogic, ID, Value, AttrID} from '@/types';
import type {JsonLogic, Value, AttrID} from '@/types';
import type { JsonLogicEvaluator } from '@/engine';
import { extractIdPart } from '@/utils';

@ -7,9 +7,10 @@
* Soporta: static, dynamic, y API-generated
*/
import type {Section, AttrID, Value, APIMethod} from '@/types';
import {type BuildImageConfig, buildSectionImageUrl} from '@/utils';
import { logger , ERRORS, INFO, MESSAGE_CATEGORIES } from '@/messages';
import type {Section, AttrID, Value, ViewID, ViewVisualConfig, Attribute} from '@/types';
import { buildSectionImageUrl } from '@/utils';
import { logger } from '@/utils';
import { ERRORS, WARNINGS, INFO, MESSAGE_CATEGORIES } from '@/messages';
import {DEFAULTS} from "@/constants";
// ============================================================================
@ -18,17 +19,17 @@ import {DEFAULTS} from "@/constants";
export interface ImageGenerationOptions {
/** Timeout para API calls (default: 5000ms) */
timeout? : number;
timeout?: number;
/** Reintentos en caso de fallo (default: 0) */
retries? : number;
retries?: number;
/** Delay entre reintentos en ms (default: 1000ms) */
retryDelay? : number;
retryDelay?: number;
}
export interface ImageGenerationResult {
url : string;
source : 'static' | 'api' | 'fallback';
cached? : boolean;
url: string;
source: 'static' | 'api' | 'fallback';
cached?: boolean;
duration?: number;
}
@ -84,8 +85,9 @@ const imageCache = new ImageUrlCache();
* Genera URL de imagen para una sección
*
* @param section - Sección a renderizar
* @param viewId
* @param globalAttrs - Atributos globales
* @param sectionAttr - Atributos de la sección
* @param sectionAttrs - Atributos de la sección
* @param options - Opciones de generación
* @returns Promise con la URL generada
*
@ -100,17 +102,29 @@ const imageCache = new ImageUrlCache();
* });
*/
export async function generateImageUrl(
section : Section,
globalAttrs : Record<AttrID, Value>,
sectionAttr : Record<AttrID, Value>,
options? : ImageGenerationOptions
section : Section,
viewId : ViewID,
globalAttrs : Record<AttrID, Value>,
sectionAttrs : Record<AttrID, Value>,
options? : ImageGenerationOptions
): Promise<ImageGenerationResult> {
const startTime = performance.now();
const config = section.visualConfig;
const config = section?.views?.[viewId]?.visualConfig;
if (!config) {
//todo
/*
logger.error(
MESSAGE_CATEGORIES.API,
ERRORS.NULL_VIEW_VISUAL_CONFIG(section.id,viewId), // ✅ MENSAJE CENTRALIZADO
{ sectionId: section.id, viewId: viewId }
);
*/
return { url: DEFAULTS.FALLBACK_IMAGE, source: 'fallback' };
}
// Check cache
const cacheKey = generateCacheKey(section, globalAttrs, sectionAttr);
const cacheKey = generateCacheKey(section, viewId, globalAttrs, sectionAttrs);
const cached = imageCache.get(cacheKey);
if (cached) {
logger.debug(MESSAGE_CATEGORIES.CACHE, INFO.CACHE_HIT(cacheKey));
@ -131,8 +145,9 @@ export async function generateImageUrl(
case 'api_generated':
result = await generateViaAPI(
section,
viewId,
globalAttrs,
sectionAttr,
sectionAttrs,
options
);
break;
@ -142,8 +157,9 @@ export async function generateImageUrl(
default:
result = generateStaticUrl(
section,
viewId,
globalAttrs,
sectionAttr
sectionAttrs
);
break;
}
@ -166,10 +182,23 @@ export async function generateImageUrl(
*/
function generateStaticUrl(
section : Section,
viewId : ViewID,
globalAttrs : Record<AttrID, Value>,
sectionAttrs: Record<AttrID, Value>
): ImageGenerationResult {
const config = section.visualConfig;
const config = section?.views?.[viewId]?.visualConfig;
if (!config) {
//todo
/*
logger.error(
MESSAGE_CATEGORIES.API,
ERRORS.NULL_VIEW_VISUAL_CONFIG(section.id,viewId), // ✅ MENSAJE CENTRALIZADO
{ sectionId: section.id, viewId: viewId }
);
*/
return { url: DEFAULTS.FALLBACK_IMAGE, source: 'fallback' };
}
// Validar que existe template
if (!config.imageUrlTemplate) {
@ -180,7 +209,7 @@ function generateStaticUrl(
);
return {
url: config.fallbackImage || DEFAULTS.FALLBACK_IMAGE,
url: config.fallbackImage || '/images/fallback.jpg',
source: 'fallback'
};
}
@ -194,7 +223,7 @@ function generateStaticUrl(
fallbackImage : config.fallbackImage,
globalAttrDependencies : config.globalAttrDependencies,
sectionAttrDependencies : config.sectionAttrDependencies
} as BuildImageConfig,
},
globalAttrs,
sectionAttrs
);
@ -214,11 +243,24 @@ function generateStaticUrl(
*/
async function generateViaAPI(
section : Section,
viewId : ViewID,
globalAttrs : Record<AttrID, Value>,
sectionAttrs: Record<AttrID, Value>,
options? : ImageGenerationOptions
): Promise<ImageGenerationResult> {
const config = section.visualConfig;
const config = section?.views?.[viewId]?.visualConfig;
if (!config) {
//todo
/*
logger.error(
MESSAGE_CATEGORIES.API,
ERRORS.NULL_VIEW_VISUAL_CONFIG(section.id,viewId), // ✅ MENSAJE CENTRALIZADO
{ sectionId: section.id, viewId: viewId }
);
*/
return { url: DEFAULTS.FALLBACK_IMAGE, source: 'fallback' };
}
const fallback = config.fallbackImage || DEFAULTS.FALLBACK_IMAGE;
// Validar configuración API
@ -233,9 +275,10 @@ async function generateViaAPI(
}
// Preparar parámetros
const params: Record<string, Value> = {};
config.apiConfig.params?.forEach((attrId) => {
const value = globalAttrs[attrId as AttrID] ?? sectionAttrs[attrId as AttrID];
const params: Record<AttrID, Value> = {};
config.apiConfig.params?.forEach((id) => {
const attrId = id as AttrID;
const value = globalAttrs[attrId] ?? sectionAttrs[attrId];
if (value !== undefined) {
params[attrId] = value;
}
@ -322,20 +365,19 @@ async function generateViaAPI(
*/
async function fetchImageFromAPI(
endpoint: string,
method : APIMethod,
body : any,
timeout : number
method: 'GET' | 'POST',
body: any,
timeout: number
): Promise<string | null> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(endpoint, {
method,
headers : { 'Content-Type': 'application/json' },
body : JSON.stringify(body),
signal : controller.signal
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: controller.signal
});
clearTimeout(timeoutId);
@ -366,17 +408,28 @@ async function fetchImageFromAPI(
* Genera clave de cache única para una combinación de sección/atributos
*/
function generateCacheKey(
section : Section,
globalAttributes : Record<AttrID, Value>,
sectionAttributes: Record<AttrID, Value>
section : Section,
viewId : ViewID,
globalAttrs : Record<AttrID, Value>,
sectionAttrs: Record<AttrID, Value>
): string {
const config = section.visualConfig;
const config = section?.views?.[viewId]?.visualConfig;
if (!config) {
//todo
/*
logger.error(
MESSAGE_CATEGORIES.API,
ERRORS.NULL_VIEW_VISUAL_CONFIG(section.id,viewId), // ✅ MENSAJE CENTRALIZADO
{ sectionId: section.id, viewId: viewId }
);
*/
return '';
}
// Incluir solo atributos relevantes
const relevantGlobal = config.globalAttrDependencies?.reduce(
(acc, id) => {
acc[id] = globalAttributes[id];
(acc , id) => {
acc[id] = globalAttrs[id];
return acc;
},
{} as Record<AttrID, Value>
@ -384,17 +437,18 @@ function generateCacheKey(
const relevantSection = config.sectionAttrDependencies?.reduce(
(acc, id) => {
acc[id] = sectionAttributes[id];
acc[id] = sectionAttrs[id];
return acc;
},
{} as Record<AttrID, Value>
) || {};
return JSON.stringify({
sectionId: section.id,
template : config.imageUrlTemplate,
global : relevantGlobal,
section : relevantSection
sectionId : section.id,
viewId : viewId,
template : config.imageUrlTemplate,
global : relevantGlobal,
section : relevantSection
});
}
@ -418,11 +472,12 @@ export const generateSectionImageUrl = generateStaticUrl;
* Alias para compatibilidad
*/
export async function generateSectionImageAsync(
section: Section,
globalAttributes: Record<AttrID, Value>,
sectionAttributes: Record<AttrID, Value>
section : Section,
viewId : ViewID,
globalAttrs : Record<AttrID, Value>,
sectionAttrs: Record<AttrID, Value>
): Promise<string> {
const result = await generateImageUrl(section, globalAttributes, sectionAttributes);
const result = await generateImageUrl(section, viewId, globalAttrs, sectionAttrs);
return result.url;
}

@ -4,20 +4,33 @@
* ============================================================================
*/
import type { Section, AttrID , Value } from '@/types';
import type {Section, AttrID, Value, ViewID} from '@/types';
import { deepEqual } from '@/utils';
/**
* Determina si una sección debe re-renderizarse
*/
export function shouldRerenderSection(
section : Section,
viewId : ViewID,
changedAttrId : AttrID,
oldValue : Value,
newValue : Value,
isGlobalAttr : boolean
): boolean {
const config = section.visualConfig;
const config = section?.views?.[viewId]?.visualConfig;
if (!config) {
//todo
/*
logger.error(
MESSAGE_CATEGORIES.API,
ERRORS.NULL_VIEW_VISUAL_CONFIG(section.id,viewId), // ✅ MENSAJE CENTRALIZADO
{ sectionId: section.id, viewId: viewId }
);
*/
return false;
}
// Verificar si el atributo afecta a esta sección
const affectsThisSection = isGlobalAttr

@ -19,3 +19,4 @@ export * from './utils';
// Engine (Business Logic)
export * from './engine';

@ -0,0 +1,9 @@
import type {I18nString, ObjectID} from "@/types";
export const ERRORS_CONFIG = {
OBJECT_NOT_FOUND: (objectId: ObjectID): I18nString => ({
es: `Objeto '${objectId}' no encontrado en el catálogo`,
en: `Object '${objectId}' not found in catalog`
})
} as const;

@ -4,7 +4,9 @@
export * from './messages';
export * from './errors';
export * from './warnings';
export * from './logger';
export * from '@/utils/logger/logger.ts';
export * from './info';
export * from './validation';
export * from './validation-section';
export * from './warnings-config';
export * from './errors-config';

@ -0,0 +1,24 @@
/**
* ============================================================================
* WARNINGS - Configuration State
* ============================================================================
*/
import type { I18nString, AttrID, OptionID, SectionID, ViewID } from '@/types';
export const WARNINGS_CONFIG = {
OPTION_NOT_ALLOWED: (optionId: OptionID, attrId: AttrID): I18nString => ({
es: `La opción '${optionId}' no está permitida para el atributo '${attrId}'`,
en: `Option '${optionId}' is not allowed for attribute '${attrId}'`
}),
SECTION_NOT_FOUND: (sectionId: SectionID): I18nString => ({
es: `Sección '${sectionId}' no encontrada`,
en: `Section '${sectionId}' not found`
}),
VIEW_NOT_FOUND: (viewId: ViewID): I18nString => ({
es: `Vista '${viewId}' no encontrada en la sección actual`,
en: `View '${viewId}' not found in current section`
})
} as const;

@ -13,15 +13,21 @@ import type {
AttrID,
OptionID,
SectionID,
ViewID, ObjectID
ViewID,
ObjectID,
SupportedLocale
} from '@/types';
import { SelectionState, type SelectionStateChangeEvent } from './SelectionState';
import { SelectionState } from './SelectionState';
import { ValidationState, type ValidationResult } from './ValidationState';
import { LocaleState } from './LocaleState';
import { WARNINGS_CONFIG, ERRORS_CONFIG } from '@/messages';
import { translate } from '@/utils/i18n/translation.utils';
export interface ConfigurationStateOptions {
catalog: ConfigurationCatalog;
objectId: string;
autoValidate?: boolean; // Auto-validar en cada cambio (default: true)
catalog : ConfigurationCatalog;
objectId : ObjectID; // ✅ ObjectID en vez de string
autoValidate? : boolean; // Auto-validar en cada cambio (default: true)
initialLocale? : SupportedLocale; // Idioma inicial (default: 'es')
}
/**
@ -29,34 +35,40 @@ export interface ConfigurationStateOptions {
*/
export class ConfigurationState {
// Core state
private catalog: ConfigurationCatalog;
private object: ConfigurableObject;
private selection: SelectionState;
private validation: ValidationState;
private catalog : ConfigurationCatalog;
private object : ConfigurableObject;
private selection : SelectionState;
private validation : ValidationState;
private locale : LocaleState;
// Options
private autoValidate: boolean;
// Current UI state
private currentSectionId: SectionID | null = null;
private currentViewId: ViewID | null = null;
private currentViewId : ViewID | null = null;
// Listeners
private stateChangeListeners: Set<() => void> = new Set();
constructor(options: ConfigurationStateOptions) {
this.catalog = options.catalog;
this.autoValidate = options.autoValidate ?? true;
this.catalog = options.catalog;
this.autoValidate = options.autoValidate ?? true;
// Inicializar locale primero (se usa en mensajes de error)
const initialLocale = options.initialLocale || this.catalog.defaultLocale;
this.locale = new LocaleState(initialLocale as SupportedLocale);
// Buscar el objeto
const obj = this.catalog.objects[options.objectId as ObjectID];
const obj = this.catalog.objects[options.objectId];
if (!obj) {
throw new Error(`Object ${options.objectId} not found in catalog`);
const error = ERRORS_CONFIG.OBJECT_NOT_FOUND(options.objectId);
throw new Error(translate(error, this.locale.get()));
}
this.object = obj;
// Inicializar estados
this.selection = new SelectionState();
this.selection = new SelectionState();
this.validation = new ValidationState();
// Configurar reglas de validación
@ -65,7 +77,7 @@ export class ConfigurationState {
}
// Suscribirse a cambios de selección para auto-validar
this.selection.subscribe((event) => {
this.selection.subscribe(() => {
if (this.autoValidate) {
this.validation.validate(this.selection.getAll());
}
@ -77,6 +89,11 @@ export class ConfigurationState {
this.notifyStateChange();
});
// Suscribirse a cambios de idioma
this.locale.subscribe(() => {
this.notifyStateChange();
});
// Inicializar con valores por defecto
this.initializeDefaults();
}
@ -107,7 +124,7 @@ export class ConfigurationState {
// Establecer primera sección como actual
const firstSectionId = this.object.sectionOrder?.[0] ||
Object.keys(this.object.sections)[0] as SectionID;
Object.keys(this.object.sections)[0] as SectionID;
if (firstSectionId) {
this.setCurrentSection(firstSectionId);
@ -142,7 +159,8 @@ export class ConfigurationState {
selectOption(attrId: AttrID, optionId: OptionID): void {
// Verificar si el valor está permitido
if (!this.validation.isValueAllowed(attrId, optionId, this.selection.getAll())) {
console.warn(`Option ${optionId} is not allowed for attribute ${attrId}`);
const warning = WARNINGS_CONFIG.OPTION_NOT_ALLOWED(optionId, attrId);
console.warn(translate(warning, this.locale.get()));
return;
}
@ -224,6 +242,24 @@ export class ConfigurationState {
return this.validation.isRequired(attrId, this.selection.getAll());
}
// ========================================================================
// LOCALE
// ========================================================================
/**
* Obtiene el idioma actual
*/
getLocale(): SupportedLocale {
return this.locale.get();
}
/**
* Establece el idioma actual
*/
setLocale(locale: SupportedLocale): void {
this.locale.set(locale);
}
// ========================================================================
// NAVIGATION
// ========================================================================
@ -234,7 +270,8 @@ export class ConfigurationState {
setCurrentSection(sectionId: SectionID): void {
const section = this.object.sections[sectionId];
if (!section) {
console.warn(`Section ${sectionId} not found`);
const warning = WARNINGS_CONFIG.SECTION_NOT_FOUND(sectionId);
console.warn(translate(warning, this.locale.get()));
return;
}
@ -265,7 +302,8 @@ export class ConfigurationState {
setCurrentView(viewId: ViewID): void {
const section = this.getCurrentSection();
if (!section?.views?.[viewId]) {
console.warn(`View ${viewId} not found in current section`);
const warning = WARNINGS_CONFIG.VIEW_NOT_FOUND(viewId);
console.warn(translate(warning, this.locale.get()));
return;
}

@ -0,0 +1,70 @@
// LocaleState.ts
/**
* ============================================================================
* LOCALE STATE - Framework Agnostic
* ============================================================================
*
* Gestiona el idioma actual de la aplicación.
*/
import type { SupportedLocale } from '@/types';
export interface LocaleStateChangeEvent {
previousLocale : SupportedLocale;
newLocale : SupportedLocale;
timestamp : number;
}
/**
* Estado del idioma actual
*/
export class LocaleState {
private currentLocale : SupportedLocale;
private listeners : Set<(event: LocaleStateChangeEvent) => void> = new Set();
constructor(initialLocale: SupportedLocale = 'es') {
this.currentLocale = initialLocale;
}
/**
* Obtiene el idioma actual
*/
get(): SupportedLocale {
return this.currentLocale;
}
/**
* Establece el idioma actual
*/
set(locale: SupportedLocale): void {
if (this.currentLocale === locale) return;
const event: LocaleStateChangeEvent = {
previousLocale : this.currentLocale,
newLocale : locale,
timestamp : Date.now()
};
this.currentLocale = locale;
this.notifyListeners(event);
}
/**
* Suscribe un listener a cambios de idioma
*/
subscribe(listener: (event: LocaleStateChangeEvent) => void): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
/**
* Notifica a todos los listeners
*/
private notifyListeners(event: LocaleStateChangeEvent): void {
this.listeners.forEach(listener => listener(event));
}
}

@ -6,32 +6,32 @@
* Gestiona el estado de validación y violaciones.
*/
import type { AttrID, ValidationRule, RuleID } from '@/types';
import type {AttrID, ValidationRule, RuleID, OptionID} from '@/types';
import type { SelectionStateData } from './SelectionState';
import { RuleEngine } from '@/engine/rule-engine';
export interface Violation {
rule: ValidationRule;
rule : ValidationRule;
severity: 'error' | 'warning' | 'info';
attrId?: AttrID;
attrId? : AttrID;
}
export interface ValidationResult {
isValid: boolean;
hasErrors: boolean;
hasWarnings: boolean;
violations: Violation[];
violationsByAttribute: Map<AttrID, Violation[]>;
isValid : boolean;
hasErrors : boolean;
hasWarnings : boolean;
violations : Violation[];
violationsByAttr: Map<AttrID, Violation[]>;
}
/**
* Estado de validación
*/
export class ValidationState {
private engine: RuleEngine;
private rules: ValidationRule[] = [];
private lastResult: ValidationResult | null = null;
private listeners: Set<(result: ValidationResult) => void> = new Set();
private engine : RuleEngine;
private rules : ValidationRule[] = [];
private lastResult : ValidationResult | null = null;
private listeners : Set<(result: ValidationResult) => void> = new Set();
constructor() {
this.engine = new RuleEngine();
@ -71,7 +71,7 @@ export class ValidationState {
hasErrors,
hasWarnings,
violations,
violationsByAttribute
violationsByAttr: violationsByAttribute
};
this.lastResult = result;
@ -92,7 +92,7 @@ export class ValidationState {
*/
getViolationsForAttribute(attrId: AttrID): Violation[] {
if (!this.lastResult) return [];
return this.lastResult.violationsByAttribute.get(attrId) || [];
return this.lastResult.violationsByAttr.get(attrId) || [];
}
/**

@ -9,64 +9,60 @@
*/
import type { I18nString } from '../i18n';
import type { APIConfig, Metadata } from '../core';
import type { Metadata } from '../core';
import type {
AttrID,
SectionID,
ViewID,
Attribute,
JsonLogic
JsonLogic, ViewType, ViewVisualConfig
} from '@/types';
import type {Hotspot} from "@/types/view/hotspot.types.ts";
// ============================================================================
// VIEW VISUAL CONFIG
// ============================================================================
export interface ViewVisualConfig {
/**
* Template para URL de imagen
* Placeholders: {basePath}, {section}, {view}, {at_<attrId>}
*/
imageUrlTemplate?: string;
/** Ruta base (puede heredarse del objeto) */
basePath?: string;
/** Atributos GLOBALES que afectan esta vista */
globalAttrDependencies?: AttrID[];
/** Atributos DE SECCIÓN que afectan esta vista */
sectionAttrDependencies?: AttrID[];
/** Config API (puede heredarse del objeto) */
apiConfig?: APIConfig;
/** Config 3D */
threeDConfig?: {
modelUrl: string;
textureAttrs: Array<{
attrId: AttrID;
scope: 'global' | 'section';
}>;
};
/** Imagen de fallback */
fallbackImage?: string;
}
// ============================================================================
// SECTION VIEW
// ============================================================================
/**
* Vista visual de una sección
*/
export interface SectionView {
/** ID único de la vista */
id: ViewID;
/** Nombre de la vista */
name: I18nString;
/** Descripción */
description?: I18nString;
/** Tipo de vista */
type: ViewType;
/** Configuración visual de esta vista */
visualConfig: ViewVisualConfig;
/** Hotspots interactivos (OPCIONAL) */
hotspots?: Hotspot[];
/** Orden de esta vista en la navegación */
order?: number;
/** Si es la vista por defecto para esta sección */
isDefault?: boolean;
/** Icono para el selector de vistas */
icon?: string;
/** Si está visible (puede ser condicional) */
visible?: boolean | JsonLogic;
}
// ============================================================================
// SECTION AVAILABILITY
// ============================================================================
@ -105,9 +101,9 @@ export interface SectionAvailability {
* debe estar en la sección que se visualiza.
*/
export interface Section {
id: SectionID;
name: I18nString;
description: I18nString;
id : SectionID;
name : I18nString;
description : I18nString;
/** Atributos de esta sección */
attrs: Attribute[];

@ -0,0 +1,49 @@
// ============================================================================
// HOTSPOT
// ============================================================================
import type {AttrID, HotspotID, I18nString, JsonLogic} from "@/types";
/**
* Punto interactivo en una vista
*/
export interface Hotspot {
/** ID único del hotspot */
id: HotspotID;
/** Nombre descriptivo */
name: I18nString;
/** Posición X en porcentaje (0-100) */
x: number;
/** Posición Y en porcentaje (0-100) */
y: number;
/** Radio del hotspot en píxeles */
radius?: number;
/** Color del hotspot */
color?: string;
/** Icono del hotspot */
icon?: string;
/** Tipo de scope al que referencia */
scopeType: 'global' | 'section';
/** ID del atributo que este hotspot controla */
targetAttributeId: AttrID;
/** Si es visible (puede ser condicional) */
visible?: boolean | JsonLogic;
/** Tooltip al hacer hover */
tooltip?: I18nString;
/** Orden z-index para superposición */
zIndex?: number;
}

@ -4,8 +4,8 @@
* ============================================================================
*/
import type { I18nString } from '../i18n';
import type { ViewID, HotspotID, AttrID , RenderingStrategy, APIConfig, JsonLogic} from "@/types";
import type { AttrID , RenderingStrategy, APIConfig } from "@/types";
// ============================================================================
// VIEW TYPES
@ -25,129 +25,41 @@ export type ViewType =
| 'blueprint' // Plano/blueprint
| 'custom'; // Tipo personalizado
// ============================================================================
// HOTSPOT
// ============================================================================
/**
* Punto interactivo en una vista
*/
export interface Hotspot {
/** ID único del hotspot */
id: HotspotID;
/** Nombre descriptivo */
name: I18nString;
/** Posición X en porcentaje (0-100) */
x: number;
/** Posición Y en porcentaje (0-100) */
y: number;
/** Radio del hotspot en píxeles */
radius?: number;
/** Color del hotspot */
color?: string;
/** Icono del hotspot */
icon?: string;
/** Tipo de scope al que referencia */
scopeType: 'global' | 'section';
/** ID del atributo que este hotspot controla */
targetAttributeId: AttrID;
/** Si es visible (puede ser condicional) */
visible?: boolean | JsonLogic;
/** Tooltip al hacer hover */
tooltip?: I18nString;
/** Orden z-index para superposición */
zIndex?: number;
}
// ============================================================================
// VIEW VISUAL CONFIG
// ============================================================================
/**
* Configuración visual específica de una vista
*/
export interface ViewVisualConfig {
/** Estrategia de renderizado */
strategy: RenderingStrategy;
/** Template para URL de imagen */
/**
* Template para URL de imagen
* Placeholders: {basePath}, {section}, {view}, {at_<attrId>}
*/
imageUrlTemplate?: string;
/** Ruta base de imágenes */
/** Ruta base (puede heredarse del objeto) */
basePath?: string;
/** Atributos globales que afectan a esta vista */
/** Atributos GLOBALES que afectan esta vista */
globalAttrDependencies?: AttrID[];
/** Atributos de sección que afectan a esta vista */
/** Atributos DE SECCIÓN que afectan esta vista */
sectionAttrDependencies?: AttrID[];
/** Imagen de fallback */
fallbackImage?: string;
/** Config para API */
apiConfig?: APIConfig ;
/** Config API (puede heredarse del objeto) */
apiConfig?: APIConfig;
/** Config 3D */
threeDConfig?: {
modelUrl : string;
cameraPosition? : {
x: number;
y: number;
z: number
};
textureAttributes? : Array<{
attributeId: AttrID;
modelUrl: string;
textureAttrs: Array<{
attrId: AttrID;
scope: 'global' | 'section';
}>;
};
}
// ============================================================================
// SECTION VIEW
// ============================================================================
/**
* Vista visual de una sección
*/
export interface SectionView {
/** ID único de la vista */
id: ViewID;
/** Nombre de la vista */
name: I18nString;
/** Descripción */
description?: I18nString;
/** Tipo de vista */
type: ViewType;
/** Configuración visual de esta vista */
visualConfig: ViewVisualConfig;
/** Hotspots interactivos (OPCIONAL) */
hotspots?: Hotspot[];
/** Orden de esta vista en la navegación */
order?: number;
/** Si es la vista por defecto para esta sección */
isDefault?: boolean;
/** Icono para el selector de vistas */
icon?: string;
/** Si está visible (puede ser condicional) */
visible?: boolean | JsonLogic;
/** Imagen de fallback */
fallbackImage?: string;
}

@ -21,3 +21,7 @@ export * from './validation';
// Equality Utilities
export * from './equality';
// Export logger
export * from './logger';

@ -0,0 +1,4 @@
export * from './logger'

@ -6,7 +6,7 @@
import type { I18nString, SupportedLocale } from '@/types';
import { translate } from '@/utils';
import type { MessageCategory } from './messages';
import type { MessageCategory } from '@/messages';
// ============================================================================
// LOG LEVELS

@ -6,6 +6,7 @@
"paths": {
"@/types/*" : ["types/*"],
"@/utils/*" : ["utils/*"],
"@/utils/*" : ["utils/*"],
"@/engine/*" : ["engine/*"],
"@/constants/*" : ["constants/*"],
"@/adapters/*" : ["adapters/*"],

@ -1,7 +1,9 @@
import { defineConfig } from 'vitest/config';
import { resolve } from 'path';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte()],
test: {
globals: true,
environment: 'node',

Loading…
Cancel
Save

Powered by TurnKey Linux.