diff --git a/package-lock.json b/package-lock.json
index 0a96bed..d744097 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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"
}
}
}
diff --git a/package.json b/package.json
index a9a2d67..574b5ee 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/readme/ARCHITECTURE.md b/readme/ARCHITECTURE.md
index 0572531..b617d38 100644
--- a/readme/ARCHITECTURE.md
+++ b/readme/ARCHITECTURE.md
@@ -322,4 +322,4 @@ Cambias el estado → UI se actualiza automáticamente.
- `/state/ConfigurationState.ts` - Orquestador principal
- `/adapters/svelte/stores/configuration.ts` - Svelte store
- `/adapters/svelte/components/Configurator.svelte` - Componente ejemplo
-- `/docs/ARCHITECTURE.md` - Este archivo
+- `/docs/ARCHITECTURE.md` - Este archivo
\ No newline at end of file
diff --git a/src/adapters/svelte/components/Configurator.svelte b/src/adapters/svelte/components/Configurator.svelte
index e94c651..c544e88 100644
--- a/src/adapters/svelte/components/Configurator.svelte
+++ b/src/adapters/svelte/components/Configurator.svelte
@@ -1,346 +1,327 @@
-
-
-
-
-
-
-
-
-
- {#if currentSection}
-
{currentSection.name.es}
-
{currentSection.description.es}
-
-
- {#each currentSection.attrs as attr}
- {#if attr.type === 'dynamic' && attr.display?.uiVisible}
-
-
-
-
+ .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;
+ }
+
\ No newline at end of file
diff --git a/src/adapters/svelte/stores/configuration.svelte.ts b/src/adapters/svelte/stores/configuration.svelte.ts
new file mode 100644
index 0000000..ccfa4aa
--- /dev/null
+++ b/src/adapters/svelte/stores/configuration.svelte.ts
@@ -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
(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) {
+ 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;
\ No newline at end of file
diff --git a/src/adapters/svelte/stores/configuration.ts b/src/adapters/svelte/stores/configuration.ts
deleted file mode 100644
index 5bb0dfe..0000000
--- a/src/adapters/svelte/stores/configuration.ts
+++ /dev/null
@@ -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) => {
- 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
-): Readable {
- return derived(configStore, $config => $config.currentSection);
-}
-
-/**
- * Derived store para la vista actual
- */
-export function deriveCurrentView(
- configStore: ReturnType
-): Readable {
- return derived(configStore, $config => $config.currentView);
-}
-
-/**
- * Derived store para validación
- */
-export function deriveValidation(
- configStore: ReturnType
-): Readable {
- return derived(configStore, $config => $config.validation);
-}
-
-/**
- * Derived store para verificar si es válido
- */
-export function deriveIsValid(
- configStore: ReturnType
-): Readable {
- return derived(configStore, $config => $config.isValid);
-}
-
-/**
- * Derived store para selección
- */
-export function deriveSelection(
- configStore: ReturnType
-): Readable {
- return derived(configStore, $config => $config.selection);
-}
-
-// ============================================================================
-// TYPE EXPORTS
-// ============================================================================
-
-export type ConfigurationStore = ReturnType;
diff --git a/src/engine/visual-config/display-evaluator.ts b/src/engine/visual-config/display-evaluator.ts
index 5ab63e9..e1daea0 100644
--- a/src/engine/visual-config/display-evaluator.ts
+++ b/src/engine/visual-config/display-evaluator.ts
@@ -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';
diff --git a/src/engine/visual-config/image-generator.ts b/src/engine/visual-config/image-generator.ts
index 3205a03..ce71c03 100644
--- a/src/engine/visual-config/image-generator.ts
+++ b/src/engine/visual-config/image-generator.ts
@@ -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,
- sectionAttr : Record,
- options? : ImageGenerationOptions
+ section : Section,
+ viewId : ViewID,
+ globalAttrs : Record,
+ sectionAttrs : Record,
+ options? : ImageGenerationOptions
): Promise {
-
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,
sectionAttrs: Record
): 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,
sectionAttrs: Record,
options? : ImageGenerationOptions
): Promise {
- 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 = {};
- config.apiConfig.params?.forEach((attrId) => {
- const value = globalAttrs[attrId as AttrID] ?? sectionAttrs[attrId as AttrID];
+ const params: Record = {};
+ 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 {
-
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,
- sectionAttributes: Record
+ section : Section,
+ viewId : ViewID,
+ globalAttrs : Record,
+ sectionAttrs: Record
): 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
@@ -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
) || {};
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,
- sectionAttributes: Record
+ section : Section,
+ viewId : ViewID,
+ globalAttrs : Record,
+ sectionAttrs: Record
): Promise {
- const result = await generateImageUrl(section, globalAttributes, sectionAttributes);
+ const result = await generateImageUrl(section, viewId, globalAttrs, sectionAttrs);
return result.url;
}
@@ -445,4 +500,4 @@ export default {
generateSectionImageUrl,
generateSectionImageAsync,
clearImageCache
-};
\ No newline at end of file
+};
diff --git a/src/engine/visual-config/rerender-detector.ts b/src/engine/visual-config/rerender-detector.ts
index c2044f3..3a2ba58 100644
--- a/src/engine/visual-config/rerender-detector.ts
+++ b/src/engine/visual-config/rerender-detector.ts
@@ -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
diff --git a/src/index.ts b/src/index.ts
index 1d69be3..3bea148 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -19,3 +19,4 @@ export * from './utils';
// Engine (Business Logic)
export * from './engine';
+
diff --git a/src/messages/errors-config.ts b/src/messages/errors-config.ts
new file mode 100644
index 0000000..2585af0
--- /dev/null
+++ b/src/messages/errors-config.ts
@@ -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;
\ No newline at end of file
diff --git a/src/messages/index.ts b/src/messages/index.ts
index 76268ae..18a14a4 100644
--- a/src/messages/index.ts
+++ b/src/messages/index.ts
@@ -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';
diff --git a/src/messages/warnings-config.ts b/src/messages/warnings-config.ts
new file mode 100644
index 0000000..688bca5
--- /dev/null
+++ b/src/messages/warnings-config.ts
@@ -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;
\ No newline at end of file
diff --git a/src/state/ConfigurationState.ts b/src/state/ConfigurationState.ts
index f7c10d0..51ce0bc 100644
--- a/src/state/ConfigurationState.ts
+++ b/src/state/ConfigurationState.ts
@@ -2,7 +2,7 @@
* ============================================================================
* CONFIGURATION STATE - Framework Agnostic
* ============================================================================
- *
+ *
* Orquesta todo el estado de la configuración.
* Coordina SelectionState, ValidationState, y el modelo del catálogo.
*/
@@ -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();
}
@@ -106,9 +123,9 @@ export class ConfigurationState {
});
// Establecer primera sección como actual
- const firstSectionId = this.object.sectionOrder?.[0] ||
- Object.keys(this.object.sections)[0] as SectionID;
-
+ const firstSectionId = this.object.sectionOrder?.[0] ||
+ 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,12 +270,13 @@ 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;
}
this.currentSectionId = sectionId;
-
+
// Establecer vista por defecto si tiene vistas
if (section.views && section.defaultView) {
this.currentViewId = section.defaultView;
@@ -254,7 +291,7 @@ export class ConfigurationState {
* Obtiene la sección actual
*/
getCurrentSection() {
- return this.currentSectionId
+ return this.currentSectionId
? this.object.sections[this.currentSectionId]
: null;
}
@@ -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;
}
@@ -306,7 +344,7 @@ export class ConfigurationState {
*/
subscribe(listener: () => void): () => void {
this.stateChangeListeners.add(listener);
-
+
return () => {
this.stateChangeListeners.delete(listener);
};
@@ -340,11 +378,11 @@ export class ConfigurationState {
*/
fromJSON(data: ReturnType): void {
this.selection.fromJSON(data.selection);
-
+
if (data.currentSectionId) {
this.setCurrentSection(data.currentSectionId);
}
-
+
if (data.currentViewId) {
this.setCurrentView(data.currentViewId);
}
@@ -374,4 +412,4 @@ export class ConfigurationState {
undo(): boolean {
return this.selection.undo();
}
-}
+}
\ No newline at end of file
diff --git a/src/state/LocaleState.ts b/src/state/LocaleState.ts
new file mode 100644
index 0000000..3b4ad42
--- /dev/null
+++ b/src/state/LocaleState.ts
@@ -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));
+ }
+}
\ No newline at end of file
diff --git a/src/state/ValidationState.ts b/src/state/ValidationState.ts
index dfb9bc6..36fa63c 100644
--- a/src/state/ValidationState.ts
+++ b/src/state/ValidationState.ts
@@ -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;
+ isValid : boolean;
+ hasErrors : boolean;
+ hasWarnings : boolean;
+ violations : Violation[];
+ violationsByAttr: Map;
}
/**
* 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) || [];
}
/**
diff --git a/src/types/model/section.types.ts b/src/types/model/section.types.ts
index 2986f28..be710a6 100644
--- a/src/types/model/section.types.ts
+++ b/src/types/model/section.types.ts
@@ -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_}
- */
- 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[];
diff --git a/src/types/view/hotspot.types.ts b/src/types/view/hotspot.types.ts
new file mode 100644
index 0000000..3e918a7
--- /dev/null
+++ b/src/types/view/hotspot.types.ts
@@ -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;
+}
+
+
diff --git a/src/types/view/view.types.ts b/src/types/view/view.types.ts
index a211581..c07016b 100644
--- a/src/types/view/view.types.ts
+++ b/src/types/view/view.types.ts
@@ -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_}
+ */
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;
+}
\ No newline at end of file
diff --git a/src/utils/index.ts b/src/utils/index.ts
index b53aa84..14e17a9 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -21,3 +21,7 @@ export * from './validation';
// Equality Utilities
export * from './equality';
+
+
+// Export logger
+export * from './logger';
\ No newline at end of file
diff --git a/src/utils/logger/index.ts b/src/utils/logger/index.ts
new file mode 100644
index 0000000..0924379
--- /dev/null
+++ b/src/utils/logger/index.ts
@@ -0,0 +1,4 @@
+
+
+export * from './logger'
+
diff --git a/src/messages/logger.ts b/src/utils/logger/logger.ts
similarity index 99%
rename from src/messages/logger.ts
rename to src/utils/logger/logger.ts
index a6e9c61..c3b8fa6 100644
--- a/src/messages/logger.ts
+++ b/src/utils/logger/logger.ts
@@ -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
diff --git a/src/utils/validation/section.validator.ts b/src/utils/validation/section-validator.ts
similarity index 100%
rename from src/utils/validation/section.validator.ts
rename to src/utils/validation/section-validator.ts
diff --git a/tsconfig.json b/tsconfig.json
index ac53799..8f693d7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,6 +6,7 @@
"paths": {
"@/types/*" : ["types/*"],
"@/utils/*" : ["utils/*"],
+ "@/utils/*" : ["utils/*"],
"@/engine/*" : ["engine/*"],
"@/constants/*" : ["constants/*"],
"@/adapters/*" : ["adapters/*"],
diff --git a/vitest.config.ts b/vitest.config.ts
index 6f732ae..e14eda7 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -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',