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", "name": "visual-configuration-engine",
"version": "2.0.0", "version": "2.0.0",
"license": "EULA", "license": "EULA",
"dependencies": {
"svelte": "^5.53.0"
},
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@types/node": "^25.2.3", "@types/node": "^25.2.3",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.18" "vitest": "^4.0.18"
} }
}, },
@ -470,11 +475,50 @@
"node": ">=18" "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": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@ -484,7 +528,6 @@
"version": "1.5.5", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
@ -855,6 +898,54 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@tsconfig/node10": {
"version": "1.0.12", "version": "1.0.12",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
@ -905,7 +996,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
@ -918,6 +1008,12 @@
"undici-types": "~7.16.0" "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": { "node_modules/@vitest/expect": {
"version": "4.0.18", "version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
@ -1033,7 +1129,6 @@
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@ -1062,6 +1157,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/assertion-error": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@ -1072,6 +1176,15 @@
"node": ">=12" "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": { "node_modules/chai": {
"version": "6.2.2", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@ -1082,6 +1195,15 @@
"node": ">=18" "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": { "node_modules/create-require": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@ -1089,6 +1211,22 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/diff": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
@ -1148,6 +1286,21 @@
"@esbuild/win32-x64": "0.27.3" "@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": { "node_modules/estree-walker": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "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": "^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": { "node_modules/magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
@ -1380,6 +1547,33 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/tinybench": {
"version": "2.9.0", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "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": { "node_modules/vitest": {
"version": "4.0.18", "version": "4.0.18",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
@ -1675,6 +1889,12 @@
"engines": { "engines": {
"node": ">=6" "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", "author": "ACTIVE THING",
"license": "EULA", "license": "EULA",
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@types/node": "^25.2.3", "@types/node": "^25.2.3",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.18" "vitest": "^4.0.18"
},
"dependencies": {
"svelte": "^5.53.0"
} }
} }

@ -1,104 +1,91 @@
<!-- <!--
============================================================================ ============================================================================
CONFIGURATOR - Example Svelte Component CONFIGURATOR - Svelte 5 Component
============================================================================ ============================================================================
Ejemplo de cómo usar el ConfigurationStore en un componente Svelte.
--> -->
<script lang="ts"> <script lang="ts">
import { createConfigurationStore } from '@/svelte/stores'; import { createConfigurationStore } from '@/svelte/stores/configuration.svelte';
import type { ConfigurationCatalog } from '@/types'; import type {AttrID, ConfigurationCatalog, ObjectID, OptionID, SectionID, ViewID} from '@/types';
// Props let { catalog, objectId }: {
export let catalog: ConfigurationCatalog; catalog : ConfigurationCatalog;
export let objectId: string; objectId: ObjectID;
} = $props();
// Crear el store
const config = createConfigurationStore({ const config = createConfigurationStore({
catalog , catalog ,
objectId, objectId,
autoValidate: true autoValidate: true
}); });
// Acceso reactivo al estado // Valores reactivos
$: currentSection = $config.currentSection; const currentSection = $derived(config.current.currentSection);
$: currentView = $config.currentView; const currentView = $derived(config.current.currentView);
$: selection = $config.selection; const selection = $derived(config.current.selection);
$: validation = $config.validation; const validation = $derived(config.current.validation);
$: isValid = $config.isValid; const isValid = $derived(config.current.isValid);
const object = $derived(config.current.object);
// Helper de traducción
const t = (i18nString: any) => config.t(i18nString);
// Handlers // Handlers con tipos correctos
function handleOptionSelect(attrId: string, optionId: string) { function handleOptionSelect(attrId: AttrID, optionId: string) {
config.selectOption(attrId, optionId); config.selectOption(attrId, optionId as OptionID);
} }
function handleSectionChange(sectionId: string) { function handleSectionChange(sectionId: SectionID) {
config.setCurrentSection(sectionId); config.setCurrentSection(sectionId);
} }
function handleViewChange(viewId: string) { function handleViewChange(viewId: ViewID) {
config.setCurrentView(viewId); config.setCurrentView(viewId);
} }
function handleReset() {
config.reset();
}
function handleUndo() {
config.undo();
}
</script> </script>
<div class="configurator"> <div class="configurator">
<!-- Header -->
<header class="configurator-header"> <header class="configurator-header">
<h1>{$config.object.name.es}</h1> <h1>{t(object.name)}</h1>
<p>{$config.object.description.es}</p> <p>{t(object.description)}</p>
<div class="actions"> <div class="actions">
<button on:click={handleUndo}> <button onclick={() => config.undo()}>⬅️ Deshacer</button>
⬅️ Deshacer <button onclick={() => config.reset()}>🔄 Resetear</button>
</button> <button disabled={!isValid}>✅ Confirmar</button>
<button on:click={handleReset}>
🔄 Resetear
</button>
<button disabled={!isValid}>
✅ Confirmar configuración
</button>
</div> </div>
</header> </header>
<div class="configurator-content"> <div class="configurator-content">
<!-- Section Navigation --> <!-- Section Nav -->
<nav class="section-nav"> <nav class="section-nav">
<h2>Secciones</h2> <h2>Secciones</h2>
<ul> <ul>
{#each Object.values($config.object.sections) as section} {#each Object.values(object.sections) as section}
<li> <li>
<button <button
class:active={section.id === currentSection?.id} class:active={section.id === currentSection?.id}
on:click={() => handleSectionChange(section.id)} onclick={() => handleSectionChange(section.id)}
> >
{section.icon || '📦'} {section.name.es} {section.icon || '📦'} {t(section.name)}
</button> </button>
</li> </li>
{/each} {/each}
</ul> </ul>
</nav> </nav>
<!-- Attributes Panel --> <!-- Attributes -->
<div class="attributes-panel"> <div class="attributes-panel">
{#if currentSection} {#if currentSection}
<h2>{currentSection.name.es}</h2> <h2>{t(currentSection.name)}</h2>
<p>{currentSection.description.es}</p> <p>{t(currentSection.description)}</p>
<!-- Atributos de la sección -->
{#each currentSection.attrs as attr} {#each currentSection.attrs as attr}
{@const selectedValue = selection[attr.id] || ''}
{#if attr.type === 'dynamic' && attr.display?.uiVisible} {#if attr.type === 'dynamic' && attr.display?.uiVisible}
<div class="attribute"> <div class="attribute">
<label for={attr.id}> <label for={attr.id}>
{attr.name.es} {t(attr.name)}
{#if config.isRequired(attr.id)} {#if config.isRequired(attr.id)}
<span class="required">*</span> <span class="required">*</span>
{/if} {/if}
@ -106,16 +93,15 @@
<select <select
id={attr.id} id={attr.id}
value={selection[attr.id] || ''} onchange={(e) => handleOptionSelect(attr.id, e.currentTarget.value)}
on:change={(e) => handleOptionSelect(attr.id, e.target.value)}
class:error={config.hasErrorsForAttribute(attr.id)} class:error={config.hasErrorsForAttribute(attr.id)}
> >
<option value="">-- Seleccionar --</option> <option value="" selected={!selectedValue}></option>
{#each attr.options as opt} {#each attr.options as opt}
{@const option = catalog.options[opt.optionId]} {@const option = catalog.options[opt.optionId]}
{#if option} {#if option}
<option value={option.id}> <option value={option.id} selected={option.id === selectedValue}>
{option.name.es} {t(option.name)}
{#if option.pricing?.baseAmount} {#if option.pricing?.baseAmount}
(+{option.pricing.baseAmount}€) (+{option.pricing.baseAmount}€)
{/if} {/if}
@ -124,10 +110,9 @@
{/each} {/each}
</select> </select>
<!-- Violaciones -->
{#each config.getViolationsForAttribute(attr.id) as violation} {#each config.getViolationsForAttribute(attr.id) as violation}
<div class="violation {violation.severity}"> <div class="violation {violation.severity}">
{violation.rule.message.es} {t(violation.rule.message)}
</div> </div>
{/each} {/each}
</div> </div>
@ -136,24 +121,23 @@
{/if} {/if}
</div> </div>
<!-- Visual Preview --> <!-- Visual -->
<div class="visual-panel"> <div class="visual-panel">
{#if currentView} {#if currentView}
<div class="view-selector"> <div class="view-selector">
<h3>Vistas</h3> <h3>Vistas</h3>
{#each Object.values(currentSection.views || {}) as view} {#each Object.values(currentSection?.views || {}) as view}
<button <button
class:active={view.id === currentView.id} class:active={view.id === currentView.id}
on:click={() => handleViewChange(view.id)} onclick={() => handleViewChange(view.id)}
> >
{view.icon || '📷'} {view.name.es} {view.icon || '📷'} {t(view.name)}
</button> </button>
{/each} {/each}
</div> </div>
<div class="preview"> <div class="preview">
<h3>{currentView.name.es}</h3> <h3>{t(currentView.name)}</h3>
<!-- Aquí iría el renderizado de la imagen -->
<div class="image-placeholder"> <div class="image-placeholder">
<p>Vista: {currentView.id}</p> <p>Vista: {currentView.id}</p>
<p>Template: {currentView.visualConfig.imageUrlTemplate}</p> <p>Template: {currentView.visualConfig.imageUrlTemplate}</p>
@ -167,7 +151,6 @@
</div> </div>
</div> </div>
<!-- Validation Summary -->
{#if validation} {#if validation}
<footer class="validation-summary"> <footer class="validation-summary">
{#if validation.hasErrors} {#if validation.hasErrors}
@ -181,9 +164,7 @@
</div> </div>
{/if} {/if}
{#if validation.isValid} {#if validation.isValid}
<div class="success"> <div class="success">✅ Configuración válida</div>
✅ Configuración válida
</div>
{/if} {/if}
</footer> </footer>
{/if} {/if}

@ -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 type { JsonLogicEvaluator } from '@/engine';
import { extractIdPart } from '@/utils'; import { extractIdPart } from '@/utils';

@ -7,9 +7,10 @@
* Soporta: static, dynamic, y API-generated * Soporta: static, dynamic, y API-generated
*/ */
import type {Section, AttrID, Value, APIMethod} from '@/types'; import type {Section, AttrID, Value, ViewID, ViewVisualConfig, Attribute} from '@/types';
import {type BuildImageConfig, buildSectionImageUrl} from '@/utils'; import { buildSectionImageUrl } from '@/utils';
import { logger , ERRORS, INFO, MESSAGE_CATEGORIES } from '@/messages'; import { logger } from '@/utils';
import { ERRORS, WARNINGS, INFO, MESSAGE_CATEGORIES } from '@/messages';
import {DEFAULTS} from "@/constants"; import {DEFAULTS} from "@/constants";
// ============================================================================ // ============================================================================
@ -84,8 +85,9 @@ const imageCache = new ImageUrlCache();
* Genera URL de imagen para una sección * Genera URL de imagen para una sección
* *
* @param section - Sección a renderizar * @param section - Sección a renderizar
* @param viewId
* @param globalAttrs - Atributos globales * @param globalAttrs - Atributos globales
* @param sectionAttr - Atributos de la sección * @param sectionAttrs - Atributos de la sección
* @param options - Opciones de generación * @param options - Opciones de generación
* @returns Promise con la URL generada * @returns Promise con la URL generada
* *
@ -101,16 +103,28 @@ const imageCache = new ImageUrlCache();
*/ */
export async function generateImageUrl( export async function generateImageUrl(
section : Section, section : Section,
viewId : ViewID,
globalAttrs : Record<AttrID, Value>, globalAttrs : Record<AttrID, Value>,
sectionAttr : Record<AttrID, Value>, sectionAttrs : Record<AttrID, Value>,
options? : ImageGenerationOptions options? : ImageGenerationOptions
): Promise<ImageGenerationResult> { ): Promise<ImageGenerationResult> {
const startTime = performance.now(); 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 // Check cache
const cacheKey = generateCacheKey(section, globalAttrs, sectionAttr); const cacheKey = generateCacheKey(section, viewId, globalAttrs, sectionAttrs);
const cached = imageCache.get(cacheKey); const cached = imageCache.get(cacheKey);
if (cached) { if (cached) {
logger.debug(MESSAGE_CATEGORIES.CACHE, INFO.CACHE_HIT(cacheKey)); logger.debug(MESSAGE_CATEGORIES.CACHE, INFO.CACHE_HIT(cacheKey));
@ -131,8 +145,9 @@ export async function generateImageUrl(
case 'api_generated': case 'api_generated':
result = await generateViaAPI( result = await generateViaAPI(
section, section,
viewId,
globalAttrs, globalAttrs,
sectionAttr, sectionAttrs,
options options
); );
break; break;
@ -142,8 +157,9 @@ export async function generateImageUrl(
default: default:
result = generateStaticUrl( result = generateStaticUrl(
section, section,
viewId,
globalAttrs, globalAttrs,
sectionAttr sectionAttrs
); );
break; break;
} }
@ -166,10 +182,23 @@ export async function generateImageUrl(
*/ */
function generateStaticUrl( function generateStaticUrl(
section : Section, section : Section,
viewId : ViewID,
globalAttrs : Record<AttrID, Value>, globalAttrs : Record<AttrID, Value>,
sectionAttrs: Record<AttrID, Value> sectionAttrs: Record<AttrID, Value>
): ImageGenerationResult { ): 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 // Validar que existe template
if (!config.imageUrlTemplate) { if (!config.imageUrlTemplate) {
@ -180,7 +209,7 @@ function generateStaticUrl(
); );
return { return {
url: config.fallbackImage || DEFAULTS.FALLBACK_IMAGE, url: config.fallbackImage || '/images/fallback.jpg',
source: 'fallback' source: 'fallback'
}; };
} }
@ -194,7 +223,7 @@ function generateStaticUrl(
fallbackImage : config.fallbackImage, fallbackImage : config.fallbackImage,
globalAttrDependencies : config.globalAttrDependencies, globalAttrDependencies : config.globalAttrDependencies,
sectionAttrDependencies : config.sectionAttrDependencies sectionAttrDependencies : config.sectionAttrDependencies
} as BuildImageConfig, },
globalAttrs, globalAttrs,
sectionAttrs sectionAttrs
); );
@ -214,11 +243,24 @@ function generateStaticUrl(
*/ */
async function generateViaAPI( async function generateViaAPI(
section : Section, section : Section,
viewId : ViewID,
globalAttrs : Record<AttrID, Value>, globalAttrs : Record<AttrID, Value>,
sectionAttrs: Record<AttrID, Value>, sectionAttrs: Record<AttrID, Value>,
options? : ImageGenerationOptions options? : ImageGenerationOptions
): Promise<ImageGenerationResult> { ): 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; const fallback = config.fallbackImage || DEFAULTS.FALLBACK_IMAGE;
// Validar configuración API // Validar configuración API
@ -233,9 +275,10 @@ async function generateViaAPI(
} }
// Preparar parámetros // Preparar parámetros
const params: Record<string, Value> = {}; const params: Record<AttrID, Value> = {};
config.apiConfig.params?.forEach((attrId) => { config.apiConfig.params?.forEach((id) => {
const value = globalAttrs[attrId as AttrID] ?? sectionAttrs[attrId as AttrID]; const attrId = id as AttrID;
const value = globalAttrs[attrId] ?? sectionAttrs[attrId];
if (value !== undefined) { if (value !== undefined) {
params[attrId] = value; params[attrId] = value;
} }
@ -322,11 +365,10 @@ async function generateViaAPI(
*/ */
async function fetchImageFromAPI( async function fetchImageFromAPI(
endpoint: string, endpoint: string,
method : APIMethod, method: 'GET' | 'POST',
body: any, body: any,
timeout: number timeout: number
): Promise<string | null> { ): Promise<string | null> {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout); const timeoutId = setTimeout(() => controller.abort(), timeout);
@ -367,16 +409,27 @@ async function fetchImageFromAPI(
*/ */
function generateCacheKey( function generateCacheKey(
section : Section, section : Section,
globalAttributes : Record<AttrID, Value>, viewId : ViewID,
sectionAttributes: Record<AttrID, Value> globalAttrs : Record<AttrID, Value>,
sectionAttrs: Record<AttrID, Value>
): string { ): string {
const config = section?.views?.[viewId]?.visualConfig;
const config = section.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 // Incluir solo atributos relevantes
const relevantGlobal = config.globalAttrDependencies?.reduce( const relevantGlobal = config.globalAttrDependencies?.reduce(
(acc , id) => { (acc , id) => {
acc[id] = globalAttributes[id]; acc[id] = globalAttrs[id];
return acc; return acc;
}, },
{} as Record<AttrID, Value> {} as Record<AttrID, Value>
@ -384,7 +437,7 @@ function generateCacheKey(
const relevantSection = config.sectionAttrDependencies?.reduce( const relevantSection = config.sectionAttrDependencies?.reduce(
(acc, id) => { (acc, id) => {
acc[id] = sectionAttributes[id]; acc[id] = sectionAttrs[id];
return acc; return acc;
}, },
{} as Record<AttrID, Value> {} as Record<AttrID, Value>
@ -392,6 +445,7 @@ function generateCacheKey(
return JSON.stringify({ return JSON.stringify({
sectionId : section.id, sectionId : section.id,
viewId : viewId,
template : config.imageUrlTemplate, template : config.imageUrlTemplate,
global : relevantGlobal, global : relevantGlobal,
section : relevantSection section : relevantSection
@ -419,10 +473,11 @@ export const generateSectionImageUrl = generateStaticUrl;
*/ */
export async function generateSectionImageAsync( export async function generateSectionImageAsync(
section : Section, section : Section,
globalAttributes: Record<AttrID, Value>, viewId : ViewID,
sectionAttributes: Record<AttrID, Value> globalAttrs : Record<AttrID, Value>,
sectionAttrs: Record<AttrID, Value>
): Promise<string> { ): Promise<string> {
const result = await generateImageUrl(section, globalAttributes, sectionAttributes); const result = await generateImageUrl(section, viewId, globalAttrs, sectionAttrs);
return result.url; 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'; import { deepEqual } from '@/utils';
/** /**
* Determina si una sección debe re-renderizarse * Determina si una sección debe re-renderizarse
*/ */
export function shouldRerenderSection( export function shouldRerenderSection(
section : Section, section : Section,
viewId : ViewID,
changedAttrId : AttrID, changedAttrId : AttrID,
oldValue : Value, oldValue : Value,
newValue : Value, newValue : Value,
isGlobalAttr : boolean isGlobalAttr : boolean
): 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 // Verificar si el atributo afecta a esta sección
const affectsThisSection = isGlobalAttr const affectsThisSection = isGlobalAttr

@ -19,3 +19,4 @@ export * from './utils';
// Engine (Business Logic) // Engine (Business Logic)
export * from './engine'; 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 './messages';
export * from './errors'; export * from './errors';
export * from './warnings'; export * from './warnings';
export * from './logger'; export * from '@/utils/logger/logger.ts';
export * from './info'; export * from './info';
export * from './validation'; 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, AttrID,
OptionID, OptionID,
SectionID, SectionID,
ViewID, ObjectID ViewID,
ObjectID,
SupportedLocale
} from '@/types'; } from '@/types';
import { SelectionState, type SelectionStateChangeEvent } from './SelectionState'; import { SelectionState } from './SelectionState';
import { ValidationState, type ValidationResult } from './ValidationState'; 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 { export interface ConfigurationStateOptions {
catalog : ConfigurationCatalog; catalog : ConfigurationCatalog;
objectId: string; objectId : ObjectID; // ✅ ObjectID en vez de string
autoValidate? : boolean; // Auto-validar en cada cambio (default: true) autoValidate? : boolean; // Auto-validar en cada cambio (default: true)
initialLocale? : SupportedLocale; // Idioma inicial (default: 'es')
} }
/** /**
@ -33,6 +39,7 @@ export class ConfigurationState {
private object : ConfigurableObject; private object : ConfigurableObject;
private selection : SelectionState; private selection : SelectionState;
private validation : ValidationState; private validation : ValidationState;
private locale : LocaleState;
// Options // Options
private autoValidate: boolean; private autoValidate: boolean;
@ -48,10 +55,15 @@ export class ConfigurationState {
this.catalog = options.catalog; this.catalog = options.catalog;
this.autoValidate = options.autoValidate ?? true; 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 // Buscar el objeto
const obj = this.catalog.objects[options.objectId as ObjectID]; const obj = this.catalog.objects[options.objectId];
if (!obj) { 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; this.object = obj;
@ -65,7 +77,7 @@ export class ConfigurationState {
} }
// Suscribirse a cambios de selección para auto-validar // Suscribirse a cambios de selección para auto-validar
this.selection.subscribe((event) => { this.selection.subscribe(() => {
if (this.autoValidate) { if (this.autoValidate) {
this.validation.validate(this.selection.getAll()); this.validation.validate(this.selection.getAll());
} }
@ -77,6 +89,11 @@ export class ConfigurationState {
this.notifyStateChange(); this.notifyStateChange();
}); });
// Suscribirse a cambios de idioma
this.locale.subscribe(() => {
this.notifyStateChange();
});
// Inicializar con valores por defecto // Inicializar con valores por defecto
this.initializeDefaults(); this.initializeDefaults();
} }
@ -142,7 +159,8 @@ export class ConfigurationState {
selectOption(attrId: AttrID, optionId: OptionID): void { selectOption(attrId: AttrID, optionId: OptionID): void {
// Verificar si el valor está permitido // Verificar si el valor está permitido
if (!this.validation.isValueAllowed(attrId, optionId, this.selection.getAll())) { 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; return;
} }
@ -224,6 +242,24 @@ export class ConfigurationState {
return this.validation.isRequired(attrId, this.selection.getAll()); 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 // NAVIGATION
// ======================================================================== // ========================================================================
@ -234,7 +270,8 @@ export class ConfigurationState {
setCurrentSection(sectionId: SectionID): void { setCurrentSection(sectionId: SectionID): void {
const section = this.object.sections[sectionId]; const section = this.object.sections[sectionId];
if (!section) { if (!section) {
console.warn(`Section ${sectionId} not found`); const warning = WARNINGS_CONFIG.SECTION_NOT_FOUND(sectionId);
console.warn(translate(warning, this.locale.get()));
return; return;
} }
@ -265,7 +302,8 @@ export class ConfigurationState {
setCurrentView(viewId: ViewID): void { setCurrentView(viewId: ViewID): void {
const section = this.getCurrentSection(); const section = this.getCurrentSection();
if (!section?.views?.[viewId]) { 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; 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,7 +6,7 @@
* Gestiona el estado de validación y violaciones. * 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 type { SelectionStateData } from './SelectionState';
import { RuleEngine } from '@/engine/rule-engine'; import { RuleEngine } from '@/engine/rule-engine';
@ -21,7 +21,7 @@ export interface ValidationResult {
hasErrors : boolean; hasErrors : boolean;
hasWarnings : boolean; hasWarnings : boolean;
violations : Violation[]; violations : Violation[];
violationsByAttribute: Map<AttrID, Violation[]>; violationsByAttr: Map<AttrID, Violation[]>;
} }
/** /**
@ -71,7 +71,7 @@ export class ValidationState {
hasErrors, hasErrors,
hasWarnings, hasWarnings,
violations, violations,
violationsByAttribute violationsByAttr: violationsByAttribute
}; };
this.lastResult = result; this.lastResult = result;
@ -92,7 +92,7 @@ export class ValidationState {
*/ */
getViolationsForAttribute(attrId: AttrID): Violation[] { getViolationsForAttribute(attrId: AttrID): Violation[] {
if (!this.lastResult) return []; 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 { I18nString } from '../i18n';
import type { APIConfig, Metadata } from '../core'; import type { Metadata } from '../core';
import type { import type {
AttrID, AttrID,
SectionID, SectionID,
ViewID, ViewID,
Attribute, Attribute,
JsonLogic JsonLogic, ViewType, ViewVisualConfig
} from '@/types'; } 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 // SECTION VIEW
// ============================================================================ // ============================================================================
/**
* Vista visual de una sección
*/
export interface SectionView { export interface SectionView {
/** ID único de la vista */
id: ViewID; id: ViewID;
/** Nombre de la vista */
name: I18nString; name: I18nString;
/** Descripción */
description?: I18nString; description?: I18nString;
/** Tipo de vista */
type: ViewType;
/** Configuración visual de esta vista */
visualConfig: ViewVisualConfig; visualConfig: ViewVisualConfig;
/** Hotspots interactivos (OPCIONAL) */
hotspots?: Hotspot[];
/** Orden de esta vista en la navegación */
order?: number; order?: number;
/** Si es la vista por defecto para esta sección */
isDefault?: boolean;
/** Icono para el selector de vistas */
icon?: string; icon?: string;
/** Si está visible (puede ser condicional) */
visible?: boolean | JsonLogic;
} }
// ============================================================================ // ============================================================================
// SECTION AVAILABILITY // SECTION AVAILABILITY
// ============================================================================ // ============================================================================

@ -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 { AttrID , RenderingStrategy, APIConfig } from "@/types";
import type { ViewID, HotspotID, AttrID , RenderingStrategy, APIConfig, JsonLogic} from "@/types";
// ============================================================================ // ============================================================================
// VIEW TYPES // VIEW TYPES
@ -25,129 +25,41 @@ export type ViewType =
| 'blueprint' // Plano/blueprint | 'blueprint' // Plano/blueprint
| 'custom'; // Tipo personalizado | '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 // VIEW VISUAL CONFIG
// ============================================================================ // ============================================================================
/**
* Configuración visual específica de una vista
*/
export interface ViewVisualConfig { export interface ViewVisualConfig {
/** Estrategia de renderizado */ /** Estrategia de renderizado */
strategy: RenderingStrategy; strategy: RenderingStrategy;
/**
/** Template para URL de imagen */ * Template para URL de imagen
* Placeholders: {basePath}, {section}, {view}, {at_<attrId>}
*/
imageUrlTemplate?: string; imageUrlTemplate?: string;
/** Ruta base de imágenes */ /** Ruta base (puede heredarse del objeto) */
basePath?: string; basePath?: string;
/** Atributos globales que afectan a esta vista */ /** Atributos GLOBALES que afectan esta vista */
globalAttrDependencies?: AttrID[]; globalAttrDependencies?: AttrID[];
/** Atributos de sección que afectan a esta vista */ /** Atributos DE SECCIÓN que afectan esta vista */
sectionAttrDependencies?: AttrID[]; sectionAttrDependencies?: AttrID[];
/** Imagen de fallback */ /** Config API (puede heredarse del objeto) */
fallbackImage?: string;
/** Config para API */
apiConfig?: APIConfig; apiConfig?: APIConfig;
/** Config 3D */ /** Config 3D */
threeDConfig?: { threeDConfig?: {
modelUrl: string; modelUrl: string;
cameraPosition? : { textureAttrs: Array<{
x: number; attrId: AttrID;
y: number;
z: number
};
textureAttributes? : Array<{
attributeId: AttrID;
scope: 'global' | 'section'; 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 */ /** Imagen de fallback */
name: I18nString; fallbackImage?: string;
/** 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;
} }

@ -21,3 +21,7 @@ export * from './validation';
// Equality Utilities // Equality Utilities
export * from './equality'; 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 type { I18nString, SupportedLocale } from '@/types';
import { translate } from '@/utils'; import { translate } from '@/utils';
import type { MessageCategory } from './messages'; import type { MessageCategory } from '@/messages';
// ============================================================================ // ============================================================================
// LOG LEVELS // LOG LEVELS

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

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

Loading…
Cancel
Save

Powered by TurnKey Linux.