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 @@
- -
-

{$config.object.name.es}

-

{$config.object.description.es}

- -
- - - -
-
- -
- - - - -
- {#if currentSection} -

{currentSection.name.es}

-

{currentSection.description.es}

- - - {#each currentSection.attrs as attr} - {#if attr.type === 'dynamic' && attr.display?.uiVisible} -
- - - + + + + +
+ {#if currentSection} +

{t(currentSection.name)}

+

{t(currentSection.description)}

+ + {#each currentSection.attrs as attr} + {@const selectedValue = selection[attr.id] || ''} + {#if attr.type === 'dynamic' && attr.display?.uiVisible} +
+ + + + + {#each config.getViolationsForAttribute(attr.id) as violation} +
+ {t(violation.rule.message)} +
+ {/each} +
+ {/if} + {/each} + {/if} +
- - {#each config.getViolationsForAttribute(attr.id) as violation} -
- {violation.rule.message.es} + +
+ {#if currentView} +
+

Vistas

+ {#each Object.values(currentSection?.views || {}) as view} + + {/each}
- {/each} -
- {/if} - {/each} - {/if} -
- - -
- {#if currentView} -
-

Vistas

- {#each Object.values(currentSection.views || {}) as view} - - {/each} -
-
-

{currentView.name.es}

- -
-

Vista: {currentView.id}

-

Template: {currentView.visualConfig.imageUrlTemplate}

-
-
- {:else} -
-

Esta sección no tiene visualización

+
+

{t(currentView.name)}

+
+

Vista: {currentView.id}

+

Template: {currentView.visualConfig.imageUrlTemplate}

+
+
+ {:else} +
+

Esta sección no tiene visualización

+
+ {/if}
- {/if}
-
- - - {#if validation} -
- {#if validation.hasErrors} -
- ❌ {validation.violations.filter(v => v.severity === 'error').length} errores -
- {/if} - {#if validation.hasWarnings} -
- ⚠️ {validation.violations.filter(v => v.severity === 'warning').length} advertencias -
- {/if} - {#if validation.isValid} -
- ✅ Configuración válida -
- {/if} -
- {/if} + + {#if validation} +
+ {#if validation.hasErrors} +
+ ❌ {validation.violations.filter(v => v.severity === 'error').length} errores +
+ {/if} + {#if validation.hasWarnings} +
+ ⚠️ {validation.violations.filter(v => v.severity === 'warning').length} advertencias +
+ {/if} + {#if validation.isValid} +
✅ Configuración válida
+ {/if} +
+ {/if}
+ .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',